// 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" "path/filepath" "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 cprtx optionally followed by v for verbose output. For compatibility with old Go build environments the op string grc is accepted as a synonym for c. For more information, run go doc cmd/pack` func usage() { fmt.Fprintln(os.Stderr, usageMessage) os.Exit(2) } func main() { log.SetFlags(0) log.SetPrefix("pack: ") // need "pack op archive" at least. if len(os.Args) < 3 { log.Print("not enough arguments") fmt.Fprintln(os.Stderr) 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 'c': ar = archive(os.Args[2], os.O_RDWR|os.O_TRUNC, os.Args[3:]) ar.addPkgdef() 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: log.Printf("invalid operation %q", os.Args[1]) fmt.Fprintln(os.Stderr) usage() } if len(ar.files) > 0 { log.Fatalf("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) { // Recognize 'go tool pack grc' because that was the // formerly canonical way to build a new archive // from a set of input files. Accepting it keeps old // build systems working with both Go 1.2 and Go 1.3. if arg == "grc" { arg = "c" } for _, r := range arg { switch r { case 'c', '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. pad int // Padding bytes required at end of current archive file matchAll bool // match all files in archive } // archive opens (and if necessary creates) the named archive. func archive(name string, mode int, files []string) *Archive { // If the file exists, it must be an archive. If it doesn't exist, or if // we're doing the c command, indicated by O_TRUNC, truncate the archive. if !existingArchive(name) || mode&os.O_TRUNC != 0 { create(name) mode &^= os.O_TRUNC } fd, err := os.OpenFile(name, mode, 0) if err != nil { log.Fatal(err) } checkHeader(fd) return &Archive{ fd: fd, files: files, matchAll: len(files) == 0, } } // create creates and initializes an archive that does not exist. func create(name string) { fd, err := os.Create(name) if err != nil { log.Fatal(err) } _, err = fmt.Fprint(fd, arHeader) if err != nil { log.Fatal(err) } fd.Close() } // existingArchive reports whether the file exists and is a valid archive. // If it exists but is not an archive, existingArchive will exit. func existingArchive(name string) bool { fd, err := os.Open(name) if err != nil { if os.IsNotExist(err) { return false } log.Fatalf("cannot open file: %s", err) } checkHeader(fd) fd.Close() return true } // checkHeader verifies the header of the file. It assumes the file // is positioned at 0 and leaves it positioned at the end of the header. func checkHeader(fd *os.File) { buf := make([]byte, len(arHeader)) _, err := io.ReadFull(fd, buf) if err != nil || string(buf) != arHeader { log.Fatalf("%s is not an archive: bad header", fd.Name()) } } // 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("file is not an archive: bad entry") } entry := new(Entry) entry.name = strings.TrimRight(string(buf[:16]), " ") if len(entry.name) == 0 { log.Fatal("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("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(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(err) } if n != entry.size { log.Fatal("short file") } if entry.size&1 == 1 { _, err := ar.fd.Seek(1, io.SeekCurrent) if err != nil { log.Fatal(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, io.SeekCurrent) if err != nil { log.Fatal(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 ar.matchAll { 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) } if !isGoCompilerObjFile(file) { fd, err := os.Open(file) if err != nil { log.Fatal(err) } ar.addFile(fd) continue } aro := archive(file, os.O_RDONLY, nil) aro.scan(func(entry *Entry) { if entry.name != "_go_.o" { aro.skip(entry) return } ar.startFile(filepath.Base(file), 0, 0, 0, 0644, entry.size) aro.output(entry, ar.fd) ar.endFile() }) } 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(err) } // mtime, uid, gid are all zero so repeated builds produce identical output. mtime := int64(0) uid := 0 gid := 0 ar.startFile(info.Name(), mtime, uid, gid, info.Mode(), info.Size()) n64, err := io.Copy(ar.fd, fd) if err != nil { log.Fatal("writing file: ", err) } if n64 != info.Size() { log.Fatalf("writing file: wrote %d bytes; file is size %d", n64, info.Size()) } ar.endFile() } // startFile writes the archive entry header. func (ar *Archive) startFile(name string, mtime int64, uid, gid int, mode os.FileMode, size int64) { n, err := fmt.Fprintf(ar.fd, entryHeader, exactly16Bytes(name), mtime, uid, gid, mode, size) if err != nil || n != entryLen { log.Fatal("writing entry header: ", err) } ar.pad = int(size & 1) } // endFile writes the archive entry tail (a single byte of padding, if the file size was odd). func (ar *Archive) endFile() { if ar.pad != 0 { _, err := ar.fd.Write([]byte{0}) if err != nil { log.Fatal("writing archive: ", err) } ar.pad = 0 } } // addPkgdef adds the __.PKGDEF file to the archive, copied // from the first Go object file on the file list, if any. // The archive is known to be empty. func (ar *Archive) addPkgdef() { done := false for _, file := range ar.files { if !isGoCompilerObjFile(file) { continue } aro := archive(file, os.O_RDONLY, nil) aro.scan(func(entry *Entry) { if entry.name != "__.PKGDEF" { aro.skip(entry) return } if verbose { fmt.Printf("__.PKGDEF # %s\n", file) } ar.startFile("__.PKGDEF", 0, 0, 0, 0644, entry.size) aro.output(entry, ar.fd) ar.endFile() done = true }) if done { break } } } // 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(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(entry, verbose) } ar.skip(entry) } // extractContents implements the 'x' command. func (ar *Archive) extractContents(entry *Entry) { if ar.match(entry) { if verbose { listEntry(entry, false) } fd, err := os.OpenFile(entry.name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, entry.mode) if err != nil { log.Fatal(err) } ar.output(entry, fd) fd.Close() } else { ar.skip(entry) } } // isGoCompilerObjFile reports whether file is an object file created // by the Go compiler. func isGoCompilerObjFile(file string) bool { fd, err := os.Open(file) if err != nil { log.Fatal(err) } // Check for "!<arch>\n" header. buf := make([]byte, len(arHeader)) _, err = io.ReadFull(fd, buf) if err != nil { if err == io.EOF { return false } log.Fatal(err) } if string(buf) != arHeader { return false } // Check for exactly two entries: "__.PKGDEF" and "_go_.o". match := []string{"__.PKGDEF", "_go_.o"} buf = make([]byte, entryLen) for { _, err := io.ReadFull(fd, buf) if err != nil { if err == io.EOF { // No entries left. return true } log.Fatal(err) } if buf[entryLen-2] != '`' || buf[entryLen-1] != '\n' { return false } name := strings.TrimRight(string(buf[:16]), " ") for { if len(match) == 0 { return false } var next string next, match = match[0], match[1:] if name == next { break } } size, err := strconv.ParseInt(strings.TrimRight(string(buf[48:58]), " "), 10, 64) if err != nil { return false } if size&1 != 0 { size++ } _, err = fd.Seek(size, io.SeekCurrent) if err != nil { log.Fatal(err) } } }