unitypkg-core 0.1.1

Manipulate Unity's portable package files
Documentation
use std::{collections::HashMap, io::Read};

use flate2::read::GzDecoder;
use regex::Regex;
use tar::Archive;
use thiserror::Error;
use uuid::Uuid;

use crate::{Package, PackageAssetBuilder};

#[derive(Error, Debug)]
pub enum PackageReadError {
    #[error("Failed to read package: {0}")]
    ReadError(#[from] std::io::Error),
    #[error("Failed to decode gzip stream: {0}")]
    GzipError(#[from] flate2::DecompressError),
    #[error("Failed to parse UUID: {0}")]
    UuidError(#[from] uuid::Error),
    #[error("Invalid asset path: {0}")]
    InvalidAssetPath(String),
}

pub fn read_package<R: Read>(r: R) -> Result<Package, PackageReadError> {
    let mut package = Package::new();
    let decoder = GzDecoder::new(r);
    let mut archive = Archive::new(decoder);

    let entries = archive.entries()?;

    let regex = Regex::new(r"\.\/([0-9a-f]{32})\/(.*)").unwrap();

    let mut found_assets: HashMap<Uuid, PackageAssetBuilder> = HashMap::new();
    for entry in entries {
        let mut entry = entry?;
        let path = entry.path()?;
        let path_str = path
            .to_str()
            .ok_or_else(|| PackageReadError::InvalidAssetPath(format!("{:?}", path)))?;

        if let Some(captures) = regex.captures(path_str) {
            let asset_guid = Uuid::parse_str(&captures[1])?;
            let asset_path = &captures[2];

            let builder = found_assets.entry(asset_guid).or_default();

            match asset_path {
                "pathname" => {
                    let mut pathname = String::new();
                    entry.read_to_string(&mut pathname)?;
                    builder.pathname = Some(pathname);
                }
                "preview.png" => {
                    let mut preview = Vec::new();
                    entry.read_to_end(&mut preview)?;
                    builder.preview = Some(preview);
                }
                "asset.meta" => {
                    let mut meta = Vec::new();
                    entry.read_to_end(&mut meta)?;
                    builder.meta = Some(meta);
                }
                "asset" => {
                    let mut data = Vec::new();
                    entry.read_to_end(&mut data)?;
                    builder.data = Some(data);
                }
                _ => {}
            }
        }
    }

    for (uuid, builder) in found_assets {
        package.assets.insert(uuid, builder.build());
    }

    Ok(package)
}

#[cfg(test)]
mod tests {
    use std::{fs::File, path::PathBuf};

    use uuid::uuid;

    use super::*;

    fn get_package_file(path: &str) -> File {
        let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        root.push(format!("tests/{}.unitypackage", path));
        File::open(root).unwrap()
    }

    #[test]
    fn asset_simple_material() {
        let package = read_package(get_package_file("simple-material")).unwrap();

        assert_eq!(package.assets.len(), 1);
        assert!(package
            .assets
            .contains_key(&uuid!("c24c6def6556015fb913fec2280e3315")));
    }

    #[test]
    fn asset_simple_cube() {
        let package = read_package(get_package_file("simple-cube")).unwrap();

        assert_eq!(package.assets.len(), 2);
        assert!(package
            .assets
            .contains_key(&uuid!("c24c6def6556015fb913fec2280e3315")));
        assert!(package
            .assets
            .contains_key(&uuid!("8109a09196ba303c59774d4f4048f48c")));
    }

    #[test]
    fn asset_invalid_uuid() {
        let package = read_package(get_package_file("invalid-uuid")).unwrap();

        assert_eq!(package.assets.len(), 0);
    }

    #[test]
    fn empty_archive() {
        let package = read_package(get_package_file("empty")).unwrap();

        assert_eq!(package.assets.len(), 0);
    }
}