1use byteorder::{BigEndian, ByteOrder};
14#[cfg(feature = "rust173-polyfill")]
15use disk_core::rust173::IntIsMultipleOf as _;
16
17pub const IS_ALIAS_FLAG: u16 = 0x8000;
19
20pub const SLNK_TYPE: &str = "slnk";
22pub const RHAP_CREATOR: &str = "rhap";
24
25fn read_pascal_string(field: &[u8]) -> String {
29 if field.is_empty() {
30 return String::new();
31 }
32 let len = field[0] as usize;
33 let max = field.len().saturating_sub(1).min(len);
34 let mut out = String::with_capacity(max);
35 for &b in &field[1..1 + max] {
36 if b < 0x80 {
37 out.push(b as char);
38 } else {
39 out.push('?');
40 }
41 }
42 out
43}
44
45pub fn parse_alis_record(data: &[u8]) -> Option<String> {
50 if data.len() < 114 {
53 return None;
54 }
55
56 let volume = read_pascal_string(&data[10..38]);
58 let target = read_pascal_string(&data[50..114]);
60
61 let abs_path = if data.len() > 150 {
63 scan_tagged_path(&data[150..])
64 } else {
65 None
66 };
67
68 match (abs_path, volume.is_empty(), target.is_empty()) {
69 (Some(p), _, _) if !p.is_empty() => Some(p),
70 (_, true, true) => None,
71 (_, true, false) => Some(target),
72 (_, false, true) => Some(format!("{volume}:")),
73 (_, false, false) => Some(format!("{volume}:{target}")),
74 }
75}
76
77fn scan_tagged_path(tags: &[u8]) -> Option<String> {
81 let mut i = 0;
82 while i + 4 <= tags.len() {
83 let tag = BigEndian::read_u16(&tags[i..i + 2]);
84 let len = BigEndian::read_u16(&tags[i + 2..i + 4]) as usize;
85 if tag == 0xFFFF {
86 return None;
87 }
88 let data_start = i + 4;
89 let data_end = data_start.checked_add(len)?;
90 if data_end > tags.len() {
91 return None;
92 }
93 match tag {
94 18 => {
96 if let Ok(s) = std::str::from_utf8(&tags[data_start..data_end]) {
97 let trimmed = s.trim_end_matches('\0');
98 if !trimmed.is_empty() {
99 return Some(trimmed.to_string());
100 }
101 }
102 }
103 2 => {
106 let mut out = String::with_capacity(len);
107 for &b in &tags[data_start..data_end] {
108 if b == 0 {
109 break;
110 } else if b < 0x80 {
111 out.push(b as char);
112 } else {
113 out.push('?');
114 }
115 }
116 if !out.is_empty() {
117 return Some(out);
118 }
119 }
120 _ => {}
121 }
122 let padded = (len + 1) & !1;
124 i = data_start + padded;
125 }
126 None
127}
128
129pub fn find_alis_in_resource_fork(rsrc: &[u8]) -> Option<Vec<u8>> {
144 if rsrc.len() < 16 {
145 return None;
146 }
147 let data_off = BigEndian::read_u32(&rsrc[0..4]) as usize;
148 let map_off = BigEndian::read_u32(&rsrc[4..8]) as usize;
149 let _data_len = BigEndian::read_u32(&rsrc[8..12]) as usize;
150 let map_len = BigEndian::read_u32(&rsrc[12..16]) as usize;
151
152 if map_off + map_len > rsrc.len() || map_off + 30 > rsrc.len() {
153 return None;
154 }
155 let map = &rsrc[map_off..map_off + map_len.min(rsrc.len() - map_off)];
156 if map.len() < 30 {
157 return None;
158 }
159
160 let type_list_off = BigEndian::read_u16(&map[24..26]) as usize;
163 if type_list_off + 2 > map.len() {
164 return None;
165 }
166
167 let num_types = BigEndian::read_u16(&map[type_list_off..type_list_off + 2]).wrapping_add(1);
168 let types_start = type_list_off + 2;
169
170 for t in 0..num_types as usize {
171 let te = types_start + t * 8;
172 if te + 8 > map.len() {
173 return None;
174 }
175 let four_cc = &map[te..te + 4];
176 let count = BigEndian::read_u16(&map[te + 4..te + 6]).wrapping_add(1);
177 let ref_list_off = BigEndian::read_u16(&map[te + 6..te + 8]) as usize;
178
179 if four_cc == b"alis" && count > 0 {
180 let re = type_list_off + ref_list_off;
182 if re + 12 > map.len() {
183 return None;
184 }
185 let data_rel = ((map[re + 5] as usize) << 16)
187 | ((map[re + 6] as usize) << 8)
188 | (map[re + 7] as usize);
189 let abs = data_off + data_rel;
190 if abs + 4 > rsrc.len() {
191 return None;
192 }
193 let res_len = BigEndian::read_u32(&rsrc[abs..abs + 4]) as usize;
194 let res_start = abs + 4;
195 if res_start + res_len > rsrc.len() {
196 return None;
197 }
198 return Some(rsrc[res_start..res_start + res_len].to_vec());
199 }
200 }
201
202 None
203}
204
205pub fn resolve_alias_target(rsrc: &[u8]) -> Option<String> {
208 let alis = find_alis_in_resource_fork(rsrc)?;
209 parse_alis_record(&alis)
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn build_pascal_field(s: &str, field_len: usize) -> Vec<u8> {
217 let mut out = vec![0u8; field_len];
218 let bytes = s.as_bytes();
219 let n = bytes.len().min(field_len - 1).min(u8::MAX as usize);
220 out[0] = n as u8;
221 out[1..1 + n].copy_from_slice(&bytes[..n]);
222 out
223 }
224
225 fn build_alis_record(volume: &str, target: &str, extra_tags: &[u8]) -> Vec<u8> {
226 let mut rec = vec![0u8; 150];
227 rec[10..38].copy_from_slice(&build_pascal_field(volume, 28));
229 rec[50..114].copy_from_slice(&build_pascal_field(target, 64));
231 rec.extend_from_slice(extra_tags);
232 rec
233 }
234
235 #[test]
236 fn parse_alis_minimal_volume_and_target() {
237 let rec = build_alis_record("MyDisk", "ReadMe", &[]);
238 assert_eq!(parse_alis_record(&rec).as_deref(), Some("MyDisk:ReadMe"));
239 }
240
241 #[test]
242 fn parse_alis_tag_18_utf8_path_wins() {
243 let mut tags = Vec::new();
244 tags.extend_from_slice(&18u16.to_be_bytes()); let path = b"/Volumes/MyDisk/Apps/MyApp.app";
246 tags.extend_from_slice(&(path.len() as u16).to_be_bytes());
247 tags.extend_from_slice(path);
248 if !path.len().is_multiple_of(2) {
249 tags.push(0);
250 }
251 tags.extend_from_slice(&0xFFFFu16.to_be_bytes());
252 tags.extend_from_slice(&0u16.to_be_bytes());
253
254 let rec = build_alis_record("MyDisk", "MyApp.app", &tags);
255 assert_eq!(
256 parse_alis_record(&rec).as_deref(),
257 Some("/Volumes/MyDisk/Apps/MyApp.app")
258 );
259 }
260
261 #[test]
262 fn parse_alis_rejects_short() {
263 assert!(parse_alis_record(&[0u8; 50]).is_none());
264 }
265
266 #[test]
267 fn parse_alis_empty_volume_and_target() {
268 let rec = build_alis_record("", "", &[]);
269 assert!(parse_alis_record(&rec).is_none());
270 }
271
272 #[test]
273 fn find_alis_in_minimal_rsrc() {
274 let alis_body = b"hello-alias";
281 let res_data_start = 16;
282 let res_len_prefix = 4;
283 let data_section_len = res_len_prefix + alis_body.len();
284
285 let mut buf = vec![0u8; 16];
286 buf.extend_from_slice(&(alis_body.len() as u32).to_be_bytes());
288 buf.extend_from_slice(alis_body);
289
290 let map_off = buf.len();
291
292 let map_header_size = 28;
300 let type_list_off_in_map = map_header_size;
301 let ref_list_off_from_type_list = 2 + 8; let mut map = vec![0u8; map_header_size];
305 map[24] = (type_list_off_in_map >> 8) as u8;
306 map[25] = (type_list_off_in_map & 0xFF) as u8;
307 map[26] = 0xFF;
309 map[27] = 0xFF;
310 map.extend_from_slice(&[0, 0]);
312 map.extend_from_slice(b"alis");
314 map.extend_from_slice(&[0, 0]); map.extend_from_slice(&(ref_list_off_from_type_list as u16).to_be_bytes());
316 map.extend_from_slice(&[0, 0]); map.extend_from_slice(&[0xFF, 0xFF]); let data_rel: u32 = 0; map.push(0); map.push(((data_rel >> 16) & 0xFF) as u8);
322 map.push(((data_rel >> 8) & 0xFF) as u8);
323 map.push((data_rel & 0xFF) as u8);
324 map.extend_from_slice(&[0, 0, 0, 0]); let map_len = map.len();
327
328 BigEndian::write_u32(&mut buf[0..4], res_data_start as u32);
330 BigEndian::write_u32(&mut buf[4..8], map_off as u32);
331 BigEndian::write_u32(&mut buf[8..12], data_section_len as u32);
332 BigEndian::write_u32(&mut buf[12..16], map_len as u32);
333
334 buf.extend_from_slice(&map);
335
336 let found = find_alis_in_resource_fork(&buf).expect("alis should be found");
337 assert_eq!(found, alis_body);
338 }
339}