#[derive(Debug, thiserror::Error)]
pub enum MetaParseError {
#[error("missing or malformed `guid:` in .meta")]
MissingGuid,
}
pub const TEXTURE_TYPE_SPRITE: u32 = 8;
pub const SPRITE_MODE_SINGLE: u32 = 1;
#[derive(Debug, Clone, Default)]
pub struct MetaInfo {
pub guid: u128,
pub sprite_sheet: Vec<(i64, String)>,
pub texture_type: Option<u32>,
pub sprite_mode: Option<u32>,
}
pub fn parse(text: &str) -> Result<MetaInfo, MetaParseError> {
let mut info = MetaInfo::default();
let mut have_guid = false;
let mut in_sprites = false;
let mut cur_name: Option<String> = None;
let mut cur_id: Option<i64> = None;
for line in text.lines() {
let trimmed_left = line.trim_start();
if !have_guid
&& let Some(rest) = trimmed_left.strip_prefix("guid:")
{
let hex = rest.trim();
if hex.len() == 32
&& let Ok(g) = u128::from_str_radix(hex, 16)
{
info.guid = g;
have_guid = true;
}
} else if info.texture_type.is_none()
&& let Some(rest) = trimmed_left.strip_prefix("textureType:")
{
info.texture_type = rest.trim().parse().ok();
} else if info.sprite_mode.is_none()
&& let Some(rest) = trimmed_left.strip_prefix("spriteMode:")
{
info.sprite_mode = rest.trim().parse().ok();
}
if in_sprites {
let trimmed = line.trim();
if !line.starts_with(' ') && !line.starts_with('\t') && !trimmed.is_empty() {
in_sprites = false;
flush_sprite(&mut info.sprite_sheet, &mut cur_name, &mut cur_id);
} else if let Some(after_dash) = trimmed.strip_prefix("- ") {
flush_sprite(&mut info.sprite_sheet, &mut cur_name, &mut cur_id);
absorb_kv(after_dash, &mut cur_name, &mut cur_id);
} else {
absorb_kv(trimmed, &mut cur_name, &mut cur_id);
}
} else if trimmed_left == "sprites:" {
in_sprites = true;
}
}
flush_sprite(&mut info.sprite_sheet, &mut cur_name, &mut cur_id);
if !have_guid {
return Err(MetaParseError::MissingGuid);
}
Ok(info)
}
fn absorb_kv(line: &str, cur_name: &mut Option<String>, cur_id: &mut Option<i64>) {
if let Some(rest) = line.strip_prefix("name:") {
let s = rest.trim();
if cur_name.is_none() && !s.is_empty() {
*cur_name = Some(s.to_string());
}
} else if let Some(rest) = line.strip_prefix("internalID:") {
*cur_id = rest.trim().parse().ok();
}
}
fn flush_sprite(out: &mut Vec<(i64, String)>, name: &mut Option<String>, id: &mut Option<i64>) {
if let (Some(n), Some(i)) = (name.take(), id.take())
&& !n.is_empty()
{
out.push((i, n));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_simple_guid() {
let text = "fileFormatVersion: 2\nguid: 7d602c2080b53413fa393df6b2c0af43\n";
let info = parse(text).unwrap();
assert_eq!(info.guid, 0x7d602c2080b53413fa393df6b2c0af43_u128);
assert!(info.sprite_sheet.is_empty());
}
#[test]
fn rejects_short_guid() {
let text = "guid: deadbeef\n";
assert!(parse(text).is_err());
}
#[test]
fn parses_sprite_sheet() {
let text = "fileFormatVersion: 2
guid: 7d602c2080b53413fa393df6b2c0af43
TextureImporter:
spriteSheet:
sprites:
- serializedVersion: 2
name: spr_a
internalID: 11111
rect:
serializedVersion: 2
- serializedVersion: 2
name: spr_b
internalID: 22222
spritePackingTag:
";
let info = parse(text).unwrap();
assert_eq!(
info.sprite_sheet,
vec![(11111, "spr_a".to_string()), (22222, "spr_b".to_string()),]
);
}
#[test]
fn parses_texture_type_and_sprite_mode() {
let text = "fileFormatVersion: 2
guid: 7d602c2080b53413fa393df6b2c0af43
TextureImporter:
textureType: 8
spriteMode: 1
spriteSheet:
sprites: []
";
let info = parse(text).unwrap();
assert_eq!(info.texture_type, Some(8));
assert_eq!(info.sprite_mode, Some(1));
}
#[test]
fn missing_texture_fields_are_none() {
let text = "fileFormatVersion: 2\nguid: 7d602c2080b53413fa393df6b2c0af43\n";
let info = parse(text).unwrap();
assert_eq!(info.texture_type, None);
assert_eq!(info.sprite_mode, None);
}
#[test]
fn sprites_block_inner_keys_dont_leak_to_top_level() {
let text = "fileFormatVersion: 2
guid: aabbccddaabbccddaabbccddaabbccdd
TextureImporter:
spriteSheet:
sprites:
- name: textureType: 99
internalID: 12345
textureType: 8
spriteMode: 2
";
let info = parse(text).unwrap();
assert_eq!(info.texture_type, Some(8));
assert_eq!(info.sprite_mode, Some(2));
assert_eq!(info.sprite_sheet.len(), 1);
assert_eq!(info.sprite_sheet[0].0, 12345);
}
#[test]
fn key_order_independence() {
let text = "fileFormatVersion: 2
guid: aabbccddaabbccddaabbccddaabbccdd
TextureImporter:
textureType: 8
spriteMode: 1
spriteSheet:
sprites: []
externalObjects: {}
";
let info = parse(text).unwrap();
assert_eq!(info.texture_type, Some(8));
assert_eq!(info.sprite_mode, Some(1));
assert!(info.sprite_sheet.is_empty());
}
#[test]
fn crlf_line_endings() {
let text = "fileFormatVersion: 2\r\nguid: 7d602c2080b53413fa393df6b2c0af43\r\nTextureImporter:\r\n textureType: 8\r\n spriteMode: 1\r\n";
let info = parse(text).unwrap();
assert_eq!(info.guid, 0x7d602c2080b53413fa393df6b2c0af43_u128);
assert_eq!(info.texture_type, Some(8));
assert_eq!(info.sprite_mode, Some(1));
}
#[test]
fn empty_file_errors_cleanly() {
let err = parse("").unwrap_err();
assert!(matches!(err, MetaParseError::MissingGuid));
}
}