Files
cursebreaker-parser-rust/src/parser/mod.rs
2025-12-31 18:40:26 +09:00

141 lines
4.1 KiB
Rust

//! Unity YAML parsing module
pub mod meta;
mod unity_tag;
mod yaml;
pub use meta::{MetaFile, get_meta_path};
pub use unity_tag::{UnityTag, parse_unity_tag};
pub use yaml::split_yaml_documents;
use crate::property::convert_yaml_value;
use crate::{Error, Result, UnityDocument, UnityFile};
use std::path::Path;
/// Parse a Unity file from the given path
///
/// # Example
///
/// ```no_run
/// use cursebreaker_parser::parser::parse_unity_file;
/// use std::path::Path;
///
/// let file = parse_unity_file(Path::new("Scene.unity"))?;
/// println!("Found {} documents", file.documents.len());
/// # Ok::<(), cursebreaker_parser::Error>(())
/// ```
pub fn parse_unity_file(path: &Path) -> Result<UnityFile> {
// Read the file
let content = std::fs::read_to_string(path)?;
// Validate Unity header
validate_unity_header(&content, path)?;
// Split into individual YAML documents
let raw_documents = split_yaml_documents(&content)?;
// Parse each document
let mut documents = Vec::new();
for raw_doc in raw_documents {
if let Some(doc) = parse_document(&raw_doc)? {
documents.push(doc);
}
}
Ok(UnityFile {
path: path.to_path_buf(),
documents,
})
}
/// Validate that the file has a proper Unity YAML header
fn validate_unity_header(content: &str, path: &Path) -> Result<()> {
let has_yaml_header = content.starts_with("%YAML");
let has_unity_tag = content.contains("%TAG !u! tag:unity3d.com");
if !has_yaml_header || !has_unity_tag {
return Err(Error::MissingHeader(path.to_path_buf()));
}
Ok(())
}
/// Parse a single YAML document into a UnityDocument
fn parse_document(raw_doc: &str) -> Result<Option<UnityDocument>> {
// Parse the Unity tag line (e.g., "--- !u!1 &12345")
let tag = match parse_unity_tag(raw_doc) {
Some(tag) => tag,
None => return Ok(None), // Skip documents without Unity tags
};
// Extract the YAML content (everything after the tag line)
let yaml_content = extract_yaml_content(raw_doc);
// Parse the YAML content
let properties = if yaml_content.trim().is_empty() {
indexmap::IndexMap::new()
} else {
match serde_yaml::from_str::<serde_yaml::Value>(yaml_content) {
Ok(serde_yaml::Value::Mapping(map)) => {
// Convert to IndexMap with PropertyValue
map.into_iter()
.filter_map(|(k, v)| {
k.as_str().and_then(|s| {
convert_yaml_value(&v)
.ok()
.map(|pv| (s.to_string(), pv))
})
})
.collect()
}
Ok(_) => indexmap::IndexMap::new(),
Err(e) => return Err(Error::Yaml(e)),
}
};
// Get class name from the first key in properties or use "Unknown"
let class_name = properties
.keys()
.next()
.map(|s| s.to_string())
.unwrap_or_else(|| format!("UnityType{}", tag.type_id));
Ok(Some(UnityDocument {
type_id: tag.type_id,
file_id: crate::types::FileID::from_i64(tag.file_id),
class_name,
properties,
}))
}
/// Extract the YAML content from a raw document (skip the Unity tag line)
fn extract_yaml_content(raw_doc: &str) -> &str {
// Find the first newline after the "--- !u!" tag
if let Some(first_line_end) = raw_doc.find('\n') {
&raw_doc[first_line_end + 1..]
} else {
""
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_unity_header() {
let valid_content = "%YAML 1.1\n%TAG !u! tag:unity3d.com,2011:\n";
assert!(validate_unity_header(valid_content, Path::new("test.unity")).is_ok());
let invalid_content = "Not a Unity file";
assert!(validate_unity_header(invalid_content, Path::new("test.unity")).is_err());
}
#[test]
fn test_extract_yaml_content() {
let raw_doc = "--- !u!1 &12345\nGameObject:\n m_Name: Test";
let content = extract_yaml_content(raw_doc);
assert_eq!(content, "GameObject:\n m_Name: Test");
}
}