Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to Semantic Versioning (https://semver.org/spec/v2.0.0.

## [Unreleased]

### Changed

- `Component(pins=...)` now validates pin names as identifiers (ASCII, non-empty, no whitespace, no `.` or `@`) during component construction.

## [0.3.43] - 2026-02-18

### Added
Expand Down
1 change: 1 addition & 0 deletions crates/pcb-zen-core/src/lang/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,7 @@ where
starlark::Error::new_other(anyhow!("pin names must be strings"))
})?
.to_owned();
validate_identifier_name(&signal_name, "Pin name")?;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing pin name validation in pin_defs path

Medium Severity

The new validate_identifier_name call validates pin names from the pins dict but not from the pin_defs dict. Since pin_defs keys become signal names in the symbol, a user can define a pin with a dot (e.g. pin_defs = {"NC.1": "1"}) and the invalid name enters the system unchecked. Worse, if that pin is later referenced in pins, the validation rejects it — creating pins that are definable but unconnectable.

Fix in Cursor Fix in Web


if !final_symbol.signal_names().any(|n| n == signal_name) {
return Err(starlark::Error::new_other(anyhow!(format!(
Expand Down
14 changes: 14 additions & 0 deletions crates/pcb-zen-core/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use pcb_zen_core::{FileProvider, FileProviderError};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

/// In-memory file provider for tests
#[derive(Clone)]
Expand All @@ -28,6 +29,19 @@ impl InMemoryFileProvider {
}
}

pub fn eval_single_file(src: &str) -> pcb_zen_core::WithDiagnostics<pcb_zen_core::EvalOutput> {
let mut files = HashMap::new();
files.insert("test.zen".to_string(), src.to_string());
let file_provider: Arc<dyn pcb_zen_core::FileProvider> =
Arc::new(InMemoryFileProvider::new(files));
pcb_zen_core::EvalContext::new(
file_provider,
pcb_zen_core::resolution::ResolutionResult::empty(),
)
.set_source_path(PathBuf::from("test.zen"))
.eval()
}

impl FileProvider for InMemoryFileProvider {
fn read_file(&self, path: &Path) -> Result<String, FileProviderError> {
let path = self.canonicalize(path)?;
Expand Down
26 changes: 26 additions & 0 deletions crates/pcb-zen-core/tests/component.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#[macro_use]
mod common;
use common::eval_single_file;

snapshot_eval!(component_properties, {
"C146731.kicad_sym" => include_str!("resources/C146731.kicad_sym"),
Expand Down Expand Up @@ -219,6 +220,31 @@ snapshot_eval!(module_dnp_propagates_to_children, {
"#
});

#[test]
#[cfg(not(target_os = "windows"))]
fn component_pins_rejects_dotted_pin_name() {
let result = eval_single_file(
r#"
Component(
name = "U1",
footprint = "TEST:FP",
pin_defs = {"NC1": "1"},
pins = {"NC.1": Net("N1")},
)
"#,
);

assert!(!result.is_success(), "evaluation should fail");
let errors: Vec<String> = result.diagnostics.iter().map(|d| d.to_string()).collect();
assert!(
errors
.iter()
.any(|e| e.contains("Pin name cannot contain invalid characters")),
"expected pin-name validation error, got: {:?}",
errors
);
}

snapshot_eval!(component_inherits_reference_prefix, {
"ic_symbol.kicad_sym" => r#"(kicad_symbol_lib
(symbol "MyIC"
Expand Down
2 changes: 2 additions & 0 deletions docs/pages/spec.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ Key parameters:
- `type`: Component type
- `properties`: Additional properties dict

Pin names in `pins = {...}` must be valid identifiers (ASCII, non-empty, no whitespace, no `.` or `@`).

During `pcb build`, reference designators are allocated per-prefix using a deterministic ordering of component hierarchical instance names. The ordering is "natural" (so `R2` sorts before `R10`).

The allocator also supports **opportunistic refdes hints** encoded in the *hierarchical instance path*: any **non-leaf** path segment that matches a valid reference designator pattern (1-3 capital letters followed by 1-3 digits, no leading zeros) may be used as the assigned reference designator **when it is safe and unambiguous**.
Expand Down
Loading