--- /dev/null
+/* -*-c-*-
+ *
+ * The STROBE protocol framework
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of Catacomb.
+ *
+ * Catacomb is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Catacomb is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with Catacomb. If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <assert.h>
+#include <ctype.h>
+#include <string.h>
+
+#include <mLib/buf.h>
+
+#include "keccak1600.h"
+#include "strobe.h"
+
+/*----- Magic constants ---------------------------------------------------*/
+
+#define DDATA 0x04
+#define DRATE 0x80
+
+/*----- Utilities ---------------------------------------------------------*/
+
+/* --- @crank@ --- *
+ *
+ * Arguments: @strobe_ctx *ctx@ = pointer to context block to initialize
+ *
+ * Returns: ---
+ *
+ * Use: Cycle the Keccak-p[1600, n] duplex function.
+ */
+
+static void crank(strobe_ctx *ctx)
+{
+ kludge64 t[25];
+ octet *p;
+ unsigned i;
+
+ /* Ensure that we've not overstepped the rate bound. */
+ assert(ctx->n <= ctx->r - 2);
+
+ /* Apply the cSHAKE and rate padding. */
+ ctx->buf[ctx->n] ^= ctx->n0;
+ ctx->buf[ctx->n + 1] ^= DDATA;
+ ctx->buf[ctx->r - 1] ^= DRATE;
+
+ /* Cycle the sponge. */
+ for (i = 0, p = ctx->buf; i < ctx->r/8; i++)
+ { LOAD64_L_(t[i], p); p += 8; }
+ keccak1600_set(&ctx->k, t, ctx->r/8);
+ keccak1600_p(&ctx->k, &ctx->k, 24);
+ keccak1600_extract(&ctx->k, t, ctx->r/8);
+ for (i = 0, p = ctx->buf; i < ctx->r/8; i++)
+ { STORE64_L_(p, t[i]); p += 8; }
+
+ /* Restart at the beginning of the buffer, and note this as a
+ * continuation.
+ */
+ ctx->n = ctx->n0 = 0;
+}
+
+/* --- @xorbuf@ --- *
+ *
+ * Arguments: @octet *z@ = pointer to output buffer
+ * @const octet *x, *y@ = pointer to input buffers
+ * @size_t sz@ = common buffer length
+ *
+ * Returns: ---
+ *
+ * Use: Store the bytewise XOR of the buffers @x@ and @y@ in @z@.
+ * The @x@ and @y@ may be equal, but otherwise the buffers must
+ * not overlap.
+ */
+
+static void xorbuf(octet *z, const octet *x, const octet *y, size_t sz)
+ { size_t i; for (i = 0; i < sz; i++) *z++ = *x++ ^ *y++; }
+
+/* --- @nonzerop@ --- *
+ *
+ * Arguments: @const octet *x@ = pointer to input buffer
+ * @size_t sz@ = buffer length
+ *
+ * Returns: ---
+ *
+ * Use: If any byte of @x@ is nonzero, then return a nonzero value
+ * between 1 and 255 inclusive; otherwise return zero.
+ */
+
+static unsigned nonzerop(const octet *x, size_t sz)
+{
+ unsigned z = 0;
+ size_t i;
+
+ for (i = 0; i < sz; i++) z |= *x++;
+ return (z);
+}
+
+/* --- @unequalp@ --- *
+ *
+ * Arguments: @const octet *x, *y@ = pointer to input buffers
+ * @size_t sz@ = common buffer length
+ *
+ * Returns: ---
+ *
+ * Use: If any respective bytes of @x@ and @y@ are unequal, then
+ * return a nonzero value between 1 and 255 inclusive; otherwise
+ * return zero.
+ */
+
+static unsigned unequalp(const octet *x, const octet *y, size_t sz)
+{
+ unsigned z = 0;
+ size_t i;
+
+ for (i = 0; i < sz; i++) z |= *x++ ^ *y++;
+ return (z);
+}
+
+/* --- @process_buffer@ --- *
+ *
+ * Arguments: @strobe_ctx *ctx@ = pointer to context block
+ * @const octet *p@ = pointer to input buffer
+ * @octet *q@ = pointer to output buffer
+ * @size_t sz@ = common buffer length
+ *
+ * Returns: ---
+ *
+ * Use: Process a portion of a STROBE input small enough to be
+ * satisfied from the internal buffer.
+ */
+
+static void process_buffer(strobe_ctx *ctx,
+ const octet *p, octet *q, size_t sz)
+{
+ octet *b = ctx->buf + ctx->n;
+ unsigned z = 0;
+
+ if (!(ctx->f&STRBF_CRYPTO)) {
+ /* No crypto to do. The `output' would be equal to the input, so that's
+ * rather uninteresting (and, indeed, forbidden). If there's input, then
+ * mix it into the state.
+ */
+
+ if (p && (ctx->f&STRBF_VRFOUT)) z |= nonzerop(p, sz);
+ if (p) xorbuf(b, b, p, sz);
+ } else if (!(ctx->f&STRBF_MIXOUT)) {
+ /* Mix the input into the sponge state. That means that the new state
+ * will be equal to the output.
+ */
+
+ if (p) xorbuf(b, b, p, sz);
+ if (ctx->f&STRBF_VRFOUT) z |= nonzerop(b, sz);
+ if (q) memcpy(q, b, sz);
+ } else if (p) {
+ /* Mix the output into the sponge state, so the new state will in fact be
+ * equal to the input. If the input and output buffers are equal then we
+ * have a dance to do.
+ */
+
+ if (!q) {
+ if (ctx->f&STRBF_VRFOUT) z |= unequalp(p, b, sz);
+ memcpy(b, p, sz);
+ } else {
+ xorbuf(q, p, b, sz);
+ if (q != p) memcpy(b, p, sz);
+ else xorbuf(b, b, q, sz);
+ if (ctx->f&STRBF_VRFOUT) z |= nonzerop(q, sz);
+ }
+ } else {
+ /* As above, only the input is hardwired to zero. That means that we
+ * copy state bytes to the output (if any), and just clobber the state
+ * when we're done.
+ */
+
+ if (q) memcpy(q, b, sz);
+ memset(b, 0, sz);
+ }
+
+ /* Set the @STRBF_NZERO@ flag if @z@ is nonzero. If @z@ is zero then
+ * subtracting one will set all of its bits, so, in particular, bits
+ * 8--15. Otherwise, @z@ is between 1 and 255, so bits 8--15 are clear and
+ * will remain so when we subtract one.
+ */
+ if (ctx->f&STRBF_VRFOUT) ctx->f |= ((z - 1)&STRBF_NZERO) ^ STRBF_NZERO;
+
+ /* Update the buffer cursor. */
+ ctx->n += sz;
+}
+
+/*----- Interface ---------------------------------------------------------*/
+
+/* --- @strobe_init@ --- *
+ *
+ * Arguments: @strobe_ctx *ctx@ = pointer to context block to initialize
+ * @unsigned lambda@ = security parameter, in bits (must be a
+ * multiple of 32)
+ *
+ * Returns: ---
+ *
+ * Use: Initialize a STROBE context for use.
+ */
+
+void strobe_init(strobe_ctx *ctx, unsigned lambda)
+{
+ const char v[] = "STROBEv1.0.2";
+ kludge64 t[25];
+ octet *p;
+ buf b;
+ unsigned n, i;
+
+ /* Check the security parameter. */
+ assert(lambda%32 == 0); assert(lambda <= 704);
+ ctx->r = (1600 - 2*lambda)/8;
+
+ /* Set up the initial cSHAKE framing. */
+ buf_init(&b, ctx->buf, ctx->r);
+ buf_putu8(&b, 1); buf_putu8(&b, ctx->r);
+ buf_putu8(&b, 1); buf_putu8(&b, 0);
+ buf_putu8(&b, 1); buf_putu8(&b, 8*(sizeof(v) - 1));
+ buf_put(&b, v, sizeof(v) - 1);
+ assert(BOK(&b));
+ n = BLEN(&b); if (n%8) memset(ctx->buf + n, 0, 8 - n%8);
+
+ /* Cycle the sponge once initially, and get the first output buffer. */
+ keccak1600_init(&ctx->k);
+ for (i = 0, p = ctx->buf; i < (n + 7)/8; i++)
+ { LOAD64_L_(t[i], p); p += 8; }
+ keccak1600_set(&ctx->k, t, (n + 7)/8);
+ keccak1600_p(&ctx->k, &ctx->k, 24);
+ keccak1600_extract(&ctx->k, t, ctx->r/8);
+ for (i = 0, p = ctx->buf; i < ctx->r/8; i++)
+ { STORE64_L_(p, t[i]); p += 8; }
+
+ /* Initialize the other parts of the state. */
+ ctx->n = ctx->n0 = 0; ctx->f = 0;
+}
+
+/* --- @strobe_begin@ --- *
+ *
+ * Arguments: @strobe_ctx *ctx@ = pointer to context block
+ * @unsigned op@ = bitmask of flags
+ *
+ * Returns: ---
+ *
+ * Use: Begin a STROBE operation. The flags determine the behaviour
+ * of the @strobe_process@ and @strobe_done@ functions.
+ *
+ * * The @I@ bit determines the primary direction of data
+ * movement. If it's clear, data comes from the application
+ * into STROBE. If it's set, data comes from STROBE towards
+ * the application.
+ *
+ * * The @C@ bit activates cryptographic processing. If it's
+ * clear, then the input and output data would be equal, so
+ * @dest@ must be null. If it's set, then input data is
+ * XORed with the keystream on its way to the output.
+ *
+ * * The @A@ bit determines whether the application is
+ * engaged. If it's set, then the input or output buffer
+ * (according to whether @I@ is clear or set, respectively)
+ * holds the application data. If it's clear, and @I@ is
+ * clear, then zero bytes are fed in; if @I@ is set, then
+ * the output is compared with zero, and @strobe_done@
+ * reports the outcome of this comparison.
+ *
+ * * The @T@ bit determines whether the transport is engaged.
+ * If it's set, then the input or output buffer (according
+ * to whether @I@ is set or clear, respectively) holds
+ * transport data. If it's clear, and @I@ is set, then zero
+ * bytes are fed in; if @I@ is clear, then the output is
+ * discarded.
+ *
+ * * The @M@ bit marks the data as metadata, but has no other
+ * effect.
+ */
+
+void strobe_begin(strobe_ctx *ctx, unsigned op)
+{
+ /* Preliminary checking. We shouldn't have an operation underway, and the
+ * operation shouldn't have reserved bits set.
+ */
+ assert(!(ctx->f&STRBF_ACTIVE)); assert(!(op&~STRBF_VALIDMASK));
+
+ /* Reset our operation state. */
+ ctx->f &= STRBF_STMASK;
+
+ /* Operation framing. Chain back to the start of the previous frame and
+ * write the new operation code. Set the sticky asymmetry bit here if
+ * necessary.
+ */
+ ctx->buf[ctx->n++] ^= ctx->n0; ctx->n0 = ctx->n;
+ if (ctx->n >= ctx->r - 2) crank(ctx);
+ if (!(op&STRBF_T))
+ ctx->buf[ctx->n++] ^= U8(op);
+ else {
+ if (!(ctx->f&STRBF_INIT)) ctx->f |= STRBF_INIT | (op&STRBF_I);
+ ctx->buf[ctx->n++] ^= U8(op ^ ctx->f);
+ }
+ if (ctx->n >= ctx->r - 2 || (op&STRBF_C)) crank(ctx);
+
+ /* The operation is now underway. */
+ ctx->f |= STRBF_ACTIVE;
+
+ /* Determine whether we expect input and/or output. */
+ if (op&(op&STRBF_I ? STRBF_T : STRBF_A))
+ ctx->f |= STRBF_WANTIN;
+ if ((op&STRBF_C) && op&(op&STRBF_I ? STRBF_A : STRBF_T))
+ ctx->f |= STRBF_WANTOUT;
+
+ /* Determine whether the keystream is engaged, and how it fits in. */
+ if (op&STRBF_C) {
+ ctx->f |= STRBF_CRYPTO;
+ if ((op&(STRBF_I | STRBF_T)) != STRBF_T) ctx->f |= STRBF_MIXOUT;
+ }
+
+ /* Determine whether the output is supposed to be all-bytes-zero. */
+ if ((op&(STRBF_I | STRBF_A | STRBF_T)) == (STRBF_I | STRBF_T))
+ ctx->f |= STRBF_VRFOUT;
+
+ /* The operation is now underway. */
+ ctx->f |= STRBF_ACTIVE;
+}
+
+/* --- @strobe_process@ --- *
+ *
+ * Arguments: @strobe_ctx *ctx@ = pointer to context block
+ * @const void *src@ = pointer to input data, or null
+ * @void *dest@ = pointer to output data, or null
+ * @size_t sz@ = common buffer length
+ *
+ * Returns: ---
+ *
+ * Use: Process data through the active STROBE operation. The exact
+ * behaviour depends on the flags passed to @strobe_begin@; see
+ * that function for details. If @src@ is null, then the
+ * behaviour is as if the input consists of @sz@ zero bytes. If
+ * @dest@ in null, then the output is discarded.
+ */
+
+void strobe_process(strobe_ctx *ctx, const void *src, void *dest, size_t sz)
+{
+ const octet *p = src; octet *q = dest;
+ unsigned spare;
+
+ /* Make sure that things are set up properly. */
+ assert(ctx->f&STRBF_ACTIVE);
+ if (!(ctx->f&STRBF_WANTIN)) assert(!src);
+ if (!(ctx->f&STRBF_WANTOUT)) assert(!dest);
+
+ /* Work through the input. */
+ spare = ctx->r - ctx->n - 2;
+ if (sz < spare)
+ { process_buffer(ctx, p, q, sz); return; }
+ if (ctx->n) {
+ process_buffer(ctx, p, q, spare); crank(ctx);
+ if (p) { p += spare; }
+ if (q) { q += spare; }
+ sz -= spare;
+ }
+
+ while (sz >= ctx->r - 2) {
+ process_buffer(ctx, p, q, ctx->r - 2); crank(ctx);
+ if (p) { p += ctx->r - 2; }
+ if (q) { q += ctx->r - 2; }
+ sz -= ctx->r - 2;
+ }
+ if (sz) process_buffer(ctx, p, q, sz);
+}
+
+/* --- @strobe_done@ --- *
+ *
+ * Arguments: @strobe_ctx *ctx@ = pointer to context block
+ *
+ * Returns: Zero on success; @-1@ on verification failure (if @I@ and @T@
+ * are set and @A@ is clear)
+ *
+ * Use: Concludes a STROBE operation, returning the result.
+ */
+
+int strobe_done(strobe_ctx *ctx)
+{
+ assert(ctx->f&STRBF_ACTIVE); ctx->f &= ~STRBF_ACTIVE;
+ if (ctx->f&STRBF_VRFOUT) return (-(int)((ctx->f/STRBF_NZERO)&1u));
+ else return (0);
+}
+
+/* --- @strobe_key@, @strobe_ad@, @strobe_@prf@, @strobe_clrout@,
+ * @strobe_clrin@, @strobe_encout@, @strobe_encin@, @strobe_macout@,
+ * @strobe_macin@, @strobe_ratchet@ --- *
+ *
+ * Arguments: @strobe_ctx *ctx@ = pointer to context block
+ *
+ * Returns: @strobe_macin@ returns zero on success, or @-1@ on
+ * verification failure
+ *
+ * Use: Perform a STROBE operation on a single buffer.
+ */
+
+static int op(strobe_ctx *ctx, unsigned f0, unsigned f1,
+ const void *src, void *dest, size_t sz)
+{
+ assert(!(f1&~STRBF_M));
+
+ strobe_begin(ctx, f0 | f1);
+ strobe_process(ctx, src, dest, sz);
+ return (strobe_done(ctx));
+}
+
+void strobe_key(strobe_ctx *ctx, unsigned f, const void *k, size_t sz)
+ { op(ctx, STROBE_KEY, f, k, 0, sz); }
+
+void strobe_ad(strobe_ctx *ctx, unsigned f, const void *h, size_t sz)
+ { op(ctx, STROBE_AD, f, h, 0, sz); }
+
+void strobe_prf(strobe_ctx *ctx, unsigned f, void *t, size_t sz)
+ { op(ctx, STROBE_PRF, f, 0, t, sz); }
+
+void strobe_clrout(strobe_ctx *ctx, unsigned f, const void *m, size_t sz)
+ { op(ctx, STROBE_CLROUT, f, m, 0, sz); }
+
+void strobe_clrin(strobe_ctx *ctx, unsigned f, const void *m, size_t sz)
+ { op(ctx, STROBE_CLRIN, f, m, 0, sz); }
+
+void strobe_encout(strobe_ctx *ctx, unsigned f,
+ const void *m, void *c, size_t sz)
+ { op(ctx, STROBE_ENCOUT, f, m, c, sz); }
+
+void strobe_encin(strobe_ctx *ctx, unsigned f,
+ const void *c, void *m, size_t sz)
+ { op(ctx, STROBE_ENCIN, f, c, m, sz); }
+
+void strobe_macout(strobe_ctx *ctx, unsigned f, void *t, size_t sz)
+ { op(ctx, STROBE_MACOUT, f, 0, t, sz); }
+
+int strobe_macin(strobe_ctx *ctx, unsigned f, const void *t, size_t sz)
+ { return (op(ctx, STROBE_MACIN, f, t, 0, sz)); }
+
+void strobe_ratchet(strobe_ctx *ctx, unsigned f, size_t sz)
+ { op(ctx, STROBE_RATCHET, f, 0, 0, sz); }
+
+/*----- Test rig ----------------------------------------------------------*/
+
+#ifdef TEST_RIG
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <mLib/hex.h>
+#include <mLib/testrig.h>
+
+#define NSTATE 16
+
+static strobe_ctx states[NSTATE];
+
+static void dump(int rc, char win, const void *p, size_t sz)
+{
+ dstr d = DSTR_INIT;
+ const char *q = p;
+ size_t i;
+ codec *hex;
+ int printable;
+
+ if (!p) {
+ if (!rc) putchar(win);
+ else putchar('-');
+ } else {
+ for (i = 0, printable = 1; i < sz; i++)
+ if (!isprint((unsigned char)q[i])) { printable = 0; break; }
+ if (printable)
+ printf("`%s'", q);
+ else {
+ hex = hex_class.encoder(CDCF_LOWERC, 0, 0);
+ hex->ops->code(hex, p, sz, &d);
+ dstr_write(&d, stdout);
+ hex->ops->destroy(hex);
+ }
+ }
+ dstr_destroy(&d);
+ putchar('\n');
+}
+
+typedef int opfunc(strobe_ctx *, unsigned, const void *, void *, size_t);
+
+static int op_init(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_init(ctx, sz); return (0); }
+
+static int op_copy(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { *ctx = states[sz]; return (0); }
+
+static int op_begin(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_begin(ctx, f); return (0); }
+
+static int op_process(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_process(ctx, p, q, sz); return (0); }
+
+static int op_done(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { return (strobe_done(ctx)); }
+
+static int op_key(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_key(ctx, f, p, sz); return (0); }
+
+static int op_ad(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_ad(ctx, f, p, sz); return (0); }
+
+static int op_prf(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_prf(ctx, f, q, sz); return (0); }
+
+static int op_clrout(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_clrout(ctx, f, p, sz); return (0); }
+
+static int op_clrin(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_clrin(ctx, f, p, sz); return (0); }
+
+static int op_encout(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_encout(ctx, f, p, q, sz); return (0); }
+
+static int op_encin(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_encin(ctx, f, p, q, sz); return (0); }
+
+static int op_macout(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_macout(ctx, f, q, sz); return (0); }
+
+static int op_macin(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { return (strobe_macin(ctx, f, p, sz)); }
+
+static int op_ratchet(strobe_ctx *ctx, unsigned f,
+ const void *p, void *q, size_t sz)
+ { strobe_ratchet(ctx, f, sz); return (0); }
+
+static const struct optab {
+ const char *name;
+ opfunc *op;
+} optab[] = {
+#define OP(op) { #op, op_##op }
+ OP(init), OP(copy),
+ OP(begin), OP(process), OP(done),
+ OP(key), OP(ad), OP(prf),
+ OP(clrout), OP(clrin),
+ OP(encout), OP(encin),
+ OP(macout), OP(macin),
+ OP(ratchet),
+ { 0 }
+#undef OP
+};
+
+static int verify(dstr v[])
+{
+ int r;
+ strobe_ctx *ctx;
+ const char *p;
+ char *q;
+ const struct optab *op;
+ dstr d0 = DSTR_INIT, d1 = DSTR_INIT;
+ codec *hex;
+ unsigned f;
+ const void *src, *destref;
+ void *dest;
+ size_t sz;
+ int rc, rcref;
+ int ok;
+
+ /* First, get the register number. */
+ r = *(int *)v[0].buf; ctx = &states[r];
+
+ /* Next job is to parse the command and flags. */
+ q = v[1].buf; p = q; q += strcspn(q, "/"); if (*q) *q++ = 0;
+ for (op = optab; op->name; op++)
+ if (!strcmp(op->name, p)) goto found_op;
+ abort();
+found_op:
+
+ f = 0;
+ for (p = q; *p; p++) {
+ switch (*p) {
+ case 'I': f |= STRBF_I; break;
+ case 'C': f |= STRBF_C; break;
+ case 'A': f |= STRBF_A; break;
+ case 'T': f |= STRBF_T; break;
+ case 'M': f |= STRBF_M; break;
+ default: abort();
+ }
+ }
+
+ /* Convert the source parameter. */
+ p = v[2].buf;
+ if (*p == '*')
+ { src = 0; sz = strtoul(p + 1, 0, 0); }
+ else if (*p == '=')
+ { src = p + 1; sz = v[2].len - 1; }
+ else if (*p == '!') {
+ hex = hex_class.decoder(CDCF_IGNCASE);
+ rc = hex->ops->code(hex, p + 1, v[2].len - 1, &d0); assert(!rc);
+ src = d0.buf; sz = d0.len;
+ hex->ops->destroy(hex);
+ } else
+ abort();
+
+ /* Convert the destination parameter. */
+ p = v[3].buf;
+ if (*p == '+')
+ { destref = 0; rcref = 0; assert(v[3].len == 1); }
+ else if (*p == '-')
+ { destref = 0; rcref = -1; assert(v[3].len == 1); }
+ else if (*p == '=')
+ { destref = p + 1; assert(sz == v[3].len - 1); rcref = 0; }
+ else if (*p == '!') {
+ hex = hex_class.decoder(CDCF_IGNCASE);
+ rc = hex->ops->code(hex, p + 1, v[3].len - 1, &d1); assert(!rc);
+ destref = d1.buf; assert(sz == d1.len);
+ hex->ops->destroy(hex);
+ rcref = 0;
+ } else
+ abort();
+ if (!destref) dest = 0;
+ else dest = xmalloc(sz);
+
+ /* Do the operation. */
+ rc = op->op(ctx, f, src, dest, sz);
+
+ /* Check we got the right answer. */
+ ok = (rc == rcref && (!destref || !memcmp(dest, destref, sz)));
+ if (!ok) {
+ printf("failed test\n");
+ printf(" state = %d\n", r);
+ printf(" operation = %s%s%s%s%s%s%s\n",
+ op->name,
+ f ? "/" : "",
+ f&STRBF_I ? "I" : "",
+ f&STRBF_A ? "A" : "",
+ f&STRBF_C ? "C" : "",
+ f&STRBF_T ? "T" : "",
+ f&STRBF_M ? "M" : "");
+ printf(" input = "); dump(0, '*', src, sz);
+ printf(" computed = "); dump(rc, '+', dest, sz);
+ printf(" expected = "); dump(rcref, '+', destref, sz);
+ }
+
+ dstr_destroy(&d0);
+ dstr_destroy(&d1);
+ free(dest);
+ return (ok);
+}
+
+static test_chunk tests[] = {
+ { "strobe", verify,
+ { &type_int, &type_string, &type_string, &type_string, 0 } },
+ { 0, 0, { 0 } }
+};
+
+int main(int argc, char *argv[])
+{
+ test_run(argc, argv, tests, SRCDIR "/t/strobe");
+ return (0);
+}
+
+#endif
+
+/*----- That's all, folks -------------------------------------------------*/