1use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
4use cfb::CompoundFile;
5use std::collections::HashMap;
6use std::fs::File;
7use std::io::{Cursor, Read, Seek, Write};
8use std::path::Path;
9
10use crate::error::{AltiumError, Result};
11use crate::format::SIZE_FLAG_MASK;
12use crate::io::reader::{
13 decode_windows_1252, read_parameters_block, read_pascal_short_string, read_string_block,
14};
15use crate::io::writer::{
16 write_block, write_parameters, write_pascal_short_string, write_string_block,
17};
18use crate::records::sch::{
19 PinConglomerateFlags, PinElectricalType, PinSymbol, SchComponent, SchGraphicalBase, SchPin,
20 SchRecord, coord_to_dxp_frac, dxp_frac_to_coord,
21};
22use crate::types::ParameterCollection;
23
24#[derive(Debug, Default)]
26pub struct SchLib {
27 section_keys: HashMap<String, String>,
29 pub components: Vec<SchLibComponent>,
31}
32
33#[derive(Debug)]
35pub struct SchLibComponent {
36 pub component: SchComponent,
38 pub primitives: Vec<SchRecord>,
40}
41
42impl SchLib {
43 pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
45 let mut schlib = SchLib::default();
46 let mut cf = CompoundFile::open(reader).map_err(|e| {
47 AltiumError::Io(std::io::Error::new(
48 std::io::ErrorKind::InvalidData,
49 e.to_string(),
50 ))
51 })?;
52
53 schlib.read_section_keys(&mut cf)?;
55
56 let ref_names = schlib.read_file_header(&mut cf)?;
58
59 for ref_name in ref_names.iter() {
61 let section_key = schlib.get_section_key(ref_name);
62 if let Ok(component) = schlib.read_component(&mut cf, §ion_key) {
63 schlib.components.push(component);
64 }
65 }
66
67 Ok(schlib)
68 }
69
70 pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
72 let file = File::open(path)?;
73 Self::open(file)
74 }
75
76 pub fn save<W: Read + Write + Seek>(&self, writer: W) -> Result<()> {
78 let mut cf = CompoundFile::create(writer)
79 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
80
81 self.write_storage(&mut cf)?;
83
84 self.write_file_header(&mut cf)?;
86
87 self.write_section_keys(&mut cf)?;
89
90 for comp in &self.components {
92 self.write_component(&mut cf, comp)?;
93 }
94
95 cf.flush()
96 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
97
98 Ok(())
99 }
100
101 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
103 let file = File::create(path)?;
104 self.save(file)
105 }
106
107 fn write_storage<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
109 let mut data = Vec::new();
110
111 let header = "|HEADER=Icon storage\0";
113 let header_bytes = header.as_bytes();
114 data.write_i32::<LittleEndian>(header_bytes.len() as i32)?;
115 data.write_all(header_bytes)?;
116
117 let stream = cf
118 .create_stream("/Storage")
119 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
120
121 let mut stream = stream;
122 stream.write_all(&data)?;
123
124 Ok(())
125 }
126
127 fn write_file_header<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
129 let mut data = Vec::new();
130
131 let mut header_params = ParameterCollection::new();
133 header_params.add(
134 "HEADER",
135 "Protel for Windows - Schematic Library Editor Binary File Version 5.0",
136 );
137 header_params.add_int("WEIGHT", self.components.len() as i32);
138
139 let mut header_block = Vec::new();
140 write_parameters(&mut header_block, &header_params)?;
141 write_block(&mut data, &header_block, 0)?;
142
143 data.write_i32::<LittleEndian>(self.components.len() as i32)?;
145
146 for comp in &self.components {
148 write_string_block(&mut data, &comp.component.lib_reference)?;
149 }
150
151 let stream = cf
152 .create_stream("/FileHeader")
153 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
154
155 let mut stream = stream;
156 stream.write_all(&data)?;
157
158 Ok(())
159 }
160
161 fn write_section_keys<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
163 let components_needing_keys: Vec<_> = self
165 .components
166 .iter()
167 .filter(|c| Self::needs_section_key(&c.component.lib_reference))
168 .collect();
169
170 if components_needing_keys.is_empty() {
171 return Ok(());
172 }
173
174 let mut data = Vec::new();
175
176 let mut params = ParameterCollection::new();
177 params.add_int("KEYCOUNT", components_needing_keys.len() as i32);
178
179 for (i, comp) in components_needing_keys.iter().enumerate() {
180 let lib_ref = &comp.component.lib_reference;
181 let section_key = Self::get_section_key_for(lib_ref);
182 params.add(&format!("LIBREF{}", i), lib_ref);
183 params.add(&format!("SECTIONKEY{}", i), §ion_key);
184 }
185
186 let mut block = Vec::new();
187 write_parameters(&mut block, ¶ms)?;
188 write_block(&mut data, &block, 0)?;
189
190 let stream = cf
191 .create_stream("/SectionKeys")
192 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
193
194 let mut stream = stream;
195 stream.write_all(&data)?;
196
197 Ok(())
198 }
199
200 fn needs_section_key(name: &str) -> bool {
202 name.len() > 31 || name.contains('/')
203 }
204
205 fn get_section_key_for(name: &str) -> String {
207 let mut key = name.replace('/', "_");
208 if key.len() > 31 {
209 key.truncate(31);
210 }
211 key
212 }
213
214 fn write_component<F: Read + Write + Seek>(
216 &self,
217 cf: &mut CompoundFile<F>,
218 comp: &SchLibComponent,
219 ) -> Result<()> {
220 let section_key = if Self::needs_section_key(&comp.component.lib_reference) {
221 Self::get_section_key_for(&comp.component.lib_reference)
222 } else {
223 comp.component.lib_reference.clone()
224 };
225
226 let storage_path = format!("/{}", section_key);
228 cf.create_storage(&storage_path)
229 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
230
231 let mut data = Vec::new();
233 for record in &comp.primitives {
234 self.write_record(&mut data, record)?;
235 }
236
237 let data_path = format!("{}/Data", storage_path);
238 let stream = cf
239 .create_stream(&data_path)
240 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
241
242 let mut stream = stream;
243 stream.write_all(&data)?;
244
245 Ok(())
246 }
247
248 fn write_record<W: Write>(&self, writer: &mut W, record: &SchRecord) -> Result<()> {
250 match record {
251 SchRecord::Pin(pin) => {
252 self.write_binary_pin(writer, pin)
254 }
255 _ => {
256 let params = record.export_to_params();
258 let mut block = Vec::new();
259 write_parameters(&mut block, ¶ms)?;
260 write_block(writer, &block, 0)
261 }
262 }
263 }
264
265 fn write_binary_pin<W: Write>(&self, writer: &mut W, pin: &SchPin) -> Result<()> {
267 let mut data = Vec::new();
268
269 data.write_i32::<LittleEndian>(2)?;
271
272 data.write_u8(0)?;
274
275 data.write_i16::<LittleEndian>(pin.graphical.base.owner_part_id.unwrap_or(1) as i16)?;
277
278 data.write_u8(pin.graphical.base.owner_part_display_mode.unwrap_or(0) as u8)?;
280
281 data.write_u8(pin.symbol_inner_edge.to_int() as u8)?;
283 data.write_u8(pin.symbol_outer_edge.to_int() as u8)?;
284 data.write_u8(pin.symbol_inside.to_int() as u8)?;
285 data.write_u8(pin.symbol_outside.to_int() as u8)?;
286
287 write_pascal_short_string(&mut data, &pin.description)?;
289
290 data.write_u8(0)?;
292
293 data.write_u8(pin.electrical.to_int() as u8)?;
295
296 data.write_u8(pin.pin_conglomerate.bits())?;
298
299 let (length, _) = coord_to_dxp_frac(pin.pin_length);
301 let (loc_x, _) = coord_to_dxp_frac(pin.graphical.location_x);
302 let (loc_y, _) = coord_to_dxp_frac(pin.graphical.location_y);
303 data.write_i16::<LittleEndian>(length as i16)?;
304 data.write_i16::<LittleEndian>(loc_x as i16)?;
305 data.write_i16::<LittleEndian>(loc_y as i16)?;
306
307 data.write_i32::<LittleEndian>(pin.graphical.color)?;
309
310 write_pascal_short_string(&mut data, &pin.name)?;
312 write_pascal_short_string(&mut data, &pin.designator)?;
313 write_pascal_short_string(&mut data, &pin.swap_id_group)?;
314
315 let part_and_sequence = if pin.swap_id_part == 0 && pin.swap_id_sequence.is_empty() {
317 String::new()
318 } else if pin.swap_id_part != 0 {
319 format!("{}|&|{}", pin.swap_id_part, pin.swap_id_sequence)
320 } else {
321 format!("|&|{}", pin.swap_id_sequence)
322 };
323 write_pascal_short_string(&mut data, &part_and_sequence)?;
324
325 write_pascal_short_string(&mut data, &pin.default_value)?;
327
328 write_block(writer, &data, 0x01)
330 }
331
332 fn get_section_key(&self, ref_name: &str) -> String {
334 self.section_keys
335 .get(ref_name)
336 .cloned()
337 .unwrap_or_else(|| ref_name.to_string())
338 }
339
340 fn read_section_keys<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
342 let stream_path = "/SectionKeys";
343 if cf.entry(stream_path).is_err() {
344 return Ok(());
345 }
346
347 let mut stream = cf.open_stream(stream_path).map_err(|e| {
348 AltiumError::Io(std::io::Error::new(
349 std::io::ErrorKind::NotFound,
350 e.to_string(),
351 ))
352 })?;
353
354 let mut data = Vec::new();
355 stream.read_to_end(&mut data)?;
356
357 if data.is_empty() {
358 return Ok(());
359 }
360
361 let mut cursor = Cursor::new(data);
362 let params = read_parameters_block(&mut cursor)?;
363
364 let key_count = match params.get("KEYCOUNT") {
365 Some(v) => v.as_int_or(0),
366 None => 0,
367 };
368
369 for i in 0..key_count {
370 let lib_ref = match params.get(&format!("LIBREF{}", i)) {
371 Some(v) => v.as_str().to_string(),
372 None => String::new(),
373 };
374 let section_key = match params.get(&format!("SECTIONKEY{}", i)) {
375 Some(v) => v.as_str().to_string(),
376 None => String::new(),
377 };
378
379 if !lib_ref.is_empty() && !section_key.is_empty() {
380 self.section_keys.insert(lib_ref, section_key);
381 }
382 }
383
384 Ok(())
385 }
386
387 fn read_file_header<R: Read + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<Vec<String>> {
389 let stream_path = "/FileHeader";
390 let mut stream = cf.open_stream(stream_path).map_err(|e| {
391 AltiumError::Io(std::io::Error::new(
392 std::io::ErrorKind::NotFound,
393 e.to_string(),
394 ))
395 })?;
396
397 let mut data = Vec::new();
398 stream.read_to_end(&mut data)?;
399
400 if data.is_empty() {
401 return Ok(Vec::new());
402 }
403
404 let mut cursor = Cursor::new(&data);
405 let params = read_parameters_block(&mut cursor)?;
406
407 let mut ref_names = Vec::new();
408
409 if cursor.position() as usize >= data.len() {
411 let comp_count = match params.get("COMPCOUNT") {
412 Some(v) => v.as_int_or(0),
413 None => 0,
414 };
415 for i in 0..comp_count {
416 if let Some(name) = params.get(&format!("LIBREF{}", i)) {
417 ref_names.push(name.as_str().to_string());
418 }
419 }
420 } else {
421 use byteorder::{LittleEndian, ReadBytesExt};
423 let count = cursor.read_u32::<LittleEndian>()?;
424 for _ in 0..count {
425 let name = read_string_block(&mut cursor)?;
426 ref_names.push(name);
427 }
428 }
429
430 Ok(ref_names)
431 }
432
433 fn read_component<R: Read + Seek>(
435 &self,
436 cf: &mut CompoundFile<R>,
437 section_key: &str,
438 ) -> Result<SchLibComponent> {
439 let stream_path = format!("/{}/Data", section_key);
440 let mut stream = cf.open_stream(&stream_path).map_err(|e| {
441 AltiumError::Io(std::io::Error::new(
442 std::io::ErrorKind::NotFound,
443 format!("Component stream not found: {} - {}", stream_path, e),
444 ))
445 })?;
446
447 let mut data = Vec::new();
448 stream.read_to_end(&mut data)?;
449
450 if data.is_empty() {
451 return Err(AltiumError::Parse("Empty component data".to_string()));
452 }
453
454 let mut cursor = Cursor::new(&data);
455 let mut primitives = Vec::new();
456
457 while (cursor.position() as usize) < data.len() {
459 match self.read_record(&mut cursor) {
460 Ok(record) => primitives.push(record),
461 Err(_) => break, }
463 }
464
465 let component = match primitives.first() {
467 Some(SchRecord::Component(c)) => c.clone(),
468 _ => {
469 return Err(AltiumError::Parse(
470 "First record is not a component".to_string(),
471 ));
472 }
473 };
474
475 Ok(SchLibComponent {
476 component,
477 primitives,
478 })
479 }
480
481 fn read_record<R: Read>(&self, reader: &mut R) -> Result<SchRecord> {
483 use byteorder::{LittleEndian, ReadBytesExt};
484
485 let size = reader.read_i32::<LittleEndian>()?;
486 let is_binary = (size as u32 & !SIZE_FLAG_MASK) != 0;
487 let clean_size = (size & SIZE_FLAG_MASK as i32) as usize;
488
489 if clean_size == 0 {
490 return Err(AltiumError::Parse("Empty record".to_string()));
491 }
492
493 let mut buffer = vec![0u8; clean_size];
494 reader.read_exact(&mut buffer)?;
495
496 if is_binary {
497 let mut cursor = Cursor::new(buffer);
499 self.read_binary_pin(&mut cursor)
500 } else {
501 let end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
503 let param_str = decode_windows_1252(&buffer[..end]);
504 let params = ParameterCollection::from_string(¶m_str);
505 SchRecord::from_params(¶ms)
506 }
507 }
508
509 fn read_binary_pin<R: Read>(&self, reader: &mut R) -> Result<SchRecord> {
514 let record_type = reader.read_i32::<LittleEndian>()?;
515 if record_type != 2 {
516 return Err(AltiumError::Parse(format!(
517 "Expected pin record type 2, got {}",
518 record_type
519 )));
520 }
521
522 let _unknown1 = reader.read_u8()?; let owner_part_id = reader.read_i16::<LittleEndian>()?;
524 let owner_part_display_mode = reader.read_u8()?;
525 let symbol_inner_edge = PinSymbol::from_int(reader.read_u8()? as i32);
526 let symbol_outer_edge = PinSymbol::from_int(reader.read_u8()? as i32);
527 let symbol_inside = PinSymbol::from_int(reader.read_u8()? as i32);
528 let symbol_outside = PinSymbol::from_int(reader.read_u8()? as i32);
529 let description = read_pascal_short_string(reader)?;
530 let _unknown2 = reader.read_u8()?; let electrical = PinElectricalType::from_int(reader.read_u8()? as i32);
532 let pin_conglomerate = PinConglomerateFlags::from_int(reader.read_u8()? as i32);
533 let pin_length_int = reader.read_i16::<LittleEndian>()? as i32;
534 let location_x_int = reader.read_i16::<LittleEndian>()? as i32;
535 let location_y_int = reader.read_i16::<LittleEndian>()? as i32;
536 let color = reader.read_i32::<LittleEndian>()?;
537 let name = read_pascal_short_string(reader)?;
538 let designator = read_pascal_short_string(reader)?;
539 let swap_id_group = read_pascal_short_string(reader)?;
540 let part_and_sequence = read_pascal_short_string(reader)?;
541 let default_value = read_pascal_short_string(reader)?;
542
543 let (swap_id_part, swap_id_sequence) = if !part_and_sequence.is_empty() {
545 let parts: Vec<&str> = part_and_sequence.split('|').collect();
546 if parts.len() == 3 {
547 (parts[0].parse().unwrap_or(0), parts[2].to_string())
548 } else {
549 (0, String::new())
550 }
551 } else {
552 (0, String::new())
553 };
554
555 let pin_length = dxp_frac_to_coord(pin_length_int, 0);
558 let location_x = dxp_frac_to_coord(location_x_int, 0);
559 let location_y = dxp_frac_to_coord(location_y_int, 0);
560
561 let mut graphical = SchGraphicalBase::default();
562 graphical.base.owner_part_id = Some(owner_part_id as i32);
563 graphical.base.owner_part_display_mode = Some(owner_part_display_mode as i32);
564 graphical.location_x = location_x;
565 graphical.location_y = location_y;
566 graphical.color = color;
567
568 let pin = SchPin {
569 graphical,
570 symbol_inner_edge,
571 symbol_outer_edge,
572 symbol_inside,
573 symbol_outside,
574 description,
575 electrical,
576 pin_conglomerate,
577 pin_length,
578 name,
579 designator,
580 swap_id_group,
581 swap_id_part,
582 swap_id_sequence,
583 default_value,
584 ..Default::default()
585 };
586
587 Ok(SchRecord::Pin(pin))
588 }
589
590 pub fn component_count(&self) -> usize {
592 self.components.len()
593 }
594
595 pub fn iter(&self) -> impl Iterator<Item = &SchLibComponent> {
597 self.components.iter()
598 }
599}
600
601impl SchLibComponent {
602 pub fn name(&self) -> &str {
604 &self.component.lib_reference
605 }
606
607 pub fn description(&self) -> &str {
609 &self.component.component_description
610 }
611
612 pub fn pin_count(&self) -> usize {
614 self.primitives
615 .iter()
616 .filter(|r| matches!(r, SchRecord::Pin(_)))
617 .count()
618 }
619
620 pub fn primitive_count(&self) -> usize {
622 self.primitives.len()
623 }
624}
625
626use crate::dump::{DumpTree, TreeBuilder};
628
629impl DumpTree for SchLib {
630 fn dump(&self, tree: &mut TreeBuilder) {
631 tree.root(&format!("SchLib ({} components)", self.components.len()));
632
633 for (i, comp) in self.components.iter().enumerate() {
634 tree.push(i < self.components.len() - 1);
635 comp.dump(tree);
636 tree.pop();
637 }
638 }
639}
640
641impl DumpTree for SchLibComponent {
642 fn dump(&self, tree: &mut TreeBuilder) {
643 tree.begin_node(&format!("Symbol: {}", self.component.lib_reference));
644 tree.push(true);
645
646 tree.push(self.primitives.len() > 1);
648 let mut meta_props = vec![];
649 if !self.component.component_description.is_empty() {
650 meta_props.push(("description", self.component.component_description.clone()));
651 }
652 meta_props.push(("parts", format!("{}", self.component.part_count)));
653 meta_props.push(("pins", format!("{}", self.pin_count())));
654 meta_props.push(("primitives", format!("{}", self.primitive_count())));
655 tree.add_leaf("Info", &meta_props);
656 tree.pop();
657
658 let child_primitives: Vec<_> = self.primitives.iter().skip(1).collect();
660 if !child_primitives.is_empty() {
661 tree.push(false);
662 tree.begin_node(&format!("Primitives ({})", child_primitives.len()));
663 for (i, prim) in child_primitives.iter().enumerate() {
664 tree.push(i < child_primitives.len() - 1);
665 prim.dump(tree);
666 tree.pop();
667 }
668 tree.pop();
669 }
670
671 tree.pop();
672 }
673}