Author: brane
Date: Tue Feb  3 05:53:32 2026
New Revision: 1931681

Log:
Add streaming/framed LZ4 compression.

* subversion/include/private/svn_subr_private.h
  (svn_lz4__compress_ctx_t,
   svn_lz4__decompress_ctx_t): New types.
  (svn_lz4__header_size_max,
   svn_lz4__compress_create,
   svn_lz4__compress_bound,
   svn_lz4__compress_update,
   svn_lz4__compress_flush,
   svn_lz4__compress_end,
   svn_lz4__decompress_create,
   svn_lz4__decompress): New prototypes.

* subversion/libsvn_subr/compress_lz4.c: Include svn_error.h,
   lz2frame.h and lz4hc.c.
  (free_lz4_cctx, free_lz4_dctx, check_compress_status): New helper functions.
  (svn_lz4__compress_ctx_t,
   svn_lz4__decompress_ctx_t): Define the new types.
  (svn_lz4__header_size_max,
   svn_lz4__compress_create,
   svn_lz4__compress_bound,
   svn_lz4__compress_update,
   svn_lz4__compress_flush,
   svn_lz4__compress_end,
   svn_lz4__decompress_create,
   svn_lz4__decompress): Implement the LZ4 compression functions.

* subversion/tests/svn_test.h
  (SVN_TEST_BUFFER_ASSERT): New test assertion for comparing binary buffers.

* subversion/tests/libsvn_subr/compress-test.c: Include apr_time.h,
   svn_error.h and svn_string.h, but not svn_pools.h which was not used.
  (uncompressed, uncompressed_size,
   compressed_block, compressed_block_size,
   compressed_frame, compressed_frame_size,
   empty_frame, empty_frame_size): New, constant data.
  (test_decompress_lz4,
   test_compress_lz4): Use the new constant data.
  (de_compress_lz4): New helper function.
  (test_compress_lz4_random): New test.
  (test_compress_lz4_empty): Use de_compress_lz4().
  (compress_lz4_stream,
   decompress_lz4_stream,
   decompress_lz4_frame,
   de_compress_lz4_stream,
   de_compress_lz4_frame_random): New helper functions.
  (test_decompress_lz4_frame,
   test_decompress_lz4_frame_stable,
   test_decompress_lz4_frame_empty,
   test_compress_lz4_frame_random,
   test_compress_lz4_frame_random_stable,
   test_compress_lz4_frame_no_header_space): New tests.
  (test_funcs): Register the new tests.

Modified:
   
subversion/branches/better-pristines/subversion/include/private/svn_subr_private.h
   subversion/branches/better-pristines/subversion/libsvn_subr/compress_lz4.c
   
subversion/branches/better-pristines/subversion/tests/libsvn_subr/compress-test.c
   subversion/branches/better-pristines/subversion/tests/svn_test.h

Modified: 
subversion/branches/better-pristines/subversion/include/private/svn_subr_private.h
==============================================================================
--- 
subversion/branches/better-pristines/subversion/include/private/svn_subr_private.h
  Tue Feb  3 05:31:33 2026        (r1931680)
+++ 
subversion/branches/better-pristines/subversion/include/private/svn_subr_private.h
  Tue Feb  3 05:53:32 2026        (r1931681)
@@ -625,6 +625,100 @@ svn__decompress_lz4(const void *data, ap
                     svn_stringbuf_t *out,
                     apr_size_t limit);
 
+/*
+ * LZ4 stream compression with framing.
+ */
+
+/* The largest size of an LZ4 frame header; see: LZ4F_HEADER_SIZE_MAX */
+apr_size_t
+svn_lz4__header_size_max(void);
+
+/* LZ4 stream compression and decompression contexts. */
+typedef struct svn_lz4__compress_ctx_t svn_lz4__compress_ctx_t;
+typedef struct svn_lz4__decompress_ctx_t svn_lz4__decompress_ctx_t;
+
+/* Creates a new LZ4 compression context CCTX, allocated from POOL.
+ * The context will be freed when the pool is cleared.
+ *
+ * If STABLE_INPUT is TRUE, the source buffer must be contiguous for the whole
+ * compression; this means that all uncompressed source data is available to
+ * svn_lz4__compress_update() through negative offsets from the INPUT pointer.
+ * When this option can be set, the compressor can avoid copying and caching
+ * source data in CCTX.
+ */
+svn_error_t *
+svn_lz4__compress_create(svn_lz4__compress_ctx_t **cctx,
+                         svn_boolean_t stable_input,
+                         apr_pool_t *pool);
+
+/* Returns the minimum CAPACITY of the output buffer to guarantee that
+ * the next svn_lz4__compress_update() with CCTX and SIZE amount
+ * of input data will succeed. If SIZE is 0, the returned value is
+ * computed for svn_lz4__compress_flush()/_end().
+ */
+apr_size_t
+svn_lz4__compress_bound(svn_lz4__compress_ctx_t *cctx, apr_size_t size);
+
+/* Using the context CCTX, compresses SIZE bytes from INPUT into the OUTPUT
+ * buffer of size CAPACITY. *LENGTH will be the number of bytes actually
+ * written to OUTPUT and can be 0 if the input data was buffered in the
+ * context.
+ *
+ * At the first call of svn_lz4__compress_update(), CAPACITY must be
+ * at least svn_lz4__header_size_max() to accomodate the frame header.
+ */
+svn_error_t *
+svn_lz4__compress_update(apr_size_t *length,
+                         svn_lz4__compress_ctx_t *cctx,
+                         void *output, apr_size_t capacity,
+                         const void *input, apr_size_t size);
+
+/* Flushes any data buffered in CCTX to the OUTPUT buffer of size CAPACITY
+ * and returns the number of bytes written in *LENGTH.
+ */
+svn_error_t *
+svn_lz4__compress_flush(apr_size_t *length,
+                        svn_lz4__compress_ctx_t *cctx,
+                        void *output, apr_size_t capacity);
+
+/* Ends the compression stream, returning the number of bytes written
+ * to OUTPUT in *LENGTH. Afterwards, CCTX will be available for to start
+ * a new compression stream with svn_lz4__compress_update().
+ */
+svn_error_t *
+svn_lz4__compress_end(apr_size_t *length,
+                      svn_lz4__compress_ctx_t *cctx,
+                      void *output, apr_size_t capacity);
+
+/* Creates a new LZ4 decompression context DCTX, allocated from POOL.
+ * The context will be freed when the pool is cleared.
+ *
+ * If STABLE_OUTPUT is TRUE, the output buffer must be contiguous for the whole
+ * decompression; this means that previously decompressed content is available
+ * to svn_lz4__decompress() through negative offsets from the OUTPUT pointer.
+ * This is the case, for example, when we store the decompressed data to a
+ * stringbuf, which remains contiguous even when it's reallocated. When this
+ * option can be set, the decompressor can avoid copying and caching output
+ * data in DCTX.
+ */
+svn_error_t *
+svn_lz4__decompress_create(svn_lz4__decompress_ctx_t **dctx,
+                           svn_boolean_t stable_output,
+                           apr_pool_t *pool);
+
+/* Using DCTX, read at most *INPUT_SIZE compressed bytes from the INPUT buffer
+ * and decompress them to the OUTPUT buffer of size *OUTPUT_SIZE. Upon return,
+ * *INPUT_SIZE will be the number of bytes actally read from INPUT and
+ * *OUTPUT_SIZE will be the number of bytes written to *OUTPUT. The returned
+ * *SIZE_HINT is a hint for the expected OUTPUT_SIZE for the next call, or 0
+ * if the frame has been completely decoded.
+ */
+svn_error_t *
+svn_lz4__decompress(apr_size_t *size_hint,
+                    svn_lz4__decompress_ctx_t *dctx,
+                    void *output, apr_size_t *output_size,
+                    const void *input, apr_size_t *input_size);
+
 /** @} */
 
 /**

Modified: 
subversion/branches/better-pristines/subversion/libsvn_subr/compress_lz4.c
==============================================================================
--- subversion/branches/better-pristines/subversion/libsvn_subr/compress_lz4.c  
Tue Feb  3 05:31:33 2026        (r1931680)
+++ subversion/branches/better-pristines/subversion/libsvn_subr/compress_lz4.c  
Tue Feb  3 05:53:32 2026        (r1931681)
@@ -23,16 +23,26 @@
 
 #include <assert.h>
 
+#include "svn_error.h"
 #include "private/svn_subr_private.h"
 
 #include "svn_private_config.h"
+#include "svn_types.h"
 
 #ifdef SVN_INTERNAL_LZ4
 #include "lz4/lz4internal.h"
+#include "lz4/lz4frame.h"
+#include "lz4/lz4hc.h"
 #else
 #include <lz4.h>
+#include <lz4frame.h>
+#include <lz4hc.h>
 #endif
 
+/*
+ * Simple compression and decompression
+ */
+
 svn_error_t *
 svn__compress_lz4(const void *data, apr_size_t len,
                   svn_stringbuf_t *out)
@@ -127,6 +137,212 @@ svn__decompress_lz4(const void *data, ap
   return SVN_NO_ERROR;
 }
 
+/*
+ * Streamy compression
+ */
+
+apr_size_t
+svn_lz4__header_size_max(void)
+{
+  return LZ4F_HEADER_SIZE_MAX;
+}
+
+struct svn_lz4__compress_ctx_t
+{
+  LZ4F_cctx *ctx;
+  unsigned started;
+  LZ4F_preferences_t prefs;
+  LZ4F_compressOptions_t options;
+};
+
+static apr_status_t free_lz4_cctx(void *data)
+{
+  svn_lz4__compress_ctx_t *const cctx = data;
+  const LZ4F_errorCode_t code = LZ4F_freeCompressionContext(cctx->ctx);
+  if (LZ4F_isError(code))
+    return SVN_ERR_LZ4_COMPRESSION_FAILED;
+  return APR_SUCCESS;
+}
+
+svn_error_t *
+svn_lz4__compress_create(svn_lz4__compress_ctx_t **cctx_out,
+                         svn_boolean_t stable_input,
+                         apr_pool_t *pool)
+{
+  static const LZ4F_preferences_t compression_prefs = {
+    {
+      LZ4F_max64KB,               /* frameInfo.blockSizeID */
+      LZ4F_blockLinked,           /* frameInfo.blockMode */
+      LZ4F_noContentChecksum,     /* frameInfo.contentChecksumFlag */
+      LZ4F_frame,                 /* frameInfo.frameType */
+      0,                          /* frameInfo.contentSize */
+      0,                          /* frameInfo.dictID */
+      LZ4F_blockChecksumEnabled   /* frameInfo.blockChecksumFlag */
+    },
+    /* NOTE: With LZ4 1.10+, the compression level will be 2, faster but less
+             compressed than with older versions of LZ4, where the value of
+             this constant was 3. This does not affect compression format
+             backward compatibility. */
+    LZ4HC_CLEVEL_MIN,           /* compressionLevel */
+    0,                          /* autoFlush */
+    1,                          /* favorDecSpeed */
+    { 0 }                       /* reserved */
+  };
+
+  LZ4F_cctx *ctx;
+  svn_lz4__compress_ctx_t *cctx;
+  LZ4F_errorCode_t code = LZ4F_createCompressionContext(&ctx, LZ4F_VERSION);
+
+  if (LZ4F_isError(code))
+    return svn_error_createf(SVN_ERR_LZ4_COMPRESSION_FAILED, NULL,
+                             _("Create LZ4 compression context: %s"),
+                             LZ4F_getErrorName(code));
+
+  cctx = apr_pcalloc(pool, sizeof(*cctx));
+  cctx->ctx = ctx;
+  cctx->prefs = compression_prefs;
+  cctx->options.stableSrc = !!stable_input;
+  apr_pool_cleanup_register(pool, cctx, free_lz4_cctx, apr_pool_cleanup_null);
+  *cctx_out = cctx;
+  return SVN_NO_ERROR;
+}
+
+static SVN__FORCE_INLINE svn_error_t *
+check_compress_status(apr_size_t *length, apr_size_t status, apr_size_t extra)
+{
+  if (LZ4F_isError(status))
+    return svn_error_createf(SVN_ERR_LZ4_COMPRESSION_FAILED, NULL,
+                             _("LZ4 compress: %s"),
+                             LZ4F_getErrorName(status));
+
+  *length = status + extra;
+  return SVN_NO_ERROR;
+}
+
+apr_size_t
+svn_lz4__compress_bound(svn_lz4__compress_ctx_t *cctx,
+                        apr_size_t size)
+{
+  return LZ4F_compressBound(size, &cctx->prefs);
+}
+
+svn_error_t *
+svn_lz4__compress_update(apr_size_t *length,
+                         svn_lz4__compress_ctx_t *cctx,
+                         void *output, apr_size_t capacity,
+                         const void *input, apr_size_t size)
+{
+  apr_size_t header_size = 0;
+  apr_size_t status;
+
+  if (!cctx->started)
+    {
+      if (capacity < LZ4F_HEADER_SIZE_MAX)
+        return svn_error_create(SVN_ERR_LZ4_COMPRESSION_FAILED, NULL,
+                                _("LZ4 compress: no space for frame header"));
+
+      status = LZ4F_compressBegin(cctx->ctx, output, capacity, &cctx->prefs);
+      SVN_ERR(check_compress_status(&header_size, status, 0));
+      output = (char*)output + header_size;
+      capacity -= header_size;
+      cctx->started = TRUE;
+    }
+
+  status = LZ4F_compressUpdate(cctx->ctx, output, capacity,
+                               input, size, &cctx->options);
+  return svn_error_trace(check_compress_status(length, status, header_size));
+}
+
+svn_error_t *
+svn_lz4__compress_flush(apr_size_t *length,
+                        svn_lz4__compress_ctx_t *cctx,
+                        void *output, apr_size_t capacity)
+{
+  const apr_size_t status = LZ4F_flush(cctx->ctx,
+                                       output, capacity,
+                                       &cctx->options);
+  return svn_error_trace(check_compress_status(length, status, 0));
+}
+
+svn_error_t *
+svn_lz4__compress_end(apr_size_t *length,
+                      svn_lz4__compress_ctx_t *cctx,
+                      void *output, apr_size_t capacity)
+{
+  const apr_size_t status = LZ4F_compressEnd(cctx->ctx,
+                                             output, capacity,
+                                             &cctx->options);
+  svn_error_t *const err = check_compress_status(length, status, 0);
+  if (!err)
+    cctx->started = FALSE;
+  return svn_error_trace(err);
+}
+
+/*
+ * Streamy decompression
+ */
+
+struct svn_lz4__decompress_ctx_t
+{
+  LZ4F_dctx* ctx;
+  LZ4F_decompressOptions_t options;
+};
+
+static apr_status_t free_lz4_dctx(void *data)
+{
+  svn_lz4__decompress_ctx_t *const dctx = data;
+  const LZ4F_errorCode_t code = LZ4F_freeDecompressionContext(dctx->ctx);
+  if (LZ4F_isError(code))
+    return SVN_ERR_LZ4_DECOMPRESSION_FAILED;
+  return APR_SUCCESS;
+}
+
+svn_error_t *
+svn_lz4__decompress_create(svn_lz4__decompress_ctx_t **dctx_out,
+                           svn_boolean_t stable_output,
+                           apr_pool_t *pool)
+{
+  LZ4F_dctx* ctx;
+  svn_lz4__decompress_ctx_t *dctx;
+  LZ4F_errorCode_t code = LZ4F_createDecompressionContext(&ctx, LZ4F_VERSION);
+
+  if (LZ4F_isError(code))
+    return svn_error_createf(SVN_ERR_LZ4_DECOMPRESSION_FAILED, NULL,
+                             _("Create LZ4 decompression context: %s"),
+                             LZ4F_getErrorName(code));
+
+  dctx = apr_pcalloc(pool, sizeof(*dctx));
+  dctx->ctx = ctx;
+  dctx->options.stableDst = !!stable_output;
+  apr_pool_cleanup_register(pool, dctx, free_lz4_dctx, apr_pool_cleanup_null);
+  *dctx_out = dctx;
+  return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_lz4__decompress(apr_size_t *size_hint,
+                    svn_lz4__decompress_ctx_t *dctx,
+                    void *output, apr_size_t *output_size,
+                    const void *input, apr_size_t *input_size)
+{
+  const apr_size_t status = LZ4F_decompress(dctx->ctx,
+                                            output, output_size,
+                                            input, input_size,
+                                            &dctx->options);
+
+  if (LZ4F_isError(status))
+    return svn_error_createf(SVN_ERR_LZ4_DECOMPRESSION_FAILED, NULL,
+                             _("LZ4 decompress: %s"),
+                             LZ4F_getErrorName(status));
+
+  *size_hint = status;
+  return SVN_NO_ERROR;
+}
+
+/*
+ * Library version
+ */
+
 const char *
 svn_lz4__compiled_version(void)
 {

Modified: 
subversion/branches/better-pristines/subversion/tests/libsvn_subr/compress-test.c
==============================================================================
--- 
subversion/branches/better-pristines/subversion/tests/libsvn_subr/compress-test.c
   Tue Feb  3 05:31:33 2026        (r1931680)
+++ 
subversion/branches/better-pristines/subversion/tests/libsvn_subr/compress-test.c
   Tue Feb  3 05:53:32 2026        (r1931681)
@@ -21,25 +21,55 @@
  * ====================================================================
  */
 
-#include "svn_pools.h"
+#include <apr_time.h>
+
 #include "private/svn_subr_private.h"
 #include "../svn_test.h"
+#include "svn_error.h"
+#include "svn_string.h"
+
+
+static const char uncompressed[] =
+  "aaaabbbbccccaaaaccccbbbbaaaabbbb"
+  "aaaabbbbccccaaaaccccbbbbaaaabbbb"
+  "aaaabbbbccccaaaaccccbbbbaaaabbbb";
+static const apr_size_t uncompressed_size = sizeof(uncompressed);
+
+static const char compressed_block[] =
+  "\x61\xc0\x61\x61\x61\x61\x62\x62\x62\x62\x63\x63\x63\x63\x0c\x00\x00\x08"
+  "\x00\x00\x10\x00\x00\x0c\x00\x08\x08\x00\x00\x18\x00\x00\x14\x00\x00\x08"
+  "\x00\x08\x18\x00\x00\x14\x00\x00\x10\x00\x00\x18\x00\x00\x0c\x00\x00\x08"
+  "\x00\x00\x10\x00\x90\x61\x61\x61\x61\x62\x62\x62\x62";
+static const apr_size_t compressed_block_size = sizeof(compressed_block);
+
+/* Generated by piping the uncompressed data through lz4:
+ *   echo -n "..." | lz4 --best --no-frame-crc -BD -BX \
+ *       | hexdump -v -e '/1 "\\x%02x"'
+ */
+static const char compressed_frame[] =
+  "\x04\x22\x4d\x18\x70\x40\xad\x22\x00\x00\x00\xc0\x61\x61\x61\x61\x62\x62"
+  "\x62\x62\x63\x63\x63\x63\x0c\x00\x00\x08\x00\x00\x10\x00\x04\x18\x00\x0f"
+  "\x20\x00\x28\x50\x61\x62\x62\x62\x62\x78\x16\xca\xbf\x00\x00\x00\x00";
+static const apr_size_t compressed_frame_size = sizeof(compressed_frame) - 1;
+
+/* Generated with:
+ *   echo -n "" | lz4 --best --no-frame-crc -BD -BX \
+ *       | hexdump -v -e '/1 "\\x%02x"'
+ */
+static const char empty_frame[] =
+  "\x04\x22\x4d\x18\x70\x40\xad\x00\x00\x00\x00";
+static const apr_size_t empty_frame_size = sizeof(empty_frame) - 1;
+
 
 static svn_error_t *
 test_decompress_lz4(apr_pool_t *pool)
 {
-  const char input[] =
-    "\x61\xc0\x61\x61\x61\x61\x62\x62\x62\x62\x63\x63\x63\x63\x0c\x00\x00\x08"
-    "\x00\x00\x10\x00\x00\x0c\x00\x08\x08\x00\x00\x18\x00\x00\x14\x00\x00\x08"
-    "\x00\x08\x18\x00\x00\x14\x00\x00\x10\x00\x00\x18\x00\x00\x0c\x00\x00\x08"
-    "\x00\x00\x10\x00\x90\x61\x61\x61\x61\x62\x62\x62\x62";
   svn_stringbuf_t *decompressed = svn_stringbuf_create_empty(pool);
 
-  SVN_ERR(svn__decompress_lz4(input, sizeof(input), decompressed, 100));
-  SVN_TEST_STRING_ASSERT(decompressed->data,
-                         "aaaabbbbccccaaaaccccbbbbaaaabbbb"
-                         "aaaabbbbccccaaaaccccbbbbaaaabbbb"
-                         "aaaabbbbccccaaaaccccbbbbaaaabbbb");
+  SVN_ERR(svn__decompress_lz4(compressed_block, compressed_block_size,
+                              decompressed, uncompressed_size));
+  SVN_TEST_INT_ASSERT(decompressed->len, uncompressed_size);
+  SVN_TEST_STRING_ASSERT(decompressed->data, uncompressed);
 
   return SVN_NO_ERROR;
 }
@@ -47,35 +77,227 @@ test_decompress_lz4(apr_pool_t *pool)
 static svn_error_t *
 test_compress_lz4(apr_pool_t *pool)
 {
-  const char input[] =
-    "aaaabbbbccccaaaaccccbbbbaaaabbbb"
-    "aaaabbbbccccaaaaccccbbbbaaaabbbb"
-    "aaaabbbbccccaaaaccccbbbbaaaabbbb";
+  svn_stringbuf_t *compressed = svn_stringbuf_create_empty(pool);
+  SVN_ERR(svn__compress_lz4(uncompressed, uncompressed_size, compressed));
+  SVN_TEST_INT_ASSERT(compressed->len, compressed_block_size);
+  SVN_TEST_BUFFER_ASSERT(compressed->data, compressed_block, compressed->len);
+
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+de_compress_lz4(const char input[], apr_size_t length, apr_pool_t *pool)
+{
   svn_stringbuf_t *compressed = svn_stringbuf_create_empty(pool);
   svn_stringbuf_t *decompressed = svn_stringbuf_create_empty(pool);
 
-  SVN_ERR(svn__compress_lz4(input, sizeof(input), compressed));
+  SVN_ERR(svn__compress_lz4(input, length, compressed));
   SVN_ERR(svn__decompress_lz4(compressed->data, compressed->len,
-                              decompressed, 100));
-  SVN_TEST_STRING_ASSERT(decompressed->data, input);
+                              decompressed, length));
+  SVN_TEST_INT_ASSERT(decompressed->len, length);
+  SVN_TEST_BUFFER_ASSERT(decompressed->data, input, decompressed->len);
 
   return SVN_NO_ERROR;
 }
 
 static svn_error_t *
+test_compress_lz4_random(apr_pool_t *pool)
+{
+  const apr_size_t length = 2047;
+  const apr_uint32_t initial_seed = (apr_uint32_t)apr_time_now();
+  apr_uint32_t seed = initial_seed;
+  void *data = svn_test_make_random_data(&seed, length, pool);
+  svn_error_t *err = de_compress_lz4(data, length, pool);
+  if (err)
+    fprintf(stderr, "SEED: %lu\n", (unsigned long) initial_seed);
+  return err;
+}
+
+static svn_error_t *
 test_compress_lz4_empty(apr_pool_t *pool)
 {
-  svn_stringbuf_t *compressed = svn_stringbuf_create_empty(pool);
-  svn_stringbuf_t *decompressed = svn_stringbuf_create_empty(pool);
+  return de_compress_lz4("", 0, pool);
+}
 
-  SVN_ERR(svn__compress_lz4("", 0, compressed));
-  SVN_ERR(svn__decompress_lz4(compressed->data, compressed->len,
-                              decompressed, 100));
-  SVN_TEST_STRING_ASSERT(decompressed->data, "");
 
+static svn_error_t *
+compress_lz4_stream(svn_stringbuf_t **compressed,
+                    const char *input, apr_size_t length,
+                    svn_boolean_t stable, apr_pool_t *pool)
+{
+  const apr_size_t block_size = 2 * svn_lz4__header_size_max();
+
+  svn_stringbuf_t *buf = svn_stringbuf_create_empty(pool);
+  svn_lz4__compress_ctx_t *cctx;
+  apr_size_t input_read = 0;
+  apr_size_t output_size;
+
+  SVN_ERR(svn_lz4__compress_create(&cctx, stable, pool));
+  while (input_read < length)
+    {
+      const apr_size_t remaining = length - input_read;
+      const apr_size_t input_size = (remaining > block_size
+                                     ? block_size : remaining);
+
+      output_size = svn_lz4__compress_bound(cctx, input_size);
+      svn_stringbuf_ensure(buf, buf->len + output_size);
+      SVN_ERR(svn_lz4__compress_update(&output_size, cctx,
+                                       buf->data + buf->len, output_size,
+                                       input, input_size));
+      buf->len += output_size;
+      input_read += input_size;
+      input += input_size;
+    }
+
+  output_size = svn_lz4__compress_bound(cctx, 0);
+  svn_stringbuf_ensure(buf, buf->len + output_size);
+  SVN_ERR(svn_lz4__compress_end(&output_size, cctx,
+                                buf->data + buf->len, output_size));
+  buf->len += output_size;
+
+  *compressed = buf;
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+decompress_lz4_stream(svn_stringbuf_t **decompressed,
+                      const char *input, apr_size_t length,
+                      svn_boolean_t stable, apr_pool_t *pool)
+{
+  const apr_size_t block_size = 2 * svn_lz4__header_size_max();
+
+  svn_stringbuf_t *buf = svn_stringbuf_create_empty(pool);
+  svn_lz4__decompress_ctx_t *dctx;
+  apr_size_t input_read = 0;
+  apr_size_t size_hint = 1;
+
+  SVN_ERR(svn_lz4__decompress_create(&dctx, stable, pool));
+  while (input_read <= length && size_hint > 0)
+    {
+      const apr_size_t remaining = length - input_read;
+      apr_size_t input_size = (remaining > block_size
+                               ? block_size : remaining);
+      apr_size_t output_size = block_size;
+
+      svn_stringbuf_ensure(buf, buf->len + output_size);
+      SVN_ERR(svn_lz4__decompress(&size_hint, dctx,
+                                  buf->data + buf->len, &output_size,
+                                  input, &input_size));
+      buf->len += output_size;
+      input_read += input_size;
+      input += input_size;
+    }
+  SVN_TEST_INT_ASSERT(size_hint, 0);
+  SVN_TEST_INT_ASSERT(input_read, length);
+
+  *decompressed = buf;
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+decompress_lz4_frame(svn_boolean_t stable, apr_pool_t *pool)
+{
+  svn_stringbuf_t *decompressed;
+
+  SVN_ERR(decompress_lz4_stream(&decompressed,
+                                compressed_frame,
+                                compressed_frame_size,
+                                stable, pool));
+
+  /* Make sure the output buffer is NUL-terminated */
+  svn_stringbuf_appendbyte(decompressed, '\0');
+  SVN_TEST_STRING_ASSERT(decompressed->data, uncompressed);
+
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+test_decompress_lz4_frame(apr_pool_t *pool)
+{
+  return decompress_lz4_frame(FALSE, pool);
+}
+
+static svn_error_t *
+test_decompress_lz4_frame_stable(apr_pool_t *pool)
+{
+  return decompress_lz4_frame(TRUE, pool);
+}
+
+static svn_error_t *
+test_decompress_lz4_frame_empty(apr_pool_t *pool)
+{
+  svn_stringbuf_t *decompressed;
+  SVN_ERR(decompress_lz4_stream(&decompressed,
+                                empty_frame, empty_frame_size,
+                                FALSE, pool));
+  SVN_TEST_INT_ASSERT(decompressed->len, 0);
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+de_compress_lz4_stream(const char *input, apr_size_t length,
+                       svn_boolean_t stable, apr_pool_t *pool)
+{
+  svn_stringbuf_t *compressed;
+  svn_stringbuf_t *decompressed;
+
+  SVN_ERR(compress_lz4_stream(&compressed, input, length, stable, pool));
+  SVN_ERR(decompress_lz4_stream(&decompressed,
+                                compressed->data, compressed->len,
+                                stable, pool));
+  SVN_TEST_INT_ASSERT(decompressed->len, length);
+  SVN_TEST_BUFFER_ASSERT(decompressed->data, input, decompressed->len);
   return SVN_NO_ERROR;
 }
 
+static svn_error_t *
+de_compress_lz4_frame_random(svn_boolean_t stable, apr_pool_t *pool)
+{
+  const apr_size_t length = 2047;
+  const apr_uint32_t initial_seed = (apr_uint32_t)apr_time_now();
+  apr_uint32_t seed = initial_seed;
+  void *data = svn_test_make_random_data(&seed, length, pool);
+  svn_error_t *err = de_compress_lz4_stream(data, length, stable, pool);
+  if (err)
+    fprintf(stderr, "SEED: %lu\n", (unsigned long) initial_seed);
+  return err;
+}
+
+static svn_error_t *
+test_compress_lz4_frame_random(apr_pool_t *pool)
+{
+  return de_compress_lz4_frame_random(FALSE, pool);
+}
+
+static svn_error_t *
+test_compress_lz4_frame_random_stable(apr_pool_t *pool)
+{
+  return de_compress_lz4_frame_random(TRUE, pool);
+}
+
+static svn_error_t *
+test_compress_lz4_frame_no_header_space(apr_pool_t *pool)
+{
+  const apr_size_t block_size = svn_lz4__header_size_max() - 1;
+  svn_stringbuf_t *compressed = svn_stringbuf_create_ensure(block_size, pool);
+  svn_lz4__compress_ctx_t *cctx;
+  apr_size_t length;
+  svn_error_t *err;
+
+  SVN_ERR(svn_lz4__compress_create(&cctx, FALSE, pool));
+
+  /* First update checks there's space for the frame header
+     before calling LZ4F_compressBegin(), and we just made
+     the space too small. */
+  err = svn_lz4__compress_update(&length, cctx,
+                                 compressed->data, block_size,
+                                 "", 0);
+  svn_error_clear(err);
+  SVN_TEST_ASSERT(err != SVN_NO_ERROR);
+  return SVN_NO_ERROR;
+}
+
+
 static int max_threads = -1;
 
 static struct svn_test_descriptor_t test_funcs[] =
@@ -85,8 +307,22 @@ static struct svn_test_descriptor_t test
                  "test svn__decompress_lz4()"),
   SVN_TEST_PASS2(test_compress_lz4,
                  "test svn__compress_lz4()"),
+  SVN_TEST_PASS2(test_compress_lz4_random,
+                 "test svn__de/compress_lz4() with random input"),
   SVN_TEST_PASS2(test_compress_lz4_empty,
-                 "test svn__compress_lz4() with empty input"),
+                 "test svn__de/compress_lz4() with empty input"),
+  SVN_TEST_PASS2(test_decompress_lz4_frame,
+                 "test svn_lz4__decompress()"),
+  SVN_TEST_PASS2(test_decompress_lz4_frame_stable,
+                 "test svn_lz4__decompress() with stable output"),
+  SVN_TEST_PASS2(test_decompress_lz4_frame_empty,
+                 "test svn_lz4__decompress() with empty input"),
+  SVN_TEST_PASS2(test_compress_lz4_frame_random,
+                 "test svn_lz4__de/compress() with random input"),
+  SVN_TEST_PASS2(test_compress_lz4_frame_random_stable,
+                 "test svn_lz4__de/compress() with stable output"),
+  SVN_TEST_PASS2(test_compress_lz4_frame_no_header_space,
+                 "test svn_lz4__compress() with small buffer"),
   SVN_TEST_NULL
 };
 

Modified: subversion/branches/better-pristines/subversion/tests/svn_test.h
==============================================================================
--- subversion/branches/better-pristines/subversion/tests/svn_test.h    Tue Feb 
 3 05:31:33 2026        (r1931680)
+++ subversion/branches/better-pristines/subversion/tests/svn_test.h    Tue Feb 
 3 05:53:32 2026        (r1931681)
@@ -141,6 +141,40 @@ extern "C" {
           tst_str2, tst_str1, __FILE__, __LINE__);                  \
   } while(0)
 
+/** Handy macro for testing buffer equality.
+ *
+ * EXPR and/or EXPECTED_EXPR may be NULL which compares equal to NULL and
+ * not equal to any non-NULL buffer.
+ */
+#define SVN_TEST_BUFFER_ASSERT(expr, expected_expr, size)           \
+  do {                                                              \
+    const char *tst_buf1 = (expr);                                  \
+    const char *tst_buf2 = (expected_expr);                         \
+    const apr_size_t tst_size = (size);                             \
+                                                                    \
+    if (tst_buf2 == NULL && tst_buf1 == NULL)                       \
+      break;                                                        \
+    if (tst_buf1 == NULL)                                           \
+      return svn_error_createf(SVN_ERR_TEST_FAILED, NULL,           \
+          "Buffers not equal\n  Expected: data\n  Found:    NULL"   \
+          "\n  at %s:%d",                                           \
+          __FILE__, __LINE__);                                      \
+    if (tst_buf2 == NULL)                                           \
+      return svn_error_createf(SVN_ERR_TEST_FAILED, NULL,           \
+          "Buffers not equal\n  Expected: NULL\n  Found:    data"   \
+          "\n  at %s:%d",                                           \
+          __FILE__, __LINE__);                                      \
+    if (memcmp(tst_buf2, tst_buf1, tst_size) != 0) {                \
+      apr_size_t i = 0;                                             \
+      while (i < tst_size && tst_buf2[i] == tst_buf1[i])            \
+        ++i;                                                        \
+      return svn_error_createf(SVN_ERR_TEST_FAILED, NULL,           \
+          "Buffers not equal at position %" APR_SIZE_T_FMT          \
+          " of %" APR_SIZE_T_FMT " at %s:%d",                       \
+          i, tst_size, __FILE__, __LINE__);                         \
+    }                                                               \
+  } while(0)
+
  /** Handy macro for testing integer equality.
   */
 #define SVN_TEST_INT_ASSERT(expr, expected_expr)                  \

Reply via email to