#include "config.h"
#include "cunit/cyrunit.h"
#include "prot.h"
#include "xmalloc.h"
#include "util.h"
#include "retry.h"
#include "map.h"
#include "util.h"
#include <sys/mman.h>

static void test_simple(void)
{
#define WORD0   "lorem"
#define WORD1   "ipsum"
    struct buf b = BUF_INITIALIZER;
    const char *s;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    buf_appendcstr(&b, WORD0);
    buf_putc(&b, ' ');
    buf_appendcstr(&b, WORD1);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1+sizeof(WORD1)-1+1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);

    s = buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1+sizeof(WORD1)-1+1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_PTR_NOT_NULL(s);
    CU_ASSERT_STRING_EQUAL(s, WORD0" "WORD1);

    buf_free(&b);

#undef WORD0
#undef WORD1
}

static void test_map(void)
{
#define WORD0   "lorem"
#define WORD1   "ipsum"
    struct buf b = BUF_INITIALIZER;
    const char *map;
    size_t len;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    buf_appendmap(&b, WORD0, sizeof(WORD0)-1);
    buf_putc(&b, ' ');
    buf_appendmap(&b, WORD1, 2);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)+2);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);

    map = (void *)0xdeadbeef;
    len = 42;
    buf_getmap(&b, &map, &len);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)+2);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_EQUAL(len, b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_PTR_NOT_NULL(map);
    CU_ASSERT(!memcmp(map, WORD0" ip", sizeof(WORD0)+2));

    buf_free(&b);

#undef WORD0
#undef WORD1
}

static void test_initm(void)
{
    struct buf b = BUF_INITIALIZER;
    /* data thanks to hipsteripsum.me */
    static const char WORD0[] = "terry richardson fanny pack";
    char *s = xstrdup(WORD0);

    buf_initm(&b, s, sizeof(WORD0)-1);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT(!memcmp(b.s, WORD0, sizeof(WORD0)-1));

    /* we don't free(s) - the buf_free() should do that */
    buf_free(&b);
}

static void test_long(void)
{
    struct buf b = BUF_INITIALIZER;
    int i;
    char *exp;
#define SZ  6
#define N 10000
    char tt[SZ+1];

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    for (i = 0 ; i < N ; i++) {
        snprintf(tt, sizeof(tt), "%c%05d", 'A'+(i%26), i);
        buf_appendcstr(&b, tt);
    }
    buf_cstring(&b);

    CU_ASSERT_EQUAL(b.len, SZ*N);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);

    exp = xmalloc(SZ*N+1);
    for (i = 0 ; i < N ; i++)
        snprintf(exp+SZ*i, SZ+1, "%c%05d", 'A'+(i%26), i);
    CU_ASSERT(!strcmp(b.s, exp));
    free(exp);

    buf_free(&b);
#undef N
#undef SZ
}

static void test_setcstr(void)
{
#define WORD0   "lorem"
    struct buf b = BUF_INITIALIZER;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    buf_setcstr(&b, WORD0);
    buf_cstring(&b);

    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0);

    buf_free(&b);
#undef WORD0
}

static void test_setmap(void)
{
#define WORD1   "ipsum"
    struct buf b = BUF_INITIALIZER;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    buf_setmap(&b, WORD1, sizeof(WORD1)-1);
    buf_cstring(&b);

    CU_ASSERT_EQUAL(b.len, sizeof(WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD1);

    buf_free(&b);
#undef WORD1
}

static void test_append(void)
{
#define WORD0   "lorem"
#define WORD1   "ipsum"
    struct buf b = BUF_INITIALIZER;
    struct buf b2 = BUF_INITIALIZER;
    const char *s;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    CU_ASSERT_EQUAL(b2.len, 0);
    CU_ASSERT(b2.alloc >= b2.len);
    CU_ASSERT_EQUAL(buf_len(&b2), b2.len);
    CU_ASSERT_PTR_NULL(b2.s);

    buf_setmap(&b, WORD0, sizeof(WORD0)-1);
    buf_setmap(&b2, WORD1, sizeof(WORD1)-1);
    buf_append(&b, &b2);

    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1+sizeof(WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    s = buf_cstring(&b);
    CU_ASSERT_STRING_EQUAL(s, WORD0""WORD1);

    CU_ASSERT_EQUAL(b2.len, sizeof(WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b2), b2.len);
    CU_ASSERT(b2.alloc >= b2.len);
    CU_ASSERT_PTR_NOT_NULL(b2.s);
    s = buf_cstring(&b2);
    CU_ASSERT_STRING_EQUAL(s, WORD1);

    buf_free(&b);
    buf_free(&b2);
#undef WORD0
#undef WORD1
}

static void test_appendbit32(void)
{
#define HEX0    0xcafebabe
#define HEX1    0xdeadbeef
    static const unsigned char HEX[8] = {
        0xca, 0xfe, 0xba, 0xbe,
        0xde, 0xad, 0xbe, 0xef
    };
    struct buf b = BUF_INITIALIZER;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    buf_appendbit32(&b, HEX0);
    buf_appendbit32(&b, HEX1);

    CU_ASSERT_EQUAL(b.len, 8);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT(!memcmp(b.s, HEX, sizeof(HEX)));

    buf_free(&b);
#undef HEX0
#undef HEX1
}

static void test_reset(void)
{
#define WORD0   "lorem"
#define WORD1   "ipsum"
    struct buf b = BUF_INITIALIZER;
    const char *s;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    buf_appendcstr(&b, WORD0);
    buf_reset(&b);
    buf_appendcstr(&b, WORD1);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);

    s = buf_cstring(&b);
    CU_ASSERT_PTR_NOT_NULL(s);
    CU_ASSERT_STRING_EQUAL(s, WORD1);

    buf_free(&b);

#undef WORD0
#undef WORD1
}

static void test_copy(void)
{
#define WORD0   "lorem"
#define WORD1   "ipsum"
    struct buf b = BUF_INITIALIZER;
    struct buf b2 = BUF_INITIALIZER;
    const char *s;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    CU_ASSERT_EQUAL(b2.len, 0);
    CU_ASSERT(b2.alloc >= b2.len);
    CU_ASSERT_EQUAL(buf_len(&b2), b2.len);
    CU_ASSERT_PTR_NULL(b2.s);

    buf_setmap(&b, WORD0, sizeof(WORD0)-1);
    buf_setmap(&b2, WORD1, sizeof(WORD1)-1);
    buf_copy(&b, &b2);

    CU_ASSERT_EQUAL(b.len, sizeof(WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    s = buf_cstring(&b);
    CU_ASSERT_STRING_EQUAL(s, WORD1);

    CU_ASSERT_EQUAL(b2.len, sizeof(WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b2), b2.len);
    CU_ASSERT(b2.alloc >= b2.len);
    CU_ASSERT_PTR_NOT_NULL(b2.s);
    s = buf_cstring(&b2);
    CU_ASSERT_STRING_EQUAL(s, WORD1);

    buf_free(&b);
    buf_free(&b2);
#undef WORD0
#undef WORD1
}

static void test_move(void)
{
#define WORD0   "lorem"
#define WORD1   "ipsummma"
    struct buf b = BUF_INITIALIZER;
    struct buf b2 = BUF_INITIALIZER;
    const char *s;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    CU_ASSERT_EQUAL(b2.len, 0);
    CU_ASSERT(b2.alloc >= b2.len);
    CU_ASSERT_EQUAL(buf_len(&b2), b2.len);
    CU_ASSERT_PTR_NULL(b2.s);

    buf_setmap(&b, WORD0, sizeof(WORD0)-1);
    buf_setmap(&b2, WORD1, sizeof(WORD1)-1);
    buf_move(&b, &b2);

    CU_ASSERT_EQUAL(b.len, sizeof(WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    s = buf_cstring(&b);
    CU_ASSERT_STRING_EQUAL(s, WORD1);

    CU_ASSERT_EQUAL(b2.len, 0);
    CU_ASSERT_EQUAL(buf_len(&b2), b2.len);
    CU_ASSERT_EQUAL(b2.alloc, 0);
    CU_ASSERT_PTR_NULL(b2.s);

    buf_free(&b);
    buf_free(&b2);
#undef WORD0
#undef WORD1
}

static void test_printf(void)
{
#define WORD0   "lorem"
#define WORD1   "ipsum"
#define DEC0    31337
#define HEX0    0xcafebabe
    struct buf b = BUF_INITIALIZER;
    const char *s;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    buf_printf(&b, WORD0" %s 0x%x %d", WORD1, HEX0, DEC0);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)+sizeof(WORD1)+16);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);

    s = buf_cstring(&b);
    CU_ASSERT_PTR_NOT_NULL(s);
    CU_ASSERT_STRING_EQUAL(s, WORD0" "WORD1" 0xcafebabe 31337");

    buf_free(&b);
#undef WORD0
#undef WORD1
#undef DEC0
#undef HEX0
}

static void test_long_printf(void)
{
    struct buf b = BUF_INITIALIZER;
    int i;
    const char *s;
    char *exp;
#define SZ  6
#define N 10000

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    exp = xmalloc(SZ*N+1);
    for (i = 0 ; i < N ; i++)
        snprintf(exp+SZ*i, SZ+1, "%c%05d", 'A'+(i%26), i);

    buf_printf(&b, "x%sy", exp);
    s = buf_cstring(&b);

    CU_ASSERT_EQUAL(b.len, SZ*N+2);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);

    CU_ASSERT_PTR_NOT_NULL(s);
    CU_ASSERT_EQUAL(s[0], 'x');
    CU_ASSERT(!memcmp(s+1, exp, SZ*N));
    CU_ASSERT_EQUAL(s[SZ*N+1], 'y');

    buf_free(&b);
    free(exp);
#undef N
#undef SZ
}

static void test_replace_all(void)
{
#define WORD0   "lorem"
#define WORD0REP "LAUREN BACALL"
#define WORD0REP2 "L0R3M"
#define WORD0REP3 "LRM"
#define WORD0REP4 "XloremY"
#define WORD1   "ipsum"
#define WORD1   "ipsum"
#define WORD2   "dolor"
#define WORD3   "sit"
#define WORD4   "amet"
    struct buf b = BUF_INITIALIZER;
    unsigned int n;
    char *buf_s;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    /* simple test: a single replacement */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0" "WORD1);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1);

    n = buf_replace_all(&b, WORD0, WORD0REP);
    CU_ASSERT_EQUAL(n, 1);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0REP" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0REP" "WORD1);

    /* simple test: failure to match */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0" "WORD1);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1);

    n = buf_replace_all(&b, WORD4, WORD0REP);
    CU_ASSERT_EQUAL(n, 0);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1);

    /* a replacement which doesn't change the size */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0" "WORD1);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1);

    buf_s = b.s;
    n = buf_replace_all(&b, WORD0, WORD0REP2);
    CU_ASSERT_EQUAL(n, 1);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0REP2" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_PTR_EQUAL(b.s, buf_s);  /* no size change => no realloc */
    CU_ASSERT_STRING_EQUAL(b.s, WORD0REP2" "WORD1);

    /* a replacement which shrinks the size */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0" "WORD1);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1);

    /* a replacement with an empty string */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0" "WORD1);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1);

    buf_s = b.s;
    n = buf_replace_all(&b, WORD0, "");
    CU_ASSERT_EQUAL(n, 1);
    CU_ASSERT_EQUAL(b.len, sizeof(" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_PTR_EQUAL(b.s, buf_s);  /* no size change => no realloc */
    CU_ASSERT_STRING_EQUAL(b.s, " "WORD1);

    /* a replacement with a NULL string */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0" "WORD1);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1);

    buf_s = b.s;
    n = buf_replace_all(&b, WORD0, NULL);
    CU_ASSERT_EQUAL(n, 1);
    CU_ASSERT_EQUAL(b.len, sizeof(" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_PTR_EQUAL(b.s, buf_s);  /* no size change => no realloc */
    CU_ASSERT_STRING_EQUAL(b.s, " "WORD1);

    /* multiple replacements, including abutted ones */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4);

    n = buf_replace_all(&b, WORD0, WORD0REP2);
    CU_ASSERT_EQUAL(n, 4);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0REP2" "WORD1" "WORD2" "WORD0REP2" "WORD3" "WORD0REP2""WORD0REP2" "WORD4)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0REP2" "WORD1" "WORD2" "WORD0REP2" "WORD3" "WORD0REP2""WORD0REP2" "WORD4);

    /* multiple replacements with a replacement which contains the match */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4);

    n = buf_replace_all(&b, WORD0, WORD0REP4);
    CU_ASSERT_EQUAL(n, 4);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0REP4" "WORD1" "WORD2" "WORD0REP4" "WORD3" "WORD0REP4""WORD0REP4" "WORD4)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0REP4" "WORD1" "WORD2" "WORD0REP4" "WORD3" "WORD0REP4""WORD0REP4" "WORD4);

    buf_free(&b);

#undef WORD0
#undef WORD0REP
#undef WORD0REP2
#undef WORD0REP3
#undef WORD0REP4
#undef WORD1
#undef WORD2
#undef WORD3
#undef WORD4
}

/* we use buf_init_ro() instead of buf_appendcstr() to test
 * the case where the input buffer is not a C string before
 * calling buf_replace_*() */
#define TESTCASE_START(in, reg, rep, expn, exp) \
    { \
        static const char _in[] = in; \
        static const char _reg[] = reg; \
        static const char *_rep = rep; \
        static const int _expn = expn; \
        static const char _exp[] = exp; \
        struct buf b = BUF_INITIALIZER; \
        regex_t re; \
        unsigned int n; \
        int r; \
 \
        buf_init_ro(&b, _in, sizeof(_in)-1); \
        CU_ASSERT_EQUAL(b.len, sizeof(_in)-1); \
        CU_ASSERT_EQUAL(buf_len(&b), b.len); \
        CU_ASSERT_PTR_NOT_NULL(b.s); \
        CU_ASSERT_STRING_EQUAL(b.s, _in); \
 \
        r = regcomp(&re, _reg, REG_EXTENDED); \
        CU_ASSERT_EQUAL(r, 0);
#define TESTCASE_MIDDLE \
        n = buf_replace_one_re(&b, &re, _rep);
#define TESTCASE_END \
        CU_ASSERT_EQUAL(n, _expn); \
        CU_ASSERT_EQUAL(b.len, sizeof(_exp)-1); \
        CU_ASSERT_EQUAL(buf_len(&b), b.len); \
        CU_ASSERT(b.alloc >= b.len); \
        CU_ASSERT_PTR_NOT_NULL(b.s); \
        CU_ASSERT_STRING_EQUAL(b.s, _exp); \
        regfree(&re); \
        buf_free(&b); \
    }

#define TESTCASE(in, reg, rep, expn, exp) \
        TESTCASE_START(in, reg, rep, expn, exp) \
        TESTCASE_MIDDLE \
        TESTCASE_END

static void test_replace_one_re(void)
{
    /* data thanks to hipsteripsum.me */
#define WORD0       "bespoke"
#define WORD0_RE    "be[a-df-z]+e"
#define WORD0REP    "butcher"
#define WORD0REP2   "BeSp0K3"
#define WORD0REP3   "BpK"
#define WORD0REP4   "XbespokeY"
#define WORD1       "tattooed"
#define WORD2       "fap"
#define WORD3       "keffiyeh"
#define WORD4       "biodiesel"
#define WORD4_RE    "[a-z]iod[a-z]+"
    const char *buf_s;

    /* simple test: a single replacement */
    TESTCASE(WORD0" "WORD1,
             WORD0_RE, WORD0REP,
             1, WORD0REP" "WORD1);

    /* simple test: failure to match */
    TESTCASE_START(WORD0" "WORD1,
                   WORD4_RE, WORD0REP,
                   0, WORD0" "WORD1);
    TESTCASE_MIDDLE
    buf_cstring(&b);
    TESTCASE_END

    /* a replacement which doesn't change the size */
    TESTCASE_START(WORD0" "WORD1,
                   WORD0_RE, WORD0REP2,
                   1, WORD0REP2" "WORD1);
    buf_s = buf_cstring(&b);
    TESTCASE_MIDDLE
    CU_ASSERT_PTR_EQUAL(buf_cstring(&b), buf_s);  /* no size change => no realloc */
    TESTCASE_END

    /* a replacement which shrinks the size */
    TESTCASE_START(WORD0" "WORD1,
                   WORD0_RE, WORD0REP3,
                   1, WORD0REP3" "WORD1);
    buf_s = buf_cstring(&b);
    TESTCASE_MIDDLE
    CU_ASSERT_PTR_EQUAL(buf_cstring(&b), buf_s);  /* no size change => no realloc */
    TESTCASE_END

    /* a replacement with an empty string */
    TESTCASE(WORD0" "WORD1,
             WORD0_RE, "",
             1, " "WORD1);

    /* a replacement with a NULL string */
    TESTCASE(WORD0" "WORD1,
             WORD0_RE, NULL,
             1, " "WORD1);

    /* multiple matches, including abutted ones */
    TESTCASE(WORD0" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4,
             WORD0_RE, WORD0REP2,
             1, WORD0REP2" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4);

    /* multiple matches with a replacement which contains the match */
    TESTCASE(WORD0" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4,
             WORD0_RE, WORD0REP4,
             1, WORD0REP4" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4);

#undef WORD0
#undef WORD0REP
#undef WORD0REP2
#undef WORD0REP3
#undef WORD0REP4
#undef WORD1
#undef WORD2
#undef WORD3
#undef WORD4
}

#undef TESTCASE_MIDDLE
#define TESTCASE_MIDDLE \
        n = buf_replace_all_re(&b, &re, _rep);

static void test_replace_all_re(void)
{
    /* data thanks to hipsteripsum.me */
#define WORD0       "bespoke"
#define WORD0_RE    "be[a-df-z]+e"
#define WORD0REP    "butcher"
#define WORD0REP2   "BeSp0K3"
#define WORD0REP3   "BpK"
#define WORD0REP4   "XbespokeY"
#define WORD1       "tattooed"
#define WORD2       "fap"
#define WORD3       "keffiyeh"
#define WORD4       "biodiesel"
#define WORD4_RE    "[a-z]iod[a-z]+"
    const char *buf_s;

    /* simple test: a single replacement */
    TESTCASE(WORD0" "WORD1,
             WORD0_RE, WORD0REP,
             1, WORD0REP" "WORD1);

    /* simple test: failure to match */
    TESTCASE_START(WORD0" "WORD1,
                   WORD4_RE, WORD0REP,
                   0, WORD0" "WORD1);
    TESTCASE_MIDDLE
    buf_cstring(&b);
    TESTCASE_END

    /* a replacement which doesn't change the size */
    TESTCASE_START(WORD0" "WORD1,
                   WORD0_RE, WORD0REP2,
                   1, WORD0REP2" "WORD1);
    buf_s = buf_cstring(&b);
    TESTCASE_MIDDLE
    CU_ASSERT_PTR_EQUAL(buf_cstring(&b), buf_s);  /* no size change => no realloc */
    TESTCASE_END

    /* a replacement which shrinks the size */
    TESTCASE_START(WORD0" "WORD1,
                   WORD0_RE, WORD0REP3,
                   1, WORD0REP3" "WORD1);
    buf_s = buf_cstring(&b);
    TESTCASE_MIDDLE
    CU_ASSERT_PTR_EQUAL(buf_cstring(&b), buf_s);  /* no size change => no realloc */
    TESTCASE_END

    /* a replacement with an empty string */
    TESTCASE(WORD0" "WORD1,
             WORD0_RE, "",
             1, " "WORD1);

    /* a replacement with a NULL string */
    TESTCASE(WORD0" "WORD1,
             WORD0_RE, NULL,
             1, " "WORD1);

    /* multiple matches, including abutted ones */
    TESTCASE(WORD0" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4,
             WORD0_RE, WORD0REP2,
             4, WORD0REP2" "WORD1" "WORD2" "WORD0REP2" "WORD3" "WORD0REP2""WORD0REP2" "WORD4);

    /* multiple matches with a replacement which contains the match */
    TESTCASE(WORD0" "WORD1" "WORD2" "WORD0" "WORD3" "WORD0""WORD0" "WORD4,
             WORD0_RE, WORD0REP4,
             4, WORD0REP4" "WORD1" "WORD2" "WORD0REP4" "WORD3" "WORD0REP4""WORD0REP4" "WORD4);

#undef WORD0
#undef WORD0REP
#undef WORD0REP2
#undef WORD0REP3
#undef WORD0REP4
#undef WORD1
#undef WORD2
#undef WORD3
#undef WORD4
}

#undef TESTCASE_START
#undef TESTCASE_MIDDLE
#undef TESTCASE_END
#undef TESTCASE



#define TESTCASE(in, off, rep, exp) \
    { \
        static const char _in[] = in; \
        int _off = off; \
        static const char _rep[] = rep; \
        static const char _exp[] = exp; \
        struct buf b = BUF_INITIALIZER; \
 \
        buf_appendcstr(&b, _in); \
        buf_cstring(&b); \
        CU_ASSERT_EQUAL(b.len, sizeof(_in)-1); \
        CU_ASSERT_PTR_NOT_NULL(b.s); \
        CU_ASSERT_STRING_EQUAL(b.s, _in); \
 \
        buf_insertcstr(&b, _off, _rep); \
        CU_ASSERT_EQUAL(b.len, sizeof(_exp)-1); \
        CU_ASSERT(b.alloc >= b.len); \
        buf_cstring(&b); \
        CU_ASSERT_PTR_NOT_NULL(b.s); \
        CU_ASSERT_STRING_EQUAL(b.s, _exp); \
        buf_free(&b); \
    }

static void test_insertcstr(void)
{
    /* data thanks to hipsteripsum.me */
#define WORD0   "gastropub"
#define WORD1   "vegan brunch"
#define WORD2   "portland"

    TESTCASE(WORD0" "WORD2,
             0, WORD1"\t",
             WORD1"\t"WORD0" "WORD2);

    TESTCASE(WORD0" "WORD2,
             sizeof(WORD0)-1, "\t"WORD1,
             WORD0"\t"WORD1" "WORD2);

    TESTCASE(WORD0" "WORD2,
             sizeof(WORD0" "WORD2)-1, "\t"WORD1,
             WORD0" "WORD2"\t"WORD1);

    TESTCASE("",
             0, WORD1,
             WORD1);

    TESTCASE("",
             1, WORD1,
             "");

#undef WORD0
#undef WORD1
#undef WORD2
}
#undef TESTCASE


#define TESTCASE(in, off, rep, exp) \
    { \
        static const char _in[] = in; \
        int _off = off; \
        static const char _rep[] = rep; \
        static const char _exp[] = exp; \
        struct buf b = BUF_INITIALIZER; \
 \
        buf_appendcstr(&b, _in); \
        buf_cstring(&b); \
        CU_ASSERT_EQUAL(b.len, sizeof(_in)-1); \
        CU_ASSERT_PTR_NOT_NULL(b.s); \
        CU_ASSERT_STRING_EQUAL(b.s, _in); \
 \
        buf_insertmap(&b, _off, _rep, sizeof(_rep)-1); \
        CU_ASSERT_EQUAL(b.len, sizeof(_exp)-1); \
        CU_ASSERT(b.alloc >= b.len); \
        buf_cstring(&b); \
        CU_ASSERT_PTR_NOT_NULL(b.s); \
        CU_ASSERT_STRING_EQUAL(b.s, _exp); \
        buf_free(&b); \
    }

static void test_insertmap(void)
{
    /* data thanks to hipsteripsum.me */
#define WORD0   "gastropub"
#define WORD1   "vegan brunch"
#define WORD2   "portland"

    TESTCASE(WORD0" "WORD2,
             0, WORD1"\t",
             WORD1"\t"WORD0" "WORD2);

    TESTCASE(WORD0" "WORD2,
             sizeof(WORD0)-1, "\t"WORD1,
             WORD0"\t"WORD1" "WORD2);

    TESTCASE(WORD0" "WORD2,
             sizeof(WORD0" "WORD2)-1, "\t"WORD1,
             WORD0" "WORD2"\t"WORD1);

    TESTCASE("",
             0, WORD1,
             WORD1);

    TESTCASE("",
             1, WORD1,
             "");

#undef WORD0
#undef WORD1
#undef WORD2
}
#undef TESTCASE


#define TESTCASE(in, off, rep, exp) \
    { \
        static const char _in[] = in; \
        int _off = off; \
        static const char _rep[] = rep; \
        struct buf _repb = BUF_INITIALIZER; \
        static const char _exp[] = exp; \
        struct buf b = BUF_INITIALIZER; \
\
        buf_appendmap(&_repb, _rep, sizeof(_rep)-1); \
 \
        buf_appendcstr(&b, _in); \
        buf_cstring(&b); \
        CU_ASSERT_EQUAL(b.len, sizeof(_in)-1); \
        CU_ASSERT_PTR_NOT_NULL(b.s); \
        CU_ASSERT_STRING_EQUAL(b.s, _in); \
 \
        buf_insert(&b, _off, &_repb); \
        CU_ASSERT_EQUAL(b.len, sizeof(_exp)-1); \
        CU_ASSERT(b.alloc >= b.len); \
        buf_cstring(&b); \
        CU_ASSERT_PTR_NOT_NULL(b.s); \
        CU_ASSERT_STRING_EQUAL(b.s, _exp); \
        buf_free(&b); \
        buf_free(&_repb); \
    }

static void test_insert(void)
{
    /* data thanks to hipsteripsum.me */
#define WORD0   "gastropub"
#define WORD1   "brunch"
#define WORD2   "portland"

    TESTCASE(WORD0" "WORD2,
             0, WORD1"\t",
             WORD1"\t"WORD0" "WORD2);

    TESTCASE(WORD0" "WORD2,
             sizeof(WORD0)-1, "\t"WORD1,
             WORD0"\t"WORD1" "WORD2);

    TESTCASE(WORD0" "WORD2,
             sizeof(WORD0" "WORD2)-1, "\t"WORD1,
             WORD0" "WORD2"\t"WORD1);

    TESTCASE("",
             0, WORD1,
             WORD1);

    TESTCASE("",
             1, WORD1,
             "");

#undef WORD0
#undef WORD1
#undef WORD2
}
#undef TESTCASE


#define TESTCASE(in, off, xlen, exp) \
    { \
        static const char _in[] = in; \
        int _off = off; \
        int _len = xlen; \
        static const char _exp[] = exp; \
        struct buf b = BUF_INITIALIZER; \
 \
        buf_appendcstr(&b, _in); \
        buf_cstring(&b); \
        CU_ASSERT_EQUAL(b.len, sizeof(_in)-1); \
        CU_ASSERT_PTR_NOT_NULL(b.s); \
        CU_ASSERT_STRING_EQUAL(b.s, _in); \
 \
        buf_remove(&b, _off, _len); \
        CU_ASSERT_EQUAL(b.len, sizeof(_exp)-1); \
        CU_ASSERT(b.alloc >= b.len); \
        buf_cstring(&b); \
        CU_ASSERT_PTR_NOT_NULL(b.s); \
        CU_ASSERT_STRING_EQUAL(b.s, _exp); \
        buf_free(&b); \
    }

static void test_remove(void)
{
    /* data thanks to hipsteripsum.me */
#define WORD0   "fingerstache"
#define WORD1   "scenester"
#define WORD2   "ennui"

    TESTCASE(WORD0" "WORD1"\t"WORD2,
             0, sizeof(WORD0),
             WORD1"\t"WORD2);

    TESTCASE(WORD0" "WORD1"\t"WORD2,
             0, 0,
             WORD0" "WORD1"\t"WORD2);

    TESTCASE(WORD0" "WORD1"\t"WORD2,
             sizeof(WORD0), sizeof(WORD1),
             WORD0" "WORD2);

    TESTCASE(WORD0" "WORD1"\t"WORD2,
             sizeof(WORD0)+sizeof(WORD1), sizeof(WORD2)-1,
             WORD0" "WORD1"\t");

    TESTCASE(WORD0" "WORD1"\t"WORD2,
             sizeof(WORD0)+sizeof(WORD1), sizeof(WORD2)-1+20,
             WORD0" "WORD1"\t");

    TESTCASE(WORD0" "WORD1"\t"WORD2,
             0, sizeof(WORD0" "WORD1"\t"WORD2)-1,
             "");

    TESTCASE("",
             0, 1,
             "");

#undef WORD0
#undef WORD1
#undef WORD2
}
#undef TESTCASE


static void test_truncate(void)
{
#define WORD0   "lorem"
#define WORD1   "ipsum"
    struct buf b = BUF_INITIALIZER;
    unsigned int i;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    /* buf_truncate() which shortens the buffer */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0" "WORD1);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1);

    buf_truncate(&b, sizeof(WORD0)-1);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0);

    /* buf_truncate() which extends and zero-fills the buffer */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0" "WORD1);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1)-1);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1);

    buf_truncate(&b, sizeof(WORD0" "WORD1)-1+2048);
    buf_cstring(&b);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0" "WORD1)-1+2048);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_STRING_EQUAL(b.s, WORD0" "WORD1);
    for (i=sizeof(WORD0" "WORD1)-1; i<sizeof(WORD0" "WORD1)-1+2048 ; i++)
        if (b.s[i] != 0)
            break;
    CU_ASSERT_EQUAL(i, sizeof(WORD0" "WORD1)-1+2048);

    buf_free(&b);

#undef WORD0
#undef WORD1
}

static void test_cmp(void)
{
/* words chosen to be in alphabetical order */
#define WORD0           "alpha"
#define WORD0SUB        "alp"
#define WORD1           "omega"
    struct buf a = BUF_INITIALIZER;
    struct buf b = BUF_INITIALIZER;
    int d;

    /* compare two empty (null) bufs */
    CU_ASSERT_PTR_NULL(a.s);
    CU_ASSERT_EQUAL(a.len, 0);
    CU_ASSERT_PTR_NULL(b.s);
    CU_ASSERT_EQUAL(b.len, 0);
    d = buf_cmp(&a, &b);
    CU_ASSERT_EQUAL(d, 0);
    d = buf_cmp(&b, &a);
    CU_ASSERT_EQUAL(d, 0);

    /* compare empty (null) vs empty (zero-length) */
    buf_appendcstr(&b, "foo");
    buf_truncate(&b, 0);
    CU_ASSERT_PTR_NULL(a.s);
    CU_ASSERT_EQUAL(a.len, 0);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_EQUAL(b.len, 0);
    d = buf_cmp(&a, &b);
    CU_ASSERT_EQUAL(d, 0);
    d = buf_cmp(&b, &a);
    CU_ASSERT_EQUAL(d, 0);

    /* compare identical strings */
    buf_reset(&a);
    buf_appendcstr(&a, WORD0);
    buf_reset(&b);
    buf_appendcstr(&b, WORD0);
    CU_ASSERT_PTR_NOT_NULL(a.s);
    CU_ASSERT_EQUAL(a.len, sizeof(WORD0)-1);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1);
    d = buf_cmp(&a, &b);
    CU_ASSERT_EQUAL(d, 0);
    d = buf_cmp(&b, &a);
    CU_ASSERT_EQUAL(d, 0);

    /* compare different strings */
    buf_reset(&a);
    buf_appendcstr(&a, WORD0);
    buf_reset(&b);
    buf_appendcstr(&b, WORD1);
    CU_ASSERT_PTR_NOT_NULL(a.s);
    CU_ASSERT_EQUAL(a.len, sizeof(WORD0)-1);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD1)-1);
    d = buf_cmp(&a, &b);
    CU_ASSERT(d < 0);
    d = buf_cmp(&b, &a);
    CU_ASSERT(d > 0);

    /* compare different strings where one is
     * an initial subset of the other */
    buf_reset(&a);
    buf_appendcstr(&a, WORD0SUB);
    buf_reset(&b);
    buf_appendcstr(&b, WORD0);
    CU_ASSERT_PTR_NOT_NULL(a.s);
    CU_ASSERT_EQUAL(a.len, sizeof(WORD0SUB)-1);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1);
    d = buf_cmp(&a, &b);
    CU_ASSERT(d < 0);
    d = buf_cmp(&b, &a);
    CU_ASSERT(d > 0);

    buf_free(&a);
    buf_free(&b);
#undef WORD0
#undef WORD0SUB
#undef WORD1
}

static void test_cow(void)
{
    static const char DATA0[] = "LoRem";
    struct buf b = BUF_INITIALIZER;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    buf_init_ro(&b, DATA0, sizeof(DATA0)-1);
    CU_ASSERT_EQUAL(b.len, sizeof(DATA0)-1);
    CU_ASSERT_EQUAL(b.alloc, 0);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_EQUAL(b.s, (char *)DATA0);

    buf_putc(&b, 'X');
    CU_ASSERT_EQUAL(b.len, sizeof(DATA0)-1+1);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NOT_EQUAL(b.s, (char *)DATA0);

    buf_free(&b);
}

static void test_cow2(void)
{
    static const char DATA0[] = "cardigan umami";
    struct buf b = BUF_INITIALIZER;

    CU_ASSERT_EQUAL(b.len, 0);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NULL(b.s);

    buf_init_ro_cstr(&b, DATA0);
    CU_ASSERT_EQUAL(b.len, sizeof(DATA0)-1);
    CU_ASSERT_EQUAL(b.alloc, 0);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_EQUAL(b.s, (char *)DATA0);

    buf_putc(&b, 'X');
    CU_ASSERT_EQUAL(b.len, sizeof(DATA0)-1+1);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_EQUAL(buf_len(&b), b.len);
    CU_ASSERT_PTR_NOT_EQUAL(b.s, (char *)DATA0);

    buf_free(&b);
}

static void test_findchar(void)
{
    struct buf b = BUF_INITIALIZER;
    int i;

    buf_appendcstr(&b, "ABCDEF");
    /* Note: buf is NOT a C-string so should get a Valgrind
     * error if findchar() runs off the end */

    i = buf_findchar(&b, 0, 'A');
    CU_ASSERT_EQUAL(i, 0);

    i = buf_findchar(&b, 0, 'C');
    CU_ASSERT_EQUAL(i, 2);

    i = buf_findchar(&b, 0, 'F');
    CU_ASSERT_EQUAL(i, 5);

    i = buf_findchar(&b, 0, 'X');
    CU_ASSERT_EQUAL(i, -1);

    i = buf_findchar(&b, 0, -1);
    CU_ASSERT_EQUAL(i, -1);

    i = buf_findchar(&b, 2, 'A');
    CU_ASSERT_EQUAL(i, -1);

    i = buf_findchar(&b, 2, 'C');
    CU_ASSERT_EQUAL(i, 2);

    i = buf_findchar(&b, 2, 'F');
    CU_ASSERT_EQUAL(i, 5);

    i = buf_findchar(&b, 2, 'X');
    CU_ASSERT_EQUAL(i, -1);

    i = buf_findchar(&b, 2, -1);
    CU_ASSERT_EQUAL(i, -1);

    i = buf_findchar(&b, 6, 'F');
    CU_ASSERT_EQUAL(i, -1);

    i = buf_findchar(&b, 6, 'X');
    CU_ASSERT_EQUAL(i, -1);

    buf_free(&b);
}

static void test_cowappendmap(void)
{
    /* data thanks to hipsteripsum.me */
#define WORD0   "beard"
#define WORD1   "narwhal"
    struct buf b = BUF_INITIALIZER;

    buf_cowappendmap(&b, WORD0, sizeof(WORD0)-1);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1);
    CU_ASSERT(b.alloc == 0);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT(!memcmp(b.s, WORD0, sizeof(WORD0)-1));

    buf_cowappendmap(&b, WORD1, sizeof(WORD1)-1);
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1+sizeof(WORD1)-1);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT(!memcmp(b.s, WORD0""WORD1, sizeof(WORD0)-1+sizeof(WORD1)-1));

    buf_free(&b);
#undef WORD0
#undef WORD1
}

static void test_cowappendfree(void)
{
    /* data thanks to hipsteripsum.me */
#define WORD0   "beard"
#define WORD1   "narwhal"
    struct buf b = BUF_INITIALIZER;
    char *s;

    s = xstrdup(WORD0);
    buf_cowappendfree(&b, s, strlen(s));
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT(!memcmp(b.s, WORD0, sizeof(WORD0)-1));

    s = xstrdup(WORD1);
    buf_cowappendfree(&b, s, strlen(s));
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)-1+sizeof(WORD1)-1);
    CU_ASSERT(b.alloc >= b.len);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT(!memcmp(b.s, WORD0""WORD1, sizeof(WORD0)-1+sizeof(WORD1)-1));

    /* we don't free s, buf_free() should do that */
    buf_free(&b);
#undef WORD0
#undef WORD1
}

static void test_bufprint(void)
{
    struct buf b = BUF_INITIALIZER;
    struct protstream *outstream;
    int i;

    outstream = prot_writebuf(&b);

    for (i = 0; i < 5000; i++) {
        prot_putc('.', outstream);
    }

    prot_flush(outstream);
    prot_free(outstream);

    CU_ASSERT_EQUAL(b.len, 5000);

    buf_free(&b);
}

#ifdef HAVE_ZLIB
static void test_compress(void)
{
    struct buf b = BUF_INITIALIZER;
    static const char DATA0[] = "LoRem";
    int r;

    buf_printf(&b, "%s", DATA0);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&b), DATA0);

    r = buf_deflate(&b, Z_DEFAULT_COMPRESSION, DEFLATE_RAW);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_STRING_NOT_EQUAL(buf_cstring(&b), DATA0);

    r = buf_inflate(&b, DEFLATE_RAW);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&b), DATA0);

    buf_free(&b);
}
#endif

static void test_replace_shorten(void)
{
    struct buf b = BUF_INITIALIZER;

    buf_appendcstr(&b, "dreamcatcher skateboard");

    /* Test that buf_replace_all() does not wander off the end of the
     * buffer if a replacement operation at the end of the string
     * shortens the string - this would show up in Valgrind. */

    buf_replace_all(&b, "skateboard", "truck");
    CU_ASSERT_STRING_EQUAL(b.s, "dreamcatcher truck");

    buf_free(&b);
}

static void test_replace_cow_cstr(void)
{
    struct buf b = BUF_INITIALIZER;
    static const char DATA0[] = "food truck vegan";

    buf_init_ro_cstr(&b, DATA0);
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_PTR_EQUAL(b.s, DATA0);
    CU_ASSERT_STRING_EQUAL(b.s, "food truck vegan");

    /* There was a bug where buf_replace_all() was enforcing that
     * the buffer was a C string but not that it was writable, which
     * is often the same but isn't after a buf_init_ro_cstr().  With
     * the bug, buf_replace_all() would SEGV */

    buf_replace_all(&b, "truck", "vinyl");
    CU_ASSERT_PTR_NOT_NULL(b.s);
    CU_ASSERT_PTR_NOT_EQUAL(b.s, DATA0);
    CU_ASSERT_STRING_EQUAL(b.s, "food vinyl vegan");

    buf_free(&b);
}

static void test_findline(void)
{
    struct buf b = BUF_INITIALIZER;
    int r;
#define WORD0 "etsy"
#define WORD1 "quinoa"
#define WORD2 "dreamcatcher"
#define WORD3 "shoreditch"
#define WORD4 "williamsburg"
#define WORD5 "brooklyn"
#define WORDU "banksy"

    buf_appendcstr(&b, WORD0"\n"WORD1"\n"WORD2"\n"WORD3" "WORD4"\n"WORD5"\n");
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)+sizeof(WORD1)+sizeof(WORD2)+
                           sizeof(WORD3)+sizeof(WORD4)+sizeof(WORD5));

    /* a zero length line is always unfound */
    r = buf_findline(&b, "");
    CU_ASSERT_EQUAL(r, -1);
    r = buf_findline(&b, "\n");
    CU_ASSERT_EQUAL(r, -1);

    /* a line which is not present is unfound */
    r = buf_findline(&b, WORDU);
    CU_ASSERT_EQUAL(r, -1);
    r = buf_findline(&b, WORDU"\n");
    CU_ASSERT_EQUAL(r, -1);

    /* only the first line of the needle is searched for */
    r = buf_findline(&b, WORD1"\n"WORD3"\n");
    CU_ASSERT_EQUAL(r, sizeof(WORD0));

    /* the very first line is found */
    r = buf_findline(&b, WORD0);
    CU_ASSERT_EQUAL(r, 0);
    r = buf_findline(&b, WORD0"\n");
    CU_ASSERT_EQUAL(r, 0);

    /* a line which is neither first nor last is found */
    r = buf_findline(&b, WORD1);
    CU_ASSERT_EQUAL(r, sizeof(WORD0));
    r = buf_findline(&b, WORD1"\n");
    CU_ASSERT_EQUAL(r, sizeof(WORD0));

    /* a word which is a subset of a present line, is unfound */
    r = buf_findline(&b, WORD3);
    CU_ASSERT_EQUAL(r, -1);
    r = buf_findline(&b, WORD3"\n");
    CU_ASSERT_EQUAL(r, -1);
    r = buf_findline(&b, WORD4);
    CU_ASSERT_EQUAL(r, -1);
    r = buf_findline(&b, WORD4"\n");
    CU_ASSERT_EQUAL(r, -1);

    /* the very last line is found */
    r = buf_findline(&b, WORD5);
    CU_ASSERT_EQUAL(r, sizeof(WORD0)+sizeof(WORD1)+sizeof(WORD2)+
                       sizeof(WORD3)+sizeof(WORD4));
    r = buf_findline(&b, WORD5"\n");
    CU_ASSERT_EQUAL(r, sizeof(WORD0)+sizeof(WORD1)+sizeof(WORD2)+
                       sizeof(WORD3)+sizeof(WORD4));

    /* a word which is a subset of an earlier line, but
     * matches a later present line, is found */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0"\n"WORD1" "WORD2"\n"WORD1"\n"WORD2"\n"WORD5"\n");
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)+sizeof(WORD1)+sizeof(WORD2)+
                           sizeof(WORD1)+sizeof(WORD2)+sizeof(WORD5));
    r = buf_findline(&b, WORD1);
    CU_ASSERT_EQUAL(r, sizeof(WORD0)+sizeof(WORD1)+sizeof(WORD2));
    r = buf_findline(&b, WORD1"\n");
    CU_ASSERT_EQUAL(r, sizeof(WORD0)+sizeof(WORD1)+sizeof(WORD2));
    r = buf_findline(&b, WORD2);
    CU_ASSERT_EQUAL(r, sizeof(WORD0)+sizeof(WORD1)+sizeof(WORD2)+
                       sizeof(WORD1));
    r = buf_findline(&b, WORD2"\n");
    CU_ASSERT_EQUAL(r, sizeof(WORD0)+sizeof(WORD1)+sizeof(WORD2)+
                       sizeof(WORD1));

    /* a word which is the first subset of the first line, is unfound */
    buf_reset(&b);
    buf_appendcstr(&b, WORD0" "WORD1"\n"WORD2" "WORD3"\n");
    CU_ASSERT_EQUAL(b.len, sizeof(WORD0)+sizeof(WORD1)+sizeof(WORD2)+
                           sizeof(WORD3));
    r = buf_findline(&b, WORD0);
    CU_ASSERT_EQUAL(r, -1);
    r = buf_findline(&b, WORD0"\n");
    CU_ASSERT_EQUAL(r, -1);

    /* a word which is the last subset of the last line, is unfound */
    r = buf_findline(&b, WORD3);
    CU_ASSERT_EQUAL(r, -1);
    r = buf_findline(&b, WORD3"\n");
    CU_ASSERT_EQUAL(r, -1);

    buf_free(&b);
#undef WORD0
#undef WORD1
#undef WORD2
#undef WORD3
#undef WORD4
#undef WORD5
#undef WORDU
}

static void _trimsto(const char *src, const char *dest)
{
    struct buf buf = BUF_INITIALIZER;
    buf_setcstr(&buf, src);
    buf_trim(&buf);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&buf), dest);
    buf_free(&buf);
}

static void test_trim(void)
{
    _trimsto("", "");
    _trimsto("a", "a");
    _trimsto("abcdefg", "abcdefg");
    _trimsto("ab\tcde fg", "ab\tcde fg");
    _trimsto("  ab\tcde fg", "ab\tcde fg");
    _trimsto("  ab\tcde fg  ", "ab\tcde fg");
    _trimsto("ab\tcde fg  ", "ab\tcde fg");
    _trimsto("\tab\tcde fg  ", "ab\tcde fg");
    _trimsto("\t  ", "");
}

static void test_appendoverlap(void)
{
    struct testcase {
        const char *buf;
        const char *str;
        const char *want;
    };

    struct testcase testcases[] = {{
        "abcdef", "efg", "abcdefg"
    }, {
        "abcd", "efg", "abcdefg"
    }, {
        "ab", "cdefg", "abcdefg"
    }, {
        "ab", "abcdefg", "abcdefg"
    }, {
        "a", "abcdefg", "abcdefg"
    }, {
        "", "abcdefg", "abcdefg"
    }, {
        "abcdefg", "", "abcdefg"
    }, {
        "abcdefg", "efg", "abcdefg"
    }};

    struct buf buf = BUF_INITIALIZER;

    size_t i;
    for (i = 0; i < sizeof(testcases) / sizeof(testcases[0]); i++) {
        struct testcase tc = testcases[i];
        buf_setcstr(&buf, tc.buf);
        buf_appendoverlap(&buf, tc.str);
        CU_ASSERT_STRING_EQUAL(tc.want, buf_cstring(&buf));
        buf_reset(&buf);
    }
    buf_free(&buf);
}

static void test_tocrlf(void)
{
    struct testcase {
        char *input;
        char *expect;
    };

    struct testcase testcases[] = {
        { "\rleading bare cr", "\r\nleading bare cr" },
        { "\nleading bare lf", "\r\nleading bare lf" },
        { "\r\nleading crlf", "\r\nleading crlf" },
        { "bare\rcr", "bare\r\ncr" },
        { "bare\nlf", "bare\r\nlf" },
        { "correct\r\ncrlf", "correct\r\ncrlf" },
        { "trailing bare cr\r", "trailing bare cr\r\n" },
        { "trailing bare lf\n", "trailing bare lf\r\n" },
        { "trailing crlf\r\n", "trailing crlf\r\n" },
        { "multiple\rbare\rcr", "multiple\r\nbare\r\ncr" },
        { "multiple\nbare\nlf", "multiple\r\nbare\r\nlf" },
        { "multiple\r\ncrlf\r\n", "multiple\r\ncrlf\r\n" },
        { "mixed cr\rand\nlf", "mixed cr\r\nand\r\nlf" },
        { "mixed bare\rcr, bare\nlf and\r\ncrlf",
           "mixed bare\r\ncr, bare\r\nlf and\r\ncrlf" },
        { "adjacent\r\rbare cr", "adjacent\r\n\r\nbare cr" },
        { "adjacent\n\nbare lf", "adjacent\r\n\r\nbare lf" },
        { "adjacent\r\n\r\ncrlf", "adjacent\r\n\r\ncrlf" },
        { "more adjacent\r\r\rbare cr", "more adjacent\r\n\r\n\r\nbare cr" },
        { "more adjacent\n\n\nbare lf", "more adjacent\r\n\r\n\r\nbare lf" },
        { "more adjacent\r\n\r\n\r\ncrlf", "more adjacent\r\n\r\n\r\ncrlf" },
        { "adjacent\n\nbare\r\rmixed", "adjacent\r\n\r\nbare\r\n\r\nmixed" },
        { "tricksy\r\r\n\nsplit", "tricksy\r\n\r\n\r\nsplit" },
    };

    struct buf buf = BUF_INITIALIZER;
    size_t i;
    for (i = 0; i < sizeof(testcases) / sizeof(testcases[0]); i++) {
        struct testcase tc = testcases[i];
        buf_setcstr(&buf, tc.input);
        buf_tocrlf(&buf);
        CU_ASSERT_STRING_EQUAL(tc.expect, buf_cstring(&buf));
        buf_reset(&buf);
    }
    buf_free(&buf);
}

/* TODO: test the Copy-On-Write feature of buf_ensure()...if anyone
 * actually uses it */
/* vim: set ft=c: */
