use core::fmt;
use crate::crc::crc16;
use crate::error::Error;
pub const HEADER_LEN: usize = 56;
pub const TRACK_HEADER_LEN: usize = 20;
const MAGIC_ARCHIVE: &[u8] = b"DMS!";
const MAGIC_TRACK: &[u8] = b"TR";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
None,
Simple,
Quick,
Medium,
Deep,
Heavy1,
Heavy2,
}
impl TryFrom<u8> for Mode {
type Error = Error;
fn try_from(value: u8) -> Result<Self, Error> {
Ok(match value {
0 => Self::None,
1 => Self::Simple,
2 => Self::Quick,
3 => Self::Medium,
4 => Self::Deep,
5 => Self::Heavy1,
6 => Self::Heavy2,
other => return Err(Error::UnknownMode(other)),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiskType {
Ofs,
Ffs,
OfsIntl,
FfsIntl,
OfsDirCache,
FfsDirCache,
Fms,
Unknown(u16),
}
impl From<u16> for DiskType {
fn from(value: u16) -> Self {
match value {
0 | 1 => Self::Ofs,
2 => Self::Ffs,
3 => Self::OfsIntl,
4 => Self::FfsIntl,
5 => Self::OfsDirCache,
6 => Self::FfsDirCache,
7 => Self::Fms,
other => Self::Unknown(other),
}
}
}
impl fmt::Display for DiskType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Ofs => "AmigaOS 1.0 OFS",
Self::Ffs => "AmigaOS 2.0 FFS",
Self::OfsIntl => "AmigaOS 3.0 OFS / International",
Self::FfsIntl => "AmigaOS 3.0 FFS / International",
Self::OfsDirCache => "AmigaOS 3.0 OFS / Dir Cache",
Self::FfsDirCache => "AmigaOS 3.0 FFS / Dir Cache",
Self::Fms => "FMS Amiga System File",
Self::Unknown(_) => "Unknown",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GenInfo(pub u16);
impl GenInfo {
pub const fn no_zero(self) -> bool {
self.0 & 0x01 != 0
}
pub const fn encrypted(self) -> bool {
self.0 & 0x02 != 0
}
pub const fn appends(self) -> bool {
self.0 & 0x04 != 0
}
pub const fn banner(self) -> bool {
self.0 & 0x08 != 0
}
pub const fn hd(self) -> bool {
self.0 & 0x10 != 0
}
pub const fn ms_dos(self) -> bool {
self.0 & 0x20 != 0
}
pub const fn dev_fixed(self) -> bool {
self.0 & 0x40 != 0
}
pub const fn registered(self) -> bool {
self.0 & 0x80 != 0
}
pub const fn file_id(self) -> bool {
self.0 & 0x0100 != 0
}
}
impl From<u16> for GenInfo {
fn from(value: u16) -> Self {
Self(value)
}
}
impl From<GenInfo> for u16 {
fn from(value: GenInfo) -> Self {
value.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TrackFlags(pub u8);
impl TrackFlags {
pub const fn keep_state(self) -> bool {
self.0 & 0x01 != 0
}
pub const fn heavy_rebuild_trees(self) -> bool {
self.0 & 0x02 != 0
}
pub const fn heavy_rle(self) -> bool {
self.0 & 0x04 != 0
}
pub const fn heavy_big_dict(self) -> bool {
self.0 & 0x08 != 0
}
}
impl From<u8> for TrackFlags {
fn from(value: u8) -> Self {
Self(value)
}
}
impl From<TrackFlags> for u8 {
fn from(value: TrackFlags) -> Self {
value.0
}
}
#[derive(Debug, Clone)]
pub struct Info {
pub creator_version: u16,
pub date: u32,
pub first_track: u16,
pub last_track: u16,
pub packed_size: u32,
pub unpacked_size: u32,
pub disk_type: DiskType,
pub default_mode: Option<Mode>,
pub info: GenInfo,
}
impl TryFrom<&[u8]> for Info {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < HEADER_LEN {
return Err(Error::Truncated);
}
if &bytes[0..4] != MAGIC_ARCHIVE {
return Err(Error::NotDms);
}
let stored = be16(bytes, HEADER_LEN - 2);
if crc16(&bytes[4..HEADER_LEN - 2]) != stored {
return Err(Error::HeaderCrc);
}
let default_mode = u8::try_from(be16(bytes, 52))
.ok()
.and_then(|mode| Mode::try_from(mode).ok());
Ok(Self {
creator_version: be16(bytes, 46),
date: be32(bytes, 12),
first_track: be16(bytes, 16),
last_track: be16(bytes, 18),
packed_size: be24(bytes, 21),
unpacked_size: be24(bytes, 25),
disk_type: DiskType::from(be16(bytes, 50)),
default_mode,
info: GenInfo::from(be16(bytes, 10)),
})
}
}
impl fmt::Display for Info {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (major, minor) = (self.creator_version / 100, self.creator_version % 100);
writeln!(f, "DMS version {major}.{minor:02}")?;
writeln!(f, "tracks {} to {}", self.first_track, self.last_track)?;
writeln!(
f,
"packed {} bytes, unpacked {} bytes",
self.packed_size, self.unpacked_size
)?;
writeln!(f, "disk type: {}", self.disk_type)?;
match self.default_mode {
Some(mode) => writeln!(f, "default mode: {mode:?}")?,
None => writeln!(f, "default mode: unknown")?,
}
write!(
f,
"encrypted: {}, banner: {}, FILEID.DIZ: {}",
self.info.encrypted(),
self.info.banner(),
self.info.file_id()
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct TrackHeader {
pub number: u16,
pub packed_len: u16,
pub intermediate_len: u16,
pub unpacked_len: u16,
pub flags: TrackFlags,
pub mode: u8,
pub checksum: u16,
pub data_crc: u16,
}
impl TryFrom<&[u8]> for TrackHeader {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < TRACK_HEADER_LEN {
return Err(Error::Truncated);
}
if &bytes[0..2] != MAGIC_TRACK {
return Err(Error::NotTrack);
}
let stored = be16(bytes, TRACK_HEADER_LEN - 2);
if crc16(&bytes[0..TRACK_HEADER_LEN - 2]) != stored {
return Err(Error::TrackHeaderCrc);
}
Ok(Self {
number: be16(bytes, 2),
packed_len: be16(bytes, 6),
intermediate_len: be16(bytes, 8),
unpacked_len: be16(bytes, 10),
flags: TrackFlags::from(bytes[12]),
mode: bytes[13],
checksum: be16(bytes, 14),
data_crc: be16(bytes, 16),
})
}
}
fn be16(bytes: &[u8], at: usize) -> u16 {
u16::from_be_bytes([bytes[at], bytes[at + 1]])
}
fn be24(bytes: &[u8], at: usize) -> u32 {
(u32::from(bytes[at]) << 16) | (u32::from(bytes[at + 1]) << 8) | u32::from(bytes[at + 2])
}
fn be32(bytes: &[u8], at: usize) -> u32 {
u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
}
#[cfg(test)]
mod tests {
use super::{DiskType, Info, Mode, TrackHeader};
use crate::crc::crc16;
use crate::error::Error;
fn archive_header() -> [u8; 56] {
let mut h = [0u8; 56];
h[0..4].copy_from_slice(b"DMS!");
h[10..12].copy_from_slice(&0x0102u16.to_be_bytes()); h[12..16].copy_from_slice(&0x1234_5678u32.to_be_bytes()); h[16..18].copy_from_slice(&2u16.to_be_bytes()); h[18..20].copy_from_slice(&83u16.to_be_bytes()); h[21..24].copy_from_slice(&[0x01, 0x02, 0x03]); h[25..28].copy_from_slice(&[0x0D, 0xC0, 0x00]); h[46..48].copy_from_slice(&123u16.to_be_bytes()); h[50..52].copy_from_slice(&4u16.to_be_bytes()); h[52..54].copy_from_slice(&6u16.to_be_bytes()); let crc = crc16(&h[4..54]);
h[54..56].copy_from_slice(&crc.to_be_bytes());
h
}
#[test]
fn parses_archive_header() {
let info = Info::try_from(&archive_header()[..]).unwrap();
assert_eq!(info.creator_version, 123);
assert_eq!(info.date, 0x1234_5678);
assert_eq!(info.first_track, 2);
assert_eq!(info.last_track, 83);
assert_eq!(info.packed_size, 0x0001_0203);
assert_eq!(info.unpacked_size, 901_120);
assert_eq!(info.disk_type, DiskType::FfsIntl);
assert_eq!(info.default_mode, Some(Mode::Heavy2));
assert!(info.info.encrypted());
assert!(info.info.file_id());
assert!(!info.info.banner());
}
#[test]
fn rejects_bad_magic() {
let mut h = archive_header();
h[0] = b'X';
assert!(matches!(Info::try_from(&h[..]), Err(Error::NotDms)));
}
#[test]
fn rejects_bad_header_crc() {
let mut h = archive_header();
h[55] ^= 0xff;
assert!(matches!(Info::try_from(&h[..]), Err(Error::HeaderCrc)));
}
#[test]
fn rejects_truncated_header() {
assert!(matches!(
Info::try_from(&[0u8; 10][..]),
Err(Error::Truncated)
));
}
#[test]
fn unknown_disk_type_round_trips_value() {
assert_eq!(DiskType::from(42), DiskType::Unknown(42));
}
#[test]
fn info_display_is_human_readable() {
let info = Info::try_from(&archive_header()[..]).unwrap();
let text = alloc::format!("{info}");
assert!(text.contains("DMS version 1.23"));
assert!(text.contains("encrypted: true"));
}
#[test]
fn unknown_mode_is_error() {
assert!(matches!(Mode::try_from(9), Err(Error::UnknownMode(9))));
}
fn track_header() -> [u8; 20] {
let mut t = [0u8; 20];
t[0..2].copy_from_slice(b"TR");
t[2..4].copy_from_slice(&5u16.to_be_bytes()); t[6..8].copy_from_slice(&100u16.to_be_bytes()); t[8..10].copy_from_slice(&200u16.to_be_bytes()); t[10..12].copy_from_slice(&5000u16.to_be_bytes()); t[12] = 0x05; t[13] = 5; t[14..16].copy_from_slice(&0xABCDu16.to_be_bytes()); t[16..18].copy_from_slice(&0x1234u16.to_be_bytes()); let crc = crc16(&t[0..18]);
t[18..20].copy_from_slice(&crc.to_be_bytes());
t
}
#[test]
fn parses_track_header() {
let th = TrackHeader::try_from(&track_header()[..]).unwrap();
assert_eq!(th.number, 5);
assert_eq!(th.packed_len, 100);
assert_eq!(th.intermediate_len, 200);
assert_eq!(th.unpacked_len, 5000);
assert_eq!(th.mode, 5);
assert_eq!(th.checksum, 0xABCD);
assert_eq!(th.data_crc, 0x1234);
assert!(th.flags.keep_state());
assert!(th.flags.heavy_rle());
assert!(!th.flags.heavy_big_dict());
}
#[test]
fn track_bad_magic_is_not_track() {
let mut t = track_header();
t[0] = b'Z';
assert!(matches!(
TrackHeader::try_from(&t[..]),
Err(Error::NotTrack)
));
}
#[test]
fn track_bad_crc() {
let mut t = track_header();
t[19] ^= 0xff;
assert!(matches!(
TrackHeader::try_from(&t[..]),
Err(Error::TrackHeaderCrc)
));
}
}