blob: 398122e00518c4d92b560c46235ac01c31b6ecc3 [file]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "formats/bmff/extractor.h"
#include <cstdint>
#include <limits>
#include <optional>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "constants/labels.h"
#include "formats/asset_box.h"
#include "formats/asset_byte_info.h"
#include "formats/bmff/test_utils.h"
#include "formats/byte_range.h"
#include "formats/extractor_result.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/base/types.h"
#include "riegeli/bytes/string_reader.h"
#include "testing/jumbf_utils.h"
#include "testing/test_string_utils.h"
namespace credentio {
namespace {
using ::absl_testing::IsOkAndHolds;
using ::absl_testing::StatusIs;
using ::credentio_testing::BadBox;
using ::credentio_testing::BigBox;
using ::credentio_testing::Box;
using ::credentio_testing::BoxFlags;
using ::credentio_testing::BoxVersion;
using ::credentio_testing::C2paBoxPayload;
using ::credentio_testing::C2paUuidBox;
using ::credentio_testing::ManifestBigBox;
using ::credentio_testing::ManifestBox;
using ::credentio_testing::Uint32Str;
using ::credentio_testing::Uint64Str;
using ::credentio_testing::UuidBoxPayload;
using ::testing::Eq;
using ::testing::HasSubstr;
using ::testing::Optional;
constexpr int64_t kMaxChunkBytes = 10 * 1024 * 1024; // 2 MiB
struct TestCase {
std::string name;
std::string contents;
absl::StatusOr<ExtractorResult> result;
};
class ExtractorTest : public testing::TestWithParam<TestCase> {};
INSTANTIATE_TEST_SUITE_P(
ExtractorTests, ExtractorTest,
testing::ValuesIn({
TestCase{
.name = "EmptyContent",
.contents = "",
.result = absl::NotFoundError("No manifest store found"),
},
TestCase{
.name = "C2paBox",
.contents = absl::StrCat(Box("blah", "irrelevant content"),
ManifestBox(/*merkle_offset=*/0, "foo")),
.result = ExtractorResult{.manifest_store = "foo",
.asset_byte_info =
{.manifest_store_location =
ByteRange{.offset = 26,
.length = 48}}},
},
TestCase{
.name = "BigC2paBox",
.contents = absl::StrCat(Box("blah", "irrelevant content"),
ManifestBigBox(/*merkle_offset=*/0,
"foo")),
.result = ExtractorResult{.manifest_store = "foo",
.asset_byte_info =
{.manifest_store_location =
ByteRange{.offset = 26,
.length = 56}}},
},
TestCase{
.name = "C2paBoxTooLarge",
.contents = absl::StrCat(Box("blah", "irrelevant content"),
ManifestBox(/*merkle_offset=*/0,
std::string(kMaxChunkBytes + 1,
'Z'))),
.result = absl::NotFoundError("C2PA box is too large"),
},
TestCase{
.name = "NoC2paBox",
.contents = absl::StrCat(Box("blah", "irrelevant content"),
Box("asdf", "more irrelevant content"),
BigBox("zxcv", "whatever")),
.result = absl::NotFoundError("No manifest store found"),
},
// --- Manifest box placement. ---
TestCase{
.name = "ManifestBetweenFtypAndMoov",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo"),
Box("moov", "movie metadata"), Box("mdat", "media data")),
.result = ExtractorResult{.manifest_store = "foo",
.asset_byte_info =
{.manifest_store_location =
ByteRange{.offset = 16,
.length = 48}}},
},
TestCase{
.name = "ManifestBetweenFtypAndMdat",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo"),
Box("mdat", "media data"), Box("moov", "movie metadata")),
.result =
ExtractorResult{
.manifest_store = "foo",
.asset_byte_info = {.manifest_store_location =
ByteRange{
.offset = 16, .length = 48}}},
},
TestCase{
.name = "ManifestAfterFreeBeforeMoov",
.contents = absl::StrCat(Box("ftyp", "isomisom"),
Box("free", "padding"), C2paUuidBox("foo"),
Box("moov", "movie metadata"),
Box("mdat", "media data")),
.result =
ExtractorResult{
.manifest_store = "foo",
.asset_byte_info = {.manifest_store_location =
ByteRange{
.offset = 31, .length = 48}}},
},
TestCase{
.name = "ManifestWithoutFtyp",
.contents = absl::StrCat(C2paUuidBox("foo"),
Box("moov", "movie metadata"),
Box("mdat", "media data")),
.result =
ExtractorResult{.manifest_store = "foo",
.asset_byte_info =
{.manifest_store_location =
ByteRange{.offset = 0, .length = 48}}},
},
TestCase{
.name = "MerkleBoxAfterMdatIgnored",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo"),
Box("moov", "movie metadata"), Box("mdat", "media data"),
Box("uuid", C2paBoxPayload("merkle", "merkle data"))),
.result =
ExtractorResult{
.manifest_store = "foo",
.asset_byte_info = {.manifest_store_location =
ByteRange{
.offset = 16, .length = 48}}},
},
TestCase{
.name = "UnknownC2paPurposeAfterMdatIgnored",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo"),
Box("mdat", "media data"), C2paUuidBox("bar", "unknown")),
.result =
ExtractorResult{
.manifest_store = "foo",
.asset_byte_info = {.manifest_store_location =
ByteRange{
.offset = 16, .length = 48}}},
},
TestCase{
.name = "NonC2paUuidBoxAfterMdatIgnored",
.contents = absl::StrCat(Box("ftyp", "isomisom"),
C2paUuidBox("foo"),
Box("mdat", "media data"),
Box("uuid", "abcdefghijklmnop data")),
.result =
ExtractorResult{
.manifest_store = "foo",
.asset_byte_info = {.manifest_store_location =
ByteRange{
.offset = 16, .length = 48}}},
},
TestCase{
.name = "ManifestBeforeFtyp",
.contents = absl::StrCat(
C2paUuidBox("foo"), Box("ftyp", "isomisom"),
Box("moov", "movie metadata"), Box("mdat", "media data")),
.result = absl::NotFoundError("manifest resides before ftyp"),
},
TestCase{
.name = "ManifestAfterMoov",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), Box("moov", "movie metadata"),
C2paUuidBox("foo"), Box("mdat", "media data")),
.result = absl::NotFoundError("manifest resides after moov"),
},
TestCase{
.name = "ManifestAfterMdat",
.contents = absl::StrCat(Box("ftyp", "isomisom"),
Box("mdat", "media data"),
C2paUuidBox("foo")),
.result = absl::NotFoundError("manifest resides after mdat"),
},
TestCase{
.name = "ManifestBetweenMdatAndMoov",
.contents =
absl::StrCat(Box("ftyp", "isomisom"), Box("mdat", "media data"),
C2paUuidBox("foo"), Box("moov", "movie metadata")),
.result = absl::NotFoundError("manifest resides after mdat"),
},
TestCase{
.name = "ManifestAfterMoovWithoutFtyp",
.contents = absl::StrCat(Box("moov", "movie metadata"),
C2paUuidBox("foo")),
.result = absl::NotFoundError("manifest resides after moov"),
},
// --- Whole-file scanning. ---
TestCase{
.name = "SecondManifestAfterMdat",
.contents =
absl::StrCat(Box("ftyp", "isomisom"), C2paUuidBox("foo"),
Box("mdat", "media data"), C2paUuidBox("bar")),
.result = absl::NotFoundError("multiple manifest boxes found"),
},
TestCase{
.name = "TruncatedBoxAfterMdat",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo"),
Box("mdat", "media data"), BadBox("free", "padding", 16)),
.result = absl::NotFoundError("truncated BMFF box"),
},
TestCase{
.name = "MalformedC2paBoxAfterMoov",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo"),
Box("moov", "movie metadata"),
Box("uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(1),
BoxFlags(0), ""))),
.result = absl::NotFoundError("unsupported C2PA box version"),
},
// --- Manifest payload bounds. ---
TestCase{
.name = "EmptyManifestPayload",
.contents = absl::StrCat(Box("ftyp", "isomisom"), C2paUuidBox(""),
Box("mdat", "media data")),
.result = absl::NotFoundError("manifest store is empty"),
},
// Regression: a manifest box too small for its merkle offset followed
// by another box used to underflow the payload size.
TestCase{
.name = "ManifestTooSmallForMerkleOffsetFollowedByBox",
.contents = absl::StrCat(Box("uuid",
C2paBoxPayload("manifest", "abc")),
Box("mdat", "media data")),
.result = absl::NotFoundError("kUnexpectedEof"),
},
// --- Manifest vs. original/update coexistence. ---
TestCase{
.name = "ManifestAndOriginalCoexist",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo"),
C2paUuidBox("bar", "original"), Box("mdat", "media data")),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
TestCase{
.name = "ManifestAndUpdateCoexist",
.contents =
absl::StrCat(Box("ftyp", "isomisom"), C2paUuidBox("foo"),
Box("mdat", "media data"),
Box("uuid", C2paBoxPayload("update", "bar"))),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (update)"),
},
TestCase{
.name = "ManifestOriginalAndUpdateCoexist",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo", "original"),
C2paUuidBox("bar"), Box("mdat", "media data"),
Box("uuid", C2paBoxPayload("update", "baz"))),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
// --- Original + update boxes. ---
TestCase{
.name = "OriginalAndUpdateUnsupported",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("orig", "original"),
Box("moov", "movie metadata"), Box("mdat", "media data"),
Box("uuid", C2paBoxPayload("update", "upd"))),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
TestCase{
.name = "OriginalDirectlyFollowedByUpdateUnsupported",
.contents = absl::StrCat(C2paUuidBox("orig", "original"),
Box("uuid",
C2paBoxPayload("update", "upd"))),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
TestCase{
.name = "OriginalAndBigBoxUpdateUnsupported",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("orig", "original"),
Box("mdat", "media data"),
BigBox("uuid", C2paBoxPayload("update", "upd"))),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
TestCase{
.name = "OriginalWithoutUpdate",
.contents = absl::StrCat(Box("ftyp", "isomisom"),
C2paUuidBox("orig", "original"),
Box("mdat", "media data")),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
TestCase{
.name = "UpdateWithoutOriginal",
.contents =
absl::StrCat(Box("ftyp", "isomisom"), Box("mdat", "media data"),
Box("uuid", C2paBoxPayload("update", "upd"))),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (update)"),
},
TestCase{
.name = "UpdateNotLastBox",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("orig", "original"),
Box("mdat", "media data"),
Box("uuid", C2paBoxPayload("update", "upd")),
Box("free", "padding")),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
TestCase{
.name = "OriginalBeforeFtyp",
.contents =
absl::StrCat(C2paUuidBox("orig", "original"),
Box("ftyp", "isomisom"), Box("mdat", "media data"),
Box("uuid", C2paBoxPayload("update", "upd"))),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
TestCase{
.name = "OriginalAfterMoov",
.contents = absl::StrCat(
Box("ftyp", "isomisom"), Box("moov", "movie metadata"),
Box("free", "padding"), C2paUuidBox("orig", "original"),
Box("uuid", C2paBoxPayload("update", "upd"))),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
TestCase{
.name = "OriginalAfterMdat",
.contents =
absl::StrCat(Box("ftyp", "isomisom"), Box("mdat", "media data"),
C2paUuidBox("orig", "original"),
Box("uuid", C2paBoxPayload("update", "upd"))),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
TestCase{
.name = "MultipleOriginalBoxes",
.contents = absl::StrCat(
C2paUuidBox("a", "original"), C2paUuidBox("b", "original"),
Box("uuid", C2paBoxPayload("update", "upd"))),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
TestCase{
.name = "MultipleUpdateBoxes",
.contents = absl::StrCat(C2paUuidBox("orig", "original"),
Box("uuid", C2paBoxPayload("update", "a")),
Box("uuid",
C2paBoxPayload("update", "b"))),
.result = absl::NotFoundError(
"unsupported C2PA metadata purpose (original)"),
},
TestCase{
// Quoting
// https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_embedded_2:
// "If there are multiple C2PA Manifest Stores present in an asset,
// they shall all be considered as invalid and the validation should
// treat this as if no manifests were located."
.name = "MultipleC2paBoxesIgnored",
.contents = absl::StrCat(ManifestBox(/*merkle_offset=*/0, "foo"),
ManifestBox(/*merkle_offset=*/0, "bar")),
.result = absl::NotFoundError("multiple manifest boxes found"),
},
TestCase{
.name = "NonC2paUuidBoxIgnored",
.contents = absl::StrCat(Box("uuid",
"abcdefghijklmnop not a C2PA box"),
ManifestBox(/*merkle_offset=*/0, "foo")),
.result =
ExtractorResult{
.manifest_store = "foo",
.asset_byte_info = {.manifest_store_location =
ByteRange{
.offset = 39, .length = 48}}},
},
TestCase{
.name = "TruncatedBoxHeader",
.contents = "abcdefg",
.result = absl::NotFoundError("No manifest store found"),
},
TestCase{
.name = "TruncatedBoxPayload",
.contents =
[]() {
std::string contents = Box("asdf", "payload");
contents.resize(contents.size() - 1);
return contents;
}(),
.result = absl::NotFoundError("truncated BMFF box"),
},
TestCase{
.name = "TruncatedBoxPayloadHugeSize",
.contents = absl::StrCat(
Uint32Str(1), "asdf",
Uint64Str(std::numeric_limits<int64_t>::max()), "payload"),
.result = absl::NotFoundError("truncated BMFF box"),
},
TestCase{
.name = "ShortUuidBox",
.contents = Box("uuid", "foo"),
.result = absl::NotFoundError("kUnexpectedEof"),
},
TestCase{
.name = "InvalidBoxSize",
.contents = absl::StrCat(Uint32Str(2), "asdf", "payload"),
.result = absl::NotFoundError("kInvalidData"),
},
TestCase{
.name = "InvalidBigBoxSize",
.contents = absl::StrCat(Uint32Str(1), "asdf", Uint64Str(15),
"payload"),
.result = absl::NotFoundError("No manifest store found"),
},
TestCase{
.name = "InvalidVersion",
.contents = Box("uuid",
UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(1),
BoxFlags(0), "")),
.result = absl::NotFoundError("unsupported C2PA box version"),
},
TestCase{
.name = "InvalidFlags",
.contents = Box("uuid",
UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0),
BoxFlags(1), "")),
.result = absl::NotFoundError("unsupported C2PA box flags"),
},
TestCase{
.name = "NonManifestC2paBoxIgnored",
.contents = absl::StrCat(
Box("uuid", C2paBoxPayload("merkle", "ignored data")),
ManifestBox(/*merkle_offset=*/0, "foo")),
.result =
ExtractorResult{
.manifest_store = "foo",
.asset_byte_info = {.manifest_store_location =
ByteRange{
.offset = 47, .length = 48}}},
},
TestCase{
// Not enough bytes for the merkle offset.
.name = "TruncatedManifestBox",
.contents = Box("uuid", C2paBoxPayload("manifest", "asdfjkl")),
.result = absl::NotFoundError("kUnexpectedEof"),
},
}),
[](const testing::TestParamInfo<ExtractorTest::ParamType>& info) {
return info.param.name;
});
TEST_P(ExtractorTest, ExtractManifestStore) {
riegeli::StringReader<> input(GetParam().contents);
absl::StatusOr<std::string> result =
BmffExtractor().ExtractManifestStore(input);
absl::StatusOr<ExtractorResult> expected_result = GetParam().result;
if (expected_result.ok()) {
ABSL_ASSERT_OK(result);
EXPECT_THAT(*result, Eq(expected_result->manifest_store));
} else {
EXPECT_THAT(result,
StatusIs(expected_result.status().code(),
HasSubstr(expected_result.status().message())));
}
}
TEST_P(ExtractorTest, ExtractWorksWhenPrefixPadded) {
std::string contents = "padding" + GetParam().contents;
riegeli::StringReader<> input(contents);
ASSERT_TRUE(input.Seek(7));
absl::StatusOr<std::string> result =
BmffExtractor().ExtractManifestStore(input);
absl::StatusOr<ExtractorResult> expected_result = GetParam().result;
if (expected_result.ok()) {
ABSL_ASSERT_OK(result);
EXPECT_THAT(*result, Eq(expected_result->manifest_store));
} else {
EXPECT_THAT(result,
StatusIs(expected_result.status().code(),
HasSubstr(expected_result.status().message())));
}
}
TEST_P(ExtractorTest, ExtractManifestStoreLocation) {
riegeli::StringReader<> input(GetParam().contents);
absl::StatusOr<std::optional<ByteRange>> result =
BmffExtractor().ExtractManifestStoreLocation(input,
/*options=*/{});
absl::StatusOr<ExtractorResult> expected_result = GetParam().result;
if (expected_result.ok()) {
ABSL_ASSERT_OK(result);
EXPECT_THAT(*result,
Eq(expected_result->asset_byte_info.manifest_store_location));
} else {
EXPECT_THAT(result,
StatusIs(expected_result.status().code(),
HasSubstr(expected_result.status().message())));
}
}
TEST_P(ExtractorTest, ExtractBoxes) {
riegeli::StringReader<> input(GetParam().contents);
absl::StatusOr<std::vector<AssetBox>> result =
BmffExtractor().ExtractBoxes(input,
/*options=*/{});
// BMFF does not have a concept of boxes, calling this method should always
// fail.
EXPECT_THAT(result, StatusIs(absl::StatusCode::kUnimplemented,
"BMFF does not have a concept of boxes"));
}
TEST(BmffExtractorTest, IsManifestStore) {
EXPECT_TRUE(BmffExtractor().MightBeC2paManifestStore(
CreateStartOfManifestStorePayload()));
}
TEST(BmffExtractorTest, EndOffsetStopsScanningBeforeTrailingData) {
const std::string media = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo"), Box("mdat", "media data"));
// Data past `end_offset` would otherwise fail extraction.
const std::string contents =
absl::StrCat(media, BadBox("free", "padding", 16), C2paUuidBox("bar"));
riegeli::StringReader<> input(contents);
EXPECT_THAT(
BmffExtractor().ExtractManifestStoreLocation(
input, {.end_offset = static_cast<int64_t>(media.size())}),
IsOkAndHolds(Optional(Eq(ByteRange{.offset = 16, .length = 48}))));
}
TEST(BmffExtractorTest, EndOffsetWithoutLimitScansWholeFile) {
const std::string contents =
absl::StrCat(Box("ftyp", "isomisom"), C2paUuidBox("foo"),
Box("mdat", "media data"), BadBox("free", "padding", 16));
riegeli::StringReader<> input(contents);
EXPECT_THAT(
BmffExtractor().ExtractManifestStoreLocation(input, {}),
StatusIs(absl::StatusCode::kNotFound, HasSubstr("truncated BMFF box")));
}
TEST(BmffExtractorTest, EndOffsetInsideBoxFails) {
const std::string contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo"), Box("mdat", "media data"));
riegeli::StringReader<> input(contents);
EXPECT_THAT(
BmffExtractor().ExtractManifestStoreLocation(
input, {.end_offset = static_cast<int64_t>(contents.size() - 1)}),
StatusIs(absl::StatusCode::kNotFound, HasSubstr("truncated BMFF box")));
}
TEST(BmffExtractorTest, UpdateBoxEndingAtEndOffsetIsLast) {
const std::string media =
absl::StrCat(C2paUuidBox("orig", "original"), Box("mdat", "media data"),
Box("uuid", C2paBoxPayload("update", "upd")));
const std::string contents = absl::StrCat(media, Box("free", "padding"));
riegeli::StringReader<> input(contents);
EXPECT_THAT(BmffExtractor().ExtractManifestStoreLocation(
input, {.end_offset = static_cast<int64_t>(media.size())}),
StatusIs(absl::StatusCode::kNotFound,
HasSubstr("No manifest store found")));
}
TEST(BmffExtractorTest, LocationReturnsNulloptWhenC2paNotRequired) {
const std::string contents = absl::StrCat(
Box("ftyp", "isomisom"), Box("mdat", "media data"), C2paUuidBox("foo"));
riegeli::StringReader<> input(contents);
EXPECT_THAT(BmffExtractor().ExtractManifestStoreLocation(
input, {.requires_c2pa = false}),
IsOkAndHolds(Eq(std::nullopt)));
}
class NoSizeStringReader : public riegeli::StringReader<> {
public:
using riegeli::StringReader<>::StringReader;
bool SupportsSize() override { return false; }
};
class SizeNotValuedStringReader : public riegeli::StringReader<> {
public:
using riegeli::StringReader<>::StringReader;
bool SupportsSize() override { return true; }
protected:
std::optional<riegeli::Position> SizeImpl() override { return std::nullopt; }
};
TEST(BmffExtractorTest, ReaderWithoutSizeFails) {
const std::string contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo"), Box("mdat", "media data"));
NoSizeStringReader input(contents);
EXPECT_THAT(BmffExtractor().ExtractManifestStore(input),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("reader size cannot be determined")));
}
TEST(BmffExtractorTest, ReaderWithUnknownSizeFails) {
const std::string contents = absl::StrCat(
Box("ftyp", "isomisom"), C2paUuidBox("foo"), Box("mdat", "media data"));
SizeNotValuedStringReader input(contents);
EXPECT_THAT(BmffExtractor().ExtractManifestStore(input),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("reader size cannot be determined")));
}
} // namespace
} // namespace credentio