mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 11:21:24 +00:00
Don't use image crate to validate gifs
This commit is contained in:
parent
a7e1bcd142
commit
6de89a3318
8 changed files with 139 additions and 64 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1381,6 +1381,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
|
"gif",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
|
|
|
@ -19,6 +19,7 @@ anyhow = "1.0"
|
||||||
bytes = "0.5"
|
bytes = "0.5"
|
||||||
env_logger = "0.7"
|
env_logger = "0.7"
|
||||||
futures = "0.3.4"
|
futures = "0.3.4"
|
||||||
|
gif = "0.10.3"
|
||||||
image = "0.23.4"
|
image = "0.23.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3.1"
|
mime = "0.3.1"
|
||||||
|
|
BIN
client-examples/earth.gif
Normal file
BIN
client-examples/earth.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 978 KiB |
|
@ -7,7 +7,8 @@ import asyncio
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
file_name = '../test.png'
|
png_name = '../test.png'
|
||||||
|
gif_name = '../earth.gif'
|
||||||
url = 'http://localhost:8080/image'
|
url = 'http://localhost:8080/image'
|
||||||
|
|
||||||
async def file_sender(file_name=None):
|
async def file_sender(file_name=None):
|
||||||
|
@ -21,9 +22,10 @@ async def file_sender(file_name=None):
|
||||||
async def req():
|
async def req():
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
data = aiohttp.FormData(quote_fields=False)
|
data = aiohttp.FormData(quote_fields=False)
|
||||||
data.add_field("images[]", file_sender(file_name=file_name), filename="image1.png", content_type="image/png")
|
data.add_field("images[]", file_sender(file_name=png_name), filename="image1.png", content_type="image/png")
|
||||||
data.add_field("images[]", file_sender(file_name=file_name), filename="image2.png", content_type="image/png")
|
data.add_field("images[]", file_sender(file_name=png_name), filename="image2.png", content_type="image/png")
|
||||||
data.add_field("images[]", file_sender(file_name=file_name), filename="image3.png", content_type="image/png")
|
data.add_field("images[]", file_sender(file_name=gif_name), filename="image1.gif", content_type="image/gif")
|
||||||
|
data.add_field("images[]", file_sender(file_name=gif_name), filename="image2.gif", content_type="image/gif")
|
||||||
|
|
||||||
async with session.post(url, data=data) as resp:
|
async with session.post(url, data=data) as resp:
|
||||||
text = await resp.text()
|
text = await resp.text()
|
||||||
|
|
13
src/error.rs
13
src/error.rs
|
@ -1,10 +1,8 @@
|
||||||
|
use crate::validate::GifError;
|
||||||
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
|
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum UploadError {
|
pub(crate) enum UploadError {
|
||||||
#[error("Invalid content type provided, {0}")]
|
|
||||||
ContentType(mime::Mime),
|
|
||||||
|
|
||||||
#[error("Couln't upload file, {0}")]
|
#[error("Couln't upload file, {0}")]
|
||||||
Upload(String),
|
Upload(String),
|
||||||
|
|
||||||
|
@ -64,6 +62,9 @@ pub enum UploadError {
|
||||||
|
|
||||||
#[error("Tried to save an image with an already-taken name")]
|
#[error("Tried to save an image with an already-taken name")]
|
||||||
DuplicateAlias,
|
DuplicateAlias,
|
||||||
|
|
||||||
|
#[error("Error validating Gif file, {0}")]
|
||||||
|
Gif(#[from] GifError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<actix_web::client::SendRequestError> for UploadError {
|
impl From<actix_web::client::SendRequestError> for UploadError {
|
||||||
|
@ -102,9 +103,9 @@ where
|
||||||
impl ResponseError for UploadError {
|
impl ResponseError for UploadError {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
UploadError::DuplicateAlias
|
UploadError::Gif(_)
|
||||||
|
| UploadError::DuplicateAlias
|
||||||
| UploadError::NoFiles
|
| UploadError::NoFiles
|
||||||
| UploadError::ContentType(_)
|
|
||||||
| UploadError::Upload(_) => StatusCode::BAD_REQUEST,
|
| UploadError::Upload(_) => StatusCode::BAD_REQUEST,
|
||||||
UploadError::MissingAlias | UploadError::MissingFilename => StatusCode::NOT_FOUND,
|
UploadError::MissingAlias | UploadError::MissingFilename => StatusCode::NOT_FOUND,
|
||||||
UploadError::InvalidToken => StatusCode::FORBIDDEN,
|
UploadError::InvalidToken => StatusCode::FORBIDDEN,
|
||||||
|
|
|
@ -15,16 +15,10 @@ mod config;
|
||||||
mod error;
|
mod error;
|
||||||
mod processor;
|
mod processor;
|
||||||
mod upload_manager;
|
mod upload_manager;
|
||||||
|
mod validate;
|
||||||
|
|
||||||
use self::{config::Config, error::UploadError, upload_manager::UploadManager};
|
use self::{config::Config, error::UploadError, upload_manager::UploadManager};
|
||||||
|
|
||||||
const ACCEPTED_MIMES: &[mime::Mime] = &[
|
|
||||||
mime::IMAGE_BMP,
|
|
||||||
mime::IMAGE_GIF,
|
|
||||||
mime::IMAGE_JPEG,
|
|
||||||
mime::IMAGE_PNG,
|
|
||||||
];
|
|
||||||
|
|
||||||
const MEGABYTES: usize = 1024 * 1024;
|
const MEGABYTES: usize = 1024 * 1024;
|
||||||
const HOURS: u32 = 60 * 60;
|
const HOURS: u32 = 60 * 60;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{config::Format, error::UploadError, safe_save_file, to_ext, ACCEPTED_MIMES};
|
use crate::{config::Format, error::UploadError, safe_save_file, to_ext, validate::validate_image};
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use futures::stream::{Stream, StreamExt};
|
use futures::stream::{Stream, StreamExt};
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
|
@ -206,7 +206,8 @@ impl UploadManager {
|
||||||
let bytes = read_stream(stream).await?;
|
let bytes = read_stream(stream).await?;
|
||||||
|
|
||||||
let (bytes, content_type) = if validate {
|
let (bytes, content_type) = if validate {
|
||||||
self.validate_image(bytes).await?
|
let format = self.inner.format.clone();
|
||||||
|
validate_image(bytes, format).await?
|
||||||
} else {
|
} else {
|
||||||
(bytes, content_type)
|
(bytes, content_type)
|
||||||
};
|
};
|
||||||
|
@ -233,7 +234,8 @@ impl UploadManager {
|
||||||
let bytes = read_stream(stream).await?;
|
let bytes = read_stream(stream).await?;
|
||||||
|
|
||||||
// -- VALIDATE IMAGE --
|
// -- VALIDATE IMAGE --
|
||||||
let (bytes, content_type) = self.validate_image(bytes).await?;
|
let format = self.inner.format.clone();
|
||||||
|
let (bytes, content_type) = validate_image(bytes, format).await?;
|
||||||
|
|
||||||
// -- DUPLICATE CHECKS --
|
// -- DUPLICATE CHECKS --
|
||||||
|
|
||||||
|
@ -331,40 +333,6 @@ impl UploadManager {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// import & export image using the image crate
|
|
||||||
async fn validate_image(
|
|
||||||
&self,
|
|
||||||
bytes: bytes::Bytes,
|
|
||||||
) -> Result<(bytes::Bytes, mime::Mime), UploadError> {
|
|
||||||
let (img, format) = web::block(move || {
|
|
||||||
let format = image::guess_format(&bytes).map_err(UploadError::InvalidImage)?;
|
|
||||||
let img = image::load_from_memory(&bytes).map_err(UploadError::InvalidImage)?;
|
|
||||||
|
|
||||||
Ok((img, format)) as Result<(image::DynamicImage, image::ImageFormat), UploadError>
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let (format, content_type) = self
|
|
||||||
.inner
|
|
||||||
.format
|
|
||||||
.as_ref()
|
|
||||||
.map(|f| (f.to_image_format(), f.to_mime()))
|
|
||||||
.unwrap_or((format.clone(), valid_format(format)?));
|
|
||||||
|
|
||||||
if ACCEPTED_MIMES.iter().all(|valid| *valid != content_type) {
|
|
||||||
return Err(UploadError::ContentType(content_type));
|
|
||||||
}
|
|
||||||
|
|
||||||
let bytes: bytes::Bytes = web::block(move || {
|
|
||||||
let mut bytes = std::io::Cursor::new(vec![]);
|
|
||||||
img.write_to(&mut bytes, format)?;
|
|
||||||
Ok(bytes::Bytes::from(bytes.into_inner())) as Result<bytes::Bytes, UploadError>
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok((bytes, content_type))
|
|
||||||
}
|
|
||||||
|
|
||||||
// produce a sh256sum of the uploaded file
|
// produce a sh256sum of the uploaded file
|
||||||
async fn hash(&self, bytes: bytes::Bytes) -> Result<Vec<u8>, UploadError> {
|
async fn hash(&self, bytes: bytes::Bytes) -> Result<Vec<u8>, UploadError> {
|
||||||
let mut hasher = self.inner.hasher.clone();
|
let mut hasher = self.inner.hasher.clone();
|
||||||
|
@ -606,13 +574,3 @@ fn variant_key_bounds(hash: &[u8]) -> (Vec<u8>, Vec<u8>) {
|
||||||
|
|
||||||
(start, end)
|
(start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_format(format: image::ImageFormat) -> Result<mime::Mime, UploadError> {
|
|
||||||
match format {
|
|
||||||
image::ImageFormat::Jpeg => Ok(mime::IMAGE_JPEG),
|
|
||||||
image::ImageFormat::Png => Ok(mime::IMAGE_PNG),
|
|
||||||
image::ImageFormat::Gif => Ok(mime::IMAGE_GIF),
|
|
||||||
image::ImageFormat::Bmp => Ok(mime::IMAGE_BMP),
|
|
||||||
_ => Err(UploadError::UnsupportedFormat),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
118
src/validate.rs
Normal file
118
src/validate.rs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
use crate::{config::Format, error::UploadError};
|
||||||
|
use actix_web::web;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use image::{ImageDecoder, ImageEncoder, ImageFormat};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub(crate) enum GifError {
|
||||||
|
#[error("Error decoding gif")]
|
||||||
|
Decode(#[from] gif::DecodingError),
|
||||||
|
|
||||||
|
#[error("Error reading bytes")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
// import & export image using the image crate
|
||||||
|
pub(crate) async fn validate_image(
|
||||||
|
bytes: Bytes,
|
||||||
|
prescribed_format: Option<Format>,
|
||||||
|
) -> Result<(Bytes, mime::Mime), UploadError> {
|
||||||
|
let tup = web::block(move || {
|
||||||
|
if let Some(prescribed) = prescribed_format {
|
||||||
|
let img = image::load_from_memory(&bytes).map_err(UploadError::InvalidImage)?;
|
||||||
|
|
||||||
|
let mime = prescribed.to_mime();
|
||||||
|
|
||||||
|
let mut bytes = Cursor::new(vec![]);
|
||||||
|
img.write_to(&mut bytes, prescribed.to_image_format())?;
|
||||||
|
return Ok((Bytes::from(bytes.into_inner()), mime));
|
||||||
|
}
|
||||||
|
|
||||||
|
let format = image::guess_format(&bytes).map_err(UploadError::InvalidImage)?;
|
||||||
|
|
||||||
|
match format {
|
||||||
|
ImageFormat::Png => Ok((validate_png(bytes)?, mime::IMAGE_PNG)),
|
||||||
|
ImageFormat::Jpeg => Ok((validate_jpg(bytes)?, mime::IMAGE_JPEG)),
|
||||||
|
ImageFormat::Bmp => Ok((validate_bmp(bytes)?, mime::IMAGE_BMP)),
|
||||||
|
ImageFormat::Gif => Ok((validate_gif(bytes)?, mime::IMAGE_GIF)),
|
||||||
|
_ => Err(UploadError::UnsupportedFormat),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(tup)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_png(bytes: Bytes) -> Result<Bytes, UploadError> {
|
||||||
|
let decoder = image::png::PngDecoder::new(Cursor::new(&bytes))?;
|
||||||
|
|
||||||
|
let mut bytes = Cursor::new(vec![]);
|
||||||
|
let encoder = image::png::PNGEncoder::new(&mut bytes);
|
||||||
|
validate_still_image(decoder, encoder)?;
|
||||||
|
|
||||||
|
Ok(Bytes::from(bytes.into_inner()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_jpg(bytes: Bytes) -> Result<Bytes, UploadError> {
|
||||||
|
let decoder = image::jpeg::JpegDecoder::new(Cursor::new(&bytes))?;
|
||||||
|
|
||||||
|
let mut bytes = Cursor::new(vec![]);
|
||||||
|
let encoder = image::jpeg::JPEGEncoder::new(&mut bytes);
|
||||||
|
validate_still_image(decoder, encoder)?;
|
||||||
|
|
||||||
|
Ok(Bytes::from(bytes.into_inner()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_bmp(bytes: Bytes) -> Result<Bytes, UploadError> {
|
||||||
|
let decoder = image::bmp::BmpDecoder::new(Cursor::new(&bytes))?;
|
||||||
|
|
||||||
|
let mut bytes = Cursor::new(vec![]);
|
||||||
|
let encoder = image::bmp::BMPEncoder::new(&mut bytes);
|
||||||
|
validate_still_image(decoder, encoder)?;
|
||||||
|
|
||||||
|
Ok(Bytes::from(bytes.into_inner()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_gif(bytes: Bytes) -> Result<Bytes, GifError> {
|
||||||
|
use gif::{Parameter, SetParameter};
|
||||||
|
|
||||||
|
let mut decoder = gif::Decoder::new(Cursor::new(&bytes));
|
||||||
|
|
||||||
|
decoder.set(gif::ColorOutput::Indexed);
|
||||||
|
|
||||||
|
let mut reader = decoder.read_info()?;
|
||||||
|
|
||||||
|
let width = reader.width();
|
||||||
|
let height = reader.height();
|
||||||
|
let global_palette = reader.global_palette().unwrap_or(&[]);
|
||||||
|
|
||||||
|
let mut bytes = Cursor::new(vec![]);
|
||||||
|
{
|
||||||
|
let mut encoder = gif::Encoder::new(&mut bytes, width, height, global_palette)?;
|
||||||
|
|
||||||
|
gif::Repeat::Infinite.set_param(&mut encoder)?;
|
||||||
|
|
||||||
|
while let Some(frame) = reader.read_next_frame()? {
|
||||||
|
encoder.write_frame(frame)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Bytes::from(bytes.into_inner()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_still_image<'a, D, E>(decoder: D, encoder: E) -> Result<(), UploadError>
|
||||||
|
where
|
||||||
|
D: ImageDecoder<'a>,
|
||||||
|
E: ImageEncoder,
|
||||||
|
{
|
||||||
|
let (width, height) = decoder.dimensions();
|
||||||
|
let color_type = decoder.color_type();
|
||||||
|
let total_bytes = decoder.total_bytes();
|
||||||
|
let mut decoded_bytes = vec![0u8; total_bytes as usize];
|
||||||
|
decoder.read_image(&mut decoded_bytes)?;
|
||||||
|
|
||||||
|
encoder.write_image(&decoded_bytes, width, height, color_type)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in a new issue