blob: a65c1ed57a056d1d694bf192ad480b1f39434d24 [file] [log] [blame]
// Copyright 2017 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 (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"encoding/json"
"go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/ignores"
"go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/pkg"
)
// Manifest describes the list of files that are to become the contents of a package
type Manifest struct {
// Srcs is a set of manifests and/or directories that are the contents of the package
Srcs []string
// Paths is the fully computed contents of a package in the form of "destination": "source"
Paths map[string]string
}
// NewManifest initializes a manifest from the given paths. If a path is a
// directory, it is globbed and the manifest includes all unignored files under
// that directory. If the path is a manifest file, the file is parsed and all
// files are mapped as described by the manifest file. Manifest files contain
// lines with "destination=source". Lines that do not match this pattern are
// ignored.
func NewManifest(paths []string) (*Manifest, error) {
m := &Manifest{
Srcs: paths,
Paths: make(map[string]string),
}
for _, path := range paths {
info, err := os.Stat(path)
if err != nil {
return nil, err
}
var newPaths map[string]string
if info.IsDir() {
newPaths, err = walk(path)
} else {
newPaths, err = parseManifest(path)
}
if err != nil {
return nil, err
}
for k, v := range newPaths {
m.Paths[k] = v
}
}
return m, nil
}
// Meta provides the list of files from the manifest that are to be included in
// meta.far.
func (m *Manifest) Meta() map[string]string {
meta := map[string]string{}
for d, s := range m.Paths {
// TODO(raggi): portable separator concerns
if strings.HasPrefix(d, "meta/") {
meta[d] = s
}
}
return meta
}
// Package loads the package descriptor from the package listed in the manifest and returns it.
func (m *Manifest) Package() (*pkg.Package, error) {
f, err := os.Open(m.Paths["meta/package"])
if err != nil {
return nil, fmt.Errorf("build.Manifest.Package: %s", err)
}
defer f.Close()
var p pkg.Package
if err := json.NewDecoder(f).Decode(&p); err != nil {
return nil, fmt.Errorf("Decode() failed: %v", err)
}
if err := p.Validate(); err != nil {
return nil, fmt.Errorf("Validate() failed: %v", err)
}
return &p, nil
}
// Content returns the list of files from the manifest that are not to be
// included in the meta.far.
func (m *Manifest) Content() map[string]string {
content := map[string]string{}
for d, s := range m.Paths {
// TODO(raggi): portable separator concerns
if !strings.HasPrefix(d, "meta/") {
content[d] = s
}
}
return content
}
func walk(root string) (map[string]string, error) {
r := map[string]string{}
err := filepath.Walk(root, func(source string, info os.FileInfo, err error) error {
if err != nil {
return err
}
dest, err := filepath.Rel(root, source)
if err != nil {
return err
}
if ignores.Match(dest) {
return filepath.SkipDir
}
if info.IsDir() {
return nil
}
r[dest] = source
return nil
})
return r, err
}
func parseManifest(path string) (map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("build.parseManifest: %s", err)
}
defer f.Close()
r := map[string]string{}
b := bufio.NewReader(f)
for {
line, err := b.ReadString('\n')
if err == io.EOF {
if len(strings.TrimSpace(line)) == 0 {
return r, nil
}
err = nil
}
if err != nil {
return r, err
}
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
continue
}
src := strings.TrimSpace(parts[1])
dest := strings.TrimSpace(parts[0])
// TODO(anmittal): make file comparision efficient.
if duplicateSrc, ok := r[dest]; ok {
if equal, err := filesEqual(src, duplicateSrc); err != nil {
return r, err
} else if !equal {
return r, fmt.Errorf("build.parseManifest: Multiple entries for key, pointing to different files: %q, [%s, %s]", dest, src, duplicateSrc)
}
continue
}
r[dest] = src
}
}
func filesEqual(file1, file2 string) (bool, error) {
f1, err := os.Open(file1)
if err != nil {
return false, fmt.Errorf("error while opening file (%s): %s", file1, err)
}
defer f1.Close()
f1s, err := f1.Stat()
if err != nil {
return false, fmt.Errorf("error while file stat (%s): %s", file1, err)
}
f2, err := os.Open(file2)
if err != nil {
return false, fmt.Errorf("error while opening file (%s): %s", file2, err)
}
defer f2.Close()
f2s, err := f2.Stat()
if err != nil {
return false, fmt.Errorf("error while file stat (%s): %s", file2, err)
}
if os.SameFile(f1s, f2s) {
return true, nil
}
if f1s.Size() != f2s.Size() {
return false, nil
}
size := f1s.Size()
chunkSize := int64(1024 * 16)
b1 := make([]byte, chunkSize)
b2 := make([]byte, chunkSize)
offset := int64(0)
for {
nextChunkSize := chunkSize
if size-offset < nextChunkSize {
nextChunkSize = size - offset
}
n1, err1 := io.ReadFull(f1, b1[0:nextChunkSize])
if err1 != nil && err1 != io.EOF {
return false, fmt.Errorf("error while reading file (%s): %s", file1, err1)
}
n2, err2 := io.ReadFull(f2, b2[0:nextChunkSize])
if err2 != nil && err2 != io.EOF {
return false, fmt.Errorf("error while reading file (%s): %s", file2, err2)
}
if n1 != n2 {
return false, fmt.Errorf("something wrong, read size should have been same for files %q and %q", file1, file2)
}
if n1 == 0 {
return true, nil
}
offset += int64(n1)
if !bytes.Equal(b1[0:n1], b2[0:n2]) {
return false, nil
}
}
}