Documentation
¶
Overview ¶
Package cab is a pure-Go reader for the Microsoft Cabinet (.cab) format, presenting a cabinet as a filesystem.Filesystem.
It needs nothing outside the standard library: MSZIP is raw DEFLATE, and compress/flate is exactly the decoder it asks for. CGO_ENABLED=0 throughout.
fs, err := cab.OpenReader(f, size) // f is an io.ReaderAt
data, err := fs.ReadFile("sub/dir/file.txt")
FS also implements the optional filesystem.Opener, so a caller that must answer a byte range without materialising a whole entry can, subject to what the format costs (see "Random access", below).
Scope: None and MSZIP are read; Quantum and LZX are not ¶
A CFFOLDER names one of four compression methods. This package reads two of them:
- None (typeCompress 0): the folder's blocks are stored.
- MSZIP (1): each block is the two bytes "CK" followed by a raw DEFLATE stream.
Quantum (2) and LZX (3) are NOT read, and this is a decision rather than an omission. Each is a substantial algorithm in its own right -- LZX especially, with its own tree-delta coding, aligned-offset blocks and E8 call-offset translation -- and nothing in this ecosystem produces or consumes them: the cabinets in circulation here come from gcab and from makecab's default, both of which write MSZIP or store. A cabinet using either one is refused at OpenReader with ErrCompressionUnsupported, and the error NAMES the method and the folder it found it in.
Three sentences this package keeps apart ¶
A reader that answers "bad file" to every question is useless to whoever has to decide what to do next, so the failures are divided by what the caller can act on:
- "I do not know what this is" -- ErrNotCabinet (the four magic bytes are not "MSCF", or the file is too small to hold them) and ErrUnknownCompression (a typeCompress this format revision does not define). Try another reader, or another file.
- "I know exactly what this is and do not read it yet" -- ErrCompressionUnsupported (Quantum, LZX) and ErrSpansCabinets (an entry continued from, or into, another cabinet of the same set, which one io.ReaderAt cannot reach). The file is fine; this package is the limit.
- "this file is broken" -- ErrTruncated (a structure runs past the end) and ErrCorrupt (a field, a checksum or a DEFLATE stream contradicts the rest). Nothing will read it.
Reserve fields change the size of every later structure ¶
When the header carries CFHEADER flag cfhdrRESERVE_PRESENT (0x0004) it is followed by three sizes -- cbCFHeader, cbCFFolder, cbCFData -- and each one inserts an opaque area into EVERY instance of the structure it names: after the header's fixed fields, after every CFFOLDER, and, for cbCFData, between every CFDATA header and its compressed bytes. A reader that skips the header's own reserve area and forgets the other two lands one field out for the rest of the file, which is the classic way a cabinet reader fails. All three are honoured here, and testdata/reserve.cab exercises all three at once.
That fixture also recorded a disagreement between reference readers: libgcab and libmspack (cabextract) both read the per-CFDATA reserve area as the specification describes it, and 7-Zip 26.03 reports "Data Error" on the same file. The specification's CFDATA layout puts abReserve between cbUncomp and ab[], two of three readers agree with it, and so does this package.
MSZIP history crosses block boundaries ¶
A folder's uncompressed stream is the concatenation of its CFDATA blocks' output, and under MSZIP the DEFLATE history carries ACROSS those blocks: block N may reference bytes emitted by block N-1. Each block is its own DEFLATE stream, so a decoder that simply starts a fresh one per block is correct for any file small enough to fit in one block -- which is every hand-made test fixture -- and silently wrong beyond that. The remedy is flate.NewReaderDict, seeded with exactly the preceding 32 KiB of folder output; this package keeps that window and testdata/mszip.cab spans eight blocks so that dropping it fails.
Random access costs what the format costs ¶
A folder is a STREAM. Under MSZIP, reaching byte n of a folder means inflating the n bytes before it, because the history window makes no block independently decodable; a handle therefore keeps its decoder and its position, so reading forwards is linear overall while reading BACKWARDS starts a fresh pass from the start of the folder. Under None there is no history, so a handle restarts at the block containing the offset and reads at most one block's worth it does not need.
Opener is implemented anyway, rather than withheld, because the alternative for a caller serving 4 KiB requests is ReadFile -- which holds the WHOLE entry in memory. A bounded rescan is worse than nothing only if the caller expected a seek to be free, and this paragraph exists so that it does not.
Read-only ¶
A cabinet is read-only: every mutating method of filesystem.Filesystem (WriteFile, MkDir, DeleteFile, DeleteDir, Rename) returns ErrReadOnly, as in the other read-only drivers of this org.
Names ¶
A CFFILE name is a NUL-terminated byte string using BACKSLASH as its path separator; this package converts to '/' and cleans the result, so a cabinet's "sub\deep\file.txt" is reached as "sub/deep/file.txt". A cabinet holds no directory entries of its own, so the directories a listing needs are synthesised from the paths that are there.
The encoding is chosen by one attribute bit: with _A_NAME_IS_UTF (0x80) the name is UTF-8, and without it Windows-1252. The five byte values Windows-1252 leaves undefined (0x81, 0x8D, 0x8F, 0x90, 0x9D) decode to U+FFFD.
Index ¶
- Variables
- func OpenReader(r io.ReaderAt, size int64) (filesystem.Filesystem, error)
- type Compression
- type FS
- func (f *FS) Attributes(p string) (uint16, error)
- func (f *FS) CabinetIndex() uint16
- func (f *FS) Close() error
- func (f *FS) Compression(p string) (Compression, error)
- func (f *FS) DeleteDir(string) error
- func (f *FS) DeleteFile(string) error
- func (f *FS) FolderCount() int
- func (f *FS) ListDir(p string) ([]filesystem.DirEntry, error)
- func (f *FS) MkDir(string, os.FileMode) error
- func (f *FS) ModTime(p string) (time.Time, error)
- func (f *FS) NextCabinet() string
- func (f *FS) NextDisk() string
- func (f *FS) OpenFile(p string) (filesystem.File, error)
- func (f *FS) PrevCabinet() string
- func (f *FS) PrevDisk() string
- func (f *FS) ReadFile(p string) ([]byte, error)
- func (f *FS) ReadLink(p string) (string, error)
- func (f *FS) Rename(string, string) error
- func (f *FS) Reserved() []byte
- func (f *FS) SetID() uint16
- func (f *FS) Stat(p string) (filesystem.Stat, error)
- func (f *FS) Version() (major, minor uint8)
- func (f *FS) WriteFile(string, []byte, os.FileMode) error
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotCabinet says the bytes are not a cabinet at all: the file is // shorter than the four magic bytes, or those bytes are not "MSCF". // "I do not know what this is." ErrNotCabinet = errors.New("cab: not a Microsoft Cabinet file") // ErrUnknownCompression says a CFFOLDER named a compression method this // format revision does not define (typeCompress low nibble 4..15). // "I do not know what this is" -- about one folder rather than the file. ErrUnknownCompression = errors.New("cab: unknown compression method") // ErrCompressionUnsupported says the cabinet is well-formed and uses a // method this package does not implement: Quantum or LZX. The error text // names which one, and which folder. "I know exactly what this is and do // not read it yet." ErrCompressionUnsupported = errors.New("cab: compression method not read by this package") // ErrSpansCabinets says an entry is continued from, or into, another // cabinet of the same set. One io.ReaderAt is one file and the rest of the // set is in other files, so this reader cannot complete such an entry -- // and does not pad it with zeros and call that a success. ErrSpansCabinets = errors.New("cab: entry continues in another cabinet of the set") // ErrTruncated says a structure the cabinet points at runs past the end of // the bytes available. "This file is broken." ErrTruncated = errors.New("cab: cabinet is truncated") // ErrCorrupt says a field, a block checksum or a DEFLATE stream // contradicts the rest of the file. "This file is broken." ErrCorrupt = errors.New("cab: cabinet is corrupt") // ErrReadOnly is returned by every mutating method (WriteFile, MkDir, // DeleteFile, DeleteDir, Rename). A cabinet is a read-only format. ErrReadOnly = errors.New("cab: cabinet is read-only") // ErrNotFound is returned when a path is not in the cabinet. It satisfies // errors.Is(err, io/fs.ErrNotExist), which is the error contract every // driver in this org owes its callers. ErrNotFound = fmt.Errorf("cab: path not found: %w", iofs.ErrNotExist) // ErrNotDirectory is returned when ListDir targets something that is not a // directory. ErrNotDirectory = errors.New("cab: not a directory") // ErrNotRegular is returned when ReadFile or OpenFile targets a directory. ErrNotRegular = errors.New("cab: not a regular file") // ErrNotSymlink is returned by ReadLink for every path: the cabinet format // records no symbolic links, so there is never one to read. ErrNotSymlink = errors.New("cab: not a symbolic link") // ErrNegativeOffset is returned by File.ReadAt for a negative offset. ErrNegativeOffset = errors.New("cab: negative offset") )
Sentinel errors. Compare with errors.Is so wrapped errors continue to match.
They are divided by what a caller can DO about the failure; see the package documentation, "Three sentences this package keeps apart".
Functions ¶
func OpenReader ¶
func OpenReader(r io.ReaderAt, size int64) (filesystem.Filesystem, error)
OpenReader reads the cabinet in the first size bytes of r.
This is the shape every driver in go-filesystems answers to, so github.com/go-filesystems/detect can open them all through one function type. It reads the header, every CFFOLDER with its block table and every CFFILE, and no file data at all: the returned filesystem decodes on demand.
r must stay valid and readable for as long as the returned filesystem is used; Close does not touch it, because OpenReader never took ownership of it.
A cabinet whose folders use Quantum or LZX is refused here, by name -- see the package documentation for why those two are out of scope.
Types ¶
type Compression ¶
type Compression uint16
Compression is the method a CFFOLDER records in the low nibble of its typeCompress field.
const ( CompressionNone Compression = 0 CompressionMSZIP Compression = 1 CompressionQuantum Compression = 2 CompressionLZX Compression = 3 )
The four methods the format defines. This package reads the first two; see the package documentation for why it refuses the other two by name.
func (Compression) String ¶
func (c Compression) String() string
String names the method, so an error can say which one it met.
type FS ¶
type FS struct {
// contains filtered or unexported fields
}
FS reads a Microsoft Cabinet as a filesystem.
It satisfies filesystem.Filesystem and the optional filesystem.Opener. See the package documentation, "Random access costs what the format costs", for what a seek inside a compressed folder actually does.
func Open ¶
Open is OpenReader returning the concrete type, for callers that want the cabinet-specific accessors -- SetID, PrevCabinet, Compression, Reserved -- that filesystem.Filesystem has no room for.
func (*FS) Attributes ¶
Attributes is the CFFILE attribute word recorded for path.
func (*FS) CabinetIndex ¶
CabinetIndex is this cabinet's position within its set, counting from zero.
func (*FS) Close ¶
Close releases nothing: FS never took ownership of the io.ReaderAt it was given, and each read decodes into memory it drops on the way out.
func (*FS) Compression ¶
func (f *FS) Compression(p string) (Compression, error)
Compression reports the method recorded in the CFFOLDER that holds path.
It exists so that a test can assert the PREMISE of a fixture rather than assume it: a writer asked for MSZIP may store a small entry anyway, and a corpus of such entries passes every method while only None ever runs.
func (*FS) DeleteFile ¶
func (*FS) FolderCount ¶
FolderCount is how many CFFOLDER streams the cabinet holds.
func (*FS) ListDir ¶
func (f *FS) ListDir(p string) ([]filesystem.DirEntry, error)
ListDir names what is directly inside p, sorted.
func (*FS) ModTime ¶
ModTime is the modification time recorded for path. The format's packed MS-DOS date and time have two-second resolution and no timezone, so the result is in UTC.
func (*FS) NextCabinet ¶
NextCabinet is the file name of the cabinet after this one in its set, and "" when the header does not name one.
func (*FS) OpenFile ¶
func (f *FS) OpenFile(p string) (filesystem.File, error)
OpenFile hands back a random-access handle on one entry, reading none of it.
func (*FS) PrevCabinet ¶
PrevCabinet is the file name of the cabinet before this one in its set, and "" when the header does not name one. PrevDisk is the accompanying media name, which is prompt text rather than a path.
func (*FS) ReadFile ¶
ReadFile returns the whole entry.
It holds the entry in memory, which for a large member of a compressed folder is the whole member. Use OpenFile for anything whose size you did not choose.
func (*FS) ReadLink ¶
ReadLink always refuses: the cabinet format records no symbolic links, so there is never one to read. The method exists because filesystem.Filesystem requires it.
func (*FS) Reserved ¶
Reserved is the opaque area the header's cbCFHeader reserved, or nil. The bytes belong to whatever wrote the cabinet; this package only skips them correctly.
func (*FS) SetID ¶
SetID is the cabinet set this file belongs to: every cabinet of one set carries the same value.
func (*FS) Stat ¶
func (f *FS) Stat(p string) (filesystem.Stat, error)
Stat reports what the cabinet recorded about p.
