refactor(media-proxy): split into modules and harden streaming (#2437)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-03 22:04:00 +02:00
1 parent 16ae98e189
commit d7d4e8da03
214 files changed
+47213 -18092

No files matched your search

@@ -0,0 +1,367 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::SniffInfo;
use crate::media_type::MediaType;
const WEBP_CHUNK_HEADER_BYTES: usize = 20;
const WEBP_EXTENDED_HEADER_BYTES: usize = 30;
const WEBP_LOSSLESS_HEADER_BYTES: usize = 25;
const GIF_LOGICAL_SCREEN_BYTES: usize = 13;
const GIF_IMAGE_DESCRIPTOR_BYTES: usize = 9;
const GIF_STRUCTURE_BLOCK_LIMIT: usize = 262_144;
const PNG_SNIFF_CHUNK_LIMIT: usize = 262_144;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum GIFAnimation {
Incomplete,
Static,
Animated,
Invalid,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum PNGAnimation {
Incomplete,
Static,
Animated(u32),
Invalid,
}
fn gif_color_table_end(data: &[u8], offset: usize, packed: u8) -> Option<usize> {
assert!(offset <= data.len());
if packed & 0x80 == 0 {
return Some(offset);
}
let entries = 1usize << ((packed & 0x07) + 1);
let table_bytes = entries
.checked_mul(3)
.expect("GIF color table byte count must fit usize");
let end = offset.checked_add(table_bytes)?;
(end <= data.len()).then_some(end)
}
fn gif_u16(data: &[u8], offset: usize) -> u16 {
assert!(offset <= data.len());
assert!(data.len() - offset >= 2);
u16::from_le_bytes(
data[offset..offset + 2]
.try_into()
.expect("validated GIF u16 slice"),
)
}
fn gif_validate_image_geometry(
data: &[u8],
offset: usize,
canvas_width: u16,
canvas_height: u16,
) -> Result<(), GIFAnimation> {
assert!(offset <= data.len());
assert!(data.len() - offset >= GIF_IMAGE_DESCRIPTOR_BYTES);
let left = gif_u16(data, offset);
let top = gif_u16(data, offset + 2);
let width = gif_u16(data, offset + 4);
let height = gif_u16(data, offset + 6);
if width == 0 {
return Err(GIFAnimation::Invalid);
}
if height == 0 {
return Err(GIFAnimation::Invalid);
}
if u32::from(left) + u32::from(width) > u32::from(canvas_width) {
return Err(GIFAnimation::Invalid);
}
if u32::from(top) + u32::from(height) > u32::from(canvas_height) {
return Err(GIFAnimation::Invalid);
}
Ok(())
}
fn gif_skip_sub_blocks(
data: &[u8],
mut offset: usize,
block_count: &mut usize,
) -> Result<usize, GIFAnimation> {
assert!(offset <= data.len());
loop {
if *block_count >= GIF_STRUCTURE_BLOCK_LIMIT {
return Err(GIFAnimation::Invalid);
}
let Some(&length) = data.get(offset) else {
return Err(GIFAnimation::Incomplete);
};
*block_count += 1;
offset += 1;
if length == 0 {
return Ok(offset);
}
let Some(end) = offset.checked_add(usize::from(length)) else {
return Err(GIFAnimation::Invalid);
};
if end > data.len() {
return Err(GIFAnimation::Incomplete);
}
offset = end;
}
}
fn gif_skip_image(
data: &[u8],
offset: usize,
canvas_width: u16,
canvas_height: u16,
block_count: &mut usize,
) -> Result<usize, GIFAnimation> {
assert!(offset <= data.len());
let Some(descriptor_end) = offset.checked_add(GIF_IMAGE_DESCRIPTOR_BYTES) else {
return Err(GIFAnimation::Invalid);
};
if descriptor_end > data.len() {
return Err(GIFAnimation::Incomplete);
}
gif_validate_image_geometry(data, offset, canvas_width, canvas_height)?;
let packed = data[offset + 8];
let Some(table_end) = gif_color_table_end(data, descriptor_end, packed) else {
return Err(GIFAnimation::Incomplete);
};
let Some(&minimum_code_size) = data.get(table_end) else {
return Err(GIFAnimation::Incomplete);
};
if !(2..=8).contains(&minimum_code_size) {
return Err(GIFAnimation::Invalid);
}
gif_skip_sub_blocks(data, table_end + 1, block_count)
}
pub(super) fn gif_animation(data: &[u8]) -> GIFAnimation {
if data.len() < 6 {
return GIFAnimation::Incomplete;
}
match &data[..6] {
b"GIF87a" | b"GIF89a" => {}
_ => return GIFAnimation::Invalid,
}
if data.len() < GIF_LOGICAL_SCREEN_BYTES {
return GIFAnimation::Incomplete;
}
let canvas_width = gif_u16(data, 6);
let canvas_height = gif_u16(data, 8);
if canvas_width == 0 {
return GIFAnimation::Invalid;
}
if canvas_height == 0 {
return GIFAnimation::Invalid;
}
let Some(mut offset) = gif_color_table_end(data, GIF_LOGICAL_SCREEN_BYTES, data[10]) else {
return GIFAnimation::Incomplete;
};
let mut image_count = 0usize;
let mut block_count = 0usize;
loop {
if block_count >= GIF_STRUCTURE_BLOCK_LIMIT {
return GIFAnimation::Invalid;
}
let Some(&introducer) = data.get(offset) else {
return GIFAnimation::Incomplete;
};
block_count += 1;
offset += 1;
match introducer {
0x3b if image_count == 1 => return GIFAnimation::Static,
0x3b => return GIFAnimation::Invalid,
0x21 => {
if data.get(offset).is_none() {
return GIFAnimation::Incomplete;
}
offset = match gif_skip_sub_blocks(data, offset + 1, &mut block_count) {
Ok(next) => next,
Err(outcome) => return outcome,
};
}
0x2c => {
offset = match gif_skip_image(
data,
offset,
canvas_width,
canvas_height,
&mut block_count,
) {
Ok(next) => next,
Err(outcome) => return outcome,
};
image_count += 1;
if image_count == 2 {
return GIFAnimation::Animated;
}
}
_ => return GIFAnimation::Invalid,
}
}
}
pub(super) fn png_animation(data: &[u8]) -> PNGAnimation {
if data.len() < 8 {
return PNGAnimation::Incomplete;
}
if &data[..8] != b"\x89PNG\r\n\x1a\n" {
return PNGAnimation::Invalid;
}
let mut offset = 8usize;
let mut first_chunk = true;
let mut chunk_count = 0usize;
loop {
if chunk_count >= PNG_SNIFF_CHUNK_LIMIT {
return PNGAnimation::Invalid;
}
let Some(length_end) = offset.checked_add(4) else {
return PNGAnimation::Invalid;
};
let Some(header_end) = offset.checked_add(8) else {
return PNGAnimation::Invalid;
};
if header_end > data.len() {
break;
}
chunk_count += 1;
let length_bytes = data[offset..length_end]
.try_into()
.expect("validated PNG chunk length slice");
let length = usize::try_from(u32::from_be_bytes(length_bytes))
.expect("PNG chunk length must fit the configured platform");
let kind = &data[length_end..header_end];
if first_chunk {
if kind != b"IHDR" {
return PNGAnimation::Invalid;
}
if length != 13 {
return PNGAnimation::Invalid;
}
}
first_chunk = false;
if kind == b"acTL" {
if length != 8 {
return PNGAnimation::Invalid;
}
let Some(chunk_end) = offset.checked_add(16) else {
return PNGAnimation::Invalid;
};
if chunk_end > data.len() {
return PNGAnimation::Incomplete;
}
let frame_end = header_end
.checked_add(4)
.expect("validated PNG animation frame count end");
let frame_bytes = data[header_end..frame_end]
.try_into()
.expect("validated PNG animation frame count slice");
let frames = u32::from_be_bytes(frame_bytes);
return if frames > 0 {
PNGAnimation::Animated(frames)
} else {
PNGAnimation::Invalid
};
}
match kind {
b"IDAT" | b"IEND" => return PNGAnimation::Static,
_ => {}
}
let Some(next) = offset
.checked_add(12)
.and_then(|next| next.checked_add(length))
else {
return PNGAnimation::Invalid;
};
if next > data.len() {
return PNGAnimation::Incomplete;
}
offset = next;
}
PNGAnimation::Incomplete
}
fn webp_u24(data: &[u8]) -> u32 {
u32::from(data[0]) | (u32::from(data[1]) << 8) | (u32::from(data[2]) << 16)
}
pub(super) fn webp_sniff(data: &[u8]) -> SniffInfo {
let mut out = SniffInfo {
mime: MediaType::WebP.mime(),
..Default::default()
};
if data.len() < WEBP_CHUNK_HEADER_BYTES {
return out;
}
match &data[12..16] {
b"VP8X" => {
let chunk_size = u32::from_le_bytes(
data[16..20]
.try_into()
.expect("validated WebP chunk size slice"),
);
if chunk_size != 10 || data.len() < WEBP_EXTENDED_HEADER_BYTES {
return out;
}
let flags = data[20];
out.animated = flags & 0x02 != 0;
out.frames = if out.animated { 2 } else { 1 };
out.has_alpha = flags & 0x10 != 0;
out.width = webp_u24(&data[24..27])
.checked_add(1)
.expect("24-bit WebP width must fit u32");
out.height = webp_u24(&data[27..30])
.checked_add(1)
.expect("24-bit WebP height must fit u32");
}
b"VP8 " if data.len() >= WEBP_EXTENDED_HEADER_BYTES => {
if &data[23..26] == b"\x9d\x01\x2a" {
out.width = u32::from(
u16::from_le_bytes(
data[26..28].try_into().expect("validated WebP width slice"),
) & 0x3fff,
);
out.height = u32::from(
u16::from_le_bytes(
data[28..30]
.try_into()
.expect("validated WebP height slice"),
) & 0x3fff,
);
}
}
b"VP8L" if data.len() >= WEBP_LOSSLESS_HEADER_BYTES && data[20] == 0x2f => {
let bits = u32::from_le_bytes(
data[21..25]
.try_into()
.expect("validated lossless WebP dimensions slice"),
);
out.width = (bits & 0x3fff)
.checked_add(1)
.expect("14-bit WebP width must fit u32");
out.height = ((bits >> 14) & 0x3fff)
.checked_add(1)
.expect("14-bit WebP height must fit u32");
out.has_alpha = bits & (1 << 28) != 0;
}
_ => {}
}
out
}
pub(super) fn webp_sniff_complete(data: &[u8]) -> bool {
if data.len() < WEBP_CHUNK_HEADER_BYTES {
return false;
}
match &data[12..16] {
b"VP8X" => {
let chunk_size = u32::from_le_bytes(
data[16..20]
.try_into()
.expect("validated WebP animation chunk size slice"),
);
chunk_size != 10 || data.len() >= WEBP_EXTENDED_HEADER_BYTES
}
b"VP8 " => data.len() >= WEBP_EXTENDED_HEADER_BYTES,
b"VP8L" => data.len() >= WEBP_LOSSLESS_HEADER_BYTES,
_ => true,
}
}
+192
View File
@@ -0,0 +1,192 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::SniffInfo;
use crate::media_type::MediaType;
const ISO_BMFF_BRAND_SCAN_BYTES: usize = 128;
const ISO_BMFF_BASE_HEADER_BYTES: usize = 8;
const ISO_BMFF_EXTENDED_HEADER_BYTES: usize = 16;
const ISO_BMFF_FTYP_FIELDS_BYTES: usize = 8;
struct FTYPLayout {
major_brand_offset: usize,
compatible_brands_offset: usize,
scan_end: usize,
}
#[derive(Default)]
struct ISOBMFFBrandSet {
avif: bool,
animated_avif: bool,
heic: bool,
heif: bool,
audio_mp4: bool,
mp4: bool,
quicktime: bool,
three_gpp: bool,
}
impl ISOBMFFBrandSet {
fn record(&mut self, brand: &[u8]) {
if matches!(brand, b"avif" | b"avis" | b"avio") {
self.avif = true;
if brand == b"avis" {
self.animated_avif = true;
}
}
if matches!(
brand,
b"heic" | b"heix" | b"heif" | b"heim" | b"heis" | b"hevc" | b"hevx" | b"hevm" | b"hevs"
) {
self.heic = true;
}
if matches!(brand, b"mif1" | b"msf1") {
self.heif = true;
}
if matches!(
brand,
b"mp41"
| b"mp42"
| b"isom"
| b"iso2"
| b"iso3"
| b"iso4"
| b"iso5"
| b"iso6"
| b"M4V "
| b"M4P "
| b"dash"
| b"msdh"
| b"msix"
| b"mj2s"
) {
self.mp4 = true;
}
if matches!(brand, b"M4A " | b"M4B " | b"M4P ") {
self.audio_mp4 = true;
}
if brand == b"qt " {
self.quicktime = true;
}
if brand.starts_with(b"3gp") {
self.three_gpp = true;
}
if brand.starts_with(b"3g2") {
self.three_gpp = true;
}
}
fn resolve(self) -> Option<SniffInfo> {
if self.avif {
return Some(SniffInfo {
mime: MediaType::AVIF.mime(),
animated: self.animated_avif,
..Default::default()
});
}
if self.heic {
return Some(SniffInfo {
mime: MediaType::HEIC.mime(),
..Default::default()
});
}
if self.heif {
return Some(SniffInfo {
mime: MediaType::HEIF.mime(),
..Default::default()
});
}
if self.quicktime {
return Some(SniffInfo {
mime: MediaType::QuickTimeVideo.mime(),
..Default::default()
});
}
if self.three_gpp {
return Some(SniffInfo {
mime: MediaType::ThreeGPPVideo.mime(),
..Default::default()
});
}
if self.audio_mp4 {
return Some(SniffInfo {
mime: MediaType::MP4Audio.mime(),
..Default::default()
});
}
if self.mp4 {
return Some(SniffInfo {
mime: MediaType::MP4Video.mime(),
..Default::default()
});
}
None
}
}
fn ftyp_layout(data: &[u8]) -> Option<FTYPLayout> {
if data.len() < ISO_BMFF_BASE_HEADER_BYTES {
return None;
}
if &data[4..8] != b"ftyp" {
return None;
}
let size32 = u32::from_be_bytes(
data[0..4]
.try_into()
.expect("validated ISO BMFF box size slice"),
);
let (header_bytes, declared_box_size) = if size32 == 1 {
if data.len() < ISO_BMFF_EXTENDED_HEADER_BYTES {
return None;
}
let size64 = u64::from_be_bytes(
data[8..16]
.try_into()
.expect("validated extended ISO BMFF box size slice"),
);
let box_size = usize::try_from(size64).ok()?;
(ISO_BMFF_EXTENDED_HEADER_BYTES, Some(box_size))
} else if size32 == 0 {
(ISO_BMFF_BASE_HEADER_BYTES, None)
} else {
let box_size = usize::try_from(size32).expect("u32 ISO BMFF box size must fit usize");
(ISO_BMFF_BASE_HEADER_BYTES, Some(box_size))
};
let box_size = declared_box_size.unwrap_or(data.len());
let minimum_size = header_bytes.checked_add(ISO_BMFF_FTYP_FIELDS_BYTES)?;
if box_size < minimum_size {
return None;
}
let major_brand_end = header_bytes.checked_add(4)?;
let scan_end = match declared_box_size {
Some(declared) if declared <= data.len() => declared,
_ => data.len().min(ISO_BMFF_BRAND_SCAN_BYTES),
};
if major_brand_end > scan_end {
return None;
}
Some(FTYPLayout {
major_brand_offset: header_bytes,
compatible_brands_offset: minimum_size,
scan_end,
})
}
pub(super) fn iso_bmff_sniff(data: &[u8]) -> Option<SniffInfo> {
let layout = ftyp_layout(data)?;
let mut brands = ISOBMFFBrandSet::default();
brands.record(&data[layout.major_brand_offset..layout.major_brand_offset + 4]);
let mut offset = layout.compatible_brands_offset;
loop {
let end = offset
.checked_add(4)
.expect("bounded ISO BMFF brand offset must fit usize");
if end > layout.scan_end {
break;
}
brands.record(&data[offset..end]);
offset = end;
}
brands.resolve()
}
+330
View File
@@ -0,0 +1,330 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
mod image_containers;
mod iso_bmff;
mod registry;
mod stream_containers;
#[cfg(test)]
mod tests;
use self::image_containers::{
GIFAnimation, PNGAnimation, gif_animation, png_animation, webp_sniff, webp_sniff_complete,
};
use self::iso_bmff::iso_bmff_sniff;
use self::stream_containers::{is_adts, looks_like_svg, matroska_sniff, mpeg_ts_sniff, ogg_sniff};
use crate::media_type::MediaType;
pub use self::registry::{category, extension_mime, normalize, passthrough_mime};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Category {
Image,
Video,
Audio,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SniffInfo {
pub mime: &'static str,
pub animated: bool,
pub frames: u32,
pub duration_ms: u32,
pub width: u32,
pub height: u32,
pub has_alpha: bool,
pub color_space: &'static str,
}
impl Default for SniffInfo {
fn default() -> Self {
Self {
mime: "application/octet-stream",
animated: false,
frames: 1,
duration_ms: 0,
width: 0,
height: 0,
has_alpha: false,
color_space: "unknown",
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct MediaPrefixSniff {
pub media: SniffInfo,
pub complete: bool,
}
pub fn is_supported_media_mime(mime_type_raw: &str) -> bool {
let Some(mime_type) = normalize(Some(mime_type_raw)) else {
return false;
};
matches!(
mime_type,
"image/jpeg"
| "image/png"
| "image/apng"
| "image/gif"
| "image/webp"
| "image/avif"
| "image/heic"
| "image/heif"
| "image/jxl"
| "image/svg+xml"
| "image/tiff"
| "image/bmp"
| "video/mp4"
| "video/webm"
| "video/quicktime"
| "video/3gpp"
| "video/x-matroska"
| "video/x-msvideo"
| "video/x-flv"
| "video/ogg"
| "video/mp2t"
| "video/mpeg"
| "video/x-ms-wmv"
| "audio/mpeg"
| "audio/wav"
| "audio/flac"
| "audio/ogg"
| "audio/aac"
| "audio/mp4"
| "audio/webm"
| "audio/aiff"
)
}
fn starts(data: &[u8], prefix: &[u8]) -> bool {
data.len() >= prefix.len() && &data[..prefix.len()] == prefix
}
pub fn sniff(data: &[u8]) -> SniffInfo {
if starts(data, b"\x89PNG\r\n\x1a\n") {
let mut out = SniffInfo {
mime: MediaType::PNG.mime(),
has_alpha: true,
..Default::default()
};
if data.len() >= 26 && &data[12..16] == b"IHDR" {
out.width =
u32::from_be_bytes(data[16..20].try_into().expect("validated PNG width slice"));
out.height =
u32::from_be_bytes(data[20..24].try_into().expect("validated PNG height slice"));
out.has_alpha = data[25] == 4 || data[25] == 6;
}
if let PNGAnimation::Animated(frames) = png_animation(data) {
out.mime = MediaType::APNG.mime();
out.animated = frames > 1;
out.frames = frames;
}
return out;
}
if starts(data, b"\xff\xd8\xff") {
return SniffInfo {
mime: MediaType::JPEG.mime(),
..Default::default()
};
}
if starts(data, b"GIF87a") || starts(data, b"GIF89a") {
let animated = gif_animation(data) == GIFAnimation::Animated;
return SniffInfo {
mime: MediaType::GIF.mime(),
animated,
frames: if animated { 2 } else { 1 },
width: if data.len() >= 10 {
u32::from(u16::from_le_bytes(
data[6..8].try_into().expect("validated GIF width slice"),
))
} else {
0
},
height: if data.len() >= 10 {
u32::from(u16::from_le_bytes(
data[8..10].try_into().expect("validated GIF height slice"),
))
} else {
0
},
..Default::default()
};
}
if data.len() >= 12 && starts(data, b"RIFF") && &data[8..12] == b"WEBP" {
return webp_sniff(data);
}
if let Some(info) = iso_bmff_sniff(data) {
return info;
}
if starts(data, b"\xff\x0a") || starts(data, b"\x00\x00\x00\x0cJXL \r\n\x87\n") {
return SniffInfo {
mime: MediaType::JXL.mime(),
..Default::default()
};
}
if starts(data, b"II*\0") || starts(data, b"MM\0*") {
return SniffInfo {
mime: MediaType::TIFF.mime(),
..Default::default()
};
}
if starts(data, b"BM") {
return SniffInfo {
mime: MediaType::BMP.mime(),
..Default::default()
};
}
if starts(data, b"\x1a\x45\xdf\xa3") {
return matroska_sniff(data);
}
if starts(data, b"FLV") {
return SniffInfo {
mime: MediaType::FLVVideo.mime(),
..Default::default()
};
}
if starts(data, b"RIFF") && data.len() >= 12 && &data[8..12] == b"AVI " {
return SniffInfo {
mime: MediaType::AVIVideo.mime(),
..Default::default()
};
}
if starts(
data,
b"\x30\x26\xb2\x75\x8e\x66\xcf\x11\xa6\xd9\x00\xaa\x00\x62\xce\x6c",
) {
return SniffInfo {
mime: MediaType::WMVVideo.mime(),
..Default::default()
};
}
if starts(data, b"\x00\x00\x01\xba") || starts(data, b"\x00\x00\x01\xb3") {
return SniffInfo {
mime: MediaType::MPEGVideo.mime(),
..Default::default()
};
}
if mpeg_ts_sniff(data) {
return SniffInfo {
mime: MediaType::MPEGTSVideo.mime(),
..Default::default()
};
}
if starts(data, b"ID3")
|| starts(data, b"\xff\xfb")
|| starts(data, b"\xff\xf3")
|| starts(data, b"\xff\xf2")
{
return SniffInfo {
mime: MediaType::MPEGAudio.mime(),
..Default::default()
};
}
if starts(data, b"OggS") {
return ogg_sniff(data);
}
if starts(data, b"fLaC") {
return SniffInfo {
mime: MediaType::FLACAudio.mime(),
..Default::default()
};
}
if is_adts(data) {
return SniffInfo {
mime: MediaType::AACAudio.mime(),
..Default::default()
};
}
if starts(data, b"RIFF") && data.len() >= 12 && &data[8..12] == b"WAVE" {
return SniffInfo {
mime: MediaType::WAVAudio.mime(),
..Default::default()
};
}
if starts(data, b"FORM") && data.len() >= 12 && matches!(&data[8..12], b"AIFF" | b"AIFC") {
return SniffInfo {
mime: MediaType::AIFFAudio.mime(),
..Default::default()
};
}
if starts(data, b"%PDF-") {
return SniffInfo {
mime: "application/pdf",
..Default::default()
};
}
if looks_like_svg(data) {
return SniffInfo {
mime: MediaType::SVG.mime(),
..Default::default()
};
}
SniffInfo::default()
}
pub fn sniff_prefix(data: &[u8], total_len: usize) -> MediaPrefixSniff {
assert!(data.len() <= total_len);
let sniffed = sniff(data);
if data.len() == total_len {
return MediaPrefixSniff {
media: sniffed,
complete: true,
};
}
let complete =
if sniffed.mime == MediaType::PNG.mime() || sniffed.mime == MediaType::APNG.mime() {
png_animation(data) != PNGAnimation::Incomplete
} else if sniffed.mime == MediaType::WebP.mime() {
webp_sniff_complete(data)
} else if sniffed.mime == MediaType::GIF.mime() {
gif_animation(data) != GIFAnimation::Incomplete
} else {
matches!(
MediaType::from_mime(sniffed.mime),
Some(
MediaType::JPEG
| MediaType::JXL
| MediaType::TIFF
| MediaType::BMP
| MediaType::SVG
)
)
};
MediaPrefixSniff {
media: sniffed,
complete,
}
}
pub fn detect(data: &[u8], filename: &str, header_mime: Option<&str>) -> String {
let sniffed = sniff(data);
if sniffed.mime != "application/octet-stream" {
if sniffed.mime == "video/mp4" && extension_mime(filename) == Some("audio/mp4") {
return "audio/mp4".to_owned();
}
return sniffed.mime.to_owned();
}
if let Some(m) = extension_mime(filename) {
return m.to_owned();
}
if let Some(m) = normalize(header_mime) {
return m.to_owned();
}
"application/octet-stream".to_owned()
}
pub fn filename_for_mime(mime_type: &str, fallback: &str) -> String {
if fallback.contains('.') {
return fallback.to_owned();
}
let ext = match mime_type {
"image/jpeg" => "jpg",
"image/png" => "png",
"image/webp" => "webp",
"image/gif" => "gif",
"video/mp4" => "mp4",
_ => "bin",
};
format!("{fallback}.{ext}")
}
+44
View File
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::Category;
use crate::media_type::MediaType;
use http::HeaderValue;
pub fn normalize(raw: Option<&str>) -> Option<&str> {
let value = raw?;
let semi = value.find(';').unwrap_or(value.len());
let trimmed = value[..semi].trim_matches([' ', '\t']);
(!trimmed.is_empty() && HeaderValue::from_bytes(trimmed.as_bytes()).is_ok()).then_some(trimmed)
}
pub fn category(mime_type: &str) -> Option<Category> {
let prefix = mime_type.as_bytes().get(..6);
if prefix.is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"image/")) {
Some(Category::Image)
} else if prefix.is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"video/")) {
Some(Category::Video)
} else if prefix.is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"audio/")) {
Some(Category::Audio)
} else {
None
}
}
pub fn passthrough_mime(raw: Option<&str>) -> Option<&'static str> {
let normalized = normalize(raw)?;
if normalized.eq_ignore_ascii_case("application/pdf") {
return Some("application/pdf");
}
if normalized.eq_ignore_ascii_case("text/css") {
return Some("text/css; charset=utf-8");
}
MediaType::from_mime(normalized).map(MediaType::mime)
}
pub fn extension_mime(filename: &str) -> Option<&'static str> {
let ext = filename.rsplit_once('.')?.1;
if ext.eq_ignore_ascii_case("css") {
return Some("text/css; charset=utf-8");
}
MediaType::from_extension(ext).map(MediaType::mime)
}
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::SniffInfo;
use crate::media_type::MediaType;
pub(super) fn mpeg_ts_sniff(data: &[u8]) -> bool {
data.len() >= 188
&& data[0] == 0x47
&& (data.len() < 376 || data[188] == 0x47)
&& (data.len() < 564 || data[376] == 0x47)
}
pub(super) fn is_adts(data: &[u8]) -> bool {
if data.len() < 7 || data[0] != 0xff || data[1] & 0xf6 != 0xf0 {
return false;
}
let sample_rate_index = (data[2] >> 2) & 0x0f;
if sample_rate_index == 0x0f {
return false;
}
let frame_length = (usize::from(data[3] & 0x03) << 11)
| (usize::from(data[4]) << 3)
| (usize::from(data[5]) >> 5);
frame_length >= 7 && frame_length <= data.len()
}
pub(super) fn looks_like_svg(data: &[u8]) -> bool {
let mut window = &data[..data.len().min(4096)];
if window.starts_with(b"\xef\xbb\xbf") {
window = &window[3..];
}
window.windows(4).any(|w| w == b"<svg")
&& (window.windows(5).any(|w| w == b"xmlns")
|| window.starts_with(b"<svg")
|| window.starts_with(b"<?xml"))
}
pub(super) fn ogg_sniff(data: &[u8]) -> SniffInfo {
let window = &data[..data.len().min(8192)];
if window.windows(6).any(|w| w == b"theora" || w == b"Theora") {
SniffInfo {
mime: MediaType::OGGVideo.mime(),
..Default::default()
}
} else {
SniffInfo {
mime: MediaType::OGGAudio.mime(),
..Default::default()
}
}
}
pub(super) fn matroska_sniff(data: &[u8]) -> SniffInfo {
let window = &data[..data.len().min(4096)];
if window.windows(4).any(|w| w == b"webm") {
SniffInfo {
mime: MediaType::WebMVideo.mime(),
..Default::default()
}
} else {
SniffInfo {
mime: MediaType::MatroskaVideo.mime(),
..Default::default()
}
}
}
+382
View File
@@ -0,0 +1,382 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::*;
use crate::test_fixtures::{
adversarial_media_bytes, animated_gif_fixture, apng_header, minimal_gif,
};
fn vp8x_webp(flags: u8, width_minus_one: u32, height_minus_one: u32) -> Vec<u8> {
let mut webp = vec![0_u8; 30];
webp[0..4].copy_from_slice(b"RIFF");
webp[8..12].copy_from_slice(b"WEBP");
webp[12..16].copy_from_slice(b"VP8X");
webp[16..20].copy_from_slice(&10_u32.to_le_bytes());
webp[20] = flags;
webp[24..27].copy_from_slice(&width_minus_one.to_le_bytes()[..3]);
webp[27..30].copy_from_slice(&height_minus_one.to_le_bytes()[..3]);
webp
}
fn png_chunk(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut chunk = Vec::new();
chunk.extend_from_slice(
&u32::try_from(payload.len())
.expect("test payload fits a PNG chunk")
.to_be_bytes(),
);
chunk.extend_from_slice(kind);
chunk.extend_from_slice(payload);
chunk.extend_from_slice(&[0; 4]);
chunk
}
#[test]
fn sniffs_common_image_formats() {
assert_eq!("image/png", sniff(b"\x89PNG\r\n\x1a\nxxxx").mime);
assert_eq!("image/jpeg", sniff(b"\xff\xd8\xff").mime);
assert_eq!("image/gif", sniff(b"GIF89a\x01\x00\x01\x00").mime);
assert_eq!("image/tiff", sniff(b"II*\0xxxx").mime);
assert_eq!("image/tiff", sniff(b"MM\0*xxxx").mime);
assert_eq!("image/bmp", sniff(b"BMxxxx").mime);
}
#[test]
fn sniffs_apng_via_actl_chunk() {
let apng = apng_header(2);
let info = sniff(&apng);
assert_eq!("image/apng", info.mime);
assert!(info.animated);
}
#[test]
fn sniffs_animated_webp_via_anim_chunk() {
let info = sniff(&vp8x_webp(0x02, 0, 0));
assert_eq!("image/webp", info.mime);
assert!(info.animated);
}
#[test]
fn sniffs_ftyp_boxes_for_heic_avif_mp4_variants() {
assert_eq!(
"image/avif",
sniff(b"\x00\x00\x00\x20ftypavifsome bytes").mime
);
assert_eq!(
"image/avif",
sniff(b"\x00\x00\x00\x20ftypavissome bytes").mime
);
assert!(sniff(b"\x00\x00\x00\x20ftypavissome bytes").animated);
assert_eq!(
"image/heif",
sniff(b"\x00\x00\x00\x20ftypmif1some bytes").mime
);
assert_eq!(
"image/heic",
sniff(b"\x00\x00\x00\x20ftypheicsome bytes").mime
);
assert_eq!(
"video/mp4",
sniff(b"\x00\x00\x00\x20ftypiso5some bytes").mime
);
assert_eq!(
"video/mp4",
sniff(b"\x00\x00\x00\x20ftypM4V some bytes").mime
);
assert_eq!(
"audio/mp4",
sniff(b"\x00\x00\x00\x20ftypM4A some bytes").mime
);
assert_eq!(
"video/quicktime",
sniff(b"\x00\x00\x00\x20ftypqt some bytes").mime
);
}
#[test]
fn sniffs_the_literal_heif_ftyp_brand() {
let heif = b"\x00\x00\x00\x20ftypheifsome bytes";
assert_eq!("image/heic", sniff(heif).mime);
assert!(!sniff(heif).animated);
}
#[test]
fn scans_every_compatible_brand_of_a_fully_present_ftyp_box() {
let compatible_brands = 40;
let box_size = 16 + compatible_brands * 4;
let mut ftyp = Vec::new();
ftyp.extend_from_slice(
&u32::try_from(box_size)
.expect("test ftyp box size fits u32")
.to_be_bytes(),
);
ftyp.extend_from_slice(b"ftyp");
ftyp.extend_from_slice(b"isom");
ftyp.extend_from_slice(&0x0000_0200_u32.to_be_bytes());
for _ in 0..compatible_brands - 1 {
ftyp.extend_from_slice(b"isom");
}
ftyp.extend_from_slice(b"avif");
assert_eq!(box_size, ftyp.len());
assert!(box_size - 4 > 128);
assert_eq!("image/avif", sniff(&ftyp).mime);
let mut truncated_box = ftyp.clone();
truncated_box[0..4].copy_from_slice(&u32::MAX.to_be_bytes());
assert_eq!("video/mp4", sniff(&truncated_box).mime);
}
#[test]
fn detect_prefers_m4a_extension_over_generic_mp4_brand() {
assert_eq!(
"audio/mp4",
detect(
b"\x00\x00\x00\x20ftypisom\x00\x00\x02\x00isomiso2mp41",
"track.m4a",
None
)
);
}
#[test]
fn sniffs_matroska_vs_webm() {
let mkv = b"\x1a\x45\xdf\xa3\x9f\x42\x86\x81\x01\x42\xf7\x81\x01\x42\xf2\x81\x04\x42\xf3\x81\x08\x42\x82\x88matroska";
assert_eq!("video/x-matroska", sniff(mkv).mime);
assert_eq!(
"video/webm",
sniff(b"\x1a\x45\xdf\xa3 here is the webm doctype").mime
);
}
#[test]
fn sniffs_audio_variants() {
assert_eq!("audio/mpeg", sniff(b"ID3\x04\x00\x00").mime);
assert_eq!("audio/mpeg", sniff(b"\xff\xfb\x90\x00").mime);
assert_eq!("audio/ogg", sniff(b"OggS\x00\x02").mime);
assert_eq!("video/ogg", sniff(b"OggS\x00\x02xxxx\x80theora").mime);
assert_eq!("audio/flac", sniff(b"fLaC\x00\x00").mime);
assert_eq!("audio/wav", sniff(b"RIFF\x00\x00\x00\x00WAVEdata").mime);
}
#[test]
fn extension_mime_covers_common_audio_and_video_containers() {
assert_eq!(Some("video/ogg"), extension_mime("movie.ogv"));
assert_eq!(Some("audio/ogg"), extension_mime("voice.opus"));
assert_eq!(Some("audio/flac"), extension_mime("track.flac"));
assert_eq!(Some("video/x-matroska"), extension_mime("clip.mkv"));
assert_eq!(Some("image/tiff"), extension_mime("scan.tiff"));
assert_eq!(Some("image/bmp"), extension_mime("bitmap.bmp"));
}
#[test]
fn sniffs_pdf() {
assert_eq!("application/pdf", sniff(b"%PDF-1.7\n").mime);
}
#[test]
fn mime_sniff_recognizes_bounded_container_headers() {
let png = apng_header(2);
let sniffed = sniff(&png);
assert_eq!("image/apng", sniffed.mime);
assert!(sniffed.animated);
assert_eq!(2, sniffed.frames);
assert_eq!((2, 3), (sniffed.width, sniffed.height));
assert!(sniffed.has_alpha);
let gif = minimal_gif();
let sniffed = sniff(&gif);
assert_eq!("image/gif", sniffed.mime);
assert!(!sniffed.animated);
assert_eq!((1, 1), (sniffed.width, sniffed.height));
let animated = animated_gif_fixture();
let sniffed = sniff(&animated);
assert_eq!("image/gif", sniffed.mime);
assert!(sniffed.animated);
assert_eq!(2, sniffed.frames);
assert_eq!((32, 32), (sniffed.width, sniffed.height));
let sniffed = sniff(&vp8x_webp(0x12, 1, 2));
assert_eq!("image/webp", sniffed.mime);
assert!(sniffed.animated);
assert!(sniffed.has_alpha);
assert_eq!((2, 3), (sniffed.width, sniffed.height));
let mut bmff = Vec::new();
bmff.extend_from_slice(&24_u32.to_be_bytes());
bmff.extend_from_slice(b"ftyp");
bmff.extend_from_slice(b"M4A ");
bmff.extend_from_slice(&0_u32.to_be_bytes());
bmff.extend_from_slice(b"isom");
bmff.extend_from_slice(b"M4A ");
assert_eq!("audio/mp4", sniff(&bmff).mime);
}
#[test]
fn mime_sniffing_stays_bounded_on_adversarial_containers() {
for bytes in adversarial_media_bytes() {
let sniffed = sniff(&bytes);
assert!(sniffed.frames >= 1, "zero frames for {} bytes", bytes.len());
let complete = sniff_prefix(&bytes, bytes.len());
assert!(complete.complete, "incomplete for {} bytes", bytes.len());
assert!(complete.media.frames >= 1);
let truncated = sniff_prefix(&bytes, bytes.len().saturating_add(1024));
assert!(truncated.media.frames >= 1);
}
}
#[test]
fn prefix_sniffing_reports_truncated_containers_as_incomplete() {
let gif = minimal_gif();
for prefix_length in 0..gif.len() {
let result = sniff_prefix(&gif[..prefix_length], gif.len());
if prefix_length < 6 {
assert_eq!("application/octet-stream", result.media.mime);
}
}
let complete = sniff_prefix(&gif, gif.len());
assert!(complete.complete);
assert_eq!("image/gif", complete.media.mime);
assert!(!sniff_prefix(&gif[..gif.len() - 1], gif.len()).complete);
let apng = apng_header(2);
assert!(sniff_prefix(&apng, apng.len() + 1024).complete);
assert!(!sniff_prefix(&apng[..20], apng.len()).complete);
let webp = vp8x_webp(0x02, 0, 0);
assert!(sniff_prefix(&webp, webp.len() + 1024).complete);
assert!(!sniff_prefix(&webp[..24], webp.len()).complete);
assert!(sniff_prefix(b"\xff\xd8\xff\xe0", 4096).complete);
assert!(!sniff_prefix(b"OggS\x00\x02", 4096).complete);
}
#[test]
fn structural_parsers_reject_the_byte_scan_false_positives() {
let mut png = b"\x89PNG\r\n\x1a\n".to_vec();
png.extend_from_slice(&png_chunk(b"IHDR", &[0; 13]));
png.extend_from_slice(&png_chunk(b"IDAT", b"acTL rides inside the pixel data"));
let info = sniff(&png);
assert_eq!("image/png", info.mime);
assert!(!info.animated);
let mut gif = minimal_gif();
gif.extend_from_slice(b"\x21\xffNETSCAPE2.0");
let info = sniff(&gif);
assert_eq!("image/gif", info.mime);
assert!(!info.animated);
let info = sniff(b"RIFF\x00\x00\x00\x00WEBPVP8XANIMxxxx");
assert_eq!("image/webp", info.mime);
assert!(!info.animated);
assert!(!info.has_alpha);
}
#[test]
fn detect_falls_back_to_the_extension_then_the_declared_header() {
assert_eq!("audio/mp4", detect(b"", "clip.m4a", None));
assert_eq!(
"IMAGE/PNG",
detect(b"", "unknown.bin", Some("IMAGE/PNG; charset=binary"))
);
assert_eq!("application/octet-stream", detect(b"", "unknown.bin", None));
assert_eq!(
"image/png",
detect(b"\x89PNG\r\n\x1a\nxxxx", "audio.m4a", Some("image/gif"))
);
assert_eq!("photo.png", filename_for_mime("image/png", "photo"));
assert_eq!("photo.gif", filename_for_mime("image/png", "photo.gif"));
assert_eq!("blob.bin", filename_for_mime("application/pdf", "blob"));
}
#[test]
fn sniffs_adts_aac_and_aiff_containers() {
let mut adts = vec![0xff, 0xf1, 0x50, 0x80, 0x01, 0xa0, 0xfc];
adts.resize(13, 0);
assert_eq!("audio/aac", sniff(&adts).mime);
assert_eq!("audio/aiff", sniff(b"FORM\x00\x00\x00\x12AIFF").mime);
assert_eq!("audio/aiff", sniff(b"FORM\x00\x00\x00\x12AIFC").mime);
assert_eq!(
"application/octet-stream",
sniff(b"FORM\x00\x00\x00\x12WAVE").mime
);
}
#[test]
fn content_type_normalization_and_categories_ignore_case_and_reject_control_bytes() {
assert_eq!(Some("image/png"), normalize(Some("image/png; charset=x")));
assert_eq!(Some("image/png"), normalize(Some(" \timage/png\t ")));
assert_eq!(None, normalize(Some("image/p\rng")));
assert_eq!(None, normalize(Some(" ")));
assert_eq!(None, normalize(None));
assert_eq!(Some(Category::Image), category("IMAGE/PNG"));
assert_eq!(Some(Category::Video), category("Video/MP4"));
assert_eq!(Some(Category::Audio), category("AUDIO/wav"));
assert_eq!(None, category("text/css"));
assert_eq!(None, category("image"));
}
#[test]
fn registry_lookups_canonicalize_extensions_and_passthrough_types() {
assert_eq!(Some("image/jpeg"), extension_mime("photo.JPG"));
assert_eq!(Some("text/css; charset=utf-8"), extension_mime("app.css"));
assert_eq!(None, extension_mime("manual.PDF"));
assert_eq!(None, extension_mime("archive.zip"));
assert_eq!(None, extension_mime("noextension"));
assert_eq!(
Some("image/png"),
passthrough_mime(Some("IMAGE/PNG; charset=binary"))
);
assert_eq!(
Some("image/avif"),
passthrough_mime(Some("image/avif-sequence"))
);
assert_eq!(
Some("application/pdf"),
passthrough_mime(Some("Application/PDF"))
);
assert_eq!(
Some("text/css; charset=utf-8"),
passthrough_mime(Some("text/css"))
);
assert_eq!(None, passthrough_mime(Some("application/octet-stream")));
assert_eq!(None, passthrough_mime(None));
}
#[test]
fn supported_media_mimes_gate_metadata_on_the_frozen_allow_list() {
assert!(is_supported_media_mime("image/heif"));
assert!(is_supported_media_mime("image/heic"));
assert!(is_supported_media_mime("video/mp4; codecs=avc1"));
assert!(!is_supported_media_mime("image/avif-sequence"));
assert!(!is_supported_media_mime("IMAGE/PNG"));
assert!(!is_supported_media_mime("application/pdf"));
assert!(!is_supported_media_mime(""));
}
#[test]
fn every_iso_bmff_brand_matches_the_old_era_table() {
for (brand, expected) in [
(&b"heic"[..], "image/heic"),
(b"heix", "image/heic"),
(b"heif", "image/heic"),
(b"heim", "image/heic"),
(b"heis", "image/heic"),
(b"hevc", "image/heic"),
(b"hevx", "image/heic"),
(b"hevm", "image/heic"),
(b"hevs", "image/heic"),
(b"mif1", "image/heif"),
(b"msf1", "image/heif"),
] {
let mut bytes = b"\x00\x00\x00\x20ftyp".to_vec();
bytes.extend_from_slice(brand);
bytes.extend_from_slice(b"some bytes");
let info = sniff(&bytes);
assert_eq!(expected, info.mime, "brand {brand:?}");
assert!(!info.animated, "brand {brand:?} must not be animated");
}
}