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) + } + }) + } +}