a2fuse 0.1.3

Mount and maintain Apple II ProDOS disk images
Documentation
use crate::error::{A2FuseError, Result};

use super::block::{BLOCK_SIZE, BlockDevice};
use super::directory::DirectoryEntry;
use super::types::StorageType;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileFork {
    pub name: String,
    pub storage_type: StorageType,
    pub key_pointer: u16,
    pub blocks_used: u16,
    pub eof: u32,
}

impl FileFork {
    pub fn from_entry(entry: &DirectoryEntry) -> Self {
        Self {
            name: entry.name.clone(),
            storage_type: entry.storage_type,
            key_pointer: entry.key_pointer,
            blocks_used: entry.blocks_used,
            eof: entry.eof,
        }
    }

    pub fn new(
        name: impl Into<String>,
        storage_type: StorageType,
        key_pointer: u16,
        blocks_used: u16,
        eof: u32,
    ) -> Self {
        Self {
            name: name.into(),
            storage_type,
            key_pointer,
            blocks_used,
            eof,
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExtendedKeyBlock {
    pub data_fork: FileFork,
    pub resource_fork: FileFork,
    pub finder_info: [u8; 36],
}

pub fn data_block_numbers(
    device: &BlockDevice,
    entry: &DirectoryEntry,
) -> Result<Vec<Option<u16>>> {
    let block_count =
        usize::try_from(entry.eof.div_ceil(BLOCK_SIZE as u32)).expect("a 24-bit EOF fits in usize");

    if block_count > 0 && entry.key_pointer == 0 {
        return Err(A2FuseError::InvalidDirectoryEntry(format!(
            "{} has data but no key block",
            entry.name
        )));
    }

    match entry.storage_type {
        StorageType::Seedling => {
            if block_count > 1 {
                return Err(A2FuseError::InvalidDirectoryEntry(format!(
                    "seedling file {} is too large for one block",
                    entry.name
                )));
            }
            Ok((block_count > 0)
                .then_some(Some(entry.key_pointer))
                .into_iter()
                .collect())
        }
        StorageType::Sapling => {
            if block_count > 256 {
                return Err(A2FuseError::InvalidDirectoryEntry(format!(
                    "sapling file {} is too large for one index block",
                    entry.name
                )));
            }
            let index = device.read_block(entry.key_pointer)?;
            Ok((0..block_count)
                .map(|position| pointer_at(index, position))
                .collect())
        }
        StorageType::Tree => {
            let master = device.read_block(entry.key_pointer)?;
            let mut blocks = Vec::with_capacity(block_count);
            for position in 0..block_count {
                let sapling_position = position / 256;
                let data_position = position % 256;
                let sapling_pointer = pointer_at(master, sapling_position);
                let data_pointer = match sapling_pointer {
                    Some(pointer) => pointer_at(device.read_block(pointer)?, data_position),
                    None => None,
                };
                blocks.push(data_pointer);
            }
            Ok(blocks)
        }
        storage_type => Err(A2FuseError::UnsupportedStorageType {
            storage_type: storage_type as u8,
            name: entry.name.clone(),
        }),
    }
}

pub fn read_fork(device: &BlockDevice, fork: &FileFork) -> Result<Vec<u8>> {
    if !fork.storage_type.is_regular_file() {
        return Err(A2FuseError::UnsupportedStorageType {
            storage_type: fork.storage_type as u8,
            name: fork.name.clone(),
        });
    }

    let block_count =
        usize::try_from(fork.eof.div_ceil(BLOCK_SIZE as u32)).expect("a 24-bit EOF fits in usize");

    if block_count > 0 && fork.key_pointer == 0 {
        return Err(A2FuseError::InvalidDirectoryEntry(format!(
            "{} has data but no key block",
            fork.name
        )));
    }

    let mut data = Vec::with_capacity(fork.eof as usize);
    for block_number in match fork.storage_type {
        StorageType::Seedling => {
            if block_count > 1 {
                return Err(A2FuseError::InvalidDirectoryEntry(format!(
                    "seedling file {} is too large for one block",
                    fork.name
                )));
            }
            (block_count > 0)
                .then_some(Some(fork.key_pointer))
                .into_iter()
                .collect()
        }
        StorageType::Sapling => {
            if block_count > 256 {
                return Err(A2FuseError::InvalidDirectoryEntry(format!(
                    "sapling file {} is too large for one index block",
                    fork.name
                )));
            }
            let index = device.read_block(fork.key_pointer)?;
            (0..block_count)
                .map(|position| pointer_at(index, position))
                .collect()
        }
        StorageType::Tree => {
            let master = device.read_block(fork.key_pointer)?;
            let mut blocks = Vec::with_capacity(block_count);
            for position in 0..block_count {
                let sapling_position = position / 256;
                let data_position = position % 256;
                let sapling_pointer = pointer_at(master, sapling_position);
                let data_pointer = match sapling_pointer {
                    Some(pointer) => pointer_at(device.read_block(pointer)?, data_position),
                    None => None,
                };
                blocks.push(data_pointer);
            }
            blocks
        }
        storage_type => {
            return Err(A2FuseError::UnsupportedStorageType {
                storage_type: storage_type as u8,
                name: fork.name.clone(),
            });
        }
    } {
        match block_number {
            Some(block_number) => data.extend_from_slice(device.read_block(block_number)?),
            None => data.resize(data.len() + BLOCK_SIZE, 0),
        }
    }
    data.truncate(fork.eof as usize);
    Ok(data)
}

pub fn read_file(device: &BlockDevice, entry: &DirectoryEntry) -> Result<Vec<u8>> {
    if entry.storage_type == StorageType::Extended {
        return read_extended_data_fork(device, entry);
    }
    if !entry.is_file() {
        return Err(A2FuseError::NotAFile(entry.name.clone()));
    }
    read_fork(device, &FileFork::from_entry(entry))
}

pub fn read_extended_data_fork(device: &BlockDevice, entry: &DirectoryEntry) -> Result<Vec<u8>> {
    read_fork(device, &extended_key_block(device, entry)?.data_fork)
}

pub fn read_extended_resource_fork(
    device: &BlockDevice,
    entry: &DirectoryEntry,
) -> Result<Vec<u8>> {
    read_fork(device, &extended_key_block(device, entry)?.resource_fork)
}

pub fn extended_key_block(
    device: &BlockDevice,
    entry: &DirectoryEntry,
) -> Result<ExtendedKeyBlock> {
    if entry.storage_type != StorageType::Extended {
        return Err(A2FuseError::UnsupportedStorageType {
            storage_type: entry.storage_type as u8,
            name: entry.name.clone(),
        });
    }
    if entry.key_pointer == 0 {
        return Err(A2FuseError::InvalidDirectoryEntry(format!(
            "{} has no key block",
            entry.name
        )));
    }
    let block = device.read_block(entry.key_pointer)?;
    let data_fork = fork_entry(entry.name.clone(), block, 0)?;
    let resource_fork = fork_entry(format!("._{}", entry.name), block, 256)?;
    let mut finder_info = [0_u8; 36];
    finder_info.copy_from_slice(&block[8..44]);
    Ok(ExtendedKeyBlock {
        data_fork,
        resource_fork,
        finder_info,
    })
}

fn fork_entry(
    name: impl Into<String>,
    block: &[u8; BLOCK_SIZE],
    offset: usize,
) -> Result<FileFork> {
    let storage_type = StorageType::from_nibble(block[offset]).ok_or_else(|| {
        A2FuseError::InvalidDirectoryEntry(format!(
            "unknown extended fork storage type {:#x}",
            block[offset]
        ))
    })?;
    Ok(FileFork::new(
        name,
        storage_type,
        u16::from_le_bytes([block[offset + 1], block[offset + 2]]),
        u16::from_le_bytes([block[offset + 3], block[offset + 4]]),
        u32::from(block[offset + 5])
            | (u32::from(block[offset + 6]) << 8)
            | (u32::from(block[offset + 7]) << 16),
    ))
}

fn pointer_at(index_block: &[u8; BLOCK_SIZE], position: usize) -> Option<u16> {
    if position >= 256 {
        return None;
    }
    let pointer = u16::from(index_block[position]) | (u16::from(index_block[position + 256]) << 8);
    (pointer != 0).then_some(pointer)
}