// Copyright 2022 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "sql/sqlite_result_code.h" #include // Needed to compile NOTREACHED() with operator <<. #include #include #include "base/check_op.h" #include "base/metrics/histogram_functions.h" #include "base/notreached.h" #include "base/ranges/algorithm.h" #include "base/strings/string_piece.h" #include "sql/sqlite_result_code_values.h" #include "third_party/sqlite/sqlite3.h" namespace sql { namespace { // The highly packed representation minimizes binary size and memory usage. struct SqliteResultCodeMappingEntry { unsigned result_code : 16; unsigned logged_code : 8; // The remaining bits will be used to encode the result values of helpers that // indicate corruption handling. }; constexpr SqliteResultCodeMappingEntry kResultCodeMapping[] = { // Entries are ordered by SQLite result code value. This should match the // ordering in https://www.sqlite.org/rescode.html. {SQLITE_OK, static_cast(SqliteLoggedResultCode::kNoError)}, {SQLITE_ERROR, static_cast(SqliteLoggedResultCode::kGeneric)}, {SQLITE_INTERNAL, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, {SQLITE_PERM, static_cast(SqliteLoggedResultCode::kPermission)}, {SQLITE_ABORT, static_cast(SqliteLoggedResultCode::kAbort)}, {SQLITE_BUSY, static_cast(SqliteLoggedResultCode::kBusy)}, // Chrome features shouldn't execute conflicting statements concurrently. {SQLITE_LOCKED, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, // Chrome should crash on OOM. {SQLITE_NOMEM, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_READONLY, static_cast(SqliteLoggedResultCode::kReadOnly)}, // Chrome doesn't use sqlite3_interrupt(). {SQLITE_INTERRUPT, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_IOERR, static_cast(SqliteLoggedResultCode::kIo)}, {SQLITE_CORRUPT, static_cast(SqliteLoggedResultCode::kCorrupt)}, // Chrome only passes a few known-good opcodes to sqlite3_file_control(). {SQLITE_NOTFOUND, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_FULL, static_cast(SqliteLoggedResultCode::kFullDisk)}, {SQLITE_CANTOPEN, static_cast(SqliteLoggedResultCode::kCantOpen)}, {SQLITE_PROTOCOL, static_cast(SqliteLoggedResultCode::kLockingProtocol)}, {SQLITE_EMPTY, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, {SQLITE_SCHEMA, static_cast(SqliteLoggedResultCode::kSchemaChanged)}, {SQLITE_TOOBIG, static_cast(SqliteLoggedResultCode::kTooBig)}, {SQLITE_CONSTRAINT, static_cast(SqliteLoggedResultCode::kConstraint)}, {SQLITE_MISMATCH, static_cast(SqliteLoggedResultCode::kTypeMismatch)}, // Chrome should not misuse SQLite's API. {SQLITE_MISUSE, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_NOLFS, static_cast(SqliteLoggedResultCode::kNoLargeFileSupport)}, // Chrome does not set an authorizer callback. {SQLITE_AUTH, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_FORMAT, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, // Chrome should not use invalid column indexes in sqlite3_{bind,column}*(). {SQLITE_RANGE, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_NOTADB, static_cast(SqliteLoggedResultCode::kNotADatabase)}, {SQLITE_NOTICE, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, {SQLITE_WARNING, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, {SQLITE_ROW, static_cast(SqliteLoggedResultCode::kNoError)}, {SQLITE_DONE, static_cast(SqliteLoggedResultCode::kNoError)}, {SQLITE_OK_LOAD_PERMANENTLY, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, // Chrome should not use collating sequence names in SQL statements. {SQLITE_ERROR_MISSING_COLLSEQ, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_BUSY_RECOVERY, static_cast(SqliteLoggedResultCode::kBusyRecovery)}, // Chrome does not use a shared page cache. {SQLITE_LOCKED_SHAREDCACHE, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_READONLY_RECOVERY, static_cast(SqliteLoggedResultCode::kReadOnlyRecovery)}, {SQLITE_IOERR_READ, static_cast(SqliteLoggedResultCode::kIoRead)}, // Chrome does not use a virtual table that signals corruption. We only use // a // virtual table code for recovery. That code does not use this error. {SQLITE_CORRUPT_VTAB, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_CANTOPEN_NOTEMPDIR, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, {SQLITE_CONSTRAINT_CHECK, static_cast(SqliteLoggedResultCode::kConstraintCheck)}, // Chrome does not set an authorizer callback. {SQLITE_AUTH_USER, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_NOTICE_RECOVER_WAL, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, {SQLITE_WARNING_AUTOINDEX, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, {SQLITE_ERROR_RETRY, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, {SQLITE_ABORT_ROLLBACK, static_cast(SqliteLoggedResultCode::kAbortRollback)}, {SQLITE_BUSY_SNAPSHOT, static_cast(SqliteLoggedResultCode::kBusySnapshot)}, // Chrome does not use a virtual table that signals conflicts. We only use a // virtual table code for recovery. That code does not use this error. {SQLITE_LOCKED_VTAB, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_READONLY_CANTLOCK, static_cast(SqliteLoggedResultCode::kReadOnlyCantLock)}, {SQLITE_IOERR_SHORT_READ, static_cast(SqliteLoggedResultCode::kIoShortRead)}, {SQLITE_CORRUPT_SEQUENCE, static_cast(SqliteLoggedResultCode::kCorruptSequence)}, {SQLITE_CANTOPEN_ISDIR, static_cast(SqliteLoggedResultCode::kCantOpenIsDir)}, // Chrome does not use commit hook callbacks. {SQLITE_CONSTRAINT_COMMITHOOK, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_NOTICE_RECOVER_ROLLBACK, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, // Chrome does not use sqlite3_snapshot_open(). {SQLITE_ERROR_SNAPSHOT, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, #ifdef SQLITE_ENABLE_SNAPSHOT #error "This code assumes that Chrome does not use sqlite3_snapshot_open()" #endif // Chrome does not use blocking Posix advisory file lock requests. {SQLITE_BUSY_TIMEOUT, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, #ifdef SQLITE_ENABLE_SETLK_TIMEOUT #error "This code assumes that Chrome does not use #endif {SQLITE_READONLY_ROLLBACK, static_cast(SqliteLoggedResultCode::kReadOnlyRollback)}, {SQLITE_IOERR_WRITE, static_cast(SqliteLoggedResultCode::kIoWrite)}, {SQLITE_CORRUPT_INDEX, static_cast(SqliteLoggedResultCode::kCorruptIndex)}, // Chrome should always pass full paths to SQLite. {SQLITE_CANTOPEN_FULLPATH, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_CONSTRAINT_FOREIGNKEY, static_cast(SqliteLoggedResultCode::kConstraintForeignKey)}, {SQLITE_READONLY_DBMOVED, static_cast(SqliteLoggedResultCode::kReadOnlyDbMoved)}, {SQLITE_IOERR_FSYNC, static_cast(SqliteLoggedResultCode::kIoFsync)}, // Chrome does not support Cygwin and does not use its VFS. {SQLITE_CANTOPEN_CONVPATH, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, // Chrome does not use extension functions. {SQLITE_CONSTRAINT_FUNCTION, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_READONLY_CANTINIT, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, {SQLITE_IOERR_DIR_FSYNC, static_cast(SqliteLoggedResultCode::kIoDirFsync)}, {SQLITE_CANTOPEN_DIRTYWAL, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, {SQLITE_CONSTRAINT_NOTNULL, static_cast(SqliteLoggedResultCode::kConstraintNotNull)}, {SQLITE_READONLY_DIRECTORY, static_cast(SqliteLoggedResultCode::kReadOnlyDirectory)}, {SQLITE_IOERR_TRUNCATE, static_cast(SqliteLoggedResultCode::kIoTruncate)}, // Chrome does not use the SQLITE_OPEN_NOFOLLOW flag. {SQLITE_CANTOPEN_SYMLINK, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_CONSTRAINT_PRIMARYKEY, static_cast(SqliteLoggedResultCode::kConstraintPrimaryKey)}, {SQLITE_IOERR_FSTAT, static_cast(SqliteLoggedResultCode::kIoFstat)}, // Chrome unconditionally disables database triggers via // sqlite3_db_config(SQLITE_DBCONFIG_ENABLE_TRIGGER). {SQLITE_CONSTRAINT_TRIGGER, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_IOERR_UNLOCK, static_cast(SqliteLoggedResultCode::kIoUnlock)}, {SQLITE_CONSTRAINT_UNIQUE, static_cast(SqliteLoggedResultCode::kConstraintUnique)}, {SQLITE_IOERR_RDLOCK, static_cast(SqliteLoggedResultCode::kIoReadLock)}, // Chrome does not use a virtual table that signals constraints. We only use // a virtual table code for recovery. That code does not use this error. {SQLITE_CONSTRAINT_VTAB, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_IOERR_DELETE, static_cast(SqliteLoggedResultCode::kIoDelete)}, {SQLITE_CONSTRAINT_ROWID, static_cast(SqliteLoggedResultCode::kConstraintRowId)}, {SQLITE_IOERR_BLOCKED, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, // Chrome unconditionally disables database triggers via // sqlite3_db_config(SQLITE_DBCONFIG_ENABLE_TRIGGER). {SQLITE_CONSTRAINT_PINNED, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, // The SQLite docus claim that this error code is "normally" converted to // SQLITE_NOMEM. This doesn't seem 100% categorical, so we're flagging this // as "unused in Chrome" per the same rationale as SQLITE_NOMEM. {SQLITE_IOERR_NOMEM, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_CONSTRAINT_DATATYPE, static_cast(SqliteLoggedResultCode::kConstraintDataType)}, {SQLITE_IOERR_ACCESS, static_cast(SqliteLoggedResultCode::kIoAccess)}, {SQLITE_IOERR_CHECKRESERVEDLOCK, static_cast(SqliteLoggedResultCode::kIoCheckReservedLock)}, {SQLITE_IOERR_LOCK, static_cast(SqliteLoggedResultCode::kIoLock)}, {SQLITE_IOERR_CLOSE, static_cast(SqliteLoggedResultCode::kIoClose)}, {SQLITE_IOERR_DIR_CLOSE, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, // Chrome will only allow enabling WAL on databases with exclusive locking. {SQLITE_IOERR_SHMOPEN, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, // Chrome will only allow enabling WAL on databases with exclusive locking. {SQLITE_IOERR_SHMSIZE, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_IOERR_SHMLOCK, static_cast(SqliteLoggedResultCode::kUnusedSqlite)}, // Chrome will only allow enabling WAL on databases with exclusive locking. {SQLITE_IOERR_SHMMAP, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_IOERR_SEEK, static_cast(SqliteLoggedResultCode::kIoSeek)}, {SQLITE_IOERR_DELETE_NOENT, static_cast(SqliteLoggedResultCode::kIoDeleteNoEntry)}, {SQLITE_IOERR_MMAP, static_cast(SqliteLoggedResultCode::kIoMemoryMapping)}, {SQLITE_IOERR_GETTEMPPATH, static_cast(SqliteLoggedResultCode::kIoGetTemporaryPath)}, // Chrome does not support Cygwin and does not use its VFS. {SQLITE_IOERR_CONVPATH, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, // Chrome does not use SQLite extensions. {SQLITE_IOERR_VNODE, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, // Chrome does not use SQLite extensions. {SQLITE_IOERR_AUTH, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_IOERR_BEGIN_ATOMIC, static_cast(SqliteLoggedResultCode::kIoBeginAtomic)}, {SQLITE_IOERR_COMMIT_ATOMIC, static_cast(SqliteLoggedResultCode::kIoCommitAtomic)}, {SQLITE_IOERR_ROLLBACK_ATOMIC, static_cast(SqliteLoggedResultCode::kIoRollbackAtomic)}, // Chrome does not use the checksum VFS shim. {SQLITE_IOERR_DATA, static_cast(SqliteLoggedResultCode::kUnusedChrome)}, {SQLITE_IOERR_CORRUPTFS, static_cast(SqliteLoggedResultCode::kIoCorruptFileSystem)}, }; // Describes the handling of unknown SQLite error codes. constexpr SqliteResultCodeMappingEntry kUnknownResultCodeMappingEntry = { 0, static_cast(SqliteLoggedResultCode::kUnusedChrome)}; // Looks up a `sqlite_result_code` in the mapping tables. // // Returns an entry in kResultCodeMapping or kUnknownResultCodeMappingEntry. // DCHECKs if the `sqlite_result_code` is not in the mapping table. SqliteResultCodeMappingEntry FindResultCode(int sqlite_result_code) { const auto* mapping_it = base::ranges::find_if( kResultCodeMapping, [&sqlite_result_code](SqliteResultCodeMappingEntry rhs) { return sqlite_result_code == rhs.result_code; }); if (mapping_it == base::ranges::end(kResultCodeMapping)) { NOTREACHED() << "Unsupported SQLite result code: " << sqlite_result_code; return kUnknownResultCodeMappingEntry; } return *mapping_it; } } // namespace #if DCHECK_IS_ON() SqliteResultCode ToSqliteResultCode(int sqlite_result_code) { SqliteLoggedResultCode logged_code = static_cast( FindResultCode(sqlite_result_code).logged_code); DCHECK_NE(logged_code, SqliteLoggedResultCode::kUnusedSqlite) << "SQLite reported code marked for internal use: " << sqlite_result_code; DCHECK_NE(logged_code, SqliteLoggedResultCode::kUnusedChrome) << "SQLite reported code that should never show up in Chrome: " << sqlite_result_code; return static_cast(sqlite_result_code); } SqliteErrorCode ToSqliteErrorCode(SqliteResultCode sqlite_error_code) { SqliteLoggedResultCode logged_code = static_cast( FindResultCode(static_cast(sqlite_error_code)).logged_code); DCHECK_NE(logged_code, SqliteLoggedResultCode::kUnusedSqlite) << "SQLite reported code marked for internal use: " << sqlite_error_code; DCHECK_NE(logged_code, SqliteLoggedResultCode::kUnusedChrome) << "SQLite reported code that should never show up in Chrome: " << sqlite_error_code; DCHECK_NE(logged_code, SqliteLoggedResultCode::kNoError) << __func__ << " called with non-error result code: " << sqlite_error_code; return static_cast(sqlite_error_code); } #endif // DCHECK_IS_ON() bool IsSqliteSuccessCode(SqliteResultCode sqlite_result_code) { // https://www.sqlite.org/rescode.html lists the result codes that are not // errors. bool is_success = (sqlite_result_code == SqliteResultCode::kOk) || (sqlite_result_code == SqliteResultCode::kRow) || (sqlite_result_code == SqliteResultCode::kDone); #if DCHECK_IS_ON() SqliteLoggedResultCode logged_code = static_cast( FindResultCode(static_cast(sqlite_result_code)).logged_code); DCHECK_EQ(is_success, logged_code == SqliteLoggedResultCode::kNoError) << __func__ << " logic disagrees with the code mapping for " << sqlite_result_code; DCHECK_NE(logged_code, SqliteLoggedResultCode::kUnusedSqlite) << "SQLite reported code marked for internal use: " << sqlite_result_code; DCHECK_NE(logged_code, SqliteLoggedResultCode::kUnusedChrome) << "SQLite reported code that should never show up in Chrome: " << sqlite_result_code; #endif // DCHECK_IS_ON() return is_success; } SqliteLoggedResultCode ToSqliteLoggedResultCode(int sqlite_result_code) { SqliteLoggedResultCode logged_code = static_cast( FindResultCode(sqlite_result_code).logged_code); DCHECK_NE(logged_code, SqliteLoggedResultCode::kUnusedSqlite) << "SQLite reported code marked for internal use: " << sqlite_result_code; DCHECK_NE(logged_code, SqliteLoggedResultCode::kUnusedChrome) << "SQLite reported code that should never show up in Chrome: " << sqlite_result_code; return logged_code; } void UmaHistogramSqliteResult(const char* histogram_name, int sqlite_result_code) { auto logged_code = ToSqliteLoggedResultCode(sqlite_result_code); base::UmaHistogramEnumeration(histogram_name, logged_code); } std::ostream& operator<<(std::ostream& os, SqliteResultCode sqlite_result_code) { return os << static_cast(sqlite_result_code); } std::ostream& operator<<(std::ostream& os, SqliteErrorCode sqlite_error_code) { return os << static_cast(sqlite_error_code); } void CheckSqliteLoggedResultCodeForTesting() { // Ensure that error codes are alphabetical. const auto* unordered_it = base::ranges::adjacent_find( kResultCodeMapping, [](SqliteResultCodeMappingEntry lhs, SqliteResultCodeMappingEntry rhs) { return lhs.result_code >= rhs.result_code; }); DCHECK_EQ(unordered_it, base::ranges::end(kResultCodeMapping)) << "Mapping ordering broken at {" << unordered_it->result_code << ", " << static_cast(unordered_it->logged_code) << "}"; std::set sqlite_result_codes; for (auto& mapping_entry : kResultCodeMapping) sqlite_result_codes.insert(mapping_entry.result_code); // SQLite doesn't have special messages for extended errors. // At the time of this writing, sqlite3_errstr() has a string table for // primary result codes, and uses it for extended error codes as well. // // So, we can only use sqlite3_errstr() to check for holes in the primary // message table. for (int result_code = 0; result_code <= 256; ++result_code) { if (sqlite_result_codes.count(result_code) != 0) continue; const char* error_message = sqlite3_errstr(result_code); static constexpr base::StringPiece kUnknownErrorMessage("unknown error"); DCHECK_EQ(kUnknownErrorMessage.compare(error_message), 0) << "Unmapped SQLite result code: " << result_code << " SQLite message: " << error_message; } // Number of #defines in https://www.sqlite.org/c3ref/c_abort.html // // This number is also stated at // https://www.sqlite.org/rescode.html#primary_result_code_list static constexpr int kPrimaryResultCodes = 31; // Number of #defines in https://www.sqlite.org/c3ref/c_abort_rollback.html // // This number is also stated at // https://www.sqlite.org/rescode.html#extended_result_code_list static constexpr int kExtendedResultCodes = 74; DCHECK_EQ(std::size(kResultCodeMapping), size_t{kPrimaryResultCodes + kExtendedResultCodes}) << "Mapping table has incorrect number of entries"; } } // namespace sql