blob: 84fee1145f8cfa65c8aed5200133821bd5039563 [file] [log] [blame]
// 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 ensure
import (
"bufio"
"fmt"
"io"
"sort"
"strings"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/iotools"
"go.chromium.org/luci/cipd/common"
)
// VersionsFile contains a mapping "(package name, version) -> instance ID" used
// to resolve versions (instead of backend calls) for ensure files that have
// $ResolvedVersions directive.
//
// In serialized form it is represented as a set of triples separated by one
// or more new lines (there must be no new lines inside the triple):
//
// """
// # Comments are allowed, they are skipped (not even considered as '\n').
// <package name>
// <version>
// <resolved instance ID>
//
// <package name>
// <version>
// <resolved instance ID>
// """
//
// Leading and trailing whitespace on a line is ignored.
//
// In the canonical serialization triples are ordered by (package, version).
//
// VersionsFile is safe for read-only concurrent use. Concurrent modifications
// should be protected by a lock. This is just a map in disguise.
type VersionsFile map[unresolvedVer]string
type unresolvedVer struct {
pkg string
ver string
}
// AddVersion adds (or overrides) an instance ID mapped to the given version.
//
// Returns an error if any of the arguments is invalid.
//
// If 'ver' is already an instance ID, just checks if it is equal to 'iid' and
// silently doesn't modify the map.
func (v VersionsFile) AddVersion(pkg, ver, iid string) error {
if err := common.ValidatePackageName(pkg); err != nil {
return err
}
if err := common.ValidateInstanceVersion(ver); err != nil {
return err
}
if err := common.ValidateInstanceID(iid, common.AnyHash); err != nil {
return err
}
if common.ValidateInstanceID(ver, common.AnyHash) == nil {
if ver != iid {
return errors.Reason(
"version given as instance ID (%q) should resolve into that ID, not into %q",
ver, iid).Err()
}
return nil
}
v[unresolvedVer{pkg, ver}] = iid
return nil
}
// ResolveVersion returns a pin matching the given version or an error if such
// version is not in the map.
//
// If 'ver' is already an instance ID, returns it right away.
func (v VersionsFile) ResolveVersion(pkg, ver string) (common.Pin, error) {
if common.ValidateInstanceID(ver, common.AnyHash) == nil {
return common.Pin{PackageName: pkg, InstanceID: ver}, nil
}
if iid, ok := v[unresolvedVer{pkg, ver}]; ok {
return common.Pin{PackageName: pkg, InstanceID: iid}, nil
}
return common.Pin{}, errors.Reason("not in the versions file").Err()
}
// Equal returns true if version files have same entries.
func (v VersionsFile) Equal(a VersionsFile) bool {
if len(v) != len(a) {
return false
}
for ver, iid := range v {
if a[ver] != iid {
return false
}
}
return true
}
// Serialize writes the VersionsFile to an io.Writer in canonical order.
func (v VersionsFile) Serialize(w io.Writer) error {
keys := make([]unresolvedVer, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
l, r := keys[i], keys[j]
if l.pkg != r.pkg {
return l.pkg < r.pkg
}
return l.ver < r.ver
})
_, err := iotools.WriteTracker(w, func(w io.Writer) error {
fmt.Fprintf(w, "# This file is auto-generated by 'cipd ensure-file-resolve'.\n")
fmt.Fprintf(w, "# Do not modify manually. All changes will be overwritten.\n")
for _, key := range keys {
fmt.Fprintf(w, "\n%s\n\t%s\n\t%s\n", key.pkg, key.ver, v[key])
}
return nil
})
return err
}
// ParseVersionsFile parses previously serialized versions file.
func ParseVersionsFile(r io.Reader) (VersionsFile, error) {
res := VersionsFile{}
lineNo := 0
makeError := func(fmtStr string, args ...interface{}) error {
args = append([]interface{}{lineNo}, args...)
return fmt.Errorf("failed to parse versions file (line %d): "+fmtStr, args...)
}
const (
stWaitingPkg = "a package name"
stWaitingVer = "a package version"
stWaitingIID = "an instance ID"
stWaitingNL = "a new line"
)
state := stWaitingPkg
pkg := ""
ver := ""
iid := ""
scanner := bufio.NewScanner(r)
for scanner.Scan() {
lineNo++
line := strings.TrimSpace(scanner.Text())
// Comments are grammatically insignificant (unlike empty lines), so skip
// the completely.
if len(line) > 0 && line[0] == '#' {
continue
}
switch state {
case stWaitingPkg:
if line == "" {
continue // can have more than one empty line between triples
}
pkg = line
if err := common.ValidatePackageName(pkg); err != nil {
return nil, makeError("%s", err)
}
state = stWaitingVer
case stWaitingVer:
if line == "" {
return nil, makeError("expecting a version name, not a new line")
}
ver = line
if err := common.ValidateInstanceVersion(ver); err != nil {
return nil, makeError("%s", err)
}
state = stWaitingIID
case stWaitingIID:
if line == "" {
return nil, makeError("expecting an instance ID, not a new line")
}
iid = line
if err := common.ValidateInstanceID(iid, common.AnyHash); err != nil {
return nil, makeError("%s", err)
}
if err := res.AddVersion(pkg, ver, iid); err != nil {
panic(err) // impossible, everything has been validated already
}
pkg, ver, iid = "", "", ""
state = stWaitingNL
case stWaitingNL:
if line == "" {
state = stWaitingPkg
continue
}
return nil, makeError("expecting an empty line between each version definition triple")
}
}
if state != stWaitingPkg && state != stWaitingNL {
return nil, makeError("unexpected EOF, expecting %s", state)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return res, nil
}