isync

mailbox synchronization program
git clone https://git.code.sf.net/p/isync/isync
Log | Files | Refs | README | LICENSE

commit 1a1ac25bc867da5b934ddf8acf00f994f6033f25
parent df4e6383f50b496f8ce156ffbbe9a0b30294de6f
Author: Oswald Buddenhagen <ossi@users.sf.net>
Date:   Tue, 26 Apr 2022 13:45:05 +0200

track IMAP message sequence numbers (and therefore expunges)

Diffstat:
M.gitignore | 1+
Msrc/.gitignore | 3+++
Msrc/Makefile.am | 9+++++++--
Msrc/drv_imap.c | 125+++++++++++++++++++++++++++++++------------------------------------------------
Asrc/imap_msgs.c | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/imap_p.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/tst_imap_msgs.c | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 427 insertions(+), 78 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -29,6 +29,7 @@ /stamp-h /stamp-h.in /stamp-h1 +/test-driver Makefile Makefile.in diff --git a/src/.gitignore b/src/.gitignore @@ -2,8 +2,11 @@ /drv_proxy.inc /mbsync /mdconvert +/tst_imap_msgs /tst_timers /tmp .deps/ +*.log *.o +*.trs diff --git a/src/Makefile.am b/src/Makefile.am @@ -5,13 +5,13 @@ mbsync_SOURCES = \ util.c config.c socket.c \ driver.c drv_proxy.c \ - drv_imap.c \ + drv_imap.c imap_msgs.c \ drv_maildir.c \ sync.c sync_state.c \ main.c main_sync.c main_list.c noinst_HEADERS = \ common.h config.h socket.h \ - driver.h \ + driver.h imap_p.h \ sync.h sync_p.h \ main_p.h mbsync_LDADD = $(DB_LIBS) $(SSL_LIBS) $(SOCK_LIBS) $(SASL_LIBS) $(Z_LIBS) $(KEYCHAIN_LIBS) @@ -52,6 +52,11 @@ endif bin_PROGRAMS = mbsync $(mdconvert_prog) man_MANS = mbsync.1 $(mdconvert_man) +tst_imap_msgs_SOURCES = tst_imap_msgs.c imap_msgs.c util.c + +check_PROGRAMS = tst_imap_msgs +TESTS = $(check_PROGRAMS) + tst_timers_SOURCES = tst_timers.c util.c EXTRA_PROGRAMS = tst_timers diff --git a/src/drv_imap.c b/src/drv_imap.c @@ -6,7 +6,7 @@ * mbsync - mailbox synchronizer */ -#include "driver.h" +#include "imap_p.h" #include "socket.h" @@ -58,14 +58,6 @@ typedef union imap_store_conf { }; } imap_store_conf_t; -typedef union imap_message { - message_t gen; - struct { - MESSAGE(union imap_message) - // uint seq; will be needed when expunges are tracked - }; -} imap_message_t; - #define NIL (void*)0x1 #define LIST (void*)0x2 @@ -112,8 +104,8 @@ union imap_store { // but mailbox totals. int total_msgs, recent_msgs; uint uidvalidity, uidnext; - imap_message_t **msgapp, *msgs; // FETCH results - uint msgcnt; + imap_messages_t msgs; + uint fetch_seq; // FETCH results uint caps; // CAPABILITY results string_list_t *auth_mechs; parse_list_state_t parse_list_sts; @@ -139,8 +131,9 @@ union imap_store { int sasl_cont; #endif + void (*expunge_callback)( message_t *msg, void *aux ); void (*bad_callback)( void *aux ); - void *bad_callback_aux; + void *drv_callback_aux; conn_t conn; // This is BIG, so put it last }; @@ -1206,10 +1199,9 @@ parse_fetch_rsp( imap_store_t *ctx, list_t *list, char *s ATTR_UNUSED ) // Workaround for server not sending UIDNEXT and/or APPENDUID. ctx->uidnext = uid + 1; } else if (ctx->fetch_sts == FetchMsgs) { - cur = nfzalloc( sizeof(*cur) ); - *ctx->msgapp = cur; - ctx->msgapp = &cur->next; - ctx->msgcnt++; + imap_ensure_absolute( &ctx->msgs ); // In case of interleaved EXPUNGE + cur = imap_new_msg( & ctx->msgs ); + cur->seq = ctx->fetch_seq; cur->uid = uid; cur->flags = mask; cur->status = status; @@ -1524,6 +1516,14 @@ prepare_trash( char **buf, const imap_store_t *ctx ) return prepare_name( buf, ctx, ctx->prefix, ctx->conf->trash ); } +static void +record_expunge( imap_store_t *ctx, uint seq ) +{ + imap_message_t *eptr = imap_expunge_msg( &ctx->msgs, seq ); + if (eptr) + ctx->expunge_callback( &eptr->gen, ctx->drv_callback_aux ); +} + typedef union { imap_cmd_t gen; struct { @@ -1542,6 +1542,7 @@ imap_socket_read( void *aux ) imap_cmd_t *cmdp, **pcmdp; char *cmd, *arg, *arg1, *p; int resp, resp2, tag; + uint seq; conn_iovec_t iov[2]; for (;;) { @@ -1622,10 +1623,19 @@ imap_socket_read( void *aux ) if (!strcmp( "EXISTS", arg1 )) { ctx->total_msgs = atoi( arg ); } else if (!strcmp( "EXPUNGE", arg1 )) { + if (!(seq = strtoul( arg, &arg1, 10 )) || *arg1) { + badseq: + error( "IMAP error: malformed sequence number '%s'\n", arg ); + break; + } + record_expunge( ctx, seq ); ctx->total_msgs--; } else if (!strcmp( "RECENT", arg1 )) { ctx->recent_msgs = atoi( arg ); } else if (!strcmp( "FETCH", arg1 )) { + if (!(seq = strtoul( arg, &arg1, 10 )) || *arg1) + goto badseq; + ctx->fetch_seq = seq; resp = parse_list( ctx, cmd, parse_fetch_rsp ); goto listret; } @@ -1777,7 +1787,7 @@ imap_cancel_store( store_t *gctx ) cancel_pending_imap_cmds( ctx ); free( ctx->ns_prefix ); free_string_list( ctx->auth_mechs ); - free_generic_messages( &ctx->msgs->gen ); + free_generic_messages( &ctx->msgs.head->gen ); free_string_list( ctx->boxes ); imap_deref( ctx ); } @@ -1793,19 +1803,20 @@ imap_deref( imap_store_t *ctx ) } static void -imap_set_callbacks( store_t *gctx, void (*exp_cb)( message_t *, void * ) ATTR_UNUSED, - void (*cb)( void * ), void *aux ) +imap_set_callbacks( store_t *gctx, void (*exp_cb)( message_t *, void * ), + void (*bad_cb)( void * ), void *aux ) { imap_store_t *ctx = (imap_store_t *)gctx; - ctx->bad_callback = cb; - ctx->bad_callback_aux = aux; + ctx->expunge_callback = exp_cb; + ctx->bad_callback = bad_cb; + ctx->drv_callback_aux = aux; } static void imap_invoke_bad_callback( imap_store_t *ctx ) { - ctx->bad_callback( ctx->bad_callback_aux ); + ctx->bad_callback( ctx->drv_callback_aux ); } /******************* imap_free_store *******************/ @@ -1838,8 +1849,7 @@ imap_free_store( store_t *gctx ) return; } - free_generic_messages( &ctx->msgs->gen ); - ctx->msgs = NULL; + reset_imap_messages( &ctx->msgs ); imap_set_callbacks( gctx, NULL, imap_cancel_unowned, gctx ); ctx->next = unowned; unowned = ctx; @@ -2630,10 +2640,7 @@ imap_select_box( store_t *gctx, const char *name ) assert( !ctx->pending && !ctx->in_progress && !ctx->wait_check ); - free_generic_messages( &ctx->msgs->gen ); - ctx->msgs = NULL; - ctx->msgapp = &ctx->msgs; - ctx->msgcnt = 0; + reset_imap_messages( &ctx->msgs ); ctx->name = name; return DRV_OK; @@ -2922,47 +2929,6 @@ imap_load_box( store_t *gctx, uint minuid, uint maxuid, uint finduid, uint pairu } } -static int -imap_sort_msgs_comp( const void *a_, const void *b_ ) -{ - const message_t *a = *(const message_t * const *)a_; - const message_t *b = *(const message_t * const *)b_; - - if (a->uid < b->uid) - return -1; - if (a->uid > b->uid) - return 1; - return 0; -} - -static void -imap_sort_msgs( imap_store_t *ctx ) -{ - uint count = ctx->msgcnt; - if (count <= 1) - return; - - imap_message_t **t = nfmalloc( sizeof(*t) * count ); - - imap_message_t *m = ctx->msgs; - for (uint i = 0; i < count; i++) { - t[i] = m; - m = m->next; - } - - qsort( t, count, sizeof(*t), imap_sort_msgs_comp ); - - ctx->msgs = t[0]; - - uint j; - for (j = 0; j < count - 1; j++) - t[j]->next = t[j + 1]; - ctx->msgapp = &t[j]->next; - *ctx->msgapp = NULL; - - free( t ); -} - static void imap_submit_load_p2( imap_store_t *, imap_cmd_t *, int ); static void @@ -2994,8 +2960,8 @@ imap_submit_load_p3( imap_store_t *ctx, imap_load_box_state_t *sts ) DONE_REFCOUNTED_STATE_ARGS(sts, { ctx->fetch_sts = FetchNone; if (sts->ret_val == DRV_OK) - imap_sort_msgs( ctx ); - }, &ctx->msgs->gen, ctx->total_msgs, ctx->recent_msgs) + imap_ensure_relative( &ctx->msgs ); + }, &ctx->msgs.head->gen, ctx->total_msgs, ctx->recent_msgs) } /******************* imap_fetch_msg *******************/ @@ -3023,7 +2989,9 @@ imap_fetch_msg_p2( imap_store_t *ctx, imap_cmd_t *gcmd, int response ) imap_cmd_fetch_msg_t *cmd = (imap_cmd_fetch_msg_t *)gcmd; if (response == RESP_OK && !cmd->msg_data->data) { - /* The FETCH succeeded, but there is no message with this UID. */ + // The UID FETCH succeeded, but there is no message with this UID. + // The corresponding EXPUNGE response has been received by this time, + // so the message is already marked as dead. response = RESP_NO; } imap_done_simple_msg( ctx, gcmd, response ); @@ -3140,9 +3108,9 @@ imap_close_box( store_t *gctx, int bl; char buf[1000]; - for (msg = ctx->msgs; ; ) { + for (msg = ctx->msgs.head; ; ) { for (bl = 0; msg && bl < 960; msg = msg->next) { - if (!(msg->flags & F_DELETED)) + if ((msg->status & M_DEAD) || !(msg->flags & F_DELETED)) continue; if (bl) buf[bl++] = ','; @@ -3161,7 +3129,9 @@ imap_close_box( store_t *gctx, } else { /* This is inherently racy: it may cause messages which other clients * marked as deleted to be expunged without being trashed. */ - // Note that, to save bandwidth, we don't use EXPUNGE. + // Note that, to save bandwidth, we don't use EXPUNGE. Also, in many + // cases, we wouldn't be able to map the EXPUNGE responses' seq numbers + // anyway, due to not having fetched the messages. INIT_IMAP_CMD(imap_cmd_simple_t, cmd, cb, aux) imap_exec( ctx, &cmd->gen, imap_done_simple_box, "CLOSE" ); } @@ -3281,7 +3251,7 @@ imap_find_new_msgs( store_t *gctx, uint newuid, imap_store_t *ctx = (imap_store_t *)gctx; INIT_IMAP_CMD(imap_cmd_find_new_t, cmd, cb, aux) - cmd->out_msgs = ctx->msgapp; + cmd->out_msgs = ctx->msgs.tail; cmd->uid = newuid; // Some servers fail to enumerate recently APPENDed messages without syncing first. imap_exec( ctx, &cmd->gen, imap_find_new_msgs_p2, "CHECK" ); @@ -3346,6 +3316,9 @@ imap_find_new_msgs_p4( imap_store_t *ctx ATTR_UNUSED, imap_cmd_t *gcmd, int resp ctx->fetch_sts = FetchNone; transform_box_response( &response ); + // Note: unlike in load_box(), we don't call imap_ensure_relative() here, + // as it's unnecessary. It being called due to unsolicited responses + // causes no harm. cmdp->callback( response, &(*cmdp->out_msgs)->gen, cmdp->callback_aux ); } diff --git a/src/imap_msgs.c b/src/imap_msgs.c @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2022 Oswald Buddenhagen <ossi@users.sf.net> +// SPDX-License-Identifier: GPL-2.0-or-later WITH LicenseRef-isync-GPL-exception +// +// mbsync - mailbox synchronizer +// + +#include "imap_p.h" + +#ifdef DEBUG_IMAP_MSGS +# define dbg(...) print(__VA_ARGS__) +#else +# define dbg(...) do { } while (0) +#endif + +imap_message_t * +imap_new_msg( imap_messages_t *msgs ) +{ + imap_message_t *msg = nfzalloc( sizeof(*msg) ); + *msgs->tail = msg; + msgs->tail = &msg->next; + msgs->count++; + return msg; +} + +void +reset_imap_messages( imap_messages_t *msgs ) +{ + free_generic_messages( &msgs->head->gen ); + msgs->head = NULL; + msgs->tail = &msgs->head; + msgs->count = 0; + msgs->cursor_ptr = NULL; + msgs->cursor_seq = 0; +} + +static int +imap_compare_msgs( const void *a_, const void *b_ ) +{ + const imap_message_t *a = *(const imap_message_t * const *)a_; + const imap_message_t *b = *(const imap_message_t * const *)b_; + + if (a->uid < b->uid) + return -1; + if (a->uid > b->uid) + return 1; + return 0; +} + +void +imap_ensure_relative( imap_messages_t *msgs ) +{ + if (msgs->cursor_ptr) + return; + uint count = msgs->count; + if (!count) + return; + if (count > 1) { + imap_message_t **t = nfmalloc( sizeof(*t) * count ); + + imap_message_t *m = msgs->head; + for (uint i = 0; i < count; i++) { + t[i] = m; + m = m->next; + } + + qsort( t, count, sizeof(*t), imap_compare_msgs ); + + imap_message_t *nm = t[0]; + msgs->head = nm; + nm->prev = NULL; + uint seq, nseq = nm->seq; + for (uint j = 0; m = nm, seq = nseq, j < count - 1; j++) { + nm = t[j + 1]; + m->next = nm; + m->next->prev = m; + nseq = nm->seq; + nm->seq = nseq - seq; + } + msgs->tail = &m->next; + *msgs->tail = NULL; + + free( t ); + } + msgs->cursor_ptr = msgs->head; + msgs->cursor_seq = msgs->head->seq; +} + +void +imap_ensure_absolute( imap_messages_t *msgs ) +{ + if (!msgs->cursor_ptr) + return; + uint seq = 0; + for (imap_message_t *msg = msgs->head; msg; msg = msg->next) { + seq += msg->seq; + msg->seq = seq; + } + msgs->cursor_ptr = NULL; + msgs->cursor_seq = 0; +} + +imap_message_t * +imap_expunge_msg( imap_messages_t *msgs, uint fseq ) +{ + dbg( "expunge %u\n", fseq ); + imap_ensure_relative( msgs ); + imap_message_t *ret = NULL, *msg = msgs->cursor_ptr; + if (msg) { + uint seq = msgs->cursor_seq; + for (;;) { + dbg( " now on message %u (uid %u), %sdead\n", seq, msg->uid, (msg->status & M_DEAD) ? "" : "not " ); + if (seq == fseq && !(msg->status & M_DEAD)) { + dbg( " => expunging\n" ); + msg->status = M_DEAD; + ret = msg; + break; + } + if (seq < fseq) { + dbg( " is below\n" ); + if (!msg->next) { + dbg( " no next\n" ); + goto done; + } + msg = msg->next; + seq += msg->seq; + } else { + dbg( " is not below\n" ); + if (!msg->prev) { + dbg( " no prev\n" ); + break; + } + uint pseq = seq - msg->seq; + if (pseq < fseq) { + dbg( " prev too low\n" ); + break; + } + seq = pseq; + msg = msg->prev; + } + } + dbg( " => lowering\n" ); + assert( msg->seq ); + msg->seq--; + seq--; + done: + dbg( " saving cursor on %u (uid %u)\n", seq, msg->uid ); + msgs->cursor_ptr = msg; + msgs->cursor_seq = seq; + } else { + dbg( " => no messages\n" ); + } + return ret; +} diff --git a/src/imap_p.h b/src/imap_p.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2022 Oswald Buddenhagen <ossi@users.sf.net> +// SPDX-License-Identifier: GPL-2.0-or-later WITH LicenseRef-isync-GPL-exception +// +// mbsync - mailbox synchronizer +// + +#ifndef IMAP_P_H +#define IMAP_P_H + +#include "driver.h" + +//#define DEBUG_IMAP_MSGS + +typedef union imap_message { + message_t gen; + struct { + MESSAGE(union imap_message) + + union imap_message *prev; // Used to optimize lookup by seq. + // This is made relative once the fetches complete - to avoid that + // each expunge re-enumerates all subsequent messages. Dead messages + // "occupy" no sequence number themselves, but may still jump a gap. + // Note that use of sequence numbers to address messages in commands + // imposes limitations on permissible pipelining. We don't do that, + // so this is of no concern; however, we might miss the closing of + // a gap, which would result in a tiny performance hit. + uint seq; + }; +} imap_message_t; + +typedef struct { + imap_message_t *head; + imap_message_t **tail; + // Bulk changes (which is where performance matters) are assumed to be + // reported sequentially (be it forward or reverse), so walking the + // sorted linked list from the previously used message is efficient. + imap_message_t *cursor_ptr; + uint cursor_seq; + uint count; +} imap_messages_t; + +imap_message_t *imap_new_msg( imap_messages_t *msgs ); +imap_message_t *imap_expunge_msg( imap_messages_t *msgs, uint fseq ); +void reset_imap_messages( imap_messages_t *msgs ); +void imap_ensure_relative( imap_messages_t *msgs ); +void imap_ensure_absolute( imap_messages_t *msgs ); + +#endif diff --git a/src/tst_imap_msgs.c b/src/tst_imap_msgs.c @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2022 Oswald Buddenhagen <ossi@users.sf.net> +// SPDX-License-Identifier: GPL-2.0-or-later +// +// isync test suite +// + +#include "imap_p.h" + +static imap_messages_t smsgs; + +// from driver.c +void +free_generic_messages( message_t *msgs ) +{ + message_t *tmsg; + + for (; msgs; msgs = tmsg) { + tmsg = msgs->next; + // free( msgs->msgid ); + free( msgs ); + } +} + +static void +dump_messages( void ) +{ + print( "=>" ); + uint seq = 0; + for (imap_message_t *msg = smsgs.head; msg; msg = msg->next) { + seq += msg->seq; + if (msg->status & M_DEAD) + print( " (%u:%u)", seq, msg->uid ); + else + print( " %u:%u", seq, msg->uid ); + } + print( "\n" ); +} + +static void +init( uint *in ) +{ + reset_imap_messages( &smsgs ); + for (; *in; in++) { + imap_message_t *msg = imap_new_msg( &smsgs ); + msg->seq = *in; + // We (ab)use the initial sequence number as the UID. That's not + // exactly realistic, but it's valid, and saves us redundant data. + msg->uid = *in; + } +} + +static void +modify( uint *in ) +{ + for (; *in; in++) { + imap_expunge_msg( &smsgs, *in ); +#ifdef DEBUG_IMAP_MSGS + dump_messages(); +#endif + } +} + +static void +verify( uint *in, const char *name ) +{ + int fails = 0; + imap_message_t *msg = smsgs.head; + for (;;) { + if (msg && *in && msg->uid == *in) { + if (msg->status & M_DEAD) { + printf( "*** %s: message %u is dead\n", name, msg->uid ); + fails++; + } else { + assert( msg->seq ); + } + msg = msg->next; + in++; + } else if (*in && (!msg || msg->uid > *in)) { + printf( "*** %s: message %u is missing\n", name, *in ); + fails++; + in++; + } else if (msg) { + if (!(msg->status & M_DEAD)) { + printf( "*** %s: excess message %u\n", name, msg->uid ); + fails++; + } + msg = msg->next; + } else { + assert( !*in ); + break; + } + } + if (fails) + dump_messages(); +} + +static void +test( uint *ex, uint *out, const char *name ) +{ + printf( "test %s ...\n", name ); + modify( ex ); + verify( out, name ); +} + +int +main( void ) +{ + static uint arr_0[] = { 0 }; + static uint arr_1[] = { 1, 0 }; + + static uint full_in[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 0 }; + init( full_in ); +#if 0 + static uint nop[] = { 0 }; + static uint nop_out[] = { /* 1, */ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, /* 17, */ 18 /*!*/, 0 }; + test( nop, nop_out, "self-test" ); +#endif + static uint full_ex_fw1[] = { 18, 13, 13, 13, 1, 1, 1, 0 }; + static uint full_out_fw1[] = { 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 17, 0 }; + test( full_ex_fw1, full_out_fw1, "full, forward 1" ); + static uint full_ex_fw2[] = { 10, 10, 0 }; + static uint full_out_fw2[] = { 4, 5, 6, 7, 8, 9, 10, 11, 12, 0 }; + test( full_ex_fw2, full_out_fw2, "full, forward 2" ); + + init( full_in ); + static uint full_ex_bw1[] = { 18, 17, 16, 15, 14, 13, 5, 4, 3, 0 }; + static uint full_out_bw1[] = { 1, 2, 6, 7, 8, 9, 10, 11, 12, 0 }; + test( full_ex_bw1, full_out_bw1, "full, backward 1" ); + static uint full_ex_bw2[] = { 2, 1, 0 }; + static uint full_out_bw2[] = { 6, 7, 8, 9, 10, 11, 12, 0 }; + test( full_ex_bw2, full_out_bw2, "full, backward 2" ); + + static uint hole_wo1_in[] = { 10, 11, 12, 20, 21, 31, 32, 33, 34, 35, 36, 37, 0 }; + init( hole_wo1_in ); + static uint hole_wo1_ex_1[] = { 31, 30, 29, 28, 22, 21, 11, 2, 1, 0 }; + static uint hole_wo1_out_1[] = { 10, 12, 20, 32, 33, 34, 35, 36, 37, 0 }; + test( hole_wo1_ex_1, hole_wo1_out_1, "hole w/o 1, backward" ); + + init( hole_wo1_in ); + static uint hole_wo1_ex_2[] = { 1, 1, 9, 18, 18, 23, 23, 23, 23, 0 }; + test( hole_wo1_ex_2, hole_wo1_out_1, "hole w/o 1, forward" ); + test( arr_1, hole_wo1_out_1, "hole w/o 1, forward 2" ); + static uint hole_wo1_ex_4[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 }; + static uint hole_wo1_out_4[] = { 37, 0 }; + test( hole_wo1_ex_4, hole_wo1_out_4, "hole w/o 1, forward 3" ); + test( arr_1, arr_0, "hole w/o 1, forward 4" ); + test( arr_1, arr_0, "hole w/o 1, forward 5" ); + + static uint hole_w1_in[] = { 1, 10, 11, 12, 0 }; + init( hole_w1_in ); + static uint hole_w1_ex_1[] = { 11, 10, 2, 1, 0 }; + static uint hole_w1_out_1[] = { 12, 0 }; + test( hole_w1_ex_1, hole_w1_out_1, "hole w/ 1, backward" ); + test( arr_1, hole_w1_out_1, "hole w/ 1, backward 2" ); + + init( hole_w1_in ); + static uint hole_w1_ex_2[] = { 1, 1, 8, 8, 0 }; + test( hole_w1_ex_2, hole_w1_out_1, "hole w/ 1, forward" ); + static uint hole_w1_ex_4[] = { 1, 1, 1, 1, 1, 1, 1, 0 }; + static uint hole_w1_out_4[] = { 12, 0 }; + test( hole_w1_ex_4, hole_w1_out_4, "hole w/ 1, forward 2" ); + test( arr_1, arr_0, "hole w/ 1, forward 3" ); + test( arr_1, arr_0, "hole w/ 1, forward 4" ); + + return 0; +}