diff --git a/lib/bond/CsProtocol.bond b/lib/bond/CsProtocol.bond index d69a287d6..4d34546e3 100644 --- a/lib/bond/CsProtocol.bond +++ b/lib/bond/CsProtocol.bond @@ -101,6 +101,13 @@ struct M365a 2: optional uint64 msp; } +struct MetaData +{ + // TODO(privacy-parity): best-effort field layout. Confirm the canonical Common + // Schema ext.metadata field ordinals/types before relying on the wire contract. + 1: optional uint64 privTags; +} + struct Xbl { 5: optional map claims; @@ -354,6 +361,8 @@ struct Record 35: optional vector extService; 36: optional vector extCs; 37: optional vector extM365a; + // TODO(privacy-parity): 38 is a best-effort ordinal for ext.metadata; confirm against canonical CS. + 38: optional vector extMetadata; 41: optional vector ext; 42: optional vector extMscv; 43: optional vector extIntWeb; diff --git a/lib/bond/generated/CsProtocol_readers.hpp b/lib/bond/generated/CsProtocol_readers.hpp index 3727b8794..d30574a97 100644 --- a/lib/bond/generated/CsProtocol_readers.hpp +++ b/lib/bond/generated/CsProtocol_readers.hpp @@ -722,6 +722,51 @@ bool Deserialize(TReader& reader, ::CsProtocol::M365a& value, bool isBase) return true; } +template +bool Deserialize(TReader& reader, ::CsProtocol::MetaData& value, bool isBase) +{ + if (!reader.ReadStructBegin(isBase)) { + return false; + } + + uint8_t type; + uint16_t id; + for (;;) { + if (!reader.ReadFieldBegin(type, id)) { + return false; + } + + if (type == BT_STOP || type == BT_STOP_BASE) { + if (isBase != (type == BT_STOP_BASE)) { + return false; + } + break; + } + + switch (id) { + case 1: { + if (!reader.ReadUInt64(value.privTags)) { + return false; + } + break; + } + + default: + return false; + } + + if (!reader.ReadFieldEnd()) { + return false; + } + } + + if (!reader.ReadStructEnd(isBase)) { + return false; + } + + return true; +} + template bool Deserialize(TReader& reader, ::CsProtocol::Xbl& value, bool isBase) { @@ -2867,6 +2912,27 @@ bool Deserialize(TReader& reader, ::CsProtocol::Record& value, bool isBase) break; } + case 38: { + uint32_t size4; + uint8_t type4; + if (!reader.ReadContainerBegin(size4, type4)) { + return false; + } + if (type4 != BT_STRUCT) { + return false; + } + value.extMetadata.resize(size4); + for (unsigned i4 = 0; i4 < size4; i4++) { + if (!Deserialize(reader, value.extMetadata[i4], false)) { + return false; + } + } + if (!reader.ReadContainerEnd()) { + return false; + } + break; + } + case 41: { uint32_t size4; uint8_t type4; diff --git a/lib/bond/generated/CsProtocol_writers.hpp b/lib/bond/generated/CsProtocol_writers.hpp index 81faf54e6..c7d33bd04 100644 --- a/lib/bond/generated/CsProtocol_writers.hpp +++ b/lib/bond/generated/CsProtocol_writers.hpp @@ -541,6 +541,22 @@ void Serialize(TWriter& writer, ::CsProtocol::M365a const& value, bool isBase) writer.WriteStructEnd(isBase); } +template +void Serialize(TWriter& writer, ::CsProtocol::MetaData const& value, bool isBase) +{ + writer.WriteStructBegin(nullptr, isBase); + + if (value.privTags != 0) { + writer.WriteFieldBegin(BT_UINT64, 1, nullptr); + writer.WriteUInt64(value.privTags); + writer.WriteFieldEnd(); + } else { + writer.WriteFieldOmitted(BT_UINT64, 1, nullptr); + } + + writer.WriteStructEnd(isBase); +} + template void Serialize(TWriter& writer, ::CsProtocol::Xbl const& value, bool isBase) { @@ -1898,6 +1914,18 @@ void Serialize(TWriter& writer, ::CsProtocol::Record const& value, bool isBase) writer.WriteFieldOmitted(BT_LIST, 37, nullptr); } + if (!value.extMetadata.empty()) { + writer.WriteFieldBegin(BT_LIST, 38, nullptr); + writer.WriteContainerBegin(value.extMetadata.size(), BT_STRUCT); + for (auto const& item2 : value.extMetadata) { + Serialize(writer, item2, false); + } + writer.WriteContainerEnd(); + writer.WriteFieldEnd(); + } else { + writer.WriteFieldOmitted(BT_LIST, 38, nullptr); + } + if (!value.ext.empty()) { writer.WriteFieldBegin(BT_LIST, 41, nullptr); writer.WriteContainerBegin(value.ext.size(), BT_STRUCT); diff --git a/lib/decorators/EventPropertiesDecorator.hpp b/lib/decorators/EventPropertiesDecorator.hpp index 5bb3e927a..814462460 100644 --- a/lib/decorators/EventPropertiesDecorator.hpp +++ b/lib/decorators/EventPropertiesDecorator.hpp @@ -7,6 +7,7 @@ #include "IDecorator.hpp" #include "EventProperties.hpp" +#include "CommonFields.h" #include "CorrelationVector.hpp" #include "utils/Utils.hpp" @@ -172,6 +173,23 @@ namespace MAT_NS_BEGIN { } const auto &k = kv.first; const auto &v = kv.second; + + // Route the privacy tag (EventInfo.PrivTags) into ext.metadata.privTags so the + // cross-platform bond path carries it the same way the UTC path does. extMetadata + // is the single source of truth: a well-typed int64 value is moved out of Part C + // (the JSON path reads it from extMetadata too). Non-int64 values are left to be + // handled as an ordinary property below. + // TODO(privacy-parity): confirm the canonical Common Schema ext.metadata wire contract. + if (k == COMMONFIELDS_EVENT_PRIVTAGS && v.type == EventProperty::TYPE_INT64) + { + if (record.extMetadata.empty()) + { + record.extMetadata.push_back(::CsProtocol::MetaData()); + } + record.extMetadata[0].privTags = static_cast(v.as_int64); + continue; + } + if (v.piiKind != PiiKind_None) { if (v.piiKind == PiiKind::CustomerContentKind_GenericData) diff --git a/lib/include/public/CsProtocol_types.hpp b/lib/include/public/CsProtocol_types.hpp index e0fcfdf5f..687fd6291 100644 --- a/lib/include/public/CsProtocol_types.hpp +++ b/lib/include/public/CsProtocol_types.hpp @@ -330,6 +330,22 @@ struct M365a { } }; +struct MetaData { + // 1: optional uint64 privTags + // TODO(privacy-parity): best-effort layout; confirm canonical Common Schema ext.metadata. + uint64_t privTags = 0; + + bool operator==(MetaData const& other) const + { + return (privTags == other.privTags); + } + + bool operator!=(MetaData const& other) const + { + return !(*this == other); + } +}; + struct Xbl { // 5: optional map claims std::map claims; @@ -1012,6 +1028,8 @@ struct Record { #endif // 37: optional vector extM365a std::vector< ::CsProtocol::M365a> extM365a; + // 38: optional vector extMetadata + std::vector< ::CsProtocol::MetaData> extMetadata; // 41: optional vector ext std::vector< ::CsProtocol::Data> ext; #ifdef HAVE_CS4_FULL @@ -1065,6 +1083,7 @@ struct Record { && (extCs == other.extCs) #endif && (extM365a == other.extM365a) + && (extMetadata == other.extMetadata) && (ext == other.ext) #ifdef HAVE_CS4_FULL && (extMscv == other.extMscv) diff --git a/lib/system/JsonFormatter.cpp b/lib/system/JsonFormatter.cpp index 71b31e4e9..37f0aaf5f 100644 --- a/lib/system/JsonFormatter.cpp +++ b/lib/system/JsonFormatter.cpp @@ -148,9 +148,11 @@ namespace MAT_NS_BEGIN ans["iKey"] = iKey; if (!source->cV.empty()) ans[CorrelationVector::PropertyName] = source->cV; - if (source->data[0].properties.find(COMMONFIELDS_EVENT_PRIVTAGS) != source->data[0].properties.end()) { - ans["ext"]["metadata"]["privTags"] = source->data[0].properties[COMMONFIELDS_EVENT_PRIVTAGS].longValue; - source->data[0].properties.erase(COMMONFIELDS_EVENT_PRIVTAGS); + // For well-typed int64 values, privTags is carried in record.extMetadata (populated by + // EventPropertiesDecorator); emit it from there whenever present. Non-int64 values are left + // as ordinary Part C properties and serialized through the normal data path below. + if (!source->extMetadata.empty()) { + ans["ext"]["metadata"]["privTags"] = source->extMetadata[0].privTags; } addExtApp(ans, source->extApp); addExtNet(ans, source->extNet); @@ -178,7 +180,6 @@ namespace MAT_NS_BEGIN source->data[0].properties.erase(COMMONFIELDS_APP_VERSION); source->data[0].properties.erase(COMMONFIELDS_EVENT_NAME); source->data[0].properties.erase(COMMONFIELDS_EVENT_INITID); - source->data[0].properties.erase(COMMONFIELDS_EVENT_PRIVTAGS); source->data[0].properties.erase(COMMONFIELDS_METADATA_VIEWINGPRODUCERID); source->data[0].properties.erase(COMMONFIELDS_METADATA_VIEWINGCATEGORY); source->data[0].properties.erase(COMMONFIELDS_METADATA_VIEWINGPAYLOADDECODERPATH); diff --git a/tests/unittests/EventPropertiesDecoratorTests.cpp b/tests/unittests/EventPropertiesDecoratorTests.cpp index f38444fd0..bcc8ba2a5 100644 --- a/tests/unittests/EventPropertiesDecoratorTests.cpp +++ b/tests/unittests/EventPropertiesDecoratorTests.cpp @@ -545,3 +545,41 @@ TEST(EventPropertiesDecoratorTests, DropPiiPartA_StripsValues) EXPECT_THAT(record->extSdk[0].installId, Eq("")); EXPECT_THAT(record->cV, Eq("")); } + +TEST(EventPropertiesDecoratorTests, Decorate_PrivTags_RoutedToExtMetadata) +{ + NullLogManager logManager; + EventPropertiesDecorator decorator(logManager); + Record record; + EventProperties props{"TestEvent"}; + const int64_t privTags = 0x0000000800000002; + props.SetProperty(COMMONFIELDS_EVENT_PRIVTAGS, privTags); + EventLatency latency = EventLatency::EventLatency_Normal; + + EXPECT_TRUE(decorator.decorate(record, latency, props)); + + // EventInfo.PrivTags is routed into ext.metadata.privTags (single source of truth) ... + ASSERT_THAT(record.extMetadata, SizeIs(1)); + EXPECT_THAT(record.extMetadata[0].privTags, Eq(static_cast(privTags))); + // ... and is not also emitted as a Part C property. + EXPECT_THAT(record.data[0].properties.find(COMMONFIELDS_EVENT_PRIVTAGS), + Eq(record.data[0].properties.end())); +} + +TEST(EventPropertiesDecoratorTests, Decorate_PrivTags_NonInt64_FallsThroughToPartC) +{ + NullLogManager logManager; + EventPropertiesDecorator decorator(logManager); + Record record; + EventProperties props{"TestEvent"}; + // A non-int64 PrivTags value must not be routed via the inactive union member; + // it falls through to normal Part C property handling instead. + props.SetProperty(COMMONFIELDS_EVENT_PRIVTAGS, std::string{"not-an-int"}); + EventLatency latency = EventLatency::EventLatency_Normal; + + EXPECT_TRUE(decorator.decorate(record, latency, props)); + + EXPECT_THAT(record.extMetadata, SizeIs(0)); + EXPECT_THAT(record.data[0].properties.find(COMMONFIELDS_EVENT_PRIVTAGS), + Ne(record.data[0].properties.end())); +}