diff --git a/docs/HARDENING_LIBSODIUM.md b/docs/HARDENING_LIBSODIUM.md new file mode 100644 index 0000000..6fffdd4 --- /dev/null +++ b/docs/HARDENING_LIBSODIUM.md @@ -0,0 +1,90 @@ +# Applying IMAGEHARDER-style hardening to libsodium + +The same practices used in IMAGEHARDER (bounded inputs, explicit error mapping, metrics, and sandboxing) can be mirrored when embedding +[libsodium](https://github.com/jedisct1/libsodium) for cryptographic workloads. This note outlines a secure-integration profile +suitable for Xen domU/CI builders. + +## Goals +- Stable, minimal FFI boundary with explicit error types. +- Deterministic builds that pin versions and compiler flags. +- Isolation of key material and misuse-resistant APIs. +- Observability: metrics, tamper-evident audit logs, and build provenance. + +## Build strategy +- **Version pinning**: vendor a known-good libsodium release and verify with `sha256sum` before build. +- **Compiler flags**: prefer `-fstack-protector-strong -D_FORTIFY_SOURCE=3 -fPIC -O2 -pipe -fno-plt` plus + `-fstack-clash-protection` on supported toolchains. Enable `-fcf-protection=full` for CET-capable targets. +- **Linking**: prefer static linking in minimal domU images; use `RUSTFLAGS="-C target-feature=+crt-static"` when pairing with + Rust consumers to avoid dynamic search-path issues. +- **Reproducibility**: capture build metadata (compiler, flags, git SHA) in an SBOM; wire `generate-sbom.sh` as part of CI. + +## FFI surface design +- Wrap each libsodium primitive in narrow functions that accept length-checked slices and return `Result`. +- Prohibit raw pointer exposure; translate C error codes into typed Rust errors with context. +- Add zeroization hooks (`zeroize::Zeroize`) for buffers that carry keys, nonces, or secrets. +- Enforce domain separation by tagging APIs per purpose (e.g., `Auth`, `Aead`, `Kdf`) to discourage misuse. + +## Runtime safeguards +- **Initialization guard**: perform a one-time `sodium_init()` with failure handling before exposing any primitive. +- **Key lifecycle**: keep keys in guarded memory (e.g., `secrecy::SecretVec`) and avoid serialization. Provide key-generation + helpers that default to high-entropy RNGs (libsodium already defaults to `randombytes_buf`), and expose optional hardware RNG + seeding for dom0/domU if available. +- **Parameter validation**: validate nonce sizes, tag lengths, and buffer boundaries before calling into libsodium. +- **Constant-time expectations**: document which APIs are constant-time and block callers from using them for unrelated purposes + (e.g., no generic equality on MACs without constant-time compare). + +## Observability and policy +- Emit Prometheus counters for successes/failures per primitive (without leaking secrets) and gauge unexpected parameter + rejections. +- Capture audit logs that include operation type, key identifier, and policy decisions; ship logs over mTLS to central storage. +- Include a policy module that enforces approved cipher suites (e.g., `xchacha20poly1305_ietf`) and blocks deprecated ones. + +## Testing and validation +- **KATs**: integrate libsodium's known-answer tests and add fuzzers around the FFI boundary to catch length/parameter issues. +- **Memory checks**: run under `valgrind`/`ASan` in CI; ensure zeroization paths are covered. +- **Cross-platform**: validate builds under Xen domU, containers, and bare-metal to confirm consistent instruction sets and + `RDRAND` availability. + +## Example integration sketch + +```rust +// Pseudocode illustrating a narrow AEAD wrapper +pub fn seal(key: &SecretVec, nonce: &[u8; crypto_aead_xchacha20poly1305_ietf_NPUBBYTES], + aad: &[u8], plaintext: &[u8]) -> Result, CryptoError> { + if plaintext.len() > MAX_MESSAGE || aad.len() > MAX_AAD { + return Err(CryptoError::InputTooLarge); + } + + let mut ciphertext = vec![0u8; plaintext.len() + TAG_LEN]; + let mut clen = 0usize; + let rc = unsafe { + crypto_aead_xchacha20poly1305_ietf_encrypt( + ciphertext.as_mut_ptr(), + &mut clen, + plaintext.as_ptr(), + plaintext.len() as u64, + aad.as_ptr(), + aad.len() as u64, + std::ptr::null(), + nonce.as_ptr(), + key.expose_secret().as_ptr(), + ) + }; + + if rc != 0 { + return Err(CryptoError::EncryptionFailed); + } + + ciphertext.truncate(clen); + Ok(ciphertext) +} +``` + +## Operational checklist +- Verify SBOM provenance for each release and store alongside build artifacts. +- Rotate keys on a fixed schedule; enforce non-reuse of nonces via per-key counters. +- Run `cargo deny` (or equivalent) to flag transitive CVEs in Rust bindings. +- Add a chaos test that simulates allocator failures to ensure safe unwinding. + +Following this pattern mirrors IMAGEHARDER's focus on bounded inputs, explicit error handling, and strong observability—adapted to the +cryptographic domain served by libsodium. diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md new file mode 100644 index 0000000..2352f4e --- /dev/null +++ b/docs/INTEGRATION.md @@ -0,0 +1,65 @@ +# Integration guide + +This guide shows how to embed IMAGEHARDER as a Git submodule and call the hardened decoder API from a parent project. + +## 1) Add the repository as a submodule +```bash +git submodule add https://github.com/SWORDIntel/IMAGEHARDER.git external/imageharder +git submodule update --init --recursive +``` + +## 2) Wire the crate into your workspace +Add the crate using a path dependency so the parent project builds against the submodule source. + +```toml +# parent-project/Cargo.toml +[dependencies] +image_harden = { path = "external/imageharder/image_harden" } +``` + +If you keep a Cargo workspace, also append the crate to `members`: +```toml +[workspace] +members = [ + "image_harden", # if IMAGEHARDER lives at repo root + "external/imageharder/image_harden" +] +``` + +## 3) Call the public API +Use the hardened helpers directly. Each decoder returns either raw bytes (images/video) or `AudioData` for audio formats. + +```rust +use image_harden::decode_png; + +fn decode_png(bytes: &[u8]) -> Result, image_harden::ImageHardenError> { + decode_png(bytes) +} +``` + +## 4) Feature flags +Optional formats are disabled by default to keep builds lean. Enable only what you need in the dependency declaration: + +```toml +[dependencies] +image_harden = { path = "external/imageharder/image_harden", features = ["avif", "jxl", "tiff", "openexr", "icc", "exif"] } +``` + +## 5) Metrics (optional) +The crate ships a tiny Prometheus exporter to surface decode metrics. + +```rust +// Expose metrics on 0.0.0.0:9898 +image_harden::metrics_server::start_metrics_server("0.0.0.0:9898")?; +``` + +## 6) Updating the submodule +Pull upstream changes and propagate to your workspace: +```bash +git submodule update --remote external/imageharder +cd external/imageharder && git checkout +``` + +## Notes for Xen domU/CI builders +- The build is pure Rust by default; C-based optional codecs (e.g., libavif) require system headers and are isolated behind feature flags. +- The CLI entrypoint (`image_harden_cli`) exercises the same hardened limits as the library for parity testing. diff --git a/ADDITIONAL_FORMATS.md b/docs/archive/ADDITIONAL_FORMATS.md similarity index 100% rename from ADDITIONAL_FORMATS.md rename to docs/archive/ADDITIONAL_FORMATS.md diff --git a/AUDIO_HARDENING.md b/docs/archive/AUDIO_HARDENING.md similarity index 100% rename from AUDIO_HARDENING.md rename to docs/archive/AUDIO_HARDENING.md diff --git a/COCKPIT_INTEGRATION.md b/docs/archive/COCKPIT_INTEGRATION.md similarity index 100% rename from COCKPIT_INTEGRATION.md rename to docs/archive/COCKPIT_INTEGRATION.md diff --git a/KERNEL_BUILD.md b/docs/archive/KERNEL_BUILD.md similarity index 100% rename from KERNEL_BUILD.md rename to docs/archive/KERNEL_BUILD.md diff --git a/LOAD_TESTING.md b/docs/archive/LOAD_TESTING.md similarity index 100% rename from LOAD_TESTING.md rename to docs/archive/LOAD_TESTING.md diff --git a/METEOR_LAKE_BUILD.md b/docs/archive/METEOR_LAKE_BUILD.md similarity index 100% rename from METEOR_LAKE_BUILD.md rename to docs/archive/METEOR_LAKE_BUILD.md diff --git a/PRODUCTION_DEPLOYMENT.md b/docs/archive/PRODUCTION_DEPLOYMENT.md similarity index 100% rename from PRODUCTION_DEPLOYMENT.md rename to docs/archive/PRODUCTION_DEPLOYMENT.md diff --git a/PR_DESCRIPTION.md b/docs/archive/PR_DESCRIPTION.md similarity index 100% rename from PR_DESCRIPTION.md rename to docs/archive/PR_DESCRIPTION.md diff --git a/QUICKSTART.md b/docs/archive/QUICKSTART.md similarity index 100% rename from QUICKSTART.md rename to docs/archive/QUICKSTART.md diff --git a/docs/archive/README.md b/docs/archive/README.md new file mode 100644 index 0000000..b10d3b4 --- /dev/null +++ b/docs/archive/README.md @@ -0,0 +1,18 @@ +# Archived root documents + +The following documents were moved out of the repository root to keep the entrypoint clean for submodule consumers. Content is unchanged: + +- `ADDITIONAL_FORMATS.md` +- `AUDIO_HARDENING.md` +- `COCKPIT_INTEGRATION.md` +- `KERNEL_BUILD.md` +- `LOAD_TESTING.md` +- `METEOR_LAKE_BUILD.md` +- `PRODUCTION_DEPLOYMENT.md` +- `PR_DESCRIPTION.md` +- `QUICKSTART.md` +- `SECURITY_ARCHITECTURE.md` +- `VIDEO_HARDENING.md` +- `mission.md` + +Refer to these files for the original deep-dives on platform-specific builds, production deployment, and project background. diff --git a/SECURITY_ARCHITECTURE.md b/docs/archive/SECURITY_ARCHITECTURE.md similarity index 100% rename from SECURITY_ARCHITECTURE.md rename to docs/archive/SECURITY_ARCHITECTURE.md diff --git a/VIDEO_HARDENING.md b/docs/archive/VIDEO_HARDENING.md similarity index 100% rename from VIDEO_HARDENING.md rename to docs/archive/VIDEO_HARDENING.md diff --git a/mission.md b/docs/archive/mission.md similarity index 100% rename from mission.md rename to docs/archive/mission.md diff --git a/image_harden/src/api.rs b/image_harden/src/api.rs new file mode 100644 index 0000000..21b125b --- /dev/null +++ b/image_harden/src/api.rs @@ -0,0 +1,159 @@ +//! Public API surface for embedding IMAGEHARDER as a library. +//! This module wraps the individual decoders into a small, stable interface +//! that parent projects can depend on when the repository is consumed as a +//! Git submodule. + +use crate::{ + decode_flac, decode_gif, decode_heif, decode_jpeg, decode_mp3, decode_png, decode_svg, + decode_video, decode_vorbis, decode_webp, AudioData, ImageHardenError, +}; + +#[cfg(feature = "avif")] +use crate::formats::avif::decode_avif; +#[cfg(feature = "exif")] +use crate::formats::exif::validate_exif; +#[cfg(feature = "openexr")] +use crate::formats::exr::decode_exr; +#[cfg(feature = "icc")] +use crate::formats::icc::validate_icc_profile; +#[cfg(feature = "jxl")] +use crate::formats::jxl::decode_jxl; +#[cfg(feature = "tiff")] +use crate::formats::tiff::decode_tiff; + +/// Supported media types for the unified decoder entrypoint. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MediaFormat { + Png, + Jpeg, + Gif, + WebP, + Heif, + Svg, + #[cfg(feature = "avif")] + Avif, + #[cfg(feature = "jxl")] + JpegXl, + #[cfg(feature = "tiff")] + Tiff, + #[cfg(feature = "openexr")] + OpenExr, + AudioMp3, + AudioVorbis, + AudioFlac, + VideoContainer, +} + +/// Decoder output variants. +#[derive(Debug, Clone)] +pub enum DecodedMedia { + Image(Vec), + Audio(AudioData), + Video(Vec), +} + +/// Optional knobs for decoding. Currently only video uses an option +/// to specify the sandboxed WASM path. +#[derive(Debug, Default, Clone)] +pub struct DecoderOptions { + pub video_wasm_path: Option, +} + +/// Convenience wrapper that exposes a minimal surface area for downstream +/// consumers. Use `decode` for sensible defaults or `decode_with_options` to +/// pass explicit configuration. +pub struct HardenedDecoder; + +impl HardenedDecoder { + /// Returns the crate version to make it easy to verify the linked build. + pub fn version() -> &'static str { + env!("CARGO_PKG_VERSION") + } + + /// Decode using defaults. + pub fn decode(format: MediaFormat, data: &[u8]) -> Result { + Self::decode_with_options(format, data, &DecoderOptions::default()) + } + + /// Decode with explicit options (e.g., WASM path for video validation). + pub fn decode_with_options( + format: MediaFormat, + data: &[u8], + options: &DecoderOptions, + ) -> Result { + match format { + MediaFormat::Png => decode_png(data).map(DecodedMedia::Image), + MediaFormat::Jpeg => decode_jpeg(data).map(DecodedMedia::Image), + MediaFormat::Gif => decode_gif(data).map(DecodedMedia::Image), + MediaFormat::WebP => decode_webp(data).map(DecodedMedia::Image), + MediaFormat::Heif => decode_heif(data).map(DecodedMedia::Image), + MediaFormat::Svg => decode_svg(data).map(DecodedMedia::Image), + #[cfg(feature = "avif")] + MediaFormat::Avif => decode_avif(data).map(DecodedMedia::Image), + #[cfg(feature = "jxl")] + MediaFormat::JpegXl => decode_jxl(data).map(DecodedMedia::Image), + #[cfg(feature = "tiff")] + MediaFormat::Tiff => decode_tiff(data).map(DecodedMedia::Image), + #[cfg(feature = "openexr")] + MediaFormat::OpenExr => decode_exr(data).map(DecodedMedia::Image), + MediaFormat::AudioMp3 => decode_mp3(data).map(DecodedMedia::Audio), + MediaFormat::AudioVorbis => decode_vorbis(data).map(DecodedMedia::Audio), + MediaFormat::AudioFlac => decode_flac(data).map(DecodedMedia::Audio), + MediaFormat::VideoContainer => { + decode_video(data, options.video_wasm_path.as_deref().unwrap_or("")) + .map(DecodedMedia::Video) + } + } + } +} + +/// Report which formats are available in the current build based on feature +/// flags. Useful for capability advertisement in parent applications. +pub fn supported_formats() -> Vec<&'static str> { + let mut formats = vec![ + "png", "jpeg", "gif", "webp", "heif", "svg", "mp3", "vorbis", "flac", "video", + ]; + + #[cfg(feature = "avif")] + { + formats.push("avif"); + } + #[cfg(feature = "jxl")] + { + formats.push("jpegxl"); + } + #[cfg(feature = "tiff")] + { + formats.push("tiff"); + } + #[cfg(feature = "openexr")] + { + formats.push("openexr"); + } + #[cfg(feature = "icc")] + { + formats.push("icc"); + } + #[cfg(feature = "exif")] + { + formats.push("exif"); + } + + formats +} + +/// Validate metadata payloads without decoding image data. +#[cfg(any(feature = "icc", feature = "exif"))] +pub fn validate_metadata(data: &[u8]) -> Result<(), ImageHardenError> { + #[cfg(feature = "icc")] + { + validate_icc_profile(data)?; + } + + #[cfg(feature = "exif")] + { + validate_exif(data)?; + } + + Ok(()) +} diff --git a/image_harden/src/lib.rs b/image_harden/src/lib.rs index 53d6474..a7eaddb 100644 --- a/image_harden/src/lib.rs +++ b/image_harden/src/lib.rs @@ -4,11 +4,11 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +use ammonia::clean; use std::ffi::CStr; use std::io::Read; use std::mem; use thiserror::Error; -use ammonia::clean; // Metrics and monitoring modules pub mod metrics; @@ -103,14 +103,24 @@ pub fn decode_png(data: &[u8]) -> Result, ImageHardenError> { let info_ptr = png_create_info_struct(png_ptr); if info_ptr.is_null() { - png_destroy_read_struct(&mut (png_ptr as png_structp), std::ptr::null_mut(), std::ptr::null_mut()); + png_destroy_read_struct( + &mut (png_ptr as png_structp), + std::ptr::null_mut(), + std::ptr::null_mut(), + ); return Err(ImageHardenError::NullPointer); } let jmp_buf_ptr = png_jmpbuf_wrapper(png_ptr) as *mut jmp_buf; if setjmp(mem::transmute(jmp_buf_ptr)) != 0 { - png_destroy_read_struct(&mut (png_ptr as png_structp), &mut (info_ptr as png_infop), std::ptr::null_mut()); - return Err(ImageHardenError::PngError("PNG decoding failed".to_string())); + png_destroy_read_struct( + &mut (png_ptr as png_structp), + &mut (info_ptr as png_infop), + std::ptr::null_mut(), + ); + return Err(ImageHardenError::PngError( + "PNG decoding failed".to_string(), + )); } png_set_user_limits(png_ptr, 8192, 8192); @@ -118,7 +128,11 @@ pub fn decode_png(data: &[u8]) -> Result, ImageHardenError> { png_set_chunk_malloc_max(png_ptr, 256 * 1024); let mut cursor = std::io::Cursor::new(data); - png_set_read_fn(png_ptr, &mut cursor as *mut _ as png_voidp, Some(read_data_fn)); + png_set_read_fn( + png_ptr, + &mut cursor as *mut _ as png_voidp, + Some(read_data_fn), + ); png_read_info(png_ptr, info_ptr); @@ -154,7 +168,11 @@ pub fn decode_png(data: &[u8]) -> Result, ImageHardenError> { png_read_image(png_ptr, row_pointers.as_mut_ptr()); - png_destroy_read_struct(&mut (png_ptr as png_structp), &mut (info_ptr as png_infop), std::ptr::null_mut()); + png_destroy_read_struct( + &mut (png_ptr as png_structp), + &mut (info_ptr as png_infop), + std::ptr::null_mut(), + ); Ok(image_data) } @@ -179,10 +197,16 @@ pub fn decode_jpeg(data: &[u8]) -> Result, ImageHardenError> { if setjmp(err_mgr.jmp_buf.as_mut_ptr()) != 0 { jpeg_destroy_decompress(&mut cinfo); - return Err(ImageHardenError::JpegError("JPEG decoding failed".to_string())); + return Err(ImageHardenError::JpegError( + "JPEG decoding failed".to_string(), + )); } - jpeg_CreateDecompress(&mut cinfo, JPEG_LIB_VERSION as i32, std::mem::size_of::()); + jpeg_CreateDecompress( + &mut cinfo, + JPEG_LIB_VERSION as i32, + std::mem::size_of::(), + ); (*cinfo.mem).max_memory_to_use = 64 * 1024 * 1024; // 64 MB for m in 0xE0..=0xEF { @@ -190,13 +214,14 @@ pub fn decode_jpeg(data: &[u8]) -> Result, ImageHardenError> { } jpeg_save_markers(&mut cinfo, JPEG_COM as i32, 0); - jpeg_mem_src(&mut cinfo, data.as_ptr(), data.len() as u64); jpeg_read_header(&mut cinfo, 1); if cinfo.image_width > 10000 || cinfo.image_height > 10000 { - return Err(ImageHardenError::JpegError("Image dimensions exceed limits".to_string())); + return Err(ImageHardenError::JpegError( + "Image dimensions exceed limits".to_string(), + )); } cinfo.out_color_space = J_COLOR_SPACE_JCS_RGB; @@ -206,7 +231,9 @@ pub fn decode_jpeg(data: &[u8]) -> Result, ImageHardenError> { let mut image_data = vec![0u8; row_stride * cinfo.output_height as usize]; while cinfo.output_scanline < cinfo.output_height { - let mut buffer = [image_data.as_mut_ptr().add(cinfo.output_scanline as usize * row_stride)]; + let mut buffer = [image_data + .as_mut_ptr() + .add(cinfo.output_scanline as usize * row_stride)]; jpeg_read_scanlines(&mut cinfo, buffer.as_mut_ptr(), 1); } @@ -232,7 +259,11 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { unsafe impl Sync for GifMemoryReader {} // GIF read function for memory buffer - unsafe extern "C" fn gif_read_fn(gif_file: *mut GifFileType, buf: *mut GifByteType, size: i32) -> i32 { + unsafe extern "C" fn gif_read_fn( + gif_file: *mut GifFileType, + buf: *mut GifByteType, + size: i32, + ) -> i32 { let reader = (*gif_file).UserData as *mut GifMemoryReader; if reader.is_null() { return 0; @@ -247,11 +278,7 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { return 0; } - std::ptr::copy_nonoverlapping( - reader_ref.data.add(pos), - buf, - to_read, - ); + std::ptr::copy_nonoverlapping(reader_ref.data.add(pos), buf, to_read); reader_ref.pos.store(pos + to_read, Ordering::Relaxed); to_read as i32 @@ -262,10 +289,14 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { return Err(ImageHardenError::GifError("File too small".to_string())); } if &data[0..3] != b"GIF" { - return Err(ImageHardenError::GifError("Invalid GIF signature".to_string())); + return Err(ImageHardenError::GifError( + "Invalid GIF signature".to_string(), + )); } if &data[3..6] != b"87a" && &data[3..6] != b"89a" { - return Err(ImageHardenError::GifError("Unknown GIF version".to_string())); + return Err(ImageHardenError::GifError( + "Unknown GIF version".to_string(), + )); } unsafe { @@ -293,7 +324,10 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { let msg = std::ffi::CStr::from_ptr(error_info.error_msg.as_ptr()) .to_string_lossy() .into_owned(); - return Err(ImageHardenError::GifError(format!("Failed to open GIF: {}", msg))); + return Err(ImageHardenError::GifError(format!( + "Failed to open GIF: {}", + msg + ))); } // Slurp GIF with comprehensive validation @@ -302,7 +336,10 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { .to_string_lossy() .into_owned(); safe_DGifClose(gif_file); - return Err(ImageHardenError::GifError(format!("Failed to decode GIF: {}", msg))); + return Err(ImageHardenError::GifError(format!( + "Failed to decode GIF: {}", + msg + ))); } let gif = &*gif_file; @@ -339,9 +376,10 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { // Validate color map if cmap.ColorCount <= 0 || cmap.ColorCount > 256 { safe_DGifClose(gif_file); - return Err(ImageHardenError::GifError( - format!("Invalid color count: {}", cmap.ColorCount) - )); + return Err(ImageHardenError::GifError(format!( + "Invalid color count: {}", + cmap.ColorCount + ))); } if cmap.Colors.is_null() { @@ -358,7 +396,9 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { // Validate bounds if img_left + img_width > width || img_top + img_height > height { safe_DGifClose(gif_file); - return Err(ImageHardenError::GifError("Image out of bounds".to_string())); + return Err(ImageHardenError::GifError( + "Image out of bounds".to_string(), + )); } // Copy pixels with bounds checking @@ -380,10 +420,11 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { // Validate color index (CVE-2019-15133 mitigation) if color_idx >= cmap.ColorCount as usize { safe_DGifClose(gif_file); - return Err(ImageHardenError::GifError( - format!("Color index {} out of range (max: {})", - color_idx, cmap.ColorCount - 1) - )); + return Err(ImageHardenError::GifError(format!( + "Color index {} out of range (max: {})", + color_idx, + cmap.ColorCount - 1 + ))); } // Get color from color map @@ -413,41 +454,52 @@ pub fn decode_webp(data: &[u8]) -> Result, ImageHardenError> { return Err(ImageHardenError::WebPError("File too small".to_string())); } if &data[0..4] != b"RIFF" { - return Err(ImageHardenError::WebPError("Invalid WebP signature: missing RIFF header".to_string())); + return Err(ImageHardenError::WebPError( + "Invalid WebP signature: missing RIFF header".to_string(), + )); } if &data[8..12] != b"WEBP" { - return Err(ImageHardenError::WebPError("Invalid WebP signature: missing WEBP marker".to_string())); + return Err(ImageHardenError::WebPError( + "Invalid WebP signature: missing WEBP marker".to_string(), + )); } // Check file size matches RIFF header let declared_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize; if declared_size + 8 != data.len() { - return Err(ImageHardenError::WebPError( - format!("WebP size mismatch: declared {} bytes, got {} bytes", - declared_size + 8, data.len()) - )); + return Err(ImageHardenError::WebPError(format!( + "WebP size mismatch: declared {} bytes, got {} bytes", + declared_size + 8, + data.len() + ))); } // Enforce reasonable file size limit (50 MB) const MAX_WEBP_FILE_SIZE: usize = 50 * 1024 * 1024; if data.len() > MAX_WEBP_FILE_SIZE { - return Err(ImageHardenError::WebPError( - format!("WebP file too large: {} bytes (max: {})", data.len(), MAX_WEBP_FILE_SIZE) - )); + return Err(ImageHardenError::WebPError(format!( + "WebP file too large: {} bytes (max: {})", + data.len(), + MAX_WEBP_FILE_SIZE + ))); } // Decode with webp crate let decoder = Decoder::new(data); - let decoded = decoder.decode() + let decoded = decoder + .decode() .ok_or_else(|| ImageHardenError::WebPError("WebP decoding failed".to_string()))?; // Validate dimensions - const MAX_WEBP_DIMENSION: u32 = 16384; // 16K max dimension + const MAX_WEBP_DIMENSION: u32 = 16384; // 16K max dimension if decoded.width() > MAX_WEBP_DIMENSION || decoded.height() > MAX_WEBP_DIMENSION { - return Err(ImageHardenError::WebPError( - format!("WebP dimensions too large: {}x{} (max: {}x{})", - decoded.width(), decoded.height(), MAX_WEBP_DIMENSION, MAX_WEBP_DIMENSION) - )); + return Err(ImageHardenError::WebPError(format!( + "WebP dimensions too large: {}x{} (max: {}x{})", + decoded.width(), + decoded.height(), + MAX_WEBP_DIMENSION, + MAX_WEBP_DIMENSION + ))); } // Return raw RGBA data @@ -457,60 +509,71 @@ pub fn decode_webp(data: &[u8]) -> Result, ImageHardenError> { // HEIF/HEIC decoder (Apple iOS/macOS format) // HEIF uses complex codec chains and requires careful validation pub fn decode_heif(data: &[u8]) -> Result, ImageHardenError> { - use libheif_rs::{HeifContext, RgbChroma, ColorSpace}; + use libheif_rs::{ColorSpace, HeifContext, RgbChroma}; // Validate HEIF signature (ISO Base Media File Format) if data.len() < 12 { return Err(ImageHardenError::HeifError("File too small".to_string())); } if &data[4..8] != b"ftyp" { - return Err(ImageHardenError::HeifError("Invalid HEIF signature: missing ftyp box".to_string())); + return Err(ImageHardenError::HeifError( + "Invalid HEIF signature: missing ftyp box".to_string(), + )); } // Check for Apple brand codes (heic, heix, mif1, msf1, hevc, hevx) let brand = &data[8..12]; let valid_brands = [b"heic", b"heix", b"mif1", b"msf1", b"hevc", b"hevx"]; if !valid_brands.iter().any(|&b| brand == b) { - return Err(ImageHardenError::HeifError( - format!("Unsupported HEIF brand: {:?}", std::str::from_utf8(brand).unwrap_or("invalid")) - )); + return Err(ImageHardenError::HeifError(format!( + "Unsupported HEIF brand: {:?}", + std::str::from_utf8(brand).unwrap_or("invalid") + ))); } // Enforce reasonable file size limit (100 MB) const MAX_HEIF_FILE_SIZE: usize = 100 * 1024 * 1024; if data.len() > MAX_HEIF_FILE_SIZE { - return Err(ImageHardenError::HeifError( - format!("HEIF file too large: {} bytes (max: {})", data.len(), MAX_HEIF_FILE_SIZE) - )); + return Err(ImageHardenError::HeifError(format!( + "HEIF file too large: {} bytes (max: {})", + data.len(), + MAX_HEIF_FILE_SIZE + ))); } // Create context and read from memory - let ctx = HeifContext::read_from_bytes(data) - .map_err(|e| ImageHardenError::HeifError(format!("Failed to read HEIF context: {:?}", e)))?; + let ctx = HeifContext::read_from_bytes(data).map_err(|e| { + ImageHardenError::HeifError(format!("Failed to read HEIF context: {:?}", e)) + })?; // Get primary image handle - let handle = ctx.primary_image_handle() - .map_err(|e| ImageHardenError::HeifError(format!("Failed to get primary image: {:?}", e)))?; + let handle = ctx.primary_image_handle().map_err(|e| { + ImageHardenError::HeifError(format!("Failed to get primary image: {:?}", e)) + })?; // Validate dimensions - const MAX_HEIF_DIMENSION: u32 = 16384; // 16K max dimension + const MAX_HEIF_DIMENSION: u32 = 16384; // 16K max dimension let width = handle.width() as u32; let height = handle.height() as u32; if width > MAX_HEIF_DIMENSION || height > MAX_HEIF_DIMENSION { - return Err(ImageHardenError::HeifError( - format!("HEIF dimensions too large: {}x{} (max: {}x{})", - width, height, MAX_HEIF_DIMENSION, MAX_HEIF_DIMENSION) - )); + return Err(ImageHardenError::HeifError(format!( + "HEIF dimensions too large: {}x{} (max: {}x{})", + width, height, MAX_HEIF_DIMENSION, MAX_HEIF_DIMENSION + ))); } // Decode image to RGB - let image = handle.decode(ColorSpace::Rgb(RgbChroma::Rgb), None) - .map_err(|e| ImageHardenError::HeifError(format!("Failed to decode HEIF image: {:?}", e)))?; + let image = handle + .decode(ColorSpace::Rgb(RgbChroma::Rgb), None) + .map_err(|e| { + ImageHardenError::HeifError(format!("Failed to decode HEIF image: {:?}", e)) + })?; // Get image planes and convert to raw bytes let planes = image.planes(); - let interleaved = planes.interleaved + let interleaved = planes + .interleaved .ok_or_else(|| ImageHardenError::HeifError("No interleaved plane data".to_string()))?; Ok(interleaved.data.to_vec()) @@ -519,8 +582,9 @@ pub fn decode_heif(data: &[u8]) -> Result, ImageHardenError> { // SVG wrapper using pure Rust resvg (memory-safe) pub fn decode_svg(data: &[u8]) -> Result, ImageHardenError> { // Sanitize SVG to remove malicious content - let sanitized_svg = clean(std::str::from_utf8(data) - .map_err(|e| ImageHardenError::SvgError(e.to_string()))?).to_string(); + let sanitized_svg = + clean(std::str::from_utf8(data).map_err(|e| ImageHardenError::SvgError(e.to_string()))?) + .to_string(); // Parse SVG with usvg let opt = usvg::Options::default(); @@ -545,7 +609,8 @@ pub fn decode_svg(data: &[u8]) -> Result, ImageHardenError> { resvg::render(&tree, transform, &mut pixmap.as_mut()); // Encode as PNG - pixmap.encode_png() + pixmap + .encode_png() .map_err(|e| ImageHardenError::SvgError(format!("Failed to encode PNG: {:?}", e))) } @@ -567,16 +632,12 @@ pub fn decode_video(data: &[u8], _wasm_path: &str) -> Result, ImageHarde // Return metadata as proof of validation let result = format!( "Video validated: {:?} {}x{} {:.1}s", - metadata.container_format, - metadata.width, - metadata.height, - metadata.duration_secs + metadata.container_format, metadata.width, metadata.height, metadata.duration_secs ); Ok(result.into_bytes()) } - extern "C" fn error_fn(png_ptr: png_structp, error_msg: png_const_charp) { let msg = unsafe { CStr::from_ptr(error_msg).to_string_lossy().into_owned() }; eprintln!("PNG error: {}", msg); @@ -617,18 +678,18 @@ unsafe extern "C" fn jpeg_error_exit(cinfo: j_common_ptr) { // like Telegram, Discord, email attachments, etc. // Security limits for audio decoding -const MAX_AUDIO_FILE_SIZE: usize = 100 * 1024 * 1024; // 100 MB -const MAX_AUDIO_DURATION_SECS: u64 = 600; // 10 minutes -const MAX_SAMPLE_RATE: u32 = 192000; // 192 kHz -const MAX_CHANNELS: u16 = 8; // 8 channels +const MAX_AUDIO_FILE_SIZE: usize = 100 * 1024 * 1024; // 100 MB +const MAX_AUDIO_DURATION_SECS: u64 = 600; // 10 minutes +const MAX_SAMPLE_RATE: u32 = 192000; // 192 kHz +const MAX_CHANNELS: u16 = 8; // 8 channels // Audio sample output format #[derive(Debug, Clone)] pub struct AudioData { - pub samples: Vec, // Interleaved samples - pub sample_rate: u32, // Hz - pub channels: u16, // 1=mono, 2=stereo, etc. - pub duration_secs: f64, // Total duration + pub samples: Vec, // Interleaved samples + pub sample_rate: u32, // Hz + pub channels: u16, // 1=mono, 2=stereo, etc. + pub duration_secs: f64, // Total duration } // MP3 decoder (using minimp3 - Rust wrapper around C minimp3) @@ -638,14 +699,18 @@ pub fn decode_mp3(data: &[u8]) -> Result { // Validate input size if data.len() > MAX_AUDIO_FILE_SIZE { - return Err(ImageHardenError::Mp3Error( - format!("File too large: {} bytes (max: {})", data.len(), MAX_AUDIO_FILE_SIZE) - )); + return Err(ImageHardenError::Mp3Error(format!( + "File too large: {} bytes (max: {})", + data.len(), + MAX_AUDIO_FILE_SIZE + ))); } // Validate MP3 signature (MPEG frame sync) if data.len() < 2 || (data[0] != 0xFF || (data[1] & 0xE0) != 0xE0) { - return Err(ImageHardenError::Mp3Error("Invalid MP3 signature".to_string())); + return Err(ImageHardenError::Mp3Error( + "Invalid MP3 signature".to_string(), + )); } let mut decoder = Decoder::new(data); @@ -656,17 +721,24 @@ pub fn decode_mp3(data: &[u8]) -> Result { loop { match decoder.next_frame() { - Ok(Frame { data: samples, sample_rate: rate, channels: ch, .. }) => { + Ok(Frame { + data: samples, + sample_rate: rate, + channels: ch, + .. + }) => { // Validate audio parameters if rate as u32 > MAX_SAMPLE_RATE { - return Err(ImageHardenError::Mp3Error( - format!("Sample rate too high: {} Hz", rate) - )); + return Err(ImageHardenError::Mp3Error(format!( + "Sample rate too high: {} Hz", + rate + ))); } if ch as u16 > MAX_CHANNELS { - return Err(ImageHardenError::Mp3Error( - format!("Too many channels: {}", ch) - )); + return Err(ImageHardenError::Mp3Error(format!( + "Too many channels: {}", + ch + ))); } // Set format from first frame @@ -679,9 +751,10 @@ pub fn decode_mp3(data: &[u8]) -> Result { total_samples += samples.len(); let duration_secs = total_samples as u64 / (sample_rate as u64 * channels as u64); if duration_secs > MAX_AUDIO_DURATION_SECS { - return Err(ImageHardenError::Mp3Error( - format!("Audio too long: {} seconds (max: {})", duration_secs, MAX_AUDIO_DURATION_SECS) - )); + return Err(ImageHardenError::Mp3Error(format!( + "Audio too long: {} seconds (max: {})", + duration_secs, MAX_AUDIO_DURATION_SECS + ))); } all_samples.extend_from_slice(&samples); @@ -692,7 +765,9 @@ pub fn decode_mp3(data: &[u8]) -> Result { } if all_samples.is_empty() { - return Err(ImageHardenError::Mp3Error("No audio data decoded".to_string())); + return Err(ImageHardenError::Mp3Error( + "No audio data decoded".to_string(), + )); } let duration_secs = all_samples.len() as f64 / (sample_rate as f64 * channels as f64); @@ -711,56 +786,66 @@ pub fn decode_vorbis(data: &[u8]) -> Result { // Validate input size if data.len() > MAX_AUDIO_FILE_SIZE { - return Err(ImageHardenError::VorbisError( - format!("File too large: {} bytes", data.len()) - )); + return Err(ImageHardenError::VorbisError(format!( + "File too large: {} bytes", + data.len() + ))); } // Validate Ogg signature if data.len() < 4 || &data[0..4] != b"OggS" { - return Err(ImageHardenError::VorbisError("Invalid Ogg signature".to_string())); + return Err(ImageHardenError::VorbisError( + "Invalid Ogg signature".to_string(), + )); } let cursor = std::io::Cursor::new(data); - let mut reader = OggStreamReader::new(cursor) - .map_err(|e| ImageHardenError::VorbisError(format!("Failed to initialize reader: {:?}", e)))?; + let mut reader = OggStreamReader::new(cursor).map_err(|e| { + ImageHardenError::VorbisError(format!("Failed to initialize reader: {:?}", e)) + })?; // Validate audio parameters let sample_rate = reader.ident_hdr.audio_sample_rate; let channels = reader.ident_hdr.audio_channels as u16; if sample_rate > MAX_SAMPLE_RATE { - return Err(ImageHardenError::VorbisError( - format!("Sample rate too high: {} Hz", sample_rate) - )); + return Err(ImageHardenError::VorbisError(format!( + "Sample rate too high: {} Hz", + sample_rate + ))); } if channels > MAX_CHANNELS { - return Err(ImageHardenError::VorbisError( - format!("Too many channels: {}", channels) - )); + return Err(ImageHardenError::VorbisError(format!( + "Too many channels: {}", + channels + ))); } let mut all_samples = Vec::new(); let mut total_samples = 0usize; - while let Some(packet) = reader.read_dec_packet_itl() - .map_err(|e| ImageHardenError::VorbisError(format!("Decode error: {:?}", e)))? { - + while let Some(packet) = reader + .read_dec_packet_itl() + .map_err(|e| ImageHardenError::VorbisError(format!("Decode error: {:?}", e)))? + { total_samples += packet.len(); // Check duration limit let duration_secs = total_samples as u64 / (sample_rate as u64 * channels as u64); if duration_secs > MAX_AUDIO_DURATION_SECS { - return Err(ImageHardenError::VorbisError( - format!("Audio too long: {} seconds", duration_secs) - )); + return Err(ImageHardenError::VorbisError(format!( + "Audio too long: {} seconds", + duration_secs + ))); } all_samples.extend_from_slice(&packet); } if all_samples.is_empty() { - return Err(ImageHardenError::VorbisError("No audio data decoded".to_string())); + return Err(ImageHardenError::VorbisError( + "No audio data decoded".to_string(), + )); } let duration_secs = all_samples.len() as f64 / (sample_rate as f64 * channels as f64); @@ -779,32 +864,38 @@ pub fn decode_flac(data: &[u8]) -> Result { // Validate input size if data.len() > MAX_AUDIO_FILE_SIZE { - return Err(ImageHardenError::FlacError( - format!("File too large: {} bytes", data.len()) - )); + return Err(ImageHardenError::FlacError(format!( + "File too large: {} bytes", + data.len() + ))); } // Validate FLAC signature if data.len() < 4 || &data[0..4] != b"fLaC" { - return Err(ImageHardenError::FlacError("Invalid FLAC signature".to_string())); + return Err(ImageHardenError::FlacError( + "Invalid FLAC signature".to_string(), + )); } let cursor = std::io::Cursor::new(data); - let mut reader = FlacReader::new(cursor) - .map_err(|e| ImageHardenError::FlacError(format!("Failed to initialize reader: {:?}", e)))?; + let mut reader = FlacReader::new(cursor).map_err(|e| { + ImageHardenError::FlacError(format!("Failed to initialize reader: {:?}", e)) + })?; let streaminfo = reader.streaminfo(); // Validate audio parameters if streaminfo.sample_rate > MAX_SAMPLE_RATE { - return Err(ImageHardenError::FlacError( - format!("Sample rate too high: {} Hz", streaminfo.sample_rate) - )); + return Err(ImageHardenError::FlacError(format!( + "Sample rate too high: {} Hz", + streaminfo.sample_rate + ))); } if streaminfo.channels as u16 > MAX_CHANNELS { - return Err(ImageHardenError::FlacError( - format!("Too many channels: {}", streaminfo.channels) - )); + return Err(ImageHardenError::FlacError(format!( + "Too many channels: {}", + streaminfo.channels + ))); } let mut all_samples = Vec::new(); @@ -812,8 +903,8 @@ pub fn decode_flac(data: &[u8]) -> Result { let mut sample_count = 0usize; while let Some(sample) = samples.next() { - let sample = sample - .map_err(|e| ImageHardenError::FlacError(format!("Decode error: {:?}", e)))?; + let sample = + sample.map_err(|e| ImageHardenError::FlacError(format!("Decode error: {:?}", e)))?; // Convert to i16 (FLAC can have various bit depths) let sample_i16 = if streaminfo.bits_per_sample <= 16 { @@ -826,19 +917,24 @@ pub fn decode_flac(data: &[u8]) -> Result { sample_count += 1; // Check duration limit - let duration_secs = sample_count as u64 / (streaminfo.sample_rate as u64 * streaminfo.channels as u64); + let duration_secs = + sample_count as u64 / (streaminfo.sample_rate as u64 * streaminfo.channels as u64); if duration_secs > MAX_AUDIO_DURATION_SECS { - return Err(ImageHardenError::FlacError( - format!("Audio too long: {} seconds", duration_secs) - )); + return Err(ImageHardenError::FlacError(format!( + "Audio too long: {} seconds", + duration_secs + ))); } } if all_samples.is_empty() { - return Err(ImageHardenError::FlacError("No audio data decoded".to_string())); + return Err(ImageHardenError::FlacError( + "No audio data decoded".to_string(), + )); } - let duration_secs = all_samples.len() as f64 / (streaminfo.sample_rate as f64 * streaminfo.channels as f64); + let duration_secs = + all_samples.len() as f64 / (streaminfo.sample_rate as f64 * streaminfo.channels as f64); Ok(AudioData { samples: all_samples, @@ -862,7 +958,9 @@ pub fn decode_audio(data: &[u8]) -> Result { } else if data.len() >= 2 && data[0] == 0xFF && (data[1] & 0xE0) == 0xE0 { decode_mp3(data) } else { - Err(ImageHardenError::AudioError("Unknown audio format".to_string())) + Err(ImageHardenError::AudioError( + "Unknown audio format".to_string(), + )) } } @@ -887,13 +985,13 @@ pub fn decode_audio(data: &[u8]) -> Result { // 6. Rate-limit and resource-bound all operations // Security limits for video validation -const MAX_VIDEO_FILE_SIZE: usize = 500 * 1024 * 1024; // 500 MB -const MAX_VIDEO_DURATION_SECS: u64 = 3600; // 1 hour -const MAX_VIDEO_WIDTH: u32 = 3840; // 4K width -const MAX_VIDEO_HEIGHT: u32 = 2160; // 4K height -const MAX_VIDEO_FRAMERATE: u32 = 120; // 120 fps -const MAX_VIDEO_BITRATE: u64 = 50_000_000; // 50 Mbps -const MAX_VIDEO_TRACKS: usize = 8; // Max audio/video/subtitle tracks +const MAX_VIDEO_FILE_SIZE: usize = 500 * 1024 * 1024; // 500 MB +const MAX_VIDEO_DURATION_SECS: u64 = 3600; // 1 hour +const MAX_VIDEO_WIDTH: u32 = 3840; // 4K width +const MAX_VIDEO_HEIGHT: u32 = 2160; // 4K height +const MAX_VIDEO_FRAMERATE: u32 = 120; // 120 fps +const MAX_VIDEO_BITRATE: u64 = 50_000_000; // 50 Mbps +const MAX_VIDEO_TRACKS: usize = 8; // Max audio/video/subtitle tracks #[derive(Debug, Clone)] pub struct VideoMetadata { @@ -919,14 +1017,16 @@ pub enum VideoContainerFormat { pub fn validate_video_container(data: &[u8]) -> Result { // File size check if data.len() > MAX_VIDEO_FILE_SIZE { - return Err(ImageHardenError::VideoValidationError( - format!("Video file too large: {} bytes (max: {})", data.len(), MAX_VIDEO_FILE_SIZE) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Video file too large: {} bytes (max: {})", + data.len(), + MAX_VIDEO_FILE_SIZE + ))); } if data.len() < 12 { return Err(ImageHardenError::VideoValidationError( - "Video file too small".to_string() + "Video file too small".to_string(), )); } @@ -938,7 +1038,7 @@ pub fn validate_video_container(data: &[u8]) -> Result validate_mkv_container(data), VideoContainerFormat::AVI => validate_avi_container(data), VideoContainerFormat::Unknown => Err(ImageHardenError::VideoValidationError( - "Unknown or unsupported video container format".to_string() + "Unknown or unsupported video container format".to_string(), )), } } @@ -982,16 +1082,17 @@ fn validate_mp4_container(data: &[u8]) -> Result MAX_VIDEO_TRACKS { - return Err(ImageHardenError::VideoValidationError( - format!("Too many tracks: {} (max: {})", context.tracks.len(), MAX_VIDEO_TRACKS) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Too many tracks: {} (max: {})", + context.tracks.len(), + MAX_VIDEO_TRACKS + ))); } let mut video_tracks = 0; @@ -1007,18 +1108,20 @@ fn validate_mp4_container(data: &[u8]) -> Result> 16; // Fixed-point to integer + let width = tkhd.width >> 16; // Fixed-point to integer let height = tkhd.height >> 16; if width > MAX_VIDEO_WIDTH { - return Err(ImageHardenError::VideoValidationError( - format!("Video width too large: {} (max: {})", width, MAX_VIDEO_WIDTH) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Video width too large: {} (max: {})", + width, MAX_VIDEO_WIDTH + ))); } if height > MAX_VIDEO_HEIGHT { - return Err(ImageHardenError::VideoValidationError( - format!("Video height too large: {} (max: {})", height, MAX_VIDEO_HEIGHT) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Video height too large: {} (max: {})", + height, MAX_VIDEO_HEIGHT + ))); } max_width = max_width.max(width); @@ -1036,10 +1139,10 @@ fn validate_mp4_container(data: &[u8]) -> Result MAX_VIDEO_DURATION_SECS as f64 { - return Err(ImageHardenError::VideoValidationError( - format!("Video too long: {:.1} seconds (max: {})", - duration_secs, MAX_VIDEO_DURATION_SECS) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Video too long: {:.1} seconds (max: {})", + duration_secs, MAX_VIDEO_DURATION_SECS + ))); } } } @@ -1053,7 +1156,7 @@ fn validate_mp4_container(data: &[u8]) -> Result Result Result MAX_VIDEO_TRACKS { - return Err(ImageHardenError::VideoValidationError( - format!("Too many tracks: {} (max: {})", - video_tracks + audio_tracks, MAX_VIDEO_TRACKS) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Too many tracks: {} (max: {})", + video_tracks + audio_tracks, + MAX_VIDEO_TRACKS + ))); } if video_tracks == 0 { return Err(ImageHardenError::VideoValidationError( - "No video tracks found in MKV/WebM".to_string() + "No video tracks found in MKV/WebM".to_string(), )); } @@ -1125,10 +1228,10 @@ fn validate_mkv_container(data: &[u8]) -> Result MAX_VIDEO_DURATION_SECS as f64 { - return Err(ImageHardenError::VideoValidationError( - format!("MKV video too long: {:.1} seconds (max: {})", - duration_secs, MAX_VIDEO_DURATION_SECS) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "MKV video too long: {:.1} seconds (max: {})", + duration_secs, MAX_VIDEO_DURATION_SECS + ))); } Ok(VideoMetadata { @@ -1149,7 +1252,7 @@ fn validate_avi_container(data: &[u8]) -> Result Result Result data.len() { - break; // Chunk extends past file end + break; // Chunk extends past file end } if chunk_id == b"avih" && chunk_size >= 56 { found_avih = true; // Parse AVI main header (56 bytes minimum) - let header_data = &data[pos+8..pos+8+56]; + let header_data = &data[pos + 8..pos + 8 + 56]; duration_microsecs = u32::from_le_bytes([ - header_data[0], header_data[1], header_data[2], header_data[3] + header_data[0], + header_data[1], + header_data[2], + header_data[3], ]); width = u32::from_le_bytes([ - header_data[32], header_data[33], header_data[34], header_data[35] + header_data[32], + header_data[33], + header_data[34], + header_data[35], ]); height = u32::from_le_bytes([ - header_data[36], header_data[37], header_data[38], header_data[39] + header_data[36], + header_data[37], + header_data[38], + header_data[39], ]); // Validate dimensions if width > MAX_VIDEO_WIDTH { - return Err(ImageHardenError::VideoValidationError( - format!("AVI width too large: {} (max: {})", width, MAX_VIDEO_WIDTH) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "AVI width too large: {} (max: {})", + width, MAX_VIDEO_WIDTH + ))); } if height > MAX_VIDEO_HEIGHT { - return Err(ImageHardenError::VideoValidationError( - format!("AVI height too large: {} (max: {})", height, MAX_VIDEO_HEIGHT) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "AVI height too large: {} (max: {})", + height, MAX_VIDEO_HEIGHT + ))); } break; @@ -1219,17 +1334,17 @@ fn validate_avi_container(data: &[u8]) -> Result MAX_VIDEO_DURATION_SECS as f64 { - return Err(ImageHardenError::VideoValidationError( - format!("AVI video too long: {:.1} seconds (max: {})", - duration_secs, MAX_VIDEO_DURATION_SECS) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "AVI video too long: {:.1} seconds (max: {})", + duration_secs, MAX_VIDEO_DURATION_SECS + ))); } Ok(VideoMetadata { @@ -1237,8 +1352,8 @@ fn validate_avi_container(data: &[u8]) -> Result