unity-asset 0.2.0

A comprehensive Rust library for parsing Unity asset files (YAML and binary formats)
Documentation
use std::path::PathBuf;
use std::str::FromStr;

use super::{BinaryObjectKey, BinarySource, BinarySourceKind};

impl std::fmt::Display for BinaryObjectKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let kind = match self.source_kind {
            BinarySourceKind::SerializedFile => "serialized",
            BinarySourceKind::AssetBundle => "bundle",
        };
        let asset_index = self
            .asset_index
            .map(|i| i.to_string())
            .unwrap_or_else(|| "-".to_string());

        let (outer, entry) = match &self.source {
            BinarySource::Path(p) => (p.to_string_lossy().to_string(), String::new()),
            BinarySource::WebEntry {
                web_path,
                entry_name,
            } => (web_path.to_string_lossy().to_string(), entry_name.clone()),
        };
        write!(
            f,
            "bok2|{}|{}|{}|{}|{}|{}|{}",
            kind,
            asset_index,
            self.path_id,
            outer.len(),
            outer,
            entry.len(),
            entry
        )
    }
}

impl FromStr for BinaryObjectKey {
    type Err = String;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        if s.starts_with("bok2|") {
            return parse_bok2(s);
        }
        if s.starts_with("bok1|") {
            return parse_bok1(s);
        }
        Err("invalid key prefix (expected: bok1|... or bok2|...)".to_string())
    }
}

fn parse_kind(kind: &str) -> std::result::Result<BinarySourceKind, String> {
    match kind {
        "bundle" => Ok(BinarySourceKind::AssetBundle),
        "serialized" => Ok(BinarySourceKind::SerializedFile),
        other => Err(format!("unknown kind: {}", other)),
    }
}

fn parse_asset_index(asset_index: &str) -> std::result::Result<Option<usize>, String> {
    if asset_index == "-" || asset_index.is_empty() {
        return Ok(None);
    }
    Ok(Some(
        asset_index
            .parse::<usize>()
            .map_err(|e| format!("invalid asset_index: {}", e))?,
    ))
}

fn parse_bok1(s: &str) -> std::result::Result<BinaryObjectKey, String> {
    let prefix = "bok1|";
    let mut rest = &s[prefix.len()..];
    let (kind, r) = split_once(rest, '|').ok_or_else(|| "missing kind".to_string())?;
    rest = r;
    let (asset_index, r) =
        split_once(rest, '|').ok_or_else(|| "missing asset_index".to_string())?;
    rest = r;
    let (path_id, r) = split_once(rest, '|').ok_or_else(|| "missing path_id".to_string())?;
    rest = r;
    let (path_len, path) =
        split_once(rest, '|').ok_or_else(|| "missing path_len/path".to_string())?;

    let source_kind = parse_kind(kind)?;
    let asset_index = parse_asset_index(asset_index)?;
    let path_id = path_id
        .parse::<i64>()
        .map_err(|e| format!("invalid path_id: {}", e))?;

    let expected_len = path_len
        .parse::<usize>()
        .map_err(|e| format!("invalid path_len: {}", e))?;
    if path.len() != expected_len {
        return Err(format!(
            "path length mismatch: expected {} bytes, got {} bytes",
            expected_len,
            path.len()
        ));
    }

    if source_kind == BinarySourceKind::AssetBundle && asset_index.is_none() {
        return Err("asset_index is required for bundle keys".to_string());
    }

    Ok(BinaryObjectKey {
        source: BinarySource::Path(PathBuf::from(path)),
        source_kind,
        asset_index,
        path_id,
    })
}

fn parse_bok2(s: &str) -> std::result::Result<BinaryObjectKey, String> {
    let prefix = "bok2|";
    let mut rest = &s[prefix.len()..];

    let (kind, r) = split_once(rest, '|').ok_or_else(|| "missing kind".to_string())?;
    rest = r;
    let (asset_index, r) =
        split_once(rest, '|').ok_or_else(|| "missing asset_index".to_string())?;
    rest = r;
    let (path_id, r) = split_once(rest, '|').ok_or_else(|| "missing path_id".to_string())?;
    rest = r;
    let (outer_len, r) = split_once(rest, '|').ok_or_else(|| "missing outer_len".to_string())?;
    rest = r;

    let source_kind = parse_kind(kind)?;
    let asset_index = parse_asset_index(asset_index)?;
    let path_id = path_id
        .parse::<i64>()
        .map_err(|e| format!("invalid path_id: {}", e))?;

    let outer_len = outer_len
        .parse::<usize>()
        .map_err(|e| format!("invalid outer_len: {}", e))?;
    if rest.len() < outer_len {
        return Err("outer is shorter than outer_len".to_string());
    }

    let outer = rest
        .get(..outer_len)
        .ok_or_else(|| "outer_len splits UTF-8 boundary".to_string())?;
    let rest = rest
        .get(outer_len..)
        .ok_or_else(|| "outer_len splits UTF-8 boundary".to_string())?;

    let rest = rest
        .strip_prefix('|')
        .ok_or_else(|| "missing entry delimiter".to_string())?;
    let (entry_len, rest) = split_once(rest, '|').ok_or_else(|| "missing entry_len".to_string())?;
    let entry_len = entry_len
        .parse::<usize>()
        .map_err(|e| format!("invalid entry_len: {}", e))?;
    if rest.len() != entry_len {
        return Err(format!(
            "entry length mismatch: expected {} bytes, got {} bytes",
            entry_len,
            rest.len()
        ));
    }

    if source_kind == BinarySourceKind::AssetBundle && asset_index.is_none() {
        return Err("asset_index is required for bundle keys".to_string());
    }

    let source = if entry_len == 0 {
        BinarySource::Path(PathBuf::from(outer))
    } else {
        BinarySource::WebEntry {
            web_path: PathBuf::from(outer),
            entry_name: rest.to_string(),
        }
    };

    Ok(BinaryObjectKey {
        source,
        source_kind,
        asset_index,
        path_id,
    })
}

fn split_once(s: &str, delim: char) -> Option<(&str, &str)> {
    let pos = s.find(delim)?;
    Some((&s[..pos], &s[pos + delim.len_utf8()..]))
}