// 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 }