1use std::{
2 io::{Cursor, Seek, SeekFrom, Write},
3 path::{Path, StripPrefixError},
4 string::FromUtf8Error,
5 time::SystemTime,
6};
7use thiserror::Error;
8
9pub const HEADER_SIZE: usize = 4377;
10const FILENAME: usize = 255;
11const SIZE_BEGIN: usize = FILENAME;
12const SIZE: usize = 14;
13const MTIME: usize = 12;
14const PREFIX: usize = 4096;
15const SIZE_END: usize = FILENAME + SIZE;
16const MTIME_BEGIN: usize = SIZE_END;
17const MTIME_END: usize = SIZE_END + MTIME;
18const PREFIX_BEGIN: usize = MTIME_END;
19pub const EOF_BLOCK: &[u8] = &[0; HEADER_SIZE];
20
21#[derive(Error, Debug)]
22pub enum FileParseError {
23 #[error("failed to read file metadata")]
24 Metadata,
25 #[error("failed to read file name")]
26 EmptyName,
27 #[error("failed to read last modified time for file")]
28 ReadLastModified,
29 #[error("failed to cast last modified date in terms of unix epoch for file")]
30 UnixEpoch,
31 #[error("{0}")]
32 Length(#[from] LengthExceededError),
33 #[error("{0}")]
34 Header(#[from] HeaderError),
35 #[error("failed reading from file: {0}")]
36 FileRead(#[from] std::io::Error),
37}
38
39#[derive(Error, Debug)]
40pub enum ExtractError {
41 #[error("failed to strip path prefix and sanitize it: {0}")]
42 PathSanitization(#[from] StripPrefixError),
43 #[error("failed writing to file: {0}")]
44 FileRead(#[from] std::io::Error),
45}
46
47#[derive(Debug, Error)]
48pub enum ArchiveError {
49 #[error("failed to create archive file: {0}")]
50 FileCreation(std::io::Error),
51 #[error("failed to add file entry to archive: {0}")]
52 EntryAddition(std::io::Error),
53 #[error("failed to traverse and recursively add files to archive: {0}")]
54 DirectoryTraversal(std::io::Error),
55 #[error("{0}")]
56 FileParse(#[from] FileParseError),
57 #[error("failed writing to archive: {0}")]
58 FileWrite(std::io::Error),
59}
60
61#[derive(Error, Debug)]
62pub enum LengthExceededError {
63 #[error("Filename is longer than the maximum of 255 bytes")]
64 Name,
65 #[error("String representation of the file's size exceeds the maximum of 14 bytes")]
66 Size,
67 #[error(
68 "String representation of the file's UNIX modified time exceeds the maximum of 12 bytes"
69 )]
70 Mtime,
71 #[error(
72 "String representation of the file's parent directories exceeds the maximum of 4096 bytes"
73 )]
74 Prefix,
75}
76#[derive(Clone, Debug)]
78pub struct Header {
79 pub name: String,
81 pub size: u64,
83 pub mtime: u64,
85 pub prefix: String,
87 pub bytes: Vec<u8>,
90}
91
92#[derive(Debug)]
93pub enum Field {
94 Name,
95 Size,
96 Mtime,
97 Prefix,
98}
99
100#[derive(Error, Debug)]
101pub enum HeaderError {
102 #[error("failed parsing block: {0}")]
103 BlockParseError(#[from] BlockParseError),
104 #[error("header ended prematurely")]
105 IncompleteHeader,
106}
107
108#[derive(Error, Debug)]
109pub enum BlockParseError {
110 #[error("failed to parse field {0:?} from block as utf-8 string")]
111 FromUtf8Error(Field),
112 #[error("failed to parse field {0:?} from utf-8 string as unsigned 64 bit integer")]
113 IntoU64Error(Field),
114}
115
116fn read_block(block: &[u8], lower: usize, upper: usize) -> Result<String, FromUtf8Error> {
117 String::from_utf8(
118 block[lower..upper]
119 .iter()
120 .take_while(|c| **c != 0)
121 .copied()
122 .collect(),
123 )
124}
125
126impl Header {
127 pub fn from_bytes(block: &[u8]) -> Result<Header, HeaderError> {
129 Ok(Header {
130 name: read_block(block, 0, FILENAME)
131 .map_err(|_| BlockParseError::FromUtf8Error(Field::Name))?,
132 size: read_block(block, SIZE_BEGIN, SIZE_END)
133 .map_err(|_| BlockParseError::FromUtf8Error(Field::Size))?
134 .parse()
135 .map_err(|_| BlockParseError::IntoU64Error(Field::Size))?,
136 mtime: read_block(block, MTIME_BEGIN, MTIME_END)
137 .map_err(|_| BlockParseError::FromUtf8Error(Field::Mtime))?
138 .parse()
139 .map_err(|_| BlockParseError::IntoU64Error(Field::Mtime))?,
140 prefix: read_block(block, PREFIX_BEGIN, HEADER_SIZE)
141 .map_err(|_| BlockParseError::FromUtf8Error(Field::Prefix))?,
142 bytes: block.to_vec(),
143 })
144 }
145
146 pub fn from_file_metadata<P: AsRef<Path>>(path: P) -> Result<Header, FileParseError> {
162 let path = path.as_ref();
163 let metadata = std::fs::metadata(path).map_err(|_| FileParseError::Metadata)?;
164
165 let name = path.file_name().ok_or(FileParseError::EmptyName)?;
166 FILENAME
167 .checked_sub(name.len())
168 .ok_or(LengthExceededError::Name)?;
169
170 let name = name.to_string_lossy().to_string();
171
172 let size = metadata.len();
173 let size_str = size.to_string();
174 SIZE.checked_sub(size_str.len())
175 .ok_or(LengthExceededError::Size)?;
176
177 let mtime = metadata
178 .modified()
179 .map_err(|_| FileParseError::ReadLastModified)?
180 .duration_since(SystemTime::UNIX_EPOCH)
181 .map_err(|_| FileParseError::UnixEpoch)?
182 .as_secs();
183 let mtime_str = mtime.to_string();
184 MTIME
185 .checked_sub(mtime_str.len())
186 .ok_or(LengthExceededError::Mtime)?;
187
188 let prefix = path
189 .parent()
190 .map_or(String::from(""), |p| p.to_string_lossy().to_string());
191 PREFIX
192 .checked_sub(prefix.len())
193 .ok_or(LengthExceededError::Prefix)?;
194
195 let mut bytes = Cursor::new(vec![0u8; HEADER_SIZE]);
196
197 bytes.write_all(name.as_bytes())?;
199 bytes.seek(SeekFrom::Start(FILENAME as u64))?;
200 bytes.write_all(size_str.as_bytes())?;
201 bytes.seek(SeekFrom::Start(SIZE_END as u64))?;
202 bytes.write_all(mtime_str.as_bytes())?;
203 bytes.seek(SeekFrom::Start(MTIME_END as u64))?;
204 bytes.write_all(prefix.as_bytes())?;
205
206 let bytes = bytes.into_inner();
207
208 Ok(Header {
209 name,
210 size,
211 mtime,
212 prefix,
213 bytes,
214 })
215 }
216}