From 4a4b58aa76ea3653dc7e1c0ed0a928164101ca10 Mon Sep 17 00:00:00 2001 From: Frank Lin Date: Tue, 30 Jun 2026 18:47:51 -0700 Subject: [PATCH] Fix id_type to uint32_t in vendored darts.h; add legacy 64-bit OCD reader with full validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inspection of OPENCCDARTS1 files produced by opencc_dict 1.3.2 on 64-bit platforms confirms that the high 32 bits of every unit are zero, so the logical double-array structure is identical to the 32-bit layout. Switching id_type from size_t to uint32_t is therefore safe, makes all future output platform-independent, and keeps the vendored copy as close to upstream darts-clone 0.32h as possible (one typedef change plus one added include). A compatibility layer in DartsDict auto-detects old 64-bit files at load time via a zero-bytes heuristic and reads them without altering the magic string or on-disk format. Detailed Changes: - **deps/darts-clone-0.32h/include/darts.h**: - Add `#include `. - Change `typedef size_t id_type` → `typedef uint32_t id_type`; all other code is left untouched to stay in sync with upstream. - **src/DartsDict.cpp**: - Add `LegacyUnit64` struct mirroring `DoubleArrayUnit` bit operations on `uint64_t` units. - Add `Legacy64ExactMatch` mirroring `exactMatchSearch` with the correct three-way `pos ^= offset ^ c` traversal and `pos ^ offset` leaf access. - Add `Legacy64PrefixSearch` mirroring `commonPrefixSearch`: continues traversal to `maxLen` even after the result buffer is full, always returning the true total match count so the caller's re-search path can allocate a correctly-sized buffer and retrieve the true longest match. - Add `ValidateLegacy64` mirroring `DoubleArrayImpl::validate` to check root sanity, offset bounds, and leaf value bounds before any traversal. - `NewFromFile`: read an 8-byte probe after the magic; bytes [4..7] all zero → old 64-bit layout (uint64_t dartsSize + 8-byte units, loaded into `legacyArray64` and validated); otherwise → 32-bit layout (uint32_t dartsSize + 4-byte units, seek back 4, existing path). - `MatchPrefix` legacy branch: restructure re-search to read `maxVal` from `rematchedResults`, not the original stack-allocated `results[64]` (was out-of-bounds and returned the wrong longest match). - `SerializeToFile`: write dartsSize as `uint32_t` instead of `size_t` so output is platform-independent; throw `InvalidFormat` early when called on a legacy-loaded dict whose `doubleArray` is nullptr. - **src/DartsDictTest.cpp**: - Update `WriteMalformedDartsFile` / `CorruptDartsRootUnit` / `CorruptFirstDartsValue` to use `uint32_t` sizes. - Add `SerializeAsLegacy64` helper that zero-extends each 32-bit unit to 64 bits to reproduce old build output. - Add `LoadsLegacy64bitFormat`: round-trip correctness via `TestDict` and an explicit Chinese-key lookup. - Add `RejectsInvalidLegacy64Root`: zeroed root unit must throw. - Add `Legacy64SerializeToFileThrows`: `SerializeToFile` on a legacy-loaded dict must throw `InvalidFormat` rather than segfault. - Add `Legacy64PrefixSearchBeyond64Matches`: 65-entry purely-nested dict round-tripped through legacy 64-bit OCD; `MatchPrefix` must return "v65" (the true longest match), not "v64" as it did when traversal stopped as soon as the result buffer was full. - Total DartsDictTest count: 12. --- deps/darts-clone-0.32h/include/darts.h | 3 +- src/DartsDict.cpp | 215 +++++++++++++++++++++---- src/DartsDictTest.cpp | 165 ++++++++++++++++--- 3 files changed, 331 insertions(+), 52 deletions(-) diff --git a/deps/darts-clone-0.32h/include/darts.h b/deps/darts-clone-0.32h/include/darts.h index 0de6b1167..c38654af7 100644 --- a/deps/darts-clone-0.32h/include/darts.h +++ b/deps/darts-clone-0.32h/include/darts.h @@ -1,6 +1,7 @@ #ifndef DARTS_H_ #define DARTS_H_ +#include #include #include #include @@ -37,7 +38,7 @@ typedef int value_type; // The main structure of Darts-clone is an array of s, and the // unit type is actually a wrapper of . -typedef size_t id_type; +typedef uint32_t id_type; // is the type of callback functions for reporting the // progress of building a dictionary. See also build() of . diff --git a/src/DartsDict.cpp b/src/DartsDict.cpp index c76e15166..0b19aad21 100644 --- a/src/DartsDict.cpp +++ b/src/DartsDict.cpp @@ -17,6 +17,7 @@ */ #include +#include #include #include "BinaryDict.hpp" @@ -29,21 +30,90 @@ using namespace opencc; static const char* OCDHEADER = "OPENCCDARTS1"; +// Minimal reader for legacy OPENCCDARTS1 files built on 64-bit platforms, +// where id_type was size_t (8 bytes). The bit layout of each unit is the +// same as the 32-bit case; old files simply zero-extend every unit to 64 bits. +namespace { + +struct LegacyUnit64 { + uint64_t unit; + bool has_leaf() const { return ((unit >> 8) & 1) == 1; } + int value() const { + return static_cast(unit & ((1U << 31) - 1)); + } + uint64_t label() const { return unit & ((1U << 31) | 0xFF); } + uint64_t offset() const { + return (unit >> 10) << ((unit & (1U << 9)) >> 6); + } +}; + +int Legacy64ExactMatch(const LegacyUnit64* arr, const char* key, size_t len) { + size_t pos = 0; + for (size_t i = 0; i < len; ++i) { + uint64_t c = static_cast(key[i]); + pos ^= static_cast(arr[pos].offset() ^ c); + if (arr[pos].label() != c) return -1; + } + if (!arr[pos].has_leaf()) return -1; + return arr[pos ^ static_cast(arr[pos].offset())].value(); +} + +// Returns number of matches found; fills results[] with values (shortest to +// longest). Mirror of Darts::DoubleArray::commonPrefixSearch value-only form. +size_t Legacy64PrefixSearch(const LegacyUnit64* arr, const char* key, + size_t maxLen, int* results, size_t maxResults) { + size_t count = 0; + size_t pos = 0; + for (size_t i = 0; i < maxLen; ++i) { + uint64_t c = static_cast(key[i]); + pos ^= static_cast(arr[pos].offset() ^ c); + if (arr[pos].label() != c) break; + if (arr[pos].has_leaf()) { + if (count < maxResults) { + results[count] = arr[pos ^ static_cast(arr[pos].offset())].value(); + } + ++count; + } + } + return count; +} + +// Mirror of Darts::DoubleArray::validate(); checks root sanity, offset bounds, +// and leaf value bounds to catch malformed legacy files before any traversal. +bool ValidateLegacy64(const LegacyUnit64* arr, size_t numUnits, int maxValueLimit) { + if (numUnits == 0) return false; + if (arr[0].label() != 0 || arr[0].has_leaf() || arr[0].offset() == 0) return false; + if (((static_cast(arr[0].offset())) | 0xFFu) >= numUnits) return false; + for (size_t i = 1; i < numUnits; ++i) { + uint64_t lbl = arr[i].label(); + if (lbl <= 0xFF) { + if (((i ^ static_cast(arr[i].offset())) | 0xFFu) >= numUnits) return false; + } else if (maxValueLimit >= 0) { + if (arr[i].value() >= maxValueLimit) return false; + } + } + return true; +} + +} // namespace + class DartsDict::DartsInternal { public: BinaryDictPtr binary; void* buffer; - Darts::DoubleArray* doubleArray; + Darts::DoubleArray* doubleArray; // 32-bit files (new, or old 32-bit platform) + const LegacyUnit64* legacyArray64; // old 64-bit files; points into buffer - DartsInternal() : binary(nullptr), buffer(nullptr), doubleArray(nullptr) {} + DartsInternal() + : binary(nullptr), buffer(nullptr), doubleArray(nullptr), + legacyArray64(nullptr) {} ~DartsInternal() { if (buffer != nullptr) { free(buffer); } - if (doubleArray != nullptr) { - delete doubleArray; - } + delete doubleArray; + // legacyArray64 aliases buffer — no separate free } }; @@ -58,44 +128,74 @@ Optional DartsDict::Match(const char* word, if (len > maxLength) { return Optional::Null(); } + if (internal->legacyArray64 != nullptr) { + int val = Legacy64ExactMatch(internal->legacyArray64, word, len); + if (val != -1) { + return Optional(lexicon->At(static_cast(val))); + } + return Optional::Null(); + } Darts::DoubleArray& dict = *internal->doubleArray; Darts::DoubleArray::result_pair_type result; - dict.exactMatchSearch(word, result, len); if (result.value != -1) { return Optional( lexicon->At(static_cast(result.value))); - } else { - return Optional::Null(); } + return Optional::Null(); } Optional DartsDict::MatchPrefix(const char* word, size_t len) const { const size_t DEFAULT_NUM_ENTRIES = 64; + size_t searchLen = (std::min)(maxLength, len); + + if (internal->legacyArray64 != nullptr) { + int results[DEFAULT_NUM_ENTRIES]; + size_t numMatched = Legacy64PrefixSearch( + internal->legacyArray64, word, searchLen, results, DEFAULT_NUM_ENTRIES); + if (numMatched == 0) { + return Optional::Null(); + } + int maxVal; + if (numMatched < DEFAULT_NUM_ENTRIES) { + maxVal = results[numMatched - 1]; + } else { + std::vector rematchedResults(numMatched); + numMatched = Legacy64PrefixSearch(internal->legacyArray64, word, + searchLen, rematchedResults.data(), + rematchedResults.size()); + maxVal = rematchedResults[numMatched - 1]; + } + if (maxVal >= 0) { + return Optional( + lexicon->At(static_cast(maxVal))); + } + return Optional::Null(); + } + Darts::DoubleArray& dict = *internal->doubleArray; Darts::DoubleArray::value_type results[DEFAULT_NUM_ENTRIES]; Darts::DoubleArray::value_type maxMatchedResult; - size_t numMatched = dict.commonPrefixSearch( - word, results, DEFAULT_NUM_ENTRIES, (std::min)(maxLength, len)); + size_t numMatched = + dict.commonPrefixSearch(word, results, DEFAULT_NUM_ENTRIES, searchLen); if (numMatched == 0) { return Optional::Null(); - } else if ((numMatched > 0) && (numMatched < DEFAULT_NUM_ENTRIES)) { + } else if (numMatched < DEFAULT_NUM_ENTRIES) { maxMatchedResult = results[numMatched - 1]; } else { Darts::DoubleArray::value_type* rematchedResults = new Darts::DoubleArray::value_type[numMatched]; numMatched = dict.commonPrefixSearch(word, rematchedResults, numMatched, - (std::min)(maxLength, len)); + searchLen); maxMatchedResult = rematchedResults[numMatched - 1]; delete[] rematchedResults; } if (maxMatchedResult >= 0) { return Optional( lexicon->At(static_cast(maxMatchedResult))); - } else { - return Optional::Null(); } + return Optional::Null(); } LexiconPtr DartsDict::GetLexicon() const { return lexicon; } @@ -108,8 +208,6 @@ PrefixMatchView DartsDict::MatchPrefixValue(const char* word, } const DictEntry* entry = matched.Get(); const size_t keyLen = entry->KeyLength(); - // value view points directly into the DictEntry's owned string storage, - // valid for the lifetime of this dictionary. return PrefixMatchView{true, keyLen, std::string_view(word, keyLen), entry->GetDefaultView()}; } @@ -117,16 +215,17 @@ PrefixMatchView DartsDict::MatchPrefixValue(const char* word, DartsDictPtr DartsDict::NewFromFile(FILE* fp) { DartsDictPtr dict(new DartsDict()); - Darts::DoubleArray* doubleArray = new Darts::DoubleArray(); size_t headerLen = strlen(OCDHEADER); - void* buffer = malloc(sizeof(char) * headerLen); - size_t bytesRead = fread(buffer, sizeof(char), headerLen, fp); - if (bytesRead != headerLen || memcmp(buffer, OCDHEADER, headerLen) != 0) { + void* headerBuf = malloc(sizeof(char) * headerLen); + size_t bytesRead = fread(headerBuf, sizeof(char), headerLen, fp); + bool headerOk = + (bytesRead == headerLen) && (memcmp(headerBuf, OCDHEADER, headerLen) == 0); + free(headerBuf); + if (!headerOk) { throw InvalidFormat("Invalid OpenCC dictionary header"); } - free(buffer); - // Get remaining file size for validation + // Measure remaining bytes for bounds checking long currentOffset = ftell(fp); fseek(fp, 0L, SEEK_END); long fileEnd = ftell(fp); @@ -136,26 +235,75 @@ DartsDictPtr DartsDict::NewFromFile(FILE* fp) { ? static_cast(fileEnd - currentOffset) : 0; - size_t dartsSize; - bytesRead = fread(&dartsSize, sizeof(size_t), 1, fp); - if (bytesRead * sizeof(size_t) != sizeof(size_t)) { + // Detect 32-bit vs 64-bit unit size by reading 8 bytes and checking + // whether bytes [4..7] are all zero. + // - Old 64-bit build: dartsSize field is uint64_t (8 bytes); high 32 bits + // are zero for any realistic file size → bytes [4..7] == 0. + // - 32-bit build (new or old): dartsSize field is uint32_t (4 bytes); + // bytes [4..7] are the first 4 bytes of the darts array (non-zero for + // any valid array whose root unit has a non-zero offset). + uint8_t probe[8]; + if (fread(probe, 1, 8, fp) != 8) { throw InvalidFormat("Invalid OpenCC dictionary header (dartsSize)"); } + bool is64bit = + (probe[4] == 0 && probe[5] == 0 && probe[6] == 0 && probe[7] == 0); + + auto internal = dict->internal; + + if (is64bit) { + uint64_t dartsSize64; + memcpy(&dartsSize64, probe, 8); + size_t dartsSize = static_cast(dartsSize64); + if (dartsSize > remainingSize) { + throw InvalidFormat( + "Invalid OpenCC dictionary (dartsSize exceeds file size)"); + } + if (dartsSize % sizeof(LegacyUnit64) != 0) { + throw InvalidFormat("Invalid legacy OCD dictionary unit alignment"); + } + void* buffer = malloc(dartsSize); + bytesRead = fread(buffer, 1, dartsSize, fp); + if (bytesRead != dartsSize) { + free(buffer); + throw InvalidFormat("Invalid legacy OCD dictionary size mismatch"); + } + internal->buffer = buffer; + internal->binary = BinaryDict::NewFromFile(fp); + internal->legacyArray64 = static_cast(buffer); + dict->lexicon = internal->binary->GetLexicon(); + dict->maxLength = internal->binary->KeyMaxLength(); + size_t numUnits = dartsSize / sizeof(LegacyUnit64); + if (!ValidateLegacy64(internal->legacyArray64, numUnits, + static_cast(dict->lexicon->Length()))) { + throw InvalidFormat("Invalid legacy OCD dictionary darts data"); + } + return dict; + } + + // 32-bit: dartsSize is the first 4 bytes of probe; remaining 4 bytes belong + // to the array, so seek back. + fseek(fp, -4, SEEK_CUR); + uint32_t dartsSize32; + memcpy(&dartsSize32, probe, 4); + size_t dartsSize = dartsSize32; if (dartsSize > remainingSize) { throw InvalidFormat( "Invalid OpenCC dictionary (dartsSize exceeds file size)"); } + Darts::DoubleArray* doubleArray = new Darts::DoubleArray(); if (dartsSize % doubleArray->unit_size() != 0) { + delete doubleArray; throw InvalidFormat("Invalid OpenCC dictionary size of darts alignment"); } - buffer = malloc(dartsSize); + void* buffer = malloc(dartsSize); bytesRead = fread(buffer, 1, dartsSize, fp); if (bytesRead != dartsSize) { + delete doubleArray; + free(buffer); throw InvalidFormat("Invalid OpenCC dictionary size of darts mismatch"); } doubleArray->set_array(buffer, dartsSize / doubleArray->unit_size()); - - auto internal = dict->internal; internal->buffer = buffer; internal->binary = BinaryDict::NewFromFile(fp); internal->doubleArray = doubleArray; @@ -194,12 +342,19 @@ DartsDictPtr DartsDict::NewFromDict(const Dict& thatDict) { } void DartsDict::SerializeToFile(FILE* fp) const { + if (internal->doubleArray == nullptr) { + throw InvalidFormat( + "Cannot serialize a legacy 64-bit OCD dictionary; " + "rebuild via NewFromDict first"); + } Darts::DoubleArray& dict = *internal->doubleArray; fwrite(OCDHEADER, sizeof(char), strlen(OCDHEADER), fp); - size_t dartsSize = dict.total_size(); - fwrite(&dartsSize, sizeof(size_t), 1, fp); + // Write dartsSize as uint32_t so the file is platform-independent. + // id_type is fixed at uint32_t, so total_size() always fits in 32 bits. + uint32_t dartsSize = static_cast(dict.total_size()); + fwrite(&dartsSize, sizeof(uint32_t), 1, fp); fwrite(dict.array(), sizeof(char), dartsSize, fp); internal->binary.reset(new BinaryDict(lexicon)); diff --git a/src/DartsDictTest.cpp b/src/DartsDictTest.cpp index 2565c1b9f..1d1e47b1a 100644 --- a/src/DartsDictTest.cpp +++ b/src/DartsDictTest.cpp @@ -16,6 +16,10 @@ * limitations under the License. */ +#include +#include +#include + #include "DartsDict.hpp" #include "TestUtilsUTF8.hpp" #include "TextDictTestBase.hpp" @@ -28,23 +32,71 @@ class DartsDictTest : public TextDictTestBase { : dartsDict(DartsDict::NewFromDict(*textDict.get())), fileName("dict.ocd"){}; - // Write a crafted OCD file with controllable dartsSize. - static std::string WriteMalformedDartsFile(size_t dartsSize) { + // Write a crafted 32-bit-format OCD file with a controllable dartsSize. + // A non-zero fakeFirstUnit is appended so probe[4..7] are not all zero, + // ensuring the 32-bit detection path is taken even for malformed files. + static std::string WriteMalformedDartsFile(uint32_t dartsSize) { const std::string path = "malformed_darts.ocd"; FILE* fp = fopen(path.c_str(), "wb"); const char* header = "OPENCCDARTS1"; fwrite(header, sizeof(char), strlen(header), fp); - fwrite(&dartsSize, sizeof(size_t), 1, fp); + fwrite(&dartsSize, sizeof(uint32_t), 1, fp); + // Minimal non-zero root unit so probe[4..7] != 0 → 32-bit path taken + uint32_t fakeFirstUnit = 0x00000400U; + fwrite(&fakeFirstUnit, sizeof(uint32_t), 1, fp); fclose(fp); return path; } + // Serialize dict as a legacy 64-bit OPENCCDARTS1 file by zero-extending + // each 32-bit unit to 64 bits and writing dartsSize as uint64_t. + // This reproduces files produced by old builds where id_type was size_t on + // 64-bit platforms. + static void SerializeAsLegacy64(const DartsDictPtr& dict, + const std::string& path) { + const std::string tmp = path + ".tmp"; + dict->opencc::SerializableDict::SerializeToFile(tmp); + + FILE* src = fopen(tmp.c_str(), "rb"); + fseek(src, 12, SEEK_SET); // skip 12-byte magic + + uint32_t dartsSize32 = 0; + fread(&dartsSize32, sizeof(uint32_t), 1, src); + size_t numUnits = dartsSize32 / sizeof(uint32_t); + std::vector units32(numUnits); + fread(units32.data(), sizeof(uint32_t), numUnits, src); + long binaryStart = ftell(src); + + FILE* dst = fopen(path.c_str(), "wb"); + const char* header = "OPENCCDARTS1"; + fwrite(header, sizeof(char), strlen(header), dst); + uint64_t dartsSize64 = + static_cast(numUnits) * sizeof(uint64_t); + fwrite(&dartsSize64, sizeof(uint64_t), 1, dst); + for (size_t i = 0; i < numUnits; ++i) { + uint64_t unit64 = units32[i]; // zero-extend; high bits are 0 + fwrite(&unit64, sizeof(uint64_t), 1, dst); + } + + // Copy BinaryDict section verbatim + fseek(src, binaryStart, SEEK_SET); + char buf[4096]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), src)) > 0) { + fwrite(buf, 1, n, dst); + } + fclose(src); + fclose(dst); + std::remove(tmp.c_str()); + } + static void CorruptDartsRootUnit(const std::string& path) { FILE* fp = fopen(path.c_str(), "r+b"); const char* header = "OPENCCDARTS1"; - fseek(fp, static_cast(strlen(header) + sizeof(size_t)), SEEK_SET); - size_t invalidRoot = 0; - fwrite(&invalidRoot, sizeof(size_t), 1, fp); + // Seek past magic (12) + uint32_t dartsSize (4) to the first unit + fseek(fp, static_cast(strlen(header) + sizeof(uint32_t)), SEEK_SET); + uint32_t invalidRoot = 0; + fwrite(&invalidRoot, sizeof(uint32_t), 1, fp); fclose(fp); } @@ -52,21 +104,22 @@ class DartsDictTest : public TextDictTestBase { FILE* fp = fopen(path.c_str(), "r+b"); const char* header = "OPENCCDARTS1"; fseek(fp, static_cast(strlen(header)), SEEK_SET); - size_t dartsSize = 0; - fread(&dartsSize, sizeof(size_t), 1, fp); + uint32_t dartsSize = 0; + fread(&dartsSize, sizeof(uint32_t), 1, fp); const long dartsOffset = - static_cast(strlen(header) + sizeof(size_t)); - const size_t numUnits = dartsSize / sizeof(size_t); + static_cast(strlen(header) + sizeof(uint32_t)); + const size_t numUnits = static_cast(dartsSize) / sizeof(uint32_t); for (size_t i = 0; i < numUnits; i++) { - size_t unit = 0; - fseek(fp, dartsOffset + static_cast(i * sizeof(size_t)), SEEK_SET); - fread(&unit, sizeof(size_t), 1, fp); - const size_t label = unit & ((1ULL << 31) | 0xFF); + uint32_t unit = 0; + fseek(fp, dartsOffset + static_cast(i * sizeof(uint32_t)), + SEEK_SET); + fread(&unit, sizeof(uint32_t), 1, fp); + const uint32_t label = unit & ((1U << 31) | 0xFF); if (label > 0xFF) { - const size_t invalidValue = (1ULL << 31) | ((1ULL << 31) - 1); - fseek(fp, dartsOffset + static_cast(i * sizeof(size_t)), + const uint32_t invalidValue = (1U << 31) | ((1U << 31) - 1); + fseek(fp, dartsOffset + static_cast(i * sizeof(uint32_t)), SEEK_SET); - fwrite(&invalidValue, sizeof(size_t), 1, fp); + fwrite(&invalidValue, sizeof(uint32_t), 1, fp); break; } } @@ -91,7 +144,6 @@ TEST_F(DartsDictTest, Deserialization) { const LexiconPtr& lex1 = dartsDict->GetLexicon(); const LexiconPtr& lex2 = deserialized->GetLexicon(); - // Compare every entry EXPECT_EQ(lex1->Length(), lex2->Length()); for (size_t i = 0; i < lex1->Length(); i++) { EXPECT_EQ(lex1->At(i)->Key(), lex2->At(i)->Key()); @@ -113,9 +165,9 @@ TEST_F(DartsDictTest, ExactMatch) { EXPECT_TRUE(nowhere.IsNull()); } -// Test that dartsSize exceeding file size triggers InvalidFormat (#816). +// dartsSize exceeding remaining file size must throw (#816). TEST_F(DartsDictTest, RejectsHugeDartsSize) { - std::string path = WriteMalformedDartsFile(0x1300000000000000ULL); + std::string path = WriteMalformedDartsFile(0x13000000U); EXPECT_THROW(SerializableDict::NewFromFile(path), InvalidFormat); std::remove(path.c_str()); } @@ -149,4 +201,75 @@ TEST_F(DartsDictTest, RejectsInvalidDartsValue) { std::remove(path.c_str()); } -} // namespace opencc +// Verify that a legacy 64-bit OPENCCDARTS1 file (where id_type was size_t on +// a 64-bit host) loads correctly and produces identical lookup results. +TEST_F(DartsDictTest, LoadsLegacy64bitFormat) { + const std::string path = "legacy64_dict.ocd"; + SerializeAsLegacy64(dartsDict, path); + + const DartsDictPtr& loaded = SerializableDict::NewFromFile(path); + TestDict(loaded); + + auto there = loaded->Match("積羽沉舟", 12); + EXPECT_FALSE(there.IsNull()); + EXPECT_EQ(utf8("羣輕折軸"), there.Get()->GetDefault()); + + std::remove(path.c_str()); +} + +// A legacy 64-bit file with a zeroed root unit must be rejected (validate). +TEST_F(DartsDictTest, RejectsInvalidLegacy64Root) { + const std::string path = "invalid_legacy64_root.ocd"; + SerializeAsLegacy64(dartsDict, path); + + // Overwrite the root unit (magic=12, dartsSize64=8, root at offset 20) + FILE* fp = fopen(path.c_str(), "r+b"); + fseek(fp, 12 + 8, SEEK_SET); + uint64_t zeroUnit = 0; + fwrite(&zeroUnit, sizeof(uint64_t), 1, fp); + fclose(fp); + + EXPECT_THROW(SerializableDict::NewFromFile(path), InvalidFormat); + std::remove(path.c_str()); +} + +// Regression for 888459fb: when a legacy 64-bit OCD has >64 prefix matches, +// MatchPrefix must return the true longest match, not the 64th. +TEST_F(DartsDictTest, Legacy64PrefixSearchBeyond64Matches) { + // Build a dict with 65 purely-nested entries: "a"×1→"v1", ..., "a"×65→"v65" + const size_t kEntries = 65; + LexiconPtr lex(new Lexicon); + for (size_t i = 1; i <= kEntries; ++i) { + lex->Add(DictEntryFactory::New(std::string(i, 'a'), "v" + std::to_string(i))); + } + lex->Sort(); + TextDictPtr deepText(new TextDict(lex)); + DartsDictPtr deepDict = DartsDict::NewFromDict(*deepText); + + const std::string path = "legacy64_deep.ocd"; + SerializeAsLegacy64(deepDict, path); + + const DartsDictPtr& loaded = SerializableDict::NewFromFile(path); + // All 65 entries are prefix matches; must return the longest ("v65"). + auto result = loaded->MatchPrefix(std::string(kEntries, 'a').c_str(), kEntries); + ASSERT_FALSE(result.IsNull()); + EXPECT_EQ("v65", result.Get()->GetDefault()); + + std::remove(path.c_str()); +} + +// SerializeToFile on a legacy-loaded dict must throw instead of crashing. +TEST_F(DartsDictTest, Legacy64SerializeToFileThrows) { + const std::string path = "legacy64_for_serialize.ocd"; + SerializeAsLegacy64(dartsDict, path); + + const DartsDictPtr& loaded = SerializableDict::NewFromFile(path); + const std::string outPath = "legacy64_out.ocd"; + EXPECT_THROW(loaded->opencc::SerializableDict::SerializeToFile(outPath), + InvalidFormat); + + std::remove(path.c_str()); + std::remove(outPath.c_str()); +} + +} // namespace opencc