Skip to content

Commit f3e45e8

Browse files
NicoHinderlingclaudeszokeasaurusrex
authored
feat(snapshots): Add 40M pixel limit validation for snapshot images (#3179)
The backend enforces a 40M pixel limit per image during snapshot comparison (`MAX_DIFF_PIXELS`), but oversized images were only rejected at comparison time, wasting upload bandwidth. The CLI now checks `width * height` against the 40,000,000 pixel threshold before uploading and reports all violations with their dimensions. Refs EME-885 --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com>
1 parent 5c58644 commit f3e45e8

File tree

1 file changed

+60
-0
lines changed

1 file changed

+60
-0
lines changed

src/commands/build/snapshots.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::str::FromStr as _;
66
use anyhow::{Context as _, Result};
77
use clap::{Arg, ArgMatches, Command};
88
use console::style;
9+
use itertools::Itertools as _;
910
use log::{debug, info, warn};
1011
use objectstore_client::{ClientBuilder, ExpirationPolicy, Usecase};
1112
use secrecy::ExposeSecret as _;
@@ -21,6 +22,7 @@ const EXPERIMENTAL_WARNING: &str =
2122
The command is subject to breaking changes, including removal, in any Sentry CLI release.";
2223

2324
const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg"];
25+
const MAX_PIXELS_PER_IMAGE: u64 = 40_000_000;
2426

2527
pub fn make_command(command: Command) -> Command {
2628
command
@@ -52,6 +54,12 @@ struct ImageInfo {
5254
height: u32,
5355
}
5456

57+
impl ImageInfo {
58+
fn pixels(&self) -> u64 {
59+
u64::from(self.width) * u64::from(self.height)
60+
}
61+
}
62+
5563
pub fn execute(matches: &ArgMatches) -> Result<()> {
5664
eprintln!("{EXPERIMENTAL_WARNING}");
5765

@@ -88,6 +96,8 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
8896
if images.len() == 1 { "file" } else { "files" }
8997
);
9098

99+
validate_image_sizes(&images)?;
100+
91101
// Upload image files to objectstore
92102
println!(
93103
"{} Uploading {} image {}",
@@ -174,6 +184,31 @@ fn collect_image_info(dir: &Path, path: &Path) -> Option<ImageInfo> {
174184
})
175185
}
176186

187+
fn validate_image_sizes(images: &[ImageInfo]) -> Result<()> {
188+
let mut violations = images
189+
.iter()
190+
.filter(|img| img.pixels() > MAX_PIXELS_PER_IMAGE)
191+
.map(|img| {
192+
let path = img.relative_path.display();
193+
let width = img.width;
194+
let height = img.height;
195+
let pixels = img.pixels();
196+
197+
format!(" {path} ({width}x{height} = {pixels} pixels)")
198+
})
199+
.peekable();
200+
201+
if violations.peek().is_some() {
202+
let violation_messages = violations.join("\n");
203+
204+
anyhow::bail!(
205+
"The following images exceed the maximum pixel limit of {MAX_PIXELS_PER_IMAGE}:\n{violation_messages}",
206+
);
207+
}
208+
209+
Ok(())
210+
}
211+
177212
fn compute_sha256_hash(data: &[u8]) -> String {
178213
let mut hasher = Sha256::new();
179214
hasher.update(data);
@@ -288,3 +323,28 @@ fn upload_images(
288323
}
289324
}
290325
}
326+
327+
#[cfg(test)]
328+
mod tests {
329+
use super::*;
330+
331+
fn make_image(width: u32, height: u32) -> ImageInfo {
332+
ImageInfo {
333+
path: PathBuf::from("img.png"),
334+
relative_path: PathBuf::from("img.png"),
335+
width,
336+
height,
337+
}
338+
}
339+
340+
#[test]
341+
fn test_validate_image_sizes_at_limit_passes() {
342+
assert!(validate_image_sizes(&[make_image(8000, 5000)]).is_ok());
343+
}
344+
345+
#[test]
346+
fn test_validate_image_sizes_over_limit_fails() {
347+
let err = validate_image_sizes(&[make_image(8001, 5000)]).unwrap_err();
348+
assert!(err.to_string().contains("exceed the maximum pixel limit"));
349+
}
350+
}

0 commit comments

Comments
 (0)