pict-rs/src/validate.rs

258 lines
6.8 KiB
Rust
Raw Permalink Normal View History

mod exiftool;
mod ffmpeg;
mod magick;
2021-10-21 01:13:39 +00:00
use crate::{
2023-07-13 22:42:21 +00:00
discover::Discovery,
2022-03-29 16:04:56 +00:00
either::Either,
error::Error,
2023-09-02 01:50:10 +00:00
error_code::ErrorCode,
2023-07-13 22:42:21 +00:00
formats::{
AnimationFormat, AnimationOutput, ImageInput, ImageOutput, InputFile, InputVideoFormat,
InternalFormat, Validations,
2023-07-13 22:42:21 +00:00
},
2021-10-21 01:13:39 +00:00
};
use actix_web::web::Bytes;
use tokio::io::AsyncRead;
2023-07-13 22:42:21 +00:00
#[derive(Debug, thiserror::Error)]
pub(crate) enum ValidationError {
#[error("Too wide")]
Width,
#[error("Too tall")]
Height,
#[error("Too many pixels")]
Area,
#[error("Too many frames")]
Frames,
#[error("Uploaded file is empty")]
Empty,
2023-07-13 22:42:21 +00:00
#[error("Filesize too large")]
Filesize,
#[error("Video is disabled")]
VideoDisabled,
}
2023-09-02 01:50:10 +00:00
impl ValidationError {
pub(crate) const fn error_code(&self) -> ErrorCode {
match self {
Self::Width => ErrorCode::VALIDATE_WIDTH,
Self::Height => ErrorCode::VALIDATE_HEIGHT,
Self::Area => ErrorCode::VALIDATE_AREA,
Self::Frames => ErrorCode::VALIDATE_FRAMES,
Self::Empty => ErrorCode::VALIDATE_FILE_EMPTY,
Self::Filesize => ErrorCode::VALIDATE_FILE_SIZE,
Self::VideoDisabled => ErrorCode::VIDEO_DISABLED,
}
}
}
2023-07-13 22:42:21 +00:00
const MEGABYTES: usize = 1024 * 1024;
#[tracing::instrument(skip_all)]
pub(crate) async fn validate_bytes(
bytes: Bytes,
2023-07-13 22:42:21 +00:00
validations: Validations<'_>,
2023-08-05 17:41:06 +00:00
timeout: u64,
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
if bytes.is_empty() {
return Err(ValidationError::Empty.into());
}
2023-07-13 22:42:21 +00:00
let Discovery {
input,
width,
height,
frames,
2023-08-05 17:41:06 +00:00
} = crate::discover::discover_bytes(timeout, bytes.clone()).await?;
2023-07-13 22:42:21 +00:00
match &input {
InputFile::Image(input) => {
2023-07-13 22:42:21 +00:00
let (format, read) =
2023-08-05 17:41:06 +00:00
process_image(bytes, *input, width, height, validations.image, timeout).await?;
2023-07-13 22:42:21 +00:00
Ok((format, Either::left(read)))
}
InputFile::Animation(input) => {
2023-07-13 22:42:21 +00:00
let (format, read) = process_animation(
bytes,
*input,
width,
height,
frames.unwrap_or(1),
validations.animation,
2023-08-05 17:41:06 +00:00
timeout,
2023-07-13 22:42:21 +00:00
)
.await?;
Ok((format, Either::right(Either::left(read))))
}
InputFile::Video(input) => {
let (format, read) = process_video(
bytes,
2023-08-31 02:00:15 +00:00
*input,
2023-07-13 22:42:21 +00:00
width,
height,
frames.unwrap_or(1),
validations.video,
2023-08-05 17:41:06 +00:00
timeout,
2023-07-13 22:42:21 +00:00
)
.await?;
Ok((format, Either::right(Either::right(read))))
}
}
}
#[tracing::instrument(skip(bytes, validations))]
async fn process_image(
bytes: Bytes,
input: ImageInput,
width: u16,
height: u16,
validations: &crate::config::Image,
2023-08-05 17:41:06 +00:00
timeout: u64,
2023-07-13 22:42:21 +00:00
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
if width > validations.max_width {
return Err(ValidationError::Width.into());
}
if height > validations.max_height {
return Err(ValidationError::Height.into());
}
if u32::from(width) * u32::from(height) > validations.max_area {
return Err(ValidationError::Area.into());
}
if bytes.len() > validations.max_file_size * MEGABYTES {
return Err(ValidationError::Filesize.into());
}
let ImageOutput {
format,
needs_transcode,
} = input.build_output(validations.format);
let read = if needs_transcode {
let quality = validations.quality_for(format);
2023-08-05 17:41:06 +00:00
Either::left(magick::convert_image(input.format, format, quality, timeout, bytes).await?)
2023-07-13 22:42:21 +00:00
} else {
2023-08-05 17:41:06 +00:00
Either::right(exiftool::clear_metadata_bytes_read(bytes, timeout)?)
2023-07-13 22:42:21 +00:00
};
Ok((InternalFormat::Image(format), read))
}
fn validate_animation(
size: usize,
width: u16,
height: u16,
frames: u32,
validations: &crate::config::Animation,
) -> Result<(), ValidationError> {
if width > validations.max_width {
return Err(ValidationError::Width);
}
if height > validations.max_height {
return Err(ValidationError::Height);
}
if u32::from(width) * u32::from(height) > validations.max_area {
return Err(ValidationError::Area);
}
if frames > validations.max_frame_count {
return Err(ValidationError::Frames);
}
if size > validations.max_file_size * MEGABYTES {
return Err(ValidationError::Filesize);
}
Ok(())
}
#[tracing::instrument(skip(bytes, validations))]
async fn process_animation(
bytes: Bytes,
input: AnimationFormat,
width: u16,
height: u16,
frames: u32,
validations: &crate::config::Animation,
2023-08-05 17:41:06 +00:00
timeout: u64,
2023-07-13 22:42:21 +00:00
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
validate_animation(bytes.len(), width, height, frames, validations)?;
let AnimationOutput {
format,
needs_transcode,
} = input.build_output(validations.format);
let read = if needs_transcode {
let quality = validations.quality_for(format);
Either::left(magick::convert_animation(input, format, quality, timeout, bytes).await?)
} else {
Either::right(exiftool::clear_metadata_bytes_read(bytes, timeout)?)
};
Ok((InternalFormat::Animation(format), read))
2023-07-13 22:42:21 +00:00
}
fn validate_video(
size: usize,
width: u16,
height: u16,
frames: u32,
validations: &crate::config::Video,
) -> Result<(), ValidationError> {
if !validations.enable {
return Err(ValidationError::VideoDisabled);
}
if width > validations.max_width {
return Err(ValidationError::Width);
}
if height > validations.max_height {
return Err(ValidationError::Height);
}
2023-07-13 22:42:21 +00:00
if u32::from(width) * u32::from(height) > validations.max_area {
return Err(ValidationError::Area);
}
if frames > validations.max_frame_count {
return Err(ValidationError::Frames);
}
if size > validations.max_file_size * MEGABYTES {
return Err(ValidationError::Filesize);
}
Ok(())
}
#[tracing::instrument(skip(bytes, validations))]
async fn process_video(
bytes: Bytes,
input: InputVideoFormat,
2023-07-13 22:42:21 +00:00
width: u16,
height: u16,
frames: u32,
validations: &crate::config::Video,
2023-08-05 17:41:06 +00:00
timeout: u64,
2023-07-13 22:42:21 +00:00
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
validate_video(bytes.len(), width, height, frames, validations)?;
let output = input.build_output(
validations.video_codec,
validations.audio_codec,
validations.allow_audio,
);
let crf = validations.crf_for(width, height);
2023-08-05 17:41:06 +00:00
let read = ffmpeg::transcode_bytes(input, output, crf, timeout, bytes).await?;
2023-07-13 22:42:21 +00:00
Ok((InternalFormat::Video(output.format.internal_format()), read))
}