module

Marcel::Magic::Zip

Procedural refinement of ZIP-based container types.

The declarative magic tables scan a bounded prefix of the file (matchers are capped at 64KB offsets when the tables are generated), so ZIP containers whose distinguishing members are catalogued later in the archive — such as OOXML documents with [Content_Types].xml stored past the 64KB mark — fall back to their generic container type. This module instead parses the ZIP central directory, a bounded read from the end of the file, to recover the specific document type, including macro-enabled variants, which no prefix bytes can distinguish.

Constants

CENTRAL_DIRECTORY_HEADER_SIZE = 46
CENTRAL_DIRECTORY_SIGNATURE = "PK\u0001\u0002".to_slice
DISCRIMINATING_PARTS = DOCUMENT_FAMILIES.flat_map do |family| [family[0], family[1]] end
DOCUMENT_FAMILIES = [{"word/document.xml", "word/vbaProject.bin", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-word.document.macroenabled.12"}, {"xl/workbook.xml", "xl/vbaProject.bin", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-excel.sheet.macroenabled.12"}, {"ppt/presentation.xml", "ppt/vbaProject.bin", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.ms-powerpoint.presentation.macroenabled.12"}]

Standardised OPC part names identifying each OOXML document family: main part, macro part, and the types each implies.

EOCD_SIGNATURE = "PK\u0005\u0006".to_slice
EOCD_SIZE = 22
MAX_CENTRAL_DIRECTORY_READ = 1 << 20
MAX_COMMENT_SIZE = 65535
MAX_ENTRIES = 8192
MAX_EOCD_CANDIDATES = 64

A malformed trailer full of EOCD signature bytes could otherwise force a quadratic backward scan; real archives find the record on the first candidate.

MAX_EOCD_SEARCH = (EOCD_SIZE + MAX_COMMENT_SIZE) + ZIP64_EOCD_LOCATOR_SIZE

The end-of-central-directory record sits at most a maximal comment from the end of the file, possibly preceded by a Zip64 locator.

REFINABLE_TYPES = ["application/zip", "application/x-tika-ooxml", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.presentationml.presentation"]

Container types a central directory listing can make more specific.

ZIP64_EOCD_LOCATOR_SIGNATURE = "PK\u0006\a".to_slice
ZIP64_EOCD_LOCATOR_SIZE = 18

4 (сигнатура) + 4 (диск) + 8 (смещение) + 2 (всего дисков, u16 — как в zip.rb: unpack("VQ<V"), 18-байтовая запись).

ZIP64_EOCD_SIGNATURE = "PK\u0006\u0006".to_slice
ZIP64_EOCD_SIZE = 56

Class methods

classify(parts : Array(String), base_type : String) : String
Source
complete?(parts : Array(String)) : Bool

A main part plus its macro part is as specific as classification gets.

Source
discriminating_parts(io : IO) : Array(String) | Nil

Reads the archive's central directory and returns the OOXML part names it catalogues, or nil if no well-formed central directory is found.

Source
locate_eocd(tail : Bytes) : Int32 | Nil

Scans backward through the file's tail for the end-of-central-directory record, validating each candidate signature against its comment length.

Source
parse_eocd(io : IO, tail : Bytes, eocd_pos : Int32) : Tuple(Int32, Int32, Int32) | Nil

Returns {entry count, central directory size, central directory offset}, following the Zip64 records when the classic fields overflow. Multi-disk archives and malformed records return nil.

Source
parse_zip64_eocd(io : IO, tail : Bytes, eocd_pos : Int32) : Tuple(Int32, Int32, Int32) | Nil
Source
read_parts(io : IO, entries : Int32, directory_size : Int32, directory_offset : Int32) : Array(String) | Nil

Walks central directory entries, collecting the OOXML part names that discriminate between document families. Reads and entry counts are bounded; the walk stops early once a family and its macro part are both seen.

Source
refine(io : IO | String, base_type : String) : String

Returns a more specific type than +base_type+ if the IO's ZIP central directory identifies one, or +base_type+ unchanged. Unseekable IOs, partial reads and malformed archives refine nothing: the base type stands.

Source
rindex_bytes(hay : Bytes, needle : Bytes, limit : Int32) : Int32 | Nil
Source
u16(bytes : Bytes, pos : Int32) : UInt16
Source
u32(bytes : Bytes, pos : Int32) : UInt32
Source
u64(bytes : Bytes, pos : Int32) : UInt64
Source