Skip to main content

siglus_assets/
lzss.rs

1use anyhow::{Result, anyhow, bail};
2
3/// Parse the Siglus LZSS header.
4///
5/// Format: `[u32 arc_size][u32 org_size][payload...]`.
6/// `arc_size` includes the eight-byte header.
7#[inline]
8fn parse_header(src: &[u8]) -> Result<(usize, usize)> {
9    if src.len() < 8 {
10        bail!("lzss: input too short");
11    }
12    let arc_size = u32::from_le_bytes([src[0], src[1], src[2], src[3]]) as usize;
13    let org_size = u32::from_le_bytes([src[4], src[5], src[6], src[7]]) as usize;
14    Ok((arc_size, org_size))
15}
16
17/// Return the decompressed size (org_size) from the header.
18pub fn lzss_decompressed_size(src: &[u8]) -> Result<usize> {
19    let (_, org_size) = parse_header(src)?;
20    Ok(org_size)
21}
22
23/// Decompress Siglus LZSS (byte-oriented) into a fresh buffer.
24///
25pub fn lzss_unpack(src: &[u8]) -> Result<Vec<u8>> {
26    let (arc_size, org_size) = parse_header(src)?;
27    if org_size == 0 {
28        bail!("lzss: org_size=0");
29    }
30
31    let payload_start = 8usize;
32    let payload_end = arc_size;
33    if payload_end < payload_start || payload_end > src.len() {
34        bail!(
35            "lzss: arc_size out of bounds (end={}, len={})",
36            payload_end,
37            src.len()
38        );
39    }
40
41    let mut pos = payload_start;
42    // The header size is untrusted: reserve no more than the input can expand to.
43    let mut out: Vec<u8> = Vec::with_capacity(org_size.min(src.len().saturating_mul(18)));
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, allowing `arc_size` to extend past the input.
113///
114/// Read only up to the declared archive boundary or the end of the input,
115/// whichever comes first. The output must still contain `org_size` bytes.
116pub fn lzss_unpack_lenient(src: &[u8]) -> Result<Vec<u8>> {
117    let (arc_size, org_size) = parse_header(src)?;
118    if org_size == 0 {
119        bail!("lzss(lenient): org_size=0");
120    }
121
122    let payload_start = 8usize;
123    let payload_end = arc_size.min(src.len());
124
125    let mut pos = payload_start;
126    // The header size is untrusted: reserve no more than the input can expand to.
127    let mut out: Vec<u8> = Vec::with_capacity(org_size.min(src.len().saturating_mul(18)));
128
129    while out.len() < org_size {
130        if pos >= payload_end {
131            break;
132        }
133        let mut flags = src[pos];
134        pos += 1;
135
136        for _ in 0..8 {
137            if out.len() >= org_size {
138                break;
139            }
140            if pos >= payload_end {
141                break;
142            }
143
144            if (flags & 1) != 0 {
145                out.push(src[pos]);
146                pos += 1;
147            } else {
148                if pos + 2 > payload_end {
149                    break;
150                }
151                let token = u16::from_le_bytes([src[pos], src[pos + 1]]);
152                pos += 2;
153
154                let offset = (token >> 4) as usize;
155                let len = ((token & 0x0F) as usize) + 2;
156
157                if offset == 0 || offset > out.len() {
158                    // In lenient mode, treat broken backrefs as end-of-stream.
159                    break;
160                }
161
162                let mut src_idx = out.len() - offset;
163                for _ in 0..len {
164                    if out.len() >= org_size {
165                        break;
166                    }
167                    let b = out[src_idx];
168                    out.push(b);
169                    src_idx += 1;
170                }
171            }
172
173            flags >>= 1;
174        }
175    }
176
177    if out.len() != org_size {
178        bail!(
179            "lzss(lenient): size mismatch (got={}, expected={})",
180            out.len(),
181            org_size
182        );
183    }
184
185    Ok(out)
186}
187
188/// Decompress Siglus LZSS32 (3-byte literal + implicit alpha, dword backrefs).
189///
190/// The output is 32bpp BGRA (little-endian dwords).
191pub fn lzss_unpack32(src: &[u8]) -> Result<Vec<u8>> {
192    let (arc_size, org_size) = parse_header(src)?;
193    if org_size == 0 {
194        bail!("lzss32: org_size=0");
195    }
196    if org_size % 4 != 0 {
197        bail!("lzss32: org_size not multiple of 4 (org_size={})", org_size);
198    }
199
200    let payload_start = 8usize;
201    let payload_end = arc_size;
202    if payload_end < payload_start || payload_end > src.len() {
203        bail!(
204            "lzss32: arc_size out of bounds (end={}, len={})",
205            payload_end,
206            src.len()
207        );
208    }
209
210    let mut pos = payload_start;
211    // The header size is untrusted: reserve no more than the input can expand to.
212    let mut out: Vec<u8> = Vec::with_capacity(org_size.min(src.len().saturating_mul(18)));
213
214    while out.len() < org_size {
215        if pos >= payload_end {
216            break;
217        }
218        let mut flags = src[pos];
219        pos += 1;
220
221        for _ in 0..8 {
222            if out.len() >= org_size {
223                break;
224            }
225
226            if (flags & 1) != 0 {
227                // literal: copy 3 bytes, then write alpha=255
228                if pos + 3 > payload_end {
229                    bail!("lzss32: truncated literal");
230                }
231                out.push(src[pos]);
232                out.push(src[pos + 1]);
233                out.push(src[pos + 2]);
234                out.push(255);
235                pos += 3;
236            } else {
237                // backref token is 16-bit: high 12 bits = offset (in dwords), low 4 bits = (len-1)
238                if pos + 2 > payload_end {
239                    bail!("lzss32: truncated backref token");
240                }
241                let token = u16::from_le_bytes([src[pos], src[pos + 1]]);
242                pos += 2;
243
244                let offset_dwords = (token >> 4) as usize;
245                let len_dwords = ((token & 0x0F) as usize) + 1;
246
247                if offset_dwords == 0 {
248                    bail!("lzss32: invalid backref offset=0");
249                }
250
251                let offset_bytes = offset_dwords
252                    .checked_mul(4)
253                    .ok_or_else(|| anyhow!("lzss32: offset overflow"))?;
254
255                if offset_bytes > out.len() {
256                    bail!(
257                        "lzss32: backref offset out of range (offset_bytes={}, out_len={})",
258                        offset_bytes,
259                        out.len()
260                    );
261                }
262
263                let mut src_idx = out.len() - offset_bytes;
264                for _ in 0..len_dwords {
265                    if out.len() + 4 > org_size {
266                        break;
267                    }
268                    // copy one dword; allow overlap like memmove
269                    let d0 = out[src_idx];
270                    let d1 = out[src_idx + 1];
271                    let d2 = out[src_idx + 2];
272                    let d3 = out[src_idx + 3];
273                    out.push(d0);
274                    out.push(d1);
275                    out.push(d2);
276                    out.push(d3);
277                    src_idx += 4;
278                }
279            }
280
281            flags >>= 1;
282            if pos > payload_end {
283                break;
284            }
285        }
286    }
287
288    if out.len() != org_size {
289        bail!(
290            "lzss32: size mismatch (got={}, expected={})",
291            out.len(),
292            org_size
293        );
294    }
295
296    Ok(out)
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    fn archive(payload: &[u8], org_size: u32) -> Vec<u8> {
304        let mut src = Vec::new();
305        src.extend_from_slice(&(8 + payload.len() as u32).to_le_bytes());
306        src.extend_from_slice(&org_size.to_le_bytes());
307        src.extend_from_slice(payload);
308        src
309    }
310
311    #[test]
312    fn archive_size_includes_header() {
313        // Two literals followed by an overlapping, distance-two match.
314        let src = archive(&[0x03, b'a', b'b', 0x22, 0x00], 6);
315        assert_eq!(lzss_unpack(&src).unwrap(), b"ababab");
316        assert_eq!(lzss_unpack_lenient(&src).unwrap(), b"ababab");
317
318        let src = archive(&[0x01, 10, 20, 30, 0x11, 0x00], 12);
319        assert_eq!(lzss_unpack32(&src).unwrap(), [10, 20, 30, 255].repeat(3));
320    }
321
322    #[test]
323    fn archive_boundary_excludes_trailing_bytes() {
324        let mut src = archive(&[0x01], 1);
325        src.push(b'a');
326        assert!(lzss_unpack(&src).is_err());
327        assert!(lzss_unpack_lenient(&src).is_err());
328
329        let mut src = archive(&[0x01, 10, 20], 4);
330        src.push(30);
331        assert!(lzss_unpack32(&src).is_err());
332    }
333
334    #[test]
335    fn strict_decoders_reject_invalid_archive_sizes() {
336        for arc_size in [0u32, 7, 100] {
337            let mut src = archive(&[0x01, 10, 20, 30], 4);
338            src[..4].copy_from_slice(&arc_size.to_le_bytes());
339            assert!(lzss_unpack(&src).is_err());
340            assert!(lzss_unpack32(&src).is_err());
341        }
342    }
343}