File Type
Green · 2026-04-21 Node + Browser UtilMagic-byte file-type detection via the Rust infer crate.
- Targets
- Node + Browser
- Version
- 0.1.0
Install
pnpm add @amigo-labs/file-typeBenchmarks
Trend (9 pts)Benchmark
file-type — small header (12 bytes PNG)
- @amigo-labs/file-type (sync) napi 1.32M hz · 23.06×
- file-type (upstream async) 57.07K hz
Benchmark
file-type — medium JPEG buffer (100KB)
- @amigo-labs/file-type (sync) napi 1.34M hz · 21.56×
- file-type (upstream async) 62.01K hz
Benchmark
file-type — large MP4 buffer (10MB)
- @amigo-labs/file-type (sync) napi 1.22M hz · 1178.71×
- @amigo-labs/file-type (async) napi 25.22K hz · 24.45×
- file-type (upstream async) 1.03K hz
README
@amigo-labs/file-type
Magic-byte file-type detection powered by the Rust
infercrate. Alternative tofile-type, compiled via NAPI-RS.
Install
npm install @amigo-labs/file-type
Usage
import { fileTypeFromBuffer, fileTypeFromBufferSync } from '@amigo-labs/file-type'
const result = await fileTypeFromBuffer(buffer)
if (result) {
console.log(result.ext, result.mime) // 'png', 'image/png'
}
const sync = fileTypeFromBufferSync(buffer)
Returns null when the magic signature isn’t recognised. infer covers ~70 formats (images, audio/video, archives, fonts, documents) — see divergences.md for the coverage gap vs file-type.
Install for the browser
The same import works in Angular, React, Vite, esbuild, and webpack ≥ 5 — the bundler picks the WASM build via the browser conditional export:
import { fileTypeFromBufferSync } from '@amigo-labs/file-type'
The async fileTypeFromBuffer variant is napi-only (no thread pool in WASM); the sync entry is what ships to the browser.
Parity
Tests in __conformance__/ run a representative subset of the upstream file-type test suite against this implementation.
Perf review
Perf-Review: @amigo-labs/file-type
Status: 🟢 Green (most extreme portfolio win) · Reviewed: 2026-04-21 · Version: 0.1.0
Verdict
31.9× (100 KB JPEG) up to 2378× (10 MB MP4) vs. upstream file-type npm. This is the largest multiplier in the portfolio — but with a clear structural reason: upstream is async-only, we are sync. The user who wants to answer “is this a JPEG?” on a 10 MB buffer pays upstream’s async-wrapper overhead + stream-read ceremony; we detect the magic bytes in the first 4 KB and return directly. The infer Rust crate is compact (<1 MB binary), the signatures live in compile-time tables, and the 4 KB head cap (MAX_MAGIC_PREFIX) avoids the otherwise visible 1–3 ms memcpy on the async path. Only divergence vs. upstream: parity is only 89 % (some exotic formats are missing from the infer crate).
Classification rationale
- Async-vs-sync is the structural lever. Upstream
file-type@19no longer has any sync API — the switch to async was an upstream design decision (readable-stream support). We offer both:fileTypeFromBufferSyncfor the hot path (1.4M Hz),fileTypeFromBuffer(AsyncTask) for non-blocking. - The
inferRust crate is very fast. Signatures are stored in PHF-like tables (compile-time), byte matching is a direct&[u8]compare. The JS upstream has RegExp-based signature tests in a chain-of-.matchpattern. - The 4 KB head cap avoids redundant memcpy. Every
infer::get()reads only the first ~4 KB. We cap the async-task input at 4 KB, which for a 10 MB MP4 eliminates the implicitto_vec()clone (1–3 ms). - The parity gap is an acceptable cost. 89 % parity (from the
docs/perf-review.mdtable in the README) means: ~10 % of the formats upstream supports are missing or diverge.infercovers the 80/20 (all mainstream image formats, office docs, archives, media). Exotic formats (HEIC variants, old DOS formats) diverge.
Evidence
Measured speedup (docs/data.json, 2026-04-18)
| Scenario | @amigo-labs/file-type (sync) | file-type npm (async) | Speedup |
|---|---|---|---|
| 100 KB JPEG | 1 445 211 Hz | 45 248 Hz | 31.9× |
| 10 MB MP4 (sync) | 1 382 301 Hz | 581.0 Hz | 2378× |
| 10 MB MP4 (async) | 36 432 Hz | 581.0 Hz | 62.7× |
Realistic use-case
File-upload validation — a user uploads a file, we check the magic bytes before trusting the extension. Format detection in batch pipelines — file-tree walk with category buckets. HTTP content-type derivation from bytes instead of headers. In all cases: a sync API is what you want (the upstream library’s async overhead is unwanted complexity for a sub-µs operation).
Benchmark gaps
- Small-buffer bucket (<1 KB) not benched. Most magic-byte checks use ~32-256 byte headers. There the FFI floor would be relatively visible. Expected: still dominantly Green thanks to async-vs-sync.
- Individual formats not measured separately. PNG, GIF, PDF, ZIP, etc. have different signature-length checks in the
infercrate. An average over a format mix would be sensible. - Stream/path-based APIs not in the bench (upstream supports
fileTypeFromFileetc.). We focus on the buffer path.
API surface
#[napi(js_name = "fileTypeFromBufferSync")] fn file_type_from_buffer_sync(buffer: Buffer) -> Option<FileTypeResult>
#[napi] fn file_type_from_buffer(buffer: Buffer) -> AsyncTask<FileTypeTask> // 4KB head cap
FileTypeResult { ext: String, mime: String }— compact result object.- The sync variant is the hot path. The async variant is for non-blocking large-buffer workloads (HTTP upload handlers).
- No stateful API, no config, no callbacks.
Bundle / binary size
The infer crate is under 100 KB + napi bindings. One of the smallest binaries in the portfolio.
FFI-overhead baseline
- Sync path: buffer transport ~180 ns, Option<{ext, mime}> return ~300 ns (two small strings). Rust work: 4 KB head scan ~1–5 µs. Total ~5 µs per call. 3.47 % FFI share on sync — tolerable.
- Async path: 4 KB memcpy ~2 µs, AsyncTask schedule ~10 µs, compute ~5 µs, resolve ~10 µs. Total ~27 µs. Dominated by async ceremony, which is the point.
Phase-C optimization checklist
| # | Lever | Applicable | Notes |
|---|---|---|---|
| C.1 | Input-type minimization | ✅ already done | Buffer zero-copy, no copy on the sync path |
| C.2 | Output-type minimization | ✅ already done | Option<{ext, mime}> compact |
| C.3 | Batch API (file_type_many) | 🟡 potential | The file-tree-walker use case could benefit. But 1.4M Hz is already so fast that the batch lever would be marginal |
| C.4 | Stateful API | ❌ not applicable | No config state |
| C.5 | Parallelization | ❌ not applicable | A single call leaves no headroom |
| C.6 | Algorithm swap | ❌ not applicable | infer is already optimal |
| C.7 | Allocator tuning | ✅ already done | 4KB head cap, no full-buffer clone in async |
| C.8 | Bundle-size | ✅ already done | Very small |
Action plan
Keep-as-is. No room upward, largest speedup win in the portfolio. The package is done.
Maintenance:
- Bench the small-buffer bucket (32-byte-header case) for documentation.
- Format-mix bench — PNG/GIF/PDF/MP4/ZIP mixed together for a “realistic median” instead of only JPEG+MP4.
- Parity-gap documentation (the missing 11 %) in
divergences.mdif not yet complete.
No open Phase-C levers, no Phase-D risks.
References
- Crate:
crates/file-type - Bench:
crates/file-type/__bench__/index.bench.ts - Lib:
crates/file-type/src/lib.rs - Cargo:
crates/file-type/Cargo.toml docs/packages.jsonspeedup:"32× faster"(stated conservatively vs. the measured 2378×)