Login
Artifact [10ffe154a6]
Login

Artifact 10ffe154a632dab0db3e07b141003903047c4fd1:


/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/*
   Copyright (c) 2013 D. Richard Hipp
  
   This program is free software; you can redistribute it and/or
   modify it under the terms of the Simplified BSD License (also
   known as the "2-Clause License" or "FreeBSD License".)
  
   This program is distributed in the hope that it will be useful,
   but without any warranty; without even the implied warranty of
   merchantability or fitness for a particular purpose.
  
   Author contact information:
     drh@hwaci.com
     http://www.hwaci.com/drh/
  
  *****************************************************************************
   This file houses the manifest/control-artifact-related APIs.
*/
#include "fossil-scm/fossil-internal.h"
#include <assert.h>
#include <memory.h> /* memcmp() */

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

/**
   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;
}

void fsl_card_F_clean(fsl_card_F *t){
  if(t){
    if(!t->externalStrings){
      fsl_free(t->name);
      fsl_free(t->uuid);
      fsl_free(t->priorName);
    }
    *t = fsl_card_F_empty;
  }
}

void fsl_card_F_free(fsl_card_F *t){
  if(t){
    fsl_card_F_clean(t);
    fsl_free(t);
  }
}

fsl_card_Q * fsl_card_Q_malloc(int type,
                               fsl_uuid_cstr target,
                               fsl_uuid_cstr baseline){
  if(!type || !target || !fsl_is_uuid(target)
     || (baseline && !fsl_is_uuid(baseline))) 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, FSL_UUID_STRLEN);
      if(!c->target) rc = FSL_RC_OOM;
      else if(baseline){
        c->baseline = fsl_strndup( baseline, FSL_UUID_STRLEN);
        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(char 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_card_F * fsl_card_F_malloc(char const * name,
                               char const * uuid,
                               fsl_file_perm_t perm,
                               char const * oldName){
  fsl_card_F * t;
  if(!name || !*name) return NULL;
  else if(uuid && !fsl_is_uuid(uuid)) return NULL;
  t = (fsl_card_F *)fsl_malloc(sizeof(fsl_card_F));
  if(t){
    int rc = 0;
    *t = fsl_card_F_empty;
    t->perm = perm;
    t->name = fsl_strdup(name);
    if(!t->name) rc = FSL_RC_OOM;
    if(!rc && uuid){
      t->uuid = fsl_strdup(uuid);
      if(!t->uuid) rc = FSL_RC_OOM;
    }
    if(!rc && oldName){
      t->priorName = fsl_strdup(oldName);
      if(!t->priorName) rc = FSL_RC_OOM;
    }
    if(rc){
      fsl_card_F_free(t);
      t = NULL;
    }
  }
  return t;
}

/**
    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_F_free(void * obj, void * visitorState ){
  if(obj) fsl_card_F_free( (fsl_card_F*)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;
}

void fsl_card_J_list_free( fsl_list * li, char 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 * 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 * f, fsl_deck * cards, fsl_catype_t type ){
  void const * allocStamp = cards->allocStamp;
  *cards = fsl_deck_empty;
  cards->allocStamp = allocStamp;
  cards->f = f;
  cards->type = type;
}

void fsl_deck_clean(fsl_deck *m){
  if(!m) return;
  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;
  }
  
#define CBUF(X) fsl_buffer_clear(&m->X)
#define SFREE(X) if(m->X) fsl_deck_free_string(m, m->X); m->X = NULL
#define SLIST(X) fsl_list_clear(&m->X, fsl_list_v_card_string_free, m)
  SFREE(uuid);
  SFREE(A.name);
  SFREE(A.tgt);
  SFREE(A.src);
  SFREE(B.uuid);
  SFREE(C);
  SFREE(E.uuid);
  fsl_list_clear(&m->F.list, fsl_list_v_card_F_free, NULL);
  fsl_card_J_list_free(&m->J, 1);
  SFREE(K);
  SFREE(L);
  SLIST(M);
  SFREE(N);
  SLIST(P);
  fsl_list_clear(&m->Q, fsl_list_v_card_Q_free, NULL);
  SFREE(R);
  fsl_list_clear(&m->T, fsl_list_v_card_T_free, NULL);
  SFREE(U);
  CBUF(W);
  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!
                */;
  fsl_error_clear(&m->error);
  {
    void const * allocStampKludge = m->allocStamp;
    fsl_cx * f = m->f;
    *m = fsl_deck_empty;
    m->allocStamp = allocStampKludge;
    m->f = f;
  }
#undef CBUF
#undef SFREE
#undef SLIST
}


void fsl_deck_finalize(fsl_deck *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_catype_t t, char card ){
  /*
    Implements this table:
    
    http://fossil-scm.org/index.html/doc/trunk/www/fileformat.wiki#summary
  */
  if('Z'==card) return 1;
  else switch(t){
    case FSL_CATYPE_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_CATYPE_ATTACHMENT:
      switch(card){
        case 'A': case 'D':
          return 1;
        case 'C': case 'N': case 'U':
          return -1;
        default:
          return 0;
      };
    case FSL_CATYPE_CLUSTER:
      return 'M'==card ? 1 : 0;
    case FSL_CATYPE_CONTROL:
      switch(card){
        case 'D': case 'T': case 'U':
          return 1;
        default:
          return 0;
      };
    case FSL_CATYPE_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_CATYPE_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_CATYPE_TICKET:
      switch(card){
        case 'D': case 'J':
        case 'K': case 'U':
          return 1;
        default:
          return 0;
      };
    case FSL_CATYPE_WIKI:
      switch(card){
        case 'D': case 'L':
        case 'U': case 'W':
          return 1;
        case 'N': case 'P':
          return -1;
        default:
          return 0;
      };
    default:
      assert(!"Invalid fsl_catype_t.");
      return 0;
  };
}

char fsl_deck_has_required_cards( fsl_deck const * d ){
  return d ? fsl_deck_required_cards_check(d, NULL) : 0;
}

char fsl_deck_required_cards_check( fsl_deck const * d,
                                    fsl_error * err){
  if(!d) return 0;
  else switch(d->type){
    case FSL_CATYPE_ANY:
      return 0;
#define NEED(CARD,COND) \
      if(!(COND)) { \
        if(err) fsl_error_set(err, FSL_RC_CA_SYNTAX,                 \
                              "Required %c-card is missing or invalid.", \
                              *#CARD);                                  \
        return 0;                                                       \
      } (void)0
    case FSL_CATYPE_ATTACHMENT:
      NEED(A,d->A.name);
      NEED(A,d->A.tgt);
      NEED(D,d->D > 0);
      return 1;
    case FSL_CATYPE_CLUSTER:
      NEED(M,d->M.used);
      return 1;
    case FSL_CATYPE_CONTROL:
      NEED(D,d->D > 0);
      NEED(U,d->U);
      NEED(T,d->T.used>0);
      return 1;
    case FSL_CATYPE_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_CATYPE_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. Rather
        than rely on d->f being set (so d->f->flags can tell us
        whether or not to calculate an R-card), we'll rely on
        downstream code to check for an R-chard if needed.
      */
      NEED(D,d->D > 0);
      NEED(C,d->C);
      NEED(C,d->U);
      NEED(F,d->F.list.used || d->R/*with initial-state md5 hash!*/);
      if(d->f
         && !d->R
         && (FSL_CX_F_CALC_R_CARD & d->f->flags)){
        if(err) fsl_error_set(err, FSL_RC_CA_SYNTAX,
                              "%s deck is missing an R-card, "
                              "yet R-card calculation is enabled.",
                              fsl_catype_cstr(d->type));
        return 0;
      }else if(d->R
               && !d->F.list.used
               && 0!=fsl_uuidcmp(d->R, FSL_MD5_INITIAL_HASH)
               ){
        if(err) fsl_error_set(err, FSL_RC_CA_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_CATYPE_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_CATYPE_WIKI:
      NEED(D,d->D > 0);
      NEED(L,d->L);
      NEED(U,d->U);
      NEED(W,d->W.used);
      return 1;
    case FSL_CATYPE_INVALID:
    default:
      assert(!"Invalid fsl_catype_t.");
      return 0;
  }
#undef NEED
}


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

char const * fsl_catype_event_cstr(fsl_catype_t t){
  switch(t){
    case FSL_CATYPE_ANY: return "*";
    case FSL_CATYPE_CHECKIN: return "ci";
    case FSL_CATYPE_EVENT: return "e";
    case FSL_CATYPE_CONTROL: return "g";
    case FSL_CATYPE_TICKET: return "t";
    case FSL_CATYPE_WIKI: return "w";
    default:
      return NULL;
  }
}


/**
    If fsl_card_is_legal(d->type, card), returns non-0 (true),
    else updated d->error with a description of the constraint
    violation and returns 0.
 */
static char fsl_deck_check_type( fsl_deck * d, char card ){
  if(fsl_card_is_legal(d->type, card)) return 1;
  else{
    fsl_error_set(&d->error, FSL_RC_TYPE,
                  "Card type '%c' is not allowed "
                  "in control artifacts of type %s.",
                  card, fsl_catype_cstr(d->type));
    return 0;
  }
}

/*
  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_RANGE if (valLen!=ASSERTLEN). ASSERTLEN
  is assumed to be either an SHA1 or MD5 hash value and it is
  validated against fsl_validate16(value,valLen), returning
  FSL_RC_CA_SYNTAX if that check fails.
 */
static int fsl_deck_sethex_impl( fsl_deck * mf, fsl_uuid_cstr value,
                                    char letter,
                                    fsl_size_t assertLen,
                                    char ** mfMember ){
  assert(mf);
  assert( assertLen==FSL_UUID_STRLEN || assertLen==FSL_MD5_STRLEN );
  if(!fsl_deck_check_type(mf,letter)) return FSL_RC_TYPE;
  else if(!value){
    fsl_deck_free_string(mf, *mfMember);
    *mfMember = NULL;
    return 0;
  }else if(fsl_strlen(value) != (assertLen)){
    return fsl_error_set(&mf->error, FSL_RC_RANGE,
                         "Invalid length for %c-card: expecting %d.",
                         letter, (int)assertLen);
  }else if(!fsl_validate16(value, assertLen)) {
    return fsl_error_set(&mf->error, FSL_RC_CA_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 * mf, char const * value,
                                     fsl_int_t valLen,
                                     char letter, fsl_buffer * buf){
  assert(mf);  
  if(!fsl_deck_check_type(mf,letter)) return FSL_RC_TYPE;
  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 * mf, fsl_uuid_cstr uuidBaseline){
  if(!mf) return FSL_RC_MISUSE;
  else{
    if(mf->B.baseline){
      fsl_deck_finalize(mf->B.baseline);
      mf->B.baseline = NULL;
    }
    return fsl_deck_sethex_impl(mf, uuidBaseline, 'B',
                                FSL_UUID_STRLEN, &mf->B.uuid);
  }
}

/**
    Internal impl for card setters which consider of a simple (char *)
    member.
 */
static int fsl_deck_set_string( fsl_deck * mf, char letter, char ** member, char const * v, fsl_int_t n ){
  if(!fsl_deck_check_type(mf, letter)) return FSL_RC_TYPE;
  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 * mf, char const * v, fsl_int_t n){
  return mf
    ? fsl_deck_set_string( mf, 'C', &mf->C, v, n )
    : FSL_RC_MISUSE;
}

int fsl_deck_J_add( fsl_deck * mf, char isAppend,
                    char const * field, char const * value){
  if(!mf || !field) return FSL_RC_MISUSE;
  else if(!*field) return FSL_RC_RANGE;
  else if(!fsl_deck_check_type(mf,'J')) return FSL_RC_TYPE;
  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 * mf, fsl_uuid_cstr uuid){
  return mf
    ? fsl_deck_sethex_impl(mf, uuid, 'K', FSL_UUID_STRLEN, &mf->K)
    : FSL_RC_MISUSE;
}

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

int fsl_deck_M_add( fsl_deck * mf, char const *uuid){
  int rc;
  char * dupe;
  if(!mf || !uuid) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf, 'M')) return FSL_RC_TYPE;
  else if(!fsl_is_uuid(uuid)) return FSL_RC_RANGE;
  dupe = fsl_strndup(uuid, FSL_UUID_STRLEN);
  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 * mf, char const * v, fsl_int_t n){
  return mf
    ? fsl_deck_set_string( mf, 'N', &mf->N, v, n )
    : FSL_RC_MISUSE;
}


int fsl_deck_P_add( fsl_deck * mf, char const *parentUuid){
  int rc;
  char * dupe;
  if(!mf || !parentUuid || !*parentUuid) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf, 'P')) return FSL_RC_TYPE;
  else if(!fsl_is_uuid(parentUuid)){
    return fsl_error_set(&mf->error, FSL_RC_RANGE,
                         "Invalid UUID for P-card.");
  }
  dupe = fsl_strndup(parentUuid, FSL_UUID_STRLEN);
  if(!dupe) rc = FSL_RC_OOM;
  else{
    rc = fsl_list_append( &mf->P, dupe );
    if(rc){
      fsl_free(dupe);
    }
  }
  return rc;
}

fsl_id_t fsl_deck_P_get_id(fsl_deck * d, int index){
  if(!d || !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 * mf, int type,
                    fsl_uuid_cstr target,
                    fsl_uuid_cstr baseline ){
  if(!mf || !target) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf,'Q')) return FSL_RC_TYPE;
  else if(!type || !fsl_is_uuid(target)
          || (baseline && !fsl_is_uuid(baseline))) return FSL_RC_RANGE;
  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 **). Note
    the pointer-to-pointer (because this was initially only used
    in a fsl_list context).
*/
static int fsl_card_F_cmp_pp( void const * lhs, void const * rhs ){
  fsl_card_F const * l = *((fsl_card_F const **)lhs);
  fsl_card_F 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_deck_F_sort(fsl_deck * mf){
  fsl_list_sort(&mf->F.list, fsl_card_F_cmp_pp );
}

int fsl_card_F_compare( fsl_card_F const * lhs,
                        fsl_card_F const * rhs){
  return (lhs == rhs) ? 0 : fsl_card_F_cmp_pp( &lhs, &rhs );
}

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

int fsl_deck_R_calc(fsl_deck * mf){
  fsl_cx * f = mf ? mf->f : NULL;
  if(!mf || !f) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf,'R')) {
    assert(mf->error.code);
    return fsl_cx_err_set_e(f, &mf->error);
  }
  else if(!mf->F.list.used){
    return fsl_deck_R_set(mf, FSL_MD5_INITIAL_HASH);
  }else{
    int rc = 0;
    fsl_card_F const * fc;
    fsl_id_t fileRid;
    fsl_buffer * buf = &f->fileContent;
    unsigned char digest[16];
    char hex[FSL_MD5_STRLEN+1];
    fsl_md5_cx md5 = fsl_md5_cx_empty;
    enum { NumBufSize = 40 };
    char numBuf[NumBufSize] = {0};
    assert(mf->f);
    assert(!buf->used && "Misuse of f->fileContent buffer.");
    /* memset(numBuf,0,NumBufSize); */
    fsl_deck_F_sort(mf);
    rc = fsl_deck_F_rewind(mf);
    if(rc) return rc;
    /*
      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;
      if(!fc->uuid) continue;
      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",
                   (fsl_size_t)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_yield_file_buffer(f);
    assert(0==buf->used);
    return rc ? rc : fsl_deck_R_set(mf, hex);
  }
}



int fsl_deck_T_add2( fsl_deck * mf, fsl_card_T * t){
  return (mf && t)
    ? (fsl_deck_check_type(mf, 'T')
       ? fsl_list_append(&mf->T, t)
       : FSL_RC_TYPE)
    : FSL_RC_MISUSE;
}

int fsl_deck_T_add( fsl_deck * mf, fsl_tag_type tagType,
                    char const * uuid, char const * name,
                    char const * value){
  if(!mf || !name) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf, 'T')) return FSL_RC_TYPE;
  else if(!*name || (uuid &&!fsl_is_uuid(uuid))) return FSL_RC_RANGE;
  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_error_set(&mf->error, FSL_RC_RANGE,
                           "Invalid tag-type value: %d",
                           (int)tagType);
  }
}

int fsl_deck_U_set( fsl_deck * mf, char const * v, fsl_int_t n){
  return mf
    ? fsl_deck_set_string( mf, 'U', &mf->U, v, n )
    : FSL_RC_MISUSE;
}

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

int fsl_deck_A_set( fsl_deck * mf, char const * name,
                    char const * tgt,
                    char const * uuidSrc ){
  if(!mf || !name || !tgt) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf, 'A')) return FSL_RC_TYPE;
  else if(!*tgt){
    return fsl_error_set(&mf->error, FSL_RC_RANGE,
                       "Invalid target name in A card.");
  }
  /* TODO: validate tgt based on mf->type and require UUID
     for types EVENT/TICKET.
  */
  else if(uuidSrc && *uuidSrc && !fsl_is_uuid(uuidSrc)){
    return fsl_error_set(&mf->error, FSL_RC_RANGE,
                       "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(uuidSrc && *uuidSrc){
      mf->A.src = fsl_strndup(uuidSrc,FSL_UUID_STRLEN);
      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 * mf, fsl_double_t date){
  if(!mf) return FSL_RC_MISUSE;
  else if(date<=0) return FSL_RC_RANGE;
  else if(!fsl_deck_check_type(mf, 'D')) return FSL_RC_TYPE;
  else{
    mf->D = date;
    return 0;
  }
}

int fsl_deck_E_set( fsl_deck * mf, fsl_double_t date, char const * uuid){
  if(!mf || !uuid) return FSL_RC_MISUSE;
  else if(date<=0){
    return fsl_error_set(&mf->error, FSL_RC_RANGE,
                       "Invalid date value for E card.");
  }else if(!fsl_is_uuid(uuid)){
    return fsl_error_set(&mf->error, 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, FSL_UUID_STRLEN);
    return mf->E.uuid ? 0 : FSL_RC_OOM;
  }
}

int fsl_deck_F_add2( fsl_deck * mf, fsl_card_F * t){
  if(!mf || !t) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf, 'F')) return FSL_RC_TYPE;
  else{
    int rc = 0;
    if(mf->F.list.used == mf->F.list.capacity){
      rc = fsl_list_reserve(&mf->F.list, mf->F.list.capacity
                            ? mf->F.list.capacity*2 : 20);
    }
    /*
      Should we search/replace entries now or fail later when we go to
      output dupes? The O(N) search could get expensive on large
      repos. Or we use fsl_deck_F_seek()?
    */
    return rc ? rc : fsl_list_append(&mf->F.list, t);
  }
}

int fsl_deck_F_add( fsl_deck * mf, char const * name,
                    char const * uuid,
                    fsl_file_perm_t perms, 
                    char const * oldName){
  if(!mf || !name) return FSL_RC_MISUSE;
  else if(!fsl_deck_check_type(mf, 'F')) return FSL_RC_TYPE;
  else if(!*name){
    return fsl_error_set(&mf->error, 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_error_set(&mf->error, FSL_RC_RANGE,
                         "Invalid filename for F-card (simple form required).");
  }
  else if(uuid && !fsl_is_uuid(uuid)){
    return fsl_error_set(&mf->error, FSL_RC_RANGE,
                         "Invalid UUID for F-card.");
  }
  else {
    int rc;
    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_file_perm_t value");
        return fsl_error_set(&mf->error, FSL_RC_RANGE,
                             "Invalid fsl_file_perm_t value "
                             "(%d) for file [%s].",
                             perms, name);
    }
    t = fsl_card_F_malloc(name, uuid, perms, oldName);
    if(!t) rc = FSL_RC_OOM;
    else{
      rc = fsl_deck_F_add2( mf, t );
      if(rc) fsl_card_F_free(t);
    }
    return rc;
  }
}

int fsl_deck_F_foreach( fsl_deck * d, char includeBaseline,
                        fsl_card_F_visitor_f cb, void * visitorState ){
  if(!d || !cb) return FSL_RC_MISUSE;
  if(!includeBaseline || !d->B.uuid){
    fsl_card_F const * fc;
    fsl_size_t i;
    int rc = 0;
    for( i = 0; !rc && (i<d->F.list.used); ++i ){
      fc = (fsl_card_F const *)d->F.list.list[i];
      rc = cb(fc, visitorState);
    }
    return (FSL_RC_BREAK==rc) ? 0 : rc;
  }else{
    int rc;
    fsl_card_F const * fc;
    rc = fsl_deck_F_rewind(d);
    if(rc) return rc;
    while( !rc && !(rc=fsl_deck_F_next(d, &fc)) && fc) {
      rc = cb( fc, visitorState );
    }
    return rc;
  }
}

  
/**
    Output state for fsl_appendf_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_appendf_f_mf().
   */
  fsl_md5_cx md5;

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

  /**
      Scratch buffer for fossilzing bytes and other temporary work.
   */
  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*/,
fsl_buffer_empty_m/*scratch*/
};

/**
    fsl_appendf_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 fsl_int_t fsl_appendf_f_mf( void * arg,
                                   char const * data,
                                   fsl_int_t n ){
  fsl_deck_out_state * 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 (0==os->rc)
    ? n
    : -1;
}

/**
    Internal helper for fsl_mf_output(). Appends formatted output to
    os->out() via fsl_appendf_f_mf(). Returns os->rc (0 on success).
 */
static int fsl_deck_append( fsl_deck_out_state * os,
                            char const * fmt, ... ){
  fsl_int_t rc;
  va_list args;
  assert(os);
  assert(fmt && *fmt);
  va_start(args,fmt);
  rc = fsl_appendfv( fsl_appendf_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 * os,
                               unsigned char const * inp,
                               fsl_int_t len){
  fsl_buffer_reset(&os->scratch);
  return os->rc = len
    ? fsl_bytes_fossilize(inp, len, &os->scratch)
    : 0;
}

static char fsl_deck_out_tcheck(fsl_deck_out_state * 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_catype_cstr(os->d->type));
  }
  return os->rc ? 0 : 1;
}

#if 0
/**
    Fossilizes b->mem to os->scratch. Returns 0 on success and all
    that.
 */
static int fsl_deck_fossilize_b( fsl_deck_out_state * os,
                                 fsl_buffer const * b ){
  return b->used
    ? fsl_deck_fossilize( os, b->mem, (fsl_int_t)b->used)
    : 0;
}
/**
    If doFossilize, fossilizes b to os->scratch and passes that result
    to fsl_deck_append(), otherwise uses b's contents directly for
    appending a card (with the given letter) to os:
   
    LETTER CONTENT\\n
   
    This is only useful for cards with simple buffer values.
 */
static int fsl_deck_out_letter_buf( fsl_deck_out_state * os,
                                    char letter,
                                    fsl_buffer const * b,
                                    char doFossilize){
  if(b->used && fsl_deck_out_tcheck(os, letter)){
    if(doFossilize){
#if 0
      fsl_deck_append(os, "%c %F\n", letter,
                      (char const *)b->mem );
#else
      /* This is functionally identical but is more efficient b/c we
         re-use os->scratch. */
      fsl_deck_fossilize_b(os, b);
      if(!os->rc) fsl_deck_append(os, "%c %b\n", letter, &os->scratch);
#endif
    }else{
      fsl_deck_append(os, "%c %.*s\n", letter,
                      (int)b->used,
                      (char const *)b->mem );
    }
  }
  return os->rc;
}
#endif

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


/* Appends the A card to os from os->d->A. */
static int fsl_deck_out_A( fsl_deck_out_state * 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_CA_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_CA_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 * 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 * 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 * 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 * os ){
  if(os->d->E.uuid && fsl_deck_out_tcheck(os, 'E')){
    char ds[24];
    char msPrecision = FSL_CATYPE_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;
}

/**
    fsl_list_visitor_f() impl for outputing F cards. obj must be a
    (fsl_card_F *). visitorState must be a (fsl_deck_out_state*).
 */
static int fsl_list_v_mf_output_card_F(void * obj, void * visitorState ){
  fsl_deck_out_state * os = (fsl_deck_out_state *)visitorState;
  fsl_card_F const * f = (fsl_card_F const *)obj;
  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, 1)){
    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 v1
           F-card-generating code i looked at 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_appendf_f_mf(os, "\n", 1);
  return os->rc;
}

static int fsl_deck_out_list_obj( fsl_deck_out_state * 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 * os ){
  return fsl_deck_out_list_obj(os, 'F', &os->d->F.list,
                               fsl_list_v_mf_output_card_F);
}


/**
    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 * 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 * 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 * 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 * 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 * 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 * os = (fsl_deck_out_state *)visitorState;
  char const * uuid = (char const *)obj;
  fsl_size_t len;
  assert(uuid);
  if(!fsl_is_uuid(uuid)){
    os->rc = fsl_error_set(&os->error, FSL_RC_RANGE,
                           "Invalid UUID in P card.");
  }
  else if(!os->counter++) fsl_appendf_f_mf(os, "P ", 2);
  else fsl_appendf_f_mf(os, " ", 1);
  /* Reminder: fsl_appendf_f_mf() updates os->rc. */
  if(!os->rc){
    len = (fsl_size_t)fsl_strlen(uuid);
    assert(FSL_UUID_STRLEN==len) /* is enforced by fsl_mf_add_P() */;
    fsl_appendf_f_mf(os, uuid, len);
  }
  return os->rc;
}


static int fsl_deck_out_P( fsl_deck_out_state * 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_appendf_f_mf(os, "\n", 1);
  }else if(FSL_CATYPE_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");
  }
  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 *) and obj->counter must be
    set to 0 before running this visit iteration.
 */
static int fsl_list_v_mf_output_card_Q(void * obj, void * visitorState ){
  fsl_deck_out_state * os = (fsl_deck_out_state *)visitorState;
  fsl_card_Q const * cp = (fsl_card_Q const *)obj;
  char const prefix = (cp->type<0) ? '-' : '+';
  assert(cp->type);
  assert(cp->target);
  if(!cp->type){
    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_catype_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 * 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 * os ){
  if(os->d->R && fsl_deck_out_tcheck(os, 'R')){
    if((FSL_MD5_STRLEN!=fsl_strlen(os->d->R))
            || !fsl_validate16(os->d->R, FSL_MD5_STRLEN)){
      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 * os = (fsl_deck_out_state *)visitorState;
  fsl_card_T * t = (fsl_card_T *)obj;
  char prefix = 0;
  /* 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);
  }

  /*
     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).
  */
  os->scratch.used = 0;
  fsl_deck_fossilize(os, (unsigned const char *)t->name, -1);
  if(os->rc) return os->rc;
  if(t->uuid && !fsl_is_uuid(t->uuid)){
      return os->rc = fsl_error_set(&os->error, FSL_RC_TYPE,
                                    "Malformed UUID in T-card: %s",
                                    t->uuid);
  }
  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_appendf_f_mf(os, " ", 1);
    if(!os->rc) fsl_appendf_f_mf(os, (char const*)os->scratch.mem,
                                 (fsl_int_t)os->scratch.used);
  }
  if(!os->rc){
    fsl_appendf_f_mf(os, "\n", 1);
  }
  return os->rc;
}


char fsl_tag_prefix_char( fsl_tag_type 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.
*/
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;
    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_CA_SYNTAX;
          else{
            rc = rc
              ? fsl_error_set(err, FSL_RC_CA_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_CA_SYNTAX,
                              "Duplicate T-card: %c%s",
                              fsl_tag_prefix_char(prev->type),
                              prev->name)
              ;
          }
          break;
        }
      }
    }
    return rc;
  }
}

/* Appends the T cards to os from os->d->T. */
static int fsl_deck_out_T( fsl_deck_out_state * 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 * 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 * 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 * os ){
  unsigned char digest[16];
  char md5[FSL_MD5_STRLEN+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_MD5_STRLEN, 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 * 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 * 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 * 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 * 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 * 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 * 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 * os ){
  int rc;
  /* Reminder: cards must be output in strict lexical order. */
  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 * 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;
}

/**
    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 * 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 * d, char calculateRCard ){
  fsl_list * li;
  int rc = 0;
  if(!d) return FSL_RC_MISUSE;
#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_CATYPE_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.list.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 * cards,
                     fsl_output_f out, void * outputState,
                     fsl_error * err ){
  static const char allowTypeAny = 0
    /* Only enable for debugging/testing. Allows outputing decks of
       type FSL_CATYPE_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 * os = &OS;
  int rc = 0;
  if(!cards || !out) return FSL_RC_MISUSE;
  else if(!fsl_deck_required_cards_check(cards,err)){
    return FSL_RC_CA_SYNTAX;
  }
  else if(FSL_CATYPE_ANY==cards->type){
    if(!allowTypeAny){
      return err
        ? fsl_error_set(err, FSL_RC_TYPE,
                        "Control artifact type ANY cannot be "
                        "output unless it is enabled in this "
                        "code (it's dangerous).")
        : FSL_RC_TYPE;
    }
    /* fall through ... */
  }
  os->d = cards;
  os->out = out;
  os->outState = outputState;
  switch(cards->type){
    case FSL_CATYPE_CLUSTER:
      rc = fsl_deck_output_cluster(os);
      break;
    case FSL_CATYPE_CONTROL:
      rc = fsl_deck_output_control(os);
      break;
    case FSL_CATYPE_EVENT:
      rc = fsl_deck_output_event(os);
      break;
    case FSL_CATYPE_CHECKIN:
      rc = fsl_deck_output_mf(os);
      break;
    case FSL_CATYPE_TICKET:
      rc = fsl_deck_output_ticket(os);
      break;
    case FSL_CATYPE_WIKI:
      rc = fsl_deck_output_wiki(os);
      break;
    case FSL_CATYPE_ANY:
      assert(allowTypeAny);
      rc = fsl_deck_output_any(os);
      break;
    case FSL_CATYPE_ATTACHMENT:
      rc = fsl_deck_output_attachment(os);
      break;
    default:
      rc = err
        ? fsl_error_set(err, FSL_RC_TYPE,
                        "Invalid/unhandled deck type (#%d).",
                        cards->type)
        : FSL_RC_TYPE;
      goto end;
  }
  if(!rc){
    rc = fsl_deck_out_Z( os );
  }
  end:
  if(err && os->rc){
    fsl_error_move(&os->error, err);
  }
  fsl_buffer_clear(&os->scratch);
  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 */



int fsl_deck_crosslink_end(fsl_cx * f){
  int rc = 0;
  fsl_db * 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);
  if(!f->cache.isCrosslinking) return FSL_RC_RANGE;
  f->cache.isCrosslinking = 0;
  assert(db->beginCount > 0);

  /* TODO: port in manifest.c:manifest_crosslink_end() */
#if 0
  /* MISSING: ticket_rebuild_entry()
   */
  rc = fsl_db_prepare(db, &q, "SELECT uuid FROM pending_tkt");
  if(rc) goto end;
  while( FSL_RC_STEP_ROW==fsl_stmt_step(&q) ){
    const char *zUuid = fsl_stmt_g_text(&q, 0, NULL);
    ticket_rebuild_entry(zUuid);
  }
  db_finalize(&q);
#endif
  rc = fsl_db_exec(db, "DROP TABLE pending_tkt");
  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_db_prepare(db, &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_db_prepare(db, &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){
    rc = fsl_db_exec(db, "UPDATE event SET "
                     "mtime=(SELECT m1 FROM time_fudge WHERE mid=objid) "
                     "WHERE objid IN (SELECT mid FROM time_fudge)");
  }
  end:
  fsl_db_exec(db, "DROP TABLE time_fudge");
  if(rc) fsl_db_transaction_rollback(db);
  else rc = fsl_db_transaction_commit(db);
  if(rc && !f->error.code) fsl_cx_uplift_db_error(f, db);
  return rc;
}

int fsl_deck_crosslink_begin(fsl_cx * f){
  int rc;
  fsl_db * db = fsl_cx_db_repo(f);
  assert(f);
  assert(db);
  assert(0==f->cache.isCrosslinking);
  if(f->cache.isCrosslinking) return FSL_RC_MISUSE;
  rc = fsl_db_transaction_begin(db);
  if(rc) return rc;
  rc = fsl_db_exec_multi(db,
     "CREATE TEMP TABLE pending_tkt(uuid TEXT UNIQUE);"
     "CREATE TEMP TABLE 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 */
     ");");
  if(!rc) f->cache.isCrosslinking = 1;
  if(rc) fsl_cx_uplift_db_error(f, db);
  return rc;
}

/** @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

    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_repo_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,
                            int isPublic,
                            int isPrimary,
                            fsl_file_perm_t mperm){
  fsl_id_t fnid, pfnid, pid, fid;
  fsl_db * db = fsl_cx_db_repo(f);
  fsl_stmt * s1 = NULL;
  int rc;
  assert(f);
  assert(db);
  assert(db->beginCount>0);
  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;
  }

  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"
                             ")");
  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);    
    rc = fsl_stmt_step(s1);
    fsl_stmt_cached_yield(s1);
    if(FSL_RC_STEP_DONE==rc){
      rc = 0;
      if(pid && fid){
        rc = fsl_content_deltify(f, pid, fid, 0);
      }
    }else{
      fsl_cx_uplift_db_error(f, db);
    }
  }
  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.    

    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 a 3rd parameter to this since it was ported.
    See the bottom part of this change:

    http://www.fossil-scm.org/index.html/info/2e51be8ec2df7cdf0dee3b33b63c46c73dcec624
*/
static fsl_card_F * fsl_deck_F_seek_base(fsl_deck * d,
                                         char const * zName){
  /* 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.list.used) return NULL;
#define FCARD(NDX) ((fsl_card_F *)d->F.list.list[NDX])
  lwr = 0;
  upr = d->F.list.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 ){
      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;
      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_CATYPE_CHECKIN!=d->type) || !zName || !*zName
     || !d->F.list.used) return NULL;
  pFile = fsl_deck_F_seek_base(d, zName);
  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);
      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);
}

/**
    Returns true if repo contains an mlink entry where mid=rid,
    else false.
 */
static char fsl_repo_has_mlink_mid( fsl_db * repo, fsl_id_t rid ){
  fsl_stmt * st = NULL;
  int rc = fsl_db_prepare_cached(repo, &st,
                                 "SELECT 1 FROM mlink WHERE mid=?");
  if(!rc){
    fsl_stmt_bind_id(st, 1, rid);
    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 ? 0 : 1;

}

/**
    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
 */
static
int fsl_repo_mlink_add( fsl_cx * f,
                        fsl_id_t pmid, fsl_deck /*const*/ * pParent,
                        fsl_id_t cid, fsl_deck /*const*/ * pChild,
                        int 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 ** ppOther;
  fsl_db * db = fsl_cx_db_repo(f);
  char isPublic;
  assert(db);
  assert(db->beginCount>0);
  /* If mlink table entires are already set for cid, then abort early
     doing no work.
  */
  if(fsl_repo_has_mlink_mid(db, 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 ){
    ppOther = &pChild;
    otherRid = cid;
  }else{
    ppOther = &pParent;
    otherRid = pmid;
  }

  rc = fsl_content_get(f, otherRid, &otherContent);
  if(rc) goto end;
  if( !otherContent.used ){
    assert(!"Is zero correct here? Can this happen? "
           "Loaded non-manifest?");
    return 0;
  }
  *ppOther = fsl_deck_malloc();
  if(!*ppOther){
    rc = FSL_RC_OOM;
    goto end;
  }
  (*ppOther)->f = f;
  rc = fsl_deck_parse2(*ppOther, &otherContent, otherRid);
  if(rc) goto end;
  if( (rc=fsl_deck_baseline_fetch(pParent))
      || (rc=fsl_deck_baseline_fetch(pChild))){
    goto end;
  }
  rc = 0;
  isPublic = !fsl_content_is_private(f, cid);

  /* 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");",
                     (fsl_id_t)pParent->rid, pParent->D,
                     (fsl_id_t)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.list.used)) \
    ? ((fsl_card_F const *)(DECK)->F.list.list[NDX]) \
   : NULL)
  /* First look at all files in pChild, ignoring its baseline.  This
     is where most of the changes will be found.
  */  
  for(i=0, pChildFile=FCARD(pChild,0);
      i<pChild->F.list.used;
      ++i, pChildFile=FCARD(pChild,i)){

    fsl_file_perm_t const mperm = pChildFile->perm;
    if( pChildFile->priorName ){
      pParentFile = fsl_deck_F_seek(pParent,
                                    pChildFile->priorName);
      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_repo_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_repo_mlink_add_one(f, pmid, 0, cid, pChildFile->uuid,
                                    pChildFile->name, 0,
                                    isPublic, isPrimary, mperm);
      }
    }else{
      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_repo_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_repo_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.list.used;
        ++i, pParentFile = FCARD(pParent,i)){
      if( pParentFile->uuid ){
        pChildFile = fsl_deck_F_seek_base(pChild, pParentFile->name);
        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_repo_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_repo_mlink_add_one(f, pmid, 0, cid, pChildFile->uuid,
                                      pChildFile->name, 0, isPublic,
                                      isPrimary, pChildFile->perm);
        }
      }
    }
  }else if( !pChild->B.uuid ){
    /* pChild is a baseline.  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_repo_mlink_add_one(f, pmid, pParentFile->uuid, cid, 0,
                                    pParentFile->name, 0, isPublic,
                                    isPrimary, pParentFile->perm);
      }
    }
  }
  end:
  if(*ppOther){
    /* TODO? cache *ppOther here, like v1 does. */
    fsl_deck_finalize(*ppOther);
  }
  fsl_buffer_clear(&otherContent);
  if(rc && db->error.code && !f->error.code){
    fsl_cx_uplift_db_error(f, db);
  }
  return rc;  
#undef FCARD
}

int fsl_deck_crosslink( fsl_deck /* const */ * d ){
  int rc;
  fsl_cx * f = d ? d->f : NULL;
  fsl_db * db = f ? fsl_needs_repo(f) : NULL;
  fsl_id_t parentid = 0;
  fsl_double_t mtime;
  fsl_int_t const rid = d ? d->rid : -1;
  /*
    TODO: split this beast up into multiple (static) routines.

    We now have fsl_xlink_listener(), so we can/should at least farm
    out the parts which update the event table.
  */
  assert(f);
  assert(d);
  if(!f || !d) 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,
                          (fsl_id_t)rid);
  }
  else if(!db) return FSL_RC_NOT_A_REPO;
  else if(!fsl_deck_required_cards_check(d, &d->f->error)){
    assert(d->f->error.code);
    return d->f->error.code;
  }else if(f->cache.xlinkClustersOnly && (FSL_CATYPE_CLUSTER!=d->type)){
    return 0;
  }
  
  mtime = (d->D>0)
    ? d->D
    : fsl_db_julian_now(db);
  
  if(FSL_CATYPE_CHECKIN==d->type){
    /* TODO: try to load baseline, and fail if we can't.  Doh, deck is
       const. So require that the caller to load it.  Or load it
       automatically somewhere along the line, maybe at parse-time.

       But since it has to be non-const for other reasons, we'll go ahead
       and load the baseline here...
    */
    if(d->B.uuid && !d->B.baseline){
      rc = fsl_deck_baseline_fetch(d);
      if(rc) goto end;
      assert(d->B.baseline);
    }
  }

  rc = fsl_db_transaction_begin(db);
  if(rc) goto end;

  if(FSL_CATYPE_CHECKIN == d->type){
    fsl_size_t i;
    fsl_stmt q = fsl_stmt_empty;
    /* 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(!fsl_repo_has_mlink_mid(db, rid)){
      /* legacy (see comments below): char *zCom; */
      char zBaseId[30] = {0};
      if(d->B.uuid){
        fsl_id_t const baseid = d->B.baseline
          ? d->B.baseline->rid
          : fsl_uuid_to_rid(f, d->B.uuid);
        if(baseid<0){
          rc = f->error.code;
          assert(0 != rc);
          goto end;
        }
        assert(baseid>0);
        fsl_snprintf( zBaseId, sizeof(zBaseId),
                      "%"FSL_ID_T_PFMT,
                      (fsl_id_t)baseid );
        
      }else{
        fsl_snprintf( zBaseId, sizeof(zBaseId), "NULL" );
      }
      for(i=0; i<d->P.used; i++){
        char const * parentUuid = (char const *)d->P.list[i];
        fsl_id_t 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)",
                         (fsl_id_t)pid, (fsl_id_t)rid,
                         ((i==0) ? 1 : 0), d->D, zBaseId/*safe-for-%s*/);
        if(rc) goto end;
        rc = fsl_repo_mlink_add(f, pid, NULL, rid, d, 0==i);
        if(rc) goto end;
        if( i==0 ){
          parentid = pid;
        }
      }
      if(d->P.used>1){
        /* http://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)",
                    (fsl_id_t)rid, (fsl_id_t)rid, (int)d->P.used
                    );
        if(rc) goto end;
      }
      rc = fsl_db_prepare(db, &q,
                          "SELECT cid, isprim FROM plink "
                          "WHERE pid=%"FSL_ID_T_PFMT" AND isprim",
                          (fsl_id_t)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);
        assert(cid>0);
        rc = fsl_repo_mlink_add(f, rid, d, cid, NULL, isPrim);
      }
      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. */
        char const isPublic = !fsl_content_is_private(f, rid);
        for(i=0; !rc && (i<d->F.list.used); ++i){
          fsl_card_F const * fc = (fsl_card_F const *)d->F.list.list[i];
          rc = fsl_repo_mlink_add_one(f, 0, 0, rid, fc->uuid, fc->name, 0,
                                      isPublic, 1, fc->perm);
        }
      }
      if(rc) goto end;
      /* FIXME: use a cached statement */
      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*/
                       "  %Q," /*comment. No, the comment _field_. */
                       "  (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*/
                       ")",
                       /* The (fsl_id_t) casts here are to work
                          around an inexplicable memory corruption
                          i am seeing in the va_list handling,
                          and kdbg doesn't work on Ubuntu 13.04 :-<
                       */
                       (int)FSL_TAGID_DATE, (fsl_id_t)rid, d->D,
                       (fsl_id_t)rid, d->U, d->C,
                       (int)FSL_TAGID_BGCOLOR, (fsl_id_t)rid,
                       (int)FSL_TAGID_USER, (fsl_id_t)rid,
                       (int)FSL_TAGID_COMMENT, (fsl_id_t)rid, d->D
                       );
      if(rc) goto end;
#if 0
      /*
        Legacy: need an option or crosslink callback for this.

        Reserve name 'fsl-timeline-wiki-links'

        but we have the problem that the last_insert_rowid() is likely
        to not be valid after this point. So how do we communicate
        that to the callback? Any links we generate are necessarily
        app-specific, meaning either we have to dictate paths to
        clients or require them to parse their own wiki content.
      */
      zCom = db_text(0, "SELECT coalesce(ecomment, comment) FROM event"
                        " WHERE rowid=last_insert_rowid()");
      wiki_extract_links(zCom, rid, 0, d->rDate, 1, WIKI_INLINE);
      free(zCom);
#endif
      /* 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 && (f->cache.seenManifest <= 0) ){
          f->cache.seenManifest = 1;
          rc = fsl_config_set_int32(f, FSL_CONFDB_REPO,
                                    "seen-delta-manifest", 1);
          if(rc) goto end;
      }
      assert(!rc);
    }/*!exists mlink*/
  }/*MANIFEST*/
  else if( FSL_CATYPE_CLUSTER == d->type ){
    /* Clean up the unclustered table... */
    fsl_size_t i;
    fsl_stmt * st = NULL;
    rc = fsl_db_prepare_cached(db, &st,
                               "DELETE FROM unclustered WHERE rid=?");
    if(rc) goto end;
    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);
        fsl_stmt_step(st);
        fsl_stmt_reset(st);
      }
    }
    fsl_stmt_cached_yield(st);
  }/*CLUSTER*/

  if( d->type==FSL_CATYPE_CONTROL
   || d->type==FSL_CATYPE_CHECKIN
   || d->type==FSL_CATYPE_EVENT
  ){
    /* Apply tags... */
    fsl_size_t i;
    fsl_list const * li = &d->T;
    fsl_double_t 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;
        goto end;
      }else if(0==tid){
        rc = fsl_cx_err_set(f, FSL_RC_RANGE,
                            "Could not get RID for [%.12s].",
                            tag->uuid);
        goto end;

      }
      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);
    }
    if(rc) goto end;
  }/*CONTROL | CHECKIN | EVENT*/
  else if( d->type==FSL_CATYPE_WIKI ){
    fsl_buffer buf = fsl_buffer_empty;
    char zLength[40] = {0};
    fsl_id_t tagid;
    fsl_id_t prior;
    char const * zWiki;
    char const * zTag;
    fsl_size_t nWiki = 0;
    fsl_buffer * cbuf = &f->scratch;
    rc = fsl_buffer_appendf(&buf, "wiki-%s", d->L);
    if(rc) goto wiki_end;
    zTag = fsl_buffer_cstr(&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. Grep the fsl_tag_id() "
                       "sources to determine its meaning.",
                       (fsl_id_t)tagid, zTag);
      goto wiki_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)
      /* 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,
                        rid, d->D, rid, NULL );
    if(rc) goto wiki_end;
    zTag = zWiki = NULL;
      /* TODO: cached statement */
    prior = fsl_db_g_id(db, 0,
                        "SELECT rid FROM tagxref"
                        " WHERE tagid=%"FSL_ID_T_PFMT
                        " AND mtime<%"FSL_JULIAN_T_PFMT
                        " ORDER BY mtime DESC",
                        (fsl_id_t)tagid, d->D);
    if(prior>0){
      rc = fsl_content_deltify(f, prior, rid, 0);
      if(rc) goto wiki_end;
    }
    /* WIKI constructs currently (201308) do not allow a C card.
       When/if they do, use it here.
    */
    cbuf->used = 0 /* re-use memory for comment text */;
    if( nWiki > 0 ){
      rc = fsl_buffer_appendf(cbuf,
                              "Changes to wiki page [%h]",
                              d->L);
    }else{
      rc = fsl_buffer_appendf(cbuf,
                              "Emptied wiki page [%h]",
                              d->L);
    }
    if(!rc){
      /* TODO: cached statement */
      fsl_db_exec(db,
                  "REPLACE INTO event(type,mtime,objid,user,comment,"
                  "                  bgcolor,euser,ecomment) "
                  "VALUES("
                  " 'w',"
                  " %"FSL_JULIAN_T_PFMT","
                  " %"FSL_ID_T_PFMT","
                  " %Q, %B,"
                  " (SELECT value FROM tagxref "
                  "  WHERE tagid=%"FSL_ID_T_PFMT
                  "  AND rid=%"FSL_ID_T_PFMT
                  "  AND tagtype>1),"
                  " (SELECT value FROM tagxref "
                  "  WHERE tagid=%"FSL_ID_T_PFMT
                  "  AND rid=%"FSL_ID_T_PFMT"),"
                  " (SELECT value FROM tagxref WHERE "
                  "  tagid=%"FSL_ID_T_PFMT
                  "  AND rid=%"FSL_ID_T_PFMT")"
                  ");",
                  d->D, (fsl_id_t)rid, d->U, cbuf, 
                  FSL_TAGID_BGCOLOR, (fsl_id_t)rid,
                  FSL_TAGID_BGCOLOR, (fsl_id_t)rid,
                  FSL_TAGID_USER, (fsl_id_t)rid,
                  FSL_TAGID_COMMENT, (fsl_id_t)rid
                  );
    }
    
    wiki_end:
    fsl_buffer_clear(&buf);
    if(rc) goto end;
  }/*WIKI*/

  if( d->type==FSL_CATYPE_EVENT ){
    char buf[FSL_UUID_STRLEN + 7 /* event-UUID */] = {0};
    char zLength[40] = {0};
    fsl_id_t tagid;
    fsl_id_t prior, subsequent;
    char const * zWiki;
    char const * zTag;
    fsl_size_t nWiki = 0;
    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].",
                       (fsl_id_t)tagid, zTag);
      goto event_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,
                        rid, d->D, rid, NULL );
    if(rc) goto event_end;
    /* TODO: cached statement */
    prior = 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 DESC",
                        (fsl_id_t)tagid, d->D, (fsl_id_t)rid);
    if(prior<0){
      assert(db->error.code);
      rc = fsl_cx_uplift_db_error(f, db);
      goto event_end;
    }
    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",
                             (fsl_id_t)tagid, d->D, (fsl_id_t)rid);
    if(subsequent<0){
      assert(db->error.code);
      rc = fsl_cx_uplift_db_error(f, db);
      goto event_end;
    }
    else if( prior > 0 ){
      rc = fsl_content_deltify(f, prior, 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")",
                         (fsl_id_t)tagid, (fsl_id_t)tagid);
      }
    }
    if(rc) goto event_end;
    if( subsequent>0 ){
      rc = fsl_content_deltify(f, rid, subsequent, 0);
    }else{
      /* TODO: cached statement */
      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, (fsl_id_t)rid,
                       (fsl_id_t)tagid,
                       d->U, d->C, 
                       (int)FSL_TAGID_BGCOLOR,
                       (fsl_id_t)rid
      );
    }
    event_end:
    if(rc) goto end;
  }/*EVENT*/
  else if( d->type==FSL_CATYPE_TICKET ){
    /*
      TODO: huge block from manifest_crosslink().  A full port
      requires other infrastructure for collapsing relatively close
      time values into the same time for timeline purposes. i'd prefer
      to farm this out to a crosslink callback.  Even then, the future
      of tickets in libfossil is uncertain, but we should crosslink
      them so that repos stay compatible with fossil(1) without
      requiring a rebuild using fossil(1).
    */
    rc = fsl_cx_err_set(f, FSL_RC_NYI,
                        "MISSING: a huge block of TICKET stuff from "
                        "manifest_crosslink(). It requires infrastructure "
                        "libfossil does not yet have.");
    
    if(rc) goto end;
  }/*TICKET*/
  else if( d->type==FSL_CATYPE_ATTACHMENT ){
    fsl_buffer comment = fsl_buffer_empty;
    char const attachedToType = fsl_is_uuid(d->A.tgt)
      ? 't' /* applies to a ticket */
      : 'w' /* applies to a wiki page named d->A.tgt */;
    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);",
                     (fsl_id_t)rid, d->D,
                     d->A.src, d->A.tgt, d->A.name,
                     (d->C ? d->C : ""), d->U);
    if(rc) goto end;
    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);
    if(rc) goto end;

    if('w'==attachedToType){
      /* Attachment applies to a wiki page */
      if(d->A.src && *d->A.src){
        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{
      /* Attachment applies to a ticket */
      if(d->A.src && *d->A.src){
        rc = fsl_buffer_appendf(&comment,
                                "Add attachment \"%h\" "
                                "to ticket [%s|%.10s]",
                                d->A.name, d->A.tgt, d->A.tgt);
      }else{
        rc = fsl_buffer_appendf(&comment,
                                "Delete attachment \"%h\" "
                                "from ticket [%s|%.10s]",
                                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)",
                       attachedToType, d->D, (fsl_id_t)rid,
                       d->U, &comment);
    }
    fsl_buffer_clear(&comment);
    if(rc) goto end;
    /* Milestone: this particular block is one of the very few of the
       ported-over fossil(1) bits which is not significantly longer
       than the original implementation :).
    */
  }/*ATTACHMENT*/
  else if( d->type==FSL_CATYPE_CONTROL){
    /*
      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).
    */
    fsl_buffer comment = fsl_buffer_empty;
    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;
    /**
       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_ctl;
      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, 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_ctl;
        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, (fsl_id_t)rid, d->U,
                       (comment.used>1)
                       ? (fsl_buffer_cstr(&comment)
                          +1/*leading space on all entries*/)
                       : NULL);
    }
    end_ctl:
    fsl_buffer_clear(&comment);
    if(rc) goto end;
  }/*CONTROL*/

  assert(0==rc);

  /* Call any crosslink callbacks... */
  if(f->xlinkers.list){
    fsl_size_t i;
    fsl_xlinker * xl = NULL;
    for( i = 0; !rc && (i < f->xlinkers.used); ++i ){
      xl = f->xlinkers.list+i;
      rc = xl->f( f, d, xl->state );
    }
    if(rc){
      assert(xl);
      if(!f->error.code){
        fsl_cx_err_set(f, rc, "Crosslink callback handler "
                       "'%s' failed with code %d (%s) for "
                       "artifact [%.12s].",
                       xl->name, rc, fsl_rc_cstr(rc),
                       d->uuid);
      }
      goto end;
    }
  }
  
  end:
  if(!rc){
    rc = fsl_db_transaction_end(db, 0);
  }else{
    if(db->error.code && !f->error.code){
      fsl_cx_uplift_db_error(f,db);
    }
    fsl_db_transaction_end(db, 1);
  }
  return rc;
}/*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.
 */
static char fsl_after_blank_line(const char *z){
  if( z[-1]!='\n' ) return 0;
  if( z[-2]=='\n' ) return 1;
  if( z[-2]=='\r' && z[-3]=='\n' ) return 1;
  return 0;
}

/**
    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_mf_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_MD5_STRLEN+1];
    unsigned char const * zHash = ca+n-FSL_MD5_STRLEN-1;
    fsl_md5_cx md5 = fsl_md5_cx_empty;
    unsigned char const * zHashEnd =
      ca + n -
      2 /* 'Z ' */
      - FSL_MD5_STRLEN
      - 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_MD5_STRLEN))
      ? 1
      : -1;
  }
}

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( 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;
  n -= i;
  *pz = z;
  for(i=n-1; i>=0; i--){
    if( z[i]=='\n' && memcmp(&z[i],"\n-----BEGIN PGP SIGNATURE-", 25)==0 ){
      n = i+1;
      break;
    }
  }
  *pn = (fsl_size_t)n;
  return;
}


/**
    Internal helper for parsing manifests. Holds a source file (memory
    range) and gets updated by fsl_mf_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.
   */
  char 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.  If
   pLen is not NULL and this function returns non-NULL then *pLen is
   set to the byte length of the new token.
*/
static unsigned char *fsl_mf_next_token(fsl_src *p, fsl_size_t *pLen){
  unsigned char *z;
  unsigned char *zStart;
  int c;
  if( p->atEol ) return NULL;
  zStart = z = p->z;
  while( (c=(*z))!=' ' && c!='\n' ){ ++z; }
  *z = 0;
  p->z = &z[1];
  p->atEol = c=='\n';
  if( pLen ) *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 mf_next_card(fsl_src *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 = 0;
  }else if( p->z[1]=='\n' ){
    p->z += 2;
    p->atEol = 1;
  }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_CATYPE_ANY if the result is ambiguous.
   
    It should guess right for any legal manifests, but it does not go
    out of its way to detect incomplete/invalid ones.
 */
static fsl_catype_t fsl_mf_guess_type( int * l ){
#if 0
  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.
  */
#define L(X) l[X-'A']
  if(L('M')) return FSL_CATYPE_CLUSTER;
  else if(L('E')) return FSL_CATYPE_EVENT;
  else if(L('L') || L('W')) return FSL_CATYPE_WIKI;
  else if(L('J') || L('K')) return FSL_CATYPE_TICKET;
  else if(L('A')) return FSL_CATYPE_ATTACHMENT;
  else if(L('B') || L('F') || L('P') || L('Q') || L('R')) return FSL_CATYPE_CHECKIN;
  else if(L('D') && L('T') && L('U')) return FSL_CATYPE_CONTROL;
#undef L
  return FSL_CATYPE_ANY;
}

int fsl_deck_parse2(fsl_deck * d, fsl_buffer * 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_CA_SYNTAX,MSG)
  char isRepeat = 0/* , hasSelfRefTag = 0 */;
  int rc = 0;
  fsl_src x = fsl_src_empty;
  char const * zMsg = NULL;
  fsl_id_bag * seen;
  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;
  fsl_double_t ts;
  int cardCount = 0;
  fsl_buffer selfHash = fsl_buffer_empty;
  fsl_db * db;
  fsl_cx * f;
  fsl_error * err;
  int stealBuf = 0 /* gets incremented if we need to steal src->mem. */;
  /*
    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. We use
    an int (instead of char) so we can keep track of how many
    of each card we've seen.
   */
  int lettersSeen[27] = {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,0,0};
  if(!d || !z) return FSL_RC_MISUSE;
  /* Every control artifact ends with a '\n' character.  Exit early
     if that is not the case for this artifact.
  */
  f = d->f;
  err = f ? &f->error : &d->error;
  if(!*z || !n || ( '\n' != z[n-1]) ){
    return fsl_error_set(err, FSL_RC_CA_SYNTAX, "%s.",
                         n ? "Not terminated with \\n"
                         : "Zero-length input");
  }
  else if(rid<0){
    return fsl_error_set(err, FSL_RC_RANGE,
                         "Invalid (negative) RID %"FSL_ID_T_PFMT
                         " for fsl_deck_parse()", (fsl_id_t)rid);
  }
  /*
    Reminder: i'm leaving out v1's manifest cache for the time
    being. We'll probably need/want it when rebuild is implemented.
  */
  db = f ? fsl_cx_db_repo(f) : NULL
    /* We originally needed the db for converting date strings to
       doubles for julian (we don't anymore). Since we have it...
       we also use it to populate d->B.uuid and d->rid.
    */;


  seen = f ? &f->cache.mfSeen : NULL;
  if(seen){
    if((0==rid) || fsl_id_bag_contains(seen,rid)){
      isRepeat = 1;
    }else{
      isRepeat = 0;
      rc = fsl_id_bag_insert(seen, rid);
      if(rc){
        assert(FSL_RC_OOM==rc);
        return rc;
      }
    }
  }

  rc = fsl_sha1sum_buffer(src, &selfHash);
  if(rc) return rc;

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

  /*
    Strip off the PGP signature if there is one.

    Reminder to self: example of signed manifest:

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

  /*
      Verify that the first few characters of the artifact look like a
      control artifact.
   */
  if( n<10 || z[0]<'A' || z[0]>'Z' || z[1]!=' ' ){
    ERROR(FSL_RC_CA_SYNTAX, "Content does not look like "
          "a control artifact");
  }

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

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

    A side-effect of our OO API is that we potentially end up
    strdup'ing much of this memory into d->CARD. What we could do (and
    what i think v1 does?) is instead bypass the OO bits and assign
    all the pointers back into this modified copy, which we would then
    give over to the manifest (and save tons of mallocs). IFF manifest
    parsing becomes prohibitively memory expensive, that would be the
    first big optimization to make, but it _would_ have side-effects
    and change the way a manifest and its constituent parts are
    cleaned up. We might want to make separate input and output
    manifest classes, with the OO API for output manifests (created by
    code) and input manifests optimized for read-only use and not
    duplicating the strings we currently dupe here. If we do that, we
    need to change this function to take over ownership of the input
    blob's contents.

    As of mid-201403, we recycle as much as possible from the source
    buffer and take over ownership _if_ we do so.
  */
  fsl_deck_clean(d);
  fsl_deck_init(f, d, FSL_CATYPE_ANY);
  /* Now parse, card by card... */
  x.z = z;
  x.zEnd = z+n;
  x.atEol= 1;

  /* Parsing helpers... */
#define TOKEN(DEFOS) tokLen=0; token = fsl_mf_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(LEN,MSG) if(token \
                                   && ((LEN)!=tokLen ||                 \
                                       !fsl_validate16((char const *)token,(LEN)))){ \
                                     SYNTAX(MSG); }
#define TOKEN_UUID(CARD) TOKEN_CHECKHEX(FSL_UUID_STRLEN,"Malformed UUID in " #CARD "-card")
  /**
     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 = mf_next_card(&x)));
       cPrevType = cType, ++cardCount ){
    if(cType<cPrevType){
      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");
    }
    if(FSL_CATYPE_ANY==d->type){
      /* See if we can guess the type now. */
      d->type = fsl_mf_guess_type(lettersSeen);
      if(FSL_CATYPE_ANY!=d->type){
        assert(FSL_CATYPE_INVALID!=d->type);
#if 0
        MARKER(("Guessed manifest type on try #%d: %s\n",
                cardCount+1, fsl_catype_cstr(d->type)));
#endif
      }
    }else{
      /* If we know the deck type, make sure this card fits. */
      if( !fsl_deck_check_type(d, cType) ){
        rc = FSL_RC_TYPE;
        goto bailout;
      }
    }
    switch(cType){
      /*
             A <filename> <target> ?<source>?
        
         Identifies an attachment to either a wiki page or a ticket.
         <source> is the artifact that is the attachment.  <source>
         is omitted to delete an attachment.  <target> is the name of
         a wiki page or ticket to which that attachment is connected.
      */
      case 'A':{
        unsigned char * name, * src;
        if(1<SEEN(A)){
          ERROR(FSL_RC_RANGE,"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);*/
        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;
        /* 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" 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;
        /* rc = fsl_deck_E_set(d, ts, (char const *)token); */
        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 * oldName = NULL;
        fsl_file_perm_t perm = FSL_FILE_PERM_REGULAR;
        fsl_card_F * fc = NULL
          /* As a malloc() optimization, rather than add these
             F-cards using fsl_deck_F_add(), we will apply the
             fsl_card_F::externalStrings optimization, pointing
             the various fc->xxx string members back to the
             original tokens rather than strdup()'ing them.
          */;
        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 'w': perm = FSL_FILE_PERM_REGULAR; break;
            case 'x': perm = FSL_FILE_PERM_EXE; break;
            case 'l': perm = FSL_FILE_PERM_LINK; break;
            default:
              assert(!"Unmatched perms string character!");
              ERROR(FSL_RC_ERROR,"Internal error: unmatched perms string character");
          }
          TOKEN(0);
          if(token) oldName = (char *)token;
        }
        fc = fsl_malloc( sizeof(fsl_card_F) );
        if(!fc){
          rc = FSL_RC_OOM;
          zMsg = "OOM";
          goto bailout;
        }
        ++stealBuf;
        *fc = fsl_card_F_empty;
        fc->externalStrings = 1;
        fc->name = name;
        fc->priorName = oldName;
        fc->uuid = (char *)uuid;
        fc->perm = perm;
        rc = fsl_list_append(&d->F.list, fc);
        if(rc) fsl_card_F_free(fc);
        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;
        char 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);
        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;
        /* rc = fsl_deck_K_set(d, (char const *)token); */
        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;
        /* rc = fsl_deck_L_set(d, (char const *)token, (fsl_int_t)tokLen); */
        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;
        rc = fsl_list_append(&d->M, token);
        break;
      }
      /*
            N <uuid>
        
         An N-line identifies the mimetype of wiki or comment text.
      */
      case 'N':{
        if(1<SEEN(N)){
          ERROR(FSL_RC_RANGE,"Multiple N-cards");
        }
        TOKEN(0);
        TOKEN_EXISTS("Missing UUID on N-card");
        ++stealBuf;
        d->N = (char *)token;
        /* rc = fsl_deck_N_set(d, (char const *)token, (fsl_int_t)tokLen); */
        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)){
          ERROR(FSL_RC_RANGE,"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);
          /* rc = fsl_deck_P_add(d, (char const *)token); */
          ++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':{
        char prefix;
        TOKEN(0);
        TOKEN_EXISTS("Missing target UUID for Q-card");
        prefix = (char)*token;
        if('-'!=prefix && '+'!=prefix){
          SYNTAX("Malformed target UUID in Q-card");
        }
        uuid = ++token; --tokLen;
        TOKEN_UUID(Q);
        TOKEN(0);
        if(token){
          TOKEN_UUID(Q);
        }
        rc = fsl_deck_Q_add(d, prefix, (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)){
          ERROR(FSL_RC_RANGE,"More than one R-card");
        }
        TOKEN(0);
        TOKEN_EXISTS("Missing MD5 token in R-card");
        TOKEN_CHECKHEX(FSL_MD5_STRLEN,"Malformed MD5 token in R-card");
        d->R = (char *)token;
        ++stealBuf;
        /* rc = fsl_deck_R_set(d, (char const *)token); */
        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_tag_type tagType = FSL_TAGTYPE_INVALID;
        TOKEN(0);
        TOKEN_EXISTS("Missing name for T-card");
        name = token;
        TOKEN(0);
        TOKEN_EXISTS("Missing UUID on T-card");
        if(FSL_UUID_STRLEN==tokLen){
          TOKEN_UUID(T);
          /* A valid UUID */
          if(FSL_CATYPE_EVENT==d->type){
            SYNTAX("Non-self-referential T-card in Event artifact");
          }
          uuid = token;
        }else if( 1==tokLen && '*'==(char)*token ){
          /* hasSelfRefTag = 1; */
          /* tag for the current artifact */
          if((FSL_CATYPE_EVENT==d->type) &&
             ('+'!=(char)*name)){
            SYNTAX("Non-+ T-card in event");
          }
          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; break;
          case '-': tagType = FSL_TAGTYPE_CANCEL; break;
          default: SYNTAX("Malformed tag name");
        }
        ++name /* skip type marker byte */;
        /* Potential todo: add the order check from this commit:

        http://fossil-scm.org/index.html/info/55cacfcace
        
         */
        rc = fsl_deck_T_add(d, tagType, (fsl_uuid_cstr)uuid,
                            (char const *)name,
                            (char const *)value);
        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", 9);
        }
        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){
          ERROR(FSL_RC_RANGE,"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;
        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_CA_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:
  if(FSL_CATYPE_ANY==d->type){
    rc = fsl_cx_err_set(f, FSL_RC_ERROR,
                        "Internal error: could not determine type of "
                        "control 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_CA_SYNTAX,
                            "Determined during post-parse processing that "
                            "the parsed deck (type %s) contains an illegal "
                            "card type (%c).", fsl_catype_cstr(d->type),
                            'A'+i);
        goto bailout;
      }
    }
  }
  assert(FSL_CATYPE_CHECKIN==d->type ||
         FSL_CATYPE_CLUSTER==d->type ||
         FSL_CATYPE_CONTROL==d->type ||
         FSL_CATYPE_WIKI==d->type ||
         FSL_CATYPE_TICKET==d->type ||
         FSL_CATYPE_ATTACHMENT==d->type ||
         FSL_CATYPE_EVENT==d->type);
  assert(0==rc);
  if(db){
    d->rid = fsl_uuid_to_rid(f, fsl_buffer_cstr(&selfHash));
    if(d->rid<0){
      assert(f->error.code);
      goto bailout;
    }
    /*
      We don't care (at this point in time) if it's not found
      (0==d->rid)
    */
  }
  d->uuid = fsl_buffer_str(&selfHash) /* transfer ownership */;
  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;
  }
  return 0;

  bailout:
  assert(0 != rc);
  fsl_buffer_clear(&selfHash);
  if(zMsg){
    fsl_error_set(err, rc, "%s", zMsg);
  }else if(f){
    if(!f->error.code){
      if(d->error.code){
        fsl_error_move( &d->error, &f->error );
      }else if(db && db->error.code){
        /* This is "essentially impossible". */
        fsl_cx_uplift_db_error(f, db);
      }
    }
  }
  return rc;
#undef SEEN
#undef TOKEN_DATETIME
#undef SYNTAX
#undef TOKEN_CHECKHEX
#undef TOKEN_EXISTS
#undef TOKEN_UUID
#undef TOKEN
#undef ERROR
}

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

int fsl_deck_load_rid( fsl_cx * f, fsl_deck * d,
                       fsl_id_t rid, fsl_catype_t type ){
  fsl_buffer buf = fsl_buffer_empty;
  int rc;
  if(!f || !d) return FSL_RC_CA_SYNTAX;
  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, (fsl_id_t)rid);
  }
  rc = fsl_content_get(f, rid, &buf);
  if(!rc){
    fsl_deck_clean(d);
    fsl_deck_init(f, d, FSL_CATYPE_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){
      assert(rid == d->rid);
      if( type!=FSL_CATYPE_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.",
                            (fsl_id_t)rid,
                            fsl_catype_cstr(d->type),
                            fsl_catype_cstr(type));
      }
    }
  }
  fsl_buffer_clear(&buf);
  return rc;
}

int fsl_deck_load_sym( fsl_cx * f, fsl_deck * d,
                       char const * symbolicName, fsl_catype_t type ){
  if(!f || !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 || !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")",
                  (fsl_id_t)d->rid, (fsl_id_t)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, (fsl_id_t)d->rid);
  }else{
    rc = fsl_deck_load_rid(f, &bl, rid, FSL_CATYPE_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 && (d->B.baseline || !d->B.uuid))
    ? 0
    : ((d && d->f)
       ? fsl_deck_baseline_load(d)
       : FSL_RC_MISUSE);
}

int fsl_deck_F_rewind( fsl_deck * d ){
  int rc = 0;
  assert(d->f);
  d->F.cursor = 0;
  assert(d->f);
  if(!rc && d->B.uuid){
    rc = fsl_deck_baseline_fetch(d);
    if(!rc){
      assert(d->B.baseline);
      d = d->B.baseline;
      d->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) ((fsl_card_F const *)(DECK)->F.list.list[NDX])
  *rv = NULL;
  if(!d->B.baseline){
    /* Manifest p 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 UUID (%.*s) but no baseline "
                            "loaded.  Load the baseline before calling "
                            "fsl_deck_F_next().",
                            12, d->B.uuid)
        /* We "could" just load the baseline from here. */;
    }
    if( d->F.cursor < (fsl_int_t)d->F.list.used ){
      *rv = FCARD(d, d->F.cursor++);
      assert(*rv);
      assert((*rv)->uuid && "Baseline manifest has deleted F-card entry!");
    }
    return 0;
  }else{
    /* Manifest p is a delta-manifest.  Scan the baseline but amend the
       file list in the baseline with changes described by p.
    */
    fsl_deck *pB = d->B.baseline;
    int cmp;
    while(1){
      if( pB->F.cursor >= (fsl_int_t)pB->F.list.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.list.used ) *rv = FCARD(d, d->F.cursor++);
        break;
      }else if( d->F.cursor >= (fsl_int_t)d->F.list.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.list.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.
        */
#if 1
        /*
          20140324: KEEP THIS #if BLOCK for a while in case the #else
          impl bites us!  This is a fundamental change to a core
          moving part of the Manifest machine, and changing it (as is
          done in the #else block) may... have unforeseen
          side-effects.
        */
        
        /* Skip them both.  Repeat the loop to find the next
           non-delete entry. */
        pB->F.cursor++;
        d->F.cursor++;
        continue;
#else
        /*
          Historically, fossil(1) elides all deleted entries in F-card
          traversal. libfossil changed that to report them, initially
          due to a misunderstanding in how deletions are recorded in
          baseline manifests[1], but also because it is "sometimes"
          interesting to know about them during card traversal.
          Probably not as often as initially thought, but there we
          are. Eliding them altogether just seems kinda wrong.
          However, _almost_ all code which uses this routine skips
          over them.

          But... the other bits in this while loop will happily return
          deleted entries, so why shouldn't we?

          [1] = i _thought_, due to a mis-interpretation of the Fossil
          File Format docs, that baselines recorded deletions the same
          way as deltas, but they don't (they simply do not include an
          F-card for deleted entries). Thus reporting of deleted
          entries leads to an unfortunate inconsistency in that we do
          _not_ report them a baseline, and cannot without also
          traversing its parent version. It seems that fossil(1) also
          internally has a minor incosistency, in that as long as a
          repo has no delta manifests (fossil(1) still has none as of
          20140324), the repo actually has no way of knowing if a
          given blob was ever "rm'd" from the repo. It can only know
          that once deltas appear. Hmm. That's never come up before.
        */
        pB->F.cursor++;
        *rv = FCARD(d, d->F.cursor++);
        break;
#endif
      }      
    }
    return 0;
  }
#undef FCARD
}

int fsl_deck_save( fsl_deck * d, char isPrivate ){
  int rc;
  fsl_cx * f = d ? d->f : NULL;
  fsl_db * db = f ? fsl_needs_repo(f) : NULL;
  fsl_buffer buf = fsl_buffer_empty;
  fsl_id_t newRid = 0;
  char const oldPrivate = f ? f->cache.markPrivate : 0;
  char const isNew = d && !d->uuid;
  if(!f || !d ) return FSL_RC_MISUSE;
  else if(!db) return FSL_RC_NOT_A_REPO;
  else if( (d->rid>0 && !d->uuid) ||
           (d->rid<=0 && d->uuid)){
    return fsl_cx_err_set(f, FSL_RC_MISUSE,
                          "The input deck must have either rid _and_ UUID "
                          "or neither of rid/UUID. A mixture is ambiguous.");
  }

  rc = fsl_deck_unshuffle(d,
                          (FSL_CX_F_CALC_R_CARD & f->flags)
                          ? ((d->F.list.used && !d->R) ? 1 : 0)
                          : 0);
  if(rc) return rc;
  
  rc = fsl_deck_output(d, fsl_output_f_buffer, &buf, &f->error);
  if(rc) return rc;

  rc = fsl_db_transaction_begin(db);
  if(rc){
    fsl_buffer_clear(&buf);
    return rc;
  }

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

  f->cache.markPrivate = isPrivate;
  rc = fsl_content_put_ex(f, &buf, d->uuid, 0,
                          0U, isPrivate, &newRid);
  if(rc) goto end;
  assert(newRid>0);

  rc = fsl_sha1sum_buffer( &buf, &buf );
  if(rc) goto end;

  /* We need d->uuid and d->rid for crosslinking purposes, but will
     unset them on error (if we set them) because their values will no
     longer be in the db after rollback...
  */
#if 0
  /* practice shows that this is not needed/desired. There are cases
     where round-tripping a manifest results in a 1-millisecond
     difference in the D-card (Julian Day/double), which changes the
     has but otherwise has no difference on the meaning. That said,
     the way we use decks means that we really don't care (for
     purposes of this function) what their initial UUID is except for
     the purpose of fsl_content_put_ex(), above.
  */
  if(d->uuid){
    /* Compare original and resulting hashes. */
    if(0 != fsl_uuidcmp(fsl_buffer_cstr(&buf), d->uuid)){
      rc = fsl_cx_err_set(f, FSL_RC_CONSISTENCY,
                          "Input deck's original UUID and resulting UUID "
                          "do not match: [%s] vs [%b]",
                          d->uuid, &buf);
      goto end;
    }
    /* ??? assert(d->rid==newRid); */
    d->rid = newRid;
  }else
#endif  
  {
    fsl_free(d->uuid);
    d->uuid = fsl_buffer_str(&buf) /* transfer ownership */;
    buf = fsl_buffer_empty;
    d->rid = newRid;
  }

#if 0
  /* Something to consider: if d is new and 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(isNew && d->P.used){
    fsl_id_t pid;
    assert(FSL_CATYPE_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(isNew && (FSL_CATYPE_WIKI==d->type)){
    /* Analog to v1's wiki.c:wiki_put(): */
    /*
      MISSING:
      v1's wiki.c:wiki_put() handles the moderation bits.
    */
    if(d->P.used){
      fsl_id_t 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 = fsl_deck_crosslink(d);

  end:
  f->cache.markPrivate = oldPrivate;
  if(!rc) rc = fsl_db_transaction_end( db, 0);
  else fsl_db_transaction_end(db, 1);
  if(rc){
    if(isNew){
      fsl_free(d->uuid);
      d->uuid = NULL;
      d->rid = 0;
    }
    if(!f->error.code){
      if(d->error.code){
        rc = fsl_cx_err_set_e(f, &d->error);
      }else if(db->error.code){
        rc = fsl_cx_uplift_db_error(f, db);
      }
    }
  }
  fsl_buffer_clear(&buf);
  return rc;
}

#undef MARKER
#undef AGE_FUDGE_WINDOW
#undef AGE_ADJUST_INCREMENT