wpress_oxide/
common.rs

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/// Metadata representation of a file with attributes necessary for an archive entry.
77#[derive(Clone, Debug)]
78pub struct Header {
79    /// Base name of the file from an entry.
80    pub name: String,
81    /// Size of the file in bytes.
82    pub size: u64,
83    /// Last modified time relative to UNIX epochs.
84    pub mtime: u64,
85    /// Path of the file without the final component, its name.
86    pub prefix: String,
87    /// A representation of `name`, `size`, `mtime` and `perfix` in a blob of bytes.
88    /// Each field is zero padded to meets predefined boundaries.
89    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    /// Parse an archive metadata entry for a file from a block of bytes.
128    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    /// Generate an archive metadata entry for a file given its path.
147    ///
148    /// # Example
149    ///
150    /// ```
151    /// use wpress_oxide::Header;
152    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
153    /// let header = Header::from_file_metadata("tests/writer/file.txt")?;
154    /// assert_eq!(header.name, "file.txt");
155    /// assert_eq!(header.size, 5);
156    /// assert_eq!(header.prefix, "tests/writer");
157    /// #    Ok(())
158    /// # }
159    /// ```
160
161    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        // If any of the following fails, panic. Something is very wrong.
198        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}