| // Copyright 2018 The LUCI Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // Package digests holds types used by selfupdate mechanism to pin client |
| // hashes. |
| package digests |
| |
| import ( |
| "bufio" |
| "fmt" |
| "io" |
| "sort" |
| "strings" |
| |
| "google.golang.org/protobuf/proto" |
| |
| "go.chromium.org/luci/common/iotools" |
| |
| api "go.chromium.org/luci/cipd/api/cipd/v1" |
| "go.chromium.org/luci/cipd/common" |
| ) |
| |
| // ClientDigestsFile holds a mapping "platform => hash of the client binary for |
| // given platform", for some particular version of the CIPD client (provided |
| // elsewhere). |
| // |
| // It is used to "lock" the client binary during updates, much in a same way |
| // $ResolvedVersions file is used to "lock" hashes of the packages. Unlike |
| // the latter, the client version file holds digests of the CIPD client binary |
| // itself, not a CIPD package. |
| // |
| // This file is parsed by 'cipd selfupdate' and also by various bootstrap |
| // scripts that fetch the initial copy of the client. For that reason the format |
| // is relatively simple: |
| // |
| // """ |
| // # Comment. |
| // |
| // <platform> <hash algo> <hex digest> |
| // ... |
| // """ |
| // |
| // Where <platform> is one of ${platform} values (e.g. "linux-amd64"), and |
| // <hash algo> is one of stringified case-insensitive HashAlgo enum values from |
| // api/cas.proto (e.g. "sha256"). |
| // |
| // Comments are allowed and must occupy their own line. Empty new lines are |
| // skipped. All non-empty lines have 3 fields (with any number of whitespace |
| // characters between fields). |
| // |
| // Order of lines is not significant. |
| type ClientDigestsFile struct { |
| entries []clientDigestEntry |
| } |
| |
| type clientDigestEntry struct { |
| plat string |
| ref *api.ObjectRef |
| } |
| |
| // AddClientRef appends the client's digest given as ObjectRef. |
| // |
| // Returns an error (platform, hash algo) combination has already been added or |
| // the hash is unrecognized. |
| func (d *ClientDigestsFile) AddClientRef(plat string, ref *api.ObjectRef) error { |
| if err := common.ValidateObjectRef(ref, common.KnownHash); err != nil { |
| return err |
| } |
| for _, e := range d.entries { |
| if e.plat == plat && e.ref.HashAlgo == ref.HashAlgo { |
| return fmt.Errorf("%s hash for %s has already been added", ref.HashAlgo, plat) |
| } |
| } |
| d.entries = append(d.entries, clientDigestEntry{plat, ref}) |
| return nil |
| } |
| |
| // ClientRef returns an expected client ObjectRef for the given platform. |
| // |
| // Returns the best hash (higher algo number) or nil if there are no digests |
| // for this platform at all. |
| func (d *ClientDigestsFile) ClientRef(plat string) (ref *api.ObjectRef) { |
| for _, e := range d.entries { |
| if e.plat == plat && (ref == nil || e.ref.HashAlgo > ref.HashAlgo) { |
| ref = e.ref |
| } |
| } |
| return |
| } |
| |
| // Contains returns true if the given ref is among refs for the given platform. |
| // |
| // Compares 'ref' to all hashes corresponding to 'plat', not only the best one. |
| func (d *ClientDigestsFile) Contains(plat string, ref *api.ObjectRef) bool { |
| for _, e := range d.entries { |
| if e.plat == plat && proto.Equal(ref, e.ref) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // Sort orders the entries by (platform, -hashAlgo). |
| func (d *ClientDigestsFile) Sort() { |
| sort.Slice(d.entries, func(i, j int) bool { |
| l, r := d.entries[i], d.entries[j] |
| if l.plat != r.plat { |
| return l.plat < r.plat |
| } |
| return l.ref.HashAlgo > r.ref.HashAlgo // more recent algos first |
| }) |
| } |
| |
| // Equal returns true if files have same entries in same order. |
| func (d *ClientDigestsFile) Equal(a *ClientDigestsFile) bool { |
| if len(d.entries) != len(a.entries) { |
| return false |
| } |
| for i, l := range d.entries { |
| if r := a.entries[i]; l.plat != r.plat || !proto.Equal(l.ref, r.ref) { |
| return false |
| } |
| } |
| return true |
| } |
| |
| // Serialize writes the ClientDigestsFile to an io.Writer. |
| // |
| // 'version' and 'versionFile' are used to construct a meaningful comment |
| // footer. |
| func (d *ClientDigestsFile) Serialize(w io.Writer, version, versionFile string) error { |
| _, err := iotools.WriteTracker(w, func(w io.Writer) error { |
| fmt.Fprintf(w, "# This file was generated by\n") |
| fmt.Fprintf(w, "#\n") |
| fmt.Fprintf(w, "# cipd selfupdate-roll -version-file %s \\\n", versionFile) |
| fmt.Fprintf(w, "# -version %s\n", version) |
| fmt.Fprintf(w, "#\n") |
| fmt.Fprintf(w, "# Do not modify manually. All changes will be overwritten.\n") |
| fmt.Fprintf(w, "# Use 'cipd selfupdate-roll ...' to modify.\n\n") |
| |
| // Align fields nicely. |
| max := []int{0, 0} |
| for _, e := range d.entries { |
| if l := len(e.plat); l > max[0] { |
| max[0] = l |
| } |
| if l := len(e.ref.HashAlgo.String()); l > max[1] { |
| max[1] = l |
| } |
| } |
| |
| for _, e := range d.entries { |
| algo := strings.ToLower(e.ref.HashAlgo.String()) |
| fmt.Fprintf(w, "%s%s%s%s%s\n", |
| e.plat, strings.Repeat(" ", max[0]+2-len(e.plat)), |
| algo, strings.Repeat(" ", max[1]+2-len(algo)), |
| e.ref.HexDigest, |
| ) |
| } |
| |
| return nil |
| }) |
| return err |
| } |
| |
| // ParseClientDigestsFile parses previously serialized client digests file. |
| // |
| // Unrecognized algorithms are silently skipped, to be compatible with files |
| // generated by the future versions of CIPD that may use different algorithms. |
| func ParseClientDigestsFile(r io.Reader) (*ClientDigestsFile, error) { |
| res := &ClientDigestsFile{} |
| |
| lineNo := 0 |
| makeError := func(fmtStr string, args ...interface{}) error { |
| args = append([]interface{}{lineNo}, args...) |
| return fmt.Errorf("failed to parse client digests file (line %d): "+fmtStr, args...) |
| } |
| |
| scanner := bufio.NewScanner(r) |
| for scanner.Scan() { |
| lineNo++ |
| |
| line := strings.TrimSpace(scanner.Text()) |
| if line == "" || line[0] == '#' { |
| continue |
| } |
| |
| tokens := strings.Fields(line) |
| if len(tokens) != 3 { |
| return nil, makeError("each line must have format \"<platform> <algo> <digest>\"") |
| } |
| |
| algoIdx := api.HashAlgo_value[strings.ToUpper(tokens[1])] |
| if algoIdx == 0 { |
| continue // skip unknown algorithms |
| } |
| ref := &api.ObjectRef{ |
| HashAlgo: api.HashAlgo(algoIdx), |
| HexDigest: tokens[2], |
| } |
| if err := common.ValidateObjectRef(ref, common.KnownHash); err != nil { |
| return nil, makeError("%s", err) |
| } |
| |
| if err := res.AddClientRef(tokens[0], ref); err != nil { |
| return nil, makeError("%s", err) |
| } |
| } |
| |
| if err := scanner.Err(); err != nil { |
| return nil, err |
| } |
| return res, nil |
| } |