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.
net/http sniff cases.application/octet-stream, so an unknown ZIP-based format is
still a ZIP, and an allowlist of "image" accepts every
image.mime.types, no Windows registry, no libmagic: the extension
table is generated from IANA’s registry and Apache httpd’s
mime.types.Install zumime from CRAN:
install.packages("zumime")Or install the development version from GitHub:
# install.packages("pak")
pak::pak("pedrobtz/zumime")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/csvmime_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 FALSEmime_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] TRUEEvery 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.
agree, encrypted and complete
columns, and the incremental sniffer on a stream’s first chunk.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.