altium-format 0.1.6

Core altium-cli library for reading and writing Altium Designer files.
Documentation

altium-format

Rust library for reading and writing Altium Designer files with a trait-based API and agent-friendly CLI.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                      altium-cli                              │
│  (clap binary with subcommands: inspect, query, export)      │
└─────────────────────────┬───────────────────────────────────┘
                          │
┌─────────────────────────▼───────────────────────────────────┐
│                    altium-format                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
│  │   api/      │  │   ops/      │  │   edit/     │          │
│  │ (3-layer)   │  │ (queries)   │  │ (sessions)  │          │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘          │
│         │                │                │                  │
│  ┌──────▼────────────────▼────────────────▼──────┐          │
│  │                  records/                      │          │
│  │  SchPrimitive trait ◄── SchRecord variants    │          │
│  │  PcbPrimitive trait ◄── PcbRecord variants    │          │
│  └──────────────────────┬────────────────────────┘          │
│                         │                                    │
│  ┌──────────────────────▼────────────────────────┐          │
│  │                   io/                          │          │
│  │  reader.rs, writer.rs, schdoc.rs, pcbdoc.rs   │          │
│  └──────────────────────┬────────────────────────┘          │
│                         │                                    │
│  ┌──────────────────────▼────────────────────────┐          │
│  │                  types/                        │          │
│  │  Coord, Color, Layer, ParameterCollection     │          │
│  └───────────────────────────────────────────────┘          │
└─────────────────────────────────────────────────────────────┘

Data Flow

Altium File (.SchLib, .PcbDoc, etc.)
         │
         ▼
    cfb crate (OLE compound document)
         │
         ▼
    io/reader.rs (block parsing, decompression)
         │
         ▼
    api/generic/ (ParameterCollection, Value)
         │
         ▼
    FromParams/FromBinary traits
         │
         ▼
    Typed records (SchComponent, PcbPad, etc.)
         │
         ▼
    SchPrimitive/PcbPrimitive trait access
         │
         ▼
    ops/ queries and transforms
         │
         ▼
    CLI JSON output

Core Abstractions

Three-Layer API

Users choose their abstraction level:

  1. Layer 1 (CFB): Raw access to compound file structure

    • Use when: Reverse engineering unknown file types
    • Example: doc.cfb().streams() lists all internal files
  2. Layer 2 (Generic): Dynamic access without type knowledge

    • Use when: Schema is unknown or varies
    • Example: record.get("LIBREFERENCE") accesses any parameter
  3. Layer 3 (Typed): Full deserialization with derive macros

    • Use when: Working with known record types
    • Example: component.lib_reference provides type-safe access

Trait Hierarchy

SchPrimitive and PcbPrimitive traits enable polymorphic dispatch:

  • RECORD_ID / OBJECT_ID: Type identifier constant
  • owner_index() / set_owner_index(): Hierarchy management
  • location(): Optional coordinate access
  • record_type_name(): Diagnostic type name
  • get_property(): Dynamic property lookup
  • calculate_bounds(): Bounding rectangle

Implemented by 30 schematic types and 15 PCB types. Adding a new record type requires only implementing the trait, not updating scattered match statements.

State Types Over Boolean Pairs

MaskExpansion replaces implicit state machines:

// Before: manual + value creates invalid states
pub paste_mask_expansion_manual: bool,
pub paste_mask_expansion: Coord,

// After: type system prevents invalid states
pub paste_mask_expansion: MaskExpansion,  // Auto | Manual(Coord)

Validation happens at construction, not at use. Corrupted data from files returns Result::Err rather than panicking for recoverability.

Coordinate System

Altium uses fixed-point coordinates:

  • 10,000 internal units = 1 mil = 0.001 inch
  • All coordinate operations use Coord newtype wrapper
  • Coord::from_mils(10) = 100,000 internal units
  • Coord::from_mm(2.54) = 1,000,000 internal units (1 inch)

Integer storage avoids floating-point precision issues. Extract raw value with .to_raw() for arithmetic.

Roundtrip Fidelity Invariants

  1. FromParams + ToParams must be inverses

    • Property-based tests verify: from_params(to_params(x)) == x
    • Unknown fields preserved in UnknownFields
  2. FromBinary + ToBinary must be inverses

    • Binary format must match byte-for-byte
    • Golden file tests lock format compatibility
  3. OWNERINDEX consistency

    • Parent-child relationships encoded via OWNERINDEX
    • Tree operations preserve hierarchy integrity
    • Editing sessions track index mutations

Operations Design

ops/ modules provide pure query functions that return data, not formatted output:

  • components_by_category() returns HashMap<Category, Vec<Component>>
  • net_connections() returns Vec<NetConnection>
  • power_map() returns HashMap<NetName, Count>

CLI commands compose these operations. Same logic powers programmatic API and command-line tools.

Deterministic UUID Context

Standalone library uses uuid::Uuid::new_v4() for standard random UUIDs. Cadatomic fork replaces () parameter with DeterminismContext for event sourcing replay.

Signature: build_deterministic(&mut ()) accepts unit type; fork swaps in context type for deterministic generation.

Format Quirks

Display Mode Bounds

display_mode indexes into array of display_mode_count elements. Out-of-bounds access causes undefined behavior when rendering symbol views.

Validation: set_display_mode() returns Result::Err for invalid indices rather than panicking. Corrupted files should be recoverable.

Manual Flag Values

Altium binary format uses flag value 2 (not 1) for manual mode, 0 for auto mode. Observed from PCB1.PcbLib test files. MaskExpansion implementation uses != 0 check for robustness against format variations.

Parameter vs Binary Records

  • Schematic records: Parameter-based (key=value pairs)

    • Format: |RECORD=1|NAME=Test|X=100mil|
    • Trait: FromParams, ToParams
  • PCB records: Binary-based (fixed structs)

    • Format: Block header + packed binary data
    • Trait: FromBinary, ToBinary

Different file types require different deserialization paths.

CLI Agent-Friendly Design

JSON-first output for programmatic consumption:

altium-cli inspect library.SchLib | jq '.components[0].name'

Selector queries for filtering:

altium-cli query library.SchLib "Component[Designator^=U]"  # ICs only

Stable schemas locked by golden file tests. Breaking changes increment semver.

Build Instructions

Library

cargo build -p altium-format
cargo test -p altium-format
cargo doc -p altium-format --open

CLI Binary

cargo build --bin altium-cli
cargo install --path crates/altium-format
altium-cli --help

Shell Completions

altium-cli completions bash > /usr/share/bash-completion/completions/altium-cli

Usage Examples

Reading a Library

use altium_format::io::SchLib;
use std::fs::File;
use std::io::BufReader;

let file = File::open("resistors.SchLib")?;
let lib = SchLib::open(BufReader::new(file))?;

for component in &lib.components {
    println!("{}: {} pins", component.name, component.pin_count());
}

Creating a Footprint

use altium_format::footprint::FootprintBuilder;
use altium_format::records::pcb::PcbPadShape;

let mut builder = FootprintBuilder::new("SOIC-8");
builder.add_dual_row_smd(
    4,      // pads per side
    1.27,   // pitch (mm)
    5.3,    // row spacing (mm)
    1.5,    // pad width (mm)
    0.6,    // pad height (mm)
    PcbPadShape::Rectangular,
);
let component = builder.build_deterministic(&mut ());

Querying Components

use altium_format::query::query_records;
use altium_format::io::SchLib;

let lib = SchLib::open(file)?;
let resistors = query_records(&lib.records, "Component[Designator^=R]")?;

for comp in resistors {
    println!("Found resistor: {}", comp.designator);
}

Editing Sessions

use altium_format::edit::EditSession;

let mut session = EditSession::new(lib);
session.set_component_property("R1", "VALUE", "10k");
session.commit()?;
session.save("modified.SchLib")?;

Testing Strategy

  1. Property-based tests: Verify trait contracts across all record types
  2. Real file integration: Test with actual Altium libraries
  3. Golden file tests: Lock CLI output format for stability
  4. Roundtrip tests: Ensure serialization is lossless

License

GPL-3.0-only

Repository

https://github.com/akiselev/altium-cli