1use crate::error::{DocError, Result};
4
5const OLE_MAGIC: &[u8] = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1";
6const ZIP_MAGIC: &[u8] = b"PK\x03\x04";
7const PDF_MAGIC: &[u8] = b"%PDF";
8
9const OOXML: &[&str] = &["docx", "xlsx", "pptx"];
10const OLE_EXT: &[&str] = &["doc", "ppt", "xls"];
11const TEXT_EXT: &[&str] = &["txt", "md", "markdown", "csv", "html", "htm"];
12
13fn head_of(path: &str, n: usize) -> Result<Vec<u8>> {
14 std::fs::read(path)
15 .map(|v| v[..v.len().min(n)].to_vec())
16 .map_err(|e| DocError::corrupt(format!("cannot read {path}: {e}")))
17}
18
19pub fn sniff(path: &str) -> Result<&'static str> {
24 let ext = path
25 .rsplit('.')
26 .next()
27 .map(|e| e.to_ascii_lowercase())
28 .unwrap_or_default();
29 let h = head_of(path, 8)?;
30
31 if h.starts_with(OLE_MAGIC) {
32 if OOXML.contains(&ext.as_str()) {
33 return Err(DocError::encrypted(format!("{ext} 为加密容器(OLE)")));
34 }
35 if OLE_EXT.contains(&ext.as_str()) {
36 return Ok("ole");
37 }
38 return Err(DocError::unsupported(format!(
39 "OLE 容器但扩展名不支持: {ext}"
40 )));
41 }
42 if h.starts_with(ZIP_MAGIC) {
43 if ext == "epub" {
45 return Ok("epub");
46 }
47 if OOXML.contains(&ext.as_str()) {
49 return Ok(match ext.as_str() {
50 "docx" => "docx",
51 "xlsx" => "xlsx",
52 _ => "pptx",
53 });
54 }
55 return Err(DocError::corrupt("ZIP 容器但缺少 OOXML 关键部件"));
56 }
57 if h.starts_with(PDF_MAGIC) {
58 return Ok("pdf");
59 }
60 if OLE_EXT.contains(&ext.as_str()) {
61 return Err(DocError::unsupported(format!(
62 "{ext} 文件头不是 OLE,疑似伪装"
63 )));
64 }
65 if TEXT_EXT.contains(&ext.as_str()) {
66 let kind: &'static str = match ext.as_str() {
67 "txt" => "txt",
68 "md" | "markdown" => "md",
69 "csv" => "csv",
70 "html" | "htm" => "html",
71 _ => "txt",
72 };
73 return Ok(kind);
74 }
75 Ok("txt") }
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use std::io::Write;
82
83 #[test]
84 fn sniff_pdf_and_ole() {
85 let d = std::env::temp_dir();
86 let pdf = d.join("docparser_router_test.pdf");
87 let mut f = std::fs::File::create(&pdf).unwrap();
88 f.write_all(b"%PDF-1.7\n%%EOF").unwrap();
89 drop(f);
90 assert_eq!(sniff(pdf.to_str().unwrap()).unwrap(), "pdf");
91 std::fs::remove_file(&pdf).ok();
92
93 let renamed = d.join("renamed_pdf.doc");
95 let mut f = std::fs::File::create(&renamed).unwrap();
96 f.write_all(b"%PDF-1.7\n%%EOF").unwrap();
97 drop(f);
98 assert_eq!(sniff(renamed.to_str().unwrap()).unwrap(), "pdf");
99 std::fs::remove_file(&renamed).ok();
100
101 let fake = d.join("fake.doc");
103 let mut f = std::fs::File::create(&fake).unwrap();
104 f.write_all(b"NOTANOFFICEFILE").unwrap();
105 drop(f);
106 let err = sniff(fake.to_str().unwrap()).unwrap_err();
107 assert_eq!(err.code.as_str(), "unsupported");
108 std::fs::remove_file(&fake).ok();
109 }
110
111 #[test]
112 fn ooxml_ole_container_is_encrypted() {
113 let d = std::env::temp_dir();
114 let fx = d.join("enc.docx");
115 let mut f = std::fs::File::create(&fx).unwrap();
116 f.write_all(OLE_MAGIC).unwrap();
117 drop(f);
118 let err = sniff(fx.to_str().unwrap()).unwrap_err();
119 assert_eq!(err.code.as_str(), "encrypted");
120 std::fs::remove_file(&fx).ok();
121 }
122}