horntail 0.4.2

maplestory resource file unpack lib
Documentation
use crate::crypto::{MapleCipher, MapleVersion};
use crate::reader::BinaryBuilder;
use crate::reader::wizet::WizetFile;
use crate::{AccessorBuilder, Error};
use crate::{AccessorOpt, error};
use std::fs::File;
use std::io;
use std::io::BufRead;
use std::path::{Path, PathBuf};

const INI_EXTENSION: &str = "ini";

pub struct Bundle {
    dir: PathBuf,
    version: MapleVersion,
    no_version: bool,
    files: Vec<WizetFile>,
}

pub type OptAndBuilder = (AccessorOpt, Box<dyn AccessorBuilder>);

impl Bundle {
    pub fn from_path<P: AsRef<Path>>(
        path: P,
        version: MapleVersion,
        no_version: bool,
    ) -> Result<Option<Bundle>, Error> {
        let path = path.as_ref();
        let file_stem = path
            .file_stem()
            .and_then(|f| f.to_str())
            .ok_or(error::io_err_invalid_input())?;
        let (path, parent_path) = if !path.is_file() {
            let test = path.with_extension(WizetFile::EXTENSION);
            if test.exists() {
                (test, path.parent().ok_or(error::io_err_invalid_input())?)
            } else {
                let file = path.join(file_stem).with_extension(WizetFile::EXTENSION);
                if !file.exists() {
                    return Ok(None);
                }
                (file, path)
            }
        } else {
            (
                path.to_path_buf(),
                path.parent().ok_or(error::io_err_invalid_input())?,
            )
        };

        let (files, parent_path) =
            if let Some(files) = load_complex_struct(path.as_path(), version, no_version) {
                let parent_path = if parent_path
                    .file_stem()
                    .and_then(|s| s.to_str())
                    .ok_or(error::io_err_invalid_input())?
                    == "Base"
                {
                    parent_path.parent().ok_or(error::io_err_invalid_input())?
                } else {
                    parent_path
                };
                (files, parent_path.to_path_buf())
            } else if let Some(file) = load_single_file(path.as_path(), version, no_version) {
                (vec![file], parent_path.to_path_buf())
            } else {
                return Ok(None);
            };

        Ok(Some(Bundle {
            dir: parent_path,
            version,
            no_version,
            files,
        }))
    }

    #[inline]
    pub fn load_by_name(&self, name: &str) -> Option<Bundle> {
        Bundle::from_path(self.dir.join(name), self.version, self.no_version)
            .unwrap_or_else(|e| panic!("load_by_name: {e}"))
    }

    pub fn builders(&self, cipher: &dyn MapleCipher) -> Result<Vec<OptAndBuilder>, Error> {
        let mut builders = Vec::with_capacity(self.files.len());
        for f in &self.files {
            let mmap = f.source().open()?;
            let builder = BinaryBuilder::from_boxed(cipher.clone_boxed(), mmap).into_boxed();
            builders.push((f.accessor_opt(), builder));
        }
        Ok(builders)
    }
}

fn load_single_file(path: &Path, version: MapleVersion, no_version: bool) -> Option<WizetFile> {
    WizetFile::new(path, version, no_version).ok()
}

fn load_complex_struct(
    path: &Path,
    version: MapleVersion,
    no_version: bool,
) -> Option<Vec<WizetFile>> {
    let parent_path = path.parent()?;
    let file_stem = path.file_stem().and_then(|s| s.to_str())?;
    let complex_struct = parent_path.join(file_stem).with_extension(INI_EXTENSION);
    let complex_file = File::open(complex_struct).ok()?;
    let mut complex_reader = io::BufReader::new(complex_file);
    let mut first_line = String::with_capacity(128);
    complex_reader.read_line(&mut first_line).ok()?;
    let (prefix, suffix) = first_line.trim().split_once('|')?;
    if prefix != "LastWzIndex" {
        return None;
    }
    let count = suffix.parse::<i16>().ok()?;
    let mut files = Vec::with_capacity((count + 1) as usize);
    files.push(load_single_file(path, version, no_version)?);
    for i in 0..=count {
        let path = parent_path
            .join(format!("{file_stem}_{i:0>3}"))
            .with_extension(WizetFile::EXTENSION);
        let file = load_single_file(path.as_path(), version, no_version)?;
        files.push(file);
    }
    Some(files)
}