// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package providerreqs import ( "fmt" "strings" ) // Hash is a specially-formatted string representing a checksum of a package // or the contents of the package. // // A Hash string is always starts with a scheme, which is a short series of // alphanumeric characters followed by a colon, and then the remainder of the // string has a different meaning depending on the scheme prefix. // // The currently-valid schemes are defined as the constants of type HashScheme // in this package. // // Callers outside of this package must not create Hash values via direct // conversion. Instead, use either the HashScheme.New method on one of the // HashScheme contents (for a hash of a particular scheme) or the ParseHash // function (if hashes of any scheme are acceptable). type Hash string // NilHash is the zero value of Hash. It isn't a valid hash, so all of its // methods will panic. const NilHash = Hash("") // ParseHash parses the string representation of a Hash into a Hash value. // // A particular version of Terraform only supports a fixed set of hash schemes, // but this function intentionally allows unrecognized schemes so that we can // silently ignore other schemes that may be introduced in the future. For // that reason, the Scheme method of the returned Hash may return a value that // isn't in one of the HashScheme constants in this package. // // This function doesn't verify that the value portion of the given hash makes // sense for the given scheme. Invalid values are just considered to not match // any packages. // // If this function returns an error then the returned Hash is invalid and // must not be used. func ParseHash(s string) (Hash, error) { colon := strings.Index(s, ":") if colon < 1 { // 1 because a zero-length scheme is not allowed return NilHash, fmt.Errorf("hash string must start with a scheme keyword followed by a colon") } return Hash(s), nil } // MustParseHash is a wrapper around ParseHash that panics if it returns an // error. func MustParseHash(s string) Hash { hash, err := ParseHash(s) if err != nil { panic(err.Error()) } return hash } // Scheme returns the scheme of the recieving hash. If the receiver is not // using valid syntax then this method will panic. func (h Hash) Scheme() HashScheme { colon := strings.Index(string(h), ":") if colon < 0 { panic(fmt.Sprintf("invalid hash string %q", h)) } return HashScheme(h[:colon+1]) } // HasScheme returns true if the given scheme matches the receiver's scheme, // or false otherwise. // // If the receiver is not using valid syntax then this method will panic. func (h Hash) HasScheme(want HashScheme) bool { return h.Scheme() == want } // Value returns the scheme-specific value from the recieving hash. The // meaning of this value depends on the scheme. // // If the receiver is not using valid syntax then this method will panic. func (h Hash) Value() string { colon := strings.Index(string(h), ":") if colon < 0 { panic(fmt.Sprintf("invalid hash string %q", h)) } return string(h[colon+1:]) } // String returns a string representation of the receiving hash. func (h Hash) String() string { return string(h) } // GoString returns a Go syntax representation of the receiving hash. // // This is here primarily to help with producing descriptive test failure // output; these results are not particularly useful at runtime. func (h Hash) GoString() string { if h == NilHash { return "getproviders.NilHash" } switch scheme := h.Scheme(); scheme { case HashScheme1: return fmt.Sprintf("getproviders.HashScheme1.New(%q)", h.Value()) case HashSchemeZip: return fmt.Sprintf("getproviders.HashSchemeZip.New(%q)", h.Value()) default: // This fallback is for when we encounter lock files or API responses // with hash schemes that the current version of Terraform isn't // familiar with. They were presumably introduced in a later version. return fmt.Sprintf("getproviders.HashScheme(%q).New(%q)", scheme, h.Value()) } } // HashScheme is an enumeration of schemes that are allowed for values of type // Hash. type HashScheme string const ( // HashScheme1 is the scheme identifier for the first hash scheme. // // Use HashV1 (or one of its wrapper functions) to calculate hashes with // this scheme. HashScheme1 HashScheme = HashScheme("h1:") // HashSchemeZip is the scheme identifier for the legacy hash scheme that // applies to distribution archives (.zip files) rather than package // contents, and can therefore only be verified against the original // distribution .zip file, not an extracted directory. // // Use PackageHashLegacyZipSHA to calculate hashes with this scheme. HashSchemeZip HashScheme = HashScheme("zh:") ) // New creates a new Hash value with the receiver as its scheme and the given // raw string as its value. // // It's the caller's responsibility to make sure that the given value makes // sense for the selected scheme. func (hs HashScheme) New(value string) Hash { return Hash(string(hs) + value) } // PreferredHashes examines all of the given hash strings and returns the one // that the current version of Terraform considers to provide the strongest // verification. // // Returns an empty string if none of the given hashes are of a supported // format. If PreferredHash returns a non-empty string then it will be one // of the hash strings in "given", and that hash is the one that must pass // verification in order for a package to be considered valid. func PreferredHashes(given []Hash) []Hash { // For now this is just filtering for the two hash formats we support, // both of which are considered equally "preferred". If we introduce // a new scheme like "h2:" in future then, depending on the characteristics // of that new version, it might make sense to rework this function so // that it only returns "h1:" hashes if the input has no "h2:" hashes, // so that h2: is preferred when possible and h1: is only a fallback for // interacting with older systems that haven't been updated with the new // scheme yet. var ret []Hash for _, hash := range given { switch hash.Scheme() { case HashScheme1, HashSchemeZip: ret = append(ret, hash) } } return ret }