Skip to main content

siglus_assets/
key_toml.rs

1use 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}