#include "extract-word.h"
#include "machine-util.h"
#include "parse-argument.h"
+#include "parse-util.h"
+#include "storage-util.h"
#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
static const char *const image_format_table[_IMAGE_FORMAT_MAX] = {
[IMAGE_FORMAT_RAW] = "raw",
DEFINE_STRING_TABLE_LOOKUP(image_format, ImageFormat);
+static const char *const read_only_mode_table[_READ_ONLY_MAX] = {
+ [READ_ONLY_NO] = "no",
+ [READ_ONLY_YES] = "yes",
+ [READ_ONLY_AUTO] = "auto",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(read_only_mode, ReadOnlyMode);
+
static const char *const disk_type_table[_DISK_TYPE_MAX] = {
[DISK_TYPE_VIRTIO_BLK] = "virtio-blk",
[DISK_TYPE_VIRTIO_SCSI] = "virtio-scsi",
*ret_path = TAKE_PTR(path);
return 0;
}
+
+BindVolume* bind_volume_free(BindVolume *v) {
+ if (!v)
+ return NULL;
+
+ free(v->provider);
+ free(v->volume);
+ free(v->config);
+ free(v->template);
+
+ return mfree(v);
+}
+
+static int bind_volume_apply_extra(BindVolume *v, const char *key, const char *value) {
+ int r;
+
+ assert(v);
+ assert(key);
+ assert(value);
+
+ if (streq(key, "template")) {
+ if (v->template)
+ return -EINVAL;
+ if (!storage_template_name_is_valid(value))
+ return -EINVAL;
+ r = free_and_strdup(&v->template, value);
+ if (r < 0)
+ return r;
+ return 0;
+ }
+
+ if (streq(key, "create")) {
+ if (v->create_mode >= 0)
+ return -EINVAL;
+ CreateMode m = create_mode_from_string(value);
+ if (m < 0)
+ return m;
+ v->create_mode = m;
+ return 0;
+ }
+
+ if (STR_IN_SET(key, "read-only", "ro")) {
+ if (v->read_only >= 0)
+ return -EINVAL;
+ ReadOnlyMode m = read_only_mode_from_string(value);
+ if (m < 0) {
+ r = parse_boolean(value);
+ if (r < 0)
+ return r;
+ m = r ? READ_ONLY_YES : READ_ONLY_NO;
+ }
+ v->read_only = m;
+ return 0;
+ }
+
+ if (STR_IN_SET(key, "size", "create-size")) {
+ if (v->create_size_bytes != UINT64_MAX)
+ return -EINVAL;
+ uint64_t sz;
+ r = parse_size(value, 1024, &sz);
+ if (r < 0)
+ return r;
+ if (sz == 0)
+ return -EINVAL;
+ v->create_size_bytes = sz;
+ return 0;
+ }
+
+ if (streq(key, "request-as")) {
+ if (v->request_as >= 0)
+ return -EINVAL;
+ VolumeType t = volume_type_from_string(value);
+ if (t < 0)
+ return t;
+ v->request_as = t;
+ return 0;
+ }
+
+ return -EINVAL;
+}
+
+int bind_volume_parse(const char *arg, BindVolume **ret) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+ int r;
+
+ assert(arg);
+ assert(ret);
+
+ v = new(BindVolume, 1);
+ if (!v)
+ return -ENOMEM;
+
+ *v = BIND_VOLUME_INIT;
+
+ const char *p = arg;
+ _cleanup_free_ char *provider = NULL, *volume = NULL, *config = NULL;
+
+ r = extract_first_word(&p, &provider, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return r;
+ if (r == 0 || isempty(provider) || !storage_provider_name_is_valid(provider))
+ return -EINVAL;
+
+ r = extract_first_word(&p, &volume, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return r;
+ if (r == 0 || isempty(volume) || !storage_volume_name_is_valid(volume))
+ return -EINVAL;
+
+ r = extract_first_word(&p, &config, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return r;
+
+ v->provider = TAKE_PTR(provider);
+ v->volume = TAKE_PTR(volume);
+ if (!isempty(config)) {
+ if (!string_is_safe(config, /* flags= */ 0))
+ return -EINVAL;
+ v->config = TAKE_PTR(config);
+ }
+
+ for (;;) {
+ _cleanup_free_ char *kv = NULL, *key = NULL, *value = NULL;
+
+ r = extract_first_word(&p, &kv, ",", 0);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ r = split_pair(kv, "=", &key, &value);
+ if (r < 0)
+ return r;
+ if (isempty(key))
+ return -EINVAL;
+
+ r = bind_volume_apply_extra(v, key, value);
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+int machine_storage_name_split(const char *s, char **ret_provider, char **ret_volume) {
+ _cleanup_free_ char *p = NULL, *v = NULL;
+ int r;
+
+ if (isempty(s))
+ return -EINVAL;
+
+ r = split_pair(s, ":", &p, &v);
+ if (r < 0)
+ return r;
+
+ if (!storage_provider_name_is_valid(p) || !storage_volume_name_is_valid(v))
+ return -EINVAL;
+
+ if (ret_provider)
+ *ret_provider = TAKE_PTR(p);
+ if (ret_volume)
+ *ret_volume = TAKE_PTR(v);
+ return 0;
+}
#pragma once
#include "shared-forward.h"
+#include "storage-util.h"
typedef enum ImageFormat {
IMAGE_FORMAT_RAW,
ImageFormat *format,
DiskType *disk_type,
char **ret_path);
+
+typedef enum ReadOnlyMode {
+ READ_ONLY_NO,
+ READ_ONLY_YES,
+ READ_ONLY_AUTO,
+ _READ_ONLY_MAX,
+ _READ_ONLY_INVALID = -EINVAL,
+} ReadOnlyMode;
+
+DECLARE_STRING_TABLE_LOOKUP(read_only_mode, ReadOnlyMode);
+
+/* Map ReadOnlyMode onto the Acquire() wire tristate (-1 unset/auto, 0 no, 1 yes). */
+static inline int read_only_mode_to_tristate(ReadOnlyMode m) {
+ switch (m) {
+ case READ_ONLY_NO: return 0;
+ case READ_ONLY_YES: return 1;
+ default: return -1;
+ }
+}
+
+/* Parsed "PROVIDER:VOLUME[:CONFIG][:K=V,K=V,...]" used by --bind-volume,
+ * machinectl bind-volume, and (future) the BindVolume= unit setting. The 'config'
+ * field is opaque here and interpreted per-backend (vmspawn: a DiskType name;
+ * nspawn: a mount path). */
+typedef struct BindVolume {
+ char *provider;
+ char *volume;
+ char *config;
+
+ /* Acquire() parameters parsed from the trailing key=value list. */
+ char *template;
+ CreateMode create_mode;
+ ReadOnlyMode read_only;
+ uint64_t create_size_bytes;
+ VolumeType request_as;
+} BindVolume;
+
+#define BIND_VOLUME_INIT \
+ (BindVolume) { \
+ .create_mode = _CREATE_MODE_INVALID, \
+ .read_only = _READ_ONLY_INVALID, \
+ .create_size_bytes = UINT64_MAX, \
+ .request_as = _VOLUME_TYPE_INVALID, \
+ }
+
+BindVolume* bind_volume_free(BindVolume *v);
+DEFINE_TRIVIAL_CLEANUP_FUNC(BindVolume*, bind_volume_free);
+
+int bind_volume_parse(const char *arg, BindVolume **ret);
+
+/* Validate a "<provider>:<volume>" binding name as used by AddStorage/RemoveStorage.
+ * ret_provider/ret_volume may each be NULL when the caller only wants validation. */
+int machine_storage_name_split(const char *s, char **ret_provider, char **ret_volume);
--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "machine-util.h"
+#include "tests.h"
+
+TEST(bind_volume_parse_minimal) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+
+ ASSERT_OK(bind_volume_parse("block:/dev/sda", &v));
+ ASSERT_STREQ(v->provider, "block");
+ ASSERT_STREQ(v->volume, "/dev/sda");
+ ASSERT_NULL(v->config);
+ ASSERT_NULL(v->template);
+ ASSERT_EQ(v->create_mode, _CREATE_MODE_INVALID);
+ ASSERT_EQ(v->request_as, _VOLUME_TYPE_INVALID);
+ ASSERT_EQ(v->read_only, _READ_ONLY_INVALID);
+ ASSERT_EQ(v->create_size_bytes, UINT64_MAX);
+}
+
+TEST(bind_volume_parse_with_config) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+
+ ASSERT_OK(bind_volume_parse("block:/dev/sda:virtio-scsi", &v));
+ ASSERT_STREQ(v->provider, "block");
+ ASSERT_STREQ(v->volume, "/dev/sda");
+ ASSERT_STREQ(v->config, "virtio-scsi");
+}
+
+TEST(bind_volume_parse_empty_config) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+
+ ASSERT_OK(bind_volume_parse("fs:vol-1::create=new,size=64M,template=sparse-file", &v));
+ ASSERT_STREQ(v->provider, "fs");
+ ASSERT_STREQ(v->volume, "vol-1");
+ ASSERT_NULL(v->config);
+ ASSERT_EQ(v->create_mode, CREATE_NEW);
+ ASSERT_STREQ(v->template, "sparse-file");
+ ASSERT_EQ(v->create_size_bytes, UINT64_C(64) * 1024 * 1024);
+}
+
+TEST(bind_volume_parse_full) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+
+ ASSERT_OK(bind_volume_parse(
+ "fs:vol-2:nvme:create=any,template=allocated-file,size=128M,ro=auto,request-as=blk",
+ &v));
+ ASSERT_STREQ(v->provider, "fs");
+ ASSERT_STREQ(v->volume, "vol-2");
+ ASSERT_STREQ(v->config, "nvme");
+ ASSERT_EQ(v->create_mode, CREATE_ANY);
+ ASSERT_STREQ(v->template, "allocated-file");
+ ASSERT_EQ(v->request_as, VOLUME_BLK);
+ ASSERT_EQ(v->create_size_bytes, UINT64_C(128) * 1024 * 1024);
+ ASSERT_EQ(v->read_only, READ_ONLY_AUTO);
+}
+
+TEST(bind_volume_parse_read_only) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+
+ ASSERT_OK(bind_volume_parse("block:/dev/sdb:scsi-cd:read-only=yes", &v));
+ ASSERT_EQ(v->read_only, READ_ONLY_YES);
+
+ v = bind_volume_free(v);
+ ASSERT_OK(bind_volume_parse("block:/dev/sdb:scsi-cd:ro=no", &v));
+ ASSERT_EQ(v->read_only, READ_ONLY_NO);
+}
+
+TEST(bind_volume_parse_invalid) {
+ BindVolume *v = NULL;
+
+ /* Missing provider */
+ ASSERT_ERROR(bind_volume_parse(":vol", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Missing volume */
+ ASSERT_ERROR(bind_volume_parse("block:", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Provider with control char */
+ ASSERT_ERROR(bind_volume_parse("bl\x01ock:vol", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Config with control char */
+ ASSERT_ERROR(bind_volume_parse("block:vol:nv\x01me", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Unknown extras key */
+ ASSERT_ERROR(bind_volume_parse("block:vol::bogus=foo", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Bogus create mode */
+ ASSERT_ERROR(bind_volume_parse("block:vol::create=bogus", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Bogus request-as */
+ ASSERT_ERROR(bind_volume_parse("block:vol::request-as=bogus", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Extras entry without '=' */
+ ASSERT_ERROR(bind_volume_parse("block:vol::nokey", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Empty key (=value with no key) */
+ ASSERT_ERROR(bind_volume_parse("block:vol::=value", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Duplicate key */
+ ASSERT_ERROR(bind_volume_parse("block:vol::create=new,create=any", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Aliased duplicate (size / create-size) */
+ ASSERT_ERROR(bind_volume_parse("block:vol::size=64M,create-size=128M", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Zero-byte size */
+ ASSERT_ERROR(bind_volume_parse("block:vol::size=0", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Duplicate read-only with explicit yes/no values */
+ ASSERT_ERROR(bind_volume_parse("block:vol::read-only=yes,read-only=no", &v), EINVAL);
+ ASSERT_NULL(v);
+ ASSERT_ERROR(bind_volume_parse("block:vol::read-only=yes,ro=auto", &v), EINVAL);
+ ASSERT_NULL(v);
+}
+
+TEST(machine_storage_name_split) {
+ _cleanup_free_ char *p = NULL, *v = NULL;
+
+ ASSERT_OK(machine_storage_name_split("block:/dev/sda", &p, &v));
+ ASSERT_STREQ(p, "block");
+ ASSERT_STREQ(v, "/dev/sda");
+
+ /* NULL outputs — validate-only mode */
+ ASSERT_OK(machine_storage_name_split("fs:vol-1", NULL, NULL));
+
+ ASSERT_ERROR(machine_storage_name_split(NULL, NULL, NULL), EINVAL);
+ ASSERT_ERROR(machine_storage_name_split("", NULL, NULL), EINVAL);
+ ASSERT_ERROR(machine_storage_name_split("no-colon", NULL, NULL), EINVAL);
+ ASSERT_ERROR(machine_storage_name_split(":vol", NULL, NULL), EINVAL);
+ ASSERT_ERROR(machine_storage_name_split("block:", NULL, NULL), EINVAL);
+ ASSERT_ERROR(machine_storage_name_split("bl\x01ock:vol", NULL, NULL), EINVAL);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);