Skip to main content

aven_core/attachments/
validation.rs

1#![allow(dead_code)]
2
3use anyhow::{Result, bail};
4
5pub const MAX_BLOB_BYTES: usize = 25 * 1024 * 1024;
6pub const MAX_FILENAME_LEN: usize = 255;
7pub const MAX_ALT_TEXT_LEN: usize = 500;
8pub const SUPPORTED_MEDIA_TYPES: &[&str] = &["image/png", "image/jpeg", "image/gif", "image/webp"];
9
10pub fn validate_attachment_id(value: &str) -> Result<()> {
11    if value.len() != 16 || !value.bytes().all(|byte| crate::ids::BASE32.contains(&byte)) {
12        bail!("error invalid-attachment-id input={value}");
13    }
14    Ok(())
15}
16
17pub fn validate_sha256(value: &str) -> Result<()> {
18    if value.len() != 64
19        || !value
20            .bytes()
21            .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
22    {
23        bail!("error invalid-sha256");
24    }
25    Ok(())
26}
27
28pub fn validate_media_type(value: &str) -> Result<()> {
29    if !SUPPORTED_MEDIA_TYPES.contains(&value) {
30        bail!("error unsupported-attachment-media-type input={value}");
31    }
32    Ok(())
33}
34
35pub fn validate_blob_size(byte_size: usize) -> Result<()> {
36    if byte_size == 0 || byte_size > MAX_BLOB_BYTES {
37        bail!("error invalid-attachment-size bytes={byte_size}");
38    }
39    Ok(())
40}
41
42pub fn validate_filename(value: Option<&str>) -> Result<()> {
43    let Some(value) = value else {
44        return Ok(());
45    };
46    if value.is_empty()
47        || value.len() > MAX_FILENAME_LEN
48        || value
49            .chars()
50            .any(|ch| ch.is_control() || ch == '/' || ch == '\\')
51    {
52        bail!("error invalid-attachment-filename");
53    }
54    Ok(())
55}
56
57pub fn validate_alt_text(value: Option<&str>) -> Result<()> {
58    let Some(value) = value else {
59        return Ok(());
60    };
61    if value.len() > MAX_ALT_TEXT_LEN || value.chars().any(char::is_control) {
62        bail!("error invalid-attachment-alt-text");
63    }
64    Ok(())
65}
66
67pub fn validate_dimensions(width: Option<i64>, height: Option<i64>) -> Result<()> {
68    let (Some(width), Some(height)) = (width, height) else {
69        bail!("error invalid-attachment-dimensions");
70    };
71    let Ok(width_u32) = u32::try_from(width) else {
72        bail!("error invalid-attachment-dimensions");
73    };
74    let Ok(height_u32) = u32::try_from(height) else {
75        bail!("error invalid-attachment-dimensions");
76    };
77    if width_u32 == 0
78        || height_u32 == 0
79        || width_u32 > crate::attachments::decode::MAX_IMAGE_EDGE
80        || height_u32 > crate::attachments::decode::MAX_IMAGE_EDGE
81        || u64::from(width_u32) * u64::from(height_u32)
82            > crate::attachments::decode::MAX_FRAME_PIXELS
83    {
84        bail!("error invalid-attachment-dimensions");
85    }
86    Ok(())
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn validates_attachment_id() {
95        assert!(validate_attachment_id("7KQ9A1X4MV2P8D6R").is_ok());
96        assert!(validate_attachment_id("short").is_err());
97        assert!(validate_attachment_id("7KQ9A1X4MV2P8D6U").is_err()); // 'U' is not base32
98        assert!(validate_attachment_id("7KQ9A1X4MV2P8D6r").is_err()); // lowercase not allowed
99    }
100
101    #[test]
102    fn validates_sha256() {
103        let valid = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
104        assert!(validate_sha256(valid).is_ok());
105        assert!(validate_sha256("short").is_err());
106        assert!(validate_sha256(&format!("{valid}g")).is_err());
107        assert!(validate_sha256(&valid.to_ascii_uppercase()).is_err());
108    }
109
110    #[test]
111    fn validates_media_type() {
112        assert!(validate_media_type("image/png").is_ok());
113        assert!(validate_media_type("image/jpeg").is_ok());
114        assert!(validate_media_type("image/gif").is_ok());
115        assert!(validate_media_type("image/webp").is_ok());
116        assert!(validate_media_type("image/svg+xml").is_err());
117        assert!(validate_media_type("").is_err());
118    }
119
120    #[test]
121    fn validates_blob_size() {
122        assert!(validate_blob_size(1).is_ok());
123        assert!(validate_blob_size(25 * 1024 * 1024).is_ok());
124        assert!(validate_blob_size(0).is_err());
125        assert!(validate_blob_size(25 * 1024 * 1024 + 1).is_err());
126    }
127
128    #[test]
129    fn validates_filename() {
130        assert!(validate_filename(Some("photo.png")).is_ok());
131        assert!(validate_filename(None).is_ok());
132        assert!(validate_filename(Some("")).is_err());
133        assert!(validate_filename(Some("a/photo.png")).is_err());
134        assert!(validate_filename(Some("a\\photo.png")).is_err());
135        let long = "a".repeat(256);
136        assert!(validate_filename(Some(&long)).is_err());
137    }
138
139    #[test]
140    fn validates_alt_text() {
141        assert!(validate_alt_text(Some("a photo of a cat")).is_ok());
142        assert!(validate_alt_text(None).is_ok());
143        assert!(validate_alt_text(Some("")).is_ok());
144        let long = "a".repeat(501);
145        assert!(validate_alt_text(Some(&long)).is_err());
146        assert!(validate_alt_text(Some("a\nb")).is_err());
147    }
148
149    #[test]
150    fn validates_dimensions() {
151        assert!(validate_dimensions(None, None).is_err());
152        assert!(validate_dimensions(Some(1920), Some(1080)).is_ok());
153        assert!(validate_dimensions(Some(0), Some(1080)).is_err());
154        assert!(validate_dimensions(Some(1920), None).is_err());
155        assert!(validate_dimensions(None, Some(1080)).is_err());
156    }
157}