Skip to main content

cpt/
structs.rs

1use binrw::{binread, BinRead, BinReaderExt};
2use bitflags::bitflags;
3use macintosh_utils::{chrono, decode_string, FinderFlags, Fork, FourCC};
4
5use crate::entry_reader::{CompressionMethod, EntrySpecification};
6
7#[derive(BinRead, Debug)]
8#[br(big)]
9pub struct ArchiveHeader {
10    /// File identifier, 0x01
11    pub magic: u8,
12    /// Volume number (meaning not entirely clear, is 0x01 for single-volume archives)
13    pub volume: u8,
14    /// Cross-volume magic number (meaning not entirely clear)
15    pub cross_volume_magic: u16,
16    /// Offset to file headers from beginning of file
17    pub header_offset: u32,
18}
19
20impl ArchiveHeader {
21    /// Total size of archive header on the wire
22    pub const PACKED_SIZE: usize = 8;
23}
24
25#[binread]
26#[derive(Debug)]
27#[br(big)]
28pub struct CatalogHeader {
29    /// CRC-32 of the header
30    pub header_checksum: u32,
31    /// Total number of files and directories
32    pub entry_count: u16,
33    /// Archive comment
34    #[br(map(macintosh_utils::string))]
35    pub comment: String,
36}
37
38#[derive(Debug, Clone)]
39pub enum Entry {
40    File(File),
41    Directory(Directory),
42}
43
44impl Entry {
45    pub fn name(&self) -> &str {
46        match self {
47            Entry::File(file_header) => &file_header.name,
48            Entry::Directory(dir_header) => &dir_header.name,
49        }
50    }
51
52    pub fn is_file(&self) -> bool {
53        matches!(self, Entry::File(_))
54    }
55
56    pub fn is_directory(&self) -> bool {
57        matches!(self, Entry::Directory(_))
58    }
59
60    pub fn as_file(&self) -> Option<&File> {
61        match self {
62            Entry::File(file) => Some(file),
63            Entry::Directory(_) => None,
64        }
65    }
66
67    pub fn as_directory(&self) -> Option<&Directory> {
68        match self {
69            Entry::File(_) => None,
70            Entry::Directory(directory) => Some(directory),
71        }
72    }
73
74    pub(crate) fn spec(&self, fork: Fork) -> EntrySpecification {
75        match self {
76            Entry::File(file) => file.spec(fork),
77            Entry::Directory(_) => EntrySpecification::default(),
78        }
79    }
80}
81
82impl BinRead for Entry {
83    type Args<'a> = ();
84
85    fn read_options<R: std::io::Read + std::io::Seek>(
86        reader: &mut R,
87        _: binrw::Endian,
88        _: Self::Args<'_>,
89    ) -> binrw::BinResult<Self> {
90        let name_len_and_type: u8 = reader.read_be()?;
91        let is_directory = (name_len_and_type & 0x80) != 0;
92        let name_len = name_len_and_type & !0x80;
93
94        if is_directory {
95            Ok(Entry::Directory(reader.read_be_args((name_len,))?))
96        } else {
97            Ok(Entry::File(reader.read_be_args((name_len,))?))
98        }
99    }
100}
101
102#[derive(BinRead, Debug, Clone)]
103#[br(import(name_len: u8), big)]
104pub struct File {
105    #[br(count(name_len), map(decode_string))]
106    pub name: String,
107    /// Volume number (meaning not entirely clear)
108    pub volume: u8,
109    /// Offset to file data from beginning of file
110    pub offset: u32,
111    /// Mac OS file type
112    pub file_code: FourCC,
113    /// Mac OS file creator
114    pub creator_code: FourCC,
115    /// Creation date in classic Mac OS format (seconds since 1904)
116    #[br(map(macintosh_utils::date))]
117    pub created_at: chrono::DateTime<chrono::Utc>,
118    /// Modification date in classic Mac OS format (seconds since 1904)
119    #[br(map(macintosh_utils::date))]
120    pub modified_at: chrono::DateTime<chrono::Utc>,
121    /// Mac OS Finder flags
122    pub finder_flags: FinderFlags,
123    /// Uncompressed file data CRC. This is calculated for the concatenation of the resource and data forks.
124    pub crc32: u32,
125    /// File flags
126    #[br(map(|v: u16| Flags::from_bits_retain(v)))]
127    pub flags: Flags,
128    /// Resource fork uncompressed length
129    pub rsrc_uncompressed_size: u32,
130    /// Data fork uncompressed length
131    pub data_uncompressed_size: u32,
132    /// Resource fork compressed length
133    pub rsrc_compressed_size: u32,
134    /// Data fork compressed length
135    pub data_compressed_size: u32,
136}
137
138impl File {
139    pub(crate) fn spec(&self, fork: Fork) -> EntrySpecification {
140        let method = if fork.is_data() {
141            if self.flags.contains(Flags::DATA_LZH_COMPRESSED) {
142                CompressionMethod::Lzh
143            } else {
144                CompressionMethod::Rle
145            }
146        } else if self.flags.contains(Flags::RSRC_LZH_COMPRESSED) {
147            CompressionMethod::Lzh
148        } else {
149            CompressionMethod::Rle
150        };
151
152        match fork {
153            Fork::Data => EntrySpecification {
154                method,
155                uncompressed_len: self.data_uncompressed_size as usize,
156                compressed_len: self.data_compressed_size as usize,
157                offset: self.offset as u64 + self.rsrc_compressed_size as u64,
158            },
159            Fork::Resource => EntrySpecification {
160                method,
161                uncompressed_len: self.rsrc_uncompressed_size as usize,
162                compressed_len: self.rsrc_compressed_size as usize,
163                offset: self.offset as u64,
164            },
165        }
166    }
167}
168
169bitflags! {
170    #[derive(Debug, Clone)]
171    pub struct Flags: u16 {
172        const ENCRYPTED = 1<<0;
173        const RSRC_LZH_COMPRESSED = 1<<1;
174        const DATA_LZH_COMPRESSED = 1<<2;
175    }
176}
177
178#[derive(BinRead, Debug, Clone)]
179#[br(import(name_len: u8), big)]
180pub struct Directory {
181    #[br(count(name_len), map(decode_string))]
182    pub name: String,
183    pub entries: u16,
184}