blob: c73c1cb985f52342ce55ab0a5a5942def64a86d4 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// 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
}