app
Adom Concur
Public Made by Adomby adom
Cross-source consensus check for chip libraries — scores whether the KiCad, EAGLE, and Altium libraries AND the adom-ds2sf datasheet extraction all AGREE on pins, pads, names, and package, across seve
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
//! KiCad `.kicad_sym` and `.kicad_mod` parser.
//!
//! Both files are KiCad-flavored S-expressions. We don't need a full parser —
//! we extract specific tokens with a tiny tokenizer that handles parens +
//! double-quoted strings (with escapes) + bare atoms. Good enough for what we
//! compare and noticeably smaller than pulling in a kicad-specific crate.
use anyhow::{Context as _, Result};
use std::fs;
use std::path::Path;
use super::{Pad, PackageInfo, Part, Pin};
pub fn parse(sym_path: Option<&Path>, mod_path: Option<&Path>) -> Result<Part> {
let mut source_part_name = None;
let mut pins: Vec<Pin> = Vec::new();
let mut pads: Vec<Pad> = Vec::new();
let mut package: Option<PackageInfo> = None;
if let Some(p) = sym_path {
let raw = fs::read_to_string(p).with_context(|| format!("reading {}", p.display()))?;
let toks = tokenize(&raw);
let (name, parsed_pins) = scan_kicad_sym(&toks);
source_part_name = name;
pins = parsed_pins;
}
if let Some(p) = mod_path {
let raw = fs::read_to_string(p).with_context(|| format!("reading {}", p.display()))?;
let toks = tokenize(&raw);
let (fp_name, parsed_pads) = scan_kicad_mod(&toks);
pads = parsed_pads;
if let Some(name) = fp_name {
package = Some(PackageInfo { raw_name: name, body_x_mm: None, body_y_mm: None });
}
}
Ok(Part {
source: "kicad".to_string(),
source_part_name,
pads,
pins,
package,
})
}
#[derive(Debug, Clone, PartialEq)]
enum Tok {
LParen,
RParen,
/// A double-quoted string with escapes resolved.
Str(String),
/// A bare atom (identifier or number).
Atom(String),
}
fn tokenize(s: &str) -> Vec<Tok> {
let mut toks = Vec::with_capacity(s.len() / 8);
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c.is_ascii_whitespace() {
i += 1;
continue;
}
if c == b'(' {
toks.push(Tok::LParen);
i += 1;
continue;
}
if c == b')' {
toks.push(Tok::RParen);
i += 1;
continue;
}
if c == b'"' {
// Quoted string with backslash escapes.
i += 1;
let mut buf = String::new();
while i < bytes.len() && bytes[i] != b'"' {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
buf.push(bytes[i + 1] as char);
i += 2;
} else {
buf.push(bytes[i] as char);
i += 1;
}
}
if i < bytes.len() {
i += 1; // closing "
}
toks.push(Tok::Str(buf));
continue;
}
// Bare atom: read until whitespace or paren.
let start = i;
while i < bytes.len() && !bytes[i].is_ascii_whitespace() && bytes[i] != b'(' && bytes[i] != b')' {
i += 1;
}
toks.push(Tok::Atom(s[start..i].to_string()));
}
toks
}
/// Scan the kicad_sym tokens for the first symbol name and every (pin ...)'s
/// (number "X") + (name "Y") pairs.
fn scan_kicad_sym(toks: &[Tok]) -> (Option<String>, Vec<Pin>) {
let mut name = None;
let mut pins: Vec<Pin> = Vec::new();
let mut i = 0;
while i < toks.len() {
// Find first `( symbol "..."` form for the part name.
if name.is_none()
&& toks[i] == Tok::LParen
&& matches!(toks.get(i + 1), Some(Tok::Atom(a)) if a == "symbol")
{
if let Some(Tok::Str(s)) = toks.get(i + 2) {
name = Some(s.clone());
}
}
// Find `( pin <type> <shape> ... ) and read the (name ...) + (number ...).
if toks[i] == Tok::LParen
&& matches!(toks.get(i + 1), Some(Tok::Atom(a)) if a == "pin")
{
// Scan until matching RParen, collecting first "(name ...)" + "(number ...)".
let mut depth = 1;
let mut j = i + 2;
let mut pin_name: Option<String> = None;
let mut pin_number: Option<String> = None;
while j < toks.len() && depth > 0 {
match &toks[j] {
Tok::LParen => {
depth += 1;
if pin_name.is_none()
&& matches!(toks.get(j + 1), Some(Tok::Atom(a)) if a == "name")
{
if let Some(Tok::Str(s)) = toks.get(j + 2) {
pin_name = Some(s.clone());
}
}
if pin_number.is_none()
&& matches!(toks.get(j + 1), Some(Tok::Atom(a)) if a == "number")
{
if let Some(Tok::Str(s)) = toks.get(j + 2) {
pin_number = Some(s.clone());
}
}
}
Tok::RParen => {
depth -= 1;
}
_ => {}
}
j += 1;
}
if let (Some(num), nm) = (pin_number.clone(), pin_name.clone()) {
let nm = nm.unwrap_or_default();
let is_stub = nm.is_empty() || nm == num;
pins.push(Pin { number: num, name: nm, is_stub_name: is_stub });
}
i = j;
continue;
}
i += 1;
}
(name, pins)
}
/// Scan the kicad_mod tokens for the (footprint "name" ...) and every (pad "X" ...).
fn scan_kicad_mod(toks: &[Tok]) -> (Option<String>, Vec<Pad>) {
let mut name = None;
let mut pads: Vec<Pad> = Vec::new();
let mut i = 0;
while i < toks.len() {
// Either `(footprint "name"` (modern KiCad ≥ 6) or `(module "name"`
// (older KiCad / many UL exports) carries the package name.
if name.is_none()
&& toks[i] == Tok::LParen
&& matches!(toks.get(i + 1), Some(Tok::Atom(a)) if a == "footprint" || a == "module")
{
if let Some(Tok::Str(s)) = toks.get(i + 2) {
name = Some(s.clone());
}
}
if toks[i] == Tok::LParen
&& matches!(toks.get(i + 1), Some(Tok::Atom(a)) if a == "pad")
{
// (pad "1" smd rect ...). The pad number is always the first arg.
if let Some(Tok::Str(num)) = toks.get(i + 2) {
pads.push(Pad { number: num.clone() });
} else if let Some(Tok::Atom(num)) = toks.get(i + 2) {
// Some KiCad libs emit pad numbers as bare atoms (e.g. "1" without quotes).
pads.push(Pad { number: num.clone() });
}
}
i += 1;
}
(name, pads)
}