#![doc = include_str!("../README.md")]
mod fork_reader;
mod lookup_table;
mod seek_over;
mod six_bit_decoder;
mod six_bit_rle_reader;
mod util;
use std::fs;
use std::io;
use std::io::{Read as _, Seek as _};
use std::path;
use binrw::{binread, BinReaderExt};
use crc::CRC_16_XMODEM;
use macintosh_utils::{FinderFlags, FourCC};
pub use fork_reader::ForkReader;
use seek_over::SeekOver;
use six_bit_rle_reader::SixBitRleReader;
use util::ReadByte;
use crate::six_bit_decoder::SixBitDecoder;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("The BinHex 4.0 header could not be located")]
HeaderNotFound,
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
BinRw(#[from] binrw::Error),
#[error("An unexpected character appeared within encoded data")]
InvalidCharacter,
#[error("Unexpected EOF")]
UnexpectedEof,
#[error("Seeking backwards or from the end is not supported at the moment")]
UnsupportedSeek,
}
impl From<Error> for io::Error {
fn from(val: Error) -> Self {
match val {
Error::Io(io) => io,
other => io::Error::other(Box::new(other)),
}
}
}
const VERIFICATION_CHUNK_SIZE: usize = 1 << 20 ;
#[derive(Debug)]
pub enum Checksum {
Header,
DataFork,
ResourceFork,
}
#[derive(Debug, thiserror::Error)]
pub enum VerificationError {
#[error("A checksum did not match")]
ChecksumMismatch(Checksum),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
BinRw(#[from] binrw::Error),
}
#[binread]
#[derive(Debug, Clone)]
#[br(big)]
pub struct ArchiveHeader {
#[br(map(macintosh_utils::string))]
pub name: String,
#[br(temp)]
_name_terminator: u8,
pub file_code: FourCC,
pub creator_code: FourCC,
pub finder_flags: FinderFlags,
pub data_len: u32,
pub resource_len: u32,
pub header_checksum: u16,
}
impl ArchiveHeader {
pub const FIXED_SIZE: usize = 22;
}
pub struct Archive<R> {
header: ArchiveHeader,
reader: SixBitRleReader<R>,
data_fork_start: u64,
}
impl<R> Archive<R> {
pub fn name(&self) -> &str {
self.header.name.as_str()
}
pub fn file_code(&self) -> FourCC {
self.header.file_code
}
pub fn creator_code(&self) -> FourCC {
self.header.creator_code
}
pub fn finder_flags(&self) -> FinderFlags {
self.header.finder_flags
}
pub fn data_len(&self) -> usize {
self.header.data_len as usize
}
pub fn resource_len(&self) -> usize {
self.header.resource_len as usize
}
pub fn header_checksum(&self) -> u16 {
self.header.header_checksum
}
}
impl<R: io::Read + io::Seek> Archive<R> {
pub const HEADER: &[u8; 45] = b"(This file must be converted with BinHex 4.0)";
pub fn try_from(mut inner: R) -> Result<Self, Error> {
inner
.seek_over_string(Self::HEADER)
.map_err(|_| Error::HeaderNotFound)?;
let data_start = loop {
if inner.read_byte()? == b':' {
break inner.stream_position()?;
}
};
let mut inner = SixBitRleReader::new(inner, data_start);
let header: ArchiveHeader = inner.read_be()?;
let data_fork_start = inner.stream_position()?;
Ok(Self {
reader: inner,
header,
data_fork_start,
})
}
pub fn data_fork(&mut self) -> Result<ForkReader<&mut SixBitRleReader<R>>, Error> {
self.reset()?;
self.reader
.seek(io::SeekFrom::Start(self.data_fork_start))?;
let length = self.data_len() as u64;
Ok(ForkReader::new(
&mut self.reader,
self.data_fork_start,
length,
))
}
pub fn resource_fork(&mut self) -> Result<ForkReader<&mut SixBitRleReader<R>>, Error> {
self.reset()?;
self.reader.seek(io::SeekFrom::Current(
self.data_fork_start as i64 + self.header.data_len as i64 + 2,
))?;
let position = self.reader.position;
let length = self.resource_len() as u64;
Ok(ForkReader::new(&mut self.reader, position, length))
}
pub fn verify(&mut self) -> Result<(), VerificationError> {
self.reset()?;
let crc = crc::Crc::<u16>::new(&CRC_16_XMODEM);
let mut buf = vec![0u8; ArchiveHeader::FIXED_SIZE];
self.reader.read_exact(&mut buf)?;
let name_length = buf[0] as usize;
buf.append(&mut vec![0u8; name_length]);
self.reader.read_exact(
&mut buf[(ArchiveHeader::FIXED_SIZE)..(ArchiveHeader::FIXED_SIZE + name_length)],
)?;
let mut digest = crc.digest();
digest.update(&buf);
if digest.finalize() != 0 {
return Err(VerificationError::ChecksumMismatch(Checksum::Header));
}
let mut chunk = [0u8; VERIFICATION_CHUNK_SIZE];
let mut digest = crc.digest();
let data_len_offset = name_length + 12;
let data_len = u32::from_be_bytes([
buf[data_len_offset],
buf[data_len_offset + 1],
buf[data_len_offset + 2],
buf[data_len_offset + 3],
]) as usize
+ 2;
for _ in 0..(data_len / VERIFICATION_CHUNK_SIZE) {
self.reader.read_exact(&mut chunk)?;
digest.update(&chunk);
}
let rest = data_len % VERIFICATION_CHUNK_SIZE;
self.reader.read_exact(&mut chunk[0..rest])?;
digest.update(&chunk[0..rest]);
if digest.finalize() != 0 {
return Err(VerificationError::ChecksumMismatch(Checksum::DataFork));
}
let mut digest = crc.digest();
let resource_len_offset = name_length + 16;
let resource_len = u32::from_be_bytes([
buf[resource_len_offset],
buf[resource_len_offset + 1],
buf[resource_len_offset + 2],
buf[resource_len_offset + 3],
]) as usize
+ 2;
for _ in 0..(resource_len / VERIFICATION_CHUNK_SIZE) {
self.reader.read_exact(&mut chunk)?;
digest.update(&chunk);
}
let rest = resource_len % VERIFICATION_CHUNK_SIZE;
self.reader.read_exact(&mut chunk[0..rest])?;
digest.update(&chunk[0..rest]);
if digest.finalize() != 0 {
return Err(VerificationError::ChecksumMismatch(Checksum::ResourceFork));
}
Ok(())
}
pub fn into_inner(self) -> R {
self.reader.into_inner()
}
fn reset(&mut self) -> io::Result<()> {
self.reader.reset()
}
}
impl Archive<fs::File> {
pub fn open<P: AsRef<path::Path>>(path: P) -> Result<Self, Error> {
Self::try_from(fs::File::open(path)?)
}
pub fn try_clone(&self) -> io::Result<Self> {
Ok(Self {
header: self.header.clone(),
reader: self.reader.try_clone()?,
data_fork_start: self.data_fork_start,
})
}
}
impl<R: Clone> Clone for Archive<R> {
fn clone(&self) -> Self {
Self {
reader: self.reader.clone(),
header: self.header.clone(),
data_fork_start: self.data_fork_start,
}
}
}
pub fn probe<R: io::Read + io::Seek>(reader: R) -> bool {
Archive::try_from(reader).is_ok()
}
#[cfg(test)]
mod test {
use std::fs;
use std::io;
use std::io::Read as _;
use std::io::Seek;
use macintosh_utils::fourcc;
use super::Archive;
use crate::{Checksum, VerificationError};
#[test]
fn decode() {
let file = fs::File::open("./sample-file.hqx").unwrap();
let archive = Archive::try_from(file).unwrap();
assert_eq!(archive.name(), "binhex.test.sit");
assert_eq!(archive.file_code(), fourcc!("SITD"));
assert_eq!(archive.creator_code(), fourcc!("SIT!"));
assert_eq!(archive.data_len(), 380);
assert_eq!(archive.resource_len(), 0);
assert_eq!(archive.header_checksum(), 0x6e3c);
}
#[test]
fn successfully_verify() {
let mut reader = Archive::open("sample-file.hqx").unwrap();
assert!(reader.verify().is_ok());
}
#[test]
fn failed_header_verification() {
let mut file = fs::File::open("./sample-file.hqx").unwrap();
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).unwrap();
buffer[137 + 3] |= 1 << 3;
let reader = io::Cursor::new(buffer);
let mut reader = Archive::try_from(reader).unwrap();
let result = reader.verify();
assert!(matches!(
result,
Err(VerificationError::ChecksumMismatch(Checksum::Header))
));
}
#[test]
fn failed_data_verification() {
let mut file = fs::File::open("./sample-file.hqx").unwrap();
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).unwrap();
buffer[240 + 3] |= 1 << 3;
let reader = io::Cursor::new(buffer);
let mut reader = Archive::try_from(reader).unwrap();
let result = reader.verify();
assert!(matches!(
result,
Err(VerificationError::ChecksumMismatch(Checksum::DataFork))
));
}
#[test]
fn reading_data_fork_multiple_times() {
let mut archive = Archive::open("sample.txt.hqx").unwrap();
assert_eq!(archive.name(), "Sample.txt");
let mut data_fork = archive.data_fork().unwrap();
let mut first_buffer = Vec::new();
data_fork.read_to_end(&mut first_buffer).unwrap();
let mut data_fork = archive.data_fork().unwrap();
let mut second_buffer = Vec::new();
data_fork.read_to_end(&mut second_buffer).unwrap();
assert_eq!(first_buffer, second_buffer);
}
#[test]
fn reading_resource_fork_multiple_times() {
let mut archive = Archive::open("sample.txt.hqx").unwrap();
assert_eq!(archive.name(), "Sample.txt");
let mut rsrc_fork = archive.resource_fork().unwrap();
let mut first_buffer = Vec::new();
rsrc_fork.read_to_end(&mut first_buffer).unwrap();
let mut rsrc_fork = archive.resource_fork().unwrap();
let mut second_buffer = Vec::new();
rsrc_fork.read_to_end(&mut second_buffer).unwrap();
assert_eq!(first_buffer, second_buffer);
}
#[test]
fn seeking_in_data_fork() {
let mut archive = Archive::open("sample.txt.hqx").unwrap();
let mut data_fork = archive.data_fork().unwrap();
let mut hello = vec![0u8; 5];
data_fork.read_exact(&mut hello).unwrap();
data_fork.seek(io::SeekFrom::Current(2)).unwrap();
let mut world = vec![0u8; 5];
data_fork.read_exact(&mut world).unwrap();
assert_eq!(hello, b"Hello");
assert_eq!(world, b"World");
data_fork.seek(io::SeekFrom::Start(5 + 2 + 5)).unwrap();
let mut rest = Vec::new();
data_fork.read_to_end(&mut rest).unwrap();
assert_eq!(rest, b"!");
}
#[test]
fn seeking_in_resource_fork() {
let mut archive = Archive::open("sample.txt.hqx").unwrap();
let mut data_fork = archive.resource_fork().unwrap();
let mut chunk = vec![0u8; 4];
data_fork.read_exact(&mut chunk).unwrap();
assert_eq!(chunk, b"\x00\x00\x01\x00");
data_fork.seek(io::SeekFrom::Start(4)).unwrap();
data_fork.read_exact(&mut chunk).unwrap();
assert_eq!(chunk, b"\x00\x00\x01\x1a");
data_fork.seek(io::SeekFrom::Start(0x138)).unwrap();
data_fork.read_exact(&mut chunk).unwrap();
assert_eq!(chunk, b"styl");
}
}