Login
cx.c at [419099b063]
Login

File src/cx.c artifact 48eeef4d63 part of check-in 419099b063


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

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

  Heavily indebted to the Fossil SCM project (https://fossil-scm.org).
*/
/*************************************************************************
  This file houses most of the context-related APIs.
*/
#include "fossil-scm/fossil-internal.h"
#include "fossil-scm/fossil-checkout.h"
#include "fossil-scm/fossil-confdb.h"
#include "fossil-scm/fossil-hash.h"
#include "fossil-ext_regexp.h"
#include "fossil-vtbl_fossil_settings.h"
#include "sqlite3.h"
#include <assert.h>

#if defined(_WIN32)
# include <windows.h>
# define F_OK 0
# define W_OK 2
#else
# include <unistd.h> /* F_OK */
#endif

#include <stdlib.h>

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


/**
   Used for setup and teardown of sqlite3_auto_extension().
*/
static volatile long sg_autoregctr = 0;

int fsl_cx_init( fsl_cx ** tgt, fsl_cx_init_opt const * param ){
  static fsl_cx_init_opt paramDefaults = fsl_cx_init_opt_default_m;
  int rc = 0;
  fsl_cx * f;
  extern int fsl_cx_install_timeline_crosslinkers(fsl_cx *f);
  if(!tgt) return FSL_RC_MISUSE;
  else if(!param){
    if(!paramDefaults.output.state.state){
      paramDefaults.output.state.state = stdout;
    }
    param = &paramDefaults;
  }
  if(*tgt){
    void const * allocStamp = (*tgt)->allocStamp;
    fsl_cx_reset(*tgt, 1) /* just to be safe */;
    f = *tgt;
    *f = fsl_cx_empty;
    f->allocStamp = allocStamp;
  }else{
    f = fsl_cx_malloc();
    if(!f) return FSL_RC_OOM;

    *tgt = f;
  }
  f->output = param->output;
  f->cxConfig = param->config;

#if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0
  sqlite3_initialize(); /*the SQLITE_MUTEX_STATIC_MASTER will not cause autoinit of sqlite for some reason*/
  sqlite3_mutex_enter(sqlite3_mutex_alloc(SQLITE_MUTEX_STATIC_MASTER));
#endif
    if ( 1 == ++sg_autoregctr )
    {
      /*register our statically linked extensions to be auto-init'ed at the appropriate time*/
      sqlite3_auto_extension((void(*)(void))(sqlite3_regexp_init));     /*sqlite regexp extension*/
      atexit(sqlite3_reset_auto_extension)
        /* Clean up pseudo-leak valgrind complains about:
           https://www.sqlite.org/c3ref/auto_extension.html */;
    }
#if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0
  sqlite3_mutex_leave(sqlite3_mutex_alloc(SQLITE_MUTEX_STATIC_MASTER));
#endif
  f->dbMem.f = f /* so that dbMem gets fsl_xxx() SQL funcs installed. */;
  rc = fsl_db_open( &f->dbMem, "", 0 );
  if(!rc){
    /* Attempt to ensure that the TEMP tables/indexes use FILE storage. */
    rc = fsl_db_exec(&f->dbMem, "PRAGMA temp_store=FILE;");
  }
  if(!rc){
    rc = fsl_cx_install_timeline_crosslinkers(f);
  }
  if(rc){
    if(f->dbMem.error.code){
      fsl_cx_uplift_db_error(f, &f->dbMem);
    }
  }else{
    f->dbMain = &f->dbMem;
    f->dbMem.role = FSL_DBROLE_MAIN;
  }
  return rc;
}
    
void fsl_cx_reset(fsl_cx * f, bool closeDatabases){
  fsl_checkin_discard(f);
#define SFREE(X) fsl_free(X); X = NULL
  if(closeDatabases){
    fsl_cx_close_dbs(f);
    /*
      Reminder: f->dbMem is NOT closed here: it's an internal detail,
      not public state. We could arguably close and reopen it here,
      but then we introduce a potenital error case (OOM) where we
      currently have none (thus the void return).
    */
    SFREE(f->ckout.dir);
    f->ckout.dirLen = 0;
    /* assert(NULL==f->dbMain); */
    assert(!f->repo.db.dbh);
    assert(!f->ckout.db.dbh);
    assert(!f->config.db.dbh);
    assert(!f->repo.db.filename);
    assert(!f->ckout.db.filename);
    assert(!f->config.db.filename);
  }
  SFREE(f->repo.user);
  SFREE(f->ckout.uuid);
  SFREE(f->cache.projectCode);
#undef SFREE
  fsl_error_clear(&f->error);
  fsl_card_J_list_free(&f->ticket.customFields, 1);
  fsl_buffer_clear(&f->scratch);
  fsl_buffer_clear(&f->fsScratch);
  fsl_buffer_clear(&f->fileContent);
  fsl_acache_clear(&f->cache.arty);
  fsl_id_bag_clear(&f->cache.leafCheck);
  fsl_id_bag_clear(&f->cache.toVerify);
  fsl_cx_clear_mf_seen(f);
#define SLIST(L) fsl_list_visit_free(L, 1)
#define GLOBL(X) SLIST(&f->cache.globs.X)
  GLOBL(ignore);
  GLOBL(binary);
  GLOBL(crnl);
#undef GLOBL
#undef SLIST
  while( f->cache.mf.head ){
    fsl_deck * next = f->cache.mf.head->next;
    f->cache.mf.head->next = NULL;
    fsl_deck_finalize(f->cache.mf.head);
    f->cache.mf.head = next;
  }
  f->cache = fsl_cx_empty.cache;
}

void fsl_cx_clear_mf_seen(fsl_cx * f){
  fsl_id_bag_clear(&f->cache.mfSeen);
}

void fsl_cx_finalize( fsl_cx * f ){
  void const * allocStamp = f ? f->allocStamp : NULL;
  if(!f) return;

  if(f->xlinkers.list){
#if 0
    /* Potential TODO: add client-specified finalizer for xlink
       callback state, using a fsl_state to replace the current
       (void*) for x->state. Seems like overkill for the time being.
     */
    fsl_size_t i;
    for( i = 0; i < f->xlinkers.used; ++i ){
      fsl_xlinker * x = f->xlinkers.list + i;
      if(x->state.finalize.f){
        x->state.finalize.f(x->state.finalize.state, x->state.state);
      }
    }
#endif    
    fsl_free(f->xlinkers.list);
    f->xlinkers = fsl_xlinker_list_empty;
  }

  if(f->clientState.finalize.f){
    f->clientState.finalize.f( f->clientState.finalize.state,
                               f->clientState.state );
  }
  f->clientState = fsl_state_empty;
  if(f->output.state.finalize.f){
    f->output.state.finalize.f( f->output.state.finalize.state,
                                f->output.state.state );
  }
  f->output = fsl_outputer_empty;
  fsl_cx_reset(f, 1);
  fsl_db_close(&f->dbMem);
  *f = fsl_cx_empty;
  if(&fsl_cx_empty == allocStamp){
    fsl_free(f);
  }else{
    f->allocStamp = allocStamp;
  }

  /* clean up the auto extension; not strictly necessary, but pleases debug malloc's */
#if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0
  sqlite3_mutex_enter(sqlite3_mutex_alloc(SQLITE_MUTEX_STATIC_MASTER));
#endif
  if ( 0 == --sg_autoregctr )
  {
    /*register our statically linked extensions to be auto-init'ed at the appropriate time*/
    sqlite3_cancel_auto_extension((void(*)(void))(sqlite3_regexp_init));     /*sqlite regexp extension*/
  }
  assert(sg_autoregctr>=0);
#if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0
  sqlite3_mutex_leave(sqlite3_mutex_alloc(SQLITE_MUTEX_STATIC_MASTER));
#endif
}

void fsl_cx_err_reset(fsl_cx * f){
  if(f){
    fsl_error_reset(&f->error);
    fsl_db_err_reset(&f->dbMem);
    fsl_db_err_reset(&f->repo.db);
    fsl_db_err_reset(&f->config.db);
    fsl_db_err_reset(&f->ckout.db);
  }
}

int fsl_cx_err_set_e( fsl_cx * f, fsl_error * err ){
  if(!f) return FSL_RC_MISUSE;
  else if(!err){
    return fsl_cx_err_set(f, 0, NULL);
  }else{
    fsl_error_move(err, &f->error);
    fsl_error_clear(err);
    return f->error.code;
  }
}

int fsl_cx_err_setv( fsl_cx * f, int code, char const * fmt,
                     va_list args ){
  return f
    ? fsl_error_setv( &f->error, code, fmt, args )
    : FSL_RC_MISUSE;
}

int fsl_cx_err_set( fsl_cx * f, int code, char const * fmt,
                    ... ){
  if(!f) return FSL_RC_MISUSE;
  else{
    int rc;
    va_list args;
    va_start(args,fmt);
    rc = fsl_error_setv( &f->error, code, fmt, args );
    va_end(args);
    return rc;
  }
}

int fsl_cx_err_get( fsl_cx * f, char const ** str, fsl_size_t * len ){
  return f
    ? fsl_error_get( &f->error, str, len )
    : FSL_RC_MISUSE;
}

fsl_id_t fsl_cx_last_insert_id(fsl_cx * f){
  return (f && f->dbMain && f->dbMain->dbh)
    ? fsl_db_last_insert_id(f->dbMain)
    : -1;
}

fsl_cx * fsl_cx_malloc(){
  fsl_cx * rc = (fsl_cx *)fsl_malloc(sizeof(fsl_cx));
  if(rc) {
    *rc = fsl_cx_empty;
    rc->allocStamp = &fsl_cx_empty;
  }
  return rc;
}

int fsl_cx_err_report( fsl_cx * f, char addNewline ){
  if(!f) return FSL_RC_MISUSE;
  else if(f->error.code){
    char const * msg = f->error.msg.used
      ? (char const *)f->error.msg.mem
      : fsl_rc_cstr(f->error.code)
      ;
    return fsl_outputf(f, "Error #%d: %s%s",
                       f->error.code, msg,
                       addNewline ? "\n" : "");
  }
  else return 0;
}

int fsl_cx_uplift_db_error( fsl_cx * f, fsl_db * db ){
  assert(f);
  if(!f) return FSL_RC_MISUSE;
  if(!db){
    db = f->dbMain;
    assert(db && "misuse: no DB handle to uplift error from!");
    if(!db) return FSL_RC_MISUSE;
  }
  fsl_error_move( &db->error, &f->error );
  return f->error.code;
}

int fsl_cx_uplift_db_error2(fsl_cx *f, fsl_db * db, int rc){
  if(!db) db = f->dbMain;
  assert(db);
  if(rc && FSL_RC_OOM!=rc && !f->error.code && db->error.code){
    rc = fsl_cx_uplift_db_error(f, db);
  }
  return rc;
}

fsl_db * fsl_cx_db_config( fsl_cx * f ){
  if(!f) return NULL;
  else if(f->config.db.dbh) return &f->config.db;
  else if(f->dbMain && (FSL_DBROLE_CONFIG & f->dbMain->role)) return f->dbMain;
  else return NULL;
}

fsl_db * fsl_cx_db_repo( fsl_cx * f ){
  if(!f) return NULL;
  else if(f->repo.db.dbh) return &f->repo.db;
  else if(f->dbMain && (FSL_DBROLE_REPO & f->dbMain->role)) return f->dbMain;
  else return NULL;
}

fsl_db * fsl_needs_repo(fsl_cx * f){
  fsl_db * const db = fsl_cx_db_repo(f);
  if(!db){
    fsl_cx_err_set(f, FSL_RC_NOT_A_REPO,
                   "Fossil context has no opened repository db.");
  }
  return db;
}

fsl_db * fsl_needs_checkout(fsl_cx * f){
  fsl_db * const db = fsl_cx_db_checkout(f);
  if(!db){
    fsl_cx_err_set(f, FSL_RC_NOT_A_CHECKOUT,
                   "Fossil context has no opened checkout db.");
  }
  return db;
}

fsl_db * fsl_cx_db_checkout( fsl_cx * f ){
  if(!f) return NULL;
  else if(f->ckout.db.dbh) return &f->ckout.db;
  else if(f->dbMain && (FSL_DBROLE_CHECKOUT & f->dbMain->role)) return f->dbMain;
  else return NULL;
}

fsl_db * fsl_cx_db( fsl_cx * f ){
  return f ? f->dbMain : NULL;
}
/**
    Returns one of f->db{Config,Repo,Ckout,Mem}
    or NULL.

    ACHTUNG and REMINDER TO SELF: the current (2021-03) design means
    that none of these handles except for FSL_DBROLE_MAIN actually has
    an sqlite3 db handle assigned to it. This returns a handle to the
    "level of abstraction" we need to keep track of each db's name and
    db-specific other state.

    e.g. passing a role of FSL_DBROLE_CHECKOUT this does NOT return
    the same thing as fsl_cx_db_checkout().
 */
static fsl_db * fsl_cx_db_for_role(fsl_cx * f, fsl_dbrole_e r){
  switch(r){
    case FSL_DBROLE_CONFIG:
      return &f->config.db;
    case FSL_DBROLE_REPO:
      return &f->repo.db;
    case FSL_DBROLE_CHECKOUT:
      return &f->ckout.db;
    case FSL_DBROLE_MAIN:
      return &f->dbMem;
    case FSL_DBROLE_NONE:
    default:
      return NULL;
  }
}

/**
    Deattaches the given db role from f->dbMain and removes the role
    from f->dbMain->role. 
 */
static int fsl_cx_detach_role(fsl_cx * f, fsl_dbrole_e r){
  if(!f || !f->dbMain) return FSL_RC_MISUSE;
  else if(!(r & f->dbMain->role)){
    assert(!"Misuse: cannot detach unattached role.");
    return FSL_RC_NOT_FOUND;
  }
  else{
    fsl_db * db = fsl_cx_db_for_role(f,r);
    int rc;
    if(!db) return FSL_RC_RANGE;
    assert(f->dbMain != db);
    f->dbMain->role &= ~r;
    rc = fsl_db_detach( f->dbMain, fsl_db_role_label(r) );
    fsl_free(db->filename);
    fsl_free(db->name);
    db->filename = NULL;
    db->name = NULL;
    return rc;
  }
}


/** @internal

    Attaches the given db file to f with the given role. This function "should"
    be static but we need it in fsl_repo.c when creating a new repository.
 */
int fsl_cx_attach_role(fsl_cx * f, const char *zDbName, fsl_dbrole_e r){
  char const * label = fsl_db_role_label(r);
  fsl_db * db = fsl_cx_db_for_role(f, r);
  char ** nameDest = NULL;
  int rc;
  if(!f->dbMain){
    assert(!"Misuse: f->dbMain has not been set: cannot attach role.");
    return FSL_RC_MISUSE;
  }
  else if(r & f->dbMain->role){
    assert(!"Misuse: role is already attached.");
    return fsl_cx_err_set(f, FSL_RC_MISUSE,
                          "Db role %s is already attached.",
                          label);                          
  }
#if 0
  MARKER(("r=%s db=%p, ckout.db=%p\n", label,
          (void*)db, (void*)&f->ckout.db));
  MARKER(("r=%s db=%p, repo.db=%p\n", label,
          (void*)db, (void*)&f->repo.db));
  MARKER(("r=%s db=%p, dbMain=%p\n", label,
          (void*)db, (void*)f->dbMain));
#endif
  assert(db);
  assert(label);
  assert(f->dbMain != db);
  assert(!db->filename);
  assert(!db->name);
  nameDest = &db->filename;
  switch(r){
    case FSL_DBROLE_CONFIG:
    case FSL_DBROLE_REPO:
    case FSL_DBROLE_CHECKOUT:
      break;
    case FSL_DBROLE_MAIN:
    case FSL_DBROLE_NONE:
    default:
      assert(!"cannot happen/not legal");
      return FSL_RC_RANGE;
  }
  *nameDest = fsl_strdup(zDbName);
  db->name = *nameDest ? fsl_strdup(label) : NULL;
  if(!db->name){
    rc = FSL_RC_OOM;
    /* Design note: we do the strdup() before the ATTACH because if
       the attach succeeds and strdup fails, detaching the db will
       almost certainly fail because it must allocate for its prepared
       statement and other internals. We would end up having to leave
       the db attached and returning a failure, which could lead to a
       memory leak (or worse) downstream.
    */
  }else{
    /*MARKER(("Attached %p role %d %s %s\n",
      (void const *)db, r, db->name, db->filename));*/
    rc = fsl_db_attach(f->dbMain, zDbName, label);
    if(rc){
      fsl_cx_uplift_db_error(f, f->dbMain);
    }else{
      //MARKER(("Attached db %p %s from %s\n",
      //  (void*)db, label, db->filename));
      f->dbMain->role |= r;
    }
  }
  return rc;
}


/*
  2020-03-04: this function has, since the switch to using ATTACH for
  all major dbs, largely been supplanted by fsl_cx_attach_role(), but
  should we ever return to a multi-db-handle world, this
  implementation (or something close to it) is what we'll want.

  Analog to fossil's db_open_or_attach().
  
  This function is still very much up for reconsideration. i'm not
  terribly happy with the "db roles" here - i'd prefer to have them
  each in their own struct, but understand that the roles may play a
  part in query generation, so i haven't yet ruled them out. On
  20140724 we got a fix which allows us to publish concrete names for
  all dbs, regardless of which one is the "main", so much of the
  reason for that concern has been alleviated. In late October, 2014,
  Dave figured out that multi-attach leads to locking problems, so
  the innards are being rewritten to use a :memory: db as the
  permanent "main", and attaching the others.

  Opens or attaches the given db file. zDbName is the
  file name of the DB. role is the role of that db
  in the framework (that determines its db name in SQL).
  
  The first time this is called, f->dbMain is set to point
  to fsl_cx_db_for_role(f, role).
  
  If f->dbMain is already set then the db is attached using a
  role-dependent name and role is added to f->dbMain->role's bitmask.
  
  If f->dbMain is not open then it is opened here and its role is set
  to the given role. It is _also_ ATTACHED using its role name. This
  allows us to publish the names of all Fossil-managed db handles
  regardless of which one is "main". e.g. if repo is opened first, it
  is addressable from SQL as both "main" or "repo".
  
  The given db zDbName is the name of a database file.  If f->dbMain
  is not opened then that db is assigned to the given db file,
  otherwise attach attach zDbName using the name zLabel.
  
  If pWasAttached is not NULL then it is set to a true value if the
  db gets attached, else a false value. It is only modified on success.
  
  It is an error to open the same db role more than once, and trying
  to do so results in a FSL_RC_ACCESS error (f's error state is
  updated).
*/
static int fsl_cx_db_main_open_or_attach( fsl_cx * f,
                                          const char *zDbName,
                                          /* const char *zLabel, */
                                          fsl_dbrole_e role,
                                          int *pWasAttached){
  int rc;
  int wasAttached = 0;
  fsl_db * db;
  assert(f);
  assert(zDbName && *zDbName);
  assert((FSL_DBROLE_CONFIG==role)
         || (FSL_DBROLE_CHECKOUT==role)
         || (FSL_DBROLE_REPO==role));
  if(!f || !zDbName) return FSL_RC_MISUSE;
  else if((FSL_DBROLE_NONE==role) || !*zDbName) return FSL_RC_RANGE;
  switch(role){
    case FSL_DBROLE_REPO:
    case FSL_DBROLE_CHECKOUT:
    case FSL_DBROLE_CONFIG:
      db = fsl_cx_db_for_role(f, role);
      break;
    default:
      assert(!"not possible");
      db = NULL;
      /* We'll fall through and segfault in a moment... */
  }
  assert(db);
  try_again:
  if(!f->dbMain) {
    assert(!"Not possible since moving to a :memory: main db.");
    /* This is the first db. It is now our main db. */
    assert( FSL_DBROLE_NONE==db->role );
    assert( NULL==db->dbh );
    assert( role == FSL_DBROLE_CONFIG
            || role == FSL_DBROLE_REPO 
            || role == FSL_DBROLE_CHECKOUT );
    f->dbMain = db;
    db->f = f;
    rc = fsl_db_open( db, zDbName, FSL_OPEN_F_RW );
    if(!rc){
      db->role = role;
      assert(!db->name);
#if 0
      db->name = fsl_strdup( fsl_db_role_label(FSL_DBROLE_MAIN) );
      if(!db->name) rc = FSL_RC_OOM;
#else
      /* Many thanks to Simon Slavin, native resident of the sqlite3
         mailing list, for this... we apply the db's "real" name via
         an ATTACH, meaning that all the client-side kludgery to get the
         proper DB name is now obsolete.
      */
      rc = fsl_db_attach( db, zDbName, fsl_db_role_label(role) );
      if(!rc){
        db->name = fsl_strdup( fsl_db_role_label(role) );
        if(!db->name) rc = FSL_RC_OOM;
      }
#endif
    }
    /* MARKER(("db->role=%d\n",db->role)); */
    /* g.db = fsl_db_open(zDbName); */
    /* g.zMainDbType = zLabel; */
    /* f->mainDbType = zLabel; */
    /* f->dbMain.role = FSL_DBROLE_MAIN; */
  }else{
    /* Main db has already been opened. Attach this one to it... */
    assert( (int)FSL_DBROLE_NONE!=f->dbMain->role );
    assert( f == f->dbMain->f );
    if(NULL == f->dbMain->dbh){
      /* It was closed in the meantime. Re-assign dbMain... */
      MARKER(("Untested: re-assigning f->dbMain after it has been closed.\n"));
      assert(!"Broken by addition of f->dbMem.");
      f->dbMain = NULL;
      goto try_again;
    }
    if((int)role == db->role){
      return fsl_cx_err_set(f, FSL_RC_ACCESS,
                            "Cannot open/attach db role "
                            "#%d (%s) more than once.",
                            (int)role, fsl_db_role_label(role));
    }
    rc = fsl_cx_attach_role(f, zDbName, role);
    wasAttached = 1;
  }
  if(!rc){
    if(pWasAttached) *pWasAttached = wasAttached;
  }else{
    if(db->error.code){
      fsl_cx_uplift_db_error(f, db);
    }
    if(db==f->dbMain) f->dbMain = NULL;
    fsl_db_close(db);
  }
  return rc;
}

int fsl_config_close( fsl_cx * f ){
  if(!f) return FSL_RC_MISUSE;
  else{
    int rc;
    fsl_db * db = &f->config.db;
    if(db->dbh){
      /* Config is our main db. Close them all */
      assert(!"Not possible since f->dbMem added.");
      assert(f->dbMain==db);
      fsl_checkout_close(f);
      fsl_repo_close(f);
      rc = fsl_db_close(db);
      f->dbMain = NULL;
    }else if(f->dbMain && (FSL_DBROLE_CONFIG & f->dbMain->role)){
      /* Config db is ATTACHed. */
      assert(f->dbMain!=db);
      rc = fsl_cx_detach_role(f, FSL_DBROLE_CONFIG);
    }
    else rc = FSL_RC_NOT_FOUND;
    assert(!db->dbh);
    fsl_db_clear_strings(db, 1);
    return rc;
  }
}

int fsl_repo_close( fsl_cx * f ){
  if(!f) return FSL_RC_MISUSE;
  else{
    int rc;
    fsl_db * db = &f->repo.db;
    if(db->dbh){
      /* Repo is our main db. Close them all */
      assert(!"Not possible since f->dbMem added.");
      assert(f->dbMain==db);
      fsl_config_close(f);
      fsl_checkout_close(f);
      rc = fsl_db_close(db);
      f->dbMain = NULL;
    }else if(f->dbMain && (FSL_DBROLE_REPO & f->dbMain->role)){
      /* Repo db is ATTACHed. */
      assert(f->dbMain!=db);
      rc = fsl_cx_detach_role(f, FSL_DBROLE_REPO);
    }
    else rc = FSL_RC_NOT_FOUND;
    assert(!db->dbh);
    fsl_db_clear_strings(db, 1);
    return rc;
  }
}

int fsl_checkout_close( fsl_cx * f ){
  if(!f) return FSL_RC_MISUSE;
  else{
    int rc;
    fsl_db * db = &f->ckout.db;
#if 0
    fsl_repo_close(f)
        /*
          Ignore error - it might not be opened.  Close it first
          because it was (if things went normally) attached to
          f->ckout.db resp. HOWEVER: we have a bug-in-waiting here,
          potentially. If repo becomes the main db for an attached
          checkout (which isn't current possibly because opening a
          checkout closes/(re)opens the repo) then we stomp on our db
          handle here. Hypothetically.

          If the repo is dbMain:

          a) we cannot have a checkout because fsl_repo_open()
          fails if a repo is already opened.

          b) fsl_repo_close() will clear f->dbMain,
          leaving the code below to return FSL_RC_NOT_FOUND,
          which would be misleading.

          But that's all hypothetical - best we make it impossible
          for the public API to have a checkout without a repo
          or a repo/checkout mismatch.
        */
        ;
#endif
    if(db->dbh){
      /* checkout is our main db. Close them all. */
      assert(!"Not possible since f->dbMem added.");
      assert(!f->repo.db.dbh);
      assert(f->dbMain==db);
      fsl_repo_close(f);
      fsl_config_close(f);
      rc = fsl_db_close(db);
      f->dbMain = NULL;
    }
    else if(f->dbMain && (FSL_DBROLE_CHECKOUT & f->dbMain->role)){
      /* Checkout db is ATTACHed. */
      assert(f->dbMain!=db);
      rc = fsl_cx_detach_role(f, FSL_DBROLE_CHECKOUT);
      fsl_repo_close(f) /*
                          Because the repo is implicitly opened, we
                          "should" implicitly close it. This is
                          debatable but "probably almost always"
                          desired. i can't currently envisage a
                          reasonable use-case which requires closing
                          the checkout but keeping the repo opened.
                          The repo can always be re-opened by itself.
                        */;
    }
    else{
      rc = FSL_RC_NOT_FOUND;
    }
    fsl_free(f->ckout.uuid);
    f->ckout.uuid = NULL;
    f->ckout.rid = 0;
    assert(!db->dbh);
    fsl_db_clear_strings(db, 1);
    return rc;
  }
}

/**
   Updates f->ckout.dir and dirLen based on the current state of
   f->ckout.db. Returns 0 on success, FSL_RC_OOM on allocation error,
   some other code if canonicalization of the name files
   (e.g. filesystem error or cwd cannot be resolved).
*/
static int fsl_update_ckout_dir(fsl_cx *f){
  int rc;
  fsl_buffer ckDir = fsl_buffer_empty;
  fsl_db * dbC = fsl_cx_db_for_role(f, FSL_DBROLE_CHECKOUT);
  assert(dbC->filename);
  assert(*dbC->filename);
  rc = fsl_file_canonical_name(dbC->filename, &ckDir, false);
  if(rc) return rc;
  char * zCanon = fsl_buffer_take(&ckDir);
  //MARKER(("dbC->filename=%s\n", dbC->filename));
  //MARKER(("zCanon=%s\n", zCanon));
  rc = fsl_file_dirpart(zCanon, -1, &ckDir, true);
  fsl_free(zCanon);
  if(rc){
    fsl_buffer_clear(&ckDir);
  }else{
    fsl_free(f->ckout.dir);
    f->ckout.dirLen = ckDir.used;
    f->ckout.dir = fsl_buffer_take(&ckDir);
    /*MARKER(("Updated ckout.dir: %d %s\n",
      (int)f->ckout.dirLen, f->ckout.dir));*/
  }
  return rc;
}

/**
   If zDbName is a valid checkout database file, open it and return 0.
   If it is not a valid local database file, return a non-0 code.
*/
static int fsl_cx_checkout_open_db(fsl_cx * f, const char *zDbName){
  /* char *zVFileDef; */
  int rc;
  fsl_int_t const lsize = fsl_file_size(zDbName);
  if( -1 == lsize  ){
    return FSL_RC_NOT_FOUND /* might be FSL_RC_ACCESS? */;
  }
  if( lsize%1024!=0 || lsize<4096 ){
    return fsl_cx_err_set(f, FSL_RC_RANGE,
                          "File's size is not correct for a "
                          "checkout db: %s",
                          zDbName);
  }
  rc = fsl_cx_db_main_open_or_attach(f, zDbName,
                                     FSL_DBROLE_CHECKOUT,
                                     NULL);
  return rc;
#if 0
  /* Historical bits: no longer needed(?). */

  zVFileDef = fsl_db_text(0, "SELECT sql FROM %s.sqlite_master"
                          " WHERE name=='vfile'",
                          fsl_cx_db_name("localdb")); /* ==> "ckout" */
  if( zVFileDef==0 ) return 0;

  /* If the "isexe" column is missing from the vfile table, then
     add it now.   This code added on 2010-03-06.  After all users have
     upgraded, this code can be safely deleted.
  */
  if( !fsl_str_glob("* isexe *", zVFileDef) ){
    fsl_db_multi_exec("ALTER TABLE vfile ADD COLUMN isexe BOOLEAN DEFAULT 0");
  }

  /* If "islink"/"isLink" columns are missing from tables, then
     add them now.   This code added on 2011-01-17 and 2011-08-27.
     After all users have upgraded, this code can be safely deleted.
  */
  if( !fsl_str_glob("* islink *", zVFileDef) ){
    db_multi_exec("ALTER TABLE vfile ADD COLUMN islink BOOLEAN DEFAULT 0");
    if( db_local_table_exists_but_lacks_column("stashfile", "isLink") ){
      db_multi_exec("ALTER TABLE stashfile ADD COLUMN isLink BOOL DEFAULT 0");
    }
    if( db_local_table_exists_but_lacks_column("undo", "isLink") ){
      db_multi_exec("ALTER TABLE undo ADD COLUMN isLink BOOLEAN DEFAULT 0");
    }
    if( db_local_table_exists_but_lacks_column("undo_vfile", "islink") ){
      db_multi_exec("ALTER TABLE undo_vfile ADD COLUMN islink BOOL DEFAULT 0");
    }
  }
#endif
  return 0;
}


int fsl_cx_prepare( fsl_cx *f, fsl_stmt * tgt, char const * sql,
                      ... ){
  if(!f || !f->dbMain) return FSL_RC_MISUSE;
  else{
    int rc;
    va_list args;
    va_start(args,sql);
    rc = fsl_db_preparev( f->dbMain, tgt, sql, args );
    va_end(args);
    return rc;
  }
}

int fsl_cx_preparev( fsl_cx *f, fsl_stmt * tgt, char const * sql,
                       va_list args ){
  return (f && f->dbMain && tgt)
    ? fsl_db_preparev(f->dbMain, tgt, sql, args)
    : FSL_RC_MISUSE;
}

/**
    Passes the fsl_schema_config() SQL code through a new/truncated
    file named dbName. If the file exists before this call, it is
    unlink()ed and fails if that operation fails.

    FIXME: this was broken by the addition of "cfg." prefix on the
    schema's tables.
 */
static int fsl_config_file_reset(fsl_cx * f, char const * dbName){
  fsl_db DB = fsl_db_empty;
  fsl_db * db = &DB;
  int rc = 0;
  bool isAttached = false;
  const char * zPrefix = fsl_db_role_label(FSL_DBROLE_CONFIG);
  if(-1 != fsl_file_size(dbName)){
    rc = fsl_file_unlink(dbName);
    if(rc){
      return fsl_cx_err_set(f, rc,
                            "Error %s while removing old config file (%s)",
                            fsl_rc_cstr(rc), dbName);
    }
  }
  /**
     Hoop-jumping: because the schema file has a cfg. prefix for the
     table(s), and we cannot assign an arbitrary name to an open()'d
     db, we first open the db (making the the "main" db), then
     ATTACH it to itself to provide the fsl_db_role_label() alias.
  */
  rc = fsl_db_open(db, dbName, FSL_OPEN_F_RWC);
  if(rc) goto end;
  rc = fsl_db_attach(db, dbName, zPrefix);
  if(rc) goto end;
  isAttached = true;
  rc = fsl_db_exec_multi(db, "%s", fsl_schema_config());
  end:
  rc = fsl_cx_uplift_db_error2(f, db, rc);
  if(isAttached) fsl_db_detach(db, zPrefix);
  fsl_db_close(db);
  return rc;
}

int fsl_config_global_preferred_name(char ** zOut){
  char * zEnv = 0;
  char * zRc = 0;
  int rc = 0;
  fsl_buffer buf = fsl_buffer_empty;

#if FSL_PLATFORM_IS_WINDOWS
#  error "TODO: port in fossil(1) db.c:db_configdb_name() Windows bits"
#else
  
#endif

  /* Option 1: $FOSSIL_HOME/.fossil */
  zEnv = fsl_getenv("FOSSIL_HOME");
  if(zEnv){
    zRc = fsl_mprintf("%s/.fossil", zEnv);
    if(!zRc) rc = FSL_RC_OOM;
    goto end;
  }
  /* Option 2: if $HOME/.fossil exists, use that */  
  rc = fsl_find_home_dir(&buf, 0);
  if(rc) goto end;
  rc = fsl_buffer_append(&buf, "/.fossil", 8);
  if(rc) goto end;
  if(fsl_file_size(fsl_buffer_cstr(&buf))>1024*3){
    zRc = fsl_buffer_take(&buf);
    goto end;
  }
  /* Option 3: $XDG_CONFIG_HOME/fossil.db */
  fsl_filename_free(zEnv);
  zEnv = fsl_getenv("XDG_CONFIG_HOME");
  if(zEnv){
    zRc = fsl_mprintf("%s/fossil.db", zEnv);
    if(!zRc) rc = FSL_RC_OOM;
    goto end;
  }
  /* Option 4: If $HOME/.config is a directory,
     use $HOME/.config/fossil.db */
  buf.used -= 8 /* "/.fossil" */;
  buf.mem[buf.used] = 0;
  rc = fsl_buffer_append(&buf, "/.config", 8);
  if(rc) goto end;
  if(fsl_dir_check(fsl_buffer_cstr(&buf))>0){
    zRc = fsl_mprintf("%b/fossil.db", &buf);
    if(!zRc) rc = FSL_RC_OOM;
    goto end;
  }
  /* Option 5: fall back to $HOME/.fossil */
  buf.used -= 8 /* "/.config" */;
  buf.mem[buf.used] = 0;
  rc = fsl_buffer_append(&buf, "/.fossil", 8);
  if(!rc) zRc = fsl_buffer_take(&buf);

  end:
  if(zEnv) fsl_filename_free(zEnv);
  if(!rc){
    assert(zRc);
    *zOut = zRc;
  }
  fsl_buffer_clear(&buf);
  return rc;
}

int fsl_config_open( fsl_cx * f, char const * openDbName ){
  int rc = 0;
  const char * zDbName = 0;
  char * zPrefName = 0;
  bool useAttach = false /* TODO? Move this into a parameter?
                            Do we need the fossil behaviour? */;
  if(!f) return FSL_RC_MISUSE;
  else if(f->config.db.dbh){
    fsl_config_close(f);
  }
  if(openDbName && *openDbName){
    zDbName = openDbName;
  }else{
    rc = fsl_config_global_preferred_name(&zPrefName);
    if(rc) goto end;
    zDbName = zPrefName;
  }
  {
    fsl_int_t const fsize = fsl_file_size(zDbName);
    if( -1==fsize || (fsize<1024*3) ){
      rc = fsl_config_file_reset(f, zDbName);
      if(rc) goto end;
    }
  }
#if defined(_WIN32) || defined(__CYGWIN__)
  /* TODO: Jan made some changes in this area in fossil(1) in
     January(?) 2014, such that only the config file needs to be
     writable, not the directory. Port that in.
  */
  if( fsl_file_access(zDbName, W_OK) ){
    rc = fsl_cx_err_set(f, FSL_RC_ACCESS,
                        "Configuration database [%s] "
                        "must be writeable.", zDbName);
    goto end;
  }
#endif

  if( useAttach ){
    rc = fsl_cx_attach_role(f, zDbName, FSL_DBROLE_CONFIG);
  }else{
    rc = fsl_cx_db_main_open_or_attach(f, zDbName,
                                       FSL_DBROLE_CONFIG,
                                       NULL);
    /* rc = fsl_db_open(&f->config.db, zDbName, FSL_OPEN_F_RWC ); */
  }
  if(!rc && !f->dbMain){
    assert(!"Not possible(?) since addition of f->dbMem");
    f->dbMain = &f->config.db;
  }
  end:
  fsl_free(zPrefName);
  return rc;
}

static void fsl_cx_username_from_repo(fsl_cx * f){
  fsl_db * dbR = fsl_cx_db_repo(f);
  char * u;
  assert(dbR);
  u = fsl_db_g_text(fsl_cx_db_repo(f), NULL,
                    "SELECT login FROM user WHERE uid=1");
  if(u){
    fsl_free(f->repo.user);
    f->repo.user = u;
  }
}

static int fsl_cx_load_glob_lists(fsl_cx * f){
  int rc;
  rc = fsl_config_globs_load(f, &f->cache.globs.ignore, "ignore-glob");
  if(!rc) rc = fsl_config_globs_load(f, &f->cache.globs.binary, "binary-glob");
  if(!rc) rc = fsl_config_globs_load(f, &f->cache.globs.crnl, "crnl-glob");
  return rc;
}

/**
   To be called after a repo or checkout/repo combination has been
   opened. This updates some internal cached info based on the
   checkout and/or repo.
*/
static int fsl_cx_after_open(fsl_cx * f){
  int rc = fsl_checkout_version_fetch(f);
  if(!rc) rc = fsl_cx_load_glob_lists(f);
  return rc;
}


static void fsl_cx_fetch_hash_policy(fsl_cx * f){
  int const iPol = fsl_config_get_int32( f, FSL_CONFDB_REPO,
                                         FSL_HPOLICY_AUTO, "hash-policy");
  fsl_hashpolicy_e p;
  switch(iPol){
    case FSL_HPOLICY_SHA3: p = FSL_HPOLICY_SHA3; break;
    case FSL_HPOLICY_SHA3_ONLY: p = FSL_HPOLICY_SHA3_ONLY; break;
    case FSL_HPOLICY_SHA1: p = FSL_HPOLICY_SHA1; break;
    case FSL_HPOLICY_SHUN_SHA1: p = FSL_HPOLICY_SHUN_SHA1; break;
    default: p = FSL_HPOLICY_AUTO; break;
  }
  f->cxConfig.hashPolicy = p;
}

int fsl_repo_open( fsl_cx * f, char const * repoDbFile/* , bool readOnlyCurrentlyIgnored */ ){
  if(!f || !repoDbFile || !*repoDbFile) return FSL_RC_MISUSE;
  else if(fsl_cx_db_repo(f)){
    return fsl_cx_err_set(f, FSL_RC_ACCESS,
                          "Context already has an opened repository.");
  }
  else {
    int rc;
    if(0!=fsl_file_access( repoDbFile, F_OK )){
      rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                          "Repository db [%s] not found or cannot be read.",
                          repoDbFile);
    }else{
      rc = fsl_cx_db_main_open_or_attach(f, repoDbFile,
                                         FSL_DBROLE_REPO,
                                         NULL);
      if(!rc && !(FSL_CX_F_IS_OPENING_CKOUT & f->flags)){
        rc = fsl_cx_after_open(f);
      }
      if(!rc){
        fsl_db * const db = fsl_cx_db_repo(f);
        fsl_cx_username_from_repo(f);
        f->cache.allowSymlinks =
          fsl_config_get_bool(f, FSL_CONFDB_REPO,
                              f->cache.allowSymlinks,
                              "allow-symlinks");
        f->cache.seenDeltaManifest =
          fsl_config_get_int32(f, FSL_CONFDB_REPO, -1,
                               "seen-delta-manifest");
        fsl_cx_fetch_hash_policy(f);
        if(f->cxConfig.hashPolicy==FSL_HPOLICY_AUTO){
          if(fsl_db_exists(db, "SELECT 1 FROM blob WHERE length(uuid)>40")
             || !fsl_db_exists(db, "SELECT 1 FROM blob WHERE length(uuid)==40")){
            f->cxConfig.hashPolicy = FSL_HPOLICY_SHA3;
          }
        }
      }
    }
    return rc;
  }
}

/**
    Tries to open the repository from which the current checkout
    derives. Returns 0 on success.
*/
static int fsl_repo_open_for_checkout(fsl_cx * f){
  char * repoDb = NULL;
  int rc;
  fsl_buffer nameBuf = fsl_buffer_empty;
  fsl_db * db = fsl_cx_db_checkout(f);
  assert(f);
  assert(f->ckout.dir);
  assert(db);
  rc = fsl_db_get_text(db, &repoDb, NULL,
                       "SELECT value FROM vvar "
                       "WHERE name='repository'");
  if(rc) fsl_cx_uplift_db_error( f, db );
  else if(repoDb){
    if(!fsl_is_absolute_path(repoDb)){
      /* Make it relative to the checkout db dir */
      rc = fsl_buffer_appendf(&nameBuf, "%s/%s", f->ckout.dir, repoDb);
      fsl_free(repoDb);
      if(rc) {
        fsl_buffer_clear(&nameBuf);
        return rc;
      }
      repoDb = (char*)nameBuf.mem /* transfer ownership */;
      nameBuf = fsl_buffer_empty;
    }
    rc = fsl_file_canonical_name(repoDb, &nameBuf, 0);
    fsl_free(repoDb);
    if(!rc){
      repoDb = fsl_buffer_str(&nameBuf);
      assert(repoDb);
      rc = fsl_repo_open(f, repoDb);
    }
    fsl_buffer_reserve(&nameBuf, 0);
  }else{
    /* This can only happen if we are not using a proper
       checkout db or someone has removed the repo link.
    */
    rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                        "Could not determine this checkout's "
                        "repository db file.");
  }
  return rc;
}

int fsl_checkout_version_fetch( fsl_cx *f ){
  fsl_id_t rid = 0;
  int rc = 0;
  fsl_db * dbC = fsl_cx_db_checkout(f);
  fsl_db * dbR = dbC ? fsl_needs_repo(f) : NULL;
  assert(!dbC || (dbC && dbR));
  fsl_free(f->ckout.uuid);
  f->ckout.rid = -1;
  f->ckout.uuid = NULL;
  if(!dbC){
    return 0;
  }
  fsl_cx_err_reset(f);
  rid = fsl_config_get_id(f, FSL_CONFDB_CKOUT, -1, "checkout");
  //MARKER(("rc=%s rid=%d\n",fsl_rc_cstr(f->error.code), (int)rid));
  if(rid>0){
    f->ckout.uuid = fsl_rid_to_uuid(f, rid);
    if(!f->ckout.uuid){
      assert(f->error.code);
      if(!f->error.code){
        rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                            "Could not load UUID for RID %"FSL_ID_T_PFMT,
                            (fsl_id_t)rid);
      }
    }else{
      assert(fsl_is_uuid(f->ckout.uuid));
      rc = 0;
    }
    f->ckout.rid = rid;
  }else if(rid==0){
    /* This is a legal case not possible before libfossil (and only
       afterwards possible in fossil(1)) - an empty repo without an
       active checkin.
    */
    f->ckout.rid = 0;
  }else{
    rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                        "Cannot determine checkout version.");
  }
  return rc;
}

/** @internal

    Sets f->ckout.rid to the given rid (which must be 0 or a valid
    RID) and f->ckout.uuid to a copy of the given uuid. If uuid is
    NULL and rid is not 0 then the uuid is fetched using
    fsl_rid_to_uuid().

    Returns 0 on success, FSL_RC_OOM if copying uuid fails, or some
    error from fsl_rid_to_uuid() if that fails.

    Does not write the changes to disk. Use
    fsl_checkout_version_write() for that.
*/
static int fsl_cx_checkout_version_set(fsl_cx *f, fsl_id_t rid,
                                       fsl_uuid_cstr uuid){
  char * u = 0;
  assert(rid>=0);
  u = uuid
    ? fsl_strdup(uuid)
    : (rid ? fsl_rid_to_uuid(f, rid) : 0);
  if(rid && !u) return FSL_RC_OOM;
  f->ckout.rid = rid;
  fsl_free(f->ckout.uuid);
  f->ckout.uuid = u;
  return 0;
}

int fsl_checkout_version_write( fsl_cx *f, fsl_id_t vid,
                                fsl_uuid_cstr hash ){
  int rc = 0;
  if(!fsl_needs_checkout(f)) return FSL_RC_NOT_A_CHECKOUT;
  else if(vid<0){
    return fsl_cx_err_set(f, FSL_RC_MISUSE,
                          "Invalid vid for fsl_checkout_version_write()");
  }
  if(f->ckout.rid!=vid){
    rc = fsl_cx_checkout_version_set(f, vid, hash);
  }
  if(!rc){
    rc = fsl_config_set_id(f, FSL_CONFDB_CKOUT,
                           "checkout", f->ckout.rid);
    if(!rc){
      rc = fsl_config_set_text(f, FSL_CONFDB_CKOUT,
                               "checkout-hash", f->ckout.uuid);
    }
  }
  return rc;
}

void fsl_checkout_version_info(fsl_cx *f, fsl_id_t * rid, fsl_uuid_cstr * uuid ){
  if(uuid) *uuid = f ? f->ckout.uuid : NULL;
  if(rid) *rid = (f && (f->ckout.rid>=0)) ? f->ckout.rid : 0;
}

int fsl_checkout_db_search( char const * dirName, fsl_int_t dirNameLen,
                            char checkParentDirs, fsl_buffer * pOut ){
  int rc;
  fsl_int_t dLen = 0, i;
  static const char aDbName[][10] = { "_FOSSIL_", ".fslckout" };
  fsl_buffer Buf = fsl_buffer_empty;
  fsl_buffer * buf = &Buf;
  enum { DbCount = 2 };
  buf->used = 0;
  if(dirName && (dirNameLen<0)) dirNameLen = (fsl_int_t)fsl_strlen(dirName);
  if(dirName){
    if((0==dirNameLen) || !*dirName) return FSL_RC_RANGE;
    rc = fsl_buffer_append( buf, dirName, dirNameLen );
    if(rc){
      fsl_buffer_clear(buf);
      return rc;
    }
    dLen = dirNameLen;
  }else{
    char zPwd[4000];
    fsl_size_t pwdLen = 0;
    rc = fsl_getcwd( zPwd, sizeof(zPwd)/sizeof(zPwd[0]), &pwdLen );
    if(rc){
      fsl_buffer_clear(buf);
#if 0
      return fsl_cx_err_set(f, rc,
                            "Could not determine current directory. "
                            "Error code %d (%s).",
                            rc, fsl_rc_cstr(rc));
#else
      return rc;
#endif
    }
    if(1 == pwdLen && '/'==*zPwd) *zPwd = '.'
      /* When in the root directory (or chroot) then change dir name
         name to something we can use.
      */
      ;
    rc = fsl_buffer_append(buf, zPwd, pwdLen);
    if(rc){
      fsl_buffer_clear(buf);
      return rc;
    }
    dLen = (fsl_int_t)pwdLen;
  }
  if(rc){
    fsl_buffer_clear(buf);
    return rc;
  }
  assert(buf->capacity>=buf->used);
  assert((buf->used == (fsl_size_t)dLen) || (1==buf->used && (int)'.'==(int)buf->mem[0]));
  assert(0==buf->mem[buf->used]);

  while(dLen>0){
    /*
      Loop over the list in aDbName, appending each one to
      the dir name in the search for something we can use.
    */
    fsl_int_t lenMarker = dLen /* position to
                                  re-set to on each
                                  sub-iteration. */ ;
    /* trim trailing slashes on this part, so that we don't end up
       with multiples between the dir and file in the final output. */
    while( dLen && ((int)'/'==(int)buf->mem[dLen-1])) --dLen;
    for( i = 0; i < DbCount; ++i ){
      char const * zName;
      buf->used = (fsl_size_t)lenMarker;
      dLen = lenMarker;
      rc = fsl_buffer_appendf( buf, "/%s", aDbName[i]);
      if(rc){
        fsl_buffer_clear(buf);
        return rc;
      }
      zName = fsl_buffer_cstr(buf);
      if(0==fsl_file_access(zName, 0)){
        if(pOut) rc = fsl_buffer_append( pOut, buf->mem, buf->used );
        fsl_buffer_clear(buf);
        return rc;
      }
      if(!checkParentDirs){
        dLen = 0;
        break;
      }else{
        /* Traverse up one dir and try again. */
        --dLen;
        while( dLen>0 && (int)buf->mem[dLen]!=(int)'/' ){ --dLen; }
        while( dLen>0 && (int)buf->mem[dLen-1]==(int)'/' ){ --dLen; }
        if(dLen>lenMarker){
          buf->mem[dLen] = 0;
        }
      }
    }
  }
  fsl_buffer_clear(buf);
  return FSL_RC_NOT_FOUND;
}

int fsl_cx_getcwd(fsl_cx * f, fsl_buffer * pOut){
  char cwd[FILENAME_MAX] = {0};
  fsl_size_t cwdLen = 0;
  int rc = fsl_getcwd(cwd, (fsl_size_t)sizeof(cwd), &cwdLen);
  if(rc){
    return fsl_cx_err_set(f, rc,
                          "Could not get current working directory!");
  }
  rc = fsl_buffer_append(pOut, cwd, cwdLen);
  return rc
    ? fsl_cx_err_set(f, rc/*must be an OOM*/, NULL)
    : 0;
}

int fsl_repo_open_checkout(fsl_cx *f, const fsl_repo_open_checkout_opt *opt){
  fsl_db   *dbC = 0;
  fsl_buffer  cwd = fsl_buffer_empty;
  bool        keep = true;
  int         rc = 0;

  if(!opt) return FSL_RC_MISUSE;
  else if(!fsl_needs_repo(f)){
    return f->error.code;
  }else if(fsl_cx_db_checkout(f)){
    return fsl_cx_err_set(f, FSL_RC_MISUSE,
                          "A checkout is already attached.");
  }else if((rc = fsl_cx_getcwd(f, &cwd))){
    assert(!cwd.used);
    return fsl_cx_err_set(f, rc, "Error %d [%s]: unable to "
                          "determine current directory.",
                          rc, fsl_rc_cstr(rc));
  }
  if(opt->targetDir &&
     0!=fsl_strcmp(opt->targetDir, fsl_buffer_cstr(&cwd))
     /* ^^^ noting that targetDir of "." might be the same directory,
        but a chdir is harmless in that case. */
     ){
    if(fsl_chdir(opt->targetDir)){
      return fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                            "Directory not found or inaccessible: %s",
                            opt->targetDir);
    }
  }
  /**
     AS OF HERE: do not use 'return'. Use goto end so that we can chdir()
     back to our original cwd!
  */
  if(!fsl_dir_is_empty(fsl_buffer_cstr(&cwd))) {
    switch(opt->fileOverwritePolicy){
      case FSL_OVERWRITE_ALWAYS: keep = false; break;
      case FSL_OVERWRITE_NEVER: keep = true; break;
      default:
        assert(FSL_OVERWRITE_ERROR==opt->fileOverwritePolicy);
        rc = fsl_cx_err_set(f, FSL_RC_ACCESS,
                            "directory not empty and "
                            "fileOverwritePolicy is FSL_OVERWRITE_ERROR: "
                            "%b", &cwd);
        goto end;
    }
  }
  if(keep){/*TODO: currently unused*/}
  /*
   * If no checkout database is attached, create and attach now.
   */
  if((dbC = fsl_cx_db_checkout(f)) == NULL) {
    const char * dbName = opt->ckoutDb
      ? opt->ckoutDb : fsl_preferred_checkout_db_name();
    fsl_cx_err_reset(f);
    rc = fsl_cx_attach_role(f, dbName, FSL_DBROLE_CHECKOUT);
    if(rc) goto end;
    fsl_db * const theDbC = fsl_cx_db_checkout(f);
    dbC = fsl_cx_db_for_role(f, FSL_DBROLE_CHECKOUT);
    assert(theDbC != dbC && "Not anymore.");
    assert(!f->error.code);
    /*MARKER(("Attached %p checkout %s %s\n",
      (void const *)dbC, dbC->name, dbC->filename));*/
    assert(dbC->name);
    assert(dbC->filename);
    if(opt->dbOverwritePolicy) {
      rc = fsl_checkout_install_schema(f, true);
#if defined(FOSSIL_LOCAL_WAL)
      /* ^^^ FIXME: make that a runtime config option */
      if(!rc){
        rc = fsl_db_exec(theDbC, "PRAGMA journal_mode=WAL");
      }
#endif
    }
    if(!rc){
      rc = fsl_db_exec(theDbC,"INSERT OR IGNORE INTO "
                       "%s.vvar (name,value) "
                       "VALUES('checkout',0),"
                       "('checkout-hash',null)",
                       dbC->name);
    }
    if(rc) rc = fsl_cx_uplift_db_error(f, dbC);
  }
  end:
  if(cwd.used){
    fsl_chdir(fsl_buffer_cstr(&cwd))
      /* Ignoring error because we have no recovery strategy! */;
  }
  if(!rc){
    fsl_db * const dbR = fsl_cx_db_for_role(f, FSL_DBROLE_REPO);
    assert(dbR);
    assert(dbR->filename && *dbR->filename);
    rc = fsl_config_set_text(f, FSL_CONFDB_CKOUT, "repository",
                             dbR->filename);
  }
  if(!rc){
    rc = fsl_update_ckout_dir(f);
  }
  fsl_buffer_clear(&cwd);
  return rc;
}

int fsl_checkout_open_dir( fsl_cx * f, char const * dirName,
                           fsl_int_t dirNameLen, bool checkParentDirs ){
  int rc;
  fsl_buffer Buf = fsl_buffer_empty;
  fsl_buffer * buf = &Buf;
  char const * zName;
  if(fsl_cx_db_checkout(f)){
    return fsl_cx_err_set( f, FSL_RC_ACCESS,
                           "A checkout is already opened. "
                           "Close it before opening another.");
  }
  if(dirName && dirNameLen<0){
    dirNameLen = (fsl_int_t)fsl_strlen(dirName);
  }
  rc = fsl_checkout_db_search(dirName, dirNameLen,
                              checkParentDirs, buf);
  if(rc){
    if(FSL_RC_NOT_FOUND==rc){
      rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                          "Could not find checkout under [%.*s].",
                          dirName ? (int)dirNameLen : (int)1,
                          dirName ? dirName : ".");
    }
    fsl_buffer_clear(buf);
    return rc;
  }
  assert(buf->used>1 /* "/<FILENAME>" */);
  zName = fsl_buffer_cstr(buf);
  rc = fsl_cx_checkout_open_db(f, zName);
  if(rc){
    fsl_buffer_clear(buf);
    return rc;
  }else{
    /* Checkout db is now opened. Fiddle some internal
       bits...
    */
    unsigned char * end = buf->mem+buf->used-1;
    /* Find dir part */
    while(end>buf->mem && (unsigned char)'/'!=*end) --end;
    assert('/' == (char)*end && "fsl_checkout_db_search() appends '/<DBNAME>'");
    fsl_free(f->ckout.dir);
    f->ckout.dirLen = end - buf->mem +1 /* for trailing '/' */ ;
    *(end+1) = 0; /* rather than strdup'ing, we'll just lop off the
                 filename part. Keep the '/' for historical
                 conventions purposes. */
    f->ckout.dir = fsl_buffer_take(buf);
    assert(!f->ckout.dir[f->ckout.dirLen]);
    assert('/' == f->ckout.dir[f->ckout.dirLen-1]);
    /*defensively drop any existing table definition; there shouldn't be one,
    but maybe could be if the application crashed before closing gracefully*/
    /* why?
       rc = fsl_config_open(f, NULL);
             
       Seems to me we don't need that at the library level. In my
       experience global config options for a lib which is used by
       multiple apps are generally a bad idea, as no single
       setting of any given property will be best for all apps.
       Fossil's global config db assumes that there is only one
       fossil-based app, which is no longer the case.
    */
    f->flags |= FSL_CX_F_IS_OPENING_CKOUT;
    rc = fsl_repo_open_for_checkout(f);
    f->flags &= ~FSL_CX_F_IS_OPENING_CKOUT;
    if(!rc){
      /*HHH this updates some copies of settings!!!*/
      rc = fsl_cx_after_open(f);
    }
    if(rc){
      /* Is this sane? Is not doing it sane? */
      fsl_checkout_close(f);
    }
    return rc;
  }
}


char const * fsl_cx_db_file_for_role(fsl_cx const * f,
                                     fsl_dbrole_e r,
                                     fsl_size_t * len){
  fsl_db const * db = fsl_cx_db_for_role((fsl_cx*)f, r);
  char const * rc = db ? db->filename : NULL;
  if(len) *len = fsl_strlen(rc);
  return rc;
}

char const * fsl_cx_db_name_for_role(fsl_cx const * f,
                                     fsl_dbrole_e r,
                                     fsl_size_t * len){
  if(FSL_DBROLE_MAIN == r){
    /* special case to be removed when f->dbMem bits are
       finished. */
    if(len) *len=4;
    return "main";
  }else{
    fsl_db const * db = fsl_cx_db_for_role((fsl_cx*)f, r);
    char const * rc = db ? db->name : NULL;
    if(len) *len = rc ? fsl_strlen(rc) : 0;
    return rc;
  }
}

char const * fsl_cx_db_file_config(fsl_cx const * f,
                                   fsl_size_t * len){
  char const * rc = NULL;
  if(f && f->config.db.filename){
    rc = f->config.db.filename;
    if(len) *len = fsl_strlen(rc);
  }
  return rc;
}

char const * fsl_cx_db_file_repo(fsl_cx const * f,
                                 fsl_size_t * len){
  char const * rc = NULL;
  if(f && f->repo.db.filename){
    rc = f->repo.db.filename;
    if(len) *len = fsl_strlen(rc);
  }
  return rc;
}

char const * fsl_cx_db_file_checkout(fsl_cx const * f,
                                     fsl_size_t * len){
  char const * rc = NULL;
  if(f && f->ckout.db.filename){
    rc = f->ckout.db.filename;
    if(len) *len = fsl_strlen(rc);
  }
  return rc;
}

char const * fsl_cx_checkout_dir_name(fsl_cx const * f,
                                      fsl_size_t * len){
  char const * rc = NULL;
  if(f && f->ckout.dir){
    rc = f->ckout.dir;
    if(len) *len = fsl_strlen(rc);
  }
  return rc;
}

int fsl_cx_flags_get( fsl_cx * f ){
  return f ? f->flags : -1;
}

int fsl_cx_flag_set( fsl_cx * f, int flags, char enable ){
  if(!f) return -1;
  else {
    if(enable) f->flags |= flags;
    else f->flags &= ~flags;
    return f->flags;
  }
}


fsl_xlinker * fsl_xlinker_by_name( fsl_cx * f, char const * name ){

  fsl_xlinker * rv = NULL;
  fsl_size_t i;
  for( i = 0; i < f->xlinkers.used; ++i ){
    rv = f->xlinkers.list + i;
    if(0==fsl_strcmp(rv->name, name)) return rv;
  }
  return NULL;
}

int fsl_xlink_listener( fsl_cx * f, char const * name,
                        fsl_deck_xlink_f cb, void * cbState ){
  fsl_xlinker * x;
  if(!f || !cb || !name || !*name) return FSL_RC_MISUSE;
  x = fsl_xlinker_by_name(f, name);
  if(x){
    /* Replace existing entry */
    x->f = cb;
    x->state = cbState;
    return 0;
  }else if(f->xlinkers.used <= f->xlinkers.capacity){
    /* Expand the array */
    fsl_size_t const n = f->xlinkers.used ? f->xlinkers.used * 2 : 5;
    fsl_xlinker * re =
      (fsl_xlinker *)fsl_realloc(f->xlinkers.list,
                                 n * sizeof(fsl_xlinker));
    if(!re) return FSL_RC_OOM;
    f->xlinkers.list = re;
    f->xlinkers.capacity = n;
  }
  x = f->xlinkers.list + f->xlinkers.used++;
  *x = fsl_xlinker_empty;
  x->f = cb;
  x->state = cbState;
  x->name = name;
  return 0;
}

int fsl_cx_user_set( fsl_cx * f, char const * userName ){
  if(!f) return FSL_RC_MISUSE;
  else if(!userName || !*userName){
    fsl_free(f->repo.user);
    f->repo.user = NULL;
    return 0;
  }else{
    char * u = fsl_strdup(userName);
    if(!u) return FSL_RC_OOM;
    else{
      fsl_free(f->repo.user);
      f->repo.user = u;
      return 0;
    }    
  }
}

char const * fsl_cx_user_get( fsl_cx const * f ){
  return f ? f->repo.user : NULL;
}

int fsl_cx_schema_ticket(fsl_cx * f, fsl_buffer * pOut){
  fsl_db * db = f ? fsl_needs_repo(f) : NULL;
  if(!f || !pOut) return FSL_RC_MISUSE;
  else if(!db) return FSL_RC_NOT_A_REPO;
  else{
    fsl_size_t const oldUsed = pOut->used;
    int rc = fsl_config_get_buffer(f, FSL_CONFDB_REPO,
                                   "ticket-table", pOut);
    if((FSL_RC_NOT_FOUND==rc)
       || (oldUsed == pOut->used/*found but it was empty*/)
       ){
      rc = fsl_buffer_append(pOut, fsl_schema_ticket(), -1);
    }
    return rc;
  }
}


int fsl_cx_stat2( fsl_cx * f, bool relativeToCwd,
                  char const * zName, fsl_fstat * tgt,
                  fsl_buffer * nameOut, bool fullPath){
  int rc;
  fsl_buffer * b = f ? &f->fsScratch : NULL;
  fsl_buffer bufRel = fsl_buffer_empty;
  fsl_size_t n;
  assert(f);
  if(!zName || !*zName) return FSL_RC_MISUSE;
  else if(!f->ckout.dir) return FSL_RC_NOT_A_CHECKOUT;
#if 1
  else{
    rc = fsl_checkout_filename_check(f, relativeToCwd, zName, &bufRel);
    if(rc) goto end;
    zName = fsl_buffer_cstr2( &bufRel, &n );
  }
#else
  else if(!fsl_is_simple_pathname(zName, 1)){
    rc = fsl_checkout_filename_check(f, relativeToCwd, zName, &bufRel);
    if(rc) goto end;
    zName = fsl_buffer_cstr2( &bufRel, &n );
    /* MARKER(("bufRel=%s\n",zName)); */
  }else{
    n = fsl_strlen(zName);
  }
#endif
  assert(n>0 &&
         "Will fail if fsl_checkout_filename_check() changes "
         "to return nothing if zName==checkout root");
  assert(b->used==0 && "Someone did not re-set f->fsScratch OR it is in use higher up the stack.");
  assert(b->capacity && "We expected this to be set up by fsl_checkout_filename_check()");
  if(!n
     /* i don't like the "." resp "./" result when zName==checkout root */
     || (1==n && '.'==bufRel.mem[0])
     || (2==n && '.'==bufRel.mem[0] && '/'==bufRel.mem[1])){
    rc = fsl_buffer_appendf(b, "%s%s", f->ckout.dir,
                            (2==n) ? "/" : "");
  }else{
    rc = fsl_buffer_appendf(b, "%s%s", f->ckout.dir, zName);
  }
  if(!rc){
    rc = fsl_stat( fsl_buffer_cstr(b), tgt, false );
    if(rc){
      fsl_cx_err_set(f, rc, "Error %s from fsl_stat(\"%b\")",
                     fsl_rc_cstr(rc), b);
    }else if(nameOut){
      rc = fullPath
        ? fsl_buffer_append(nameOut, b->mem, b->used)
        : fsl_buffer_append(nameOut, zName, n);
    }
  }
  end:
  fsl_buffer_reuse(b);
  fsl_buffer_clear(&bufRel);
  return rc;
}

int fsl_cx_stat(fsl_cx * f, bool relativeToCwd, char const * zName,
                fsl_fstat * tgt){
  return fsl_cx_stat2(f, relativeToCwd, zName, tgt, NULL, 0);
}


void fsl_cx_case_sensitive_set(fsl_cx * f, bool caseSensitive){
  f->cache.caseInsensitive = caseSensitive;
}

bool fsl_cx_is_case_sensitive(fsl_cx const * f){
  return !f->cache.caseInsensitive;
}

char const * fsl_cx_filename_collation(fsl_cx const * f){
  return f->cache.caseInsensitive ? "COLLATE nocase" : "";
}


void fsl_cx_yield_file_buffer(fsl_cx * f){
  enum { MaxSize = 1024 * 1024 * 2 };
  assert(f);
  if(f->fileContent.capacity>MaxSize){
    fsl_buffer_resize(&f->fileContent, MaxSize);
    assert(f->fileContent.capacity<=MaxSize+1);
  }
  fsl_buffer_reuse(&f->fileContent);
}

fsl_error const * fsl_cx_err_get_e(fsl_cx const * f){
  return f ? &f->error : NULL;
}

void fsl_cx_close_dbs( fsl_cx * f ){
  if(!f) return;
  else{
    int const errCheck = f->error.code;
    fsl_repo_close(f);
    fsl_checkout_close(f);
    fsl_config_close(f);
    assert(!f->repo.db.dbh);
    assert(!f->ckout.db.dbh);
    assert(!f->config.db.dbh);
    if(!errCheck && f->error.code){
      /* The error state was clean before we started but one of the
         above updated it. Suppress the error.
      */
      fsl_cx_err_reset(f);
    }
  }
}

char const * fsl_cx_glob_matches( fsl_cx * f, int gtype,
                                  char const * str ){
  int i, count = 0;
  char const * rv = NULL;
  fsl_list const * lists[] = {0,0,0};
  if(!f || !str || !*str) return NULL;
  if(gtype & FSL_GLOBS_IGNORE) lists[count++] = &f->cache.globs.ignore;
  if(gtype & FSL_GLOBS_CRNL) lists[count++] = &f->cache.globs.crnl;
  /*CRNL/BINARY together makes little sense, but why strictly prohibit
    it?*/
  if(gtype & FSL_GLOBS_BINARY) lists[count++] = &f->cache.globs.binary;
  for( i = 0; i < count; ++i ){
    if( (rv = fsl_glob_list_matches( lists[i], str )) ) break;
  }
  return rv;
}

int fsl_output_f_fsl_cx(void * state, void const * src, fsl_size_t n ){
  return (state && src && n)
    ? fsl_output((fsl_cx*)state, src, n)
    : (n ? FSL_RC_MISUSE : 0);
}

int fsl_cx_hash_buffer( fsl_cx const * f, bool useAlternate,
                        fsl_buffer const * pIn, fsl_buffer * pOut){
  /* fossil(1) counterpart: hname_hash() */
  if(useAlternate){
    switch(f->cxConfig.hashPolicy){
      case FSL_HPOLICY_AUTO:
      case FSL_HPOLICY_SHA1:
        return fsl_sha3sum_buffer(pIn, pOut);
      case FSL_HPOLICY_SHA3:
        return fsl_sha1sum_buffer(pIn, pOut);
      default: return FSL_RC_UNSUPPORTED;
    }
  }else{
    switch(f->cxConfig.hashPolicy){
      case FSL_HPOLICY_SHA1:
      case FSL_HPOLICY_AUTO:
        return fsl_sha1sum_buffer(pIn, pOut);
      case FSL_HPOLICY_SHA3:
      case FSL_HPOLICY_SHA3_ONLY:
      case FSL_HPOLICY_SHUN_SHA1:
        return fsl_sha3sum_buffer(pIn, pOut);
    }
  }
  assert(!"not reached");
  return FSL_RC_RANGE;
}

int fsl_cx_hash_filename( fsl_cx * f, bool useAlternate,
                          const char * zFilename, fsl_buffer * pOut){
  /* FIXME: reimplement this to stream the content in bite-sized
     chunks. That requires duplicating most of fsl_buffer_fill_from()
     and fsl_cx_hash_buffer(). */
  fsl_buffer * content = &f->fileContent;
  int rc;
  assert(!content->used && "Internal recursive misuse of fsl_cx::fileContent");
  fsl_buffer_reuse(content);
  rc = fsl_buffer_fill_from_filename(content, zFilename);
  if(!rc){
    rc = fsl_cx_hash_buffer(f, useAlternate, content, pOut);
  }
  fsl_buffer_reuse(content);
  return rc;
}

char const * fsl_hash_policy_name(fsl_hashpolicy_e p){
  switch(p){
    case FSL_HPOLICY_SHUN_SHA1: return "shun-sha1";
    case FSL_HPOLICY_SHA3: return "sha3";
    case FSL_HPOLICY_SHA3_ONLY: return "sha3-only";
    case FSL_HPOLICY_SHA1: return "sha1";
    case FSL_HPOLICY_AUTO: return "auto";
    default: return NULL;
  }
}

fsl_hashpolicy_e fsl_cx_hash_policy_set(fsl_cx *f, fsl_hashpolicy_e p){
  fsl_hashpolicy_e const old = f->cxConfig.hashPolicy;
  fsl_db * const dbR = fsl_cx_db_repo(f);
  if(dbR){
    /* Write it regardless of whether it's the same as the old policy
       so that we're sure the db knows the policy. */
    if(FSL_HPOLICY_AUTO==p &&
       fsl_db_exists(dbR,"SELECT 1 FROM blob WHERE length(uuid)>40")){
      p = FSL_HPOLICY_SHA3;
    }
    fsl_config_set_int32(f, FSL_CONFDB_REPO, "hash-policy", p);
  }
  f->cxConfig.hashPolicy = p;
  return old;
}

fsl_hashpolicy_e fsl_cx_hash_policy_get(fsl_cx const*f){
  return f->cxConfig.hashPolicy;
}

int fsl_cx_transaction_level(fsl_cx * f){
  return f->dbMain
    ? fsl_db_transaction_level(f->dbMain)
    : 0;
}

int fsl_cx_transaction_begin(fsl_cx * f){
  return fsl_db_transaction_begin(f->dbMain);
}

int fsl_cx_transaction_end(fsl_cx * f, bool doRollback){
  return fsl_db_transaction_end(f->dbMain, doRollback);
}

void fsl_cx_confirmer(fsl_cx * f, fsl_confirm_callback_f callback,
                      void * callbackState){
  f->confirmer.callback = callback;
  f->confirmer.callbackState = callbackState;
}

void fsl_cx_confirmer_get(fsl_cx const * f, fsl_confirmer * dest){
  *dest = f->confirmer;
}

int fsl_cx_confirm(fsl_cx *f, fsl_confirm_event_e eventId,
                   char const * zFilename,
                   fsl_confirm_response_e *outAnswer){
  if(f->confirmer.callback){
    return f->confirmer.callback(eventId, f->confirmer.callbackState,
                                 zFilename, outAnswer);
  }
  switch(eventId){
    case FSL_CEVENT_OVERWRITE_MOD_FILE:
    case FSL_CEVENT_OVERWRITE_UNMGD_FILE:
      *outAnswer =  FSL_CRESPONSE_NEVER;
      break;
    case FSL_CEVENT_RM_MOD_UNMGD_FILE:
      *outAnswer = FSL_CRESPONSE_NEVER;
      break;
    default:
      assert(!"Unhandled fsl_confirm_event_e value");
      fsl_fatal(FSL_RC_UNSUPPORTED,
                "Unhandled fsl_confirm_event_e value: %d",
                eventId)/*does not return*/;
  }
  return 0;
}


#if 0
struct tm * fsl_cx_localtime( fsl_cx const * f, const time_t * clock ){
  if(!clock) return NULL;
  else if(!f) return localtime(clock);
  else return (f->flags & FSL_CX_F_LOCALTIME_GMT)
         ? gmtime(clock)
         : localtime(clock)
         ;
}

struct tm * fsl_localtime( const time_t * clock ){
  return fsl_cx_localtime(NULL, clock);
}

time_t fsl_cx_time_adj(fsl_cx const * f, time_t clock){
  struct tm * tm = fsl_cx_localtime(f, &clock);
  return tm ? mktime(tm) : 0;
}
#endif

#undef MARKER