Browse Source

LibCompress: Brotli support metadata of skip_length=0

The relevant RFC section from
https://www.rfc-editor.org/rfc/rfc7932#section-9.2

MSKIPBYTES * 8 bits: MSKIPLEN - 1, where MSKIPLEN is
       the number of metadata bytes; this field is
       only present if MSKIPBYTES is positive;
       otherwise, MSKIPLEN is 0 (if MSKIPBYTES is
       greater than 1, and the last byte is all
       zeros, then the stream should be rejected as
       invalid)

So when skip_bytes is zero we need to break and
re-align bytes.

Added the relevant test case that demonstrates this from:
https://github.com/google/brotli/blob/master/tests/testdata/x.compressed
Tommy Murphy 2 years ago
parent
commit
0ee98c69c1

+ 5 - 0
Tests/LibCompress/TestBrotli.cpp

@@ -80,6 +80,11 @@ TEST_CASE(brotli_single_z)
     run_test("single-z.txt"sv);
 }
 
+TEST_CASE(brotli_single_x)
+{
+    run_test("single-x.txt"sv);
+}
+
 TEST_CASE(brotli_decompress_zero_one_bin)
 {
     // This makes sure that the tests will run both on target and in Lagom.

+ 1 - 0
Tests/LibCompress/brotli-test-files/single-x.txt

@@ -0,0 +1 @@
+X

BIN
Tests/LibCompress/brotli-test-files/single-x.txt.br


+ 47 - 1
Userland/Libraries/LibCompress/Brotli.cpp

@@ -589,22 +589,59 @@ ErrorOr<Bytes> BrotliDecompressionStream::read(Bytes output_buffer)
             if (m_read_final_block)
                 break;
 
+            // RFC 7932 section 9.1
+            //
+            // 1 bit:  ISLAST, set to 1 if this is the last meta-block
             m_read_final_block = TRY(m_input_stream.read_bit());
             if (m_read_final_block) {
+                // 1 bit:  ISLASTEMPTY, if set to 1, the meta-block is empty; this
+                //       field is only present if ISLAST bit is set -- if it is 1,
+                //       then the meta-block and the brotli stream ends at that
+                //       bit, with any remaining bits in the last byte of the
+                //       compressed stream filled with zeros (if the fill bits are
+                //       not zero, then the stream should be rejected as invalid)
                 bool is_last_block_empty = TRY(m_input_stream.read_bit());
                 // If the last block is empty we are done decompressing
                 if (is_last_block_empty)
                     break;
             }
 
+            // 2 bits: MNIBBLES, number of nibbles to represent the uncompressed
+            //         length
             size_t size_number_of_nibbles = TRY(read_size_number_of_nibbles());
+
+            // If MNIBBLES is 0, the meta-block is empty, i.e., it does
+            // not generate any uncompressed data.  In this case, the
+            // rest of the meta-block has the following format:
             if (size_number_of_nibbles == 0) {
-                // This block only contains meta-data
+
+                // 1 bit:  reserved, must be zero
                 bool reserved = TRY(m_input_stream.read_bit());
                 if (reserved)
                     return Error::from_string_literal("invalid reserved bit");
 
+                // 2 bits: MSKIPBYTES, number of bytes to represent
+                //         metadata length
+                //
+                // MSKIPBYTES * 8 bits: MSKIPLEN - 1, where MSKIPLEN is
+                //    the number of metadata bytes; this field is
+                //    only present if MSKIPBYTES is positive;
+                //    otherwise, MSKIPLEN is 0 (if MSKIPBYTES is
+                //    greater than 1, and the last byte is all
+                //    zeros, then the stream should be rejected as
+                //    invalid)
                 size_t skip_bytes = TRY(m_input_stream.read_bits(2));
+                if (skip_bytes == 0) {
+                    // 0..7 bits: fill bits until the next byte boundary,
+                    //         must be all zeros
+                    u8 remainder = m_input_stream.align_to_byte_boundary();
+                    if (remainder != 0)
+                        return Error::from_string_literal("remainder bits are non-zero");
+                    continue;
+                }
+
+                // MSKIPLEN bytes of metadata, not part of the
+                //         uncompressed data or the sliding window
                 size_t skip_length = 1 + TRY(m_input_stream.read_bits(8 * skip_bytes));
 
                 u8 remainder = m_input_stream.align_to_byte_boundary();
@@ -619,6 +656,8 @@ ErrorOr<Bytes> BrotliDecompressionStream::read(Bytes output_buffer)
                     auto metadata_bytes = TRY(m_input_stream.read(temp_bytes_slice));
                     if (metadata_bytes.is_empty())
                         return Error::from_string_literal("eof");
+                    if (metadata_bytes.last() == 0)
+                        return Error::from_string_literal("invalid stream");
                     skip_length -= metadata_bytes.size();
                 }
 
@@ -626,6 +665,13 @@ ErrorOr<Bytes> BrotliDecompressionStream::read(Bytes output_buffer)
             }
 
             size_t uncompressed_size = 1 + TRY(m_input_stream.read_bits(4 * size_number_of_nibbles));
+
+            // 1 bit:  ISUNCOMPRESSED, if set to 1, any bits of compressed data
+            //       up to the next byte boundary are ignored, and the rest of
+            //       the meta-block contains MLEN bytes of literal data; this
+            //       field is only present if the ISLAST bit is not set (if the
+            //       ignored bits are not all zeros, the stream should be
+            //       rejected as invalid)
             bool is_uncompressed = false;
             if (!m_read_final_block)
                 is_uncompressed = TRY(m_input_stream.read_bit());