blob: b91d92f66acb31ec240c914acb10be74733265db [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package build
import (
"encoding/json"
"fmt"
"os"
"regexp"
"sort"
"strings"
)
// Snapshot contains metadata from one or more packages
type Snapshot struct {
Packages map[string]Package `json:"packages"`
Blobs map[MerkleRoot]BlobInfo `json:"blobs"`
}
// BlobInfo contains metadata about a single blob
type BlobInfo struct {
Size uint64 `json:"size"`
}
// Package contains metadata about a package, including a list of all files in
// the package
type Package struct {
Files map[string]MerkleRoot `json:"files"`
Tags []string `json:"tags,omitempty"`
}
// String equality with basic wildcard support. "*" can match any number of
// characters. All other characters in each glob are matched literally. A
// string matches globs if it matches any glob.
func compileGlobs(globs []string) *regexp.Regexp {
parts := []string{}
for _, glob := range globs {
globParts := []string{}
for _, part := range strings.Split(glob, "*") {
globParts = append(globParts, regexp.QuoteMeta(part))
}
parts = append(parts, strings.Join(globParts, ".*"))
}
s := fmt.Sprintf("^(%s)$", strings.Join(parts, "|"))
return regexp.MustCompile(s)
}
func anyMatchRegexp(tags []string, re *regexp.Regexp) bool {
for _, tag := range tags {
if re.MatchString(tag) {
return true
}
}
return false
}
// LoadSnapshot reads and verifies a JSON formatted Snapshot from the provided
// path.
func LoadSnapshot(path string) (Snapshot, error) {
data, err := os.ReadFile(path)
if err != nil {
return Snapshot{}, err
}
return ParseSnapshot(data)
}
// ParseSnapshot deserializes and verifies a JSON formatted Snapshot from the
// provided data.
func ParseSnapshot(jsonData []byte) (Snapshot, error) {
var s Snapshot
if err := json.Unmarshal(jsonData, &s); err != nil {
return s, err
}
if err := s.Verify(); err != nil {
return s, err
}
return s, nil
}
// AddPackage adds the metadata for a single package to the given package
// snapshot, detecting and reporting any inconsistencies with the provided
// metadata.
func (s *Snapshot) AddPackage(name string, blobs []PackageBlobInfo, tags []string) error {
pkg := Package{
make(map[string]MerkleRoot),
tags,
}
newBlobs := make(map[MerkleRoot]BlobInfo)
for _, blob := range blobs {
pkg.Files[blob.Path] = blob.Merkle
info := BlobInfo{blob.Size}
if dup, ok := s.Blobs[blob.Merkle]; ok && dup != info {
return fmt.Errorf("snapshot contains inconsistent blob metadata for %q (%v != %v)", blob.Merkle, dup, info)
}
newBlobs[blob.Merkle] = info
}
if dup, ok := s.Packages[name]; ok {
allSame := true
for name, merkle := range dup.Files {
if pkg.Files[name] != merkle {
allSame = false
break
}
}
if len(dup.Files) != len(pkg.Files) || !allSame {
return fmt.Errorf("snapshot contains more than one package called %q (%v, %v)", name, dup, pkg)
}
allTags := make(map[string]struct{})
for _, tag := range pkg.Tags {
allTags[tag] = struct{}{}
}
for _, tag := range dup.Tags {
allTags[tag] = struct{}{}
}
pkg.Tags = make([]string, 0, len(allTags))
for tag := range allTags {
pkg.Tags = append(pkg.Tags, tag)
}
sort.Strings(pkg.Tags)
}
s.Packages[name] = pkg
for merkle, info := range newBlobs {
s.Blobs[merkle] = info
}
return nil
}
// Verify determines if the snapshot is internally consistent. Specifically, it
// ensures that no package references a blob that does not have metadata and
// that the snapshot does not contain blob metadata that is not referenced by
// any package.
func (s *Snapshot) Verify() error {
blobs := map[MerkleRoot]struct{}{}
for name, pkg := range s.Packages {
for path, merkle := range pkg.Files {
blobs[merkle] = struct{}{}
if _, ok := s.Blobs[merkle]; !ok {
return fmt.Errorf("%s/%s references blob %v, but the blob does not exist", name, path, merkle)
}
}
}
for merkle := range s.Blobs {
if _, ok := blobs[merkle]; !ok {
return fmt.Errorf("snapshot contains blob %v, but no package references it", merkle)
}
}
return nil
}
// Filter produces a subset of the Snapshot based on a series of include and
// exclude filters. In order for a package to be included in the result, its tags
// (including the package name itself) must match at least one include filter
// and must not match any exclude filters. Include and exclude filters may
// contain wildcard characters ("*"), which will match 0 or more characters.
func (s *Snapshot) Filter(include []string, exclude []string) Snapshot {
res := Snapshot{
Packages: map[string]Package{},
Blobs: map[MerkleRoot]BlobInfo{},
}
includeGlobs := compileGlobs(include)
excludeGlobs := compileGlobs(exclude)
for name, pkg := range s.Packages {
tags := append(pkg.Tags, name)
if anyMatchRegexp(tags, includeGlobs) && !anyMatchRegexp(tags, excludeGlobs) {
res.Packages[name] = pkg
for _, merkle := range pkg.Files {
res.Blobs[merkle] = s.Blobs[merkle]
}
}
}
return res
}
// Size determines the storage required for all blobs in the snapshot (not
// including block alignment or filesystem overhead).
func (s *Snapshot) Size() uint64 {
var res uint64
for _, blob := range s.Blobs {
res += blob.Size
}
return res
}