1use anyhow::{anyhow, bail, Context, Result};
2use serde_yaml::Value;
3use std::collections::{BTreeMap, HashMap};
4use std::path::Path;
5
6#[derive(Clone, Copy, Debug)]
7pub enum Nls {
8 Utf8,
9 ShiftJis,
10 Gb18030,
11}
12
13impl Nls {
14 pub fn parse(s: &str) -> Result<Self> {
15 let ss = s.trim().to_ascii_lowercase();
16 match ss.as_str() {
17 "utf8" | "utf-8" => Ok(Nls::Utf8),
18 "sjis" | "shiftjis" | "shift_jis" | "shift-jis" => Ok(Nls::ShiftJis),
19 "gbk" | "gb18030" => Ok(Nls::Gb18030),
20 other => bail!("unsupported nls: {other}"),
21 }
22 }
23}
24
25#[derive(Clone, Debug)]
26pub struct Syscall {
27 pub id: u16,
28 pub args: u8,
29 pub name: String,
30}
31
32#[derive(Clone, Debug)]
33pub struct Meta {
34 pub nls: Nls,
35 pub game_title: String,
36 pub game_mode: u8,
37 pub game_mode_reserved: u8,
38 pub non_volatile_global_count: u16,
39 pub volatile_global_count: u16,
40 pub custom_syscall_count: u16,
41 pub syscalls: Vec<Syscall>,
42
43 name_to_id: HashMap<String, u16>,
44 id_to_args: HashMap<u16, u8>,
45}
46
47impl Meta {
48 pub fn syscall_id_by_name(&self, name: &str) -> Option<u16> {
49 self.name_to_id.get(name).copied()
50 }
51
52 pub fn syscall_args_by_id(&self, id: u16) -> Option<u8> {
53 self.id_to_args.get(&id).copied()
54 }
55
56 pub fn syscall_count(&self) -> u16 {
57 self.syscalls.len() as u16
58 }
59}
60
61fn as_u16(v: &Value, key: &str) -> Result<u16> {
62 match v {
63 Value::Number(n) => n
64 .as_u64()
65 .and_then(|x| u16::try_from(x).ok())
66 .ok_or_else(|| anyhow!("{key} must be a u16")),
67 _ => bail!("{key} must be a number"),
68 }
69}
70
71fn as_u8(v: &Value, key: &str) -> Result<u8> {
72 match v {
73 Value::Number(n) => n
74 .as_u64()
75 .and_then(|x| u8::try_from(x).ok())
76 .ok_or_else(|| anyhow!("{key} must be a u8")),
77 _ => bail!("{key} must be a number"),
78 }
79}
80
81fn as_u32_opt(v: Option<&Value>) -> Result<Option<u32>> {
82 if let Some(Value::Number(n)) = v {
83 return Ok(Some(
84 n.as_u64()
85 .and_then(|x| u32::try_from(x).ok())
86 .ok_or_else(|| anyhow!("value must be a u32"))?,
87 ));
88 }
89 Ok(None)
90}
91
92fn as_str(v: &Value, key: &str) -> Result<String> {
93 match v {
94 Value::String(s) => Ok(s.clone()),
95 _ => bail!("{key} must be a string"),
96 }
97}
98
99pub fn load_meta(path: &Path) -> Result<Meta> {
100 let txt = std::fs::read_to_string(path).with_context(|| format!("read meta: {}", path.display()))?;
101 let doc: Value = serde_yaml::from_str(&txt).context("parse yaml")?;
102 let map = doc
103 .as_mapping()
104 .ok_or_else(|| anyhow!("meta must be a mapping"))?;
105
106 let get = |k: &str| -> Option<&Value> { map.get(&Value::String(k.to_string())) };
107
108 let nls = if let Some(v) = get("nls") {
109 Nls::parse(&as_str(v, "nls")?)?
110 } else {
111 Nls::ShiftJis
112 };
113
114 let game_title = if let Some(v) = get("game_title") {
115 as_str(v, "game_title")?
116 } else {
117 String::new()
118 };
119
120 let game_mode = if let Some(v) = get("game_mode") {
121 as_u8(v, "game_mode")?
122 } else {
123 0
124 };
125
126 let game_mode_reserved = if let Some(v) = get("game_mode_reserved") {
127 as_u8(v, "game_mode_reserved")?
128 } else {
129 0
130 };
131
132 let non_volatile_global_count = if let Some(v) = get("non_volatile_global_count") {
133 as_u16(v, "non_volatile_global_count")?
134 } else {
135 0
136 };
137
138 let volatile_global_count = if let Some(v) = get("volatile_global_count") {
139 as_u16(v, "volatile_global_count")?
140 } else {
141 0
142 };
143
144 let custom_syscall_count = if let Some(v) = get("custom_syscall_count") {
145 as_u16(v, "custom_syscall_count")?
146 } else {
147 0
148 };
149
150 let _sys_desc_offset = as_u32_opt(get("sys_desc_offset")).unwrap_or(None);
152 let _entry_point = as_u32_opt(get("entry_point")).unwrap_or(None);
153
154 let syscalls_v = get("syscalls").ok_or_else(|| anyhow!("meta.syscalls is required"))?;
156
157 let mut syscalls: Vec<Syscall> = Vec::new();
158
159 match syscalls_v {
160 Value::Sequence(seq) => {
161 for (i, it) in seq.iter().enumerate() {
163 let m = it
164 .as_mapping()
165 .ok_or_else(|| anyhow!("meta.syscalls[{i}] must be a mapping"))?;
166 let name = m
167 .get(&Value::String("name".to_string()))
168 .ok_or_else(|| anyhow!("meta.syscalls[{i}].name missing"))?;
169 let args = m
170 .get(Value::String("args".to_string()))
171 .unwrap();
172
173 let name = as_str(name, "name")?;
174 let argc = as_u16(args, "args")?;
175 let argc_u8 = u8::try_from(argc).map_err(|_| anyhow!("syscall args must fit u8"))?;
176
177 syscalls.push(Syscall {
178 id: u16::try_from(i).unwrap(),
179 args: argc_u8,
180 name,
181 });
182 }
183 }
184 Value::Mapping(m) => {
185 let mut tmp: BTreeMap<u16, Syscall> = BTreeMap::new();
188 for (k, v) in m.iter() {
189 let id = match k {
190 Value::Number(n) => n
191 .as_u64()
192 .and_then(|x| u16::try_from(x).ok())
193 .ok_or_else(|| anyhow!("syscalls key must fit u16"))?,
194 Value::String(s) => s
195 .parse::<u16>()
196 .map_err(|_| anyhow!("syscalls key must be integer"))?,
197 _ => bail!("syscalls key must be integer"),
198 };
199
200 let vm = v
201 .as_mapping()
202 .ok_or_else(|| anyhow!("syscalls[{id}] must be a mapping"))?;
203
204 let name_v = vm
205 .get(&Value::String("name".to_string()))
206 .ok_or_else(|| anyhow!("syscalls[{id}].name missing"))?;
207 let args_v = vm
208 .get(&Value::String("args".to_string()))
209 .ok_or_else(|| anyhow!("syscalls[{id}].args missing"))?;
210
211 let name = as_str(name_v, "name")?;
212 let argc = as_u16(args_v, "args")?;
213 let argc_u8 = u8::try_from(argc).map_err(|_| anyhow!("syscall args must fit u8"))?;
214
215 if tmp.contains_key(&id) {
216 bail!("duplicate syscall id: {id}");
217 }
218 tmp.insert(
219 id,
220 Syscall {
221 id,
222 args: argc_u8,
223 name,
224 },
225 );
226 }
227
228 let declared_count = get("syscall_count")
230 .and_then(|v| as_u16(v, "syscall_count").ok());
231 let count = if let Some(dc) = declared_count {
232 dc
233 } else {
234 tmp.keys().next_back().map(|x| x + 1).unwrap_or(0)
236 };
237
238 for id in 0..count {
239 if !tmp.contains_key(&id) {
240 bail!("missing syscall id {id} in meta.syscalls");
241 }
242 }
243
244 syscalls = tmp.into_values().collect();
245
246 if declared_count.is_some() {
247 if syscalls.len() != usize::from(count) {
248 bail!("syscall_count mismatch: declared {count}, found {}", syscalls.len());
249 }
250 }
251 }
252 _ => bail!("meta.syscalls must be a sequence or mapping"),
253 }
254
255 let mut name_to_id: HashMap<String, u16> = HashMap::new();
256 let mut id_to_args: HashMap<u16, u8> = HashMap::new();
257 for sc in &syscalls {
258 if name_to_id.insert(sc.name.clone(), sc.id).is_some() {
259 bail!("duplicate syscall name: {}", sc.name);
260 }
261 id_to_args.insert(sc.id, sc.args);
262 }
263
264 Ok(Meta {
265 nls,
266 game_title,
267 game_mode,
268 game_mode_reserved,
269 non_volatile_global_count,
270 volatile_global_count,
271 custom_syscall_count,
272 syscalls,
273 name_to_id,
274 id_to_args,
275 })
276}