Skip to main content

altium_format/io/
reader.rs

1//! Base reader utilities for Altium CFB files.
2//!
3//! Provides common reading operations for compound file binary format.
4
5use byteorder::{LittleEndian, ReadBytesExt};
6use encoding_rs::WINDOWS_1252;
7use flate2::read::ZlibDecoder;
8use std::io::{Cursor, Read, Seek, SeekFrom};
9
10use crate::error::{AltiumError, Result};
11use crate::format::{CFB_COMPRESSED_TAG, SIZE_FLAG_MASK};
12use crate::types::{Coord, CoordPoint, ParameterCollection};
13
14/// Reads a size-prefixed block of data.
15///
16/// The block starts with an i32 size header, followed by that many bytes.
17pub fn read_block<R: Read>(reader: &mut R) -> Result<Vec<u8>> {
18    let size = reader.read_i32::<LittleEndian>()?;
19    // High byte contains flags; extract 24-bit size
20    let clean_size = (size & SIZE_FLAG_MASK as i32) as usize;
21
22    if clean_size == 0 {
23        return Ok(Vec::new());
24    }
25
26    let mut buffer = vec![0u8; clean_size];
27    reader.read_exact(&mut buffer)?;
28    Ok(buffer)
29}
30
31/// Reads a size-prefixed block and interprets it using a callback.
32pub fn read_block_with<R: Read, T, F>(reader: &mut R, interpreter: F) -> Result<T>
33where
34    F: FnOnce(&[u8]) -> Result<T>,
35{
36    let data = read_block(reader)?;
37    interpreter(&data)
38}
39
40/// Decompresses zlib-compressed data.
41///
42/// Skips the 2-byte zlib header before decompressing.
43pub fn decompress_zlib(data: &[u8]) -> Result<Vec<u8>> {
44    if data.len() < 2 {
45        return Err(AltiumError::Decompression(
46            "Data too short for zlib".to_string(),
47        ));
48    }
49
50    // Skip 2-byte zlib header
51    let mut decoder = ZlibDecoder::new(&data[2..]);
52    let mut output = Vec::new();
53    decoder
54        .read_to_end(&mut output)
55        .map_err(|e| AltiumError::Decompression(format!("Zlib decompression failed: {}", e)))?;
56    Ok(output)
57}
58
59/// Decodes a byte slice from Windows-1252 encoding to a String.
60pub fn decode_windows_1252(data: &[u8]) -> String {
61    let (cow, _, _) = WINDOWS_1252.decode(data);
62    cow.into_owned()
63}
64
65/// Reads a raw string of known length (no length prefix, no null terminator).
66pub fn read_raw_string<R: Read>(reader: &mut R, size: usize) -> Result<String> {
67    if size == 0 {
68        return Ok(String::new());
69    }
70    let mut buffer = vec![0u8; size];
71    reader.read_exact(&mut buffer)?;
72    Ok(decode_windows_1252(&buffer))
73}
74
75/// Reads a C-style null-terminated string of known buffer size.
76///
77/// The buffer is `size` bytes, but the string ends at the first null byte.
78pub fn read_c_string<R: Read>(reader: &mut R, size: usize) -> Result<String> {
79    if size == 0 {
80        return Ok(String::new());
81    }
82    let mut buffer = vec![0u8; size];
83    reader.read_exact(&mut buffer)?;
84
85    // Find null terminator
86    let end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
87    Ok(decode_windows_1252(&buffer[..end]))
88}
89
90/// Reads a Pascal-style string (i32 size prefix, null-terminated content).
91pub fn read_pascal_string<R: Read>(reader: &mut R) -> Result<String> {
92    let data = read_block(reader)?;
93    if data.is_empty() {
94        return Ok(String::new());
95    }
96    // Remove null terminator if present
97    let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
98    Ok(decode_windows_1252(&data[..end]))
99}
100
101/// Reads a Pascal short string (byte size prefix, no null terminator).
102pub fn read_pascal_short_string<R: Read>(reader: &mut R) -> Result<String> {
103    let size = reader.read_u8()? as usize;
104    read_raw_string(reader, size)
105}
106
107/// Reads a fixed-length UTF-16 font name (32 bytes, null-terminated UTF-16).
108pub fn read_font_name<R: Read + Seek>(reader: &mut R) -> Result<String> {
109    let start_pos = reader.stream_position()?;
110    let mut chars = Vec::new();
111
112    for _ in 0..16 {
113        let code = reader.read_u16::<LittleEndian>()?;
114        if code == 0 {
115            break;
116        }
117        chars.push(code);
118    }
119
120    // Always read exactly 32 bytes total
121    reader.seek(SeekFrom::Start(start_pos + 32))?;
122
123    String::from_utf16(&chars)
124        .map_err(|e| AltiumError::Encoding(format!("Invalid UTF-16 font name: {}", e)))
125}
126
127/// Reads a string block (i32 size prefix, then byte size prefix for content).
128pub fn read_string_block<R: Read>(reader: &mut R) -> Result<String> {
129    let data = read_block(reader)?;
130    if data.is_empty() {
131        return Ok(String::new());
132    }
133    let mut cursor = Cursor::new(data);
134    read_pascal_short_string(&mut cursor)
135}
136
137/// Reads parameters from a sized block.
138///
139/// The block contains a null-terminated parameter string.
140pub fn read_parameters<R: Read>(
141    reader: &mut R,
142    size: usize,
143    raw: bool,
144) -> Result<ParameterCollection> {
145    let data = if raw {
146        read_raw_string(reader, size)?
147    } else {
148        read_c_string(reader, size)?
149    };
150    Ok(ParameterCollection::from_string(&data))
151}
152
153/// Reads parameters from a block (i32 size prefix).
154pub fn read_parameters_block<R: Read>(reader: &mut R) -> Result<ParameterCollection> {
155    let data = read_block(reader)?;
156    if data.is_empty() {
157        return Ok(ParameterCollection::new());
158    }
159    // Remove null terminator
160    let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
161    let s = decode_windows_1252(&data[..end]);
162    Ok(ParameterCollection::from_string(&s))
163}
164
165/// Reads a CoordPoint (two i32 values for x, y).
166pub fn read_coord_point<R: Read>(reader: &mut R) -> Result<CoordPoint> {
167    let x = reader.read_i32::<LittleEndian>()?;
168    let y = reader.read_i32::<LittleEndian>()?;
169    Ok(CoordPoint::from_raw(x, y))
170}
171
172/// Reads the header stream (u32 data size).
173pub fn read_header<R: Read>(reader: &mut R) -> Result<u32> {
174    Ok(reader.read_u32::<LittleEndian>()?)
175}
176
177/// Reads compressed storage: (id, data) pair with zlib compression.
178pub fn read_compressed_storage<R: Read>(reader: &mut R) -> Result<(String, Vec<u8>)> {
179    let block_data = read_block(reader)?;
180    if block_data.is_empty() {
181        return Ok((String::new(), Vec::new()));
182    }
183
184    let mut cursor = Cursor::new(block_data);
185
186    // Altium format uses 0xD0 tag to mark zlib-compressed streams
187    let tag = cursor.read_u8()?;
188    if tag != CFB_COMPRESSED_TAG {
189        return Err(AltiumError::Parse(
190            "Expected 0xD0 tag in compressed storage".to_string(),
191        ));
192    }
193
194    // Read ID as Pascal short string
195    let id = read_pascal_short_string(&mut cursor)?;
196
197    // Read compressed data block
198    let compressed = read_block(&mut cursor)?;
199
200    // Decompress
201    let data = decompress_zlib(&compressed)?;
202
203    Ok((id, data))
204}
205
206/// Extension trait for convenient reading operations.
207pub trait ReadExt: Read {
208    /// Reads a Coord value (i32).
209    fn read_coord(&mut self) -> Result<Coord>
210    where
211        Self: Sized,
212    {
213        let value = self.read_i32::<LittleEndian>()?;
214        Ok(Coord::from_raw(value))
215    }
216
217    /// Reads a boolean byte.
218    fn read_bool8(&mut self) -> Result<bool>
219    where
220        Self: Sized,
221    {
222        let value = self.read_u8()?;
223        Ok(value != 0)
224    }
225}
226
227impl<R: Read> ReadExt for R {}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use std::io::Cursor;
233
234    #[test]
235    fn test_read_block() {
236        // 4-byte size (5) + 5 bytes of data = "hello"
237        let data = [5, 0, 0, 0, b'h', b'e', b'l', b'l', b'o'];
238        let mut cursor = Cursor::new(&data);
239
240        let result = read_block(&mut cursor).unwrap();
241        assert_eq!(result, b"hello");
242    }
243
244    #[test]
245    fn test_read_pascal_short_string() {
246        // 1-byte size (5) + "hello"
247        let data = [5, b'h', b'e', b'l', b'l', b'o'];
248        let mut cursor = Cursor::new(&data);
249
250        let result = read_pascal_short_string(&mut cursor).unwrap();
251        assert_eq!(result, "hello");
252    }
253
254    #[test]
255    fn test_read_coord_point() {
256        // Two i32 values
257        let data = [0, 0, 1, 0, 0, 0, 2, 0]; // 65536, 131072
258        let mut cursor = Cursor::new(&data);
259
260        let point = read_coord_point(&mut cursor).unwrap();
261        assert_eq!(point.x.to_raw(), 65536);
262        assert_eq!(point.y.to_raw(), 131072);
263    }
264
265    #[test]
266    fn test_decode_windows_1252() {
267        let data = b"Hello World";
268        let result = decode_windows_1252(data);
269        assert_eq!(result, "Hello World");
270    }
271}