Skip to main content

siglus_assets/
angou.rs

1//! Siglus "angou" (encryption/obfuscation) helpers.
2//!
3//! ## Important
4//!
5//! Different games may use different "angou" materials.
6//! In practice there can be:
7//!
8//! - a **base** (engine) angou code table
9//! - a **game-specific** angou code table
10//!
11//! Both can be applied (typically as sequential XOR streams) on top of an
12//! optional 16-byte exe-derived key.
13//!
14//! This module intentionally **exposes** those inputs, instead of hard-coding a
15//! single table, so the port can support multiple titles without rewrites.
16
17use anyhow::{Result, bail};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum AngouStepKind {
21    ExeKey16,
22    BaseCode,
23    GameCode,
24}
25
26#[derive(Debug, Clone)]
27pub struct AngouStep {
28    pub kind: AngouStepKind,
29    pub key: Vec<u8>,
30}
31
32impl AngouStep {
33    pub fn new(kind: AngouStepKind, key: Vec<u8>) -> Result<Self> {
34        if key.is_empty() {
35            bail!("angou: empty key for step {kind:?}");
36        }
37        Ok(Self { kind, key })
38    }
39}
40
41/// A chain of XOR steps. Each step is applied with cyclic indexing.
42#[derive(Debug, Clone, Default)]
43pub struct AngouChain {
44    pub steps: Vec<AngouStep>,
45}
46
47impl AngouChain {
48    pub fn apply_in_place(&self, buf: &mut [u8]) {
49        for step in &self.steps {
50            xor_cycle_in_place(buf, &step.key);
51        }
52    }
53
54    pub fn describe(&self) -> Vec<(AngouStepKind, usize)> {
55        self.steps.iter().map(|s| (s.kind, s.key.len())).collect()
56    }
57}
58
59pub fn xor_cycle_in_place(buf: &mut [u8], key: &[u8]) {
60    if key.is_empty() {
61        return;
62    }
63    for (i, b) in buf.iter_mut().enumerate() {
64        *b ^= key[i % key.len()];
65    }
66}
67
68/// Parse a hex string into bytes.
69///
70/// Accepts with/without `0x` prefix and ignores spaces/underscores.
71pub fn parse_hex_bytes(s: &str) -> Result<Vec<u8>> {
72    let mut hex = String::with_capacity(s.len());
73    for ch in s.chars() {
74        if ch == 'x' || ch == 'X' {
75            // keep as-is; 0x will be filtered by non-hex anyway.
76        }
77        if ch.is_ascii_hexdigit() {
78            hex.push(ch);
79        }
80    }
81
82    if !hex.len().is_multiple_of(2) {
83        bail!("hex string has odd length");
84    }
85    let mut out = Vec::with_capacity(hex.len() / 2);
86    let bytes = hex.as_bytes();
87    for i in (0..bytes.len()).step_by(2) {
88        let hi = from_hex_digit(bytes[i])?;
89        let lo = from_hex_digit(bytes[i + 1])?;
90        out.push((hi << 4) | lo);
91    }
92    Ok(out)
93}
94
95fn from_hex_digit(c: u8) -> Result<u8> {
96    match c {
97        b'0'..=b'9' => Ok(c - b'0'),
98        b'a'..=b'f' => Ok(c - b'a' + 10),
99        b'A'..=b'F' => Ok(c - b'A' + 10),
100        _ => bail!("invalid hex digit"),
101    }
102}