1use std::collections::HashMap;
30use std::io::{self, Read, Seek, SeekFrom};
31use std::path::Path;
32
33use crate::block::BlockDevice;
34use crate::fs::{DirEntry, EntryKind, Filesystem, MutationCapability};
35use crate::{Error, Result};
36
37mod editor;
38mod size_plan;
39mod writer;
40pub use size_plan::AffsSizePlan;
41pub use writer::AffsFormatOpts;
42
43enum Write {
45 None,
47 Format(writer::AffsWriter),
50 InPlace(editor::AffsEditor),
53}
54
55const BSIZE: usize = 512;
57const HT_SIZE: usize = 72;
59const MAX_DATABLK: usize = 72;
61const MAX_NAME_LEN: usize = 30;
63
64const T_HEADER: i32 = 2;
66const T_LIST: i32 = 16;
67const T_DATA: i32 = 8;
69
70const ST_ROOT: i32 = 1;
72const ST_USERDIR: i32 = 2;
73const ST_SOFTLINK: i32 = 3;
74const ST_LINKDIR: i32 = 4;
75const ST_FILE: i32 = -3;
76const ST_LINKFILE: i32 = -4;
77
78const OFF_TYPE: usize = 0x000;
80const OFF_HIGH_SEQ: usize = 0x008;
81const OFF_HASHTABLE: usize = 0x018;
82const OFF_BYTE_SIZE: usize = 0x144; const OFF_DAYS: usize = 0x1a4; const OFF_NAME_LEN: usize = 0x1b0;
85const OFF_NEXT_SAME_HASH: usize = 0x1f0;
86const OFF_EXTENSION: usize = 0x1f8; const OFF_SEC_TYPE: usize = 0x1fc;
88
89const AMIGA_EPOCH: u64 = 2922 * 86_400;
92
93#[inline]
94fn be_u32(b: &[u8], off: usize) -> u32 {
95 u32::from_be_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
96}
97
98#[inline]
99fn be_i32(b: &[u8], off: usize) -> i32 {
100 be_u32(b, off) as i32
101}
102
103fn block_checksum_ok(b: &[u8]) -> bool {
106 let mut sum = 0u32;
107 let mut i = 0;
108 while i < BSIZE {
109 sum = sum.wrapping_add(be_u32(b, i));
110 i += 4;
111 }
112 sum == 0
113}
114
115fn amiga_date_to_unix(days: i32, mins: i32, ticks: i32) -> u32 {
118 if days < 0 || mins < 0 || ticks < 0 {
119 return 0;
120 }
121 let secs = AMIGA_EPOCH + days as u64 * 86_400 + mins as u64 * 60 + ticks as u64 / 50;
122 secs.try_into().unwrap_or(u32::MAX)
123}
124
125fn read_name(block: &[u8]) -> String {
128 let len = (block[OFF_NAME_LEN] as usize).min(MAX_NAME_LEN);
129 let start = OFF_NAME_LEN + 1;
130 block[start..start + len]
131 .iter()
132 .map(|&b| b as char)
133 .collect()
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138pub struct Variant {
139 pub ffs: bool,
142 pub intl: bool,
144 pub dircache: bool,
146}
147
148impl Variant {
149 fn from_flag(flag: u8) -> Self {
150 Self {
151 ffs: flag & 1 != 0,
152 intl: flag & 2 != 0,
153 dircache: flag & 4 != 0,
154 }
155 }
156
157 pub fn dos_label(&self) -> String {
159 let n = (self.ffs as u8) | (self.intl as u8) << 1 | (self.dircache as u8) << 2;
160 format!("DOS\\{n}")
161 }
162}
163
164#[derive(Clone, Debug)]
166struct Node {
167 name: String,
168 block: u32,
170 kind: EntryKind,
171 size: u64,
173 link_target: Option<String>,
175 mtime: u32,
176}
177
178pub struct Affs {
180 block_size: u32,
181 root_block: u32,
182 variant: Variant,
183 pub volume_name: String,
185 children: HashMap<u32, Vec<Node>>,
188 mode: Write,
190}
191
192impl Affs {
193 pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
195 let total = dev.total_size();
196 let block_size = BSIZE as u32;
197 if total < (BSIZE as u64) * 4 {
198 return Err(Error::InvalidImage("affs: image too small".into()));
199 }
200
201 let mut boot = [0u8; 12];
203 dev.read_at(0, &mut boot)?;
204 if &boot[0..3] != b"DOS" || boot[3] > 7 {
205 return Err(Error::InvalidImage(
206 "affs: missing DOS boot signature".into(),
207 ));
208 }
209 let variant = Variant::from_flag(boot[3]);
210
211 let total_blocks = (total / BSIZE as u64) as u32;
212 let boot_root = be_u32(&boot, 8);
215 let candidates = [boot_root, total_blocks / 2, (total_blocks - 1) / 2];
216 let mut root_block = 0u32;
217 let mut root = vec![0u8; BSIZE];
218 for &cand in &candidates {
219 if cand == 0 || cand as u64 >= total_blocks as u64 {
220 continue;
221 }
222 dev.read_at(cand as u64 * BSIZE as u64, &mut root)?;
223 if be_i32(&root, OFF_TYPE) == T_HEADER
224 && be_i32(&root, OFF_SEC_TYPE) == ST_ROOT
225 && block_checksum_ok(&root)
226 {
227 root_block = cand;
228 break;
229 }
230 }
231 if root_block == 0 {
232 return Err(Error::InvalidImage(
233 "affs: no valid root block found".into(),
234 ));
235 }
236
237 let volume_name = read_name(&root);
238
239 let mut affs = Self {
240 block_size,
241 root_block,
242 variant,
243 volume_name,
244 children: HashMap::new(),
245 mode: Write::None,
246 };
247 affs.build_index(dev, total_blocks)?;
248 Ok(affs)
249 }
250
251 pub fn format(dev: &mut dyn BlockDevice, opts: &AffsFormatOpts) -> Result<Self> {
254 let w = writer::AffsWriter::format(dev, opts)?;
255 Ok(Self {
256 block_size: BSIZE as u32,
257 root_block: 0,
258 variant: w.variant(),
259 volume_name: w.volume_name().to_string(),
260 children: HashMap::new(),
261 mode: Write::Format(w),
262 })
263 }
264
265 pub fn open_writable(dev: &mut dyn BlockDevice) -> Result<Self> {
270 let mut affs = Self::open(dev)?;
271 let total_blocks = (dev.total_size() / BSIZE as u64) as u32;
272 let ed = editor::AffsEditor::open(dev, total_blocks, affs.variant, affs.root_block)?;
273 affs.mode = Write::InPlace(ed);
274 Ok(affs)
275 }
276
277 fn refresh_index(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
280 let total_blocks = (dev.total_size() / BSIZE as u64) as u32;
281 self.build_index(dev, total_blocks)
282 }
283
284 fn parent_block_and_name<'p>(&self, path: &'p str) -> Result<(u32, &'p str)> {
288 let trimmed = path.trim_matches('/');
289 let (dir, name) = trimmed.rsplit_once('/').unwrap_or(("", trimmed));
290 if name.is_empty() {
291 return Err(Error::InvalidArgument("affs: empty entry name".into()));
292 }
293 let parent = match self.resolve(dir) {
294 Some(Resolved::Dir(b)) => b,
295 Some(_) => {
296 return Err(Error::InvalidArgument(
297 "affs: parent is not a directory".into(),
298 ));
299 }
300 None => {
301 return Err(Error::InvalidArgument(format!(
302 "affs: no such directory {dir:?}"
303 )));
304 }
305 };
306 if self
307 .children
308 .get(&parent)
309 .is_some_and(|kids| kids.iter().any(|n| n.name.eq_ignore_ascii_case(name)))
310 {
311 return Err(Error::InvalidArgument(format!(
312 "affs: {name:?} already exists"
313 )));
314 }
315 Ok((parent, name))
316 }
317
318 fn locate_for_remove<'p>(&self, path: &'p str) -> Result<(u32, u32, &'p str)> {
320 let trimmed = path.trim_matches('/');
321 let (dir, name) = trimmed.rsplit_once('/').unwrap_or(("", trimmed));
322 if name.is_empty() {
323 return Err(Error::InvalidArgument(
324 "affs: cannot remove the root".into(),
325 ));
326 }
327 let parent = match self.resolve(dir) {
328 Some(Resolved::Dir(b)) => b,
329 _ => {
330 return Err(Error::InvalidArgument(format!(
331 "affs: no such directory {dir:?}"
332 )));
333 }
334 };
335 let entry = self
336 .children
337 .get(&parent)
338 .and_then(|kids| kids.iter().find(|n| n.name.eq_ignore_ascii_case(name)))
339 .map(|n| n.block)
340 .ok_or_else(|| Error::InvalidArgument(format!("affs: no such path {path:?}")))?;
341 Ok((parent, entry, name))
342 }
343
344 fn build_index(&mut self, dev: &mut dyn BlockDevice, total_blocks: u32) -> Result<()> {
346 let mut children: HashMap<u32, Vec<Node>> = HashMap::new();
347 let mut stack = vec![self.root_block];
349 let mut visited = std::collections::HashSet::new();
350 while let Some(dir_block) = stack.pop() {
351 if !visited.insert(dir_block) {
352 continue;
353 }
354 let mut block = vec![0u8; BSIZE];
355 dev.read_at(dir_block as u64 * BSIZE as u64, &mut block)?;
356 let entries = self.read_hashtable(dev, &block, total_blocks, &visited)?;
357 for node in &entries {
358 if node.kind == EntryKind::Dir {
359 stack.push(node.block);
360 }
361 }
362 children.insert(dir_block, entries);
363 }
364 self.children = children;
365 Ok(())
366 }
367
368 fn read_hashtable(
371 &self,
372 dev: &mut dyn BlockDevice,
373 dir: &[u8],
374 total_blocks: u32,
375 visited: &std::collections::HashSet<u32>,
376 ) -> Result<Vec<Node>> {
377 let mut out = Vec::new();
378 for i in 0..HT_SIZE {
379 let mut entry = be_u32(dir, OFF_HASHTABLE + i * 4);
380 let mut seen = std::collections::HashSet::new();
387 while entry != 0 && (entry as u64) < total_blocks as u64 {
388 if visited.contains(&entry) {
389 break; }
391 if !seen.insert(entry) {
392 break; }
394 let mut hb = vec![0u8; BSIZE];
395 dev.read_at(entry as u64 * BSIZE as u64, &mut hb)?;
396 let sec_type = be_i32(&hb, OFF_SEC_TYPE);
397 let kind = match sec_type {
398 ST_USERDIR | ST_LINKDIR => EntryKind::Dir,
399 ST_FILE | ST_LINKFILE => EntryKind::Regular,
400 ST_SOFTLINK => EntryKind::Symlink,
401 _ => EntryKind::Unknown,
402 };
403 let name = read_name(&hb);
404 let size = if kind == EntryKind::Regular {
405 be_u32(&hb, OFF_BYTE_SIZE) as u64
406 } else {
407 0
408 };
409 let link_target = if kind == EntryKind::Symlink {
410 Some(read_softlink_target(&hb))
411 } else {
412 None
413 };
414 let mtime = amiga_date_to_unix(
415 be_i32(&hb, OFF_DAYS),
416 be_i32(&hb, OFF_DAYS + 4),
417 be_i32(&hb, OFF_DAYS + 8),
418 );
419 if !name.is_empty() && kind != EntryKind::Unknown {
420 out.push(Node {
421 name,
422 block: entry,
423 kind,
424 size,
425 link_target,
426 mtime,
427 });
428 }
429 entry = be_u32(&hb, OFF_NEXT_SAME_HASH);
430 }
431 }
432 Ok(out)
433 }
434
435 fn resolve<'a>(&'a self, path: &str) -> Option<Resolved<'a>> {
437 let trimmed = path.trim_matches('/');
438 if trimmed.is_empty() {
439 return Some(Resolved::Dir(self.root_block));
440 }
441 let mut cur_dir = self.root_block;
442 let mut comps = trimmed.split('/').peekable();
443 while let Some(comp) = comps.next() {
444 let kids = self.children.get(&cur_dir)?;
445 let node = kids.iter().find(|n| n.name.eq_ignore_ascii_case(comp))?;
446 let is_last = comps.peek().is_none();
447 match node.kind {
448 EntryKind::Dir => {
449 if is_last {
450 return Some(Resolved::Dir(node.block));
451 }
452 cur_dir = node.block;
453 }
454 _ => {
455 if is_last {
456 return Some(Resolved::Node(node));
457 }
458 return None; }
460 }
461 }
462 None
463 }
464
465 pub fn list_path(&self, path: &str) -> Result<Vec<DirEntry>> {
469 if let Write::Format(w) = &self.mode {
470 return w.list(path);
471 }
472 let block = match self.resolve(path) {
473 Some(Resolved::Dir(b)) => b,
474 Some(_) => {
475 return Err(Error::InvalidArgument("affs: not a directory".into()));
476 }
477 None => {
478 return Err(Error::InvalidArgument(format!(
479 "affs: no such path {path:?}"
480 )));
481 }
482 };
483 let mut out = Vec::new();
484 if let Some(kids) = self.children.get(&block) {
485 for n in kids {
486 out.push(DirEntry {
487 name: n.name.clone(),
488 inode: n.block,
489 kind: n.kind,
490 size: n.size,
491 });
492 }
493 }
494 Ok(out)
495 }
496
497 fn data_blocks(&self, dev: &mut dyn BlockDevice, header: u32) -> Result<Vec<u32>> {
500 let total_blocks = (dev.total_size() / BSIZE as u64) as u32;
506 let mut blocks = Vec::new();
507 let mut cur = header;
508 let mut visited = std::collections::HashSet::new();
509 let mut buf = vec![0u8; BSIZE];
510 while cur != 0 {
511 if (cur as u64) >= total_blocks as u64 {
512 return Err(Error::InvalidImage(
513 "affs: file extension block out of range".into(),
514 ));
515 }
516 if !visited.insert(cur) {
517 return Err(Error::InvalidImage("affs: file extension loop".into()));
518 }
519 dev.read_at(cur as u64 * BSIZE as u64, &mut buf)?;
520 let high_seq = be_i32(&buf, OFF_HIGH_SEQ).clamp(0, MAX_DATABLK as i32) as usize;
521 for i in 0..high_seq {
524 let slot = MAX_DATABLK - 1 - i;
525 let ptr = be_u32(&buf, OFF_HASHTABLE + slot * 4);
526 if ptr != 0 {
527 blocks.push(ptr);
528 }
529 }
530 cur = be_u32(&buf, OFF_EXTENSION);
531 }
532 Ok(blocks)
533 }
534
535 pub fn open_file_reader<'a>(
540 &self,
541 dev: &'a mut dyn BlockDevice,
542 path: &str,
543 ) -> Result<AffsFileReader<'a>> {
544 if let Write::Format(w) = &self.mode {
545 let data = w.read(path)?;
546 let size = data.len() as u64;
547 return Ok(AffsFileReader {
548 dev,
549 blocks: Vec::new(),
550 block_size: self.block_size,
551 ofs: false,
552 size,
553 pos: 0,
554 mem: Some(data),
555 });
556 }
557 let (header, size) = match self.resolve(path) {
558 Some(Resolved::Node(n)) if n.kind == EntryKind::Regular => (n.block, n.size),
559 Some(_) => return Err(Error::InvalidArgument("affs: not a regular file".into())),
560 None => {
561 return Err(Error::InvalidArgument(format!(
562 "affs: no such path {path:?}"
563 )));
564 }
565 };
566 let blocks = self.data_blocks(dev, header)?;
567 Ok(AffsFileReader {
568 dev,
569 blocks,
570 block_size: self.block_size,
571 ofs: !self.variant.ffs,
572 size,
573 pos: 0,
574 mem: None,
575 })
576 }
577
578 pub fn variant(&self) -> Variant {
580 self.variant
581 }
582}
583
584enum Resolved<'a> {
585 Dir(u32),
586 Node(&'a Node),
587}
588
589fn read_softlink_target(hb: &[u8]) -> String {
592 let start = OFF_HASHTABLE;
593 let end = (start..BSIZE).find(|&i| hb[i] == 0).unwrap_or(BSIZE);
594 hb[start..end].iter().map(|&b| b as char).collect()
595}
596
597pub struct AffsFileReader<'a> {
601 dev: &'a mut dyn BlockDevice,
602 blocks: Vec<u32>,
603 block_size: u32,
604 ofs: bool,
605 size: u64,
606 pos: u64,
607 mem: Option<Vec<u8>>,
610}
611
612impl AffsFileReader<'_> {
613 fn payload(&self) -> u64 {
615 if self.ofs {
616 self.block_size as u64 - 24
617 } else {
618 self.block_size as u64
619 }
620 }
621}
622
623impl Read for AffsFileReader<'_> {
624 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
625 if self.pos >= self.size {
626 return Ok(0);
627 }
628 if let Some(m) = &self.mem {
629 let start = self.pos as usize;
630 let want = (buf.len()).min(m.len() - start);
631 buf[..want].copy_from_slice(&m[start..start + want]);
632 self.pos += want as u64;
633 return Ok(want);
634 }
635 let payload = self.payload();
636 let blk_idx = (self.pos / payload) as usize;
637 let within = self.pos % payload;
638 let Some(&blk) = self.blocks.get(blk_idx) else {
639 return Ok(0);
640 };
641 let data_off = if self.ofs { 24 } else { 0 };
642 let avail_in_block = (payload - within).min(self.size - self.pos);
643 let want = (buf.len() as u64).min(avail_in_block) as usize;
644 let off = blk as u64 * self.block_size as u64 + data_off + within;
645 self.dev
646 .read_at(off, &mut buf[..want])
647 .map_err(|e| io::Error::other(e.to_string()))?;
648 self.pos += want as u64;
649 Ok(want)
650 }
651}
652
653impl Seek for AffsFileReader<'_> {
654 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
655 let new = match pos {
656 SeekFrom::Start(n) => n as i64,
657 SeekFrom::End(n) => self.size as i64 + n,
658 SeekFrom::Current(n) => self.pos as i64 + n,
659 };
660 if new < 0 {
661 return Err(io::Error::new(
662 io::ErrorKind::InvalidInput,
663 "affs: seek before start",
664 ));
665 }
666 self.pos = new as u64;
667 Ok(self.pos)
668 }
669}
670
671impl crate::fs::FileReadHandle for AffsFileReader<'_> {
672 fn len(&self) -> u64 {
673 self.size
674 }
675}
676
677impl crate::fs::FilesystemFactory for Affs {
678 type FormatOpts = AffsFormatOpts;
679
680 fn format(dev: &mut dyn BlockDevice, opts: &Self::FormatOpts) -> Result<Self> {
681 Affs::format(dev, opts)
682 }
683
684 fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
685 Affs::open(dev)
686 }
687
688 fn size_plan(opts: &Self::FormatOpts) -> Option<Box<dyn crate::fs::FsSizePlan>> {
689 Some(Box::new(AffsSizePlan::new(opts.ffs)))
690 }
691}
692
693impl Filesystem for Affs {
694 fn streams_immediately(&self) -> bool {
695 true
697 }
698
699 fn create_file(
700 &mut self,
701 dev: &mut dyn BlockDevice,
702 path: &Path,
703 src: crate::fs::FileSource,
704 meta: crate::fs::FileMeta,
705 ) -> Result<()> {
706 let s = path
707 .to_str()
708 .ok_or_else(|| Error::InvalidArgument("affs: non-UTF-8 path".into()))?;
709 if matches!(self.mode, Write::None) {
710 return Err(Error::Immutable {
711 kind: "affs",
712 op: "add",
713 });
714 }
715 let (mut reader, len) = src.open()?;
716 let mut data = Vec::with_capacity(len as usize);
717 std::io::Read::take(&mut reader, len).read_to_end(&mut data)?;
718 if let Write::Format(w) = &mut self.mode {
719 w.insert_file(s, data, meta.mtime)?;
720 return Ok(());
721 }
722 let (parent, name) = self.parent_block_and_name(s)?;
724 if let Write::InPlace(ed) = &mut self.mode {
725 ed.create_file(dev, parent, name, &data, meta.mtime)?;
726 }
727 self.refresh_index(dev)
728 }
729
730 fn create_dir(
731 &mut self,
732 dev: &mut dyn BlockDevice,
733 path: &Path,
734 meta: crate::fs::FileMeta,
735 ) -> Result<()> {
736 let s = path
737 .to_str()
738 .ok_or_else(|| Error::InvalidArgument("affs: non-UTF-8 path".into()))?;
739 if matches!(self.mode, Write::None) {
740 return Err(Error::Immutable {
741 kind: "affs",
742 op: "mkdir",
743 });
744 }
745 if let Write::Format(w) = &mut self.mode {
746 w.insert_dir(s, meta.mtime)?;
747 return Ok(());
748 }
749 let (parent, name) = self.parent_block_and_name(s)?;
750 if let Write::InPlace(ed) = &mut self.mode {
751 ed.create_dir(dev, parent, name, meta.mtime)?;
752 }
753 self.refresh_index(dev)
754 }
755
756 fn create_symlink(
757 &mut self,
758 _dev: &mut dyn BlockDevice,
759 _path: &Path,
760 _target: &Path,
761 _meta: crate::fs::FileMeta,
762 ) -> Result<()> {
763 Err(Error::Unsupported(
766 "affs: symlink creation not yet implemented".into(),
767 ))
768 }
769
770 fn create_device(
771 &mut self,
772 _dev: &mut dyn BlockDevice,
773 _path: &Path,
774 _kind: crate::fs::DeviceKind,
775 _major: u32,
776 _minor: u32,
777 _meta: crate::fs::FileMeta,
778 ) -> Result<()> {
779 Err(Error::Immutable {
780 kind: "affs",
781 op: "mknod",
782 })
783 }
784
785 fn remove(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<()> {
786 let s = path
787 .to_str()
788 .ok_or_else(|| Error::InvalidArgument("affs: non-UTF-8 path".into()))?;
789 if matches!(self.mode, Write::None) {
790 return Err(Error::Immutable {
791 kind: "affs",
792 op: "rm",
793 });
794 }
795 if let Write::Format(w) = &mut self.mode {
796 return w.remove(s);
797 }
798 let (parent, entry, name) = self.locate_for_remove(s)?;
799 if let Write::InPlace(ed) = &mut self.mode {
800 ed.remove(dev, parent, entry, name)?;
801 }
802 self.refresh_index(dev)
803 }
804
805 fn list(&mut self, _dev: &mut dyn BlockDevice, path: &Path) -> Result<Vec<DirEntry>> {
806 let s = path
807 .to_str()
808 .ok_or_else(|| Error::InvalidArgument("affs: non-UTF-8 path".into()))?;
809 self.list_path(s)
810 }
811
812 fn getattr(&mut self, _dev: &mut dyn BlockDevice, path: &Path) -> Result<crate::fs::FileAttrs> {
813 let s = path
814 .to_str()
815 .ok_or_else(|| Error::InvalidArgument("affs: non-UTF-8 path".into()))?;
816 if let Write::Format(w) = &self.mode {
817 return w.getattr(s);
818 }
819 let (kind, size, mtime, inode) = match self.resolve(s) {
820 Some(Resolved::Dir(b)) => (EntryKind::Dir, 0u64, 0u32, b),
821 Some(Resolved::Node(n)) => (n.kind, n.size, n.mtime, n.block),
822 None => return Err(Error::InvalidArgument(format!("affs: no such path {s:?}"))),
823 };
824 let mode = match kind {
825 EntryKind::Dir => 0o755,
826 EntryKind::Symlink => 0o777,
827 _ => 0o644,
828 };
829 Ok(crate::fs::FileAttrs {
830 kind,
831 mode,
832 uid: 0,
833 gid: 0,
834 size,
835 blocks: size.div_ceil(512),
836 nlink: if kind == EntryKind::Dir { 2 } else { 1 },
837 atime: mtime,
838 mtime,
839 ctime: mtime,
840 rdev: 0,
841 inode,
842 })
843 }
844
845 fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
846 match &mut self.mode {
847 Write::Format(w) => w.flush(dev)?,
848 Write::InPlace(ed) => ed.flush(dev)?,
849 Write::None => {}
850 }
851 Ok(())
852 }
853
854 fn read_file<'a>(
855 &'a mut self,
856 dev: &'a mut dyn BlockDevice,
857 path: &Path,
858 ) -> Result<Box<dyn Read + 'a>> {
859 let s = path
860 .to_str()
861 .ok_or_else(|| Error::InvalidArgument("affs: non-UTF-8 path".into()))?;
862 Ok(Box::new(self.open_file_reader(dev, s)?))
863 }
864
865 fn open_file_ro<'a>(
866 &'a mut self,
867 dev: &'a mut dyn BlockDevice,
868 path: &Path,
869 ) -> Result<Box<dyn crate::fs::FileReadHandle + 'a>> {
870 let s = path
871 .to_str()
872 .ok_or_else(|| Error::InvalidArgument("affs: non-UTF-8 path".into()))?;
873 Ok(Box::new(self.open_file_reader(dev, s)?))
874 }
875
876 fn read_symlink(
877 &mut self,
878 _dev: &mut dyn BlockDevice,
879 path: &Path,
880 ) -> Result<std::path::PathBuf> {
881 let s = path
882 .to_str()
883 .ok_or_else(|| Error::InvalidArgument("affs: non-UTF-8 path".into()))?;
884 match self.resolve(s) {
885 Some(Resolved::Node(n)) if n.kind == EntryKind::Symlink => Ok(
886 std::path::PathBuf::from(n.link_target.clone().unwrap_or_default()),
887 ),
888 Some(_) => Err(Error::InvalidArgument("affs: not a symlink".into())),
889 None => Err(Error::InvalidArgument(format!("affs: no such path {s:?}"))),
890 }
891 }
892
893 fn mutation_capability(&self) -> MutationCapability {
894 if matches!(self.mode, Write::Format(_) | Write::InPlace(_)) {
895 MutationCapability::Mutable
896 } else {
897 MutationCapability::Immutable
898 }
899 }
900}
901
902#[allow(dead_code)]
903const _: () = {
904 assert!(HT_SIZE == BSIZE / 4 - 56);
906 assert!(OFF_SEC_TYPE == BSIZE - 4);
907 assert!(OFF_EXTENSION == BSIZE - 8);
908 assert!(OFF_NEXT_SAME_HASH == BSIZE - 16);
909 assert!(T_LIST == 16 && T_HEADER == 2);
910};
911
912#[cfg(test)]
913mod tests;