Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -739,5 +739,90 @@ namespace AdaptiveCardsSharedModelUnitTest
const auto serializedCard = card->SerializeToJsonValue();
Assert::IsTrue(serializedCard["body"][0]["isMultiline"].asBool());
}

// Helper: build a card JSON with N TextBlocks in the body
static std::string MakeCardWithNElements(unsigned int count)
{
std::string json = R"({"type":"AdaptiveCard","version":"1.5","body":[)";
for (unsigned int i = 0; i < count; i++)
{
if (i > 0) json += ",";
json += R"({"type":"TextBlock","text":"Item )" + std::to_string(i) + R"("})";
}
json += "]}";
return json;
}

// Helper: build a card JSON with N OpenUrl actions
static std::string MakeCardWithNActions(unsigned int count)
{
std::string json = R"({"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"hi"}],"actions":[)";
for (unsigned int i = 0; i < count; i++)
{
if (i > 0) json += ",";
json += R"({"type":"Action.OpenUrl","title":"L)" + std::to_string(i) + R"(","url":"https://x.com"})";
}
json += "]}";
return json;
}

TEST_METHOD(ElementCollection_WithinLimit_ParsesFully)
{
const auto json = MakeCardWithNElements(10);
const auto result = AdaptiveCard::DeserializeFromString(json, "1.5");
Assert::AreEqual(10ui64, result->GetAdaptiveCard()->GetBody().size());
}

TEST_METHOD(ElementCollection_AtLimit_ParsesFully)
{
const unsigned int limit = ParseContext::c_maxElementsPerCollection;
const auto json = MakeCardWithNElements(limit);
const auto result = AdaptiveCard::DeserializeFromString(json, "1.5");
Assert::AreEqual(static_cast<size_t>(limit), result->GetAdaptiveCard()->GetBody().size());
}

TEST_METHOD(ElementCollection_ExceedsLimit_CappedWithWarning)
{
const unsigned int limit = ParseContext::c_maxElementsPerCollection;
const auto json = MakeCardWithNElements(limit + 50);
const auto result = AdaptiveCard::DeserializeFromString(json, "1.5");

// Body should be capped at the limit
Assert::AreEqual(static_cast<size_t>(limit), result->GetAdaptiveCard()->GetBody().size());

// Should have a warning about it
bool foundWarning = false;
for (const auto& w : result->GetWarnings())
{
if (w->GetReason().find("Maximum number of elements") != std::string::npos)
{
foundWarning = true;
break;
}
}
Assert::IsTrue(foundWarning, L"Expected warning about element collection limit");
}

TEST_METHOD(ActionCollection_ExceedsLimit_CappedWithWarning)
{
const unsigned int limit = ParseContext::c_maxElementsPerCollection;
const auto json = MakeCardWithNActions(limit + 50);
const auto result = AdaptiveCard::DeserializeFromString(json, "1.5");

// Actions should be capped
Assert::IsTrue(result->GetAdaptiveCard()->GetActions().size() <= static_cast<size_t>(limit));

// Should have a warning
bool foundWarning = false;
for (const auto& w : result->GetWarnings())
{
if (w->GetReason().find("Maximum number of actions") != std::string::npos)
{
foundWarning = true;
break;
}
}
Assert::IsTrue(foundWarning, L"Expected warning about action collection limit");
}
};
}
3 changes: 3 additions & 0 deletions source/shared/cpp/ObjectModel/ParseContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class ParseContext
void RemoveProhibitedElementType(const std::vector<std::string>& list);
void ShouldParse(const std::string& type);

// Max items allowed in a single collection (body, actions, columns, etc.)
static constexpr unsigned int c_maxElementsPerCollection = 200;

private:
const AdaptiveCards::InternalId GetNearestFallbackId(const AdaptiveCards::InternalId& skipId) const;
// This enum is just a helper to keep track of the position of contents within the std::tuple used in
Expand Down
12 changes: 11 additions & 1 deletion source/shared/cpp/ObjectModel/ParseUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,20 @@ std::vector<std::shared_ptr<BaseActionElement>> ParseUtil::GetActionCollection(
return elements;
}

elements.reserve(elementArray.size());
const size_t maxElements = static_cast<size_t>(ParseContext::c_maxElementsPerCollection);
elements.reserve(std::min(static_cast<size_t>(elementArray.size()), maxElements));

for (const auto& curJsonValue : elementArray)
{
// Cap the number of actions to prevent resource exhaustion
if (elements.size() >= maxElements)
{
context.warnings.emplace_back(
std::make_shared<AdaptiveCardParseWarning>(WarningStatusCode::CustomWarning,
"Maximum number of actions in a collection exceeded; remaining items were dropped"));
break;
}

auto action = ParseUtil::GetActionFromJsonValue(context, curJsonValue);
if (action != nullptr)
{
Expand Down
23 changes: 21 additions & 2 deletions source/shared/cpp/ObjectModel/ParseUtil.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,21 @@ std::vector<std::shared_ptr<T>> ParseUtil::GetElementCollectionOfSingleType(
return elements;
}

elements.reserve(elementArray.size());
const size_t maxElements = static_cast<size_t>(ParseContext::c_maxElementsPerCollection);
elements.reserve(std::min(static_cast<size_t>(elementArray.size()), maxElements));

// Deserialize every element in the array
for (const Json::Value& curJsonValue : elementArray)
{
// Cap the number of elements to prevent resource exhaustion
if (elements.size() >= maxElements)
{
context.warnings.emplace_back(
std::make_shared<AdaptiveCardParseWarning>(WarningStatusCode::CustomWarning,
"Maximum number of elements in a collection exceeded; remaining items were dropped"));
break;
}

// Parse the element
auto el = deserializer(context, curJsonValue);
if (el != nullptr)
Expand Down Expand Up @@ -280,13 +290,22 @@ std::vector<std::shared_ptr<T>> ParseUtil::GetElementCollection(
}

const size_t elemSize = elementArray.size();
elements.reserve(elemSize);
const size_t maxElements = static_cast<size_t>(ParseContext::c_maxElementsPerCollection);
elements.reserve(std::min(elemSize, maxElements));

const ContainerBleedDirection previousBleedState = context.GetBleedDirection();

size_t currentIndex = 0;
for (auto& curJsonValue : elementArray)
{
// Cap the number of elements to prevent resource exhaustion
if (elements.size() >= maxElements)
{
context.warnings.emplace_back(
std::make_shared<AdaptiveCardParseWarning>(WarningStatusCode::CustomWarning,
"Maximum number of elements in a collection exceeded; remaining items were dropped"));
break;
}
ContainerBleedDirection currentBleedState = previousBleedState;

if (currentIndex != 0)
Expand Down