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