Skip to main content

tailtalk_packets/afp/
commands.rs

1use super::types::{AfpError, AfpUam, AfpVersion, CreateFlag, PathType};
2use crate::afp::util::MacString;
3use crate::afp::{
4    FPAccessRights, FPByteRangeLockFlags, FPDirectoryBitmap, FPFileAttributes, FPFileBitmap,
5    FPVolumeBitmap,
6};
7
8/// AFP Command Codes
9pub const AFP_CMD_BYTE_RANGE_LOCK: u8 = 1;
10pub const AFP_CMD_CLOSE_VOL: u8 = 2;
11pub const AFP_CMD_CLOSE_FORK: u8 = 4;
12pub const AFP_CMD_CREATE_DIR: u8 = 6;
13pub const AFP_CMD_CREATE_FILE: u8 = 7;
14pub const AFP_CMD_DELETE: u8 = 8;
15pub const AFP_CMD_ENUMERATE: u8 = 9;
16pub const AFP_CMD_FLUSH: u8 = 10;
17pub const AFP_CMD_GET_FORK_PARMS: u8 = 14;
18pub const AFP_CMD_GET_SRVR_PARMS: u8 = 16;
19pub const AFP_CMD_GET_VOL_PARMS: u8 = 17;
20pub const AFP_CMD_LOGIN: u8 = 18;
21pub const AFP_CMD_LOGOUT: u8 = 20;
22pub const AFP_CMD_MOVE_AND_RENAME: u8 = 23;
23pub const AFP_CMD_OPEN_VOL: u8 = 24;
24pub const AFP_CMD_OPEN_FORK: u8 = 26;
25pub const AFP_CMD_READ: u8 = 27;
26pub const AFP_CMD_RENAME: u8 = 28;
27pub const AFP_CMD_SET_DIR_PARMS: u8 = 29;
28pub const AFP_CMD_SET_FORK_PARMS: u8 = 31;
29pub const AFP_CMD_WRITE: u8 = 33;
30pub const AFP_CMD_GET_FILE_DIR_PARMS: u8 = 34;
31pub const AFP_CMD_SET_FILE_DIR_PARMS: u8 = 35;
32pub const AFP_CMD_GET_SRVR_MSG: u8 = 38;
33pub const AFP_CMD_OPEN_DT: u8 = 48;
34pub const AFP_CMD_CLOSE_DT: u8 = 49;
35pub const AFP_CMD_GET_ICON: u8 = 51;
36pub const AFP_CMD_GTICNINFO: u8 = 52;
37pub const AFP_CMD_ADD_APPL: u8 = 53;
38pub const AFP_CMD_ADD_COMMENT: u8 = 56;
39pub const AFP_CMD_REMOVE_COMMENT: u8 = 57;
40pub const AFP_CMD_GET_COMMENT: u8 = 58;
41pub const AFP_CMD_ADD_ICON: u8 = 192;
42
43/// Authentication payload for FPLogin, varies by UAM
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum FPLoginAuth {
46    /// No authentication required
47    NoUserAuthent,
48
49    /// Clear text password authentication
50    CleartxtPasswrd {
51        username: MacString,
52        password: [u8; 8], // Exactly 8 bytes, padded with nulls
53    },
54}
55
56/// FPLogin command - authentication request from client to server
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct FPLogin {
59    /// AFP version the client wants to use
60    pub afp_version: AfpVersion,
61
62    /// User authentication method and credentials
63    pub auth: FPLoginAuth,
64}
65
66impl FPLogin {
67    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
68        if buf.len() < 2 {
69            return Err(AfpError::InvalidSize);
70        }
71
72        let mut offset = 0;
73
74        // Helper to read a Pascal string
75        let read_pstr = |offset: usize| -> Result<(MacString, usize), AfpError> {
76            let parsed = MacString::try_from(&buf[offset..])?;
77            let next_offset = offset + parsed.byte_len();
78            Ok((parsed, next_offset))
79        };
80
81        // Parse AFP version
82        let (afp_version_str, next_offset) = read_pstr(offset)?;
83        let afp_version = AfpVersion::try_from(afp_version_str.as_str())?;
84        offset = next_offset;
85
86        // Parse UAM
87        let (uam_str, next_offset) = read_pstr(offset)?;
88        let uam = AfpUam::try_from(uam_str.as_str())?;
89        offset = next_offset;
90
91        // Parse auth data based on UAM
92        let auth = match uam {
93            AfpUam::NoUserAuthent => FPLoginAuth::NoUserAuthent,
94
95            AfpUam::CleartxtPasswrd => {
96                // Parse username
97                let (username, next_offset) = read_pstr(offset)?;
98                offset = next_offset;
99
100                // Parse 8-byte password
101                if offset + 8 > buf.len() {
102                    return Err(AfpError::InvalidSize);
103                }
104                let mut password = [0u8; 8];
105                password.copy_from_slice(&buf[offset..offset + 8]);
106
107                FPLoginAuth::CleartxtPasswrd { username, password }
108            }
109
110            _ => return Err(AfpError::BadUam),
111        };
112
113        Ok(FPLogin { afp_version, auth })
114    }
115
116    pub fn to_bytes(&self) -> Result<Vec<u8>, AfpError> {
117        let mut buf = Vec::new();
118
119        // Serialize AFP version
120        let afp_version_str = self.afp_version.as_str();
121        let len = afp_version_str.len() as u8;
122        buf.push(len);
123        buf.extend_from_slice(afp_version_str.as_bytes());
124
125        // Serialize UAM and auth data based on variant
126        match &self.auth {
127            FPLoginAuth::NoUserAuthent => {
128                let uam_str = AfpUam::NoUserAuthent.as_str();
129                let len = uam_str.len() as u8;
130                buf.push(len);
131                buf.extend_from_slice(uam_str.as_bytes());
132                // No additional data for NoUserAuthent
133            }
134
135            FPLoginAuth::CleartxtPasswrd { username, password } => {
136                let uam_str = AfpUam::CleartxtPasswrd.as_str();
137                let len = uam_str.len() as u8;
138                buf.push(len);
139                buf.extend_from_slice(uam_str.as_bytes());
140
141                // Serialize username
142                let mut username_buf = [0u8; 256];
143                let written = username.bytes(&mut username_buf)?;
144                buf.extend_from_slice(&username_buf[..written]);
145
146                // Serialize 8-byte password
147                buf.extend_from_slice(password);
148            }
149        }
150
151        Ok(buf)
152    }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct FPGetSrvrInfo {
157    pub machine_type: MacString,
158    pub afp_versions: Vec<AfpVersion>,
159    pub uams: Vec<AfpUam>,
160    pub volume_icon: Option<[u8; 256]>,
161    pub flags: u16,
162    pub server_name: MacString,
163}
164
165impl FPGetSrvrInfo {
166    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
167        if buf.len() < 11 {
168            // 10 bytes header + at least 1 byte server name len
169            return Err(AfpError::InvalidSize);
170        }
171
172        let machine_type_offset = u16::from_be_bytes([buf[0], buf[1]]) as usize;
173        let afp_versions_offset = u16::from_be_bytes([buf[2], buf[3]]) as usize;
174        let uams_offset = u16::from_be_bytes([buf[4], buf[5]]) as usize;
175        let volume_icon_offset = u16::from_be_bytes([buf[6], buf[7]]) as usize;
176        let flags = u16::from_be_bytes([buf[8], buf[9]]);
177
178        // Server Name is inline at offset 10
179        let server_name_len = buf[10] as usize;
180        if 10 + 1 + server_name_len > buf.len() {
181            return Err(AfpError::InvalidSize);
182        }
183        let server_name = MacString::try_from(&buf[10..10 + 1 + server_name_len])?;
184
185        // Helper to read a pascal string at a given offset
186        let read_pstr = |offset: usize| -> Result<MacString, AfpError> {
187            if offset >= buf.len() {
188                return Err(AfpError::InvalidSize);
189            }
190            let len = buf[offset] as usize;
191            if offset + 1 + len > buf.len() {
192                return Err(AfpError::InvalidSize);
193            }
194            MacString::try_from(&buf[offset..offset + 1 + len])
195        };
196
197        // Helper to read a list of pascal strings (Count byte + Strings)
198        let read_pstr_list = |offset: usize| -> Result<Vec<MacString>, AfpError> {
199            if offset >= buf.len() {
200                return Err(AfpError::InvalidSize);
201            }
202            let count = buf[offset] as usize;
203            let mut strings: Vec<MacString> = Vec::with_capacity(count);
204            let mut current_pos = offset + 1;
205
206            for _ in 0..count {
207                if current_pos >= buf.len() {
208                    return Err(AfpError::InvalidSize);
209                }
210
211                let new_string = MacString::try_from(&buf[current_pos..])?;
212                current_pos += new_string.byte_len();
213                strings.push(new_string);
214            }
215            Ok(strings)
216        };
217
218        let machine_type = read_pstr(machine_type_offset)?;
219        let afp_versions_strings: Vec<MacString> = read_pstr_list(afp_versions_offset)?;
220        let afp_versions: Vec<AfpVersion> = afp_versions_strings
221            .iter()
222            .map(|s| AfpVersion::try_from(s.as_str()))
223            .collect::<Result<Vec<_>, _>>()
224            .map_err(|_| AfpError::BadVersNum)?;
225        let uams_strings: Vec<MacString> = read_pstr_list(uams_offset)?;
226        let uams: Vec<AfpUam> = uams_strings
227            .iter()
228            .map(|s| AfpUam::try_from(s.as_str()).map_err(|_| AfpError::BadUam))
229            .collect::<Result<Vec<_>, _>>()?;
230
231        // server_name already read
232
233        let volume_icon = if volume_icon_offset != 0 {
234            if volume_icon_offset + 256 > buf.len() {
235                return Err(AfpError::InvalidSize);
236            }
237            let mut icon = [0u8; 256];
238            icon.copy_from_slice(&buf[volume_icon_offset..volume_icon_offset + 256]);
239            Some(icon)
240        } else {
241            None
242        };
243
244        Ok(Self {
245            machine_type,
246            afp_versions,
247            uams,
248            volume_icon,
249            flags,
250            server_name,
251        })
252    }
253
254    pub fn to_bytes(&self) -> Result<Vec<u8>, AfpError> {
255        let mut buf = Vec::new();
256
257        let mut server_name_buf = [0u8; 256];
258        let written = self.server_name.bytes(&mut server_name_buf)?;
259        let server_name_len = (written - 1).min(255);
260
261        // Header size = 10 bytes (offsets + flags)
262        let mut base_offset = 10 + 1 + server_name_len;
263        let mut padding_needed = 0;
264        
265        // Inside AppleTalk specifies that fields following the Server Name
266        // must be word-aligned (even byte boundary).
267        if base_offset % 2 != 0 {
268            base_offset += 1;
269            padding_needed = 1;
270        }
271
272        let mut current_offset = base_offset as u16;
273
274        let mut variable_data = Vec::new();
275
276        let mut tmp_macstr = [0u8; 256];
277        let machine_type_ptr = current_offset;
278        {
279            let written_mt = self.machine_type.bytes(&mut tmp_macstr)?;
280            variable_data.extend_from_slice(&tmp_macstr[..written_mt]);
281        }
282        current_offset = base_offset as u16 + variable_data.len() as u16;
283
284        let afp_versions_ptr = current_offset;
285        variable_data.push(self.afp_versions.len() as u8);
286        for v in &self.afp_versions {
287            let s = v.as_str();
288            let len = s.len() as u8;
289            variable_data.push(len);
290            variable_data.extend_from_slice(s.as_bytes());
291        }
292        current_offset = base_offset as u16 + variable_data.len() as u16;
293
294        let uams_ptr = current_offset;
295        variable_data.push(self.uams.len() as u8);
296        for u in &self.uams {
297            let s = u.as_str();
298            let len = s.len() as u8;
299            variable_data.push(len);
300            variable_data.extend_from_slice(s.as_bytes());
301        }
302        current_offset = base_offset as u16 + variable_data.len() as u16;
303
304        let volume_icon_ptr = if let Some(icon) = &self.volume_icon {
305            let ptr = current_offset;
306            variable_data.extend_from_slice(icon);
307            ptr
308        } else {
309            0
310        };
311
312        buf.extend_from_slice(&machine_type_ptr.to_be_bytes());
313        buf.extend_from_slice(&afp_versions_ptr.to_be_bytes());
314        buf.extend_from_slice(&uams_ptr.to_be_bytes());
315        buf.extend_from_slice(&volume_icon_ptr.to_be_bytes());
316        buf.extend_from_slice(&self.flags.to_be_bytes());
317
318        buf.push(server_name_len as u8);
319        buf.extend_from_slice(&server_name_buf[1..1 + server_name_len]);
320
321        if padding_needed > 0 {
322            buf.push(0); // Pad with null byte to achieve word alignment
323        }
324
325        buf.extend_from_slice(&variable_data);
326
327        Ok(buf)
328    }
329}
330
331pub struct FPVolume {
332    pub has_password: bool,
333    pub has_config_info: bool,
334    pub name: MacString,
335}
336
337impl FPVolume {
338    pub fn to_bytes(&self, buf: &mut [u8]) -> Result<usize, AfpError> {
339        // Size is 1 byte for flags, 1 byte for name length, and then name bytes
340        let target_size = 2 + self.name.len();
341
342        if buf.len() < target_size {
343            return Err(AfpError::InvalidSize);
344        }
345
346        // Reslice to avoid bounds check on each copy
347        let target = &mut buf[..target_size];
348
349        target[0] = (self.has_password as u8) << 7 | (self.has_config_info as u8) << 6;
350        target[1] = (self.name.byte_len() - 1) as u8;
351        self.name.bytes(&mut target[1..])?;
352
353        Ok(target_size)
354    }
355
356    pub fn size(&self) -> usize {
357        2 + self.name.byte_len() - 1
358    }
359}
360
361pub struct FPGetSrvrParms {
362    pub server_time: u32,
363    pub volumes: Vec<FPVolume>,
364}
365
366impl FPGetSrvrParms {
367    pub fn to_bytes(&self, buf: &mut [u8]) -> Result<usize, AfpError> {
368        let mut offset = 0;
369
370        // Size is 4 bytes for server time + 1 byte for volume count + sum of all volume sizes
371        let target_size = 5 + self.volumes.iter().map(|v| v.size()).sum::<usize>();
372
373        if buf.len() < target_size {
374            return Err(AfpError::InvalidSize);
375        }
376
377        // Reslice to avoid bounds check on each copy
378        let target = &mut buf[..target_size];
379
380        target[offset..offset + 4].copy_from_slice(&self.server_time.to_be_bytes());
381        offset += 4;
382        target[offset] = self.volumes.len() as u8;
383        offset += 1;
384
385        for volume in &self.volumes {
386            let volume_size = volume.to_bytes(&mut target[offset..])?;
387            offset += volume_size;
388        }
389
390        Ok(target_size)
391    }
392}
393
394#[derive(Debug)]
395pub struct FPEnumerate {
396    pub volume_id: u16,
397    pub directory_id: u32,
398    pub file_bitmap: FPFileBitmap,
399    pub directory_bitmap: FPDirectoryBitmap,
400    pub req_count: u16,
401    pub start_index: u16,
402    pub max_reply_size: u16,
403    pub path: MacString,
404}
405
406impl FPEnumerate {
407    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
408        let volume_id = u16::from_be_bytes(*buf[0..2].as_array().unwrap());
409        let directory_id = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
410        let file_bitmap = FPFileBitmap::from(u16::from_be_bytes(*buf[6..8].as_array().unwrap()));
411        let directory_bitmap =
412            FPDirectoryBitmap::from(u16::from_be_bytes(*buf[8..10].as_array().unwrap()));
413        let req_count = u16::from_be_bytes(*buf[10..12].as_array().unwrap());
414        let start_index = u16::from_be_bytes(*buf[12..14].as_array().unwrap());
415        let max_reply_size = u16::from_be_bytes(*buf[14..16].as_array().unwrap());
416        let _path_type = buf[16];
417        let path = MacString::try_from(&buf[17..])?;
418
419        Ok(Self {
420            volume_id,
421            directory_id,
422            file_bitmap,
423            directory_bitmap,
424            req_count,
425            start_index,
426            max_reply_size,
427            path,
428        })
429    }
430}
431
432#[derive(Debug)]
433pub struct FPByteRangeLock {
434    pub fork_id: u16,
435    pub offset: i32,
436    pub length: u32,
437    pub flags: FPByteRangeLockFlags,
438}
439
440impl FPByteRangeLock {
441    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
442        // Here Be Dragons:
443        // This command also does not match Inside AppleTalk. Perhaps a version difference? Very confusing.
444        let flags = FPByteRangeLockFlags::from(buf[0]);
445        let fork_id = u16::from_be_bytes(*buf[1..3].as_array().unwrap());
446        let offset = i32::from_be_bytes(*buf[3..7].as_array().unwrap());
447        let length = u32::from_be_bytes(*buf[7..11].as_array().unwrap());
448
449        Ok(Self {
450            fork_id,
451            offset,
452            length,
453            flags,
454        })
455    }
456}
457
458#[derive(Debug)]
459pub struct FPCloseFork {
460    pub fork_id: u16,
461}
462
463impl FPCloseFork {
464    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
465        let fork_id = u16::from_be_bytes(*buf[0..2].as_array().unwrap());
466
467        Ok(Self { fork_id })
468    }
469}
470
471#[derive(Debug)]
472pub struct FPSetDirParms {
473    pub volume_id: u16,
474    pub directory_id: u32,
475    pub dir_bitmap: FPDirectoryBitmap,
476    pub path: MacString,
477    pub attributes: Option<FPFileAttributes>,
478    pub finder_info: Option<[u8; 32]>,
479    pub owner_id: Option<u32>,
480    pub group_id: Option<u32>,
481    pub owner_access: Option<FPAccessRights>,
482    pub group_access: Option<FPAccessRights>,
483    pub everyone_access: Option<FPAccessRights>,
484    pub user_access: Option<FPAccessRights>,
485}
486
487impl FPSetDirParms {
488    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
489        let volume_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
490        let directory_id = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
491        let dir_bitmap =
492            FPDirectoryBitmap::from(u16::from_be_bytes(*buf[6..8].as_array().unwrap()));
493        let _path_type = buf[8];
494        let path = MacString::try_from(&buf[9..])?;
495
496        let mut offset = 9 + path.byte_len();
497
498        let mut parsed_parms = Self {
499            volume_id,
500            directory_id,
501            dir_bitmap,
502            path,
503            attributes: None,
504            finder_info: None,
505            owner_id: None,
506            group_id: None,
507            owner_access: None,
508            group_access: None,
509            everyone_access: None,
510            user_access: None,
511        };
512
513        if dir_bitmap.contains(FPDirectoryBitmap::ATTRIBUTES) {
514            let attributes = FPFileAttributes::from(u16::from_be_bytes(
515                *buf[offset..offset + 2].as_array().unwrap(),
516            ));
517            parsed_parms.attributes = Some(attributes);
518            offset += 2;
519        }
520
521        if dir_bitmap.contains(FPDirectoryBitmap::FINDER_INFO) {
522            let mut finder_info = [0u8; 32];
523            finder_info.copy_from_slice(&buf[offset..offset + 32]);
524            parsed_parms.finder_info = Some(finder_info);
525            offset += 32;
526        }
527
528        if dir_bitmap.contains(FPDirectoryBitmap::OWNER_ID) {
529            let owner_id = u32::from_be_bytes(*buf[offset..offset + 4].as_array().unwrap());
530            parsed_parms.owner_id = Some(owner_id);
531            offset += 4;
532        }
533
534        if dir_bitmap.contains(FPDirectoryBitmap::GROUP_ID) {
535            let group_id = u32::from_be_bytes(*buf[offset..offset + 4].as_array().unwrap());
536            parsed_parms.group_id = Some(group_id);
537            offset += 4;
538        }
539
540        if dir_bitmap.contains(FPDirectoryBitmap::ACCESS_RIGHTS) {
541            let owner_access = FPAccessRights::from(buf[offset]);
542            parsed_parms.owner_access = Some(owner_access);
543            offset += 1;
544
545            let group_access = FPAccessRights::from(buf[offset]);
546            parsed_parms.group_access = Some(group_access);
547            offset += 1;
548
549            let everyone_access = FPAccessRights::from(buf[offset]);
550            parsed_parms.everyone_access = Some(everyone_access);
551            offset += 1;
552
553            let user_access = FPAccessRights::from(buf[offset]);
554            parsed_parms.everyone_access = Some(user_access);
555        }
556
557        Ok(parsed_parms)
558    }
559}
560
561#[derive(Debug)]
562pub struct FPRead {
563    /// The Fork ID this request is wanting to read from. Must be open already.
564    pub fork_id: u16,
565    /// The offset into the fork to start reading from.
566    pub offset: u32,
567    /// The number of bytes requested to be read. Note that this can be higher than the ASP QuantumSize.
568    /// The server should truncate the response to the QuantumSize.
569    pub req_count: u32,
570    /// The newline mask to use when reading the file. If set to a non-zero value it is to be AND'd with each
571    /// byte read from the fork and the result compared to to [Self::newline_char]. If they match the read should be
572    /// terminated at this point and the server should return the number of bytes read.
573    pub newline_mask: u8,
574    /// The newline character to be searching for where to terminate the read.
575    pub newline_char: u8,
576}
577
578impl FPRead {
579    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
580        let fork_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
581        let offset = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
582        let req_count = u32::from_be_bytes(*buf[6..10].as_array().unwrap());
583        let newline_mask = buf[10];
584        let newline_char = buf[11];
585
586        Ok(Self {
587            fork_id,
588            offset,
589            req_count,
590            newline_mask,
591            newline_char,
592        })
593    }
594
595    /// Checks if a byte matches the newline mask and character. If true the read should be terminated.
596    pub fn byte_matches_newline(&self, byte: u8) -> bool {
597        (byte & self.newline_mask) == self.newline_char
598    }
599}
600
601pub struct FPFlush {
602    pub volume_id: u16,
603}
604
605impl FPFlush {
606    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
607        let volume_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
608        Ok(Self { volume_id })
609    }
610}
611
612pub struct FPGetVolParms {
613    pub volume_id: u16,
614    pub bitmap: FPVolumeBitmap,
615}
616
617impl FPGetVolParms {
618    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
619        let volume_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
620        let bitmap = FPVolumeBitmap::from(u16::from_be_bytes(*buf[2..4].as_array().unwrap()));
621        Ok(Self { volume_id, bitmap })
622    }
623}
624
625pub struct FPDelete {
626    pub volume_id: u16,
627    pub directory_id: u32,
628    pub path: MacString,
629}
630
631impl FPDelete {
632    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
633        let volume_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
634        let directory_id = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
635        let _path_type = buf[6];
636        let path = MacString::try_from(&buf[7..])?;
637
638        Ok(Self {
639            volume_id,
640            directory_id,
641            path,
642        })
643    }
644}
645
646#[derive(Debug)]
647pub struct FPAddIcon {
648    pub dt_ref_num: u16,
649    pub file_creator: [u8; 4],
650    pub file_type: [u8; 4],
651    pub icon_type: u8,
652    pub icon_tag: u32,
653    pub size: u16,
654}
655
656impl FPAddIcon {
657    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
658        if buf.len() < 18 {
659            return Err(AfpError::InvalidSize);
660        }
661        let dt_ref_num = u16::from_be_bytes(*buf[..2].as_array().unwrap());
662        let file_creator: [u8; 4] = *buf[2..6].as_array().unwrap();
663        let file_type: [u8; 4] = *buf[6..10].as_array().unwrap();
664        let icon_type = buf[10];
665        // pad byte at 11
666        let icon_tag = u32::from_be_bytes(*buf[12..16].as_array().unwrap());
667        let size = u16::from_be_bytes(*buf[16..18].as_array().unwrap());
668
669        Ok(Self {
670            dt_ref_num,
671            file_creator,
672            file_type,
673            icon_type,
674            icon_tag,
675            size,
676        })
677    }
678}
679
680#[derive(Debug)]
681pub struct FPGetIcon {
682    pub dt_ref_num: u16,
683    pub file_creator: [u8; 4],
684    pub file_type: [u8; 4],
685    pub icon_type: u8,
686    pub size: u16,
687}
688
689impl FPGetIcon {
690    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
691        let dt_ref_num = u16::from_be_bytes(*buf[..2].as_array().unwrap());
692        let file_creator: [u8; 4] = *buf[2..6].as_array().unwrap();
693        let file_type: [u8; 4] = *buf[6..10].as_array().unwrap();
694        let icon_type = buf[10];
695        // Pad byte here, skip one.
696        let size = u16::from_be_bytes(*buf[12..14].as_array().unwrap());
697
698        Ok(Self {
699            dt_ref_num,
700            file_creator,
701            file_type,
702            icon_type,
703            size,
704        })
705    }
706}
707
708#[derive(Debug)]
709pub struct FPGetIconInfo {
710    pub dt_ref_num: u16,
711    pub file_creator: [u8; 4],
712    pub icon_type: u16,
713}
714
715impl FPGetIconInfo {
716    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
717        if buf.len() < 8 {
718            return Err(AfpError::InvalidSize);
719        }
720        let dt_ref_num = u16::from_be_bytes(*buf[..2].as_array().unwrap());
721        let file_creator: [u8; 4] = *buf[2..6].as_array().unwrap();
722        // 2 byte icon type
723        let icon_type = u16::from_be_bytes(*buf[6..8].as_array().unwrap());
724
725        Ok(Self {
726            dt_ref_num,
727            file_creator,
728            icon_type,
729        })
730    }
731}
732
733#[derive(Debug)]
734pub struct FPGetComment {
735    pub dt_ref_num: u16,
736    pub directory_id: u32,
737    pub path: MacString,
738}
739
740impl FPGetComment {
741    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
742        let dt_ref_num = u16::from_be_bytes(*buf[..2].as_array().unwrap());
743        let directory_id = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
744        let _path_type = buf[6];
745        let path = MacString::try_from(&buf[7..])?;
746
747        Ok(Self {
748            dt_ref_num,
749            directory_id,
750            path,
751        })
752    }
753}
754
755#[derive(Debug)]
756pub struct FPRemoveComment {
757    pub directory_id: u32,
758    pub path: MacString,
759}
760
761impl FPRemoveComment {
762    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
763        let directory_id = u32::from_be_bytes(*buf[1..5].as_array().unwrap());
764        let _path_type = buf[5];
765        let path = MacString::try_from(&buf[6..])?;
766
767        Ok(Self { directory_id, path })
768    }
769}
770
771#[derive(Debug)]
772pub struct FPAddComment {
773    pub dt_ref_num: u16,
774    pub directory_id: u32,
775    pub path: MacString,
776    pub comment: Vec<u8>,
777}
778
779impl FPAddComment {
780    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
781        let dt_ref_num = u16::from_be_bytes(*buf[..2].as_array().unwrap());
782        let directory_id = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
783        let _path_type = buf[6];
784        let path = if buf.len() > 7 {
785            MacString::try_from(&buf[7..])?
786        } else {
787            MacString::from("")
788        };
789
790        // Comment starts after the variable length path. It must be padded to be even.
791        let mut comment_offset = 7 + path.byte_len() - 1;
792        // Commands are word-aligned, so start of comment string is at an even offset from the START of the command
793        // Since buf here starts from the command payload, we know DSI header was before it.
794        // It's safer to just skip padding dynamically.
795        if comment_offset % 2 != 0 {
796            comment_offset += 1;
797        }
798
799        let comment_data = if comment_offset < buf.len() {
800            let comment_len = buf[comment_offset] as usize;
801            if comment_len > 0 && comment_offset + 1 + comment_len <= buf.len() {
802                buf[comment_offset + 1..comment_offset + 1 + comment_len].to_vec()
803            } else {
804                vec![]
805            }
806        } else {
807            vec![]
808        };
809
810        Ok(Self {
811            dt_ref_num,
812            directory_id,
813            path,
814            comment: comment_data,
815        })
816    }
817}
818
819#[derive(Debug)]
820pub struct FPWrite {
821    pub fork_id: u16,
822    pub offset: u32,
823    pub req_count: u32,
824    pub start_end_flag: bool,
825}
826
827impl FPWrite {
828    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
829        if buf.len() < 10 {
830            return Err(AfpError::InvalidSize);
831        }
832        let fork_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
833        let offset = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
834        let req_count_raw = u32::from_be_bytes(*buf[6..10].as_array().unwrap());
835
836        // High bit of req_count is the Start/End flag
837        let start_end_flag = (req_count_raw & 0x8000_0000) != 0;
838        let req_count = req_count_raw & 0x7FFF_FFFF;
839
840        Ok(Self {
841            fork_id,
842            offset,
843            req_count,
844            start_end_flag,
845        })
846    }
847}
848
849/// Indicates a request from a client to either increase or decrease the size of a fork on disk. If neither
850/// data fork length or resource fork length are set, this command is a no-op but a success code should
851/// still be returned to the client.
852#[derive(Debug)]
853pub struct FPSetForkParms {
854    /// which fork ref this command is for
855    pub fork_ref_num: u16,
856    /// the file bitmap describing what arguments will be set. _only_ fork length is allowed to be set.
857    pub file_bitmap: FPFileBitmap,
858    /// Requested new data fork length value, if the data fork length bit was set in the bitmap.
859    pub data_fork_length: Option<u32>,
860    /// Requested new resource fork length value, if the resource fork length bit was set in the bitmap.
861    pub resource_fork_length: Option<u32>,
862}
863
864impl FPSetForkParms {
865    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
866        let fork_ref_num = u16::from_be_bytes(*buf[..2].as_array().unwrap());
867        let file_bitmap = FPFileBitmap::from(u16::from_be_bytes(*buf[2..4].as_array().unwrap()));
868
869        let mut offset = 4;
870
871        let data_fork_length = if file_bitmap.contains(FPFileBitmap::DATA_FORK_LENGTH) {
872            if offset + 4 > buf.len() {
873                return Err(AfpError::InvalidSize);
874            }
875            let val = u32::from_be_bytes(*buf[offset..offset + 4].as_array().unwrap());
876            offset += 4;
877            Some(val)
878        } else {
879            None
880        };
881
882        let resource_fork_length = if file_bitmap.contains(FPFileBitmap::RESOURCE_FORK_LENGTH) {
883            if offset + 4 > buf.len() {
884                return Err(AfpError::InvalidSize);
885            }
886            let val = u32::from_be_bytes(*buf[offset..offset + 4].as_array().unwrap());
887            Some(val)
888        } else {
889            None
890        };
891
892        Ok(Self {
893            fork_ref_num,
894            file_bitmap,
895            data_fork_length,
896            resource_fork_length,
897        })
898    }
899}
900
901#[cfg(test)]
902mod tests {
903    use super::*;
904
905    #[test]
906    fn test_fp_enumerate_parse() {
907        let buf = &[
908            0x9u8, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x4, 0x7, 0x7f, 0x13, 0x7f, 0x0, 0x45, 0x0, 0x1,
909            0x12, 0x0, 0x2, 0x0,
910        ];
911        let _enumerate = FPEnumerate::parse(&buf[2..]).unwrap();
912    }
913
914    #[test]
915    fn test_fp_rename_parse() {
916        // FPRename: rename "old.txt" to "new.txt" in directory 2.
917        #[rustfmt::skip]
918        let raw: &[u8] = &[
919            0x1c, 0x00,             // command=28, pad
920            0x00, 0x01,             // volume_id=1
921            0x00, 0x00, 0x00, 0x02, // directory_id=2
922            0x02,                   // path_type=LongName
923            0x07,                   // path len=7
924            b'o', b'l', b'd', b'.', b't', b'x', b't', // "old.txt"
925            0x02,                   // new_name_path_type=LongName
926            0x07,                   // new_name len=7
927            b'n', b'e', b'w', b'.', b't', b'x', b't', // "new.txt"
928        ];
929
930        let cmd = FPRename::parse(&raw[2..]).expect("parse should succeed");
931
932        assert_eq!(cmd.volume_id, 1);
933        assert_eq!(cmd.directory_id, 2);
934        assert_eq!(cmd.path_type, PathType::LongName);
935        assert_eq!(cmd.path.as_str(), "old.txt");
936        assert_eq!(cmd.new_name_path_type, PathType::LongName);
937        assert_eq!(cmd.new_name.as_str(), "new.txt");
938    }
939
940    #[test]
941    fn test_fp_move_and_rename_parse() {
942        // Real packet captured from Mac Finder via Wireshark.
943        // FPMoveAndRename: move "appleshare.smi.bin" from DID=2 to DID=13, no rename.
944        #[rustfmt::skip]
945        let raw: &[u8] = &[
946            0x17, 0x00,             // command=23, pad
947            0x00, 0x01,             // volume_id=1
948            0x00, 0x00, 0x00, 0x02, // src_directory_id=2
949            0x00, 0x00, 0x00, 0x0d, // dst_directory_id=13
950            0x02,                   // src_path_type=LongName
951            0x12,                   // src_path len=18
952            b'a', b'p', b'p', b'l', b'e', b's', b'h', b'a', b'r', b'e',
953            b'.', b's', b'm', b'i', b'.', b'b', b'i', b'n', // "appleshare.smi.bin"
954            0x02,                   // dst_path_type=LongName
955            0x00,                   // dst_path len=0 (empty)
956            0x02,                   // new_name_path_type=LongName
957            0x00,                   // new_name len=0 (empty, keep original name)
958        ];
959
960        // Server passes buf[2..] (skipping command byte + pad) to parse.
961        let cmd = FPMoveAndRename::parse(&raw[2..]).expect("parse should succeed");
962
963        assert_eq!(cmd.volume_id, 1);
964        assert_eq!(cmd.src_directory_id, 2);
965        assert_eq!(cmd.dst_directory_id, 13);
966        assert_eq!(cmd.src_path_type, PathType::LongName);
967        assert_eq!(cmd.src_path.as_str(), "appleshare.smi.bin");
968        assert_eq!(cmd.dst_path_type, PathType::LongName);
969        assert_eq!(cmd.dst_path.as_str(), "");
970        assert_eq!(cmd.new_name_path_type, PathType::LongName);
971        assert_eq!(cmd.new_name.as_str(), "");
972    }
973}
974
975#[derive(Debug)]
976pub struct FPOpenDT {
977    pub volume_id: u16,
978}
979
980impl FPOpenDT {
981    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
982        if buf.len() < 2 {
983            return Err(AfpError::InvalidSize);
984        }
985        let volume_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
986        Ok(Self { volume_id })
987    }
988}
989
990#[derive(Debug)]
991pub struct FPGetForkParms {
992    pub fork_id: u16,
993    pub file_bitmap: FPFileBitmap,
994}
995
996impl FPGetForkParms {
997    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
998        if buf.len() < 4 {
999            return Err(AfpError::InvalidSize);
1000        }
1001        let fork_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
1002        let file_bitmap = FPFileBitmap::from(u16::from_be_bytes(*buf[2..4].as_array().unwrap()));
1003        Ok(Self { fork_id, file_bitmap })
1004    }
1005}
1006
1007#[derive(Debug)]
1008pub struct FPCreateDir {
1009    pub volume_id: u16,
1010    pub directory_id: u32,
1011    pub path_type: PathType,
1012    pub path: MacString,
1013}
1014
1015impl FPCreateDir {
1016    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
1017        if buf.len() < 8 {
1018            return Err(AfpError::InvalidSize);
1019        }
1020        let volume_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
1021        let directory_id = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
1022        let path_type = PathType::from(buf[6]);
1023        let path = MacString::try_from(&buf[7..])?;
1024        Ok(Self { volume_id, directory_id, path_type, path })
1025    }
1026}
1027
1028#[derive(Debug)]
1029pub struct FPCreateFile {
1030    pub create_flag: CreateFlag,
1031    pub volume_id: u16,
1032    pub directory_id: u32,
1033    pub path_type: PathType,
1034    pub path: MacString,
1035}
1036
1037impl FPCreateFile {
1038    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
1039        // Note: wire layout differs from Inside AppleTalk docs. Observed order from real client:
1040        // [0]=create_flag, [1..3]=volume_id, [3..7]=directory_id, [7]=path_type, [8..]=path
1041        if buf.len() < 9 {
1042            return Err(AfpError::InvalidSize);
1043        }
1044        let create_flag = CreateFlag::from(buf[0]);
1045        let volume_id = u16::from_be_bytes(*buf[1..3].as_array().unwrap());
1046        let directory_id = u32::from_be_bytes(*buf[3..7].as_array().unwrap());
1047        let path_type = PathType::from(buf[7]);
1048        let path = MacString::try_from(&buf[8..])?;
1049        Ok(Self { create_flag, volume_id, directory_id, path_type, path })
1050    }
1051}
1052
1053#[derive(Debug)]
1054pub struct FPOpenFork {
1055    pub volume_id: u16,
1056    pub directory_id: u32,
1057    pub file_bitmap: FPFileBitmap,
1058    pub access_mode: u16,
1059    pub path_type: PathType,
1060    pub path: MacString,
1061}
1062
1063impl FPOpenFork {
1064    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
1065        if buf.len() < 12 {
1066            return Err(AfpError::InvalidSize);
1067        }
1068        let volume_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
1069        let directory_id = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
1070        let file_bitmap = FPFileBitmap::from(u16::from_be_bytes(*buf[6..8].as_array().unwrap()));
1071        let access_mode = u16::from_be_bytes(*buf[8..10].as_array().unwrap());
1072        let path_type = PathType::from(buf[10]);
1073        let path = MacString::try_from(&buf[11..])?;
1074        Ok(Self { volume_id, directory_id, file_bitmap, access_mode, path_type, path })
1075    }
1076}
1077
1078#[derive(Debug)]
1079pub struct FPGetFileDirParms {
1080    pub volume_id: u16,
1081    pub directory_id: u32,
1082    pub file_bitmap: FPFileBitmap,
1083    pub dir_bitmap: FPDirectoryBitmap,
1084    pub path_type: PathType,
1085    pub path: MacString,
1086}
1087
1088impl FPGetFileDirParms {
1089    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
1090        if buf.len() < 12 {
1091            return Err(AfpError::InvalidSize);
1092        }
1093        let volume_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
1094        let directory_id = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
1095        let file_bitmap = FPFileBitmap::from(u16::from_be_bytes(*buf[6..8].as_array().unwrap()));
1096        let dir_bitmap = FPDirectoryBitmap::from(u16::from_be_bytes(*buf[8..10].as_array().unwrap()));
1097        let path_type = PathType::from(buf[10]);
1098        let path = MacString::try_from(&buf[11..])?;
1099        Ok(Self { volume_id, directory_id, file_bitmap, dir_bitmap, path_type, path })
1100    }
1101}
1102
1103#[derive(Debug)]
1104pub struct FPSetFileDirParms {
1105    pub volume_id: u16,
1106    pub directory_id: u32,
1107    /// Single bitmap governing both file and directory changes; common fields share bit positions.
1108    pub file_bitmap: FPFileBitmap,
1109    pub path_type: PathType,
1110    pub path: MacString,
1111    pub params: Vec<u8>,
1112}
1113
1114impl FPSetFileDirParms {
1115    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
1116        if buf.len() < 10 {
1117            return Err(AfpError::InvalidSize);
1118        }
1119        let volume_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
1120        let directory_id = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
1121        let file_bitmap = FPFileBitmap::from(u16::from_be_bytes(*buf[6..8].as_array().unwrap()));
1122        let path_type = PathType::from(buf[8]);
1123        let path = MacString::try_from(&buf[9..])?;
1124        let mut param_offset = 9 + path.byte_len();
1125        if param_offset % 2 != 0 {
1126            param_offset += 1;
1127        }
1128        let params = buf[param_offset..].to_vec();
1129        Ok(Self { volume_id, directory_id, file_bitmap, path_type, path, params })
1130    }
1131}
1132
1133/// FPRename: renames a file or directory within its current parent directory.
1134///
1135/// Wire layout (from buf[2..] — after command byte and pad):
1136///   [0..2]  VolumeID
1137///   [2..6]  DirectoryID (parent directory of the object)
1138///   [6]     PathType
1139///   [7..]   Path (Pascal string — identifies the object to rename)
1140///   [7+path_len] NewNamePathType
1141///   [7+path_len+1..] NewName (Pascal string — the new name)
1142#[derive(Debug)]
1143pub struct FPRename {
1144    pub volume_id: u16,
1145    pub directory_id: u32,
1146    pub path_type: PathType,
1147    pub path: MacString,
1148    pub new_name_path_type: PathType,
1149    pub new_name: MacString,
1150}
1151
1152impl FPRename {
1153    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
1154        if buf.len() < 8 {
1155            return Err(AfpError::InvalidSize);
1156        }
1157        let volume_id = u16::from_be_bytes(*buf[..2].as_array().unwrap());
1158        let directory_id = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
1159        let path_type = PathType::from(buf[6]);
1160        let path = MacString::try_from(&buf[7..])?;
1161
1162        let new_name_type_offset = 7 + path.byte_len();
1163        if new_name_type_offset >= buf.len() {
1164            return Err(AfpError::InvalidSize);
1165        }
1166        let new_name_path_type = PathType::from(buf[new_name_type_offset]);
1167        let new_name = MacString::try_from(&buf[new_name_type_offset + 1..])?;
1168
1169        Ok(Self {
1170            volume_id,
1171            directory_id,
1172            path_type,
1173            path,
1174            new_name_path_type,
1175            new_name,
1176        })
1177    }
1178}
1179
1180/// FPMoveAndRename: atomically moves and/or renames a file or directory.
1181///
1182/// Wire layout (from buf[2..] — after command byte and pad):
1183///   [0..2]  VolumeID
1184///   [2..6]  SourceDirectoryID
1185///   [6..10] DestinationDirectoryID
1186///   [10]    SourcePathType
1187///   [11..]  SourcePath (Pascal string)
1188///   [11+src_len] DestinationPathType
1189///   [11+src_len+1..] DestinationPath (Pascal string)
1190///   [after dst] NewNamePathType
1191///   [after dst+1] NewName (Pascal string — zero-length means keep original name)
1192#[derive(Debug)]
1193pub struct FPMoveAndRename {
1194    pub volume_id: u16,
1195    pub src_directory_id: u32,
1196    pub dst_directory_id: u32,
1197    pub src_path_type: PathType,
1198    pub src_path: MacString,
1199    pub dst_path_type: PathType,
1200    pub dst_path: MacString,
1201    pub new_name_path_type: PathType,
1202    /// New name for the object. Empty string means keep the original name.
1203    pub new_name: MacString,
1204}
1205
1206impl FPMoveAndRename {
1207    pub fn parse(buf: &[u8]) -> Result<Self, AfpError> {
1208        if buf.len() < 12 {
1209            return Err(AfpError::InvalidSize);
1210        }
1211        let volume_id = u16::from_be_bytes(*buf[0..2].as_array().unwrap());
1212        let src_directory_id = u32::from_be_bytes(*buf[2..6].as_array().unwrap());
1213        let dst_directory_id = u32::from_be_bytes(*buf[6..10].as_array().unwrap());
1214        let src_path_type = PathType::from(buf[10]);
1215        let src_path = MacString::try_from(&buf[11..])?;
1216
1217        let dst_type_offset = 11 + src_path.byte_len();
1218        if dst_type_offset >= buf.len() {
1219            return Err(AfpError::InvalidSize);
1220        }
1221        let dst_path_type = PathType::from(buf[dst_type_offset]);
1222        let dst_path = MacString::try_from(&buf[dst_type_offset + 1..])?;
1223
1224        // NewName has its own type byte prefix, just like src/dst paths.
1225        // The whole NewName section is absent when the client sends a pure move.
1226        let new_name_type_offset = dst_type_offset + 1 + dst_path.byte_len();
1227        let (new_name_path_type, new_name) = if new_name_type_offset < buf.len() {
1228            let new_name_path_type = PathType::from(buf[new_name_type_offset]);
1229            let new_name = if new_name_type_offset + 1 < buf.len() {
1230                MacString::try_from(&buf[new_name_type_offset + 1..])?
1231            } else {
1232                MacString::new(String::new())
1233            };
1234            (new_name_path_type, new_name)
1235        } else {
1236            (PathType::LongName, MacString::new(String::new()))
1237        };
1238
1239        Ok(Self {
1240            volume_id,
1241            src_directory_id,
1242            dst_directory_id,
1243            src_path_type,
1244            src_path,
1245            dst_path_type,
1246            dst_path,
1247            new_name_path_type,
1248            new_name,
1249        })
1250    }
1251}