psarc2 0.1.0

PlayStation archive reader
Documentation
//! Path handling within the archive.

use std::{
    ffi::OsStr,
    io::{Read, Seek},
    path::{Path, PathBuf},
};

use aes::{Aes256, cipher::KeyIvInit as _};
use cfb_mode::Decryptor;

use crate::{
    PlaystationArchive,
    error::{Error, Result},
    file::FileEntry,
    toc::DecryptionKey,
};

/// How the paths of the archive are formatted.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ArchiveFlags {
    /// The paths won't have slash at the start of every line, everything is accessed as if the archive is a directory.
    Relative,
    /// All paths are case insensitive.
    IgnoreCase,
    /// All paths start with a slash.
    Absolute,
    /// TOC is encrypted.
    Encrypted,
}

impl ArchiveFlags {
    /// Decrypt a buffer in-place if necessary.
    pub(crate) fn decrypt(
        self,
        buffer: &mut [u8],
        decryption_key: Option<DecryptionKey>,
    ) -> Result<()> {
        // Decrypt the buffer if needed
        if self == Self::Encrypted {
            // Check if we have the required config
            let Some(DecryptionKey { key, iv }) = decryption_key else {
                return Err(Error::MissingDecryptionKey);
            };

            // Decrypt the entries in-place
            Decryptor::<Aes256>::new(&key.into(), &iv.into()).decrypt(buffer);
        }

        Ok(())
    }
}

impl TryFrom<u32> for ArchiveFlags {
    type Error = Error;

    fn try_from(value: u32) -> Result<Self> {
        match value {
            0 => Ok(Self::Relative),
            1 => Ok(Self::IgnoreCase),
            2 => Ok(Self::Absolute),
            4 => Ok(Self::Encrypted),
            _ => Err(Error::Corrupt("unrecognized archive flags")),
        }
    }
}

impl<R: Read + Seek> PlaystationArchive<R> {
    /// Returns an iterator over all paths of files in this archive.
    ///
    /// Ignores the manifest file.
    pub fn paths(&self) -> impl Iterator<Item = &'_ Path> {
        self.file_entries
            .iter()
            .skip(1)
            .map(|entry| entry.path.as_path())
    }

    /// Get the index of a file entry by file name, if it's present.
    pub fn index_for_name(&self, name: &str) -> Option<usize> {
        let (index, _entry) = self
            .file_entries
            .iter()
            .enumerate()
            .find(|(_index, entry)| {
                entry
                    .path
                    .file_name()
                    .is_some_and(|file_name| file_name == name)
            })?;

        Some(index)
    }

    /// Get the index of a file entry by path, if it's present.
    pub fn index_for_path<P>(&self, path: P) -> Option<usize>
    where
        P: AsRef<Path>,
    {
        let path = path.as_ref();

        let (index, _entry) = self
            .file_entries
            .iter()
            .enumerate()
            .find(|(_index, entry)| entry.path == path)?;

        Some(index)
    }

    /// Get the file name of an entry, if it's present.
    pub fn name_for_index<P>(&self, index: usize) -> Option<&OsStr> {
        self.file_entries.get(index)?.path.file_name()
    }

    /// Get an file entry by name.
    pub(crate) fn entry_by_name(&self, name: &str) -> Result<&FileEntry> {
        self.file_entries
            .iter()
            .find(|entry| {
                entry
                    .path
                    .file_name()
                    .is_some_and(|file_name| file_name == name)
            })
            .ok_or_else(|| Error::FileAtPathDoesNotExist(PathBuf::from(name)))
    }

    /// Get an file entry by path.
    pub(crate) fn entry_by_path<P>(&self, path: P) -> Result<&FileEntry>
    where
        P: AsRef<Path>,
    {
        let path = path.as_ref();

        self.file_entries
            .iter()
            .find(|entry| entry.path == path)
            .ok_or_else(|| Error::FileAtPathDoesNotExist(path.to_path_buf()))
    }
}