diff --git a/docs_v1.0/DESIGN/Thumbnail_JPEG_Validation_Impl.md b/docs_v1.0/DESIGN/Thumbnail_JPEG_Validation_Impl.md new file mode 100644 index 0000000..4350073 --- /dev/null +++ b/docs_v1.0/DESIGN/Thumbnail_JPEG_Validation_Impl.md @@ -0,0 +1,187 @@ +--- +title: Thumbnail JPEG Validation Implementation +version: 1.0.0 +date: 2026-05-27 +author: M5Max128 +status: ready_for_implementation +--- + +# Thumbnail JPEG Validation Implementation + +## Overview + +Add JPEG quality validation to all ffmpeg image extraction endpoints to prevent: +- Empty images (0 bytes) +- Corrupted JPEG (missing header/footer) +- Incomplete JPEG (truncated output) + +## Files to Create/Modify + +### 1. Create: `src/core/thumbnail/validator.rs` + +```rust +use anyhow::{bail, Result}; + +pub const JPEG_MIN_SIZE: usize = 100; +pub const JPEG_SOI_MARKER: [u8; 3] = [0xFF, 0xD8, 0xFF]; +pub const JPEG_EOI_MARKER: [u8; 2] = [0xFF, 0xD9]; + +pub fn validate_jpeg(data: &[u8]) -> Result<()> { + if data.len() < JPEG_MIN_SIZE { + bail!("JPEG too small: {} bytes (minimum {})", data.len(), JPEG_MIN_SIZE); + } + + if data[0..3] != JPEG_SOI_MARKER { + bail!("Invalid JPEG header: expected {:02X?}, got {:02X?}", JPEG_SOI_MARKER, &data[0..3]); + } + + if data[data.len() - 2..] != JPEG_EOI_MARKER { + bail!("Incomplete JPEG: missing EOI marker, got {:02X?}", &data[data.len() - 2..]); + } + + Ok(()) +} + +pub fn is_valid_jpeg(data: &[u8]) -> bool { + validate_jpeg(data).is_ok() +} + +pub fn jpeg_size_ok(data: &[u8]) -> bool { + data.len() >= JPEG_MIN_SIZE +} + +pub fn jpeg_header_ok(data: &[u8]) -> bool { + data.len() >= 3 && data[0..3] == JPEG_SOI_MARKER +} + +pub fn jpeg_footer_ok(data: &[u8]) -> bool { + data.len() >= 2 && data[data.len() - 2..] == JPEG_EOI_MARKER +} +``` + +### 2. Modify: `src/core/thumbnail/mod.rs` + +Add module declaration at line 1: + +```rust +pub mod validator; + +use anyhow::{Context, Result}; +// ... rest of file +``` + +### 3. Modify: `src/api/media_api.rs` + +Location: `face_thumbnail()` function, after ffmpeg output check (around line 754) + +Add validation: + +```rust +if !output.status.success() { + return Err(StatusCode::INTERNAL_SERVER_ERROR); +} + +// ADD THIS LINE: +crate::core::thumbnail::validator::validate_jpeg(&output.stdout) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + +Ok(Response::builder() + // ... rest of response +``` + +### 4. Modify: `src/api/trace_agent_api.rs` + +Location: `get_trace_thumbnail()` function, after reading bytes (around line 544) + +Add validation: + +```rust +let bytes = tokio::fs::read(&tmp).await.map_err(|e| { + (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))) +})?; + +let _ = tokio::fs::remove_file(&tmp).await; + +// ADD THIS LINE: +crate::core::thumbnail::validator::validate_jpeg(&bytes) + .map_err(|e| { + (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))) + })?; + +Ok(Response::builder() + // ... rest of response +``` + +### 5. Modify: `src/core/frame_cache.rs` + +Location: `FrameManager::extract()`, when iterating extracted frames (around line 73) + +Replace the frame collection logic: + +```rust +for entry in &entries { + let fname = entry.file_name(); + let fname_str = fname.to_string_lossy(); + if let Some(num_str) = fname_str + .strip_prefix("frame_") + .and_then(|s| s.strip_suffix(".jpg")) + { + if let Ok(frame_num) = num_str.parse::() { + let frame_path = entry.path(); + // ADD VALIDATION: + if let Ok(data) = std::fs::read(&frame_path) { + if crate::core::thumbnail::validator::is_valid_jpeg(&data) { + let timestamp = frame_num as f64 / fps; + frames.push(CachedFrame { + path: frame_path, + frame_number: frame_num, + timestamp_secs: timestamp, + }); + } else { + info!("[FrameCache] Skipping invalid JPEG: {:?}", frame_path); + } + } + } + } +} +``` + +## Validation Logic + +| Check | Condition | Error if failed | +|-------|-----------|-----------------| +| Minimum size | `len() >= 100` | "JPEG too small" | +| SOI marker | `[0..3] == [0xFF,0xD8,0xFF]` | "Invalid JPEG header" | +| EOI marker | `[-2..] == [0xFF,0xD9]` | "Incomplete JPEG" | + +## Testing + +After implementation, run: + +```bash +source ~/.cargo/env +export MOMENTRY_PYTHON_PATH="/Users/accusys/momentry_core/venv/bin/python" +cargo clippy --lib +cargo test --lib +``` + +Expected: 220 passed, 0 failed + +## Commit Message + +``` +feat: add JPEG validation to thumbnail endpoints + +- Create validator module with JPEG header/footer/size checks +- Add validation to face_thumbnail endpoint +- Add validation to get_trace_thumbnail endpoint +- Filter invalid JPEGs in FrameManager::extract + +Prevents serving corrupted/incomplete JPEG images to frontend. +``` + +## Version History + +| Version | Date | Author | Changes | +|---------|------|--------|---------| +| 1.0.0 | 2026-05-27 | M5Max128 | Implementation plan ready | \ No newline at end of file