| // 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 ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "os" |
| "path/filepath" |
| "strconv" |
| "strings" |
| |
| "go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/build" |
| "go.fuchsia.dev/fuchsia/src/testing/host-target-testing/ffx" |
| "go.fuchsia.dev/fuchsia/src/testing/host-target-testing/util" |
| "go.fuchsia.dev/fuchsia/tools/lib/logger" |
| "go.fuchsia.dev/fuchsia/tools/lib/osmisc" |
| ) |
| |
| type BlobStore interface { |
| Dir() string |
| BlobPath(ctx context.Context, deliveryBlobType *int, merkle build.MerkleRoot) (string, error) |
| OpenBlob(ctx context.Context, deliveryBlobType *int, merkle build.MerkleRoot) (*os.File, error) |
| BlobSize(ctx context.Context, deliveryBlobType *int, merkle build.MerkleRoot) (uint64, error) |
| } |
| |
| type DirBlobStore struct { |
| dir string |
| } |
| |
| func NewDirBlobStore(dir string) BlobStore { |
| return &DirBlobStore{dir} |
| } |
| |
| func (fs *DirBlobStore) BlobPath(ctx context.Context, deliveryBlobType *int, merkle build.MerkleRoot) (string, error) { |
| if deliveryBlobType == nil { |
| return filepath.Join(fs.dir, merkle.String()), nil |
| } else { |
| return filepath.Join(fs.dir, strconv.Itoa(*deliveryBlobType), merkle.String()), nil |
| } |
| } |
| |
| func (fs *DirBlobStore) OpenBlob(ctx context.Context, deliveryBlobType *int, merkle build.MerkleRoot) (*os.File, error) { |
| path, err := fs.BlobPath(ctx, deliveryBlobType, merkle) |
| if err != nil { |
| return nil, err |
| } |
| return os.Open(path) |
| } |
| |
| func (fs *DirBlobStore) BlobSize(ctx context.Context, deliveryBlobType *int, merkle build.MerkleRoot) (uint64, error) { |
| path, err := fs.BlobPath(ctx, deliveryBlobType, merkle) |
| if err != nil { |
| return 0, err |
| } |
| |
| s, err := os.Stat(path) |
| if err != nil { |
| return 0, err |
| } |
| |
| size := s.Size() |
| if size < 0 { |
| return 0, fmt.Errorf("merkle %s has size less than zero: %d", merkle, size) |
| } |
| |
| return uint64(size), nil |
| } |
| |
| func (fs *DirBlobStore) Dir() string { |
| return fs.dir |
| } |
| |
| type Repository struct { |
| rootDir string |
| metadataDir string |
| blobStore BlobStore |
| ffx *ffx.FFXTool |
| deliveryBlobType *int |
| } |
| |
| type signed struct { |
| Signed targets `json:"signed"` |
| } |
| |
| type targets struct { |
| Targets map[string]targetFile `json:"targets"` |
| } |
| |
| type targetFile struct { |
| Custom custom `json:"custom"` |
| } |
| |
| type custom struct { |
| Merkle string `json:"merkle"` |
| } |
| |
| // NewRepository parses the repository from the specified directory. It returns |
| // an error if the repository does not exist, or it contains malformed metadata. |
| func NewRepository(ctx context.Context, dir string, blobStore BlobStore, ffx *ffx.FFXTool, deliveryBlobType *int) (*Repository, error) { |
| logger.Infof(ctx, "creating a repository for %q and %q", dir, blobStore.Dir()) |
| |
| // The repository may have out of date metadata. This updates the repository to |
| // the latest version so TUF won't complain about the data being old. |
| if err := ffx.RepositoryPublish(ctx, dir, []string{}, "--refresh-root"); err != nil { |
| return nil, err |
| } |
| |
| return &Repository{ |
| rootDir: dir, |
| metadataDir: filepath.Join(dir, "repository"), |
| blobStore: blobStore, |
| ffx: ffx, |
| deliveryBlobType: deliveryBlobType, |
| }, nil |
| } |
| |
| // NewRepositoryFromTar extracts a repository from a tar.gz, and returns a |
| // Repository parsed from it. It returns an error if the repository does not |
| // exist, or contains malformed metadata. |
| func NewRepositoryFromTar(ctx context.Context, dst string, src string, ffx *ffx.FFXTool, deliveryBlobType *int) (*Repository, error) { |
| if err := util.Untar(ctx, dst, src); err != nil { |
| return nil, fmt.Errorf("failed to extract packages: %w", err) |
| } |
| |
| return NewRepository( |
| ctx, |
| filepath.Join(dst, "amber-files"), |
| NewDirBlobStore(filepath.Join(dst, "amber-files", "repository", "blobs")), |
| ffx, |
| deliveryBlobType, |
| ) |
| } |
| |
| // This clones this repository, copying the repository metadata into this |
| // directory. |
| func (r *Repository) CloneIntoDir(ctx context.Context, path string) (*Repository, error) { |
| logger.Infof(ctx, "Cloning repository %s into %s", r.metadataDir, path) |
| |
| // CopyDir wants absolute paths. |
| rootDir, err := filepath.Abs(r.rootDir) |
| if err != nil { |
| return nil, err |
| } |
| |
| if _, err := osmisc.CopyDir(rootDir, path, osmisc.RaiseError); err != nil { |
| return nil, err |
| } |
| |
| return NewRepository(ctx, path, r.blobStore, r.ffx, r.deliveryBlobType) |
| } |
| |
| // OpenPackage opens a package from the repository. |
| func (r *Repository) OpenPackage(ctx context.Context, path string) (Package, error) { |
| // Parse the targets file so we can access packages locally. |
| f, err := os.Open(filepath.Join(r.metadataDir, "targets.json")) |
| if err != nil { |
| return Package{}, err |
| } |
| defer f.Close() |
| |
| var s signed |
| if err = json.NewDecoder(f).Decode(&s); err != nil { |
| return Package{}, err |
| } |
| |
| if target, ok := s.Signed.Targets[path]; ok { |
| merkle, err := build.DecodeMerkleRoot([]byte(target.Custom.Merkle)) |
| if err != nil { |
| return Package{}, fmt.Errorf( |
| "failed to parse package %s merkle %q from TUF: %w", |
| path, |
| merkle, |
| err, |
| ) |
| } |
| |
| return newPackage(ctx, r, path, merkle) |
| } |
| |
| return Package{}, fmt.Errorf("could not find package: %q", path) |
| } |
| |
| func (r *Repository) UncompressedBlobPath(ctx context.Context, merkle build.MerkleRoot) (string, error) { |
| return r.blobStore.BlobPath(ctx, nil, merkle) |
| } |
| |
| func (r *Repository) OpenUncompressedBlob(ctx context.Context, merkle build.MerkleRoot) (*os.File, error) { |
| return r.blobStore.OpenBlob(ctx, nil, merkle) |
| } |
| |
| func (r *Repository) OpenUpdatePackage(ctx context.Context, path string) (*UpdatePackage, error) { |
| p, err := r.OpenPackage(ctx, path) |
| if err != nil { |
| return nil, err |
| } |
| |
| return newUpdatePackage(ctx, r, p) |
| } |
| |
| func (r *Repository) OpenBlob(ctx context.Context, merkle build.MerkleRoot) (*os.File, error) { |
| return r.blobStore.OpenBlob(ctx, r.deliveryBlobType, merkle) |
| } |
| |
| func (r *Repository) BlobSize(ctx context.Context, merkle build.MerkleRoot) (uint64, error) { |
| return r.blobStore.BlobSize(ctx, r.deliveryBlobType, merkle) |
| } |
| |
| func (r *Repository) AlignedBlobSize(ctx context.Context, merkle build.MerkleRoot) (uint64, error) { |
| size, err := r.BlobSize(ctx, merkle) |
| if err != nil { |
| return 0, err |
| } |
| |
| // Align the number to the next block. |
| remainder := size % BlobBlockSize |
| if remainder != 0 { |
| size += BlobBlockSize - remainder |
| } |
| |
| return size, nil |
| |
| } |
| |
| // sumBlobSizes sums up all the blob sizes from the blob store. |
| func (r *Repository) sumAlignedBlobSizes(ctx context.Context, blobs map[build.MerkleRoot]struct{}) (uint64, error) { |
| totalSize := uint64(0) |
| for blob := range blobs { |
| size, err := r.AlignedBlobSize(ctx, blob) |
| if err != nil { |
| return 0, nil |
| } |
| |
| totalSize += size |
| } |
| |
| return totalSize, nil |
| } |
| |
| func (r *Repository) Serve(ctx context.Context, localHostname string, repoName string, repoPort int) (*Server, error) { |
| return newServer(ctx, r.metadataDir, r.blobStore, localHostname, repoName, repoPort) |
| } |
| |
| func (r *Repository) VerifyMatchesAnyUpdateSystemImageMerkle(ctx context.Context, merkle build.MerkleRoot) error { |
| update, err := r.OpenUpdatePackage(ctx, "update/0") |
| if err != nil { |
| return err |
| } |
| |
| systemImage, err := update.OpenPackage(ctx, "system_image/0") |
| if err != nil { |
| return err |
| } |
| if merkle == systemImage.Merkle() { |
| return nil |
| } |
| |
| updatePrime, err := r.OpenUpdatePackage(ctx, "update_prime/0") |
| if err != nil { |
| return err |
| } |
| |
| systemImagePrime, err := updatePrime.OpenPackage(ctx, "system_image/0") |
| if err != nil { |
| return err |
| } |
| if merkle == systemImagePrime.Merkle() { |
| return nil |
| } |
| |
| return fmt.Errorf("expected device to be running a system image of %s or %s, got %s", |
| systemImage.Merkle(), systemImagePrime.Merkle(), merkle) |
| } |
| |
| // CreatePackage creates a package in this repository named `packagePath` by: |
| // * creating a temporary directory |
| // * passing it to the `createFunc` closure. The closure then adds any necessary files. |
| // * creating a package from the directory contents. |
| // * publishing the package to the repository with the `packagePath` path. |
| func (r *Repository) CreatePackage( |
| ctx context.Context, |
| packagePath string, |
| createFunc func(path string) error, |
| ) (Package, error) { |
| logger.Infof(ctx, "creating package %q", packagePath) |
| |
| // Extract the package name from the path. The variant currently is optional, but if specified, must be "0". |
| packageName, packageVariant, found := strings.Cut(packagePath, "/") |
| if found && packageVariant != "0" { |
| return Package{}, fmt.Errorf("invalid package path found: %q", packagePath) |
| } |
| packageVariant = "0" |
| |
| // Create temp directory. The content of this directory will be included in the package. |
| tempDir, err := os.MkdirTemp("", "") |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to create a temp directory: %w", err) |
| } |
| defer os.RemoveAll(tempDir) |
| |
| // Package content will be created by the user by leveraging the createFunc closure. |
| if err := createFunc(tempDir); err != nil { |
| return Package{}, fmt.Errorf("failed to create content of the package: %w", err) |
| } |
| |
| // Create package from the temp directory. The package builder doesn't use |
| // the repository name, so it can be set as `testrepository.com`. |
| pkgBuilder, err := NewPackageBuilderFromDir(tempDir, packageName, packageVariant, "testrepository.com") |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to parse the package from %q: %w", tempDir, err) |
| } |
| |
| // Publish the package and get the merkle of the package. |
| pkg, err := pkgBuilder.Publish(ctx, r) |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to publish the package %q: %w", packagePath, err) |
| } |
| |
| return pkg, nil |
| } |
| |
| // EditPackage takes the content of the source package from srcPackagePath, |
| // copies the content to destination package at dstPackagePath and edits the |
| // content at destination with the help of editFunc closure. |
| func (r *Repository) EditPackage( |
| ctx context.Context, |
| srcPackage Package, |
| dstPackagePath string, |
| editFunc func(path string) error, |
| ) (Package, error) { |
| logger.Infof(ctx, "editing package %q. will create %q", srcPackage.Path(), dstPackagePath) |
| |
| // Next create a destination package based on the content oft the source package. |
| pkg, err := r.CreatePackage(ctx, dstPackagePath, func(tempDir string) error { |
| if err := srcPackage.Expand(ctx, tempDir); err != nil { |
| return fmt.Errorf("failed to expand the package to %s: %w", tempDir, err) |
| } |
| |
| // User can edit the content and return it. |
| return editFunc(tempDir) |
| }) |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to create the package %q: %w", dstPackagePath, err) |
| } |
| |
| return pkg, nil |
| } |
| |
| func (r *Repository) Publish(ctx context.Context, packageManifestPath string) error { |
| extraArgs := []string{"--blob-repo-dir", r.blobStore.Dir()} |
| if r.deliveryBlobType != nil { |
| extraArgs = append(extraArgs, "--delivery-blob-type", fmt.Sprint(*r.deliveryBlobType)) |
| } |
| |
| return r.ffx.RepositoryPublish(ctx, r.rootDir, []string{packageManifestPath}, extraArgs...) |
| } |