Skip to main content

altium_format/io/
writer.rs

1//! Base writer utilities for Altium CFB files.
2//!
3//! Provides common writing operations for compound file binary format.
4
5use std::io::Write;
6
7#[cfg(test)]
8use std::io::Cursor;
9
10use byteorder::{LittleEndian, WriteBytesExt};
11use encoding_rs::WINDOWS_1252;
12use flate2::Compression;
13use flate2::write::ZlibEncoder;
14
15use crate::error::Result;
16use crate::types::{Coord, CoordPoint, ParameterCollection};
17
18/// Writes a size-prefixed block of data.
19///
20/// The block starts with an i32 size header (with optional flags in high byte),
21/// followed by that many bytes.
22pub fn write_block<W: Write>(writer: &mut W, data: &[u8], flags: u8) -> Result<()> {
23    let size = data.len() as i32;
24    let header = ((flags as i32) << 24) | size;
25    writer.write_i32::<LittleEndian>(header)?;
26    if !data.is_empty() {
27        writer.write_all(data)?;
28    }
29    Ok(())
30}
31
32/// Writes a size-prefixed block using a serializer function.
33///
34/// The serializer writes to an internal buffer, then the block is written
35/// with the computed size.
36pub fn write_block_with<W: Write, F>(writer: &mut W, serializer: F, flags: u8) -> Result<()>
37where
38    F: FnOnce(&mut Vec<u8>) -> Result<()>,
39{
40    let mut buffer = Vec::new();
41    serializer(&mut buffer)?;
42    write_block(writer, &buffer, flags)
43}
44
45/// Compresses data using zlib format.
46///
47/// Includes the 2-byte zlib header (0x78 0x9C for default compression).
48pub fn compress_zlib(data: &[u8]) -> Result<Vec<u8>> {
49    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
50    encoder.write_all(data)?;
51    let compressed = encoder.finish()?;
52    Ok(compressed)
53}
54
55/// Encodes a string to Windows-1252 bytes.
56pub fn encode_windows_1252(s: &str) -> Vec<u8> {
57    let (bytes, _, _) = WINDOWS_1252.encode(s);
58    bytes.into_owned()
59}
60
61/// Writes a raw string (no length prefix, no null terminator).
62pub fn write_raw_string<W: Write>(writer: &mut W, s: &str) -> Result<()> {
63    let bytes = encode_windows_1252(s);
64    writer.write_all(&bytes)?;
65    Ok(())
66}
67
68/// Writes a C-style null-terminated string.
69pub fn write_c_string<W: Write>(writer: &mut W, s: &str) -> Result<()> {
70    write_raw_string(writer, s)?;
71    writer.write_u8(0)?;
72    Ok(())
73}
74
75/// Writes a Pascal-style string (i32 size prefix, null-terminated content).
76pub fn write_pascal_string<W: Write>(writer: &mut W, s: &str) -> Result<()> {
77    let mut buffer = Vec::new();
78    write_c_string(&mut buffer, s)?;
79    write_block(writer, &buffer, 0)
80}
81
82/// Writes a Pascal short string (byte size prefix, no null terminator).
83pub fn write_pascal_short_string<W: Write>(writer: &mut W, s: &str) -> Result<()> {
84    let bytes = encode_windows_1252(s);
85    if bytes.len() > 255 {
86        // Truncate to 255 bytes max
87        writer.write_u8(255)?;
88        writer.write_all(&bytes[..255])?;
89    } else {
90        writer.write_u8(bytes.len() as u8)?;
91        writer.write_all(&bytes)?;
92    }
93    Ok(())
94}
95
96/// Writes a fixed-length UTF-16 font name (32 bytes, null-terminated UTF-16).
97///
98/// Format: 32 bytes (16 UTF-16 LE code units)
99/// - Strings up to 15 characters: written with null terminator, then zero-padded
100/// - Strings exactly 16 characters: written without null terminator (uses all 32 bytes)
101/// - Strings longer than 16 characters: truncated to 16 characters, no null terminator
102///
103/// This ensures round-trip integrity for font names of any length found in files.
104pub fn write_font_name<W: Write>(writer: &mut W, s: &str) -> Result<()> {
105    // Convert to UTF-16 and take at most 16 code units (to fit in 32 bytes)
106    let utf16: Vec<u16> = s.encode_utf16().take(16).collect();
107
108    // Write UTF-16 code units
109    for &code in &utf16 {
110        writer.write_u16::<LittleEndian>(code)?;
111    }
112
113    // Calculate bytes written
114    let written = utf16.len() * 2;
115
116    // If less than 16 code units, write null terminator and pad
117    if utf16.len() < 16 {
118        writer.write_u16::<LittleEndian>(0)?; // Null terminator
119        let with_null = written + 2;
120
121        // Pad remaining bytes to reach 32 total
122        for _ in with_null..32 {
123            writer.write_u8(0)?;
124        }
125    }
126    // If exactly 16 code units, no padding needed (uses all 32 bytes)
127
128    Ok(())
129}
130
131/// Writes a string block (i32 size prefix, then byte size prefix for content).
132pub fn write_string_block<W: Write>(writer: &mut W, s: &str) -> Result<()> {
133    let mut buffer = Vec::new();
134    write_pascal_short_string(&mut buffer, s)?;
135    write_block(writer, &buffer, 0)
136}
137
138/// Writes parameters as a C-string (null-terminated).
139pub fn write_parameters<W: Write>(writer: &mut W, params: &ParameterCollection) -> Result<()> {
140    let s = params.to_string();
141    write_c_string(writer, &s)
142}
143
144/// Writes parameters as a raw string (no null terminator).
145pub fn write_parameters_raw<W: Write>(writer: &mut W, params: &ParameterCollection) -> Result<()> {
146    let s = params.to_string();
147    write_raw_string(writer, &s)
148}
149
150/// Writes parameters in a block (i32 size prefix, null-terminated content).
151pub fn write_parameters_block<W: Write>(
152    writer: &mut W,
153    params: &ParameterCollection,
154) -> Result<()> {
155    let mut buffer = Vec::new();
156    write_parameters(&mut buffer, params)?;
157    write_block(writer, &buffer, 0)
158}
159
160/// Writes a CoordPoint (two i32 values for x, y).
161pub fn write_coord_point<W: Write>(writer: &mut W, point: CoordPoint) -> Result<()> {
162    writer.write_i32::<LittleEndian>(point.x.to_raw())?;
163    writer.write_i32::<LittleEndian>(point.y.to_raw())?;
164    Ok(())
165}
166
167/// Writes the header stream (u32 record count).
168pub fn write_header<W: Write>(writer: &mut W, record_count: u32) -> Result<()> {
169    writer.write_u32::<LittleEndian>(record_count)?;
170    Ok(())
171}
172
173/// Writes compressed storage: (id, data) pair with zlib compression.
174///
175/// Format: block containing [0xD0 tag, Pascal short string id, compressed data block]
176pub fn write_compressed_storage<W: Write>(writer: &mut W, id: &str, data: &[u8]) -> Result<()> {
177    write_block_with(
178        writer,
179        |buffer| {
180            // Write 0xD0 tag
181            buffer.write_u8(0xD0)?;
182
183            // Write ID as Pascal short string
184            write_pascal_short_string(buffer, id)?;
185
186            // Compress and write data
187            let compressed = compress_zlib(data)?;
188            write_block(buffer, &compressed, 0)?;
189
190            Ok(())
191        },
192        0x01,
193    ) // Compressed blocks have 0x01 flag
194}
195
196/// Writes compressed storage with a serializer function.
197pub fn write_compressed_storage_with<W: Write, F>(
198    writer: &mut W,
199    id: &str,
200    serializer: F,
201) -> Result<()>
202where
203    F: FnOnce(&mut Vec<u8>) -> Result<()>,
204{
205    let mut data = Vec::new();
206    serializer(&mut data)?;
207    write_compressed_storage(writer, id, &data)
208}
209
210/// Extension trait for convenient writing operations.
211pub trait WriteExt: Write {
212    /// Writes a Coord value (i32).
213    fn write_coord(&mut self, value: Coord) -> Result<()>
214    where
215        Self: Sized,
216    {
217        self.write_i32::<LittleEndian>(value.to_raw())?;
218        Ok(())
219    }
220
221    /// Writes a boolean byte.
222    fn write_bool8(&mut self, value: bool) -> Result<()>
223    where
224        Self: Sized,
225    {
226        self.write_u8(if value { 1 } else { 0 })?;
227        Ok(())
228    }
229}
230
231impl<W: Write> WriteExt for W {}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_write_block() {
239        let mut buffer = Vec::new();
240        write_block(&mut buffer, b"hello", 0).unwrap();
241
242        // 4-byte size (5) + 5 bytes of data
243        assert_eq!(buffer, [5, 0, 0, 0, b'h', b'e', b'l', b'l', b'o']);
244    }
245
246    #[test]
247    fn test_write_block_with_flags() {
248        let mut buffer = Vec::new();
249        write_block(&mut buffer, b"test", 0x01).unwrap();
250
251        // Flag in high byte: 0x01000004
252        assert_eq!(buffer[0], 4);
253        assert_eq!(buffer[3], 0x01);
254    }
255
256    #[test]
257    fn test_write_pascal_short_string() {
258        let mut buffer = Vec::new();
259        write_pascal_short_string(&mut buffer, "hello").unwrap();
260
261        // 1-byte size (5) + "hello"
262        assert_eq!(buffer, [5, b'h', b'e', b'l', b'l', b'o']);
263    }
264
265    #[test]
266    fn test_write_c_string() {
267        let mut buffer = Vec::new();
268        write_c_string(&mut buffer, "test").unwrap();
269
270        assert_eq!(buffer, [b't', b'e', b's', b't', 0]);
271    }
272
273    #[test]
274    fn test_write_coord_point() {
275        let mut buffer = Vec::new();
276        let point = CoordPoint::from_raw(65536, 131072);
277        write_coord_point(&mut buffer, point).unwrap();
278
279        // Two i32 values in little-endian
280        assert_eq!(buffer.len(), 8);
281        let mut cursor = Cursor::new(&buffer);
282        use byteorder::ReadBytesExt;
283        assert_eq!(cursor.read_i32::<LittleEndian>().unwrap(), 65536);
284        assert_eq!(cursor.read_i32::<LittleEndian>().unwrap(), 131072);
285    }
286
287    #[test]
288    fn test_encode_windows_1252() {
289        let bytes = encode_windows_1252("Hello World");
290        assert_eq!(bytes, b"Hello World");
291    }
292
293    #[test]
294    fn test_write_font_name_empty() {
295        let mut buffer = Vec::new();
296        write_font_name(&mut buffer, "").unwrap();
297
298        // Should be: null terminator (2 bytes) + 30 bytes of padding = 32 bytes
299        assert_eq!(buffer.len(), 32);
300        assert_eq!(&buffer[0..2], &[0, 0]); // Null terminator
301        assert!(buffer[2..].iter().all(|&b| b == 0)); // All padding
302    }
303
304    #[test]
305    fn test_write_font_name_short() {
306        let mut buffer = Vec::new();
307        write_font_name(&mut buffer, "Arial").unwrap();
308
309        // Should be: "Arial" (5 chars * 2 bytes = 10 bytes) + null (2) + padding (20) = 32
310        assert_eq!(buffer.len(), 32);
311
312        // Check the string content
313        let expected_utf16: Vec<u16> = "Arial".encode_utf16().collect();
314        for (i, &code) in expected_utf16.iter().enumerate() {
315            let offset = i * 2;
316            let actual = u16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
317            assert_eq!(actual, code);
318        }
319
320        // Check null terminator
321        let null_offset = 5 * 2;
322        assert_eq!(buffer[null_offset], 0);
323        assert_eq!(buffer[null_offset + 1], 0);
324
325        // Check padding
326        assert!(buffer[null_offset + 2..].iter().all(|&b| b == 0));
327    }
328
329    #[test]
330    fn test_write_font_name_15_chars() {
331        let mut buffer = Vec::new();
332        let name = "Times New Roman"; // Exactly 15 characters
333        write_font_name(&mut buffer, name).unwrap();
334
335        // Should be: 15 chars * 2 bytes = 30 bytes + null (2) = 32 bytes
336        assert_eq!(buffer.len(), 32);
337
338        // Verify content
339        let utf16: Vec<u16> = name.encode_utf16().collect();
340        assert_eq!(utf16.len(), 15);
341
342        for (i, &code) in utf16.iter().enumerate() {
343            let offset = i * 2;
344            let actual = u16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
345            assert_eq!(actual, code);
346        }
347
348        // Verify null terminator at position 30
349        assert_eq!(buffer[30], 0);
350        assert_eq!(buffer[31], 0);
351    }
352
353    #[test]
354    fn test_write_font_name_16_chars() {
355        let mut buffer = Vec::new();
356        let name = "1234567890ABCDEF"; // Exactly 16 characters
357        write_font_name(&mut buffer, name).unwrap();
358
359        // Should use all 32 bytes (16 chars * 2 bytes), no null terminator
360        assert_eq!(buffer.len(), 32);
361
362        // Verify content
363        let utf16: Vec<u16> = name.encode_utf16().collect();
364        assert_eq!(utf16.len(), 16);
365
366        for (i, &code) in utf16.iter().enumerate() {
367            let offset = i * 2;
368            let actual = u16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
369            assert_eq!(actual, code);
370        }
371
372        // No null terminator or padding since all 32 bytes are used
373    }
374
375    #[test]
376    fn test_write_font_name_too_long() {
377        let mut buffer = Vec::new();
378        let name = "ThisIsAVeryLongFontNameThatExceeds16Characters"; // > 16 characters
379        write_font_name(&mut buffer, name).unwrap();
380
381        // Should be truncated to 16 characters (32 bytes), no null
382        assert_eq!(buffer.len(), 32);
383
384        // Verify only first 16 characters are written
385        let utf16: Vec<u16> = name.encode_utf16().take(16).collect();
386        assert_eq!(utf16.len(), 16);
387
388        for (i, &code) in utf16.iter().enumerate() {
389            let offset = i * 2;
390            let actual = u16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
391            assert_eq!(actual, code);
392        }
393    }
394
395    #[test]
396    fn test_write_font_name_unicode() {
397        let mut buffer = Vec::new();
398        write_font_name(&mut buffer, "微软雅黑").unwrap(); // Chinese font name
399
400        assert_eq!(buffer.len(), 32);
401
402        // Verify UTF-16 encoding
403        let utf16: Vec<u16> = "微软雅黑".encode_utf16().collect();
404        for (i, &code) in utf16.iter().enumerate() {
405            let offset = i * 2;
406            let actual = u16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
407            assert_eq!(actual, code);
408        }
409
410        // Should have null terminator since < 16 chars
411        let null_offset = utf16.len() * 2;
412        assert_eq!(buffer[null_offset], 0);
413        assert_eq!(buffer[null_offset + 1], 0);
414    }
415}