1use std::collections::BTreeMap;
5use std::io::Cursor;
6
7use crate::uri::validate_absolute_uri_identity;
8use crate::{
9 PersonalOriginalIdentity, ReadablePersonalOriginalIdentity, Result, SharedOriginalMetadata,
10 SharedRelativeOriginalUri, SharedRepositoryContext, ThumbnailError, ThumbnailSize,
11 UnixMtimeSeconds,
12};
13
14const MAX_RENDERED_PIXELS: u64 = 16_777_216;
15const MAX_RENDERED_DECODE_BYTES: usize = 256 * 1024 * 1024;
16
17#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
19#[non_exhaustive]
20pub enum RawThumbnailPixelFormat {
21 Rgb8,
23 Rgba8,
25}
26
27impl RawThumbnailPixelFormat {
28 const fn channels(self) -> usize {
29 match self {
30 Self::Rgb8 => 3,
31 Self::Rgba8 => 4,
32 }
33 }
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct RawThumbnailImage<'a> {
42 width: u32,
43 height: u32,
44 stride: usize,
45 format: RawThumbnailPixelFormat,
46 pixels: &'a [u8],
47}
48
49impl<'a> RawThumbnailImage<'a> {
50 pub fn new(
57 width: u32,
58 height: u32,
59 stride: usize,
60 format: RawThumbnailPixelFormat,
61 pixels: &'a [u8],
62 ) -> Result<Self> {
63 validate_raw_thumbnail_image(width, height, stride, format, pixels)?;
64 Ok(Self {
65 width,
66 height,
67 stride,
68 format,
69 pixels,
70 })
71 }
72
73 #[must_use]
75 pub const fn width(&self) -> u32 {
76 self.width
77 }
78
79 #[must_use]
81 pub const fn height(&self) -> u32 {
82 self.height
83 }
84
85 #[must_use]
87 pub const fn stride(&self) -> usize {
88 self.stride
89 }
90
91 #[must_use]
93 pub const fn format(&self) -> RawThumbnailPixelFormat {
94 self.format
95 }
96
97 #[must_use]
99 pub const fn pixels(&self) -> &'a [u8] {
100 self.pixels
101 }
102}
103
104#[derive(Debug, Eq, PartialEq)]
106pub struct OwnedRawThumbnailImage {
107 width: u32,
108 height: u32,
109 stride: usize,
110 format: RawThumbnailPixelFormat,
111 pixels: Vec<u8>,
112}
113
114impl OwnedRawThumbnailImage {
115 pub fn new(
122 width: u32,
123 height: u32,
124 stride: usize,
125 format: RawThumbnailPixelFormat,
126 pixels: Vec<u8>,
127 ) -> Result<Self> {
128 validate_raw_thumbnail_image(width, height, stride, format, &pixels)?;
129 Ok(Self {
130 width,
131 height,
132 stride,
133 format,
134 pixels,
135 })
136 }
137
138 #[must_use]
140 pub fn as_borrowed(&self) -> RawThumbnailImage<'_> {
141 RawThumbnailImage {
142 width: self.width,
143 height: self.height,
144 stride: self.stride,
145 format: self.format,
146 pixels: &self.pixels,
147 }
148 }
149
150 #[must_use]
152 pub const fn width(&self) -> u32 {
153 self.width
154 }
155
156 #[must_use]
158 pub const fn height(&self) -> u32 {
159 self.height
160 }
161
162 #[must_use]
164 pub const fn stride(&self) -> usize {
165 self.stride
166 }
167
168 #[must_use]
170 pub const fn format(&self) -> RawThumbnailPixelFormat {
171 self.format
172 }
173
174 #[must_use]
176 pub fn pixels(&self) -> &[u8] {
177 &self.pixels
178 }
179
180 #[must_use]
182 pub fn into_parts(self) -> OwnedRawThumbnailImageParts {
183 OwnedRawThumbnailImageParts {
184 width: self.width,
185 height: self.height,
186 stride: self.stride,
187 format: self.format,
188 pixels: self.pixels,
189 }
190 }
191}
192
193#[derive(Debug, Eq, PartialEq)]
195#[non_exhaustive]
196pub struct OwnedRawThumbnailImageParts {
197 pub width: u32,
199 pub height: u32,
201 pub stride: usize,
203 pub format: RawThumbnailPixelFormat,
205 pub pixels: Vec<u8>,
207}
208
209#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
216#[non_exhaustive]
217pub enum CacheEntryProblem {
218 Metadata(ThumbnailMetadataProblem),
220 UnreadableEntry,
222 UnverifiableOriginal,
224 InvalidPngStructure,
226 NonconformingPngFormat,
228 DimensionsExceedNamespace,
230 ResourceLimitExceeded,
232 NonstandardFilename,
234 UriFilenameMismatch,
236}
237
238#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
240#[non_exhaustive]
241pub enum ThumbnailMetadataKey {
242 Uri,
244 Mtime,
246 Size,
248 MimeType,
250}
251
252#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
254#[non_exhaustive]
255pub enum ThumbnailMetadataProblemKind {
256 MissingRequired,
258 InvalidSyntax,
260 ValueMismatch,
262}
263
264#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
266pub struct ThumbnailMetadataProblem {
267 key: ThumbnailMetadataKey,
268 kind: ThumbnailMetadataProblemKind,
269}
270
271impl ThumbnailMetadataProblem {
272 #[must_use]
274 pub const fn new(key: ThumbnailMetadataKey, kind: ThumbnailMetadataProblemKind) -> Self {
275 Self { key, kind }
276 }
277
278 #[must_use]
280 pub const fn key(self) -> ThumbnailMetadataKey {
281 self.key
282 }
283
284 #[must_use]
286 pub const fn kind(self) -> ThumbnailMetadataProblemKind {
287 self.kind
288 }
289}
290
291#[derive(Clone, Debug, Eq, PartialEq)]
293#[non_exhaustive]
294pub enum PersonalValidationOutcome {
295 FullyVerified,
297 Invalid(Vec<CacheEntryProblem>),
299}
300
301#[derive(Clone, Debug, Eq, PartialEq)]
303#[non_exhaustive]
304pub enum SharedValidationOutcome {
305 FullyVerified,
307 MetadataIncomplete,
309 Invalid(Vec<CacheEntryProblem>),
311}
312
313#[derive(Clone, Debug, Default, Eq, PartialEq)]
315pub struct ThumbnailMetadata {
316 values: BTreeMap<String, String>,
317}
318
319impl ThumbnailMetadata {
320 #[must_use]
322 pub fn get(&self, key: &str) -> Option<&str> {
323 self.values.get(key).map(String::as_str)
324 }
325
326 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> + '_ {
328 self.values
329 .iter()
330 .map(|(key, value)| (key.as_str(), value.as_str()))
331 }
332
333 #[must_use]
335 pub fn thumb_uri(&self) -> Option<&str> {
336 self.get("Thumb::URI")
337 }
338
339 #[must_use]
341 pub fn thumb_mtime_lossy(&self) -> Option<UnixMtimeSeconds> {
342 self.try_thumb_mtime().ok().flatten()
343 }
344
345 pub fn try_thumb_mtime(&self) -> Result<Option<UnixMtimeSeconds>> {
352 self.get("Thumb::MTime").map(parse_thumb_mtime).transpose()
353 }
354
355 pub(crate) fn thumb_mtime_result(&self) -> Result<Option<UnixMtimeSeconds>> {
356 self.try_thumb_mtime()
357 }
358
359 #[must_use]
361 pub fn thumb_size_lossy(&self) -> Option<u64> {
362 self.try_thumb_size().ok().flatten()
363 }
364
365 pub fn try_thumb_size(&self) -> Result<Option<u64>> {
371 self.get("Thumb::Size").map(parse_thumb_size).transpose()
372 }
373
374 pub(crate) fn thumb_size_result(&self) -> Result<Option<u64>> {
375 self.try_thumb_size()
376 }
377
378 #[must_use]
380 pub fn thumb_mime_type(&self) -> Option<&str> {
381 self.get("Thumb::Mimetype")
382 }
383}
384
385fn parse_thumb_mtime(value: &str) -> Result<UnixMtimeSeconds> {
386 value
387 .parse::<u64>()
388 .map(UnixMtimeSeconds::new)
389 .map_err(|_| ThumbnailError::invalid_metadata("invalid Thumb::MTime"))
390}
391
392fn parse_thumb_size(value: &str) -> Result<u64> {
393 value
394 .parse::<u64>()
395 .map_err(|_| ThumbnailError::invalid_metadata("invalid Thumb::Size"))
396}
397
398#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
400#[non_exhaustive]
401pub enum ThumbnailPngBitDepth {
402 One,
404 Two,
406 Four,
408 Eight,
410 Sixteen,
412}
413
414impl ThumbnailPngBitDepth {
415 fn from_png(value: png::BitDepth) -> Self {
416 match value {
417 png::BitDepth::One => Self::One,
418 png::BitDepth::Two => Self::Two,
419 png::BitDepth::Four => Self::Four,
420 png::BitDepth::Eight => Self::Eight,
421 png::BitDepth::Sixteen => Self::Sixteen,
422 }
423 }
424}
425
426#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
428#[non_exhaustive]
429pub enum ThumbnailPngColorType {
430 Grayscale,
432 Rgb,
434 Indexed,
436 GrayscaleAlpha,
438 Rgba,
440}
441
442impl ThumbnailPngColorType {
443 fn from_png(value: png::ColorType) -> Self {
444 match value {
445 png::ColorType::Grayscale => Self::Grayscale,
446 png::ColorType::Rgb => Self::Rgb,
447 png::ColorType::Indexed => Self::Indexed,
448 png::ColorType::GrayscaleAlpha => Self::GrayscaleAlpha,
449 png::ColorType::Rgba => Self::Rgba,
450 }
451 }
452}
453
454#[derive(Clone, Debug, Eq, PartialEq)]
456pub struct ParsedThumbnailPng {
457 width: u32,
458 height: u32,
459 bit_depth: ThumbnailPngBitDepth,
460 color_type: ThumbnailPngColorType,
461 interlaced: bool,
462 metadata: ThumbnailMetadata,
463}
464
465impl ParsedThumbnailPng {
466 pub fn parse(bytes: &[u8]) -> Result<Self> {
473 let decoder = png::Decoder::new(Cursor::new(bytes));
474 let mut reader = decoder
475 .read_info()
476 .map_err(|err| ThumbnailError::png(err.to_string()))?;
477 let Some(output_buffer_size) = reader.output_buffer_size() else {
478 return Err(ThumbnailError::png(
479 "png output buffer size is unavailable".to_owned(),
480 ));
481 };
482 let info = reader.info();
483 ensure_parsed_png_resource_limits(info.width, info.height, output_buffer_size)?;
484 let mut buffer = vec![0; output_buffer_size];
485 reader
486 .next_frame(&mut buffer)
487 .map_err(|err| ThumbnailError::png(err.to_string()))?;
488
489 let info = reader.info();
490 let mut values = BTreeMap::new();
491 for chunk in &info.uncompressed_latin1_text {
492 values.insert(chunk.keyword.clone(), chunk.text.clone());
493 }
494 for chunk in &info.compressed_latin1_text {
495 let text = chunk
496 .get_text()
497 .map_err(|err| ThumbnailError::png(err.to_string()))?;
498 values.insert(chunk.keyword.clone(), text);
499 }
500 for chunk in &info.utf8_text {
501 let text = chunk
502 .get_text()
503 .map_err(|err| ThumbnailError::png(err.to_string()))?;
504 values.insert(chunk.keyword.clone(), text);
505 }
506
507 Ok(Self {
508 width: info.width,
509 height: info.height,
510 bit_depth: ThumbnailPngBitDepth::from_png(info.bit_depth),
511 color_type: ThumbnailPngColorType::from_png(info.color_type),
512 interlaced: info.interlaced,
513 metadata: ThumbnailMetadata { values },
514 })
515 }
516
517 #[must_use]
519 pub const fn width(&self) -> u32 {
520 self.width
521 }
522
523 #[must_use]
525 pub const fn height(&self) -> u32 {
526 self.height
527 }
528
529 #[must_use]
531 pub const fn bit_depth(&self) -> ThumbnailPngBitDepth {
532 self.bit_depth
533 }
534
535 #[must_use]
537 pub const fn color_type(&self) -> ThumbnailPngColorType {
538 self.color_type
539 }
540
541 #[must_use]
543 pub const fn interlaced(&self) -> bool {
544 self.interlaced
545 }
546
547 #[must_use]
549 pub const fn metadata(&self) -> &ThumbnailMetadata {
550 &self.metadata
551 }
552
553 pub(crate) fn into_metadata(self) -> ThumbnailMetadata {
554 self.metadata
555 }
556
557 #[must_use]
559 pub fn into_parts(self) -> ParsedThumbnailPngParts {
560 ParsedThumbnailPngParts {
561 width: self.width,
562 height: self.height,
563 bit_depth: self.bit_depth,
564 color_type: self.color_type,
565 interlaced: self.interlaced,
566 metadata: self.metadata,
567 }
568 }
569
570 pub(crate) fn conformance_problems(&self, size: ThumbnailSize) -> Vec<CacheEntryProblem> {
571 let mut problems = Vec::new();
572 if self.bit_depth != ThumbnailPngBitDepth::Eight
573 || self.interlaced
574 || !matches!(
575 self.color_type,
576 ThumbnailPngColorType::Rgba | ThumbnailPngColorType::GrayscaleAlpha
577 )
578 {
579 push_problem(&mut problems, CacheEntryProblem::NonconformingPngFormat);
580 }
581 if self.width > size.max_dimension() || self.height > size.max_dimension() {
582 push_problem(&mut problems, CacheEntryProblem::DimensionsExceedNamespace);
583 }
584 problems
585 }
586}
587
588#[derive(Clone, Debug, Eq, PartialEq)]
590#[non_exhaustive]
591pub struct ParsedThumbnailPngParts {
592 pub width: u32,
594 pub height: u32,
596 pub bit_depth: ThumbnailPngBitDepth,
598 pub color_type: ThumbnailPngColorType,
600 pub interlaced: bool,
602 pub metadata: ThumbnailMetadata,
604}
605
606fn ensure_parsed_png_resource_limits(
607 width: u32,
608 height: u32,
609 output_buffer_size: usize,
610) -> Result<()> {
611 let pixels = u64::from(width) * u64::from(height);
612 if pixels > MAX_RENDERED_PIXELS || output_buffer_size > MAX_RENDERED_DECODE_BYTES {
613 return Err(ThumbnailError::resource_limit_exceeded(
614 "PNG decode resource limit exceeded",
615 ));
616 }
617 Ok(())
618}
619
620fn parse_thumbnail_for_validation(
621 bytes: &[u8],
622) -> std::result::Result<ParsedThumbnailPng, CacheEntryProblem> {
623 match ParsedThumbnailPng::parse(bytes) {
624 Ok(parsed) => Ok(parsed),
625 Err(ThumbnailError::ResourceLimitExceeded { .. }) => {
626 Err(CacheEntryProblem::ResourceLimitExceeded)
627 }
628 Err(_) => Err(CacheEntryProblem::InvalidPngStructure),
629 }
630}
631
632#[must_use]
634pub fn validate_personal_thumbnail(
635 bytes: &[u8],
636 original: &ReadablePersonalOriginalIdentity,
637 size: ThumbnailSize,
638) -> PersonalValidationOutcome {
639 validate_personal_thumbnail_identity(bytes, original.identity(), size)
640}
641
642pub(crate) fn validate_personal_thumbnail_identity(
643 bytes: &[u8],
644 original: &PersonalOriginalIdentity,
645 size: ThumbnailSize,
646) -> PersonalValidationOutcome {
647 let parsed = match parse_thumbnail_for_validation(bytes) {
648 Ok(parsed) => parsed,
649 Err(problem) => {
650 return PersonalValidationOutcome::Invalid(vec![problem]);
651 }
652 };
653
654 let mut problems = parsed.conformance_problems(size);
655 compare_personal_metadata(&mut problems, parsed.metadata(), original);
656
657 if problems.is_empty() {
658 PersonalValidationOutcome::FullyVerified
659 } else {
660 PersonalValidationOutcome::Invalid(problems)
661 }
662}
663
664#[must_use]
670pub fn validate_personal_failure_entry(
671 bytes: &[u8],
672 original: &ReadablePersonalOriginalIdentity,
673) -> PersonalValidationOutcome {
674 validate_personal_failure_entry_identity(bytes, original.identity())
675}
676
677pub(crate) fn validate_personal_failure_entry_identity(
678 bytes: &[u8],
679 original: &PersonalOriginalIdentity,
680) -> PersonalValidationOutcome {
681 let parsed = match parse_thumbnail_for_validation(bytes) {
682 Ok(parsed) => parsed,
683 Err(problem) => {
684 return PersonalValidationOutcome::Invalid(vec![problem]);
685 }
686 };
687
688 let mut problems = Vec::new();
689 compare_personal_metadata(&mut problems, parsed.metadata(), original);
690
691 if problems.is_empty() {
692 PersonalValidationOutcome::FullyVerified
693 } else {
694 PersonalValidationOutcome::Invalid(problems)
695 }
696}
697
698#[must_use]
701pub fn validate_shared_thumbnail(
702 bytes: &[u8],
703 context: &SharedRepositoryContext,
704 original: SharedOriginalMetadata,
705 size: ThumbnailSize,
706) -> SharedValidationOutcome {
707 let parsed = match parse_thumbnail_for_validation(bytes) {
708 Ok(parsed) => parsed,
709 Err(problem) => {
710 return SharedValidationOutcome::Invalid(vec![problem]);
711 }
712 };
713
714 let mut problems = parsed.conformance_problems(size);
715 let mut incomplete = false;
716 let metadata = parsed.metadata();
717
718 match metadata.thumb_uri() {
719 Some(uri) if uri == context.shared_uri().as_str() => {}
720 Some(uri) if SharedRelativeOriginalUri::parse(uri).is_err() => {
721 push_problem(
722 &mut problems,
723 metadata_problem(
724 ThumbnailMetadataKey::Uri,
725 ThumbnailMetadataProblemKind::InvalidSyntax,
726 ),
727 );
728 }
729 Some(_) => push_problem(
730 &mut problems,
731 metadata_problem(
732 ThumbnailMetadataKey::Uri,
733 ThumbnailMetadataProblemKind::ValueMismatch,
734 ),
735 ),
736 None => incomplete = true,
737 }
738
739 match (metadata.thumb_mtime_result(), original.mtime()) {
740 (Ok(Some(stored)), Some(expected)) if stored == expected => {}
741 (Ok(Some(_)), Some(_)) => push_problem(
742 &mut problems,
743 metadata_problem(
744 ThumbnailMetadataKey::Mtime,
745 ThumbnailMetadataProblemKind::ValueMismatch,
746 ),
747 ),
748 (Ok(Some(_)), None) => push_problem(&mut problems, CacheEntryProblem::UnverifiableOriginal),
749 (Ok(None), _) => incomplete = true,
750 (Err(_), _) => push_problem(
751 &mut problems,
752 metadata_problem(
753 ThumbnailMetadataKey::Mtime,
754 ThumbnailMetadataProblemKind::InvalidSyntax,
755 ),
756 ),
757 }
758
759 compare_optional_size(
760 &mut problems,
761 metadata,
762 original.original_byte_size(),
763 false,
764 );
765
766 if !problems.is_empty() {
767 SharedValidationOutcome::Invalid(problems)
768 } else if incomplete {
769 SharedValidationOutcome::MetadataIncomplete
770 } else {
771 SharedValidationOutcome::FullyVerified
772 }
773}
774
775fn compare_personal_metadata(
776 problems: &mut Vec<CacheEntryProblem>,
777 metadata: &ThumbnailMetadata,
778 original: &PersonalOriginalIdentity,
779) {
780 match metadata.thumb_uri() {
781 Some(uri) if uri == original.uri().as_str() => {}
782 Some(uri) if validate_absolute_uri_identity(uri).is_err() => {
783 push_problem(
784 problems,
785 metadata_problem(
786 ThumbnailMetadataKey::Uri,
787 ThumbnailMetadataProblemKind::InvalidSyntax,
788 ),
789 );
790 }
791 Some(_) => push_problem(
792 problems,
793 metadata_problem(
794 ThumbnailMetadataKey::Uri,
795 ThumbnailMetadataProblemKind::ValueMismatch,
796 ),
797 ),
798 None => push_problem(
799 problems,
800 metadata_problem(
801 ThumbnailMetadataKey::Uri,
802 ThumbnailMetadataProblemKind::MissingRequired,
803 ),
804 ),
805 }
806
807 match metadata.thumb_mtime_result() {
808 Ok(Some(mtime)) if mtime == original.mtime() => {}
809 Ok(Some(_)) => push_problem(
810 problems,
811 metadata_problem(
812 ThumbnailMetadataKey::Mtime,
813 ThumbnailMetadataProblemKind::ValueMismatch,
814 ),
815 ),
816 Ok(None) => push_problem(
817 problems,
818 metadata_problem(
819 ThumbnailMetadataKey::Mtime,
820 ThumbnailMetadataProblemKind::MissingRequired,
821 ),
822 ),
823 Err(_) => push_problem(
824 problems,
825 metadata_problem(
826 ThumbnailMetadataKey::Mtime,
827 ThumbnailMetadataProblemKind::InvalidSyntax,
828 ),
829 ),
830 }
831
832 compare_optional_size(problems, metadata, original.original_byte_size(), false);
833}
834
835fn compare_optional_size(
836 problems: &mut Vec<CacheEntryProblem>,
837 metadata: &ThumbnailMetadata,
838 expected: Option<u64>,
839 missing_is_problem: bool,
840) {
841 match (metadata.thumb_size_result(), expected) {
842 (Ok(Some(stored)), Some(expected)) if stored == expected => {}
843 (Ok(Some(_)), Some(_)) => push_problem(
844 problems,
845 metadata_problem(
846 ThumbnailMetadataKey::Size,
847 ThumbnailMetadataProblemKind::ValueMismatch,
848 ),
849 ),
850 (Ok(None), Some(_)) if missing_is_problem => push_problem(
851 problems,
852 metadata_problem(
853 ThumbnailMetadataKey::Size,
854 ThumbnailMetadataProblemKind::MissingRequired,
855 ),
856 ),
857 (Ok(_), _) => {}
858 (Err(_), _) => push_problem(
859 problems,
860 metadata_problem(
861 ThumbnailMetadataKey::Size,
862 ThumbnailMetadataProblemKind::InvalidSyntax,
863 ),
864 ),
865 }
866}
867
868pub(crate) const fn metadata_problem(
869 key: ThumbnailMetadataKey,
870 kind: ThumbnailMetadataProblemKind,
871) -> CacheEntryProblem {
872 CacheEntryProblem::Metadata(ThumbnailMetadataProblem::new(key, kind))
873}
874
875pub(crate) fn push_problem(problems: &mut Vec<CacheEntryProblem>, problem: CacheEntryProblem) {
876 if !problems.contains(&problem) {
877 problems.push(problem);
878 }
879}
880
881struct RgbaImage {
882 width: u32,
883 height: u32,
884 pixels: Vec<u8>,
885}
886
887pub(crate) struct DecodedThumbnailRgba8 {
888 pub(crate) width: u32,
889 pub(crate) height: u32,
890 pub(crate) stride: usize,
891 pub(crate) pixels: Vec<u8>,
892}
893
894pub(crate) fn normalized_personal_thumbnail_png(
895 rendered_png: &[u8],
896 original: &PersonalOriginalIdentity,
897 size: ThumbnailSize,
898) -> Result<Vec<u8>> {
899 let image = decode_rendered_png_to_rgba8(rendered_png)?;
900 normalized_personal_thumbnail_rgba_png(image, original, size)
901}
902
903pub(crate) fn normalized_personal_thumbnail_raw_png(
904 image: RawThumbnailImage<'_>,
905 original: &PersonalOriginalIdentity,
906 size: ThumbnailSize,
907) -> Result<Vec<u8>> {
908 let image = raw_thumbnail_to_rgba8(image)?;
909 normalized_personal_thumbnail_rgba_png(image, original, size)
910}
911
912pub(crate) fn normalized_personal_thumbnail_from_cache_png(
913 thumbnail_png: &[u8],
914 original: &PersonalOriginalIdentity,
915 size: ThumbnailSize,
916) -> Result<Vec<u8>> {
917 let image = decode_validated_thumbnail_png_to_rgba_image(thumbnail_png)?;
918 normalized_personal_thumbnail_rgba_png(image, original, size)
919}
920
921pub(crate) fn downscaled_validated_thumbnail_png_to_rgba8(
922 thumbnail_png: &[u8],
923 size: ThumbnailSize,
924) -> Result<DecodedThumbnailRgba8> {
925 let image = decode_validated_thumbnail_png_to_rgba_image(thumbnail_png)?;
926 let image = downscale_to_namespace(image, size)?;
927 Ok(DecodedThumbnailRgba8 {
928 width: image.width,
929 height: image.height,
930 stride: usize::try_from(image.width)
931 .ok()
932 .and_then(|width| width.checked_mul(4))
933 .ok_or_else(|| {
934 ThumbnailError::resource_limit_exceeded("PNG decode resource limit exceeded")
935 })?,
936 pixels: image.pixels,
937 })
938}
939
940fn normalized_personal_thumbnail_rgba_png(
941 image: RgbaImage,
942 original: &PersonalOriginalIdentity,
943 size: ThumbnailSize,
944) -> Result<Vec<u8>> {
945 let image = downscale_to_namespace(image, size)?;
946 let metadata = thumbnail_metadata_pairs(original);
947 let png = encode_rgba_png(image.width, image.height, &image.pixels, &metadata)?;
948 match validate_personal_thumbnail_identity(&png, original, size) {
949 PersonalValidationOutcome::FullyVerified => Ok(png),
950 PersonalValidationOutcome::Invalid(problems) => {
951 Err(ThumbnailError::unsupported_rendered_thumbnail(
952 rendered_validation_error(problems.as_slice()),
953 ))
954 }
955 }
956}
957
958fn rendered_validation_error(problems: &[CacheEntryProblem]) -> &'static str {
959 if problems.contains(&CacheEntryProblem::ResourceLimitExceeded) {
960 "resource limit exceeded"
961 } else if problems.contains(&CacheEntryProblem::DimensionsExceedNamespace) {
962 "dimensions exceed namespace"
963 } else if problems.contains(&CacheEntryProblem::NonconformingPngFormat) {
964 "nonconforming final PNG"
965 } else if problems.contains(&CacheEntryProblem::InvalidPngStructure) {
966 "invalid final PNG structure"
967 } else {
968 "metadata validation failed"
969 }
970}
971
972pub(crate) fn thumbnail_metadata_pairs(
973 original: &PersonalOriginalIdentity,
974) -> Vec<(String, String)> {
975 let mut metadata = vec![
976 ("Thumb::URI".to_owned(), original.uri().as_str().to_owned()),
977 ("Thumb::MTime".to_owned(), original.mtime().to_string()),
978 ];
979 if let Some(original_byte_size) = original.original_byte_size() {
980 metadata.push(("Thumb::Size".to_owned(), original_byte_size.to_string()));
981 }
982 if let Some(mime_type) = original.mime_type() {
983 metadata.push(("Thumb::Mimetype".to_owned(), mime_type.to_owned()));
984 }
985 metadata
986}
987
988fn ensure_rendered_resource_limits(
989 width: u32,
990 height: u32,
991 output_buffer_size: usize,
992) -> Result<()> {
993 let pixels = u64::from(width) * u64::from(height);
994 if pixels > MAX_RENDERED_PIXELS || output_buffer_size > MAX_RENDERED_DECODE_BYTES {
995 return Err(ThumbnailError::unsupported_rendered_thumbnail(
996 "rendered PNG resource limit exceeded",
997 ));
998 }
999 Ok(())
1000}
1001
1002fn validate_raw_thumbnail_image(
1003 width: u32,
1004 height: u32,
1005 stride: usize,
1006 format: RawThumbnailPixelFormat,
1007 pixels: &[u8],
1008) -> Result<()> {
1009 if width == 0 || height == 0 {
1010 return Err(ThumbnailError::unsupported_rendered_thumbnail(
1011 "raw thumbnail dimensions must be non-zero",
1012 ));
1013 }
1014
1015 let row_bytes = raw_row_bytes(width, format)?;
1016 let decoded_len = rgba_buffer_len(width, height)?;
1017 ensure_raw_resource_limits(width, height, decoded_len)?;
1018
1019 if stride < row_bytes {
1020 return Err(ThumbnailError::unsupported_rendered_thumbnail(
1021 "raw thumbnail stride is too small",
1022 ));
1023 }
1024
1025 let required_len = raw_required_buffer_len(height, stride, row_bytes)?;
1026 if pixels.len() < required_len {
1027 return Err(ThumbnailError::unsupported_rendered_thumbnail(
1028 "raw thumbnail buffer is too short",
1029 ));
1030 }
1031
1032 Ok(())
1033}
1034
1035fn ensure_raw_resource_limits(width: u32, height: u32, output_buffer_size: usize) -> Result<()> {
1036 ensure_rendered_resource_limits(width, height, output_buffer_size).map_err(
1037 |error| match error {
1038 ThumbnailError::UnsupportedRenderedThumbnail { .. } => {
1039 ThumbnailError::unsupported_rendered_thumbnail(
1040 "raw thumbnail resource limit exceeded",
1041 )
1042 }
1043 error => error,
1044 },
1045 )
1046}
1047
1048fn raw_row_bytes(width: u32, format: RawThumbnailPixelFormat) -> Result<usize> {
1049 usize::try_from(width)
1050 .map_err(|_| {
1051 ThumbnailError::unsupported_rendered_thumbnail("raw thumbnail width overflows usize")
1052 })?
1053 .checked_mul(format.channels())
1054 .ok_or_else(|| {
1055 ThumbnailError::unsupported_rendered_thumbnail(
1056 "raw thumbnail row length overflows usize",
1057 )
1058 })
1059}
1060
1061fn raw_required_buffer_len(height: u32, stride: usize, row_bytes: usize) -> Result<usize> {
1062 let height = usize::try_from(height).map_err(|_| {
1063 ThumbnailError::unsupported_rendered_thumbnail("raw thumbnail height overflows usize")
1064 })?;
1065 stride
1066 .checked_mul(height.saturating_sub(1))
1067 .and_then(|bytes_before_last_row| bytes_before_last_row.checked_add(row_bytes))
1068 .ok_or_else(|| {
1069 ThumbnailError::unsupported_rendered_thumbnail(
1070 "raw thumbnail buffer length overflows usize",
1071 )
1072 })
1073}
1074
1075fn pixel_count_len(width: u32, height: u32) -> Result<usize> {
1076 let pixels = u64::from(width) * u64::from(height);
1077 usize::try_from(pixels).map_err(|_| {
1078 ThumbnailError::unsupported_rendered_thumbnail("rendered PNG dimensions are too large")
1079 })
1080}
1081
1082fn rgba_buffer_len(width: u32, height: u32) -> Result<usize> {
1083 pixel_count_len(width, height)?
1084 .checked_mul(4)
1085 .ok_or_else(|| {
1086 ThumbnailError::unsupported_rendered_thumbnail("RGBA buffer length overflows usize")
1087 })
1088}
1089
1090fn validated_lookup_rgba_buffer_len(width: u32, height: u32) -> Result<usize> {
1091 let pixels = u64::from(width) * u64::from(height);
1092 let len = usize::try_from(pixels)
1093 .ok()
1094 .and_then(|pixels| pixels.checked_mul(4))
1095 .ok_or_else(|| {
1096 ThumbnailError::resource_limit_exceeded("PNG decode resource limit exceeded")
1097 })?;
1098 if len > MAX_RENDERED_DECODE_BYTES {
1099 return Err(ThumbnailError::resource_limit_exceeded(
1100 "PNG decode resource limit exceeded",
1101 ));
1102 }
1103 Ok(len)
1104}
1105
1106pub(crate) fn decode_validated_thumbnail_png_to_rgba8(
1107 bytes: &[u8],
1108) -> Result<DecodedThumbnailRgba8> {
1109 let image = decode_validated_thumbnail_png_to_rgba_image(bytes)?;
1110 Ok(DecodedThumbnailRgba8 {
1111 width: image.width,
1112 height: image.height,
1113 stride: usize::try_from(image.width)
1114 .ok()
1115 .and_then(|width| width.checked_mul(4))
1116 .ok_or_else(|| {
1117 ThumbnailError::resource_limit_exceeded("PNG decode resource limit exceeded")
1118 })?,
1119 pixels: image.pixels,
1120 })
1121}
1122
1123fn decode_validated_thumbnail_png_to_rgba_image(bytes: &[u8]) -> Result<RgbaImage> {
1124 let decoder = png::Decoder::new(Cursor::new(bytes));
1125 let mut reader = decoder
1126 .read_info()
1127 .map_err(|err| ThumbnailError::png(err.to_string()))?;
1128 let Some(output_buffer_size) = reader.output_buffer_size() else {
1129 return Err(ThumbnailError::png(
1130 "png output buffer size is unavailable".to_owned(),
1131 ));
1132 };
1133 let info = reader.info();
1134 ensure_parsed_png_resource_limits(info.width, info.height, output_buffer_size)?;
1135 let mut buffer = vec![0; output_buffer_size];
1136 let output = reader
1137 .next_frame(&mut buffer)
1138 .map_err(|err| ThumbnailError::png(err.to_string()))?;
1139 if output.bit_depth != png::BitDepth::Eight {
1140 return Err(ThumbnailError::png(
1141 "validated thumbnail did not decode to 8-bit samples".to_owned(),
1142 ));
1143 }
1144
1145 let expected_len = validated_lookup_rgba_buffer_len(output.width, output.height)?;
1146 let frame = &buffer[..output.buffer_size()];
1147 let pixels = match output.color_type {
1148 png::ColorType::Rgba => {
1149 if frame.len() != expected_len {
1150 return Err(ThumbnailError::png(
1151 "decoded RGBA buffer length does not match dimensions".to_owned(),
1152 ));
1153 }
1154 frame.to_vec()
1155 }
1156 png::ColorType::GrayscaleAlpha => {
1157 let expected_gray_alpha_len = expected_len / 2;
1158 if frame.len() != expected_gray_alpha_len {
1159 return Err(ThumbnailError::png(
1160 "decoded grayscale-alpha buffer length does not match dimensions".to_owned(),
1161 ));
1162 }
1163 let mut out = Vec::with_capacity(expected_len);
1164 for pixel in frame.chunks_exact(2) {
1165 out.extend_from_slice(&[pixel[0], pixel[0], pixel[0], pixel[1]]);
1166 }
1167 out
1168 }
1169 png::ColorType::Grayscale | png::ColorType::Rgb | png::ColorType::Indexed => {
1170 return Err(ThumbnailError::png(
1171 "validated thumbnail color type is not RGBA or grayscale-alpha".to_owned(),
1172 ));
1173 }
1174 };
1175
1176 Ok(RgbaImage {
1177 width: output.width,
1178 height: output.height,
1179 pixels,
1180 })
1181}
1182
1183fn decode_rendered_png_to_rgba8(bytes: &[u8]) -> Result<RgbaImage> {
1184 let mut decoder = png::Decoder::new(Cursor::new(bytes));
1185 decoder.set_transformations(
1186 png::Transformations::EXPAND | png::Transformations::STRIP_16 | png::Transformations::ALPHA,
1187 );
1188 let mut reader = decoder
1189 .read_info()
1190 .map_err(|err| ThumbnailError::png(err.to_string()))?;
1191 let info = reader.info();
1192 if info.animation_control.is_some() {
1193 return Err(ThumbnailError::unsupported_rendered_thumbnail(
1194 "animated PNG output is unsupported",
1195 ));
1196 }
1197 let Some(output_buffer_size) = reader.output_buffer_size() else {
1198 return Err(ThumbnailError::png(
1199 "png output buffer size is unavailable".to_owned(),
1200 ));
1201 };
1202 ensure_rendered_resource_limits(info.width, info.height, output_buffer_size)?;
1203 let mut buffer = vec![0; output_buffer_size];
1204 let output = reader
1205 .next_frame(&mut buffer)
1206 .map_err(|err| ThumbnailError::png(err.to_string()))?;
1207 let frame = &buffer[..output.buffer_size()];
1208 if output.bit_depth != png::BitDepth::Eight {
1209 return Err(ThumbnailError::unsupported_rendered_thumbnail(
1210 "decoded PNG did not normalize to 8-bit samples",
1211 ));
1212 }
1213
1214 let pixels = match output.color_type {
1215 png::ColorType::Rgba => frame.to_vec(),
1216 png::ColorType::Rgb => {
1217 let mut out = Vec::with_capacity(rgba_buffer_len(output.width, output.height)?);
1218 for pixel in frame.chunks_exact(3) {
1219 out.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 255]);
1220 }
1221 out
1222 }
1223 png::ColorType::GrayscaleAlpha => {
1224 let mut out = Vec::with_capacity(rgba_buffer_len(output.width, output.height)?);
1225 for pixel in frame.chunks_exact(2) {
1226 out.extend_from_slice(&[pixel[0], pixel[0], pixel[0], pixel[1]]);
1227 }
1228 out
1229 }
1230 png::ColorType::Grayscale | png::ColorType::Indexed => {
1231 let mut out = Vec::with_capacity(rgba_buffer_len(output.width, output.height)?);
1232 for &gray in frame {
1233 out.extend_from_slice(&[gray, gray, gray, 255]);
1234 }
1235 out
1236 }
1237 };
1238
1239 Ok(RgbaImage {
1240 width: output.width,
1241 height: output.height,
1242 pixels,
1243 })
1244}
1245
1246fn raw_thumbnail_to_rgba8(image: RawThumbnailImage<'_>) -> Result<RgbaImage> {
1247 let row_bytes = raw_row_bytes(image.width, image.format)?;
1248 let mut pixels = Vec::with_capacity(rgba_buffer_len(image.width, image.height)?);
1249 for row_index in 0..image.height as usize {
1250 let start = row_index * image.stride;
1251 let row = &image.pixels[start..start + row_bytes];
1252 match image.format {
1253 RawThumbnailPixelFormat::Rgb8 => {
1254 for pixel in row.chunks_exact(3) {
1255 pixels.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 255]);
1256 }
1257 }
1258 RawThumbnailPixelFormat::Rgba8 => pixels.extend_from_slice(row),
1259 }
1260 }
1261 Ok(RgbaImage {
1262 width: image.width,
1263 height: image.height,
1264 pixels,
1265 })
1266}
1267
1268fn downscale_to_namespace(image: RgbaImage, size: ThumbnailSize) -> Result<RgbaImage> {
1269 let max = size.max_dimension();
1270 if image.width <= max && image.height <= max {
1271 return Ok(image);
1272 }
1273 let (width, height) = constrain_dimensions(image.width, image.height, max);
1274 let source = image
1275 .pixels
1276 .chunks_exact(4)
1277 .map(|pixel| resize::px::RGBA::new(pixel[0], pixel[1], pixel[2], pixel[3]))
1278 .collect::<Vec<_>>();
1279 let mut dest = vec![resize::px::RGBA::new(0, 0, 0, 0); pixel_count_len(width, height)?];
1280 let mut resizer = resize::new(
1281 image.width as usize,
1282 image.height as usize,
1283 width as usize,
1284 height as usize,
1285 resize::Pixel::RGBA8P,
1286 resize::Type::Lanczos3,
1287 )
1288 .map_err(|_| ThumbnailError::unsupported_rendered_thumbnail("resize setup failed"))?;
1289 resizer
1290 .resize(&source, &mut dest)
1291 .map_err(|_| ThumbnailError::unsupported_rendered_thumbnail("resize failed"))?;
1292 let mut pixels = Vec::with_capacity(dest.len() * 4);
1293 for pixel in dest {
1294 pixels.extend_from_slice(&[pixel.r, pixel.g, pixel.b, pixel.a]);
1295 }
1296 Ok(RgbaImage {
1297 width,
1298 height,
1299 pixels,
1300 })
1301}
1302
1303fn constrain_dimensions(width: u32, height: u32, max: u32) -> (u32, u32) {
1304 if width >= height {
1305 let scaled_height = (u64::from(height) * u64::from(max) / u64::from(width)).max(1) as u32;
1306 (max, scaled_height)
1307 } else {
1308 let scaled_width = (u64::from(width) * u64::from(max) / u64::from(height)).max(1) as u32;
1309 (scaled_width, max)
1310 }
1311}
1312
1313pub(crate) fn encode_rgba_png(
1314 width: u32,
1315 height: u32,
1316 pixels: &[u8],
1317 metadata: &[(String, String)],
1318) -> Result<Vec<u8>> {
1319 let expected_len = rgba_buffer_len(width, height)?;
1320 if pixels.len() != expected_len {
1321 return Err(ThumbnailError::unsupported_rendered_thumbnail(
1322 "RGBA buffer length does not match dimensions",
1323 ));
1324 }
1325 let mut output = Vec::new();
1326 {
1327 let mut encoder = png::Encoder::new(&mut output, width, height);
1328 encoder.set_color(png::ColorType::Rgba);
1329 encoder.set_depth(png::BitDepth::Eight);
1330 for (key, value) in metadata {
1331 encoder
1332 .add_text_chunk(key.clone(), value.clone())
1333 .map_err(|err| ThumbnailError::png(err.to_string()))?;
1334 }
1335 let mut writer = encoder
1336 .write_header()
1337 .map_err(|err| ThumbnailError::png(err.to_string()))?;
1338 writer
1339 .write_image_data(pixels)
1340 .map_err(|err| ThumbnailError::png(err.to_string()))?;
1341 }
1342 Ok(output)
1343}
1344
1345pub(crate) fn validate_mime_type(mime_type: &str) -> Result<()> {
1346 if mime_type.is_empty()
1347 || !mime_type.is_ascii()
1348 || mime_type.bytes().any(|byte| byte.is_ascii_control())
1349 || !mime_type.contains('/')
1350 {
1351 return Err(ThumbnailError::invalid_metadata("invalid MIME type"));
1352 }
1353 Ok(())
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358 use super::*;
1359
1360 #[test]
1361 fn rendered_resource_limit_rejects_large_dimensions() {
1362 let error = ensure_rendered_resource_limits(4097, 4097, 4097 * 4097 * 4).unwrap_err();
1363 assert!(matches!(
1364 error,
1365 ThumbnailError::UnsupportedRenderedThumbnail {
1366 reason: "rendered PNG resource limit exceeded",
1367 ..
1368 }
1369 ));
1370 }
1371
1372 #[test]
1373 fn rgba_buffer_len_rejects_overflow() {
1374 let error = rgba_buffer_len(u32::MAX, u32::MAX).unwrap_err();
1375 assert!(matches!(
1376 error,
1377 ThumbnailError::UnsupportedRenderedThumbnail {
1378 reason: "RGBA buffer length overflows usize",
1379 ..
1380 }
1381 ));
1382 }
1383
1384 #[test]
1385 fn rendered_png_rejects_apng() {
1386 let mut output = Vec::new();
1387 {
1388 let mut encoder = png::Encoder::new(&mut output, 1, 1);
1389 encoder.set_color(png::ColorType::Rgba);
1390 encoder.set_depth(png::BitDepth::Eight);
1391 encoder.set_animated(1, 0).unwrap();
1392 let mut writer = encoder.write_header().unwrap();
1393 writer.write_image_data(&[255, 0, 0, 255]).unwrap();
1394 }
1395
1396 let error = match decode_rendered_png_to_rgba8(&output) {
1397 Ok(_) => panic!("APNG rendered output was accepted"),
1398 Err(error) => error,
1399 };
1400 assert!(matches!(
1401 error,
1402 ThumbnailError::UnsupportedRenderedThumbnail {
1403 reason: "animated PNG output is unsupported",
1404 ..
1405 }
1406 ));
1407 }
1408}