1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package ocirequest
16
17 import (
18 "encoding/base64"
19 "errors"
20 "fmt"
21 "net/url"
22 "strconv"
23 "strings"
24 "unicode/utf8"
25
26 "cuelabs.dev/go/oci/ociregistry"
27 )
28
29
30
31 type ParseError struct {
32 Err error
33 }
34
35 func (e *ParseError) Error() string {
36 return e.Err.Error()
37 }
38
39 func (e *ParseError) Unwrap() error {
40 return e.Err
41 }
42
43 var (
44 ErrNotFound = errors.New("page not found")
45 ErrBadlyFormedDigest = errors.New("badly formed digest")
46 ErrMethodNotAllowed = errors.New("method not allowed")
47 ErrBadRequest = errors.New("bad request")
48 )
49
50 func badAPIUseError(f string, a ...any) error {
51 return ociregistry.NewError(fmt.Sprintf(f, a...), ociregistry.ErrUnsupported.Code(), nil)
52 }
53
54 type Request struct {
55 Kind Kind
56
57
58
59 Repo string
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 Digest string
78
79
80
81
82
83
84
85 Tag string
86
87
88
89 FromRepo string
90
91
92
93
94
95
96 UploadID string
97
98
99
100
101
102
103
104
105 ListN int
106
107
108
109
110
111
112
113
114 ListLast string
115 }
116
117 type Kind int
118
119 const (
120
121 ReqPing = Kind(iota)
122
123
124
125
126 ReqBlobGet
127
128
129 ReqBlobHead
130
131
132 ReqBlobDelete
133
134
135 ReqBlobStartUpload
136
137
138 ReqBlobUploadBlob
139
140
141 ReqBlobMount
142
143
144
145
146 ReqBlobUploadInfo
147
148
149
150
151 ReqBlobUploadChunk
152
153
154
155
156 ReqBlobCompleteUpload
157
158
159
160
161 ReqManifestGet
162
163
164 ReqManifestHead
165
166
167 ReqManifestPut
168
169
170 ReqManifestDelete
171
172
173
174
175
176 ReqTagsList
177
178
179
180
181 ReqReferrersList
182
183
184
185 ReqCatalogList
186 )
187
188
189
190
191
192
193
194 func Parse(method string, u *url.URL) (*Request, error) {
195 req, err := parse(method, u)
196 if err != nil {
197 return nil, &ParseError{err}
198 }
199 return req, nil
200 }
201
202 func parse(method string, u *url.URL) (*Request, error) {
203 path := u.Path
204 urlq, err := url.ParseQuery(u.RawQuery)
205 if err != nil {
206 return nil, err
207 }
208
209 var rreq Request
210 if path == "/v2" || path == "/v2/" {
211 rreq.Kind = ReqPing
212 return &rreq, nil
213 }
214 path, ok := strings.CutPrefix(path, "/v2/")
215 if !ok {
216 return nil, ociregistry.NewError("unknown URL path", ociregistry.ErrNameUnknown.Code(), nil)
217 }
218 if path == "_catalog" {
219 if method != "GET" {
220 return nil, ErrMethodNotAllowed
221 }
222 rreq.Kind = ReqCatalogList
223 setListQueryParams(&rreq, urlq)
224 return &rreq, nil
225 }
226 uploadPath, ok := strings.CutSuffix(path, "/blobs/uploads/")
227 if !ok {
228 uploadPath, ok = strings.CutSuffix(path, "/blobs/uploads")
229 }
230 if ok {
231 rreq.Repo = uploadPath
232 if !ociregistry.IsValidRepoName(rreq.Repo) {
233 return nil, ociregistry.ErrNameInvalid
234 }
235 if method != "POST" {
236 return nil, ErrMethodNotAllowed
237 }
238 if d := urlq.Get("mount"); d != "" {
239
240 rreq.Digest = d
241 if !ociregistry.IsValidDigest(rreq.Digest) {
242 return nil, ociregistry.ErrDigestInvalid
243 }
244 rreq.FromRepo = urlq.Get("from")
245 if rreq.FromRepo == "" {
246
247
248 rreq.Kind = ReqBlobStartUpload
249
250 rreq.Digest = ""
251 return &rreq, nil
252 }
253 if !ociregistry.IsValidRepoName(rreq.FromRepo) {
254 return nil, ociregistry.ErrNameInvalid
255 }
256 rreq.Kind = ReqBlobMount
257 return &rreq, nil
258 }
259 if d := urlq.Get("digest"); d != "" {
260
261 rreq.Digest = d
262 if !ociregistry.IsValidDigest(d) {
263 return nil, ErrBadlyFormedDigest
264 }
265 rreq.Kind = ReqBlobUploadBlob
266 return &rreq, nil
267 }
268
269 rreq.Kind = ReqBlobStartUpload
270 return &rreq, nil
271 }
272 path, last, ok := cutLast(path, "/")
273 if !ok {
274 return nil, ErrNotFound
275 }
276 path, lastButOne, ok := cutLast(path, "/")
277 if !ok {
278 return nil, ErrNotFound
279 }
280 switch lastButOne {
281 case "blobs":
282 rreq.Repo = path
283 if !ociregistry.IsValidDigest(last) {
284 return nil, ErrBadlyFormedDigest
285 }
286 if !ociregistry.IsValidRepoName(rreq.Repo) {
287 return nil, ociregistry.ErrNameInvalid
288 }
289 rreq.Digest = last
290 switch method {
291 case "GET":
292 rreq.Kind = ReqBlobGet
293 case "HEAD":
294 rreq.Kind = ReqBlobHead
295 case "DELETE":
296 rreq.Kind = ReqBlobDelete
297 default:
298 return nil, ErrMethodNotAllowed
299 }
300 return &rreq, nil
301 case "uploads":
302
303
304 repo, ok := strings.CutSuffix(path, "/blobs")
305 if !ok {
306 return nil, ErrNotFound
307 }
308 rreq.Repo = repo
309 if !ociregistry.IsValidRepoName(rreq.Repo) {
310 return nil, ociregistry.ErrNameInvalid
311 }
312 uploadID64 := last
313 if uploadID64 == "" {
314 return nil, ErrNotFound
315 }
316 uploadID, err := base64.RawURLEncoding.DecodeString(uploadID64)
317 if err != nil {
318 return nil, fmt.Errorf("invalid upload ID %q (cannot decode)", uploadID64)
319 }
320 if !utf8.Valid(uploadID) {
321 return nil, fmt.Errorf("upload ID %q decoded to invalid utf8", uploadID64)
322 }
323 rreq.UploadID = string(uploadID)
324
325 switch method {
326 case "GET":
327 rreq.Kind = ReqBlobUploadInfo
328 case "PATCH":
329 rreq.Kind = ReqBlobUploadChunk
330 case "PUT":
331 rreq.Kind = ReqBlobCompleteUpload
332 rreq.Digest = urlq.Get("digest")
333 if !ociregistry.IsValidDigest(rreq.Digest) {
334 return nil, ErrBadlyFormedDigest
335 }
336 default:
337 return nil, ErrMethodNotAllowed
338 }
339 return &rreq, nil
340 case "manifests":
341 rreq.Repo = path
342 if !ociregistry.IsValidRepoName(rreq.Repo) {
343 return nil, ociregistry.ErrNameInvalid
344 }
345 switch {
346 case ociregistry.IsValidDigest(last):
347 rreq.Digest = last
348 case ociregistry.IsValidTag(last):
349 rreq.Tag = last
350 default:
351 return nil, ErrNotFound
352 }
353 switch method {
354 case "GET":
355 rreq.Kind = ReqManifestGet
356 case "HEAD":
357 rreq.Kind = ReqManifestHead
358 case "PUT":
359 rreq.Kind = ReqManifestPut
360 case "DELETE":
361 rreq.Kind = ReqManifestDelete
362 default:
363 return nil, ErrMethodNotAllowed
364 }
365 return &rreq, nil
366
367 case "tags":
368 if last != "list" {
369 return nil, ErrNotFound
370 }
371 if err := setListQueryParams(&rreq, urlq); err != nil {
372 return nil, err
373 }
374 if method != "GET" {
375 return nil, ErrMethodNotAllowed
376 }
377 rreq.Repo = path
378 if !ociregistry.IsValidRepoName(rreq.Repo) {
379 return nil, ociregistry.ErrNameInvalid
380 }
381 rreq.Kind = ReqTagsList
382 return &rreq, nil
383 case "referrers":
384 if !ociregistry.IsValidDigest(last) {
385 return nil, ErrBadlyFormedDigest
386 }
387 if method != "GET" {
388 return nil, ErrMethodNotAllowed
389 }
390 rreq.Repo = path
391 if !ociregistry.IsValidRepoName(rreq.Repo) {
392 return nil, ociregistry.ErrNameInvalid
393 }
394
395
396 rreq.ListN = -1
397 rreq.Digest = last
398 rreq.Kind = ReqReferrersList
399 return &rreq, nil
400 }
401 return nil, ErrNotFound
402 }
403
404 func setListQueryParams(rreq *Request, urlq url.Values) error {
405 rreq.ListN = -1
406 if nstr := urlq.Get("n"); nstr != "" {
407 n, err := strconv.Atoi(nstr)
408 if err != nil {
409 return fmt.Errorf("n is not a valid integer: %w", ErrBadRequest)
410 }
411 rreq.ListN = n
412 }
413 rreq.ListLast = urlq.Get("last")
414 return nil
415 }
416
417 func cutLast(s, sep string) (before, after string, found bool) {
418 if i := strings.LastIndex(s, sep); i >= 0 {
419 return s[:i], s[i+len(sep):], true
420 }
421 return "", s, false
422 }
423
424
425
426
427 func ParseRange(s string) (start, end int64, ok bool) {
428 p0s, p1s, ok := strings.Cut(s, "-")
429 if !ok {
430 return 0, 0, false
431 }
432 p0, err0 := strconv.ParseInt(p0s, 10, 64)
433 p1, err1 := strconv.ParseInt(p1s, 10, 64)
434 if p1 > 0 {
435 p1++
436 }
437 return p0, p1, err0 == nil && err1 == nil
438 }
439
440
441
442
443 func RangeString(start, end int64) string {
444 end--
445 if end < 0 {
446 end = 0
447 }
448 return fmt.Sprintf("%d-%d", start, end)
449 }
450
View as plain text