unity_asset_binary/metadata/
mod.rs

1//! Unity asset metadata processing module
2//!
3//! This module provides comprehensive metadata extraction and analysis capabilities
4//! for Unity assets, organized following best practices for maintainability.
5//!
6//! # Architecture
7//!
8//! The module is organized into several sub-modules:
9//! - `types` - Core data structures for metadata representation
10//! - `extractor` - Main metadata extraction functionality
11//! - `analyzer` - Advanced dependency and relationship analysis
12//!
13//! # Examples
14//!
15//! ```rust,no_run
16//! use unity_asset_binary::metadata::{MetadataExtractor, ExtractionConfig};
17//! use unity_asset_binary::asset::{SerializedFile, SerializedFileHeader};
18//!
19//! // Create extractor with custom configuration
20//! let config = ExtractionConfig {
21//!     include_dependencies: true,
22//!     include_hierarchy: true,
23//!     max_objects: Some(1000),
24//!     include_performance: true,
25//!     include_object_details: true,
26//! };
27//! let extractor = MetadataExtractor::with_config(config);
28//!
29//! // Note: In real usage, you would load a SerializedFile from actual data
30//! // For demonstration, we'll just show the extractor creation
31//! println!("Extractor created with config");
32//! println!("Metadata extracted successfully");
33//! # Ok::<(), unity_asset_binary::error::BinaryError>(())
34//! ```
35
36pub mod analyzer;
37pub mod extractor;
38pub mod types;
39
40// Re-export main types for easy access
41pub use analyzer::{DependencyAnalyzer, RelationshipAnalyzer};
42pub use extractor::MetadataExtractor;
43pub use types::{
44    // Core metadata types
45    AssetMetadata,
46    AssetReference,
47    // Relationship types
48    AssetRelationships,
49    ComponentRelationship,
50    DependencyGraph,
51    // Dependency types
52    DependencyInfo,
53    ExternalObjectRef,
54    ExternalReference,
55    ExtractionConfig,
56    ExtractionResult,
57    ExtractionStats,
58    FileInfo,
59    GameObjectHierarchy,
60    InternalReference,
61    MemoryUsage,
62    ObjectStatistics,
63    ObjectSummary,
64    // Performance and configuration
65    PerformanceMetrics,
66    // Constants
67    class_ids,
68};
69
70use crate::asset::SerializedFile;
71use crate::bundle::AssetBundle;
72
73/// Main metadata processing facade
74///
75/// This struct provides a high-level interface for metadata processing,
76/// combining extraction and analysis functionality.
77pub struct MetadataProcessor {
78    extractor: MetadataExtractor,
79    dependency_analyzer: Option<DependencyAnalyzer>,
80    relationship_analyzer: Option<RelationshipAnalyzer>,
81}
82
83fn apply_dependency_info_to_relationships(
84    dependencies: &DependencyInfo,
85    relationships: &mut AssetRelationships,
86) {
87    let mut by_from: std::collections::HashMap<i64, Vec<i64>> = std::collections::HashMap::new();
88    for r in &dependencies.internal_references {
89        by_from.entry(r.from_object).or_default().push(r.to_object);
90    }
91    for v in by_from.values_mut() {
92        v.sort_unstable();
93        v.dedup();
94    }
95
96    let mut by_from_external: std::collections::HashMap<i64, Vec<ExternalObjectRef>> =
97        std::collections::HashMap::new();
98    for r in &dependencies.external_references {
99        let ext = ExternalObjectRef {
100            file_id: r.file_id,
101            path_id: r.path_id,
102            file_path: r.file_path.clone(),
103            guid: r.guid,
104        };
105        for from in &r.referenced_by {
106            by_from_external.entry(*from).or_default().push(ext.clone());
107        }
108    }
109    for v in by_from_external.values_mut() {
110        v.sort_by_key(|e| (e.file_id, e.path_id));
111        v.dedup_by_key(|e| (e.file_id, e.path_id));
112    }
113
114    for rel in &mut relationships.component_relationships {
115        rel.dependencies = by_from.get(&rel.component_id).cloned().unwrap_or_default();
116        rel.external_dependencies = by_from_external
117            .get(&rel.component_id)
118            .cloned()
119            .unwrap_or_default();
120    }
121
122    // Build asset reference summary (internal targets + external targets).
123    let gameobject_ids: std::collections::HashSet<i64> = relationships
124        .gameobject_hierarchy
125        .iter()
126        .map(|h| h.gameobject_id)
127        .collect();
128    let component_ids: std::collections::HashSet<i64> = relationships
129        .component_relationships
130        .iter()
131        .map(|c| c.component_id)
132        .collect();
133
134    let mut referenced_by_internal: std::collections::HashMap<i64, Vec<i64>> =
135        std::collections::HashMap::new();
136    for r in &dependencies.internal_references {
137        referenced_by_internal
138            .entry(r.to_object)
139            .or_default()
140            .push(r.from_object);
141    }
142    for v in referenced_by_internal.values_mut() {
143        v.sort_unstable();
144        v.dedup();
145    }
146
147    let mut refs: Vec<AssetReference> = Vec::new();
148
149    for (asset_id, referenced_by) in referenced_by_internal {
150        let asset_type = if gameobject_ids.contains(&asset_id) {
151            "GameObject".to_string()
152        } else if component_ids.contains(&asset_id) {
153            "Component".to_string()
154        } else {
155            "Object".to_string()
156        };
157        refs.push(AssetReference {
158            asset_id,
159            asset_type,
160            referenced_by,
161            file_path: None,
162        });
163    }
164
165    for r in &dependencies.external_references {
166        refs.push(AssetReference {
167            asset_id: r.path_id,
168            asset_type: format!("ExternalObject(file_id={})", r.file_id),
169            referenced_by: r.referenced_by.clone(),
170            file_path: r.file_path.clone(),
171        });
172    }
173
174    refs.sort_by(|a, b| {
175        // Prefer refs with known file path, then by ref count desc, then asset_id asc.
176        match (b.file_path.is_some()).cmp(&a.file_path.is_some()) {
177            std::cmp::Ordering::Equal => {}
178            ord => return ord,
179        }
180        match b.referenced_by.len().cmp(&a.referenced_by.len()) {
181            std::cmp::Ordering::Equal => {}
182            ord => return ord,
183        }
184        a.asset_id.cmp(&b.asset_id)
185    });
186
187    relationships.asset_references = refs;
188}
189
190impl MetadataProcessor {
191    /// Create a new metadata processor with default settings
192    pub fn new() -> Self {
193        Self {
194            extractor: MetadataExtractor::new(),
195            dependency_analyzer: None,
196            relationship_analyzer: None,
197        }
198    }
199
200    /// Create a metadata processor with custom configuration
201    pub fn with_config(config: ExtractionConfig) -> Self {
202        let enable_advanced = config.include_dependencies || config.include_hierarchy;
203
204        Self {
205            extractor: MetadataExtractor::with_config(config),
206            dependency_analyzer: if enable_advanced {
207                Some(DependencyAnalyzer::new())
208            } else {
209                None
210            },
211            relationship_analyzer: if enable_advanced {
212                Some(RelationshipAnalyzer::new())
213            } else {
214                None
215            },
216        }
217    }
218
219    /// Process metadata from a SerializedFile
220    pub fn process_asset(
221        &mut self,
222        asset: &SerializedFile,
223    ) -> crate::error::Result<ExtractionResult> {
224        let mut result = self.extractor.extract_from_asset(asset)?;
225
226        // Enhanced dependency analysis if analyzer is available
227        if let Some(ref mut analyzer) = self.dependency_analyzer
228            && self.extractor.config().include_dependencies
229        {
230            let objects: Vec<&crate::asset::ObjectInfo> =
231                if let Some(max) = self.extractor.config().max_objects {
232                    asset.objects.iter().take(max).collect()
233                } else {
234                    asset.objects.iter().collect()
235                };
236
237            match analyzer.analyze_dependencies_in_asset(asset, &objects) {
238                Ok(deps) => {
239                    if self.extractor.config().include_object_details {
240                        let mut by_from: std::collections::HashMap<i64, Vec<i64>> =
241                            std::collections::HashMap::new();
242                        for r in &deps.internal_references {
243                            by_from.entry(r.from_object).or_default().push(r.to_object);
244                        }
245                        for v in by_from.values_mut() {
246                            v.sort_unstable();
247                            v.dedup();
248                        }
249                        for summary in &mut result.metadata.object_stats.largest_objects {
250                            summary.dependencies =
251                                by_from.get(&summary.path_id).cloned().unwrap_or_default();
252                        }
253                    }
254                    result.metadata.dependencies = deps;
255                }
256                Err(e) => {
257                    result.add_warning(format!("Enhanced dependency analysis failed: {}", e));
258                }
259            }
260        }
261
262        // Enhanced relationship analysis if analyzer is available
263        if let Some(ref mut analyzer) = self.relationship_analyzer
264            && self.extractor.config().include_hierarchy
265        {
266            let objects: Vec<&crate::asset::ObjectInfo> =
267                if let Some(max) = self.extractor.config().max_objects {
268                    asset.objects.iter().take(max).collect()
269                } else {
270                    asset.objects.iter().collect()
271                };
272
273            match analyzer.analyze_relationships_in_asset(asset, &objects) {
274                Ok(mut rels) => {
275                    if self.extractor.config().include_dependencies {
276                        apply_dependency_info_to_relationships(
277                            &result.metadata.dependencies,
278                            &mut rels,
279                        );
280                    }
281                    result.metadata.relationships = rels;
282                }
283                Err(e) => {
284                    result.add_warning(format!("Enhanced relationship analysis failed: {}", e));
285                }
286            }
287        }
288
289        Ok(result)
290    }
291
292    /// Process metadata from an AssetBundle
293    pub fn process_bundle(
294        &mut self,
295        bundle: &AssetBundle,
296    ) -> crate::error::Result<Vec<ExtractionResult>> {
297        let mut results = Vec::new();
298
299        for asset in &bundle.assets {
300            let result = self.process_asset(asset)?;
301            results.push(result);
302        }
303
304        Ok(results)
305    }
306
307    /// Get the current extraction configuration
308    pub fn config(&self) -> &ExtractionConfig {
309        self.extractor.config()
310    }
311
312    /// Update the extraction configuration
313    pub fn set_config(&mut self, config: ExtractionConfig) {
314        let enable_advanced = config.include_dependencies || config.include_hierarchy;
315
316        self.extractor.set_config(config);
317
318        // Initialize analyzers if needed
319        if enable_advanced {
320            if self.dependency_analyzer.is_none() {
321                self.dependency_analyzer = Some(DependencyAnalyzer::new());
322            }
323            if self.relationship_analyzer.is_none() {
324                self.relationship_analyzer = Some(RelationshipAnalyzer::new());
325            }
326        }
327    }
328
329    /// Clear internal caches
330    pub fn clear_caches(&mut self) {
331        if let Some(ref mut analyzer) = self.dependency_analyzer {
332            analyzer.clear_cache();
333        }
334        if let Some(ref mut analyzer) = self.relationship_analyzer {
335            analyzer.clear_cache();
336        }
337    }
338
339    /// Check if advanced analysis is enabled
340    pub fn has_advanced_analysis(&self) -> bool {
341        self.dependency_analyzer.is_some() || self.relationship_analyzer.is_some()
342    }
343}
344
345impl Default for MetadataProcessor {
346    fn default() -> Self {
347        Self::new()
348    }
349}
350
351/// Convenience functions for common operations
352/// Create a metadata processor with default settings
353pub fn create_processor() -> MetadataProcessor {
354    MetadataProcessor::default()
355}
356
357/// Create a metadata processor with performance-focused configuration
358pub fn create_performance_processor() -> MetadataProcessor {
359    let config = ExtractionConfig {
360        include_dependencies: false,
361        include_hierarchy: false,
362        max_objects: Some(1000),
363        include_performance: true,
364        include_object_details: false,
365    };
366    MetadataProcessor::with_config(config)
367}
368
369/// Create a metadata processor with comprehensive analysis
370pub fn create_comprehensive_processor() -> MetadataProcessor {
371    let config = ExtractionConfig {
372        include_dependencies: true,
373        include_hierarchy: true,
374        max_objects: None,
375        include_performance: true,
376        include_object_details: true,
377    };
378    MetadataProcessor::with_config(config)
379}
380
381/// Extract basic metadata from an asset
382pub fn extract_basic_metadata(asset: &SerializedFile) -> crate::error::Result<AssetMetadata> {
383    let mut processor = MetadataProcessor::with_config(ExtractionConfig::default());
384    let result = processor.process_asset(asset)?;
385    Ok(result.metadata)
386}
387
388/// Extract metadata with custom configuration
389pub fn extract_metadata_with_config(
390    asset: &SerializedFile,
391    config: ExtractionConfig,
392) -> crate::error::Result<ExtractionResult> {
393    let mut processor = MetadataProcessor::with_config(config);
394    processor.process_asset(asset)
395}
396
397/// Get quick statistics for an asset
398pub fn get_asset_statistics(asset: &SerializedFile) -> AssetStatistics {
399    AssetStatistics {
400        object_count: asset.objects.len(),
401        type_count: asset.types.len(),
402        external_count: asset.externals.len(),
403        file_size: asset.header.file_size,
404        unity_version: asset.unity_version.clone(),
405        format_version: asset.header.version,
406    }
407}
408
409/// Quick asset statistics
410#[derive(Debug, Clone)]
411pub struct AssetStatistics {
412    pub object_count: usize,
413    pub type_count: usize,
414    pub external_count: usize,
415    pub file_size: u64,
416    pub unity_version: String,
417    pub format_version: u32,
418}
419
420/// Metadata processing options
421#[derive(Debug, Clone)]
422pub struct ProcessingOptions {
423    pub enable_caching: bool,
424    pub max_cache_size: usize,
425    pub parallel_processing: bool,
426    pub memory_limit_mb: Option<usize>,
427}
428
429impl Default for ProcessingOptions {
430    fn default() -> Self {
431        Self {
432            enable_caching: true,
433            max_cache_size: 1000,
434            parallel_processing: false,
435            memory_limit_mb: None,
436        }
437    }
438}
439
440/// Check if metadata extraction is supported for an asset
441pub fn is_extraction_supported(asset: &SerializedFile) -> bool {
442    // Support Unity 5.0+ (version 10+)
443    asset.header.version >= 10
444}
445
446/// Get recommended extraction configuration for an asset
447pub fn get_recommended_config(asset: &SerializedFile) -> ExtractionConfig {
448    let object_count = asset.objects.len();
449
450    if object_count > 10000 {
451        // Large asset - performance focused
452        ExtractionConfig {
453            include_dependencies: false,
454            include_hierarchy: false,
455            max_objects: Some(5000),
456            include_performance: true,
457            include_object_details: false,
458        }
459    } else if object_count > 1000 {
460        // Medium asset - balanced
461        ExtractionConfig {
462            include_dependencies: true,
463            include_hierarchy: false,
464            max_objects: Some(2000),
465            include_performance: true,
466            include_object_details: true,
467        }
468    } else {
469        // Small asset - comprehensive
470        ExtractionConfig::default()
471    }
472}
473
474#[cfg(test)]
475mod processor_tests {
476    use super::*;
477
478    #[test]
479    fn test_apply_dependency_info_to_relationships_fills_component_dependencies() {
480        let dependencies = DependencyInfo {
481            external_references: vec![ExternalReference {
482                file_id: 2,
483                path_id: 999,
484                referenced_by: vec![11],
485                file_path: Some("library/external.assets".to_string()),
486                guid: Some([7u8; 16]),
487            }],
488            internal_references: vec![
489                InternalReference {
490                    from_object: 10,
491                    to_object: 1,
492                    reference_type: "Direct".to_string(),
493                },
494                InternalReference {
495                    from_object: 10,
496                    to_object: 2,
497                    reference_type: "Direct".to_string(),
498                },
499                InternalReference {
500                    from_object: 11,
501                    to_object: 3,
502                    reference_type: "Direct".to_string(),
503                },
504            ],
505            dependency_graph: DependencyGraph {
506                nodes: Vec::new(),
507                edges: Vec::new(),
508                root_objects: Vec::new(),
509                leaf_objects: Vec::new(),
510            },
511            circular_dependencies: Vec::new(),
512        };
513
514        let mut relationships = AssetRelationships {
515            gameobject_hierarchy: Vec::new(),
516            component_relationships: vec![
517                ComponentRelationship {
518                    component_id: 10,
519                    component_type: "Transform".to_string(),
520                    gameobject_id: 100,
521                    dependencies: Vec::new(),
522                    external_dependencies: Vec::new(),
523                },
524                ComponentRelationship {
525                    component_id: 11,
526                    component_type: "MeshRenderer".to_string(),
527                    gameobject_id: 100,
528                    dependencies: Vec::new(),
529                    external_dependencies: Vec::new(),
530                },
531                ComponentRelationship {
532                    component_id: 12,
533                    component_type: "Unknown".to_string(),
534                    gameobject_id: 100,
535                    dependencies: Vec::new(),
536                    external_dependencies: Vec::new(),
537                },
538            ],
539            asset_references: Vec::new(),
540        };
541
542        apply_dependency_info_to_relationships(&dependencies, &mut relationships);
543
544        assert_eq!(
545            relationships.component_relationships[0].dependencies,
546            vec![1, 2]
547        );
548        assert_eq!(
549            relationships.component_relationships[1].dependencies,
550            vec![3]
551        );
552        assert_eq!(
553            relationships.component_relationships[2].dependencies,
554            Vec::<i64>::new()
555        );
556
557        assert_eq!(
558            relationships.component_relationships[0]
559                .external_dependencies
560                .len(),
561            0
562        );
563        assert_eq!(
564            relationships.component_relationships[1]
565                .external_dependencies
566                .len(),
567            1
568        );
569        assert_eq!(
570            relationships.component_relationships[1].external_dependencies[0],
571            ExternalObjectRef {
572                file_id: 2,
573                path_id: 999,
574                file_path: Some("library/external.assets".to_string()),
575                guid: Some([7u8; 16]),
576            }
577        );
578
579        let ext_ref = relationships
580            .asset_references
581            .iter()
582            .find(|r| r.asset_id == 999)
583            .expect("external asset reference exists");
584        assert_eq!(ext_ref.asset_type, "ExternalObject(file_id=2)");
585        assert_eq!(
586            ext_ref.file_path,
587            Some("library/external.assets".to_string())
588        );
589        assert_eq!(ext_ref.referenced_by, vec![11]);
590
591        let internal_ref_1 = relationships
592            .asset_references
593            .iter()
594            .find(|r| r.asset_id == 1)
595            .expect("internal asset reference 1 exists");
596        assert_eq!(internal_ref_1.file_path, None);
597        assert_eq!(internal_ref_1.referenced_by, vec![10]);
598
599        let internal_ref_3 = relationships
600            .asset_references
601            .iter()
602            .find(|r| r.asset_id == 3)
603            .expect("internal asset reference 3 exists");
604        assert_eq!(internal_ref_3.referenced_by, vec![11]);
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611
612    #[test]
613    fn test_processor_creation() {
614        let processor = create_processor();
615        assert!(!processor.has_advanced_analysis());
616    }
617
618    #[test]
619    fn test_comprehensive_processor() {
620        let processor = create_comprehensive_processor();
621        assert!(processor.has_advanced_analysis());
622        assert!(processor.config().include_dependencies);
623        assert!(processor.config().include_hierarchy);
624    }
625
626    #[test]
627    fn test_performance_processor() {
628        let processor = create_performance_processor();
629        assert!(!processor.config().include_dependencies);
630        assert!(!processor.config().include_hierarchy);
631        assert_eq!(processor.config().max_objects, Some(1000));
632    }
633
634    #[test]
635    fn test_extraction_support() {
636        // This would need a mock SerializedFile for proper testing
637        // For now, just test that the function exists
638    }
639}