Merge 1226362f1ebdf96d195349b263082ce129958136 into 19b877fa50cbbb9282763099fb177a1e5cc5c850

This commit is contained in:
Name 2025-12-09 14:40:37 +00:00 committed by GitHub
commit aa012169e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 89 additions and 30 deletions

View File

@ -5,7 +5,9 @@
package gin package gin
import ( import (
"bufio"
"bytes" "bytes"
"cmp"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -21,9 +23,10 @@ import (
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
) )
const dunno = "???" const (
dunno = "???"
var dunnoBytes = []byte(dunno) stackSkip = 3
)
// RecoveryFunc defines the function passable to CustomRecovery. // RecoveryFunc defines the function passable to CustomRecovery.
type RecoveryFunc func(c *Context, err any) type RecoveryFunc func(c *Context, err any)
@ -72,7 +75,6 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
brokenPipe = true brokenPipe = true
} }
if logger != nil { if logger != nil {
const stackSkip = 3
if brokenPipe { if brokenPipe {
logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset) logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset)
} else if IsDebugging() { } else if IsDebugging() {
@ -120,8 +122,11 @@ func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data buf := new(bytes.Buffer) // the returned data
// As we loop, we open files and read them. These variables record the currently // As we loop, we open files and read them. These variables record the currently
// loaded file. // loaded file.
var lines [][]byte var (
var lastFile string nLine string
lastFile string
err error
)
for i := skip; ; i++ { // Skip the expected number of frames for i := skip; ; i++ { // Skip the expected number of frames
pc, file, line, ok := runtime.Caller(i) pc, file, line, ok := runtime.Caller(i)
if !ok { if !ok {
@ -130,25 +135,44 @@ func stack(skip int) []byte {
// Print this much at least. If we can't find the source, it won't show. // Print this much at least. If we can't find the source, it won't show.
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
if file != lastFile { if file != lastFile {
data, err := os.ReadFile(file) nLine, err = readNthLine(file, line-1)
if err != nil { if err != nil {
continue continue
} }
lines = bytes.Split(data, []byte{'\n'})
lastFile = file lastFile = file
} }
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) fmt.Fprintf(buf, "\t%s: %s\n", function(pc), cmp.Or(nLine, dunno))
} }
return buf.Bytes() return buf.Bytes()
} }
// source returns a space-trimmed slice of the n'th line. // readNthLine reads the nth line from the file.
func source(lines [][]byte, n int) []byte { // It returns the trimmed content of the line if found,
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed // or an empty string if the line doesn't exist.
if n < 0 || n >= len(lines) { // If there's an error opening the file, it returns the error.
return dunnoBytes func readNthLine(file string, n int) (string, error) {
if n < 0 {
return "", nil
} }
return bytes.TrimSpace(lines[n])
f, err := os.Open(file)
if err != nil {
return "", err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for i := 0; i < n; i++ {
if !scanner.Scan() {
return "", nil
}
}
if scanner.Scan() {
return strings.TrimSpace(scanner.Text()), nil
}
return "", nil
} }
// function returns, if possible, the name of the function containing the PC. // function returns, if possible, the name of the function containing the PC.

View File

@ -88,21 +88,6 @@ func TestPanicWithAbort(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
} }
func TestSource(t *testing.T) {
bs := source(nil, 0)
assert.Equal(t, dunnoBytes, bs)
in := [][]byte{
[]byte("Hello world."),
[]byte("Hi, gin.."),
}
bs = source(in, 10)
assert.Equal(t, dunnoBytes, bs)
bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs)
}
func TestFunction(t *testing.T) { func TestFunction(t *testing.T) {
bs := function(1) bs := function(1)
assert.Equal(t, dunno, bs) assert.Equal(t, dunno, bs)
@ -331,3 +316,53 @@ func TestSecureRequestDump(t *testing.T) {
}) })
} }
} }
// TestReadNthLine tests the readNthLine function with various scenarios.
func TestReadNthLine(t *testing.T) {
// Create a temporary test file
testContent := "line 0 \n line 1 \nline 2 \nline 3 \nline 4"
tempFile, err := os.CreateTemp("", "testfile*.txt")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tempFile.Name())
// Write test content to the temporary file
if _, err := tempFile.WriteString(testContent); err != nil {
t.Fatal(err)
}
if err := tempFile.Close(); err != nil {
t.Fatal(err)
}
// Test cases
tests := []struct {
name string
lineNum int
fileName string
want string
wantErr bool
}{
{name: "Read first line", lineNum: 0, fileName: tempFile.Name(), want: "line 0", wantErr: false},
{name: "Read middle line", lineNum: 2, fileName: tempFile.Name(), want: "line 2", wantErr: false},
{name: "Read last line", lineNum: 4, fileName: tempFile.Name(), want: "line 4", wantErr: false},
{name: "Line number exceeds file length", lineNum: 10, fileName: tempFile.Name(), want: "", wantErr: false},
{name: "Negative line number", lineNum: -1, fileName: tempFile.Name(), want: "", wantErr: false},
{name: "Non-existent file", lineNum: 1, fileName: "/non/existent/file.txt", want: "", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := readNthLine(tt.fileName, tt.lineNum)
assert.Equal(t, tt.wantErr, err != nil)
assert.Equal(t, tt.want, got)
})
}
}
func BenchmarkStack(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
_ = stack(stackSkip)
}
}