Skip to content

Commit 48c3f17

Browse files
Allow : in URL template segments (#7046)
Co-authored-by: Amaury Chamayou <amaury@xargs.fr>
1 parent c0a60d5 commit 48c3f17

3 files changed

Lines changed: 70 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1313

1414
- Nodes will now avoid re-parsing `.committed` files in the main directory if they have established a later commit point in the `read_only` directories. This should significantly reduce start-up time for nodes with large existing ledgers.
1515

16+
### Changed
17+
18+
- Allow `:` within regex matched templated URL components again, while still terminating matched segments correctly (#7046).
19+
1620
### Dependencies
1721

1822
- Updated didx509cpp to 0.11.0 (#7050).

src/endpoints/endpoint_registry.cpp

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -131,42 +131,30 @@ namespace ccf::endpoints
131131

132132
PathTemplateSpec spec;
133133

134-
const std::string allowed_delimiters = "/:";
135-
136134
std::string regex_s(uri);
137135
template_start = regex_s.find_first_of('{');
136+
size_t template_end = 0;
138137
while (template_start != std::string::npos)
139138
{
140-
if (template_start != 0)
141-
{
142-
const auto prev_char = regex_s[template_start - 1];
143-
if (allowed_delimiters.find(prev_char) == std::string::npos)
144-
{
145-
throw std::logic_error(fmt::format(
146-
"Invalid templated path - illegal character ({}) preceding "
147-
"template: {}",
148-
prev_char,
149-
uri));
150-
}
151-
}
152-
153-
const auto template_end = regex_s.find_first_of('}', template_start);
139+
template_end = regex_s.find_first_of('}', template_start);
154140
if (template_end == std::string::npos)
155141
{
156142
throw std::logic_error(fmt::format(
157143
"Invalid templated path - missing closing curly bracket: {}", uri));
158144
}
159145

160-
if (template_end + 1 != regex_s.size())
146+
// Default regex is "([^/]+)", aka "match everything until the next /"
147+
std::string regex_terminator = "/";
148+
149+
if (template_end < regex_s.size() - 1)
161150
{
162-
const auto next_char = regex_s[template_end + 1];
163-
if (allowed_delimiters.find(next_char) == std::string::npos)
151+
const auto terminator_candidate = regex_s[template_end + 1];
152+
if (terminator_candidate != '/')
164153
{
165-
throw std::logic_error(fmt::format(
166-
"Invalid templated path - illegal character ({}) following "
167-
"template: {}",
168-
next_char,
169-
uri));
154+
// If there's some other character literal following the template,
155+
// treat that as a terminator as well.
156+
// eg: "/{foo}:bar" => "/(^[/:]):bar"
157+
regex_terminator += terminator_candidate;
170158
}
171159
}
172160

@@ -175,7 +163,8 @@ namespace ccf::endpoints
175163
regex_s.replace(
176164
template_start,
177165
template_end - template_start + 1,
178-
fmt::format("([^{}]+)", allowed_delimiters));
166+
fmt::format("([^{}]+)", regex_terminator));
167+
179168
template_start = regex_s.find_first_of('{', template_start + 1);
180169
}
181170

src/endpoints/test/endpoint_registry.cpp

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,34 @@ std::optional<PathTemplateSpec> require_parsed_components(
3131
return spec;
3232
}
3333

34+
using ExpectedMatchTestCase = std::map<std::string, std::vector<std::string>>;
35+
36+
void require_template_parsing(
37+
const std::string& url_template,
38+
const ExpectedMatchTestCase& matched = {},
39+
const std::vector<std::string>& unmatched = {})
40+
{
41+
std::optional<PathTemplateSpec> spec;
42+
REQUIRE_NOTHROW(spec = PathTemplateSpec::parse(url_template));
43+
44+
for (const auto& [path, elements] : matched)
45+
{
46+
std::smatch match;
47+
REQUIRE(std::regex_match(path, match, spec->template_regex));
48+
REQUIRE(match.size() == elements.size() + 1);
49+
for (size_t i = 1; i < match.size(); ++i)
50+
{
51+
REQUIRE(match[i].str() == elements[i - 1]);
52+
}
53+
}
54+
55+
for (const auto& path : unmatched)
56+
{
57+
std::smatch match;
58+
REQUIRE_FALSE(std::regex_match(path, match, spec->template_regex));
59+
}
60+
}
61+
3462
TEST_CASE("URL template parsing")
3563
{
3664
ccf::logger::config::default_init();
@@ -54,9 +82,9 @@ TEST_CASE("URL template parsing")
5482
REQUIRE(match[2].str() == "spain");
5583

5684
path = prefix + "/alice:jump/spain";
57-
REQUIRE_FALSE(std::regex_match(path, match, parsed->template_regex));
58-
path = prefix + "/alice/spain:jump";
59-
REQUIRE_FALSE(std::regex_match(path, match, parsed->template_regex));
85+
REQUIRE(std::regex_match(path, match, parsed->template_regex));
86+
REQUIRE(match[1].str() == "alice:jump");
87+
REQUIRE(match[2].str() == "spain");
6088

6189
require_parsed_components(prefix + "/{name}:do", {"name"});
6290
require_parsed_components(prefix + "/{name}:do/world", {"name"});
@@ -84,15 +112,27 @@ TEST_CASE("URL template parsing")
84112
REQUIRE(match[3].str() == "spain");
85113
}
86114

87-
REQUIRE_THROWS(PathTemplateSpec::parse("/foo{id}"));
88-
REQUIRE_THROWS(PathTemplateSpec::parse("/foo{id}bar"));
89-
REQUIRE_THROWS(PathTemplateSpec::parse("/{id}bar"));
90-
REQUIRE_THROWS(PathTemplateSpec::parse("/{id}-{name}"));
91-
REQUIRE_THROWS(PathTemplateSpec::parse("/id{id}"));
92-
REQUIRE_THROWS(PathTemplateSpec::parse("/foo{id}:"));
93-
REQUIRE_THROWS(PathTemplateSpec::parse("/foo{id}/bar"));
94-
REQUIRE_THROWS(PathTemplateSpec::parse("/foo/{id}bar"));
95-
REQUIRE_THROWS(PathTemplateSpec::parse("/foo/id{id}:bar"));
115+
require_template_parsing(
116+
"/foo{id}",
117+
{{"/foo1", {"1"}}, {"/foobar", {"bar"}}},
118+
{"/foo", "/foo/1", "/foo/bar"});
119+
require_template_parsing(
120+
"/foo{id}bar",
121+
{{"/foo1bar", {"1"}}, {"/foofazbar", {"faz"}}},
122+
{"/foobar", "/foobazbar", "/foo/bar", "/foo/baz/bar"});
123+
require_template_parsing(
124+
"/{id}bar",
125+
{{"/1bar", {"1"}}, {"/foobar", {"foo"}}},
126+
{"/bar", "/bazbar", "/foo/bar", "foo/bar"});
127+
require_template_parsing(
128+
"/{id}-{name}",
129+
{{"/foo-bar", {"foo", "bar"}}, {"/1-2", {"1", "2"}}},
130+
{"/foobar", "/foo/-bar", "/foo-/bar", "/foo/-/bar"});
131+
require_template_parsing("/id{id}");
132+
require_template_parsing("/foo{id}:");
133+
require_template_parsing("/foo{id}/bar");
134+
require_template_parsing("/foo/{id}bar");
135+
require_template_parsing("/foo/id{id}:bar");
96136

97137
REQUIRE_THROWS(PathTemplateSpec::parse("/{id}/{id}"));
98138
REQUIRE_THROWS(PathTemplateSpec::parse("/foo/{id}/{id}"));

0 commit comments

Comments
 (0)