use super::entry::{EntryType, FileEntry};
use super::filesystem::{Filesystem, FilesystemError};
use crate::sector_reader::SectorReader;
const ENTRY_FIXED: usize = 68;
pub fn detect_opera(reader: &mut dyn SectorReader) -> bool {
match reader.read_sector(0) {
Ok(b) => b.len() >= 6 && b[0] == 0x01 && b[1..6] == [0x5A; 5],
Err(_) => false,
}
}
pub fn read_label(reader: &mut dyn SectorReader) -> Option<String> {
let vh = reader.read_sector(0).ok()?;
if vh.len() < 72 || vh[0] != 0x01 || vh[1..6] != [0x5A; 5] {
return None;
}
let s = ascii_field(&vh[40..72]);
if s.is_empty() {
None
} else {
Some(s)
}
}
pub struct OperaFilesystem {
reader: Box<dyn SectorReader>,
block_size: u32,
root_block: u32,
root_blocks: u32,
label: String,
}
impl OperaFilesystem {
pub fn new(mut reader: Box<dyn SectorReader>) -> Result<Self, FilesystemError> {
let vh = reader
.read_sector(0)
.map_err(|e| FilesystemError::Io(io_of(e)))?;
if vh.len() < 0x6C || vh[0] != 0x01 || vh[1..6] != [0x5A; 5] {
return Err(FilesystemError::Parse("not a 3DO Opera volume".into()));
}
let label = ascii_field(&vh[40..72]);
let block_size = be32(&vh, 0x4C);
let root_blocks = be32(&vh, 0x58);
let root_block = be32(&vh, 0x64);
if block_size == 0 {
return Err(FilesystemError::Parse("Opera block size is zero".into()));
}
Ok(Self {
reader,
block_size,
root_block,
root_blocks,
label,
})
}
fn read_blocks(&mut self, start_block: u32, count: u32) -> Result<Vec<u8>, FilesystemError> {
let byte_off = start_block as u64 * self.block_size as u64;
let len = count.max(1) as usize * self.block_size as usize;
self.reader
.read_bytes(byte_off, len)
.map_err(|e| FilesystemError::Io(io_of(e)))
}
fn read_dir(
&mut self,
start: u32,
blocks: u32,
base_path: &str,
) -> Result<Vec<FileEntry>, FilesystemError> {
let bs = self.block_size as usize;
let data = self.read_blocks(start, blocks)?;
let mut out = Vec::new();
let nblocks = (data.len() / bs).max(1);
for b in 0..nblocks {
let block = &data[b * bs..(b * bs + bs).min(data.len())];
if block.len() < 20 {
break;
}
let content_end = (be32(block, 12) as usize).min(block.len());
let first_entry = be32(block, 16) as usize;
let mut pos = first_entry;
while pos + ENTRY_FIXED <= content_end {
let flags = be32(block, pos);
let type_tag = &block[pos + 8..pos + 12];
let byte_count = be32(block, pos + 16);
let name = ascii_field(&block[pos + 32..pos + 64]);
let copies = be32(block, pos + 64);
let avatar0 = be32(block, pos + ENTRY_FIXED);
let entry_len = ENTRY_FIXED + (copies as usize + 1) * 4;
let is_dir = type_tag == b"*dir";
if !name.is_empty() {
let path = format!("{}/{}", base_path.trim_end_matches('/'), name);
out.push(make_entry(name, path, avatar0, byte_count, is_dir));
}
pos += entry_len;
if flags & 0x8000_0000 != 0 {
return Ok(out);
}
if flags & 0x4000_0000 != 0 {
break;
}
}
}
Ok(out)
}
fn blocks_for(&self, byte_size: u32) -> u32 {
byte_size.div_ceil(self.block_size).max(1)
}
}
impl Filesystem for OperaFilesystem {
fn root(&mut self) -> Result<FileEntry, FilesystemError> {
Ok(make_entry(
String::new(),
"/".into(),
self.root_block,
self.root_blocks * self.block_size,
true,
))
}
fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
if entry.entry_type != EntryType::Directory {
return Err(FilesystemError::NotADirectory(entry.path.clone()));
}
let blocks = self.blocks_for(entry.size as u32);
let path = entry.path.clone();
self.read_dir(entry.location as u32, blocks, &path)
}
fn read_file(&mut self, entry: &FileEntry) -> Result<Vec<u8>, FilesystemError> {
let byte_off = entry.location * self.block_size as u64;
self.reader
.read_bytes(byte_off, entry.size as usize)
.map_err(|e| FilesystemError::Io(io_of(e)))
}
fn read_file_range(
&mut self,
entry: &FileEntry,
offset: u64,
length: usize,
) -> Result<Vec<u8>, FilesystemError> {
let clamped = length.min(entry.size.saturating_sub(offset) as usize);
let byte_off = entry.location * self.block_size as u64 + offset;
self.reader
.read_bytes(byte_off, clamped)
.map_err(|e| FilesystemError::Io(io_of(e)))
}
fn read_resource_fork(
&mut self,
_entry: &FileEntry,
) -> Result<Option<Vec<u8>>, FilesystemError> {
Ok(None)
}
fn read_resource_fork_range(
&mut self,
_entry: &FileEntry,
_offset: u64,
_length: usize,
) -> Result<Option<Vec<u8>>, FilesystemError> {
Ok(None)
}
fn volume_name(&self) -> Option<&str> {
if self.label.is_empty() {
None
} else {
Some(&self.label)
}
}
}
fn make_entry(name: String, path: String, block: u32, size: u32, is_dir: bool) -> FileEntry {
FileEntry {
name,
path,
entry_type: if is_dir {
EntryType::Directory
} else {
EntryType::File
},
size: size as u64,
location: block as u64,
children: None,
resource_fork_size: None,
type_code: None,
creator_code: None,
finder_flags: None,
symlink_target: None,
timestamps: None,
posix: None,
}
}
fn ascii_field(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).trim().to_string()
}
fn be32(b: &[u8], o: usize) -> u32 {
u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
}
fn io_of(e: crate::error::OpticaldiscsError) -> std::io::Error {
match e {
crate::error::OpticaldiscsError::Io(io) => io,
other => std::io::Error::other(other.to_string()),
}
}