Skip to main content

winprint_kit/
file_type.rs

1//! Detect file types from URL or Content-Type.
2
3use url::Url;
4
5const SUPPORTED_TYPES: &[&str] = &[
6    "pdf", "docx", "doc", "xlsx", "xls", "pptx", "ppt", "odt", "ods", "odp", "jpg", "jpeg", "png",
7    "gif", "bmp", "tif", "tiff", "webp", "html", "htm",
8    "xps",
9];
10
11const BOUNDARY_CHARS: &[u8] = b"?&#/\"' ;:";
12
13fn validate_extension(ext: &str) -> Option<String> {
14    let ext = ext.trim().to_lowercase();
15    if ext.is_empty() || ext.len() > 10 {
16        return None;
17    }
18    SUPPORTED_TYPES.contains(&ext.as_str()).then_some(ext)
19}
20
21fn is_boundary(s: &str, pos: usize) -> bool {
22    pos >= s.len() || BOUNDARY_CHARS.contains(&s.as_bytes()[pos])
23}
24
25fn extract_ext_from_filename(filename: &str) -> Option<String> {
26    let dot_pos = filename.rfind('.')?;
27    validate_extension(&filename[dot_pos + 1..])
28}
29
30pub fn detect_type_from_url(url: &str) -> Option<String> {
31    if let Ok(parsed) = Url::parse(url) {
32        if let Some(filename) = parsed.path_segments().and_then(|mut segs| segs.next_back()) {
33            if let Some(ext) = extract_ext_from_filename(filename) {
34                return Some(ext);
35            }
36        }
37
38        for (_key, value) in parsed.query_pairs() {
39            if let Some(ext) = extract_ext_from_filename(&value) {
40                return Some(ext);
41            }
42        }
43    }
44
45    let url_lower = url.to_lowercase();
46
47    SUPPORTED_TYPES.iter().find_map(|ext| {
48        [format!(".{}", ext), format!("%2e{}", ext)]
49            .into_iter()
50            .find_map(|pattern| {
51                url_lower.find(&pattern).and_then(|pos| {
52                    is_boundary(&url_lower, pos + pattern.len()).then(|| ext.to_string())
53                })
54            })
55    })
56}
57
58pub fn detect_type_from_content_type(content_type: &str) -> Option<String> {
59    let ct = content_type.split(';').next()?.trim().to_lowercase();
60    let extension = match ct.as_str() {
61        "application/pdf" => "pdf",
62        "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => "docx",
63        "application/msword" => "doc",
64        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => "xlsx",
65        "application/vnd.ms-excel" => "xls",
66        "application/vnd.openxmlformats-officedocument.presentationml.presentation" => "pptx",
67        "application/vnd.ms-powerpoint" => "ppt",
68        "application/vnd.oasis.opendocument.text" => "odt",
69        "application/vnd.oasis.opendocument.spreadsheet" => "ods",
70        "application/vnd.oasis.opendocument.presentation" => "odp",
71        "application/vnd.ms-xpsdocument" => "xps",
72        "image/jpeg" => "jpg",
73        "image/png" => "png",
74        "image/gif" => "gif",
75        "image/bmp" => "bmp",
76        "image/tiff" => "tiff",
77        "image/webp" => "webp",
78        "text/html" => "html",
79        "application/xhtml+xml" => "htm",
80        _ => return None,
81    };
82    Some(extension.to_string())
83}
84
85pub fn is_supported_type(file_type: &str) -> bool {
86    SUPPORTED_TYPES.contains(&file_type)
87}
88
89pub fn is_office_type(typ: &str) -> bool {
90    matches!(
91        typ,
92        "doc" | "docx" | "odt" | "xls" | "xlsx" | "ods" | "ppt" | "pptx" | "odp"
93    )
94}
95
96pub fn is_image_type(typ: &str) -> bool {
97    matches!(
98        typ,
99        "png" | "jpg" | "jpeg" | "gif" | "bmp" | "tiff" | "tif" | "webp"
100    )
101}
102
103pub fn is_html_type(typ: &str) -> bool {
104    matches!(typ, "html" | "htm")
105}