Skip to content

Commit 1e75776

Browse files
authored
Merge pull request #30 from thaJeztah/platforms_refactor
refactor, optimize FormatAll, ParseAll
2 parents e543b9f + d028ee3 commit 1e75776

File tree

7 files changed

+214
-23
lines changed

7 files changed

+214
-23
lines changed

‎cpuinfo_linux.go‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ func getMachineArch() (string, error) {
4545
// So we don't need to access the ARM registers to detect platform information
4646
// by ourselves. We can just parse these information from /proc/cpuinfo
4747
func getCPUInfo(pattern string) (info string, err error) {
48-
4948
cpuinfo, err := os.Open("/proc/cpuinfo")
5049
if err != nil {
5150
return "", err
@@ -75,7 +74,6 @@ func getCPUInfo(pattern string) (info string, err error) {
7574

7675
// getCPUVariantFromArch get CPU variant from arch through a system call
7776
func getCPUVariantFromArch(arch string) (string, error) {
78-
7977
var variant string
8078

8179
arch = strings.ToLower(arch)

‎cpuinfo_linux_test.go‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ func TestCPUVariant(t *testing.T) {
4646
}
4747

4848
func TestGetCPUVariantFromArch(t *testing.T) {
49-
5049
for _, testcase := range []struct {
5150
name string
5251
input string
@@ -127,13 +126,11 @@ func TestGetCPUVariantFromArch(t *testing.T) {
127126
t.Fatalf("Expect to get variant: %v, however %v returned", testcase.output, variant)
128127
}
129128
}
130-
131129
} else {
132130
if !errors.Is(err, testcase.expectedErr) {
133131
t.Fatalf("Expect to get error: %v, however error %v returned", testcase.expectedErr, err)
134132
}
135133
}
136134
})
137-
138135
}
139136
}

‎cpuinfo_other.go‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
)
2525

2626
func getCPUVariant() (string, error) {
27-
2827
var variant string
2928

3029
switch runtime.GOOS {

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/containerd/platforms
22

3-
go 1.21
3+
go 1.24
44

55
require (
66
github.com/containerd/log v0.1.0

‎platform_windows_compat_test.go‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,5 +183,4 @@ func Test_PlatformOrder(t *testing.T) {
183183
}
184184
})
185185
}
186-
187186
}

‎platforms.go‎

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -280,13 +280,9 @@ func Parse(specifier string) (specs.Platform, error) {
280280
}
281281
p.OSVersion = osVersion
282282
if osOptions[3] != "" {
283-
rawFeatures := strings.Split(osOptions[3][1:], "+")
284-
p.OSFeatures = make([]string, len(rawFeatures))
285-
for i, f := range rawFeatures {
286-
p.OSFeatures[i], err = decodeOSOption(f)
287-
if err != nil {
288-
return specs.Platform{}, fmt.Errorf("%q has an invalid OS feature %q: %w", specifier, f, err)
289-
}
283+
p.OSFeatures, err = parseOSFeatures(osOptions[3][1:])
284+
if err != nil {
285+
return specs.Platform{}, fmt.Errorf("%q has invalid OS features: %w", specifier, err)
290286
}
291287
}
292288
} else {
@@ -346,6 +342,30 @@ func Parse(specifier string) (specs.Platform, error) {
346342
return specs.Platform{}, fmt.Errorf("%q: cannot parse platform specifier: %w", specifier, errInvalidArgument)
347343
}
348344

345+
func parseOSFeatures(s string) ([]string, error) {
346+
if s == "" {
347+
return nil, nil
348+
}
349+
350+
var features []string
351+
for raw := range strings.SplitSeq(s, "+") {
352+
raw = strings.TrimSpace(raw)
353+
if raw == "" {
354+
return nil, fmt.Errorf("empty os feature: %w", errInvalidArgument)
355+
}
356+
feature, err := decodeOSOption(raw)
357+
if err != nil {
358+
return nil, fmt.Errorf("invalid os feature %q: %w", raw, err)
359+
}
360+
if feature == "" {
361+
continue
362+
}
363+
features = append(features, feature)
364+
}
365+
366+
return features, nil
367+
}
368+
349369
// MustParse is like Parses but panics if the specifier cannot be parsed.
350370
// Simplifies initialization of global variables.
351371
func MustParse(specifier string) specs.Platform {
@@ -371,21 +391,55 @@ func FormatAll(platform specs.Platform) string {
371391
if platform.OS == "" {
372392
return "unknown"
373393
}
394+
if platform.OSVersion == "" && len(platform.OSFeatures) == 0 {
395+
return path.Join(platform.OS, platform.Architecture, platform.Variant)
396+
}
397+
398+
var b strings.Builder
399+
b.WriteString(platform.OS)
400+
osv := encodeOSOption(platform.OSVersion)
401+
formatted := formatOSFeatures(platform.OSFeatures)
402+
if osv != "" || formatted != "" {
403+
b.Grow(len(osv) + len(formatted) + 3) // parens + maybe '+'
404+
b.WriteByte('(')
405+
if osv != "" {
406+
b.WriteString(osv)
407+
}
408+
if formatted != "" {
409+
b.WriteByte('+')
410+
b.WriteString(formatted)
411+
}
412+
b.WriteByte(')')
413+
}
414+
415+
return path.Join(b.String(), platform.Architecture, platform.Variant)
416+
}
417+
418+
func formatOSFeatures(features []string) string {
419+
if len(features) == 0 {
420+
return ""
421+
}
374422

375-
osOptions := encodeOSOption(platform.OSVersion)
376-
features := platform.OSFeatures
377423
if !slices.IsSorted(features) {
378424
features = slices.Clone(features)
379425
slices.Sort(features)
380426
}
427+
var b strings.Builder
428+
var wrote bool
429+
var prev string
381430
for _, f := range features {
382-
osOptions += "+" + encodeOSOption(f)
383-
}
384-
if osOptions != "" {
385-
OSAndVersion := fmt.Sprintf("%s(%s)", platform.OS, osOptions)
386-
return path.Join(OSAndVersion, platform.Architecture, platform.Variant)
431+
if f == "" || f == prev {
432+
// skip empty and duplicate values
433+
continue
434+
}
435+
prev = f
436+
if wrote {
437+
b.WriteByte('+')
438+
}
439+
b.WriteString(encodeOSOption(f))
440+
wrote = true
387441
}
388-
return path.Join(platform.OS, platform.Architecture, platform.Variant)
442+
return b.String()
389443
}
390444

391445
// osOptionReplacer encodes characters in OS option values (version and

‎platforms_test.go‎

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"path"
2121
"reflect"
2222
"runtime"
23+
"strconv"
24+
"strings"
2325
"testing"
2426

2527
specs "github.com/opencontainers/image-spec/specs-go/v1"
@@ -578,6 +580,20 @@ func TestParseSelectorInvalid(t *testing.T) {
578580
}
579581
}
580582

583+
func TestFormatAllSkipsEmptyOSFeatures(t *testing.T) {
584+
p := specs.Platform{
585+
OS: "linux",
586+
Architecture: "amd64",
587+
OSFeatures: []string{"", "gpu", "", "simd"},
588+
}
589+
590+
formatted := FormatAll(p)
591+
expected := "linux(+gpu+simd)/amd64"
592+
if formatted != expected {
593+
t.Fatalf("unexpected format: %q != %q", formatted, expected)
594+
}
595+
}
596+
581597
func FuzzPlatformsParse(f *testing.F) {
582598
f.Add("linux/amd64")
583599
f.Fuzz(func(t *testing.T, s string) {
@@ -587,3 +603,131 @@ func FuzzPlatformsParse(f *testing.F) {
587603
}
588604
})
589605
}
606+
607+
func BenchmarkParseOSOptions(b *testing.B) {
608+
maxFeatures := 16
609+
610+
benchmarks := []struct {
611+
doc string
612+
input string
613+
}{
614+
{
615+
doc: "valid windows version and feature",
616+
input: "windows(10.0.17763+win32k)/amd64",
617+
},
618+
{
619+
doc: "valid but lengthy features",
620+
input: "linux(+" + strings.Repeat("+feature", maxFeatures) + ")/amd64",
621+
},
622+
{
623+
doc: "exploding plus chain",
624+
input: "linux(" + strings.Repeat("+", 64*1024) + ")/amd64",
625+
},
626+
{
627+
doc: "kernel config feature blob",
628+
input: "linux(+CONFIG_" + strings.Repeat("FOO=y_", 16*1024) + "BAR)/amd64",
629+
},
630+
}
631+
632+
b.ReportAllocs()
633+
b.ResetTimer()
634+
for _, bm := range benchmarks {
635+
b.Run(bm.doc, func(b *testing.B) {
636+
for range b.N {
637+
_, _ = Parse(bm.input)
638+
}
639+
})
640+
}
641+
}
642+
643+
func BenchmarkFormatAllOSFeatures(b *testing.B) {
644+
maxFeatures := 16
645+
646+
benchmarks := []struct {
647+
doc string
648+
platform specs.Platform
649+
}{
650+
{
651+
doc: "plain linux amd64",
652+
platform: specs.Platform{
653+
OS: "linux",
654+
Architecture: "amd64",
655+
},
656+
},
657+
{
658+
doc: "windows version and feature",
659+
platform: specs.Platform{
660+
OS: "windows",
661+
OSVersion: "10.0.17763",
662+
OSFeatures: []string{"win32k"},
663+
Architecture: "amd64",
664+
},
665+
},
666+
{
667+
doc: "valid but lengthy features",
668+
platform: specs.Platform{
669+
OS: "linux",
670+
OSFeatures: func() (out []string) {
671+
for range maxFeatures {
672+
out = append(out, "feature")
673+
}
674+
return out
675+
}(),
676+
Architecture: "amd64",
677+
},
678+
},
679+
{
680+
doc: "skips empty features",
681+
platform: specs.Platform{
682+
OS: "linux",
683+
OSFeatures: []string{"", "gpu", "", "simd"},
684+
Architecture: "amd64",
685+
},
686+
},
687+
{
688+
doc: "kernel config feature blob",
689+
platform: specs.Platform{
690+
OS: "linux",
691+
OSFeatures: []string{"CONFIG_" + strings.Repeat("FOO_", 16*1024) + "BAR"},
692+
Architecture: "amd64",
693+
},
694+
},
695+
{
696+
doc: "many kernel config features with empties",
697+
platform: specs.Platform{
698+
OS: "linux",
699+
OSFeatures: func() []string {
700+
n := 1024
701+
out := make([]string, n)
702+
for i := range out {
703+
if i%10 == 0 {
704+
out[i] = "" // simulate bad data
705+
} else {
706+
out[i] = "CONFIG_FOO_" + strconv.Itoa(i)
707+
}
708+
}
709+
return out
710+
}(),
711+
Architecture: "amd64",
712+
},
713+
},
714+
{
715+
doc: "too many features",
716+
platform: specs.Platform{
717+
OS: "linux",
718+
OSFeatures: make([]string, maxFeatures+1),
719+
Architecture: "amd64",
720+
},
721+
},
722+
}
723+
724+
b.ReportAllocs()
725+
b.ResetTimer()
726+
for _, bm := range benchmarks {
727+
b.Run(bm.doc, func(b *testing.B) {
728+
for range b.N {
729+
_ = FormatAll(bm.platform)
730+
}
731+
})
732+
}
733+
}

0 commit comments

Comments
 (0)