mirror of
https://github.com/savoirfairelinux/opendht.git
synced 2026-11-08 12:28:42 +00:00
Query::isSatisfiedBy() considers an unfiltered answer sufficient for a filtered query, which is true for a vanilla get: onGetValuesDone() applies the operation filter to the values before calling back. A projected query is different. It receives the fields another query selected and can no longer evaluate its own filters on them, all the more so as those filters may target fields that were not selected at all. Two concurrent queries such as "SELECT id" and "SELECT id WHERE value_type=1" therefore made the filtered callback report identifiers whose type does not match, and searchNodeGetDone() additionally cancelled its own request so it never got a correctly filtered answer. Add Query::isProjectionSatisfiedBy(), which additionally requires both queries to apply the same filters, and use it for operations with a query callback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
669 lines
19 KiB
C++
669 lines
19 KiB
C++
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "value.h"
|
|
|
|
#include "default_types.h"
|
|
#include "securedht.h" // print certificate ID
|
|
|
|
#ifdef OPENDHT_JSONCPP
|
|
#include "base64.h"
|
|
#endif
|
|
|
|
namespace dht {
|
|
|
|
const std::string Query::QUERY_PARSE_ERROR {"Error parsing query."};
|
|
|
|
Value::Filter
|
|
bindFilterRaw(FilterRaw raw_filter, void* user_data)
|
|
{
|
|
if (not raw_filter)
|
|
return {};
|
|
return [=](const Value& value) {
|
|
return raw_filter(value, user_data);
|
|
};
|
|
}
|
|
|
|
std::ostream&
|
|
operator<<(std::ostream& s, const Value& v)
|
|
{
|
|
auto flags(s.flags());
|
|
s << "Value[id:" << std::hex << v.id << std::dec << ' ';
|
|
if (v.isEncrypted())
|
|
s << "encrypted ";
|
|
else if (v.isSigned()) {
|
|
s << "signed (v" << v.seq << ") ";
|
|
if (v.recipient)
|
|
s << "decrypted ";
|
|
}
|
|
if (not v.isEncrypted()) {
|
|
if (v.type == IpServiceAnnouncement::TYPE.id) {
|
|
s << IpServiceAnnouncement(v.data);
|
|
} else if (v.type == CERTIFICATE_TYPE.id) {
|
|
s << "Certificate";
|
|
#ifdef OPENDHT_LOG_CRT_ID
|
|
try {
|
|
auto h = crypto::Certificate(v.data).getPublicKey().getLongId();
|
|
s << " with ID " << h;
|
|
} catch (const std::exception& e) {
|
|
s << " (invalid)";
|
|
}
|
|
#endif
|
|
} else {
|
|
if (v.user_type.empty())
|
|
s << "data:";
|
|
else
|
|
s << "data(" << v.user_type << "):";
|
|
if (v.user_type == "text/plain") {
|
|
s << '"';
|
|
s.write(reinterpret_cast<const char*>(v.data.data()), v.data.size());
|
|
s << '"';
|
|
} else if (v.data.size() < 1024) {
|
|
s << toHex(v.data.data(), v.data.size());
|
|
} else {
|
|
s << v.data.size() << " bytes";
|
|
}
|
|
}
|
|
}
|
|
s << ']';
|
|
s.flags(flags);
|
|
return s;
|
|
}
|
|
|
|
const ValueType ValueType::USER_DATA = {0, "User Data"};
|
|
|
|
bool
|
|
ValueType::DEFAULT_STORE_POLICY(InfoHash, const std::shared_ptr<Value>& v, const InfoHash&, const SockAddr&)
|
|
{
|
|
return v->size() <= MAX_VALUE_SIZE;
|
|
}
|
|
|
|
size_t
|
|
Value::size() const
|
|
{
|
|
return cypher.size() + data.size() + signature.size() + user_type.size() + pushType.size();
|
|
}
|
|
|
|
void
|
|
Value::msgpack_unpack(const msgpack::object& o)
|
|
{
|
|
if (o.type != msgpack::type::MAP or o.via.map.size < 2)
|
|
throw msgpack::type_error();
|
|
|
|
if (auto rid = findMapValue(o, VALUE_KEY_ID)) {
|
|
id = rid->as<Id>();
|
|
} else
|
|
throw msgpack::type_error();
|
|
|
|
if (auto rdat = findMapValue(o, VALUE_KEY_DAT)) {
|
|
msgpack_unpack_body(*rdat);
|
|
} else
|
|
throw msgpack::type_error();
|
|
|
|
priority = 0;
|
|
pushType.clear();
|
|
if (auto rprio = findMapValue(o, VALUE_KEY_PRIO)) {
|
|
priority = rprio->as<unsigned>();
|
|
}
|
|
if (auto rpt = findMapValue(o, VALUE_KEY_PUSHTYPE)) {
|
|
pushType = rpt->as<std::string>();
|
|
}
|
|
}
|
|
|
|
void
|
|
Value::msgpack_unpack_body(const msgpack::object& o)
|
|
{
|
|
owner = {};
|
|
recipient = {};
|
|
cypher.clear();
|
|
signature.clear();
|
|
data.clear();
|
|
type = 0;
|
|
|
|
if (o.type == msgpack::type::BIN) {
|
|
auto dat = o.as<std::vector<char>>();
|
|
cypher = {dat.begin(), dat.end()};
|
|
} else {
|
|
if (o.type != msgpack::type::MAP)
|
|
throw msgpack::type_error();
|
|
auto rbody = findMapValue(o, VALUE_KEY_BODY);
|
|
if (not rbody)
|
|
throw msgpack::type_error();
|
|
|
|
if (auto rdata = findMapValue(*rbody, VALUE_KEY_DATA)) {
|
|
data = unpackBlob(*rdata);
|
|
} else
|
|
throw msgpack::type_error();
|
|
|
|
if (auto rtype = findMapValue(*rbody, VALUE_KEY_TYPE)) {
|
|
type = rtype->as<ValueType::Id>();
|
|
} else
|
|
throw msgpack::type_error();
|
|
|
|
if (auto rutype = findMapValue(*rbody, VALUE_KEY_USERTYPE)) {
|
|
user_type = rutype->as<std::string>();
|
|
}
|
|
|
|
if (auto rowner = findMapValue(*rbody, VALUE_KEY_OWNER)) {
|
|
if (auto rseq = findMapValue(*rbody, VALUE_KEY_SEQ))
|
|
seq = rseq->as<decltype(seq)>();
|
|
else
|
|
throw msgpack::type_error();
|
|
crypto::PublicKey new_owner;
|
|
new_owner.msgpack_unpack(*rowner);
|
|
owner = std::make_shared<crypto::PublicKey>(std::move(new_owner));
|
|
if (auto rrecipient = findMapValue(*rbody, VALUE_KEY_TO)) {
|
|
recipient = rrecipient->as<InfoHash>();
|
|
}
|
|
|
|
if (auto rsig = findMapValue(o, VALUE_KEY_SIGNATURE)) {
|
|
signature = unpackBlob(*rsig);
|
|
} else
|
|
throw msgpack::type_error();
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef OPENDHT_JSONCPP
|
|
Value::Value(const Json::Value& json)
|
|
{
|
|
id = Value::Id(unpackId(json, VALUE_KEY_ID));
|
|
const auto& jcypher = json["cypher"];
|
|
if (jcypher.isString())
|
|
cypher = base64_decode(jcypher.asString());
|
|
const auto& jsig = json[VALUE_KEY_SIGNATURE];
|
|
if (jsig.isString())
|
|
signature = base64_decode(jsig.asString());
|
|
const auto& jseq = json[VALUE_KEY_SEQ];
|
|
if (!jseq.isNull())
|
|
seq = jseq.asInt();
|
|
const auto& jowner = json[VALUE_KEY_OWNER];
|
|
if (jowner.isString()) {
|
|
auto ownerStr = jowner.asString();
|
|
auto ownerBlob = std::vector<unsigned char>(ownerStr.begin(), ownerStr.end());
|
|
owner = std::make_shared<crypto::PublicKey>(ownerBlob);
|
|
}
|
|
const auto& jto = json[VALUE_KEY_TO];
|
|
if (jto.isString())
|
|
recipient = InfoHash(jto.asString());
|
|
const auto& jtype = json[VALUE_KEY_TYPE];
|
|
if (!jtype.isNull())
|
|
type = jtype.asInt();
|
|
const auto& jdata = json[VALUE_KEY_DATA];
|
|
if (jdata.isString())
|
|
data = base64_decode(jdata.asString());
|
|
const auto& jutype = json[VALUE_KEY_USERTYPE];
|
|
if (jutype.isString())
|
|
user_type = jutype.asString();
|
|
const auto& jprio = json["prio"];
|
|
if (jprio.isIntegral())
|
|
priority = jprio.asUInt();
|
|
const auto& jpt = json[VALUE_KEY_PUSHTYPE];
|
|
if (jpt.isString())
|
|
pushType = jpt.asString();
|
|
}
|
|
|
|
Json::Value
|
|
Value::toJson() const
|
|
{
|
|
Json::Value val;
|
|
val[VALUE_KEY_ID] = std::to_string(id);
|
|
if (isEncrypted()) {
|
|
val["cypher"] = base64_encode(cypher);
|
|
} else {
|
|
if (isSigned())
|
|
val[VALUE_KEY_SIGNATURE] = base64_encode(signature);
|
|
bool has_owner = owner && *owner;
|
|
if (has_owner) { // isSigned
|
|
val[VALUE_KEY_SEQ] = seq;
|
|
val[VALUE_KEY_OWNER] = owner->toString();
|
|
if (recipient)
|
|
val[VALUE_KEY_TO] = recipient.toString();
|
|
}
|
|
val[VALUE_KEY_TYPE] = type;
|
|
val[VALUE_KEY_DATA] = base64_encode(data);
|
|
if (not user_type.empty())
|
|
val[VALUE_KEY_USERTYPE] = user_type;
|
|
}
|
|
if (priority)
|
|
val["prio"] = priority;
|
|
if (not pushType.empty())
|
|
val[VALUE_KEY_PUSHTYPE] = pushType;
|
|
return val;
|
|
}
|
|
|
|
uint64_t
|
|
unpackId(const Json::Value& json, const std::string& key)
|
|
{
|
|
uint64_t ret = 0;
|
|
try {
|
|
const auto& t = json[key];
|
|
if (t.isString()) {
|
|
ret = std::stoull(t.asString());
|
|
} else {
|
|
ret = t.asLargestUInt();
|
|
}
|
|
} catch (...) {
|
|
}
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
void
|
|
Value::sign(const crypto::PrivateKey& key)
|
|
{
|
|
if (isEncrypted())
|
|
throw DhtException("Can't sign encrypted data.");
|
|
owner = key.getSharedPublicKey();
|
|
signature = key.sign(getToSign());
|
|
}
|
|
|
|
Value
|
|
Value::encrypt(const crypto::PrivateKey& from, const crypto::PublicKey& to)
|
|
{
|
|
if (isEncrypted())
|
|
throw DhtException("Data is already encrypted.");
|
|
setRecipient(to.getId());
|
|
sign(from);
|
|
Value nv {id};
|
|
nv.priority = priority;
|
|
nv.pushType = pushType;
|
|
nv.setCypher(to.encrypt(getToEncrypt()));
|
|
return nv;
|
|
}
|
|
|
|
bool
|
|
Value::checkSignature()
|
|
{
|
|
if (!signatureChecked) {
|
|
signatureChecked = true;
|
|
if (isSigned()) {
|
|
signatureValid = owner and owner->checkSignature(getToSign(), signature);
|
|
} else {
|
|
signatureValid = true;
|
|
}
|
|
}
|
|
return signatureValid;
|
|
}
|
|
|
|
Sp<Value>
|
|
Value::decrypt(const crypto::PrivateKey& key)
|
|
{
|
|
if (not decrypted) {
|
|
decrypted = true;
|
|
if (isEncrypted()) {
|
|
auto decryptedBlob = key.decrypt(cypher);
|
|
auto msg = msgpack::unpack((const char*) decryptedBlob.data(), decryptedBlob.size());
|
|
auto v = std::make_unique<Value>(id);
|
|
v->msgpack_unpack_body(msg.get());
|
|
if (v->recipient != key.getPublicKey().getId())
|
|
throw crypto::DecryptError("Recipient mismatch");
|
|
// Ignore values belonging to other people
|
|
if (not v->owner or not v->owner->checkSignature(v->getToSign(), v->signature))
|
|
throw crypto::DecryptError("Signature mismatch");
|
|
decryptedValue = std::move(v);
|
|
}
|
|
}
|
|
return decryptedValue;
|
|
}
|
|
|
|
bool
|
|
FieldValue::operator==(const FieldValue& vfd) const
|
|
{
|
|
if (field != vfd.field)
|
|
return false;
|
|
switch (field) {
|
|
case Value::Field::Id:
|
|
case Value::Field::ValueType:
|
|
case Value::Field::SeqNum:
|
|
return intValue == vfd.intValue;
|
|
case Value::Field::OwnerPk:
|
|
return hashValue == vfd.hashValue;
|
|
case Value::Field::UserType:
|
|
return blobValue == vfd.blobValue;
|
|
case Value::Field::None:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Value::Filter
|
|
FieldValue::getLocalFilter() const
|
|
{
|
|
switch (field) {
|
|
case Value::Field::Id:
|
|
return Value::IdFilter(intValue);
|
|
case Value::Field::ValueType:
|
|
return Value::TypeFilter(intValue);
|
|
case Value::Field::OwnerPk:
|
|
return Value::OwnerFilter(hashValue);
|
|
case Value::Field::SeqNum:
|
|
return Value::SeqNumFilter(intValue);
|
|
case Value::Field::UserType:
|
|
return Value::UserTypeFilter(std::string {blobValue.begin(), blobValue.end()});
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
FieldValueIndex::FieldValueIndex(const Value& v, const Select& s)
|
|
{
|
|
if (not s.empty()) {
|
|
auto selection = s.getSelection();
|
|
std::transform(selection.begin(),
|
|
selection.end(),
|
|
std::inserter(index, index.end()),
|
|
[](const std::set<Value::Field>::value_type& f) { return std::make_pair(f, FieldValue {}); });
|
|
} else {
|
|
index.clear();
|
|
for (size_t f = 1; f < static_cast<int>(Value::Field::COUNT); ++f)
|
|
index[static_cast<Value::Field>(f)] = {};
|
|
}
|
|
for (const auto& fvp : index) {
|
|
const auto& f = fvp.first;
|
|
switch (f) {
|
|
case Value::Field::Id:
|
|
index[f] = {f, v.id};
|
|
break;
|
|
case Value::Field::ValueType:
|
|
index[f] = {f, v.type};
|
|
break;
|
|
case Value::Field::OwnerPk:
|
|
index[f] = {f, v.owner ? v.owner->getId() : InfoHash()};
|
|
break;
|
|
case Value::Field::SeqNum:
|
|
index[f] = {f, v.seq};
|
|
break;
|
|
case Value::Field::UserType:
|
|
index[f] = {
|
|
f, Blob {v.user_type.begin(), v.user_type.end()}
|
|
};
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
FieldValueIndex::containedIn(const FieldValueIndex& other) const
|
|
{
|
|
if (index.size() > other.index.size())
|
|
return false;
|
|
for (const auto& field : index) {
|
|
auto other_field = other.index.find(field.first);
|
|
if (other_field == other.index.end() or not (field.second == other_field->second))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::ostream&
|
|
operator<<(std::ostream& os, const FieldValueIndex& fvi)
|
|
{
|
|
os << "Index[";
|
|
for (auto v = fvi.index.begin(); v != fvi.index.end(); ++v) {
|
|
switch (v->first) {
|
|
case Value::Field::Id: {
|
|
auto flags(os.flags());
|
|
os << "Id:" << std::hex << v->second.getInt();
|
|
os.flags(flags);
|
|
break;
|
|
}
|
|
case Value::Field::ValueType:
|
|
os << "ValueType:" << v->second.getInt();
|
|
break;
|
|
case Value::Field::OwnerPk:
|
|
os << "Owner:" << v->second.getHash();
|
|
break;
|
|
case Value::Field::SeqNum:
|
|
os << "Seq:" << v->second.getInt();
|
|
break;
|
|
case Value::Field::UserType: {
|
|
auto ut = v->second.getBlob();
|
|
os << "UserType:" << std::string(ut.begin(), ut.end());
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
os << (std::next(v) != fvi.index.end() ? "," : "");
|
|
}
|
|
return os << "]";
|
|
}
|
|
|
|
void
|
|
FieldValueIndex::msgpack_unpack_fields(const std::set<Value::Field>& fields, const msgpack::object& o, unsigned offset)
|
|
{
|
|
index.clear();
|
|
|
|
unsigned j = 0;
|
|
for (const auto& field : fields) {
|
|
auto& field_value = o.via.array.ptr[offset + (j++)];
|
|
switch (field) {
|
|
case Value::Field::Id:
|
|
case Value::Field::ValueType:
|
|
case Value::Field::SeqNum:
|
|
index[field] = FieldValue(field, field_value.as<uint64_t>());
|
|
break;
|
|
case Value::Field::OwnerPk:
|
|
index[field] = FieldValue(field, field_value.as<InfoHash>());
|
|
break;
|
|
case Value::Field::UserType:
|
|
index[field] = FieldValue(field, field_value.as<Blob>());
|
|
break;
|
|
default:
|
|
throw msgpack::type_error();
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
trim_str(std::string& str)
|
|
{
|
|
auto first = std::min(str.size(), str.find_first_not_of(' '));
|
|
auto last = std::min(str.size(), str.find_last_not_of(' '));
|
|
str = str.substr(first, last - first + 1);
|
|
}
|
|
|
|
Select::Select(std::string_view q_str)
|
|
{
|
|
std::istringstream q_iss {std::string(q_str)};
|
|
std::string token {};
|
|
q_iss >> token;
|
|
|
|
if (token == "SELECT" or token == "select") {
|
|
q_iss >> token;
|
|
std::istringstream fields {token};
|
|
|
|
while (std::getline(fields, token, ',')) {
|
|
trim_str(token);
|
|
if (token == VALUE_KEY_ID)
|
|
field(Value::Field::Id);
|
|
else if (token == "value_type")
|
|
field(Value::Field::ValueType);
|
|
else if (token == "owner_pk")
|
|
field(Value::Field::OwnerPk);
|
|
if (token == VALUE_KEY_SEQ)
|
|
field(Value::Field::SeqNum);
|
|
else if (token == "user_type")
|
|
field(Value::Field::UserType);
|
|
}
|
|
}
|
|
}
|
|
|
|
Where::Where(std::string_view q_str)
|
|
{
|
|
std::istringstream q_iss {std::string(q_str)};
|
|
std::string token {};
|
|
q_iss >> token;
|
|
if (token == "WHERE" or token == "where") {
|
|
std::getline(q_iss, token);
|
|
std::istringstream restrictions {token};
|
|
while (std::getline(restrictions, token, ',')) {
|
|
trim_str(token);
|
|
std::istringstream eq_ss {token};
|
|
std::string field_str, value_str;
|
|
std::getline(eq_ss, field_str, '=');
|
|
trim_str(field_str);
|
|
std::getline(eq_ss, value_str, '=');
|
|
trim_str(value_str);
|
|
|
|
if (not value_str.empty()) {
|
|
uint64_t v = 0;
|
|
std::string s {};
|
|
std::istringstream convert {value_str};
|
|
convert >> v;
|
|
if (not convert and value_str.size() > 1 and value_str[0] == '"'
|
|
and value_str[value_str.size() - 1] == '"')
|
|
s = value_str.substr(1, value_str.size() - 2);
|
|
else
|
|
s = value_str;
|
|
if (field_str == VALUE_KEY_ID)
|
|
id(v);
|
|
else if (field_str == "value_type")
|
|
valueType(v);
|
|
else if (field_str == "owner_pk")
|
|
owner(InfoHash(s));
|
|
else if (field_str == VALUE_KEY_SEQ)
|
|
seq(v);
|
|
else if (field_str == "user_type")
|
|
userType(s);
|
|
else
|
|
throw std::invalid_argument(Query::QUERY_PARSE_ERROR
|
|
+ " (WHERE) incorrect token near: " + field_str);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
Query::msgpack_unpack(const msgpack::object& o)
|
|
{
|
|
if (o.type != msgpack::type::MAP)
|
|
throw msgpack::type_error();
|
|
|
|
auto rfilters = findMapValue(o, "w"sv); /* unpacking filters */
|
|
if (rfilters)
|
|
where.msgpack_unpack(*rfilters);
|
|
else
|
|
throw msgpack::type_error();
|
|
|
|
auto rfield_selector = findMapValue(o, "s"sv); /* unpacking field selectors */
|
|
if (rfield_selector)
|
|
select.msgpack_unpack(*rfield_selector);
|
|
else
|
|
throw msgpack::type_error();
|
|
}
|
|
|
|
template<typename T>
|
|
bool
|
|
subset(const std::vector<T>& fds, const std::vector<T>& qfds)
|
|
{
|
|
for (const auto& fd : fds) {
|
|
if (std::find_if(qfds.begin(), qfds.end(), [&fd](const T& _vfd) { return fd == _vfd; }) == qfds.end())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
Select::isSatisfiedBy(const Select& os) const
|
|
{
|
|
/* empty, means all values are selected. */
|
|
return fieldSelection_.empty() ? os.fieldSelection_.empty() : subset(fieldSelection_, os.fieldSelection_);
|
|
}
|
|
|
|
bool
|
|
Where::isSatisfiedBy(const Where& ow) const
|
|
{
|
|
return subset(ow.filters_, filters_);
|
|
}
|
|
|
|
bool
|
|
Query::isSatisfiedBy(const Query& q) const
|
|
{
|
|
return none or (where.isSatisfiedBy(q.where) and select.isSatisfiedBy(q.select));
|
|
}
|
|
|
|
bool
|
|
Query::isProjectionSatisfiedBy(const Query& q) const
|
|
{
|
|
/* A projected answer can not be filtered afterwards, so requiring the
|
|
filters of both queries to be a subset of each other, that is equal. */
|
|
return none or (isSatisfiedBy(q) and q.where.isSatisfiedBy(where));
|
|
}
|
|
|
|
std::ostream&
|
|
operator<<(std::ostream& s, const dht::Select& select)
|
|
{
|
|
s << "SELECT ";
|
|
if (select.fieldSelection_.empty())
|
|
s << '*';
|
|
else
|
|
for (auto fs = select.fieldSelection_.begin(); fs != select.fieldSelection_.end();) {
|
|
switch (*fs) {
|
|
case Value::Field::Id:
|
|
s << VALUE_KEY_ID;
|
|
break;
|
|
case Value::Field::ValueType:
|
|
s << "value_type";
|
|
break;
|
|
case Value::Field::UserType:
|
|
s << "user_type";
|
|
break;
|
|
case Value::Field::OwnerPk:
|
|
s << "owner_public_key";
|
|
break;
|
|
case Value::Field::SeqNum:
|
|
s << VALUE_KEY_SEQ;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (++fs != select.fieldSelection_.end())
|
|
s << ',';
|
|
}
|
|
return s;
|
|
}
|
|
|
|
std::ostream&
|
|
operator<<(std::ostream& s, const dht::Where& where)
|
|
{
|
|
if (not where.filters_.empty()) {
|
|
s << "WHERE ";
|
|
for (auto f = where.filters_.begin(); f != where.filters_.end(); ++f) {
|
|
switch (f->getField()) {
|
|
case Value::Field::Id:
|
|
s << VALUE_KEY_ID << '=' << f->getInt();
|
|
break;
|
|
case Value::Field::ValueType:
|
|
s << "value_type=" << f->getInt();
|
|
break;
|
|
case Value::Field::OwnerPk:
|
|
s << "owner_pk_hash=" << f->getHash();
|
|
break;
|
|
case Value::Field::SeqNum:
|
|
s << VALUE_KEY_SEQ << '=' << f->getInt();
|
|
break;
|
|
case Value::Field::UserType: {
|
|
auto b = f->getBlob();
|
|
s << "user_type=" << std::string {b.begin(), b.end()};
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
s << (std::next(f) != where.filters_.end() ? "," : "");
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
} // namespace dht
|