mod audio;
mod image;
mod video;
pub use audio::Audio;
pub use image::Image;
pub use video::Video;
use sha1::{Digest, Sha1};
pub(super) fn compute_sha1(data: &[u8]) -> String {
let mut hasher = Sha1::new();
hasher.update(data);
let result = hasher.finalize();
hex_encode(&result)
}
fn hex_encode(bytes: &[u8]) -> String {
use std::fmt::Write;
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(s, "{b:02x}");
}
s
}
pub(super) fn detect_format_from_bytes(data: &[u8]) -> Option<(&'static str, &'static str)> {
if is_svg_data(data) {
return Some(("svg", "image/svg+xml"));
}
if data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
return Some(("png", "image/png"));
}
if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
return Some(("jpg", "image/jpeg"));
}
if data.starts_with(b"GIF8") {
return Some(("gif", "image/gif"));
}
if data.starts_with(b"BM") {
return Some(("bmp", "image/bmp"));
}
if data.starts_with(&[0x49, 0x49, 0x2A, 0x00]) {
return Some(("tiff", "image/tiff"));
}
if data.starts_with(&[0x4D, 0x4D, 0x00, 0x2A]) {
return Some(("tiff", "image/tiff"));
}
if data.len() >= 12 && data.starts_with(b"RIFF") && &data[8..12] == b"WEBP" {
return Some(("webp", "image/webp"));
}
if data.len() >= 44 && data.starts_with(&[0x01, 0x00, 0x00, 0x00]) && &data[40..44] == b" EMF" {
return Some(("emf", "image/x-emf"));
}
if data.starts_with(&[0xD7, 0xCD, 0xC6, 0x9A]) {
return Some(("wmf", "image/x-wmf"));
}
None
}
fn is_svg_data(data: &[u8]) -> bool {
let trimmed = if data.starts_with(&[0xEF, 0xBB, 0xBF]) {
&data[3..]
} else {
data
};
let trimmed = trimmed
.iter()
.position(|&b| !b.is_ascii_whitespace())
.map_or(trimmed, |pos| &trimmed[pos..]);
if trimmed.starts_with(b"<svg") || trimmed.starts_with(b"<SVG") {
return true;
}
if trimmed.starts_with(b"<?xml") {
let check_len = trimmed.len().min(1024);
let check = &trimmed[..check_len];
if let Ok(s) = std::str::from_utf8(check) {
let lower = s.to_lowercase();
return lower.contains("<svg");
}
}
false
}
pub(super) fn ext_to_content_type(ext: &str) -> Option<&'static str> {
match ext {
"png" => Some("image/png"),
"jpg" | "jpeg" | "jpe" => Some("image/jpeg"),
"gif" => Some("image/gif"),
"bmp" => Some("image/bmp"),
"tif" | "tiff" => Some("image/tiff"),
"emf" => Some("image/x-emf"),
"wmf" => Some("image/x-wmf"),
"svg" => Some("image/svg+xml"),
"webp" => Some("image/webp"),
_ => None,
}
}
pub(super) fn content_type_to_ext(ct: &str) -> Option<&'static str> {
match ct {
"image/png" => Some("png"),
"image/jpeg" => Some("jpg"),
"image/gif" => Some("gif"),
"image/bmp" => Some("bmp"),
"image/tiff" => Some("tiff"),
"image/x-emf" => Some("emf"),
"image/x-wmf" => Some("wmf"),
"image/svg+xml" => Some("svg"),
"image/webp" => Some("webp"),
_ => None,
}
}
pub(super) fn video_ext_to_content_type(ext: &str) -> Option<&'static str> {
match ext {
"mp4" => Some("video/mp4"),
"mov" => Some("video/quicktime"),
"avi" => Some("video/x-msvideo"),
"wmv" => Some("video/x-ms-wmv"),
_ => None,
}
}
pub(super) fn video_content_type_to_ext(ct: &str) -> Option<&'static str> {
match ct {
"video/mp4" => Some("mp4"),
"video/quicktime" => Some("mov"),
"video/x-msvideo" => Some("avi"),
"video/x-ms-wmv" => Some("wmv"),
_ => None,
}
}
pub(super) fn audio_ext_to_content_type(ext: &str) -> Option<&'static str> {
match ext {
"mp3" => Some("audio/mpeg"),
"wav" => Some("audio/wav"),
"m4a" => Some("audio/mp4"),
_ => None,
}
}
pub(super) fn audio_content_type_to_ext(ct: &str) -> Option<&'static str> {
match ct {
"audio/mpeg" => Some("mp3"),
"audio/wav" => Some("wav"),
"audio/mp4" => Some("m4a"),
_ => None,
}
}
#[cfg(test)]
mod tests;