Skip to content
heff

std.mime

Media types (MIME types, RFC 2045 and RFC 6838): the type of a file by its extension (typeByExtension), the extensions of a type (extensionsByType), and a type with its parameters read from its text (parseMediaType, RFC 2045 §5.1 and RFC 2231’s continuations and encodings) and written back (renderMediaType). Go’s mime is the model. A Content-Disposition (RFC 2183) has the same shape, a disposition and its parameters, and is read the same way. Import it by a name, import std.mime as MIME.

import std.mime as MIME
-- | >>> charset "text/html; charset=\"UTF-8\""
-- | Just "UTF-8"
charset : String -> Maybe String
charset text = case MIME.parseMediaType text of
Nothing -> Nothing
Just m -> List.lookup "charset" m.parameters

MediaType

type MediaType = { mediaType : String, parameters : List (String, String) }

A media type as parseMediaType reads it: the type and its subtype, text/html, or a disposition, form-data — in lower case — and its parameters in their order, each a name, in lower case, and its value, unquoted and unescaped.

typeByExtension

typeByExtension : String -> Maybe String

The type of a file by its extension, its dot before it, its case not minded, as Go’s TypeByExtension has it from its own table: .avif .css .gif .htm .html .jpeg .jpg .js .json .mjs .pdf .png .svg .wasm .webp .xml; and .csv .gz .ico .md .mp3 .mp4 .ogg .otf .tar .toml .ttf .txt .wav .webm .woff .woff2 .yaml .yml .zip, as IANA registers them (.ico image/vnd.microsoft.icon, .tar application/x-tar, .wav audio/wav, as browsers name it). A text type has charset=utf-8; no file is read, as Go reads the system’s.

>>> List.map typeByExtension [".html", ".PNG", ".Woff2", ".yml"]
[Just "text/html; charset=utf-8", Just "image/png", Just "font/woff2", Just "application/yaml"]
>>> List.map typeByExtension ["html", ".exe", ""]
[Nothing, Nothing, Nothing]

parseMediaType

parseMediaType : String -> Maybe MediaType

A media type read from its text, as Go’s ParseMediaType reads it: a token, / and a token — or a token alone, a disposition — in either case, then ; and a parameter each, a token, = and a value, a token or a quoted string, spaces around them all. In a quoted string a \ escapes the tspecial after it, any other it is itself (as Internet Explorer writes a path, "C:\dir\a.txt"); a line break in one is none. A ; at the end is no parameter. A name may be there twice with the same value, kept once; with another, nothing. RFC 2231’s are read: name*=utf-8''%E2%82%AC (its charset utf-8 or us-ascii, the language not kept), and name*0, name*1… in pieces, any of them *-encoded, each one parameter name; one that does not read is left out.

>>> parseMediaType "Text/HTML; Charset=\"utf-8\"; format=fixed"
Just { mediaType = "text/html", parameters = [MkPair "charset" "utf-8", MkPair "format" "fixed"] }
>>> parseMediaType " form-data ; name=foo ;"
Just { mediaType = "form-data", parameters = [MkPair "name" "foo"] }
>>> parseMediaType "text/plain"
Just { mediaType = "text/plain", parameters = [] }

(quoted strings: escapes, a \ before what is no tspecial, empty)

>>> parseMediaType "attachment; filename=\"C:\\dev\\go\\robots.txt\""
Just { mediaType = "attachment", parameters = [MkPair "filename" "C:\\dev\\go\\robots.txt"] }
>>> parseMediaType "attachment; filename=\"C:\\\\dev\\\\go\\\\robots.txt\""
Just { mediaType = "attachment", parameters = [MkPair "filename" "C:\\dev\\go\\robots.txt"] }
>>> parseMediaType "foo; a=\"say \\\"hi\\\"; ok\"; b=\"\""
Just { mediaType = "foo", parameters = [MkPair "a" "say \"hi\"; ok", MkPair "b" ""] }

(a name twice: once with the same value, nothing with another)

>>> parseMediaType "foo; key=val; KEY=\"val\""
Just { mediaType = "foo", parameters = [MkPair "key" "val"] }
>>> parseMediaType "foo; key=val1; key=the-key-appears-again-which-is-bogus"
Nothing

(RFC 2231’s examples)

>>> parseMediaType "message/external-body; access-type=URL; URL*0=\"ftp://\"; URL*1=\"cs.utk.edu/pub/moore/bulk-mailer/bulk-mailer.tar\""
Just { mediaType = "message/external-body", parameters = [MkPair "access-type" "URL", MkPair "url" "ftp://cs.utk.edu/pub/moore/bulk-mailer/bulk-mailer.tar"] }
>>> parseMediaType "application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A"
Just { mediaType = "application/x-stuff", parameters = [MkPair "title" "This is ***fun***"] }
>>> parseMediaType "application/x-stuff; title*0*=us-ascii'en'This%20is%20even%20more%20; title*1*=%2A%2A%2Afun%2A%2A%2A%20; title*2=\"isn't it!\""
Just { mediaType = "application/x-stuff", parameters = [MkPair "title" "This is even more ***fun*** isn't it!"] }
>>> parseMediaType "attachment; filename=\"EURO rates\"; filename*=utf-8''%e2%82%ac%20rates"
Just { mediaType = "attachment", parameters = [MkPair "filename" "\u{20ac} rates"] }
>>> parseMediaType "attachment; filename=plain; filename*=iso-8859-1''%A3"
Just { mediaType = "attachment", parameters = [MkPair "filename" "plain"] }

(what is not one: no type, no subtype, two slashes, a space in it, a parameter without = or a value, a quote not closed, a line break in one, text after one, a ; alone)

>>> (List.map (\s -> maybe True (\_ -> False) (parseMediaType s)) ["", "/html", "text/", "text/html/x", "text html", "foo; bar", "foo; bar=", "foo; =x", "foo; a=\"b", "foo; a=\"b\nc\"", "foo; a=\"b\"c", "foo; a=b c", "foo; ; a=b", "bogus ;========="] : List Bool)
[True, True, True, True, True, True, True, True, True, True, True, True, True, True]

extensionsByType

extensionsByType : String -> List String

The extensions of a media type, its parameters not minded, in their order: those typeByExtension has of it.

>>> extensionsByType "text/html; charset=utf-8"
[".htm", ".html"]
>>> List.map extensionsByType ["IMAGE/JPEG", "application/yaml", "text/x-nothing", "not a type"]
[[".jpeg", ".jpg"], [".yaml", ".yml"], [], []]

renderMediaType

renderMediaType : MediaType -> String

The text of a media type, as Go’s FormatMediaType writes it, but in the parameters’ order: the type, then ; and a parameter each, the type and the names in lower case, a value a token as it is, any other quoted, a " and a \ in it escaped — one with a control byte or one not ASCII (a tab is neither) written name*=utf-8'' and its bytes, those not of a token or *'% escaped %XX, as RFC 2231 writes it. Of a type or a name that is not one, "".

>>> renderMediaType { mediaType = "Text/HTML", parameters = [("Charset", "utf-8"), ("title", "say \"hi\"\t\\o/")] }
"text/html; charset=utf-8; title=\"say \\\"hi\\\"\t\\\\o/\""
>>> renderMediaType { mediaType = "attachment", parameters = [("filename", "\u{20ac} 100%.txt"), ("empty", "")] }
"attachment; filename*=utf-8''%E2%82%AC%20100%25.txt; empty=\"\""
>>> List.map renderMediaType [{ mediaType = "text/", parameters = [] }, { mediaType = "text/plain", parameters = [("a b", "c")] }]
["", ""]

(what it writes, parseMediaType reads back)

>>> parseMediaType (renderMediaType { mediaType = "form-data", parameters = [("name", "a;b"), ("filename", "\u{e9}t\u{e9} \"x\".txt"), ("x", "\\")] })
Just { mediaType = "form-data", parameters = [MkPair "name" "a;b", MkPair "filename" "\u{e9}t\u{e9} \"x\".txt", MkPair "x" "\\"] }