Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/memci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ jobs:

- uses: actions/setup-go@v6
with:
go-version: "1.24"
# TinyGo 0.42 builds with Go 1.25 through 1.27 and refuses anything
# outside that window, so the two versions below move together.
go-version: "1.26"
- uses: acifani/setup-tinygo@v2
with:
tinygo-version: "0.42.0"

- uses: soypat/memci@main
with:
args: -targets ./cmd/... -kind package
# bindiff lives in this repo, so measure with the copy under test
# rather than installing a published one.
bindiff: go run ./cmd/bindiff
tinygo: tinygo
6 changes: 2 additions & 4 deletions build/elfutil/dwarf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,7 @@ func TestStreamedLineTableMatchesResident(t *testing.T) {
sec := xdwarf.Sections{Str: bytes.NewReader(str), LineStr: bytes.NewReader(lineStr)}

// The fixture's largest unit header wants 1867 bytes of aux, so 2048 is
// about as tight as this walk goes -- and tight is the point, since a small
// aux means a refill across nearly every read.
// about as tight as this walk goes.
for _, aux := range []int{2048, 4096, 16384} {
t.Run("aux="+itoa(aux), func(t *testing.T) {
sec.Line = bytes.NewReader(raw)
Expand Down Expand Up @@ -181,10 +180,9 @@ func TestStreamBufferTooSmall(t *testing.T) {
var next int64
next, err = xdwarf.DecodeLineUnit(&u, sec, off, size, auxBuf)
if err == nil {
// LineUnit.Rows reports no error of its own, so a rewind that
// happens mid-program is only visible on the next unit's decode.
for range u.Rows {
}
err = u.Err()
}
if err != nil {
break
Expand Down
25 changes: 21 additions & 4 deletions build/xdwarf/line.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,12 @@ type LineUnit struct {

sec Sections
hdr []byte // The unit header, at the front of aux. Inline strRefs index it.
win []byte // The tail of aux, the fill buffer VisitRows streams through.
win []byte // The tail of aux, the fill buffer Rows streams through.
// The opcode stream, as absolute offsets within .debug_line.
progStart, progEnd int64
// err holds why the last [LineUnit.Rows] walk stopped early. An iterator
// cannot return one, so it is kept here for [LineUnit.Err].
err error
}

// Row is one row of the line table matrix: the state of the line program at a
Expand Down Expand Up @@ -316,7 +319,7 @@ func (u *LineUnit) appendStr(dst []byte, r strRef) ([]byte, error) {
// appendCStr appends the NUL-terminated string at off to dst, reading straight
// onto dst's own spare capacity so it needs no scratch of its own. That matters
// because the aux tail, the obvious place for scratch, is simultaneously the
// window a [LineUnit.VisitRows] walk is streaming the opcode program through.
// window a [LineUnit.Rows] walk is streaming the opcode program through.
func appendCStr(dst []byte, r io.ReaderAt, off int64) ([]byte, error) {
if r == nil {
return dst, errNoStrSection
Expand Down Expand Up @@ -348,7 +351,7 @@ func appendCStr(dst []byte, r io.ReaderAt, off int64) ([]byte, error) {
// aux is scratch owned by the caller and borrowed by dst until the next decode
// into the same buffer. The unit's header is copied to its front, where dst's
// directory and file tables point, and its tail becomes the fill buffer
// [LineUnit.VisitRows] streams the opcode program through. An aux too small to
// [LineUnit.Rows] streams the opcode program through. An aux too small to
// hold both reports how large it needs to be. Passing the same dst and aux back
// on the next call makes decoding allocation-free.
func DecodeLineUnit(dst *LineUnit, sec Sections, off, size int64, aux []byte) (next int64, err error) {
Expand Down Expand Up @@ -723,8 +726,19 @@ func (u *LineUnit) skipForm(c *cursor, f Form) error {
return c.err
}

// Rows is an [iter.Seq] iterator implementation oveer LineUnit rows.
// Err reports why the last [LineUnit.Rows] walk stopped, or nil if it ran to
// the end of the opcode program. A malformed unit, or a lookback window too
// small for the decoder to step back through, ends a walk early; because an
// [iter.Seq] cannot return an error, a caller that needs to tell a truncated
// walk from a complete one has to ask here.
//
// [LineUnit.Rows] clears it, so Err describes the most recent walk only.
func (u *LineUnit) Err() error { return u.err }

// Rows is an [iter.Seq] iterator over the unit's line table rows. A walk that
// ends early leaves its reason in [LineUnit.Err].
func (u *LineUnit) Rows(yield func(r Row) bool) {
u.err = nil
var c streamCursor
c.config(u.sec.Line, u.win, u.progStart, u.progEnd, u.sec.byteOrder())

Expand Down Expand Up @@ -828,6 +842,9 @@ func (u *LineUnit) Rows(yield func(r Row) bool) {
}
}
}
// Only reached by running the program out; every early return above is a
// caller stopping the walk, which leaves u.err nil as cleared on entry.
u.err = c.err
}

func (u *LineUnit) resetRow() Row {
Expand Down
33 changes: 33 additions & 0 deletions build/xdwarf/line_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,39 @@ func TestRowsStop(t *testing.T) {
if n != 3 {
t.Errorf("visited %d rows after stopping at 3", n)
}
// Stopping is the caller's own doing. Reporting it as an error would make
// every early exit look like a malformed unit.
if err := u.Err(); err != nil {
t.Errorf("stopping the walk reported an error: %s", err)
}
}

// TestRowsErr pins the rest of the Err contract: a walk that reaches the end of
// the opcode program reports nothing, and each walk describes only itself.
func TestRowsErr(t *testing.T) {
sec, size := loadSections(t, "../../testdata/helloc.elf")
var u xdwarf.LineUnit
if _, err := xdwarf.DecodeLineUnit(&u, sec, 0, size, make([]byte, defaultAux)); err != nil {
t.Fatal(err)
}
rows := 0
for range u.Rows {
rows++
}
if rows == 0 {
t.Fatal("the unit yielded no rows")
}
if err := u.Err(); err != nil {
t.Fatalf("a complete walk reported an error: %s", err)
}
// A second walk over the same unit must start from a clean slate rather
// than inheriting the first walk's verdict.
for range u.Rows {
break
}
if err := u.Err(); err != nil {
t.Errorf("a second walk inherited an error: %s", err)
}
}

func TestDecodeLineUnitRejectsBadInput(t *testing.T) {
Expand Down
Loading