use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::Path;
use crate::error::{AlzError, AlzResult};
trait ReadSeek: Read + Seek {}
impl<T: Read + Seek> ReadSeek for T {}
const MAX_VOLUMES: usize = 1000;
const VOLUME_HEADER_SIZE: u64 = 8;
const VOLUME_TRAILER_SIZE: u64 = 16;
struct Volume {
file: Box<dyn ReadSeek>,
file_size: u64,
header_size: u64,
tail_size: u64,
}
impl Volume {
fn data_size(&self) -> u64 {
self.file_size
.saturating_sub(self.header_size)
.saturating_sub(self.tail_size)
}
}
pub struct MultiVolumeReader {
volumes: Vec<Volume>,
cur_volume: usize,
virtual_pos: u64,
tail: [u8; 16],
}
impl MultiVolumeReader {
pub fn open<P: AsRef<Path>>(path: P) -> AlzResult<Self> {
let path = path.as_ref();
let path_str = path.to_string_lossy().to_string();
let cut = path_str.len().wrapping_sub(3);
let multivol_prefix = if path_str.len() >= 4 && path_str.is_char_boundary(cut) {
Some(&path_str[..cut])
} else {
None
};
let mut volumes = Vec::new();
for i in 0..MAX_VOLUMES {
let vol_path = if i == 0 {
path_str.clone()
} else {
match multivol_prefix {
Some(prefix) => {
let letter = (b'a' + ((i - 1) / 100) as u8) as char;
let num = (i - 1) % 100;
format!("{prefix}{letter}{num:02}")
}
None => break,
}
};
let file = match File::open(&vol_path) {
Ok(f) => f,
Err(_) => break,
};
let file_size = file.metadata()?.len();
let header_size = if i == 0 { 0 } else { VOLUME_HEADER_SIZE };
let tail_size = VOLUME_TRAILER_SIZE;
volumes.push(Volume {
file: Box::new(file),
file_size,
header_size,
tail_size,
});
}
if volumes.is_empty() {
return Err(AlzError::CantOpenFile(io::Error::new(
io::ErrorKind::NotFound,
format!("can't open: {path_str}"),
)));
}
if let Some(last) = volumes.last_mut() {
last.tail_size = 0;
}
for vol in &volumes {
if vol.file_size < vol.header_size + vol.tail_size {
return Err(AlzError::CorruptedFile);
}
}
let mut tail = [0u8; 16];
let vol0 = &mut volumes[0];
if vol0.file_size >= 16 {
vol0.file.seek(SeekFrom::Start(vol0.file_size - 16))?;
vol0.file.read_exact(&mut tail)?;
}
let mut reader = MultiVolumeReader {
volumes,
cur_volume: 0,
virtual_pos: 0,
tail,
};
reader.seek_to_virtual(0)?;
Ok(reader)
}
pub fn from_bytes(data: Vec<u8>) -> Self {
let len = data.len() as u64;
let mut tail = [0u8; 16];
if data.len() >= 16 {
tail.copy_from_slice(&data[data.len() - 16..]);
}
MultiVolumeReader {
volumes: vec![Volume {
file: Box::new(io::Cursor::new(data)),
file_size: len,
header_size: 0,
tail_size: 0,
}],
cur_volume: 0,
virtual_pos: 0,
tail,
}
}
pub fn tail(&self) -> &[u8; 16] {
&self.tail
}
pub fn total_size(&self) -> u64 {
self.volumes.iter().map(|v| v.data_size()).sum()
}
fn seek_to_virtual(&mut self, offset: u64) -> AlzResult<()> {
self.virtual_pos = offset;
let mut remain = offset;
for (i, vol) in self.volumes.iter_mut().enumerate() {
let data_size = vol.data_size();
if remain <= data_size {
let phys_pos = remain + vol.header_size;
vol.file.seek(SeekFrom::Start(phys_pos))?;
self.cur_volume = i;
return Ok(());
}
remain -= data_size;
}
let last = self.volumes.len() - 1;
let vol = &mut self.volumes[last];
let end = vol.file_size.saturating_sub(vol.tail_size);
vol.file.seek(SeekFrom::Start(end))?;
self.cur_volume = last;
Ok(())
}
}
impl Read for MultiVolumeReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() || self.cur_volume >= self.volumes.len() {
return Ok(0);
}
let mut total_read = 0;
while total_read < buf.len() && self.cur_volume < self.volumes.len() {
let vol = &mut self.volumes[self.cur_volume];
let phys_pos = vol.file.stream_position()?;
let data_end = vol.file_size.saturating_sub(vol.tail_size);
let avail = data_end.saturating_sub(phys_pos) as usize;
if avail == 0 {
self.cur_volume += 1;
if self.cur_volume >= self.volumes.len() {
break;
}
let next_vol = &mut self.volumes[self.cur_volume];
next_vol.file.seek(SeekFrom::Start(next_vol.header_size))?;
continue;
}
let to_read = avail.min(buf.len() - total_read);
let n = vol.file.read(&mut buf[total_read..total_read + to_read])?;
if n == 0 {
break;
}
total_read += n;
self.virtual_pos += n as u64;
}
Ok(total_read)
}
}
impl Seek for MultiVolumeReader {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let out_of_range =
|| io::Error::new(io::ErrorKind::InvalidInput, "seek position out of range");
let new_pos = match pos {
SeekFrom::Start(p) => p,
SeekFrom::Current(delta) => self
.virtual_pos
.checked_add_signed(delta)
.ok_or_else(out_of_range)?,
SeekFrom::End(delta) => self
.total_size()
.checked_add_signed(delta)
.ok_or_else(out_of_range)?,
};
self.seek_to_virtual(new_pos)
.map_err(|e| io::Error::other(e.to_string()))?;
Ok(self.virtual_pos)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn truncated_volume_errors_instead_of_panicking() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("unalz_mv_test_{nanos}"));
std::fs::create_dir_all(&dir).unwrap();
let alz = dir.join("x.alz");
std::fs::write(&alz, b"ALZ\x01").unwrap(); std::fs::write(dir.join("x.a00"), [0u8; 4]).unwrap();
let result = MultiVolumeReader::open(&alz);
assert!(matches!(result, Err(AlzError::CorruptedFile)));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn non_char_boundary_name_does_not_panic() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("unalz_mv_boundary_{nanos}"));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("\u{0440}\u{0445}");
std::fs::write(&path, [0u8; 32]).unwrap();
let _ = MultiVolumeReader::open(&path);
let _ = std::fs::remove_dir_all(&dir);
}
}