Compare commits

...

14 Commits

Author SHA1 Message Date
Cursor Agent 89f970069c Remove Parser.Range and subsliceOffset
Range() existed to recover byte offsets from Highlight subslices.
Now that ParserError carries an explicit Offset field, Range() is
unnecessary. Remove it along with the private subsliceOffset helper
in ast.go.

Tests now use perr.Offset directly and construct Range literals
for Shape() calls.

Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
2026-04-12 19:17:12 +00:00
Cursor Agent a646ffd9fa Make error position tracking explicit with Offset field on ParserError
Thread byte offset information through all error creation sites,
eliminating the need for SubsliceOffset to recover position from
pointer comparison.

Changes:
- Add Offset field to ParserError struct
- Add offset parameter to NewParserError
- Add Parser.offsetOf helper for suffix-length arithmetic
- Thread base offset through scanner functions (scanComment,
  scanBasicString, scanMultilineBasicString, scanLiteralString,
  scanMultilineLiteralString, scanWindowsNewline)
- Thread base offset through standalone functions (expect, hexToRune)
- Thread base offset through all decode functions (parseInteger,
  parseFloat, parseLocalDate, parseLocalTime, parseLocalDateTime,
  parseDateTime, checkAndRemoveUnderscores*)
- Update all unmarshaler call sites to pass value.Raw.Offset
- Update localtime.go UnmarshalText methods with base=0
- Update strict.go to populate Offset from key ranges
- Change wrapDecodeError to read de.Offset directly
- Change Utf8TomlValidAlreadyEscaped to return int index (-1 if valid)
  instead of a byte subslice
- Unexport SubsliceOffset (now only used internally by Range())

This makes error positions self-describing: each ParserError carries its
own byte offset, so callers no longer need the original document slice
and address arithmetic to determine where an error occurred.

Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
2026-04-12 19:08:55 +00:00
Cursor Agent d75117e61f Consolidate subslice offset into a single SubsliceOffset function
Remove the private subsliceOffset methods from both parser.go and
errors.go. Replace them with a single exported SubsliceOffset function
in ast.go (next to the Range type it serves).

SubsliceOffset finds the byte offset by comparing element addresses:
&data[i] == &subslice[0]. This is well-defined Go pointer comparison
on elements of the same backing array.

This fixes the v2.3.0 regression (#1047) where the parser's
subsliceOffset used len(data) - len(b), which only works for suffix
slices, not arbitrary subslices like error highlights. It also removes
the reflect-based implementation from errors.go.

Fixes #1047

Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
2026-04-12 18:33:22 +00:00
Cursor Agent 19174a4293 Remove cap tricks, use address comparison for subslice offset
Replace cap(parent) - cap(subslice) with a straightforward scan
that compares element addresses: &data[i] == &subslice[0]. This is
well-defined Go pointer comparison on elements of the same backing
array, with no dependency on capacity semantics, reflect, or unsafe.

The scan is O(n) but only runs on error paths, and TOML documents
are small per the project's design constraints.

Also remove the Offset field from ParserError and the setErrOffset
machinery — the offset is computed at the point of consumption
(wrapDecodeError, Parser.Range) rather than cached on the error.

Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
2026-04-12 18:17:55 +00:00
Cursor Agent 96ac48eb74 Remove optional offset and fallback, guarantee offset by construction
ParserError.Offset is now a plain exported int field, always set:
- The parser sets it via setErrOffset() when capturing parse errors
- strict.go sets it from the key's Raw range at construction
- wrapDecodeError computes it inline from cap(document) - cap(highlight)

This eliminates:
- The SetOffset/Offset() accessor methods and offsetValid flag
- The subsliceOffset fallback function in errors.go
- Any conditional logic around whether the offset is present

The offset is guaranteed by construction at every path that creates
or consumes a ParserError.

Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
2026-04-12 17:25:32 +00:00
Cursor Agent f7136d052b Replace reflect-based subslice offset with cap arithmetic
Use cap(parent) - cap(subslice) to compute byte offsets between slices
that share a backing array. This is safe pure Go: subslicing preserves
the backing array and adjusts the capacity accordingly, so the
difference in capacities equals the byte offset.

This removes the reflect import from both errors.go and
unstable/parser.go, eliminating the last reflect-based pointer
arithmetic used for error position tracking.

Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
2026-04-12 17:08:58 +00:00
Cursor Agent 154d80392f Cache error offset in ParserError for safer position tracking
Instead of requiring downstream consumers to re-derive the byte offset
from pointer arithmetic on the Highlight slice, compute and cache the
offset inside the parser at error-capture time via setErrOffset().

This is safer because:
- The parser is the one place where the backing-array guarantee is known
  to hold (Highlight is always a subslice of the parse buffer)
- Downstream consumers (wrapDecodeError) can use the cached offset
  directly, avoiding the need for pointer comparison
- Errors created outside the parser (strict.go) set the offset from
  existing Raw ranges, which are already correct by construction

Add ParserError.SetOffset/Offset methods for setting and retrieving the
cached offset. Update wrapDecodeError to prefer the cached offset when
available, falling back to subsliceOffset for backward compatibility.

Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
2026-04-12 13:00:38 +00:00
Cursor Agent d528d3c6b4 Fix Parser.Range returning wrong offset for error highlights
The subsliceOffset method incorrectly computed offset as
len(p.data) - len(b), which only works when b is a suffix (tail) of
p.data. However, error highlights (ParserError.Highlight) are arbitrary
subslices from the middle of the input (e.g., b[0:1] from parseSimpleKey),
so their len has no relationship to their position.

This was a regression introduced in commit 3aaf147 (Remove unsafe package
usage) which replaced danger.SubsliceOffset (pointer arithmetic) with the
incorrect len-based approach.

Fix by using reflect.ValueOf().Pointer() to compute the actual byte
offset between slice data pointers, matching the approach already used
in errors.go:subsliceOffset.

Fixes #1047

Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
2026-04-12 12:40:14 +00:00
Thomas Pelletier f36a3ece9e Reduce marshal and unmarshal overhead (#1044)
* Reduce marshal and unmarshal overhead

Targeted optimizations to reduce performance overhead introduced by
recent feature additions and the unsafe removal.

Unmarshal:
- parseKeyval: access the node directly in the builder's slice to set
  Raw, bypassing NodeAt which triggers a GC write barrier for the
  nodes-pointer on every key-value expression.
- Iterator.Next: cache the *nodes slice dereference in a local variable
  to avoid repeated pointer-to-slice indirection in the hot loop.

Marshal:
- Guard shouldOmitZero calls with an inlineable options.omitzero check.
  shouldOmitZero has inlining cost 1145 (budget 80), so avoiding the
  function call when omitzero is not set removes per-field overhead.
- Inline the isNil check in encodeMap. isNil has inlining cost 93
  (budget 80), so expanding it at the single hot call site avoids
  per-map-entry function call overhead.

Update README benchmarks.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:08:39 +00:00
Thomas Pelletier 77f3862df4 Fix benchmark script replacing internal package imports (#1042)
* Fix benchmark script replacing internal package imports

The sed command in bench() was replacing all occurrences of the go-toml
module path, including sub-package imports like internal/assert. This
caused the BurntSushi/toml benchmark to fail because it tried to import
github.com/BurntSushi/toml/internal/assert which doesn't exist.

Fix by anchoring the sed pattern to only match the import path when
followed by a closing quote, preserving internal package imports.

Also add a guard in the benchstathtml Python script to give a clear
error instead of an IndexError when no benchmark results are available.

https://claude.ai/code/session_016JGASo49PeFSfCaDxvrGFE

* Update benchmark results in README

https://claude.ai/code/session_016JGASo49PeFSfCaDxvrGFE

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-23 22:00:18 -04:00
Thomas Pelletier 16b1ef5508 Fix parser error pointing to wrong line when last line has no trailing newline (#1041)
When parsing a key without '=' at EOF (e.g., "a = 1\nb = 2\nc"), the
error highlight was an empty slice, causing subsliceOffset to return 0
and the error to point at line 1 instead of line 3. Pass the consumed
key bytes as the highlight instead of the empty remainder.

Fixes #1032

https://claude.ai/code/session_01UWv8pyc8P1ktAPfHpveixj

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-23 21:34:12 -04:00
dependabot[bot] e14bde7c1d build(deps): bump docker/login-action from 3 to 4 (#1039)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 21:04:21 -04:00
dependabot[bot] 4b1ff01eb3 build(deps): bump docker/setup-buildx-action from 3 to 4 (#1040)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 21:04:11 -04:00
Thomas Pelletier 048a25f0f2 Go 1.26 (#1030)
* ci(release): drop unsupported windows/arm targets
2026-03-03 01:29:35 -05:00
22 changed files with 469 additions and 359 deletions
+1 -1
View File
@@ -15,6 +15,6 @@ jobs:
- name: Setup go
uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"
- name: Run tests with coverage
run: ./ci.sh coverage -d "${GITHUB_BASE_REF-HEAD}"
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Run Go versions compatibility test
run: |
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
- name: Setup go
uses: actions/setup-go@v6
with:
go-version: "1.24"
go-version: "1.26"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
+2 -2
View File
@@ -22,9 +22,9 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
fail-fast: false
matrix:
os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest', 'macos-14' ]
go: [ '1.24', '1.25' ]
go: [ '1.25', '1.26' ]
runs-on: ${{ matrix.os }}
name: ${{ matrix.go }}/${{ matrix.os }}
steps:
-3
View File
@@ -22,7 +22,6 @@ builds:
- linux_riscv64
- windows_amd64
- windows_arm64
- windows_arm
- darwin_amd64
- darwin_arm64
- id: tomljson
@@ -42,7 +41,6 @@ builds:
- linux_riscv64
- windows_amd64
- windows_arm64
- windows_arm
- darwin_amd64
- darwin_arm64
- id: jsontoml
@@ -62,7 +60,6 @@ builds:
- linux_arm
- windows_amd64
- windows_arm64
- windows_arm
- darwin_amd64
- darwin_arm64
universal_binaries:
+27 -27
View File
@@ -235,17 +235,17 @@ the AST level. See https://pkg.go.dev/github.com/pelletier/go-toml/v2/unstable.
Execution time speedup compared to other Go TOML libraries:
<table>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/HugoFrontMatter-2</td><td>1.9x</td><td>2.2x</td></tr>
<tr><td>Marshal/ReferenceFile/map-2</td><td>1.7x</td><td>2.1x</td></tr>
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.2x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.7x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.6x</td><td>2.7x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.6x</td><td>5.1x</td></tr>
</tbody>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/HugoFrontMatter-2</td><td>2.1x</td><td>2.0x</td></tr>
<tr><td>Marshal/ReferenceFile/map-2</td><td>2.0x</td><td>2.0x</td></tr>
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.3x</td><td>2.5x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>3.3x</td><td>2.8x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.9x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.8x</td><td>5.0x</td></tr>
</tbody>
</table>
<details><summary>See more</summary>
<p>The table above has the results of the most common use-cases. The table below
@@ -253,22 +253,22 @@ contains the results of all benchmarks, including unrealistic ones. It is
provided for completeness.</p>
<table>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/SimpleDocument/map-2</td><td>1.8x</td><td>2.7x</td></tr>
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.7x</td><td>3.8x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>3.8x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>5.6x</td><td>4.1x</td></tr>
<tr><td>UnmarshalDataset/example-2</td><td>3.0x</td><td>3.2x</td></tr>
<tr><td>UnmarshalDataset/code-2</td><td>2.3x</td><td>2.9x</td></tr>
<tr><td>UnmarshalDataset/twitter-2</td><td>2.6x</td><td>2.7x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.2x</td><td>2.3x</td></tr>
<tr><td>UnmarshalDataset/canada-2</td><td>1.8x</td><td>1.5x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>4.1x</td><td>2.9x</td></tr>
<tr><td>geomean</td><td>2.7x</td><td>2.8x</td></tr>
</tbody>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/SimpleDocument/map-2</td><td>2.0x</td><td>2.9x</td></tr>
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.5x</td><td>3.6x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>4.2x</td><td>3.4x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>5.9x</td><td>4.4x</td></tr>
<tr><td>UnmarshalDataset/example-2</td><td>3.2x</td><td>2.9x</td></tr>
<tr><td>UnmarshalDataset/code-2</td><td>2.4x</td><td>2.8x</td></tr>
<tr><td>UnmarshalDataset/twitter-2</td><td>2.7x</td><td>2.5x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.3x</td><td>2.3x</td></tr>
<tr><td>UnmarshalDataset/canada-2</td><td>1.9x</td><td>1.5x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>5.4x</td><td>3.0x</td></tr>
<tr><td>geomean</td><td>2.9x</td><td>2.8x</td></tr>
</tbody>
</table>
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
</details>
+6 -1
View File
@@ -147,7 +147,7 @@ bench() {
pushd "$dir"
if [ "${replace}" != "" ]; then
find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2|${replace}|g" {} \;
find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2\"|${replace}\"|g" {} \;
go get "${replace}"
fi
@@ -195,6 +195,11 @@ for line in reversed(lines[2:]):
"%.1fx" % (float(line[3])/v2), # v1
"%.1fx" % (float(line[7])/v2), # bs
])
if not results:
print("No benchmark results to display.", file=sys.stderr)
sys.exit(1)
# move geomean to the end
results.append(results[0])
del results[0]
+78 -93
View File
@@ -9,64 +9,60 @@ import (
"github.com/pelletier/go-toml/v2/unstable"
)
func parseInteger(b []byte) (int64, error) {
func parseInteger(b []byte, base int) (int64, error) {
if len(b) > 2 && b[0] == '0' {
switch b[1] {
case 'x':
return parseIntHex(b)
return parseIntHex(b, base)
case 'b':
return parseIntBin(b)
return parseIntBin(b, base)
case 'o':
return parseIntOct(b)
return parseIntOct(b, base)
default:
panic(fmt.Errorf("invalid base '%c', should have been checked by scanIntOrFloat", b[1]))
}
}
return parseIntDec(b)
return parseIntDec(b, base)
}
func parseLocalDate(b []byte) (LocalDate, error) {
// full-date = date-fullyear "-" date-month "-" date-mday
// date-fullyear = 4DIGIT
// date-month = 2DIGIT ; 01-12
// date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
func parseLocalDate(b []byte, base int) (LocalDate, error) {
var date LocalDate
if len(b) != 10 || b[4] != '-' || b[7] != '-' {
return date, unstable.NewParserError(b, "dates are expected to have the format YYYY-MM-DD")
return date, unstable.NewParserError(b, base, "dates are expected to have the format YYYY-MM-DD")
}
var err error
date.Year, err = parseDecimalDigits(b[0:4])
date.Year, err = parseDecimalDigits(b[0:4], base)
if err != nil {
return LocalDate{}, err
}
date.Month, err = parseDecimalDigits(b[5:7])
date.Month, err = parseDecimalDigits(b[5:7], base+5)
if err != nil {
return LocalDate{}, err
}
date.Day, err = parseDecimalDigits(b[8:10])
date.Day, err = parseDecimalDigits(b[8:10], base+8)
if err != nil {
return LocalDate{}, err
}
if !isValidDate(date.Year, date.Month, date.Day) {
return LocalDate{}, unstable.NewParserError(b, "impossible date")
return LocalDate{}, unstable.NewParserError(b, base, "impossible date")
}
return date, nil
}
func parseDecimalDigits(b []byte) (int, error) {
func parseDecimalDigits(b []byte, base int) (int, error) {
v := 0
for i, c := range b {
if c < '0' || c > '9' {
return 0, unstable.NewParserError(b[i:i+1], "expected digit (0-9)")
return 0, unstable.NewParserError(b[i:i+1], base+i, "expected digit (0-9)")
}
v *= 10
v += int(c - '0')
@@ -75,21 +71,18 @@ func parseDecimalDigits(b []byte) (int, error) {
return v, nil
}
func parseDateTime(b []byte) (time.Time, error) {
// offset-date-time = full-date time-delim full-time
// full-time = partial-time time-offset
// time-offset = "Z" / time-numoffset
// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
dt, b, err := parseLocalDateTime(b)
func parseDateTime(b []byte, base int) (time.Time, error) {
origLen := len(b)
dt, b, err := parseLocalDateTime(b, base)
if err != nil {
return time.Time{}, err
}
tzBase := base + origLen - len(b)
var zone *time.Location
if len(b) == 0 {
// parser should have checked that when assigning the date time node
panic("date time should have a timezone")
}
@@ -99,7 +92,7 @@ func parseDateTime(b []byte) (time.Time, error) {
} else {
const dateTimeByteLen = 6
if len(b) != dateTimeByteLen {
return time.Time{}, unstable.NewParserError(b, "invalid date-time timezone")
return time.Time{}, unstable.NewParserError(b, tzBase, "invalid date-time timezone")
}
var direction int
switch b[0] {
@@ -108,27 +101,27 @@ func parseDateTime(b []byte) (time.Time, error) {
case '+':
direction = +1
default:
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset character")
return time.Time{}, unstable.NewParserError(b[:1], tzBase, "invalid timezone offset character")
}
if b[3] != ':' {
return time.Time{}, unstable.NewParserError(b[3:4], "expected a : separator")
return time.Time{}, unstable.NewParserError(b[3:4], tzBase+3, "expected a : separator")
}
hours, err := parseDecimalDigits(b[1:3])
hours, err := parseDecimalDigits(b[1:3], tzBase+1)
if err != nil {
return time.Time{}, err
}
if hours > 23 {
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset hours")
return time.Time{}, unstable.NewParserError(b[:1], tzBase, "invalid timezone offset hours")
}
minutes, err := parseDecimalDigits(b[4:6])
minutes, err := parseDecimalDigits(b[4:6], tzBase+4)
if err != nil {
return time.Time{}, err
}
if minutes > 59 {
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset minutes")
return time.Time{}, unstable.NewParserError(b[:1], tzBase, "invalid timezone offset minutes")
}
seconds := direction * (hours*3600 + minutes*60)
@@ -141,7 +134,7 @@ func parseDateTime(b []byte) (time.Time, error) {
}
if len(b) > 0 {
return time.Time{}, unstable.NewParserError(b, "extra bytes at the end of the timezone")
return time.Time{}, unstable.NewParserError(b, tzBase, "extra bytes at the end of the timezone")
}
t := time.Date(
@@ -157,15 +150,15 @@ func parseDateTime(b []byte) (time.Time, error) {
return t, nil
}
func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
func parseLocalDateTime(b []byte, base int) (LocalDateTime, []byte, error) {
var dt LocalDateTime
const localDateTimeByteMinLen = 11
if len(b) < localDateTimeByteMinLen {
return dt, nil, unstable.NewParserError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
return dt, nil, unstable.NewParserError(b, base, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
}
date, err := parseLocalDate(b[:10])
date, err := parseLocalDate(b[:10], base)
if err != nil {
return dt, nil, err
}
@@ -173,10 +166,10 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
sep := b[10]
if sep != 'T' && sep != ' ' && sep != 't' {
return dt, nil, unstable.NewParserError(b[10:11], "datetime separator is expected to be T or a space")
return dt, nil, unstable.NewParserError(b[10:11], base+10, "datetime separator is expected to be T or a space")
}
t, rest, err := parseLocalTime(b[11:])
t, rest, err := parseLocalTime(b[11:], base+11)
if err != nil {
return dt, nil, err
}
@@ -188,53 +181,53 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
// parseLocalTime is a bit different because it also returns the remaining
// []byte that is didn't need. This is to allow parseDateTime to parse those
// remaining bytes as a timezone.
func parseLocalTime(b []byte) (LocalTime, []byte, error) {
func parseLocalTime(b []byte, base int) (LocalTime, []byte, error) {
var (
nspow = [10]int{0, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1e0}
t LocalTime
)
// check if b matches to have expected format HH:MM:SS[.NNNNNN]
const localTimeByteLen = 8
if len(b) < localTimeByteLen {
return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
return t, nil, unstable.NewParserError(b, base, "times are expected to have the format HH:MM:SS[.NNNNNN]")
}
var err error
t.Hour, err = parseDecimalDigits(b[0:2])
t.Hour, err = parseDecimalDigits(b[0:2], base)
if err != nil {
return t, nil, err
}
if t.Hour > 23 {
return t, nil, unstable.NewParserError(b[0:2], "hour cannot be greater 23")
return t, nil, unstable.NewParserError(b[0:2], base, "hour cannot be greater 23")
}
if b[2] != ':' {
return t, nil, unstable.NewParserError(b[2:3], "expecting colon between hours and minutes")
return t, nil, unstable.NewParserError(b[2:3], base+2, "expecting colon between hours and minutes")
}
t.Minute, err = parseDecimalDigits(b[3:5])
t.Minute, err = parseDecimalDigits(b[3:5], base+3)
if err != nil {
return t, nil, err
}
if t.Minute > 59 {
return t, nil, unstable.NewParserError(b[3:5], "minutes cannot be greater 59")
return t, nil, unstable.NewParserError(b[3:5], base+3, "minutes cannot be greater 59")
}
if b[5] != ':' {
return t, nil, unstable.NewParserError(b[5:6], "expecting colon between minutes and seconds")
return t, nil, unstable.NewParserError(b[5:6], base+5, "expecting colon between minutes and seconds")
}
t.Second, err = parseDecimalDigits(b[6:8])
t.Second, err = parseDecimalDigits(b[6:8], base+6)
if err != nil {
return t, nil, err
}
if t.Second > 59 {
return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater than 59")
return t, nil, unstable.NewParserError(b[6:8], base+6, "seconds cannot be greater than 59")
}
b = b[8:]
base += 8
if len(b) >= 1 && b[0] == '.' {
frac := 0
@@ -244,7 +237,7 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
for i, c := range b[1:] {
if !isDigit(c) {
if i == 0 {
return t, nil, unstable.NewParserError(b[0:1], "need at least one digit after fraction point")
return t, nil, unstable.NewParserError(b[0:1], base, "need at least one digit after fraction point")
}
break
}
@@ -252,13 +245,6 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
const maxFracPrecision = 9
if i >= maxFracPrecision {
// go-toml allows decoding fractional seconds
// beyond the supported precision of 9
// digits. It truncates the fractional component
// to the supported precision and ignores the
// remaining digits.
//
// https://github.com/pelletier/go-toml/discussions/707
continue
}
@@ -268,7 +254,7 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
}
if precision == 0 {
return t, nil, unstable.NewParserError(b[:1], "nanoseconds need at least one digit")
return t, nil, unstable.NewParserError(b[:1], base, "nanoseconds need at least one digit")
}
t.Nanosecond = frac * nspow[precision]
@@ -279,35 +265,35 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
return t, b, nil
}
func parseFloat(b []byte) (float64, error) {
func parseFloat(b []byte, base int) (float64, error) {
if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' {
return math.NaN(), nil
}
cleaned, err := checkAndRemoveUnderscoresFloats(b)
cleaned, err := checkAndRemoveUnderscoresFloats(b, base)
if err != nil {
return 0, err
}
if cleaned[0] == '.' {
return 0, unstable.NewParserError(b, "float cannot start with a dot")
return 0, unstable.NewParserError(b, base, "float cannot start with a dot")
}
if cleaned[len(cleaned)-1] == '.' {
return 0, unstable.NewParserError(b, "float cannot end with a dot")
return 0, unstable.NewParserError(b, base, "float cannot end with a dot")
}
dotAlreadySeen := false
for i, c := range cleaned {
if c == '.' {
if dotAlreadySeen {
return 0, unstable.NewParserError(b[i:i+1], "float can have at most one decimal point")
return 0, unstable.NewParserError(b[i:i+1], base+i, "float can have at most one decimal point")
}
if !isDigit(cleaned[i-1]) {
return 0, unstable.NewParserError(b[i-1:i+1], "float decimal point must be preceded by a digit")
return 0, unstable.NewParserError(b[i-1:i+1], base+i-1, "float decimal point must be preceded by a digit")
}
if !isDigit(cleaned[i+1]) {
return 0, unstable.NewParserError(b[i:i+2], "float decimal point must be followed by a digit")
return 0, unstable.NewParserError(b[i:i+2], base+i, "float decimal point must be followed by a digit")
}
dotAlreadySeen = true
}
@@ -318,54 +304,54 @@ func parseFloat(b []byte) (float64, error) {
start = 1
}
if cleaned[start] == '0' && len(cleaned) > start+1 && isDigit(cleaned[start+1]) {
return 0, unstable.NewParserError(b, "float integer part cannot have leading zeroes")
return 0, unstable.NewParserError(b, base, "float integer part cannot have leading zeroes")
}
f, err := strconv.ParseFloat(string(cleaned), 64)
if err != nil {
return 0, unstable.NewParserError(b, "unable to parse float: %w", err)
return 0, unstable.NewParserError(b, base, "unable to parse float: %w", err)
}
return f, nil
}
func parseIntHex(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
func parseIntHex(b []byte, base int) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:], base+2)
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(string(cleaned), 16, 64)
if err != nil {
return 0, unstable.NewParserError(b, "couldn't parse hexadecimal number: %w", err)
return 0, unstable.NewParserError(b, base, "couldn't parse hexadecimal number: %w", err)
}
return i, nil
}
func parseIntOct(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
func parseIntOct(b []byte, base int) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:], base+2)
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(string(cleaned), 8, 64)
if err != nil {
return 0, unstable.NewParserError(b, "couldn't parse octal number: %w", err)
return 0, unstable.NewParserError(b, base, "couldn't parse octal number: %w", err)
}
return i, nil
}
func parseIntBin(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
func parseIntBin(b []byte, base int) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:], base+2)
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(string(cleaned), 2, 64)
if err != nil {
return 0, unstable.NewParserError(b, "couldn't parse binary number: %w", err)
return 0, unstable.NewParserError(b, base, "couldn't parse binary number: %w", err)
}
return i, nil
@@ -375,8 +361,8 @@ func isSign(b byte) bool {
return b == '+' || b == '-'
}
func parseIntDec(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b)
func parseIntDec(b []byte, base int) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b, base)
if err != nil {
return 0, err
}
@@ -388,18 +374,18 @@ func parseIntDec(b []byte) (int64, error) {
}
if len(cleaned) > startIdx+1 && cleaned[startIdx] == '0' {
return 0, unstable.NewParserError(b, "leading zero not allowed on decimal number")
return 0, unstable.NewParserError(b, base, "leading zero not allowed on decimal number")
}
i, err := strconv.ParseInt(string(cleaned), 10, 64)
if err != nil {
return 0, unstable.NewParserError(b, "couldn't parse decimal number: %w", err)
return 0, unstable.NewParserError(b, base, "couldn't parse decimal number: %w", err)
}
return i, nil
}
func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
func checkAndRemoveUnderscoresIntegers(b []byte, base int) ([]byte, error) {
start := 0
if b[start] == '+' || b[start] == '-' {
start++
@@ -410,11 +396,11 @@ func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
}
if b[start] == '_' {
return nil, unstable.NewParserError(b[start:start+1], "number cannot start with underscore")
return nil, unstable.NewParserError(b[start:start+1], base+start, "number cannot start with underscore")
}
if b[len(b)-1] == '_' {
return nil, unstable.NewParserError(b[len(b)-1:], "number cannot end with underscore")
return nil, unstable.NewParserError(b[len(b)-1:], base+len(b)-1, "number cannot end with underscore")
}
// fast path
@@ -436,7 +422,7 @@ func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
c := b[i]
if c == '_' {
if !before {
return nil, unstable.NewParserError(b[i-1:i+1], "number must have at least one digit between underscores")
return nil, unstable.NewParserError(b[i-1:i+1], base+i-1, "number must have at least one digit between underscores")
}
before = false
} else {
@@ -448,13 +434,13 @@ func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
return cleaned, nil
}
func checkAndRemoveUnderscoresFloats(b []byte) ([]byte, error) {
func checkAndRemoveUnderscoresFloats(b []byte, base int) ([]byte, error) {
if b[0] == '_' {
return nil, unstable.NewParserError(b[0:1], "number cannot start with underscore")
return nil, unstable.NewParserError(b[0:1], base, "number cannot start with underscore")
}
if b[len(b)-1] == '_' {
return nil, unstable.NewParserError(b[len(b)-1:], "number cannot end with underscore")
return nil, unstable.NewParserError(b[len(b)-1:], base+len(b)-1, "number cannot end with underscore")
}
// fast path
@@ -477,27 +463,26 @@ func checkAndRemoveUnderscoresFloats(b []byte) ([]byte, error) {
switch c {
case '_':
if !before {
return nil, unstable.NewParserError(b[i-1:i+1], "number must have at least one digit between underscores")
return nil, unstable.NewParserError(b[i-1:i+1], base+i-1, "number must have at least one digit between underscores")
}
if i < len(b)-1 && (b[i+1] == 'e' || b[i+1] == 'E') {
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore before exponent")
return nil, unstable.NewParserError(b[i+1:i+2], base+i+1, "cannot have underscore before exponent")
}
before = false
case '+', '-':
// signed exponents
cleaned = append(cleaned, c)
before = false
case 'e', 'E':
if i < len(b)-1 && b[i+1] == '_' {
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore after exponent")
return nil, unstable.NewParserError(b[i+1:i+2], base+i+1, "cannot have underscore after exponent")
}
cleaned = append(cleaned, c)
case '.':
if i < len(b)-1 && b[i+1] == '_' {
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore after decimal point")
return nil, unstable.NewParserError(b[i+1:i+2], base+i+1, "cannot have underscore after decimal point")
}
if i > 0 && b[i-1] == '_' {
return nil, unstable.NewParserError(b[i-1:i], "cannot have underscore before decimal point")
return nil, unstable.NewParserError(b[i-1:i], base+i-1, "cannot have underscore before decimal point")
}
cleaned = append(cleaned, c)
default:
+1 -21
View File
@@ -2,7 +2,6 @@ package toml
import (
"fmt"
"reflect"
"strconv"
"strings"
@@ -100,7 +99,7 @@ func (e *DecodeError) Key() Key {
//
//nolint:funlen
func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
offset := subsliceOffset(document, de.Highlight)
offset := de.Offset
errMessage := de.Error()
errLine, errColumn := positionAtEnd(document[:offset])
@@ -262,22 +261,3 @@ func positionAtEnd(b []byte) (row int, column int) {
return row, column
}
// subsliceOffset returns the byte offset of subslice within data.
// subslice must share the same backing array as data.
func subsliceOffset(data []byte, subslice []byte) int {
if len(subslice) == 0 {
return 0
}
// Use reflect to get the data pointers of both slices.
// This is safe because we're only reading the pointer values for comparison.
dataPtr := reflect.ValueOf(data).Pointer()
subPtr := reflect.ValueOf(subslice).Pointer()
offset := int(subPtr - dataPtr)
if offset < 0 || offset > len(data) {
panic("subslice is not within data")
}
return offset
}
+101
View File
@@ -171,6 +171,7 @@ line 5`,
err := wrapDecodeError(doc, &unstable.ParserError{
Highlight: hl,
Offset: start,
Message: e.msg,
})
@@ -259,6 +260,12 @@ func TestDecodeError_Position(t *testing.T) {
expectedRow: 3,
minCol: 5,
},
{
name: "missing equals on last line without trailing newline",
doc: "a = 1\nb = 2\nc",
expectedRow: 3,
minCol: 1,
},
}
for _, e := range examples {
@@ -280,6 +287,100 @@ func TestDecodeError_Position(t *testing.T) {
}
}
func TestDecodeError_PositionAfterComments(t *testing.T) {
examples := []struct {
name string
doc string
expectedRow int
expectedCol int
errContains string
}{
{
name: "invalid key after comment",
doc: "# comment\n= \"value\"",
expectedRow: 2,
expectedCol: 1,
errContains: "invalid character at start of key",
},
{
name: "invalid key after multiple comments",
doc: "# line 1\n# line 2\n= \"value\"",
expectedRow: 3,
expectedCol: 1,
errContains: "invalid character at start of key",
},
{
name: "invalid key after valid assignment and comment",
doc: "a = 1\n# comment\n= \"value\"",
expectedRow: 3,
expectedCol: 1,
errContains: "invalid character at start of key",
},
{
name: "invalid key on first line",
doc: "= \"value\"",
expectedRow: 1,
expectedCol: 1,
errContains: "invalid character at start of key",
},
{
name: "invalid key with leading whitespace",
doc: "# comment\n = \"value\"",
expectedRow: 2,
expectedCol: 3,
errContains: "invalid character at start of key",
},
}
for _, e := range examples {
t.Run(e.name, func(t *testing.T) {
var v map[string]interface{}
err := Unmarshal([]byte(e.doc), &v)
if err == nil {
t.Fatal("expected an error")
}
var derr *DecodeError
if !errors.As(err, &derr) {
t.Fatalf("expected DecodeError, got %T: %v", err, err)
}
row, col := derr.Position()
if row != e.expectedRow {
t.Errorf("row: got %d, want %d (error: %s)", row, e.expectedRow, derr.String())
}
if col != e.expectedCol {
t.Errorf("col: got %d, want %d (error: %s)", col, e.expectedCol, derr.String())
}
if !strings.Contains(derr.Error(), e.errContains) {
t.Errorf("error %q does not contain %q", derr.Error(), e.errContains)
}
})
}
}
func TestDecodeError_HumanStringAfterComments(t *testing.T) {
doc := "# comment\n= \"value\""
var v map[string]interface{}
err := Unmarshal([]byte(doc), &v)
if err == nil {
t.Fatal("expected an error")
}
var derr *DecodeError
if !errors.As(err, &derr) {
t.Fatalf("expected DecodeError, got %T: %v", err, err)
}
human := derr.String()
if !strings.Contains(human, "= \"value\"") {
t.Errorf("human-readable error should show the offending line, got:\n%s", human)
}
if !strings.Contains(human, "2|") {
t.Errorf("human-readable error should reference line 2, got:\n%s", human)
}
}
func TestStrictErrorUnwrap(t *testing.T) {
fo := bytes.NewBufferString(`
Missing = 1
+12 -16
View File
@@ -24,61 +24,57 @@ import (
// 0x9 => tab, ok
// 0xA - 0x1F => invalid
// 0x7F => invalid
func Utf8TomlValidAlreadyEscaped(p []byte) []byte {
func Utf8TomlValidAlreadyEscaped(p []byte) int {
consumed := 0
// Fast path. Check for and skip 8 bytes of ASCII characters per iteration.
for len(p) >= 8 {
// Combining two 32 bit loads allows the same code to be used
// for 32 and 64 bit platforms.
// The compiler can generate a 32bit load for first32 and second32
// on many platforms. See test/codegen/memcombine.go.
first32 := uint32(p[0]) | uint32(p[1])<<8 | uint32(p[2])<<16 | uint32(p[3])<<24
second32 := uint32(p[4]) | uint32(p[5])<<8 | uint32(p[6])<<16 | uint32(p[7])<<24
if (first32|second32)&0x80808080 != 0 {
// Found a non ASCII byte (>= RuneSelf).
break
}
for i, b := range p[:8] {
if InvalidASCII(b) {
return p[i : i+1]
return consumed + i
}
}
p = p[8:]
consumed += 8
}
n := len(p)
for i := 0; i < n; {
pi := p[i]
if pi < utf8.RuneSelf {
if InvalidASCII(pi) {
return p[i : i+1]
return consumed + i
}
i++
continue
}
x := first[pi]
if x == xx {
// Illegal starter byte.
return p[i : i+1]
return consumed + i
}
size := int(x & 7)
if i+size > n {
// Short or invalid.
return p[i:n]
return consumed + i
}
accept := acceptRanges[x>>4]
if c := p[i+1]; c < accept.lo || accept.hi < c {
return p[i : i+2]
return consumed + i
} else if size == 2 { //revive:disable:empty-block
} else if c := p[i+2]; c < locb || hicb < c {
return p[i : i+3]
return consumed + i
} else if size == 3 { //revive:disable:empty-block
} else if c := p[i+3]; c < locb || hicb < c {
return p[i : i+4]
return consumed + i
}
i += size
}
return nil
return -1
}
// Utf8ValidNext returns the size of the next rune if valid, 0 otherwise.
+5 -5
View File
@@ -32,7 +32,7 @@ func (d LocalDate) MarshalText() ([]byte, error) {
// UnmarshalText parses b using RFC 3339 to fill d.
func (d *LocalDate) UnmarshalText(b []byte) error {
res, err := parseLocalDate(b)
res, err := parseLocalDate(b, 0)
if err != nil {
return err
}
@@ -75,9 +75,9 @@ func (d LocalTime) MarshalText() ([]byte, error) {
// UnmarshalText parses b using RFC 3339 to fill d.
func (d *LocalTime) UnmarshalText(b []byte) error {
res, left, err := parseLocalTime(b)
res, left, err := parseLocalTime(b, 0)
if err == nil && len(left) != 0 {
err = unstable.NewParserError(left, "extra characters")
err = unstable.NewParserError(left, len(b)-len(left), "extra characters")
}
if err != nil {
return err
@@ -109,9 +109,9 @@ func (d LocalDateTime) MarshalText() ([]byte, error) {
// UnmarshalText parses b using RFC 3339 to fill d.
func (d *LocalDateTime) UnmarshalText(data []byte) error {
res, left, err := parseLocalDateTime(data)
res, left, err := parseLocalDateTime(data, 0)
if err == nil && len(left) != 0 {
err = unstable.NewParserError(left, "extra characters")
err = unstable.NewParserError(left, len(data)-len(left), "extra characters")
}
if err != nil {
return err
+12 -9
View File
@@ -704,15 +704,18 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte
for iter.Next() {
v := iter.Value()
if isNil(v) {
// For nil pointers, convert to zero value of the element type.
// This allows round-trip marshaling of maps with nil pointer values.
// For nil interfaces and nil maps, skip since we can't derive a type.
if v.Kind() == reflect.Ptr {
// Handle nil values: convert nil pointers to zero value,
// skip nil interfaces and nil maps.
switch v.Kind() {
case reflect.Ptr:
if v.IsNil() {
v = reflect.Zero(v.Type().Elem())
} else {
}
case reflect.Interface, reflect.Map:
if v.IsNil() {
continue
}
default:
}
k, err := enc.keyToString(iter.Key())
@@ -936,7 +939,7 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
if shouldOmitEmpty(kv.Options, kv.Value) {
continue
}
if shouldOmitZero(kv.Options, kv.Value) {
if kv.Options.omitzero && shouldOmitZero(kv.Options, kv.Value) {
continue
}
hasNonEmptyKV = true
@@ -958,7 +961,7 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
if shouldOmitEmpty(table.Options, table.Value) {
continue
}
if shouldOmitZero(table.Options, table.Value) {
if table.Options.omitzero && shouldOmitZero(table.Options, table.Value) {
continue
}
if first {
@@ -995,7 +998,7 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte
if shouldOmitEmpty(kv.Options, kv.Value) {
continue
}
if shouldOmitZero(kv.Options, kv.Value) {
if kv.Options.omitzero && shouldOmitZero(kv.Options, kv.Value) {
continue
}
+8 -6
View File
@@ -54,10 +54,12 @@ func (s *strict) MissingTable(node *unstable.Node) {
return
}
loc, offset := s.keyLocation(node)
s.missing = append(s.missing, unstable.ParserError{
Highlight: s.keyLocation(node),
Highlight: loc,
Message: "missing table",
Key: s.key.Key(),
Offset: offset,
})
}
@@ -66,10 +68,12 @@ func (s *strict) MissingField(node *unstable.Node) {
return
}
loc, offset := s.keyLocation(node)
s.missing = append(s.missing, unstable.ParserError{
Highlight: s.keyLocation(node),
Highlight: loc,
Message: "missing field",
Key: s.key.Key(),
Offset: offset,
})
}
@@ -90,7 +94,7 @@ func (s *strict) Error(doc []byte) error {
return err
}
func (s *strict) keyLocation(node *unstable.Node) []byte {
func (s *strict) keyLocation(node *unstable.Node) ([]byte, int) {
k := node.Key()
hasOne := k.Next()
@@ -98,7 +102,6 @@ func (s *strict) keyLocation(node *unstable.Node) []byte {
panic("should not be called with empty key")
}
// Get the range from the first key to the last key.
firstRaw := k.Node().Raw
lastRaw := firstRaw
@@ -106,9 +109,8 @@ func (s *strict) keyLocation(node *unstable.Node) []byte {
lastRaw = k.Node().Raw
}
// Compute the slice from the document using the ranges.
start := firstRaw.Offset
end := lastRaw.Offset + lastRaw.Length
return s.doc[start:end]
return s.doc[start:end], int(start)
}
+5 -4
View File
@@ -9,7 +9,7 @@ YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Go versions to test (1.11 through 1.25)
# Go versions to test (1.11 through 1.26)
GO_VERSIONS=(
"1.11"
"1.12"
@@ -26,6 +26,7 @@ GO_VERSIONS=(
"1.23"
"1.24"
"1.25"
"1.26"
)
# Default values
@@ -64,7 +65,7 @@ EXAMPLES:
$0 # Test all Go versions in parallel
$0 --sequential # Test all Go versions sequentially
$0 1.21 1.22 1.23 # Test specific versions
$0 --verbose --output ./results 1.24 1.25 # Verbose output to custom directory
$0 --verbose --output ./results 1.25 1.26 # Verbose output to custom directory
EXIT CODES:
0 Recent Go versions pass (good compatibility)
@@ -136,8 +137,8 @@ fi
# Validate Go versions
for version in "${GO_VERSIONS[@]}"; do
if ! [[ "$version" =~ ^1\.(1[1-9]|2[0-5])$ ]]; then
log_error "Invalid Go version: $version. Supported versions: 1.11-1.25"
if ! [[ "$version" =~ ^1\.(1[1-9]|2[0-6])$ ]]; then
log_error "Invalid Go version: $version. Supported versions: 1.11-1.26"
exit 1
fi
done
+20 -20
View File
@@ -625,7 +625,7 @@ func (d *decoder) handleTable(key unstable.Iterator, v reflect.Value) (reflect.V
}
}
}
return reflect.Value{}, unstable.NewParserError(key.Node().Data, "cannot store a table in a slice")
return reflect.Value{}, unstable.NewParserError(key.Node().Data, int(key.Node().Raw.Offset), "cannot store a table in a slice")
}
if key.Next() {
// Still scoping the key
@@ -748,7 +748,7 @@ func (d *decoder) tryTextUnmarshaler(node *unstable.Node, v reflect.Value) (bool
if v.CanAddr() && v.Addr().Type().Implements(textUnmarshalerType) {
err := v.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText(node.Data)
if err != nil {
return false, unstable.NewParserError(d.p.Raw(node.Raw), "%w", err)
return false, unstable.NewParserError(d.p.Raw(node.Raw), int(node.Raw.Offset), "%w", err)
}
return true, nil
@@ -896,7 +896,7 @@ func (d *decoder) unmarshalInlineTable(itable *unstable.Node, v reflect.Value) e
}
return d.unmarshalInlineTable(itable, elem)
default:
return unstable.NewParserError(d.p.Raw(itable.Raw), "cannot store inline table in Go type %s", v.Kind())
return unstable.NewParserError(d.p.Raw(itable.Raw), int(itable.Raw.Offset), "cannot store inline table in Go type %s", v.Kind())
}
it := itable.Children()
@@ -916,26 +916,26 @@ func (d *decoder) unmarshalInlineTable(itable *unstable.Node, v reflect.Value) e
}
func (d *decoder) unmarshalDateTime(value *unstable.Node, v reflect.Value) error {
dt, err := parseDateTime(value.Data)
dt, err := parseDateTime(value.Data, int(value.Raw.Offset))
if err != nil {
return err
}
if v.Kind() != reflect.Interface && v.Type() != timeType {
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("datetime", v.Type()))
return unstable.NewParserError(d.p.Raw(value.Raw), int(value.Raw.Offset), "%s", d.typeMismatchString("datetime", v.Type()))
}
v.Set(reflect.ValueOf(dt))
return nil
}
func (d *decoder) unmarshalLocalDate(value *unstable.Node, v reflect.Value) error {
ld, err := parseLocalDate(value.Data)
ld, err := parseLocalDate(value.Data, int(value.Raw.Offset))
if err != nil {
return err
}
if v.Kind() != reflect.Interface && v.Type() != timeType {
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("local date", v.Type()))
return unstable.NewParserError(d.p.Raw(value.Raw), int(value.Raw.Offset), "%s", d.typeMismatchString("local date", v.Type()))
}
if v.Type() == timeType {
v.Set(reflect.ValueOf(ld.AsTime(time.Local)))
@@ -946,34 +946,34 @@ func (d *decoder) unmarshalLocalDate(value *unstable.Node, v reflect.Value) erro
}
func (d *decoder) unmarshalLocalTime(value *unstable.Node, v reflect.Value) error {
lt, rest, err := parseLocalTime(value.Data)
lt, rest, err := parseLocalTime(value.Data, int(value.Raw.Offset))
if err != nil {
return err
}
if len(rest) > 0 {
return unstable.NewParserError(rest, "extra characters at the end of a local time")
return unstable.NewParserError(rest, int(value.Raw.Offset)+len(value.Data)-len(rest), "extra characters at the end of a local time")
}
if v.Kind() != reflect.Interface {
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("local time", v.Type()))
return unstable.NewParserError(d.p.Raw(value.Raw), int(value.Raw.Offset), "%s", d.typeMismatchString("local time", v.Type()))
}
v.Set(reflect.ValueOf(lt))
return nil
}
func (d *decoder) unmarshalLocalDateTime(value *unstable.Node, v reflect.Value) error {
ldt, rest, err := parseLocalDateTime(value.Data)
ldt, rest, err := parseLocalDateTime(value.Data, int(value.Raw.Offset))
if err != nil {
return err
}
if len(rest) > 0 {
return unstable.NewParserError(rest, "extra characters at the end of a local date time")
return unstable.NewParserError(rest, int(value.Raw.Offset)+len(value.Data)-len(rest), "extra characters at the end of a local date time")
}
if v.Kind() != reflect.Interface && v.Type() != timeType {
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("local datetime", v.Type()))
return unstable.NewParserError(d.p.Raw(value.Raw), int(value.Raw.Offset), "%s", d.typeMismatchString("local datetime", v.Type()))
}
if v.Type() == timeType {
v.Set(reflect.ValueOf(ldt.AsTime(time.Local)))
@@ -992,14 +992,14 @@ func (d *decoder) unmarshalBool(value *unstable.Node, v reflect.Value) error {
case reflect.Interface:
v.Set(reflect.ValueOf(b))
default:
return unstable.NewParserError(value.Data, "cannot assign boolean to a %t", b)
return unstable.NewParserError(value.Data, int(value.Raw.Offset), "cannot assign boolean to a %t", b)
}
return nil
}
func (d *decoder) unmarshalFloat(value *unstable.Node, v reflect.Value) error {
f, err := parseFloat(value.Data)
f, err := parseFloat(value.Data, int(value.Raw.Offset))
if err != nil {
return err
}
@@ -1009,13 +1009,13 @@ func (d *decoder) unmarshalFloat(value *unstable.Node, v reflect.Value) error {
v.SetFloat(f)
case reflect.Float32:
if f > math.MaxFloat32 {
return unstable.NewParserError(value.Data, "number %f does not fit in a float32", f)
return unstable.NewParserError(value.Data, int(value.Raw.Offset), "number %f does not fit in a float32", f)
}
v.SetFloat(f)
case reflect.Interface:
v.Set(reflect.ValueOf(f))
default:
return unstable.NewParserError(value.Data, "float cannot be assigned to %s", v.Kind())
return unstable.NewParserError(value.Data, int(value.Raw.Offset), "float cannot be assigned to %s", v.Kind())
}
return nil
@@ -1048,7 +1048,7 @@ func (d *decoder) unmarshalInteger(value *unstable.Node, v reflect.Value) error
return d.unmarshalFloat(value, v)
}
i, err := parseInteger(value.Data)
i, err := parseInteger(value.Data, int(value.Raw.Offset))
if err != nil {
return err
}
@@ -1116,7 +1116,7 @@ func (d *decoder) unmarshalInteger(value *unstable.Node, v reflect.Value) error
case reflect.Interface:
r = reflect.ValueOf(i)
default:
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("integer", v.Type()))
return unstable.NewParserError(d.p.Raw(value.Raw), int(value.Raw.Offset), "%s", d.typeMismatchString("integer", v.Type()))
}
if !r.Type().AssignableTo(v.Type()) {
@@ -1135,7 +1135,7 @@ func (d *decoder) unmarshalString(value *unstable.Node, v reflect.Value) error {
case reflect.Interface:
v.Set(reflect.ValueOf(string(value.Data)))
default:
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("string", v.Type()))
return unstable.NewParserError(d.p.Raw(value.Raw), int(value.Raw.Offset), "%s", d.typeMismatchString("string", v.Type()))
}
return nil
+7 -3
View File
@@ -28,12 +28,16 @@ func (c *Iterator) Next() bool {
if c.nodes == nil {
return false
}
nodes := *c.nodes
if !c.started {
c.started = true
} else if c.idx >= 0 {
c.idx = (*c.nodes)[c.idx].next
} else {
idx := c.idx
if idx >= 0 && int(idx) < len(nodes) {
c.idx = nodes[idx].next
}
}
return c.idx >= 0 && int(c.idx) < len(*c.nodes)
return c.idx >= 0 && int(c.idx) < len(nodes)
}
// IsLast returns true if the current node of the iterator is the last
+1 -1
View File
@@ -35,7 +35,7 @@ func BenchmarkScanComments(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _ = scanComment(input)
_, _, _ = scanComment(input, 0)
}
})
}
+62 -72
View File
@@ -16,6 +16,7 @@ type ParserError struct {
Highlight []byte
Message string
Key []string // optional
Offset int
}
// Error is the implementation of the error interface.
@@ -27,9 +28,10 @@ func (e *ParserError) Error() string {
//
// Warning: Highlight needs to be a subslice of Parser.data, so only slices
// returned by Parser.Raw are valid candidates.
func NewParserError(highlight []byte, format string, args ...interface{}) error {
func NewParserError(highlight []byte, offset int, format string, args ...interface{}) error {
return &ParserError{
Highlight: highlight,
Offset: offset,
Message: fmt.Errorf(format, args...).Error(),
}
}
@@ -64,14 +66,8 @@ func (p *Parser) Data() []byte {
return p.data
}
// Range returns a range description that corresponds to a given slice of the
// input. If the argument is not a subslice of the parser input, this function
// panics.
func (p *Parser) Range(b []byte) Range {
return Range{
Offset: uint32(p.subsliceOffset(b)), //nolint:gosec // TOML documents are small
Length: uint32(len(b)), //nolint:gosec // TOML documents are small
}
func (p *Parser) offsetOf(b []byte) int {
return len(p.data) - len(b)
}
// rangeOfToken computes the Range of a token given the remaining bytes after the token.
@@ -82,13 +78,6 @@ func (p *Parser) rangeOfToken(token, rest []byte) Range {
return Range{Offset: uint32(offset), Length: uint32(len(token))} //nolint:gosec // TOML documents are small
}
// subsliceOffset returns the byte offset of subslice b within p.data.
// b must be a suffix (tail) of p.data.
func (p *Parser) subsliceOffset(b []byte) int {
// b is a suffix of p.data, so its offset is len(p.data) - len(b)
return len(p.data) - len(b)
}
// Raw returns the slice corresponding to the bytes in the given range.
func (p *Parser) Raw(raw Range) []byte {
return p.data[raw.Offset : raw.Offset+raw.Length]
@@ -198,16 +187,16 @@ func (p *Parser) parseNewline(b []byte) ([]byte, error) {
}
if b[0] == '\r' {
_, rest, err := scanWindowsNewline(b)
_, rest, err := scanWindowsNewline(b, p.offsetOf(b))
return rest, err
}
return nil, NewParserError(b[0:1], "expected newline but got %#U", b[0])
return nil, NewParserError(b[0:1], p.offsetOf(b), "expected newline but got %#U", b[0])
}
func (p *Parser) parseComment(b []byte) (reference, []byte, error) {
ref := invalidReference
data, rest, err := scanComment(b)
data, rest, err := scanComment(b, p.offsetOf(b))
if p.KeepComments && err == nil {
ref = p.builder.Push(Node{
Kind: Comment,
@@ -291,12 +280,12 @@ func (p *Parser) parseArrayTable(b []byte) (reference, []byte, error) {
p.builder.AttachChild(ref, k)
b = p.parseWhitespace(b)
b, err = expect(']', b)
b, err = expect(']', b, p.offsetOf(b))
if err != nil {
return ref, nil, err
}
b, err = expect(']', b)
b, err = expect(']', b, p.offsetOf(b))
return ref, b, err
}
@@ -321,7 +310,7 @@ func (p *Parser) parseStdTable(b []byte) (reference, []byte, error) {
b = p.parseWhitespace(b)
b, err = expect(']', b)
b, err = expect(']', b, p.offsetOf(b))
return ref, b, err
}
@@ -345,10 +334,10 @@ func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) {
b = p.parseWhitespace(b)
if len(b) == 0 {
return invalidReference, nil, NewParserError(b, "expected = after a key, but the document ends there")
return invalidReference, nil, NewParserError(startB[:len(startB)-len(b)], p.offsetOf(startB), "expected = after a key, but the document ends there")
}
b, err = expect('=', b)
b, err = expect('=', b, p.offsetOf(b))
if err != nil {
return invalidReference, nil, err
}
@@ -363,9 +352,10 @@ func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) {
p.builder.Chain(valRef, key)
p.builder.AttachChild(ref, valRef)
// Set Raw to span the entire key-value expression
node := p.builder.NodeAt(ref)
node.Raw = p.rangeOfToken(startB[:len(startB)-len(b)], b)
// Set Raw to span the entire key-value expression.
// Access the node directly in the slice to avoid the write barrier
// that NodeAt's nodes-pointer setup would trigger.
p.builder.tree.nodes[ref].Raw = p.rangeOfToken(startB[:len(startB)-len(b)], b)
return ref, b, err
}
@@ -376,7 +366,7 @@ func (p *Parser) parseVal(b []byte) (reference, []byte, error) {
ref := invalidReference
if len(b) == 0 {
return ref, nil, NewParserError(b, "expected value, not eof")
return ref, nil, NewParserError(b, p.offsetOf(b), "expected value, not eof")
}
var err error
@@ -421,7 +411,7 @@ func (p *Parser) parseVal(b []byte) (reference, []byte, error) {
return ref, b, err
case 't':
if !scanFollowsTrue(b) {
return ref, nil, NewParserError(atmost(b, 4), "expected 'true'")
return ref, nil, NewParserError(atmost(b, 4), p.offsetOf(b), "expected 'true'")
}
ref = p.builder.Push(Node{
@@ -432,7 +422,7 @@ func (p *Parser) parseVal(b []byte) (reference, []byte, error) {
return ref, b[4:], nil
case 'f':
if !scanFollowsFalse(b) {
return ref, nil, NewParserError(atmost(b, 5), "expected 'false'")
return ref, nil, NewParserError(atmost(b, 5), p.offsetOf(b), "expected 'false'")
}
ref = p.builder.Push(Node{
@@ -459,7 +449,7 @@ func atmost(b []byte, n int) []byte {
}
func (p *Parser) parseLiteralString(b []byte) ([]byte, []byte, []byte, error) {
v, rest, err := scanLiteralString(b)
v, rest, err := scanLiteralString(b, p.offsetOf(b))
if err != nil {
return nil, nil, nil, err
}
@@ -491,7 +481,7 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
b = p.parseWhitespace(b)
if len(b) == 0 {
return parent, nil, NewParserError(previousB[:1], "inline table is incomplete")
return parent, nil, NewParserError(previousB[:1], p.offsetOf(previousB), "inline table is incomplete")
}
if b[0] == '}' {
@@ -499,7 +489,7 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
}
if !first {
b, err = expect(',', b)
b, err = expect(',', b, p.offsetOf(b))
if err != nil {
return parent, nil, err
}
@@ -523,7 +513,7 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
first = false
}
rest, err := expect('}', b)
rest, err := expect('}', b, p.offsetOf(b))
return parent, rest, err
}
@@ -572,7 +562,7 @@ func (p *Parser) parseValArray(b []byte) (reference, []byte, error) {
}
if len(b) == 0 {
return parent, nil, NewParserError(arrayStart[:1], "array is incomplete")
return parent, nil, NewParserError(arrayStart[:1], p.offsetOf(arrayStart), "array is incomplete")
}
if b[0] == ']' {
@@ -581,7 +571,7 @@ func (p *Parser) parseValArray(b []byte) (reference, []byte, error) {
if b[0] == ',' {
if first {
return parent, nil, NewParserError(b[0:1], "array cannot start with comma")
return parent, nil, NewParserError(b[0:1], p.offsetOf(b), "array cannot start with comma")
}
b = b[1:]
@@ -593,7 +583,7 @@ func (p *Parser) parseValArray(b []byte) (reference, []byte, error) {
addChild(cref)
}
} else if !first {
return parent, nil, NewParserError(b[0:1], "array elements must be separated by commas")
return parent, nil, NewParserError(b[0:1], p.offsetOf(b), "array elements must be separated by commas")
}
// TOML allows trailing commas in arrays.
@@ -620,7 +610,7 @@ func (p *Parser) parseValArray(b []byte) (reference, []byte, error) {
first = false
}
rest, err := expect(']', b)
rest, err := expect(']', b, p.offsetOf(b))
return parent, rest, err
}
@@ -675,7 +665,7 @@ func (p *Parser) parseOptionalWhitespaceCommentNewline(b []byte) (reference, []b
}
func (p *Parser) parseMultilineLiteralString(b []byte) ([]byte, []byte, []byte, error) {
token, rest, err := scanMultilineLiteralString(b)
token, rest, err := scanMultilineLiteralString(b, p.offsetOf(b))
if err != nil {
return nil, nil, nil, err
}
@@ -704,7 +694,7 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
// mlb-quotes = 1*2quotation-mark
// mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// mlb-escaped-nl = escape ws newline *( wschar / newline )
token, escaped, rest, err := scanMultilineBasicString(b)
token, escaped, rest, err := scanMultilineBasicString(b, p.offsetOf(b))
if err != nil {
return nil, nil, nil, err
}
@@ -721,14 +711,15 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
// fast path
startIdx := i
endIdx := len(token) - len(`"""`)
tokenBase := p.offsetOf(token)
if !escaped {
str := token[startIdx:endIdx]
highlight := characters.Utf8TomlValidAlreadyEscaped(str)
if len(highlight) == 0 {
invalidIdx := characters.Utf8TomlValidAlreadyEscaped(str)
if invalidIdx < 0 {
return token, str, rest, nil
}
return nil, nil, nil, NewParserError(highlight, "invalid UTF-8")
return nil, nil, nil, NewParserError(str[invalidIdx:invalidIdx+1], tokenBase+startIdx+invalidIdx, "invalid UTF-8")
}
var builder bytes.Buffer
@@ -793,14 +784,14 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
case 'e':
builder.WriteByte(0x1B)
case 'u':
x, err := hexToRune(atmost(token[i+1:], 4), 4)
x, err := hexToRune(atmost(token[i+1:], 4), tokenBase+i+1, 4)
if err != nil {
return nil, nil, nil, err
}
builder.WriteRune(x)
i += 4
case 'U':
x, err := hexToRune(atmost(token[i+1:], 8), 8)
x, err := hexToRune(atmost(token[i+1:], 8), tokenBase+i+1, 8)
if err != nil {
return nil, nil, nil, err
}
@@ -808,13 +799,13 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
builder.WriteRune(x)
i += 8
default:
return nil, nil, nil, NewParserError(token[i:i+1], "invalid escaped character %#U", c)
return nil, nil, nil, NewParserError(token[i:i+1], tokenBase+i, "invalid escaped character %#U", c)
}
i++
} else {
size := characters.Utf8ValidNext(token[i:])
if size == 0 {
return nil, nil, nil, NewParserError(token[i:i+1], "invalid character %#U", c)
return nil, nil, nil, NewParserError(token[i:i+1], tokenBase+i, "invalid character %#U", c)
}
builder.Write(token[i : i+size])
i += size
@@ -869,12 +860,9 @@ func (p *Parser) parseKey(b []byte) (reference, []byte, error) {
func (p *Parser) parseSimpleKey(b []byte) (raw, key, rest []byte, err error) {
if len(b) == 0 {
return nil, nil, nil, NewParserError(b, "expected key but found none")
return nil, nil, nil, NewParserError(b, p.offsetOf(b), "expected key but found none")
}
// simple-key = quoted-key / unquoted-key
// unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
// quoted-key = basic-string / literal-string
switch {
case b[0] == '\'':
return p.parseLiteralString(b)
@@ -884,7 +872,7 @@ func (p *Parser) parseSimpleKey(b []byte) (raw, key, rest []byte, err error) {
key, rest = scanUnquotedKey(b)
return key, key, rest, nil
default:
return nil, nil, nil, NewParserError(b[0:1], "invalid character at start of key: %c", b[0])
return nil, nil, nil, NewParserError(b[0:1], p.offsetOf(b), "invalid character at start of key: %c", b[0])
}
}
@@ -904,7 +892,7 @@ func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
// escape-seq-char =/ %x74 ; t tab U+0009
// escape-seq-char =/ %x75 4HEXDIG ; uXXXX U+XXXX
// escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX U+XXXXXXXX
token, escaped, rest, err := scanBasicString(b)
token, escaped, rest, err := scanBasicString(b, p.offsetOf(b))
if err != nil {
return nil, nil, nil, err
}
@@ -915,13 +903,15 @@ func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
// Fast path. If there is no escape sequence, the string should just be
// an UTF-8 encoded string, which is the same as Go. In that case,
// validate the string and return a direct reference to the buffer.
tokenBase := p.offsetOf(token)
if !escaped {
str := token[startIdx:endIdx]
highlight := characters.Utf8TomlValidAlreadyEscaped(str)
if len(highlight) == 0 {
invalidIdx := characters.Utf8TomlValidAlreadyEscaped(str)
if invalidIdx < 0 {
return token, str, rest, nil
}
return nil, nil, nil, NewParserError(highlight, "invalid UTF-8")
return nil, nil, nil, NewParserError(str[invalidIdx:invalidIdx+1], tokenBase+startIdx+invalidIdx, "invalid UTF-8")
}
i := startIdx
@@ -952,7 +942,7 @@ func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
case 'e':
builder.WriteByte(0x1B)
case 'u':
x, err := hexToRune(token[i+1:len(token)-1], 4)
x, err := hexToRune(token[i+1:len(token)-1], tokenBase+i+1, 4)
if err != nil {
return nil, nil, nil, err
}
@@ -960,7 +950,7 @@ func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
builder.WriteRune(x)
i += 4
case 'U':
x, err := hexToRune(token[i+1:len(token)-1], 8)
x, err := hexToRune(token[i+1:len(token)-1], tokenBase+i+1, 8)
if err != nil {
return nil, nil, nil, err
}
@@ -968,13 +958,13 @@ func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
builder.WriteRune(x)
i += 8
default:
return nil, nil, nil, NewParserError(token[i:i+1], "invalid escaped character %#U", c)
return nil, nil, nil, NewParserError(token[i:i+1], tokenBase+i, "invalid escaped character %#U", c)
}
i++
} else {
size := characters.Utf8ValidNext(token[i:])
if size == 0 {
return nil, nil, nil, NewParserError(token[i:i+1], "invalid character %#U", c)
return nil, nil, nil, NewParserError(token[i:i+1], tokenBase+i, "invalid character %#U", c)
}
builder.Write(token[i : i+size])
i += size
@@ -984,9 +974,9 @@ func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
return token, builder.Bytes(), rest, nil
}
func hexToRune(b []byte, length int) (rune, error) {
func hexToRune(b []byte, base int, length int) (rune, error) {
if len(b) < length {
return -1, NewParserError(b, "unicode point needs %d character, not %d", length, len(b))
return -1, NewParserError(b, base, "unicode point needs %d character, not %d", length, len(b))
}
b = b[:length]
@@ -1001,13 +991,13 @@ func hexToRune(b []byte, length int) (rune, error) {
case 'A' <= c && c <= 'F':
d = uint32(c - 'A' + 10)
default:
return -1, NewParserError(b[i:i+1], "non-hex character")
return -1, NewParserError(b[i:i+1], base+i, "non-hex character")
}
r = r*16 + d
}
if r > unicode.MaxRune || 0xD800 <= r && r < 0xE000 {
return -1, NewParserError(b, "escape sequence is invalid Unicode code point")
return -1, NewParserError(b, base, "escape sequence is invalid Unicode code point")
}
return rune(r), nil
@@ -1027,7 +1017,7 @@ func (p *Parser) parseIntOrFloatOrDateTime(b []byte) (reference, []byte, error)
switch b[0] {
case 'i':
if !scanFollowsInf(b) {
return invalidReference, nil, NewParserError(atmost(b, 3), "expected 'inf'")
return invalidReference, nil, NewParserError(atmost(b, 3), p.offsetOf(b), "expected 'inf'")
}
return p.builder.Push(Node{
@@ -1037,7 +1027,7 @@ func (p *Parser) parseIntOrFloatOrDateTime(b []byte) (reference, []byte, error)
}), b[3:], nil
case 'n':
if !scanFollowsNan(b) {
return invalidReference, nil, NewParserError(atmost(b, 3), "expected 'nan'")
return invalidReference, nil, NewParserError(atmost(b, 3), p.offsetOf(b), "expected 'nan'")
}
return p.builder.Push(Node{
@@ -1196,7 +1186,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) {
}), b[i+3:], nil
}
return invalidReference, nil, NewParserError(b[i:i+1], "unexpected character 'i' while scanning for a number")
return invalidReference, nil, NewParserError(b[i:i+1], p.offsetOf(b)+i, "unexpected character 'i' while scanning for a number")
}
if c == 'n' {
@@ -1208,14 +1198,14 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) {
}), b[i+3:], nil
}
return invalidReference, nil, NewParserError(b[i:i+1], "unexpected character 'n' while scanning for a number")
return invalidReference, nil, NewParserError(b[i:i+1], p.offsetOf(b)+i, "unexpected character 'n' while scanning for a number")
}
break
}
if i == 0 {
return invalidReference, b, NewParserError(b, "incomplete number")
return invalidReference, b, NewParserError(b, p.offsetOf(b), "incomplete number")
}
kind := Integer
@@ -1252,13 +1242,13 @@ func isValidBinaryRune(r byte) bool {
return r == '0' || r == '1' || r == '_'
}
func expect(x byte, b []byte) ([]byte, error) {
func expect(x byte, b []byte, base int) ([]byte, error) {
if len(b) == 0 {
return nil, NewParserError(b, "expected character %c but the document ended here", x)
return nil, NewParserError(b, base, "expected character %c but the document ended here", x)
}
if b[0] != x {
return nil, NewParserError(b[0:1], "expected character %c", x)
return nil, NewParserError(b[0:1], base, "expected character %c", x)
}
return b[1:], nil
+91
View File
@@ -1,6 +1,7 @@
package unstable
import (
"errors"
"fmt"
"strconv"
"strings"
@@ -673,6 +674,96 @@ key3 = "value3"
assert.Equal(t, []string{"key1", "key2", "key3"}, keys)
}
func TestErrorOffsetAfterComment(t *testing.T) {
input := []byte("# comment\n= \"value\"")
p := Parser{}
p.Reset(input)
for p.NextExpression() {
}
err := p.Error()
if err == nil {
t.Fatal("expected an error")
}
var perr *ParserError
if !errors.As(err, &perr) {
t.Fatalf("expected ParserError, got %T", err)
}
if perr.Offset != 10 {
t.Errorf("offset: got %d, want 10", perr.Offset)
}
shape := p.Shape(Range{Offset: uint32(perr.Offset), Length: uint32(len(perr.Highlight))})
if shape.Start.Line != 2 || shape.Start.Column != 1 {
t.Errorf("position: got %d:%d, want 2:1", shape.Start.Line, shape.Start.Column)
}
}
func TestErrorHighlightPositions(t *testing.T) {
examples := []struct {
desc string
input string
wantLine int
wantColumn int
}{
{
desc: "invalid key start after comment",
input: "# comment\n= \"value\"",
wantLine: 2,
wantColumn: 1,
},
{
desc: "invalid key start on first line",
input: "= \"value\"",
wantLine: 1,
wantColumn: 1,
},
{
desc: "invalid key after multiple comments",
input: "# comment 1\n# comment 2\n= \"value\"",
wantLine: 3,
wantColumn: 1,
},
{
desc: "invalid key after valid key-value",
input: "a = 1\n= \"value\"",
wantLine: 2,
wantColumn: 1,
},
{
desc: "invalid key after whitespace on line",
input: "a = 1\n = \"value\"",
wantLine: 2,
wantColumn: 3,
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
p := Parser{}
p.Reset([]byte(e.input))
for p.NextExpression() {
}
err := p.Error()
if err == nil {
t.Fatal("expected an error")
}
var perr *ParserError
if !errors.As(err, &perr) {
t.Fatalf("expected ParserError, got %T", err)
}
shape := p.Shape(Range{Offset: uint32(perr.Offset), Length: uint32(len(perr.Highlight))})
if shape.Start.Line != e.wantLine {
t.Errorf("line: got %d, want %d", shape.Start.Line, e.wantLine)
}
if shape.Start.Column != e.wantColumn {
t.Errorf("column: got %d, want %d", shape.Start.Column, e.wantColumn)
}
})
}
}
func ExampleParser() {
doc := `
hello = "world"
+27 -72
View File
@@ -47,48 +47,31 @@ func isUnquotedKeyChar(r byte) bool {
return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_'
}
func scanLiteralString(b []byte) ([]byte, []byte, error) {
// literal-string = apostrophe *literal-char apostrophe
// apostrophe = %x27 ; ' apostrophe
// literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
func scanLiteralString(b []byte, base int) ([]byte, []byte, error) {
for i := 1; i < len(b); {
switch b[i] {
case '\'':
return b[:i+1], b[i+1:], nil
case '\n', '\r':
return nil, nil, NewParserError(b[i:i+1], "literal strings cannot have new lines")
return nil, nil, NewParserError(b[i:i+1], base+i, "literal strings cannot have new lines")
}
size := characters.Utf8ValidNext(b[i:])
if size == 0 {
return nil, nil, NewParserError(b[i:i+1], "invalid character")
return nil, nil, NewParserError(b[i:i+1], base+i, "invalid character")
}
i += size
}
return nil, nil, NewParserError(b[len(b):], "unterminated literal string")
return nil, nil, NewParserError(b[len(b):], base+len(b), "unterminated literal string")
}
func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
// ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
// ml-literal-string-delim
// ml-literal-string-delim = 3apostrophe
// ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
//
// mll-content = mll-char / newline
// mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
// mll-quotes = 1*2apostrophe
func scanMultilineLiteralString(b []byte, base int) ([]byte, []byte, error) {
for i := 3; i < len(b); {
switch b[i] {
case '\'':
if scanFollowsMultilineLiteralStringDelimiter(b[i:]) {
i += 3
// At that point we found 3 apostrophe, and i is the
// index of the byte after the third one. The scanner
// needs to be eager, because there can be an extra 2
// apostrophe that can be accepted at the end of the
// string.
if i >= len(b) || b[i] != '\'' {
return b[:i], b[i:], nil
}
@@ -100,39 +83,39 @@ func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
i++
if i < len(b) && b[i] == '\'' {
return nil, nil, NewParserError(b[i-3:i+1], "''' not allowed in multiline literal string")
return nil, nil, NewParserError(b[i-3:i+1], base+i-3, "''' not allowed in multiline literal string")
}
return b[:i], b[i:], nil
}
case '\r':
if len(b) < i+2 {
return nil, nil, NewParserError(b[len(b):], `need a \n after \r`)
return nil, nil, NewParserError(b[len(b):], base+len(b), `need a \n after \r`)
}
if b[i+1] != '\n' {
return nil, nil, NewParserError(b[i:i+2], `need a \n after \r`)
return nil, nil, NewParserError(b[i:i+2], base+i, `need a \n after \r`)
}
i += 2 // skip the \n
i += 2
continue
}
size := characters.Utf8ValidNext(b[i:])
if size == 0 {
return nil, nil, NewParserError(b[i:i+1], "invalid character")
return nil, nil, NewParserError(b[i:i+1], base+i, "invalid character")
}
i += size
}
return nil, nil, NewParserError(b[len(b):], `multiline literal string not terminated by '''`)
return nil, nil, NewParserError(b[len(b):], base+len(b), `multiline literal string not terminated by '''`)
}
func scanWindowsNewline(b []byte) ([]byte, []byte, error) {
func scanWindowsNewline(b []byte, base int) ([]byte, []byte, error) {
const lenCRLF = 2
if len(b) < lenCRLF {
return nil, nil, NewParserError(b, "windows new line expected")
return nil, nil, NewParserError(b, base, "windows new line expected")
}
if b[1] != '\n' {
return nil, nil, NewParserError(b, `windows new line should be \r\n`)
return nil, nil, NewParserError(b, base, `windows new line should be \r\n`)
}
return b[:lenCRLF], b[lenCRLF:], nil
@@ -151,13 +134,7 @@ func scanWhitespace(b []byte) ([]byte, []byte) {
return b, b[len(b):]
}
func scanComment(b []byte) ([]byte, []byte, error) {
// comment-start-symbol = %x23 ; #
// non-ascii = %x80-D7FF / %xE000-10FFFF
// non-eol = %x09 / %x20-7F / non-ascii
//
// comment = comment-start-symbol *non-eol
func scanComment(b []byte, base int) ([]byte, []byte, error) {
for i := 1; i < len(b); {
if b[i] == '\n' {
return b[:i], b[i:], nil
@@ -166,11 +143,11 @@ func scanComment(b []byte) ([]byte, []byte, error) {
if i+1 < len(b) && b[i+1] == '\n' {
return b[:i+1], b[i+1:], nil
}
return nil, nil, NewParserError(b[i:i+1], "invalid character in comment")
return nil, nil, NewParserError(b[i:i+1], base+i, "invalid character in comment")
}
size := characters.Utf8ValidNext(b[i:])
if size == 0 {
return nil, nil, NewParserError(b[i:i+1], "invalid character in comment")
return nil, nil, NewParserError(b[i:i+1], base+i, "invalid character in comment")
}
i += size
@@ -179,12 +156,7 @@ func scanComment(b []byte) ([]byte, []byte, error) {
return b, b[len(b):], nil
}
func scanBasicString(b []byte) ([]byte, bool, []byte, error) {
// basic-string = quotation-mark *basic-char quotation-mark
// quotation-mark = %x22 ; "
// basic-char = basic-unescaped / escaped
// basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// escaped = escape escape-seq-char
func scanBasicString(b []byte, base int) ([]byte, bool, []byte, error) {
escaped := false
i := 1
@@ -193,31 +165,20 @@ func scanBasicString(b []byte) ([]byte, bool, []byte, error) {
case '"':
return b[:i+1], escaped, b[i+1:], nil
case '\n', '\r':
return nil, escaped, nil, NewParserError(b[i:i+1], "basic strings cannot have new lines")
return nil, escaped, nil, NewParserError(b[i:i+1], base+i, "basic strings cannot have new lines")
case '\\':
if len(b) < i+2 {
return nil, escaped, nil, NewParserError(b[i:i+1], "need a character after \\")
return nil, escaped, nil, NewParserError(b[i:i+1], base+i, "need a character after \\")
}
escaped = true
i++ // skip the next character
}
}
return nil, escaped, nil, NewParserError(b[len(b):], `basic string not terminated by "`)
return nil, escaped, nil, NewParserError(b[len(b):], base+len(b), `basic string not terminated by "`)
}
func scanMultilineBasicString(b []byte) ([]byte, bool, []byte, error) {
// ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
// ml-basic-string-delim
// ml-basic-string-delim = 3quotation-mark
// ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
//
// mlb-content = mlb-char / newline / mlb-escaped-nl
// mlb-char = mlb-unescaped / escaped
// mlb-quotes = 1*2quotation-mark
// mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// mlb-escaped-nl = escape ws newline *( wschar / newline )
func scanMultilineBasicString(b []byte, base int) ([]byte, bool, []byte, error) {
escaped := false
i := 3
@@ -227,12 +188,6 @@ func scanMultilineBasicString(b []byte) ([]byte, bool, []byte, error) {
if scanFollowsMultilineBasicStringDelimiter(b[i:]) {
i += 3
// At that point we found 3 apostrophe, and i is the
// index of the byte after the third one. The scanner
// needs to be eager, because there can be an extra 2
// apostrophe that can be accepted at the end of the
// string.
if i >= len(b) || b[i] != '"' {
return b[:i], escaped, b[i:], nil
}
@@ -244,27 +199,27 @@ func scanMultilineBasicString(b []byte) ([]byte, bool, []byte, error) {
i++
if i < len(b) && b[i] == '"' {
return nil, escaped, nil, NewParserError(b[i-3:i+1], `""" not allowed in multiline basic string`)
return nil, escaped, nil, NewParserError(b[i-3:i+1], base+i-3, `""" not allowed in multiline basic string`)
}
return b[:i], escaped, b[i:], nil
}
case '\\':
if len(b) < i+2 {
return nil, escaped, nil, NewParserError(b[len(b):], "need a character after \\")
return nil, escaped, nil, NewParserError(b[len(b):], base+len(b), "need a character after \\")
}
escaped = true
i++ // skip the next character
case '\r':
if len(b) < i+2 {
return nil, escaped, nil, NewParserError(b[len(b):], `need a \n after \r`)
return nil, escaped, nil, NewParserError(b[len(b):], base+len(b), `need a \n after \r`)
}
if b[i+1] != '\n' {
return nil, escaped, nil, NewParserError(b[i:i+2], `need a \n after \r`)
return nil, escaped, nil, NewParserError(b[i:i+2], base+i, `need a \n after \r`)
}
i++ // skip the \n
}
}
return nil, escaped, nil, NewParserError(b[len(b):], `multiline basic string not terminated by """`)
return nil, escaped, nil, NewParserError(b[len(b):], base+len(b), `multiline basic string not terminated by """`)
}