Remove unsafe package usage (#1021)

Removes all unsafe operations from go-toml, making the codebase
fully safe Go code. The internal/danger package that contained
unsafe operations has been deleted.

Changes:
- Replace pointer-based node navigation with index-based navigation
- Node.next and Node.child now store absolute indices into the
  backing nodes slice instead of relative offsets
- Add nodes pointer to Node and Iterator for safe navigation
- Replace danger.TypeID with reflect.Type for cache keys
- Delete internal/danger package entirely

Performance overhead is under 10% compared to the unsafe version,
which is acceptable for the safety and maintainability benefits.

[Cursor][claude-sonnet-4-20250514]
This commit is contained in:
Thomas Pelletier
2026-01-04 13:16:47 -05:00
committed by GitHub
parent a675c6b3e2
commit 3aaf147e3e
12 changed files with 295 additions and 360 deletions
+28 -7
View File
@@ -2,10 +2,10 @@ package toml
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/unstable"
)
@@ -58,14 +58,14 @@ func (s *StrictMissingError) String() string {
//
// Implements errors.Join() interface.
func (s *StrictMissingError) Unwrap() []error {
errs := make([]error, 0, len(s.Errors))
errs := make([]error, len(s.Errors))
for i := range s.Errors {
errs = append(errs, &s.Errors[i])
errs[i] = &s.Errors[i]
}
return errs
}
// Key is a slice of strings that represents a path to a value in a TOML document.
// Key represents a TOML key as a sequence of key parts.
type Key []string
// Error returns the error message contained in the DecodeError.
@@ -93,12 +93,14 @@ func (e *DecodeError) Key() Key {
// wrapDecodeError creates a DecodeError referencing a highlighted
// range of bytes from document.
//
// Highlight needs to be a sub-slice of document, or this function panics.
// highlight needs to be a sub-slice of document, or this function panics.
//
// The function copies all bytes used in DecodeError, so that document and
// highlight can be freely deallocated.
//
//nolint:funlen
func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
offset := danger.SubsliceOffset(document, de.Highlight)
offset := subsliceOffset(document, de.Highlight)
errMessage := de.Error()
errLine, errColumn := positionAtEnd(document[:offset])
@@ -258,5 +260,24 @@ func positionAtEnd(b []byte) (row int, column int) {
}
}
return row, column
return
}
// 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
}