-
Notifications
You must be signed in to change notification settings - Fork 7
/
id.go
243 lines (207 loc) · 6.74 KB
/
id.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
// SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0
package wrp
import (
"errors"
"fmt"
"regexp"
"strings"
"unicode"
)
const (
hexDigits = "0123456789abcdefABCDEF"
macDelimiters = ":-.,"
SchemeMAC = "mac"
SchemeUUID = "uuid"
SchemeDNS = "dns"
SchemeSerial = "serial"
SchemeSelf = "self"
SchemeEvent = "event"
SchemeUnknown = ""
)
var (
ErrorInvalidDeviceName = errors.New("invalid device name")
ErrorInvalidLocator = errors.New("invalid locator")
invalidDeviceID = DeviceID("")
// Locator/DeviceID form:
// {scheme|prefix}:{authority|id}/{service}/{ignored}
//
// If the scheme is "mac", "uuid", or "serial" then the authority is the
// device identifier.
// If the scheme is "dns" then the authority is the FQDN of the service.
// If the scheme is "event" then the authority is the event name.
// If the scheme is "self" then the authority is the empty string.
// DevicIDPattern is the precompiled regular expression that all device identifiers must match.
// Matching is partial, as everything after the service is ignored.
DeviceIDPattern = regexp.MustCompile(
`^(?P<prefix>(?i)mac|uuid|dns|serial|self):(?P<id>[^/]+)(?P<service>/[^/]+)?`,
)
// LocatorPattern is the precompiled regular expression that all locators must match.
LocatorPattern = regexp.MustCompile(
`^(?P<scheme>(?i)mac|uuid|dns|serial|event|self):(?P<authority>[^/]+)?(?P<service>/[^/]+)?(?P<ignored>.+)?`,
)
)
// ID represents a normalized identifier for a device.
type DeviceID string
// Bytes is a convenience function to obtain the []byte representation of an ID.
func (id DeviceID) Bytes() []byte {
return []byte(id)
}
// String is a convenience function to obtain the string representation of the
// prefix portion of the ID.
func (id DeviceID) Prefix() string {
prefix, _ := id.split()
return prefix
}
// ID is a convenience function to obtain the string representation of the
// identifier portion of the ID.
func (id DeviceID) ID() string {
_, idPart := id.split()
return idPart
}
func (id DeviceID) split() (prefix, idPart string) {
parts := strings.SplitN(string(id), ":", 2)
if len(parts) != 2 {
return parts[0], ""
}
return parts[0], parts[1]
}
// ParseID parses a raw device name into a canonicalized identifier.
func ParseDeviceID(deviceName string) (DeviceID, error) {
match := DeviceIDPattern.FindStringSubmatch(deviceName)
if match == nil {
return invalidDeviceID, ErrorInvalidDeviceName
}
return makeDeviceID(match[1], match[2])
}
// Locator represents a device locator, which is a device identifier an optional
// service name and an optional ignored portion at the end.
//
// The general format is:
//
// {scheme}:{authority}/{service}/{ignored}
//
// See https://xmidt.io/docs/wrp/basics/#locators for more details.
type Locator struct {
// Scheme is the scheme type of the locator. A CPE will have the forms
// `mac`, `uuid`, `serial`, `self`. A server or cloud service will have
// the form `dns`. An event locator that is used for pub-sub listeners
// will have the form `event`.
//
// The Scheme MUST NOT be used to determine where to send a message, but
// rather to determine how to interpret the authority and service.
//
// The Scheme value will always be lower case.
Scheme string
// Authority is the authority portion of the locator. For a CPE, this
// will be the device identifier. For a server or cloud service, this
// will be the DNS name of the service. For an event locator, this will
// be the event name.
Authority string
// Service is the service name portion of the locator. This is optional
// and is used to identify which service(s) the message is targeting or
// originated from. A Service value will not contain any `/` characters.
Service string
// Ignored is an optional portion of the locator that is ignored by the
// WRP spec, but is provided to consumers for their usage. The Ignored
// value will contain a prefix of the `/` character.
Ignored string
// ID is the device identifier portion of the locator if it is one.
ID DeviceID
}
// ParseLocator parses a raw locator string into a canonicalized locator.
func ParseLocator(locator string) (Locator, error) {
match := LocatorPattern.FindStringSubmatch(locator)
if match == nil {
return Locator{}, fmt.Errorf("%w: `%s` does not match expected locator pattern", ErrorInvalidLocator, locator)
}
var l Locator
l.Scheme = strings.TrimSpace(strings.ToLower(match[1]))
l.Authority = strings.TrimSpace(match[2])
if len(match) > 3 {
l.Service = strings.TrimSpace(strings.TrimPrefix(match[3], "/"))
}
if len(match) > 4 {
l.Ignored = strings.TrimSpace(match[4])
}
// If the locator is a device identifier, then we need to parse it.
switch l.Scheme {
case SchemeDNS:
if l.Authority == "" {
return Locator{}, fmt.Errorf("%w: empty authority", ErrorInvalidLocator)
}
case SchemeEvent:
if l.Authority == "" {
return Locator{}, fmt.Errorf("%w: empty authority", ErrorInvalidLocator)
}
if l.Service != "" {
l.Ignored = "/" + l.Service + l.Ignored
l.Service = ""
}
case SchemeMAC, SchemeUUID, SchemeSerial, SchemeSelf:
id, err := makeDeviceID(l.Scheme, l.Authority)
if err != nil {
return Locator{}, fmt.Errorf("%w: unable to make a device ID with scheme `%s` and authority `%s`", err, l.Scheme, l.Authority)
}
l.ID = id
default:
}
return l, nil
}
// HasDeviceID returns true if the locator is a device identifier.
func (l Locator) HasDeviceID() bool {
return l.ID != ""
}
// IsSelf returns true if the locator is a self locator.
func (l Locator) IsSelf() bool {
return l.Scheme == SchemeSelf
}
func (l Locator) String() string {
var buf strings.Builder
buf.WriteString(l.Scheme)
buf.WriteString(":")
buf.WriteString(l.Authority)
if l.Service != "" {
buf.WriteString("/")
buf.WriteString(l.Service)
}
if l.Ignored != "" {
buf.WriteString(l.Ignored)
}
return buf.String()
}
func makeDeviceID(prefix, idPart string) (DeviceID, error) {
prefix = strings.ToLower(prefix)
switch prefix {
case SchemeSelf:
if idPart != "" {
return invalidDeviceID, ErrorInvalidDeviceName
}
case SchemeUUID, SchemeSerial:
if idPart == "" {
return invalidDeviceID, ErrorInvalidDeviceName
}
case SchemeMAC:
var invalidCharacter rune = -1
idPart = strings.Map(
func(r rune) rune {
switch {
case strings.ContainsRune(hexDigits, r):
return unicode.ToLower(r)
case strings.ContainsRune(macDelimiters, r):
return -1
default:
invalidCharacter = r
return -1
}
},
idPart,
)
if invalidCharacter != -1 ||
((len(idPart) != 12) && (len(idPart) != 16) && (len(idPart) != 40)) {
return invalidDeviceID, ErrorInvalidDeviceName
}
default:
}
return DeviceID(fmt.Sprintf("%s:%s", prefix, idPart)), nil
}