box_format/file/
reader.rs

1use std::fs::File;
2use std::fs::{self, OpenOptions};
3use std::io::{self, prelude::*, BufReader, BufWriter, SeekFrom};
4use std::num::NonZeroU64;
5use std::path::{Path, PathBuf};
6
7use comde::Decompress;
8use memmap2::{Mmap, MmapOptions};
9
10use super::meta::Records;
11use super::{meta::RecordsItem, BoxMetadata};
12use crate::path::IntoBoxPathError;
13use crate::{
14    de::DeserializeOwned,
15    header::BoxHeader,
16    path::BoxPath,
17    record::{FileRecord, LinkRecord, Record},
18};
19
20#[derive(Debug)]
21pub struct BoxFileReader {
22    pub(crate) file: BufReader<File>,
23    pub(crate) path: PathBuf,
24    pub(crate) header: BoxHeader,
25    pub(crate) meta: BoxMetadata,
26    pub(crate) offset: u64,
27}
28
29#[inline(always)]
30pub(super) fn read_header<R: Read + Seek>(file: &mut R, offset: u64) -> io::Result<BoxHeader> {
31    file.seek(SeekFrom::Start(offset))?;
32    BoxHeader::deserialize_owned(file)
33}
34
35#[inline(always)]
36pub(super) fn read_trailer<R: Read + Seek, P: AsRef<Path>>(
37    reader: &mut R,
38    ptr: NonZeroU64,
39    _path: P,
40    offset: u64,
41) -> io::Result<BoxMetadata> {
42    reader.seek(SeekFrom::Start(offset + ptr.get()))?;
43    let meta = BoxMetadata::deserialize_owned(reader)?;
44
45    Ok(meta)
46}
47
48#[derive(Debug, thiserror::Error)]
49pub enum OpenError {
50    #[error("Could not find trailer (the end of the file is missing).")]
51    MissingTrailer,
52
53    #[error("Invalid trailer data (the data that describes where all the files are is invalid).")]
54    InvalidTrailer(#[source] std::io::Error),
55
56    #[error("Could not read header. Is this a valid Box archive?")]
57    MissingHeader(#[source] std::io::Error),
58
59    #[error("Invalid path to Box file. Path: '{}'", .1.display())]
60    InvalidPath(#[source] std::io::Error, PathBuf),
61
62    #[error("Failed to read Box file. Path: '{}'", .1.display())]
63    ReadFailed(#[source] std::io::Error, PathBuf),
64}
65
66#[derive(Debug, thiserror::Error)]
67pub enum ExtractError {
68    #[error("Creating directory failed. Path: '{}'", .1.display())]
69    CreateDirFailed(#[source] std::io::Error, PathBuf),
70
71    #[error("Creating file failed. Path: '{}'", .1.display())]
72    CreateFileFailed(#[source] std::io::Error, PathBuf),
73
74    #[error("Path not found in archive. Path: '{}'", .0.display())]
75    NotFoundInArchive(PathBuf),
76
77    #[error("Decompressing file failed. Path: '{}'", .1.display())]
78    DecompressionFailed(#[source] std::io::Error, PathBuf),
79
80    #[error("Creating link failed. Path: '{}' -> '{}'", .1.display(), .2.display())]
81    CreateLinkFailed(#[source] std::io::Error, PathBuf, PathBuf),
82
83    #[error("Resolving link failed: Path: '{}' -> '{}'", .1.name, .1.target)]
84    ResolveLinkFailed(#[source] std::io::Error, LinkRecord),
85
86    #[error("Could not convert to a valid Box path. Path suffix: '{}'", .1)]
87    ResolveBoxPathFailed(#[source] IntoBoxPathError, String),
88}
89
90impl BoxFileReader {
91    /// This will open an existing `.box` file for reading and writing, and error if the file is not valid.
92    pub fn open_at_offset<P: AsRef<Path>>(
93        path: P,
94        offset: u64,
95    ) -> Result<BoxFileReader, OpenError> {
96        let path = path.as_ref().to_path_buf();
97        let path = path
98            .canonicalize()
99            .map_err(|e| OpenError::InvalidPath(e, path.to_path_buf()))?;
100
101        let mut file = OpenOptions::new()
102            .read(true)
103            .open(&path)
104            .map_err(|e| OpenError::ReadFailed(e, path.clone()))?;
105
106        // Try to load the header so we can easily rewrite it when saving.
107        // If header is invalid, we're not even loading a .box file.
108        let (header, meta) = {
109            let mut reader = BufReader::new(&mut file);
110            let header = read_header(&mut reader, offset).map_err(OpenError::MissingHeader)?;
111            let ptr = header.trailer.ok_or(OpenError::MissingTrailer)?;
112            let meta =
113                read_trailer(&mut reader, ptr, &path, offset).map_err(OpenError::InvalidTrailer)?;
114
115            (header, meta)
116        };
117
118        let f = BoxFileReader {
119            file: BufReader::new(file),
120            path,
121            header,
122            meta,
123            offset,
124        };
125
126        Ok(f)
127    }
128
129    /// This will open an existing `.box` file for reading and writing, and error if the file is not valid.
130    #[inline]
131    pub fn open<P: AsRef<Path>>(path: P) -> Result<BoxFileReader, OpenError> {
132        Self::open_at_offset(path, 0)
133    }
134
135    #[inline(always)]
136    pub fn path(&self) -> &Path {
137        &self.path
138    }
139
140    #[inline(always)]
141    pub fn alignment(&self) -> u64 {
142        self.header.alignment
143    }
144
145    #[inline(always)]
146    pub fn version(&self) -> u32 {
147        self.header.version
148    }
149
150    #[inline(always)]
151    pub fn metadata(&self) -> &BoxMetadata {
152        &self.meta
153    }
154
155    #[inline(always)]
156    pub fn decompress_value<V: Decompress>(&self, record: &FileRecord) -> io::Result<V> {
157        let mmap = unsafe { self.memory_map(record)? };
158        record.compression.decompress(io::Cursor::new(mmap))
159    }
160
161    #[inline(always)]
162    pub fn decompress<W: Write>(&self, record: &FileRecord, dest: W) -> io::Result<()> {
163        let mmap = unsafe { self.memory_map(record)? };
164        record
165            .compression
166            .decompress_write(io::Cursor::new(mmap), dest)
167    }
168
169    #[inline(always)]
170    pub fn find(&self, path: &BoxPath) -> Result<&Record, ExtractError> {
171        let record = self
172            .meta
173            .inode(path)
174            .and_then(|x| self.meta.record(x))
175            .ok_or_else(|| ExtractError::NotFoundInArchive(path.to_path_buf()))?;
176        Ok(record)
177    }
178
179    #[inline(always)]
180    pub fn extract<P: AsRef<Path>>(
181        &self,
182        path: &BoxPath,
183        output_path: P,
184    ) -> Result<(), ExtractError> {
185        let output_path = output_path.as_ref();
186        let record = self
187            .meta
188            .inode(path)
189            .and_then(|x| self.meta.record(x))
190            .ok_or_else(|| ExtractError::NotFoundInArchive(path.to_path_buf()))?;
191        self.extract_inner(path, record, output_path)
192    }
193
194    #[inline(always)]
195    pub fn extract_recursive<P: AsRef<Path>>(
196        &self,
197        path: &BoxPath,
198        output_path: P,
199    ) -> Result<(), ExtractError> {
200        let output_path = output_path.as_ref();
201
202        let inode = self
203            .meta
204            .inode(path)
205            .ok_or_else(|| ExtractError::NotFoundInArchive(path.to_path_buf()))?;
206
207        Records::new(&self.meta, &[inode], None).try_for_each(
208            |RecordsItem { path, record, .. }| self.extract_inner(&path, record, output_path),
209        )
210    }
211
212    #[inline(always)]
213    pub fn extract_all<P: AsRef<Path>>(&self, output_path: P) -> Result<(), ExtractError> {
214        let output_path = output_path.as_ref();
215        self.meta
216            .iter()
217            .try_for_each(|RecordsItem { path, record, .. }| {
218                self.extract_inner(&path, record, output_path)
219            })
220    }
221
222    #[inline(always)]
223    pub fn resolve_link(&self, link: &LinkRecord) -> io::Result<RecordsItem> {
224        match self.meta.inode(&link.target) {
225            Some(inode) => Ok(RecordsItem {
226                inode,
227                path: link.target.to_owned(),
228                record: self.meta.record(inode).unwrap(),
229            }),
230            None => Err(io::Error::new(
231                io::ErrorKind::NotFound,
232                format!("No inode for link target: {}", link.target),
233            )),
234        }
235    }
236
237    #[inline(always)]
238    pub fn read_bytes(&self, record: &FileRecord) -> io::Result<io::Take<File>> {
239        let mut file = OpenOptions::new().read(true).open(&self.path)?;
240
241        file.seek(io::SeekFrom::Start(self.offset + record.data.get()))?;
242        Ok(file.take(record.length))
243    }
244
245    /// # Safety
246    ///
247    /// Use of memory maps is unsafe as modifications to the file could affect the operation
248    /// of the application. Ensure that the Box being operated on is not mutated while a memory
249    /// map is in use.
250    #[inline(always)]
251    pub unsafe fn memory_map(&self, record: &FileRecord) -> io::Result<Mmap> {
252        MmapOptions::new()
253            .offset(self.offset + record.data.get())
254            .len(record.length as usize)
255            .map(self.file.get_ref())
256    }
257
258    #[inline(always)]
259    fn extract_inner(
260        &self,
261        path: &BoxPath,
262        record: &Record,
263        output_path: &Path,
264    ) -> Result<(), ExtractError> {
265        // println!("{} -> {}: {:?}", path, output_path.display(), record);
266        match record {
267            Record::File(file) => {
268                fs::create_dir_all(output_path)
269                    .map_err(|e| ExtractError::CreateDirFailed(e, output_path.to_path_buf()))?;
270                let out_path = output_path.join(path.to_path_buf());
271                let mut out_file = std::fs::OpenOptions::new();
272
273                #[cfg(unix)]
274                {
275                    use std::os::unix::fs::OpenOptionsExt;
276
277                    let mode: Option<u32> = record
278                        .attr(self.metadata(), "unix.mode")
279                        .filter(|x| x.len() == 4)
280                        .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]));
281
282                    if let Some(mode) = mode {
283                        out_file.mode(mode);
284                    }
285                }
286
287                let out_file = out_file
288                    .create(true)
289                    .write(true)
290                    .open(&out_path)
291                    .map_err(|e| ExtractError::CreateFileFailed(e, out_path.to_path_buf()))?;
292
293                let out_file = BufWriter::new(out_file);
294                self.decompress(file, out_file)
295                    .map_err(|e| ExtractError::DecompressionFailed(e, path.to_path_buf()))?;
296
297                Ok(())
298            }
299            Record::Directory(_dir) => {
300                fs::create_dir_all(output_path)
301                    .map_err(|e| ExtractError::CreateDirFailed(e, output_path.to_path_buf()))?;
302                let new_dir = output_path.join(path.to_path_buf());
303                fs::create_dir(&new_dir).map_err(|e| ExtractError::CreateDirFailed(e, new_dir))
304            }
305            #[cfg(unix)]
306            Record::Link(link) => {
307                let link_target = self
308                    .resolve_link(link)
309                    .map_err(|e| ExtractError::ResolveLinkFailed(e, link.clone()))?;
310
311                let source = output_path.join(path.to_path_buf());
312                let destination = output_path.join(link_target.path.to_path_buf());
313
314                std::os::unix::fs::symlink(&source, &destination)
315                    .map_err(|e| ExtractError::CreateLinkFailed(e, source, destination))
316            }
317            #[cfg(windows)]
318            Record::Link(link) => {
319                let link_target = self
320                    .resolve_link(link)
321                    .map_err(|e| ExtractError::ResolveLinkFailed(e, link.clone()))?;
322
323                let source = output_path.join(path.to_path_buf());
324                let destination = output_path.join(link_target.path.to_path_buf());
325
326                if link_target.record.as_directory().is_some() {
327                    std::os::windows::fs::symlink_dir(&source, &destination)
328                        .map_err(|e| ExtractError::CreateLinkFailed(e, source, destination))
329                } else {
330                    std::os::windows::fs::symlink_file(&source, &destination)
331                        .map_err(|e| ExtractError::CreateLinkFailed(e, source, destination))
332                }
333            }
334        }
335    }
336}