diff options
Diffstat (limited to 'libbutl/lz4.cxx')
-rw-r--r-- | libbutl/lz4.cxx | 555 |
1 files changed, 555 insertions, 0 deletions
diff --git a/libbutl/lz4.cxx b/libbutl/lz4.cxx new file mode 100644 index 0000000..2db7af2 --- /dev/null +++ b/libbutl/lz4.cxx @@ -0,0 +1,555 @@ +// file : libbutl/lz4.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <libbutl/lz4.hxx> + +// This careful macro dance makes sure that all the LZ4 C API functions are +// made static while making sure we include the headers in the same way as the +// implementation files that we include below. +// +#define LZ4LIB_VISIBILITY static +#define LZ4_STATIC_LINKING_ONLY +#define LZ4_PUBLISH_STATIC_FUNCTIONS +#define LZ4_DISABLE_DEPRECATE_WARNINGS +#include "lz4.h" +#include "lz4hc.h" + +#define LZ4FLIB_VISIBILITY static +#define LZ4F_STATIC_LINKING_ONLY +#define LZ4F_PUBLISH_STATIC_FUNCTIONS +#define LZ4F_DISABLE_DEPRECATE_WARNINGS +#include "lz4frame.h" + +#include <new> // bad_alloc +#include <memory> // unique_ptr +#include <cstring> // memcpy() +#include <cassert> +#include <stdexcept> // invalid_argument, logic_error + +#include <libbutl/utility.hxx> // eos() + +#if 0 +#include <libbutl/lz4-stream.hxx> +#endif + +using namespace std; + +namespace butl +{ + namespace lz4 + { + static inline size_t + block_size (LZ4F_blockSizeID_t id) + { + return (id == LZ4F_max4MB ? 4 * 1024 * 1024 : + id == LZ4F_max1MB ? 1 * 1024 * 1024 : + id == LZ4F_max256KB ? 256 * 1024 : + id == LZ4F_max64KB ? 64 * 1024 : 0); + } + + [[noreturn]] static void + throw_exception (LZ4F_errorCodes c) + { + using i = invalid_argument; + + switch (c) + { + case LZ4F_ERROR_GENERIC: throw i ("generic LZ4 error"); + case LZ4F_ERROR_maxBlockSize_invalid: throw i ("invalid LZ4 block size"); + case LZ4F_ERROR_blockMode_invalid: throw i ("invalid LZ4 block mode"); + case LZ4F_ERROR_contentChecksumFlag_invalid: throw i ("invalid LZ4 content checksum flag"); + case LZ4F_ERROR_compressionLevel_invalid: throw i ("invalid LZ4 compression level"); + case LZ4F_ERROR_headerVersion_wrong: throw i ("wrong LZ4 header version"); + case LZ4F_ERROR_blockChecksum_invalid: throw i ("invalid LZ4 block checksum"); + case LZ4F_ERROR_reservedFlag_set: throw i ("reserved LZ4 flag set"); + case LZ4F_ERROR_srcSize_tooLarge: throw i ("LZ4 input too large"); + case LZ4F_ERROR_dstMaxSize_tooSmall: throw i ("LZ4 output too small"); + case LZ4F_ERROR_frameHeader_incomplete: throw i ("incomplete LZ4 frame header"); + case LZ4F_ERROR_frameType_unknown: throw i ("unknown LZ4 frame type"); + case LZ4F_ERROR_frameSize_wrong: throw i ("wrong LZ4 frame size"); + case LZ4F_ERROR_decompressionFailed: throw i ("invalid LZ4 compressed content"); + case LZ4F_ERROR_headerChecksum_invalid: throw i ("invalid LZ4 header checksum"); + case LZ4F_ERROR_contentChecksum_invalid: throw i ("invalid LZ4 content checksum"); + + case LZ4F_ERROR_allocation_failed: throw bad_alloc (); + + // These seem to be programming errors. + // + case LZ4F_ERROR_srcPtr_wrong: // NULL pointer. + case LZ4F_ERROR_frameDecoding_alreadyStarted: // Incorrect call seq. + + // We should never get these. + // + case LZ4F_OK_NoError: + case LZ4F_ERROR_maxCode: + case _LZ4F_dummy_error_enum_for_c89_never_used: + break; + } + + assert (false); + throw logic_error (LZ4F_getErrorName ((LZ4F_errorCode_t)(-c))); + } + + // As above but for erroneous LZ4F_*() function result. + // + [[noreturn]] static inline void + throw_exception (size_t r) + { + throw_exception (LZ4F_getErrorCode (r)); + } + + // compression + // + + compressor:: + ~compressor () + { + if (LZ4F_cctx* ctx = static_cast<LZ4F_cctx*> (ctx_)) + { + LZ4F_errorCode_t e (LZ4F_freeCompressionContext (ctx)); + assert (!LZ4F_isError (e)); + } + } + + inline void compressor:: + init_preferences (void* vp) const + { + LZ4F_preferences_t* p (static_cast<LZ4F_preferences_t*> (vp)); + + p->autoFlush = 1; + p->favorDecSpeed = 0; + p->compressionLevel = level_; + p->frameInfo.blockMode = LZ4F_blockLinked; + p->frameInfo.blockSizeID = static_cast<LZ4F_blockSizeID_t> (block_id_); + p->frameInfo.blockChecksumFlag = LZ4F_noBlockChecksum; + p->frameInfo.contentChecksumFlag = LZ4F_contentChecksumEnabled; + p->frameInfo.contentSize = content_size_ + ? static_cast<unsigned long long> (*content_size_) + : 0; + } + + void compressor:: + begin (int level, + int block_id, + optional<uint64_t> content_size) + { + assert (block_id >= 4 && block_id <= 7); + + level_ = level; + block_id_ = block_id; + content_size_ = content_size; + + LZ4F_preferences_t prefs = LZ4F_INIT_PREFERENCES; + init_preferences (&prefs); + + // Input/output buffer capacities. + // + // To be binary compatible with the lz4 utility we have to compress + // files that fit into the block with a single *_compressFrame() call + // instead of *_compressBegin()/*_compressUpdate(). And to determine the + // output buffer capacity we must use *_compressFrameBound() instead of + // *_compressBound(). The problem is, at this stage (before filling the + // input buffer), we don't know which case it will be. + // + // However, in our case (autoFlush=1), *Bound() < *FrameBound() and so + // we can always use the latter at the cost of slight overhead. Also, + // using *FrameBound() allows us to call *Begin() and *Update() without + // flushing the buffer in between (this insight is based on studying the + // implementation of the *Bound() functions). + // + // Actually, we can use content_size (we can get away with much smaller + // buffers for small inputs). We just need to verify the caller is not + // lying to us (failed that, we may end up with strange error like + // insufficient output buffer space). + // + ic = block_size (prefs.frameInfo.blockSizeID); + + if (content_size_ && *content_size_ < ic) + { + // This is nuanced: we need to add an extra byte in order to detect + // EOF. + // + ic = static_cast<size_t> (*content_size_) + 1; + } + + oc = LZ4F_compressFrameBound (ic, &prefs); + + begin_ = true; + } + + void compressor:: + next (bool end) + { + LZ4F_cctx* ctx; + + // Unlike the decompression case below, compression cannot fail due to + // invalid content. So any LZ4F_*() function failure is either due to a + // programming bug or argument inconsistencies (e.g., content size does + // not match actual). + + if (begin_) + { + begin_ = false; + + LZ4F_preferences_t prefs = LZ4F_INIT_PREFERENCES; + init_preferences (&prefs); + + // If we've allocated smaller buffers based on content_size_, then + // verify the input size matches what's promised. + // + // Note also that LZ4F_compressFrame() does not fail if it doesn't + // match instead replacing it with the actual value. + // + size_t bs (block_size (prefs.frameInfo.blockSizeID)); + if (content_size_ && *content_size_ < bs) + { + if (!end || in != *content_size_) + throw_exception (LZ4F_ERROR_frameSize_wrong); + } + + // Must be < for lz4 compatibility (see EOF nuance above for the + // likely reason). + // + if (end && in < bs) + { + on = LZ4F_compressFrame (ob, oc, ib, in, &prefs); + if (LZ4F_isError (on)) + throw_exception (on); + + in = 0; // All consumed. + return; + } + else + { + if (LZ4F_isError (LZ4F_createCompressionContext (&ctx, LZ4F_VERSION))) + throw bad_alloc (); + + ctx_ = ctx; + + // Write the header. + // + on = LZ4F_compressBegin (ctx, ob, oc, &prefs); + if (LZ4F_isError (on)) + throw_exception (on); + + // Fall through. + } + } + else + { + ctx = static_cast<LZ4F_cctx*> (ctx_); + on = 0; + } + + size_t n; + + if (in != 0) + { + n = LZ4F_compressUpdate (ctx, ob + on, oc - on, ib, in, nullptr); + if (LZ4F_isError (n)) + throw_exception (n); + + in = 0; // All consumed. + on += n; + } + + // Write the end marker. + // + if (end) + { + // Note that this call also verifies specified and actual content + // sizes match. + // + n = LZ4F_compressEnd (ctx, ob + on, oc - on, nullptr); + if (LZ4F_isError (n)) + throw_exception (n); + + on += n; + } + } + + uint64_t + compress (ofdstream& os, ifdstream& is, + int level, + int block_id, + optional<uint64_t> content_size) + { +#if 0 + char buf[1024 * 3 + 7]; + ostream cos (os, level, block_id, content_size); + + for (bool e (false); !e; ) + { + e = eof (is.read (buf, sizeof (buf))); + cos.write (buf, is.gcount ()); + //for (streamsize i (0), n (is.gcount ()); i != n; ++i) + // cos.put (buf[i]); + } + + cos.close (); + return content_size ? *content_size : 0; +#else + compressor c; + + // Input/output buffer guards. + // + unique_ptr<char[]> ibg; + unique_ptr<char[]> obg; + + // First determine required buffer capacities. + // + c.begin (level, block_id, content_size); + + ibg.reset ((c.ib = new char[c.ic])); + obg.reset ((c.ob = new char[c.oc])); + + // Read into the input buffer updating the eof flag. + // + // Note that we could try to do direct fd read/write but that would + // complicate things quite a bit (error handling, stream state, etc). + // + bool eof (false); + auto read = [&is, &c, &eof] () + { + eof = butl::eof (is.read (c.ib, c.ic)); + c.in = static_cast<size_t> (is.gcount ()); + }; + + // Write from the output buffer updating the total written. + // + uint64_t ot (0); + auto write = [&os, &c, &ot] () + { + os.write (c.ob, static_cast<streamsize> (c.on)); + ot += c.on; + }; + + // Keep reading, compressing, and writing chunks of content. + // + while (!eof) + { + read (); + + c.next (eof); + + if (c.on != 0) // next() may just buffer the data. + write (); + } + + return ot; +#endif + } + + // decompression + // + + static_assert (sizeof (decompressor::hb) == LZ4F_HEADER_SIZE_MAX, + "LZ4 header size mismatch"); + + decompressor:: + ~decompressor () + { + if (LZ4F_dctx* ctx = static_cast<LZ4F_dctx*> (ctx_)) + { + LZ4F_errorCode_t e (LZ4F_freeDecompressionContext (ctx)); + assert (!LZ4F_isError (e)); + } + } + + size_t decompressor:: + begin (optional<uint64_t>* content_size) + { + LZ4F_dctx* ctx; + + if (LZ4F_isError (LZ4F_createDecompressionContext (&ctx, LZ4F_VERSION))) + throw bad_alloc (); + + ctx_ = ctx; + + LZ4F_frameInfo_t info = LZ4F_INIT_FRAMEINFO; + + // Input hint and end as signalled by the LZ4F_*() functions. + // + size_t h, e; + + h = LZ4F_getFrameInfo (ctx, &info, hb, &(e = hn)); + if (LZ4F_isError (h)) + throw_exception (h); + + if (content_size != nullptr) + { + if (info.contentSize != 0) + *content_size = static_cast<uint64_t> (info.contentSize); + else + *content_size = nullopt; + } + + // Use the block size for the output buffer capacity and compressed + // bound plus the header size for the input. The expectation is that + // LZ4F_decompress() should never hint for more than that. + // + oc = block_size (info.blockSizeID); + ic = LZ4F_compressBound (oc, nullptr) + LZ4F_BLOCK_HEADER_SIZE; + + assert (h <= ic); + + // Move over whatever is left in the header buffer to be beginning. + // + hn -= e; + memmove (hb, hb + e, hn); + + return h; + } + + size_t decompressor:: + next () + { + LZ4F_dctx* ctx (static_cast<LZ4F_dctx*> (ctx_)); + + size_t h, e; + + // Note that LZ4F_decompress() verifies specified and actual content + // sizes match (similar to compression). + // + h = LZ4F_decompress (ctx, ob, &(on = oc), ib, &(e = in), nullptr); + if (LZ4F_isError (h)) + throw_exception (h); + + // We expect LZ4F_decompress() to consume what it asked for. + // + assert (e == in && h <= ic); + in = 0; // All consumed. + + return h; + } + + uint64_t + decompress (ofdstream& os, ifdstream& is) + { + // Write the specified number of bytes from the output buffer updating + // the total written. + // + uint64_t ot (0); + auto write = [&os, &ot] (char* b, size_t n) + { + os.write (b, static_cast<streamsize> (n)); + ot += n; + }; + +#if 0 + char buf[1024 * 3 + 7]; + istream dis (is, true, istream::badbit); + + for (bool e (false); !e; ) + { + e = eof (dis.read (buf, sizeof (buf))); + write (buf, static_cast<size_t> (dis.gcount ())); + } +#else + // Read into the specified buffer returning the number of bytes read and + // updating the eof flag. + // + bool eof (false); + auto read = [&is, &eof] (char* b, size_t c) -> size_t + { + size_t n (0); + do + { + eof = butl::eof (is.read (b + n, c - n)); + n += static_cast<size_t> (is.gcount ()); + } + while (!eof && n != c); + + return n; + }; + + decompressor d; + + // Input/output buffer guards. + // + unique_ptr<char[]> ibg; + unique_ptr<char[]> obg; + + size_t h; // Input hint. + + // First read in the header and allocate the buffers. + // + // What if we hit EOF here? And could begin() return 0? Turns out the + // answer to both questions is yes: 0-byte content compresses to 15 + // bytes (with or without content size; 1-byte -- to 20/28 bytes). We + // can ignore EOF here since an attempt to read more will result in + // another EOF. And code below is prepared to handle 0 initial hint. + // + // @@ We could end up leaving some of the input content from the + // header in the input buffer which the caller will have to way + // of using/detecting. + // + d.hn = read (d.hb, sizeof (d.hb)); + h = d.begin (); + + ibg.reset ((d.ib = new char[d.ic])); + obg.reset ((d.ob = new char[d.oc])); + + // Copy over whatever is left in the header buffer and read up to + // the hinted size. + // + memcpy (d.ib, d.hb, (d.in = d.hn)); + + if (h > d.in) + d.in += read (d.ib + d.in, h - d.in); + + // Keep decompressing, writing, and reading chunks of compressed + // content. + // + while (h != 0) + { + h = d.next (); + + if (d.on != 0) // next() may just buffer the data. + write (d.ob, d.on); + + if (h != 0) + { + if (eof) + throw invalid_argument ("incomplete LZ4 compressed content"); + + d.in = read (d.ib, h); + } + } +#endif + + return ot; + } + } +} + +// Include the implementation into our translation unit. Let's keep it last +// since the implementation defines a bunch of macros. +// +#if defined(__clang__) || defined(__GNUC__) +# pragma GCC diagnostic ignored "-Wunused-function" +#endif + +// This header is only include in the implementation so we can include it +// here instead of the above. +// +#define XXH_PRIVATE_API // Makes API static and includes xxhash.c. +#include "xxhash.h" + +// Clang targeting MSVC prior to version 10 has difficulty with _tzcnt_u64() +// (see Clang bug 47099 for a potentially related issue). Including relevant +// headers (<immintrin.h>, <intrin.h>) does not appear to help. So for now we +// just disable the use of _tzcnt_u64(). +// +#if defined(_MSC_VER) && defined(__clang__) && __clang_major__ < 10 +# define LZ4_FORCE_SW_BITCOUNT +#endif + +// Note that the order of inclusion is important (see *_SRC_INCLUDED macros). +// +extern "C" +{ +#include "lz4.c" +#include "lz4hc.c" +#include "lz4frame.c" +} |