@@ -16,11 +16,13 @@ use crate::OutputFormat;
1616pub struct CharacteristicsData {
1717 pub package : Option < String > ,
1818 pub value : Option < String > ,
19+ pub path : Option < String > ,
20+ pub alternatives : Vec < pcb_sch:: Alternative > ,
1921 pub properties : std:: collections:: BTreeMap < String , String > ,
2022}
2123
2224/// Extract characteristics from IPC-2581 Characteristics
23- /// Returns package, value, and custom properties
25+ /// Returns package, value, alternatives, and custom properties
2426/// Note: MPN and Manufacturer must come from AVL/Enterprise (canonical IPC-2581 way)
2527pub fn extract_characteristics (
2628 ipc : & ipc2581:: Ipc2581 ,
@@ -37,12 +39,18 @@ pub fn extract_characteristics(
3739 match name_lower. as_str ( ) {
3840 "package" | "footprint" => data. package = Some ( val_str) ,
3941 "value" => data. value = Some ( val_str) ,
40- // Exclude well-known fields and instance-specific metadata
42+ "path" => data. path = Some ( val_str) ,
43+ "alternatives" => {
44+ if let Some ( alternative) = parse_alternative_json ( & val_str) {
45+ data. alternatives . push ( alternative) ;
46+ }
47+ }
48+ // Exclude well-known fields (MPN/Manufacturer come from AVL)
49+ // and instance-specific metadata
4150 "mpn"
4251 | "manufacturerpartnumber"
4352 | "partnumber"
4453 | "manufacturer"
45- | "path"
4654 | "prefix"
4755 | "symbol_name"
4856 | "symbol_path" => { }
@@ -56,6 +64,22 @@ pub fn extract_characteristics(
5664 data
5765}
5866
67+ /// Parse alternative part data from JSON string
68+ /// Handles HTML-encoded JSON like: {"mpn": "...", "manufacturer": "..."}
69+ fn parse_alternative_json ( json_str : & str ) -> Option < pcb_sch:: Alternative > {
70+ // Decode HTML entities using quick-xml
71+ let decoded = quick_xml:: escape:: unescape ( json_str) . ok ( ) ?. to_string ( ) ;
72+
73+ // Parse as JSON
74+ let parsed: serde_json:: Value = serde_json:: from_str ( & decoded) . ok ( ) ?;
75+
76+ // Extract mpn and manufacturer
77+ let mpn = parsed. get ( "mpn" ) ?. as_str ( ) ?. to_string ( ) ;
78+ let manufacturer = parsed. get ( "manufacturer" ) ?. as_str ( ) ?. to_string ( ) ;
79+
80+ Some ( pcb_sch:: Alternative { mpn, manufacturer } )
81+ }
82+
5983pub fn execute ( file : & Path , format : OutputFormat ) -> Result < ( ) > {
6084 let content = file_utils:: load_ipc_file ( file) ?;
6185 let ipc = ipc2581:: Ipc2581 :: parse ( & content) ?;
@@ -93,6 +117,8 @@ fn extract_bom_from_ipc(ipc: &ipc2581::Ipc2581) -> Result<Bom> {
93117 let CharacteristicsData {
94118 package,
95119 value,
120+ path : component_path,
121+ alternatives : textual_alternatives,
96122 properties,
97123 } = item
98124 . characteristics
@@ -104,6 +130,10 @@ fn extract_bom_from_ipc(ipc: &ipc2581::Ipc2581) -> Result<Bom> {
104130 let ( mpn, manufacturer, avl_alternatives) =
105131 lookup_from_avl ( ipc, item. oem_design_number_ref ) ;
106132
133+ // Merge alternatives: AVL takes precedence, then textual characteristics
134+ let mut alternatives = avl_alternatives;
135+ alternatives. extend ( textual_alternatives) ;
136+
107137 // Use BomItem description attribute if present, otherwise fallback to value
108138 let description = item
109139 . description
@@ -113,35 +143,30 @@ fn extract_bom_from_ipc(ipc: &ipc2581::Ipc2581) -> Result<Bom> {
113143 // Build entry
114144 let entry = BomEntry {
115145 mpn,
116- alternatives : avl_alternatives ,
146+ alternatives,
117147 manufacturer,
118148 package,
119149 value,
120150 description,
121151 generic_data : None ,
122152 offers : Vec :: new ( ) ,
123- dnp : false , // Check ref des for populate flag
153+ dnp : false , // Will be set per ref_des
124154 skip_bom : false ,
125155 properties,
126156 } ;
127157
128158 // Process reference designators
129159 for ref_des in & item. ref_des_list {
130160 let designator = ipc. resolve ( ref_des. name ) . to_string ( ) ;
131-
132- // Skip empty designators (invalid/placeholder entries)
133161 if designator. is_empty ( ) {
134162 continue ;
135163 }
136164
137- // Use Path property if available, otherwise use ipc::designator format
138- let path = entry
139- . properties
140- . get ( "Path" )
141- . cloned ( )
165+ // Use Path from characteristics, or fallback to ipc::designator format
166+ let path = component_path
167+ . clone ( )
142168 . unwrap_or_else ( || format ! ( "ipc::{}" , designator) ) ;
143169
144- // Check DNP status
145170 let mut entry_with_dnp = entry. clone ( ) ;
146171 entry_with_dnp. dnp = !ref_des. populate ;
147172
@@ -198,23 +223,17 @@ pub fn lookup_from_avl(
198223 ipc : & ipc2581:: Ipc2581 ,
199224 oem_design_number_ref : ipc2581:: Symbol ,
200225) -> ( Option < String > , Option < String > , Vec < pcb_sch:: Alternative > ) {
201- // Check if AVL section exists
202- let avl = match ipc. avl ( ) {
203- Some ( avl) => avl,
204- None => return ( None , None , Vec :: new ( ) ) ,
226+ let Some ( avl) = ipc. avl ( ) else {
227+ return ( None , None , Vec :: new ( ) ) ;
205228 } ;
206229
207- // Get the OEM design number string to match against
208230 let oem_design_number_str = ipc. resolve ( oem_design_number_ref) ;
209-
210- // Find matching AvlItem
211- let avl_item = match avl
231+ let Some ( avl_item) = avl
212232 . items
213233 . iter ( )
214234 . find ( |item| ipc. resolve ( item. oem_design_number ) == oem_design_number_str)
215- {
216- Some ( item) => item,
217- None => return ( None , None , Vec :: new ( ) ) ,
235+ else {
236+ return ( None , None , Vec :: new ( ) ) ;
218237 } ;
219238
220239 if avl_item. vmpn_list . is_empty ( ) {
@@ -237,17 +256,14 @@ pub fn lookup_from_avl(
237256 } ) ;
238257
239258 // Rest are alternatives
240- let alternatives: Vec < pcb_sch :: Alternative > = sorted_vmpn[ 1 ..]
259+ let alternatives = sorted_vmpn[ 1 ..]
241260 . iter ( )
242261 . filter_map ( |vmpn| {
243- let mpn = vmpn. mpns . first ( ) ?. name ;
244- let manufacturer_ref = vmpn. vendors . first ( ) ?. enterprise_ref ;
245- let manufacturer = ipc. resolve_enterprise ( manufacturer_ref) ?;
246-
247- Some ( pcb_sch:: Alternative {
248- mpn : ipc. resolve ( mpn) . to_string ( ) ,
249- manufacturer : manufacturer. to_string ( ) ,
250- } )
262+ let mpn = ipc. resolve ( vmpn. mpns . first ( ) ?. name ) . to_string ( ) ;
263+ let manufacturer = ipc
264+ . resolve_enterprise ( vmpn. vendors . first ( ) ?. enterprise_ref ) ?
265+ . to_string ( ) ;
266+ Some ( pcb_sch:: Alternative { mpn, manufacturer } )
251267 } )
252268 . collect ( ) ;
253269
0 commit comments