// Copyright The OpenTelemetry Authors// SPDX-License-Identifier: Apache-2.0package baggage // import "go.opentelemetry.io/otel/baggage"import ()const ( maxMembers = 180 maxBytesPerMembers = 4096 maxBytesPerBaggageString = 8192 listDelimiter = "," keyValueDelimiter = "=" propertyDelimiter = ";")var ( errInvalidKey = errors.New("invalid key") errInvalidValue = errors.New("invalid value") errInvalidProperty = errors.New("invalid baggage list-member property") errInvalidMember = errors.New("invalid baggage list-member") errMemberNumber = errors.New("too many list-members in baggage-string") errMemberBytes = errors.New("list-member too large") errBaggageBytes = errors.New("baggage-string too large"))// Property is an additional metadata entry for a baggage list-member.typePropertystruct { key, value string// hasValue indicates if a zero-value value means the property does not // have a value or if it was the zero-value. hasValue bool}// NewKeyProperty returns a new Property for key.//// The passed key must be valid, non-empty UTF-8 string.// If key is invalid, an error will be returned.// However, the specific Propagators that are used to transmit baggage entries across// component boundaries may impose their own restrictions on Property key.// For example, the W3C Baggage specification restricts the Property keys to strings that// satisfy the token definition from RFC7230, Section 3.2.6.// For maximum compatibility, alphanumeric value are strongly recommended to be used as Property key.func ( string) (Property, error) {if !validateBaggageName() {returnnewInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, ) } := Property{key: }return , nil}// NewKeyValueProperty returns a new Property for key with value.//// The passed key must be compliant with W3C Baggage specification.// The passed value must be percent-encoded as defined in W3C Baggage specification.//// Notice: Consider using [NewKeyValuePropertyRaw] instead// that does not require percent-encoding of the value.func (, string) (Property, error) {if !validateKey() {returnnewInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, ) }if !validateValue() {returnnewInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, ) } , := url.PathUnescape()if != nil {returnnewInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, ) }returnNewKeyValuePropertyRaw(, )}// NewKeyValuePropertyRaw returns a new Property for key with value.//// The passed key must be valid, non-empty UTF-8 string.// The passed value must be valid UTF-8 string.// However, the specific Propagators that are used to transmit baggage entries across// component boundaries may impose their own restrictions on Property key.// For example, the W3C Baggage specification restricts the Property keys to strings that// satisfy the token definition from RFC7230, Section 3.2.6.// For maximum compatibility, alphanumeric value are strongly recommended to be used as Property key.func (, string) (Property, error) {if !validateBaggageName() {returnnewInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, ) }if !validateBaggageValue() {returnnewInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, ) } := Property{key: ,value: ,hasValue: true, }return , nil}func newInvalidProperty() Property {returnProperty{}}// parseProperty attempts to decode a Property from the passed string. It// returns an error if the input is invalid according to the W3C Baggage// specification.func parseProperty( string) (Property, error) {if == "" {returnnewInvalidProperty(), nil } , := parsePropertyInternal()if ! {returnnewInvalidProperty(), fmt.Errorf("%w: %q", errInvalidProperty, ) }return , nil}// validate ensures p conforms to the W3C Baggage specification, returning an// error otherwise.func ( Property) () error { := func( error) error {returnfmt.Errorf("invalid property: %w", ) }if !validateBaggageName(.key) {return (fmt.Errorf("%w: %q", errInvalidKey, .key)) }if !.hasValue && .value != "" {return (errors.New("inconsistent value")) }if .hasValue && !validateBaggageValue(.value) {return (fmt.Errorf("%w: %q", errInvalidValue, .value)) }returnnil}// Key returns the Property key.func ( Property) () string {return .key}// Value returns the Property value. Additionally, a boolean value is returned// indicating if the returned value is the empty if the Property has a value// that is empty or if the value is not set.func ( Property) () (string, bool) {return .value, .hasValue}// String encodes Property into a header string compliant with the W3C Baggage// specification.// It would return empty string if the key is invalid with the W3C Baggage// specification. This could happen for a UTF-8 key, as it may contain// invalid characters.func ( Property) () string {// W3C Baggage specification does not allow percent-encoded keys.if !validateKey(.key) {return"" }if .hasValue {returnfmt.Sprintf("%s%s%v", .key, keyValueDelimiter, valueEscape(.value)) }return .key}type properties []Propertyfunc fromInternalProperties( []baggage.Property) properties {iflen() == 0 {returnnil } := make(properties, len())for , := range { [] = Property{key: .Key,value: .Value,hasValue: .HasValue, } }return}func ( properties) () []baggage.Property {iflen() == 0 {returnnil } := make([]baggage.Property, len())for , := range { [] = baggage.Property{Key: .key,Value: .value,HasValue: .hasValue, } }return}func ( properties) () properties {iflen() == 0 {returnnil } := make(properties, len())copy(, )return}// validate ensures each Property in p conforms to the W3C Baggage// specification, returning an error otherwise.func ( properties) () error {for , := range {if := .validate(); != nil {return } }returnnil}// String encodes properties into a header string compliant with the W3C Baggage// specification.func ( properties) () string { := make([]string, 0, len())for , := range { := .String()// Ignored empty properties.if != "" { = append(, ) } }returnstrings.Join(, propertyDelimiter)}// Member is a list-member of a baggage-string as defined by the W3C Baggage// specification.typeMemberstruct { key, value string properties properties// hasData indicates whether the created property contains data or not. // Properties that do not contain data are invalid with no other check // required. hasData bool}// NewMember returns a new Member from the passed arguments.//// The passed key must be compliant with W3C Baggage specification.// The passed value must be percent-encoded as defined in W3C Baggage specification.//// Notice: Consider using [NewMemberRaw] instead// that does not require percent-encoding of the value.func (, string, ...Property) (Member, error) {if !validateKey() {returnnewInvalidMember(), fmt.Errorf("%w: %q", errInvalidKey, ) }if !validateValue() {returnnewInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, ) } , := url.PathUnescape()if != nil {returnnewInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, ) }returnNewMemberRaw(, , ...)}// NewMemberRaw returns a new Member from the passed arguments.//// The passed key must be valid, non-empty UTF-8 string.// The passed value must be valid UTF-8 string.// However, the specific Propagators that are used to transmit baggage entries across// component boundaries may impose their own restrictions on baggage key.// For example, the W3C Baggage specification restricts the baggage keys to strings that// satisfy the token definition from RFC7230, Section 3.2.6.// For maximum compatibility, alphanumeric value are strongly recommended to be used as baggage key.func (, string, ...Property) (Member, error) { := Member{key: ,value: ,properties: properties().Copy(),hasData: true, }if := .validate(); != nil {returnnewInvalidMember(), }return , nil}func newInvalidMember() Member {returnMember{}}// parseMember attempts to decode a Member from the passed string. It returns// an error if the input is invalid according to the W3C Baggage// specification.func parseMember( string) (Member, error) {if := len(); > maxBytesPerMembers {returnnewInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, ) }varproperties , , := strings.Cut(, propertyDelimiter)if {// Parse the member properties.for , := rangestrings.Split(, propertyDelimiter) { , := parseProperty()if != nil {returnnewInvalidMember(), } = append(, ) } }// Parse the member key/value pair.// Take into account a value can contain equal signs (=). , , := strings.Cut(, keyValueDelimiter)if ! {returnnewInvalidMember(), fmt.Errorf("%w: %q", errInvalidMember, ) }// "Leading and trailing whitespaces are allowed but MUST be trimmed // when converting the header into a data structure." := strings.TrimSpace()if !validateKey() {returnnewInvalidMember(), fmt.Errorf("%w: %q", errInvalidKey, ) } := strings.TrimSpace()if !validateValue() {returnnewInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, ) }// Decode a percent-encoded value. , := url.PathUnescape()if != nil {returnnewInvalidMember(), fmt.Errorf("%w: %w", errInvalidValue, ) } := replaceInvalidUTF8Sequences(len(), )returnMember{key: , value: , properties: , hasData: true}, nil}// replaceInvalidUTF8Sequences replaces invalid UTF-8 sequences with '�'.func replaceInvalidUTF8Sequences( int, string) string {ifutf8.ValidString() {return }// W3C baggage spec: // https://github.com/w3c/baggage/blob/8c215efbeebd3fa4b1aceb937a747e56444f22f3/baggage/HTTP_HEADER_FORMAT.md?plain=1#L69varstrings.Builder .Grow()for := 0; < len(); { , := utf8.DecodeRuneInString([:])if == utf8.RuneError && == 1 {// Invalid UTF-8 sequence found, replace it with '�' _, _ = .WriteString("�") } else { _, _ = .WriteRune() } += }return .String()}// validate ensures m conforms to the W3C Baggage specification.// A key must be an ASCII string, returning an error otherwise.func ( Member) () error {if !.hasData {returnfmt.Errorf("%w: %q", errInvalidMember, ) }if !validateBaggageName(.key) {returnfmt.Errorf("%w: %q", errInvalidKey, .key) }if !validateBaggageValue(.value) {returnfmt.Errorf("%w: %q", errInvalidValue, .value) }return .properties.validate()}// Key returns the Member key.func ( Member) () string { return .key }// Value returns the Member value.func ( Member) () string { return .value }// Properties returns a copy of the Member properties.func ( Member) () []Property { return .properties.Copy() }// String encodes Member into a header string compliant with the W3C Baggage// specification.// It would return empty string if the key is invalid with the W3C Baggage// specification. This could happen for a UTF-8 key, as it may contain// invalid characters.func ( Member) () string {// W3C Baggage specification does not allow percent-encoded keys.if !validateKey(.key) {return"" } := .key + keyValueDelimiter + valueEscape(.value)iflen(.properties) > 0 { += propertyDelimiter + .properties.String() }return}// Baggage is a list of baggage members representing the baggage-string as// defined by the W3C Baggage specification.typeBaggagestruct { //nolint:golint list baggage.List}// New returns a new valid Baggage. It returns an error if it results in a// Baggage exceeding limits set in that specification.//// It expects all the provided members to have already been validated.func ( ...Member) (Baggage, error) {iflen() == 0 {returnBaggage{}, nil } := make(baggage.List)for , := range {if !.hasData {returnBaggage{}, errInvalidMember }// OpenTelemetry resolves duplicates by last-one-wins. [.key] = baggage.Item{Value: .value,Properties: .properties.asInternal(), } }// Check member numbers after deduplication.iflen() > maxMembers {returnBaggage{}, errMemberNumber } := Baggage{}if := len(.String()); > maxBytesPerBaggageString {returnBaggage{}, fmt.Errorf("%w: %d", errBaggageBytes, ) }return , nil}// Parse attempts to decode a baggage-string from the passed string. It// returns an error if the input is invalid according to the W3C Baggage// specification.//// If there are duplicate list-members contained in baggage, the last one// defined (reading left-to-right) will be the only one kept. This diverges// from the W3C Baggage specification which allows duplicate list-members, but// conforms to the OpenTelemetry Baggage specification.func ( string) (Baggage, error) {if == "" {returnBaggage{}, nil }if := len(); > maxBytesPerBaggageString {returnBaggage{}, fmt.Errorf("%w: %d", errBaggageBytes, ) } := make(baggage.List)for , := rangestrings.Split(, listDelimiter) { , := parseMember()if != nil {returnBaggage{}, }// OpenTelemetry resolves duplicates by last-one-wins. [.key] = baggage.Item{Value: .value,Properties: .properties.asInternal(), } }// OpenTelemetry does not allow for duplicate list-members, but the W3C // specification does. Now that we have deduplicated, ensure the baggage // does not exceed list-member limits.iflen() > maxMembers {returnBaggage{}, errMemberNumber }returnBaggage{}, nil}// Member returns the baggage list-member identified by key.//// If there is no list-member matching the passed key the returned Member will// be a zero-value Member.// The returned member is not validated, as we assume the validation happened// when it was added to the Baggage.func ( Baggage) ( string) Member { , := .list[]if ! {// We do not need to worry about distinguishing between the situation // where a zero-valued Member is included in the Baggage because a // zero-valued Member is invalid according to the W3C Baggage // specification (it has an empty key).returnnewInvalidMember() }returnMember{key: ,value: .Value,properties: fromInternalProperties(.Properties),hasData: true, }}// Members returns all the baggage list-members.// The order of the returned list-members is not significant.//// The returned members are not validated, as we assume the validation happened// when they were added to the Baggage.func ( Baggage) () []Member {iflen(.list) == 0 {returnnil } := make([]Member, 0, len(.list))for , := range .list { = append(, Member{key: ,value: .Value,properties: fromInternalProperties(.Properties),hasData: true, }) }return}// SetMember returns a copy of the Baggage with the member included. If the// baggage contains a Member with the same key, the existing Member is// replaced.//// If member is invalid according to the W3C Baggage specification, an error// is returned with the original Baggage.func ( Baggage) ( Member) (Baggage, error) {if !.hasData {return , errInvalidMember } := len(.list)if , := .list[.key]; ! { ++ } := make(baggage.List, )for , := range .list {// Do not copy if we are just going to overwrite.if == .key {continue } [] = } [.key] = baggage.Item{Value: .value,Properties: .properties.asInternal(), }returnBaggage{list: }, nil}// DeleteMember returns a copy of the Baggage with the list-member identified// by key removed.func ( Baggage) ( string) Baggage { := len(.list)if , := .list[]; { -- } := make(baggage.List, )for , := range .list {if == {continue } [] = }returnBaggage{list: }}// Len returns the number of list-members in the Baggage.func ( Baggage) () int {returnlen(.list)}// String encodes Baggage into a header string compliant with the W3C Baggage// specification.// It would ignore members where the member key is invalid with the W3C Baggage// specification. This could happen for a UTF-8 key, as it may contain// invalid characters.func ( Baggage) () string { := make([]string, 0, len(.list))for , := range .list { := Member{key: ,value: .Value,properties: fromInternalProperties(.Properties), }.String()// Ignored empty members.if != "" { = append(, ) } }returnstrings.Join(, listDelimiter)}// parsePropertyInternal attempts to decode a Property from the passed string.// It follows the spec at https://www.w3.org/TR/baggage/#definition.func parsePropertyInternal( string) ( Property, bool) {// For the entire function we will use " key = value " as an example. // Attempting to parse the key. // First skip spaces at the beginning "< >key = value " (they could be empty). := skipSpace(, 0)// Parse the key: " <key> = value ". := := for , := range [:] {if !validateKeyChar() {break } ++ }// If we couldn't find any valid key character, // it means the key is either empty or invalid.if == {return }// Skip spaces after the key: " key< >= value ". = skipSpace(, )if == len() {// A key can have no value, like: " key ". = true .key = [:]return }// If we have not reached the end and we can't find the '=' delimiter, // it means the property is invalid.if [] != keyValueDelimiter[0] {return }// Attempting to parse the value. // Match: " key =< >value ". = skipSpace(, +1)// Match the value string: " key = <value> ". // A valid property can be: " key =". // Therefore, we don't have to check if the value is empty. := := for , := range [:] {if !validateValueChar() {break } ++ }// Skip all trailing whitespaces: " key = value< >". = skipSpace(, )// If after looking for the value and skipping whitespaces // we have not reached the end, it means the property is // invalid, something like: " key = value value1".if != len() {return }// Decode a percent-encoded value. := [:] , := url.PathUnescape()if != nil {return } := replaceInvalidUTF8Sequences(len(), ) = true .key = [:] .hasValue = true .value = return}func skipSpace( string, int) int { := for ; < len(); ++ { := []if != ' ' && != '\t' {break } }return}var safeKeyCharset = [utf8.RuneSelf]bool{// 0x23 to 0x27'#': true,'$': true,'%': true,'&': true,'\'': true,// 0x30 to 0x39'0': true,'1': true,'2': true,'3': true,'4': true,'5': true,'6': true,'7': true,'8': true,'9': true,// 0x41 to 0x5a'A': true,'B': true,'C': true,'D': true,'E': true,'F': true,'G': true,'H': true,'I': true,'J': true,'K': true,'L': true,'M': true,'N': true,'O': true,'P': true,'Q': true,'R': true,'S': true,'T': true,'U': true,'V': true,'W': true,'X': true,'Y': true,'Z': true,// 0x5e to 0x7a'^': true,'_': true,'`': true,'a': true,'b': true,'c': true,'d': true,'e': true,'f': true,'g': true,'h': true,'i': true,'j': true,'k': true,'l': true,'m': true,'n': true,'o': true,'p': true,'q': true,'r': true,'s': true,'t': true,'u': true,'v': true,'w': true,'x': true,'y': true,'z': true,// remainder'!': true,'*': true,'+': true,'-': true,'.': true,'|': true,'~': true,}// validateBaggageName checks if the string is a valid OpenTelemetry Baggage name.// Baggage name is a valid, non-empty UTF-8 string.func validateBaggageName( string) bool {if == "" {returnfalse }returnutf8.ValidString()}// validateBaggageValue checks if the string is a valid OpenTelemetry Baggage value.// Baggage value is a valid UTF-8 strings.// Empty string is also a valid UTF-8 string.func validateBaggageValue( string) bool {returnutf8.ValidString()}// validateKey checks if the string is a valid W3C Baggage key.func validateKey( string) bool {if == "" {returnfalse }for , := range {if !validateKeyChar() {returnfalse } }returntrue}func validateKeyChar( int32) bool {return >= 0 && < int32(utf8.RuneSelf) && safeKeyCharset[]}// validateValue checks if the string is a valid W3C Baggage value.func validateValue( string) bool {for , := range {if !validateValueChar() {returnfalse } }returntrue}var safeValueCharset = [utf8.RuneSelf]bool{'!': true, // 0x21// 0x23 to 0x2b'#': true,'$': true,'%': true,'&': true,'\'': true,'(': true,')': true,'*': true,'+': true,// 0x2d to 0x3a'-': true,'.': true,'/': true,'0': true,'1': true,'2': true,'3': true,'4': true,'5': true,'6': true,'7': true,'8': true,'9': true,':': true,// 0x3c to 0x5b'<': true, // 0x3C'=': true, // 0x3D'>': true, // 0x3E'?': true, // 0x3F'@': true, // 0x40'A': true, // 0x41'B': true, // 0x42'C': true, // 0x43'D': true, // 0x44'E': true, // 0x45'F': true, // 0x46'G': true, // 0x47'H': true, // 0x48'I': true, // 0x49'J': true, // 0x4A'K': true, // 0x4B'L': true, // 0x4C'M': true, // 0x4D'N': true, // 0x4E'O': true, // 0x4F'P': true, // 0x50'Q': true, // 0x51'R': true, // 0x52'S': true, // 0x53'T': true, // 0x54'U': true, // 0x55'V': true, // 0x56'W': true, // 0x57'X': true, // 0x58'Y': true, // 0x59'Z': true, // 0x5A'[': true, // 0x5B// 0x5d to 0x7e']': true, // 0x5D'^': true, // 0x5E'_': true, // 0x5F'`': true, // 0x60'a': true, // 0x61'b': true, // 0x62'c': true, // 0x63'd': true, // 0x64'e': true, // 0x65'f': true, // 0x66'g': true, // 0x67'h': true, // 0x68'i': true, // 0x69'j': true, // 0x6A'k': true, // 0x6B'l': true, // 0x6C'm': true, // 0x6D'n': true, // 0x6E'o': true, // 0x6F'p': true, // 0x70'q': true, // 0x71'r': true, // 0x72's': true, // 0x73't': true, // 0x74'u': true, // 0x75'v': true, // 0x76'w': true, // 0x77'x': true, // 0x78'y': true, // 0x79'z': true, // 0x7A'{': true, // 0x7B'|': true, // 0x7C'}': true, // 0x7D'~': true, // 0x7E}func validateValueChar( int32) bool {return >= 0 && < int32(utf8.RuneSelf) && safeValueCharset[]}// valueEscape escapes the string so it can be safely placed inside a baggage value,// replacing special characters with %XX sequences as needed.//// The implementation is based on:// https://github.com/golang/go/blob/f6509cf5cdbb5787061b784973782933c47f1782/src/net/url/url.go#L285.func valueEscape( string) string { := 0for := 0; < len(); ++ { := []ifshouldEscape() { ++ } }if == 0 {return }var [64]bytevar []byte := len() + 2*if <= len() { = [:] } else { = make([]byte, ) } := 0for := 0; < len(); ++ { := []ifshouldEscape([]) {const = "0123456789ABCDEF" [] = '%' [+1] = [>>4] [+2] = [&15] += 3 } else { [] = ++ } }returnstring()}// shouldEscape returns true if the specified byte should be escaped when// appearing in a baggage value string.func shouldEscape( byte) bool {if == '%' {// The percent character must be encoded so that percent-encoding can work.returntrue }return !validateValueChar(int32())}
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.