Skip to main content

siglus_assets/
lzss.rs

1use anyhow::{anyhow, bail, Result};
2
3/// Parse the Siglus LZSS header.
4///
5/// Format: `[u32 arc_size][u32 org_size][payload...]`.
6#[inline]
7fn parse_header(src: &[u8]) -> Result<(usize, usize)> {
8    if src.len() < 8 {
9        bail!("lzss: input too short");
10    }
11    let arc_size = u32::from_le_bytes([src[0], src[1], src[2], src[3]]) as usize;
12    let org_size = u32::from_le_bytes([src[4], src[5], src[6], src[7]]) as usize;
13    Ok((arc_size, org_size))
14}
15
16/// Return the decompressed size (org_size) from the header.
17pub fn lzss_decompressed_size(src: &[u8]) -> Result<usize> {
18    let (_, org_size) = parse_header(src)?;
19    Ok(org_size)
20}
21
22/// Decompress Siglus LZSS (byte-oriented) into a fresh buffer.
23///
24pub fn lzss_unpack(src: &[u8]) -> Result<Vec<u8>> {
25    let (arc_size, org_size) = parse_header(src)?;
26    if org_size == 0 {
27        bail!("lzss: org_size=0");
28    }
29
30    let payload_start = 8usize;
31    let payload_end = payload_start
32        .checked_add(arc_size)
33        .ok_or_else(|| anyhow!("lzss: arc_size overflow"))?;
34    if payload_end > src.len() {
35        bail!(
36            "lzss: arc_size out of bounds (end={}, len={})",
37            payload_end,
38            src.len()
39        );
40    }
41
42    let mut pos = payload_start;
43    let mut out: Vec<u8> = Vec::with_capacity(org_size);
44
45    while out.len() < org_size {
46        if pos >= payload_end {
47            break;
48        }
49        let mut flags = src[pos];
50        pos += 1;
51
52        for _ in 0..8 {
53            if out.len() >= org_size {
54                break;
55            }
56            if pos >= payload_end {
57                break;
58            }
59
60            if (flags & 1) != 0 {
61                // literal
62                out.push(src[pos]);
63                pos += 1;
64            } else {
65                if pos + 2 > payload_end {
66                    bail!("lzss: truncated backref token");
67                }
68                let token = u16::from_le_bytes([src[pos], src[pos + 1]]);
69                pos += 2;
70
71                let offset = (token >> 4) as usize;
72                // LZSS_BREAK_EVEN is 1 in Siglus's implementation.
73                let len = ((token & 0x0F) as usize) + 2;
74
75                if offset == 0 {
76                    bail!("lzss: invalid backref offset=0");
77                }
78                if offset > out.len() {
79                    bail!(
80                        "lzss: backref offset out of range (offset={}, out_len={})",
81                        offset,
82                        out.len()
83                    );
84                }
85
86                let mut src_idx = out.len() - offset;
87                for _ in 0..len {
88                    if out.len() >= org_size {
89                        break;
90                    }
91                    let b = out[src_idx];
92                    out.push(b);
93                    src_idx += 1;
94                }
95            }
96
97            flags >>= 1;
98        }
99    }
100
101    if out.len() != org_size {
102        bail!(
103            "lzss: size mismatch (got={}, expected={})",
104            out.len(),
105            org_size
106        );
107    }
108
109    Ok(out)
110}
111
112/// Decompress Siglus LZSS but be tolerant about the declared `arc_size`.
113///
114/// The original engine does not always hard-fail on mismatched `arc_size`.
115/// Instead, it will typically decode until either:
116/// - the output reaches `org_size`, or
117/// - the input stream ends.
118///
119/// We still require the final output length to match `org_size`.
120pub fn lzss_unpack_lenient(src: &[u8]) -> Result<Vec<u8>> {
121    let (arc_size, org_size) = parse_header(src)?;
122    if org_size == 0 {
123        bail!("lzss(lenient): org_size=0");
124    }
125
126    let payload_start = 8usize;
127    let payload_end = payload_start
128        .checked_add(arc_size)
129        .ok_or_else(|| anyhow!("lzss(lenient): arc_size overflow"))?
130        .min(src.len());
131
132    let mut pos = payload_start;
133    let mut out: Vec<u8> = Vec::with_capacity(org_size);
134
135    while out.len() < org_size {
136        if pos >= payload_end {
137            break;
138        }
139        let mut flags = src[pos];
140        pos += 1;
141
142        for _ in 0..8 {
143            if out.len() >= org_size {
144                break;
145            }
146            if pos >= payload_end {
147                break;
148            }
149
150            if (flags & 1) != 0 {
151                out.push(src[pos]);
152                pos += 1;
153            } else {
154                if pos + 2 > payload_end {
155                    break;
156                }
157                let token = u16::from_le_bytes([src[pos], src[pos + 1]]);
158                pos += 2;
159
160                let offset = (token >> 4) as usize;
161                let len = ((token & 0x0F) as usize) + 2;
162
163                if offset == 0 || offset > out.len() {
164                    // In lenient mode, treat broken backrefs as end-of-stream.
165                    break;
166                }
167
168                let mut src_idx = out.len() - offset;
169                for _ in 0..len {
170                    if out.len() >= org_size {
171                        break;
172                    }
173                    let b = out[src_idx];
174                    out.push(b);
175                    src_idx += 1;
176                }
177            }
178
179            flags >>= 1;
180        }
181    }
182
183    if out.len() != org_size {
184        bail!(
185            "lzss(lenient): size mismatch (got={}, expected={})",
186            out.len(),
187            org_size
188        );
189    }
190
191    Ok(out)
192}
193
194/// Decompress Siglus LZSS32 (3-byte literal + implicit alpha, dword backrefs).
195///
196/// The output is 32bpp BGRA (little-endian dwords).
197pub fn lzss_unpack32(src: &[u8]) -> Result<Vec<u8>> {
198    let (arc_size, org_size) = parse_header(src)?;
199    if org_size == 0 {
200        bail!("lzss32: org_size=0");
201    }
202    if org_size % 4 != 0 {
203        bail!("lzss32: org_size not multiple of 4 (org_size={})", org_size);
204    }
205
206    let payload_start = 8usize;
207    let payload_end = payload_start
208        .checked_add(arc_size)
209        .ok_or_else(|| anyhow!("lzss32: arc_size overflow"))?;
210    if payload_end > src.len() {
211        bail!(
212            "lzss32: arc_size out of bounds (end={}, len={})",
213            payload_end,
214            src.len()
215        );
216    }
217
218    let mut pos = payload_start;
219    let mut out: Vec<u8> = Vec::with_capacity(org_size);
220
221    while out.len() < org_size {
222        if pos >= payload_end {
223            break;
224        }
225        let mut flags = src[pos];
226        pos += 1;
227
228        for _ in 0..8 {
229            if out.len() >= org_size {
230                break;
231            }
232
233            if (flags & 1) != 0 {
234                // literal: copy 3 bytes, then write alpha=255
235                if pos + 3 > payload_end {
236                    bail!("lzss32: truncated literal");
237                }
238                out.push(src[pos]);
239                out.push(src[pos + 1]);
240                out.push(src[pos + 2]);
241                out.push(255);
242                pos += 3;
243            } else {
244                // backref token is 16-bit: high 12 bits = offset (in dwords), low 4 bits = (len-1)
245                if pos + 2 > payload_end {
246                    bail!("lzss32: truncated backref token");
247                }
248                let token = u16::from_le_bytes([src[pos], src[pos + 1]]);
249                pos += 2;
250
251                let offset_dwords = (token >> 4) as usize;
252                let len_dwords = ((token & 0x0F) as usize) + 1;
253
254                if offset_dwords == 0 {
255                    bail!("lzss32: invalid backref offset=0");
256                }
257
258                let offset_bytes = offset_dwords
259                    .checked_mul(4)
260                    .ok_or_else(|| anyhow!("lzss32: offset overflow"))?;
261
262                if offset_bytes > out.len() {
263                    bail!(
264                        "lzss32: backref offset out of range (offset_bytes={}, out_len={})",
265                        offset_bytes,
266                        out.len()
267                    );
268                }
269
270                let mut src_idx = out.len() - offset_bytes;
271                for _ in 0..len_dwords {
272                    if out.len() + 4 > org_size {
273                        break;
274                    }
275                    // copy one dword; allow overlap like memmove
276                    let d0 = out[src_idx];
277                    let d1 = out[src_idx + 1];
278                    let d2 = out[src_idx + 2];
279                    let d3 = out[src_idx + 3];
280                    out.push(d0);
281                    out.push(d1);
282                    out.push(d2);
283                    out.push(d3);
284                    src_idx += 4;
285                }
286            }
287
288            flags >>= 1;
289            if pos > payload_end {
290                break;
291            }
292        }
293    }
294
295    if out.len() != org_size {
296        bail!(
297            "lzss32: size mismatch (got={}, expected={})",
298            out.len(),
299            org_size
300        );
301    }
302
303    Ok(out)
304}