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