Advance past the whole delimiter rune in splitPath - #77
Open
youdie006 wants to merge 1 commit into
Open
Conversation
splitPath ranges over the path, so i is a byte index while r is a rune,
but the delimiter branch returns path[i+1:]. With a multi-byte delimiter
the remaining continuation bytes are prepended to the next key segment,
so DelimitWith('.') style round-trips fail for any non-ASCII delimiter:
EncodeToStringWith(v, '·', '\\', false) -> "Foo%C2%B7Bar=x"
Decode of that -> "\ufffdBar doesn't exist"
escape, unescape, merge and the implicit-key handling all use string(d)
and are already rune-generic; this one line was doing byte arithmetic.
DecodeRuneInString returns (RuneError, 1) for an invalid byte, matching
what range consumed, so an ill-formed path advances exactly as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
splitPath(node.go:85) ranges over the path, soiis a byte index whileris a rune, but the delimiter branch returnspath[i+1:]:With a multi-byte delimiter the remaining continuation bytes get prepended to the next key segment. The encoder and decoder in this library then disagree with each other:
splitPath('\u00b7', '\\', "Foo\u00b7Bar")returnsrest = "\xb7Bar".Everything else in the file is already rune-generic
That is what convinced me this is a bug rather than an undocumented ASCII-only restriction:
node.go:28--p + escape(d, e, k) + string(d)node.go:44--strings.HasSuffix(k, string(d)+implicitKey)node.go:52--strconv.Itoa(i) + string(d) + knode.go:162,167--escape/unescapeviastrings.Replace(s, string(d), ...)I checked those on unmodified master rather than assuming:
escape('\u00b7','\\',"A\u00b7B")gives"A\\\u00b7B",unescapeinverts it, andvaluesproduces the keyFoo\u00b7Barcorrectly. One line out of six does byte arithmetic.Encoder.DelimitWithandDecoder.DelimitWithboth take arune, and neither the doc comments (decode.go:35,encode.go:34) nor the README restrict it to ASCII.The fix
utf8.DecodeRuneInStringfor the width, standard library only. I picked it overutf8.RuneLen(r)deliberately: on an invalid byte,DecodeRuneInStringreturns(RuneError, 1), which is exactly whatrangeconsumed, whereasRuneLen(utf8.RuneError)is 3 and would over-advance on ill-formed input.Compatibility note
Per CONTRIBUTING's stable-wire-format rule: for any single-byte delimiter
size == 1, so the expression is byte-identical to master and nothing changes. The only inputs whose result changes are those that currently produce mangled keys or a decode error -- nothing that works today stops working. The full suite, includingnode_test.go'sescapingTestCases, is green unmodified; no existing assertion had to change.Verification
The three commands CONTRIBUTING asks for:
gofmt -l .prints nothing,go vet ./...clean,go test ./...givesok github.com/ajg/formandok github.com/ajg/form/multipart. I also ran the fuzz target you mention --go test -run='^$' -fuzz=FuzzDecodeString -fuzztime=30s, 768,851 execs, PASS.Two tests added: a public-API round-trip over delimiters of 1, 2, 3 and 4 bytes, and a
splitPathtable that includes an escaped multi-byte delimiter. I checked the width from four directions rather than only confirming the new rows go green:path[i+1:]path[i+size+1:]'|'rowpath[i+size-1:]size := 2The last two matter most: they show the table pins the width exactly rather than merely rejecting the old behaviour, and that this is a width computation rather than a shifted offset.
What I did not change
escape,unescape,merge,parseValues-- already correct, verified on master.escstate machine for a multi-byte escape rune -- it does no byte arithmetic; the table row{'\u00b7', "Foo\\\u00b7Bar\u00b7Qux"}covers the escaped-multibyte-delimiter path.Disclosure: AI-assisted. I found and prepared this with an AI assistant, and I ran and verified everything above myself.