From a98d426951b6b1e2304068875115210fcc6f1851 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 18 Sep 2023 21:43:24 -0400 Subject: [PATCH] Cherrypick altium work from the ecad-gui branch --- altium-macros/src/lib.rs | 211 +++++++++++++++++++++-------- altium/Cargo.toml | 7 +- altium/src/common.rs | 27 +++- altium/src/error.rs | 60 ++++++--- altium/src/font.rs | 20 ++- altium/src/parse/bin.rs | 40 ++++-- altium/src/parse/utf8.rs | 2 +- altium/src/sch.rs | 2 +- altium/src/sch/component.rs | 49 +------ altium/src/sch/pin.rs | 69 +++++++++- altium/src/sch/record.rs | 213 +++++++++++++++++++++++++++--- altium/src/sch/record/draw.rs | 118 +++++++++++++++-- altium/src/sch/record/parse.rs | 54 ++++++++ altium/src/sch/schdoc.rs | 134 ++++++++++++++++++- altium/src/sch/schlib.rs | 22 +-- altium/src/sch/storage.rs | 18 +-- altium/tests/include_test_util.rs | 11 ++ altium/tests/test_schdoc.rs | 13 ++ altium/tests/test_schlib.rs | 12 ++ 19 files changed, 876 insertions(+), 206 deletions(-) create mode 100644 altium/src/sch/record/parse.rs create mode 100644 altium/tests/include_test_util.rs create mode 100644 altium/tests/test_schdoc.rs diff --git a/altium-macros/src/lib.rs b/altium-macros/src/lib.rs index 0dc80cc..03ef58b 100644 --- a/altium-macros/src/lib.rs +++ b/altium-macros/src/lib.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; use convert_case::{Case, Casing}; use proc_macro::TokenStream; -use proc_macro2::{Ident, Literal, Span, TokenStream as TokenStream2, TokenTree}; +use proc_macro2::{Delimiter, Ident, Literal, Span, TokenStream as TokenStream2, TokenTree}; use quote::{quote, ToTokens}; use syn::{parse2, Attribute, Data, DeriveInput, Meta, Type}; @@ -45,6 +45,7 @@ fn inner(tokens: TokenStream2) -> syn::Result { panic!("record id should be a literal"); }; + // Handle cases where we want to box the struct let use_box = match struct_attr_map.remove("use_box") { Some(TokenTree::Ident(val)) if val == "true" => true, Some(TokenTree::Ident(val)) if val == "false" => true, @@ -52,6 +53,13 @@ fn inner(tokens: TokenStream2) -> syn::Result { None => false, }; + // Handle cases where our struct doesn't have the same name as the enum variant + let record_variant = match struct_attr_map.remove("record_variant") { + Some(TokenTree::Ident(val)) => val, + Some(v) => panic!("Expected ident but got {v:?}"), + None => name.clone(), + }; + error_if_map_not_empty(&struct_attr_map); let mut match_stmts: Vec = Vec::new(); @@ -72,7 +80,11 @@ fn inner(tokens: TokenStream2) -> syn::Result { .remove("count") .expect("missing 'count' attribute"); - process_array(&name, &field_name, count_ident, &mut match_stmts); + let arr_map = field_attr_map + .remove("map") + .expect("missing 'map' attribute"); + + process_array(&name, &field_name, count_ident, arr_map, &mut match_stmts); error_if_map_not_empty(&field_attr_map); continue; } else if arr_val_str != "false" { @@ -129,7 +141,9 @@ fn inner(tokens: TokenStream2) -> syn::Result { let def_flag = quote! { let mut #flag_ident: bool = false; }; let check_flag = quote! { if #flag_ident { - ::log::debug!("skipping {} after finding utf8", #field_name_str); + ::log::debug!(concat!( + "skipping ", #field_name_str, " after finding utf8 version" + )); continue; } }; @@ -164,9 +178,9 @@ fn inner(tokens: TokenStream2) -> syn::Result { } let ret_val = if use_box { - quote! { Ok(SchRecord::#name(Box::new(ret))) } + quote! { Ok(SchRecord::#record_variant(Box::new(ret))) } } else { - quote! { Ok(SchRecord::#name(ret)) } + quote! { Ok(SchRecord::#record_variant(ret)) } }; let ret = quote! { @@ -198,7 +212,7 @@ fn inner(tokens: TokenStream2) -> syn::Result { /// Next type of token we are expecting #[derive(Clone, Debug, PartialEq)] -enum AttrState { +enum AttrParseState { Key, /// Contains the last key we had Eq(String), @@ -218,35 +232,32 @@ fn parse_attrs(attrs: Vec) -> Option> { panic!("invalid usage; use `#[from_record(...=..., ...)]`"); }; - let mut state = AttrState::Key; + let mut state = AttrParseState::Key; let mut map = BTreeMap::new(); for token in list.tokens { match state { - AttrState::Key => { + AttrParseState::Key => { let TokenTree::Ident(idtoken) = token else { panic!("expected an identifier at {token}"); }; - state = AttrState::Eq(idtoken.to_string()); + state = AttrParseState::Eq(idtoken.to_string()); } - AttrState::Eq(key) => { - match token { - TokenTree::Punct(v) if v.as_char() == '=' => (), - _ => panic!("expected `=` at {token}"), + AttrParseState::Eq(key) => { + if !matches!(&token, TokenTree::Punct(v) if v.as_char() == '=') { + panic!("expected `=` at {token}"); } - - state = AttrState::Val(key); + state = AttrParseState::Val(key); } - AttrState::Val(key) => { + AttrParseState::Val(key) => { map.insert(key, token); - state = AttrState::Comma; + state = AttrParseState::Comma; } - AttrState::Comma => { - match token { - TokenTree::Punct(v) if v.as_char() == ',' => (), - _ => panic!("expected `,` at {token}"), - }; - state = AttrState::Key; + AttrParseState::Comma => { + if !matches!(&token, TokenTree::Punct(v) if v.as_char() == ',') { + panic!("expected `,` at {token}"); + } + state = AttrParseState::Key; } } } @@ -254,6 +265,74 @@ fn parse_attrs(attrs: Vec) -> Option> { Some(map) } +/// Next type of token we are expecting +#[derive(Clone, Debug, PartialEq)] +enum MapParseState { + Key, + /// Contains the last key we had + Dash(Ident), + Gt(Ident), + Val(Ident), + Comma, +} + +/// Parse a `(X -> x, Y -> y)` map that tells us how to set members based on +/// found items in an array. +/// +/// E.g. with the above, `X1` will set `record[1].x` +fn parse_map(map: TokenTree) -> Vec<(Ident, Ident)> { + let mut ret = Vec::new(); + + let TokenTree::Group(group) = map else { + panic!("expected group but got {map:?}") + }; + + if group.delimiter() != Delimiter::Parenthesis { + panic!("expected parenthese but got {:?}", group.delimiter()); + }; + + let mut state = MapParseState::Key; + + for token in group.stream() { + match state { + MapParseState::Key => { + let TokenTree::Ident(idtoken) = token else { + panic!("expected an identifier at {token}"); + }; + state = MapParseState::Dash(idtoken); + } + MapParseState::Dash(key) => { + if !matches!(&token, TokenTree::Punct(v) if v.as_char() == '-') { + panic!("expected `->` at {token}"); + } + state = MapParseState::Gt(key); + } + MapParseState::Gt(key) => { + if !matches!(&token, TokenTree::Punct(v) if v.as_char() == '>') { + panic!("expected `->` at {token}"); + } + state = MapParseState::Val(key); + } + MapParseState::Val(key) => { + let TokenTree::Ident(ident) = token else { + panic!("expcected ident but got {token}"); + }; + ret.push((key, ident)); + state = MapParseState::Comma; + } + MapParseState::Comma => { + if !matches!(&token, TokenTree::Punct(v) if v.as_char() == ',') { + panic!("expected `,` at {token}"); + } + + state = MapParseState::Key; + } + } + } + + ret +} + fn error_if_map_not_empty(map: &BTreeMap) { assert!(map.is_empty(), "unexpected pairs {map:?}"); } @@ -263,11 +342,13 @@ fn process_array( name: &Ident, field_name: &Ident, count_ident_tt: TokenTree, + arr_map_tt: TokenTree, match_stmts: &mut Vec, ) { let TokenTree::Literal(match_pat) = count_ident_tt else { panic!("expected a literal for `count`"); }; + let arr_map = parse_map(arr_map_tt); let field_name_str = field_name.to_string(); let match_pat_str = match_pat.to_string(); @@ -279,45 +360,69 @@ fn process_array( stringify!(#name), "` (via proc macro array)" ))?; - ret.#field_name = vec![crate::common::Location::default(); count]; + ret.#field_name = vec![Default::default(); count].into(); }, + }; - // Set an X value if given - xstr if crate::common::is_number_pattern(xstr, b'X') => { - let idx: usize = xstr.strip_prefix(b"X").unwrap() - .parse_as_utf8() - .or_context(|| format!( - "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) - ))?; + match_stmts.push(count_match); - let x = val.parse_as_utf8().or_context(|| format!( - "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) - ))?; + for (match_pfx, assign_value) in arr_map { + let match_pfx_bstr = Literal::byte_string(match_pfx.to_string().as_bytes()); - ret.#field_name[idx - 1].x = x; - }, + let item_match = quote! { + match_val if crate::common::is_number_pattern(match_val, #match_pfx_bstr) => { + let idx: usize = match_val.strip_prefix(#match_pfx_bstr).unwrap() + .parse_as_utf8() + .or_context(|| format!( + "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + String::from_utf8_lossy(match_val), #field_name_str, stringify!(#name) + ))?; - // Set a Y value if given - ystr if crate::common::is_number_pattern(ystr, b'Y') => { - let idx: usize = ystr.strip_prefix(b"Y").unwrap() - .parse_as_utf8() - .or_context(|| format!( + let parsed_val = val.parse_as_utf8().or_context(|| format!( "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) + String::from_utf8_lossy(match_val), #field_name_str, stringify!(#name) ))?; - let y = val.parse_as_utf8().or_context(|| format!( - "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) - ))?; - - ret.#field_name[idx - 1].y = y; - }, - }; + ret.#field_name[idx - 1].#assign_value = parsed_val; + }, + }; + match_stmts.push(item_match); + } - match_stmts.push(count_match); + // // Set an X value if given + // xstr if crate::common::is_number_pattern(xstr, b'X') => { + // let idx: usize = xstr.strip_prefix(b"X").unwrap() + // .parse_as_utf8() + // .or_context(|| format!( + // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + // String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) + // ))?; + + // let x = val.parse_as_utf8().or_context(|| format!( + // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + // String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) + // ))?; + + // ret.#field_name[idx - 1].x = x; + // }, + + // // Set a Y value if given + // ystr if crate::common::is_number_pattern(ystr, b'Y') => { + // let idx: usize = ystr.strip_prefix(b"Y").unwrap() + // .parse_as_utf8() + // .or_context(|| format!( + // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + // String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) + // ))?; + + // let y = val.parse_as_utf8().or_context(|| format!( + // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + // String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) + // ))?; + + // ret.#field_name[idx - 1].y = y; + // }, + // }; } /// From a field in our struct, create the name we should match by diff --git a/altium/Cargo.toml b/altium/Cargo.toml index 0468e82..ba61c43 100644 --- a/altium/Cargo.toml +++ b/altium/Cargo.toml @@ -9,7 +9,9 @@ description = "A library for processing Altium file types" [dependencies] altium-macros = { path = "../altium-macros", version = "0.1.0" } base64 = "0.21.2" -cfb = "0.8.1" +# Use custom rev so we get debug outputs +cfb = { git = "https://github.com/mdsteele/rust-cfb.git", rev = "5c5279d6" } +# cfb = "0.8.1" flate2 = "1.0.26" # image = "0.24.6" image = { version = "0.24.6", default-features = false, features = ["png", "bmp", "jpeg"] } @@ -25,6 +27,9 @@ svg = "0.13.1" uuid = { version = "1.4.1", features = ["v1", "v4", "fast-rng"]} xml-rs = "0.8.16" +[dev-dependencies] +env_logger = "0.10.0" + [package.metadata.release] shared-version = true diff --git a/altium/src/common.rs b/altium/src/common.rs index 211043d..6f51ba4 100644 --- a/altium/src/common.rs +++ b/altium/src/common.rs @@ -39,6 +39,15 @@ impl Location { } } +impl From<(i32, i32)> for Location { + fn from(value: (i32, i32)) -> Self { + Self { + x: value.0, + y: value.1, + } + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq)] pub enum Visibility { Hidden, @@ -46,6 +55,12 @@ pub enum Visibility { Visible, } +impl FromUtf8<'_> for Visibility { + fn from_utf8(buf: &[u8]) -> Result { + todo!("{}", String::from_utf8_lossy(buf)) + } +} + /// A unique ID /// // TODO: figure out what file types use this exact format @@ -202,6 +217,12 @@ impl Rotation { } } +impl FromUtf8<'_> for Rotation { + fn from_utf8(buf: &[u8]) -> Result { + todo!("{}", String::from_utf8_lossy(buf)) + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq)] pub enum ReadOnlyState { #[default] @@ -216,7 +237,7 @@ impl TryFrom for ReadOnlyState { let res = match value { x if x == Self::ReadWrite as u8 => Self::ReadWrite, x if x == Self::ReadOnly as u8 => Self::ReadOnly, - _ => return Err(ErrorKind::SheetStyle(value)), + _ => return Err(ErrorKind::ReadOnlyState(value)), }; Ok(res) @@ -249,9 +270,9 @@ pub enum PosVert { } /// Verify a number pattern matches, e.g. `X100` -pub fn is_number_pattern(s: &[u8], prefix: u8) -> bool { +pub fn is_number_pattern(s: &[u8], prefix: &[u8]) -> bool { if let Some(stripped) = s - .strip_prefix(&[prefix]) + .strip_prefix(prefix) .map(|s| s.strip_prefix(&[b'-']).unwrap_or(s)) { if stripped.iter().all(u8::is_ascii_digit) { diff --git a/altium/src/error.rs b/altium/src/error.rs index 0032022..792391f 100644 --- a/altium/src/error.rs +++ b/altium/src/error.rs @@ -1,5 +1,6 @@ //! Error types used throughout this crate +use std::borrow::Cow; use std::cmp::min; use std::fmt; use std::fmt::Write; @@ -71,39 +72,47 @@ impl fmt::Debug for Error { #[derive(Debug)] #[non_exhaustive] pub enum ErrorKind { - Io(io::Error), + BufferTooShort(usize, TruncBuf), + ElectricalType(u8), + ExpectedBool(String), + ExpectedColor(TruncBuf), + ExpectedFloat(String, ParseFloatError), + ExpectedInt(String, ParseIntError), + ExpectedNul(TruncBuf), + FileType(String, &'static str), + Image(image::ImageError), IniFormat(Box), - MissingSection(String), - MissingUniqueId(String), - InvalidUniqueId(TruncBuf), + InvalidHeader(Box, &'static str), + InvalidKey(Box), InvalidStorageData(TruncBuf), - FileType(String, &'static str), InvalidStream(Box, usize), - RequiredSplit(String), - Utf8(Utf8Error, String), - ExpectedInt(String, ParseIntError), - ExpectedFloat(String, ParseFloatError), - InvalidKey(Box), - InvalidHeader(Box), - ExpectedBool(String), - ExpectedColor(TruncBuf), - SheetStyle(u8), - ReadOnlyState(u8), + InvalidUniqueId(TruncBuf), + Io(io::Error), Justification(u8), + MissingSection(String), + MissingUniqueId, Pin(PinError), - BufferTooShort(usize, TruncBuf), - Image(image::ImageError), + ReadOnlyState(u8), + RequiredSplit(String), + SheetStyle(u8), + Utf8(Utf8Error, String), } impl fmt::Display for ErrorKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ErrorKind::IniFormat(e) => write!(f, "error parsing ini: {e}"), + ErrorKind::ElectricalType(e) => write!(f, "invalid electrical type {e}"), ErrorKind::Io(e) => write!(f, "io error: {e}"), ErrorKind::MissingSection(e) => write!(f, "missing required section `{e}`"), - ErrorKind::MissingUniqueId(e) => write!(f, "bad or missing unique ID section `{e}`"), + ErrorKind::MissingUniqueId => write!(f, "bad or missing unique ID section"), ErrorKind::InvalidUniqueId(e) => { - write!(f, "invalid unique ID section `{e}` (len {})", e.orig_len) + write!( + f, + "invalid unique ID section `{e}` (len {}, `{}`)", + e.orig_len, + e.as_str() + ) } ErrorKind::FileType(n, ty) => write!(f, "file `{n}` is not a valid {ty} file"), ErrorKind::InvalidStream(s, n) => { @@ -116,7 +125,7 @@ impl fmt::Display for ErrorKind { write!(f, "invalid storage data near `{e:x}`") } ErrorKind::Utf8(e, s) => write!(f, "utf8 error: {e} at '{s}'"), - ErrorKind::InvalidHeader(e) => write!(f, "invalid header '{e}'"), + ErrorKind::InvalidHeader(e, v) => write!(f, "invalid header '{e}'; expected `{v}`"), ErrorKind::ExpectedInt(s, e) => write!(f, "error parsing integer from `{s}`: {e}"), ErrorKind::ExpectedFloat(s, e) => write!(f, "error parsing float from `{s}`: {e}"), ErrorKind::InvalidKey(s) => write!(f, "invalid key found: `{s}`"), @@ -132,6 +141,7 @@ impl fmt::Display for ErrorKind { b.len() ), ErrorKind::Image(e) => write!(f, "image error: {e}"), + ErrorKind::ExpectedNul(e) => write!(f, "expected nul near {e}"), } } } @@ -147,8 +157,8 @@ impl ErrorKind { Self::InvalidKey(String::from_utf8_lossy(key).into()) } - pub(crate) fn new_invalid_header(header: &[u8]) -> Self { - Self::InvalidHeader(String::from_utf8_lossy(header).into()) + pub(crate) fn new_invalid_header(header: &[u8], expected: &'static str) -> Self { + Self::InvalidHeader(String::from_utf8_lossy(header).into(), expected) } } @@ -330,6 +340,12 @@ impl TruncBuf { } } +impl TruncBuf { + pub(crate) fn as_str(&self) -> Cow { + String::from_utf8_lossy(&self.buf) + } +} + impl From<&[T]> for TruncBuf { fn from(value: &[T]) -> Self { Self::new(value) diff --git a/altium/src/font.rs b/altium/src/font.rs index 08cbbd5..05d4b45 100644 --- a/altium/src/font.rs +++ b/altium/src/font.rs @@ -1,6 +1,6 @@ //! Objects related to font as Altium sees it. -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; lazy_static::lazy_static! { pub static ref DEFAULT_FONT: Font = Font { @@ -10,7 +10,7 @@ lazy_static::lazy_static! { } /// A font that is stored in a library -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Font { pub(crate) name: Box, pub(crate) size: u16, @@ -41,7 +41,7 @@ impl Default for &Font { // // Or `Arc>>>`. Yucky, but editable (edit the // font if you're the only user duplicate it if you're not) -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct FontCollection(Vec); impl FontCollection { @@ -62,3 +62,17 @@ impl From> for FontCollection { Self(value) } } + +impl Deref for FontCollection { + type Target = [Font]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for FontCollection { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/altium/src/parse/bin.rs b/altium/src/parse/bin.rs index 2c29244..4220349 100644 --- a/altium/src/parse/bin.rs +++ b/altium/src/parse/bin.rs @@ -7,26 +7,40 @@ use crate::{ ErrorKind, }; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum BufLenMatch { /// Length is a 3-byte value in a 4-byte integer, and the upper value must /// be equal to 0x01. This is used to indicate binary data U24UpperOne, + /// 3-byte value in a 4-byte integer, the upper value must be equal to 0x00 + U24UpperZero, /// Length is a 4-byte value U32, /// Length is a single byte value U8, } -/// Extract a buffer that starts with a 3-byte header -pub fn extract_sized_buf(buf: &[u8], len_match: BufLenMatch) -> Result<(&[u8], &[u8]), ErrorKind> { +/// Extract a buffer that starts with a 1-, 3-, or 4-byte header. +/// +/// - `len_match`: Configure how the leading bytes define the length +/// - `expect_nul`: Configure whether or not there should be a nul terminator +pub fn extract_sized_buf( + buf: &[u8], + len_match: BufLenMatch, + expect_nul: bool, +) -> Result<(&[u8], &[u8]), ErrorKind> { let (data_len, rest): (usize, _) = match len_match { - BufLenMatch::U24UpperOne => { + BufLenMatch::U24UpperOne | BufLenMatch::U24UpperZero => { let [l0, l1, l2, l3, rest @ ..] = buf else { return Err(ErrorKind::BufferTooShort(4, TruncBuf::new(buf))); }; - assert_eq!(*l3, 0x01, "expected 0x01 in uppper bit but got {l3}"); + if len_match == BufLenMatch::U24UpperOne { + assert_eq!(*l3, 0x01, "expected 0x01 in uppper bit but got {l3}"); + } else if len_match == BufLenMatch::U24UpperZero { + assert_eq!(*l3, 0x00, "expected 0x00 in uppper bit but got {l3}"); + } + let len = u32::from_le_bytes([*l0, *l1, *l2, 0x00]) .try_into() .unwrap(); @@ -52,15 +66,25 @@ pub fn extract_sized_buf(buf: &[u8], len_match: BufLenMatch) -> Result<(&[u8], & let data = rest .get(..data_len) .ok_or(ErrorKind::BufferTooShort(data_len, rest.into()))?; - Ok((data, &rest[data_len..])) + let rest = &rest[data_len..]; + + if expect_nul { + let Some(0) = data.last() else { + return Err(ErrorKind::ExpectedNul(TruncBuf::new_end(data))); + }; + Ok((&data[..data.len() - 1], rest)) + } else { + Ok((data, rest)) + } } -/// Extract a buffer that starts with a 3-byte header to a string +/// Extract a buffer that starts with a 1-, 3- or 4-byte header to a string pub fn extract_sized_utf8_buf( buf: &[u8], len_match: BufLenMatch, + expect_nul: bool, ) -> Result<(&str, &[u8]), ErrorKind> { - let (str_buf, rest) = extract_sized_buf(buf, len_match)?; + let (str_buf, rest) = extract_sized_buf(buf, len_match, expect_nul)?; let text = str_from_utf8(str_buf)?; Ok((text, rest)) } diff --git a/altium/src/parse/utf8.rs b/altium/src/parse/utf8.rs index 64ad1e2..95c3f6c 100644 --- a/altium/src/parse/utf8.rs +++ b/altium/src/parse/utf8.rs @@ -14,7 +14,7 @@ use crate::{ /// Extension trait for `&[u8]` that will parse a string as utf8/ASCII for /// anything implementing `FromUtf8` pub trait ParseUtf8<'a> { - /// Parse this as utf8 to whatever the target type is + /// Parse this as a utf8 string to whatever the target type is fn parse_as_utf8>(self) -> Result; } diff --git a/altium/src/sch.rs b/altium/src/sch.rs index b0fd007..4dfbf89 100644 --- a/altium/src/sch.rs +++ b/altium/src/sch.rs @@ -15,5 +15,5 @@ pub use component::Component; pub use params::{Justification, SheetStyle}; pub use pin::PinError; pub(crate) use record::{SchDrawCtx, SchRecord}; -pub use schdoc::SchDoc; +pub use schdoc::{SchDoc, SchDocRecords}; pub use schlib::{ComponentMeta, ComponentsIter, SchLib}; diff --git a/altium/src/sch/component.rs b/altium/src/sch/component.rs index 7a25199..5751913 100644 --- a/altium/src/sch/component.rs +++ b/altium/src/sch/component.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use svg::node::element::SVG as Svg; +use super::record::parse_all_records; use super::storage::Storage; use super::{SchDrawCtx, SchRecord}; use crate::draw::{Canvas, Draw, SvgCtx}; @@ -99,51 +100,3 @@ impl Component { self.name.partial_cmp(&other.name) } } - -/// Given a buffer for a component, split the records up -/// -/// Name is only used for diagnostics -fn parse_all_records(buf: &[u8], name: &str) -> Result, Error> { - // Our info u32 is something like `0xttllllll`, where `tt` are 8 bits - // representing a type (currently only values 0 and 1 known) and the `l`s - // are the length - const TY_SHIFT: u32 = 24; - const TY_MAEK: u32 = 0xff000000; - const LEN_MASK: u32 = 0x00ffffff; - const UTF8_RECORD_TY: u32 = 0x00; - const PIN_RECORD_TY: u32 = 0x01; - // No magic values :) - const U32_BYTES: usize = 4; - - let mut working = buf; - let mut parsed = Vec::new(); - while !working.is_empty() { - assert!( - working.len() >= 4, - "expected at least 4 bytes, only got {}", - working.len() - ); - - let info = u32::from_le_bytes(working[..4].try_into().unwrap()); - let ty = (info & TY_MAEK) >> TY_SHIFT; - let len: usize = (info & LEN_MASK).try_into().unwrap(); - - // Don't include the null terminator (which is included in `len`) - let to_parse = &working[U32_BYTES..(U32_BYTES + len - 1)]; - - // But do do a sanity check that the null exists - assert_eq!(working[U32_BYTES + len - 1], 0, "Expected null terimation"); - - working = &working[U32_BYTES + len..]; - - let record = match ty { - UTF8_RECORD_TY => parse_any_record(to_parse), - PIN_RECORD_TY => SchPin::parse(to_parse).map_err(Into::into), - _ => panic!("unexpected record type {ty:02x}"), - }; - - parsed.push(record.context(format!("in `parse_all_records` for `{name}`"))?); - } - - Ok(parsed) -} diff --git a/altium/src/sch/pin.rs b/altium/src/sch/pin.rs index 6da3ab5..eaf776a 100644 --- a/altium/src/sch/pin.rs +++ b/altium/src/sch/pin.rs @@ -3,24 +3,44 @@ use core::fmt; use std::str::{self, Utf8Error}; +use altium_macros::FromRecord; use log::warn; +use num_enum::TryFromPrimitive; use super::SchRecord; use crate::common::{Location, Rotation, Visibility}; +use crate::error::AddContext; +use crate::parse::ParseUtf8; +use crate::parse::{FromRecord, FromUtf8}; +use crate::{ErrorKind, UniqueId}; -#[derive(Clone, Debug, Default, PartialEq)] +/// Representation of a pin +/// +/// Altium stores pins as binary in the schematic libraries but text in the +/// schematic documents, so we need to parse both. +#[derive(Clone, Debug, Default, PartialEq, FromRecord)] +#[from_record(id = 2, record_variant = Pin)] pub struct SchPin { + pub(super) formal_type: u8, pub(super) owner_index: u8, pub(super) owner_part_id: u8, pub(super) description: Box, + // #[from_record(rename = b"PinDesignator")] pub(super) designator: Box, pub(super) name: Box, pub(super) location_x: i32, pub(super) location_y: i32, + pub(super) electrical: ElectricalType, + #[from_record(rename = b"PinLength")] pub(super) length: u32, + #[from_record(rename = b"SwapIDPart")] + pub(super) swap_id_part: Box, pub(super) designator_vis: Visibility, pub(super) name_vis: Visibility, pub(super) rotation: Rotation, + #[from_record(rename = b"PinPropagationDelay")] + pub(super) propegation_delay: f32, + pub(super) unique_id: UniqueId, } impl SchPin { @@ -37,13 +57,13 @@ impl SchPin { let (description, rest) = sized_buf_to_utf8(rest, "description")?; // TODO: ty_info - let [formal_ty, ty_info, rot_hide, l0, l1, x0, x1, y0, y1, rest @ ..] = rest else { + let [formal_type, ty_info, rot_hide, l0, l1, x0, x1, y0, y1, rest @ ..] = rest else { return Err(PinError::TooShort(rest.len(), "position extraction")); }; assert_eq!( - *formal_ty, 1, - "expected formal type of 1 but got {formal_ty}" + *formal_type, 1, + "expected formal type of 1 but got {formal_type}" ); let (rotation, des_vis, name_vis) = get_rotation_and_hiding(*rot_hide); let length = u16::from_le_bytes([*l0, *l1]); @@ -62,6 +82,7 @@ impl SchPin { } let retval = Self { + formal_type: *formal_type, owner_index: 0, owner_part_id: 0, description: description.into(), @@ -76,6 +97,7 @@ impl SchPin { designator_vis: des_vis, name_vis, rotation, + ..Default::default() }; Ok(SchRecord::Pin(retval)) @@ -153,8 +175,43 @@ fn get_rotation_and_hiding(val: u8) -> (Rotation, Visibility, Visibility) { (rotation, des_vis, name_vis) } -fn _print_buf(buf: &[u8], s: &str) { - println!("pin buf at {s}: {buf:02x?}"); +#[repr(u8)] +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] +pub enum ElectricalType { + #[default] + Input = 0, + Id = 1, + Output = 2, + OpenCollector = 3, + Passive = 4, + HighZ = 5, + OpenEmitter = 6, + Power = 7, +} + +impl FromUtf8<'_> for ElectricalType { + fn from_utf8(buf: &[u8]) -> Result { + let num: u8 = buf.parse_as_utf8()?; + num.try_into() + } +} + +impl TryFrom for ElectricalType { + type Error = ErrorKind; + + fn try_from(value: u8) -> Result { + match value { + x if x == Self::Input as u8 => Ok(Self::Input), + x if x == Self::Id as u8 => Ok(Self::Id), + x if x == Self::Output as u8 => Ok(Self::Output), + x if x == Self::OpenCollector as u8 => Ok(Self::OpenCollector), + x if x == Self::Passive as u8 => Ok(Self::Passive), + x if x == Self::HighZ as u8 => Ok(Self::HighZ), + x if x == Self::OpenEmitter as u8 => Ok(Self::OpenEmitter), + x if x == Self::Power as u8 => Ok(Self::Power), + _ => Err(ErrorKind::ElectricalType(value)), + } + } } /// Errors related specifically to pin parsing diff --git a/altium/src/sch/record.rs b/altium/src/sch/record.rs index 66ba438..fcb1bb3 100644 --- a/altium/src/sch/record.rs +++ b/altium/src/sch/record.rs @@ -51,17 +51,19 @@ //! We provide a derive macro for `FromRecord`, so most types in this module //! don't need to do anything special. mod draw; +mod parse; use std::str; use altium_macros::FromRecord; pub use draw::SchDrawCtx; -use num_enum::TryFromPrimitive; +pub(super) use parse::parse_all_records; use super::params::Justification; use super::pin::SchPin; use crate::common::{Location, ReadOnlyState, UniqueId}; -use crate::error::AddContext; +use crate::error::{AddContext, TruncBuf}; +use crate::font::{Font, FontCollection}; use crate::Error; use crate::{ common::Color, @@ -107,11 +109,17 @@ pub enum SchRecord { Template(Template), Parameter(Parameter), ImplementationList(ImplementationList), + Implementation(Implementation), + ImplementationChild1(ImplementationChild1), + ImplementationChild2(ImplementationChild2), } -/// Try all known record types (excludes pins) +/// Try all known record types (excludes binary pins) pub fn parse_any_record(buf: &[u8]) -> Result { - let buf = buf.strip_prefix(b"|RECORD=").unwrap(); + let buf = buf.strip_prefix(b"|RECORD=").unwrap_or_else(|| { + let tb = TruncBuf::new(buf); + panic!("no record prefix in {tb} ({})", tb.as_str()); + }); let num_chars = buf.iter().take_while(|ch| ch.is_ascii_digit()).count(); let record_id_str = str::from_utf8(&buf[..num_chars]).unwrap(); let record_id: u32 = record_id_str @@ -123,6 +131,7 @@ pub fn parse_any_record(buf: &[u8]) -> Result { // Try parsing all our types, they will just skip to the next one if the // record ID doesn't match MetaData::parse_if_matches(record_id, to_parse) + .or_else(|| SchPin::parse_if_matches(record_id, to_parse)) .or_else(|| IeeeSymbol::parse_if_matches(record_id, to_parse)) .or_else(|| Label::parse_if_matches(record_id, to_parse)) .or_else(|| Bezier::parse_if_matches(record_id, to_parse)) @@ -154,25 +163,15 @@ pub fn parse_any_record(buf: &[u8]) -> Result { .or_else(|| Template::parse_if_matches(record_id, to_parse)) .or_else(|| Parameter::parse_if_matches(record_id, to_parse)) .or_else(|| ImplementationList::parse_if_matches(record_id, to_parse)) + .or_else(|| Implementation::parse_if_matches(record_id, to_parse)) + .or_else(|| ImplementationChild1::parse_if_matches(record_id, to_parse)) + .or_else(|| ImplementationChild2::parse_if_matches(record_id, to_parse)) .unwrap_or_else(|| { - eprintln!("unknown record id {record_id}"); + log::error!("unknown record id {record_id}"); Ok(SchRecord::Undefined) }) } -#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] -#[repr(u8)] -enum PinType { - Input = 0, - Id = 1, - Output = 2, - OpenCollector = 3, - Passive = 4, - HighZ = 5, - OpenEmitter = 6, - Power = 7, -} - /// Component metadata (AKA "Component") #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 1, use_box = true)] @@ -195,10 +194,13 @@ pub struct MetaData { part_count: u8, part_id_locked: bool, not_use_db_table_name: bool, + orientation: i32, sheet_part_file_name: Box, design_item_id: Box, source_library_name: Box, target_file_name: Box, + location_x: i32, + location_y: i32, unique_id: UniqueId, } @@ -236,7 +238,7 @@ pub struct Bezier { index_in_sheet: i16, is_not_accessible: bool, line_width: u16, - #[from_record(array = true, count = b"LocationCount")] + #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] locations: Vec, owner_index: u8, owner_part_id: i8, @@ -253,7 +255,7 @@ pub struct PolyLine { index_in_sheet: i16, line_width: u16, color: Color, - #[from_record(array = true, count = b"LocationCount")] + #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] locations: Vec, unique_id: UniqueId, } @@ -267,7 +269,7 @@ pub struct Polygon { is_not_accessible: bool, is_solid: bool, line_width: u16, - #[from_record(array = true, count = b"LocationCount")] + #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] locations: Vec, owner_index: u8, owner_part_id: i8, @@ -411,6 +413,21 @@ pub struct Rectangle { pub struct SheetSymbol { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + line_width: u16, + color: Color, + area_color: Color, + is_solid: bool, + symbol_type: Box, + show_net_name: bool, + location_y: i32, + x_size: i32, + y_size: i32, + location_x: i32, + orientation: i32, + font_id: u16, + text: Box, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -418,6 +435,19 @@ pub struct SheetSymbol { pub struct SheetEntry { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + text_color: Color, + area_color: Color, + text_font_id: u16, + text_style: Box, + name: Box, + unique_id: UniqueId, + arrow_kind: Box, + distance_from_top: i32, + color: Color, + #[from_record(rename = b"IOType")] + io_type: i32, + side: i32, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -425,13 +455,39 @@ pub struct SheetEntry { pub struct PowerPort { owner_index: u8, owner_part_id: i8, + is_cross_sheet_connector: bool, + index_in_sheet: i16, + style: i16, + show_net_name: bool, + location_y: i32, + location_x: i32, + orientation: i32, + font_id: u16, + text: Box, + unique_id: UniqueId, + color: Color, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 18)] pub struct Port { + alignment: u16, + area_color: Color, + border_width: i32, + color: Color, + font_id: u16, + height: i32, + width: i32, + index_in_sheet: i16, + #[from_record(rename = b"IOType")] + io_type: u16, + location_x: i32, + location_y: i32, + name: Box, owner_index: u8, owner_part_id: i8, + text_color: Color, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -439,6 +495,15 @@ pub struct Port { pub struct NoErc { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + orientation: i16, + symbol: Box, + is_active: bool, + suppress_all: bool, + location_x: i32, + location_y: i32, + color: Color, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -446,6 +511,13 @@ pub struct NoErc { pub struct NetLabel { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + location_x: i32, + location_y: i32, + color: Color, + font_id: u16, + text: Box, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -453,6 +525,12 @@ pub struct NetLabel { pub struct Bus { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + line_width: u16, + color: Color, + #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] + locations: Vec, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -460,13 +538,31 @@ pub struct Bus { pub struct Wire { owner_index: u8, owner_part_id: i8, + line_width: u16, + color: Color, + #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] + locations: Vec, + index_in_sheet: i16, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 28)] pub struct TextFrame { + location_x: i32, + location_y: i32, + corner_x: i32, + corner_y: i32, + area_color: Color, owner_index: u8, owner_part_id: i8, + font_id: u16, + alignment: u16, + word_wrap: bool, + text: Box, + index_in_sheet: i16, + clip_to_rect: bool, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -500,6 +596,32 @@ pub struct Image { pub struct Sheet { owner_index: u8, owner_part_id: i8, + snap_grid_size: i32, + snap_grid_on: bool, + visible_grid_on: bool, + visible_grid_size: i32, + custom_x: i32, + custom_y: i32, + custom_x_zones: u16, + custom_y_zones: u16, + custom_margin_width: u16, + hot_spot_grid_on: bool, + hot_spot_grid_size: i32, + system_font: u16, + #[from_record(array = true, count = b"FontIdCount", map = (FontName -> name, Size -> size))] + pub(super) fonts: FontCollection, + border_on: bool, + sheet_number_space_size: i32, + area_color: Color, + // FIXME: make this an enum + #[from_record(rename = b"Display_Unit")] + display_unit: u16, + #[from_record(rename = b"UseMBCS")] + use_mbcs: bool, + #[from_record(rename = b"IsBOC")] + is_boc: bool, + // FIXME: seems to be base64 + file_version_info: Box, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -507,6 +629,13 @@ pub struct Sheet { pub struct SheetName { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + location_x: i32, + location_y: i32, + color: Color, + font_id: u16, + text: Box, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -514,6 +643,13 @@ pub struct SheetName { pub struct FileName { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + location_x: i32, + location_y: i32, + color: Color, + font_id: u16, + text: Box, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -545,6 +681,8 @@ pub struct BusEntry { pub struct Template { owner_index: u8, owner_part_id: i8, + is_not_accessible: bool, + file_name: Box, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -570,3 +708,36 @@ pub struct ImplementationList { owner_index: u8, owner_part_id: i8, } + +/// Things like models, including footprints +#[derive(Clone, Debug, Default, PartialEq, FromRecord)] +#[from_record(id = 45)] +pub struct Implementation { + owner_index: u8, + owner_part_id: i8, + use_component_library: bool, + model_name: Box, + model_type: Box, + datafile_count: u16, + model_datafile_entity0: Box, + model_datafile_kind0: Box, + is_current: bool, + datalinks_locked: bool, + database_datalinks_locked: bool, + unique_id: UniqueId, + index_in_sheet: i16, +} + +#[derive(Clone, Debug, Default, PartialEq, FromRecord)] +#[from_record(id = 46)] +pub struct ImplementationChild1 { + owner_index: u8, + owner_part_id: i8, +} + +#[derive(Clone, Debug, Default, PartialEq, FromRecord)] +#[from_record(id = 48)] +pub struct ImplementationChild2 { + owner_index: u8, + owner_part_id: i8, +} diff --git a/altium/src/sch/record/draw.rs b/altium/src/sch/record/draw.rs index aadb3e9..0bde4ec 100644 --- a/altium/src/sch/record/draw.rs +++ b/altium/src/sch/record/draw.rs @@ -44,14 +44,14 @@ impl Draw for record::SchRecord { // record::SchRecord::Arc(v) => v.draw(canvas, ctx), record::SchRecord::Line(v) => v.draw(canvas, ctx), record::SchRecord::Rectangle(v) => v.draw(canvas, ctx), - // record::SchRecord::SheetSymbol(v) => v.draw(canvas, ctx), + record::SchRecord::SheetSymbol(v) => v.draw(canvas, ctx), // record::SchRecord::SheetEntry(v) => v.draw(canvas, ctx), // record::SchRecord::PowerPort(v) => v.draw(canvas, ctx), - // record::SchRecord::Port(v) => v.draw(canvas, ctx), + record::SchRecord::Port(v) => v.draw(canvas, ctx), // record::SchRecord::NoErc(v) => v.draw(canvas, ctx), - // record::SchRecord::NetLabel(v) => v.draw(canvas, ctx), - // record::SchRecord::Bus(v) => v.draw(canvas, ctx), - // record::SchRecord::Wire(v) => v.draw(canvas, ctx), + record::SchRecord::NetLabel(v) => v.draw(canvas, ctx), + record::SchRecord::Bus(v) => v.draw(canvas, ctx), + record::SchRecord::Wire(v) => v.draw(canvas, ctx), // record::SchRecord::TextFrame(v) => v.draw(canvas, ctx), // record::SchRecord::Junction(v) => v.draw(canvas, ctx), record::SchRecord::Image(v) => v.draw(canvas, ctx), @@ -267,14 +267,112 @@ impl Draw for record::Rectangle { } } -// impl Draw for record::SheetSymbol {} +impl Draw for record::SheetSymbol { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + canvas.draw_rectangle(DrawRectangle { + x: self.location_x, + y: self.location_y - self.y_size, + width: self.x_size, + height: self.y_size, + fill_color: self.area_color, + stroke_color: self.color, + stroke_width: self.line_width, + }); + } +} + // impl Draw for record::SheetEntry {} // impl Draw for record::PowerPort {} -// impl Draw for record::Port {} + +impl Draw for record::Port { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + // match self.io_type + let h2 = self.height / 2; + let mut locations = [Location::default(); 6]; + + locations[0] = Location::new(self.location_x, self.location_y + h2); + locations[1] = Location::new(self.location_x + self.width - h2, self.location_y + h2); + locations[2] = Location::new(self.location_x + self.width, self.location_y); + locations[3] = Location::new(self.location_x + self.width - h2, self.location_y - h2); + locations[4] = Location::new(self.location_x, self.location_y - h2); + locations[5] = Location::new(self.location_x, self.location_y + h2); + + canvas.draw_polygon(DrawPolygon { + locations: &locations, + fill_color: self.area_color, + stroke_color: self.color, + stroke_width: self.border_width.try_into().unwrap(), + }); + + let font = &ctx.fonts.get_idx(self.font_id.into()); + canvas.draw_text(DrawText { + x: self.location_x, + y: self.location_y, + text: &self.name, + color: self.text_color, + font, + ..Default::default() + }); + } +} + // impl Draw for record::NoErc {} -// impl Draw for record::NetLabel {} -// impl Draw for record::Bus {} -// impl Draw for record::Wire {} + +impl Draw for record::NetLabel { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + let font = &ctx.fonts.get_idx(self.font_id.into()); + + canvas.draw_text(DrawText { + x: self.location_x, + y: self.location_y, + text: &self.text, + color: self.color, + font, + ..Default::default() + }); + } +} + +impl Draw for record::Bus { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + for window in self.locations.windows(2) { + let &[a, b] = window else { unreachable!() }; + + canvas.draw_line(DrawLine { + start: a, + end: b, + color: self.color, + width: self.line_width * 4, + }); + } + } +} + +impl Draw for record::Wire { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + for window in self.locations.windows(2) { + let &[a, b] = window else { unreachable!() }; + + canvas.draw_line(DrawLine { + start: a, + end: b, + color: self.color, + width: self.line_width * 4, + }); + } + } +} + // impl Draw for record::TextFrame {} // impl Draw for record::Junction {} impl Draw for record::Image { diff --git a/altium/src/sch/record/parse.rs b/altium/src/sch/record/parse.rs new file mode 100644 index 0000000..77568f3 --- /dev/null +++ b/altium/src/sch/record/parse.rs @@ -0,0 +1,54 @@ +use super::SchRecord; +use crate::error::AddContext; +use crate::{ + sch::{pin::SchPin, record::parse_any_record}, + Error, +}; + +/// Given a buffer for a component, split the records up +/// +/// Name is only used for diagnostics +pub fn parse_all_records(buf: &[u8], err_name: &str) -> Result, Error> { + // Our info u32 is something like `0xttllllll`, where `tt` are 8 bits + // representing a type (currently only values 0 and 1 known) and the `l`s + // are the length + const TY_SHIFT: u32 = 24; + const TY_MAEK: u32 = 0xff000000; + const LEN_MASK: u32 = 0x00ffffff; + const UTF8_RECORD_TY: u32 = 0x00; + const PIN_RECORD_TY: u32 = 0x01; + // No magic values :) + const U32_BYTES: usize = 4; + + let mut working = buf; + let mut parsed = Vec::new(); + while !working.is_empty() { + assert!( + working.len() >= 4, + "expected at least 4 bytes, only got {}", + working.len() + ); + + let info = u32::from_le_bytes(working[..4].try_into().unwrap()); + let ty = (info & TY_MAEK) >> TY_SHIFT; + let len: usize = (info & LEN_MASK).try_into().unwrap(); + + // Don't include the null terminator (which is included in `len`) + let to_parse = &working[U32_BYTES..(U32_BYTES + len - 1)]; + + // But do do a sanity check that the null exists + assert_eq!(working[U32_BYTES + len - 1], 0, "Expected null terimation"); + + working = &working[U32_BYTES + len..]; + + let record = match ty { + UTF8_RECORD_TY => parse_any_record(to_parse), + PIN_RECORD_TY => SchPin::parse(to_parse).map_err(Into::into), + _ => panic!("unexpected record type {ty:02x}"), + }; + + parsed.push(record.or_context(|| format!("in `parse_all_records` for `{err_name}`"))?); + } + + Ok(parsed) +} diff --git a/altium/src/sch/schdoc.rs b/altium/src/sch/schdoc.rs index 658d32e..d458bf6 100644 --- a/altium/src/sch/schdoc.rs +++ b/altium/src/sch/schdoc.rs @@ -1,27 +1,147 @@ #![allow(unused)] +use core::fmt; +use std::cell::RefCell; use std::fs::File; -use std::io::{self, Read, Seek}; +use std::io::{self, Cursor, Read, Seek}; use std::path::Path; +use std::sync::Arc; use cfb::CompoundFile; -use crate::Error; +use super::record::{parse_all_records, Sheet}; +use super::storage::Storage; +use super::{SchDrawCtx, SchRecord}; +use crate::common::split_altium_map; +use crate::draw::Canvas; +use crate::draw::Draw; +use crate::error::AddContext; +use crate::parse::{extract_sized_buf, extract_sized_utf8_buf, BufLenMatch, ParseUtf8}; +use crate::{Error, ErrorKind, UniqueId}; /// Magic string found in the `FileHeader` stream -const HEADER: &str = "HEADER=Protel for Windows - Schematic Library Editor Binary File Version 5.0"; +const HEADER: &str = "Protel for Windows - Schematic Capture Binary File Version 5.0"; +/// Where most content is stored +const DATA_STREAM: &str = "FileHeader"; /// Representation of a schematic file pub struct SchDoc { - cfile: CompoundFile, + cfile: RefCell>, + sheet: Sheet, + records: Vec, + unique_id: UniqueId, + storage: Arc, } +/// Impls that are specific to a file impl SchDoc { + /// Open a file from disk pub fn open>(path: P) -> Result { - let cfile = cfb::open(path)?; + let cfile = cfb::open(&path)?; + Self::from_cfile(cfile) + .context("parsing SchLib") + .or_context(|| format!("with file {}", path.as_ref().display())) + } +} + +impl<'a> SchDoc> { + /// Open an in-memory file from a buffer + pub fn from_buffer(buf: &'a [u8]) -> Result { + let cfile = cfb::CompoundFile::open(Cursor::new(buf))?; + Self::from_cfile(cfile).context("parsing SchDoc from Cursor") + } +} + +impl SchDoc { + pub fn into_records(self) -> SchDocRecords { + SchDocRecords { + sheet: self.sheet, + records: self.records, + storage: self.storage, + } + } + + /// Create a `SchLib` representation from any `Read`able compound file. + fn from_cfile(mut cfile: CompoundFile) -> Result { + let mut tmp_buf: Vec = Vec::new(); // scratch memory + + let mut storage = Storage::parse_cfile(&mut cfile, &mut tmp_buf)?; + tmp_buf.clear(); - Ok(Self { cfile }) + { + let mut stream = cfile.open_stream(DATA_STREAM).map_err(|e| { + Error::from(e).context(format!("reading required stream `{DATA_STREAM}`")) + })?; + stream.read_to_end(&mut tmp_buf).unwrap(); + } + + let (rest, unique_id) = parse_header(&tmp_buf)?; + let mut records = parse_all_records(rest, "SchDoc::from_cfile")?; + let sheet_pos = records + .iter() + .position(|x| matches!(x, SchRecord::Sheet(_))); + let sheet = sheet_pos + .map(|idx| { + let SchRecord::Sheet(sheet) = records.remove(idx) else { + unreachable!() + }; + sheet + }) + .unwrap_or_default(); + + Ok(Self { + cfile: RefCell::new(cfile), + records, + sheet, + storage: storage.into(), + unique_id, + }) } } -impl SchDoc {} +impl fmt::Debug for SchDoc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SchDoc") + .field("unique_id", &self.unique_id) + .finish_non_exhaustive() + } +} + +/// Holdover until we figure out how we want to expose this +#[derive(Debug, Default)] +pub struct SchDocRecords { + sheet: Sheet, + records: Vec, + storage: Arc, +} + +impl SchDocRecords { + pub fn draw(&self, canvas: &mut C) { + let ctx = SchDrawCtx { + storage: &self.storage, + fonts: &self.sheet.fonts, + }; + self.records.iter().for_each(|r| r.draw(canvas, &ctx)); + } +} + +/// Extract the header, return the residual and the document unique ID +fn parse_header(buf: &[u8]) -> Result<(&[u8], UniqueId), Error> { + let mut uid = None; + let (hdr, rest) = extract_sized_buf(buf, BufLenMatch::U32, true)?; + for (key, val) in split_altium_map(hdr) { + match key { + b"HEADER" => { + if val != HEADER.as_bytes() { + return Err(ErrorKind::new_invalid_header(val, HEADER).into()); + } + } + b"UniqueID" => uid = Some(val.parse_as_utf8()?), + _ => (), + } + } + + let uid = uid.ok_or(ErrorKind::MissingUniqueId)?; + + Ok((rest, uid)) +} diff --git a/altium/src/sch/schlib.rs b/altium/src/sch/schlib.rs index 5cc201f..2daaee6 100644 --- a/altium/src/sch/schlib.rs +++ b/altium/src/sch/schlib.rs @@ -27,6 +27,7 @@ pub struct SchLib { /// Information contained in the compound file header. We use this as a /// lookup to see what we can extract from the file. header: SchLibMeta, + /// Blob storage used by Altium storage: Arc, } @@ -35,7 +36,9 @@ impl SchLib { /// Open a file from disk pub fn open>(path: P) -> Result { let cfile = cfb::open(&path)?; - Self::from_cfile(cfile).or_context(|| format!("opening {}", path.as_ref().display())) + Self::from_cfile(cfile) + .context("parsing SchLib") + .or_context(|| format!("with file {}", path.as_ref().display())) } } @@ -43,7 +46,7 @@ impl<'a> SchLib> { /// Open an in-memory file from a buffer pub fn from_buffer(buf: &'a [u8]) -> Result { let cfile = cfb::CompoundFile::open(Cursor::new(buf))?; - Self::from_cfile(cfile) + Self::from_cfile(cfile).context("parsing SchLib from Cursor") } } @@ -93,12 +96,10 @@ impl SchLib { { // Scope of refcell borrow let mut cfile_ref = self.cfile.borrow_mut(); - let mut stream = cfile_ref.open_stream(&data_path).unwrap_or_else(|e| { - dbg!(&meta); - dbg!(&data_path); + let mut stream = cfile_ref.open_stream(&data_path).map_err(|e| { let path_disp = data_path.display(); - panic!("missing required stream `{path_disp}` with error {e}") - }); + Error::from(e).context(format!("reading required stream `{path_disp}`",)) + })?; stream.read_to_end(&mut buf).unwrap(); } @@ -224,7 +225,6 @@ impl SchLibMeta { /// Magic header found in all streams const HEADER: &'static [u8] = b"HEADER=Protel for Windows - Schematic Library Editor Binary File Version 5.0"; - const HEADER_KEY: &'static [u8] = b"HEADER"; // /// Every header starts with this // const PFX: &[u8] = &[0x7a, 0x04, 0x00, 0x00, b'|']; @@ -295,7 +295,7 @@ impl SchLibMeta { } match key { - Self::HEADER_KEY => continue, + 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()?, @@ -340,8 +340,8 @@ impl SchLibMeta { let idx: usize = key[Self::COMP_PARTCOUNT_PFX.len()..].parse_as_utf8()?; ret.components[idx].part_count = val.parse_as_utf8()?; } - _ => eprintln!( - "unsupported file header key {}:{}", + _ => log::warn!( + "unsupported SchLib file header key {}:{}", buf2lstring(key), buf2lstring(val) ), diff --git a/altium/src/sch/storage.rs b/altium/src/sch/storage.rs index ffab15e..f83d631 100644 --- a/altium/src/sch/storage.rs +++ b/altium/src/sch/storage.rs @@ -89,19 +89,15 @@ impl Storage { pub(crate) fn parse(buf: &[u8]) -> Result { let (mut header, mut rest) = - extract_sized_buf(buf, BufLenMatch::U32).context("parsing storage")?; - - assert_eq!( - header.last(), - Some(&0), - "expected null termination at {:02x}", - TruncBuf::new_end(header) - ); + extract_sized_buf(buf, BufLenMatch::U32, true).context("parsing storage")?; + header = &header[..header.len().saturating_sub(1)]; 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).context("parsing storage")); + return Err( + ErrorKind::new_invalid_header(header, "Icon storage").context("parsing storage") + ); }; // Weight indicates how many items are in the storage @@ -129,8 +125,8 @@ impl Storage { rest = &rest[5..]; // Path comes first, then data - (path, rest) = extract_sized_utf8_buf(rest, BufLenMatch::U8)?; - (data, rest) = extract_sized_buf(rest, BufLenMatch::U32)?; + (path, rest) = extract_sized_utf8_buf(rest, BufLenMatch::U8, false)?; + (data, rest) = extract_sized_buf(rest, BufLenMatch::U32, false)?; map.insert( path.into(), diff --git a/altium/tests/include_test_util.rs b/altium/tests/include_test_util.rs new file mode 100644 index 0000000..d6ec1b8 --- /dev/null +++ b/altium/tests/include_test_util.rs @@ -0,0 +1,11 @@ +#[allow(unused)] +fn test_init_once() { + use std::sync::OnceLock; + static ONCE: OnceLock<()> = OnceLock::new(); + + ONCE.get_or_init(|| { + env_logger::init_from_env( + env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "debug"), + ) + }); +} diff --git a/altium/tests/test_schdoc.rs b/altium/tests/test_schdoc.rs new file mode 100644 index 0000000..608b62f --- /dev/null +++ b/altium/tests/test_schdoc.rs @@ -0,0 +1,13 @@ +include!("include_test_util.rs"); + +use altium::sch::SchDoc; + +const SCHDOC_SIMPLE: &str = "tests/samples/schdoc/simple.SchDoc"; + +#[test] +fn test_parse() { + test_init_once(); + // Just test error free parsing + let schdoc = SchDoc::open(SCHDOC_SIMPLE).unwrap(); + println!("{schdoc:#?}"); +} diff --git a/altium/tests/test_schlib.rs b/altium/tests/test_schlib.rs index 998b8a6..cfdbb3a 100644 --- a/altium/tests/test_schlib.rs +++ b/altium/tests/test_schlib.rs @@ -1,3 +1,5 @@ +include!("include_test_util.rs"); + use std::cmp::min; use std::io::prelude::*; use std::{ @@ -30,6 +32,8 @@ const SIMPLE_COMP_NAME2: &str = "PinProperties"; #[test] fn test_parse() { + test_init_once(); + let wd = std::env::current_dir().unwrap(); println!("working directory: {}", wd.display()); @@ -42,6 +46,8 @@ fn test_parse() { #[test] fn test_record() { + test_init_once(); + let schlib = SchLib::open(SCHLIB_SIMPLE).unwrap(); let comp = schlib.get_component(SIMPLE_COMP_NAME1).unwrap(); println!("comp {SIMPLE_COMP_NAME1}:\n{comp:#?}"); @@ -51,6 +57,8 @@ fn test_record() { #[test] fn test_draw_single_svg() { + test_init_once(); + // Only draw my favorite symbol let schlib = SchLib::open(SCHLIB_SIMPLE).unwrap(); let mut out_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); @@ -73,6 +81,8 @@ fn test_draw_single_svg() { #[test] fn test_draw_all_svgs() { + test_init_once(); + for schlib_path in ALL_SCHLIBS { let schlib = SchLib::open(schlib_path).unwrap(); @@ -97,6 +107,8 @@ fn test_draw_all_svgs() { #[test] fn test_storage() { + test_init_once(); + let schlib = SchLib::open(SCHLIB_SIMPLE).unwrap(); let mut out_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); out_dir.extend(["test_output", "storage"]);