a2fuse 0.1.3

Mount and maintain Apple II ProDOS disk images
Documentation
use std::path::Path;

use crate::error::{A2FuseError, Result};

use super::block::BlockDevice;
use super::directory::{Directory, DirectoryEntry, PRODOS_ENTRY_LENGTH};
use super::file::{
    FileFork, extended_key_block, read_extended_data_fork, read_extended_resource_fork, read_fork,
};
use super::path::{MetadataMode, decode_filename_with_case, host_filename};
use super::types::{AccessFlags, ProdosTimestamp, StorageType};

pub const VOLUME_DIRECTORY_KEY_BLOCK: u16 = 2;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VolumeHeader {
    pub name: String,
    pub creation: Option<ProdosTimestamp>,
    pub access: AccessFlags,
    pub entry_length: u8,
    pub entries_per_block: u8,
    pub file_count: u16,
    pub bitmap_pointer: u16,
    pub total_blocks: u16,
}

impl VolumeHeader {
    fn parse(bytes: &[u8]) -> Result<Self> {
        if bytes.len() < PRODOS_ENTRY_LENGTH {
            return Err(A2FuseError::InvalidVolume(
                "volume header is shorter than 39 bytes".to_owned(),
            ));
        }
        let storage_type = bytes[0] >> 4;
        let name_length = usize::from(bytes[0] & 0x0f);
        if storage_type != StorageType::VolumeHeader as u8 {
            return Err(A2FuseError::InvalidVolume(format!(
                "block 2 does not begin with a volume header (storage type {storage_type:#x})"
            )));
        }
        if name_length == 0 || name_length > 15 {
            return Err(A2FuseError::InvalidVolume(format!(
                "invalid volume name length {name_length}"
            )));
        }

        let header = Self {
            name: decode_filename_with_case(&bytes[1..1 + name_length], read_u16(bytes, 0x16)),
            creation: ProdosTimestamp::decode(read_u16(bytes, 0x18), read_u16(bytes, 0x1a)),
            access: AccessFlags(bytes[0x1e]),
            entry_length: bytes[0x1f],
            entries_per_block: bytes[0x20],
            file_count: read_u16(bytes, 0x21),
            bitmap_pointer: read_u16(bytes, 0x23),
            total_blocks: read_u16(bytes, 0x25),
        };

        if usize::from(header.entry_length) != PRODOS_ENTRY_LENGTH {
            return Err(A2FuseError::InvalidVolume(format!(
                "unsupported directory entry length {}",
                header.entry_length
            )));
        }
        if header.entries_per_block != 13 {
            return Err(A2FuseError::InvalidVolume(format!(
                "unsupported entries-per-block value {}",
                header.entries_per_block
            )));
        }
        Ok(header)
    }
}

#[derive(Clone, Debug)]
pub struct Node {
    pub entry: DirectoryEntry,
    pub data_fork: Option<FileFork>,
    pub resource_fork: Option<FileFork>,
    pub children: Vec<Node>,
}

impl Node {
    pub fn host_name(&self, metadata_mode: MetadataMode) -> String {
        host_filename(&self.entry, metadata_mode)
    }

    pub fn is_directory(&self) -> bool {
        self.entry.is_directory()
    }

    pub fn is_extended_file(&self) -> bool {
        self.resource_fork.is_some()
    }

    pub fn effective_eof(&self) -> u32 {
        self.data_fork
            .as_ref()
            .map_or(self.entry.eof, |fork| fork.eof)
    }

    pub fn effective_blocks_used(&self) -> u16 {
        self.data_fork
            .as_ref()
            .map_or(self.entry.blocks_used, |fork| fork.blocks_used)
    }

    pub fn effective_storage_type(&self) -> StorageType {
        self.data_fork
            .as_ref()
            .map_or(self.entry.storage_type, |fork| fork.storage_type)
    }
}

#[derive(Clone, Debug)]
pub struct Volume {
    device: BlockDevice,
    pub header: VolumeHeader,
    pub root: Vec<Node>,
}

impl Volume {
    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
        Self::from_device(BlockDevice::open(path)?)
    }

    pub fn from_device(device: BlockDevice) -> Result<Self> {
        if device.block_count() <= usize::from(VOLUME_DIRECTORY_KEY_BLOCK) {
            return Err(A2FuseError::InvalidVolume(
                "image is too small to contain a volume directory".to_owned(),
            ));
        }

        let directory_block = device.read_block(VOLUME_DIRECTORY_KEY_BLOCK)?;
        let header = VolumeHeader::parse(&directory_block[4..4 + PRODOS_ENTRY_LENGTH])?;
        if header.total_blocks != 0 && usize::from(header.total_blocks) > device.block_count() {
            return Err(A2FuseError::InvalidVolume(format!(
                "volume claims {} blocks but image contains {}",
                header.total_blocks,
                device.block_count()
            )));
        }

        let root_directory = Directory::read(&device, VOLUME_DIRECTORY_KEY_BLOCK)?;
        let root = Self::load_nodes(&device, root_directory, &mut Vec::new())?;
        Ok(Self {
            device,
            header,
            root,
        })
    }

    pub fn read_entry(&self, entry: &DirectoryEntry) -> Result<Vec<u8>> {
        if entry.storage_type == StorageType::Extended {
            read_extended_data_fork(&self.device, entry)
        } else {
            read_fork(&self.device, &FileFork::from_entry(entry))
        }
    }

    pub fn read_fork(&self, fork: &FileFork) -> Result<Vec<u8>> {
        read_fork(&self.device, fork)
    }

    pub fn read_resource_fork(&self, entry: &DirectoryEntry) -> Result<Vec<u8>> {
        read_extended_resource_fork(&self.device, entry)
    }

    pub fn find<'a>(&'a self, path: &str, metadata_mode: MetadataMode) -> Result<&'a Node> {
        let mut nodes = &self.root;
        let mut found = None;

        for component in path.split('/').filter(|part| !part.is_empty()) {
            let node = nodes
                .iter()
                .find(|node| {
                    node.host_name(metadata_mode)
                        .eq_ignore_ascii_case(component)
                })
                .ok_or_else(|| A2FuseError::PathNotFound(path.to_owned()))?;
            found = Some(node);
            nodes = &node.children;
        }

        found.ok_or_else(|| A2FuseError::PathNotFound(path.to_owned()))
    }

    fn load_nodes(
        device: &BlockDevice,
        directory: Directory,
        ancestors: &mut Vec<u16>,
    ) -> Result<Vec<Node>> {
        if ancestors.contains(&directory.key_block) {
            return Err(A2FuseError::InvalidDirectory(format!(
                "recursive directory reference to block {}",
                directory.key_block
            )));
        }
        ancestors.push(directory.key_block);

        let mut nodes = Vec::with_capacity(directory.entries.len());
        for entry in directory.entries {
            let (data_fork, resource_fork, children) = if entry.is_directory() {
                if entry.key_pointer == 0 {
                    return Err(A2FuseError::InvalidDirectory(format!(
                        "subdirectory {} has no key block",
                        entry.name
                    )));
                }
                let child_directory = Directory::read(device, entry.key_pointer)?;
                let children = Self::load_nodes(device, child_directory, ancestors)?;
                (None, None, children)
            } else if entry.storage_type == StorageType::Extended {
                let extended = extended_key_block(device, &entry)?;
                (
                    Some(extended.data_fork),
                    Some(extended.resource_fork),
                    Vec::new(),
                )
            } else {
                if entry.eof > 0 && entry.key_pointer == 0 {
                    return Err(A2FuseError::InvalidDirectoryEntry(format!(
                        "{} has no key block",
                        entry.name
                    )));
                }
                (Some(FileFork::from_entry(&entry)), None, Vec::new())
            };
            nodes.push(Node {
                entry,
                data_fork,
                resource_fork,
                children,
            });
        }

        ancestors.pop();
        Ok(nodes)
    }
}

fn read_u16(bytes: &[u8], offset: usize) -> u16 {
    u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
}