From 8f0a19ad5ba8503294c7cbb73a42ca13491086c9 Mon Sep 17 00:00:00 2001 From: tonic Date: Tue, 26 May 2026 20:00:48 +0800 Subject: [PATCH] fix(types): allow ':' and '/' in snapshot names for OCI tag-aware refs The validName regex added in #61 (`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$`) rejects vk-cocoon's tag-aware local-snapshot naming (`repo:tag`, e.g. `simular/ubuntu-hot-testing:v1`), breaking PullSnapshot for any cocoon VM create that goes through vk-cocoon. The cocoon subprocess exits with name validation error before reading stdin; vk-cocoon then writes the blob into a closed pipe and surfaces the symptom as "stream snapshot: ... broken pipe" with no visible upstream hint (its `command()` discards subprocess stderr). Split: keep validName strict for VMConfig (hostname / DNS-1123 / cidata constraints apply) and introduce validSnapshotName that additionally allows ':' and '/' for SnapshotConfig. Snapshot names are local cocoon DB keys and never propagate to hostname / DNS / cidata, so the strict charset isn't load-bearing there. Shell-unsafe chars and leading non-alnum remain rejected. --- types/snapshot.go | 4 ++-- types/snapshot_test.go | 8 +++++++- types/vm.go | 5 +++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/types/snapshot.go b/types/snapshot.go index 0186a53d..37da8eee 100644 --- a/types/snapshot.go +++ b/types/snapshot.go @@ -20,8 +20,8 @@ type SnapshotConfig struct { // Validate checks SnapshotConfig caller-controlled fields. Empty Name is allowed (name is optional). func (cfg *SnapshotConfig) Validate() error { - if cfg.Name != "" && !validName.MatchString(cfg.Name) { - return fmt.Errorf("snapshot name %q is invalid: must match %s (max 63 chars)", cfg.Name, validName.String()) + if cfg.Name != "" && !validSnapshotName.MatchString(cfg.Name) { + return fmt.Errorf("snapshot name %q is invalid: must match %s (max 63 chars)", cfg.Name, validSnapshotName.String()) } return nil } diff --git a/types/snapshot_test.go b/types/snapshot_test.go index ce7502da..c16dace2 100644 --- a/types/snapshot_test.go +++ b/types/snapshot_test.go @@ -14,11 +14,17 @@ func TestSnapshotConfig_Validate(t *testing.T) { {"empty allowed", "", false}, {"simple", "my-snap", false}, {"with dot underscore", "my.snap_v1", false}, + {"oci repo:tag", "namespace/repo:v1", false}, + {"oci repo only", "namespace/repo", false}, + {"colon only", "repo:tag", false}, {"max 63", strings.Repeat("a", 63), false}, {"over 63", strings.Repeat("a", 64), true}, {"leading hyphen", "-bad", true}, + {"leading slash", "/bad", true}, + {"leading colon", ":bad", true}, {"space", "bad name", true}, - {"slash", "bad/name", true}, + {"semicolon", "bad;rm", true}, + {"backtick", "bad`name", true}, {"control char", "bad\x00name", true}, } for _, tt := range cases { diff --git a/types/vm.go b/types/vm.go index de10f534..261f1cf5 100644 --- a/types/vm.go +++ b/types/vm.go @@ -15,8 +15,9 @@ const ( ) var ( - validName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$`) - validUsername = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,31}$`) + validName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$`) + validSnapshotName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,62}$`) + validUsername = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,31}$`) // shellUnsafe rejects chars that break the chpasswd YAML scalar in cidata. shellUnsafe = regexp.MustCompile("[`$;|&(){}\\\\<>!'\"\\x00-\\x1f\\x7f]") )