meta phase 1
This commit is contained in:
@@ -38,15 +38,15 @@ pub mod types;
|
||||
// Re-exports
|
||||
pub use error::{Error, Result};
|
||||
pub use model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
|
||||
pub use parser::{meta::MetaFile, parse_unity_file};
|
||||
pub use parser::{find_project_root, meta::MetaFile, parse_unity_file, GuidResolver};
|
||||
// TODO: Re-enable once project module is updated
|
||||
// pub use project::UnityProject;
|
||||
pub use property::PropertyValue;
|
||||
pub use types::{
|
||||
get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, EcsInsertable,
|
||||
ExternalRef, FileID, FileRef, GameObject, LocalID, PrefabInstance, PrefabInstanceComponent,
|
||||
PrefabModification, PrefabResolver, Quaternion, RectTransform, Transform, TypeFilter,
|
||||
UnityComponent, UnityReference, Vector2, Vector3, yaml_helpers,
|
||||
ExternalRef, FileID, FileRef, GameObject, Guid, LocalID, PrefabInstance,
|
||||
PrefabInstanceComponent, PrefabModification, PrefabResolver, Quaternion, RectTransform,
|
||||
Transform, TypeFilter, UnityComponent, UnityReference, Vector2, Vector3, yaml_helpers,
|
||||
};
|
||||
|
||||
// Re-export the derive macro from the macro crate
|
||||
|
||||
492
cursebreaker-parser/src/parser/guid_resolver.rs
Normal file
492
cursebreaker-parser/src/parser/guid_resolver.rs
Normal file
@@ -0,0 +1,492 @@
|
||||
//! Unity GUID resolution for MonoBehaviour scripts
|
||||
//!
|
||||
//! This module resolves Unity GUIDs to their corresponding MonoBehaviour class names
|
||||
//! by scanning .meta files and parsing C# scripts.
|
||||
//!
|
||||
//! # How Unity GUID Resolution Works
|
||||
//!
|
||||
//! 1. Every asset has a `.meta` file with a unique GUID
|
||||
//! 2. MonoBehaviour components reference scripts via GUID in `m_Script`
|
||||
//! 3. We scan `.cs.meta` files to build a GUID → Script Path mapping
|
||||
//! 4. We parse the `.cs` files to extract the class name
|
||||
//! 5. Result: GUID → Class Name mapping for component registration
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use cursebreaker_parser::parser::GuidResolver;
|
||||
//! use std::path::Path;
|
||||
//!
|
||||
//! // Build resolver from Unity project directory
|
||||
//! let project_path = Path::new("path/to/UnityProject");
|
||||
//! let resolver = GuidResolver::from_project(project_path)?;
|
||||
//!
|
||||
//! // Resolve a GUID to class name
|
||||
//! let guid = "091c537484687e9419460cdcd7038234";
|
||||
//! if let Some(class_name) = resolver.resolve_class_name(guid) {
|
||||
//! println!("GUID {} → {}", guid, class_name);
|
||||
//! }
|
||||
//! # Ok::<(), cursebreaker_parser::Error>(())
|
||||
//! ```
|
||||
|
||||
use crate::parser::meta::MetaFile;
|
||||
use crate::types::Guid;
|
||||
use crate::{Error, Result};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Resolves Unity GUIDs to MonoBehaviour class names
|
||||
///
|
||||
/// This struct builds a mapping from GUID to class name by scanning
|
||||
/// a Unity project's `.cs.meta` files and parsing the corresponding
|
||||
/// C# scripts to extract class names.
|
||||
///
|
||||
/// Uses a 128-bit `Guid` type for efficient storage and fast comparisons.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GuidResolver {
|
||||
/// Map from GUID to MonoBehaviour class name
|
||||
guid_to_class: HashMap<Guid, String>,
|
||||
}
|
||||
|
||||
impl GuidResolver {
|
||||
/// Create a new empty GuidResolver
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
guid_to_class: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a GuidResolver by scanning a Unity project directory
|
||||
///
|
||||
/// This scans for all `.cs.meta` files (MonoBehaviour scripts),
|
||||
/// parses them to extract GUIDs, then parses the corresponding
|
||||
/// C# files to extract class names.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use cursebreaker_parser::parser::GuidResolver;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let resolver = GuidResolver::from_project(Path::new("MyUnityProject"))?;
|
||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||
/// ```
|
||||
pub fn from_project(project_path: impl AsRef<Path>) -> Result<Self> {
|
||||
let project_path = project_path.as_ref();
|
||||
|
||||
// Verify this looks like a Unity project
|
||||
let assets_dir = project_path.join("Assets");
|
||||
if !assets_dir.exists() {
|
||||
return Err(Error::invalid_format(format!(
|
||||
"Not a Unity project: missing Assets/ directory at {}",
|
||||
project_path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut resolver = Self::new();
|
||||
|
||||
// Walk the Assets directory looking for .cs.meta files
|
||||
for entry in WalkDir::new(&assets_dir)
|
||||
.follow_links(false)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let path = entry.path();
|
||||
|
||||
// Only process .cs.meta files (MonoBehaviour scripts)
|
||||
if !is_cs_meta_file(path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the .meta file to get the GUID
|
||||
let meta = match MetaFile::from_path(path) {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to parse {}: {}", path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the corresponding .cs file path
|
||||
let cs_path = path.with_file_name(
|
||||
path.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.and_then(|s| s.strip_suffix(".meta"))
|
||||
.unwrap_or(""),
|
||||
);
|
||||
|
||||
// Extract the class name from the .cs file
|
||||
let class_name = match extract_class_name(&cs_path) {
|
||||
Ok(Some(name)) => name,
|
||||
Ok(None) => {
|
||||
// No MonoBehaviour class found in this file
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: Failed to extract class name from {}: {}",
|
||||
cs_path.display(),
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse the GUID string to Guid type
|
||||
let guid = match Guid::from_hex(meta.guid()) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: Invalid GUID in {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Store the mapping
|
||||
resolver.guid_to_class.insert(guid, class_name);
|
||||
}
|
||||
|
||||
Ok(resolver)
|
||||
}
|
||||
|
||||
/// Resolve a GUID to its class name
|
||||
///
|
||||
/// Accepts either a `&Guid` or a string that can be parsed as a GUID.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `guid` - The Unity GUID (e.g., "091c537484687e9419460cdcd7038234" or a `Guid`)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The class name if the GUID is found, otherwise None
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use cursebreaker_parser::{GuidResolver, Guid};
|
||||
/// # use std::path::Path;
|
||||
/// # let resolver = GuidResolver::from_project(Path::new("."))?;
|
||||
/// // Resolve by string
|
||||
/// if let Some(class_name) = resolver.resolve_class_name("091c537484687e9419460cdcd7038234") {
|
||||
/// println!("Found class: {}", class_name);
|
||||
/// }
|
||||
///
|
||||
/// // Resolve by Guid
|
||||
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234")?;
|
||||
/// if let Some(class_name) = resolver.resolve_class_name(&guid) {
|
||||
/// println!("Found class: {}", class_name);
|
||||
/// }
|
||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||
/// ```
|
||||
pub fn resolve_class_name<G: AsGuid>(&self, guid: G) -> Option<&str> {
|
||||
guid.as_guid()
|
||||
.and_then(|g| self.guid_to_class.get(&g))
|
||||
.map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Get the number of GUID mappings
|
||||
pub fn len(&self) -> usize {
|
||||
self.guid_to_class.len()
|
||||
}
|
||||
|
||||
/// Check if the resolver is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.guid_to_class.is_empty()
|
||||
}
|
||||
|
||||
/// Insert a GUID → class name mapping manually
|
||||
///
|
||||
/// Useful for testing or adding custom mappings
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::{GuidResolver, Guid};
|
||||
///
|
||||
/// let mut resolver = GuidResolver::new();
|
||||
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
/// resolver.insert(guid, "PlaySFX".to_string());
|
||||
/// ```
|
||||
pub fn insert(&mut self, guid: Guid, class_name: String) {
|
||||
self.guid_to_class.insert(guid, class_name);
|
||||
}
|
||||
|
||||
/// Get all resolved GUIDs
|
||||
///
|
||||
/// Returns an iterator over all GUIDs in the resolver.
|
||||
pub fn guids(&self) -> impl Iterator<Item = Guid> + '_ {
|
||||
self.guid_to_class.keys().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GuidResolver {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for types that can be converted to a GUID for lookup
|
||||
///
|
||||
/// This allows the `resolve_class_name` method to accept both `&Guid` and `&str`.
|
||||
pub trait AsGuid {
|
||||
/// Convert to a GUID, returning None if conversion fails
|
||||
fn as_guid(&self) -> Option<Guid>;
|
||||
}
|
||||
|
||||
impl AsGuid for Guid {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Some(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for &Guid {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Some(**self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for str {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Guid::from_hex(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for &str {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Guid::from_hex(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for String {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Guid::from_hex(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a path points to a C# .meta file
|
||||
fn is_cs_meta_file(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|ext| ext == "meta")
|
||||
.unwrap_or(false)
|
||||
&& path
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|name| name.ends_with(".cs.meta"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Extract the MonoBehaviour class name from a C# script file
|
||||
///
|
||||
/// This looks for patterns like:
|
||||
/// - `public class ClassName : MonoBehaviour`
|
||||
/// - `class ClassName : MonoBehaviour`
|
||||
/// - `public class ClassName:MonoBehaviour`
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cs_path` - Path to the .cs file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(Some(class_name)) if a MonoBehaviour class was found
|
||||
/// Ok(None) if no MonoBehaviour class was found
|
||||
/// Err if the file couldn't be read
|
||||
fn extract_class_name(cs_path: &Path) -> Result<Option<String>> {
|
||||
let content = std::fs::read_to_string(cs_path)?;
|
||||
|
||||
// Regex to match MonoBehaviour class declarations
|
||||
// Matches: public class ClassName : MonoBehaviour
|
||||
// Also matches: class ClassName:MonoBehaviour (no space)
|
||||
// Captures the class name in group 1
|
||||
let class_regex = Regex::new(
|
||||
r"(?:public\s+)?class\s+(\w+)\s*:\s*MonoBehaviour"
|
||||
).unwrap();
|
||||
|
||||
// Find the first MonoBehaviour class in the file
|
||||
if let Some(captures) = class_regex.captures(&content) {
|
||||
if let Some(class_name) = captures.get(1) {
|
||||
return Ok(Some(class_name.as_str().to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Find the Unity project root from any path within the project
|
||||
///
|
||||
/// Searches upward from the given path until it finds a directory
|
||||
/// containing an "Assets" folder.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Any path within the Unity project (file or directory)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The project root path if found, otherwise an error
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use cursebreaker_parser::parser::find_project_root;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let scene_path = Path::new("MyProject/Assets/Scenes/Main.unity");
|
||||
/// let project_root = find_project_root(scene_path)?;
|
||||
/// assert_eq!(project_root.file_name().unwrap(), "MyProject");
|
||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||
/// ```
|
||||
pub fn find_project_root(path: impl AsRef<Path>) -> Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
|
||||
// Start from the file's directory (or the directory itself)
|
||||
let mut current = if path.is_file() {
|
||||
path.parent().ok_or_else(|| {
|
||||
Error::invalid_format("Cannot get parent directory")
|
||||
})?
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
// Search upward for Assets/ directory
|
||||
loop {
|
||||
let assets_dir = current.join("Assets");
|
||||
if assets_dir.exists() && assets_dir.is_dir() {
|
||||
return Ok(current.to_path_buf());
|
||||
}
|
||||
|
||||
// Move up one directory
|
||||
current = current.parent().ok_or_else(|| {
|
||||
Error::invalid_format(format!(
|
||||
"Could not find Unity project root from {}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_cs_meta_file() {
|
||||
assert!(is_cs_meta_file(Path::new("PlaySFX.cs.meta")));
|
||||
assert!(is_cs_meta_file(Path::new("Assets/Scripts/PlaySFX.cs.meta")));
|
||||
assert!(!is_cs_meta_file(Path::new("PlaySFX.cs")));
|
||||
assert!(!is_cs_meta_file(Path::new("Scene.unity.meta")));
|
||||
assert!(!is_cs_meta_file(Path::new("texture.png.meta")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_class_name() {
|
||||
// Test standard MonoBehaviour class
|
||||
let content = r#"
|
||||
using UnityEngine;
|
||||
|
||||
public class PlaySFX : MonoBehaviour
|
||||
{
|
||||
public float volume = 1.0f;
|
||||
}
|
||||
"#;
|
||||
let temp_file = std::env::temp_dir().join("test_playsfx.cs");
|
||||
std::fs::write(&temp_file, content).unwrap();
|
||||
let result = extract_class_name(&temp_file).unwrap();
|
||||
assert_eq!(result, Some("PlaySFX".to_string()));
|
||||
std::fs::remove_file(&temp_file).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_class_name_no_public() {
|
||||
// Test without public modifier
|
||||
let content = r#"
|
||||
using UnityEngine;
|
||||
|
||||
class MyBehaviour : MonoBehaviour
|
||||
{
|
||||
void Start() {}
|
||||
}
|
||||
"#;
|
||||
let temp_file = std::env::temp_dir().join("test_mybehaviour.cs");
|
||||
std::fs::write(&temp_file, content).unwrap();
|
||||
let result = extract_class_name(&temp_file).unwrap();
|
||||
assert_eq!(result, Some("MyBehaviour".to_string()));
|
||||
std::fs::remove_file(&temp_file).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_class_name_no_space() {
|
||||
// Test with no space after colon
|
||||
let content = r#"
|
||||
using UnityEngine;
|
||||
|
||||
public class TestScript:MonoBehaviour
|
||||
{
|
||||
void Update() {}
|
||||
}
|
||||
"#;
|
||||
let temp_file = std::env::temp_dir().join("test_testscript.cs");
|
||||
std::fs::write(&temp_file, content).unwrap();
|
||||
let result = extract_class_name(&temp_file).unwrap();
|
||||
assert_eq!(result, Some("TestScript".to_string()));
|
||||
std::fs::remove_file(&temp_file).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_class_name_not_monobehaviour() {
|
||||
// Test class that doesn't inherit from MonoBehaviour
|
||||
let content = r#"
|
||||
using UnityEngine;
|
||||
|
||||
public class HelperClass
|
||||
{
|
||||
public int value;
|
||||
}
|
||||
"#;
|
||||
let temp_file = std::env::temp_dir().join("test_helper.cs");
|
||||
std::fs::write(&temp_file, content).unwrap();
|
||||
let result = extract_class_name(&temp_file).unwrap();
|
||||
assert_eq!(result, None);
|
||||
std::fs::remove_file(&temp_file).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_resolver_manual() {
|
||||
let mut resolver = GuidResolver::new();
|
||||
assert!(resolver.is_empty());
|
||||
|
||||
let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
resolver.insert(guid, "PlaySFX".to_string());
|
||||
|
||||
assert_eq!(resolver.len(), 1);
|
||||
|
||||
// Test resolving by string
|
||||
assert_eq!(
|
||||
resolver.resolve_class_name("091c537484687e9419460cdcd7038234"),
|
||||
Some("PlaySFX")
|
||||
);
|
||||
|
||||
// Test resolving by Guid
|
||||
assert_eq!(
|
||||
resolver.resolve_class_name(&guid),
|
||||
Some("PlaySFX")
|
||||
);
|
||||
|
||||
// Test nonexistent GUID
|
||||
assert_eq!(
|
||||
resolver.resolve_class_name("00000000000000000000000000000000"),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
//! Unity YAML parsing module
|
||||
|
||||
pub mod guid_resolver;
|
||||
pub mod meta;
|
||||
mod unity_tag;
|
||||
mod yaml;
|
||||
|
||||
pub use guid_resolver::{find_project_root, GuidResolver};
|
||||
pub use meta::{get_meta_path, MetaFile};
|
||||
pub use unity_tag::{parse_unity_tag, UnityTag};
|
||||
pub use yaml::split_yaml_documents;
|
||||
|
||||
255
cursebreaker-parser/src/types/guid.rs
Normal file
255
cursebreaker-parser/src/types/guid.rs
Normal file
@@ -0,0 +1,255 @@
|
||||
//! Unity GUID type
|
||||
//!
|
||||
//! Unity uses 128-bit GUIDs to uniquely identify assets across a project.
|
||||
//! GUIDs are stored as 32 hexadecimal characters (e.g., "091c537484687e9419460cdcd7038234").
|
||||
//!
|
||||
//! This module provides a type-safe, efficient representation of Unity GUIDs
|
||||
//! using a 128-bit integer for fast comparisons and minimal memory footprint.
|
||||
|
||||
use crate::{Error, Result};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// A Unity GUID represented as a 128-bit integer
|
||||
///
|
||||
/// Unity GUIDs are 32 hexadecimal characters representing a 128-bit value.
|
||||
/// This type stores the GUID as a `u128` for efficient storage and comparison.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::Guid;
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// // Parse from string
|
||||
/// let guid = Guid::from_str("091c537484687e9419460cdcd7038234").unwrap();
|
||||
///
|
||||
/// // Convert back to string
|
||||
/// assert_eq!(guid.to_string(), "091c537484687e9419460cdcd7038234");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Guid(u128);
|
||||
|
||||
impl Guid {
|
||||
/// Create a new GUID from a 128-bit integer
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::Guid;
|
||||
///
|
||||
/// let guid = Guid::from_u128(0x091c537484687e9419460cdcd7038234);
|
||||
/// ```
|
||||
pub const fn from_u128(value: u128) -> Self {
|
||||
Guid(value)
|
||||
}
|
||||
|
||||
/// Get the underlying 128-bit integer value
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::Guid;
|
||||
///
|
||||
/// let guid = Guid::from_u128(42);
|
||||
/// assert_eq!(guid.as_u128(), 42);
|
||||
/// ```
|
||||
pub const fn as_u128(&self) -> u128 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Create a GUID from a hex string
|
||||
///
|
||||
/// The string must be exactly 32 hexadecimal characters.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the string is not valid hex or not 32 characters.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::Guid;
|
||||
///
|
||||
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
/// ```
|
||||
pub fn from_hex(s: &str) -> Result<Self> {
|
||||
// Unity GUIDs are always 32 hex characters (128 bits)
|
||||
if s.len() != 32 {
|
||||
return Err(Error::invalid_format(format!(
|
||||
"GUID must be 32 hex characters, got {}",
|
||||
s.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// Parse hex string to u128
|
||||
let value = u128::from_str_radix(s, 16).map_err(|e| {
|
||||
Error::invalid_format(format!("Invalid hex in GUID '{}': {}", s, e))
|
||||
})?;
|
||||
|
||||
Ok(Guid(value))
|
||||
}
|
||||
|
||||
/// Convert the GUID to a lowercase hex string
|
||||
///
|
||||
/// Returns a 32-character hex string representing the GUID.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::Guid;
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// let guid = Guid::from_str("091c537484687e9419460cdcd7038234").unwrap();
|
||||
/// assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234");
|
||||
/// ```
|
||||
pub fn to_hex(&self) -> String {
|
||||
format!("{:032x}", self.0)
|
||||
}
|
||||
|
||||
/// A zero GUID (all bits set to 0)
|
||||
pub const ZERO: Guid = Guid(0);
|
||||
}
|
||||
|
||||
impl FromStr for Guid {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
Self::from_hex(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Guid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:032x}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u128> for Guid {
|
||||
fn from(value: u128) -> Self {
|
||||
Guid(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Guid> for u128 {
|
||||
fn from(guid: Guid) -> Self {
|
||||
guid.0
|
||||
}
|
||||
}
|
||||
|
||||
// Implement serde serialization if serde feature is enabled
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for Guid {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_hex())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for Guid {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Guid::from_hex(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_guid_from_hex() {
|
||||
let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_from_str() {
|
||||
let guid: Guid = "091c537484687e9419460cdcd7038234".parse().unwrap();
|
||||
assert_eq!(guid.to_string(), "091c537484687e9419460cdcd7038234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_invalid_length() {
|
||||
assert!(Guid::from_hex("123").is_err());
|
||||
assert!(Guid::from_hex("091c537484687e9419460cdcd70382341234").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_invalid_hex() {
|
||||
assert!(Guid::from_hex("091c537484687e9419460cdcd703823g").is_err());
|
||||
assert!(Guid::from_hex("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_zero() {
|
||||
let zero = Guid::ZERO;
|
||||
assert_eq!(zero.as_u128(), 0);
|
||||
assert_eq!(zero.to_hex(), "00000000000000000000000000000000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_from_u128() {
|
||||
let value: u128 = 0x091c537484687e9419460cdcd7038234;
|
||||
let guid = Guid::from_u128(value);
|
||||
assert_eq!(guid.as_u128(), value);
|
||||
assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_equality() {
|
||||
let guid1 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
let guid2 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
let guid3 = Guid::from_hex("091c537484687e9419460cdcd7038235").unwrap();
|
||||
|
||||
assert_eq!(guid1, guid2);
|
||||
assert_ne!(guid1, guid3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_hash() {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut set = HashSet::new();
|
||||
let guid1 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
let guid2 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
let guid3 = Guid::from_hex("091c537484687e9419460cdcd7038235").unwrap();
|
||||
|
||||
set.insert(guid1);
|
||||
set.insert(guid2); // Duplicate, shouldn't increase size
|
||||
set.insert(guid3);
|
||||
|
||||
assert_eq!(set.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_ordering() {
|
||||
let guid1 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
let guid2 = Guid::from_hex("091c537484687e9419460cdcd7038235").unwrap();
|
||||
|
||||
assert!(guid1 < guid2);
|
||||
assert!(guid2 > guid1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_uppercase_hex() {
|
||||
// Unity GUIDs are lowercase, but we should handle uppercase too
|
||||
let guid = Guid::from_hex("091C537484687E9419460CDCD7038234").unwrap();
|
||||
// Output is always lowercase
|
||||
assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_conversion() {
|
||||
let value: u128 = 12345678901234567890;
|
||||
let guid: Guid = value.into();
|
||||
let back: u128 = guid.into();
|
||||
assert_eq!(value, back);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
mod component;
|
||||
mod game_object;
|
||||
mod guid;
|
||||
mod ids;
|
||||
mod prefab_instance;
|
||||
mod reference;
|
||||
@@ -19,6 +20,7 @@ pub use component::{
|
||||
LinkingContext, UnityComponent,
|
||||
};
|
||||
pub use game_object::GameObject;
|
||||
pub use guid::Guid;
|
||||
pub use ids::{FileID, LocalID};
|
||||
pub use prefab_instance::{
|
||||
PrefabInstance, PrefabInstanceComponent, PrefabModification, PrefabResolver,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Integration tests for parsing real Unity projects
|
||||
|
||||
use cursebreaker_parser::UnityFile;
|
||||
use cursebreaker_parser::{GuidResolver, UnityFile};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Instant;
|
||||
@@ -400,3 +400,77 @@ fn benchmark_parsing() {
|
||||
|
||||
stats.print_summary();
|
||||
}
|
||||
|
||||
/// Test GUID resolution for MonoBehaviour scripts
|
||||
#[test]
|
||||
fn test_guid_resolution() {
|
||||
let project_path = match clone_test_project(&TestProject::VR_HORROR) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to clone project: {}", e);
|
||||
eprintln!("Skipping GUID resolution test (git may not be available)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!("Testing GUID Resolution");
|
||||
println!("{}", "=".repeat(60));
|
||||
|
||||
// Build GUID resolver from project
|
||||
println!("\nBuilding GuidResolver from project...");
|
||||
let resolver = GuidResolver::from_project(&project_path)
|
||||
.expect("Should successfully build GuidResolver");
|
||||
|
||||
println!(" ✓ Found {} GUID mappings", resolver.len());
|
||||
assert!(
|
||||
!resolver.is_empty(),
|
||||
"Should find at least some C# scripts with GUIDs"
|
||||
);
|
||||
|
||||
// Test the known PlaySFX GUID from the roadmap
|
||||
let playsfx_guid_str = "091c537484687e9419460cdcd7038234";
|
||||
println!("\nTesting known GUID resolution:");
|
||||
println!(" GUID: {}", playsfx_guid_str);
|
||||
|
||||
// Test resolution by string
|
||||
match resolver.resolve_class_name(playsfx_guid_str) {
|
||||
Some(class_name) => {
|
||||
println!(" ✓ Resolved by string to: {}", class_name);
|
||||
assert_eq!(
|
||||
class_name, "PlaySFX",
|
||||
"PlaySFX GUID should resolve to 'PlaySFX' class name"
|
||||
);
|
||||
}
|
||||
None => {
|
||||
panic!("Failed to resolve PlaySFX GUID. Available GUIDs: {:?}",
|
||||
resolver.guids().take(5).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
// Test resolution by Guid type
|
||||
use cursebreaker_parser::Guid;
|
||||
let playsfx_guid = Guid::from_hex(playsfx_guid_str).unwrap();
|
||||
match resolver.resolve_class_name(&playsfx_guid) {
|
||||
Some(class_name) => {
|
||||
println!(" ✓ Resolved by Guid type to: {}", class_name);
|
||||
assert_eq!(class_name, "PlaySFX");
|
||||
}
|
||||
None => panic!("Failed to resolve PlaySFX GUID by Guid type"),
|
||||
}
|
||||
|
||||
// Show sample of resolved GUIDs
|
||||
println!("\nSample of resolved GUIDs:");
|
||||
for guid in resolver.guids().take(5) {
|
||||
if let Some(class_name) = resolver.resolve_class_name(&guid) {
|
||||
println!(" {} → {}", guid, class_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrate memory efficiency
|
||||
println!("\nMemory efficiency:");
|
||||
println!(" Each Guid: {} bytes (u128)", std::mem::size_of::<Guid>());
|
||||
println!(" Each String GUID: ~{} bytes (heap allocated)", 32 + std::mem::size_of::<String>());
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user