Author: kotkov
Date: Mon Mar 13 10:37:25 2017
New Revision: 1786654

URL: http://svn.apache.org/viewvc?rev=1786654&view=rev
Log:
Add support for decompressing Brotli (RFC 7932) data and allow decoding
"Content-Encoding: br" responses.

The patch introduces an optional dependency on the Brotli library from
https://github.com/google/brotli and a new bucket that allows decoding
Brotli data.  The new bucket features zero-copy processing, which is only
possible with the new API from the upcoming 1.0.x series of the library.

* serf_bucket_types.h
  (serf_bucket_is_brotli_supported,
   serf_bucket_brotli_decompress_create): Declare new functions.
  (SERF_BUCKET_IS_BROTLI_DECOMPRESS,
   serf_bucket_brotli_decompress_create): Declare new bucket type.

* buckets/brotli_buckets.c:
  New file containing the implementation of the new bucket.  Brotli is
  an optional dependency, so provide function stubs if it is not supported
  and indicate that in serf_bucket_is_brotli_supported().

* buckets/response_buckets.c
  (run_machine): Decode "Content-Encoding: br" if Brotli is supported.

* test/serf_bwtp.c
  (setup_request, setup_channel): Indicate Brotli support in "Accept-Encoding".

* test/serf_get.c
  (setup_request): Indicate Brotli support in "Accept-Encoding".

* test/serf_spider.c
  (setup_request): Indicate Brotli support in "Accept-Encoding".

* test/test_buckets.c
  (test_brotli_decompress_bucket_basic,
   test_brotli_decompress_bucket_truncated_input,
   test_brotli_decompress_bucket_read_bytewise,
   test_brotli_decompress_bucket_chunked_input,
   test_brotli_decompress_bucket_chunked_input2,
   test_brotli_decompress_bucket_garbage_at_end,
   test_brotli_decompress_response_body): New tests.
  (test_buckets): Run new tests if Serf is built with Brotli support.

* SConstruct: Pick up location of the Brotli library from the new
  BROTLI variable.  In case it's set, ensure that we are building
  with an appropriate version of the library and define SERF_HAVE_BROTLI.

Approved by: rhuijben

Added:
    serf/trunk/buckets/brotli_buckets.c   (with props)
Modified:
    serf/trunk/SConstruct
    serf/trunk/buckets/response_buckets.c
    serf/trunk/serf_bucket_types.h
    serf/trunk/test/serf_bwtp.c
    serf/trunk/test/serf_get.c
    serf/trunk/test/serf_spider.c
    serf/trunk/test/test_buckets.c

Modified: serf/trunk/SConstruct
URL: 
http://svn.apache.org/viewvc/serf/trunk/SConstruct?rev=1786654&r1=1786653&r2=1786654&view=diff
==============================================================================
--- serf/trunk/SConstruct (original)
+++ serf/trunk/SConstruct Mon Mar 13 10:37:25 2017
@@ -102,6 +102,10 @@ opts.AddVariables(
                "Path to GSSAPI's install area",
                None,
                None),
+  PathVariable('BROTLI',
+               "Path to Brotli's install area",
+               None,
+               PathVariable.PathIsDir),
   BoolVariable('DEBUG',
                "Enable debugging info and strict compile warnings",
                False),
@@ -200,6 +204,7 @@ apr = str(env['APR'])
 apu = str(env['APU'])
 zlib = str(env['ZLIB'])
 gssapi = env.get('GSSAPI', None)
+brotli = env.get('BROTLI', None)
 
 if gssapi and os.path.isdir(gssapi):
   krb5_config = os.path.join(gssapi, 'bin', 'krb5-config')
@@ -278,6 +283,10 @@ if sys.platform != 'win32':
   if sys.platform == 'sunos5':
     env.Append(LIBS=['m'])
     env.Append(PLATFORM='posix')
+
+  if brotli:
+    env.Append(LIBS=['brotlicommon', 'brotlidec'])
+
 else:
   # Warning level 4, no unused argument warnings
   env.Append(CCFLAGS=['/W4',
@@ -363,6 +372,20 @@ if sys.platform == 'win32':
   else:
     env.Append(CPPPATH=['$OPENSSL/inc32'],
                LIBPATH=['$OPENSSL/out32dll'])
+
+  # brotli
+  if brotli:
+    brotli_libs = 'brotlicommon.lib brotlidec.lib'
+    env.Append(LIBS=['brotlicommon.lib', 'brotlidec.lib'])
+    if not env.get('SOURCE_LAYOUT', None):
+      env.Append(CPPPATH=['$BROTLI/include'],
+                 LIBPATH=['$BROTLI/lib'])
+    else:
+      env.Append(CPPPATH=['$BROTLI/include'],
+                 LIBPATH=['$BROTLI/Release'])
+  else:
+    brotli_libs = ''
+
 else:
   if CALLOUT_OKAY:
     if os.path.isdir(apr):
@@ -415,6 +438,13 @@ else:
     env.Append(CPPPATH=['$OPENSSL/include'])
     env.Append(LIBPATH=['$OPENSSL/lib'])
 
+  if brotli:
+    brotli_libs = '-lbrotlicommon -lbrotlienc'
+    env.Append(CPPPATH=['$BROTLI/include'],
+               LIBPATH=['$BROTLI/lib'])
+  else:
+    brotli_libs = ''
+
 # Check for OpenSSL functions which are only available in some of
 # the versions we support. Also handles forks like LibreSSL.
 conf = Configure(env)
@@ -441,6 +471,16 @@ if gssapi and CALLOUT_OKAY:
 if sys.platform == 'win32':
   env.Append(CPPDEFINES=['SERF_HAVE_SSPI'])
 
+if brotli and CALLOUT_OKAY:
+  conf = Configure(env)
+  if conf.CheckCHeader('brotli/decode.h') and \
+     conf.CheckFunc('BrotliDecoderTakeOutput'):
+    env.Append(CPPDEFINES=['SERF_HAVE_BROTLI'])
+  else:
+    print "Cannot find Brotli library >= 1.0.0 in '%s'." % env.get('BROTLI')
+    Exit(1)
+  env = conf.Finish()
+
 # Set preprocessor define to disable the logging framework
 if disablelogging:
     env.Append(CPPDEFINES=['SERF_DISABLE_LOGGING'])
@@ -460,8 +500,9 @@ pkgconfig = env.Textfile('serf-%d.pc' %
                            '@LIBDIR@': '$LIBDIR',
                            '@INCLUDE_SUBDIR@': 'serf-%d' % (MAJOR,),
                            '@VERSION@': '%d.%d.%d' % (MAJOR, MINOR, PATCH),
-                           '@LIBS@': '%s %s %s -lz' % (apu_libs, apr_libs,
-                                                       env.get('GSSAPI_LIBS', 
'')),
+                           '@LIBS@': '%s %s %s %s -lz' % (apu_libs, apr_libs,
+                                                          
env.get('GSSAPI_LIBS', ''),
+                                                          brotli_libs),
                            })
 
 env.Default(lib_static, lib_shared, pkgconfig)

Added: serf/trunk/buckets/brotli_buckets.c
URL: 
http://svn.apache.org/viewvc/serf/trunk/buckets/brotli_buckets.c?rev=1786654&view=auto
==============================================================================
--- serf/trunk/buckets/brotli_buckets.c (added)
+++ serf/trunk/buckets/brotli_buckets.c Mon Mar 13 10:37:25 2017
@@ -0,0 +1,235 @@
+/* ====================================================================
+ *    Licensed to the Apache Software Foundation (ASF) under one
+ *    or more contributor license agreements.  See the NOTICE file
+ *    distributed with this work for additional information
+ *    regarding copyright ownership.  The ASF licenses this file
+ *    to you under the Apache License, Version 2.0 (the
+ *    "License"); you may not use this file except in compliance
+ *    with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing,
+ *    software distributed under the License is distributed on an
+ *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *    KIND, either express or implied.  See the License for the
+ *    specific language governing permissions and limitations
+ *    under the License.
+ * ====================================================================
+ */
+
+#include "serf.h"
+#include "serf_bucket_util.h"
+#include "serf_private.h"
+
+#ifdef SERF_HAVE_BROTLI
+
+#include <brotli/decode.h>
+
+int serf_bucket_is_brotli_supported()
+{
+    return TRUE;
+}
+
+typedef struct brotli_decompress_context_t {
+    BrotliDecoderState *state;
+    serf_bucket_t *input;
+    serf_bucket_t *output;
+    const char *pending_data;
+    apr_size_t pending_len;
+    /* Did we see an APR_EOF for the input stream? */
+    int hit_eof;
+    /* Did the decoder report the end of the compressed data? */
+    int done;
+} brotli_decompress_context_t;
+
+static void *alloc_func(void *opaque, size_t size)
+{
+    serf_bucket_alloc_t *alloc = opaque;
+
+    return serf_bucket_mem_alloc(alloc, size);
+}
+
+static void free_func(void *opaque, void *block)
+{
+    serf_bucket_alloc_t *alloc = opaque;
+
+    if (block)
+        serf_bucket_mem_free(alloc, block);
+}
+
+/* Implements serf_bucket_aggregate_eof_t */
+static apr_status_t refill_output(void *baton, serf_bucket_t *aggregate_bkt)
+{
+    brotli_decompress_context_t *ctx = baton;
+
+    while (1) {
+        if (ctx->pending_len == 0 && !ctx->hit_eof) {
+            apr_status_t status;
+
+            status = serf_bucket_read(ctx->input, SERF_READ_ALL_AVAIL,
+                                      &ctx->pending_data, &ctx->pending_len);
+            if (APR_STATUS_IS_EOF(status))
+                ctx->hit_eof = TRUE;
+            else if (status)
+                return status;
+        }
+
+        if (ctx->done && ctx->hit_eof && ctx->pending_len == 0) {
+            return APR_EOF;
+        }
+        else if (ctx->done) {
+            /* Finished with some input still there in the bucket, that's
+             * an error. */
+            return SERF_ERROR_DECOMPRESSION_FAILED;
+        }
+        else {
+            BrotliDecoderResult result;
+            apr_size_t avail_out = 0;
+
+            result = BrotliDecoderDecompressStream(
+                         ctx->state, &ctx->pending_len,
+                         (const uint8_t **)&ctx->pending_data, &avail_out,
+                         NULL, NULL);
+
+            if (result == BROTLI_DECODER_RESULT_ERROR) {
+                return SERF_ERROR_DECOMPRESSION_FAILED;
+            }
+            else if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT
+                     && ctx->hit_eof) {
+                /* The decoder says it requires more data, but we don't have
+                 * it.  This could happen either if the input is truncated or
+                 * corrupted, but as we don't know for sure, return a generic
+                 * error. */
+                return SERF_ERROR_DECOMPRESSION_FAILED;
+            }
+            else if (result == BROTLI_DECODER_RESULT_SUCCESS
+                     && !BrotliDecoderHasMoreOutput(ctx->state)) {
+                ctx->done = TRUE;
+            }
+
+            if (BrotliDecoderHasMoreOutput(ctx->state)) {
+                serf_bucket_t *output_bkt;
+                const uint8_t *output;
+                apr_size_t output_len = 0;
+
+                /* There is some output for us.  Place it into the aggregate
+                 * bucket, and avoid making a copy by wrapping a pointer to
+                 * the internal output buffer.  This data is valid until the
+                 * next call to BrotliDecoderDecompressStream(), which won't
+                 * happen until this bucket is read. */
+                output = BrotliDecoderTakeOutput(ctx->state, &output_len);
+                output_bkt = serf_bucket_simple_create((const char *)output,
+                                                       output_len, NULL, NULL,
+                                                       
aggregate_bkt->allocator);
+                serf_bucket_aggregate_append(aggregate_bkt, output_bkt);
+
+                return APR_SUCCESS;
+            }
+        }
+    }
+}
+
+serf_bucket_t *
+serf_bucket_brotli_decompress_create(serf_bucket_t *stream,
+                                     serf_bucket_alloc_t *alloc)
+{
+    brotli_decompress_context_t *ctx =
+        serf_bucket_mem_calloc(alloc, sizeof(*ctx));
+
+    ctx->state = BrotliDecoderCreateInstance(alloc_func, free_func, alloc);
+    ctx->input = stream;
+    ctx->output = serf_bucket_aggregate_create(alloc);
+    ctx->pending_data = NULL;
+    ctx->pending_len = 0;
+    ctx->hit_eof = FALSE;
+    ctx->done = FALSE;
+
+    serf_bucket_aggregate_hold_open(ctx->output, refill_output, ctx);
+
+    return serf_bucket_create(&serf_bucket_type_brotli_decompress, alloc, ctx);
+}
+
+static apr_status_t serf_brotli_decompress_read(serf_bucket_t *bucket,
+                                                apr_size_t requested,
+                                                const char **data,
+                                                apr_size_t *len)
+{
+    brotli_decompress_context_t *ctx = bucket->data;
+
+    return serf_bucket_read(ctx->output, requested, data, len);
+}
+
+static apr_status_t serf_brotli_decompress_readline(serf_bucket_t *bucket,
+                                                    int acceptable, int *found,
+                                                    const char **data,
+                                                    apr_size_t *len)
+{
+    brotli_decompress_context_t *ctx = bucket->data;
+
+    return serf_bucket_readline(ctx->output, acceptable, found, data, len);
+}
+
+static apr_status_t serf_brotli_decompress_peek(serf_bucket_t *bucket,
+                                                const char **data,
+                                                apr_size_t *len)
+{
+    brotli_decompress_context_t *ctx = bucket->data;
+
+    return serf_bucket_peek(ctx->output, data, len);
+}
+
+static void serf_brotli_decompress_destroy_and_data(serf_bucket_t *bucket)
+{
+    brotli_decompress_context_t *ctx = bucket->data;
+
+    BrotliDecoderDestroyInstance(ctx->state);
+    serf_bucket_destroy(ctx->input);
+    serf_bucket_destroy(ctx->output);
+    serf_default_destroy_and_data(bucket);
+}
+
+static apr_status_t serf_brotli_decompress_set_config(serf_bucket_t *bucket,
+                                                      serf_config_t *config)
+{
+    brotli_decompress_context_t *ctx = bucket->data;
+    apr_status_t status;
+
+    status = serf_bucket_set_config(ctx->input, config);
+    if (status)
+        return status;
+
+    return serf_bucket_set_config(ctx->output, config);
+}
+
+const serf_bucket_type_t serf_bucket_type_brotli_decompress = {
+    "BROTLI-DECOMPRESS",
+    serf_brotli_decompress_read,
+    serf_brotli_decompress_readline,
+    serf_default_read_iovec,
+    serf_default_read_for_sendfile,
+    serf_buckets_are_v2,
+    serf_brotli_decompress_peek,
+    serf_brotli_decompress_destroy_and_data,
+    serf_default_read_bucket,
+    serf_default_get_remaining,
+    serf_brotli_decompress_set_config,
+};
+
+#else /* SERF_HAVE_BROTLI */
+
+int serf_bucket_is_brotli_supported()
+{
+    return FALSE;
+}
+
+serf_bucket_t *
+serf_bucket_brotli_decompress_create(serf_bucket_t *stream,
+                                     serf_bucket_alloc_t *alloc)
+{
+    return NULL;
+}
+
+const serf_bucket_type_t serf_bucket_type_brotli_decompress = { 0 };
+
+#endif /* SERF_HAVE_BROTLI */

Propchange: serf/trunk/buckets/brotli_buckets.c
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: serf/trunk/buckets/response_buckets.c
URL: 
http://svn.apache.org/viewvc/serf/trunk/buckets/response_buckets.c?rev=1786654&r1=1786653&r2=1786654&view=diff
==============================================================================
--- serf/trunk/buckets/response_buckets.c (original)
+++ serf/trunk/buckets/response_buckets.c Mon Mar 13 10:37:25 2017
@@ -459,6 +459,14 @@ static apr_status_t run_machine(serf_buc
                                                    SERF_DEFLATE_DEFLATE);
                     serf_bucket_set_config(ctx->body, ctx->config);
                 }
+                else if (serf_bucket_is_brotli_supported()
+                         && v && strcasecmp("br", v) == 0)
+                {
+                    ctx->body =
+                        serf_bucket_brotli_decompress_create(ctx->body,
+                                                             bkt->allocator);
+                    serf_bucket_set_config(ctx->body, ctx->config);
+                }
             }
         }
         break;

Modified: serf/trunk/serf_bucket_types.h
URL: 
http://svn.apache.org/viewvc/serf/trunk/serf_bucket_types.h?rev=1786654&r1=1786653&r2=1786654&view=diff
==============================================================================
--- serf/trunk/serf_bucket_types.h (original)
+++ serf/trunk/serf_bucket_types.h Mon Mar 13 10:37:25 2017
@@ -872,6 +872,28 @@ void serf_bucket_split_create(serf_bucke
                               apr_size_t max_chunk_size);
 
 
+/**
+ * Check if Serf bucket functions support Brotli (RFC 7932) format.
+ * Return non-zero if Brotli is supported and zero otherwise.  If Brotli
+ * is not supported, the behavior of all related bucket functions such
+ * as @a serf_bucket_brotli_decompress_create is undefined.
+ *
+ * @since New in 1.4.
+ */
+int serf_bucket_is_brotli_supported();
+
+/** @since New in 1.4. */
+extern const serf_bucket_type_t serf_bucket_type_brotli_decompress;
+/** @since New in 1.4. */
+#define SERF_BUCKET_IS_BROTLI_DECOMPRESS(b) \
+    SERF_BUCKET_CHECK((b), brotli_decompress)
+
+/** @since New in 1.4. */
+serf_bucket_t *
+serf_bucket_brotli_decompress_create(serf_bucket_t *stream,
+                                     serf_bucket_alloc_t *alloc);
+
+
 /* ### do we need a PIPE bucket type? they are simple apr_file_t objects */
 
 

Modified: serf/trunk/test/serf_bwtp.c
URL: 
http://svn.apache.org/viewvc/serf/trunk/test/serf_bwtp.c?rev=1786654&r1=1786653&r2=1786654&view=diff
==============================================================================
--- serf/trunk/test/serf_bwtp.c (original)
+++ serf/trunk/test/serf_bwtp.c Mon Mar 13 10:37:25 2017
@@ -191,7 +191,10 @@ static apr_status_t setup_request(serf_r
     serf_bucket_headers_setn(hdrs_bkt, "User-Agent",
                              "Serf/" SERF_VERSION_STRING);
     /* Shouldn't serf do this for us? */
-    serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip");
+    if (serf_bucket_is_brotli_supported())
+        serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip, br");
+    else
+        serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip");
 
     if (ctx->authn != NULL) {
         serf_bucket_headers_setn(hdrs_bkt, "Authorization", ctx->authn);
@@ -255,7 +258,10 @@ static apr_status_t setup_channel(serf_r
     serf_bucket_headers_setn(hdrs_bkt, "User-Agent",
                              "Serf/" SERF_VERSION_STRING);
     /* Shouldn't serf do this for us? */
-    serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip");
+    if (serf_bucket_is_brotli_supported())
+        serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip, br");
+    else
+        serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip");
 
     if (ctx->authn != NULL) {
         serf_bucket_headers_setn(hdrs_bkt, "Authorization", ctx->authn);

Modified: serf/trunk/test/serf_get.c
URL: 
http://svn.apache.org/viewvc/serf/trunk/test/serf_get.c?rev=1786654&r1=1786653&r2=1786654&view=diff
==============================================================================
--- serf/trunk/test/serf_get.c (original)
+++ serf/trunk/test/serf_get.c Mon Mar 13 10:37:25 2017
@@ -372,7 +372,10 @@ static apr_status_t setup_request(serf_r
     serf_bucket_headers_setn(hdrs_bkt, "User-Agent",
                              "Serf/" SERF_VERSION_STRING);
     /* Shouldn't serf do this for us? */
-    serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip");
+    if (serf_bucket_is_brotli_supported())
+        serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip, br");
+    else
+        serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip");
 #ifdef CONNECTION_CLOSE_HDR
     serf_bucket_headers_setn(hdrs_bkt, "Connection", "close");
 #endif

Modified: serf/trunk/test/serf_spider.c
URL: 
http://svn.apache.org/viewvc/serf/trunk/test/serf_spider.c?rev=1786654&r1=1786653&r2=1786654&view=diff
==============================================================================
--- serf/trunk/test/serf_spider.c (original)
+++ serf/trunk/test/serf_spider.c Mon Mar 13 10:37:25 2017
@@ -323,7 +323,10 @@ static apr_status_t setup_request(serf_r
                              "Serf/" SERF_VERSION_STRING);
 
     /* Shouldn't serf do this for us? */
-    serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip");
+    if (serf_bucket_is_brotli_supported())
+        serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip, br");
+    else
+        serf_bucket_headers_setn(hdrs_bkt, "Accept-Encoding", "gzip");
 
     if (ctx->app_ctx->authn != NULL) {
         serf_bucket_headers_setn(hdrs_bkt, "Authorization",

Modified: serf/trunk/test/test_buckets.c
URL: 
http://svn.apache.org/viewvc/serf/trunk/test/test_buckets.c?rev=1786654&r1=1786653&r2=1786654&view=diff
==============================================================================
--- serf/trunk/test/test_buckets.c (original)
+++ serf/trunk/test/test_buckets.c Mon Mar 13 10:37:25 2017
@@ -2968,6 +2968,207 @@ static void test_http2_frame_bucket_basi
   serf_bucket_destroy(frame_out);
 }
 
+static void test_brotli_decompress_bucket_basic(CuTest *tc)
+{
+    test_baton_t *tb = tc->testBaton;
+    serf_bucket_t *input;
+    serf_bucket_t *bkt;
+    serf_bucket_alloc_t *alloc = test__create_bucket_allocator(tc, tb->pool);
+
+    {
+        const char input_data[] = {
+            "\x3B"
+        };
+
+        input = serf_bucket_simple_create(input_data, sizeof(input_data) - 1,
+                                          NULL, NULL, alloc);
+        bkt = serf_bucket_brotli_decompress_create(input, alloc);
+        read_and_check_bucket(tc, bkt, "");
+        serf_bucket_destroy(bkt);
+    }
+
+    {
+        const char input_data[] = {
+            "\x8B\x03\x80\x61\x62\x63\x64\x65\x66\x67\x68\x03"
+        };
+
+        input = serf_bucket_simple_create(input_data, sizeof(input_data) - 1,
+                                          NULL, NULL, alloc);
+        bkt = serf_bucket_brotli_decompress_create(input, alloc);
+        read_and_check_bucket(tc, bkt, "abcdefgh");
+        serf_bucket_destroy(bkt);
+    }
+
+    {
+        const char input_data[] = {
+            "\x1B\x0E\x00\x00\x84\x71\xC0\xC6\xDA\x50\x22\x80\x88\x26"
+            "\x81\x14\x35\x1F"
+        };
+
+        input = serf_bucket_simple_create(input_data, sizeof(input_data) - 1,
+                                          NULL, NULL, alloc);
+        bkt = serf_bucket_brotli_decompress_create(input, alloc);
+        read_and_check_bucket(tc, bkt, "aaabbbcccdddeee");
+        serf_bucket_destroy(bkt);
+    }
+}
+
+static void test_brotli_decompress_bucket_truncated_input(CuTest *tc)
+{
+    test_baton_t *tb = tc->testBaton;
+    serf_bucket_t *input;
+    serf_bucket_t *bkt;
+    serf_bucket_alloc_t *alloc = test__create_bucket_allocator(tc, tb->pool);
+    const char input_data[] = {
+        "\x8B\x03\x80\x61\x62\x63\x64\x65\x66\x67\x68\x03"
+    };
+
+    /* Truncate our otherwise valid input. */
+    input = serf_bucket_simple_create(input_data, 5, NULL, NULL, alloc);
+    bkt = serf_bucket_brotli_decompress_create(input, alloc);
+
+    {
+        char buf[1024];
+        apr_size_t len;
+        apr_status_t status;
+
+        status = read_all(bkt, buf, sizeof(buf), &len);
+        CuAssertIntEquals(tc, SERF_ERROR_DECOMPRESSION_FAILED, status);
+    }
+
+    serf_bucket_destroy(bkt);
+}
+
+static void test_brotli_decompress_bucket_read_bytewise(CuTest *tc)
+{
+    test_baton_t *tb = tc->testBaton;
+    serf_bucket_t *input;
+    serf_bucket_t *bkt;
+    serf_bucket_alloc_t *alloc = test__create_bucket_allocator(tc, tb->pool);
+    apr_status_t status;
+    apr_size_t total_read = 0;
+    /* Brotli-encoded sequence of 100,000 zeroes. */
+    const char input_data[] = {
+        "\x5B\x9F\x86\x01\x40\x02\x26\x1E\x0B\x24\xCB\x2F\x00"
+    };
+
+    input = serf_bucket_simple_create(input_data, sizeof(input_data) - 1,
+                                      NULL, NULL, alloc);
+    bkt = serf_bucket_brotli_decompress_create(input, alloc);
+
+    do {
+        const char *data;
+        apr_size_t len;
+
+        status = serf_bucket_read(bkt, 1, &data, &len);
+        if (SERF_BUCKET_READ_ERROR(status))
+            CuFail(tc, "Got error during bucket reading.");
+
+        if (len > 1) {
+            CuFail(tc, "Unexpected read with len > 1.");
+        }
+        else if (len == 1) {
+            CuAssertIntEquals(tc, '0', data[0]);
+            total_read += len;
+        }
+    } while (status != APR_EOF);
+
+    CuAssertIntEquals(tc, 100000, (int)total_read);
+
+    serf_bucket_destroy(bkt);
+}
+
+static void test_brotli_decompress_bucket_chunked_input(CuTest *tc)
+{
+    test_baton_t *tb = tc->testBaton;
+    serf_bucket_t *input;
+    serf_bucket_t *bkt;
+    serf_bucket_alloc_t *alloc = test__create_bucket_allocator(tc, tb->pool);
+
+    /* What if the encoded data spans over multiple chunks?
+     * (And let's throw in a couple of empty chunks as well...) */
+    input = serf_bucket_aggregate_create(alloc);
+    serf_bucket_aggregate_append(input,
+        SERF_BUCKET_SIMPLE_STRING("\x8B\x03", alloc));
+    serf_bucket_aggregate_append(input,
+        SERF_BUCKET_SIMPLE_STRING("\x80\x61\x62", alloc));
+    serf_bucket_aggregate_append(input,
+        SERF_BUCKET_SIMPLE_STRING("", alloc));
+    serf_bucket_aggregate_append(input,
+        SERF_BUCKET_SIMPLE_STRING("\x63\x64\x65\x66\x67\x68\x03", alloc));
+    serf_bucket_aggregate_append(input,
+        SERF_BUCKET_SIMPLE_STRING("", alloc));
+
+    bkt = serf_bucket_brotli_decompress_create(input, alloc);
+    read_and_check_bucket(tc, bkt, "abcdefgh");
+    serf_bucket_destroy(bkt);
+}
+
+static void test_brotli_decompress_bucket_chunked_input2(CuTest *tc)
+{
+    test_baton_t *tb = tc->testBaton;
+    serf_bucket_t *input;
+    serf_bucket_t *bkt;
+    serf_bucket_alloc_t *alloc = test__create_bucket_allocator(tc, tb->pool);
+
+    /* Try an edge case where the valid encoded data (empty string) is
+     * followed by an empty chunk. */
+    input = serf_bucket_aggregate_create(alloc);
+    serf_bucket_aggregate_append(input,
+        SERF_BUCKET_SIMPLE_STRING("\x3B", alloc));
+    serf_bucket_aggregate_append(input,
+        SERF_BUCKET_SIMPLE_STRING("", alloc));
+
+    bkt = serf_bucket_brotli_decompress_create(input, alloc);
+    read_and_check_bucket(tc, bkt, "");
+    serf_bucket_destroy(bkt);
+}
+
+static void test_brotli_decompress_bucket_garbage_at_end(CuTest *tc)
+{
+    test_baton_t *tb = tc->testBaton;
+    serf_bucket_t *input;
+    serf_bucket_t *bkt;
+    serf_bucket_alloc_t *alloc = test__create_bucket_allocator(tc, tb->pool);
+    const char input_data[] = {
+        "\x8B\x03\x80\x61\x62\x63\x64\x65\x66\x67\x68\x03garbage"
+    };
+
+    input = serf_bucket_simple_create(input_data, sizeof(input_data) - 1,
+                                      NULL, NULL, alloc);
+    bkt = serf_bucket_brotli_decompress_create(input, alloc);
+
+    {
+        char buf[1024];
+        apr_size_t len;
+        apr_status_t status;
+
+        status = read_all(bkt, buf, sizeof(buf), &len);
+        CuAssertIntEquals(tc, SERF_ERROR_DECOMPRESSION_FAILED, status);
+    }
+}
+
+static void test_brotli_decompress_response_body(CuTest *tc)
+{
+    test_baton_t *tb = tc->testBaton;
+    serf_bucket_t *input;
+    serf_bucket_t *bkt;
+    serf_bucket_alloc_t *alloc = test__create_bucket_allocator(tc, tb->pool);
+
+    input = SERF_BUCKET_SIMPLE_STRING(
+        "HTTP/1.1 200 OK" CRLF
+        "Content-Type: text/html" CRLF
+        "Content-Length: 12" CRLF
+        "Content-Encoding: br" CRLF
+        CRLF
+        "\x8B\x03\x80\x61\x62\x63\x64\x65\x66\x67\x68\x03",
+        alloc);
+
+    bkt = serf_bucket_response_create(input, alloc);
+    read_and_check_bucket(tc, bkt, "abcdefgh");
+    serf_bucket_destroy(bkt);
+}
+
 CuSuite *test_buckets(void)
 {
     CuSuite *suite = CuSuiteNew();
@@ -3010,6 +3211,15 @@ CuSuite *test_buckets(void)
     SUITE_ADD_TEST(suite, test_hpack_huffman_encode);
     SUITE_ADD_TEST(suite, test_hpack_header_encode);
     SUITE_ADD_TEST(suite, test_http2_frame_bucket_basic);
+#ifdef SERF_HAVE_BROTLI
+    SUITE_ADD_TEST(suite, test_brotli_decompress_bucket_basic);
+    SUITE_ADD_TEST(suite, test_brotli_decompress_bucket_truncated_input);
+    SUITE_ADD_TEST(suite, test_brotli_decompress_bucket_read_bytewise);
+    SUITE_ADD_TEST(suite, test_brotli_decompress_bucket_chunked_input);
+    SUITE_ADD_TEST(suite, test_brotli_decompress_bucket_chunked_input2);
+    SUITE_ADD_TEST(suite, test_brotli_decompress_bucket_garbage_at_end);
+    SUITE_ADD_TEST(suite, test_brotli_decompress_response_body);
+#endif /* SERF_HAVE_BROTLI */
 #if 0
     /* This test for issue #152 takes a lot of time generating 4GB+ of random
        data so it's disabled by default. */


Reply via email to