Skip to content

Commit caa7d77

Browse files
committed
Add grammar for platform string
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>
1 parent db76a43 commit caa7d77

8 files changed

Lines changed: 162 additions & 69 deletions

database.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,27 @@ func isKnownArch(arch string) bool {
5959
return false
6060
}
6161

62+
// normalizeOSAndVersion splits the provided platform specifier OS segment
63+
// into an OS and OSVersion.
64+
// The expected format is `<OS>[(<OSVersion>)]`, e.g., `windows(10.0.17763)`.
65+
// If `<os>` is not provided, the current host OS is returned as the first value.
66+
// If optional `(<OSVersion>)` is not provided, an empty string is returned as the
67+
// second value.
68+
func normalizeOSAndVersion(OSAndVersion string) (OS string, OSVersion string) {
69+
if OSAndVersion == "" {
70+
return runtime.GOOS, ""
71+
}
72+
73+
parts := osVersionRe.Split(OSAndVersion, -1)
74+
OS = normalizeOS(parts[0])
75+
OSVersion = ""
76+
if len(parts) > 1 && parts[1] != "" {
77+
OSVersion = parts[1]
78+
}
79+
80+
return OS, OSVersion
81+
}
82+
6283
func normalizeOS(os string) string {
6384
if os == "" {
6485
return runtime.GOOS

defaults.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package platforms
1818

1919
// DefaultString returns the default string specifier for the platform.
2020
func DefaultString() string {
21-
return Format(DefaultSpec())
21+
return FormatAll(DefaultSpec())
2222
}
2323

2424
// DefaultStrict returns strict form of Default.

defaults_unix_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func TestDefault(t *testing.T) {
3838
}
3939

4040
s := DefaultString()
41-
if s != Format(p) {
41+
if s != FormatAll(p) {
4242
t.Fatalf("default specifier should match formatted default spec: %v != %v", s, p)
4343
}
4444
}

defaults_windows_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestDefault(t *testing.T) {
4242
}
4343

4444
s := DefaultString()
45-
if s != Format(p) {
45+
if s != FormatAll(p) {
4646
t.Fatalf("default specifier should match formatted default spec: %v != %v", s, p)
4747
}
4848
}

platforms.go

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,13 @@ import (
121121
)
122122

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

129+
const osAndVersionFormat = "%s(%s)"
130+
127131
// Platform is a type alias for convenience, so there is no need to import image-spec package everywhere.
128132
type Platform = specs.Platform
129133

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

158162
func (m *matcher) String() string {
159-
return Format(m.Platform)
163+
return FormatAll(m.Platform)
160164
}
161165

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

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

189-
parts := strings.Split(specifier, "/")
196+
// Limit to 4 elements to prevent unbounded split
197+
parts := strings.SplitN(specifier, "/", 4)
190198

191-
for _, part := range parts {
192-
if !specifierRe.MatchString(part) {
193-
return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument)
199+
var p specs.Platform
200+
for i, part := range parts {
201+
if i == 0 {
202+
// First element is <os>[(<OSVersion>)]
203+
if !osAndVersionRe.MatchString(part) {
204+
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)
205+
}
206+
207+
p.OS, p.OSVersion = normalizeOSAndVersion(part)
208+
} else {
209+
if !specifierRe.MatchString(part) {
210+
return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument)
211+
}
194212
}
195213
}
196214

197-
var p specs.Platform
198215
switch len(parts) {
199216
case 1:
200-
// in this case, we will test that the value might be an OS, then look
201-
// it up. If it is not known, we'll treat it as an architecture. Since
217+
// in this case, we will test that the value might be an OS (with or
218+
// without the optional OSVersion specified) and look it up.
219+
// If it is not known, we'll treat it as an architecture. Since
202220
// we have very little information about the platform here, we are
203221
// going to be a little more strict if we don't know about the argument
204222
// value.
205-
p.OS = normalizeOS(parts[0])
223+
p.OS, p.OSVersion = normalizeOSAndVersion(parts[0])
206224
if isKnownOS(p.OS) {
207225
// picks a default architecture
208226
p.Architecture = runtime.GOARCH
209227
if p.Architecture == "arm" && cpuVariant() != "v7" {
210228
p.Variant = cpuVariant()
211229
}
212230

213-
if p.OS == "windows" {
214-
p.OSVersion = GetWindowsOsVersion()
215-
}
216-
217231
return p, nil
218232
}
219233

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

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

239-
if p.OS == "windows" {
240-
p.OSVersion = GetWindowsOsVersion()
241-
}
242-
243253
return p, nil
244254
case 3:
245255
// we have a fully specified variant, this is rare
246-
p.OS = normalizeOS(parts[0])
256+
p.OS, p.OSVersion = normalizeOSAndVersion(parts[0])
247257
p.Architecture, p.Variant = normalizeArch(parts[1], parts[2])
248258
if p.Architecture == "arm64" && p.Variant == "" {
249259
p.Variant = "v8"
250260
}
251261

252-
if p.OS == "windows" {
253-
p.OSVersion = GetWindowsOsVersion()
254-
}
255-
256262
return p, nil
257263
}
258264

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

287+
// FormatAll returns a string specifier that also includes the OSVersion from the
288+
// provided platform specification.
289+
func FormatAll(platform specs.Platform) string {
290+
if platform.OS == "" {
291+
return "unknown"
292+
}
293+
294+
if platform.OSVersion != "" {
295+
OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion)
296+
return path.Join(OSAndVersion, platform.Architecture, platform.Variant)
297+
}
298+
return path.Join(platform.OS, platform.Architecture, platform.Variant)
299+
}
300+
281301
// Normalize validates and translate the platform to the canonical value.
282302
//
283303
// For example, if "Aarch64" is encountered, we change it to "arm64" or if

platforms_other.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,3 @@ func newDefaultMatcher(platform specs.Platform) Matcher {
2828
Platform: Normalize(platform),
2929
}
3030
}
31-
32-
func GetWindowsOsVersion() string {
33-
return ""
34-
}

0 commit comments

Comments
 (0)