diff --git a/src/plugins/streamers/oasis/db_plugin/dbOASISReader.cc b/src/plugins/streamers/oasis/db_plugin/dbOASISReader.cc index 46d93413a..3d988905c 100644 --- a/src/plugins/streamers/oasis/db_plugin/dbOASISReader.cc +++ b/src/plugins/streamers/oasis/db_plugin/dbOASISReader.cc @@ -33,6 +33,9 @@ #include "tlString.h" #include "tlClassRegistry.h" +#include +#include + namespace db { @@ -110,86 +113,122 @@ OASISReader::init (const db::LoadLayoutOptions &options) m_expect_strict_mode = oasis_options.expect_strict_mode; } -inline int64_t -OASISReader::get_int64 () +// C++20 branch hints where the compiler has them +#if __cplusplus >= 202002L && defined(__has_cpp_attribute) +# if __has_cpp_attribute(likely) +# define OASIS_LIKELY [[likely]] +# endif +#endif +#if !defined(OASIS_LIKELY) +# define OASIS_LIKELY +#endif + +template +inline T +OASISReader::get_unsigned (const char *type_name) { - uint64_t u = get_uint64 (); - if ((u & 1) != 0) { - return -(int64_t) (u >> 1); - } else { - return (int64_t) (u >> 1); + static_assert (std::numeric_limits::is_integer && ! std::numeric_limits::is_signed, "get_unsigned needs an unsigned integer type"); + + // a base-128 varint of T has at most max_bytes bytes; the last one may only carry the remaining high bits + constexpr unsigned int digits = std::numeric_limits::digits; + constexpr unsigned int max_bytes = (digits + 6) / 7; + constexpr unsigned int last_byte_max = (1u << (digits - 7 * (max_bytes - 1))) - 1; + + // fast path: with max_bytes contiguous bytes buffered, a terminated varint is decoded without per-byte stream calls + size_t navail = 0; + const unsigned char *b = (const unsigned char *) m_stream.peek (navail); + if (b && navail >= max_bytes) OASIS_LIKELY { + + T v = 0; + for (unsigned int i = 0; i < max_bytes; ++i) { + const unsigned int c = b [i]; + if ((c & 0x80) == 0) { + if (i == max_bytes - 1 && (c & 0x7f) > last_byte_max) { + m_stream.skip (max_bytes); + error (tl::sprintf (tl::to_string (tr ("%s value overflow")), type_name)); + return 0; + } + v += T (c & 0x7f) << (7 * i); + m_stream.skip (i + 1); + return v; + } + v += T (c & 0x7f) << (7 * i); + } + + // no terminator within max_bytes: the byte-wise path below reports this the same way as always + } -} -inline uint64_t -OASISReader::get_uint64 () -{ - uint64_t v = 0; - uint64_t vm = 1; - char c; + T v = 0; + T vm = 1; + unsigned int c; do { - unsigned char *b = (unsigned char *) m_stream.get (1); - if (! b) { + const unsigned char *bb = (const unsigned char *) m_stream.get (1); + if (! bb) { error (tl::to_string (tr ("Unexpected end-of-file"))); return 0; } - c = *b; - if (vm > std::numeric_limits ::max () / 128 && - (uint64_t) (c & 0x7f) > (std::numeric_limits ::max () / vm)) { - error (tl::to_string (tr ("uint64 value overflow"))); + c = *bb; + if (vm > std::numeric_limits::max () / 128 && + T (c & 0x7f) > (std::numeric_limits::max () / vm)) { + error (tl::sprintf (tl::to_string (tr ("%s value overflow")), type_name)); } - v += (uint64_t) (c & 0x7f) * vm; + v += T (c & 0x7f) * vm; vm <<= 7; } while ((c & 0x80) != 0); return v; } -inline uint64_t -OASISReader::get_uint64_for_divider () +template +inline S +OASISReader::get_signed (const char *type_name) { - uint64_t l = get_uint64 (); - if (l == 0) { - error (tl::to_string (tr ("Divider must not be zero"))); + static_assert (std::numeric_limits::is_integer && std::numeric_limits::is_signed, "get_signed needs a signed integer type"); + + // the sign is carried in the lowest bit + typedef typename std::make_unsigned::type U; + const U u = get_unsigned (type_name); + if ((u & 1) != 0) { + return -S (u >> 1); + } else { + return S (u >> 1); } - return l; +} + +inline int64_t +OASISReader::get_int64 () +{ + return get_signed ("uint64"); +} + +inline uint64_t +OASISReader::get_uint64 () +{ + return get_unsigned ("uint64"); } inline int32_t OASISReader::get_int32 () { - uint32_t u = get_uint32 (); - if ((u & 1) != 0) { - return -int32_t (u >> 1); - } else { - return int32_t (u >> 1); - } + return get_signed ("uint32"); } inline uint32_t OASISReader::get_uint32 () { - uint32_t v = 0; - uint32_t vm = 1; - char c; - - do { - unsigned char *b = (unsigned char *) m_stream.get (1); - if (! b) { - error (tl::to_string (tr ("Unexpected end-of-file"))); - return 0; - } - c = *b; - if (vm > std::numeric_limits ::max () / 128 && - (uint32_t) (c & 0x7f) > (std::numeric_limits ::max () / vm)) { - error (tl::to_string (tr ("uin32 value overflow"))); - } - v += (uint32_t) (c & 0x7f) * vm; - vm <<= 7; - } while ((c & 0x80) != 0); + return get_unsigned ("uint32"); +} - return v; +inline uint64_t +OASISReader::get_uint64_for_divider () +{ + uint64_t l = get_uint64 (); + if (l == 0) { + error (tl::to_string (tr ("Divider must not be zero"))); + } + return l; } std::string @@ -1011,12 +1050,14 @@ OASISReader::do_read (db::Layout &layout) error (tl::sprintf (tl::to_string (tr ("Invalid CBLOCK compression type %d")), type)); } - uint64_t dummy = 0; - get (dummy); // uncomp-byte-count - not needed - get (dummy); // comp-byte-count - not needed + uint64_t uncomp_byte_count = 0; + get (uncomp_byte_count); // uncomp-byte-count - // put the stream into deflating mode - m_stream.inflate (); + uint64_t comp_byte_count = 0; + get (comp_byte_count); // comp-byte-count + + // put the stream into block-deflating mode + m_stream.inflate_block (size_t (comp_byte_count), size_t (uncomp_byte_count)); } else { error (tl::sprintf (tl::to_string (tr ("Invalid record type on global level %d")), int (r))); @@ -1491,12 +1532,14 @@ OASISReader::read_element_properties (bool ignore_special) error (tl::sprintf (tl::to_string (tr ("Invalid CBLOCK compression type %d")), type)); } - uint64_t dummy = 0; - get (dummy); // uncomp-byte-count - not needed - get (dummy); // comp-byte-count - not needed + uint64_t uncomp_byte_count = 0; + get (uncomp_byte_count); // uncomp-byte-count + + uint64_t comp_byte_count = 0; + get (comp_byte_count); // comp-byte-count - // put the stream into deflating mode - m_stream.inflate (); + // put the stream into block-deflating mode + m_stream.inflate_block (size_t (comp_byte_count), size_t (uncomp_byte_count)); } else if (m == 28 /*PROPERTY*/) { @@ -3596,12 +3639,14 @@ OASISReader::do_read_cell (db::cell_index_type cell_index, db::Layout &layout) error (tl::sprintf (tl::to_string (tr ("Invalid CBLOCK compression type %d")), type)); } - uint64_t dummy = 0; - get (dummy); // uncomp-byte-count - not needed - get (dummy); // comp-byte-count - not needed + uint64_t uncomp_byte_count = 0; + get (uncomp_byte_count); // uncomp-byte-count + + uint64_t comp_byte_count = 0; + get (comp_byte_count); // comp-byte-count - // put the stream into deflating mode - m_stream.inflate (); + // put the stream into block-deflating mode + m_stream.inflate_block (size_t (comp_byte_count), size_t (uncomp_byte_count)); } else { // put the byte back into the stream diff --git a/src/plugins/streamers/oasis/db_plugin/dbOASISReader.h b/src/plugins/streamers/oasis/db_plugin/dbOASISReader.h index a085d1bac..b2356c54f 100644 --- a/src/plugins/streamers/oasis/db_plugin/dbOASISReader.h +++ b/src/plugins/streamers/oasis/db_plugin/dbOASISReader.h @@ -237,6 +237,8 @@ class DB_PLUGIN_PUBLIC OASISReader } } + template T get_unsigned (const char *type_name); + template S get_signed (const char *type_name); int64_t get_int64 (); uint64_t get_uint64 (); uint64_t get_uint64_for_divider (); diff --git a/src/plugins/streamers/oasis/unit_tests/dbOASISReaderTests.cc b/src/plugins/streamers/oasis/unit_tests/dbOASISReaderTests.cc index 0c8516b91..1065261b5 100644 --- a/src/plugins/streamers/oasis/unit_tests/dbOASISReaderTests.cc +++ b/src/plugins/streamers/oasis/unit_tests/dbOASISReaderTests.cc @@ -743,3 +743,65 @@ TEST(CBlockLargePropertyString) } } + +// CBLOCK-compressed OASIS, read plain and through a gz stream +TEST(CBlockGzip) +{ + db::Layout layout_org (false); + + unsigned int layer = layout_org.insert_layer (db::LayerProperties (1, 0)); + db::Cell &top = layout_org.cell (layout_org.add_cell ("TOP")); + db::Cell &child = layout_org.cell (layout_org.add_cell ("CHILD")); + + child.shapes (layer).insert (db::Box (0, 0, 100, 100)); + child.shapes (layer).insert (db::Box (-200, -100, 300, 500)); + top.shapes (layer).insert (db::Box (-50, -50, 50, 50)); + top.insert (db::CellInstArray (child.cell_index (), db::Trans (db::Vector (10, 20)))); + + std::string tmp_file = tl::TestBase::tmp_file ("tmp_OASISReaderCBlockGzip.oas"); + + { + tl::OutputStream out (tmp_file); + db::SaveLayoutOptions options; + db::OASISWriterOptions &oasis_options = options.get_options (); + oasis_options.write_cblocks = true; + oasis_options.strict_mode = false; + db::OASISWriter writer; + writer.write (layout_org, out, options); + } + + db::Layout layout_read; + + { + tl::InputStream in (tmp_file); + db::OASISReader reader (in); + reader.read (layout_read); + } + + EXPECT_EQ (db::compare_layouts (layout_org, layout_read, db::layout_diff::f_verbose, 0), true); + + // same file wrapped into a gz stream: the CBLOCK data is read through + // a gz-decompressing delegate + std::string tmp_file_gz = tl::TestBase::tmp_file ("tmp_OASISReaderCBlockGzip.oas.gz"); + + { + tl::OutputStream out (tmp_file_gz, tl::OutputStream::OM_Zlib); + db::SaveLayoutOptions options; + db::OASISWriterOptions &oasis_options = options.get_options (); + oasis_options.write_cblocks = true; + oasis_options.strict_mode = false; + db::OASISWriter writer; + writer.write (layout_org, out, options); + } + + db::Layout layout_read_gz; + + { + tl::InputStream in (tmp_file_gz); + db::OASISReader reader (in); + reader.read (layout_read_gz); + } + + EXPECT_EQ (db::compare_layouts (layout_org, layout_read_gz, db::layout_diff::f_verbose, 0), true); + EXPECT_EQ (db::compare_layouts (layout_read, layout_read_gz, db::layout_diff::f_verbose, 0), true); +} diff --git a/src/tl/tl/tlStream.cc b/src/tl/tl/tlStream.cc index fce7a43ae..caa14015c 100644 --- a/src/tl/tl/tlStream.cc +++ b/src/tl/tl/tlStream.cc @@ -298,7 +298,8 @@ class RawDataDelegate } InputStream::InputStream (InputStreamBase &delegate) - : m_pos (0), mp_bptr (0), mp_delegate (&delegate), m_owns_delegate (false), mp_inflate (0), m_inflate_always (false), m_stop_after_inflate (false) + : m_pos (0), mp_bptr (0), mp_delegate (&delegate), m_owns_delegate (false), mp_inflate (0), m_inflate_always (false), m_stop_after_inflate (false), + m_block_buffer (), mp_block_ptr (0), m_block_avail (0), m_block_active (false) { m_bcap = 4096; // initial buffer capacity m_blen = 0; @@ -309,7 +310,8 @@ InputStream::InputStream (InputStreamBase &delegate) } InputStream::InputStream (InputStreamBase *delegate) - : m_pos (0), mp_bptr (0), mp_delegate (delegate), m_owns_delegate (true), mp_inflate (0), m_inflate_always (false), m_stop_after_inflate (false) + : m_pos (0), mp_bptr (0), mp_delegate (delegate), m_owns_delegate (true), mp_inflate (0), m_inflate_always (false), m_stop_after_inflate (false), + m_block_buffer (), mp_block_ptr (0), m_block_avail (0), m_block_active (false) { m_bcap = 4096; // initial buffer capacity m_blen = 0; @@ -322,7 +324,8 @@ InputStream::InputStream (InputStreamBase *delegate) } InputStream::InputStream (const std::string &abstract_path_in, bool allow_explicit_suffix) - : m_pos (0), mp_bptr (0), mp_delegate (0), m_owns_delegate (false), mp_inflate (0), m_inflate_always (false), m_stop_after_inflate (false) + : m_pos (0), mp_bptr (0), mp_delegate (0), m_owns_delegate (false), mp_inflate (0), m_inflate_always (false), m_stop_after_inflate (false), + m_block_buffer (), mp_block_ptr (0), m_block_avail (0), m_block_active (false) { m_bcap = 4096; // initial buffer capacity m_blen = 0; @@ -570,7 +573,7 @@ std::string InputStream::relative_path (const std::string &path1, const std::str } const char * -InputStream::get (size_t n, bool bypass_inflate) +InputStream::get_slow (size_t n, bool bypass_inflate) { // if deflating, employ the deflate filter to get the data if (mp_inflate && ! bypass_inflate) { @@ -593,6 +596,24 @@ InputStream::get (size_t n, bool bypass_inflate) } } + if (m_block_active) { + + if (m_block_avail > 0) { + + // the data must not reach over the end of the decompressed block - + // the streaming inflate filter behaves the same way + throw tl::Exception (tl::to_string (tr ("Unexpected end of file (DEFLATE implementation)"))); + + } else { + // the decompressed block is exhausted: continue with the raw stream + // and release the block buffer + m_block_active = false; + m_block_buffer.reset (); + mp_block_ptr = 0; + } + + } + if (m_blen < n) { // to keep move activity low, allocate twice as much as required @@ -642,6 +663,12 @@ InputStream::unget (size_t n) // TODO: this will not work if mp_inflate just got destroyed // (no unget into previous compressed block) mp_inflate->unget (n); + } else if (m_block_active) { + // NOTE: same restriction - no unget into a compressed block that has + // been left already (m_block_active is cleared by the first raw get) + tl_assert (mp_block_ptr - n >= m_block_buffer.get ()); + mp_block_ptr -= n; + m_block_avail += n; } else { tl_assert (mp_buffer + n <= mp_bptr); mp_bptr -= n; @@ -671,6 +698,15 @@ InputStream::read_all (size_t max_count) } else { + // first, take what is left of the decompressed block + if (m_block_active && m_block_avail > 0) { + size_t nb = std::min (max_count, m_block_avail); + const char *b = get (nb); + tl_assert (b != 0); + str += std::string (b, nb); + max_count -= nb; + } + while (max_count > 0) { size_t n = std::min (max_count, std::max (size_t (1), m_blen)); const char *b = get (n); @@ -707,6 +743,14 @@ InputStream::read_all () } else { + // first, take what is left of the decompressed block + if (m_block_active && m_block_avail > 0) { + size_t nb = m_block_avail; + const char *b = get (nb); + tl_assert (b != 0); + str += std::string (b, nb); + } + while (true) { size_t n = std::max (size_t (1), m_blen); const char *b = get (n); @@ -736,10 +780,146 @@ void InputStream::inflate (bool stop_after) { tl_assert (mp_inflate == 0); + tl_assert (! m_block_active); mp_inflate = new tl::InflateFilter (*this); m_stop_after_inflate = stop_after; } +void +InputStream::inflate_block (size_t comp_bytes, size_t uncomp_bytes) +{ + tl_assert (mp_inflate == 0); + tl_assert (! m_block_active); + + // NOTE: pos() stays at the end of the compressed data while the block + // is read - the compressed bytes are consumed up front. + + // comp-byte-count 0 means there is no compressed data at all - a case + // we cannot handle here: use the streaming decoder which takes the + // data from the raw stream + if (comp_bytes == 0) { + inflate (); + return; + } + + // uncomp_bytes is a hint only and must not be trusted for the allocation: + // start small and grow the buffer as needed + const size_t max_block_bytes = 0x40000000; // 1GiB + size_t cap = std::min (std::max (uncomp_bytes, size_t (4096)), size_t (1 << 20)); + std::unique_ptr block (new char [cap]); + + z_stream zs = z_stream (); + if (inflateInit2 (&zs, -15 /* == raw DEFLATE data */) != Z_OK) { + throw tl::Exception (tl::to_string (tr ("Unable to initialize the DEFLATE decompressor"))); + } + std::unique_ptr cleanup (&zs, &inflateEnd); + + // takes up to "max" raw bytes; the delegate may deliver less than requested (e.g. pipes), a real EOF is an error + auto next_raw_chunk = [this] (size_t max, size_t &chunk) -> const char * { + chunk = std::min (max, size_t (65536)); + const char *b = get (chunk, true /*bypass_inflate*/); + if (! b) { + chunk = std::min (m_blen, max); + if (chunk == 0) { + throw tl::Exception (tl::to_string (tr ("Unexpected end of file (DEFLATE implementation)"))); + } + b = get (chunk, true /*bypass_inflate*/); + tl_assert (b != 0); + } + return b; + }; + + size_t produced = 0; + size_t to_read = comp_bytes; + bool stream_end = false; + + while (! stream_end) { + + // feed the next chunk of compressed data + if (zs.avail_in == 0) { + + if (to_read == 0) { + // the compressed block ends here, but the DEFLATE stream does not + throw tl::Exception (tl::to_string (tr ("Unexpected end of file (DEFLATE implementation)"))); + } + + size_t chunk = 0; + const char *b = next_raw_chunk (to_read, chunk); + + zs.next_in = (Bytef *) b; + zs.avail_in = (uInt) chunk; + to_read -= chunk; + + } + + while (zs.avail_in > 0 && ! stream_end) { + + zs.next_out = (Bytef *) (block.get () + produced); + zs.avail_out = (uInt) (cap - produced); + + int err = ::inflate (&zs, Z_NO_FLUSH); + + produced = cap - zs.avail_out; + + if (err == Z_STREAM_END) { + stream_end = true; + } else if (err == Z_BUF_ERROR && zs.avail_out == 0) { + // the output buffer is full, but the stream is not finished yet + if (cap >= max_block_bytes) { + throw tl::Exception (tl::sprintf (tl::to_string (tr ("DEFLATE block expands to more than the maximum of %ld bytes")), max_block_bytes)); + } + size_t new_cap = std::min (cap * 2, max_block_bytes); + std::unique_ptr grown (new char [new_cap]); + std::memcpy (grown.get (), block.get (), produced); + block.swap (grown); + cap = new_cap; + } else if (err != Z_OK) { + throw tl::Exception (tl::sprintf (tl::to_string (tr ("Error in DEFLATE decompression: %s")), zs.msg ? zs.msg : "unknown")); + } + + } + + } + + // comp_bytes is authoritative for the start of the next record: skip over + // padding between the end of the DEFLATE stream and the end of the block + // (input left over in the current chunk is gone from the stream anyway) + while (to_read > 0) { + + size_t chunk = 0; + next_raw_chunk (to_read, chunk); + to_read -= chunk; + + } + + m_block_buffer.swap (block); + mp_block_ptr = m_block_buffer.get (); + m_block_avail = produced; + m_block_active = true; +} + +const char * +InputStream::peek_slow (size_t &n) +{ + if (mp_inflate) { + // the streaming inflate filter does not support read-ahead + n = 0; + return 0; + } + + // NOTE: an exhausted decompressed block is deliberately not ended here - + // that would invalidate a pending unget() into the block. The next get() + // ends the block mode. + + if (m_blen == 0 && mp_delegate) { + mp_bptr = mp_buffer; + m_blen = mp_delegate->read (mp_buffer, m_bcap); + } + + n = m_blen; + return m_blen > 0 ? mp_bptr : 0; +} + void InputStream::inflate_always () { @@ -755,14 +935,20 @@ InputStream::close () } } -void +void InputStream::reset () { // stop inflate if (mp_inflate) { delete mp_inflate; mp_inflate = 0; - } + } + + // discard a decompressed block + m_block_buffer.reset (); + mp_block_ptr = 0; + m_block_avail = 0; + m_block_active = false; // optimize for a reset in the first m_bcap bytes // -> this reduces the reset calls on mp_delegate which may not support this diff --git a/src/tl/tl/tlStream.h b/src/tl/tl/tlStream.h index 0b9764611..7e90e39f8 100644 --- a/src/tl/tl/tlStream.h +++ b/src/tl/tl/tlStream.h @@ -26,6 +26,7 @@ #include "tlCommon.h" +#include "tlAssert.h" #include "tlException.h" #include "tlString.h" @@ -33,6 +34,8 @@ #include #include #include +#include +#include namespace tl @@ -422,22 +425,99 @@ class TL_PUBLIC InputStream */ virtual ~InputStream (); - /** + /** * @brief This is the outer read method to call - * - * This implementation obtains data through the + * + * This implementation obtains data through the * protected read call and buffers the data accordingly so * a contigious memory block can be returned. * If inline deflating is enabled, the method will return * inflate data unless "bypass_inflate" is set to true. + * The same is true while a block inflated by "inflate_block" + * is being served - the compressed bytes are gone in that case, + * so "bypass_inflate" has no effect there. * * @return 0 if not enough data can be obtained */ - const char *get (size_t n, bool bypass_inflate = false); + const char *get (size_t n, bool bypass_inflate = false) + { + // inline fast path: the bytes are available in the current buffer + if (mp_inflate == 0) { + + if (m_block_active) { + + if (n <= m_block_avail) { + const char *r = mp_block_ptr; + mp_block_ptr += n; + m_block_avail -= n; + return r; + } + + } else if (n <= m_blen) { + const char *r = mp_bptr; + mp_bptr += n; + m_blen -= n; + m_pos += n; + return r; + } - /** + } + + return get_slow (n, bypass_inflate); + } + + /** + * @brief Peek the next contiguous bytes + * + * This method delivers read-ahead: it returns a pointer to the bytes + * currently available and reports their count in n. The bytes are not + * consumed - use skip() to consume them, or get() to consume and obtain + * them in one step. + * n is the number of bytes available in the current buffer. The buffer + * is filled when it is empty, but n may still be less than desired. + * Returns 0 if no more bytes are available. + * While a block inflated by "inflate_block" is being served, the bytes + * delivered are decompressed ones. The streaming inflate filter (see + * "inflate") does not support read-ahead. + * NOTE: peek does not end the block mode when the decompressed block is + * exhausted - the next get does. This way a pending unget() into the + * block stays valid across a peek. + * The pointer is valid until the next get, unget, skip or peek call. + */ + const char *peek (size_t &n) + { + if (mp_inflate == 0) { + + if (m_block_active && m_block_avail > 0) { + n = m_block_avail; + return mp_block_ptr; + } else if (! m_block_active && m_blen > 0) { + n = m_blen; + return mp_bptr; + } + + } + + return peek_slow (n); + } + + /** + * @brief Skip n bytes + * + * This method consumes n bytes - typically after a peek call. + * n must not be larger than the number of bytes reported by peek. + */ + void skip (size_t n) + { + if (n > 0) { + const char *r = get (n); + tl_assert (r != 0); + } + } + + /** * @brief Undo a previous get call - * + * * This call puts back the bytes read by a previous get call. * Only one call can be made undone. */ @@ -484,6 +564,27 @@ class TL_PUBLIC InputStream */ void inflate_always (); + /** + * @brief Uncompress a DEFLATE-compressed block of known size + * + * This method reads comp_bytes bytes from the raw stream and decompresses + * them in one go into an internal buffer. Subsequent get() calls deliver + * the decompressed bytes. When the buffer is exhausted, reading continues + * with the raw stream right after the compressed bytes: comp_bytes is + * authoritative for that position, bytes between the end of the DEFLATE + * stream and the end of the block are skipped. + * uncomp_bytes is a hint for the initial buffer size only - the buffer + * is grown as needed (up to 1GiB) and exactly the produced bytes are + * served. + * NOTE: pos() stays at the end of the compressed data while the block + * is read. + * The stream must not be in inflate state. + * + * comp_bytes == 0 (no compressed data at all) falls back to the + * streaming inflate() mode. + */ + void inflate_block (size_t comp_bytes, size_t uncomp_bytes); + /** * @brief Obtain the current file position */ @@ -500,7 +601,7 @@ class TL_PUBLIC InputStream */ size_t blen () const { - return m_blen; + return (m_block_active && m_block_avail > 0) ? m_block_avail : m_blen; } /** @@ -609,6 +710,9 @@ class TL_PUBLIC InputStream } private: + const char *get_slow (size_t n, bool bypass_inflate); + const char *peek_slow (size_t &n); + size_t m_pos; char *mp_buffer; size_t m_bcap; @@ -620,11 +724,20 @@ class TL_PUBLIC InputStream std::string m_suffix; bool m_explicit_suffix; - // inflate support + // inflate support InflateFilter *mp_inflate; bool m_inflate_always; bool m_stop_after_inflate; + // inflate-block support: buffer holding the decompressed block, + // read pointer and number of bytes left. m_block_active is also true + // when the buffer is exhausted - only a get on the raw stream ends + // the block mode (this keeps unget() working over the block end). + std::unique_ptr m_block_buffer; + const char *mp_block_ptr; + size_t m_block_avail; + bool m_block_active; + // No copying currently InputStream (const InputStream &); InputStream &operator= (const InputStream &); diff --git a/src/tl/unit_tests/tlDeflateTests.cc b/src/tl/unit_tests/tlDeflateTests.cc index 2278031c5..1840b1e3e 100644 --- a/src/tl/unit_tests/tlDeflateTests.cc +++ b/src/tl/unit_tests/tlDeflateTests.cc @@ -27,6 +27,33 @@ #include "zlib.h" +#include +#include + +static std::string +make_random (size_t n) +{ + std::string d; + d.reserve (n); + unsigned int r = 12345; + for (size_t i = 0; i < n; ++i) { + r = r * 1103515245u + 12345u; + d += char ((r >> 16) & 0xff); + } + return d; +} + +static std::string +deflate_data (const std::string &data) +{ + tl::OutputStringStream oss; + tl::OutputStream os (oss); + tl::DeflateFilter fg (os); + fg.put (data.c_str (), data.size ()); + fg.flush (); + return oss.string (); +} + TEST(1) { unsigned char data[] = { @@ -115,7 +142,211 @@ TEST(3) } EXPECT_EQ (out, hello); - + delete[] hello; } +// inflate_block: round trip over several sizes, with trailing raw bytes +TEST(4) +{ + const size_t sizes[] = { 1, 1000, 100000, 1000000 }; + + for (size_t i = 0; i < sizeof (sizes) / sizeof (sizes[0]); ++i) { + + size_t n = sizes [i]; + + std::string data = make_random (n); + std::string deflated = deflate_data (data); + + // bytes which must be served raw after the decompressed block + std::string trailing = "some bytes after the compressed block"; + std::string all = deflated + trailing; + + tl::InputMemoryStream ims (all.c_str (), all.size ()); + tl::InputStream is (ims); + + is.inflate_block (deflated.size (), data.size ()); + + // the position is right after the compressed block + EXPECT_EQ (is.pos (), deflated.size ()); + + // get/unget works inside the decompressed block + const char *b0 = is.get (1); + EXPECT_EQ (*b0, data [0]); + is.unget (1); + const char *b1 = is.get (1); + EXPECT_EQ (*b1, data [0]); + + std::string out; + out += *b1; + while (out.size () < data.size ()) { + size_t nn = std::min (size_t (65536), data.size () - out.size ()); + const char *b = is.get (nn); + EXPECT_EQ (b != 0, true); + out.append (b, nn); + } + + EXPECT_EQ (out, data); + + // the last block byte can be ungotten even though the block is exhausted + is.unget (1); + const char *lb = is.get (1); + EXPECT_EQ (*lb, data [data.size () - 1]); + + // peek does not end the block mode: a pending unget stays valid + size_t navail = 0; + EXPECT_EQ (is.peek (navail) != 0, true); + is.unget (1); + const char *lb2 = is.get (1); + EXPECT_EQ (*lb2, data [data.size () - 1]); + + // after the block, the raw bytes are served + const char *t = is.get (trailing.size ()); + EXPECT_EQ (t != 0, true); + EXPECT_EQ (std::string (t, trailing.size ()), trailing); + EXPECT_EQ (is.pos (), all.size ()); + + } +} + +// inflate_block: error cases +TEST(5) +{ + std::string data = make_random (10000); + std::string deflated = deflate_data (data); + std::string trailing = "trailing"; + + // truncated compressed data is an error + { + std::string half = deflated.substr (0, deflated.size () / 2); + tl::InputMemoryStream ims (half.c_str (), half.size ()); + tl::InputStream is (ims); + + bool error = false; + try { + is.inflate_block (deflated.size (), data.size ()); + } catch (tl::Exception &) { + error = true; + } + EXPECT_EQ (error, true); + } + + // reading over the end of the decompressed block is an error + // (the streaming inflate filter behaves the same way) + { + std::string all = deflated + trailing; + tl::InputMemoryStream ims (all.c_str (), all.size ()); + tl::InputStream is (ims); + is.inflate_block (deflated.size (), data.size ()); + + bool error = false; + try { + is.get (data.size () + 1); + } catch (tl::Exception &) { + error = true; + } + EXPECT_EQ (error, true); + } + + // a comp-byte-count of 0 falls back to the streaming decoder + { + std::string all = deflated + trailing; + tl::InputMemoryStream ims (all.c_str (), all.size ()); + tl::InputStream is (ims); + is.inflate_block (0, data.size ()); + + std::string out = is.read_all (data.size ()); + EXPECT_EQ (out, data); + + const char *t = is.get (trailing.size ()); + EXPECT_EQ (t != 0, true); + EXPECT_EQ (std::string (t, trailing.size ()), trailing); + } +} + +// inflate_block: lenient byte counts +TEST(7) +{ + std::string data = make_random (10000); + std::string deflated = deflate_data (data); + std::string trailing = "trailing"; + + // comp-byte-count is authoritative: padding between the DEFLATE stream + // and the end of the block is skipped + { + std::string all = deflated + std::string (4, '\0') + trailing; + tl::InputMemoryStream ims (all.c_str (), all.size ()); + tl::InputStream is (ims); + + is.inflate_block (deflated.size () + 4, data.size ()); + + EXPECT_EQ (is.read_all (data.size ()), data); + // the next record starts right after the padding + EXPECT_EQ (is.pos (), deflated.size () + 4); + + const char *t = is.get (trailing.size ()); + EXPECT_EQ (t != 0, true); + EXPECT_EQ (std::string (t, trailing.size ()), trailing); + } + + // uncomp-byte-count too small: what the DEFLATE stream produces wins + { + std::string all = deflated + trailing; + tl::InputMemoryStream ims (all.c_str (), all.size ()); + tl::InputStream is (ims); + is.inflate_block (deflated.size (), data.size () - 1); + + EXPECT_EQ (is.read_all (data.size ()), data); + + const char *t = is.get (trailing.size ()); + EXPECT_EQ (std::string (t, trailing.size ()), trailing); + } + + // uncomp-byte-count too large + { + std::string all = deflated + trailing; + tl::InputMemoryStream ims (all.c_str (), all.size ()); + tl::InputStream is (ims); + is.inflate_block (deflated.size (), data.size () + 1000); + + EXPECT_EQ (is.read_all (data.size ()), data); + + const char *t = is.get (trailing.size ()); + EXPECT_EQ (std::string (t, trailing.size ()), trailing); + } + + // a huge uncomp-byte-count is a hint only: no huge allocation, and the + // data is still read correctly + { + std::string all = deflated + trailing; + tl::InputMemoryStream ims (all.c_str (), all.size ()); + tl::InputStream is (ims); + is.inflate_block (deflated.size (), 0x40000000); + + EXPECT_EQ (is.read_all (data.size ()), data); + + const char *t = is.get (trailing.size ()); + EXPECT_EQ (std::string (t, trailing.size ()), trailing); + } +} + +// inflate_block: read_all over the block boundary +TEST(6) +{ + std::string data = make_random (50000); + std::string deflated = deflate_data (data); + std::string trailing = make_random (3000); + + std::string all = deflated + trailing; + tl::InputMemoryStream ims (all.c_str (), all.size ()); + tl::InputStream is (ims); + + is.inflate_block (deflated.size (), data.size ()); + + // a partial read from the decompressed block + EXPECT_EQ (is.read_all (12345), data.substr (0, 12345)); + + // the block remainder plus the trailing raw bytes + EXPECT_EQ (is.read_all (), data.substr (12345) + trailing); +} +