Files

File type

What a file really is from its first bytes, not its name: MIME type, extension and kind, for upload checks.

Version 1.0.0 · 3 functions · 1 crate

#files#uploads#forms#security

Functions

What use file_type.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use file_type, file_type_allowed from file_type (Quillo 0.6).

file_type(path: str) → anyor an error

What the file at path is, from its first 8 KB: { mime, extension, kind }, kind one of image, video, audio, archive, document, book, font, app, text, other. A file with no known signature (plain text, CSV, JSON) gives { mime: "application/octet-stream", extension: "", kind: "unknown" }.

file_type_allowed(path: str, allowed: list) → boolor an error

Whether the file is one of allowed: each entry an extension ("png"), a MIME type ("application/pdf"), a family ("image/*") or a kind ("image"). A file with no known signature is never allowed: for uploads of pictures, PDFs and the like.

file_type_matches_name(path: str, name: str) → boolor an error

Whether a file's content fits the extension of the name it came with (name, as the browser sent it): false for a PDF called "photo.jpg" or a program called "cv.pdf". Names ending in txt, csv, json, md, html, svg, xml or log have no signature to check, and match when the content has none either.

Get it

$ quillo install file_type               # file_type.qwer into this folder$ quillo install file_type --example     # and its example, file_type.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/file_type.qwer$ curl -O https://quillolang.org/plugins/files/file_type.qwe$ curl -O https://quillolang.org/plugins/files/file_type_sample.png

Then, in your program, all of it or only what you call:

main.qwe
use file_type.qweruse file_type, file_type_allowed from file_type      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
infer = { version = "0.19", default-features = false, features = ["std"] }

Example

file_type.qwe
// Upload checks that read what a file is, not what it is called.////   quillo run file_type.qwe      then open http://localhost:8080/checkuse "file_type.qwer"// A real upload form: only pictures and PDFs, and only when the name tells the truth.route GET /upload = <html><body>    <form method="post" action="/upload" enctype="multipart/form-data">        <input type="file" name="doc"/> <button>Send</button>    </form></body></html>route POST /upload {    let f = request.files.doc ?? reply 400 "Choose a file"    let path = save_upload(f, `data/uploads/${uuid()}`)?    if !file_type_allowed(path, ["image/*", "pdf"])? { delete_file(path); reply 415 "Pictures and PDFs only" }    if !file_type_matches_name(path, f.filename)? { delete_file(path); reply 415 "The file is not what its name says" }    { saved: path, type: file_type(path)? }}// The sample picture, and a "photo" that is really a PDF.route GET /check {    write_file("data/holiday.jpg", "%PDF-1.7\n1 0 obj << >> endobj\n")    {        sample: file_type("file_type_sample.png")?,        sample_is_image: file_type_allowed("file_type_sample.png", ["image"])?,        fake: file_type("data/holiday.jpg")?,        fake_is_image: file_type_allowed("data/holiday.jpg", ["image/*"])?,        fake_name_fits: file_type_matches_name("data/holiday.jpg", "holiday.jpg")?,    }}
$ quillo run file_type.qwe$ curl 'localhost:8080/check'

Source

file_type.qwer
//! What a file really is, from its first bytes rather than its name: MIME type, extension and kind, for checking uploads.//! version: 1.0.0//!//! [dependencies]//! infer = { version = "0.19", default-features = false, features = ["std"] }use serde_json::{json, Value};use std::io::Read;fn head(path: &str) -> Result<Vec<u8>, String> {    let f = std::fs::File::open(path).map_err(|e| format!("{}: {}", path, e))?;    let mut buf = Vec::with_capacity(8192);    f.take(8192).read_to_end(&mut buf).map_err(|e| format!("{}: {}", path, e))?;    Ok(buf)}fn kind(t: &infer::Type) -> &'static str {    match t.matcher_type() {        infer::MatcherType::App => "app",        infer::MatcherType::Archive => "archive",        infer::MatcherType::Audio => "audio",        infer::MatcherType::Book => "book",        infer::MatcherType::Doc => "document",        infer::MatcherType::Font => "font",        infer::MatcherType::Image => "image",        infer::MatcherType::Text => "text",        infer::MatcherType::Video => "video",        infer::MatcherType::Custom => "other",    }}fn detect(path: &str) -> Result<Option<(String, String, &'static str)>, String> {    let buf = head(path)?;    Ok(infer::get(&buf).map(|t| (t.mime_type().to_string(), t.extension().to_string(), kind(&t))))}// The same type under several extensions.fn same_ext(a: &str, b: &str) -> bool {    let norm = |e: &str| match e {        "jpeg" | "jpe" | "jfif" => "jpg".to_string(),        "tiff" => "tif".to_string(),        "htm" => "html".to_string(),        "mpeg" => "mpg".to_string(),        "oga" | "opus" => "ogg".to_string(),        "m4v" => "mp4".to_string(),        e => e.to_string(),    };    norm(a) == norm(b)}/// What the file at `path` is, from its first 8 KB: `{ mime, extension, kind }`, `kind` one of image, video, audio, archive, document, book, font, app, text, other./// A file with no known signature (plain text, CSV, JSON) gives `{ mime: "application/octet-stream", extension: "", kind: "unknown" }`.pub fn file_type(path: &str) -> Result<Value, String> {    Ok(match detect(path)? {        Some((mime, ext, kind)) => json!({ "mime": mime, "extension": ext, "kind": kind }),        None => json!({ "mime": "application/octet-stream", "extension": "", "kind": "unknown" }),    })}/// Whether the file is one of `allowed`: each entry an extension ("png"), a MIME type ("application/pdf"), a family ("image/*") or a kind ("image")./// A file with no known signature is never allowed: for uploads of pictures, PDFs and the like.pub fn file_type_allowed(path: &str, allowed: Vec<String>) -> Result<bool, String> {    let Some((mime, ext, kind)) = detect(path)? else { return Ok(false) };    Ok(allowed.iter().map(|a| a.trim().to_lowercase()).any(|a| {        let a = a.trim_start_matches('.');        if let Some(family) = a.strip_suffix("/*") {            mime.split('/').next() == Some(family)        } else if a.contains('/') {            a == mime        } else {            same_ext(a, &ext) || a == kind        }    }))}/// Whether a file's content fits the extension of the name it came with (`name`, as the browser sent it): false for a PDF called "photo.jpg" or a program called "cv.pdf"./// Names ending in txt, csv, json, md, html, svg, xml or log have no signature to check, and match when the content has none either.pub fn file_type_matches_name(path: &str, name: &str) -> Result<bool, String> {    let claimed = name.rsplit_once('.').map(|(_, e)| e.to_lowercase()).unwrap_or_default();    Ok(match detect(path)? {        Some((_, ext, _)) => same_ext(&claimed, &ext),        None => matches!(claimed.as_str(), "txt" | "csv" | "tsv" | "json" | "md" | "html" | "htm" | "svg" | "xml" | "log" | "ics" | "vcf"),    })}
Thread
79Useful? Like it, or add to its thread below.

Thread 0 posts

On File type: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.