use core::fmt;
use std::io::{Cursor, Read, Seek};
use std::sync::Mutex;
use std::{collections::BTreeMap, sync::Arc};
use cfb::CompoundFile;
use flate2::read::ZlibDecoder;
use crate::common::split_altium_map;
use crate::error::{AddContext, TruncBuf};
use crate::parse::{extract_sized_buf, extract_sized_utf8_buf, BufLenMatch};
use crate::{Error, ErrorKind};
#[derive(Debug, Default)]
pub struct Storage(BTreeMap<Box<str>, Mutex<CompressedData>>);
#[derive(Clone)]
pub enum CompressedData {
Compressed(Box<[u8]>),
Expanded(Arc<[u8]>),
}
impl Storage {
const STREAMNAME: &'static str = "Storage";
pub fn get_data(&self, path: &str) -> Option<Arc<[u8]>> {
self.try_get_data(path).map(Result::unwrap)
}
pub fn try_get_data(&self, path: &str) -> Option<Result<Arc<[u8]>, Error>> {
let Some(mtx) = self.0.get(path) else {
return None;
};
let data_res = (*mtx.lock().unwrap()).uncompressed();
Some(data_res.or_context(|| format!("accessing data for '{path}'")))
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.0.keys().map(AsRef::as_ref)
}
pub(crate) fn parse_cfile<F: Read + Seek>(
cfile: &mut CompoundFile<F>,
tmp_buf: &mut Vec<u8>,
) -> Result<Self, Error> {
let mut stream = cfile.open_stream(Self::STREAMNAME)?;
stream.read_to_end(tmp_buf)?;
Self::parse(tmp_buf)
}
pub(crate) fn parse(buf: &[u8]) -> Result<Self, Error> {
let (header, mut rest) =
extract_sized_buf(buf, BufLenMatch::U32, true).context("parsing storage")?;
let mut map_kv = split_altium_map(header);
let Some((b"HEADER", b"Icon storage")) = map_kv.next() else {
return Err(
ErrorKind::new_invalid_header(header, "Icon storage").context("parsing storage")
);
};
let Some((b"Weight", _weight_val)) = map_kv.next() else {
assert!(
rest.is_empty(),
"weight not present but rest was not empty at {}",
TruncBuf::new(rest)
);
return Ok(Self::default());
};
let mut map = BTreeMap::new();
let mut path;
let mut data;
while !rest.is_empty() {
let Some([_, _, _, 0x01, 0xd0]) = rest.get(..5) else {
return Err(ErrorKind::InvalidStorageData(rest.into()).context("parsing storage"));
};
rest = &rest[5..];
(path, rest) = extract_sized_utf8_buf(rest, BufLenMatch::U8, false)?;
(data, rest) = extract_sized_buf(rest, BufLenMatch::U32, false)?;
map.insert(
path.into(),
Mutex::new(CompressedData::Compressed(data.into())),
);
}
Ok(Self(map))
}
}
impl CompressedData {
fn uncompressed(&mut self) -> Result<Arc<[u8]>, ErrorKind> {
let compressed = match self {
Self::Compressed(d) => d,
Self::Expanded(arc) => return Ok(Arc::clone(arc)),
};
let mut tmp_buf = Vec::with_capacity(compressed.len() * 2);
let mut z = ZlibDecoder::new(&**compressed);
z.read_to_end(&mut tmp_buf)?;
let img = image::load_from_memory(&tmp_buf)?;
tmp_buf.clear();
let mut img = img.into_rgba8();
img.pixels_mut()
.filter(|px| px[0] == 255 && px[1] == 255 && px[2] == 255)
.for_each(|px| px[3] = 0);
img.write_to(
&mut Cursor::new(&mut tmp_buf),
image::ImageOutputFormat::Png,
)?;
let arc: Arc<[u8]> = tmp_buf.into();
*self = Self::Expanded(Arc::clone(&arc));
Ok(arc)
}
}
impl fmt::Debug for CompressedData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (name, data): (_, &[u8]) = match self {
CompressedData::Compressed(v) => ("Compressed", v),
CompressedData::Expanded(v) => ("Expanded", v),
};
write!(f, "{name}({:02x})", &TruncBuf::new(data))
}
}
pub fn file_name(path: &str) -> &str {
let is_sep = |ch: char| ch == '\\' || ch == '/';
let rpos = path.rfind(is_sep).unwrap_or(path.len());
path[(rpos)..].trim_start_matches(is_sep)
}