c2a6ef817930149ad6f55818fa0196d99c47e02b
braney
  Fri Sep 11 10:40:11 2026 -0700
cheapcgi: skip a CGI pair with no =value instead of aborting, refs #38335

Both query string parsers looked for the '=' across the whole rest of the
string rather than inside the current pair.  A pair with no '=' in it
therefore ran into the pair after it and took its value.  "g-catV2&db=hg38"
was stored as one variable named "g-catV2&db", so db was lost with no
warning, and that corrupt name was copied on into the cart.  The same pair
at the end of the string had no '=' left to find and aborted the whole
request, which is what the "Mangled CGI input string g-catV2" entries in the
hgw1 logs were.

Both parsers now find the end of the pair first, keeping the existing
separator precedence ('&', then ';' for DAS), and skip a pair with no '='.
A mixed "a=1;b=2&c=3" still parses the way it did.

Adds lib/tests/cgiParseTest, which runs 18 query strings through both
parsers.  It covers the empty pair of #38185 as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git src/lib/tests/expected/cgiParseTest src/lib/tests/expected/cgiParseTest
new file mode 100644
index 00000000000..2e38ca7d26e
--- /dev/null
+++ src/lib/tests/expected/cgiParseTest
@@ -0,0 +1,72 @@
+in  : db=hg38&position=chr1:1-1000
+next: [db=hg38][position=chr1:1-1000]
+hash: [db=hg38][position=chr1:1-1000]
+
+in  : a=1;b=2;c=3
+next: [a=1][b=2][c=3]
+hash: [a=1][b=2][c=3]
+
+in  : a=1;b=2&c=3
+next: [a=1;b=2][c=3]
+hash: [a=1;b=2][c=3]
+
+in  : &db=hg38&position=chr1:1-1000
+next: [db=hg38][position=chr1:1-1000]
+hash: [db=hg38][position=chr1:1-1000]
+
+in  : db=hg38&&position=chr1:1-1000
+next: [db=hg38][position=chr1:1-1000]
+hash: [db=hg38][position=chr1:1-1000]
+
+in  : db=hg38&position=chr1:1-1000&&
+next: [db=hg38][position=chr1:1-1000]
+hash: [db=hg38][position=chr1:1-1000]
+
+in  : &&;&db=hg38
+next: [db=hg38]
+hash: [db=hg38]
+
+in  : g-catV2&db=hg38
+next: [db=hg38]
+hash: [db=hg38]
+
+in  : db=hg38&i&position=chr1:1-1000
+next: [db=hg38][position=chr1:1-1000]
+hash: [db=hg38][position=chr1:1-1000]
+
+in  : db=hg38&g-catV2
+next: [db=hg38]
+hash: [db=hg38]
+
+in  : g-catV2
+next: 
+hash: 
+
+in  : a=1&b=&c=3
+next: [a=1][b=][c=3]
+hash: [a=1][b=][c=3]
+
+in  : =v
+next: [=v]
+hash: [=v]
+
+in  : a=1&=&b=2
+next: [a=1][=][b=2]
+hash: [a=1][=][b=2]
+
+in  : 
+next: 
+hash: 
+
+in  : &
+next: 
+hash: 
+
+in  : hgt.customText=track name=one
+next: [hgt.customText=track name=one]
+hash: [hgt.customText=track name=one]
+
+in  : position=chr1%3A1-1000&name=a+b&other=x%26y
+next: [position=chr1:1-1000][name=a b][other=x&y]
+hash: [position=chr1:1-1000][name=a b][other=x&y]
+