blob: 0a8ca53113d20e229bcea62f608feba4c57d74d4 [file] [log] [blame]
// Copyright 2019 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 packages
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/build"
far "go.fuchsia.dev/fuchsia/src/sys/pkg/lib/far/go"
"go.fuchsia.dev/fuchsia/src/sys/pkg/lib/merkle"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
)
const (
BlobBlockSize = 8192
maxBlobFileSize = BlobBlockSize * 1000
)
type FileData []byte
type Package struct {
merkle build.MerkleRoot
repo *Repository
path string
contents build.MetaContents
subpackages map[string]build.MerkleRoot
}
// newPackage extracts out a package from the repository.
func newPackage(
ctx context.Context,
repo *Repository,
path string,
merkle build.MerkleRoot,
) (Package, error) {
// Need to parse out the package meta.far to find the package contents.
blob, err := repo.OpenUncompressedBlob(ctx, merkle)
if err != nil {
return Package{}, err
}
defer blob.Close()
f, err := far.NewReader(blob)
if err != nil {
return Package{}, err
}
defer f.Close()
b, err := f.ReadFile("meta/contents")
if err != nil {
return Package{}, err
}
contents, err := build.ParseMetaContents(bytes.NewReader(b))
if err != nil {
return Package{}, err
}
subpackages := make(map[string]build.MerkleRoot)
if b, err := f.ReadFile("meta/fuchsia.pkg/subpackages"); err == nil {
var metaSubpackages *build.MetaSubpackages
if err := json.Unmarshal(b, &metaSubpackages); err != nil {
return Package{}, fmt.Errorf("Unable to parse subpackage for package %s: %w", merkle, err)
}
for path, merkleString := range metaSubpackages.Subpackages {
merkle, err := build.DecodeMerkleRoot([]byte(merkleString))
if err != nil {
return Package{}, err
}
subpackages[path] = merkle
}
}
return Package{
merkle: merkle,
repo: repo,
path: path,
contents: contents,
subpackages: subpackages,
}, nil
}
// Repository returns the repository that contains this package.
func (p *Package) Repository() *Repository {
return p.repo
}
func (p *Package) String() string {
return fmt.Sprintf("%s @ %s", p.Path(), p.Merkle())
}
// Path returns the package path.
func (p *Package) Path() string {
return p.path
}
// Merkle returns the meta.far merkle.
func (p *Package) Merkle() build.MerkleRoot {
return p.merkle
}
// Returns the package's blobs.
func (p *Package) Blobs() map[build.MerkleRoot]struct{} {
blobs := make(map[build.MerkleRoot]struct{})
blobs[p.merkle] = struct{}{}
for _, merkle := range p.contents {
blobs[merkle] = struct{}{}
}
return blobs
}
// Returns the package's subpackages.
func (p *Package) Subpackages() map[string]build.MerkleRoot {
subpackages := make(map[string]build.MerkleRoot)
for path, merkle := range p.subpackages {
subpackages[path] = merkle
}
return subpackages
}
// Open opens a file in the package.
func (p *Package) Open(ctx context.Context, path string) (*os.File, error) {
merkle, ok := p.contents[path]
if !ok {
return nil, os.ErrNotExist
}
return p.repo.OpenUncompressedBlob(ctx, merkle)
}
func (p *Package) FilePath(ctx context.Context, path string) (string, error) {
merkle, ok := p.contents[path]
if !ok {
return "", os.ErrNotExist
}
return p.repo.UncompressedBlobPath(ctx, merkle)
}
// ReadFile reads a file from a package.
func (p *Package) ReadFile(ctx context.Context, path string) ([]byte, error) {
r, err := p.Open(ctx, path)
if err != nil {
return nil, err
}
return io.ReadAll(r)
}
func (p *Package) Expand(ctx context.Context, dir string) error {
for path := range p.contents {
data, err := p.ReadFile(ctx, path)
if err != nil {
return fmt.Errorf("invalid path. %w", err)
}
realPath := filepath.Join(dir, path)
if err := os.MkdirAll(filepath.Dir(realPath), 0755); err != nil {
return fmt.Errorf("could not create parent directories for %s, %w", realPath, err)
}
if err = os.WriteFile(realPath, data, 0644); err != nil {
return fmt.Errorf("could not export %s to %s. %w", path, realPath, err)
}
}
blob, err := p.repo.OpenUncompressedBlob(ctx, p.merkle)
if err != nil {
return fmt.Errorf("failed to open meta.far blob. %w", err)
}
defer blob.Close()
f, err := far.NewReader(blob)
if err != nil {
return fmt.Errorf("failed to open reader on blob. %w", err)
}
defer f.Close()
for _, path := range f.List() {
data, err := f.ReadFile(path)
if err != nil {
fmt.Errorf("failed to read %s. %w", path, err)
}
realPath := filepath.Join(dir, path)
if err := os.MkdirAll(filepath.Dir(realPath), 0755); err != nil {
return fmt.Errorf("could not create parent directories for %s, %w", realPath, err)
}
if err = os.WriteFile(realPath, data, 0644); err != nil {
fmt.Errorf("failed to write file %s. %w", realPath, err)
}
}
return nil
}
func (p *Package) EditContents(
ctx context.Context,
dstPath string,
editFunc func(tempDir string) error,
) (Package, error) {
return p.repo.EditPackage(ctx, *p, dstPath, editFunc)
}
// TransitiveBlobs returns all the blobs in this package and subpackages.
func (p *Package) TransitiveBlobs(ctx context.Context) (map[build.MerkleRoot]struct{}, error) {
visitedPackages := make(map[build.MerkleRoot]struct{})
blobs := make(map[build.MerkleRoot]struct{})
if err := p.transitiveBlobs(ctx, visitedPackages, blobs); err != nil {
return nil, err
}
return blobs, nil
}
func (p *Package) transitiveBlobs(
ctx context.Context,
visitedPackages map[build.MerkleRoot]struct{},
blobs map[build.MerkleRoot]struct{},
) error {
// Exit early if we've already processed this package.
if _, ok := visitedPackages[p.merkle]; ok {
return nil
}
visitedPackages[p.merkle] = struct{}{}
for blob := range p.Blobs() {
blobs[blob] = struct{}{}
}
// Recurse into any subpackages.
for path, merkle := range p.Subpackages() {
pkg, err := newPackage(ctx, p.repo, path, merkle)
if err != nil {
return err
}
if err := pkg.transitiveBlobs(ctx, visitedPackages, blobs); err != nil {
return err
}
}
return nil
}
// TransitiveBlobSize computes total size of all the blobs in this package and subpackages.
func (p *Package) TransitiveBlobSize(ctx context.Context) (uint64, error) {
blobs, err := p.TransitiveBlobs(ctx)
if err != nil {
return 0, err
}
return p.repo.sumAlignedBlobSizes(ctx, blobs)
}
// AddRandomFilesWithAdditionalBytes will add random files to this package that
// will increase the package size with `bytesToAdd` additional bytes.
func (p *Package) AddRandomFilesWithAdditionalBytes(
ctx context.Context,
rand *rand.Rand,
dstPath string,
bytesToAdd uint64,
) (Package, error) {
logger.Infof(ctx, "Adding %d bytes to package %s", bytesToAdd, p.path)
return p.EditContents(ctx, dstPath, func(tempDir string) error {
otaTestDir := filepath.Join(tempDir, "ota-test")
if err := os.Mkdir(otaTestDir, 0700); err != nil {
return fmt.Errorf("failed to create %s: %w", otaTestDir, err)
}
for bytesToAdd > 0 {
// Create blobs up to the max blob size.
var blobSize uint64
if maxBlobFileSize < bytesToAdd {
blobSize = maxBlobFileSize
} else {
blobSize = bytesToAdd
}
if err := generateRandomFile(ctx, rand, otaTestDir, blobSize); err != nil {
return err
}
bytesToAdd -= blobSize
}
return nil
})
}
// AddRandomFilesWithUpperBound will add random files to this package that
// increases the package size up to `maxSize` bytes.
func (p *Package) AddRandomFilesWithUpperBound(
ctx context.Context,
rand *rand.Rand,
dstPath string,
maxSize uint64,
) (Package, error) {
return p.addRandomFilesWithUpperBound(
ctx,
rand,
dstPath,
maxSize,
map[build.MerkleRoot]struct{}{},
)
}
func (p *Package) addRandomFilesWithUpperBound(
ctx context.Context,
rand *rand.Rand,
dstPath string,
maxSize uint64,
additionalBlobs map[build.MerkleRoot]struct{},
) (Package, error) {
// Merge the package blobs with the additional blobs to compute the
// initial size.
packageBlobs, err := p.TransitiveBlobs(ctx)
if err != nil {
return Package{}, err
}
initialBlobs := make(map[build.MerkleRoot]struct{})
for blob := range packageBlobs {
initialBlobs[blob] = struct{}{}
}
for blob := range additionalBlobs {
initialBlobs[blob] = struct{}{}
}
initialSize, err := p.repo.sumAlignedBlobSizes(ctx, initialBlobs)
if err != nil {
return Package{}, err
}
logger.Infof(
ctx,
"Trying to grow package %s with %d blobs and size %d to up to %d bytes",
p.path,
len(initialBlobs),
initialSize,
maxSize,
)
if maxSize < initialSize {
return Package{}, fmt.Errorf(
"initial package %s with size %d is greater than the max size %d",
p.path,
initialSize,
maxSize,
)
}
bytesToAdd := maxSize - initialSize
for bytesToAdd != 0 {
dstPackage, err := p.AddRandomFilesWithAdditionalBytes(
ctx,
rand,
dstPath,
bytesToAdd,
)
if err != nil {
return Package{}, fmt.Errorf(
"failed to create the package %s: %w",
dstPath,
err,
)
}
// Find all the blobs we just added to the package.
blobs, err := dstPackage.TransitiveBlobs(ctx)
if err != nil {
return Package{}, err
}
// Merge the package blobs with our initial blob set, since there could
// be external blobs we want to include, such as from the system image
// packages or the update images.
for blob := range additionalBlobs {
blobs[blob] = struct{}{}
}
size, err := p.repo.sumAlignedBlobSizes(ctx, blobs)
if err != nil {
return Package{}, err
}
// Otherwise reduce the amount of bytes we're trying to add, and try
// again.
logger.Infof(
ctx,
"Generated package %s by adding %d bytes, produced package size %d",
dstPackage.Path(),
bytesToAdd,
size,
)
if size <= maxSize {
logger.Infof(
ctx,
"Accepting package %s with size %d is smaller than %d",
dstPackage.Path(),
size,
maxSize,
)
return dstPackage, nil
}
// Otherwise we overshot our target.
delta := size - maxSize
logger.Warningf(
ctx,
"Generated package %s overshot %d by %d.",
dstPackage.Path(),
maxSize,
delta,
)
// Error out if we failed to add a single byte.
if bytesToAdd == 1 {
break
}
// Otherwise, subtract some bytes from the amount we're trying to add to
// the package.
if delta < bytesToAdd {
bytesToAdd -= delta
} else {
// If our delta is larger than the amount of bytes we're trying to
// add, check if just adding a single byte is sufficient.
bytesToAdd = 1
}
}
return Package{}, fmt.Errorf(
"failed to generate package %s with size less than %d",
dstPath,
maxSize,
)
}
func generateRandomFile(
ctx context.Context,
rand *rand.Rand,
dir string,
blobSize uint64,
) error {
b := make([]byte, blobSize)
if _, err := rand.Read(b); err != nil {
return fmt.Errorf("failed to read %d random bytes: %w", blobSize, err)
}
var tree merkle.Tree
if _, err := tree.ReadFrom(bytes.NewReader(b)); err != nil {
return fmt.Errorf("failed to compute merkle: %w", err)
}
merkle := hex.EncodeToString(tree.Root())
blobPath := filepath.Join(dir, merkle)
if err := os.WriteFile(blobPath, b, 0600); err != nil {
return fmt.Errorf("failed to write %s: %w", blobPath, err)
}
logger.Infof(ctx, "generated blob %s with %d size", merkle, blobSize)
return nil
}