usearch 2.26.0 → 2.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -545,11 +545,11 @@ index = Index(ndim=ndim, metric=CompiledMetric(
545
545
 
546
546
  ```bibtex
547
547
  @software{Vardanian_USearch,
548
- doi = {10.5281/zenodo.7949416},
548
+ doi = {10.5281/zenodo.7949415},
549
549
  author = {Vardanian, Ash},
550
550
  title = {{USearch by Unum Cloud}},
551
551
  url = {https://github.com/unum-cloud/USearch},
552
- version = {2.26.0},
552
+ version = {2.26.1},
553
553
  year = {2026},
554
554
  }
555
555
  ```
package/cpp/test.cpp CHANGED
@@ -1112,9 +1112,6 @@ template <typename key_at, typename slot_at> void test_strings() {
1112
1112
  }
1113
1113
  }
1114
1114
 
1115
- /**
1116
- * @brief Tests replacing and updating entries in index_dense_gt to ensure consistency after modifications.
1117
- */
1118
1115
  /**
1119
1116
  * @brief Churns a tightly-reserved index, so `slot_lookup_` accumulates tombstones.
1120
1117
  *
@@ -1181,6 +1178,10 @@ template <typename key_at, typename slot_at> void test_slot_lookup_churn() {
1181
1178
  expect(copy.contains(key));
1182
1179
  }
1183
1180
 
1181
+ /**
1182
+ * @brief Tests replacing and updating entries in `index_dense_gt`, to ensure consistency
1183
+ * after modifications.
1184
+ */
1184
1185
  template <typename key_at, typename slot_at> void test_replacing_update() {
1185
1186
 
1186
1187
  using vector_key_t = key_at;
@@ -1427,6 +1428,67 @@ void test_load_after_metric_make() {
1427
1428
  std::remove(path);
1428
1429
  }
1429
1430
 
1431
+ /**
1432
+ * @brief Feeds `view` a truncated index file.
1433
+ *
1434
+ * The matrix dimensions and the node count come from the file itself, so a short file
1435
+ * describes a matrix and a level array that reach past the mapping. Both must be rejected
1436
+ * rather than walked, and a caller-supplied offset past the end must fail the same way.
1437
+ */
1438
+ void test_view_of_truncated_file() {
1439
+ std::printf("Testing view of a truncated index file\n");
1440
+
1441
+ using index_t = index_dense_gt<std::int64_t, std::uint32_t>;
1442
+ std::size_t const dimensions = 64;
1443
+ std::size_t const collection = 2048;
1444
+ char const* path = "tmp_truncated.usearch";
1445
+
1446
+ metric_punned_t metric(dimensions, metric_kind_t::l2sq_k, scalar_kind<f32_t>());
1447
+ index_t::state_result_t built = index_t::make(metric);
1448
+ expect(built);
1449
+ expect(built.index.try_reserve(collection));
1450
+ std::vector<float> vector(dimensions);
1451
+ for (std::size_t i = 0; i != collection; ++i) {
1452
+ for (std::size_t d = 0; d != dimensions; ++d)
1453
+ vector[d] = static_cast<float>((i * 31 + d) % 997) / 997.f;
1454
+ expect(built.index.add(static_cast<std::int64_t>(i), vector.data()));
1455
+ }
1456
+ expect(built.index.save(path));
1457
+
1458
+ std::vector<char> prefix;
1459
+ {
1460
+ std::FILE* file = std::fopen(path, "rb");
1461
+ expect(file != nullptr);
1462
+ std::fseek(file, 0, SEEK_END);
1463
+ long const bytes = std::ftell(file);
1464
+ std::fseek(file, 0, SEEK_SET);
1465
+ prefix.resize(static_cast<std::size_t>(bytes) / 8);
1466
+ expect(std::fread(prefix.data(), 1, prefix.size(), file) == prefix.size());
1467
+ std::fclose(file);
1468
+ }
1469
+ {
1470
+ std::FILE* file = std::fopen(path, "wb");
1471
+ expect(file != nullptr);
1472
+ std::fwrite(prefix.data(), 1, prefix.size(), file);
1473
+ std::fclose(file);
1474
+ }
1475
+
1476
+ index_t::state_result_t viewed = index_t::make(metric);
1477
+ expect(viewed);
1478
+ serialization_result_t truncated = viewed.index.view(path);
1479
+ expect(!truncated);
1480
+ truncated.error.release();
1481
+
1482
+ // A caller-supplied offset past the end must fail the same way.
1483
+ index_t::state_result_t offset_viewed = index_t::make(metric);
1484
+ expect(offset_viewed);
1485
+ serialization_result_t beyond = offset_viewed.index.view(memory_mapped_file_t(path), prefix.size() * 4);
1486
+ expect(!beyond);
1487
+ beyond.error.release();
1488
+
1489
+ std::remove(path);
1490
+ }
1491
+
1430
1492
  int main(int, char**) {
1431
1493
  install_crash_handlers();
1432
1494
 
@@ -1523,5 +1585,6 @@ int main(int, char**) {
1523
1585
  test_filtered_search();
1524
1586
  test_isolate();
1525
1587
  test_load_after_metric_make();
1588
+ test_view_of_truncated_file();
1526
1589
  return 0;
1527
1590
  }
@@ -9,7 +9,7 @@
9
9
 
10
10
  #define USEARCH_VERSION_MAJOR 2
11
11
  #define USEARCH_VERSION_MINOR 26
12
- #define USEARCH_VERSION_PATCH 0
12
+ #define USEARCH_VERSION_PATCH 1
13
13
 
14
14
  // Inferring C++ version
15
15
  // https://stackoverflow.com/a/61552074
@@ -3970,6 +3970,10 @@ class index_gt {
3970
3970
  : checked_size_overflow();
3971
3971
  if (!first_offset)
3972
3972
  return result.failed("Index is too large");
3973
+ if (file.size() < first_offset.value) {
3974
+ reset();
3975
+ return result.failed("File is corrupted and can't fit the node levels");
3976
+ }
3973
3977
  offsets[0u] = first_offset.value;
3974
3978
  for (std::size_t i = 1; i < header_size.value; ++i) {
3975
3979
  checked_size_result_t next_offset = checked_add(offsets[i - 1], node_bytes_(levels[i - 1]));
@@ -1050,17 +1050,18 @@ class index_dense_gt {
1050
1050
  */
1051
1051
  bool try_reserve(index_limits_t limits) {
1052
1052
 
1053
+ // Reserving is monotonic: reducing thread limits must not shrink member-addressed storage.
1054
+ limits.members = (std::max)(limits.members, typed_->capacity());
1055
+ limits.members = (std::max)(limits.members, vectors_lookup_.size());
1056
+
1053
1057
  // The slot lookup system will generally prefer power-of-two sizes.
1054
1058
  if (config_.enable_key_lookups) {
1055
1059
  unique_lock_t lock(slot_lookup_mutex_);
1056
1060
  if (!slot_lookup_.try_reserve(limits.members))
1057
1061
  return false;
1058
- limits.members = slot_lookup_.capacity();
1059
1062
  }
1060
1063
 
1061
- // Once the `slot_lookup_` grows, let's use its capacity as the new
1062
- // target for the `vectors_lookup_` to synchronize allocations and
1063
- // expensive index re-organizations.
1064
+ // Hash-table load-factor slack does not need corresponding vector or graph slots.
1064
1065
  if (limits.members != vectors_lookup_.size()) {
1065
1066
  vectors_lookup_t new_vectors_lookup(limits.members);
1066
1067
  if (!new_vectors_lookup)
@@ -1358,6 +1359,11 @@ class index_dense_gt {
1358
1359
  if (!result)
1359
1360
  return result;
1360
1361
 
1362
+ // `offset` is caller-supplied, so every `file.size() - offset` below would
1363
+ // otherwise underflow into a huge span instead of failing.
1364
+ if (offset > file.size())
1365
+ return result.failed("File is corrupted and lacks matrix dimensions");
1366
+
1361
1367
  // Infer the new index size
1362
1368
  std::uint64_t matrix_rows = 0;
1363
1369
  std::uint64_t matrix_cols = 0;
@@ -1383,7 +1389,12 @@ class index_dense_gt {
1383
1389
  matrix_cols = dimensions[1];
1384
1390
  offset += sizeof(dimensions);
1385
1391
  }
1386
- vectors_buffer = {file.data() + offset, static_cast<std::size_t>(matrix_rows * matrix_cols)};
1392
+ // bound the vectors matrix span against the file (overflow-safe; offset <= file.size() holds here)
1393
+ checked_size_result_t vectors_bytes =
1394
+ checked_mul(static_cast<std::size_t>(matrix_rows), static_cast<std::size_t>(matrix_cols));
1395
+ if (!vectors_bytes || file.size() - offset < vectors_bytes.value)
1396
+ return result.failed("File is corrupted: vectors matrix exceeds file size");
1397
+ vectors_buffer = {file.data() + offset, vectors_bytes.value};
1387
1398
  offset += vectors_buffer.size();
1388
1399
  }
1389
1400
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "2.26.0",
3
+ "version": "2.26.1",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ash Vardanian (https://ashvardanian.com/)",
6
6
  "license": "Apache 2.0",
Binary file
Binary file
Binary file