1use crate::{Error, ImporterType, MetaFile, Result, UnityGuid};
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug, Clone)]
9pub enum AssetType {
10 Binary(Vec<u8>),
12 Text(String),
14 Folder,
16}
17
18#[derive(Debug, Clone)]
20pub struct Asset {
21 pub path: String,
23 pub guid: UnityGuid,
25 pub content: AssetType,
27 pub importer_type: ImporterType,
29}
30
31impl Asset {
32 pub fn binary(path: impl Into<String>, data: Vec<u8>) -> Self {
34 let path = path.into();
35 let ext = path.rsplit('.').next().unwrap_or("");
36 let importer_type = ImporterType::from_extension(ext);
37
38 Self {
39 guid: UnityGuid::from_path(&path),
40 path,
41 content: AssetType::Binary(data),
42 importer_type,
43 }
44 }
45
46 pub fn text(path: impl Into<String>, content: impl Into<String>) -> Self {
48 let path = path.into();
49 let ext = path.rsplit('.').next().unwrap_or("");
50 let importer_type = ImporterType::from_extension(ext);
51
52 Self {
53 guid: UnityGuid::from_path(&path),
54 path,
55 content: AssetType::Text(content.into()),
56 importer_type,
57 }
58 }
59
60 pub fn folder(path: impl Into<String>) -> Self {
62 let path = path.into();
63
64 Self {
65 guid: UnityGuid::from_path(&path),
66 path,
67 content: AssetType::Folder,
68 importer_type: ImporterType::Folder,
69 }
70 }
71
72 pub fn with_guid(mut self, guid: UnityGuid) -> Self {
74 self.guid = guid;
75 self
76 }
77
78 pub fn with_random_guid(mut self) -> Self {
80 self.guid = UnityGuid::new();
81 self
82 }
83
84 pub fn with_importer(mut self, importer: ImporterType) -> Self {
86 self.importer_type = importer;
87 self
88 }
89
90 pub fn meta_file(&self) -> MetaFile {
92 if matches!(self.content, AssetType::Folder) {
93 MetaFile::folder(self.guid)
94 } else {
95 MetaFile::new(self.guid, self.importer_type)
96 }
97 }
98
99 pub fn content_bytes(&self) -> Option<Vec<u8>> {
101 match &self.content {
102 AssetType::Binary(data) => Some(data.clone()),
103 AssetType::Text(text) => Some(text.as_bytes().to_vec()),
104 AssetType::Folder => None,
105 }
106 }
107
108 pub fn is_folder(&self) -> bool {
110 matches!(self.content, AssetType::Folder)
111 }
112
113 pub fn binary_from_slice(path: impl Into<String>, data: &[u8]) -> Self {
115 Self::binary(path, data.to_vec())
116 }
117
118 pub fn from_file(unity_path: impl Into<String>, file_path: impl AsRef<Path>) -> Result<Self> {
125 let unity_path = unity_path.into();
126 let file_path = file_path.as_ref();
127 let data = fs::read(file_path)?;
128
129 let ext = unity_path.rsplit('.').next().unwrap_or("");
130
131 let is_text = matches!(
133 ext.to_lowercase().as_str(),
134 "json" | "txt" | "cs" | "js" | "xml" | "yaml" | "yml" | "md" | "shader" | "cginc"
135 );
136
137 if is_text {
138 match String::from_utf8(data) {
139 Ok(content) => Ok(Self::text(unity_path, content)),
140 Err(e) => Err(Error::Serialization(format!(
141 "File appears to be text but is not valid UTF-8: {}",
142 e
143 ))),
144 }
145 } else {
146 Ok(Self::binary(unity_path, data))
147 }
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_binary_asset() {
157 let asset = Asset::binary("Assets/Data/test.raw", vec![1, 2, 3, 4]);
158 assert_eq!(asset.path, "Assets/Data/test.raw");
159 assert_eq!(asset.importer_type, ImporterType::RawHeightmap);
160 assert!(!asset.is_folder());
161 }
162
163 #[test]
164 fn test_text_asset() {
165 let asset = Asset::text("Assets/Config/settings.json", r#"{"key": "value"}"#);
166 assert_eq!(asset.importer_type, ImporterType::Text);
167 }
168
169 #[test]
170 fn test_folder_asset() {
171 let asset = Asset::folder("Assets/Terrain");
172 assert!(asset.is_folder());
173 assert_eq!(asset.importer_type, ImporterType::Folder);
174 }
175
176 #[test]
177 fn test_deterministic_guid() {
178 let asset1 = Asset::binary("Assets/test.raw", vec![]);
179 let asset2 = Asset::binary("Assets/test.raw", vec![1, 2, 3]);
180 assert_eq!(asset1.guid, asset2.guid); }
182}