Skip to content

Commit 57b060e

Browse files
committed
pkg/cdi: implement qualified device names.
Implement composing, parsing and validating qualified device names and their vendor, class and device components. Signed-off-by: Krisztian Litkey <[email protected]>
1 parent da449e3 commit 57b060e

File tree

2 files changed

+342
-0
lines changed

2 files changed

+342
-0
lines changed

pkg/cdi/qualified-device.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
Copyright © 2021 The CDI Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cdi
18+
19+
import (
20+
"strings"
21+
22+
"github.com/pkg/errors"
23+
)
24+
25+
// QualifiedName returns the qualified name for a device.
26+
// The syntax for a qualified device names is
27+
// "<vendor>/<class>=<name>".
28+
// A valid vendor name may contain the following runes:
29+
// 'A'-'Z', 'a'-'z', '0'-'9', '.', '-', '_'.
30+
// A valid class name may contain the following runes:
31+
// 'A'-'Z', 'a'-'z', '0'-'9', '-', '_'.
32+
// A valid device name may containe the following runes:
33+
// 'A'-'Z', 'a'-'z', '0'-'9', '-', '_', '.', ':'
34+
func QualifiedName(vendor, class, name string) string {
35+
return vendor + "/" + class + "=" + name
36+
}
37+
38+
// IsQualifiedName tests if a device name is qualified.
39+
func IsQualifiedName(device string) bool {
40+
_, _, _, err := ParseQualifiedName(device)
41+
return err == nil
42+
}
43+
44+
// ParseQualifiedName splits a qualified name into device vendor, class,
45+
// and name. If the device fails to parse as a qualified name, or if any
46+
// of the split components fail to pass syntax validation, vendor and
47+
// class are returned as empty, together with the verbatim input as the
48+
// name and an error describing the reason for failure.
49+
func ParseQualifiedName(device string) (string, string, string, error) {
50+
vendor, class, name := ParseDevice(device)
51+
52+
if vendor == "" {
53+
return "", "", device, errors.Errorf("unqualified device %q, missing vendor", device)
54+
}
55+
if class == "" {
56+
return "", "", device, errors.Errorf("unqualified device %q, missing class", device)
57+
}
58+
if name == "" {
59+
return "", "", device, errors.Errorf("unqualified device %q, missing device name", device)
60+
}
61+
62+
if err := ValidateVendorName(vendor); err != nil {
63+
return "", "", device, errors.Wrapf(err, "invalid device %q", device)
64+
}
65+
if err := ValidateClassName(class); err != nil {
66+
return "", "", device, errors.Wrapf(err, "invalid device %q", device)
67+
}
68+
if err := ValidateDeviceName(name); err != nil {
69+
return "", "", device, errors.Wrapf(err, "invalid device %q", device)
70+
}
71+
72+
return vendor, class, name, nil
73+
}
74+
75+
// ParseDevice tries to split a device name into vendor, class, and name.
76+
// If this fails, for instance in the case of unqualified device names,
77+
// ParseDevice returns an empty vendor and class together with name set
78+
// to the verbatim input.
79+
func ParseDevice(device string) (string, string, string) {
80+
if device == "" || device[0] == '/' {
81+
return "", "", device
82+
}
83+
84+
parts := strings.SplitN(device, "=", 2)
85+
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
86+
return "", "", device
87+
}
88+
89+
name := parts[1]
90+
vendor, class := ParseQualifier(parts[0])
91+
if vendor == "" {
92+
return "", "", device
93+
}
94+
95+
return vendor, class, name
96+
}
97+
98+
// ParseQualifier splits a device qualifier into vendor and class.
99+
// The syntax for a device qualifier is
100+
// "<vendor>/<class>"
101+
// If parsing fails, an empty vendor and the class set to the
102+
// verbatim input is returned.
103+
func ParseQualifier(kind string) (string, string) {
104+
parts := strings.SplitN(kind, "/", 2)
105+
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
106+
return "", kind
107+
}
108+
return parts[0], parts[1]
109+
}
110+
111+
// ValidateVendorName checks the validity of a vendor name.
112+
// A vendor name may contain the following ASCII characters:
113+
// - upper- and lowercase letters ('A'-'Z', 'a'-'z')
114+
// - digits ('0'-'9')
115+
// - underscore, dash, and dot ('_', '-', and '.')
116+
func ValidateVendorName(vendor string) error {
117+
if vendor == "" {
118+
return errors.Errorf("invalid (empty) vendor name")
119+
}
120+
if !isLetter(rune(vendor[0])) {
121+
return errors.Errorf("invalid vendor %q, should start with letter", vendor)
122+
}
123+
for _, c := range string(vendor[1 : len(vendor)-1]) {
124+
switch {
125+
case isAlphaNumeric(c):
126+
case c == '_' || c == '-' || c == '.':
127+
default:
128+
return errors.Errorf("invalid character '%c' in vendor name %q",
129+
c, vendor)
130+
}
131+
}
132+
if !isAlphaNumeric(rune(vendor[len(vendor)-1])) {
133+
return errors.Errorf("invalid vendor %q, should end with letter", vendor)
134+
}
135+
136+
return nil
137+
}
138+
139+
// ValidateClassName checks the validity of class name.
140+
// A class name may contain the following ASCII characters:
141+
// - upper- and lowercase letters ('A'-'Z', 'a'-'z')
142+
// - digits ('0'-'9')
143+
// - underscore and dash ('_', '-')
144+
func ValidateClassName(class string) error {
145+
if class == "" {
146+
return errors.Errorf("invalid (empty) device class")
147+
}
148+
if !isLetter(rune(class[0])) {
149+
return errors.Errorf("invalid class %q, should start with letter", class)
150+
}
151+
for _, c := range string(class[1 : len(class)-1]) {
152+
switch {
153+
case isAlphaNumeric(c):
154+
case c == '_' || c == '-':
155+
default:
156+
return errors.Errorf("invalid character '%c' in device class %q",
157+
c, class)
158+
}
159+
}
160+
if !isAlphaNumeric(rune(class[len(class)-1])) {
161+
return errors.Errorf("invalid class %q, should end with letter", class)
162+
}
163+
return nil
164+
}
165+
166+
// ValidateDeviceName checks the validity of a device name.
167+
// A device name may contain the following ASCII characters:
168+
// - upper- and lowercase letters ('A'-'Z', 'a'-'z')
169+
// - digits ('0'-'9')
170+
// - underscore, dash, dot, colon ('_', '-', '.', ':')
171+
func ValidateDeviceName(name string) error {
172+
if name == "" {
173+
return errors.Errorf("invalid (empty) device name")
174+
}
175+
if !isLetter(rune(name[0])) {
176+
return errors.Errorf("invalid name %q, should start with letter", name)
177+
}
178+
for _, c := range string(name[1 : len(name)-1]) {
179+
switch {
180+
case isAlphaNumeric(c):
181+
case c == '_' || c == '-' || c == '.' || c == ':':
182+
default:
183+
return errors.Errorf("invalid character '%c' in device name %q",
184+
c, name)
185+
}
186+
}
187+
if !isAlphaNumeric(rune(name[len(name)-1])) {
188+
return errors.Errorf("invalid name %q, should start with letter", name)
189+
}
190+
return nil
191+
}
192+
193+
func isLetter(c rune) bool {
194+
return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')
195+
}
196+
197+
func isDigit(c rune) bool {
198+
return '0' <= c && c <= '9'
199+
}
200+
201+
func isAlphaNumeric(c rune) bool {
202+
return isLetter(c) || isDigit(c)
203+
}

pkg/cdi/qualified-device_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
Copyright © 2021 The CDI Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cdi
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func TestQualifiedName(t *testing.T) {
26+
type testCase = struct {
27+
device string
28+
vendor string
29+
class string
30+
name string
31+
isQualified bool
32+
isParsable bool
33+
}
34+
35+
for _, tc := range []*testCase{
36+
{
37+
device: "vendor.com/class=dev",
38+
vendor: "vendor.com",
39+
class: "class",
40+
name: "dev",
41+
isQualified: true,
42+
},
43+
{
44+
device: "vendor1.com/class1=dev1",
45+
vendor: "vendor1.com",
46+
class: "class1",
47+
name: "dev1",
48+
isQualified: true,
49+
},
50+
{
51+
device: "other-vendor1.com/class_1=dev_1",
52+
vendor: "other-vendor1.com",
53+
class: "class_1",
54+
name: "dev_1",
55+
isQualified: true,
56+
},
57+
{
58+
device: "yet_another-vendor2.com/c-lass_2=dev_1:2.3",
59+
vendor: "yet_another-vendor2.com",
60+
class: "c-lass_2",
61+
name: "dev_1:2.3",
62+
isQualified: true,
63+
},
64+
{
65+
device: "_invalid.com/class=dev",
66+
vendor: "_invalid.com",
67+
class: "class",
68+
name: "dev",
69+
isParsable: true,
70+
},
71+
{
72+
device: "invalid2.com-/class=dev",
73+
vendor: "invalid2.com-",
74+
class: "class",
75+
name: "dev",
76+
isParsable: true,
77+
},
78+
{
79+
device: "invalid3.com/_class=dev",
80+
vendor: "invalid3.com",
81+
class: "_class",
82+
name: "dev",
83+
isParsable: true,
84+
},
85+
{
86+
device: "invalid4.com/class_=dev",
87+
vendor: "invalid4.com",
88+
class: "class_",
89+
name: "dev",
90+
isParsable: true,
91+
},
92+
{
93+
device: "invalid5.com/class=-dev",
94+
vendor: "invalid5.com",
95+
class: "class",
96+
name: "-dev",
97+
isParsable: true,
98+
},
99+
{
100+
device: "invalid6.com/class=dev:",
101+
vendor: "invalid6.com",
102+
class: "class",
103+
name: "dev:",
104+
isParsable: true,
105+
},
106+
{
107+
device: "*.com/*dev=*gpu*",
108+
vendor: "*.com",
109+
class: "*dev",
110+
name: "*gpu*",
111+
isParsable: true,
112+
},
113+
} {
114+
t.Run(tc.name, func(t *testing.T) {
115+
vendor, class, name, err := ParseQualifiedName(tc.device)
116+
if tc.isQualified {
117+
require.True(t, IsQualifiedName(tc.device), "qualified name %q", tc.device)
118+
require.NoError(t, err)
119+
require.Equal(t, tc.vendor, vendor, "qualified name %q", tc.device)
120+
require.Equal(t, tc.class, class, "qualified name %q", tc.device)
121+
require.Equal(t, tc.name, name, "qualified name %q", tc.device)
122+
123+
vendor, class, name = ParseDevice(tc.device)
124+
require.Equal(t, tc.vendor, vendor, "parsed name %q", tc.device)
125+
require.Equal(t, tc.class, class, "parse name %q", tc.device)
126+
require.Equal(t, tc.name, name, "parsed name %q", tc.device)
127+
128+
device := QualifiedName(vendor, class, name)
129+
require.Equal(t, tc.device, device, "constructed device %q", tc.device)
130+
} else if tc.isParsable {
131+
require.False(t, IsQualifiedName(tc.device), "parsed name %q", tc.device)
132+
vendor, class, name = ParseDevice(tc.device)
133+
require.Equal(t, tc.vendor, vendor, "parsed name %q", tc.device)
134+
require.Equal(t, tc.class, class, "parse name %q", tc.device)
135+
require.Equal(t, tc.name, name, "parsed name %q", tc.device)
136+
}
137+
})
138+
}
139+
}

0 commit comments

Comments
 (0)