Guard against failed commands not erroring

This commit is contained in:
asonix 2023-07-11 17:52:46 -05:00
parent 688990d0cd
commit 943c4d5b58
5 changed files with 92 additions and 5 deletions

View File

@ -111,6 +111,9 @@ pub(crate) enum FfMpegError {
#[error("Invalid file path")] #[error("Invalid file path")]
Path, Path,
#[error("Output from ffmpeg was empty")]
Empty,
} }
impl FfMpegError { impl FfMpegError {
@ -118,6 +121,7 @@ impl FfMpegError {
// Failing validation or ffmpeg bailing probably means bad input // Failing validation or ffmpeg bailing probably means bad input
matches!(self, Self::ValidateDetails(_)) matches!(self, Self::ValidateDetails(_))
|| matches!(self, Self::Process(ProcessError::Status(_))) || matches!(self, Self::Process(ProcessError::Status(_)))
|| matches!(self, Self::Empty)
} }
pub(crate) fn is_not_found(&self) -> bool { pub(crate) fn is_not_found(&self) -> bool {
@ -482,6 +486,10 @@ async fn alpha_pixel_formats() -> Result<HashSet<String>, FfMpegError> {
.await .await
.map_err(FfMpegError::Read)?; .map_err(FfMpegError::Read)?;
if output.is_empty() {
return Err(FfMpegError::Empty);
}
let formats: PixelFormatOutput = serde_json::from_slice(&output).map_err(FfMpegError::Json)?; let formats: PixelFormatOutput = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
Ok(parse_pixel_formats(formats)) Ok(parse_pixel_formats(formats))
@ -562,6 +570,11 @@ where
.read_to_end(&mut output) .read_to_end(&mut output)
.await .await
.map_err(FfMpegError::Read)?; .map_err(FfMpegError::Read)?;
if output.is_empty() {
return Err(FfMpegError::Empty);
}
tokio::fs::remove_file(input_file_str) tokio::fs::remove_file(input_file_str)
.await .await
.map_err(FfMpegError::RemoveFile)?; .map_err(FfMpegError::RemoveFile)?;
@ -643,6 +656,11 @@ async fn pixel_format(input_file: &str) -> Result<String, FfMpegError> {
.read_to_end(&mut output) .read_to_end(&mut output)
.await .await
.map_err(FfMpegError::Read)?; .map_err(FfMpegError::Read)?;
if output.is_empty() {
return Err(FfMpegError::Empty);
}
Ok(String::from_utf8_lossy(&output).trim().to_string()) Ok(String::from_utf8_lossy(&output).trim().to_string())
} }

View File

@ -0,0 +1,14 @@
{
"programs": [
],
"streams": [
{
"width": 0,
"height": 0
}
],
"format": {
"format_name": "webp_pipe"
}
}

View File

@ -1,6 +1,6 @@
use super::{Details, DetailsOutput, PixelFormatOutput}; use super::{Details, DetailsOutput, PixelFormatOutput};
fn details_tests() -> [(&'static str, Option<Details>); 9] { fn details_tests() -> [(&'static str, Option<Details>); 10] {
[ [
("avif", None), ("avif", None),
( (
@ -43,6 +43,7 @@ fn details_tests() -> [(&'static str, Option<Details>); 9] {
}), }),
), ),
("webp", None), ("webp", None),
("animated_webp", None),
] ]
} }

View File

@ -47,6 +47,9 @@ pub(crate) enum MagickError {
#[error("Invalid file path")] #[error("Invalid file path")]
Path, Path,
#[error("Output from ImageMagick was empty")]
Empty,
} }
impl MagickError { impl MagickError {
@ -54,6 +57,7 @@ impl MagickError {
// Failing validation or imagemagick bailing probably means bad input // Failing validation or imagemagick bailing probably means bad input
matches!(self, Self::ValidateDetails(_)) matches!(self, Self::ValidateDetails(_))
|| matches!(self, Self::Process(ProcessError::Status(_))) || matches!(self, Self::Process(ProcessError::Status(_)))
|| matches!(self, Self::Empty)
} }
pub(crate) fn is_not_found(&self) -> bool { pub(crate) fn is_not_found(&self) -> bool {
@ -246,6 +250,10 @@ pub(crate) async fn details_bytes(
.await .await
.map_err(MagickError::Read)?; .map_err(MagickError::Read)?;
if bytes.is_empty() {
return Err(MagickError::Empty);
}
let details_output: Vec<DetailsOutput> = let details_output: Vec<DetailsOutput> =
serde_json::from_slice(&bytes).map_err(MagickError::Json)?; serde_json::from_slice(&bytes).map_err(MagickError::Json)?;
@ -315,6 +323,10 @@ pub(crate) async fn details_store<S: Store + 'static>(
.await .await
.map_err(MagickError::Read)?; .map_err(MagickError::Read)?;
if output.is_empty() {
return Err(MagickError::Empty);
}
let details_output: Vec<DetailsOutput> = let details_output: Vec<DetailsOutput> =
serde_json::from_slice(&output).map_err(MagickError::Json)?; serde_json::from_slice(&output).map_err(MagickError::Json)?;
@ -333,6 +345,11 @@ pub(crate) async fn details_file(path_str: &str) -> Result<Details, MagickError>
.read_to_end(&mut output) .read_to_end(&mut output)
.await .await
.map_err(MagickError::Read)?; .map_err(MagickError::Read)?;
if output.is_empty() {
return Err(MagickError::Empty);
}
tokio::fs::remove_file(path_str) tokio::fs::remove_file(path_str)
.await .await
.map_err(MagickError::RemoveFile)?; .map_err(MagickError::RemoveFile)?;

View File

@ -15,7 +15,7 @@ use tokio::{
use tracing::{Instrument, Span}; use tracing::{Instrument, Span};
#[derive(Debug)] #[derive(Debug)]
struct StatusError; struct StatusError(ExitStatus);
pub(crate) struct Process { pub(crate) struct Process {
child: Child, child: Child,
@ -38,6 +38,7 @@ pin_project_lite::pin_project! {
err_recv: Receiver<std::io::Error>, err_recv: Receiver<std::io::Error>,
err_closed: bool, err_closed: bool,
handle: DropHandle, handle: DropHandle,
eof: bool,
} }
} }
@ -159,7 +160,7 @@ impl Process {
if !status.success() { if !status.success() {
let _ = tx.send(std::io::Error::new( let _ = tx.send(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
&StatusError, StatusError(status),
)); ));
} }
} }
@ -177,6 +178,7 @@ impl Process {
err_recv: rx, err_recv: rx,
err_closed: false, err_closed: false,
handle: DropHandle { inner: handle }, handle: DropHandle { inner: handle },
eof: false,
} }
} }
} }
@ -194,18 +196,53 @@ where
let err_recv = this.err_recv; let err_recv = this.err_recv;
let err_closed = this.err_closed; let err_closed = this.err_closed;
let eof = this.eof;
let inner = this.inner; let inner = this.inner;
if !*err_closed { if !*err_closed {
if let Poll::Ready(res) = Pin::new(err_recv).poll(cx) { if let Poll::Ready(res) = Pin::new(err_recv).poll(cx) {
*err_closed = true; *err_closed = true;
if let Ok(err) = res { if let Ok(err) = res {
return Poll::Ready(Err(err)); return Poll::Ready(Err(err));
} }
if *eof {
return Poll::Ready(Ok(()));
}
} }
} }
inner.poll_read(cx, buf) if !*eof {
let before_size = buf.filled().len();
return match inner.poll_read(cx, buf) {
Poll::Ready(Ok(())) => {
if buf.filled().len() == before_size {
*eof = true;
if !*err_closed {
// reached end of stream & haven't received process signal
return Poll::Pending;
}
}
Poll::Ready(Ok(()))
}
Poll::Ready(Err(e)) => {
*eof = true;
Poll::Ready(Err(e))
}
Poll::Pending => Poll::Pending,
};
}
if *err_closed && *eof {
return Poll::Ready(Ok(()));
}
Poll::Pending
} }
} }
@ -217,7 +254,7 @@ impl Drop for DropHandle {
impl std::fmt::Display for StatusError { impl std::fmt::Display for StatusError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Command failed with bad status") write!(f, "Command failed with bad status: {}", self.0)
} }
} }