siglus_assets/
key_toml.rs1use std::fs;
2use std::path::Path;
3
4use anyhow::{bail, Context, Result};
5
6use crate::angou::{self, AngouStepKind};
7
8pub fn load_key16_from_project_dir(project_dir: &Path) -> Result<Option<[u8; 16]>> {
9 let path = project_dir.join("key.toml");
10 if !path.is_file() {
11 return Ok(None);
12 }
13 load_key16_from_file(&path)
14}
15
16pub fn load_key16_from_file(path: &Path) -> Result<Option<[u8; 16]>> {
17 let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
18 parse_key16_toml(&text)
19}
20
21pub fn parse_key16_toml(text: &str) -> Result<Option<[u8; 16]>> {
22 let Some(bytes) = parse_named_bytes(text, "key", "key_hex")? else {
23 return Ok(None);
24 };
25 if bytes.len() != 16 {
26 bail!(
27 "key.toml: key must contain exactly 16 bytes, got {}",
28 bytes.len()
29 );
30 }
31 let mut out = [0u8; 16];
32 out.copy_from_slice(&bytes);
33 Ok(Some(out))
34}
35
36#[derive(Debug, Clone, Default)]
37pub struct KeyTomlConfig {
38 pub exe_key16: Option<[u8; 16]>,
39 pub base_angou_code: Option<Vec<u8>>,
40 pub game_angou_code: Option<Vec<u8>>,
41 pub chain_order: Option<Vec<AngouStepKind>>,
42}
43
44pub fn load_key_toml_from_project_dir(project_dir: &Path) -> Result<Option<KeyTomlConfig>> {
45 let path = project_dir.join("key.toml");
46 if !path.is_file() {
47 return Ok(None);
48 }
49 load_key_toml_from_file(&path).map(Some)
50}
51
52pub fn load_key_toml_from_file(path: &Path) -> Result<KeyTomlConfig> {
53 let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
54 parse_key_toml(&text)
55}
56
57pub fn parse_key_toml(text: &str) -> Result<KeyTomlConfig> {
58 let mut out = KeyTomlConfig::default();
59
60 out.exe_key16 = parse_key16_toml(text)?;
61 out.base_angou_code = parse_named_bytes(text, "base_angou_code", "base_angou_hex")?;
62 out.game_angou_code = parse_named_bytes(text, "game_angou_code", "game_angou_hex")?;
63 out.chain_order = parse_chain_order(text)?;
64
65 Ok(out)
66}
67
68fn parse_named_bytes(text: &str, key: &str, alt_hex_key: &str) -> Result<Option<Vec<u8>>> {
69 if let Some(raw) = collect_rhs_for_key(text, key) {
70 return parse_bytes_value(&raw, key);
71 }
72 if let Some(raw) = collect_rhs_for_key(text, alt_hex_key) {
73 return parse_hex_value(&raw, alt_hex_key);
74 }
75 Ok(None)
76}
77
78fn parse_bytes_value(raw: &str, key: &str) -> Result<Option<Vec<u8>>> {
79 let raw = raw.trim();
80 if raw.is_empty() {
81 return Ok(None);
82 }
83 if raw.contains('[') {
84 let (inner, _) = extract_bracketed(raw)
85 .ok_or_else(|| anyhow::anyhow!("key.toml: {key} array missing closing ]"))?;
86 return Ok(Some(parse_byte_array(inner, key)?));
87 }
88 if raw.starts_with('"') && raw.ends_with('"') && raw.len() >= 2 {
89 let inner = &raw[1..raw.len() - 1];
90 let bytes = angou::parse_hex_bytes(inner)
91 .with_context(|| format!("key.toml: invalid hex for {key}"))?;
92 return Ok(Some(bytes));
93 }
94 Ok(None)
95}
96
97fn parse_hex_value(raw: &str, key: &str) -> Result<Option<Vec<u8>>> {
98 let raw = raw.trim();
99 if raw.is_empty() {
100 return Ok(None);
101 }
102 let inner = raw.trim_matches('"');
103 let bytes = angou::parse_hex_bytes(inner)
104 .with_context(|| format!("key.toml: invalid hex for {key}"))?;
105 Ok(Some(bytes))
106}
107
108fn parse_byte_array(inner: &str, key: &str) -> Result<Vec<u8>> {
109 let mut bytes = Vec::new();
110 for part in inner.split(',') {
111 let tok = part.trim();
112 if tok.is_empty() {
113 continue;
114 }
115 let value = if let Some(hex) = tok.strip_prefix("0x").or_else(|| tok.strip_prefix("0X")) {
116 u8::from_str_radix(hex, 16)
117 .with_context(|| format!("key.toml: invalid hex byte {tok}"))?
118 } else {
119 let v: u16 = tok
120 .parse()
121 .with_context(|| format!("key.toml: invalid byte {tok}"))?;
122 if v > 0xFF {
123 bail!("key.toml: byte out of range {tok}");
124 }
125 v as u8
126 };
127 bytes.push(value);
128 }
129 if bytes.is_empty() {
130 bail!("key.toml: {key} array is empty");
131 }
132 Ok(bytes)
133}
134
135fn collect_rhs_for_key(text: &str, key: &str) -> Option<String> {
136 let mut collecting = false;
137 let mut out = String::new();
138
139 for raw_line in text.lines() {
140 let line = raw_line.split('#').next().unwrap_or("").trim();
141 if line.is_empty() {
142 continue;
143 }
144
145 if !collecting {
146 let Some((lhs, rhs)) = line.split_once('=') else {
147 continue;
148 };
149 if lhs.trim() != key {
150 continue;
151 }
152 collecting = true;
153 out.push_str(rhs.trim());
154 if rhs.contains(']') {
155 break;
156 }
157 } else {
158 out.push(' ');
159 out.push_str(line);
160 if line.contains(']') {
161 break;
162 }
163 }
164 }
165
166 if out.is_empty() {
167 None
168 } else {
169 Some(out)
170 }
171}
172
173fn extract_bracketed(raw: &str) -> Option<(&str, &str)> {
174 let start = raw.find('[')?;
175 let end = raw[start + 1..].find(']').map(|v| start + 1 + v)?;
176 Some((&raw[start + 1..end], &raw[end + 1..]))
177}
178
179fn parse_chain_order(text: &str) -> Result<Option<Vec<AngouStepKind>>> {
180 let Some(raw) = collect_rhs_for_key(text, "chain_order") else {
181 return Ok(None);
182 };
183 let raw = raw.trim();
184 if raw.is_empty() {
185 return Ok(None);
186 }
187 let inner = if raw.contains('[') {
188 let (inner, _) = extract_bracketed(raw)
189 .ok_or_else(|| anyhow::anyhow!("key.toml: chain_order missing closing ]"))?;
190 inner
191 } else {
192 raw
193 };
194 let mut out = Vec::new();
195 for part in inner.split(',') {
196 let tok = part.trim().trim_matches('"').trim_matches('\'');
197 if tok.is_empty() {
198 continue;
199 }
200 let kind = match tok.to_ascii_lowercase().as_str() {
201 "exe" | "exe_key16" => AngouStepKind::ExeKey16,
202 "base" | "base_code" => AngouStepKind::BaseCode,
203 "game" | "game_code" => AngouStepKind::GameCode,
204 other => bail!("key.toml: unknown chain_order item {other}"),
205 };
206 out.push(kind);
207 }
208 if out.is_empty() {
209 return Ok(None);
210 }
211 Ok(Some(out))
212}