Copilot commented on code in PR #13424: URL: https://github.com/apache/trafficserver/pull/13424#discussion_r4147835016
########## plugins/header_rewrite/url_query.cc: ########## @@ -0,0 +1,91 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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 + + http://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 "url_query.h" + +#include <algorithm> +#include <vector> + +#include "swoc/TextView.h" + +namespace +{ + +std::vector<std::string_view> +split(std::string_view text, char delimiter) +{ + std::vector<std::string_view> tokens; + swoc::TextView view(text); + + while (view) { + tokens.push_back(view.take_prefix_at(delimiter)); + } + + return tokens; +} + +std::string_view +param_name(std::string_view param) +{ + return param.substr(0, param.find('=')); +} + +} // namespace + +std::string +sort_query(std::string_view query) +{ + if (query.empty()) { + return {}; + } + + std::vector<std::string_view> params = split(query, '&'); Review Comment: `sort_query()` does not reliably “drop empty tokens” as described. Empty tokens are only *incidentally* removed when they sort ahead of all non-empty parameter names; if empty tokens are adjacent to parameters with the same sort key (e.g. empty-name parameters like "=x"), they can remain in the output and even reintroduce "&&" (example: "=x&&=y" can round-trip with an empty token preserved). Fix by explicitly filtering out empty `param` tokens (and only joining non-empty params) before sorting and building the result. ########## plugins/header_rewrite/operators.cc: ########## @@ -532,6 +533,67 @@ OperatorRMDestination::exec(const Resources &res) const return true; } +// OperatorSortDestination +void +OperatorSortDestination::initialize(Parser &p) +{ + Operator::initialize(p); + + _url_qual = parse_url_qualifier(p.get_arg()); + + require_resources(RSRC_CLIENT_REQUEST_HEADERS); + require_resources(RSRC_SERVER_REQUEST_HEADERS); +} + +bool +OperatorSortDestination::exec(const Resources &res) const +{ + if (res._rri || (res.bufp && res.hdr_loc)) { + TSMBuffer bufp; + TSMLoc url_m_loc; + + // Determine which TSMBuffer and TSMLoc to use + if (res._rri) { + bufp = res._rri->requestBufp; + url_m_loc = res._rri->requestUrl; + } else { + bufp = res.bufp; + if (TSHttpHdrUrlGet(res.bufp, res.hdr_loc, &url_m_loc) != TS_SUCCESS) { + Dbg(pi_dbg_ctl, "TSHttpHdrUrlGet was unable to return the url m_loc"); + return true; + } + } + + switch (_url_qual) { + case URL_QUAL_QUERY: { + int q_len = 0; + const char *q_ptr = TSUrlHttpQueryGet(bufp, url_m_loc, &q_len); + std::string_view query = q_len > 0 ? std::string_view(q_ptr, static_cast<size_t>(q_len)) : std::string_view(); + + if (is_query_sorted(query)) { + Dbg(pi_dbg_ctl, "OperatorSortDestination::exec() QUERY already sorted, leaving it unchanged"); + break; + } + + std::string sorted = sort_query(query); + + const_cast<Resources &>(res).changed_url = true; + TSUrlHttpQuerySet(bufp, url_m_loc, sorted.c_str(), sorted.size()); Review Comment: `TSUrlHttpQuerySet()` takes an `int` length and returns a `TSReturnCode`. Passing `sorted.size()` (size_t) risks signed/unsigned conversion warnings, and ignoring the return code can hide failures. Cast the length to the expected type and handle/log non-`TS_SUCCESS` so the operator doesn’t silently claim success when the query wasn’t updated. ########## plugins/header_rewrite/operators.cc: ########## @@ -532,6 +533,67 @@ OperatorRMDestination::exec(const Resources &res) const return true; } +// OperatorSortDestination +void +OperatorSortDestination::initialize(Parser &p) +{ + Operator::initialize(p); + + _url_qual = parse_url_qualifier(p.get_arg()); + + require_resources(RSRC_CLIENT_REQUEST_HEADERS); + require_resources(RSRC_SERVER_REQUEST_HEADERS); +} + +bool +OperatorSortDestination::exec(const Resources &res) const +{ + if (res._rri || (res.bufp && res.hdr_loc)) { + TSMBuffer bufp; + TSMLoc url_m_loc; + + // Determine which TSMBuffer and TSMLoc to use + if (res._rri) { + bufp = res._rri->requestBufp; + url_m_loc = res._rri->requestUrl; + } else { + bufp = res.bufp; + if (TSHttpHdrUrlGet(res.bufp, res.hdr_loc, &url_m_loc) != TS_SUCCESS) { + Dbg(pi_dbg_ctl, "TSHttpHdrUrlGet was unable to return the url m_loc"); + return true; + } + } Review Comment: `url_m_loc` obtained via `TSHttpHdrUrlGet()` must be released with `TSHandleMLocRelease()` when you’re done with it (using the header `res.hdr_loc` as the parent). As written, the `else` branch leaks the URL handle on success. Track whether `url_m_loc` came from `TSHttpHdrUrlGet()` and release it before returning from `exec()` (including early exits after the switch). ########## plugins/header_rewrite/url_query.cc: ########## @@ -0,0 +1,91 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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 + + http://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 "url_query.h" + +#include <algorithm> +#include <vector> + +#include "swoc/TextView.h" + +namespace +{ + +std::vector<std::string_view> +split(std::string_view text, char delimiter) +{ + std::vector<std::string_view> tokens; + swoc::TextView view(text); + + while (view) { + tokens.push_back(view.take_prefix_at(delimiter)); + } + + return tokens; +} + +std::string_view +param_name(std::string_view param) +{ + return param.substr(0, param.find('=')); +} + +} // namespace + +std::string +sort_query(std::string_view query) +{ + if (query.empty()) { + return {}; + } + + std::vector<std::string_view> params = split(query, '&'); + + std::stable_sort(params.begin(), params.end(), + [](std::string_view a, std::string_view b) { return param_name(a) < param_name(b); }); + + std::string result; + result.reserve(query.size()); // same length as query, capped at 64KB by request_line_max_size + + for (const auto ¶m : params) { + if (!result.empty()) { + result += '&'; + } + result.append(param); + } Review Comment: `sort_query()` does not reliably “drop empty tokens” as described. Empty tokens are only *incidentally* removed when they sort ahead of all non-empty parameter names; if empty tokens are adjacent to parameters with the same sort key (e.g. empty-name parameters like "=x"), they can remain in the output and even reintroduce "&&" (example: "=x&&=y" can round-trip with an empty token preserved). Fix by explicitly filtering out empty `param` tokens (and only joining non-empty params) before sorting and building the result. ########## plugins/header_rewrite/unit_tests/test_url_query.cc: ########## @@ -0,0 +1,109 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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 + + http://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 <catch2/catch_test_macros.hpp> + +#include "url_query.h" + +TEST_CASE("sort_query orders params by name", "[header_rewrite][url_query]") +{ + SECTION("out-of-order params are sorted") + { + CHECK(sort_query("b=2&a=1") == "a=1&b=2"); + } + + SECTION("valueless params sort by their own name") + { + CHECK(sort_query("b&a=1") == "a=1&b"); + } + + SECTION("params with duplicate names keep their relative order") + { + CHECK(sort_query("x=1&a=2&x=3") == "a=2&x=1&x=3"); + } + + SECTION("empty query stays empty") + { + CHECK(sort_query("") == ""); + } + + SECTION("single param is unaffected") + { + CHECK(sort_query("a=1") == "a=1"); + } + + SECTION("trailing '&' produces a clean drop, not an empty trailing token") + { + CHECK(sort_query("a=1&") == "a=1"); + } + + SECTION("param value containing '=' is preserved and sorts by its name only") + { + CHECK(sort_query("a=1=2") == "a=1=2"); + } + + SECTION("leading '&' produces a clean drop, not an empty leading token") + { + CHECK(sort_query("&a=1") == "a=1"); + } + + SECTION("consecutive '&'s produce a clean drop, not empty middle tokens") + { + CHECK(sort_query("b=2&&a=1&&c=3") == "a=1&b=2&c=3"); + } +} + +TEST_CASE("is_query_sorted detects queries sort_query would leave unchanged", "[header_rewrite][url_query]") +{ + SECTION("params in name order are sorted") + { + CHECK(is_query_sorted("a=1&b=2&c=3")); + } + + SECTION("out-of-order params are not sorted") + { + CHECK_FALSE(is_query_sorted("b=2&a=1")); + } + + SECTION("duplicate names in any value order are sorted, since the sort is stable") + { + CHECK(is_query_sorted("a=2&x=3&x=1")); + } + + SECTION("empty query and single param are sorted") + { + CHECK(is_query_sorted("")); + CHECK(is_query_sorted("a=1")); + } + + SECTION("empty params are not sorted, since sort_query drops them") + { + CHECK_FALSE(is_query_sorted("&a=1")); + CHECK_FALSE(is_query_sorted("a=1&")); + CHECK_FALSE(is_query_sorted("a=1&&b=2")); + CHECK_FALSE(is_query_sorted("&")); + } + + SECTION("agrees with sort_query on every edge case") + { + for (std::string_view q : {"", "&", "&&", "a", "a=1", "a=1&", "&a=1", "a=1&&b=2", "b=2&a=1", "a=1&b=2", "b&a=1", "x=1&a=2&x=3", + "a=2&x=3&x=1", "a=1=2", "=x&a=1", "a=1&=x", "a1=x&a=x", "a=x&a1=x"}) { + INFO("query: \"" << q << "\""); + CHECK(is_query_sorted(q) == (sort_query(q) == q)); + } + } Review Comment: There’s no test covering empty-token dropping when neighboring parameters have the same sort key (notably empty-name parameters like `"=x"`). Add a case such as `"=x&&=y"` (and/or `"=x&=&=y"`) asserting `sort_query()` removes empty tokens and normalizes to a single `&` separator between remaining params, to prevent regressions of the empty-token handling bug. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
