Help us shape the future of Go! Take the 2026 Go Developer Survey

rpm

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: BSD-3-Clause Imports: 16 Imported by: 1

README

go-filesystems/rpm

go-filesystems/rpm

A pure-Go, CGO-free reader for RPM packages: the metadata, and the cpio payload as a filesystem.Filesystem.

No rpm library, no librpm, no shelling out. Works on every platform Go targets.

import (
    "os"

    "github.com/go-filesystems/rpm"
)

f, _ := os.Open("rootfiles-8.1-35.el9.noarch.rpm")
st, _ := f.Stat()

// Headers only: a few kilobytes, whatever the package weighs.
p, err := rpm.Metadata(f, st.Size())
// p.Name, p.Version, p.Release, p.Arch, p.Summary, p.License,
// p.PayloadFormat, p.PayloadCompressor, p.NEVRA()

// Headers and payload, as a filesystem.
fsys, err := rpm.OpenReader(f, st.Size())
body, err := fsys.ReadFile("usr/share/rootfiles/.bashrc")

API

func Metadata(r io.ReaderAt, size int64) (*Package, error)
func Open(r io.ReaderAt, size int64) (*FS, error)
func OpenLimit(r io.ReaderAt, size, maxPayload int64) (*FS, error)
func OpenReader(r io.ReaderAt, size int64) (filesystem.Filesystem, error)

filesystem is github.com/go-filesystems/interface v0.3.0. *FS satisfies filesystem.Filesystem and the optional filesystem.Opener; FS.OpenFile returns a filesystem.File whose ReadAt follows io.ReaderAt to the letter.

Every mutating method (WriteFile, MkDir, DeleteFile, DeleteDir, Rename) returns ErrReadOnly. An RPM's headers carry digests over the bytes that follow them and its payload is one compressed stream, so there is no in-place write that leaves a valid package behind.

Header is exposed for tags this package does not name: h.String(tag), h.Strings(tag), h.Int(tag), h.Ints(tag), h.Bytes(tag), h.Has(tag), h.Lookup(tag).

Errors
sentinel meaning
ErrNotRPM the lead magic is not ED AB EE DB — this is not an RPM
ErrCorrupt it is an RPM and something inside it does not hold together
ErrUnsupported a payload format that is not cpio, or a compressor that is not one of the five
ErrNotFound a path the payload does not carry — satisfies fs.ErrNotExist
ErrNotRegular the path resolved and is the wrong kind — satisfies fs.ErrInvalid
ErrReadOnly every mutating method
ErrTooLarge the package asks for more than the ceilings allow — see Untrusted input

⛔ ErrCorrupt is deliberately not wrapped in fs.ErrNotExist. A caller mapping this onto HTTP or NFS must be able to answer 500 for a damaged package and 404 for a name that is not in it; collapsing the two makes a real fault arrive as a routine miss and nothing anywhere reports it. Both directions are asserted in witness_test.go.

Memory

Metadata reads the two headers and nothing else.

⛔ Open decompresses the whole payload into memory, and that is the format rather than a shortcut: an RPM payload is one compressed stream, so the last file's bytes cannot be reached without decoding every byte before them. Budget for the installed size of the package, not its download size. It is also what makes filesystem.Opener honest here — the bytes are already resident, so a range read costs a copy, and the decoding is accounted for once at open instead of hidden inside each read.

Untrusted input

A package is read as hostile; every allocation or loop a header or the payload can size is bounded before it happens. Each row was measured on the code before the fix (TotalAlloc or wall time around the call):

What the file controls Before Bound now
the size the payload inflates to (gzip, bzip2, xz, zstd, none) 526 630-byte package, 256 MiB file: 1 073 799 504 bytes allocated OpenLimit(r, size, maxPayload); Open = DefaultMaxPayload (1 GiB); ErrTooLarge, having held at most maxPayload+1
a zstd window 9-byte frame: 537 151 024 bytes MaxDecoderWindow = 128 MiB, libzstd's own default limit
an xz LZMA2 dictionary ulikunitz/xz: 44-byte stream declaring 1.5 GiB: 1 610 663 976 bytes allocated (up to 4 GiB declarable) go-compressions/xz: the window grows with the data decoded, so the same stream allocates 38 064 bytes in all of Open; a window that would pass MaxDecoderWindow (128 MiB) is refused before it is allocated, in any block of any stream: ErrTooLarge
a path in the payload 1 MiB a/a/a/… (2 329 bytes of package): Open still running after 9 minutes (path.Dir per component) MaxPathLength = 4096 (PATH_MAX); parents found by last slash
files in one directory quadratic (slices.Contains per file) linear
entries, real plus undeclared parent directories unbounded MaxEntries = 4 194 304, checked in the cpio walk before the list is built (cpio.RecordsExactLimit)
h.Int(tag), h.String(tag) 64 Mi INT8 epoch: 603 994 688 bytes; 16 Mi empty strings as NAME: 285 222 360 bytes first value only, nothing allocated
string walking in a header 8 arrays × 8 Mi strings over one 8 MiB store: 1.55 s (65 535 arrays over a 100 MB store: days) total walked ≤ 2× the data store
header counts 1 Mi entries, 256 MiB rpm's own HEADER_TAGS_MAX 65 535 and HEADER_DATA_MAX 0x0fffffff (lib/header.cc)
a cpio size field with a sign Open never returned (cpio < v0.7.0) refused: go-filesystems/cpio v0.8.0

The memory a successful Open holds is the payload plus about 400 bytes per entry, so budget a small multiple of maxPayload. FuzzOpen ran 100 s over the whole reader without a finding.

The xz payload is decoded by go-compressions/xz, configured with MaxDictionary = MaxDecoderWindow. A Block Header's dictionary size is a claim the decoder never allocates from: the window starts at 16 KiB and doubles as output arrives, so only data that fills a window over the ceiling is refused (ErrTooLarge, wrapping xz.ErrTooLarge), not a header that merely declares one. This replaces a guard that read the first Block Header of the first stream only, which a second block or a second stream walked around; the ceiling now sits in the decoder and covers every block. ulikunitz/xz is no longer used to read anything; it remains a test-only dependency, to write the xz fixtures. The bound is tested with a planted bomb (TestAnXZDictionaryBombAllocatesNothing), which CI runs as a compiled binary under ulimit -v and GOMEMLIMIT.

The cpio reader comes from go-filesystems/cpio

The payload is a cpio archive and go-filesystems/cpio parses it. This package used to carry a second copy of that parser — newc only, about 220 lines — with a notice here saying so and asking for exactly this consolidation. The cost it named was real: a defect fixed in one copy stayed present in the other.

What stays here is the mapping onto this package's entry, the index in fs.go, and posixMode, which is the direction the shared parser does not go. The filesystem is deliberately not shared: a payload is one compressed stream, so it is in memory and entries are sections of it, while unarchive indexes into a file it keeps open.

It calls cpio.RecordsExact, not cpio.Records. A payload's length is known exactly, so a record that does not parse, a magic that is not one, and a missing trailer are all damage — not the block padding an initramfs legitimately carries. Records tolerates all three, and using it here turned three of this package's refusals into silent successes, which is how that distinction was found.

Two disagreements the consolidation settled

Neither was visible to any test on either side, because each repository only ever ran its own reader.

  • Every numeric header field is checked. This package did; the other ignored the errors on inode and mtime, so a header with a non-hex inode parsed to inode 0 and looked perfectly ordinary.
  • The inode is carried, because this package reports it in Stat. It is not portable across cpio variants: odc's field and the old binary variant's are too narrow to hold a real one, and cpio(1) renumbers them 1..n.

⚠⚠ Where the witnesses come from, and what that costs in confidence

No reference implementation was available. rpm, rpm2cpio and rpmbuild are all absent from the machine this was written on. So there are two kinds of control here and they are not of equal weight.

Real packages — the load-bearing witnesses

testdata/ holds three builds of the same source package by rpmbuild, spanning 2007 to 2024 and three payload compressors:

fixture built compressor sig. padding bytes
rootfiles-el5-gzip.rpm CentOS 5 (2007) gzip 0 4 686
rootfiles-el7-xz.rpm CentOS 7 (2020) xz 4 7 512
rootfiles-el9-zstd.rpm Rocky 9 (2024) zstd 4 9 556

All three are License: Public Domain — see testdata/README.md for the provenance and the redistribution reasoning. Their payloads carry five real regular files with real bytes, and the tests assert bytes, never counts: a count is satisfied by the right number of wrong entries, which is exactly what a mis-rounded newc pad produces.

What they witness, because the bytes are rpmbuild's and this package had no hand in them: the lead, both header structures, the 8-byte padding, the index's byte order, the tag numbering, the gzip/xz/zstd dispatch, and newc's 4-byte padding.

The expected values were read out with an independent 40-line Python script written from the specification. That is weaker than rpm would be: two readers written from one specification can share a misreading of it. The bytes are still rpmbuild's; only the interpretation is doubled rather than cross-checked.

Crafted fixtures — strictly weaker, and used only where nothing else exists

⚠ A fixture of the writer's own making cannot fail the way a real package can. If the builder and the parser misunderstand the format in the same way, the test agrees with the bug. The 8-byte padding is exactly that shape: omit it in both and the round trip is perfect, while every real package since 2000 fails.

So build_test.go is used only for what the real packages cannot provide:

  • a bzip2 payload, and an uncompressed one. rpm has supported both for twenty years; neither appears in any mainstream distribution, so no small public-domain package carries them. Go has no bzip2 writer either, so the bzip2 archive was compressed by CPython's bz2 from a cpio that a third implementation wrote, and committed as testdata/crafted-payload.cpio.bz2. The test cross-checks that blob against the Go builder byte for byte, so the two cannot drift apart unnoticed.
  • a package with no PAYLOADCOMPRESSOR tag, which rpm defines as gzip. Every package older than rpm 4.0 (2000) is in this class and none was obtainable small enough and clearly redistributable. The default is reasoned from rpm's source, not measured. This is the weakest claim in the package.
  • fifos, character and block devices, sockets in a payload, which exercise both directions of the st_mode translation. No rootfiles package has one.
  • every one of the ten index types in one header, and each corruption in turn.

The ablation table

Each ablation removes one rule and asserts the suite then fails on a real package. A rule nothing depends on passes its test and passes with the rule deleted, which is indistinguishable from a rule that matters — so the ablation is the measurement, not the test.

TestAblationHarnessMatchesTheReader is the control: the parametrised harness with every knob at its correct setting must reproduce Metadata exactly, on all three real packages. Without it the rest measures a copy.

# rule removed result note
1 the 8-byte padding after the signature header fails on 2 of 3 ⛔ PASSES on el5: its signature data ends at byte 440, already 8-aligned, so the padding is zero bytes wide and the rule is invisible to it. A reader tested against one pre-2000 package would ship the bug. This is why the witness set spans three build eras.
2 big-endian index fields (read little-endian) fails on 3 of 3 ⛔ and it produces no parse error. Tag 1000 read backwards is 0xE8030000, so every lookup misses and the package comes back with an empty name — "missing metadata", not a byte-order fault. Asserting on the error would have found nothing; the test asserts on the values.
3 the lead's 96-byte length (tried 64, 95, 97, 128) fails on 3 of 3 every offset refused at the signature magic
4 the payload compressor dispatch (each payload read as each other compressor) fails on 3 of 3 ⚠ none decompresses "successfully" — it hands the bytes through untouched — and is then refused one layer later, at the cpio magic. That is the case readCpioRecord's error message names explicitly, because it is where a missing PAYLOADCOMPRESSOR tag surfaces.
5 each tag lookup (NAME, VERSION, RELEASE, ARCH, SUMMARY, PAYLOADFORMAT), renumbered in the real file fails on 3 of 3 × 6 the file is mutated, not the reader, so this exercises the shipping lookup. Each renumbering empties exactly one field and moves nothing else. PAYLOADCOMPRESSOR is its own case: removed, it must give gzip, not "" — and the package must then fail to open, because it really is zstd.
6 newc's 4-byte padding (rounded to 1) fails on 3 of 3 1 record read instead of 5, 5 and 7

Two findings, both recorded above: ablation 1 passes on one of the three witnesses, and ablation 2 fails silently rather than loudly.

A further measurement: the lead cannot be trusted

TestLeadDisagreesWithHeader records it. All three packages are noarch by their ARCH tag; their lead Archnum fields say 255, 1 and 255. A caller that decided the architecture from the lead would get a different answer per build era for the same package. Metadata takes every fact from the main header and exposes Lead only so a tool can show what the lead claims.

Development

export GOWORK=off GOFLAGS=-mod=mod
go vet ./...
go test -race ./...

CI runs 4 native lanes (linux/amd64, linux/arm64, darwin/arm64, windows/amd64) and 4 emulated ones (riscv64, loong64, ppc64le, s390x) behind a 100% statement coverage gate.

⚠ The emulated lanes copy a go test -c binary into a QEMU container with no testdata/ beside it, so every fixture is embedded with //go:embed. Verify that yourself with:

go test -c -o /tmp/t.test . && cd /tmp && ./t.test

Documentation

Overview

Package rpm reads RPM packages: their metadata, and their cpio payload as a filesystem.

It is pure Go, CGO-free, and depends on no rpm library. Two entry points:

func Metadata(r io.ReaderAt, size int64) (*Package, error)
func OpenReader(r io.ReaderAt, size int64) (filesystem.Filesystem, error)

Metadata reads the lead and the two header structures and stops; its cost is a few kilobytes whatever the package weighs. OpenReader additionally decompresses the payload and returns it as a github.com/go-filesystems/interface Filesystem. Open is OpenReader with the concrete *FS, for a caller that wants Package() or the optional interfaces by name.

The filesystem is READ-ONLY: every mutating method returns ErrReadOnly. The optional filesystem.Opener is implemented, so a caller can read a byte range of one payload file without materialising it; see FS.OpenFile for why that is honest here and would not be for a driver that streamed.

The format, and the one rule that catches everyone

A .rpm is: a 96-byte lead, a signature header structure, the header proper, then the payload. Everything is big-endian. Each header structure is a 3-byte magic, a version, four reserved bytes, an entry count, a data size, that many 16-byte index entries, and that many bytes of data store.

⛔ THE SIGNATURE HEADER'S DATA STORE IS PADDED TO AN 8-BYTE BOUNDARY AND THE HEADER PROPER'S IS NOT. Getting this wrong lands 1 to 7 bytes inside the second magic and the error reads as a corrupt package. It is asymmetric, it is easy to apply in the wrong place, and -- measured here -- it is invisible to a substantial minority of real packages, whose signature data happens to end 8-aligned already. See nextHeaderOffset and README.md's ablation table.

The cpio reader comes from go-filesystems/cpio

The payload is a cpio archive and github.com/go-filesystems/cpio parses it. This package used to carry a second copy of that parser -- newc only, about 220 lines -- with a notice here saying so and asking for exactly this consolidation. The cost it named was real: a defect fixed in one copy stayed present in the other.

What stays here is the mapping onto this package's entry, the index in fs.go, and posixMode, which is the direction the shared parser does not go. The FILESYSTEM is deliberately not shared: a payload is one compressed stream, so it is in memory and entries are sections of it, while unarchive indexes into a file it keeps open.

It calls cpio.RecordsExact rather than cpio.Records. A payload's length is known exactly, so a record that does not parse, a magic that is not one, and a missing trailer are all damage -- not the block padding an initramfs legitimately carries. Records tolerates all three, and using it here turned three of this package's refusals into silent successes, which is how that distinction was found.

Two things the consolidation settled, both of them disagreements between the two readers that no test on either side could see:

  • the shared parser checks EVERY numeric header field. This package did; the other ignored the errors on inode and mtime, so a header with a non-hex inode parsed to inode 0 and looked ordinary.
  • the inode is carried, because this package reports it in Stat. It is NOT portable across cpio variants: odc's field and the old binary variant's are too narrow to hold a real one, and cpio(1) renumbers them 1..n.

⚠⚠ WHERE THE WITNESSES COME FROM, AND WHAT THEY CANNOT SHOW

No reference implementation exists on the machine this was written on: no rpm, no rpm2cpio, no rpmbuild. So the controls are of two kinds, and they are not of equal weight.

REAL PACKAGES (testdata/rootfiles-el{5,7,9}-*.rpm). Three builds of the same source package, by rpmbuild, spanning 2007 to 2024 and the gzip, xz and zstd payload compressors. Their BYTES are rpmbuild's, which is what makes them worth having: the lead, both headers, the 8-byte padding, the index's byte order, the tag numbering and newc's 4-byte padding are all witnessed against output this package had no hand in. The EXPECTED VALUES were read out with an independent 40-line Python script written from the specification -- a weaker control than rpm itself, because two readers written from one specification can share a misreading of it. They are Public Domain and redistributable; see testdata/README.md.

CRAFTED FIXTURES (build_test.go). A package this reader's own tests assemble. ⚠ A fixture of the writer's own making CANNOT FAIL THE WAY A REAL PACKAGE CAN: if the builder and the parser misunderstand the format the same way, the test agrees with the bug. The 8-byte padding is exactly that shape -- omit it in both and the round trip is perfect while every real package since 2000 fails.

So the crafted fixtures are used only for what the real ones cannot provide, and each is a strictly weaker witness than a real package would be:

  • a bzip2 payload, and an uncompressed one. rpm has supported both for twenty years and neither appears in any mainstream distribution, so no small public-domain package carries them. Go has no bzip2 WRITER either, so the bzip2 archive was compressed by CPython's bz2 module from a cpio that a third implementation wrote, and committed as a blob; the test cross-checks that blob against build_test.go's builder so the two cannot drift apart unnoticed.
  • a package with NO PAYLOADCOMPRESSOR TAG, which rpm defines as gzip. Every package older than rpm 4.0 (2000) is in this class and none was obtainable small enough and clearly redistributable; the default is therefore reasoned from rpm's source, not measured. THIS IS THE WEAKEST CLAIM IN THE PACKAGE.
  • fifos, character and block devices, and sockets in a payload, which exercise both directions of the st_mode translation. No rootfiles package has one.
  • every one of the ten index types in one header, and each corruption in turn.

Reading the tests

witness_test.go asserts BYTES per payload entry, never counts: a count is satisfied by the right number of wrong entries, and a mis-rounded newc pad produces exactly that. ablation_test.go removes one rule at a time and asserts the suite then fails; two of its six ablations have something to report, and README.md tabulates all of them.

Index

Constants

View Source
const (
	CompressorNone  = "none"
	CompressorGzip  = "gzip"
	CompressorBzip2 = "bzip2"
	CompressorXz    = "xz"
	CompressorZstd  = "zstd"
)

The five values rpm writes into PAYLOADCOMPRESSOR.

View Source
const (
	// DefaultMaxPayload is what Open allows a payload to inflate to: 1 GiB, the
	// installed size of a very large package. OpenLimit takes another.
	DefaultMaxPayload = 1 << 30
	// MaxDecoderWindow bounds the memory a compressor may ask for up front: a zstd
	// window or an xz LZMA2 dictionary. 128 MiB is libzstd's default limit.
	MaxDecoderWindow = 128 << 20
	// MaxEntries bounds the entries of the payload, real and synthesised parent
	// directories together. A real package has hundreds of thousands at most.
	MaxEntries = 1 << 22
	// MaxPathLength bounds one entry's path: Linux's PATH_MAX (limits.h), which is
	// also what the kernel's initramfs unpacker refuses beyond (init/initramfs.c).
	MaxPathLength = 4096
)

The ceilings. An RPM is untrusted input and a payload is the one thing in it that is compressed, so what it inflates to is the file's choice unless these say otherwise. Each is a number the format does not fix; the reference each was taken from is named where it is used.

View Source
const (
	TagName        = 1000
	TagVersion     = 1001
	TagRelease     = 1002
	TagEpoch       = 1003
	TagSummary     = 1004
	TagDescription = 1005
	TagBuildTime   = 1006
	TagSize        = 1009
	TagVendor      = 1011
	TagLicense     = 1014
	TagPackager    = 1015
	TagGroup       = 1016
	TagURL         = 1020
	TagOS          = 1021
	TagArch        = 1022

	// TagFileSizes and its neighbours describe the payload's files in
	// PARALLEL ARRAYS -- one entry per file, in payload order. They are not
	// read here: the cpio payload carries the same facts and carries them
	// with the bytes, so reading them from the header would be a second
	// source that can disagree with the first.
	TagFileSizes = 1028
	TagFileModes = 1030
	TagFileNames = 1027 // retired in rpm 4; see TagBaseNames

	TagSourceRPM   = 1044
	TagArchiveSize = 1046
	TagBaseNames   = 1117
	TagDirNames    = 1118

	// TagPayloadFormat is very nearly always "cpio". rpm 5 could write
	// "drpm"; nothing this reader opens does.
	TagPayloadFormat = 1124
	// TagPayloadCompressor names the stream wrapped around the cpio:
	// "gzip", "bzip2", "xz", "zstd" or "none".
	//
	// ⛔ ITS ABSENCE IS NOT AN ERROR AND NOT "none". A package built before
	// rpm 4.0 carries no such tag and its payload is gzip: that is what
	// rpmlib's own default is, and treating a missing tag as an
	// uncompressed payload turns every pre-2000 package into a corrupt one.
	TagPayloadCompressor = 1125
	TagPayloadFlags      = 1126
)

The tags this reader names. RPM defines several hundred; these are the ones something here reads, plus the few a caller most often wants next.

The numbers are rpm's own (rpmtag.h). They are part of the on-disk format and cannot be renumbered, which is why they are written as literals rather than as an iota block: a tag's value is data, not a position in a list.

View Source
const (
	SigTagSize        = 1000 // size of the header + payload, in bytes
	SigTagMD5         = 1004
	SigTagGPG         = 1005
	SigTagPayloadSize = 1007
	SigTagSHA1        = 269
	SigTagSHA256      = 273
)

Signature-header tags. They share the numeric space with the main header's but mean different things in it, which is the reason the two headers are kept as separate values here instead of being merged into one tag map.

View Source
const PayloadFormatCpio = "cpio"

PayloadFormatCpio is the only PAYLOADFORMAT this reader opens.

Variables

View Source
var ErrCorrupt = errors.New("rpm: corrupt package")

ErrCorrupt is returned when the file IS an RPM -- the lead magic matched -- and something inside it does not hold together: a header magic that is not there, a count or size that runs past the end of the file, an index entry whose offset points outside its own data store.

⛔ It is deliberately NOT wrapped in fs.ErrNotExist. A caller that maps a driver's errors onto HTTP or NFS status must be able to answer 500 for a damaged package and 404 for a name that is not in it; collapsing the two makes a corrupt package look like a routine miss and nothing anywhere reports the fault. See the error-contract note in github.com/go-filesystems/interface.

View Source
var ErrNotFound = fmt.Errorf("rpm: path not found in payload: %w", iofs.ErrNotExist)

ErrNotFound is the driver's single "that path is not in this package" sentinel, and it satisfies the interface module's error contract: errors.Is(err, fs.ErrNotExist) is true for it and for anything wrapping it.

Every site that raises it means a name that the payload does not carry. A payload that cannot be parsed raises ErrCorrupt instead, so the two do not share a sentinel and a broken package never reads as a 404 -- which is the distinction the interface module asks each driver to check before wrapping.

View Source
var ErrNotRPM = errors.New("rpm: not an RPM package (bad lead magic)")

ErrNotRPM is returned when the 96-byte lead does not begin with RPM's magic. It is the one error that means "this is not an RPM at all", which is what a format-probing caller such as go-filesystems/detect needs to tell apart from "this is an RPM and something in it is wrong".

View Source
var ErrNotRegular = fmt.Errorf("rpm: not a regular file: %w", iofs.ErrInvalid)

ErrNotRegular is returned by OpenFile and ReadFile for a path that is in the payload and is not a regular file -- a directory, a symlink, a device node. It satisfies fs.ErrInvalid rather than fs.ErrNotExist: the name resolved, so answering 404 would be wrong.

View Source
var ErrReadOnly = errors.New("rpm: package is read-only")

ErrReadOnly is returned by every mutating method. An RPM package is a distribution artefact: its payload is a single compressed stream and its headers carry digests over the bytes that follow them, so there is no in-place write that would leave a valid package behind.

View Source
var ErrTooLarge = errors.New("rpm: over the size ceiling")

ErrTooLarge is returned when a package asks for more memory than the ceilings allow: a payload that inflates past the limit given to OpenLimit (a compression bomb is a few hundred kilobytes of file for gigabytes of payload), a compressor window or dictionary over MaxDecoderWindow, more entries than MaxEntries, or a path longer than MaxPathLength. The package may be a well-formed one that this reader declines to hold; it is not ErrCorrupt.

View Source
var ErrUnsupported = errors.New("rpm: unsupported")

ErrUnsupported is returned for a package this reader understands well enough to identify and not well enough to open: a payload format that is not cpio, or a compressor that is not one of the five named in PAYLOADCOMPRESSOR.

Functions

func OpenReader

func OpenReader(r io.ReaderAt, size int64) (filesystem.Filesystem, error)

OpenReader is Open under the name every driver in go-filesystems answers to.

github.com/go-filesystems/detect registers drivers as func(io.ReaderAt, int64) (filesystem.Filesystem, error); Open is the right shape but returns the concrete *FS, which is a different type and does not fit. A caller writes detect.Register(detect.RPM, rpm.OpenReader) and is done.

Types

type Entry

type Entry struct {
	Tag    uint32
	Type   Type
	Offset uint32
	Count  uint32
}

Entry is one 16-byte index entry: which tag, of what type, where in the header's data store, and how many of them.

type FS

type FS struct {
	// contains filtered or unexported fields
}

FS is an opened RPM package seen as a filesystem: the cpio payload, decompressed, indexed by path.

It satisfies filesystem.Filesystem and filesystem.Opener, and every mutating method returns ErrReadOnly.

func Open

func Open(r io.ReaderAt, size int64) (*FS, error)

Open reads an RPM's metadata AND decompresses its payload, returning the cpio contents as a filesystem.

⛔ The whole payload is decompressed into memory here, because an RPM payload is one compressed stream and no byte in it is reachable without decoding every byte before it. Budget for the INSTALLED size of the package. A caller that only wants the name and version should call Metadata, which reads the headers alone.

func OpenLimit added in v0.4.0

func OpenLimit(r io.ReaderAt, size, maxPayload int64) (*FS, error)

OpenLimit is Open with the payload allowed to inflate to maxPayload bytes rather than DefaultMaxPayload; maxPayload <= 0 means the default. A payload that inflates past it is refused with ErrTooLarge, having held at most maxPayload+1 bytes (the buffer may touch twice that while it grows).

The memory a successful Open holds is the payload plus an index of its entries, about 400 bytes per entry (MaxEntries caps them), so budget a small multiple of maxPayload.

func (*FS) Close

func (f *FS) Close() error

Close releases nothing: the payload is a byte slice and the io.ReaderAt it was read from belongs to the caller. It is here because filesystem.Filesystem asks for it, and it returns nil rather than pretending to a flush it does not do.

func (*FS) DeleteDir

func (f *FS) DeleteDir(string) error

func (*FS) DeleteFile

func (f *FS) DeleteFile(string) error

func (*FS) ListDir

func (f *FS) ListDir(p string) ([]filesystem.DirEntry, error)

ListDir lists one directory. The root is "", "." or "/".

func (*FS) MkDir

func (f *FS) MkDir(string, os.FileMode) error

func (*FS) OpenFile

func (f *FS) OpenFile(p string) (filesystem.File, error)

OpenFile satisfies filesystem.Opener.

It is implemented, rather than left out, because the payload is already resident: decompressPayload had to materialise the whole cpio to reach any of it, so a byte range costs a copy. The interface module's own guidance is that a driver which cannot answer a range without decoding everything before it should NOT implement Opener -- the decoding here happens once, at open, and is accounted for there rather than hidden inside each read.

func (*FS) Package

func (f *FS) Package() *Package

Package returns the metadata of the package this filesystem came from, so a caller that opened the payload does not have to parse the headers twice.

func (*FS) ReadFile

func (f *FS) ReadFile(p string) ([]byte, error)

ReadFile returns the whole contents of a regular file in the payload.

func (f *FS) ReadLink(p string) (string, error)

ReadLink returns a symlink's target, as the payload stored it. The target is not resolved: a relative target stays relative.

func (*FS) Rename

func (f *FS) Rename(string, string) error

func (*FS) Stat

func (f *FS) Stat(p string) (filesystem.Stat, error)

Stat reports mode, size and inode for a path.

func (*FS) WriteFile

func (f *FS) WriteFile(string, []byte, os.FileMode) error

The mutating half of filesystem.Filesystem. An RPM's headers carry digests over the bytes that follow them and its payload is a single compressed stream, so there is no in-place write that leaves a valid package behind.

type Header struct {
	Version uint8
	// Entries are in the order the file lists them, which is rpm's own tag
	// order and is therefore sorted ascending in every package measured.
	// Nothing here relies on that.
	Entries []Entry

	// Offset is where this header's magic sits in the file, and End is the
	// first byte after its data store -- BEFORE any alignment padding. The
	// padding is the caller's business because only the signature header has
	// it; see nextHeaderOffset.
	Offset, End int64
	// contains filtered or unexported fields
}

Header is one of an RPM's two header structures: the signature, or the header proper. Both have exactly this shape, which is why one parser reads both.

The data store is held whole. A header is at most a few kilobytes -- the largest in this repository's witnesses is 4.8 KiB -- so copying it once buys accessors that cannot fail and need no io.ReaderAt of their own.

func (*Header) Bytes

func (h *Header) Bytes(tag uint32) []byte

Bytes returns the raw value stored under a BIN or CHAR tag -- a digest, a signature, a blob. The slice aliases the header's own copy of the data store and must not be written to.

func (*Header) Has

func (h *Header) Has(tag uint32) bool

Has says whether the header carries the tag at all.

func (*Header) Int

func (h *Header) Int(tag uint32) (int64, bool)

Int returns the first integer stored under tag, and whether there was one.

⛔ Like String it reads the first value and builds no slice: Ints widens every value to 8 bytes, so an INT8 entry of 256 Mi values would be 2 GiB to answer "what is the epoch".

func (*Header) Ints

func (h *Header) Ints(tag uint32) []int64

Ints returns every integer stored under tag, widened to int64. CHAR and INT8 count as integers here; BIN does not, because a BIN entry is a digest or a blob and reading it as numbers is never what a caller meant.

func (*Header) Lookup

func (h *Header) Lookup(tag uint32) (Entry, bool)

Lookup returns the index entry for a tag.

func (*Header) String

func (h *Header) String(tag uint32) string

String returns the first string stored under tag, or "" if the tag is absent or holds something that is not a string.

"" for an absent tag is the right default HERE and would not be everywhere: every string tag this package reads is one whose absence and whose emptiness mean the same thing to a caller (no summary, no license, no compressor named). The one tag where that is false -- PAYLOADCOMPRESSOR, whose absence means gzip and not "no compression" -- is read through Has first. See payloadCompressor.

⛔ It reads the FIRST string and builds nothing else. It used to call Strings and take element 0, which allocates a slice of Count strings: 16 bytes each, for a header whose store is 256 MiB of NULs, is 4 GiB to return one empty string -- and Metadata calls this for fifteen tags. Measured on the old code: a 16 MiB package whose NAME was a STRING_ARRAY of 16 Mi empty strings made Metadata allocate 268 MB.

func (*Header) Strings

func (h *Header) Strings(tag uint32) []string

Strings returns every string stored under tag. A STRING or I18NSTRING yields at most one; a STRING_ARRAY yields Count of them.

type Lead

type Lead struct {
	Major uint8 // 3 in every package rpm has written since 1997
	Minor uint8
	// Type is 0 for a binary package and 1 for a source package.
	Type uint16
	// Archnum is a number from rpm's own table, not a Linux machine type.
	Archnum uint16
	// Name is the 66-byte NUL-padded "name-version-release" field, trimmed.
	// It is truncated, not wrapped, for a package whose NVR is longer.
	Name    string
	Osnum   uint16
	SigType uint16 // 5 for the header-structure signature used since rpm 2.1
}

Lead is the 96-byte prologue of every RPM package.

⛔ IT IS LEGACY AND ITS FIELDS ARE NOT TO BE TRUSTED. rpm stopped maintaining the lead in 2000 and keeps writing it only so that file(1) and older tools still recognise a package. The measured witnesses in this repository show why reading anything but the magic out of it is a mistake: `Archnum` is 255 in both the el5 and el9 noarch packages and 1 in the el7 one, for three packages whose ARCH tag says "noarch" in all three. A caller that decided the architecture from the lead would get a different answer per build era for the same package.

So this type exists to be VALIDATED (the magic) and EXPOSED (for a tool that wants to show what the lead claims), and Metadata takes every fact it reports, architecture included, from the main header instead.

type Package

type Package struct {
	Name    string
	Version string
	Release string
	Arch    string
	Summary string

	// Epoch is absent from most packages. rpm treats an absent epoch as 0 for
	// comparison but an ABSENT epoch and an epoch of 0 are distinguishable in
	// the header, so the flag says which this was rather than collapsing them.
	Epoch    int64
	HasEpoch bool

	License     string
	Description string
	Group       string
	OS          string
	Vendor      string
	Packager    string
	URL         string
	SourceRPM   string

	// PayloadFormat is PAYLOADFORMAT, almost always "cpio".
	PayloadFormat string
	// PayloadCompressor is PAYLOADCOMPRESSOR, or "gzip" for a package that
	// carries no such tag -- see payloadCompressor for why the default is
	// gzip and not "none".
	PayloadCompressor string

	// PayloadOffset is the first byte of the compressed payload: the byte
	// after the main header's data store, with NO padding. PayloadSize is
	// what is left of the file from there.
	PayloadOffset int64
	PayloadSize   int64

	// Lead, Signature and Header are the raw structures, for a caller that
	// wants a tag this type does not name.
	Lead      *Lead
	Signature *Header
	Header    *Header
}

Package is what an RPM says about itself.

Every field here is taken from the MAIN HEADER and none from the lead, which is legacy and disagrees with the header about the architecture in two of this repository's three witnesses. Lead is kept so a caller can see what the lead claims, clearly labelled as its own.

func Metadata

func Metadata(r io.ReaderAt, size int64) (*Package, error)

Metadata reads the lead and both header structures and returns what the package says about itself. It does NOT touch the payload, so its cost is the size of the two headers -- a few kilobytes -- whatever the package weighs.

func (*Package) NEVRA

func (p *Package) NEVRA() string

NEVRA is the canonical "name-[epoch:]version-release.arch" spelling.

type Type

type Type uint32

Type is an index entry's value type, as stored in the entry's second u32.

const (
	TypeNull Type = iota
	TypeChar
	TypeInt8
	TypeInt16
	TypeInt32
	TypeInt64
	TypeString
	TypeBin
	TypeStringArray
	TypeI18NString
)

The ten index types. RPM has never added an eleventh.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL