diff options
Diffstat (limited to 'src/db.c')
| -rw-r--r-- | src/db.c | 5733 |
1 files changed, 5733 insertions, 0 deletions
diff --git a/src/db.c b/src/db.c new file mode 100644 index 0000000..43f6081 --- /dev/null +++ b/src/db.c @@ -0,0 +1,5733 @@ +/* +** Copyright (c) 2006 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/ +** +******************************************************************************* +** +** Code for interfacing to the various databases. +** +** There are three separate database files that fossil interacts +** with: +** +** (1) The "configdb" database in ~/.fossil or ~/.config/fossil.db +** or in %LOCALAPPDATA%/_fossil +** +** (2) The "repository" database +** +** (3) A local check-out database named "_FOSSIL_" or ".fslckout" +** and located at the root of the local copy of the source tree. +** +*/ +#include "config.h" +#if defined(_WIN32) +# if USE_SEE +# include <windows.h> +# define GETPID (int)GetCurrentProcessId +# endif +#else +# include <pwd.h> +# if USE_SEE +# define GETPID getpid +# endif +#endif +#if USE_SEE && !defined(SQLITE_HAS_CODEC) +# define SQLITE_HAS_CODEC +#endif +#if USE_SEE && defined(__linux__) +# include <sys/uio.h> +#endif +#include <sqlite3.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <time.h> + +/* BUGBUG: This (PID_T) does not work inside of INTERFACE block. */ +#if USE_SEE +#if defined(_WIN32) +typedef DWORD PID_T; +#else +typedef pid_t PID_T; +#endif +#endif + +#include "db.h" + +#if INTERFACE +/* +** Type definitions used for handling the saved encryption key for SEE. +*/ +#if !defined(_WIN32) +typedef void *LPVOID; +typedef size_t SIZE_T; +#endif + +/* +** Operations for db_maybe_handle_saved_encryption_key_for_process, et al. +*/ +#define SEE_KEY_READ ((int)0) +#define SEE_KEY_WRITE ((int)1) +#define SEE_KEY_ZERO ((int)2) + +/* +** An single SQL statement is represented as an instance of the following +** structure. +*/ +struct Stmt { + Blob sql; /* The SQL for this statement */ + sqlite3_stmt *pStmt; /* The results of sqlite3_prepare_v2() */ + Stmt *pNext, *pPrev; /* List of all unfinalized statements */ + int nStep; /* Number of sqlite3_step() calls */ + int rc; /* Error from db_vprepare() */ +}; + +/* +** Copy this to initialize a Stmt object to a clean/empty state. This +** is useful to help avoid assertions when performing cleanup in some +** error handling cases. +*/ +#define empty_Stmt_m {BLOB_INITIALIZER,NULL, NULL, NULL, 0, 0} +#endif /* INTERFACE */ +const struct Stmt empty_Stmt = empty_Stmt_m; + +/* +** Call this routine when a database error occurs. +** This routine throws a fatal error. It does not return. +*/ +static void db_err(const char *zFormat, ...){ + va_list ap; + char *z; + va_start(ap, zFormat); + z = vmprintf(zFormat, ap); + va_end(ap); +#ifdef FOSSIL_ENABLE_JSON + if( g.json.isJsonMode!=0 ){ + /* + ** Avoid calling into the JSON support subsystem if it + ** has not yet been initialized, e.g. early SQLite log + ** messages, etc. + */ + json_bootstrap_early(); + json_err( 0, z, 1 ); + } + else +#endif /* FOSSIL_ENABLE_JSON */ + if( g.xferPanic && g.cgiOutput==1 ){ + cgi_reset_content(); + @ error Database\serror:\s%F(z) + cgi_reply(); + } + if( strstr(z,"attempt to write a readonly database") ){ + static const char *azDbNames[] = { "repository", "localdb", "configdb" }; + int i; + for(i=0; i<3; i++){ + if( sqlite3_db_readonly(g.db, azDbNames[i])==1 ){ + z = mprintf("\"%s\" is readonly.\n%s", + sqlite3_db_filename(g.db,azDbNames[i]), z); + } + } + } + fossil_fatal("Database error: %s", z); +} + +/* +** Check a result code. If it is not SQLITE_OK, print the +** corresponding error message and exit. +*/ +static void db_check_result(int rc, Stmt *pStmt){ + if( rc!=SQLITE_OK ){ + db_err("SQL error (%d,%d: %s) while running [%s]", + rc, sqlite3_extended_errcode(g.db), + sqlite3_errmsg(g.db), blob_str(&pStmt->sql)); + } +} + +/* +** All static variable that a used by only this file are gathered into +** the following structure. +*/ +static struct DbLocalData { + unsigned protectMask; /* Prevent changes to database */ + int nBegin; /* Nesting depth of BEGIN */ + int doRollback; /* True to force a rollback */ + int nCommitHook; /* Number of commit hooks */ + int wrTxn; /* Outer-most TNX is a write */ + Stmt *pAllStmt; /* List of all unfinalized statements */ + int nPrepare; /* Number of calls to sqlite3_prepare_v2() */ + int nDeleteOnFail; /* Number of entries in azDeleteOnFail[] */ + struct sCommitHook { + int (*xHook)(void); /* Functions to call at db_end_transaction() */ + int sequence; /* Call functions in sequence order */ + } aHook[6]; + char *azDeleteOnFail[3]; /* Files to delete on a failure */ + char *azBeforeCommit[5]; /* Commands to run prior to COMMIT */ + int nBeforeCommit; /* Number of entries in azBeforeCommit */ + int nPriorChanges; /* sqlite3_total_changes() at transaction start */ + const char *zStartFile; /* File in which transaction was started */ + int iStartLine; /* Line of zStartFile where transaction started */ + int (*xAuth)(void*,int,const char*,const char*,const char*,const char*); + void *pAuthArg; /* Argument to the authorizer */ + const char *zAuthName; /* Name of the authorizer */ + int bProtectTriggers; /* True if protection triggers already exist */ + int nProtect; /* Slots of aProtect used */ + unsigned aProtect[12]; /* Saved values of protectMask */ + int pauseDmlLog; /* Ignore pDmlLog if positive */ + Blob *pDmlLog; /* Append DML statements here, of not NULL */ +} db = { + PROTECT_USER|PROTECT_CONFIG|PROTECT_BASELINE, /* protectMask */ + 0, 0, 0, 0, 0, 0, 0, {{0}}, {0}, {0}, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0}}; + +/* +** Arrange for the given file to be deleted on a failure. +*/ +void db_delete_on_failure(const char *zFilename){ + assert( db.nDeleteOnFail<count(db.azDeleteOnFail) ); + if( zFilename==0 ) return; + db.azDeleteOnFail[db.nDeleteOnFail++] = fossil_strdup(zFilename); +} + +/* +** Return the transaction nesting depth. 0 means we are currently +** not in a transaction. +*/ +int db_transaction_nesting_depth(void){ + return db.nBegin; +} + +/* +** Return a pointer to a string that is the code point where the +** current transaction was started. +*/ +char *db_transaction_start_point(void){ + return mprintf("%s:%d", db.zStartFile, db.iStartLine); +} + +/* +** This routine is called by the SQLite commit-hook mechanism +** just prior to each commit. All this routine does is verify +** that nBegin really is zero. That insures that transactions +** cannot commit by any means other than by calling db_end_transaction() +** below. +** +** This is just a safety and sanity check. +*/ +static int db_verify_at_commit(void *notUsed){ + if( db.nBegin ){ + fossil_panic("illegal commit attempt"); + return 1; + } + return 0; +} + +/* +** Silently add the filename and line number as parameter to each +** db_begin_transaction call. +*/ +#if INTERFACE +#define db_begin_transaction() db_begin_transaction_real(__FILE__,__LINE__) +#define db_begin_write() db_begin_write_real(__FILE__,__LINE__) +#define db_commit_transaction() db_end_transaction(0) +#define db_rollback_transaction() db_end_transaction(1) +#endif + +/* +** Begin a nested transaction +*/ +void db_begin_transaction_real(const char *zStartFile, int iStartLine){ + if( db.nBegin==0 ){ + db_multi_exec("BEGIN"); + sqlite3_commit_hook(g.db, db_verify_at_commit, 0); + db.nPriorChanges = sqlite3_total_changes(g.db); + db.doRollback = 0; + db.zStartFile = zStartFile; + db.iStartLine = iStartLine; + db.wrTxn = 0; + } + db.nBegin++; +} +/* +** Begin a new transaction for writing. +*/ +void db_begin_write_real(const char *zStartFile, int iStartLine){ + if( db.nBegin==0 ){ + if( !db_is_writeable("repository") ){ + db_multi_exec("BEGIN"); + }else{ + db_multi_exec("BEGIN IMMEDIATE"); + sqlite3_commit_hook(g.db, db_verify_at_commit, 0); + db.nPriorChanges = sqlite3_total_changes(g.db); + db.doRollback = 0; + db.zStartFile = zStartFile; + db.iStartLine = iStartLine; + db.wrTxn = 1; + } + }else if( !db.wrTxn ){ + fossil_warning("read txn at %s:%d might cause SQLITE_BUSY " + "for the write txn at %s:%d", + db.zStartFile, db.iStartLine, zStartFile, iStartLine); + } + db.nBegin++; +} + +/* End a transaction previously started using db_begin_transaction() +** or db_begin_write(). +*/ +void db_end_transaction(int rollbackFlag){ + if( g.db==0 ) return; + if( db.nBegin<=0 ){ + fossil_warning("Extra call to db_end_transaction"); + return; + } + if( rollbackFlag ){ + db.doRollback = 1; + if( g.fSqlTrace ) fossil_trace("-- ROLLBACK by request\n"); + } + db.nBegin--; + if( db.nBegin==0 ){ + int i; + if( db.doRollback==0 && db.nPriorChanges<sqlite3_total_changes(g.db) ){ + i = 0; + db_protect_only(PROTECT_SENSITIVE); + while( db.nBeforeCommit ){ + db.nBeforeCommit--; + sqlite3_exec(g.db, db.azBeforeCommit[i], 0, 0, 0); + sqlite3_free(db.azBeforeCommit[i]); + i++; + } + leaf_do_pending_checks(); + db_protect_pop(); + } + for(i=0; db.doRollback==0 && i<db.nCommitHook; i++){ + int rc = db.aHook[i].xHook(); + if( rc ){ + db.doRollback = 1; + if( g.fSqlTrace ) fossil_trace("-- ROLLBACK due to aHook[%d]\n", i); + } + } + while( db.pAllStmt ){ + db_finalize(db.pAllStmt); + } + db_multi_exec("%s", db.doRollback ? "ROLLBACK" : "COMMIT"); + db.doRollback = 0; + } +} + +/* +** Force a rollback and shutdown the database +*/ +void db_force_rollback(void){ + int i; + static int busy = 0; + sqlite3_stmt *pStmt = 0; + if( busy || g.db==0 ) return; + busy = 1; + undo_rollback(); + while( (pStmt = sqlite3_next_stmt(g.db,pStmt))!=0 ){ + sqlite3_reset(pStmt); + } + while( db.pAllStmt ){ + db_finalize(db.pAllStmt); + } + if( db.nBegin ){ + sqlite3_exec(g.db, "ROLLBACK", 0, 0, 0); + db.nBegin = 0; + } + busy = 0; + db_close(0); + for(i=0; i<db.nDeleteOnFail; i++){ + file_delete(db.azDeleteOnFail[i]); + } +} + +/* +** Install a commit hook. Hooks are installed in sequence order. +** It is an error to install the same commit hook more than once. +** +** Each commit hook is called (in order of ascending sequence) at +** each commit operation. If any commit hook returns non-zero, +** the subsequence commit hooks are omitted and the transaction +** rolls back rather than commit. It is the responsibility of the +** hooks themselves to issue any error messages. +*/ +void db_commit_hook(int (*x)(void), int sequence){ + int i; + assert( db.nCommitHook < count(db.aHook) ); + for(i=0; i<db.nCommitHook; i++){ + assert( x!=db.aHook[i].xHook ); + if( db.aHook[i].sequence>sequence ){ + int s = sequence; + int (*xS)(void) = x; + sequence = db.aHook[i].sequence; + x = db.aHook[i].xHook; + db.aHook[i].sequence = s; + db.aHook[i].xHook = xS; + } + } + db.aHook[db.nCommitHook].sequence = sequence; + db.aHook[db.nCommitHook].xHook = x; + db.nCommitHook++; +} + +#if INTERFACE +/* +** Flag bits for db_protect() and db_unprotect() indicating which parts +** of the databases should be write protected or write enabled, respectively. +*/ +#define PROTECT_USER 0x01 /* USER table */ +#define PROTECT_CONFIG 0x02 /* CONFIG and GLOBAL_CONFIG tables */ +#define PROTECT_SENSITIVE 0x04 /* Sensitive and/or global settings */ +#define PROTECT_READONLY 0x08 /* everything except TEMP tables */ +#define PROTECT_BASELINE 0x10 /* protection system is working */ +#define PROTECT_ALL 0x1f /* All of the above */ +#define PROTECT_NONE 0x00 /* Nothing. Everything is open */ +#endif /* INTERFACE */ + +/* +** Enable or disable database write protections. +** +** db_protext(X) Add protects on X +** db_unprotect(X) Remove protections on X +** db_protect_only(X) Remove all prior protections then set +** protections to only X. +** +** Each of these routines pushes the previous protection mask onto +** a finite-size stack. Each should be followed by a call to +** db_protect_pop() to pop the stack and restore the protections that +** existed prior to the call. The protection mask stack has a limited +** depth, so take care not to nest calls too deeply. +** +** About Database Write Protection +** ------------------------------- +** +** This is *not* a primary means of defending the application from +** attack. Fossil should be secure even if this mechanism is disabled. +** The purpose of database write protection is to provide an additional +** layer of defense in case SQL injection bugs somehow slip into other +** parts of the system. In other words, database write protection is +** not the primary defense but rather defense in depth. +** +** This mechanism mostly focuses on the USER table, to prevent an +** attacker from giving themselves Admin privilegs, and on the +** CONFIG table and especially "sensitive" settings such as +** "diff-command" or "editor" that if compromised by an attacker +** could lead to an RCE. +** +** By default, the USER and CONFIG tables are read-only. Various +** subsystems that legitimately need to change those tables can +** temporarily do so using: +** +** db_unprotect(PROTECT_xxx); +** // make the legitmate changes here +** db_protect_pop(); +** +** Code that runs inside of reduced protections should be carefully +** reviewed to ensure that it is harmless and not subject to SQL +** injection. +** +** Read-only operations (such as many web pages like /timeline) +** can invoke db_protect(PROTECT_ALL) to effectively make the database +** read-only. TEMP tables (which are often used for these kinds of +** pages) are still writable, however. +** +** The PROTECT_SENSITIVE protection is a subset of PROTECT_CONFIG +** that blocks changes to all of the global_config table, but only +** "sensitive" settings in the config table. PROTECT_SENSITIVE +** relies on triggers and the protected_setting() SQL function to +** prevent changes to sensitive settings. +** +** PROTECT_READONLY is set for any HTTP request for which the HTTP_REFERER +** is not the same origin. This is an additional defense against cross-site- +** scripting attacks. As with all of these defenses, this is only an extra +** backup layer. Fossil should be proof against XSS attacks even without this. +** +** Any violation of these security restrictions results in a SECURITY message +** in the server log (if enabled). A violation of any of these restrictions +** probably indicates a bug in Fossil and should be reported to the +** developers. +** +** Additional Notes +** ---------------- +** +** Calls to routines like db_set() and db_unset() temporarily disable +** the PROTECT_CONFIG protection. The assumption is that these calls +** cannot be invoked by an SQL injection and are thus safe. Make sure +** this is the case by always using a string literal as the name argument +** to db_set() and db_unset() and friend, not a variable that might +** be compromised by an attack. +*/ +void db_protect_only(unsigned flags){ + if( db.nProtect>=count(db.aProtect)-2 ){ + fossil_panic("too many db_protect() calls"); + } + db.aProtect[db.nProtect++] = db.protectMask; + if( (flags & PROTECT_SENSITIVE)!=0 + && db.bProtectTriggers==0 + && g.repositoryOpen + ){ + /* Create the triggers needed to protect sensitive settings from + ** being created or modified the first time that PROTECT_SENSITIVE + ** is enabled. Deleting a sensitive setting is harmless, so there + ** is not trigger to block deletes. After being created once, the + ** triggers persist for the life of the database connection. */ + unsigned savedProtectMask = db.protectMask; + db.protectMask = 0; + db_multi_exec( + "CREATE TEMP TRIGGER protect_1 BEFORE INSERT ON config" + " WHEN protected_setting(new.name) BEGIN" + " SELECT raise(abort,'not authorized');" + "END;\n" + "CREATE TEMP TRIGGER protect_2 BEFORE UPDATE ON config" + " WHEN protected_setting(new.name) BEGIN" + " SELECT raise(abort,'not authorized');" + "END;\n" + ); + db.bProtectTriggers = 1; + db.protectMask = savedProtectMask; + } + db.protectMask = flags; +} +void db_protect(unsigned flags){ + db_protect_only(db.protectMask | flags); +} +void db_unprotect(unsigned flags){ + if( db.nProtect>=count(db.aProtect)-2 ){ + fossil_panic("too many db_unprotect() calls"); + } + db.aProtect[db.nProtect++] = db.protectMask; + db.protectMask &= ~(flags|PROTECT_READONLY); +} +void db_protect_pop(void){ + if( db.nProtect<1 ){ + fossil_panic("too many db_protect_pop() calls"); + } + db.protectMask = db.aProtect[--db.nProtect]; +} +int db_is_protected(unsigned flags){ + return (db.protectMask & flags)!=0; +} + +/* +** Verify that the desired database write protections are in place. +** Throw a fatal error if not. +*/ +void db_assert_protected(unsigned flags){ + if( (flags & db.protectMask)!=flags ){ + fossil_fatal("missing database write protection bits: %02x", + flags & ~db.protectMask); + } +} + +/* +** Assert that either all protections are off (including PROTECT_BASELINE +** which is usually always enabled), or the setting named in the argument +** is no a sensitive setting. +** +** This assert() is used to verify that the db_set() and db_set_int() +** interfaces do not modify a sensitive setting. +*/ +void db_assert_protection_off_or_not_sensitive(const char *zName){ + if( db.protectMask!=0 && db_setting_is_protected(zName) ){ + fossil_panic("unauthorized change to protected setting \"%s\"", zName); + } +} + +/* +** Every Fossil database connection automatically registers the following +** overarching authenticator callback, and leaves it registered for the +** duration of the connection. This authenticator will call any +** sub-authenticators that are registered using db_set_authorizer(). +** +** == Testing Notes == +** +** Run Fossil as using a command like this: +** +** ./fossil sql --test --errorlog - +** +** Then enter SQL commands like one of these: +** +** SELECT db_protect('user'); +** SELECT db_protect('config'); +** SELECT db_protect('sensitive'); +** SELECT db_protect('readonly'); +** SELECT db_protect('all'); +** +** Then try to do SQL statements that would violate the constraints and +** verify that SECURITY warnings appear in the error log output. See +** also the sqlcmd_db_protect() function in sqlcmd.c. +*/ +int db_top_authorizer( + void *pNotUsed, + int eCode, + const char *z0, + const char *z1, + const char *z2, + const char *z3 +){ + int rc = SQLITE_OK; + switch( eCode ){ + case SQLITE_INSERT: + case SQLITE_UPDATE: + case SQLITE_DELETE: { + if( (db.protectMask & PROTECT_USER)!=0 + && sqlite3_stricmp(z0,"user")==0 ){ + fossil_errorlog( + "SECURITY: authorizer blocks DML on protected USER table\n"); + rc = SQLITE_DENY; + }else if( (db.protectMask & PROTECT_CONFIG)!=0 && + (sqlite3_stricmp(z0,"config")==0 || + sqlite3_stricmp(z0,"global_config")==0) ){ + fossil_errorlog( + "SECURITY: authorizer blocks DML on protected table \"%s\"\n", z0); + rc = SQLITE_DENY; + }else if( (db.protectMask & PROTECT_SENSITIVE)!=0 && + sqlite3_stricmp(z0,"global_config")==0 ){ + fossil_errorlog( + "SECURITY: authorizer blocks DML on protected GLOBAL_CONFIG table\n"); + rc = SQLITE_DENY; + }else if( (db.protectMask & PROTECT_READONLY)!=0 + && (sqlite3_stricmp(z2, "repository")==0 + || sqlite3_stricmp(z2,"configdb")==0 + || sqlite3_stricmp(z2,"localdb")==0) ){ + /* The READONLY constraint only applies to persistent database files. + ** "temp" and "mem1" and other transient databases are not + ** constrained by READONLY. */ + fossil_errorlog( + "SECURITY: authorizer blocks DML on table \"%s\" due to the " + "request coming from a different origin\n", z0); + rc = SQLITE_DENY; + } + break; + } + case SQLITE_DROP_TEMP_TRIGGER: { + /* Do not allow the triggers that enforce PROTECT_SENSITIVE + ** to be dropped */ + fossil_errorlog( + "SECURITY: authorizer blocks attempt to drop a temporary trigger\n"); + rc = SQLITE_DENY; + break; + } + } + if( db.xAuth && rc==SQLITE_OK ){ + rc = db.xAuth(db.pAuthArg, eCode, z0, z1, z2, z3); + } + return rc; +} + +/* +** Set or unset the query authorizer callback function +*/ +void db_set_authorizer( + int(*xAuth)(void*,int,const char*,const char*,const char*,const char*), + void *pArg, + const char *zName /* for tracing */ +){ + if( db.xAuth ){ + fossil_panic("multiple active db_set_authorizer() calls"); + } + db.xAuth = xAuth; + db.pAuthArg = pArg; + db.zAuthName = zName; + if( g.fSqlTrace ) fossil_trace("-- set authorizer %s\n", zName); +} +void db_clear_authorizer(void){ + if( db.zAuthName && g.fSqlTrace ){ + fossil_trace("-- discontinue authorizer %s\n", db.zAuthName); + } + db.xAuth = 0; + db.pAuthArg = 0; + db.zAuthName = 0; +} + +#if INTERFACE +/* +** Possible flags to db_vprepare +*/ +#define DB_PREPARE_IGNORE_ERROR 0x001 /* Suppress errors */ +#define DB_PREPARE_PERSISTENT 0x002 /* Stmt will stick around for a while */ +#endif + +/* +** If zSql is a DML statement, append it db.pDmlLog. +*/ +static void db_append_dml(const char *zSql){ + size_t nSql; + if( db.pDmlLog==0 ) return; + if( db.pauseDmlLog ) return; + if( zSql==0 ) return; + nSql = strlen(zSql); + while( nSql>0 && fossil_isspace(zSql[0]) ){ nSql--; zSql++; } + while( nSql>0 && fossil_isspace(zSql[nSql-1]) ) nSql--; + if( nSql<6 ) return; + if( fossil_strnicmp(zSql, "SELECT", 6)==0 ) return; + if( fossil_strnicmp(zSql, "PRAGMA", 6)==0 ) return; + blob_append(db.pDmlLog, zSql, nSql); + if( zSql[nSql-1]!=';' ) blob_append_char(db.pDmlLog, ';'); + blob_append_char(db.pDmlLog, '\n'); +} + +/* +** Set the Blob to which DML statement text should be appended. Set it +** to zero to stop appending DML statement text. +*/ +void db_append_dml_to_blob(Blob *pBlob){ + db.pDmlLog = pBlob; +} + +/* +** Pause or unpause the DML log +*/ +void db_pause_dml_log(void){ db.pauseDmlLog++; } +void db_unpause_dml_log(void){ db.pauseDmlLog--; } + +/* +** Prepare a Stmt. Assume that the Stmt is previously uninitialized. +** If the input string contains multiple SQL statements, only the first +** one is processed. All statements beyond the first are silently ignored. +*/ +int db_vprepare(Stmt *pStmt, int flags, const char *zFormat, va_list ap){ + int rc; + int prepFlags = 0; + char *zSql; + const char *zExtra = 0; + blob_zero(&pStmt->sql); + blob_vappendf(&pStmt->sql, zFormat, ap); + va_end(ap); + zSql = blob_str(&pStmt->sql); + db.nPrepare++; + db_append_dml(zSql); + if( flags & DB_PREPARE_PERSISTENT ){ + prepFlags = SQLITE_PREPARE_PERSISTENT; + } + rc = sqlite3_prepare_v3(g.db, zSql, -1, prepFlags, &pStmt->pStmt, &zExtra); + if( rc!=0 && (flags & DB_PREPARE_IGNORE_ERROR)==0 ){ + db_err("%s\n%s", sqlite3_errmsg(g.db), zSql); + }else if( zExtra && !fossil_all_whitespace(zExtra) ){ + db_err("surplus text follows SQL: \"%s\"", zExtra); + } + pStmt->pNext = db.pAllStmt; + pStmt->pPrev = 0; + if( db.pAllStmt ) db.pAllStmt->pPrev = pStmt; + db.pAllStmt = pStmt; + pStmt->nStep = 0; + pStmt->rc = rc; + return rc; +} +int db_prepare(Stmt *pStmt, const char *zFormat, ...){ + int rc; + va_list ap; + va_start(ap, zFormat); + rc = db_vprepare(pStmt, 0, zFormat, ap); + va_end(ap); + return rc; +} +int db_prepare_ignore_error(Stmt *pStmt, const char *zFormat, ...){ + int rc; + va_list ap; + va_start(ap, zFormat); + rc = db_vprepare(pStmt, DB_PREPARE_IGNORE_ERROR, zFormat, ap); + va_end(ap); + return rc; +} + +/* This variant of db_prepare() checks to see if the statement has +** already been prepared, and if it has it becomes a no-op. +*/ +int db_static_prepare(Stmt *pStmt, const char *zFormat, ...){ + int rc = SQLITE_OK; + if( blob_size(&pStmt->sql)==0 ){ + va_list ap; + va_start(ap, zFormat); + rc = db_vprepare(pStmt, DB_PREPARE_PERSISTENT, zFormat, ap); + va_end(ap); + } + return rc; +} + +/* Return TRUE if static Stmt object pStmt has been initialized. +*/ +int db_static_stmt_is_init(Stmt *pStmt){ + return blob_size(&pStmt->sql)>0; +} + +/* Prepare a statement using text placed inside a Blob +** using blob_append_sql(). +*/ +int db_prepare_blob(Stmt *pStmt, Blob *pSql){ + int rc; + char *zSql; + pStmt->sql = *pSql; + blob_init(pSql, 0, 0); + zSql = blob_sql_text(&pStmt->sql); + db.nPrepare++; + rc = sqlite3_prepare_v3(g.db, zSql, -1, 0, &pStmt->pStmt, 0); + if( rc!=0 ){ + db_err("%s\n%s", sqlite3_errmsg(g.db), zSql); + } + pStmt->pNext = pStmt->pPrev = 0; + pStmt->nStep = 0; + pStmt->rc = rc; + return rc; +} + + +/* +** Return the index of a bind parameter +*/ +static int paramIdx(Stmt *pStmt, const char *zParamName){ + int i = sqlite3_bind_parameter_index(pStmt->pStmt, zParamName); + if( i==0 ){ + db_err("no such bind parameter: %s\nSQL: %b", zParamName, &pStmt->sql); + } + return i; +} +/* +** Bind an integer, string, or Blob value to a named parameter. +*/ +int db_bind_int(Stmt *pStmt, const char *zParamName, int iValue){ + return sqlite3_bind_int(pStmt->pStmt, paramIdx(pStmt, zParamName), iValue); +} +int db_bind_int64(Stmt *pStmt, const char *zParamName, i64 iValue){ + return sqlite3_bind_int64(pStmt->pStmt, paramIdx(pStmt, zParamName), iValue); +} +int db_bind_double(Stmt *pStmt, const char *zParamName, double rValue){ + return sqlite3_bind_double(pStmt->pStmt, paramIdx(pStmt, zParamName), rValue); +} +int db_bind_text(Stmt *pStmt, const char *zParamName, const char *zValue){ + return sqlite3_bind_text(pStmt->pStmt, paramIdx(pStmt, zParamName), zValue, + -1, SQLITE_STATIC); +} +int db_bind_text16(Stmt *pStmt, const char *zParamName, const char *zValue){ + return sqlite3_bind_text16(pStmt->pStmt, paramIdx(pStmt, zParamName), zValue, + -1, SQLITE_STATIC); +} +int db_bind_null(Stmt *pStmt, const char *zParamName){ + return sqlite3_bind_null(pStmt->pStmt, paramIdx(pStmt, zParamName)); +} +int db_bind_blob(Stmt *pStmt, const char *zParamName, Blob *pBlob){ + return sqlite3_bind_blob(pStmt->pStmt, paramIdx(pStmt, zParamName), + blob_buffer(pBlob), blob_size(pBlob), SQLITE_STATIC); +} + +/* bind_str() treats a Blob object like a TEXT string and binds it +** to the SQL variable. Contrast this to bind_blob() which treats +** the Blob object like an SQL BLOB. +*/ +int db_bind_str(Stmt *pStmt, const char *zParamName, Blob *pBlob){ + return sqlite3_bind_text(pStmt->pStmt, paramIdx(pStmt, zParamName), + blob_buffer(pBlob), blob_size(pBlob), SQLITE_STATIC); +} + +/* +** Step the SQL statement. Return either SQLITE_ROW or an error code +** or SQLITE_OK if the statement finishes successfully. +*/ +int db_step(Stmt *pStmt){ + int rc; + if( pStmt->pStmt==0 ) return pStmt->rc; + rc = sqlite3_step(pStmt->pStmt); + pStmt->nStep++; + return rc; +} + +/* +** Print warnings if a query is inefficient. +*/ +static void db_stats(Stmt *pStmt){ +#ifdef FOSSIL_DEBUG + int c1, c2, c3; + const char *zSql = sqlite3_sql(pStmt->pStmt); + if( zSql==0 ) return; + c1 = sqlite3_stmt_status(pStmt->pStmt, SQLITE_STMTSTATUS_FULLSCAN_STEP, 1); + c2 = sqlite3_stmt_status(pStmt->pStmt, SQLITE_STMTSTATUS_AUTOINDEX, 1); + c3 = sqlite3_stmt_status(pStmt->pStmt, SQLITE_STMTSTATUS_SORT, 1); + if( c1>pStmt->nStep*4 && strstr(zSql,"/*scan*/")==0 ){ + fossil_warning("%d scan steps for %d rows in [%s]", c1, pStmt->nStep, zSql); + }else if( c2 ){ + fossil_warning("%d automatic index rows in [%s]", c2, zSql); + }else if( c3 && strstr(zSql,"/*sort*/")==0 && strstr(zSql,"/*scan*/")==0 ){ + fossil_warning("sort w/o index in [%s]", zSql); + } + pStmt->nStep = 0; +#endif +} + +/* +** Reset or finalize a statement. +*/ +int db_reset(Stmt *pStmt){ + int rc; + if( g.fSqlStats ){ db_stats(pStmt); } + rc = sqlite3_reset(pStmt->pStmt); + db_check_result(rc, pStmt); + return rc; +} +int db_finalize(Stmt *pStmt){ + int rc; + if( pStmt->pNext ){ + pStmt->pNext->pPrev = pStmt->pPrev; + } + if( pStmt->pPrev ){ + pStmt->pPrev->pNext = pStmt->pNext; + }else if( db.pAllStmt==pStmt ){ + db.pAllStmt = pStmt->pNext; + } + pStmt->pNext = 0; + pStmt->pPrev = 0; + if( g.fSqlStats ){ db_stats(pStmt); } + blob_reset(&pStmt->sql); + rc = sqlite3_finalize(pStmt->pStmt); + db_check_result(rc, pStmt); + pStmt->pStmt = 0; + return rc; +} + +/* +** Return the rowid of the most recent insert +*/ +int db_last_insert_rowid(void){ + i64 x = sqlite3_last_insert_rowid(g.db); + if( x<0 || x>(i64)2147483647 ){ + fossil_panic("rowid out of range (0..2147483647)"); + } + return (int)x; +} + +/* +** Return the number of rows that were changed by the most recent +** INSERT, UPDATE, or DELETE. Auxiliary changes caused by triggers +** or other side effects are not counted. +*/ +int db_changes(void){ + return sqlite3_changes(g.db); +} + +/* +** Extract text, integer, or blob values from the N-th column of the +** current row. +*/ +int db_column_type(Stmt *pStmt, int N){ + return sqlite3_column_type(pStmt->pStmt, N); +} +int db_column_bytes(Stmt *pStmt, int N){ + return sqlite3_column_bytes(pStmt->pStmt, N); +} +int db_column_int(Stmt *pStmt, int N){ + return sqlite3_column_int(pStmt->pStmt, N); +} +i64 db_column_int64(Stmt *pStmt, int N){ + return sqlite3_column_int64(pStmt->pStmt, N); +} +double db_column_double(Stmt *pStmt, int N){ + return sqlite3_column_double(pStmt->pStmt, N); +} +const char *db_column_text(Stmt *pStmt, int N){ + return (char*)sqlite3_column_text(pStmt->pStmt, N); +} +const char *db_column_raw(Stmt *pStmt, int N){ + return (const char*)sqlite3_column_blob(pStmt->pStmt, N); +} +const char *db_column_name(Stmt *pStmt, int N){ + return (char*)sqlite3_column_name(pStmt->pStmt, N); +} +int db_column_count(Stmt *pStmt){ + return sqlite3_column_count(pStmt->pStmt); +} +char *db_column_malloc(Stmt *pStmt, int N){ + return fossil_strdup_nn(db_column_text(pStmt, N)); +} +void db_column_blob(Stmt *pStmt, int N, Blob *pBlob){ + blob_append(pBlob, sqlite3_column_blob(pStmt->pStmt, N), + sqlite3_column_bytes(pStmt->pStmt, N)); +} +Blob db_column_text_as_blob(Stmt *pStmt, int N){ + Blob x; + blob_init(&x, (char*)sqlite3_column_text(pStmt->pStmt,N), + sqlite3_column_bytes(pStmt->pStmt,N)); + return x; +} + +/* +** Initialize a blob to an ephemeral copy of the content of a +** column in the current row. The data in the blob will become +** invalid when the statement is stepped or reset. +*/ +void db_ephemeral_blob(Stmt *pStmt, int N, Blob *pBlob){ + blob_init(pBlob, sqlite3_column_blob(pStmt->pStmt, N), + sqlite3_column_bytes(pStmt->pStmt, N)); +} + +/* +** Execute a single prepared statement until it finishes. +*/ +int db_exec(Stmt *pStmt){ + int rc; + while( (rc = db_step(pStmt))==SQLITE_ROW ){} + rc = db_reset(pStmt); + db_check_result(rc, pStmt); + return rc; +} + +/* +** COMMAND: test-db-exec-error +** Usage: %fossil test-db-exec-error +** +** Invoke the db_exec() interface with an erroneous SQL statement +** in order to verify the error handling logic. +*/ +void db_test_db_exec_cmd(void){ + Stmt err; + db_find_and_open_repository(0,0); + db_prepare(&err, "INSERT INTO repository.config(name) VALUES(NULL);"); + db_exec(&err); +} + +/* +** COMMAND: test-db-prepare +** Usage: %fossil test-db-prepare ?OPTIONS? SQL-STATEMENT +** +** Options: +** --auth-report Enable the ticket report query authorizer +** --auth-ticket Enable the ticket schema query authorizer +** +** Invoke db_prepare() on the SQL input. Report any errors encountered. +** This command is used to verify error detection logic in the db_prepare() +** utility routine. +*/ +void db_test_db_prepare(void){ + const int fAuthReport = find_option("auth-report",0,0)!=0; + const int fAuthSchema = find_option("auth-ticket",0,0)!=0; + char * zReportErr = 0; /* auth-report error string. */ + int nSchemaErr = 0; /* Number of auth-ticket errors. */ + Stmt err; + + if(fAuthReport + fAuthSchema > 1){ + fossil_fatal("Only one of --auth-report or --auth-ticket " + "may be used."); + } + db_find_and_open_repository(0,0); + verify_all_options(); + if( g.argc!=3 ) usage("?OPTIONS? SQL"); + if(fAuthReport){ + report_restrict_sql(&zReportErr); + }else if(fAuthSchema){ + ticket_restrict_sql(&nSchemaErr); + } + db_prepare(&err, "%s", g.argv[2]/*safe-for-%s*/); + db_finalize(&err); + if(fAuthReport){ + report_unrestrict_sql(); + if(zReportErr){ + fossil_warning("Report authorizer error: %s\n", zReportErr); + fossil_free(zReportErr); + } + }else if(fAuthSchema){ + ticket_unrestrict_sql(); + if(nSchemaErr){ + fossil_warning("Ticket schema authorizer error count: %d\n", + nSchemaErr); + } + } +} + +/* +** Print the output of one or more SQL queries on standard output. +** This routine is used for debugging purposes only. +*/ +int db_debug(const char *zSql, ...){ + Blob sql; + int rc = SQLITE_OK; + va_list ap; + const char *z, *zEnd; + sqlite3_stmt *pStmt; + blob_init(&sql, 0, 0); + va_start(ap, zSql); + blob_vappendf(&sql, zSql, ap); + va_end(ap); + z = blob_str(&sql); + while( rc==SQLITE_OK && z[0] ){ + pStmt = 0; + rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd); + if( rc!=SQLITE_OK ) break; + if( pStmt ){ + int nRow = 0; + db.nPrepare++; + while( sqlite3_step(pStmt)==SQLITE_ROW ){ + int i, n; + if( nRow++ > 0 ) fossil_print("\n"); + n = sqlite3_column_count(pStmt); + for(i=0; i<n; i++){ + fossil_print("%s = %s\n", sqlite3_column_name(pStmt, i), + sqlite3_column_text(pStmt,i)); + } + } + rc = sqlite3_finalize(pStmt); + if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z); + } + z = zEnd; + } + blob_reset(&sql); + return rc; +} + +/* +** Execute multiple SQL statements. The input text is executed +** directly without any formatting. +*/ +int db_exec_sql(const char *z){ + int rc = SQLITE_OK; + sqlite3_stmt *pStmt; + const char *zEnd; + while( rc==SQLITE_OK && z[0] ){ + pStmt = 0; + rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd); + if( rc ){ + db_err("%s: {%s}", sqlite3_errmsg(g.db), z); + }else if( pStmt ){ + db.nPrepare++; + db_append_dml(sqlite3_sql(pStmt)); + while( sqlite3_step(pStmt)==SQLITE_ROW ){} + rc = sqlite3_finalize(pStmt); + if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z); + } + z = zEnd; + } + return rc; +} + +/* +** Execute multiple SQL statements using printf-style formatting. +*/ +int db_multi_exec(const char *zSql, ...){ + Blob sql; + int rc; + va_list ap; + + blob_init(&sql, 0, 0); + va_start(ap, zSql); + blob_vappendf(&sql, zSql, ap); + va_end(ap); + rc = db_exec_sql(blob_str(&sql)); + blob_reset(&sql); + return rc; +} + +/* +** Optionally make the following changes to the database if feasible and +** convenient. Do not start a transaction for these changes, but only +** make these changes if other changes are also being made. +*/ +void db_optional_sql(const char *zDb, const char *zSql, ...){ + if( db_is_writeable(zDb) && db.nBeforeCommit < count(db.azBeforeCommit) ){ + va_list ap; + va_start(ap, zSql); + db.azBeforeCommit[db.nBeforeCommit++] = sqlite3_vmprintf(zSql, ap); + va_end(ap); + } +} + +/* +** Execute a query and return a single integer value. +*/ +i64 db_int64(i64 iDflt, const char *zSql, ...){ + va_list ap; + Stmt s; + i64 rc; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)!=SQLITE_ROW ){ + rc = iDflt; + }else{ + rc = db_column_int64(&s, 0); + } + db_finalize(&s); + return rc; +} +int db_int(int iDflt, const char *zSql, ...){ + va_list ap; + Stmt s; + int rc; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)!=SQLITE_ROW ){ + rc = iDflt; + }else{ + rc = db_column_int(&s, 0); + } + db_finalize(&s); + return rc; +} + +/* +** Return TRUE if the query would return 1 or more rows. Return +** FALSE if the query result would be an empty set. +*/ +int db_exists(const char *zSql, ...){ + va_list ap; + Stmt s; + int rc; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)!=SQLITE_ROW ){ + rc = 0; + }else{ + rc = 1; + } + db_finalize(&s); + return rc; +} + + +/* +** Execute a query and return a floating-point value. +*/ +double db_double(double rDflt, const char *zSql, ...){ + va_list ap; + Stmt s; + double r; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)!=SQLITE_ROW ){ + r = rDflt; + }else{ + r = db_column_double(&s, 0); + } + db_finalize(&s); + return r; +} + +/* +** Execute a query and append the first column of the first row +** of the result set to blob given in the first argument. +*/ +void db_blob(Blob *pResult, const char *zSql, ...){ + va_list ap; + Stmt s; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)==SQLITE_ROW ){ + blob_append(pResult, sqlite3_column_blob(s.pStmt, 0), + sqlite3_column_bytes(s.pStmt, 0)); + } + db_finalize(&s); +} + +/* +** Execute a query. Return the first column of the first row +** of the result set as a string. Space to hold the string is +** obtained from fossil_strdup() and should be freed using fossil_free(). +** If the result set is empty, return a copy of zDefault instead. +*/ +char *db_text(const char *zDefault, const char *zSql, ...){ + va_list ap; + Stmt s; + char *z; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)==SQLITE_ROW ){ + z = fossil_strdup_nn((const char*)sqlite3_column_text(s.pStmt, 0)); + }else{ + z = fossil_strdup(zDefault); + } + db_finalize(&s); + return z; +} + +/* +** Initialize a new database file with the given schema. If anything +** goes wrong, call db_err() to exit. +** +** If zFilename is NULL, then create an empty repository in an in-memory +** database. +*/ +void db_init_database( + const char *zFileName, /* Name of database file to create */ + const char *zSchema, /* First part of schema */ + ... /* Additional SQL to run. Terminate with NULL. */ +){ + sqlite3 *xdb; + int rc; + const char *zSql; + va_list ap; + + xdb = db_open(zFileName ? zFileName : ":memory:"); + sqlite3_exec(xdb, "BEGIN EXCLUSIVE", 0, 0, 0); + rc = sqlite3_exec(xdb, zSchema, 0, 0, 0); + if( rc!=SQLITE_OK ){ + db_err("%s", sqlite3_errmsg(xdb)); + } + va_start(ap, zSchema); + while( (zSql = va_arg(ap, const char*))!=0 ){ + rc = sqlite3_exec(xdb, zSql, 0, 0, 0); + if( rc!=SQLITE_OK ){ + db_err("%s", sqlite3_errmsg(xdb)); + } + } + va_end(ap); + sqlite3_exec(xdb, "COMMIT", 0, 0, 0); + if( zFileName || g.db!=0 ){ + sqlite3_close(xdb); + }else{ + g.db = xdb; + } +} + +/* +** Function to return the number of seconds since 1970. This is +** the same as strftime('%s','now') but is more compact. +*/ +void db_now_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + sqlite3_result_int64(context, time(0)); +} + +/* +** Function to return the check-in time for a file. +** +** checkin_mtime(CKINID,RID) +** +** CKINID: The RID for the manifest for a check-in. +** RID: The RID of a file in CKINID for which the check-in time +** is desired. +** +** Returns: The check-in time in seconds since 1970. +*/ +void db_checkin_mtime_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + i64 mtime; + int rc = mtime_of_manifest_file(sqlite3_value_int(argv[0]), + sqlite3_value_int(argv[1]), &mtime); + if( rc==0 ){ + sqlite3_result_int64(context, mtime); + } +} + +/* +** SQL wrapper around the symbolic_name_to_rid() C-language API. +** Examples: +** +** symbolic_name_to_rid('trunk'); +** symbolic_name_to_rid('trunk','w'); +** +** Its result is either an RID or NULL. +*/ +void db_sym2rid_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char *arg; + const char *type; + if(1 != argc && 2 != argc){ + sqlite3_result_error(context, "Expecting one or two arguments", -1); + return; + } + arg = (const char*)sqlite3_value_text(argv[0]); + if(!arg){ + sqlite3_result_error(context, "Expecting a STRING argument", -1); + }else{ + int rid; + type = (2==argc) ? (const char*)sqlite3_value_text(argv[1]) : 0; + if(!type) type = "ci"; + rid = symbolic_name_to_rid( arg, type ); + if(rid<0){ + sqlite3_result_error(context, "Symbolic name is ambiguous.", -1); + }else if(0==rid){ + sqlite3_result_null(context); + }else{ + sqlite3_result_int64(context, rid); + } + } +} + + +/* +** SETTING: timeline-utc boolean default=on +** +** If the timeline-utc setting is true, then Fossil tries to understand +** and display all time values using UTC. If this setting is false, Fossil +** tries to understand and display time values using the local timezone. +** +** The word "timeline" in the name of this setting is historical. +** This setting applies to all user interfaces of Fossil, +** not just the timeline. +** +** Note that when accessing Fossil using the web interface, the localtime +** used is the localtime on the server, not on the client. +*/ +/* +** Return true if Fossil is set to display times as UTC. Return false +** if it wants to display times using the local timezone. +** +** False is returned if display is set to localtime even if the localtime +** happens to be the same as UTC. +*/ +int fossil_ui_utctime(void){ + if( g.fTimeFormat==0 ){ + if( db_get_int("timeline-utc", 1) ){ + g.fTimeFormat = 1; /* UTC */ + }else{ + g.fTimeFormat = 2; /* Localtime */ + } + } + return g.fTimeFormat==1; +} + +/* +** Return true if Fossil is set to display times using the local timezone. +*/ +int fossil_ui_localtime(void){ + return fossil_ui_utctime()==0; +} + +/* +** The toLocal() SQL function returns a string that is an argument to a +** date/time function that is appropriate for modifying the time for display. +** If UTC time display is selected, no modification occurs. If local time +** display is selected, the time is adjusted appropriately. +** +** Example usage: +** +** SELECT datetime('now',toLocal()); +*/ +void db_tolocal_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + if( fossil_ui_utctime() ){ + sqlite3_result_text(context, "0 seconds", -1, SQLITE_STATIC); + }else{ + sqlite3_result_text(context, "localtime", -1, SQLITE_STATIC); + } +} + +/* +** The fromLocal() SQL function returns a string that is an argument to a +** date/time function that is appropriate to convert an input time to UTC. +** If UTC time display is selected, no modification occurs. If local time +** display is selected, the time is adjusted from local to UTC. +** +** Example usage: +** +** SELECT julianday(:user_input,fromLocal()); +*/ +void db_fromlocal_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + if( fossil_ui_utctime() ){ + sqlite3_result_text(context, "0 seconds", -1, SQLITE_STATIC); + }else{ + sqlite3_result_text(context, "utc", -1, SQLITE_STATIC); + } +} + +/* +** If the input is a hexadecimal string, convert that string into a BLOB. +** If the input is not a hexadecimal string, return NULL. +*/ +void db_hextoblob( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const unsigned char *zIn = sqlite3_value_text(argv[0]); + int nIn = sqlite3_value_bytes(argv[0]); + unsigned char *zOut; + if( zIn==0 ) return; + if( nIn&1 ) return; + if( !validate16((const char*)zIn, nIn) ) return; + zOut = sqlite3_malloc64( nIn/2 + 1 ); + if( zOut==0 ){ + sqlite3_result_error_nomem(context); + return; + } + decode16(zIn, zOut, nIn); + sqlite3_result_blob(context, zOut, nIn/2, sqlite3_free); +} + +/* +** Return the XOR-obscured version of the input text. Useful for +** updating authentication strings in Fossil settings. To change +** the password locally stored for sync, for instance: +** +** echo "UPDATE config +** SET value = obscure('monkey123') +** WHERE name = 'last-sync-pw'" | +** fossil sql +** +** Note that user.pw uses a different obscuration algorithm, but +** you don't need to use 'fossil sql' for that anyway. Just call +** +** fossil user pass monkey123 +** +** to change the local user entry's password in the same way. +** +** 2022-12-30: If the user-data pointer is not NULL, then operate +** as unobscure() rather than obscure(). The obscure() variant of +** this routine is commonly available. But unobscure is (currently) +** only registered by the "fossil remote config-data --show-passwords" +** command. +*/ +void db_obscure( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const unsigned char *zIn = sqlite3_value_text(argv[0]); + int nIn = sqlite3_value_bytes(argv[0]); + char *zOut, *zTemp; + if( 0==zIn ) return; + if( 0==(zOut = sqlite3_malloc64( nIn * 2 + 3 )) ){ + sqlite3_result_error_nomem(context); + return; + } + if( sqlite3_user_data(context)==0 ){ + zTemp = obscure((char*)zIn); + }else{ + zTemp = unobscure((char*)zIn); + } + fossil_strcpy(zOut, zTemp); + fossil_free(zTemp); + sqlite3_result_text(context, zOut, strlen(zOut), sqlite3_free); +} + +/* +** Return True if zName is a protected (a.k.a. "sensitive") setting. +*/ +int db_setting_is_protected(const char *zName){ + const Setting *pSetting = zName ? db_find_setting(zName,0) : 0; + return pSetting!=0 && pSetting->sensitive!=0; +} + +/* +** Implement the protected_setting(X) SQL function. This function returns +** true if X is the name of a protected (security-sensitive) setting and +** the db.protectSensitive flag is enabled. It returns false otherwise. +*/ +LOCAL void db_protected_setting_func( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char *zSetting; + if( (db.protectMask & PROTECT_SENSITIVE)==0 ){ + sqlite3_result_int(context, 0); + return; + } + zSetting = (const char*)sqlite3_value_text(argv[0]); + sqlite3_result_int(context, db_setting_is_protected(zSetting)); +} + +/* +** Copied from SQLite ext/misc/uint.c... +** +** Compare text in lexicographic order, except strings of digits +** compare in numeric order. +** +** This version modified to also ignore case. +*/ +static int uintNocaseCollFunc( + void *notUsed, + int nKey1, const void *pKey1, + int nKey2, const void *pKey2 +){ + const unsigned char *zA = (const unsigned char*)pKey1; + const unsigned char *zB = (const unsigned char*)pKey2; + int i=0, j=0, x; + (void)notUsed; + while( i<nKey1 && j<nKey2 ){ + if( fossil_isdigit(zA[i]) && fossil_isdigit(zB[j]) ){ + int k; + while( i<nKey1 && zA[i]=='0' ){ i++; } + while( j<nKey2 && zB[j]=='0' ){ j++; } + k = 0; + while( i+k<nKey1 && fossil_isdigit(zA[i+k]) + && j+k<nKey2 && fossil_isdigit(zB[j+k]) ){ + k++; + } + if( i+k<nKey1 && fossil_isdigit(zA[i+k]) ){ + return +1; + }else if( j+k<nKey2 && fossil_isdigit(zB[j+k]) ){ + return -1; + }else{ + x = memcmp(zA+i, zB+j, k); + if( x ) return x; + i += k; + j += k; + } + }else + if( zA[i]!=zB[j] + && (x = fossil_tolower(zA[i]) - fossil_tolower(zB[j]))!=0 + ){ + return x; + }else{ + i++; + j++; + } + } + return (nKey1 - i) - (nKey2 - j); +} + + +/* +** Register the SQL functions that are useful both to the internal +** representation and to the "fossil sql" command. +*/ +void db_add_aux_functions(sqlite3 *db){ + sqlite3_create_collation(db, "uintnocase", SQLITE_UTF8,0,uintNocaseCollFunc); + sqlite3_create_function(db, "checkin_mtime", 2, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_checkin_mtime_function, 0, 0); + sqlite3_create_function(db, "symbolic_name_to_rid", 1, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_sym2rid_function, 0, 0); + sqlite3_create_function(db, "symbolic_name_to_rid", 2, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_sym2rid_function, 0, 0); + sqlite3_create_function(db, "now", 0, + SQLITE_UTF8|SQLITE_INNOCUOUS, 0, + db_now_function, 0, 0); + sqlite3_create_function(db, "toLocal", 0, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_tolocal_function, 0, 0); + sqlite3_create_function(db, "fromLocal", 0, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_fromlocal_function, 0, 0); + sqlite3_create_function(db, "hextoblob", 1, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_hextoblob, 0, 0); + sqlite3_create_function(db, "capunion", 1, SQLITE_UTF8, 0, + 0, capability_union_step, capability_union_finalize); + sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0, + capability_fullcap, 0, 0); + sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0, + alert_find_emailaddr_func, 0, 0); + sqlite3_create_function(db, "display_name", 1, SQLITE_UTF8, 0, + alert_display_name_func, 0, 0); + sqlite3_create_function(db, "obscure", 1, SQLITE_UTF8, 0, + db_obscure, 0, 0); + sqlite3_create_function(db, "protected_setting", 1, SQLITE_UTF8, 0, + db_protected_setting_func, 0, 0); + sqlite3_create_function(db, "win_reserved", 1, SQLITE_UTF8, 0, + db_win_reserved_func,0,0); + sqlite3_create_function(db, "url_nouser", 1, SQLITE_UTF8, 0, + url_nouser_func,0,0); + sqlite3_create_function(db, "chat_msg_from_event", 4, + SQLITE_UTF8 | SQLITE_INNOCUOUS, 0, + chat_msg_from_event, 0, 0); + sqlite3_create_function(db, "inode", 1, SQLITE_UTF8, 0, + file_inode_sql_func,0,0); + sqlite3_create_function(db, "artifact_to_json", 1, SQLITE_UTF8, 0, + artifact_to_json_sql_func,0,0); + +} + +#if USE_SEE +/* +** This is a pointer to the saved database encryption key string. +*/ +static char *zSavedKey = 0; + +/* +** This is the size of the saved database encryption key, in bytes. +*/ +static size_t savedKeySize = 0; + +/* +** This function returns non-zero if there is a saved database encryption +** key available. +*/ +int db_have_saved_encryption_key(){ + return db_is_valid_saved_encryption_key(zSavedKey, savedKeySize); +} + +/* +** This function returns non-zero if the specified database encryption key +** is valid. +*/ +int db_is_valid_saved_encryption_key(const char *p, size_t n){ + if( p==0 ) return 0; + if( n==0 ) return 0; + if( p[0]==0 ) return 0; + return 1; +} + +/* +** This function returns the saved database encryption key -OR- zero if +** no database encryption key is saved. +*/ +char *db_get_saved_encryption_key(){ + return zSavedKey; +} + +/* +** This function returns the size of the saved database encryption key +** -OR- zero if no database encryption key is saved. +*/ +size_t db_get_saved_encryption_key_size(){ + return savedKeySize; +} + +/* +** This function arranges for the saved database encryption key buffer +** to be allocated and then sets up the environment variable to allow +** a child process to initialize it with the actual database encryption +** key. +*/ +void db_setup_for_saved_encryption_key(){ + void *p = NULL; + size_t n = 0; + size_t pageSize = 0; + Blob pidKey; + + assert( !db_have_saved_encryption_key() ); + db_unsave_encryption_key(); + fossil_get_page_size(&pageSize); + assert( pageSize>0 ); + p = fossil_secure_alloc_page(&n); + assert( p!=NULL ); + assert( n==pageSize ); + blob_zero(&pidKey); + blob_appendf(&pidKey, "%lu:%p:%u", (unsigned long)GETPID(), p, n); + fossil_setenv("FOSSIL_SEE_PID_KEY", blob_str(&pidKey)); + zSavedKey = p; + savedKeySize = n; +} + +/* +** This function arranges for the database encryption key to be securely +** saved in non-pageable memory (on platforms where this is possible). +*/ +static void db_save_encryption_key( + Blob *pKey +){ + void *p = NULL; + size_t n = 0; + size_t pageSize = 0; + size_t blobSize = 0; + + assert( !db_have_saved_encryption_key() ); + blobSize = blob_size(pKey); + if( blobSize==0 ) return; + fossil_get_page_size(&pageSize); + assert( pageSize>0 ); + if( blobSize>pageSize ){ + fossil_panic("key blob too large: %u versus %u", blobSize, pageSize); + } + p = fossil_secure_alloc_page(&n); + assert( p!=NULL ); + assert( n==pageSize ); + assert( n>=blobSize ); + memcpy(p, blob_str(pKey), blobSize); + zSavedKey = p; + savedKeySize = n; +} + +/* +** This function arranges for the saved database encryption key to be +** securely zeroed, unlocked (if necessary), and freed. +*/ +void db_unsave_encryption_key(){ + fossil_secure_free_page(zSavedKey, savedKeySize); + zSavedKey = NULL; + savedKeySize = 0; +} + +/* +** This function sets the saved database encryption key to the specified +** string value, allocating or freeing the underlying memory if needed. +*/ +static void db_set_saved_encryption_key( + Blob *pKey +){ + if( zSavedKey!=NULL ){ + size_t blobSize = blob_size(pKey); + if( blobSize==0 ){ + db_unsave_encryption_key(); + }else{ + if( blobSize>savedKeySize ){ + fossil_panic("key blob too large: %u versus %u", + blobSize, savedKeySize); + } + fossil_secure_zero(zSavedKey, savedKeySize); + memcpy(zSavedKey, blob_str(pKey), blobSize); + } + }else{ + db_save_encryption_key(pKey); + } +} + +/* +** WEBPAGE: setseekey +** +** Sets the sets the saved database encryption key to one that gets passed +** via the "key" query string parameter. If the saved database encryption +** key has already been set, does nothing. This web page does not produce +** any output on success or failure. No permissions are required and none +** are checked (partially due to lack of encrypted database access). +** +** Query parameters: +** +** key The string to set as the saved database encryption +** key. +*/ +void db_set_see_key_page(void){ + Blob key; + const char *zKey; + if( db_have_saved_encryption_key() ){ + fossil_trace("SEE: encryption key was already set\n"); + return; + } + zKey = P("key"); + blob_init(&key, 0, 0); + if( zKey!=0 ){ + PID_T processId; + blob_set(&key, zKey); + db_set_saved_encryption_key(&key); + processId = db_maybe_handle_saved_encryption_key_for_process( + SEE_KEY_WRITE + ); + fossil_trace("SEE: set encryption key for process %lu, length %u\n", + (unsigned long)processId, blob_size(&key)); + }else{ + fossil_trace("SEE: no encryption key specified\n"); + } + blob_reset(&key); +} + +/* +** WEBPAGE: unsetseekey +** +** Sets the saved database encryption key to zeros in the current and parent +** Fossil processes. This web page does not produce any output on success +** or failure. Setup permission is required. +*/ +void db_unset_see_key_page(void){ + PID_T processId; + login_check_credentials(); + if( !g.perm.Setup ){ login_needed(0); return; } + processId = db_maybe_handle_saved_encryption_key_for_process( + SEE_KEY_ZERO + ); + fossil_trace("SEE: unset encryption key for process %lu\n", + (unsigned long)processId); +} + +/* +** This function reads the saved database encryption key from the +** specified Fossil parent process. This is only necessary (or +** functional) on Windows or Linux. +*/ +static void db_read_saved_encryption_key_from_process( + PID_T processId, /* Identifier for Fossil parent process. */ + LPVOID pAddress, /* Pointer to saved key buffer in the parent process. */ + SIZE_T nSize /* Size of saved key buffer in the parent process. */ +){ + void *p = NULL; + size_t n = 0; + size_t pageSize = 0; + + fossil_get_page_size(&pageSize); + assert( pageSize>0 ); + if( nSize>pageSize ){ + fossil_panic("key too large: %u versus %u", nSize, pageSize); + } + p = fossil_secure_alloc_page(&n); + assert( p!=NULL ); + assert( n==pageSize ); + assert( n>=nSize ); + { +#if defined(_WIN32) + HANDLE hProcess = OpenProcess(PROCESS_VM_READ, FALSE, processId); + if( hProcess!=NULL ){ + SIZE_T nRead = 0; + if( ReadProcessMemory(hProcess, pAddress, p, nSize, &nRead) ){ + CloseHandle(hProcess); + if( nRead==nSize ){ + db_unsave_encryption_key(); + zSavedKey = p; + savedKeySize = n; + }else{ + fossil_secure_free_page(p, n); + fossil_panic("bad size read, %u out of %u bytes at %p from pid %lu", + nRead, nSize, pAddress, processId); + } + }else{ + CloseHandle(hProcess); + fossil_secure_free_page(p, n); + fossil_panic("failed read, %u bytes at %p from pid %lu: %lu", nSize, + pAddress, processId, GetLastError()); + } + }else{ + fossil_secure_free_page(p, n); + fossil_panic("failed to open pid %lu: %lu", processId, GetLastError()); + } +#elif defined(__linux__) + ssize_t nRead; + struct iovec liov = {0}; + struct iovec riov = {0}; + liov.iov_base = p; + liov.iov_len = n; + riov.iov_base = pAddress; + riov.iov_len = nSize; + nRead = process_vm_readv(processId, &liov, 1, &riov, 1, 0); + if( nRead==nSize ){ + db_unsave_encryption_key(); + zSavedKey = p; + savedKeySize = n; + }else{ + fossil_secure_free_page(p, n); + fossil_panic("bad size read, %zd out of %zu bytes at %p from pid %lu", + nRead, nSize, pAddress, (unsigned long)processId); + } +#else + fossil_secure_free_page(p, n); + fossil_trace("db_read_saved_encryption_key_from_process unsupported"); +#endif + } +} + +/* +** This function writes the saved database encryption key into the +** specified Fossil parent process. This is only necessary (or +** functional) on Windows or Linux. +*/ +static void db_write_saved_encryption_key_to_process( + PID_T processId, /* Identifier for Fossil parent process. */ + LPVOID pAddress, /* Pointer to saved key buffer in the parent process. */ + SIZE_T nSize /* Size of saved key buffer in the parent process. */ +){ + void *p = db_get_saved_encryption_key(); + size_t n = db_get_saved_encryption_key_size(); + size_t pageSize = 0; + + fossil_get_page_size(&pageSize); + assert( pageSize>0 ); + if( nSize>pageSize ){ + fossil_panic("key too large: %u versus %u", nSize, pageSize); + } + assert( p!=NULL ); + assert( n==pageSize ); + assert( n>=nSize ); + { +#if defined(_WIN32) + HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_WRITE, + FALSE, processId); + if( hProcess!=NULL ){ + SIZE_T nWrite = 0; + if( WriteProcessMemory(hProcess, pAddress, p, nSize, &nWrite) ){ + CloseHandle(hProcess); + if( nWrite!=nSize ){ + fossil_panic("bad size write, %u out of %u bytes at %p from pid %lu", + nWrite, nSize, pAddress, processId); + } + }else{ + CloseHandle(hProcess); + fossil_panic("failed write, %u bytes at %p from pid %lu: %lu", nSize, + pAddress, processId, GetLastError()); + } + }else{ + fossil_panic("failed to open pid %lu: %lu", processId, GetLastError()); + } +#elif defined(__linux__) + ssize_t nWrite; + struct iovec liov = {0}; + struct iovec riov = {0}; + liov.iov_base = p; + liov.iov_len = n; + riov.iov_base = pAddress; + riov.iov_len = nSize; + nWrite = process_vm_writev(processId, &liov, 1, &riov, 1, 0); + if( nWrite!=nSize ){ + fossil_panic("bad size write, %zd out of %zu bytes at %p from pid %lu", + nWrite, nSize, pAddress, (unsigned long)processId); + } +#else + fossil_trace("db_write_saved_encryption_key_to_process unsupported"); +#endif + } +} + +/* +** This function zeros the saved database encryption key in the specified +** Fossil parent process. This is only necessary (or functional) on +** Windows or Linux. +*/ +static void db_zero_saved_encryption_key_in_process( + PID_T processId, /* Identifier for Fossil parent process. */ + LPVOID pAddress, /* Pointer to saved key buffer in the parent process. */ + SIZE_T nSize /* Size of saved key buffer in the parent process. */ +){ + void *p = NULL; + size_t n = 0; + size_t pageSize = 0; + + fossil_get_page_size(&pageSize); + assert( pageSize>0 ); + if( nSize>pageSize ){ + fossil_panic("key too large: %u versus %u", nSize, pageSize); + } + p = fossil_secure_alloc_page(&n); + assert( p!=NULL ); + assert( n==pageSize ); + assert( n>=nSize ); + { +#if defined(_WIN32) + HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_WRITE, + FALSE, processId); + if( hProcess!=NULL ){ + SIZE_T nWrite = 0; + if( WriteProcessMemory(hProcess, pAddress, p, nSize, &nWrite) ){ + CloseHandle(hProcess); + fossil_secure_free_page(p, n); + if( nWrite!=nSize ){ + fossil_panic("bad size zero, %u out of %u bytes at %p from pid %lu", + nWrite, nSize, pAddress, processId); + } + }else{ + CloseHandle(hProcess); + fossil_secure_free_page(p, n); + fossil_panic("failed zero, %u bytes at %p from pid %lu: %lu", nSize, + pAddress, processId, GetLastError()); + } + }else{ + fossil_secure_free_page(p, n); + fossil_panic("failed to open pid %lu: %lu", processId, GetLastError()); + } +#elif defined(__linux__) + ssize_t nWrite; + struct iovec liov = {0}; + struct iovec riov = {0}; + liov.iov_base = p; + liov.iov_len = n; + riov.iov_base = pAddress; + riov.iov_len = nSize; + nWrite = process_vm_writev(processId, &liov, 1, &riov, 1, 0); + if( nWrite!=nSize ){ + fossil_secure_free_page(p, n); + fossil_panic("bad size zero, %zd out of %zu bytes at %p from pid %lu", + nWrite, nSize, pAddress, (unsigned long)processId); + } +#else + fossil_secure_free_page(p, n); + fossil_trace("db_zero_saved_encryption_key_in_process unsupported"); +#endif + } +} + +/* +** This function evaluates the specified TH1 script and attempts to parse +** its result as a colon-delimited triplet containing a process identifier, +** address, and size (in bytes) of the database encryption key. This is +** only necessary (or functional) on Windows or Linux. +*/ +static PID_T db_handle_saved_encryption_key_for_process_via_th1( + const char *zConfig, /* The TH1 script to evaluate. */ + int eType /* Non-zero to write key to parent process -OR- + * zero to read it from the parent process. */ +){ + PID_T processId = 0; + int rc; + char *zResult; + char *zPwd = file_getcwd(0, 0); + Th_FossilInit(TH_INIT_DEFAULT | TH_INIT_NEED_CONFIG | TH_INIT_NO_REPO); + rc = Th_Eval(g.interp, 0, zConfig, -1); + zResult = (char*)Th_GetResult(g.interp, 0); + if( rc!=TH_OK ){ + fossil_fatal("script for pid key failed: %s", zResult); + } + if( zResult ){ + LPVOID pAddress = NULL; + SIZE_T nSize = 0; + parse_pid_key_value(zResult, &processId, &pAddress, &nSize); + if( eType==SEE_KEY_READ ){ + db_read_saved_encryption_key_from_process(processId, pAddress, nSize); + }else if( eType==SEE_KEY_WRITE ){ + db_write_saved_encryption_key_to_process(processId, pAddress, nSize); + }else if( eType==SEE_KEY_ZERO ){ + db_zero_saved_encryption_key_in_process(processId, pAddress, nSize); + }else{ + fossil_panic("unsupported SEE key operation %d", eType); + } + } + file_chdir(zPwd, 0); + fossil_free(zPwd); + return processId; +} + +/* +** This function sets the saved database encryption key to one that gets +** read from the specified Fossil parent process, if applicable. This is +** only necessary (or functional) on Windows or Linux. +*/ +PID_T db_maybe_handle_saved_encryption_key_for_process(int eType){ + PID_T processId = 0; + g.zPidKey = find_option("usepidkey",0,1); + if( !g.zPidKey ){ + g.zPidKey = fossil_getenv("FOSSIL_SEE_PID_KEY"); + } + if( g.zPidKey ){ + LPVOID pAddress = NULL; + SIZE_T nSize = 0; + parse_pid_key_value(g.zPidKey, &processId, &pAddress, &nSize); + if( eType==SEE_KEY_READ ){ + db_read_saved_encryption_key_from_process(processId, pAddress, nSize); + }else if( eType==SEE_KEY_WRITE ){ + db_write_saved_encryption_key_to_process(processId, pAddress, nSize); + }else if( eType==SEE_KEY_ZERO ){ + db_zero_saved_encryption_key_in_process(processId, pAddress, nSize); + }else{ + fossil_panic("unsupported SEE key operation %d", eType); + } + }else{ + const char *zSeeDbConfig = find_option("seedbcfg",0,1); + if( !zSeeDbConfig ){ + zSeeDbConfig = fossil_getenv("FOSSIL_SEE_DB_CONFIG"); + } + if( zSeeDbConfig ){ + processId = db_handle_saved_encryption_key_for_process_via_th1( + zSeeDbConfig, eType + ); + } + } + return processId; +} +#endif /* USE_SEE */ + +/* +** If the database file zDbFile has a name that suggests that it is +** encrypted, then prompt for the database encryption key and return it +** in the blob *pKey. Or, if the encryption key has previously been +** requested, just return a copy of the previous result. The blob in +** *pKey must be initialized. +*/ +static void db_maybe_obtain_encryption_key( + const char *zDbFile, /* Name of the database file */ + Blob *pKey /* Put the encryption key here */ +){ +#if USE_SEE + if( sqlite3_strglob("*.efossil", zDbFile)==0 ){ + char *zKey = db_get_saved_encryption_key(); + if( zKey ){ + blob_set(pKey, zKey); + }else{ + char *zPrompt = mprintf("\rencryption key for '%s': ", zDbFile); + prompt_for_password(zPrompt, pKey, 0); + fossil_free(zPrompt); + db_set_saved_encryption_key(pKey); + } + } +#endif +} + + +/* +** Sets the encryption key for the database, if necessary. +*/ +void db_maybe_set_encryption_key(sqlite3 *db, const char *zDbName){ + Blob key; + blob_init(&key, 0, 0); + db_maybe_obtain_encryption_key(zDbName, &key); + if( blob_size(&key)>0 ){ + if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){ + char *zCmd = sqlite3_mprintf("PRAGMA key(%Q)", blob_str(&key)); + sqlite3_exec(db, zCmd, 0, 0, 0); + fossil_secure_zero(zCmd, strlen(zCmd)); + sqlite3_free(zCmd); +#if USE_SEE + }else{ + sqlite3_key(db, blob_str(&key), -1); +#endif + } + } + blob_reset(&key); +} + +/* +** Open a database file. Return a pointer to the new database +** connection. An error results in process abort. +*/ +LOCAL sqlite3 *db_open(const char *zDbName){ + int rc; + sqlite3 *db; + Blob bNameCheck = BLOB_INITIALIZER; + + if( g.fSqlTrace ) fossil_trace("-- sqlite3_open: [%s]\n", zDbName); + file_canonical_name(zDbName, &bNameCheck, 0) + /* For purposes of the apndvfs check, g.nameOfExe and zDbName must + ** both be canonicalized, else chances are very good that they + ** will not match even if they're the same file. Details: + ** https://fossil-scm.org/forum/forumpost/16880a28aad1a868 */; + if( strcmp(blob_str(&bNameCheck), g.nameOfExe)==0 ){ + extern int sqlite3_appendvfs_init( + sqlite3 *, char **, const sqlite3_api_routines * + ); + sqlite3_appendvfs_init(0,0,0); + g.zVfsName = "apndvfs"; + } + blob_reset(&bNameCheck); + rc = sqlite3_open_v2( + zDbName, &db, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, + g.zVfsName + ); + if( rc!=SQLITE_OK ){ + db_err("[%s]: %s", zDbName, sqlite3_errmsg(db)); + } + db_maybe_set_encryption_key(db, zDbName); + sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_FKEY, 0, &rc); + sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_TRIGGER, 0, &rc); + sqlite3_db_config(db, SQLITE_DBCONFIG_TRUSTED_SCHEMA, 0, &rc); + sqlite3_db_config(db, SQLITE_DBCONFIG_DQS_DDL, 0, &rc); + sqlite3_db_config(db, SQLITE_DBCONFIG_DQS_DML, 0, &rc); + sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, &rc); + sqlite3_busy_timeout(db, 15000); + sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ + sqlite3_create_function(db, "user", 0, SQLITE_UTF8, 0, db_sql_user, 0, 0); + sqlite3_create_function(db, "cgi", 1, SQLITE_UTF8, 0, db_sql_cgi, 0, 0); + sqlite3_create_function(db, "cgi", 2, SQLITE_UTF8, 0, db_sql_cgi, 0, 0); + sqlite3_create_function(db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0); + sqlite3_create_function( + db, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 + ); + sqlite3_create_function( + db, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0 + ); + if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0); + db_add_aux_functions(db); + re_add_sql_func(db); /* The REGEXP operator */ + foci_register(db); /* The "files_of_checkin" virtual table */ + sqlite3_set_authorizer(db, db_top_authorizer, db); + db_register_fts5(db) /* in search.c */; + return db; +} + + +/* +** Detaches the zLabel database. +*/ +void db_detach(const char *zLabel){ + db_multi_exec("DETACH DATABASE %Q", zLabel); +} + +/* +** zDbName is the name of a database file. Attach zDbName using +** the name zLabel. +*/ +void db_attach(const char *zDbName, const char *zLabel){ + Blob key; + if( db_table_exists(zLabel,"sqlite_schema") ) return; + blob_init(&key, 0, 0); + db_maybe_obtain_encryption_key(zDbName, &key); + if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){ + char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", + zDbName, zLabel, blob_str(&key)); + db_exec_sql(zCmd); + fossil_secure_zero(zCmd, strlen(zCmd)); + sqlite3_free(zCmd); + }else{ + char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY ''", + zDbName, zLabel); + db_exec_sql(zCmd); + sqlite3_free(zCmd); +#if USE_SEE + if( blob_size(&key)>0 ){ + sqlite3_key_v2(g.db, zLabel, blob_str(&key), -1); + } +#endif + } + blob_reset(&key); +} + +/* +** Change the schema name of the "main" database to zLabel. +** zLabel must be a static string that is unchanged for the life of +** the database connection. +** +** After calling this routine, db_database_slot(zLabel) should +** return 0. +*/ +void db_set_main_schemaname(sqlite3 *db, const char *zLabel){ + if( sqlite3_db_config(db, SQLITE_DBCONFIG_MAINDBNAME, zLabel) ){ + fossil_panic("Fossil requires a version of SQLite that supports the " + "SQLITE_DBCONFIG_MAINDBNAME interface."); + } +} + +/* +** Return the slot number for database zLabel. The first database +** opened is slot 0. The "temp" database is slot 1. Attached databases +** are slots 2 and higher. +** +** Return -1 if zLabel does not match any open database. +*/ +int db_database_slot(const char *zLabel){ + int iSlot = -1; + int rc; + Stmt q; + if( g.db==0 ) return iSlot; + rc = db_prepare_ignore_error(&q, "PRAGMA database_list"); + if( rc==SQLITE_OK ){ + while( db_step(&q)==SQLITE_ROW ){ + if( fossil_strcmp(db_column_text(&q,1),zLabel)==0 ){ + iSlot = db_column_int(&q, 0); + break; + } + } + } + db_finalize(&q); + return iSlot; +} + +/* +** zDbName is the name of a database file. If no other database +** file is open, then open this one. If another database file is +** already open, then attach zDbName using the name zLabel. +*/ +void db_open_or_attach(const char *zDbName, const char *zLabel){ + if( !g.db ){ + g.db = db_open(zDbName); + db_set_main_schemaname(g.db, zLabel); + }else{ + db_attach(zDbName, zLabel); + } +} + +/* +** Close the per-user configuration database file +*/ +void db_close_config(){ + int iSlot = db_database_slot("configdb"); + if( iSlot>0 ){ + db_detach("configdb"); + }else if( g.dbConfig ){ + sqlite3_wal_checkpoint(g.dbConfig, 0); + sqlite3_close(g.dbConfig); + g.dbConfig = 0; + }else if( g.db && 0==iSlot ){ + int rc; + sqlite3_wal_checkpoint(g.db, 0); + rc = sqlite3_close(g.db); + if( g.fSqlTrace ) fossil_trace("-- db_close_config(%d)\n", rc); + g.db = 0; + g.repositoryOpen = 0; + g.localOpen = 0; + }else{ + return; + } + fossil_free(g.zConfigDbName); + g.zConfigDbName = 0; +} + +/* +** Compute the name of the configuration database. If unable to find the +** database, return 0 if isOptional is true, or panic if isOptional is false. +** +** Space to hold the result comes from fossil_malloc(). +*/ +static char *db_configdb_name(int isOptional){ + char *zHome; /* Home directory */ + char *zDbName; /* Name of the database file */ + + + /* On Windows, look for these directories, in order: + ** + ** FOSSIL_HOME + ** LOCALAPPDATA + ** APPDATA + ** USERPROFILE + ** HOMEDRIVE HOMEPATH + */ +#if defined(_WIN32) || defined(__CYGWIN__) + zHome = fossil_getenv("FOSSIL_HOME"); + if( zHome==0 ){ + zHome = fossil_getenv("LOCALAPPDATA"); + if( zHome==0 ){ + zHome = fossil_getenv("APPDATA"); + if( zHome==0 ){ + zHome = fossil_getenv("USERPROFILE"); + if( zHome==0 ){ + char *zDrive = fossil_getenv("HOMEDRIVE"); + char *zPath = fossil_getenv("HOMEPATH"); + if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath); + } + } + } + } + zDbName = mprintf("%//_fossil", zHome); + fossil_free(zHome); + return zDbName; + +#else /* if unix */ + char *zXdgHome; + + /* For unix. a 5-step algorithm is used. + ** See ../www/tech_overview.wiki for discussion. + ** + ** Step 1: If FOSSIL_HOME exists -> $FOSSIL_HOME/.fossil + */ + zHome = fossil_getenv("FOSSIL_HOME"); + if( zHome!=0 ) return mprintf("%s/.fossil", zHome); + + /* Step 2: If HOME exists and file $HOME/.fossil exists -> $HOME/.fossil + */ + zHome = fossil_getenv("HOME"); + if( zHome ){ + zDbName = mprintf("%s/.fossil", zHome); + if( file_size(zDbName, ExtFILE)>1024*3 ){ + return zDbName; + } + fossil_free(zDbName); + } + + /* Step 3: if XDG_CONFIG_HOME exists -> $XDG_CONFIG_HOME/fossil.db + */ + zXdgHome = fossil_getenv("XDG_CONFIG_HOME"); + if( zXdgHome!=0 ){ + return mprintf("%s/fossil.db", zXdgHome); + } + + /* The HOME variable is required in order to continue. + */ + if( zHome==0 ){ + if( isOptional ) return 0; + fossil_fatal("cannot locate home directory - please set one of the " + "FOSSIL_HOME, XDG_CONFIG_HOME, or HOME environment " + "variables"); + } + + /* Step 4: If $HOME/.config is a directory -> $HOME/.config/fossil.db + */ + zXdgHome = mprintf("%s/.config", zHome); + if( file_isdir(zXdgHome, ExtFILE)==1 ){ + fossil_free(zXdgHome); + return mprintf("%s/.config/fossil.db", zHome); + } + + /* Step 5: Otherwise -> $HOME/.fossil + */ + return mprintf("%s/.fossil", zHome); +#endif /* unix */ +} + +/* +** Open the configuration database. Create the database anew if +** it does not already exist. +** +** If the useAttach flag is 0 (the usual case) then the configuration +** database is opened on a separate database connection g.dbConfig. +** This prevents the database from becoming locked on long check-in or sync +** operations which hold an exclusive transaction. In a few cases, though, +** it is convenient for the database to be attached to the main database +** connection so that we can join between the various databases. In that +** case, invoke this routine with useAttach as 1. +*/ +int db_open_config(int useAttach, int isOptional){ + char *zDbName; + if( g.zConfigDbName ){ + int alreadyAttached = db_database_slot("configdb")>0; + if( useAttach==alreadyAttached ) return 1; /* Already open. */ + db_close_config(); + } + zDbName = db_configdb_name(isOptional); + if( zDbName==0 ) return 0; + if( file_size(zDbName, ExtFILE)<1024*3 ){ + char *zHome = file_dirname(zDbName); + int rc; + if( file_isdir(zHome, ExtFILE)==0 ){ + file_mkdir(zHome, ExtFILE, 0); + } + rc = file_access(zHome, W_OK); + if( rc ){ + if( isOptional ) return 0; + fossil_fatal("home directory \"%s\" must be writeable", zHome); + } + db_init_database(zDbName, zConfigSchema, (char*)0); + fossil_free(zHome); + } + if( file_access(zDbName, W_OK) ){ + if( isOptional ) return 0; + fossil_fatal("configuration file %s must be writeable", zDbName); + } + if( useAttach ){ + db_open_or_attach(zDbName, "configdb"); + g.dbConfig = 0; + }else{ + g.dbConfig = db_open(zDbName); + db_set_main_schemaname(g.dbConfig, "configdb"); + } + g.zConfigDbName = zDbName; + return 1; +} + +/* +** Return TRUE if zTable exists. +*/ +int db_table_exists( + const char *zDb, /* One of: NULL, "configdb", "localdb", "repository" */ + const char *zTable /* Name of table */ +){ + return sqlite3_table_column_metadata(g.db, zDb, zTable, 0, + 0, 0, 0, 0, 0)==SQLITE_OK; +} + +/* +** Return TRUE if zTable exists and contains column zColumn. +** Return FALSE if zTable does not exist or if zTable exists +** but lacks zColumn. +*/ +int db_table_has_column( + const char *zDb, /* One of: NULL, "config", "localdb", "repository" */ + const char *zTable, /* Name of table */ + const char *zColumn /* Name of column in table */ +){ + return sqlite3_table_column_metadata(g.db, zDb, zTable, zColumn, + 0, 0, 0, 0, 0)==SQLITE_OK; +} + +/* +** Returns TRUE if zTable exists in the local database but lacks column +** zColumn +*/ +static int db_local_table_exists_but_lacks_column( + const char *zTable, + const char *zColumn +){ + return db_table_exists("localdb", zTable) + && !db_table_has_column("localdb", zTable, zColumn); +} + +/* +** If zDbName is a valid local database file, open it and return +** true. If it is not a valid local database file, return 0. +*/ +static int isValidLocalDb(const char *zDbName){ + i64 lsize; + + if( file_access(zDbName, F_OK) ) return 0; + lsize = file_size(zDbName, ExtFILE); + if( lsize%1024!=0 || lsize<4096 ) return 0; + db_open_or_attach(zDbName, "localdb"); + + /* Check to see if the check-out database has the latest schema changes. + ** The most recent schema change (2019-01-19) is the addition of the + ** vmerge.mhash and vfile.mhash fields. If the schema has the vmerge.mhash + ** column, assume everything else is up-to-date. + */ + if( db_table_has_column("localdb","vmerge","mhash") ){ + return 1; /* This is a check-out database with the latest schema */ + } + + /* If there is no vfile table, then assume we have picked up something + ** that is not even close to being a valid check-out database */ + if( !db_table_exists("localdb","vfile") ){ + return 0; /* Not a DB */ + } + + /* If the "isexe" column is missing from the vfile table, then + ** add it now. This code added on 2010-03-06. After all users have + ** upgraded, this code can be safely deleted. + */ + if( !db_table_has_column("localdb","vfile","isexe") ){ + db_multi_exec("ALTER TABLE vfile ADD COLUMN isexe BOOLEAN DEFAULT 0"); + } + + /* If "islink"/"isLink" columns are missing from tables, then + ** add them now. This code added on 2011-01-17 and 2011-08-27. + ** After all users have upgraded, this code can be safely deleted. + */ + if( !db_table_has_column("localdb","vfile","isLink") ){ + db_multi_exec("ALTER TABLE vfile ADD COLUMN islink BOOLEAN DEFAULT 0"); + if( db_local_table_exists_but_lacks_column("stashfile", "isLink") ){ + db_multi_exec("ALTER TABLE stashfile ADD COLUMN isLink BOOL DEFAULT 0"); + } + if( db_local_table_exists_but_lacks_column("undo", "isLink") ){ + db_multi_exec("ALTER TABLE undo ADD COLUMN isLink BOOLEAN DEFAULT 0"); + } + if( db_local_table_exists_but_lacks_column("undo_vfile", "islink") ){ + db_multi_exec("ALTER TABLE undo_vfile ADD COLUMN islink BOOL DEFAULT 0"); + } + } + + /* The design of the check-out database changed on 2019-01-19 adding the mhash + ** column to vfile and vmerge and changing the UNIQUE index on vmerge into + ** a PRIMARY KEY that includes the new mhash column. However, we must have + ** the repository database at hand in order to do the migration, so that + ** step is deferred. */ + return 1; +} + +/* +** Locate the root directory of the local repository tree. The root +** directory is found by searching for a file named "_FOSSIL_" or ".fslckout" +** that contains a valid repository database. +** +** For legacy, also look for ".fos". The use of ".fos" is deprecated +** since "fos" has negative connotations in Hungarian, we are told. +** +** If no valid _FOSSIL_ or .fslckout file is found, we move up one level and +** try again. Once the file is found, the g.zLocalRoot variable is set +** to the root of the repository tree and this routine returns 1. If +** no database is found, then this routine return 0. +** +** In db_open_local_v2(), if the bRootOnly flag is true, then only +** look in the CWD for the check-out database. Do not scan upwards in +** the file hierarchy. +** +** This routine always opens the user database regardless of whether or +** not the repository database is found. If the _FOSSIL_ or .fslckout file +** is found, it is attached to the open database connection too. +*/ +int db_open_local_v2(const char *zDbName, int bRootOnly){ + int i, n; + char zPwd[2000]; + static const char *(aDbName[]) = { "_FOSSIL_", ".fslckout", ".fos" }; + + if( g.localOpen ) return 1; + file_getcwd(zPwd, sizeof(zPwd)-20); + n = strlen(zPwd); + while( n>0 ){ + for(i=0; i<count(aDbName); i++){ + sqlite3_snprintf(sizeof(zPwd)-n, &zPwd[n], "/%s", aDbName[i]); + if( isValidLocalDb(zPwd) ){ + if( db_open_config(0, 1)==0 ){ + return 0; /* Configuration could not be opened */ + } + /* Found a valid check-out database file */ + g.zLocalDbName = fossil_strdup(zPwd); + zPwd[n] = 0; + while( n>0 && zPwd[n-1]=='/' ){ + n--; + zPwd[n] = 0; + } + g.zLocalRoot = mprintf("%s/", zPwd); + g.localOpen = 1; + db_open_repository(zDbName); + return 1; + } + } + if( bRootOnly ) break; + n--; + while( n>1 && zPwd[n]!='/' ){ n--; } + while( n>1 && zPwd[n-1]=='/' ){ n--; } + zPwd[n] = 0; + } + + /* A check-out database file could not be found */ + return 0; +} +int db_open_local(const char *zDbName){ + return db_open_local_v2(zDbName, 0); +} + +/* +** Get the full pathname to the repository database file. The +** local database (the _FOSSIL_ or .fslckout database) must have already +** been opened before this routine is called. +*/ +const char *db_repository_filename(void){ + static char *zRepo = 0; + assert( g.localOpen ); + assert( g.zLocalRoot ); + if( zRepo==0 ){ + zRepo = db_lget("repository", 0); + if( zRepo && !file_is_absolute_path(zRepo) ){ + char * zFree = zRepo; + zRepo = mprintf("%s%s", g.zLocalRoot, zRepo); + fossil_free(zFree); + zFree = zRepo; + zRepo = file_canonical_name_dup(zFree); + fossil_free(zFree); + } + } + return zRepo; +} + +/* +** Returns non-zero if support for symlinks is currently enabled. +*/ +int db_allow_symlinks(void){ + return g.allowSymlinks; +} + +/* +** Return TRUE if the file in the argument seems like it might be an +** SQLite database file that contains a Fossil repository schema. +*/ +int db_looks_like_a_repository(const char *zDbName){ + sqlite3 *db = 0; + i64 sz; + int rc; + int res = 0; + sqlite3_stmt *pStmt = 0; + + sz = file_size(zDbName, ExtFILE); + if( sz<16834 ) return 0; + db = db_open(zDbName); + if( !db ) return 0; + if( !g.zVfsName && sz%512 ) return 0; + rc = sqlite3_prepare_v2(db, + "SELECT count(*) FROM sqlite_schema" + " WHERE name COLLATE nocase IN" + "('blob','delta','rcvfrom','user','config','mlink','plink');", + -1, &pStmt, 0); + if( rc ) goto is_repo_end; + rc = sqlite3_step(pStmt); + if( rc!=SQLITE_ROW ) goto is_repo_end; + if( sqlite3_column_int(pStmt, 0)!=7 ) goto is_repo_end; + res = 1; + +is_repo_end: + sqlite3_finalize(pStmt); + sqlite3_close(db); + return res; +} + +/* +** COMMAND: test-is-repo +** Usage: %fossil test-is-repo FILENAME... +** +** Test whether the specified files look like a SQLite database +** containing a Fossil repository schema. +*/ +void test_is_repo(void){ + int i; + verify_all_options(); + for(i=2; i<g.argc; i++){ + fossil_print("%s: %s\n", + db_looks_like_a_repository(g.argv[i]) ? "yes" : " no", + g.argv[i] + ); + } +} + + +/* +** Open the repository database given by zDbName. If zDbName==NULL then +** get the name from the already open local database. +*/ +void db_open_repository(const char *zDbName){ + if( g.repositoryOpen ) return; + if( zDbName==0 ){ + if( g.localOpen ){ + zDbName = db_repository_filename(); + } + if( zDbName==0 ){ + db_err("unable to find the name of a repository database"); + } + } + if( !db_looks_like_a_repository(zDbName) ){ + if( file_access(zDbName, F_OK) ){ +#ifdef FOSSIL_ENABLE_JSON + g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; +#endif + fossil_fatal("repository does not exist or" + " is in an unreadable directory: %s", zDbName); + }else if( file_access(zDbName, R_OK) ){ +#ifdef FOSSIL_ENABLE_JSON + g.json.resultCode = FSL_JSON_E_DENIED; +#endif + fossil_fatal("read permission denied for repository %s", zDbName); + }else{ +#ifdef FOSSIL_ENABLE_JSON + g.json.resultCode = FSL_JSON_E_DB_NOT_VALID; +#endif + fossil_fatal("not a valid repository: %s", zDbName); + } + } + g.zRepositoryName = fossil_strdup(zDbName); + db_open_or_attach(g.zRepositoryName, "repository"); + g.repositoryOpen = 1; + sqlite3_file_control(g.db, "repository", SQLITE_FCNTL_DATA_VERSION, + &g.iRepoDataVers); + + /* Cache "allow-symlinks" option, because we'll need it on every stat call */ + g.allowSymlinks = db_get_boolean("allow-symlinks",0); + + g.zAuxSchema = db_get("aux-schema",""); + g.eHashPolicy = db_get_int("hash-policy",-1); + if( g.eHashPolicy<0 ){ + g.eHashPolicy = hname_default_policy(); + db_set_int("hash-policy", g.eHashPolicy, 0); + } + +#if 0 /* No longer automatic. Need to run "fossil rebuild" to migrate */ + /* Make a change to the CHECK constraint on the BLOB table for + ** version 2.0 and later. + */ + rebuild_schema_update_2_0(); /* Do the Fossil-2.0 schema updates */ +#endif + + /* Additional checks that occur when opening the check-out database */ + if( g.localOpen ){ + + /* If the repository database that was just opened has been + ** replaced by a clone of the same project, with different RID + ** values, then renumber the RID values stored in various tables + ** of the check-out database, so that the repository and check-out + ** databases align. + */ + if( !db_fingerprint_ok() ){ + if( find_option("no-rid-adjust",0,0)!=0 ){ + /* The --no-rid-adjust command-line option bypasses the RID value + ** updates. Intended for use during debugging, especially to be + ** able to run "fossil sql" after a database swap. */ + fossil_print( + "WARNING: repository change detected, but no adjust made.\n" + ); + }else if( find_option("rid-renumber-dryrun",0,0)!=0 ){ + /* the --rid-renumber-dryrun option shows how RID values would be + ** renumbered, but does not actually perform the renumbering. + ** This is a debugging-only option. */ + vfile_rid_renumbering_event(1); + exit(0); + }else{ + char *z; + stash_rid_renumbering_event(); + vfile_rid_renumbering_event(0); + undo_reset(); + bisect_reset(); + z = db_fingerprint(0, 1); + db_lset("fingerprint", z); + fossil_free(z); + fossil_print( + "WARNING: The repository database has been replaced by a clone.\n" + "Bisect history and undo have been lost.\n" + ); + } + } + + /* Make sure the check-out database schema migration of 2019-01-20 + ** has occurred. + ** + ** The 2019-01-19 migration is the addition of the vmerge.mhash and + ** vfile.mhash columns and making the vmerge.mhash column part of the + ** PRIMARY KEY for vmerge. + */ + if( !db_table_has_column("localdb", "vfile", "mhash") ){ + db_multi_exec("ALTER TABLE vfile ADD COLUMN mhash;"); + db_multi_exec( + "UPDATE vfile" + " SET mhash=(SELECT uuid FROM blob WHERE blob.rid=vfile.mrid)" + " WHERE mrid!=rid;" + ); + if( !db_table_has_column("localdb", "vmerge", "mhash") ){ + db_exec_sql("ALTER TABLE vmerge RENAME TO old_vmerge;"); + db_exec_sql(zLocalSchemaVmerge); + db_exec_sql( + "INSERT OR IGNORE INTO vmerge(id,merge,mhash)" + " SELECT id, merge, blob.uuid FROM old_vmerge, blob" + " WHERE old_vmerge.merge=blob.rid;" + "DROP TABLE old_vmerge;" + ); + } + } + } +} + +/* +** Return true if there have been any changes to the repository +** database since it was opened. +** +** Changes to "config" and "localdb" and "temp" do not count. +** This routine only returns true if there have been changes +** to "repository". +*/ +int db_repository_has_changed(void){ + unsigned int v; + if( !g.repositoryOpen ) return 0; + sqlite3_file_control(g.db, "repository", SQLITE_FCNTL_DATA_VERSION, &v); + return g.iRepoDataVers != v; +} + +/* +** Flags for the db_find_and_open_repository() function. +*/ +#if INTERFACE +#define OPEN_OK_NOT_FOUND 0x001 /* Do not error out if not found */ +#define OPEN_ANY_SCHEMA 0x002 /* Do not error if schema is wrong */ +#define OPEN_SUBSTITUTE 0x004 /* Fake in-memory repo if not found */ +#endif + +/* +** Try to find the repository and open it. Use the -R or --repository +** option to locate the repository. If no such option is available, then +** use the repository of the open check-out if there is one. +** +** Error out if the repository cannot be opened. +*/ +void db_find_and_open_repository(int bFlags, int nArgUsed){ + const char *zRep = find_repository_option(); + if( zRep && file_isdir(zRep, ExtFILE)==1 ){ + goto rep_not_found; + } + if( zRep==0 && nArgUsed && g.argc==nArgUsed+1 ){ + zRep = g.argv[nArgUsed]; + } + if( zRep==0 ){ + if( db_open_local(0)==0 ){ + goto rep_not_found; + } + zRep = db_repository_filename(); + if( zRep==0 ){ + goto rep_not_found; + } + } + db_open_repository(zRep); + if( g.repositoryOpen ){ + if( (bFlags & OPEN_ANY_SCHEMA)==0 ) db_verify_schema(); + return; + } +rep_not_found: + if( bFlags & OPEN_OK_NOT_FOUND ){ + /* No errors if the database is not found */ + if( bFlags & OPEN_SUBSTITUTE ){ + db_create_repository(0); + } + }else{ +#ifdef FOSSIL_ENABLE_JSON + g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; +#endif + if( nArgUsed==0 ){ + fossil_fatal("use --repository or -R to specify the repository database"); + }else{ + fossil_fatal("specify the repository name as a command-line argument"); + } + } +} + +/* +** Return TRUE if the schema is out-of-date +*/ +int db_schema_is_outofdate(void){ + return strcmp(g.zAuxSchema,AUX_SCHEMA_MIN)<0 + || strcmp(g.zAuxSchema,AUX_SCHEMA_MAX)>0; +} + +/* +** Return true if the database is writeable +*/ +int db_is_writeable(const char *zName){ + return g.db!=0 && !sqlite3_db_readonly(g.db, zName); +} + +/* +** Verify that the repository schema is correct. If it is not correct, +** issue a fatal error and die. +*/ +void db_verify_schema(void){ + if( db_schema_is_outofdate() ){ +#ifdef FOSSIL_ENABLE_JSON + g.json.resultCode = FSL_JSON_E_DB_NEEDS_REBUILD; +#endif + fossil_warning("incorrect repository schema version: " + "current repository schema version is \"%s\" " + "but need versions between \"%s\" and \"%s\".", + g.zAuxSchema, AUX_SCHEMA_MIN, AUX_SCHEMA_MAX); + fossil_fatal("run \"fossil rebuild\" to fix this problem"); + } +} + + +/* +** COMMAND: test-move-repository +** +** Usage: %fossil test-move-repository PATHNAME +** +** Change the location of the repository database on a local check-out. +** Use this command to avoid having to close and reopen a check-out +** when relocating the repository database. +*/ +void move_repo_cmd(void){ + Blob repo; + char *zRepo; + if( g.argc!=3 ){ + usage("PATHNAME"); + } + file_canonical_name(g.argv[2], &repo, 0); + zRepo = blob_str(&repo); + if( file_access(zRepo, F_OK) ){ + fossil_fatal("no such file: %s", zRepo); + } + if( db_open_local(zRepo)==0 ){ + fossil_fatal("not in a local check-out"); + return; + } + db_open_or_attach(zRepo, "test_repo"); + db_lset("repository", blob_str(&repo)); + db_record_repository_filename(blob_str(&repo)); + db_close(1); +} + + +/* +** Open the local database. If unable, exit with an error. +*/ +void db_must_be_within_tree(void){ + if( find_repository_option() ){ + fossil_fatal("the \"%s\" command only works from within an open check-out", + g.argv[1]); + } + if( db_open_local(0)==0 ){ + fossil_fatal("current directory is not within an open check-out"); + } + db_open_repository(0); + db_verify_schema(); +} + +/* +** Close the database connection. +** +** Check for unfinalized statements and report errors if the reportErrors +** argument is true. Ignore unfinalized statements when false. +*/ +void db_close(int reportErrors){ + sqlite3_stmt *pStmt; + if( g.db==0 ) return; + sqlite3_set_authorizer(g.db, 0, 0); + if( g.fSqlStats ){ + int cur, hiwtr; + sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_USED, &cur, &hiwtr, 0); + fprintf(stderr, "-- LOOKASIDE_USED %10d %10d\n", cur, hiwtr); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_HIT, &cur, &hiwtr, 0); + fprintf(stderr, "-- LOOKASIDE_HIT %10d\n", hiwtr); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE, &cur,&hiwtr,0); + fprintf(stderr, "-- LOOKASIDE_MISS_SIZE %10d\n", hiwtr); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL, &cur,&hiwtr,0); + fprintf(stderr, "-- LOOKASIDE_MISS_FULL %10d\n", hiwtr); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_CACHE_USED, &cur, &hiwtr, 0); + fprintf(stderr, "-- CACHE_USED %10d\n", cur); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_SCHEMA_USED, &cur, &hiwtr, 0); + fprintf(stderr, "-- SCHEMA_USED %10d\n", cur); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_STMT_USED, &cur, &hiwtr, 0); + fprintf(stderr, "-- STMT_USED %10d\n", cur); + sqlite3_status(SQLITE_STATUS_MEMORY_USED, &cur, &hiwtr, 0); + fprintf(stderr, "-- MEMORY_USED %10d %10d\n", cur, hiwtr); + sqlite3_status(SQLITE_STATUS_MALLOC_SIZE, &cur, &hiwtr, 0); + fprintf(stderr, "-- MALLOC_SIZE %10d\n", hiwtr); + sqlite3_status(SQLITE_STATUS_MALLOC_COUNT, &cur, &hiwtr, 0); + fprintf(stderr, "-- MALLOC_COUNT %10d %10d\n", cur, hiwtr); + sqlite3_status(SQLITE_STATUS_PAGECACHE_OVERFLOW, &cur, &hiwtr, 0); + fprintf(stderr, "-- PCACHE_OVFLOW %10d %10d\n", cur, hiwtr); + fprintf(stderr, "-- prepared statements %10d\n", db.nPrepare); + } + while( db.pAllStmt ){ + db_finalize(db.pAllStmt); + } + if( db.nBegin ){ + if( reportErrors ){ + fossil_warning("Transaction started at %s:%d never commits", + db.zStartFile, db.iStartLine); + } + db_end_transaction(1); + } + pStmt = 0; + sqlite3_busy_timeout(g.db, 0); + g.dbIgnoreErrors++; /* Stop "database locked" warnings */ + sqlite3_exec(g.db, "PRAGMA optimize", 0, 0, 0); + g.dbIgnoreErrors--; + db_close_config(); + + /* If the localdb has a lot of unused free space, + ** then VACUUM it as we shut down. + */ + if( db_database_slot("localdb")>=0 ){ + int nFree = db_int(0, "PRAGMA localdb.freelist_count"); + int nTotal = db_int(0, "PRAGMA localdb.page_count"); + if( nFree>nTotal/4 ){ + db_unprotect(PROTECT_ALL); + db_multi_exec("VACUUM localdb;"); + db_protect_pop(); + } + } + + if( g.db ){ + int rc; + sqlite3_wal_checkpoint(g.db, 0); + rc = sqlite3_close(g.db); + if( g.fSqlTrace ) fossil_trace("-- sqlite3_close(%d)\n", rc); + if( rc==SQLITE_BUSY && reportErrors ){ + while( (pStmt = sqlite3_next_stmt(g.db, pStmt))!=0 ){ + fossil_warning("unfinalized SQL statement: [%s]", sqlite3_sql(pStmt)); + } + } + g.db = 0; + } + g.repositoryOpen = 0; + g.localOpen = 0; + db.bProtectTriggers = 0; + assert( g.dbConfig==0 ); + assert( g.zConfigDbName==0 ); + backoffice_run_if_needed(); +} + +/* +** Close the database as quickly as possible without unnecessary processing. +*/ +void db_panic_close(void){ + if( g.db ){ + int rc; + sqlite3_wal_checkpoint(g.db, 0); + rc = sqlite3_close(g.db); + if( g.fSqlTrace ) fossil_trace("-- sqlite3_close(%d)\n", rc); + db_clear_authorizer(); + } + g.db = 0; + g.repositoryOpen = 0; + g.localOpen = 0; +} + +/* +** Create a new empty repository database with the given name. +** +** Only the schema is initialized. The required VAR tables entries +** are not set by this routine and must be set separately in order +** to make the new file a valid database. +*/ +void db_create_repository(const char *zFilename){ + db_init_database( + zFilename, + zRepositorySchema1, + zRepositorySchemaDefaultReports, + zRepositorySchema2, + (char*)0 + ); + db_delete_on_failure(zFilename); +} + +/* +** Create the default user accounts in the USER table. +*/ +void db_create_default_users(int setupUserOnly, const char *zDefaultUser){ + const char *zUser = zDefaultUser; + if( zUser==0 ){ + zUser = db_get("default-user", 0); + } + if( zUser==0 ){ + zUser = fossil_getenv("FOSSIL_USER"); + } + if( zUser==0 ){ + zUser = fossil_getenv("USER"); + } + if( zUser==0 ){ + zUser = fossil_getenv("LOGNAME"); + } + if( zUser==0 ){ + zUser = fossil_getenv("USERNAME"); + } + if( zUser==0 ){ + zUser = "root"; + } + db_unprotect(PROTECT_USER); + db_multi_exec( + "INSERT OR IGNORE INTO user(login, info) VALUES(%Q,'')", zUser + ); + db_multi_exec( + "UPDATE user SET cap='s', pw=%Q" + " WHERE login=%Q", fossil_random_password(10), zUser + ); + if( !setupUserOnly ){ + db_multi_exec( + "INSERT OR IGNORE INTO user(login,pw,cap,info)" + " VALUES('anonymous',hex(randomblob(8)),'hz','Anon');" + "INSERT OR IGNORE INTO user(login,pw,cap,info)" + " VALUES('nobody','','gjorz','Nobody');" + "INSERT OR IGNORE INTO user(login,pw,cap,info)" + " VALUES('developer','','ei','Dev');" + "INSERT OR IGNORE INTO user(login,pw,cap,info)" + " VALUES('reader','','kptw','Reader');" + ); + } + db_protect_pop(); +} + +/* +** Return a pointer to a string that contains the RHS of an IN operator +** that will select CONFIG table names that are in the list of control +** settings. +*/ +const char *db_setting_inop_rhs(){ + Blob x; + int i; + int nSetting; + const Setting *aSetting = setting_info(&nSetting); + const char *zSep = ""; + + blob_zero(&x); + blob_append_sql(&x, "("); + for(i=0; i<nSetting; i++){ + blob_append_sql(&x, "%s%Q", zSep/*safe-for-%s*/, aSetting[i].name); + zSep = ","; + } + blob_append_sql(&x, ")"); + return blob_sql_text(&x); +} + +/* +** Fill an empty repository database with the basic information for a +** repository. This function is shared between 'create_repository_cmd' +** ('new') and 'reconstruct_cmd' ('reconstruct'), both of which create +** new repositories. +** +** The zTemplate parameter determines if the settings for the repository +** should be copied from another repository. If zTemplate is 0 then the +** settings will have their normal default values. If zTemplate is +** non-zero, it is assumed that the caller of this function has already +** attached a database using the label "settingSrc". If not, the call to +** this function will fail. +** +** The zInitialDate parameter determines the date of the initial check-in +** that is automatically created. If zInitialDate is 0 then no initial +** check-in is created. The makeServerCodes flag determines whether or +** not server and project codes are invented for this repository. +*/ +void db_initial_setup( + const char *zTemplate, /* Repository from which to copy settings. */ + const char *zInitialDate, /* Initial date of repository. (ex: "now") */ + const char *zDefaultUser /* Default user for the repository */ +){ + char *zDate; + Blob hash; + Blob manifest; + + db_unprotect(PROTECT_ALL); + db_set("content-schema", CONTENT_SCHEMA, 0); + db_set("aux-schema", AUX_SCHEMA_MAX, 0); + db_set("rebuilt", get_version(), 0); + db_multi_exec( + "INSERT INTO config(name,value,mtime)" + " VALUES('server-code', lower(hex(randomblob(20))),now());" + "INSERT INTO config(name,value,mtime)" + " VALUES('project-code', lower(hex(randomblob(20))),now());" + ); + db_create_default_users(0, zDefaultUser); + if( zDefaultUser ) g.zLogin = zDefaultUser; + user_select(); + + if( zTemplate ){ + /* + ** Copy all settings from the supplied template repository. + */ + db_multi_exec( + "INSERT OR REPLACE INTO config" + " SELECT name,value,mtime FROM settingSrc.config" + " WHERE (name IN %s OR name IN %s OR name GLOB 'walias:/*')" + " AND name NOT GLOB 'project-*'" + " AND name NOT GLOB 'short-project-*';", + configure_inop_rhs(CONFIGSET_ALL), + db_setting_inop_rhs() + ); + g.eHashPolicy = db_get_int("hash-policy", g.eHashPolicy); + db_multi_exec( + "REPLACE INTO reportfmt SELECT * FROM settingSrc.reportfmt;" + ); + + /* + ** 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. + */ + db_multi_exec("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');" + ); + } + db_protect_pop(); + + if( zInitialDate ){ + int rid; + blob_zero(&manifest); + blob_appendf(&manifest, "C initial\\sempty\\scheck-in\n"); + zDate = date_in_standard_format(zInitialDate); + blob_appendf(&manifest, "D %s\n", zDate); + md5sum_init(); + /* The R-card is necessary here because without it + * fossil versions earlier than versions 1.27 would + * interpret this artifact as a "control". */ + blob_appendf(&manifest, "R %s\n", md5sum_finish(0)); + blob_appendf(&manifest, "T *branch * trunk\n"); + blob_appendf(&manifest, "T *sym-trunk *\n"); + blob_appendf(&manifest, "U %F\n", g.zLogin); + md5sum_blob(&manifest, &hash); + blob_appendf(&manifest, "Z %b\n", &hash); + blob_reset(&hash); + rid = content_put(&manifest); + manifest_crosslink(rid, &manifest, MC_NONE); + } +} + +/* +** COMMAND: new# +** COMMAND: init +** +** Usage: %fossil new ?OPTIONS? FILENAME +** or: %fossil init ?OPTIONS? FILENAME +** +** Create a repository for a new project in the file named FILENAME. +** This command is distinct from "clone". The "clone" command makes +** a copy of an existing project. This command starts a new project. +** +** By default, your current login name is used to create the default +** admin user. This can be overridden using the -A|--admin-user +** parameter. +** +** By default, all settings will be initialized to their default values. +** This can be overridden using the --template parameter to specify a +** repository file from which to copy the initial settings. When a template +** repository is used, almost all of the settings accessible from the setup +** page, either directly or indirectly, will be copied. Normal users and +** their associated permissions will not be copied; however, the system +** default users "anonymous", "nobody", "reader", "developer", and their +** associated permissions will be copied. In case of SQL errors, rebuild the +** template repository and try again. +** +** Options: +** --template FILE Copy settings from repository file +** -A|--admin-user USERNAME Select given USERNAME as admin user +** --date-override DATETIME Use DATETIME as time of the initial check-in +** --sha1 Use an initial hash policy of "sha1" +** --project-name STRING The name of the project "project name in +** quotes" +** --project-desc STRING The description of the project "project +** description in quotes" +** +** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in +** year-month-day form, it may be truncated, the "T" may be replaced by +** a space, and it may also name a timezone offset from UTC as "-HH:MM" +** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" +** means UTC. +** +** See also: [[clone]] +*/ +void create_repository_cmd(void){ + char *zPassword; + const char *zTemplate; /* Repository from which to copy settings */ + const char *zDate; /* Date of the initial check-in */ + const char *zDefaultUser; /* Optional name of the default user */ + const char *zProjectName; /* Optional project name of the repo */ + const char *zProjectDesc; /* Optional project description "description + ** of project in quotes" */ + int bUseSha1 = 0; /* True to set the hash-policy to sha1 */ + + + zTemplate = find_option("template",0,1); + zDate = find_option("date-override",0,1); + zDefaultUser = find_option("admin-user","A",1); + bUseSha1 = find_option("sha1",0,0)!=0; + zProjectName = find_option("project-name", 0, 1); + zProjectDesc = find_option("project-desc", 0, 1); + /* We should be done with options.. */ + verify_all_options(); + + if( g.argc!=3 ){ + usage("REPOSITORY-NAME"); + } + + if( -1 != file_size(g.argv[2], ExtFILE) ){ + fossil_fatal("file already exists: %s", g.argv[2]); + } + + db_create_repository(g.argv[2]); + db_open_repository(g.argv[2]); + db_open_config(0, 0); + if( zTemplate ) db_attach(zTemplate, "settingSrc"); + db_begin_transaction(); + if( bUseSha1 ){ + g.eHashPolicy = HPOLICY_SHA1; + db_set_int("hash-policy", HPOLICY_SHA1, 0); + } + if( zProjectName ) db_set("project-name", zProjectName, 0); + if( zProjectDesc ) db_set("project-description", zProjectDesc, 0); + if( zDate==0 ) zDate = "now"; + db_initial_setup(zTemplate, zDate, zDefaultUser); + db_end_transaction(0); + if( zTemplate ) db_detach("settingSrc"); + if( zProjectName ) fossil_print("project-name: %s\n", zProjectName); + if( zProjectDesc ) fossil_print("project-description: %s\n", zProjectDesc); + fossil_print("project-id: %s\n", db_get("project-code", 0)); + fossil_print("server-id: %s\n", db_get("server-code", 0)); + zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin); + fossil_print("admin-user: %s (initial remote-access password is \"%s\")\n", + g.zLogin, zPassword); + hash_user_password(g.zLogin); +} + +/* +** SQL functions for debugging. +** +** The print() function writes its arguments on stdout, but only +** if the -sqlprint command-line option is turned on. +*/ +void db_sql_print( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + int i; + if( g.fSqlPrint ){ + for(i=0; i<argc; i++){ + char c = i==argc-1 ? '\n' : ' '; + fossil_print("%s%c", sqlite3_value_text(argv[i]), c); + } + } +} + +/* +** Callback for sqlite3_trace_v2(); +*/ +int db_sql_trace(unsigned m, void *notUsed, void *pP, void *pX){ + sqlite3_stmt *pStmt = (sqlite3_stmt*)pP; + char *zSql; + int n; + const char *zArg = (const char*)pX; + char zEnd[100]; + if( m & SQLITE_TRACE_CLOSE ){ + /* If we are tracking closes, that means we want to clean up static + ** prepared statements. */ + while( db.pAllStmt ){ + db_finalize(db.pAllStmt); + } + return 0; + } + if( zArg[0]=='-' ) return 0; + if( m & SQLITE_TRACE_PROFILE ){ + sqlite3_int64 nNano = *(sqlite3_int64*)pX; + double rMillisec = 0.000001 * nNano; + int nRun = sqlite3_stmt_status(pStmt, SQLITE_STMTSTATUS_RUN, 0); + int nVmStep = sqlite3_stmt_status(pStmt, SQLITE_STMTSTATUS_VM_STEP, 1); + sqlite3_snprintf(sizeof(zEnd),zEnd," /* %.3fms, %r run, %d vm-steps */\n", + rMillisec, nRun, nVmStep); + }else{ + zEnd[0] = '\n'; + zEnd[1] = 0; + } + zSql = sqlite3_expanded_sql(pStmt); + n = (int)strlen(zSql); + fossil_trace("%s%s%s", zSql, (n>0 && zSql[n-1]==';') ? "" : ";", zEnd); + sqlite3_free(zSql); + return 0; +} + +/* +** Implement the user() SQL function. user() takes no arguments and +** returns the user ID of the current user. +*/ +LOCAL void db_sql_user( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + if( g.zLogin!=0 ){ + sqlite3_result_text(context, g.zLogin, -1, SQLITE_STATIC); + } +} + +/* +** Implement the cgi() SQL function. cgi() takes an argument which is +** a name of CGI query parameter. The value of that parameter is returned, +** if available. Optional second argument will be returned if the first +** doesn't exist as a CGI parameter. +*/ +LOCAL void db_sql_cgi(sqlite3_context *context, int argc, sqlite3_value **argv){ + const char* zP; + if( argc!=1 && argc!=2 ) return; + zP = P((const char*)sqlite3_value_text(argv[0])); + if( zP ){ + sqlite3_result_text(context, zP, -1, SQLITE_STATIC); + }else if( argc==2 ){ + zP = (const char*)sqlite3_value_text(argv[1]); + if( zP ) sqlite3_result_text(context, zP, -1, SQLITE_TRANSIENT); + } +} + +/* +** SQL function: +** +** is_selected(id) +** if_selected(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 g.aCommitFile[] array. This function looks at that array +** to see if a file is named on the command-line. +** +** In the first form (1 argument) return TRUE if either no files are +** named on the command line (g.aCommitFile is NULL meaning that all +** changes are to be committed) or if id is found in g.aCommitFile[] +** (meaning that id was named on the command-line). +** +** In the second form (3 arguments) return argument X if true and Y +** if false. Except if Y is NULL then always return X. +*/ +LOCAL void file_is_selected( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + int rc = 0; + + assert(argc==1 || argc==3); + if( g.aCommitFile ){ + int iId = sqlite3_value_int(argv[0]); + int ii; + for(ii=0; g.aCommitFile[ii]; ii++){ + if( iId==g.aCommitFile[ii] ){ + rc = 1; + break; + } + } + }else{ + rc = 1; + } + if( argc==1 ){ + sqlite3_result_int(context, rc); + }else{ + assert( argc==3 ); + assert( rc==0 || rc==1 ); + if( sqlite3_value_type(argv[2-rc])==SQLITE_NULL ) rc = 1-rc; + sqlite3_result_value(context, argv[2-rc]); + } +} + +/* +** Implementation of the "win_reserved(X)" SQL function, a wrapper +** for file_is_win_reserved(X) which returns true if X is +** a Windows-reserved filename. +*/ +LOCAL void db_win_reserved_func( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char * zName = (const char *)sqlite3_value_text(argv[0]); + if( zName!=0 ){ + sqlite3_result_int(context, file_is_win_reserved(zName)!=0); + } +} + +/* +** Convert the input string into an artifact hash. Make a notation in the +** CONCEALED table so that the hash can be undo using the db_reveal() +** function at some later time. +** +** The value returned is stored in static space and will be overwritten +** on subsequent calls. +** +** If zContent is already a well-formed artifact hash, then return a copy +** of that hash, not a hash of the hash. +** +** The CONCEALED table is meant to obscure email addresses. Every valid +** email address will contain a "@" character and "@" is not valid within +** a SHA1 hash so there is no chance that a valid email address will go +** unconcealed. +*/ +char *db_conceal(const char *zContent, int n){ + static char zHash[HNAME_MAX+1]; + Blob out; + if( hname_validate(zContent, n) ){ + memcpy(zHash, zContent, n); + zHash[n] = 0; + }else{ + sha1sum_step_text(zContent, n); + sha1sum_finish(&out); + sqlite3_snprintf(sizeof(zHash), zHash, "%s", blob_str(&out)); + blob_reset(&out); + db_multi_exec( + "INSERT OR IGNORE INTO concealed(hash,content,mtime)" + " VALUES(%Q,%#Q,now())", + zHash, n, zContent + ); + } + return zHash; +} + +/* +** Attempt to look up the input in the CONCEALED table. If found, +** and if the okRdAddr permission is enabled then return the +** original value for which the input is a hash. If g.perm.RdAddr is +** false or if the lookup fails, return the original string content. +** +** In either case, the string returned is stored in space obtained +** from malloc and should be freed by the calling function. +*/ +char *db_reveal(const char *zKey){ + char *zOut; + if( g.perm.RdAddr ){ + zOut = db_text(0, "SELECT content FROM concealed WHERE hash=%Q", zKey); + }else{ + zOut = 0; + } + if( zOut==0 ){ + zOut = fossil_strdup_nn(zKey); + } + return zOut; +} + +/* +** Return true if the string zVal represents "true" (or "false"). +*/ +int is_truth(const char *zVal){ + static const char *const azOn[] = { "on", "yes", "true" }; + int i; + for(i=0; i<count(azOn); i++){ + if( fossil_stricmp(zVal,azOn[i])==0 ) return 1; + } + return atoi(zVal); +} +int is_false(const char *zVal){ + static const char *const azOff[] = { "off", "no", "false", "0" }; + int i; + for(i=0; i<count(azOff); i++){ + if( fossil_stricmp(zVal,azOff[i])==0 ) return 1; + } + return 0; +} + +/* +** Swap the g.db and g.dbConfig connections so that the various db_* routines +** work on the configuration database instead of on the repository database. +** Be sure to swap them back after doing the operation. +** +** If the configuration database has already been opened as the main database +** or is attached to the main database, no connection swaps are required so +** this routine is a no-op. +*/ +void db_swap_connections(void){ + /* + ** When swapping the main database connection with the config database + ** connection, the config database connection must be open (not simply + ** attached); otherwise, the swap would end up leaving the main database + ** connection invalid, defeating the very purpose of this routine. This + ** same constraint also holds true when restoring the previously swapped + ** database connection; otherwise, it means that no swap was performed + ** because the main database connection was already pointing to the config + ** database. + */ + if( g.dbConfig ){ + sqlite3 *dbTemp = g.db; + g.db = g.dbConfig; + g.dbConfig = dbTemp; + } +} + +/* +** Try to read a versioned setting string from .fossil-settings/<name>. +** +** Return the text of the string if it is found. Return NULL if not +** found. +** +** If the zNonVersionedSetting parameter is not NULL then it holds the +** non-versioned value for this setting. If both a versioned and a +** non-versioned value exist and are not equal, then a warning message +** might be generated. +** +** zCkin is normally NULL. In that case, the versioned setting is +** take from the local check-out, if a local checkout exists, or from +** check-in named by the g.zOpenRevision global variable. If zCkin is +** not NULL, then zCkin is the name of the specific check-in from which +** versioned setting value is taken. When zCkin is not NULL, the cache +** is bypassed. +*/ +char *db_get_versioned( + const char *zName, + char *zNonVersionedSetting, + const char *zCkin +){ + char *zVersionedSetting = 0; + int noWarn = 0; + int found = 0; + struct _cacheEntry { + struct _cacheEntry *next; + const char *zName, *zValue; + } *cacheEntry = 0; + static struct _cacheEntry *cache = 0; + + if( !g.localOpen && g.zOpenRevision==0 && zCkin==0 ){ + return zNonVersionedSetting; + } + + /* Look up name in cache */ + if( zCkin==0 ){ + cacheEntry = cache; + while( cacheEntry!=0 ){ + if( fossil_strcmp(cacheEntry->zName, zName)==0 ){ + zVersionedSetting = fossil_strdup(cacheEntry->zValue); + break; + } + cacheEntry = cacheEntry->next; + } + } + + /* Attempt to read value from file in check-out if there wasn't a cache hit.*/ + if( cacheEntry==0 ){ + Blob versionedPathname; + Blob setting; + blob_init(&versionedPathname, 0, 0); + blob_init(&setting, 0, 0); + if( !g.localOpen || zCkin!=0 ){ + /* Repository is in the process of being opened, but files have not been + * written to disk. Load from the database. */ + blob_appendf(&versionedPathname, ".fossil-settings/%s", zName); + if( historical_blob(zCkin ? zCkin : g.zOpenRevision, + blob_str(&versionedPathname), + &setting, 0) + ){ + found = 1; + } + }else{ + blob_appendf(&versionedPathname, "%s.fossil-settings/%s", + g.zLocalRoot, zName); + if( file_size(blob_str(&versionedPathname), ExtFILE)>=0 ){ + /* File exists, and contains the value for this setting. Load from + ** the file. */ + const char *zFile = blob_str(&versionedPathname); + if( blob_read_from_file(&setting, zFile, ExtFILE)>=0 ){ + found = 1; + } + /* See if there's a no-warn flag */ + blob_append(&versionedPathname, ".no-warn", -1); + if( file_size(blob_str(&versionedPathname), ExtFILE)>=0 ){ + noWarn = 1; + } + } + } + blob_reset(&versionedPathname); + if( found ){ + blob_strip_comment_lines(&setting, &setting); + blob_trim(&setting); /* Avoid non-obvious problems with line endings + ** on boolean properties */ + zVersionedSetting = fossil_strdup(blob_str(&setting)); + } + blob_reset(&setting); + + /* Store result in cache, which can be the value or 0 if not found */ + if( zCkin==0 ){ + cacheEntry = (struct _cacheEntry*)fossil_malloc(sizeof(*cacheEntry)); + cacheEntry->next = cache; + cacheEntry->zName = zName; + cacheEntry->zValue = fossil_strdup(zVersionedSetting); + cache = cacheEntry; + } + } + + /* Display a warning? */ + if( zVersionedSetting!=0 + && zNonVersionedSetting!=0 + && zNonVersionedSetting[0]!='\0' + && zCkin==0 + && !noWarn + ){ + /* There's a versioned setting, and a non-versioned setting. Tell + ** the user about the conflict */ + fossil_warning( + "setting %s has both versioned and non-versioned values: using " + "versioned value from file \"%/.fossil-settings/%s\" (to silence " + "this warning, either create an empty file named " + "\"%/.fossil-settings/%s.no-warn\" in the check-out root, or delete " + "the non-versioned setting with \"fossil unset %s\")", zName, + g.zLocalRoot, zName, g.zLocalRoot, zName, zName + ); + } + + /* Prefer the versioned setting */ + return ( zVersionedSetting!=0 ) ? zVersionedSetting : zNonVersionedSetting; +} + + +/* +** Get and set values from the CONFIG, GLOBAL_CONFIG and VVAR table in the +** repository and local databases. +** +** If no such variable exists, return zDefault. Or, if zName is the name +** of a setting, then the zDefault is ignored and the default value of the +** setting is returned instead. If zName is a versioned setting, then +** versioned value takes priority. +*/ +char *db_get(const char *zName, const char *zDefault){ + char *z = 0; + const Setting *pSetting = db_find_setting(zName, 0); + if( g.repositoryOpen ){ + static Stmt q1; + const char *zRes; + db_static_prepare(&q1, "SELECT value FROM config WHERE name=$n"); + db_bind_text(&q1, "$n", zName); + if( db_step(&q1)==SQLITE_ROW && (zRes = db_column_text(&q1,0))!=0 ){ + z = fossil_strdup(zRes); + } + db_reset(&q1); + } + if( z==0 && g.zConfigDbName ){ + static Stmt q2; + const char *zRes; + db_swap_connections(); + db_static_prepare(&q2, "SELECT value FROM global_config WHERE name=$n"); + db_swap_connections(); + db_bind_text(&q2, "$n", zName); + if( db_step(&q2)==SQLITE_ROW && (zRes = db_column_text(&q2,0))!=0 ){ + z = fossil_strdup(zRes); + } + db_reset(&q2); + } + if( pSetting!=0 && pSetting->versionable ){ + /* This is a versionable setting, try and get the info from a + ** checked-out file */ + char * zZ = z; + z = db_get_versioned(zName, z, 0); + if(zZ != z){ + fossil_free(zZ); + } + } + if( z==0 ){ + if( zDefault==0 && pSetting && pSetting->def[0] ){ + z = fossil_strdup(pSetting->def); + }else{ + z = fossil_strdup(zDefault); + } + } + return z; +} +char *db_get_mtime(const char *zName, const char *zFormat, + const char *zDefault){ + char *z = 0; + if( g.repositoryOpen ){ + z = db_text(0, "SELECT mtime FROM config WHERE name=%Q", zName); + } + if( z==0 ){ + z = fossil_strdup(zDefault); + }else if( zFormat!=0 ){ + z = db_text(0, "SELECT strftime(%Q,%Q,'unixepoch');", zFormat, z); + } + return z; +} +void db_set(const char *zName, const char *zValue, int globalFlag){ + const CmdOrPage *pCmd = 0; + db_assert_protection_off_or_not_sensitive(zName); + if( zValue!=0 && zValue[0]==0 + && dispatch_name_search(zName, CMDFLAG_SETTING, &pCmd)==0 + && (pCmd->eCmdFlags & CMDFLAG_KEEPEMPTY)==0 + ){ + /* Changing a setting to an empty string is the same as unsetting it, + ** unless that setting has the keep-empty flag. */ + db_unset(zName/*works-like:"x"*/, globalFlag); + return; + } + db_unprotect(PROTECT_CONFIG); + db_begin_transaction(); + if( globalFlag ){ + db_swap_connections(); + db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%Q)", + zName, zValue); + db_swap_connections(); + }else{ + db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now())", + zName, zValue); + } + if( globalFlag && g.repositoryOpen ){ + db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + } + db_end_transaction(0); + db_protect_pop(); +} +void db_unset(const char *zName, int globalFlag){ + db_begin_transaction(); + db_unprotect(PROTECT_CONFIG); + if( globalFlag ){ + db_swap_connections(); + db_multi_exec("DELETE FROM global_config WHERE name=%Q", zName); + db_swap_connections(); + }else{ + db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + } + if( globalFlag && g.repositoryOpen ){ + db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + } + db_protect_pop(); + db_end_transaction(0); +} +int db_is_global(const char *zName){ + int rc = 0; + if( g.zConfigDbName ){ + db_swap_connections(); + rc = db_exists("SELECT 1 FROM global_config WHERE name=%Q", zName); + db_swap_connections(); + } + return rc; +} +int db_get_int(const char *zName, int dflt){ + int v = dflt; + int rc; + if( g.repositoryOpen ){ + static Stmt q; + db_static_prepare(&q, "SELECT value FROM config WHERE name=$n"); + db_bind_text(&q, "$n", zName); + rc = db_step(&q); + if( rc==SQLITE_ROW ){ + v = db_column_int(&q, 0); + } + db_reset(&q); + }else{ + rc = SQLITE_DONE; + } + if( rc==SQLITE_DONE && g.zConfigDbName ){ + static Stmt q2; + db_swap_connections(); + db_static_prepare(&q2, "SELECT value FROM global_config WHERE name=$n"); + db_swap_connections(); + db_bind_text(&q2, "$n", zName); + if( db_step(&q2)==SQLITE_ROW ){ + v = db_column_int(&q2, 0); + } + db_reset(&q2); + } + return v; +} +i64 db_large_file_size(void){ + /* Return size of the largest file that is not considered oversized */ + return strtoll(db_get("large-file-size","20000000"),0,0); +} +void db_set_int(const char *zName, int value, int globalFlag){ + db_assert_protection_off_or_not_sensitive(zName); + db_unprotect(PROTECT_CONFIG); + if( globalFlag ){ + db_swap_connections(); + db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%d)", + zName, value); + db_swap_connections(); + }else{ + db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%d,now())", + zName, value); + } + if( globalFlag && g.repositoryOpen ){ + db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + } + db_protect_pop(); +} +int db_get_boolean(const char *zName, int dflt){ + char *zVal = db_get(zName, dflt ? "on" : "off"); + if( is_truth(zVal) ){ + dflt = 1; + }else if( is_false(zVal) ){ + dflt = 0; + } + fossil_free(zVal); + return dflt; +} +int db_get_versioned_boolean(const char *zName, int dflt){ + char *zVal = db_get_versioned(zName, 0, 0); + if( zVal==0 ) return dflt; + if( is_truth(zVal) ) return 1; + if( is_false(zVal) ) return 0; + return dflt; +} +char *db_lget(const char *zName, const char *zDefault){ + return db_text(zDefault, + "SELECT value FROM vvar WHERE name=%Q", zName); +} +void db_lset(const char *zName, const char *zValue){ + db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%Q)", zName, zValue); +} +int db_lget_int(const char *zName, int dflt){ + return db_int(dflt, "SELECT value FROM vvar WHERE name=%Q", zName); +} +int db_lget_boolean(const char *zName, int dflt){ + char *zVal = db_lget(zName, dflt ? "on" : "off"); + if( is_truth(zVal) ){ + dflt = 1; + }else if( is_false(zVal) ){ + dflt = 0; + } + fossil_free(zVal); + return dflt; +} +void db_lset_int(const char *zName, int value){ + db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%d)", zName, value); +} + +/* Va-args versions of db_get(), db_set(), and db_unset() +** +** codecheck1.c verifies that the format string for db_set_mprintf() +** and db_unset_mprintf() begins with an ASCII character prefix. We +** don't want that format string to begin with %s or %d as that might +** allow an injection attack to set or overwrite arbitrary settings. +*/ +char *db_get_mprintf(const char *zDefault, const char *zFormat, ...){ + va_list ap; + char *zName; + char *zResult; + va_start(ap, zFormat); + zName = vmprintf(zFormat, ap); + va_end(ap); + zResult = db_get(zName, zDefault); + fossil_free(zName); + return zResult; +} +void db_set_mprintf(const char *zNew, int iGlobal, const char *zFormat, ...){ + va_list ap; + char *zName; + va_start(ap, zFormat); + zName = vmprintf(zFormat, ap); + va_end(ap); + db_set(zName/*works-like:"x"*/, zNew, iGlobal); + fossil_free(zName); +} +void db_unset_mprintf(int iGlobal, const char *zFormat, ...){ + va_list ap; + char *zName; + va_start(ap, zFormat); + zName = vmprintf(zFormat, ap); + va_end(ap); + db_unset(zName/*works-like:"x"*/, iGlobal); + fossil_free(zName); +} + +/* +** Get a setting that is tailored to subsystem. The return value is +** NULL if the setting does not exist, or a string obtained from mprintf() +** if the setting is available. +** +** The actual setting can be a comma-separated list of values of the form: +** +** * VALUE +** * SUBSYSTEM=VALUE +** +** A VALUE without the SUBSYSTEM= prefix is the default. This routine +** returns the VALUE that with the matching SUBSYSTEM, or the default +** VALUE if there is no match. +*/ +char *db_get_for_subsystem(const char *zName, const char *zSubsys){ + int nSubsys; + char *zToFree = 0; + char *zCopy; + char *zNext; + char *zResult = 0; + const char *zSetting = db_get(zName, 0); + if( zSetting==0 ) return 0; + zCopy = zToFree = fossil_strdup(zSetting); + if( zSubsys==0 ) zSubsys = ""; + nSubsys = (int)strlen(zSubsys); + while( zCopy ){ + zNext = strchr(zCopy, ','); + if( zNext ){ + zNext[0] = 0; + do{ zNext++; }while( fossil_isspace(zNext[0]) ); + if( zNext[0]==0 ) zNext = 0; + } + if( strchr(zCopy,'=')==0 ){ + if( zResult==0 ) zResult = zCopy; + }else + if( nSubsys + && strncmp(zCopy, zSubsys, nSubsys)==0 + && zCopy[nSubsys]=='=' + ){ + zResult = &zCopy[nSubsys+1]; + break; + } + zCopy = zNext; + } + if( zResult ) zResult = fossil_strdup(zResult); + fossil_free(zToFree); + return zResult; +} + +#if INTERFACE +/* Manifest generation flags */ +#define MFESTFLG_RAW 0x01 +#define MFESTFLG_UUID 0x02 +#define MFESTFLG_TAGS 0x04 +#endif /* INTERFACE */ + +/* +** Get the manifest setting. For backwards compatibility first check if the +** value is a boolean. If it's not a boolean, treat each character as a flag +** to enable a manifest type. This system puts certain boundary conditions on +** which letters can be used to represent flags (any permutation of flags must +** not be able to fully form one of the boolean values). +** +** "manifest" is a versionable setting. But we do not issue a warning +** if there is a conflict. Instead, the value returned is the value for +** the versioned setting if the versioned setting exists, or the ordinary +** setting otherwise. +** +** The argument zCkin is the specific check-in for which we want the +** manifest setting. +*/ +int db_get_manifest_setting(const char *zCkin){ + int flg; + char *zVal; + + /* Look for the versioned setting first */ + zVal = db_get_versioned("manifest", 0, zCkin); + + if( zVal==0 && g.repositoryOpen ){ + /* No versioned setting, look for the repository setting second */ + zVal = db_text(0, "SELECT value FROM config WHERE name='manifest'"); + } + if( zVal==0 || is_false(zVal) ){ + return 0; + }else if( is_truth(zVal) ){ + return MFESTFLG_RAW|MFESTFLG_UUID; + } + flg = 0; + while( *zVal ){ + switch( *zVal ){ + case 'r': flg |= MFESTFLG_RAW; break; + case 'u': flg |= MFESTFLG_UUID; break; + case 't': flg |= MFESTFLG_TAGS; break; + } + zVal++; + } + return flg; +} + +/* +** COMMAND: test-manifest-setting +** +** Usage: %fossil test-manifest-setting VERSION VERSION ... +** +** Display the value for the "manifest" setting for various versions +** of the repository. +*/ +void test_manfest_setting_cmd(void){ + int i; + db_find_and_open_repository(0, 0); + for(i=2; i<g.argc; i++){ + int m = db_get_manifest_setting(g.argv[i]); + fossil_print("%s:\n", g.argv[i]); + fossil_print(" flags = 0x%02x\n", m); + if( m & MFESTFLG_RAW ){ + fossil_print(" manifest\n"); + } + if( m & MFESTFLG_UUID ){ + fossil_print(" manifest.uuid\n"); + } + if( m & MFESTFLG_TAGS ){ + fossil_print(" manifest.tags\n"); + } + } +} + + +/* +** Record the name of a local repository in the global_config() database. +** The repository filename %s is recorded as an entry with a "name" field +** of the following form: +** +** repo:%s +** +** The value field is set to 1. +** +** If running from a local check-out, also record the root of the check-out +** as follows: +** +** ckout:%s +** +** Where %s is the check-out root. The value is the repository file. +*/ +void db_record_repository_filename(const char *zName){ + char *zRepoSetting; + char *zCkoutSetting; + Blob full; + if( zName==0 ){ + if( !g.localOpen ) return; + zName = db_repository_filename(); + } + file_canonical_name(zName, &full, 0); + (void)filename_collation(); /* Initialize before connection swap */ + db_swap_connections(); + zRepoSetting = mprintf("repo:%q", blob_str(&full)); + + db_unprotect(PROTECT_CONFIG); + db_multi_exec( + "DELETE FROM global_config WHERE name %s = %Q;", + filename_collation(), zRepoSetting + ); + db_multi_exec( + "INSERT OR IGNORE INTO global_config(name,value)" + "VALUES(%Q,1);", + zRepoSetting + ); + db_protect_pop(); + fossil_free(zRepoSetting); + if( g.localOpen && g.zLocalRoot && g.zLocalRoot[0] ){ + Blob localRoot; + file_canonical_name(g.zLocalRoot, &localRoot, 1); + zCkoutSetting = mprintf("ckout:%q", blob_str(&localRoot)); + db_unprotect(PROTECT_CONFIG); + db_multi_exec( + "DELETE FROM global_config WHERE name %s = %Q;", + filename_collation(), zCkoutSetting + ); + db_multi_exec( + "REPLACE INTO global_config(name, value)" + "VALUES(%Q,%Q);", + zCkoutSetting, blob_str(&full) + ); + db_swap_connections(); + db_optional_sql("repository", + "DELETE FROM config WHERE name %s = %Q;", + filename_collation(), zCkoutSetting + ); + db_optional_sql("repository", + "REPLACE INTO config(name,value,mtime)" + "VALUES(%Q,1,now());", + zCkoutSetting + ); + db_protect_pop(); + fossil_free(zCkoutSetting); + blob_reset(&localRoot); + }else{ + db_swap_connections(); + } + blob_reset(&full); +} + +/* +** COMMAND: open +** +** Usage: %fossil open REPOSITORY ?VERSION? ?OPTIONS? +** +** Open a new connection to the repository name REPOSITORY. A check-out +** for the repository is created with its root at the current working +** directory, or in DIR if the "--workdir DIR" is used. If VERSION is +** specified then that version is checked out. Otherwise the most recent +** check-in on the main branch (usually "trunk") is used. +** +** REPOSITORY can be the filename for a repository that already exists on the +** local machine or it can be a URI for a remote repository. If REPOSITORY +** is a URI in one of the formats recognized by the [[clone]] command, the +** remote repo is first cloned, then the clone is opened. The clone will be +** stored in the current directory, or in DIR if the "--repodir DIR" option +** is used. The name of the clone will be taken from the last term of the URI. +** For "http:" and "https:" URIs, you can append an extra term to the end of +** the URI to get any repository name you like. For example: +** +** fossil open https://fossil-scm.org/home/new-name +** +** The base URI for cloning is "https://fossil-scm.org/home". The extra +** "new-name" term means that the cloned repository will be called +** "new-name.fossil". +** +** Options: +** --empty Initialize check-out as being empty, but still connected +** with the local repository. If you commit this check-out, +** it will become a new "initial" commit in the repository. +** -f|--force Continue with the open even if the working directory is +** not empty, or if auto-sync fails. +** --force-missing Force opening a repository with missing content +** -k|--keep Only modify the manifest file(s) +** --nested Allow opening a repository inside an opened check-out +** --nosync Do not auto-sync the repository prior to opening even +** if the autosync setting is on. +** --proxy PROXY Use PROXY as http proxy during sync operation +** --reopen REPO "Reopen" an existing checkout to use a different +** repository file REPO. Useful to reconnect an existing +** checkout to a repository after the original repository +** file is moved. CAUTION: may lose stash and bisect +** history. +** --repodir DIR If REPOSITORY is a URI that will be cloned, store +** the clone in DIR rather than in "." +** --setmtime Set timestamps of all files to match their SCM-side +** times (the timestamp of the last check-in which modified +** them). +** --verbose If passed a URI then this flag is passed on to the clone +** operation, otherwise it has no effect +** --workdir DIR Use DIR as the working directory instead of ".". The DIR +** directory is created if it does not exist. +** +** See also: [[close]], [[clone]] +*/ +void cmd_open(void){ + int emptyFlag; + int keepFlag; + int forceMissingFlag; + int allowNested; + int setmtimeFlag; /* --setmtime. Set mtimes on files */ + int bForce = 0; /* --force. Open even if non-empty dir */ + static char *azNewArgv[] = { 0, "checkout", "--prompt", 0, 0, 0, 0 }; + const char *zWorkDir; /* --workdir value */ + const char *zRepo = 0; /* Name of the repository file */ + const char *zRepoDir = 0; /* --repodir value */ + const char *zReopen = 0; /* --reopen REPOFILE */ + char *zPwd; /* Initial working directory */ + int isUri = 0; /* True if REPOSITORY is a URI */ + int nLocal; /* Number of preexisting files in cwd */ + int bVerbose = 0; /* --verbose option for clone */ + + zReopen = find_option("reopen",0,1); + if( 0!=zReopen ){ + g.argc = 3; + g.argv[2] = (char*)zReopen; + move_repo_cmd(); + return; + } + + url_proxy_options(); + emptyFlag = find_option("empty",0,0)!=0; + keepFlag = find_option("keep","k",0)!=0; + forceMissingFlag = find_option("force-missing",0,0)!=0; + allowNested = find_option("nested",0,0)!=0; + setmtimeFlag = find_option("setmtime",0,0)!=0; + zWorkDir = find_option("workdir",0,1); + zRepoDir = find_option("repodir",0,1); + bForce = find_option("force","f",0)!=0; + if( find_option("nosync",0,0) ) g.fNoSync = 1; + bVerbose = find_option("verbose",0,0)!=0; + zPwd = file_getcwd(0,0); + + /* We should be done with options.. */ + verify_all_options(); + + if( g.argc!=3 && g.argc!=4 ){ + usage("REPOSITORY-FILENAME ?VERSION?"); + } + zRepo = g.argv[2]; + if( sqlite3_strglob("http://*", zRepo)==0 + || sqlite3_strglob("https://*", zRepo)==0 + || sqlite3_strglob("ssh:*", zRepo)==0 + || sqlite3_strglob("file:*", zRepo)==0 + ){ + isUri = 1; + } + + /* If --workdir is specified, change to the requested working directory */ + if( zWorkDir ){ + if( !isUri ){ + zRepo = file_canonical_name_dup(zRepo); + } + if( zRepoDir ){ + zRepoDir = file_canonical_name_dup(zRepoDir); + } + if( file_isdir(zWorkDir, ExtFILE)!=1 ){ + file_mkfolder(zWorkDir, ExtFILE, 0, 0); + if( file_mkdir(zWorkDir, ExtFILE, 0) ){ + fossil_fatal("cannot create directory %s", zWorkDir); + } + } + if( file_chdir(zWorkDir, 0) ){ + fossil_fatal("unable to make %s the working directory", zWorkDir); + } + } + if( keepFlag==0 + && bForce==0 + && (nLocal = file_directory_list(".", 0, 1, 2, 0))>0 + && (nLocal>1 || isUri || !file_in_cwd(zRepo)) + ){ + fossil_fatal("directory %s is not empty\n" + "use the -f (--force) option to override\n" + "or the -k (--keep) option to keep local files unchanged", + file_getcwd(0,0)); + } + + if( db_open_local_v2(0, allowNested) ){ + fossil_fatal("there is already an open tree at %s", g.zLocalRoot); + } + + /* If REPOSITORY looks like a URI, then try to clone it first */ + if( isUri ){ + char *zNewBase; /* Base name of the cloned repository file */ + const char *zUri; /* URI to clone */ + int rc; /* Result code from fossil_system() */ + Blob cmd; /* Clone command to be run */ + char *zCmd; /* String version of the clone command */ + + zUri = zRepo; + zNewBase = url_to_repo_basename(zUri); + if( zNewBase==0 ){ + fossil_fatal("unable to deduce a repository name from the url \"%s\"", + zUri); + } + if( zRepoDir==0 ){ + zRepo = mprintf("%s.fossil", zNewBase); + }else{ + zRepo = mprintf("%s/%s.fossil", zRepoDir, zNewBase); + } + fossil_free(zNewBase); + blob_init(&cmd, 0, 0); + blob_append_escaped_arg(&cmd, g.nameOfExe, 1); + blob_append(&cmd, " clone", -1); + if(0!=bVerbose){ + blob_append(&cmd, " --verbose", -1); + } + blob_append_escaped_arg(&cmd, zUri, 1); + blob_append_escaped_arg(&cmd, zRepo, 1); + zCmd = blob_str(&cmd); + fossil_print("%s\n", zCmd); + if( zWorkDir ) file_chdir(zPwd, 0); + rc = fossil_system(zCmd); + if( rc ){ + fossil_fatal("clone of %s failed", zUri); + } + blob_reset(&cmd); + if( zWorkDir ) file_chdir(zWorkDir, 0); + }else if( zRepoDir ){ + fossil_fatal("the --repodir option only makes sense if the REPOSITORY " + "argument is a URI that begins with http:, https:, ssh:, " + "or file:"); + } + + db_open_config(0,0); + db_open_repository(zRepo); + + /* Figure out which revision to open. */ + if( !emptyFlag ){ + if( g.argc==4 ){ + g.zOpenRevision = g.argv[3]; + }else if( db_exists("SELECT 1 FROM event WHERE type='ci'") ){ + g.zOpenRevision = fossil_strdup(db_main_branch()); + } + if( autosync_loop(SYNC_PULL, !bForce, "open") && !bForce ){ + fossil_fatal("unable to auto-sync the repository"); + } + } + + +#if defined(_WIN32) || defined(__CYGWIN__) +# define LOCALDB_NAME "./_FOSSIL_" +#else +# define LOCALDB_NAME "./.fslckout" +#endif + db_init_database(LOCALDB_NAME, zLocalSchema, zLocalSchemaVmerge, +#ifdef FOSSIL_LOCAL_WAL + "COMMIT; PRAGMA journal_mode=WAL; BEGIN;", +#endif + (char*)0); + db_delete_on_failure(LOCALDB_NAME); + db_open_local(0); + db_lset("repository", zRepo); + db_record_repository_filename(zRepo); + db_set_checkout(0, 0); /* manifest files handled by checkout_cmd */ + azNewArgv[0] = g.argv[0]; + g.argv = azNewArgv; + if( !emptyFlag ){ + g.argc = 3; + if( g.zOpenRevision ){ + azNewArgv[g.argc-1] = g.zOpenRevision; + }else{ + azNewArgv[g.argc-1] = "--latest"; + } + if( keepFlag ){ + azNewArgv[g.argc++] = "--keep"; + } + if( forceMissingFlag ){ + azNewArgv[g.argc++] = "--force-missing"; + } + checkout_cmd(); + } + if( setmtimeFlag ){ + int const vid = db_lget_int("checkout", 0); + if(vid!=0){ + vfile_check_signature(vid, CKSIG_SETMTIME); + } + } + g.argc = 2; + info_cmd(); +} + +/* +** Return true if pSetting has its default value assuming its +** current value is zVal. +*/ +int setting_has_default_value(const Setting *pSetting, const char *zVal){ + if( zVal==0 ) return 1; + if( pSetting->def==0 ) return 0; + if( pSetting->width==0 ){ + return is_false(pSetting->def)==is_false(zVal); + } + if( fossil_strcmp(pSetting->def, zVal)==0 ) return 1; + if( is_false(zVal) && is_false(pSetting->def) ) return 1; + if( is_truth(zVal) && is_truth(pSetting->def) ) return 1; + return 0; +} + +/* +** Print the current value of a setting identified by the pSetting +** pointer. +** +** Only show the value, not the setting name, if valueOnly is true. +** +** Show nothing if bIfChng is true and the setting is not currently set +** or is set to its default value. +*/ +void print_setting(const Setting *pSetting, int valueOnly, int bIfChng){ + Stmt q; + int versioned = 0; + if( pSetting->versionable && g.localOpen ){ + /* Check to see if this is overridden by a versionable settings file */ + Blob versionedPathname; + blob_zero(&versionedPathname); + blob_appendf(&versionedPathname, "%s.fossil-settings/%s", + g.zLocalRoot, pSetting->name); + if( file_size(blob_str(&versionedPathname), ExtFILE)>=0 ){ + versioned = 1; + } + blob_reset(&versionedPathname); + } + if( valueOnly && versioned ){ + const char *zVal = db_get_versioned(pSetting->name, NULL, NULL); + if( !bIfChng || (zVal!=0 && fossil_strcmp(zVal, pSetting->def)!=0) ){ + fossil_print("%s\n", db_get_versioned(pSetting->name, NULL, NULL)); + }else{ + versioned = 0; + } + return; + } + if( g.repositoryOpen ){ + db_prepare(&q, + "SELECT '(local)', value FROM config WHERE name=%Q" + " UNION ALL " + "SELECT '(global)', value FROM global_config WHERE name=%Q", + pSetting->name, pSetting->name + ); + }else{ + db_prepare(&q, + "SELECT '(global)', value FROM global_config WHERE name=%Q", + pSetting->name + ); + } + if( db_step(&q)==SQLITE_ROW ){ + const char *zVal = db_column_text(&q,1); + if( bIfChng && setting_has_default_value(pSetting,zVal) ){ + if( versioned ){ + fossil_print("%-24s (versioned)\n", pSetting->name); + versioned = 0; + } + }else if( valueOnly ){ + fossil_print("%s\n", db_column_text(&q, 1)); + }else{ + const char *zVal = (const char*)db_column_text(&q,1); + const char *zName = (const char*)db_column_text(&q,0); + if( zVal==0 ) zVal = "NULL"; + if( strchr(zVal,'\n')==0 ){ + fossil_print("%-24s %-11s %s\n", pSetting->name, zName, zVal); + }else{ + fossil_print("%-24s %-11s\n", pSetting->name, zName); + while( zVal[0] ){ + const char *zNL = strchr(zVal, '\n'); + if( zNL==0 ){ + fossil_print(" %s\n", zVal); + break; + }else{ + int n = (int)(zNL - zVal); + while( n>0 && fossil_isspace(zVal[n-1]) ){ n--; } + fossil_print(" %.*s\n", n, zVal); + zVal = zNL+1; + } + } + } + } + }else if( bIfChng ){ + /* Display nothing */ + versioned = 0; + }else if( valueOnly ){ + fossil_print("\n"); + }else{ + fossil_print("%-24s\n", pSetting->name); + } + if( versioned ){ + fossil_print(" (overridden by contents of file .fossil-settings/%s)\n", + pSetting->name); + } + db_finalize(&q); +} + +#if INTERFACE +/* +** Define all settings, which can be controlled via the set/unset +** command. +** +** var is the name of the internal configuration name for db_(un)set. +** If var is 0, the settings name is used. +** +** width is the length for the edit field on the behavior page, 0 is +** used for on/off checkboxes. A negative value indicates that the +** page should not render this setting. Such values may be rendered +** separately/manually on another page, e.g., /setup_access, and are +** exposed via the CLI settings command. +** +** The behaviour page doesn't use a special layout. It lists all +** set-commands and displays the 'set'-help as info. +*/ +struct Setting { + const char *name; /* Name of the setting */ + const char *var; /* Internal variable name used by db_set() */ + int width; /* Width of display. 0 for boolean values and + ** negative for values which should not appear + ** on the /setup_settings page. */ + char versionable; /* Is this setting versionable? */ + char forceTextArea; /* Force using a text area for display? */ + char sensitive; /* True if this a security-sensitive setting */ + const char *def; /* Default value */ +}; + +#endif /* INTERFACE */ + +/* +** SETTING: access-log boolean default=on +** +** When the access-log setting is enabled, all login attempts (successful +** and unsuccessful) on the web interface are recorded in the "access" table +** of the repository. +*/ +/* +** SETTING: admin-log boolean default=on +** +** When the admin-log setting is enabled, configuration changes are recorded +** in the "admin_log" table of the repository. +*/ +/* +** SETTING: allow-symlinks boolean default=off sensitive +** +** When allow-symlinks is OFF, Fossil does not see symbolic links +** (a.k.a "symlinks") on disk as a separate class of object. Instead Fossil +** sees the object that the symlink points to. Fossil will only manage files +** and directories, not symlinks. When a symlink is added to a repository, +** the object that the symlink points to is added, not the symlink itself. +** +** When allow-symlinks is ON, Fossil sees symlinks on disk as a separate +** object class that is distinct from files and directories. When a symlink +** is added to a repository, Fossil stores the target filename. In other +** words, Fossil stores the symlink itself, not the object that the symlink +** points to. +** +** Symlinks are not cross-platform. They are not available on all +** operating systems and file systems. Hence the allow-symlinks setting is +** OFF by default, for portability. +*/ +/* +** SETTING: auto-captcha boolean default=on variable=autocaptcha +** If enabled, the /login page provides a button that will automatically +** fill in the captcha password. This makes things easier for human users, +** at the expense of also making logins easier for malicious robots. +*/ +/* +** SETTING: auto-hyperlink width=16 default=1 +** +** If non-zero, enable hyperlinks on web pages even for users that lack +** the "h" privilege as long as the UserAgent string in the HTTP request +** (The HTTP_USER_AGENT cgi variable) looks like it comes from a human and +** not a robot. Details depend on the value of the setting. +** +** (0) Off: No adjustments are made to the 'h' privilege based on +** the user agent. +** +** (1) UserAgent and Javascript: The the href= values of hyperlinks +** initially point to /honeypot and are changed to point to the +** correct target by javascript that runs after the page loads. +** The auto-hyperlink-delay and auto-hyperlink-mouseover settings +** influence that javascript. +** +** (2) UserAgent only: If the HTTP_USER_AGENT looks human +** then generate hyperlinks, otherwise do not. +** +** Better robot exclusion is obtained when this setting is 1 versus 2. +** However, a value of 1 causes the visited/unvisited colors of hyperlinks +** to stop working on Safari-derived web browsers. When this setting is 2, +** the hyperlinks work better on Safari, but more robots are able to sneak +** in. +*/ +/* +** SETTING: auto-hyperlink-delay width=16 default=0 +** +** When the auto-hyperlink setting is 1, the javascript that runs to set +** the href= attributes of hyperlinks delays by this many milliseconds +** after the page load. Suggested values: 50 to 200. +*/ +/* +** SETTING: auto-hyperlink-mouseover boolean default=off +** +** When the auto-hyperlink setting is 1 and this setting is on, the +** javascript that runs to set the href= attributes of hyperlinks waits +** until either a mousedown or mousemove event is seen. This helps +** to distinguish real users from robots. For maximum robot defense, +** the recommended setting is ON. +*/ +/* +** SETTING: auto-shun boolean default=on +** If enabled, automatically pull the shunning list +** from a server to which the client autosyncs. +*/ +/* +** SETTING: autosync width=16 default=on +** This setting determines when autosync occurs. The setting is a +** string that provides a lot of flexibility for determining when and +** when not to autosync. Examples: +** +** on Always autosync for command where autosync +** makes sense ("commit", "merge", "open", "update") +** +** off Never autosync. +** +** pullonly Only to pull autosyncs +** +** all Sync with all remotes +** +** on,open=off Autosync for most commands, but not for "open" +** +** off,commit=pullonly Do not autosync, except do a pull before each +** "commit", presumably to avoid undesirable +** forks. +** +** The syntax is a comma-separated list of VALUE and COMMAND=VALUE entries. +** A plain VALUE entry is the default that is used if no COMMAND matches. +** Otherwise, the VALUE of the matching command is used. +** +** The "all" value is special in that it applies to the "sync" command in +** addition to "commit", "merge", "open", and "update". +*/ +/* +** SETTING: autosync-tries width=16 default=1 +** If autosync is enabled setting this to a value greater +** than zero will cause autosync to try no more than this +** number of attempts if there is a sync failure. +*/ +/* +** SETTING: backoffice-nodelay boolean default=off +** If backoffice-nodelay is true, then the backoffice processing +** will never invoke sleep(). If it has nothing useful to do, +** it simply exits. +*/ +/* +** SETTING: backoffice-disable boolean default=off +** If backoffice-disable is true, then the automatic backoffice +** processing is disabled. Automatic backoffice processing is the +** backoffice work that normally runs after each web page is +** rendered. Backoffice processing that is triggered by the +** "fossil backoffice" command is unaffected by this setting. +** +** Backoffice processing does things such as delivering +** email notifications. So if this setting is true, and if +** there is no cron job periodically running "fossil backoffice", +** email notifications and other work normally done by the +** backoffice will not occur. +*/ +/* +** SETTING: backoffice-logfile width=40 sensitive +** If backoffice-logfile is not an empty string and is a valid +** filename, then a one-line message is appended to that file +** every time the backoffice runs. This can be used for debugging, +** to ensure that backoffice is running appropriately. +*/ +/* +** SETTING: binary-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files +** that should be treated as "binary" for committing and merging +** purposes. Example: *.jpg,*.png The parsing rules are complex; +** see https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +*/ +#if defined(_WIN32)||defined(__CYGWIN__)||defined(__DARWIN__) +/* +** SETTING: case-sensitive boolean default=off +** If TRUE, the files whose names differ only in case +** are considered distinct. If FALSE files whose names +** differ only in case are the same file. Defaults to +** TRUE for unix and FALSE for Cygwin, Mac and Windows. +*/ +#endif +#if !(defined(_WIN32)||defined(__CYGWIN__)||defined(__DARWIN__)) +/* +** SETTING: case-sensitive boolean default=on +** If TRUE, the files whose names differ only in case +** are considered distinct. If FALSE files whose names +** differ only in case are the same file. Defaults to +** TRUE for unix and FALSE for Cygwin, Mac and Windows. +*/ +#endif +/* +** SETTING: clean-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files +** that the "clean" command will delete without prompting or allowing +** undo. Example: *.a,*.o,*.so The parsing rules are complex; +** see https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +*/ +/* +** SETTING: clearsign boolean default=off +** When enabled, fossil will attempt to sign all commits +** with gpg or ssh. When disabled, commits will be unsigned. +*/ +/* +** SETTING: comment-format width=16 default=1 +** Set the algorithm for printing timeline comments to the console. +** +** Possible values are: +** 1 Use the original comment printing algorithm: +** * Leading and trailing whitespace is removed +** * Internal whitespace is converted into a single space (0x20) +** * Line breaks occurs at whitespace or hyphens if possible +** This is the recommended value and the default. +** +** Or a bitwise combination of the following flags: +** 2 Trim leading and trailing CR and LF characters. +** 4 Trim leading and trailing white space characters. +** 8 Attempt to break lines on word boundaries. +** 16 Break lines before the original comment embedded in other text. +** +** Note: To preserve line breaks and/or other whitespace within comment text, +** make this setting some integer value that omits the "1" bit. +*/ +/* +** SETTING: crlf-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files +** in which it is allowed to have CR, CR+LF or mixed line endings, +** suppressing Fossil's normal warning about this. Set it to "*" to +** disable CR+LF checking entirely. Example: *.md,*.txt +** The crnl-glob setting is a compatibility alias. +*/ +/* +** SETTING: crnl-glob width=40 versionable block-text +** This is an alias for the crlf-glob setting. +*/ +/* +** SETTING: default-perms width=16 default=u sensitive keep-empty +** Permissions given automatically to new users. For more +** information on permissions see the Users page in Server +** Administration of the HTTP UI. +*/ +/* +** SETTING: diff-binary boolean default=on +** If enabled, permit files that may be binary +** or that match the "binary-glob" setting to be used with +** external diff programs. If disabled, skip these files. +*/ +/* +** SETTING: diff-command width=40 sensitive +** The value is an external command to run when performing a diff. +** If undefined, the internal text diff will be used. +*/ +/* +** SETTING: dont-commit boolean default=off +** If enabled, prevent committing to this repository, as an extra precaution +** against accidentally checking in to a repository intended to be read-only. +*/ +/* +** SETTING: dont-push boolean default=off +** If enabled, prevent this repository from pushing from client to +** server. This can be used as an extra precaution to prevent +** accidental pushes to a public server from a private clone. +*/ +/* +** SETTING: dotfiles boolean versionable default=off +** If enabled, include --dotfiles option for all compatible commands. +*/ +/* +** SETTING: editor width=32 sensitive +** The value is an external command that will launch the +** text editor command used for check-in comments. +** +** If this value is not set, then environment variables VISUAL and +** EDITOR are consulted, in that order. If neither of those are set, +** then a search is made for common text editors, including +** "notepad", "nano", "pico", "jove", "edit", "vi", "vim", and "ed". +** +** If this setting is false ("off", "no", "false", or "0") then no +** text editor is used. +*/ +/* +** SETTING: empty-dirs width=40 versionable block-text +** The value is a list of pathnames parsed according to the same rules as +** the *-glob settings. On update and checkout commands, if no directory +** exists with that name, an empty directory will be created, even if +** it must create one or more parent directories. +*/ +/* +** SETTING: encoding-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files that +** the "commit" command will ignore when issuing warnings about text files +** that may use another encoding than ASCII or UTF-8. Set to "*" to disable +** encoding checking. Example: *.md,*.txt The parsing rules are complex; +** see https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +*/ +#if defined(FOSSIL_ENABLE_EXEC_REL_PATHS) +/* +** SETTING: exec-rel-paths boolean default=on +** When executing certain external commands (e.g. diff and +** gdiff), use relative paths. +*/ +#endif +#if !defined(FOSSIL_ENABLE_EXEC_REL_PATHS) +/* +** SETTING: exec-rel-paths boolean default=off +** When executing certain external commands (e.g. diff and +** gdiff), use relative paths. +*/ +#endif + +/* +** SETTING: fileedit-glob width=40 block-text +** The VALUE of this setting is a list of GLOB patterns matching files +** which are allowed to be edited using the /fileedit page. An empty list +** suppresses the feature. Example: *.md,*.txt The parsing rules are +** complex; see https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +** Note that /fileedit cannot edit binary files, so the list should not +** contain any globs for, e.g., images or PDFs. +*/ +/* +** SETTING: forbid-delta-manifests boolean default=off +** If enabled on a client, new delta manifests are prohibited on +** commits. If enabled on a server, whenever a client attempts +** to obtain a check-in lock during auto-sync, the server will +** send the "pragma avoid-delta-manifests" statement in its reply, +** which will cause the client to avoid generating a delta +** manifest. +*/ +/* +** SETTING: gdiff-command width=40 sensitive +** The value is an external command to run when performing a graphical +** diff. If undefined, a --tk diff is done if commands "tclsh" and "wish" +** are on PATH, or a --by diff is done if "tclsh" or "wish" are unavailable. +*/ +/* +** SETTING: gmerge-command width=40 sensitive +** The value is a graphical merge conflict resolver command operating +** on four files. Examples: +** +** kdiff3 "%baseline" "%original" "%merge" -o "%output" +** xxdiff "%original" "%baseline" "%merge" -M "%output" +** meld "%baseline" "%original" "%merge" --output "%output" +*/ +/* +** SETTING: hash-digits width=5 default=10 +** The number of hexadecimal digits of the SHA3 hash to display. +*/ +/* +** SETTING: http-port width=16 default=8080 +** The default TCP/IP port number to use by the "server" +** and "ui" commands. +*/ +/* +** SETTING: https-login boolean default=off +** If true, then the Fossil web server will redirect unencrypted +** login screen requests to HTTPS. +*/ +/* +** SETTING: ignore-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files that +** the "add", "addremove", "clean", and "extras" commands will ignore. +** Example: *.log,notes.txt The parsing rules are complex; see +** https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +*/ +/* +** SETTING: keep-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files that +** the "clean" command must not delete. Example: build/precious.exe +** The parsing rules are complex; see +** https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +*/ +/* +** SETTING: localauth boolean default=off +** If enabled, require that HTTP connections from the loopback +** address (127.0.0.1) be authenticated by password. If false, +** some HTTP requests might be granted full "Setup" user +** privileges without having to present login credentials. +** This mechanism allows the "fossil ui" command to provide +** full access to the repository without requiring the user to +** log in first. +** +** In order for full "Setup" privilege to be granted without a +** login, the following conditions must be met: +** +** (1) This setting ("localauth") must be off +** (2) The HTTP request arrive over the loopback TCP/IP +** address (127.0.01) or else via SSH. +** (3) The request must be HTTP, not HTTPS. (This +** restriction is designed to help prevent accidentally +** providing "Setup" privileges to requests arriving +** over a reverse proxy.) +** (4) The command that launched the fossil server must be +** one of the following: +** (a) "fossil ui" +** (b) "fossil server" with the --localauth option +** (c) "fossil http" with the --localauth option +** (d) CGI with the "localauth" setting in the cgi script. +** +** For maximum security, set "localauth" to 1. However, because +** of the other restrictions (2) through (4), it should be safe +** to leave "localauth" set to 0 in most installations, and +** especially on cloned repositories on workstations. Leaving +** "localauth" at 0 makes the "fossil ui" command more convenient +** to use. +*/ +/* +** SETTING: lock-timeout width=25 default=60 +** This is the number of seconds that a check-in lock will be held on +** the server before the lock expires. The default is a 60-second delay. +** Set this value to zero to disable the check-in lock mechanism. +** +** This value should be set on the server to which users auto-sync +** their work. This setting has no effect on client repositories. The +** check-in lock mechanism is only effective if all users are auto-syncing +** to the same server. +** +** Check-in locks are an advisory mechanism designed to help prevent +** accidental forks due to a check-in race in installations where many +** users are committing to the same branch and auto-sync is enabled. +** As forks are harmless, there is no danger in disabling this mechanism. +** However, keeping check-in locks turned on can help prevent unnecessary +** confusion. +*/ +/* +** SETTING: main-branch width=40 default=trunk +** The value is the primary branch for the project. +*/ +/* +** SETTING: manifest width=5 versionable +** If enabled, automatically create files "manifest" and "manifest.uuid" +** in every check-out. +** +** Optionally use combinations of characters 'r' for "manifest", +** 'u' for "manifest.uuid" and 't' for "manifest.tags". The SQLite +** and Fossil repositories both require manifests. +*/ +/* +** SETTING: max-loadavg width=25 default=0.0 +** Some CPU-intensive web pages (ex: /zip, /tarball, /blame) +** are disallowed if the system load average goes above this +** value. "0.0" means no limit. This only works on unix. +** Only local settings of this value make a difference since +** when running as a web-server, Fossil does not open the +** global configuration database. +*/ +/* +** SETTING: max-upload width=25 default=250000 +** A limit on the size of uplink HTTP requests. +*/ +/* +** SETTING: mimetypes width=40 versionable block-text +** A list of file extension-to-mimetype mappings, one per line. e.g. +** "foo application/x-foo". File extensions are compared +** case-insensitively in the order listed in this setting. A leading +** '.' on file extensions is permitted but not required. +*/ +/* +** SETTING: mtime-changes boolean default=on +** Use file modification times (mtimes) to detect when +** files have been modified. If disabled, all managed files +** are hashed to detect changes, which can be slow for large +** projects. +*/ +/* +** SETTING: mv-rm-files boolean default=off +** If enabled, the "mv" and "rename" commands will also move +** the associated files within the check-out -AND- the "rm" +** and "delete" commands will also remove the associated +** files from within the check-out. +*/ +/* +** SETTING: pgp-command width=40 sensitive +** Command used to clear-sign manifests at check-in. +** Default value is "gpg --clearsign -o". +** For SSH, use e.g. "ssh-keygen -q -Y sign -n fossilscm -f ~/.ssh/id_ed25519" +*/ +/* +** SETTING: proxy width=32 default=system +** URL of the HTTP proxy. If undefined or "system", the "http_proxy" +** environment variable is consulted. If "off", a direct HTTP connection is +** used. +*/ +/* +** SETTING: redirect-to-https default=0 width=2 +** Specifies whether or not to redirect unencrypted "http://" requests to +** encrypted "https://" URIs. A value of 0 (the default) means do not +** redirect, 1 means to redirect only the /login page, and 2 +** means to always redirect. +** +** For security, a value of 2 is recommended. The default value is 0 +** because not all sites are TLS-capable. But you should definitely enable +** TLS and change this setting to 2 for all public-facing repositories. +*/ +/* +** SETTING: relative-paths boolean default=on +** When showing changes and extras, report paths relative +** to the current working directory. +*/ +/* +** SETTING: repo-cksum boolean default=on +** Compute checksums over all files in each check-out as a double-check +** of correctness. Disable this on large repositories for a performance +** improvement. +*/ +/* +** SETTING: repolist-skin width=2 default=0 +** If non-zero then use this repository as the skin for a repository list +** such as created by the one of: +** +** 1) fossil server DIRECTORY --repolist +** 2) fossil ui DIRECTORY --repolist +** 3) fossil http DIRECTORY --repolist +** 4) (The "repolist" option in a CGI script) +** 5) fossil all ui +** 6) fossil all server +** +** All repositories are searched (in lexicographical order) and the first +** repository with a non-zero "repolist-skin" value is used as the skin +** for the repository list page. If none of the repositories on the list +** have a non-zero "repolist-skin" setting then the repository list is +** displayed using unadorned HTML ("skinless"), with the page title taken +** from the FOSSIL_REPOLIST_TITLE environment variable. +** +** If repolist-skin has a value of 2, then the repository is omitted from +** the list in use cases 1 through 4, but not for 5 and 6. +*/ +/* +** SETTING: self-pw-reset boolean default=off sensitive +** Allow users to request that an email containing a hyperlink +** to the /resetpw page be sent to their email address of record, +** thus allowing forgetful users to reset their forgotten passwords +** without administrator involvement. +*/ +/* +** SETTING: self-register boolean default=off sensitive +** Allow users to register themselves through the HTTP UI. +** This is useful if you want to see other names than +** "Anonymous" in e.g. ticketing system. On the other hand +** users can not be deleted. +*/ +/* +** SETTING: ssh-command width=40 sensitive +** The command used to talk to a remote machine with the "ssh://" protocol. +*/ + +/* +** SETTING: ssl-ca-location width=40 sensitive +** The full pathname to a file containing PEM encoded +** CA root certificates, or a directory of certificates +** with filenames formed from the certificate hashes as +** required by OpenSSL. +** +** If set, this will override the OS default list of +** OpenSSL CAs. If unset, the default list will be used. +** Some platforms may add additional certificates. +** Checking your platform behaviour is required if the +** exact contents of the CA root is critical for your +** application. +** +** This setting is overridden by environment variables +** SSL_CERT_FILE and SSL_CERT_DIR. +*/ +/* +** SETTING: ssl-identity width=40 sensitive +** The full pathname to a file containing a certificate +** and private key in PEM format. Create by concatenating +** the certificate and private key files. +** +** This identity will be presented to SSL servers to +** authenticate this client, in addition to the normal +** password authentication. +*/ +#ifdef FOSSIL_ENABLE_TCL +/* +** SETTING: tcl boolean default=off sensitive +** If enabled Tcl integration commands will be added to the TH1 +** interpreter, allowing arbitrary Tcl expressions and +** scripts to be evaluated from TH1. Additionally, the Tcl +** interpreter will be able to evaluate arbitrary TH1 +** expressions and scripts. +*/ +/* +** SETTING: tcl-setup width=40 block-text sensitive +** This is the setup script to be evaluated after creating +** and initializing the Tcl interpreter. By default, this +** is empty and no extra setup is performed. +*/ +#endif /* FOSSIL_ENABLE_TCL */ +/* +** SETTING: tclsh width=80 default=tclsh sensitive +** Name of the external TCL interpreter used for such things +** as running the GUI diff viewer launched by the --tk option +** of the various "diff" commands. +*/ +#ifdef FOSSIL_ENABLE_TH1_DOCS +/* +** SETTING: th1-docs boolean default=off sensitive +** If enabled, this allows embedded documentation files to contain +** arbitrary TH1 scripts that are evaluated on the server. If native +** Tcl integration is also enabled, this setting has the +** potential to allow anybody with check-in privileges to +** do almost anything that the associated operating system +** user account could do. Extreme caution should be used +** when enabling this setting. +*/ +#endif +#ifdef FOSSIL_ENABLE_TH1_HOOKS +/* +** SETTING: th1-hooks boolean default=off +** If enabled, special TH1 commands will be called before and +** after any Fossil command or web page. +*/ +#endif +/* +** SETTING: th1-setup width=40 block-text sensitive +** This is the setup script to be evaluated after creating +** and initializing the TH1 interpreter. By default, this +** is empty and no extra setup is performed. +*/ +/* +** SETTING: th1-uri-regexp width=40 block-text +** Specify which URI's are allowed in HTTP requests from +** TH1 scripts. If empty, no HTTP requests are allowed +** whatsoever. +*/ +/* +** SETTING: default-csp width=40 block-text keep-empty +** +** The text of the Content Security Policy that is included +** in the Content-Security-Policy: header field of the HTTP +** reply and in the default HTML <head> section that is added when the +** skin header does not specify a <head> section. The text "$nonce" +** is replaced by the random nonce that is created for each web page. +** +** If this setting is an empty string or is omitted, then +** the following default Content Security Policy is used: +** +** default-src 'self' data:; +** script-src 'self' 'nonce-$nonce'; +** style-src 'self' 'unsafe-inline'; +** img-src * data:; +** +** The default CSP is recommended. The main reason to change +** this setting would be to add CDNs from which it is safe to +** load additional content. +*/ +/* +** SETTING: uv-sync boolean default=off +** If true, automatically send unversioned files as part +** of a "fossil clone" or "fossil sync" command. The +** default is false, in which case the -u option is +** needed to clone or sync unversioned files. +*/ +/* +** SETTING: web-browser width=30 sensitive +** A shell command used to launch your preferred +** web browser when given a URL as an argument. +** Defaults to "start" on windows, "open" on Mac, +** and "firefox" on Unix. +*/ +/* +** SETTING: large-file-size width=10 default=200000000 +** Fossil considers any file whose size is greater than this value +** to be a "large file". Fossil might issue warnings if you try to +** "add" or "commit" a "large file". Set this value to 0 or less +** to disable all such warnings. +*/ + +/* +** Look up a control setting by its name. Return a pointer to the Setting +** object, or NULL if there is no such setting. +** +** If allowPrefix is true, then the Setting returned is the first one for +** which zName is a prefix of the Setting name. +*/ +Setting *db_find_setting(const char *zName, int allowPrefix){ + int lwr, mid, upr, c; + int n = (int)strlen(zName) + !allowPrefix; + int nSetting; + const Setting *aSetting = setting_info(&nSetting); + lwr = 0; + upr = nSetting - 1; + while( upr>=lwr ){ + mid = (upr+lwr)/2; + c = fossil_strncmp(zName, aSetting[mid].name, n); + if( c<0 ){ + upr = mid - 1; + }else if( c>0 ){ + lwr = mid + 1; + }else{ + if( allowPrefix ){ + while( mid>lwr && fossil_strncmp(zName, aSetting[mid-1].name, n)==0 ){ + mid--; + } + } + return (Setting*)&aSetting[mid]; + } + } + return 0; +} + +/* +** COMMAND: settings +** COMMAND: unset* +** +** Usage: %fossil settings ?SETTING? ?VALUE? ?OPTIONS? +** or: %fossil unset SETTING ?OPTIONS? +** +** The "settings" command with no arguments lists all settings and their +** values. With just a SETTING name it shows the current value of that setting. +** With a VALUE argument it changes the property for the current repository. +** +** Settings marked as versionable are overridden by the contents of the +** file named .fossil-settings/PROPERTY in the check-out root, if that +** file exists. +** +** The "unset" command clears a setting. +** +** Settings can have both a "local" repository-only value and "global" value +** that applies to all repositories. The local values are stored in the +** "config" table of the repository and the global values are stored in the +** configuration database. If both a local and a global value exists for a +** setting, the local value takes precedence. This command normally operates +** on the local settings. Use the --global option to change global settings. +** +** Options: +** --changed Only show settings if the value differs from the default +** --exact Only consider exact name matches +** --global Set or unset the given property globally instead of +** setting or unsetting it for the open repository only +** --value Only show the value of a given property (implies --exact) +** +** See also: [[configuration]] +*/ +void setting_cmd(void){ + int i; + int globalFlag = find_option("global","g",0)!=0; + int bIfChng = find_option("changed",0,0)!=0; + int exactFlag = find_option("exact",0,0)!=0; + int valueFlag = find_option("value",0,0)!=0; + /* Undocumented "--test-for-subsystem SUBSYS" option used to test + ** the db_get_for_subsystem() interface: */ + const char *zSubsys = find_option("test-for-subsystem",0,1); + int unsetFlag = g.argv[1][0]=='u'; + int nSetting; + const Setting *aSetting = setting_info(&nSetting); + find_repository_option(); + verify_all_options(); + db_open_config(1, 0); + if( !globalFlag ){ + db_find_and_open_repository(OPEN_ANY_SCHEMA | OPEN_OK_NOT_FOUND, 0); + } + if( !g.repositoryOpen ){ + globalFlag = 1; + } + if( unsetFlag && g.argc!=3 ){ + usage("PROPERTY ?-global?"); + } + if( valueFlag ){ + if( g.argc!=3 ){ + fossil_fatal("--value is only supported when qurying a given property"); + } + } + + if( g.argc==2 ){ + for(i=0; i<nSetting; i++){ + print_setting(&aSetting[i], 0, bIfChng); + } + }else if( g.argc==3 || g.argc==4 ){ + const char *zName = g.argv[2]; + int n = (int)strlen(zName); + const Setting *pSetting = db_find_setting(zName, !exactFlag); + if( pSetting==0 ){ + fossil_fatal("no such setting: %s", zName); + } + if( globalFlag && fossil_strcmp(pSetting->name, "manifest")==0 ){ + fossil_fatal("cannot set 'manifest' globally"); + } + if( unsetFlag || g.argc==4 ){ + int isManifest = fossil_strcmp(pSetting->name, "manifest")==0; + if( n!=(int)strlen(pSetting[0].name) && pSetting[1].name && + fossil_strncmp(pSetting[1].name, zName, n)==0 ){ + Blob x; + int i; + blob_init(&x,0,0); + for(i=0; pSetting[i].name; i++){ + if( fossil_strncmp(pSetting[i].name,zName,n)!=0 ) break; + blob_appendf(&x, " %s", pSetting[i].name); + } + fossil_fatal("ambiguous setting \"%s\" - might be:%s", + zName, blob_str(&x)); + } + if( globalFlag && isManifest ){ + fossil_fatal("cannot set 'manifest' globally"); + } + if( unsetFlag ){ + db_unset(pSetting->name/*works-like:"x"*/, globalFlag); + }else{ + db_protect_only(PROTECT_NONE); + db_set(pSetting->name/*works-like:"x"*/, g.argv[3], globalFlag); + db_protect_pop(); + } + if( isManifest && g.localOpen ){ + manifest_to_disk(db_lget_int("checkout", 0)); + } + }else{ + while( pSetting->name ){ + if( exactFlag ){ + if( fossil_strcmp(pSetting->name,zName)!=0 ) break; + }else{ + if( fossil_strncmp(pSetting->name,zName,n)!=0 ) break; + } + if( zSubsys ){ + char *zValue = db_get_for_subsystem(pSetting->name, zSubsys); + fossil_print("%s (subsystem %s) ->", pSetting->name, zSubsys); + if( zValue ){ + fossil_print(" [%s]", zValue); + fossil_free(zValue); + } + fossil_print("\n"); + }else{ + print_setting(pSetting, valueFlag, bIfChng); + } + pSetting++; + } + } + }else{ + usage("?PROPERTY? ?VALUE? ?-global?"); + } +} + +/* +** The input in a timespan measured in days. Return a string which +** describes that timespan in units of seconds, minutes, hours, days, +** or years, depending on its duration. +*/ +char *db_timespan_name(double rSpan){ + if( rSpan<0 ) rSpan = -rSpan; + rSpan *= 24.0*3600.0; /* Convert units to seconds */ + if( rSpan<120.0 ){ + return sqlite3_mprintf("%.1f seconds", rSpan); + } + rSpan /= 60.0; /* Convert units to minutes */ + if( rSpan<90.0 ){ + return sqlite3_mprintf("%.1f minutes", rSpan); + } + rSpan /= 60.0; /* Convert units to hours */ + if( rSpan<=48.0 ){ + return sqlite3_mprintf("%.1f hours", rSpan); + } + rSpan /= 24.0; /* Convert units to days */ + if( rSpan<=365.0 ){ + return sqlite3_mprintf("%.1f days", rSpan); + } + rSpan /= 356.24; /* Convert units to years */ + return sqlite3_mprintf("%.1f years", rSpan); +} + +/* +** COMMAND: test-timespan +** +** Usage: %fossil test-timespan TIMESTAMP +** +** Print the approximate span of time from now to TIMESTAMP. +*/ +void test_timespan_cmd(void){ + double rDiff; + if( g.argc!=3 ) usage("TIMESTAMP"); + sqlite3_open(":memory:", &g.db); + rDiff = db_double(0.0, "SELECT julianday('now') - julianday(%Q)", g.argv[2]); + fossil_print("Time differences: %s\n", db_timespan_name(rDiff)); + sqlite3_close(g.db); + g.db = 0; + g.repositoryOpen = 0; + g.localOpen = 0; +} + +/* +** COMMAND: test-without-rowid +** +** Usage: %fossil test-without-rowid FILENAME... +** +** Change the Fossil repository FILENAME to make use of the WITHOUT ROWID +** optimization. FILENAME can also be the configuration database file +** (~/.fossil or ~/.config/fossil.db) or a local .fslckout or _FOSSIL_ file. +** +** The purpose of this command is for testing the WITHOUT ROWID capabilities +** of SQLite. There is no big advantage to using WITHOUT ROWID in Fossil. +** +** Options: +** -n|--dry-run No changes. Just print what would happen. +*/ +void test_without_rowid(void){ + int i, j; + Stmt q; + Blob allSql; + int dryRun = find_option("dry-run", "n", 0)!=0; + for(i=2; i<g.argc; i++){ + db_open_or_attach(g.argv[i], "main"); + blob_init(&allSql, "BEGIN;\n", -1); + db_prepare(&q, + "SELECT name, sql FROM main.sqlite_schema " + " WHERE type='table' AND sql NOT LIKE '%%WITHOUT ROWID%%'" + " AND name IN ('global_config','shun','concealed','config'," + " 'plink','tagxref','backlink','vcache');" + ); + while( db_step(&q)==SQLITE_ROW ){ + const char *zTName = db_column_text(&q, 0); + const char *zOrigSql = db_column_text(&q, 1); + Blob newSql; + blob_init(&newSql, 0, 0); + for(j=0; zOrigSql[j]; j++){ + if( fossil_strnicmp(zOrigSql+j,"unique",6)==0 ){ + blob_append(&newSql, zOrigSql, j); + blob_append(&newSql, "PRIMARY KEY", -1); + zOrigSql += j+6; + j = -1; + } + } + blob_append(&newSql, zOrigSql, -1); + blob_append_sql(&allSql, + "ALTER TABLE \"%w\" RENAME TO \"x_%w\";\n" + "%s WITHOUT ROWID;\n" + "INSERT INTO \"%w\" SELECT * FROM \"x_%w\";\n" + "DROP TABLE \"x_%w\";\n", + zTName, zTName, blob_sql_text(&newSql), zTName, zTName, zTName + ); + fossil_print("Converting table %s of %s to WITHOUT ROWID.\n", + zTName, g.argv[i]); + blob_reset(&newSql); + } + blob_append_sql(&allSql, "COMMIT;\n"); + db_finalize(&q); + if( dryRun ){ + fossil_print("SQL that would have been evaluated:\n"); + fossil_print("%.78c\n", '-'); + fossil_print("%s", blob_sql_text(&allSql)); + }else{ + db_multi_exec("%s", blob_sql_text(&allSql)); + } + blob_reset(&allSql); + db_close(1); + } +} + +/* +** Make sure the adminlog table exists. Create it if it does not +*/ +void create_admin_log_table(void){ + static int once = 0; + if( once ) return; + if( !db_table_exists("repository","admin_log") ){ + once = 1; + db_multi_exec( + "CREATE TABLE repository.admin_log(\n" + " id INTEGER PRIMARY KEY,\n" + " time INTEGER, -- Seconds since 1970\n" + " page TEXT, -- path of page\n" + " who TEXT, -- User who made the change\n" + " what TEXT -- What changed\n" + ")" + ); + } +} + +/* +** Write a message into the admin_event table, if admin logging is +** enabled via the admin-log configuration option. +*/ +void admin_log(const char *zFormat, ...){ + Blob what = empty_blob; + va_list ap; + if( !db_get_boolean("admin-log", 0) ){ + /* Potential leak here (on %z params) but + the alternative is to let blob_vappendf() + do it below. */ + return; + } + create_admin_log_table(); + va_start(ap,zFormat); + blob_vappendf( &what, zFormat, ap ); + va_end(ap); + db_multi_exec("INSERT INTO admin_log(time,page,who,what)" + " VALUES(now(), %Q, %Q, %B)", + g.zPath, g.zLogin, &what); + blob_reset(&what); +} + +/* +** COMMAND: test-database-names +** +** Print the names of the various database files: +** (1) The main repository database +** (2) The local check-out database +** (3) The global configuration database +*/ +void test_database_name_cmd(void){ + db_find_and_open_repository(OPEN_ANY_SCHEMA, 0); + fossil_print("Repository database: %s\n", g.zRepositoryName); + fossil_print("Local database: %s\n", g.zLocalDbName); + fossil_print("Config database: %s\n", g.zConfigDbName); +} + +/* +** Compute a "fingerprint" on the repository. A fingerprint is used +** to verify that the repository has not been replaced by a clone +** of the same repository. More precisely, a fingerprint is used to +** verify that the mapping between SHA3 hashes and RID values is unchanged. +** +** The check-out database ("localdb") stores RID values. When associating +** a check-out database against a repository database, it is useful to verify +** the fingerprint so that we know that the RID values in the check-out +** database still correspond to the correct entries in the BLOB table of +** the repository. +** +** The fingerprint is based on the RCVFROM table. When constructing a +** new fingerprint, use the most recent RCVFROM entry. (Set rcvid==0 to +** accomplish this.) When verifying an old fingerprint, use the same +** RCVFROM entry that generated the fingerprint in the first place. +** +** The fingerprint consists of the rcvid, a "/", and the MD5 checksum of +** the remaining fields of the RCVFROM table entry. MD5 is used for this +** because it is 4x faster than SHA3 and 5x faster than SHA1, and there +** are no security concerns - this is just a checksum, not a security +** token. +*/ +char *db_fingerprint(int rcvid, int iVersion){ + char *z = 0; + Blob sql = BLOB_INITIALIZER; + Stmt q; + if( iVersion==0 ){ + /* The original fingerprint algorithm used "quote(mtime)". But this + ** could give slightly different answers depending on how the floating- + ** point hardware is configured. For example, it gave different + ** answers on native Linux versus running under valgrind. */ + blob_append_sql(&sql, + "SELECT rcvid, quote(uid), quote(mtime), quote(nonce), quote(ipaddr)" + " FROM rcvfrom" + ); + }else{ + /* These days, we use "datetime(mtime)" for more consistent answers */ + blob_append_sql(&sql, + "SELECT rcvid, quote(uid), datetime(mtime), quote(nonce), quote(ipaddr)" + " FROM rcvfrom" + ); + } + if( rcvid<=0 ){ + blob_append_sql(&sql, " ORDER BY rcvid DESC LIMIT 1"); + }else{ + blob_append_sql(&sql, " WHERE rcvid=%d", rcvid); + } + db_prepare_blob(&q, &sql); + blob_reset(&sql); + if( db_step(&q)==SQLITE_ROW ){ + int i; + md5sum_init(); + for(i=1; i<=4; i++){ + md5sum_step_text(db_column_text(&q,i),-1); + } + z = mprintf("%d/%s",db_column_int(&q,0),md5sum_finish(0)); + } + db_finalize(&q); + return z; +} + +/* +** COMMAND: test-fingerprint +** +** Usage: %fossil test-fingerprint ?RCVID? +** +** Display the repository fingerprint using the supplied RCVID or +** using the latest RCVID if none is given on the command line. +** Show both the legacy and the newer version of the fingerprint, +** and the currently stored fingerprint if there is one. +*/ +void test_fingerprint(void){ + int rcvid = 0; + db_find_and_open_repository(OPEN_ANY_SCHEMA,0); + if( g.argc==3 ){ + rcvid = atoi(g.argv[2]); + }else if( g.argc!=2 ){ + fossil_fatal("wrong number of arguments"); + } + fossil_print("legacy: %z\n", db_fingerprint(rcvid, 0)); + fossil_print("version-1: %z\n", db_fingerprint(rcvid, 1)); + if( g.localOpen ){ + fossil_print("localdb: %z\n", db_lget("fingerprint","(none)")); + fossil_print("db_fingerprint_ok(): %d\n", db_fingerprint_ok()); + } + fossil_print("Fossil version: %s - %.10s %.19s\n", + RELEASE_VERSION, MANIFEST_DATE, MANIFEST_UUID); +} + +/* +** Set the value of the "checkout" entry in the VVAR table. +** If bWriteManifest is non-zero then also attempt to write the manifest +** files to disk. +** +** Also set "fingerprint" and "checkout-hash". +*/ +void db_set_checkout(int rid, int bWriteManifest){ + char *z; + if( bWriteManifest ) manifest_to_disk(rid); + db_lset_int("checkout", rid); + if (rid != 0) { + z = db_text(0,"SELECT uuid FROM blob WHERE rid=%d",rid); + db_lset("checkout-hash", z); + fossil_free(z); + z = db_fingerprint(0, 1); + db_lset("fingerprint", z); + fossil_free(z); + } +} + +/* +** Verify that the fingerprint recorded in the "fingerprint" entry +** of the VVAR table matches the fingerprint on the currently +** connected repository. Return true if the fingerprint is ok, and +** return false if the fingerprint does not match. +*/ +int db_fingerprint_ok(void){ + char *zCkout; /* The fingerprint recorded in the check-out database */ + char *zRepo; /* The fingerprint of the repository */ + int rc; /* Result */ + + if( !db_lget_int("checkout", 0) ){ + /* We have an empty check-out, fingerprint is still NULL. */ + return 2; + } + zCkout = db_text(0,"SELECT value FROM localdb.vvar WHERE name='fingerprint'"); + if( zCkout==0 ){ + /* This is an older check-out that does not record a fingerprint. + ** We have to assume everything is ok */ + return 2; + } + zRepo = db_fingerprint(atoi(zCkout), 1); + rc = fossil_strcmp(zCkout,zRepo)==0; + fossil_free(zRepo); + /* If the initial test fails, try again using the older fingerprint + ** algorithm */ + if( !rc ){ + zRepo = db_fingerprint(atoi(zCkout), 0); + rc = fossil_strcmp(zCkout,zRepo)==0; + fossil_free(zRepo); + } + fossil_free(zCkout); + return rc; +} + +/* +** Adds the given rid to the UNSENT table. +*/ +void db_add_unsent(int rid){ + db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", rid); +} |
