fix: honour a chosen separator, allocate explicitly-zero sections - #25
Merged
Conversation
Three bugs found while auditing v0.3.0. A separator the caller chose was silently replaced. v0.3.0 collapsed repeated separators by routing the value through strings.Fields whenever the separator looked like whitespace, so SplitChar = "\t" split on spaces too: "a b\tc d" became four elements instead of two. v0.2.5 got this right. Only the default separator collapses repeats now, and it does so by dropping empty elements rather than re-splitting, which also stops a tab inside an element from being treated as a separator. An optional section was allocated based on the result being non-zero, which cannot tell "nothing was said about this section" from "every value in it is zero". PORT=0 left the section nil, and List advertised keys that Get could not read back. The walk now reports whether a field was actually assigned, from the environment or from a default tag, and the section is allocated on that. A value that merely contains an "e" was blamed on the fragment after it: PORT=not-a-number reported `strconv.Atoi: parsing "r"` and PORT=hello reported `parsing "llo"`. A non-numeric exponent means the value is not scientific notation at all, so it is handed back unchanged and reported whole. This one predates v0.3.0. Also documents that the scientific and thousand-separator notation is deliberately not applied to slice elements, where "1,2" is far more likely to be the wrong separator than the number 12. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Three bugs found while auditing v0.3.0. Two of them are mine from #24, one predates it.
1. A separator the caller chose was silently replaced — regression from #24
#24 made the default separator collapse repeats (
"a b"should be two elements, not three). It did that by routing the value throughstrings.Fieldswhenever the separator looked like whitespace — so a deliberate"\t","\n"or" "was thrown away and the value was split on every whitespace run instead.The fix keys off is it the default separator, not is it whitespace, and collapses by dropping empty elements rather than re-splitting:
Splitting first and filtering after also fixes a second-order bug: a tab inside an element is no longer treated as a separator when the default is in use.
Worth checking:
parts[:0]reuses the backing array. The write index never overtakes the read index, so it is the standard in-place filter, but it is the kind of thing worth a second pair of eyes.2. Sections were allocated on the wrong signal — incomplete feature from #24
Pointer sections were allocated when the filled-in value came out non-zero. That cannot tell "nothing was said about this section" from "every value in it is zero":
It also left
ListandGetdisagreeing:ListadvertisedP_PORTandP_NAME, andGetthen answeredkey not foundfor both.The walk now carries a
filled *boolthat is marked at each site that actually assigns something — the customSetValuehook,setPointer,parseSlice,setValue, and a nested section that allocated — andrangeOverPointerallocates on that instead of onIsZero.Worth checking:
defaulttag counts as an assignment, so a section whose only default isdefault:"0"is now allocated where it previously was not. That reads as correct to me — an explicit default is a statement of intent — but it is a deliberate behaviour change and the one judgement call in this PR.Emptycase inTestPointerSectionExplicitZero.3. Errors blamed the wrong part of the value — predates #24
Any non-numeric value containing an
ewas reported as a failure to parse the fragment after thee:not-a-numberstrconv.Atoi: parsing "r"strconv.ParseInt: parsing "not-a-number"hellostrconv.Atoi: parsing "llo"strconv.ParseInt: parsing "hello"A non-numeric exponent means the value is not scientific notation at all, so
parseScientifichands it back unchanged and lets the caller report it whole.1e,1E,1e1e1eand1e-3still error insideparseScientific, anderrors.As(err, &numErr)still unwraps to*strconv.NumError.Verification
Regression tests added for all three (
TestCustomWhitespaceSplitChar,TestPointerSectionExplicitZero,TestNonNumericValueErrorMessage). The section test also asserts that every keyListadvertises is readable back throughGet, which is the invariant that was broken.Coverage 94.2% -> 95.2%. vet, golint and gofmt clean. Passes
-race -shuffle=on -count=2.Checked against a v0.2.5 build: the tab separator now matches it again, and the comma separator was never affected.
Audited beyond these three and found nothing else wrong: named types and
*time.Duration, exponent bounds (1e18ok,1e19out of range for int64 but fine for uint64,1e21rejected, int64 max exact), negative/+/comma numbers, mutually recursive types terminating in bothParseandList, deep nested sections, sections under a prefix,-tags on sections, errors propagating out of sections, 32 concurrentParsecalls under-race, andGetwith a prefix on a read-only config.guare-checked against this branch: tests pass and its example prints byte-identical output across all eight scenarios.Not in this PR
Embedded unexported struct types are skipped entirely, in every version including v0.1.4 —
type Conf struct { inner; ... }never fillsinner's exported fields. It is fixable (the promoted field reportsCanSet() == true; it is ourIsExported()check that skips it) but it needs a naming decision first: prefix the keys like exported embedded structs do today (INNER_HOST), or flatten them likeencoding/json(HOST). Flattening would be a breaking change for exported embedding, so I left it alone.🤖 Generated with Claude Code