Documentation
¶
Overview ¶
Package unarchive opens an archive without being told what it is, and extracts it while checking that what came out is what the archive promised.
Why the bytes and not the name ¶
A format is recognised by what is IN the file. An extension is a claim by whoever last renamed it: a .rar that is really a zip, a video saved as .mp4 that is an MPEG transport stream, a volume renamed by a download manager -- all ordinary, and all wrong if the name decides.
Why the check ¶
An extractor that reports success is not the same as an extractor that got everything. libarchive, which is bsdtar and therefore macOS, does not follow a multi-volume RAR chain: it writes the first volume, pads the remainder with ZEROS to the declared length and exits 0. The result is exactly the right SIZE, which is what every check afterwards looks at. This package compares what was written against what the header declared, per entry, and says so when they differ.
Index ¶
- Constants
- Variables
- func FromFS(fsys iofs.FS) filesystem.Filesystem
- func FromFSWithModes(fsys iofs.FS, modes map[string]os.FileMode) filesystem.Filesystem
- func InnerName(name string) string
- func SealTo(o *overlay.Overlay, target string) error
- func WithoutPartNumber(name string) string
- type Entry
- type Format
- type Limits
- type Options
- type Result
- type WriteTarget
Constants ¶
const ( // DefaultMaxBytes is 64 GiB. DefaultMaxBytes = 64 << 30 // DefaultMaxEntries is 4,194,304. DefaultMaxEntries = 1 << 22 // DefaultMaxDepth is 1024 levels, a quarter of Linux's PATH_MAX at the // shortest name an entry can have. DefaultMaxDepth = 1024 )
Defaults for Options.
Variables ¶
var DefaultLimits = Limits{
MaxEntries: 1 << 20,
MaxReadFile: 1 << 30,
MaxSpool: 64 << 30,
MaxDictionary: 256 << 20,
}
DefaultLimits is what Open and the readers behind it apply. It is process wide: set its fields once at start-up, before opening anything; it is read, never written, by this package.
The defaults sit above anything an honest archive needs and below what a few dozen hostile bytes can ask for: a 9-byte LZMA header asked for a 1 GiB dictionary and a 218-byte zip for a 4 GiB ReadFile.
var ErrCannotWrite = errors.New("unarchive: this format is read but not written")
ErrCannotWrite is returned for a format this package reads and does not write.
A third sentence alongside the other two: "I know what this is, I can read it, and I cannot produce one". Only RAR is left in it, and for a reason that is not going to change here: no free writer exists.
bzip2 used to be in it too, because the standard library decompresses bzip2 and nothing compressed it. That was a missing LIBRARY rather than a property of the format, so the library was written -- go-compressions/bzip2 -- and the sentence stopped being true. Worth saying, because the two reasons look identical from the outside and only one of them is permanent.
var ErrEscapes = errors.New("unarchive: entry would be written outside the destination")
ErrEscapes is returned for an entry whose path would land outside the destination.
An archive is somebody else's list of names, and "../../.ssh/authorized_keys" is a name. Every extractor that trusted it has had the same bug, so this one does not trust it: the resolved path is checked against the destination, and an entry that leaves it is refused rather than sanitised, because silently rewriting a path puts the file somewhere the person did not ask for either.
var ErrExists = errors.New("unarchive: file exists")
ErrExists is returned when a file is already there and Overwrite is not set.
var ErrIncomplete = errors.New("unarchive: entry is shorter than its header declares")
ErrIncomplete is returned when an entry yielded fewer bytes than its header declared.
It is the point of this package. An extractor that writes what it got and reports success leaves a file of exactly the right SIZE when the source was zero-padded, and every check afterwards agrees with it.
var ErrMissingPart = errors.New("unarchive: a part of this split is missing")
ErrMissingPart is returned when a numbered set has a hole in it.
A hole is not the same as the end. Concatenating parts 1, 2 and 4 produces a file of a plausible SIZE whose middle is wrong, and every reader then reports corruption -- of the archive, which is intact. Refusing here names the part that is absent.
var ErrNestedUnderNonDirectory = errors.New("unarchive: an entry is nested under something that is not a directory")
ErrNestedUnderNonDirectory is returned when an archive lists an entry beneath a path that is not a directory -- a file under a symbolic link, most often.
var ErrNotImplemented = errors.New("unarchive: recognised, but this format is not read yet")
ErrNotImplemented is returned for a format that is recognised and not yet read. Telling the two apart matters to somebody holding a file: "I do not know what this is" and "I know exactly what this is and cannot open it yet" send them to different places.
var ErrReadOnly = errors.New("unarchive: archive is read-only")
ErrReadOnly is returned by every mutating method of a filesystem this package builds. An archive is read-only.
var ErrTooDeeplyNested = errors.New("unarchive: more stream wrappers than this will peel")
ErrTooDeeplyNested is returned when a file is wrapped more times than maxNesting.
var ErrTooLarge = errors.New("unarchive: exceeds the size limit")
ErrTooLarge is returned when an archive asks for more than a limit allows: more entries, longer paths, a larger dictionary, a bigger spool, a larger ReadFile than Limits (or, for Extract, Options) permit. It is raised before the memory or the disk is spent. The archive is not necessarily corrupt: raise the limit if the size is expected.
var ErrUnknownFormat = errors.New("unarchive: the bytes match no format this knows")
ErrUnknownFormat is returned when nothing recognises the bytes.
Functions ¶
func FromFS ¶
func FromFS(fsys iofs.FS) filesystem.Filesystem
FromFS presents any io/fs.FS as a filesystem.Filesystem.
It exists because the standard library already answers fs.FS for some of these formats -- a *zip.Reader is one -- so the driver for them is an adaptation and not a decoder. Writing a second zip reader to satisfy a different interface would be a duplicate of something already correct.
⛔ Random access costs what the underlying fs.FS charges. An fs.File is a stream: unless it also answers io.ReaderAt, reaching an offset means reading what comes before it, and reading BACKWARDS means opening the entry again. A zip entry is stored on its own, so a fresh open is cheap; an entry inside a solidly-compressed archive is not, and that is the format's shape rather than this adapter's.
func FromFSWithModes ¶
func FromFSWithModes(fsys iofs.FS, modes map[string]os.FileMode) filesystem.Filesystem
FromFSWithModes is FromFS for an archive whose own metadata records modes the io/fs view does not report.
⛔ It exists because both io/fs adapters here LOSE a directory's permissions. archive/zip answers every directory with `fs.ModeDir | 0555` from its synthesised fileListEntry, and bodgit/sevenzip does the same, whatever the archive stored -- so a 0755 directory converted to tar came out dr-xr-xr-x, and the extracted tree could not be deleted without a chmod. Measured, not guessed: tar to tar kept 0755, zip to tar and 7z to tar both lost the owner's write bit, which is what pointed at the adapters rather than at the writers.
The map is keyed by path with no trailing slash. An entry whose recorded permissions are ZERO is skipped rather than believed: some archives carry no Unix mode at all, and a mode of 0 is the absence of a statement rather than a statement that nobody may read the file.
func InnerName ¶
InnerName is what the stream inside a compressed wrapper is called: the base name with the wrapper's suffix taken off, so notes.txt.gz gives notes.txt and archive.tgz gives archive.tar.
It is exported because the command needs the same answer for a different question -- what to call the directory it extracts into -- and computing it from filepath.Ext there gives "fixture.tar" for fixture.tar.gz, which is a directory named after half a suffix.
func SealTo ¶
SealTo writes everything the overlay holds into target, once.
The format comes from target's name -- see TargetFor -- and the write is atomic: the archive is built beside target and RENAMED over it, so an interrupted seal leaves whatever was there before untouched rather than half-replaced.
func WithoutPartNumber ¶ added in v0.4.0
WithoutPartNumber strips a trailing part number, so film.zip.001 gives film.zip.
It is exported for the same reason InnerName is: the command needs the answer for a different question -- what to call the directory it extracts into -- and computing it from filepath.Ext there leaves "film.zip", a directory named after the archive's own extension.
Types ¶
type Entry ¶
Entry is one thing an archive holds, as List reports it.
func List ¶
func List(fsys filesystem.Filesystem) ([]Entry, error)
List names everything in fsys without writing anything.
It walks the same way Extract does -- and refuses the same names, so a person who lists before extracting is told about an entry that would leave the destination BEFORE several gigabytes land somewhere.
type Format ¶
type Format string
Format is an archive format this package can recognise.
const ( FormatUnknown Format = "" FormatRAR Format = "rar" // RAR 1.5-4.x and RAR5, single or multi-volume FormatZIP Format = "zip" // PKZIP, including the jar/epub/odf family Format7z Format = "7z" // 7-Zip FormatTar Format = "tar" // POSIX tar: v7, USTAR, PAX and GNU alike FormatAr Format = "ar" // the static-library and .deb container FormatCpio Format = "cpio" // an initramfs, and an RPM's payload FormatGzip Format = "gzip" // a stream wrapper, usually around a tar FormatBzip2 Format = "bzip2" // likewise FormatXZ Format = "xz" // likewise FormatZstd Format = "zstd" // likewise FormatLZ4 Format = "lz4" // likewise // compress(1)'s .Z, read and not written -- see Format.Note. FormatZ Format = "compress" // Two more stream wrappers, same shape as the ones above. FormatLZO Format = "lzo" // lzop's container around LZO1X FormatLzip Format = "lzip" // LZMA in lzip's framing, not xz's // Archives whose driver reads them through an io.ReaderAt, like the images // below -- but they are archives, not filesystems, and they do NOT adopt the // handle they are given. See openReaderArchive. FormatXAR Format = "xar" // the macOS .pkg container FormatCAB Format = "cab" // Microsoft Cabinet FormatRPM Format = "rpm" // lead + headers + one compressed cpio payload FormatWARC Format = "warc" // a web crawl, one record per entry // Filesystem IMAGES, which are archives in every way that matters here: one // file holding a tree, handed around to be unpacked. They are read through // the org's own drivers rather than anything written here. // // The line is drawn at formats people DISTRIBUTE. You download an .iso and // you ship a .squashfs; you do not pass somebody an ext4 image expecting them // to unpack it. go-filesystems has drivers for a dozen more and this opens // two, on purpose. FormatISO9660 Format = "iso9660" FormatSquashFS Format = "squashfs" // HFS+ is here for the same reason: it is what a .dmg made before APFS // holds, so it is a format people receive rather than one they administer. FormatHFSPlus Format = "hfsplus" // A UDIF disk image -- a .dmg -- which is a CONTAINER around one of the // images above rather than an image itself. Its sectors are compressed and // may be stored out of order, so there is nothing for a driver to read in // place; it is peeled like a stream wrapper and what comes out is sniffed. FormatDMG Format = "dmg" // Recognised and deliberately not unpacked. See Format.Note. FormatPtar Format = "ptar" )
func Open ¶
func Open(path string) (filesystem.Filesystem, Format, error)
Open reads the archive at path, deciding what it is from its bytes, and returns it as a filesystem together with the format it turned out to be.
A multi-volume set is followed from here: the rar driver resolves the rest of the set by volume NUMBER within path's directory, so files carrying an id a download inserted are still reached and nothing is renamed.
func Sniff ¶
Sniff names the format of the archive or filesystem image in r.
The size is needed for the image half and not for the archive half: an archive's identity is in its first 262 bytes, while ISO 9660's is at offset 32769 and a probe that deep has to know where the file ends, or a truncated image turns into a read past the end.
The image probing is go-filesystems/detect's, not a second table written here. It is bounded through safeio, it handles both endiannesses of squashfs, and it knows a dozen filesystems -- so duplicating the two entries this package opens would mean re-deriving something deliberately hardened, and would drift from it.
func Writable ¶
Writable opens an archive and returns it as a filesystem that can be CHANGED, without rewriting the archive.
Changes go into a temporary cache; the archive underneath is untouched until SealTo. So renaming one entry in a 4 GiB archive costs a rename, not 4 GiB of rewriting, and an interrupted session leaves the original exactly as it was.
The caller closes the returned Overlay, which discards the cache. Closing it does NOT write anything back: that is SealTo's job, and keeping them apart is what makes "I changed my mind" free.
func (Format) Compressed ¶
Compressed says whether a format is a stream wrapper rather than an archive of its own: gzip holds one stream, which is usually a tar and might be anything.
func (Format) Image ¶ added in v0.3.0
Image says whether a format is a filesystem image rather than an archive.
The distinction is not cosmetic: an image is already a filesystem, so there is nothing to decode and nothing to spool -- Open hands back a driver reading the file in place.
func (Format) Note ¶ added in v0.4.0
Note returns a sentence about a format this package recognises and does not unpack, or "" for the rest.
It exists because "recognised, not read yet" is true of ptar and MISLEADING on its own: it suggests somebody will get round to it. What a .ptar is makes that the wrong expectation, and the person holding one is better served by being told which tool opens it than by waiting.
func (Format) ReaderArchive ¶ added in v0.9.0
ReaderArchive says whether a format's driver reads it in place through an io.ReaderAt, the way an image is read, while being an ARCHIVE and not a filesystem.
The distinction is not academic. Both kinds are opened from the handle Open already has and neither is spooled, so they look alike from here -- but they differ about who CLOSES that handle, and getting it the wrong way round fails in opposite directions.
type Limits ¶ added in v0.14.0
type Limits struct {
// MaxEntries bounds the paths an index holds (tar, ar, cpio), counting the
// directories an archive implies.
MaxEntries int
// MaxReadFile bounds what ReadFile returns, in bytes. Extract and OpenFile
// stream and are not affected.
MaxReadFile int64
// MaxSpool bounds what a stream wrapper (.gz, .xz, .zst, ...) may decompress
// to the temporary file it is spooled to.
MaxSpool int64
// MaxDictionary bounds the dictionary or window a decoder may reserve: the
// LZMA2 or LZMA dictionary of an .xz stream or a zip entry (the window grows
// with the data decoded and ErrTooLarge is returned before it would pass this),
// a zstd window (refused from the frame header).
MaxDictionary int64
}
Limits bounds what an untrusted archive may make Open, ReadFile and the stream wrappers reserve. A field that is zero or negative keeps its default.
type Options ¶
type Options struct {
// Overwrite lets an existing file be replaced. Off by default: an
// extraction that overwrites by default destroys data on a mistyped
// destination, and the mistake is not recoverable.
Overwrite bool
// Perm is the mode for created files and directories, before umask.
// Zero means 0o644 for files and 0o755 for directories.
Perm os.FileMode
// Progress, when set, is told each entry as it completes.
Progress func(name string, bytes int64)
// MaxBytes bounds the bytes written in all. Zero means DefaultMaxBytes;
// negative removes the limit. A compressed archive of a few kilobytes can
// hold terabytes (42.zip), and an extractor that trusts it fills the disk.
MaxBytes int64
// MaxEntries bounds the files, directories and links created. Zero means
// DefaultMaxEntries; negative removes the limit.
MaxEntries int
// MaxDepth bounds how deep the tree goes. Zero means DefaultMaxDepth;
// negative removes the limit. The walk recurses once per level, and a Go
// stack that runs out is a fatal error, not a panic that can be recovered.
MaxDepth int
}
Options tune an extraction.
type Result ¶
type Result struct {
Dirs int
Files int
// Links counts symbolic links written. They were counted as Files until
// today, and written as empty regular ones -- see walk.
Links int
Bytes int64
}
Result is what an extraction did.
func Extract ¶
func Extract(fsys filesystem.Filesystem, dest string, opt Options) (*Result, error)
Extract writes everything in fsys under dest.
Each file is compared against the size its header declared, and a shortfall is an error rather than a smaller number -- see ErrIncomplete.
type WriteTarget ¶
type WriteTarget struct {
Archive Format // FormatTar, FormatZIP or Format7z
Wrapper Format // FormatUnknown, or one of the stream formats
}
WriteTarget is what a target name resolves to: an archive format, and an optional stream wrapper around it.
func TargetFor ¶
func TargetFor(name string) (WriteTarget, error)
TargetFor says what to write into a file with this name.
⛔ This is the one place in the package where the NAME decides, and it has to: an output file has no bytes yet to read. Everywhere else a format comes from the content, because an extension is a claim by whoever last renamed the file; here the extension is the only statement of intent there is.
func (WriteTarget) String ¶
func (t WriteTarget) String() string
String names the pair the way a file name spells it.
