← Commit history

2.41.1: every export verifies its writes; a missing output directory is created (wiki #26)

Colby Knox ·cfeaf33bbd ·1mo ago ·parent 951c84f
7 files changed +158−22
Cargo.lock+79−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-lbr"-version = "2.41.0"+version = "2.41.1" dependencies = [  "altium-codec",  "anyhow",@@ -20,6 +20,7 @@ dependencies = [  "regex-lite",  "serde",  "serde_json",+ "sha2",  "tiny_http", ] @@ -116,6 +117,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]]+name = "block-buffer"+version = "0.10.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"+dependencies = [+ "generic-array",+]+ [[package]] name = "bumpalo" version = "3.20.3"@@ -206,6 +216,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]]+name = "cpufeatures"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"+dependencies = [+ "libc",+]+ [[package]] name = "crc32fast" version = "1.5.0"@@ -215,6 +234,26 @@ dependencies = [  "cfg-if", ] +[[package]]+name = "crypto-common"+version = "0.1.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"+dependencies = [+ "generic-array",+ "typenum",+]++[[package]]+name = "digest"+version = "0.10.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"+dependencies = [+ "block-buffer",+ "crypto-common",+]+ [[package]] name = "flate2" version = "1.1.9"@@ -255,6 +294,16 @@ dependencies = [  "slab", ] +[[package]]+name = "generic-array"+version = "0.14.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"+dependencies = [+ "typenum",+ "version_check",+]+ [[package]] name = "heck" version = "0.5.0"@@ -290,6 +339,12 @@ dependencies = [  "wasm-bindgen", ] +[[package]]+name = "libc"+version = "0.2.189"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"+ [[package]] name = "log" version = "0.4.32"@@ -413,6 +468,17 @@ dependencies = [  "zmij", ] +[[package]]+name = "sha2"+version = "0.10.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"+dependencies = [+ "cfg-if",+ "cpufeatures",+ "digest",+]+ [[package]] name = "simd-adler32" version = "0.3.9"@@ -454,6 +520,12 @@ dependencies = [  "log", ] +[[package]]+name = "typenum"+version = "1.20.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"+ [[package]] name = "unicode-ident" version = "1.0.24"@@ -476,6 +548,12 @@ dependencies = [  "wasm-bindgen", ] +[[package]]+name = "version_check"+version = "0.9.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"+ [[package]] name = "wasm-bindgen" version = "0.2.126"
Cargo.toml+2−1
@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.41.0"+version = "2.41.1" edition = "2021" description = "adom-lbr generator / EDA translator: the canonical adom-lbr JSON is the hub, with native converters to Altium (.SchLib/.PcbLib/.IntLib), EAGLE/Fusion (.lbr), and KiCad. Altium output is written natively — no Altium needed at conversion time." @@ -9,6 +9,7 @@ name = "adom-lbr" path = "src/main.rs"  [dependencies]+sha2 = "0.10" clap = { version = "4", features = ["derive"] } clap_complete = "4" regex-lite = "0.1"
VERSION+1−1
@@ -1 +1 @@-2.41.0+2.41.1
bin/adom-lbr
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "2.41.0",+  "version": "2.41.1",   "type": "app",   "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.",   "dependencies": {},
page.json+8−8
@@ -1,8 +1,8 @@ {   "slug": "adom-lbr",-  "version": "2.41.0",+  "version": "2.41.1",   "type": "app",-  "description": "adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.",+  "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.",   "dependencies": {},   "tags": [     "eda",@@ -61,15 +61,15 @@     "screenshots/**",     "dockbar.json"   ],-  "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools — KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion — through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.",+  "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools \u2014 KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion \u2014 through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.",   "sample_prompts": [     {       "prompt": "Convert this KiCad symbol and footprint to an Altium SchLib and PcbLib",-      "label": "KiCad → Altium"+      "label": "KiCad \u2192 Altium"     },     {       "prompt": "Import this Altium footprint into the adom-lbr JSON",-      "label": "Altium → JSON"+      "label": "Altium \u2192 JSON"     },     {       "prompt": "Bundle this part into a single Altium IntLib",@@ -77,15 +77,15 @@     },     {       "prompt": "Convert this Altium SchLib and PcbLib to a KiCad symbol and footprint",-      "label": "Altium → KiCad"+      "label": "Altium \u2192 KiCad"     },     {       "prompt": "Export this adom-lbr JSON to a KiCad .kicad_sym and .kicad_mod",-      "label": "JSON → KiCad"+      "label": "JSON \u2192 KiCad"     },     {       "prompt": "Open the adom-lbr Manager to browse wiki components and view the layer stackup",       "label": "Browse parts"     }   ]-}\ No newline at end of file+}
src/main.rs+67−10
@@ -2899,6 +2899,41 @@ const EAGLE_LAYERS: &str = r#"<layers> // ── Main ──  /// Read a text file or exit with a clear error.+/// Write `bytes` to `path` (creating the directory), read the file back and+/// require it to match, returning the sha256. Any failure is a coded error+/// and exit 2: export-kicad once printed "OK: wrote" for two files in a+/// directory that did not exist, because every write went through+/// `fs::write(..).ok()` and nothing created the directory (wiki #26).+fn write_verified(path: &std::path::Path, bytes: &[u8]) -> String {+    use sha2::{Digest, Sha256};+    if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {+        if let Err(e) = fs::create_dir_all(dir) {+            eprintln!("ERROR: [OUTPUT_NOT_WRITTEN] cannot create {}: {e}", dir.display());+            process::exit(2);+        }+    }+    if let Err(e) = fs::write(path, bytes) {+        eprintln!("ERROR: [OUTPUT_NOT_WRITTEN] cannot write {}: {e}", path.display());+        process::exit(2);+    }+    match fs::read(path) {+        Ok(back) if back == bytes => format!("{:x}", Sha256::digest(bytes)),+        Ok(back) => {+            eprintln!("ERROR: [OUTPUT_NOT_WRITTEN] {} read back {} bytes, expected {}", path.display(), back.len(), bytes.len());+            process::exit(2);+        }+        Err(e) => {+            eprintln!("ERROR: [OUTPUT_NOT_WRITTEN] {} was written but cannot be read back: {e}", path.display());+            process::exit(2);+        }+    }+}++/// The per-file receipt after a verified write.+fn written_hint(path: &std::path::Path, bytes: usize, sha: &str) {+    println!("Hint: verified {} ({bytes} bytes, sha256 {sha}, read back identical)", path.display());+}+ fn read_or_exit(path: &std::path::Path) -> String {     fs::read_to_string(path).unwrap_or_else(|e| {         eprintln!("ERROR: read {}: {e}", path.display());@@ -3488,8 +3523,10 @@ The geometry is fine but a BOM cannot order this part - pass --part-metadata or                 }             }             let outp = out.unwrap_or_else(|| PathBuf::from(format!("{mpn}.adom-lbr.json")));-            fs::write(&outp, serde_json::to_string_pretty(&part).unwrap()).ok();+            let json_out = serde_json::to_string_pretty(&part).unwrap();+            let sha = write_verified(&outp, json_out.as_bytes());             println!("OK: wrote {} (adom-lbr JSON — the canonical hub)", outp.display());+            written_hint(&outp, json_out.len(), &sha);             println!("Hint: convert it with `adom-lbr export-altium {}` or `export-intlib`.", outp.display());         } @@ -3504,10 +3541,12 @@ The geometry is fine but a BOM cannot order this part - pass --part-metadata or                 .unwrap_or_else(|e| { eprintln!("ERROR: {e:#}"); process::exit(1); });             let sp = out_dir.join(format!("{}.SchLib", p.mpn));             let pp = out_dir.join(format!("{}.PcbLib", p.mpn));-            fs::write(&sp, &sym_bytes).ok();-            fs::write(&pp, &fp_bytes).ok();+            let sha_s = write_verified(&sp, &sym_bytes);+            let sha_p = write_verified(&pp, &fp_bytes);             println!("OK: wrote {} ({} pins) + {} ({} pads)",                 sp.display(), p.symbol.pins.len(), pp.display(), p.footprint.pads.len());+            written_hint(&sp, sym_bytes.len(), &sha_s);+            written_hint(&pp, fp_bytes.len(), &sha_p);         }          Commands::AssembleAltium { out, parts } => {@@ -3670,8 +3709,9 @@ The geometry is fine but a BOM cannot order this part - pass --part-metadata or             let bytes = altium_codec::encode_intlib(&symbol, &p.footprint, &descr)                 .unwrap_or_else(|e| { eprintln!("ERROR: {e:#}"); process::exit(1); });             let outp = out.unwrap_or_else(|| PathBuf::from(format!("{}.IntLib", p.mpn)));-            fs::write(&outp, &bytes).ok();+            let sha = write_verified(&outp, &bytes);             println!("OK: wrote {} ({} bytes) — symbol + footprint bundled", outp.display(), bytes.len());+            written_hint(&outp, bytes.len(), &sha);         }          Commands::ExportKicad { part, out_dir } => {@@ -3682,11 +3722,13 @@ The geometry is fine but a BOM cannot order this part - pass --part-metadata or             let fp_txt = altium_codec::export_kicad_footprint(&p.footprint);             let sp = out_dir.join(format!("{}.kicad_sym", p.mpn));             let fp = out_dir.join(format!("{}.kicad_mod", p.mpn));-            fs::write(&sp, sym_txt).ok();-            fs::write(&fp, fp_txt).ok();+            let sha_s = write_verified(&sp, sym_txt.as_bytes());+            let sha_f = write_verified(&fp, fp_txt.as_bytes());             println!("OK: wrote {} ({} pins) + {} ({} pads, {} graphics)",                 sp.display(), p.symbol.pins.len(), fp.display(),                 p.footprint.pads.len(), p.footprint.graphics.len());+            written_hint(&sp, sym_txt.len(), &sha_s);+            written_hint(&fp, fp_txt.len(), &sha_f);         }          Commands::AddToSchlib { existing, part, out } => {@@ -3705,8 +3747,9 @@ The geometry is fine but a BOM cannot order this part - pass --part-metadata or             let merged = altium_codec::add_symbols_to_schlib(&existing_bytes, &syms)                 .unwrap_or_else(|e| { eprintln!("ERROR: {e:#}"); process::exit(1); });             let outp = out.unwrap_or_else(|| PathBuf::from("merged.SchLib"));-            fs::write(&outp, &merged).ok();+            let sha = write_verified(&outp, &merged);             println!("OK: wrote {} — added {} symbol(s) into the library", outp.display(), syms.len());+            written_hint(&outp, merged.len(), &sha);         }          Commands::ImportAltium { file, pcblib, schlib, mpn, out } => {@@ -3731,16 +3774,17 @@ The geometry is fine but a BOM cannot order this part - pass --part-metadata or             .unwrap_or_else(|e| { eprintln!("ERROR: {e:#}"); process::exit(1); });             let name = if part.mpn.is_empty() { "part".to_string() } else { part.mpn.clone() };             let outp = out.unwrap_or_else(|| PathBuf::from(format!("{name}.adom-lbr.json")));-            fs::write(&outp, serde_json::to_string_pretty(&part).unwrap()).ok();+            let json_out = serde_json::to_string_pretty(&part).unwrap();+            let sha = write_verified(&outp, json_out.as_bytes());             println!("OK: wrote {} — {} pins, {} pads, {} footprint graphics",                 outp.display(), part.symbol.pins.len(), part.footprint.pads.len(), part.footprint.graphics.len());+            written_hint(&outp, json_out.len(), &sha);         }          Commands::Install => {             let home = std::env::var("HOME").unwrap_or_else(|_| "/home/adom".to_string());             let skill_dir = PathBuf::from(&home).join(".claude/skills/adom-lbr");-            fs::create_dir_all(&skill_dir).ok();-            fs::write(skill_dir.join("SKILL.md"), SKILL).ok();+            write_verified(&skill_dir.join("SKILL.md"), SKILL.as_bytes());             println!("Installed skill: adom-lbr -> {}", skill_dir.display());             println!("Installed adom-lbr v{}", VERSION.trim());         }@@ -4067,6 +4111,19 @@ fn cmd_embed(dir: &std::path::Path, out: Option<PathBuf>) -> Result<(), String>  #[cfg(test)] mod lint_tests {+    #[test]+    fn write_verified_creates_the_directory_and_returns_the_hash() {+        use sha2::{Digest, Sha256};+        let dir = std::env::temp_dir().join(format!("adom-lbr-wv-{}-{}", std::process::id(),+            std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));+        let path = dir.join("nested").join("out.txt");+        assert!(!dir.exists(), "fresh temp dir must not pre-exist");+        let sha = super::write_verified(&path, b"hello export");+        assert_eq!(std::fs::read(&path).unwrap(), b"hello export", "the file must exist with the bytes");+        assert_eq!(sha, format!("{:x}", Sha256::digest(b"hello export")));+        let _ = std::fs::remove_dir_all(&dir);+    }+     use super::{dedup_pad_names, lint_lbr};      // ---- 2.21.0 / 2.22.x conversion fidelity -------------------------------------