pk2_sync/
lib.rs

1pub mod fs;
2use self::fs::{Directory, File, FileMut};
3
4mod io;
5
6use std::io::{Cursor, Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
7use std::marker::PhantomData;
8use std::path::Path;
9use std::{fs as stdfs, io as stdio};
10
11use pk2::block_chain::PackBlock;
12use pk2::blowfish::Blowfish;
13use pk2::chain_index::{ChainIndex, ChainIndexParser};
14use pk2::entry::PackEntry;
15use pk2::header::PackHeader;
16use pk2::{ChainOffset, StreamOffset};
17
18/// An IO wrapper type that only exposes read and seek operations.
19pub struct ReadOnly<B>(pub B);
20impl<B: stdio::Read> stdio::Read for ReadOnly<B> {
21    fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
22        self.0.read(buf)
23    }
24}
25impl<B: stdio::Seek> stdio::Seek for ReadOnly<B> {
26    fn seek(&mut self, pos: stdio::SeekFrom) -> IoResult<u64> {
27        self.0.seek(pos)
28    }
29}
30
31/// A type that allows mutable access to its inner value via interior mutability.
32pub trait Lock<T> {
33    /// Create a new instance of the lock.
34    fn new(b: T) -> Self;
35    /// Consume the lock and return the inner value.
36    fn into_inner(self) -> T;
37    /// Perform an operation on the inner value by taking the lock.
38    fn with_lock<R>(&self, f: impl FnOnce(&mut T) -> R) -> R;
39}
40
41/// A type that allows choosing between different locking mechanisms for the backing buffer of the
42/// pk2 archive.
43pub trait LockChoice {
44    /// The type of lock to be used.
45    type Lock<T>: Lock<T>;
46    /// Wrap the value in our lock.
47    fn new_locked<T>(t: T) -> Self::Lock<T> {
48        Self::Lock::new(t)
49    }
50}
51
52macro_rules! gen_type_aliases {
53    ($lock:ident) => {
54        pub type Pk2<Buffer = std::fs::File> = crate::Pk2<Buffer, $lock>;
55
56        pub type File<'pk2, Buffer = std::fs::File> = crate::fs::File<'pk2, Buffer, $lock>;
57        pub type FileMut<'pk2, Buffer = std::fs::File> = crate::fs::FileMut<'pk2, Buffer, $lock>;
58        pub type DirEntry<'pk2, Buffer = std::fs::File> = crate::fs::DirEntry<'pk2, Buffer, $lock>;
59        pub type Directory<'pk2, Buffer = std::fs::File> =
60            crate::fs::Directory<'pk2, Buffer, $lock>;
61        /// Read-only versions of the API types.
62        pub mod readonly {
63            pub type Pk2<Buffer = std::fs::File> = super::Pk2<crate::ReadOnly<Buffer>>;
64
65            pub type File<'pk2, Buffer = std::fs::File> =
66                super::File<'pk2, crate::ReadOnly<Buffer>>;
67            pub type FileMut<'pk2, Buffer = std::fs::File> =
68                super::FileMut<'pk2, crate::ReadOnly<Buffer>>;
69            pub type DirEntry<'pk2, Buffer = std::fs::File> =
70                super::DirEntry<'pk2, crate::ReadOnly<Buffer>>;
71            pub type Directory<'pk2, Buffer = std::fs::File> =
72                super::Directory<'pk2, crate::ReadOnly<Buffer>>;
73        }
74    };
75}
76
77pub use self::sync::Lock as SyncLock;
78pub mod sync {
79    use std::sync::Mutex;
80
81    /// A lock that uses a [`std::sync::Mutex`] to provide interior mutability.
82    pub enum Lock {}
83    impl super::LockChoice for Lock {
84        type Lock<T> = Mutex<T>;
85    }
86
87    gen_type_aliases! {
88        Lock
89    }
90
91    impl<T> super::Lock<T> for Mutex<T> {
92        fn new(b: T) -> Self {
93            Mutex::new(b)
94        }
95        fn into_inner(self) -> T {
96            self.into_inner().unwrap()
97        }
98        fn with_lock<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
99            f(&mut self.lock().unwrap())
100        }
101    }
102}
103
104pub use self::unsync::Lock as UnsyncLock;
105pub mod unsync {
106    use std::cell::RefCell;
107
108    /// A lock that uses a [`std::cell::RefCell`] to provide interior mutability.
109    pub enum Lock {}
110    impl super::LockChoice for Lock {
111        type Lock<T> = RefCell<T>;
112    }
113
114    gen_type_aliases! {
115        Lock
116    }
117
118    impl<T> super::Lock<T> for RefCell<T> {
119        fn new(b: T) -> Self {
120            RefCell::new(b)
121        }
122        fn into_inner(self) -> T {
123            self.into_inner()
124        }
125        fn with_lock<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
126            f(&mut self.borrow_mut())
127        }
128    }
129}
130
131use IoResult as OpenResult;
132
133/// A Pk2 archive.
134pub struct Pk2<Buffer, L: LockChoice> {
135    stream: <L as LockChoice>::Lock<Buffer>,
136    blowfish: Option<Box<Blowfish>>,
137    chain_index: ChainIndex,
138    유령: PhantomData<Buffer>,
139}
140
141impl<L: LockChoice> Pk2<stdfs::File, L> {
142    /// Creates a new [`File`](stdfs::File) based archive at the given path.
143    pub fn create_new<P: AsRef<Path>, K: AsRef<[u8]>>(path: P, key: K) -> OpenResult<Self> {
144        let file = stdfs::OpenOptions::new()
145            .create_new(true)
146            .write(true)
147            .read(true)
148            .open(path.as_ref())?;
149        Self::_create_impl(file, key)
150    }
151
152    /// Opens an archive at the given path.
153    ///
154    /// Note this eagerly parses the whole archive's file table into memory incurring a lot of read
155    /// operations on the file making this operation potentially slow.
156    pub fn open<P: AsRef<Path>, K: AsRef<[u8]>>(path: P, key: K) -> OpenResult<Self> {
157        let file = stdfs::OpenOptions::new().write(true).read(true).open(path)?;
158        Self::_open_in_impl(file, key)
159    }
160}
161
162impl<L: LockChoice> Pk2<ReadOnly<stdfs::File>, L> {
163    /// Opens an archive at the given path.
164    ///
165    /// Note this eagerly parses the whole archive's file table into memory incurring a lot of read
166    /// operations on the file making this operation potentially slow.
167    pub fn open_readonly<P: AsRef<Path>, K: AsRef<[u8]>>(path: P, key: K) -> OpenResult<Self> {
168        let file = stdfs::OpenOptions::new().read(true).open(path)?;
169        Self::_open_in_impl(ReadOnly(file), key)
170    }
171}
172
173impl<L: LockChoice> Pk2<Cursor<Vec<u8>>, L> {
174    /// Creates a new archive in memory.
175    pub fn create_new_in_memory<K: AsRef<[u8]>>(key: K) -> Result<Self, pk2::blowfish::InvalidKey> {
176        Self::_create_impl(Cursor::new(Vec::with_capacity(4096)), key).map_err(|_| {
177            // the only error that can actually occur here is an InvalidKey error
178            pk2::blowfish::InvalidKey
179        })
180    }
181}
182
183impl<L: LockChoice> From<Pk2<Cursor<Vec<u8>>, L>> for Vec<u8> {
184    fn from(pk2: Pk2<Cursor<Vec<u8>>, L>) -> Self {
185        pk2.stream.into_inner().into_inner()
186    }
187}
188
189impl<B, L> Pk2<B, L>
190where
191    B: stdio::Read + stdio::Seek,
192    L: LockChoice,
193{
194    /// Returns a reference to the chain index.
195    pub fn chain_index(&self) -> &ChainIndex {
196        &self.chain_index
197    }
198
199    /// Opens an archive from the given stream.
200    ///
201    /// Note this eagerly parses the whole archive's file table into memory incurring a lot of read
202    /// operations on the stream.
203    pub fn open_in<K: AsRef<[u8]>>(mut stream: B, key: K) -> OpenResult<Self> {
204        stream.seek(stdio::SeekFrom::Start(0))?;
205        Self::_open_in_impl(stream, key)
206    }
207
208    fn _open_in_impl<K: AsRef<[u8]>>(mut stream: B, key: K) -> OpenResult<Self> {
209        let mut buffer = [0; PackHeader::PACK_HEADER_LEN];
210        stream.read_exact(&mut buffer)?;
211        let header = PackHeader::parse(&buffer);
212        header.validate_sig().map_err(|e| IoError::new(IoErrorKind::InvalidData, e))?;
213        let blowfish = if header.encrypted {
214            let bf = Blowfish::new(key.as_ref())
215                .map_err(|e| IoError::new(IoErrorKind::InvalidInput, e))?;
216            header.verify(&bf).map_err(|e| IoError::new(IoErrorKind::InvalidInput, e))?;
217            Some(Box::new(bf))
218        } else {
219            None
220        };
221        let chain_index = ChainIndex::read_sync(&mut stream, blowfish.as_deref())?;
222
223        Ok(Pk2 {
224            stream: <L as LockChoice>::Lock::new(stream),
225            blowfish,
226            chain_index,
227            유령: PhantomData,
228        })
229    }
230}
231
232impl<B, L> Pk2<B, L>
233where
234    B: stdio::Read + stdio::Write + stdio::Seek,
235    L: LockChoice,
236{
237    pub fn create_new_in<K: AsRef<[u8]>>(mut stream: B, key: K) -> OpenResult<Self> {
238        stream.seek(stdio::SeekFrom::Start(0))?;
239        Self::_create_impl(stream, key)
240    }
241
242    fn _create_impl<K: AsRef<[u8]>>(mut stream: B, key: K) -> OpenResult<Self> {
243        let (header, blowfish) = if key.as_ref().is_empty() {
244            (PackHeader::default(), None)
245        } else {
246            let bf = Blowfish::new(key.as_ref())
247                .map_err(|e| IoError::new(IoErrorKind::InvalidInput, e))?;
248            (PackHeader::new_encrypted(&bf), Some(Box::new(bf)))
249        };
250
251        let mut out = [0; PackHeader::PACK_HEADER_LEN];
252        header.write_into(&mut out);
253        stream.write_all(&out)?;
254        let mut block = PackBlock::default();
255        block[0] = PackEntry::new_directory(".", ChainIndex::PK2_ROOT_CHAIN_OFFSET, None);
256        crate::io::write_block(
257            blowfish.as_deref(),
258            &mut stream,
259            ChainIndex::PK2_ROOT_BLOCK_OFFSET,
260            &block,
261        )?;
262
263        let bf = blowfish.as_deref();
264        let mut chain_index = ChainIndex::default();
265        let mut fsm = ChainIndexParser::new(
266            &mut chain_index,
267            vec![(ChainIndex::PK2_ROOT_CHAIN_OFFSET, ChainIndex::PK2_ROOT_BLOCK_OFFSET)],
268        );
269        let mut buffer = [0; PackBlock::PK2_FILE_BLOCK_SIZE];
270        while let Some(offset) = fsm.wants_read_at() {
271            stream.seek(std::io::SeekFrom::Start(offset.0.get()))?;
272            stream.read_exact(&mut buffer)?;
273            if let Some(bf) = bf {
274                bf.decrypt(&mut buffer);
275            }
276            fsm.progress(&buffer).map_err(|e| {
277                std::io::Error::new(
278                    std::io::ErrorKind::InvalidData,
279                    format!("Failed to parse block at offset {}: {}", offset.0, e),
280                )
281            })?;
282        }
283
284        Ok(Pk2 { stream: L::new_locked(stream), blowfish, chain_index, 유령: PhantomData })
285    }
286}
287
288impl<L: LockChoice, B> Pk2<B, L> {
289    fn root_resolve_path_to_entry_and_parent<P: AsRef<str>>(
290        &self,
291        path: P,
292    ) -> OpenResult<Option<(ChainOffset, usize, &PackEntry)>> {
293        let path = check_root(path.as_ref())?;
294        if path.is_empty() {
295            return Ok(None);
296        }
297        self.chain_index
298            .resolve_path_to_entry_and_parent(ChainIndex::PK2_ROOT_CHAIN_OFFSET, path)
299            .map(Some)
300            .map_err(|e| IoError::new(IoErrorKind::InvalidData, e))
301    }
302
303    fn root_resolve_path_to_entry_and_parent_mut<P: AsRef<str>>(
304        &mut self,
305        path: P,
306    ) -> OpenResult<Option<(ChainOffset, usize, &mut PackEntry)>> {
307        let path = check_root(path.as_ref())?;
308        if path.is_empty() {
309            return Ok(None);
310        }
311        self.chain_index
312            .resolve_path_to_entry_and_parent_mut(ChainIndex::PK2_ROOT_CHAIN_OFFSET, path)
313            .map(Some)
314            .map_err(|e| IoError::new(IoErrorKind::InvalidData, e))
315    }
316
317    fn is_file(entry: &PackEntry) -> OpenResult<()> {
318        match entry.is_file() {
319            true => Ok(()),
320            false => Err(IoError::new(IoErrorKind::InvalidData, "Expected a file entry")),
321        }
322    }
323
324    fn is_dir(entry: &PackEntry) -> OpenResult<()> {
325        match entry.is_directory() {
326            true => Ok(()),
327            false => Err(IoError::new(IoErrorKind::InvalidData, "Expected a directory entry")),
328        }
329    }
330}
331
332impl<B, L: LockChoice> Pk2<B, L> {
333    pub fn open_file<P: AsRef<str>>(&self, path: P) -> OpenResult<File<'_, B, L>> {
334        let (chain, entry_idx, entry) = self
335            .root_resolve_path_to_entry_and_parent(path)?
336            .ok_or_else(|| IoError::new(IoErrorKind::InvalidData, "Expected a file entry"))?;
337        Self::is_file(entry)?;
338        Ok(File::new(self, chain, entry_idx))
339    }
340
341    pub fn open_directory<P: AsRef<str>>(&self, path: P) -> OpenResult<Directory<'_, B, L>> {
342        match self.root_resolve_path_to_entry_and_parent(path)? {
343            Some((chain, entry_idx, entry)) => {
344                Self::is_dir(entry)?;
345                Ok(Directory::new(self, Some(chain), entry_idx))
346            }
347            None => Ok(Directory::new(self, None, 0)),
348        }
349    }
350
351    pub fn open_root_dir(&self) -> Directory<'_, B, L> {
352        Directory::new(self, None, 0)
353    }
354
355    /// Invokes cb on every file in the sub directories of `base`, including
356    /// files inside of its subdirectories. Cb gets invoked with its
357    /// relative path to `base` and the file object.
358    pub fn for_each_file(
359        &self,
360        base: impl AsRef<str>,
361        cb: impl FnMut(&Path, File<'_, B, L>) -> OpenResult<()>,
362    ) -> OpenResult<()> {
363        self.open_directory(base)?.for_each_file(cb)
364    }
365}
366
367impl<B, L> Pk2<B, L>
368where
369    B: stdio::Read + stdio::Seek,
370    L: LockChoice,
371{
372    pub fn read<P: AsRef<str>>(&self, path: P) -> OpenResult<Vec<u8>> {
373        let mut file = self.open_file(path)?;
374        let mut buf = Vec::with_capacity(file.size() as usize);
375        stdio::Read::read_to_end(&mut file, &mut buf)?;
376        Ok(buf)
377    }
378}
379
380impl<B, L> Pk2<B, L>
381where
382    B: stdio::Read + stdio::Write + stdio::Seek,
383    L: LockChoice,
384{
385    pub fn open_file_mut<P: AsRef<str>>(&mut self, path: P) -> OpenResult<FileMut<'_, B, L>> {
386        let (chain, entry_idx, entry) = self
387            .root_resolve_path_to_entry_and_parent(path)?
388            .ok_or_else(|| IoError::new(IoErrorKind::InvalidData, "Expected a file entry"))?;
389        Self::is_file(entry)?;
390        Ok(FileMut::new(self, chain, entry_idx))
391    }
392
393    /// Currently only replaces the entry with an empty one making the data
394    /// inaccessible by normal means
395    pub fn delete_file<P: AsRef<str>>(&mut self, path: P) -> OpenResult<()> {
396        let (chain_index, entry_idx, entry) = self
397            .root_resolve_path_to_entry_and_parent_mut(path)?
398            .ok_or_else(|| IoError::new(IoErrorKind::InvalidData, "Expected a file entry"))?;
399        Self::is_file(entry)?;
400        entry.clear();
401
402        self.stream.with_lock(|stream| {
403            crate::io::write_chain_entry(
404                self.blowfish.as_deref(),
405                stream,
406                self.chain_index.get(chain_index).unwrap(),
407                entry_idx,
408            )
409        })?;
410        Ok(())
411    }
412
413    pub fn create_file<P: AsRef<str>>(&mut self, path: P) -> OpenResult<FileMut<'_, B, L>> {
414        let path = check_root(path.as_ref())?;
415        let (chain, entry_idx, file_name) = self.stream.with_lock(|stream| {
416            Self::create_entry_at(
417                &mut self.chain_index,
418                self.blowfish.as_deref(),
419                stream,
420                ChainIndex::PK2_ROOT_CHAIN_OFFSET,
421                path,
422            )
423        })?;
424        let entry = self.chain_index.get_entry_mut(chain, entry_idx).unwrap();
425        // The stream offset is a dummy value
426        *entry = PackEntry::new_file(
427            file_name,
428            StreamOffset(ChainIndex::PK2_ROOT_BLOCK_OFFSET.0),
429            0,
430            entry.next_block(),
431        );
432        Ok(FileMut::new(self, chain, entry_idx))
433    }
434
435    /// Creates a file at the given path, or truncates it if it already exists.
436    ///
437    /// If the file already exists, it will be opened for writing with its
438    /// contents discarded. The existing file entry is retained.
439    /// If the file does not exist, a new file is created.
440    pub fn create_file_truncate<P: AsRef<str>>(
441        &mut self,
442        path: P,
443    ) -> OpenResult<FileMut<'_, B, L>> {
444        let path_str = path.as_ref();
445        // Try to open the file if it exists
446        match self.root_resolve_path_to_entry_and_parent(path_str) {
447            Ok(Some((chain, entry_idx, entry))) if entry.is_file() => {
448                // File exists, return a truncated FileMut
449                Ok(FileMut::new_truncated(self, chain, entry_idx))
450            }
451            Ok(Some(_)) => {
452                // Path exists but is not a file (e.g., directory)
453                Err(IoError::new(IoErrorKind::InvalidData, "Expected a file entry"))
454            }
455            Ok(None) | Err(_) => {
456                // File doesn't exist, create it
457                self.create_file(path_str)
458            }
459        }
460    }
461
462    /// This function traverses the whole path creating anything that does not
463    /// yet exist returning the last created entry. This means using parent and
464    /// current dir parts in a path that in the end directs to an already
465    /// existing path might still create new directories that arent actually being used.
466    fn create_entry_at<'p>(
467        chain_index: &mut ChainIndex,
468        blowfish: Option<&Blowfish>,
469        mut stream: &mut B,
470        chain: ChainOffset,
471        path: &'p str,
472    ) -> OpenResult<(ChainOffset, usize, &'p str)> {
473        use crate::io::{allocate_empty_block, allocate_new_block_chain, write_chain_entry};
474        let (mut current_chain_index, mut components) = chain_index
475            .validate_dir_path_until(chain, path)
476            .map_err(|e| IoError::new(IoErrorKind::InvalidInput, e))?
477            .ok_or_else(|| IoError::from(IoErrorKind::AlreadyExists))?;
478
479        while let Some(component) = components.next() {
480            let current_chain = chain_index
481                .get_mut(current_chain_index)
482                .ok_or_else(|| IoError::from(IoErrorKind::InvalidInput))?;
483
484            // Check if a file or directory with this name already exists
485            if current_chain.entries().any(|e| e.name_eq_ignore_ascii_case(component)) {
486                return Err(IoErrorKind::AlreadyExists.into());
487            }
488
489            let empty_pos = current_chain.entries().position(PackEntry::is_empty);
490            let chain_entry_idx = if let Some(idx) = empty_pos {
491                idx
492            } else {
493                // current chain is full so create a new block and append it
494                let (offset, block) = allocate_empty_block(blowfish, &mut stream)?;
495                let chain_entry_idx = current_chain.num_entries();
496                current_chain.push_and_link(offset, block);
497                write_chain_entry(blowfish, &mut stream, current_chain, chain_entry_idx - 1)?;
498                chain_entry_idx
499            };
500            // Are we done after this? if not, create a new blockchain since this is a new
501            // directory
502            if components.peek().is_some() {
503                let block_chain = allocate_new_block_chain(
504                    blowfish,
505                    &mut stream,
506                    current_chain,
507                    component,
508                    chain_entry_idx,
509                )?;
510                current_chain_index = block_chain.chain_index();
511                chain_index.insert(current_chain_index, block_chain);
512            } else {
513                return Ok((current_chain.chain_index(), chain_entry_idx, component));
514            }
515        }
516        Err(IoErrorKind::AlreadyExists.into())
517    }
518}
519
520fn check_root(path: &str) -> OpenResult<&str> {
521    path.strip_prefix("/").ok_or_else(|| IoError::new(IoErrorKind::InvalidInput, "invalid path"))
522}