Skip to main content

siglus_assets/
angou.rs

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