9.7 KiB
Capability Spec: MIME
Package: @suaveplan/mime
Version: 0.0.0 (genesis)
Status: ADDED
ADDED Requirements
MIME-01 — Magic-byte detection via compiled trie
detectFromMagicBytes(bytes) MUST match bytes against a compiled trie built from the registered signature table. Matching MUST be offset-aware (each Signature may declare a non-zero offset) and mask-aware (Signature.mask applies a bitmask before comparison). If bytes is shorter than the minimum bytes required by any registered signature, the function MUST throw InsufficientBytesError with context.needed and context.got populated. On success it MUST return a MagicBytesResult identifying the best-match MIME type and the matched Signature.
MIME-02 — All-matches magic-byte detection
detectAllFromMagicBytes(bytes) MUST return every MagicBytesResult whose signature matches bytes, sorted by specificity (longest matching byte sequence first). It MUST NOT throw InsufficientBytesError; signatures that cannot be evaluated against a too-short buffer are silently skipped.
MIME-03 — RFC 7231 Content-Type parsing
parseContentType(value) MUST parse a Content-Type header value per RFC 7231 §3.1.1.5. It MUST handle:
- Optional leading/trailing OWS
- Case-insensitive
type/subtype(normalised to lowercase on output) - Quoted-string parameter values (including escaped characters per RFC 7230 §3.2.6)
- Multiple parameters separated by
; charsetparameter normalised to lowercase
On parse failure it MUST throw InvalidContentTypeError (code MIME.INVALID_CONTENT_TYPE) with context.raw set to the input string.
MIME-04 — Content-Type round-trip serialisation
formatContentType(parsed) MUST serialise a ParsedContentType to a canonical string. Parameter values that contain token-unsafe characters MUST be quoted. The round-trip invariant parseContentType(formatContentType(p)) MUST yield a ParsedContentType structurally equal to p for any valid p.
MIME-05 — Content-Type equality and matching
contentTypeEquals(a, b) MUST return true iff both parsed values have identical (case-normalised) type, subtype, and the same parameter keys and values regardless of order.
contentTypeMatches(actual, pattern) MUST return true when pattern.subtype === '*' and actual.type === pattern.type, or when both type and subtype match exactly (case-insensitive). Wildcard */* MUST match any media type.
MIME-06 — Curated IANA-sourced registry
BUILTIN_DB MUST contain a comprehensive, read-only set of MimeEntry records covering at minimum: all major image formats (PNG, JPEG, GIF, WebP, AVIF, SVG, TIFF, BMP, ICO), audio formats (MP3, OGG, FLAC, WAV, AAC, OPUS), video formats (MP4, WebM, OGV, AVI, MKV), common application types (JSON, XML, PDF, ZIP, GZIP, WASM, CBOR, MessagePack, JavaScript, CSS, HTML, plain text), and font formats (WOFF, WOFF2, TTF, OTF). Each entry MUST include mimeType, category, and extensions; compressible and charset are optional.
MIME-07 — Runtime registry extension
registerMimeEntry(entry) MUST validate entry against MimeEntrySchema before insertion and MUST throw ParseError on schema violation. After registration, getMimeEntry(entry.mimeType) MUST return the registered entry, and getEntryByExtension(ext) MUST return the entry for every extension listed in entry.extensions. listMimeEntries() MUST return a sorted, deduplicated snapshot including all built-in and registered entries.
MIME-08 — Reverse lookups
toAllExtensions(mimeType) MUST return all extensions associated with the given MIME type string (empty array if unknown). toDefaultExtension(mimeType) MUST return the first extension from the matching MimeEntry.extensions array, or undefined. toCategory(mimeType) MUST return the MimeCategory for the MIME type, or undefined.
MIME-09 — BOM charset sniffing
sniffBom(bytes) MUST detect and return a BomDetection ({ charset, bomLength }) for the following BOM sequences, checked in order:
00 00 FE FF→UTF-32BE, bomLength 4FF FE 00 00→UTF-32LE, bomLength 4FE FF→UTF-16BE, bomLength 2FF FE→UTF-16LE, bomLength 2EF BB BF→UTF-8, bomLength 3
It MUST return null when no BOM is found. It MUST NOT throw for any input.
MIME-10 — HTML meta charset sniffing
sniffHtmlMeta(bytes) MUST scan up to the first 1 024 bytes for <meta charset="…"> and <meta http-equiv="Content-Type" content="…; charset=…"> patterns using case-insensitive matching. It MUST return the charset string (lowercased) or null if not found. It MUST NOT throw for any input.
MIME-11 — XML declaration charset sniffing
sniffXmlDecl(bytes) MUST extract the encoding attribute from an XML declaration (<?xml … encoding="…" … ?>) if present within the first 128 bytes. It MUST return the encoding name (uppercased to match XML convention) or null. It MUST NOT throw for any input.
MIME-12 — Unified detect()
detect(input) MUST resolve to a DetectResult by applying the following priority order:
- If
input.bytesis provided, attemptdetectFromMagicBytes; onInsufficientBytesErrorfall through. - If
input.extensionis provided, attemptdetectFromExtension; onUnknownMimeTypeErrorfall through. - If
input.contentTypeis provided, attemptdetectFromContentType. - If all sources are exhausted, throw
UnknownMimeTypeError.
detect MUST check input.signal before each source attempt and reject with a structured abort error if cancelled.
MIME-13 — detectFromStream() interim shim
detectFromStream(stream, opts?) MUST be exported with the same signature as the main branch. It MUST read the first available chunk from the provided ReadableStream or async iterable, respecting opts.signal. It MUST delegate to detectFromMagicBytes with the chunk bytes and wrap the result as StreamDetectionResult. It MUST document in its co-located .md that full streaming integration is deferred pending @suaveplan/streams.
MIME-14 — Signature registry extension
registerSignature(sig) MUST validate sig against SignatureSchema before insertion and MUST throw ParseError on invalid shape. After registration, the compiled trie MUST be rebuilt (lazily on next detection call) to include the new signature. registerSignature MUST be idempotent for identical sig objects.
MIME-15 — Structured errors
All thrown errors MUST extend MimeError which extends SuaveplanError. Error codes MUST be registered via registerMimeErrorCodes() which is called as a side-effect of importing src/index.ts. The following codes MUST be defined:
MIME.PARSE_ERROR— generic parse failureMIME.INSUFFICIENT_BYTES— buffer too short for magic-byte detectionMIME.UNKNOWN_SIGNATURE— unrecognised or conflicting signatureMIME.UNKNOWN_TYPE— MIME type not found in registryMIME.INVALID_CONTENT_TYPE— Content-Type header failed to parse
MIME-16 — Zod schemas
The following schemas MUST be exported from their respective *.schemas.ts files:
ParsedContentTypeSchema,MediaTypeSchema— fromsrc/schemas/content-type.schemas.tsMimeCategorySchema,MimeEntrySchema— fromsrc/schemas/db.schemas.tsSignatureSchema,DetectInputSchema,DetectResultSchema(discriminated union bysource),MagicBytesResultSchema,ExtensionDetectionResultSchema,ContentTypeDetectionResultSchema,StreamDetectionResultSchema— fromsrc/schemas/detection.schemas.ts
All schemas MUST be used for boundary validation in registerMimeEntry, registerSignature, and detect.
MIME-17 — OTel instrumentation
The following metrics MUST be emitted via @opentelemetry/api using meter name @suaveplan/mime:
- Counter
mime.detect.totalwith attributesource(magic/extension/content-type/stream) - Histogram
mime.detect.duration_msrecorded on everydetect()call - Counter
mime.parse.totalwith attributeoutcome(success/error) recorded on everyparseContentType()call
All instruments MUST be no-ops when no OTel metric exporter is configured.
MIME-18 — Legacy shim
getMimeType(ext) MUST be exported and MUST delegate to getEntryByExtension(ext)?.mimeType ?? undefined. It MUST never throw. This export is retained solely for API surface completeness.
MIME-19 — Property-based tests
The following fast-check properties MUST be included in the test suite:
- Round-trip: for any synthetically valid Content-Type string,
formatContentType(parseContentType(s))parses back to aParsedContentTypethat is structurally equal to the original parse result. - Magic-byte exhaustiveness:
detectFromMagicBytes(bytes)with anyUint8Arrayeither returns aMagicBytesResultor throws exactlyInsufficientBytesError— no other error class is acceptable. - Registry round-trip: any
MimeEntrysatisfyingMimeEntrySchemathat is passed toregisterMimeEntryis retrievable viagetMimeEntrywith an identical shape.
MIME-20 — Benchmarks
The following mitata benchmark suites MUST be included under src/__benchmarks__/:
parseContentType— 10 000 iterations over a representative set of realistic Content-Type stringsdetectfrom a 4 KBUint8Array— 10 000 iterationsgetMimeEntrylookup — 100 000 iterations against the built-in database
Baseline numbers from the genesis run MUST be published in README.md.
MIME-21 — sideEffects: false + 100% coverage
"sideEffects": false MUST be set in package.json and enforced by a tree-shake guard test in tests/. Vitest statement/branch/function/line thresholds MUST all be set to 100. No .skip or .only is permitted in src/.