Skip to content

Commit

Permalink
MONGOCRYPT-705 Add crypto parameters to payloads (#872)
Browse files Browse the repository at this point in the history
* apply crypto params to `mc_FLE2InsertUpdatePayloadV2_t`

Add new fields. Note fields that only applicable to range payloads

* change `_assert_match_bson` to a macro

To include the line number of the macro invocation on error. And remove unnecessary NULL check.

* apply crypto params to `mc_FLE2FindRangePayloadV2_t`

Fix documentation for `FLE2FindRangePayloadV2`. `g` is nested inside a `payload` document. See server IDL for reference.

* read random numbers as little-endian

To fix new tests on zSeries. May simplify testing with deterministic random-number generation on big-endian machines.

* update payloads in existing tests

Payloads now include crypto param fields.

* update payloads in Python and Java tests

Notably: java bindings test with `trimFactor` of 0. Python tests with `trimFactor` of 1.

* represent `trimFactor` as an `int32_t`

To match server representation

* include value of trimFactor in error message, error if negative

* represent `precision` as an `int32_t`

To match server representation
  • Loading branch information
kevinAlbs committed Jul 31, 2024
1 parent 3fd7377 commit bb12cda
Show file tree
Hide file tree
Showing 48 changed files with 1,137 additions and 187 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"age": {
"$gte": {
"$binary": {
"base64": "Dd0BAAADcGF5bG9hZACZAQAABGcAhQEAAAMwAH0AAAAFZAAgAAAAAInd0noBhIiJMv8QTjcfgRqnnVhxRJRRACLfvgT+CTR/BXMAIAAAAADm0EjqF/T4EmR6Dw6NaPLrL0OuzS4AFvm90czFluAAygVsACAAAAAA5MXcYWjYlzhPFUDebBEa17B5z2bupmaW9uCdtLjc7RkAAzEAfQAAAAVkACAAAAAA7lkNtT6RLw91aJ07K/blwlFs5wi9pQjqUXDcaCTxe98FcwAgAAAAAPwySffuLQihmF70Ot93KtaUMNU8KpmA+niyPRcvarNMBWwAIAAAAACDv6fJXXwRqwZH3O2kO+hdeLZ36U6bMZSui8kv0PsPtAADMgB9AAAABWQAIAAAAACcMWVTbZC4ox5VdjWeYKLgf4oBjpPlbTTAkucm9JPK0wVzACAAAAAA3tIww4ZTytkxFsUKyJbc3zwQ2w7DhkOqaNvX9g8pi3gFbAAgAAAAAGs9XR3Q1JpxV+HPW8P2GvCuCBF5bGZ8Kl1zHqzZcd5/AAASY20ABAAAAAAAAAAAEHBheWxvYWRJZAAAAAAAEGZpcnN0T3BlcmF0b3IAAgAAABBzZWNvbmRPcGVyYXRvcgAEAAAAAA==",
"base64": "DQECAAADcGF5bG9hZACZAQAABGcAhQEAAAMwAH0AAAAFZAAgAAAAAInd0noBhIiJMv8QTjcfgRqnnVhxRJRRACLfvgT+CTR/BXMAIAAAAADm0EjqF/T4EmR6Dw6NaPLrL0OuzS4AFvm90czFluAAygVsACAAAAAA5MXcYWjYlzhPFUDebBEa17B5z2bupmaW9uCdtLjc7RkAAzEAfQAAAAVkACAAAAAA7lkNtT6RLw91aJ07K/blwlFs5wi9pQjqUXDcaCTxe98FcwAgAAAAAPwySffuLQihmF70Ot93KtaUMNU8KpmA+niyPRcvarNMBWwAIAAAAACDv6fJXXwRqwZH3O2kO+hdeLZ36U6bMZSui8kv0PsPtAADMgB9AAAABWQAIAAAAACcMWVTbZC4ox5VdjWeYKLgf4oBjpPlbTTAkucm9JPK0wVzACAAAAAA3tIww4ZTytkxFsUKyJbc3zwQ2w7DhkOqaNvX9g8pi3gFbAAgAAAAAGs9XR3Q1JpxV+HPW8P2GvCuCBF5bGZ8Kl1zHqzZcd5/AAASY20ABAAAAAAAAAAAEHBheWxvYWRJZAAAAAAAEGZpcnN0T3BlcmF0b3IAAgAAABBzZWNvbmRPcGVyYXRvcgAEAAAAEnNwAAEAAAAAAAAAEHRmAAEAAAAQbW4AAAAAABBteADIAAAAAA==",
"subType": "06"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"age": {
"$gte": {
"$binary": {
"base64": "Dd0BAAADcGF5bG9hZACZAQAABGcAhQEAAAMwAH0AAAAFZAAgAAAAAInd0noBhIiJMv8QTjcfgRqnnVhxRJRRACLfvgT+CTR/BXMAIAAAAADm0EjqF/T4EmR6Dw6NaPLrL0OuzS4AFvm90czFluAAygVsACAAAAAA5MXcYWjYlzhPFUDebBEa17B5z2bupmaW9uCdtLjc7RkAAzEAfQAAAAVkACAAAAAA7lkNtT6RLw91aJ07K/blwlFs5wi9pQjqUXDcaCTxe98FcwAgAAAAAPwySffuLQihmF70Ot93KtaUMNU8KpmA+niyPRcvarNMBWwAIAAAAACDv6fJXXwRqwZH3O2kO+hdeLZ36U6bMZSui8kv0PsPtAADMgB9AAAABWQAIAAAAACcMWVTbZC4ox5VdjWeYKLgf4oBjpPlbTTAkucm9JPK0wVzACAAAAAA3tIww4ZTytkxFsUKyJbc3zwQ2w7DhkOqaNvX9g8pi3gFbAAgAAAAAGs9XR3Q1JpxV+HPW8P2GvCuCBF5bGZ8Kl1zHqzZcd5/AAASY20ABAAAAAAAAAAAEHBheWxvYWRJZAAAAAAAEGZpcnN0T3BlcmF0b3IAAgAAABBzZWNvbmRPcGVyYXRvcgAEAAAAAA==",
"base64": "DQECAAADcGF5bG9hZACZAQAABGcAhQEAAAMwAH0AAAAFZAAgAAAAAInd0noBhIiJMv8QTjcfgRqnnVhxRJRRACLfvgT+CTR/BXMAIAAAAADm0EjqF/T4EmR6Dw6NaPLrL0OuzS4AFvm90czFluAAygVsACAAAAAA5MXcYWjYlzhPFUDebBEa17B5z2bupmaW9uCdtLjc7RkAAzEAfQAAAAVkACAAAAAA7lkNtT6RLw91aJ07K/blwlFs5wi9pQjqUXDcaCTxe98FcwAgAAAAAPwySffuLQihmF70Ot93KtaUMNU8KpmA+niyPRcvarNMBWwAIAAAAACDv6fJXXwRqwZH3O2kO+hdeLZ36U6bMZSui8kv0PsPtAADMgB9AAAABWQAIAAAAACcMWVTbZC4ox5VdjWeYKLgf4oBjpPlbTTAkucm9JPK0wVzACAAAAAA3tIww4ZTytkxFsUKyJbc3zwQ2w7DhkOqaNvX9g8pi3gFbAAgAAAAAGs9XR3Q1JpxV+HPW8P2GvCuCBF5bGZ8Kl1zHqzZcd5/AAASY20ABAAAAAAAAAAAEHBheWxvYWRJZAAAAAAAEGZpcnN0T3BlcmF0b3IAAgAAABBzZWNvbmRPcGVyYXRvcgAEAAAAEnNwAAEAAAAAAAAAEHRmAAAAAAAQbW4AAAAAABBteADIAAAAAA==",
"subType": "06"
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/mc-fle2-encryption-placeholder-private.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ typedef struct {
bson_iter_t indexMax;
// precision determines the number of digits after the decimal point for
// floating point values.
mc_optional_uint32_t precision;
mc_optional_int32_t precision;
// trimFactor determines how many root levels of the hypergraph to trim.
mc_optional_uint32_t trimFactor;
mc_optional_int32_t trimFactor;
} mc_FLE2RangeFindSpecEdgesInfo_t;

/** FLE2RangeFindSpec represents the range find specification that is encoded
Expand Down Expand Up @@ -94,9 +94,9 @@ typedef struct {
bson_iter_t max;
// precision determines the number of digits after the decimal point for
// floating point values.
mc_optional_uint32_t precision;
mc_optional_int32_t precision;
// trimFactor determines how many root levels of the hypergraph to trim.
mc_optional_uint32_t trimFactor;
mc_optional_int32_t trimFactor;
} mc_FLE2RangeInsertSpec_t;

bool mc_FLE2RangeInsertSpec_parse(mc_FLE2RangeInsertSpec_t *out,
Expand Down
8 changes: 4 additions & 4 deletions src/mc-fle2-encryption-placeholder.c
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ static bool mc_FLE2RangeFindSpecEdgesInfo_parse(mc_FLE2RangeFindSpecEdgesInfo_t
goto fail;
}

out->precision = OPT_U32((uint32_t)val);
out->precision = OPT_I32(val);
}
END_IF_FIELD

Expand All @@ -294,7 +294,7 @@ static bool mc_FLE2RangeFindSpecEdgesInfo_parse(mc_FLE2RangeFindSpecEdgesInfo_t
goto fail;
}

out->trimFactor = OPT_U32((uint32_t)val);
out->trimFactor = OPT_I32(val);
}
END_IF_FIELD
}
Expand Down Expand Up @@ -454,7 +454,7 @@ bool mc_FLE2RangeInsertSpec_parse(mc_FLE2RangeInsertSpec_t *out,
CLIENT_ERR_PREFIXED("'precision' must be non-negative");
goto fail;
}
out->precision = OPT_U32((uint32_t)val);
out->precision = OPT_I32(val);
}
END_IF_FIELD

Expand All @@ -468,7 +468,7 @@ bool mc_FLE2RangeInsertSpec_parse(mc_FLE2RangeInsertSpec_t *out,
CLIENT_ERR_PREFIXED("'trimFactor' must be non-negative");
goto fail;
}
out->trimFactor = OPT_U32((uint32_t)val);
out->trimFactor = OPT_I32(val);
}
END_IF_FIELD
}
Expand Down
21 changes: 18 additions & 3 deletions src/mc-fle2-find-range-payload-private-v2.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

#include "mc-array-private.h"
#include "mc-fle2-range-operator-private.h"
#include "mc-optional-private.h"

/** FLE2FindRangePayloadEdgesInfoV2 represents the token information for a range
* find query. It is encoded inside an FLE2FindRangePayloadV2.
Expand All @@ -44,8 +45,17 @@ typedef struct {
* } FLE2FindRangePayloadV2;
*
* bson is a BSON document of this form:
* g: array<EdgeFindTokenSetV2> // Array of Edges
* cm: <int64> // Queryable Encryption max counter
* payload: <document>
* g: array<EdgeFindTokenSetV2> // Array of Edges
* cm: <int64> // Queryable Encryption max counter
* payloadId: <int32> // Payload ID.
* firstOperator: <int32>
* secondOperator: <int32>
* sp: optional<int64> // Sparsity.
* pn: optional<int32> // Precision.
* tf: optional<int32> // Trim Factor.
* mn: optional<any> // Index Min.
* mx: optional<any> // Index Max.
*/
typedef struct {
struct {
Expand All @@ -61,6 +71,11 @@ typedef struct {
// secondOperator represents the second query operator for which this payload
// was generated. Only populated for two-sided ranges. It is 0 if unset.
mc_FLE2RangeOperator_t secondOperator;
mc_optional_int64_t sparsity; // sp
mc_optional_int32_t precision; // pn
mc_optional_int32_t trimFactor; // tf
bson_value_t indexMin; // mn
bson_value_t indexMax; // mx
} mc_FLE2FindRangePayloadV2_t;

/**
Expand All @@ -81,7 +96,7 @@ typedef struct {

void mc_FLE2FindRangePayloadV2_init(mc_FLE2FindRangePayloadV2_t *payload);

bool mc_FLE2FindRangePayloadV2_serialize(const mc_FLE2FindRangePayloadV2_t *payload, bson_t *out);
bool mc_FLE2FindRangePayloadV2_serialize(const mc_FLE2FindRangePayloadV2_t *payload, bson_t *out, bool use_range_v2);

void mc_FLE2FindRangePayloadV2_cleanup(mc_FLE2FindRangePayloadV2_t *payload);

Expand Down
38 changes: 37 additions & 1 deletion src/mc-fle2-find-range-payload-v2.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ void mc_FLE2FindRangePayloadV2_cleanup(mc_FLE2FindRangePayloadV2_t *payload) {
return false; \
}

bool mc_FLE2FindRangePayloadV2_serialize(const mc_FLE2FindRangePayloadV2_t *payload, bson_t *out) {
bool mc_FLE2FindRangePayloadV2_serialize(const mc_FLE2FindRangePayloadV2_t *payload, bson_t *out, bool use_range_v2) {
BSON_ASSERT_PARAM(out);
BSON_ASSERT_PARAM(payload);

Expand Down Expand Up @@ -131,6 +131,42 @@ bool mc_FLE2FindRangePayloadV2_serialize(const mc_FLE2FindRangePayloadV2_t *payl
return false;
}

if (use_range_v2) {
// Encode parameters that were used to generate the mincover.
// The crypto parameters are all optionally set. Find payloads may come in pairs (a lower and upper bound).
// One of the pair includes the mincover. The other payload was not generated with crypto parameters.

if (payload->sparsity.set) {
if (!BSON_APPEND_INT64(out, "sp", payload->sparsity.value)) {
return false;
}
}

if (payload->precision.set) {
if (!BSON_APPEND_INT32(out, "pn", payload->precision.value)) {
return false;
}
}

if (payload->trimFactor.set) {
if (!BSON_APPEND_INT32(out, "tf", payload->trimFactor.value)) {
return false;
}
}

if (payload->indexMin.value_type != BSON_TYPE_EOD) {
if (!BSON_APPEND_VALUE(out, "mn", &payload->indexMin)) {
return false;
}
}

if (payload->indexMax.value_type != BSON_TYPE_EOD) {
if (!BSON_APPEND_VALUE(out, "mx", &payload->indexMax)) {
return false;
}
}
}

return true;
}

Expand Down
17 changes: 15 additions & 2 deletions src/mc-fle2-insert-update-payload-private-v2.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <bson/bson.h>

#include "mc-array-private.h"
#include "mc-optional-private.h"
#include "mongocrypt-buffer-private.h"
#include "mongocrypt-private.h"
#include "mongocrypt.h"
Expand All @@ -45,7 +46,12 @@
* e: <binary> // ServerDataEncryptionLevel1Token
* l: <binary> // ServerDerivedFromDataToken
* k: <int64> // Randomly sampled contention factor value
* g: array<EdgeTokenSetV2> // Array of Edges
* g: array<EdgeTokenSetV2> // Array of Edges. Only included for range payloads.
* sp: optional<int64> // Sparsity. Only included for range payloads.
* pn: optional<int32> // Precision. Only included for range payloads.
* tf: optional<int32> // Trim Factor. Only included for range payloads.
* mn: optional<any> // Index Min. Only included for range payloads.
* mx: optional<any> // Index Max. Only included for range payloads.
*
* p is the result of:
* Encrypt(
Expand All @@ -72,6 +78,11 @@ typedef struct {
_mongocrypt_buffer_t serverDerivedFromDataToken; // l
int64_t contentionFactor; // k
mc_array_t edgeTokenSetArray; // g
mc_optional_int64_t sparsity; // sp
mc_optional_int32_t precision; // pn
mc_optional_int32_t trimFactor; // tf
bson_value_t indexMin; // mn
bson_value_t indexMax; // mx
_mongocrypt_buffer_t plaintext;
_mongocrypt_buffer_t userKeyId;
} mc_FLE2InsertUpdatePayloadV2_t;
Expand Down Expand Up @@ -110,7 +121,9 @@ const _mongocrypt_buffer_t *mc_FLE2InsertUpdatePayloadV2_decrypt(_mongocrypt_cry

bool mc_FLE2InsertUpdatePayloadV2_serialize(const mc_FLE2InsertUpdatePayloadV2_t *payload, bson_t *out);

bool mc_FLE2InsertUpdatePayloadV2_serializeForRange(const mc_FLE2InsertUpdatePayloadV2_t *payload, bson_t *out);
bool mc_FLE2InsertUpdatePayloadV2_serializeForRange(const mc_FLE2InsertUpdatePayloadV2_t *payload,
bson_t *out,
bool use_range_v2);

void mc_FLE2InsertUpdatePayloadV2_cleanup(mc_FLE2InsertUpdatePayloadV2_t *payload);

Expand Down
90 changes: 89 additions & 1 deletion src/mc-fle2-insert-update-payload-v2.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include "mc-fle2-insert-update-payload-private-v2.h"
#include "mongocrypt-buffer-private.h"
#include "mongocrypt-util-private.h" // mc_bson_type_to_string
#include "mongocrypt.h"

void mc_FLE2InsertUpdatePayloadV2_init(mc_FLE2InsertUpdatePayloadV2_t *payload) {
Expand Down Expand Up @@ -53,6 +54,8 @@ void mc_FLE2InsertUpdatePayloadV2_cleanup(mc_FLE2InsertUpdatePayloadV2_t *payloa
mc_EdgeTokenSetV2_cleanup(&entry);
}
_mc_array_destroy(&payload->edgeTokenSetArray);
bson_value_destroy(&payload->indexMin);
bson_value_destroy(&payload->indexMax);
}

#define IF_FIELD(Name) \
Expand Down Expand Up @@ -104,6 +107,7 @@ bool mc_FLE2InsertUpdatePayloadV2_parse(mc_FLE2InsertUpdatePayloadV2_t *out,
bool has_d = false, has_s = false, has_p = false;
bool has_u = false, has_t = false, has_v = false;
bool has_e = false, has_l = false, has_k = false;
bool has_sp = false, has_pn = false, has_tf = false, has_mn = false, has_mx = false;
bson_t in_bson;

BSON_ASSERT_PARAM(out);
Expand Down Expand Up @@ -163,6 +167,57 @@ bool mc_FLE2InsertUpdatePayloadV2_parse(mc_FLE2InsertUpdatePayloadV2_t *out,
PARSE_BINARY(v, value)
PARSE_BINARY(e, serverEncryptionToken)
PARSE_BINARY(l, serverDerivedFromDataToken)

IF_FIELD(sp) {
if (!BSON_ITER_HOLDS_INT64(&iter)) {
CLIENT_ERR("Field 'sp' expected to hold an int64, got: %s",
mc_bson_type_to_string(bson_iter_type(&iter)));
goto fail;
}
int64_t sparsity = bson_iter_int64(&iter);
out->sparsity = OPT_I64(sparsity);
}
END_IF_FIELD

IF_FIELD(pn) {
if (!BSON_ITER_HOLDS_INT32(&iter)) {
CLIENT_ERR("Field 'pn' expected to hold an int32, got: %s",
mc_bson_type_to_string(bson_iter_type(&iter)));
goto fail;
}
int32_t precision = bson_iter_int32(&iter);
if (precision < 0) {
CLIENT_ERR("Field 'pn' must be non-negative, got: %" PRId32, precision);
goto fail;
}
out->precision = OPT_I32(precision);
}
END_IF_FIELD

IF_FIELD(tf) {
if (!BSON_ITER_HOLDS_INT32(&iter)) {
CLIENT_ERR("Field 'tf' expected to hold an int32, got: %s",
mc_bson_type_to_string(bson_iter_type(&iter)));
goto fail;
}
int32_t trimFactor = bson_iter_int32(&iter);
if (trimFactor < 0) {
CLIENT_ERR("Field 'tf' must be non-negative, got: %" PRId32, trimFactor);
goto fail;
}
out->trimFactor = OPT_I32(trimFactor);
}
END_IF_FIELD

IF_FIELD(mn) {
bson_value_copy(bson_iter_value(&iter), &out->indexMin);
}
END_IF_FIELD

IF_FIELD(mx) {
bson_value_copy(bson_iter_value(&iter), &out->indexMax);
}
END_IF_FIELD
}

CHECK_HAS(d);
Expand All @@ -174,6 +229,7 @@ bool mc_FLE2InsertUpdatePayloadV2_parse(mc_FLE2InsertUpdatePayloadV2_t *out,
CHECK_HAS(e);
CHECK_HAS(l);
CHECK_HAS(k);
// The fields `sp`, `pn`, `tf`, `mn`, and `mx` are only set for "range" payloads.

if (!_mongocrypt_buffer_from_subrange(&out->userKeyId, &out->value, 0, UUID_LEN)) {
CLIENT_ERR("failed to create userKeyId buffer");
Expand Down Expand Up @@ -213,7 +269,9 @@ bool mc_FLE2InsertUpdatePayloadV2_serialize(const mc_FLE2InsertUpdatePayloadV2_t
return true;
}

bool mc_FLE2InsertUpdatePayloadV2_serializeForRange(const mc_FLE2InsertUpdatePayloadV2_t *payload, bson_t *out) {
bool mc_FLE2InsertUpdatePayloadV2_serializeForRange(const mc_FLE2InsertUpdatePayloadV2_t *payload,
bson_t *out,
bool use_range_v2) {
BSON_ASSERT_PARAM(out);
BSON_ASSERT_PARAM(payload);

Expand Down Expand Up @@ -257,6 +315,36 @@ bool mc_FLE2InsertUpdatePayloadV2_serializeForRange(const mc_FLE2InsertUpdatePay
return false;
}

if (use_range_v2) {
// Encode parameters that were used to generate the payload.
BSON_ASSERT(payload->sparsity.set);
if (!BSON_APPEND_INT64(out, "sp", payload->sparsity.value)) {
return false;
}

// Precision may be unset.
if (payload->precision.set) {
if (!BSON_APPEND_INT32(out, "pn", payload->precision.value)) {
return false;
}
}

BSON_ASSERT(payload->trimFactor.set);
if (!BSON_APPEND_INT32(out, "tf", payload->trimFactor.value)) {
return false;
}

BSON_ASSERT(payload->indexMin.value_type != BSON_TYPE_EOD);
if (!BSON_APPEND_VALUE(out, "mn", &payload->indexMin)) {
return false;
}

BSON_ASSERT(payload->indexMax.value_type != BSON_TYPE_EOD);
if (!BSON_APPEND_VALUE(out, "mx", &payload->indexMax)) {
return false;
}
}

return true;
}

Expand Down
4 changes: 2 additions & 2 deletions src/mc-fle2-rfds-private.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ typedef struct {
bson_iter_t indexMax;
int64_t maxContentionFactor;
int64_t sparsity;
mc_optional_uint32_t precision;
mc_optional_uint32_t trimFactor;
mc_optional_int32_t precision;
mc_optional_int32_t trimFactor;
} mc_makeRangeFindPlaceholder_args_t;

// mc_makeRangeFindPlaceholder creates a placeholder to be consumed by
Expand Down
6 changes: 2 additions & 4 deletions src/mc-fle2-rfds.c
Original file line number Diff line number Diff line change
Expand Up @@ -364,12 +364,10 @@ bool mc_makeRangeFindPlaceholder(mc_makeRangeFindPlaceholder_args_t *args,
TRY(bson_append_iter(edgesInfo, "indexMin", -1, &args->indexMin));
TRY(bson_append_iter(edgesInfo, "indexMax", -1, &args->indexMax));
if (args->precision.set) {
BSON_ASSERT(args->precision.value <= INT32_MAX);
TRY(BSON_APPEND_INT32(edgesInfo, "precision", (int32_t)args->precision.value));
TRY(BSON_APPEND_INT32(edgesInfo, "precision", args->precision.value));
}
if (args->trimFactor.set) {
BSON_ASSERT(args->trimFactor.value <= INT32_MAX);
TRY(BSON_APPEND_INT32(edgesInfo, "trimFactor", (int32_t)args->trimFactor.value));
TRY(BSON_APPEND_INT32(edgesInfo, "trimFactor", args->trimFactor.value));
}
TRY(BSON_APPEND_DOCUMENT(v, "edgesInfo", edgesInfo));
}
Expand Down
Loading

0 comments on commit bb12cda

Please sign in to comment.