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