1use std::sync::Arc;
8
9use serde::de::{self, Deserializer, MapAccess, Visitor};
10use serde::ser::{SerializeStruct, Serializer};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, PartialEq, Eq, Default)]
16pub struct Attachment {
17 pub id: String,
18 pub format: String,
20 pub bytes: u64,
22 pub hash: String,
24 pub source: Option<String>,
27 pub data: Option<Arc<[u8]>>,
29 pub inline_data: bool,
31}
32
33impl Attachment {
34 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]), 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 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)]), 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 pub fn mime(&self) -> &'static str {
63 mime_of(&self.format)
64 }
65}
66
67pub 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
88pub 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
154struct 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 let human = s.is_human_readable();
167 let data = self.data.as_deref().filter(|_| !human || self.inline_data);
168 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
244struct 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}