Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/datacell/dense_duplicate_tracker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@

#include "dense_duplicate_tracker.h"

#include <fmt/format.h>

#include <algorithm>

#include "vsag_exception.h"

namespace vsag {

DenseDuplicateTracker::DenseDuplicateTracker(Allocator* allocator)
Expand Down Expand Up @@ -120,11 +124,30 @@ DenseDuplicateTracker::Deserialize(StreamReader& reader) {
for (size_t i = 0; i < duplicate_count_; ++i) {
InnerIdType head_id;
StreamReader::ReadObj(reader, head_id);
if (head_id >= size) {
throw VsagException(
ErrorType::INVALID_BINARY,
fmt::format("head_id {} >= size {} in duplicate tracker", head_id, size));
}
Comment thread
LHT129 marked this conversation as resolved.
if (duplicate_ids_[head_id] != head_id) {
throw VsagException(ErrorType::INVALID_BINARY,
fmt::format("duplicate head_id {} in duplicate tracker", head_id));
}
Comment thread
LHT129 marked this conversation as resolved.
Comment thread
LHT129 marked this conversation as resolved.
Comment thread
LHT129 marked this conversation as resolved.
Vector<InnerIdType> id_list(allocator_);
StreamReader::ReadVector(reader, id_list);

auto current_id = head_id;
for (const auto& dup_id : id_list) {
Comment thread
LHT129 marked this conversation as resolved.
if (dup_id >= size) {
throw VsagException(
ErrorType::INVALID_BINARY,
fmt::format("dup_id {} >= size {} in duplicate tracker", dup_id, size));
}
Comment thread
LHT129 marked this conversation as resolved.
Comment thread
LHT129 marked this conversation as resolved.
Comment thread
LHT129 marked this conversation as resolved.
if (duplicate_ids_[dup_id] != dup_id) {
throw VsagException(
ErrorType::INVALID_BINARY,
fmt::format("duplicate dup_id {} in duplicate tracker", dup_id));
}
duplicate_ids_[current_id] = dup_id;
current_id = dup_id;
Comment thread
LHT129 marked this conversation as resolved.
}
Expand All @@ -150,10 +173,32 @@ DenseDuplicateTracker::DeserializeFromLegacyFormat(StreamReader& reader, size_t
for (InnerIdType i = 0; i < duplicate_count_; ++i) {
InnerIdType id;
StreamReader::ReadObj<InnerIdType>(reader, id);
if (id >= total_size) {
throw VsagException(
ErrorType::INVALID_BINARY,
fmt::format("id {} >= total_size {} in duplicate tracker legacy", id, total_size));
}
Comment thread
LHT129 marked this conversation as resolved.
if (duplicate_ids_[id] != id) {
throw VsagException(ErrorType::INVALID_BINARY,
fmt::format("duplicate id {} in duplicate tracker legacy", id));
}
Comment thread
LHT129 marked this conversation as resolved.
Comment thread
LHT129 marked this conversation as resolved.
Vector<InnerIdType> id_list(allocator_);
StreamReader::ReadVector(reader, id_list);
auto current_id = id;
for (const auto& duplicate_id : id_list) {
Comment thread
LHT129 marked this conversation as resolved.
if (duplicate_id >= total_size) {
throw VsagException(
ErrorType::INVALID_BINARY,
fmt::format("duplicate_id {} >= total_size {} in duplicate tracker legacy",
duplicate_id,
total_size));
}
Comment thread
LHT129 marked this conversation as resolved.
Comment thread
LHT129 marked this conversation as resolved.
Comment thread
LHT129 marked this conversation as resolved.
if (duplicate_ids_[duplicate_id] != duplicate_id) {
throw VsagException(
ErrorType::INVALID_BINARY,
fmt::format("duplicate duplicate_id {} in duplicate tracker legacy",
duplicate_id));
}
duplicate_ids_[current_id] = duplicate_id;
current_id = duplicate_id;
Comment thread
LHT129 marked this conversation as resolved.
}
Expand Down
191 changes: 191 additions & 0 deletions src/datacell/dense_duplicate_tracker_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#include "impl/allocator/default_allocator.h"
#include "unittest.h"
#include "vsag_exception.h"
using namespace vsag;

namespace {
Expand Down Expand Up @@ -131,3 +132,193 @@ TEST_CASE("DenseDuplicateTracker deserializes legacy format", "[ut][DenseDuplica
REQUIRE(tracker.GetGroupId(2) == 0);
REQUIRE(tracker.GetGroupId(5) == 4);
}

TEST_CASE("DenseDuplicateTracker deserialize rejects out-of-range ids",
"[ut][DenseDuplicateTracker]") {
auto allocator = std::make_shared<DefaultAllocator>();

SECTION("head_id out of range") {
std::stringstream ss;
IOStreamWriter writer(ss);
size_t duplicate_count = 1;
size_t size = 4;
StreamWriter::WriteObj(writer, duplicate_count);
StreamWriter::WriteObj(writer, size);

InnerIdType head_id = 10; // >= size
StreamWriter::WriteObj(writer, head_id);
std::vector<InnerIdType> id_list{1};
StreamWriter::WriteVector(writer, id_list);

DenseDuplicateTracker tracker(allocator.get());
IOStreamReader reader(ss);
REQUIRE_THROWS_AS(tracker.Deserialize(reader), VsagException);
}

SECTION("dup_id out of range") {
std::stringstream ss;
IOStreamWriter writer(ss);
size_t duplicate_count = 1;
size_t size = 4;
StreamWriter::WriteObj(writer, duplicate_count);
StreamWriter::WriteObj(writer, size);

InnerIdType head_id = 0;
StreamWriter::WriteObj(writer, head_id);
std::vector<InnerIdType> id_list{99}; // >= size
StreamWriter::WriteVector(writer, id_list);

DenseDuplicateTracker tracker(allocator.get());
IOStreamReader reader(ss);
REQUIRE_THROWS_AS(tracker.Deserialize(reader), VsagException);
}
}

TEST_CASE("DenseDuplicateTracker legacy deserialize rejects out-of-range ids",
"[ut][DenseDuplicateTracker]") {
auto allocator = std::make_shared<DefaultAllocator>();

SECTION("id out of range") {
std::stringstream ss;
IOStreamWriter writer(ss);
size_t duplicate_count = 1;
StreamWriter::WriteObj(writer, duplicate_count);

InnerIdType id = 10; // >= total_size (6)
StreamWriter::WriteObj(writer, id);
std::vector<InnerIdType> id_list{1};
StreamWriter::WriteVector(writer, id_list);

DenseDuplicateTracker tracker(allocator.get());
IOStreamReader reader(ss);
REQUIRE_THROWS_AS(tracker.DeserializeFromLegacyFormat(reader, 6), VsagException);
}

SECTION("duplicate_id out of range") {
std::stringstream ss;
IOStreamWriter writer(ss);
size_t duplicate_count = 1;
StreamWriter::WriteObj(writer, duplicate_count);

InnerIdType id = 0;
StreamWriter::WriteObj(writer, id);
std::vector<InnerIdType> id_list{99}; // >= total_size (6)
StreamWriter::WriteVector(writer, id_list);

DenseDuplicateTracker tracker(allocator.get());
IOStreamReader reader(ss);
REQUIRE_THROWS_AS(tracker.DeserializeFromLegacyFormat(reader, 6), VsagException);
}
}

TEST_CASE("DenseDuplicateTracker deserialize rejects overlapping groups",
"[ut][DenseDuplicateTracker]") {
Comment thread
LHT129 marked this conversation as resolved.
auto allocator = std::make_shared<DefaultAllocator>();

SECTION("duplicate head_id across groups") {
std::stringstream ss;
IOStreamWriter writer(ss);
size_t duplicate_count = 2;
size_t size = 6;
StreamWriter::WriteObj(writer, duplicate_count);
StreamWriter::WriteObj(writer, size);

// group 1: head=0, members=[1]
InnerIdType head0 = 0;
StreamWriter::WriteObj(writer, head0);
std::vector<InnerIdType> group0{1};
StreamWriter::WriteVector(writer, group0);

// group 2: head=0 again — should be rejected
InnerIdType head1 = 0;
StreamWriter::WriteObj(writer, head1);
std::vector<InnerIdType> group1{2};
StreamWriter::WriteVector(writer, group1);

DenseDuplicateTracker tracker(allocator.get());
IOStreamReader reader(ss);
REQUIRE_THROWS_AS(tracker.Deserialize(reader), VsagException);
}
}

TEST_CASE("DenseDuplicateTracker legacy deserialize rejects overlapping groups",
"[ut][DenseDuplicateTracker]") {
Comment thread
LHT129 marked this conversation as resolved.
auto allocator = std::make_shared<DefaultAllocator>();

SECTION("duplicate id across groups") {
std::stringstream ss;
IOStreamWriter writer(ss);
size_t duplicate_count = 2;
StreamWriter::WriteObj(writer, duplicate_count);

InnerIdType id0 = 0;
StreamWriter::WriteObj(writer, id0);
std::vector<InnerIdType> group0{1};
StreamWriter::WriteVector(writer, group0);

InnerIdType id1 = 0; // already used
StreamWriter::WriteObj(writer, id1);
std::vector<InnerIdType> group1{2};
StreamWriter::WriteVector(writer, group1);

DenseDuplicateTracker tracker(allocator.get());
IOStreamReader reader(ss);
REQUIRE_THROWS_AS(tracker.DeserializeFromLegacyFormat(reader, 6), VsagException);
}
}

TEST_CASE("DenseDuplicateTracker deserialize rejects duplicate member ids",
"[ut][DenseDuplicateTracker]") {
Comment thread
LHT129 marked this conversation as resolved.
auto allocator = std::make_shared<DefaultAllocator>();

SECTION("dup_id already used in another group") {
std::stringstream ss;
IOStreamWriter writer(ss);
size_t duplicate_count = 2;
size_t size = 6;
StreamWriter::WriteObj(writer, duplicate_count);
StreamWriter::WriteObj(writer, size);

// group 1: head=0, members=[1]
InnerIdType head0 = 0;
StreamWriter::WriteObj(writer, head0);
std::vector<InnerIdType> group0{1};
StreamWriter::WriteVector(writer, group0);

// group 2: head=2, members=[1] — id 1 already used
InnerIdType head1 = 2;
StreamWriter::WriteObj(writer, head1);
std::vector<InnerIdType> group1{1};
StreamWriter::WriteVector(writer, group1);

DenseDuplicateTracker tracker(allocator.get());
IOStreamReader reader(ss);
REQUIRE_THROWS_AS(tracker.Deserialize(reader), VsagException);
}
}

TEST_CASE("DenseDuplicateTracker legacy deserialize rejects duplicate member ids",
"[ut][DenseDuplicateTracker]") {
Comment thread
LHT129 marked this conversation as resolved.
auto allocator = std::make_shared<DefaultAllocator>();

SECTION("duplicate_id already used in another group") {
std::stringstream ss;
IOStreamWriter writer(ss);
size_t duplicate_count = 2;
StreamWriter::WriteObj(writer, duplicate_count);

InnerIdType id0 = 0;
StreamWriter::WriteObj(writer, id0);
std::vector<InnerIdType> group0{1};
StreamWriter::WriteVector(writer, group0);

InnerIdType id1 = 2;
StreamWriter::WriteObj(writer, id1);
std::vector<InnerIdType> group1{1}; // already used
StreamWriter::WriteVector(writer, group1);

DenseDuplicateTracker tracker(allocator.get());
IOStreamReader reader(ss);
REQUIRE_THROWS_AS(tracker.DeserializeFromLegacyFormat(reader, 6), VsagException);
}
}
Loading