blob: 21dee72614c3b1f7f72d1c299945e9ddc249fec2 [file] [log] [blame]
// Copyright 2014 The Go 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 main
import (
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
"time"
"unicode/utf8"
)
/*
The archive format is:
First, on a line by itself
!<arch>
Then zero or more file records. Each file record has a fixed-size one-line header
followed by data bytes followed by an optional padding byte. The header is:
%-16s%-12d%-6d%-6d%-8o%-10d`
name mtime uid gid mode size
(note the trailing backquote). The %-16s here means at most 16 *bytes* of
the name, and if shorter, space padded on the right.
*/
const usageMessage = `Usage: pack op file.a [name....]
Where op is one of prtx optionally followed by v for verbose output.
For more information, run
godoc cmd/pack`
func usage() {
fmt.Fprintln(os.Stderr, usageMessage)
os.Exit(2)
}
func main() {
log.SetFlags(0)
// need "pack op archive" at least.
if len(os.Args) < 3 {
usage()
}
setOp(os.Args[1])
var ar *Archive
switch op {
case 'p':
ar = archive(os.Args[2], os.O_RDONLY, os.Args[3:])
ar.scan(ar.printContents)
case 'r':
ar = archive(os.Args[2], os.O_RDWR, os.Args[3:])
ar.scan(ar.skipContents)
ar.addFiles()
case 't':
ar = archive(os.Args[2], os.O_RDONLY, os.Args[3:])
ar.scan(ar.tableOfContents)
case 'x':
ar = archive(os.Args[2], os.O_RDONLY, os.Args[3:])
ar.scan(ar.extractContents)
default:
usage()
}
if len(ar.files) > 0 {
log.Fatalf("pack: file %q not in archive", ar.files[0])
}
}
// The unusual ancestry means the arguments are not Go-standard.
// These variables hold the decoded operation specified by the first argument.
// op holds the operation we are doing (prtx).
// verbose tells whether the 'v' option was specified.
var (
op rune
verbose bool
)
// setOp parses the operation string (first argument).
func setOp(arg string) {
for _, r := range arg {
switch r {
case 'p', 'r', 't', 'x':
if op != 0 {
// At most one can be set.
usage()
}
op = r
case 'v':
if verbose {
// Can be set only once.
usage()
}
verbose = true
default:
usage()
}
}
}
const (
arHeader = "!<arch>\n"
entryHeader = "%s%-12d%-6d%-6d%-8o%-10d`\n"
// In entryHeader the first entry, the name, is always printed as 16 bytes right-padded.
entryLen = 16 + 12 + 6 + 6 + 8 + 10 + 1 + 1
timeFormat = "Jan _2 15:04 2006"
)
// An Archive represents an open archive file. It is always scanned sequentially
// from start to end, without backing up.
type Archive struct {
fd *os.File // Open file descriptor.
files []string // Explicit list of files to be processed.
}
// archive opens (or if necessary creates) the named archive.
func archive(name string, mode int, files []string) *Archive {
fd, err := os.OpenFile(name, mode, 0)
if err != nil && mode == os.O_RDWR && os.IsNotExist(err) {
fd, err = create(name)
}
if err != nil {
log.Fatal("pack: ", err)
}
mustBeArchive(fd)
return &Archive{
fd: fd,
files: files,
}
}
// create creates and initializes an archive that does not exist.
func create(name string) (*os.File, error) {
fd, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return nil, err
}
fmt.Fprint(fd, arHeader)
fd.Seek(0, 0)
return fd, nil
}
// mustBeArchive verifies the header of the file. It assumes the file offset
// is 0 coming in, and leaves it positioned immediately after the header.
func mustBeArchive(fd *os.File) {
buf := make([]byte, len(arHeader))
_, err := io.ReadFull(fd, buf)
if err != nil || string(buf) != arHeader {
log.Fatal("pack: file is not an archive: bad header")
}
}
// An Entry is the internal representation of the per-file header information of one entry in the archive.
type Entry struct {
name string
mtime int64
uid int
gid int
mode os.FileMode
size int64
}
func (e *Entry) String() string {
return fmt.Sprintf("%s %6d/%-6d %12d %s %s",
(e.mode & 0777).String(),
e.uid,
e.gid,
e.size,
time.Unix(e.mtime, 0).Format(timeFormat),
e.name)
}
// readMetadata reads and parses the metadata for the next entry in the archive.
func (ar *Archive) readMetadata() *Entry {
buf := make([]byte, entryLen)
_, err := io.ReadFull(ar.fd, buf)
if err == io.EOF {
// No entries left.
return nil
}
if err != nil || buf[entryLen-2] != '`' || buf[entryLen-1] != '\n' {
log.Fatal("pack: file is not an archive: bad entry")
}
entry := new(Entry)
entry.name = strings.TrimRight(string(buf[:16]), " ")
if len(entry.name) == 0 {
log.Fatal("pack: file is not an archive: bad name")
}
buf = buf[16:]
str := string(buf)
get := func(width, base, bitsize int) int64 {
v, err := strconv.ParseInt(strings.TrimRight(str[:width], " "), base, bitsize)
if err != nil {
log.Fatal("pack: file is not an archive: bad number in entry: ", err)
}
str = str[width:]
return v
}
// %-16s%-12d%-6d%-6d%-8o%-10d`
entry.mtime = get(12, 10, 64)
entry.uid = int(get(6, 10, 32))
entry.gid = int(get(6, 10, 32))
entry.mode = os.FileMode(get(8, 8, 32))
entry.size = get(10, 10, 64)
return entry
}
// scan scans the archive and executes the specified action on each entry.
// When action returns, the file offset is at the start of the next entry.
func (ar *Archive) scan(action func(*Entry)) {
for {
entry := ar.readMetadata()
if entry == nil {
break
}
action(entry)
}
}
// listEntry prints to standard output a line describing the entry.
func listEntry(ar *Archive, entry *Entry, verbose bool) {
if verbose {
fmt.Fprintf(stdout, "%s\n", entry)
} else {
fmt.Fprintf(stdout, "%s\n", entry.name)
}
}
// output copies the entry to the specified writer.
func (ar *Archive) output(entry *Entry, w io.Writer) {
n, err := io.Copy(w, io.LimitReader(ar.fd, entry.size))
if err != nil {
log.Fatal("pack: ", err)
}
if n != entry.size {
log.Fatal("pack: short file")
}
if entry.size&1 == 1 {
_, err := ar.fd.Seek(1, 1)
if err != nil {
log.Fatal("pack: ", err)
}
}
}
// skip skips the entry without reading it.
func (ar *Archive) skip(entry *Entry) {
size := entry.size
if size&1 == 1 {
size++
}
_, err := ar.fd.Seek(size, 1)
if err != nil {
log.Fatal("pack: ", err)
}
}
// match reports whether the entry matches the argument list.
// If it does, it also drops the file from the to-be-processed list.
func (ar *Archive) match(entry *Entry) bool {
if len(ar.files) == 0 {
return true
}
for i, name := range ar.files {
if entry.name == name {
copy(ar.files[i:], ar.files[i+1:])
ar.files = ar.files[:len(ar.files)-1]
return true
}
}
return false
}
// addFiles adds files to the archive. The archive is known to be
// sane and we are positioned at the end. No attempt is made
// to check for existing files.
func (ar *Archive) addFiles() {
if len(ar.files) == 0 {
usage()
}
for _, file := range ar.files {
if verbose {
fmt.Printf("%s\n", file)
}
fd, err := os.Open(file)
if err != nil {
log.Fatal("pack: ", err)
}
ar.addFile(fd)
}
ar.files = nil
}
// FileLike abstracts the few methods we need, so we can test without needing real files.
type FileLike interface {
Name() string
Stat() (os.FileInfo, error)
Read([]byte) (int, error)
Close() error
}
// addFile adds a single file to the archive
func (ar *Archive) addFile(fd FileLike) {
defer fd.Close()
// Format the entry.
// First, get its info.
info, err := fd.Stat()
if err != nil {
log.Fatal("pack: ", err)
}
// mtime, uid, gid are all zero so repeated builds produce identical output.
mtime := int64(0)
uid := 0
gid := 0
n, err := fmt.Fprintf(ar.fd, entryHeader, exactly16Bytes(info.Name()), mtime, uid, gid, info.Mode(), info.Size())
if err != nil || n != entryLen {
log.Fatal("pack: writing entry header: ", err)
}
n64, err := io.Copy(ar.fd, fd)
if err != nil {
log.Fatal("pack: writing file: ", err)
}
if n64 != info.Size() {
log.Fatal("pack: writing file: wrote %d bytes; file is size %d", n64, info.Size())
}
if info.Size()&1 == 1 {
_, err = ar.fd.Write([]byte{0})
if err != nil {
log.Fatal("pack: writing archive: ", err)
}
}
}
// exactly16Bytes truncates the string if necessary so it is at most 16 bytes long,
// then pads the result with spaces to be exactly 16 bytes.
// Fmt uses runes for its width calculation, but we need bytes in the entry header.
func exactly16Bytes(s string) string {
for len(s) > 16 {
_, wid := utf8.DecodeLastRuneInString(s)
s = s[:len(s)-wid]
}
const sixteenSpaces = " "
s += sixteenSpaces[:16-len(s)]
return s
}
// Finally, the actual commands. Each is an action.
// can be modified for testing.
var stdout io.Writer = os.Stdout
// printContents implements the 'p' command.
func (ar *Archive) printContents(entry *Entry) {
if ar.match(entry) {
if verbose {
listEntry(ar, entry, false)
}
ar.output(entry, stdout)
} else {
ar.skip(entry)
}
}
// skipContents implements the first part of the 'r' command.
// It just scans the archive to make sure it's intact.
func (ar *Archive) skipContents(entry *Entry) {
ar.skip(entry)
}
// tableOfContents implements the 't' command.
func (ar *Archive) tableOfContents(entry *Entry) {
if ar.match(entry) {
listEntry(ar, entry, verbose)
}
ar.skip(entry)
}
// extractContents implements the 'x' command.
func (ar *Archive) extractContents(entry *Entry) {
if ar.match(entry) {
if verbose {
listEntry(ar, entry, false)
}
fd, err := os.OpenFile(entry.name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, entry.mode)
if err != nil {
log.Fatal("pack: ", err)
}
ar.output(entry, fd)
fd.Close()
} else {
ar.skip(entry)
}
}