1#[derive(Debug, thiserror::Error)]
9pub enum MetaParseError {
10 #[error("missing or malformed `guid:` in .meta")]
11 MissingGuid,
12}
13
14pub const TEXTURE_TYPE_SPRITE: u32 = 8;
17
18pub const SPRITE_MODE_SINGLE: u32 = 1;
21
22#[derive(Debug, Clone, Default)]
24pub struct MetaInfo {
25 pub guid: u128,
27 pub sprite_sheet: Vec<(i64, String)>,
30 pub texture_type: Option<u32>,
32 pub sprite_mode: Option<u32>,
34}
35
36pub fn parse(text: &str) -> Result<MetaInfo, MetaParseError> {
48 let mut info = MetaInfo::default();
49 let mut have_guid = false;
50
51 let mut in_sprites = false;
56 let mut cur_name: Option<String> = None;
57 let mut cur_id: Option<i64> = None;
58
59 for line in text.lines() {
60 let trimmed_left = line.trim_start();
61
62 if !have_guid
67 && let Some(rest) = trimmed_left.strip_prefix("guid:")
68 {
69 let hex = rest.trim();
70 if hex.len() == 32
71 && let Ok(g) = u128::from_str_radix(hex, 16)
72 {
73 info.guid = g;
74 have_guid = true;
75 }
76 } else if info.texture_type.is_none()
77 && let Some(rest) = trimmed_left.strip_prefix("textureType:")
78 {
79 info.texture_type = rest.trim().parse().ok();
80 } else if info.sprite_mode.is_none()
81 && let Some(rest) = trimmed_left.strip_prefix("spriteMode:")
82 {
83 info.sprite_mode = rest.trim().parse().ok();
84 }
85
86 if in_sprites {
93 let trimmed = line.trim();
94 if !line.starts_with(' ') && !line.starts_with('\t') && !trimmed.is_empty() {
97 in_sprites = false;
98 flush_sprite(&mut info.sprite_sheet, &mut cur_name, &mut cur_id);
99 } else if let Some(after_dash) = trimmed.strip_prefix("- ") {
100 flush_sprite(&mut info.sprite_sheet, &mut cur_name, &mut cur_id);
101 absorb_kv(after_dash, &mut cur_name, &mut cur_id);
102 } else {
103 absorb_kv(trimmed, &mut cur_name, &mut cur_id);
104 }
105 } else if trimmed_left == "sprites:" {
106 in_sprites = true;
107 }
108 }
109
110 flush_sprite(&mut info.sprite_sheet, &mut cur_name, &mut cur_id);
112
113 if !have_guid {
114 return Err(MetaParseError::MissingGuid);
115 }
116 Ok(info)
117}
118
119fn absorb_kv(line: &str, cur_name: &mut Option<String>, cur_id: &mut Option<i64>) {
120 if let Some(rest) = line.strip_prefix("name:") {
121 let s = rest.trim();
122 if cur_name.is_none() && !s.is_empty() {
123 *cur_name = Some(s.to_string());
124 }
125 } else if let Some(rest) = line.strip_prefix("internalID:") {
126 *cur_id = rest.trim().parse().ok();
127 }
128}
129
130fn flush_sprite(out: &mut Vec<(i64, String)>, name: &mut Option<String>, id: &mut Option<i64>) {
131 if let (Some(n), Some(i)) = (name.take(), id.take())
132 && !n.is_empty()
133 {
134 out.push((i, n));
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn parses_simple_guid() {
144 let text = "fileFormatVersion: 2\nguid: 7d602c2080b53413fa393df6b2c0af43\n";
145 let info = parse(text).unwrap();
146 assert_eq!(info.guid, 0x7d602c2080b53413fa393df6b2c0af43_u128);
147 assert!(info.sprite_sheet.is_empty());
148 }
149
150 #[test]
151 fn rejects_short_guid() {
152 let text = "guid: deadbeef\n";
153 assert!(parse(text).is_err());
154 }
155
156 #[test]
157 fn parses_sprite_sheet() {
158 let text = "fileFormatVersion: 2
159guid: 7d602c2080b53413fa393df6b2c0af43
160TextureImporter:
161 spriteSheet:
162 sprites:
163 - serializedVersion: 2
164 name: spr_a
165 internalID: 11111
166 rect:
167 serializedVersion: 2
168 - serializedVersion: 2
169 name: spr_b
170 internalID: 22222
171 spritePackingTag:
172";
173 let info = parse(text).unwrap();
174 assert_eq!(
175 info.sprite_sheet,
176 vec![(11111, "spr_a".to_string()), (22222, "spr_b".to_string()),]
177 );
178 }
179
180 #[test]
181 fn parses_texture_type_and_sprite_mode() {
182 let text = "fileFormatVersion: 2
183guid: 7d602c2080b53413fa393df6b2c0af43
184TextureImporter:
185 textureType: 8
186 spriteMode: 1
187 spriteSheet:
188 sprites: []
189";
190 let info = parse(text).unwrap();
191 assert_eq!(info.texture_type, Some(8));
192 assert_eq!(info.sprite_mode, Some(1));
193 }
194
195 #[test]
196 fn missing_texture_fields_are_none() {
197 let text = "fileFormatVersion: 2\nguid: 7d602c2080b53413fa393df6b2c0af43\n";
198 let info = parse(text).unwrap();
199 assert_eq!(info.texture_type, None);
200 assert_eq!(info.sprite_mode, None);
201 }
202
203 #[test]
209 fn sprites_block_inner_keys_dont_leak_to_top_level() {
210 let text = "fileFormatVersion: 2
211guid: aabbccddaabbccddaabbccddaabbccdd
212TextureImporter:
213 spriteSheet:
214 sprites:
215 - name: textureType: 99
216 internalID: 12345
217 textureType: 8
218 spriteMode: 2
219";
220 let info = parse(text).unwrap();
221 assert_eq!(info.texture_type, Some(8));
224 assert_eq!(info.sprite_mode, Some(2));
225 assert_eq!(info.sprite_sheet.len(), 1);
226 assert_eq!(info.sprite_sheet[0].0, 12345);
227 }
228
229 #[test]
233 fn key_order_independence() {
234 let text = "fileFormatVersion: 2
235guid: aabbccddaabbccddaabbccddaabbccdd
236TextureImporter:
237 textureType: 8
238 spriteMode: 1
239 spriteSheet:
240 sprites: []
241 externalObjects: {}
242";
243 let info = parse(text).unwrap();
244 assert_eq!(info.texture_type, Some(8));
245 assert_eq!(info.sprite_mode, Some(1));
246 assert!(info.sprite_sheet.is_empty());
247 }
248
249 #[test]
254 fn crlf_line_endings() {
255 let text = "fileFormatVersion: 2\r\nguid: 7d602c2080b53413fa393df6b2c0af43\r\nTextureImporter:\r\n textureType: 8\r\n spriteMode: 1\r\n";
256 let info = parse(text).unwrap();
257 assert_eq!(info.guid, 0x7d602c2080b53413fa393df6b2c0af43_u128);
258 assert_eq!(info.texture_type, Some(8));
259 assert_eq!(info.sprite_mode, Some(1));
260 }
261
262 #[test]
266 fn empty_file_errors_cleanly() {
267 let err = parse("").unwrap_err();
268 assert!(matches!(err, MetaParseError::MissingGuid));
269 }
270}