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
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 errorWhat 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 errorWhether 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 errorWhether 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.qweOr 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.pngThen, in your program, all of it or only what you call:
use file_type.qweruse file_type, file_type_allowed from file_type // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
infer = { version = "0.19", default-features = false, features = ["std"] }Example
// 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
//! 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"), })}Related
models_3d.qwer · 1.0.0STL and OBJ models read: triangles, size, volume, surface area and whether the mesh is closed, a 3D-print quote by weight, and OBJ turned into binary STL.6 views · 7 downloadsDisk space disk_space.qwer · 1.0.0Free and total space of the disk a path is on, for health checks, refusing uploads before the disk fills, and admin pages.8 views · 8 downloadsFonts fonts.qwer · 1.0.0A font file's names, weight and style, glyph count, which characters it covers, and how wide a text is set in it.5 views · 7 downloadsMIME types mime.qwer · 1.0.0MIME types from file names and extensions, the extensions of a type, and Content-Type headers with a charset for text.8 views · 7 downloadsZip files archive.qwer · 1.0.0Zip files made from several files and opened safely: nothing written outside the folder, no zip bombs.6 views · 5 downloadsUpload checks upload_guard.qwer · 1.0.0Uploads checked before they are kept: the real type by magic bytes against the name and an allow list, size, double extensions, programs, zip bombs and zip slips, SVGs and PDFs that run scripts, macros and polyglots.7 views · 7 downloadsThread 0 posts
On File type: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.