:: commit c07972934a55aa97900dd17ed44814ff2dd94b7c

Kamila Szewczyk <27734421+kspalaiologos@users.noreply.github.com> — 2022-10-23 10:47

parents: 1f8165b30f

High-level API (#58)

diff --git a/.gitignore b/.gitignore
index 23a2c3e..666d93a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,9 @@ autom4te.cache/
 !/build-aux/ax_pthread.m4
 !/build-aux/git-version-gen
 bzip3.pc
+
+examples/hl-api
+
+examples/compress-file
+
+examples/decompress-file
diff --git a/Makefile.am b/Makefile.am
index 7ce9ac5..782cb30 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -56,7 +56,7 @@ dist-hook:
 
 .PHONY: format
 format: $(bzip3_SOURCES) $(include_HEADERS) $(noinst_HEADERS)
-	clang-format -i $^
+	clang-format -i $^ examples/*.c
 
 .PHONY: cloc
 cloc: $(bzip3_SOURCES) $(include_HEADERS) $(noinst_HEADERS)
diff --git a/NEWS b/NEWS
index fe04945..97c18b4 100644
--- a/NEWS
+++ b/NEWS
@@ -36,3 +36,9 @@ v1.1.6:
 
 v1.1.7:
 * rename unbzip3 as bunzip3 for bzip2/gzip/lzip/... compatibility.
+* high level api for libbzip3: `bz3_bound`, `bz3_compress` and `bz3_decompress`.
+* more robust decompression; safety checks for the RLE and LZP steps.
+* documentation for the frame format.
+* examples of bzip3 API usage, AFL fuzzing instructions.
+* `bz3_version` API function
+* more robust I/O handling and fsync (linux only) calls to ensure a correct I/O transaction.
diff --git a/doc/file_format.md b/doc/file_format.md
index 84a7274..c2e6fd1 100644
--- a/doc/file_format.md
+++ b/doc/file_format.md
@@ -1,7 +1,7 @@
 
 # The bzip3 file format
 
-## The header
+## The file header
 
 Each bzip3-compressed file starts with the marker `BZ3v1`. After the signature, the compressor encodes a 32-bit number signifying the maximum block size in bytes in the file. As such, no block after decompression in the stream can exceed it. The maximum block size must be between 65KiB and 511MiB.
 
@@ -20,7 +20,9 @@ static void write_neutral_s32(u8 * data, s32 value) {
 }
 ```
 
-## The chunks
+After the file header, the bzip3-compressed file contains a series of independent blocks.
+
+## The blocks
 
 After the header, the file may contain an unlimited amount of chunks. Each chunk starts with the _new_ size - a 32-bit integer signifying the _compressed_ size of the block, and the _old_ size - a 32-bit integer signifying the _decompressed_ size. Then, a sequence of bzip3-compressed data follows. CRC32 checking is left up to libbz3.
 
@@ -33,3 +35,7 @@ Otherwise, the chunk starts with the 32-bit CRC32 checksum value, the Burrows-Wh
 - No other bit can be set in the _model_.
 
 The size of libbz3's block header can be calculated using the formula `popcnt(model) * 4 + 9`.
+
+## The frame format
+
+The bzip3 frame format is a concatenation of bzip3-compressed blocks. It's used exclusively by the `bz3_compress` and `bz3_decompress` function. Each frame start with the ASCII "BZ3v1" signature, followed by the 32-bit maximum block size in bytes and the 32-bit amount of blocks in the frame. After the 13 byte header, a sequence of independent blocks follows.
diff --git a/etc/bitflip.c b/etc/bitflip.c
deleted file mode 100644
index 298595e..0000000
--- a/etc/bitflip.c
+++ /dev/null
@@ -1,52 +0,0 @@
-
-// If it wasn't obvious, you're expected to compile this with UBSan and ASan.
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <signal.h>
-#include <time.h>
-#include <libbz3.h>
-
-uint8_t * new_buf;
-uint32_t orig_size, new_size;
-FILE * dest;
-uint32_t n = 0;
-
-void try_flip(struct bz3_state * s) {
-    uint32_t idx = rand() % new_size, bit = rand() % 8;
-    new_buf[idx] ^= (1 << bit);
-    fseek(dest, idx, SEEK_SET); fputc(new_buf[idx], dest); fflush(dest);
-
-    // The operation will fail. It's just important so that it doesn't segfault/access wrong memory.
-    bz3_decode_block(s, new_buf, new_size, orig_size);
-
-    printf("\r%d", n++); fflush(stdout);
-}
-
-int main(void) {
-    FILE * f = fopen("shakespeare.txt", "r");
-    if (!f) { perror("fopen"); return 1; }
-    fseek(f, 0, SEEK_END);
-    size_t size = ftell(f);
-    fseek(f, 0, SEEK_SET);
-    uint8_t * buf = malloc(size);
-    if (!buf) { perror("malloc"); return 1; }
-    if (fread(buf, 1, size, f) != size) { perror("fread"); return 1; }
-    fclose(f);
-    struct bz3_state * s = bz3_new(6 * 1024 * 1024);
-    if (!s) { printf("error in bz3_new.\n"); return 1; }
-    int32_t len = bz3_encode_block(s, buf, size);
-    if (len < 0) { printf("error in bz3_compress. %s.\n", bz3_strerror(s)); return 1; }
-    new_buf = malloc(size + size / 50 + 32);
-    if (!new_buf) { perror("malloc"); return 1; }
-    orig_size = size;
-    new_size = len;
-    dest = fopen("shakespeare.bz3", "w");
-    if (!dest) { perror("fopen"); return 1; }
-    srand(time(NULL));
-    fwrite(buf, 1, len, dest);
-    while(1) {
-        memcpy(new_buf, buf, len);
-        try_flip(s);
-    }
-}
diff --git a/examples/compress-file.c b/examples/compress-file.c
new file mode 100644
index 0000000..c6338d6
--- /dev/null
+++ b/examples/compress-file.c
@@ -0,0 +1,44 @@
+
+/* Compress a file SEQUENTIALLY (i.e. *not* in parallel) using bzip3 high level API with block size of 6 MB. */
+/* This is just a demonstration of bzip3 library usage, it does not contain all the necessary error checks and will not
+ * support cross-endian encoding/decoding. */
+
+#include <libbz3.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#define MB (1024 * 1024)
+
+int main(int argc, char ** argv) {
+    if (argc != 3) {
+        printf("Usage: %s <input file> <output file>\n");
+        return 1;
+    }
+
+    // Read the entire input file to memory:
+    FILE * fp = fopen(argv[1], "rb");
+    fseek(fp, 0, SEEK_END);
+    size_t size = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    uint8_t * buffer = malloc(size);
+    fread(buffer, 1, size, fp);
+    fclose(fp);
+
+    // Compress the file:
+    size_t out_size = bz3_bound(size);
+    uint8_t * outbuf = malloc(out_size);
+    int bzerr = bz3_compress(6 * MB, buffer, outbuf, size, &out_size);
+    if (bzerr != BZ3_OK) {
+        printf("bz3_compress() failed with error code %d", bzerr);
+        return 1;
+    }
+
+    FILE * outfp = fopen(argv[2], "wb");
+    /* XXX: Doesn't preserve endianess. We should write the `size_t` value manually with known endianess. */
+    fwrite(&size, 1, sizeof(size_t), outfp);
+    fwrite(outbuf, 1, out_size, outfp);
+    fclose(outfp);
+
+    printf("OK, %d => %d\n", size, out_size);
+    return 0;
+}
diff --git a/examples/decompress-file.c b/examples/decompress-file.c
new file mode 100644
index 0000000..65ec477
--- /dev/null
+++ b/examples/decompress-file.c
@@ -0,0 +1,40 @@
+
+/* Decompress a file SEQUENTIALLY (i.e. *not* in parallel) using bzip3 high level API. */
+/* This is just a demonstration of bzip3 library usage, it does not contain all the necessary error checks and will not
+ * support cross-endian encoding/decoding. */
+
+#include <libbz3.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(int argc, char ** argv) {
+    if (argc != 3) {
+        printf("Usage: %s <input file> <output file>");
+        return 1;
+    }
+
+    // Read the entire input file to memory:
+    FILE * fp = fopen(argv[1], "rb");
+    fseek(fp, 0, SEEK_END);
+    size_t size = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    uint8_t * buffer = malloc(size);
+    fread(buffer, 1, size, fp);
+    fclose(fp);
+
+    // Decompress the file:
+    size_t orig_size = *(size_t *)buffer;
+    uint8_t * outbuf = malloc(orig_size);
+    int bzerr = bz3_decompress(buffer + sizeof(size_t), outbuf, size - sizeof(size_t), &orig_size);
+    if (bzerr != BZ3_OK) {
+        printf("bz3_decompress() failed with error code %d", bzerr);
+        return 1;
+    }
+
+    FILE * outfp = fopen(argv[2], "wb");
+    fwrite(outbuf, 1, orig_size, outfp);
+    fclose(outfp);
+
+    printf("OK, %d => %d", size, orig_size);
+    return 0;
+}
diff --git a/examples/fuzz.c b/examples/fuzz.c
new file mode 100644
index 0000000..c811941
--- /dev/null
+++ b/examples/fuzz.c
@@ -0,0 +1,46 @@
+
+/* A tiny utility for fuzzing bzip3.
+ *
+ * Instructions:
+ * mkdir -p afl_in && mkdir -p afl_out
+ * ./compress-file ../Makefile afl_in/a.bz3
+ * afl-clang examples/fuzz.c -Iinclude src/libbz3.c -o examples/fuzz -g3 "-DVERSION=\"0.0.0\"" -O3 -march=native
+ * AFL_SKIP_CPUFREQ=1 afl-fuzz -i afl_in -o afl_out -- ./decompress-fuzz @@
+ *
+ * If you find a crash, consider also doing the following:
+ * gcc examples/fuzz.c src/libbz3.c -g3 -O3 -march=native -o examples/fuzz_asan -Iinclude "-DVERSION=\"0.0.0\""
+ * -fsanitize=undefined -fsanitize=address
+ *
+ * And run fuzz_asan on the crashing test case. Attach the test case /and/ the output of fuzz_asan to the bug report.
+ */
+
+#include <libbz3.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(int argc, char ** argv) {
+    // Read the entire input file to memory:
+    FILE * fp = fopen(argv[1], "rb");
+    fseek(fp, 0, SEEK_END);
+    size_t size = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    volatile uint8_t * buffer = malloc(size);
+    fread(buffer, 1, size, fp);
+    fclose(fp);
+
+    // Decompress the file:
+    size_t orig_size = *(size_t *)buffer;
+    if (orig_size >= 0x10000000) {
+        // Sanity check: don't allocate more than 256MB.
+        return 0;
+    }
+    uint8_t * outbuf = malloc(orig_size);
+    int bzerr = bz3_decompress(buffer + sizeof(size_t), outbuf, size - sizeof(size_t), &orig_size);
+    if (bzerr != BZ3_OK) {
+        printf("bz3_decompress() failed with error code %d", bzerr);
+        return 1;
+    }
+
+    printf("OK, %d => %d", size, orig_size);
+    return 0;
+}
diff --git a/examples/hl-api.c b/examples/hl-api.c
new file mode 100644
index 0000000..e65b52d
--- /dev/null
+++ b/examples/hl-api.c
@@ -0,0 +1,43 @@
+
+#include <libbz3.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#define MB (1024 * 1024)
+
+int main(void) {
+    printf("Compressing shakespeare.txt back and forth in memory.\n");
+
+    // Read the entire "shakespeare.txt" file to memory:
+    FILE * fp = fopen("shakespeare.txt", "rb");
+    fseek(fp, 0, SEEK_END);
+    size_t size = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    char * buffer = malloc(size);
+    fread(buffer, 1, size, fp);
+    fclose(fp);
+
+    // Compress the file:
+    size_t out_size = bz3_bound(size);
+    char * outbuf = malloc(out_size);
+    int bzerr = bz3_compress(1 * MB, buffer, outbuf, size, &out_size);
+    if (bzerr != BZ3_OK) {
+        printf("bz3_compress() failed with error code %d", bzerr);
+        return 1;
+    }
+
+    printf("%d => %d\n", size, out_size);
+
+    // Decompress the file.
+    bzerr = bz3_decompress(outbuf, buffer, out_size, &size);
+    if (bzerr != BZ3_OK) {
+        printf("bz3_decompress() failed with error code %d", bzerr);
+        return 1;
+    }
+
+    printf("%d => %d\n", out_size, size);
+
+    free(buffer);
+    free(outbuf);
+    return 0;
+}
diff --git a/etc/shakespeare.txt b/examples/shakespeare.txt
similarity index 100%
rename from etc/shakespeare.txt
rename to examples/shakespeare.txt
diff --git a/include/common.h b/include/common.h
index 1fd3904..589456b 100644
--- a/include/common.h
+++ b/include/common.h
@@ -35,7 +35,7 @@ typedef int8_t s8;
 typedef int16_t s16;
 typedef int32_t s32;
 
-static s32 read_neutral_s32(u8 * data) {
+static s32 read_neutral_s32(const u8 * data) {
     return ((u32)data[0]) | (((u32)data[1]) << 8) | (((u32)data[2]) << 16) | (((u32)data[3]) << 24);
 }
 
@@ -74,11 +74,13 @@ static void write_neutral_s32(u8 * data, s32 value) {
 #if defined(HAS_BUILTIN_PREFECTCH)
     #define prefetch(address) __builtin_prefetch((const void *)(address), 0, 0)
     #define prefetchw(address) __builtin_prefetch((const void *)(address), 1, 0)
-#elif defined(_M_IX86) || defined(_M_AMD64) || defined(__x86_64__) || defined(i386) || defined(__i386__) || defined(__i386)
+#elif defined(_M_IX86) || defined(_M_AMD64) || defined(__x86_64__) || defined(i386) || defined(__i386__) || \
+    defined(__i386)
     #include <intrin.h>
     #define prefetch(address) _mm_prefetch((const void *)(address), _MM_HINT_NTA)
     #define prefetchw(address) _m_prefetchw((const void *)(address))
-#elif defined(_M_ARM) || defined(__ARM_ARCH_7__) || defined(__ARM_ARCH_7A__) || defined(__ARM_ARCH_7R__) || defined(__ARM_ARCH_7M__) || defined(__ARM_ARCH_7S__)
+#elif defined(_M_ARM) || defined(__ARM_ARCH_7__) || defined(__ARM_ARCH_7A__) || defined(__ARM_ARCH_7R__) || \
+    defined(__ARM_ARCH_7M__) || defined(__ARM_ARCH_7S__)
     #include <intrin.h>
     #define prefetch(address) __prefetch((const void *)(address))
     #define prefetchw(address) __prefetchw((const void *)(address))
diff --git a/include/getopt-shim.h b/include/getopt-shim.h
index 0e7e6b5..6a7a9db 100644
--- a/include/getopt-shim.h
+++ b/include/getopt-shim.h
@@ -25,8 +25,8 @@
 #define _GETOPT_H
 
 #ifdef WIN32
-static void flockfile(FILE *f) { _lock_file(f); }
-static void funlockfile(FILE *f) { _unlock_file(f); }
+static void flockfile(FILE * f) { _lock_file(f); }
+static void funlockfile(FILE * f) { _unlock_file(f); }
 #endif
 
 int getopt(int, char * const[], const char *);
diff --git a/include/libbz3.h b/include/libbz3.h
index 76cddea..c9b377b 100644
--- a/include/libbz3.h
+++ b/include/libbz3.h
@@ -20,20 +20,21 @@
 #ifndef _LIBBZ3_H
 #define _LIBBZ3_H
 
+#include <stddef.h>
 #include <stdint.h>
 
 /* Symbol visibility control. */
 #ifndef BZIP3_VISIBLE
     #if defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__MINGW32__)
-        #define BZIP3_VISIBLE __attribute__ ((visibility ("default")))
+        #define BZIP3_VISIBLE __attribute__((visibility("default")))
     #else
         #define BZIP3_VISIBLE
     #endif
 #endif
 
-#if defined(BZIP3_DLL_EXPORT) && (BZIP3_DLL_EXPORT==1)
+#if defined(BZIP3_DLL_EXPORT) && (BZIP3_DLL_EXPORT == 1)
     #define BZIP3_API __declspec(dllexport) BZIP3_VISIBLE
-#elif defined(BZIP3_DLL_IMPORT) && (BZIP3_DLL_IMPORT==1)
+#elif defined(BZIP3_DLL_IMPORT) && (BZIP3_DLL_IMPORT == 1)
     #define BZIP3_API __declspec(dllimport) BZIP3_VISIBLE
 #else
     #define BZIP3_API BZIP3_VISIBLE
@@ -46,6 +47,7 @@
 #define BZ3_ERR_MALFORMED_HEADER -4
 #define BZ3_ERR_TRUNCATED_DATA -5
 #define BZ3_ERR_DATA_TOO_BIG -6
+#define BZ3_ERR_INIT -7
 
 struct bz3_state;
 
@@ -76,17 +78,45 @@ BZIP3_API struct bz3_state * bz3_new(int32_t block_size);
  */
 BZIP3_API void bz3_free(struct bz3_state * state);
 
+/**
+ * @brief Return the recommended size of the output buffer for the compression functions.
+ */
+BZIP3_API size_t bz3_bound(size_t input_size);
+
+/* ** HIGH LEVEL APIs ** */
+
+/**
+ * @brief Compress a block of data. This function does not support parallelism
+ * by itself, consider using the low level `bz3_encode_blocks()` function instead.
+ * Using the low level API might provide better performance.
+ * Returns a bzip3 error code; BZ3_OK when the operation is successful.
+ * Make sure to set out_size to the size of the output buffer before the operation;
+ * out_size must be at least equal to `bz3_bound(in_size)'.
+ */
+BZIP3_API int bz3_compress(uint32_t block_size, const uint8_t * in, uint8_t * out, size_t in_size, size_t * out_size);
+
+/**
+ * @brief Decompress a block of data. This function does not support parallelism
+ * by itself, consider using the low level `bz3_decode_blocks()` function instad.
+ * Using the low level API might provide better performance.
+ * Returns a bzip3 error code; BZ3_OK when the operation is successful.
+ * Make sure to set out_size to the size of the output buffer before the operation.
+ */
+BZIP3_API int bz3_decompress(const uint8_t * in, uint8_t * out, size_t in_size, size_t * out_size);
+
+/* ** LOW LEVEL APIs ** */
+
 /**
  * @brief Encode a single block. Returns the amount of bytes written to `buffer'.
- * `buffer' must be able to hold at least `size + size / 50 + 32' bytes. The size must not
+ * `buffer' must be able to hold at least `bz3_bound(size)' bytes. The size must not
  * exceed the block size associated with the state.
  */
 BZIP3_API int32_t bz3_encode_block(struct bz3_state * state, uint8_t * buffer, int32_t size);
 
 /**
  * @brief Decode a single block.
- * `buffer' must be able to hold at least `size + size / 50 + 32' bytes. The size must not exceed
- * the block size associated with the state.
+ * `buffer' must be able to hold at least `orig_size' bytes. The size must not exceed the block size
+ * associated with the state.
  * @param size The size of the compressed data in `buffer'
  * @param orig_size The original size of the data before compression.
  */
diff --git a/src/libbz3.c b/src/libbz3.c
index 6d64c64..dcd4cbd 100644
--- a/src/libbz3.c
+++ b/src/libbz3.c
@@ -156,14 +156,15 @@ static s32 lzp_encode_block(const u8 * RESTRICT in, const u8 * in_end, u8 * REST
     return out >= out_eob ? -1 : (s32)(out - outs);
 }
 
-static s32 lzp_decode_block(const u8 * RESTRICT in, const u8 * in_end, s32 * RESTRICT lut, u8 * RESTRICT out) {
+static s32 lzp_decode_block(const u8 * RESTRICT in, const u8 * in_end, s32 * RESTRICT lut, u8 * RESTRICT out,
+                            const u8 * out_end) {
     const u8 * outs = out;
 
     for (s32 i = 0; i < 4; ++i) *out++ = *in++;
 
     u32 ctx = ((u32)out[-1]) | (((u32)out[-2]) << 8) | (((u32)out[-3]) << 16) | (((u32)out[-4]) << 24);
 
-    while (in < in_end) {
+    while (in < in_end && out < out_end) {
         u32 idx = (ctx >> 15 ^ ctx ^ ctx >> 3) & ((s32)(1 << LZP_DICTIONARY) - 1);
         s32 val = lut[idx];
         lut[idx] = (s32)(out - outs);
@@ -172,14 +173,16 @@ static s32 lzp_decode_block(const u8 * RESTRICT in, const u8 * in_end, s32 * RES
             if (*in != 255) {
                 s32 len = LZP_MIN_MATCH;
                 while (1) {
+                    if (in == in_end) return -1;
                     len += *in;
                     if (*in++ != 254) break;
                 }
 
                 const u8 * ref = outs + val;
-                u8 * out_end = out + len;
+                u8 * oe = out + len;
+                if (oe > out_end) return -1;
 
-                while (out < out_end) *out++ = *ref++;
+                while (out < oe) *out++ = *ref++;
 
                 ctx = ((u32)out[-1]) | (((u32)out[-2]) << 8) | (((u32)out[-3]) << 16) | (((u32)out[-4]) << 24);
             } else {
@@ -202,12 +205,12 @@ static s32 lzp_compress(const u8 * RESTRICT in, u8 * RESTRICT out, s32 n, s32 *
     return lzp_encode_block(in, in + n, out, out + n, lut);
 }
 
-static s32 lzp_decompress(const u8 * RESTRICT in, u8 * RESTRICT out, s32 n, s32 * RESTRICT lut) {
+static s32 lzp_decompress(const u8 * RESTRICT in, u8 * RESTRICT out, s32 n, s32 max, s32 * RESTRICT lut) {
     if (n < 4) return -1;
 
     memset(lut, 0, sizeof(s32) * (1 << LZP_DICTIONARY));
 
-    return lzp_decode_block(in, in + n, lut, out);
+    return lzp_decode_block(in, in + n, lut, out, out + max);
 }
 
 /* RLE code. Unlike RLE in other compressors, we collapse all runs if they yield a net gain
@@ -272,7 +275,7 @@ static void mrled(u8 * RESTRICT in, u8 * RESTRICT out, s32 outlen) {
             for (run = 0; (pc = in[ip++]) == 255; run += 255)
                 ;
             run += pc + 1;
-            for (; run > 0; --run) out[op++] = c;
+            for (; run > 0 && op < outlen; --run) out[op++] = c;
         } else
             out[op++] = c;
     }
@@ -457,6 +460,8 @@ BZIP3_API s8 bz3_last_error(struct bz3_state * state) { return state->last_error
 
 BZIP3_API const char * bz3_version(void) { return VERSION; }
 
+BZIP3_API size_t bz3_bound(size_t input_size) { return input_size + input_size / 50 + 32; }
+
 BZIP3_API const char * bz3_strerror(struct bz3_state * state) {
     switch (state->last_error) {
         case BZ3_OK:
@@ -680,7 +685,11 @@ BZIP3_API s32 bz3_decode_block(struct bz3_state * state, u8 * buffer, s32 data_s
 
     // Undo LZP
     if (model & 2) {
-        size_src = lzp_decompress(b1, b2, lzp_size, state->lzp_lut);
+        size_src = lzp_decompress(b1, b2, lzp_size, state->block_size, state->lzp_lut);
+        if (size_src == -1) {
+            state->last_error = BZ3_ERR_CRC;
+            return -1;
+        }
         swap(b1, b2);
     }
 
@@ -753,8 +762,7 @@ BZIP3_API void bz3_encode_blocks(struct bz3_state * states[], u8 * buffers[], s3
     for (s32 i = 0; i < n; i++) sizes[i] = messages[i].size;
 }
 
-BZIP3_API void bz3_decode_blocks(struct bz3_state * states[], u8 * buffers[], s32 sizes[],
-                                 s32 orig_sizes[], s32 n) {
+BZIP3_API void bz3_decode_blocks(struct bz3_state * states[], u8 * buffers[], s32 sizes[], s32 orig_sizes[], s32 n) {
     decode_thread_msg messages[n];
     pthread_t threads[n];
     for (s32 i = 0; i < n; i++) {
@@ -768,3 +776,123 @@ BZIP3_API void bz3_decode_blocks(struct bz3_state * states[], u8 * buffers[], s3
 }
 
 #endif
+
+/* High level API implementations. */
+
+BZIP3_API int bz3_compress(u32 block_size, const u8 * const in, u8 * out, size_t in_size, size_t * out_size) {
+    if (block_size > in_size) block_size = in_size + 16;
+    block_size = block_size <= KiB(65) ? KiB(65) : block_size;
+
+    struct bz3_state * state = bz3_new(block_size);
+    if (!state) return BZ3_ERR_INIT;
+
+    u8 * compression_buf = malloc(block_size);
+    if (!compression_buf) {
+        bz3_free(state);
+        return BZ3_ERR_INIT;
+    }
+
+    size_t buf_max = *out_size;
+    *out_size = 0;
+
+    u32 n_blocks = in_size / block_size;
+    if (in_size % block_size) n_blocks++;
+
+    if (buf_max < 13 || buf_max < in_size + in_size / 50 + 32) {
+        bz3_free(state);
+        free(compression_buf);
+        return BZ3_ERR_DATA_TOO_BIG;
+    }
+
+    out[0] = 'B';
+    out[1] = 'Z';
+    out[2] = '3';
+    out[3] = 'v';
+    out[4] = '1';
+    write_neutral_s32(out + 5, block_size);
+    write_neutral_s32(out + 9, n_blocks);
+    *out_size += 13;
+
+    // Compress and write the blocks.
+    for (u32 i = 0; i < n_blocks; i++) {
+        s32 size = block_size;
+        if (i == n_blocks - 1) size = in_size % block_size;
+        memcpy(compression_buf, in, size);
+        s32 out_size_block = bz3_encode_block(state, compression_buf, size);
+        if (bz3_last_error(state) != BZ3_OK) {
+            s8 last_error = state->last_error;
+            bz3_free(state);
+            free(compression_buf);
+            return last_error;
+        }
+        memcpy(out + *out_size + 8, compression_buf, out_size_block);
+        write_neutral_s32(out + *out_size, out_size_block);
+        write_neutral_s32(out + *out_size + 4, size);
+        *out_size += out_size_block + 8;
+    }
+
+    bz3_free(state);
+    free(compression_buf);
+    return BZ3_OK;
+}
+
+BZIP3_API int bz3_decompress(const uint8_t * in, uint8_t * out, size_t in_size, size_t * out_size) {
+    if (in_size < 13) return BZ3_ERR_MALFORMED_HEADER;
+    if (in[0] != 'B' || in[1] != 'Z' || in[2] != '3' || in[3] != 'v' || in[4] != '1') {
+        return BZ3_ERR_MALFORMED_HEADER;
+    }
+    u32 block_size = read_neutral_s32(in + 5);
+    u32 n_blocks = read_neutral_s32(in + 9);
+    in_size -= 13;
+    in += 13;
+
+    struct bz3_state * state = bz3_new(block_size);
+    if (!state) return BZ3_ERR_INIT;
+
+    u8 * compression_buf = malloc(block_size);
+    if (!compression_buf) {
+        bz3_free(state);
+        return BZ3_ERR_INIT;
+    }
+
+    size_t buf_max = *out_size;
+    *out_size = 0;
+
+    for (u32 i = 0; i < n_blocks; i++) {
+        if (in_size < 8) {
+        malformed_header:
+            bz3_free(state);
+            free(compression_buf);
+            return BZ3_ERR_MALFORMED_HEADER;
+        }
+        s32 size = read_neutral_s32(in);
+        if (size < 0 || size > block_size) goto malformed_header;
+        if (in_size < size + 8) {
+            bz3_free(state);
+            free(compression_buf);
+            return BZ3_ERR_TRUNCATED_DATA;
+        }
+        s32 orig_size = read_neutral_s32(in + 4);
+        if (orig_size < 0) goto malformed_header;
+        if (buf_max < *out_size + orig_size) {
+            bz3_free(state);
+            free(compression_buf);
+            return BZ3_ERR_DATA_TOO_BIG;
+        }
+        memcpy(compression_buf, in + 8, size);
+        bz3_decode_block(state, compression_buf, size, orig_size);
+        if (bz3_last_error(state) != BZ3_OK) {
+            s8 last_error = state->last_error;
+            bz3_free(state);
+            free(compression_buf);
+            return last_error;
+        }
+        memcpy(out + *out_size, compression_buf, orig_size);
+        *out_size += orig_size;
+        in += size + 8;
+        in_size -= size + 8;
+    }
+
+    bz3_free(state);
+    return BZ3_OK;
+}
diff --git a/src/main.c b/src/main.c
index 18ff19f..6f6a18f 100644
--- a/src/main.c
+++ b/src/main.c
@@ -94,9 +94,8 @@ static size_t xread(void * data, size_t size, size_t len, FILE * des) {
 static size_t xread_eofcheck(void * data, size_t size, size_t len, FILE * des) {
     size_t written = xread(data, size, len, des);
     /* feof will be true */
-    if (!written)
-        return 0;
-    if (feof (des)) {
+    if (!written) return 0;
+    if (feof(des)) {
         fprintf(stderr, "Error: Corrupt file\n");
         exit(1);
     }
@@ -105,7 +104,7 @@ static size_t xread_eofcheck(void * data, size_t size, size_t len, FILE * des) {
 
 /* Always read len items */
 static void xread_noeof(void * data, size_t size, size_t len, FILE * des) {
-    if (!xread_eofcheck (data, size, len, des)) {
+    if (!xread_eofcheck(data, size, len, des)) {
         fprintf(stderr, "Error: Corrupt file\n");
         exit(1);
     }
@@ -124,10 +123,8 @@ static void close_out_file(FILE * des) {
         while (1) {
             int status = fsync(outfd);
             if (status == -1) {
-                if (errno == EINVAL)
-                    break;
-                if (errno == EINTR)
-                    continue;
+                if (errno == EINVAL) break;
+                if (errno == EINTR) continue;
                 fprintf(stderr, "Error: Failed on fsync: %s\n", strerror(errno));
                 exit(1);
             }
@@ -135,8 +132,7 @@ static void close_out_file(FILE * des) {
         }
 #endif
 
-        if (des != stdout
-            && fclose(des)) {
+        if (des != stdout && fclose(des)) {
             fprintf(stderr, "Error: Failed on fclose: %s\n", strerror(errno));
             exit(1);
         }
@@ -163,8 +159,7 @@ static int process(FILE * input_des, FILE * output_des, int mode, int block_size
         case MODE_TEST: {
             char signature[5];
 
-            if (xread(signature, 5, 1, input_des) != 1
-                || strncmp(signature, "BZ3v1", 5) != 0) {
+            if (xread(signature, 5, 1, input_des) != 1 || strncmp(signature, "BZ3v1", 5) != 0) {
                 fprintf(stderr, "Invalid signature.\n");
                 return 1;
             }
@@ -227,8 +222,7 @@ static int process(FILE * input_des, FILE * output_des, int mode, int block_size
         } else if (mode == MODE_DECODE) {
             s32 new_size, old_size;
             while (!feof(input_des)) {
-                if (!xread_eofcheck(&byteswap_buf, 1, 4, input_des))
-                    continue;
+                if (!xread_eofcheck(&byteswap_buf, 1, 4, input_des)) continue;
 
                 new_size = read_neutral_s32(byteswap_buf);
                 xread_noeof(&byteswap_buf, 1, 4, input_des);
@@ -244,8 +238,7 @@ static int process(FILE * input_des, FILE * output_des, int mode, int block_size
         } else if (mode == MODE_TEST) {
             s32 new_size, old_size;
             while (!feof(input_des)) {
-                if (!xread_eofcheck(&byteswap_buf, 1, 4, input_des))
-                    continue;
+                if (!xread_eofcheck(&byteswap_buf, 1, 4, input_des)) continue;
                 new_size = read_neutral_s32(byteswap_buf);
                 xread_noeof(&byteswap_buf, 1, 4, input_des);
                 old_size = read_neutral_s32(byteswap_buf);
@@ -315,8 +308,7 @@ static int process(FILE * input_des, FILE * output_des, int mode, int block_size
             while (!feof(input_des)) {
                 s32 i = 0;
                 for (; i < workers; i++) {
-                    if (!xread_eofcheck(&byteswap_buf, 1, 4, input_des))
-                        break;
+                    if (!xread_eofcheck(&byteswap_buf, 1, 4, input_des)) break;
                     sizes[i] = read_neutral_s32(byteswap_buf);
                     xread_noeof(&byteswap_buf, 1, 4, input_des);
                     old_sizes[i] = read_neutral_s32(byteswap_buf);
@@ -338,8 +330,7 @@ static int process(FILE * input_des, FILE * output_des, int mode, int block_size
             while (!feof(input_des)) {
                 s32 i = 0;
                 for (; i < workers; i++) {
-                    if (!xread_eofcheck(&byteswap_buf, 1, 4, input_des))
-                        break;
+                    if (!xread_eofcheck(&byteswap_buf, 1, 4, input_des)) break;
                     sizes[i] = read_neutral_s32(byteswap_buf);
                     xread_noeof(&byteswap_buf, 1, 4, input_des);
                     old_sizes[i] = read_neutral_s32(byteswap_buf);
tab: 248 wrap: offon