# 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 `;` - `charset` parameter 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: 1. `00 00 FE FF` → `UTF-32BE`, bomLength 4 2. `FF FE 00 00` → `UTF-32LE`, bomLength 4 3. `FE FF` → `UTF-16BE`, bomLength 2 4. `FF FE` → `UTF-16LE`, bomLength 2 5. `EF 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 `` and `` 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 (``) 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: 1. If `input.bytes` is provided, attempt `detectFromMagicBytes`; on `InsufficientBytesError` fall through. 2. If `input.extension` is provided, attempt `detectFromExtension`; on `UnknownMimeTypeError` fall through. 3. If `input.contentType` is provided, attempt `detectFromContentType`. 4. 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 failure - `MIME.INSUFFICIENT_BYTES` — buffer too short for magic-byte detection - `MIME.UNKNOWN_SIGNATURE` — unrecognised or conflicting signature - `MIME.UNKNOWN_TYPE` — MIME type not found in registry - `MIME.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` — from `src/schemas/content-type.schemas.ts` - `MimeCategorySchema`, `MimeEntrySchema` — from `src/schemas/db.schemas.ts` - `SignatureSchema`, `DetectInputSchema`, `DetectResultSchema` (discriminated union by `source`), `MagicBytesResultSchema`, `ExtensionDetectionResultSchema`, `ContentTypeDetectionResultSchema`, `StreamDetectionResultSchema` — from `src/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.total` with attribute `source` (`magic` / `extension` / `content-type` / `stream`) - Histogram `mime.detect.duration_ms` recorded on every `detect()` call - Counter `mime.parse.total` with attribute `outcome` (`success` / `error`) recorded on every `parseContentType()` 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: 1. **Round-trip**: for any synthetically valid Content-Type string, `formatContentType(parseContentType(s))` parses back to a `ParsedContentType` that is structurally equal to the original parse result. 2. **Magic-byte exhaustiveness**: `detectFromMagicBytes(bytes)` with any `Uint8Array` either returns a `MagicBytesResult` or throws exactly `InsufficientBytesError` — no other error class is acceptable. 3. **Registry round-trip**: any `MimeEntry` satisfying `MimeEntrySchema` that is passed to `registerMimeEntry` is retrievable via `getMimeEntry` with 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 strings - `detect` from a 4 KB `Uint8Array` — 10 000 iterations - `getMimeEntry` lookup — 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/`.