zx0/
compressor.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
use crate::{
    MAX_OFFSET_ZX0,
    MAX_OFFSET_ZX7
};

use crate::compress::{Block, compress};
use crate::optimize::optimize;

/// A struct containing a vector representing the compressed data, as well as metadata related to
/// the compression operation.
pub struct CompressionResult {
    /// A vector containing the compressed output data.
    pub output: Vec<u8>,

    /// This value represents the minimum gap that should be maintained between the compressed
    /// data's end address and the uncompressed data's end address when decompressing in-place.
    /// When using the backwards compression mode the gap has to be between the start of the
    /// compressed data and the start of the uncompressed data.
    ///
    /// Please refer to the original C implementation's
    /// [readme](https://github.com/einar-saukas/ZX0#compressing-with-prefix) for an in-depth
    /// explanation.
    pub delta: usize
}

pub type ProgressCallback<'a> = Box<dyn FnMut(f32) + 'a>;

/// This struct provides a means of initializing and performing a ZX0 compression operation by
/// leveraging the builder pattern.
///
/// By calling [`Compressor::new`] a new [`Compressor`] will be instantiated using the following
/// default values:
///
/// - No prefix/suffix skipping
/// - Quick mode disabled
/// - Backwards mode disabled
/// - Classic mode disabled
///
/// After constructing a [`Compressor`] instance the method [`compress`](Compressor::compress) is available to compress
/// `u8` slices. The [`Compressor`] can be resued again afterwards.
///
/// In contrast to the original C implementation, compression using the Rust ZX0 compressor is
/// thread-safe, and can therefore be used to compress several slices in parallel.
pub struct Compressor<'a> {
    skip: usize,
    quick_mode: bool,
    backwards_mode: bool,
    classic_mode: bool,
    progress_callback: ProgressCallback<'a>
}

impl<'a> Compressor<'a> {
    /// Instantiate a new [`Compressor`] using the following default values:
    ///
    /// - No prefix/suffix skipping
    /// - Quick mode disabled
    /// - Backwards mode disabled
    /// - Classic mode disabled
    pub fn new() -> Self {
        Self {
            skip: 0,
            quick_mode: false,
            backwards_mode: false,
            classic_mode: false,
            progress_callback: Box::new(|_| ())
        }
    }

    /// Change the value for the quick mode setting. When enabled, this will cause the ZX0
    /// compressor to use a smaller dictionary size, at the cost of a less efficient compression
    /// ratio.
    ///
    /// Enabling this setting can be useful when producing debug assets where a short feedback loop
    /// is more important than getting a good compression ratio.
    pub fn quick_mode(&mut self, quick_mode: bool) -> &mut Self {
        self.quick_mode = quick_mode;
        self
    }

    /// Change the value for the backwards compression mode setting. This will cause the ZX0
    /// compressor to create compressed data that should be decompressed back-to-front. This can be
    /// useful in situations where in-place decompression is desired, and the end of the compressed
    /// data overlaps with the end of the region that the uncompressed data should be positioned
    /// in.
    ///
    /// Please refer to the original C implementation's
    /// [readme](https://github.com/einar-saukas/ZX0#compressing-backwards) for an in-depth
    /// explanation.
    pub fn backwards_mode(&mut self, backwards_mode: bool) -> &mut Self {
        self.backwards_mode = backwards_mode;
        self
    }

    /// Change the value for the classic compression mode setting. Enabling this will cause the ZX0
    /// compressor to output compressed data in its legacy V1 file format. This can be useful when
    /// compressing for one of the platforms that only provides a V1 decompression routine.
    pub fn classic_mode(&mut self, classic_mode: bool) -> &mut Self {
        self.classic_mode = classic_mode;
        self
    }

    /// Set a progress callback. When providing a closure to this function, that closure will be
    /// called repeatedly during compression. The closure will be called with a progress value
    /// between `0.0` and `1.0`. Note that due to the nature of the compression algorithm, this
    /// value is not increasing linearly with time, and thus should be interpreted as a rough
    /// estimate.
    pub fn progress_callback<C: FnMut(f32) + 'a>(&mut self, progress_callback: C) -> &mut Self {
        self.progress_callback = Box::new(progress_callback);
        self
    }

    /// Set the number of prefix/suffix bytes to skip during compression. This will cause the
    /// compressor to create a dictionary based on data that will already be in memory before the
    /// compressed data during decompression. Of course, for this to work the prefix (or suffix in
    /// case of backwards mode) in the file must be 100% identical to the data that is in memory
    /// before or after the block of compressed data when attempting to decompress it.
    ///
    /// Please refer to the original C implementation's
    /// [readme](https://github.com/einar-saukas/ZX0#compressing-with-prefix) for an in-depth
    /// explanation.
    pub fn skip(&mut self, skip: usize) -> &mut Self {
        self.skip = skip;
        self
    }

    /// Compress the provided slice.
    ///
    /// This returns a [`CompressionResult`] struct containing both the compressed data as well as
    /// metadata related to the compression operation.
    ///
    /// The [`Compressor`] does not have to be discarded after calling this method. It does not
    /// contain any state (only the configuration) and thus can be reused again for compressing
    /// additional data.
    pub fn compress(&mut self, input: &[u8]) -> CompressionResult {
        let chain = {
            let (allocator, mut optimal) = optimize(
                input,
                self.skip,
                if self.quick_mode { MAX_OFFSET_ZX7 } else { MAX_OFFSET_ZX0 },
                &mut self.progress_callback
            );

            let mut chain = Vec::new();

            while optimal != 0 {
                let oblock = allocator.get(optimal);

                chain.push(Block {
                    bits: oblock.bits as u32,
                    index: oblock.index as isize,
                    offset: oblock.offset as usize
                });

                optimal = oblock.next_index;
            }

            chain
        };

        let invert_mode = !self.classic_mode && !self.backwards_mode;
        let mut delta = 0;

        let output = compress(
            chain,
            input,
            self.skip,
            self.backwards_mode,
            invert_mode,
            &mut delta
        );

        CompressionResult {
            output,
            delta
        }
    }
}

impl<'a> Default for Compressor<'a> {
    fn default() -> Self {
        Self::new()
    }
}