docs: add JPEG validation implementation plan for M5Max48
This commit is contained in:
187
docs_v1.0/DESIGN/Thumbnail_JPEG_Validation_Impl.md
Normal file
187
docs_v1.0/DESIGN/Thumbnail_JPEG_Validation_Impl.md
Normal file
@@ -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::<u64>() {
|
||||
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 |
|
||||
Reference in New Issue
Block a user