Login
Artifact [3478473cfd]
Login

Artifact 3478473cfd45f4788d1545c9f673a6cdb9842b4f:


/* -*- 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 implements most of the fsl_repo_xxx() APIs.
*/
#include "fossil-scm/fossil-internal.h"
#include <assert.h>
#include <memory.h> /* memcpy() */
#include <time.h> /* time() */
#include <errno.h>

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


int fsl_sym_to_rid( fsl_cx * f, char const * sym, fsl_catype_t type,
                    fsl_id_t * rv ){
  fsl_id_t rid = 0;
  fsl_id_t vid;
  fsl_size_t symLen;
  /* fsl_int_t i; */
  fsl_db * dbR = fsl_cx_db_repo(f);
  fsl_db * dbC = fsl_cx_db_checkout(f);
  if(!f || !sym || !*sym || !rv) return FSL_RC_MISUSE;
  else if(!dbR) return FSL_RC_NOT_A_REPO;
  /* special keyword: "tip" */
  if( 0==fsl_strcmp(sym,"tip")
      && (FSL_CATYPE_ANY==type || FSL_CATYPE_CHECKIN==type)){
    rid = fsl_db_g_id(dbR, 0,
                      "SELECT objid FROM event"
                      " WHERE type='ci'"
                      " ORDER BY event.mtime DESC"
                      " LIMIT 1");
    if(rid>0) goto gotit;
  }
  /* special keywords: "prev", "previous", "current", and "next".
     These require a checkout.
  */
  vid = dbC
    ? fsl_config_get_id(f, FSL_CONFDB_CKOUT, -1, "checkout")
    : 0;
  /* MARKER(("has vid=%"FSL_ID_T_PFMT"\n", vid)); */
  if( vid>0){
    if( 0==fsl_strcmp(sym, "current") ){
      rid = vid;
    }
    else if( 0==fsl_strcmp(sym, "prev")
             || 0==fsl_strcmp(sym, "previous") ){
      rid = fsl_db_g_id(dbR, 0,
                        "SELECT pid FROM plink WHERE "
                        "cid=%"FSL_ID_T_PFMT" AND isprim",
                        (fsl_id_t)vid);
    }
    else if( 0==fsl_strcmp(sym, "next") ){
      rid = fsl_db_g_id(dbR, 0,
                        "SELECT cid FROM plink WHERE "
                        "pid=%"FSL_ID_T_PFMT
                        " ORDER BY isprim DESC, mtime DESC",
                        (fsl_id_t)vid);
    }
    if(rid>0) goto gotit;
  }

  /* Date and times */
  if( 0==memcmp(sym, "date:", 5) ){
    rid = fsl_db_g_id(dbR, 0, 
                      "SELECT objid FROM event"
                      " WHERE mtime<=julianday(%Q,'utc')"
                      " AND type GLOB '%q'"
                      " ORDER BY mtime DESC LIMIT 1",
                      sym+5, fsl_catype_event_cstr(type));
    *rv = rid;
    return 0;
  }
  if( fsl_str_is_date(sym) ){
    rid = fsl_db_g_id(dbR, 0, 
                      "SELECT objid FROM event"
                      " WHERE mtime<=julianday(%Q,'utc')"
                      " AND type GLOB '%q'"
                      " ORDER BY mtime DESC LIMIT 1",
                      sym, fsl_catype_event_cstr(type));
    if(rid>0) goto gotit;
  }

  /* Deprecated time formats elided: local:..., utc:... */

  /* "tag:" + symbolic-name */
  if( memcmp(sym, "tag:", 4)==0 ){
    rid = fsl_db_g_id(dbR, 0,
                      "SELECT event.objid, max(event.mtime)"
                      "  FROM tag, tagxref, event"
                      " WHERE tag.tagname='sym-%q'"
                      "   AND tagxref.tagid=tag.tagid"
                      "   AND tagxref.tagtype>0"
                      "   AND event.objid=tagxref.rid "
                      "   AND event.type GLOB '%q'",
                      sym+4, fsl_catype_event_cstr(type)
                      );
    goto gotit;
  }

  /* root:TAG -> The origin of the branch */
  if( memcmp(sym, "root:", 5)==0 ){
    fsl_stmt q = fsl_stmt_empty;
    int rc;
    char *zBr;
    rc = fsl_sym_to_rid(f, sym+5, type, &rid);
    zBr = fsl_db_g_text(dbR, NULL,
                        "SELECT value FROM tagxref"
                        " WHERE rid=%"FSL_ID_T_PFMT" AND tagid=%d"
                        " AND tagtype>0",
                        (fsl_id_t)rid, (int)FSL_TAGID_BRANCH);
    rc = fsl_db_prepare(dbR, &q,
               "SELECT pid, EXISTS(SELECT 1 FROM tagxref"
               " WHERE tagid=%d AND tagtype>0"
               "   AND value=%Q AND rid=plink.pid)"
               "  FROM plink"
               " WHERE cid=? AND isprim",
               FSL_TAGID_BRANCH,
               zBr ? zBr : "trunk"
               );
    if(zBr) fsl_free(zBr);
    if(!rc) do{
      fsl_stmt_reset(&q);
      fsl_stmt_bind_id(&q, 1, rid);
      rc = fsl_stmt_step(&q);
      if( rc!=FSL_RC_STEP_ROW ) break;
      rid = fsl_stmt_g_id(&q, 0);
    }while( fsl_stmt_g_int32(&q, 1)==1 && rid>0 );
    fsl_stmt_finalize(&q);
    goto gotit;
  }

  symLen = fsl_strlen(sym);
  /* SHA1 hash or prefix */
  if( symLen>=4
      && symLen<=FSL_UUID_STRLEN
      && fsl_validate16(sym, symLen) ){
    fsl_stmt q = fsl_stmt_empty;
    char zUuid[FSL_UUID_STRLEN+1];
    memcpy(zUuid, sym, symLen);
    zUuid[symLen] = 0;
    fsl_canonical16(zUuid, symLen);
    rid = 0;
    /* Reminder to self: caching these queries would be cool but it
       can't work with the GLOBs.
    */
    if( FSL_CATYPE_ANY==type ){
      fsl_db_prepare(dbR, &q,
                       "SELECT rid FROM blob WHERE uuid GLOB '%s*'",
                       zUuid);
    }else{
      fsl_db_prepare(dbR, &q,
                     "SELECT blob.rid"
                     "  FROM blob, event"
                     " WHERE blob.uuid GLOB '%s*'"
                     "   AND event.objid=blob.rid"
                     "   AND event.type GLOB '%q'",
                     zUuid, fsl_catype_event_cstr(type) );
    }
    if( fsl_stmt_step(&q)==FSL_RC_STEP_ROW ){
      fsl_int64_t r64 = 0;
      fsl_stmt_get_int64(&q, 0, &r64);
      if( fsl_stmt_step(&q)==FSL_RC_STEP_ROW ) rid = -1
        /* Ambiguous results */
        ;
      else rid = (fsl_id_t)r64;
    }
    fsl_stmt_finalize(&q);
    if(rid<0){
      fsl_cx_err_set(f, FSL_RC_AMBIGUOUS,
                     "Symbolic name '%s' is ambiguous.",
                     sym);
    }
    goto gotit
      /* None of the further checks against the sym can pass. */
      ;
  }

  /* Symbolic name ... */
  rid = fsl_db_g_id(dbR, 0,
                    "SELECT event.objid, max(event.mtime)"
                    "  FROM tag, tagxref, event"
                    " WHERE tag.tagname='sym-%q' "
                    "   AND tagxref.tagid=tag.tagid AND tagxref.tagtype>0 "
                    "   AND event.objid=tagxref.rid "
                    "   AND event.type GLOB '%q'",
                    sym, fsl_catype_event_cstr(type)
                    );
  if( rid>0 ) goto gotit;

  /* Undocumented: rid:### ==> rid */
  if(symLen>4 && 0==fsl_strncmp("rid:",sym,4)){
    int i;
    char const * oldSym = sym;
    sym += 4;
    for(i=0; fsl_isdigit(sym[i]); i++){}
    if( sym[i]==0 ){
      if( FSL_CATYPE_ANY==type ){
        rid = fsl_db_g_id(dbR, 0, 
                          "SELECT rid"
                          "  FROM blob"
                          " WHERE rid=%s",
                          sym);
      }else{
        rid = fsl_db_g_id(dbR, 0, 
                          "SELECT event.objid"
                          "  FROM event"
                          " WHERE event.objid=%s"
                          "   AND event.type GLOB '%q'",
                          sym, fsl_catype_event_cstr(type));
      }
      if( rid>0 ) goto gotit;
    }
    sym = oldSym;
  }

  gotit:
  if(rid<=0){
    return f->error.code
      ? f->error.code
      : fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                       "Could not resolve symbolic name "
                       "'%s' as artifact type '%s'.",
                       sym, fsl_catype_event_cstr(type) );
  }
  *rv = rid;
  return 0;
}

fsl_id_t fsl_uuid_to_rid2( fsl_cx * f, fsl_uuid_cstr uuid,
                                fsl_phantom_t mode ){
    if(!f) return -1;
    else if(!fsl_is_uuid(uuid)){
      fsl_cx_err_set(f, FSL_RC_MISUSE,
                     "fsl_uuid_to_rid2() requires a "
                     "full UUID. Got: %s", uuid);
      return -2;
    }else{
      fsl_id_t rv;
      rv = fsl_uuid_to_rid(f, uuid);
      if((0==rv) && (FSL_PHANTOM_NONE!=mode)
         && 0!=fsl_content_new(f, uuid,
                               (FSL_PHANTOM_PRIVATE==mode),
                               &rv)){
        assert(f->error.code);
        rv = -3;
      }
      return rv;
    }
}

int fsl_sym_to_uuid( fsl_cx * f, char const * sym, fsl_catype_t type,
                     fsl_uuid_str * rv, fsl_id_t * rvId ){
  fsl_id_t rid = 0;
  fsl_db * dbR = fsl_needs_repo(f);
  fsl_uuid_str rvv = NULL;
  int rc = dbR
    ? fsl_sym_to_rid(f, sym, type, &rid)
    : FSL_RC_NOT_A_REPO;
  if(!rc){
    if(rvId) *rvId = rid;
    rvv = fsl_rid_to_uuid(f, rid)
      /* TODO: use a cached "exists" check if !rv, to avoid allocating
         rvv if we don't need it.
      */;
    if(!rvv){
      if(!f->error.code){
        rc = fsl_cx_err_set(f, FSL_RC_RANGE,
                            "Cannot find UUID for RID %"FSL_ID_T_PFMT".",
                            rid);
      }
    }
    else if(rv){
      *rv = rvv;
    }else{
      fsl_free( rvv );
    }
  }
  return rc;
}

fsl_id_t fsl_uuid_to_rid( fsl_cx * f, char const * uuid ){
  fsl_db * db = fsl_needs_repo(f);
  fsl_size_t uuidLen = (uuid && db) ? fsl_strlen(uuid) : 0;
  if(!f || !uuid) return -1;
  else if(!db){
    /* f's error state has already been set */
    return -2;
  }
  else if(!fsl_validate16(uuid, uuidLen)){
    fsl_cx_err_set(f, FSL_RC_RANGE, "Invalid UUID (prefix): %s", uuid);
    return -3;
  }
  else if(uuidLen>FSL_UUID_STRLEN){
    fsl_cx_err_set(f, FSL_RC_RANGE, "UUID is too long: %s", uuid);
    return -4;
  }
  else {
    fsl_id_t rid = -5;
    fsl_stmt q = fsl_stmt_empty;
    fsl_stmt * qS = NULL;
    int rc;
    rc = (uuidLen==FSL_UUID_STRLEN)
      /* Optimization for the common internally-used case */
      ? fsl_db_prepare_cached(db, &qS,
                              "SELECT rid FROM blob WHERE "
                              "uuid=?")
      : fsl_db_prepare(db, &q,
                       "SELECT rid FROM blob WHERE "
                       "uuid GLOB '%s*'",
                       uuid);
    if(!rc){
      fsl_stmt * st = qS ? qS : &q;
      if(qS){
        rc = fsl_stmt_bind_text(qS, 1, uuid, FSL_UUID_STRLEN, 0);
      }
      if(!rc){
        rc = fsl_stmt_step(st);
        switch(rc){
          case FSL_RC_STEP_ROW:
            rc = 0;
            rid = fsl_stmt_g_id(st, 0);
            if(!qS){
              /*
                Check for an ambiguous result. We don't need this for
                the (qS==st) case because that one does an exact match
                on a unique key.
              */
              rc = fsl_stmt_step(st);
              switch(rc){
                case FSL_RC_STEP_ROW:
                  rc = 0;
                  fsl_cx_err_set(f, FSL_RC_AMBIGUOUS,
                                 "UUID prefix is ambiguous: %s",
                                 uuid);
                  rid = -6;
                break;
                case FSL_RC_STEP_DONE:
                  /* Unambiguous UUID */
                  rc = 0;
                  break;
                default:
                  assert(st->db->error.code);
                  /* fall through and uplift the db error below... */
              }
            }
            break;
          case FSL_RC_STEP_DONE:
            rid = 0;
            rc = 0;
            break;
          default:
            assert(st->db->error.code);
            rid = -7;
            break;
        }
      }
      if(rc && db->error.code && !f->error.code){
        fsl_cx_uplift_db_error(f, db);
      }
      if(qS) fsl_stmt_cached_yield(qS);
      else fsl_stmt_finalize(&q);
    }
    return rid;
  }
}

int fsl_repo_filename_to_fnid( fsl_cx * f, char const * fn, fsl_id_t * rv, char createNew ){
  fsl_db * db = fsl_cx_db_repo(f);
  fsl_id_t fnid = 0;
  fsl_stmt * qSel = NULL;
  int rc;
  assert(f);
  assert(db);
  assert(rv);
  if(!fn || !fsl_is_simple_pathname(fn, 1)){
    return fsl_cx_err_set(f, FSL_RC_RANGE,
                          "Filename is not a \"simple\" path: %s",
                          fn);
  }
  *rv = 0;
  rc = fsl_db_prepare_cached(db, &qSel,
                             "SELECT fnid FROM filename WHERE name=?");
  if(rc){
      fsl_cx_uplift_db_error(f, db);
      return rc;
  }
  rc = fsl_stmt_bind_text(qSel, 1, fn, -1, 0);
  if(rc){
    fsl_stmt_cached_yield(qSel);
  }else{
    rc = fsl_stmt_step(qSel);
    if( FSL_RC_STEP_ROW == rc ){
      rc = 0;
      fnid = fsl_stmt_g_id(qSel, 0);
      assert(fnid>0);
    }else if(FSL_RC_STEP_DONE == rc){
      rc = 0;
    }
    fsl_stmt_cached_yield(qSel);
    if(!rc && (fnid==0) && createNew){
      fsl_stmt * qIns = NULL;
      rc = fsl_db_prepare_cached(db, &qIns,
                                 "INSERT INTO filename(name) VALUES(?)");
      if(!rc){
        rc = fsl_stmt_bind_text(qIns, 1, fn, -1, 0);
        if(!rc){
          rc = fsl_stmt_step(qIns);
          if(FSL_RC_STEP_DONE==rc){
            rc = 0;
            fnid = fsl_db_last_insert_id(db);
          }
        }
        fsl_stmt_cached_yield(qIns);
      }
    }
  }
  if(!rc){
    assert(!createNew || (fnid>0));
    *rv = fnid;
  }else if(db->error.code){
    fsl_cx_uplift_db_error(f, db);
  }
  return rc;
}

int fsl_delta_src_id( fsl_cx * f, fsl_id_t deltaRid, fsl_id_t * rv ){
  fsl_db * dbR = f ? fsl_cx_db_repo(f) : NULL;
  if(!f || !rv) return FSL_RC_MISUSE;
  else if(deltaRid<=0) return FSL_RC_RANGE;
  else if(!dbR) return FSL_RC_NOT_A_REPO;
  else {
    int rc;
    fsl_stmt * q = NULL;
    *rv = 0;
    rc = fsl_db_prepare_cached(dbR, &q,
                               "SELECT srcid FROM delta "
                               "WHERE rid=?");
    if(!rc){
      rc = fsl_stmt_bind_id(q, 1, deltaRid);
      if(!rc){
        if(FSL_RC_STEP_ROW==(rc=fsl_stmt_step(q))){
          rc = 0;
          *rv = fsl_stmt_g_id(q, 0);
        }else if(FSL_RC_STEP_DONE==rc) rc = 0;
      }
      fsl_stmt_cached_yield(q);
    }
    return rc;
  }
}



int fsl_repo_verify_before_commit( fsl_cx * f, fsl_id_t rid ){
  if(0){
    /*
       v1 adds a commit hook here on the first entry, but it only
       seems to ever use one commit hook, so the infrastructure seems
       like overkill here. Thus this final verification is called from
       the commit (that's where v1 calls the hook).

       If we eventually add commit hooks, this is the place to do it.
    */
  }
  assert( fsl_cx_db_repo(f)->beginCount > 0 );
  return rid>0
    ? fsl_id_bag_insert(&f->cache.toVerify, rid)
    : FSL_RC_RANGE;    
}

void fsl_repo_verify_cancel( fsl_cx * f ){
  fsl_id_bag_clear(&f->cache.toVerify);
}

fsl_uuid_str fsl_rid_to_uuid(fsl_cx * f, fsl_id_t rid){
  fsl_db * db = f ? fsl_cx_db_repo(f) : NULL;
  if(!f || !db || (rid<=0)){
    if(f) fsl_cx_err_set(f, FSL_RC_MISUSE,
                         "fsl_rid_to_uuid() requires "
                         "an opened repository and a "
                         "positive RID value.");
    return NULL;
  }else{
    char * rv = NULL;
    fsl_stmt * st = NULL;
    int rc;
    rc = fsl_db_prepare_cached(db, &st,
                               "SELECT uuid FROM blob "
                               "WHERE rid=?");
    if(!rc){
      rc = fsl_stmt_bind_id(st, 1, rid);
      if(!rc){
        rc = fsl_stmt_step(st);
        if(FSL_RC_STEP_ROW==rc){
          fsl_size_t len = 0;
          char const * x = fsl_stmt_g_text(st, 0, &len);
          rc = 0;
          rv = x ? fsl_strndup(x, (fsl_int_t)len ) : NULL;
          if(x && !rv){
            fsl_cx_err_set(f, FSL_RC_OOM, NULL);
          }
        }else if(FSL_RC_STEP_DONE){
          fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                         "No blob found for rid %"FSL_ID_T_PFMT".",
                         rid);
        }
      }
      fsl_stmt_cached_yield(st);
      if(rc && !f->error.code){
        assert(db->error.code);
        fsl_cx_uplift_db_error(f, db);
      }
    }
    return rv;
  }
}

fsl_uuid_str fsl_rid_to_artifact_uuid(fsl_cx * f, fsl_id_t rid, fsl_catype_t type){
  fsl_db * db = f ? fsl_cx_db_repo(f) : NULL;
  if(!f || !db || (rid<=0)) return NULL;
  else{
    char * rv = NULL;
    fsl_stmt * st = NULL;
    int rc;
    rc = fsl_db_prepare_cached(db, &st,
                               "SELECT uuid FROM blob "
                               "WHERE rid=?1 AND EXISTS "
                               "(SELECT 1 FROM event"
                               " WHERE event.objid=?1 "
                               " AND event.type GLOB %Q)",
                               fsl_catype_event_cstr(type)
                               );
    if(!rc){
      rc = fsl_stmt_bind_id(st, 1, rid);
      if(!rc){
        rc = fsl_stmt_step(st);
        if(FSL_RC_STEP_ROW==rc){
          fsl_size_t len = 0;
          char const * x = fsl_stmt_g_text(st, 0, &len);
          rv = x ? fsl_strndup(x, (fsl_int_t)len ) : NULL;
          if(x && !rv){
            fsl_cx_err_set(f, FSL_RC_OOM, NULL);
          }
        }else if(FSL_RC_STEP_DONE){
          fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                         "No %s artifact found with rid %"FSL_ID_T_PFMT".",
                         fsl_catype_cstr(type), (fsl_id_t) rid);
        }
      }
      fsl_stmt_cached_yield(st);
      if(rc && !f->error.code){
        fsl_cx_uplift_db_error(f, db);
      }
    }
    return rv;
  }
}


/**
    Load the record identify by rid. Make sure we can reproduce it
    without error.
   
    Return non-0 and set f's error state if anything goes wrong.  If
    this procedure returns 0 it means that everything looks OK.
 */
static int fsl_repo_verify_rid(fsl_cx * f, fsl_id_t rid){
  fsl_uuid_str uuid = NULL;
  fsl_buffer hash = fsl_buffer_empty;
  fsl_buffer content = fsl_buffer_empty;
  int rc;
  fsl_db * db;
  if( fsl_content_size(f, rid)<0 ){
    return 0 /* No way to verify phantoms */;
  }
  db = fsl_cx_db_repo(f);
  assert(db);
  uuid = fsl_rid_to_uuid(f, rid);
  if(!uuid){
    rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                        "Could not find blob record for "
                        "rid #%"FSL_ID_T_PFMT".",
                        (fsl_id_t)rid);
  }
  else{
    if(!fsl_is_uuid(uuid)){
      rc = fsl_cx_err_set(f, FSL_RC_RANGE,
                          "Invalid uuid for rid #%"FSL_ID_T_PFMT": %s",
                          (fsl_id_t)rid, uuid);
    }
    else if( 0==(rc=fsl_content_get(f, rid, &content)) ){
      rc = fsl_sha1sum_buffer(&content, &hash);
      if( !rc && 0!=fsl_uuidcmp(uuid, fsl_buffer_cstr(&hash)) ){
        rc = fsl_cx_err_set(f, FSL_RC_CONSISTENCY,
                            "Hash of rid %"FSL_ID_T_PFMT" (%b) "
                            "does not match its uuid (%s)",
                            (fsl_id_t)rid, &hash, uuid);
      }
    }
  }
  fsl_free(uuid);
  fsl_buffer_clear(&hash);
  fsl_buffer_clear(&content);
  return rc;
}


int fsl_repo_verify_at_commit( fsl_cx * f ){
  fsl_id_t rid;
  int rc = 0;
  fsl_id_bag * bag = &f->cache.toVerify;
  /* v1 does content_cache_clear() here. */
  f->cache.inFinalVerify = 1;
  rid = fsl_id_bag_first(bag);
  if(f->cxConfig.traceSql){
    fsl_db_exec(f->dbMain,
                "SELECT 'Starting verify-at-commit.'");
  }
  while( !rc && rid>0 ){
    rc = fsl_repo_verify_rid(f, rid);
    if(!rc) rid = fsl_id_bag_next(bag, rid);
  }
  fsl_id_bag_clear(bag);
  f->cache.inFinalVerify = 0;
  if(rc && !f->error.code){
    fsl_cx_err_set(f, rc,
                   "Error #%d (%s) in fsl_repo_verify_at_commit()",
                   rc, fsl_rc_cstr(rc));
  }
  return rc;
}


static int fsl_repo_create_default_users(fsl_db * db, char addOnlyUser,
                                         char const * defaultUser ){
  int rc = fsl_db_exec(db,
                       "INSERT OR IGNORE INTO user(login, info) "
                       "VALUES(%Q,'')", defaultUser);
  if(!rc){
    rc = fsl_db_exec(db,
                     "UPDATE user SET cap='s', pw=lower(hex(randomblob(3)))"
                     " WHERE login=%Q", defaultUser);
    if( !rc && !addOnlyUser ){
      fsl_db_exec_multi(db,
                        "INSERT OR IGNORE INTO user(login,pw,cap,info)"
                        "   VALUES('anonymous',hex(randomblob(8)),'hmncz',"
                        "          'Anon');"
                        "INSERT OR IGNORE INTO user(login,pw,cap,info)"
                        "   VALUES('nobody','','gjor','Nobody');"
                        "INSERT OR IGNORE INTO user(login,pw,cap,info)"
                        "   VALUES('developer','','dei','Dev');"
                        "INSERT OR IGNORE INTO user(login,pw,cap,info)"
                        "   VALUES('reader','','kptw','Reader');"
                        );
    }
  }
  return rc;                       
}

int fsl_repo_create(fsl_cx * f, fsl_repo_create_opt const * opt ){

  fsl_db DB = fsl_db_empty;
  fsl_db * db = &DB;
  fsl_cx F = fsl_cx_empty /* used if !f */;
  int rc = 0;
  char const * userName;
  fsl_time_t const unixNow = (fsl_time_t)time(0);
  char fileExists;
  if(!opt || !opt->filename) return FSL_RC_MISUSE;
  fileExists = 0 == fsl_file_access(opt->filename,0);
  if(fileExists && !opt->allowOverwrite){
    return f
      ? fsl_cx_err_set(f, FSL_RC_ALREADY_EXISTS,
                       "File already exists and "
                       "allowOverwrite is false: %s",
                       opt->filename)
      : FSL_RC_ALREADY_EXISTS;
  }
  if(f){
    fsl_checkout_close(f) /* ignore result */;
  }else{
    f = &F;
    rc = fsl_cx_init( &f, NULL );
    if(rc){
      fsl_cx_finalize(f);
      return rc;
    }
  }
#if 1
  /* We probably should truncate/unlink the file here
     before continuing, to ensure a clean slate.
  */
  if(fileExists){
    FILE * f = fsl_fopen(opt->filename, "w"/*truncates it*/);
    if(!f) rc = fsl_errno_to_rc(errno, FSL_RC_IO);
    if(rc) goto end2;
  }
#endif
  db->f = f;
  rc = fsl_db_open(db, opt->filename, FSL_OPEN_F_RWC);
  if(rc){
    if(db->error.code){
      fsl_cx_uplift_db_error(f, db);
    }
    fsl_db_close(db);
    if(f==&F) fsl_cx_finalize(&F);
    return rc;
  }
  if(!f->repo.user){
    f->repo.user = fsl_guess_user_name()
      /* Ignore OOM error here - we'll use 'root'
         by default (but if we're really OOM here then
         the next op will fail).
      */;
  }
  userName = opt->username;

  rc = fsl_db_transaction_begin(db);
  if(rc) goto end1;
  /* Install the schemas... */
  rc = fsl_db_exec_multi(db, "%s",
                         fsl_schema_repo1());
  if(!rc) rc = fsl_db_exec_multi(db, "%s",
                                 fsl_schema_repo2());
  if(!rc) rc = fsl_db_exec_multi(db, "%s",
                                 fsl_schema_ticket_reports());
  if(rc) goto end1;

  if(1){
    /*
      Set up server-code and project-code...

      in v1 this is optional, so we will presumably eventually have to
      make it so here as well. Not yet sure where this routine is used
      in v1 (i.e. whether the option is actually exercised).
     */
    rc = fsl_db_exec_multi(db,
                           "INSERT INTO config (name,value,mtime) "
                           "VALUES ('server-code',"
                           "lower(hex(randomblob(20))),"
                           "%"FSL_INT64_T_PFMT");"
                           "INSERT INTO config (name,value,mtime) "
                           "VALUES ('project-code',"
                           "lower(hex(randomblob(20))),"
                           "%"FSL_INT64_T_PFMT");",
                           (fsl_int64_t)unixNow,
                           (fsl_int64_t)unixNow
                           );
    if(rc) goto end1;
  }

  
  /* Set some config vars ... */
  {
    fsl_stmt st = fsl_stmt_empty;
    rc = fsl_db_prepare(db, &st,
                        "INSERT INTO config (name,value,mtime) "
                        "VALUES (?,?,%"FSL_INT64_T_PFMT")",
                        (fsl_int64_t)unixNow);
    if(!rc){
      fsl_stmt_bind_int64(&st, 3, unixNow);
#define DBSET_STR(KEY,VAL) \
      fsl_stmt_bind_text(&st, 1, KEY, -1, 0);    \
      fsl_stmt_bind_text(&st, 2, VAL, -1, 0); \
      fsl_stmt_step(&st); \
      fsl_stmt_reset(&st)
      DBSET_STR("content-schema",FSL_CONTENT_SCHEMA);
      DBSET_STR("aux-schema",FSL_AUX_SCHEMA);
#undef DBSET_STR

#define DBSET_INT(KEY,VAL) \
      fsl_stmt_bind_text(&st, 1, KEY, -1, 0 );    \
      fsl_stmt_bind_int32(&st, 2, VAL); \
      fsl_stmt_step(&st); \
      fsl_stmt_reset(&st)

      DBSET_INT("autosync",1);
      DBSET_INT("localauth",0);
      DBSET_INT("timeline-plaintext", 1);
      
#undef DBSET_INT
      fsl_stmt_finalize(&st);
    }
  }

  rc = fsl_repo_create_default_users(db, 0, userName);
  if(rc) goto end1;

  end1:
  if(db->error.code && !f->error.code){
    rc = fsl_cx_uplift_db_error(f, db);
  }
  if(!rc) rc = fsl_db_transaction_end(db, 0);
  else fsl_db_transaction_end(db, 1);
  fsl_db_close(db);
  if(rc) goto end2;

  /**
      In order for injection of the first commit to go through
      cleanly (==without any ugly kludging of f->dbMain), we
      need to now open the new db so that it gets connected
      to f properly...
   */
  rc = fsl_repo_open( f, opt->filename );
  if(rc) goto end2;
  db = fsl_cx_db_repo(f);
  assert(db);
  assert(db == f->dbMain);

  if(!userName || !*userName){
    userName = f->repo.user
      ? f->repo.user
      : "root" /* historical value */;
  }

  /*
    Copy config...

    This is done in the second phase because...

    "cannot ATTACH database within transaction"

    and installing the initial schemas outside a transaction is
    horribly slow.
  */
  if( opt->configRepo && *opt->configRepo ){
    char inTrans = 0;
    char * inopConfig = fsl_config_inop_rhs(FSL_CONFIGSET_ALL);
    char * inopDb = inopConfig ? fsl_db_setting_inop_rhs() : NULL;
    if(!inopConfig || !inopDb){
      fsl_free(inopConfig);
      rc = FSL_RC_OOM;
      goto end2;
    }
    rc = fsl_db_attach(db, opt->configRepo, "settingSrc");
    if(rc){
      fsl_cx_uplift_db_error(f, db);
      goto end2;
    }
    rc = fsl_db_transaction_begin(db);
    if(rc){
      fsl_cx_uplift_db_error(f, db);
      goto detach;
    }
    inTrans = 1;
    /*
       Copy all settings from the supplied template repository.
    */
    rc = fsl_db_exec(db,
                     "INSERT OR REPLACE INTO config"
                     " SELECT name,value,mtime FROM settingSrc.config"
                     "  WHERE (name IN %s OR name IN %s)"
                     "    AND name NOT GLOB 'project-*';",
                     inopConfig, inopDb);
    if(rc) goto detach;
    rc = fsl_db_exec(db,
                     "REPLACE INTO reportfmt "
                     "SELECT * FROM settingSrc.reportfmt;");
    if(rc) goto detach;

    /*
       Copy the user permissions, contact information, last modified
       time, and photo for all the "system" users from the supplied
       template repository into the one being setup.  The other
       columns are not copied because they contain security
       information or other data specific to the other repository.
       The list of columns copied by this SQL statement may need to be
       revised in the future.
    */
    rc = fsl_db_exec(db, "UPDATE user SET"
      "  cap = (SELECT u2.cap FROM settingSrc.user u2"
      "         WHERE u2.login = user.login),"
      "  info = (SELECT u2.info FROM settingSrc.user u2"
      "          WHERE u2.login = user.login),"
      "  mtime = (SELECT u2.mtime FROM settingSrc.user u2"
      "           WHERE u2.login = user.login),"
      "  photo = (SELECT u2.photo FROM settingSrc.user u2"
      "           WHERE u2.login = user.login)"
      " WHERE user.login IN ('anonymous','nobody','developer','reader');"
    );

    detach:
    fsl_free(inopConfig);
    fsl_free(inopDb);
    if(inTrans){
      if(!rc) rc = fsl_db_transaction_end(db,0);
      else fsl_db_transaction_end(db,1);
    }
    fsl_db_detach(db, "settingSrc");
    if(rc) goto end2;
  }

  if(opt->commitMessage && *opt->commitMessage){
    /*
      Set up initial commit. Because of the historically empty P-card
      on the first commit, we can't create that one using the fsl_deck
      API unless we elide the P-card (not as v1 does) and insert an
      empty R-card (as v1 does). We need one of P- or R-card to
      unambiguously distinguish this MANIFEST from a CONTROL artifact.
    */
    fsl_deck d = fsl_deck_empty;
    fsl_cx_err_reset(f);
    fsl_deck_init(f, &d, FSL_CATYPE_CHECKIN);
    rc = fsl_deck_C_set(&d, opt->commitMessage, -1);
    if(!rc) rc = fsl_deck_D_set(&d, fsl_julian_now());
    if(!rc) rc = fsl_deck_R_set(&d, FSL_MD5_INITIAL_HASH);
    /* Reminder: setting tags in "wrong" (unsorted) order to
       test/assert that the sorting gets done automatically. */
    if(!rc) rc = fsl_deck_T_add(&d, FSL_TAGTYPE_PROPAGATING, NULL,
                         "sym-trunk", NULL);
    if(!rc) rc = fsl_deck_T_add(&d, FSL_TAGTYPE_PROPAGATING, NULL,
                         "branch", "trunk");
    if(!rc) rc =fsl_deck_U_set(&d, userName, -1);
    if(!rc){
      rc = fsl_deck_save(&d, 0);
      if(!rc){
        rc = fsl_deck_crosslink(&d);
      }
    }
    if(rc && d.error.code && !f->error.code){
      rc = fsl_cx_err_set_e(f, &d.error);
    }
    fsl_deck_finalize(&d);
  }
  
  end2:
  if(f == &F){
    fsl_cx_finalize(f);
  }
  return rc;
}

static int fsl_repo_dir_names_rid( fsl_cx * f, fsl_id_t rid, fsl_list * tgt,
                         char addSlash){
  fsl_db * dbR = fsl_needs_repo(f);
  fsl_deck D = fsl_deck_empty;
  fsl_deck * d = &D;
  int rc = 0;
  fsl_stmt st = fsl_stmt_empty;
  fsl_buffer tname = fsl_buffer_empty;
  int count = 0;
  fsl_card_F const * fc;

  /*
    This is a poor-man's impl. A more efficient one would calculate
    the directory names without using the database.
  */

  assert(rid>0);
  assert(dbR);
  rc = fsl_deck_load_rid( f, d, rid, FSL_CATYPE_CHECKIN);
  if(rc){
    fsl_deck_clean(d);
    return rc;
  }
  rc = fsl_buffer_appendf(&tname,
                          "tmp_filelist_for_rid_%d",
                          (int)rid);
  if(rc) goto end;
  rc = fsl_deck_F_rewind(d);
  while( !rc && !(rc=fsl_deck_F_next(d, &fc)) && fc ){
    if(!fc->name) continue;
    assert(*fc->name);
    if(!st.stmt){
      rc = fsl_db_exec(dbR, "CREATE TEMP TABLE IF NOT EXISTS "
                       "%b(n TEXT UNIQUE ON CONFLICT IGNORE)",
                       &tname);
      if(!rc){
        rc = fsl_db_prepare(dbR, &st,
                            "INSERT INTO %b(n) "
                            "VALUES(fsl_dirpart(?,%d))",
                            &tname, addSlash ? 1 : 0);
      }
      if(rc) goto end;
      assert(st.stmt);
    }
    rc = fsl_stmt_bind_text(&st, 1, fc->name, -1, 0);
    if(!rc){
      rc = fsl_stmt_step(&st);
      if(FSL_RC_STEP_DONE==rc){
        ++count;
        rc = 0;
      }
    }
    fsl_stmt_reset(&st);
    fc = 0;
  }

  if(!rc && (count>0)){
    fsl_stmt_finalize(&st);
    rc = fsl_db_prepare(dbR, &st,
                        "SELECT n FROM %b WHERE n "
                        "IS NOT NULL ORDER BY n %s",
                        &tname,
                        fsl_cx_filename_collation(f));
    while( !rc && (FSL_RC_STEP_ROW==(rc=fsl_stmt_step(&st))) ){
      fsl_size_t nLen = 0;
      char const * name = fsl_stmt_g_text(&st, 0, &nLen);
      rc = 0;
      if(name){
        char * cp;
        assert(nLen);
        cp = fsl_strndup( name, (fsl_int_t)nLen );
        if(!cp){
          rc = FSL_RC_OOM;
          break;
        }
        rc = fsl_list_append(tgt, cp);
        if(rc){
          fsl_free(cp);
          break;
        }
      }
    }
    if(FSL_RC_STEP_DONE==rc) rc = 0;
  }

  end:
  if(rc && !f->error.code && dbR->error.code){
    fsl_cx_uplift_db_error(f, dbR);
  }
  fsl_stmt_finalize(&st);
  fsl_deck_clean(d);
  if(tname.used){
    fsl_db_exec(dbR, "DROP TABLE IF EXISTS %b", &tname);
  }
  fsl_buffer_clear(&tname);
  return rc;
}

int fsl_repo_dir_names( fsl_cx * f, fsl_id_t rid, fsl_list * tgt,
                        char addSlash ){
  fsl_db * db = (f && tgt) ? fsl_needs_repo(f) : NULL;
  if(!f || !tgt) return FSL_RC_MISUSE;
  else if(!db) return FSL_RC_NOT_A_REPO;
  else {
    int rc;
    if(rid>=0){
      if(!rid){
        /* Dir list for current checkout version */
        if(f->ckout.rid>0){
          rid = f->ckout.rid;
        }else{
          return fsl_cx_err_set(f, FSL_RC_RANGE,
                                "The rid argument is 0 (indicating "
                                "the current checkout), but there is "
                                "no opened checkout.");
        }
      }
      assert(rid>0);
      rc = fsl_repo_dir_names_rid(f, rid, tgt, addSlash);
    }else{
      /* Dir list across all versions */
      fsl_stmt s = fsl_stmt_empty;
      rc = fsl_db_prepare(db, &s,
                          "SELECT DISTINCT(fsl_dirpart(name,%d)) dname "
                          "FROM filename WHERE dname IS NOT NULL "
                          "ORDER BY dname", addSlash ? 1 : 0);
      if(rc){
        fsl_cx_uplift_db_error(f, db);
        assert(!s.stmt);
        return rc;
      }
      while( !rc && (FSL_RC_STEP_ROW==(rc=fsl_stmt_step(&s)))){
        fsl_size_t len = 0;
        char const * col = fsl_stmt_g_text(&s, 0, &len);
        char * cp = fsl_strndup( col, (fsl_int_t)len );
        if(!cp){
          rc = FSL_RC_OOM;
          break;
        }
        rc = fsl_list_append(tgt, cp);
        if(rc) fsl_free(cp);
      }
      if(FSL_RC_STEP_DONE==rc) rc = 0;
      fsl_stmt_finalize(&s);
    }
    return rc;
  }
}

/* UNTESTED */
char fsl_repo_is_readonly(fsl_cx const * f){
  if(!f || !f->dbMain) return 0;
  else{
    int const roleId = f->ckout.db.dbh ? FSL_DB_ROLE_MAIN : FSL_DB_ROLE_REPO
      /* If CKOUT is attached, it is the main DB and REPO is ATTACHed. */
      ;
    char const * zRole = fsl_db_role_label(roleId);
    assert(f->dbMain);
    return sqlite3_db_readonly(f->dbMain->dbh, zRole) ? 1 : 0;
  }
}

int fsl_repo_record_filename(fsl_cx * f){
  fsl_buffer full = fsl_buffer_empty;
  fsl_db * dbR = fsl_needs_repo(f);
  fsl_db * dbC;
  fsl_db * dbConf;
  char const * zCDir;
  char const * zName = dbR ? dbR->filename : NULL;
  int rc;
  if(!dbR) return FSL_RC_NOT_A_REPO;
  assert(zName);
  assert(f);
  rc = fsl_file_canonical_name(zName, &full, 0);
  if(rc){
    fsl_cx_err_set(f, rc, "Error %s canonicalizing filename: %s", zName);
    goto end;
  }

  /*
    If global config is open, write the repo db's name to it.
   */
  dbConf = fsl_cx_db_config(f);
  if(dbConf){
    int const dbRole = (f->dbMain==&f->config.db)
      ? FSL_DB_ROLE_MAIN : FSL_DB_ROLE_CONFIG;
    rc = fsl_db_exec(dbConf,
                     "INSERT OR IGNORE INTO %s.global_config(name,value) "
                     "VALUES('repo:%q',1)",
                     fsl_db_role_label(dbRole),
                     fsl_buffer_cstr(&full));
    if(rc) goto end;
  }

  dbC = fsl_cx_db_checkout(f);
  if(dbC && (zCDir=f->ckout.dir)){
    /* If we have a checkout, update its repo's list of checkouts... */
    /* Assumption: if we have an opened checkout, dbR is ATTACHed with
       the role REPO. */
    int ro;
    assert(dbR);
    ro = sqlite3_db_readonly(dbR->dbh,
                             fsl_db_role_label(FSL_DB_ROLE_REPO));
    assert(ro>=0);
    if(!ro){
      fsl_buffer localRoot = fsl_buffer_empty;
      rc = fsl_file_canonical_name(zCDir, &localRoot, 1);
      if(0==rc){
        if(dbConf){
          /*
            If global config is open, write the checkout db's name to it.
          */
          int const dbRole = (f->dbMain==&f->config.db)
            ? FSL_DB_ROLE_MAIN : FSL_DB_ROLE_CONFIG;
          rc = fsl_db_exec(dbConf,
                           "REPLACE INTO INTO %s.global_config(name,value) "
                           "VALUES('ckout:%q',1)",
                           fsl_db_role_label(dbRole),
                           fsl_buffer_cstr(&localRoot));
        }
        if(0==rc){
          /* We know that repo is ATTACHed to ckout here. */
          assert(dbR == dbC);
          rc = fsl_db_exec(dbR,
                           "REPLACE INTO %s.config(name, value, mtime) "
                           "VALUES('ckout:%q', 1, now())",
                           fsl_db_role_label(FSL_DB_ROLE_REPO),
                           fsl_buffer_cstr(&localRoot));
        }
      }
      fsl_buffer_clear(&localRoot);
    }
  }

  end:
  if(rc && !f->error.code && f->dbMain->error.code){
    fsl_cx_uplift_db_error(f, f->dbMain);
  }
  fsl_buffer_clear(&full);
  return rc;

}

char fsl_rid_is_a_checkin(fsl_cx * f, fsl_id_t rid){
  fsl_db * db = f ? fsl_cx_db_repo(f) : NULL;
  if(!db || (rid<=0)) return 0;
  else{
    fsl_stmt * st = 0;
    char rv = 0;
    int rc = fsl_db_prepare_cached(db, &st,
                                   "SELECT 1 FROM event WHERE "
                                   "objid=? AND type='ci'");
    if(!rc){
      rc = fsl_stmt_bind_id( st, 1, rid);
      if(!rc){
        rc = fsl_stmt_step(st);
        if(FSL_RC_STEP_ROW==rc){
          rv = 1;
        }
      }
      fsl_stmt_cached_yield(st);
    }
    if(db->error.code){
      fsl_cx_uplift_db_error(f, db);
    }
    return rv;
  }
}

#define fsl_repo_extract_state_empty_m {\
    NULL/*state*/, NULL/*fc*/, NULL/*content*/,\
    NULL/*f*/, 0/*versionRid*/, 0/*fileRid*/, \
    0/*reportDeletions*/\
  }
static const fsl_repo_extract_state fsl_repo_extract_state_empty =
  fsl_repo_extract_state_empty_m;


int fsl_repo_extract( fsl_cx * f, fsl_id_t vid,
                      char reportDeletions,
                      fsl_repo_extract_f callback, void * callbackState ){
  if(!f || !callback) return FSL_RC_MISUSE;
  else if(!fsl_needs_repo(f)) return FSL_RC_NOT_A_REPO;
  else if(vid<=0) return FSL_RC_RANGE;
  else{
    int rc;
    fsl_deck mf = fsl_deck_empty;
    fsl_buffer * content = &f->fileContent;
    fsl_id_t fid;
    fsl_repo_extract_state xst = fsl_repo_extract_state_empty;
    fsl_deck parent = fsl_deck_empty;
    fsl_card_F const * fc;
    assert(!content->used && "Misuse of f->fileContent");
    rc = fsl_deck_load_rid(f, &mf, vid, FSL_CATYPE_CHECKIN);
    if(rc) goto end;
    assert(mf.f==f);

    /*
      For delta manifests, we know which files were deleted between
      the parent and baseline because they are in the
      manifest. Baseline manifests, however, do not store deletion
      info. In order to be able to report all files deleted between
      the parent (not baseline) version and vid, we have to load the
      parent version's manifest. Note that we do not bother with the
      multi-parent edge case here, but it is hoped that that won't be
      too problematic in practice.
    */
    if(reportDeletions
       && mf.P.used/*we have a parent version*/){
      char const * zParent = (char const *)mf.P.list[0];
      fsl_id_t pid = fsl_uuid_to_rid(f, zParent);
      if(pid<0){
        assert(f->error.code);
        rc = f->error.code;
        goto end;
      }else if(!pid){
        rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                            "Cannot resolve UUID: %s", zParent);
        goto end;
      }
      rc = fsl_deck_load_rid(f, &parent, pid, FSL_CATYPE_CHECKIN);
      if(!rc) rc = fsl_deck_F_rewind(&parent);
      if(rc) goto end;
    }

    rc = fsl_deck_F_rewind(&mf);
    while( !rc && !(rc=fsl_deck_F_next(&mf, &fc)) && fc){
      fsl_card_F * pfc = reportDeletions
        ? fsl_deck_F_seek(&parent, fc->name)
        : NULL;
      if(pfc && pfc->uuid){
        /* We'll mark each child entry we've seen by freeing its uuid
           in the parent. At the end, any in the parent we have not
           marked were deleted between parent and vid.
        */
        fsl_free(pfc->uuid);
        pfc->uuid = NULL;
      }
      if(!fc->uuid){
        if(!reportDeletions) continue;
        fid = 0 /* file was deleted in this manifest */;
      }else{
        fid = fsl_uuid_to_rid(f, fc->uuid);
        if(fid<=0){
          assert(f->error.code);
          rc = f->error.code;
          goto end;
        }else{
          content->used = 0;
          rc = fsl_content_get(f, fid, content);
        }
      }
      if(!rc){
        xst.f = f;
        xst.fc = fc;
        xst.content = (fid>0) ? content : NULL;
        xst.versionRid = vid;
        xst.fileRid = fid;
        xst.state = callbackState;
        xst.reportDeletions = reportDeletions;
        rc = callback( &xst );
        if(FSL_RC_BREAK==rc){
          rc = 0;
          break;
        }
      }
    }
    if(!rc && reportDeletions){
      fsl_card_F const * pfc = NULL;
      fsl_deck_F_rewind(&parent);
      while( !(rc=fsl_deck_F_next(&parent, &pfc)) && pfc){
        if(!pfc || !pfc->uuid) continue;
        else{
          /*
            This file was not seen in the vid, so it "must have"
            been deleted between parent and child. We NULL
            out the UUID to tell the caller it was deleted.
          */
          fsl_card_F tmpF = *pfc;
          tmpF.uuid = NULL;
          xst.f = f;
          xst.fc = &tmpF;
          xst.versionRid = vid;
          xst.fileRid = 0;
          xst.content = NULL;
          xst.state = callbackState;
          xst.reportDeletions = 1;
          rc = callback( &xst );
          if(FSL_RC_BREAK==rc){
            rc = 0;
            break;
          }
        }
      }
    }
    end:
    fsl_buffer_reset(content);
    fsl_deck_finalize(&mf);
    fsl_deck_finalize(&parent);
    return rc;
  }
}

enum fsl_version_file_change_t {
FSL_VCHANGE_ADDED,
FSL_VCHANGE_MODIFIED,
FSL_VCHANGE_REMOVED,
FSL_VCHANGE_RENAMED
};
typedef enum fsl_version_file_change_t fsl_version_file_change_t;

struct fsl_version_file_change {
  fsl_version_file_change_t type;
  char const * uuid;
  char const * priorContent;
};
typedef struct fsl_version_file_change fsl_version_file_change;
typedef int (*fsl_version_file_changes_f)();

int fsl_calc_version_file_changes( fsl_cx * f, fsl_id_t v1, fsl_id_t v2,
                                   fsl_version_file_changes_f callback,
                                   void * state ){
  int rc = FSL_RC_NYI;
  return rc;
}


#undef MARKER