diff --git a/src/database/database.cpp b/src/database/database.cpp index 9b978b8c..e826f576 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -28,6 +28,7 @@ #include "leveldb/iterator.h" #include "leveldb/options.h" #include "leveldb/slice.h" +#include "leveldb/status.h" #include "leveldb/write_batch.h" #include "db_events.hpp" @@ -51,12 +52,13 @@ static const char kDbPath[] = "/.tangara-db"; static const char kKeyDbVersion[] = "schema_version"; static const uint8_t kCurrentDbVersion = 3; - +static const char kKeyCollator[] = "collator"; static const char kKeyTrackId[] = "next_track_id"; static std::atomic sIsDbOpen(false); -static auto CreateNewDatabase(leveldb::Options& options) -> leveldb::DB* { +static auto CreateNewDatabase(leveldb::Options& options, locale::ICollator& col) + -> leveldb::DB* { Database::Destroy(); leveldb::DB* db; options.create_if_missing = true; @@ -71,9 +73,48 @@ static auto CreateNewDatabase(leveldb::Options& options) -> leveldb::DB* { delete db; return nullptr; } + ESP_LOGI(kTag, "opening db with collator %s", + col.Describe().value_or("NULL").c_str()); + status = db->Put(leveldb::WriteOptions{}, kKeyCollator, + col.Describe().value_or("")); + if (!status.ok()) { + delete db; + return nullptr; + } return db; } +static auto CheckDatabase(leveldb::DB& db, locale::ICollator& col) -> bool { + leveldb::Status status; + + std::string raw_version; + std::optional version{}; + status = db.Get(leveldb::ReadOptions{}, kKeyDbVersion, &raw_version); + if (status.ok()) { + version = std::stoi(raw_version); + } + if (!version || *version != kCurrentDbVersion) { + ESP_LOGW(kTag, "db version missing or incorrect"); + return false; + } + + std::string collator; + status = db.Get(leveldb::ReadOptions{}, kKeyCollator, &collator); + if (!status.ok()) { + ESP_LOGW(kTag, "db collator is unknown"); + return false; + } + auto needed = col.Describe(); + + if ((needed && needed.value() != collator) || + (!needed && !collator.empty())) { + ESP_LOGW(kTag, "db collator is mismatched"); + return false; + } + + return true; +} + auto Database::Open(IFileGatherer& gatherer, ITagParser& parser, locale::ICollator& collator) @@ -107,23 +148,16 @@ auto Database::Open(IFileGatherer& gatherer, auto status = leveldb::DB::Open(options, kDbPath, &db); if (!status.ok()) { ESP_LOGI(kTag, "opening db failed. recreating."); - db = CreateNewDatabase(options); + db = CreateNewDatabase(options, collator); if (db == nullptr) { return cpp::fail(FAILED_TO_OPEN); } } - std::string raw_version; - std::optional version{}; - status = - db->Get(leveldb::ReadOptions{}, kKeyDbVersion, &raw_version); - if (status.ok()) { - version = std::stoi(raw_version); - } - if (!version || *version != kCurrentDbVersion) { - ESP_LOGI(kTag, "db version missing or incorrect. recreating."); + if (!CheckDatabase(*db, collator)) { + ESP_LOGI(kTag, "db incompatible. recreating."); delete db; - db = CreateNewDatabase(options); + db = CreateNewDatabase(options, collator); if (db == nullptr) { return cpp::fail(FAILED_TO_OPEN); } diff --git a/src/locale/collation.cpp b/src/locale/collation.cpp index f5e8272a..2d787b5a 100644 --- a/src/locale/collation.cpp +++ b/src/locale/collation.cpp @@ -58,19 +58,26 @@ auto GLibCollator::create() -> GLibCollator* { return nullptr; } + // We reserve the first 8 bytes of the partition for an identifier / name. + // Copy it out, then crop the rest of the region so that the LC_COLLATE parser + // doesn't see it. + std::string name{static_cast(region)}; + region = static_cast(region) + 8; + auto data = std::make_unique(); - if (!parse_locale_data(region, partition->size, data.get())) { + if (!parse_locale_data(region, partition->size - 8, data.get())) { ESP_LOGE(kTag, "parsing locale data failed"); esp_partition_munmap(handle); return nullptr; } - return new GLibCollator(handle, std::move(data)); + return new GLibCollator(name, handle, std::move(data)); } -GLibCollator::GLibCollator(const esp_partition_mmap_handle_t handle, +GLibCollator::GLibCollator(const std::string& name, + const esp_partition_mmap_handle_t handle, std::unique_ptr locale) - : handle_(handle), locale_data_(std::move(locale)) {} + : name_(name), handle_(handle), locale_data_(std::move(locale)) {} GLibCollator::~GLibCollator() { esp_partition_munmap(handle_); diff --git a/src/locale/include/collation.hpp b/src/locale/include/collation.hpp index e8b6fa17..b666860d 100644 --- a/src/locale/include/collation.hpp +++ b/src/locale/include/collation.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include "esp_partition.h" @@ -17,31 +18,56 @@ namespace locale { +/* + * Interface for sorting database entries. + * For performance, our database exclusively orders entries via byte + * comparisons of each key. Our collators therefore work by transforming keys + * such that a byte-order comparison results in a natural ordering. + */ class ICollator { public: virtual ~ICollator() {} + /* + * Returns an identify that uniquely describes this collator. Does not need + * to be human readable. + */ + virtual auto Describe() -> std::optional = 0; virtual auto Transform(const std::string&) -> std::string = 0; }; +/* Creates and returns the best available collator. */ +auto CreateCollator() -> std::unique_ptr; + +/* + * Collator that doesn't do anything. Used only when there is no available + * locale data. + */ class NoopCollator : public ICollator { public: + auto Describe() -> std::optional override { return {}; } auto Transform(const std::string& in) -> std::string override { return in; } }; -auto CreateCollator() -> std::unique_ptr; - +/* + * Collator that uses glibc's `strxfrm` to transform keys. Relies on an + * LC_COLLATE file (+ 8 byte name header) flashed on a partition in internal + * flash. + */ class GLibCollator : public ICollator { public: static auto create() -> GLibCollator*; ~GLibCollator(); + auto Describe() -> std::optional override { return name_; } auto Transform(const std::string& in) -> std::string override; private: - GLibCollator(const esp_partition_mmap_handle_t, + GLibCollator(const std::string& name, + const esp_partition_mmap_handle_t, std::unique_ptr); + const std::string name_; const esp_partition_mmap_handle_t handle_; std::unique_ptr locale_data_; };