1use core::fmt;
8
9use crate::crc::crc16;
10use crate::error::Error;
11
12pub const HEADER_LEN: usize = 56;
14pub const TRACK_HEADER_LEN: usize = 20;
16
17const MAGIC_ARCHIVE: &[u8] = b"DMS!";
18const MAGIC_TRACK: &[u8] = b"TR";
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Mode {
23 None,
25 Simple,
27 Quick,
29 Medium,
31 Deep,
33 Heavy1,
35 Heavy2,
37}
38
39impl TryFrom<u8> for Mode {
40 type Error = Error;
41
42 fn try_from(value: u8) -> Result<Self, Error> {
43 Ok(match value {
44 0 => Self::None,
45 1 => Self::Simple,
46 2 => Self::Quick,
47 3 => Self::Medium,
48 4 => Self::Deep,
49 5 => Self::Heavy1,
50 6 => Self::Heavy2,
51 other => return Err(Error::UnknownMode(other)),
52 })
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum DiskType {
59 Ofs,
61 Ffs,
63 OfsIntl,
65 FfsIntl,
67 OfsDirCache,
69 FfsDirCache,
71 Fms,
73 Unknown(u16),
75}
76
77impl From<u16> for DiskType {
78 fn from(value: u16) -> Self {
79 match value {
80 0 | 1 => Self::Ofs,
81 2 => Self::Ffs,
82 3 => Self::OfsIntl,
83 4 => Self::FfsIntl,
84 5 => Self::OfsDirCache,
85 6 => Self::FfsDirCache,
86 7 => Self::Fms,
87 other => Self::Unknown(other),
88 }
89 }
90}
91
92impl fmt::Display for DiskType {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.write_str(match self {
95 Self::Ofs => "AmigaOS 1.0 OFS",
96 Self::Ffs => "AmigaOS 2.0 FFS",
97 Self::OfsIntl => "AmigaOS 3.0 OFS / International",
98 Self::FfsIntl => "AmigaOS 3.0 FFS / International",
99 Self::OfsDirCache => "AmigaOS 3.0 OFS / Dir Cache",
100 Self::FfsDirCache => "AmigaOS 3.0 FFS / Dir Cache",
101 Self::Fms => "FMS Amiga System File",
102 Self::Unknown(_) => "Unknown",
103 })
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct GenInfo(pub u16);
110
111impl GenInfo {
112 pub const fn no_zero(self) -> bool {
114 self.0 & 0x01 != 0
115 }
116 pub const fn encrypted(self) -> bool {
118 self.0 & 0x02 != 0
119 }
120 pub const fn appends(self) -> bool {
122 self.0 & 0x04 != 0
123 }
124 pub const fn banner(self) -> bool {
126 self.0 & 0x08 != 0
127 }
128 pub const fn hd(self) -> bool {
130 self.0 & 0x10 != 0
131 }
132 pub const fn ms_dos(self) -> bool {
134 self.0 & 0x20 != 0
135 }
136 pub const fn dev_fixed(self) -> bool {
138 self.0 & 0x40 != 0
139 }
140 pub const fn registered(self) -> bool {
142 self.0 & 0x80 != 0
143 }
144 pub const fn file_id(self) -> bool {
146 self.0 & 0x0100 != 0
147 }
148}
149
150impl From<u16> for GenInfo {
151 fn from(value: u16) -> Self {
152 Self(value)
153 }
154}
155
156impl From<GenInfo> for u16 {
157 fn from(value: GenInfo) -> Self {
158 value.0
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub struct TrackFlags(pub u8);
165
166impl TrackFlags {
167 pub const fn keep_state(self) -> bool {
169 self.0 & 0x01 != 0
170 }
171 pub const fn heavy_rebuild_trees(self) -> bool {
173 self.0 & 0x02 != 0
174 }
175 pub const fn heavy_rle(self) -> bool {
177 self.0 & 0x04 != 0
178 }
179 pub const fn heavy_big_dict(self) -> bool {
181 self.0 & 0x08 != 0
182 }
183}
184
185impl From<u8> for TrackFlags {
186 fn from(value: u8) -> Self {
187 Self(value)
188 }
189}
190
191impl From<TrackFlags> for u8 {
192 fn from(value: TrackFlags) -> Self {
193 value.0
194 }
195}
196
197#[derive(Debug, Clone)]
199pub struct Info {
200 pub creator_version: u16,
202 pub date: u32,
204 pub first_track: u16,
206 pub last_track: u16,
208 pub packed_size: u32,
210 pub unpacked_size: u32,
212 pub disk_type: DiskType,
214 pub default_mode: Option<Mode>,
216 pub info: GenInfo,
218}
219
220impl TryFrom<&[u8]> for Info {
221 type Error = Error;
222
223 fn try_from(bytes: &[u8]) -> Result<Self, Error> {
224 if bytes.len() < HEADER_LEN {
225 return Err(Error::Truncated);
226 }
227 if &bytes[0..4] != MAGIC_ARCHIVE {
228 return Err(Error::NotDms);
229 }
230 let stored = be16(bytes, HEADER_LEN - 2);
231 if crc16(&bytes[4..HEADER_LEN - 2]) != stored {
233 return Err(Error::HeaderCrc);
234 }
235 let default_mode = u8::try_from(be16(bytes, 52))
236 .ok()
237 .and_then(|mode| Mode::try_from(mode).ok());
238 Ok(Self {
239 creator_version: be16(bytes, 46),
240 date: be32(bytes, 12),
241 first_track: be16(bytes, 16),
242 last_track: be16(bytes, 18),
243 packed_size: be24(bytes, 21),
245 unpacked_size: be24(bytes, 25),
246 disk_type: DiskType::from(be16(bytes, 50)),
247 default_mode,
248 info: GenInfo::from(be16(bytes, 10)),
249 })
250 }
251}
252
253impl fmt::Display for Info {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 let (major, minor) = (self.creator_version / 100, self.creator_version % 100);
256 writeln!(f, "DMS version {major}.{minor:02}")?;
257 writeln!(f, "tracks {} to {}", self.first_track, self.last_track)?;
258 writeln!(
259 f,
260 "packed {} bytes, unpacked {} bytes",
261 self.packed_size, self.unpacked_size
262 )?;
263 writeln!(f, "disk type: {}", self.disk_type)?;
264 match self.default_mode {
265 Some(mode) => writeln!(f, "default mode: {mode:?}")?,
266 None => writeln!(f, "default mode: unknown")?,
267 }
268 write!(
269 f,
270 "encrypted: {}, banner: {}, FILEID.DIZ: {}",
271 self.info.encrypted(),
272 self.info.banner(),
273 self.info.file_id()
274 )
275 }
276}
277
278#[derive(Debug, Clone, Copy)]
280pub struct TrackHeader {
281 pub number: u16,
283 pub packed_len: u16,
285 pub intermediate_len: u16,
287 pub unpacked_len: u16,
289 pub flags: TrackFlags,
291 pub mode: u8,
294 pub checksum: u16,
296 pub data_crc: u16,
298}
299
300impl TryFrom<&[u8]> for TrackHeader {
301 type Error = Error;
302
303 fn try_from(bytes: &[u8]) -> Result<Self, Error> {
304 if bytes.len() < TRACK_HEADER_LEN {
305 return Err(Error::Truncated);
306 }
307 if &bytes[0..2] != MAGIC_TRACK {
308 return Err(Error::NotTrack);
309 }
310 let stored = be16(bytes, TRACK_HEADER_LEN - 2);
311 if crc16(&bytes[0..TRACK_HEADER_LEN - 2]) != stored {
312 return Err(Error::TrackHeaderCrc);
313 }
314 Ok(Self {
315 number: be16(bytes, 2),
316 packed_len: be16(bytes, 6),
317 intermediate_len: be16(bytes, 8),
318 unpacked_len: be16(bytes, 10),
319 flags: TrackFlags::from(bytes[12]),
320 mode: bytes[13],
321 checksum: be16(bytes, 14),
322 data_crc: be16(bytes, 16),
323 })
324 }
325}
326
327fn be16(bytes: &[u8], at: usize) -> u16 {
328 u16::from_be_bytes([bytes[at], bytes[at + 1]])
329}
330
331fn be24(bytes: &[u8], at: usize) -> u32 {
332 (u32::from(bytes[at]) << 16) | (u32::from(bytes[at + 1]) << 8) | u32::from(bytes[at + 2])
333}
334
335fn be32(bytes: &[u8], at: usize) -> u32 {
336 u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
337}
338
339#[cfg(test)]
340mod tests {
341 use super::{DiskType, Info, Mode, TrackHeader};
342 use crate::crc::crc16;
343 use crate::error::Error;
344
345 fn archive_header() -> [u8; 56] {
346 let mut h = [0u8; 56];
347 h[0..4].copy_from_slice(b"DMS!");
348 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]);
358 h[54..56].copy_from_slice(&crc.to_be_bytes());
359 h
360 }
361
362 #[test]
363 fn parses_archive_header() {
364 let info = Info::try_from(&archive_header()[..]).unwrap();
365 assert_eq!(info.creator_version, 123);
366 assert_eq!(info.date, 0x1234_5678);
367 assert_eq!(info.first_track, 2);
368 assert_eq!(info.last_track, 83);
369 assert_eq!(info.packed_size, 0x0001_0203);
370 assert_eq!(info.unpacked_size, 901_120);
371 assert_eq!(info.disk_type, DiskType::FfsIntl);
372 assert_eq!(info.default_mode, Some(Mode::Heavy2));
373 assert!(info.info.encrypted());
374 assert!(info.info.file_id());
375 assert!(!info.info.banner());
376 }
377
378 #[test]
379 fn rejects_bad_magic() {
380 let mut h = archive_header();
381 h[0] = b'X';
382 assert!(matches!(Info::try_from(&h[..]), Err(Error::NotDms)));
383 }
384
385 #[test]
386 fn rejects_bad_header_crc() {
387 let mut h = archive_header();
388 h[55] ^= 0xff;
389 assert!(matches!(Info::try_from(&h[..]), Err(Error::HeaderCrc)));
390 }
391
392 #[test]
393 fn rejects_truncated_header() {
394 assert!(matches!(
395 Info::try_from(&[0u8; 10][..]),
396 Err(Error::Truncated)
397 ));
398 }
399
400 #[test]
401 fn unknown_disk_type_round_trips_value() {
402 assert_eq!(DiskType::from(42), DiskType::Unknown(42));
403 }
404
405 #[test]
406 fn info_display_is_human_readable() {
407 let info = Info::try_from(&archive_header()[..]).unwrap();
408 let text = alloc::format!("{info}");
409 assert!(text.contains("DMS version 1.23"));
410 assert!(text.contains("encrypted: true"));
411 }
412
413 #[test]
414 fn unknown_mode_is_error() {
415 assert!(matches!(Mode::try_from(9), Err(Error::UnknownMode(9))));
416 }
417
418 fn track_header() -> [u8; 20] {
419 let mut t = [0u8; 20];
420 t[0..2].copy_from_slice(b"TR");
421 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]);
430 t[18..20].copy_from_slice(&crc.to_be_bytes());
431 t
432 }
433
434 #[test]
435 fn parses_track_header() {
436 let th = TrackHeader::try_from(&track_header()[..]).unwrap();
437 assert_eq!(th.number, 5);
438 assert_eq!(th.packed_len, 100);
439 assert_eq!(th.intermediate_len, 200);
440 assert_eq!(th.unpacked_len, 5000);
441 assert_eq!(th.mode, 5);
442 assert_eq!(th.checksum, 0xABCD);
443 assert_eq!(th.data_crc, 0x1234);
444 assert!(th.flags.keep_state());
445 assert!(th.flags.heavy_rle());
446 assert!(!th.flags.heavy_big_dict());
447 }
448
449 #[test]
450 fn track_bad_magic_is_not_track() {
451 let mut t = track_header();
452 t[0] = b'Z';
453 assert!(matches!(
454 TrackHeader::try_from(&t[..]),
455 Err(Error::NotTrack)
456 ));
457 }
458
459 #[test]
460 fn track_bad_crc() {
461 let mut t = track_header();
462 t[19] ^= 0xff;
463 assert!(matches!(
464 TrackHeader::try_from(&t[..]),
465 Err(Error::TrackHeaderCrc)
466 ));
467 }
468}