Login
udf.c at [6ecdbab284]
Login

File src/udf.c artifact be8d16c183 part of check-in 6ecdbab284


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

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

  Heavily indebted to the Fossil SCM project (https://fossil-scm.org).
*/
/*************************************************************************
  This file houses fsl_cx-related sqlite3 User Defined Functions (UDFs).
*/
#include "fossil-scm/internal.h"
#if !defined(FSL_ENABLE_SQLITE_REGEXP)
#  define FSL_ENABLE_SQLITE_REGEXP 0
#endif
#if FSL_ENABLE_SQLITE_REGEXP
#  include "fossil-ext_regexp.h"
#endif
#include "sqlite3.h"
#include <assert.h>

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


/**
   SQL function for debugging.
  
   The print() function writes its arguments to fsl_output()
   if the bound fsl_cx->cxConfig.sqlPrint flag is true.
*/
static void fsl_db_sql_print(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  fsl_cx * f = (fsl_cx*)sqlite3_user_data(context);
  assert(f);
  if( f->cxConfig.sqlPrint ){
    int i;
    for(i=0; i<argc; i++){
      char c = i==argc-1 ? '\n' : ' ';
      fsl_outputf(f, "%s%c", sqlite3_value_text(argv[i]), c);
    }
  }
}

/**
   SQL function to return the number of seconds since 1970.  This is
   the same as strftime('%s','now') but is more compact.
*/
static void fsl_db_now_udf(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  sqlite3_result_int64(context, (sqlite3_int64)time(0));
}

/**
   SQL function to convert a Julian Day to a Unix timestamp.
*/
static void fsl_db_j2u_udf(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  double const jd = (double)sqlite3_value_double(argv[0]);
  sqlite3_result_int64(context, (sqlite3_int64)fsl_julian_to_unix(jd));
}

/**
   SQL function FSL_CKOUT_DIR([bool includeTrailingSlash=1]) returns
   the top-level checkout directory, optionally (by default) with a
   trailing slash. Returns NULL if the fsl_cx instance bound to
   sqlite3_user_data() has no checkout.
*/
static void fsl_db_cx_chkout_dir_udf(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  fsl_cx * f = (fsl_cx*)sqlite3_user_data(context);
  int const includeSlash = argc
    ? sqlite3_value_int(argv[0])
    : 1;
  if(f && f->ckout.dir && f->ckout.dirLen){
    sqlite3_result_text(context, f->ckout.dir,
                        (int)f->ckout.dirLen
                        - (includeSlash ? 0 : 1),
                        SQLITE_TRANSIENT);
  }else{
    sqlite3_result_null(context);
  }
}


/**
    SQL Function to return the check-in time for a file.
    Requires (vid,fid) RID arguments, as described for
    fsl_mtime_of_manifest_file().
 */
static void fsl_db_checkin_mtime_udf(
                                     sqlite3_context *context,
                                     int argc,
                                     sqlite3_value **argv
){
  fsl_cx * f = (fsl_cx*)sqlite3_user_data(context);
  fsl_time_t mtime = 0;
  int rc;
  fsl_id_t vid, fid;
  assert(f);
  vid = (fsl_id_t)sqlite3_value_int(argv[0]);
  fid = (fsl_id_t)sqlite3_value_int(argv[1]);
  rc = fsl_mtime_of_manifest_file(f, vid, fid, &mtime);
  if( rc==0 ){
    sqlite3_result_int64(context, mtime);
  }else{
    sqlite3_result_error(context, "fsl_mtime_of_manifest_file() failed", -1); 
  }
}


/**
   SQL UDF binding for fsl_content_get().

   FSL_CONTENT(RID INTEGER | SYMBOLIC_NAME) returns the
   undeltified/uncompressed content of the [blob] record identified by
   the given RID or symbolic name.
*/
static void fsl_db_content_udf(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  fsl_cx * const f = (fsl_cx*)sqlite3_user_data(context);
  fsl_id_t rid = 0;
  char const * arg;
  int rc;
  fsl_buffer b = fsl_buffer_empty;
  assert(f);
  if(1 != argc){
    sqlite3_result_error(context, "Expecting one argument", -1);
    return;
  }
  if(SQLITE_INTEGER==sqlite3_value_type(argv[0])){
    rid = (fsl_id_t)sqlite3_value_int64(argv[0]);
    arg = NULL;
  }else{
    arg = (const char*)sqlite3_value_text(argv[0]);
    if(!arg){
      sqlite3_result_error(context, "Invalid argument", -1);
      return;
    }
    rc = fsl_sym_to_rid(f, arg, FSL_SATYPE_ANY, &rid);
    if(rc) goto cx_err;
    else if(!rid){
      sqlite3_result_error(context, "No blob found", -1);
      return;
    }
  }
  rc = fsl_content_get(f, rid, &b);
  if(rc) goto cx_err;
  /* Curiously, i'm seeing no difference in allocation counts here whether
     we copy the blob here or pass off ownership... */
  sqlite3_result_blob(context, b.mem, (int)b.used, fsl_free);
  b = fsl_buffer_empty;
  return;
  cx_err:
  fsl_buffer_clear(&b);
  assert(f->error.msg.used);
  if(FSL_RC_OOM==rc){
    sqlite3_result_error_nomem(context);
  }else{
    assert(f->error.msg.used);
    sqlite3_result_error(context, (char const *)f->error.msg.mem,
                         (int)f->error.msg.used);
  }
}

/**
   SQL UDF FSL_SYM2RID(SYMBOLIC_NAME) resolves the name to a [blob].[rid]
   value or triggers an error if it cannot be resolved.
 */
static void fsl_db_sym2rid_udf(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  fsl_cx * const f = (fsl_cx*)sqlite3_user_data(context);
  char const * arg;
  assert(f);
  if(1 != argc){
    sqlite3_result_error(context, "Expecting one argument", -1);
    return;
  }
  arg = (const char*)sqlite3_value_text(argv[0]);
  if(!arg){
    sqlite3_result_error(context, "Expecting a STRING argument", -1);
  }else{
    fsl_id_t rid = 0;
    int const rc = fsl_sym_to_rid(f, arg, FSL_SATYPE_ANY, &rid);
    if(rc){
      if(FSL_RC_OOM==rc){
        sqlite3_result_error_nomem(context);
      }else{
        assert(f->error.msg.used);
        sqlite3_result_error(context, (char const *)f->error.msg.mem,
                             (int)f->error.msg.used);
      }
      fsl_cx_err_reset(f)
        /* This is arguable but keeps this error from poluting
           down-stream code (seen it happen in unit tests).  The irony
           is, it's very possible/likely that the error will propagate
           back up into f->error at some point.
        */;
    }else{
      assert(rid>0);
      sqlite3_result_int64(context, rid);
    }
  }
}

static void fsl_db_dirpart_udf(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  char const * arg;
  int rc;
  fsl_buffer b = fsl_buffer_empty;
  int fSlash = 0;
  if(argc<1 || argc>2){
    sqlite3_result_error(context,
                         "Expecting (string) or (string,bool) arguments",
                         -1);
    return;
  }
  arg = (const char*)sqlite3_value_text(argv[0]);
  if(!arg){
    sqlite3_result_error(context, "Invalid argument", -1);
    return;
  }
  if(argc>1){
    fSlash = sqlite3_value_int(argv[1]);
  }
  rc = fsl_file_dirpart(arg, -1, &b, fSlash ? 1 : 0);
  if(!rc){
    if(b.used && *b.mem){
#if 0
      sqlite3_result_text(context, (char const *)b.mem,
                          (int)b.used, SQLITE_TRANSIENT);
#else
      sqlite3_result_text(context, (char const *)b.mem,
                          (int)b.used, fsl_free);
      b = fsl_buffer_empty /* we passed ^^^^^ on ownership of b.mem */;
#endif
    }else{
      sqlite3_result_null(context);
    }
  }else{
    if(FSL_RC_OOM==rc){
      sqlite3_result_error_nomem(context);
    }else{
      sqlite3_result_error(context, "fsl_dirpart() failed!", -1);
    }
  }
  fsl_buffer_clear(&b);
}


/*
   Implement the user() SQL function.  user() takes no arguments and
   returns the user ID of the current user.
*/
static void fsl_db_user_udf(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  fsl_cx * f = (fsl_cx*)sqlite3_user_data(context);
  assert(f);
  if(f->repo.user){
    sqlite3_result_text(context, f->repo.user, -1, SQLITE_STATIC);
  }else{
    sqlite3_result_null(context);
  }
}

/**
   SQL function:

   fsl_is_enqueued(vfile.id)
   fsl_if_enqueued(vfile.id, X, Y)

   On the commit command, when filenames are specified (in order to do
   a partial commit) the vfile.id values for the named files are
   loaded into the fsl_cx state.  This function looks at that state to
   see if a file is named in that list.

   In the first form (1 argument) return TRUE if either no files are
   named (meaning that all changes are to be committed) or if id is
   found in the list.

   In the second form (3 arguments) return argument X if true and Y if
   false unless Y is NULL, in which case always return X.
*/
static void fsl_db_selected_for_checkin_udf(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  int rc = 0;
  fsl_cx * f = (fsl_cx*)sqlite3_user_data(context);
  fsl_id_bag * bag = &f->ckin.selectedIds;
  assert(argc==1 || argc==3);
  if( bag->entryCount ){
    fsl_id_t const iId = (fsl_id_t)sqlite3_value_int64(argv[0]);
    rc = iId ? (fsl_id_bag_contains(bag, iId) ? 1 : 0) : 0;
  }else{
    rc = 1;
  }
  if(1==argc){
    sqlite3_result_int(context, rc);
  }else{
    assert(3 == argc);
    assert( rc==0 || rc==1 );
    if( sqlite3_value_type(argv[2-rc])==SQLITE_NULL ) rc = 1-rc;
    sqlite3_result_value(context, argv[2-rc]);
  }
}

/**
   fsl_match_vfile_or_dir(p1,p2)

   A helper for resolving expressions like:

   WHERE pathname='X' C OR
      (pathname>'X/' C AND pathname<'X0' C)

   i.e. is 'X' a match for the LHS or is it a directory prefix of
   LHS?

   C = empty or COLLATE NOCASE, depending on the case-sensitivity
   setting of the fsl_cx instance associated with
   sqlite3_user_data(context). p1 is typically vfile.pathname or
   vfile.origname, and p2 is the string being compared against that.

   Resolves to NULL if either argument is NULL, 0 if the comparison
   shown above is false, 1 if the comparison is an exact match, or 2
   if p2 is a directory prefix part of p1.
*/
static void fsl_db_match_vfile_or_dir(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  fsl_cx * f = (fsl_cx*)sqlite3_user_data(context);
  char const * p1;
  char const * p2;
  fsl_buffer * b = 0;
  int rc = 0;
  assert(f);
  if(2 != argc){
    sqlite3_result_error(context, "Expecting two arguments", -1);
    return;
  }
  p1 = (const char*)sqlite3_value_text(argv[0]);
  p2 = (const char*)sqlite3_value_text(argv[1]);
  if(!p1 || !p2){
    sqlite3_result_null(context);
    return;
  }
  int (*cmp)(char const *, char const *) =
    f->cache.caseInsensitive ? fsl_stricmp : fsl_strcmp;
  if(0==cmp(p1, p2)){
    sqlite3_result_int(context, 1);
    return;
  }
  b = fsl__cx_scratchpad(f);
  rc = fsl_buffer_appendf(b, "%s/", p2);
  if(rc) goto oom;
  else if(cmp(p1, fsl_buffer_cstr(b))>0){
    b->mem[b->used-1] = '0';
    if(cmp(p1, fsl_buffer_cstr(b))<0)
    rc = 2;
  }
  assert(0==rc || 2==rc);
  sqlite3_result_int(context, rc);
  end:
  fsl__cx_scratchpad_yield(f, b);
  return;
  oom:
  sqlite3_result_error_nomem(context);
  goto end;
}

/**
   F(glob-list-name, filename)

   Returns 1 if the 2nd argument matches any glob in the fossil glob
   list named by the first argument. The first argument must be a name
   resolvable via fsl_glob_name_to_category() or an error is
   triggered. The second value is intended to be a string, but NULL is
   accepted (but never matches anything).

   If no match is found, 0 is returned. An empty glob list never matches
   anything.
*/
static void fsl_db_cx_glob_udf(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  fsl_cx * const f = (fsl_cx*)sqlite3_user_data(context);
  fsl_list * li = NULL;
  fsl_glob_category_e globType;
  char const * p1;
  char const * p2;
  p2 = (const char*)sqlite3_value_text(argv[1])/*value to check*/;
  if(NULL==p2 || 0==p2[0]){
    sqlite3_result_int(context, 0);
    return;
  }
  p1 = (const char*)sqlite3_value_text(argv[0])/*glob set name*/;
  globType  = fsl_glob_name_to_category(p1);
  if(FSL_GLOBS_INVALID==globType){
    char buf[100] = {0};
    buf[sizeof(buf)-1] = 0;
    fsl_snprintf(buf, (fsl_size_t)sizeof(buf)-1,
                 "Unknown glob pattern name: %#.*s",
                 50, p1 ? p1 : "NULL");
    sqlite3_result_error(context, buf, -1);
    return;
  }
  fsl_cx_glob_list(f, globType, &li, false);
  assert(li);
  sqlite3_result_int(context, fsl_glob_list_matches(li, p2) ? 1 : 0);
}


/**
   Plug in fsl_cx-specific db functionality into the given db handle.
   This must only be passed the MAIN db handle for the context.
*/
int fsl__cx_init_db(fsl_cx * const f, fsl_db * const db){
  int rc;
  assert(!f->dbMain);
  assert(db==&f->dbMem && "Currently the case - may change later.");
  if(f->cxConfig.traceSql){
    fsl_db_sqltrace_enable(db, stdout);
  }
  f->dbMain = db;
  db->role = FSL_DBROLE_MAIN;
  /* This all comes from db.c:db_open()... */
  /* FIXME: check result codes here. */
  sqlite3 * const dbh = db->dbh;
  sqlite3_busy_timeout(dbh, 5000 /* historical value */);
  sqlite3_wal_autocheckpoint(dbh, 1);  /* Set to checkpoint frequently */
  rc = fsl_cx_exec_multi(f,
                         "PRAGMA foreign_keys=OFF;"
                         // ^^^ vmerge table relies on this for its magical
                         // vmerge.id values.
                         "PRAGMA main.temp_store=FILE;"
                         "PRAGMA main.journal_mode=TRUNCATE;"
                         // ^^^ note that WAL is not possible on a TEMP db
                         // and OFF leads to undefined behaviour if
                         // ROLLBACK is used!
                         );
  if(rc) goto end;
  sqlite3_create_function(dbh, "now", 0, SQLITE_ANY, 0,
                          fsl_db_now_udf, 0, 0);
  sqlite3_create_function(dbh, "fsl_ci_mtime", 2,
                          SQLITE_ANY | SQLITE_DETERMINISTIC, f,
                          fsl_db_checkin_mtime_udf, 0, 0);
  sqlite3_create_function(dbh, "fsl_user", 0,
                          SQLITE_ANY | SQLITE_DETERMINISTIC, f,
                          fsl_db_user_udf, 0, 0);
  sqlite3_create_function(dbh, "fsl_print", -1,
                          SQLITE_UTF8
                          /* not strictly SQLITE_DETERMINISTIC
                             because it produces output */,
                          f, fsl_db_sql_print,0,0);
  sqlite3_create_function(dbh, "fsl_content", 1,
                          SQLITE_ANY | SQLITE_DETERMINISTIC, f,
                          fsl_db_content_udf, 0, 0);
  sqlite3_create_function(dbh, "fsl_sym2rid", 1,
                          SQLITE_ANY | SQLITE_DETERMINISTIC, f,
                          fsl_db_sym2rid_udf, 0, 0);
  sqlite3_create_function(dbh, "fsl_dirpart", 1,
                          SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL,
                          fsl_db_dirpart_udf, 0, 0);
  sqlite3_create_function(dbh, "fsl_dirpart", 2,
                          SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL,
                          fsl_db_dirpart_udf, 0, 0);
  sqlite3_create_function(dbh, "fsl_j2u", 1,
                          SQLITE_ANY | SQLITE_DETERMINISTIC, NULL,
                          fsl_db_j2u_udf, 0, 0);
  /*
    fsl_i[sf]_selected() both require access to the f's list of
    files being considered for commit.
  */
  sqlite3_create_function(dbh, "fsl_is_enqueued", 1, SQLITE_UTF8, f,
                          fsl_db_selected_for_checkin_udf,0,0 );
  sqlite3_create_function(dbh, "fsl_if_enqueued", 3, SQLITE_UTF8, f,
                          fsl_db_selected_for_checkin_udf,0,0 );

  sqlite3_create_function(dbh, "fsl_ckout_dir", -1,
                          SQLITE_UTF8 | SQLITE_DETERMINISTIC,
                          f, fsl_db_cx_chkout_dir_udf,0,0 );
  sqlite3_create_function(dbh, "fsl_match_vfile_or_dir", 2,
                          SQLITE_UTF8 | SQLITE_DETERMINISTIC,
                          f, fsl_db_match_vfile_or_dir,0,0 );
  sqlite3_create_function(dbh, "fsl_glob", 2,
                          SQLITE_UTF8 | SQLITE_DETERMINISTIC,
                          /* noting that ^^^^^ it's only deterministic
                             for a given statement execution IF no SQL
                             triggers an effect which forces the globs to
                             reload. That "shouldn't ever happen." */
                          f, fsl_db_cx_glob_udf, 0, 0 );

#if 0
  /* functions registered in v1 by db.c:db_open(). */
  /* porting cgi() requires access to the HTTP/CGI
     layer. i.e. this belongs downstream. */
  sqlite3_create_function(dbh, "cgi", 1, SQLITE_ANY, 0, db_sql_cgi, 0, 0);
  sqlite3_create_function(dbh, "cgi", 2, SQLITE_ANY, 0, db_sql_cgi, 0, 0);
  re_add_sql_func(db) /* Requires the regex bits. */;
#endif
  end:
  return rc;
}


#undef MARKER