Login
deck.c at [6ecdbab284]
Login

File src/deck.c artifact 029a214d00 part of check-in 6ecdbab284


/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/*
  Copyright 2013-2021 The Libfossil Authors, see LICENSES/BSD-2-Clause.txt

  SPDX-License-Identifier: BSD-2-Clause-FreeBSD
  SPDX-FileCopyrightText: 2021 The Libfossil Authors
  SPDX-ArtifactOfProjectName: Libfossil
  SPDX-FileType: Code

  Heavily indebted to the Fossil SCM project (https://fossil-scm.org).
*/
/*
  This file houses the manifest/control-artifact-related APIs.
*/
#include "fossil-scm/internal.h"
#include "fossil-scm/hash.h"
#include "fossil-scm/forum.h"
#include "fossil-scm/confdb.h"
#include <assert.h>
#include <stdlib.h> /* qsort() */
#include <memory.h> /* memcmp() */

/* Only for debugging */
#define MARKER(pfexp)                                               \
  do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__);   \
    printf pfexp;                                                   \
  } while(0)

typedef int StaticAssertMCacheArraySizes[
 ((sizeof(fsl__mcache_empty.aAge)
  /sizeof(fsl__mcache_empty.aAge[0]))
 == (sizeof(fsl__mcache_empty.decks)
     /sizeof(fsl__mcache_empty.decks[0])))
 ? 1 : -1
];

enum fsl_card_F_list_flags_e {
FSL_CARD_F_LIST_NEEDS_SORT = 0x01
};

/**
   Transfers the contents of d into f->cache.mcache. If d is
   dynamically allocated then it is also freed. In any case, after
   calling this the caller must behave as if the deck had been passed
   to fsl_deck_finalize().

   If manifest caching is disabled for f, d is immediately finalized.
*/
static void fsl__cx_mcache_insert(fsl_cx *f, fsl_deck * d){
  if(!(f->flags & FSL_CX_F_MANIFEST_CACHE)){
    fsl_deck_finalize(d);
    return;
  }
  static const unsigned cacheLen =
    (unsigned)(sizeof(fsl__mcache_empty.aAge)
               /sizeof(fsl__mcache_empty.aAge[0]));
  fsl__mcache * const mc = &f->cache.mcache;
  while( d ){
    unsigned i;
    fsl_deck *pBaseline = d->B.baseline;
    d->B.baseline = 0;
    for(i=0; i<cacheLen; ++i){
      if( !mc->decks[i].rid ) break;
    }
    if( i>=cacheLen ){
      unsigned oldest = 0;
      unsigned oldestAge = mc->aAge[0];
      for(i=1; i<cacheLen; ++i){
        if( mc->aAge[i]<oldestAge ){
          oldest = i;
          oldestAge = mc->aAge[i];
        }
      }
      fsl_deck_finalize(&mc->decks[oldest]);
      i = oldest;
    }
    mc->aAge[i] = ++mc->nextAge;
    mc->decks[i] = *d;
    *d = fsl_deck_empty;
    if(&fsl_deck_empty == mc->decks[i].allocStamp){
      /* d was fsl_deck_malloc()'d so we need to free it, but cannot
         send it through fsl_deck_finalize() because that would try to
         clean up the memory we just transferred ownership of to
         mc->decks[i]. So... */
      mc->decks[i].allocStamp = 0;
      fsl_free(d);
    }
    d = pBaseline;
  }
}

/**
   Searches f->cache.mcache for a deck with the given RID. If found,
   it is bitwise copied over tgt, that entry is removed from the
   cache, and true is returned. If no match is found, tgt is not
   modified and false is returned.

   If manifest caching is disabled for f, false is immediately
   returned without causing side effects.
*/
static bool fsl__cx_mcache_search(fsl_cx * const f, fsl_id_t rid,
                                 fsl_deck * const tgt){
  if(!(f->flags & FSL_CX_F_MANIFEST_CACHE)){
    ++f->cache.mcache.misses;
    return false;
  }
  static const unsigned cacheLen =
    (int)(sizeof(fsl__mcache_empty.aAge)
          /sizeof(fsl__mcache_empty.aAge[0]));
  unsigned i;
  assert(cacheLen ==
         (unsigned)(sizeof(fsl__mcache_empty.decks)
                    /sizeof(fsl__mcache_empty.decks[0])));
  for(i=0; i<cacheLen; ++i){
    if( f->cache.mcache.decks[i].rid==rid ){
      *tgt = f->cache.mcache.decks[i];
      f->cache.mcache.decks[i] = fsl_deck_empty;
      ++f->cache.mcache.hits;
      return true;
    }
  }
  ++f->cache.mcache.misses;
  return false;
}

/**
   Code duplication reducer for fsl_deck_parse2() and
   fsl_deck_load_rid(). Checks fsl__cx_mcache_search() for rid.  If
   found, overwrites tgt with its contents and returns true.  If not
   found, returns false. If an entry is found and type!=FSL_SATYPE_ANY
   and the found deck->type differs from type then false is returned,
   FSL_RC_TYPE is returned via *rc, and f's error state is updated
   with a description of the problem. In all other case *rc is set to
   0.
 */
static bool fsl__cx_mcache_search2(fsl_cx * const f, fsl_id_t rid,
                                  fsl_deck * const tgt,
                                  fsl_satype_e type,
                                  int * const rc){
  *rc = 0;
  if(fsl__cx_mcache_search(f, rid, tgt)){
    assert(f == tgt->f);
    if(type!=FSL_SATYPE_ANY && type!=tgt->type){
      *rc = fsl_cx_err_set(f, FSL_RC_TYPE,
                           "Unexpected match of RID #%" FSL_ID_T_PFMT " "
                           "to a different artifact type (%d) "
                           "than requested (%d).",
                           tgt->type, type);
      fsl__cx_mcache_insert(f, tgt);
      assert(!tgt->f);
      return false;
    }else{
      //MARKER(("Got cached deck: rid=%d\n", (int)d->rid));
      return true;
    }
  }
  return false;
}


/**
   If mem is NULL or inside d->content.mem then this function does
   nothing, else it passes mem to fsl_free(). Intended to be used to
   clean up d->XXX string members (or sub-members) which have been
   optimized away via d->content.
*/
static void fsl_deck_free_string(fsl_deck * d, char * mem){
  assert(d);
  if(mem
     && (!d->content.used
         || !(((unsigned char const *)mem >=d->content.mem)
              &&
              ((unsigned char const *)mem < (d->content.mem+d->content.capacity)))
         )){
    fsl_free(mem);
  }/* else do nothing - the memory is NULL or owned by d->content. */
}

/**
   fsl_list_visitor_f() impl which frees fsl_list-of-(char*) card entries
   in ((fsl_deck*)visitorState).
*/
static int fsl_list_v_card_string_free(void * mCard, void * visitorState ){
  fsl_deck_free_string( (fsl_deck*)visitorState, mCard );
  return 0;
}

/** Evals to a pointer to the F-card at the given index
    in the given fsl_card_F_list pointer. Each arg is
    evaluated only once. */
#define F_at(LISTP,NDX) (&(LISTP)->list[NDX])

static int fsl_card_F_list_reserve2( fsl_card_F_list * li ){
  return (li->used<li->capacity)
    ? 0
    : fsl_card_F_list_reserve(li, li->capacity
                               ? li->capacity*4/3+1
                               : 50);
}

static void fsl_card_F_clean( fsl_card_F * f ){
  if(!f->deckOwnsStrings){
    fsl_free(f->name);
    fsl_free(f->uuid);
    fsl_free(f->priorName);
  }
  *f = fsl_card_F_empty;
}

/**
   Cleans up the F-card at li->list[ndx] and shifts all F-cards to its
   right one entry to the left.
*/
static void fsl_card_F_list_remove(fsl_card_F_list * li,
                                   uint32_t ndx){
  uint32_t i;
  assert(li->used);
  assert(ndx<li->used);
  fsl_card_F_clean(F_at(li,ndx));
  for( i = ndx; i < li->used - 1; ++i ){
    li->list[i] = li->list[i+1];
  }
  li->list[li->used] = fsl_card_F_empty;
  --li->used;
}

void fsl_card_F_list_finalize( fsl_card_F_list * li ){
  uint32_t i;
  for(i=0; i < li->used; ++i){
    fsl_card_F_clean(F_at(li,i));  
  }
  li->used = li->capacity = 0;
  fsl_free(li->list);
  *li = fsl_card_F_list_empty;
}

int fsl_card_F_list_reserve( fsl_card_F_list * li, uint32_t n ){
  if(li->capacity>=n) return 0;
  else if(n==0){
    fsl_card_F_list_finalize(li);
    return 0;
  }else{
    fsl_card_F * re = fsl_realloc(li->list, n * sizeof(fsl_card_F));
    if(re){
      li->list = re;
      li->capacity = n;
    }
    return re ? 0 : FSL_RC_OOM;
  }
}

/**
   Adjusts the end of the give list by +1, reserving more space if
   needed, and returns the next available F-card in a cleanly-wiped
   state. Returns NULL on alloc error.
*/
static fsl_card_F * fsl_card_F_list_push( fsl_card_F_list * li ){
  if(li->used==li->capacity && fsl_card_F_list_reserve2(li)) return NULL;
  li->list[li->used] = fsl_card_F_empty;
  if(li->used){
    li->flags |= FSL_CARD_F_LIST_NEEDS_SORT/*pessimistic assumption*/;
  }
  return &li->list[li->used++];
}
/**
   Chops the last entry off of the given list, freeing any resources
   owned by that entry. Decrements li->used. Asserts that li->used is
   positive.
*/
static void fsl_card_F_list_pop( fsl_card_F_list * li ){
  assert(li->used);
  if(li->used) fsl_card_F_clean(F_at(li, --li->used));
}

fsl_card_Q * fsl_card_Q_malloc(fsl_cherrypick_type_e type,
                               fsl_uuid_cstr target,
                               fsl_uuid_cstr baseline){
  int const targetLen = target ? fsl_is_uuid(target) : 0;
  int const baselineLen = baseline ? fsl_is_uuid(baseline) : 0;
  if((type!=FSL_CHERRYPICK_ADD && type!=FSL_CHERRYPICK_BACKOUT)
     || !target || !targetLen
     || (baseline && !baselineLen)) return NULL;
  else{
    fsl_card_Q * c =
      (fsl_card_Q*)fsl_malloc(sizeof(fsl_card_Q));
    if(c){
      int rc = 0;
      *c = fsl_card_Q_empty;
      c->type = type;
      c->target = fsl_strndup(target, targetLen);
      if(!c->target) rc = FSL_RC_OOM;
      else if(baseline){
        c->baseline = fsl_strndup(baseline, baselineLen);
        if(!c->baseline) rc = FSL_RC_OOM;
      }
      if(rc){
        fsl_card_Q_free(c);
        c = NULL;
      }
    }
    return c;
  }
}

void fsl_card_Q_free( fsl_card_Q * cp ){
  if(cp){
    fsl_free(cp->target);
    fsl_free(cp->baseline);
    *cp = fsl_card_Q_empty;
    fsl_free(cp);
  }
}

fsl_card_J * fsl_card_J_malloc(bool isAppend,
                               char const * field,
                               char const * value){
  if(!field || !*field) return NULL;
  else{
    fsl_card_J * c =
      (fsl_card_J*)fsl_malloc(sizeof(fsl_card_J));
    if(c){
      int rc = 0;
      fsl_size_t const lF = fsl_strlen(field);
      fsl_size_t const lV = value ? fsl_strlen(value) : 0;
      *c = fsl_card_J_empty;
      c->append = isAppend ? 1 : 0;
      c->field = fsl_strndup(field, (fsl_int_t)lF);
      if(!c->field) rc = FSL_RC_OOM;
      else if(value && *value){
        c->value = fsl_strndup(value, (fsl_int_t)lV);
        if(!c->value) rc = FSL_RC_OOM;
      }
      if(rc){
        fsl_card_J_free(c);
        c = NULL;
      }
    }
    return c;
  }
}

void fsl_card_J_free( fsl_card_J * cp ){
  if(cp){
    fsl_free(cp->field);
    fsl_free(cp->value);
    *cp = fsl_card_J_empty;
    fsl_free(cp);
  }
}

/**
    fsl_list_visitor_f() impl which requires that obj be-a (fsl_card_T*),
    which this function passes to fsl_card_T_free().
*/
static int fsl_list_v_card_T_free(void * obj, void * visitorState ){
  if(obj) fsl_card_T_free( (fsl_card_T*)obj );
  return 0;
}

static int fsl_list_v_card_Q_free(void * obj, void * visitorState ){
  if(obj) fsl_card_Q_free( (fsl_card_Q*)obj );
  return 0;
}

static int fsl_list_v_card_J_free(void * obj, void * visitorState ){
  if(obj) fsl_card_J_free( (fsl_card_J*)obj );
  return 0;
}

fsl_deck * fsl_deck_malloc(){
  fsl_deck * rc = (fsl_deck *)fsl_malloc(sizeof(fsl_deck));
  if(rc){
    *rc = fsl_deck_empty;
    rc->allocStamp = &fsl_deck_empty;
  }
  return rc;
}

void fsl_deck_init( fsl_cx * const f, fsl_deck * const cards, fsl_satype_e type ){
  void const * allocStamp = cards->allocStamp;
  *cards = fsl_deck_empty;
  cards->allocStamp = allocStamp;
  cards->f = f;
  cards->type = type;
}

void fsl__card_J_list_free(fsl_list * li, bool alsoListMem){
  if(li->used) fsl_list_visit(li, 0, fsl_list_v_card_J_free, NULL);
  if(alsoListMem) fsl_list_reserve(li, 0);
  else li->used = 0;
}

/* fsl_deck cleanup helpers... */
#define SFREE(X) fsl_deck_clean_string(m, &m->X)
#define SLIST(X) fsl_list_clear(&m->X, fsl_list_v_card_string_free, m)
#define CBUF(X) fsl_buffer_clear(&m->X)
static void fsl_deck_clean_string(fsl_deck *m, char **member){
  fsl_deck_free_string(m, *member);
  *member = 0;
}
static void fsl_deck_clean_version(fsl_deck *const m){
  m->rid = 0;
}
static void fsl_deck_clean_A(fsl_deck *const m){
  SFREE(A.name);
  SFREE(A.tgt);
  SFREE(A.src);
}
static void fsl_deck_clean_B(fsl_deck * const m){
  if(m->B.baseline){
    assert(!m->B.baseline->B.uuid && "Baselines cannot have a B-card. API misuse?");
    fsl_deck_finalize(m->B.baseline);
    m->B.baseline = NULL;
  }
  SFREE(B.uuid);
}
static void fsl_deck_clean_C(fsl_deck * const m){
  fsl_deck_clean_string(m, &m->C);
}
static void fsl_deck_clean_E(fsl_deck * const m){
  fsl_deck_clean_string(m, &m->E.uuid);
  m->E = fsl_deck_empty.E;
}
static void fsl_deck_clean_F(fsl_deck * const m){
  if(m->F.list){
    fsl_card_F_list_finalize(&m->F);
    m->F = fsl_deck_empty.F;
  }
}
static void fsl_deck_clean_G(fsl_deck * const m){
  fsl_deck_clean_string(m, &m->G);
}
static void fsl_deck_clean_H(fsl_deck * const m){
  fsl_deck_clean_string(m, &m->H);
}
static void fsl_deck_clean_I(fsl_deck * const m){
  fsl_deck_clean_string(m, &m->I);
}
static void fsl_deck_clean_J(fsl_deck * const m, bool alsoListMem){
  fsl__card_J_list_free(&m->J, alsoListMem);
}
static void fsl_deck_clean_K(fsl_deck * const m){
  fsl_deck_clean_string(m, &m->K);
}
static void fsl_deck_clean_L(fsl_deck * const m){
  fsl_deck_clean_string(m, &m->L);
}
static void fsl_deck_clean_M(fsl_deck * const m){
  SLIST(M);
}
static void fsl_deck_clean_N(fsl_deck * const m){
  fsl_deck_clean_string(m, &m->N);
}
static void fsl_deck_clean_P(fsl_deck * const m){
  fsl_list_clear(&m->P, fsl_list_v_card_string_free, m);
}
static void fsl_deck_clean_Q(fsl_deck * const m){
  fsl_list_clear(&m->Q, fsl_list_v_card_Q_free, NULL);
}
static void fsl_deck_clean_R(fsl_deck * const m){
  fsl_deck_clean_string(m, &m->R);
}
static void fsl_deck_clean_T(fsl_deck * const m){
  fsl_list_clear(&m->T, fsl_list_v_card_T_free, NULL);
}
static void fsl_deck_clean_U(fsl_deck * const m){
  fsl_deck_clean_string(m, &m->U);
}
static void fsl_deck_clean_W(fsl_deck * const m){
  if(m->W.capacity/*dynamically-allocated buffer*/){
    CBUF(W);
  }else{/*empty or external buffer pointing into to m->content.mem*/
    m->W = fsl_buffer_empty;
  }
}

void fsl_deck_clean2(fsl_deck * const m, fsl_buffer * const xferBuf){
  if(!m) return;
  fsl_deck_clean_version(m);  
  fsl_deck_clean_A(m);
  fsl_deck_clean_B(m);
  fsl_deck_clean_C(m);
  m->D = 0.0;
  fsl_deck_clean_E(m);
  fsl_deck_clean_F(m);
  fsl_deck_clean_G(m);
  fsl_deck_clean_H(m);
  fsl_deck_clean_I(m);
  fsl_deck_clean_J(m,true);
  fsl_deck_clean_K(m);
  fsl_deck_clean_L(m);
  fsl_deck_clean_M(m);
  fsl_deck_clean_N(m);
  fsl_deck_clean_P(m);
  fsl_deck_clean_Q(m);
  fsl_deck_clean_R(m);
  fsl_deck_clean_T(m);
  fsl_deck_clean_U(m);
  fsl_deck_clean_W(m);
  if(xferBuf){
    fsl_buffer_swap(&m->content, xferBuf);
    fsl_buffer_reuse(xferBuf);
  }
  CBUF(content) /* content must be after all cards because some point
                   back into it and we need this memory intact in
                   order to know that!
                */;
  {
    void const * const allocStampKludge = m->allocStamp;
    fsl_cx * const f = m->f;
    *m = fsl_deck_empty;
    m->allocStamp = allocStampKludge;
    m->f = f;
  }
}
#undef CBUF
#undef SFREE
#undef SLIST

void fsl_deck_clean(fsl_deck * const m){
  fsl_deck_clean2(m, NULL);
}

void fsl_deck_finalize(fsl_deck * const m){
  void const * allocStamp;
  if(!m) return;
  allocStamp = m->allocStamp;
  fsl_deck_clean(m);
  if(allocStamp == &fsl_deck_empty){
    fsl_free(m);
  }else{
    m->allocStamp = allocStamp;
  }
}

int fsl_card_is_legal( fsl_satype_e t, char card ){
  /*
    Implements this table:
    
    https://fossil-scm.org/index.html/doc/trunk/www/fileformat.wiki#summary
  */
  if('Z'==card) return 1;
  else switch(t){
    case FSL_SATYPE_ANY:
      switch(card){
        case 'A': case 'B': case 'C': case 'D':
        case 'E': case 'F': case 'J': case 'K':
        case 'L': case 'M': case 'N': case 'P':
        case 'Q': case 'R': case 'T': case 'U':
        case 'W':
          return -1;
        default:
          return 0;
      }
    case FSL_SATYPE_ATTACHMENT:
      switch(card){
        case 'A': case 'D':
          return 1;
        case 'C': case 'N': case 'U':
          return -1;
        default:
          return 0;
      };
    case FSL_SATYPE_CLUSTER:
      return 'M'==card ? 1 : 0;
    case FSL_SATYPE_CONTROL:
      switch(card){
        case 'D': case 'T': case 'U':
          return 1;
        default:
          return 0;
      };
    case FSL_SATYPE_EVENT:
      switch(card){
        case 'D': case 'E':
        case 'W':
          return 1;
        case 'C': case 'N':
        case 'P': case 'T':
        case 'U':
          return -1;
        default:
          return 0;
      };
    case FSL_SATYPE_CHECKIN:
      switch(card){
        case 'C': case 'D':
        case 'U':
          return 1;
        case 'B': case 'F': 
        case 'N': case 'P':
        case 'Q': case 'R':
        case 'T': 
          return -1;
        default:
          return 0;
      };
    case FSL_SATYPE_TICKET:
      switch(card){
        case 'D': case 'J':
        case 'K': case 'U':
          return 1;
        default:
          return 0;
      };
    case FSL_SATYPE_WIKI:
      switch(card){
        case 'D': case 'L':
        case 'U': case 'W':
          return 1;
        case 'C':
        case 'N': case 'P':
          return -1;
        default:
          return 0;
      };
    case FSL_SATYPE_FORUMPOST:
      switch(card){
        case 'D': case 'U': case 'W':
          return 1;
        case 'G': case 'H': case 'I':
        case 'N': case 'P':
          return -1;
        default:
          return 0;
      };
    default:
      MARKER(("invalid fsl_satype_e value: %d, card=%c\n", t, card));
      assert(!"Invalid fsl_satype_e.");
      return 0;
  };
}

bool fsl_deck_has_required_cards( fsl_deck const * d ){
  if(!d) return 0;
  switch(d->type){
    case FSL_SATYPE_ANY:
      return 0;
#define NEED(CARD,COND) \
      if(!(COND)) {                                         \
        fsl_cx_err_set(d->f, FSL_RC_SYNTAX,                 \
                       "Required %c-card is missing or invalid.", \
                       *#CARD);                                   \
        return false;                                             \
      } (void)0
    case FSL_SATYPE_ATTACHMENT:
      NEED(A,d->A.name);
      NEED(A,d->A.tgt);
      NEED(D,d->D > 0);
      return 1;
    case FSL_SATYPE_CLUSTER:
      NEED(M,d->M.used);
      return 1;
    case FSL_SATYPE_CONTROL:
      NEED(D,d->D > 0);
      NEED(U,d->U);
      NEED(T,d->T.used>0);
      return 1;
    case FSL_SATYPE_EVENT:
      NEED(D,d->D > 0);
      NEED(E,d->E.julian>0);
      NEED(E,d->E.uuid);
      NEED(W,d->W.used);
      return 1;
    case FSL_SATYPE_CHECKIN:
      /*
        Historically we need both or neither of F- and R-cards, but
        the R-card has become optional because it's so expensive to
        calculate and verify.

        Manifest #1 has an empty file list and an R-card with a
        constant (repo/manifest-independent) hash
        (d41d8cd98f00b204e9800998ecf8427e, the initial MD5 hash
        state).

        R-card calculation is runtime-configurable option.
      */
      NEED(D,d->D > 0);
      NEED(C,d->C);
      NEED(U,d->U);
#if 0
      /* It turns out that because the R-card is optional,
         we can have a legal manifest with no F-cards. */
      NEED(F,d->F.used || d->R/*with initial-state md5 hash!*/);
#endif
      if(!d->R
         && (FSL_CX_F_CALC_R_CARD & d->f->flags)){
        fsl_cx_err_set(d->f, FSL_RC_SYNTAX,
                       "%s deck is missing an R-card, "
                       "yet R-card calculation is enabled.",
                       fsl_satype_cstr(d->type));
        return 0;
      }else if(d->R
               && !d->F.used
               && 0!=fsl_strcmp(d->R, FSL_MD5_INITIAL_HASH)
               ){
        fsl_cx_err_set(d->f, FSL_RC_SYNTAX,
                       "Deck has no F-cards, so we expect its "
                       "R-card is to have the initial-state MD5 "
                       "hash (%.12s...). Instead we got: %s",
                       FSL_MD5_INITIAL_HASH, d->R);
        return 0;
      }
      return 1;
    case FSL_SATYPE_TICKET:
      NEED(D,d->D > 0);
      NEED(K,d->K);
      NEED(U,d->U);
      NEED(J,d->J.used)
        /* Is a J strictly required?  Spec is not clear but DRH
           confirms the current fossil(1) code expects a J card. */;
      return 1;
    case FSL_SATYPE_WIKI:
      NEED(D,d->D > 0);
      NEED(L,d->L);
      NEED(U,d->U);
      /*NEED(W,d->W.used);*/
      return 1;
    case FSL_SATYPE_FORUMPOST:
      NEED(D,d->D > 0);
      NEED(U,d->U);
      /*NEED(W,d->W.used);*/
      return 1;
    case FSL_SATYPE_INVALID:
    default:
      assert(!"Invalid fsl_satype_e.");
      return 0;
  }
#undef NEED
}

char const * fsl_satype_cstr(fsl_satype_e t){
  switch(t){
#define C(X) case FSL_SATYPE_##X: return #X
    C(ANY);
    C(CHECKIN);
    C(CLUSTER);
    C(CONTROL);
    C(WIKI);
    C(TICKET);
    C(ATTACHMENT);
    C(EVENT);
    C(FORUMPOST);
    C(INVALID);
    C(BRANCH_START);
    default:
      assert(!"UNHANDLED fsl_satype_e");
      return "!UNKNOWN!";
  }
}

char const * fsl_satype_event_cstr(fsl_satype_e t){
  switch(t){
    case FSL_SATYPE_ANY: return "*";
    case FSL_SATYPE_BRANCH_START:
    case FSL_SATYPE_CHECKIN: return "ci";
    case FSL_SATYPE_EVENT: return "e";
    case FSL_SATYPE_CONTROL: return "g";
    case FSL_SATYPE_TICKET: return "t";
    case FSL_SATYPE_WIKI: return "w";
    case FSL_SATYPE_FORUMPOST: return "f";
    default:
      return NULL;
  }
}


/**
    If fsl_card_is_legal(d->type, card), returns true, else updates
    d->f->error with a description of the constraint violation and
    returns 0.
 */
static bool fsl_deck_check_type( fsl_deck * const d, char card ){
  if(fsl_card_is_legal(d->type, card)) return true;
  else{
    fsl_cx_err_set(d->f, FSL_RC_TYPE,
                   "Card type '%c' is not allowed "
                   "in artifacts of type %s.",
                   card, fsl_satype_cstr(d->type));
    return false;
  }
}

/**
   If the first n bytes of the given string contain any values <=32,
   returns FSL_RC_SYNTAX, else returns 0. mf->f's error state is
   updated no error. n<0 means to use fsl_strlen() to count the
   length.
*/
static int fsl_deck_strcheck_ctrl_chars(fsl_deck * const mf, char cardName, char const * v, fsl_int_t n){
  const char * z = v;
  int rc = 0;
  if(v && n<0) n = fsl_strlen(v);
  for( ; v && z < v+n; ++z ){
    if(*z <= 32){
      rc = fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                         "Invalid character in %c-card.", cardName);
      break;
    }
  }
  return rc;
}

/*
  Implements fsl_deck_LETTER_set() for certain letters: those
  implemented as a fsl_uuid_str or an md5, holding a single hex string
  value.
  
  The function returns FSL_RC_SYNTAX if
  (valLen!=ASSERTLEN). ASSERTLEN is assumed to be either an SHA1,
  SHA3, or MD5 hash value and it is validated against
  fsl_validate16(value,valLen), returning FSL_RC_SYNTAX if that
  check fails. In debug builds, the expected ranges are assert()ed.

  If value is NULL then it is removed from the card instead
  (semantically freed), *mfMember is set to NULL, and 0 is returned.
*/
static int fsl_deck_sethex_impl( fsl_deck * const mf, fsl_uuid_cstr value,
                                 char letter,
                                 fsl_size_t assertLen,
                                 char ** mfMember ){
  assert(mf);
  assert( value ? (assertLen==FSL_STRLEN_SHA1
                   || assertLen==FSL_STRLEN_K256
                   || assertLen==FSL_STRLEN_MD5)
          : 0==assertLen );
  if(value && !fsl_deck_check_type(mf,letter)) return mf->f->error.code;
  else if(!value){
    fsl_deck_free_string(mf, *mfMember);
    *mfMember = NULL;
    return 0;
  }else if(fsl_strlen(value) != assertLen){
    return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                          "Invalid length for %c-card: expecting %d.",
                          letter, (int)assertLen);
  }else if(!fsl_validate16(value, assertLen)) {
    return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                          "Invalid hexadecimal value for %c-card.", letter);
  }else{
    fsl_deck_free_string(mf, *mfMember);
    *mfMember = fsl_strndup(value, assertLen);
    return *mfMember ? 0 : FSL_RC_OOM;
  }
}

/**
    Implements fsl_set_set_XXX() where XXX is a fsl_buffer member of fsl_deck.
 */
static int fsl_deck_b_setuffer_impl( fsl_deck * const mf, char const * value,
                                     fsl_int_t valLen,
                                     char letter, fsl_buffer * buf){
  assert(mf);  
  if(!fsl_deck_check_type(mf,letter)) return mf->f->error.code;
  else if(valLen<0) valLen = (fsl_int_t)fsl_strlen(value);
  buf->used = 0;
  if(value && (valLen>0)){
    return fsl_buffer_append( buf, value, valLen );
  }else{
    if(buf->mem) buf->mem[0] = 0;
    return 0;
  }
}

int fsl_deck_B_set( fsl_deck * const mf, fsl_uuid_cstr uuidBaseline){
  if(!mf) return FSL_RC_MISUSE;
  else{
    int const bLen = uuidBaseline ? fsl_is_uuid(uuidBaseline) : 0;
    if(uuidBaseline && !bLen){
      return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                            "Invalid B-card value: %s", uuidBaseline);
    }
    if(mf->B.baseline){
      fsl_deck_finalize(mf->B.baseline);
      mf->B.baseline = NULL;
    }
    return fsl_deck_sethex_impl(mf, uuidBaseline, 'B',
                                bLen, &mf->B.uuid);
  }
}

/**
    Internal impl for card setters which consist of a simple (char *)
    member. Replaces and frees any prior value. Passing NULL for the
    4th argument unsets the given card (assigns NULL to it).
 */
static int fsl_deck_set_string( fsl_deck * const mf, char letter, char ** member, char const * v, fsl_int_t n ){
  if(!fsl_deck_check_type(mf, letter)) return mf->f->error.code;
  fsl_deck_free_string(mf, *member);
  *member = v ? fsl_strndup(v, n) : NULL;
  if(v && !*member) return FSL_RC_OOM;
  else return 0;
}

int fsl_deck_C_set( fsl_deck * const mf, char const * v, fsl_int_t n){
  return fsl_deck_set_string( mf, 'C', &mf->C, v, n );
}

int fsl_deck_G_set( fsl_deck * const mf, fsl_uuid_cstr uuid){
  int const uLen = fsl_is_uuid(uuid);
  return uLen
    ? fsl_deck_sethex_impl(mf, uuid, 'G', uLen, &mf->G)
    : FSL_RC_SYNTAX;
}

int fsl_deck_H_set( fsl_deck * const mf, char const * v, fsl_int_t n){
  if(v && mf->I) return FSL_RC_SYNTAX;
  return fsl_deck_set_string( mf, 'H', &mf->H, v, n );
}

int fsl_deck_I_set( fsl_deck * const mf, fsl_uuid_cstr uuid){
  if(uuid && mf->H) return FSL_RC_SYNTAX;
  int const uLen = uuid ? fsl_is_uuid(uuid) : 0;
  return fsl_deck_sethex_impl(mf, uuid, 'I', uLen, &mf->I);
}

int fsl_deck_J_add( fsl_deck * const mf, char isAppend,
                    char const * field, char const * value){
  if(!field) return FSL_RC_MISUSE;
  else if(!*field) return FSL_RC_SYNTAX;
  else if(!fsl_deck_check_type(mf,'J')) return mf->f->error.code;
  else{
    int rc;
    fsl_card_J * cp = fsl_card_J_malloc(isAppend, field, value);
    if(!cp) rc = FSL_RC_OOM;
    else if( 0 != (rc = fsl_list_append(&mf->J, cp))){
      fsl_card_J_free(cp);
    }
    return rc;
  }
}


int fsl_deck_K_set( fsl_deck * const mf, fsl_uuid_cstr uuid){
  int const uLen = fsl_is_uuid(uuid);
  return uLen
    ? fsl_deck_sethex_impl(mf, uuid, 'K', uLen, &mf->K)
    : FSL_RC_SYNTAX;
}

int fsl_deck_L_set( fsl_deck * const mf, char const * v, fsl_int_t n){
  return mf
    ? fsl_deck_set_string(mf, 'L', &mf->L, v, n)
    : FSL_RC_SYNTAX;
}

int fsl_deck_M_add( fsl_deck * const mf, char const *uuid){
  int rc;
  char * dupe;
  int const uLen = uuid ? fsl_is_uuid(uuid) : 0;
  if(!uuid) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf, 'M')) return mf->f->error.code;
  else if(!uLen) return FSL_RC_SYNTAX;
  dupe = fsl_strndup(uuid, uLen);
  if(!dupe) rc = FSL_RC_OOM;
  else{
    rc = fsl_list_append( &mf->M, dupe );
    if(rc){
      fsl_free(dupe);
    }
  }
  return rc;
}

int fsl_deck_N_set( fsl_deck * const mf, char const * v, fsl_int_t n){
  int rc = 0;
  if(v && n!=0){
    if(n<0) n = fsl_strlen(v);
    rc = fsl_deck_strcheck_ctrl_chars(mf, 'N', v, n);
  }
  return rc ? rc : fsl_deck_set_string( mf, 'N', &mf->N, v, n );
}


/**
   Adds either parentUuid or takeParentUuid to mf->P. ONE
   of those must e non-NULL and the other must be NULL. If
   takeParentUuid is not NULL then ownership of it is transfered
   to this function regardless of success or failure.
*/
static int fsl__deck_P_add_impl( fsl_deck * const mf,
                                 fsl_uuid_cstr parentUuid,
                                 fsl_uuid_str takeParentUuid){
  if(!fsl_deck_check_type(mf, 'P')){
    fsl_free(takeParentUuid);
    return mf->f->error.code;
  }
  int rc;
  char * dupe;
  fsl_uuid_cstr z = parentUuid ? parentUuid : takeParentUuid;
  assert(parentUuid ? !takeParentUuid : !!takeParentUuid);
  int const uLen = fsl_is_uuid(z);
  if(!uLen){
    fsl_free(takeParentUuid);
    return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                         "Invalid UUID for P-card.");
  }
  dupe = takeParentUuid
    ? takeParentUuid
    : fsl_strndup(parentUuid, uLen);
  if(!dupe) rc = FSL_RC_OOM;
  else{
    rc = fsl_list_append( &mf->P, dupe );
    if(rc){
      fsl_free(dupe);
    }
  }
  return rc;
}


int fsl_deck_P_add(fsl_deck * const mf, char const *parentUuid){
  return fsl__deck_P_add_impl(mf, parentUuid, NULL);
}

int fsl_deck_P_add_rid( fsl_deck * const mf, fsl_id_t rid ){
  fsl_uuid_str pU = fsl_rid_to_uuid(mf->f, rid);
  return pU ? fsl__deck_P_add_impl(mf, NULL, pU) : mf->f->error.code;
}


fsl_id_t fsl_deck_P_get_id(fsl_deck * const d, int index){
  if(!d->f) return -1;
  else if(index>(int)d->P.used) return 0;
  else return fsl_uuid_to_rid(d->f, (char const *)d->P.list[index]);
}


int fsl_deck_Q_add( fsl_deck * const mf, int type,
                    fsl_uuid_cstr target,
                    fsl_uuid_cstr baseline ){
  if(!target) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf,'Q')) return mf->f->error.code;
  else if(!type || !fsl_is_uuid(target)
          || (baseline && !fsl_is_uuid(baseline))) return FSL_RC_SYNTAX;
  else{
    int rc;
    fsl_card_Q * cp = fsl_card_Q_malloc(type, target, baseline);
    if(!cp) rc = FSL_RC_OOM;
    else if( 0 != (rc = fsl_list_append(&mf->Q, cp))){
      fsl_card_Q_free(cp);
    }
    return rc;
  }
}

/**
    A comparison routine for qsort(3) which compares fsl_card_F
    instances in a lexical manner based on their names.  The order is
    important for card ordering in generated manifests.

    It expects that each argument is a (fsl_card_F const *).
*/
static int fsl_card_F_cmp( void const * lhs, void const * rhs ){
  fsl_card_F const * const l = (fsl_card_F const *)lhs;
  fsl_card_F const * const r = (fsl_card_F const *)rhs;
  /* Compare NULL as larger so that NULLs move to the right. That said,
     we aren't expecting any NULLs. */
  assert(l);
  assert(r);
  if(!l) return r ? 1 : 0;
  else if(!r) return -1;
  else return fsl_strcmp(l->name, r->name);
}

static void fsl_card_F_list_sort(fsl_card_F_list * li){
  if(FSL_CARD_F_LIST_NEEDS_SORT & li->flags){
    qsort(li->list, li->used, sizeof(fsl_card_F),
          fsl_card_F_cmp );
    li->flags &= ~FSL_CARD_F_LIST_NEEDS_SORT;
  }
}

static void fsl_deck_F_sort(fsl_deck * const mf){
  fsl_card_F_list_sort(&mf->F);
}

int fsl_card_F_compare_name( fsl_card_F const * const lhs,
                             fsl_card_F const * const rhs){
  return (lhs == rhs) ? 0 : fsl_card_F_cmp( lhs, rhs );
}

int fsl_deck_R_set( fsl_deck * const mf, fsl_uuid_cstr md5){
  return mf
    ? fsl_deck_sethex_impl(mf, md5, 'R', md5 ? FSL_STRLEN_MD5 : 0, &mf->R)
    : FSL_RC_MISUSE;
}

int fsl_deck_R_calc2(fsl_deck * const mf, char ** tgt){
  fsl_cx * const f = mf->f;
  char const * theHash = 0;
  char hex[FSL_STRLEN_MD5+1];
  if(!f) return FSL_RC_MISUSE;
  else if(!fsl_needs_repo(f)){
    return FSL_RC_NOT_A_REPO;
  }else if(!fsl_deck_check_type(mf,'R')) {
    assert(mf->f->error.code);
    return mf->f->error.code;
  }else if(!mf->F.used){
    theHash = FSL_MD5_INITIAL_HASH;
    /* fall through and set hash */
  }else{
    int rc = 0;
    fsl_card_F const * fc;
    fsl_id_t fileRid;
    fsl_buffer * const buf = &f->cache.fileContent;
    unsigned char digest[16];
    fsl_md5_cx md5 = fsl_md5_cx_empty;
    enum { NumBufSize = 40 };
    char numBuf[NumBufSize] = {0};
    assert(!buf->used && "Misuse of f->fileContent buffer.");
    rc = fsl_deck_F_rewind(mf);
    if(rc) goto end;
    fsl_deck_F_sort(mf);
    /*
      TODO:

      Missing functionality:

      - The "wd" (working directory) family of functions, needed for
      symlink handling.
    */
    while(1){
      rc = fsl_deck_F_next(mf, &fc);
      /* MARKER(("R rc=%s file #%d: %s %s\n", fsl_rc_cstr(rc), ++i, fc ? fc->name : "<END>", fc ? fc->uuid : NULL)); */
      if(rc || !fc) break;
      assert(fc->uuid && "We no longer iterate over deleted entries.");
      if(FSL_FILE_PERM_LINK==fc->perm){
        rc = fsl_cx_err_set(f, FSL_RC_UNSUPPORTED,
                            "This code does not yet properly handle "
                            "F-cards of symlinks.");
        goto end;
      }
      fileRid = fsl_uuid_to_rid( f, fc->uuid );
      if(0==fileRid){
        rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                            "Cannot resolve RID for F-card UUID [%s].",
                            fc->uuid);
        goto end;
      }else if(fileRid<0){
        assert(f->error.code);
        rc = f->error.code
          ? f->error.code
          : fsl_cx_err_set(f, FSL_RC_ERROR,
                           "Error resolving RID for F-card UUID [%s].",
                           fc->uuid);
        goto end;
      }
      fsl_md5_update_cstr(&md5, fc->name, -1);
      rc = fsl_content_get(f, fileRid, buf);
      if(rc){
        goto end;
      }
      numBuf[0] = 0;
      fsl_snprintf(numBuf, NumBufSize,
                   " %"FSL_SIZE_T_PFMT"\n",
                   buf->used);
      fsl_md5_update_cstr(&md5, numBuf, -1);
      fsl_md5_update_buffer(&md5, buf);
    }
    if(!rc){
      fsl_md5_final(&md5, digest);
      fsl_md5_digest_to_base16(digest, hex);
    }
    end:
    fsl__cx_content_buffer_yield(f);
    assert(0==buf->used);
    if(rc) return rc;
    fsl_deck_F_rewind(mf);
    theHash = hex;
  }
  assert(theHash);
  if(*tgt){
    memcpy(*tgt, theHash, FSL_STRLEN_MD5);
    (*tgt)[FSL_STRLEN_MD5] = 0;
    return 0;
  }else{
    char * x = fsl_strdup(theHash);
    if(x) *tgt = x;
    return x ? 0 : FSL_RC_OOM;
  }
}

int fsl_deck_R_calc(fsl_deck * const mf){
  char R[FSL_STRLEN_MD5+1] = {0};
  char * r = R;
  const int rc = fsl_deck_R_calc2(mf, &r);
  return rc ? rc : fsl_deck_R_set(mf, r);
}

int fsl_deck_T_add2( fsl_deck * const mf, fsl_card_T * t){
  if(!t) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf, 'T')){
    return mf->f->error.code;
  }else if(FSL_SATYPE_CONTROL==mf->type && NULL==t->uuid){
    return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                            "CONTROL artifacts may not have "
                            "self-referential tags.");
  }else if(FSL_SATYPE_TECHNOTE==mf->type){
    if(NULL!=t->uuid){
      return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                              "TECHNOTE artifacts may not have "
                              "tags which refer to other objects.");
    }else if(FSL_TAGTYPE_ADD!=t->type){
      return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                            "TECHNOTE artifacts may only have "
                            "ADD-type tags.");
    }
  }
  if(!t->name || !*t->name){
    return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                         "Tag name may not be empty.");
  }else if(fsl_validate16(t->name, fsl_strlen(t->name))){
    return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                         "Tag name may not be hexadecimal.");
  }else if(t->uuid && !fsl_is_uuid(t->uuid)){
    return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                         "Invalid UUID in tag.");
  }
  return fsl_list_append(&mf->T, t);
}

int fsl_deck_T_add( fsl_deck * const mf, fsl_tagtype_e tagType,
                    char const * uuid, char const * name,
                    char const * value){
  if(!name) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf, 'T')) return mf->f->error.code;
  else if(!*name || (uuid &&!fsl_is_uuid(uuid))) return FSL_RC_SYNTAX;
  else switch(tagType){
    case FSL_TAGTYPE_CANCEL:
    case FSL_TAGTYPE_ADD:
    case FSL_TAGTYPE_PROPAGATING:{
      int rc;
      fsl_card_T * t;
      t = fsl_card_T_malloc(tagType, uuid, name, value);
      if(!t) return FSL_RC_OOM;
      rc = fsl_deck_T_add2( mf, t );
      if(rc) fsl_card_T_free(t);
      return rc;
    }
    default:
      assert(!"Invalid tagType value");
      return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                            "Invalid tag-type value: %d",
                            (int)tagType);
  }
}

/**
   Returns true if the NUL-terminated string contains only
   "reasonable" branch name character, with the native assumption that
   anything <=32d is "unreasonable" and anything >=128 is part of a
   multibyte UTF8 character.
*/
static bool fsl_is_valid_branchname(char const * z_){
  unsigned char const * z = (unsigned char const*)z_;
  unsigned len = 0;
  for(; z[len]; ++len){
    if(z[len] <= 32) return false;
  }
  return len>0;
}

int fsl_deck_branch_set( fsl_deck * const d, char const * branchName ){
  if(!fsl_is_valid_branchname(branchName)){
    return fsl_cx_err_set(d->f, FSL_RC_RANGE, "Branch name contains "
                          "invalid characters.");
  }
  int rc= fsl_deck_T_add(d, FSL_TAGTYPE_PROPAGATING, NULL,
                         "branch", branchName);
  if(!rc){
    char * sym = fsl_mprintf("sym-%s", branchName);
    if(sym){
      rc = fsl_deck_T_add(d, FSL_TAGTYPE_PROPAGATING, NULL,
                          sym, NULL);
      fsl_free(sym);
    }else{
      rc = FSL_RC_OOM;
    }
  }
  return rc;
}

int fsl_deck_U_set( fsl_deck * const mf, char const * v){
  return fsl_deck_set_string( mf, 'U', &mf->U, v, -1 );
}

int fsl_deck_W_set( fsl_deck * const mf, char const * v, fsl_int_t n){
  return fsl_deck_b_setuffer_impl(mf, v, n, 'W', &mf->W);
}

int fsl_deck_A_set( fsl_deck * const mf, char const * name,
                    char const * tgt,
                    char const * uuidSrc ){
  int const uLen = (uuidSrc && *uuidSrc) ? fsl_is_uuid(uuidSrc) : 0;
  if(!name || !tgt) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf, 'A')) return mf->f->error.code;
  else if(!*tgt){
    return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                          "Invalid target name in A card.");
  }
  /* TODO: validate tgt based on mf->type and require UUID
     for types EVENT/TICKET.
  */
  else if(uuidSrc && *uuidSrc && !uLen){
    return fsl_cx_err_set(mf->f, FSL_RC_SYNTAX,
                          "Invalid source UUID in A card.");
  }
  else{
    int rc = 0;
    fsl_deck_free_string(mf, mf->A.tgt);
    fsl_deck_free_string(mf, mf->A.src);
    fsl_deck_free_string(mf, mf->A.name);
    mf->A.name = mf->A.src = NULL;
    if(! (mf->A.tgt = fsl_strdup(tgt))) rc = FSL_RC_OOM;
    else if( !(mf->A.name = fsl_strdup(name))) rc = FSL_RC_OOM;
    else if(uLen){
      mf->A.src = fsl_strndup(uuidSrc,uLen);
      if(!mf->A.src) rc = FSL_RC_OOM
        /* Leave mf->A.tgt/name for downstream cleanup. */;
    }
    return rc;
  }
}


int fsl_deck_D_set( fsl_deck * const mf, double date){
  if(date<0) return FSL_RC_RANGE;
  else if(date>0 && !fsl_deck_check_type(mf, 'D')){
    return mf->f->error.code;
  }else{
    mf->D = date;
    return 0;
  }
}

int fsl_deck_E_set( fsl_deck * const mf, double date, char const * uuid){
  int const uLen = uuid ? fsl_is_uuid(uuid) : 0;
  if(!mf || !uLen) return FSL_RC_MISUSE;
  else if(date<=0){
    return fsl_cx_err_set(mf->f, FSL_RC_RANGE,
                          "Invalid date value for E card.");
  }else if(!uLen){
    return fsl_cx_err_set(mf->f, FSL_RC_RANGE,
                          "Invalid UUID for E card.");
  }
  else{
    mf->E.julian = date;
    fsl_deck_free_string(mf, mf->E.uuid);
    mf->E.uuid = fsl_strndup(uuid, uLen);
    return mf->E.uuid ? 0 : FSL_RC_OOM;
  }
}

int fsl_deck_F_add( fsl_deck * const mf, char const * name,
                    char const * uuid,
                    fsl_fileperm_e perms, 
                    char const * oldName){
  int const uLen = uuid ? fsl_is_uuid(uuid) : 0;
  if(!mf || !name) return FSL_RC_MISUSE;
  else if(!uuid && !mf->B.uuid){
    return fsl_cx_err_set(mf->f, FSL_RC_MISUSE,
                         "NULL UUID is not valid for baseline "
                          "manifests.");
  }
  else if(!fsl_deck_check_type(mf, 'F')) return mf->f->error.code;
  else if(!*name){
    return fsl_cx_err_set(mf->f, FSL_RC_RANGE,
                         "F-card name may not be empty.");
  }
  else if(!fsl_is_simple_pathname(name, 1)
          || (oldName && !fsl_is_simple_pathname(oldName, 1))){
    return fsl_cx_err_set(mf->f, FSL_RC_RANGE,
                          "Invalid filename for F-card (simple form required): "
                          "name=[%s], oldName=[%s].", name, oldName);
  }
  else if(uuid && !uLen){
    return fsl_cx_err_set(mf->f, FSL_RC_RANGE,
                          "Invalid UUID for F-card.");
  }
  else {
    int rc = 0;
    fsl_card_F * t;
    switch(perms){
      case FSL_FILE_PERM_EXE:
      case FSL_FILE_PERM_LINK:
      case FSL_FILE_PERM_REGULAR:
        break;
      default:
        assert(!"Invalid fsl_fileperm_e value");
        return fsl_cx_err_set(mf->f, FSL_RC_RANGE,
                              "Invalid fsl_fileperm_e value "
                              "(%d) for file [%s].",
                              perms, name);
    }
    t = fsl_card_F_list_push(&mf->F);
    if(!t) return FSL_RC_OOM;
    assert(mf->F.used>1
           ? (FSL_CARD_F_LIST_NEEDS_SORT & mf->F.flags)
           : 1);
    assert(!t->name);
    assert(!t->uuid);
    assert(!t->priorName);
    assert(!t->deckOwnsStrings);
    t->perm = perms;
    if(0==(t->name = fsl_strdup(name))){
      rc = FSL_RC_OOM;
    }else if(uuid && 0==(t->uuid = fsl_strdup(uuid))){
      rc = FSL_RC_OOM;
    }else if(oldName && 0==(t->priorName = fsl_strdup(oldName))){
      rc = FSL_RC_OOM;
    }
    if(rc){
      fsl_card_F_list_pop(&mf->F);
    }
    return rc;
  }
}

int fsl_deck_F_foreach( fsl_deck * const d, fsl_card_F_visitor_f cb, void * const visitorState ){
  if(!cb) return FSL_RC_MISUSE;
  else{
    fsl_card_F const * fc;
    int rc = fsl_deck_F_rewind(d);
    while( !rc && !(rc=fsl_deck_F_next(d, &fc)) && fc) {
      rc = cb( fc, visitorState );
    }
    return (FSL_RC_BREAK==rc) ? 0 : rc;
  }
}

  
/**
    Output state for fsl_output_f_mf() and friends. Used for managing
    the output of a fsl_deck.
 */
struct fsl_deck_out_state {
  /**
      The set of cards being output. We use this to delegate certain
      output bits.
   */
  fsl_deck const * d;
  /**
      Output routine to send manifest to.
   */
  fsl_output_f out;
  /**
      State to pass as the first arg of this->out().
   */
  void * outState;

  /**
     The previously-visited card, for confirming that all cards are in
     proper lexical order.
   */
  fsl_card_F const * prevCard;
  /**
      f() result code, so that we can feed the code back through the
      fsl_appendf() layer. If this is non-0, processing must stop.  We
      "could" use this->error.code instead, but this is simple.
   */
  int rc;
  /**
      Counter for list-visiting routines. Must be re-set before each
      visit loop if the visitor makes use of this (most do not).
   */
  fsl_int_t counter;

  /**
      Incrementally-calculated MD5 sum of all output sent via
      fsl_output_f_mf().
   */
  fsl_md5_cx md5;

  /* Holds error state for propagating back to the client. */
  fsl_error error;

  /**
      Scratch buffer for fossilizing bytes and other temporary work.
      This value comes from fsl__cx_scratchpad().
  */
  fsl_buffer * scratch;
};
typedef struct fsl_deck_out_state fsl_deck_out_state;
static const fsl_deck_out_state fsl_deck_out_state_empty = {
NULL/*d*/,
NULL/*out*/,
NULL/*outState*/,
NULL/*prevCard*/,
0/*rc*/,
0/*counter*/,
fsl_md5_cx_empty_m/*md5*/,
fsl_error_empty_m/*error*/,
NULL/*scratch*/
};

/**
    fsl_output_f() impl which forwards its data to arg->out(). arg
    must be a (fsl_deck_out_state *). Updates arg->rc to the result of
    calling arg->out(fp->fState, data, n). If arg->out() succeeds then
    arg->md5 is updated to reflect the given data. i.e. this is where
    the Z-card gets calculated incrementally during output of a deck.
*/
static int fsl_output_f_mf( void * arg, void const * data,
                            fsl_size_t n ){
  fsl_deck_out_state * const os = (fsl_deck_out_state *)arg;
  if((n>0)
     && !(os->rc = os->out(os->outState, data, (fsl_size_t)n))
     && (os->md5.isInit)){
    fsl_md5_update( &os->md5, data, (fsl_size_t)n );
  }
  return os->rc;
}

/**
    Internal helper for fsl_deck_output(). Appends formatted output to
    os->out() via fsl_output_f_mf(). Returns os->rc (0 on success).
 */
static int fsl_deck_append( fsl_deck_out_state * const os,
                            char const * fmt, ... ){
  fsl_int_t rc;
  va_list args;
  assert(os);
  assert(fmt && *fmt);
  va_start(args,fmt);
  rc = fsl_appendfv( fsl_output_f_mf, os, fmt, args);
  va_end(args);
  if(rc<0 && !os->rc) os->rc = FSL_RC_IO;
  return os->rc;
}

/**
    Fossilizes (inp, inp+len] bytes to os->scratch,
    overwriting any existing contents.
    Updates and returns os->rc.
 */
static int fsl_deck_fossilize( fsl_deck_out_state * const os,
                               unsigned char const * inp,
                               fsl_int_t len){
  fsl_buffer_reuse(os->scratch);
  return os->rc = len
    ? fsl_bytes_fossilize(inp, len, os->scratch)
    : 0;
}

/** Confirms that the given card letter is valid for od->d->type, and
    updates os->rc and os->error if it's not. Returns true if it's
    valid.
*/
static bool fsl_deck_out_tcheck(fsl_deck_out_state * const os, char letter){
  if(!fsl_card_is_legal(os->d->type, letter)){
    os->rc = fsl_error_set(&os->error, FSL_RC_TYPE,
                           "%c-card is not valid for deck type %s.",
                           letter, fsl_satype_cstr(os->d->type));
  }
  return os->rc ? false : true;
}

/* Appends a UUID-valued card to os from os->d->{{card}} if the given
   UUID is not NULL, else this is a no-op. */
static int fsl_deck_out_uuid( fsl_deck_out_state * const os, char card, fsl_uuid_str uuid ){
  if(uuid && fsl_deck_out_tcheck(os, card)){
    if(!fsl_is_uuid(uuid)){
      os->rc = fsl_error_set(&os->error, FSL_RC_RANGE,
                             "Malformed UUID in %c card.", card);
    }else{
      fsl_deck_append(os, "%c %s\n", card, uuid);
    }
  }
  return os->rc;
}

/* Appends the B card to os from os->d->B. */
static int fsl_deck_out_B( fsl_deck_out_state * const os ){
  return fsl_deck_out_uuid(os, 'B', os->d->B.uuid);
}


/* Appends the A card to os from os->d->A. */
static int fsl_deck_out_A( fsl_deck_out_state * const os ){
  if(os->d->A.name && fsl_deck_out_tcheck(os, 'A')){
    if(!os->d->A.name || !*os->d->A.name){
      os->rc = fsl_error_set(&os->error, FSL_RC_SYNTAX,
                             "A-card is missing its name property");

    }else if(!os->d->A.tgt || !*os->d->A.tgt){
      os->rc = fsl_error_set(&os->error, FSL_RC_SYNTAX,
                             "A-card is missing its tgt property: %s",
                             os->d->A.name);

    }else if(os->d->A.src && !fsl_is_uuid(os->d->A.src)){
      os->rc = fsl_error_set(&os->error, FSL_RC_TYPE,
                             "Invalid src UUID in A-card: name=%s, "
                             "invalid uuid=%s",
                             os->d->A.name, os->d->A.src);
    }else{
      fsl_deck_append(os, "A %F %F",
                      os->d->A.name, os->d->A.tgt);
      if(!os->rc){
        if(os->d->A.src){
          fsl_deck_append(os, " %s", os->d->A.src);
        }
        if(!os->rc) fsl_deck_append(os, "\n");
      }
    }
  }
  return os->rc;
}

/**
    Internal helper for outputing cards which are simple strings.
    str is the card to output (NULL values are ignored), letter is
    the card letter being output. If doFossilize is true then
    the output gets fossilize-formatted.
 */
static int fsl_deck_out_letter_str( fsl_deck_out_state * const os, char letter,
                                    char const * str, char doFossilize ){
  if(str && fsl_deck_out_tcheck(os, letter)){
    if(doFossilize){
      fsl_deck_fossilize(os, (unsigned char const *)str, -1);
      if(!os->rc){
        fsl_deck_append(os, "%c %b\n", letter, os->scratch);
      }
    }else{
      fsl_deck_append(os, "%c %s\n", letter, str);
    }
  }
  return os->rc;
}

/* Appends the C card to os from os->d->C. */
static int fsl_deck_out_C( fsl_deck_out_state * const os ){
  return fsl_deck_out_letter_str( os, 'C', os->d->C, 1 );
}


/* Appends the D card to os from os->d->D. */
static int fsl_deck_out_D( fsl_deck_out_state * const os ){
  if((os->d->D > 0.0) && fsl_deck_out_tcheck(os, 'D')){
    char ds[24];
    if(!fsl_julian_to_iso8601(os->d->D, ds, 1)){
      os->rc = fsl_error_set(&os->error, FSL_RC_RANGE,
                             "D-card contains invalid "
                             "Julian Day value.");
    }else{
      fsl_deck_append(os, "D %s\n", ds);
    }
  }
  return os->rc;
}

/* Appends the E card to os from os->d->E. */
static int fsl_deck_out_E( fsl_deck_out_state * const os ){
  if(os->d->E.uuid && fsl_deck_out_tcheck(os, 'E')){
    char ds[24];
    char msPrecision = FSL_SATYPE_EVENT!=os->d->type
      /* The timestamps on Events historically have seconds precision,
         not ms.
      */;
    if(!fsl_is_uuid(os->d->E.uuid)){
      os->rc = fsl_error_set(&os->error, FSL_RC_TYPE,
                             "Invalid UUID in E-card: %s",
                             os->d->E.uuid);
    }
    else if(!fsl_julian_to_iso8601(os->d->E.julian, ds, msPrecision)){
      os->rc = fsl_error_set(&os->error, FSL_RC_TYPE,
                             "Invalid Julian Day value in E-card.");
    }
    else{
      fsl_deck_append(os, "E %s %s\n", ds, os->d->E.uuid);
    }
  }
  return os->rc;
}

/* Appends the G card to os from os->d->G. */
static int fsl_deck_out_G( fsl_deck_out_state * const os ){
  return fsl_deck_out_uuid(os, 'G', os->d->G);
}

/* Appends the H card to os from os->d->H. */
static int fsl_deck_out_H( fsl_deck_out_state * const os ){
  if(os->d->H && os->d->I){
    return os->rc = fsl_error_set(&os->error, FSL_RC_SYNTAX,
                                  "Forum post may not have both H- and I-cards.");
  }
  return fsl_deck_out_letter_str( os, 'H', os->d->H, 1 );
}

/* Appends the I card to os from os->d->I. */
static int fsl_deck_out_I( fsl_deck_out_state * const os ){
  if(os->d->I && os->d->H){
    return os->rc = fsl_error_set(&os->error, FSL_RC_SYNTAX,
                                  "Forum post may not have both H- and I-cards.");
  }
  return fsl_deck_out_uuid(os, 'I', os->d->I);
}


static int fsl_deck_out_F_one(fsl_deck_out_state *os,
                              fsl_card_F const * f){
  int rc;
  char hasOldName;
  char const * zPerm;
  assert(f);
  if(os->prevCard){
    int const cmp = fsl_strcmp(os->prevCard->name, f->name);
    if(0==cmp){
      return fsl_error_set(&os->error, FSL_RC_RANGE,
                           "Duplicate F-card name: %s",
                           f->name);
    }else if(cmp>0){
      return fsl_error_set(&os->error, FSL_RC_RANGE,
                           "Out-of-order F-card names: %s before %s",
                           os->prevCard->name, f->name);
    }
  }
  if(!fsl_is_simple_pathname(f->name, true)){
    return fsl_error_set(&os->error, FSL_RC_RANGE,
                         "Filename is invalid as F-card: %s",
                         f->name);
  }
  if(!f->uuid && !os->d->B.uuid){
    return fsl_error_set(&os->error, FSL_RC_MISUSE,
                         "Baseline manifests may not have F-cards "
                         "without UUIDs (file deletion entries). To "
                         "delete files, simply do not inject an F-card "
                         "for them. Delta manifests, however, require "
                         "NULL UUIDs for deletion entries! File: %s",
                         f->name);
  }

  rc = fsl_deck_fossilize(os, (unsigned char const *)f->name, -1);
  if(!rc) rc = fsl_deck_append(os, "F %b", os->scratch);
  if(!rc && f->uuid){
    assert(fsl_is_uuid(f->uuid));
    rc = fsl_deck_append( os, " %s", f->uuid);
    if(rc) return rc;
  }
  if(f->uuid){
    hasOldName = f->priorName && (0!=fsl_strcmp(f->name,f->priorName));
    switch(f->perm){
      case FSL_FILE_PERM_EXE: zPerm = " x"; break;
      case FSL_FILE_PERM_LINK: zPerm = " l"; break;
      default:
        /* When hasOldName, we have to inject an otherwise optional
           'w' to avoid an ambiguity. Or at least that's what the
           fossil F-card-generating code does.
        */
        zPerm = hasOldName ? " w" : ""; break;
    }
    if(*zPerm) rc = fsl_deck_append( os, "%s", zPerm);
    if(!rc && hasOldName){
      assert(*zPerm);
      rc = fsl_deck_fossilize(os, (unsigned char const *)f->priorName, -1);
      if(!rc) rc = fsl_deck_append( os, " %b", os->scratch);
    }
  }
  if(!rc) fsl_output_f_mf(os, "\n", 1);
  return os->rc;
}

static int fsl_deck_out_list_obj( fsl_deck_out_state * const os,
                                  char letter,
                                  fsl_list const * li,
                                  fsl_list_visitor_f visitor){
  if(li->used && fsl_deck_out_tcheck(os, letter)){
    os->rc = fsl_list_visit( li, 0, visitor, os );
  }
  return os->rc;
}

static int fsl_deck_out_F( fsl_deck_out_state * const os ){
  if(os->d->F.used && fsl_deck_out_tcheck(os, 'F')){
    uint32_t i;
    for(i=0; !os->rc && i <os->d->F.used; ++i){
      os->rc = fsl_deck_out_F_one(os, F_at(&os->d->F, i));
    }
  }
  return os->rc;
}


/**
    A comparison routine for qsort(3) which compares fsl_card_J
    instances in a lexical manner based on their names.  The order is
    important for card ordering in generated manifests.
 */
int fsl__qsort_cmp_J_cards( void const * lhs, void const * rhs ){
  fsl_card_J const * l = *((fsl_card_J const **)lhs);
  fsl_card_J const * r = *((fsl_card_J const **)rhs);
  /* Compare NULL as larger so that NULLs move to the right. That said,
     we aren't expecting any NULLs. */
  assert(l);
  assert(r);
  if(!l) return r ? 1 : 0;
  else if(!r) return -1;
  else{
    /* The '+' sorts before any legal field name bits (letters). */
    if(l->append != r->append) return r->append - l->append
      /* Footnote: that will break if, e.g. l->isAppend==2 and
         r->isAppend=1, or some such. Shame C89 doesn't have a true
         boolean.
      */;
    else return fsl_strcmp(l->field, r->field);
  }
}

/**
    fsl_list_visitor_f() impl for outputing J cards. obj must
    be a (fsl_card_J *).
 */
static int fsl_list_v_mf_output_card_J(void * obj, void * visitorState ){
  fsl_deck_out_state * const os = (fsl_deck_out_state *)visitorState;
  fsl_card_J const * c = (fsl_card_J const *)obj;
  fsl_deck_fossilize( os, (unsigned char const *)c->field, -1 );
  if(!os->rc){
    fsl_deck_append(os, "J %s%b", c->append ? "+" : "", os->scratch);
    if(!os->rc){
      if(c->value && *c->value){
        fsl_deck_fossilize( os, (unsigned char const *)c->value, -1 );
        if(!os->rc){
          fsl_deck_append(os, " %b\n", os->scratch);
        }
      }else{
        fsl_deck_append(os, "\n");
      }
    }
  }
  return os->rc;
}

static int fsl_deck_out_J( fsl_deck_out_state * const os ){
  return fsl_deck_out_list_obj(os, 'J', &os->d->J,
                               fsl_list_v_mf_output_card_J);
}

/* Appends the K card to os from os->d->K. */
static int fsl_deck_out_K( fsl_deck_out_state * const os ){
  if(os->d->K && fsl_deck_out_tcheck(os, 'K')){
    if(!fsl_is_uuid(os->d->K)){
      os->rc = fsl_error_set(&os->error, FSL_RC_RANGE,
                             "Invalid UUID in K card.");
    }
    else{
      fsl_deck_append(os, "K %s\n", os->d->K);
    }
  }
  return os->rc;
}


/* Appends the L card to os from os->d->L. */
static int fsl_deck_out_L( fsl_deck_out_state * const os ){
  return fsl_deck_out_letter_str(os, 'L', os->d->L, 1);
}

/* Appends the N card to os from os->d->N. */
static int fsl_deck_out_N( fsl_deck_out_state * const os ){
  return fsl_deck_out_letter_str( os, 'N', os->d->N, 1 );
}

/**
    fsl_list_visitor_f() impl for outputing P cards. obj must
    be a (fsl_deck_out_state *) and obj->counter must be
    set to 0 before running the visit iteration.
 */
static int fsl_list_v_mf_output_card_P(void * obj, void * visitorState ){
  fsl_deck_out_state * const os = (fsl_deck_out_state *)visitorState;
  char const * uuid = (char const *)obj;
  int const uLen = uuid ? fsl_is_uuid(uuid) : 0;
  if(!uLen){
    os->rc = fsl_error_set(&os->error, FSL_RC_RANGE,
                           "Invalid UUID in P card.");
  }
  else if(!os->counter++) fsl_output_f_mf(os, "P ", 2);
  else fsl_output_f_mf(os, " ", 1);
  /* Reminder: fsl_appendf_f_mf() updates os->rc. */
  if(!os->rc){
    fsl_output_f_mf(os, uuid, (fsl_size_t)uLen);
  }
  return os->rc;
}


static int fsl_deck_out_P( fsl_deck_out_state * const os ){
  if(!fsl_deck_out_tcheck(os, 'P')) return os->rc;
  else if(os->d->P.used){
    os->counter = 0;
    os->rc = fsl_list_visit( &os->d->P, 0, fsl_list_v_mf_output_card_P, os );
    assert(os->counter);
    if(!os->rc) fsl_output_f_mf(os, "\n", 1);
  }
#if 1
  /* Arguable: empty P-cards are harmless but cosmetically unsightly. */
  else if(FSL_SATYPE_CHECKIN==os->d->type){
    /*
      Evil ugly hack, primarily for round-trip compatibility with
      manifest #1, which has an empty P card.

      fossil(1) ignores empty P-cards in all cases, and must continue
      to do so for backwards compatibility with rid #1 in all repos.

      Pedantic note: there must be no space between the 'P' and the
      newline.
    */
    fsl_deck_append(os, "P\n");
  }
#endif
  return os->rc;
}

/**
    A comparison routine for qsort(3) which compares fsl_card_Q
    instances in a lexical manner. The order is important for card
    ordering in generated manifests.
*/
static int qsort_cmp_Q_cards( void const * lhs, void const * rhs ){
  fsl_card_Q const * l = *((fsl_card_Q const **)lhs);
  fsl_card_Q const * r = *((fsl_card_Q const **)rhs);
  /* Compare NULL as larger so that NULLs move to the right. That said,
     we aren't expecting any NULLs. */
  assert(l);
  assert(r);
  if(!l) return r ? 1 : 0;
  else if(!r) return -1;
  else{
    /* Lexical sorting must account for the +/- characters, and a '+'
       sorts before '-', which is why this next part may seem
       backwards at first. */
    assert(l->type);
    assert(r->type);
    if(l->type<0 && r->type>0) return 1;
    else if(l->type>0 && r->type<0) return -1;
    else return fsl_strcmp(l->target, r->target);
  }
}

/**
    fsl_list_visitor_f() impl for outputing Q cards. obj must
    be a (fsl_deck_out_state *).
*/
static int fsl_list_v_mf_output_card_Q(void * obj, void * visitorState ){
  fsl_deck_out_state * const os = (fsl_deck_out_state *)visitorState;
  fsl_card_Q const * cp = (fsl_card_Q const *)obj;
  char const prefix = (cp->type==FSL_CHERRYPICK_ADD)
    ? '+' : '-';
  assert(cp->type);
  assert(cp->target);
  if(cp->type != FSL_CHERRYPICK_ADD &&
     cp->type != FSL_CHERRYPICK_BACKOUT){
    os->rc = fsl_error_set(&os->error, FSL_RC_RANGE,
                           "Invalid type value in Q-card.");
  }else if(!fsl_card_is_legal(os->d->type, 'Q')){
    os->rc = fsl_error_set(&os->error, FSL_RC_TYPE,
                           "Q-card is not valid for deck type %s",
                           fsl_satype_cstr(os->d->type));
  }else if(!fsl_is_uuid(cp->target)){
    os->rc = fsl_error_set(&os->error, FSL_RC_RANGE,
                           "Invalid target UUID in Q-card: %s",
                           cp->target);
  }else if(cp->baseline){
    if(!fsl_is_uuid(cp->baseline)){
      os->rc = fsl_error_set(&os->error, FSL_RC_RANGE,
                             "Invalid baseline UUID in Q-card: %s",
                             cp->baseline);
    }else{
      fsl_deck_append(os, "Q %c%s %s\n", prefix, cp->target, cp->baseline);
    }
  }else{
    fsl_deck_append(os, "Q %c%s\n", prefix, cp->target);
  }
  return os->rc;
}

static int fsl_deck_out_Q( fsl_deck_out_state * const os ){
  return fsl_deck_out_list_obj(os, 'Q', &os->d->Q,
                               fsl_list_v_mf_output_card_Q);
}

/**
    Appends the R card from os->d->R to os.
 */
static int fsl_deck_out_R( fsl_deck_out_state * const os ){
  if(os->d->R && fsl_deck_out_tcheck(os, 'R')){
    if((FSL_STRLEN_MD5!=fsl_strlen(os->d->R))
            || !fsl_validate16(os->d->R, FSL_STRLEN_MD5)){
      os->rc = fsl_error_set(&os->error, FSL_RC_RANGE,
                             "Malformed MD5 in R-card.");
    }
    else{
      fsl_deck_append(os, "R %s\n", os->d->R);
    }
  }
  return os->rc;
}

/**
    fsl_list_visitor_f() impl for outputing T cards. obj must
    be a (fsl_deck_out_state *).
 */
static int fsl_list_v_mf_output_card_T(void * obj, void * visitorState ){
  fsl_deck_out_state * const os = (fsl_deck_out_state *)visitorState;
  fsl_card_T * t = (fsl_card_T *)obj;
  char prefix = 0;
  switch(os->d->type){
    case FSL_SATYPE_TECHNOTE:
      if( t->uuid ){
        return os->rc = fsl_error_set(&os->error, FSL_RC_SYNTAX,
                                      "Non-self-referential T-card is not "
                                      "permitted in a technote.");
      }else if(FSL_TAGTYPE_ADD!=t->type){
        return os->rc = fsl_error_set(&os->error, FSL_RC_SYNTAX,
                                      "Non-ADD T-card is not permitted "
                                      "in a technote.");
      }
      break;
    case FSL_SATYPE_CONTROL:
      if( !t->uuid ){
        return os->rc = fsl_error_set(&os->error, FSL_RC_SYNTAX,
                                      "Self-referential T-card is not "
                                      "permitted in a control artifact.");
      }
      break;
    default:
      break;
  }
  /* Determine the prefix character... */
  switch(t->type){
    case FSL_TAGTYPE_CANCEL: prefix = '-'; break;
    case FSL_TAGTYPE_ADD: prefix = '+'; break;
    case FSL_TAGTYPE_PROPAGATING: prefix = '*'; break;
    default:
      return os->rc = fsl_error_set(&os->error, FSL_RC_TYPE,
                                    "Invalid tag type #%d in T-card.",
                                    t->type);
  }
  if(!t->name || !*t->name){
    return os->rc = fsl_error_set(&os->error, FSL_RC_SYNTAX,
                                  "T-card name may not be empty.");
  }else if(fsl_validate16(t->name, fsl_strlen(t->name))){
    return os->rc = fsl_error_set(&os->error, FSL_RC_SYNTAX,
                                  "T-card name may not be hexadecimal.");
  }else if(t->uuid && !fsl_is_uuid(t->uuid)){
      return os->rc = fsl_error_set(&os->error, FSL_RC_SYNTAX,
                                    "Malformed UUID in T-card: %s",
                                    t->uuid);
  }
  /*
     Fossilize and output the prefix, name, and uuid, or a '*' if no
     uuid is set (which is only legal when tagging the current
     artifact, as '*' is a placeholder for the current artifact's
     UUID, which is not yet known).
  */
  fsl_buffer_reuse(os->scratch);
  fsl_deck_fossilize(os, (unsigned const char *)t->name, -1);
  if(os->rc) return os->rc;
  os->rc = fsl_deck_append(os, "T %c%s %s", prefix,
                           (char const*)os->scratch->mem,
                           t->uuid ? t->uuid : "*");
  if(os->rc) return os->rc;
  if(/*(t->type != FSL_TAGTYPE_CANCEL) &&*/t->value && *t->value){
    /* CANCEL tags historically don't store a value but
       the spec doesn't disallow it and they are harmless
       for (aren't used by) fossil(1). */
    fsl_deck_fossilize(os, (unsigned char const *)t->value, -1);
    if(!os->rc) fsl_output_f_mf(os, " ", 1);
    if(!os->rc) fsl_output_f_mf(os, (char const*)os->scratch->mem,
                                (fsl_int_t)os->scratch->used);
  }
  if(!os->rc){
    fsl_output_f_mf(os, "\n", 1);
  }
  return os->rc;
}


char fsl_tag_prefix_char( fsl_tagtype_e t ){
  switch(t){
    case FSL_TAGTYPE_CANCEL: return '-';
    case FSL_TAGTYPE_ADD: return '+';
    case FSL_TAGTYPE_PROPAGATING: return '*';
    default:
      return 0;
  }
}

/**
    A comparison routine for qsort(3) which compares fsl_card_T
    instances in a lexical manner based on (type, name, uuid, value).
    The order of those is important for card ordering in generated
    manifests. Interestingly, CANCEL tags (with a '-' prefix) sort
    last, meaning it is possible to cancel a tag set in the same
    manifest because crosslinking processes them in the order given
    (which will be lexical order for all legal manifests).

    Reminder: lhs and rhs must be (fsl_card_T**), as we use this to
    qsort() such lists. When using it to compare two tags, make sure
    to pass ptr-to-ptr.
*/
static int fsl_card_T_cmp( void const * lhs, void const * rhs ){
  fsl_card_T const * l = *((fsl_card_T const **)lhs);
  fsl_card_T const * r = *((fsl_card_T const **)rhs);
  /* Compare NULL as larger so that NULLs move to the right. That said,
     we aren't expecting any NULLs. */
  assert(l);
  assert(r);
  if(!l) return r ? 1 : 0;
  else if(!r) return -1;
  else if(l->type != r->type){
    char const lc = fsl_tag_prefix_char(l->type);
    char const rc = fsl_tag_prefix_char(r->type);
    return (lc<rc) ? -1 : 1;
  }else{
    int rc = fsl_strcmp(l->name, r->name);
    if(rc) return rc;
    else {
      rc = fsl_uuidcmp(l->uuid, r->uuid);
      return rc
        ? rc
        : fsl_strcmp(l->value, r->value);
    }
  }
}

/**
   Confirms that any T-cards in d are properly sorted. If not,
   returns non-0. If err is not NULL, it is updated with a
   description of the problem.

   Possibly fixme one day: this code permits that the same tag/target
   combination may be added or removed, or added as a normal and
   propagating tag, in the same deck. Though that's not technically
   disallowed, we "should" disallow it. That requires a more thorough
   scan of the cards, though.
*/
static int fsl_deck_T_verify_order( fsl_deck const * d, fsl_error * err ){
  if(d->T.used<2) return 0;
  else{
    fsl_size_t i = 0, j;
    int rc = 0;
    fsl_card_T const * tag;
    fsl_card_T const * prev = NULL;
    for( i = 0; i < d->T.used; ++i, prev = tag, rc = 0){
      tag = (fsl_card_T const *)d->T.list[i];
      if(prev){
        if( (rc = fsl_card_T_cmp(&prev, &tag)) >= 0 ){
          if(!err) rc = FSL_RC_SYNTAX;
          else{
            rc = rc
              ? fsl_error_set(err, FSL_RC_SYNTAX,
                              "Invalid T-card order: "
                              "[%c%s] must precede [%c%s]",
                              fsl_tag_prefix_char(prev->type),
                              prev->name,
                              fsl_tag_prefix_char(tag->type),
                              tag->name)
              : fsl_error_set(err, FSL_RC_SYNTAX,
                              "Duplicate T-card: %c%s",
                              fsl_tag_prefix_char(prev->type),
                              prev->name)
              ;
          }
          break;
        }
      }
    }
    /**
       And now, for bonus points: disallow the same tag name/artifact
       combination appearing twice in the deck. Though that's not
       explicitly disallowed by the fossil specs, we "should" disallow
       it. That requires a more thorough scan of the cards, though.

       This logic is NOT in fossil, and though we don't have any such
       tags in the fossil repo, we may have to disable this for
       compatibility's sake. OTOH, we only check this when outputing
       manifests, and we never (aside from testing) have to output
       manifests which were generated by fossil. Thus... this only
       triggers (except for some tests) on manifests generated by
       libfossil, so we can justify having it.
    */
    for( i=0; !rc && i < d->T.used; ++i ){
      fsl_card_T const * t1 = (fsl_card_T const *)d->T.list[i];
      for( j = 0; j < d->T.used; ++j ){
        if(i==j) continue;
        fsl_card_T const * t2 = (fsl_card_T const *)d->T.list[j];
        if(0==fsl_strcmp(t1->name, t2->name)
           && ((!t1->uuid && !t2->uuid)
               || 0==fsl_strcmp(t1->uuid, t2->uuid))){
              rc = fsl_error_set(err, FSL_RC_SYNTAX,
                                 "An artifact may not contain the same "
                                 "T-card name and target artifact "
                                 "multiple times: "
                                 "name=%s target=%s",
                                 t1->name, t1->uuid ? t1->uuid : "*");
              break;
        }
      }
    }
    return rc;
  }
}

/* Appends the T cards to os from os->d->T. */
static int fsl_deck_out_T( fsl_deck_out_state * const os ){
  os->rc = fsl_deck_T_verify_order( os->d, &os->error);
  return os->rc
    ? os->rc
    : fsl_deck_out_list_obj(os, 'T', &os->d->T,
                            fsl_list_v_mf_output_card_T);
}

/* Appends the U card to os from os->d->U. */
static int fsl_deck_out_U( fsl_deck_out_state * const os ){
  return fsl_deck_out_letter_str(os, 'U', os->d->U, 1);
}

/* Appends the W card to os from os->d->W. */
static int fsl_deck_out_W( fsl_deck_out_state * const os ){
  if(os->d->W.used && fsl_deck_out_tcheck(os, 'W')){
    fsl_deck_append(os, "W %"FSL_SIZE_T_PFMT"\n%b\n",
                    (fsl_size_t)os->d->W.used,
                    &os->d->W );
  }
  return os->rc;
}


/* Appends the Z card to os from os' accummulated md5 hash. */
static int fsl_deck_out_Z( fsl_deck_out_state * const os ){
  unsigned char digest[16];
  char md5[FSL_STRLEN_MD5+1];
  fsl_md5_final(&os->md5, digest);
  fsl_md5_digest_to_base16(digest, md5);
  assert(!md5[32]);
  os->md5.isInit = 0 /* Keep further output from updating the MD5 */;
  return fsl_deck_append(os, "Z %.*s\n", FSL_STRLEN_MD5, md5);
}

static int qsort_cmp_strings( void const * lhs, void const * rhs ){
  char const * l = *((char const **)lhs);
  char const * r = *((char const **)rhs);
  return fsl_strcmp(l,r);
}

static int fsl_list_v_mf_output_card_M(void * obj, void * visitorState ){
  fsl_deck_out_state * const os = (fsl_deck_out_state *)visitorState;
  char const * m = (char const *)obj;
  return fsl_deck_append(os, "M %s\n", m);
}

static int fsl_deck_output_cluster( fsl_deck_out_state * const os ){
  if(!os->d->M.used){
    os->rc = fsl_error_set(&os->error, FSL_RC_RANGE,
                           "M-card list may not be empty.");
  }else{
    fsl_deck_out_list_obj(os, 'M', &os->d->M,
                          fsl_list_v_mf_output_card_M);
  }
  return os->rc;
}


/* Helper for fsl_deck_output_CATYPE() */
#define DOUT(LETTER) rc = fsl_deck_out_##LETTER(os); \
  if(rc || os->rc) return os->rc ? os->rc : rc

static int fsl_deck_output_control( fsl_deck_out_state * const os ){
  int rc;
  /* Reminder: cards must be output in strict lexical order. */
  DOUT(D);
  DOUT(T);
  DOUT(U);
  return os->rc;
}

static int fsl_deck_output_event( fsl_deck_out_state * const os ){
  int rc = 0;
  /* Reminder: cards must be output in strict lexical order. */
  DOUT(C);
  DOUT(D);
  DOUT(E);
  DOUT(N);
  DOUT(P);
  DOUT(T);
  DOUT(U);
  DOUT(W);
  return os->rc;
}

static int fsl_deck_output_mf( fsl_deck_out_state * const os ){
  int rc = 0;
  /* Reminder: cards must be output in strict lexical order. */
  DOUT(B);
  DOUT(C);
  DOUT(D);
  DOUT(F);
  DOUT(K);
  DOUT(L);
  DOUT(N);
  DOUT(P);
  DOUT(Q);
  DOUT(R);
  DOUT(T);
  DOUT(U);
  DOUT(W);
  return os->rc;
}


static int fsl_deck_output_ticket( fsl_deck_out_state * const os ){
  int rc;
  /* Reminder: cards must be output in strict lexical order. */
  DOUT(D);
  DOUT(J);
  DOUT(K);
  DOUT(U);
  return os->rc;
}

static int fsl_deck_output_wiki( fsl_deck_out_state * const os ){
  int rc;
  /* Reminder: cards must be output in strict lexical order. */
  DOUT(C);
  DOUT(D);
  DOUT(L);
  DOUT(N);
  DOUT(P);
  DOUT(U);
  DOUT(W);
  return os->rc;
}

static int fsl_deck_output_attachment( fsl_deck_out_state * const os ){
  int rc = 0;
  /* Reminder: cards must be output in strict lexical order. */
  DOUT(A);
  DOUT(C);
  DOUT(D);
  DOUT(N);
  DOUT(U);
  return os->rc;
}

static int fsl_deck_output_forumpost( fsl_deck_out_state * const os ){
  int rc;
  /* Reminder: cards must be output in strict lexical order. */
  DOUT(D);
  DOUT(G);
  DOUT(H);
  DOUT(I);
  DOUT(N);
  DOUT(P);
  DOUT(U);
  DOUT(W);
  return os->rc;
}

/**
    Only for testing/debugging purposes, as it allows constructs which
    are not semantically legal and are CERTAINLY not legal to stuff in
    the database.
 */
static int fsl_deck_output_any( fsl_deck_out_state * const os ){
  int rc = 0;
  /* Reminder: cards must be output in strict lexical order. */
  DOUT(B);
  DOUT(C);
  DOUT(D);
  DOUT(E);
  DOUT(F);
  DOUT(J);
  DOUT(K);
  DOUT(L);
  DOUT(N);
  DOUT(P);
  DOUT(Q);
  DOUT(R);
  DOUT(T);
  DOUT(U);
  DOUT(W);
  return os->rc;
}

#undef DOUT


int fsl_deck_unshuffle( fsl_deck * const d, bool calculateRCard ){
  fsl_list * li;
  int rc = 0;
  if(!d || !d->f) return FSL_RC_MISUSE;
  fsl_cx_err_reset(d->f);
#define SORT(CARD,CMP) li = &d->CARD; fsl_list_sort(li, CMP)
  SORT(J,fsl__qsort_cmp_J_cards);
  SORT(M,qsort_cmp_strings);
  SORT(Q,qsort_cmp_Q_cards);
  SORT(T,fsl_card_T_cmp);
#undef SORT
  if(FSL_SATYPE_CHECKIN!=d->type){
    assert(!fsl_card_is_legal(d->type,'R'));
    assert(!fsl_card_is_legal(d->type,'F'));
  }else{
    assert(fsl_card_is_legal(d->type, 'R') && "in-lib unit testing");
    if(calculateRCard){
      rc = fsl_deck_R_calc(d) /* F-card list is sorted there */;
    }else{
      fsl_deck_F_sort(d);
      if(!d->R){
        rc = fsl_deck_R_set(d,
                            (d->F.used || d->B.uuid || d->P.used)
                            ? NULL
                            : FSL_MD5_INITIAL_HASH)
          /* Special case: for manifests with no (B,F,P)-cards we inject
             the initial-state R-card, analog to the initial checkin
             (RID 1). We need one of (B,F,P,R) to unambiguously identify
             a MANIFEST from a CONTROL, but RID 1 has an empty P-card,
             no F-cards, and no B-card, so it _needs_ an R-card in order
             to be unambiguously a Manifest. That said, that ambiguity
             is/would be harmless in practice because CONTROLs go
             through most of the same crosslinking processes as
             MANIFESTs (the ones which are important for this purpose,
             anyway).
          */;
      }
    }
  }
  return rc;
}

int fsl_deck_output( fsl_deck * const d, fsl_output_f out,
                     void * outputState ){
  static const bool allowTypeAny = false
    /* Only enable for debugging/testing. Allows outputing decks of
       type FSL_SATYPE_ANY, which bypasses some validation checks and
       may trigger other validation assertions. And may allow you to
       inject garbage into the repo. So be careful.
    */;

  fsl_deck_out_state OS = fsl_deck_out_state_empty;
  fsl_deck_out_state * const os = &OS;
  fsl_cx * const f = d->f;
  int rc = 0;
  if(NULL==out && NULL==outputState && f){
    out = f->output.out;
    outputState = f->output.state;
  }
  if(!f || !out) return FSL_RC_MISUSE;
  else if(FSL_SATYPE_ANY==d->type){
    if(!allowTypeAny){
      return fsl_cx_err_set(d->f, FSL_RC_TYPE,
                            "Artifact type ANY cannot be"
                            "output unless it is enabled in this "
                            "code (it's dangerous).");
    }
    /* fall through ... */
  }
  rc = fsl_deck_unshuffle(d,
                          (FSL_CX_F_CALC_R_CARD & f->flags)
                          ? ((d->F.used && !d->R) ? 1 : 0)
                          : 0);
  /* ^^^^ unshuffling might install an R-card, so we have to
     do that before checking whether all required cards are
     set... */  
  if(rc) return rc;
  else if(!fsl_deck_has_required_cards(d)){
    return FSL_RC_SYNTAX;
  }

  os->d = d;
  os->out = out;
  os->outState = outputState;
  os->scratch = fsl__cx_scratchpad(f);
  switch(d->type){
    case FSL_SATYPE_CLUSTER:
      rc = fsl_deck_output_cluster(os);
      break;
    case FSL_SATYPE_CONTROL:
      rc = fsl_deck_output_control(os);
      break;
    case FSL_SATYPE_EVENT:
      rc = fsl_deck_output_event(os);
      break;
    case FSL_SATYPE_CHECKIN:
      rc = fsl_deck_output_mf(os);
      break;
    case FSL_SATYPE_TICKET:
      rc = fsl_deck_output_ticket(os);
      break;
    case FSL_SATYPE_WIKI:
      rc = fsl_deck_output_wiki(os);
      break;
    case FSL_SATYPE_ANY:
      assert(allowTypeAny);
      rc = fsl_deck_output_any(os);
      break;
    case FSL_SATYPE_ATTACHMENT:
      rc = fsl_deck_output_attachment(os);
      break;
    case FSL_SATYPE_FORUMPOST:
      rc = fsl_deck_output_forumpost(os);
      break;
    default:
      rc = fsl_cx_err_set(f, FSL_RC_TYPE,
                          "Invalid/unhandled deck type (#%d).",
                          d->type);
      goto end;
  }
  if(!rc){
    rc = fsl_deck_out_Z( os );
  }
  end:
  fsl__cx_scratchpad_yield(f, os->scratch);
  if(os->rc && os->error.code){
    fsl_error_move(&os->error, &f->error);
  }
  fsl_error_clear(&os->error);
  return os->rc ? os->rc : rc;
}

/* Timestamps might be adjusted slightly to ensure that checkins appear
   on the timeline in chronological order.  This is the maximum amount
   of the adjustment window, in days.
*/
#define AGE_FUDGE_WINDOW      (2.0/86400.0)       /* 2 seconds */

/* This is increment (in days) by which timestamps are adjusted for
   use on the timeline.
*/
#define AGE_ADJUST_INCREMENT  (25.0/86400000.0)   /* 25 milliseconds */

/**
   Adds a record in the pending_xlink temp table, to be processed
   when crosslinking is completed. Returns 0 on success, non-0 for
   db error.
*/
static int fsl__deck_crosslink_add_pending(fsl_cx * f, char cType, fsl_uuid_cstr uuid){
  assert(f->cache.isCrosslinking);
  return fsl_cx_exec(f,
                   "INSERT OR IGNORE INTO pending_xlink VALUES('%c%q')",
                   cType, uuid);
}

/** @internal
   
    Add a single entry to the mlink table.  Also add the filename to
    the filename table if it is not there already.
   
    Parameters:
   
    pmid: Record for parent manifest. Use 0 to indicate no parent.

    zFromUuid: UUID for the content in parent (the new ==mlink.pid). 0
    or "" to add file.

    mid: The record ID of the manifest
   
    zToUuid:UUID for the mlink.fid. "" to delete
   
    zFilename: Filename
   
    zPrior: Previous filename. NULL if unchanged 
   
    isPublic:True if mid is not a private manifest
   
    isPrimary: true if pmid is the primary parent of mid.

    mperm: permissions
 */
static
int fsl_mlink_add_one( fsl_cx * f,
                       fsl_id_t pmid, fsl_uuid_cstr zFromUuid,
                       fsl_id_t mid, fsl_uuid_cstr zToUuid,
                       char const * zFilename,
                       char const * zPrior,
                       bool isPublic,
                       bool isPrimary,
                       fsl_fileperm_e mperm){
  fsl_id_t fnid, pfnid, pid, fid;
  fsl_db * db = fsl_cx_db_repo(f);
  fsl_stmt * s1 = NULL;
  int rc;
  bool doInsert = false;
  assert(f);
  assert(db);
  assert(db->beginCount>0);
  //MARKER(("%s() pmid=%d mid=%d\n", __func__, (int)pmid, (int)mid));

  rc = fsl__repo_filename_fnid2(f, zFilename, &fnid, 1);
  if(rc) return rc;
  if( zPrior && *zPrior ){
    rc = fsl__repo_filename_fnid2(f, zPrior, &pfnid, 1);
    if(rc) return rc;
  }else{
    pfnid = 0;
  }
  if( zFromUuid && *zFromUuid ){
    pid = fsl__uuid_to_rid2(f, zFromUuid, FSL_PHANTOM_PUBLIC);
    if(pid<0){
      assert(f->error.code);
      return f->error.code;
    }
    assert(pid>0);
  }else{
    pid = 0;
  }

  if( zToUuid && *zToUuid ){
    fid = fsl__uuid_to_rid2(f, zToUuid, FSL_PHANTOM_PUBLIC);
    if(fid<0){
      assert(f->error.code);
      return f->error.code;
    }else if( isPublic ){
      rc = fsl_content_make_public(f, fid);
      if(rc) return rc;
    }
  }else{
    fid = 0;
  }

  if(isPrimary){
    doInsert = true;
  }else{
    fsl_stmt * sInsCheck = 0;
    rc = fsl_db_prepare_cached(db, &sInsCheck,
                               "SELECT 1 FROM mlink WHERE "
                               "mid=? AND fnid=? AND NOT isaux"
                               "/*%s()*/",__func__);
    if(rc){
      rc = fsl_cx_uplift_db_error(f, db);
      goto end;
    }
    fsl_stmt_bind_id(sInsCheck, 1, mid);
    fsl_stmt_bind_id(sInsCheck, 2, fnid);
    rc = fsl_stmt_step(sInsCheck);
    fsl_stmt_cached_yield(sInsCheck);
    doInsert = (FSL_RC_STEP_ROW==rc) ? true : false;
    rc = 0;
  }
  if(doInsert){
    rc = fsl_db_prepare_cached(db, &s1,
                               "INSERT INTO mlink("
                               "mid,fid,pmid,pid,"
                               "fnid,pfnid,mperm,isaux"
                               ")VALUES("
                               ":m,:f,:pm,:p,:n,:pfn,:mp,:isaux"
                               ")"
                               "/*%s()*/",__func__);
    if(!rc){
      fsl_stmt_bind_id_name(s1, ":m", mid);
      fsl_stmt_bind_id_name(s1, ":f", fid);
      fsl_stmt_bind_id_name(s1, ":pm", pmid);
      fsl_stmt_bind_id_name(s1, ":p", pid);
      fsl_stmt_bind_id_name(s1, ":n", fnid);
      fsl_stmt_bind_id_name(s1, ":pfn", pfnid);
      fsl_stmt_bind_id_name(s1, ":mp", mperm);    
      fsl_stmt_bind_int32_name(s1, ":isaux", isPrimary ? 0 : 1);    
      rc = fsl_stmt_step(s1);
      fsl_stmt_cached_yield(s1);
      if(FSL_RC_STEP_DONE==rc){
        rc = 0;
      }else{
        fsl_cx_uplift_db_error(f, db);
      }
    }
  }
  if(!rc && pid>0 && fid){
    /* Reminder to self: this costs almost 1ms per checkin in very
       basic tests with 2003 checkins on my NUC unit. */
    rc = fsl__content_deltify(f, pid, fid, 0);
  }
  end:
  return rc;  
}

/**
    Do a binary search to find a file in d->F.list.  
   
    As an optimization, guess that the file we seek is at index
    d->F.cursor.  That will usually be the case.  If it is not found
    there, then do the actual binary search.

    Update d->F.cursor to be the index of the file that is found.

    If d->f is NULL then this perform a case-sensitive search,
    otherwise it uses case-sensitive or case-insensitive,
    depending on f->cache.caseInsensitive.    

    If the 3rd argument is not NULL and non-NULL is returned then
    *atNdx gets set to the d->F.list index of the resulting object.
    If NULL is returned, *atNdx is not modified.

    Reminder to self: if this requires a non-const deck (and it does
    right now) then the whole downstream chain will require a
    non-const instance or they'll have to make local copies to make
    the manipulation of d->F.cursor legal (but that would break
    following of baselines without yet more trickery).

    Reminder to self:

    Fossil(1) added another parameter to this since it was ported,
    indicating whether only an exact match or the "closest match" is
    acceptable, but currently (2021-03-10) only the fusefs module uses
    the closest-match option. It's a trivial code change but currently
    looks like YAGNI.
*/
static fsl_card_F * fsl__deck_F_seek_base(fsl_deck * d,
                                         char const * zName,
                                         uint32_t * atNdx ){
  /* Maintenance reminder: this algo relies on the various
     counters being signed. */
  fsl_int_t lwr, upr;
  int c;
  fsl_int_t i;
  assert(d);
  assert(zName && *zName);
  if(!d->F.used) return NULL;
  else if(FSL_CARD_F_LIST_NEEDS_SORT & d->F.flags){
    fsl_card_F_list_sort(&d->F);
  }
#define FCARD(NDX) F_at(&d->F, (NDX))
  lwr = 0;
  upr = d->F.used-1;
  if( d->F.cursor>=lwr && d->F.cursor<upr ){
    c = (d->f && d->f->cache.caseInsensitive)
      ? fsl_stricmp(FCARD(d->F.cursor+1)->name, zName)
      : fsl_strcmp(FCARD(d->F.cursor+1)->name, zName);
    if( c==0 ){
      if(atNdx) *atNdx = (uint32_t)d->F.cursor+1;
      return FCARD(++d->F.cursor);
    }else if( c>0 ){
      upr = d->F.cursor;
    }else{
      lwr = d->F.cursor+1;
    }
  }
  while( lwr<=upr ){
    i = (lwr+upr)/2;
    c = (d->f && d->f->cache.caseInsensitive)
      ? fsl_stricmp(FCARD(i)->name, zName)
      : fsl_strcmp(FCARD(i)->name, zName);
    if( c<0 ){
      lwr = i+1;
    }else if( c>0 ){
      upr = i-1;
    }else{
      d->F.cursor = i;
      if(atNdx) *atNdx = (uint32_t)i;
      return FCARD(i);
    }
  }
  return NULL;
#undef FCARD
}

fsl_card_F * fsl__deck_F_seek(fsl_deck * const d, const char *zName){
  fsl_card_F *pFile;
  assert(d);
  assert(zName && *zName);
  if(!d || (FSL_SATYPE_CHECKIN!=d->type) || !zName || !*zName
     || !d->F.used) return NULL;
  pFile = fsl__deck_F_seek_base(d, zName, NULL);
  if( !pFile &&
      (d->B.baseline /* we have a baseline or... */
       || (d->f && d->B.uuid) /* we can load the baseline */
       )){
        /* Check baseline manifest...

           Sidebar: while the delta manifest model outwardly appears
           to support recursive delta manifests, fossil(1) does not
           use them and there would seem to be little practical use
           for them (no notable size benefit for the majority of
           cases), so we're not recursing here.
         */
    int const rc = d->B.baseline ? 0 : fsl_deck_baseline_fetch(d);
    if(rc){
      assert(d->f->error.code);
    }else if( d->B.baseline ){
      assert(d->B.baseline->f && "How can this happen?");
      assert((d->B.baseline->f == d->f) &&
             "Universal laws are out of balance.");
      pFile = fsl__deck_F_seek_base(d->B.baseline, zName, NULL);
      if(pFile){
        assert(pFile->uuid &&
               "Per fossil-dev thread with DRH on 20140422, "
               "baselines never have removed files.");
      }
    }
  }
  return pFile;
}

fsl_card_F const * fsl_deck_F_search(fsl_deck *d, const char *zName){
  assert(d);
  return fsl__deck_F_seek(d, zName);
}

int fsl_deck_F_set( fsl_deck * d, char const * zName,
                    char const * uuid,
                    fsl_fileperm_e perms, 
                    char const * priorName){
  uint32_t fcNdx = 0;
  fsl_card_F * fc = 0;
  if(d->rid>0){
    return fsl_cx_err_set(d->f, FSL_RC_MISUSE,
                          "%s() cannot be applied to a saved deck.",
                          __func__);
  }else if(!fsl_deck_check_type(d, 'F')){
    return d->f->error.code;
  }
  fc = fsl__deck_F_seek_base(d, zName, &fcNdx);
  if(!uuid){
    if(fc){
      fsl_card_F_list_remove(&d->F, fcNdx);
      return 0;
    }else{
      return FSL_RC_NOT_FOUND;
    }
  }else if(!fsl_is_uuid(uuid)){
    return fsl_cx_err_set(d->f, FSL_RC_RANGE,
                          "Invalid UUID for F-card.");
  }
  if(fc){
    /* Got a match. Replace its contents. */
    char * n = 0;
    if(!fc->deckOwnsStrings){
      /* We can keep fc->name but need a tiny bit of hoop-jumping
         to do so. */
      n = fc->name;
      fc->name = 0;
    }
    fsl_card_F_clean(fc);
    assert(!fc->deckOwnsStrings);
    if(!(fc->name = n ? n : fsl_strdup(zName))) return FSL_RC_OOM;
    if(!(fc->uuid = fsl_strdup(uuid))) return FSL_RC_OOM;
    if(priorName && *priorName){
      if(!fsl_is_simple_pathname(priorName, 1)){
        return fsl_cx_err_set(d->f, FSL_RC_RANGE,
                              "Invalid priorName for F-card "
                              "(simple form required): %s", priorName);
      }else if(!(fc->priorName = fsl_strdup(priorName))){
        return FSL_RC_OOM;
      }
    }
    fc->perm = perms;
    return 0;
  }else{
    return fsl_deck_F_add(d, zName, uuid, perms, priorName);
  }
}

int fsl_deck_F_set_content( fsl_deck * const d, char const * zName,
                            fsl_buffer const * const src,
                            fsl_fileperm_e perm, 
                            char const * priorName){
  fsl_uuid_str zHash = 0;
  fsl_id_t rid = 0;
  fsl_id_t prevRid = 0;
  int rc = 0;
  assert(d->f);
  if(d->rid>0){
    return fsl_cx_err_set(d->f, FSL_RC_MISUSE,
                          "%s() cannot be applied to a saved deck.",
                          __func__);
  }else if(!fsl_cx_transaction_level(d->f)){
    return fsl_cx_err_set(d->f, FSL_RC_MISUSE,
                          "%s() requires that a transaction is active.",
                          __func__);
  }else if(!fsl_is_simple_pathname(zName, true)){
    return fsl_cx_err_set(d->f, FSL_RC_RANGE,
                          "Filename is not valid for use as a repository "
                          "entry: %s", zName);
  }
  rc = fsl_repo_blob_lookup(d->f, src, &rid, &zHash);
  if(rc && FSL_RC_NOT_FOUND!=rc) goto end;
  assert(zHash);
  if(!rid){
    fsl_card_F const * fc;
    /* This is new content. Save it, then see if we have a previous version
       to delta against this one. */
    rc = fsl__content_put_ex(d->f, src, zHash, 0, 0, false, &rid);
    if(rc) goto end;
    fc = fsl__deck_F_seek(d, zName);
    if(fc){
      prevRid = fsl_uuid_to_rid(d->f, fc->uuid);
      if(prevRid<0) goto end;
      else if(!prevRid){
        assert(!"cannot happen");
        rc = fsl_cx_err_set(d->f, FSL_RC_NOT_FOUND,
                            "Cannot find RID of file content %s [%s]\n",
                            fc->name, fc->uuid);
        goto end;
      }
      rc = fsl__content_deltify(d->f, prevRid, rid, false);
      if(rc) goto end;
    }
  }
  rc = fsl_deck_F_set(d, zName, zHash, perm, priorName);
  end:
  fsl_free(zHash);
  return rc;
}

void fsl__deck_clean_cards(fsl_deck * const d, char const * letters){
  char const * c = letters
    ? letters
    : "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  for( ; *c; ++c ){
    switch(*c){
      case 'A': fsl_deck_clean_A(d); break;
      case 'B': fsl_deck_clean_B(d); break;
      case 'C': fsl_deck_clean_C(d); break;
      case 'D': d->D = 0.0; break;
      case 'E': fsl_deck_clean_E(d); break;
      case 'F': fsl_deck_clean_F(d); break;
      case 'G': fsl_deck_clean_G(d); break;
      case 'H': fsl_deck_clean_H(d); break;
      case 'I': fsl_deck_clean_I(d); break;
      case 'J': fsl_deck_clean_J(d,true); break;
      case 'K': fsl_deck_clean_K(d); break;
      case 'L': fsl_deck_clean_L(d); break;
      case 'M': fsl_deck_clean_M(d); break;
      case 'N': fsl_deck_clean_N(d); break;
      case 'P': fsl_deck_clean_P(d); break;
      case 'Q': fsl_deck_clean_Q(d); break;
      case 'R': fsl_deck_clean_R(d); break;
      case 'T': fsl_deck_clean_T(d); break;
      case 'U': fsl_deck_clean_U(d); break;
      case 'W': fsl_deck_clean_W(d); break;
      default: break;
    }
  }
}

int fsl_deck_derive(fsl_deck * const d){
  int rc = 0;
  if(d->rid<=0) return FSL_RC_MISUSE;
  assert(d->f);
  if(FSL_SATYPE_CHECKIN!=d->type) return FSL_RC_TYPE;
  fsl_deck_clean_P(d);
  {
    fsl_uuid_str pUuid = fsl_rid_to_uuid(d->f, d->rid);
    if(pUuid){
      rc = fsl_list_append(&d->P, pUuid);
      if(rc){
        assert(NULL==d->P.list);
        fsl_free(pUuid);
      }
    }else{
      assert(d->f->error.code);
      rc = d->f->error.code;
    }
    if(rc) return rc;
  }
  d->rid = 0;
  fsl__deck_clean_cards(d, "ACDEGHIJKLMNQRTUW");
  while(d->B.uuid){
    /* This is a delta manifest. Convert this deck into a baseline by
       build a new, complete F-card list. */
    fsl_card_F const * fc;
    fsl_card_F_list flist = fsl_card_F_list_empty;
    uint32_t fCount = 0;
    rc = fsl_deck_F_rewind(d);
    if(rc) return rc;
    while( 0==(rc=fsl_deck_F_next(d, &fc)) && fc ){
      ++fCount;
    }
    rc = fsl_deck_F_rewind(d);
    assert(0==rc
           && "fsl_deck_F_rewind() cannot fail after initial call.");
    assert(0==d->F.cursor);
    assert(0==d->B.baseline->F.cursor);
    rc = fsl_card_F_list_reserve(&flist, fCount);
    if(rc) break;
    while( 1 ){
      rc = fsl_deck_F_next(d, &fc);
      if(rc || !fc) break;
      fsl_card_F * const fNew = fsl_card_F_list_push(&flist);
      assert(fc->uuid);
      assert(fc->name);
      /* We must copy these strings because their ownership is
         otherwise unmanageable. e.g. they might live in d->content
         or d->B.baseline->content. */
      if(!(fNew->name = fsl_strdup(fc->name))
         || !(fNew->uuid = fsl_strdup(fc->uuid))){
        /* Reminder: we do not want/need to copy fc->priorName. Those
           renames were already applied in the parent checkin. */
        rc = FSL_RC_OOM;
        break;
      }
      fNew->perm = fc->perm;
    }
    fsl_deck_clean_B(d);
    fsl_deck_clean_F(d);
    if(rc) fsl_card_F_list_finalize(&flist);
    else d->F = flist/*transfer ownership*/;
    break;
  }
  return rc;
}

/**
    Returns true if repo contains an mlink entry where mid=rid, else
    false.
*/
static bool fsl_repo_has_mlink_mid( fsl_db * repo, fsl_id_t rid ){
#if 0
  return fsl_db_exists(repo,
                       "SELECT 1 FROM mlink WHERE mid=%"FSL_ID_T_PFMT,
                       rid);
#else
  fsl_stmt * st = NULL;
  bool gotone = false;
  int rc = fsl_db_prepare_cached(repo, &st,
                                 "SELECT 1 FROM mlink WHERE mid=?"
                                 "/*%s()*/",__func__);
  
  if(!rc){
    fsl_stmt_bind_id(st, 1, rid);
    rc = fsl_stmt_step(st);
    fsl_stmt_cached_yield(st);
    gotone = rc==FSL_RC_STEP_ROW;
  }
  return gotone;
#endif
}

static bool fsl_repo_has_mlink_pmid_mid( fsl_db * repo, fsl_id_t pmid, fsl_id_t mid ){
  fsl_stmt * st = NULL;
  int rc = fsl_db_prepare_cached(repo, &st,
                                 "SELECT 1 FROM mlink WHERE mid=? "
                                 "AND pmid=?"
                                 "/*%s()*/",__func__);
  if(!rc){
    fsl_stmt_bind_id(st, 1, mid);
    fsl_stmt_bind_id(st, 2, pmid);
    rc = fsl_stmt_step(st);
    fsl_stmt_cached_yield(st);
    if( rc==FSL_RC_STEP_ROW ) rc = 0;
  }
  /* MARKER(("fsl_repo_has_mlink_mid(%d) rc=%d\n", (int)rid, rc)); */
  return rc ? false : true;
}

/**
    Add mlink table entries associated with manifest cid, pChild.  The
    parent manifest is pid, pParent.  One of either pChild or pParent
    will be NULL and it will be computed based on cid/pid.
   
    A single mlink entry is added for every file that changed content,
    name, and/or permissions going from pid to cid.
   
    Deleted files have mlink.fid=0.
   
    Added files have mlink.pid=0.
   
    File added by merge have mlink.pid=-1.

    Edited files have both mlink.pid!=0 and mlink.fid!=0

    Comments from the original implementation:

    Many mlink entries for merge parents will only be added if another
    mlink entry already exists for the same file from the primary
    parent.  Therefore, to ensure that all merge-parent mlink entries
    are properly created:

    (1) Make this routine a no-op if pParent is a merge parent and the
        primary parent is a phantom.

    (2) Invoke this routine recursively for merge-parents if pParent
        is the primary parent.
*/
static int fsl_mlink_add( fsl_cx * const f,
                          fsl_id_t pmid, fsl_deck /*const*/ * pParent,
                          fsl_id_t cid, fsl_deck /*const*/ * pChild,
                          bool isPrimary){
  fsl_buffer otherContent = fsl_buffer_empty;
  fsl_id_t otherRid;
  fsl_size_t i = 0;
  int rc = 0;
  fsl_card_F const * pChildFile = NULL;
  fsl_card_F const * pParentFile = NULL;
  fsl_deck dOther = fsl_deck_empty;
  fsl_db * const db = fsl_cx_db_repo(f);
  bool isPublic;
  assert(db);
  assert(db->beginCount>0);
  /* If mlink table entires are already set for pmid/cid, then abort
     early doing no work.
  */
  //MARKER(("%s() pmid=%d cid=%d\n", __func__, (int)pmid, (int)cid));
  if(fsl_repo_has_mlink_pmid_mid(db, pmid, cid)) return 0;
  /* Compute the value of the missing pParent or pChild parameter.
     Fetch the baseline checkins for both.
  */
  assert( pParent==0 || pChild==0 );
  if( pParent ){
    assert(!pChild);
    pChild = &dOther;
    otherRid = cid;
  }else{
    pParent = &dOther;
    otherRid = pmid;
  }

  if(otherRid && !fsl__cx_mcache_search(f, otherRid, &dOther)){
    rc = fsl_content_get(f, otherRid, &otherContent);
    if(rc){
      /* fossil(1) simply ignores errors here and returns. We'll ignore
         the phantom case because (1) erroring out here would be bad and
         (2) fossil does so. The exact implications of doing so are
         unclear, though. */
      if(FSL_RC_PHANTOM==rc){
        rc = 0;
      }else if(!f->error.msg.used && FSL_RC_OOM!=rc){
        rc = fsl_cx_err_set(f, rc,
                            "Fetching content of rid %"FSL_ID_T_PFMT" failed: %s",
                            otherRid, fsl_rc_cstr(rc));
      }
      goto end;
    }
    if( !otherContent.used ){
      /* ??? fossil(1) ignores this case and returns. */
      fsl_buffer_clear(&otherContent)/*for empty file case*/;
      rc = 0;
      goto end;
    }
    dOther.f = f;
    rc = fsl_deck_parse2(&dOther, &otherContent, otherRid);
    assert(dOther.f);
    if(rc) goto end;
  }
  if( (pParent->f && (rc=fsl_deck_baseline_fetch(pParent)))
      || (pChild->f && (rc=fsl_deck_baseline_fetch(pChild)))){
    goto end;
  }
  isPublic = !fsl_content_is_private(f, cid);

  /* If pParent is not the primary parent of pChild, and the primary
  ** parent of pChild is a phantom, then abort this routine without
  ** doing any work.  The mlink entries will be computed when the
  ** primary parent dephantomizes.
  */
  if( !isPrimary && otherRid==cid ){
    assert(pChild->P.used);
    if(!fsl_db_exists(db,"SELECT 1 FROM blob WHERE uuid=%Q AND size>0",
                      (char const *)pChild->P.list[0])){
      rc = 0;
      fsl__cx_mcache_insert(f, &dOther);
      goto end;
    }
  }

  if(pmid>0){
    /* Try to make the parent manifest a delta from the child, if that
       is an appropriate thing to do.  For a new baseline, make the 
       previous baseline a delta from the current baseline.
    */
    if( (pParent->B.uuid==0)==(pChild->B.uuid==0) ){
      rc = fsl__content_deltify(f, pmid, cid, 0);
    }else if( pChild->B.uuid==NULL && pParent->B.uuid!=NULL ){
      rc = fsl__content_deltify(f, pParent->B.baseline->rid, cid, 0);
    }
    if(rc) goto end;
  }

  /* Remember all children less than a few seconds younger than their parent,
     as we might want to fudge the times for those children.
  */
  if( f->cache.isCrosslinking &&
      (pChild->D < pParent->D+AGE_FUDGE_WINDOW)
  ){
    rc = fsl_db_exec(db, "INSERT OR REPLACE INTO time_fudge VALUES"
                     "(%"FSL_ID_T_PFMT", %"FSL_JULIAN_T_PFMT
                     ", %"FSL_ID_T_PFMT", %"FSL_JULIAN_T_PFMT");",
                     pParent->rid, pParent->D,
                     pChild->rid, pChild->D);
    if(rc) goto end;
  }

  /* First look at all files in pChild, ignoring its baseline.  This
     is where most of the changes will be found.
  */  
#define FCARD(DECK,NDX) \
  ((((NDX)<(DECK)->F.used)) \
   ? F_at(&(DECK)->F,NDX)  \
   : NULL)
  for(i=0, pChildFile=FCARD(pChild,0);
      i<pChild->F.used;
      ++i, pChildFile=FCARD(pChild,i)){
    fsl_fileperm_e const mperm = pChildFile->perm;
    if( pChildFile->priorName ){
      pParentFile = pmid
        ? fsl__deck_F_seek(pParent, pChildFile->priorName)
        : 0;
      if( pParentFile ){
        /* File with name change */
        /*
          libfossil checkin 8625a31eff708dea93b16582e4ec5d583794d1af
          contains these two interesting F-cards:

F src/net/wanderinghorse/libfossil/FossilCheckout.java
F src/org/fossil_scm/libfossil/Checkout.java 6e58a47089d3f4911c9386c25bac36c8e98d4d21 w src/net/wanderinghorse/libfossil/FossilCheckout.java

          Note the placement of FossilCheckout.java (twice).

          Up until then, i thought a delete/rename combination was not possible.
        */
        rc = fsl_mlink_add_one(f, pmid, pParentFile->uuid,
                               cid, pChildFile->uuid, pChildFile->name,
                               pChildFile->priorName, isPublic,
                               isPrimary, mperm);
      }else{
         /* File name changed, but the old name is not found in the parent!
            Treat this like a new file. */
        rc = fsl_mlink_add_one(f, pmid, 0, cid, pChildFile->uuid,
                               pChildFile->name, 0,
                               isPublic, isPrimary, mperm);
      }
    }else if(pmid){
      pParentFile = fsl__deck_F_seek(pParent, pChildFile->name);
      if(!pParentFile || !pParentFile->uuid){
        /* Parent does not have it or it was removed in parent. */
        if( pChildFile->uuid ){
          /* A new or re-added file */
          rc = fsl_mlink_add_one(f, pmid, 0, cid, pChildFile->uuid,
                                 pChildFile->name, 0,
                                 isPublic, isPrimary, mperm);
        }
      }
      else if( fsl_strcmp(pChildFile->uuid, pParentFile->uuid)!=0
                || (pParentFile->perm!=mperm) ){
         /* Changes in file content or permissions */
        rc = fsl_mlink_add_one(f, pmid, pParentFile->uuid,
                               cid, pChildFile->uuid,
                               pChildFile->name, 0,
                               isPublic, isPrimary, mperm);
      }
    }
  } /* end pChild card list loop */
  if(rc) goto end;
  else if( pParent->B.uuid && pChild->B.uuid ){
    /* Both parent and child are delta manifests.  Look for files that
       are deleted or modified in the parent but which reappear or revert
       to baseline in the child and show such files as being added or changed
       in the child. */
    for(i=0, pParentFile=FCARD(pParent,0);
        i<pParent->F.used;
        ++i, pParentFile = FCARD(pParent,i)){
      if( pParentFile->uuid ){
        pChildFile = fsl__deck_F_seek_base(pChild, pParentFile->name, NULL);
        if( !pChildFile || !pChildFile->uuid){
          /* The child file reverts to baseline or is deleted.
             Show this as a change. */
          if(!pChildFile){
            pChildFile = fsl__deck_F_seek(pChild, pParentFile->name);
          }
          if( pChildFile && pChildFile->uuid ){
            rc = fsl_mlink_add_one(f, pmid, pParentFile->uuid, cid,
                                   pChildFile->uuid, pChildFile->name,
                                   0, isPublic, isPrimary,
                                   pChildFile->perm);
          }
        }
      }else{
        /* Was deleted in the parent. */
        pChildFile = fsl__deck_F_seek(pChild, pParentFile->name);
        if( pChildFile && pChildFile->uuid ){
          /* File resurrected in the child after having been deleted in
             the parent.  Show this as an added file. */
          rc = fsl_mlink_add_one(f, pmid, 0, cid, pChildFile->uuid,
                                 pChildFile->name, 0, isPublic,
                                 isPrimary, pChildFile->perm);
        }
      }
      if(rc) goto end;
    }
    assert(0==rc);
  }else if( pmid && !pChild->B.uuid ){
    /* pChild is a baseline with a parent.  Look for files that are
       present in pParent but are missing from pChild and mark them as
       having been deleted. */
    fsl_card_F const * cfc = NULL;
    fsl_deck_F_rewind(pParent);
    while( (0==(rc=fsl_deck_F_next(pParent,&cfc))) && cfc){
      pParentFile = cfc;
      pChildFile = fsl__deck_F_seek(pChild, pParentFile->name);
      if( (!pChildFile || !pChildFile->uuid) && pParentFile->uuid ){
        rc = fsl_mlink_add_one(f, pmid, pParentFile->uuid, cid, 0,
                               pParentFile->name, 0, isPublic,
                               isPrimary, pParentFile->perm);
      }
    }
    if(rc) goto end;
  }

  fsl__cx_mcache_insert(f, &dOther);
  
  /* If pParent is the primary parent of pChild, also run this analysis
  ** for all merge parents of pChild */
  if( pmid && isPrimary ){
    for(i=1; i<pChild->P.used; i++){
      pmid = fsl_uuid_to_rid(f, (char const*)pChild->P.list[i]);
      if( pmid<=0 ) continue;
      rc = fsl_mlink_add(f, pmid, 0, cid, pChild, false);
      if(rc) goto end;
    }
    for(i=0; i<pChild->Q.used; i++){
      fsl_card_Q const * q = (fsl_card_Q const *)pChild->Q.list[i];
      if( q->type>0 && (pmid = fsl_uuid_to_rid(f, q->target))>0 ){
        rc = fsl_mlink_add(f, pmid, 0, cid, pChild, false);
        if(rc) goto end;
      }
    }
  }

  end:
  fsl_deck_finalize(&dOther);
  fsl_buffer_clear(&otherContent);
  if(rc && !f->error.code && db->error.code){
    rc = fsl_cx_uplift_db_error(f, db);
  }
  return rc;
#undef FCARD
}

/**
   Apply all tags defined in deck d. If parentId is >0 then any
   propagating tags from that parent are well and duly propagated.
   Returns 0 on success. Potential TODO: if parentId<=0 and
   d->P.used>0 then use d->P.list[0] in place of parentId.
*/
static int fsl__deck_crosslink_apply_tags(fsl_cx * f, fsl_deck *d,
                                         fsl_db * db, fsl_id_t rid,
                                         fsl_id_t parentId){
  int rc = 0;
  fsl_size_t i;
  fsl_list const * li = &d->T;
  double tagTime = d->D;
  if(li->used && tagTime<=0){
    tagTime = fsl_db_julian_now(db);
    if(tagTime<=0){
      rc = FSL_RC_DB;
      goto end;
    }
  }      
  for( i = 0; !rc && (i < li->used); ++i){
    fsl_id_t tid;
    fsl_card_T const * tag = (fsl_card_T const *)li->list[i];
    assert(tag);
    if(!tag->uuid){
      tid = rid;
    }else{
      tid = fsl_uuid_to_rid( f, tag->uuid);
    }
    if(tid<0){
      assert(f->error.code);
      rc = f->error.code;
      break;
    }else if(0==tid){
      rc = fsl_cx_err_set(f, FSL_RC_RANGE,
                          "Could not get RID for [%.12s].",
                          tag->uuid);
      break;
    }
    rc = fsl__tag_insert(f, tag->type,
                        tag->name, tag->value,
                        rid, tagTime, tid, NULL);
  }
  if( !rc && (parentId>0) ){
    rc = fsl__tag_propagate_all(f, parentId);
  }
  end:
  return rc;
}

/**
   Part of the checkin crosslink machinery: create all appropriate
   plink and mlink table entries for d->P.

   If parentId is not NULL, *parentId gets assigned to the rid of the
   first parent, or 0 if d->P is empty.
*/
static int fsl_deck_add_checkin_linkages(fsl_deck *d, fsl_id_t * parentId){
  int rc = 0;
  fsl_size_t nLink = 0;
  char zBaseId[30] = {0}/*RID of baseline or "NULL" if no baseline */;
  fsl_size_t i;
  fsl_stmt q = fsl_stmt_empty;
  fsl_id_t _parentId = 0;
  fsl_cx * const f = d->f;
  fsl_db * const db = fsl_cx_db_repo(f);
  assert(f && db);
  if(!parentId) parentId = &_parentId;
  if(d->B.uuid){
    fsl_id_t const baseid = d->B.baseline
      ? d->B.baseline->rid
      : fsl_uuid_to_rid(d->f, d->B.uuid);
    if(baseid<0){
      rc = d->f->error.code;
      assert(0 != rc);
      goto end;
    }
    assert(baseid>0);
    fsl_snprintf( zBaseId, sizeof(zBaseId),
                  "%"FSL_ID_T_PFMT,
                  baseid );
        
  }else{
    fsl_snprintf( zBaseId, sizeof(zBaseId), "NULL" );
  }
  *parentId = 0;
  for(i=0; i<d->P.used; ++i){
    char const * parentUuid = (char const *)d->P.list[i];
    fsl_id_t const pid = fsl__uuid_to_rid2(f, parentUuid, FSL_PHANTOM_PUBLIC);
    if(pid<0){
      assert(f->error.code);
      rc = f->error.code;
      goto end;
    }
    rc = fsl_db_exec(db, "INSERT OR IGNORE "
                     "INTO plink(pid, cid, isprim, mtime, baseid) "
                     "VALUES(%"FSL_ID_T_PFMT", %"FSL_ID_T_PFMT
                     ", %d, %"FSL_JULIAN_T_PFMT", %s)",
                     pid, d->rid,
                     ((i==0) ? 1 : 0), d->D, zBaseId);
    if(rc) goto end;
    if(0==i) *parentId = pid;
  }
  rc = fsl_mlink_add(f, *parentId, NULL, d->rid, d, true);
  if(rc) goto end;
  nLink = d->P.used;
  for(i=0; i<d->Q.used; ++i){
    fsl_card_Q const * q = (fsl_card_Q const *)d->Q.list[i];
    if(q->type>0) ++nLink;
  }
  if(nLink>1){
    /* https://www.fossil-scm.org/index.html/info/8e44cf6f4df4f9f0 */
    /* Change MLINK.PID from 0 to -1 for files that are added by merge. */
    rc = fsl_db_exec(db,
                     "UPDATE mlink SET pid=-1"
                     " WHERE mid=%"FSL_ID_T_PFMT
                     "   AND pid=0"
                     "   AND fnid IN "
                     "  (SELECT fnid FROM mlink WHERE mid=%"FSL_ID_T_PFMT
                     " GROUP BY fnid"
                     "    HAVING count(*)<%d)",
                     d->rid, d->rid, (int)nLink
                     );
    if(rc) goto end;
  }
  rc = fsl_db_prepare(db, &q,
                      "SELECT cid, isprim FROM plink "
                      "WHERE pid=%"FSL_ID_T_PFMT,
                      d->rid);
  while( !rc && (FSL_RC_STEP_ROW==(rc=fsl_stmt_step(&q))) ){
    fsl_id_t const cid = fsl_stmt_g_id(&q, 0);
    int const isPrim = fsl_stmt_g_int32(&q, 1);
    /* This block is only hit a couple of times during a fresh rebuild (empty mlink/plink
       tables), but many times on a rebuilds if those tables are not emptied in advance? */
    assert(cid>0);
    rc = fsl_mlink_add(f, d->rid, d, cid, NULL, isPrim ? true : false);
  }
  if(FSL_RC_STEP_DONE==rc) rc = 0;
  fsl_stmt_finalize(&q);
  if(rc) goto end;
  if( !d->P.used ){
    /* For root files (files without parents) add mlink entries
       showing all content as new.

       Historically, fossil has been unable to create such checkins
       because the initial checkin has no files.
    */
    int const isPublic = !fsl_content_is_private(f, d->rid);
    for(i=0; !rc && (i<d->F.used); ++i){
      fsl_card_F const * fc = F_at(&d->F, i);
      rc = fsl_mlink_add_one(f, 0, 0, d->rid, fc->uuid, fc->name, 0,
                             isPublic, 1, fc->perm);
    }
  }
  end:
  return rc;
}

/**
   Applies the value of a "parent" tag (reparent) to the given
   artifact id. zTagVal must be the value of a parent tag (a list of
   full UUIDs). This is only to be run as part of fsl__crosslink_end().

   Returns 0 on success.

   POTENTIAL fixme: perhaps return without side effects if rid is not
   found (like fossil(1) does). That said, this step is only run after
   crosslinking and would only result in a not-found if the tagxref
   table contents is out of date.

   POTENTIAL fixme: fail without error if the tag value is malformed,
   under the assumption that the tag was intended for some purpose
   other than reparenting.
*/
static int fsl_crosslink_reparent(fsl_cx * f, fsl_id_t rid, char const *zTagVal){
  int rc = 0;
  char * zDup = 0;
  char * zPos;
  fsl_size_t maxP, nP = 0;
  fsl_deck d = fsl_deck_empty;
  fsl_list fakeP = fsl_list_empty
    /* fake P-card for purposes of passing the reparented deck through
       fsl_deck_add_checkin_linkages() */;
  maxP = (fsl_strlen(zTagVal)+1) / (FSL_STRLEN_SHA1+1);
  if(!maxP) return FSL_RC_RANGE;
  rc = fsl_list_reserve(&fakeP, maxP);
  if(rc) return rc;
  zDup = fsl_strdup(zTagVal);
  if(!zDup){
    rc = FSL_RC_OOM;
    goto end;
  }
  /* Split zTagVal into list of parent IDs... */
  for( nP = 0, zPos = zDup; *zPos; ){
    char const * zBegin = zPos;
    for( ; *zPos && ' '!=*zPos; ++zPos){}
    if(' '==*zPos){
      *zPos = 0;
      ++zPos;
    }
    if(!fsl_is_uuid(zBegin)){
      rc = fsl_cx_err_set(f, FSL_RC_RANGE,
                          "Invalid value [%s] in reparent tag value "
                          "[%s] for rid %"FSL_ID_T_PFMT".",
                          zBegin, zTagVal, rid);
      goto end;
    }
    fakeP.list[nP++] = (void *)zBegin;
  }
  assert(!rc);
  fakeP.used = nP;
  rc = fsl_deck_load_rid(f, &d, rid, FSL_SATYPE_ANY);
  if(rc) goto end;
  switch(d.type){
    case FSL_SATYPE_CHECKIN:
    case FSL_SATYPE_TECHNOTE:
    case FSL_SATYPE_WIKI:
    case FSL_SATYPE_FORUMPOST:
      break;
    default:
      rc = fsl_cx_err_set(f, FSL_RC_TYPE, "Invalid deck type (%s) "
                          "for use with the 'parent' tag.",
                          fsl_satype_cstr(d.type));
      goto end;
  }
  assert(d.rid==rid);
  assert(d.f);
  fsl_db * const db = fsl_cx_db_repo(f);
  rc = fsl_db_exec_multi(db,
                         "DELETE FROM plink WHERE cid=%"FSL_ID_T_PFMT";"
                         "DELETE FROM mlink WHERE mid=%"FSL_ID_T_PFMT";",
                         rid, rid);
  if(rc) goto end;
  fsl_list const origP = d.P;
  d.P = fakeP;
  rc = fsl_deck_add_checkin_linkages(&d, NULL);
  d.P = origP;
  fsl_deck_finalize(&d);

  end:
  fsl_list_reserve(&fakeP, 0);
  fsl_free(zDup);
  return rc;
}

/**
   Inserts plink entries for FORUM, WIKI, and TECHNOTE manifests. May
   assert for other manifest types. If a parent entry exists, it also
   propagates any tags for that parent. This is a no-op if
   the deck has no parents.
*/
static int fsl__deck_crosslink_fwt_plink(fsl_deck * d){
  int i;
  fsl_id_t parentId = 0;
  fsl_db * db;
  int rc = 0;
  assert(d->type==FSL_SATYPE_WIKI ||
         d->type==FSL_SATYPE_FORUMPOST ||
         d->type==FSL_SATYPE_TECHNOTE);
  assert(d->f);
  assert(d->rid>0);
  if(!d->P.used) return rc;
  db = fsl_cx_db_repo(d->f);
  fsl__phantom_e const fantomMode = fsl_content_is_private(d->f, d->rid)
    ? FSL_PHANTOM_PRIVATE : FSL_PHANTOM_PUBLIC;
  for(i=0; 0==rc && i<(int)d->P.used; ++i){
    fsl_id_t const pid = fsl__uuid_to_rid2(d->f, (char const *)d->P.list[i],
                                          fantomMode);
    if(0==i) parentId = pid;
    rc = fsl_db_exec_multi(db,
                           "INSERT OR IGNORE INTO plink"
                           "(pid, cid, isprim, mtime, baseid)"
                           "VALUES(%"FSL_ID_T_PFMT", %"FSL_ID_T_PFMT", "
                           "%d, %"FSL_JULIAN_T_PFMT", NULL)",
                           pid, d->rid, i==0, d->D);
  }
  if(!rc && parentId){
    rc = fsl__tag_propagate_all(d->f, parentId);
  }
  return rc;
}

int fsl__call_xlink_listeners(fsl_deck * const d){
  int rc = 0;
  fsl_xlinker * xl = NULL;
  fsl_cx_err_reset(d->f);
  for( fsl_size_t i = 0; !rc && (i < d->f->xlinkers.used); ++i ){
    xl = d->f->xlinkers.list+i;
    rc = xl->f( d, xl->state );
  }
  if(rc && !d->f->error.code){
    assert(xl);
    rc = fsl_cx_err_set(d->f, rc, "Crosslink callback handler "
                        "'%s' failed with code %d (%s) for "
                        "artifact RID #%" FSL_ID_T_PFMT ".",
                        xl->name, rc, fsl_rc_cstr(rc),
                        d->rid);
  }
  return rc;
}

/**
   Overrideable crosslink listener which updates the timeline for
   attachment records.
*/
static int fsl_deck_xlink_f_attachment(fsl_deck * const d, void * state){
  if(FSL_SATYPE_ATTACHMENT!=d->type) return 0;
  int rc;
  fsl_db * const db = fsl_cx_db_repo(d->f);
  fsl_buffer * const comment = fsl__cx_scratchpad(d->f);
  const char isAdd = (d->A.src && *d->A.src) ? 1 : 0;
  char attachToType = 'w'
    /* Assume wiki until we know otherwise, keeping in mind that the
       d->A.tgt might not yet be in the blob table, in which case
       we are unable to know, for certain, what the target is.
       That only affects the timeline (event table), though, not
       the crosslinking of the attachment itself. */;
  assert(db);
  if(fsl_is_uuid(d->A.tgt)){
    if( fsl_db_exists(db, "SELECT 1 FROM tag WHERE tagname='tkt-%q'",
                      d->A.tgt)){
      attachToType = 't' /* attach to a known ticket */;
    }else if( fsl_db_exists(db, "SELECT 1 FROM tag WHERE tagname='event-%q'",
                            d->A.tgt)){
      attachToType = 'e' /* attach to a known technote (event) */;
    }
  }
  if('w'==attachToType){
    /* Attachment applies to a wiki page */
    if(isAdd){
      rc = fsl_buffer_appendf(comment,
                              "Add attachment \"%h\" "
                              "to wiki page [%h]",
                              d->A.name, d->A.tgt);
    }else{
      rc = fsl_buffer_appendf(comment,
                              "Delete attachment \"%h\" "
                              "from wiki page [%h]",
                              d->A.name, d->A.tgt);
    }
  }else if('e' == attachToType){/*technote*/
    if(isAdd){
      rc = fsl_buffer_appendf(comment,
                              "Add attachment [/artifact/%!S|%h] to "
                              "tech note [/technote/%!S|%S]",
                              d->A.src, d->A.name, d->A.tgt, d->A.tgt);
    }else{
      rc = fsl_buffer_appendf(comment,
                              "Delete attachment \"/artifact/%!S|%h\" "
                              "from tech note [/technote/%!S|%S]",
                              d->A.name, d->A.name, d->A.tgt,
                              d->A.tgt);
    }
  }else{
    /* Attachment applies to a ticket */
    if(isAdd){
      rc = fsl_buffer_appendf(comment,
                              "Add attachment [/artifact/%!S|%h] "
                              "to ticket [%!S|%S]",
                              d->A.src, d->A.name, d->A.tgt, d->A.tgt);
    }else{
      rc = fsl_buffer_appendf(comment,
                              "Delete attachment \"%h\" "
                              "from ticket [%!S|%S]",
                              d->A.name, d->A.tgt, d->A.tgt);
    }
  }
  if(!rc){
    rc = fsl_db_exec(db,
                     "REPLACE INTO event(type,mtime,objid,user,comment)"
                     "VALUES("
                     "'%c',%"FSL_JULIAN_T_PFMT",%"FSL_ID_T_PFMT","
                     "%Q,%B)",
                     attachToType, d->D, d->rid, d->U, comment);
  }
  fsl__cx_scratchpad_yield(d->f, comment);
  return rc;
}

/**
   Overrideable crosslink listener which updates the timeline for
   checkin records.
*/
static int fsl_deck_xlink_f_checkin(fsl_deck * const d, void * state){
  if(FSL_SATYPE_CHECKIN!=d->type) return 0;
  int rc;
  fsl_db * db;
  db = fsl_cx_db_repo(d->f);
  assert(db);
  rc = fsl_db_exec(db,
       "REPLACE INTO event(type,mtime,objid,user,comment,"
       "bgcolor,euser,ecomment,omtime)"
       "VALUES('ci',"
       "  coalesce(" /*mtime*/
       "    (SELECT julianday(value) FROM tagxref "
       "      WHERE tagid=%d AND rid=%"FSL_ID_T_PFMT
       "    ),"
       "    %"FSL_JULIAN_T_PFMT""
       "  ),"
       "  %"FSL_ID_T_PFMT","/*objid*/
       "  %Q," /*user*/
#if 1
       "  %Q," /*comment. No, the comment _field_. */
#else
       /* just for testing... */
       "  'xlink: %q'," /*comment. No, the comment _field_. */
#endif
       "  (SELECT value FROM tagxref " /*bgcolor*/
       "    WHERE tagid=%d AND rid=%"FSL_ID_T_PFMT
       "    AND tagtype>0"
       "  ),"
       "  (SELECT value FROM tagxref " /*euser*/
       "    WHERE tagid=%d AND rid=%"FSL_ID_T_PFMT
       "  ),"
       "  (SELECT value FROM tagxref " /*ecomment*/
       "    WHERE tagid=%d AND rid=%"FSL_ID_T_PFMT
       "  ),"
       "  %"FSL_JULIAN_T_PFMT/*omtime*/
       /* RETURNING coalesce(ecomment,comment)
          see comments below about zCom */
       ")",
       /* The casts here are to please the va_list. */
       (int)FSL_TAGID_DATE, d->rid, d->D,
       d->rid, d->U, d->C,
       (int)FSL_TAGID_BGCOLOR, d->rid,
       (int)FSL_TAGID_USER, d->rid,
       (int)FSL_TAGID_COMMENT, d->rid, d->D
  );
  return fsl_cx_uplift_db_error2(d->f, db, rc);
}

static int fsl_deck_xlink_f_control(fsl_deck * const d, void * state){
  if(FSL_SATYPE_CONTROL!=d->type) return 0;
  /*
    Create timeline event entry for all tags in this control
    construct. Note that we are using a lot of historical code which
    hard-codes english-lanuage text and links which only work in
    fossil(1). i would prefer to farm this out to a crosslink
    callback, and provide a default implementation which more or
    less mimics fossil(1).
  */
  int rc = 0;
  fsl_buffer * const comment = fsl__cx_scratchpad(d->f);
  fsl_size_t i;
  const char *zName;
  const char *zValue;
  const char *zUuid;
  int branchMove = 0;
  int const uuidLen = 8;
  fsl_card_T const * tag = NULL;
  fsl_card_T const * prevTag = NULL;
  fsl_list const * li = &d->T;
  fsl_db * const db = fsl_cx_db_repo(d->f);
  double mtime = (d->D>0)
    ? d->D
    : fsl_db_julian_now(db);
  assert(db);
  /**
     Reminder to self: fossil(1) has a comment here:

     // Next loop expects tags to be sorted on UUID, so sort it.
     qsort(p->aTag, p->nTag, sizeof(p->aTag[0]), tag_compare);

     That sort plays a role in hook code execution and is needed to
     avoid duplicate hook execution in some cases. libfossil
     outsources that type of thing to crosslink callbacks, though,
     so we won't concern ourselves with it here. We also don't
     really want to modify the deck during crosslinking. The only
     reason the deck is not const in this routine is because of the
     fsl_deck::F::cursor bits inherited from fossil(1), largely
     worth its cost except that many routines can no longer be
     const. Shame C doesn't have C++'s "mutable" keyword.

     That said, sorting by UUID would have a nice side-effect on the
     output of grouping tags by the UUID they tag. So far
     (201404) such groups of tags have not appeared in the wild
     because fossil(1) has no mechanism for creating them.
  */
  for( i = 0; !rc && (i < li->used); ++i, prevTag = tag){
    char isProp = 0, isAdd = 0, isCancel = 0;
    tag = (fsl_card_T const *)li->list[i];
    zUuid = tag->uuid;
    if(!zUuid /*tag on self*/) continue;
    if( i==0 || 0!=fsl_uuidcmp(tag->uuid, prevTag->uuid)){
      rc = fsl_buffer_appendf(comment,
                              " Edit [%.*s]:", uuidLen, zUuid);
      branchMove = 0;
    }
    if(rc) goto end;
    isProp = FSL_TAGTYPE_PROPAGATING==tag->type;
    isAdd = FSL_TAGTYPE_ADD==tag->type;
    isCancel = FSL_TAGTYPE_CANCEL==tag->type;
    assert(isProp || isAdd || isCancel);
    zName = tag->name;
    zValue = tag->value;
    if( isProp && 0==fsl_strcmp(zName, "branch")){
      rc = fsl_buffer_appendf(comment,
                              " Move to branch %s"
                              "[/timeline?r=%h&nd&dp=%.*s | %h].",
                              zValue, zValue, uuidLen, zUuid, zValue);
      branchMove = 1;
    }else if( isProp && fsl_strcmp(zName, "bgcolor")==0 ){
      rc = fsl_buffer_appendf(comment,
                              " Change branch background color to \"%h\".", zValue);
    }else if( isAdd && fsl_strcmp(zName, "bgcolor")==0 ){
      rc = fsl_buffer_appendf(comment,
                              " Change background color to \"%h\".", zValue);
    }else if( isCancel && fsl_strcmp(zName, "bgcolor")==0 ){
      rc = fsl_buffer_appendf(comment, " Cancel background color.");
    }else if( isAdd && fsl_strcmp(zName, "comment")==0 ){
      rc = fsl_buffer_appendf(comment, " Edit check-in comment.");
    }else if( isAdd && fsl_strcmp(zName, "user")==0 ){
      rc = fsl_buffer_appendf(comment, " Change user to \"%h\".", zValue);
    }else if( isAdd && fsl_strcmp(zName, "date")==0 ){
      rc = fsl_buffer_appendf(comment, " Timestamp %h.", zValue);
    }else if( isCancel && memcmp(zName, "sym-",4)==0 ){
      if( !branchMove ){
        rc = fsl_buffer_appendf(comment, " Cancel tag %h.", zName+4);
      }
    }else if( isProp && memcmp(zName, "sym-",4)==0 ){
      if( !branchMove ){
        rc = fsl_buffer_appendf(comment, " Add propagating tag \"%h\".", zName+4);
      }
    }else if( isAdd && memcmp(zName, "sym-",4)==0 ){
      rc = fsl_buffer_appendf(comment, " Add tag \"%h\".", zName+4);
    }else if( isCancel && memcmp(zName, "sym-",4)==0 ){
      rc = fsl_buffer_appendf(comment, " Cancel tag \"%h\".", zName+4);
    }else if( isAdd && fsl_strcmp(zName, "closed")==0 ){
      rc = fsl_buffer_append(comment, " Marked \"Closed\"", -1);
      if( !rc && zValue && *zValue ){
        rc = fsl_buffer_appendf(comment, " with note \"%h\"", zValue);
      }
      if(!rc) rc = fsl_buffer_append(comment, ".", 1);
    }else if( isCancel && fsl_strcmp(zName, "closed")==0 ){
      rc = fsl_buffer_append(comment, " Removed the \"Closed\" mark", -1);
      if( !rc && zValue && *zValue ){
        rc = fsl_buffer_appendf(comment, " with note \"%h\"", zValue);
      }
      if(!rc) rc = fsl_buffer_append(comment, ".", 1);
    }else {
      if( isCancel ){
        rc = fsl_buffer_appendf(comment, " Cancel \"%h\"", zName);
      }else if( isAdd ){
        rc = fsl_buffer_appendf(comment, " Add \"%h\"", zName);
      }else{
        assert(isProp);
        rc = fsl_buffer_appendf(comment, " Add propagating \"%h\"", zName);
      }
      if(rc) goto end;
      if( zValue && zValue[0] ){
        rc = fsl_buffer_appendf(comment, " with value \"%h\".", zValue);
      }else{
        rc = fsl_buffer_append(comment, ".", 1);
      }
    }
  } /* foreach tag loop */
  if(!rc){
    /* TODO: cached statement */
    rc = fsl_db_exec(db,
                     "REPLACE INTO event"
                     "(type,mtime,objid,user,comment) "
                     "VALUES('g',"
                     "%"FSL_JULIAN_T_PFMT","
                     "%"FSL_ID_T_PFMT","
                     "%Q,%Q)",
                     mtime, d->rid, d->U,
                     (comment->used>1)
                     ? (fsl_buffer_cstr(comment)
                        +1/*leading space on all entries*/)
                     : NULL);
  }

  end:
  fsl__cx_scratchpad_yield(d->f, comment);
  return rc;

}

static int fsl_deck_xlink_f_forum(fsl_deck * const d, void * state){
  if(FSL_SATYPE_FORUMPOST!=d->type) return 0;
  int rc = 0;
  fsl_db * const db = fsl_cx_db_repo(d->f);
  assert(db);
  fsl_cx * const f = d->f;
  fsl_id_t const froot = d->G ? fsl_uuid_to_rid(f, d->G) : d->rid;
  fsl_id_t const fprev = d->P.used ? fsl_uuid_to_rid(f, (char const *)d->P.list[0]): 0;
  fsl_id_t const firt = d->I ? fsl_uuid_to_rid(f, d->I) : 0;
  if( 0==firt ){
    /* This is the start of a new thread, either the initial entry
    ** or an edit of the initial entry. */
    const char * zTitle = d->H;
    const char * zFType;
    if(!zTitle || !*zTitle){
      zTitle = "(Deleted)";
    }
    zFType = fprev ? "Edit" : "Post";
    /* FSL-MISSING:
       assert( manifest_event_triggers_are_enabled ); */
    rc = fsl_db_exec_multi(db,
        "REPLACE INTO event(type,mtime,objid,user,comment)"
        "VALUES('f',%"FSL_JULIAN_T_PFMT",%" FSL_ID_T_PFMT
        ",%Q,'%q: %q')",
        d->D, d->rid, d->U, zFType, zTitle);
    if(rc) goto dberr;
      /*
      ** If this edit is the most recent, then make it the title for
      ** all other entries for the same thread
      */
    if( !fsl_db_exists(db,"SELECT 1 FROM forumpost "
                       "WHERE froot=%" FSL_ID_T_PFMT " AND firt=0"
                       " AND fpid!=%" FSL_ID_T_PFMT
                       " AND fmtime>%"FSL_JULIAN_T_PFMT,
                       froot, d->rid, d->D)){
        /* This entry establishes a new title for all entries on the thread */
      rc = fsl_db_exec_multi(db,
          "UPDATE event"
          " SET comment=substr(comment,1,instr(comment,':')) || ' %q'"
          " WHERE objid IN (SELECT fpid FROM forumpost WHERE froot=% " FSL_ID_T_PFMT ")",
          zTitle, froot);
      if(rc) goto dberr;
    }
  }else{
      /* This is a reply to a prior post.  Take the title from the root. */
    char const * zFType = 0;
    char * zTitle = fsl_db_g_text(
           db, 0, "SELECT substr(comment,instr(comment,':')+2)"
           "  FROM event WHERE objid=%"FSL_ID_T_PFMT, froot);
    if( zTitle==0 ){
      zTitle = fsl_strdup("<i>Unknown</i>");
      if(!zTitle){
        rc = FSL_RC_OOM;
        goto end;
      }
    }
    if( !d->W.used ){
      zFType = "Delete reply";
    }else if( fprev ){
      zFType = "Edit reply";
    }else{
      zFType = "Reply";
    }
    /* FSL-MISSING:
       assert( manifest_event_triggers_are_enabled ); */
    rc = fsl_db_exec_multi(db,
        "REPLACE INTO event(type,mtime,objid,user,comment)"
        "VALUES('f',%"FSL_JULIAN_T_PFMT
        ",%"FSL_ID_T_PFMT",%Q,'%q: %q')",
        d->D, d->rid, d->U, zFType, zTitle);
    fsl_free(zTitle);
    if(rc) goto end;
    if( d->W.used ){
      /* FSL-MISSING:
         backlink_extract(&d->W, d->N, d->rid, BKLNK_FORUM, d->D, 1); */
    }
  }
  end:
  return rc;
  dberr:
  assert(rc);
  assert(db->error.code);
  return fsl_cx_uplift_db_error(f, db);
}


static int fsl_deck_xlink_f_technote(fsl_deck * const d, void * state){
  if(FSL_SATYPE_TECHNOTE!=d->type) return 0;
  char buf[FSL_STRLEN_K256 + 7 /* event-UUID\0 */] = {0};
  fsl_id_t tagid;
  char const * zTag;
  int rc = 0;
  fsl_cx * const f = d->f;
  fsl_db * const db = fsl_cx_db_repo(d->f);
  fsl_snprintf(buf, sizeof(buf), "event-%s", d->E.uuid);
  zTag = buf;
  tagid = fsl_tag_id( f, zTag, 1 );
  if(tagid<=0){
    return f->error.code ? f->error.code :
      fsl_cx_err_set(f, FSL_RC_RANGE,
                     "Got unexpected RID (%"FSL_ID_T_PFMT") "
                     "for tag [%s].",
                     tagid, zTag);
  }
  fsl_id_t const subsequent
    = fsl_db_g_id(db, 0,
                  "SELECT rid FROM tagxref"
                  " WHERE tagid=%"FSL_ID_T_PFMT
                  " AND mtime>=%"FSL_JULIAN_T_PFMT
                  " AND rid!=%"FSL_ID_T_PFMT
                  " ORDER BY mtime",
                  tagid, d->D, d->rid);
  if(subsequent<0){
    rc = fsl_cx_uplift_db_error(d->f, db);
  }else{
    rc = fsl_db_exec(db,
                     "REPLACE INTO event("
                     "type,mtime,"
                     "objid,tagid,"
                     "user,comment,bgcolor"
                     ")VALUES("
                     "'e',%"FSL_JULIAN_T_PFMT","
                     "%"FSL_ID_T_PFMT",%"FSL_ID_T_PFMT","
                     "%Q,%Q,"
                     "  (SELECT value FROM tagxref WHERE "
                     "   tagid=%d"
                     "   AND rid=%"FSL_ID_T_PFMT")"
                     ");",
                     d->E.julian, d->rid, tagid,
                     d->U, d->C, 
                     (int)FSL_TAGID_BGCOLOR, d->rid);
  }
  return rc;
}

static int fsl_deck_xlink_f_wiki(fsl_deck * const d, void * state){
  if(FSL_SATYPE_WIKI!=d->type) return 0;
  int rc;
  char const * zWiki;
  fsl_size_t nWiki = 0;
  char cPrefix = 0;
  char * zTag = fsl_mprintf("wiki-%s", d->L);
  if(!zTag) return FSL_RC_OOM;
  fsl_id_t const tagid = fsl_tag_id( d->f, zTag, 1 );
  if(tagid<=0){
    rc = fsl_cx_err_set(d->f, FSL_RC_ERROR,
                        "Tag [%s] must have been added by main wiki crosslink step.",
                        zTag);
    goto end;
  }
  /* Some of this is duplicated in the main wiki crosslinking code :/. */
  zWiki = d->W.used ? fsl_buffer_cstr(&d->W) : "";
  while( *zWiki && fsl_isspace(*zWiki) ){
    ++zWiki;
    /* Historical behaviour: strip leading spaces. */
  }
  /* As of late 2020, fossil changed the conventions for how wiki
     entries are to be added to the timeline. They requrie a prefix
     character which tells the timeline display and email notification
     generator code what type of change this is: create/update/delete */
  nWiki = fsl_strlen(zWiki);
  if(!nWiki) cPrefix = '-';
  else if( !d->P.used ) cPrefix = '+';
  else cPrefix = ':';
  fsl_db * const db = fsl_cx_db_repo(d->f);
  rc = fsl_db_exec(db,
                   "REPLACE INTO event(type,mtime,objid,user,comment) "
                   "VALUES('w',%"FSL_JULIAN_T_PFMT
                   ",%"FSL_ID_T_PFMT",%Q,'%c%q%q%q');",
                   d->D, d->rid, d->U, cPrefix, d->L,
                   ((d->C && *d->C) ? ": " : ""),
                   ((d->C && *d->C) ? d->C : ""));
  /* Note that wiki pages optionally support d->C (change comment),
     but it's historically unused because it was a late addition to
     the artifact format and is not supported by older fossil
     versions. */
  rc = fsl_cx_uplift_db_error2(d->f, db, rc);
  end:
  fsl_free(zTag);
  return rc;
}


/** @internal

    Installs the core overridable crosslink listeners. "The plan" is
    to do all updates to the event (timeline) table via these
    crosslinkers and perform the core, UI-agnostic, crosslinking bits
    in the internal fsl__deck_crosslink_XXX() functions. That should
    allow clients to override how the timeline is updated without
    requiring them to understand the rest of the required schema
    updates.
*/
int fsl__cx_install_timeline_crosslinkers(fsl_cx * const f){
  int rc;
  assert(!f->xlinkers.used);
  assert(!f->xlinkers.list);
  rc = fsl_xlink_listener(f, "fsl/attachment/timeline",
                          fsl_deck_xlink_f_attachment, 0);
  if(!rc) rc = fsl_xlink_listener(f, "fsl/checkin/timeline",
                          fsl_deck_xlink_f_checkin, 0);
  if(!rc) rc = fsl_xlink_listener(f, "fsl/control/timeline",
                          fsl_deck_xlink_f_control, 0);
  if(!rc) rc = fsl_xlink_listener(f, "fsl/forumpost/timeline",
                          fsl_deck_xlink_f_forum, 0);
  if(!rc) rc = fsl_xlink_listener(f, "fsl/technote/timeline",
                          fsl_deck_xlink_f_technote, 0);
  if(!rc) rc = fsl_xlink_listener(f, "fsl/wiki/timeline",
                          fsl_deck_xlink_f_wiki, 0);
  return rc;
}


static int fsl__deck_crosslink_checkin(fsl_deck * const d,
                                      fsl_id_t *parentid ){
  int rc = 0;
  fsl_cx * const f = d->f;
  fsl_db * const db = fsl_cx_db_repo(f);

  /* TODO: convert these queries to cached statements, for
     the sake of rebuild and friends. And bind() doubles
     instead of %FSL_JULIAN_T_PFMT'ing them.
  */
  if(d->Q.used && fsl_db_table_exists(db, FSL_DBROLE_REPO,
                                      "cherrypick")){
    fsl_size_t i;
    for(i=0; i < d->Q.used; ++i){
      fsl_card_Q const * q = (fsl_card_Q const *)d->Q.list[i];
      rc = fsl_db_exec(db,
          "REPLACE INTO cherrypick(parentid,childid,isExclude)"
          " SELECT rid, %"FSL_ID_T_PFMT", %d"
          " FROM blob WHERE uuid=%Q",
          d->rid, q->type<0 ? 1 : 0, q->target
      );
      if(rc) goto end;
    }
  }
  if(!fsl_repo_has_mlink_mid(db, d->rid)){
    rc = fsl_deck_add_checkin_linkages(d, parentid);
    if(rc) goto end;
    /* FSL-MISSING:
       assert( manifest_event_triggers_are_enabled ); */
    rc = fsl__search_doc_touch(f, d->type, d->rid, 0);
    if(rc) goto end;
    /* If this is a delta-manifest, record the fact that this repository
       contains delta manifests, to free the "commit" logic to generate
       new delta manifests. */
    if(d->B.uuid){
      rc = fsl__cx_update_seen_delta_deck(f);
      if(rc) goto end;
    }
    assert(!rc);
  }/*!exists mlink*/
  end:
  if(rc && !f->error.code && db->error.code){
    fsl_cx_uplift_db_error(f, db);
  }
  return rc;
}

static int fsl__deck_crosslink_wiki(fsl_deck *d){
  char zLength[40] = {0};
  fsl_id_t prior = 0;
  char const * zWiki;
  fsl_size_t nWiki = 0;
  int rc;
  char * zTag = fsl_mprintf("wiki-%s", d->L);
  fsl_cx * const f = d->f;
  fsl_db * const db = fsl_cx_db_repo(f);
  if(!zTag){
    return FSL_RC_OOM;
  }
  assert(f && db);
  zWiki = d->W.used ? fsl_buffer_cstr(&d->W) : "";
  while( *zWiki && fsl_isspace(*zWiki) ){
    ++zWiki;
    /* Historical behaviour: strip leading spaces. */
  }
  nWiki = fsl_strlen(zWiki)
    /* Reminder: use strlen instead of d->W.used just in case that
       one contains embedded NULs in the content. "Shouldn't
       happen," but the API doesn't explicitly prohibit it.
    */;
  fsl_snprintf(zLength, sizeof(zLength), "%"FSL_SIZE_T_PFMT,
               (fsl_size_t)nWiki);
  rc = fsl__tag_insert(f, FSL_TAGTYPE_ADD, zTag, zLength,
                      d->rid, d->D, d->rid, NULL );
  if(rc) goto end;
  if(d->P.used){
    prior = fsl_uuid_to_rid(f, (const char *)d->P.list[0]);
  }
  if(prior>0){
    rc = fsl__content_deltify(f, prior, d->rid, 0);
    if(rc) goto end;
  }
  rc = fsl__search_doc_touch(f, d->type, d->rid, d->L);
  if(rc) goto end;
  if( f->cache.isCrosslinking ){
    rc = fsl__deck_crosslink_add_pending(f, 'w',d->L);
    if(rc) goto end;
  }else{
    /* FSL-MISSING:
       backlink_wiki_refresh(d->L); */
  }
  assert(0==rc);
  rc = fsl__deck_crosslink_fwt_plink(d);
  end:
  fsl_free(zTag);
  return rc;
}

static int fsl__deck_crosslink_attachment(fsl_deck * const d){
  int rc;
  fsl_cx * const f = d->f;
  fsl_db * const db = fsl_cx_db_repo(f);

  rc = fsl_db_exec(db,
                   /* REMINDER: fossil(1) uses INSERT here, but that
                      breaks libfossil crosslinking tests due to a
                      unique constraint violation on attachid. */
                   "REPLACE INTO attachment(attachid, mtime, src, target,"
                   "filename, comment, user) VALUES("
                   "%"FSL_ID_T_PFMT",%"FSL_JULIAN_T_PFMT","
                   "%Q,%Q,%Q,"
                   "%Q,%Q);",
                   d->rid, d->D,
                   d->A.src, d->A.tgt, d->A.name,
                   (d->C ? d->C : ""), d->U);
  if(!rc){
    rc = fsl_db_exec(db,
                   "UPDATE attachment SET isLatest = (mtime=="
                   "(SELECT max(mtime) FROM attachment"
                   "  WHERE target=%Q AND filename=%Q))"
                   " WHERE target=%Q AND filename=%Q",
                   d->A.tgt, d->A.name,
                   d->A.tgt, d->A.name);
  }
  return rc;  
}

static int fsl__deck_crosslink_cluster(fsl_deck * const d){
  /* Clean up the unclustered table... */
  fsl_size_t i;
  fsl_stmt * st = NULL;
  int rc;
  fsl_cx * const f = d->f;
  fsl_db * const db = fsl_cx_db_repo(f);

  assert(d->rid>0);
  rc = fsl__tag_insert(f, FSL_TAGTYPE_ADD, "cluster", NULL,
                       d->rid, d->D, d->rid, NULL);
  if(rc) return rc;
  
  rc = fsl_db_prepare_cached(db, &st,
                             "DELETE FROM unclustered WHERE rid=?"
                             "/*%s()*/",__func__);
  if(rc) return fsl_cx_uplift_db_error(f, db);
  assert(st);
  for( i = 0; i < d->M.used; ++i ){
    fsl_id_t mid;
    char const * uuid = (char const *)d->M.list[i];
    mid = fsl_uuid_to_rid(f, uuid);
    if(mid>0){
      fsl_stmt_bind_id(st, 1, mid);
      if(FSL_RC_STEP_DONE!=fsl_stmt_step(st)){
        rc = fsl_cx_uplift_db_error(f, db);
        break;
      }
      fsl_stmt_reset(st);
    }
  }
  fsl_stmt_cached_yield(st);
  return rc;
}

#if 0
static int fsl__deck_crosslink_control(fsl_deck *const d){
}
#endif

static int fsl__deck_crosslink_forum(fsl_deck * const d){
  int rc = 0;
  fsl_cx * const f = d->f;
  rc = fsl_repo_install_schema_forum(f);
  if(rc) return rc;
  fsl_db * const db = fsl_cx_db_repo(f);
  fsl_id_t const froot = d->G ? fsl_uuid_to_rid(f, d->G) : d->rid;
  fsl_id_t const fprev = d->P.used ? fsl_uuid_to_rid(f, (char const *)d->P.list[0]): 0;
  fsl_id_t const firt = d->I ? fsl_uuid_to_rid(f, d->I) : 0;
  assert(f && db);
  rc = fsl_db_exec_multi(db,
      "REPLACE INTO forumpost(fpid,froot,fprev,firt,fmtime)"
      "VALUES(%" FSL_ID_T_PFMT ",%" FSL_ID_T_PFMT ","
      "nullif(%" FSL_ID_T_PFMT ",0),"
      "nullif(%" FSL_ID_T_PFMT ",0),%"FSL_JULIAN_T_PFMT")",
      d->rid, froot, fprev, firt, d->D
  );
  rc = fsl_cx_uplift_db_error2(f, db, rc);
  if(!rc){
    rc = fsl__search_doc_touch(f, d->type, d->rid, 0);
  }
  if(!rc){
    rc = fsl__deck_crosslink_fwt_plink(d);
  }
  return rc;
}

static int fsl__deck_crosslink_technote(fsl_deck * const d){
  char buf[FSL_STRLEN_K256 + 7 /* event-UUID\0 */] = {0};
  char zLength[40] = {0};
  fsl_id_t tagid;
  fsl_id_t prior = 0, subsequent;
  char const * zWiki;
  char const * zTag;
  fsl_size_t nWiki = 0;
  int rc;
  fsl_cx * const f = d->f;
  fsl_db * const db = fsl_cx_db_repo(f);
  fsl_snprintf(buf, sizeof(buf), "event-%s", d->E.uuid);
  zTag = buf;
  tagid = fsl_tag_id( f, zTag, 1 );
  if(tagid<=0){
    rc = f->error.code ? f->error.code :
      fsl_cx_err_set(f, FSL_RC_RANGE,
                     "Got unexpected RID (%"FSL_ID_T_PFMT") "
                     "for tag [%s].",
                     tagid, zTag);
    goto end;
  }
  zWiki = d->W.used ? fsl_buffer_cstr(&d->W) : "";
  while( *zWiki && fsl_isspace(*zWiki) ){
    ++zWiki;
    /* Historical behaviour: strip leading spaces. */
  }
  nWiki = fsl_strlen(zWiki);
  fsl_snprintf( zLength, sizeof(zLength), "%"FSL_SIZE_T_PFMT,
                (fsl_size_t)nWiki);
  rc = fsl__tag_insert(f, FSL_TAGTYPE_ADD, zTag, zLength,
                      d->rid, d->D, d->rid, NULL );
  if(rc) goto end;
  if(d->P.used){
    prior = fsl_uuid_to_rid(f, (const char *)d->P.list[0]);
    if(prior<0){
      assert(f->error.code);
      rc = f->error.code;
      goto end;
    }
  }
  subsequent = fsl_db_g_id(db, 0,
  /* BUG: see:
     https://fossil-scm.org/forum/forumpost/c58fd8de53 */
                           "SELECT rid FROM tagxref"
                           " WHERE tagid=%"FSL_ID_T_PFMT
                           " AND mtime>=%"FSL_JULIAN_T_PFMT
                           " AND rid!=%"FSL_ID_T_PFMT
                           " ORDER BY mtime",
                           tagid, d->D, d->rid);
  if(subsequent<0){
    assert(db->error.code);
    rc = fsl_cx_uplift_db_error(f, db);
    goto end;
  }
  else if( prior > 0 ){
    rc = fsl__content_deltify(f, prior, d->rid, 0);
    if( !rc && !subsequent ){
      rc = fsl_db_exec(db,
                       "DELETE FROM event"
                       " WHERE type='e'"
                       "   AND tagid=%"FSL_ID_T_PFMT
                       "   AND objid IN"
                       " (SELECT rid FROM tagxref "
                       " WHERE tagid=%"FSL_ID_T_PFMT")",
                       tagid, tagid);
    }
  }
  if(rc) goto end;
  if( subsequent>0 ){
    rc = fsl__content_deltify(f, d->rid, subsequent, 0);
  }else{
    /* timeline update is deferred to another crosslink
       handler */
    rc = fsl__search_doc_touch(f, d->type, d->rid, 0);
    /* FSL-MISSING:
       assert( manifest_event_triggers_are_enabled ); */
  }
  if(!rc){
    rc = fsl__deck_crosslink_fwt_plink(d);
  }
  end:
  return rc;
}

static int fsl__deck_crosslink_ticket(fsl_deck * const d){
  int rc;
  fsl_cx * const f = d->f;
  char * zTag;
  fsl_stmt qAtt = fsl_stmt_empty;
  assert(f->cache.isCrosslinking
         && "This only works if fsl__crosslink_begin() is active.");
  zTag = fsl_mprintf("tkt-%s", d->K);
  if(!zTag) return FSL_RC_OOM;
  rc = fsl__tag_insert(f, FSL_TAGTYPE_ADD, zTag, NULL,
                       d->rid, d->D, d->rid, NULL);
  fsl_free(zTag);
  if(rc) goto end;
  assert(d->K);
  rc = fsl__deck_crosslink_add_pending(f, 't', d->K);
  if(rc) goto end;;
  /* Locate and update comment for any attachments */
  rc = fsl_cx_prepare(f, &qAtt,
                      "SELECT attachid, src, target, filename FROM attachment"
                      " WHERE target=%Q",
                      d->K);
  while(0==rc && FSL_RC_STEP_ROW==fsl_stmt_step(&qAtt)){
    const char *zAttachId = fsl_stmt_g_text(&qAtt, 0, NULL);
    const char *zSrc = fsl_stmt_g_text(&qAtt, 1, NULL);
    const char *zTarget = fsl_stmt_g_text(&qAtt, 2, NULL);
    const char *zName = fsl_stmt_g_text(&qAtt, 3, NULL);
    const bool isAdd = (zSrc && zSrc[0]) ? true : false;
    char *zComment;
    if( isAdd ){
      zComment =
        fsl_mprintf(
                    "Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]",
                    zSrc, zName, zTarget, zTarget);
    }else{
      zComment =
        fsl_mprintf("Delete attachment \"%h\" from ticket [%!S|%S]",
                    zName, zTarget, zTarget);
    }
    if(!zComment){
      rc = FSL_RC_OOM;
      break;
    }
    rc = fsl_cx_exec_multi(f, "UPDATE event SET comment=%Q, type='t'"
                           " WHERE objid=%Q",
                           zComment, zAttachId);
    fsl_free(zComment);
  }
  end:
  fsl_stmt_finalize(&qAtt);
  return rc;
}

int fsl__deck_crosslink_one( fsl_deck * const d ){
  int rc;
  assert(d->f && "API misuse:fsl_deck::f == NULL");
  rc = fsl__crosslink_begin(d->f);
  if(rc) return rc;
  rc = fsl__deck_crosslink(d);
  assert(0!=fsl_db_transaction_level(fsl_cx_db_repo(d->f))
         && "Expecting transaction level from fsl__crosslink_begin()");
  rc = fsl__crosslink_end(d->f, rc);
  return rc;
}

int fsl__deck_crosslink( fsl_deck /* const */ * const d ){
  int rc = 0;
  fsl_cx * f = d->f;
  fsl_db * db = f ? fsl_needs_repo(f) : NULL;
  fsl_id_t parentid = 0;
  fsl_int_t const rid = d->rid;
  if(!f) return FSL_RC_MISUSE;
  else if(rid<=0){
    return fsl_cx_err_set(f, FSL_RC_RANGE,
                          "Invalid RID for crosslink: %"FSL_ID_T_PFMT,
                          rid);
  }
  else if(!db) return FSL_RC_NOT_A_REPO;
  else if(!fsl_deck_has_required_cards(d)){
    assert(d->f->error.code);
    return d->f->error.code;
  }else if(f->cache.xlinkClustersOnly && (FSL_SATYPE_CLUSTER!=d->type)){
    /* is it okay to bypass the registered xlink listeners here?  The
       use case called for by this is not yet implemented in
       libfossil. */
    return 0;
  }
  rc = fsl_db_transaction_begin(db);
  if(rc) goto end;
  if(FSL_SATYPE_CHECKIN==d->type
     && d->B.uuid && !d->B.baseline){
    rc = fsl_deck_baseline_fetch(d);
    if(rc) goto end;
    assert(d->B.baseline);
  }
  switch(d->type){
    case FSL_SATYPE_CHECKIN:
      rc = fsl__deck_crosslink_checkin(d, &parentid);
      break;
    case FSL_SATYPE_CLUSTER:
      rc = fsl__deck_crosslink_cluster(d);
      break;
    default:
      break;
  }
  if(rc) goto end;
  switch(d->type){
    case FSL_SATYPE_CONTROL:
    case FSL_SATYPE_CHECKIN:
    case FSL_SATYPE_TECHNOTE:
      rc = fsl__deck_crosslink_apply_tags(f, d, db, rid, parentid);
      break;
    default:
      break;
  }
  if(rc) goto end;
  switch(d->type){
    case FSL_SATYPE_WIKI:
      rc = fsl__deck_crosslink_wiki(d);
      break;
    case FSL_SATYPE_FORUMPOST:
      rc = fsl__deck_crosslink_forum(d);
      break;
    case FSL_SATYPE_TECHNOTE:
      rc = fsl__deck_crosslink_technote(d);
      break;
    case FSL_SATYPE_TICKET:
      rc = fsl__deck_crosslink_ticket(d);
      break;
    case FSL_SATYPE_ATTACHMENT:
      rc = fsl__deck_crosslink_attachment(d);
      break;
    /* FSL_SATYPE_CONTROL is handled above except for the timeline
       update, which is handled by a callback below */
    default:
      break;
  }
  /* Call any crosslink callbacks... */
  if(0==rc && FSL_SATYPE_TICKET!=d->type){
    /* ^^^ the real work of ticket crosslinking is delated until
       fsl__crosslink_end(), for reasons lost to history, so we'll skip
       calling the xlink listeners for those until that step. */
    rc = fsl__call_xlink_listeners(d);
  }
  end:
  if(!rc){
    rc = fsl_db_transaction_end(db, false);
  }else{
    if(db->error.code && !f->error.code){
      fsl_cx_uplift_db_error(f,db);
    }
    fsl_db_transaction_end(db, true);
  }
  return rc ? rc : f->interrupted;
}/*end fsl__deck_crosslink()*/


/**
    Return true if z points to the first character after a blank line.
    Tolerate either \r\n or \n line endings. As this looks backwards
    in z, z must point to at least 3 characters past the beginning of
    a legal string.
 */
static bool fsl_after_blank_line(const char *z){
  if( z[-1]!='\n' ) return false;
  if( z[-2]=='\n' ) return true;
  if( z[-2]=='\r' && z[-3]=='\n' ) return true;
  return false;
}

/**
    Verifies that ca points to at least 35 bytes of memory
    which hold (at the end) a Z card and its hash value.
   
    Returns 0 if the string does not contain a Z card,
    a positive value if it can validate the Z card's hash,
    and a negative value on hash mismatch.
*/
static int fsl_deck_verify_Z_card(unsigned char const * ca, fsl_size_t n){
  if( n<35 ) return 0;
  if( ca[n-35]!='Z' || ca[n-34]!=' ' ) return 0;
  else{
    unsigned char digest[16];
    char hex[FSL_STRLEN_MD5+1];
    unsigned char const * zHash = ca+n-FSL_STRLEN_MD5-1;
    fsl_md5_cx md5 = fsl_md5_cx_empty;
    unsigned char const * zHashEnd =
      ca + n -
      2 /* 'Z ' */
      - FSL_STRLEN_MD5
      - 1 /* \n */;
    assert( 'Z' == (char)*zHashEnd );
    fsl_md5_update(&md5, ca, zHashEnd-ca);
    fsl_md5_final(&md5, digest);
    fsl_md5_digest_to_base16(digest, hex);
    return (0==memcmp(zHash, hex, FSL_STRLEN_MD5))
      ? 1
      : -1;
  }
}


/** @internal

    Remove the PGP signature from a raw artifact, if there is one.

    Expects *pz to point to *pn bytes of string memory which might
    or might not be prefixed by a PGP signature.  If the string is
    enveloped in a signature, then upon returning *pz will point to
    the first byte after the end of the PGP header and *pn will
    contain the length of the content up to, but not including, the
    PGP footer.

    If *pz does not look like a PGP header then this is a no-op.

    Neither pointer may be NULL and *pz must point to *pn bytes of
    valid memory. If *pn is initially less than 59, this is a no-op.
*/
static void fsl__remove_pgp_signature(unsigned char const **pz, fsl_size_t *pn){
  unsigned char const *z = *pz;
  fsl_int_t n = (fsl_int_t)*pn;
  fsl_int_t i;
  if( n<59 || memcmp(z, "-----BEGIN PGP SIGNED MESSAGE-----", 34)!=0 ) return;
  for(i=34; i<n && !fsl_after_blank_line((char const *)(z+i)); i++){}
  if( i>=n ) return;
  z += i;
  *pz = z;
#if 1
  unsigned char const * bps =
    (unsigned char const *)strstr((char const *)z, "\n-----BEGIN PGP SIGNATURE-");
  if(bps){
    n = (fsl_int_t)(bps - z) + 1 /*newline*/;
  }
#else
  n -= i;
  for(i=n-1; i>=0; i--){
    if( z[i]=='\n' && memcmp(&z[i],"\n-----BEGIN PGP SIGNATURE-", 25)==0 ){
      /** valgrind warns on ^^^^ this ^^^^ line:
          Conditional jump or move depends on uninitialised value(s)

          It affects at least 2 artifacts in the libfossil repo:

          240deb757b12bf953b5dbf5c087c80f60ae68934
          f4e5795f9ec7df587756f08ea875c8be259b7917
      */
      n = i+1;
      break;
    }
  }
#endif
  *pn = (fsl_size_t)n;
  return;
}


/**
    Internal helper for parsing manifests. Holds a source file (memory
    range) and gets updated by fsl__deck_next_token() and friends.
*/
struct fsl__src {
  /**
      First char of the next token.
   */
  unsigned char * z;
  /**
      One-past-the-end of the manifest.
   */
  unsigned char * zEnd;
  /**
      True if z points to the start of a new line.
   */
  bool atEol;
};
typedef struct fsl__src fsl__src;
static const fsl__src fsl__src_empty = {NULL,NULL,0};

/**
   Return a pointer to the next token.  The token is zero-terminated.
   Return NULL if there are no more tokens on the current line, but
   the call after that will return non-NULL unless we have reached the
   end of the input. If this function returns non-NULL then *pLen is
   set to the byte length of the new token. Once the end of the input
   is reached, this function always returns NULL.
*/
static unsigned char *fsl__deck_next_token(fsl__src * const p,
                                           fsl_size_t * const pLen){
  if( p->atEol ) return NULL;
  int c;
  unsigned char * z = p->z;
  unsigned char * const zStart = z;
  while( (c=(*z))!=' ' && c!='\n' ) ++z;
  *z = 0;
  p->z = &z[1];
  p->atEol = c=='\n';
  *pLen = z - zStart;
  return zStart;
}

/**
    Return the card-type for the next card. Return 0 if there are no
    more cards or if we are not at the end of the current card.
 */
static unsigned char deck__next_card(fsl__src * const p){
  unsigned char c;
  if( !p->atEol || p->z>=p->zEnd ) return 0;
  c = p->z[0];
  if( p->z[1]==' ' ){
    p->z += 2;
    p->atEol = false;
  }else if( p->z[1]=='\n' ){
    p->z += 2;
    p->atEol = true;
  }else{
    c = 0;
  }
  return c;
}

/**
    Internal helper for fsl_deck_parse(). Expects l to be an array of
    26 entries, representing the letters of the alphabet (A-Z), with a
    value of 0 if the card was not seen during parsing and a value >0
    if it was. Returns the deduced artifact type.  Returns
    FSL_SATYPE_ANY if the result is ambiguous.

    Note that we cannot reliably guess until we've seen at least 3
    cards. 2 cards is enough for most cases but can lead to
    FSL_SATYPE_CHECKIN being prematurely selected in one case.
   
    It should guess right for any legal manifests, but it does not go
    out of its way to detect incomplete/invalid ones.
 */
static fsl_satype_e fsl_deck_guess_type( const int * l ){
#if 0
  /* For parser testing only... */
  int i;
  assert(!l[26]);
  MARKER(("Cards seen during parse:\n"));
  for( i = 0; i < 26; ++i ){
    if(l[i]) putchar('A'+i);
  }
  putchar('\n');
#endif
  /*
     Now look for combinations of cards which will uniquely
     identify any syntactical legal combination of cards.
     
     A larger brain than mine could probably come up with a hash of
     l[] which could determine this in O(1). But please don't
     reimplement this as such unless mere mortals can maintain it -
     any performance gain is insignificant in the context of the
     underlying SCM/db operations.

     Note that the order of these checks is sometimes significant!
  */
#define L(X) l[X-'A']
  if(L('M')) return FSL_SATYPE_CLUSTER;
  else if(L('E')) return FSL_SATYPE_EVENT;
  else if(L('G') || L('H') || L('I')) return FSL_SATYPE_FORUMPOST;
  else if(L('L') || L('W')) return FSL_SATYPE_WIKI;
  else if(L('J') || L('K')) return FSL_SATYPE_TICKET;
  else if(L('A')) return FSL_SATYPE_ATTACHMENT;
  else if(L('B') || L('C') || L('F')
          || L('P') || L('Q') || L('R')) return FSL_SATYPE_CHECKIN;
  else if(L('D') && L('T') && L('U')) return FSL_SATYPE_CONTROL;
#undef L
  return FSL_SATYPE_ANY;
}

bool fsl_might_be_artifact(fsl_buffer const * const src){
  unsigned const char * z = src->mem;
  fsl_size_t n = src->used;
  if(n<36) return false;
  fsl__remove_pgp_signature(&z, &n);
  if(n<36) return false;
  else if(z[0]<'A' || z[0]>'Z' || z[1]!=' '
          || z[n-35]!='Z'
          || z[n-34]!=' '
          || !fsl_validate16((const char *)z+n-33, FSL_STRLEN_MD5)){
    return false;
  }
  return true;
}

int fsl_deck_parse2(fsl_deck * const d, fsl_buffer * const src, fsl_id_t rid){
#ifdef ERROR
#  undef ERROR
#endif
#define ERROR(RC,MSG) do{ rc = (RC); zMsg = (MSG); goto bailout; } while(0)
#define SYNTAX(MSG) ERROR(rc ? rc : FSL_RC_SYNTAX,MSG)
  bool isRepeat = 0/* , hasSelfRefTag = 0 */;
  int rc = 0;
  fsl__src x = fsl__src_empty;
  char const * zMsg = NULL;
  char cType = 0, cPrevType = 0;
  unsigned char * z = src ? src->mem : NULL;
  fsl_size_t tokLen = 0;
  unsigned char * token;
  fsl_size_t n = z ? src->used : 0;
  unsigned char * uuid;
  double ts;
  int cardCount = 0;
  fsl_cx * const f = d->f;
  fsl_error * const err = f ? &f->error : 0;
  int stealBuf = 0 /* gets incremented if we need to steal src->mem. */;
  unsigned nSelfTag = 0 /* number of T cards which refer to '*' (this artifact). */;
  unsigned nSimpleTag = 0 /* number of T cards with "+" prefix */;
  /*
    lettersSeen keeps track of the card letters we have seen so that
    we can then relatively quickly figure out what type of manifest we
    have parsed without having to inspect the card contents. Each
    index records a count of how many of that card we've seen.
  */
  int lettersSeen[27] = {0/*A*/,0,0,0,0,0,0,0,0,0,0,0,0,
                         0,0,0,0,0,0,0,0,0,0,0,0,0/*Z*/,
                         0 /* sentinel element for reasons lost to
                             history but removing it breaks stuff. */};
  if(!f || !z) return FSL_RC_MISUSE;
  if(rid>0 && (fsl__cx_mcache_search2(f, rid, d, FSL_SATYPE_ANY, &rc)
               || rc)){
    if(0==rc){
      assert(d->rid == rid);
      fsl_buffer_clear(src);
    }
    return rc;
  }else if(rid<0){
    return fsl_error_set(err, FSL_RC_RANGE,
                         "Invalid (negative) RID %" FSL_ID_T_PFMT
                         " for %s()", rid, __func__);
  }else if(!*z || !n || ( '\n' != z[n-1]) ){
    /* Every control artifact ends with a '\n' character. Exit early
       if that is not the case for this artifact. */
    return fsl_error_set(err, FSL_RC_SYNTAX, "%s.",
                         n ? "Not terminated with \\n"
                         : "Zero-length input");
  }
  if((0==rid) || fsl_id_bag_contains(&f->cache.mfSeen,rid)){
    isRepeat = 1;
  }else{
    isRepeat = 0;
    rc = fsl_id_bag_insert(&f->cache.mfSeen, rid);
    if(rc){
      assert(FSL_RC_OOM==rc);
      return rc;
    }
  }

  /*
    Verify that the first few characters of the artifact look like a
    control artifact.
  */
  if( !fsl_might_be_artifact(src) ){
    SYNTAX("Content does not look like a structural artifact");
  }

  fsl_deck_clean(d);
  fsl_deck_init(f, d, FSL_SATYPE_ANY);

  /*
    Strip off the PGP signature if there is one. Example of signed
    manifest:

    https://fossil-scm.org/index.html/artifact/28987096ac
  */
  {
    unsigned char const * zz = z;
    fsl__remove_pgp_signature(&zz, &n);
    z = (unsigned char *)zz;
  }

  /* Verify the Z card */
  if( fsl_deck_verify_Z_card(z, n) < 0 ){
    ERROR(FSL_RC_CONSISTENCY, "Z-card checksum mismatch");
  }

  /* legacy: not yet clear if we need this:
     if( !isRepeat ) g.parseCnt[0]++; */

  /*
    Reminder: parsing modifies the input (to simplify the
    tokenization/parsing).

    As of mid-201403, we recycle as much as possible from the source
    buffer.
  */
  /* Now parse, card by card... */
  x.z = z;
  x.zEnd = z+n;
  x.atEol= true;

  /* Parsing helpers... */
#define TOKEN(DEFOS) tokLen=0; token = fsl__deck_next_token(&x,&tokLen);    \
  if(token && tokLen && (DEFOS)) fsl_bytes_defossilize(token, &tokLen)
#define TOKEN_EXISTS(MSG_IF_NOT) if(!token){ SYNTAX(MSG_IF_NOT); }(void)0
#define TOKEN_CHECKHEX(MSG) if(token && (int)tokLen!=fsl_is_uuid((char const *)token))\
  { SYNTAX(MSG); }
#define TOKEN_UUID(CARD) TOKEN_CHECKHEX("Malformed UUID in " #CARD "-card")
#define TOKEN_MD5(ERRMSG) if(!token || FSL_STRLEN_MD5!=(int)tokLen) \
  {SYNTAX(ERRMSG);}
  /**
     Reminder: we do not know the type of the manifest at this point,
     so all of the fsl_deck_add/set() bits below can't do their
     validation. We have to determine at parse-time (or afterwards)
     which type of deck it is based on the cards we've seen. We guess
     the type as early as possible to enable during-parse validation,
     and do a post-parse check for the legality of cards added before
     validation became possible.
   */
  
#define SEEN(CARD) lettersSeen[*#CARD - 'A']
  for( cPrevType=1; !rc && (0 < (cType = deck__next_card(&x)));
       cPrevType = cType ){
    ++cardCount;
    if(cType<cPrevType){
      if(d->E.uuid && 'N'==cType && 'P'==cPrevType){
        /* Workaround for a pair of historical fossil bugs
           which synergized to allow malformed technotes to
           be saved:
           https://fossil-scm.org/home/info/023fddeec4029306 */
      }else{
        SYNTAX("Cards are not in strict lexical order");
      }
    }
    assert(cType>='A' && cType<='Z');
    if(cType>='A' && cType<='Z'){
        ++lettersSeen[cType-'A'];
    }else{
      SYNTAX("Invalid card name");
    }
    switch(cType){
      /*
             A <filename> <target> ?<source>?

         Identifies an attachment to either a wiki page, a ticket, or
         a technote.  <source> is the artifact that is the attachment.
         <source> is omitted to delete an attachment.  <target> is the
         name of a wiki page, technote, or ticket to which that
         attachment is connected.
      */
      case 'A':{
        unsigned char * name, * src;
        if(1<SEEN(A)){
          SYNTAX("Multiple A-cards");
        }
        TOKEN(1);
        TOKEN_EXISTS("Missing filename for A-card");
        name = token;
        if(!fsl_is_simple_pathname( (char const *)name, 0 )){
          SYNTAX("Invalid filename in A-card");
        }          
        TOKEN(1);
        TOKEN_EXISTS("Missing target name in A-card");
        uuid = token;
        TOKEN(0);
        TOKEN_UUID(A);
        src = token;
        d->A.name = (char *)name;
        d->A.tgt = (char *)uuid;
        d->A.src = (char *)src;
        ++stealBuf;
        /*rc = fsl_deck_A_set(d, (char const *)name,
          (char const *)uuid, (char const *)src);*/
        d->type = FSL_SATYPE_ATTACHMENT;
        break;
      }
      /*
            B <uuid>

         A B-line gives the UUID for the baseline of a delta-manifest.
      */
      case 'B':{
        if(d->B.uuid){
          SYNTAX("Multiple B-cards");
        }
        TOKEN(0);
        TOKEN_UUID(B);
        d->B.uuid = (char *)token;
        ++stealBuf;
        d->type = FSL_SATYPE_CHECKIN;
        /* rc = fsl_deck_B_set(d, (char const *)token); */
        break;
      }
      /*
             C <comment>

         Comment text is fossil-encoded.  There may be no more than
         one C line.  C lines are required for manifests, are optional
         for Events and Attachments, and are disallowed on all other
         control files.
      */
      case 'C':{
        if( d->C ){
          SYNTAX("more than one C-card");
        }
        TOKEN(1);
        TOKEN_EXISTS("Missing comment text for C-card");
        /* rc = fsl_deck_C_set(d, (char const *)token, (fsl_int_t)tokLen); */
        d->C = (char *)token;
        ++stealBuf;
        break;
      }
      /*
             D <timestamp>

         The timestamp should be ISO 8601.   YYYY-MM-DDtHH:MM:SS
         There can be no more than 1 D line.  D lines are required
         for all control files except for clusters.
      */
      case 'D':{
#define TOKEN_DATETIME(LETTER,MEMBER)                                     \
        if( d->MEMBER>0.0 ) { SYNTAX("More than one "#LETTER"-card"); } \
        TOKEN(0); \
        TOKEN_EXISTS("Missing date part of "#LETTER"-card"); \
        if(!fsl_str_is_date((char const *)token)){\
          SYNTAX("Malformed date part of "#LETTER"-card"); \
        } \
        if(!fsl_iso8601_to_julian((char const *)token, &ts)){   \
          SYNTAX("Cannot parse date from "#LETTER"-card"); \
        } (void)0

        TOKEN_DATETIME(D,D);
        rc = fsl_deck_D_set(d, ts);
        break;
      }
      /*
             E <timestamp> <uuid>

         An "event" (technote) card that contains the timestamp of the
         event in the format YYYY-MM-DDtHH:MM:SS and a unique
         identifier for the event. The event timestamp is distinct
         from the D timestamp. The D timestamp is when the artifact
         was created whereas the E timestamp is when the specific
         event is said to occur.
      */
      case 'E':{
        TOKEN_DATETIME(E,E.julian);
        TOKEN(0);
        TOKEN_EXISTS("Missing UUID part of E-card");
        TOKEN_UUID(E);
        d->E.julian = ts;
        d->E.uuid = (char *)token;
        ++stealBuf;
        d->type = FSL_SATYPE_EVENT;
        break;
      }
      /*
             F <filename> ?<uuid>? ?<permissions>? ?<old-name>?

         Identifies a file in a manifest.  Multiple F lines are
         allowed in a manifest.  F lines are not allowed in any other
         control file.  The filename and old-name are fossil-encoded.

         In delta manifests, deleted files are denoted by the 1-arg
         form. In baseline manifests, deleted files simply are not in
         the manifest.
      */
      case 'F':{
        char * name;
        char * perms = NULL;
        char * priorName = NULL;
        fsl_fileperm_e perm = FSL_FILE_PERM_REGULAR;
        fsl_card_F * fc = NULL;
        rc = 0;
        if(!d->F.capacity){
          /**
             Basic tests with various repos have shown that the
             approximate number of F-cards in a manifest is roughly
             the manifest size/75. We'll use that as an initial alloc
             size.
          */
          rc = fsl_card_F_list_reserve(&d->F, src->used/75+10);
        }
        TOKEN(0);
        TOKEN_EXISTS("Missing name for F-card");
        name = (char *)token;
        TOKEN(0);
        TOKEN_UUID(F);
        uuid = token;
        TOKEN(0);
        if(token){
          perms = (char *)token;
          switch(*perms){
            case 0:
              /* Some (maybe only 1) ancient fossil(1) artifact(s) have a trailing
                 space which triggers this. e.g.

                 https://fossil-scm.org/home/info/32b480faa3465591b8549bdfd889d62d7a8d16a8
              */
              break;
            case 'w': perm = FSL_FILE_PERM_REGULAR; break;
            case 'x': perm = FSL_FILE_PERM_EXE; break;
            case 'l': perm = FSL_FILE_PERM_LINK; break;
            default:
              /*MARKER(("Unmatched perms string character: %d / %c !", (int)*perms, *perms));*/
              SYNTAX("Invalid perms string character");
          }
          TOKEN(0);
          if(token) priorName = (char *)token;
        }
        fsl_bytes_defossilize( (unsigned char *)name, 0 );
        if(fsl_is_reserved_fn(name, -1)){
          /* Some historical (pre-late-2020) manifests contain files
             they really shouldn't, like _FOSSIL_ and .fslckout.
             Since late 2020, fossil simply skips over these when
             parsing manifests, so we'll do the same. */
          break;
        }
        if(priorName) fsl_bytes_defossilize( (unsigned char *)priorName, 0 );
        fc = rc ? 0 : fsl_card_F_list_push(&d->F);
        if(!fc){
          zMsg = "OOM";
          goto bailout;
        }
        ++stealBuf;
        assert(d->F.used>1
               ? (FSL_CARD_F_LIST_NEEDS_SORT & d->F.flags)
               : 1);
        fc->deckOwnsStrings = true;
        fc->name = name;
        fc->priorName = priorName;
        fc->perm = perm;
        fc->uuid = (fsl_uuid_str)uuid;
        d->type = FSL_SATYPE_CHECKIN;
        break;
      }
      /*
        G <uuid>

        A G-line gives the UUID for the thread root of a forum post.
      */
      case 'G':{
        if(d->G){
          SYNTAX("Multiple G-cards");
        }
        TOKEN(0);
        TOKEN_EXISTS("Missing UUID in G-card");
        TOKEN_UUID(G);
        d->G = (char*)token;
        ++stealBuf;
        d->type = FSL_SATYPE_FORUMPOST;
        break;
      }
     /*
         H <forum post title>

         H text is fossil-encoded.  There may be no more than one H
         line.  H lines are optional for forum posts and are
         disallowed on all other control files.
      */
      case 'H':{
        if( d->H ){
          SYNTAX("more than one H-card");
        }
        TOKEN(1);
        TOKEN_EXISTS("Missing text for H-card");
        d->H = (char *)token;
        ++stealBuf;
        d->type = FSL_SATYPE_FORUMPOST;
        break;
      }
      /*
        I <uuid>

        A I-line gives the UUID for the in-response-to UUID for 
        a forum post.
      */
      case 'I':{
        if(d->I){
          SYNTAX("Multiple I-cards");
        }
        TOKEN(0);
        TOKEN_EXISTS("Missing UUID in I-card");
        TOKEN_UUID(I);
        d->I = (char*)token;
        ++stealBuf;
        d->type = FSL_SATYPE_FORUMPOST;
        break;
      }
      /*
         J <name> ?<value>?

         Specifies a name value pair for ticket.  If the first character
         of <name> is "+" then the <value> is appended to any preexisting
         value.  If <value> is omitted then it is understood to be an
         empty string.
      */
      case 'J':{
        char const * field;
        bool isAppend = 0;
        TOKEN(1);
        TOKEN_EXISTS("Missing field name for J-card");
        field = (char const *)token;
        if('+'==*field){
          isAppend = 1;
          ++field;
        }
        TOKEN(1);
        rc = fsl_deck_J_add(d, isAppend, field,
                            (char const *)token);
        d->type = FSL_SATYPE_TICKET;
        break;
      }
      /*
         K <uuid>

         A K-line gives the UUID for the ticket which this control file
         is amending.
      */
      case 'K':{
        if(d->K){
          SYNTAX("Multiple K-cards");
        }
        TOKEN(0);
        TOKEN_EXISTS("Missing UUID in K-card");
        TOKEN_UUID(K);
        d->K = (char*)token;
        ++stealBuf;
        d->type = FSL_SATYPE_TICKET;
        break;
      }
      /*
         L <wikititle>

         The wiki page title is fossil-encoded.  There may be no more than
         one L line.
      */
      case 'L':{
        if(d->L){
          SYNTAX("Multiple L-cards");
        }
        TOKEN(1);
        TOKEN_EXISTS("Missing text for L-card");
        d->L = (char*)token;
        ++stealBuf;
        d->type = FSL_SATYPE_WIKI;
        break;
      }
      /*
         M <uuid>

         An M-line identifies another artifact by its UUID.  M-lines
         occur in clusters only.
      */
      case 'M':{
        TOKEN(0);
        TOKEN_EXISTS("Missing UUID for M-card");
        TOKEN_UUID(M);
        ++stealBuf;
        d->type = FSL_SATYPE_CLUSTER;
        rc = fsl_list_append(&d->M, token);
        if( !rc && d->M.used>1 &&
            fsl_strcmp((char const *)d->M.list[d->M.used-2],
                       (char const *)token)>=0 ){
          SYNTAX("M-card in the wrong order");
        }
        break;
      }
      /*
         N <uuid>

         An N-line identifies the mimetype of wiki or comment text.
      */
      case 'N':{
        if(1<SEEN(N)){
          SYNTAX("Multiple N-cards");
        }
        TOKEN(0);
        TOKEN_EXISTS("Missing UUID on N-card");
        ++stealBuf;
        d->N = (char *)token;
        break;
      }

      /*
         P <uuid> ...

         Specify one or more other artifacts which are the parents of
         this artifact.  The first parent is the primary parent.  All
         others are parents by merge.
      */
      case 'P':{
        if(1<SEEN(P)){
          SYNTAX("More than one P-card");
        }
        TOKEN(0);
#if 0
        /* The docs all claim that this card does not exist on the first
           manifest, but in fact it does exist but has no UUID,
           which is invalid per all the P-card docs. Skip this
           check (A) for the sake of manifest #1 and (B) because
           fossil(1) does it this way.
        */
        TOKEN_EXISTS("Missing primary parent UUID for P-card");
#endif
        while( token && !rc ){
          TOKEN_UUID(P);
          ++stealBuf;
          rc = fsl_list_append(&d->P, token);
          if(!rc){
            TOKEN(0);
          }
        }
        break;
      }
      /*
         Q (+|-)<uuid> ?<uuid>?

         Specify one or a range of checkins that are cherrypicked into
         this checkin ("+") or backed out of this checkin ("-").
      */
      case 'Q':{
        fsl_cherrypick_type_e qType = FSL_CHERRYPICK_INVALID;
        TOKEN(0);
        TOKEN_EXISTS("Missing target UUID for Q-card");
        switch((char)*token){
          case '-': qType = FSL_CHERRYPICK_BACKOUT; break;
          case '+': qType = FSL_CHERRYPICK_ADD; break;
          default:
            SYNTAX("Malformed target UUID in Q-card");
        }
        assert(qType);
        uuid = ++token; --tokLen;
        TOKEN_UUID(Q);
        TOKEN(0);
        if(token){
          TOKEN_UUID(Q);
        }
        d->type = FSL_SATYPE_CHECKIN;
        rc = fsl_deck_Q_add(d, qType, (char const *)uuid,
                            (char const *)token);
        break;
      }
      /*
         R <md5sum>

         Specify the MD5 checksum over the name and content of all files
         in the manifest.
      */
      case 'R':{
        if(1<SEEN(R)){
          SYNTAX("More than one R-card");
        }
        TOKEN(0);
        TOKEN_EXISTS("Missing MD5 token in R-card");
        TOKEN_MD5("Malformed MD5 token in R-card");
        d->R = (char *)token;
        ++stealBuf;
        d->type = FSL_SATYPE_CHECKIN;
        break;
      }
      /*
         T (+|*|-)<tagname> <uuid> ?<value>?

         Create or cancel a tag or property. The tagname is fossil-encoded.
         The first character of the name must be either "+" to create a
         singleton tag, "*" to create a propagating tag, or "-" to create
         anti-tag that undoes a prior "+" or blocks propagation of of
         a "*".

         The tag is applied to <uuid>. If <uuid> is "*" then the tag is
         applied to the current manifest. If <value> is provided then 
         the tag is really a property with the given value.

         Tags are not allowed in clusters.  Multiple T lines are allowed.
      */
      case 'T':{
        unsigned char * name, * value;
        fsl_tagtype_e tagType = FSL_TAGTYPE_INVALID;
        TOKEN(1);
        TOKEN_EXISTS("Missing name for T-card");
        name = token;
        if( fsl_validate16((char const *)&name[1],
                           fsl_strlen((char const *)&name[1])) ){
          /* Do not allow tags whose names look like a hash */
          SYNTAX("T-card name looks like a hexadecimal hash");
        }
        TOKEN(0);
        TOKEN_EXISTS("Missing UUID on T-card");
        if(fsl_is_uuid_len((int)tokLen)){
          TOKEN_UUID(T);
          uuid = token;
        }else if( 1==tokLen && '*'==(char)*token ){
          /* tag for the current artifact */
          ++nSelfTag;
          uuid = NULL;
        }else{
          SYNTAX("Malformed UUID in T-card");
        }
        TOKEN(1);
        value = token;
        switch(*name){
          case '*': tagType = FSL_TAGTYPE_PROPAGATING; break;
          case '+': tagType = FSL_TAGTYPE_ADD;
            ++nSimpleTag;
            break;
          case '-': tagType = FSL_TAGTYPE_CANCEL; break;
          default: SYNTAX("Malformed tag name");
        }
        ++name /* skip type marker byte */;
        /* Tag order check from:

           https://fossil-scm.org/home/info/55cacfcace

           (It was subsequently made stricter so that the same tag
           type/name/target combination fails.)

           That's difficult to do here until _after_ we add the new
           tag to the list... */
        rc = fsl_deck_T_add(d, tagType, (fsl_uuid_cstr)uuid,
                            (char const *)name,
                            (char const *)value);
        if(0==rc && d->T.used>1){
          fsl_card_T const * tagPrev =
            (fsl_card_T const *)d->T.list[d->T.used-2];
          fsl_card_T const * tagSelf =
            (fsl_card_T const *)d->T.list[d->T.used-1];
          int const cmp = fsl_card_T_cmp(&tagPrev, &tagSelf);
          if(cmp>=0){
            rc = fsl_cx_err_set(d->f, FSL_RC_SYNTAX,
                                "T-cards are not in lexical order: "
                                "%c%s %s %c%s",
                                fsl_tag_prefix_char(tagPrev->type),
                                tagPrev->name,
                                cmp ? ">=" : "==",
                                fsl_tag_prefix_char(tagSelf->type),
                                tagSelf->name);
            goto bailout;
          }
        }
        break;
      }
      /*
         U ?<login>?

         Identify the user who created this control file by their
         login.  Only one U line is allowed.  Prohibited in clusters.
         If the user name is omitted, take that to be "anonymous".
      */
      case 'U':{
        if(d->U) SYNTAX("More than one U-card");
        TOKEN(1);
        if(token){
          /* rc = fsl_deck_U_set( d, (char const *)token, (fsl_int_t)tokLen ); */
          ++stealBuf;
          d->U = (char *)token;
        }else{
          rc = fsl_deck_U_set( d, "anonymous" );
        }
        break;
      }
      /*
             W <size>
        
         The next <size> bytes of the file contain the text of the wiki
         page.  There is always an extra \n before the start of the next
         record.
      */
      case 'W':{
        fsl_size_t wlen;
        if(d->W.used){
          SYNTAX("More than one W-card");
        }
        TOKEN(0);
        TOKEN_EXISTS("Missing size token for W-card");
        wlen = fsl_str_to_size((char const *)token);
        if((fsl_size_t)-1==wlen){
          SYNTAX("Wiki size token is invalid");
        }
        if( (&x.z[wlen+1]) > x.zEnd){
          SYNTAX("Not enough content after W-card");
        }
        //rc = fsl_buffer_append(&d->W, x.z, wlen);
        //if(rc) goto bailout;
        fsl_buffer_external(&d->W, x.z, wlen);
        ++stealBuf;
        x.z += wlen;
        if( '\n' != x.z[0] ){
          SYNTAX("W-card content not \\n terminated");
        }
        x.z[0] = 0;
        ++x.z;
        break;
      }
      /*
             Z <md5sum>

         MD5 checksum on this control file.  The checksum is over all
         lines (other than PGP-signature lines) prior to the current
         line.  This must be the last record.
        
         This card is required for all control file types except for
         Manifest. It is not required for manifest only for historical
         compatibility reasons.
      */
      case 'Z':{
        /* We validated the Z card first. We cannot compare against
           the original blob now because we've modified it.
        */
        goto end;
      }
      default:
        rc = fsl_cx_err_set(f, FSL_RC_SYNTAX,
                            "Unknown card '%c' in manifest",
                            cType);
        goto bailout;
    }/*switch(cType)*/
    if(rc) goto bailout;
  }/* for-each-card */

#if 1
  /* Remove these when we are done porting
     resp. we can avoid these unused-var warnings. */
  if(isRepeat){}
#endif

  end:
  assert(0==rc);
  if(cardCount>2 && FSL_SATYPE_ANY==d->type){
    /* See if we need to guess the type now.
       We need(?) at least two card to ensure that this is
       free of ambiguities. */
    d->type = fsl_deck_guess_type(lettersSeen);
    if(FSL_SATYPE_ANY!=d->type){
      assert(FSL_SATYPE_INVALID!=d->type);
#if 0
      MARKER(("Guessed manifest type with %d cards: %s\n",
                cardCount, fsl_satype_cstr(d->type)));
#endif
    }
  }
  /* Make sure all of the cards we put in it belong to that deck
     type. */
  if( !fsl_deck_check_type(d, cType) ){
    rc = d->f->error.code;
    goto bailout;
  }

  if(FSL_SATYPE_ANY==d->type){
    rc = fsl_cx_err_set(f, FSL_RC_ERROR,
                        "Internal error: could not determine type of "
                        "artifact we just (successfully!) parsed.");
    goto bailout;
  }else {
    /*
      Make sure we didn't pick up any cards which were picked up
      before d->type was guessed and are invalid for the post-guessed
      type.
    */
    int i = 0;
    for( ; i < 27; ++i ){
      if((lettersSeen[i]>0) && !fsl_card_is_legal(d->type, 'A'+i )){
        rc = fsl_cx_err_set(f, FSL_RC_SYNTAX,
                            "Determined during post-parse processing that "
                            "the parsed deck (type %s) contains an illegal "
                            "card type (%c).", fsl_satype_cstr(d->type),
                            'A'+i);
        goto bailout;
      }
    }
  }
  assert(FSL_SATYPE_CHECKIN==d->type ||
         FSL_SATYPE_CLUSTER==d->type ||
         FSL_SATYPE_CONTROL==d->type ||
         FSL_SATYPE_WIKI==d->type ||
         FSL_SATYPE_TICKET==d->type ||
         FSL_SATYPE_ATTACHMENT==d->type ||
         FSL_SATYPE_TECHNOTE==d->type ||
         FSL_SATYPE_FORUMPOST==d->type);
  assert(0==rc);

  /* Additional checks based on artifact type */
  switch( d->type ){
    case FSL_SATYPE_CONTROL: {
      if( nSelfTag ){
        SYNTAX("Self-referential T-card in control artifact");
      }
      break;
    }
    case FSL_SATYPE_TECHNOTE: {
      if( d->T.used!=nSelfTag ){
        SYNTAX("Non-self-referential T-card in technote");
      }else if( d->T.used!=nSimpleTag ){
        SYNTAX("T-card with '*' or '-' in technote");
      }
      break;
    }
    case FSL_SATYPE_FORUMPOST: {
      if( d->H && d->I ){
        SYNTAX("Cannot have I-card and H-card in a forum post");
      }else if( d->P.used>1 ){
        SYNTAX("Too many arguments to P-card");
      }
      break;
    }
    default: break;
  }

  assert(!d->content.mem);
  if(stealBuf>0){
    /* We stashed something which points to src->mem, so we need to
       steal that memory. */
    d->content = *src;
    *src = fsl_buffer_empty;
  }else{
    /* Clearing the source buffer if we don't take it over
       provides more consistency in the public API than _sometimes_
       requiring the client to clear it. */
    fsl_buffer_clear(src);
  }
  d->rid = rid;
  d->F.flags &= ~FSL_CARD_F_LIST_NEEDS_SORT/*we know all cards were read in order*/;
  return 0;

  bailout:
  if(stealBuf>0){
    d->content = *src;
    *src = fsl_buffer_empty;
  }
  assert(0 != rc);
  if(zMsg){
    fsl_error_set(err, rc, "%s", zMsg);
  }
  return rc;
#undef SEEN
#undef TOKEN_DATETIME
#undef SYNTAX
#undef TOKEN_CHECKHEX
#undef TOKEN_EXISTS
#undef TOKEN_UUID
#undef TOKEN_MD5
#undef TOKEN
#undef ERROR
}

int fsl_deck_parse(fsl_deck * const d, fsl_buffer * const src){
  return fsl_deck_parse2(d, src, 0);
}

int fsl_deck_load_rid( fsl_cx * const f, fsl_deck * const d,
                       fsl_id_t rid, fsl_satype_e type ){
  fsl_buffer buf = fsl_buffer_empty;
  int rc = 0;
  if(0==rid) rid = f->ckout.rid;
  if(rid<0){
    return fsl_cx_err_set(f, FSL_RC_RANGE,
                          "Invalid RID for fsl_deck_load_rid(): "
                          "%"FSL_ID_T_PFMT, rid);
  }
  fsl_deck_clean(d);
  d->f = f;
  if(fsl__cx_mcache_search2(f, rid, d, type, &rc) || rc){
    return rc;
  }
  rc = fsl_content_get(f, rid, &buf);
  if(rc) goto end;
#if 0
  MARKER(("fsl_content_get(%d) len=%d =\n%.*s\n",
          (int)rid, (int)buf.used, (int)buf.used, (char const*)buf.mem));
#endif
  fsl_deck_init(f, d, FSL_SATYPE_ANY);
#if 0
  /*
    If we set d->type=type, the parser can fail more
    quickly. However, that failure will bypass our more specific
    reporting of the problem (see below).  As the type mismatch case
    is expected to be fairly rare, we'll leave this out for now, but
    it might be worth considering as a small optimization later on.
  */
  d->type = type /* may help parsing fail more quickly if
                    it's not the type we want.*/;
#endif
  rc = fsl_deck_parse(d, &buf);
  if(!rc){
    if( type!=FSL_SATYPE_ANY && d->type!=type ){
      rc = fsl_cx_err_set(f, FSL_RC_TYPE,
                          "RID %"FSL_ID_T_PFMT" is of type %s, "
                          "but the caller requested type %s.",
                          rid,
                          fsl_satype_cstr(d->type),
                          fsl_satype_cstr(type));
    }else if(d->B.uuid ){
      rc = fsl__cx_update_seen_delta_deck(f);
    }
  }
  end:
  if(0==rc) d->rid = rid;
  fsl_buffer_clear(&buf);
  return rc;
}

int fsl_deck_load_sym( fsl_cx * const f, fsl_deck * const d,
                       char const * symbolicName, fsl_satype_e type ){
  if(!symbolicName || !d) return FSL_RC_MISUSE;
  else{
    fsl_id_t vid = 0;
    int rc = fsl_sym_to_rid(f, symbolicName, type, &vid);
    if(!rc){
      assert(vid>0);
      rc = fsl_deck_load_rid(f, d, vid, type);
    }
    return rc;
  }
}

  
static int fsl_deck_baseline_load( fsl_deck * d ){
  int rc = 0;
  fsl_deck bl = fsl_deck_empty;
  fsl_id_t rid;
  fsl_cx * f = d ? d->f : NULL;
  fsl_db * db = f ? fsl_needs_repo(f) : NULL;
  assert(d->f);
  assert(d);
  if(!d->f) return FSL_RC_MISUSE;
  else if(d->B.baseline || !d->B.uuid) return 0 /* nothing to do! */;
  else if(!db) return FSL_RC_NOT_A_REPO;
#if 0
  else if(d->rid<=0){
    return fsl_cx_err_set(f, FSL_RC_RANGE,
                          "fsl_deck_baseline_load(): "
                          "fsl_deck::rid is not set.");
  }
#endif
  rid = fsl_uuid_to_rid(f, d->B.uuid);
  if(rid<0){
    assert(f->error.code);
    return f->error.code;
  }
  else if(!rid){
    if(d->rid>0){
      fsl_db_exec(db, 
                  "INSERT OR IGNORE INTO orphan(rid, baseline) "
                  "VALUES(%"FSL_ID_T_PFMT",%"FSL_ID_T_PFMT")",
                  d->rid, rid);
    }
    rc = fsl_cx_err_set(f, FSL_RC_RANGE,
                        "Could not find/load baseline manifest [%s], "
                        "parent of manifest rid #%"FSL_ID_T_PFMT".",
                        d->B.uuid, d->rid);
  }else{
    rc = fsl_deck_load_rid(f, &bl, rid, FSL_SATYPE_CHECKIN);
    if(!rc){
      d->B.baseline = fsl_deck_malloc();
      if(!d->B.baseline){
        fsl_deck_clean(&bl);
        rc = FSL_RC_OOM;
      }else{
        void const * allocStampKludge = d->B.baseline->allocStamp;
        *d->B.baseline = bl /* Transfer ownership */;
        d->B.baseline->allocStamp = allocStampKludge /* But we need this intact
                                                        for deallocation to work */;
        assert(f==d->B.baseline->f);
      }
    }else{
      /* bl might be partially populated */
      fsl_deck_finalize(&bl);
    }
  }
  return rc;
}

int fsl_deck_baseline_fetch( fsl_deck * d ){
  return (d->B.baseline || !d->B.uuid)
    ? 0
    : fsl_deck_baseline_load(d);
}

int fsl_deck_F_rewind( fsl_deck * d ){
  int rc = 0;
  d->F.cursor = 0;
  assert(d->f);
  if(d->B.uuid){
    rc = fsl_deck_baseline_fetch(d);
    if(!rc){
      assert(d->B.baseline);
      d->B.baseline->F.cursor = 0;
    }
  }
  return rc;
}

int fsl_deck_F_next( fsl_deck * d, fsl_card_F const ** rv ){
  assert(d);
  assert(d->f);
  assert(rv);
#define FCARD(DECK,NDX) F_at(&(DECK)->F, NDX)
  *rv = NULL;
  if(!d->B.baseline){
    /* Manifest d is a baseline-manifest.  Just scan down the list
       of files. */
    if(d->B.uuid){
      return fsl_cx_err_set(d->f, FSL_RC_MISUSE,
                            "Deck has a B-card (%s) but no baseline "
                            "loaded. Load the baseline before calling "
                            "%s().",
                            d->B.uuid, __func__)
        /* We "could" just load the baseline from here. */;
    }
    if( d->F.cursor < (int32_t)d->F.used ){
      *rv = FCARD(d, d->F.cursor++);
      assert(*rv);
      assert((*rv)->uuid && "Baseline manifest has deleted F-card entry!");
    }
    return 0;
  }else{
    /* Manifest d is a delta-manifest.  Scan the baseline but amend the
       file list in the baseline with changes described by d.
    */
    fsl_deck * const pB = d->B.baseline;
    int cmp;
    while(1){
      if( pB->F.cursor >= (fsl_int_t)pB->F.used ){
        /* We have used all entries out of the baseline.  Return the next
           entry from the delta. */
        if( d->F.cursor < (fsl_int_t)d->F.used ) *rv = FCARD(d, d->F.cursor++);
        break;
      }else if( d->F.cursor >= (fsl_int_t)d->F.used ){
        /* We have used all entries from the delta.  Return the next
           entry from the baseline. */
        if( pB->F.cursor < (fsl_int_t)pB->F.used ) *rv = FCARD(pB, pB->F.cursor++);
        break;
      }else if( (cmp = fsl_strcmp(FCARD(pB,pB->F.cursor)->name,
                                  FCARD(d, d->F.cursor)->name)) < 0){
        /* The next baseline entry comes before the next delta entry.
           So return the baseline entry. */
        *rv = FCARD(pB, pB->F.cursor++);
        break;
      }else if( cmp>0 ){
        /* The next delta entry comes before the next baseline
           entry so return the delta entry */
        *rv = FCARD(d, d->F.cursor++);
        break;
      }else if( FCARD(d, d->F.cursor)->uuid ){
        /* The next delta entry is a replacement for the next baseline
           entry.  Skip the baseline entry and return the delta entry */
        pB->F.cursor++;
        *rv = FCARD(d, d->F.cursor++);
        break;
      }else{
        assert(0==cmp);
        /*
          The next delta entry is a delete of the next baseline entry.
        */
        /* Skip them both.  Repeat the loop to find the next
           non-delete entry. */
        pB->F.cursor++;
        d->F.cursor++;
        continue;
      }      
    }
    return 0;
  }
#undef FCARD
}

int fsl_deck_save( fsl_deck * const d, bool isPrivate ){
  int rc;
  fsl_cx * const f = d->f;
  fsl_db * const db = fsl_needs_repo(f);
  fsl_buffer * const buf = &d->f->cache.fileContent;
  fsl_id_t newRid = 0;
  bool const oldPrivate = f->cache.markPrivate;
  if(!f || !d ) return FSL_RC_MISUSE;
  else if(!db) return FSL_RC_NOT_A_REPO;
  if(d->rid>0){
#if 1
    return 0;
#else
    return fsl_cx_err_set(f, FSL_RC_ALREADY_EXISTS,
                          "Cannot re-save an existing deck, as that could "
                          "lead to inconsistent data.");
#endif
  }
  if(d->B.uuid && fsl_repo_forbids_delta_manifests(f)){
    return fsl_cx_err_set(f, FSL_RC_ACCESS,
                          "This deck is a delta manifest, but this "
                          "repository has disallowed those via the "
                          "forbid-delta-manifests config option.");
  }

  fsl_cx_err_reset(f);
  fsl_size_t const reserveSize =
    (1024 * 10)
    + (50 * d->T.used)
    + (50 * d->M.used)
    + (120 * d->F.used)
    + d->W.used;
  fsl_buffer_reuse(buf);
  rc = fsl_buffer_reserve(buf, reserveSize);
  if(0==rc) rc = fsl_deck_output(d, fsl_output_f_buffer, buf);
  if(rc){
    fsl_buffer_reuse(buf);
    return rc;
  }

  rc = fsl_db_transaction_begin(db);
  if(rc){
    fsl_buffer_reuse(buf);
    return rc;
  }
  if(0){
    MARKER(("Saving deck:\n%s\n", fsl_buffer_cstr(buf)));
  }

  /* Starting here, don't return, use (goto end) instead. */

  f->cache.markPrivate = isPrivate;
  {
    rc = fsl__content_put_ex(f, buf, NULL, 0,
                            0U, isPrivate, &newRid);
    if(rc) goto end;
    assert(newRid>0);
  }

  /* We need d->rid for crosslinking purposes, but will unset it on
     error because its value will no longer be in the db after
     rollback...
  */
  d->rid = newRid;

#if 0
  /* Something to consider: has a parent, deltify the parent. The
     branch operation does this, but it is not yet clear whether that
     is a general pattern for manifests.
  */
  if(d->P.used){
    fsl_id_t pid;
    assert(FSL_SATYPE_CHECKIN == d->type);
    pid = fsl_uuid_to_rid(f, (char const *)d->P.list[0]);
    if(pid>0){
      rc = fsl__content_deltify(f, pid, d->rid, 0);
      if(rc) goto end;
    }
  }
#endif

  if(FSL_SATYPE_WIKI==d->type){
    /* Analog to fossil's wiki.c:wiki_put(): */
    /*
      MISSING:
      fossil's wiki.c:wiki_put() handles the moderation bits.
    */
    if(d->P.used){
      fsl_id_t const pid = fsl_deck_P_get_id(d, 0);
      assert(pid>0);
      if(pid<0){
        assert(f->error.code);
        rc = f->error.code;
        goto end;
      }else if(!pid){
        if(!f->error.code){
          rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                              "Did not find matching RID "
                              "for P-card[0] (%s).",
                              (char const *)d->P.list[0]);
        }
        goto end;
      }
      rc = fsl__content_deltify(f, pid, d->rid, 0);
      if(rc) goto end;
    }
    rc = fsl_db_exec_multi(db,
                           "INSERT OR IGNORE INTO unsent "
                           "VALUES(%"FSL_ID_T_PFMT");"
                           "INSERT OR IGNORE INTO unclustered "
                           "VALUES(%"FSL_ID_T_PFMT");",
                           d->rid, d->rid);
    if(rc){
      fsl_cx_uplift_db_error(f, db);
      goto end;
    }
  }

  rc = f->cache.isCrosslinking
    ? fsl__deck_crosslink(d)
    : fsl__deck_crosslink_one(d);

  end:
  f->cache.markPrivate = oldPrivate;
  if(!rc) rc = fsl_db_transaction_end(db, 0);
  else fsl_db_transaction_end(db, 1);
  if(rc){
    d->rid = 0 /* this blob.rid will be lost after rollback */;
    if(!f->error.code && db->error.code){
      rc = fsl_cx_uplift_db_error(f, db);
    }
  }
  fsl_buffer_reuse(buf);
  return rc;
}

int fsl__crosslink_end(fsl_cx * const f, int resultCode){
  int rc = 0;
  fsl_db * const db = fsl_cx_db_repo(f);
  fsl_stmt q = fsl_stmt_empty;
  fsl_stmt u = fsl_stmt_empty;
  int i;
  assert(f);
  assert(db);
  assert(f->cache.isCrosslinking && "Internal API misuse.");
  if(!f->cache.isCrosslinking){
    fsl__fatal(FSL_RC_MISUSE,
              "Internal API misuse: %s() called while "
              "f->cache.isCrosslinking is false.", __func__);
    return fsl_cx_err_set(f, FSL_RC_MISUSE,
                          "Crosslink is not running.");
  }
  f->cache.isCrosslinking = false;
  if(resultCode){
    assert(0!=fsl_cx_transaction_level(f)
           && "Expecting a transaction level from fsl__crosslink_begin()");
    fsl_db_transaction_end(db, true)
      /* pop transaction started from fsl__crosslink_begin().  We use
         fsl_db_transaction_end() instead of fsl_cx_transaction_end()
         so that any db-level error which is set during a failed
         rollback does not trump any pending f->error.code. */;
    return resultCode;
  }
  assert(db->beginCount > 0);

  /* Handle any reparenting via tags... */
  rc = fsl_cx_prepare(f, &q,
                     "SELECT rid, value FROM tagxref"
                      " WHERE tagid=%d AND tagtype=%d",
                      (int)FSL_TAGID_PARENT, (int)FSL_TAGTYPE_ADD);
  if(rc) goto end;
  while(FSL_RC_STEP_ROW==fsl_stmt_step(&q)){
    fsl_id_t const rid = fsl_stmt_g_id(&q, 0);
    const char *zTagVal = fsl_stmt_g_text(&q, 1, 0);
    rc = fsl_crosslink_reparent(f,rid, zTagVal);
    if(rc) break;
  }
  fsl_stmt_finalize(&q);
  if(rc) goto end;

  /* Process entries from pending_xlink temp table... */
  rc = fsl_cx_prepare(f, &q, "SELECT id FROM pending_xlink");
  while( 0==rc && FSL_RC_STEP_ROW==fsl_stmt_step(&q) ){
    const char *zId = fsl_stmt_g_text(&q, 0, NULL);
    char cType;
    if(!zId || !*zId) continue;
    cType = zId[0];
    ++zId;
    if('t'==cType){
      rc = fsl__ticket_rebuild(f, zId);
      continue;
    }else if('w'==cType){
      /* FSL-MISSING:
         backlink_wiki_refresh(zId) */
      continue;
    }
  }
  fsl_stmt_finalize(&q); 
  if(rc) goto end;
  rc = fsl_cx_exec(f, "DELETE FROM pending_xlink");
  if(rc) goto end;
  /* If multiple check-ins happen close together in time, adjust their
     times by a few milliseconds to make sure they appear in chronological
     order.
  */
  rc = fsl_cx_prepare(f, &q,
                      "UPDATE time_fudge SET m1=m2-:incr "
                      "WHERE m1>=m2 AND m1<m2+:window"
  );
  if(rc) goto end;
  fsl_stmt_bind_double_name(&q, ":incr", AGE_ADJUST_INCREMENT);
  fsl_stmt_bind_double_name(&q, ":window", AGE_FUDGE_WINDOW);
  rc = fsl_cx_prepare(f, &u,
                      "UPDATE time_fudge SET m2="
                      "(SELECT x.m1 FROM time_fudge AS x"
                      " WHERE x.mid=time_fudge.cid)");
  for(i=0; !rc && i<30; i++){ /* where does 30 come from? */
    rc = fsl_stmt_step(&q);
    if(FSL_RC_STEP_DONE==rc) rc=0;
    else break;
    fsl_stmt_reset(&q);
    if( fsl_db_changes_recent(db)==0 ) break;
    rc = fsl_stmt_step(&u);
    if(FSL_RC_STEP_DONE==rc) rc=0;
    else break;
    fsl_stmt_reset(&u);
  }
  fsl_stmt_finalize(&q);
  fsl_stmt_finalize(&u);
  if(!rc && fsl_db_exists(db,"SELECT 1 FROM time_fudge")){
    rc = fsl_cx_exec(f, "UPDATE event SET"
                     " mtime=(SELECT m1 FROM time_fudge WHERE mid=objid)"
                     " WHERE objid IN (SELECT mid FROM time_fudge)"
                     " AND (mtime=omtime OR omtime IS NULL)"
                     );
  }
  end:
  fsl_cx_exec(f, "DELETE FROM time_fudge");
  if(rc) fsl_cx_transaction_end(f, true);
  else rc = fsl_cx_transaction_end(f, false);
  return rc;
}

int fsl__crosslink_begin(fsl_cx * const f){
  int rc;
  assert(f);
  assert(0==f->cache.isCrosslinking);
  if(f->cache.isCrosslinking){
    return fsl_cx_err_set(f, FSL_RC_MISUSE,
                          "Crosslink is already running.");
  }
  rc = fsl_cx_transaction_begin(f);
  if(rc) return rc;
  rc = fsl_cx_exec_multi(f,
     "CREATE TEMP TABLE IF NOT EXISTS "
     "pending_xlink(id TEXT PRIMARY KEY)WITHOUT ROWID;"
     "CREATE TEMP TABLE IF NOT EXISTS time_fudge("
     "  mid INTEGER PRIMARY KEY,"    /* The rid of a manifest */
     "  m1 REAL,"                    /* The timestamp on mid */
     "  cid INTEGER,"                /* A child or mid */
     "  m2 REAL"                     /* Timestamp on the child */
     ");"
     "DELETE FROM pending_xlink; "
     "DELETE FROM time_fudge;");
  if(0==rc){
    f->cache.isCrosslinking = true;
  }else{
    fsl_cx_transaction_end(f, true);
  }
  return rc;
}

#undef MARKER
#undef AGE_FUDGE_WINDOW
#undef AGE_ADJUST_INCREMENT
#undef F_at