Skip to content

MIME Type Reference — Zig source

A searchable map of common file extensions to their MIME types - text, image, audio, video, application, font, and multipart. Look up by extension or by type, filter by category, and copy the exact Content-Type string for any format.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

// mime-types — Zig port of the curated extension ↔ MIME table, ported from
// src/lib/mime-types.ts. Display source for CosmoDev's polyglot tool pages.
//
// The table is a comptime-known array of 71 entries grouped by the seven
// top-level media types (RFC 2046). Each entry's `type` always equals the
// top-level segment of its own `mime`, so the category filter and the
// Content-Type never disagree.
//
// Zig has no allocation-free "list" return, so the lookups write their
// matches into a caller-provided buffer and return the match count —
// the idiomatic Zig shape for "return a filtered list".

const std = @import("std");

/// The seven top-level media types a curated entry can belong to.
pub const MimeCategory = enum {
    text,
    image,
    audio,
    video,
    application,
    font,
    multipart,

    /// Lowercase label — also how the query/type strings in the TS source
    /// are spelled, so a caller can map a raw string onto the enum.
    pub fn label(self: MimeCategory) []const u8 {
        return switch (self) {
            .text => "text",
            .image => "image",
            .audio => "audio",
            .video => "video",
            .application => "application",
            .font => "font",
            .multipart => "multipart",
        };
    }
};

/// One curated extension ↔ MIME mapping (immutable value).
pub const MimeEntry = struct {
    /// File extension, lowercase, no leading dot (e.g. "json").
    ext: []const u8,
    /// Full MIME / Content-Type string (e.g. "application/json").
    mime: []const u8,
    /// Top-level media type — matches the `mime` string's first segment.
    type: MimeCategory,
    /// Human-readable format name (e.g. "JSON").
    name: []const u8,
};

/// Display order of the categories (used by the UI filter).
pub const MIME_CATEGORIES = [_]MimeCategory{
    .text, .image, .audio, .video, .application, .font, .multipart,
};

/// The curated extension ↔ MIME table. One entry is one (extension, type)
/// pair, so an extension with several MIME types (e.g. .js) appears more
/// than once, and a MIME with several extensions (e.g. image/jpeg) appears
/// once per extension. Values follow IANA / MDN conventions.
pub const MIME = [_]MimeEntry{
    // ── text ────────────────────────────────────────────────────────────
    .{ .ext = "txt", .mime = "text/plain", .type = .text, .name = "Plain text" },
    .{ .ext = "html", .mime = "text/html", .type = .text, .name = "HTML document" },
    .{ .ext = "htm", .mime = "text/html", .type = .text, .name = "HTML document" },
    .{ .ext = "xhtml", .mime = "application/xhtml+xml", .type = .application, .name = "XHTML" },
    .{ .ext = "css", .mime = "text/css", .type = .text, .name = "CSS stylesheet" },
    .{ .ext = "csv", .mime = "text/csv", .type = .text, .name = "Comma-separated values" },
    .{ .ext = "xml", .mime = "application/xml", .type = .application, .name = "XML document" },
    .{ .ext = "js", .mime = "text/javascript", .type = .text, .name = "JavaScript" },
    .{ .ext = "js", .mime = "application/javascript", .type = .application, .name = "JavaScript (legacy)" },
    .{ .ext = "mjs", .mime = "text/javascript", .type = .text, .name = "JavaScript module" },
    .{ .ext = "json", .mime = "application/json", .type = .application, .name = "JSON" },
    .{ .ext = "md", .mime = "text/markdown", .type = .text, .name = "Markdown" },
    .{ .ext = "yaml", .mime = "text/yaml", .type = .text, .name = "YAML" },
    .{ .ext = "yml", .mime = "text/yaml", .type = .text, .name = "YAML" },
    .{ .ext = "ics", .mime = "text/calendar", .type = .text, .name = "iCalendar" },
    .{ .ext = "rtf", .mime = "application/rtf", .type = .application, .name = "Rich text" },
    .{ .ext = "tsv", .mime = "text/tab-separated-values", .type = .text, .name = "Tab-separated values" },

    // ── image ───────────────────────────────────────────────────────────
    .{ .ext = "jpg", .mime = "image/jpeg", .type = .image, .name = "JPEG image" },
    .{ .ext = "jpeg", .mime = "image/jpeg", .type = .image, .name = "JPEG image" },
    .{ .ext = "png", .mime = "image/png", .type = .image, .name = "PNG image" },
    .{ .ext = "gif", .mime = "image/gif", .type = .image, .name = "GIF image" },
    .{ .ext = "webp", .mime = "image/webp", .type = .image, .name = "WebP image" },
    .{ .ext = "avif", .mime = "image/avif", .type = .image, .name = "AVIF image" },
    .{ .ext = "svg", .mime = "image/svg+xml", .type = .image, .name = "SVG image" },
    .{ .ext = "bmp", .mime = "image/bmp", .type = .image, .name = "Bitmap image" },
    .{ .ext = "ico", .mime = "image/x-icon", .type = .image, .name = "Icon" },
    .{ .ext = "tif", .mime = "image/tiff", .type = .image, .name = "TIFF image" },
    .{ .ext = "tiff", .mime = "image/tiff", .type = .image, .name = "TIFF image" },
    .{ .ext = "heic", .mime = "image/heic", .type = .image, .name = "HEIC image" },

    // ── audio ───────────────────────────────────────────────────────────
    .{ .ext = "mp3", .mime = "audio/mpeg", .type = .audio, .name = "MP3 audio" },
    .{ .ext = "wav", .mime = "audio/wav", .type = .audio, .name = "WAV audio" },
    .{ .ext = "ogg", .mime = "audio/ogg", .type = .audio, .name = "Ogg audio" },
    .{ .ext = "oga", .mime = "audio/ogg", .type = .audio, .name = "Ogg audio" },
    .{ .ext = "flac", .mime = "audio/flac", .type = .audio, .name = "FLAC audio" },
    .{ .ext = "aac", .mime = "audio/aac", .type = .audio, .name = "AAC audio" },
    .{ .ext = "m4a", .mime = "audio/mp4", .type = .audio, .name = "M4A audio" },
    .{ .ext = "weba", .mime = "audio/webm", .type = .audio, .name = "WebM audio" },
    .{ .ext = "mid", .mime = "audio/midi", .type = .audio, .name = "MIDI" },
    .{ .ext = "midi", .mime = "audio/midi", .type = .audio, .name = "MIDI" },

    // ── video ───────────────────────────────────────────────────────────
    .{ .ext = "mp4", .mime = "video/mp4", .type = .video, .name = "MP4 video" },
    .{ .ext = "webm", .mime = "video/webm", .type = .video, .name = "WebM video" },
    .{ .ext = "avi", .mime = "video/x-msvideo", .type = .video, .name = "AVI video" },
    .{ .ext = "mov", .mime = "video/quicktime", .type = .video, .name = "QuickTime video" },
    .{ .ext = "mkv", .mime = "video/x-matroska", .type = .video, .name = "Matroska video" },
    .{ .ext = "mpeg", .mime = "video/mpeg", .type = .video, .name = "MPEG video" },
    .{ .ext = "mpg", .mime = "video/mpeg", .type = .video, .name = "MPEG video" },
    .{ .ext = "ogv", .mime = "video/ogg", .type = .video, .name = "Ogg video" },
    .{ .ext = "m4v", .mime = "video/mp4", .type = .video, .name = "M4V video" },

    // ── application ─────────────────────────────────────────────────────
    .{ .ext = "pdf", .mime = "application/pdf", .type = .application, .name = "PDF document" },
    .{ .ext = "zip", .mime = "application/zip", .type = .application, .name = "ZIP archive" },
    .{ .ext = "gz", .mime = "application/gzip", .type = .application, .name = "Gzip archive" },
    .{ .ext = "tar", .mime = "application/x-tar", .type = .application, .name = "Tar archive" },
    .{ .ext = "rar", .mime = "application/vnd.rar", .type = .application, .name = "RAR archive" },
    .{ .ext = "7z", .mime = "application/x-7z-compressed", .type = .application, .name = "7-Zip archive" },
    .{ .ext = "bin", .mime = "application/octet-stream", .type = .application, .name = "Binary data" },
    .{ .ext = "exe", .mime = "application/x-msdownload", .type = .application, .name = "Windows executable" },
    .{ .ext = "jar", .mime = "application/java-archive", .type = .application, .name = "Java archive" },
    .{ .ext = "wasm", .mime = "application/wasm", .type = .application, .name = "WebAssembly" },
    .{ .ext = "doc", .mime = "application/msword", .type = .application, .name = "Word document" },
    .{ .ext = "docx", .mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document", .type = .application, .name = "Word document (OOXML)" },
    .{ .ext = "xls", .mime = "application/vnd.ms-excel", .type = .application, .name = "Excel spreadsheet" },
    .{ .ext = "xlsx", .mime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", .type = .application, .name = "Excel spreadsheet (OOXML)" },
    .{ .ext = "ppt", .mime = "application/vnd.ms-powerpoint", .type = .application, .name = "PowerPoint" },
    .{ .ext = "pptx", .mime = "application/vnd.openxmlformats-officedocument.presentationml.presentation", .type = .application, .name = "PowerPoint (OOXML)" },

    // ── font ────────────────────────────────────────────────────────────
    .{ .ext = "woff", .mime = "font/woff", .type = .font, .name = "WOFF font" },
    .{ .ext = "woff2", .mime = "font/woff2", .type = .font, .name = "WOFF2 font" },
    .{ .ext = "ttf", .mime = "font/ttf", .type = .font, .name = "TrueType font" },
    .{ .ext = "otf", .mime = "font/otf", .type = .font, .name = "OpenType font" },
    .{ .ext = "eot", .mime = "application/vnd.ms-fontobject", .type = .application, .name = "Embedded OpenType" },

    // ── multipart ───────────────────────────────────────────────────────
    .{ .ext = "mht", .mime = "multipart/related", .type = .multipart, .name = "MHTML web archive" },
    .{ .ext = "mhtml", .mime = "multipart/related", .type = .multipart, .name = "MHTML web archive" },
};

/// Lowercase an extension and drop a leading dot so ".JSON" == "json".
/// Writes into `out` (truncating if the input exceeds it) and returns the
/// normalized slice. Mirrors trim().toLowerCase().replace(/^\.+/, "").
pub fn normalizeExt(ext: []const u8, out: []u8) []const u8 {
    var s = std.mem.trim(u8, ext, " \t\r\n");
    while (s.len > 0 and s[0] == '.') s = s[1..]; // drop one or more dots
    const n = @min(s.len, out.len);
    return std.ascii.lowerString(out[0..n], s[0..n]);
}

/// Every entry whose extension matches. The dot is optional and matching is
/// case-insensitive, so ".js", "JS", and "js" are equivalent. Matches are
/// copied into `out`; returns the match count.
pub fn lookupByExt(ext: []const u8, out: []MimeEntry) usize {
    var buf: [64]u8 = undefined;
    const e = normalizeExt(ext, &buf);
    if (e.len == 0) return 0;
    var n: usize = 0;
    for (MIME) |m| {
        if (std.mem.eql(u8, m.ext, e)) {
            if (n < out.len) out[n] = m;
            n += 1;
        }
    }
    return n;
}

/// Every entry whose MIME matches, case-insensitively, tolerating
/// surrounding whitespace. One MIME commonly maps to several extensions
/// (image/jpeg → jpg, jpeg), so all are returned.
pub fn lookupByMime(mimeType: []const u8, out: []MimeEntry) usize {
    var buf: [128]u8 = undefined;
    const m = std.ascii.lowerString(&buf, std.mem.trim(u8, mimeType, " \t\r\n"));
    if (m.len == 0) return 0;
    var n: usize = 0;
    for (MIME) |e| {
        // Table mimes are already lowercase, so the comparison is one-sided.
        if (std.mem.eql(u8, e.mime, m)) {
            if (n < out.len) out[n] = e;
            n += 1;
        }
    }
    return n;
}

/// Optional constraints — null fields are ignored, mirroring `undefined`
/// fields in the TypeScript source.
pub const MimeFilter = struct {
    type: ?MimeCategory = null,
    query: ?[]const u8 = null,
};

/// Case-insensitive substring test: lowers `hay` into `buf` first.
fn containsLower(hay: []const u8, needle_lower: []const u8, buf: []u8) bool {
    const hay_lower = std.ascii.lowerString(buf[0..@min(hay.len, buf.len)], hay[0..@min(hay.len, buf.len)]);
    return std.mem.indexOf(u8, hay_lower, needle_lower) != null;
}

/// Filter the full table by category and/or a free-text query matched
/// (case-insensitively) against ext, mime, and name. Omitted options are
/// ignored; an empty query returns the category-filtered set unchanged.
/// An unknown category value yields nothing (it is not silently coerced).
pub fn filter(opts: MimeFilter, out: []MimeEntry) usize {
    var qbuf: [64]u8 = undefined;
    const q = if (opts.query) |raw|
        std.ascii.lowerString(&qbuf, std.mem.trim(u8, raw, " \t\r\n"))
    else
        qbuf[0..0];
    var n: usize = 0;
    for (MIME) |e| {
        if (opts.type) |t| {
            if (e.type != t) continue;
        }
        if (q.len > 0) {
            var eb: [16]u8 = undefined;
            var mb: [96]u8 = undefined;
            var nb: [48]u8 = undefined;
            if (!containsLower(e.ext, q, &eb) and
                !containsLower(e.mime, q, &mb) and
                !containsLower(e.name, q, &nb))
                continue;
        }
        if (n < out.len) out[n] = e;
        n += 1;
    }
    return n;
}

pub fn main() !void {
    const w = std.io.getStdOut().writer();

    var hits: [MIME.len]MimeEntry = undefined;
    const n = lookupByExt(".JSON", &hits);
    if (n > 0) try w.print(".JSON -> {s} ({s})\n", .{ hits[0].mime, hits[0].name });

    const j = lookupByMime("IMAGE/JPEG", &hits);
    for (hits[0..j]) |e| try w.print("image/jpeg -> .{s}\n", .{e.ext});

    const f = filter(.{ .query = "font" }, &hits);
    try w.print("query \"font\": {d} of {d} entries\n", .{ f, MIME.len });
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →