lnmp_codec/binary/
frame.rs

1//! Binary frame structure for LNMP v0.4 protocol.
2//!
3//! A BinaryFrame represents a complete LNMP record in binary format:
4//! ```text
5//! ┌─────────┬─────────┬─────────────┬──────────────────────┐
6//! │ VERSION │  FLAGS  │ ENTRY_COUNT │      ENTRIES...      │
7//! │ (1 byte)│(1 byte) │  (VarInt)   │     (variable)       │
8//! └─────────┴─────────┴─────────────┴──────────────────────┘
9//! ```
10
11use super::entry::BinaryEntry;
12use super::error::BinaryError;
13use super::varint;
14use lnmp_core::{LnmpField, LnmpRecord};
15
16/// Protocol version for LNMP v0.4 binary format
17const VERSION_0_4: u8 = 0x04;
18
19/// Binary frame representing a complete LNMP record
20#[derive(Debug, Clone, PartialEq)]
21pub struct BinaryFrame {
22    /// Protocol version byte (0x04 for v0.4)
23    version: u8,
24    /// Flags byte (reserved for future use, 0x00 in v0.4)
25    flags: u8,
26    /// Entries in the frame
27    entries: Vec<BinaryEntry>,
28}
29
30impl BinaryFrame {
31    /// Creates a new frame with version 0x04 and flags 0x00
32    ///
33    /// # Arguments
34    ///
35    /// * `entries` - Vector of binary entries (should be sorted by FID for canonical form)
36    pub fn new(entries: Vec<BinaryEntry>) -> Self {
37        Self {
38            version: VERSION_0_4,
39            flags: 0x00,
40            entries,
41        }
42    }
43
44    /// Encodes the frame to bytes
45    ///
46    /// Binary layout:
47    /// - VERSION (1 byte): 0x04
48    /// - FLAGS (1 byte): 0x00
49    /// - ENTRY_COUNT (VarInt): Number of entries
50    /// - ENTRIES: Each entry encoded sequentially
51    pub fn encode(&self) -> Vec<u8> {
52        let mut bytes = Vec::new();
53
54        // Write VERSION
55        bytes.push(self.version);
56
57        // Write FLAGS
58        bytes.push(self.flags);
59
60        // Write ENTRY_COUNT as VarInt
61        bytes.extend_from_slice(&varint::encode(self.entries.len() as i64));
62
63        // Write each entry
64        for entry in &self.entries {
65            bytes.extend_from_slice(&entry.encode());
66        }
67
68        bytes
69    }
70
71    /// Decodes a frame from bytes
72    ///
73    /// # Errors
74    ///
75    /// Returns errors for:
76    /// - `UnexpectedEof`: Insufficient data
77    /// - `UnsupportedVersion`: Version byte is not 0x04
78    /// - `InvalidVarInt`: Malformed entry count
79    /// - Entry decoding errors
80    pub fn decode(bytes: &[u8]) -> Result<Self, BinaryError> {
81        Self::decode_with_options(bytes, true)
82    }
83
84    /// Decodes binary frame without enforcing canonical FID ordering.
85    pub fn decode_allow_unsorted(bytes: &[u8]) -> Result<Self, BinaryError> {
86        Self::decode_with_options(bytes, false)
87    }
88
89    fn decode_with_options(bytes: &[u8], enforce_sorted: bool) -> Result<Self, BinaryError> {
90        let mut offset = 0;
91
92        // Read VERSION (1 byte)
93        if bytes.is_empty() {
94            return Err(BinaryError::UnexpectedEof {
95                expected: 1,
96                found: bytes.len(),
97            });
98        }
99        let version = bytes[offset];
100        offset += 1;
101
102        // Validate version
103        if version != VERSION_0_4 {
104            return Err(BinaryError::UnsupportedVersion {
105                found: version,
106                supported: vec![VERSION_0_4],
107            });
108        }
109
110        // Read FLAGS (1 byte)
111        if bytes.len() < offset + 1 {
112            return Err(BinaryError::UnexpectedEof {
113                expected: offset + 1,
114                found: bytes.len(),
115            });
116        }
117        let flags = bytes[offset];
118        offset += 1;
119
120        // Decode ENTRY_COUNT (VarInt)
121        let (entry_count, consumed) =
122            varint::decode(&bytes[offset..]).map_err(|_| BinaryError::InvalidVarInt {
123                reason: "Invalid entry count VarInt".to_string(),
124            })?;
125        offset += consumed;
126
127        if entry_count < 0 {
128            return Err(BinaryError::InvalidValue {
129                field_id: 0,
130                type_tag: 0,
131                reason: format!("Negative entry count: {}", entry_count),
132            });
133        }
134
135        let entry_count = entry_count as usize;
136        let mut entries = Vec::with_capacity(entry_count);
137
138        // Decode each entry
139        for _ in 0..entry_count {
140            let (entry, consumed) = BinaryEntry::decode(&bytes[offset..])?;
141            offset += consumed;
142            entries.push(entry);
143        }
144
145        if enforce_sorted {
146            let mut prev_fid: Option<u16> = None;
147            for entry in &entries {
148                if let Some(prev) = prev_fid {
149                    if entry.fid < prev {
150                        return Err(BinaryError::CanonicalViolation {
151                            reason: format!(
152                                "entries must be sorted by FID (saw {} after {})",
153                                entry.fid, prev
154                            ),
155                        });
156                    }
157                }
158                prev_fid = Some(entry.fid);
159            }
160        }
161
162        Ok(Self {
163            version,
164            flags,
165            entries,
166        })
167    }
168
169    /// Converts to LnmpRecord
170    pub fn to_record(&self) -> LnmpRecord {
171        let fields: Vec<LnmpField> = self.entries.iter().map(|entry| entry.to_field()).collect();
172
173        LnmpRecord::from_sorted_fields(fields)
174    }
175
176    /// Creates from LnmpRecord, sorting fields by FID
177    ///
178    /// # Errors
179    ///
180    /// Returns `BinaryError::InvalidValue` if any field contains nested structures
181    /// (not supported in v0.4 binary format)
182    pub fn from_record(record: &LnmpRecord) -> Result<Self, BinaryError> {
183        // Get fields sorted by FID for canonical form
184        let sorted_fields = record.sorted_fields();
185
186        // Convert each field to BinaryEntry
187        let mut entries = Vec::with_capacity(sorted_fields.len());
188        for field in sorted_fields {
189            entries.push(BinaryEntry::from_field(&field)?);
190        }
191
192        Ok(Self::new(entries))
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    #![allow(clippy::approx_constant)]
199
200    use super::super::types::{BinaryValue, TypeTag};
201    use super::*;
202    use lnmp_core::LnmpValue;
203
204    #[test]
205    fn test_new_frame() {
206        let entries = vec![BinaryEntry {
207            fid: 1,
208            tag: TypeTag::Int,
209            value: BinaryValue::Int(42),
210        }];
211
212        let frame = BinaryFrame::new(entries.clone());
213        assert_eq!(frame.version, VERSION_0_4);
214        assert_eq!(frame.flags, 0x00);
215        assert_eq!(frame.entries, entries);
216    }
217
218    #[test]
219    fn test_encode_empty_frame() {
220        let frame = BinaryFrame::new(vec![]);
221        let bytes = frame.encode();
222
223        // VERSION
224        assert_eq!(bytes[0], 0x04);
225        // FLAGS
226        assert_eq!(bytes[1], 0x00);
227        // ENTRY_COUNT (0 as VarInt)
228        assert_eq!(bytes[2], 0x00);
229        assert_eq!(bytes.len(), 3);
230    }
231
232    #[test]
233    fn test_encode_single_entry() {
234        let entries = vec![BinaryEntry {
235            fid: 7,
236            tag: TypeTag::Bool,
237            value: BinaryValue::Bool(true),
238        }];
239
240        let frame = BinaryFrame::new(entries);
241        let bytes = frame.encode();
242
243        // VERSION
244        assert_eq!(bytes[0], 0x04);
245        // FLAGS
246        assert_eq!(bytes[1], 0x00);
247        // ENTRY_COUNT (1 as VarInt)
248        assert_eq!(bytes[2], 0x01);
249        // Entry data follows
250        assert!(bytes.len() > 3);
251    }
252
253    #[test]
254    fn test_encode_multiple_entries() {
255        let entries = vec![
256            BinaryEntry {
257                fid: 7,
258                tag: TypeTag::Bool,
259                value: BinaryValue::Bool(true),
260            },
261            BinaryEntry {
262                fid: 12,
263                tag: TypeTag::Int,
264                value: BinaryValue::Int(14532),
265            },
266            BinaryEntry {
267                fid: 23,
268                tag: TypeTag::StringArray,
269                value: BinaryValue::StringArray(vec!["admin".to_string(), "dev".to_string()]),
270            },
271        ];
272
273        let frame = BinaryFrame::new(entries);
274        let bytes = frame.encode();
275
276        // VERSION
277        assert_eq!(bytes[0], 0x04);
278        // FLAGS
279        assert_eq!(bytes[1], 0x00);
280        // ENTRY_COUNT (3 as VarInt)
281        assert_eq!(bytes[2], 0x03);
282    }
283
284    #[test]
285    fn test_decode_empty_frame() {
286        let frame = BinaryFrame::new(vec![]);
287        let bytes = frame.encode();
288        let decoded = BinaryFrame::decode(&bytes).unwrap();
289
290        assert_eq!(decoded, frame);
291    }
292
293    #[test]
294    fn test_decode_single_entry() {
295        let entries = vec![BinaryEntry {
296            fid: 1,
297            tag: TypeTag::String,
298            value: BinaryValue::String("hello".to_string()),
299        }];
300
301        let frame = BinaryFrame::new(entries);
302        let bytes = frame.encode();
303        let decoded = BinaryFrame::decode(&bytes).unwrap();
304
305        assert_eq!(decoded, frame);
306    }
307
308    #[test]
309    fn test_decode_multiple_entries() {
310        let entries = vec![
311            BinaryEntry {
312                fid: 1,
313                tag: TypeTag::Int,
314                value: BinaryValue::Int(-42),
315            },
316            BinaryEntry {
317                fid: 2,
318                tag: TypeTag::Float,
319                value: BinaryValue::Float(3.14),
320            },
321            BinaryEntry {
322                fid: 3,
323                tag: TypeTag::Bool,
324                value: BinaryValue::Bool(false),
325            },
326        ];
327
328        let frame = BinaryFrame::new(entries);
329        let bytes = frame.encode();
330        let decoded = BinaryFrame::decode(&bytes).unwrap();
331
332        assert_eq!(decoded, frame);
333    }
334
335    #[test]
336    fn test_decode_unsupported_version() {
337        let bytes = vec![0x99, 0x00, 0x00]; // Invalid version
338        let result = BinaryFrame::decode(&bytes);
339
340        assert!(matches!(
341            result,
342            Err(BinaryError::UnsupportedVersion { found: 0x99, .. })
343        ));
344    }
345
346    #[test]
347    fn test_decode_insufficient_data_version() {
348        let bytes = vec![];
349        let result = BinaryFrame::decode(&bytes);
350
351        assert!(matches!(result, Err(BinaryError::UnexpectedEof { .. })));
352    }
353
354    #[test]
355    fn test_decode_insufficient_data_flags() {
356        let bytes = vec![0x04]; // Only version, no flags
357        let result = BinaryFrame::decode(&bytes);
358
359        assert!(matches!(result, Err(BinaryError::UnexpectedEof { .. })));
360    }
361
362    #[test]
363    fn test_decode_invalid_entry_count_varint() {
364        let bytes = vec![0x04, 0x00, 0x80]; // Incomplete VarInt
365        let result = BinaryFrame::decode(&bytes);
366
367        assert!(matches!(result, Err(BinaryError::InvalidVarInt { .. })));
368    }
369
370    #[test]
371    fn test_decode_insufficient_entry_data() {
372        let bytes = vec![0x04, 0x00, 0x01]; // Says 1 entry but no entry data
373        let result = BinaryFrame::decode(&bytes);
374
375        assert!(matches!(result, Err(BinaryError::UnexpectedEof { .. })));
376    }
377
378    #[test]
379    fn test_to_record() {
380        let entries = vec![
381            BinaryEntry {
382                fid: 7,
383                tag: TypeTag::Bool,
384                value: BinaryValue::Bool(true),
385            },
386            BinaryEntry {
387                fid: 12,
388                tag: TypeTag::Int,
389                value: BinaryValue::Int(14532),
390            },
391        ];
392
393        let frame = BinaryFrame::new(entries);
394        let record = frame.to_record();
395
396        assert_eq!(record.fields().len(), 2);
397        assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
398        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
399    }
400
401    #[test]
402    fn test_from_record() {
403        let mut record = LnmpRecord::new();
404        record.add_field(LnmpField {
405            fid: 12,
406            value: LnmpValue::Int(14532),
407        });
408        record.add_field(LnmpField {
409            fid: 7,
410            value: LnmpValue::Bool(true),
411        });
412
413        let frame = BinaryFrame::from_record(&record).unwrap();
414
415        // Should be sorted by FID
416        assert_eq!(frame.entries.len(), 2);
417        assert_eq!(frame.entries[0].fid, 7);
418        assert_eq!(frame.entries[1].fid, 12);
419    }
420
421    #[test]
422    fn test_from_record_sorts_fields() {
423        let mut record = LnmpRecord::new();
424        record.add_field(LnmpField {
425            fid: 23,
426            value: LnmpValue::StringArray(vec!["admin".to_string()]),
427        });
428        record.add_field(LnmpField {
429            fid: 7,
430            value: LnmpValue::Bool(true),
431        });
432        record.add_field(LnmpField {
433            fid: 12,
434            value: LnmpValue::Int(14532),
435        });
436
437        let frame = BinaryFrame::from_record(&record).unwrap();
438
439        // Should be sorted by FID: 7, 12, 23
440        assert_eq!(frame.entries.len(), 3);
441        assert_eq!(frame.entries[0].fid, 7);
442        assert_eq!(frame.entries[1].fid, 12);
443        assert_eq!(frame.entries[2].fid, 23);
444    }
445
446    #[test]
447    fn test_roundtrip_record_to_frame_to_record() {
448        let mut record = LnmpRecord::new();
449        record.add_field(LnmpField {
450            fid: 1,
451            value: LnmpValue::Int(-42),
452        });
453        record.add_field(LnmpField {
454            fid: 2,
455            value: LnmpValue::Float(3.14159),
456        });
457        record.add_field(LnmpField {
458            fid: 3,
459            value: LnmpValue::Bool(true),
460        });
461        record.add_field(LnmpField {
462            fid: 4,
463            value: LnmpValue::String("hello".to_string()),
464        });
465        record.add_field(LnmpField {
466            fid: 5,
467            value: LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()]),
468        });
469
470        let frame = BinaryFrame::from_record(&record).unwrap();
471        let decoded_record = frame.to_record();
472
473        // Fields should match (in sorted order)
474        assert_eq!(decoded_record.fields().len(), 5);
475        assert_eq!(
476            decoded_record.get_field(1).unwrap().value,
477            LnmpValue::Int(-42)
478        );
479        assert_eq!(
480            decoded_record.get_field(2).unwrap().value,
481            LnmpValue::Float(3.14159)
482        );
483        assert_eq!(
484            decoded_record.get_field(3).unwrap().value,
485            LnmpValue::Bool(true)
486        );
487        assert_eq!(
488            decoded_record.get_field(4).unwrap().value,
489            LnmpValue::String("hello".to_string())
490        );
491        assert_eq!(
492            decoded_record.get_field(5).unwrap().value,
493            LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()])
494        );
495    }
496
497    #[test]
498    fn test_roundtrip_frame_encode_decode() {
499        let entries = vec![
500            BinaryEntry {
501                fid: 1,
502                tag: TypeTag::Int,
503                value: BinaryValue::Int(100),
504            },
505            BinaryEntry {
506                fid: 2,
507                tag: TypeTag::String,
508                value: BinaryValue::String("test".to_string()),
509            },
510        ];
511
512        let frame = BinaryFrame::new(entries);
513        let bytes = frame.encode();
514        let decoded = BinaryFrame::decode(&bytes).unwrap();
515
516        assert_eq!(decoded, frame);
517    }
518
519    #[test]
520    fn test_flags_preserved() {
521        let frame = BinaryFrame::new(vec![]);
522        let bytes = frame.encode();
523        let decoded = BinaryFrame::decode(&bytes).unwrap();
524
525        assert_eq!(decoded.flags, 0x00);
526    }
527
528    #[test]
529    fn test_empty_record() {
530        let record = LnmpRecord::new();
531        let frame = BinaryFrame::from_record(&record).unwrap();
532
533        assert_eq!(frame.entries.len(), 0);
534
535        let bytes = frame.encode();
536        let decoded = BinaryFrame::decode(&bytes).unwrap();
537        let decoded_record = decoded.to_record();
538
539        assert_eq!(decoded_record.fields().len(), 0);
540    }
541
542    #[test]
543    fn test_from_record_with_all_types() {
544        let mut record = LnmpRecord::new();
545        record.add_field(LnmpField {
546            fid: 1,
547            value: LnmpValue::Int(42),
548        });
549        record.add_field(LnmpField {
550            fid: 2,
551            value: LnmpValue::Float(2.718),
552        });
553        record.add_field(LnmpField {
554            fid: 3,
555            value: LnmpValue::Bool(false),
556        });
557        record.add_field(LnmpField {
558            fid: 4,
559            value: LnmpValue::String("world".to_string()),
560        });
561        record.add_field(LnmpField {
562            fid: 5,
563            value: LnmpValue::StringArray(vec!["x".to_string(), "y".to_string()]),
564        });
565
566        let frame = BinaryFrame::from_record(&record).unwrap();
567        assert_eq!(frame.entries.len(), 5);
568
569        let bytes = frame.encode();
570        let decoded = BinaryFrame::decode(&bytes).unwrap();
571        let decoded_record = decoded.to_record();
572
573        assert_eq!(decoded_record.fields().len(), 5);
574    }
575
576    #[test]
577    fn test_version_validation() {
578        // Test that only version 0x04 is accepted
579        let valid_bytes = vec![0x04, 0x00, 0x00];
580        assert!(BinaryFrame::decode(&valid_bytes).is_ok());
581
582        let invalid_versions = vec![0x00, 0x01, 0x02, 0x03, 0x05, 0xFF];
583        for version in invalid_versions {
584            let bytes = vec![version, 0x00, 0x00];
585            let result = BinaryFrame::decode(&bytes);
586            assert!(matches!(
587                result,
588                Err(BinaryError::UnsupportedVersion { .. })
589            ));
590        }
591    }
592}