blob: ac412114b0b3b6dc78d481a0947a69ac272f1c1f [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.
// +build !build_with_native_toolchain
package pkgfs
import (
"bytes"
"encoding/json"
"log"
"os"
"regexp"
"strings"
"syscall/zx"
"time"
"go.fuchsia.dev/fuchsia/src/lib/thinfs/fs"
"go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/pkg"
"go.fuchsia.dev/fuchsia/src/sys/pkg/lib/far/go"
)
const (
identityBlob = "15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b"
)
var merklePat = regexp.MustCompile("^[0-9a-f]{64}$")
/*
The following VNodes are defined in this file:
/install - installDir
/install/pkg - installPkgDir
/install/pkg/{f} - installFile{isPkg:true}
/install/blob - installBlobDir
/install/blob/{f} - installFile{isPkg:false}
*/
// installDir is located at /install.
type installDir struct {
unsupportedDirectory
fs *Filesystem
}
func (d *installDir) Dup() (fs.Directory, error) {
return d, nil
}
func (d *installDir) Stat() (int64, time.Time, time.Time, error) {
return 0, d.fs.mountTime, d.fs.mountTime, nil
}
func (d *installDir) Open(name string, flags fs.OpenFlags) (fs.File, fs.Directory, *fs.Remote, error) {
name = clean(name)
if name == "" {
return nil, d, nil, nil
}
parts := strings.SplitN(name, "/", 2)
var nd fs.Directory
switch parts[0] {
case "pkg":
nd = &installPkgDir{fs: d.fs}
case "blob":
nd = &installBlobDir{fs: d.fs}
default:
return nil, nil, nil, fs.ErrNotFound
}
if len(parts) == 1 {
if flags.Directory() {
return nil, nd, nil, nil
}
return nil, nd, nil, fs.ErrNotSupported
}
return nd.Open(parts[1], flags)
}
func (d *installDir) Read() ([]fs.Dirent, error) {
return []fs.Dirent{dirDirEnt("pkg"), dirDirEnt("blob")}, nil
}
func (d *installDir) Close() error {
return nil
}
// installPkgDir is located at /install/pkg
type installPkgDir struct {
unsupportedDirectory
fs *Filesystem
}
func (d *installPkgDir) Dup() (fs.Directory, error) {
return d, nil
}
func (d *installPkgDir) Stat() (int64, time.Time, time.Time, error) {
return 0, d.fs.mountTime, d.fs.mountTime, nil
}
func (d *installPkgDir) Open(name string, flags fs.OpenFlags) (fs.File, fs.Directory, *fs.Remote, error) {
name = clean(name)
if name == "" {
return nil, d, nil, nil
}
if !flags.Create() {
return nil, nil, nil, fs.ErrNotSupported
}
f := &installFile{fs: d.fs, name: name, isPkg: true}
err := f.open()
return f, nil, nil, err
}
func (d *installPkgDir) Read() ([]fs.Dirent, error) {
return []fs.Dirent{}, nil
}
func (d *installPkgDir) Close() error {
return nil
}
// installBlobDir is located at /install/blob
type installBlobDir struct {
unsupportedDirectory
fs *Filesystem
}
func (d *installBlobDir) Dup() (fs.Directory, error) {
return d, nil
}
func (d *installBlobDir) Stat() (int64, time.Time, time.Time, error) {
return 0, d.fs.mountTime, d.fs.mountTime, nil
}
func (d *installBlobDir) Open(name string, flags fs.OpenFlags) (fs.File, fs.Directory, *fs.Remote, error) {
name = clean(name)
if name == "" {
return nil, d, nil, nil
}
// TODO(raggi): support write resumption..
if !flags.Create() {
return nil, nil, nil, fs.ErrNotSupported
}
f := &installFile{fs: d.fs, name: name, isPkg: false}
var err error
if !flags.Path() {
err = f.open()
}
return f, nil, nil, err
}
func (d *installBlobDir) Read() ([]fs.Dirent, error) {
return []fs.Dirent{}, nil
}
func (d *installBlobDir) Close() error {
return nil
}
type installFile struct {
unsupportedFile
fs *Filesystem
isPkg bool
size uint64
written uint64
name string
blob *os.File
}
func (f *installFile) open() error {
if !merklePat.Match([]byte(f.name)) {
return fs.ErrInvalidArgs
}
var err error
// TODO(raggi): propagate flags instead to allow for resumption and so on
f.blob, err = f.fs.blobfs.OpenFile(f.name, os.O_WRONLY|os.O_CREATE, 0777)
// When opening a blob for write, blobfs returns a permission error if
// the blob is in the process of being written or already exists. If we
// can confirm the blob is readable, report to the caller that it
// already exists. Otherwise, bubble the error to the caller without
// fulfilling the need.
if os.IsPermission(err) {
if !f.fs.blobfs.HasBlob(f.name) {
return goErrToFSErr(err)
}
// "Fulfill" any needs against the blob that was attempted to be written.
f.fs.index.Fulfill(f.name)
if f.isPkg {
// Importing a package file that is already present in blobfs could fail for
// a number of reasons, such as the package being invalid, and so on. We need
// to report such cases back to the caller. In the case where we import fine,
// we must fall through to passing ErrAlreadyExists back to the caller, so
// that they know that the package file itself is already complete. Getting
// `fs.ErrAlreadyExists` on a package meta.far write does not in and of
// itself indicate that the whole package is present, only that the package
// metadata blob is present.
if err := f.importPackage(); err != nil {
return err
}
}
return fs.ErrAlreadyExists
}
if err != nil {
return goErrToFSErr(err)
}
if f.isPkg {
f.fs.index.Installing(f.name)
}
return nil
}
func (f *installFile) Write(p []byte, off int64, whence int) (int, error) {
var err error
n := 0
if whence != fs.WhenceFromCurrent || off != 0 {
err = &zx.Error{Status: zx.ErrNotSupported}
return n, goErrToFSErr(err)
}
// It is illegal to write past the truncated size of a blob.
if f.written > f.size {
err = &zx.Error{Status: zx.ErrInvalidArgs}
return n, goErrToFSErr(err)
}
n, err = f.blob.Write(p)
f.written += uint64(n)
if f.written >= f.size && err == nil {
// "Fulfill" any needs against the blob that was attempted to be written.
f.fs.index.Fulfill(f.name)
if f.isPkg {
// If a package installation fails, the error is returned here.
return n, goErrToFSErr(f.importPackage())
}
}
return n, goErrToFSErr(err)
}
func (f *installFile) Close() error {
if f.blob == nil {
return nil
}
if err := f.blob.Close(); err != nil {
log.Printf("error closing file: %s\n", err)
return goErrToFSErr(err)
}
return nil
}
func (f *installFile) Stat() (int64, time.Time, time.Time, error) {
return int64(f.written), time.Time{}, time.Time{}, nil
}
func (f *installFile) Truncate(sz uint64) error {
var err error
f.size = sz
err = f.blob.Truncate(int64(f.size))
if f.size == 0 && f.name == identityBlob && err == nil {
// Fulfill any needs against the identity blob
f.fs.index.Fulfill(f.name)
}
return goErrToFSErr(err)
}
// importPackage uses f.name to import the package that was just written. It
// returns an fs.Error to report back to the user at write time.
func (f *installFile) importPackage() error {
return importPackage(f.fs, f.name)
}
// importPackage uses name to import the package that was just written. It
// returns an fs.Error to report back to the user at write time.
func importPackage(f *Filesystem, name string) error {
b, err := f.blobfs.Open(name)
if err != nil {
f.index.InstallingFailedForPackage(name)
log.Printf("error opening package blob after writing: %s: %s", name, err)
return fs.ErrFailedPrecondition
}
defer b.Close()
r, err := far.NewReader(b)
if err != nil {
f.index.InstallingFailedForPackage(name)
log.Printf("error reading package archive: %s", err)
// Note: translates to zx.ErrBadState
return fs.ErrFailedPrecondition
}
pf, err := r.ReadFile("meta/package")
if err != nil {
f.index.InstallingFailedForPackage(name)
log.Printf("error reading package metadata: %s", err)
// Note: translates to zx.ErrBadState
return fs.ErrFailedPrecondition
}
var p pkg.Package
err = json.Unmarshal(pf, &p)
if err != nil {
f.index.InstallingFailedForPackage(name)
log.Printf("error parsing package metadata: %s", err)
// Note: translates to zx.ErrBadState
return fs.ErrFailedPrecondition
}
if err := p.Validate(); err != nil {
f.index.InstallingFailedForPackage(name)
log.Printf("package is invalid: %s", err)
// Note: translates to zx.ErrBadState
return fs.ErrFailedPrecondition
}
// Tell the index the identity of this package.
f.index.UpdateInstalling(name, p)
contents, err := r.ReadFile("meta/contents")
if err != nil {
log.Printf("error parsing package contents file for %s: %s", p, err)
// Note: translates to zx.ErrBadState
return fs.ErrFailedPrecondition
}
files := bytes.Split(contents, []byte{'\n'})
// Note: the following heuristic is coarse and not easy to compute. For small
// packages enumerating all blobs in blobfs will be slow, but for very large
// packages it's more likely that polling blobfs the expensive way for missing
// blobs is going to be expensive. This can be improved by blobfs providing an
// API to handle this case of "which of the following blobs are already
// readable"
mayHaveBlob := func(root string) bool { return true }
if len(files) > 20 {
dnames, err := f.blobfs.Blobs()
if err != nil {
log.Printf("error readdir blobfs: %s", err)
// Note: translates to zx.ErrBadState
return fs.ErrFailedPrecondition
}
names := map[string]struct{}{}
for _, name := range dnames {
names[name] = struct{}{}
}
mayHaveBlob = func(root string) bool {
_, found := names[root]
return found
}
}
needBlobs := make(map[string]struct{})
foundBlobs := make(map[string]struct{})
needsCount := 0
for i := range files {
// Silence apparent errors from last line in file/empty lines.
if len(files[i]) == 0 {
continue
}
parts := bytes.SplitN(files[i], []byte{'='}, 2)
if len(parts) != 2 {
log.Printf("skipping bad package entry: %q", files[i])
continue
}
root := string(parts[1])
if mayHaveBlob(root) && f.blobfs.HasBlob(root) {
foundBlobs[root] = struct{}{}
continue
}
needsCount++
needBlobs[root] = struct{}{}
}
// NOTE: the EEXIST returned here is sometimes not strictly "this package was
// already activated", as the package may have just been activated during the
// above loop that calls fulfill with each blob that is found that already
// exists. Doing otherwise would significantly harm performance, as it would
// require a strong consistency rather than an eventual consistency model, that
// requires global locking of the filesystem. What this means in the not
// entirely correct case is either:
// a) we raced another process that was fulfilling the same needs as this
// package.
// b) all of the content was already on the system, but the package was missing
// from the index.
// This state could be improved if there was an in-memory precomputed index of
// all active meta.far blobs on the system.
if needsCount == 0 {
// It is possible that we already had all of the content for a package at the
// time when importPackage starts, for example if a package is updated and
// then reverted to a prior version wihtout GC. In that case, we should still
// activate the package, even though there is nothing to fulfill.
f.index.Add(p, name)
return fs.ErrAlreadyExists
}
// We tell the index about needs that we have which were explicitly not found
// on the system.
err = goErrToFSErr(f.index.AddNeeds(name, needBlobs))
// In order to ensure eventual consistency in the case where multiple processes
// are racing on these needs, we must re-publish all of those fulfillments
// after publishing the locally discovered needs.
for blob := range foundBlobs {
f.index.Fulfill(blob)
}
// AddNeeds may return os.ErrExist if the package activation won the race
// between our needs check loop above, and the following registration of the
// packages needs.
return err
}