use super::inventor::container::{BudgetFile, Metadata, preflight};
use crate::Result;
use image::{DynamicImage, ImageDecoder, ImageFormat, ImageReader, RgbaImage};
use std::{
fs::File,
io::{Cursor, Read, Seek, SeekFrom},
path::Path,
time::{Duration, Instant},
};
const MAX_SOURCE: u64 = 512 * 1024 * 1024;
const MAX_STREAM: u64 = 8 * 1024 * 1024;
const MAX_DECODED: u64 = 64 * 1024 * 1024;
pub fn extract(path: &Path) -> Result<Option<RgbaImage>> {
let started = Instant::now();
let file = File::open(path)?;
let size = file.metadata()?.len();
if size > MAX_SOURCE {
return Err("SolidWorks source exceeds the 512 MiB extraction limit".into());
}
let mut source = BudgetFile::new(file, size, started);
let metadata = preflight(&mut source, size)?.ok_or(
"SolidWorks preview extraction requires compound storage; compressed-section containers are unsupported",
)?;
source.seek(SeekFrom::Start(0))?;
let mut compound = cfb::CompoundFile::open(source)?;
if !compound.is_stream("/ISolidWorksInformation") {
return Err("compound file has no root SolidWorks document information stream".into());
}
let mut failure = None;
for (name, png) in [("PreviewPNG", true), ("Preview", false)] {
check_time(started)?;
if !compound.exists(name) {
continue;
}
let result = (|| -> Result<Option<RgbaImage>> {
let entry = compound.entry(name)?;
if !entry.is_stream() {
return Err("SolidWorks document preview is not a stream".into());
}
let size = entry.len();
if size > MAX_STREAM {
return Err("SolidWorks preview stream exceeds the 8 MiB limit".into());
}
if size == 0 {
return Ok(None);
}
let data = read_stream(&mut compound, &metadata, entry.name(), size)?;
let image = if png {
decode_png(&data)?
} else {
decode_dib(&data)?
};
check_time(started)?;
Ok(Some(image))
})();
match result {
Ok(Some(image)) => return Ok(Some(image)),
Ok(None) => {}
Err(error) => failure = Some(error),
}
}
match failure {
Some(error) => Err(error),
None => Ok(None),
}
}
fn read_stream(
compound: &mut cfb::CompoundFile<BudgetFile>,
metadata: &Metadata,
name: &str,
size: u64,
) -> Result<Vec<u8>> {
metadata.validate_stream(name, size)?;
let mut data = Vec::new();
compound
.open_stream(name)?
.take(MAX_STREAM + 1)
.read_to_end(&mut data)?;
if data.len() as u64 != size {
return Err("SolidWorks preview stream does not match its declared length".into());
}
Ok(data)
}
fn check_time(started: Instant) -> Result<()> {
if started.elapsed() > Duration::from_secs(5) {
return Err("SolidWorks preview processing exceeded the five-second budget".into());
}
Ok(())
}
fn limits() -> image::Limits {
let mut limits = image::Limits::default();
limits.max_image_width = Some(4096);
limits.max_image_height = Some(4096);
limits.max_alloc = Some(MAX_DECODED);
limits
}
fn decode_png(data: &[u8]) -> Result<RgbaImage> {
let mut reader = ImageReader::with_format(Cursor::new(data), ImageFormat::Png);
reader.limits(limits());
Ok(reader.decode()?.into_rgba8())
}
fn decode_dib(data: &[u8]) -> Result<RgbaImage> {
let prefix = data
.get(..4)
.ok_or("truncated SolidWorks DIB preview length")?;
let size = usize::try_from(u32::from_le_bytes(prefix.try_into()?))?;
let dib = data
.get(4..)
.filter(|dib| dib.len() == size)
.ok_or("SolidWorks DIB preview length does not match the stream")?;
let mut decoder = image::codecs::bmp::BmpDecoder::new_without_file_header(Cursor::new(dib))?;
decoder.set_limits(limits())?;
Ok(DynamicImage::from_decoder(decoder)?.into_rgba8())
}
#[cfg(test)]
#[path = "solidworks_tests.rs"]
mod tests;