blob: 2b534520697ff1a98f5e49687e4a65bc08f5f963 [file] [log] [blame]
// Copyright 2022 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package quotakeys
import (
"encoding/ascii85"
"strings"
"go.chromium.org/luci/common/errors"
)
const (
// ASIFieldDelim is used to delimit sections within Application Specific
// Identifiers (ASIs).
//
// NOTE: this is ascii85-safe.
ASIFieldDelim = "|"
// EncodedSectionPrefix is the prefix used for ASI sections which are nominally
// encoded with ascii85.
//
// NOTE: this is ascii85-safe.
EncodedSectionPrefix = "{"
encodedSectionPrefixRune = '{'
// EscapedCharacters is the set of characters which are reserved within
// Application-specific-identifiers (ASIs) and will cause the ASI section to be
// escaped with ascii85.
//
// We also encode ASI sections which start with `EncodedSectionPrefix`.
EscapedCharacters = QuotaFieldDelim + ASIFieldDelim
)
func extendBuffer(buf []byte, n int) []byte {
if cap(buf)-len(buf) < n {
buf = append(make([]byte, 0, len(buf)+n), buf...)
}
return buf[:n]
}
// AssembleASI will return an ASI with the given sections.
//
// Sections are assembled with a "|" separator verbatim, unless the section
// contains a "|", "~" or begins with "{". In this case the section will be
// encoded with ascii85 and inserted to the final string with a "{" prefix
// character.
func AssembleASI(sections ...string) string {
encodedSections := make([]string, len(sections))
var buf []byte
for i, section := range sections {
if strings.HasPrefix(section, EncodedSectionPrefix) || strings.ContainsAny(section, EscapedCharacters) {
buf = extendBuffer(buf, ascii85.MaxEncodedLen(len(section))+1)
buf[0] = encodedSectionPrefixRune
smallBuf := buf[1:]
encodedSections[i] = string(buf[:ascii85.Encode(smallBuf, []byte(section))+1])
} else {
encodedSections[i] = section
}
}
return strings.Join(encodedSections, ASIFieldDelim)
}
// DecodeASI will return the sections within an ASI, decoding any which appear
// to be ascii85-encoded.
//
// If a section has the ascii85 prefix, but doesn't correctly decode, this
// returns an error.
func DecodeASI(asi string) ([]string, error) {
if asi == "" {
return nil, nil
}
var buf []byte
sections := strings.Split(asi, ASIFieldDelim)
for i, section := range sections {
if strings.HasPrefix(section, EncodedSectionPrefix) {
buf = extendBuffer(buf, len(section)-1)
ndst, _, err := ascii85.Decode(buf, []byte(section[1:]), true)
if err != nil {
return nil, errors.Annotate(err, "DecodeASI: section[%d]", i).Err()
}
sections[i] = string(buf[:ndst])
}
}
return sections, nil
}