app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
Colby Knox
Merge PR #307: kicad-lane: 1.0.64, live adds through KiCad's protobuf API, #126/#129, 17 standard core verbs
a8fbf9f
14d ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189
//! Schematic edit in place. KiCad 10 has no schematic API, so the kicad_sch_* verbs
//! rewrite `.kicad_sch` on disk. This module is the OS-neutral half of that:
//!
//! * a lossless s-expression document model. Every token keeps its original text
//! (numbers stay the digits KiCad wrote, quoted strings keep their escapes) and every
//! run of whitespace is kept on the node that follows it, so a file that is parsed
//! and written back unchanged is byte-identical, and an edit touches only the nodes
//! it edits;
//! * a pretty printer that lays out NEW nodes the way eeschema's formatter does
//! (tabs, one list per line, closing paren on its own line when the list has list
//! children, `(xy ..)` pairs sharing a line);
//! * the edit operations: place a symbol (with its `lib_symbols` entry copied from the
//! library, derived symbols flattened), wire, label, move, delete, set property;
//! * library resolution for a `Nick:Name` lib id through the project and global
//! sym-lib-table (via `libraries::LibCtx` and `expand_kicad_uri`), and a v4 UUID
//! generator built on std only.
//!
//! Validation (ERC) and the reload hint live in the verb layer.
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use crate::libraries::{self, expand_kicad_uri, LibCtx, LibTable};
// ---------------------------------------------------------------------------
// Lossless document model
// ---------------------------------------------------------------------------
/// One s-expression node. `lead` is the whitespace that precedes it in the file.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Node {
pub lead: String,
pub kind: Kind,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Kind {
/// Raw token text: a bare atom (`yes`, `1.27`, `xy`) or a quoted string WITH its
/// quotes and escapes (`"R1"`, `"a \"b\""`).
Atom(String),
/// `close_lead` is the whitespace before the closing paren.
List { children: Vec<Node>, close_lead: String },
}
/// A whole file: the root list plus whatever follows it (KiCad writes one "\n").
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Document {
pub root: Node,
pub tail: String,
}
/// Quote a string the way KiCad's CTX_QUOTED_STR escaper does.
pub fn quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out.push('"');
out
}
/// Undo `quote`. A bare atom comes back unchanged.
pub fn unquote(raw: &str) -> String {
if raw.len() < 2 || !raw.starts_with('"') || !raw.ends_with('"') {
return raw.to_string();
}
let inner = &raw[1..raw.len() - 1];
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('t') => out.push('\t'),
Some(o) => out.push(o),
None => out.push('\\'),
}
}
out
}
/// Millimetres the way eeschema writes them: up to four decimals, no trailing zeros.
pub fn fmt_num(v: f64) -> String {
let r = (v * 10000.0).round() / 10000.0;
let r = if r == 0.0 { 0.0 } else { r }; // no "-0"
let mut s = format!("{r:.4}");
while s.ends_with('0') {
s.pop();
}
if s.ends_with('.') {
s.pop();
}
s
}
impl Node {
pub fn atom(raw: impl Into<String>) -> Node {
Node { lead: String::new(), kind: Kind::Atom(raw.into()) }
}
pub fn quoted(s: &str) -> Node {
Node::atom(quote(s))
}
pub fn number(v: f64) -> Node {
Node::atom(fmt_num(v))
}
/// `(head child child ...)` with no layout yet; run `pretty` before inserting.
pub fn list(head: &str, mut rest: Vec<Node>) -> Node {
let mut children = Vec::with_capacity(rest.len() + 1);
children.push(Node::atom(head));
children.append(&mut rest);
Node { lead: String::new(), kind: Kind::List { children, close_lead: String::new() } }
}
pub fn is_list(&self) -> bool {
matches!(self.kind, Kind::List { .. })
}
pub fn children(&self) -> &[Node] {
match &self.kind {
Kind::List { children, .. } => children,
Kind::Atom(_) => &[],
}
}
pub fn children_mut(&mut self) -> Option<&mut Vec<Node>> {
match &mut self.kind {
Kind::List { children, .. } => Some(children),
Kind::Atom(_) => None,
}
}
/// Raw token text of an atom.
pub fn raw(&self) -> Option<&str> {
match &self.kind {
Kind::Atom(s) => Some(s),
Kind::List { .. } => None,
}
}
/// Unquoted value of an atom.
pub fn value(&self) -> Option<String> {
self.raw().map(unquote)
}
/// The head atom of a list: `lib_id` for `(lib_id "Device:R")`.
pub fn head(&self) -> Option<&str> {
self.children().first().and_then(Node::raw)
}
/// The i-th atom after the head, unquoted. `arg(0)` of `(at 1 2 3)` is `1`.
pub fn arg(&self, i: usize) -> Option<String> {
self.children().get(i + 1).and_then(Node::value)
}
pub fn num(&self, i: usize) -> Option<f64> {
self.arg(i)?.parse().ok()
}
pub fn child(&self, name: &str) -> Option<&Node> {
self.children().iter().find(|c| c.head() == Some(name))
}
pub fn child_mut(&mut self, name: &str) -> Option<&mut Node> {
self.children_mut()?.iter_mut().find(|c| c.head() == Some(name))
}
pub fn child_value(&self, name: &str) -> Option<String> {
self.child(name)?.arg(0)
}
/// Replace the i-th atom after the head (index must exist).
pub fn set_arg(&mut self, i: usize, node: Node) -> bool {
match self.children_mut().and_then(|c| c.get_mut(i + 1)) {
Some(slot) => {
let lead = std::mem::take(&mut slot.lead);
*slot = node;
slot.lead = if lead.is_empty() { " ".into() } else { lead };
true
}
None => false,
}
}
pub fn write(&self, out: &mut String) {
out.push_str(&self.lead);
match &self.kind {
Kind::Atom(s) => out.push_str(s),
Kind::List { children, close_lead } => {
out.push('(');
for c in children {
c.write(out);
}
out.push_str(close_lead);
out.push(')');
}
}
}
pub fn to_text(&self) -> String {
let mut s = String::new();
self.write(&mut s);
s
}
}
struct Parser<'a> {
text: &'a str,
b: &'a [u8],
i: usize,
}
impl<'a> Parser<'a> {
fn ws(&mut self) -> String {
let start = self.i;
while self.i < self.b.len() && (self.b[self.i] as char).is_ascii_whitespace() {
self.i += 1;
}
self.text[start..self.i].to_string()
}
fn node(&mut self, lead: String) -> Result<Node, String> {
let Some(&c) = self.b.get(self.i) else { return Err("unexpected end of file".into()) };
match c {
b'(' => {
self.i += 1;
let mut children = Vec::new();
loop {
let ws = self.ws();
match self.b.get(self.i) {
None => return Err(format!("unbalanced s-expression: list opened at byte {} never closes", self.i)),
Some(b')') => {
self.i += 1;
return Ok(Node { lead, kind: Kind::List { children, close_lead: ws } });
}
Some(_) => children.push(self.node(ws)?),
}
}
}
b')' => Err(format!("unexpected ')' at byte {}", self.i)),
b'"' => {
let start = self.i;
self.i += 1;
let mut esc = false;
loop {
let Some(&ch) = self.b.get(self.i) else { return Err(format!("unterminated quoted string at byte {start}")) };
self.i += 1;
if esc {
esc = false;
} else if ch == b'\\' {
esc = true;
} else if ch == b'"' {
break;
}
}
Ok(Node { lead, kind: Kind::Atom(self.text[start..self.i].to_string()) })
}
_ => {
let start = self.i;
while let Some(&ch) = self.b.get(self.i) {
if (ch as char).is_ascii_whitespace() || ch == b'(' || ch == b')' || ch == b'"' {
break;
}
self.i += 1;
}
Ok(Node { lead, kind: Kind::Atom(self.text[start..self.i].to_string()) })
}
}
}
}
/// Parse a single node (leading whitespace kept as its `lead`); trailing text is an error.
pub fn parse_node(text: &str) -> Result<Node, String> {
let mut p = Parser { text, b: text.as_bytes(), i: 0 };
let lead = p.ws();
let n = p.node(lead)?;
if !text[p.i..].trim().is_empty() {
return Err(format!("trailing content after the s-expression at byte {}", p.i));
}
Ok(n)
}
impl Document {
pub fn parse(text: &str) -> Result<Document, String> {
let mut p = Parser { text, b: text.as_bytes(), i: 0 };
let lead = p.ws();
if p.b.get(p.i) != Some(&b'(') {
return Err("not an s-expression file (no opening paren)".into());
}
let root = p.node(lead)?;
let tail = text[p.i..].to_string();
if !tail.trim().is_empty() {
return Err("trailing content after the root list".into());
}
Ok(Document { root, tail })
}
pub fn to_text(&self) -> String {
let mut s = String::new();
self.root.write(&mut s);
s.push_str(&self.tail);
s
}
}
/// Lay a node out the way eeschema's formatter does, at `depth` tabs. Atoms follow the
/// head on one line; every list child starts a new line one level deeper; a list with
/// list children closes on its own line; consecutive `(xy ..)` pairs share a line.
pub fn pretty(node: &mut Node, depth: usize) {
fn indent(depth: usize) -> String {
let mut s = String::with_capacity(depth + 1);
s.push('\n');
for _ in 0..depth {
s.push('\t');
}
s
}
node.lead = indent(depth);
layout_children(node, depth);
fn layout_children(node: &mut Node, depth: usize) {
let Kind::List { children, close_lead } = &mut node.kind else { return };
let mut has_list = false;
let mut prev_xy = false;
for (i, c) in children.iter_mut().enumerate() {
match &c.kind {
Kind::Atom(_) => {
c.lead = if i == 0 { String::new() } else { " ".into() };
prev_xy = false;
}
Kind::List { .. } => {
has_list = true;
let is_xy = c.head() == Some("xy");
c.lead = if is_xy && prev_xy { " ".into() } else { indent(depth + 1) };
prev_xy = is_xy;
layout_children(c, depth + 1);
}
}
}
*close_lead = if has_list { indent(depth) } else { String::new() };
}
}
// ---------------------------------------------------------------------------
// UUIDs
// ---------------------------------------------------------------------------
/// A random v4 UUID from std only: the OS-seeded `RandomState` hasher mixed with the
/// clock, the pid and a process-wide counter. Same code on every OS.
pub fn new_uuid() -> String {
use std::hash::{BuildHasher, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let nanos = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0);
let mut words = [0u64; 2];
let stack_marker = &n as *const u64 as usize;
for (k, w) in words.iter_mut().enumerate() {
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
h.write_u128(nanos);
h.write_u32(std::process::id());
h.write_u64(n);
h.write_usize(k);
h.write_usize(stack_marker);
*w = h.finish();
}
let mut b = [0u8; 16];
b[..8].copy_from_slice(&words[0].to_le_bytes());
b[8..].copy_from_slice(&words[1].to_le_bytes());
b[6] = (b[6] & 0x0f) | 0x40; // version 4
b[8] = (b[8] & 0x3f) | 0x80; // RFC 4122 variant
let hex: Vec<String> = b.iter().map(|x| format!("{x:02x}")).collect();
let h = hex.concat();
format!("{}-{}-{}-{}-{}", &h[0..8], &h[8..12], &h[12..16], &h[16..20], &h[20..32])
}
// ---------------------------------------------------------------------------
// Schematic
// ---------------------------------------------------------------------------
/// One placed symbol as the verbs report it.
#[derive(Clone, Debug, serde::Serialize)]
pub struct SymbolInfo {
pub uuid: String,
pub reference: String,
#[serde(rename = "libId")]
pub lib_id: String,
pub value: String,
pub at: [f64; 2],
pub rotation: f64,
pub unit: u32,
}
#[derive(Clone, Debug)]
pub struct PlaceSymbol<'a> {
pub lib_id: &'a str,
pub reference: &'a str,
pub at: (f64, f64),
pub rotation: f64,
pub unit: u32,
pub value: Option<&'a str>,
pub footprint: Option<&'a str>,
}
#[derive(Clone, Debug)]
pub struct Placed {
pub uuid: String,
/// True when the `lib_symbols` entry was copied in by this call.
pub lib_symbol_added: bool,
pub pins: Vec<String>,
pub is_power: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LabelKind {
Local,
Global,
Hierarchical,
}
impl LabelKind {
pub fn parse(s: &str) -> Option<LabelKind> {
match s.trim().to_ascii_lowercase().as_str() {
"local" | "label" | "net" | "" => Some(LabelKind::Local),
"global" | "global_label" => Some(LabelKind::Global),
"hierarchical" | "hier" | "hierarchical_label" | "sheet" => Some(LabelKind::Hierarchical),
_ => None,
}
}
pub fn token(self) -> &'static str {
match self {
LabelKind::Local => "label",
LabelKind::Global => "global_label",
LabelKind::Hierarchical => "hierarchical_label",
}
}
}
/// A `.kicad_sch` open for editing.
pub struct Schematic {
pub doc: Document,
/// Project name hint from the file's location (`<name>.kicad_pro` next to it, else
/// the file stem). Existing symbol instances win over it.
pub project_hint: Option<String>,
}
const TOP_TAIL: &[&str] = &["sheet_instances", "symbol_instances", "embedded_fonts"];
impl Schematic {
pub fn parse(text: &str) -> Result<Schematic, String> {
let doc = Document::parse(text)?;
if doc.root.head() != Some("kicad_sch") {
return Err(format!("not a schematic: the root list is ({})", doc.root.head().unwrap_or("")));
}
Ok(Schematic { doc, project_hint: None })
}
pub fn load(path: &Path) -> Result<Schematic, String> {
let text = std::fs::read_to_string(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
let mut s = Schematic::parse(&text)?;
s.project_hint = project_name_for(path);
Ok(s)
}
pub fn to_text(&self) -> String {
self.doc.to_text()
}
/// Copy the current file to `<file>.bak`, then write the document atomically.
pub fn save_with_backup(&self, path: &Path) -> Result<PathBuf, String> {
let mut bak = path.as_os_str().to_owned();
bak.push(".bak");
let bak = PathBuf::from(bak);
if path.exists() {
std::fs::copy(path, &bak).map_err(|e| format!("cannot write backup {}: {e}", bak.display()))?;
}
libraries::write_atomic(path, self.to_text().as_bytes())?;
Ok(bak)
}
pub fn root_uuid(&self) -> Option<String> {
self.doc.root.child_value("uuid")
}
pub fn version(&self) -> Option<String> {
self.doc.root.child_value("version")
}
pub fn top(&self) -> &[Node] {
self.doc.root.children()
}
pub fn top_mut(&mut self) -> &mut Vec<Node> {
self.doc.root.children_mut().expect("root is a list")
}
/// Placed symbols (top-level `(symbol (lib_id ..) ..)` items).
pub fn symbols(&self) -> Vec<SymbolInfo> {
self.top().iter().filter(|n| is_symbol_instance(n)).map(symbol_info).collect()
}
pub fn find_symbol_index(&self, reference: &str) -> Option<usize> {
self.top().iter().position(|n| is_symbol_instance(n) && symbol_property(n, "Reference").as_deref() == Some(reference))
}
pub fn find_uuid_index(&self, uuid: &str) -> Option<usize> {
self.top().iter().position(|n| n.is_list() && n.child_value("uuid").as_deref() == Some(uuid))
}
/// Project name for the instances block: what the existing symbols use, else the
/// `.kicad_pro` next to the file, else the file stem.
pub fn project_name(&self) -> String {
for s in self.top().iter().filter(|n| is_symbol_instance(n)) {
if let Some(p) = s.child("instances").and_then(|i| i.child_value("project")) {
if !p.is_empty() {
return p;
}
}
}
self.project_hint.clone().unwrap_or_else(|| "project".into())
}
/// Instances path for a new symbol: the path existing symbols use on this sheet
/// (so a sub-sheet keeps its hierarchy), else "/<root uuid>".
pub fn instance_path(&self) -> Result<String, String> {
for s in self.top().iter().filter(|n| is_symbol_instance(n)) {
if let Some(p) = s.child("instances").and_then(|i| i.child("project")).and_then(|p| p.child_value("path")) {
if p.starts_with('/') {
return Ok(p);
}
}
}
self.root_uuid().map(|u| format!("/{u}")).ok_or_else(|| "the schematic has no (uuid ...) at the root; cannot build the symbol instances path".to_string())
}
pub fn has_lib_symbol(&self, lib_id: &str) -> bool {
self.lib_symbol(lib_id).is_some()
}
pub fn lib_symbol(&self, lib_id: &str) -> Option<&Node> {
self.doc.root.child("lib_symbols")?.children().iter().find(|n| n.head() == Some("symbol") && n.arg(0).as_deref() == Some(lib_id))
}
/// Add a library symbol block (as read from a `.kicad_sym`, already flattened) under
/// `lib_symbols` as `lib_id`. No-op when the entry exists.
pub fn add_lib_symbol(&mut self, lib_id: &str, block: &Node) -> Result<bool, String> {
if self.has_lib_symbol(lib_id) {
return Ok(false);
}
if block.head() != Some("symbol") || block.arg(0).is_none() {
return Err("library symbol block must be (symbol \"Name\" ...)".into());
}
let mut entry = block.clone();
entry.set_arg(0, Node::quoted(lib_id));
pretty(&mut entry, 2);
if self.doc.root.child("lib_symbols").is_none() {
let mut ls = Node::list("lib_symbols", vec![]);
pretty(&mut ls, 1);
let top = self.top_mut();
let after = ["title_block", "paper", "uuid", "generator_version", "generator", "version"];
let mut idx = 1;
for name in after {
if let Some(i) = top.iter().position(|n| n.head() == Some(name)) {
idx = i + 1;
break;
}
}
top.insert(idx, ls);
}
let ls = self.doc.root.child_mut("lib_symbols").expect("just ensured");
let Kind::List { children, close_lead } = &mut ls.kind else { unreachable!() };
children.push(entry);
if close_lead.is_empty() {
*close_lead = "\n\t".into();
}
Ok(true)
}
pub fn insert_item(&mut self, mut node: Node) {
pretty(&mut node, 1);
let top = self.top_mut();
let idx = top.iter().position(|n| n.head().map(|h| TOP_TAIL.contains(&h)).unwrap_or(false)).unwrap_or(top.len());
top.insert(idx, node);
}
/// Place a symbol. `lib_block` is the `(symbol "Name" ...)` block from the library,
/// needed only when the sheet has no `lib_symbols` entry for the lib id yet.
pub fn place_symbol(&mut self, p: &PlaceSymbol, lib_block: Option<&Node>) -> Result<Placed, String> {
if p.reference.trim().is_empty() {
return Err("reference is required".into());
}
// Another unit of a placed multi-unit part is fine (same lib id, a unit not yet
// placed), and unannotated `X?` references may repeat; anything else is a clash.
if !p.reference.trim().ends_with('?') {
for n in self.top().iter().filter(|n| is_symbol_instance(n) && symbol_property(n, "Reference").as_deref() == Some(p.reference)) {
let same_part = n.child_value("lib_id").as_deref() == Some(p.lib_id);
let unit: u32 = n.child_value("unit").and_then(|u| u.parse().ok()).unwrap_or(1);
if !same_part {
return Err(format!("reference {} is already placed on this sheet", p.reference));
}
if unit == p.unit {
return Err(format!("reference {} unit {} is already placed on this sheet", p.reference, p.unit));
}
}
}
let rot = norm_rotation(p.rotation)?;
let mut added = false;
if !self.has_lib_symbol(p.lib_id) {
let blk = lib_block.ok_or_else(|| format!("no lib_symbols entry for {} in the sheet and no library block supplied", p.lib_id))?;
added = self.add_lib_symbol(p.lib_id, blk)?;
}
let lib = self.lib_symbol(p.lib_id).expect("ensured").clone();
let base_name = p.lib_id.split_once(':').map(|(_, n)| n).unwrap_or(p.lib_id);
let is_power = lib.child("power").is_some();
let pins = lib_symbol_pins(&lib, base_name, p.unit);
let project = self.project_name();
let path = self.instance_path()?;
let (x, y) = p.at;
let uuid = new_uuid();
let mut props: Vec<Node> = Vec::new();
let lib_prop = |name: &str| -> String { lib_property_value(&lib, name).unwrap_or_default() };
let value = p.value.map(str::to_string).unwrap_or_else(|| {
let v = lib_prop("Value");
if v.is_empty() { base_name.to_string() } else { v }
});
let footprint = p.footprint.map(str::to_string).unwrap_or_else(|| lib_prop("Footprint"));
// Reference and Value sit beside the body; the rest is hidden at the anchor.
props.push(property_node("Reference", p.reference, (x + 2.54, y - 1.27), Some("left"), is_power));
props.push(property_node("Value", &value, (x + 2.54, y + 1.27), Some("left"), false));
props.push(property_node("Footprint", &footprint, (x, y), None, true));
props.push(property_node("Datasheet", &lib_prop("Datasheet"), (x, y), None, true));
props.push(property_node("Description", &lib_prop("Description"), (x, y), None, true));
for extra in lib.children().iter().filter(|n| n.head() == Some("property")) {
let name = extra.arg(0).unwrap_or_default();
if name.starts_with("ki_") || ["Reference", "Value", "Footprint", "Datasheet", "Description"].contains(&name.as_str()) {
continue;
}
props.push(property_node(&name, &extra.arg(1).unwrap_or_default(), (x, y), None, true));
}
let mut children = vec![
Node::list("lib_id", vec![Node::quoted(p.lib_id)]),
Node::list("at", vec![Node::number(x), Node::number(y), Node::number(rot)]),
Node::list("unit", vec![Node::atom(p.unit.to_string())]),
Node::list("exclude_from_sim", vec![Node::atom("no")]),
Node::list("in_bom", vec![Node::atom("yes")]),
Node::list("on_board", vec![Node::atom("yes")]),
Node::list("dnp", vec![Node::atom("no")]),
Node::list("fields_autoplaced", vec![Node::atom("yes")]),
Node::list("uuid", vec![Node::quoted(&uuid)]),
];
children.append(&mut props);
for pin in &pins {
children.push(Node::list("pin", vec![Node::quoted(pin), Node::list("uuid", vec![Node::quoted(&new_uuid())])]));
}
children.push(Node::list(
"instances",
vec![Node::list(
"project",
vec![
Node::quoted(&project),
Node::list(
"path",
vec![
Node::quoted(&path),
Node::list("reference", vec![Node::quoted(p.reference)]),
Node::list("unit", vec![Node::atom(p.unit.to_string())]),
],
),
],
)],
));
self.insert_item(Node::list("symbol", children));
Ok(Placed { uuid, lib_symbol_added: added, pins, is_power })
}
/// One `(wire ...)` per straight segment; returns their uuids in order.
pub fn add_wire(&mut self, points: &[(f64, f64)]) -> Result<Vec<String>, String> {
if points.len() < 2 {
return Err("a wire needs at least two points".into());
}
let mut uuids = Vec::new();
for seg in points.windows(2) {
let (a, b) = (seg[0], seg[1]);
if a == b {
continue;
}
let uuid = new_uuid();
self.insert_item(Node::list(
"wire",
vec![
Node::list("pts", vec![xy(a), xy(b)]),
Node::list("stroke", vec![Node::list("width", vec![Node::number(0.0)]), Node::list("type", vec![Node::atom("default")])]),
Node::list("uuid", vec![Node::quoted(&uuid)]),
],
));
uuids.push(uuid);
}
if uuids.is_empty() {
return Err("all points are the same; nothing to draw".into());
}
Ok(uuids)
}
/// A net label. `shape` (input|output|bidirectional|tri_state|passive) applies to
/// global and hierarchical labels only.
pub fn add_label(&mut self, text: &str, kind: LabelKind, at: (f64, f64), rotation: f64, shape: Option<&str>) -> Result<String, String> {
if text.trim().is_empty() {
return Err("label text is required".into());
}
let rot = norm_rotation(rotation)?;
let uuid = new_uuid();
let (x, y) = at;
let right = rot == 180.0 || rot == 270.0;
let mut children = vec![Node::quoted(text)];
let justify: Vec<Node> = match kind {
LabelKind::Local => vec![Node::atom(if right { "right" } else { "left" }), Node::atom("bottom")],
_ => vec![Node::atom(if right { "right" } else { "left" })],
};
if kind != LabelKind::Local {
let shape = shape.map(|s| s.trim().to_ascii_lowercase()).unwrap_or_else(|| if kind == LabelKind::Global { "bidirectional".into() } else { "input".into() });
if !["input", "output", "bidirectional", "tri_state", "passive"].contains(&shape.as_str()) {
return Err(format!("shape must be input, output, bidirectional, tri_state or passive (got {shape})"));
}
children.push(Node::list("shape", vec![Node::atom(shape)]));
}
children.push(Node::list("at", vec![Node::number(x), Node::number(y), Node::number(rot)]));
if kind != LabelKind::Local {
children.push(Node::list("fields_autoplaced", vec![Node::atom("yes")]));
}
children.push(Node::list("effects", vec![font_node(), Node::list("justify", justify)]));
children.push(Node::list("uuid", vec![Node::quoted(&uuid)]));
if kind == LabelKind::Global {
children.push(property_node("Intersheetrefs", "${INTERSHEET_REFS}", (x, y), None, true));
}
self.insert_item(Node::list(kind.token(), children));
Ok(uuid)
}
/// Where each pin of a placed symbol connects, in sheet millimetres (y down), every unit placed
/// under that reference: [(pin number, (x, y))]. Library pins are y up; the symbol's angle turns
/// them counterclockwise as seen, then its mirror flips them (KiCad's order).
pub fn pin_points(&self, reference: &str) -> Result<Vec<(String, (f64, f64))>, String> {
let mut out = Vec::new();
let mut found = false;
for inst in self.top().iter().filter(|n| is_symbol_instance(n) && symbol_property(n, "Reference").as_deref() == Some(reference)) {
found = true;
let lib_key = inst.child_value("lib_name").or_else(|| inst.child_value("lib_id")).unwrap_or_default();
let Some(lib) = self.lib_symbol(&lib_key) else { return Err(format!("{reference}: its library symbol {lib_key} is not in the sheet's lib_symbols")) };
let base = lib_key.rsplit(':').next().unwrap_or(&lib_key).to_string();
let unit = inst.child("unit").and_then(|u| u.num(0)).unwrap_or(1.0) as u32;
let at = inst.child("at").ok_or("symbol has no (at ...)")?;
let (sx, sy, rot) = (at.num(0).unwrap_or(0.0), at.num(1).unwrap_or(0.0), at.num(2).unwrap_or(0.0));
let mirror = inst.child_value("mirror");
let (sin, cos) = rot.to_radians().sin_cos();
for sub in lib.children().iter().filter(|n| n.head() == Some("symbol")) {
let Some(name) = sub.arg(0) else { continue };
let Some(u) = sub_unit(&name, &base) else { continue };
if u != 0 && u != unit {
continue;
}
for pin in sub.children().iter().filter(|n| n.head() == Some("pin")) {
let (Some(num), Some(pat)) = (pin.child_value("number"), pin.child("at")) else { continue };
let (px, py) = (pat.num(0).unwrap_or(0.0), pat.num(1).unwrap_or(0.0));
let (mut qx, mut qy) = (px * cos - py * sin, px * sin + py * cos);
match mirror.as_deref() {
Some("x") => qy = -qy,
Some("y") => qx = -qx,
_ => {}
}
let r = |v: f64| (v * 1e4).round() / 1e4 + 0.0;
out.push((num, (r(sx + qx), r(sy - qy))));
}
}
}
if !found {
return Err(format!("no symbol with reference {reference} on this sheet"));
}
Ok(out)
}
/// A placed symbol's pins as (number, name), from its library symbol.
pub fn pin_names(&self, reference: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
for inst in self.top().iter().filter(|n| is_symbol_instance(n) && symbol_property(n, "Reference").as_deref() == Some(reference)) {
let lib_key = inst.child_value("lib_name").or_else(|| inst.child_value("lib_id")).unwrap_or_default();
let Some(lib) = self.lib_symbol(&lib_key) else { continue };
for sub in lib.children().iter().filter(|n| n.head() == Some("symbol")) {
for pin in sub.children().iter().filter(|n| n.head() == Some("pin")) {
if let (Some(num), Some(name)) = (pin.child_value("number"), pin.child_value("name")) {
if !out.iter().any(|(n, _): &(String, String)| *n == num) {
out.push((num, name));
}
}
}
}
}
out
}
/// A no-connect flag at `at` (on a pin's connection point): `(no_connect (at x y) (uuid ..))`.
pub fn add_no_connect(&mut self, at: (f64, f64)) -> Result<String, String> {
let uuid = new_uuid();
self.insert_item(Node::list("no_connect", vec![Node::list("at", vec![Node::number(at.0), Node::number(at.1)]), Node::list("uuid", vec![Node::quoted(&uuid)])]));
Ok(uuid)
}
/// Move a symbol (and its fields with it) so its anchor lands on `to`.
pub fn move_symbol(&mut self, reference: &str, to: (f64, f64)) -> Result<(String, (f64, f64)), String> {
let idx = self.find_symbol_index(reference).ok_or_else(|| format!("no symbol with reference {reference} on this sheet"))?;
let node = &mut self.top_mut()[idx];
let at = node.child("at").ok_or("symbol has no (at ...)")?;
let (ox, oy) = (at.num(0).unwrap_or(0.0), at.num(1).unwrap_or(0.0));
let (dx, dy) = (to.0 - ox, to.1 - oy);
shift_at(node.child_mut("at").expect("checked"), dx, dy);
for prop in node.children_mut().expect("list").iter_mut().filter(|n| n.head() == Some("property")) {
if let Some(a) = prop.child_mut("at") {
shift_at(a, dx, dy);
}
}
let uuid = node.child_value("uuid").unwrap_or_default();
Ok((uuid, (ox, oy)))
}
/// Delete any top-level item by uuid (symbol, wire, label, junction, sheet, ...).
/// Returns the item's kind.
pub fn delete(&mut self, uuid: &str) -> Result<String, String> {
let idx = self.find_uuid_index(uuid).ok_or_else(|| format!("no item with uuid {uuid} on this sheet"))?;
let node = self.top_mut().remove(idx);
let kind = node.head().unwrap_or("item").to_string();
Ok(kind)
}
/// Set (or add) a field on a symbol. Reference also updates the instances block.
/// Set (or add, hidden) field `name` on every unit of the part `reference` placed on this sheet
/// (a multi-unit part is one top-level symbol per unit). Answers how many units changed, and the
/// value the first one had before (None when the field was added).
pub fn set_property_all_units(&mut self, reference: &str, name: &str, value: &str) -> Result<(usize, Option<String>), String> {
if name == "Reference" {
return Err("Reference is renamed with sch_set_property, one part at a time".into());
}
let idx: Vec<usize> = self.top().iter().enumerate().filter(|(_, n)| is_symbol_instance(n) && symbol_property(n, "Reference").as_deref() == Some(reference)).map(|(i, _)| i).collect();
if idx.is_empty() {
return Err(format!("no symbol with reference {reference} on this sheet"));
}
let mut first_old: Option<Option<String>> = None;
for i in idx.iter().copied() {
let node = &mut self.top_mut()[i];
let (ax, ay) = node.child("at").map(|a| (a.num(0).unwrap_or(0.0), a.num(1).unwrap_or(0.0))).unwrap_or((0.0, 0.0));
let children = node.children_mut().expect("list");
let old = match children.iter_mut().find(|n| n.head() == Some("property") && n.arg(0).as_deref() == Some(name)) {
Some(prop) => {
let o = prop.arg(1);
prop.set_arg(1, Node::quoted(value));
o
}
None => {
let mut p = property_node(name, value, (ax, ay), None, true);
pretty(&mut p, 2);
let last_prop = children.iter().rposition(|n| n.head() == Some("property"));
let at = last_prop.map(|i| i + 1).unwrap_or_else(|| children.iter().position(|n| n.head() == Some("pin") || n.head() == Some("instances")).unwrap_or(children.len()));
children.insert(at, p);
None
}
};
first_old.get_or_insert(old);
}
Ok((idx.len(), first_old.flatten()))
}
pub fn set_property(&mut self, reference: &str, name: &str, value: &str) -> Result<(String, Option<String>), String> {
if name.trim().is_empty() {
return Err("property name is required".into());
}
if name == "Reference" && value.trim().is_empty() {
return Err("Reference cannot be empty".into());
}
if name == "Reference" && value != reference && self.find_symbol_index(value).is_some() {
return Err(format!("reference {value} is already used on this sheet"));
}
let idx = self.find_symbol_index(reference).ok_or_else(|| format!("no symbol with reference {reference} on this sheet"))?;
let node = &mut self.top_mut()[idx];
let uuid = node.child_value("uuid").unwrap_or_default();
let (ax, ay) = node.child("at").map(|a| (a.num(0).unwrap_or(0.0), a.num(1).unwrap_or(0.0))).unwrap_or((0.0, 0.0));
let children = node.children_mut().expect("list");
let mut old = None;
if let Some(prop) = children.iter_mut().find(|n| n.head() == Some("property") && n.arg(0).as_deref() == Some(name)) {
old = prop.arg(1);
prop.set_arg(1, Node::quoted(value));
} else {
let mut p = property_node(name, value, (ax, ay), None, true);
let depth = 2;
pretty(&mut p, depth);
let last_prop = children.iter().rposition(|n| n.head() == Some("property"));
let at = last_prop.map(|i| i + 1).unwrap_or_else(|| children.iter().position(|n| n.head() == Some("pin") || n.head() == Some("instances")).unwrap_or(children.len()));
children.insert(at, p);
}
if name == "Reference" {
if let Some(inst) = children.iter_mut().find(|n| n.head() == Some("instances")) {
for proj in inst.children_mut().expect("list").iter_mut().filter(|n| n.head() == Some("project")) {
for path in proj.children_mut().expect("list").iter_mut().filter(|n| n.head() == Some("path")) {
if let Some(r) = path.child_mut("reference") {
r.set_arg(0, Node::quoted(value));
}
}
}
}
}
Ok((uuid, old))
}
/// Next free `<prefix><n>` reference on this sheet (`#PWR` gives `#PWR01` style).
pub fn next_reference(&self, prefix: &str) -> String {
let used: BTreeSet<u32> = self
.symbols()
.iter()
.filter_map(|s| s.reference.strip_prefix(prefix).and_then(|n| n.parse::<u32>().ok()))
.collect();
let mut n = 1;
while used.contains(&n) {
n += 1;
}
if prefix.starts_with('#') {
format!("{prefix}{n:02}")
} else {
format!("{prefix}{n}")
}
}
}
pub fn xy(p: (f64, f64)) -> Node {
Node::list("xy", vec![Node::number(p.0), Node::number(p.1)])
}
pub fn font_node() -> Node {
Node::list("font", vec![Node::list("size", vec![Node::number(1.27), Node::number(1.27)])])
}
pub fn property_node(name: &str, value: &str, at: (f64, f64), justify: Option<&str>, hide: bool) -> Node {
let mut effects = vec![font_node()];
if let Some(j) = justify {
effects.push(Node::list("justify", vec![Node::atom(j)]));
}
if hide {
effects.push(Node::list("hide", vec![Node::atom("yes")]));
}
Node::list(
"property",
vec![
Node::quoted(name),
Node::quoted(value),
Node::list("at", vec![Node::number(at.0), Node::number(at.1), Node::number(0.0)]),
Node::list("effects", effects),
],
)
}
pub fn shift_at(at: &mut Node, dx: f64, dy: f64) {
let x = at.num(0).unwrap_or(0.0) + dx;
let y = at.num(1).unwrap_or(0.0) + dy;
at.set_arg(0, Node::number(x));
at.set_arg(1, Node::number(y));
}
pub fn norm_rotation(r: f64) -> Result<f64, String> {
let r = ((r % 360.0) + 360.0) % 360.0;
if [0.0, 90.0, 180.0, 270.0].contains(&r) {
Ok(r)
} else {
Err(format!("rotation must be 0, 90, 180 or 270 (got {r})"))
}
}
/// A top-level `(symbol (lib_id ...) ...)`, as opposed to a `lib_symbols` entry.
pub fn is_symbol_instance(n: &Node) -> bool {
n.head() == Some("symbol") && n.child("lib_id").is_some()
}
pub fn symbol_property(n: &Node, name: &str) -> Option<String> {
n.children().iter().find(|c| c.head() == Some("property") && c.arg(0).as_deref() == Some(name))?.arg(1)
}
fn symbol_info(n: &Node) -> SymbolInfo {
let at = n.child("at");
SymbolInfo {
uuid: n.child_value("uuid").unwrap_or_default(),
reference: symbol_property(n, "Reference").unwrap_or_default(),
lib_id: n.child_value("lib_id").unwrap_or_default(),
value: symbol_property(n, "Value").unwrap_or_default(),
at: [at.and_then(|a| a.num(0)).unwrap_or(0.0), at.and_then(|a| a.num(1)).unwrap_or(0.0)],
rotation: at.and_then(|a| a.num(2)).unwrap_or(0.0),
unit: n.child_value("unit").and_then(|u| u.parse().ok()).unwrap_or(1),
}
}
fn lib_property_value(lib: &Node, name: &str) -> Option<String> {
symbol_property(lib, name)
}
/// Pin numbers a placed unit carries: the pins of sub-symbols `Name_0_*` (shared) and
/// `Name_<unit>_*`, in file order, de-duplicated.
pub fn lib_symbol_pins(lib: &Node, base_name: &str, unit: u32) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for sub in lib.children().iter().filter(|n| n.head() == Some("symbol")) {
let Some(name) = sub.arg(0) else { continue };
let Some(u) = sub_unit(&name, base_name) else { continue };
if u != 0 && u != unit {
continue;
}
for pin in sub.children().iter().filter(|n| n.head() == Some("pin")) {
if let Some(num) = pin.child_value("number") {
if !out.contains(&num) {
out.push(num);
}
}
}
}
out
}
/// `R_1_1` with base `R` -> unit 1.
pub fn sub_unit(name: &str, base: &str) -> Option<u32> {
let rest = name.strip_prefix(base)?.strip_prefix('_')?;
let (u, _style) = rest.split_once('_')?;
u.parse().ok()
}
pub fn project_name_for(path: &Path) -> Option<String> {
let dir = path.parent()?;
if let Ok(rd) = std::fs::read_dir(dir) {
let mut pros: Vec<String> = rd
.flatten()
.filter_map(|e| {
let p = e.path();
if p.extension().and_then(|x| x.to_str()) == Some("kicad_pro") {
p.file_stem().map(|s| s.to_string_lossy().to_string())
} else {
None
}
})
.collect();
pros.sort();
let stem = path.file_stem().map(|s| s.to_string_lossy().to_string());
if let Some(s) = &stem {
if pros.iter().any(|p| p == s) {
return stem;
}
}
if pros.len() == 1 {
return pros.pop();
}
}
path.file_stem().map(|s| s.to_string_lossy().to_string())
}
// ---------------------------------------------------------------------------
// Library symbols
// ---------------------------------------------------------------------------
/// The `(symbol "Name" ...)` block for `name` from a library's text, with `extends`
/// resolved the way KiCad's Flatten does: the parent's drawings and pins, the child's
/// properties on top, units renamed to the child's name.
pub fn flatten_lib_symbol(lib_text: &str, name: &str) -> Result<Node, String> {
let symbols = libraries::extract_top_level_symbols(lib_text)?;
flatten_in(&symbols, name, 0)
}
fn flatten_in(symbols: &[(String, String)], name: &str, depth: u32) -> Result<Node, String> {
if depth > 8 {
return Err(format!("symbol {name}: extends chain deeper than 8"));
}
let (_, blk) = symbols.iter().find(|(n, _)| n == name).ok_or_else(|| format!("symbol {name} not found in the library ({} symbols)", symbols.len()))?;
let mut node = parse_node(blk)?;
node.lead.clear();
let Some(parent_name) = node.child_value("extends") else { return Ok(node) };
let mut parent = flatten_in(symbols, &parent_name, depth + 1)?;
parent.set_arg(0, Node::quoted(name));
let parent_children = parent.children_mut().expect("list");
parent_children.retain(|n| n.head() != Some("extends"));
for sub in parent_children.iter_mut().filter(|n| n.head() == Some("symbol")) {
if let Some(sn) = sub.arg(0) {
if let Some(rest) = sn.strip_prefix(&parent_name).and_then(|r| r.strip_prefix('_')) {
sub.set_arg(0, Node::quoted(&format!("{name}_{rest}")));
}
}
}
for child in node.children().iter().filter(|n| n.head() == Some("property")) {
let pname = child.arg(0).unwrap_or_default();
let mut replacement = child.clone();
replacement.lead.clear();
if let Some(slot) = parent_children.iter_mut().find(|n| n.head() == Some("property") && n.arg(0).as_deref() == Some(pname.as_str())) {
*slot = replacement;
} else {
let at = parent_children.iter().rposition(|n| n.head() == Some("property")).map(|i| i + 1).unwrap_or(parent_children.len());
parent_children.insert(at, replacement);
}
}
// Child-level flags (power, pin_names offset, ...) override the parent's.
for child in node.children().iter().filter(|n| n.is_list() && !matches!(n.head(), Some("property") | Some("symbol") | Some("extends"))) {
let h = child.head().unwrap_or("");
let mut replacement = child.clone();
replacement.lead.clear();
if let Some(slot) = parent_children.iter_mut().find(|n| n.head() == Some(h)) {
*slot = replacement;
}
}
Ok(parent)
}
#[derive(Clone, Debug)]
pub struct ResolvedSymbol {
pub library_path: String,
pub name: String,
pub block: Node,
pub tables_checked: Vec<String>,
}
/// Find `Nick:Name` through the project's `sym-lib-table` (next to the schematic,
/// `${KIPRJMOD}` honoured) and then the global one from `ctx`, and return the
/// flattened block.
pub fn resolve_lib_symbol(ctx: &LibCtx, lib_id: &str, project_dir: Option<&Path>) -> Result<ResolvedSymbol, String> {
let (nick, name) = lib_id.split_once(':').ok_or_else(|| format!("libId must be Library:Symbol (got {lib_id})"))?;
let mut tables: Vec<PathBuf> = Vec::new();
if let Some(d) = project_dir {
tables.push(d.join("sym-lib-table"));
}
if let Some(g) = ctx.sym_lib_table() {
tables.push(g);
}
let mut checked = Vec::new();
for t in &tables {
if !t.is_file() {
continue;
}
checked.push(libraries::norm_path(t));
let table = LibTable::parse_file_resolved(t)?;
let Some(entry) = table.get(nick) else { continue };
let uri = match project_dir {
Some(d) => entry.uri.replace("${KIPRJMOD}", &libraries::norm_path(d)),
None => entry.uri.clone(),
};
let path = expand_kicad_uri(&uri, ctx).ok_or_else(|| format!("library {nick} uri {} uses a path variable this bridge cannot expand (is KiCad installed?)", entry.uri))?;
let mut r = load_lib_symbol(Path::new(&path), name)?;
r.tables_checked = checked;
return Ok(r);
}
Err(format!("library {nick} is not in any sym-lib-table (checked: {})", if checked.is_empty() { "none found".to_string() } else { checked.join(", ") }))
}
/// Read `name` straight from a `.kicad_sym` file.
pub fn load_lib_symbol(lib_path: &Path, name: &str) -> Result<ResolvedSymbol, String> {
let text = std::fs::read_to_string(lib_path).map_err(|e| format!("cannot read library {}: {e}", lib_path.display()))?;
let block = flatten_lib_symbol(&text, name).map_err(|e| format!("{}: {e}", lib_path.display()))?;
Ok(ResolvedSymbol { library_path: libraries::norm_path(lib_path), name: name.to_string(), block, tables_checked: vec![] })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quote_roundtrip() {
for s in ["", "R1", "a \"b\" c", "back\\slash", "tab\there", "line\nbreak"] {
assert_eq!(unquote("e(s)), s);
}
}
#[test]
fn numbers_look_like_kicad() {
assert_eq!(fmt_num(127.0), "127");
assert_eq!(fmt_num(80.0099), "80.0099");
assert_eq!(fmt_num(1.27), "1.27");
assert_eq!(fmt_num(-0.00001), "0");
assert_eq!(fmt_num(2.54 * 3.0), "7.62");
}
#[test]
fn pretty_matches_eeschema_layout() {
let mut n = Node::list("wire", vec![Node::list("pts", vec![xy((1.0, 2.0)), xy((3.0, 4.0))]), Node::list("uuid", vec![Node::quoted("u")])]);
pretty(&mut n, 1);
assert_eq!(n.to_text(), "\n\t(wire\n\t\t(pts\n\t\t\t(xy 1 2) (xy 3 4)\n\t\t)\n\t\t(uuid \"u\")\n\t)");
}
#[test]
fn uuids_are_v4_and_unique() {
let a = new_uuid();
let b = new_uuid();
assert_ne!(a, b);
assert_eq!(a.len(), 36);
assert_eq!(&a[14..15], "4");
assert!("89ab".contains(&a[19..20]));
}
}