pcapsql_core/protocol/
field.rs

1//! Field value types for protocol parsing.
2//!
3//! This module provides zero-copy field values where possible. FieldValue
4//! can reference packet data directly (Str, Bytes variants) or own data
5//! when construction is necessary (OwnedString, OwnedBytes).
6
7use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
8
9use compact_str::CompactString;
10
11/// Possible field value types (maps to Arrow types).
12///
13/// FieldValue supports zero-copy parsing where possible:
14/// - `Str` and `Bytes` reference packet data directly
15/// - `OwnedString` and `OwnedBytes` are used when values must be constructed
16///
17/// The lifetime parameter `'data` ties the value to the packet/buffer data.
18#[derive(Debug, Clone)]
19pub enum FieldValue<'data> {
20    // === Primitives (trivial copies) ===
21    /// Unsigned 8-bit integer
22    UInt8(u8),
23    /// Unsigned 16-bit integer
24    UInt16(u16),
25    /// Unsigned 32-bit integer
26    UInt32(u32),
27    /// Unsigned 64-bit integer
28    UInt64(u64),
29    /// Signed 64-bit integer
30    Int64(i64),
31    /// Boolean value
32    Bool(bool),
33
34    // === Network types (small fixed arrays) ===
35    /// IP address (v4 or v6)
36    IpAddr(IpAddr),
37    /// MAC address (6 bytes)
38    MacAddr([u8; 6]),
39
40    // === Zero-copy references into packet/buffer ===
41    /// Zero-copy string reference into packet data.
42    /// Use for strings that exist verbatim in the packet (e.g., TLS SNI, SSH version).
43    Str(&'data str),
44    /// Zero-copy byte slice reference into packet data.
45    /// Use for payload or binary data that exists verbatim in the packet.
46    Bytes(&'data [u8]),
47
48    // === Constructed/owned values ===
49    /// Owned string for constructed values (DNS names, joined lists, enum names).
50    /// Uses CompactString for small-string optimization (inline up to 24 bytes).
51    OwnedString(CompactString),
52    /// Owned bytes for constructed/decoded data.
53    OwnedBytes(Vec<u8>),
54
55    /// List of values (for multi-valued fields like DNS answers).
56    /// All elements should be of the same type.
57    /// Note: Uses Vec because FieldValue is recursive (SmallVec inline storage causes infinite size).
58    List(Vec<FieldValue<'data>>),
59
60    /// Null/missing value
61    Null,
62}
63
64/// Type alias for FieldValue that owns all its data.
65/// Useful for caching where lifetime of packet data is not available.
66pub type OwnedFieldValue = FieldValue<'static>;
67
68impl<'data> FieldValue<'data> {
69    /// Create a MAC address from bytes.
70    pub fn mac(bytes: &[u8]) -> Self {
71        if bytes.len() >= 6 {
72            let mut mac = [0u8; 6];
73            mac.copy_from_slice(&bytes[..6]);
74            FieldValue::MacAddr(mac)
75        } else {
76            FieldValue::Null
77        }
78    }
79
80    /// Create an IPv4 address from bytes.
81    pub fn ipv4(bytes: &[u8]) -> Self {
82        if bytes.len() >= 4 {
83            FieldValue::IpAddr(IpAddr::V4(Ipv4Addr::new(
84                bytes[0], bytes[1], bytes[2], bytes[3],
85            )))
86        } else {
87            FieldValue::Null
88        }
89    }
90
91    /// Create an IPv6 address from bytes.
92    pub fn ipv6(bytes: &[u8]) -> Self {
93        if bytes.len() >= 16 {
94            let mut arr = [0u8; 16];
95            arr.copy_from_slice(&bytes[..16]);
96            FieldValue::IpAddr(IpAddr::V6(Ipv6Addr::from(arr)))
97        } else {
98            FieldValue::Null
99        }
100    }
101
102    /// Format a MAC address as a string.
103    pub fn format_mac(mac: &[u8; 6]) -> String {
104        format!(
105            "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
106            mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
107        )
108    }
109
110    /// Check if this is a null value.
111    pub fn is_null(&self) -> bool {
112        matches!(self, FieldValue::Null)
113    }
114
115    /// Try to get as u64.
116    pub fn as_u64(&self) -> Option<u64> {
117        match self {
118            FieldValue::UInt8(v) => Some(*v as u64),
119            FieldValue::UInt16(v) => Some(*v as u64),
120            FieldValue::UInt32(v) => Some(*v as u64),
121            FieldValue::UInt64(v) => Some(*v),
122            _ => None,
123        }
124    }
125
126    /// Try to get as i64.
127    pub fn as_i64(&self) -> Option<i64> {
128        match self {
129            FieldValue::Int64(v) => Some(*v),
130            FieldValue::UInt8(v) => Some(*v as i64),
131            FieldValue::UInt16(v) => Some(*v as i64),
132            FieldValue::UInt32(v) => Some(*v as i64),
133            _ => None,
134        }
135    }
136
137    /// Try to get as u16.
138    pub fn as_u16(&self) -> Option<u16> {
139        match self {
140            FieldValue::UInt16(v) => Some(*v),
141            FieldValue::UInt8(v) => Some(*v as u16),
142            _ => None,
143        }
144    }
145
146    /// Try to get as str reference.
147    pub fn as_str(&self) -> Option<&str> {
148        match self {
149            FieldValue::Str(s) => Some(s),
150            FieldValue::OwnedString(s) => Some(s.as_str()),
151            _ => None,
152        }
153    }
154
155    /// Try to get as bytes reference.
156    pub fn as_bytes(&self) -> Option<&[u8]> {
157        match self {
158            FieldValue::Bytes(b) => Some(b),
159            FieldValue::OwnedBytes(b) => Some(b.as_slice()),
160            _ => None,
161        }
162    }
163
164    /// Try to get as string (owned, allocates).
165    pub fn as_string(&self) -> Option<String> {
166        match self {
167            FieldValue::Str(s) => Some(s.to_string()),
168            FieldValue::OwnedString(s) => Some(s.to_string()),
169            FieldValue::IpAddr(addr) => Some(addr.to_string()),
170            FieldValue::MacAddr(mac) => Some(Self::format_mac(mac)),
171            FieldValue::UInt8(v) => Some(v.to_string()),
172            FieldValue::UInt16(v) => Some(v.to_string()),
173            FieldValue::UInt32(v) => Some(v.to_string()),
174            FieldValue::UInt64(v) => Some(v.to_string()),
175            FieldValue::Int64(v) => Some(v.to_string()),
176            FieldValue::Bool(v) => Some(v.to_string()),
177            _ => None,
178        }
179    }
180
181    /// Try to get as list reference.
182    pub fn as_list(&self) -> Option<&[FieldValue<'data>]> {
183        match self {
184            FieldValue::List(items) => Some(items.as_slice()),
185            _ => None,
186        }
187    }
188
189    /// Get the number of elements if this is a list, or None otherwise.
190    pub fn list_len(&self) -> Option<usize> {
191        match self {
192            FieldValue::List(items) => Some(items.len()),
193            _ => None,
194        }
195    }
196
197    /// Convert to an owned version (for caching).
198    /// Copies borrowed data into owned variants.
199    pub fn to_owned(&self) -> FieldValue<'static> {
200        match self {
201            FieldValue::UInt8(v) => FieldValue::UInt8(*v),
202            FieldValue::UInt16(v) => FieldValue::UInt16(*v),
203            FieldValue::UInt32(v) => FieldValue::UInt32(*v),
204            FieldValue::UInt64(v) => FieldValue::UInt64(*v),
205            FieldValue::Int64(v) => FieldValue::Int64(*v),
206            FieldValue::Bool(v) => FieldValue::Bool(*v),
207            FieldValue::IpAddr(v) => FieldValue::IpAddr(*v),
208            FieldValue::MacAddr(v) => FieldValue::MacAddr(*v),
209            FieldValue::Str(s) => FieldValue::OwnedString(CompactString::new(s)),
210            FieldValue::Bytes(b) => FieldValue::OwnedBytes(b.to_vec()),
211            FieldValue::OwnedString(s) => FieldValue::OwnedString(s.clone()),
212            FieldValue::OwnedBytes(b) => FieldValue::OwnedBytes(b.clone()),
213            FieldValue::List(items) => {
214                FieldValue::List(items.iter().map(|v| v.to_owned()).collect())
215            }
216            FieldValue::Null => FieldValue::Null,
217        }
218    }
219}
220
221impl<'data> std::fmt::Display for FieldValue<'data> {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        match self {
224            FieldValue::UInt8(v) => write!(f, "{v}"),
225            FieldValue::UInt16(v) => write!(f, "{v}"),
226            FieldValue::UInt32(v) => write!(f, "{v}"),
227            FieldValue::UInt64(v) => write!(f, "{v}"),
228            FieldValue::Int64(v) => write!(f, "{v}"),
229            FieldValue::Bool(v) => write!(f, "{v}"),
230            FieldValue::Str(s) => write!(f, "{s}"),
231            FieldValue::OwnedString(s) => write!(f, "{s}"),
232            FieldValue::Bytes(b) => write!(f, "[{} bytes]", b.len()),
233            FieldValue::OwnedBytes(b) => write!(f, "[{} bytes]", b.len()),
234            FieldValue::IpAddr(addr) => write!(f, "{addr}"),
235            FieldValue::MacAddr(mac) => write!(f, "{}", Self::format_mac(mac)),
236            FieldValue::List(items) => {
237                write!(f, "[")?;
238                for (i, item) in items.iter().enumerate() {
239                    if i > 0 {
240                        write!(f, ", ")?;
241                    }
242                    write!(f, "{item}")?;
243                }
244                write!(f, "]")
245            }
246            FieldValue::Null => write!(f, "NULL"),
247        }
248    }
249}
250
251// Implement PartialEq manually to handle borrowed vs owned comparison
252impl<'a, 'b> PartialEq<FieldValue<'b>> for FieldValue<'a> {
253    fn eq(&self, other: &FieldValue<'b>) -> bool {
254        match (self, other) {
255            (FieldValue::UInt8(a), FieldValue::UInt8(b)) => a == b,
256            (FieldValue::UInt16(a), FieldValue::UInt16(b)) => a == b,
257            (FieldValue::UInt32(a), FieldValue::UInt32(b)) => a == b,
258            (FieldValue::UInt64(a), FieldValue::UInt64(b)) => a == b,
259            (FieldValue::Int64(a), FieldValue::Int64(b)) => a == b,
260            (FieldValue::Bool(a), FieldValue::Bool(b)) => a == b,
261            (FieldValue::IpAddr(a), FieldValue::IpAddr(b)) => a == b,
262            (FieldValue::MacAddr(a), FieldValue::MacAddr(b)) => a == b,
263            // String comparisons: allow cross-comparison between Str and OwnedString
264            (FieldValue::Str(a), FieldValue::Str(b)) => a == b,
265            (FieldValue::Str(a), FieldValue::OwnedString(b)) => *a == b.as_str(),
266            (FieldValue::OwnedString(a), FieldValue::Str(b)) => a.as_str() == *b,
267            (FieldValue::OwnedString(a), FieldValue::OwnedString(b)) => a == b,
268            // Bytes comparisons: allow cross-comparison between Bytes and OwnedBytes
269            (FieldValue::Bytes(a), FieldValue::Bytes(b)) => a == b,
270            (FieldValue::Bytes(a), FieldValue::OwnedBytes(b)) => *a == b.as_slice(),
271            (FieldValue::OwnedBytes(a), FieldValue::Bytes(b)) => a.as_slice() == *b,
272            (FieldValue::OwnedBytes(a), FieldValue::OwnedBytes(b)) => a == b,
273            // List comparison: element-wise
274            (FieldValue::List(a), FieldValue::List(b)) => {
275                a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x == y)
276            }
277            (FieldValue::Null, FieldValue::Null) => true,
278            _ => false,
279        }
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_zero_copy_str() {
289        let packet = b"GET /index.html HTTP/1.1\r\n";
290        let value = FieldValue::Str(std::str::from_utf8(&packet[4..15]).unwrap());
291
292        match value {
293            FieldValue::Str(s) => {
294                assert_eq!(s, "/index.html");
295                // Verify it's a reference, not owned
296                assert!(std::ptr::eq(s.as_ptr(), packet[4..].as_ptr()));
297            }
298            _ => panic!("Expected Str variant"),
299        }
300    }
301
302    #[test]
303    fn test_zero_copy_bytes() {
304        let packet = vec![0x45, 0x00, 0x00, 0x28, 0xde, 0xad, 0xbe, 0xef];
305        let payload = &packet[4..];
306        let value = FieldValue::Bytes(payload);
307
308        match value {
309            FieldValue::Bytes(b) => {
310                assert_eq!(b, &[0xde, 0xad, 0xbe, 0xef]);
311                assert!(std::ptr::eq(b.as_ptr(), packet[4..].as_ptr()));
312            }
313            _ => panic!("Expected Bytes variant"),
314        }
315    }
316
317    #[test]
318    fn test_owned_string() {
319        // DNS domain name must be constructed (labels + dots)
320        let domain = CompactString::new("www.example.com");
321        let value = FieldValue::OwnedString(domain);
322
323        match value {
324            FieldValue::OwnedString(s) => assert_eq!(s.as_str(), "www.example.com"),
325            _ => panic!("Expected OwnedString variant"),
326        }
327    }
328
329    #[test]
330    fn test_compact_string_inline() {
331        // CompactString stores small strings inline (no heap alloc)
332        let small = CompactString::new("example.com"); // 11 bytes - inline
333        assert!(!small.is_heap_allocated());
334
335        let large = CompactString::new("this-is-a-very-long-domain-name.example.com");
336        assert!(large.is_heap_allocated());
337    }
338
339    #[test]
340    fn test_str_owned_string_equality() {
341        let borrowed = FieldValue::Str("hello");
342        let owned = FieldValue::OwnedString(CompactString::new("hello"));
343
344        assert_eq!(borrowed, owned);
345        assert_eq!(owned, borrowed);
346    }
347
348    #[test]
349    fn test_bytes_owned_bytes_equality() {
350        let data = &[1u8, 2, 3, 4][..];
351        let borrowed = FieldValue::Bytes(data);
352        let owned = FieldValue::OwnedBytes(vec![1, 2, 3, 4]);
353
354        assert_eq!(borrowed, owned);
355        assert_eq!(owned, borrowed);
356    }
357
358    #[test]
359    fn test_to_owned() {
360        let packet = b"example.com";
361        let borrowed = FieldValue::Str(std::str::from_utf8(packet).unwrap());
362        let owned = borrowed.to_owned();
363
364        // Should be equal
365        assert_eq!(borrowed, owned);
366
367        // But owned should be OwnedString
368        match owned {
369            FieldValue::OwnedString(s) => assert_eq!(s.as_str(), "example.com"),
370            _ => panic!("Expected OwnedString variant"),
371        }
372    }
373
374    #[test]
375    fn test_as_str() {
376        let str_val = FieldValue::Str("hello");
377        let owned_val = FieldValue::OwnedString(CompactString::new("world"));
378        let int_val = FieldValue::UInt32(42);
379
380        assert_eq!(str_val.as_str(), Some("hello"));
381        assert_eq!(owned_val.as_str(), Some("world"));
382        assert_eq!(int_val.as_str(), None);
383    }
384
385    #[test]
386    fn test_as_bytes() {
387        let bytes_val = FieldValue::Bytes(&[1, 2, 3]);
388        let owned_val = FieldValue::OwnedBytes(vec![4, 5, 6]);
389        let int_val = FieldValue::UInt32(42);
390
391        assert_eq!(bytes_val.as_bytes(), Some(&[1u8, 2, 3][..]));
392        assert_eq!(owned_val.as_bytes(), Some(&[4u8, 5, 6][..]));
393        assert_eq!(int_val.as_bytes(), None);
394    }
395
396    #[test]
397    fn test_list_basic() {
398        let list = FieldValue::List(vec![
399            FieldValue::UInt32(1),
400            FieldValue::UInt32(2),
401            FieldValue::UInt32(3),
402        ]);
403
404        assert_eq!(list.list_len(), Some(3));
405        assert!(list.as_list().is_some());
406
407        let items = list.as_list().unwrap();
408        assert_eq!(items[0], FieldValue::UInt32(1));
409        assert_eq!(items[1], FieldValue::UInt32(2));
410        assert_eq!(items[2], FieldValue::UInt32(3));
411    }
412
413    #[test]
414    fn test_list_display() {
415        let list = FieldValue::List(vec![FieldValue::UInt32(10), FieldValue::UInt32(20)]);
416        assert_eq!(format!("{}", list), "[10, 20]");
417
418        let empty: FieldValue = FieldValue::List(vec![]);
419        assert_eq!(format!("{}", empty), "[]");
420
421        let string_list = FieldValue::List(vec![
422            FieldValue::OwnedString(CompactString::new("hello")),
423            FieldValue::OwnedString(CompactString::new("world")),
424        ]);
425        assert_eq!(format!("{}", string_list), "[hello, world]");
426    }
427
428    #[test]
429    fn test_list_equality() {
430        let list1 = FieldValue::List(vec![FieldValue::UInt32(1), FieldValue::UInt32(2)]);
431        let list2 = FieldValue::List(vec![FieldValue::UInt32(1), FieldValue::UInt32(2)]);
432        let list3 = FieldValue::List(vec![FieldValue::UInt32(1), FieldValue::UInt32(3)]);
433
434        assert_eq!(list1, list2);
435        assert_ne!(list1, list3);
436    }
437
438    #[test]
439    fn test_list_to_owned() {
440        let packet = b"test";
441        let list_with_borrowed: FieldValue = FieldValue::List(vec![
442            FieldValue::Str(std::str::from_utf8(packet).unwrap()),
443            FieldValue::UInt32(42),
444        ]);
445
446        let owned = list_with_borrowed.to_owned();
447
448        // Should still be equal
449        assert_eq!(list_with_borrowed, owned);
450
451        // Verify it's owned
452        match &owned {
453            FieldValue::List(items) => {
454                assert!(matches!(&items[0], FieldValue::OwnedString(_)));
455                assert!(matches!(&items[1], FieldValue::UInt32(42)));
456            }
457            _ => panic!("Expected List variant"),
458        }
459    }
460}