Login
Artifact [3029592771]
Login

Artifact 30295927710400a8af50a38f940d3e5c1f39803c:


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

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

  Heavily indebted to the Fossil SCM project (https://fossil-scm.org).
*/
/*  
  *****************************************************************************
  This file houses the code for checkout-level APIS.
*/
#include <assert.h>

#include "fossil-scm/fossil-internal.h"
#include "fossil-scm/fossil-checkout.h"
#include "fossil-scm/fossil-hash.h"
#include <string.h> /* memcmp() */

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



/**
    Kludge for type-safe strncmp/strnicmp inconsistency.
 */
static int fsl_strnicmp_int(char const *zA, char const * zB, fsl_size_t nByte){
  return fsl_strnicmp( zA, zB, (fsl_int_t)nByte);
}

static int fsl_cx_init_fsscratch(fsl_cx * f){
  enum {
  /* because testing shows a lot of re-allocs via some of the lower-level
     stat()-related bits, we pre-allocate this many bytes into f->fsScratch.
     Curiously, i see almost no difference in (re)allocation behaviour until i
     increase this to over 200.
  */
  InitialScratchCapacity = 512
  };
  int rc = 0;
  if(f->fsScratch.capacity < InitialScratchCapacity){
    rc = fsl_buffer_reserve(&f->fsScratch, InitialScratchCapacity);
    assert(rc || (f->fsScratch.capacity >= InitialScratchCapacity));
  }
  return rc;
}

int fsl_checkout_filename_check( fsl_cx * f, bool relativeToCwd,
                                 char const * zOrigName, fsl_buffer * pOut ){
  int rc;
  if(!zOrigName || !*zOrigName) return FSL_RC_MISUSE;
  else if(!f->ckout.dir
          || !fsl_needs_checkout(f)/* will update f's error state*/
          ) return FSL_RC_NOT_A_CHECKOUT;
#if 0
  /* Is this sane? */
  else if(fsl_is_simple_pathname(zOrigName,1)){
    rc = 0;
    if(pOut){
      rc = fsl_buffer_append(pOut, zOrigName, fsl_strlen(zOrigName));
    }
  }
#endif
  else{
    char const * zLocalRoot;
    char const * zFull;
    fsl_size_t nLocalRoot;
    fsl_size_t nFull;
    fsl_buffer * full = &f->fsScratch;
    int (*xCmp)(char const *, char const *,fsl_size_t);
    char endsWithSlash;
    assert((pOut != full) && "Misuse of f->fsScratch!");
    assert(full->used==0 && "Someone did not re-set f->fsScratch OR it is in use higher up the stack.");
    if(!full->mem){
      rc = fsl_cx_init_fsscratch(f);
      if(rc) goto end;
    }else{
      fsl_buffer_reuse(full);
    }
    zLocalRoot = f->ckout.dir;
    assert(zLocalRoot);
    assert(*zLocalRoot);
    nLocalRoot = f->ckout.dirLen;
    assert(nLocalRoot);
    assert('/' == zLocalRoot[nLocalRoot-1]);
    full->used = 0;
    rc = fsl_file_canonical_name2(relativeToCwd ? NULL : zLocalRoot,
                                  zOrigName, full, 1);
#if 0
    MARKER(("canon2: %p (%s) %s ==> %s\n", (void const *)full->mem,
            relativeToCwd ? "cwd" : "ckout", zOrigName, fsl_buffer_cstr(full)));
#endif
    if(rc){
      if(FSL_RC_OOM != rc){
        rc = fsl_cx_err_set(f, rc, "Error #%d (%s) canonicalizing "
                            "file name: %s\n",
                            rc, fsl_rc_cstr(rc),
                            zOrigName);
      }
      goto end;
    }
    zFull = fsl_buffer_cstr2(full, &nFull);
    xCmp = fsl_cx_is_case_sensitive(f)
      ? fsl_strncmp
      : fsl_strnicmp_int;
    assert(zFull);
    assert(nFull>0);
    endsWithSlash = '/' == zFull[nFull-1];
    if( ((nFull==nLocalRoot-1 || (nFull==nLocalRoot && endsWithSlash))
         && xCmp(zLocalRoot, zFull, nFull)==0)
        || (nFull==1 && zFull[0]=='/' && nLocalRoot==1 && zLocalRoot[0]=='/') ){
      enum { OutputDot = 1 };
      /* Special case.  zOrigName refers to zLocalRoot directory.

         Outputing "." instead of nothing is a historical decision
         which may be worth re-evaluating. Currently fsl_cx_stat() relies
         on it.
      */
      if(pOut){
        char const * zOut;
        fsl_size_t nOut;
        if(endsWithSlash){ /* retain trailing slash */
          zOut = "./";
          nOut = 2;
        }else{
          zOut = ".";
          nOut = 1;
        };
        rc = fsl_buffer_append(pOut, zOut, nOut);
      }else{
        rc = 0;
      }
      goto end;
    }

    if( nFull<=nLocalRoot || xCmp(zLocalRoot, zFull, nLocalRoot) ){
      rc = fsl_cx_err_set(f, FSL_RC_RANGE,
                          "File is outside of checkout tree: %s",
                          zOrigName);
      goto end;
    }

    if(pOut){
      rc = fsl_buffer_append(pOut, zFull + nLocalRoot, nFull - nLocalRoot);
    }

    end:
    fsl_buffer_reuse(full);
  }
  return rc;
}


/**
    Returns a fsl_checkout_change_e value for the given
    fsl_vfile_change_e value.

    Why are these not consolidated into one enum?
*/
static fsl_checkout_change_e fsl_vfile_to_ckout(int vChange){
  switch((fsl_vfile_change_e)vChange){
#define EE(X) case FSL_VFILE_CHANGE_##X: return FSL_CKOUT_CHANGE_##X
    EE(NONE);
    EE(MOD);
    EE(MERGE_MOD);
    EE(MERGE_ADD);
    EE(INTEGRATE_MOD);
    EE(INTEGRATE_ADD);
#undef EE
    default:
       assert(!"Unhandled fsl_vfile_change_e value!");
      return FSL_CKOUT_CHANGE_NONE;
  }
}

int fsl_checkout_changes_visit( fsl_cx * f, fsl_id_t vid,
                                bool doScan,
                                fsl_checkout_changes_f visitor,
                                void * state ){
  int rc;
  fsl_db * db;
  fsl_stmt st = fsl_stmt_empty;
  int count = 0;
  fsl_checkout_change_e change;
  fsl_fstat fstat;
  if(!f || !visitor) return FSL_RC_MISUSE;
  db = fsl_needs_checkout(f);
  if(!db) return FSL_RC_NOT_A_CHECKOUT;
  if(vid<0){
    vid = f->ckout.rid;
    assert(vid>=0);
  }
  if(doScan){
    rc = fsl_vfile_changes_scan(f, vid, 0);
    if(rc) goto end;
  }
  rc = fsl_db_prepare(db, &st,
                      "SELECT chnged, deleted, rid, pathname, origname "
                      "FROM vfile WHERE vid=%"FSL_ID_T_PFMT,
                      (fsl_id_t)vid);
  assert(!rc);
  while( FSL_RC_STEP_ROW == fsl_stmt_step(&st) ){
    int const changed = fsl_stmt_g_int32(&st, 0);
    int const deleted = fsl_stmt_g_int32(&st,1);
    fsl_id_t const vrid = fsl_stmt_g_id(&st,2);
    char const * name;
    char const * oname = NULL;
    name = fsl_stmt_g_text(&st, 3, NULL);
    oname = fsl_stmt_g_text(&st,4,NULL);
    if(oname && (0==fsl_strcmp(name, oname))){
      /* Work around a fossil oddity which sets origname=pathname
         during a 'mv' operation.
      */
      oname = NULL;
    }
    change = FSL_CKOUT_CHANGE_NONE;
    if(deleted){
      change = FSL_CKOUT_CHANGE_REMOVED;
    }else if(0==vrid){
      change = FSL_CKOUT_CHANGE_ADDED;
    }else if(NULL != oname){
      change = FSL_CKOUT_CHANGE_RENAMED;
    }else{
      fstat = fsl_fstat_empty;
      if( fsl_cx_stat(f, 0, name, &fstat ) ){
        change = FSL_CKOUT_CHANGE_MISSING;
        fsl_cx_err_reset(f) /* keep FSL_RC_NOT_FOUND from bubbling
                               up to the client! */;
      }else if(!changed){
        continue;
      }else{
        change = fsl_vfile_to_ckout(changed);
      }
    }
    if(!change){
      MARKER(("INTERNAL ERROR: unhandled vfile.chnged value %d for file [%s]\n",
              changed, name));
      continue;
    }
    ++count;
    rc = visitor(state, change, name, oname);
    if(rc){
      if(FSL_RC_BREAK==rc){
        rc = 0;
        break;
      }else if(!f->error.code && (FSL_RC_OOM!=rc)){
        fsl_cx_err_set(f, rc, "Error %s returned from changes callback.",
                       fsl_rc_cstr(rc));
      }
      break;
    }
  }
  end:
  fsl_stmt_finalize(&st);
  if(rc && db->error.code && !f->error.code){
    fsl_cx_uplift_db_error(f, db);
  }

  return rc;
}

static bool fsl_co_is_in_vfile(fsl_cx *f,
                               char const *zFilename){
  return fsl_db_exists(fsl_cx_db_checkout(f),
                       "SELECT 1 FROM vfile"
                       " WHERE vid=%"FSL_ID_T_PFMT
                       " AND pathname=%Q %s",
                       f->ckout.rid, zFilename,
                       fsl_cx_filename_collation(f));
}
/**
   Internal machinery for fsl_checkout_file_add(). zFilename MUST
   be a checkout-relative file which is known to exist. fst MUST
   be an object populated by fsl_stat()'ing zFilename. isInVFile
   MUST be the result of having passed zFilename to fsl_co_is_in_vfile().
 */
static int fsl_checkout_file_add_impl( fsl_cx * f, char const *zFilename,
                                       fsl_fstat const *fst,
                                       bool isInVFile){
  int rc = 0;
  fsl_db * const db = fsl_needs_checkout(f);
  assert(fsl_is_simple_pathname(zFilename, true));
  if( isInVFile ){
    rc = fsl_db_exec(db, "UPDATE vfile SET deleted=0,"
                     " mtime=%"PRIi64
                     " WHERE vid=%"FSL_ID_T_PFMT
                     " AND pathname=%Q %s",
                     (int64_t)fst->mtime,
                     f->ckout.rid, zFilename,
                     fsl_cx_filename_collation(f));
  }else{
    int const chnged = FSL_VFILE_CHANGE_MOD
      /* fossil(1) sets chnged=0 on 'add'ed vfile records, but then the 'status'
         command updates the field to 1. To avoid down-stream inconsistencies
         (such as the ones which lead me here), we'll go ahead and set it to
         1 here.
      */;
    rc = fsl_db_exec(db,
                     "INSERT INTO "
                     "vfile(vid,chnged,deleted,rid,mrid,pathname,isexe,islink,mtime)"
                     "VALUES(%"FSL_ID_T_PFMT",%d,0,0,0,%Q,%d,%d,%"PRIi64")",
                     f->ckout.rid, chnged, zFilename,
                     (FSL_FSTAT_PERM_EXE==fst->perm) ? 1 : 0,
                     (FSL_FSTAT_TYPE_LINK==fst->type) ? 1 : 0,
                     (int64_t)fst->mtime
                     );
  }
  if(rc) rc = fsl_cx_uplift_db_error2(f, db, rc);
  return rc;
}

/**
   Internal state for the recursive file-add process.
*/
struct CoAddState {
  fsl_cx * f;
  fsl_checkout_file_add_opt * opt;
  fsl_buffer * absBuf; // absolute path of file to check
  fsl_buffer * coRelBuf; // checkout-relative path of absBuf
  fsl_fstat fst; // fsl_stat() state of absBuf's file
};
typedef struct CoAddState CoAddState;
static const CoAddState CoAddState_empty =
  {NULL, NULL, NULL, NULL, fsl_fstat_empty_m};

/**
   fsl_dircrawl_f() impl for recursively adding files to a
   repo. state must be a (CoAddState*)/
*/
static int fsl_dircrawl_f_add(fsl_dircrawl_state const *);

/**
   Attempts to add file or directory (recursively) cas->absBuf to the
   current repository. isCrawling must be true if this is a
   fsl_dircrawl()-invoked call, else false.
*/
static int co_add_one(CoAddState * cas, bool isCrawling){
  int rc = 0;
  fsl_buffer_reuse(cas->coRelBuf);
  rc = fsl_cx_stat2(cas->f, cas->opt->relativeToCwd,
                    fsl_buffer_cstr(cas->absBuf), &cas->fst,
                    fsl_buffer_reuse(cas->coRelBuf), false);
  if(rc) return rc;
  switch(cas->fst.type){
    case FSL_FSTAT_TYPE_FILE:{
      bool skipped = false;
      char const * zCoRel = fsl_buffer_cstr(cas->coRelBuf);
      bool const isInVFile = fsl_co_is_in_vfile(cas->f, zCoRel);
      if(!isInVFile){
        if(fsl_reserved_fn_check(cas->f, zCoRel,-1)){
          /* ^^^ we need to use fsl_reserved_fn_check(), instead of
             fsl_is_reserved_fn(), so that we will inherit any
             new checks which require a context object. If that
             check fails, though, it updates cas->f with an error
             message which we need to suppress here to avoid it
             accidedentally propagating and causing downstream
             confusion. */
          fsl_cx_err_reset(cas->f);
          skipped = true;
        }else if(cas->opt->checkIgnoreGlobs){
          char const * m =
            fsl_cx_glob_matches(cas->f, FSL_GLOBS_IGNORE, zCoRel);
          if(m) skipped = true;
        }
        if(!skipped && cas->opt->callback){
          bool yes = false;
          rc = cas->opt->callback( zCoRel, cas->opt->callbackState,
                                   &yes );
          if(rc) goto end;
          else if(!yes) skipped = true;
        }
      }
      if(skipped){
        ++cas->opt->counts.skipped;
      }else{
        rc = fsl_checkout_file_add_impl(cas->f, zCoRel, &cas->fst,
                                        isInVFile);
        if(!rc){
          if(isInVFile) ++cas->opt->counts.updated;
          else ++cas->opt->counts.added;
        }
      }
      break;
    }
    case FSL_FSTAT_TYPE_DIR:
      if(!isCrawling){
        /* Reminder to self: fsl_dircrawl() copies its first argument
           for canonicalizing it, so this is safe even though
           cas->absBuf may be reallocated during the recursive
           call. We're done with these particular contents of
           cas->absBuf at this point. */
        rc = fsl_dircrawl(fsl_buffer_cstr(cas->absBuf),
                          fsl_dircrawl_f_add, cas);
        if(rc && !cas->f->error.code){
          rc = fsl_cx_err_set(cas->f, rc, "fsl_dircrawl() returned %s.",
                              fsl_rc_cstr(rc));
        }
      }else{
        assert(!"Cannot happen - caught higher up");
        fsl_fatal(FSL_RC_ERROR, "Internal API misuse in/around %s().",
                  __func__);
      }
      break;
    default:
      rc = fsl_cx_err_set(cas->f, FSL_RC_TYPE,
                          "Unhandled filesystem entry type: "
                          "fsl_fstat_type_e #%d", cas->fst.type);
      break;
  }
  end:
  return rc;
}

static int fsl_dircrawl_f_add(fsl_dircrawl_state const *dst){
  if(FSL_FSTAT_TYPE_FILE!=dst->entryType) return 0;
  CoAddState * cas = (CoAddState*)dst->callbackState;
  int rc = fsl_buffer_appendf(fsl_buffer_reuse(cas->absBuf),
                              "%s/%s", dst->absoluteDir, dst->entryName);
  if(!rc) rc = co_add_one(cas, true);
  return rc;
}

int fsl_checkout_file_add( fsl_cx * f, fsl_checkout_file_add_opt * opt_ ){
  int rc = 0;
  CoAddState cas = CoAddState_empty;
  fsl_checkout_file_add_opt opt = *opt_
    /*use a copy in case the user manages to modify
      opt_ from a callback. */;
  if(!f) return FSL_RC_MISUSE;
  else if(!fsl_needs_checkout(f)) return FSL_RC_NOT_A_CHECKOUT;
  assert(f->ckout.rid>0);
  opt_->counts = fsl_checkout_file_add_opt_empty.counts;
  cas.absBuf = fsl_buffer_reuse(&f->scratch)
    /* Reminder: we can't reuse f->fsScratch here b/c it will be used by
       recursive calls we make. */;
  cas.coRelBuf = fsl_buffer_reuse(&f->fileContent);
  rc = fsl_file_canonical_name(opt.filename, cas.absBuf, false);
  if(!rc){
    cas.f = f;
    cas.opt = &opt;
    rc = co_add_one(&cas, false);
    opt_->counts = opt.counts;
  }
  fsl_buffer_reuse(cas.absBuf);
  fsl_buffer_reuse(cas.coRelBuf);
  return rc;
}

int fsl_checkout_file_rm( fsl_cx * f, bool relativeToCwd, char const * zFilename,
                          bool dirsRecursive ){
  int rc;
  fsl_db * db = fsl_needs_checkout(f);
  fsl_buffer fname = fsl_buffer_empty;
  char const * zNorm;
  char const * zCollate;
  fsl_id_t const vid = f ? f->ckout.rid : 0;
  if(!db) return FSL_RC_NOT_A_CHECKOUT;
  if(!zFilename || !*zFilename) return FSL_RC_MISUSE;
  else   assert(vid>=0);

  rc = fsl_checkout_filename_check(f, relativeToCwd, zFilename, &fname);
  if(rc) goto end;
  zNorm = fsl_buffer_cstr(&fname);

  /* MARKER(("fsl_checkout_file_rm(%d, %s) ==> %s\n", relativeToCwd, zFilename, zNorm)); */
  assert(zNorm);

  if(fname.used){
    /* Should this be done by fsl_checkout_filename_check()? */
    /* Trim trailing slashes... */
    char * tailCheck = fsl_buffer_str(&fname) + fname.used - 1;
    for( ; (tailCheck>zNorm) && ('/'==*tailCheck); --tailCheck){
      *tailCheck = 0;
      --fname.used;
    }
  }

  zCollate = fsl_cx_filename_collation(f);

  if(dirsRecursive){
    rc = fsl_db_exec(db,
                     "UPDATE vfile SET deleted=1 "
                     "WHERE vid=%"FSL_ID_T_PFMT
                     " AND NOT deleted"
                     " AND (pathname=%Q %s OR "
                     "     (pathname>'%q/' %s AND pathname<'%q0' %s))",
                     (fsl_id_t)vid,
                     zNorm, zCollate,
                     zNorm, zCollate, zNorm, zCollate);
  }else{
    rc = fsl_db_exec(db,
                     "UPDATE vfile SET deleted=1 "
                     "WHERE vid=%"FSL_ID_T_PFMT
                     " AND NOT deleted"
                     " AND pathname=%Q %s",
                     (fsl_id_t)vid,
                     zNorm, zCollate);
  }
  if(!rc){
    /* Remove ADDed-but-not-yet-committed entries... */
    rc = fsl_db_exec(db,
                     "DELETE FROM vfile WHERE vid=%"FSL_ID_T_PFMT
                     " AND rid=0 AND deleted",
                     (fsl_id_t)vid);
  }
  end:
  fsl_buffer_clear(&fname);
  if(rc){
    rc = fsl_cx_uplift_db_error2(f, db, rc);
  }
  return rc;
}

int fsl_checkout_changes_scan(fsl_cx * f){
  return fsl_vfile_changes_scan(f, -1, 0);
}

int fsl_reserved_fn_check(fsl_cx *f, const char *zPath,
                          fsl_int_t nPath){
  int rc = 0;
  if(nPath<0) nPath = (fsl_int_t)fsl_strlen(zPath);
  if(fsl_is_reserved_fn(zPath, nPath)){
    rc = fsl_cx_err_set(f, FSL_RC_MISUSE,
                        "Filename is reserved, not legal "
                        "for adding to a repository: %.*s",
                        (int)nPath, zPath);
  }
  return rc;
}

int fsl_checkout_install_schema(fsl_cx *f, bool dropIfExists){
  char const * tNames[] = {
  "vvar", "vfile", "vmerge", 0
  };
  int rc;
  fsl_db * const db = fsl_needs_checkout(f);
  if(!db) return f->error.code;
  if(dropIfExists){
    char const * t;
    int i;
    char const * dbName = fsl_db_role_label(FSL_DBROLE_CHECKOUT);
    for(i=0; 0!=(t = tNames[i]); ++i){
      rc = fsl_db_exec(db, "DROP TABLE IF EXISTS %s.%s",
                       dbName, t);
      if(rc) break;
    }
    if(!rc){
      rc = fsl_db_exec(db, "DROP TRIGGER IF EXISTS "
                       "%s.vmerge_ck1",
                       dbName);
    }
  }else{
    if(fsl_db_table_exists(db, FSL_DBROLE_CHECKOUT,
                           tNames[0])){
      return 0;
    }
  }
  rc = fsl_db_exec_multi(db, "%s", fsl_schema_checkout());
  return fsl_cx_uplift_db_error2(f, db, rc);
}

bool fsl_checkout_has_changes(fsl_cx *f){
  fsl_db * const db = fsl_cx_db_checkout(f);
  if(!db) return false;
  return fsl_db_exists(db,
                       "SELECT 1 FROM vfile WHERE chnged "
                       "OR coalesce(origname != pathname, 0)")
    || fsl_db_exists(db,"SELECT 1 FROM vmerge");
}



int fsl_checkout_clear_merge_state( fsl_cx *f ){
  fsl_db * d = fsl_needs_checkout(f);
  int rc;
  if(d){
    rc = fsl_db_exec(d,"DELETE FROM vmerge");
    rc = fsl_cx_uplift_db_error2(f, d, rc);
  }else{
    rc = FSL_RC_NOT_A_CHECKOUT;
  }
  return rc;
}

int fsl_checkout_clear_db(fsl_cx *f){
  fsl_db * const db = fsl_needs_checkout(f);
  if(!db) return f->error.code;
  return fsl_db_exec_multi(db,
                           "DELETE FROM vfile;"
                           "DELETE FROM vmerge;"
                           "DELETE FROM vvar WHERE name IN"
                           "('checkout','checkout-hash')");
}

int fsl_is_locally_modified(fsl_cx * f, const char * zFilename,
                            fsl_size_t origSize,
                            const char * zOrigHash,
                            fsl_int_t zOrigHashLen,
                            bool * isModified){
  int rc = 0;
  int const hashLen = zOrigHashLen>=0
    ? zOrigHashLen : fsl_is_uuid(zOrigHash);
  fsl_buffer hash = fsl_buffer_empty;
  fsl_buffer * fname = &f->fsScratch;
  fsl_fstat fst = fsl_fstat_empty;
  if(!fsl_is_uuid_len(hashLen)){
    return fsl_cx_err_set(f, FSL_RC_RANGE, "%s(): invalid hash length "
                          "%d for file: %s", __func__, hashLen, zFilename);
  }else if(!f->ckout.dir){
    return fsl_cx_err_set(f, FSL_RC_NOT_A_CHECKOUT,
                          "%s() requires a checkout.", __func__);
  }
  assert(!fname->used && "Internal misuse of fsl_cx::fsScratch");
  if(!fsl_is_absolute_path(zFilename)){
    fsl_buffer_reuse(fname);
    rc = fsl_file_canonical_name2(f->ckout.dir, zFilename, fname, false);
    if(rc) goto end;
    zFilename = fsl_buffer_cstr(fname);
  }
  rc = fsl_stat(zFilename, &fst, false);
  if(0==rc){
    if(origSize!=fst.size){
      *isModified = true;
      return 0;
    }
  }else{
    rc = fsl_cx_err_set(f, rc, "%s(): stat() failed for file: %s",
                        __func__, zFilename);
    goto end;
  }
  if(FSL_STRLEN_SHA1==hashLen){
    rc = fsl_sha1sum_filename(zFilename, &hash);
  }else if(FSL_STRLEN_K256==hashLen){
    rc = fsl_sha3sum_filename(zFilename, &hash);
  }else{
    assert(!"This \"cannot happen\".");
    return FSL_RC_UNSUPPORTED;
  }
  if(rc){
    rc = fsl_cx_err_set(f, rc, "%s: error hashing file: %s",
                        __func__, zFilename);
  }else{
    assert(hashLen==(int)hash.used);
    *isModified = memcmp(hash.mem, zOrigHash, (size_t)hashLen)
      ? true : false;
    /*MARKER(("%d: %s %s %s\n", *isModified, zOrigHash,
      (char const *)hash.mem, zFilename));*/
  }
  end:
  fsl_buffer_reuse(fname);
  fsl_buffer_clear(&hash);
  return rc;
}

#undef MARKER