1use crate::{Error, ErrorKind, Question, Result, State};
10use base64::{Engine as _, engine::general_purpose::STANDARD};
11use indexmap::IndexMap;
12use serde_json::Value;
13use std::{
14 fmt,
15 io::Read,
16 path::Path,
17 sync::{
18 Arc,
19 atomic::{AtomicU64, Ordering},
20 },
21};
22
23pub const MIN_SIDE: u32 = 256;
25pub const MAX_SIDE: u32 = 1568;
27pub const MIN_AREA: u64 = 78_400;
29pub const MAX_AREA: u64 = 2_458_624;
31pub const MAX_FILE_BYTES: usize = 16 * 1024 * 1024;
33pub const MAX_REQUEST_BYTES: usize = 32 * 1024 * 1024;
35static NEXT_IMAGE: AtomicU64 = AtomicU64::new(1);
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum ImageFormat {
40 Png,
42 Jpeg,
44}
45impl ImageFormat {
46 pub const fn as_str(self) -> &'static str {
48 match self {
49 Self::Png => "png",
50 Self::Jpeg => "jpeg",
51 }
52 }
53 pub const fn mime_type(self) -> &'static str {
55 match self {
56 Self::Png => "image/png",
57 Self::Jpeg => "image/jpeg",
58 }
59 }
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
64pub enum ImageDiagnostic {
65 ExifOrientation(u16),
67 NonBaselineJpeg,
69}
70
71#[derive(Clone)]
76pub struct Image {
77 data: Arc<[u8]>,
78 format: ImageFormat,
79 width: u32,
80 height: u32,
81 placeholder: String,
82 diagnostics: Vec<ImageDiagnostic>,
83}
84impl fmt::Debug for Image {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.debug_struct("Image")
87 .field("format", &self.format)
88 .field("width", &self.width)
89 .field("height", &self.height)
90 .field("size", &self.size())
91 .field("diagnostics", &self.diagnostics)
92 .finish_non_exhaustive()
93 }
94}
95impl fmt::Display for Image {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 f.write_str(&self.placeholder)
98 }
99}
100impl Image {
101 pub fn from_bytes(data: impl Into<Arc<[u8]>>) -> Result<Self> {
108 let data = data.into();
109 if data.len() > MAX_FILE_BYTES {
110 return Err(invalid("image exceeds 16 MiB per-file limit"));
111 }
112 let (format, width, height, diagnostics) = if data.starts_with(b"\x89PNG\r\n\x1a\n") {
113 let (w, h) = png(&data)?;
114 (ImageFormat::Png, w, h, Vec::new())
115 } else if data.starts_with(b"\xff\xd8\xff") {
116 let (w, h, d) = jpeg(&data)?;
117 (ImageFormat::Jpeg, w, h, d)
118 } else {
119 return Err(invalid("image must be PNG or JPEG"));
120 };
121 if !(MIN_SIDE..=MAX_SIDE).contains(&width) || !(MIN_SIDE..=MAX_SIDE).contains(&height) {
122 return Err(invalid("image sides must be 256-1568 pixels"));
123 }
124 if !(MIN_AREA..=MAX_AREA).contains(&(u64::from(width) * u64::from(height))) {
125 return Err(invalid("image area must be 78400-2458624 pixels"));
126 }
127 #[allow(deprecated)]
129 let id = NEXT_IMAGE
130 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_add(1))
131 .map_err(|_| invalid("image reference identity space exhausted"))?;
132 let placeholder = format!(
133 "image-00000000-0000-0000-{:04x}-{:012x}",
134 id >> 48,
135 id & 0xffff_ffff_ffff
136 );
137 Ok(Self {
138 data,
139 format,
140 width,
141 height,
142 placeholder,
143 diagnostics,
144 })
145 }
146 pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
152 let file = std::fs::File::open(path).map_err(|_| invalid("cannot open image file"))?;
153 let mut data = Vec::new();
154 file.take((MAX_FILE_BYTES + 1) as u64)
155 .read_to_end(&mut data)
156 .map_err(|_| invalid("cannot read image file"))?;
157 Self::from_bytes(data)
158 }
159 pub fn from_base64(text: &str) -> Result<Self> {
165 if text.starts_with("data:") {
166 return Err(invalid("base64 image must not have a data: URL prefix"));
167 }
168 let mut compact = String::new();
169 let max_encoded = MAX_FILE_BYTES.div_ceil(3) * 4;
170 for ch in text.chars().filter(|ch| !ch.is_whitespace()) {
171 if compact.len() >= max_encoded {
172 return Err(invalid("base64 image exceeds 16 MiB limit"));
173 }
174 compact.push(ch);
175 }
176 let bytes = STANDARD
177 .decode(compact)
178 .map_err(|_| invalid("invalid base64 image data"))?;
179 Self::from_bytes(bytes)
180 }
181 pub fn bytes(&self) -> &[u8] {
183 &self.data
184 }
185 pub fn format(&self) -> ImageFormat {
187 self.format
188 }
189 pub fn width(&self) -> u32 {
191 self.width
192 }
193 pub fn height(&self) -> u32 {
195 self.height
196 }
197 pub fn size(&self) -> usize {
199 self.data.len()
200 }
201 pub fn placeholder(&self) -> &str {
203 &self.placeholder
204 }
205 pub fn diagnostics(&self) -> &[ImageDiagnostic] {
207 &self.diagnostics
208 }
209 pub fn base64(&self) -> String {
211 STANDARD.encode(&self.data)
212 }
213}
214
215#[derive(Clone, Default)]
217pub struct Assets {
218 images: IndexMap<String, Image>,
219}
220impl fmt::Debug for Assets {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 f.debug_struct("Assets")
223 .field("image_count", &self.images.len())
224 .finish_non_exhaustive()
225 }
226}
227#[derive(Debug)]
229pub struct ImageReference<'a> {
230 image: &'a Image,
231 uses: usize,
232}
233impl<'a> ImageReference<'a> {
234 pub fn image(&self) -> &'a Image {
236 self.image
237 }
238 pub fn uses(&self) -> usize {
240 self.uses
241 }
242}
243impl Assets {
244 pub fn new() -> Self {
246 Self::default()
247 }
248 pub fn attach(&mut self, image: Image) -> String {
250 let key = image.placeholder.clone();
251 self.images.entry(key.clone()).or_insert(image);
252 key
253 }
254 pub fn len(&self) -> usize {
256 self.images.len()
257 }
258 pub fn is_empty(&self) -> bool {
260 self.images.is_empty()
261 }
262 pub fn validate(&self, state: &State, questions: &IndexMap<String, Question>) -> Result<()> {
268 self.references(state, questions).map(|_| ())
269 }
270 pub fn references<'a>(
279 &'a self,
280 state: &State,
281 questions: &IndexMap<String, Question>,
282 ) -> Result<Vec<ImageReference<'a>>> {
283 let mut found = IndexMap::<String, usize>::new();
284 self.walk(state.as_value(), &mut found)?;
285 for (name, question) in questions {
286 self.text(name, &mut found)?;
287 self.text(question.instructions(), &mut found)?;
288 match question {
289 Question::Boolean(q) => {
290 if let Some(text) = q.true_description() {
291 self.text(text, &mut found)?;
292 }
293 if let Some(text) = q.false_description() {
294 self.text(text, &mut found)?;
295 }
296 }
297 Question::Choice(q) => {
298 for (name, description) in q.options() {
299 self.text(name, &mut found)?;
300 if let Some(text) = description {
301 self.text(text, &mut found)?;
302 }
303 }
304 }
305 Question::Score(q) => {
306 for text in q.levels() {
307 self.text(text, &mut found)?;
308 }
309 }
310 Question::Box(_) => {}
311 }
312 }
313 let mut total = 0usize;
314 let mut result = Vec::new();
315 for (key, uses) in found {
316 let image = self
317 .images
318 .get(&key)
319 .ok_or_else(|| invalid("unknown image reference"))?;
320 total = image
321 .size()
322 .checked_mul(uses)
323 .and_then(|bytes| total.checked_add(bytes))
324 .ok_or_else(|| invalid("image request size overflow"))?;
325 if total > MAX_REQUEST_BYTES {
326 return Err(invalid(
327 "images exceed 32 MiB request limit, counting every use",
328 ));
329 }
330 result.push(ImageReference { image, uses });
331 }
332 Ok(result)
333 }
334 pub fn validate_service_request(
343 &self,
344 state: &State,
345 questions: &IndexMap<String, Question>,
346 ) -> Result<()> {
347 for reference in self.references(state, questions)? {
348 if reference.image.width() < 280 || reference.image.height() < 280 {
349 return Err(invalid("TypeSafe image sides must be at least 280 pixels"));
350 }
351 }
352 let reject_key = |key: &str| -> Result<()> {
353 let mut found = IndexMap::new();
354 self.text(key, &mut found)?;
355 if !found.is_empty() {
356 return Err(invalid(
357 "TypeSafe does not replace image references in object keys",
358 ));
359 }
360 Ok(())
361 };
362 let mut stack = vec![state.as_value()];
363 while let Some(value) = stack.pop() {
364 match value {
365 Value::Object(values) => {
366 for (key, value) in values {
367 reject_key(key)?;
368 stack.push(value);
369 }
370 }
371 Value::Array(values) => stack.extend(values),
372 _ => {}
373 }
374 }
375 for (name, question) in questions {
376 reject_key(name)?;
377 if let Question::Choice(choice) = question {
378 for name in choice.options().keys() {
379 reject_key(name)?;
380 }
381 }
382 }
383 Ok(())
384 }
385 fn walk(&self, value: &Value, found: &mut IndexMap<String, usize>) -> Result<()> {
386 enum Item<'a> {
387 Value(&'a Value),
388 Key(&'a str),
389 }
390 let mut stack = vec![Item::Value(value)];
391 while let Some(item) = stack.pop() {
392 match item {
393 Item::Key(key) => self.text(key, found)?,
394 Item::Value(Value::String(text)) => self.text(text, found)?,
395 Item::Value(Value::Array(values)) => {
396 stack.extend(values.iter().rev().map(Item::Value))
397 }
398 Item::Value(Value::Object(values)) => {
399 for (key, value) in values.iter().rev() {
400 stack.push(Item::Value(value));
401 stack.push(Item::Key(key));
402 }
403 }
404 _ => {}
405 }
406 }
407 Ok(())
408 }
409 fn text(&self, text: &str, found: &mut IndexMap<String, usize>) -> Result<()> {
410 for (start, _) in text.match_indices("image-") {
411 let Some(candidate) = text.get(start..start + 42) else {
412 continue;
413 };
414 if !is_reference(candidate) {
415 continue;
416 }
417 if !self.images.contains_key(candidate) {
418 return Err(invalid(
419 "unknown image reference; attach its image to this request",
420 ));
421 }
422 let count = found.entry(candidate.to_owned()).or_default();
423 *count = count
424 .checked_add(1)
425 .ok_or_else(|| invalid("image reference count overflow"))?;
426 }
427 Ok(())
428 }
429}
430fn is_reference(text: &str) -> bool {
431 text.len() == 42
432 && text.as_bytes()[6..].iter().enumerate().all(|(i, b)| {
433 if [8, 13, 18, 23].contains(&i) {
434 *b == b'-'
435 } else {
436 b.is_ascii_digit() || (b'a'..=b'f').contains(b)
437 }
438 })
439}
440
441pub fn frames(
451 assets: &mut Assets,
452 images: impl IntoIterator<Item = Image>,
453 seconds_ago: Option<&[f64]>,
454) -> Result<State> {
455 let images: Vec<_> = images.into_iter().collect();
456 if images.is_empty() {
457 return Err(invalid("frames needs at least one image"));
458 }
459 if let Some(times) = seconds_ago {
460 if times.len() != images.len() {
461 return Err(invalid("frames and seconds_ago lengths differ"));
462 }
463 if times.iter().any(|t| !t.is_finite()) {
464 return Err(invalid("frame timestamps must be finite"));
465 }
466 }
467 let frames = images
468 .into_iter()
469 .enumerate()
470 .map(|(i, image)| {
471 let mut value = serde_json::Map::new();
472 if let Some(times) = seconds_ago {
473 value.insert("seconds_ago".into(), Value::from(times[i]));
474 }
475 value.insert("frame".into(), Value::String(assets.attach(image)));
476 Value::Object(value)
477 })
478 .collect();
479 State::try_from(Value::Array(frames))
480}
481fn invalid(message: &str) -> Error {
482 Error::new(ErrorKind::InvalidImage, message)
483}
484fn be32(data: &[u8]) -> Option<u32> {
485 Some(u32::from_be_bytes(data.get(..4)?.try_into().ok()?))
486}
487fn be16(data: &[u8]) -> Option<u16> {
488 Some(u16::from_be_bytes(data.get(..2)?.try_into().ok()?))
489}
490fn png(data: &[u8]) -> Result<(u32, u32)> {
491 if data.len() < 33
492 || data.get(12..16) != Some(b"IHDR".as_slice())
493 || be32(&data[8..]) != Some(13)
494 {
495 return Err(invalid("malformed PNG: missing or invalid IHDR"));
496 }
497 let width = be32(&data[16..]).ok_or_else(|| invalid("malformed PNG width"))?;
498 let height = be32(&data[20..]).ok_or_else(|| invalid("malformed PNG height"))?;
499 let mut pos = 8usize;
500 while pos < data.len() {
501 let header = data
502 .get(pos..pos.saturating_add(8))
503 .ok_or_else(|| invalid("malformed PNG chunk header"))?;
504 let length = be32(header).ok_or_else(|| invalid("malformed PNG chunk size"))? as usize;
505 let end = pos
506 .checked_add(12)
507 .and_then(|p| p.checked_add(length))
508 .ok_or_else(|| invalid("malformed PNG chunk overflow"))?;
509 if end > data.len() {
510 return Err(invalid("malformed PNG: truncated chunk"));
511 }
512 let kind = &header[4..8];
513 if kind == b"acTL" {
514 return Err(invalid("animated PNGs are not accepted"));
515 }
516 if kind == b"IDAT" || kind == b"IEND" {
517 return Ok((width, height));
518 }
519 pos = end;
520 }
521 Err(invalid("malformed PNG: missing image data"))
522}
523fn jpeg(data: &[u8]) -> Result<(u32, u32, Vec<ImageDiagnostic>)> {
524 let mut pos = 2usize;
525 let mut diagnostics = Vec::new();
526 while let Some(header) = data.get(pos..pos.saturating_add(2)) {
527 if header[0] != 0xff {
528 return Err(invalid("malformed JPEG: bad marker"));
529 }
530 let marker = header[1];
531 if marker == 0xff {
532 pos += 1;
533 continue;
534 }
535 if matches!(marker, 0xd8 | 0x01 | 0xd0..=0xd7) {
536 pos += 2;
537 continue;
538 }
539 if matches!(marker, 0xd9 | 0xda) {
540 break;
541 }
542 let length = data
543 .get(pos + 2..)
544 .and_then(be16)
545 .ok_or_else(|| invalid("malformed JPEG segment length"))? as usize;
546 if length < 2 {
547 return Err(invalid("malformed JPEG segment length"));
548 }
549 let end = pos
550 .checked_add(2)
551 .and_then(|p| p.checked_add(length))
552 .ok_or_else(|| invalid("malformed JPEG segment overflow"))?;
553 let segment = data
554 .get(pos + 4..end)
555 .ok_or_else(|| invalid("malformed JPEG: truncated segment"))?;
556 if marker == 0xe1 && segment.starts_with(b"Exif\0\0") {
557 if let Some(orientation) = exif_orientation(&segment[6..]) {
558 if orientation != 1 {
559 diagnostics.push(ImageDiagnostic::ExifOrientation(orientation));
560 }
561 }
562 }
563 if matches!(marker,0xc0..=0xc3|0xc5..=0xc7|0xc9..=0xcb|0xcd..=0xcf) {
564 let height = segment
565 .get(1..)
566 .and_then(be16)
567 .ok_or_else(|| invalid("malformed JPEG frame header"))?;
568 let width = segment
569 .get(3..)
570 .and_then(be16)
571 .ok_or_else(|| invalid("malformed JPEG frame header"))?;
572 if !matches!(marker, 0xc0 | 0xc1) {
573 diagnostics.push(ImageDiagnostic::NonBaselineJpeg);
574 }
575 return Ok((u32::from(width), u32::from(height), diagnostics));
576 }
577 pos = end;
578 }
579 Err(invalid("malformed JPEG: no frame header with size"))
580}
581fn exif_orientation(tiff: &[u8]) -> Option<u16> {
582 let little = match tiff.get(..2)? {
583 b"II" => true,
584 b"MM" => false,
585 _ => return None,
586 };
587 let u16_at = |pos: usize| -> Option<u16> {
588 let bytes = tiff.get(pos..pos.checked_add(2)?)?.try_into().ok()?;
589 Some(if little {
590 u16::from_le_bytes(bytes)
591 } else {
592 u16::from_be_bytes(bytes)
593 })
594 };
595 let bytes = tiff.get(4..8)?.try_into().ok()?;
596 let offset = if little {
597 u32::from_le_bytes(bytes)
598 } else {
599 u32::from_be_bytes(bytes)
600 } as usize;
601 let count = usize::from(u16_at(offset)?);
602 for i in 0..count {
603 let pos = offset.checked_add(2)?.checked_add(i.checked_mul(12)?)?;
604 if u16_at(pos)? == 0x0112 {
605 return u16_at(pos.checked_add(8)?);
606 }
607 }
608 None
609}