chd/compression/
lzma.rs

1use crate::compression::{
2    CodecImplementation, CompressionCodec, CompressionCodecType, DecompressResult,
3};
4use crate::error::{Error, Result};
5use crate::header::CodecType;
6use lzma_rs::decompress::raw::{LzmaDecoder, LzmaParams, LzmaProperties};
7use std::io::Cursor;
8/// LZMA (lzma) decompression codec.
9///
10/// ## Format Details
11/// CHD compresses LZMA hunks without an xz stream header using
12/// the default compression settings for LZMA 19.0. These settings are
13///
14/// * Literal Context Bits (`lc`): 3
15/// * Literal Position Bits (`lp`): 0
16/// * Position Bits (`pb`): 2
17///
18/// The dictionary size is determined via the following algorithm with a level of 9, and a
19/// reduction size of hunk size.
20///
21/// ```rust
22/// fn get_lzma_dict_size(level: u32, reduce_size: u32) -> u32 {
23///     let mut dict_size = if level <= 5 {
24///         1 << (level * 2 + 14)
25///     } else if level <= 7 {
26///         1 << 25
27///     } else {
28///        1 << 26
29///     };
30///
31///     // This does the same thing as LzmaEnc.c when determining dict_size
32///     if dict_size > reduce_size {
33///         for i in 11..=30 {
34///             if reduce_size <= (2u32 << i) {
35///                 dict_size = 2u32 << i;
36///                 break;
37///             }
38///             if reduce_size <= (3u32 << i) {
39///                 dict_size = 3u32 << i;
40///                 break;
41///             }
42///         }
43///     }
44///     dict_size
45/// }
46/// ```
47///
48/// ## Buffer Restrictions
49/// Each compressed LZMA hunk decompresses to a hunk-sized chunk.
50/// The input buffer must contain exactly enough data to fill the output buffer
51/// when decompressed.
52pub struct LzmaCodec {
53    // The LZMA codec for CHD uses raw LZMA chunks without a stream header. The result
54    // is that the chunks are encoded with the defaults used in LZMA 19.0.
55    // These defaults are lc = 3, lp = 0, pb = 2.
56    engine: LzmaDecoder,
57}
58
59impl CompressionCodec for LzmaCodec {}
60
61/// MAME/libchdr uses an ancient LZMA 19.00.
62///
63/// To match the proper dictionary size, we copy the algorithm from
64/// [`LzmaEnc::LzmaEncProps_Normalize`](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/deps/lzma-19.00/src/LzmaEnc.c#L52).
65fn get_lzma_dict_size(level: u32, reduce_size: u32) -> u32 {
66    let mut dict_size = if level <= 5 {
67        1 << (level * 2 + 14)
68    } else if level <= 7 {
69        1 << 25
70    } else {
71        1 << 26
72    };
73
74    // this does the same thing as LzmaEnc.c when determining dict_size
75    if dict_size > reduce_size {
76        // might be worth converting this to a while loop for const
77        // depends if we can const-propagate hunk_size.
78        // todo: #[feature(const_inherent_unchecked_arith)
79        for i in 11..=30 {
80            if reduce_size <= (2u32 << i) {
81                dict_size = 2u32 << i;
82                break;
83            }
84            if reduce_size <= (3u32 << i) {
85                dict_size = 3u32 << i;
86                break;
87            }
88        }
89    }
90
91    dict_size
92}
93
94impl CompressionCodecType for LzmaCodec {
95    fn codec_type(&self) -> CodecType
96    where
97        Self: Sized,
98    {
99        CodecType::LzmaV5
100    }
101}
102
103#[cfg(not(feature = "fast_lzma"))]
104impl CodecImplementation for LzmaCodec {
105    fn new(hunk_size: u32) -> Result<Self> {
106        Ok(LzmaCodec {
107            engine: LzmaDecoder::new(
108                LzmaParams::new(
109                    LzmaProperties {
110                        lc: 3,
111                        lp: 0,
112                        pb: 2,
113                    },
114                    get_lzma_dict_size(9, hunk_size),
115                    None,
116                ),
117                None,
118            )
119            .map_err(|_| Error::DecompressionError)?,
120        })
121    }
122
123    fn decompress(&mut self, input: &[u8], mut output: &mut [u8]) -> Result<DecompressResult> {
124        let mut read = Cursor::new(input);
125        let len = output.len();
126        self.engine.reset(Some(Some(len as u64)));
127        self.engine
128            .decompress(&mut read, &mut output)
129            .map_err(|_| Error::DecompressionError)?;
130        Ok(DecompressResult::new(len, read.position() as usize))
131    }
132}
133
134#[cfg(feature = "fast_lzma")]
135impl CodecImplementation for LzmaCodec {
136    fn new(hunk_size: u32) -> Result<Self> {
137        let dict_size = get_lzma_dict_size(9, hunk_size);
138        Ok(LzmaCodec {
139            engine: LzmaDecoder::new_with_buffer(
140                LzmaParams::new(
141                    LzmaProperties {
142                        lc: 3,
143                        lp: 0,
144                        pb: 2,
145                    },
146                    dict_size,
147                    None,
148                ),
149                None,
150                vec![0; dict_size as usize],
151            )
152            .map_err(|_| Error::CodecError)?,
153        })
154    }
155
156    fn decompress(&mut self, input: &[u8], mut output: &mut [u8]) -> Result<DecompressResult> {
157        use lzma_rs::decompress::raw::LzAccumBuffer;
158        let mut read = Cursor::new(input);
159        let len = output.len();
160        self.engine.reset(Some(Some(len as u64)));
161        self.engine
162            .decompress_with_buffer::<LzAccumBuffer<_>, _, _>(&mut read, &mut output)
163            .map_err(|_| Error::DecompressionError)?;
164        Ok(DecompressResult::new(len, read.position() as usize))
165    }
166}