1#[cfg(feature = "rust173-polyfill")]
69use disk_core::rust173::IntIsMultipleOf as _;
70use std::io::{Read, Seek, SeekFrom, Write};
71
72use byteorder::{BigEndian, ByteOrder};
73
74use disk_core::entry::{EntryType, FileEntry};
75use disk_core::filesystem::{
76 CreateDirectoryOptions, CreateFileOptions, EditableFilesystem, Filesystem, FilesystemError,
77};
78use disk_core::fsck::{FsckIssue, FsckResult, FsckStats, RepairReport};
79
80pub const MFS_SIGNATURE: u16 = 0xD2D7;
82
83const MDB_OFFSET: u64 = 1024;
85
86pub const MFS_400K_BYTES: u64 = 409_600;
88
89pub const MFS_800K_BYTES: u64 = 819_200;
91
92const MAX_ALLOC_BLOCKS: u32 = 0xFFFF;
94
95const MAX_ALLOC_CHAIN: usize = 4096;
99
100#[derive(Debug, Clone)]
103pub struct MfsMdb {
104 pub create_date: u32,
105 pub last_backup_date: u32,
106 pub volume_attributes: u16,
107 pub num_files: u16,
108 pub dir_start_sector: u16,
109 pub dir_length_sectors: u16,
110 pub num_alloc_blocks: u16,
111 pub alloc_block_size: u32,
112 pub clump_size: u32,
113 pub first_alloc_block_sector: u16,
114 pub next_file_number: u32,
115 pub free_blocks: u16,
116 pub volume_name: String,
117 map_offset: u64,
120}
121
122impl MfsMdb {
123 fn parse(buf: &[u8; 512]) -> Result<Self, FilesystemError> {
125 let sig = BigEndian::read_u16(&buf[0..2]);
126 if sig != MFS_SIGNATURE {
127 return Err(FilesystemError::InvalidData(format!(
128 "bad MFS signature: 0x{sig:04X} (expected 0x{MFS_SIGNATURE:04X})"
129 )));
130 }
131
132 let create_date = BigEndian::read_u32(&buf[2..6]);
133 let last_backup_date = BigEndian::read_u32(&buf[6..10]);
134 let volume_attributes = BigEndian::read_u16(&buf[10..12]);
135 let num_files = BigEndian::read_u16(&buf[12..14]);
136 let dir_start_sector = BigEndian::read_u16(&buf[14..16]);
137 let dir_length_sectors = BigEndian::read_u16(&buf[16..18]);
138 let num_alloc_blocks = BigEndian::read_u16(&buf[18..20]);
139 let alloc_block_size = BigEndian::read_u32(&buf[20..24]);
140 let clump_size = BigEndian::read_u32(&buf[24..28]);
141 let first_alloc_block_sector = BigEndian::read_u16(&buf[28..30]);
142 let next_file_number = BigEndian::read_u32(&buf[30..34]);
143 let free_blocks = BigEndian::read_u16(&buf[34..36]);
144
145 let name_len = buf[36] as usize;
147 if 37 + name_len > 64 {
148 return Err(FilesystemError::InvalidData(format!(
151 "MFS volume name length {name_len} exceeds 27"
152 )));
153 }
154 let volume_name = crate::hfs::mac_roman_to_utf8(&buf[37..37 + name_len]);
155
156 let map_offset = MDB_OFFSET + 64;
164
165 if alloc_block_size == 0 || alloc_block_size % 512 != 0 {
167 return Err(FilesystemError::InvalidData(format!(
168 "MFS allocation block size {alloc_block_size} is not a positive multiple of 512"
169 )));
170 }
171 if num_alloc_blocks as u32 > MAX_ALLOC_BLOCKS {
172 return Err(FilesystemError::InvalidData(format!(
173 "MFS allocation-block count {num_alloc_blocks} exceeds 65535"
174 )));
175 }
176 if dir_length_sectors == 0 {
177 return Err(FilesystemError::InvalidData(
178 "MFS file directory length is 0".into(),
179 ));
180 }
181
182 Ok(MfsMdb {
183 create_date,
184 last_backup_date,
185 volume_attributes,
186 num_files,
187 dir_start_sector,
188 dir_length_sectors,
189 num_alloc_blocks,
190 alloc_block_size,
191 clump_size,
192 first_alloc_block_sector,
193 next_file_number,
194 free_blocks,
195 volume_name,
196 map_offset,
197 })
198 }
199}
200
201#[derive(Debug, Clone)]
203#[allow(dead_code)] struct MfsDirEntry {
205 flags: u8,
207 finder_info: [u8; 16],
209 file_number: u32,
210 data_first_block: u16,
211 data_logical_length: u32,
212 rsrc_first_block: u16,
213 rsrc_logical_length: u32,
214 create_date: u32,
215 modify_date: u32,
216 name: String,
217}
218
219impl MfsDirEntry {
220 fn is_in_use(&self) -> bool {
221 self.flags & 0x80 != 0
222 }
223
224 fn finder_flags(&self) -> u16 {
228 u16::from_be_bytes([self.finder_info[8], self.finder_info[9]])
229 }
230}
231
232pub struct MfsFilesystem<R: Read + Seek + Send> {
236 reader: R,
237 partition_offset: u64,
238 pub(crate) mdb: MfsMdb,
239 entries: Vec<MfsDirEntry>,
242 map_bytes: Vec<u8>,
244}
245
246impl<R: Read + Seek + Send> MfsFilesystem<R> {
247 pub fn open(mut reader: R, partition_offset: u64) -> Result<Self, FilesystemError> {
249 reader.seek(SeekFrom::Start(partition_offset + MDB_OFFSET))?;
250 let mut mdb_buf = [0u8; 512];
251 reader.read_exact(&mut mdb_buf)?;
252 let mdb = MfsMdb::parse(&mdb_buf)?;
253
254 let map_bits = mdb.num_alloc_blocks as usize * 12;
257 let map_byte_len = map_bits.div_ceil(8);
258 let mut map_bytes = vec![0u8; map_byte_len];
259 reader.seek(SeekFrom::Start(partition_offset + mdb.map_offset))?;
260 reader.read_exact(&mut map_bytes)?;
261
262 let mut entries = Vec::new();
264 for sector_idx in 0..mdb.dir_length_sectors {
265 let sector_no = mdb.dir_start_sector as u64 + sector_idx as u64;
266 let sector_off = partition_offset + sector_no * 512;
267 reader.seek(SeekFrom::Start(sector_off))?;
268 let mut sec = [0u8; 512];
269 if reader.read_exact(&mut sec).is_err() {
270 break;
271 }
272 parse_dir_sector(&sec, &mut entries);
273 }
274
275 Ok(MfsFilesystem {
276 reader,
277 partition_offset,
278 mdb,
279 entries,
280 map_bytes,
281 })
282 }
283
284 fn entry_by_file_number(&self, fnum: u32) -> Option<&MfsDirEntry> {
286 self.entries
287 .iter()
288 .find(|e| e.is_in_use() && e.file_number == fnum)
289 }
290
291 fn map_get(&self, block: u16) -> u16 {
294 if block < 2 {
295 return 0;
296 }
297 let bit_off = (block as usize - 2) * 12;
301 let byte_off = bit_off / 8;
302 if byte_off + 1 >= self.map_bytes.len() {
303 return 0;
304 }
305 if block.is_multiple_of(2) {
308 let hi = self.map_bytes[byte_off] as u16;
312 let lo = (self.map_bytes[byte_off + 1] >> 4) as u16;
313 (hi << 4) | lo
314 } else {
315 let hi = (self.map_bytes[byte_off] & 0x0F) as u16;
316 let lo = self.map_bytes[byte_off + 1] as u16;
317 (hi << 8) | lo
318 }
319 }
320
321 fn alloc_block_offset(&self, block: u16) -> u64 {
323 debug_assert!(block >= 2);
326 let sectors_per_block = (self.mdb.alloc_block_size / 512) as u64;
327 let sector =
328 self.mdb.first_alloc_block_sector as u64 + (block as u64 - 2) * sectors_per_block;
329 sector * 512
330 }
331
332 fn read_chain(&mut self, first: u16, logical_len: u32) -> Result<Vec<u8>, FilesystemError> {
336 if first == 0 || logical_len == 0 {
337 return Ok(Vec::new());
338 }
339 let mut data = Vec::with_capacity(logical_len as usize);
340 let bs = self.mdb.alloc_block_size as usize;
341 let mut block = first;
342 for _hop in 0..MAX_ALLOC_CHAIN {
343 if block < 2 || block as u32 - 2 >= self.mdb.num_alloc_blocks as u32 {
344 return Err(FilesystemError::InvalidData(format!(
345 "MFS chain references out-of-range alloc block {block}"
346 )));
347 }
348 let off = self.partition_offset + self.alloc_block_offset(block);
349 self.reader.seek(SeekFrom::Start(off))?;
350 let want = (logical_len as usize).saturating_sub(data.len()).min(bs);
351 let mut chunk = vec![0u8; bs];
352 self.reader.read_exact(&mut chunk)?;
353 data.extend_from_slice(&chunk[..want]);
354 if data.len() >= logical_len as usize {
355 break;
356 }
357 let next = self.map_get(block);
358 if next == 1 {
359 break;
362 }
363 if next == 0 {
364 return Err(FilesystemError::InvalidData(format!(
365 "MFS chain hits a free block at {block}"
366 )));
367 }
368 block = next;
369 }
370 Ok(data)
371 }
372
373 pub fn read_resource_fork(&mut self, entry: &FileEntry) -> Result<Vec<u8>, FilesystemError> {
379 let fnum = entry.location as u32;
380 let de = self
381 .entry_by_file_number(fnum)
382 .ok_or_else(|| FilesystemError::NotFound(format!("file number {fnum} not found")))?
383 .clone();
384 self.read_chain(de.rsrc_first_block, de.rsrc_logical_length)
385 }
386}
387
388fn parse_dir_sector(sec: &[u8; 512], out: &mut Vec<MfsDirEntry>) {
392 let mut off = 0usize;
393 while off + 50 < sec.len() {
394 let flags = sec[off];
395 if flags & 0x80 == 0 {
396 break;
398 }
399 let mut finder_info = [0u8; 16];
400 finder_info.copy_from_slice(&sec[off + 2..off + 18]);
401 let file_number = BigEndian::read_u32(&sec[off + 18..off + 22]);
402 let data_first_block = BigEndian::read_u16(&sec[off + 22..off + 24]);
403 let data_logical_length = BigEndian::read_u32(&sec[off + 24..off + 28]);
404 let _data_phys_length = BigEndian::read_u32(&sec[off + 28..off + 32]);
405 let rsrc_first_block = BigEndian::read_u16(&sec[off + 32..off + 34]);
406 let rsrc_logical_length = BigEndian::read_u32(&sec[off + 34..off + 38]);
407 let _rsrc_phys_length = BigEndian::read_u32(&sec[off + 38..off + 42]);
408 let create_date = BigEndian::read_u32(&sec[off + 42..off + 46]);
409 let modify_date = BigEndian::read_u32(&sec[off + 46..off + 50]);
410 let name_len = sec[off + 50] as usize;
411 if off + 51 + name_len > sec.len() {
412 break;
414 }
415 let name = crate::hfs::mac_roman_to_utf8(&sec[off + 51..off + 51 + name_len]);
416
417 out.push(MfsDirEntry {
418 flags,
419 finder_info,
420 file_number,
421 data_first_block,
422 data_logical_length,
423 rsrc_first_block,
424 rsrc_logical_length,
425 create_date,
426 modify_date,
427 name,
428 });
429
430 let entry_len = 51 + name_len;
432 let padded = entry_len + (entry_len & 1);
433 off += padded;
434 }
435}
436
437pub const MFS_MAX_NAME_LEN: usize = 27;
440
441fn mfs_names_equal(a: &str, b: &str) -> bool {
444 let a_bytes = crate::hfs::utf8_to_mac_roman(a).unwrap_or_else(|_| a.as_bytes().to_vec());
445 let b_bytes = crate::hfs::utf8_to_mac_roman(b).unwrap_or_else(|_| b.as_bytes().to_vec());
446 crate::hfs_common::compare_hfs_keys(0, &a_bytes, 0, &b_bytes) == std::cmp::Ordering::Equal
447}
448
449fn validate_mfs_name(name: &str) -> Result<Vec<u8>, FilesystemError> {
453 if name.is_empty() {
454 return Err(FilesystemError::InvalidData("MFS filename is empty".into()));
455 }
456 if name.contains(':') {
457 return Err(FilesystemError::InvalidData(
458 "MFS filename contains ':' (Mac path separator) — use '-' or '_'".into(),
459 ));
460 }
461 let bytes = crate::hfs::utf8_to_mac_roman(name).map_err(|_| {
462 FilesystemError::InvalidData(
463 "MFS filename contains characters not representable in Mac Roman".into(),
464 )
465 })?;
466 if bytes.len() > MFS_MAX_NAME_LEN {
467 return Err(FilesystemError::InvalidData(format!(
468 "MFS filename is {} bytes; max is {MFS_MAX_NAME_LEN}",
469 bytes.len()
470 )));
471 }
472 Ok(bytes)
473}
474
475impl MfsDirEntry {
476 fn encode(&self, alloc_block_size: u32) -> Vec<u8> {
481 let physical = |logical: u32| -> u32 {
483 if logical == 0 || alloc_block_size == 0 {
484 0
485 } else {
486 logical
487 .div_ceil(alloc_block_size)
488 .saturating_mul(alloc_block_size)
489 }
490 };
491 let name_bytes = crate::hfs::utf8_to_mac_roman(&self.name).unwrap_or_default();
492 let name_len = name_bytes.len().min(MFS_MAX_NAME_LEN) as u8;
493 let total = 51 + name_len as usize;
494 let padded = total + (total & 1);
495 let mut buf = vec![0u8; padded];
496 buf[0] = self.flags;
497 buf[2..18].copy_from_slice(&self.finder_info);
499 BigEndian::write_u32(&mut buf[18..22], self.file_number);
500 BigEndian::write_u16(&mut buf[22..24], self.data_first_block);
501 BigEndian::write_u32(&mut buf[24..28], self.data_logical_length);
502 BigEndian::write_u32(&mut buf[28..32], physical(self.data_logical_length));
503 BigEndian::write_u16(&mut buf[32..34], self.rsrc_first_block);
504 BigEndian::write_u32(&mut buf[34..38], self.rsrc_logical_length);
505 BigEndian::write_u32(&mut buf[38..42], physical(self.rsrc_logical_length));
506 BigEndian::write_u32(&mut buf[42..46], self.create_date);
507 BigEndian::write_u32(&mut buf[46..50], self.modify_date);
508 buf[50] = name_len;
509 buf[51..51 + name_len as usize].copy_from_slice(&name_bytes[..name_len as usize]);
510 buf
511 }
512}
513
514impl<R: Read + Write + Seek + Send> MfsFilesystem<R> {
515 fn map_set(&mut self, block: u16, value: u16) {
520 debug_assert!(
521 block as u32 >= 2 && (block as u32) < self.mdb.num_alloc_blocks as u32 + 2,
522 "map_set out-of-range block {block}"
523 );
524 let value = value & 0x0FFF;
525 let bit_off = (block as usize - 2) * 12;
527 let byte_off = bit_off / 8;
528 if block.is_multiple_of(2) {
529 self.map_bytes[byte_off] = (value >> 4) as u8;
530 self.map_bytes[byte_off + 1] =
531 (self.map_bytes[byte_off + 1] & 0x0F) | (((value & 0x0F) as u8) << 4);
532 } else {
533 self.map_bytes[byte_off] =
534 (self.map_bytes[byte_off] & 0xF0) | ((value >> 8) as u8 & 0x0F);
535 self.map_bytes[byte_off + 1] = (value & 0xFF) as u8;
536 }
537 }
538
539 fn alloc_chain(&mut self, count: u32) -> Result<u16, FilesystemError> {
543 if count == 0 {
544 return Ok(0);
545 }
546 if (count as u16) > self.mdb.free_blocks {
547 return Err(FilesystemError::InvalidData(format!(
548 "out of space: need {count} alloc blocks, have {} free",
549 self.mdb.free_blocks
550 )));
551 }
552 let mut picked = Vec::with_capacity(count as usize);
554 for b in 2..(self.mdb.num_alloc_blocks as u32 + 2) {
558 if self.map_get(b as u16) == 0 {
559 picked.push(b as u16);
560 if picked.len() == count as usize {
561 break;
562 }
563 }
564 }
565 if picked.len() < count as usize {
566 return Err(FilesystemError::InvalidData(format!(
567 "fragmentation: {} free per MDB but only {} blocks found in map",
568 self.mdb.free_blocks,
569 picked.len()
570 )));
571 }
572 for w in picked.windows(2) {
574 let cur = w[0];
575 let nxt = w[1];
576 self.map_set(cur, nxt);
577 }
578 let last = *picked.last().expect("picked non-empty");
580 self.map_set(last, 1);
581 self.mdb.free_blocks -= count as u16;
582 Ok(picked[0])
583 }
584
585 fn free_chain(&mut self, first: u16) -> Result<(), FilesystemError> {
589 if first == 0 {
590 return Ok(());
591 }
592 let mut block = first;
593 let mut visited = std::collections::HashSet::new();
594 for _hop in 0..MAX_ALLOC_CHAIN {
595 if block < 2 || block as u32 - 2 >= self.mdb.num_alloc_blocks as u32 {
596 return Err(FilesystemError::InvalidData(format!(
597 "free_chain hit out-of-range block {block}"
598 )));
599 }
600 if !visited.insert(block) {
601 return Err(FilesystemError::InvalidData(format!(
602 "free_chain cycle at block {block}"
603 )));
604 }
605 let next = self.map_get(block);
606 self.map_set(block, 0);
607 self.mdb.free_blocks += 1;
608 if next == 1 || next == 0 {
609 return Ok(());
610 }
611 block = next;
612 }
613 Err(FilesystemError::InvalidData(
614 "free_chain ran past MAX_ALLOC_CHAIN".into(),
615 ))
616 }
617
618 fn alloc_and_write_fork(&mut self, bytes: &[u8]) -> Result<(u16, u32), FilesystemError> {
622 let bs = self.mdb.alloc_block_size as usize;
623 let count = if bytes.is_empty() {
624 0
625 } else {
626 bytes.len().div_ceil(bs) as u32
627 };
628 let first = self.alloc_chain(count)?;
629 if first != 0 {
630 self.write_data_chain(first, bytes)?;
631 }
632 Ok((first, bytes.len() as u32))
633 }
634
635 fn write_data_chain(&mut self, first: u16, data: &[u8]) -> Result<(), FilesystemError> {
640 if first == 0 || data.is_empty() {
641 return Ok(());
642 }
643 let bs = self.mdb.alloc_block_size as usize;
644 let mut written = 0usize;
645 let mut block = first;
646 for _hop in 0..MAX_ALLOC_CHAIN {
647 let chunk_off = written;
648 let chunk_end = (written + bs).min(data.len());
649 let abs = self.partition_offset + self.alloc_block_offset(block);
650 self.reader.seek(SeekFrom::Start(abs))?;
651 let chunk = &data[chunk_off..chunk_end];
652 self.reader.write_all(chunk)?;
653 if chunk.len() < bs {
655 let pad = vec![0u8; bs - chunk.len()];
656 self.reader.write_all(&pad)?;
657 }
658 written = chunk_end;
659 if written >= data.len() {
660 return Ok(());
661 }
662 let next = self.map_get(block);
663 if next == 1 || next == 0 {
664 if written < data.len() {
665 return Err(FilesystemError::InvalidData(format!(
666 "write_data_chain short-chained: {} bytes unwritten",
667 data.len() - written
668 )));
669 }
670 return Ok(());
671 }
672 block = next;
673 }
674 Err(FilesystemError::InvalidData(
675 "write_data_chain ran past MAX_ALLOC_CHAIN".into(),
676 ))
677 }
678
679 fn dir_write_back(&mut self) -> Result<(), FilesystemError> {
684 let sector_count = self.mdb.dir_length_sectors as usize;
685 let mut packed = vec![vec![0u8; 512]; sector_count];
686 let mut sector_idx = 0usize;
687 let mut byte_off = 0usize;
688 for e in self.entries.iter().filter(|e| e.is_in_use()) {
689 let bytes = e.encode(self.mdb.alloc_block_size);
690 if byte_off + bytes.len() + 1 > 512 {
693 sector_idx += 1;
694 byte_off = 0;
695 if sector_idx >= sector_count {
696 return Err(FilesystemError::InvalidData(format!(
697 "directory full: {} sectors of {sector_count} can't hold {} entries",
698 sector_count,
699 self.entries.iter().filter(|e| e.is_in_use()).count()
700 )));
701 }
702 }
703 packed[sector_idx][byte_off..byte_off + bytes.len()].copy_from_slice(&bytes);
704 byte_off += bytes.len();
705 }
706 for (i, sec) in packed.into_iter().enumerate() {
707 let sector_no = self.mdb.dir_start_sector as u64 + i as u64;
708 let off = self.partition_offset + sector_no * 512;
709 self.reader.seek(SeekFrom::Start(off))?;
710 self.reader.write_all(&sec)?;
711 }
712 Ok(())
713 }
714
715 fn mdb_write_back(&mut self) -> Result<(), FilesystemError> {
717 self.reader
721 .seek(SeekFrom::Start(self.partition_offset + MDB_OFFSET))?;
722 let mut buf = [0u8; 512];
723 self.reader.read_exact(&mut buf)?;
724 BigEndian::write_u16(&mut buf[0..2], MFS_SIGNATURE);
726 BigEndian::write_u32(&mut buf[2..6], self.mdb.create_date);
727 BigEndian::write_u32(&mut buf[6..10], self.mdb.last_backup_date);
728 BigEndian::write_u16(&mut buf[10..12], self.mdb.volume_attributes);
729 BigEndian::write_u16(&mut buf[12..14], self.mdb.num_files);
730 BigEndian::write_u16(&mut buf[14..16], self.mdb.dir_start_sector);
731 BigEndian::write_u16(&mut buf[16..18], self.mdb.dir_length_sectors);
732 BigEndian::write_u16(&mut buf[18..20], self.mdb.num_alloc_blocks);
733 BigEndian::write_u32(&mut buf[20..24], self.mdb.alloc_block_size);
734 BigEndian::write_u32(&mut buf[24..28], self.mdb.clump_size);
735 BigEndian::write_u16(&mut buf[28..30], self.mdb.first_alloc_block_sector);
736 BigEndian::write_u32(&mut buf[30..34], self.mdb.next_file_number);
737 BigEndian::write_u16(&mut buf[34..36], self.mdb.free_blocks);
738 let name_bytes = crate::hfs::utf8_to_mac_roman(&self.mdb.volume_name).unwrap_or_default();
739 let name_len = name_bytes.len().min(MFS_MAX_NAME_LEN);
740 buf[36] = name_len as u8;
741 for b in buf[37..37 + MFS_MAX_NAME_LEN].iter_mut() {
743 *b = 0;
744 }
745 buf[37..37 + name_len].copy_from_slice(&name_bytes[..name_len]);
746 self.reader
747 .seek(SeekFrom::Start(self.partition_offset + MDB_OFFSET))?;
748 self.reader.write_all(&buf)?;
749 Ok(())
750 }
751
752 fn map_write_back(&mut self) -> Result<(), FilesystemError> {
756 self.reader
757 .seek(SeekFrom::Start(self.partition_offset + self.mdb.map_offset))?;
758 let bytes = self.map_bytes.clone();
759 self.reader.write_all(&bytes)?;
760 Ok(())
761 }
762}
763struct MfsWalk {
773 used: Vec<bool>,
774 files: u32,
775 broken: Vec<String>,
776}
777
778impl<R: Read + Seek + Send> MfsFilesystem<R> {
779 fn last_block(&self) -> u16 {
781 self.mdb.num_alloc_blocks + 1
782 }
783
784 fn walk_fork(
788 &self,
789 first: u16,
790 owner: &str,
791 fork: &str,
792 used: &mut [bool],
793 broken: &mut Vec<String>,
794 ) {
795 if first == 0 {
796 return;
797 }
798 let last = self.last_block();
799 let mut block = first;
800 for _ in 0..MAX_ALLOC_CHAIN {
801 if block < 2 || block > last {
802 broken.push(format!(
803 "file '{owner}' {fork} fork links to block {block}, outside the volume (2..={last})"
804 ));
805 return;
806 }
807 if used[block as usize] {
808 broken.push(format!(
809 "block {block} is cross-linked (claimed by '{owner}' {fork} fork and another chain)"
810 ));
811 return;
812 }
813 used[block as usize] = true;
814 let next = self.map_get(block);
815 if next == 1 {
816 return; }
818 if next == 0 {
819 broken.push(format!(
820 "file '{owner}' {fork} fork chain hits a free block after {block}"
821 ));
822 return;
823 }
824 block = next;
825 }
826 broken.push(format!(
827 "file '{owner}' {fork} fork chain exceeds {MAX_ALLOC_CHAIN} blocks (runaway)"
828 ));
829 }
830
831 fn compute_used(&self) -> MfsWalk {
835 let mut used = vec![false; self.last_block() as usize + 1];
836 let mut broken = Vec::new();
837 let mut files = 0u32;
838 let entries: Vec<MfsDirEntry> = self
839 .entries
840 .iter()
841 .filter(|e| e.is_in_use())
842 .cloned()
843 .collect();
844 for de in &entries {
845 files += 1;
846 self.walk_fork(
847 de.data_first_block,
848 &de.name,
849 "data",
850 &mut used,
851 &mut broken,
852 );
853 self.walk_fork(
854 de.rsrc_first_block,
855 &de.name,
856 "resource",
857 &mut used,
858 &mut broken,
859 );
860 }
861 MfsWalk {
862 used,
863 files,
864 broken,
865 }
866 }
867
868 fn map_free_count(&self) -> u32 {
870 (2..=self.last_block())
871 .filter(|&b| self.map_get(b) == 0)
872 .count() as u32
873 }
874
875 fn run_fsck(&mut self) -> Result<FsckResult, FilesystemError> {
880 let walk = self.compute_used();
881 let walk_clean = walk.broken.is_empty();
882 let last = self.last_block();
883
884 let mut leaked = 0u32;
887 for b in 2..=last {
888 if self.map_get(b) != 0 && !walk.used[b as usize] {
889 leaked += 1;
890 }
891 }
892
893 let mut errors = Vec::new();
894 for msg in &walk.broken {
895 errors.push(FsckIssue {
896 code: "MfsBrokenChain".into(),
897 message: msg.clone(),
898 repairable: false,
899 debug: false,
900 });
901 }
902 if leaked > 0 {
903 errors.push(FsckIssue {
904 code: "MfsLostBlocks".into(),
905 message: format!(
906 "{leaked} allocation block(s) marked used but unreachable from any file"
907 ),
908 repairable: walk_clean,
909 debug: false,
910 });
911 }
912
913 let map_free = self.map_free_count();
917 if self.mdb.free_blocks as u32 != map_free {
918 errors.push(FsckIssue {
919 code: "MfsFreeCountMismatch".into(),
920 message: format!(
921 "MDB free-block count is {} but the allocation map has {map_free} free",
922 self.mdb.free_blocks
923 ),
924 repairable: walk_clean,
925 debug: false,
926 });
927 }
928
929 if self.mdb.num_files as u32 != walk.files {
931 errors.push(FsckIssue {
932 code: "MfsFileCountMismatch".into(),
933 message: format!(
934 "MDB file count is {} but the directory holds {} file(s)",
935 self.mdb.num_files, walk.files
936 ),
937 repairable: walk_clean,
938 debug: false,
939 });
940 }
941
942 let repairable = errors.iter().any(|e| e.repairable);
943 let free_after = map_free + leaked; Ok(FsckResult {
945 errors,
946 warnings: Vec::new(),
947 stats: FsckStats {
948 files_checked: walk.files,
949 directories_checked: 1,
950 extra: vec![
951 (
952 "free blocks".into(),
953 format!("{free_after} / {}", self.mdb.num_alloc_blocks),
954 ),
955 (
956 "allocation map".into(),
957 if leaked == 0 && walk_clean {
958 "consistent".into()
959 } else {
960 "needs rebuild".into()
961 },
962 ),
963 ],
964 },
965 repairable,
966 orphaned_entries: Vec::new(),
967 })
968 }
969}
970
971impl<R: Read + Write + Seek + Send> MfsFilesystem<R> {
976 fn run_repair(&mut self) -> Result<RepairReport, FilesystemError> {
982 let walk = self.compute_used();
983 if !walk.broken.is_empty() {
984 return Ok(RepairReport {
985 fixes_applied: Vec::new(),
986 fixes_failed: vec![
987 "structural damage (broken/cross-linked chain) — the allocation map was left \
988 unchanged to avoid losing live blocks"
989 .into(),
990 ],
991 unrepairable_count: walk.broken.len(),
992 });
993 }
994
995 let last = self.last_block();
996 let mut freed = 0u32;
997 for b in 2..=last {
998 if self.map_get(b) != 0 && !walk.used[b as usize] {
999 self.map_set(b, 0);
1000 freed += 1;
1001 }
1002 }
1003
1004 let map_free = self.map_free_count();
1005 let mut fixes = Vec::new();
1006 if freed > 0 {
1007 fixes.push(format!(
1008 "reclaimed {freed} unreachable allocation block(s) into the free map"
1009 ));
1010 }
1011 if self.mdb.free_blocks as u32 != map_free {
1012 fixes.push(format!("corrected the MDB free-block count to {map_free}"));
1013 self.mdb.free_blocks = map_free as u16;
1014 }
1015 if self.mdb.num_files as u32 != walk.files {
1016 fixes.push(format!("corrected the MDB file count to {}", walk.files));
1017 self.mdb.num_files = walk.files as u16;
1018 }
1019
1020 if fixes.is_empty() {
1021 return Ok(RepairReport {
1022 fixes_applied: Vec::new(),
1023 fixes_failed: Vec::new(),
1024 unrepairable_count: 0,
1025 });
1026 }
1027
1028 self.map_write_back()?;
1029 self.mdb_write_back()?;
1030 self.reader.flush()?;
1031 Ok(RepairReport {
1032 fixes_applied: fixes,
1033 fixes_failed: Vec::new(),
1034 unrepairable_count: 0,
1035 })
1036 }
1037}
1038
1039impl<R: Read + Seek + Send> Filesystem for MfsFilesystem<R> {
1040 fn root(&mut self) -> Result<FileEntry, FilesystemError> {
1041 Ok(FileEntry::new_directory("/".into(), "/".into(), 0))
1042 }
1043
1044 fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
1045 if entry.path != "/" {
1046 return Ok(Vec::new());
1048 }
1049 let mut out = Vec::with_capacity(self.entries.len());
1050 for de in &self.entries {
1051 if !de.is_in_use() {
1052 continue;
1053 }
1054 let path = format!("/{}", de.name);
1055 let mut fe = FileEntry::new_file(
1056 de.name.clone(),
1057 path,
1058 de.data_logical_length as u64,
1059 de.file_number as u64,
1060 );
1061 fe.type_code = Some(de.finder_info[0..4].try_into().unwrap());
1062 fe.creator_code = Some(de.finder_info[4..8].try_into().unwrap());
1063 fe.finder_flags = Some(de.finder_flags());
1064 if de.rsrc_logical_length > 0 {
1065 fe.resource_fork_size = Some(de.rsrc_logical_length as u64);
1066 }
1067 fe.modified_unix = disk_core::times::mac_local_to_unix(de.modify_date);
1069 if de.modify_date != 0 || de.create_date != 0 {
1070 fe.mac_dates = Some((de.create_date, de.modify_date, 0));
1071 if let Some(s) = crate::hfs_common::format_mac_date(de.modify_date) {
1072 fe.modified = Some(s);
1073 }
1074 }
1075 out.push(fe);
1076 }
1077 out.sort_by_key(|a| a.name.to_lowercase());
1078 Ok(out)
1079 }
1080
1081 fn read_file(
1082 &mut self,
1083 entry: &FileEntry,
1084 max_bytes: usize,
1085 ) -> Result<Vec<u8>, FilesystemError> {
1086 let fnum = entry.location as u32;
1087 let de = self
1088 .entry_by_file_number(fnum)
1089 .ok_or_else(|| FilesystemError::NotFound(format!("file number {fnum} not found")))?
1090 .clone();
1091 let mut data = self.read_chain(de.data_first_block, de.data_logical_length)?;
1092 data.truncate(max_bytes);
1093 Ok(data)
1094 }
1095
1096 fn validate_name(&self, name: &str) -> Result<(), FilesystemError> {
1097 validate_mfs_name(name).map(|_| ())
1098 }
1099 fn fs_type(&self) -> &str {
1100 "MFS"
1101 }
1102
1103 fn volume_label(&self) -> Option<&str> {
1104 if self.mdb.volume_name.is_empty() {
1105 None
1106 } else {
1107 Some(&self.mdb.volume_name)
1108 }
1109 }
1110
1111 fn total_size(&self) -> u64 {
1112 self.mdb.num_alloc_blocks as u64 * self.mdb.alloc_block_size as u64
1113 }
1114
1115 fn used_size(&self) -> u64 {
1116 (self.mdb.num_alloc_blocks - self.mdb.free_blocks) as u64 * self.mdb.alloc_block_size as u64
1117 }
1118
1119 fn allocation_unit(&self) -> Option<u64> {
1125 if self.mdb.alloc_block_size == 0 {
1126 None
1127 } else {
1128 Some(self.mdb.alloc_block_size as u64)
1129 }
1130 }
1131
1132 fn write_resource_fork_to(
1138 &mut self,
1139 entry: &FileEntry,
1140 writer: &mut dyn std::io::Write,
1141 ) -> Result<u64, FilesystemError> {
1142 let data = self.read_resource_fork(entry)?;
1143 writer.write_all(&data)?;
1144 Ok(data.len() as u64)
1145 }
1146
1147 fn resource_fork_size(&mut self, entry: &FileEntry) -> u64 {
1148 self.entry_by_file_number(entry.location as u32)
1149 .map(|de| de.rsrc_logical_length as u64)
1150 .unwrap_or(0)
1151 }
1152
1153 fn fsck(&mut self) -> Option<Result<FsckResult, FilesystemError>> {
1154 Some(self.run_fsck())
1155 }
1156}
1157
1158impl<R: Read + Write + Seek + Send> EditableFilesystem for MfsFilesystem<R> {
1159 fn as_filesystem(&self) -> &dyn disk_core::filesystem::Filesystem {
1160 self
1161 }
1162 fn as_filesystem_mut(&mut self) -> &mut dyn disk_core::filesystem::Filesystem {
1163 self
1164 }
1165 fn create_file(
1166 &mut self,
1167 parent: &FileEntry,
1168 name: &str,
1169 data: &mut dyn std::io::Read,
1170 data_len: u64,
1171 options: &CreateFileOptions,
1172 ) -> Result<FileEntry, FilesystemError> {
1173 if parent.path != "/" {
1174 return Err(FilesystemError::Unsupported(
1175 "MFS is flat; only the root directory accepts new files".into(),
1176 ));
1177 }
1178 let _ = validate_mfs_name(name)?;
1179 if self
1181 .entries
1182 .iter()
1183 .any(|e| e.is_in_use() && mfs_names_equal(&e.name, name))
1184 {
1185 return Err(FilesystemError::InvalidData(format!(
1186 "MFS file '{name}' already exists"
1187 )));
1188 }
1189 if data_len > u32::MAX as u64 {
1190 return Err(FilesystemError::InvalidData(
1191 "MFS file size capped at u32::MAX bytes".into(),
1192 ));
1193 }
1194 let mut bytes = Vec::with_capacity(data_len as usize);
1197 data.read_to_end(&mut bytes)?;
1198 if bytes.len() as u64 != data_len {
1199 return Err(FilesystemError::InvalidData(format!(
1200 "data_len {data_len} != actual {} bytes",
1201 bytes.len()
1202 )));
1203 }
1204 let (first_block, _) = self.alloc_and_write_fork(&bytes)?;
1205
1206 let rsrc_bytes: Vec<u8> = match &options.resource_fork {
1211 Some(disk_core::filesystem::ResourceForkSource::Data(d)) => d.clone(),
1212 Some(disk_core::filesystem::ResourceForkSource::File(p)) => {
1213 std::fs::read(p).map_err(FilesystemError::Io)?
1214 }
1215 None => Vec::new(),
1216 };
1217 let (rsrc_first_block, rsrc_logical_length) = match self.alloc_and_write_fork(&rsrc_bytes) {
1218 Ok(v) => v,
1219 Err(e) => {
1220 let _ = self.free_chain(first_block);
1221 return Err(e);
1222 }
1223 };
1224
1225 let file_number = self.mdb.next_file_number;
1226 self.mdb.next_file_number += 1;
1227 self.mdb.num_files += 1;
1228
1229 let (type_code, creator_code) = crate::hfs_common::resolve_create_type_creator(
1234 name,
1235 options.os_type,
1236 options.os_creator,
1237 options.type_code.as_deref(),
1238 options.creator_code.as_deref(),
1239 );
1240 let mut finder_info = [0u8; 16];
1241 finder_info[0..4].copy_from_slice(&type_code);
1242 finder_info[4..8].copy_from_slice(&creator_code);
1243
1244 let mtime_secs = options
1249 .unix_times
1250 .map(|t| t.mtime_or_now())
1251 .unwrap_or_else(disk_core::times::now);
1252 let stamp = disk_core::times::unix_to_mac_local(mtime_secs);
1253 let entry = MfsDirEntry {
1254 flags: 0x80, finder_info,
1256 file_number,
1257 data_first_block: first_block,
1258 data_logical_length: bytes.len() as u32,
1259 rsrc_first_block,
1260 rsrc_logical_length,
1261 create_date: stamp,
1262 modify_date: stamp,
1263 name: name.to_string(),
1264 };
1265 self.entries.push(entry);
1266 let mut fe = FileEntry::new_file(
1269 name.to_string(),
1270 format!("/{name}"),
1271 bytes.len() as u64,
1272 file_number as u64,
1273 );
1274 fe.type_code = Some(type_code);
1275 fe.creator_code = Some(creator_code);
1276 if rsrc_logical_length > 0 {
1277 fe.resource_fork_size = Some(rsrc_logical_length as u64);
1278 }
1279 fe.modified_unix = Some(mtime_secs);
1280 fe.mac_dates = Some((stamp, stamp, 0));
1281 Ok(fe)
1282 }
1283
1284 fn create_directory(
1285 &mut self,
1286 _parent: &FileEntry,
1287 _name: &str,
1288 _options: &CreateDirectoryOptions,
1289 ) -> Result<FileEntry, FilesystemError> {
1290 Err(FilesystemError::Unsupported(
1291 "MFS has no directory hierarchy — all files live in the volume root".into(),
1292 ))
1293 }
1294
1295 fn delete_entry(
1296 &mut self,
1297 parent: &FileEntry,
1298 entry: &FileEntry,
1299 ) -> Result<(), FilesystemError> {
1300 if parent.path != "/" {
1301 return Err(FilesystemError::Unsupported(
1302 "MFS is flat; only files in the root are deletable".into(),
1303 ));
1304 }
1305 if entry.entry_type != EntryType::File {
1306 return Err(FilesystemError::InvalidData(
1307 "MFS only stores files; cannot delete a directory".into(),
1308 ));
1309 }
1310 let fnum = entry.location as u32;
1311 let (data_head, rsrc_head) = {
1313 let de = self
1314 .entries
1315 .iter()
1316 .find(|e| e.is_in_use() && e.file_number == fnum)
1317 .ok_or_else(|| {
1318 FilesystemError::NotFound(format!("file number {fnum} not in directory"))
1319 })?;
1320 (de.data_first_block, de.rsrc_first_block)
1321 };
1322 self.free_chain(data_head)?;
1323 self.free_chain(rsrc_head)?;
1324 self.entries
1326 .retain(|e| !(e.is_in_use() && e.file_number == fnum));
1327 self.mdb.num_files = self.mdb.num_files.saturating_sub(1);
1328 Ok(())
1329 }
1330
1331 fn sync_metadata(&mut self) -> Result<(), FilesystemError> {
1332 self.dir_write_back()?;
1333 self.map_write_back()?;
1334 self.mdb_write_back()?;
1335 self.reader.flush()?;
1336 Ok(())
1337 }
1338
1339 fn free_space(&mut self) -> Result<u64, FilesystemError> {
1340 Ok(self.mdb.free_blocks as u64 * self.mdb.alloc_block_size as u64)
1341 }
1342
1343 fn repair(&mut self) -> Result<RepairReport, FilesystemError> {
1344 self.run_repair()
1345 }
1346
1347 fn write_resource_fork(
1353 &mut self,
1354 entry: &FileEntry,
1355 data: &mut dyn std::io::Read,
1356 len: u64,
1357 ) -> Result<(), FilesystemError> {
1358 if len > u32::MAX as u64 {
1359 return Err(FilesystemError::InvalidData(
1360 "MFS resource fork capped at u32::MAX bytes".into(),
1361 ));
1362 }
1363 let fnum = entry.location as u32;
1364 let old_head = self
1365 .entries
1366 .iter()
1367 .find(|e| e.is_in_use() && e.file_number == fnum)
1368 .map(|e| e.rsrc_first_block)
1369 .ok_or_else(|| {
1370 FilesystemError::NotFound(format!("file number {fnum} not in directory"))
1371 })?;
1372 let mut bytes = Vec::with_capacity(len as usize);
1373 data.read_to_end(&mut bytes)?;
1374 self.free_chain(old_head)?;
1375 let (first, rlen) = self.alloc_and_write_fork(&bytes)?;
1376 let de = self
1377 .entries
1378 .iter_mut()
1379 .find(|e| e.is_in_use() && e.file_number == fnum)
1380 .expect("entry present after lookup above");
1381 de.rsrc_first_block = first;
1382 de.rsrc_logical_length = rlen;
1383 Ok(())
1384 }
1385
1386 fn set_type_creator(
1387 &mut self,
1388 entry: &FileEntry,
1389 type_code: &str,
1390 creator_code: &str,
1391 ) -> Result<(), FilesystemError> {
1392 if type_code.len() != 4 || creator_code.len() != 4 {
1393 return Err(FilesystemError::InvalidData(
1394 "Mac OSType / creator must be exactly 4 chars".into(),
1395 ));
1396 }
1397 let fnum = entry.location as u32;
1398 let de = self
1399 .entries
1400 .iter_mut()
1401 .find(|e| e.is_in_use() && e.file_number == fnum)
1402 .ok_or_else(|| {
1403 FilesystemError::NotFound(format!("file number {fnum} not in directory"))
1404 })?;
1405 de.finder_info[0..4].copy_from_slice(type_code.as_bytes());
1406 de.finder_info[4..8].copy_from_slice(creator_code.as_bytes());
1407 Ok(())
1408 }
1409
1410 fn rename(
1411 &mut self,
1412 parent: &FileEntry,
1413 entry: &FileEntry,
1414 new_name: &str,
1415 ) -> Result<(), FilesystemError> {
1416 if new_name == entry.name {
1417 return Ok(());
1418 }
1419 if parent.path != "/" {
1420 return Err(FilesystemError::Unsupported(
1421 "MFS is flat; only files in the root are renamable".into(),
1422 ));
1423 }
1424 if entry.entry_type != EntryType::File {
1425 return Err(FilesystemError::InvalidData(
1426 "MFS only stores files; cannot rename a directory".into(),
1427 ));
1428 }
1429 let _ = validate_mfs_name(new_name)?;
1430
1431 let fnum = entry.location as u32;
1432 if self
1435 .entries
1436 .iter()
1437 .any(|e| e.is_in_use() && e.file_number != fnum && mfs_names_equal(&e.name, new_name))
1438 {
1439 return Err(FilesystemError::AlreadyExists(new_name.to_string()));
1440 }
1441
1442 let de = self
1446 .entries
1447 .iter_mut()
1448 .find(|e| e.is_in_use() && e.file_number == fnum)
1449 .ok_or_else(|| {
1450 FilesystemError::NotFound(format!("file number {fnum} not in directory"))
1451 })?;
1452 de.name = new_name.to_string();
1453 Ok(())
1454 }
1455}
1456
1457struct MfsBlankGeom {
1463 total_sectors: u64,
1464 alloc_block_size: u32,
1465 num_alloc_blocks: u16,
1466 dir_start_sector: u16,
1467 dir_length_sectors: u16,
1468 first_alloc_block_sector: u16,
1469}
1470
1471fn blank_dir_sectors(num_alloc_blocks: u32) -> u64 {
1476 (num_alloc_blocks as u64 / 40).clamp(12, 255)
1477}
1478
1479fn mfs_blank_geometry(size_bytes: u64) -> Result<MfsBlankGeom, FilesystemError> {
1488 const SECTOR: u64 = 512;
1489 if size_bytes < 32 * SECTOR {
1490 return Err(FilesystemError::InvalidData(
1491 "MFS volume must be at least 16 KiB".into(),
1492 ));
1493 }
1494 let total_sectors = size_bytes / SECTOR;
1495
1496 for spb in 2..=128u64 {
1499 let abs = spb * SECTOR;
1500 let mut first_ab: u64 = 16; let mut num_ab = 0u64;
1502 let mut dir_start = 4u64;
1503 let mut dir_len = 12u64;
1504 let mut converged = false;
1505 for _ in 0..16 {
1506 if total_sectors <= first_ab {
1507 break;
1508 }
1509 num_ab = (total_sectors - first_ab) / spb;
1510 let map_bytes = (num_ab * 12).div_ceil(8);
1511 let mdb_map_sectors = (64 + map_bytes).div_ceil(SECTOR);
1514 dir_start = 2 + mdb_map_sectors;
1515 dir_len = blank_dir_sectors(num_ab as u32);
1516 let next_first = dir_start + dir_len;
1517 if next_first == first_ab {
1518 converged = true;
1519 break;
1520 }
1521 first_ab = next_first;
1522 }
1523 if !converged || num_ab == 0 {
1524 continue;
1525 }
1526 if num_ab > 4094 {
1527 continue; }
1529 return Ok(MfsBlankGeom {
1530 total_sectors,
1531 alloc_block_size: abs as u32,
1532 num_alloc_blocks: num_ab as u16,
1533 dir_start_sector: dir_start as u16,
1534 dir_length_sectors: dir_len as u16,
1535 first_alloc_block_sector: first_ab as u16,
1536 });
1537 }
1538 Err(FilesystemError::InvalidData(
1539 "MFS volume too large to format (max ~256 MB)".into(),
1540 ))
1541}
1542
1543pub fn create_blank_mfs(size_bytes: u64, name: &str) -> Result<Vec<u8>, FilesystemError> {
1551 let g = mfs_blank_geometry(size_bytes)?;
1552 let total_bytes = g.total_sectors * 512;
1553 let mut disk = vec![0u8; total_bytes as usize];
1554
1555 let data_bytes = g.num_alloc_blocks as u32 * g.alloc_block_size;
1559 let clump = (g.alloc_block_size.saturating_mul(8)).min(data_bytes.max(g.alloc_block_size));
1560
1561 let mdb = &mut disk[MDB_OFFSET as usize..MDB_OFFSET as usize + 512];
1562 BigEndian::write_u16(&mut mdb[0..2], MFS_SIGNATURE);
1563 BigEndian::write_u16(&mut mdb[10..12], 0); BigEndian::write_u16(&mut mdb[12..14], 0); BigEndian::write_u16(&mut mdb[14..16], g.dir_start_sector);
1567 BigEndian::write_u16(&mut mdb[16..18], g.dir_length_sectors);
1568 BigEndian::write_u16(&mut mdb[18..20], g.num_alloc_blocks);
1569 BigEndian::write_u32(&mut mdb[20..24], g.alloc_block_size);
1570 BigEndian::write_u32(&mut mdb[24..28], clump);
1571 BigEndian::write_u16(&mut mdb[28..30], g.first_alloc_block_sector);
1572 BigEndian::write_u32(&mut mdb[30..34], 1); BigEndian::write_u16(&mut mdb[34..36], g.num_alloc_blocks); let name_bytes = crate::hfs::utf8_to_mac_roman(name).unwrap_or_default();
1576 let name_len = name_bytes.len().min(MFS_MAX_NAME_LEN);
1577 mdb[36] = name_len as u8;
1578 mdb[37..37 + name_len].copy_from_slice(&name_bytes[..name_len]);
1579
1580 Ok(disk)
1584}
1585
1586#[cfg(test)]
1587mod tests {
1588 use super::*;
1589 use std::io::Cursor;
1590
1591 fn build_test_mfs() -> Vec<u8> {
1604 const SECTOR: usize = 512;
1605 const NUM_SECTORS: usize = 32; const ALLOC_BLOCK_SIZE: u32 = 1024;
1607 const FIRST_ALLOC_BLOCK_SECTOR: u16 = 5;
1608 const DIR_START: u16 = 3;
1609 const DIR_LEN: u16 = 2;
1610 const NUM_ALLOC_BLOCKS: u16 = 8;
1611
1612 let mut disk = vec![0u8; NUM_SECTORS * SECTOR];
1613
1614 let mdb = &mut disk[1024..1024 + 512];
1616 BigEndian::write_u16(&mut mdb[0..2], MFS_SIGNATURE);
1617 BigEndian::write_u32(&mut mdb[2..6], 0); BigEndian::write_u32(&mut mdb[6..10], 0); BigEndian::write_u16(&mut mdb[10..12], 0); BigEndian::write_u16(&mut mdb[12..14], 2); BigEndian::write_u16(&mut mdb[14..16], DIR_START);
1622 BigEndian::write_u16(&mut mdb[16..18], DIR_LEN);
1623 BigEndian::write_u16(&mut mdb[18..20], NUM_ALLOC_BLOCKS);
1624 BigEndian::write_u32(&mut mdb[20..24], ALLOC_BLOCK_SIZE);
1625 BigEndian::write_u32(&mut mdb[24..28], ALLOC_BLOCK_SIZE);
1626 BigEndian::write_u16(&mut mdb[28..30], FIRST_ALLOC_BLOCK_SECTOR);
1627 BigEndian::write_u32(&mut mdb[30..34], 3); BigEndian::write_u16(&mut mdb[34..36], NUM_ALLOC_BLOCKS - 3); let vname = b"Tester";
1630 mdb[36] = vname.len() as u8;
1631 mdb[37..37 + vname.len()].copy_from_slice(vname);
1632
1633 let map_start = 1024 + 64;
1642 let mut map = vec![0u8; 12];
1643 write_map_entry(&mut map, 2, 1);
1644 write_map_entry(&mut map, 3, 1);
1645 write_map_entry(&mut map, 4, 1);
1646 disk[map_start..map_start + map.len()].copy_from_slice(&map);
1647
1648 let mut dir_sec = vec![0u8; SECTOR];
1651 let off = encode_dir_entry(
1652 &mut dir_sec,
1653 0,
1654 DirEntryIn {
1655 flags: 0x80,
1656 file_number: 1,
1657 data_first_block: 2,
1658 data_logical_length: 9, data_physical_length: ALLOC_BLOCK_SIZE,
1660 rsrc_first_block: 0,
1661 rsrc_logical_length: 0,
1662 rsrc_physical_length: 0,
1663 type_code: *b"TEXT",
1664 creator_code: *b"ttxt",
1665 name: b"Hello",
1666 },
1667 );
1668 let off = encode_dir_entry(
1669 &mut dir_sec,
1670 off,
1671 DirEntryIn {
1672 flags: 0x80,
1673 file_number: 2,
1674 data_first_block: 3,
1675 data_logical_length: 11, data_physical_length: ALLOC_BLOCK_SIZE,
1677 rsrc_first_block: 4,
1678 rsrc_logical_length: 4, rsrc_physical_length: ALLOC_BLOCK_SIZE,
1680 type_code: *b"TEXT",
1681 creator_code: *b"ttxt",
1682 name: b"Doc",
1683 },
1684 );
1685 let _ = off;
1686 disk[3 * SECTOR..3 * SECTOR + SECTOR].copy_from_slice(&dir_sec);
1687
1688 let alloc_start = FIRST_ALLOC_BLOCK_SECTOR as usize * SECTOR;
1690 disk[alloc_start..alloc_start + 9].copy_from_slice(b"hello mfs");
1692 let b3 = alloc_start + ALLOC_BLOCK_SIZE as usize;
1694 disk[b3..b3 + 11].copy_from_slice(b"first line\n");
1695 let b4 = alloc_start + 2 * ALLOC_BLOCK_SIZE as usize;
1697 disk[b4..b4 + 4].copy_from_slice(b"RSRC");
1698
1699 disk
1700 }
1701
1702 struct DirEntryIn<'a> {
1703 flags: u8,
1704 file_number: u32,
1705 data_first_block: u16,
1706 data_logical_length: u32,
1707 data_physical_length: u32,
1708 rsrc_first_block: u16,
1709 rsrc_logical_length: u32,
1710 rsrc_physical_length: u32,
1711 type_code: [u8; 4],
1712 creator_code: [u8; 4],
1713 name: &'a [u8],
1714 }
1715
1716 fn encode_dir_entry(buf: &mut [u8], off: usize, e: DirEntryIn) -> usize {
1717 buf[off] = e.flags;
1718 buf[off + 1] = 0; buf[off + 2..off + 6].copy_from_slice(&e.type_code);
1720 buf[off + 6..off + 10].copy_from_slice(&e.creator_code);
1721 BigEndian::write_u32(&mut buf[off + 18..off + 22], e.file_number);
1723 BigEndian::write_u16(&mut buf[off + 22..off + 24], e.data_first_block);
1724 BigEndian::write_u32(&mut buf[off + 24..off + 28], e.data_logical_length);
1725 BigEndian::write_u32(&mut buf[off + 28..off + 32], e.data_physical_length);
1726 BigEndian::write_u16(&mut buf[off + 32..off + 34], e.rsrc_first_block);
1727 BigEndian::write_u32(&mut buf[off + 34..off + 38], e.rsrc_logical_length);
1728 BigEndian::write_u32(&mut buf[off + 38..off + 42], e.rsrc_physical_length);
1729 buf[off + 50] = e.name.len() as u8;
1731 buf[off + 51..off + 51 + e.name.len()].copy_from_slice(e.name);
1732 let entry_len = 51 + e.name.len();
1733 off + entry_len + (entry_len & 1)
1734 }
1735
1736 fn write_map_entry(map: &mut [u8], block: u16, value: u16) {
1739 let value = value & 0x0FFF;
1740 let bit_off = (block as usize - 2) * 12;
1741 let byte_off = bit_off / 8;
1742 if block.is_multiple_of(2) {
1743 map[byte_off] = (value >> 4) as u8;
1744 map[byte_off + 1] = (map[byte_off + 1] & 0x0F) | (((value & 0x0F) as u8) << 4);
1745 } else {
1746 map[byte_off] = (map[byte_off] & 0xF0) | ((value >> 8) as u8 & 0x0F);
1747 map[byte_off + 1] = (value & 0xFF) as u8;
1748 }
1749 }
1750
1751 #[test]
1752 fn parses_mdb_from_synthetic_volume() {
1753 let disk = build_test_mfs();
1754 let cur = Cursor::new(disk);
1755 let fs = MfsFilesystem::open(cur, 0).unwrap();
1756 assert_eq!(fs.mdb.num_files, 2);
1757 assert_eq!(fs.mdb.alloc_block_size, 1024);
1758 assert_eq!(fs.mdb.num_alloc_blocks, 8);
1759 assert_eq!(fs.mdb.first_alloc_block_sector, 5);
1760 assert_eq!(fs.volume_label(), Some("Tester"));
1761 assert_eq!(fs.fs_type(), "MFS");
1762 }
1763
1764 #[test]
1765 fn lists_root_directory() {
1766 let disk = build_test_mfs();
1767 let cur = Cursor::new(disk);
1768 let mut fs = MfsFilesystem::open(cur, 0).unwrap();
1769 let root = fs.root().unwrap();
1770 let entries = fs.list_directory(&root).unwrap();
1771 assert_eq!(entries.len(), 2);
1772 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
1773 assert!(names.contains(&"Doc"));
1774 assert!(names.contains(&"Hello"));
1775 let doc = entries.iter().find(|e| e.name == "Doc").unwrap();
1776 assert_eq!(doc.resource_fork_size, Some(4));
1777 let hello = entries.iter().find(|e| e.name == "Hello").unwrap();
1778 assert_eq!(hello.resource_fork_size, None);
1779 assert_eq!(hello.type_code, Some(*b"TEXT"));
1780 assert_eq!(hello.creator_code, Some(*b"ttxt"));
1781 }
1782
1783 #[test]
1784 fn reads_data_fork() {
1785 let disk = build_test_mfs();
1786 let cur = Cursor::new(disk);
1787 let mut fs = MfsFilesystem::open(cur, 0).unwrap();
1788 let root = fs.root().unwrap();
1789 let entries = fs.list_directory(&root).unwrap();
1790 let hello = entries.iter().find(|e| e.name == "Hello").unwrap();
1791 let data = fs.read_file(hello, 1024).unwrap();
1792 assert_eq!(&data, b"hello mfs");
1793 let doc = entries.iter().find(|e| e.name == "Doc").unwrap();
1794 let data = fs.read_file(doc, 1024).unwrap();
1795 assert_eq!(&data, b"first line\n");
1796 }
1797
1798 #[test]
1799 fn reads_resource_fork() {
1800 let disk = build_test_mfs();
1801 let cur = Cursor::new(disk);
1802 let mut fs = MfsFilesystem::open(cur, 0).unwrap();
1803 let root = fs.root().unwrap();
1804 let entries = fs.list_directory(&root).unwrap();
1805 let doc = entries.iter().find(|e| e.name == "Doc").unwrap();
1806 let rsrc = fs.read_resource_fork(doc).unwrap();
1807 assert_eq!(&rsrc, b"RSRC");
1808 let hello = entries.iter().find(|e| e.name == "Hello").unwrap();
1809 let empty = fs.read_resource_fork(hello).unwrap();
1810 assert!(empty.is_empty());
1811 }
1812
1813 #[test]
1814 fn resource_fork_via_trait_object() {
1815 let disk = build_test_mfs();
1822 let mut fs = MfsFilesystem::open(Cursor::new(disk), 0).unwrap();
1823 let root = fs.root().unwrap();
1824 let entries = fs.list_directory(&root).unwrap();
1825 let doc = entries.iter().find(|e| e.name == "Doc").unwrap().clone();
1826 let hello = entries.iter().find(|e| e.name == "Hello").unwrap().clone();
1827
1828 let fsys: &mut dyn Filesystem = &mut fs;
1829 assert_eq!(fsys.resource_fork_size(&doc), 4);
1830 let mut buf = Vec::new();
1831 assert_eq!(fsys.write_resource_fork_to(&doc, &mut buf).unwrap(), 4);
1832 assert_eq!(&buf, b"RSRC");
1833
1834 assert_eq!(fsys.resource_fork_size(&hello), 0);
1836 let mut empty = Vec::new();
1837 assert_eq!(fsys.write_resource_fork_to(&hello, &mut empty).unwrap(), 0);
1838 assert!(empty.is_empty());
1839 }
1840
1841 #[test]
1842 fn resource_fork_survives_real_export_consumer() {
1843 use crate::fork_export::export_file_with_fork;
1849 use crate::resource_fork::ResourceForkMode;
1850
1851 let dir = std::env::temp_dir().join(format!("rb_mfs_fork_{}", std::process::id()));
1852 let _ = std::fs::remove_dir_all(&dir);
1853 std::fs::create_dir_all(&dir).unwrap();
1854
1855 let mut fs = MfsFilesystem::open(Cursor::new(build_test_mfs()), 0).unwrap();
1856 let root = fs.root().unwrap();
1857 let doc = fs
1858 .list_directory(&root)
1859 .unwrap()
1860 .into_iter()
1861 .find(|e| e.name == "Doc")
1862 .unwrap();
1863
1864 export_file_with_fork(&mut fs, &doc, &dir, "Doc", ResourceForkMode::SeparateRsrc).unwrap();
1865
1866 assert_eq!(std::fs::read(dir.join("Doc")).unwrap(), b"first line\n");
1869 assert_eq!(std::fs::read(dir.join("Doc.rsrc")).unwrap(), b"RSRC");
1870
1871 let _ = std::fs::remove_dir_all(&dir);
1872 }
1873
1874 #[test]
1875 fn create_file_with_resource_fork_persists() {
1876 use disk_core::filesystem::ResourceForkSource;
1880 let mut cur = Cursor::new(build_test_mfs());
1881 {
1882 let mut fs = MfsFilesystem::open(&mut cur, 0).unwrap();
1883 let root = fs.root().unwrap();
1884 let opts = CreateFileOptions {
1885 resource_fork: Some(ResourceForkSource::Data(b"NEW-RSRC-BYTES".to_vec())),
1886 ..Default::default()
1887 };
1888 let data = b"data fork content";
1889 let fe = fs
1890 .create_file(&root, "Forked", &mut &data[..], data.len() as u64, &opts)
1891 .unwrap();
1892 assert_eq!(fe.resource_fork_size, Some(14));
1893 fs.sync_metadata().unwrap();
1894 }
1895 cur.set_position(0);
1896 let mut fs = MfsFilesystem::open(&mut cur, 0).unwrap();
1897 let root = fs.root().unwrap();
1898 let forked = fs
1899 .list_directory(&root)
1900 .unwrap()
1901 .into_iter()
1902 .find(|e| e.name == "Forked")
1903 .unwrap();
1904 assert_eq!(forked.resource_fork_size, Some(14));
1905 assert_eq!(
1906 fs.read_file(&forked, usize::MAX).unwrap(),
1907 b"data fork content"
1908 );
1909 let fsys: &mut dyn Filesystem = &mut fs;
1911 let mut buf = Vec::new();
1912 fsys.write_resource_fork_to(&forked, &mut buf).unwrap();
1913 assert_eq!(&buf, b"NEW-RSRC-BYTES");
1914 }
1915
1916 #[test]
1917 fn write_resource_fork_sets_and_replaces() {
1918 let mut cur = Cursor::new(build_test_mfs());
1921 {
1922 let mut fs = MfsFilesystem::open(&mut cur, 0).unwrap();
1923 let root = fs.root().unwrap();
1924 let fe = fs
1925 .create_file(
1926 &root,
1927 "SetR",
1928 &mut &b"body"[..],
1929 4,
1930 &CreateFileOptions::default(),
1931 )
1932 .unwrap();
1933 fs.write_resource_fork(&fe, &mut &b"FIRST"[..], 5).unwrap();
1934 fs.sync_metadata().unwrap();
1935 }
1936 cur.set_position(0);
1937 {
1938 let mut fs = MfsFilesystem::open(&mut cur, 0).unwrap();
1939 let root = fs.root().unwrap();
1940 let e = fs
1941 .list_directory(&root)
1942 .unwrap()
1943 .into_iter()
1944 .find(|e| e.name == "SetR")
1945 .unwrap();
1946 assert_eq!(fs.read_resource_fork(&e).unwrap(), b"FIRST");
1947 fs.write_resource_fork(&e, &mut &b"SECOND-and-longer"[..], 17)
1948 .unwrap();
1949 fs.sync_metadata().unwrap();
1950 }
1951 cur.set_position(0);
1952 let mut fs = MfsFilesystem::open(&mut cur, 0).unwrap();
1953 let root = fs.root().unwrap();
1954 let e = fs
1955 .list_directory(&root)
1956 .unwrap()
1957 .into_iter()
1958 .find(|e| e.name == "SetR")
1959 .unwrap();
1960 assert_eq!(fs.read_resource_fork(&e).unwrap(), b"SECOND-and-longer");
1961 }
1962
1963 #[test]
1964 fn rejects_non_mfs_signature() {
1965 let mut disk = build_test_mfs();
1966 disk[1024] = 0xFF;
1967 disk[1025] = 0xFF;
1968 let cur = Cursor::new(disk);
1969 match MfsFilesystem::open(cur, 0) {
1970 Ok(_) => panic!("expected open to fail on bad signature"),
1971 Err(FilesystemError::InvalidData(msg)) => {
1972 assert!(msg.contains("bad MFS signature"), "got: {msg}");
1973 }
1974 Err(other) => panic!("expected InvalidData, got {other:?}"),
1975 }
1976 }
1977
1978 #[test]
1983 fn create_blank_400k_round_trips_and_fscks_clean() {
1984 let img = create_blank_mfs(MFS_400K_BYTES, "Blank400").unwrap();
1985 assert_eq!(img.len() as u64, MFS_400K_BYTES);
1986 let mut fs = MfsFilesystem::open(Cursor::new(img), 0).unwrap();
1987 assert_eq!(fs.fs_type(), "MFS");
1988 assert_eq!(fs.volume_label(), Some("Blank400"));
1989 assert_eq!(fs.mdb.num_files, 0);
1990 assert_eq!(fs.mdb.free_blocks, fs.mdb.num_alloc_blocks);
1991 let root = fs.root().unwrap();
1992 assert!(fs.list_directory(&root).unwrap().is_empty());
1993 let res = fs.fsck().unwrap().unwrap();
1994 assert!(
1995 res.is_clean(),
1996 "blank 400K should fsck clean: {:?}",
1997 res.errors
1998 );
1999 }
2000
2001 #[test]
2002 fn create_blank_geometry_valid_across_sizes() {
2003 for size in [
2004 MFS_400K_BYTES,
2005 MFS_800K_BYTES,
2006 1_474_560, 4 * 1024 * 1024,
2008 20 * 1024 * 1024,
2009 ] {
2010 let img = create_blank_mfs(size, "Vol").unwrap();
2011 let mut fs = MfsFilesystem::open(Cursor::new(img), 0).unwrap();
2012 let (first_ab, nab, abs) = (
2014 fs.mdb.first_alloc_block_sector as u64,
2015 fs.mdb.num_alloc_blocks as u64,
2016 fs.mdb.alloc_block_size as u64,
2017 );
2018 let last_sector = first_ab + nab * (abs / 512);
2019 assert!(
2020 last_sector <= size / 512,
2021 "size {size}: data region ends at sector {last_sector}, past {}",
2022 size / 512
2023 );
2024 assert!(nab <= 4094, "size {size}: too many blocks");
2025 let res = fs.fsck().unwrap().unwrap();
2026 assert!(
2027 res.is_clean(),
2028 "size {size} should fsck clean: {:?}",
2029 res.errors
2030 );
2031 }
2032 }
2033
2034 #[test]
2035 fn create_blank_too_small_is_rejected() {
2036 assert!(create_blank_mfs(8 * 1024, "X").is_err());
2037 }
2038
2039 #[test]
2040 fn blank_volume_accepts_files_and_stays_clean() {
2041 let img = create_blank_mfs(MFS_400K_BYTES, "Work").unwrap();
2042 let mut cur = Cursor::new(img);
2043 {
2044 let mut fs = MfsFilesystem::open(&mut cur, 0).unwrap();
2045 let root = fs.root().unwrap();
2046 fs.create_file(
2047 &root,
2048 "ReadMe",
2049 &mut &b"hello from a blank MFS volume"[..],
2050 29,
2051 &CreateFileOptions::default(),
2052 )
2053 .unwrap();
2054 fs.sync_metadata().unwrap();
2055 }
2056 cur.set_position(0);
2057 let mut fs = MfsFilesystem::open(&mut cur, 0).unwrap();
2058 let root = fs.root().unwrap();
2059 let entries = fs.list_directory(&root).unwrap();
2060 assert_eq!(entries.len(), 1);
2061 assert_eq!(entries[0].name, "ReadMe");
2062 assert_eq!(
2063 fs.read_file(&entries[0], usize::MAX).unwrap(),
2064 b"hello from a blank MFS volume"
2065 );
2066 let res = fs.fsck().unwrap().unwrap();
2067 assert!(
2068 res.is_clean(),
2069 "populated volume should fsck clean: {:?}",
2070 res.errors
2071 );
2072 }
2073
2074 #[test]
2075 fn fsck_clean_on_synthetic_volume() {
2076 let mut fs = MfsFilesystem::open(Cursor::new(build_test_mfs()), 0).unwrap();
2077 let res = fs.fsck().unwrap().unwrap();
2078 assert!(res.is_clean(), "expected clean, got {:?}", res.errors);
2079 assert_eq!(res.stats.files_checked, 2);
2080 }
2081
2082 #[test]
2083 fn fsck_detects_and_repairs_leaked_block() {
2084 let mut cur = Cursor::new(build_test_mfs());
2085 {
2086 let mut fs = MfsFilesystem::open(&mut cur, 0).unwrap();
2087 fs.map_set(6, 1);
2090 let res = fs.fsck().unwrap().unwrap();
2091 assert!(!res.is_clean());
2092 assert!(res
2093 .errors
2094 .iter()
2095 .any(|e| e.code == "MfsLostBlocks" && e.repairable));
2096 assert!(res.repairable);
2097 let report = fs.repair().unwrap();
2098 assert_eq!(report.unrepairable_count, 0);
2099 assert!(!report.fixes_applied.is_empty());
2100 fs.reader.flush().unwrap();
2101 }
2102 cur.set_position(0);
2104 let mut fs = MfsFilesystem::open(&mut cur, 0).unwrap();
2105 assert_eq!(fs.map_get(6), 0, "block 6 reclaimed");
2106 let res = fs.fsck().unwrap().unwrap();
2107 assert!(
2108 res.is_clean(),
2109 "post-repair should be clean: {:?}",
2110 res.errors
2111 );
2112 }
2113
2114 #[test]
2115 fn fsck_detects_and_repairs_file_count() {
2116 let mut cur = Cursor::new(build_test_mfs());
2117 {
2118 let mut fs = MfsFilesystem::open(&mut cur, 0).unwrap();
2119 fs.mdb.num_files = 9; let res = fs.fsck().unwrap().unwrap();
2121 assert!(res
2122 .errors
2123 .iter()
2124 .any(|e| e.code == "MfsFileCountMismatch" && e.repairable));
2125 fs.repair().unwrap();
2126 fs.reader.flush().unwrap();
2127 }
2128 cur.set_position(0);
2129 let mut fs = MfsFilesystem::open(&mut cur, 0).unwrap();
2130 assert_eq!(fs.mdb.num_files, 2);
2131 assert!(fs.fsck().unwrap().unwrap().is_clean());
2132 }
2133
2134 #[test]
2135 fn fsck_withholds_repair_on_broken_chain() {
2136 let mut fs = MfsFilesystem::open(Cursor::new(build_test_mfs()), 0).unwrap();
2137 fs.entries[0].data_first_block = 50; let res = fs.fsck().unwrap().unwrap();
2140 assert!(res.errors.iter().any(|e| e.code == "MfsBrokenChain"));
2141 assert!(!res.repairable, "a broken chain must not be repairable");
2142 let report = fs.repair().unwrap();
2143 assert!(report.fixes_applied.is_empty());
2144 assert!(report.unrepairable_count >= 1);
2145 }
2146
2147 #[test]
2148 fn map_get_round_trip() {
2149 let disk = build_test_mfs();
2151 let cur = Cursor::new(disk);
2152 let fs = MfsFilesystem::open(cur, 0).unwrap();
2153 assert_eq!(fs.map_get(2), 1, "block 2 marked end-of-chain");
2154 assert_eq!(fs.map_get(3), 1);
2155 assert_eq!(fs.map_get(4), 1);
2156 assert_eq!(fs.map_get(5), 0, "block 5 marked free");
2157 assert_eq!(fs.map_get(0), 0, "reserved block 0");
2158 assert_eq!(fs.map_get(1), 0, "reserved block 1");
2159 }
2160
2161 #[test]
2162 fn total_and_used_size_reflect_mdb_counts() {
2163 let disk = build_test_mfs();
2164 let cur = Cursor::new(disk);
2165 let fs = MfsFilesystem::open(cur, 0).unwrap();
2166 assert_eq!(fs.total_size(), 8 * 1024);
2167 assert_eq!(fs.used_size(), 3 * 1024);
2169 }
2170
2171 fn edit_fixture() -> Cursor<Vec<u8>> {
2180 Cursor::new(build_test_mfs())
2181 }
2182
2183 #[test]
2184 fn create_file_writes_data_and_round_trips_through_sync() {
2185 let mut fs = MfsFilesystem::open(edit_fixture(), 0).unwrap();
2186 let root = fs.root().unwrap();
2187 let before_free = EditableFilesystem::free_space(&mut fs).unwrap();
2188 let payload = b"freshly-created via EditableFilesystem".to_vec();
2189 let mut src = Cursor::new(payload.clone());
2190 let opts = CreateFileOptions::default();
2191 let fe = fs
2192 .create_file(&root, "NewFile", &mut src, payload.len() as u64, &opts)
2193 .unwrap();
2194 assert_eq!(fe.name, "NewFile");
2195 assert_eq!(fe.size, payload.len() as u64);
2196
2197 fs.sync_metadata().unwrap();
2198 let inner = fs.reader.clone();
2200 let mut fs2 = MfsFilesystem::open(inner, 0).unwrap();
2201 let root2 = fs2.root().unwrap();
2202 let entries = fs2.list_directory(&root2).unwrap();
2203 let new = entries.iter().find(|e| e.name == "NewFile").unwrap();
2204 let read_back = fs2.read_file(new, 4096).unwrap();
2205 assert_eq!(read_back, payload);
2206
2207 let after_free = EditableFilesystem::free_space(&mut fs2).unwrap();
2209 assert_eq!(after_free, before_free - 1024);
2210 }
2211
2212 #[test]
2213 fn delete_entry_releases_blocks_and_clears_directory() {
2214 let mut fs = MfsFilesystem::open(edit_fixture(), 0).unwrap();
2215 let root = fs.root().unwrap();
2216 let before_free = EditableFilesystem::free_space(&mut fs).unwrap();
2217 let entries = fs.list_directory(&root).unwrap();
2218 let doc = entries.iter().find(|e| e.name == "Doc").unwrap().clone();
2219
2220 fs.delete_entry(&root, &doc).unwrap();
2221 fs.sync_metadata().unwrap();
2222
2223 let inner = fs.reader.clone();
2224 let mut fs2 = MfsFilesystem::open(inner, 0).unwrap();
2225 let root2 = fs2.root().unwrap();
2226 let entries = fs2.list_directory(&root2).unwrap();
2227 assert!(
2228 entries.iter().all(|e| e.name != "Doc"),
2229 "Doc should be gone after delete"
2230 );
2231
2232 let after_free = EditableFilesystem::free_space(&mut fs2).unwrap();
2234 assert_eq!(after_free, before_free + 2 * 1024);
2235 }
2236
2237 #[test]
2238 fn rename_in_place_round_trips_through_sync() {
2239 let mut fs = MfsFilesystem::open(edit_fixture(), 0).unwrap();
2240 let root = fs.root().unwrap();
2241 let entries = fs.list_directory(&root).unwrap();
2242 let doc = entries.iter().find(|e| e.name == "Doc").unwrap().clone();
2243 let fnum = doc.location;
2244
2245 fs.rename(&root, &doc, "Report").unwrap();
2246 fs.sync_metadata().unwrap();
2247
2248 let inner = fs.reader.clone();
2250 let mut fs2 = MfsFilesystem::open(inner, 0).unwrap();
2251 let root2 = fs2.root().unwrap();
2252 let entries2 = fs2.list_directory(&root2).unwrap();
2253 let names: Vec<&str> = entries2.iter().map(|e| e.name.as_str()).collect();
2254 assert!(names.contains(&"Report"));
2255 assert!(!names.contains(&"Doc"));
2256
2257 let renamed = entries2.iter().find(|e| e.name == "Report").unwrap();
2258 assert_eq!(renamed.location, fnum);
2260 assert_eq!(renamed.resource_fork_size, Some(4));
2261 assert_eq!(fs2.read_file(renamed, 1024).unwrap(), b"first line\n");
2262 assert_eq!(fs2.read_resource_fork(renamed).unwrap(), b"RSRC");
2263 }
2264
2265 #[test]
2268 fn names_differing_only_in_case_are_the_same_file() {
2269 let mut fs = MfsFilesystem::open(edit_fixture(), 0).unwrap();
2270 let root = fs.root().unwrap();
2271 let err = fs
2272 .create_file(
2273 &root,
2274 "hello",
2275 &mut &b"x"[..],
2276 1,
2277 &CreateFileOptions::default(),
2278 )
2279 .unwrap_err();
2280 assert!(format!("{err}").contains("already exists"), "{err}");
2281 let entries = fs.list_directory(&root).unwrap();
2282 let doc = entries.iter().find(|e| e.name == "Doc").unwrap().clone();
2283 let err = fs.rename(&root, &doc, "HELLO").unwrap_err();
2284 assert!(matches!(err, FilesystemError::AlreadyExists(_)));
2285 fs.rename(&root, &doc, "DOC").unwrap();
2287 }
2288
2289 #[test]
2290 fn rename_rejects_collision() {
2291 let mut fs = MfsFilesystem::open(edit_fixture(), 0).unwrap();
2292 let root = fs.root().unwrap();
2293 let entries = fs.list_directory(&root).unwrap();
2294 let doc = entries.iter().find(|e| e.name == "Doc").unwrap().clone();
2295 let err = fs.rename(&root, &doc, "Hello").unwrap_err();
2297 assert!(matches!(err, FilesystemError::AlreadyExists(_)));
2298 }
2299
2300 #[test]
2301 fn create_file_rejects_duplicate_names() {
2302 let mut fs = MfsFilesystem::open(edit_fixture(), 0).unwrap();
2303 let root = fs.root().unwrap();
2304 let mut src = Cursor::new(b"x".to_vec());
2305 let err = fs
2306 .create_file(&root, "Hello", &mut src, 1, &CreateFileOptions::default())
2307 .unwrap_err();
2308 assert!(matches!(err, FilesystemError::InvalidData(_)));
2309 }
2310
2311 #[test]
2312 fn create_file_rejects_oversize_data() {
2313 let mut fs = MfsFilesystem::open(edit_fixture(), 0).unwrap();
2314 let root = fs.root().unwrap();
2315 let huge = vec![0u8; 8 * 1024];
2318 let mut src = Cursor::new(huge);
2319 let err = fs
2320 .create_file(
2321 &root,
2322 "TooBig",
2323 &mut src,
2324 8 * 1024,
2325 &CreateFileOptions::default(),
2326 )
2327 .unwrap_err();
2328 assert!(matches!(err, FilesystemError::InvalidData(_)));
2329 }
2330
2331 #[test]
2332 fn create_then_delete_returns_to_original_free_count() {
2333 let mut fs = MfsFilesystem::open(edit_fixture(), 0).unwrap();
2334 let root = fs.root().unwrap();
2335 let before = EditableFilesystem::free_space(&mut fs).unwrap();
2336 let mut src = Cursor::new(b"abc".to_vec());
2337 let fe = fs
2338 .create_file(&root, "Tmp", &mut src, 3, &CreateFileOptions::default())
2339 .unwrap();
2340 let mid = EditableFilesystem::free_space(&mut fs).unwrap();
2341 assert_eq!(mid, before - 1024);
2342 fs.delete_entry(&root, &fe).unwrap();
2344 let after = EditableFilesystem::free_space(&mut fs).unwrap();
2345 assert_eq!(after, before, "free count must return after create+delete");
2346 }
2347
2348 #[test]
2349 fn create_directory_is_unsupported() {
2350 let mut fs = MfsFilesystem::open(edit_fixture(), 0).unwrap();
2351 let root = fs.root().unwrap();
2352 let err = fs
2353 .create_directory(&root, "subdir", &CreateDirectoryOptions::default())
2354 .unwrap_err();
2355 assert!(matches!(err, FilesystemError::Unsupported(_)));
2356 }
2357
2358 #[test]
2359 fn set_type_creator_updates_finder_info_and_persists() {
2360 let mut fs = MfsFilesystem::open(edit_fixture(), 0).unwrap();
2361 let root = fs.root().unwrap();
2362 let entries = fs.list_directory(&root).unwrap();
2363 let hello = entries.iter().find(|e| e.name == "Hello").unwrap().clone();
2364 fs.set_type_creator(&hello, "PICT", "8BIM").unwrap();
2365 fs.sync_metadata().unwrap();
2366
2367 let inner = fs.reader.clone();
2368 let mut fs2 = MfsFilesystem::open(inner, 0).unwrap();
2369 let root2 = fs2.root().unwrap();
2370 let entries = fs2.list_directory(&root2).unwrap();
2371 let hello2 = entries.iter().find(|e| e.name == "Hello").unwrap();
2372 assert_eq!(hello2.type_code, Some(*b"PICT"));
2373 assert_eq!(hello2.creator_code, Some(*b"8BIM"));
2374 }
2375}
2376
2377#[cfg(test)]
2378mod physical_length_tests {
2379 use super::*;
2380 use disk_core::filesystem::{CreateFileOptions, Filesystem};
2381 use std::io::Cursor;
2382
2383 #[test]
2386 fn physical_lengths_are_whole_allocation_blocks() {
2387 let img = create_blank_mfs(400 * 1024, "T").unwrap();
2388 let mut fs = MfsFilesystem::open(Cursor::new(img), 0).unwrap();
2389 let root = fs.root().unwrap();
2390 let mut data = Cursor::new(vec![0x5Au8; 100]);
2391 fs.create_file(&root, "tiny", &mut data, 100, &CreateFileOptions::default())
2392 .unwrap();
2393 fs.sync_metadata().unwrap();
2394 let abs = fs.mdb.alloc_block_size;
2395 let off = fs.partition_offset + fs.mdb.dir_start_sector as u64 * 512;
2396 fs.reader.seek(SeekFrom::Start(off)).unwrap();
2397 let mut sector = [0u8; 512];
2398 fs.reader.read_exact(&mut sector).unwrap();
2399 assert_ne!(sector[0] & 0x80, 0, "first directory entry is in use");
2400 assert_eq!(
2401 BigEndian::read_u32(§or[24..28]),
2402 100,
2403 "logical data length"
2404 );
2405 assert_eq!(
2406 BigEndian::read_u32(§or[28..32]),
2407 abs,
2408 "physical data length"
2409 );
2410 assert_eq!(BigEndian::read_u32(§or[38..42]), 0, "no resource fork");
2411 }
2412}