Skip to main content

siglus_assets/
ogg_xor.rs

1//! Ogg/Vorbis related helpers used by Siglus/Tona.
2//!
3//! The original engine sometimes stores Ogg data with a simple XOR obfuscation.
4//! Some runtimes apply this XOR transform on reads.
5
6use anyhow::{bail, Result};
7use std::fs::File;
8use std::io::{Read, Seek, SeekFrom};
9use std::path::Path;
10
11/// A `Read + Seek` wrapper that restricts access to a `[start, start+len)` range.
12///
13/// If `xor_key` is `Some(k)`, each byte returned from `read()` is XORed with `k`.
14pub struct BoundedFile {
15    file: File,
16    start: u64,
17    len: u64,
18    pos: u64,
19    xor_key: Option<u8>,
20}
21
22impl BoundedFile {
23    pub fn open<P: AsRef<Path>>(
24        path: P,
25        start: u64,
26        len: u64,
27        xor_key: Option<u8>,
28    ) -> Result<Self> {
29        let mut file = File::open(path)?;
30        file.seek(SeekFrom::Start(start))?;
31        Ok(Self {
32            file,
33            start,
34            len,
35            pos: 0,
36            xor_key,
37        })
38    }
39
40    pub fn remaining(&self) -> u64 {
41        self.len.saturating_sub(self.pos)
42    }
43
44    pub fn into_inner(self) -> File {
45        self.file
46    }
47
48    /// Read the entire bounded region into memory (applying XOR if configured).
49    pub fn read_all(mut self) -> Result<Vec<u8>> {
50        let mut out = Vec::with_capacity(self.len.min(256 * 1024) as usize);
51        self.seek(SeekFrom::Start(0))?;
52        self.read_to_end(&mut out)?;
53        Ok(out)
54    }
55}
56
57impl Read for BoundedFile {
58    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
59        if self.pos >= self.len {
60            return Ok(0);
61        }
62        let max = (self.len - self.pos) as usize;
63        let to_read = buf.len().min(max);
64        let n = self.file.read(&mut buf[..to_read])?;
65        if let Some(k) = self.xor_key {
66            for b in &mut buf[..n] {
67                *b ^= k;
68            }
69        }
70        self.pos += n as u64;
71        Ok(n)
72    }
73}
74
75impl Seek for BoundedFile {
76    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
77        let new_pos: i128 = match pos {
78            SeekFrom::Start(x) => x as i128,
79            SeekFrom::Current(x) => self.pos as i128 + x as i128,
80            SeekFrom::End(x) => self.len as i128 + x as i128,
81        };
82        if new_pos < 0 {
83            return Err(std::io::Error::new(
84                std::io::ErrorKind::InvalidInput,
85                "negative seek",
86            ));
87        }
88        let new_pos_u = new_pos as u64;
89        if new_pos_u > self.len {
90            return Err(std::io::Error::new(
91                std::io::ErrorKind::InvalidInput,
92                "seek past end of bounded region",
93            ));
94        }
95        self.file.seek(SeekFrom::Start(self.start + new_pos_u))?;
96        self.pos = new_pos_u;
97        Ok(self.pos)
98    }
99}
100
101/// Decrypt a whole file by XORing each byte with `key`.
102///
103/// This is a convenience API for small/medium assets. For streaming decode, prefer `BoundedFile`.
104pub fn xor_file_to_vec<P: AsRef<Path>>(path: P, key: u8) -> Result<Vec<u8>> {
105    let mut f = File::open(path)?;
106    let mut v = Vec::new();
107    f.read_to_end(&mut v)?;
108    for b in &mut v {
109        *b ^= key;
110    }
111    // quick sanity check: Ogg pages start with "OggS"
112    if v.len() >= 4 && &v[..4] != b"OggS" {
113        // Not all sources begin with OggS at byte 0 (e.g., leading junk), so don't hard-fail.
114        // But this helps catch wrong-key usage early.
115        // The caller may still proceed.
116    }
117    Ok(v)
118}
119
120/// Basic sniffing for Ogg container magic.
121pub fn looks_like_ogg(buf: &[u8]) -> bool {
122    buf.len() >= 4 && &buf[..4] == b"OggS"
123}
124
125/// Validate `offset`/`size` against the file length.
126pub fn validate_subrange(file_len: u64, offset: u64, size: u64) -> Result<()> {
127    if offset > file_len {
128        bail!("subrange offset {offset} exceeds file length {file_len}");
129    }
130    if size == 0 {
131        // size==0 means "until EOF" in the original code.
132        return Ok(());
133    }
134    if offset.saturating_add(size) > file_len {
135        bail!("subrange [offset={offset}, size={size}] exceeds file length {file_len}");
136    }
137    Ok(())
138}