diff --git a/pkg/pkg.go b/pkg/pkg.go index 891a5a7..f9c4537 100644 --- a/pkg/pkg.go +++ b/pkg/pkg.go @@ -1,19 +1,229 @@ package pkg import ( + "archive/tar" + "compress/bzip2" + "crypto/sha256" "fmt" + "io" + "log" + "os" + "path/filepath" + "sort" + "strings" + "github.com/pkg/errors" "golang.org/x/crypto/openpgp" + "mcquay.me/fs" + + "mcquay.me/pm/keyring" ) +var validNames map[string]bool +var crypto []string + +func init() { + validNames = map[string]bool{ + "root.tar.bz2": true, + "meta.yaml": true, + "bin/pre-install": true, + "bin/post-install": true, + "bin/pre-upgrade": true, + "bin/post-upgrade": true, + "bin/pre-remove": true, + "bin/post-remove": true, + } + + crypto = []string{ + "bom.sha256", + "manifest.sha256", + "manifest.sha256.asc", + } +} + // Create traverses the contents of dir and emits a valid pkg, signed by id -func Create(e *openpgp.Entity, dir string) error { +func Create(key *openpgp.Entity, dir string) error { if !fs.Exists(dir) { return fmt.Errorf("%q: doesn't exist", dir) } if !fs.IsDir(dir) { return fmt.Errorf("%q: is not a directory", dir) } - return fmt.Errorf("creating package from %q for %q: NYI", dir, e.PrimaryKey.KeyIdShortString()) + + if err := clean(dir); err != nil { + return errors.Wrap(err, "cleaning up directory") + } + + files := []string{} + found := map[string]bool{} + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if path == dir { + return nil + } + if info.IsDir() { + return nil + } + p := strings.TrimPrefix(path, dir)[1:] + + if _, ok := validNames[p]; !ok { + return fmt.Errorf("unxpected filename: %q", p) + } + + if strings.HasPrefix(p, "bin") && info.Mode()&0100 == 0 { + return fmt.Errorf("%q is not executable", path) + } + files = append(files, p) + found[p] = true + return nil + }) + if err != nil { + return errors.Wrap(err, "checking directory contents") + } + + if _, ok := found["root.tar.bz2"]; !ok { + return fmt.Errorf("did not find root.tar.bz2") + } + if _, ok := found["meta.yaml"]; !ok { + return fmt.Errorf("did not find meta.yaml") + } + + f, err := os.Open(filepath.Join(dir, "root.tar.bz2")) + if err != nil { + return errors.Wrap(err, "opening overlay tarball") + } + + bom, err := os.Create(filepath.Join(dir, "bom.sha256")) + if err != nil { + return errors.Wrap(err, "creating bom.sha256") + } + tr := tar.NewReader(bzip2.NewReader(f)) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + errors.Wrap(err, "traversing tarball") + } + if hdr.FileInfo().IsDir() { + continue + } + s := sha256.New() + if c, err := io.Copy(s, tr); err != nil { + return errors.Wrapf(err, "copy after %d bytes", c) + } + fmt.Fprintf(bom, "%x\t%s\n", s.Sum(nil), hdr.Name) + } + if err := bom.Close(); err != nil { + return errors.Wrap(err, "closing bom") + } + + files = append(files, "bom.sha256") + sort.Strings(files) + + manifest, err := os.Create(filepath.Join(dir, "manifest.sha256")) + if err != nil { + return errors.Wrap(err, "creating manifest.sha256") + } + for _, fn := range files { + full := filepath.Join(dir, fn) + f, err := os.Open(full) + if err != nil { + return errors.Wrap(err, "opening file for manifest checksumming") + } + s := sha256.New() + if c, err := io.Copy(s, f); err != nil { + return errors.Wrapf(err, "copy after %d bytes", c) + } + if err := f.Close(); err != nil { + return errors.Wrapf(err, "closing %q", f.Name()) + } + fmt.Fprintf(manifest, "%x\t%s\n", s.Sum(nil), fn) + } + if err := manifest.Close(); err != nil { + return errors.Wrap(err, "closing manifest") + } + + sig, err := os.Create(filepath.Join(dir, "manifest.sha256.asc")) + if err != nil { + return errors.Wrap(err, "creating sig file") + } + mfi, err := os.Open(filepath.Join(dir, "manifest.sha256")) + if err != nil { + return errors.Wrap(err, "opening manifest") + } + if err := keyring.Sign(key, mfi, sig); err != nil { + return errors.Wrap(err, "signing") + } + + files = append(files, "manifest.sha256") + files = append(files, "manifest.sha256.asc") + sort.Strings(files) + + tn, pn := filepath.Split(dir) + tn = filepath.Join(tn, pn+".pkg") + tf, err := os.Create(tn) + if err != nil { + return errors.Wrap(err, "opening final .pkg") + } + + tw := tar.NewWriter(tf) + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if path == dir { + return nil + } + p := strings.TrimPrefix(path, dir)[1:] + hdr, err := tar.FileInfoHeader(info, "") + if err != nil { + return errors.Wrap(err, "file info header") + } + hdr.Name = p + if err := tw.WriteHeader(hdr); err != nil { + return errors.Wrapf(err, "writing tar header for %v", p) + } + + // only need to do real writing for non-directories + if info.IsDir() { + return nil + } + + f, err := os.Open(path) + if err != nil { + return errors.Wrap(err, "opening file for tar creation") + } + if c, err := io.Copy(tw, f); err != nil { + log.Printf("%+v", err == io.EOF) + return errors.Wrapf(err, "copy after %d bytes", c) + } + if err := f.Close(); err != nil { + return errors.Wrap(err, "closing file during tar creation") + } + return nil + }) + if err != nil { + return errors.Wrap(err, "traversing the prepared directory") + } + if err := tw.Close(); err != nil { + return errors.Wrap(err, "closing tar writer") + } + + if err := tf.Close(); err != nil { + return errors.Wrap(err, "closing final .pkg") + } + + return nil +} + +func clean(root string) error { + for _, f := range crypto { + path := filepath.Join(root, f) + if !fs.Exists(path) { + continue + } + if err := os.Remove(path); err != nil { + return errors.Wrapf(err, "removing %q", path) + } + } + return nil }