mod section_keys;
use std::cell::RefCell;
use std::fs::File;
use std::io::{Cursor, Read, Seek};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{fmt, str};
use cfb::CompoundFile;
use section_keys::update_section_keys;
use crate::common::{buf2lstr, split_altium_map, Rgb, UniqueId};
use crate::error::{AddContext, ErrorKind};
use crate::font::{Font, FontCollection};
use crate::parse::ParseUtf8;
use crate::sch::{storage::Storage, Component, SheetStyle};
use crate::Error;
const DATA_DEFAULT_CAP: usize = 200;
pub struct SchLib<F> {
cfile: RefCell<CompoundFile<F>>,
header: SchLibMeta,
storage: Arc<Storage>,
}
impl SchLib<File> {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let cfile = cfb::open(&path)?;
Self::from_cfile(cfile)
.context("parsing SchLib")
.or_context(|| format!("with file {}", path.as_ref().display()))
}
}
impl<'a> SchLib<Cursor<&'a [u8]>> {
pub fn from_buffer(buf: &'a [u8]) -> Result<Self, Error> {
let cfile = cfb::CompoundFile::open(Cursor::new(buf))?;
Self::from_cfile(cfile).context("parsing SchLib from Cursor")
}
}
impl<F: Read + Seek> SchLib<F> {
pub fn unique_id(&self) -> UniqueId {
self.header.unique_id
}
pub fn component_meta(&self) -> &[ComponentMeta] {
&self.header.components
}
pub fn get_component(&self, libref: &str) -> Option<Component> {
self.try_get_component(libref).unwrap()
}
fn try_get_component(&self, libref: &str) -> Result<Option<Component>, Error> {
let Some(meta) = &self
.header
.components
.iter()
.find(|meta| &*meta.libref == libref)
else {
return Ok(None);
};
let key = &meta.sec_key;
let data_path = PathBuf::from_iter([key, "Data"]);
let _pintext_path = PathBuf::from_iter([key, "PinTextData"]);
let _pinwide_path = PathBuf::from_iter([key, "PinWideText"]);
let mut buf = Vec::with_capacity(DATA_DEFAULT_CAP);
{
let mut cfile_ref = self.cfile.borrow_mut();
let mut stream = cfile_ref.open_stream(&data_path).map_err(|e| {
let path_disp = data_path.display();
Error::from(e).context(format!("reading required stream `{path_disp}`",))
})?;
stream.read_to_end(&mut buf).unwrap();
}
let comp = Component::from_buf(
libref,
&buf,
Arc::clone(&self.header.fonts),
Arc::clone(&self.storage),
)?;
Ok(Some(comp))
}
pub fn components(&self) -> ComponentsIter<'_, F> {
ComponentsIter {
schlib: self,
current: 0,
}
}
pub fn fonts(&self) -> impl Iterator<Item = &Font> {
self.header.fonts.iter()
}
pub fn storage(&self) -> &Storage {
&self.storage
}
fn from_cfile(mut cfile: CompoundFile<F>) -> Result<Self, Error> {
let mut tmp_buf: Vec<u8> = Vec::new();
let mut header = SchLibMeta::parse_cfile(&mut cfile, &mut tmp_buf)?;
tmp_buf.clear();
let storage = Storage::parse_cfile(&mut cfile, &mut tmp_buf)?;
tmp_buf.clear();
update_section_keys(&mut cfile, &mut tmp_buf, &mut header)?;
Ok(Self {
cfile: RefCell::new(cfile),
header,
storage: storage.into(),
})
}
}
impl<F> fmt::Debug for SchLib<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SchLib")
.field("header", &self.header)
.finish_non_exhaustive()
}
}
pub struct ComponentsIter<'a, F> {
schlib: &'a SchLib<F>,
current: usize,
}
impl<'a, F: Read + Seek> Iterator for ComponentsIter<'a, F> {
type Item = Component;
fn next(&mut self) -> Option<Self::Item> {
let meta = self.schlib.component_meta();
if self.current >= meta.len() {
None
} else {
let libref = meta[self.current].libref();
let ret = self
.schlib
.try_get_component(libref)
.expect("component should exist!");
self.current += 1;
Some(ret.unwrap())
}
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct SchLibMeta {
weight: u32,
minor_version: u8,
unique_id: UniqueId,
fonts: Arc<FontCollection>,
use_mbcs: bool,
is_boc: bool,
sheet_style: SheetStyle,
border_on: bool,
sheet_number_space_size: u16,
area_color: Rgb,
snap_grid_on: bool,
snap_grid_size: u16,
visible_grid_on: bool,
visible_grid_size: u16,
custom_x: u32,
custom_y: u32,
use_custom_sheet: bool,
reference_zones_on: bool,
display_unit: u16, components: Vec<ComponentMeta>,
}
impl SchLibMeta {
const STREAMNAME: &'static str = "FileHeader";
const HEADER: &'static [u8] =
b"HEADER=Protel for Windows - Schematic Library Editor Binary File Version 5.0";
const PFX_LEN: usize = 5;
const SFX: &'static [u8] = &[0x00];
const FONT_NAME_PFX: &'static [u8] = b"FontName";
const FONT_SIZE_PFX: &'static [u8] = b"Size";
const COMP_LIBREF_PFX: &'static [u8] = b"LibRef";
const COMP_DESC_PFX: &'static [u8] = b"CompDescr";
const COMP_PARTCOUNT_PFX: &'static [u8] = b"PartCount";
fn parse_cfile<F: Read + Seek>(
cfile: &mut CompoundFile<F>,
tmp_buf: &mut Vec<u8>,
) -> Result<Self, ErrorKind> {
let mut stream = cfile.open_stream(Self::STREAMNAME)?;
stream.read_to_end(tmp_buf)?;
let to_parse = tmp_buf
.get(Self::PFX_LEN..)
.ok_or(ErrorKind::new_invalid_stream(Self::STREAMNAME, 0))?
.strip_suffix(Self::SFX)
.ok_or(ErrorKind::new_invalid_stream(
Self::STREAMNAME,
tmp_buf.len(),
))?;
let sep_pos = to_parse
.iter()
.position(|b| *b == b'|')
.unwrap_or(to_parse.len());
if &to_parse[..sep_pos] != Self::HEADER {
return Err(ErrorKind::new_invalid_stream(
Self::STREAMNAME,
Self::PFX_LEN,
));
}
let mut skip_keys = Vec::new();
let mut ret = Self::default();
let mut fonts = Vec::new();
for (mut key, val) in split_altium_map(to_parse) {
if key.starts_with(b"%UTF8%") {
key = &key[6..];
skip_keys.push(key);
} else if skip_keys.contains(&key) {
continue;
}
match key {
b"HEADER" => continue,
b"Weight" => ret.weight = val.parse_as_utf8()?,
b"MinorVersion" => ret.minor_version = val.parse_as_utf8()?,
b"UniqueID" => ret.unique_id = val.parse_as_utf8()?,
b"FontIdCount" => fonts = vec![Font::default(); val.parse_as_utf8()?],
b"UseMBCS" => ret.use_mbcs = val.parse_as_utf8()?,
b"IsBOC" => ret.is_boc = val.parse_as_utf8()?,
b"SheetStyle" => ret.sheet_style = val.parse_as_utf8()?,
b"BorderOn" => ret.border_on = val.parse_as_utf8()?,
b"SheetNumberSpaceSize" => ret.sheet_number_space_size = val.parse_as_utf8()?,
b"AreaColor" => ret.area_color = val.parse_as_utf8()?,
b"SnapGridOn" => ret.snap_grid_on = val.parse_as_utf8()?,
b"SnapGridSize" => ret.snap_grid_size = val.parse_as_utf8()?,
b"VisibleGridOn" => ret.visible_grid_on = val.parse_as_utf8()?,
b"VisibleGridSize" => ret.visible_grid_size = val.parse_as_utf8()?,
b"CustomX" => ret.custom_x = val.parse_as_utf8()?,
b"CustomY" => ret.custom_y = val.parse_as_utf8()?,
b"UseCustomSheet" => ret.use_custom_sheet = val.parse_as_utf8()?,
b"ReferenceZonesOn" => ret.reference_zones_on = val.parse_as_utf8()?,
b"Display_Unit" => ret.display_unit = val.parse_as_utf8()?,
b"CompCount" => {
ret.components = vec![ComponentMeta::default(); val.parse_as_utf8()?];
}
x if x.starts_with(Self::FONT_NAME_PFX) => {
let idx: usize = key[Self::FONT_NAME_PFX.len()..].parse_as_utf8()?;
fonts[idx - 1].name = val.parse_as_utf8()?;
}
x if x.starts_with(Self::FONT_SIZE_PFX) => {
let idx: usize = key[Self::FONT_SIZE_PFX.len()..].parse_as_utf8()?;
fonts[idx - 1].size = val.parse_as_utf8()?;
}
x if x.starts_with(Self::COMP_LIBREF_PFX) => {
let idx: usize = key[Self::COMP_LIBREF_PFX.len()..].parse_as_utf8()?;
let tmp: Box<str> = val.parse_as_utf8()?;
ret.components[idx].libref = tmp.clone();
ret.components[idx].sec_key = tmp;
}
x if x.starts_with(Self::COMP_DESC_PFX) => {
let idx: usize = key[Self::COMP_DESC_PFX.len()..].parse_as_utf8()?;
ret.components[idx].description = val.parse_as_utf8()?;
}
x if x.starts_with(Self::COMP_PARTCOUNT_PFX) => {
let idx: usize = key[Self::COMP_PARTCOUNT_PFX.len()..].parse_as_utf8()?;
ret.components[idx].part_count = val.parse_as_utf8()?;
}
_ => log::warn!(
"unsupported SchLib file header key {}:{}",
buf2lstr(key),
buf2lstr(val)
),
}
}
ret.fonts = Arc::new(fonts.into());
Ok(ret)
}
}
#[derive(Clone, Debug, Default)]
pub struct ComponentMeta {
libref: Box<str>,
sec_key: Box<str>,
description: Box<str>,
part_count: u16,
}
impl ComponentMeta {
pub fn libref(&self) -> &str {
&self.libref
}
pub fn description(&self) -> &str {
&self.description
}
#[allow(unused)]
fn part_count(&self) -> u16 {
self.part_count
}
}