use std::io::Read;
use std::path::{Path, PathBuf};
use crate::formats::{DecodeOptions, dispatch};
use crate::sniff::{self, KnownFormat};
use crate::{Error, Image, Limits, Result, SandboxSelector};
enum Source {
Path(PathBuf),
Bytes(Vec<u8>),
Reader(Box<dyn Read + Send + 'static>),
}
impl Source {
fn read_all(self) -> Result<Vec<u8>> {
match self {
Self::Path(p) => Ok(std::fs::read(p)?),
Self::Bytes(b) => Ok(b),
Self::Reader(mut r) => {
let mut buf = Vec::new();
r.read_to_end(&mut buf)?;
Ok(buf)
}
}
}
}
pub struct Loader {
source: Source,
extension_hint: Option<String>,
format_hint: Option<KnownFormat>,
limits: Limits,
sandbox: SandboxSelector,
apply_transformations: bool,
render_size_hint: Option<(u32, u32)>,
}
impl Loader {
pub fn new_path<P: AsRef<Path>>(path: P) -> Self {
let path = path.as_ref().to_path_buf();
let extension_hint = path
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_ascii_lowercase());
Self::new(Source::Path(path), extension_hint)
}
pub fn new_bytes(bytes: Vec<u8>) -> Self {
Self::new(Source::Bytes(bytes), None)
}
pub fn new_reader<R: Read + Send + 'static>(reader: R) -> Self {
Self::new(Source::Reader(Box::new(reader)), None)
}
fn new(source: Source, extension_hint: Option<String>) -> Self {
Self {
source,
extension_hint,
format_hint: None,
limits: Limits::default(),
sandbox: SandboxSelector::default(),
apply_transformations: true,
render_size_hint: None,
}
}
pub fn render_size_hint(mut self, width: u32, height: u32) -> Self {
self.render_size_hint = Some((width, height));
self
}
pub fn limits(mut self, limits: Limits) -> Self {
self.limits = limits;
self
}
pub fn sandbox_selector(mut self, sandbox: SandboxSelector) -> Self {
self.sandbox = sandbox;
self
}
pub fn require_sandbox(mut self) -> Self {
self.sandbox.strict = true;
self
}
pub fn apply_transformations(mut self, apply: bool) -> Self {
self.apply_transformations = apply;
self
}
pub fn format_hint(mut self, format: KnownFormat) -> Self {
self.format_hint = Some(format);
self
}
pub fn load(self) -> Result<Image> {
let Loader {
source,
extension_hint,
format_hint,
limits,
sandbox,
apply_transformations,
render_size_hint,
} = self;
let bytes = source.read_all()?;
let format = format_hint
.or_else(|| sniff::detect(&bytes))
.or_else(|| {
extension_hint
.as_deref()
.and_then(KnownFormat::from_extension)
})
.ok_or(Error::UnsupportedFormat)?;
let opts = DecodeOptions {
limits,
apply_transformations,
render_size_hint,
};
let (mut image, posture) = crate::sandbox::run_in_worker(sandbox, limits, move || {
dispatch(format, &bytes, &opts)
})?;
#[cfg(feature = "metadata")]
crate::metadata::apply_orientation_if_present(&mut image, apply_transformations);
image.set_sandbox_posture(posture);
Ok(image)
}
}
impl std::fmt::Debug for Loader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Loader")
.field(
"source",
&match &self.source {
Source::Path(p) => format!("Path({})", p.display()),
Source::Bytes(b) => format!("Bytes({} bytes)", b.len()),
Source::Reader(_) => "Reader(..)".to_string(),
},
)
.field("extension_hint", &self.extension_hint)
.field("format_hint", &self.format_hint)
.field("limits", &self.limits)
.field("sandbox", &self.sandbox)
.field("apply_transformations", &self.apply_transformations)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use tempfile::tempdir;
fn truncated_png_signature() -> Vec<u8> {
let mut v = b"\x89PNG\r\n\x1a\n".to_vec();
v.extend_from_slice(b"\0\0\0\rIHDR");
v
}
#[test]
fn bytes_loader_sniffs_and_dispatches() {
let err = Loader::new_bytes(truncated_png_signature())
.load()
.unwrap_err();
assert!(matches!(
err,
Error::Malformed(_) | Error::Io(_) | Error::UnsupportedFormat
));
}
#[test]
fn unknown_bytes_return_unsupported_format() {
let err = Loader::new_bytes(b"garbage".to_vec()).load().unwrap_err();
assert!(matches!(err, Error::UnsupportedFormat));
}
#[test]
fn path_extension_resolves_tga() {
let dir = tempdir().unwrap();
let path = dir.path().join("dummy.tga");
std::fs::write(&path, b"\0\0\x02\0\0\0\0\0\0\0\0\0").unwrap();
let err = Loader::new_path(&path).load().unwrap_err();
assert!(matches!(
err,
Error::UnsupportedFormat | Error::Malformed(_) | Error::Decoder { .. }
));
}
#[test]
fn format_hint_overrides_sniffer() {
let err = Loader::new_bytes(b"".to_vec())
.format_hint(KnownFormat::Png)
.load()
.unwrap_err();
assert!(matches!(
err,
Error::Malformed(_) | Error::Io(_) | Error::UnsupportedFormat
));
}
#[test]
fn reader_buffers_into_memory() {
let cursor = Cursor::new(truncated_png_signature());
let err = Loader::new_reader(cursor).load().unwrap_err();
assert!(matches!(
err,
Error::Malformed(_) | Error::Io(_) | Error::UnsupportedFormat
));
}
#[test]
fn builder_methods_chain() {
let l = Loader::new_bytes(vec![0u8; 8])
.limits(Limits::unlimited())
.sandbox_selector(SandboxSelector::none())
.require_sandbox()
.apply_transformations(false)
.format_hint(KnownFormat::Tga);
let dbg = format!("{l:?}");
assert!(dbg.contains("Tga"));
assert!(dbg.contains("apply_transformations: false"));
}
#[test]
fn missing_path_returns_io_error() {
let err = Loader::new_path("/nonexistent/path/to/image.png")
.load()
.unwrap_err();
assert!(matches!(err, Error::Io(_)));
}
}