Skip to content

Commit

Permalink
Add grammar for platform string
Browse files Browse the repository at this point in the history
Platform string to be of the form
<os>[(<osversion>)]|<arch>|<os>[(<OSVersion>)]/<arch>[/<variant>]
OSVersion is optional only and currently used only by Windows OS.

Signed-off-by: Kirtana Ashok <kiashok@microsoft.com>
  • Loading branch information
kiashok committed Apr 8, 2024
1 parent db76a43 commit 220143f
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 70 deletions.
21 changes: 21 additions & 0 deletions database.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,27 @@ func isKnownArch(arch string) bool {
return false
}

// normalizeOSAndVersion splits the provided platform specifier OS segment
// into an OS and OSVersion.
// The expected format is `<OS>[(<OSVersion>)]`, e.g., `windows(10.0.17763)`.
// If `<os>` is not provided, the current host OS is returned as the first value.
// If optional `(<OSVersion>)` is not provided, an empty string is returned as the
// second value.
func normalizeOSAndVersion(OSAndVersion string) (OS string, OSVersion string) {
if OSAndVersion == "" {
return runtime.GOOS, ""
}

parts := osVersionRe.Split(OSAndVersion, -1)
OS = normalizeOS(parts[0])
OSVersion = ""
if len(parts) > 1 && parts[1] != "" {
OSVersion = parts[1]
}

return OS, OSVersion
}

func normalizeOS(os string) string {
if os == "" {
return runtime.GOOS
Expand Down
2 changes: 1 addition & 1 deletion defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package platforms

// DefaultString returns the default string specifier for the platform.
func DefaultString() string {
return Format(DefaultSpec())
return FormatAll(DefaultSpec())
}

// DefaultStrict returns strict form of Default.
Expand Down
2 changes: 1 addition & 1 deletion defaults_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestDefault(t *testing.T) {
}

s := DefaultString()
if s != Format(p) {
if s != FormatAll(p) {
t.Fatalf("default specifier should match formatted default spec: %v != %v", s, p)
}
}
2 changes: 1 addition & 1 deletion defaults_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestDefault(t *testing.T) {
}

s := DefaultString()
if s != Format(p) {
if s != FormatAll(p) {
t.Fatalf("default specifier should match formatted default spec: %v != %v", s, p)
}
}
Expand Down
1 change: 0 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import "errors"
//
//nolint:unused // not all errors are used on all platforms.
var (
errNotFound = errors.New("not found")
errInvalidArgument = errors.New("invalid argument")
errNotImplemented = errors.New("not implemented")
)
74 changes: 47 additions & 27 deletions platforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,13 @@ import (
)

var (
specifierRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
specifierRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
osAndVersionRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)\(?([A-Za-z0-9_.-]*)\)?$`)
osVersionRe = regexp.MustCompile(`[()]`)
)

const osAndVersionFormat = "%s(%s)"

// Platform is a type alias for convenience, so there is no need to import image-spec package everywhere.
type Platform = specs.Platform

Expand Down Expand Up @@ -156,7 +160,7 @@ func (m *matcher) Match(platform specs.Platform) bool {
}

func (m *matcher) String() string {
return Format(m.Platform)
return FormatAll(m.Platform)
}

// ParseAll parses a list of platform specifiers into a list of platform.
Expand All @@ -174,9 +178,12 @@ func ParseAll(specifiers []string) ([]specs.Platform, error) {

// Parse parses the platform specifier syntax into a platform declaration.
//
// Platform specifiers are in the format `<os>|<arch>|<os>/<arch>[/<variant>]`.
// Platform specifiers are in the format `<os>[(<OSVersion>)]|<arch>|<os>[(<OSVersion>)]/<arch>[/<variant>]`.
// The minimum required information for a platform specifier is the operating
// system or architecture. If there is only a single string (no slashes), the
// system or architecture. The OSVersion can be part of the OS like `windows(10.0.17763)`
// When an OSVersion is specified, then specs.Platform.OSVersion is populated with that value,
// and an empty string otherwise.
// If there is only a single string (no slashes), the
// value will be matched against the known set of operating systems, then fall
// back to the known set of architectures. The missing component will be
// inferred based on the local environment.
Expand All @@ -186,34 +193,41 @@ func Parse(specifier string) (specs.Platform, error) {
return specs.Platform{}, fmt.Errorf("%q: wildcards not yet supported: %w", specifier, errInvalidArgument)
}

parts := strings.Split(specifier, "/")
// Limit to 4 elements to prevent unbounded split
parts := strings.SplitN(specifier, "/", 4)

for _, part := range parts {
if !specifierRe.MatchString(part) {
return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument)
var p specs.Platform
for i, part := range parts {
if i == 0 {
// First element is <os>[(<OSVersion>)]
if !osAndVersionRe.MatchString(part) {
return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osAndVersionRe.String(), errInvalidArgument)
}

p.OS, p.OSVersion = normalizeOSAndVersion(part)
} else {
if !specifierRe.MatchString(part) {
return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument)
}
}
}

var p specs.Platform
switch len(parts) {
case 1:
// in this case, we will test that the value might be an OS, then look
// it up. If it is not known, we'll treat it as an architecture. Since
// in this case, we will test that the value might be an OS (with or
// without the optional OSVersion specified) and look it up.
// If it is not known, we'll treat it as an architecture. Since
// we have very little information about the platform here, we are
// going to be a little more strict if we don't know about the argument
// value.
p.OS = normalizeOS(parts[0])
p.OS, p.OSVersion = normalizeOSAndVersion(parts[0])
if isKnownOS(p.OS) {
// picks a default architecture
p.Architecture = runtime.GOARCH
if p.Architecture == "arm" && cpuVariant() != "v7" {
p.Variant = cpuVariant()
}

if p.OS == "windows" {
p.OSVersion = GetWindowsOsVersion()
}

return p, nil
}

Expand All @@ -228,31 +242,23 @@ func Parse(specifier string) (specs.Platform, error) {

return specs.Platform{}, fmt.Errorf("%q: unknown operating system or architecture: %w", specifier, errInvalidArgument)
case 2:
// In this case, we treat as a regular os/arch pair. We don't care
// In this case, we treat as a regular OS[(OSVersion)]/arch pair. We don't care
// about whether or not we know of the platform.
p.OS = normalizeOS(parts[0])
p.OS, p.OSVersion = normalizeOSAndVersion(parts[0])
p.Architecture, p.Variant = normalizeArch(parts[1], "")
if p.Architecture == "arm" && p.Variant == "v7" {
p.Variant = ""
}

if p.OS == "windows" {
p.OSVersion = GetWindowsOsVersion()
}

return p, nil
case 3:
// we have a fully specified variant, this is rare
p.OS = normalizeOS(parts[0])
p.OS, p.OSVersion = normalizeOSAndVersion(parts[0])
p.Architecture, p.Variant = normalizeArch(parts[1], parts[2])
if p.Architecture == "arm64" && p.Variant == "" {
p.Variant = "v8"
}

if p.OS == "windows" {
p.OSVersion = GetWindowsOsVersion()
}

return p, nil
}

Expand All @@ -278,6 +284,20 @@ func Format(platform specs.Platform) string {
return path.Join(platform.OS, platform.Architecture, platform.Variant)
}

// FormatAll returns a string specifier that also includes the OSVersion from the
// provided platform specification.
func FormatAll(platform specs.Platform) string {
if platform.OS == "" {
return "unknown"
}

if platform.OSVersion != "" {
OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion)
return path.Join(OSAndVersion, platform.Architecture, platform.Variant)
}
return path.Join(platform.OS, platform.Architecture, platform.Variant)
}

// Normalize validates and translate the platform to the canonical value.
//
// For example, if "Aarch64" is encountered, we change it to "arm64" or if
Expand Down
4 changes: 0 additions & 4 deletions platforms_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,3 @@ func newDefaultMatcher(platform specs.Platform) Matcher {
Platform: Normalize(platform),
}
}

func GetWindowsOsVersion() string {
return ""
}
Loading

0 comments on commit 220143f

Please sign in to comment.