1
1
/*
2
- * MinIO Cloud Storage, (C) 2020 MinIO, Inc.
2
+ * MinIO Go Library for Amazon S3 Compatible Cloud Storage
3
+ * Copyright 2020-2022 MinIO, Inc.
3
4
*
4
5
* Licensed under the Apache License, Version 2.0 (the "License");
5
6
* you may not use this file except in compliance with the License.
@@ -20,6 +21,8 @@ import (
20
21
"encoding/xml"
21
22
"io"
22
23
"net/url"
24
+ "regexp"
25
+ "sort"
23
26
"strings"
24
27
"unicode/utf8"
25
28
)
@@ -63,17 +66,28 @@ const (
63
66
maxTagCount = 50
64
67
)
65
68
69
+ // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#tag-restrictions
70
+ // borrowed from this article and also testing various ASCII characters following regex
71
+ // is supported by AWS S3 for both tags and values.
72
+ var validTagKeyValue = regexp .MustCompile (`^[a-zA-Z0-9-+\-._:/@]+$` )
73
+
66
74
func checkKey (key string ) error {
67
- if len (key ) == 0 || utf8 .RuneCountInString (key ) > maxKeyLength || strings .Contains (key , "&" ) {
75
+ if len (key ) == 0 {
76
+ return errInvalidTagKey
77
+ }
78
+
79
+ if utf8 .RuneCountInString (key ) > maxKeyLength || ! validTagKeyValue .MatchString (key ) {
68
80
return errInvalidTagKey
69
81
}
70
82
71
83
return nil
72
84
}
73
85
74
86
func checkValue (value string ) error {
75
- if utf8 .RuneCountInString (value ) > maxValueLength || strings .Contains (value , "&" ) {
76
- return errInvalidTagValue
87
+ if value != "" {
88
+ if utf8 .RuneCountInString (value ) > maxValueLength || ! validTagKeyValue .MatchString (value ) {
89
+ return errInvalidTagValue
90
+ }
77
91
}
78
92
79
93
return nil
@@ -136,11 +150,26 @@ type tagSet struct {
136
150
}
137
151
138
152
func (tags tagSet ) String () string {
139
- vals := make (url.Values )
140
- for key , value := range tags .tagMap {
141
- vals .Set (key , value )
153
+ if len (tags .tagMap ) == 0 {
154
+ return ""
142
155
}
143
- return vals .Encode ()
156
+ var buf strings.Builder
157
+ keys := make ([]string , 0 , len (tags .tagMap ))
158
+ for k := range tags .tagMap {
159
+ keys = append (keys , k )
160
+ }
161
+ sort .Strings (keys )
162
+ for _ , k := range keys {
163
+ keyEscaped := url .QueryEscape (k )
164
+ valueEscaped := url .QueryEscape (tags .tagMap [k ])
165
+ if buf .Len () > 0 {
166
+ buf .WriteByte ('&' )
167
+ }
168
+ buf .WriteString (keyEscaped )
169
+ buf .WriteByte ('=' )
170
+ buf .WriteString (valueEscaped )
171
+ }
172
+ return buf .String ()
144
173
}
145
174
146
175
func (tags * tagSet ) remove (key string ) {
@@ -175,7 +204,7 @@ func (tags *tagSet) set(key, value string, failOnExist bool) error {
175
204
}
176
205
177
206
func (tags tagSet ) toMap () map [string ]string {
178
- m := make (map [string ]string )
207
+ m := make (map [string ]string , len ( tags . tagMap ) )
179
208
for key , value := range tags .tagMap {
180
209
m [key ] = value
181
210
}
@@ -188,6 +217,7 @@ func (tags tagSet) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
188
217
Tags []Tag `xml:"Tag"`
189
218
}{}
190
219
220
+ tagList .Tags = make ([]Tag , 0 , len (tags .tagMap ))
191
221
for key , value := range tags .tagMap {
192
222
tagList .Tags = append (tagList .Tags , Tag {key , value })
193
223
}
@@ -213,7 +243,7 @@ func (tags *tagSet) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
213
243
return errTooManyTags
214
244
}
215
245
216
- m := map [string ]string {}
246
+ m := make ( map [string ]string , len ( tagList . Tags ))
217
247
for _ , tag := range tagList .Tags {
218
248
if _ , found := m [tag .Key ]; found {
219
249
return errDuplicateTagKey
@@ -311,25 +341,58 @@ func ParseObjectXML(reader io.Reader) (*Tags, error) {
311
341
return unmarshalXML (reader , true )
312
342
}
313
343
344
+ // stringsCut slices s around the first instance of sep,
345
+ // returning the text before and after sep.
346
+ // The found result reports whether sep appears in s.
347
+ // If sep does not appear in s, cut returns s, "", false.
348
+ func stringsCut (s , sep string ) (before , after string , found bool ) {
349
+ if i := strings .Index (s , sep ); i >= 0 {
350
+ return s [:i ], s [i + len (sep ):], true
351
+ }
352
+ return s , "" , false
353
+ }
354
+
355
+ func (tags * tagSet ) parseTags (tgs string ) (err error ) {
356
+ for tgs != "" {
357
+ var key string
358
+ key , tgs , _ = stringsCut (tgs , "&" )
359
+ if key == "" {
360
+ continue
361
+ }
362
+ key , value , _ := stringsCut (key , "=" )
363
+ key , err1 := url .QueryUnescape (key )
364
+ if err1 != nil {
365
+ if err == nil {
366
+ err = err1
367
+ }
368
+ continue
369
+ }
370
+ value , err1 = url .QueryUnescape (value )
371
+ if err1 != nil {
372
+ if err == nil {
373
+ err = err1
374
+ }
375
+ continue
376
+ }
377
+ if err = tags .set (key , value , true ); err != nil {
378
+ return err
379
+ }
380
+ }
381
+ return err
382
+ }
383
+
314
384
// Parse decodes HTTP query formatted string into tags which is limited by isObject.
315
385
// A query formatted string is like "key1=value1&key2=value2".
316
386
func Parse (s string , isObject bool ) (* Tags , error ) {
317
- values , err := url .ParseQuery (s )
318
- if err != nil {
319
- return nil , err
320
- }
321
-
322
387
tagging := & Tags {
323
388
TagSet : & tagSet {
324
389
tagMap : make (map [string ]string ),
325
390
isObject : isObject ,
326
391
},
327
392
}
328
393
329
- for key := range values {
330
- if err := tagging .TagSet .set (key , values .Get (key ), true ); err != nil {
331
- return nil , err
332
- }
394
+ if err := tagging .TagSet .parseTags (s ); err != nil {
395
+ return nil , err
333
396
}
334
397
335
398
return tagging , nil
0 commit comments