From 6ba0b647d76ad51292a8ebd980bdf00cf7eb8b37 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 30 Oct 2026 13:35:24 +0100 Subject: [PATCH] platforms: add Validate utility for OCI platform specifications Add Validate to check that a platform has non-empty OS and architecture fields and that its components are well-formed. Validation follows the existing platform specifier syntax, rejecting invalid characters and path components such as "." and "..", without restricting values to known or supported platforms. This allows callers to validate platform specifications without formatting, parsing, or normalizing them. Signed-off-by: Sebastiaan van Stijn --- platforms.go | 35 +++++++++++ platforms_test.go | 149 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) diff --git a/platforms.go b/platforms.go index 75cbf97..e3b5f08 100644 --- a/platforms.go +++ b/platforms.go @@ -129,6 +129,16 @@ var ( osRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.%-]*)((?:\+[A-Za-z0-9_.%-]+)*)\))?$`) ) +// validateOSComponent checks that s is a valid OS name. Unlike other +// specifier components, OS names do not allow dots or OS options. +func validateOSComponent(s string) error { + m := osRe.FindStringSubmatch(s) + if m == nil || m[1] != s { + return fmt.Errorf("%q must be an OS name matching [A-Za-z0-9_-]+: %w", s, errInvalidArgument) + } + return nil +} + // validateSpecifierComponent checks that s is a valid platform specifier // component, excluding "." and ".." which would be cleaned by path.Join. func validateSpecifierComponent(s string) error { @@ -484,3 +494,28 @@ func Normalize(platform specs.Platform) specs.Platform { return platform } + +// Validate checks whether the platform is well-formed. +// +// OS and Architecture must be non-empty, and all components must be valid +// platform specifiers. It does not check whether the platform is supported. +func Validate(platform specs.Platform) error { + if platform.OS == "" { + return fmt.Errorf("missing OS: %w", errInvalidArgument) + } + if platform.Architecture == "" { + return fmt.Errorf("missing architecture: %w", errInvalidArgument) + } + if err := validateOSComponent(platform.OS); err != nil { + return fmt.Errorf("invalid OS: %w", err) + } + if err := validateSpecifierComponent(platform.Architecture); err != nil { + return fmt.Errorf("invalid architecture: %w", err) + } + if platform.Variant != "" { + if err := validateSpecifierComponent(platform.Variant); err != nil { + return fmt.Errorf("invalid variant: %w", err) + } + } + return nil +} diff --git a/platforms_test.go b/platforms_test.go index ffdf9cd..f4385b0 100644 --- a/platforms_test.go +++ b/platforms_test.go @@ -17,6 +17,7 @@ package platforms import ( + "errors" "path" "reflect" "runtime" @@ -743,3 +744,151 @@ func BenchmarkFormatAllOSFeatures(b *testing.B) { }) } } + +func TestValidate(t *testing.T) { + tests := []struct { + name string + platform specs.Platform + wantErr bool + wantErrMsg string + }{ + { + name: "empty platform", + wantErr: true, + }, + { + name: "missing OS", + platform: specs.Platform{Architecture: "amd64"}, + wantErr: true, + }, + { + name: "missing architecture", + platform: specs.Platform{OS: "linux"}, + wantErr: true, + }, + { + name: "linux/amd64", + platform: specs.Platform{OS: "linux", Architecture: "amd64"}, + }, + { + name: "linux/loong64", + platform: specs.Platform{OS: "linux", Architecture: "loong64"}, + }, + { + name: "unknown OS and architecture", + platform: specs.Platform{OS: "customos", Architecture: "customarch"}, + }, + { + name: "OS with underscore and hyphen", + platform: specs.Platform{OS: "custom_os-v2", Architecture: "amd64"}, + }, + { + name: "variant", + platform: specs.Platform{OS: "linux", Architecture: "arm", Variant: "v7"}, + }, + { + name: "architecture with dot", + platform: specs.Platform{OS: "linux", Architecture: "custom.arch"}, + }, + { + name: "architecture with underscore and hyphen", + platform: specs.Platform{OS: "linux", Architecture: "custom_arch-v2"}, + }, + { + name: "variant with dot", + platform: specs.Platform{OS: "linux", Architecture: "arm", Variant: "v7.1"}, + }, + { + name: "invalid OS", + platform: specs.Platform{OS: "invalid/os", Architecture: "amd64"}, + wantErr: true, + wantErrMsg: "must be an OS name matching", + }, + { + name: "OS with whitespace", + platform: specs.Platform{OS: "invalid os", Architecture: "amd64"}, + wantErr: true, + wantErrMsg: "must be an OS name matching", + }, + { + name: "OS with dot", + platform: specs.Platform{OS: "foo.bar", Architecture: "amd64"}, + wantErr: true, + wantErrMsg: "must be an OS name matching", + }, + { + name: "OS with version", + platform: specs.Platform{OS: "windows(10.0.17763)", Architecture: "amd64"}, + wantErr: true, + wantErrMsg: "must be an OS name matching", + }, + { + name: "invalid OS character", + platform: specs.Platform{OS: "linux@", Architecture: "amd64"}, + wantErr: true, + wantErrMsg: "must be an OS name matching", + }, + { + name: "dot OS", + platform: specs.Platform{OS: ".", Architecture: "amd64"}, + wantErr: true, + }, + { + name: "dot-dot OS", + platform: specs.Platform{OS: "..", Architecture: "amd64"}, + wantErr: true, + }, + { + name: "invalid architecture", + platform: specs.Platform{OS: "linux", Architecture: "invalid/arch"}, + wantErr: true, + wantErrMsg: "must match", + }, + { + name: "dot architecture", + platform: specs.Platform{OS: "linux", Architecture: "."}, + wantErr: true, + wantErrMsg: `must not be "." or ".."`, + }, + { + name: "dot-dot architecture", + platform: specs.Platform{OS: "linux", Architecture: ".."}, + wantErr: true, + wantErrMsg: `must not be "." or ".."`, + }, + { + name: "invalid variant", + platform: specs.Platform{OS: "linux", Architecture: "arm", Variant: "invalid/variant"}, + wantErr: true, + wantErrMsg: "must match", + }, + { + name: "dot variant", + platform: specs.Platform{OS: "linux", Architecture: "arm", Variant: "."}, + wantErr: true, + wantErrMsg: `must not be "." or ".."`, + }, + { + name: "dot-dot variant", + platform: specs.Platform{OS: "linux", Architecture: "arm", Variant: ".."}, + wantErr: true, + wantErrMsg: `must not be "." or ".."`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := Validate(tc.platform) + if tc.wantErr { + if !errors.Is(err, errInvalidArgument) { + t.Fatalf("Validate(%+v) error = %v, want %v", tc.platform, err, errInvalidArgument) + } + if tc.wantErrMsg != "" && !strings.Contains(err.Error(), tc.wantErrMsg) { + t.Errorf("Validate(%+v) error = %q, want to contain %q", tc.platform, err, tc.wantErrMsg) + } + } else if err != nil { + t.Fatalf("Validate(%+v) unexpected error: %v", tc.platform, err) + } + }) + } +}