zumime

Lifecycle: experimental R-CMD-check coverage

A file’s name says what someone hopes it is; its bytes say what it is. An upload named invoice.pdf can be a PNG, a .docx is a ZIP archive inside, and a .csv.gz holds a CSV you cannot see until you open the gzip. Before a parser starts, you want to know which of these you have, and whether the file is encrypted or cut short.

zumime answers from both signals in one call and says whether they agree. It needs no other package at run time and no system library, so it gives the same answer on every machine.

Installation

Install zumime from CRAN:

install.packages("zumime")

Or install the development version from GitHub:

# install.packages("pak")
pak::pak("pedrobtz/zumime")

Usage

library(zumime)

Take a folder of uploads: a real PDF, a PNG that claims to be a PDF, a compressed CSV and an R data file.

uploads <- tempfile("uploads")
dir.create(uploads)
writeLines("%PDF-1.7", file.path(uploads, "report.pdf"))
writeBin(as.raw(c(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a)),
  file.path(uploads, "invoice.pdf"))
con <- gzfile(file.path(uploads, "sales.csv.gz"), "w")
writeLines(c("region,total", "north,10"), con)
close(con)
saveRDS(mtcars, file.path(uploads, "cars.rds"))

paths <- list.files(uploads, full.names = TRUE)

mime_type() types each file from its bytes and from its name. agree is FALSE for the PNG named invoice.pdf, and inner names what the gzip holds, guessed from the .csv before the .gz:

files <- mime_type(paths)
files$path <- basename(files$path)
files[c("path", "type", "by_name", "agree", "inner")]
#> <zumime_result: 4 rows>
#>           path              type           by_name agree    inner
#> 1     cars.rds application/x-rds application/x-rds  TRUE     <NA>
#> 2  invoice.pdf         image/png   application/pdf FALSE     <NA>
#> 3   report.pdf   application/pdf   application/pdf  TRUE     <NA>
#> 4 sales.csv.gz  application/gzip  application/gzip  TRUE text/csv

mime_check() asks only the allowlist question, from the bytes alone, since that is the question you ask because names lie. Of the two files named .pdf, one is:

setNames(mime_check(paths, "application/pdf"), basename(paths))
#>     cars.rds  invoice.pdf   report.pdf sales.csv.gz 
#>        FALSE        FALSE         TRUE        FALSE

mime_sniff() runs the standard on bytes in hand, as a browser would on a response. A text/plain label is one some servers send for anything, so the standard checks it and finds binary bytes; under nosniff the label stands:

png <- readBin(file.path(uploads, "invoice.pdf"), "raw", 8)
mime_sniff(png, type = "text/plain")$type
#> [1] "application/octet-stream"
mime_sniff(png, type = "text/plain", no_sniff = TRUE)$type
#> [1] "text/plain"

Types form a hierarchy, so a question about a family covers its members:

docx <- "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
mime_parent(docx)
#> [[1]]
#> [1] "application/zip"          "application/octet-stream"
mime_is(docx, "application/zip")
#> [1] TRUE

Every function but zumime_info() starts with mime_. Every type zumime finds is a canonical name from the registry that mime_types() lists; a supplied type that stands comes back as its lower-cased essence.

Learn more

The byte answer follows the WHATWG MIME Sniffing Standard and is tested against Go’s net/http sniff cases; mime_parse() is tested against the web-platform-tests mimesniff fixtures. Container inspection follows file-type, filetype and mimetype; the extension table is generated from the IANA media types registry and Apache httpd’s mime.types. zumime’s code is written from these references, not copied from them. The test suite uses Go’s sniff cases and file-type’s answers as oracles, credited where they are used.