diff --git a/cmd/api-response.go b/cmd/api-response.go index 290e8f79ab03d9..64de4fd7e0c3b4 100644 --- a/cmd/api-response.go +++ b/cmd/api-response.go @@ -29,13 +29,13 @@ import ( "strings" "time" - "github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio/internal/amztime" "github.com/minio/minio/internal/crypto" "github.com/minio/minio/internal/handlers" "github.com/minio/minio/internal/hash" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" + "github.com/minio/minio/internal/tagging" xxml "github.com/minio/xxml" ) @@ -362,7 +362,7 @@ type Object struct { UserMetadata *Metadata `xml:"UserMetadata,omitempty"` // x-amz-tagging values in their k/v values. - UserTags *tags.Tags `json:"userTags,omitempty" xml:"Tagging,omitempty"` + UserTags *tagging.Tagging `json:"userTags,omitempty" xml:"TagSet,omitempty"` } // CopyObjectResponse container returns ETag and LastModified of the successfully copied object @@ -630,9 +630,9 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, } content.Owner = owner if metadata { - userTags, err := tags.ParseObjectTags(object.UserTags) - if err == nil { - content.UserTags = userTags + if len(object.UserTags) > 0 { + t := tagging.Tagging(object.UserTags) + content.UserTags = &t } content.UserMetadata = &Metadata{} switch kind, _ := crypto.IsEncrypted(object.UserDefined); kind { diff --git a/internal/tagging/tags.go b/internal/tagging/tags.go new file mode 100644 index 00000000000000..aea5be80c483ad --- /dev/null +++ b/internal/tagging/tags.go @@ -0,0 +1,119 @@ +// Copyright (c) 2015-2023 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tagging + +import ( + "encoding/json" + "net/url" + "strings" + + xxml "github.com/minio/xxml" +) + +// Tags encoded +type Tagging string + +// stringsCut slices s around the first instance of sep, +// returning the text before and after sep. +// The found result reports whether sep appears in s. +// If sep does not appear in s, cut returns s, "", false. +func stringsCut(s, sep string) (before, after string, found bool) { + if i := strings.Index(s, sep); i >= 0 { + return s[:i], s[i+len(sep):], true + } + return s, "", false +} + +// MarshalXML encodes tags to XML data. +// Format corresponds to https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectTagging.html +func (tags Tagging) MarshalXML(e *xxml.Encoder, start xxml.StartElement) (err error) { + // Tag denotes key and value. + type TagSet struct { + Key string `xml:"Key"` + Value string `xml:"Value"` + } + + tagName := xxml.Name{Space: "", Local: "Tag"} + if err := e.EncodeToken(start); err != nil { + return err + } + tgs := string(tags) + for tgs != "" { + var key string + key, tgs, _ = stringsCut(tgs, "&") + if key == "" { + continue + } + key, value, _ := stringsCut(key, "=") + key, err := url.QueryUnescape(key) + if err != nil { + return err + } + + value, err = url.QueryUnescape(value) + if err != nil { + return err + } + + // tagList.Tags = append(tagList.Tags, tag{key, value}) + err = e.EncodeElement(TagSet{key, value}, xxml.StartElement{Name: tagName}) + if err != nil { + return err + } + } + + return e.EncodeToken(start.End()) +} + +// MarshalJSON returns a JSON representation of the tags. +func (tags Tagging) MarshalJSON() ([]byte, error) { + m, err := tags.Map() + if err != nil { + return nil, err + } + return json.Marshal(m) +} + +// Map returns a map representation of the tags. +func (tags Tagging) Map() (map[string]string, error) { + if len(tags) == 0 { + return map[string]string{}, nil + } + tgs := string(tags) + guess := strings.Count(tgs, "=") + res := make(map[string]string, guess) + for tgs != "" { + var key string + key, tgs, _ = stringsCut(tgs, "&") + if key == "" { + continue + } + key, value, _ := stringsCut(key, "=") + key, err := url.QueryUnescape(key) + if err != nil { + return nil, err + } + + value, err = url.QueryUnescape(value) + if err != nil { + return nil, err + } + res[key] = value + } + return res, nil +} diff --git a/internal/tagging/tags_test.go b/internal/tagging/tags_test.go new file mode 100644 index 00000000000000..271fd18b96e7ea --- /dev/null +++ b/internal/tagging/tags_test.go @@ -0,0 +1,56 @@ +// Copyright (c) 2015-2023 MinIO, Inc. +// +// # This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +package tagging + +import ( + "encoding/json" + "testing" + + xxml "github.com/minio/xxml" +) + +func TestTags_MarshalXML(t *testing.T) { + type Sample struct { + Tagging *Tagging + } + tagString := Tagging("key=value&key2=&key3=else") + sample := Sample{Tagging: &tagString} + got, err := xxml.Marshal(sample) + if err != nil { + t.Fatal(err) + } + want := "keyvaluekey2key3else" + if want != string(got) { + t.Errorf("want %s\ngot %s", want, got) + } +} + +func TestTagging_MarshalJSON(t *testing.T) { + type Sample struct { + Tagging *Tagging + } + tagString := Tagging("key=value&key2=&key3=else") + sample := Sample{Tagging: &tagString} + got, err := json.Marshal(sample) + if err != nil { + t.Fatal(err) + } + want := `{"Tagging":{"key":"value","key2":"","key3":"else"}}` + if want != string(got) { + t.Errorf("want %s\ngot %s", want, got) + } +}