Skip to main content

tailtalk_packets/
nbp.rs

1#![allow(dead_code)]
2
3use std::fmt::Display;
4
5/// Represents an NBP packet for AppleTalk.
6#[derive(Debug)]
7pub struct NbpPacket {
8    pub operation: NbpOperation, // NBP operation type
9    pub transaction_id: u8,      // Transaction ID for matching requests and responses
10    pub tuples: Vec<NbpTuple>,   // List of NBP tuples
11}
12
13/// Enum for NBP operation types.
14#[derive(Debug)]
15#[repr(u8)]
16pub enum NbpOperation {
17    BroadcastRequest = 1,
18    Lookup = 2,
19    LookupReply = 3,
20    ForwardRequest = 4,
21    Unknown(u8),
22}
23
24impl NbpOperation {
25    /// Parse a u8 into an NbpOperation.
26    fn from_u8(value: u8) -> Self {
27        match value {
28            1 => NbpOperation::BroadcastRequest,
29            2 => NbpOperation::Lookup,
30            3 => NbpOperation::LookupReply,
31            4 => NbpOperation::ForwardRequest,
32            _ => NbpOperation::Unknown(value),
33        }
34    }
35
36    /// Convert an NbpOperation into a u8.
37    fn to_u8(&self) -> u8 {
38        match self {
39            NbpOperation::BroadcastRequest => 1,
40            NbpOperation::Lookup => 2,
41            NbpOperation::LookupReply => 3,
42            NbpOperation::ForwardRequest => 4,
43            NbpOperation::Unknown(value) => *value,
44        }
45    }
46}
47
48/// Represents a single NBP tuple.
49#[derive(Debug)]
50pub struct NbpTuple {
51    pub network_number: u16,     // 2-byte network number
52    pub node_id: u8,             // 1-byte node ID
53    pub socket_number: u8,       // 1-byte socket number
54    pub enumerator: u8,          // 1-byte enumerator
55    pub entity_name: EntityName, // The entity name (object, type, zone)
56}
57
58/// Represents an entity name in NBP.
59#[derive(Debug, Eq, PartialEq)]
60pub struct EntityName {
61    pub object: String,      // Object name
62    pub entity_type: String, // Type of the entity (e.g., service type)
63    pub zone: String,        // Zone name
64}
65
66impl NbpPacket {
67    /// Parses an NBP packet from raw bytes.
68    pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
69        if data.len() < 2 {
70            return Err("Packet too short to be valid".to_string());
71        }
72
73        let control_byte = data[0];
74        let operation = NbpOperation::from_u8(control_byte >> 4);
75        let tuple_count = control_byte & 0x0F;
76        let transaction_id = data[1];
77        let mut offset = 2;
78        let mut tuples = Vec::new();
79
80        for _ in 0..tuple_count {
81            if offset + 5 > data.len() {
82                return Err("Packet too short for declared tuple count".to_string());
83            }
84
85            let network_number = u16::from_be_bytes([data[offset], data[offset + 1]]);
86            let node_id = data[offset + 2];
87            let socket_number = data[offset + 3];
88            let enumerator = data[offset + 4];
89            offset += 5;
90
91            let (entity_name, name_length) = EntityName::from_bytes(&data[offset..])?;
92            offset += name_length;
93
94            tuples.push(NbpTuple {
95                network_number,
96                node_id,
97                socket_number,
98                enumerator,
99                entity_name,
100            });
101        }
102
103        Ok(NbpPacket {
104            operation,
105            transaction_id,
106            tuples,
107        })
108    }
109
110    /// Serializes the NBP packet into a byte slice.
111    /// Returns the size of the serialized data.
112    pub fn to_bytes(&self, buffer: &mut [u8]) -> Result<usize, String> {
113        let mut offset = 0;
114
115        if buffer.len() < 2 {
116            return Err("Buffer too small to hold the header".to_string());
117        }
118
119        // Write control byte (operation and tuple count)
120        buffer[offset] = (self.operation.to_u8() << 4) | (self.tuples.len() as u8 & 0x0F);
121        offset += 1;
122
123        // Write transaction ID
124        buffer[offset] = self.transaction_id;
125        offset += 1;
126
127        // Write tuples
128        for tuple in &self.tuples {
129            if offset + 5 > buffer.len() {
130                return Err("Buffer too small to hold tuple data".to_string());
131            }
132
133            buffer[offset..offset + 2].copy_from_slice(&tuple.network_number.to_be_bytes());
134            offset += 2;
135
136            buffer[offset] = tuple.node_id;
137            offset += 1;
138
139            buffer[offset] = tuple.socket_number;
140            offset += 1;
141
142            buffer[offset] = tuple.enumerator;
143            offset += 1;
144
145            let entity_name_size = tuple.entity_name.to_bytes(&mut buffer[offset..])?;
146            offset += entity_name_size;
147        }
148
149        Ok(offset)
150    }
151}
152
153impl TryFrom<&str> for EntityName {
154    type Error = &'static str;
155
156    fn try_from(value: &str) -> Result<Self, Self::Error> {
157        let first_index = value
158            .find(":")
159            .ok_or("malformed entity name - missing : separator")?;
160        let second_index = value
161            .find("@")
162            .ok_or("malformed entity name - missing @ separator")?;
163
164        if first_index > second_index {
165            return Err("malformed entity name - : was found before @");
166        }
167
168        let object = &value[..first_index];
169        if object.is_empty() {
170            return Err("malformed entity name - object is empty");
171        }
172
173        let entity_type = &value[first_index + 1..second_index];
174        if entity_type.is_empty() {
175            return Err("malformed entity name - type is empty");
176        }
177
178        let zone = &value[second_index + 1..];
179        if zone.is_empty() {
180            return Err("malformed entity name - zone is empty");
181        }
182
183        Ok(EntityName {
184            object: object.into(),
185            entity_type: entity_type.into(),
186            zone: zone.into(),
187        })
188    }
189}
190
191impl Display for EntityName {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        write!(f, "{}:{}@{}", self.object, self.entity_type, self.zone)
194    }
195}
196
197impl EntityName {
198    /// Parses an entity name (object, type, zone) from raw bytes.
199    pub fn from_bytes(data: &[u8]) -> Result<(Self, usize), String> {
200        let mut offset = 0;
201
202        let object_length = *data.get(offset).ok_or("Missing object length")? as usize;
203        offset += 1;
204
205        let (object_cow, _, _) =
206            encoding_rs::MACINTOSH.decode(&data[offset..offset + object_length]);
207        let object = object_cow.into_owned();
208        offset += object_length;
209
210        let type_length = *data.get(offset).ok_or("Missing type length")? as usize;
211        offset += 1;
212
213        let (type_cow, _, _) = encoding_rs::MACINTOSH.decode(&data[offset..offset + type_length]);
214        let entity_type = type_cow.into_owned();
215        offset += type_length;
216
217        let zone_length = *data.get(offset).ok_or("Missing zone length")? as usize;
218        offset += 1;
219        let (zone_cow, _, _) = encoding_rs::MACINTOSH.decode(&data[offset..offset + zone_length]);
220        let zone = zone_cow.into_owned();
221        offset += zone_length;
222
223        Ok((
224            EntityName {
225                object,
226                entity_type,
227                zone,
228            },
229            offset,
230        ))
231    }
232
233    /// Serializes the entity name into a byte slice.
234    /// Returns the size of the serialized data.
235    pub fn to_bytes(&self, buffer: &mut [u8]) -> Result<usize, String> {
236        let mut offset = 0;
237
238        let (object_cow, _, _) = encoding_rs::MACINTOSH.encode(self.object.as_str());
239        let object = object_cow.into_owned();
240        let (type_cow, _, _) = encoding_rs::MACINTOSH.encode(self.entity_type.as_str());
241        let entity_type = type_cow.into_owned();
242        let (zone_cow, _, _) = encoding_rs::MACINTOSH.encode(self.zone.as_str());
243        let zone = zone_cow.into_owned();
244
245        let calc_size = 1 + object.len() + 1 + entity_type.len() + 1 + zone.len();
246        if buffer.len() < calc_size {
247            return Err(format!(
248                "Buffer too small to hold entity name. Buf is: {}, calc_size: {calc_size}",
249                buffer.len()
250            ));
251        }
252
253        buffer[offset] = object.len() as u8;
254        offset += 1;
255        buffer[offset..offset + object.len()].copy_from_slice(&object);
256        offset += object.len();
257
258        buffer[offset] = entity_type.len() as u8;
259        offset += 1;
260        buffer[offset..offset + entity_type.len()].copy_from_slice(&entity_type);
261        offset += entity_type.len();
262
263        buffer[offset] = zone.len() as u8;
264        offset += 1;
265        buffer[offset..offset + zone.len()].copy_from_slice(&zone);
266        offset += zone.len();
267
268        Ok(offset)
269    }
270
271    pub fn matches(&self, pattern: &EntityName) -> bool {
272        let match_part = |concrete: &str, pattern: &str| -> bool {
273            if pattern == "=" || pattern == "≈" || pattern == "*" {
274                return true;
275            }
276            concrete.eq_ignore_ascii_case(pattern)
277        };
278
279        match_part(&self.object, &pattern.object)
280            && match_part(&self.entity_type, &pattern.entity_type)
281            && match_part(&self.zone, &pattern.zone)
282    }
283
284    pub fn fully_qualified(&self) -> bool {
285        const LOOKUP_FLAGS: [char; 3] = ['*', '=', '≈'];
286
287        for flag in LOOKUP_FLAGS {
288            if self.object.contains(flag) || self.entity_type.contains(flag) || self.zone != "*" {
289                return false;
290            }
291        }
292
293        true
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_parse_nbp() {
303        const TEST_DATA: &[u8] = &[
304            0x21, 0x01, 0xff, 0x54, 0x44, 0xfe, 0x00, 0x20, 0x30, 0x41, 0x45, 0x30, 0x34, 0x39,
305            0x36, 0x30, 0x33, 0x30, 0x44, 0x42, 0x43, 0x34, 0x30, 0x34, 0x31, 0x38, 0x30, 0x30,
306            0x41, 0x44, 0x43, 0x44, 0x30, 0x34, 0x37, 0x40, 0x4d, 0x4f, 0x52, 0x4f, 0x1c, 0x4d,
307            0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0xa8, 0x20, 0x57, 0x69, 0x6e, 0x64,
308            0x6f, 0x77, 0x73, 0x20, 0x32, 0x30, 0x30, 0x30, 0xaa, 0x20, 0x50, 0x72, 0x74, 0x01,
309            0x2a,
310        ];
311
312        let packet = NbpPacket::from_bytes(TEST_DATA).expect("failed to parse");
313        let mut buf = [0u8; TEST_DATA.len()];
314
315        packet.to_bytes(&mut buf).expect("failed to encode");
316
317        assert_eq!(TEST_DATA, buf);
318    }
319
320    #[test]
321    fn test_parse_entity() {
322        let example_name = "Judy:Mailbox@Bandley3";
323
324        let entity: EntityName = example_name.try_into().expect("failed to parse");
325
326        assert_eq!(entity.object, "Judy");
327        assert_eq!(entity.entity_type, "Mailbox");
328        assert_eq!(entity.zone, "Bandley3");
329    }
330
331    #[test]
332    fn test_malformed_entity() {
333        assert!(EntityName::try_from("").is_err());
334        assert!(EntityName::try_from(":@").is_err());
335        assert!(EntityName::try_from("pants@waffles:com").is_err());
336        assert!(EntityName::try_from("Pannenkoek:Waffles@").is_err());
337        assert!(EntityName::try_from("Pannenkoek:@Waffles").is_err());
338        assert!(EntityName::try_from("Pannenkoek:@@@:").is_err());
339    }
340    #[test]
341    fn test_matches() {
342        let name: EntityName = "Steve:Workstation@Twilight".try_into().unwrap();
343
344        // Exact match
345        assert!(name.matches(&"Steve:Workstation@Twilight".try_into().unwrap()));
346
347        // Case insensitive
348        assert!(name.matches(&"steve:workstation@twilight".try_into().unwrap()));
349
350        // Wildcards
351        assert!(name.matches(&"=:=@*".try_into().unwrap()));
352
353        assert!(name.matches(&"≈:Workstation@*".try_into().unwrap()));
354
355        // Mismatches
356        assert!(!name.matches(&"Bob:Workstation@Twilight".try_into().unwrap()));
357
358        assert!(!name.matches(&"Steve:Printer@Twilight".try_into().unwrap()));
359    }
360}