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