Skip to main content

altium_format/io/
intlib.rs

1//! IntLib reader/writer for Altium Integrated Library files.
2//!
3//! IntLib files are CFB containers that bundle:
4//! - Embedded SchLib (zlib-compressed CFB)
5//! - Embedded PcbLib (zlib-compressed CFB)
6//! - Cross-reference mapping components to symbols and footprints
7//! - Consolidated component parameters
8
9use 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/// An integrated library containing schematic symbols and PCB footprints.
23#[derive(Debug, Default)]
24pub struct IntLib {
25    /// Version of the IntLib format.
26    pub version: u32,
27    /// Embedded schematic library.
28    pub schlib: SchLib,
29    /// Embedded PCB footprint library.
30    pub pcblib: PcbLib,
31    /// Cross-reference entries mapping components to their symbols and footprints.
32    pub cross_refs: Vec<CrossReference>,
33    /// Component parameters (BOM data).
34    pub parameters: Vec<ComponentParameters>,
35}
36
37/// Cross-reference entry linking a component to its symbol and footprint.
38#[derive(Debug, Clone, Default)]
39pub struct CrossReference {
40    /// Component name.
41    pub name: String,
42    /// Schematic symbol library path (relative within IntLib).
43    pub schlib_path: String,
44    /// Description from the schematic symbol.
45    pub description: String,
46    /// Original source path.
47    pub source_path: String,
48    /// PCB footprint name.
49    pub footprint: String,
50    /// PCB library type (e.g., "PCBLIB").
51    pub pcblib_type: String,
52    /// PCB library path (relative within IntLib).
53    pub pcblib_path: String,
54    /// Original PCB library source path.
55    pub pcblib_source_path: String,
56}
57
58/// Parameters for a component (BOM data).
59#[derive(Debug, Clone)]
60pub struct ComponentParameters {
61    /// Component name.
62    pub name: String,
63    /// Key-value parameters.
64    pub params: ParameterCollection,
65}
66
67impl IntLib {
68    /// Open and read an IntLib file.
69    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        // Read version
79        intlib.read_version(&mut cf)?;
80
81        // Read cross-references
82        intlib.read_cross_refs(&mut cf)?;
83
84        // Read parameters
85        intlib.read_parameters(&mut cf)?;
86
87        // Read embedded SchLib
88        intlib.read_schlib(&mut cf)?;
89
90        // Read embedded PcbLib
91        intlib.read_pcblib(&mut cf)?;
92
93        Ok(intlib)
94    }
95
96    /// Open and read an IntLib file from a path.
97    pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
98        let file = File::open(path)?;
99        Self::open(file)
100    }
101
102    /// Save the IntLib to a file.
103    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        // Write version
108        self.write_version(&mut cf)?;
109
110        // Write cross-references
111        self.write_cross_refs(&mut cf)?;
112
113        // Write parameters
114        self.write_parameters(&mut cf)?;
115
116        // Write embedded SchLib
117        self.write_schlib(&mut cf)?;
118
119        // Write embedded PcbLib
120        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    /// Save the IntLib to a file path.
129    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    /// Read the version stream.
135    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            // Format: [0x00, version_low, version_high, 0x00, 0x00]
153            self.version = data[1] as u32 | ((data[2] as u32) << 8);
154        }
155
156        Ok(())
157    }
158
159    /// Write the version stream.
160    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    /// Read the cross-reference stream.
176    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        // Decompress: first byte is 0x02, rest is zlib
197        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    /// Parse cross-reference data.
206    ///
207    /// The format is:
208    /// - 4-byte entry count
209    /// - For each entry, 11 blocks:
210    ///   0: name, 1: schlib_path, 2: empty, 3: description, 4: schlib_source,
211    ///   5: empty, 6: footprint, 7: pcblib_type, 8: empty, 9: pcblib_path, 10: pcblib_source
212    ///
213    /// Each block is:
214    /// - 4-byte size
215    /// - If size <= 1: empty (no content)
216    /// - If size > 1: 1-byte length + string
217    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        // Read entry count
227        let entry_count = cursor.read_u32::<LittleEndian>()? as usize;
228
229        // Read each entry (11 blocks per entry)
230        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    /// Read a string block.
245    ///
246    /// Format:
247    /// - 4-byte size
248    /// - If size <= 1: empty (no content bytes)
249    /// - If size > 1: 1-byte length + string (total = size bytes)
250    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        // Empty block - no content
256        if block_size <= 1 {
257            return Ok(String::new());
258        }
259
260        // Read the string length (1 byte)
261        let str_len = reader.read_u8()? as usize;
262        if str_len == 0 {
263            // Skip remaining bytes in block
264            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        // Read string
273        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    /// Read a single cross-reference entry (11 blocks).
280    fn read_cross_ref_entry<R: Read>(&self, reader: &mut R) -> Result<CrossReference> {
281        // Block 0: name
282        let name = Self::read_block_string(reader)?;
283        // Block 1: schlib_path (relative)
284        let schlib_path = Self::read_block_string(reader)?;
285        // Block 2: empty
286        let _ = Self::read_block_string(reader)?;
287        // Block 3: description
288        let description = Self::read_block_string(reader)?;
289        // Block 4: schlib_source (absolute path)
290        let source_path = Self::read_block_string(reader)?;
291        // Block 5: empty
292        let _ = Self::read_block_string(reader)?;
293        // Block 6: footprint
294        let footprint = Self::read_block_string(reader)?;
295        // Block 7: pcblib_type
296        let pcblib_type = Self::read_block_string(reader)?;
297        // Block 8: empty
298        let _ = Self::read_block_string(reader)?;
299        // Block 9: pcblib_path (relative)
300        let pcblib_path = Self::read_block_string(reader)?;
301        // Block 10: pcblib_source (absolute path)
302        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    /// Write cross-reference stream.
317    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        // Write entry count
327        data.write_u32::<LittleEndian>(self.cross_refs.len() as u32)?;
328
329        // Write each entry
330        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    /// Write a string block.
349    fn write_block_string<W: Write>(writer: &mut W, s: &str) -> Result<()> {
350        use byteorder::{LittleEndian, WriteBytesExt};
351
352        if s.is_empty() {
353            // Empty block - just size field with value 1
354            writer.write_u32::<LittleEndian>(1)?;
355        } else {
356            // Normal block - size + length + string
357            let bytes = s.as_bytes();
358            let block_size = 1 + bytes.len(); // 1 for length byte + string
359            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    /// Write a single cross-reference entry (11 blocks).
367    fn write_cross_ref_entry<W: Write>(
368        &self,
369        writer: &mut W,
370        entry: &CrossReference,
371    ) -> Result<()> {
372        // Block 0: name
373        Self::write_block_string(writer, &entry.name)?;
374        // Block 1: schlib_path
375        Self::write_block_string(writer, &entry.schlib_path)?;
376        // Block 2: empty
377        Self::write_block_string(writer, "")?;
378        // Block 3: description
379        Self::write_block_string(writer, &entry.description)?;
380        // Block 4: schlib_source
381        Self::write_block_string(writer, &entry.source_path)?;
382        // Block 5: empty
383        Self::write_block_string(writer, "")?;
384        // Block 6: footprint
385        Self::write_block_string(writer, &entry.footprint)?;
386        // Block 7: pcblib_type
387        Self::write_block_string(writer, &entry.pcblib_type)?;
388        // Block 8: empty
389        Self::write_block_string(writer, "")?;
390        // Block 9: pcblib_path
391        Self::write_block_string(writer, &entry.pcblib_path)?;
392        // Block 10: pcblib_source
393        Self::write_block_string(writer, &entry.pcblib_source_path)?;
394
395        Ok(())
396    }
397
398    /// Read the parameters stream.
399    fn read_parameters<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
400        // The stream name has spaces: "Parameters   .bin"
401        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        // Decompress: first byte is 0x02, rest is zlib
421        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    /// Parse parameters data.
430    fn parse_parameters(&mut self, data: &[u8]) -> Result<()> {
431        // Format: length-prefixed parameter blocks separated by some bytes
432        // Each block is like: "Key1=Value1|Key2=Value2|..."
433        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    /// Read a single parameter entry.
446    fn read_parameter_entry<R: Read>(&self, reader: &mut R) -> Result<ComponentParameters> {
447        use byteorder::{LittleEndian, ReadBytesExt};
448
449        // Read length (u16)
450        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        // Parse as pipe-delimited parameters
459        let text = String::from_utf8_lossy(&buf).to_string();
460        let params = ParameterCollection::from_string(&text);
461
462        // Extract component name from Library Reference or Designator
463        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    /// Write parameters stream.
473    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    /// Read the embedded SchLib.
503    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        // Decompress: first byte is 0x02, rest is zlib
524        if data.len() > 1 && data[0] == 0x02 {
525            let decompressed = decompress_zlib(&data[1..])?;
526            // Parse as SchLib CFB
527            let cursor = Cursor::new(decompressed);
528            self.schlib = SchLib::open(cursor)?;
529        }
530
531        Ok(())
532    }
533
534    /// Write the embedded SchLib.
535    fn write_schlib<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
536        // Create SchLib storage
537        cf.create_storage("/SchLib")
538            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
539
540        // Write SchLib to a buffer
541        let mut schlib_buf = Cursor::new(Vec::new());
542        self.schlib.save(&mut schlib_buf)?;
543
544        // Compress and write
545        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    /// Read the embedded PcbLib.
560    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        // Decompress: first byte is 0x02, rest is zlib
581        if data.len() > 1 && data[0] == 0x02 {
582            let decompressed = decompress_zlib(&data[1..])?;
583            // Parse as PcbLib CFB
584            let cursor = Cursor::new(decompressed);
585            self.pcblib = PcbLib::open(cursor)?;
586        }
587
588        Ok(())
589    }
590
591    /// Write the embedded PcbLib.
592    fn write_pcblib<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
593        // Create PcbLib storage
594        cf.create_storage("/PCBLib")
595            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
596
597        // Write PcbLib to a buffer
598        let mut pcblib_buf = Cursor::new(Vec::new());
599        self.pcblib.save(&mut pcblib_buf)?;
600
601        // Compress and write
602        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    /// Get the number of schematic components.
617    pub fn schematic_component_count(&self) -> usize {
618        self.schlib.component_count()
619    }
620
621    /// Get the number of PCB footprints.
622    pub fn footprint_count(&self) -> usize {
623        self.pcblib.component_count()
624    }
625
626    /// Get cross-reference for a component by name.
627    pub fn get_cross_ref(&self, name: &str) -> Option<&CrossReference> {
628        self.cross_refs.iter().find(|r| r.name == name)
629    }
630
631    /// Get parameters for a component by name.
632    pub fn get_parameters(&self, name: &str) -> Option<&ComponentParameters> {
633        self.parameters.iter().find(|p| p.name == name)
634    }
635
636    /// Get a mapping of component names to their footprints.
637    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
645/// Decompress zlib data.
646fn 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
658/// Compress data with zlib.
659fn 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
670// DumpTree implementation
671use 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        // Cross-references summary
683        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        // SchLib section
698        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        // PcbLib section
715        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}