Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check start tags were matched to an end tag #923

Merged
merged 3 commits into from
Dec 2, 2024
Merged
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
79 changes: 61 additions & 18 deletions include/crow/mustache.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,37 @@ namespace crow // NOTE: Already documented in "crow/app.h"
*/
struct Action
{
bool has_end_match;
char tag_char;
int start;
int end;
int pos;
ActionType t;
Action(ActionType t_, size_t start_, size_t end_, size_t pos_ = 0):
start(static_cast<int>(start_)), end(static_cast<int>(end_)), pos(static_cast<int>(pos_)), t(t_)

Action(char tag_char_, ActionType t_, size_t start_, size_t end_, size_t pos_ = 0):
has_end_match(false), tag_char(tag_char_), start(static_cast<int>(start_)), end(static_cast<int>(end_)), pos(static_cast<int>(pos_)), t(t_)
{
}

bool missing_end_pair() const {
switch (t)
{
case ActionType::Ignore:
case ActionType::Tag:
case ActionType::UnescapeTag:
case ActionType::CloseBlock:
case ActionType::Partial:
return false;

// requires a match
case ActionType::OpenBlock:
case ActionType::ElseBlock:
return !has_end_match;

default:
throw std::logic_error("invalid type");
}
}
};

/**
Expand Down Expand Up @@ -483,7 +506,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
if (idx == body_.npos)
{
fragments_.emplace_back(static_cast<int>(current), static_cast<int>(body_.size()));
actions_.emplace_back(ActionType::Ignore, 0, 0);
actions_.emplace_back('!', ActionType::Ignore, 0, 0);
break;
}
fragments_.emplace_back(static_cast<int>(current), static_cast<int>(idx));
Expand All @@ -500,7 +523,8 @@ namespace crow // NOTE: Already documented in "crow/app.h"
throw invalid_template_exception("not matched opening tag");
}
current = endIdx + tag_close.size();
switch (body_[idx])
char tag_char = body_[idx];
switch (tag_char)
{
case '#':
idx++;
Expand All @@ -509,7 +533,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
while (body_[endIdx - 1] == ' ')
endIdx--;
blockPositions.emplace_back(static_cast<int>(actions_.size()));
actions_.emplace_back(ActionType::OpenBlock, idx, endIdx);
actions_.emplace_back(tag_char, ActionType::OpenBlock, idx, endIdx);
break;
case '/':
idx++;
Expand All @@ -522,13 +546,18 @@ namespace crow // NOTE: Already documented in "crow/app.h"
if (body_.compare(idx, endIdx - idx,
body_, matched.start, matched.end - matched.start) != 0)
{
throw invalid_template_exception("not matched {{# {{/ pair: " +
body_.substr(matched.start, matched.end - matched.start) + ", " +
body_.substr(idx, endIdx - idx));
throw invalid_template_exception(
std::string("not matched {{")
+ matched.tag_char
+ "{{/ pair: "
+ body_.substr(matched.start, matched.end - matched.start) + ", "
+ body_.substr(idx, endIdx - idx)
);
}
matched.pos = actions_.size();
matched.pos = static_cast<int>(actions_.size());
matched.has_end_match = true;
}
actions_.emplace_back(ActionType::CloseBlock, idx, endIdx, blockPositions.back());
actions_.emplace_back(tag_char, ActionType::CloseBlock, idx, endIdx, blockPositions.back());
blockPositions.pop_back();
break;
case '^':
Expand All @@ -538,19 +567,19 @@ namespace crow // NOTE: Already documented in "crow/app.h"
while (body_[endIdx - 1] == ' ')
endIdx--;
blockPositions.emplace_back(static_cast<int>(actions_.size()));
actions_.emplace_back(ActionType::ElseBlock, idx, endIdx);
actions_.emplace_back(tag_char, ActionType::ElseBlock, idx, endIdx);
break;
case '!':
// do nothing action
actions_.emplace_back(ActionType::Ignore, idx + 1, endIdx);
actions_.emplace_back(tag_char, ActionType::Ignore, idx + 1, endIdx);
break;
case '>': // partial
idx++;
while (body_[idx] == ' ')
idx++;
while (body_[endIdx - 1] == ' ')
endIdx--;
actions_.emplace_back(ActionType::Partial, idx, endIdx);
actions_.emplace_back(tag_char, ActionType::Partial, idx, endIdx);
break;
case '{':
if (tag_open != "{{" || tag_close != "}}")
Expand All @@ -565,7 +594,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
idx++;
while (body_[endIdx - 1] == ' ')
endIdx--;
actions_.emplace_back(ActionType::UnescapeTag, idx, endIdx);
actions_.emplace_back(tag_char, ActionType::UnescapeTag, idx, endIdx);
current++;
break;
case '&':
Expand All @@ -574,12 +603,12 @@ namespace crow // NOTE: Already documented in "crow/app.h"
idx++;
while (body_[endIdx - 1] == ' ')
endIdx--;
actions_.emplace_back(ActionType::UnescapeTag, idx, endIdx);
actions_.emplace_back(tag_char, ActionType::UnescapeTag, idx, endIdx);
break;
case '=':
// tag itself is no-op
idx++;
actions_.emplace_back(ActionType::Ignore, idx, endIdx);
actions_.emplace_back(tag_char, ActionType::Ignore, idx, endIdx);
endIdx--;
if (body_[endIdx] != '=')
throw invalid_template_exception("{{=: not matching = tag: " + body_.substr(idx, endIdx - idx));
Expand Down Expand Up @@ -620,13 +649,27 @@ namespace crow // NOTE: Already documented in "crow/app.h"
idx++;
while (body_[endIdx - 1] == ' ')
endIdx--;
actions_.emplace_back(ActionType::Tag, idx, endIdx);
actions_.emplace_back(tag_char, ActionType::Tag, idx, endIdx);
break;
}
}

// ensure no unmatched tags
for (int i = 0; i < static_cast<int>(actions_.size()); i++)
{
if (actions_[i].missing_end_pair())
{
throw invalid_template_exception(
std::string("open tag has no matching end tag {{")
+ actions_[i].tag_char
+ " {{/ pair: "
+ body_.substr(actions_[i].start, actions_[i].end - actions_[i].start)
);
}
}

// removing standalones
for (int i = actions_.size() - 2; i >= 0; i--)
for (int i = static_cast<int>(actions_.size()) - 2; i >= 0; i--)
{
if (actions_[i].t == ActionType::Tag || actions_[i].t == ActionType::UnescapeTag)
continue;
Expand Down
15 changes: 15 additions & 0 deletions tests/template/crow_extra_mustache_tests.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"overview": "Check Crow's behaviour when given invalid mustache templates",
"tests": [
{
"name": "Missing end-tags",
"desc": "Missing end-tags should fail to render ... and not enter infinite loops or other undefined behaviour",
"data": {
"boolean": true
},
"template": "\"{{#boolean}}{{^boolean}}\"",
"expected": "COMPILE EXCEPTION: crow::mustache error: open tag has no matching end tag {{# {{/ pair: boolean"
}
],
"__ATTN__": "This file was hand-written"
}
30 changes: 18 additions & 12 deletions tests/template/mustachetest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,23 @@ string read_all(const string& filename)

int main()
{
auto data = json::load(read_all("data"));
auto templ = compile(read_all("template"));
auto partials = json::load(read_all("partials"));
set_loader([&](std::string name) -> std::string {
if (partials.count(name))
{
return partials[name].s();
}
return "";
});
context ctx(data);
cout << templ.render_string(ctx);
try {
auto data = json::load(read_all("data"));
auto templ = compile(read_all("template"));
auto partials = json::load(read_all("partials"));
set_loader([&](std::string name) -> std::string {
if (partials.count(name))
{
return partials[name].s();
}
return "";
});
context ctx(data);
cout << templ.render_string(ctx);
}
// catch and return compile errors as text, for the python test to compare
catch (invalid_template_exception & err) {
cout << "COMPILE EXCEPTION: " << err.what();
}
return 0;
}
2 changes: 1 addition & 1 deletion tests/template/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
print('Data: ', json.dumps(test["data"]))
print('Template: ', test["template"])
print('Expected:', repr(test["expected"]))
print('Actual:', repr(ret))
print('Actual: ', repr(ret))
assert ret == test["expected"]

os.unlink('data')
Expand Down
Loading