1use cfb::CompoundFile;
10use flate2::Compression;
11use flate2::read::ZlibDecoder;
12use flate2::write::ZlibEncoder;
13use std::collections::HashMap;
14use std::fs::File;
15use std::io::{Cursor, Read, Seek, Write};
16use std::path::Path;
17
18use crate::error::{AltiumError, Result};
19use crate::io::{PcbLib, SchLib};
20use crate::types::ParameterCollection;
21
22#[derive(Debug, Default)]
24pub struct IntLib {
25 pub version: u32,
27 pub schlib: SchLib,
29 pub pcblib: PcbLib,
31 pub cross_refs: Vec<CrossReference>,
33 pub parameters: Vec<ComponentParameters>,
35}
36
37#[derive(Debug, Clone, Default)]
39pub struct CrossReference {
40 pub name: String,
42 pub schlib_path: String,
44 pub description: String,
46 pub source_path: String,
48 pub footprint: String,
50 pub pcblib_type: String,
52 pub pcblib_path: String,
54 pub pcblib_source_path: String,
56}
57
58#[derive(Debug, Clone)]
60pub struct ComponentParameters {
61 pub name: String,
63 pub params: ParameterCollection,
65}
66
67impl IntLib {
68 pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
70 let mut intlib = IntLib::default();
71 let mut cf = CompoundFile::open(reader).map_err(|e| {
72 AltiumError::Io(std::io::Error::new(
73 std::io::ErrorKind::InvalidData,
74 e.to_string(),
75 ))
76 })?;
77
78 intlib.read_version(&mut cf)?;
80
81 intlib.read_cross_refs(&mut cf)?;
83
84 intlib.read_parameters(&mut cf)?;
86
87 intlib.read_schlib(&mut cf)?;
89
90 intlib.read_pcblib(&mut cf)?;
92
93 Ok(intlib)
94 }
95
96 pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
98 let file = File::open(path)?;
99 Self::open(file)
100 }
101
102 pub fn save<W: Read + Write + Seek>(&self, writer: W) -> Result<()> {
104 let mut cf = CompoundFile::create(writer)
105 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
106
107 self.write_version(&mut cf)?;
109
110 self.write_cross_refs(&mut cf)?;
112
113 self.write_parameters(&mut cf)?;
115
116 self.write_schlib(&mut cf)?;
118
119 self.write_pcblib(&mut cf)?;
121
122 cf.flush()
123 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
124
125 Ok(())
126 }
127
128 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
130 let file = File::create(path)?;
131 self.save(file)
132 }
133
134 fn read_version<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
136 let stream_path = "/Version.Txt";
137 if cf.entry(stream_path).is_err() {
138 return Ok(());
139 }
140
141 let mut stream = cf.open_stream(stream_path).map_err(|e| {
142 AltiumError::Io(std::io::Error::new(
143 std::io::ErrorKind::NotFound,
144 e.to_string(),
145 ))
146 })?;
147
148 let mut data = Vec::new();
149 stream.read_to_end(&mut data)?;
150
151 if data.len() >= 5 {
152 self.version = data[1] as u32 | ((data[2] as u32) << 8);
154 }
155
156 Ok(())
157 }
158
159 fn write_version<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
161 let mut data = vec![0u8; 5];
162 data[1] = (self.version & 0xFF) as u8;
163 data[2] = ((self.version >> 8) & 0xFF) as u8;
164
165 let stream = cf
166 .create_stream("/Version.Txt")
167 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
168
169 let mut stream = stream;
170 stream.write_all(&data)?;
171
172 Ok(())
173 }
174
175 fn read_cross_refs<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
177 let stream_path = "/LibCrossRef.Txt";
178 if cf.entry(stream_path).is_err() {
179 return Ok(());
180 }
181
182 let mut stream = cf.open_stream(stream_path).map_err(|e| {
183 AltiumError::Io(std::io::Error::new(
184 std::io::ErrorKind::NotFound,
185 e.to_string(),
186 ))
187 })?;
188
189 let mut data = Vec::new();
190 stream.read_to_end(&mut data)?;
191
192 if data.is_empty() {
193 return Ok(());
194 }
195
196 if data.len() > 1 && data[0] == 0x02 {
198 let decompressed = decompress_zlib(&data[1..])?;
199 self.parse_cross_refs(&decompressed)?;
200 }
201
202 Ok(())
203 }
204
205 fn parse_cross_refs(&mut self, data: &[u8]) -> Result<()> {
218 use byteorder::{LittleEndian, ReadBytesExt};
219
220 if data.len() < 4 {
221 return Ok(());
222 }
223
224 let mut cursor = Cursor::new(data);
225
226 let entry_count = cursor.read_u32::<LittleEndian>()? as usize;
228
229 for _ in 0..entry_count {
231 match self.read_cross_ref_entry(&mut cursor) {
232 Ok(entry) => {
233 if !entry.name.is_empty() {
234 self.cross_refs.push(entry);
235 }
236 }
237 Err(_) => break,
238 }
239 }
240
241 Ok(())
242 }
243
244 fn read_block_string<R: Read>(reader: &mut R) -> Result<String> {
251 use byteorder::{LittleEndian, ReadBytesExt};
252
253 let block_size = reader.read_u32::<LittleEndian>()? as usize;
254
255 if block_size <= 1 {
257 return Ok(String::new());
258 }
259
260 let str_len = reader.read_u8()? as usize;
262 if str_len == 0 {
263 let remaining = block_size.saturating_sub(1);
265 if remaining > 0 {
266 let mut skip = vec![0u8; remaining];
267 let _ = reader.read_exact(&mut skip);
268 }
269 return Ok(String::new());
270 }
271
272 let mut buf = vec![0u8; str_len];
274 reader.read_exact(&mut buf)?;
275
276 Ok(String::from_utf8_lossy(&buf).to_string())
277 }
278
279 fn read_cross_ref_entry<R: Read>(&self, reader: &mut R) -> Result<CrossReference> {
281 let name = Self::read_block_string(reader)?;
283 let schlib_path = Self::read_block_string(reader)?;
285 let _ = Self::read_block_string(reader)?;
287 let description = Self::read_block_string(reader)?;
289 let source_path = Self::read_block_string(reader)?;
291 let _ = Self::read_block_string(reader)?;
293 let footprint = Self::read_block_string(reader)?;
295 let pcblib_type = Self::read_block_string(reader)?;
297 let _ = Self::read_block_string(reader)?;
299 let pcblib_path = Self::read_block_string(reader)?;
301 let pcblib_source_path = Self::read_block_string(reader)?;
303
304 Ok(CrossReference {
305 name,
306 schlib_path,
307 description,
308 source_path,
309 footprint,
310 pcblib_type,
311 pcblib_path,
312 pcblib_source_path,
313 })
314 }
315
316 fn write_cross_refs<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
318 if self.cross_refs.is_empty() {
319 return Ok(());
320 }
321
322 use byteorder::{LittleEndian, WriteBytesExt};
323
324 let mut data = Vec::new();
325
326 data.write_u32::<LittleEndian>(self.cross_refs.len() as u32)?;
328
329 for entry in &self.cross_refs {
331 self.write_cross_ref_entry(&mut data, entry)?;
332 }
333
334 let compressed = compress_zlib(&data)?;
335 let mut final_data = vec![0x02u8];
336 final_data.extend(compressed);
337
338 let stream = cf
339 .create_stream("/LibCrossRef.Txt")
340 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
341
342 let mut stream = stream;
343 stream.write_all(&final_data)?;
344
345 Ok(())
346 }
347
348 fn write_block_string<W: Write>(writer: &mut W, s: &str) -> Result<()> {
350 use byteorder::{LittleEndian, WriteBytesExt};
351
352 if s.is_empty() {
353 writer.write_u32::<LittleEndian>(1)?;
355 } else {
356 let bytes = s.as_bytes();
358 let block_size = 1 + bytes.len(); writer.write_u32::<LittleEndian>(block_size as u32)?;
360 writer.write_u8(bytes.len() as u8)?;
361 writer.write_all(bytes)?;
362 }
363 Ok(())
364 }
365
366 fn write_cross_ref_entry<W: Write>(
368 &self,
369 writer: &mut W,
370 entry: &CrossReference,
371 ) -> Result<()> {
372 Self::write_block_string(writer, &entry.name)?;
374 Self::write_block_string(writer, &entry.schlib_path)?;
376 Self::write_block_string(writer, "")?;
378 Self::write_block_string(writer, &entry.description)?;
380 Self::write_block_string(writer, &entry.source_path)?;
382 Self::write_block_string(writer, "")?;
384 Self::write_block_string(writer, &entry.footprint)?;
386 Self::write_block_string(writer, &entry.pcblib_type)?;
388 Self::write_block_string(writer, "")?;
390 Self::write_block_string(writer, &entry.pcblib_path)?;
392 Self::write_block_string(writer, &entry.pcblib_source_path)?;
394
395 Ok(())
396 }
397
398 fn read_parameters<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
400 let stream_path = "/Parameters .bin";
402 if cf.entry(stream_path).is_err() {
403 return Ok(());
404 }
405
406 let mut stream = cf.open_stream(stream_path).map_err(|e| {
407 AltiumError::Io(std::io::Error::new(
408 std::io::ErrorKind::NotFound,
409 e.to_string(),
410 ))
411 })?;
412
413 let mut data = Vec::new();
414 stream.read_to_end(&mut data)?;
415
416 if data.is_empty() {
417 return Ok(());
418 }
419
420 if data.len() > 1 && data[0] == 0x02 {
422 let decompressed = decompress_zlib(&data[1..])?;
423 self.parse_parameters(&decompressed)?;
424 }
425
426 Ok(())
427 }
428
429 fn parse_parameters(&mut self, data: &[u8]) -> Result<()> {
431 let mut cursor = Cursor::new(data);
434
435 while (cursor.position() as usize) < data.len() {
436 match self.read_parameter_entry(&mut cursor) {
437 Ok(entry) => self.parameters.push(entry),
438 Err(_) => break,
439 }
440 }
441
442 Ok(())
443 }
444
445 fn read_parameter_entry<R: Read>(&self, reader: &mut R) -> Result<ComponentParameters> {
447 use byteorder::{LittleEndian, ReadBytesExt};
448
449 let len = reader.read_u16::<LittleEndian>()? as usize;
451 if len == 0 {
452 return Err(AltiumError::Parse("Empty parameter block".to_string()));
453 }
454
455 let mut buf = vec![0u8; len];
456 reader.read_exact(&mut buf)?;
457
458 let text = String::from_utf8_lossy(&buf).to_string();
460 let params = ParameterCollection::from_string(&text);
461
462 let name = params
464 .get("Library Reference")
465 .or_else(|| params.get("Designator"))
466 .map(|v| v.as_str().to_string())
467 .unwrap_or_default();
468
469 Ok(ComponentParameters { name, params })
470 }
471
472 fn write_parameters<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
474 if self.parameters.is_empty() {
475 return Ok(());
476 }
477
478 use byteorder::{LittleEndian, WriteBytesExt};
479
480 let mut data = Vec::new();
481 for entry in &self.parameters {
482 let param_str = entry.params.to_string();
483 let bytes = param_str.as_bytes();
484 data.write_u16::<LittleEndian>(bytes.len() as u16)?;
485 data.write_all(bytes)?;
486 }
487
488 let compressed = compress_zlib(&data)?;
489 let mut final_data = vec![0x02u8];
490 final_data.extend(compressed);
491
492 let stream = cf
493 .create_stream("/Parameters .bin")
494 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
495
496 let mut stream = stream;
497 stream.write_all(&final_data)?;
498
499 Ok(())
500 }
501
502 fn read_schlib<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
504 let stream_path = "/SchLib/0.schlib";
505 if cf.entry(stream_path).is_err() {
506 return Ok(());
507 }
508
509 let mut stream = cf.open_stream(stream_path).map_err(|e| {
510 AltiumError::Io(std::io::Error::new(
511 std::io::ErrorKind::NotFound,
512 e.to_string(),
513 ))
514 })?;
515
516 let mut data = Vec::new();
517 stream.read_to_end(&mut data)?;
518
519 if data.is_empty() {
520 return Ok(());
521 }
522
523 if data.len() > 1 && data[0] == 0x02 {
525 let decompressed = decompress_zlib(&data[1..])?;
526 let cursor = Cursor::new(decompressed);
528 self.schlib = SchLib::open(cursor)?;
529 }
530
531 Ok(())
532 }
533
534 fn write_schlib<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
536 cf.create_storage("/SchLib")
538 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
539
540 let mut schlib_buf = Cursor::new(Vec::new());
542 self.schlib.save(&mut schlib_buf)?;
543
544 let compressed = compress_zlib(schlib_buf.get_ref())?;
546 let mut final_data = vec![0x02u8];
547 final_data.extend(compressed);
548
549 let stream = cf
550 .create_stream("/SchLib/0.schlib")
551 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
552
553 let mut stream = stream;
554 stream.write_all(&final_data)?;
555
556 Ok(())
557 }
558
559 fn read_pcblib<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
561 let stream_path = "/PCBLib/0.pcblib";
562 if cf.entry(stream_path).is_err() {
563 return Ok(());
564 }
565
566 let mut stream = cf.open_stream(stream_path).map_err(|e| {
567 AltiumError::Io(std::io::Error::new(
568 std::io::ErrorKind::NotFound,
569 e.to_string(),
570 ))
571 })?;
572
573 let mut data = Vec::new();
574 stream.read_to_end(&mut data)?;
575
576 if data.is_empty() {
577 return Ok(());
578 }
579
580 if data.len() > 1 && data[0] == 0x02 {
582 let decompressed = decompress_zlib(&data[1..])?;
583 let cursor = Cursor::new(decompressed);
585 self.pcblib = PcbLib::open(cursor)?;
586 }
587
588 Ok(())
589 }
590
591 fn write_pcblib<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
593 cf.create_storage("/PCBLib")
595 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
596
597 let mut pcblib_buf = Cursor::new(Vec::new());
599 self.pcblib.save(&mut pcblib_buf)?;
600
601 let compressed = compress_zlib(pcblib_buf.get_ref())?;
603 let mut final_data = vec![0x02u8];
604 final_data.extend(compressed);
605
606 let stream = cf
607 .create_stream("/PCBLib/0.pcblib")
608 .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
609
610 let mut stream = stream;
611 stream.write_all(&final_data)?;
612
613 Ok(())
614 }
615
616 pub fn schematic_component_count(&self) -> usize {
618 self.schlib.component_count()
619 }
620
621 pub fn footprint_count(&self) -> usize {
623 self.pcblib.component_count()
624 }
625
626 pub fn get_cross_ref(&self, name: &str) -> Option<&CrossReference> {
628 self.cross_refs.iter().find(|r| r.name == name)
629 }
630
631 pub fn get_parameters(&self, name: &str) -> Option<&ComponentParameters> {
633 self.parameters.iter().find(|p| p.name == name)
634 }
635
636 pub fn component_footprint_map(&self) -> HashMap<String, String> {
638 self.cross_refs
639 .iter()
640 .map(|r| (r.name.clone(), r.footprint.clone()))
641 .collect()
642 }
643}
644
645fn decompress_zlib(data: &[u8]) -> Result<Vec<u8>> {
647 let mut decoder = ZlibDecoder::new(data);
648 let mut decompressed = Vec::new();
649 decoder.read_to_end(&mut decompressed).map_err(|e| {
650 AltiumError::Io(std::io::Error::new(
651 std::io::ErrorKind::InvalidData,
652 format!("zlib decompress failed: {}", e),
653 ))
654 })?;
655 Ok(decompressed)
656}
657
658fn compress_zlib(data: &[u8]) -> Result<Vec<u8>> {
660 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
661 encoder.write_all(data)?;
662 encoder.finish().map_err(|e| {
663 AltiumError::Io(std::io::Error::other(format!(
664 "zlib compress failed: {}",
665 e
666 )))
667 })
668}
669
670use crate::dump::{DumpTree, TreeBuilder};
672
673impl DumpTree for IntLib {
674 fn dump(&self, tree: &mut TreeBuilder) {
675 tree.root(&format!(
676 "IntLib (v{}, {} symbols, {} footprints)",
677 self.version,
678 self.schematic_component_count(),
679 self.footprint_count()
680 ));
681
682 tree.push(true);
684 tree.begin_node(&format!("Cross-References ({})", self.cross_refs.len()));
685 for (i, xref) in self.cross_refs.iter().enumerate() {
686 tree.push(i < self.cross_refs.len() - 1);
687 let props = vec![
688 ("symbol", xref.name.clone()),
689 ("footprint", xref.footprint.clone()),
690 ("description", xref.description.clone()),
691 ];
692 tree.add_leaf(&xref.name, &props);
693 tree.pop();
694 }
695 tree.pop();
696
697 tree.push(true);
699 tree.begin_node(&format!(
700 "SchLib ({} components)",
701 self.schlib.component_count()
702 ));
703 for (i, comp) in self.schlib.iter().enumerate() {
704 tree.push(i < self.schlib.component_count() - 1);
705 let props = vec![
706 ("name", comp.name().to_string()),
707 ("pins", format!("{}", comp.pin_count())),
708 ];
709 tree.add_leaf(comp.name(), &props);
710 tree.pop();
711 }
712 tree.pop();
713
714 tree.push(false);
716 tree.begin_node(&format!(
717 "PcbLib ({} footprints)",
718 self.pcblib.component_count()
719 ));
720 for (i, comp) in self.pcblib.iter().enumerate() {
721 tree.push(i < self.pcblib.component_count() - 1);
722 let props = vec![
723 ("name", comp.pattern.clone()),
724 ("pads", format!("{}", comp.pad_count())),
725 ];
726 tree.add_leaf(&comp.pattern, &props);
727 tree.pop();
728 }
729 tree.pop();
730 }
731}