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 fn write_blank_hfs_sized_into<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 min_extents_bytes: u32,
3060 min_catalog_bytes: u32,
3061) -> Result<u64, FilesystemError> {
3062 let (front, mdb, image_size) = build_blank_hfs_front(
3063 target_size_bytes,
3064 block_size,
3065 volume_name,
3066 min_extents_bytes,
3067 min_catalog_bytes,
3068 hfs_common::hfs_now(),
3069 )?;
3070 target.seek(SeekFrom::Start(at_offset))?;
3071 target.write_all(&front)?;
3072 target.seek(SeekFrom::Start(at_offset + image_size - 1024))?;
3073 target.write_all(&mdb)?;
3074 target.write_all(&[0u8; 512])?;
3075 Ok(image_size)
3076}
3077
3078pub(crate) fn write_blank_hfs_into_at<W: Write + Seek>(
3080 target: &mut W,
3081 at_offset: u64,
3082 target_size_bytes: u64,
3083 block_size: u32,
3084 volume_name: &str,
3085 now: u32,
3086) -> Result<u64, FilesystemError> {
3087 let (front, mdb, image_size) =
3088 build_blank_hfs_front(target_size_bytes, block_size, volume_name, 0, 0, now)?;
3089 target.seek(SeekFrom::Start(at_offset))?;
3090 target.write_all(&front)?;
3091 target.seek(SeekFrom::Start(at_offset + image_size - 1024))?;
3092 target.write_all(&mdb)?;
3093 target.write_all(&[0u8; 512])?;
3094 Ok(image_size)
3095}
3096
3097fn build_blank_hfs_front(
3102 target_size_bytes: u64,
3103 block_size: u32,
3104 volume_name: &str,
3105 min_extents_bytes: u32,
3106 min_catalog_bytes: u32,
3107 now: u32,
3108) -> Result<(Vec<u8>, [u8; 512], u64), FilesystemError> {
3109 if block_size == 0 || !block_size.is_multiple_of(512) {
3110 return Err(FilesystemError::InvalidData(format!(
3111 "HFS block_size must be a non-zero multiple of 512 (got {block_size})"
3112 )));
3113 }
3114
3115 let name_raw = utf8_to_mac_roman(volume_name)?;
3116 if name_raw.len() > 27 {
3117 return Err(FilesystemError::InvalidData(format!(
3118 "HFS volume name '{volume_name}' exceeds 27 Mac Roman bytes"
3119 )));
3120 }
3121
3122 let mut first_alloc_block: u16 = 3;
3126 let total_blocks: u32;
3127 loop {
3128 let pre_alloc = first_alloc_block as u64 * 512;
3129 if target_size_bytes <= pre_alloc + 512 {
3130 return Err(FilesystemError::InvalidData(format!(
3131 "HFS volume too small ({target_size_bytes} bytes) for block_size {block_size}"
3132 )));
3133 }
3134 let usable = target_size_bytes - pre_alloc - 1024; let tb = usable / block_size as u64;
3136 if tb == 0 {
3137 return Err(FilesystemError::InvalidData(format!(
3138 "HFS volume too small for block_size {block_size}"
3139 )));
3140 }
3141 if tb > 65535 {
3142 return Err(FilesystemError::InvalidData(format!(
3143 "HFS would have {tb} allocation blocks at block_size {block_size}, exceeding u16 ceiling 65535"
3144 )));
3145 }
3146 let tb = tb as u32;
3147 let bitmap_bytes = (tb as u64).div_ceil(8);
3148 let bitmap_sectors = bitmap_bytes.div_ceil(512) as u16;
3149 let needed = 3u16 + bitmap_sectors;
3150 if needed <= first_alloc_block {
3151 total_blocks = tb;
3152 break;
3153 }
3154 first_alloc_block = needed;
3155 }
3156
3157 let (default_catalog, default_extents) = default_btree_sizes(target_size_bytes, block_size);
3161 let blocks_for = |bytes: u32, default: u32| -> u16 {
3162 let bytes = if bytes == 0 { default } else { bytes };
3163 let n = (bytes as u64).div_ceil(block_size as u64).max(4);
3164 n.min(u16::MAX as u64) as u16
3165 };
3166 let extents_blocks = blocks_for(min_extents_bytes, default_extents);
3167 let catalog_blocks = blocks_for(min_catalog_bytes, default_catalog);
3168 let extents_start: u16 = 0;
3169 let catalog_start: u16 = extents_blocks;
3170 let btree_blocks: u16 = extents_blocks + catalog_blocks;
3171
3172 if total_blocks < btree_blocks as u32 {
3173 return Err(FilesystemError::InvalidData(format!(
3174 "HFS volume too small: {total_blocks} allocation blocks is below the minimum of {btree_blocks} needed for B-trees"
3175 )));
3176 }
3177
3178 let extents_size = extents_blocks as u32 * block_size;
3179 let catalog_size = catalog_blocks as u32 * block_size;
3180
3181 let extents_node_size = pick_btree_node_size(extents_size as u64) as usize;
3185 let catalog_node_size = pick_btree_node_size(catalog_size as u64) as usize;
3186
3187 let image_size =
3190 first_alloc_block as u64 * 512 + total_blocks as u64 * block_size as u64 + 1024;
3191 let front_size = first_alloc_block as usize * 512 + btree_blocks as usize * block_size as usize;
3195 let mut front = vec![0u8; front_size];
3196
3197 let bitmap_off = 3 * 512;
3200 for b in 0..btree_blocks as u32 {
3201 let byte_idx = (b / 8) as usize;
3202 let bit_idx = 7 - (b % 8) as u8;
3203 front[bitmap_off + byte_idx] |= 1 << bit_idx;
3204 }
3205
3206 let extents_bytes =
3208 build_empty_hfs_extents_btree_with_node_size(extents_size as usize, extents_node_size)?;
3209 let extents_off = first_alloc_block as u64 * 512 + extents_start as u64 * block_size as u64;
3210 front[extents_off as usize..extents_off as usize + extents_bytes.len()]
3211 .copy_from_slice(&extents_bytes);
3212
3213 let catalog_bytes = build_empty_hfs_catalog_with_node_size(
3218 catalog_size as usize,
3219 catalog_node_size,
3220 &name_raw,
3221 now,
3222 )?;
3223 let catalog_off = first_alloc_block as u64 * 512 + catalog_start as u64 * block_size as u64;
3224 front[catalog_off as usize..catalog_off as usize + catalog_bytes.len()]
3225 .copy_from_slice(&catalog_bytes);
3226
3227 let mdb = build_blank_mdb(
3229 now,
3230 &name_raw,
3231 total_blocks as u16,
3232 block_size,
3233 first_alloc_block,
3234 catalog_start,
3235 catalog_blocks,
3236 catalog_size,
3237 extents_start,
3238 extents_blocks,
3239 extents_size,
3240 btree_blocks,
3241 );
3242 front[1024..1024 + 512].copy_from_slice(&mdb);
3243
3244 Ok((front, mdb, image_size))
3248}
3249
3250#[allow(clippy::too_many_arguments)]
3251fn build_blank_mdb(
3252 now: u32,
3253 name_raw: &[u8],
3254 total_blocks: u16,
3255 block_size: u32,
3256 first_alloc_block: u16,
3257 catalog_start: u16,
3258 catalog_blocks: u16,
3259 catalog_size: u32,
3260 extents_start: u16,
3261 extents_blocks: u16,
3262 extents_size: u32,
3263 allocated_blocks: u16,
3264) -> [u8; 512] {
3265 let mut mdb = [0u8; 512];
3266 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
3267 BigEndian::write_u32(&mut mdb[2..6], now); BigEndian::write_u32(&mut mdb[6..10], now); BigEndian::write_u16(&mut mdb[10..12], 0x0100);
3273 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;
3281 mdb[37..37 + name_raw.len()].copy_from_slice(name_raw);
3282 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);
3289 BigEndian::write_u16(&mut mdb[136..138], extents_blocks);
3290 BigEndian::write_u32(&mut mdb[146..150], catalog_size); BigEndian::write_u16(&mut mdb[150..152], catalog_start);
3292 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
3293 mdb
3294}
3295
3296impl<R: Read + Seek + Send> Filesystem for HfsFilesystem<R> {
3297 fn root(&mut self) -> Result<FileEntry, FilesystemError> {
3298 Ok(FileEntry {
3299 name: "/".into(),
3300 path: "/".into(),
3301 entry_type: EntryType::Directory,
3302 size: 0,
3303 location: 2, modified: None,
3305 modified_unix: None,
3306 type_code: None,
3307 creator_code: None,
3308 symlink_target: None,
3309 special_type: None,
3310 mode: None,
3311 uid: None,
3312 gid: None,
3313 resource_fork_size: None,
3314 aux_type: None,
3315 link_target_cnid: None,
3316 amiga_protection: None,
3317 amiga_comment: None,
3318 amiga_date: None,
3319 dos_attributes: None,
3320 finder_flags: None,
3321 prodos_file_type: None,
3322 mac_dates: None,
3323 owner_label: None,
3324 native_attrs: None,
3325 qnx2_attrs: None,
3326 })
3327 }
3328
3329 fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
3330 let parent_id = entry.location as u32;
3331 let children = self.list_children(parent_id)?;
3332
3333 let mut entries = Vec::new();
3334 for child in children {
3335 match child {
3336 CatalogRecord::Directory {
3337 dir_id,
3338 name,
3339 dates,
3340 ..
3341 } => {
3342 let path = if entry.path == "/" {
3343 format!("/{name}")
3344 } else {
3345 format!("{}/{name}", entry.path)
3346 };
3347 let mut fe = FileEntry::new_directory(name, path, dir_id as u64);
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 entries.push(fe);
3352 }
3353 CatalogRecord::File {
3354 file_id,
3355 name,
3356 data_size,
3357 rsrc_size,
3358 rsrc_extents,
3359 type_code,
3360 creator_code,
3361 finder_flags,
3362 dates,
3363 ..
3364 } => {
3365 let path = if entry.path == "/" {
3366 format!("/{name}")
3367 } else {
3368 format!("{}/{name}", entry.path)
3369 };
3370 let mut fe = FileEntry::new_file(name, path, data_size as u64, file_id as u64);
3371 fe.type_code = Some(type_code);
3372 fe.creator_code = Some(creator_code);
3373 fe.finder_flags = Some(finder_flags);
3374 fe.modified = hfs_common::format_mac_date(dates.1);
3375 fe.modified_unix = disk_core::times::mac_local_to_unix(dates.1);
3376 fe.mac_dates = Some(dates);
3377 if rsrc_size > 0 {
3378 fe.resource_fork_size = Some(rsrc_size as u64);
3379 }
3380 if finder_flags & crate::mac_alias::IS_ALIAS_FLAG != 0 && rsrc_size > 0 {
3384 if let Ok(rsrc) = read_fork_data(
3385 &mut self.reader,
3386 self.partition_offset,
3387 &self.mdb,
3388 &rsrc_extents,
3389 rsrc_size as u64,
3390 ) {
3391 if let Some(target) = crate::mac_alias::resolve_alias_target(&rsrc) {
3392 fe.symlink_target = Some(target);
3393 }
3394 }
3395 }
3396 entries.push(fe);
3397 }
3398 }
3399 }
3400
3401 entries.sort_by_key(|a| a.name.to_lowercase());
3402 Ok(entries)
3403 }
3404
3405 fn read_file(
3406 &mut self,
3407 entry: &FileEntry,
3408 max_bytes: usize,
3409 ) -> Result<Vec<u8>, FilesystemError> {
3410 let file_id = entry.location as u32;
3411 let (data_size, extents, _rsrc_size, _rsrc_extents) =
3412 self.find_file_by_id(file_id).ok_or_else(|| {
3413 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3414 })?;
3415
3416 let mut data = self.read_fork_with_overflow(file_id, 0x00, &extents, data_size as u64)?;
3417 data.truncate(max_bytes);
3418 Ok(data)
3419 }
3420
3421 fn write_file_to(
3422 &mut self,
3423 entry: &FileEntry,
3424 writer: &mut dyn std::io::Write,
3425 ) -> Result<u64, FilesystemError> {
3426 if entry.is_directory() {
3427 return Err(FilesystemError::NotADirectory(entry.path.clone()));
3428 }
3429 let file_id = entry.location as u32;
3430 let (data_size, extents, _rsrc_size, _rsrc_extents) =
3431 self.find_file_by_id(file_id).ok_or_else(|| {
3432 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3433 })?;
3434 self.write_fork_with_overflow_to(file_id, 0x00, &extents, data_size as u64, writer)
3435 }
3436
3437 fn volume_label(&self) -> Option<&str> {
3438 if self.mdb.volume_name.is_empty() {
3439 None
3440 } else {
3441 Some(&self.mdb.volume_name)
3442 }
3443 }
3444
3445 fn fs_type(&self) -> &str {
3446 "HFS"
3447 }
3448
3449 fn validate_name(&self, name: &str) -> Result<(), FilesystemError> {
3450 validate_hfs_create_name(name).map(|_| ())
3451 }
3452
3453 fn uses_colon_paths(&self) -> bool {
3454 true
3455 }
3456
3457 fn total_size(&self) -> u64 {
3458 self.mdb.total_blocks as u64 * self.mdb.block_size as u64
3459 }
3460
3461 fn used_size(&self) -> u64 {
3462 (self.mdb.total_blocks - self.mdb.free_blocks) as u64 * self.mdb.block_size as u64
3463 }
3464
3465 fn allocation_unit(&self) -> Option<u64> {
3471 if self.mdb.block_size == 0 {
3472 None
3473 } else {
3474 Some(self.mdb.block_size as u64)
3475 }
3476 }
3477
3478 fn last_data_byte(&mut self) -> Result<u64, FilesystemError> {
3479 let bitmap = self.read_volume_bitmap()?;
3480 let last_block = find_last_set_bit(&bitmap, self.mdb.total_blocks as u32);
3481 match last_block {
3483 Some(block) => Ok(self.alloc_block_offset(block + 1) + 1024),
3484 None => Ok(self.alloc_block_offset(self.mdb.total_blocks as u32) + 1024),
3485 }
3486 }
3487
3488 fn write_resource_fork_to(
3489 &mut self,
3490 entry: &FileEntry,
3491 writer: &mut dyn std::io::Write,
3492 ) -> Result<u64, FilesystemError> {
3493 let file_id = entry.location as u32;
3494 let (_data_size, _data_ext, rsrc_size, rsrc_extents) =
3495 self.find_file_by_id(file_id).ok_or_else(|| {
3496 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3497 })?;
3498 if rsrc_size == 0 {
3499 return Ok(0);
3500 }
3501 let data = self.read_fork_with_overflow(file_id, 0xFF, &rsrc_extents, rsrc_size as u64)?;
3502 writer.write_all(&data)?;
3503 Ok(data.len() as u64)
3504 }
3505
3506 fn resource_fork_size(&mut self, entry: &FileEntry) -> u64 {
3507 let file_id = entry.location as u32;
3508 self.find_file_by_id(file_id)
3509 .map(|(_ds, _de, rs, _re)| rs as u64)
3510 .unwrap_or(0)
3511 }
3512
3513 fn fragmentation_stats(
3514 &mut self,
3515 ) -> Option<Result<disk_core::filesystem::FragmentationStats, FilesystemError>> {
3516 Some(Ok(hfs_fragmentation_stats(&self.catalog_data)))
3517 }
3518
3519 fn blessed_system_folder(&mut self) -> Option<(u64, String)> {
3520 let cnid = self.mdb.finder_info[0];
3521 if cnid == 0 {
3522 return None;
3523 }
3524 if let Some((_node, _rec, offset)) = self.find_catalog_record_by_cnid(cnid) {
3526 let key_len = self.catalog_data[offset] as usize;
3527 let mut rec_data_start = offset + 1 + key_len;
3528 if !rec_data_start.is_multiple_of(2) {
3529 rec_data_start += 1;
3530 }
3531 if rec_data_start + 15 <= self.catalog_data.len() {
3533 let name_len = self.catalog_data[rec_data_start + 14] as usize;
3534 if rec_data_start + 15 + name_len <= self.catalog_data.len() {
3535 let name = mac_roman_to_utf8(
3536 &self.catalog_data[rec_data_start + 15..rec_data_start + 15 + name_len],
3537 );
3538 return Some((cnid as u64, name));
3539 }
3540 }
3541 }
3542 Some((cnid as u64, format!("CNID {}", cnid)))
3543 }
3544
3545 fn fsck(&mut self) -> Option<Result<disk_core::fsck::FsckResult, FilesystemError>> {
3546 Some(self.fsck())
3547 }
3548}
3549
3550const CATALOG_DIR_THREAD: i8 = 3;
3553
3554impl<R: Read + Write + Seek + Send> EditableFilesystem for HfsFilesystem<R> {
3555 fn as_filesystem(&self) -> &dyn disk_core::filesystem::Filesystem {
3556 self
3557 }
3558 fn as_filesystem_mut(&mut self) -> &mut dyn disk_core::filesystem::Filesystem {
3559 self
3560 }
3561 fn begin_bulk(&mut self) {
3562 self.bulk_mode = true;
3563 }
3564
3565 fn end_bulk(&mut self) {
3566 self.bulk_mode = false;
3567 }
3568
3569 fn create_file(
3570 &mut self,
3571 parent: &FileEntry,
3572 name: &str,
3573 data: &mut dyn std::io::Read,
3574 data_len: u64,
3575 options: &CreateFileOptions,
3576 ) -> Result<FileEntry, FilesystemError> {
3577 self.ensure_catalog_initialized()?;
3578 let snap = self.snapshot();
3579 let extents_before = self.extents_overflow_data.clone();
3580 let result = (|| {
3581 let parent_id = parent.location as u32;
3582
3583 let name_bytes = validate_hfs_create_name(name)?;
3584
3585 if self
3587 .find_catalog_record_by_name(parent_id, &name_bytes)
3588 .is_some()
3589 {
3590 return Err(FilesystemError::AlreadyExists(name.into()));
3591 }
3592
3593 let file_id = self.mdb.next_catalog_id;
3595 self.mdb.next_catalog_id += 1;
3596
3597 let (type_code, creator_code) = hfs_common::resolve_create_type_creator(
3601 name,
3602 options.os_type,
3603 options.os_creator,
3604 options.type_code.as_deref(),
3605 options.creator_code.as_deref(),
3606 );
3607
3608 let data_extents = self.write_data_to_blocks(data, data_len)?;
3610
3611 let (rsrc_extents, rsrc_size) = if let Some(ref rsrc_src) = options.resource_fork {
3613 match rsrc_src {
3614 disk_core::filesystem::ResourceForkSource::Data(rsrc_data) => {
3615 let mut cursor = std::io::Cursor::new(rsrc_data);
3616 let ext = self.write_data_to_blocks(&mut cursor, rsrc_data.len() as u64)?;
3617 (ext, rsrc_data.len() as u32)
3618 }
3619 disk_core::filesystem::ResourceForkSource::File(path) => {
3620 let mut f = std::fs::File::open(path)?;
3621 let len = f.metadata()?.len();
3622 let ext = self.write_data_to_blocks(&mut f, len)?;
3623 (ext, len as u32)
3624 }
3625 }
3626 } else {
3627 (Vec::new(), 0)
3628 };
3629
3630 let mtime = options.unix_times.map(|t| t.mtime_or_now());
3635 let mut file_rec = Self::build_file_record(
3636 file_id,
3637 data_len as u32,
3638 data_extents.first().map_or(0, |e| e.start_block),
3639 Self::extents_block_total(&data_extents),
3640 rsrc_size,
3641 rsrc_extents.first().map_or(0, |e| e.start_block),
3642 Self::extents_block_total(&rsrc_extents),
3643 &type_code,
3644 &creator_code,
3645 self.mdb.block_size,
3646 mtime,
3647 options.finder_flags.unwrap_or(0),
3648 );
3649 Self::set_inline_extents(&mut file_rec, 74, &data_extents);
3650 Self::set_inline_extents(&mut file_rec, 86, &rsrc_extents);
3651
3652 let key = Self::build_catalog_key(parent_id, &name_bytes);
3654 let mut key_record = key;
3655 key_record.extend_from_slice(&file_rec);
3656
3657 self.insert_catalog_record(&key_record)?;
3658
3659 self.update_parent_valence(parent_id, 1)?;
3669
3670 self.mdb.file_count += 1;
3672
3673 let mut overflow_changed = self.insert_fork_overflow(file_id, 0x00, &data_extents)?;
3674 overflow_changed |= self.insert_fork_overflow(file_id, 0xFF, &rsrc_extents)?;
3675 if overflow_changed {
3676 self.write_extents_overflow_back()?;
3677 }
3678
3679 let path = if parent.path == "/" {
3680 format!("/{name}")
3681 } else {
3682 format!("{}/{name}", parent.path)
3683 };
3684 let mut fe = FileEntry::new_file(name.to_string(), path, data_len, file_id as u64);
3685 if type_code != [0; 4] {
3686 fe.type_code = Some(type_code);
3687 fe.creator_code = Some(creator_code);
3688 }
3689 fe.finder_flags = options.finder_flags;
3690 if rsrc_size > 0 {
3691 fe.resource_fork_size = Some(rsrc_size as u64);
3692 }
3693 let stamped = mtime.unwrap_or_else(disk_core::times::now);
3694 fe.modified_unix = Some(stamped);
3695 fe.mac_dates = Some((
3696 disk_core::times::unix_to_mac_local(stamped),
3697 disk_core::times::unix_to_mac_local(stamped),
3698 0,
3699 ));
3700 Ok(fe)
3701 })();
3702 if result.is_err() {
3703 self.extents_overflow_data = extents_before;
3704 self.restore_snapshot(snap);
3705 }
3706 result
3707 }
3708
3709 fn create_directory(
3710 &mut self,
3711 parent: &FileEntry,
3712 name: &str,
3713 options: &CreateDirectoryOptions,
3714 ) -> Result<FileEntry, FilesystemError> {
3715 self.ensure_catalog_initialized()?;
3716 let snap = self.snapshot();
3717 let result = (|| {
3718 let parent_id = parent.location as u32;
3719
3720 let name_bytes = validate_hfs_create_name(name)?;
3721
3722 if self
3724 .find_catalog_record_by_name(parent_id, &name_bytes)
3725 .is_some()
3726 {
3727 return Err(FilesystemError::AlreadyExists(name.into()));
3728 }
3729
3730 let folder_id = self.mdb.next_catalog_id;
3732 self.mdb.next_catalog_id += 1;
3733
3734 let mtime = options.unix_times.map(|t| t.mtime_or_now());
3737 let folder_rec = Self::build_dir_record_with_dates(folder_id, mtime);
3738
3739 let key = Self::build_catalog_key(parent_id, &name_bytes);
3741 let mut key_record = key;
3742 key_record.extend_from_slice(&folder_rec);
3743
3744 self.insert_catalog_record(&key_record)?;
3745
3746 let (_, thread_data) =
3748 Self::build_thread_record(CATALOG_DIR_THREAD, parent_id, &name_bytes);
3749 let thread_key = Self::build_catalog_key(folder_id, &[]);
3750 let mut thread_record = thread_key;
3751 thread_record.extend_from_slice(&thread_data);
3752 self.insert_catalog_record(&thread_record)?;
3753
3754 self.update_parent_valence(parent_id, 1)?;
3756
3757 self.mdb.folder_count += 1;
3759
3760 let path = if parent.path == "/" {
3761 format!("/{name}")
3762 } else {
3763 format!("{}/{name}", parent.path)
3764 };
3765 let mut fe = FileEntry::new_directory(name.to_string(), path, folder_id as u64);
3766 let stamped = mtime.unwrap_or_else(disk_core::times::now);
3767 fe.modified_unix = Some(stamped);
3768 fe.mac_dates = Some((
3769 disk_core::times::unix_to_mac_local(stamped),
3770 disk_core::times::unix_to_mac_local(stamped),
3771 0,
3772 ));
3773 Ok(fe)
3774 })();
3775 if result.is_err() {
3776 self.restore_snapshot(snap);
3777 }
3778 result
3779 }
3780
3781 fn delete_entry(
3782 &mut self,
3783 parent: &FileEntry,
3784 entry: &FileEntry,
3785 ) -> Result<(), FilesystemError> {
3786 let snap = self.snapshot();
3787 let extents_before = self.extents_overflow_data.clone();
3788 let result = (|| {
3789 let parent_id = parent.location as u32;
3790 let cnid = entry.location as u32;
3791
3792 if entry.is_directory() {
3794 let children = self.list_children(cnid)?;
3795 if !children.is_empty() {
3796 return Err(FilesystemError::InvalidData(
3797 "cannot delete non-empty directory".into(),
3798 ));
3799 }
3800 }
3801
3802 let name_bytes = utf8_to_mac_roman(&entry.name)?;
3804 let (node_idx, rec_idx, _offset) = self
3805 .find_catalog_record_by_name(parent_id, &name_bytes)
3806 .ok_or_else(|| {
3807 FilesystemError::NotFound(format!(
3808 "entry '{}' not found in catalog",
3809 entry.name
3810 ))
3811 })?;
3812
3813 let mut overflow_changed = false;
3815 if !entry.is_directory() {
3816 if let Some((_, data_extents, _, rsrc_extents)) = self.find_file_by_id(cnid) {
3817 self.free_extent_blocks(&data_extents);
3818 self.free_extent_blocks(&rsrc_extents);
3819 overflow_changed |= self.free_fork_overflow(cnid, 0x00)?;
3821 overflow_changed |= self.free_fork_overflow(cnid, 0xFF)?;
3822 }
3823 }
3824
3825 self.remove_catalog_record(node_idx, rec_idx)?;
3826
3827 if let Some((t_node, t_rec, _)) = self.find_catalog_record_by_cnid(cnid) {
3829 self.remove_catalog_record(t_node, t_rec)?;
3830 }
3831
3832 self.update_parent_valence(parent_id, -1)?;
3834
3835 if entry.is_directory() {
3837 self.mdb.folder_count = self.mdb.folder_count.saturating_sub(1);
3838 } else {
3839 self.mdb.file_count = self.mdb.file_count.saturating_sub(1);
3840 }
3841
3842 if overflow_changed {
3843 self.write_extents_overflow_back()?;
3844 }
3845
3846 Ok(())
3847 })();
3848 if result.is_err() {
3849 self.extents_overflow_data = extents_before;
3850 self.restore_snapshot(snap);
3851 }
3852 result
3853 }
3854
3855 fn rename(
3856 &mut self,
3857 parent: &FileEntry,
3858 entry: &FileEntry,
3859 new_name: &str,
3860 ) -> Result<(), FilesystemError> {
3861 if new_name == entry.name {
3862 return Ok(());
3863 }
3864 let parent_id = parent.location as u32;
3865 let cnid = entry.location as u32;
3866 let new_name_raw = validate_hfs_create_name(new_name)?;
3867 let old_name_raw = utf8_to_mac_roman(&entry.name)?;
3868
3869 let snap = self.snapshot();
3870 let result = (|| -> Result<(), FilesystemError> {
3871 let (node_idx, rec_idx, _key_abs) = self
3873 .find_catalog_record_by_name(parent_id, &old_name_raw)
3874 .ok_or_else(|| {
3875 FilesystemError::NotFound(format!(
3876 "entry '{}' not found in catalog",
3877 entry.name
3878 ))
3879 })?;
3880
3881 if let Some((n2, r2, _)) = self.find_catalog_record_by_name(parent_id, &new_name_raw) {
3884 if (n2, r2) != (node_idx, rec_idx) {
3885 return Err(FilesystemError::AlreadyExists(new_name.to_string()));
3886 }
3887 }
3888
3889 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
3892 let node_off = node_idx as usize * node_size;
3893 let node = &self.catalog_data[node_off..node_off + node_size];
3894 let (rec_start, rec_end) = hfs_common::btree_record_range(node, node_size, rec_idx);
3895 let key_len = node[rec_start] as usize;
3896 let mut data_rel = 1 + key_len;
3897 if !data_rel.is_multiple_of(2) {
3898 data_rel += 1;
3899 }
3900 let data_bytes = node[rec_start + data_rel..rec_end].to_vec();
3901
3902 let mut new_record = Self::build_catalog_key(parent_id, &new_name_raw);
3903 if !new_record.len().is_multiple_of(2) {
3904 new_record.push(0);
3905 }
3906 new_record.extend_from_slice(&data_bytes);
3907 if !new_record.len().is_multiple_of(2) {
3908 new_record.push(0);
3909 }
3910
3911 self.remove_catalog_record(node_idx, rec_idx)?;
3912 self.insert_catalog_record(&new_record)?;
3913
3914 if let Some((_n, _r, t_key_abs)) = self.find_catalog_record_by_cnid(cnid) {
3918 let t_key_len = self.catalog_data[t_key_abs] as usize;
3919 let mut t_data_off = t_key_abs + 1 + t_key_len;
3920 if !t_data_off.is_multiple_of(2) {
3921 t_data_off += 1;
3922 }
3923 if t_data_off + 15 + 31 <= self.catalog_data.len() {
3924 self.catalog_data[t_data_off + 14] = new_name_raw.len() as u8;
3925 for i in 0..31 {
3926 self.catalog_data[t_data_off + 15 + i] = 0;
3927 }
3928 self.catalog_data[t_data_off + 15..t_data_off + 15 + new_name_raw.len()]
3929 .copy_from_slice(&new_name_raw);
3930 }
3931 }
3932 Ok(())
3934 })();
3935 if result.is_err() {
3936 self.restore_snapshot(snap);
3937 }
3938 result
3939 }
3940
3941 fn set_type_creator(
3942 &mut self,
3943 entry: &FileEntry,
3944 type_code: &str,
3945 creator_code: &str,
3946 ) -> Result<(), FilesystemError> {
3947 let snap = self.snapshot();
3948 let result = (|| {
3949 let frec_start = self.locate_record_data(entry.location as u32)?;
3950 if self.catalog_data[frec_start] as i8 != CATALOG_FILE {
3951 return Err(FilesystemError::InvalidData(
3952 "set_type_creator: not a file record".into(),
3953 ));
3954 }
3955
3956 let tc = hfs_common::encode_fourcc(type_code);
3958 let cc = hfs_common::encode_fourcc(creator_code);
3959 self.catalog_data[frec_start + 4..frec_start + 8].copy_from_slice(&tc);
3960 self.catalog_data[frec_start + 8..frec_start + 12].copy_from_slice(&cc);
3961
3962 Ok(())
3963 })();
3964 if result.is_err() {
3965 self.restore_snapshot(snap);
3966 }
3967 result
3968 }
3969
3970 fn write_resource_fork(
3971 &mut self,
3972 entry: &FileEntry,
3973 data: &mut dyn std::io::Read,
3974 len: u64,
3975 ) -> Result<(), FilesystemError> {
3976 let snap = self.snapshot();
3977 let extents_before = self.extents_overflow_data.clone();
3978 let result = (|| {
3979 let cnid = entry.location as u32;
3980
3981 let mut overflow_changed = false;
3983 if let Some((_, _, _, rsrc_extents)) = self.find_file_by_id(cnid) {
3984 self.free_extent_blocks(&rsrc_extents);
3985 overflow_changed = self.free_fork_overflow(cnid, 0xFF)?;
3986 }
3987
3988 let rsrc_extents = self.write_data_to_blocks(data, len)?;
3990
3991 let frec_start = self.locate_record_data(cnid)?;
3992 if self.catalog_data[frec_start] as i8 != CATALOG_FILE {
3993 return Err(FilesystemError::InvalidData(
3994 "write_resource_fork: not a file record".into(),
3995 ));
3996 }
3997
3998 self.catalog_data[frec_start + 34..frec_start + 36].fill(0);
4004 BigEndian::write_u32(
4006 &mut self.catalog_data[frec_start + 36..frec_start + 40],
4007 len as u32,
4008 );
4009 BigEndian::write_u32(
4011 &mut self.catalog_data[frec_start + 40..frec_start + 44],
4012 Self::extents_block_total(&rsrc_extents) as u32 * self.mdb.block_size,
4013 );
4014 Self::set_inline_extents(
4016 &mut self.catalog_data[frec_start..frec_start + 102],
4017 86,
4018 &rsrc_extents,
4019 );
4020 overflow_changed |= self.insert_fork_overflow(cnid, 0xFF, &rsrc_extents)?;
4021
4022 if overflow_changed {
4023 self.write_extents_overflow_back()?;
4024 }
4025
4026 Ok(())
4027 })();
4028 if result.is_err() {
4029 self.extents_overflow_data = extents_before;
4030 self.restore_snapshot(snap);
4031 }
4032 result
4033 }
4034
4035 fn repair(&mut self) -> Result<disk_core::fsck::RepairReport, FilesystemError> {
4036 self.ensure_bitmap()?;
4037 let mut extents_data = if self.mdb.extents_file_size > 0 {
4038 read_fork_data(
4039 &mut self.reader,
4040 self.partition_offset,
4041 &self.mdb,
4042 &self.mdb.extents_file_extents,
4043 self.mdb.extents_file_size as u64,
4044 )
4045 .ok()
4046 } else {
4047 None
4048 };
4049
4050 let report = crate::hfs_fsck::repair_hfs(
4051 &mut self.mdb,
4052 &mut self.catalog_data,
4053 self.bitmap.as_mut().unwrap(),
4054 extents_data.as_mut(),
4055 );
4056
4057 if let Some(ref ext_data) = extents_data {
4059 if self.mdb.extents_file_size > 0 {
4060 write_extents_overflow_data(
4061 &mut self.reader,
4062 self.partition_offset,
4063 &self.mdb,
4064 ext_data,
4065 )?;
4066 }
4067 }
4068
4069 self.do_sync_metadata()?;
4070 Ok(report)
4071 }
4072
4073 fn sync_metadata(&mut self) -> Result<(), FilesystemError> {
4074 self.do_sync_metadata()
4075 }
4076
4077 fn free_space(&mut self) -> Result<u64, FilesystemError> {
4078 Ok(self.mdb.free_blocks as u64 * self.mdb.block_size as u64)
4079 }
4080
4081 fn set_blessed_folder(&mut self, entry: &FileEntry) -> Result<(), FilesystemError> {
4082 let snap = self.snapshot();
4083 let result = (|| {
4084 if !entry.is_directory() {
4085 return Err(FilesystemError::InvalidData(
4086 "can only bless a directory".into(),
4087 ));
4088 }
4089 self.mdb.finder_info[0] = entry.location as u32;
4090 Ok(())
4091 })();
4092 if result.is_err() {
4093 self.restore_snapshot(snap);
4094 }
4095 result
4096 }
4097
4098 fn write_boot_blocks(&mut self, blocks: &[u8; 1024]) -> Result<(), FilesystemError> {
4099 HfsFilesystem::set_boot_blocks(self, blocks);
4100 Ok(())
4101 }
4102
4103 fn set_volume_name(&mut self, new_name: &str) -> Result<(), FilesystemError> {
4104 HfsFilesystem::set_volume_name(self, new_name)
4105 }
4106
4107 fn set_finder_info(
4108 &mut self,
4109 entry: &FileEntry,
4110 finfo: [u8; 16],
4111 fxinfo: [u8; 16],
4112 ) -> Result<(), FilesystemError> {
4113 HfsFilesystem::set_finder_info(self, entry, finfo, fxinfo)
4114 }
4115
4116 fn set_dates(
4117 &mut self,
4118 entry: &FileEntry,
4119 create: u32,
4120 modify: u32,
4121 backup: u32,
4122 ) -> Result<(), FilesystemError> {
4123 HfsFilesystem::set_dates(self, entry, create, modify, backup)
4124 }
4125}
4126
4127fn collect_fork_overflow_extents(
4133 extents_data: &[u8],
4134 file_id: u32,
4135 fork_type: u8,
4136 min_start_block: u32,
4137) -> Vec<HfsExtDescriptor> {
4138 use crate::hfs_common::{btree_record_range, BTreeHeader, BTREE_LEAF_NODE};
4139 use std::collections::HashSet;
4140
4141 let mut out: Vec<(u16, HfsExtDescriptor)> = Vec::new();
4142 if extents_data.len() < 512 {
4143 return Vec::new();
4144 }
4145 let header = BTreeHeader::read(extents_data);
4146 let node_size = header.node_size as usize;
4147 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE || extents_data.len() < node_size {
4148 return Vec::new();
4149 }
4150
4151 let mut node_idx = header.first_leaf_node;
4152 let mut visited: HashSet<u32> = HashSet::new();
4153 while node_idx != 0 {
4154 if !visited.insert(node_idx) {
4155 break;
4156 }
4157 let off = node_idx as usize * node_size;
4158 if off + node_size > extents_data.len() {
4159 break;
4160 }
4161 let node = &extents_data[off..off + node_size];
4162 let flink = BigEndian::read_u32(&node[0..4]);
4163 let kind = node[8] as i8;
4164 if kind != BTREE_LEAF_NODE {
4165 break;
4166 }
4167 let num_records = BigEndian::read_u16(&node[10..12]) as usize;
4168 for i in 0..num_records {
4169 let (rec_start, _) = btree_record_range(node, node_size, i);
4170 if rec_start + 8 > node_size {
4171 continue;
4172 }
4173 let key_len = node[rec_start] as usize;
4174 if key_len < 7 || rec_start + 1 + key_len > node_size {
4175 continue;
4176 }
4177 let rec_fork_type = node[rec_start + 1];
4178 let rec_file_id = BigEndian::read_u32(&node[rec_start + 2..rec_start + 6]);
4179 let rec_start_block = BigEndian::read_u16(&node[rec_start + 6..rec_start + 8]);
4180 if rec_fork_type != fork_type || rec_file_id != file_id {
4181 continue;
4182 }
4183 if (rec_start_block as u32) < min_start_block {
4184 continue;
4185 }
4186 let mut data_off = rec_start + 1 + key_len;
4187 if !data_off.is_multiple_of(2) {
4188 data_off += 1;
4189 }
4190 if data_off + 12 > node_size {
4191 continue;
4192 }
4193 for j in 0..3 {
4194 let ext = HfsExtDescriptor::parse(&node[data_off + j * 4..data_off + j * 4 + 4]);
4195 if ext.block_count > 0 {
4196 out.push((rec_start_block, ext));
4197 }
4198 }
4199 }
4200 node_idx = flink;
4201 }
4202
4203 out.sort_by_key(|(sb, _)| *sb);
4204 out.into_iter().map(|(_, e)| e).collect()
4205}
4206
4207fn read_fork_data<R: Read + Seek>(
4208 reader: &mut R,
4209 partition_offset: u64,
4210 mdb: &HfsMasterDirectoryBlock,
4211 extents: &[HfsExtDescriptor; 3],
4212 size: u64,
4213) -> Result<Vec<u8>, FilesystemError> {
4214 let mut data = Vec::with_capacity(size as usize);
4215 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4216
4217 for ext in extents {
4218 if ext.block_count == 0 {
4219 break;
4220 }
4221 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4222 let len = ext.block_count as u64 * mdb.block_size as u64;
4223 reader.seek(SeekFrom::Start(offset))?;
4224 let read_len = len.min(size - data.len() as u64) as usize;
4225 let mut buf = vec![0u8; read_len];
4226 reader.read_exact(&mut buf)?;
4227 data.extend_from_slice(&buf);
4228 if data.len() as u64 >= size {
4229 break;
4230 }
4231 }
4232
4233 data.truncate(size as usize);
4234 Ok(data)
4235}
4236
4237fn remove_fork_overflow_records(
4240 extents_data: &mut [u8],
4241 file_id: u32,
4242 fork_type: u8,
4243) -> Result<u32, FilesystemError> {
4244 use crate::hfs_common::{
4245 btree_remove_record, btree_retire_empty_leaf, compare_classic_extents_keys,
4246 walk_leaf_records, BTreeHeader,
4247 };
4248 let kf = BTreeKeyFormat::CLASSIC_EXTENTS;
4249 if extents_data.len() < 512 {
4250 return Ok(0);
4251 }
4252 let header = BTreeHeader::read(extents_data);
4253 let node_size = header.node_size as usize;
4254 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE || extents_data.len() < node_size {
4255 return Ok(0);
4256 }
4257 let mut victims: Vec<(u32, usize)> = Vec::new();
4258 walk_leaf_records::<(), _>(
4259 extents_data,
4260 header.first_leaf_node,
4261 node_size,
4262 |node_idx, rec_idx, _off, rec| {
4263 if rec.len() >= 8
4265 && rec[0] >= 7
4266 && rec[1] == fork_type
4267 && BigEndian::read_u32(&rec[2..6]) == file_id
4268 {
4269 victims.push((node_idx, rec_idx));
4270 }
4271 None
4272 },
4273 );
4274 if victims.is_empty() {
4275 return Ok(0);
4276 }
4277 victims.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
4279 for &(node_idx, rec_idx) in &victims {
4280 let off = node_idx as usize * node_size;
4281 btree_remove_record(&mut extents_data[off..off + node_size], node_size, rec_idx);
4282 }
4283 let mut h = BTreeHeader::read(extents_data);
4284 h.leaf_records = h.leaf_records.saturating_sub(victims.len() as u32);
4285 h.write(extents_data);
4286 let mut nodes: Vec<u32> = victims.iter().map(|v| v.0).collect();
4287 nodes.dedup();
4288 for node_idx in nodes {
4289 let off = node_idx as usize * node_size;
4290 if BigEndian::read_u16(&extents_data[off + 10..off + 12]) == 0 {
4291 btree_retire_empty_leaf(
4293 extents_data,
4294 node_size,
4295 node_idx,
4296 &kf,
4297 &compare_classic_extents_keys,
4298 )?;
4299 } else if victims.iter().any(|&(n, r)| n == node_idx && r == 0) {
4300 btree_refresh_index_keys(
4302 extents_data,
4303 node_size,
4304 node_idx,
4305 &kf,
4306 &compare_classic_extents_keys,
4307 )?;
4308 }
4309 }
4310 Ok(victims.len() as u32)
4311}
4312
4313fn write_extents_overflow_data<W: std::io::Write + Seek>(
4315 writer: &mut W,
4316 partition_offset: u64,
4317 mdb: &HfsMasterDirectoryBlock,
4318 data: &[u8],
4319) -> Result<(), FilesystemError> {
4320 let alloc_start = partition_offset + mdb.first_alloc_block as u64 * 512;
4321 let mut data_start = 0usize;
4322 for extent in mdb.extents_file_extents.iter() {
4323 if extent.block_count == 0 {
4324 break;
4325 }
4326 let block_off = alloc_start + extent.start_block as u64 * mdb.block_size as u64;
4327 let byte_len = extent.block_count as usize * mdb.block_size as usize;
4328 let data_end = (data_start + byte_len).min(data.len());
4329 if data_start < data_end {
4330 writer.seek(std::io::SeekFrom::Start(block_off))?;
4331 writer.write_all(&data[data_start..data_end])?;
4332 }
4333 data_start += byte_len;
4334 }
4335 Ok(())
4336}
4337
4338fn write_fork_data_to<R: Read + Seek>(
4340 reader: &mut R,
4341 partition_offset: u64,
4342 mdb: &HfsMasterDirectoryBlock,
4343 extents: &[HfsExtDescriptor; 3],
4344 writer: &mut dyn std::io::Write,
4345 size: u64,
4346) -> Result<u64, FilesystemError> {
4347 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4348 let mut written: u64 = 0;
4349 let mut buf = vec![0u8; 64 * 1024];
4350
4351 for ext in extents {
4352 if ext.block_count == 0 || written >= size {
4353 break;
4354 }
4355 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4356 let extent_len = ext.block_count as u64 * mdb.block_size as u64;
4357 let to_emit = extent_len.min(size - written);
4358 reader.seek(SeekFrom::Start(offset))?;
4359 let mut left = to_emit;
4360 while left > 0 {
4361 let n = (buf.len() as u64).min(left) as usize;
4362 reader.read_exact(&mut buf[..n])?;
4363 writer.write_all(&buf[..n])?;
4364 left -= n as u64;
4365 }
4366 written += to_emit;
4367 }
4368 Ok(written)
4369}
4370
4371fn write_hfs_fork_data<R: Write + Seek>(
4373 writer: &mut R,
4374 partition_offset: u64,
4375 mdb: &HfsMasterDirectoryBlock,
4376 extents: &[HfsExtDescriptor; 3],
4377 data: &[u8],
4378) -> Result<(), FilesystemError> {
4379 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4380 let mut written = 0usize;
4381 for ext in extents {
4382 if ext.block_count == 0 || written >= data.len() {
4383 break;
4384 }
4385 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4386 let extent_len = ext.block_count as u64 * mdb.block_size as u64;
4387 let to_write = extent_len.min((data.len() - written) as u64) as usize;
4388 writer.seek(SeekFrom::Start(offset))?;
4389 writer.write_all(&data[written..written + to_write])?;
4390 written += to_write;
4391 }
4392 Ok(())
4393}
4394
4395fn find_last_set_bit(bitmap: &[u8], max_bits: u32) -> Option<u32> {
4397 let mut last = None;
4398 for bit in 0..max_bits {
4399 let byte_idx = bit as usize / 8;
4400 let bit_idx = 7 - (bit % 8); if byte_idx < bitmap.len() && (bitmap[byte_idx] >> bit_idx) & 1 == 1 {
4402 last = Some(bit);
4403 }
4404 }
4405 last
4406}
4407
4408pub struct CompactHfsReader<R: Read + Seek> {
4416 reader: R,
4417 partition_offset: u64,
4418 mdb: HfsMasterDirectoryBlock,
4419 bitmap: Vec<u8>,
4420 phase: u8,
4422 pre_alloc_pos: u64,
4424 current_block: u32,
4426 block_pos: u64,
4428 #[allow(dead_code)]
4429 original_size: u64,
4430 #[allow(dead_code)]
4431 allocated_blocks: u32,
4432}
4433
4434impl<R: Read + Seek> CompactHfsReader<R> {
4435 pub fn new(
4436 mut reader: R,
4437 partition_offset: u64,
4438 ) -> Result<(Self, CompactResult), FilesystemError> {
4439 log::debug!(
4441 "[HFS compact] seeking to MDB at offset {}",
4442 partition_offset + 1024
4443 );
4444 reader.seek(SeekFrom::Start(partition_offset + 1024))?;
4445 let mut mdb_buf = [0u8; 162];
4446 reader.read_exact(&mut mdb_buf)?;
4447 let mdb = HfsMasterDirectoryBlock::parse(&mdb_buf).map_err(|e| {
4448 log::debug!("[HFS compact] MDB parse failed: {e}");
4449 e
4450 })?;
4451 log::debug!(
4452 "[HFS compact] MDB ok: block_size={}, total_blocks={}, first_alloc_block={}, volume_bitmap_block={}",
4453 mdb.block_size, mdb.total_blocks, mdb.first_alloc_block, mdb.volume_bitmap_block,
4454 );
4455
4456 if mdb.has_embedded_hfs_plus() {
4457 log::debug!("[HFS compact] has embedded HFS+ — refusing to compact HFS wrapper");
4458 return Err(FilesystemError::Unsupported(
4459 "cannot compact HFS wrapper with embedded HFS+".into(),
4460 ));
4461 }
4462
4463 let bitmap_offset = partition_offset + mdb.volume_bitmap_block as u64 * 512;
4465 log::debug!("[HFS compact] reading bitmap at offset {bitmap_offset}");
4466 let bitmap_size = (mdb.total_blocks as u32).div_ceil(8) as usize;
4467 reader.seek(SeekFrom::Start(bitmap_offset))?;
4468 let mut bitmap = vec![0u8; bitmap_size];
4469 reader.read_exact(&mut bitmap)?;
4470 log::debug!("[HFS compact] bitmap read: {bitmap_size} bytes");
4471
4472 let mut allocated = 0u32;
4474 for bit in 0..mdb.total_blocks as u32 {
4475 let byte_idx = bit as usize / 8;
4476 let bit_idx = 7 - (bit % 8);
4477 if byte_idx < bitmap.len() && (bitmap[byte_idx] >> bit_idx) & 1 == 1 {
4478 allocated += 1;
4479 }
4480 }
4481 log::debug!(
4482 "[HFS compact] allocated={} / {} total blocks ({} free)",
4483 allocated,
4484 mdb.total_blocks,
4485 mdb.total_blocks as u32 - allocated,
4486 );
4487
4488 let pre_alloc_size = mdb.first_alloc_block as u64 * 512;
4490 let original_size = pre_alloc_size + mdb.total_blocks as u64 * mdb.block_size as u64;
4491 let compacted_size = original_size;
4494 let data_size = pre_alloc_size + allocated as u64 * mdb.block_size as u64;
4496 log::debug!(
4497 "[HFS compact] pre_alloc_size={}, data_size={}, compacted_size={} original_size={} (layout-preserving; free blocks -> zeros)",
4498 pre_alloc_size, data_size, compacted_size, original_size
4499 );
4500
4501 let result = CompactResult {
4502 original_size,
4503 compacted_size,
4504 data_size,
4505 clusters_used: allocated,
4506 };
4507
4508 Ok((
4509 CompactHfsReader {
4510 reader,
4511 partition_offset,
4512 mdb,
4513 bitmap,
4514 phase: 0,
4515 pre_alloc_pos: 0,
4516 current_block: 0,
4517 block_pos: 0,
4518 original_size,
4519 allocated_blocks: allocated,
4520 },
4521 result,
4522 ))
4523 }
4524
4525 fn is_block_allocated(&self, block: u32) -> bool {
4526 let byte_idx = block as usize / 8;
4527 let bit_idx = 7 - (block % 8);
4528 byte_idx < self.bitmap.len() && (self.bitmap[byte_idx] >> bit_idx) & 1 == 1
4529 }
4530}
4531
4532impl<R: Read + Seek> Read for CompactHfsReader<R> {
4533 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
4534 if self.phase > 1 {
4535 return Ok(0);
4536 }
4537
4538 if self.phase == 0 {
4539 let pre_alloc_size = self.mdb.first_alloc_block as u64 * 512;
4542 let remaining = pre_alloc_size - self.pre_alloc_pos;
4543 if remaining == 0 {
4544 self.phase = 1;
4545 return self.read(buf);
4546 }
4547 let to_read = (remaining as usize).min(buf.len());
4548 let offset = self.partition_offset + self.pre_alloc_pos;
4549 self.reader
4550 .seek(SeekFrom::Start(offset))
4551 .map_err(disk_core::io_other)?;
4552 let n = self.reader.read(&mut buf[..to_read])?;
4553 self.pre_alloc_pos += n as u64;
4554 return Ok(n);
4555 }
4556
4557 let total_blocks = self.mdb.total_blocks as u32;
4560 if self.current_block >= total_blocks {
4561 self.phase = 2;
4562 return Ok(0);
4563 }
4564
4565 let block_size = self.mdb.block_size as u64;
4566
4567 let allocated = self.is_block_allocated(self.current_block);
4572 let mut run_end = self.current_block + 1;
4573 while run_end < total_blocks && self.is_block_allocated(run_end) == allocated {
4574 run_end += 1;
4575 }
4576 let bytes_in_run = (run_end - self.current_block) as u64 * block_size - self.block_pos;
4577 let to_read = (bytes_in_run as usize).min(buf.len());
4578
4579 let n = if allocated {
4580 let offset = self.partition_offset
4581 + self.mdb.first_alloc_block as u64 * 512
4582 + self.current_block as u64 * block_size
4583 + self.block_pos;
4584 self.reader
4585 .seek(SeekFrom::Start(offset))
4586 .map_err(disk_core::io_other)?;
4587 self.reader.read(&mut buf[..to_read])?
4588 } else {
4589 buf[..to_read].fill(0);
4591 to_read
4592 };
4593
4594 let mut remaining_in_n = n as u64;
4596 if self.block_pos > 0 {
4597 let want = block_size - self.block_pos;
4598 let take = remaining_in_n.min(want);
4599 self.block_pos += take;
4600 remaining_in_n -= take;
4601 if self.block_pos >= block_size {
4602 self.block_pos = 0;
4603 self.current_block += 1;
4604 }
4605 }
4606 let whole = (remaining_in_n / block_size) as u32;
4607 self.current_block += whole;
4608 remaining_in_n -= whole as u64 * block_size;
4609 if remaining_in_n > 0 {
4610 self.block_pos = remaining_in_n;
4611 }
4612
4613 Ok(n)
4614 }
4615}
4616
4617pub fn fit_hfs_volume_to_partition(
4632 device: &mut (impl Read + Write + Seek),
4633 partition_offset: u64,
4634 partition_len: u64,
4635 log: &mut impl FnMut(&str),
4636) -> anyhow::Result<bool> {
4637 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4638 let mut sector = [0u8; 512];
4639 device.read_exact(&mut sector)?;
4640 if BigEndian::read_u16(§or[0..2]) != HFS_SIGNATURE {
4641 return Ok(false);
4642 }
4643 if BigEndian::read_u16(§or[0x7C..0x7E]) == 0x482B {
4645 return Ok(false);
4646 }
4647 let total_blocks = BigEndian::read_u16(§or[0x12..0x14]) as u64;
4648 let block_size = BigEndian::read_u32(§or[0x14..0x18]) as u64;
4649 let first_alloc = BigEndian::read_u16(§or[0x1C..0x1E]) as u64;
4650 let volume_len = first_alloc * 512 + total_blocks * block_size + 1024;
4651 if block_size == 0 || volume_len >= partition_len {
4652 return Ok(false);
4653 }
4654 let growable = hfs_max_growable_size(device, partition_offset).unwrap_or(0);
4655 if growable < partition_len {
4656 log(&format!(
4657 "Warning: the HFS volume ({volume_len} bytes) does not fill its {partition_len}-byte \
4658 partition and its volume bitmap cannot address more blocks; Disk First Aid will \
4659 report an invalid allocation block start. Format the volume at the partition size"
4660 ));
4661 return Ok(false);
4662 }
4663 resize_hfs_in_place(device, partition_offset, partition_len, log)?;
4664 log(&format!(
4665 "HFS volume grown from {volume_len} bytes to fill its {partition_len}-byte partition"
4666 ));
4667 Ok(true)
4668}
4669
4670pub fn hfs_max_growable_size(
4671 device: &mut (impl Read + Seek),
4672 partition_offset: u64,
4673) -> Option<u64> {
4674 device.seek(SeekFrom::Start(partition_offset + 1024)).ok()?;
4675 let mut sector = [0u8; 512];
4676 device.read_exact(&mut sector).ok()?;
4677
4678 let sig = BigEndian::read_u16(§or[0..2]);
4679 if sig != HFS_SIGNATURE {
4680 return None;
4681 }
4682
4683 let block_size = BigEndian::read_u32(§or[20..24]) as u64;
4684 let vbm_start = BigEndian::read_u16(§or[14..16]) as u64;
4685 let first_alloc = BigEndian::read_u16(§or[28..30]) as u64;
4686
4687 if block_size == 0 || first_alloc <= vbm_start {
4688 return None;
4689 }
4690
4691 let vbm_capacity = (first_alloc - vbm_start) * 512 * 8;
4692 let max_blocks = vbm_capacity.min(u16::MAX as u64);
4693 let overhead = first_alloc * 512 + 1024;
4695
4696 Some(max_blocks * block_size + overhead)
4697}
4698
4699pub fn resize_hfs_in_place(
4700 device: &mut (impl Read + Write + Seek),
4701 partition_offset: u64,
4702 new_size_bytes: u64,
4703 log: &mut impl FnMut(&str),
4704) -> anyhow::Result<()> {
4705 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4709 let mut sector = [0u8; 512];
4710 device.read_exact(&mut sector)?;
4711
4712 let sig = BigEndian::read_u16(§or[0..2]);
4713 if sig != HFS_SIGNATURE {
4714 log("HFS resize: not an HFS volume, skipping");
4715 return Ok(());
4716 }
4717
4718 let block_size = BigEndian::read_u32(§or[20..24]);
4719 let old_total = BigEndian::read_u16(§or[18..20]);
4720 let free_blocks = BigEndian::read_u16(§or[34..36]);
4721 let vbm_start = BigEndian::read_u16(§or[14..16]);
4722 let first_alloc = BigEndian::read_u16(§or[28..30]);
4723 let used_blocks = old_total - free_blocks;
4724
4725 let vbm_capacity_blocks = if first_alloc > vbm_start {
4729 (first_alloc - vbm_start) as u64 * 512 * 8
4730 } else {
4731 0
4732 };
4733
4734 let overhead = first_alloc as u64 * 512 + 1024;
4737 if new_size_bytes <= overhead {
4738 anyhow::bail!(
4739 "HFS resize: new size {} bytes <= overhead {} bytes",
4740 new_size_bytes,
4741 overhead
4742 );
4743 }
4744 let new_total_u64 = (new_size_bytes - overhead) / block_size as u64;
4745
4746 if new_total_u64 > u16::MAX as u64 {
4749 let max_bytes = u16::MAX as u64 * block_size as u64 + overhead;
4750 anyhow::bail!(
4751 "HFS resize: requested size {} MB needs {} allocation blocks at {}-byte blocks, \
4752 but HFS caps at 65535 blocks (max {} MB for this volume). \
4753 Growing further would require reformatting with a larger allocation block size.",
4754 new_size_bytes / 1024 / 1024,
4755 new_total_u64,
4756 block_size,
4757 max_bytes / 1024 / 1024,
4758 );
4759 }
4760 if new_total_u64 > vbm_capacity_blocks {
4761 anyhow::bail!(
4762 "HFS resize: requested {} allocation blocks exceeds the Volume Bitmap capacity \
4763 ({} blocks) for this volume. The VBM lives in fixed sectors [{}..{}) and cannot \
4764 be grown without reformatting: the most this volume can grow to is {} bytes. \
4765 Mac OS wants a classic HFS volume to fill its partition, so format one at the \
4766 partition size instead (`new volume hfs --size`, then `new hd --fill`).",
4767 new_total_u64,
4768 vbm_capacity_blocks,
4769 vbm_start,
4770 first_alloc,
4771 vbm_capacity_blocks.min(u16::MAX as u64) * block_size as u64 + overhead,
4772 );
4773 }
4774 let new_total = new_total_u64 as u16;
4775
4776 let vbm_len = (first_alloc - vbm_start) as usize * 512;
4779 let mut vbm = vec![0u8; vbm_len];
4780 device.seek(SeekFrom::Start(partition_offset + vbm_start as u64 * 512))?;
4781 device.read_exact(&mut vbm)?;
4782 let highest_used = find_last_set_bit(&vbm, old_total as u32).map_or(0, |b| b + 1);
4783 if (new_total as u32) < highest_used {
4784 anyhow::bail!(
4785 "HFS resize: new size {} blocks ends before the last allocated block ({}); \
4786 {} of {} blocks are in use",
4787 new_total,
4788 highest_used,
4789 used_blocks,
4790 old_total
4791 );
4792 }
4793 for b in old_total.min(new_total) as u32..old_total.max(new_total) as u32 {
4796 bitmap_clear_bit_be(&mut vbm, b);
4797 }
4798 device.seek(SeekFrom::Start(partition_offset + vbm_start as u64 * 512))?;
4799 device.write_all(&vbm)?;
4800
4801 let new_free = new_total - used_blocks;
4802
4803 log(&format!(
4804 "HFS resize: {} -> {} blocks ({} free)",
4805 old_total, new_total, new_free
4806 ));
4807
4808 BigEndian::write_u16(&mut sector[18..20], new_total);
4810 BigEndian::write_u16(&mut sector[34..36], new_free);
4811
4812 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4814 device.write_all(§or)?;
4815
4816 if new_size_bytes > 1024 {
4818 device.seek(SeekFrom::Start(partition_offset + new_size_bytes - 1024))?;
4819 device.write_all(§or)?;
4820 }
4821
4822 device.flush()?;
4823 Ok(())
4824}
4825
4826pub fn validate_hfs_integrity(
4828 device: &mut (impl Read + Seek),
4829 partition_offset: u64,
4830 log: &mut impl FnMut(&str),
4831) -> anyhow::Result<()> {
4832 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4834 let mut sector = [0u8; 512];
4835 device.read_exact(&mut sector)?;
4836
4837 let sig = BigEndian::read_u16(§or[0..2]);
4838 if sig != HFS_SIGNATURE {
4839 log("HFS validate: not an HFS volume, skipping");
4840 return Ok(());
4841 }
4842
4843 let block_size = BigEndian::read_u32(§or[20..24]);
4844 let total_blocks = BigEndian::read_u16(§or[18..20]);
4845
4846 if !block_size.is_power_of_two() || block_size < 512 {
4848 anyhow::bail!("HFS validate: invalid block size {block_size}");
4849 }
4850 if total_blocks == 0 {
4851 anyhow::bail!("HFS validate: zero total blocks");
4852 }
4853
4854 log(&format!(
4855 "HFS validate: OK ({total_blocks} blocks, {} block size)",
4856 block_size
4857 ));
4858 Ok(())
4859}
4860
4861#[cfg(test)]
4862#[allow(clippy::identity_op)] mod tests {
4864 use super::*;
4865 use std::io::Cursor;
4866
4867 #[test]
4871 fn write_blank_hfs_into_matches_create_blank_hfs() {
4872 let (size, bs) = (8 * 1024 * 1024u64, 4096u32);
4873 let now = 0xB0A1_2345;
4874 let mem = create_blank_hfs_sized_at(size, bs, "StreamEq", 0, 0, now).unwrap();
4875
4876 let mut sink = Cursor::new(vec![0u8; 0]);
4877 let written = write_blank_hfs_into_at(&mut sink, 0, size, bs, "StreamEq", now).unwrap();
4878 let mut streamed = sink.into_inner();
4879 streamed.resize(written as usize, 0);
4880
4881 assert_eq!(written as usize, mem.len());
4882 assert_eq!(streamed, mem);
4883 }
4884
4885 #[test]
4888 fn write_blank_hfs_into_honours_the_offset() {
4889 let (size, bs) = (4 * 1024 * 1024u64, 2048u32);
4890 let now = 0xB0A1_2345;
4891 let mem = create_blank_hfs_sized_at(size, bs, "Offset", 0, 0, now).unwrap();
4892
4893 let offset = 64 * 512u64;
4894 let mut sink = Cursor::new(vec![0u8; 0]);
4895 let written = write_blank_hfs_into_at(&mut sink, offset, size, bs, "Offset", now).unwrap();
4896 let mut streamed = sink.into_inner();
4897 streamed.resize((offset + written) as usize, 0);
4898
4899 assert!(streamed[..offset as usize].iter().all(|&b| b == 0));
4900 assert_eq!(&streamed[offset as usize..], &mem[..]);
4901 }
4902
4903 #[test]
4904 fn test_mac_roman_to_utf8_ascii() {
4905 let data = b"Hello World";
4906 assert_eq!(mac_roman_to_utf8(data), "Hello World");
4907 }
4908
4909 #[test]
4910 fn test_mac_roman_to_utf8_special() {
4911 let data = [0x80, 0x81, 0x87];
4913 assert_eq!(mac_roman_to_utf8(&data), "ÄÅá");
4914 }
4915
4916 #[test]
4917 fn test_decode_mac_filename_utf8() {
4918 let mut data = b"HeapFixer".to_vec();
4921 data.extend_from_slice(&[0xE2, 0x84, 0xA2]);
4922 data.extend_from_slice(b" 1.01");
4923 assert_eq!(decode_mac_filename(&data), "HeapFixer™ 1.01");
4924 }
4925
4926 #[test]
4927 fn test_decode_mac_filename_macroman() {
4928 assert_eq!(decode_mac_filename(&[0xC4]), "ƒ");
4931 assert_eq!(decode_mac_filename(b"Cache\xA5Flow"), "Cache•Flow");
4932 assert_eq!(decode_mac_filename(b"Virex\xA8 3.51"), "Virex® 3.51");
4933 }
4934
4935 #[test]
4936 fn test_decode_mac_filename_ascii() {
4937 assert_eq!(decode_mac_filename(b"ScriptEdit"), "ScriptEdit");
4938 }
4939
4940 #[test]
4941 fn test_format_ostype() {
4942 assert_eq!(format_ostype(&[b'S', 0xA5, b'L', b' ']), "S•L");
4944 assert_eq!(format_ostype(b"APPL"), "APPL");
4945 assert_eq!(format_ostype(&[b'L', b's', 0x00, 0x00]), "Ls");
4947 }
4948
4949 #[test]
4950 fn test_mdb_parse() {
4951 let mut data = [0u8; 162];
4952 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
4954 BigEndian::write_u16(&mut data[18..20], 1000);
4956 BigEndian::write_u32(&mut data[20..24], 4096);
4958 BigEndian::write_u16(&mut data[34..36], 200);
4960 data[36] = 7;
4962 data[37..44].copy_from_slice(b"TestVol");
4963
4964 let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
4965 assert_eq!(mdb.signature, HFS_SIGNATURE);
4966 assert_eq!(mdb.total_blocks, 1000);
4967 assert_eq!(mdb.block_size, 4096);
4968 assert_eq!(mdb.free_blocks, 200);
4969 assert_eq!(mdb.volume_name, "TestVol");
4970 assert!(!mdb.has_embedded_hfs_plus());
4971 }
4972
4973 #[test]
4974 fn test_mdb_detect_embedded_hfs_plus() {
4975 let mut data = [0u8; 162];
4976 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
4977 BigEndian::write_u16(&mut data[18..20], 1000);
4978 BigEndian::write_u32(&mut data[20..24], 4096);
4979 BigEndian::write_u16(&mut data[124..126], HFS_PLUS_EMBEDDED_SIGNATURE);
4981 BigEndian::write_u16(&mut data[126..128], 10); BigEndian::write_u16(&mut data[128..130], 500); let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
4985 assert!(mdb.has_embedded_hfs_plus());
4986 }
4987
4988 #[test]
4989 fn test_find_last_set_bit() {
4990 let bitmap = [0b10100000, 0b00001000]; assert_eq!(find_last_set_bit(&bitmap, 16), Some(12));
4992
4993 let bitmap_empty = [0u8; 2];
4994 assert_eq!(find_last_set_bit(&bitmap_empty, 16), None);
4995
4996 let bitmap_first = [0b10000000]; assert_eq!(find_last_set_bit(&bitmap_first, 8), Some(0));
4998 }
4999
5000 #[test]
5001 fn test_utf8_to_mac_roman_roundtrip() {
5002 let text = "Hello World";
5003 let mac = utf8_to_mac_roman(text).unwrap();
5004 assert_eq!(mac, b"Hello World");
5005 assert_eq!(mac_roman_to_utf8(&mac), text);
5006 }
5007
5008 #[test]
5009 fn test_utf8_to_mac_roman_special() {
5010 let text = "Ä"; let mac = utf8_to_mac_roman(text).unwrap();
5012 assert_eq!(mac, &[0x80]);
5013 assert_eq!(mac_roman_to_utf8(&mac), text);
5014 }
5015
5016 #[test]
5017 fn test_utf8_to_mac_roman_unencodable() {
5018 let text = "\u{4E2D}"; assert!(utf8_to_mac_roman(text).is_err());
5020 }
5021
5022 #[test]
5024 fn tiny_catalog_node_size_does_not_panic() {
5025 use disk_core::filesystem::Filesystem;
5026 let clean = create_blank_hfs(1474560, 512, "Tiny").unwrap();
5027 let mdb = {
5028 let fs = HfsFilesystem::open(Cursor::new(clean.clone()), 0).unwrap();
5029 fs.mdb.clone()
5030 };
5031 let cat_off = mdb.first_alloc_block as usize * 512
5032 + mdb.catalog_file_extents[0].start_block as usize * mdb.block_size as usize;
5033 for bad in [1u16, 3, 9, 14, 17, 27, 28, 511] {
5034 let mut img = clean.clone();
5035 img[cat_off + 32..cat_off + 34].copy_from_slice(&bad.to_be_bytes());
5036 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5037 let root = fs.root().unwrap();
5038 let listed = fs.list_directory(&root);
5039 assert!(
5040 listed.map(|v| v.is_empty()).unwrap_or(true),
5041 "node size {bad}: expected an empty listing or an error"
5042 );
5043 }
5044 }
5045
5046 #[test]
5051 fn classic_hfs_stamps_are_local_time_and_round_trip_to_utc() {
5052 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5053 use disk_core::times::{local_utc_offset_secs, unix_to_mac_epoch, UnixTimes};
5054 const T: u64 = 1_700_000_000; let mut img = create_blank_hfs(4 * 1024 * 1024, 4096, "Zone").unwrap();
5056 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5057 let root = fs.root().unwrap();
5058 let opts = CreateFileOptions {
5059 unix_times: Some(UnixTimes::mtime_only(T)),
5060 ..Default::default()
5061 };
5062 fs.create_file(&root, "stamp", &mut &b"x"[..], 1, &opts)
5063 .unwrap();
5064 fs.sync_metadata().unwrap();
5065 let entry = fs
5066 .list_directory(&root)
5067 .unwrap()
5068 .into_iter()
5069 .find(|e| e.name == "stamp")
5070 .unwrap();
5071 assert_eq!(entry.modified_unix, Some(T), "UTC must round-trip");
5072 let raw = entry.mac_dates.expect("raw Mac dates").1;
5073 let expected = (T as i64 + local_utc_offset_secs()) as u64;
5074 assert_eq!(
5075 raw,
5076 unix_to_mac_epoch(expected),
5077 "on disk it is the local wall clock"
5078 );
5079 }
5080
5081 #[test]
5085 fn the_alternate_mdb_follows_the_partition_end_when_the_length_is_known() {
5086 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5087 const VOLUME: u64 = 8 * 1024 * 1024;
5088 const PARTITION: u64 = VOLUME + 64 * 1024;
5089 let mut img = create_blank_hfs(VOLUME, 4096, "Slack").unwrap();
5090 img.resize(PARTITION as usize, 0);
5091 {
5092 let mut fs =
5093 HfsFilesystem::open_sized(Cursor::new(&mut img), 0, Some(PARTITION)).unwrap();
5094 let root = fs.root().unwrap();
5095 fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
5096 .unwrap();
5097 fs.sync_metadata().unwrap();
5098 }
5099 let primary = &img[1024..1536];
5100 let alt = &img[PARTITION as usize - 1024..PARTITION as usize - 512];
5101 assert_eq!(
5102 &alt[0..2],
5103 b"BD",
5104 "alternate MDB missing at the partition end"
5105 );
5106 assert_eq!(primary, alt, "the mirror must equal the primary MDB");
5107 }
5108
5109 #[test]
5112 fn create_file_writes_the_finder_flags_it_is_given() {
5113 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5114 let mut img = create_blank_hfs(4 * 1024 * 1024, 4096, "Flags").unwrap();
5115 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5116 let root = fs.root().unwrap();
5117 let opts = CreateFileOptions {
5118 os_type: Some(*b"APPL"),
5119 os_creator: Some(*b"ttxt"),
5120 finder_flags: Some(0x2000),
5121 ..Default::default()
5122 };
5123 fs.create_file(&root, "app", &mut &b"x"[..], 1, &opts)
5124 .unwrap();
5125 fs.sync_metadata().unwrap();
5126 let entry = fs
5127 .list_directory(&root)
5128 .unwrap()
5129 .into_iter()
5130 .find(|e| e.name == "app")
5131 .unwrap();
5132 assert_eq!(entry.finder_flags, Some(0x2000));
5133 }
5134
5135 #[test]
5136 fn shrink_is_refused_when_data_sits_past_the_new_end() {
5137 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5138 const SIZE: u64 = 8 * 1024 * 1024;
5139 let mut img = create_blank_hfs(SIZE, 4096, "Tail").unwrap();
5140 {
5141 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5142 let root = fs.root().unwrap();
5143 fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
5144 .unwrap();
5145 fs.sync_metadata().unwrap();
5146 }
5147 let total = BigEndian::read_u16(&img[1024 + 18..1024 + 20]) as usize;
5148 let vbm_start = BigEndian::read_u16(&img[1024 + 14..1024 + 16]) as usize;
5149 let tail = total - 1;
5151 let byte = vbm_start * 512 + tail / 8;
5152 img[byte] |= 0x80 >> (tail % 8);
5153 let err = resize_hfs_in_place(&mut Cursor::new(&mut img), 0, SIZE / 2, &mut |_| {})
5154 .expect_err("a tail block in use must refuse the shrink");
5155 assert!(format!("{err}").contains("last allocated block"), "{err}");
5156 img[byte] &= !(0x80 >> (tail % 8));
5157 resize_hfs_in_place(&mut Cursor::new(&mut img), 0, SIZE / 2, &mut |_| {})
5158 .expect("with the tail free the shrink goes through");
5159 }
5160
5161 #[test]
5163 fn last_data_byte_is_a_size_the_volume_shrinks_to() {
5164 use disk_core::filesystem::Filesystem;
5165 const SIZE: u64 = 8 * 1024 * 1024;
5166 let mut img = create_blank_hfs(SIZE, 4096, "Tail").unwrap();
5167 let total = BigEndian::read_u16(&img[1024 + 18..1024 + 20]) as usize;
5168 let vbm_start = BigEndian::read_u16(&img[1024 + 14..1024 + 16]) as usize;
5169 let free = BigEndian::read_u16(&img[1024 + 34..1024 + 36]);
5170 let last = total / 2;
5172 img[vbm_start * 512 + last / 8] |= 0x80 >> (last % 8);
5173 BigEndian::write_u16(&mut img[1024 + 34..1024 + 36], free - 1);
5174 let floor = {
5175 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5176 fs.last_data_byte().unwrap()
5177 };
5178 resize_hfs_in_place(&mut Cursor::new(&mut img), 0, floor, &mut |_| {})
5179 .expect("a shrink to last_data_byte must keep the last allocated block");
5180 let new_total = BigEndian::read_u16(&img[1024 + 18..1024 + 20]) as usize;
5181 assert_eq!(
5182 new_total,
5183 last + 1,
5184 "the floor holds exactly the used blocks"
5185 );
5186 }
5187
5188 #[test]
5189 fn test_resize_hfs_rejects_u16_overflow() {
5190 let mut sector = [0u8; 512];
5194 BigEndian::write_u16(&mut sector[0..2], HFS_SIGNATURE);
5195 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];
5207 img[1024..1024 + 512].copy_from_slice(§or);
5208
5209 let mut cursor = Cursor::new(img);
5210 let res = resize_hfs_in_place(
5211 &mut cursor,
5212 0,
5213 2047 * 1024 * 1024, &mut |_| {},
5215 );
5216 let err = res.expect_err("resize should have bailed on u16 overflow");
5217 let msg = format!("{err}");
5218 assert!(
5219 msg.contains("65535") || msg.contains("u16") || msg.contains("Volume Bitmap"),
5220 "unexpected error: {msg}"
5221 );
5222 }
5223
5224 #[test]
5225 fn test_mdb_serialize_roundtrip() {
5226 let mut data = [0u8; 512];
5227 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
5228 BigEndian::write_u16(&mut data[18..20], 100);
5229 BigEndian::write_u32(&mut data[20..24], 4096);
5230 BigEndian::write_u16(&mut data[34..36], 50);
5231 BigEndian::write_u32(&mut data[30..34], 10); BigEndian::write_u32(&mut data[92..96], 42); data[36] = 4;
5234 data[37..41].copy_from_slice(b"Test");
5235
5236 let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
5237 let serialized = mdb.serialize_to_sector();
5238 assert_eq!(BigEndian::read_u16(&serialized[18..20]), 100);
5239 assert_eq!(BigEndian::read_u32(&serialized[30..34]), 10);
5240 assert_eq!(BigEndian::read_u32(&serialized[92..96]), 42);
5241 }
5242
5243 fn make_editable_hfs_image() -> Vec<u8> {
5255 let block_size = 4096u32;
5256 let total_blocks = 128u16;
5257 let first_alloc_block = 5u16; let alloc_start = first_alloc_block as usize * 512;
5259 let image_size = alloc_start + total_blocks as usize * block_size as usize;
5260 let mut img = vec![0u8; image_size];
5261
5262 let catalog_start = 0u16; let catalog_blocks = 8u16;
5264 let catalog_size = catalog_blocks as u32 * block_size;
5265
5266 let bitmap_sector = 3u16;
5268 let bitmap_off = bitmap_sector as usize * 512;
5269 img[bitmap_off] = 0b11111111;
5271
5272 let node_size = 4096usize;
5274 let catalog_offset = alloc_start; let hdr_off = catalog_offset;
5278 img[hdr_off + 8] = 1; BigEndian::write_u16(&mut img[hdr_off + 10..hdr_off + 12], 3); let hr = hdr_off + 14;
5283 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;
5295 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;
5302
5303 let leaf_off = catalog_offset + node_size;
5305 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;
5313 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;
5319 let r0_data = r0_off + 8;
5321 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;
5328 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;
5334 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;
5344 BigEndian::write_u16(&mut img[lot - 2..lot], 14); let r1_rel = (r1_off - leaf_off) as u16;
5346 BigEndian::write_u16(&mut img[lot - 4..lot - 2], r1_rel); let free_rel = (r1_data + 46 - leaf_off) as u16;
5348 BigEndian::write_u16(&mut img[lot - 6..lot - 4], free_rel); let mdb_off = 1024;
5352 let mut mdb = [0u8; 512];
5353 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
5354 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);
5358 BigEndian::write_u32(&mut mdb[20..24], block_size);
5359 BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
5360 BigEndian::write_u32(&mut mdb[30..34], 16); BigEndian::write_u16(&mut mdb[34..36], total_blocks - catalog_blocks); mdb[36] = 7;
5364 mdb[37..44].copy_from_slice(b"TestVol");
5365 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);
5369 BigEndian::write_u16(&mut mdb[150..152], catalog_start);
5371 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
5372
5373 img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
5374
5375 img
5376 }
5377
5378 fn make_uninitialized_hfs_image() -> Vec<u8> {
5383 let block_size = 512u32;
5384 let total_blocks = 256u16;
5385 let first_alloc_block = 6u16;
5386 let alloc_start = first_alloc_block as usize * 512;
5387 let image_size = alloc_start + total_blocks as usize * block_size as usize;
5388 let mut img = vec![0u8; image_size];
5389
5390 let extents_start = 0u16;
5392 let extents_blocks = 4u16;
5393 let catalog_start = 4u16;
5395 let catalog_blocks = 4u16;
5396
5397 let mdb_off = 1024usize;
5400 let mut mdb = [0u8; 512];
5401 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
5402 BigEndian::write_u32(&mut mdb[2..6], hfs_common::hfs_now());
5403 BigEndian::write_u32(&mut mdb[6..10], hfs_common::hfs_now());
5404 BigEndian::write_u16(&mut mdb[14..16], 3); BigEndian::write_u16(&mut mdb[18..20], total_blocks);
5406 BigEndian::write_u32(&mut mdb[20..24], block_size);
5407 BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
5408 BigEndian::write_u32(&mut mdb[30..34], 16);
5409 BigEndian::write_u16(
5410 &mut mdb[34..36],
5411 total_blocks - extents_blocks - catalog_blocks,
5412 );
5413 mdb[36] = 5;
5415 mdb[37..42].copy_from_slice(b"Blank");
5416 BigEndian::write_u32(&mut mdb[130..134], extents_blocks as u32 * block_size);
5418 BigEndian::write_u16(&mut mdb[134..136], extents_start);
5419 BigEndian::write_u16(&mut mdb[136..138], extents_blocks);
5420 BigEndian::write_u32(&mut mdb[146..150], catalog_blocks as u32 * block_size);
5422 BigEndian::write_u16(&mut mdb[150..152], catalog_start);
5423 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
5424
5425 img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
5426 img
5427 }
5428
5429 #[test]
5430 fn test_hfs_empty_volume_auto_initialize_and_create_file() {
5431 let img = make_uninitialized_hfs_image();
5434 let cursor = Cursor::new(img);
5435 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5436 assert_eq!(fs.fs_type(), "HFS");
5437
5438 let root = fs.root().unwrap();
5440 let entries = fs.list_directory(&root).unwrap();
5441 assert!(entries.is_empty());
5442
5443 let data = b"auto-init test";
5445 let mut cursor = Cursor::new(data.as_slice());
5446 let fe = fs
5447 .create_file(
5448 &root,
5449 "hello.txt",
5450 &mut cursor,
5451 data.len() as u64,
5452 &CreateFileOptions::default(),
5453 )
5454 .unwrap();
5455 assert_eq!(fe.name, "hello.txt");
5456 fs.sync_metadata().unwrap();
5457
5458 let entries = fs.list_directory(&root).unwrap();
5460 assert_eq!(entries.len(), 1);
5461 assert_eq!(entries[0].name, "hello.txt");
5462 let read_back = fs.read_file(&fe, 1024).unwrap();
5463 assert_eq!(&read_back, data);
5464
5465 let result = fs.fsck().unwrap();
5467 let err_msgs: Vec<_> = result.errors.iter().map(|e| e.message.clone()).collect();
5468 let warn_msgs: Vec<_> = result.warnings.iter().map(|w| w.message.clone()).collect();
5469 assert!(
5470 result.errors.is_empty(),
5471 "unexpected fsck errors: {err_msgs:?}"
5472 );
5473 assert!(
5474 result.warnings.is_empty(),
5475 "unexpected fsck warnings: {warn_msgs:?}"
5476 );
5477 }
5478
5479 #[test]
5480 fn test_hfs_editable_open_and_free_space() {
5481 let img = make_editable_hfs_image();
5482 let cursor = Cursor::new(img);
5483 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5484 assert_eq!(fs.fs_type(), "HFS");
5485 assert_eq!(fs.volume_label(), Some("TestVol"));
5486 let free = fs.free_space().unwrap();
5487 assert_eq!(free, 120 * 4096);
5489 }
5490
5491 #[test]
5492 fn test_hfs_editable_create_file_and_read() {
5493 let img = make_editable_hfs_image();
5494 let cursor = Cursor::new(img);
5495 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5496
5497 let root = fs.root().unwrap();
5498 let test_data = b"Hello, HFS World!";
5499 let mut data_reader = Cursor::new(test_data.as_slice());
5500 let options = CreateFileOptions::default();
5501 let fe = fs
5502 .create_file(
5503 &root,
5504 "test.txt",
5505 &mut data_reader,
5506 test_data.len() as u64,
5507 &options,
5508 )
5509 .unwrap();
5510
5511 assert_eq!(fe.name, "test.txt");
5512 assert_eq!(fe.size, test_data.len() as u64);
5513
5514 let entries = fs.list_directory(&root).unwrap();
5516 assert!(entries.iter().any(|e| e.name == "test.txt"));
5517
5518 let read_back = fs.read_file(&fe, 1024).unwrap();
5520 assert_eq!(&read_back, test_data);
5521 }
5522
5523 #[test]
5531 fn test_hfs_file_record_first_alloc_block_fields_are_zero() {
5532 use crate::hfs_common::walk_leaf_records;
5533 use disk_core::filesystem::ResourceForkSource;
5534
5535 let img = make_editable_hfs_image();
5536 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5537 let root = fs.root().unwrap();
5538
5539 let data = b"data fork contents that span a block";
5542 let options = CreateFileOptions {
5543 resource_fork: Some(ResourceForkSource::Data(b"resource fork bytes".to_vec())),
5544 ..Default::default()
5545 };
5546 fs.create_file(
5547 &root,
5548 "forked.bin",
5549 &mut Cursor::new(data.as_slice()),
5550 data.len() as u64,
5551 &options,
5552 )
5553 .unwrap();
5554 fs.sync_metadata().unwrap();
5555
5556 let node_size = BigEndian::read_u16(&fs.catalog_data[32..34]) as usize;
5557 let first_leaf = BigEndian::read_u32(&fs.catalog_data[24..28]);
5558 let found = walk_leaf_records::<bool, _>(
5559 &fs.catalog_data,
5560 first_leaf,
5561 node_size,
5562 |_n, _i, _off, rec| {
5563 let klen = rec[0] as usize;
5564 let mut ds = 1 + klen;
5565 if !ds.is_multiple_of(2) {
5566 ds += 1;
5567 }
5568 let body = &rec[ds..];
5569 if body.first() != Some(&2) {
5570 return None; }
5572 let fil_st_blk = BigEndian::read_u16(&body[24..26]);
5573 let fil_rst_blk = BigEndian::read_u16(&body[34..36]);
5574 let data_ext_start = BigEndian::read_u16(&body[74..76]);
5575 let rsrc_ext_start = BigEndian::read_u16(&body[86..88]);
5576 assert_eq!(fil_st_blk, 0, "filStBlk (offset 24) must be 0 on disk");
5577 assert_eq!(fil_rst_blk, 0, "filRStBlk (offset 34) must be 0 on disk");
5578 assert_ne!(data_ext_start, 0, "data fork extent should be allocated");
5580 assert_ne!(rsrc_ext_start, 0, "rsrc fork extent should be allocated");
5581 Some(true)
5582 },
5583 );
5584 assert_eq!(found, Some(true), "file record not found in catalog");
5585 }
5586
5587 #[test]
5592 fn test_hfs_catalog_dates_round_trip() {
5593 let img = make_editable_hfs_image();
5594 let mut buf = img.clone();
5595
5596 {
5598 let mut fs = HfsFilesystem::open(Cursor::new(&mut buf), 0).unwrap();
5599 let root = fs.root().unwrap();
5600 let data = b"x";
5601 let fe = fs
5602 .create_file(
5603 &root,
5604 "dated.txt",
5605 &mut Cursor::new(data.as_slice()),
5606 1,
5607 &CreateFileOptions::default(),
5608 )
5609 .unwrap();
5610 fs.sync_metadata().unwrap();
5611
5612 let listed = fs.list_directory(&root).unwrap();
5613 let f = listed.iter().find(|e| e.name == "dated.txt").unwrap();
5614 let (c, m, b) = f.mac_dates.expect("HFS file should carry mac_dates");
5615 assert!(c != 0 && m != 0, "create/modify stamped by create_file");
5616 assert_eq!(b, 0, "backup date defaults to 0");
5617
5618 let target = (0x10000000u32, 0x20000000u32, 0x30000000u32);
5620 fs.set_dates(&fe, target.0, target.1, target.2).unwrap();
5621 fs.sync_metadata().unwrap();
5622 }
5623
5624 let mut fs = HfsFilesystem::open(Cursor::new(&mut buf), 0).unwrap();
5627 let root = fs.root().unwrap();
5628 let listed = fs.list_directory(&root).unwrap();
5629 let f = listed.iter().find(|e| e.name == "dated.txt").unwrap();
5630 assert_eq!(f.mac_dates, Some((0x10000000, 0x20000000, 0x30000000)));
5631 assert_eq!(f.modified, hfs_common::format_mac_date(0x20000000));
5632 }
5633
5634 #[test]
5635 fn test_hfs_editable_create_directory() {
5636 let img = make_editable_hfs_image();
5637 let cursor = Cursor::new(img);
5638 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5639
5640 let root = fs.root().unwrap();
5641 let options = CreateDirectoryOptions::default();
5642 let dir = fs.create_directory(&root, "NewDir", &options).unwrap();
5643
5644 assert_eq!(dir.name, "NewDir");
5645 assert!(dir.is_directory());
5646
5647 let entries = fs.list_directory(&root).unwrap();
5648 assert!(entries
5649 .iter()
5650 .any(|e| e.name == "NewDir" && e.is_directory()));
5651 }
5652
5653 #[test]
5654 fn test_hfs_editable_rename_file() {
5655 let img = make_editable_hfs_image();
5656 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5657 let root = fs.root().unwrap();
5658
5659 let data = b"keep my bytes across a rename";
5660 let mut r = Cursor::new(data.as_slice());
5661 let fe = fs
5662 .create_file(
5663 &root,
5664 "Before",
5665 &mut r,
5666 data.len() as u64,
5667 &CreateFileOptions::default(),
5668 )
5669 .unwrap();
5670 let cnid = fe.location;
5671
5672 fs.rename(&root, &fe, "After Doc").unwrap();
5673
5674 let entries = fs.list_directory(&root).unwrap();
5675 assert!(entries.iter().any(|e| e.name == "After Doc"), "{entries:?}");
5676 assert!(!entries.iter().any(|e| e.name == "Before"), "{entries:?}");
5677 let renamed = entries.iter().find(|e| e.name == "After Doc").unwrap();
5678 assert_eq!(renamed.location, cnid);
5681 assert_eq!(&fs.read_file(renamed, 1024).unwrap(), data);
5682
5683 fs.create_directory(&root, "OldDir", &CreateDirectoryOptions::default())
5686 .unwrap();
5687 let old_dir = fs
5688 .list_directory(&root)
5689 .unwrap()
5690 .into_iter()
5691 .find(|e| e.name == "OldDir")
5692 .unwrap();
5693 fs.rename(&root, &old_dir, "NewDir").unwrap();
5694 let new_dir = fs
5695 .list_directory(&root)
5696 .unwrap()
5697 .into_iter()
5698 .find(|e| e.name == "NewDir")
5699 .expect("renamed dir present");
5700 assert!(fs.list_directory(&new_dir).is_ok());
5701
5702 let after = fs
5704 .list_directory(&root)
5705 .unwrap()
5706 .into_iter()
5707 .find(|e| e.name == "After Doc")
5708 .unwrap();
5709 assert!(matches!(
5710 fs.rename(&root, &after, "NewDir"),
5711 Err(FilesystemError::AlreadyExists(_))
5712 ));
5713
5714 fs.sync_metadata().unwrap();
5715 }
5716
5717 #[test]
5718 fn test_hfs_editable_delete_file() {
5719 let img = make_editable_hfs_image();
5720 let cursor = Cursor::new(img);
5721 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5722
5723 let root = fs.root().unwrap();
5724 let test_data = b"delete me";
5725 let mut data_reader = Cursor::new(test_data.as_slice());
5726 let options = CreateFileOptions::default();
5727 let fe = fs
5728 .create_file(
5729 &root,
5730 "gone.txt",
5731 &mut data_reader,
5732 test_data.len() as u64,
5733 &options,
5734 )
5735 .unwrap();
5736
5737 let free_before = fs.free_space().unwrap();
5738 fs.delete_entry(&root, &fe).unwrap();
5739
5740 let entries = fs.list_directory(&root).unwrap();
5741 assert!(!entries.iter().any(|e| e.name == "gone.txt"));
5742
5743 let free_after = fs.free_space().unwrap();
5744 assert!(free_after > free_before);
5745 }
5746
5747 #[test]
5748 fn test_hfs_editable_duplicate_name_rejected() {
5749 let img = make_editable_hfs_image();
5750 let cursor = Cursor::new(img);
5751 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5752
5753 let root = fs.root().unwrap();
5754 let test_data = b"first";
5755 let mut r1 = Cursor::new(test_data.as_slice());
5756 let options = CreateFileOptions::default();
5757 fs.create_file(&root, "dup.txt", &mut r1, 5, &options)
5758 .unwrap();
5759
5760 let mut r2 = Cursor::new(test_data.as_slice());
5761 let result = fs.create_file(&root, "dup.txt", &mut r2, 5, &options);
5762 assert!(matches!(result, Err(FilesystemError::AlreadyExists(_))));
5763 }
5764
5765 #[test]
5766 fn test_hfs_editable_name_too_long() {
5767 let img = make_editable_hfs_image();
5768 let cursor = Cursor::new(img);
5769 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5770
5771 let root = fs.root().unwrap();
5772 let long_name = "a".repeat(32); let test_data = b"x";
5774 let mut r = Cursor::new(test_data.as_slice());
5775 let options = CreateFileOptions::default();
5776 let result = fs.create_file(&root, &long_name, &mut r, 1, &options);
5777 assert!(matches!(result, Err(FilesystemError::InvalidData(_))));
5778 }
5779
5780 #[test]
5781 fn test_hfs_editable_blessed_folder() {
5782 let img = make_editable_hfs_image();
5783 let cursor = Cursor::new(img);
5784 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5785
5786 assert!(fs.blessed_system_folder().is_none());
5788
5789 let root = fs.root().unwrap();
5791 let options = CreateDirectoryOptions::default();
5792 let sys_dir = fs.create_directory(&root, "System", &options).unwrap();
5793
5794 fs.set_blessed_folder(&sys_dir).unwrap();
5796
5797 let blessed = fs.blessed_system_folder();
5799 assert!(blessed.is_some());
5800 let (cnid, name) = blessed.unwrap();
5801 assert_eq!(cnid, sys_dir.location);
5802 assert_eq!(name, "System");
5803 }
5804
5805 #[test]
5806 fn test_hfs_editable_type_creator_auto_detect() {
5807 let img = make_editable_hfs_image();
5808 let cursor = Cursor::new(img);
5809 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5810
5811 let root = fs.root().unwrap();
5812 let test_data = b"text content";
5813 let mut data_reader = Cursor::new(test_data.as_slice());
5814 let options = CreateFileOptions::default();
5815 let fe = fs
5816 .create_file(
5817 &root,
5818 "readme.txt",
5819 &mut data_reader,
5820 test_data.len() as u64,
5821 &options,
5822 )
5823 .unwrap();
5824
5825 assert!(fe.type_code.is_some());
5826 assert_eq!(fe.type_code, Some(*b"TEXT"));
5827 }
5828
5829 fn assert_fsck_clean<R: Read + Seek>(fs: &mut HfsFilesystem<R>) {
5831 let result = fs.fsck().unwrap();
5832 if !result.is_clean() {
5833 let msgs: Vec<_> = result
5834 .errors
5835 .iter()
5836 .map(|e| format!("[{}] {}", e.code, e.message))
5837 .collect();
5838 panic!("fsck found {} error(s):\n{}", msgs.len(), msgs.join("\n"));
5839 }
5840 }
5841
5842 #[test]
5843 fn test_fsck_clean_fresh_image() {
5844 let img = make_editable_hfs_image();
5845 let cursor = Cursor::new(img);
5846 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5847
5848 let result = fs.fsck().unwrap();
5849 let err_msgs: Vec<&str> = result.errors.iter().map(|e| e.message.as_str()).collect();
5850 assert!(
5851 result.is_clean(),
5852 "fresh image should be clean: {:?}",
5853 err_msgs
5854 );
5855 assert_eq!(result.stats.directories_checked, 1); assert_eq!(result.stats.files_checked, 0);
5857 }
5858
5859 #[test]
5860 fn test_fsck_clean_after_create_file() {
5861 let img = make_editable_hfs_image();
5862 let cursor = Cursor::new(img);
5863 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5864
5865 let root = fs.root().unwrap();
5866 let test_data = b"Hello, HFS World!";
5867 let mut data_reader = Cursor::new(test_data.as_slice());
5868 let options = CreateFileOptions::default();
5869 fs.create_file(
5870 &root,
5871 "test.txt",
5872 &mut data_reader,
5873 test_data.len() as u64,
5874 &options,
5875 )
5876 .unwrap();
5877
5878 assert_fsck_clean(&mut fs);
5879 }
5880
5881 #[test]
5882 fn test_fsck_clean_after_create_directory() {
5883 let img = make_editable_hfs_image();
5884 let cursor = Cursor::new(img);
5885 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5886
5887 let root = fs.root().unwrap();
5888 let options = CreateDirectoryOptions::default();
5889 fs.create_directory(&root, "NewDir", &options).unwrap();
5890
5891 assert_fsck_clean(&mut fs);
5892 }
5893
5894 #[test]
5895 fn test_fsck_clean_after_delete_file() {
5896 let img = make_editable_hfs_image();
5897 let cursor = Cursor::new(img);
5898 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5899
5900 let root = fs.root().unwrap();
5901 let test_data = b"delete me";
5902 let mut data_reader = Cursor::new(test_data.as_slice());
5903 let options = CreateFileOptions::default();
5904 let fe = fs
5905 .create_file(
5906 &root,
5907 "gone.txt",
5908 &mut data_reader,
5909 test_data.len() as u64,
5910 &options,
5911 )
5912 .unwrap();
5913
5914 assert_fsck_clean(&mut fs);
5915 fs.delete_entry(&root, &fe).unwrap();
5916 assert_fsck_clean(&mut fs);
5917 }
5918
5919 #[test]
5928 fn test_hfs_random_insert_packs_densely_and_fsck_clean() {
5929 use crate::hfs_common::BTreeHeader;
5930 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
5931 let block_size = 32 * 1024u32;
5932 let img = create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Pack", 0, 16 * 1024 * 1024)
5933 .unwrap();
5934 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
5935 let root_id = fs.root().unwrap().location as u32;
5936
5937 let n = 16000u32;
5939 let mut seen = std::collections::HashSet::new();
5940 let order: Vec<u32> = (0..n)
5941 .map(|i| i.wrapping_mul(2654435761) % n)
5942 .filter(|x| seen.insert(*x))
5943 .collect();
5944 let count = order.len() as u32;
5945 fs.begin_bulk();
5946 for &i in &order {
5947 let name = format!("f{:05}", i);
5948 let mut kr = Fs::build_catalog_key(root_id, name.as_bytes());
5949 kr.extend_from_slice(&Fs::build_file_record(
5950 16 + i,
5951 0,
5952 0,
5953 0,
5954 0,
5955 0,
5956 0,
5957 &[0u8; 4],
5958 &[0u8; 4],
5959 block_size,
5960 None,
5961 0,
5962 ));
5963 fs.insert_catalog_record(&kr).unwrap();
5964 }
5965 fs.end_bulk();
5966 fs.mdb.file_count = count;
5967 fs.mdb.next_catalog_id = 16 + n;
5968 fs.update_parent_valence(root_id, count as i32).unwrap();
5969
5970 let h = BTreeHeader::read(fs.catalog_data());
5973 let used = h.total_nodes - h.free_nodes;
5974 let rec_per_node = count as f64 / used as f64;
5975 assert!(
5976 rec_per_node > 2.8,
5977 "random-order packing too sparse: {count} records in {used} nodes ({rec_per_node:.2}/node)"
5978 );
5979
5980 let result = fs.fsck().unwrap();
5981 assert!(
5982 result.errors.is_empty(),
5983 "fsck errors after random import: {:?}",
5984 result
5985 .errors
5986 .iter()
5987 .take(8)
5988 .map(|e| &e.message)
5989 .collect::<Vec<_>>()
5990 );
5991 }
5992
5993 #[test]
6000 fn test_hfs_bulk_mode_import_fsck_clean() {
6001 let block_size = 32 * 1024u32;
6002 let img = create_blank_hfs_sized(
6003 64 * 1024 * 1024,
6004 block_size,
6005 "BulkImport",
6006 0,
6007 8 * 1024 * 1024,
6008 )
6009 .expect("create blank volume");
6010 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6011 let root = fs.root().unwrap();
6012 let root_id = root.location as u32;
6013 let options = CreateFileOptions::default();
6014
6015 fs.begin_bulk();
6016 for i in 0..9000 {
6017 let name = format!("f{:05}.txt", i);
6018 let mut empty = Cursor::new(Vec::new());
6019 fs.create_file(&root, &name, &mut empty, 0, &options)
6020 .unwrap_or_else(|e| panic!("bulk create_file #{i}: {e}"));
6021 }
6022 fs.end_bulk();
6023
6024 assert_eq!(fs.mdb.file_count, 9000);
6025 let result = fs.fsck().unwrap();
6026 assert!(
6027 result.errors.is_empty(),
6028 "bulk-mode import left fsck errors: {:?}",
6029 result
6030 .errors
6031 .iter()
6032 .take(8)
6033 .map(|e| &e.message)
6034 .collect::<Vec<_>>()
6035 );
6036 assert!(fs
6038 .find_catalog_record_by_name(root_id, b"f04500.txt")
6039 .is_some());
6040 assert_eq!(fs.list_directory(&root).unwrap().len(), 9000);
6041
6042 let mut empty = Cursor::new(Vec::new());
6045 assert!(fs
6046 .create_file(&root, "f00000.txt", &mut empty, 0, &options)
6047 .is_err());
6048 assert_fsck_clean(&mut fs);
6049 }
6050
6051 #[test]
6066 fn test_hfs_catalog_scales_past_header_bitmap_fsck_clean() {
6067 use crate::hfs_common::BTreeHeader;
6068 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
6069 let block_size = 32 * 1024u32;
6070 let img = create_blank_hfs_sized(
6074 64 * 1024 * 1024,
6075 block_size,
6076 "Scale20k",
6077 0,
6078 10 * 1024 * 1024,
6079 )
6080 .expect("create blank volume");
6081 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6082 let root = fs.root().unwrap();
6083 let root_id = root.location as u32;
6084
6085 let n: u32 = 22_000;
6086 for i in 0..n {
6087 let name = format!("f{:05}.txt", i);
6088 let mut key_record = Fs::build_catalog_key(root_id, name.as_bytes());
6089 let file_rec = Fs::build_file_record(
6090 16 + i,
6091 0,
6092 0,
6093 0,
6094 0,
6095 0,
6096 0,
6097 &[0u8; 4],
6098 &[0u8; 4],
6099 block_size,
6100 None,
6101 0,
6102 );
6103 key_record.extend_from_slice(&file_rec);
6104 fs.insert_catalog_record(&key_record)
6105 .unwrap_or_else(|e| panic!("insert #{i} ({name}): {e}"));
6106 }
6107
6108 fs.mdb.file_count = n;
6111 fs.mdb.next_catalog_id = 16 + n;
6112 fs.update_parent_valence(root_id, n as i32).unwrap();
6113
6114 let header = BTreeHeader::read(fs.catalog_data());
6117 assert!(
6118 header.depth >= 3,
6119 "expected a multi-level index, depth = {}",
6120 header.depth
6121 );
6122 let used = header.total_nodes - header.free_nodes;
6123 assert!(
6124 used > 2048,
6125 "expected > 2048 catalog nodes in use, got {used}"
6126 );
6127
6128 let result = fs.fsck().unwrap();
6129 assert!(
6130 result.errors.is_empty(),
6131 "fsck found {} errors after {n}-record import: {:?}",
6132 result.errors.len(),
6133 result
6134 .errors
6135 .iter()
6136 .take(8)
6137 .map(|e| &e.message)
6138 .collect::<Vec<_>>()
6139 );
6140
6141 for i in [0u32, 1, 7386, 12345, n - 1] {
6145 let name = format!("f{:05}.txt", i);
6146 assert!(
6147 fs.find_catalog_record_by_name(root_id, name.as_bytes())
6148 .is_some(),
6149 "record {name} not findable after bulk insert"
6150 );
6151 }
6152 }
6153
6154 #[test]
6164 fn test_hfs_incremental_shuffled_multidir_packs_dense_fsck_clean() {
6165 use crate::hfs_common::BTreeHeader;
6166 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
6167 let block_size = 32 * 1024u32;
6168 let img = create_blank_hfs_sized(
6172 128 * 1024 * 1024,
6173 block_size,
6174 "PutShuffle",
6175 0,
6176 6 * 1024 * 1024,
6177 )
6178 .expect("create blank volume");
6179 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6180
6181 const DIRS: u32 = 200;
6183 let root = fs.root().unwrap();
6184 let mut dir_ids = Vec::with_capacity(DIRS as usize);
6185 for d in 0..DIRS {
6186 let name = format!("dir{d:03}");
6187 let de = fs
6188 .create_directory(&root, &name, &Default::default())
6189 .unwrap();
6190 dir_ids.push(de.location as u32);
6191 }
6192 let base_cnid = fs.mdb.next_catalog_id;
6193
6194 let n: u32 = 20_000;
6198 let mut per_dir = vec![0i32; DIRS as usize];
6199 for i in 0..n {
6200 let hash = i.wrapping_mul(2_654_435_761);
6201 let d = (hash % DIRS) as usize;
6202 let name = format!("f{hash:08x}");
6203 let mut kr = Fs::build_catalog_key(dir_ids[d], name.as_bytes());
6204 kr.extend_from_slice(&Fs::build_file_record(
6205 base_cnid + i,
6206 0,
6207 0,
6208 0,
6209 0,
6210 0,
6211 0,
6212 &[0u8; 4],
6213 &[0u8; 4],
6214 block_size,
6215 None,
6216 0,
6217 ));
6218 fs.insert_catalog_record(&kr)
6219 .unwrap_or_else(|e| panic!("insert #{i} into dir{d}: {e}"));
6220 per_dir[d] += 1;
6221 }
6222 fs.mdb.file_count += n;
6225 fs.mdb.next_catalog_id = base_cnid + n;
6226 for (d, &cnt) in per_dir.iter().enumerate() {
6227 fs.update_parent_valence(dir_ids[d], cnt).unwrap();
6228 }
6229
6230 let h = BTreeHeader::read(fs.catalog_data());
6234 let used = h.total_nodes - h.free_nodes;
6235 let rec_per_node = h.leaf_records as f64 / used as f64;
6236 assert!(
6237 rec_per_node > 2.6,
6238 "shuffled multi-dir packing too sparse: {} records in {used} nodes ({rec_per_node:.2}/node)",
6239 h.leaf_records
6240 );
6241
6242 let result = fs.fsck().unwrap();
6244 assert!(
6245 result.errors.is_empty(),
6246 "fsck found {} errors after 20k shuffled multi-dir put: {:?}",
6247 result.errors.len(),
6248 result
6249 .errors
6250 .iter()
6251 .take(8)
6252 .map(|e| &e.message)
6253 .collect::<Vec<_>>()
6254 );
6255
6256 for i in [0u32, 7, 9_999, n - 1] {
6258 let hash = i.wrapping_mul(2_654_435_761);
6259 let parent = dir_ids[(hash % DIRS) as usize];
6260 let name = format!("f{hash:08x}");
6261 assert!(
6262 fs.find_catalog_record_by_name(parent, name.as_bytes())
6263 .is_some(),
6264 "record {name} not findable after shuffled multi-dir insert"
6265 );
6266 }
6267 }
6268
6269 #[test]
6274 fn test_rebuild_index_nodes_past_header_bitmap_fsck_clean() {
6275 use crate::hfs_common::BTreeHeader;
6276 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
6277 let block_size = 32 * 1024u32;
6278 let img =
6279 create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Rebuild", 0, 10 * 1024 * 1024)
6280 .expect("create blank volume");
6281 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6282 let root_id = fs.root().unwrap().location as u32;
6283
6284 let n: u32 = 12_000;
6285 for i in 0..n {
6286 let name = format!("f{:05}.txt", i);
6287 let mut key_record = Fs::build_catalog_key(root_id, name.as_bytes());
6288 let file_rec = Fs::build_file_record(
6289 16 + i,
6290 0,
6291 0,
6292 0,
6293 0,
6294 0,
6295 0,
6296 &[0u8; 4],
6297 &[0u8; 4],
6298 block_size,
6299 None,
6300 0,
6301 );
6302 key_record.extend_from_slice(&file_rec);
6303 fs.insert_catalog_record(&key_record).unwrap();
6304 }
6305 fs.mdb.file_count = n;
6306 fs.mdb.next_catalog_id = 16 + n;
6307 fs.update_parent_valence(root_id, n as i32).unwrap();
6308
6309 let node_size = BTreeHeader::read(fs.catalog_data()).node_size as usize;
6310 let used_before = {
6311 let h = BTreeHeader::read(fs.catalog_data());
6312 h.total_nodes - h.free_nodes
6313 };
6314 assert!(
6315 used_before > 2048,
6316 "test needs > 2048 nodes, got {used_before}"
6317 );
6318
6319 let mut report = disk_core::fsck::RepairReport {
6321 fixes_applied: Vec::new(),
6322 fixes_failed: Vec::new(),
6323 unrepairable_count: 0,
6324 };
6325 crate::hfs_fsck::rebuild_index_nodes(&mut fs.catalog_data, node_size, &mut report);
6326 assert!(
6327 report.fixes_failed.is_empty(),
6328 "rebuild reported failures: {:?}",
6329 report.fixes_failed
6330 );
6331
6332 let result = fs.fsck().unwrap();
6333 assert!(
6334 result.errors.is_empty(),
6335 "fsck after rebuild: {:?}",
6336 result
6337 .errors
6338 .iter()
6339 .take(8)
6340 .map(|e| &e.message)
6341 .collect::<Vec<_>>()
6342 );
6343 }
6344
6345 #[test]
6351 fn test_hfs_create_file_multilevel_index_fsck_clean() {
6352 use crate::hfs_common::BTreeHeader;
6353 let block_size = 32 * 1024u32;
6354 let img =
6355 create_blank_hfs_sized(32 * 1024 * 1024, block_size, "MultiIdx", 0, 4 * 1024 * 1024)
6356 .expect("create blank volume");
6357 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6358 let root = fs.root().unwrap();
6359 let options = CreateFileOptions::default();
6360 for i in 0..2000 {
6361 let name = format!("f{:05}.txt", i);
6362 let mut empty = Cursor::new(Vec::new());
6363 fs.create_file(&root, &name, &mut empty, 0, &options)
6364 .unwrap_or_else(|e| panic!("create_file #{i}: {e}"));
6365 }
6366 let header = BTreeHeader::read(fs.catalog_data());
6367 assert!(
6368 header.depth >= 2,
6369 "expected an index level, depth = {}",
6370 header.depth
6371 );
6372 let result = fs.fsck().unwrap();
6373 assert!(
6374 result.errors.is_empty(),
6375 "fsck errors: {:?}",
6376 result
6377 .errors
6378 .iter()
6379 .take(8)
6380 .map(|e| &e.message)
6381 .collect::<Vec<_>>()
6382 );
6383 assert_eq!(fs.mdb.file_count, 2000);
6384 }
6385
6386 #[test]
6392 fn deleting_a_leafs_first_record_refreshes_the_separators() {
6393 use crate::hfs_common::{
6396 btree_record_range, btree_stale_index_keys, BTreeHeader, BTreeKeyFormat,
6397 };
6398 let block_size = 32 * 1024u32;
6399 let img = create_blank_hfs_sized(32 * 1024 * 1024, block_size, "IKey", 0, 4 * 1024 * 1024)
6400 .expect("create blank volume");
6401 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6402 let root = fs.root().unwrap();
6403 let options = CreateFileOptions::default();
6404 for i in 0..2000 {
6405 let mut empty = Cursor::new(Vec::new());
6406 fs.create_file(&root, &format!("f{i:05}.txt"), &mut empty, 0, &options)
6407 .unwrap();
6408 }
6409 let node_size = BigEndian::read_u16(&fs.catalog_data()[32..34]) as usize;
6410 let header = BTreeHeader::read(fs.catalog_data());
6411 assert!(
6412 header.depth >= 2,
6413 "need an index level, depth {}",
6414 header.depth
6415 );
6416
6417 let first = header.first_leaf_node as usize;
6418 let second =
6419 BigEndian::read_u32(&fs.catalog_data()[first * node_size..first * node_size + 4])
6420 as usize;
6421 let name = {
6422 let node = &fs.catalog_data()[second * node_size..(second + 1) * node_size];
6423 let (s, e) = btree_record_range(node, node_size, 0);
6424 let rec = &node[s..e];
6425 let name_len = rec[6] as usize;
6426 String::from_utf8_lossy(&rec[7..7 + name_len]).into_owned()
6427 };
6428 assert!(
6429 name.starts_with('f'),
6430 "expected a file record first, got {name:?}"
6431 );
6432 let victim = fs
6433 .list_directory(&root)
6434 .unwrap()
6435 .into_iter()
6436 .find(|e| e.name == name)
6437 .expect("victim listed");
6438 fs.delete_entry(&root, &victim).unwrap();
6439
6440 let stale = btree_stale_index_keys(
6441 fs.catalog_data(),
6442 node_size,
6443 &BTreeKeyFormat::CLASSIC_CATALOG,
6444 &HfsFilesystem::<Cursor<Vec<u8>>>::catalog_compare,
6445 );
6446 assert!(stale.is_empty(), "separators still stale: {stale:?}");
6447 let result = fs.fsck().unwrap();
6448 assert!(
6449 result.errors.is_empty(),
6450 "{:?}",
6451 result
6452 .errors
6453 .iter()
6454 .map(|e| &e.message)
6455 .take(4)
6456 .collect::<Vec<_>>()
6457 );
6458 }
6459
6460 #[test]
6461 fn test_hfs_control_char_filename_roundtrip() {
6462 let img = make_editable_hfs_image();
6463 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6464 let root = fs.root().unwrap();
6465 let options = CreateFileOptions::default();
6466 let name = "IPNetRouter\u{7f}\u{7f}_154_68k";
6467 let mut data = Cursor::new(b"x".to_vec());
6468 fs.create_file(&root, name, &mut data, 1, &options)
6469 .expect("create_file with raw 0x7F bytes in the name");
6470 let entries = fs.list_directory(&root).unwrap();
6471 assert!(
6472 entries.iter().any(|e| e.name == name),
6473 "0x7F-laden name not preserved; got {:?}",
6474 entries.iter().map(|e| &e.name).collect::<Vec<_>>()
6475 );
6476 assert_fsck_clean(&mut fs);
6477 }
6478
6479 #[test]
6484 fn test_hfs_null_byte_name_is_warning_not_error() {
6485 let img = make_editable_hfs_image();
6486 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6487 let root = fs.root().unwrap();
6488 let options = CreateFileOptions::default();
6489 let mut data = Cursor::new(b"x".to_vec());
6490 fs.create_file(&root, "\u{0}Sorts First", &mut data, 1, &options)
6491 .expect("create_file with a leading null byte in the name");
6492 let result = fs.fsck().unwrap();
6493 assert!(
6494 result.errors.is_empty(),
6495 "a null-byte name must not be a fsck error: {:?}",
6496 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6497 );
6498 assert!(
6499 result
6500 .warnings
6501 .iter()
6502 .any(|w| w.code == "UnusualCatalogName"),
6503 "expected an UnusualCatalogName warning for the null-byte name"
6504 );
6505 }
6506
6507 #[test]
6508 fn test_fsck_stress_many_files() {
6509 let img = make_editable_hfs_image();
6510 let cursor = Cursor::new(img);
6511 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6512
6513 let root = fs.root().unwrap();
6514 let options = CreateFileOptions::default();
6515
6516 for i in 0..35 {
6518 let name = format!("file_{:03}.txt", i);
6519 let data = format!("content of file {}", i);
6520 let mut reader = Cursor::new(data.as_bytes().to_vec());
6521 fs.create_file(&root, &name, &mut reader, data.len() as u64, &options)
6522 .unwrap();
6523
6524 assert_fsck_clean(&mut fs);
6526 }
6527
6528 let result = fs.fsck().unwrap();
6529 assert!(result.is_clean());
6530 assert_eq!(result.stats.files_checked, 35);
6531 assert_eq!(result.stats.directories_checked, 1); }
6533
6534 #[test]
6535 fn test_fsck_clean_after_blessed_folder() {
6536 let img = make_editable_hfs_image();
6537 let cursor = Cursor::new(img);
6538 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6539
6540 let root = fs.root().unwrap();
6541 let options = CreateDirectoryOptions::default();
6542 let sys_dir = fs.create_directory(&root, "System", &options).unwrap();
6543 fs.set_blessed_folder(&sys_dir).unwrap();
6544
6545 assert_fsck_clean(&mut fs);
6546 }
6547
6548 #[test]
6553 fn unerased_free_nodes_warn_once_and_repair_zeroes_them() {
6554 use crate::hfs_common::{btree_bitmap_test, BTreeHeader};
6556
6557 let mut fs = HfsFilesystem::open(Cursor::new(make_editable_hfs_image()), 0).unwrap();
6558 let node_size = BTreeHeader::read(&fs.catalog_data).node_size as usize;
6559 let free: Vec<usize> = (1..fs.catalog_data.len() / node_size)
6560 .filter(|&i| !btree_bitmap_test(&fs.catalog_data, node_size, i as u32))
6561 .take(3)
6562 .collect();
6563 assert_eq!(free.len(), 3, "blank catalog has free nodes");
6564 for &i in &free {
6565 fs.catalog_data[i * node_size + 20] = 0xA5;
6566 }
6567
6568 let result = fs.fsck().unwrap();
6569 assert!(
6570 result.errors.is_empty(),
6571 "not an error: {:?}",
6572 result.errors
6573 );
6574 let warns: Vec<_> = result
6575 .warnings
6576 .iter()
6577 .filter(|w| w.code == "FreeNodeNotErased")
6578 .collect();
6579 assert_eq!(warns.len(), 1, "one summary per tree, not one per node");
6580 assert!(
6581 warns[0].message.contains("3 unused node(s)"),
6582 "{}",
6583 warns[0].message
6584 );
6585 assert!(result.repairable);
6586
6587 let report = fs.repair().unwrap();
6588 assert!(
6589 report
6590 .fixes_applied
6591 .iter()
6592 .any(|f| f == "Erased 3 unused catalog B-tree node(s)"),
6593 "{:?}",
6594 report.fixes_applied
6595 );
6596 let after = fs.fsck().unwrap();
6597 assert!(after.warnings.iter().all(|w| w.code != "FreeNodeNotErased"));
6598 assert!(after.errors.is_empty(), "{:?}", after.errors);
6599 }
6600
6601 #[test]
6602 fn test_repair_index_records_are_even_aligned() {
6603 use crate::hfs_common::{btree_record_range, BTreeHeader};
6604
6605 let img = make_editable_hfs_image();
6606 let cursor = Cursor::new(img);
6607 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6608 let root = fs.root().unwrap();
6609 let options = CreateFileOptions::default();
6610
6611 for i in 0..40 {
6614 let name = if i % 2 == 0 {
6615 format!("f{:03}.txt", i) } else {
6617 format!("fi{:03}.txt", i) };
6619 let data = format!("data{}", i);
6620 let mut reader = Cursor::new(data.as_bytes().to_vec());
6621 fs.create_file(&root, &name, &mut reader, data.len() as u64, &options)
6622 .unwrap();
6623 }
6624
6625 let report = fs.repair().unwrap();
6627 assert!(
6628 report.fixes_failed.is_empty(),
6629 "repair failures: {:?}",
6630 report.fixes_failed
6631 );
6632
6633 let cat = &fs.catalog_data;
6635 let header = BTreeHeader::read(cat);
6636 let node_size = header.node_size as usize;
6637 let max_nodes = cat.len() / node_size;
6638
6639 let mut index_recs_checked = 0u32;
6640 for n in 1..max_nodes {
6641 let off = n * node_size;
6642 if off + node_size > cat.len() {
6643 break;
6644 }
6645 let kind = cat[off + 8] as i8;
6646 if kind != 0 {
6647 continue;
6648 }
6649 let num_recs = BigEndian::read_u16(&cat[off + 10..off + 12]) as usize;
6650 for r in 0..num_recs {
6651 let (rec_start, rec_end) =
6652 btree_record_range(&cat[off..off + node_size], node_size, r);
6653 if rec_start >= rec_end || rec_end > node_size {
6654 continue;
6655 }
6656 let key_len = cat[off + rec_start] as usize;
6657 let rec_len = rec_end - rec_start;
6658
6659 assert_eq!(
6662 key_len, 0x25,
6663 "Index node {} rec {}: key_len={} but expected 0x25",
6664 n, r, key_len
6665 );
6666 assert_eq!(
6667 rec_len, 42,
6668 "Index node {} rec {}: rec_len={} but expected 42",
6669 n, r, rec_len
6670 );
6671 index_recs_checked += 1;
6672 }
6673 }
6674 assert!(
6675 index_recs_checked > 0,
6676 "no index records found — test needs more files"
6677 );
6678
6679 let result = fs.fsck().unwrap();
6681 let error_msgs: Vec<&str> = result.errors.iter().map(|e| e.message.as_str()).collect();
6682 assert!(
6683 result.errors.is_empty(),
6684 "post-repair check found errors: {:?}",
6685 error_msgs
6686 );
6687 }
6688
6689 #[test]
6693 fn test_create_blank_hfs_roundtrip() {
6694 let cases: &[(u64, u32, &str)] = &[
6697 (1 * 1024 * 1024, 512, "1MB @ 512"),
6698 (8 * 1024 * 1024, 4096, "8MB @ 4K"),
6699 (64 * 1024 * 1024, 16384, "64MB @ 16K"),
6700 (256 * 1024 * 1024, 32768, "256MB @ 32K"),
6701 ];
6702
6703 for (total_size, block_size, label) in cases {
6704 let img = create_blank_hfs(*total_size, *block_size, "Fresh")
6705 .unwrap_or_else(|e| panic!("{label}: create_blank_hfs failed: {e}"));
6706
6707 let atrb = BigEndian::read_u16(&img[1024 + 10..1024 + 12]);
6711 assert_eq!(
6712 atrb & 0x0100,
6713 0x0100,
6714 "{label}: drAtrb missing kHFSVolumeUnmountedMask (got 0x{atrb:04x})"
6715 );
6716 let alt_off = img.len() - 1024;
6718 let alt_atrb = BigEndian::read_u16(&img[alt_off + 10..alt_off + 12]);
6719 assert_eq!(
6720 alt_atrb, atrb,
6721 "{label}: alt MDB drAtrb (0x{alt_atrb:04x}) does not mirror primary (0x{atrb:04x})"
6722 );
6723
6724 assert!(
6729 (img.len() as u64) <= *total_size,
6730 "{label}: image {} exceeds requested {}",
6731 img.len(),
6732 total_size
6733 );
6734
6735 let mut fs = HfsFilesystem::open(Cursor::new(img.clone()), 0)
6736 .unwrap_or_else(|e| panic!("{label}: open failed: {e}"));
6737 assert_eq!(fs.mdb().block_size, *block_size, "{label}: block_size");
6738 assert_eq!(fs.mdb().volume_name, "Fresh", "{label}: volume_name");
6739
6740 let root = fs.root().unwrap();
6741 let entries = fs.list_directory(&root).unwrap();
6742 assert!(
6743 entries.is_empty(),
6744 "{label}: root not empty on fresh volume"
6745 );
6746
6747 let result = fs.fsck().unwrap();
6748 assert!(
6749 result.errors.is_empty(),
6750 "{label}: fsck errors: {:?}",
6751 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6752 );
6753
6754 let mut backing = img;
6756 {
6757 let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
6758 let root = fs.root().unwrap();
6759 let payload = b"contents".to_vec();
6760 let mut src = payload.as_slice();
6761 fs.create_file(
6762 &root,
6763 "hello",
6764 &mut src,
6765 payload.len() as u64,
6766 &CreateFileOptions::default(),
6767 )
6768 .unwrap_or_else(|e| panic!("{label}: create_file failed: {e}"));
6769 fs.sync_metadata()
6770 .unwrap_or_else(|e| panic!("{label}: sync_metadata failed: {e}"));
6771 }
6772 let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
6773 let root = fs.root().unwrap();
6774 let entries = fs.list_directory(&root).unwrap();
6775 assert_eq!(entries.len(), 1, "{label}: expected 1 child after create");
6776 assert_eq!(entries[0].name, "hello", "{label}: child name");
6777 let data = fs.read_file(&entries[0], 64).unwrap();
6778 assert_eq!(data, b"contents", "{label}: file content round-trip");
6779
6780 let result = fs.fsck().unwrap();
6781 assert!(
6782 result.errors.is_empty(),
6783 "{label}: post-create fsck errors: {:?}",
6784 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6785 );
6786 }
6787 }
6788
6789 #[test]
6797 #[cfg(target_pointer_width = "64")]
6798 fn test_create_blank_hfs_near_2gb() {
6799 let total = 2u64 * 1024 * 1024 * 1024 - 1;
6800 let img = create_blank_hfs(total, 32768, "Near2GB")
6801 .expect("create_blank_hfs near 2GB should succeed");
6802 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6803 assert_eq!(fs.mdb().block_size, 32768);
6804 assert!(fs.mdb().total_blocks >= 65000);
6805 let result = fs.fsck().unwrap();
6806 assert!(
6807 result.errors.is_empty(),
6808 "fsck errors: {:?}",
6809 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6810 );
6811 }
6812
6813 #[test]
6817 fn catalog_grows_when_its_nodes_run_out() {
6818 let img = create_blank_hfs_sized(16 * 1024 * 1024, 4096, "Grow", 0, 16384).unwrap();
6819 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6820 let size0 = fs.mdb.catalog_file_size;
6821 assert_eq!(size0, 16384);
6822 let root = fs.root().unwrap();
6823 for i in 0..1000 {
6824 let name = format!("f{i:04}.txt");
6825 fs.create_file(
6826 &root,
6827 &name,
6828 &mut &b"x"[..],
6829 1,
6830 &CreateFileOptions::default(),
6831 )
6832 .unwrap_or_else(|e| panic!("creating {name}: {e}"));
6833 }
6834 assert!(fs.mdb.catalog_file_size > size0, "the catalog never grew");
6835 let inline: u32 = fs
6838 .mdb
6839 .catalog_file_extents
6840 .iter()
6841 .map(|e| e.block_count as u32)
6842 .sum();
6843 let spilled: u32 = collect_fork_overflow_extents(
6844 fs.extents_overflow_data
6845 .as_ref()
6846 .expect("overflow tree loaded"),
6847 CNID_CATALOG,
6848 0x00,
6849 inline,
6850 )
6851 .iter()
6852 .map(|e| e.block_count as u32)
6853 .sum();
6854 assert!(spilled > 0, "no CNID 4 overflow records");
6855 assert_eq!(
6856 (inline + spilled) * 4096,
6857 fs.mdb.catalog_file_size,
6858 "extents cover the file"
6859 );
6860 fs.sync_metadata().unwrap();
6861 let report = fs.fsck().unwrap();
6862 assert!(report.errors.is_empty(), "{:?}", report.errors);
6863
6864 let img = fs.reader.get_ref().clone();
6865 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6866 let root = fs.root().unwrap();
6867 let kids = fs.list_directory(&root).unwrap();
6868 assert_eq!(kids.len(), 1000);
6869 for e in &kids {
6870 fs.delete_entry(&root, e).unwrap();
6871 }
6872 fs.create_file(
6873 &root,
6874 "last.txt",
6875 &mut &b"y"[..],
6876 1,
6877 &CreateFileOptions::default(),
6878 )
6879 .unwrap();
6880 fs.sync_metadata().unwrap();
6881 let report = fs.fsck().unwrap();
6882 assert!(report.errors.is_empty(), "{:?}", report.errors);
6883 }
6884
6885 #[test]
6888 fn extents_tree_grows_when_its_nodes_run_out() {
6889 let img = create_blank_hfs_sized(4 * 1024 * 1024, 512, "Frag", 2048, 0).unwrap();
6890 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6891 let xt0 = fs.mdb.extents_file_size;
6892 assert_eq!(xt0, 2048);
6893 let root = fs.root().unwrap();
6894 let block = vec![0x5Au8; 512];
6895 for i in 0..400 {
6896 fs.create_file(
6897 &root,
6898 &format!("h{i:03}"),
6899 &mut Cursor::new(&block),
6900 512,
6901 &CreateFileOptions::default(),
6902 )
6903 .unwrap();
6904 }
6905 for e in fs.list_directory(&root).unwrap() {
6906 if e.name[1..].parse::<u32>().unwrap() % 2 == 1 {
6907 fs.delete_entry(&root, &e).unwrap();
6908 }
6909 }
6910 let tail = (fs.mdb.free_blocks - 264) as usize * 512;
6913 fs.create_file(
6914 &root,
6915 "tail",
6916 &mut Cursor::new(vec![0u8; tail]),
6917 tail as u64,
6918 &CreateFileOptions::default(),
6919 )
6920 .unwrap();
6921 let data: Vec<u8> = (0..200 * 512).map(|i| (i / 512) as u8).collect();
6922 let entry = fs
6923 .create_file(
6924 &root,
6925 "spread",
6926 &mut Cursor::new(&data),
6927 data.len() as u64,
6928 &CreateFileOptions::default(),
6929 )
6930 .unwrap();
6931 assert!(
6932 fs.mdb.extents_file_size > xt0,
6933 "the extents tree never grew"
6934 );
6935 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
6936 fs.sync_metadata().unwrap();
6937 let report = fs.fsck().unwrap();
6938 assert!(report.errors.is_empty(), "{:?}", report.errors);
6939
6940 let img = fs.reader.get_ref().clone();
6941 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6942 let root = fs.root().unwrap();
6943 let entry = fs
6944 .list_directory(&root)
6945 .unwrap()
6946 .into_iter()
6947 .find(|e| e.name == "spread")
6948 .unwrap();
6949 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
6950 fs.delete_entry(&root, &entry).unwrap();
6951 fs.sync_metadata().unwrap();
6952 let report = fs.fsck().unwrap();
6953 assert!(report.errors.is_empty(), "{:?}", report.errors);
6954 }
6955
6956 #[test]
6959 fn catalog_growth_adds_a_map_node_past_the_header_bitmap() {
6960 let img = create_blank_hfs_sized(24 * 1024 * 1024, 512, "Map", 0, 16384).unwrap();
6961 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6962 let root = fs.root().unwrap();
6963 for i in 0..9000 {
6964 let name = format!("f{i:04}");
6965 fs.create_file(
6966 &root,
6967 &name,
6968 &mut &b"x"[..],
6969 1,
6970 &CreateFileOptions::default(),
6971 )
6972 .unwrap_or_else(|e| panic!("creating {name}: {e}"));
6973 }
6974 let h = BTreeHeader::read(&fs.catalog_data);
6975 assert!(h.total_nodes > 2048, "{} nodes", h.total_nodes);
6976 assert!(
6977 hfs_common::btree_bitmap_segments(&fs.catalog_data, 512).len() > 1,
6978 "no map node"
6979 );
6980 fs.sync_metadata().unwrap();
6981 let report = fs.fsck().unwrap();
6982 assert!(report.errors.is_empty(), "{:?}", report.errors);
6983 let img = fs.reader.get_ref().clone();
6984 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6985 let root = fs.root().unwrap();
6986 assert_eq!(fs.list_directory(&root).unwrap().len(), 9000);
6987 }
6988
6989 #[test]
6992 fn thousand_file_churn_keeps_the_catalog_clean() {
6993 let img = create_blank_hfs(64 * 1024 * 1024, 4096, "Churn").unwrap();
6994 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6995 let root = fs.root().unwrap();
6996 let clean = |fs: &mut HfsFilesystem<Cursor<Vec<u8>>>, stage: &str| {
6997 fs.sync_metadata().unwrap();
6998 let r = fs.fsck().unwrap();
6999 assert!(r.errors.is_empty(), "{stage}: {:?}", r.errors);
7000 };
7001 let put = |fs: &mut HfsFilesystem<Cursor<Vec<u8>>>, name: &str, data: &[u8]| {
7002 fs.create_file(
7003 &root,
7004 name,
7005 &mut Cursor::new(data),
7006 data.len() as u64,
7007 &CreateFileOptions::default(),
7008 )
7009 .unwrap_or_else(|e| panic!("creating {name}: {e}"))
7010 };
7011 let free0 = fs.mdb.free_blocks;
7012 for i in 0..1000 {
7013 put(
7014 &mut fs,
7015 &format!("f{i:04}.txt"),
7016 format!("file {i:04}").as_bytes(),
7017 );
7018 }
7019 clean(&mut fs, "after 1000 creates");
7020 let extra = put(&mut fs, "extra.bin", &[7u8; 5000]);
7021 clean(&mut fs, "after one more");
7022 fs.delete_entry(&root, &extra).unwrap();
7023 clean(&mut fs, "after deleting it");
7024 for e in fs.list_directory(&root).unwrap() {
7025 fs.delete_entry(&root, &e).unwrap();
7026 }
7027 assert_eq!(fs.mdb.free_blocks, free0, "every block came back");
7028 clean(&mut fs, "after deleting the 1000");
7029 put(&mut fs, "last.bin", &[9u8; 100]);
7030 clean(&mut fs, "after the last create");
7031 let names: Vec<String> = fs
7032 .list_directory(&root)
7033 .unwrap()
7034 .into_iter()
7035 .map(|e| e.name)
7036 .collect();
7037 assert_eq!(names, ["last.bin"]);
7038 }
7039
7040 #[test]
7043 fn fsck_reports_a_volume_that_does_not_fill_its_partition() {
7044 let img = create_blank_hfs(4 * 1024 * 1024, 2048, "Short").unwrap();
7045 let exact = img.len() as u64;
7046 let mut fs = HfsFilesystem::open_sized(Cursor::new(img.clone()), 0, Some(exact)).unwrap();
7047 assert!(fs.partition_shortfall().is_none());
7048 let report = fs.fsck().unwrap();
7049 assert!(report.errors.is_empty(), "{:?}", report.errors);
7050
7051 let mut fs =
7054 HfsFilesystem::open_sized(Cursor::new(img.clone()), 0, Some(exact + 512)).unwrap();
7055 assert_eq!(fs.partition_shortfall(), Some(512));
7056 let report = fs.fsck().unwrap();
7057 assert!(report.errors.is_empty(), "{:?}", report.errors);
7058
7059 let mut fs =
7060 HfsFilesystem::open_sized(Cursor::new(img), 0, Some(exact + 1024 * 1024)).unwrap();
7061 assert_eq!(fs.partition_shortfall(), Some(1024 * 1024));
7062 let report = fs.fsck().unwrap();
7063 let codes: Vec<&str> = report.errors.iter().map(|e| e.code.as_str()).collect();
7064 assert_eq!(codes, ["AllocationAreaEnd"], "{:?}", report.errors);
7065 assert!(report.errors[0].message.contains("resize --size"));
7066 }
7067
7068 #[test]
7071 fn create_file_spreads_a_fork_over_free_runs() {
7072 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "Runs").unwrap();
7073 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7074 let root = fs.root().unwrap();
7075 let block = vec![0xA5u8; 4096];
7076 for i in 0..40 {
7077 fs.create_file(
7078 &root,
7079 &format!("f{i:02}"),
7080 &mut Cursor::new(block.as_slice()),
7081 4096,
7082 &CreateFileOptions::default(),
7083 )
7084 .unwrap();
7085 }
7086 for e in fs.list_directory(&root).unwrap() {
7088 if e.name[1..].parse::<u32>().unwrap() % 2 == 1 {
7089 fs.delete_entry(&root, &e).unwrap();
7090 }
7091 }
7092 let tail = (fs.mdb.free_blocks - 20) as usize;
7094 let tail_data = vec![0x5Au8; tail * 4096];
7095 fs.create_file(
7096 &root,
7097 "tail",
7098 &mut Cursor::new(tail_data.as_slice()),
7099 tail_data.len() as u64,
7100 &CreateFileOptions::default(),
7101 )
7102 .unwrap();
7103 assert_eq!(fs.mdb.free_blocks, 20);
7104 let free_before = fs.mdb.free_blocks;
7105
7106 let data: Vec<u8> = (0..7 * 4096).map(|i| (i / 4096) as u8 + 1).collect();
7108 let rsrc: Vec<u8> = (0..5 * 4096).map(|i| 0x80 | (i / 4096) as u8).collect();
7109 let opts = CreateFileOptions {
7110 resource_fork: Some(disk_core::filesystem::ResourceForkSource::Data(
7111 rsrc.clone(),
7112 )),
7113 ..Default::default()
7114 };
7115 let entry = fs
7116 .create_file(
7117 &root,
7118 "frag",
7119 &mut Cursor::new(data.as_slice()),
7120 data.len() as u64,
7121 &opts,
7122 )
7123 .unwrap();
7124 let cnid = entry.location as u32;
7125 assert_eq!(fs.mdb.free_blocks, free_before - 12);
7126 let (dsize, dext, rsize, rext) = fs.find_file_by_id(cnid).unwrap();
7127 assert_eq!((dsize as usize, rsize as usize), (data.len(), rsrc.len()));
7128 assert!(dext.iter().all(|e| e.block_count == 1) && rext.iter().all(|e| e.block_count == 1));
7129 let tree = fs
7130 .extents_overflow_data
7131 .as_ref()
7132 .expect("overflow tree loaded");
7133 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).len(), 4);
7134 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0xFF, 0).len(), 2);
7135 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
7136 let mut got = Vec::new();
7137 fs.write_resource_fork_to(&entry, &mut got).unwrap();
7138 assert_eq!(got, rsrc);
7139
7140 let rsrc2: Vec<u8> = (0..4 * 4096).map(|i| 0xC0 | (i / 4096) as u8).collect();
7142 fs.write_resource_fork(
7143 &entry,
7144 &mut Cursor::new(rsrc2.as_slice()),
7145 rsrc2.len() as u64,
7146 )
7147 .unwrap();
7148 assert_eq!(fs.mdb.free_blocks, free_before - 11);
7149 let tree = fs.extents_overflow_data.as_ref().unwrap();
7150 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0xFF, 0).len(), 1);
7151 let mut got = Vec::new();
7152 fs.write_resource_fork_to(&entry, &mut got).unwrap();
7153 assert_eq!(got, rsrc2);
7154
7155 fs.sync_metadata().unwrap();
7156 let report = fs.fsck().unwrap();
7157 assert!(report.errors.is_empty(), "{:?}", report.errors);
7158
7159 let img = fs.reader.get_ref().clone();
7161 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7162 let root = fs.root().unwrap();
7163 let entry = fs
7164 .list_directory(&root)
7165 .unwrap()
7166 .into_iter()
7167 .find(|e| e.name == "frag")
7168 .unwrap();
7169 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
7170 fs.delete_entry(&root, &entry).unwrap();
7171 assert_eq!(fs.mdb.free_blocks, free_before);
7172 let tree = fs.extents_overflow_data.as_ref().unwrap();
7173 assert!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).is_empty());
7174 fs.sync_metadata().unwrap();
7175 let report = fs.fsck().unwrap();
7176 assert!(report.errors.is_empty(), "{:?}", report.errors);
7177 }
7178
7179 #[test]
7182 fn delete_frees_overflow_extents_and_their_records() {
7183 use crate::hfs_common::{
7184 bitmap_test_bit_be, btree_alloc_node, BTreeHeader, BTREE_LEAF_NODE,
7185 };
7186 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "Overflow").unwrap();
7188 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7189 assert!(fs.mdb.extents_file_size > 0);
7190 let root = fs.root().unwrap();
7191 let free_at_start = fs.mdb.free_blocks;
7192 let data = b"inline extent contents";
7193 fs.create_file(
7194 &root,
7195 "frag.bin",
7196 &mut Cursor::new(data.as_slice()),
7197 data.len() as u64,
7198 &CreateFileOptions::default(),
7199 )
7200 .unwrap();
7201 let entry = fs
7202 .list_directory(&root)
7203 .unwrap()
7204 .into_iter()
7205 .find(|e| e.name == "frag.bin")
7206 .unwrap();
7207 let cnid = entry.location as u32;
7208
7209 let start = fs.allocate_blocks(2).unwrap();
7212 fs.ensure_extents_overflow().unwrap();
7213 {
7214 let tree = fs.extents_overflow_data.as_mut().expect("extents tree");
7215 let mut h = BTreeHeader::read(tree);
7216 let node_size = h.node_size as usize;
7217 let leaf = btree_alloc_node(tree, node_size, h.total_nodes).unwrap();
7218 h.root_node = leaf;
7219 h.first_leaf_node = leaf;
7220 h.last_leaf_node = leaf;
7221 h.depth = 1;
7222 h.leaf_records = 1;
7223 h.free_nodes -= 1;
7224 h.write(tree);
7225 let off = leaf as usize * node_size;
7226 let node = &mut tree[off..off + node_size];
7227 node[8] = BTREE_LEAF_NODE as u8;
7228 node[9] = 1;
7229 BigEndian::write_u16(&mut node[10..12], 1);
7230 node[14] = 7;
7232 node[15] = 0x00;
7233 BigEndian::write_u32(&mut node[16..20], cnid);
7234 BigEndian::write_u16(&mut node[20..22], 1);
7235 BigEndian::write_u16(&mut node[22..24], start as u16);
7236 BigEndian::write_u16(&mut node[24..26], 2);
7237 BigEndian::write_u16(&mut node[node_size - 2..], 14);
7238 BigEndian::write_u16(&mut node[node_size - 4..node_size - 2], 34);
7239 }
7240 let tree = fs.extents_overflow_data.as_ref().unwrap();
7241 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).len(), 1);
7242
7243 fs.delete_entry(&root, &entry).unwrap();
7244
7245 let tree = fs.extents_overflow_data.as_ref().unwrap();
7246 assert!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).is_empty());
7247 let h = BTreeHeader::read(tree);
7248 assert_eq!((h.leaf_records, h.root_node, h.depth), (0, 0, 0));
7249 assert!(
7251 crate::hfs_common::btree_lonely_empty_leaves(tree, h.node_size as usize).is_empty()
7252 );
7253 let bitmap = fs.bitmap.as_ref().unwrap();
7254 assert!(!bitmap_test_bit_be(bitmap, start) && !bitmap_test_bit_be(bitmap, start + 1));
7255 assert_eq!(fs.mdb.free_blocks, free_at_start, "every block came back");
7256 fs.sync_metadata().unwrap();
7257 let report = fs.fsck().unwrap();
7258 assert!(report.errors.is_empty(), "{:?}", report.errors);
7259 }
7260
7261 #[test]
7265 fn test_collect_fork_overflow_extents() {
7266 let node_size = 512usize;
7267 let total_nodes = 3u32;
7269 let mut buf = vec![0u8; node_size * total_nodes as usize];
7270
7271 buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
7273 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);
7280 BigEndian::write_u16(&mut buf[34..36], 7);
7281 BigEndian::write_u32(&mut buf[36..40], total_nodes);
7282 BigEndian::write_u32(&mut buf[40..44], total_nodes - 2);
7283 buf[248] = 0b11000000;
7285
7286 let leaf_off = node_size;
7288 let leaf = &mut buf[leaf_off..leaf_off + node_size];
7289 leaf[8] = hfs_common::BTREE_LEAF_NODE as u8;
7290 BigEndian::write_u16(&mut leaf[10..12], 1); let rec_off = 14usize;
7294 leaf[rec_off] = 7;
7295 leaf[rec_off + 1] = 0x00;
7296 BigEndian::write_u32(&mut leaf[rec_off + 2..rec_off + 6], 0x100);
7297 BigEndian::write_u16(&mut leaf[rec_off + 6..rec_off + 8], 3);
7298 let data_off = rec_off + 8;
7300 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);
7303 BigEndian::write_u16(&mut leaf[data_off + 6..data_off + 8], 1);
7304 BigEndian::write_u16(&mut leaf[data_off + 8..data_off + 10], 0); BigEndian::write_u16(&mut leaf[data_off + 10..data_off + 12], 0);
7306
7307 let nr = 1usize;
7309 BigEndian::write_u16(&mut leaf[node_size - 2..], 14);
7310 BigEndian::write_u16(&mut leaf[node_size - 4..node_size - 2], 34);
7311 let _ = nr;
7312
7313 let got = collect_fork_overflow_extents(&buf, 0x100, 0x00, 3);
7315 assert_eq!(got.len(), 2, "expected 2 non-empty extents, got {got:?}");
7316 assert_eq!((got[0].start_block, got[0].block_count), (50, 2));
7317 assert_eq!((got[1].start_block, got[1].block_count), (60, 1));
7318
7319 let none = collect_fork_overflow_extents(&buf, 0x100, 0x00, 4);
7321 assert!(
7322 none.is_empty(),
7323 "expected empty when filtered, got {none:?}"
7324 );
7325
7326 let rsrc = collect_fork_overflow_extents(&buf, 0x100, 0xFF, 0);
7328 assert!(rsrc.is_empty());
7329
7330 let other = collect_fork_overflow_extents(&buf, 0x101, 0x00, 0);
7332 assert!(other.is_empty());
7333 }
7334
7335 #[test]
7339 fn test_create_blank_hfs_sized_custom_btree_sizes() {
7340 let block_size = 32 * 1024u32;
7341 let img =
7343 create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Sized", 64 * 1024, 256 * 1024)
7344 .expect("create_blank_hfs_sized");
7345 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open");
7346 assert_eq!(fs.mdb().extents_file_size, 4 * block_size);
7348 assert_eq!(fs.mdb().catalog_file_size, 8 * block_size);
7350 let result = fs.fsck().expect("fsck");
7351 assert!(
7352 result.errors.is_empty(),
7353 "unexpected fsck errors: {:?}",
7354 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7355 );
7356 }
7357
7358 #[test]
7363 fn test_pick_btree_node_size_thresholds() {
7364 assert_eq!(pick_btree_node_size(512 * 2048), 512);
7365 assert_eq!(pick_btree_node_size(512 * 2049), 512);
7366 assert_eq!(pick_btree_node_size(1024 * 6144), 512);
7367 assert_eq!(pick_btree_node_size(u32::MAX as u64), 512);
7368 }
7369
7370 #[test]
7375 fn test_create_blank_hfs_sized_large_catalog() {
7376 let block_size = 32 * 1024u32;
7377 let img = create_blank_hfs_sized(
7378 128 * 1024 * 1024,
7379 block_size,
7380 "Big",
7381 0,
7382 HFS_MAX_BTREE_FILE_SIZE,
7383 )
7384 .expect("create_blank_hfs_sized big catalog");
7385 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open big");
7386 let cat = fs.catalog_data();
7387 let node_size = BigEndian::read_u16(&cat[32..34]);
7388 assert_eq!(
7389 node_size, 512,
7390 "node_size must stay at 512 for Mac OS compat"
7391 );
7392 let result = fs.fsck().expect("fsck big");
7393 assert!(
7394 result.errors.is_empty(),
7395 "unexpected fsck errors: {:?}",
7396 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7397 );
7398 }
7399
7400 #[test]
7404 fn test_create_blank_hfs_with_map_nodes() {
7405 let img =
7408 create_blank_hfs_sized(64 * 1024 * 1024, 32 * 1024, "MapNodes", 0, 2 * 1024 * 1024)
7409 .expect("create_blank_hfs_sized 2 MiB cat");
7410 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open");
7411 let cat = fs.catalog_data().to_vec();
7412
7413 let header_flink = BigEndian::read_u32(&cat[0..4]);
7415 assert_eq!(header_flink, 2, "header.fLink should point to map node 2");
7416
7417 let map = &cat[1024..1536];
7420 assert_eq!(map[8] as i8, 2, "node 2 should be a map node");
7421 assert_eq!(BigEndian::read_u16(&map[10..12]), 1);
7422 assert_eq!(
7423 BigEndian::read_u32(&map[0..4]),
7424 0,
7425 "single map node, fLink=0"
7426 );
7427
7428 let header = hfs_common::BTreeHeader::read(&cat);
7430 assert_eq!(header.total_nodes, 4096);
7431 assert_eq!(header.free_nodes, 4093);
7432
7433 let segs = hfs_common::btree_bitmap_segments(&cat, 512);
7436 assert_eq!(segs.len(), 2);
7437 assert_eq!(segs[0].base_bit, 0);
7438 assert_eq!(segs[1].base_bit, 2048);
7439 assert!(
7440 (segs[0].len + segs[1].len) * 8 >= 4096,
7441 "bitmap chain must address all 4096 nodes"
7442 );
7443
7444 let result = fs.fsck().expect("fsck");
7446 assert!(
7447 result.errors.is_empty(),
7448 "unexpected fsck errors: {:?}",
7449 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7450 );
7451 }
7452
7453 #[test]
7455 fn test_create_blank_hfs_rejects_bad_block_size() {
7456 assert!(create_blank_hfs(1 << 20, 0, "X").is_err());
7457 assert!(create_blank_hfs(1 << 20, 500, "X").is_err());
7458 assert!(create_blank_hfs(1 << 20, 513, "X").is_err());
7459 }
7460
7461 fn make_fs_with_file() -> (HfsFilesystem<Cursor<Vec<u8>>>, FileEntry) {
7465 let img = make_editable_hfs_image();
7466 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7467 let root = fs.root().unwrap();
7468 let data = b"hello";
7469 let mut r = Cursor::new(data.as_slice());
7470 let fe = fs
7471 .create_file(
7472 &root,
7473 "thing.bin",
7474 &mut r,
7475 data.len() as u64,
7476 &CreateFileOptions::default(),
7477 )
7478 .unwrap();
7479 (fs, fe)
7480 }
7481
7482 #[test]
7483 fn test_hfs_set_finder_info_round_trip() {
7484 let (mut fs, fe) = make_fs_with_file();
7485 let mut finfo = [0u8; 16];
7486 finfo[0..4].copy_from_slice(b"APPL");
7487 finfo[4..8].copy_from_slice(b"MACS");
7488 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];
7493 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();
7498
7499 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7500 assert_eq!(&fs.catalog_data[rec + 4..rec + 20], &finfo);
7501 assert_eq!(&fs.catalog_data[rec + 56..rec + 72], &fxinfo);
7502
7503 let root = fs.root().unwrap();
7506 let listed = fs.list_directory(&root).unwrap();
7507 let entry = listed.iter().find(|e| e.name == "thing.bin").unwrap();
7508 assert_eq!(entry.finder_flags, Some(0x4000));
7509 }
7510
7511 #[test]
7515 fn test_hfs_read_surfaces_has_bundle_flag() {
7516 let (mut fs, fe) = make_fs_with_file();
7517 let mut finfo = [0u8; 16];
7518 finfo[0..4].copy_from_slice(b"APPL");
7519 finfo[4..8].copy_from_slice(b"Po.P");
7520 BigEndian::write_u16(&mut finfo[8..10], 0x2000); fs.set_finder_info(&fe, finfo, [0u8; 16]).unwrap();
7522
7523 let root = fs.root().unwrap();
7524 let listed = fs.list_directory(&root).unwrap();
7525 let entry = listed.iter().find(|e| e.name == "thing.bin").unwrap();
7526 assert_eq!(entry.finder_flags, Some(0x2000));
7527 }
7528
7529 #[test]
7530 fn test_hfs_set_finder_info_rejects_directory() {
7531 let img = make_editable_hfs_image();
7532 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7533 let root = fs.root().unwrap();
7534 let dir = fs
7535 .create_directory(&root, "Dir", &CreateDirectoryOptions::default())
7536 .unwrap();
7537 let err = fs.set_finder_info(&dir, [0u8; 16], [0u8; 16]).unwrap_err();
7538 assert!(matches!(err, FilesystemError::InvalidData(_)));
7539 }
7540
7541 #[test]
7542 fn test_hfs_set_directory_finder_info_round_trip() {
7543 let img = make_editable_hfs_image();
7544 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7545 let root = fs.root().unwrap();
7546 let dir = fs
7547 .create_directory(&root, "Apps", &CreateDirectoryOptions::default())
7548 .unwrap();
7549
7550 let mut dinfo = [0u8; 16];
7551 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];
7557 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();
7562
7563 let rec = fs.locate_record_data(dir.location as u32).unwrap();
7564 assert_eq!(&fs.catalog_data[rec + 22..rec + 38], &dinfo);
7565 assert_eq!(&fs.catalog_data[rec + 38..rec + 54], &dxinfo);
7566 }
7567
7568 #[test]
7569 fn test_hfs_set_directory_finder_info_rejects_file() {
7570 let (mut fs, fe) = make_fs_with_file();
7571 let err = fs
7572 .set_directory_finder_info(&fe, [0u8; 16], [0u8; 16])
7573 .unwrap_err();
7574 assert!(matches!(err, FilesystemError::InvalidData(_)));
7575 }
7576
7577 #[test]
7578 fn test_hfs_set_dates_file() {
7579 let (mut fs, fe) = make_fs_with_file();
7580 let create = 0x1000_0001u32;
7581 let modify = 0x2000_0002u32;
7582 let backup = 0x3000_0003u32;
7583 fs.set_dates(&fe, create, modify, backup).unwrap();
7584
7585 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7586 assert_eq!(
7587 BigEndian::read_u32(&fs.catalog_data[rec + 44..rec + 48]),
7588 create
7589 );
7590 assert_eq!(
7591 BigEndian::read_u32(&fs.catalog_data[rec + 48..rec + 52]),
7592 modify
7593 );
7594 assert_eq!(
7595 BigEndian::read_u32(&fs.catalog_data[rec + 52..rec + 56]),
7596 backup
7597 );
7598 }
7599
7600 #[test]
7601 fn test_hfs_set_dates_directory() {
7602 let img = make_editable_hfs_image();
7603 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7604 let root = fs.root().unwrap();
7605 let dir = fs
7606 .create_directory(&root, "D", &CreateDirectoryOptions::default())
7607 .unwrap();
7608
7609 let create = 0xAAAAu32;
7610 let modify = 0xBBBBu32;
7611 let backup = 0xCCCCu32;
7612 fs.set_dates(&dir, create, modify, backup).unwrap();
7613
7614 let rec = fs.locate_record_data(dir.location as u32).unwrap();
7615 assert_eq!(
7616 BigEndian::read_u32(&fs.catalog_data[rec + 10..rec + 14]),
7617 create
7618 );
7619 assert_eq!(
7620 BigEndian::read_u32(&fs.catalog_data[rec + 14..rec + 18]),
7621 modify
7622 );
7623 assert_eq!(
7624 BigEndian::read_u32(&fs.catalog_data[rec + 18..rec + 22]),
7625 backup
7626 );
7627 }
7628
7629 #[test]
7630 fn test_hfs_set_file_locked_toggle() {
7631 let (mut fs, fe) = make_fs_with_file();
7632 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7633 let initial = fs.catalog_data[rec + 2];
7634 assert_eq!(initial & 0x01, 0);
7635
7636 fs.set_file_locked(&fe, true).unwrap();
7637 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7638 assert_eq!(fs.catalog_data[rec + 2] & 0x01, 0x01);
7639
7640 fs.set_file_locked(&fe, false).unwrap();
7641 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7642 assert_eq!(fs.catalog_data[rec + 2] & 0x01, 0);
7643 }
7644
7645 #[test]
7646 fn test_hfs_set_file_locked_rejects_directory() {
7647 let img = make_editable_hfs_image();
7648 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7649 let root = fs.root().unwrap();
7650 let dir = fs
7651 .create_directory(&root, "Dir", &CreateDirectoryOptions::default())
7652 .unwrap();
7653 let err = fs.set_file_locked(&dir, true).unwrap_err();
7654 assert!(matches!(err, FilesystemError::InvalidData(_)));
7655 }
7656
7657 #[test]
7658 fn test_hfs_set_volume_dates_persists_through_sync() {
7659 let img = make_editable_hfs_image();
7660 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7661
7662 let create = 0x4242_4242u32;
7663 let modify = 0x5151_5151u32;
7664 let backup = 0x6060_6060u32;
7665 fs.set_volume_dates(create, modify, backup);
7666 fs.sync_metadata().unwrap();
7667
7668 assert_eq!(fs.mdb.create_date, create);
7669 assert_eq!(fs.mdb.modify_date, modify);
7670 assert_eq!(BigEndian::read_u32(&fs.mdb.raw_sector[64..68]), backup);
7671
7672 let bytes = fs.reader.get_ref().clone();
7674 let mut fs2 = HfsFilesystem::open(Cursor::new(bytes), 0).unwrap();
7675 assert_eq!(fs2.mdb.create_date, create);
7676 assert_eq!(fs2.mdb.modify_date, modify);
7677 assert_eq!(BigEndian::read_u32(&fs2.mdb.raw_sector[64..68]), backup);
7678
7679 fs2.sync_metadata().unwrap();
7681 assert_ne!(fs2.mdb.modify_date, modify);
7682 }
7683
7684 #[test]
7685 fn test_hfs_set_boot_blocks_written_on_sync() {
7686 let img = make_editable_hfs_image();
7687 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7688 let mut bb = [0u8; 1024];
7689 bb[0] = 0x4C; bb[1] = 0x4B;
7691 for (i, slot) in bb.iter_mut().enumerate().skip(2).take(200) {
7692 *slot = (i & 0xFF) as u8;
7693 }
7694 fs.set_boot_blocks(&bb);
7695 fs.sync_metadata().unwrap();
7696
7697 let read_back = fs.read_boot_blocks().unwrap();
7698 assert_eq!(&read_back[..], &bb[..]);
7699 }
7700
7701 #[test]
7702 fn test_hfs_set_volume_finder_info_round_trip() {
7703 let img = make_editable_hfs_image();
7704 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7705
7706 let mut fi = [0u8; 32];
7707 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);
7711 assert_eq!(fs.mdb.finder_info[0], 100);
7712 assert_eq!(fs.mdb.finder_info[3], 200);
7713 assert_eq!(fs.mdb.finder_info[5], 300);
7714
7715 fs.sync_metadata().unwrap();
7716 let bytes = fs.reader.get_ref().clone();
7717 let fs2 = HfsFilesystem::open(Cursor::new(bytes), 0).unwrap();
7718 assert_eq!(fs2.mdb.finder_info[0], 100);
7719 assert_eq!(fs2.mdb.finder_info[3], 200);
7720 assert_eq!(fs2.mdb.finder_info[5], 300);
7721 }
7722
7723 #[test]
7724 fn test_default_btree_sizes_scale_and_floor() {
7725 let (cat, ext) = default_btree_sizes(1 * 1024 * 1024, 512);
7727 let floor = 24 * 512;
7728 assert_eq!(cat, floor, "small-volume catalog should hit floor");
7729 assert_eq!(ext, floor, "small-volume extents should hit floor");
7730
7731 let (cat, ext) = default_btree_sizes(20 * 1024 * 1024, 512);
7735 assert!(cat >= 100 * 1024, "20MB catalog too small: {cat}");
7736 assert_eq!(cat % 2048, 0, "catalog not clump-aligned");
7737 assert_eq!(ext % 2048, 0, "extents not clump-aligned");
7738 assert!(ext <= cat, "extents should not exceed catalog");
7739 assert!(ext * 2 >= cat - 2048, "extents should be ~half catalog");
7740
7741 let (cat_big, _) = default_btree_sizes(200 * 1024 * 1024, 4096);
7743 assert!(cat_big > cat, "larger volume should get a larger catalog");
7744
7745 let (c, e) = default_btree_sizes(20 * 1024 * 1024, 4096);
7748 let img = create_blank_hfs_sized(20 * 1024 * 1024, 4096, "Ref20", e, c).unwrap();
7749 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7750 let result = fs.fsck().unwrap();
7751 assert!(
7752 result.errors.is_empty(),
7753 "fsck errors on default-sized volume: {:?}",
7754 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7755 );
7756 }
7757
7758 #[test]
7759 fn test_clone_target_btree_sizes_floors_at_blank_default() {
7760 let target = 2046 * 1024 * 1024;
7765 let (default_cat, _) = default_btree_sizes(target, 32768);
7766 let (cat, _ext) = clone_target_btree_sizes(target, 32768, 2 * 1024 * 1024, 1024 * 1024);
7767 assert_eq!(
7768 cat, default_cat,
7769 "large target must floor catalog at the blank default, not 1.5x source"
7770 );
7771 assert!(
7772 cat > 2 * 1024 * 1024 * 3 / 2,
7773 "floor must exceed 1.5x source"
7774 );
7775
7776 let small_target = 40 * 1024 * 1024;
7779 let (cat2, _) = clone_target_btree_sizes(small_target, 4096, 4 * 1024 * 1024, 256 * 1024);
7780 assert_eq!(
7781 cat2,
7782 4 * 1024 * 1024 * 3 / 2,
7783 "source-derived value should win"
7784 );
7785
7786 let (cat3, ext3) = clone_target_btree_sizes(target, 32768, u32::MAX, u32::MAX);
7788 assert!(cat3 <= HFS_MAX_BTREE_FILE_SIZE);
7789 assert!(ext3 <= HFS_MAX_BTREE_FILE_SIZE);
7790 }
7791
7792 #[test]
7793 fn test_hfs_set_volume_name_roundtrip() {
7794 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "OldName").unwrap();
7797 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7798
7799 fs.set_volume_name("NewName").unwrap();
7800 assert_eq!(fs.mdb.volume_name, "NewName");
7801 fs.sync_metadata().unwrap();
7802
7803 let bytes = fs.reader.get_ref().clone();
7804 let mut fs2 = HfsFilesystem::open(Cursor::new(bytes.clone()), 0).unwrap();
7805 assert_eq!(fs2.mdb.volume_name, "NewName");
7806
7807 let new_raw = b"NewName".to_vec();
7810 assert!(
7811 fs2.find_catalog_record_by_name(1, &new_raw).is_some(),
7812 "root dir record not re-keyed to new name"
7813 );
7814 assert!(
7815 fs2.find_catalog_record_by_name(1, b"OldName").is_none(),
7816 "old root dir key still present"
7817 );
7818
7819 let (_n, _r, key_abs) = fs2.find_catalog_record_by_cnid(2).unwrap();
7821 let key_len = fs2.catalog_data[key_abs] as usize;
7822 let mut data_off = key_abs + 1 + key_len;
7823 if !data_off.is_multiple_of(2) {
7824 data_off += 1;
7825 }
7826 let thd_len = fs2.catalog_data[data_off + 14] as usize;
7827 assert_eq!(thd_len, new_raw.len(), "thdCName length not updated");
7828 assert_eq!(
7829 &fs2.catalog_data[data_off + 15..data_off + 15 + thd_len],
7830 new_raw.as_slice(),
7831 "thdCName bytes not updated"
7832 );
7833
7834 let mdb_off = 1024 + 36;
7836 assert_eq!(bytes[mdb_off] as usize, new_raw.len());
7837 assert_eq!(
7838 &bytes[mdb_off + 1..mdb_off + 1 + new_raw.len()],
7839 new_raw.as_slice()
7840 );
7841
7842 let result = fs2.fsck().unwrap();
7843 assert!(
7844 result.errors.is_empty(),
7845 "fsck errors after rename: {:?}",
7846 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7847 );
7848 }
7849
7850 #[test]
7851 fn test_drallocptr_advances_with_allocations() {
7852 let img = create_blank_hfs(4 * 1024 * 1024, 512, "AllocPtr").unwrap();
7856 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7857 assert_eq!(
7858 BigEndian::read_u16(&fs.mdb.raw_sector[16..18]),
7859 0,
7860 "fresh volume drAllocPtr should be 0"
7861 );
7862
7863 let root = fs.root().unwrap();
7864 let payload = vec![0xABu8; 8 * 1024]; let mut src = payload.as_slice();
7866 fs.create_file(
7867 &root,
7868 "ptr",
7869 &mut src,
7870 payload.len() as u64,
7871 &CreateFileOptions::default(),
7872 )
7873 .unwrap();
7874 let after = BigEndian::read_u16(&fs.mdb.raw_sector[16..18]);
7875 assert!(after > 0, "drAllocPtr should advance after allocation");
7876 }
7877
7878 #[test]
7879 fn test_hfs_set_volume_name_rejects_invalid() {
7880 let img = create_blank_hfs(1 * 1024 * 1024, 512, "X").unwrap();
7881 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7882 assert!(
7883 fs.set_volume_name("").is_err(),
7884 "empty name must be rejected"
7885 );
7886 let too_long = "A".repeat(28);
7887 assert!(
7888 fs.set_volume_name(&too_long).is_err(),
7889 ">27 byte name must be rejected"
7890 );
7891 assert_eq!(fs.mdb.volume_name, "X");
7893 }
7894
7895 #[test]
7896 fn test_hfs_finder_info_setters_survive_fsck() {
7897 let (mut fs, fe) = make_fs_with_file();
7899 fs.set_finder_info(&fe, [0x11; 16], [0x22; 16]).unwrap();
7900 fs.set_dates(&fe, 1, 2, 3).unwrap();
7901 fs.set_file_locked(&fe, true).unwrap();
7902 fs.set_volume_dates(10, 20, 30);
7903 fs.set_volume_finder_info(&[0x33; 32]);
7904 fs.sync_metadata().unwrap();
7905 let result = fs.fsck().unwrap();
7906 assert!(
7907 result.errors.is_empty(),
7908 "fsck errors: {:?}",
7909 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7910 );
7911 }
7912
7913 #[test]
7914 fn sized_streaming_matches_in_memory() {
7915 for _ in 0..5 {
7917 let t0 = hfs_common::hfs_now();
7918 let mem = create_blank_hfs_sized(20 << 20, 1024, "Vol", 64 << 10, 128 << 10).unwrap();
7919 let mut cur = std::io::Cursor::new(vec![0u8; 20 << 20]);
7920 let len =
7921 write_blank_hfs_sized_into(&mut cur, 0, 20 << 20, 1024, "Vol", 64 << 10, 128 << 10)
7922 .unwrap();
7923 if hfs_common::hfs_now() != t0 {
7924 continue;
7925 }
7926 assert_eq!(len as usize, mem.len());
7927 assert!(cur.get_ref()[..mem.len()] == mem[..]);
7928 return;
7929 }
7930 panic!("the clock ticked during every attempt");
7931 }
7932}