blob: f203039bcbb2d1bd18ae3723c9072a0f5837e21b [file] [log] [blame]
// Copyright 2019 The Chromium OS 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 "arc/apk-cache/apk_cache_database.h"
#include <cinttypes>
#include <cstring>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <base/files/file_path.h>
#include <base/logging.h>
#include <base/optional.h>
#include <base/strings/stringprintf.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include <sqlite3.h>
namespace apk_cache {
namespace {
int IntegrityCheckCallback(void* data, int count, char** row, char** names) {
auto* integrity_result = static_cast<std::string*>(data);
if (!row[0]) {
LOG(ERROR) << "Integrity check returned null";
return SQLITE_ERROR;
}
integrity_result->assign(row[0]);
return SQLITE_OK;
}
int GetSessionsCallback(void* data, int count, char** row, char** names) {
auto* sessions_out = static_cast<std::vector<Session>*>(data);
Session session;
if (!row[0]) {
LOG(ERROR) << "Session.id is null";
return SQLITE_ERROR;
}
if (!base::StringToInt64(row[0], &session.id)) {
LOG(ERROR) << "Session.id is not a number";
return SQLITE_ERROR;
}
if (!row[1]) {
LOG(ERROR) << "Session.source is null";
return SQLITE_ERROR;
}
session.source = row[1];
if (!row[2]) {
LOG(ERROR) << "Session.timestamp is null";
return SQLITE_ERROR;
}
int64_t timestamp;
if (!base::StringToInt64(row[2], &timestamp)) {
LOG(ERROR) << "Session.timestamp is not a number";
return SQLITE_ERROR;
}
session.timestamp = base::Time::FromJavaTime(timestamp);
if (!row[3]) {
LOG(ERROR) << "Session.status is null";
return SQLITE_ERROR;
}
if (!base::StringToInt(row[3], &session.status)) {
LOG(ERROR) << "Session.status is not a number";
return SQLITE_ERROR;
}
sessions_out->push_back(std::move(session));
return SQLITE_OK;
}
int GetFileEntriesCallback(void* data, int count, char** row, char** names) {
auto* file_entries_out = static_cast<std::vector<FileEntry>*>(data);
FileEntry file_entry;
if (!row[0]) {
LOG(ERROR) << "FileEntry.id is null";
return SQLITE_ERROR;
}
if (!base::StringToInt64(row[0], &file_entry.id)) {
LOG(ERROR) << "FileEntry.id is not a number";
return SQLITE_ERROR;
}
if (!row[1]) {
LOG(ERROR) << "FileEntry.package_name is null";
return SQLITE_ERROR;
}
file_entry.package_name = row[1];
if (!row[2]) {
LOG(ERROR) << "FileEntry.version_code is null";
return SQLITE_ERROR;
}
if (!base::StringToInt64(row[2], &file_entry.version_code)) {
LOG(ERROR) << "FileEntry.version_code is not a number";
return SQLITE_ERROR;
}
if (!row[3]) {
LOG(ERROR) << "FileEntry.type is null";
return SQLITE_ERROR;
}
file_entry.type = row[3];
if (row[4])
file_entry.attributes = std::string(row[4]);
if (!row[5]) {
LOG(ERROR) << "FileEntry.size is null";
return SQLITE_ERROR;
}
if (!base::StringToInt64(row[5], &file_entry.size)) {
LOG(ERROR) << "FileEntry.size is not a number";
return SQLITE_ERROR;
}
if (row[6])
file_entry.hash = std::string(row[6]);
if (!row[7]) {
LOG(ERROR) << "FileEntry.access_time is null";
return SQLITE_ERROR;
}
int64_t access_time;
if (!base::StringToInt64(row[7], &access_time)) {
LOG(ERROR) << "FileEntry.access_time is not a number";
return SQLITE_ERROR;
}
file_entry.access_time = base::Time::FromJavaTime(access_time);
if (!row[8]) {
LOG(ERROR) << "FileEntry.priority is null";
return SQLITE_ERROR;
}
if (!base::StringToInt(row[8], &file_entry.priority)) {
LOG(ERROR) << "FileEntry.priority is not a number";
return SQLITE_ERROR;
}
if (!row[9]) {
LOG(ERROR) << "FileEntry.session_id is null";
return SQLITE_ERROR;
}
if (!base::StringToInt64(row[9], &file_entry.session_id)) {
LOG(ERROR) << "FileEntry.session_id is not a number";
return SQLITE_ERROR;
}
file_entries_out->push_back(std::move(file_entry));
return SQLITE_OK;
}
} // namespace
std::string EscapeSQLString(const std::string& string_to_escape) {
std::string escaped_string = string_to_escape;
base::ReplaceSubstringsAfterOffset(&escaped_string, 0, "'", "''");
return escaped_string;
}
ApkCacheDatabase::ApkCacheDatabase(const base::FilePath& db_path)
: db_path_(db_path), db_(nullptr, nullptr) {}
ApkCacheDatabase::~ApkCacheDatabase() {
Close();
}
int ApkCacheDatabase::Init() {
sqlite3* db_ptr;
int result = sqlite3_open(db_path_.MaybeAsASCII().c_str(), &db_ptr);
db_ = std::unique_ptr<sqlite3, decltype(&sqlite3_close)>(db_ptr,
&sqlite3_close);
if (result == SQLITE_OK) {
result = EnableForeignKey();
} else {
LOG(ERROR) << "Failed to connect to database: " << result;
db_ = nullptr;
}
return result;
}
bool ApkCacheDatabase::IsOpen() {
return db_.get() != nullptr;
}
int ApkCacheDatabase::Close() {
if (!db_)
return SQLITE_OK;
// Error code will be returned in case of error. The caller may retry in this
// case. If the database is successfully closed, db_ pointer must be released,
// Otherwise sqlite3_close will be called again on already released db_
// pointer by the destructor, which will result in undefined behavior.
int result = sqlite3_close(db_.get());
if (result == SQLITE_OK)
db_.release();
return result;
}
bool ApkCacheDatabase::CheckIntegrity() const {
// Integrity_check(N) returns a single row and a single column with string
// "ok" if there is no error. Otherwise a maximum of N rows are returned with
// each row representing a single error.
std::string integrity_result;
ExecResult result = ExecSQL("PRAGMA integrity_check(1)",
IntegrityCheckCallback, &integrity_result);
if (result.code != SQLITE_OK) {
LOG(ERROR) << "Failed to check integrity: (" << result.code << ") "
<< result.error_msg;
return false;
}
return integrity_result == "ok";
}
bool ApkCacheDatabase::SessionsTableExists() const {
ExecResult result = ExecSQL("SELECT id FROM sessions LIMIT 1");
return result.error_msg.find("no such table") == std::string::npos;
}
int64_t ApkCacheDatabase::InsertSession(const Session& session) const {
const std::string sql = base::StringPrintf(
"INSERT INTO sessions (source, timestamp, status)"
" VALUES ('%s', %" PRId64 ", %d)",
EscapeSQLString(session.source).c_str(), session.timestamp.ToJavaTime(),
session.status);
ExecResult result = ExecSQL(sql);
if (result.code != SQLITE_OK) {
LOG(ERROR) << "Failed to insert session: (" << result.code << ") "
<< result.error_msg;
return 0;
}
return sqlite3_last_insert_rowid(db_.get());
}
base::Optional<std::vector<Session>> ApkCacheDatabase::GetSessions() const {
std::vector<Session> sessions;
ExecResult result = ExecSQL("SELECT id,source,timestamp,status FROM sessions",
GetSessionsCallback, &sessions);
if (result.code != SQLITE_OK) {
LOG(ERROR) << "Failed to query: (" << result.code << ") "
<< result.error_msg;
return base::nullopt;
}
return base::make_optional(std::move(sessions));
}
base::Optional<std::vector<FileEntry>> ApkCacheDatabase::GetFileEntries()
const {
std::vector<FileEntry> file_entries;
ExecResult result = ExecSQL(
"SELECT id,package_name,version_code,type,"
"attributes,size,hash,access_time,priority,"
"session_id FROM file_entries",
GetFileEntriesCallback, &file_entries);
if (result.code != SQLITE_OK) {
LOG(ERROR) << "Failed to query: (" << result.code << ") "
<< result.error_msg;
return base::nullopt;
}
return base::make_optional(std::move(file_entries));
}
bool ApkCacheDatabase::DeleteSession(int64_t session_id) const {
const std::string sql = base::StringPrintf(
"DELETE FROM sessions WHERE id = %" PRId64, session_id);
if (ExecDeleteSQL(sql) != 1) {
LOG(ERROR) << "Session " << session_id << " does not exist in the database";
return false;
}
return true;
}
int ApkCacheDatabase::DeleteSessionsWithoutFileEntries(
int64_t current_session) const {
const std::string sql = base::StringPrintf(
"DELETE FROM sessions WHERE id IN ("
"SELECT s.id FROM sessions s LEFT JOIN file_entries f ON "
"s.id = f.session_id WHERE f.id IS NULL AND s.id != %" PRId64 ")",
current_session);
return ExecDeleteSQL(sql);
}
bool ApkCacheDatabase::DeleteFileEntry(int64_t file_id) const {
const std::string sql = base::StringPrintf(
"DELETE FROM file_entries WHERE id = %" PRId64, file_id);
if (ExecDeleteSQL(sql) != 1) {
LOG(ERROR) << "File entry " << file_id << " does not exist in the database";
return false;
}
return true;
}
int ApkCacheDatabase::DeletePackage(const std::string& name,
int64_t version) const {
const std::string sql = base::StringPrintf(
"DELETE FROM file_entries WHERE "
"package_name = '%s' AND version_code = %" PRId64,
EscapeSQLString(name).c_str(), version);
return ExecDeleteSQL(sql);
}
bool ApkCacheDatabase::UpdateSessionStatus(int64_t id, int32_t status) const {
const std::string sql = base::StringPrintf(
"UPDATE sessions SET status = %" PRId32 " WHERE id = %" PRId64, status,
id);
ExecResult result = ExecSQL(sql);
if (result.code != SQLITE_OK) {
LOG(ERROR) << "Failed to update session status: (" << result.code << ") "
<< result.error_msg;
return -1;
}
if (sqlite3_changes(db_.get()) != 1) {
LOG(ERROR) << "Session " << id << " does not exist";
return false;
}
return true;
}
int ApkCacheDatabase::EnableForeignKey() const {
ExecResult result = ExecSQL("PRAGMA foreign_keys = ON");
if (result.code != SQLITE_OK) {
LOG(ERROR) << "Failed to enable foreign key: (" << result.code << ") "
<< result.error_msg;
}
return result.code;
}
ApkCacheDatabase::ExecResult ApkCacheDatabase::ExecSQL(
const std::string& sql) const {
return ExecSQL(sql, nullptr, nullptr);
}
ApkCacheDatabase::ExecResult ApkCacheDatabase::ExecSQL(const std::string& sql,
SqliteCallback callback,
void* data) const {
char* error_msg = nullptr;
int result = sqlite3_exec(db_.get(), sql.c_str(), callback, data, &error_msg);
// According to sqlite3_exec() documentation, error_msg points to memory
// allocated by sqlite3_malloc(), which must be freed by sqlite3_free().
std::string error_msg_str;
if (error_msg) {
error_msg_str.assign(error_msg);
sqlite3_free(error_msg);
}
return {result, error_msg_str};
}
int ApkCacheDatabase::ExecDeleteSQL(const std::string& sql) const {
ExecResult result = ExecSQL(sql);
if (result.code != SQLITE_OK) {
LOG(ERROR) << "Failed to delete: (" << result.code << ") "
<< result.error_msg;
return -1;
}
return sqlite3_changes(db_.get());
}
} // namespace apk_cache