Skip to main content

altium_format/api/generic/
binary_record.rs

1//! BinaryRecord for binary-format PCB records.
2
3use indexmap::IndexMap;
4
5use super::Value;
6use crate::records::pcb::PcbObjectId;
7
8/// A record with dynamic field access for binary format.
9///
10/// Used for PCB records which are stored in binary format rather than
11/// pipe-delimited parameters. Preserves raw bytes for lossless round-tripping.
12#[derive(Debug, Clone)]
13pub struct BinaryRecord {
14    /// Object type ID
15    object_id: PcbObjectId,
16    /// Original raw binary data
17    raw_data: Vec<u8>,
18    /// Parsed fields (if known object type)
19    fields: Option<IndexMap<String, Value>>,
20    /// Whether any fields were modified
21    modified: bool,
22}
23
24impl BinaryRecord {
25    /// Creates a BinaryRecord from raw data and object ID.
26    pub fn from_binary(object_id: PcbObjectId, data: Vec<u8>) -> Self {
27        BinaryRecord {
28            object_id,
29            raw_data: data,
30            fields: None,
31            modified: false,
32        }
33    }
34
35    /// Creates a BinaryRecord with pre-parsed fields.
36    pub fn from_binary_with_fields(
37        object_id: PcbObjectId,
38        data: Vec<u8>,
39        fields: IndexMap<String, Value>,
40    ) -> Self {
41        BinaryRecord {
42            object_id,
43            raw_data: data,
44            fields: Some(fields),
45            modified: false,
46        }
47    }
48
49    /// Returns the object type ID.
50    pub fn object_id(&self) -> PcbObjectId {
51        self.object_id
52    }
53
54    /// Returns a human-readable type name.
55    pub fn type_name(&self) -> &'static str {
56        match self.object_id {
57            PcbObjectId::Arc => "Arc",
58            PcbObjectId::Pad => "Pad",
59            PcbObjectId::Via => "Via",
60            PcbObjectId::Track => "Track",
61            PcbObjectId::Text => "Text",
62            PcbObjectId::Fill => "Fill",
63            PcbObjectId::Region => "Region",
64            PcbObjectId::ComponentBody => "ComponentBody",
65            _ => "Unknown",
66        }
67    }
68
69    /// Returns the raw binary data.
70    pub fn raw_data(&self) -> &[u8] {
71        &self.raw_data
72    }
73
74    /// Returns true if fields have been parsed.
75    pub fn has_fields(&self) -> bool {
76        self.fields.is_some()
77    }
78
79    /// Gets a parsed field value.
80    pub fn get(&self, key: &str) -> Option<&Value> {
81        self.fields.as_ref()?.get(&key.to_uppercase())
82    }
83
84    /// Gets a field as an integer.
85    pub fn get_int(&self, key: &str) -> Option<i64> {
86        self.get(key).and_then(|v| v.as_int())
87    }
88
89    /// Gets a field as a float.
90    pub fn get_float(&self, key: &str) -> Option<f64> {
91        self.get(key).and_then(|v| v.as_float())
92    }
93
94    /// Gets a field as a coordinate.
95    pub fn get_coord(&self, key: &str) -> Option<crate::types::Coord> {
96        self.get(key).and_then(|v| v.as_coord())
97    }
98
99    /// Iterates over parsed fields.
100    pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
101        self.fields
102            .iter()
103            .flat_map(|f| f.iter())
104            .map(|(k, v)| (k.as_str(), v))
105    }
106
107    /// Returns true if the record was modified.
108    pub fn is_modified(&self) -> bool {
109        self.modified
110    }
111
112    /// Sets a field value.
113    pub fn set(&mut self, key: &str, value: impl Into<Value>) {
114        let fields = self.fields.get_or_insert_with(IndexMap::new);
115        fields.insert(key.to_uppercase(), value.into());
116        self.modified = true;
117    }
118
119    /// Converts back to binary data.
120    ///
121    /// If the record was not modified, returns the original raw data.
122    /// If modified, returns the original data (typed serialization requires
123    /// using the typed API).
124    pub fn to_binary(&self) -> Vec<u8> {
125        // For now, always return original data
126        // Full binary reconstruction would require type-specific serialization
127        self.raw_data.clone()
128    }
129
130    /// Returns the size of the binary data.
131    pub fn size(&self) -> usize {
132        self.raw_data.len()
133    }
134}
135
136impl Default for BinaryRecord {
137    fn default() -> Self {
138        BinaryRecord {
139            object_id: PcbObjectId::None,
140            raw_data: Vec::new(),
141            fields: None,
142            modified: false,
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_from_binary() {
153        let data = vec![0x01, 0x02, 0x03, 0x04];
154        let record = BinaryRecord::from_binary(PcbObjectId::Track, data.clone());
155
156        assert_eq!(record.object_id(), PcbObjectId::Track);
157        assert_eq!(record.raw_data(), &data);
158        assert!(!record.has_fields());
159    }
160
161    #[test]
162    fn test_with_fields() {
163        let mut fields = IndexMap::new();
164        fields.insert("WIDTH".to_string(), Value::Int(1000));
165
166        let record = BinaryRecord::from_binary_with_fields(PcbObjectId::Track, vec![], fields);
167
168        assert!(record.has_fields());
169        assert_eq!(record.get_int("WIDTH"), Some(1000));
170    }
171
172    #[test]
173    fn test_modification() {
174        let mut record = BinaryRecord::from_binary(PcbObjectId::Track, vec![]);
175        assert!(!record.is_modified());
176
177        record.set("WIDTH", 2000i64);
178        assert!(record.is_modified());
179    }
180}