use anyhow::Result;
#[derive(Debug, Clone, Default)]
pub struct AssetInfo {
pub top_class_id: Option<u32>,
pub top_file_id: Option<i64>,
pub script_guid: Option<u128>,
pub sub_assets: Vec<SubAssetEntry>,
}
#[derive(Debug, Clone)]
pub struct SubAssetEntry {
pub class_id: u32,
pub file_id: i64,
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseMode {
TopOnly,
WithSubAssets,
}
pub fn parse(text: &str, mode: ParseMode) -> Result<AssetInfo> {
let mut info = AssetInfo::default();
struct DocAccum {
class_id: u32,
file_id: i64,
name: Option<String>,
script_guid: Option<u128>,
}
let mut doc_idx: usize = 0;
let mut cur: Option<DocAccum> = None;
let flush = |info: &mut AssetInfo, doc_idx: usize, d: DocAccum| {
if doc_idx == 0 {
info.top_class_id = Some(d.class_id);
info.top_file_id = Some(d.file_id);
info.script_guid = d.script_guid;
} else {
info.sub_assets.push(SubAssetEntry {
class_id: d.class_id,
file_id: d.file_id,
name: d.name.unwrap_or_default(),
});
}
};
for line in text.lines() {
if let Some((cls, fid)) = parse_doc_header(line) {
if let Some(d) = cur.take() {
flush(&mut info, doc_idx, d);
doc_idx += 1;
if mode == ParseMode::TopOnly {
return Ok(info);
}
}
cur = Some(DocAccum {
class_id: cls,
file_id: fid,
name: None,
script_guid: None,
});
continue;
}
let Some(d) = cur.as_mut() else { continue };
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("m_Name:") {
if d.name.is_none() {
let s = rest.trim();
if !s.is_empty() {
d.name = Some(s.to_string());
}
}
} else if d.script_guid.is_none()
&& let Some(rest) = trimmed.strip_prefix("m_Script:")
{
d.script_guid = parse_inline_guid(rest);
}
}
if let Some(d) = cur.take() {
flush(&mut info, doc_idx, d);
}
Ok(info)
}
fn parse_doc_header(line: &str) -> Option<(u32, i64)> {
let rest = line.strip_prefix("--- !u!")?;
let (cls_str, after) = rest.split_once(" &")?;
let cls: u32 = cls_str.trim().parse().ok()?;
let fid_str = after.split_whitespace().next()?;
let fid: i64 = fid_str.parse().ok()?;
Some((cls, fid))
}
fn parse_inline_guid(rest: &str) -> Option<u128> {
let s = rest.trim();
let s = s.trim_start_matches('{').trim_end_matches('}');
for part in s.split(',') {
let part = part.trim();
if let Some(hex) = part.strip_prefix("guid:") {
let hex = hex.trim();
if hex.len() == 32 {
return u128::from_str_radix(hex, 16).ok();
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_top_only() {
let text = "%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1001 &100100000
PrefabInstance:
m_ObjectHideFlags: 0
";
let info = parse(text, ParseMode::WithSubAssets).unwrap();
assert_eq!(info.top_class_id, Some(1001));
assert_eq!(info.top_file_id, Some(100100000));
assert!(info.sub_assets.is_empty());
}
#[test]
fn parses_monobehaviour_with_script_guid() {
let text = "--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_Script: {fileID: 11500000, guid: 7d602c2080b53413fa393df6b2c0af43, type: 3}
m_Name: TweenSeqDef
";
let info = parse(text, ParseMode::WithSubAssets).unwrap();
assert_eq!(info.top_class_id, Some(114));
assert_eq!(
info.script_guid,
Some(0x7d602c2080b53413fa393df6b2c0af43_u128)
);
}
#[test]
fn top_only_skips_sub_docs() {
let text = "--- !u!28 &2800000
Texture2D:
m_Name: Sheet
--- !u!213 &21300000
Sprite:
m_Name: spr_a
";
let info = parse(text, ParseMode::TopOnly).unwrap();
assert_eq!(info.top_class_id, Some(28));
assert!(info.sub_assets.is_empty());
}
#[test]
fn parses_sub_assets() {
let text = "--- !u!28 &2800000
Texture2D:
m_Name: Sheet
--- !u!213 &21300000
Sprite:
m_Name: spr_a
--- !u!213 &21300002
Sprite:
m_Name: spr_b
";
let info = parse(text, ParseMode::WithSubAssets).unwrap();
assert_eq!(info.top_class_id, Some(28));
assert_eq!(info.sub_assets.len(), 2);
assert_eq!(info.sub_assets[0].file_id, 21300000);
assert_eq!(info.sub_assets[0].name, "spr_a");
assert_eq!(info.sub_assets[1].name, "spr_b");
}
#[test]
fn parses_keeps_all_named_subdocs_regardless_of_class() {
let text = "--- !u!114 &11400000
MonoBehaviour:
m_Name: TimelineAsset
--- !u!114 &-7938135556022269506
MonoBehaviour:
m_Name: 'Animation Track (1)'
--- !u!1 &111111
GameObject:
m_Name: '@SomeGo'
--- !u!74 &-444444
AnimationClip:
m_Name: EmbeddedClip
";
let info = parse(text, ParseMode::WithSubAssets).unwrap();
assert_eq!(info.sub_assets.len(), 3);
assert_eq!(info.sub_assets[0].name, "'Animation Track (1)'");
assert_eq!(info.sub_assets[1].name, "'@SomeGo'");
assert_eq!(info.sub_assets[2].name, "EmbeddedClip");
}
}