github.com/hugorut/terraform@v1.1.3/src/getproviders/hash.go (about)

     1  package getproviders
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"golang.org/x/mod/sumdb/dirhash"
    12  )
    13  
    14  // Hash is a specially-formatted string representing a checksum of a package
    15  // or the contents of the package.
    16  //
    17  // A Hash string is always starts with a scheme, which is a short series of
    18  // alphanumeric characters followed by a colon, and then the remainder of the
    19  // string has a different meaning depending on the scheme prefix.
    20  //
    21  // The currently-valid schemes are defined as the constants of type HashScheme
    22  // in this package.
    23  //
    24  // Callers outside of this package must not create Hash values via direct
    25  // conversion. Instead, use either the HashScheme.New method on one of the
    26  // HashScheme contents (for a hash of a particular scheme) or the ParseHash
    27  // function (if hashes of any scheme are acceptable).
    28  type Hash string
    29  
    30  // NilHash is the zero value of Hash. It isn't a valid hash, so all of its
    31  // methods will panic.
    32  const NilHash = Hash("")
    33  
    34  // ParseHash parses the string representation of a Hash into a Hash value.
    35  //
    36  // A particular version of Terraform only supports a fixed set of hash schemes,
    37  // but this function intentionally allows unrecognized schemes so that we can
    38  // silently ignore other schemes that may be introduced in the future. For
    39  // that reason, the Scheme method of the returned Hash may return a value that
    40  // isn't in one of the HashScheme constants in this package.
    41  //
    42  // This function doesn't verify that the value portion of the given hash makes
    43  // sense for the given scheme. Invalid values are just considered to not match
    44  // any packages.
    45  //
    46  // If this function returns an error then the returned Hash is invalid and
    47  // must not be used.
    48  func ParseHash(s string) (Hash, error) {
    49  	colon := strings.Index(s, ":")
    50  	if colon < 1 { // 1 because a zero-length scheme is not allowed
    51  		return NilHash, fmt.Errorf("hash string must start with a scheme keyword followed by a colon")
    52  	}
    53  	return Hash(s), nil
    54  }
    55  
    56  // MustParseHash is a wrapper around ParseHash that panics if it returns an
    57  // error.
    58  func MustParseHash(s string) Hash {
    59  	hash, err := ParseHash(s)
    60  	if err != nil {
    61  		panic(err.Error())
    62  	}
    63  	return hash
    64  }
    65  
    66  // Scheme returns the scheme of the recieving hash. If the receiver is not
    67  // using valid syntax then this method will panic.
    68  func (h Hash) Scheme() HashScheme {
    69  	colon := strings.Index(string(h), ":")
    70  	if colon < 0 {
    71  		panic(fmt.Sprintf("invalid hash string %q", h))
    72  	}
    73  	return HashScheme(h[:colon+1])
    74  }
    75  
    76  // HasScheme returns true if the given scheme matches the receiver's scheme,
    77  // or false otherwise.
    78  //
    79  // If the receiver is not using valid syntax then this method will panic.
    80  func (h Hash) HasScheme(want HashScheme) bool {
    81  	return h.Scheme() == want
    82  }
    83  
    84  // Value returns the scheme-specific value from the recieving hash. The
    85  // meaning of this value depends on the scheme.
    86  //
    87  // If the receiver is not using valid syntax then this method will panic.
    88  func (h Hash) Value() string {
    89  	colon := strings.Index(string(h), ":")
    90  	if colon < 0 {
    91  		panic(fmt.Sprintf("invalid hash string %q", h))
    92  	}
    93  	return string(h[colon+1:])
    94  }
    95  
    96  // String returns a string representation of the receiving hash.
    97  func (h Hash) String() string {
    98  	return string(h)
    99  }
   100  
   101  // GoString returns a Go syntax representation of the receiving hash.
   102  //
   103  // This is here primarily to help with producing descriptive test failure
   104  // output; these results are not particularly useful at runtime.
   105  func (h Hash) GoString() string {
   106  	if h == NilHash {
   107  		return "getproviders.NilHash"
   108  	}
   109  	switch scheme := h.Scheme(); scheme {
   110  	case HashScheme1:
   111  		return fmt.Sprintf("getproviders.HashScheme1.New(%q)", h.Value())
   112  	case HashSchemeZip:
   113  		return fmt.Sprintf("getproviders.HashSchemeZip.New(%q)", h.Value())
   114  	default:
   115  		// This fallback is for when we encounter lock files or API responses
   116  		// with hash schemes that the current version of Terraform isn't
   117  		// familiar with. They were presumably introduced in a later version.
   118  		return fmt.Sprintf("getproviders.HashScheme(%q).New(%q)", scheme, h.Value())
   119  	}
   120  }
   121  
   122  // HashScheme is an enumeration of schemes that are allowed for values of type
   123  // Hash.
   124  type HashScheme string
   125  
   126  const (
   127  	// HashScheme1 is the scheme identifier for the first hash scheme.
   128  	//
   129  	// Use HashV1 (or one of its wrapper functions) to calculate hashes with
   130  	// this scheme.
   131  	HashScheme1 HashScheme = HashScheme("h1:")
   132  
   133  	// HashSchemeZip is the scheme identifier for the legacy hash scheme that
   134  	// applies to distribution archives (.zip files) rather than package
   135  	// contents, and can therefore only be verified against the original
   136  	// distribution .zip file, not an extracted directory.
   137  	//
   138  	// Use PackageHashLegacyZipSHA to calculate hashes with this scheme.
   139  	HashSchemeZip HashScheme = HashScheme("zh:")
   140  )
   141  
   142  // New creates a new Hash value with the receiver as its scheme and the given
   143  // raw string as its value.
   144  //
   145  // It's the caller's responsibility to make sure that the given value makes
   146  // sense for the selected scheme.
   147  func (hs HashScheme) New(value string) Hash {
   148  	return Hash(string(hs) + value)
   149  }
   150  
   151  // PackageHash computes a hash of the contents of the package at the given
   152  // location, using whichever hash algorithm is the current default.
   153  //
   154  // Currently, this method returns version 1 hashes as produced by the
   155  // function PackageHashV1, but this function may switch to other versions in
   156  // later releases. Call PackageHashV1 directly if you specifically need a V1
   157  // hash.
   158  //
   159  // PackageHash can be used only with the two local package location types
   160  // PackageLocalDir and PackageLocalArchive, because it needs to access the
   161  // contents of the indicated package in order to compute the hash. If given
   162  // a non-local location this function will always return an error.
   163  func PackageHash(loc PackageLocation) (Hash, error) {
   164  	return PackageHashV1(loc)
   165  }
   166  
   167  // PackageMatchesHash returns true if the package at the given location matches
   168  // the given hash, or false otherwise.
   169  //
   170  // If it cannot read from the given location, or if the given hash is in an
   171  // unsupported format, PackageMatchesHash returns an error.
   172  //
   173  // There is currently only one hash format, as implemented by HashV1. However,
   174  // if others are introduced in future PackageMatchesHash may accept multiple
   175  // formats, and may generate errors for any formats that become obsolete.
   176  //
   177  // PackageMatchesHash can be used only with the two local package location types
   178  // PackageLocalDir and PackageLocalArchive, because it needs to access the
   179  // contents of the indicated package in order to compute the hash. If given
   180  // a non-local location this function will always return an error.
   181  func PackageMatchesHash(loc PackageLocation, want Hash) (bool, error) {
   182  	switch want.Scheme() {
   183  	case HashScheme1:
   184  		got, err := PackageHashV1(loc)
   185  		if err != nil {
   186  			return false, err
   187  		}
   188  		return got == want, nil
   189  	case HashSchemeZip:
   190  		archiveLoc, ok := loc.(PackageLocalArchive)
   191  		if !ok {
   192  			return false, fmt.Errorf(`ziphash scheme ("zh:" prefix) is not supported for unpacked provider packages`)
   193  		}
   194  		got, err := PackageHashLegacyZipSHA(archiveLoc)
   195  		if err != nil {
   196  			return false, err
   197  		}
   198  		return got == want, nil
   199  	default:
   200  		return false, fmt.Errorf("unsupported hash format (this may require a newer version of Terraform)")
   201  	}
   202  }
   203  
   204  // PackageMatchesAnyHash returns true if the package at the given location
   205  // matches at least one of the given hashes, or false otherwise.
   206  //
   207  // If it cannot read from the given location, PackageMatchesAnyHash returns an
   208  // error. Unlike the singular PackageMatchesHash, PackageMatchesAnyHash
   209  // considers unsupported hash formats as successfully non-matching, rather
   210  // than returning an error.
   211  //
   212  // PackageMatchesAnyHash can be used only with the two local package location
   213  // types PackageLocalDir and PackageLocalArchive, because it needs to access the
   214  // contents of the indicated package in order to compute the hash. If given
   215  // a non-local location this function will always return an error.
   216  func PackageMatchesAnyHash(loc PackageLocation, allowed []Hash) (bool, error) {
   217  	// It's likely that we'll have multiple hashes of the same scheme in
   218  	// the "allowed" set, in which case we'll avoid repeatedly re-reading the
   219  	// given package by caching its result for each of the two
   220  	// currently-supported hash formats. These will be NilHash until we
   221  	// encounter the first hash of the corresponding scheme.
   222  	var v1Hash, zipHash Hash
   223  	for _, want := range allowed {
   224  		switch want.Scheme() {
   225  		case HashScheme1:
   226  			if v1Hash == NilHash {
   227  				got, err := PackageHashV1(loc)
   228  				if err != nil {
   229  					return false, err
   230  				}
   231  				v1Hash = got
   232  			}
   233  			if v1Hash == want {
   234  				return true, nil
   235  			}
   236  		case HashSchemeZip:
   237  			archiveLoc, ok := loc.(PackageLocalArchive)
   238  			if !ok {
   239  				// A zip hash can never match an unpacked directory
   240  				continue
   241  			}
   242  			if zipHash == NilHash {
   243  				got, err := PackageHashLegacyZipSHA(archiveLoc)
   244  				if err != nil {
   245  					return false, err
   246  				}
   247  				zipHash = got
   248  			}
   249  			if zipHash == want {
   250  				return true, nil
   251  			}
   252  		default:
   253  			// If it's not a supported format then it can't match.
   254  			continue
   255  		}
   256  	}
   257  	return false, nil
   258  }
   259  
   260  // PreferredHashes examines all of the given hash strings and returns the one
   261  // that the current version of Terraform considers to provide the strongest
   262  // verification.
   263  //
   264  // Returns an empty string if none of the given hashes are of a supported
   265  // format. If PreferredHash returns a non-empty string then it will be one
   266  // of the hash strings in "given", and that hash is the one that must pass
   267  // verification in order for a package to be considered valid.
   268  func PreferredHashes(given []Hash) []Hash {
   269  	// For now this is just filtering for the two hash formats we support,
   270  	// both of which are considered equally "preferred". If we introduce
   271  	// a new scheme like "h2:" in future then, depending on the characteristics
   272  	// of that new version, it might make sense to rework this function so
   273  	// that it only returns "h1:" hashes if the input has no "h2:" hashes,
   274  	// so that h2: is preferred when possible and h1: is only a fallback for
   275  	// interacting with older systems that haven't been updated with the new
   276  	// scheme yet.
   277  
   278  	var ret []Hash
   279  	for _, hash := range given {
   280  		switch hash.Scheme() {
   281  		case HashScheme1, HashSchemeZip:
   282  			ret = append(ret, hash)
   283  		}
   284  	}
   285  	return ret
   286  }
   287  
   288  // PackageHashLegacyZipSHA implements the old provider package hashing scheme
   289  // of taking a SHA256 hash of the containing .zip archive itself, rather than
   290  // of the contents of the archive.
   291  //
   292  // The result is a hash string with the "zh:" prefix, which is intended to
   293  // represent "zip hash". After the prefix is a lowercase-hex encoded SHA256
   294  // checksum, intended to exactly match the formatting used in the registry
   295  // API (apart from the prefix) so that checksums can be more conveniently
   296  // compared by humans.
   297  //
   298  // Because this hashing scheme uses the official provider .zip file as its
   299  // input, it accepts only PackageLocalArchive locations.
   300  func PackageHashLegacyZipSHA(loc PackageLocalArchive) (Hash, error) {
   301  	archivePath, err := filepath.EvalSymlinks(string(loc))
   302  	if err != nil {
   303  		return "", err
   304  	}
   305  
   306  	f, err := os.Open(archivePath)
   307  	if err != nil {
   308  		return "", err
   309  	}
   310  	defer f.Close()
   311  
   312  	h := sha256.New()
   313  	_, err = io.Copy(h, f)
   314  	if err != nil {
   315  		return "", err
   316  	}
   317  
   318  	gotHash := h.Sum(nil)
   319  	return HashSchemeZip.New(fmt.Sprintf("%x", gotHash)), nil
   320  }
   321  
   322  // HashLegacyZipSHAFromSHA is a convenience method to produce the schemed-string
   323  // hash format from an already-calculated hash of a provider .zip archive.
   324  //
   325  // This just adds the "zh:" prefix and encodes the string in hex, so that the
   326  // result is in the same format as PackageHashLegacyZipSHA.
   327  func HashLegacyZipSHAFromSHA(sum [sha256.Size]byte) Hash {
   328  	return HashSchemeZip.New(fmt.Sprintf("%x", sum[:]))
   329  }
   330  
   331  // PackageHashV1 computes a hash of the contents of the package at the given
   332  // location using hash algorithm 1. The resulting Hash is guaranteed to have
   333  // the scheme HashScheme1.
   334  //
   335  // The hash covers the paths to files in the directory and the contents of
   336  // those files. It does not cover other metadata about the files, such as
   337  // permissions.
   338  //
   339  // This function is named "PackageHashV1" in anticipation of other hashing
   340  // algorithms being added in a backward-compatible way in future. The result
   341  // from PackageHashV1 always begins with the prefix "h1:" so that callers can
   342  // distinguish the results of potentially multiple different hash algorithms in
   343  // future.
   344  //
   345  // PackageHashV1 can be used only with the two local package location types
   346  // PackageLocalDir and PackageLocalArchive, because it needs to access the
   347  // contents of the indicated package in order to compute the hash. If given
   348  // a non-local location this function will always return an error.
   349  func PackageHashV1(loc PackageLocation) (Hash, error) {
   350  	// Our HashV1 is really just the Go Modules hash version 1, which is
   351  	// sufficient for our needs and already well-used for identity of
   352  	// Go Modules distribution packages. It is also blocked from incompatible
   353  	// changes by being used in a wide array of go.sum files already.
   354  	//
   355  	// In particular, it also supports computing an equivalent hash from
   356  	// an unpacked zip file, which is not important for Terraform workflow
   357  	// today but is likely to become so in future if we adopt a top-level
   358  	// lockfile mechanism that is intended to be checked in to version control,
   359  	// rather than just a transient lock for a particular local cache directory.
   360  	// (In that case we'd need to check hashes of _packed_ packages, too.)
   361  	//
   362  	// Internally, dirhash.Hash1 produces a string containing a sequence of
   363  	// newline-separated path+filehash pairs for all of the files in the
   364  	// directory, and then finally produces a hash of that string to return.
   365  	// In both cases, the hash algorithm is SHA256.
   366  
   367  	switch loc := loc.(type) {
   368  
   369  	case PackageLocalDir:
   370  		// We'll first dereference a possible symlink at our PackageDir location,
   371  		// as would be created if this package were linked in from another cache.
   372  		packageDir, err := filepath.EvalSymlinks(string(loc))
   373  		if err != nil {
   374  			return "", err
   375  		}
   376  
   377  		// The dirhash.HashDir result is already in our expected h1:...
   378  		// format, so we can just convert directly to Hash.
   379  		s, err := dirhash.HashDir(packageDir, "", dirhash.Hash1)
   380  		return Hash(s), err
   381  
   382  	case PackageLocalArchive:
   383  		archivePath, err := filepath.EvalSymlinks(string(loc))
   384  		if err != nil {
   385  			return "", err
   386  		}
   387  
   388  		// The dirhash.HashDir result is already in our expected h1:...
   389  		// format, so we can just convert directly to Hash.
   390  		s, err := dirhash.HashZip(archivePath, dirhash.Hash1)
   391  		return Hash(s), err
   392  
   393  	default:
   394  		return "", fmt.Errorf("cannot hash package at %s", loc.String())
   395  	}
   396  }
   397  
   398  // Hash computes a hash of the contents of the package at the location
   399  // associated with the reciever, using whichever hash algorithm is the current
   400  // default.
   401  //
   402  // This method will change to use new hash versions as they are introduced
   403  // in future. If you need a specific hash version, call the method for that
   404  // version directly instead, such as HashV1.
   405  //
   406  // Hash can be used only with the two local package location types
   407  // PackageLocalDir and PackageLocalArchive, because it needs to access the
   408  // contents of the indicated package in order to compute the hash. If given
   409  // a non-local location this function will always return an error.
   410  func (m PackageMeta) Hash() (Hash, error) {
   411  	return PackageHash(m.Location)
   412  }
   413  
   414  // MatchesHash returns true if the package at the location associated with
   415  // the receiver matches the given hash, or false otherwise.
   416  //
   417  // If it cannot read from the given location, or if the given hash is in an
   418  // unsupported format, MatchesHash returns an error.
   419  //
   420  // MatchesHash can be used only with the two local package location types
   421  // PackageLocalDir and PackageLocalArchive, because it needs to access the
   422  // contents of the indicated package in order to compute the hash. If given
   423  // a non-local location this function will always return an error.
   424  func (m PackageMeta) MatchesHash(want Hash) (bool, error) {
   425  	return PackageMatchesHash(m.Location, want)
   426  }
   427  
   428  // MatchesAnyHash returns true if the package at the location associated with
   429  // the receiver matches at least one of the given hashes, or false otherwise.
   430  //
   431  // If it cannot read from the given location, MatchesHash returns an error.
   432  // Unlike the signular MatchesHash, MatchesAnyHash considers an unsupported
   433  // hash format to be a successful non-match.
   434  func (m PackageMeta) MatchesAnyHash(acceptable []Hash) (bool, error) {
   435  	return PackageMatchesAnyHash(m.Location, acceptable)
   436  }
   437  
   438  // HashV1 computes a hash of the contents of the package at the location
   439  // associated with the receiver using hash algorithm 1.
   440  //
   441  // The hash covers the paths to files in the directory and the contents of
   442  // those files. It does not cover other metadata about the files, such as
   443  // permissions.
   444  //
   445  // HashV1 can be used only with the two local package location types
   446  // PackageLocalDir and PackageLocalArchive, because it needs to access the
   447  // contents of the indicated package in order to compute the hash. If given
   448  // a non-local location this function will always return an error.
   449  func (m PackageMeta) HashV1() (Hash, error) {
   450  	return PackageHashV1(m.Location)
   451  }