blob: ad8b1aea347839449a144b3c88e22d93a84429ac [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 far implements Fuchsia archive operations. At this time the optional
// hash chunks are not written, and only archive writing is supported. The
// specification for the archive format can be found in
// https://fuchsia.googlesource.com/docs/+/master/archive_format.md.
package far
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"os"
"sort"
)
// Magic is the first bytes of a FAR archive
const Magic = "\xc8\xbf\x0b\x48\xad\xab\xc5\x11"
// ErrInvalidArchive is returned from reads when the archive is not of the expected format or is corrupted.
type ErrInvalidArchive string
func (e ErrInvalidArchive) Error() string {
return fmt.Sprintf("far: archive is not valid. %s", string(e))
}
// ChunkType is a uint64 representing the type of a non-index chunk
type ChunkType uint64
// alignment values from the FAR specification
const (
contentAlignment = 4096
nameAlignment = 8
)
// Various chunk types
const (
HashChunk ChunkType = 0 // 0
DirHashChunk ChunkType = 0x2d48534148524944 // "DIRHASH-"
DirChunk ChunkType = 0x2d2d2d2d2d524944 // "DIR-----"
DirNamesChunk ChunkType = 0x53454d414e524944 // "DIRNAMES"
)
// Index is the first chunk of an archive
type Index struct {
// Magic bytes must equal the Magic constant
Magic [8]byte
// Length of all index entries in bytes
Length uint64
}
// IndexLen is the byte size of the Index struct
const IndexLen = 8 + 8
// IndexEntry identifies the type and position of a chunk in the archive
type IndexEntry struct {
// Type of the chunk
Type ChunkType
// Offset from the start of the archive
Offset uint64
// Length of the chunk
Length uint64
}
// IndexEntryLen is the byte size of the IndexEntry struct
const IndexEntryLen = 8 + 8 + 8
// DirectoryEntry indexes into the dirnames and contents chunks to provide
// access to file names and file contents respectively.
type DirectoryEntry struct {
NameOffset uint32
NameLength uint16
Reserved uint16
DataOffset uint64
DataLength uint64
Reserved2 uint64
}
// DirectoryEntryLen is the byte size of the DirectoryEntry struct
const DirectoryEntryLen = 4 + 2 + 2 + 8 + 8 + 8
// PathData is a concatenated list of names of files that makes up the unpadded
// portion of a dirnames chunk.
type PathData []byte
// Write writes a list of files to the given io. The inputs map provides a list
// of target archive paths mapped to on-disk file paths from which the content
// should be fetched.
func Write(w io.Writer, inputs map[string]string) error {
var filenames = make([]string, 0, len(inputs))
for name := range inputs {
filenames = append(filenames, name)
}
sort.Strings(filenames)
var pathData PathData
var entries []DirectoryEntry
for _, name := range filenames {
bname := []byte(name)
entries = append(entries, DirectoryEntry{
NameOffset: uint32(len(pathData)),
NameLength: uint16(len(bname)),
})
pathData = append(pathData, bname...)
}
index := Index{
Length: IndexEntryLen * 2,
}
copy(index.Magic[:], []byte(Magic))
dirIndex := IndexEntry{
Type: DirChunk,
Offset: IndexLen + IndexEntryLen*2,
Length: uint64(len(entries) * DirectoryEntryLen),
}
nameIndex := IndexEntry{
Type: DirNamesChunk,
Offset: dirIndex.Offset + dirIndex.Length,
Length: align(uint64(len(pathData)), 8),
}
if err := binary.Write(w, binary.LittleEndian, index); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, dirIndex); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, nameIndex); err != nil {
return err
}
contentOffset := align(nameIndex.Offset+nameIndex.Length, contentAlignment)
for i := range entries {
entries[i].DataOffset = contentOffset
n, err := fileSize(inputs[filenames[i]])
if err != nil {
return err
}
entries[i].DataLength = uint64(n)
contentOffset = align(contentOffset+entries[i].DataLength, contentAlignment)
if err := binary.Write(w, binary.LittleEndian, entries[i]); err != nil {
return err
}
}
if err := binary.Write(w, binary.LittleEndian, pathData); err != nil {
return err
}
if _, err := w.Write(make([]byte, int(nameIndex.Length)-len(pathData))); err != nil {
return err
}
pos := nameIndex.Offset + nameIndex.Length
pad := align(pos, contentAlignment) - pos
if _, err := w.Write(make([]byte, pad)); err != nil {
return err
}
for i, name := range filenames {
f, err := os.Open(inputs[name])
if err != nil {
return err
}
if _, err := io.Copy(w, f); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
pos := entries[i].DataOffset + entries[i].DataLength
pad := align(pos, contentAlignment) - pos
if _, err := w.Write(make([]byte, pad)); err != nil {
return err
}
}
return nil
}
// Reader wraps an io.ReaderAt providing access to FAR contents from that io.
// It caches the directory and path information after it is read, and provides
// io.Readers for files contained in the archive.
type Reader struct {
source io.ReaderAt
index Index
indexEntries []IndexEntry
dirEntries []DirectoryEntry
pathData PathData
}
// NewReader wraps the given io.ReaderAt and returns a struct that provides indexed access to the FAR contents.
func NewReader(s io.ReaderAt) (*Reader, error) {
r := &Reader{source: s}
if err := r.readIndex(); err != nil {
return nil, err
}
return r, nil
}
// Close closes the underlying reader source, if it implements io.Closer
func (r *Reader) Close() error {
if c, ok := r.source.(io.Closer); ok {
return c.Close()
}
return nil
}
func (r *Reader) readIndex() error {
buf := make([]byte, IndexLen)
if _, err := r.source.ReadAt(buf, 0); err != nil {
return err
}
copy(r.index.Magic[:], buf)
r.index.Length = binary.LittleEndian.Uint64(buf[len(r.index.Magic):])
if !bytes.Equal(r.index.Magic[:], []byte(Magic)) {
return ErrInvalidArchive("bad magic")
}
if r.index.Length%IndexEntryLen != 0 {
return ErrInvalidArchive("bad index length")
}
nentries := r.index.Length / IndexEntryLen
if nentries == 0 {
return nil
}
r.indexEntries = make([]IndexEntry, nentries)
var dirIndex, dirNamesIndex *IndexEntry
buf = make([]byte, IndexEntryLen)
for i := range r.indexEntries {
if _, err := r.source.ReadAt(buf, int64(IndexLen+(i*IndexEntryLen))); err != nil {
return err
}
r.indexEntries[i].Type = ChunkType(binary.LittleEndian.Uint64(buf))
r.indexEntries[i].Offset = binary.LittleEndian.Uint64(buf[8:])
r.indexEntries[i].Length = binary.LittleEndian.Uint64(buf[16:])
if i > 0 && r.indexEntries[i-1].Type > r.indexEntries[i].Type {
return ErrInvalidArchive("invalid index entry order")
}
if r.indexEntries[i].Offset < r.index.Length {
return ErrInvalidArchive("short offset")
}
switch r.indexEntries[i].Type {
case DirChunk:
dirIndex = &r.indexEntries[i]
if dirIndex.Length%DirectoryEntryLen != 0 {
return ErrInvalidArchive("bad directory index")
}
case DirNamesChunk:
dirNamesIndex = &r.indexEntries[i]
}
}
if dirIndex == nil || dirNamesIndex == nil {
return ErrInvalidArchive("missing required chunk")
}
buf = make([]byte, dirIndex.Length)
if _, err := r.source.ReadAt(buf, int64(dirIndex.Offset)); err != nil {
return err
}
r.dirEntries = make([]DirectoryEntry, dirIndex.Length/DirectoryEntryLen)
// TODO(raggi): eradicate copies, etc.
if err := binary.Read(bytes.NewReader(buf), binary.LittleEndian, &r.dirEntries); err != nil {
return err
}
r.pathData = make([]byte, dirNamesIndex.Length)
_, err := r.source.ReadAt(r.pathData, int64(dirNamesIndex.Offset))
return err
}
// List provides the list of all file names in the archive
func (r *Reader) List() []string {
var names = make([]string, 0, len(r.dirEntries))
for i := range r.dirEntries {
de := &r.dirEntries[i]
names = append(names, string(r.pathData[de.NameOffset:de.NameOffset+uint32(de.NameLength)]))
}
return names
}
func (r *Reader) openEntry(de *DirectoryEntry) io.ReaderAt {
return &entryReader{de.DataOffset, de.DataLength, r.source}
}
// Open finds the file in the archive and returns an io.ReaderAt that can read the contents
func (r *Reader) Open(path string) (io.ReaderAt, error) {
bpath := []byte(path)
for i := range r.dirEntries {
de := &r.dirEntries[i]
if bytes.Equal(bpath, r.pathData[de.NameOffset:de.NameOffset+uint32(de.NameLength)]) {
return r.openEntry(de), nil
}
}
return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
}
// ReadFile reads a whole file out of the archive
func (r *Reader) ReadFile(path string) ([]byte, error) {
bpath := []byte(path)
for i := range r.dirEntries {
de := &r.dirEntries[i]
if bytes.Equal(bpath, r.pathData[de.NameOffset:de.NameOffset+uint32(de.NameLength)]) {
buf := make([]byte, de.DataLength)
_, err := r.source.ReadAt(buf, int64(de.DataOffset))
return buf, err
}
}
return nil, os.ErrNotExist
}
func (r *Reader) GetSize(path string) uint64 {
bpath := []byte(path)
for i := range r.dirEntries {
de := &r.dirEntries[i]
if bytes.Equal(bpath, r.pathData[de.NameOffset:de.NameOffset+uint32(de.NameLength)]) {
return de.DataLength
}
}
return 0
}
// IsFAR looks for the FAR magic header, returning true if it is found. Only the header is consumed from the given input. If any IO error occurs, false is returned.
func IsFAR(r io.Reader) bool {
m := make([]byte, len(Magic))
_, err := io.ReadFull(r, m)
if err != nil {
return false
}
return bytes.Equal(m, []byte(Magic))
}
// TODO(raggi): implement a VMO clone fdio approach on fuchsia
type entryReader struct {
offset uint64
length uint64
source io.ReaderAt
}
func (e *entryReader) ReadAt(buf []byte, offset int64) (int, error) {
if offset >= int64(e.length) || offset < 0 {
return 0, io.EOF
}
// clamp the read request to the top of the range
max := int(e.length - uint64(offset))
if max > len(buf) {
max = len(buf)
}
return e.source.ReadAt(buf[:max], int64(e.offset+uint64(offset)))
}
// align rounds i up to a multiple of n
func align(i, n uint64) uint64 {
n--
return (i + n) & ^n
}
func fileSize(path string) (int64, error) {
info, err := os.Stat(path)
if err != nil {
return 0, err
}
return info.Size(), nil
}