support decompressing an empty frame into NULL
fix #1385 : 
decompressing into `NULL` is an automatic error in `v1.3.7`.
This patch now allows it, as long as the content of the frame is empty.

This behavior seems to simplify things a bit for `arrow`.
Not sure if any other project may rely on this behavior...
I'm maintaining an arrow-cpp (https://github.com/apache/arrow) package in nixpkgs. arrow-cpp can use zstd as one of the compression backends. Since we made an upgrade for zstd from 1.3.5 to 1.3.6 one of the tests in arrow-cpp started to fail. After some debugging I found that the source of failure is the call to `ZSTD_decompress` with `dstCapacity=0` and `dst=NULL` that is not working in zstd 1.3.6+. I've come up with a minimal reproducing example: 
```c++
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <zstd.h>

#define MAX_UNCOMPRESSED_SIZE 4096
#define MAX_COMPRESSED_SIZE 4096

#define CHECK_ERROR(code) \
  if (ZSTD_isError(code)) { \
    printf("Error: %s\n", ZSTD_getErrorName(code)); \
    exit(EXIT_FAILURE); \
  }

int main(void) {
  char uncompressed[MAX_UNCOMPRESSED_SIZE];
  char compressed[MAX_COMPRESSED_SIZE];
  int compression_level = 1;
  size_t input_len = 0;
  memset(uncompressed, 0, sizeof(uncompressed));
  size_t compressed_size = ZSTD_compress(compressed, MAX_COMPRESSED_SIZE, uncompressed, input_len, compression_level);
  CHECK_ERROR(compressed_size);
  printf("compressed_size = %zu\n", compressed_size);

  {
    printf("test1\n");
    memset(uncompressed, 0, sizeof(uncompressed));
    size_t uncompressed_size = ZSTD_decompress(uncompressed, MAX_UNCOMPRESSED_SIZE, compressed, compressed_size);
    CHECK_ERROR(uncompressed_size);
    printf("uncompressed_size = %zu\n", uncompressed_size);
  }
  {
    printf("test2\n");
    memset(uncompressed, 0, sizeof(uncompressed));
    size_t uncompressed_size = ZSTD_decompress(uncompressed, 0, compressed, compressed_size);
    CHECK_ERROR(uncompressed_size);
    printf("uncompressed_size = %zu\n", uncompressed_size);
  }
  {
    printf("test3\n");
    size_t uncompressed_size = ZSTD_decompress(NULL, 0, compressed, compressed_size);
    CHECK_ERROR(uncompressed_size);
    printf("uncompressed_size = %zu\n", uncompressed_size);
  }
  return EXIT_SUCCESS;
}
```
On zstd 1.3.5:
```
compressed_size = 9
test1
uncompressed_size = 0
test2
uncompressed_size = 0
test3
uncompressed_size = 0
```
On zstd 1.3.7:
```
compressed_size = 9
test1
uncompressed_size = 0
test2
uncompressed_size = 0
test3
Error: Destination buffer is too small
```
I'm not very familiar with arrow-cpp's codebase, but, from what I understand, the situation when `dstCapacity=0` and `dst=NULL` is possible at runtime in arrow-cpp, for example, when reading "parquet" files with empty columns. It also seems that all other decompressors (GZIP, ZLIB, LZ4, SNAPPY, BROTLI) can handle these zero-length output buffers starting at NULL, as they pass the same test. I was wondering if it is possible to address this issue in zstd.
ZSTD_decompress(NULL, 0, ...) returns -ZSTD_error_dstSize_tooSmall
diff --git a/lib/decompress/zstd_decompress.c b/lib/decompress/zstd_decompress.c
index 54b9bf70d4d..7f721e20bab 100644
--- a/lib/decompress/zstd_decompress.c
+++ b/lib/decompress/zstd_decompress.c
@@ -404,9 +404,9 @@ unsigned long long ZSTD_findDecompressedSize(const void* src, size_t srcSize)
 }
 
 /** ZSTD_getDecompressedSize() :
-*   compatible with legacy mode
-*   @return : decompressed size if known, 0 otherwise
-              note : 0 can mean any of the following :
+ *  compatible with legacy mode
+ * @return : decompressed size if known, 0 otherwise
+             note : 0 can mean any of the following :
                    - frame content is empty
                    - decompressed size field is not present in frame header
                    - frame header unknown / not supported
@@ -420,8 +420,8 @@ unsigned long long ZSTD_getDecompressedSize(const void* src, size_t srcSize)
 
 
 /** ZSTD_decodeFrameHeader() :
-*   `headerSize` must be the size provided by ZSTD_frameHeaderSize().
-*   @return : 0 if success, or an error code, which can be tested using ZSTD_isError() */
+ * `headerSize` must be the size provided by ZSTD_frameHeaderSize().
+ * @return : 0 if success, or an error code, which can be tested using ZSTD_isError() */
 static size_t ZSTD_decodeFrameHeader(ZSTD_DCtx* dctx, const void* src, size_t headerSize)
 {
     size_t const result = ZSTD_getFrameHeader_advanced(&(dctx->fParams), src, headerSize, dctx->format);
@@ -439,7 +439,7 @@ static size_t ZSTD_decodeFrameHeader(ZSTD_DCtx* dctx, const void* src, size_t he
  ***************************************************************/
 
 /*! ZSTD_getcBlockSize() :
-*   Provides the size of compressed block from block header `src` */
+ *  Provides the size of compressed block from block header `src`. */
 size_t ZSTD_getcBlockSize(const void* src, size_t srcSize,
                           blockProperties_t* bpPtr)
 {
@@ -459,9 +459,10 @@ size_t ZSTD_getcBlockSize(const void* src, size_t srcSize,
 static size_t ZSTD_copyRawBlock(void* dst, size_t dstCapacity,
                           const void* src, size_t srcSize)
 {
-    if (dst==NULL) return ERROR(dstSize_tooSmall);
+    DEBUGLOG(5, "ZSTD_copyRawBlock");
+    if (dst == NULL) dstCapacity = 0;  /* better safe than sorry */
     if (srcSize > dstCapacity) return ERROR(dstSize_tooSmall);
-    memcpy(dst, src, srcSize);
+    if (dst) memcpy(dst, src, srcSize);
     return srcSize;
 }
 
@@ -1761,7 +1762,9 @@ size_t ZSTD_findFrameCompressedSize(const void *src, size_t srcSize)
 }
 
 /*! ZSTD_decompressFrame() :
-*   @dctx must be properly initialized */
+ * @dctx must be properly initialized
+ *  will update *srcPtr and *srcSizePtr,
+ *  to make *srcPtr progress by one frame. */
 static size_t ZSTD_decompressFrame(ZSTD_DCtx* dctx,
                                    void* dst, size_t dstCapacity,
                              const void** srcPtr, size_t *srcSizePtr)
@@ -1770,31 +1773,33 @@ static size_t ZSTD_decompressFrame(ZSTD_DCtx* dctx,
     BYTE* const ostart = (BYTE* const)dst;
     BYTE* const oend = ostart + dstCapacity;
     BYTE* op = ostart;
-    size_t remainingSize = *srcSizePtr;
+    size_t remainingSrcSize = *srcSizePtr;
+
+    DEBUGLOG(4, "ZSTD_decompressFrame (srcSize:%i)", (int)*srcSizePtr);
 
     /* check */
-    if (remainingSize < ZSTD_frameHeaderSize_min+ZSTD_blockHeaderSize)
+    if (remainingSrcSize < ZSTD_frameHeaderSize_min+ZSTD_blockHeaderSize)
         return ERROR(srcSize_wrong);
 
     /* Frame Header */
     {   size_t const frameHeaderSize = ZSTD_frameHeaderSize(ip, ZSTD_frameHeaderSize_prefix);
         if (ZSTD_isError(frameHeaderSize)) return frameHeaderSize;
-        if (remainingSize < frameHeaderSize+ZSTD_blockHeaderSize)
+        if (remainingSrcSize < frameHeaderSize+ZSTD_blockHeaderSize)
             return ERROR(srcSize_wrong);
         CHECK_F( ZSTD_decodeFrameHeader(dctx, ip, frameHeaderSize) );
-        ip += frameHeaderSize; remainingSize -= frameHeaderSize;
+        ip += frameHeaderSize; remainingSrcSize -= frameHeaderSize;
     }
 
     /* Loop on each block */
     while (1) {
         size_t decodedSize;
         blockProperties_t blockProperties;
-        size_t const cBlockSize = ZSTD_getcBlockSize(ip, remainingSize, &blockProperties);
+        size_t const cBlockSize = ZSTD_getcBlockSize(ip, remainingSrcSize, &blockProperties);
         if (ZSTD_isError(cBlockSize)) return cBlockSize;
 
         ip += ZSTD_blockHeaderSize;
-        remainingSize -= ZSTD_blockHeaderSize;
-        if (cBlockSize > remainingSize) return ERROR(srcSize_wrong);
+        remainingSrcSize -= ZSTD_blockHeaderSize;
+        if (cBlockSize > remainingSrcSize) return ERROR(srcSize_wrong);
 
         switch(blockProperties.blockType)
         {
@@ -1817,7 +1822,7 @@ static size_t ZSTD_decompressFrame(ZSTD_DCtx* dctx,
             XXH64_update(&dctx->xxhState, op, decodedSize);
         op += decodedSize;
         ip += cBlockSize;
-        remainingSize -= cBlockSize;
+        remainingSrcSize -= cBlockSize;
         if (blockProperties.lastBlock) break;
     }
 
@@ -1828,16 +1833,16 @@ static size_t ZSTD_decompressFrame(ZSTD_DCtx* dctx,
     if (dctx->fParams.checksumFlag) { /* Frame content checksum verification */
         U32 const checkCalc = (U32)XXH64_digest(&dctx->xxhState);
         U32 checkRead;
-        if (remainingSize<4) return ERROR(checksum_wrong);
+        if (remainingSrcSize<4) return ERROR(checksum_wrong);
         checkRead = MEM_readLE32(ip);
         if (checkRead != checkCalc) return ERROR(checksum_wrong);
         ip += 4;
-        remainingSize -= 4;
+        remainingSrcSize -= 4;
     }
 
     /* Allow caller to get size read */
     *srcPtr = ip;
-    *srcSizePtr = remainingSize;
+    *srcSizePtr = remainingSrcSize;
     return op-ostart;
 }
 
@@ -1869,7 +1874,9 @@ static size_t ZSTD_decompressMultiFrame(ZSTD_DCtx* dctx,
             if (dctx->staticSize) return ERROR(memory_allocation);
 
             decodedSize = ZSTD_decompressLegacy(dst, dstCapacity, src, frameSize, dict, dictSize);
+            if (ZSTD_isError(decodedSize)) return decodedSize;
 
+            assert(decodedSize <=- dstCapacity);
             dst = (BYTE*)dst + decodedSize;
             dstCapacity -= decodedSize;
 
@@ -1922,7 +1929,7 @@ static size_t ZSTD_decompressMultiFrame(ZSTD_DCtx* dctx,
                 return ERROR(srcSize_wrong);
             }
             if (ZSTD_isError(res)) return res;
-            /* no need to bound check, ZSTD_decompressFrame already has */
+            assert(res <= dstCapacity);
             dst = (BYTE*)dst + res;
             dstCapacity -= res;
         }

diff --git a/tests/fuzzer.c b/tests/fuzzer.c
index 5616285b9ed..d17140392dd 100644
--- a/tests/fuzzer.c
+++ b/tests/fuzzer.c
@@ -411,11 +411,55 @@ static int basicUnitTests(U32 seed, double compressibility)
     DISPLAYLEVEL(3, "OK \n");
 
     DISPLAYLEVEL(3, "test%3d : check CCtx size after compressing empty input : ", testNb++);
-    {   ZSTD_CCtx* cctx = ZSTD_createCCtx();
+    {   ZSTD_CCtx* const cctx = ZSTD_createCCtx();
         size_t const r = ZSTD_compressCCtx(cctx, compressedBuffer, compressedBufferSize, NULL, 0, 19);
         if (ZSTD_isError(r)) goto _output_error;
         if (ZSTD_sizeof_CCtx(cctx) > (1U << 20)) goto _output_error;
         ZSTD_freeCCtx(cctx);
+        cSize = r;
+    }
+    DISPLAYLEVEL(3, "OK \n");
+
+    DISPLAYLEVEL(3, "test%3d : decompress empty frame into NULL : ", testNb++);
+    {   size_t const r = ZSTD_decompress(NULL, 0, compressedBuffer, cSize);
+        if (ZSTD_isError(r)) goto _output_error;
+        if (r != 0) goto _output_error;
+    }
+    {   ZSTD_CCtx* const cctx = ZSTD_createCCtx();
+        ZSTD_outBuffer output;
+        if (cctx==NULL) goto _output_error;
+        output.dst = compressedBuffer;
+        output.size = compressedBufferSize;
+        output.pos = 0;
+        CHECK_Z( ZSTD_initCStream(cctx, 1) );    /* content size unknown */
+        CHECK_Z( ZSTD_flushStream(cctx, &output) );   /* ensure no possibility to "concatenate" and determine the content size */
+        CHECK_Z( ZSTD_endStream(cctx, &output) );
+        ZSTD_freeCCtx(cctx);
+        /* single scan decompression */
+        {   size_t const r = ZSTD_decompress(NULL, 0, compressedBuffer, output.pos);
+            if (ZSTD_isError(r)) goto _output_error;
+            if (r != 0) goto _output_error;
+        }
+        /* streaming decompression */
+        {   ZSTD_DCtx* const dstream = ZSTD_createDStream();
+            ZSTD_inBuffer dinput;
+            ZSTD_outBuffer doutput;
+            size_t ipos;
+            if (dstream==NULL) goto _output_error;
+            dinput.src = compressedBuffer;
+            dinput.size = 0;
+            dinput.pos = 0;
+            doutput.dst = NULL;
+            doutput.size = 0;
+            doutput.pos = 0;
+            CHECK_Z ( ZSTD_initDStream(dstream) );
+            for (ipos=1; ipos<=output.pos; ipos++) {
+                dinput.size = ipos;
+                CHECK_Z ( ZSTD_decompressStream(dstream, &doutput, &dinput) );
+            }
+            if (doutput.pos != 0) goto _output_error;
+            ZSTD_freeDStream(dstream);
+        }
     }
     DISPLAYLEVEL(3, "OK \n");
 

{"all tests": {"run": "PASS", "test": "FAIL", "fix": "PASS"}}
{"all tests": {"run": "PASS", "test": "FAIL", "fix": "PASS"}}
{"passed_count": 1, "failed_count": 0, "skipped_count": 0, "passed_tests": ["all tests"], "failed_tests": [], "skipped_tests": []}
{"passed_count": 0, "failed_count": 1, "skipped_count": 0, "passed_tests": [], "failed_tests": ["all tests"], "skipped_tests": []}
{"passed_count": 1, "failed_count": 0, "skipped_count": 0, "passed_tests": ["all tests"], "failed_tests": [], "skipped_tests": []}