Skip to content

Commit 6348234

Browse files
authored
Fix removing attachments with reserved keys (#115)
* Fix #114: escape reserved attachment keys * link changelog entry to pr and explain escaping
1 parent 164234b commit 6348234

3 files changed

Lines changed: 39 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Fixed
1515

1616
- Fix removing attachments with numeric keys, [PR-113](https://github.com/reductstore/reduct-cpp/pull/113)
17+
- Fix removing attachments with reserved `$`-prefixed keys, [PR-115](https://github.com/reductstore/reduct-cpp/pull/115)
1718

1819
## 1.18.0 - 2026-02-04
1920

src/reduct/bucket.cc

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <optional>
2121
#include <set>
2222
#include <thread>
23+
#include <vector>
2324

2425
#include "reduct/internal/batch_v1.h"
2526
#include "reduct/internal/batch_v2.h"
@@ -251,11 +252,23 @@ class Bucket : public IBucket {
251252
const std::set<std::string>& attachment_keys) const noexcept override {
252253
QueryOptions options;
253254
if (!attachment_keys.empty()) {
255+
std::vector<std::string> escaped_keys;
256+
escaped_keys.reserve(attachment_keys.size());
257+
for (const auto& key : attachment_keys) {
258+
// Keys starting with '$' must be escaped so the server treats them as literals
259+
// instead of interpreting them as messagepack filter operators.
260+
if (!key.empty() && key.front() == '$') {
261+
escaped_keys.emplace_back(fmt::format("${}", key));
262+
} else {
263+
escaped_keys.emplace_back(key);
264+
}
265+
}
266+
254267
nlohmann::json when;
255268
when["$in"] = nlohmann::json::array();
256269
when["$in"].push_back({{"&key", {{"$cast", "string"}}}});
257-
for (const auto& key : attachment_keys) {
258-
when["$in"].push_back(key);
270+
for (const auto& escaped_key : escaped_keys) {
271+
when["$in"].push_back(escaped_key);
259272
}
260273
options.when = when.dump();
261274
}

tests/reduct/entry_api_test.cc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,29 @@ TEST_CASE("reduct::IBucket should remove entry attachments with numeric keys", "
782782
REQUIRE(stored_after.empty());
783783
}
784784

785+
786+
TEST_CASE("reduct::IBucket should remove entry attachments with reserved keys", "[entry_api][1_19]") {
787+
Fixture ctx;
788+
auto [bucket, _] = ctx.client->CreateBucket(kBucketName);
789+
REQUIRE(bucket);
790+
791+
IBucket::AttachmentMap attachments{
792+
{"meta-1", R"({"value":1})"},
793+
{"$system", R"({"value":"test"})"},
794+
{"$internal", R"({"value":"hidden"})"},
795+
};
796+
797+
REQUIRE(bucket->WriteAttachments("entry-1", attachments) == Error::kOk);
798+
REQUIRE(bucket->RemoveAttachments("entry-1", std::set<std::string>{"$system", "$internal"}) == Error::kOk);
799+
800+
auto [stored, err] = bucket->ReadAttachments("entry-1");
801+
REQUIRE(err == Error::kOk);
802+
REQUIRE(stored.size() == 1);
803+
REQUIRE(stored.contains("meta-1"));
804+
REQUIRE(nlohmann::json::parse(stored.at("meta-1")) == nlohmann::json::parse(attachments.at("meta-1")));
805+
}
806+
807+
785808
TEST_CASE("Batch should slice data", "[batch]") {
786809
auto batch = IBucket::Batch();
787810

0 commit comments

Comments
 (0)