1 /*******************************************************************************
2 
3     LZO1X-1 (Mini LZO) compressor/uncompressor generating/accepting chunks of
4     compressed data with a length and checksum header
5 
6     Usage example:
7 
8                                                                              ---
9         import $(TITLE);
10 
11         void[] lzo_chunk;
12         void[] uncompressed_chunk;
13 
14         void run ( )
15         {
16             scope lzo = new LzoChunk;
17 
18             // preallocate lzo_chunk and uncompressed_chunk (optional)
19 
20             lzo_chunk = new void[LzoChunk.maxChunkLength(4096)];
21 
22             uncompressed_chunk  = new void[4096];
23 
24             char[] data;
25 
26             // populate data with data to compress...
27 
28             lzo.compress(data, lzo_chunk);
29 
30             // lzo_chunk now holds an LZO chunk with compressed data
31 
32             lzo.uncompress(lzo_chunk, uncompressed);
33 
34             // uncompressed_chunk now holds data, restored from lzo_chunk
35         }
36                                                                              ---
37 
38     Copyright:
39         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
40         All rights reserved.
41 
42     License:
43         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
44         Alternatively, this file may be distributed under the terms of the Tango
45         3-Clause BSD License (see LICENSE_BSD.txt for details).
46 
47  ******************************************************************************/
48 
49 module ocean.io.compress.lzo.LzoChunk;
50 
51 
52 import      ocean.io.compress.Lzo;
53 
54 import      ocean.io.compress.lzo.LzoHeader;
55 
56 import      ocean.io.compress.CompressException;
57 
58 import      ocean.core.Enforce;
59 
60 version (unittest) import ocean.core.Test;
61 
62 /*******************************************************************************
63 
64     LzoChunk compressor/decompressor
65 
66     Chunk data layout if size_t has a width of 32-bit:
67 
68                                                                              ---
69         void[] chunk
70 
71         chunk[0 .. 16] // header
72         chunk[16 .. $] // compressed data
73                                                                              ---
74 
75     Header data layout
76 
77                                                                              ---
78         chunk[0  ..  4] // length of chunk[4 .. $] (or compressed data length
79                         // + header length - 4)
80         chunk[4 ..   8] // 32-bit CRC value of following header elements and
81                         // compressed data (chunk[8 .. $]), calculated using
82                         //  lzo_crc32()
83         chunk[8  .. 12] // chunk/compression type code (signed 32-bit integer)
84         chunk[12 .. 16] // length of uncompressed data
85                                                                              ---
86 
87  ******************************************************************************/
88 
89 class LzoChunk ( bool LengthInline = true )
90 {
91     /***************************************************************************
92 
93         Lzo instance
94 
95      **************************************************************************/
96 
97     private             Lzo                     lzo;
98 
99     /***************************************************************************
100 
101         Flag set to true if the lzo instance is a reference (ie was passed from
102         the outside, is *not* owned by this instance, and should not be deleted
103         in the destructor).
104 
105      **************************************************************************/
106 
107     bool lzo_is_reference;
108 
109     /***************************************************************************
110 
111         Constructor - creates a new lzo instance internally.
112 
113      **************************************************************************/
114 
115     public this ( )
116     {
117         this.lzo   = new Lzo;
118     }
119 
120     /***************************************************************************
121 
122         Constructor - sets this instance to use an lzo object passed externally
123         (this allows multiple instances to use the same lzo object).
124 
125         Params:
126             lzo = lzo object to use
127 
128      **************************************************************************/
129 
130     public this ( Lzo lzo )
131     {
132         this.lzo = lzo;
133         this.lzo_is_reference = true;
134     }
135 
136     /***************************************************************************
137 
138         Compresses a data chunk
139 
140         Params:
141             data = data to compress
142             compressed = LZO compressed data chunk with header
143                          (output ref parameter)
144 
145         Returns:
146             this instance
147 
148      **************************************************************************/
149 
150     public typeof (this) compress ( T ) ( void[] data, ref T[] compressed )
151     {
152         static assert (T.sizeof == 1);
153 
154         LzoHeader!(LengthInline) header;
155 
156         size_t end;
157 
158         header.uncompressed_length = data.length;
159         header.type                = header.type.LZO1X;
160 
161         compressed.length = this.maxChunkLength(data.length);
162 
163         end = header.length + this.lzo.compress(data, header.strip(compressed));
164 
165         compressed.length = end;
166 
167         header.write(compressed);
168 
169         return this;
170     }
171 
172     /***************************************************************************
173 
174         Uncompresses a LZO chunk
175 
176         Params:
177             compressed = LZO compressed data chunk to uncompress
178             data = uncompressed data (output ref parameter)
179 
180         Returns:
181             this instance
182 
183         FIXME: - Add assertion for chunk length
184 
185      **************************************************************************/
186 
187     public typeof (this) uncompress ( T ) ( void[] compressed, ref T[] data )
188     {
189         static assert (T.sizeof == 1);
190 
191         LzoHeader!(LengthInline) header;
192 
193         void[] buf = header.read(compressed);
194 
195         data.length = header.uncompressed_length;
196 
197         enforce!(CompressException)(header.type == header.type.LZO1X, "Not LZO1X");
198 
199         this.lzo.uncompress(buf, data);
200 
201         return this;
202     }
203 
204     /***************************************************************************
205 
206         Static method alias, to be used as
207 
208                                                                              ---
209         static size_t maxCompressedLength ( size_t uncompressed_length );
210                                                                              ---
211 
212         Calculates the maximum compressed length of data which has a length of
213         uncompressed_length.
214 
215         Note: Surprisingly, this is more than uncompressed_length but that's the
216               worst case for completely uncompressable data.
217 
218         Parameters:
219             uncompressed_length = length of data to compressed
220 
221         Returns:
222             maximum compressed length of data
223 
224      **************************************************************************/
225 
226     alias Lzo.maxCompressedLength maxCompressedLength;
227 
228     /***************************************************************************
229 
230         Calculates the maximum chunk length from input data which has a length
231         of uncompressed_length.
232 
233         Note: Surprisingly, this is more than uncompressed_length but that's the
234               worst case for completely uncompressable data.
235 
236         Parameters:
237             uncompressed_length = length of data to compressed
238 
239         Returns:
240             maximum compressed length of data
241 
242      **************************************************************************/
243 
244     static size_t maxChunkLength ( size_t uncompressed_length )
245     {
246         return maxCompressedLength(uncompressed_length) + LzoHeader!().length;
247     }
248 }
249 
250 version (unittest):
251 
252 /*******************************************************************************
253 
254     Unit test
255 
256     Add -debug=GcDisabled to the compiler command line to disable the garbage
257     collector.
258 
259     Note: The unit test requires a file named "lzotest.dat" in the current
260     working directory at runtime. This file provides the data to be compressed
261     for testing and performance measurement.
262 
263  ******************************************************************************/
264 
265 import ocean.io.device.File;
266 import ocean.time.StopWatch;
267 
268 debug (GcDisabled) import ocean.core.Memory;
269 
270 import ocean.text.util.MetricPrefix;
271 
272 import core.stdc.signal: signal, SIGINT;
273 
274 /*******************************************************************************
275 
276     Rounds x to the nearest integer value
277 
278     Params:
279         x = value to round
280 
281     Returns:
282         nearest integer value of x
283 
284  ******************************************************************************/
285 
286 extern (C) int lrintf ( float x );
287 
288 /*******************************************************************************
289 
290     Terminator structure
291 
292  ******************************************************************************/
293 
294 struct Terminator
295 {
296     static:
297 
298     /***************************************************************************
299 
300         Termination flag
301 
302      **************************************************************************/
303 
304     bool terminated = false;
305 
306     /***************************************************************************
307 
308         Signal handler; raises the termination flag
309 
310      **************************************************************************/
311 
312     extern (C) void terminate ( int code ) nothrow @nogc
313     {
314         terminated = true;
315     }
316 }
317 
318 version (UnitTestVerbose) import ocean.io.Stdout;
319 
320 unittest
321 {
322     // Uncomment the next line to see UnitTest output
323     // version = UnitTestVerbose;
324 
325     debug (GcDisabled)
326     {
327         version (UnitTestVerbose)
328             Stdout.formatln("LzoChunk unittest: garbage collector disabled");
329         GC.disable();
330     }
331 
332     StopWatch swatch;
333 
334     MetricPrefix pre_comp_sz, pre_uncomp_sz,
335                  pre_comp_tm, pre_uncomp_tm, pre_crc_tm;
336 
337     version (UnitTestVerbose)
338         Stdout.formatln(
339             `LzoChunk unittest: loading test data from file "lzotest.dat\"`);
340 
341     // FLAKEY: Avoid IO in unittests and specially fixed file names
342     File file;
343 
344     try file = new File("lzotest.dat");
345     catch (Exception e)
346     return;
347 
348     scope data         = new void[file.length];
349     scope compressed   = new void[LzoChunk!().maxChunkLength(data.length)];
350     scope uncompressed = new void[file.length];
351 
352     scope lzo          = new LzoChunk!();
353 
354     file.read(data);
355 
356     file.close();
357 
358     swatch.start();
359 
360     lzo.compress(data, compressed);
361 
362     ulong compr_us = swatch.microsec;
363 
364     lzo.uncompress(compressed, uncompressed);
365 
366     ulong uncomp_us = swatch.microsec;
367 
368     uncomp_us -= compr_us;
369 
370     test (data.length == uncompressed.length, "data lengthmismatch");
371     test (data        == uncompressed,        "data mismatch");
372 
373     pre_comp_sz.bin(compressed.length);
374     pre_uncomp_sz.bin(data.length);
375 
376     pre_comp_tm.dec(compr_us, -2);
377     pre_uncomp_tm.dec(uncomp_us, -2);
378 
379     version (UnitTestVerbose)
380     {
381         Stdout.formatln("LzoChunk unittest results:\n\t"
382                  ~ "uncompressed length: {} {}B\t({} bytes)\n\t"
383                  ~ "compressed length:   {} {}B\t({} bytes)\n\t"
384                  ~ "compression ratio:   {}%\n\t"
385                  ~ "compression time:    {} {}s\t({} µs)\n\t"
386                  ~ "uncompression time:  {} {}s\t({} µs)\n\t"
387                  ~ "\n"
388                  ~ "LzoChunk unittest: Looping for memory leak detection; "
389                  ~ "watch memory usage and press Ctrl+C to quit",
390                    pre_uncomp_sz.scaled, pre_uncomp_sz.prefix, data.length,
391                    pre_comp_sz.scaled, pre_comp_sz.prefix, compressed.length,
392                    lrintf((compressed.length * 100.f) / data.length),
393                    pre_comp_tm.scaled, pre_comp_tm.prefix, compr_us,
394                    pre_uncomp_tm.scaled, pre_uncomp_tm.prefix, uncomp_us
395                    );
396     }
397 
398     auto prev_sigint_handler = signal(SIGINT, &Terminator.terminate);
399 
400     scope (exit) signal(SIGINT, prev_sigint_handler);
401 
402     while (!Terminator.terminated)
403     {
404         lzo.compress(data, compressed).uncompress(compressed, uncompressed);
405     }
406 }