Skip to main content

qpak_lib/
pak.rs

1// SPDX-License-Identifier: MIT
2//! Quake PAK archive manipulation.
3use std::{fs::File, io::{BufReader, BufWriter, Read, Seek, Write}, path::Path};
4use byteorder::{ReadBytesExt, WriteBytesExt, LittleEndian};
5use std::path::PathBuf;
6use tokio::{fs::File as AsyncFile, io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt, BufReader as AsyncBufReader, BufWriter as AsyncBufWriter,SeekFrom}};
7use crate::{Error, Result};
8
9/// The chunk size to use when reading and writing files.
10const IO_BYTE_SIZE: usize = 8388608; // 8MiB
11
12/// The header of a PAK archive describes where the table is located in the file and its size.
13#[derive(Debug)]
14pub struct Header {
15    /// Where the table is stored. Stored in little-endian.
16    table_offset: u32,
17    /// The size of the table, including entries. Stored in little-endian.
18    table_size: u32,
19}
20
21impl Header {
22    pub(crate) const PAK_FILE_DESIGNATOR: [u8; 4] = [b'P', b'A', b'C', b'K'];
23
24    pub fn new(table_offset: u32, table_size: u32) -> Self {
25        Header { table_offset, table_size }
26    }
27
28    /// Reads a header from a (buffered) reader.
29    pub async fn read<R>(reader: &mut R) -> Result<Self>
30    where
31        R: AsyncRead + AsyncSeek + Unpin,
32    {
33        let mut magic = [0u8; 4];
34        reader.read(&mut magic).await?;
35        if magic != Self::PAK_FILE_DESIGNATOR {
36            return Err(Error::InvalidMagicNumber(magic));
37        }
38
39        let table_offset = reader.read_u32_le().await?;
40        let table_size = reader.read_u32_le().await?;
41
42        Ok(Header::new(table_offset, table_size))
43    }
44
45    /// Reads a header from a (buffered) reader.
46    pub fn read_sync<R>(reader: &mut R) -> Result<Self>
47    where
48        R: Read + Seek
49    {
50        let mut magic = [0u8; 4];
51        reader.read(&mut magic)?;
52        if magic != Self::PAK_FILE_DESIGNATOR {
53            return Err(Error::InvalidMagicNumber(magic));
54        }
55
56        let table_offset = reader.read_u32::<LittleEndian>()?;
57        let table_size = reader.read_u32::<LittleEndian>()?;
58
59        Ok(Header::new(table_offset, table_size))
60    }
61
62    /// Writes the header to a (buffered) writer.
63    pub async fn write<R>(&self, writer: &mut R) -> Result<()>
64    where
65        R: AsyncWrite + Unpin
66    {
67        writer.write_all(&Self::PAK_FILE_DESIGNATOR).await?;
68        writer.write_u32_le(self.table_offset).await?;
69        writer.write_u32_le(self.table_size).await?;
70        Ok(())
71    }
72
73    /// Writes the header to a (buffered) writer.
74    pub fn write_sync<R>(&self, writer: &mut R) -> Result<()>
75    where
76        R: Write
77    {
78        writer.write_all(&Self::PAK_FILE_DESIGNATOR)?;
79        writer.write_u32::<LittleEndian>(self.table_offset)?;
80        writer.write_u32::<LittleEndian>(self.table_size)?;
81        Ok(())
82    }
83
84    /// Where the table is stored in the PAK file
85    pub fn table_offset(&self) -> u32 {
86        self.table_offset
87    }
88
89    /// The size of the table, including entries
90    pub fn table_size(&self) -> u32 {
91        self.table_size
92    }
93}
94
95/// PAK tables act as an index of all files in the archive.
96/// Each [TableEntry] points to its corresponding file data.
97#[derive(Debug)]
98pub struct Table {
99    entries: Vec<TableEntry>,
100}
101
102impl Table {
103    pub fn new(entries: Vec<TableEntry>) -> Self {
104        Table { entries }
105    }
106
107    /// Reads a table and its entries from a (buffered) reader.
108    pub async fn read<R>(reader: &mut R, header: &Header) -> Result<Self>
109    where
110        R: AsyncRead + AsyncSeek + Unpin
111    {
112        reader.seek(SeekFrom::Start(header.table_offset as u64)).await?;
113
114        let mut entries = Vec::new();
115        for _ in 0..header.table_size / TableEntry::SIZE as u32 {
116            let entry = TableEntry::read(reader).await?;
117            entries.push(entry);
118        }
119
120        Ok(Table::new(entries))
121    }
122
123    /// Reads a table and its entries from a (buffered) reader.
124    pub fn read_sync<R>(reader: &mut R, header: &Header) -> Result<Self>
125    where
126        R: Read + Seek
127    {
128        reader.seek(SeekFrom::Start(header.table_offset as u64))?;
129
130        let mut entries = Vec::new();
131        for _ in 0..header.table_size / TableEntry::SIZE as u32 {
132            let entry = TableEntry::read_sync(reader)?;
133            entries.push(entry);
134        }
135
136        Ok(Table::new(entries))
137    }
138
139    /// Retrieves the [TableEntry] items
140    pub fn entries(&self) -> &Vec<TableEntry> {
141        &self.entries
142    }
143
144    /// TRUE if the table contains an entry with the given path
145    pub fn contains<P: AsRef<Path>>(&self, path: P) -> bool {
146        let path = path.as_ref().to_string_lossy();
147        self.entries.iter().any(|entry| entry.path == path)
148    }
149
150    /// Writes the table and its entries to a (buffered) writer.
151    pub async fn write<W>(&self, writer: &mut W) -> Result<()>
152    where
153        W: AsyncWrite + Unpin,
154    {
155        for entry in &self.entries {
156            entry.write(writer).await?;
157        }
158
159        Ok(())
160    }
161
162    /// Writes the table and its entries to a (buffered) writer.
163    pub fn write_sync<W>(&self, writer: &mut W) -> Result<()>
164    where
165        W: Write,
166    {
167        for entry in &self.entries {
168            entry.write_sync(writer)?;
169        }
170
171        Ok(())
172    }
173}
174
175/// A single entry in a PAK [Table].
176/// Each entry is 64 bytes in size.
177#[derive(Debug, Clone)]
178pub struct TableEntry {
179    /// Stored as a null-terminated [TableEntry::PATH_SIZE] UTF8 string. Paths do not use '/' for root.
180    path: String,
181    /// Where the item file is stored in the PAK file. Stored in little-endian.
182    offset: u32,
183    /// The size of the item file in bytes. Stored in little-endian.
184    size: u32,
185}
186
187impl TableEntry {
188    /// The fixed size of [TableEntry::path] (56 bytes)
189    const PATH_SIZE: usize = 56;
190    /// The fixed size of a table entry (64 bytes)
191    const SIZE: usize = Self::PATH_SIZE + size_of::<u32>() + size_of::<u32>();
192
193    /// Will throw an [Error::FilenameTooLong] if the path is greater than 56 characters
194    pub fn new(path: String, offset: u32, size: u32) -> Result<Self> {
195        if path.len() > Self::PATH_SIZE {
196            return Err(Error::FilenameTooLong(path));
197        }
198
199        Ok(TableEntry { path, offset, size })
200    }
201
202    /// Reads a table entry from a (buffered) reader.
203    pub async fn read<R>(reader: &mut R) -> Result<Self>
204    where
205        R: AsyncRead + Unpin
206    {
207        let mut path = [0u8; Self::PATH_SIZE];
208        reader.read(&mut path).await?;
209        let path_end = path.iter()
210            .position(|&b| b == 0)
211            .ok_or(Error::FilenameTooLong(String::from_utf8_lossy(&path).into_owned()))?;
212        let path = String::from_utf8(path[0..path_end].to_vec())
213            .map_err(|e| Error::NonUtf8Filename(e))?;
214
215        let offset = reader.read_u32_le().await?;
216        let size = reader.read_u32_le().await?;
217
218        Ok(TableEntry::new(path, offset, size)?)
219    }
220
221    /// Reads a table entry from a (buffered) reader.
222    pub fn read_sync<R>(reader: &mut R) -> Result<Self>
223    where
224        R: Read
225    {
226        let mut path = [0u8; Self::PATH_SIZE];
227        reader.read(&mut path)?;
228        let path_end = path.iter()
229            .position(|b| *b == 0)
230            .ok_or(Error::FilenameTooLong(String::from_utf8_lossy(&path).into_owned()))?;
231        let path = String::from_utf8(path[0..path_end].to_vec())
232            .map_err(|e| Error::NonUtf8Filename(e))?;
233
234        let offset = reader.read_u32::<LittleEndian>()?;
235        let size = reader.read_u32::<LittleEndian>()?;
236
237        Ok(TableEntry::new(path, offset, size)?)
238    }
239
240    /// Retrieves the path of a table entry as a String.
241    /// Its stored format is a fixed 56 byte null-terminated UTF-8 string.
242    pub fn path(&self) -> &str {
243        &self.path
244    }
245
246    /// Where the table entry file data is located in the file.
247    pub fn offset(&self) -> u32 {
248        self.offset
249    }
250
251    /// The size of the table entry file's data.
252    pub fn size(&self) -> u32 {
253        self.size
254    }
255
256    /// Writes the table entry to a (buffered) writer.
257    pub async fn write<W>(&self, writer: &mut W) -> Result<()>
258    where
259        W: AsyncWrite + Unpin
260    {
261
262        let mut path = [0u8; Self::PATH_SIZE];
263        for (i, byte) in self.path.as_bytes().iter().enumerate() {
264            path[i] = *byte;
265        }
266
267        writer.write_all(&path).await?;
268        writer.write_u32_le(self.offset).await?;
269        writer.write_u32_le(self.size).await?;
270        Ok(())
271    }
272
273    /// Writes the table entry to a (buffered) writer.
274    pub fn write_sync<W>(&self, writer: &mut W) -> Result<()>
275    where
276        W: Write,
277    {
278        let mut path = [0u8; Self::PATH_SIZE];
279        for (i, byte) in self.path.as_bytes().iter().enumerate() {
280            path[i] = *byte;
281        }
282
283        writer.write_all(&path)?;
284        writer.write_u32::<LittleEndian>(self.offset)?;
285        writer.write_u32::<LittleEndian>(self.size)?;
286        Ok(())
287    }
288}
289
290/// Represents the header, table, and table directory of a PAK file.
291#[derive(Debug)]
292pub struct PakManifest {
293    header: Header,
294    table: Table,
295}
296
297impl PakManifest {
298    /// The default offset for the table directory when stored at the end of the PAK file as is typical (and standard in this lib).
299    const ITEMS_OFFSET: u32 = ((Header::PAK_FILE_DESIGNATOR.len() * size_of::<u8>()) + size_of::<u32>() + size_of::<u32>()) as u32;
300
301    pub fn new(header: Header, table: Table) -> Self {
302        PakManifest { header, table }
303    }
304
305    /// Reads a PAK manifest from an (buffered) reader.
306    pub async fn read<R>(reader: &mut R) -> Result<Self>
307    where
308        R: AsyncRead + AsyncSeek + Unpin
309    {
310        let header = Header::read(reader).await?;
311        let table = Table::read(reader, &header).await?;
312        Ok(Self::new(header, table))
313    }
314
315    /// Reads a PAK manifest from a (buffered) reader.
316    pub fn read_sync<R>(reader: &mut R) -> Result<Self>
317    where
318        R: Read + Seek
319    {
320        let header = Header::read_sync(reader)?;
321        let table = Table::read_sync(reader, &header)?;
322        Ok(Self::new(header, table))
323    }
324
325    /// Generates a PAK manifest from a directory.
326    /// Throws [Error::NonUtf8Path], [Error::FilenameTooLong]
327    pub fn from_dir_sync<P: AsRef<Path>>(input_dir: P) -> Result<Self> {
328        let files = walk_pak_dir_sync(input_dir)?;
329        Self::from_walk_results(files)
330    }
331
332    /// Generates a PAK manifest from a directory.
333    /// Throws [Error::NonUtf8Path], [Error::FilenameTooLong]
334    pub async fn from_dir<P: AsRef<Path>>(input_dir: P) -> Result<Self> {
335        let files = walk_pak_dir(input_dir).await?;
336        Self::from_walk_results(files)
337    }
338
339    pub fn from_walk_results(files: Vec<(PathBuf, u64)>) -> Result<Self> {
340        let mut total_size = 0;
341        for (_, size) in &files {
342            total_size += size;
343        }
344
345        let table_size = (files.len() * TableEntry::SIZE) as u32;
346        let header = Header::new(Self::ITEMS_OFFSET + total_size as u32, table_size);
347
348        let mut table_entries = Vec::with_capacity(files.len());
349        let mut entry_offset = Self::ITEMS_OFFSET;
350        for (path, size) in files {
351            let path = path.to_str()
352                .ok_or_else(|| Error::NonUtf8Path(path.clone()))?
353                .to_string();
354            let size = size as u32;
355            let entry = TableEntry::new(path, entry_offset, size)?;
356            table_entries.push(entry);
357            entry_offset += size;
358        }
359
360        let table = Table::new(table_entries);
361        Ok(Self::new(header, table))
362    }
363
364    // Returns the header of a PAK, containing offsets.
365    pub fn header(&self) -> &Header {
366        &self.header
367    }
368
369    /// Returns the table (index) of a PAK
370    pub fn table(&self) -> &Table {
371        &self.table
372    }
373
374    /// Returns the table entries of a PAK, including filepath, size, and offset of each item.
375    pub fn table_entries(&self) -> &Vec<TableEntry> {
376        &self.table.entries
377    }
378}
379
380/// Represents a PAK file by filepath and [PakManifest]
381/// Provides methods to iterate over file contents.
382#[derive(Debug)]
383pub struct PakFile {
384    /// The filepath of the PAK file
385    filepath: PathBuf,
386    /// The manifest of the PAK file, including header, table, and table entries.
387    manifest: PakManifest
388}
389
390impl PakFile {
391    pub fn new(filepath: PathBuf, manifest: PakManifest) -> Self {
392        PakFile { filepath, manifest }
393    }
394
395    /// Constructs from a PAK file.
396    /// Throws [Error::OpenPak], [Error::ReadPak]
397    pub async fn from_file<P>(filepath: P) -> Result<Self>
398    where
399        P: AsRef<Path>,
400    {
401        let filepath = PathBuf::from(filepath.as_ref());
402        let file = AsyncFile::open(&filepath).await
403            .map_err(|e| Error::OpenPak(e))?;
404
405        let mut reader = AsyncBufReader::new(file);
406        let manifest = PakManifest::read(&mut reader).await
407            .map_err(|e| Error::ReadPak(filepath.to_path_buf(), e.to_string()))?;
408
409        Ok(Self::new(filepath, manifest))
410    }
411
412    /// Constructs from a PAK file.
413    /// Throws [Error::OpenPak], [Error::ReadPak]
414    pub fn from_file_sync<P>(filepath: P) -> Result<Self>
415    where
416        P: AsRef<Path>,
417    {
418        let filepath = PathBuf::from(filepath.as_ref());
419        let file = File::open(&filepath)
420            .map_err(|e| Error::OpenPak(e))?;
421
422        let mut reader = BufReader::new(file);
423        let manifest = PakManifest::read_sync(&mut reader)
424            .map_err(|e| Error::ReadPak(filepath.to_path_buf(), e.to_string()))?;
425
426        Ok(Self::new(filepath, manifest))
427    }
428
429    /// Creates a PAK file by copying files from a directory. The manifest for the directory must already have been generated using [PakManifest::from_dir].
430    pub async fn create_from_dir<P>(input_dir: P, manifest: PakManifest, output_filepath: P) -> Result<Self>
431    where
432        P: AsRef<Path>,
433    {
434        let out_file = AsyncFile::create(output_filepath.as_ref()).await?;
435        let mut writer = AsyncBufWriter::new(out_file);
436
437        manifest.header.write(&mut writer).await?;
438
439        for entry in manifest.table.entries.iter() {
440            let input_path = input_dir.as_ref().join(&entry.path);
441            let in_file = AsyncFile::open(&input_path).await?;
442            let mut reader = AsyncBufReader::new(in_file);
443
444            let mut size_remaining = entry.size as usize;
445            while size_remaining > 0 {
446                let chunk_size = std::cmp::min(size_remaining, IO_BYTE_SIZE);
447                let mut chunk = vec![0; chunk_size];
448                reader.read_exact(&mut chunk).await?;
449                writer.write_all(&chunk).await?;
450                size_remaining -= chunk_size;
451            }
452        }
453
454        manifest.table.write(&mut writer).await?;
455        writer.flush().await?;
456
457        Ok(Self::new(PathBuf::from(output_filepath.as_ref()), manifest))
458    }
459
460    /// Creates (writes) a PAK file by copying files from a directory. The manifest for the directory must already have been generated using [PakManifest::from_dir_sync].
461    pub fn create_from_dir_sync<P>(input_dir: P, manifest: PakManifest, output_filepath: P) -> Result<Self>
462    where
463        P: AsRef<Path>,
464    {
465        let out_file = File::create(output_filepath.as_ref())?;
466        let mut writer = BufWriter::new(out_file);
467
468        manifest.header.write_sync(&mut writer)?;
469
470        for entry in manifest.table.entries.iter() {
471            let input_path = input_dir.as_ref().join(&entry.path);
472            let input_file = File::open(&input_path)?;
473            let mut reader = BufReader::new(input_file);
474
475            let mut size_remaining = entry.size as usize;
476            while size_remaining > 0 {
477                let chunk_size = std::cmp::min(size_remaining, IO_BYTE_SIZE);
478                let mut chunk = vec![0; chunk_size];
479                reader.read_exact(&mut chunk)?;
480                writer.write_all(&chunk)?;
481                size_remaining -= chunk_size;
482            }
483        }
484
485        manifest.table.write_sync(&mut writer)?;
486        writer.flush()?;
487
488        Ok(Self::new(PathBuf::from(output_filepath.as_ref()), manifest))
489    }
490
491    /// Extracts the contents of the PAK file to the specified directory.
492    /// Throws [Error::CreateDirectory], [Error::OpenPak], [Error::WritePak]
493    pub fn extract_sync<P: AsRef<Path>>(&self, dest_dir: P) -> Result<()> {
494        for pak_item in self.read_items_sync()? {
495            let pak_item = pak_item?;
496
497            let mut path = std::path::PathBuf::from(dest_dir.as_ref());
498            path.push(&pak_item.table_entry().path());
499
500            if let Some(parent) = path.parent() {
501                std::fs::create_dir_all(parent)
502                    .map_err(|e| Error::CreateDirectory(e))?;
503            }
504
505            let file = File::create(&path)
506                .map_err(|e| Error::OpenPak(e))?;
507
508            let mut writer = BufWriter::new(file);
509            writer.write_all(pak_item.data().as_ref())
510                .map_err(|e| Error::WritePak(e))?;
511
512            writer.flush()?;
513        }
514
515        Ok(())
516    }
517
518    /// Extracts the contents of the PAK file to the specified directory.
519    /// Throws [Error::CreateDirectory], [Error::OpenPak], [Error::WritePak]
520    pub async fn extract<P: AsRef<Path>>(&self, dest_dir: P) -> Result<()> {
521        use tokio_stream::StreamExt;
522        let mut items = std::pin::pin!(self.read_items());
523        while let Some(pak_item) = items.try_next().await? {
524            let mut path = std::path::PathBuf::from(dest_dir.as_ref());
525            path.push(&pak_item.table_entry().path());
526
527            if let Some(parent) = path.parent() {
528                tokio::fs::create_dir_all(parent).await
529                    .map_err(|e| Error::CreateDirectory(e))?;
530            }
531
532            let file = AsyncFile::create(&path).await
533                .map_err(|e| Error::OpenPak(e))?;
534
535            let mut writer = AsyncBufWriter::new(file);
536            writer.write_all(pak_item.data().as_ref()).await
537                .map_err(|e| Error::WritePak(e))?;
538
539            writer.flush().await
540                .map_err(|e| Error::WritePak(e))?;
541        }
542
543        Ok(())
544    }
545
546    /// Returns an iterator over each file item in the PAK, including data.
547    /// Throws [Error::OpenPak], [Error::ReadPak]
548    pub fn read_items<'p>(&'p self) -> impl tokio_stream::Stream<Item = Result<PakItem<'p>>> {
549        async_stream::try_stream!{
550            let table_entries = &self.manifest.table.entries;
551            let file = AsyncFile::open(&self.filepath).await
552                .map_err(|e| Error::OpenPak(e))?;
553            let mut reader = AsyncBufReader::new(file);
554
555            for i in 0..table_entries.len() {
556                let table_entry = table_entries.get(i).unwrap();
557                reader.seek(SeekFrom::Start(table_entry.offset as u64)).await
558                    .map_err(|e| Error::ReadPak(self.filepath.to_path_buf(), e.to_string()))?;
559
560                let mut data: Vec<u8> = Vec::with_capacity(table_entry.size as usize);
561                (&mut reader)
562                    .take(table_entry.size as u64)
563                    .read_to_end(&mut data).await
564                        .map_err(|e| Error::ReadPak(self.filepath.to_path_buf(), e.to_string()))?;
565
566                let item = PakItem { table_entry, data };
567                yield item;
568            }
569        }
570    }
571
572    /// Returns an iterator over each file item in the PAK, including data.
573    /// Throws [Error::OpenPak], [Error::ReadPak]
574    pub fn read_items_sync<'p>(&'p self) -> Result<impl Iterator<Item = Result<PakItem<'p>>>> {
575        let table_entries = &self.manifest.table.entries;
576        let file = File::open(&self.filepath)
577            .map_err(|e| Error::OpenPak(e))?;
578        let mut reader = BufReader::new(file);
579
580        //todo: make sure this is actually lazy in the way it's used in the cli cmd
581        let map = table_entries.iter().map(move |table_entry| {
582            reader.seek(SeekFrom::Start(table_entry.offset as u64))
583                .map_err(|e| Error::ReadPak(self.filepath.to_path_buf(), e.to_string()))?;
584
585            let mut data: Vec<u8> = Vec::with_capacity(table_entry.size as usize);
586            (&mut reader)
587                .take(table_entry.size as u64)
588                .read_to_end(&mut data)
589                    .map_err(|e| Error::ReadPak(self.filepath.to_path_buf(), e.to_string()))?;
590
591            Ok(PakItem { table_entry, data })
592        });
593
594        Ok(map)
595    }
596
597    // Returns the header, table, and table entries of the PAK file.
598    pub fn manifest(&self) -> &PakManifest {
599        &self.manifest
600    }
601}
602
603/// An iterator item representing a file in a PAK archive.
604#[derive(Debug)]
605pub struct PakItem<'t> {
606    /// The table entry for this item
607    table_entry: &'t TableEntry,
608    /// The data for this item's file
609    data: Vec<u8>
610}
611
612impl PakItem<'_> {
613    /// The table entry for this item
614    pub fn table_entry(&self) -> &TableEntry {
615        self.table_entry
616    }
617
618    /// The data for this item's file
619    pub fn data(&self) -> &Vec<u8> {
620        &self.data
621    }
622}
623
624/// Walks a directory and returns a vector of tuples containing the path and size of each file.
625/// Sorts based on heirarchy and file name.
626/// This sort order should be maintained in each PAK.
627/// Returns: (path: PathBuf, size: u64)
628/// Throws [Error::ReadDirectory]
629pub fn walk_pak_dir_sync<P: AsRef<Path>>(dir: P) -> Result<Vec<(PathBuf, u64)>> {
630    let mut entries = walkdir::WalkDir::new(&dir)
631        .follow_links(true)
632        .into_iter()
633        .filter_map(|entry| entry.ok())
634        .filter(|entry| !entry.file_name().to_string_lossy().starts_with('.'))
635        .filter(|entry| entry.metadata().is_ok_and(|metadata| metadata.is_file()))
636        .map(|entry| {
637            let metadata = entry.metadata()
638                .map_err(|e| Error::ReadDirectory(entry.path().to_path_buf(), e.to_string()))?;
639            let path = entry.path().strip_prefix(&dir)
640                .map_err(|e| Error::ReadDirectory(entry.path().to_path_buf(), e.to_string()))?
641                .to_path_buf();
642            let len = metadata.len();
643            Ok::<(PathBuf, u64), Error>((path, len))
644        })
645        .collect::<Vec<_>>()
646        .into_iter()
647        .collect::<Result<Vec<_>>>()?;
648
649    sort_walk_paths(&mut entries);
650    Ok(entries)
651}
652
653/// Sorts based on heirarchy and file name
654pub fn sort_walk_paths(entries: &mut Vec<(PathBuf, u64)>) {
655    entries.sort_by(|a, b| {
656        match a.0.parent().cmp(&b.0.parent()) {
657            std::cmp::Ordering::Equal => a.0.file_name().cmp(&b.0.file_name()),
658            other => other,
659        }
660    });
661}
662
663/// Walks a directory and returns a vector of tuples containing the path and size of each file.
664/// Sorts based on heirarchy and file name.
665/// This sort order should be maintained in each PAK.
666/// Returns: (path: PathBuf, size: u64)
667/// Throws [Error::ReadDirectory]
668pub async fn walk_pak_dir<P: AsRef<Path>>(dir: P) -> Result<Vec<(PathBuf, u64)>> {
669    let dir = dir.as_ref().to_path_buf();
670    tokio::task::spawn(async move {
671        walk_pak_dir_sync(dir)
672    }).await.expect("Expected task to join properly")
673}