1use byteorder::{BigEndian, ByteOrder};
2#[cfg(feature = "rust173-polyfill")]
3use disk_core::rust173::IntIsMultipleOf as _;
4use std::cmp::Ordering;
5use std::io::{Read, Seek, SeekFrom, Write};
6
7use crate::hfs_common::{
8 self, bitmap_clear_bit_be, bitmap_collect_clear_runs_be, bitmap_find_clear_run_be,
9 bitmap_set_bit_be, bitmap_test_bit_be, btree_free_node, btree_grow_blocks,
10 btree_map_chain_tail, btree_refresh_index_keys, btree_remove_record, BTreeHeader,
11 BTreeKeyFormat,
12};
13use disk_core::entry::{EntryType, FileEntry};
14use disk_core::filesystem::CompactResult;
15use disk_core::filesystem::{
16 CreateDirectoryOptions, CreateFileOptions, EditableFilesystem, Filesystem, FilesystemError,
17};
18
19const HFS_SIGNATURE: u16 = 0x4244;
20const HFS_PLUS_EMBEDDED_SIGNATURE: u16 = 0x482B;
21
22static MAC_ROMAN_TABLE: [char; 128] = [
24 '\u{00C4}', '\u{00C5}', '\u{00C7}', '\u{00C9}', '\u{00D1}', '\u{00D6}', '\u{00DC}', '\u{00E1}',
25 '\u{00E0}', '\u{00E2}', '\u{00E4}', '\u{00E3}', '\u{00E5}', '\u{00E7}', '\u{00E9}', '\u{00E8}',
26 '\u{00EA}', '\u{00EB}', '\u{00ED}', '\u{00EC}', '\u{00EE}', '\u{00EF}', '\u{00F1}', '\u{00F3}',
27 '\u{00F2}', '\u{00F4}', '\u{00F6}', '\u{00F5}', '\u{00FA}', '\u{00F9}', '\u{00FB}', '\u{00FC}',
28 '\u{2020}', '\u{00B0}', '\u{00A2}', '\u{00A3}', '\u{00A7}', '\u{2022}', '\u{00B6}', '\u{00DF}',
29 '\u{00AE}', '\u{00A9}', '\u{2122}', '\u{00B4}', '\u{00A8}', '\u{2260}', '\u{00C6}', '\u{00D8}',
30 '\u{221E}', '\u{00B1}', '\u{2264}', '\u{2265}', '\u{00A5}', '\u{00B5}', '\u{2202}', '\u{2211}',
31 '\u{220F}', '\u{03C0}', '\u{222B}', '\u{00AA}', '\u{00BA}', '\u{03A9}', '\u{00E6}', '\u{00F8}',
32 '\u{00BF}', '\u{00A1}', '\u{00AC}', '\u{221A}', '\u{0192}', '\u{2248}', '\u{2206}', '\u{00AB}',
33 '\u{00BB}', '\u{2026}', '\u{00A0}', '\u{00C0}', '\u{00C3}', '\u{00D5}', '\u{0152}', '\u{0153}',
34 '\u{2013}', '\u{2014}', '\u{201C}', '\u{201D}', '\u{2018}', '\u{2019}', '\u{00F7}', '\u{25CA}',
35 '\u{00FF}', '\u{0178}', '\u{2044}', '\u{20AC}', '\u{2039}', '\u{203A}', '\u{FB01}', '\u{FB02}',
36 '\u{2021}', '\u{00B7}', '\u{201A}', '\u{201E}', '\u{2030}', '\u{00C2}', '\u{00CA}', '\u{00C1}',
37 '\u{00CB}', '\u{00C8}', '\u{00CD}', '\u{00CE}', '\u{00CF}', '\u{00CC}', '\u{00D3}', '\u{00D4}',
38 '\u{F8FF}', '\u{00D2}', '\u{00DA}', '\u{00DB}', '\u{00D9}', '\u{0131}', '\u{02C6}', '\u{02DC}',
39 '\u{00AF}', '\u{02D8}', '\u{02D9}', '\u{02DA}', '\u{00B8}', '\u{02DD}', '\u{02DB}', '\u{02C7}',
40];
41
42pub fn utf8_to_mac_roman(s: &str) -> Result<Vec<u8>, FilesystemError> {
45 let mut result = Vec::with_capacity(s.len());
46 for ch in s.chars() {
47 if (ch as u32) < 0x80 {
48 result.push(ch as u8);
49 } else {
50 let mut found = false;
52 for (i, &table_char) in MAC_ROMAN_TABLE.iter().enumerate() {
53 if table_char == ch {
54 result.push((i as u8) + 0x80);
55 found = true;
56 break;
57 }
58 }
59 if !found {
60 return Err(FilesystemError::InvalidData(format!(
61 "character '{}' (U+{:04X}) cannot be encoded in Mac Roman",
62 ch, ch as u32
63 )));
64 }
65 }
66 }
67 Ok(result)
68}
69
70pub fn mac_roman_to_utf8(data: &[u8]) -> String {
72 data.iter()
73 .map(|&b| {
74 if b < 0x80 {
75 b as char
76 } else {
77 MAC_ROMAN_TABLE[(b - 0x80) as usize]
78 }
79 })
80 .collect()
81}
82
83pub fn decode_mac_filename(data: &[u8]) -> String {
97 if data.iter().any(|&b| b >= 0x80) {
98 if let Ok(s) = std::str::from_utf8(data) {
99 return s.to_string();
100 }
101 }
102 mac_roman_to_utf8(data)
103}
104
105pub fn format_ostype(code: &[u8; 4]) -> String {
112 code.iter()
113 .map(|&b| match b {
114 0x00..=0x1f => ' ',
115 b if b < 0x80 => b as char,
116 b => MAC_ROMAN_TABLE[(b - 0x80) as usize],
117 })
118 .collect::<String>()
119 .trim_end()
120 .to_string()
121}
122
123fn validate_hfs_create_name(name: &str) -> Result<Vec<u8>, FilesystemError> {
127 if name.is_empty() {
128 return Err(FilesystemError::InvalidData(
129 "filename is empty — pick a non-blank name".into(),
130 ));
131 }
132 if name.contains(':') {
133 return Err(FilesystemError::InvalidData(
134 "filename contains ':', which classic Mac OS uses as a path separator — \
135 rename the file (try '-' or '_' instead)"
136 .into(),
137 ));
138 }
139 let name_bytes = utf8_to_mac_roman(name).map_err(|_| {
140 FilesystemError::InvalidData(
141 "filename contains characters that can't be encoded in Mac Roman \
142 (HFS predates Unicode) — rename using plain ASCII or common accented letters"
143 .into(),
144 )
145 })?;
146 if name_bytes.is_empty() {
147 return Err(FilesystemError::InvalidData(
148 "filename is empty — pick a non-blank name".into(),
149 ));
150 }
151 if name_bytes.len() > 31 {
152 return Err(FilesystemError::InvalidData(format!(
153 "filename is too long ({} chars); classic HFS allows up to 31 — shorten the name",
154 name_bytes.len()
155 )));
156 }
157 Ok(name_bytes)
158}
159
160fn hfs_fragmentation_stats(catalog_data: &[u8]) -> disk_core::filesystem::FragmentationStats {
169 use crate::hfs_common::walk_leaf_records;
170
171 let mut stats = disk_core::filesystem::FragmentationStats::default();
172 if catalog_data.len() < 34 {
173 return stats;
174 }
175 let node_size = BigEndian::read_u16(&catalog_data[32..34]) as usize;
176 let first_leaf = BigEndian::read_u32(&catalog_data[24..28]);
177 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE || first_leaf == 0 {
178 return stats;
179 }
180 walk_leaf_records::<(), _>(
181 catalog_data,
182 first_leaf,
183 node_size,
184 |_node_idx, _rec_idx, _abs_off, rec| {
185 if rec.is_empty() {
186 return None;
187 }
188 let key_len = rec[0] as usize;
189 let mut data_off = 1 + key_len;
190 if !data_off.is_multiple_of(2) {
191 data_off += 1;
192 }
193 if data_off + 102 > rec.len() {
194 return None;
195 }
196 if rec[data_off] as i8 != CATALOG_FILE {
197 return None;
198 }
199 let body = &rec[data_off..];
200 let phys_bytes = BigEndian::read_u32(&body[30..34]);
201 if phys_bytes == 0 {
202 return None;
203 }
204 let mut inline_extents: u32 = 0;
205 let mut inline_blocks: u32 = 0;
206 for j in 0..3 {
207 let count = BigEndian::read_u16(&body[74 + j * 4 + 2..74 + j * 4 + 4]);
208 if count > 0 {
209 inline_extents += 1;
210 inline_blocks = inline_blocks.saturating_add(count as u32);
211 }
212 }
213 stats.files_with_data += 1;
214 let overflows = inline_blocks == 0 && phys_bytes > 0;
221 if inline_extents > 1 || overflows {
222 stats.fragmented_files += 1;
223 }
224 None
225 },
226 );
227 stats
228}
229
230fn is_catalog_uninitialized(catalog_data: &[u8]) -> bool {
231 if catalog_data.len() < 34 {
232 return true;
233 }
234 BigEndian::read_u16(&catalog_data[32..34]) == 0
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240enum HfsTree {
241 Catalog,
242 Extents,
243}
244
245impl HfsTree {
246 fn label(self) -> &'static str {
247 match self {
248 HfsTree::Catalog => "catalog",
249 HfsTree::Extents => "extents-overflow",
250 }
251 }
252}
253
254#[derive(Debug, Clone, Copy)]
256pub struct HfsExtDescriptor {
257 pub start_block: u16,
258 pub block_count: u16,
259}
260
261#[derive(Debug, Clone, Copy)]
266pub struct HfsFileLocation {
267 pub offset_in_image: u64,
268 pub length: u64,
269 pub fragmented: bool,
270}
271
272#[derive(Debug, Clone)]
277pub struct HfsVolumeSummary {
278 pub volume_name: String,
279 pub block_size: u32,
280 pub total_blocks: u16,
281 pub free_blocks: u16,
282 pub used_bytes: u64,
283 pub file_count: u32,
284 pub folder_count: u32,
285 pub catalog_file_size: u32,
286 pub extents_file_size: u32,
287}
288
289impl HfsExtDescriptor {
290 pub fn parse(data: &[u8]) -> Self {
291 HfsExtDescriptor {
292 start_block: BigEndian::read_u16(&data[0..2]),
293 block_count: BigEndian::read_u16(&data[2..4]),
294 }
295 }
296
297 fn serialize(&self, out: &mut [u8]) {
298 BigEndian::write_u16(&mut out[0..2], self.start_block);
299 BigEndian::write_u16(&mut out[2..4], self.block_count);
300 }
301}
302
303#[derive(Debug, Clone)]
305#[allow(dead_code)]
306pub(crate) struct HfsMasterDirectoryBlock {
307 pub(crate) signature: u16,
308 pub(crate) create_date: u32,
309 pub(crate) modify_date: u32,
310 pub(crate) total_blocks: u16,
311 pub(crate) block_size: u32,
312 pub(crate) free_blocks: u16,
313 pub(crate) volume_name: String,
314 pub(crate) volume_name_raw: Vec<u8>,
315 pub(crate) volume_bitmap_block: u16,
316 pub(crate) first_alloc_block: u16,
318 pub(crate) next_catalog_id: u32,
319 pub(crate) file_count: u32,
320 pub(crate) folder_count: u32,
321 pub(crate) finder_info: [u32; 8],
322 pub(crate) catalog_file_size: u32,
323 pub(crate) catalog_file_extents: [HfsExtDescriptor; 3],
324 pub(crate) extents_file_size: u32,
325 pub(crate) extents_file_extents: [HfsExtDescriptor; 3],
326 pub(crate) embedded_signature: u16,
327 pub(crate) embedded_start_block: u16,
328 pub(crate) embedded_block_count: u16,
329 pub(crate) raw_sector: [u8; 512],
331}
332
333impl HfsMasterDirectoryBlock {
334 fn parse(data: &[u8]) -> Result<Self, FilesystemError> {
335 if data.len() < 162 {
336 return Err(FilesystemError::Parse("MDB too short".into()));
337 }
338 let sig = BigEndian::read_u16(&data[0..2]);
339 if sig != HFS_SIGNATURE {
340 return Err(FilesystemError::Parse(format!(
341 "bad MDB signature: 0x{sig:04X}"
342 )));
343 }
344
345 let name_len = data[36] as usize;
347 let name_bytes = &data[37..37 + name_len.min(27)];
348 let volume_name = mac_roman_to_utf8(name_bytes);
349 let volume_name_raw = name_bytes.to_vec();
350
351 let mut extents_extents = [HfsExtDescriptor {
352 start_block: 0,
353 block_count: 0,
354 }; 3];
355 for i in 0..3 {
356 extents_extents[i] = HfsExtDescriptor::parse(&data[134 + i * 4..138 + i * 4]);
357 }
358
359 let mut catalog_extents = [HfsExtDescriptor {
360 start_block: 0,
361 block_count: 0,
362 }; 3];
363 for i in 0..3 {
364 catalog_extents[i] = HfsExtDescriptor::parse(&data[150 + i * 4..154 + i * 4]);
365 }
366
367 let embedded_sig = BigEndian::read_u16(&data[124..126]);
369 let embedded_start = BigEndian::read_u16(&data[126..128]);
370 let embedded_count = BigEndian::read_u16(&data[128..130]);
371
372 let mut finder_info = [0u32; 8];
374 for i in 0..8 {
375 finder_info[i] = BigEndian::read_u32(&data[92 + i * 4..96 + i * 4]);
376 }
377
378 let mut raw_sector = [0u8; 512];
380 let copy_len = data.len().min(512);
381 raw_sector[..copy_len].copy_from_slice(&data[..copy_len]);
382
383 Ok(HfsMasterDirectoryBlock {
384 signature: sig,
385 create_date: BigEndian::read_u32(&data[2..6]),
386 modify_date: BigEndian::read_u32(&data[6..10]),
387 total_blocks: BigEndian::read_u16(&data[18..20]),
388 block_size: BigEndian::read_u32(&data[20..24]),
389 free_blocks: BigEndian::read_u16(&data[34..36]),
390 volume_name,
391 volume_name_raw,
392 volume_bitmap_block: BigEndian::read_u16(&data[14..16]),
393 first_alloc_block: BigEndian::read_u16(&data[28..30]),
394 next_catalog_id: BigEndian::read_u32(&data[30..34]),
395 file_count: BigEndian::read_u32(&data[84..88]),
396 folder_count: BigEndian::read_u32(&data[88..92]),
397 finder_info,
398 extents_file_size: BigEndian::read_u32(&data[130..134]),
399 extents_file_extents: extents_extents,
400 catalog_file_size: BigEndian::read_u32(&data[146..150]),
401 catalog_file_extents: catalog_extents,
402 embedded_signature: embedded_sig,
403 embedded_start_block: embedded_start,
404 embedded_block_count: embedded_count,
405 raw_sector,
406 })
407 }
408
409 fn serialize_to_sector(&self) -> [u8; 512] {
412 let mut out = self.raw_sector;
413 BigEndian::write_u32(&mut out[6..10], self.modify_date);
414 BigEndian::write_u16(&mut out[18..20], self.total_blocks);
415 BigEndian::write_u16(&mut out[34..36], self.free_blocks);
416 BigEndian::write_u32(&mut out[30..34], self.next_catalog_id);
417 BigEndian::write_u32(&mut out[84..88], self.file_count);
418 BigEndian::write_u32(&mut out[88..92], self.folder_count);
419 for i in 0..8 {
420 BigEndian::write_u32(&mut out[92 + i * 4..96 + i * 4], self.finder_info[i]);
421 }
422 BigEndian::write_u32(&mut out[130..134], self.extents_file_size);
423 for i in 0..3 {
424 self.extents_file_extents[i].serialize(&mut out[134 + i * 4..138 + i * 4]);
425 }
426 BigEndian::write_u32(&mut out[146..150], self.catalog_file_size);
427 for i in 0..3 {
428 self.catalog_file_extents[i].serialize(&mut out[150 + i * 4..154 + i * 4]);
429 }
430 out
431 }
432
433 fn has_embedded_hfs_plus(&self) -> bool {
435 self.embedded_signature == HFS_PLUS_EMBEDDED_SIGNATURE
436 }
437}
438
439pub(crate) const CATALOG_DIR: i8 = 1;
441const CNID_CATALOG: u32 = 4;
443pub(crate) const CATALOG_FILE: i8 = 2;
444
445#[derive(Debug, Clone)]
449#[allow(dead_code)]
450enum CatalogRecord {
451 Directory {
452 dir_id: u32,
453 name: String,
454 parent_id: u32,
455 dates: (u32, u32, u32),
457 },
458 File {
459 file_id: u32,
460 name: String,
461 parent_id: u32,
462 data_size: u32,
463 data_extents: [HfsExtDescriptor; 3],
464 rsrc_size: u32,
465 rsrc_extents: [HfsExtDescriptor; 3],
466 type_code: [u8; 4],
469 creator_code: [u8; 4],
470 finder_flags: u16,
472 dates: (u32, u32, u32),
474 },
475}
476
477pub struct HfsFilesystem<R: Read + Seek> {
479 reader: R,
480 partition_offset: u64,
481 partition_size: Option<u64>,
484 mdb: HfsMasterDirectoryBlock,
485 catalog_data: Vec<u8>,
487 extents_overflow_data: Option<Vec<u8>>,
490 bitmap: Option<Vec<u8>>,
492 pending_boot_blocks: Option<Box<[u8; 1024]>>,
494 pending_volume_dates: Option<(u32, u32, u32)>,
497 bulk_mode: bool,
503 btrees_initialized: bool,
508}
509
510impl<R: Read + Seek> HfsFilesystem<R> {
511 fn alt_mdb_offset(&self) -> Option<u64> {
514 if let Some(size) = self.partition_size.filter(|&s| s >= 2048) {
515 return Some(self.partition_offset + size - 1024);
516 }
517 if self.mdb.block_size == 0 || self.mdb.total_blocks == 0 {
518 return None;
519 }
520 let sectors_per_block = self.mdb.block_size as u64 / 512;
521 let alt_sector =
522 self.mdb.first_alloc_block as u64 + self.mdb.total_blocks as u64 * sectors_per_block;
523 Some(self.partition_offset + alt_sector * 512)
524 }
525
526 pub fn open(reader: R, partition_offset: u64) -> Result<Self, FilesystemError> {
527 Self::open_sized(reader, partition_offset, None)
528 }
529
530 pub fn open_for_edit(
533 reader: R,
534 partition_offset: u64,
535 partition_size: Option<u64>,
536 ) -> Result<Self, FilesystemError> {
537 let fs = Self::open_sized(reader, partition_offset, partition_size)?;
538 if let Some(short) = fs.partition_shortfall() {
539 log::warn!(
540 "the HFS volume is {short} bytes smaller than its partition; Mac OS and Disk \
541 First Aid expect a classic HFS volume to fill its partition. Grow it with \
542 `rb-cli resize IMG@N --size <partition size>` (R-058)"
543 );
544 }
545 Ok(fs)
546 }
547
548 pub fn partition_shortfall(&self) -> Option<u64> {
551 let size = self.partition_size.filter(|&s| s >= 2048)?;
552 if self.mdb.block_size == 0 {
553 return None;
554 }
555 let volume_len = self.mdb.first_alloc_block as u64 * 512
556 + self.mdb.total_blocks as u64 * self.mdb.block_size as u64
557 + 1024;
558 if volume_len < size {
559 Some(size - volume_len)
560 } else {
561 None
562 }
563 }
564
565 pub fn open_sized(
568 mut reader: R,
569 partition_offset: u64,
570 partition_size: Option<u64>,
571 ) -> Result<Self, FilesystemError> {
572 reader.seek(SeekFrom::Start(partition_offset + 1024))?;
574 let mut mdb_buf = [0u8; 512];
575 reader.read_exact(&mut mdb_buf)?;
576 let mdb = HfsMasterDirectoryBlock::parse(&mdb_buf)?;
577
578 if mdb.has_embedded_hfs_plus() {
579 return Err(FilesystemError::Unsupported(
580 "this HFS volume contains an embedded HFS+ volume; use HFS+ reader instead".into(),
581 ));
582 }
583
584 let catalog_data = read_fork_data(
586 &mut reader,
587 partition_offset,
588 &mdb,
589 &mdb.catalog_file_extents,
590 mdb.catalog_file_size as u64,
591 )?;
592
593 let mut fs = HfsFilesystem {
594 reader,
595 partition_offset,
596 partition_size,
597 mdb,
598 catalog_data,
599 extents_overflow_data: None,
600 bitmap: None,
601 pending_boot_blocks: None,
602 pending_volume_dates: None,
603 bulk_mode: false,
604 btrees_initialized: false,
605 };
606 if (fs.catalog_data.len() as u64) < fs.mdb.catalog_file_size as u64 {
609 let inline = fs.mdb.catalog_file_extents;
610 let size = fs.mdb.catalog_file_size as u64;
611 fs.catalog_data = fs.read_fork_with_overflow(CNID_CATALOG, 0x00, &inline, size)?;
612 }
613 Ok(fs)
614 }
615
616 fn list_children(&self, parent_id: u32) -> Result<Vec<CatalogRecord>, FilesystemError> {
618 if self.catalog_data.len() < 512 {
619 return Ok(vec![]);
620 }
621
622 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as u32;
626 if (node_size as usize) < crate::hfs_common::BTREE_MIN_NODE_SIZE
627 || self.catalog_data.len() < node_size as usize
628 {
629 return Ok(vec![]);
630 }
631 let header_node = &self.catalog_data[0..node_size as usize];
632 let first_leaf = BigEndian::read_u32(&header_node[24..28]);
633
634 let mut results = Vec::new();
635 let mut node_idx = first_leaf;
636
637 while node_idx != 0 {
638 let offset = node_idx as usize * node_size as usize;
639 if offset + node_size as usize > self.catalog_data.len() {
640 break;
641 }
642 let node = &self.catalog_data[offset..offset + node_size as usize];
643
644 let next_node = BigEndian::read_u32(&node[0..4]);
646 let _kind = node[8] as i8; let num_records = BigEndian::read_u16(&node[10..12]);
648
649 for i in 0..num_records as usize {
650 let offset_pos = node_size as usize - 2 * (i + 1);
652 if offset_pos + 2 > node.len() {
653 break;
654 }
655 let rec_offset = BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize;
656 if rec_offset + 6 > node.len() {
657 continue;
658 }
659
660 let key_len = node[rec_offset] as usize;
662 if key_len < 6 || rec_offset + 1 + key_len > node.len() {
663 continue;
664 }
665 let key_data = &node[rec_offset + 1..rec_offset + 1 + key_len];
666 let rec_parent_id = BigEndian::read_u32(&key_data[1..5]);
668 let name_len = key_data[5] as usize;
669 let name = if name_len > 0 && 6 + name_len <= key_data.len() {
670 mac_roman_to_utf8(&key_data[6..6 + name_len])
671 } else {
672 String::new()
673 };
674
675 let mut rec_data_offset = rec_offset + 1 + key_len;
677 if !rec_data_offset.is_multiple_of(2) {
678 rec_data_offset += 1;
679 }
680 if rec_data_offset + 2 > node.len() {
681 continue;
682 }
683
684 let record_type = node[rec_data_offset] as i8;
685
686 if rec_parent_id != parent_id {
687 continue;
688 }
689
690 match record_type {
691 CATALOG_DIR => {
692 if rec_data_offset + 70 > node.len() {
693 continue;
694 }
695 let dir_id =
696 BigEndian::read_u32(&node[rec_data_offset + 6..rec_data_offset + 10]);
697 let dir = &node[rec_data_offset..];
699 let dates = (
700 BigEndian::read_u32(&dir[10..14]),
701 BigEndian::read_u32(&dir[14..18]),
702 BigEndian::read_u32(&dir[18..22]),
703 );
704 results.push(CatalogRecord::Directory {
705 dir_id,
706 name,
707 parent_id: rec_parent_id,
708 dates,
709 });
710 }
711 CATALOG_FILE => {
712 if rec_data_offset + 102 > node.len() {
713 continue;
714 }
715 let rec = &node[rec_data_offset..];
716 let type_code: [u8; 4] = rec[4..8].try_into().unwrap();
718 let creator_code: [u8; 4] = rec[8..12].try_into().unwrap();
719 let finder_flags = BigEndian::read_u16(&rec[12..14]);
720 let file_id = BigEndian::read_u32(&rec[20..24]);
722 let data_size = BigEndian::read_u32(&rec[26..30]);
724 let mut data_extents = [HfsExtDescriptor {
725 start_block: 0,
726 block_count: 0,
727 }; 3];
728 for j in 0..3 {
729 data_extents[j] = HfsExtDescriptor::parse(&rec[74 + j * 4..78 + j * 4]);
730 }
731 let rsrc_size = BigEndian::read_u32(&rec[36..40]);
733 let mut rsrc_extents = [HfsExtDescriptor {
734 start_block: 0,
735 block_count: 0,
736 }; 3];
737 for j in 0..3 {
738 rsrc_extents[j] = HfsExtDescriptor::parse(&rec[86 + j * 4..90 + j * 4]);
739 }
740 let dates = (
742 BigEndian::read_u32(&rec[44..48]),
743 BigEndian::read_u32(&rec[48..52]),
744 BigEndian::read_u32(&rec[52..56]),
745 );
746 results.push(CatalogRecord::File {
747 file_id,
748 name,
749 parent_id: rec_parent_id,
750 data_size,
751 data_extents,
752 rsrc_size,
753 rsrc_extents,
754 type_code,
755 creator_code,
756 finder_flags,
757 dates,
758 });
759 }
760 _ => {}
761 }
762 }
763
764 node_idx = next_node;
765 }
766
767 Ok(results)
768 }
769
770 fn find_file_by_id(
773 &self,
774 file_id: u32,
775 ) -> Option<(u32, [HfsExtDescriptor; 3], u32, [HfsExtDescriptor; 3])> {
776 if self.catalog_data.len() < 512 {
777 return None;
778 }
779 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as u32;
780 if (node_size as usize) < crate::hfs_common::BTREE_MIN_NODE_SIZE
781 || self.catalog_data.len() < node_size as usize
782 {
783 return None;
784 }
785 let header_node = &self.catalog_data[0..node_size as usize];
786 let first_leaf = BigEndian::read_u32(&header_node[24..28]);
787
788 let mut node_idx = first_leaf;
789 while node_idx != 0 {
790 let offset = node_idx as usize * node_size as usize;
791 if offset + node_size as usize > self.catalog_data.len() {
792 break;
793 }
794 let node = &self.catalog_data[offset..offset + node_size as usize];
795 let next_node = BigEndian::read_u32(&node[0..4]);
796 let num_records = BigEndian::read_u16(&node[10..12]);
797
798 for i in 0..num_records as usize {
799 let offset_pos = node_size as usize - 2 * (i + 1);
800 if offset_pos + 2 > node.len() {
801 break;
802 }
803 let rec_offset = BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize;
804 if rec_offset + 6 > node.len() {
805 continue;
806 }
807 let key_len = node[rec_offset] as usize;
808 if key_len < 6 || rec_offset + 1 + key_len > node.len() {
809 continue;
810 }
811 let mut rec_data_offset = rec_offset + 1 + key_len;
812 if !rec_data_offset.is_multiple_of(2) {
813 rec_data_offset += 1;
814 }
815 if rec_data_offset + 2 > node.len() {
816 continue;
817 }
818 let record_type = node[rec_data_offset] as i8;
819 if record_type != CATALOG_FILE {
820 continue;
821 }
822 if rec_data_offset + 102 > node.len() {
823 continue;
824 }
825 let rec = &node[rec_data_offset..];
826 let rec_file_id = BigEndian::read_u32(&rec[20..24]);
828 if rec_file_id != file_id {
829 continue;
830 }
831 let data_size = BigEndian::read_u32(&rec[26..30]);
833 let mut data_extents = [HfsExtDescriptor {
834 start_block: 0,
835 block_count: 0,
836 }; 3];
837 for j in 0..3 {
838 data_extents[j] = HfsExtDescriptor::parse(&rec[74 + j * 4..78 + j * 4]);
839 }
840 let rsrc_size = BigEndian::read_u32(&rec[36..40]);
842 let mut rsrc_extents = [HfsExtDescriptor {
843 start_block: 0,
844 block_count: 0,
845 }; 3];
846 for j in 0..3 {
847 rsrc_extents[j] = HfsExtDescriptor::parse(&rec[86 + j * 4..90 + j * 4]);
848 }
849 return Some((data_size, data_extents, rsrc_size, rsrc_extents));
850 }
851 node_idx = next_node;
852 }
853 None
854 }
855
856 fn read_volume_bitmap(&mut self) -> Result<Vec<u8>, FilesystemError> {
858 let bitmap_offset = self.partition_offset + self.mdb.volume_bitmap_block as u64 * 512;
859 let bitmap_size = (self.mdb.total_blocks as u32).div_ceil(8) as usize;
860 self.reader.seek(SeekFrom::Start(bitmap_offset))?;
861 let mut bitmap = vec![0u8; bitmap_size];
862 self.reader.read_exact(&mut bitmap)?;
863 Ok(bitmap)
864 }
865
866 fn alloc_block_offset(&self, block: u32) -> u64 {
868 self.mdb.first_alloc_block as u64 * 512 + block as u64 * self.mdb.block_size as u64
869 }
870
871 fn ensure_bitmap(&mut self) -> Result<(), FilesystemError> {
873 if self.bitmap.is_none() {
874 let bm = self.read_volume_bitmap()?;
875 self.bitmap = Some(bm);
876 }
877 Ok(())
878 }
879
880 pub fn fsck(&mut self) -> Result<disk_core::fsck::FsckResult, FilesystemError> {
882 self.ensure_bitmap()?;
883 let extents_data = if self.mdb.extents_file_size > 0 {
885 read_fork_data(
886 &mut self.reader,
887 self.partition_offset,
888 &self.mdb,
889 &self.mdb.extents_file_extents,
890 self.mdb.extents_file_size as u64,
891 )
892 .ok()
893 } else {
894 None
895 };
896 let alt_mdb_sector = if let Some(alt_offset) = self.alt_mdb_offset() {
899 let mut alt_buf = [0u8; 512];
900 if self
901 .reader
902 .seek(SeekFrom::Start(alt_offset))
903 .and_then(|_| self.reader.read_exact(&mut alt_buf))
904 .is_ok()
905 {
906 Some(alt_buf)
907 } else {
908 None
909 }
910 } else {
911 None
912 };
913 Ok(crate::hfs_fsck::check_hfs_integrity(
914 &self.mdb,
915 &self.catalog_data,
916 self.bitmap.as_ref().unwrap(),
917 extents_data.as_deref(),
918 alt_mdb_sector.as_ref(),
919 self.partition_size,
920 ))
921 }
922
923 pub(crate) fn build_catalog_key(parent_id: u32, name: &[u8]) -> Vec<u8> {
925 let key_len = 1 + 4 + 1 + name.len(); let mut key = Vec::with_capacity(1 + key_len);
927 key.push(key_len as u8); key.push(0); let mut buf4 = [0u8; 4];
930 BigEndian::write_u32(&mut buf4, parent_id);
931 key.extend_from_slice(&buf4);
932 key.push(name.len() as u8);
933 key.extend_from_slice(name);
934 if key.len() % 2 != 0 {
936 key.push(0);
937 }
938 key
939 }
940
941 pub(crate) fn catalog_compare(a: &[u8], b: &[u8]) -> Ordering {
943 if a.len() < 7 || b.len() < 7 {
945 return a.len().cmp(&b.len());
946 }
947 let parent_a = BigEndian::read_u32(&a[2..6]);
948 let parent_b = BigEndian::read_u32(&b[2..6]);
949 let name_len_a = a[6] as usize;
950 let name_len_b = b[6] as usize;
951 let name_a = &a[7..7 + name_len_a.min(a.len() - 7)];
952 let name_b = &b[7..7 + name_len_b.min(b.len() - 7)];
953 hfs_common::compare_hfs_keys(parent_a, name_a, parent_b, name_b)
954 }
955
956 fn find_catalog_record_by_name(
969 &self,
970 parent_id: u32,
971 name: &[u8],
972 ) -> Option<(u32, usize, usize)> {
973 let search_key = Self::build_catalog_key(parent_id, name);
974 let header = BTreeHeader::read(&self.catalog_data);
975 let node_size = header.node_size as usize;
976 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE || self.catalog_data.len() < node_size
977 {
978 return None;
979 }
980
981 let (leaf_idx, _) = hfs_common::btree_find_insert_leaf(
982 &self.catalog_data,
983 &header,
984 &search_key,
985 &Self::catalog_compare,
986 );
987
988 let off = leaf_idx as usize * node_size;
989 if off + node_size > self.catalog_data.len() {
990 return None;
991 }
992 let node = &self.catalog_data[off..off + node_size];
993 let num_records = BigEndian::read_u16(&node[10..12]) as usize;
994 for rec_idx in 0..num_records {
995 let (start, end) = hfs_common::btree_record_range(node, node_size, rec_idx);
996 if start >= end || end > node_size {
997 continue;
998 }
999 let rec = &node[start..end];
1000 if rec.len() < 7 {
1001 continue;
1002 }
1003 let key_len = rec[0] as usize;
1004 let key_end = 1 + key_len;
1005 if key_end > rec.len() {
1006 continue;
1007 }
1008 if Self::catalog_compare(&rec[..key_end], &search_key) == Ordering::Equal {
1009 return Some((leaf_idx, rec_idx, off + start));
1010 }
1011 }
1012 None
1013 }
1014
1015 fn find_catalog_record_by_cnid(&self, cnid: u32) -> Option<(u32, usize, usize)> {
1017 self.find_catalog_record_by_name(cnid, &[])
1018 }
1019
1020 fn update_parent_valence(&mut self, parent_id: u32, delta: i32) -> Result<(), FilesystemError> {
1022 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1023 let first_leaf = BigEndian::read_u32(&self.catalog_data[24..28]);
1024
1025 let val_off = hfs_common::walk_leaf_records(
1028 &self.catalog_data,
1029 first_leaf,
1030 node_size,
1031 |_node_idx, _rec_idx, abs_off, rec| {
1032 if rec.len() < 7 {
1033 return None;
1034 }
1035 let key_len = rec[0] as usize;
1036 let mut data_off = abs_off + 1 + key_len;
1037 if !data_off.is_multiple_of(2) {
1038 data_off += 1;
1039 }
1040 let abs_end = abs_off + rec.len();
1041 if data_off + 10 > abs_end {
1042 return None;
1043 }
1044 if self.catalog_data[data_off] as i8 != CATALOG_DIR {
1045 return None;
1046 }
1047 let dir_id = BigEndian::read_u32(&self.catalog_data[data_off + 6..data_off + 10]);
1049 if dir_id == parent_id {
1050 Some(data_off + 4)
1051 } else {
1052 None
1053 }
1054 },
1055 );
1056 let val_off = val_off.ok_or_else(|| {
1057 FilesystemError::NotFound(format!(
1058 "parent directory CNID {} not found in catalog",
1059 parent_id
1060 ))
1061 })?;
1062 let old_val = BigEndian::read_u16(&self.catalog_data[val_off..val_off + 2]);
1063 let new_val = (old_val as i32 + delta).max(0) as u16;
1064 BigEndian::write_u16(&mut self.catalog_data[val_off..val_off + 2], new_val);
1065 Ok(())
1066 }
1067
1068 fn insert_catalog_record(&mut self, key_record: &[u8]) -> Result<(), FilesystemError>
1086 where
1087 R: Write,
1088 {
1089 let mut grown = false;
1092 loop {
1093 match hfs_common::btree_insert_full(
1094 &mut self.catalog_data,
1095 key_record,
1096 &hfs_common::BTreeKeyFormat::CLASSIC_CATALOG,
1097 &Self::catalog_compare,
1098 ) {
1099 Ok(()) => return Ok(()),
1100 Err(FilesystemError::DiskFull(_)) if !grown => {
1101 self.grow_btree(HfsTree::Catalog)?;
1102 grown = true;
1103 }
1104 Err(e) => return Err(e),
1105 }
1106 }
1107 }
1108
1109 fn grow_btree(&mut self, tree: HfsTree) -> Result<(), FilesystemError>
1114 where
1115 R: Write,
1116 {
1117 let block_size = self.mdb.block_size;
1118 if tree == HfsTree::Extents {
1119 self.ensure_extents_overflow()?;
1120 }
1121 let (node_size, total_nodes, existing_map) = {
1122 let buf = self.tree_buf(tree)?;
1123 let h = BTreeHeader::read(buf);
1124 let segs = hfs_common::btree_bitmap_segments(buf, h.node_size as usize);
1125 (
1126 h.node_size as usize,
1127 h.total_nodes,
1128 (segs.len() as u32).saturating_sub(1),
1129 )
1130 };
1131 if node_size < 256 || block_size == 0 {
1132 return Err(FilesystemError::InvalidData(format!(
1133 "{} B-tree has node size {node_size}",
1134 tree.label()
1135 )));
1136 }
1137 let clump_off = if tree == HfsTree::Catalog { 78 } else { 74 };
1139 let clump = BigEndian::read_u32(&self.mdb.raw_sector[clump_off..clump_off + 4]) as usize;
1140 let grow_nodes = (clump.div_ceil(node_size).max(1) as u32).max(8);
1141 let header_cap = ((node_size - 256) * 8) as u32;
1142 let per_map = (hfs_common::map_node_bitmap_bytes(node_size) * 8) as u32;
1143 let need_map = total_nodes + grow_nodes > header_cap + existing_map * per_map;
1144 let add_slots = grow_nodes + u32::from(need_map);
1145 let grow_blocks = btree_grow_blocks(total_nodes, add_slots, node_size, block_size);
1146 let mut add_slots = add_slots;
1147 if grow_blocks > 0 {
1148 let mut inline = match tree {
1149 HfsTree::Catalog => self.mdb.catalog_file_extents,
1150 HfsTree::Extents => self.mdb.extents_file_extents,
1151 };
1152 let mut all: Vec<HfsExtDescriptor> = inline
1154 .iter()
1155 .copied()
1156 .filter(|e| e.block_count > 0)
1157 .collect();
1158 let inline_blocks: u32 = all.iter().map(|e| e.block_count as u32).sum();
1159 if tree == HfsTree::Catalog && all.len() == 3 {
1160 self.ensure_extents_overflow()?;
1161 if let Some(x) = self.extents_overflow_data.as_ref() {
1162 all.extend(collect_fork_overflow_extents(
1163 x,
1164 CNID_CATALOG,
1165 0x00,
1166 inline_blocks,
1167 ));
1168 }
1169 }
1170 let tail = all
1171 .last()
1172 .map(|e| e.start_block as u32 + e.block_count as u32);
1173 match tail {
1174 Some(t) if self.allocate_blocks_at(t, grow_blocks)? => {
1175 all.last_mut().unwrap().block_count += grow_blocks as u16;
1176 }
1177 _ => {
1178 let want_nodes = (total_nodes + add_slots).max(grow_nodes + 8);
1181 let more = btree_grow_blocks(
1182 total_nodes,
1183 want_nodes - total_nodes,
1184 node_size,
1185 block_size,
1186 )
1187 .max(grow_blocks);
1188 add_slots = want_nodes.max(total_nodes + add_slots) - total_nodes;
1189 if tree == HfsTree::Extents && all.len() >= 3 {
1190 return Err(FilesystemError::DiskFull(
1191 "extents-overflow file has no free extent slot to grow into".into(),
1192 ));
1193 }
1194 let (start, more) = match self.allocate_blocks(more) {
1196 Ok(start) => (start, more),
1197 Err(_) => {
1198 add_slots = grow_nodes + u32::from(need_map);
1199 (self.allocate_blocks(grow_blocks)?, grow_blocks)
1200 }
1201 };
1202 all.push(HfsExtDescriptor {
1203 start_block: start as u16,
1204 block_count: more as u16,
1205 });
1206 }
1207 }
1208 inline = [HfsExtDescriptor {
1209 start_block: 0,
1210 block_count: 0,
1211 }; 3];
1212 for (i, e) in all.iter().take(3).enumerate() {
1213 inline[i] = *e;
1214 }
1215 match tree {
1216 HfsTree::Catalog => {
1217 self.mdb.catalog_file_extents = inline;
1218 if all.len() > 3 {
1219 let data = self.extents_overflow_data.as_mut().expect("loaded above");
1221 remove_fork_overflow_records(data, CNID_CATALOG, 0x00)?;
1222 self.insert_fork_overflow(CNID_CATALOG, 0x00, &all)?;
1223 self.write_extents_overflow_back()?;
1224 }
1225 }
1226 HfsTree::Extents => self.mdb.extents_file_extents = inline,
1227 }
1228 }
1229 let buf = self.tree_buf(tree)?;
1230 buf.resize(buf.len() + add_slots as usize * node_size, 0);
1231 if need_map {
1232 let map_idx = total_nodes;
1235 let tail = btree_map_chain_tail(buf, node_size);
1236 hfs_common::init_map_node(buf, node_size, map_idx, tail, 0);
1237 let linker = tail as usize * node_size;
1238 BigEndian::write_u32(&mut buf[linker..linker + 4], map_idx);
1239 hfs_common::btree_bitmap_set(buf, node_size, map_idx);
1240 }
1241 let mut h = BTreeHeader::read(buf);
1242 h.total_nodes += add_slots;
1243 h.free_nodes += add_slots - u32::from(need_map);
1244 h.write(buf);
1245 let added = add_slots * node_size as u32;
1246 match tree {
1247 HfsTree::Catalog => self.mdb.catalog_file_size += added,
1248 HfsTree::Extents => self.mdb.extents_file_size += added,
1249 }
1250 Ok(())
1251 }
1252
1253 fn tree_buf(&mut self, tree: HfsTree) -> Result<&mut Vec<u8>, FilesystemError> {
1255 match tree {
1256 HfsTree::Catalog => Ok(&mut self.catalog_data),
1257 HfsTree::Extents => self.extents_overflow_data.as_mut().ok_or_else(|| {
1258 FilesystemError::Unsupported("volume has no extents-overflow B-tree".into())
1259 }),
1260 }
1261 }
1262
1263 fn allocate_blocks_at(&mut self, start: u32, count: u32) -> Result<bool, FilesystemError> {
1265 self.ensure_bitmap()?;
1266 let total = self.mdb.total_blocks as u32;
1267 let bitmap = self.bitmap.as_mut().unwrap();
1268 if start + count > total || (start..start + count).any(|b| bitmap_test_bit_be(bitmap, b)) {
1269 return Ok(false);
1270 }
1271 for b in start..start + count {
1272 bitmap_set_bit_be(bitmap, b);
1273 }
1274 self.mdb.free_blocks = self.mdb.free_blocks.saturating_sub(count as u16);
1275 Ok(true)
1276 }
1277
1278 fn remove_catalog_record(
1280 &mut self,
1281 node_idx: u32,
1282 rec_idx: usize,
1283 ) -> Result<(), FilesystemError> {
1284 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1285 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE {
1286 return Ok(());
1287 }
1288 let offset = node_idx as usize * node_size;
1289 if offset + node_size > self.catalog_data.len() {
1290 return Ok(());
1291 }
1292
1293 let num_before = {
1294 let node = &self.catalog_data[offset..offset + node_size];
1295 BigEndian::read_u16(&node[10..12]) as usize
1296 };
1297
1298 {
1299 let node = &mut self.catalog_data[offset..offset + node_size];
1300 btree_remove_record(node, node_size, rec_idx);
1301 }
1302
1303 let node = &self.catalog_data[offset..offset + node_size];
1305 let num_after = BigEndian::read_u16(&node[10..12]) as usize;
1306 if num_after == 0 && num_before > 0 {
1307 let prev = BigEndian::read_u32(&self.catalog_data[offset + 4..offset + 8]);
1308 let next = BigEndian::read_u32(&self.catalog_data[offset..offset + 4]);
1309
1310 if prev != 0 {
1311 let prev_off = prev as usize * node_size;
1312 if prev_off + 4 <= self.catalog_data.len() {
1313 BigEndian::write_u32(&mut self.catalog_data[prev_off..prev_off + 4], next);
1314 }
1315 }
1316 if next != 0 {
1317 let next_off = next as usize * node_size;
1318 if next_off + 8 <= self.catalog_data.len() {
1319 BigEndian::write_u32(&mut self.catalog_data[next_off + 4..next_off + 8], prev);
1320 }
1321 }
1322
1323 let mut h = BTreeHeader::read(&self.catalog_data);
1324 if h.first_leaf_node == node_idx {
1325 h.first_leaf_node = next;
1326 }
1327 if h.last_leaf_node == node_idx {
1328 h.last_leaf_node = prev;
1329 }
1330 btree_free_node(&mut self.catalog_data, node_size, node_idx);
1331 h.free_nodes += 1;
1332 if h.root_node == node_idx {
1334 h.root_node = 0;
1335 h.depth = 0;
1336 }
1337 h.write(&mut self.catalog_data);
1338
1339 let header = BTreeHeader::read(&self.catalog_data);
1341 if header.depth > 1 {
1342 let mut dummy_report = disk_core::fsck::RepairReport {
1343 fixes_applied: Vec::new(),
1344 fixes_failed: Vec::new(),
1345 unrepairable_count: 0,
1346 };
1347 crate::hfs_fsck::rebuild_index_nodes(
1348 &mut self.catalog_data,
1349 node_size,
1350 &mut dummy_report,
1351 );
1352 }
1353 } else if rec_idx == 0 && num_after > 0 {
1354 btree_refresh_index_keys(
1357 &mut self.catalog_data,
1358 node_size,
1359 node_idx,
1360 &BTreeKeyFormat::CLASSIC_CATALOG,
1361 &Self::catalog_compare,
1362 )?;
1363 }
1364
1365 let mut h = BTreeHeader::read(&self.catalog_data);
1367 h.leaf_records = h.leaf_records.saturating_sub(1);
1368 h.write(&mut self.catalog_data);
1369 Ok(())
1370 }
1371
1372 fn snapshot(&self) -> Option<(Vec<u8>, Option<Vec<u8>>, HfsMasterDirectoryBlock)> {
1381 if self.bulk_mode {
1382 return None;
1383 }
1384 Some((
1385 self.catalog_data.clone(),
1386 self.bitmap.clone(),
1387 self.mdb.clone(),
1388 ))
1389 }
1390
1391 fn restore_snapshot(
1394 &mut self,
1395 snap: Option<(Vec<u8>, Option<Vec<u8>>, HfsMasterDirectoryBlock)>,
1396 ) {
1397 let Some((catalog_data, bitmap, mdb)) = snap else {
1398 return;
1399 };
1400 self.catalog_data = catalog_data;
1401 self.bitmap = bitmap;
1402 self.mdb = mdb;
1403 }
1404
1405 pub(crate) fn mdb(&self) -> &HfsMasterDirectoryBlock {
1406 &self.mdb
1407 }
1408
1409 pub fn volume_summary(&self) -> HfsVolumeSummary {
1413 let used_blocks = self.mdb.total_blocks.saturating_sub(self.mdb.free_blocks) as u64;
1414 HfsVolumeSummary {
1415 volume_name: self.mdb.volume_name.clone(),
1416 block_size: self.mdb.block_size,
1417 total_blocks: self.mdb.total_blocks,
1418 free_blocks: self.mdb.free_blocks,
1419 used_bytes: used_blocks * self.mdb.block_size as u64,
1420 file_count: self.mdb.file_count,
1421 folder_count: self.mdb.folder_count,
1422 catalog_file_size: self.mdb.catalog_file_size,
1423 extents_file_size: self.mdb.extents_file_size,
1424 }
1425 }
1426
1427 pub(crate) fn catalog_data(&self) -> &[u8] {
1428 &self.catalog_data
1429 }
1430
1431 #[allow(dead_code)]
1432 pub(crate) fn partition_offset(&self) -> u64 {
1433 self.partition_offset
1434 }
1435
1436 pub fn locate_file_by_cnid(&self, cnid: u32) -> Option<HfsFileLocation> {
1448 let (data_size, data_extents, _rsrc_size, _rsrc_extents) = self.find_file_by_id(cnid)?;
1449 let first = data_extents.first().copied()?;
1450 if first.block_count == 0 {
1451 return None;
1452 }
1453 let fragmented = data_extents.iter().skip(1).any(|e| e.block_count != 0);
1454 let offset_in_image =
1458 self.partition_offset + self.alloc_block_offset(first.start_block as u32);
1459 Some(HfsFileLocation {
1460 offset_in_image,
1461 length: data_size as u64,
1462 fragmented,
1463 })
1464 }
1465
1466 pub(crate) fn read_boot_blocks(&mut self) -> Result<[u8; 1024], FilesystemError> {
1468 self.reader.seek(SeekFrom::Start(self.partition_offset))?;
1469 let mut buf = [0u8; 1024];
1470 self.reader.read_exact(&mut buf)?;
1471 Ok(buf)
1472 }
1473
1474 #[allow(dead_code)]
1477 pub(crate) fn read_fork(
1478 &mut self,
1479 extents: &[HfsExtDescriptor; 3],
1480 size: u64,
1481 ) -> Result<Vec<u8>, FilesystemError> {
1482 read_fork_data(
1483 &mut self.reader,
1484 self.partition_offset,
1485 &self.mdb,
1486 extents,
1487 size,
1488 )
1489 }
1490
1491 fn ensure_extents_overflow(&mut self) -> Result<(), FilesystemError> {
1494 if self.extents_overflow_data.is_some() {
1495 return Ok(());
1496 }
1497 if self.mdb.extents_file_size == 0 {
1498 return Ok(());
1499 }
1500 let data = read_fork_data(
1501 &mut self.reader,
1502 self.partition_offset,
1503 &self.mdb,
1504 &self.mdb.extents_file_extents,
1505 self.mdb.extents_file_size as u64,
1506 )?;
1507 self.extents_overflow_data = Some(data);
1508 Ok(())
1509 }
1510
1511 fn write_fork_with_overflow_to(
1518 &mut self,
1519 file_id: u32,
1520 fork_type: u8,
1521 inline: &[HfsExtDescriptor; 3],
1522 size: u64,
1523 writer: &mut dyn std::io::Write,
1524 ) -> Result<u64, FilesystemError> {
1525 let mut written = write_fork_data_to(
1526 &mut self.reader,
1527 self.partition_offset,
1528 &self.mdb,
1529 inline,
1530 writer,
1531 size,
1532 )?;
1533 if written >= size {
1534 return Ok(written);
1535 }
1536 self.ensure_extents_overflow()?;
1537 let Some(ext_data) = self.extents_overflow_data.as_ref() else {
1538 return Err(FilesystemError::InvalidData(format!(
1539 "file {file_id} fork {fork_type:#x}: {size} bytes requested but only {written} \
1540 bytes in inline extents and no extents-overflow B-tree present",
1541 )));
1542 };
1543 let inline_blocks: u32 = inline.iter().map(|e| e.block_count as u32).sum();
1544 let overflow = collect_fork_overflow_extents(ext_data, file_id, fork_type, inline_blocks);
1545 let block_size = self.mdb.block_size as u64;
1546 let first_alloc_offset = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
1547 let mut buf = vec![0u8; 64 * 1024];
1548 for ext in overflow {
1549 if written >= size {
1550 break;
1551 }
1552 if ext.block_count == 0 {
1553 continue;
1554 }
1555 let offset = first_alloc_offset + ext.start_block as u64 * block_size;
1556 let extent_len = ext.block_count as u64 * block_size;
1557 let to_emit = extent_len.min(size - written);
1558 self.reader.seek(SeekFrom::Start(offset))?;
1559 let mut left = to_emit;
1560 while left > 0 {
1561 let n = (buf.len() as u64).min(left) as usize;
1562 self.reader.read_exact(&mut buf[..n])?;
1563 writer.write_all(&buf[..n])?;
1564 left -= n as u64;
1565 }
1566 written += to_emit;
1567 }
1568 if written < size {
1569 return Err(FilesystemError::InvalidData(format!(
1570 "file {file_id} fork {fork_type:#x}: extents (inline + overflow) cover \
1571 {written} of {size} bytes",
1572 )));
1573 }
1574 Ok(written)
1575 }
1576
1577 fn read_fork_with_overflow(
1578 &mut self,
1579 file_id: u32,
1580 fork_type: u8,
1581 inline: &[HfsExtDescriptor; 3],
1582 size: u64,
1583 ) -> Result<Vec<u8>, FilesystemError> {
1584 let mut data = read_fork_data(
1585 &mut self.reader,
1586 self.partition_offset,
1587 &self.mdb,
1588 inline,
1589 size,
1590 )?;
1591 if (data.len() as u64) >= size {
1592 return Ok(data);
1593 }
1594
1595 self.ensure_extents_overflow()?;
1596 let Some(ext_data) = self.extents_overflow_data.as_ref() else {
1597 return Err(FilesystemError::InvalidData(format!(
1598 "file {file_id} fork {fork_type:#x}: {size} bytes requested but only {} bytes \
1599 in inline extents and no extents-overflow B-tree present",
1600 data.len()
1601 )));
1602 };
1603
1604 let inline_blocks: u32 = inline.iter().map(|e| e.block_count as u32).sum();
1605 let overflow = collect_fork_overflow_extents(ext_data, file_id, fork_type, inline_blocks);
1606
1607 let block_size = self.mdb.block_size as u64;
1608 let first_alloc_offset = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
1609
1610 for ext in overflow {
1611 if data.len() as u64 >= size {
1612 break;
1613 }
1614 if ext.block_count == 0 {
1615 continue;
1616 }
1617 let offset = first_alloc_offset + ext.start_block as u64 * block_size;
1618 let extent_len = ext.block_count as u64 * block_size;
1619 let to_read = extent_len.min(size - data.len() as u64) as usize;
1620 self.reader.seek(SeekFrom::Start(offset))?;
1621 let mut buf = vec![0u8; to_read];
1622 self.reader.read_exact(&mut buf)?;
1623 data.extend_from_slice(&buf);
1624 }
1625
1626 if (data.len() as u64) < size {
1627 return Err(FilesystemError::InvalidData(format!(
1628 "file {file_id} fork {fork_type:#x}: extents (inline + overflow) cover \
1629 {} of {size} bytes",
1630 data.len()
1631 )));
1632 }
1633 Ok(data)
1634 }
1635
1636 fn locate_record_data(&self, cnid: u32) -> Result<usize, FilesystemError> {
1648 if let Some((_, _, t_offset)) = self.find_catalog_record_by_cnid(cnid) {
1649 let key_len = self.catalog_data[t_offset] as usize;
1650 let mut rec_data_start = t_offset + 1 + key_len;
1651 if !rec_data_start.is_multiple_of(2) {
1652 rec_data_start += 1;
1653 }
1654 let thread_parent =
1655 BigEndian::read_u32(&self.catalog_data[rec_data_start + 10..rec_data_start + 14]);
1656 let thread_name_len = self.catalog_data[rec_data_start + 14] as usize;
1657 let thread_name = self.catalog_data
1658 [rec_data_start + 15..rec_data_start + 15 + thread_name_len]
1659 .to_vec();
1660
1661 if let Some((_, _, f_offset)) =
1662 self.find_catalog_record_by_name(thread_parent, &thread_name)
1663 {
1664 let fkey_len = self.catalog_data[f_offset] as usize;
1665 let mut frec_start = f_offset + 1 + fkey_len;
1666 if !frec_start.is_multiple_of(2) {
1667 frec_start += 1;
1668 }
1669 return Ok(frec_start);
1670 }
1671 }
1672
1673 if let Some((_, _, frec_start)) = self.find_file_record_offset_by_cnid(cnid) {
1674 return Ok(frec_start);
1675 }
1676
1677 Err(FilesystemError::NotFound(format!(
1678 "catalog record for CNID {cnid} not found"
1679 )))
1680 }
1681
1682 fn find_file_record_offset_by_cnid(&self, cnid: u32) -> Option<(u32, usize, usize)> {
1691 if self.catalog_data.len() < 512 {
1692 return None;
1693 }
1694 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1695 let first_leaf = BigEndian::read_u32(&self.catalog_data[24..28]);
1696
1697 hfs_common::walk_leaf_records(
1698 &self.catalog_data,
1699 first_leaf,
1700 node_size,
1701 |node_idx, rec_idx, abs_off, rec| {
1702 if rec.len() < 7 {
1703 return None;
1704 }
1705 let key_len = rec[0] as usize;
1706 if key_len < 6 || 1 + key_len > rec.len() {
1707 return None;
1708 }
1709 let mut data_rel = 1 + key_len;
1710 if !data_rel.is_multiple_of(2) {
1711 data_rel += 1;
1712 }
1713 if data_rel + 24 > rec.len() {
1714 return None;
1715 }
1716 if rec[data_rel] as i8 != CATALOG_FILE {
1717 return None;
1718 }
1719 let rec_cnid = BigEndian::read_u32(&rec[data_rel + 20..data_rel + 24]);
1721 if rec_cnid == cnid {
1722 Some((node_idx, rec_idx, abs_off + data_rel))
1723 } else {
1724 None
1725 }
1726 },
1727 )
1728 }
1729
1730 pub fn set_finder_info(
1733 &mut self,
1734 entry: &FileEntry,
1735 finfo: [u8; 16],
1736 fxinfo: [u8; 16],
1737 ) -> Result<(), FilesystemError> {
1738 if entry.is_directory() {
1739 return Err(FilesystemError::InvalidData(
1740 "set_finder_info: entry is a directory".into(),
1741 ));
1742 }
1743 let snap = self.snapshot();
1744 let result = (|| {
1745 let rec = self.locate_record_data(entry.location as u32)?;
1746 if self.catalog_data[rec] as i8 != CATALOG_FILE {
1747 return Err(FilesystemError::InvalidData("not a file record".into()));
1748 }
1749 self.catalog_data[rec + 4..rec + 20].copy_from_slice(&finfo);
1750 self.catalog_data[rec + 56..rec + 72].copy_from_slice(&fxinfo);
1751 Ok(())
1752 })();
1753 if result.is_err() {
1754 self.restore_snapshot(snap);
1755 }
1756 result
1757 }
1758
1759 pub fn set_directory_finder_info(
1762 &mut self,
1763 entry: &FileEntry,
1764 dinfo: [u8; 16],
1765 dxinfo: [u8; 16],
1766 ) -> Result<(), FilesystemError> {
1767 if !entry.is_directory() {
1768 return Err(FilesystemError::InvalidData(
1769 "set_directory_finder_info: entry is not a directory".into(),
1770 ));
1771 }
1772 let snap = self.snapshot();
1773 let result = (|| {
1774 let rec = self.locate_record_data(entry.location as u32)?;
1775 if self.catalog_data[rec] as i8 != CATALOG_DIR {
1776 return Err(FilesystemError::InvalidData(
1777 "not a directory record".into(),
1778 ));
1779 }
1780 self.catalog_data[rec + 22..rec + 38].copy_from_slice(&dinfo);
1781 self.catalog_data[rec + 38..rec + 54].copy_from_slice(&dxinfo);
1782 Ok(())
1783 })();
1784 if result.is_err() {
1785 self.restore_snapshot(snap);
1786 }
1787 result
1788 }
1789
1790 pub fn set_dates(
1794 &mut self,
1795 entry: &FileEntry,
1796 create: u32,
1797 modify: u32,
1798 backup: u32,
1799 ) -> Result<(), FilesystemError> {
1800 let snap = self.snapshot();
1801 let result = (|| {
1802 let rec = self.locate_record_data(entry.location as u32)?;
1803 let record_type = self.catalog_data[rec] as i8;
1804 let (cr_off, md_off, bk_off) = match record_type {
1805 CATALOG_FILE => (44, 48, 52),
1806 CATALOG_DIR => (10, 14, 18),
1807 other => {
1808 return Err(FilesystemError::InvalidData(format!(
1809 "unexpected catalog record type: {other}"
1810 )))
1811 }
1812 };
1813 BigEndian::write_u32(
1814 &mut self.catalog_data[rec + cr_off..rec + cr_off + 4],
1815 create,
1816 );
1817 BigEndian::write_u32(
1818 &mut self.catalog_data[rec + md_off..rec + md_off + 4],
1819 modify,
1820 );
1821 BigEndian::write_u32(
1822 &mut self.catalog_data[rec + bk_off..rec + bk_off + 4],
1823 backup,
1824 );
1825 Ok(())
1826 })();
1827 if result.is_err() {
1828 self.restore_snapshot(snap);
1829 }
1830 result
1831 }
1832
1833 pub fn set_file_locked(
1836 &mut self,
1837 entry: &FileEntry,
1838 locked: bool,
1839 ) -> Result<(), FilesystemError> {
1840 if entry.is_directory() {
1841 return Err(FilesystemError::InvalidData(
1842 "set_file_locked: entry is a directory".into(),
1843 ));
1844 }
1845 let snap = self.snapshot();
1846 let result = (|| {
1847 let rec = self.locate_record_data(entry.location as u32)?;
1848 if self.catalog_data[rec] as i8 != CATALOG_FILE {
1849 return Err(FilesystemError::InvalidData("not a file record".into()));
1850 }
1851 let flags = self.catalog_data[rec + 2];
1852 self.catalog_data[rec + 2] = if locked { flags | 0x01 } else { flags & !0x01 };
1853 Ok(())
1854 })();
1855 if result.is_err() {
1856 self.restore_snapshot(snap);
1857 }
1858 result
1859 }
1860
1861 pub fn set_volume_dates(&mut self, create: u32, modify: u32, backup: u32) {
1865 self.pending_volume_dates = Some((create, modify, backup));
1866 self.mdb.create_date = create;
1867 self.mdb.modify_date = modify;
1868 BigEndian::write_u32(&mut self.mdb.raw_sector[2..6], create);
1869 BigEndian::write_u32(&mut self.mdb.raw_sector[6..10], modify);
1870 BigEndian::write_u32(&mut self.mdb.raw_sector[64..68], backup);
1871 }
1872
1873 pub fn set_boot_blocks(&mut self, blocks: &[u8; 1024]) {
1876 self.pending_boot_blocks = Some(Box::new(*blocks));
1877 }
1878
1879 pub fn set_volume_finder_info(&mut self, finder_info: &[u8; 32]) {
1883 for i in 0..8 {
1884 self.mdb.finder_info[i] = BigEndian::read_u32(&finder_info[i * 4..(i + 1) * 4]);
1885 }
1886 }
1887
1888 pub fn set_volume_name(&mut self, new_name: &str) -> Result<(), FilesystemError>
1894 where
1895 R: Write,
1896 {
1897 let new_name_raw = utf8_to_mac_roman(new_name)?;
1898 if new_name_raw.is_empty() {
1899 return Err(FilesystemError::InvalidData(
1900 "HFS volume name must not be empty (Mac OS rejects unnamed volumes with -127)"
1901 .into(),
1902 ));
1903 }
1904 if new_name_raw.len() > 27 {
1905 return Err(FilesystemError::InvalidData(format!(
1906 "HFS volume name '{new_name}' exceeds 27 Mac Roman bytes"
1907 )));
1908 }
1909
1910 let snap = self.snapshot();
1911 let result = (|| -> Result<(), FilesystemError> {
1912 let old_name_raw = self.mdb.volume_name_raw.clone();
1913 if new_name_raw == old_name_raw {
1914 return Ok(());
1915 }
1916
1917 let (node_idx, rec_idx, key_abs) = self
1919 .find_catalog_record_by_name(1, &old_name_raw)
1920 .ok_or_else(|| {
1921 FilesystemError::NotFound("root directory record not found in catalog".into())
1922 })?;
1923
1924 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1927 let node_off = node_idx as usize * node_size;
1928 let node = &self.catalog_data[node_off..node_off + node_size];
1929 let (rec_start, rec_end) = hfs_common::btree_record_range(node, node_size, rec_idx);
1930 debug_assert_eq!(key_abs, node_off + rec_start);
1932 let key_len = node[rec_start] as usize;
1933 let mut data_rel = 1 + key_len;
1934 if !data_rel.is_multiple_of(2) {
1935 data_rel += 1;
1936 }
1937 let data_bytes = node[rec_start + data_rel..rec_end].to_vec();
1938
1939 let new_key = Self::build_catalog_key(1, &new_name_raw);
1941 let mut new_record = new_key;
1942 if !new_record.len().is_multiple_of(2) {
1943 new_record.push(0);
1944 }
1945 new_record.extend_from_slice(&data_bytes);
1946 if !new_record.len().is_multiple_of(2) {
1947 new_record.push(0);
1948 }
1949
1950 self.remove_catalog_record(node_idx, rec_idx)?;
1951 self.insert_catalog_record(&new_record)?;
1952
1953 if let Some((_n, _r, t_key_abs)) = self.find_catalog_record_by_cnid(2) {
1957 let t_key_len = self.catalog_data[t_key_abs] as usize;
1958 let mut t_data_off = t_key_abs + 1 + t_key_len;
1959 if !t_data_off.is_multiple_of(2) {
1960 t_data_off += 1;
1961 }
1962 if t_data_off + 15 + 27 <= self.catalog_data.len() {
1963 self.catalog_data[t_data_off + 14] = new_name_raw.len() as u8;
1964 for i in 0..27 {
1965 self.catalog_data[t_data_off + 15 + i] = 0;
1966 }
1967 self.catalog_data[t_data_off + 15..t_data_off + 15 + new_name_raw.len()]
1968 .copy_from_slice(&new_name_raw);
1969 }
1970 }
1971
1972 self.mdb.volume_name = mac_roman_to_utf8(&new_name_raw);
1977 self.mdb.volume_name_raw = new_name_raw.clone();
1978 self.mdb.raw_sector[36] = new_name_raw.len() as u8;
1979 for i in 0..27 {
1980 self.mdb.raw_sector[37 + i] = 0;
1981 }
1982 self.mdb.raw_sector[37..37 + new_name_raw.len()].copy_from_slice(&new_name_raw);
1983
1984 Ok(())
1985 })();
1986 if result.is_err() {
1987 self.restore_snapshot(snap);
1988 }
1989 result
1990 }
1991}
1992
1993impl<R: Read + Write + Seek> HfsFilesystem<R> {
1995 fn write_block(&mut self, block: u32, data: &[u8]) -> Result<(), FilesystemError> {
1997 let offset = self.partition_offset + self.alloc_block_offset(block);
1998 self.reader.seek(SeekFrom::Start(offset))?;
1999 self.reader.write_all(data)?;
2000 Ok(())
2001 }
2002
2003 fn write_mdb(&mut self) -> Result<(), FilesystemError> {
2009 BigEndian::write_u16(&mut self.mdb.raw_sector[10..12], 0x0100);
2017
2018 let (root_files, root_dirs) = match self.list_children(2) {
2024 Ok(children) => {
2025 let mut f = 0u16;
2026 let mut d = 0u16;
2027 for c in &children {
2028 match c {
2029 CatalogRecord::File { .. } => f = f.saturating_add(1),
2030 CatalogRecord::Directory { .. } => d = d.saturating_add(1),
2031 }
2032 }
2033 (f, d)
2034 }
2035 Err(_) => (0, 0),
2036 };
2037 BigEndian::write_u16(&mut self.mdb.raw_sector[12..14], root_files);
2038 BigEndian::write_u16(&mut self.mdb.raw_sector[82..84], root_dirs);
2039
2040 let mdb_bytes = self.mdb.serialize_to_sector();
2041 self.reader
2042 .seek(SeekFrom::Start(self.partition_offset + 1024))?;
2043 self.reader.write_all(&mdb_bytes)?;
2044 if let Some(alt_offset) = self.alt_mdb_offset() {
2048 self.reader.seek(SeekFrom::Start(alt_offset))?;
2049 self.reader.write_all(&mdb_bytes)?;
2050 }
2051 Ok(())
2052 }
2053
2054 fn write_catalog(&mut self) -> Result<(), FilesystemError> {
2056 write_hfs_fork_data(
2057 &mut self.reader,
2058 self.partition_offset,
2059 &self.mdb,
2060 &self.mdb.catalog_file_extents,
2061 &self.catalog_data,
2062 )?;
2063 let inline_blocks: u32 = self
2066 .mdb
2067 .catalog_file_extents
2068 .iter()
2069 .map(|e| e.block_count as u32)
2070 .sum();
2071 let inline_bytes = inline_blocks as usize * self.mdb.block_size as usize;
2072 if self.catalog_data.len() <= inline_bytes {
2073 return Ok(());
2074 }
2075 self.ensure_extents_overflow()?;
2076 let Some(ext_data) = self.extents_overflow_data.as_ref() else {
2077 return Err(FilesystemError::InvalidData(
2078 "catalog file runs past its inline extents and the volume has no extents-overflow tree".into(),
2079 ));
2080 };
2081 let overflow = collect_fork_overflow_extents(ext_data, CNID_CATALOG, 0x00, inline_blocks);
2082 let first_alloc = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
2083 let mut written = inline_bytes;
2084 for ext in overflow {
2085 if written >= self.catalog_data.len() {
2086 break;
2087 }
2088 let off = first_alloc + ext.start_block as u64 * self.mdb.block_size as u64;
2089 let len = (ext.block_count as usize * self.mdb.block_size as usize)
2090 .min(self.catalog_data.len() - written);
2091 self.reader.seek(SeekFrom::Start(off))?;
2092 self.reader
2093 .write_all(&self.catalog_data[written..written + len])?;
2094 written += len;
2095 }
2096 if written < self.catalog_data.len() {
2097 return Err(FilesystemError::InvalidData(format!(
2098 "catalog file: extents cover {written} of {} bytes",
2099 self.catalog_data.len()
2100 )));
2101 }
2102 Ok(())
2103 }
2104
2105 fn write_volume_bitmap(&mut self) -> Result<(), FilesystemError> {
2107 if let Some(ref bm) = self.bitmap {
2108 let bitmap_offset = self.partition_offset + self.mdb.volume_bitmap_block as u64 * 512;
2109 self.reader.seek(SeekFrom::Start(bitmap_offset))?;
2110 self.reader.write_all(bm)?;
2111 }
2112 Ok(())
2113 }
2114
2115 fn allocate_blocks(&mut self, count: u32) -> Result<u32, FilesystemError> {
2118 self.ensure_bitmap()?;
2119 let bitmap = self.bitmap.as_mut().unwrap();
2120 let start = bitmap_find_clear_run_be(bitmap, self.mdb.total_blocks as u32, count)
2121 .ok_or_else(|| {
2122 FilesystemError::DiskFull(format!("cannot find {} contiguous free blocks", count))
2123 })?;
2124 for i in 0..count {
2125 bitmap_set_bit_be(bitmap, start + i);
2126 }
2127 self.mdb.free_blocks = self
2129 .mdb
2130 .free_blocks
2131 .saturating_sub(count.min(u16::MAX as u32) as u16);
2132 let next = (start + count) % self.mdb.total_blocks.max(1) as u32;
2138 BigEndian::write_u16(&mut self.mdb.raw_sector[16..18], next as u16);
2139 Ok(start)
2140 }
2141
2142 fn allocate_extents(&mut self, count: u32) -> Result<Vec<HfsExtDescriptor>, FilesystemError> {
2145 if count == 0 {
2146 return Ok(Vec::new());
2147 }
2148 if count > u16::MAX as u32 {
2149 return Err(FilesystemError::InvalidData(
2150 "file too large for classic HFS (>65535 blocks)".into(),
2151 ));
2152 }
2153 if let Ok(start) = self.allocate_blocks(count) {
2154 return Ok(vec![HfsExtDescriptor {
2155 start_block: start as u16,
2156 block_count: count as u16,
2157 }]);
2158 }
2159 self.ensure_bitmap()?;
2160 let total_blocks = self.mdb.total_blocks as u32;
2161 let free = self.mdb.free_blocks as u32;
2162 if free < count {
2163 return Err(FilesystemError::DiskFull(format!(
2164 "{free} blocks free, {count} requested"
2165 )));
2166 }
2167 let bitmap = self.bitmap.as_mut().unwrap();
2168 let mut extents = Vec::new();
2169 let mut remaining = count;
2170 for (start, len) in bitmap_collect_clear_runs_be(bitmap, total_blocks) {
2171 if remaining == 0 {
2172 break;
2173 }
2174 let take = len.min(remaining);
2175 extents.push(HfsExtDescriptor {
2176 start_block: start as u16,
2177 block_count: take as u16,
2178 });
2179 remaining -= take;
2180 }
2181 if remaining > 0 {
2182 return Err(FilesystemError::DiskFull(format!(
2183 "needed {count} blocks, short by {remaining} after gathering every free run"
2184 )));
2185 }
2186 for ext in &extents {
2187 for i in 0..ext.block_count as u32 {
2188 bitmap_set_bit_be(bitmap, ext.start_block as u32 + i);
2189 }
2190 }
2191 self.mdb.free_blocks = self.mdb.free_blocks.saturating_sub(count as u16);
2192 let last = extents.last().unwrap();
2193 let next = (last.start_block as u32 + last.block_count as u32) % total_blocks.max(1);
2194 BigEndian::write_u16(&mut self.mdb.raw_sector[16..18], next as u16);
2195 Ok(extents)
2196 }
2197
2198 fn free_blocks(&mut self, start: u32, count: u32) {
2200 self.ensure_bitmap().ok();
2201 if let Some(ref mut bitmap) = self.bitmap {
2202 for i in 0..count {
2203 bitmap_clear_bit_be(bitmap, start + i);
2204 }
2205 }
2206 self.mdb.free_blocks += count as u16;
2207 }
2208
2209 fn free_extent_blocks(&mut self, extents: &[HfsExtDescriptor; 3]) {
2211 for ext in extents {
2212 if ext.block_count == 0 {
2213 break;
2214 }
2215 self.free_blocks(ext.start_block as u32, ext.block_count as u32);
2216 }
2217 }
2218
2219 fn free_fork_overflow(&mut self, file_id: u32, fork_type: u8) -> Result<bool, FilesystemError> {
2221 self.ensure_extents_overflow()?;
2222 let Some(data) = self.extents_overflow_data.as_ref() else {
2223 return Ok(false);
2224 };
2225 let extents = collect_fork_overflow_extents(data, file_id, fork_type, 0);
2226 for ext in &extents {
2227 self.free_blocks(ext.start_block as u32, ext.block_count as u32);
2228 }
2229 let data = self.extents_overflow_data.as_mut().expect("loaded above");
2230 Ok(remove_fork_overflow_records(data, file_id, fork_type)? > 0)
2231 }
2232
2233 fn write_extents_overflow_back(&mut self) -> Result<(), FilesystemError> {
2235 if let Some(data) = self.extents_overflow_data.as_ref() {
2236 if self.mdb.extents_file_size > 0 {
2237 write_extents_overflow_data(
2238 &mut self.reader,
2239 self.partition_offset,
2240 &self.mdb,
2241 data,
2242 )?;
2243 }
2244 }
2245 Ok(())
2246 }
2247
2248 fn write_data_to_blocks(
2251 &mut self,
2252 data: &mut dyn std::io::Read,
2253 data_len: u64,
2254 ) -> Result<Vec<HfsExtDescriptor>, FilesystemError> {
2255 if data_len == 0 {
2256 return Ok(Vec::new());
2257 }
2258 let block_size = self.mdb.block_size as u64;
2259 let blocks_needed = data_len.div_ceil(block_size) as u32;
2260 let extents = self.allocate_extents(blocks_needed)?;
2261
2262 let mut buf = vec![0u8; block_size as usize];
2263 let mut remaining = data_len;
2264 for ext in &extents {
2265 for i in 0..ext.block_count as u32 {
2266 let to_read = remaining.min(block_size) as usize;
2267 buf.fill(0);
2268 data.read_exact(&mut buf[..to_read]).map_err(|e| {
2269 FilesystemError::Io(std::io::Error::new(
2270 e.kind(),
2271 format!("reading file data: {e}"),
2272 ))
2273 })?;
2274 self.write_block(ext.start_block as u32 + i, &buf)?;
2275 remaining -= to_read as u64;
2276 }
2277 }
2278
2279 Ok(extents)
2280 }
2281
2282 fn extents_block_total(extents: &[HfsExtDescriptor]) -> u16 {
2284 extents.iter().map(|e| e.block_count).sum()
2285 }
2286
2287 fn set_inline_extents(rec: &mut [u8], base: usize, extents: &[HfsExtDescriptor]) {
2289 rec[base..base + 12].fill(0);
2290 for (i, ext) in extents.iter().take(3).enumerate() {
2291 let off = base + i * 4;
2292 BigEndian::write_u16(&mut rec[off..off + 2], ext.start_block);
2293 BigEndian::write_u16(&mut rec[off + 2..off + 4], ext.block_count);
2294 }
2295 }
2296
2297 fn insert_fork_overflow(
2300 &mut self,
2301 file_id: u32,
2302 fork_type: u8,
2303 extents: &[HfsExtDescriptor],
2304 ) -> Result<bool, FilesystemError> {
2305 use crate::hfs_common::compare_classic_extents_keys;
2306 if extents.len() <= 3 {
2307 return Ok(false);
2308 }
2309 self.ensure_extents_overflow()?;
2310 if self.extents_overflow_data.is_none() {
2311 return Err(FilesystemError::Unsupported(
2312 "volume has no extents-overflow B-tree; a fragmented fork cannot be created".into(),
2313 ));
2314 }
2315 let mut fork_block: u32 = extents.iter().take(3).map(|e| e.block_count as u32).sum();
2316 for chunk in extents[3..].chunks(3) {
2317 let mut rec = vec![7u8, fork_type];
2319 rec.extend_from_slice(&file_id.to_be_bytes());
2320 rec.extend_from_slice(&(fork_block as u16).to_be_bytes());
2321 for i in 0..3 {
2322 let (start, count) = chunk
2323 .get(i)
2324 .map_or((0, 0), |e| (e.start_block, e.block_count));
2325 rec.extend_from_slice(&start.to_be_bytes());
2326 rec.extend_from_slice(&count.to_be_bytes());
2327 }
2328 let mut grown = false;
2329 loop {
2330 let data = self.extents_overflow_data.as_mut().expect("loaded above");
2331 match hfs_common::btree_insert_full(
2332 data,
2333 &rec,
2334 &BTreeKeyFormat::CLASSIC_EXTENTS,
2335 &compare_classic_extents_keys,
2336 ) {
2337 Ok(()) => break,
2338 Err(FilesystemError::DiskFull(_)) if !grown => {
2339 self.grow_btree(HfsTree::Extents)?;
2340 grown = true;
2341 }
2342 Err(e) => return Err(e),
2343 }
2344 }
2345 fork_block += chunk.iter().map(|e| e.block_count as u32).sum::<u32>();
2346 }
2347 Ok(true)
2348 }
2349
2350 #[allow(clippy::too_many_arguments)]
2356 fn build_file_record(
2357 file_id: u32,
2358 data_size: u32,
2359 data_start: u16,
2360 data_blocks: u16,
2361 rsrc_size: u32,
2362 rsrc_start: u16,
2363 rsrc_blocks: u16,
2364 type_code: &[u8; 4],
2365 creator_code: &[u8; 4],
2366 block_size: u32,
2367 mtime_secs: Option<u64>,
2368 finder_flags: u16,
2369 ) -> [u8; 102] {
2370 let mut rec = [0u8; 102];
2371 let now = match mtime_secs {
2373 Some(s) => disk_core::times::unix_to_mac_local(s),
2374 None => disk_core::times::mac_local_now(),
2375 };
2376 rec[0] = CATALOG_FILE as u8; rec[4..8].copy_from_slice(type_code);
2380 rec[8..12].copy_from_slice(creator_code);
2381 BigEndian::write_u16(&mut rec[12..14], finder_flags); BigEndian::write_u32(&mut rec[20..24], file_id);
2384 BigEndian::write_u32(&mut rec[26..30], data_size);
2392 BigEndian::write_u32(&mut rec[30..34], data_blocks as u32 * block_size);
2394 BigEndian::write_u32(&mut rec[36..40], rsrc_size);
2397 BigEndian::write_u32(&mut rec[40..44], rsrc_blocks as u32 * block_size);
2399 BigEndian::write_u32(&mut rec[44..48], now);
2401 BigEndian::write_u32(&mut rec[48..52], now);
2403 if data_blocks > 0 {
2405 BigEndian::write_u16(&mut rec[74..76], data_start);
2406 BigEndian::write_u16(&mut rec[76..78], data_blocks);
2407 }
2408 if rsrc_blocks > 0 {
2410 BigEndian::write_u16(&mut rec[86..88], rsrc_start);
2411 BigEndian::write_u16(&mut rec[88..90], rsrc_blocks);
2412 }
2413 rec
2414 }
2415
2416 fn build_dir_record_with_dates(dir_id: u32, mtime_secs: Option<u64>) -> [u8; 70] {
2421 let mut rec = [0u8; 70];
2422 let now = match mtime_secs {
2424 Some(s) => disk_core::times::unix_to_mac_local(s),
2425 None => disk_core::times::mac_local_now(),
2426 };
2427 rec[0] = CATALOG_DIR as u8; BigEndian::write_u32(&mut rec[6..10], dir_id);
2432 BigEndian::write_u32(&mut rec[10..14], now);
2434 BigEndian::write_u32(&mut rec[14..18], now);
2436 rec
2437 }
2438
2439 pub(crate) fn build_thread_record(
2442 thread_type: i8,
2443 parent_id: u32,
2444 name: &[u8],
2445 ) -> (Vec<u8>, Vec<u8>) {
2446 let mut rec = Vec::with_capacity(46);
2450 rec.push(thread_type as u8); rec.push(0); rec.extend_from_slice(&[0u8; 8]); let mut buf4 = [0u8; 4];
2454 BigEndian::write_u32(&mut buf4, parent_id);
2455 rec.extend_from_slice(&buf4); let name_len = name.len().min(31);
2458 let mut str31 = [0u8; 32];
2459 str31[0] = name_len as u8;
2460 str31[1..1 + name_len].copy_from_slice(&name[..name_len]);
2461 rec.extend_from_slice(&str31);
2462 (vec![], rec) }
2465
2466 fn do_sync_metadata(&mut self) -> Result<(), FilesystemError> {
2468 if let Some((create, modify, backup)) = self.pending_volume_dates.take() {
2469 self.mdb.create_date = create;
2470 self.mdb.modify_date = modify;
2471 BigEndian::write_u32(&mut self.mdb.raw_sector[2..6], create);
2472 BigEndian::write_u32(&mut self.mdb.raw_sector[6..10], modify);
2473 BigEndian::write_u32(&mut self.mdb.raw_sector[64..68], backup);
2474 } else {
2475 self.mdb.modify_date = disk_core::times::mac_local_now();
2476 }
2477 if let Some(blocks) = self.pending_boot_blocks.take() {
2478 self.reader.seek(SeekFrom::Start(self.partition_offset))?;
2479 self.reader.write_all(&blocks[..])?;
2480 }
2481 self.write_catalog()?;
2482 self.write_volume_bitmap()?;
2483 self.write_mdb()?;
2484 self.reader.flush()?;
2485 Ok(())
2486 }
2487
2488 fn ensure_catalog_initialized(&mut self) -> Result<(), FilesystemError> {
2494 if self.btrees_initialized {
2498 return Ok(());
2499 }
2500 let catalog_blank = is_catalog_uninitialized(&self.catalog_data);
2501 let extents_data = if self.mdb.extents_file_size > 0 {
2503 read_fork_data(
2504 &mut self.reader,
2505 self.partition_offset,
2506 &self.mdb,
2507 &self.mdb.extents_file_extents,
2508 self.mdb.extents_file_size as u64,
2509 )
2510 .ok()
2511 } else {
2512 None
2513 };
2514 let extents_blank = extents_data
2515 .as_deref()
2516 .map(is_catalog_uninitialized)
2517 .unwrap_or(false);
2518 if !catalog_blank && !extents_blank {
2519 self.btrees_initialized = true;
2520 return Ok(());
2521 }
2522 self.initialize_empty_btrees(catalog_blank, extents_blank)?;
2523 self.btrees_initialized = true;
2524 Ok(())
2525 }
2526
2527 fn initialize_empty_btrees(
2528 &mut self,
2529 init_catalog: bool,
2530 init_extents: bool,
2531 ) -> Result<(), FilesystemError> {
2532 if init_catalog {
2533 let catalog_size = self.catalog_data.len();
2534 if catalog_size < 1024 {
2535 return Err(FilesystemError::InvalidData(format!(
2536 "cannot initialize empty HFS catalog: region is only {catalog_size} bytes"
2537 )));
2538 }
2539 let vol_name = self.mdb.volume_name_raw.clone();
2543 let node_size = pick_btree_node_size(catalog_size as u64) as usize;
2544 let buf = build_empty_hfs_catalog_with_node_size(
2545 catalog_size,
2546 node_size,
2547 &vol_name,
2548 hfs_common::hfs_now(),
2549 )?;
2550 self.catalog_data = buf;
2551 self.write_catalog()?;
2552 }
2553
2554 if init_extents {
2555 let extents_size = self.mdb.extents_file_size as usize;
2556 if extents_size >= 512 {
2557 let buf = build_empty_hfs_extents_btree(extents_size)?;
2558 write_hfs_fork_data(
2559 &mut self.reader,
2560 self.partition_offset,
2561 &self.mdb,
2562 &self.mdb.extents_file_extents,
2563 &buf,
2564 )?;
2565 }
2566 }
2567
2568 let bitmap_size = (self.mdb.total_blocks as u32).div_ceil(8) as usize;
2572 let mut bitmap = vec![0u8; bitmap_size];
2573 let mut used_blocks = 0u32;
2574 for ext in self.mdb.catalog_file_extents.iter() {
2575 if ext.block_count == 0 {
2576 break;
2577 }
2578 for i in 0..ext.block_count {
2579 let bit = ext.start_block as u32 + i as u32;
2580 if bit < self.mdb.total_blocks as u32 {
2581 hfs_common::bitmap_set_bit_be(&mut bitmap, bit);
2582 used_blocks += 1;
2583 }
2584 }
2585 }
2586 for ext in self.mdb.extents_file_extents.iter() {
2587 if ext.block_count == 0 {
2588 break;
2589 }
2590 for i in 0..ext.block_count {
2591 let bit = ext.start_block as u32 + i as u32;
2592 if bit < self.mdb.total_blocks as u32 {
2593 hfs_common::bitmap_set_bit_be(&mut bitmap, bit);
2594 used_blocks += 1;
2595 }
2596 }
2597 }
2598 self.bitmap = Some(bitmap);
2599 self.mdb.free_blocks = (self.mdb.total_blocks as u32).saturating_sub(used_blocks) as u16;
2600
2601 self.mdb.file_count = 0;
2603 self.mdb.folder_count = 0;
2604 if self.mdb.next_catalog_id < 16 {
2605 self.mdb.next_catalog_id = 16;
2606 }
2607 self.mdb.modify_date = disk_core::times::mac_local_now();
2608
2609 self.write_volume_bitmap()?;
2610 self.write_mdb()?;
2611 self.reader.flush()?;
2612 Ok(())
2613 }
2614}
2615
2616fn pick_btree_node_size(_target_bytes: u64) -> u16 {
2631 512
2632}
2633
2634pub const HFS_MAX_BTREE_FILE_SIZE: u32 = 16 * 1024 * 1024;
2640
2641fn hfs_map_nodes_required(total_nodes: u32, node_size: usize) -> u32 {
2644 let header_cap = ((node_size - 256) * 8) as u32; if total_nodes <= header_cap {
2646 return 0;
2647 }
2648 let per_map = (hfs_common::map_node_bitmap_bytes(node_size) * 8) as u32;
2649 if per_map == 0 {
2650 return 0;
2651 }
2652 let extra = total_nodes - header_cap;
2653 extra.div_ceil(per_map)
2654}
2655
2656fn build_empty_hfs_catalog_with_node_size(
2657 catalog_size: usize,
2658 node_size: usize,
2659 volume_name_raw: &[u8],
2660 now: u32,
2661) -> Result<Vec<u8>, FilesystemError> {
2662 if volume_name_raw.len() > 27 {
2663 return Err(FilesystemError::InvalidData(format!(
2664 "HFS volume name length {} exceeds 27 Mac Roman bytes",
2665 volume_name_raw.len()
2666 )));
2667 }
2668 let total_nodes = (catalog_size / node_size) as u32;
2669 if total_nodes < 2 {
2670 return Err(FilesystemError::InvalidData(format!(
2671 "cannot initialize empty HFS catalog: only {total_nodes} node(s) fit"
2672 )));
2673 }
2674 let map_nodes = hfs_map_nodes_required(total_nodes, node_size);
2677 let used_nodes = 2 + map_nodes;
2678 if total_nodes < used_nodes {
2679 return Err(FilesystemError::InvalidData(format!(
2680 "cannot initialize empty HFS catalog: {total_nodes} nodes < {used_nodes} required \
2681 (header + leaf + {map_nodes} map nodes)"
2682 )));
2683 }
2684 let mut buf = vec![0u8; catalog_size];
2685
2686 if map_nodes > 0 {
2689 BigEndian::write_u32(&mut buf[0..4], 2);
2690 }
2691 buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
2692 buf[9] = 0;
2693 BigEndian::write_u16(&mut buf[10..12], 3);
2694
2695 BigEndian::write_u16(&mut buf[14..16], 1); BigEndian::write_u32(&mut buf[16..20], 1); BigEndian::write_u32(&mut buf[20..24], 2); BigEndian::write_u32(&mut buf[24..28], 1); BigEndian::write_u32(&mut buf[28..32], 1); BigEndian::write_u16(&mut buf[32..34], node_size as u16); BigEndian::write_u16(&mut buf[34..36], 37); BigEndian::write_u32(&mut buf[36..40], total_nodes);
2704 BigEndian::write_u32(&mut buf[40..44], total_nodes - used_nodes); let bitmap_rec_offset = 248usize;
2713 let header_bitmap_len = node_size - 256; for bit in 0..used_nodes {
2715 let local = bit;
2717 let byte_idx = (local / 8) as usize;
2718 let bit_in_byte = 7 - (local % 8) as u8;
2719 if byte_idx < header_bitmap_len {
2720 buf[bitmap_rec_offset + byte_idx] |= 1u8 << bit_in_byte;
2721 }
2722 }
2723
2724 let free_offset = (node_size - 8) as u16;
2726 BigEndian::write_u16(&mut buf[node_size - 2..node_size], 14);
2727 BigEndian::write_u16(&mut buf[node_size - 4..node_size - 2], 120);
2728 BigEndian::write_u16(
2729 &mut buf[node_size - 6..node_size - 4],
2730 bitmap_rec_offset as u16,
2731 );
2732 BigEndian::write_u16(&mut buf[node_size - 8..node_size - 6], free_offset);
2733
2734 let leaf = node_size;
2736 buf[leaf + 8] = hfs_common::BTREE_LEAF_NODE as u8; buf[leaf + 9] = 1; BigEndian::write_u16(&mut buf[leaf + 10..leaf + 12], 2); let name_len = volume_name_raw.len();
2744 let r0_key = leaf + 14;
2745 let key_data_len: u8 = (1 + 4 + 1 + name_len) as u8; buf[r0_key] = key_data_len;
2747 buf[r0_key + 1] = 0;
2748 BigEndian::write_u32(&mut buf[r0_key + 2..r0_key + 6], 1);
2749 buf[r0_key + 6] = name_len as u8;
2750 buf[r0_key + 7..r0_key + 7 + name_len].copy_from_slice(volume_name_raw);
2751 let r0_key_total = 1 + key_data_len as usize;
2754 let r0_pad = r0_key_total & 1;
2755 let r0_data = r0_key + r0_key_total + r0_pad;
2756 buf[r0_data] = CATALOG_DIR as u8;
2757 BigEndian::write_u32(&mut buf[r0_data + 6..r0_data + 10], 2); BigEndian::write_u32(&mut buf[r0_data + 10..r0_data + 14], now); BigEndian::write_u32(&mut buf[r0_data + 14..r0_data + 18], now); let r1_key = r0_data + 70;
2767 buf[r1_key] = 6;
2768 buf[r1_key + 1] = 0;
2769 BigEndian::write_u32(&mut buf[r1_key + 2..r1_key + 6], 2);
2770 buf[r1_key + 6] = 0;
2771 buf[r1_key + 7] = 0;
2772 let r1_data = r1_key + 8;
2773 buf[r1_data] = CATALOG_DIR_THREAD as u8;
2774 BigEndian::write_u32(&mut buf[r1_data + 10..r1_data + 14], 1); buf[r1_data + 14] = name_len as u8;
2776 buf[r1_data + 15..r1_data + 15 + name_len].copy_from_slice(volume_name_raw);
2777 let free_rel = (r1_data + 46 - leaf) as u16;
2780
2781 BigEndian::write_u16(&mut buf[leaf + node_size - 2..leaf + node_size], 14);
2783 BigEndian::write_u16(
2784 &mut buf[leaf + node_size - 4..leaf + node_size - 2],
2785 (r1_key - leaf) as u16,
2786 );
2787 BigEndian::write_u16(
2788 &mut buf[leaf + node_size - 6..leaf + node_size - 4],
2789 free_rel,
2790 );
2791
2792 for i in 0..map_nodes {
2794 let node_idx = 2 + i;
2795 let next_link = if i + 1 < map_nodes { node_idx + 1 } else { 0 };
2796 let prev_link = if i == 0 { 0 } else { node_idx - 1 };
2797 hfs_common::init_map_node(&mut buf, node_size, node_idx, prev_link, next_link);
2798 }
2799
2800 Ok(buf)
2801}
2802
2803fn build_empty_hfs_extents_btree(size: usize) -> Result<Vec<u8>, FilesystemError> {
2807 build_empty_hfs_extents_btree_with_node_size(size, 512)
2808}
2809
2810fn build_empty_hfs_extents_btree_with_node_size(
2811 size: usize,
2812 node_size: usize,
2813) -> Result<Vec<u8>, FilesystemError> {
2814 let total_nodes = (size / node_size) as u32;
2815 if total_nodes < 1 {
2816 return Err(FilesystemError::InvalidData(format!(
2817 "cannot initialize empty HFS extents B-tree: region is only {size} bytes"
2818 )));
2819 }
2820 let map_nodes = hfs_map_nodes_required(total_nodes, node_size);
2821 let used_nodes = 1 + map_nodes;
2822 if total_nodes < used_nodes {
2823 return Err(FilesystemError::InvalidData(format!(
2824 "cannot initialize empty HFS extents B-tree: {total_nodes} nodes < {used_nodes} \
2825 required (header + {map_nodes} map nodes)"
2826 )));
2827 }
2828 let mut buf = vec![0u8; size];
2829
2830 if map_nodes > 0 {
2832 BigEndian::write_u32(&mut buf[0..4], 1);
2833 }
2834 buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
2835 buf[9] = 0;
2836 BigEndian::write_u16(&mut buf[10..12], 3);
2837
2838 BigEndian::write_u16(&mut buf[14..16], 0); BigEndian::write_u32(&mut buf[16..20], 0); BigEndian::write_u32(&mut buf[20..24], 0); BigEndian::write_u32(&mut buf[24..28], 0); BigEndian::write_u32(&mut buf[28..32], 0); BigEndian::write_u16(&mut buf[32..34], node_size as u16); BigEndian::write_u16(&mut buf[34..36], 7); BigEndian::write_u32(&mut buf[36..40], total_nodes);
2847 BigEndian::write_u32(&mut buf[40..44], total_nodes - used_nodes);
2848
2849 let bitmap_rec_offset = 248usize;
2853 let header_bitmap_len = node_size - 256;
2854 for bit in 0..used_nodes {
2855 let byte_idx = (bit / 8) as usize;
2856 let bit_in_byte = 7 - (bit % 8) as u8;
2857 if byte_idx < header_bitmap_len {
2858 buf[bitmap_rec_offset + byte_idx] |= 1u8 << bit_in_byte;
2859 }
2860 }
2861
2862 let free_offset = (node_size - 8) as u16;
2863 BigEndian::write_u16(&mut buf[node_size - 2..node_size], 14);
2864 BigEndian::write_u16(&mut buf[node_size - 4..node_size - 2], 120);
2865 BigEndian::write_u16(
2866 &mut buf[node_size - 6..node_size - 4],
2867 bitmap_rec_offset as u16,
2868 );
2869 BigEndian::write_u16(&mut buf[node_size - 8..node_size - 6], free_offset);
2870
2871 for i in 0..map_nodes {
2873 let node_idx = 1 + i;
2874 let next_link = if i + 1 < map_nodes { node_idx + 1 } else { 0 };
2875 let prev_link = if i == 0 { 0 } else { node_idx - 1 };
2876 hfs_common::init_map_node(&mut buf, node_size, node_idx, prev_link, next_link);
2877 }
2878
2879 Ok(buf)
2880}
2881
2882pub fn create_blank_hfs(
2903 target_size_bytes: u64,
2904 block_size: u32,
2905 volume_name: &str,
2906) -> Result<Vec<u8>, FilesystemError> {
2907 create_blank_hfs_sized(target_size_bytes, block_size, volume_name, 0, 0)
2908}
2909
2910pub fn default_btree_sizes(target_volume_bytes: u64, block_size: u32) -> (u32, u32) {
2921 if block_size == 0 {
2922 return (0, 0);
2923 }
2924 let clump = 4u64 * block_size as u64;
2925 let floor = 24u64 * block_size as u64;
2926 let raw = target_volume_bytes / 200; let catalog = raw.max(floor).div_ceil(clump) * clump;
2928 let extents = (raw / 2).max(floor).div_ceil(clump) * clump;
2929 (
2930 catalog.min(u32::MAX as u64) as u32,
2931 extents.min(u32::MAX as u64) as u32,
2932 )
2933}
2934
2935pub fn clone_target_btree_sizes(
2952 target_size: u64,
2953 block_size: u32,
2954 source_catalog: u32,
2955 source_extents: u32,
2956) -> (u32, u32) {
2957 let (default_cat, default_ext) = default_btree_sizes(target_size, block_size);
2958 let catalog = source_catalog
2959 .saturating_mul(3)
2960 .saturating_div(2)
2961 .max(default_cat)
2962 .min(HFS_MAX_BTREE_FILE_SIZE);
2963 let extents = source_extents.max(default_ext).min(HFS_MAX_BTREE_FILE_SIZE);
2964 (catalog, extents)
2965}
2966
2967pub fn pick_block_size(volume_bytes: u64) -> u32 {
2970 let mut bs: u32 = 512;
2971 while volume_bytes / bs as u64 > 65535 {
2972 bs = bs.saturating_mul(2);
2973 if bs == 0 {
2974 return 1 << 16;
2975 }
2976 }
2977 bs
2978}
2979
2980pub fn create_blank_hfs_sized(
2987 target_size_bytes: u64,
2988 block_size: u32,
2989 volume_name: &str,
2990 min_extents_bytes: u32,
2991 min_catalog_bytes: u32,
2992) -> Result<Vec<u8>, FilesystemError> {
2993 create_blank_hfs_sized_at(
2994 target_size_bytes,
2995 block_size,
2996 volume_name,
2997 min_extents_bytes,
2998 min_catalog_bytes,
2999 hfs_common::hfs_now(),
3000 )
3001}
3002
3003pub(crate) fn create_blank_hfs_sized_at(
3005 target_size_bytes: u64,
3006 block_size: u32,
3007 volume_name: &str,
3008 min_extents_bytes: u32,
3009 min_catalog_bytes: u32,
3010 now: u32,
3011) -> Result<Vec<u8>, FilesystemError> {
3012 let (front, mdb, image_size) = build_blank_hfs_front(
3013 target_size_bytes,
3014 block_size,
3015 volume_name,
3016 min_extents_bytes,
3017 min_catalog_bytes,
3018 now,
3019 )?;
3020 let mut img = vec![0u8; image_size as usize];
3021 img[..front.len()].copy_from_slice(&front);
3022 let alt_off = image_size as usize - 1024;
3023 img[alt_off..alt_off + 512].copy_from_slice(&mdb);
3024 Ok(img)
3025}
3026
3027pub fn write_blank_hfs_into<W: Write + Seek>(
3036 target: &mut W,
3037 at_offset: u64,
3038 target_size_bytes: u64,
3039 block_size: u32,
3040 volume_name: &str,
3041) -> Result<u64, FilesystemError> {
3042 write_blank_hfs_into_at(
3043 target,
3044 at_offset,
3045 target_size_bytes,
3046 block_size,
3047 volume_name,
3048 hfs_common::hfs_now(),
3049 )
3050}
3051
3052pub(crate) fn write_blank_hfs_into_at<W: Write + Seek>(
3054 target: &mut W,
3055 at_offset: u64,
3056 target_size_bytes: u64,
3057 block_size: u32,
3058 volume_name: &str,
3059 now: u32,
3060) -> Result<u64, FilesystemError> {
3061 let (front, mdb, image_size) =
3062 build_blank_hfs_front(target_size_bytes, block_size, volume_name, 0, 0, now)?;
3063 target.seek(SeekFrom::Start(at_offset))?;
3064 target.write_all(&front)?;
3065 target.seek(SeekFrom::Start(at_offset + image_size - 1024))?;
3066 target.write_all(&mdb)?;
3067 target.write_all(&[0u8; 512])?;
3068 Ok(image_size)
3069}
3070
3071fn build_blank_hfs_front(
3076 target_size_bytes: u64,
3077 block_size: u32,
3078 volume_name: &str,
3079 min_extents_bytes: u32,
3080 min_catalog_bytes: u32,
3081 now: u32,
3082) -> Result<(Vec<u8>, [u8; 512], u64), FilesystemError> {
3083 if block_size == 0 || !block_size.is_multiple_of(512) {
3084 return Err(FilesystemError::InvalidData(format!(
3085 "HFS block_size must be a non-zero multiple of 512 (got {block_size})"
3086 )));
3087 }
3088
3089 let name_raw = utf8_to_mac_roman(volume_name)?;
3090 if name_raw.len() > 27 {
3091 return Err(FilesystemError::InvalidData(format!(
3092 "HFS volume name '{volume_name}' exceeds 27 Mac Roman bytes"
3093 )));
3094 }
3095
3096 let mut first_alloc_block: u16 = 3;
3100 let total_blocks: u32;
3101 loop {
3102 let pre_alloc = first_alloc_block as u64 * 512;
3103 if target_size_bytes <= pre_alloc + 512 {
3104 return Err(FilesystemError::InvalidData(format!(
3105 "HFS volume too small ({target_size_bytes} bytes) for block_size {block_size}"
3106 )));
3107 }
3108 let usable = target_size_bytes - pre_alloc - 1024; let tb = usable / block_size as u64;
3110 if tb == 0 {
3111 return Err(FilesystemError::InvalidData(format!(
3112 "HFS volume too small for block_size {block_size}"
3113 )));
3114 }
3115 if tb > 65535 {
3116 return Err(FilesystemError::InvalidData(format!(
3117 "HFS would have {tb} allocation blocks at block_size {block_size}, exceeding u16 ceiling 65535"
3118 )));
3119 }
3120 let tb = tb as u32;
3121 let bitmap_bytes = (tb as u64).div_ceil(8);
3122 let bitmap_sectors = bitmap_bytes.div_ceil(512) as u16;
3123 let needed = 3u16 + bitmap_sectors;
3124 if needed <= first_alloc_block {
3125 total_blocks = tb;
3126 break;
3127 }
3128 first_alloc_block = needed;
3129 }
3130
3131 let (default_catalog, default_extents) = default_btree_sizes(target_size_bytes, block_size);
3135 let blocks_for = |bytes: u32, default: u32| -> u16 {
3136 let bytes = if bytes == 0 { default } else { bytes };
3137 let n = (bytes as u64).div_ceil(block_size as u64).max(4);
3138 n.min(u16::MAX as u64) as u16
3139 };
3140 let extents_blocks = blocks_for(min_extents_bytes, default_extents);
3141 let catalog_blocks = blocks_for(min_catalog_bytes, default_catalog);
3142 let extents_start: u16 = 0;
3143 let catalog_start: u16 = extents_blocks;
3144 let btree_blocks: u16 = extents_blocks + catalog_blocks;
3145
3146 if total_blocks < btree_blocks as u32 {
3147 return Err(FilesystemError::InvalidData(format!(
3148 "HFS volume too small: {total_blocks} allocation blocks is below the minimum of {btree_blocks} needed for B-trees"
3149 )));
3150 }
3151
3152 let extents_size = extents_blocks as u32 * block_size;
3153 let catalog_size = catalog_blocks as u32 * block_size;
3154
3155 let extents_node_size = pick_btree_node_size(extents_size as u64) as usize;
3159 let catalog_node_size = pick_btree_node_size(catalog_size as u64) as usize;
3160
3161 let image_size =
3164 first_alloc_block as u64 * 512 + total_blocks as u64 * block_size as u64 + 1024;
3165 let front_size = first_alloc_block as usize * 512 + btree_blocks as usize * block_size as usize;
3169 let mut front = vec![0u8; front_size];
3170
3171 let bitmap_off = 3 * 512;
3174 for b in 0..btree_blocks as u32 {
3175 let byte_idx = (b / 8) as usize;
3176 let bit_idx = 7 - (b % 8) as u8;
3177 front[bitmap_off + byte_idx] |= 1 << bit_idx;
3178 }
3179
3180 let extents_bytes =
3182 build_empty_hfs_extents_btree_with_node_size(extents_size as usize, extents_node_size)?;
3183 let extents_off = first_alloc_block as u64 * 512 + extents_start as u64 * block_size as u64;
3184 front[extents_off as usize..extents_off as usize + extents_bytes.len()]
3185 .copy_from_slice(&extents_bytes);
3186
3187 let catalog_bytes = build_empty_hfs_catalog_with_node_size(
3192 catalog_size as usize,
3193 catalog_node_size,
3194 &name_raw,
3195 now,
3196 )?;
3197 let catalog_off = first_alloc_block as u64 * 512 + catalog_start as u64 * block_size as u64;
3198 front[catalog_off as usize..catalog_off as usize + catalog_bytes.len()]
3199 .copy_from_slice(&catalog_bytes);
3200
3201 let mdb = build_blank_mdb(
3203 now,
3204 &name_raw,
3205 total_blocks as u16,
3206 block_size,
3207 first_alloc_block,
3208 catalog_start,
3209 catalog_blocks,
3210 catalog_size,
3211 extents_start,
3212 extents_blocks,
3213 extents_size,
3214 btree_blocks,
3215 );
3216 front[1024..1024 + 512].copy_from_slice(&mdb);
3217
3218 Ok((front, mdb, image_size))
3222}
3223
3224#[allow(clippy::too_many_arguments)]
3225fn build_blank_mdb(
3226 now: u32,
3227 name_raw: &[u8],
3228 total_blocks: u16,
3229 block_size: u32,
3230 first_alloc_block: u16,
3231 catalog_start: u16,
3232 catalog_blocks: u16,
3233 catalog_size: u32,
3234 extents_start: u16,
3235 extents_blocks: u16,
3236 extents_size: u32,
3237 allocated_blocks: u16,
3238) -> [u8; 512] {
3239 let mut mdb = [0u8; 512];
3240 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
3241 BigEndian::write_u32(&mut mdb[2..6], now); BigEndian::write_u32(&mut mdb[6..10], now); BigEndian::write_u16(&mut mdb[10..12], 0x0100);
3247 BigEndian::write_u16(&mut mdb[14..16], 3); BigEndian::write_u16(&mut mdb[18..20], total_blocks); BigEndian::write_u32(&mut mdb[20..24], block_size); BigEndian::write_u32(&mut mdb[24..28], 4 * block_size); BigEndian::write_u16(&mut mdb[28..30], first_alloc_block); BigEndian::write_u32(&mut mdb[30..34], 16); BigEndian::write_u16(&mut mdb[34..36], total_blocks - allocated_blocks); mdb[36] = name_raw.len() as u8;
3255 mdb[37..37 + name_raw.len()].copy_from_slice(name_raw);
3256 BigEndian::write_u32(&mut mdb[74..78], extents_size); BigEndian::write_u32(&mut mdb[78..82], catalog_size); BigEndian::write_u32(&mut mdb[130..134], extents_size); BigEndian::write_u16(&mut mdb[134..136], extents_start);
3263 BigEndian::write_u16(&mut mdb[136..138], extents_blocks);
3264 BigEndian::write_u32(&mut mdb[146..150], catalog_size); BigEndian::write_u16(&mut mdb[150..152], catalog_start);
3266 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
3267 mdb
3268}
3269
3270impl<R: Read + Seek + Send> Filesystem for HfsFilesystem<R> {
3271 fn root(&mut self) -> Result<FileEntry, FilesystemError> {
3272 Ok(FileEntry {
3273 name: "/".into(),
3274 path: "/".into(),
3275 entry_type: EntryType::Directory,
3276 size: 0,
3277 location: 2, modified: None,
3279 modified_unix: None,
3280 type_code: None,
3281 creator_code: None,
3282 symlink_target: None,
3283 special_type: None,
3284 mode: None,
3285 uid: None,
3286 gid: None,
3287 resource_fork_size: None,
3288 aux_type: None,
3289 link_target_cnid: None,
3290 amiga_protection: None,
3291 amiga_comment: None,
3292 amiga_date: None,
3293 dos_attributes: None,
3294 finder_flags: None,
3295 prodos_file_type: None,
3296 mac_dates: None,
3297 owner_label: None,
3298 native_attrs: None,
3299 qnx2_attrs: None,
3300 })
3301 }
3302
3303 fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
3304 let parent_id = entry.location as u32;
3305 let children = self.list_children(parent_id)?;
3306
3307 let mut entries = Vec::new();
3308 for child in children {
3309 match child {
3310 CatalogRecord::Directory {
3311 dir_id,
3312 name,
3313 dates,
3314 ..
3315 } => {
3316 let path = if entry.path == "/" {
3317 format!("/{name}")
3318 } else {
3319 format!("{}/{name}", entry.path)
3320 };
3321 let mut fe = FileEntry::new_directory(name, path, dir_id as u64);
3322 fe.modified = hfs_common::format_mac_date(dates.1);
3323 fe.modified_unix = disk_core::times::mac_local_to_unix(dates.1);
3324 fe.mac_dates = Some(dates);
3325 entries.push(fe);
3326 }
3327 CatalogRecord::File {
3328 file_id,
3329 name,
3330 data_size,
3331 rsrc_size,
3332 rsrc_extents,
3333 type_code,
3334 creator_code,
3335 finder_flags,
3336 dates,
3337 ..
3338 } => {
3339 let path = if entry.path == "/" {
3340 format!("/{name}")
3341 } else {
3342 format!("{}/{name}", entry.path)
3343 };
3344 let mut fe = FileEntry::new_file(name, path, data_size as u64, file_id as u64);
3345 fe.type_code = Some(type_code);
3346 fe.creator_code = Some(creator_code);
3347 fe.finder_flags = Some(finder_flags);
3348 fe.modified = hfs_common::format_mac_date(dates.1);
3349 fe.modified_unix = disk_core::times::mac_local_to_unix(dates.1);
3350 fe.mac_dates = Some(dates);
3351 if rsrc_size > 0 {
3352 fe.resource_fork_size = Some(rsrc_size as u64);
3353 }
3354 if finder_flags & crate::mac_alias::IS_ALIAS_FLAG != 0 && rsrc_size > 0 {
3358 if let Ok(rsrc) = read_fork_data(
3359 &mut self.reader,
3360 self.partition_offset,
3361 &self.mdb,
3362 &rsrc_extents,
3363 rsrc_size as u64,
3364 ) {
3365 if let Some(target) = crate::mac_alias::resolve_alias_target(&rsrc) {
3366 fe.symlink_target = Some(target);
3367 }
3368 }
3369 }
3370 entries.push(fe);
3371 }
3372 }
3373 }
3374
3375 entries.sort_by_key(|a| a.name.to_lowercase());
3376 Ok(entries)
3377 }
3378
3379 fn read_file(
3380 &mut self,
3381 entry: &FileEntry,
3382 max_bytes: usize,
3383 ) -> Result<Vec<u8>, FilesystemError> {
3384 let file_id = entry.location as u32;
3385 let (data_size, extents, _rsrc_size, _rsrc_extents) =
3386 self.find_file_by_id(file_id).ok_or_else(|| {
3387 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3388 })?;
3389
3390 let mut data = self.read_fork_with_overflow(file_id, 0x00, &extents, data_size as u64)?;
3391 data.truncate(max_bytes);
3392 Ok(data)
3393 }
3394
3395 fn write_file_to(
3396 &mut self,
3397 entry: &FileEntry,
3398 writer: &mut dyn std::io::Write,
3399 ) -> Result<u64, FilesystemError> {
3400 if entry.is_directory() {
3401 return Err(FilesystemError::NotADirectory(entry.path.clone()));
3402 }
3403 let file_id = entry.location as u32;
3404 let (data_size, extents, _rsrc_size, _rsrc_extents) =
3405 self.find_file_by_id(file_id).ok_or_else(|| {
3406 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3407 })?;
3408 self.write_fork_with_overflow_to(file_id, 0x00, &extents, data_size as u64, writer)
3409 }
3410
3411 fn volume_label(&self) -> Option<&str> {
3412 if self.mdb.volume_name.is_empty() {
3413 None
3414 } else {
3415 Some(&self.mdb.volume_name)
3416 }
3417 }
3418
3419 fn fs_type(&self) -> &str {
3420 "HFS"
3421 }
3422
3423 fn validate_name(&self, name: &str) -> Result<(), FilesystemError> {
3424 validate_hfs_create_name(name).map(|_| ())
3425 }
3426
3427 fn uses_colon_paths(&self) -> bool {
3428 true
3429 }
3430
3431 fn total_size(&self) -> u64 {
3432 self.mdb.total_blocks as u64 * self.mdb.block_size as u64
3433 }
3434
3435 fn used_size(&self) -> u64 {
3436 (self.mdb.total_blocks - self.mdb.free_blocks) as u64 * self.mdb.block_size as u64
3437 }
3438
3439 fn allocation_unit(&self) -> Option<u64> {
3445 if self.mdb.block_size == 0 {
3446 None
3447 } else {
3448 Some(self.mdb.block_size as u64)
3449 }
3450 }
3451
3452 fn last_data_byte(&mut self) -> Result<u64, FilesystemError> {
3453 let bitmap = self.read_volume_bitmap()?;
3454 let last_block = find_last_set_bit(&bitmap, self.mdb.total_blocks as u32);
3455 match last_block {
3457 Some(block) => Ok(self.alloc_block_offset(block + 1) + 1024),
3458 None => Ok(self.alloc_block_offset(self.mdb.total_blocks as u32) + 1024),
3459 }
3460 }
3461
3462 fn write_resource_fork_to(
3463 &mut self,
3464 entry: &FileEntry,
3465 writer: &mut dyn std::io::Write,
3466 ) -> Result<u64, FilesystemError> {
3467 let file_id = entry.location as u32;
3468 let (_data_size, _data_ext, rsrc_size, rsrc_extents) =
3469 self.find_file_by_id(file_id).ok_or_else(|| {
3470 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3471 })?;
3472 if rsrc_size == 0 {
3473 return Ok(0);
3474 }
3475 let data = self.read_fork_with_overflow(file_id, 0xFF, &rsrc_extents, rsrc_size as u64)?;
3476 writer.write_all(&data)?;
3477 Ok(data.len() as u64)
3478 }
3479
3480 fn resource_fork_size(&mut self, entry: &FileEntry) -> u64 {
3481 let file_id = entry.location as u32;
3482 self.find_file_by_id(file_id)
3483 .map(|(_ds, _de, rs, _re)| rs as u64)
3484 .unwrap_or(0)
3485 }
3486
3487 fn fragmentation_stats(
3488 &mut self,
3489 ) -> Option<Result<disk_core::filesystem::FragmentationStats, FilesystemError>> {
3490 Some(Ok(hfs_fragmentation_stats(&self.catalog_data)))
3491 }
3492
3493 fn blessed_system_folder(&mut self) -> Option<(u64, String)> {
3494 let cnid = self.mdb.finder_info[0];
3495 if cnid == 0 {
3496 return None;
3497 }
3498 if let Some((_node, _rec, offset)) = self.find_catalog_record_by_cnid(cnid) {
3500 let key_len = self.catalog_data[offset] as usize;
3501 let mut rec_data_start = offset + 1 + key_len;
3502 if !rec_data_start.is_multiple_of(2) {
3503 rec_data_start += 1;
3504 }
3505 if rec_data_start + 15 <= self.catalog_data.len() {
3507 let name_len = self.catalog_data[rec_data_start + 14] as usize;
3508 if rec_data_start + 15 + name_len <= self.catalog_data.len() {
3509 let name = mac_roman_to_utf8(
3510 &self.catalog_data[rec_data_start + 15..rec_data_start + 15 + name_len],
3511 );
3512 return Some((cnid as u64, name));
3513 }
3514 }
3515 }
3516 Some((cnid as u64, format!("CNID {}", cnid)))
3517 }
3518
3519 fn fsck(&mut self) -> Option<Result<disk_core::fsck::FsckResult, FilesystemError>> {
3520 Some(self.fsck())
3521 }
3522}
3523
3524const CATALOG_DIR_THREAD: i8 = 3;
3527
3528impl<R: Read + Write + Seek + Send> EditableFilesystem for HfsFilesystem<R> {
3529 fn as_filesystem(&self) -> &dyn disk_core::filesystem::Filesystem {
3530 self
3531 }
3532 fn as_filesystem_mut(&mut self) -> &mut dyn disk_core::filesystem::Filesystem {
3533 self
3534 }
3535 fn begin_bulk(&mut self) {
3536 self.bulk_mode = true;
3537 }
3538
3539 fn end_bulk(&mut self) {
3540 self.bulk_mode = false;
3541 }
3542
3543 fn create_file(
3544 &mut self,
3545 parent: &FileEntry,
3546 name: &str,
3547 data: &mut dyn std::io::Read,
3548 data_len: u64,
3549 options: &CreateFileOptions,
3550 ) -> Result<FileEntry, FilesystemError> {
3551 self.ensure_catalog_initialized()?;
3552 let snap = self.snapshot();
3553 let extents_before = self.extents_overflow_data.clone();
3554 let result = (|| {
3555 let parent_id = parent.location as u32;
3556
3557 let name_bytes = validate_hfs_create_name(name)?;
3558
3559 if self
3561 .find_catalog_record_by_name(parent_id, &name_bytes)
3562 .is_some()
3563 {
3564 return Err(FilesystemError::AlreadyExists(name.into()));
3565 }
3566
3567 let file_id = self.mdb.next_catalog_id;
3569 self.mdb.next_catalog_id += 1;
3570
3571 let (type_code, creator_code) = hfs_common::resolve_create_type_creator(
3575 name,
3576 options.os_type,
3577 options.os_creator,
3578 options.type_code.as_deref(),
3579 options.creator_code.as_deref(),
3580 );
3581
3582 let data_extents = self.write_data_to_blocks(data, data_len)?;
3584
3585 let (rsrc_extents, rsrc_size) = if let Some(ref rsrc_src) = options.resource_fork {
3587 match rsrc_src {
3588 disk_core::filesystem::ResourceForkSource::Data(rsrc_data) => {
3589 let mut cursor = std::io::Cursor::new(rsrc_data);
3590 let ext = self.write_data_to_blocks(&mut cursor, rsrc_data.len() as u64)?;
3591 (ext, rsrc_data.len() as u32)
3592 }
3593 disk_core::filesystem::ResourceForkSource::File(path) => {
3594 let mut f = std::fs::File::open(path)?;
3595 let len = f.metadata()?.len();
3596 let ext = self.write_data_to_blocks(&mut f, len)?;
3597 (ext, len as u32)
3598 }
3599 }
3600 } else {
3601 (Vec::new(), 0)
3602 };
3603
3604 let mtime = options.unix_times.map(|t| t.mtime_or_now());
3609 let mut file_rec = Self::build_file_record(
3610 file_id,
3611 data_len as u32,
3612 data_extents.first().map_or(0, |e| e.start_block),
3613 Self::extents_block_total(&data_extents),
3614 rsrc_size,
3615 rsrc_extents.first().map_or(0, |e| e.start_block),
3616 Self::extents_block_total(&rsrc_extents),
3617 &type_code,
3618 &creator_code,
3619 self.mdb.block_size,
3620 mtime,
3621 options.finder_flags.unwrap_or(0),
3622 );
3623 Self::set_inline_extents(&mut file_rec, 74, &data_extents);
3624 Self::set_inline_extents(&mut file_rec, 86, &rsrc_extents);
3625
3626 let key = Self::build_catalog_key(parent_id, &name_bytes);
3628 let mut key_record = key;
3629 key_record.extend_from_slice(&file_rec);
3630
3631 self.insert_catalog_record(&key_record)?;
3632
3633 self.update_parent_valence(parent_id, 1)?;
3643
3644 self.mdb.file_count += 1;
3646
3647 let mut overflow_changed = self.insert_fork_overflow(file_id, 0x00, &data_extents)?;
3648 overflow_changed |= self.insert_fork_overflow(file_id, 0xFF, &rsrc_extents)?;
3649 if overflow_changed {
3650 self.write_extents_overflow_back()?;
3651 }
3652
3653 let path = if parent.path == "/" {
3654 format!("/{name}")
3655 } else {
3656 format!("{}/{name}", parent.path)
3657 };
3658 let mut fe = FileEntry::new_file(name.to_string(), path, data_len, file_id as u64);
3659 if type_code != [0; 4] {
3660 fe.type_code = Some(type_code);
3661 fe.creator_code = Some(creator_code);
3662 }
3663 fe.finder_flags = options.finder_flags;
3664 if rsrc_size > 0 {
3665 fe.resource_fork_size = Some(rsrc_size as u64);
3666 }
3667 let stamped = mtime.unwrap_or_else(disk_core::times::now);
3668 fe.modified_unix = Some(stamped);
3669 fe.mac_dates = Some((
3670 disk_core::times::unix_to_mac_local(stamped),
3671 disk_core::times::unix_to_mac_local(stamped),
3672 0,
3673 ));
3674 Ok(fe)
3675 })();
3676 if result.is_err() {
3677 self.extents_overflow_data = extents_before;
3678 self.restore_snapshot(snap);
3679 }
3680 result
3681 }
3682
3683 fn create_directory(
3684 &mut self,
3685 parent: &FileEntry,
3686 name: &str,
3687 options: &CreateDirectoryOptions,
3688 ) -> Result<FileEntry, FilesystemError> {
3689 self.ensure_catalog_initialized()?;
3690 let snap = self.snapshot();
3691 let result = (|| {
3692 let parent_id = parent.location as u32;
3693
3694 let name_bytes = validate_hfs_create_name(name)?;
3695
3696 if self
3698 .find_catalog_record_by_name(parent_id, &name_bytes)
3699 .is_some()
3700 {
3701 return Err(FilesystemError::AlreadyExists(name.into()));
3702 }
3703
3704 let folder_id = self.mdb.next_catalog_id;
3706 self.mdb.next_catalog_id += 1;
3707
3708 let mtime = options.unix_times.map(|t| t.mtime_or_now());
3711 let folder_rec = Self::build_dir_record_with_dates(folder_id, mtime);
3712
3713 let key = Self::build_catalog_key(parent_id, &name_bytes);
3715 let mut key_record = key;
3716 key_record.extend_from_slice(&folder_rec);
3717
3718 self.insert_catalog_record(&key_record)?;
3719
3720 let (_, thread_data) =
3722 Self::build_thread_record(CATALOG_DIR_THREAD, parent_id, &name_bytes);
3723 let thread_key = Self::build_catalog_key(folder_id, &[]);
3724 let mut thread_record = thread_key;
3725 thread_record.extend_from_slice(&thread_data);
3726 self.insert_catalog_record(&thread_record)?;
3727
3728 self.update_parent_valence(parent_id, 1)?;
3730
3731 self.mdb.folder_count += 1;
3733
3734 let path = if parent.path == "/" {
3735 format!("/{name}")
3736 } else {
3737 format!("{}/{name}", parent.path)
3738 };
3739 let mut fe = FileEntry::new_directory(name.to_string(), path, folder_id as u64);
3740 let stamped = mtime.unwrap_or_else(disk_core::times::now);
3741 fe.modified_unix = Some(stamped);
3742 fe.mac_dates = Some((
3743 disk_core::times::unix_to_mac_local(stamped),
3744 disk_core::times::unix_to_mac_local(stamped),
3745 0,
3746 ));
3747 Ok(fe)
3748 })();
3749 if result.is_err() {
3750 self.restore_snapshot(snap);
3751 }
3752 result
3753 }
3754
3755 fn delete_entry(
3756 &mut self,
3757 parent: &FileEntry,
3758 entry: &FileEntry,
3759 ) -> Result<(), FilesystemError> {
3760 let snap = self.snapshot();
3761 let extents_before = self.extents_overflow_data.clone();
3762 let result = (|| {
3763 let parent_id = parent.location as u32;
3764 let cnid = entry.location as u32;
3765
3766 if entry.is_directory() {
3768 let children = self.list_children(cnid)?;
3769 if !children.is_empty() {
3770 return Err(FilesystemError::InvalidData(
3771 "cannot delete non-empty directory".into(),
3772 ));
3773 }
3774 }
3775
3776 let name_bytes = utf8_to_mac_roman(&entry.name)?;
3778 let (node_idx, rec_idx, _offset) = self
3779 .find_catalog_record_by_name(parent_id, &name_bytes)
3780 .ok_or_else(|| {
3781 FilesystemError::NotFound(format!(
3782 "entry '{}' not found in catalog",
3783 entry.name
3784 ))
3785 })?;
3786
3787 let mut overflow_changed = false;
3789 if !entry.is_directory() {
3790 if let Some((_, data_extents, _, rsrc_extents)) = self.find_file_by_id(cnid) {
3791 self.free_extent_blocks(&data_extents);
3792 self.free_extent_blocks(&rsrc_extents);
3793 overflow_changed |= self.free_fork_overflow(cnid, 0x00)?;
3795 overflow_changed |= self.free_fork_overflow(cnid, 0xFF)?;
3796 }
3797 }
3798
3799 self.remove_catalog_record(node_idx, rec_idx)?;
3800
3801 if let Some((t_node, t_rec, _)) = self.find_catalog_record_by_cnid(cnid) {
3803 self.remove_catalog_record(t_node, t_rec)?;
3804 }
3805
3806 self.update_parent_valence(parent_id, -1)?;
3808
3809 if entry.is_directory() {
3811 self.mdb.folder_count = self.mdb.folder_count.saturating_sub(1);
3812 } else {
3813 self.mdb.file_count = self.mdb.file_count.saturating_sub(1);
3814 }
3815
3816 if overflow_changed {
3817 self.write_extents_overflow_back()?;
3818 }
3819
3820 Ok(())
3821 })();
3822 if result.is_err() {
3823 self.extents_overflow_data = extents_before;
3824 self.restore_snapshot(snap);
3825 }
3826 result
3827 }
3828
3829 fn rename(
3830 &mut self,
3831 parent: &FileEntry,
3832 entry: &FileEntry,
3833 new_name: &str,
3834 ) -> Result<(), FilesystemError> {
3835 if new_name == entry.name {
3836 return Ok(());
3837 }
3838 let parent_id = parent.location as u32;
3839 let cnid = entry.location as u32;
3840 let new_name_raw = validate_hfs_create_name(new_name)?;
3841 let old_name_raw = utf8_to_mac_roman(&entry.name)?;
3842
3843 let snap = self.snapshot();
3844 let result = (|| -> Result<(), FilesystemError> {
3845 let (node_idx, rec_idx, _key_abs) = self
3847 .find_catalog_record_by_name(parent_id, &old_name_raw)
3848 .ok_or_else(|| {
3849 FilesystemError::NotFound(format!(
3850 "entry '{}' not found in catalog",
3851 entry.name
3852 ))
3853 })?;
3854
3855 if let Some((n2, r2, _)) = self.find_catalog_record_by_name(parent_id, &new_name_raw) {
3858 if (n2, r2) != (node_idx, rec_idx) {
3859 return Err(FilesystemError::AlreadyExists(new_name.to_string()));
3860 }
3861 }
3862
3863 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
3866 let node_off = node_idx as usize * node_size;
3867 let node = &self.catalog_data[node_off..node_off + node_size];
3868 let (rec_start, rec_end) = hfs_common::btree_record_range(node, node_size, rec_idx);
3869 let key_len = node[rec_start] as usize;
3870 let mut data_rel = 1 + key_len;
3871 if !data_rel.is_multiple_of(2) {
3872 data_rel += 1;
3873 }
3874 let data_bytes = node[rec_start + data_rel..rec_end].to_vec();
3875
3876 let mut new_record = Self::build_catalog_key(parent_id, &new_name_raw);
3877 if !new_record.len().is_multiple_of(2) {
3878 new_record.push(0);
3879 }
3880 new_record.extend_from_slice(&data_bytes);
3881 if !new_record.len().is_multiple_of(2) {
3882 new_record.push(0);
3883 }
3884
3885 self.remove_catalog_record(node_idx, rec_idx)?;
3886 self.insert_catalog_record(&new_record)?;
3887
3888 if let Some((_n, _r, t_key_abs)) = self.find_catalog_record_by_cnid(cnid) {
3892 let t_key_len = self.catalog_data[t_key_abs] as usize;
3893 let mut t_data_off = t_key_abs + 1 + t_key_len;
3894 if !t_data_off.is_multiple_of(2) {
3895 t_data_off += 1;
3896 }
3897 if t_data_off + 15 + 31 <= self.catalog_data.len() {
3898 self.catalog_data[t_data_off + 14] = new_name_raw.len() as u8;
3899 for i in 0..31 {
3900 self.catalog_data[t_data_off + 15 + i] = 0;
3901 }
3902 self.catalog_data[t_data_off + 15..t_data_off + 15 + new_name_raw.len()]
3903 .copy_from_slice(&new_name_raw);
3904 }
3905 }
3906 Ok(())
3908 })();
3909 if result.is_err() {
3910 self.restore_snapshot(snap);
3911 }
3912 result
3913 }
3914
3915 fn set_type_creator(
3916 &mut self,
3917 entry: &FileEntry,
3918 type_code: &str,
3919 creator_code: &str,
3920 ) -> Result<(), FilesystemError> {
3921 let snap = self.snapshot();
3922 let result = (|| {
3923 let frec_start = self.locate_record_data(entry.location as u32)?;
3924 if self.catalog_data[frec_start] as i8 != CATALOG_FILE {
3925 return Err(FilesystemError::InvalidData(
3926 "set_type_creator: not a file record".into(),
3927 ));
3928 }
3929
3930 let tc = hfs_common::encode_fourcc(type_code);
3932 let cc = hfs_common::encode_fourcc(creator_code);
3933 self.catalog_data[frec_start + 4..frec_start + 8].copy_from_slice(&tc);
3934 self.catalog_data[frec_start + 8..frec_start + 12].copy_from_slice(&cc);
3935
3936 Ok(())
3937 })();
3938 if result.is_err() {
3939 self.restore_snapshot(snap);
3940 }
3941 result
3942 }
3943
3944 fn write_resource_fork(
3945 &mut self,
3946 entry: &FileEntry,
3947 data: &mut dyn std::io::Read,
3948 len: u64,
3949 ) -> Result<(), FilesystemError> {
3950 let snap = self.snapshot();
3951 let extents_before = self.extents_overflow_data.clone();
3952 let result = (|| {
3953 let cnid = entry.location as u32;
3954
3955 let mut overflow_changed = false;
3957 if let Some((_, _, _, rsrc_extents)) = self.find_file_by_id(cnid) {
3958 self.free_extent_blocks(&rsrc_extents);
3959 overflow_changed = self.free_fork_overflow(cnid, 0xFF)?;
3960 }
3961
3962 let rsrc_extents = self.write_data_to_blocks(data, len)?;
3964
3965 let frec_start = self.locate_record_data(cnid)?;
3966 if self.catalog_data[frec_start] as i8 != CATALOG_FILE {
3967 return Err(FilesystemError::InvalidData(
3968 "write_resource_fork: not a file record".into(),
3969 ));
3970 }
3971
3972 self.catalog_data[frec_start + 34..frec_start + 36].fill(0);
3978 BigEndian::write_u32(
3980 &mut self.catalog_data[frec_start + 36..frec_start + 40],
3981 len as u32,
3982 );
3983 BigEndian::write_u32(
3985 &mut self.catalog_data[frec_start + 40..frec_start + 44],
3986 Self::extents_block_total(&rsrc_extents) as u32 * self.mdb.block_size,
3987 );
3988 Self::set_inline_extents(
3990 &mut self.catalog_data[frec_start..frec_start + 102],
3991 86,
3992 &rsrc_extents,
3993 );
3994 overflow_changed |= self.insert_fork_overflow(cnid, 0xFF, &rsrc_extents)?;
3995
3996 if overflow_changed {
3997 self.write_extents_overflow_back()?;
3998 }
3999
4000 Ok(())
4001 })();
4002 if result.is_err() {
4003 self.extents_overflow_data = extents_before;
4004 self.restore_snapshot(snap);
4005 }
4006 result
4007 }
4008
4009 fn repair(&mut self) -> Result<disk_core::fsck::RepairReport, FilesystemError> {
4010 self.ensure_bitmap()?;
4011 let mut extents_data = if self.mdb.extents_file_size > 0 {
4012 read_fork_data(
4013 &mut self.reader,
4014 self.partition_offset,
4015 &self.mdb,
4016 &self.mdb.extents_file_extents,
4017 self.mdb.extents_file_size as u64,
4018 )
4019 .ok()
4020 } else {
4021 None
4022 };
4023
4024 let report = crate::hfs_fsck::repair_hfs(
4025 &mut self.mdb,
4026 &mut self.catalog_data,
4027 self.bitmap.as_mut().unwrap(),
4028 extents_data.as_mut(),
4029 );
4030
4031 if let Some(ref ext_data) = extents_data {
4033 if self.mdb.extents_file_size > 0 {
4034 write_extents_overflow_data(
4035 &mut self.reader,
4036 self.partition_offset,
4037 &self.mdb,
4038 ext_data,
4039 )?;
4040 }
4041 }
4042
4043 self.do_sync_metadata()?;
4044 Ok(report)
4045 }
4046
4047 fn sync_metadata(&mut self) -> Result<(), FilesystemError> {
4048 self.do_sync_metadata()
4049 }
4050
4051 fn free_space(&mut self) -> Result<u64, FilesystemError> {
4052 Ok(self.mdb.free_blocks as u64 * self.mdb.block_size as u64)
4053 }
4054
4055 fn set_blessed_folder(&mut self, entry: &FileEntry) -> Result<(), FilesystemError> {
4056 let snap = self.snapshot();
4057 let result = (|| {
4058 if !entry.is_directory() {
4059 return Err(FilesystemError::InvalidData(
4060 "can only bless a directory".into(),
4061 ));
4062 }
4063 self.mdb.finder_info[0] = entry.location as u32;
4064 Ok(())
4065 })();
4066 if result.is_err() {
4067 self.restore_snapshot(snap);
4068 }
4069 result
4070 }
4071
4072 fn write_boot_blocks(&mut self, blocks: &[u8; 1024]) -> Result<(), FilesystemError> {
4073 HfsFilesystem::set_boot_blocks(self, blocks);
4074 Ok(())
4075 }
4076
4077 fn set_volume_name(&mut self, new_name: &str) -> Result<(), FilesystemError> {
4078 HfsFilesystem::set_volume_name(self, new_name)
4079 }
4080
4081 fn set_finder_info(
4082 &mut self,
4083 entry: &FileEntry,
4084 finfo: [u8; 16],
4085 fxinfo: [u8; 16],
4086 ) -> Result<(), FilesystemError> {
4087 HfsFilesystem::set_finder_info(self, entry, finfo, fxinfo)
4088 }
4089
4090 fn set_dates(
4091 &mut self,
4092 entry: &FileEntry,
4093 create: u32,
4094 modify: u32,
4095 backup: u32,
4096 ) -> Result<(), FilesystemError> {
4097 HfsFilesystem::set_dates(self, entry, create, modify, backup)
4098 }
4099}
4100
4101fn collect_fork_overflow_extents(
4107 extents_data: &[u8],
4108 file_id: u32,
4109 fork_type: u8,
4110 min_start_block: u32,
4111) -> Vec<HfsExtDescriptor> {
4112 use crate::hfs_common::{btree_record_range, BTreeHeader, BTREE_LEAF_NODE};
4113 use std::collections::HashSet;
4114
4115 let mut out: Vec<(u16, HfsExtDescriptor)> = Vec::new();
4116 if extents_data.len() < 512 {
4117 return Vec::new();
4118 }
4119 let header = BTreeHeader::read(extents_data);
4120 let node_size = header.node_size as usize;
4121 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE || extents_data.len() < node_size {
4122 return Vec::new();
4123 }
4124
4125 let mut node_idx = header.first_leaf_node;
4126 let mut visited: HashSet<u32> = HashSet::new();
4127 while node_idx != 0 {
4128 if !visited.insert(node_idx) {
4129 break;
4130 }
4131 let off = node_idx as usize * node_size;
4132 if off + node_size > extents_data.len() {
4133 break;
4134 }
4135 let node = &extents_data[off..off + node_size];
4136 let flink = BigEndian::read_u32(&node[0..4]);
4137 let kind = node[8] as i8;
4138 if kind != BTREE_LEAF_NODE {
4139 break;
4140 }
4141 let num_records = BigEndian::read_u16(&node[10..12]) as usize;
4142 for i in 0..num_records {
4143 let (rec_start, _) = btree_record_range(node, node_size, i);
4144 if rec_start + 8 > node_size {
4145 continue;
4146 }
4147 let key_len = node[rec_start] as usize;
4148 if key_len < 7 || rec_start + 1 + key_len > node_size {
4149 continue;
4150 }
4151 let rec_fork_type = node[rec_start + 1];
4152 let rec_file_id = BigEndian::read_u32(&node[rec_start + 2..rec_start + 6]);
4153 let rec_start_block = BigEndian::read_u16(&node[rec_start + 6..rec_start + 8]);
4154 if rec_fork_type != fork_type || rec_file_id != file_id {
4155 continue;
4156 }
4157 if (rec_start_block as u32) < min_start_block {
4158 continue;
4159 }
4160 let mut data_off = rec_start + 1 + key_len;
4161 if !data_off.is_multiple_of(2) {
4162 data_off += 1;
4163 }
4164 if data_off + 12 > node_size {
4165 continue;
4166 }
4167 for j in 0..3 {
4168 let ext = HfsExtDescriptor::parse(&node[data_off + j * 4..data_off + j * 4 + 4]);
4169 if ext.block_count > 0 {
4170 out.push((rec_start_block, ext));
4171 }
4172 }
4173 }
4174 node_idx = flink;
4175 }
4176
4177 out.sort_by_key(|(sb, _)| *sb);
4178 out.into_iter().map(|(_, e)| e).collect()
4179}
4180
4181fn read_fork_data<R: Read + Seek>(
4182 reader: &mut R,
4183 partition_offset: u64,
4184 mdb: &HfsMasterDirectoryBlock,
4185 extents: &[HfsExtDescriptor; 3],
4186 size: u64,
4187) -> Result<Vec<u8>, FilesystemError> {
4188 let mut data = Vec::with_capacity(size as usize);
4189 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4190
4191 for ext in extents {
4192 if ext.block_count == 0 {
4193 break;
4194 }
4195 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4196 let len = ext.block_count as u64 * mdb.block_size as u64;
4197 reader.seek(SeekFrom::Start(offset))?;
4198 let read_len = len.min(size - data.len() as u64) as usize;
4199 let mut buf = vec![0u8; read_len];
4200 reader.read_exact(&mut buf)?;
4201 data.extend_from_slice(&buf);
4202 if data.len() as u64 >= size {
4203 break;
4204 }
4205 }
4206
4207 data.truncate(size as usize);
4208 Ok(data)
4209}
4210
4211fn remove_fork_overflow_records(
4214 extents_data: &mut [u8],
4215 file_id: u32,
4216 fork_type: u8,
4217) -> Result<u32, FilesystemError> {
4218 use crate::hfs_common::{
4219 btree_remove_record, btree_retire_empty_leaf, compare_classic_extents_keys,
4220 walk_leaf_records, BTreeHeader,
4221 };
4222 let kf = BTreeKeyFormat::CLASSIC_EXTENTS;
4223 if extents_data.len() < 512 {
4224 return Ok(0);
4225 }
4226 let header = BTreeHeader::read(extents_data);
4227 let node_size = header.node_size as usize;
4228 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE || extents_data.len() < node_size {
4229 return Ok(0);
4230 }
4231 let mut victims: Vec<(u32, usize)> = Vec::new();
4232 walk_leaf_records::<(), _>(
4233 extents_data,
4234 header.first_leaf_node,
4235 node_size,
4236 |node_idx, rec_idx, _off, rec| {
4237 if rec.len() >= 8
4239 && rec[0] >= 7
4240 && rec[1] == fork_type
4241 && BigEndian::read_u32(&rec[2..6]) == file_id
4242 {
4243 victims.push((node_idx, rec_idx));
4244 }
4245 None
4246 },
4247 );
4248 if victims.is_empty() {
4249 return Ok(0);
4250 }
4251 victims.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
4253 for &(node_idx, rec_idx) in &victims {
4254 let off = node_idx as usize * node_size;
4255 btree_remove_record(&mut extents_data[off..off + node_size], node_size, rec_idx);
4256 }
4257 let mut h = BTreeHeader::read(extents_data);
4258 h.leaf_records = h.leaf_records.saturating_sub(victims.len() as u32);
4259 h.write(extents_data);
4260 let mut nodes: Vec<u32> = victims.iter().map(|v| v.0).collect();
4261 nodes.dedup();
4262 for node_idx in nodes {
4263 let off = node_idx as usize * node_size;
4264 if BigEndian::read_u16(&extents_data[off + 10..off + 12]) == 0 {
4265 btree_retire_empty_leaf(
4267 extents_data,
4268 node_size,
4269 node_idx,
4270 &kf,
4271 &compare_classic_extents_keys,
4272 )?;
4273 } else if victims.iter().any(|&(n, r)| n == node_idx && r == 0) {
4274 btree_refresh_index_keys(
4276 extents_data,
4277 node_size,
4278 node_idx,
4279 &kf,
4280 &compare_classic_extents_keys,
4281 )?;
4282 }
4283 }
4284 Ok(victims.len() as u32)
4285}
4286
4287fn write_extents_overflow_data<W: std::io::Write + Seek>(
4289 writer: &mut W,
4290 partition_offset: u64,
4291 mdb: &HfsMasterDirectoryBlock,
4292 data: &[u8],
4293) -> Result<(), FilesystemError> {
4294 let alloc_start = partition_offset + mdb.first_alloc_block as u64 * 512;
4295 let mut data_start = 0usize;
4296 for extent in mdb.extents_file_extents.iter() {
4297 if extent.block_count == 0 {
4298 break;
4299 }
4300 let block_off = alloc_start + extent.start_block as u64 * mdb.block_size as u64;
4301 let byte_len = extent.block_count as usize * mdb.block_size as usize;
4302 let data_end = (data_start + byte_len).min(data.len());
4303 if data_start < data_end {
4304 writer.seek(std::io::SeekFrom::Start(block_off))?;
4305 writer.write_all(&data[data_start..data_end])?;
4306 }
4307 data_start += byte_len;
4308 }
4309 Ok(())
4310}
4311
4312fn write_fork_data_to<R: Read + Seek>(
4314 reader: &mut R,
4315 partition_offset: u64,
4316 mdb: &HfsMasterDirectoryBlock,
4317 extents: &[HfsExtDescriptor; 3],
4318 writer: &mut dyn std::io::Write,
4319 size: u64,
4320) -> Result<u64, FilesystemError> {
4321 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4322 let mut written: u64 = 0;
4323 let mut buf = vec![0u8; 64 * 1024];
4324
4325 for ext in extents {
4326 if ext.block_count == 0 || written >= size {
4327 break;
4328 }
4329 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4330 let extent_len = ext.block_count as u64 * mdb.block_size as u64;
4331 let to_emit = extent_len.min(size - written);
4332 reader.seek(SeekFrom::Start(offset))?;
4333 let mut left = to_emit;
4334 while left > 0 {
4335 let n = (buf.len() as u64).min(left) as usize;
4336 reader.read_exact(&mut buf[..n])?;
4337 writer.write_all(&buf[..n])?;
4338 left -= n as u64;
4339 }
4340 written += to_emit;
4341 }
4342 Ok(written)
4343}
4344
4345fn write_hfs_fork_data<R: Write + Seek>(
4347 writer: &mut R,
4348 partition_offset: u64,
4349 mdb: &HfsMasterDirectoryBlock,
4350 extents: &[HfsExtDescriptor; 3],
4351 data: &[u8],
4352) -> Result<(), FilesystemError> {
4353 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4354 let mut written = 0usize;
4355 for ext in extents {
4356 if ext.block_count == 0 || written >= data.len() {
4357 break;
4358 }
4359 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4360 let extent_len = ext.block_count as u64 * mdb.block_size as u64;
4361 let to_write = extent_len.min((data.len() - written) as u64) as usize;
4362 writer.seek(SeekFrom::Start(offset))?;
4363 writer.write_all(&data[written..written + to_write])?;
4364 written += to_write;
4365 }
4366 Ok(())
4367}
4368
4369fn find_last_set_bit(bitmap: &[u8], max_bits: u32) -> Option<u32> {
4371 let mut last = None;
4372 for bit in 0..max_bits {
4373 let byte_idx = bit as usize / 8;
4374 let bit_idx = 7 - (bit % 8); if byte_idx < bitmap.len() && (bitmap[byte_idx] >> bit_idx) & 1 == 1 {
4376 last = Some(bit);
4377 }
4378 }
4379 last
4380}
4381
4382pub struct CompactHfsReader<R: Read + Seek> {
4390 reader: R,
4391 partition_offset: u64,
4392 mdb: HfsMasterDirectoryBlock,
4393 bitmap: Vec<u8>,
4394 phase: u8,
4396 pre_alloc_pos: u64,
4398 current_block: u32,
4400 block_pos: u64,
4402 #[allow(dead_code)]
4403 original_size: u64,
4404 #[allow(dead_code)]
4405 allocated_blocks: u32,
4406}
4407
4408impl<R: Read + Seek> CompactHfsReader<R> {
4409 pub fn new(
4410 mut reader: R,
4411 partition_offset: u64,
4412 ) -> Result<(Self, CompactResult), FilesystemError> {
4413 log::debug!(
4415 "[HFS compact] seeking to MDB at offset {}",
4416 partition_offset + 1024
4417 );
4418 reader.seek(SeekFrom::Start(partition_offset + 1024))?;
4419 let mut mdb_buf = [0u8; 162];
4420 reader.read_exact(&mut mdb_buf)?;
4421 let mdb = HfsMasterDirectoryBlock::parse(&mdb_buf).map_err(|e| {
4422 log::debug!("[HFS compact] MDB parse failed: {e}");
4423 e
4424 })?;
4425 log::debug!(
4426 "[HFS compact] MDB ok: block_size={}, total_blocks={}, first_alloc_block={}, volume_bitmap_block={}",
4427 mdb.block_size, mdb.total_blocks, mdb.first_alloc_block, mdb.volume_bitmap_block,
4428 );
4429
4430 if mdb.has_embedded_hfs_plus() {
4431 log::debug!("[HFS compact] has embedded HFS+ — refusing to compact HFS wrapper");
4432 return Err(FilesystemError::Unsupported(
4433 "cannot compact HFS wrapper with embedded HFS+".into(),
4434 ));
4435 }
4436
4437 let bitmap_offset = partition_offset + mdb.volume_bitmap_block as u64 * 512;
4439 log::debug!("[HFS compact] reading bitmap at offset {bitmap_offset}");
4440 let bitmap_size = (mdb.total_blocks as u32).div_ceil(8) as usize;
4441 reader.seek(SeekFrom::Start(bitmap_offset))?;
4442 let mut bitmap = vec![0u8; bitmap_size];
4443 reader.read_exact(&mut bitmap)?;
4444 log::debug!("[HFS compact] bitmap read: {bitmap_size} bytes");
4445
4446 let mut allocated = 0u32;
4448 for bit in 0..mdb.total_blocks as u32 {
4449 let byte_idx = bit as usize / 8;
4450 let bit_idx = 7 - (bit % 8);
4451 if byte_idx < bitmap.len() && (bitmap[byte_idx] >> bit_idx) & 1 == 1 {
4452 allocated += 1;
4453 }
4454 }
4455 log::debug!(
4456 "[HFS compact] allocated={} / {} total blocks ({} free)",
4457 allocated,
4458 mdb.total_blocks,
4459 mdb.total_blocks as u32 - allocated,
4460 );
4461
4462 let pre_alloc_size = mdb.first_alloc_block as u64 * 512;
4464 let original_size = pre_alloc_size + mdb.total_blocks as u64 * mdb.block_size as u64;
4465 let compacted_size = original_size;
4468 let data_size = pre_alloc_size + allocated as u64 * mdb.block_size as u64;
4470 log::debug!(
4471 "[HFS compact] pre_alloc_size={}, data_size={}, compacted_size={} original_size={} (layout-preserving; free blocks -> zeros)",
4472 pre_alloc_size, data_size, compacted_size, original_size
4473 );
4474
4475 let result = CompactResult {
4476 original_size,
4477 compacted_size,
4478 data_size,
4479 clusters_used: allocated,
4480 };
4481
4482 Ok((
4483 CompactHfsReader {
4484 reader,
4485 partition_offset,
4486 mdb,
4487 bitmap,
4488 phase: 0,
4489 pre_alloc_pos: 0,
4490 current_block: 0,
4491 block_pos: 0,
4492 original_size,
4493 allocated_blocks: allocated,
4494 },
4495 result,
4496 ))
4497 }
4498
4499 fn is_block_allocated(&self, block: u32) -> bool {
4500 let byte_idx = block as usize / 8;
4501 let bit_idx = 7 - (block % 8);
4502 byte_idx < self.bitmap.len() && (self.bitmap[byte_idx] >> bit_idx) & 1 == 1
4503 }
4504}
4505
4506impl<R: Read + Seek> Read for CompactHfsReader<R> {
4507 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
4508 if self.phase > 1 {
4509 return Ok(0);
4510 }
4511
4512 if self.phase == 0 {
4513 let pre_alloc_size = self.mdb.first_alloc_block as u64 * 512;
4516 let remaining = pre_alloc_size - self.pre_alloc_pos;
4517 if remaining == 0 {
4518 self.phase = 1;
4519 return self.read(buf);
4520 }
4521 let to_read = (remaining as usize).min(buf.len());
4522 let offset = self.partition_offset + self.pre_alloc_pos;
4523 self.reader
4524 .seek(SeekFrom::Start(offset))
4525 .map_err(disk_core::io_other)?;
4526 let n = self.reader.read(&mut buf[..to_read])?;
4527 self.pre_alloc_pos += n as u64;
4528 return Ok(n);
4529 }
4530
4531 let total_blocks = self.mdb.total_blocks as u32;
4534 if self.current_block >= total_blocks {
4535 self.phase = 2;
4536 return Ok(0);
4537 }
4538
4539 let block_size = self.mdb.block_size as u64;
4540
4541 let allocated = self.is_block_allocated(self.current_block);
4546 let mut run_end = self.current_block + 1;
4547 while run_end < total_blocks && self.is_block_allocated(run_end) == allocated {
4548 run_end += 1;
4549 }
4550 let bytes_in_run = (run_end - self.current_block) as u64 * block_size - self.block_pos;
4551 let to_read = (bytes_in_run as usize).min(buf.len());
4552
4553 let n = if allocated {
4554 let offset = self.partition_offset
4555 + self.mdb.first_alloc_block as u64 * 512
4556 + self.current_block as u64 * block_size
4557 + self.block_pos;
4558 self.reader
4559 .seek(SeekFrom::Start(offset))
4560 .map_err(disk_core::io_other)?;
4561 self.reader.read(&mut buf[..to_read])?
4562 } else {
4563 buf[..to_read].fill(0);
4565 to_read
4566 };
4567
4568 let mut remaining_in_n = n as u64;
4570 if self.block_pos > 0 {
4571 let want = block_size - self.block_pos;
4572 let take = remaining_in_n.min(want);
4573 self.block_pos += take;
4574 remaining_in_n -= take;
4575 if self.block_pos >= block_size {
4576 self.block_pos = 0;
4577 self.current_block += 1;
4578 }
4579 }
4580 let whole = (remaining_in_n / block_size) as u32;
4581 self.current_block += whole;
4582 remaining_in_n -= whole as u64 * block_size;
4583 if remaining_in_n > 0 {
4584 self.block_pos = remaining_in_n;
4585 }
4586
4587 Ok(n)
4588 }
4589}
4590
4591pub fn fit_hfs_volume_to_partition(
4606 device: &mut (impl Read + Write + Seek),
4607 partition_offset: u64,
4608 partition_len: u64,
4609 log: &mut impl FnMut(&str),
4610) -> anyhow::Result<bool> {
4611 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4612 let mut sector = [0u8; 512];
4613 device.read_exact(&mut sector)?;
4614 if BigEndian::read_u16(§or[0..2]) != HFS_SIGNATURE {
4615 return Ok(false);
4616 }
4617 if BigEndian::read_u16(§or[0x7C..0x7E]) == 0x482B {
4619 return Ok(false);
4620 }
4621 let total_blocks = BigEndian::read_u16(§or[0x12..0x14]) as u64;
4622 let block_size = BigEndian::read_u32(§or[0x14..0x18]) as u64;
4623 let first_alloc = BigEndian::read_u16(§or[0x1C..0x1E]) as u64;
4624 let volume_len = first_alloc * 512 + total_blocks * block_size + 1024;
4625 if block_size == 0 || volume_len >= partition_len {
4626 return Ok(false);
4627 }
4628 let growable = hfs_max_growable_size(device, partition_offset).unwrap_or(0);
4629 if growable < partition_len {
4630 log(&format!(
4631 "Warning: the HFS volume ({volume_len} bytes) does not fill its {partition_len}-byte \
4632 partition and its volume bitmap cannot address more blocks; Disk First Aid will \
4633 report an invalid allocation block start. Format the volume at the partition size"
4634 ));
4635 return Ok(false);
4636 }
4637 resize_hfs_in_place(device, partition_offset, partition_len, log)?;
4638 log(&format!(
4639 "HFS volume grown from {volume_len} bytes to fill its {partition_len}-byte partition"
4640 ));
4641 Ok(true)
4642}
4643
4644pub fn hfs_max_growable_size(
4645 device: &mut (impl Read + Seek),
4646 partition_offset: u64,
4647) -> Option<u64> {
4648 device.seek(SeekFrom::Start(partition_offset + 1024)).ok()?;
4649 let mut sector = [0u8; 512];
4650 device.read_exact(&mut sector).ok()?;
4651
4652 let sig = BigEndian::read_u16(§or[0..2]);
4653 if sig != HFS_SIGNATURE {
4654 return None;
4655 }
4656
4657 let block_size = BigEndian::read_u32(§or[20..24]) as u64;
4658 let vbm_start = BigEndian::read_u16(§or[14..16]) as u64;
4659 let first_alloc = BigEndian::read_u16(§or[28..30]) as u64;
4660
4661 if block_size == 0 || first_alloc <= vbm_start {
4662 return None;
4663 }
4664
4665 let vbm_capacity = (first_alloc - vbm_start) * 512 * 8;
4666 let max_blocks = vbm_capacity.min(u16::MAX as u64);
4667 let overhead = first_alloc * 512 + 1024;
4669
4670 Some(max_blocks * block_size + overhead)
4671}
4672
4673pub fn resize_hfs_in_place(
4674 device: &mut (impl Read + Write + Seek),
4675 partition_offset: u64,
4676 new_size_bytes: u64,
4677 log: &mut impl FnMut(&str),
4678) -> anyhow::Result<()> {
4679 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4683 let mut sector = [0u8; 512];
4684 device.read_exact(&mut sector)?;
4685
4686 let sig = BigEndian::read_u16(§or[0..2]);
4687 if sig != HFS_SIGNATURE {
4688 log("HFS resize: not an HFS volume, skipping");
4689 return Ok(());
4690 }
4691
4692 let block_size = BigEndian::read_u32(§or[20..24]);
4693 let old_total = BigEndian::read_u16(§or[18..20]);
4694 let free_blocks = BigEndian::read_u16(§or[34..36]);
4695 let vbm_start = BigEndian::read_u16(§or[14..16]);
4696 let first_alloc = BigEndian::read_u16(§or[28..30]);
4697 let used_blocks = old_total - free_blocks;
4698
4699 let vbm_capacity_blocks = if first_alloc > vbm_start {
4703 (first_alloc - vbm_start) as u64 * 512 * 8
4704 } else {
4705 0
4706 };
4707
4708 let overhead = first_alloc as u64 * 512 + 1024;
4711 if new_size_bytes <= overhead {
4712 anyhow::bail!(
4713 "HFS resize: new size {} bytes <= overhead {} bytes",
4714 new_size_bytes,
4715 overhead
4716 );
4717 }
4718 let new_total_u64 = (new_size_bytes - overhead) / block_size as u64;
4719
4720 if new_total_u64 > u16::MAX as u64 {
4723 let max_bytes = u16::MAX as u64 * block_size as u64 + overhead;
4724 anyhow::bail!(
4725 "HFS resize: requested size {} MB needs {} allocation blocks at {}-byte blocks, \
4726 but HFS caps at 65535 blocks (max {} MB for this volume). \
4727 Growing further would require reformatting with a larger allocation block size.",
4728 new_size_bytes / 1024 / 1024,
4729 new_total_u64,
4730 block_size,
4731 max_bytes / 1024 / 1024,
4732 );
4733 }
4734 if new_total_u64 > vbm_capacity_blocks {
4735 anyhow::bail!(
4736 "HFS resize: requested {} allocation blocks exceeds the Volume Bitmap capacity \
4737 ({} blocks) for this volume. The VBM lives in fixed sectors [{}..{}) and cannot \
4738 be grown without reformatting: the most this volume can grow to is {} bytes. \
4739 Mac OS wants a classic HFS volume to fill its partition, so format one at the \
4740 partition size instead (`new volume hfs --size`, then `new hd --fill`).",
4741 new_total_u64,
4742 vbm_capacity_blocks,
4743 vbm_start,
4744 first_alloc,
4745 vbm_capacity_blocks.min(u16::MAX as u64) * block_size as u64 + overhead,
4746 );
4747 }
4748 let new_total = new_total_u64 as u16;
4749
4750 let vbm_len = (first_alloc - vbm_start) as usize * 512;
4753 let mut vbm = vec![0u8; vbm_len];
4754 device.seek(SeekFrom::Start(partition_offset + vbm_start as u64 * 512))?;
4755 device.read_exact(&mut vbm)?;
4756 let highest_used = find_last_set_bit(&vbm, old_total as u32).map_or(0, |b| b + 1);
4757 if (new_total as u32) < highest_used {
4758 anyhow::bail!(
4759 "HFS resize: new size {} blocks ends before the last allocated block ({}); \
4760 {} of {} blocks are in use",
4761 new_total,
4762 highest_used,
4763 used_blocks,
4764 old_total
4765 );
4766 }
4767 for b in old_total.min(new_total) as u32..old_total.max(new_total) as u32 {
4770 bitmap_clear_bit_be(&mut vbm, b);
4771 }
4772 device.seek(SeekFrom::Start(partition_offset + vbm_start as u64 * 512))?;
4773 device.write_all(&vbm)?;
4774
4775 let new_free = new_total - used_blocks;
4776
4777 log(&format!(
4778 "HFS resize: {} -> {} blocks ({} free)",
4779 old_total, new_total, new_free
4780 ));
4781
4782 BigEndian::write_u16(&mut sector[18..20], new_total);
4784 BigEndian::write_u16(&mut sector[34..36], new_free);
4785
4786 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4788 device.write_all(§or)?;
4789
4790 if new_size_bytes > 1024 {
4792 device.seek(SeekFrom::Start(partition_offset + new_size_bytes - 1024))?;
4793 device.write_all(§or)?;
4794 }
4795
4796 device.flush()?;
4797 Ok(())
4798}
4799
4800pub fn validate_hfs_integrity(
4802 device: &mut (impl Read + Seek),
4803 partition_offset: u64,
4804 log: &mut impl FnMut(&str),
4805) -> anyhow::Result<()> {
4806 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4808 let mut sector = [0u8; 512];
4809 device.read_exact(&mut sector)?;
4810
4811 let sig = BigEndian::read_u16(§or[0..2]);
4812 if sig != HFS_SIGNATURE {
4813 log("HFS validate: not an HFS volume, skipping");
4814 return Ok(());
4815 }
4816
4817 let block_size = BigEndian::read_u32(§or[20..24]);
4818 let total_blocks = BigEndian::read_u16(§or[18..20]);
4819
4820 if !block_size.is_power_of_two() || block_size < 512 {
4822 anyhow::bail!("HFS validate: invalid block size {block_size}");
4823 }
4824 if total_blocks == 0 {
4825 anyhow::bail!("HFS validate: zero total blocks");
4826 }
4827
4828 log(&format!(
4829 "HFS validate: OK ({total_blocks} blocks, {} block size)",
4830 block_size
4831 ));
4832 Ok(())
4833}
4834
4835#[cfg(test)]
4836#[allow(clippy::identity_op)] mod tests {
4838 use super::*;
4839 use std::io::Cursor;
4840
4841 #[test]
4845 fn write_blank_hfs_into_matches_create_blank_hfs() {
4846 let (size, bs) = (8 * 1024 * 1024u64, 4096u32);
4847 let now = 0xB0A1_2345;
4848 let mem = create_blank_hfs_sized_at(size, bs, "StreamEq", 0, 0, now).unwrap();
4849
4850 let mut sink = Cursor::new(vec![0u8; 0]);
4851 let written = write_blank_hfs_into_at(&mut sink, 0, size, bs, "StreamEq", now).unwrap();
4852 let mut streamed = sink.into_inner();
4853 streamed.resize(written as usize, 0);
4854
4855 assert_eq!(written as usize, mem.len());
4856 assert_eq!(streamed, mem);
4857 }
4858
4859 #[test]
4862 fn write_blank_hfs_into_honours_the_offset() {
4863 let (size, bs) = (4 * 1024 * 1024u64, 2048u32);
4864 let now = 0xB0A1_2345;
4865 let mem = create_blank_hfs_sized_at(size, bs, "Offset", 0, 0, now).unwrap();
4866
4867 let offset = 64 * 512u64;
4868 let mut sink = Cursor::new(vec![0u8; 0]);
4869 let written = write_blank_hfs_into_at(&mut sink, offset, size, bs, "Offset", now).unwrap();
4870 let mut streamed = sink.into_inner();
4871 streamed.resize((offset + written) as usize, 0);
4872
4873 assert!(streamed[..offset as usize].iter().all(|&b| b == 0));
4874 assert_eq!(&streamed[offset as usize..], &mem[..]);
4875 }
4876
4877 #[test]
4878 fn test_mac_roman_to_utf8_ascii() {
4879 let data = b"Hello World";
4880 assert_eq!(mac_roman_to_utf8(data), "Hello World");
4881 }
4882
4883 #[test]
4884 fn test_mac_roman_to_utf8_special() {
4885 let data = [0x80, 0x81, 0x87];
4887 assert_eq!(mac_roman_to_utf8(&data), "ÄÅá");
4888 }
4889
4890 #[test]
4891 fn test_decode_mac_filename_utf8() {
4892 let mut data = b"HeapFixer".to_vec();
4895 data.extend_from_slice(&[0xE2, 0x84, 0xA2]);
4896 data.extend_from_slice(b" 1.01");
4897 assert_eq!(decode_mac_filename(&data), "HeapFixer™ 1.01");
4898 }
4899
4900 #[test]
4901 fn test_decode_mac_filename_macroman() {
4902 assert_eq!(decode_mac_filename(&[0xC4]), "ƒ");
4905 assert_eq!(decode_mac_filename(b"Cache\xA5Flow"), "Cache•Flow");
4906 assert_eq!(decode_mac_filename(b"Virex\xA8 3.51"), "Virex® 3.51");
4907 }
4908
4909 #[test]
4910 fn test_decode_mac_filename_ascii() {
4911 assert_eq!(decode_mac_filename(b"ScriptEdit"), "ScriptEdit");
4912 }
4913
4914 #[test]
4915 fn test_format_ostype() {
4916 assert_eq!(format_ostype(&[b'S', 0xA5, b'L', b' ']), "S•L");
4918 assert_eq!(format_ostype(b"APPL"), "APPL");
4919 assert_eq!(format_ostype(&[b'L', b's', 0x00, 0x00]), "Ls");
4921 }
4922
4923 #[test]
4924 fn test_mdb_parse() {
4925 let mut data = [0u8; 162];
4926 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
4928 BigEndian::write_u16(&mut data[18..20], 1000);
4930 BigEndian::write_u32(&mut data[20..24], 4096);
4932 BigEndian::write_u16(&mut data[34..36], 200);
4934 data[36] = 7;
4936 data[37..44].copy_from_slice(b"TestVol");
4937
4938 let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
4939 assert_eq!(mdb.signature, HFS_SIGNATURE);
4940 assert_eq!(mdb.total_blocks, 1000);
4941 assert_eq!(mdb.block_size, 4096);
4942 assert_eq!(mdb.free_blocks, 200);
4943 assert_eq!(mdb.volume_name, "TestVol");
4944 assert!(!mdb.has_embedded_hfs_plus());
4945 }
4946
4947 #[test]
4948 fn test_mdb_detect_embedded_hfs_plus() {
4949 let mut data = [0u8; 162];
4950 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
4951 BigEndian::write_u16(&mut data[18..20], 1000);
4952 BigEndian::write_u32(&mut data[20..24], 4096);
4953 BigEndian::write_u16(&mut data[124..126], HFS_PLUS_EMBEDDED_SIGNATURE);
4955 BigEndian::write_u16(&mut data[126..128], 10); BigEndian::write_u16(&mut data[128..130], 500); let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
4959 assert!(mdb.has_embedded_hfs_plus());
4960 }
4961
4962 #[test]
4963 fn test_find_last_set_bit() {
4964 let bitmap = [0b10100000, 0b00001000]; assert_eq!(find_last_set_bit(&bitmap, 16), Some(12));
4966
4967 let bitmap_empty = [0u8; 2];
4968 assert_eq!(find_last_set_bit(&bitmap_empty, 16), None);
4969
4970 let bitmap_first = [0b10000000]; assert_eq!(find_last_set_bit(&bitmap_first, 8), Some(0));
4972 }
4973
4974 #[test]
4975 fn test_utf8_to_mac_roman_roundtrip() {
4976 let text = "Hello World";
4977 let mac = utf8_to_mac_roman(text).unwrap();
4978 assert_eq!(mac, b"Hello World");
4979 assert_eq!(mac_roman_to_utf8(&mac), text);
4980 }
4981
4982 #[test]
4983 fn test_utf8_to_mac_roman_special() {
4984 let text = "Ä"; let mac = utf8_to_mac_roman(text).unwrap();
4986 assert_eq!(mac, &[0x80]);
4987 assert_eq!(mac_roman_to_utf8(&mac), text);
4988 }
4989
4990 #[test]
4991 fn test_utf8_to_mac_roman_unencodable() {
4992 let text = "\u{4E2D}"; assert!(utf8_to_mac_roman(text).is_err());
4994 }
4995
4996 #[test]
4998 fn tiny_catalog_node_size_does_not_panic() {
4999 use disk_core::filesystem::Filesystem;
5000 let clean = create_blank_hfs(1474560, 512, "Tiny").unwrap();
5001 let mdb = {
5002 let fs = HfsFilesystem::open(Cursor::new(clean.clone()), 0).unwrap();
5003 fs.mdb.clone()
5004 };
5005 let cat_off = mdb.first_alloc_block as usize * 512
5006 + mdb.catalog_file_extents[0].start_block as usize * mdb.block_size as usize;
5007 for bad in [1u16, 3, 9, 14, 17, 27, 28, 511] {
5008 let mut img = clean.clone();
5009 img[cat_off + 32..cat_off + 34].copy_from_slice(&bad.to_be_bytes());
5010 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5011 let root = fs.root().unwrap();
5012 let listed = fs.list_directory(&root);
5013 assert!(
5014 listed.map(|v| v.is_empty()).unwrap_or(true),
5015 "node size {bad}: expected an empty listing or an error"
5016 );
5017 }
5018 }
5019
5020 #[test]
5025 fn classic_hfs_stamps_are_local_time_and_round_trip_to_utc() {
5026 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5027 use disk_core::times::{local_utc_offset_secs, unix_to_mac_epoch, UnixTimes};
5028 const T: u64 = 1_700_000_000; let mut img = create_blank_hfs(4 * 1024 * 1024, 4096, "Zone").unwrap();
5030 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5031 let root = fs.root().unwrap();
5032 let opts = CreateFileOptions {
5033 unix_times: Some(UnixTimes::mtime_only(T)),
5034 ..Default::default()
5035 };
5036 fs.create_file(&root, "stamp", &mut &b"x"[..], 1, &opts)
5037 .unwrap();
5038 fs.sync_metadata().unwrap();
5039 let entry = fs
5040 .list_directory(&root)
5041 .unwrap()
5042 .into_iter()
5043 .find(|e| e.name == "stamp")
5044 .unwrap();
5045 assert_eq!(entry.modified_unix, Some(T), "UTC must round-trip");
5046 let raw = entry.mac_dates.expect("raw Mac dates").1;
5047 let expected = (T as i64 + local_utc_offset_secs()) as u64;
5048 assert_eq!(
5049 raw,
5050 unix_to_mac_epoch(expected),
5051 "on disk it is the local wall clock"
5052 );
5053 }
5054
5055 #[test]
5059 fn the_alternate_mdb_follows_the_partition_end_when_the_length_is_known() {
5060 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5061 const VOLUME: u64 = 8 * 1024 * 1024;
5062 const PARTITION: u64 = VOLUME + 64 * 1024;
5063 let mut img = create_blank_hfs(VOLUME, 4096, "Slack").unwrap();
5064 img.resize(PARTITION as usize, 0);
5065 {
5066 let mut fs =
5067 HfsFilesystem::open_sized(Cursor::new(&mut img), 0, Some(PARTITION)).unwrap();
5068 let root = fs.root().unwrap();
5069 fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
5070 .unwrap();
5071 fs.sync_metadata().unwrap();
5072 }
5073 let primary = &img[1024..1536];
5074 let alt = &img[PARTITION as usize - 1024..PARTITION as usize - 512];
5075 assert_eq!(
5076 &alt[0..2],
5077 b"BD",
5078 "alternate MDB missing at the partition end"
5079 );
5080 assert_eq!(primary, alt, "the mirror must equal the primary MDB");
5081 }
5082
5083 #[test]
5086 fn create_file_writes_the_finder_flags_it_is_given() {
5087 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5088 let mut img = create_blank_hfs(4 * 1024 * 1024, 4096, "Flags").unwrap();
5089 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5090 let root = fs.root().unwrap();
5091 let opts = CreateFileOptions {
5092 os_type: Some(*b"APPL"),
5093 os_creator: Some(*b"ttxt"),
5094 finder_flags: Some(0x2000),
5095 ..Default::default()
5096 };
5097 fs.create_file(&root, "app", &mut &b"x"[..], 1, &opts)
5098 .unwrap();
5099 fs.sync_metadata().unwrap();
5100 let entry = fs
5101 .list_directory(&root)
5102 .unwrap()
5103 .into_iter()
5104 .find(|e| e.name == "app")
5105 .unwrap();
5106 assert_eq!(entry.finder_flags, Some(0x2000));
5107 }
5108
5109 #[test]
5110 fn shrink_is_refused_when_data_sits_past_the_new_end() {
5111 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5112 const SIZE: u64 = 8 * 1024 * 1024;
5113 let mut img = create_blank_hfs(SIZE, 4096, "Tail").unwrap();
5114 {
5115 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5116 let root = fs.root().unwrap();
5117 fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
5118 .unwrap();
5119 fs.sync_metadata().unwrap();
5120 }
5121 let total = BigEndian::read_u16(&img[1024 + 18..1024 + 20]) as usize;
5122 let vbm_start = BigEndian::read_u16(&img[1024 + 14..1024 + 16]) as usize;
5123 let tail = total - 1;
5125 let byte = vbm_start * 512 + tail / 8;
5126 img[byte] |= 0x80 >> (tail % 8);
5127 let err = resize_hfs_in_place(&mut Cursor::new(&mut img), 0, SIZE / 2, &mut |_| {})
5128 .expect_err("a tail block in use must refuse the shrink");
5129 assert!(format!("{err}").contains("last allocated block"), "{err}");
5130 img[byte] &= !(0x80 >> (tail % 8));
5131 resize_hfs_in_place(&mut Cursor::new(&mut img), 0, SIZE / 2, &mut |_| {})
5132 .expect("with the tail free the shrink goes through");
5133 }
5134
5135 #[test]
5137 fn last_data_byte_is_a_size_the_volume_shrinks_to() {
5138 use disk_core::filesystem::Filesystem;
5139 const SIZE: u64 = 8 * 1024 * 1024;
5140 let mut img = create_blank_hfs(SIZE, 4096, "Tail").unwrap();
5141 let total = BigEndian::read_u16(&img[1024 + 18..1024 + 20]) as usize;
5142 let vbm_start = BigEndian::read_u16(&img[1024 + 14..1024 + 16]) as usize;
5143 let free = BigEndian::read_u16(&img[1024 + 34..1024 + 36]);
5144 let last = total / 2;
5146 img[vbm_start * 512 + last / 8] |= 0x80 >> (last % 8);
5147 BigEndian::write_u16(&mut img[1024 + 34..1024 + 36], free - 1);
5148 let floor = {
5149 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5150 fs.last_data_byte().unwrap()
5151 };
5152 resize_hfs_in_place(&mut Cursor::new(&mut img), 0, floor, &mut |_| {})
5153 .expect("a shrink to last_data_byte must keep the last allocated block");
5154 let new_total = BigEndian::read_u16(&img[1024 + 18..1024 + 20]) as usize;
5155 assert_eq!(
5156 new_total,
5157 last + 1,
5158 "the floor holds exactly the used blocks"
5159 );
5160 }
5161
5162 #[test]
5163 fn test_resize_hfs_rejects_u16_overflow() {
5164 let mut sector = [0u8; 512];
5168 BigEndian::write_u16(&mut sector[0..2], HFS_SIGNATURE);
5169 BigEndian::write_u16(&mut sector[14..16], 3); BigEndian::write_u16(&mut sector[18..20], 64110); BigEndian::write_u32(&mut sector[20..24], 8192); BigEndian::write_u16(&mut sector[28..30], 19); BigEndian::write_u16(&mut sector[34..36], 31423); let mut img = vec![0u8; 2048];
5181 img[1024..1024 + 512].copy_from_slice(§or);
5182
5183 let mut cursor = Cursor::new(img);
5184 let res = resize_hfs_in_place(
5185 &mut cursor,
5186 0,
5187 2047 * 1024 * 1024, &mut |_| {},
5189 );
5190 let err = res.expect_err("resize should have bailed on u16 overflow");
5191 let msg = format!("{err}");
5192 assert!(
5193 msg.contains("65535") || msg.contains("u16") || msg.contains("Volume Bitmap"),
5194 "unexpected error: {msg}"
5195 );
5196 }
5197
5198 #[test]
5199 fn test_mdb_serialize_roundtrip() {
5200 let mut data = [0u8; 512];
5201 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
5202 BigEndian::write_u16(&mut data[18..20], 100);
5203 BigEndian::write_u32(&mut data[20..24], 4096);
5204 BigEndian::write_u16(&mut data[34..36], 50);
5205 BigEndian::write_u32(&mut data[30..34], 10); BigEndian::write_u32(&mut data[92..96], 42); data[36] = 4;
5208 data[37..41].copy_from_slice(b"Test");
5209
5210 let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
5211 let serialized = mdb.serialize_to_sector();
5212 assert_eq!(BigEndian::read_u16(&serialized[18..20]), 100);
5213 assert_eq!(BigEndian::read_u32(&serialized[30..34]), 10);
5214 assert_eq!(BigEndian::read_u32(&serialized[92..96]), 42);
5215 }
5216
5217 fn make_editable_hfs_image() -> Vec<u8> {
5229 let block_size = 4096u32;
5230 let total_blocks = 128u16;
5231 let first_alloc_block = 5u16; let alloc_start = first_alloc_block as usize * 512;
5233 let image_size = alloc_start + total_blocks as usize * block_size as usize;
5234 let mut img = vec![0u8; image_size];
5235
5236 let catalog_start = 0u16; let catalog_blocks = 8u16;
5238 let catalog_size = catalog_blocks as u32 * block_size;
5239
5240 let bitmap_sector = 3u16;
5242 let bitmap_off = bitmap_sector as usize * 512;
5243 img[bitmap_off] = 0b11111111;
5245
5246 let node_size = 4096usize;
5248 let catalog_offset = alloc_start; let hdr_off = catalog_offset;
5252 img[hdr_off + 8] = 1; BigEndian::write_u16(&mut img[hdr_off + 10..hdr_off + 12], 3); let hr = hdr_off + 14;
5257 BigEndian::write_u16(&mut img[hr..hr + 2], 1); BigEndian::write_u32(&mut img[hr + 2..hr + 6], 1); BigEndian::write_u32(&mut img[hr + 6..hr + 10], 2); BigEndian::write_u32(&mut img[hr + 10..hr + 14], 1); BigEndian::write_u32(&mut img[hr + 14..hr + 18], 1); BigEndian::write_u16(&mut img[hr + 18..hr + 20], node_size as u16); BigEndian::write_u16(&mut img[hr + 20..hr + 22], 37); BigEndian::write_u32(&mut img[hr + 22..hr + 26], 8); BigEndian::write_u32(&mut img[hr + 26..hr + 30], 6); let ot = hdr_off + node_size;
5269 BigEndian::write_u16(&mut img[ot - 2..ot], 14); BigEndian::write_u16(&mut img[ot - 4..ot - 2], 142); BigEndian::write_u16(&mut img[ot - 6..ot - 4], 270); BigEndian::write_u16(&mut img[ot - 8..ot - 6], 526); img[hdr_off + 270] = 0b11000000;
5276
5277 let leaf_off = catalog_offset + node_size;
5279 img[leaf_off + 8] = 0xFF; img[leaf_off + 9] = 1; BigEndian::write_u16(&mut img[leaf_off + 10..leaf_off + 12], 2); let r0_off = leaf_off + 14;
5287 img[r0_off] = 6; img[r0_off + 1] = 0; BigEndian::write_u32(&mut img[r0_off + 2..r0_off + 6], 1); img[r0_off + 6] = 0; img[r0_off + 7] = 0;
5293 let r0_data = r0_off + 8;
5295 img[r0_data] = CATALOG_DIR as u8; BigEndian::write_u32(&mut img[r0_data + 6..r0_data + 10], 2); let r1_off = r0_data + 70;
5302 img[r1_off] = 6; img[r1_off + 1] = 0; BigEndian::write_u32(&mut img[r1_off + 2..r1_off + 6], 2); img[r1_off + 6] = 0; img[r1_off + 7] = 0; let r1_data = r1_off + 8;
5308 img[r1_data] = CATALOG_DIR_THREAD as u8; img[r1_data + 1] = 0; BigEndian::write_u32(&mut img[r1_data + 10..r1_data + 14], 1); img[r1_data + 14] = 0; let lot = leaf_off + node_size;
5318 BigEndian::write_u16(&mut img[lot - 2..lot], 14); let r1_rel = (r1_off - leaf_off) as u16;
5320 BigEndian::write_u16(&mut img[lot - 4..lot - 2], r1_rel); let free_rel = (r1_data + 46 - leaf_off) as u16;
5322 BigEndian::write_u16(&mut img[lot - 6..lot - 4], free_rel); let mdb_off = 1024;
5326 let mut mdb = [0u8; 512];
5327 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
5328 BigEndian::write_u32(&mut mdb[2..6], hfs_common::hfs_now()); BigEndian::write_u32(&mut mdb[6..10], hfs_common::hfs_now()); BigEndian::write_u16(&mut mdb[14..16], bitmap_sector); BigEndian::write_u16(&mut mdb[18..20], total_blocks);
5332 BigEndian::write_u32(&mut mdb[20..24], block_size);
5333 BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
5334 BigEndian::write_u32(&mut mdb[30..34], 16); BigEndian::write_u16(&mut mdb[34..36], total_blocks - catalog_blocks); mdb[36] = 7;
5338 mdb[37..44].copy_from_slice(b"TestVol");
5339 BigEndian::write_u32(&mut mdb[84..88], 0); BigEndian::write_u32(&mut mdb[88..92], 0); BigEndian::write_u32(&mut mdb[146..150], catalog_size);
5343 BigEndian::write_u16(&mut mdb[150..152], catalog_start);
5345 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
5346
5347 img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
5348
5349 img
5350 }
5351
5352 fn make_uninitialized_hfs_image() -> Vec<u8> {
5357 let block_size = 512u32;
5358 let total_blocks = 256u16;
5359 let first_alloc_block = 6u16;
5360 let alloc_start = first_alloc_block as usize * 512;
5361 let image_size = alloc_start + total_blocks as usize * block_size as usize;
5362 let mut img = vec![0u8; image_size];
5363
5364 let extents_start = 0u16;
5366 let extents_blocks = 4u16;
5367 let catalog_start = 4u16;
5369 let catalog_blocks = 4u16;
5370
5371 let mdb_off = 1024usize;
5374 let mut mdb = [0u8; 512];
5375 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
5376 BigEndian::write_u32(&mut mdb[2..6], hfs_common::hfs_now());
5377 BigEndian::write_u32(&mut mdb[6..10], hfs_common::hfs_now());
5378 BigEndian::write_u16(&mut mdb[14..16], 3); BigEndian::write_u16(&mut mdb[18..20], total_blocks);
5380 BigEndian::write_u32(&mut mdb[20..24], block_size);
5381 BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
5382 BigEndian::write_u32(&mut mdb[30..34], 16);
5383 BigEndian::write_u16(
5384 &mut mdb[34..36],
5385 total_blocks - extents_blocks - catalog_blocks,
5386 );
5387 mdb[36] = 5;
5389 mdb[37..42].copy_from_slice(b"Blank");
5390 BigEndian::write_u32(&mut mdb[130..134], extents_blocks as u32 * block_size);
5392 BigEndian::write_u16(&mut mdb[134..136], extents_start);
5393 BigEndian::write_u16(&mut mdb[136..138], extents_blocks);
5394 BigEndian::write_u32(&mut mdb[146..150], catalog_blocks as u32 * block_size);
5396 BigEndian::write_u16(&mut mdb[150..152], catalog_start);
5397 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
5398
5399 img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
5400 img
5401 }
5402
5403 #[test]
5404 fn test_hfs_empty_volume_auto_initialize_and_create_file() {
5405 let img = make_uninitialized_hfs_image();
5408 let cursor = Cursor::new(img);
5409 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5410 assert_eq!(fs.fs_type(), "HFS");
5411
5412 let root = fs.root().unwrap();
5414 let entries = fs.list_directory(&root).unwrap();
5415 assert!(entries.is_empty());
5416
5417 let data = b"auto-init test";
5419 let mut cursor = Cursor::new(data.as_slice());
5420 let fe = fs
5421 .create_file(
5422 &root,
5423 "hello.txt",
5424 &mut cursor,
5425 data.len() as u64,
5426 &CreateFileOptions::default(),
5427 )
5428 .unwrap();
5429 assert_eq!(fe.name, "hello.txt");
5430 fs.sync_metadata().unwrap();
5431
5432 let entries = fs.list_directory(&root).unwrap();
5434 assert_eq!(entries.len(), 1);
5435 assert_eq!(entries[0].name, "hello.txt");
5436 let read_back = fs.read_file(&fe, 1024).unwrap();
5437 assert_eq!(&read_back, data);
5438
5439 let result = fs.fsck().unwrap();
5441 let err_msgs: Vec<_> = result.errors.iter().map(|e| e.message.clone()).collect();
5442 let warn_msgs: Vec<_> = result.warnings.iter().map(|w| w.message.clone()).collect();
5443 assert!(
5444 result.errors.is_empty(),
5445 "unexpected fsck errors: {err_msgs:?}"
5446 );
5447 assert!(
5448 result.warnings.is_empty(),
5449 "unexpected fsck warnings: {warn_msgs:?}"
5450 );
5451 }
5452
5453 #[test]
5454 fn test_hfs_editable_open_and_free_space() {
5455 let img = make_editable_hfs_image();
5456 let cursor = Cursor::new(img);
5457 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5458 assert_eq!(fs.fs_type(), "HFS");
5459 assert_eq!(fs.volume_label(), Some("TestVol"));
5460 let free = fs.free_space().unwrap();
5461 assert_eq!(free, 120 * 4096);
5463 }
5464
5465 #[test]
5466 fn test_hfs_editable_create_file_and_read() {
5467 let img = make_editable_hfs_image();
5468 let cursor = Cursor::new(img);
5469 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5470
5471 let root = fs.root().unwrap();
5472 let test_data = b"Hello, HFS World!";
5473 let mut data_reader = Cursor::new(test_data.as_slice());
5474 let options = CreateFileOptions::default();
5475 let fe = fs
5476 .create_file(
5477 &root,
5478 "test.txt",
5479 &mut data_reader,
5480 test_data.len() as u64,
5481 &options,
5482 )
5483 .unwrap();
5484
5485 assert_eq!(fe.name, "test.txt");
5486 assert_eq!(fe.size, test_data.len() as u64);
5487
5488 let entries = fs.list_directory(&root).unwrap();
5490 assert!(entries.iter().any(|e| e.name == "test.txt"));
5491
5492 let read_back = fs.read_file(&fe, 1024).unwrap();
5494 assert_eq!(&read_back, test_data);
5495 }
5496
5497 #[test]
5505 fn test_hfs_file_record_first_alloc_block_fields_are_zero() {
5506 use crate::hfs_common::walk_leaf_records;
5507 use disk_core::filesystem::ResourceForkSource;
5508
5509 let img = make_editable_hfs_image();
5510 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5511 let root = fs.root().unwrap();
5512
5513 let data = b"data fork contents that span a block";
5516 let options = CreateFileOptions {
5517 resource_fork: Some(ResourceForkSource::Data(b"resource fork bytes".to_vec())),
5518 ..Default::default()
5519 };
5520 fs.create_file(
5521 &root,
5522 "forked.bin",
5523 &mut Cursor::new(data.as_slice()),
5524 data.len() as u64,
5525 &options,
5526 )
5527 .unwrap();
5528 fs.sync_metadata().unwrap();
5529
5530 let node_size = BigEndian::read_u16(&fs.catalog_data[32..34]) as usize;
5531 let first_leaf = BigEndian::read_u32(&fs.catalog_data[24..28]);
5532 let found = walk_leaf_records::<bool, _>(
5533 &fs.catalog_data,
5534 first_leaf,
5535 node_size,
5536 |_n, _i, _off, rec| {
5537 let klen = rec[0] as usize;
5538 let mut ds = 1 + klen;
5539 if !ds.is_multiple_of(2) {
5540 ds += 1;
5541 }
5542 let body = &rec[ds..];
5543 if body.first() != Some(&2) {
5544 return None; }
5546 let fil_st_blk = BigEndian::read_u16(&body[24..26]);
5547 let fil_rst_blk = BigEndian::read_u16(&body[34..36]);
5548 let data_ext_start = BigEndian::read_u16(&body[74..76]);
5549 let rsrc_ext_start = BigEndian::read_u16(&body[86..88]);
5550 assert_eq!(fil_st_blk, 0, "filStBlk (offset 24) must be 0 on disk");
5551 assert_eq!(fil_rst_blk, 0, "filRStBlk (offset 34) must be 0 on disk");
5552 assert_ne!(data_ext_start, 0, "data fork extent should be allocated");
5554 assert_ne!(rsrc_ext_start, 0, "rsrc fork extent should be allocated");
5555 Some(true)
5556 },
5557 );
5558 assert_eq!(found, Some(true), "file record not found in catalog");
5559 }
5560
5561 #[test]
5566 fn test_hfs_catalog_dates_round_trip() {
5567 let img = make_editable_hfs_image();
5568 let mut buf = img.clone();
5569
5570 {
5572 let mut fs = HfsFilesystem::open(Cursor::new(&mut buf), 0).unwrap();
5573 let root = fs.root().unwrap();
5574 let data = b"x";
5575 let fe = fs
5576 .create_file(
5577 &root,
5578 "dated.txt",
5579 &mut Cursor::new(data.as_slice()),
5580 1,
5581 &CreateFileOptions::default(),
5582 )
5583 .unwrap();
5584 fs.sync_metadata().unwrap();
5585
5586 let listed = fs.list_directory(&root).unwrap();
5587 let f = listed.iter().find(|e| e.name == "dated.txt").unwrap();
5588 let (c, m, b) = f.mac_dates.expect("HFS file should carry mac_dates");
5589 assert!(c != 0 && m != 0, "create/modify stamped by create_file");
5590 assert_eq!(b, 0, "backup date defaults to 0");
5591
5592 let target = (0x10000000u32, 0x20000000u32, 0x30000000u32);
5594 fs.set_dates(&fe, target.0, target.1, target.2).unwrap();
5595 fs.sync_metadata().unwrap();
5596 }
5597
5598 let mut fs = HfsFilesystem::open(Cursor::new(&mut buf), 0).unwrap();
5601 let root = fs.root().unwrap();
5602 let listed = fs.list_directory(&root).unwrap();
5603 let f = listed.iter().find(|e| e.name == "dated.txt").unwrap();
5604 assert_eq!(f.mac_dates, Some((0x10000000, 0x20000000, 0x30000000)));
5605 assert_eq!(f.modified, hfs_common::format_mac_date(0x20000000));
5606 }
5607
5608 #[test]
5609 fn test_hfs_editable_create_directory() {
5610 let img = make_editable_hfs_image();
5611 let cursor = Cursor::new(img);
5612 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5613
5614 let root = fs.root().unwrap();
5615 let options = CreateDirectoryOptions::default();
5616 let dir = fs.create_directory(&root, "NewDir", &options).unwrap();
5617
5618 assert_eq!(dir.name, "NewDir");
5619 assert!(dir.is_directory());
5620
5621 let entries = fs.list_directory(&root).unwrap();
5622 assert!(entries
5623 .iter()
5624 .any(|e| e.name == "NewDir" && e.is_directory()));
5625 }
5626
5627 #[test]
5628 fn test_hfs_editable_rename_file() {
5629 let img = make_editable_hfs_image();
5630 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5631 let root = fs.root().unwrap();
5632
5633 let data = b"keep my bytes across a rename";
5634 let mut r = Cursor::new(data.as_slice());
5635 let fe = fs
5636 .create_file(
5637 &root,
5638 "Before",
5639 &mut r,
5640 data.len() as u64,
5641 &CreateFileOptions::default(),
5642 )
5643 .unwrap();
5644 let cnid = fe.location;
5645
5646 fs.rename(&root, &fe, "After Doc").unwrap();
5647
5648 let entries = fs.list_directory(&root).unwrap();
5649 assert!(entries.iter().any(|e| e.name == "After Doc"), "{entries:?}");
5650 assert!(!entries.iter().any(|e| e.name == "Before"), "{entries:?}");
5651 let renamed = entries.iter().find(|e| e.name == "After Doc").unwrap();
5652 assert_eq!(renamed.location, cnid);
5655 assert_eq!(&fs.read_file(renamed, 1024).unwrap(), data);
5656
5657 fs.create_directory(&root, "OldDir", &CreateDirectoryOptions::default())
5660 .unwrap();
5661 let old_dir = fs
5662 .list_directory(&root)
5663 .unwrap()
5664 .into_iter()
5665 .find(|e| e.name == "OldDir")
5666 .unwrap();
5667 fs.rename(&root, &old_dir, "NewDir").unwrap();
5668 let new_dir = fs
5669 .list_directory(&root)
5670 .unwrap()
5671 .into_iter()
5672 .find(|e| e.name == "NewDir")
5673 .expect("renamed dir present");
5674 assert!(fs.list_directory(&new_dir).is_ok());
5675
5676 let after = fs
5678 .list_directory(&root)
5679 .unwrap()
5680 .into_iter()
5681 .find(|e| e.name == "After Doc")
5682 .unwrap();
5683 assert!(matches!(
5684 fs.rename(&root, &after, "NewDir"),
5685 Err(FilesystemError::AlreadyExists(_))
5686 ));
5687
5688 fs.sync_metadata().unwrap();
5689 }
5690
5691 #[test]
5692 fn test_hfs_editable_delete_file() {
5693 let img = make_editable_hfs_image();
5694 let cursor = Cursor::new(img);
5695 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5696
5697 let root = fs.root().unwrap();
5698 let test_data = b"delete me";
5699 let mut data_reader = Cursor::new(test_data.as_slice());
5700 let options = CreateFileOptions::default();
5701 let fe = fs
5702 .create_file(
5703 &root,
5704 "gone.txt",
5705 &mut data_reader,
5706 test_data.len() as u64,
5707 &options,
5708 )
5709 .unwrap();
5710
5711 let free_before = fs.free_space().unwrap();
5712 fs.delete_entry(&root, &fe).unwrap();
5713
5714 let entries = fs.list_directory(&root).unwrap();
5715 assert!(!entries.iter().any(|e| e.name == "gone.txt"));
5716
5717 let free_after = fs.free_space().unwrap();
5718 assert!(free_after > free_before);
5719 }
5720
5721 #[test]
5722 fn test_hfs_editable_duplicate_name_rejected() {
5723 let img = make_editable_hfs_image();
5724 let cursor = Cursor::new(img);
5725 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5726
5727 let root = fs.root().unwrap();
5728 let test_data = b"first";
5729 let mut r1 = Cursor::new(test_data.as_slice());
5730 let options = CreateFileOptions::default();
5731 fs.create_file(&root, "dup.txt", &mut r1, 5, &options)
5732 .unwrap();
5733
5734 let mut r2 = Cursor::new(test_data.as_slice());
5735 let result = fs.create_file(&root, "dup.txt", &mut r2, 5, &options);
5736 assert!(matches!(result, Err(FilesystemError::AlreadyExists(_))));
5737 }
5738
5739 #[test]
5740 fn test_hfs_editable_name_too_long() {
5741 let img = make_editable_hfs_image();
5742 let cursor = Cursor::new(img);
5743 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5744
5745 let root = fs.root().unwrap();
5746 let long_name = "a".repeat(32); let test_data = b"x";
5748 let mut r = Cursor::new(test_data.as_slice());
5749 let options = CreateFileOptions::default();
5750 let result = fs.create_file(&root, &long_name, &mut r, 1, &options);
5751 assert!(matches!(result, Err(FilesystemError::InvalidData(_))));
5752 }
5753
5754 #[test]
5755 fn test_hfs_editable_blessed_folder() {
5756 let img = make_editable_hfs_image();
5757 let cursor = Cursor::new(img);
5758 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5759
5760 assert!(fs.blessed_system_folder().is_none());
5762
5763 let root = fs.root().unwrap();
5765 let options = CreateDirectoryOptions::default();
5766 let sys_dir = fs.create_directory(&root, "System", &options).unwrap();
5767
5768 fs.set_blessed_folder(&sys_dir).unwrap();
5770
5771 let blessed = fs.blessed_system_folder();
5773 assert!(blessed.is_some());
5774 let (cnid, name) = blessed.unwrap();
5775 assert_eq!(cnid, sys_dir.location);
5776 assert_eq!(name, "System");
5777 }
5778
5779 #[test]
5780 fn test_hfs_editable_type_creator_auto_detect() {
5781 let img = make_editable_hfs_image();
5782 let cursor = Cursor::new(img);
5783 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5784
5785 let root = fs.root().unwrap();
5786 let test_data = b"text content";
5787 let mut data_reader = Cursor::new(test_data.as_slice());
5788 let options = CreateFileOptions::default();
5789 let fe = fs
5790 .create_file(
5791 &root,
5792 "readme.txt",
5793 &mut data_reader,
5794 test_data.len() as u64,
5795 &options,
5796 )
5797 .unwrap();
5798
5799 assert!(fe.type_code.is_some());
5800 assert_eq!(fe.type_code, Some(*b"TEXT"));
5801 }
5802
5803 fn assert_fsck_clean<R: Read + Seek>(fs: &mut HfsFilesystem<R>) {
5805 let result = fs.fsck().unwrap();
5806 if !result.is_clean() {
5807 let msgs: Vec<_> = result
5808 .errors
5809 .iter()
5810 .map(|e| format!("[{}] {}", e.code, e.message))
5811 .collect();
5812 panic!("fsck found {} error(s):\n{}", msgs.len(), msgs.join("\n"));
5813 }
5814 }
5815
5816 #[test]
5817 fn test_fsck_clean_fresh_image() {
5818 let img = make_editable_hfs_image();
5819 let cursor = Cursor::new(img);
5820 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5821
5822 let result = fs.fsck().unwrap();
5823 let err_msgs: Vec<&str> = result.errors.iter().map(|e| e.message.as_str()).collect();
5824 assert!(
5825 result.is_clean(),
5826 "fresh image should be clean: {:?}",
5827 err_msgs
5828 );
5829 assert_eq!(result.stats.directories_checked, 1); assert_eq!(result.stats.files_checked, 0);
5831 }
5832
5833 #[test]
5834 fn test_fsck_clean_after_create_file() {
5835 let img = make_editable_hfs_image();
5836 let cursor = Cursor::new(img);
5837 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5838
5839 let root = fs.root().unwrap();
5840 let test_data = b"Hello, HFS World!";
5841 let mut data_reader = Cursor::new(test_data.as_slice());
5842 let options = CreateFileOptions::default();
5843 fs.create_file(
5844 &root,
5845 "test.txt",
5846 &mut data_reader,
5847 test_data.len() as u64,
5848 &options,
5849 )
5850 .unwrap();
5851
5852 assert_fsck_clean(&mut fs);
5853 }
5854
5855 #[test]
5856 fn test_fsck_clean_after_create_directory() {
5857 let img = make_editable_hfs_image();
5858 let cursor = Cursor::new(img);
5859 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5860
5861 let root = fs.root().unwrap();
5862 let options = CreateDirectoryOptions::default();
5863 fs.create_directory(&root, "NewDir", &options).unwrap();
5864
5865 assert_fsck_clean(&mut fs);
5866 }
5867
5868 #[test]
5869 fn test_fsck_clean_after_delete_file() {
5870 let img = make_editable_hfs_image();
5871 let cursor = Cursor::new(img);
5872 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5873
5874 let root = fs.root().unwrap();
5875 let test_data = b"delete me";
5876 let mut data_reader = Cursor::new(test_data.as_slice());
5877 let options = CreateFileOptions::default();
5878 let fe = fs
5879 .create_file(
5880 &root,
5881 "gone.txt",
5882 &mut data_reader,
5883 test_data.len() as u64,
5884 &options,
5885 )
5886 .unwrap();
5887
5888 assert_fsck_clean(&mut fs);
5889 fs.delete_entry(&root, &fe).unwrap();
5890 assert_fsck_clean(&mut fs);
5891 }
5892
5893 #[test]
5902 fn test_hfs_random_insert_packs_densely_and_fsck_clean() {
5903 use crate::hfs_common::BTreeHeader;
5904 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
5905 let block_size = 32 * 1024u32;
5906 let img = create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Pack", 0, 16 * 1024 * 1024)
5907 .unwrap();
5908 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
5909 let root_id = fs.root().unwrap().location as u32;
5910
5911 let n = 16000u32;
5913 let mut seen = std::collections::HashSet::new();
5914 let order: Vec<u32> = (0..n)
5915 .map(|i| i.wrapping_mul(2654435761) % n)
5916 .filter(|x| seen.insert(*x))
5917 .collect();
5918 let count = order.len() as u32;
5919 fs.begin_bulk();
5920 for &i in &order {
5921 let name = format!("f{:05}", i);
5922 let mut kr = Fs::build_catalog_key(root_id, name.as_bytes());
5923 kr.extend_from_slice(&Fs::build_file_record(
5924 16 + i,
5925 0,
5926 0,
5927 0,
5928 0,
5929 0,
5930 0,
5931 &[0u8; 4],
5932 &[0u8; 4],
5933 block_size,
5934 None,
5935 0,
5936 ));
5937 fs.insert_catalog_record(&kr).unwrap();
5938 }
5939 fs.end_bulk();
5940 fs.mdb.file_count = count;
5941 fs.mdb.next_catalog_id = 16 + n;
5942 fs.update_parent_valence(root_id, count as i32).unwrap();
5943
5944 let h = BTreeHeader::read(fs.catalog_data());
5947 let used = h.total_nodes - h.free_nodes;
5948 let rec_per_node = count as f64 / used as f64;
5949 assert!(
5950 rec_per_node > 2.8,
5951 "random-order packing too sparse: {count} records in {used} nodes ({rec_per_node:.2}/node)"
5952 );
5953
5954 let result = fs.fsck().unwrap();
5955 assert!(
5956 result.errors.is_empty(),
5957 "fsck errors after random import: {:?}",
5958 result
5959 .errors
5960 .iter()
5961 .take(8)
5962 .map(|e| &e.message)
5963 .collect::<Vec<_>>()
5964 );
5965 }
5966
5967 #[test]
5974 fn test_hfs_bulk_mode_import_fsck_clean() {
5975 let block_size = 32 * 1024u32;
5976 let img = create_blank_hfs_sized(
5977 64 * 1024 * 1024,
5978 block_size,
5979 "BulkImport",
5980 0,
5981 8 * 1024 * 1024,
5982 )
5983 .expect("create blank volume");
5984 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5985 let root = fs.root().unwrap();
5986 let root_id = root.location as u32;
5987 let options = CreateFileOptions::default();
5988
5989 fs.begin_bulk();
5990 for i in 0..9000 {
5991 let name = format!("f{:05}.txt", i);
5992 let mut empty = Cursor::new(Vec::new());
5993 fs.create_file(&root, &name, &mut empty, 0, &options)
5994 .unwrap_or_else(|e| panic!("bulk create_file #{i}: {e}"));
5995 }
5996 fs.end_bulk();
5997
5998 assert_eq!(fs.mdb.file_count, 9000);
5999 let result = fs.fsck().unwrap();
6000 assert!(
6001 result.errors.is_empty(),
6002 "bulk-mode import left fsck errors: {:?}",
6003 result
6004 .errors
6005 .iter()
6006 .take(8)
6007 .map(|e| &e.message)
6008 .collect::<Vec<_>>()
6009 );
6010 assert!(fs
6012 .find_catalog_record_by_name(root_id, b"f04500.txt")
6013 .is_some());
6014 assert_eq!(fs.list_directory(&root).unwrap().len(), 9000);
6015
6016 let mut empty = Cursor::new(Vec::new());
6019 assert!(fs
6020 .create_file(&root, "f00000.txt", &mut empty, 0, &options)
6021 .is_err());
6022 assert_fsck_clean(&mut fs);
6023 }
6024
6025 #[test]
6040 fn test_hfs_catalog_scales_past_header_bitmap_fsck_clean() {
6041 use crate::hfs_common::BTreeHeader;
6042 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
6043 let block_size = 32 * 1024u32;
6044 let img = create_blank_hfs_sized(
6048 64 * 1024 * 1024,
6049 block_size,
6050 "Scale20k",
6051 0,
6052 10 * 1024 * 1024,
6053 )
6054 .expect("create blank volume");
6055 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6056 let root = fs.root().unwrap();
6057 let root_id = root.location as u32;
6058
6059 let n: u32 = 22_000;
6060 for i in 0..n {
6061 let name = format!("f{:05}.txt", i);
6062 let mut key_record = Fs::build_catalog_key(root_id, name.as_bytes());
6063 let file_rec = Fs::build_file_record(
6064 16 + i,
6065 0,
6066 0,
6067 0,
6068 0,
6069 0,
6070 0,
6071 &[0u8; 4],
6072 &[0u8; 4],
6073 block_size,
6074 None,
6075 0,
6076 );
6077 key_record.extend_from_slice(&file_rec);
6078 fs.insert_catalog_record(&key_record)
6079 .unwrap_or_else(|e| panic!("insert #{i} ({name}): {e}"));
6080 }
6081
6082 fs.mdb.file_count = n;
6085 fs.mdb.next_catalog_id = 16 + n;
6086 fs.update_parent_valence(root_id, n as i32).unwrap();
6087
6088 let header = BTreeHeader::read(fs.catalog_data());
6091 assert!(
6092 header.depth >= 3,
6093 "expected a multi-level index, depth = {}",
6094 header.depth
6095 );
6096 let used = header.total_nodes - header.free_nodes;
6097 assert!(
6098 used > 2048,
6099 "expected > 2048 catalog nodes in use, got {used}"
6100 );
6101
6102 let result = fs.fsck().unwrap();
6103 assert!(
6104 result.errors.is_empty(),
6105 "fsck found {} errors after {n}-record import: {:?}",
6106 result.errors.len(),
6107 result
6108 .errors
6109 .iter()
6110 .take(8)
6111 .map(|e| &e.message)
6112 .collect::<Vec<_>>()
6113 );
6114
6115 for i in [0u32, 1, 7386, 12345, n - 1] {
6119 let name = format!("f{:05}.txt", i);
6120 assert!(
6121 fs.find_catalog_record_by_name(root_id, name.as_bytes())
6122 .is_some(),
6123 "record {name} not findable after bulk insert"
6124 );
6125 }
6126 }
6127
6128 #[test]
6138 fn test_hfs_incremental_shuffled_multidir_packs_dense_fsck_clean() {
6139 use crate::hfs_common::BTreeHeader;
6140 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
6141 let block_size = 32 * 1024u32;
6142 let img = create_blank_hfs_sized(
6146 128 * 1024 * 1024,
6147 block_size,
6148 "PutShuffle",
6149 0,
6150 6 * 1024 * 1024,
6151 )
6152 .expect("create blank volume");
6153 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6154
6155 const DIRS: u32 = 200;
6157 let root = fs.root().unwrap();
6158 let mut dir_ids = Vec::with_capacity(DIRS as usize);
6159 for d in 0..DIRS {
6160 let name = format!("dir{d:03}");
6161 let de = fs
6162 .create_directory(&root, &name, &Default::default())
6163 .unwrap();
6164 dir_ids.push(de.location as u32);
6165 }
6166 let base_cnid = fs.mdb.next_catalog_id;
6167
6168 let n: u32 = 20_000;
6172 let mut per_dir = vec![0i32; DIRS as usize];
6173 for i in 0..n {
6174 let hash = i.wrapping_mul(2_654_435_761);
6175 let d = (hash % DIRS) as usize;
6176 let name = format!("f{hash:08x}");
6177 let mut kr = Fs::build_catalog_key(dir_ids[d], name.as_bytes());
6178 kr.extend_from_slice(&Fs::build_file_record(
6179 base_cnid + i,
6180 0,
6181 0,
6182 0,
6183 0,
6184 0,
6185 0,
6186 &[0u8; 4],
6187 &[0u8; 4],
6188 block_size,
6189 None,
6190 0,
6191 ));
6192 fs.insert_catalog_record(&kr)
6193 .unwrap_or_else(|e| panic!("insert #{i} into dir{d}: {e}"));
6194 per_dir[d] += 1;
6195 }
6196 fs.mdb.file_count += n;
6199 fs.mdb.next_catalog_id = base_cnid + n;
6200 for (d, &cnt) in per_dir.iter().enumerate() {
6201 fs.update_parent_valence(dir_ids[d], cnt).unwrap();
6202 }
6203
6204 let h = BTreeHeader::read(fs.catalog_data());
6208 let used = h.total_nodes - h.free_nodes;
6209 let rec_per_node = h.leaf_records as f64 / used as f64;
6210 assert!(
6211 rec_per_node > 2.6,
6212 "shuffled multi-dir packing too sparse: {} records in {used} nodes ({rec_per_node:.2}/node)",
6213 h.leaf_records
6214 );
6215
6216 let result = fs.fsck().unwrap();
6218 assert!(
6219 result.errors.is_empty(),
6220 "fsck found {} errors after 20k shuffled multi-dir put: {:?}",
6221 result.errors.len(),
6222 result
6223 .errors
6224 .iter()
6225 .take(8)
6226 .map(|e| &e.message)
6227 .collect::<Vec<_>>()
6228 );
6229
6230 for i in [0u32, 7, 9_999, n - 1] {
6232 let hash = i.wrapping_mul(2_654_435_761);
6233 let parent = dir_ids[(hash % DIRS) as usize];
6234 let name = format!("f{hash:08x}");
6235 assert!(
6236 fs.find_catalog_record_by_name(parent, name.as_bytes())
6237 .is_some(),
6238 "record {name} not findable after shuffled multi-dir insert"
6239 );
6240 }
6241 }
6242
6243 #[test]
6248 fn test_rebuild_index_nodes_past_header_bitmap_fsck_clean() {
6249 use crate::hfs_common::BTreeHeader;
6250 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
6251 let block_size = 32 * 1024u32;
6252 let img =
6253 create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Rebuild", 0, 10 * 1024 * 1024)
6254 .expect("create blank volume");
6255 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6256 let root_id = fs.root().unwrap().location as u32;
6257
6258 let n: u32 = 12_000;
6259 for i in 0..n {
6260 let name = format!("f{:05}.txt", i);
6261 let mut key_record = Fs::build_catalog_key(root_id, name.as_bytes());
6262 let file_rec = Fs::build_file_record(
6263 16 + i,
6264 0,
6265 0,
6266 0,
6267 0,
6268 0,
6269 0,
6270 &[0u8; 4],
6271 &[0u8; 4],
6272 block_size,
6273 None,
6274 0,
6275 );
6276 key_record.extend_from_slice(&file_rec);
6277 fs.insert_catalog_record(&key_record).unwrap();
6278 }
6279 fs.mdb.file_count = n;
6280 fs.mdb.next_catalog_id = 16 + n;
6281 fs.update_parent_valence(root_id, n as i32).unwrap();
6282
6283 let node_size = BTreeHeader::read(fs.catalog_data()).node_size as usize;
6284 let used_before = {
6285 let h = BTreeHeader::read(fs.catalog_data());
6286 h.total_nodes - h.free_nodes
6287 };
6288 assert!(
6289 used_before > 2048,
6290 "test needs > 2048 nodes, got {used_before}"
6291 );
6292
6293 let mut report = disk_core::fsck::RepairReport {
6295 fixes_applied: Vec::new(),
6296 fixes_failed: Vec::new(),
6297 unrepairable_count: 0,
6298 };
6299 crate::hfs_fsck::rebuild_index_nodes(&mut fs.catalog_data, node_size, &mut report);
6300 assert!(
6301 report.fixes_failed.is_empty(),
6302 "rebuild reported failures: {:?}",
6303 report.fixes_failed
6304 );
6305
6306 let result = fs.fsck().unwrap();
6307 assert!(
6308 result.errors.is_empty(),
6309 "fsck after rebuild: {:?}",
6310 result
6311 .errors
6312 .iter()
6313 .take(8)
6314 .map(|e| &e.message)
6315 .collect::<Vec<_>>()
6316 );
6317 }
6318
6319 #[test]
6325 fn test_hfs_create_file_multilevel_index_fsck_clean() {
6326 use crate::hfs_common::BTreeHeader;
6327 let block_size = 32 * 1024u32;
6328 let img =
6329 create_blank_hfs_sized(32 * 1024 * 1024, block_size, "MultiIdx", 0, 4 * 1024 * 1024)
6330 .expect("create blank volume");
6331 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6332 let root = fs.root().unwrap();
6333 let options = CreateFileOptions::default();
6334 for i in 0..2000 {
6335 let name = format!("f{:05}.txt", i);
6336 let mut empty = Cursor::new(Vec::new());
6337 fs.create_file(&root, &name, &mut empty, 0, &options)
6338 .unwrap_or_else(|e| panic!("create_file #{i}: {e}"));
6339 }
6340 let header = BTreeHeader::read(fs.catalog_data());
6341 assert!(
6342 header.depth >= 2,
6343 "expected an index level, depth = {}",
6344 header.depth
6345 );
6346 let result = fs.fsck().unwrap();
6347 assert!(
6348 result.errors.is_empty(),
6349 "fsck errors: {:?}",
6350 result
6351 .errors
6352 .iter()
6353 .take(8)
6354 .map(|e| &e.message)
6355 .collect::<Vec<_>>()
6356 );
6357 assert_eq!(fs.mdb.file_count, 2000);
6358 }
6359
6360 #[test]
6366 fn deleting_a_leafs_first_record_refreshes_the_separators() {
6367 use crate::hfs_common::{
6370 btree_record_range, btree_stale_index_keys, BTreeHeader, BTreeKeyFormat,
6371 };
6372 let block_size = 32 * 1024u32;
6373 let img = create_blank_hfs_sized(32 * 1024 * 1024, block_size, "IKey", 0, 4 * 1024 * 1024)
6374 .expect("create blank volume");
6375 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6376 let root = fs.root().unwrap();
6377 let options = CreateFileOptions::default();
6378 for i in 0..2000 {
6379 let mut empty = Cursor::new(Vec::new());
6380 fs.create_file(&root, &format!("f{i:05}.txt"), &mut empty, 0, &options)
6381 .unwrap();
6382 }
6383 let node_size = BigEndian::read_u16(&fs.catalog_data()[32..34]) as usize;
6384 let header = BTreeHeader::read(fs.catalog_data());
6385 assert!(
6386 header.depth >= 2,
6387 "need an index level, depth {}",
6388 header.depth
6389 );
6390
6391 let first = header.first_leaf_node as usize;
6392 let second =
6393 BigEndian::read_u32(&fs.catalog_data()[first * node_size..first * node_size + 4])
6394 as usize;
6395 let name = {
6396 let node = &fs.catalog_data()[second * node_size..(second + 1) * node_size];
6397 let (s, e) = btree_record_range(node, node_size, 0);
6398 let rec = &node[s..e];
6399 let name_len = rec[6] as usize;
6400 String::from_utf8_lossy(&rec[7..7 + name_len]).into_owned()
6401 };
6402 assert!(
6403 name.starts_with('f'),
6404 "expected a file record first, got {name:?}"
6405 );
6406 let victim = fs
6407 .list_directory(&root)
6408 .unwrap()
6409 .into_iter()
6410 .find(|e| e.name == name)
6411 .expect("victim listed");
6412 fs.delete_entry(&root, &victim).unwrap();
6413
6414 let stale = btree_stale_index_keys(
6415 fs.catalog_data(),
6416 node_size,
6417 &BTreeKeyFormat::CLASSIC_CATALOG,
6418 &HfsFilesystem::<Cursor<Vec<u8>>>::catalog_compare,
6419 );
6420 assert!(stale.is_empty(), "separators still stale: {stale:?}");
6421 let result = fs.fsck().unwrap();
6422 assert!(
6423 result.errors.is_empty(),
6424 "{:?}",
6425 result
6426 .errors
6427 .iter()
6428 .map(|e| &e.message)
6429 .take(4)
6430 .collect::<Vec<_>>()
6431 );
6432 }
6433
6434 #[test]
6435 fn test_hfs_control_char_filename_roundtrip() {
6436 let img = make_editable_hfs_image();
6437 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6438 let root = fs.root().unwrap();
6439 let options = CreateFileOptions::default();
6440 let name = "IPNetRouter\u{7f}\u{7f}_154_68k";
6441 let mut data = Cursor::new(b"x".to_vec());
6442 fs.create_file(&root, name, &mut data, 1, &options)
6443 .expect("create_file with raw 0x7F bytes in the name");
6444 let entries = fs.list_directory(&root).unwrap();
6445 assert!(
6446 entries.iter().any(|e| e.name == name),
6447 "0x7F-laden name not preserved; got {:?}",
6448 entries.iter().map(|e| &e.name).collect::<Vec<_>>()
6449 );
6450 assert_fsck_clean(&mut fs);
6451 }
6452
6453 #[test]
6458 fn test_hfs_null_byte_name_is_warning_not_error() {
6459 let img = make_editable_hfs_image();
6460 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6461 let root = fs.root().unwrap();
6462 let options = CreateFileOptions::default();
6463 let mut data = Cursor::new(b"x".to_vec());
6464 fs.create_file(&root, "\u{0}Sorts First", &mut data, 1, &options)
6465 .expect("create_file with a leading null byte in the name");
6466 let result = fs.fsck().unwrap();
6467 assert!(
6468 result.errors.is_empty(),
6469 "a null-byte name must not be a fsck error: {:?}",
6470 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6471 );
6472 assert!(
6473 result
6474 .warnings
6475 .iter()
6476 .any(|w| w.code == "UnusualCatalogName"),
6477 "expected an UnusualCatalogName warning for the null-byte name"
6478 );
6479 }
6480
6481 #[test]
6482 fn test_fsck_stress_many_files() {
6483 let img = make_editable_hfs_image();
6484 let cursor = Cursor::new(img);
6485 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6486
6487 let root = fs.root().unwrap();
6488 let options = CreateFileOptions::default();
6489
6490 for i in 0..35 {
6492 let name = format!("file_{:03}.txt", i);
6493 let data = format!("content of file {}", i);
6494 let mut reader = Cursor::new(data.as_bytes().to_vec());
6495 fs.create_file(&root, &name, &mut reader, data.len() as u64, &options)
6496 .unwrap();
6497
6498 assert_fsck_clean(&mut fs);
6500 }
6501
6502 let result = fs.fsck().unwrap();
6503 assert!(result.is_clean());
6504 assert_eq!(result.stats.files_checked, 35);
6505 assert_eq!(result.stats.directories_checked, 1); }
6507
6508 #[test]
6509 fn test_fsck_clean_after_blessed_folder() {
6510 let img = make_editable_hfs_image();
6511 let cursor = Cursor::new(img);
6512 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6513
6514 let root = fs.root().unwrap();
6515 let options = CreateDirectoryOptions::default();
6516 let sys_dir = fs.create_directory(&root, "System", &options).unwrap();
6517 fs.set_blessed_folder(&sys_dir).unwrap();
6518
6519 assert_fsck_clean(&mut fs);
6520 }
6521
6522 #[test]
6527 fn unerased_free_nodes_warn_once_and_repair_zeroes_them() {
6528 use crate::hfs_common::{btree_bitmap_test, BTreeHeader};
6530
6531 let mut fs = HfsFilesystem::open(Cursor::new(make_editable_hfs_image()), 0).unwrap();
6532 let node_size = BTreeHeader::read(&fs.catalog_data).node_size as usize;
6533 let free: Vec<usize> = (1..fs.catalog_data.len() / node_size)
6534 .filter(|&i| !btree_bitmap_test(&fs.catalog_data, node_size, i as u32))
6535 .take(3)
6536 .collect();
6537 assert_eq!(free.len(), 3, "blank catalog has free nodes");
6538 for &i in &free {
6539 fs.catalog_data[i * node_size + 20] = 0xA5;
6540 }
6541
6542 let result = fs.fsck().unwrap();
6543 assert!(
6544 result.errors.is_empty(),
6545 "not an error: {:?}",
6546 result.errors
6547 );
6548 let warns: Vec<_> = result
6549 .warnings
6550 .iter()
6551 .filter(|w| w.code == "FreeNodeNotErased")
6552 .collect();
6553 assert_eq!(warns.len(), 1, "one summary per tree, not one per node");
6554 assert!(
6555 warns[0].message.contains("3 unused node(s)"),
6556 "{}",
6557 warns[0].message
6558 );
6559 assert!(result.repairable);
6560
6561 let report = fs.repair().unwrap();
6562 assert!(
6563 report
6564 .fixes_applied
6565 .iter()
6566 .any(|f| f == "Erased 3 unused catalog B-tree node(s)"),
6567 "{:?}",
6568 report.fixes_applied
6569 );
6570 let after = fs.fsck().unwrap();
6571 assert!(after.warnings.iter().all(|w| w.code != "FreeNodeNotErased"));
6572 assert!(after.errors.is_empty(), "{:?}", after.errors);
6573 }
6574
6575 #[test]
6576 fn test_repair_index_records_are_even_aligned() {
6577 use crate::hfs_common::{btree_record_range, BTreeHeader};
6578
6579 let img = make_editable_hfs_image();
6580 let cursor = Cursor::new(img);
6581 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6582 let root = fs.root().unwrap();
6583 let options = CreateFileOptions::default();
6584
6585 for i in 0..40 {
6588 let name = if i % 2 == 0 {
6589 format!("f{:03}.txt", i) } else {
6591 format!("fi{:03}.txt", i) };
6593 let data = format!("data{}", i);
6594 let mut reader = Cursor::new(data.as_bytes().to_vec());
6595 fs.create_file(&root, &name, &mut reader, data.len() as u64, &options)
6596 .unwrap();
6597 }
6598
6599 let report = fs.repair().unwrap();
6601 assert!(
6602 report.fixes_failed.is_empty(),
6603 "repair failures: {:?}",
6604 report.fixes_failed
6605 );
6606
6607 let cat = &fs.catalog_data;
6609 let header = BTreeHeader::read(cat);
6610 let node_size = header.node_size as usize;
6611 let max_nodes = cat.len() / node_size;
6612
6613 let mut index_recs_checked = 0u32;
6614 for n in 1..max_nodes {
6615 let off = n * node_size;
6616 if off + node_size > cat.len() {
6617 break;
6618 }
6619 let kind = cat[off + 8] as i8;
6620 if kind != 0 {
6621 continue;
6622 }
6623 let num_recs = BigEndian::read_u16(&cat[off + 10..off + 12]) as usize;
6624 for r in 0..num_recs {
6625 let (rec_start, rec_end) =
6626 btree_record_range(&cat[off..off + node_size], node_size, r);
6627 if rec_start >= rec_end || rec_end > node_size {
6628 continue;
6629 }
6630 let key_len = cat[off + rec_start] as usize;
6631 let rec_len = rec_end - rec_start;
6632
6633 assert_eq!(
6636 key_len, 0x25,
6637 "Index node {} rec {}: key_len={} but expected 0x25",
6638 n, r, key_len
6639 );
6640 assert_eq!(
6641 rec_len, 42,
6642 "Index node {} rec {}: rec_len={} but expected 42",
6643 n, r, rec_len
6644 );
6645 index_recs_checked += 1;
6646 }
6647 }
6648 assert!(
6649 index_recs_checked > 0,
6650 "no index records found — test needs more files"
6651 );
6652
6653 let result = fs.fsck().unwrap();
6655 let error_msgs: Vec<&str> = result.errors.iter().map(|e| e.message.as_str()).collect();
6656 assert!(
6657 result.errors.is_empty(),
6658 "post-repair check found errors: {:?}",
6659 error_msgs
6660 );
6661 }
6662
6663 #[test]
6667 fn test_create_blank_hfs_roundtrip() {
6668 let cases: &[(u64, u32, &str)] = &[
6671 (1 * 1024 * 1024, 512, "1MB @ 512"),
6672 (8 * 1024 * 1024, 4096, "8MB @ 4K"),
6673 (64 * 1024 * 1024, 16384, "64MB @ 16K"),
6674 (256 * 1024 * 1024, 32768, "256MB @ 32K"),
6675 ];
6676
6677 for (total_size, block_size, label) in cases {
6678 let img = create_blank_hfs(*total_size, *block_size, "Fresh")
6679 .unwrap_or_else(|e| panic!("{label}: create_blank_hfs failed: {e}"));
6680
6681 let atrb = BigEndian::read_u16(&img[1024 + 10..1024 + 12]);
6685 assert_eq!(
6686 atrb & 0x0100,
6687 0x0100,
6688 "{label}: drAtrb missing kHFSVolumeUnmountedMask (got 0x{atrb:04x})"
6689 );
6690 let alt_off = img.len() - 1024;
6692 let alt_atrb = BigEndian::read_u16(&img[alt_off + 10..alt_off + 12]);
6693 assert_eq!(
6694 alt_atrb, atrb,
6695 "{label}: alt MDB drAtrb (0x{alt_atrb:04x}) does not mirror primary (0x{atrb:04x})"
6696 );
6697
6698 assert!(
6703 (img.len() as u64) <= *total_size,
6704 "{label}: image {} exceeds requested {}",
6705 img.len(),
6706 total_size
6707 );
6708
6709 let mut fs = HfsFilesystem::open(Cursor::new(img.clone()), 0)
6710 .unwrap_or_else(|e| panic!("{label}: open failed: {e}"));
6711 assert_eq!(fs.mdb().block_size, *block_size, "{label}: block_size");
6712 assert_eq!(fs.mdb().volume_name, "Fresh", "{label}: volume_name");
6713
6714 let root = fs.root().unwrap();
6715 let entries = fs.list_directory(&root).unwrap();
6716 assert!(
6717 entries.is_empty(),
6718 "{label}: root not empty on fresh volume"
6719 );
6720
6721 let result = fs.fsck().unwrap();
6722 assert!(
6723 result.errors.is_empty(),
6724 "{label}: fsck errors: {:?}",
6725 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6726 );
6727
6728 let mut backing = img;
6730 {
6731 let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
6732 let root = fs.root().unwrap();
6733 let payload = b"contents".to_vec();
6734 let mut src = payload.as_slice();
6735 fs.create_file(
6736 &root,
6737 "hello",
6738 &mut src,
6739 payload.len() as u64,
6740 &CreateFileOptions::default(),
6741 )
6742 .unwrap_or_else(|e| panic!("{label}: create_file failed: {e}"));
6743 fs.sync_metadata()
6744 .unwrap_or_else(|e| panic!("{label}: sync_metadata failed: {e}"));
6745 }
6746 let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
6747 let root = fs.root().unwrap();
6748 let entries = fs.list_directory(&root).unwrap();
6749 assert_eq!(entries.len(), 1, "{label}: expected 1 child after create");
6750 assert_eq!(entries[0].name, "hello", "{label}: child name");
6751 let data = fs.read_file(&entries[0], 64).unwrap();
6752 assert_eq!(data, b"contents", "{label}: file content round-trip");
6753
6754 let result = fs.fsck().unwrap();
6755 assert!(
6756 result.errors.is_empty(),
6757 "{label}: post-create fsck errors: {:?}",
6758 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6759 );
6760 }
6761 }
6762
6763 #[test]
6771 #[cfg(target_pointer_width = "64")]
6772 fn test_create_blank_hfs_near_2gb() {
6773 let total = 2u64 * 1024 * 1024 * 1024 - 1;
6774 let img = create_blank_hfs(total, 32768, "Near2GB")
6775 .expect("create_blank_hfs near 2GB should succeed");
6776 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6777 assert_eq!(fs.mdb().block_size, 32768);
6778 assert!(fs.mdb().total_blocks >= 65000);
6779 let result = fs.fsck().unwrap();
6780 assert!(
6781 result.errors.is_empty(),
6782 "fsck errors: {:?}",
6783 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6784 );
6785 }
6786
6787 #[test]
6791 fn catalog_grows_when_its_nodes_run_out() {
6792 let img = create_blank_hfs_sized(16 * 1024 * 1024, 4096, "Grow", 0, 16384).unwrap();
6793 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6794 let size0 = fs.mdb.catalog_file_size;
6795 assert_eq!(size0, 16384);
6796 let root = fs.root().unwrap();
6797 for i in 0..1000 {
6798 let name = format!("f{i:04}.txt");
6799 fs.create_file(
6800 &root,
6801 &name,
6802 &mut &b"x"[..],
6803 1,
6804 &CreateFileOptions::default(),
6805 )
6806 .unwrap_or_else(|e| panic!("creating {name}: {e}"));
6807 }
6808 assert!(fs.mdb.catalog_file_size > size0, "the catalog never grew");
6809 let inline: u32 = fs
6812 .mdb
6813 .catalog_file_extents
6814 .iter()
6815 .map(|e| e.block_count as u32)
6816 .sum();
6817 let spilled: u32 = collect_fork_overflow_extents(
6818 fs.extents_overflow_data
6819 .as_ref()
6820 .expect("overflow tree loaded"),
6821 CNID_CATALOG,
6822 0x00,
6823 inline,
6824 )
6825 .iter()
6826 .map(|e| e.block_count as u32)
6827 .sum();
6828 assert!(spilled > 0, "no CNID 4 overflow records");
6829 assert_eq!(
6830 (inline + spilled) * 4096,
6831 fs.mdb.catalog_file_size,
6832 "extents cover the file"
6833 );
6834 fs.sync_metadata().unwrap();
6835 let report = fs.fsck().unwrap();
6836 assert!(report.errors.is_empty(), "{:?}", report.errors);
6837
6838 let img = fs.reader.get_ref().clone();
6839 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6840 let root = fs.root().unwrap();
6841 let kids = fs.list_directory(&root).unwrap();
6842 assert_eq!(kids.len(), 1000);
6843 for e in &kids {
6844 fs.delete_entry(&root, e).unwrap();
6845 }
6846 fs.create_file(
6847 &root,
6848 "last.txt",
6849 &mut &b"y"[..],
6850 1,
6851 &CreateFileOptions::default(),
6852 )
6853 .unwrap();
6854 fs.sync_metadata().unwrap();
6855 let report = fs.fsck().unwrap();
6856 assert!(report.errors.is_empty(), "{:?}", report.errors);
6857 }
6858
6859 #[test]
6862 fn extents_tree_grows_when_its_nodes_run_out() {
6863 let img = create_blank_hfs_sized(4 * 1024 * 1024, 512, "Frag", 2048, 0).unwrap();
6864 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6865 let xt0 = fs.mdb.extents_file_size;
6866 assert_eq!(xt0, 2048);
6867 let root = fs.root().unwrap();
6868 let block = vec![0x5Au8; 512];
6869 for i in 0..400 {
6870 fs.create_file(
6871 &root,
6872 &format!("h{i:03}"),
6873 &mut Cursor::new(&block),
6874 512,
6875 &CreateFileOptions::default(),
6876 )
6877 .unwrap();
6878 }
6879 for e in fs.list_directory(&root).unwrap() {
6880 if e.name[1..].parse::<u32>().unwrap() % 2 == 1 {
6881 fs.delete_entry(&root, &e).unwrap();
6882 }
6883 }
6884 let tail = (fs.mdb.free_blocks - 264) as usize * 512;
6887 fs.create_file(
6888 &root,
6889 "tail",
6890 &mut Cursor::new(vec![0u8; tail]),
6891 tail as u64,
6892 &CreateFileOptions::default(),
6893 )
6894 .unwrap();
6895 let data: Vec<u8> = (0..200 * 512).map(|i| (i / 512) as u8).collect();
6896 let entry = fs
6897 .create_file(
6898 &root,
6899 "spread",
6900 &mut Cursor::new(&data),
6901 data.len() as u64,
6902 &CreateFileOptions::default(),
6903 )
6904 .unwrap();
6905 assert!(
6906 fs.mdb.extents_file_size > xt0,
6907 "the extents tree never grew"
6908 );
6909 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
6910 fs.sync_metadata().unwrap();
6911 let report = fs.fsck().unwrap();
6912 assert!(report.errors.is_empty(), "{:?}", report.errors);
6913
6914 let img = fs.reader.get_ref().clone();
6915 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6916 let root = fs.root().unwrap();
6917 let entry = fs
6918 .list_directory(&root)
6919 .unwrap()
6920 .into_iter()
6921 .find(|e| e.name == "spread")
6922 .unwrap();
6923 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
6924 fs.delete_entry(&root, &entry).unwrap();
6925 fs.sync_metadata().unwrap();
6926 let report = fs.fsck().unwrap();
6927 assert!(report.errors.is_empty(), "{:?}", report.errors);
6928 }
6929
6930 #[test]
6933 fn catalog_growth_adds_a_map_node_past_the_header_bitmap() {
6934 let img = create_blank_hfs_sized(24 * 1024 * 1024, 512, "Map", 0, 16384).unwrap();
6935 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6936 let root = fs.root().unwrap();
6937 for i in 0..9000 {
6938 let name = format!("f{i:04}");
6939 fs.create_file(
6940 &root,
6941 &name,
6942 &mut &b"x"[..],
6943 1,
6944 &CreateFileOptions::default(),
6945 )
6946 .unwrap_or_else(|e| panic!("creating {name}: {e}"));
6947 }
6948 let h = BTreeHeader::read(&fs.catalog_data);
6949 assert!(h.total_nodes > 2048, "{} nodes", h.total_nodes);
6950 assert!(
6951 hfs_common::btree_bitmap_segments(&fs.catalog_data, 512).len() > 1,
6952 "no map node"
6953 );
6954 fs.sync_metadata().unwrap();
6955 let report = fs.fsck().unwrap();
6956 assert!(report.errors.is_empty(), "{:?}", report.errors);
6957 let img = fs.reader.get_ref().clone();
6958 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6959 let root = fs.root().unwrap();
6960 assert_eq!(fs.list_directory(&root).unwrap().len(), 9000);
6961 }
6962
6963 #[test]
6966 fn thousand_file_churn_keeps_the_catalog_clean() {
6967 let img = create_blank_hfs(64 * 1024 * 1024, 4096, "Churn").unwrap();
6968 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6969 let root = fs.root().unwrap();
6970 let clean = |fs: &mut HfsFilesystem<Cursor<Vec<u8>>>, stage: &str| {
6971 fs.sync_metadata().unwrap();
6972 let r = fs.fsck().unwrap();
6973 assert!(r.errors.is_empty(), "{stage}: {:?}", r.errors);
6974 };
6975 let put = |fs: &mut HfsFilesystem<Cursor<Vec<u8>>>, name: &str, data: &[u8]| {
6976 fs.create_file(
6977 &root,
6978 name,
6979 &mut Cursor::new(data),
6980 data.len() as u64,
6981 &CreateFileOptions::default(),
6982 )
6983 .unwrap_or_else(|e| panic!("creating {name}: {e}"))
6984 };
6985 let free0 = fs.mdb.free_blocks;
6986 for i in 0..1000 {
6987 put(
6988 &mut fs,
6989 &format!("f{i:04}.txt"),
6990 format!("file {i:04}").as_bytes(),
6991 );
6992 }
6993 clean(&mut fs, "after 1000 creates");
6994 let extra = put(&mut fs, "extra.bin", &[7u8; 5000]);
6995 clean(&mut fs, "after one more");
6996 fs.delete_entry(&root, &extra).unwrap();
6997 clean(&mut fs, "after deleting it");
6998 for e in fs.list_directory(&root).unwrap() {
6999 fs.delete_entry(&root, &e).unwrap();
7000 }
7001 assert_eq!(fs.mdb.free_blocks, free0, "every block came back");
7002 clean(&mut fs, "after deleting the 1000");
7003 put(&mut fs, "last.bin", &[9u8; 100]);
7004 clean(&mut fs, "after the last create");
7005 let names: Vec<String> = fs
7006 .list_directory(&root)
7007 .unwrap()
7008 .into_iter()
7009 .map(|e| e.name)
7010 .collect();
7011 assert_eq!(names, ["last.bin"]);
7012 }
7013
7014 #[test]
7017 fn fsck_reports_a_volume_that_does_not_fill_its_partition() {
7018 let img = create_blank_hfs(4 * 1024 * 1024, 2048, "Short").unwrap();
7019 let exact = img.len() as u64;
7020 let mut fs = HfsFilesystem::open_sized(Cursor::new(img.clone()), 0, Some(exact)).unwrap();
7021 assert!(fs.partition_shortfall().is_none());
7022 let report = fs.fsck().unwrap();
7023 assert!(report.errors.is_empty(), "{:?}", report.errors);
7024
7025 let mut fs =
7028 HfsFilesystem::open_sized(Cursor::new(img.clone()), 0, Some(exact + 512)).unwrap();
7029 assert_eq!(fs.partition_shortfall(), Some(512));
7030 let report = fs.fsck().unwrap();
7031 assert!(report.errors.is_empty(), "{:?}", report.errors);
7032
7033 let mut fs =
7034 HfsFilesystem::open_sized(Cursor::new(img), 0, Some(exact + 1024 * 1024)).unwrap();
7035 assert_eq!(fs.partition_shortfall(), Some(1024 * 1024));
7036 let report = fs.fsck().unwrap();
7037 let codes: Vec<&str> = report.errors.iter().map(|e| e.code.as_str()).collect();
7038 assert_eq!(codes, ["AllocationAreaEnd"], "{:?}", report.errors);
7039 assert!(report.errors[0].message.contains("resize --size"));
7040 }
7041
7042 #[test]
7045 fn create_file_spreads_a_fork_over_free_runs() {
7046 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "Runs").unwrap();
7047 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7048 let root = fs.root().unwrap();
7049 let block = vec![0xA5u8; 4096];
7050 for i in 0..40 {
7051 fs.create_file(
7052 &root,
7053 &format!("f{i:02}"),
7054 &mut Cursor::new(block.as_slice()),
7055 4096,
7056 &CreateFileOptions::default(),
7057 )
7058 .unwrap();
7059 }
7060 for e in fs.list_directory(&root).unwrap() {
7062 if e.name[1..].parse::<u32>().unwrap() % 2 == 1 {
7063 fs.delete_entry(&root, &e).unwrap();
7064 }
7065 }
7066 let tail = (fs.mdb.free_blocks - 20) as usize;
7068 let tail_data = vec![0x5Au8; tail * 4096];
7069 fs.create_file(
7070 &root,
7071 "tail",
7072 &mut Cursor::new(tail_data.as_slice()),
7073 tail_data.len() as u64,
7074 &CreateFileOptions::default(),
7075 )
7076 .unwrap();
7077 assert_eq!(fs.mdb.free_blocks, 20);
7078 let free_before = fs.mdb.free_blocks;
7079
7080 let data: Vec<u8> = (0..7 * 4096).map(|i| (i / 4096) as u8 + 1).collect();
7082 let rsrc: Vec<u8> = (0..5 * 4096).map(|i| 0x80 | (i / 4096) as u8).collect();
7083 let opts = CreateFileOptions {
7084 resource_fork: Some(disk_core::filesystem::ResourceForkSource::Data(
7085 rsrc.clone(),
7086 )),
7087 ..Default::default()
7088 };
7089 let entry = fs
7090 .create_file(
7091 &root,
7092 "frag",
7093 &mut Cursor::new(data.as_slice()),
7094 data.len() as u64,
7095 &opts,
7096 )
7097 .unwrap();
7098 let cnid = entry.location as u32;
7099 assert_eq!(fs.mdb.free_blocks, free_before - 12);
7100 let (dsize, dext, rsize, rext) = fs.find_file_by_id(cnid).unwrap();
7101 assert_eq!((dsize as usize, rsize as usize), (data.len(), rsrc.len()));
7102 assert!(dext.iter().all(|e| e.block_count == 1) && rext.iter().all(|e| e.block_count == 1));
7103 let tree = fs
7104 .extents_overflow_data
7105 .as_ref()
7106 .expect("overflow tree loaded");
7107 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).len(), 4);
7108 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0xFF, 0).len(), 2);
7109 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
7110 let mut got = Vec::new();
7111 fs.write_resource_fork_to(&entry, &mut got).unwrap();
7112 assert_eq!(got, rsrc);
7113
7114 let rsrc2: Vec<u8> = (0..4 * 4096).map(|i| 0xC0 | (i / 4096) as u8).collect();
7116 fs.write_resource_fork(
7117 &entry,
7118 &mut Cursor::new(rsrc2.as_slice()),
7119 rsrc2.len() as u64,
7120 )
7121 .unwrap();
7122 assert_eq!(fs.mdb.free_blocks, free_before - 11);
7123 let tree = fs.extents_overflow_data.as_ref().unwrap();
7124 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0xFF, 0).len(), 1);
7125 let mut got = Vec::new();
7126 fs.write_resource_fork_to(&entry, &mut got).unwrap();
7127 assert_eq!(got, rsrc2);
7128
7129 fs.sync_metadata().unwrap();
7130 let report = fs.fsck().unwrap();
7131 assert!(report.errors.is_empty(), "{:?}", report.errors);
7132
7133 let img = fs.reader.get_ref().clone();
7135 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7136 let root = fs.root().unwrap();
7137 let entry = fs
7138 .list_directory(&root)
7139 .unwrap()
7140 .into_iter()
7141 .find(|e| e.name == "frag")
7142 .unwrap();
7143 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
7144 fs.delete_entry(&root, &entry).unwrap();
7145 assert_eq!(fs.mdb.free_blocks, free_before);
7146 let tree = fs.extents_overflow_data.as_ref().unwrap();
7147 assert!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).is_empty());
7148 fs.sync_metadata().unwrap();
7149 let report = fs.fsck().unwrap();
7150 assert!(report.errors.is_empty(), "{:?}", report.errors);
7151 }
7152
7153 #[test]
7156 fn delete_frees_overflow_extents_and_their_records() {
7157 use crate::hfs_common::{
7158 bitmap_test_bit_be, btree_alloc_node, BTreeHeader, BTREE_LEAF_NODE,
7159 };
7160 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "Overflow").unwrap();
7162 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7163 assert!(fs.mdb.extents_file_size > 0);
7164 let root = fs.root().unwrap();
7165 let free_at_start = fs.mdb.free_blocks;
7166 let data = b"inline extent contents";
7167 fs.create_file(
7168 &root,
7169 "frag.bin",
7170 &mut Cursor::new(data.as_slice()),
7171 data.len() as u64,
7172 &CreateFileOptions::default(),
7173 )
7174 .unwrap();
7175 let entry = fs
7176 .list_directory(&root)
7177 .unwrap()
7178 .into_iter()
7179 .find(|e| e.name == "frag.bin")
7180 .unwrap();
7181 let cnid = entry.location as u32;
7182
7183 let start = fs.allocate_blocks(2).unwrap();
7186 fs.ensure_extents_overflow().unwrap();
7187 {
7188 let tree = fs.extents_overflow_data.as_mut().expect("extents tree");
7189 let mut h = BTreeHeader::read(tree);
7190 let node_size = h.node_size as usize;
7191 let leaf = btree_alloc_node(tree, node_size, h.total_nodes).unwrap();
7192 h.root_node = leaf;
7193 h.first_leaf_node = leaf;
7194 h.last_leaf_node = leaf;
7195 h.depth = 1;
7196 h.leaf_records = 1;
7197 h.free_nodes -= 1;
7198 h.write(tree);
7199 let off = leaf as usize * node_size;
7200 let node = &mut tree[off..off + node_size];
7201 node[8] = BTREE_LEAF_NODE as u8;
7202 node[9] = 1;
7203 BigEndian::write_u16(&mut node[10..12], 1);
7204 node[14] = 7;
7206 node[15] = 0x00;
7207 BigEndian::write_u32(&mut node[16..20], cnid);
7208 BigEndian::write_u16(&mut node[20..22], 1);
7209 BigEndian::write_u16(&mut node[22..24], start as u16);
7210 BigEndian::write_u16(&mut node[24..26], 2);
7211 BigEndian::write_u16(&mut node[node_size - 2..], 14);
7212 BigEndian::write_u16(&mut node[node_size - 4..node_size - 2], 34);
7213 }
7214 let tree = fs.extents_overflow_data.as_ref().unwrap();
7215 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).len(), 1);
7216
7217 fs.delete_entry(&root, &entry).unwrap();
7218
7219 let tree = fs.extents_overflow_data.as_ref().unwrap();
7220 assert!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).is_empty());
7221 let h = BTreeHeader::read(tree);
7222 assert_eq!((h.leaf_records, h.root_node, h.depth), (0, 0, 0));
7223 assert!(
7225 crate::hfs_common::btree_lonely_empty_leaves(tree, h.node_size as usize).is_empty()
7226 );
7227 let bitmap = fs.bitmap.as_ref().unwrap();
7228 assert!(!bitmap_test_bit_be(bitmap, start) && !bitmap_test_bit_be(bitmap, start + 1));
7229 assert_eq!(fs.mdb.free_blocks, free_at_start, "every block came back");
7230 fs.sync_metadata().unwrap();
7231 let report = fs.fsck().unwrap();
7232 assert!(report.errors.is_empty(), "{:?}", report.errors);
7233 }
7234
7235 #[test]
7239 fn test_collect_fork_overflow_extents() {
7240 let node_size = 512usize;
7241 let total_nodes = 3u32;
7243 let mut buf = vec![0u8; node_size * total_nodes as usize];
7244
7245 buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
7247 BigEndian::write_u16(&mut buf[10..12], 3); BigEndian::write_u16(&mut buf[14..16], 1); BigEndian::write_u32(&mut buf[16..20], 1); BigEndian::write_u32(&mut buf[20..24], 1); BigEndian::write_u32(&mut buf[24..28], 1); BigEndian::write_u32(&mut buf[28..32], 1); BigEndian::write_u16(&mut buf[32..34], node_size as u16);
7254 BigEndian::write_u16(&mut buf[34..36], 7);
7255 BigEndian::write_u32(&mut buf[36..40], total_nodes);
7256 BigEndian::write_u32(&mut buf[40..44], total_nodes - 2);
7257 buf[248] = 0b11000000;
7259
7260 let leaf_off = node_size;
7262 let leaf = &mut buf[leaf_off..leaf_off + node_size];
7263 leaf[8] = hfs_common::BTREE_LEAF_NODE as u8;
7264 BigEndian::write_u16(&mut leaf[10..12], 1); let rec_off = 14usize;
7268 leaf[rec_off] = 7;
7269 leaf[rec_off + 1] = 0x00;
7270 BigEndian::write_u32(&mut leaf[rec_off + 2..rec_off + 6], 0x100);
7271 BigEndian::write_u16(&mut leaf[rec_off + 6..rec_off + 8], 3);
7272 let data_off = rec_off + 8;
7274 BigEndian::write_u16(&mut leaf[data_off..data_off + 2], 50); BigEndian::write_u16(&mut leaf[data_off + 2..data_off + 4], 2); BigEndian::write_u16(&mut leaf[data_off + 4..data_off + 6], 60);
7277 BigEndian::write_u16(&mut leaf[data_off + 6..data_off + 8], 1);
7278 BigEndian::write_u16(&mut leaf[data_off + 8..data_off + 10], 0); BigEndian::write_u16(&mut leaf[data_off + 10..data_off + 12], 0);
7280
7281 let nr = 1usize;
7283 BigEndian::write_u16(&mut leaf[node_size - 2..], 14);
7284 BigEndian::write_u16(&mut leaf[node_size - 4..node_size - 2], 34);
7285 let _ = nr;
7286
7287 let got = collect_fork_overflow_extents(&buf, 0x100, 0x00, 3);
7289 assert_eq!(got.len(), 2, "expected 2 non-empty extents, got {got:?}");
7290 assert_eq!((got[0].start_block, got[0].block_count), (50, 2));
7291 assert_eq!((got[1].start_block, got[1].block_count), (60, 1));
7292
7293 let none = collect_fork_overflow_extents(&buf, 0x100, 0x00, 4);
7295 assert!(
7296 none.is_empty(),
7297 "expected empty when filtered, got {none:?}"
7298 );
7299
7300 let rsrc = collect_fork_overflow_extents(&buf, 0x100, 0xFF, 0);
7302 assert!(rsrc.is_empty());
7303
7304 let other = collect_fork_overflow_extents(&buf, 0x101, 0x00, 0);
7306 assert!(other.is_empty());
7307 }
7308
7309 #[test]
7313 fn test_create_blank_hfs_sized_custom_btree_sizes() {
7314 let block_size = 32 * 1024u32;
7315 let img =
7317 create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Sized", 64 * 1024, 256 * 1024)
7318 .expect("create_blank_hfs_sized");
7319 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open");
7320 assert_eq!(fs.mdb().extents_file_size, 4 * block_size);
7322 assert_eq!(fs.mdb().catalog_file_size, 8 * block_size);
7324 let result = fs.fsck().expect("fsck");
7325 assert!(
7326 result.errors.is_empty(),
7327 "unexpected fsck errors: {:?}",
7328 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7329 );
7330 }
7331
7332 #[test]
7337 fn test_pick_btree_node_size_thresholds() {
7338 assert_eq!(pick_btree_node_size(512 * 2048), 512);
7339 assert_eq!(pick_btree_node_size(512 * 2049), 512);
7340 assert_eq!(pick_btree_node_size(1024 * 6144), 512);
7341 assert_eq!(pick_btree_node_size(u32::MAX as u64), 512);
7342 }
7343
7344 #[test]
7349 fn test_create_blank_hfs_sized_large_catalog() {
7350 let block_size = 32 * 1024u32;
7351 let img = create_blank_hfs_sized(
7352 128 * 1024 * 1024,
7353 block_size,
7354 "Big",
7355 0,
7356 HFS_MAX_BTREE_FILE_SIZE,
7357 )
7358 .expect("create_blank_hfs_sized big catalog");
7359 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open big");
7360 let cat = fs.catalog_data();
7361 let node_size = BigEndian::read_u16(&cat[32..34]);
7362 assert_eq!(
7363 node_size, 512,
7364 "node_size must stay at 512 for Mac OS compat"
7365 );
7366 let result = fs.fsck().expect("fsck big");
7367 assert!(
7368 result.errors.is_empty(),
7369 "unexpected fsck errors: {:?}",
7370 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7371 );
7372 }
7373
7374 #[test]
7378 fn test_create_blank_hfs_with_map_nodes() {
7379 let img =
7382 create_blank_hfs_sized(64 * 1024 * 1024, 32 * 1024, "MapNodes", 0, 2 * 1024 * 1024)
7383 .expect("create_blank_hfs_sized 2 MiB cat");
7384 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open");
7385 let cat = fs.catalog_data().to_vec();
7386
7387 let header_flink = BigEndian::read_u32(&cat[0..4]);
7389 assert_eq!(header_flink, 2, "header.fLink should point to map node 2");
7390
7391 let map = &cat[1024..1536];
7394 assert_eq!(map[8] as i8, 2, "node 2 should be a map node");
7395 assert_eq!(BigEndian::read_u16(&map[10..12]), 1);
7396 assert_eq!(
7397 BigEndian::read_u32(&map[0..4]),
7398 0,
7399 "single map node, fLink=0"
7400 );
7401
7402 let header = hfs_common::BTreeHeader::read(&cat);
7404 assert_eq!(header.total_nodes, 4096);
7405 assert_eq!(header.free_nodes, 4093);
7406
7407 let segs = hfs_common::btree_bitmap_segments(&cat, 512);
7410 assert_eq!(segs.len(), 2);
7411 assert_eq!(segs[0].base_bit, 0);
7412 assert_eq!(segs[1].base_bit, 2048);
7413 assert!(
7414 (segs[0].len + segs[1].len) * 8 >= 4096,
7415 "bitmap chain must address all 4096 nodes"
7416 );
7417
7418 let result = fs.fsck().expect("fsck");
7420 assert!(
7421 result.errors.is_empty(),
7422 "unexpected fsck errors: {:?}",
7423 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7424 );
7425 }
7426
7427 #[test]
7429 fn test_create_blank_hfs_rejects_bad_block_size() {
7430 assert!(create_blank_hfs(1 << 20, 0, "X").is_err());
7431 assert!(create_blank_hfs(1 << 20, 500, "X").is_err());
7432 assert!(create_blank_hfs(1 << 20, 513, "X").is_err());
7433 }
7434
7435 fn make_fs_with_file() -> (HfsFilesystem<Cursor<Vec<u8>>>, FileEntry) {
7439 let img = make_editable_hfs_image();
7440 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7441 let root = fs.root().unwrap();
7442 let data = b"hello";
7443 let mut r = Cursor::new(data.as_slice());
7444 let fe = fs
7445 .create_file(
7446 &root,
7447 "thing.bin",
7448 &mut r,
7449 data.len() as u64,
7450 &CreateFileOptions::default(),
7451 )
7452 .unwrap();
7453 (fs, fe)
7454 }
7455
7456 #[test]
7457 fn test_hfs_set_finder_info_round_trip() {
7458 let (mut fs, fe) = make_fs_with_file();
7459 let mut finfo = [0u8; 16];
7460 finfo[0..4].copy_from_slice(b"APPL");
7461 finfo[4..8].copy_from_slice(b"MACS");
7462 BigEndian::write_u16(&mut finfo[8..10], 0x4000); BigEndian::write_i16(&mut finfo[10..12], 100); BigEndian::write_i16(&mut finfo[12..14], 200); BigEndian::write_u16(&mut finfo[14..16], 0x1234); let mut fxinfo = [0u8; 16];
7467 BigEndian::write_u16(&mut fxinfo[0..2], 0xABCD); fxinfo[8] = 0x55; BigEndian::write_u32(&mut fxinfo[12..16], 0xDEADBEEF); fs.set_finder_info(&fe, finfo, fxinfo).unwrap();
7472
7473 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7474 assert_eq!(&fs.catalog_data[rec + 4..rec + 20], &finfo);
7475 assert_eq!(&fs.catalog_data[rec + 56..rec + 72], &fxinfo);
7476
7477 let root = fs.root().unwrap();
7480 let listed = fs.list_directory(&root).unwrap();
7481 let entry = listed.iter().find(|e| e.name == "thing.bin").unwrap();
7482 assert_eq!(entry.finder_flags, Some(0x4000));
7483 }
7484
7485 #[test]
7489 fn test_hfs_read_surfaces_has_bundle_flag() {
7490 let (mut fs, fe) = make_fs_with_file();
7491 let mut finfo = [0u8; 16];
7492 finfo[0..4].copy_from_slice(b"APPL");
7493 finfo[4..8].copy_from_slice(b"Po.P");
7494 BigEndian::write_u16(&mut finfo[8..10], 0x2000); fs.set_finder_info(&fe, finfo, [0u8; 16]).unwrap();
7496
7497 let root = fs.root().unwrap();
7498 let listed = fs.list_directory(&root).unwrap();
7499 let entry = listed.iter().find(|e| e.name == "thing.bin").unwrap();
7500 assert_eq!(entry.finder_flags, Some(0x2000));
7501 }
7502
7503 #[test]
7504 fn test_hfs_set_finder_info_rejects_directory() {
7505 let img = make_editable_hfs_image();
7506 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7507 let root = fs.root().unwrap();
7508 let dir = fs
7509 .create_directory(&root, "Dir", &CreateDirectoryOptions::default())
7510 .unwrap();
7511 let err = fs.set_finder_info(&dir, [0u8; 16], [0u8; 16]).unwrap_err();
7512 assert!(matches!(err, FilesystemError::InvalidData(_)));
7513 }
7514
7515 #[test]
7516 fn test_hfs_set_directory_finder_info_round_trip() {
7517 let img = make_editable_hfs_image();
7518 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7519 let root = fs.root().unwrap();
7520 let dir = fs
7521 .create_directory(&root, "Apps", &CreateDirectoryOptions::default())
7522 .unwrap();
7523
7524 let mut dinfo = [0u8; 16];
7525 BigEndian::write_i16(&mut dinfo[0..2], 10); BigEndian::write_i16(&mut dinfo[2..4], 20); BigEndian::write_i16(&mut dinfo[4..6], 300); BigEndian::write_i16(&mut dinfo[6..8], 400); BigEndian::write_u16(&mut dinfo[8..10], 0x0100); let mut dxinfo = [0u8; 16];
7531 BigEndian::write_i16(&mut dxinfo[0..2], -5); BigEndian::write_i16(&mut dxinfo[2..4], -7); BigEndian::write_u32(&mut dxinfo[4..8], 0xCAFEBABE); fs.set_directory_finder_info(&dir, dinfo, dxinfo).unwrap();
7536
7537 let rec = fs.locate_record_data(dir.location as u32).unwrap();
7538 assert_eq!(&fs.catalog_data[rec + 22..rec + 38], &dinfo);
7539 assert_eq!(&fs.catalog_data[rec + 38..rec + 54], &dxinfo);
7540 }
7541
7542 #[test]
7543 fn test_hfs_set_directory_finder_info_rejects_file() {
7544 let (mut fs, fe) = make_fs_with_file();
7545 let err = fs
7546 .set_directory_finder_info(&fe, [0u8; 16], [0u8; 16])
7547 .unwrap_err();
7548 assert!(matches!(err, FilesystemError::InvalidData(_)));
7549 }
7550
7551 #[test]
7552 fn test_hfs_set_dates_file() {
7553 let (mut fs, fe) = make_fs_with_file();
7554 let create = 0x1000_0001u32;
7555 let modify = 0x2000_0002u32;
7556 let backup = 0x3000_0003u32;
7557 fs.set_dates(&fe, create, modify, backup).unwrap();
7558
7559 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7560 assert_eq!(
7561 BigEndian::read_u32(&fs.catalog_data[rec + 44..rec + 48]),
7562 create
7563 );
7564 assert_eq!(
7565 BigEndian::read_u32(&fs.catalog_data[rec + 48..rec + 52]),
7566 modify
7567 );
7568 assert_eq!(
7569 BigEndian::read_u32(&fs.catalog_data[rec + 52..rec + 56]),
7570 backup
7571 );
7572 }
7573
7574 #[test]
7575 fn test_hfs_set_dates_directory() {
7576 let img = make_editable_hfs_image();
7577 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7578 let root = fs.root().unwrap();
7579 let dir = fs
7580 .create_directory(&root, "D", &CreateDirectoryOptions::default())
7581 .unwrap();
7582
7583 let create = 0xAAAAu32;
7584 let modify = 0xBBBBu32;
7585 let backup = 0xCCCCu32;
7586 fs.set_dates(&dir, create, modify, backup).unwrap();
7587
7588 let rec = fs.locate_record_data(dir.location as u32).unwrap();
7589 assert_eq!(
7590 BigEndian::read_u32(&fs.catalog_data[rec + 10..rec + 14]),
7591 create
7592 );
7593 assert_eq!(
7594 BigEndian::read_u32(&fs.catalog_data[rec + 14..rec + 18]),
7595 modify
7596 );
7597 assert_eq!(
7598 BigEndian::read_u32(&fs.catalog_data[rec + 18..rec + 22]),
7599 backup
7600 );
7601 }
7602
7603 #[test]
7604 fn test_hfs_set_file_locked_toggle() {
7605 let (mut fs, fe) = make_fs_with_file();
7606 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7607 let initial = fs.catalog_data[rec + 2];
7608 assert_eq!(initial & 0x01, 0);
7609
7610 fs.set_file_locked(&fe, true).unwrap();
7611 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7612 assert_eq!(fs.catalog_data[rec + 2] & 0x01, 0x01);
7613
7614 fs.set_file_locked(&fe, false).unwrap();
7615 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7616 assert_eq!(fs.catalog_data[rec + 2] & 0x01, 0);
7617 }
7618
7619 #[test]
7620 fn test_hfs_set_file_locked_rejects_directory() {
7621 let img = make_editable_hfs_image();
7622 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7623 let root = fs.root().unwrap();
7624 let dir = fs
7625 .create_directory(&root, "Dir", &CreateDirectoryOptions::default())
7626 .unwrap();
7627 let err = fs.set_file_locked(&dir, true).unwrap_err();
7628 assert!(matches!(err, FilesystemError::InvalidData(_)));
7629 }
7630
7631 #[test]
7632 fn test_hfs_set_volume_dates_persists_through_sync() {
7633 let img = make_editable_hfs_image();
7634 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7635
7636 let create = 0x4242_4242u32;
7637 let modify = 0x5151_5151u32;
7638 let backup = 0x6060_6060u32;
7639 fs.set_volume_dates(create, modify, backup);
7640 fs.sync_metadata().unwrap();
7641
7642 assert_eq!(fs.mdb.create_date, create);
7643 assert_eq!(fs.mdb.modify_date, modify);
7644 assert_eq!(BigEndian::read_u32(&fs.mdb.raw_sector[64..68]), backup);
7645
7646 let bytes = fs.reader.get_ref().clone();
7648 let mut fs2 = HfsFilesystem::open(Cursor::new(bytes), 0).unwrap();
7649 assert_eq!(fs2.mdb.create_date, create);
7650 assert_eq!(fs2.mdb.modify_date, modify);
7651 assert_eq!(BigEndian::read_u32(&fs2.mdb.raw_sector[64..68]), backup);
7652
7653 fs2.sync_metadata().unwrap();
7655 assert_ne!(fs2.mdb.modify_date, modify);
7656 }
7657
7658 #[test]
7659 fn test_hfs_set_boot_blocks_written_on_sync() {
7660 let img = make_editable_hfs_image();
7661 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7662 let mut bb = [0u8; 1024];
7663 bb[0] = 0x4C; bb[1] = 0x4B;
7665 for (i, slot) in bb.iter_mut().enumerate().skip(2).take(200) {
7666 *slot = (i & 0xFF) as u8;
7667 }
7668 fs.set_boot_blocks(&bb);
7669 fs.sync_metadata().unwrap();
7670
7671 let read_back = fs.read_boot_blocks().unwrap();
7672 assert_eq!(&read_back[..], &bb[..]);
7673 }
7674
7675 #[test]
7676 fn test_hfs_set_volume_finder_info_round_trip() {
7677 let img = make_editable_hfs_image();
7678 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7679
7680 let mut fi = [0u8; 32];
7681 BigEndian::write_u32(&mut fi[0..4], 100); BigEndian::write_u32(&mut fi[12..16], 200); BigEndian::write_u32(&mut fi[20..24], 300); fs.set_volume_finder_info(&fi);
7685 assert_eq!(fs.mdb.finder_info[0], 100);
7686 assert_eq!(fs.mdb.finder_info[3], 200);
7687 assert_eq!(fs.mdb.finder_info[5], 300);
7688
7689 fs.sync_metadata().unwrap();
7690 let bytes = fs.reader.get_ref().clone();
7691 let fs2 = HfsFilesystem::open(Cursor::new(bytes), 0).unwrap();
7692 assert_eq!(fs2.mdb.finder_info[0], 100);
7693 assert_eq!(fs2.mdb.finder_info[3], 200);
7694 assert_eq!(fs2.mdb.finder_info[5], 300);
7695 }
7696
7697 #[test]
7698 fn test_default_btree_sizes_scale_and_floor() {
7699 let (cat, ext) = default_btree_sizes(1 * 1024 * 1024, 512);
7701 let floor = 24 * 512;
7702 assert_eq!(cat, floor, "small-volume catalog should hit floor");
7703 assert_eq!(ext, floor, "small-volume extents should hit floor");
7704
7705 let (cat, ext) = default_btree_sizes(20 * 1024 * 1024, 512);
7709 assert!(cat >= 100 * 1024, "20MB catalog too small: {cat}");
7710 assert_eq!(cat % 2048, 0, "catalog not clump-aligned");
7711 assert_eq!(ext % 2048, 0, "extents not clump-aligned");
7712 assert!(ext <= cat, "extents should not exceed catalog");
7713 assert!(ext * 2 >= cat - 2048, "extents should be ~half catalog");
7714
7715 let (cat_big, _) = default_btree_sizes(200 * 1024 * 1024, 4096);
7717 assert!(cat_big > cat, "larger volume should get a larger catalog");
7718
7719 let (c, e) = default_btree_sizes(20 * 1024 * 1024, 4096);
7722 let img = create_blank_hfs_sized(20 * 1024 * 1024, 4096, "Ref20", e, c).unwrap();
7723 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7724 let result = fs.fsck().unwrap();
7725 assert!(
7726 result.errors.is_empty(),
7727 "fsck errors on default-sized volume: {:?}",
7728 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7729 );
7730 }
7731
7732 #[test]
7733 fn test_clone_target_btree_sizes_floors_at_blank_default() {
7734 let target = 2046 * 1024 * 1024;
7739 let (default_cat, _) = default_btree_sizes(target, 32768);
7740 let (cat, _ext) = clone_target_btree_sizes(target, 32768, 2 * 1024 * 1024, 1024 * 1024);
7741 assert_eq!(
7742 cat, default_cat,
7743 "large target must floor catalog at the blank default, not 1.5x source"
7744 );
7745 assert!(
7746 cat > 2 * 1024 * 1024 * 3 / 2,
7747 "floor must exceed 1.5x source"
7748 );
7749
7750 let small_target = 40 * 1024 * 1024;
7753 let (cat2, _) = clone_target_btree_sizes(small_target, 4096, 4 * 1024 * 1024, 256 * 1024);
7754 assert_eq!(
7755 cat2,
7756 4 * 1024 * 1024 * 3 / 2,
7757 "source-derived value should win"
7758 );
7759
7760 let (cat3, ext3) = clone_target_btree_sizes(target, 32768, u32::MAX, u32::MAX);
7762 assert!(cat3 <= HFS_MAX_BTREE_FILE_SIZE);
7763 assert!(ext3 <= HFS_MAX_BTREE_FILE_SIZE);
7764 }
7765
7766 #[test]
7767 fn test_hfs_set_volume_name_roundtrip() {
7768 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "OldName").unwrap();
7771 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7772
7773 fs.set_volume_name("NewName").unwrap();
7774 assert_eq!(fs.mdb.volume_name, "NewName");
7775 fs.sync_metadata().unwrap();
7776
7777 let bytes = fs.reader.get_ref().clone();
7778 let mut fs2 = HfsFilesystem::open(Cursor::new(bytes.clone()), 0).unwrap();
7779 assert_eq!(fs2.mdb.volume_name, "NewName");
7780
7781 let new_raw = b"NewName".to_vec();
7784 assert!(
7785 fs2.find_catalog_record_by_name(1, &new_raw).is_some(),
7786 "root dir record not re-keyed to new name"
7787 );
7788 assert!(
7789 fs2.find_catalog_record_by_name(1, b"OldName").is_none(),
7790 "old root dir key still present"
7791 );
7792
7793 let (_n, _r, key_abs) = fs2.find_catalog_record_by_cnid(2).unwrap();
7795 let key_len = fs2.catalog_data[key_abs] as usize;
7796 let mut data_off = key_abs + 1 + key_len;
7797 if !data_off.is_multiple_of(2) {
7798 data_off += 1;
7799 }
7800 let thd_len = fs2.catalog_data[data_off + 14] as usize;
7801 assert_eq!(thd_len, new_raw.len(), "thdCName length not updated");
7802 assert_eq!(
7803 &fs2.catalog_data[data_off + 15..data_off + 15 + thd_len],
7804 new_raw.as_slice(),
7805 "thdCName bytes not updated"
7806 );
7807
7808 let mdb_off = 1024 + 36;
7810 assert_eq!(bytes[mdb_off] as usize, new_raw.len());
7811 assert_eq!(
7812 &bytes[mdb_off + 1..mdb_off + 1 + new_raw.len()],
7813 new_raw.as_slice()
7814 );
7815
7816 let result = fs2.fsck().unwrap();
7817 assert!(
7818 result.errors.is_empty(),
7819 "fsck errors after rename: {:?}",
7820 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7821 );
7822 }
7823
7824 #[test]
7825 fn test_drallocptr_advances_with_allocations() {
7826 let img = create_blank_hfs(4 * 1024 * 1024, 512, "AllocPtr").unwrap();
7830 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7831 assert_eq!(
7832 BigEndian::read_u16(&fs.mdb.raw_sector[16..18]),
7833 0,
7834 "fresh volume drAllocPtr should be 0"
7835 );
7836
7837 let root = fs.root().unwrap();
7838 let payload = vec![0xABu8; 8 * 1024]; let mut src = payload.as_slice();
7840 fs.create_file(
7841 &root,
7842 "ptr",
7843 &mut src,
7844 payload.len() as u64,
7845 &CreateFileOptions::default(),
7846 )
7847 .unwrap();
7848 let after = BigEndian::read_u16(&fs.mdb.raw_sector[16..18]);
7849 assert!(after > 0, "drAllocPtr should advance after allocation");
7850 }
7851
7852 #[test]
7853 fn test_hfs_set_volume_name_rejects_invalid() {
7854 let img = create_blank_hfs(1 * 1024 * 1024, 512, "X").unwrap();
7855 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7856 assert!(
7857 fs.set_volume_name("").is_err(),
7858 "empty name must be rejected"
7859 );
7860 let too_long = "A".repeat(28);
7861 assert!(
7862 fs.set_volume_name(&too_long).is_err(),
7863 ">27 byte name must be rejected"
7864 );
7865 assert_eq!(fs.mdb.volume_name, "X");
7867 }
7868
7869 #[test]
7870 fn test_hfs_finder_info_setters_survive_fsck() {
7871 let (mut fs, fe) = make_fs_with_file();
7873 fs.set_finder_info(&fe, [0x11; 16], [0x22; 16]).unwrap();
7874 fs.set_dates(&fe, 1, 2, 3).unwrap();
7875 fs.set_file_locked(&fe, true).unwrap();
7876 fs.set_volume_dates(10, 20, 30);
7877 fs.set_volume_finder_info(&[0x33; 32]);
7878 fs.sync_metadata().unwrap();
7879 let result = fs.fsck().unwrap();
7880 assert!(
7881 result.errors.is_empty(),
7882 "fsck errors: {:?}",
7883 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7884 );
7885 }
7886}