480 lines
14 KiB
Go
480 lines
14 KiB
Go
// Copyright 2019 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package rpmpack packs files to rpm files.
|
|
// It is designed to be simple to use and deploy, not requiring any filesystem access
|
|
// to create rpm files.
|
|
package rpmpack
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"sort"
|
|
"time"
|
|
|
|
cpio "github.com/cavaliercoder/go-cpio"
|
|
"github.com/pkg/errors"
|
|
"github.com/ulikunitz/xz"
|
|
"github.com/ulikunitz/xz/lzma"
|
|
)
|
|
|
|
var (
|
|
// ErrWriteAfterClose is returned when a user calls Write() on a closed rpm.
|
|
ErrWriteAfterClose = errors.New("rpm write after close")
|
|
// ErrWrongFileOrder is returned when files are not sorted by name.
|
|
ErrWrongFileOrder = errors.New("wrong file addition order")
|
|
)
|
|
|
|
// RPMMetaData contains meta info about the whole package.
|
|
type RPMMetaData struct {
|
|
Name,
|
|
Summary,
|
|
Description,
|
|
Version,
|
|
Release,
|
|
Arch,
|
|
OS,
|
|
Vendor,
|
|
URL,
|
|
Packager,
|
|
Group,
|
|
Licence,
|
|
BuildHost,
|
|
Compressor string
|
|
Epoch uint32
|
|
BuildTime time.Time
|
|
Provides,
|
|
Obsoletes,
|
|
Suggests,
|
|
Recommends,
|
|
Requires,
|
|
Conflicts Relations
|
|
}
|
|
|
|
// RPM holds the state of a particular rpm file. Please use NewRPM to instantiate it.
|
|
type RPM struct {
|
|
RPMMetaData
|
|
di *dirIndex
|
|
payload *bytes.Buffer
|
|
payloadSize uint
|
|
cpio *cpio.Writer
|
|
basenames []string
|
|
dirindexes []uint32
|
|
filesizes []uint32
|
|
filemodes []uint16
|
|
fileowners []string
|
|
filegroups []string
|
|
filemtimes []uint32
|
|
filedigests []string
|
|
filelinktos []string
|
|
fileflags []uint32
|
|
closed bool
|
|
compressedPayload io.WriteCloser
|
|
files map[string]RPMFile
|
|
prein string
|
|
postin string
|
|
preun string
|
|
postun string
|
|
pretrans string
|
|
posttrans string
|
|
customTags map[int]IndexEntry
|
|
customSigs map[int]IndexEntry
|
|
pgpSigner func([]byte) ([]byte, error)
|
|
}
|
|
|
|
// NewRPM creates and returns a new RPM struct.
|
|
func NewRPM(m RPMMetaData) (*RPM, error) {
|
|
var err error
|
|
|
|
if m.OS == "" {
|
|
m.OS = "linux"
|
|
}
|
|
|
|
if m.Arch == "" {
|
|
m.Arch = "noarch"
|
|
}
|
|
|
|
p := &bytes.Buffer{}
|
|
var z io.WriteCloser
|
|
switch m.Compressor {
|
|
case "":
|
|
m.Compressor = "gzip"
|
|
fallthrough
|
|
case "gzip":
|
|
z, err = gzip.NewWriterLevel(p, 9)
|
|
case "lzma":
|
|
z, err = lzma.NewWriter(p)
|
|
case "xz":
|
|
z, err = xz.NewWriter(p)
|
|
default:
|
|
err = fmt.Errorf("unknown compressor type %s", m.Compressor)
|
|
}
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to create compression writer")
|
|
}
|
|
|
|
rpm := &RPM{
|
|
RPMMetaData: m,
|
|
di: newDirIndex(),
|
|
payload: p,
|
|
compressedPayload: z,
|
|
cpio: cpio.NewWriter(z),
|
|
files: make(map[string]RPMFile),
|
|
customTags: make(map[int]IndexEntry),
|
|
customSigs: make(map[int]IndexEntry),
|
|
}
|
|
|
|
// A package must provide itself...
|
|
rpm.Provides.addIfMissing(&Relation{
|
|
Name: rpm.Name,
|
|
Version: rpm.FullVersion(),
|
|
Sense: SenseEqual,
|
|
})
|
|
|
|
return rpm, nil
|
|
}
|
|
|
|
// FullVersion properly combines version and release fields to a version string
|
|
func (r *RPM) FullVersion() string {
|
|
if r.Release != "" {
|
|
return fmt.Sprintf("%s-%s", r.Version, r.Release)
|
|
}
|
|
|
|
return r.Version
|
|
}
|
|
|
|
// Write closes the rpm and writes the whole rpm to an io.Writer
|
|
func (r *RPM) Write(w io.Writer) error {
|
|
if r.closed {
|
|
return ErrWriteAfterClose
|
|
}
|
|
// Add all of the files, sorted alphabetically.
|
|
fnames := []string{}
|
|
for fn := range r.files {
|
|
fnames = append(fnames, fn)
|
|
}
|
|
sort.Strings(fnames)
|
|
for _, fn := range fnames {
|
|
if err := r.writeFile(r.files[fn]); err != nil {
|
|
return errors.Wrapf(err, "failed to write file %q", fn)
|
|
}
|
|
}
|
|
if err := r.cpio.Close(); err != nil {
|
|
return errors.Wrap(err, "failed to close cpio payload")
|
|
}
|
|
if err := r.compressedPayload.Close(); err != nil {
|
|
return errors.Wrap(err, "failed to close gzip payload")
|
|
}
|
|
|
|
if _, err := w.Write(lead(r.Name, r.FullVersion())); err != nil {
|
|
return errors.Wrap(err, "failed to write lead")
|
|
}
|
|
// Write the regular header.
|
|
h := newIndex(immutable)
|
|
r.writeGenIndexes(h)
|
|
|
|
// do not write file indexes if there are no files (meta package)
|
|
// doing so will result in an invalid package
|
|
if (len(r.files)) > 0 {
|
|
r.writeFileIndexes(h)
|
|
}
|
|
|
|
if err := r.writeRelationIndexes(h); err != nil {
|
|
return err
|
|
}
|
|
// CustomTags must be the last to be added, because they can overwrite values.
|
|
h.AddEntries(r.customTags)
|
|
hb, err := h.Bytes()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to retrieve header")
|
|
}
|
|
// Write the signatures
|
|
s := newIndex(signatures)
|
|
if err := r.writeSignatures(s, hb); err != nil {
|
|
return errors.Wrap(err, "failed to create signatures")
|
|
}
|
|
|
|
s.AddEntries(r.customSigs)
|
|
sb, err := s.Bytes()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to retrieve signatures header")
|
|
}
|
|
|
|
if _, err := w.Write(sb); err != nil {
|
|
return errors.Wrap(err, "failed to write signature bytes")
|
|
}
|
|
//Signatures are padded to 8-byte boundaries
|
|
if _, err := w.Write(make([]byte, (8-len(sb)%8)%8)); err != nil {
|
|
return errors.Wrap(err, "failed to write signature padding")
|
|
}
|
|
if _, err := w.Write(hb); err != nil {
|
|
return errors.Wrap(err, "failed to write header body")
|
|
}
|
|
_, err = w.Write(r.payload.Bytes())
|
|
return errors.Wrap(err, "failed to write payload")
|
|
|
|
}
|
|
|
|
// SetPGPSigner registers a function that will accept the header and payload as bytes,
|
|
// and return a signature as bytes. The function should simulate what gpg does,
|
|
// probably by using golang.org/x/crypto/openpgp or by forking a gpg process.
|
|
func (r *RPM) SetPGPSigner(f func([]byte) ([]byte, error)) {
|
|
r.pgpSigner = f
|
|
}
|
|
|
|
// Only call this after the payload and header were written.
|
|
func (r *RPM) writeSignatures(sigHeader *index, regHeader []byte) error {
|
|
sigHeader.Add(sigSize, EntryInt32([]int32{int32(r.payload.Len() + len(regHeader))}))
|
|
sigHeader.Add(sigSHA256, EntryString(fmt.Sprintf("%x", sha256.Sum256(regHeader))))
|
|
sigHeader.Add(sigPayloadSize, EntryInt32([]int32{int32(r.payloadSize)}))
|
|
if r.pgpSigner != nil {
|
|
// For sha 256 you need to sign the header and payload separately
|
|
header := append([]byte{}, regHeader...)
|
|
headerSig, err := r.pgpSigner(header)
|
|
if err != nil {
|
|
return errors.Wrap(err, "call to signer failed")
|
|
}
|
|
sigHeader.Add(sigRSA, EntryBytes(headerSig))
|
|
|
|
body := append(header, r.payload.Bytes()...)
|
|
bodySig, err := r.pgpSigner(body)
|
|
if err != nil {
|
|
return errors.Wrap(err, "call to signer failed")
|
|
}
|
|
sigHeader.Add(sigPGP, EntryBytes(bodySig))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *RPM) writeRelationIndexes(h *index) error {
|
|
// add all relation categories
|
|
if err := r.Provides.AddToIndex(h, tagProvides, tagProvideVersion, tagProvideFlags); err != nil {
|
|
return errors.Wrap(err, "failed to add provides")
|
|
}
|
|
if err := r.Obsoletes.AddToIndex(h, tagObsoletes, tagObsoleteVersion, tagObsoleteFlags); err != nil {
|
|
return errors.Wrap(err, "failed to add obsoletes")
|
|
}
|
|
if err := r.Suggests.AddToIndex(h, tagSuggests, tagSuggestVersion, tagSuggestFlags); err != nil {
|
|
return errors.Wrap(err, "failed to add suggests")
|
|
}
|
|
if err := r.Recommends.AddToIndex(h, tagRecommends, tagRecommendVersion, tagRecommendFlags); err != nil {
|
|
return errors.Wrap(err, "failed to add recommends")
|
|
}
|
|
if err := r.Requires.AddToIndex(h, tagRequires, tagRequireVersion, tagRequireFlags); err != nil {
|
|
return errors.Wrap(err, "failed to add requires")
|
|
}
|
|
if err := r.Conflicts.AddToIndex(h, tagConflicts, tagConflictVersion, tagConflictFlags); err != nil {
|
|
return errors.Wrap(err, "failed to add conflicts")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddCustomTag adds or overwrites a tag value in the index.
|
|
func (r *RPM) AddCustomTag(tag int, e IndexEntry) {
|
|
r.customTags[tag] = e
|
|
}
|
|
|
|
// AddCustomSig adds or overwrites a signature tag value.
|
|
func (r *RPM) AddCustomSig(tag int, e IndexEntry) {
|
|
r.customSigs[tag] = e
|
|
}
|
|
|
|
func (r *RPM) writeGenIndexes(h *index) {
|
|
h.Add(tagHeaderI18NTable, EntryString("C"))
|
|
h.Add(tagSize, EntryInt32([]int32{int32(r.payloadSize)}))
|
|
h.Add(tagName, EntryString(r.Name))
|
|
h.Add(tagVersion, EntryString(r.Version))
|
|
h.Add(tagEpoch, EntryUint32([]uint32{r.Epoch}))
|
|
h.Add(tagSummary, EntryString(r.Summary))
|
|
h.Add(tagDescription, EntryString(r.Description))
|
|
h.Add(tagBuildHost, EntryString(r.BuildHost))
|
|
if !r.BuildTime.IsZero() {
|
|
// time.Time zero value is confusing, avoid if not supplied
|
|
// see https://github.com/google/rpmpack/issues/43
|
|
h.Add(tagBuildTime, EntryInt32([]int32{int32(r.BuildTime.Unix())}))
|
|
}
|
|
h.Add(tagRelease, EntryString(r.Release))
|
|
h.Add(tagPayloadFormat, EntryString("cpio"))
|
|
h.Add(tagPayloadCompressor, EntryString(r.Compressor))
|
|
h.Add(tagPayloadFlags, EntryString("9"))
|
|
h.Add(tagArch, EntryString(r.Arch))
|
|
h.Add(tagOS, EntryString(r.OS))
|
|
h.Add(tagVendor, EntryString(r.Vendor))
|
|
h.Add(tagLicence, EntryString(r.Licence))
|
|
h.Add(tagPackager, EntryString(r.Packager))
|
|
h.Add(tagGroup, EntryString(r.Group))
|
|
h.Add(tagURL, EntryString(r.URL))
|
|
h.Add(tagPayloadDigest, EntryStringSlice([]string{fmt.Sprintf("%x", sha256.Sum256(r.payload.Bytes()))}))
|
|
h.Add(tagPayloadDigestAlgo, EntryInt32([]int32{hashAlgoSHA256}))
|
|
|
|
// rpm utilities look for the sourcerpm tag to deduce if this is not a source rpm (if it has a sourcerpm,
|
|
// it is NOT a source rpm).
|
|
h.Add(tagSourceRPM, EntryString(fmt.Sprintf("%s-%s.src.rpm", r.Name, r.FullVersion())))
|
|
if r.pretrans != "" {
|
|
h.Add(tagPretrans, EntryString(r.pretrans))
|
|
h.Add(tagPretransProg, EntryString("/bin/sh"))
|
|
}
|
|
if r.prein != "" {
|
|
h.Add(tagPrein, EntryString(r.prein))
|
|
h.Add(tagPreinProg, EntryString("/bin/sh"))
|
|
}
|
|
if r.postin != "" {
|
|
h.Add(tagPostin, EntryString(r.postin))
|
|
h.Add(tagPostinProg, EntryString("/bin/sh"))
|
|
}
|
|
if r.preun != "" {
|
|
h.Add(tagPreun, EntryString(r.preun))
|
|
h.Add(tagPreunProg, EntryString("/bin/sh"))
|
|
}
|
|
if r.postun != "" {
|
|
h.Add(tagPostun, EntryString(r.postun))
|
|
h.Add(tagPostunProg, EntryString("/bin/sh"))
|
|
}
|
|
if r.posttrans != "" {
|
|
h.Add(tagPosttrans, EntryString(r.posttrans))
|
|
h.Add(tagPosttransProg, EntryString("/bin/sh"))
|
|
}
|
|
}
|
|
|
|
// WriteFileIndexes writes file related index headers to the header
|
|
func (r *RPM) writeFileIndexes(h *index) {
|
|
h.Add(tagBasenames, EntryStringSlice(r.basenames))
|
|
h.Add(tagDirindexes, EntryUint32(r.dirindexes))
|
|
h.Add(tagDirnames, EntryStringSlice(r.di.AllDirs()))
|
|
h.Add(tagFileSizes, EntryUint32(r.filesizes))
|
|
h.Add(tagFileModes, EntryUint16(r.filemodes))
|
|
h.Add(tagFileUserName, EntryStringSlice(r.fileowners))
|
|
h.Add(tagFileGroupName, EntryStringSlice(r.filegroups))
|
|
h.Add(tagFileMTimes, EntryUint32(r.filemtimes))
|
|
h.Add(tagFileDigests, EntryStringSlice(r.filedigests))
|
|
h.Add(tagFileLinkTos, EntryStringSlice(r.filelinktos))
|
|
h.Add(tagFileFlags, EntryUint32(r.fileflags))
|
|
|
|
inodes := make([]int32, len(r.dirindexes))
|
|
digestAlgo := make([]int32, len(r.dirindexes))
|
|
verifyFlags := make([]int32, len(r.dirindexes))
|
|
fileRDevs := make([]int16, len(r.dirindexes))
|
|
fileLangs := make([]string, len(r.dirindexes))
|
|
|
|
for ii := range inodes {
|
|
// is inodes just a range from 1..len(dirindexes)? maybe different with hard links
|
|
inodes[ii] = int32(ii + 1)
|
|
digestAlgo[ii] = hashAlgoSHA256
|
|
// With regular files, it seems like we can always enable all of the verify flags
|
|
verifyFlags[ii] = int32(-1)
|
|
fileRDevs[ii] = int16(1)
|
|
}
|
|
h.Add(tagFileINodes, EntryInt32(inodes))
|
|
h.Add(tagFileDigestAlgo, EntryInt32(digestAlgo))
|
|
h.Add(tagFileVerifyFlags, EntryInt32(verifyFlags))
|
|
h.Add(tagFileRDevs, EntryInt16(fileRDevs))
|
|
h.Add(tagFileLangs, EntryStringSlice(fileLangs))
|
|
}
|
|
|
|
// AddPretrans adds a pretrans scriptlet
|
|
func (r *RPM) AddPretrans(s string) {
|
|
r.pretrans = s
|
|
}
|
|
|
|
// AddPrein adds a prein scriptlet
|
|
func (r *RPM) AddPrein(s string) {
|
|
r.prein = s
|
|
}
|
|
|
|
// AddPostin adds a postin scriptlet
|
|
func (r *RPM) AddPostin(s string) {
|
|
r.postin = s
|
|
}
|
|
|
|
// AddPreun adds a preun scriptlet
|
|
func (r *RPM) AddPreun(s string) {
|
|
r.preun = s
|
|
}
|
|
|
|
// AddPostun adds a postun scriptlet
|
|
func (r *RPM) AddPostun(s string) {
|
|
r.postun = s
|
|
}
|
|
|
|
// AddPosttrans adds a posttrans scriptlet
|
|
func (r *RPM) AddPosttrans(s string) {
|
|
r.posttrans = s
|
|
}
|
|
|
|
// AddFile adds an RPMFile to an existing rpm.
|
|
func (r *RPM) AddFile(f RPMFile) {
|
|
if f.Name == "/" { // rpm does not allow the root dir to be included.
|
|
return
|
|
}
|
|
r.files[f.Name] = f
|
|
}
|
|
|
|
// writeFile writes the file to the indexes and cpio.
|
|
func (r *RPM) writeFile(f RPMFile) error {
|
|
dir, file := path.Split(f.Name)
|
|
r.dirindexes = append(r.dirindexes, r.di.Get(dir))
|
|
r.basenames = append(r.basenames, file)
|
|
r.fileowners = append(r.fileowners, f.Owner)
|
|
r.filegroups = append(r.filegroups, f.Group)
|
|
r.filemtimes = append(r.filemtimes, f.MTime)
|
|
r.fileflags = append(r.fileflags, uint32(f.Type))
|
|
|
|
links := 1
|
|
switch {
|
|
case f.Mode&040000 != 0: // directory
|
|
r.filesizes = append(r.filesizes, 4096)
|
|
r.filedigests = append(r.filedigests, "")
|
|
r.filelinktos = append(r.filelinktos, "")
|
|
links = 2
|
|
case f.Mode&0120000 == 0120000: // symlink
|
|
r.filesizes = append(r.filesizes, uint32(len(f.Body)))
|
|
r.filedigests = append(r.filedigests, "")
|
|
r.filelinktos = append(r.filelinktos, string(f.Body))
|
|
default: // regular file
|
|
f.Mode = f.Mode | 0100000
|
|
r.filesizes = append(r.filesizes, uint32(len(f.Body)))
|
|
r.filedigests = append(r.filedigests, fmt.Sprintf("%x", sha256.Sum256(f.Body)))
|
|
r.filelinktos = append(r.filelinktos, "")
|
|
}
|
|
r.filemodes = append(r.filemodes, uint16(f.Mode))
|
|
|
|
// Ghost files have no payload
|
|
if f.Type == GhostFile {
|
|
return nil
|
|
}
|
|
return r.writePayload(f, links)
|
|
}
|
|
|
|
func (r *RPM) writePayload(f RPMFile, links int) error {
|
|
hdr := &cpio.Header{
|
|
Name: f.Name,
|
|
Mode: cpio.FileMode(f.Mode),
|
|
Size: int64(len(f.Body)),
|
|
Links: links,
|
|
}
|
|
if err := r.cpio.WriteHeader(hdr); err != nil {
|
|
return errors.Wrap(err, "failed to write payload file header")
|
|
}
|
|
if _, err := r.cpio.Write(f.Body); err != nil {
|
|
return errors.Wrap(err, "failed to write payload file content")
|
|
}
|
|
r.payloadSize += uint(len(f.Body))
|
|
return nil
|
|
}
|