Skip to main content

pagesift_api/types/
attachment.rs

1//! 附件:随页面一起导出的文件(PDF 内嵌图片、Office 媒体、网页图片)。
2//!
3//! 对外的 JSON 只有 `{id, format, bytes, hash}`(加上来源 `source`);文件内容不进 JSON,
4//! HTTP API 用单独的接口下载(`GET /v2/attachments/{id}`)。在 pagesift 内部(缓存、多服务器的
5//! 任务结果)用非人类可读的二进制编码时连同内容一起编码,结果交给网关时附件不丢。
6
7use std::sync::Arc;
8
9use serde::de::{self, Deserializer, MapAccess, Visitor};
10use serde::ser::{SerializeStruct, Serializer};
11use serde::{Deserialize, Serialize};
12
13/// 一个附件。`id` 是内容寻址的文件名:SHA-256 的前 32 位十六进制加扩展名(与 PDF、Office 的
14/// 导出文件名同一规则),同一内容只有一个 id。
15#[derive(Debug, Clone, PartialEq, Eq, Default)]
16pub struct Attachment {
17    pub id: String,
18    /// 格式(文件扩展名):jpg、png、gif、webp、svg、jp2、tif、jb2……
19    pub format: String,
20    /// 字节数。
21    pub bytes: u64,
22    /// 内容的 SHA-256(小写十六进制)。
23    pub hash: String,
24    /// 来源:网页图片的原地址、Office 文档里的部件路径(与 Markdown 里的引用一致);PDF 图片为空
25    /// (Markdown 里直接引用 `images/<id>`)。
26    pub source: Option<String>,
27    /// 文件内容。
28    pub data: Option<Arc<[u8]>>,
29    /// Include Base64 data in human-readable output for the opt-in asset API.
30    pub inline_data: bool,
31}
32
33impl Attachment {
34    /// 由内容构造:算哈希、定 id。
35    pub fn new(data: Vec<u8>, format: &str, source: Option<String>) -> Self {
36        let hash = sha256_hex(&data);
37        Self {
38            id: format!("{}.{format}", &hash[..32]), // [协议] 与 PDF/Office 导出同一规则:32 位十六进制 = 128 位哈希前缀
39            format: format.to_string(),
40            bytes: data.len() as u64,
41            hash,
42            source,
43            data: Some(data.into()),
44            inline_data: false,
45        }
46    }
47
48    /// 已经算好哈希(PDF、Office 的导出)时直接构造。
49    pub fn with_hash(hash: String, format: &str, data: Vec<u8>, source: Option<String>) -> Self {
50        Self {
51            id: format!("{}.{format}", &hash[..hash.len().min(32)]), // [协议] 同上
52            format: format.to_string(),
53            bytes: data.len() as u64,
54            hash,
55            source,
56            data: Some(data.into()),
57            inline_data: false,
58        }
59    }
60
61    /// 按格式给的 MIME 类型(下载接口的 Content-Type)。
62    pub fn mime(&self) -> &'static str {
63        mime_of(&self.format)
64    }
65}
66
67/// 扩展名 → MIME 类型(IANA 媒体类型注册表)。
68pub fn mime_of(format: &str) -> &'static str {
69    match format {
70        "jpg" | "jpeg" => "image/jpeg",
71        "png" => "image/png",
72        "gif" => "image/gif",
73        "webp" => "image/webp",
74        "svg" => "image/svg+xml",
75        "bmp" => "image/bmp",
76        "avif" => "image/avif",
77        "ico" => "image/vnd.microsoft.icon",
78        "tif" | "tiff" => "image/tiff",
79        "jp2" => "image/jp2",
80        "j2k" => "image/j2c",
81        "jb2" => "image/x-jbig2",
82        "emf" => "image/emf",
83        "wmf" => "image/wmf",
84        _ => "application/octet-stream",
85    }
86}
87
88/// Identify an image from its bytes rather than a filename or HTTP header.
89/// SVG has no binary magic; this recognizes an SVG root but is not a sanitizer.
90pub fn sniff_image_format(body: &[u8]) -> Option<&'static str> {
91    let starts = |sig: &[u8]| body.starts_with(sig);
92    if starts(b"\x89PNG\r\n\x1a\n") {
93        Some("png")
94    } else if starts(&[0xff, 0xd8, 0xff]) {
95        Some("jpg")
96    } else if starts(b"GIF87a") || starts(b"GIF89a") {
97        Some("gif")
98    } else if starts(b"RIFF") && body.get(8..12) == Some(b"WEBP") {
99        Some("webp")
100    } else if body.get(4..12) == Some(b"ftypavif") {
101        Some("avif")
102    } else if starts(b"BM") {
103        Some("bmp")
104    } else if starts(&[0, 0, 1, 0]) {
105        Some("ico")
106    } else if starts(b"II\x2a\0")
107        || starts(b"MM\0\x2a")
108        || starts(b"II\x2b\0")
109        || starts(b"MM\0\x2b")
110    {
111        Some("tif")
112    } else if starts(b"\0\0\0\x0cjP  \r\n\x87\n") {
113        Some("jp2")
114    } else if starts(&[0xff, 0x4f, 0xff, 0x51]) {
115        Some("j2k")
116    } else if starts(b"\x97JB2\r\n\x1a\n") {
117        Some("jb2")
118    } else if starts(&[1, 0, 0, 0]) && body.get(40..44) == Some(b" EMF") {
119        Some("emf")
120    } else if starts(&[0xd7, 0xcd, 0xc6, 0x9a]) || starts(&[1, 0, 9, 0]) || starts(&[2, 0, 9, 0]) {
121        Some("wmf")
122    } else if svg_root(body) {
123        Some("svg")
124    } else {
125        None
126    }
127}
128
129fn svg_root(body: &[u8]) -> bool {
130    let mut rest = body.strip_prefix(b"\xef\xbb\xbf").unwrap_or(body);
131    loop {
132        rest = rest.trim_ascii_start();
133        let end = if rest.starts_with(b"<?xml") {
134            rest.windows(2).position(|w| w == b"?>").map(|p| p + 2)
135        } else if rest.starts_with(b"<!--") {
136            rest.windows(3).position(|w| w == b"-->").map(|p| p + 3)
137        } else {
138            break;
139        };
140        let Some(end) = end else { return false };
141        rest = &rest[end..];
142    }
143    rest.starts_with(b"<svg")
144        && rest
145            .get(4)
146            .is_some_and(|b| b.is_ascii_whitespace() || matches!(b, b'>' | b'/'))
147}
148
149fn sha256_hex(data: &[u8]) -> String {
150    let digest = ring::digest::digest(&ring::digest::SHA256, data);
151    digest.as_ref().iter().map(|b| format!("{b:02x}")).collect()
152}
153
154/// 原样写成 serde 的字节串(二进制编码里是一个长度前缀 + 原样字节,不是逐字节的数组)。
155struct Raw<'a>(&'a [u8]);
156
157impl Serialize for Raw<'_> {
158    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
159        s.serialize_bytes(self.0)
160    }
161}
162
163impl Serialize for Attachment {
164    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
165        // 人类可读(JSON)的出口只列元数据;内部二进制编码连内容一起带上
166        let human = s.is_human_readable();
167        let data = self.data.as_deref().filter(|_| !human || self.inline_data);
168        // [协议] 固定字段 id、format、bytes、hash 共 4 个
169        let fields = 4
170            + usize::from(self.source.is_some())
171            + usize::from(data.is_some())
172            + usize::from(human && self.inline_data)
173            + usize::from(self.inline_data);
174        let mut st = s.serialize_struct("Attachment", fields)?;
175        st.serialize_field("id", &self.id)?;
176        st.serialize_field("format", &self.format)?;
177        st.serialize_field("bytes", &self.bytes)?;
178        st.serialize_field("hash", &self.hash)?;
179        if let Some(src) = &self.source {
180            st.serialize_field("source", src)?;
181        }
182        if let Some(d) = data {
183            if human {
184                let encoded = pagesift_base64::STANDARD
185                    .encode_to_string(d)
186                    .map_err(serde::ser::Error::custom)?;
187                st.serialize_field("data", &encoded)?;
188            } else {
189                st.serialize_field("data", &Raw(d))?;
190            }
191        }
192        if human && self.inline_data {
193            st.serialize_field("mime", self.mime())?;
194        }
195        if self.inline_data {
196            st.serialize_field("inlineData", &true)?;
197        }
198        st.end()
199    }
200}
201
202impl<'de> Deserialize<'de> for Attachment {
203    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
204        d.deserialize_map(AttachmentVisitor)
205    }
206}
207
208struct AttachmentVisitor;
209
210impl<'de> Visitor<'de> for AttachmentVisitor {
211    type Value = Attachment;
212
213    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
214        f.write_str("an attachment")
215    }
216
217    fn visit_map<A: MapAccess<'de>>(self, mut m: A) -> Result<Attachment, A::Error> {
218        let mut a = Attachment::default();
219        while let Some(key) = m.next_key::<String>()? {
220            match key.as_str() {
221                "id" => a.id = m.next_value()?,
222                "format" => a.format = m.next_value()?,
223                "bytes" => a.bytes = m.next_value()?,
224                "hash" => a.hash = m.next_value()?,
225                "source" => a.source = m.next_value()?,
226                "data" => {
227                    let Bytes(data, encoded) = m.next_value::<Bytes>()?;
228                    a.data = Some(data.into());
229                    a.inline_data |= encoded;
230                }
231                "inlineData" => a.inline_data = m.next_value()?,
232                _ => {
233                    m.next_value::<de::IgnoredAny>()?;
234                }
235            }
236        }
237        if a.id.is_empty() {
238            return Err(de::Error::missing_field("id"));
239        }
240        Ok(a)
241    }
242}
243
244/// 读字节串:二进制编码给 bytes,自描述格式也可能给数组。
245struct Bytes(Vec<u8>, bool);
246
247impl<'de> Deserialize<'de> for Bytes {
248    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
249        let human = d.is_human_readable();
250        struct V(bool);
251        impl<'de> Visitor<'de> for V {
252            type Value = Bytes;
253            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
254                f.write_str("bytes")
255            }
256            fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Bytes, E> {
257                if self.0 {
258                    pagesift_base64::STANDARD
259                        .decode_to_vec(v)
260                        .map(|b| Bytes(b, true))
261                        .map_err(E::custom)
262                } else {
263                    Ok(Bytes(v.to_vec(), false))
264                }
265            }
266            fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Bytes, E> {
267                Ok(Bytes(v, false))
268            }
269            fn visit_seq<A: de::SeqAccess<'de>>(self, mut s: A) -> Result<Bytes, A::Error> {
270                let mut out = Vec::new();
271                while let Some(b) = s.next_element::<u8>()? {
272                    out.push(b);
273                }
274                Ok(Bytes(out, false))
275            }
276            fn visit_str<E: de::Error>(self, v: &str) -> Result<Bytes, E> {
277                pagesift_base64::STANDARD
278                    .decode_to_vec(v.as_bytes())
279                    .map(|bytes| Bytes(bytes, true))
280                    .map_err(E::custom)
281            }
282        }
283        d.deserialize_bytes(V(human))
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn image_format_comes_from_bytes() {
293        assert_eq!(sniff_image_format(b"\x89PNG\r\n\x1a\nrest"), Some("png"));
294        assert_eq!(sniff_image_format(b"\xff\xd8\xff\xe0"), Some("jpg"));
295        assert_eq!(sniff_image_format(b"GIF89a"), Some("gif"));
296        assert_eq!(sniff_image_format(b"II\x2a\0"), Some("tif"));
297        assert_eq!(sniff_image_format(b"\x97JB2\r\n\x1a\n"), Some("jb2"));
298        assert_eq!(
299            sniff_image_format(
300                b"<?xml version=\"1.0\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\"/>"
301            ),
302            Some("svg")
303        );
304        assert_eq!(sniff_image_format(b"<svg-not-an-image>"), None);
305        assert_eq!(sniff_image_format(b"<html><svg/></html>"), None);
306    }
307
308    #[test]
309    fn json_lists_metadata_only() {
310        let a = Attachment::new(
311            b"\x89PNG\r\n\x1a\nxyz".to_vec(),
312            "png",
313            Some("https://e.x/a.png".into()),
314        );
315        assert_eq!(a.id.len(), 32 + 4);
316        assert_eq!(a.bytes, 11);
317        let v = serde_json::to_value(&a).unwrap();
318        assert_eq!(v["format"], "png");
319        assert_eq!(v["hash"].as_str().unwrap().len(), 64);
320        assert!(v.get("data").is_none(), "内容不进 JSON");
321        let back: Attachment = serde_json::from_value(v).unwrap();
322        assert_eq!(back.data, None);
323        assert_eq!(back.id, a.id);
324    }
325
326    #[test]
327    fn opt_in_json_contains_base64_and_round_trips() {
328        let mut a = Attachment::new(
329            b"\x89PNG\r\n\x1a\nxyz".to_vec(),
330            "png",
331            Some("https://e.x/a".into()),
332        );
333        a.inline_data = true;
334        let value = serde_json::to_value(&a).unwrap();
335        assert_eq!(value["mime"], "image/png");
336        assert_eq!(value["data"], "iVBORw0KGgp4eXo=");
337        let back: Attachment = serde_json::from_value(value).unwrap();
338        assert_eq!(back, a);
339        let raw = serde_json::to_vec(&a).unwrap();
340        let back: Attachment = serde_json::from_slice(&raw).unwrap();
341        assert_eq!(back, a);
342    }
343
344    #[test]
345    fn deduplicated_attachment_round_trips_without_data() {
346        let mut a = Attachment::new(b"image".to_vec(), "png", None);
347        a.inline_data = true;
348        a.data = None;
349        let raw = serde_json::to_vec(&a).unwrap();
350        let value: serde_json::Value = serde_json::from_slice(&raw).unwrap();
351        assert!(value.get("data").is_none());
352        assert_eq!(value["mime"], "image/png");
353        let back: Attachment = serde_json::from_slice(&raw).unwrap();
354        assert_eq!(back, a);
355    }
356}