fix(media): stream large files and honour size settings (#3387)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-11 13:28:24 +02:00
1 parent 6acd7f3d22
commit 45b78a13c2
97 files changed
+2547 -409

No files matched your search

@@ -120,9 +120,6 @@ pub fn probe_av_metadata(
if input.is_empty() {
return Err(MediaError::MediaDecodeFailed);
}
if input.len() > media_limits.max_media_proxy_bytes() {
return Err(MediaError::StreamTooLong);
}
if preview_max_dimension > NSFW_PREVIEW_MAX_DIMENSION {
return Err(MediaError::InvalidImageDimensions);
}
@@ -137,15 +137,15 @@ fn metadata_blocking(request: MetadataBlockingRequest<'_>) -> Result<PreparedMet
input,
options,
} = request;
if input.len() > constants::MAX_MEDIA_PROXY_BYTES {
return Err(MediaError::StreamTooLong);
}
let sniffed = mime::sniff(input);
if !mime::is_supported_media_mime(sniffed.mime) {
return Err(MediaError::UnsupportedMediaType);
}
let initial_category = mime::category(sniffed.mime).ok_or(MediaError::UnsupportedMediaType)?;
let is_image = initial_category == mime::Category::Image;
if is_image && input.len() > constants::MAX_MEDIA_PROXY_BYTES {
return Err(MediaError::StreamTooLong);
}
let dims = if is_image {
let dims = probe_image_dims(media_limits, input)?;
validate_jxl_decode_pixels(sniffed.mime, dims)?;
+1 -1
View File
@@ -37,7 +37,7 @@ pub use metadata::{
MetadataOptions, PreparedMetadata, finish_metadata, metadata_json_with_options,
prepare_metadata,
};
pub use native_cost::{AV_NATIVE_COST_BYTES, image_transform_cost, metadata_cost};
pub use native_cost::{AV_NATIVE_COST_BYTES, av_native_cost, image_transform_cost, metadata_cost};
pub use nsfw_processing::encode_static_image_for_nsfw;
pub use transform::transform_image;
pub use video_thumbnail::{
@@ -5,8 +5,8 @@ use super::image_probe::probe_image_dims;
use super::loaded_image::static_output_pixels;
use super::native_runtime::ensure_vips_init;
use crate::{
image_quality::ImageQuality, image_transform::ImageOptions, media_limits::MediaLimits, mime,
output_format::OutputFormat,
constants, image_quality::ImageQuality, image_transform::ImageOptions,
media_limits::MediaLimits, mime, output_format::OutputFormat,
};
pub const AV_NATIVE_COST_BYTES: usize = 256 << 20;
@@ -53,6 +53,12 @@ pub fn image_transform_cost(
Ok(input.len().saturating_add(pixel_cost))
}
pub fn av_native_cost(input_len: usize) -> usize {
input_len
.min(constants::MAX_MEDIA_PROXY_BYTES)
.saturating_add(AV_NATIVE_COST_BYTES)
}
pub fn metadata_cost(input: &[u8], media_limits: &MediaLimits) -> Result<usize, MediaError> {
let sniffed_mime = mime::sniff(input).mime;
match mime::category(sniffed_mime) {
@@ -64,9 +70,7 @@ pub fn metadata_cost(input: &[u8], media_limits: &MediaLimits) -> Result<usize,
.len()
.saturating_add(source_pixels * decode_bytes_per_pixel(sniffed_mime)))
}
Some(mime::Category::Video | mime::Category::Audio) => {
Ok(input.len().saturating_add(AV_NATIVE_COST_BYTES))
}
Some(mime::Category::Video | mime::Category::Audio) => Ok(av_native_cost(input.len())),
_ => Ok(input.len()),
}
}
@@ -111,7 +111,6 @@ pub(super) async fn classify_nsfw_buffers(
fn nsfw_video_scan_buffers(source: &NSFWScanSource<'_>) -> Result<Vec<Vec<u8>>, MediaError> {
extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
media_limits: source.media_limits,
input: source.input,
duration_seconds: source.duration_seconds,
deadline_ms: source.deadline_ms,
@@ -363,7 +362,6 @@ pub(super) fn nsfw_frame_seed(input: &[u8]) -> u64 {
}
pub(super) struct VideoNSFWFramesRequest<'a> {
pub(super) media_limits: &'a MediaLimits,
pub(super) input: &'a [u8],
pub(super) duration_seconds: Option<f64>,
pub(super) deadline_ms: Option<i64>,
@@ -374,13 +372,12 @@ pub(super) fn extract_video_frames_for_nsfw(
) -> Result<Vec<Vec<u8>>, MediaError> {
use rand::SeedableRng as _;
let VideoNSFWFramesRequest {
media_limits,
input,
duration_seconds,
deadline_ms,
} = request;
if input.is_empty() || input.len() > media_limits.max_media_proxy_bytes() {
return Err(MediaError::StreamTooLong);
if input.is_empty() {
return Err(MediaError::MediaDecodeFailed);
}
let runtime = vips_runtime()?;
let seed = nsfw_frame_seed(input);
@@ -56,7 +56,6 @@ fn video_nsfw_frames_are_downscaled_to_the_frame_cap() {
.probe
.duration_seconds;
let frames = extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
media_limits: &media_limits,
input: &mp4,
duration_seconds: duration,
deadline_ms: None,
@@ -215,7 +214,6 @@ fn a_sample_beyond_the_packet_budget_keeps_the_frames_that_decoded() {
};
let media_limits = limits();
let frames = extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
media_limits: &media_limits,
input: &mp4,
duration_seconds: Some(12.0),
deadline_ms: None,
@@ -1,22 +1,33 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::super::nsfw_processing::{VideoNSFWFramesRequest, extract_video_frames_for_nsfw};
use super::super::placeholder::{encode_thumbhash, optional_thumbhash};
use super::super::{
MediaError, MetadataOptions, NSFW_PREVIEW_MAX_DIMENSION, metadata_json_with_options,
probe_av_metadata,
AV_NATIVE_COST_BYTES, MediaError, MetadataOptions, NSFW_PREVIEW_MAX_DIMENSION,
extract_video_thumbnail, metadata_cost, metadata_json_with_options, probe_av_metadata,
};
use super::fixtures::{metadata_value, test_media_limits};
use crate::{
constants,
metrics::transform::TransformMetrics,
mime,
nsfw::{NSFWClient, NSFWPolicy},
output_format::OutputFormat,
test_fixtures::{
fixture_audio_mp3_with_png_cover_art, fixture_audio_mp4_with_attached_picture,
fixture_audio_only_mp4, fixture_h264_mp4, fixture_mkv_with_png_video_stream,
fixture_mp4_with_undecodable_video, synthetic_png, synthetic_wav,
},
};
use std::sync::Arc;
use std::{io::Write as _, sync::Arc};
fn mapped_past_the_buffer_bound(head: &[u8]) -> memmap2::Mmap {
let mut file = tempfile::tempfile().expect("temp file");
file.write_all(head).expect("written head");
file.set_len(constants::MAX_MEDIA_PROXY_BYTES as u64 + 1)
.expect("sparse tail");
unsafe { memmap2::Mmap::map(&file).expect("mapped file") }
}
#[test]
fn metadata_json_includes_dimensions_and_placeholder() {
@@ -258,3 +269,54 @@ fn metadata_refuses_jxl_above_the_decode_pixel_cap() {
});
assert_eq!(MediaError::InvalidImageDimensions, err);
}
#[test]
fn metadata_refuses_an_image_past_the_buffer_bound() {
let png = mapped_past_the_buffer_bound(&synthetic_png(16, 16));
assert_eq!("image/png", mime::sniff(&png).mime);
let err = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap()
.block_on(async {
metadata_json_with_options(
&png,
"huge.png",
MetadataOptions::default(),
&test_media_limits(),
&NSFWClient::disabled(),
&TransformMetrics::new(),
)
.await
.unwrap_err()
});
assert_eq!(MediaError::StreamTooLong, err);
}
#[test]
fn native_cost_of_a_video_past_the_buffer_bound_leaves_out_the_mapped_tail() {
let mp4 = mapped_past_the_buffer_bound(&fixture_h264_mp4());
assert_eq!(
constants::MAX_MEDIA_PROXY_BYTES + AV_NATIVE_COST_BYTES,
metadata_cost(&mp4, &test_media_limits()).expect("video cost")
);
}
#[test]
fn video_reads_accept_an_input_past_the_buffer_bound() {
let mp4 = mapped_past_the_buffer_bound(&fixture_h264_mp4());
let limits = test_media_limits();
let probe = probe_av_metadata(&mp4, NSFW_PREVIEW_MAX_DIMENSION, &limits, None)
.expect("probed video")
.probe;
assert!(probe.has_video);
let poster = extract_video_thumbnail(&mp4, OutputFormat::WebP, &limits).expect("poster frame");
assert_eq!("image/webp", mime::sniff(&poster.bytes).mime);
let frames = extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
input: &mp4,
duration_seconds: probe.duration_seconds,
deadline_ms: None,
})
.expect("sampled frames");
assert!(!frames.is_empty());
}
@@ -4,7 +4,7 @@ use super::super::nsfw_processing::{
VideoNSFWFramesRequest, compute_frame_sample_timestamps, extract_video_frames_for_nsfw,
nsfw_frame_seed,
};
use super::super::probe_av_metadata;
use super::super::{MediaError, probe_av_metadata};
use super::fixtures::test_media_limits;
#[test]
@@ -58,7 +58,6 @@ fn extract_video_frames_for_nsfw_returns_multiple_frames() {
.probe
.duration_seconds;
let frames = extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
media_limits: &limits,
input: &bytes,
duration_seconds: duration,
deadline_ms: None,
@@ -74,3 +73,17 @@ fn extract_video_frames_for_nsfw_returns_multiple_frames() {
assert_eq!(&f[..2], &[0xFF, 0xD8], "not a JPEG");
}
}
#[test]
fn an_empty_input_fails_to_decode_for_the_probe_and_the_frame_sampler() {
let frames = extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
input: &[],
duration_seconds: None,
deadline_ms: None,
});
assert_eq!(Some(MediaError::MediaDecodeFailed), frames.err());
assert_eq!(
Some(MediaError::MediaDecodeFailed),
probe_av_metadata(&[], 0, &test_media_limits(), None).err()
);
}
@@ -45,9 +45,6 @@ pub fn extract_video_thumbnail_with_options(
options: VideoThumbnailOptions,
media_limits: &MediaLimits,
) -> Result<ProcessedMedia, MediaError> {
if input.len() > media_limits.max_media_proxy_bytes() {
return Err(MediaError::StreamTooLong);
}
let VideoThumbnailOptions {
format,
width,