This is an automated email from the ASF dual-hosted git repository.

serrislew pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 2441fa7114 Cripts: make URL::Query copyable via deep-copy of _state 
(#13269)
2441fa7114 is described below

commit 2441fa711474b334e66b376e78b1a333a5e6041b
Author: Serris Santos <[email protected]>
AuthorDate: Thu Jun 18 12:31:47 2026 -0700

    Cripts: make URL::Query copyable via deep-copy of _state (#13269)
---
 include/cripts/Urls.hpp                        | 38 +++++++++++++++
 src/cripts/tests/query_test.cc                 | 15 ++++++
 tests/gold_tests/cripts/cripts.test.py         | 44 +++++++++++++++++-
 tests/gold_tests/cripts/files/query_copy.cript | 64 ++++++++++++++++++++++++++
 tests/gold_tests/cripts/gold/basic_cript.gold  |  1 +
 tests/gold_tests/cripts/gold/certs_cript.gold  |  1 +
 6 files changed, 162 insertions(+), 1 deletion(-)

diff --git a/include/cripts/Urls.hpp b/include/cripts/Urls.hpp
index 73580aa068..3ed143bae2 100644
--- a/include/cripts/Urls.hpp
+++ b/include/cripts/Urls.hpp
@@ -335,6 +335,25 @@ public:
 
     using Component::Component;
 
+    // _state is deep-copied, but the segment `string_view`s (and 
Component::_owner) still
+    // reference the source URL. The copy's views dangle if the source URL's 
path is rewritten,
+    // and Flush()/operator= on the copy write back to the source URL via 
TSUrlPathSet.
+    Path(const self_type &o) : Component(o), _state(o._state ? 
std::make_unique<State>(*o._state) : nullptr) {}
+
+    self_type &
+    operator=(const self_type &o)
+    {
+      if (this != &o) {
+        auto new_state = o._state ? std::make_unique<State>(*o._state) : 
nullptr;
+        Component::operator=(o);
+        _state = std::move(new_state);
+      }
+      return *this;
+    }
+
+    Path(self_type &&)                 = default;
+    self_type &operator=(self_type &&) = default;
+
     void Reset() override;
 
     cripts::string_view GetSV() override;
@@ -485,6 +504,25 @@ public:
       _state->standalone = true;
     }
 
+    // _state is deep-copied, but the parameter `string_view`s (and 
Component::_owner) still
+    // reference the source URL. The copy's views dangle if the source URL's 
query is rewritten,
+    // and Flush()/operator= on the copy write back to the source URL via 
TSUrlHttpQuerySet.
+    Query(const self_type &o) : Component(o), _state(o._state ? 
std::make_unique<State>(*o._state) : nullptr) {}
+
+    self_type &
+    operator=(const self_type &o)
+    {
+      if (this != &o) {
+        auto new_state = o._state ? std::make_unique<State>(*o._state) : 
nullptr;
+        Component::operator=(o);
+        _state = std::move(new_state);
+      }
+      return *this;
+    }
+
+    Query(self_type &&)                = default;
+    self_type &operator=(self_type &&) = default;
+
     void Reset() override;
 
     cripts::string_view GetSV() override;
diff --git a/src/cripts/tests/query_test.cc b/src/cripts/tests/query_test.cc
index 8c29598201..83500dd23a 100644
--- a/src/cripts/tests/query_test.cc
+++ b/src/cripts/tests/query_test.cc
@@ -31,6 +31,21 @@ do_remap()
 {
   borrow url = Client::URL::get();
 
+  // Force the live Query to parse so _state is populated;
+  // otherwise copies won't exercise the deep-copy path.
+  url.query["foo"];
+
+  // Copy-construct Query to exercise the explicit copy ctor (Query owns a 
std::unique_ptr<State>).
+  // Note: this remains tied to the same underlying URL; it is not an 
independent snapshot.
+  cripts::Url::Query query_copy = url.query;
+  query_copy.Erase("foo");
+
+  // Print the copy before mutating the live URL. After url.query.Flush() 
below,
+  // url.query is Reset()'d and TSUrlHttpQuerySet may invalidate the TS-owned
+  // buffer that query_copy's State string_views point into.
+  CDebug("Live: {}", url.query);
+  CDebug("Copy (foo erased): {}", query_copy);
+
   switch (AsInteger(instance.data[0])) {
   case 0:
     url.query.Erase({"foo", "bar"}, true);
diff --git a/tests/gold_tests/cripts/cripts.test.py 
b/tests/gold_tests/cripts/cripts.test.py
index bffe3c4c87..6c93336547 100644
--- a/tests/gold_tests/cripts/cripts.test.py
+++ b/tests/gold_tests/cripts/cripts.test.py
@@ -40,12 +40,24 @@ class CriptsBasicTest:
 
         request_header = {"headers": "GET / HTTP/1.1\r\nHost: 
www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
         response_header = {
-            "headers": "HTTP/1.1 200 OK\r\responseHeader: unchanged\r\n\r\n",
+            "headers": "HTTP/1.1 200 OK\r\nresponseHeader: 
unchanged\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
             "timestamp": "1469733493.993",
             "body": ""
         }
         self.server.addResponse("sessionfile.log", request_header, 
response_header)
 
+        query_request_header = {
+            "headers": "GET /path/to/resource?foo=1&bar=2&baz=3 
HTTP/1.1\r\nHost: does.not.matter\r\n\r\n",
+            "timestamp": "1469733493.993",
+            "body": ""
+        }
+        query_response_header = {
+            "headers": "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: 
close\r\n\r\n",
+            "timestamp": "1469733493.993",
+            "body": ""
+        }
+        self.server.addResponse("sessionfile.log", query_request_header, 
query_response_header)
+
     def setUpTS(self):
         self.ts = Test.MakeATSProcess("ts_in", enable_tls=True, 
enable_cache=False, enable_cripts=True)
 
@@ -59,6 +71,7 @@ ssl_multicert:
 """.split("\n"))
 
         self.ts.Setup.Copy('files/basic.cript', self.ts.Variables.CONFIGDIR)
+        self.ts.Setup.Copy('files/query_copy.cript', 
self.ts.Variables.CONFIGDIR)
 
         self.ts.Disk.records_config.update(
             {
@@ -72,6 +85,8 @@ ssl_multicert:
         self.ts.Disk.remap_config.AddLine(
             f'map https://www.example.com:{self.ts.Variables.ssl_port} 
http://127.0.0.1:{self.server.Variables.Port} @plugin=basic.cript'
         )
+        self.ts.Disk.remap_config.AddLine(
+            f'map http://query.example.com 
http://127.0.0.1:{self.server.Variables.Port} @plugin=query_copy.cript')
 
     def runHeaderTest(self):
         tr = Test.AddTestRun('Exercise traffic through cripts.')
@@ -91,8 +106,35 @@ ssl_multicert:
         tr.Processes.Default.Streams.stderr = "gold/certs_cript.gold"
         tr.StillRunningAfter = self.server
 
+    def runQueryCopyTest(self):
+        tr = Test.AddTestRun('Exercise Query/Path copy ctor / copy-assign in 
cripts.')
+        tr.MakeCurlCommand(
+            f'-v -H "Host: query.example.com" 
"http://127.0.0.1:{self.ts.Variables.port}/path/to/resource?foo=1&bar=2&baz=3";',
+            ts=self.ts)
+        tr.Processes.Default.ReturnCode = 0
+        # Live URL is untouched: original query is preserved.
+        tr.Processes.Default.Streams.stderr = Testers.ContainsExpression(
+            r"X-Original-Query: foo=1&bar=2&baz=3", "Original query must 
round-trip unchanged")
+        # Mutating the copy (Erase "foo") must not have leaked into the 
original.
+        tr.Processes.Default.Streams.stderr += Testers.ContainsExpression(
+            r"X-Copy-Query: bar=2&baz=3", "Erase on the copy must drop foo 
without affecting the original")
+        # Copy-assignment path produces a snapshot equal to the original.
+        tr.Processes.Default.Streams.stderr += Testers.ContainsExpression(
+            r"X-Assigned-Query: foo=1&bar=2&baz=3", "Copy-assigned query must 
equal the original")
+        # Live URL path is untouched: original path is preserved.
+        tr.Processes.Default.Streams.stderr += Testers.ContainsExpression(
+            r"X-Original-Path: path/to/resource", "Original path must 
round-trip unchanged")
+        # Mutating the copy (segment[0] = "edited") must not have leaked into 
the original.
+        tr.Processes.Default.Streams.stderr += Testers.ContainsExpression(
+            r"X-Copy-Path: edited/to/resource", "Segment edit on the copy must 
not affect the original")
+        # Copy-assignment path produces a snapshot equal to the original.
+        tr.Processes.Default.Streams.stderr += Testers.ContainsExpression(
+            r"X-Assigned-Path: path/to/resource", "Copy-assigned path must 
equal the original")
+        tr.StillRunningAfter = self.server
+
     def run(self):
         self.runHeaderTest()
+        self.runQueryCopyTest()
         if not Condition.CurlUsingUnixDomainSocket():
             self.runCertsTest()
 
diff --git a/tests/gold_tests/cripts/files/query_copy.cript 
b/tests/gold_tests/cripts/files/query_copy.cript
new file mode 100644
index 0000000000..3121a6c4fd
--- /dev/null
+++ b/tests/gold_tests/cripts/files/query_copy.cript
@@ -0,0 +1,64 @@
+/*
+  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 <cripts/Preamble.hpp>
+
+do_remap()
+{
+  borrow url = cripts::Client::URL::Get();
+
+  // Force the live Query / Path to parse so _state is populated; otherwise the
+  // copies below take the nullptr branch and the deep-copy logic isn't 
exercised.
+  url.query["foo"];
+  url.path[0];
+
+  // Copy-construct to exercise the explicit copy ctor (each owns a 
std::unique_ptr<State>).
+  // Note: this remains tied to the same underlying URL; it is not an 
independent snapshot.
+  cripts::Url::Query query_copy = url.query;
+  cripts::Url::Query query_assigned;
+  cripts::Url::Path  path_copy = url.path;
+  cripts::Url::Path  path_assigned;
+
+  query_assigned = url.query;
+  path_assigned  = url.path;
+
+  query_copy.Erase("foo");
+  path_copy[0] = "edited";
+
+  // Materialize into owned strings so do_send_response sees stable values.
+  txn_data[0] = cripts::string(url.query.GetSV());
+  txn_data[1] = cripts::string(query_copy.GetSV());
+  txn_data[2] = cripts::string(query_assigned.GetSV());
+  txn_data[3] = cripts::string(url.path.GetSV());
+  txn_data[4] = cripts::string(path_copy.GetSV());
+  txn_data[5] = cripts::string(path_assigned.GetSV());
+}
+
+do_send_response()
+{
+  borrow resp = cripts::Client::Response::Get();
+
+  resp["X-Original-Query"] = AsString(txn_data[0]);
+  resp["X-Copy-Query"]     = AsString(txn_data[1]);
+  resp["X-Assigned-Query"] = AsString(txn_data[2]);
+  resp["X-Original-Path"]  = AsString(txn_data[3]);
+  resp["X-Copy-Path"]      = AsString(txn_data[4]);
+  resp["X-Assigned-Path"]  = AsString(txn_data[5]);
+}
+
+#include <cripts/Epilogue.hpp>
diff --git a/tests/gold_tests/cripts/gold/basic_cript.gold 
b/tests/gold_tests/cripts/gold/basic_cript.gold
index f3a6ea87f2..f06580416c 100644
--- a/tests/gold_tests/cripts/gold/basic_cript.gold
+++ b/tests/gold_tests/cripts/gold/basic_cript.gold
@@ -7,6 +7,7 @@
 ``
 < HTTP/1.1 200 OK
 < responseHeader: changed
+< Content-Length: 0
 < Date: ``
 < Age: ``
 < Connection: keep-alive
diff --git a/tests/gold_tests/cripts/gold/certs_cript.gold 
b/tests/gold_tests/cripts/gold/certs_cript.gold
index bfc31a9e71..7bb8621565 100644
--- a/tests/gold_tests/cripts/gold/certs_cript.gold
+++ b/tests/gold_tests/cripts/gold/certs_cript.gold
@@ -6,6 +6,7 @@
 ``
 < HTTP/1.1 200 OK
 < responseHeader: changed
+< Content-Length: 0
 < Date: ``
 < Age: ``
 < Connection: keep-alive

Reply via email to