| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package statekeys | |
| import ( | |
| "fmt" | |
| ) | |
| // Parse attempts to parse the given string as a state key, and returns the | |
| // result if successful. | |
| // | |
| // A returned error means that the given string is syntactically invalid, | |
| // which could mean either that it doesn't meet the basic requirements for | |
| // any state key, or that it has a recognized key type but the remainder is | |
| // not valid for that type. | |
| // | |
| // Parse DOES NOT return an error for a syntactically-valid key of an | |
| // unrecognized type. Instead, it returns an [UnrecognizedKey] value which | |
| // callers can detect using [RecognizedType], which will return false for | |
| // a key of an unrecognized type. | |
| func Parse(raw string) (Key, error) { | |
| if len(raw) < 4 { | |
| // All state keys must have at least four characters, since that's | |
| // how long a key prefix is. | |
| return nil, fmt.Errorf("too short to be a valid state key") | |
| } | |
| keyType := KeyType(raw[:4]) | |
| remain := raw[4:] | |
| parser := keyParsers[keyType] | |
| if parser == nil { | |
| if !isPlausibleRawKeyType(string(keyType)) { | |
| return nil, fmt.Errorf("invalid key type prefix %q", keyType) | |
| } | |
| return Unrecognized{ | |
| ApparentKeyType: keyType, | |
| remainder: remain, | |
| }, nil | |
| } | |
| return parser(remain) | |
| } | |
| var keyParsers = map[KeyType]func(string) (Key, error){ | |
| ResourceInstanceObjectType: parseResourceInstanceObject, | |
| ComponentInstanceType: parseComponentInstance, | |
| OutputType: parseOutput, | |
| VariableType: parseVariable, | |
| } | |
| // cutKeyField is a key parsing helper for key types that consist of | |
| // multiple fields concatenated together. | |
| // | |
| // cutKeyField returns the raw string content of the next field, and | |
| // also returns any remaining text after the field delimeter which | |
| // could therefore be used in a subsequent call to cutKeyField. | |
| // | |
| // The field delimiter is a comma, but the parser ignores any comma | |
| // that appears to be inside a pair of double-quote characters (") | |
| // so that it's safe to include an address with a string-based instance key | |
| // (which could potentially contain a literal comma) and get back that same | |
| // address as a single field. | |
| // | |
| // If the given string does not contain any delimiters, the result is the | |
| // same string verbatim and an empty "remain" result. | |
| func cutKeyField(raw string) (field, remain string) { | |
| i := keyDelimiterIdx(raw) | |
| if i == -1 { | |
| return raw, "" | |
| } | |
| return raw[:i], raw[i+1:] | |
| } | |
| // finalKeyField returns the given string and true if it doesn't contain a key | |
| // field delimiter, or "", false if the string does have a delimiter. | |
| func finalKeyField(raw string) (string, bool) { | |
| i := keyDelimiterIdx(raw) | |
| if i != -1 { | |
| return "", false | |
| } | |
| return raw, true | |
| } | |
| // keyDelimiterIdx finds the index of the first delimiter in the given | |
| // string, or returns -1 if there is no delimiter in the string. | |
| func keyDelimiterIdx(raw string) int { | |
| inQuotes := false | |
| escape := false | |
| for i, c := range raw { | |
| if c == ',' && !inQuotes { | |
| return i | |
| } | |
| if c == '\\' { | |
| escape = true | |
| continue | |
| } | |
| if c == '"' && !escape { | |
| inQuotes = !inQuotes | |
| } | |
| escape = false | |
| } | |
| // If we fall out here then the entire string seems to be | |
| // a single field, with no delimiters. | |
| return -1 | |
| } |