Bug:filex.LastLine returns a wrong line when the content after the last separator is a multiple of 1024 bytes
filex.LastLine returns a wrong line when the content after the last separator is a multiple of 1024 bytes
Description
filex.LastLine can return a wrong result: part (or all) of the previous line gets merged into the returned "last line".
The bug is triggered when the number of bytes after the last separator '\n' is an exact multiple of the internal block size (bufSize = 1024) — for example when the last line is exactly 1024 bytes long and the file does not end with a newline. Fixed-length record files and machine-generated logs can hit this alignment.
Root cause
In core/filex/file.go, lastLine() reads the file backwards in 1024-byte blocks. Before scanning a block for '\n', it unconditionally trims a trailing newline:
if buf[n-1] == '\n' {
buf = buf[:n-1]
n--
} else {
buf = buf[:n]
}This trim is only correct for the tail block (the first block read), where a trailing '\n' is the file's final newline.
For an interior block, a '\n' at the block's last byte is the start boundary of the last line (the next block, already read, begins exactly at the start of the last line). Trimming it destroys that boundary; the remaining block then contains no '\n', so the whole block — which belongs to the previous line — is prepended to the accumulated result:
last = append(buf, last...) // buf belongs to the previous line, wrongly mergedPrecise trigger condition: (fileSize - lastSeparatorOffset - 1) % 1024 == 0, i.e. the length of the content after the last '\n' is a multiple of 1024.
Reproduction
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/zeromicro/go-zero/core/filex"
)
func main() {
// any previous line + '\n' + a last line of exactly 1024 bytes, no trailing newline
content := "previous line\n" + strings.Repeat("C", 1024)
dir, _ := os.MkdirTemp("", "repro")
defer os.RemoveAll(dir)
f := filepath.Join(dir, "t.txt")
os.WriteFile(f, []byte(content), 0o644)
got, _ := filex.LastLine(f)
fmt.Printf("expected len=1024, got len=%d, got=%q...\n", len(got), got[:min(24, len(got))])
}Output:
expected len=1024, got len=1037, got="previous lineCCCCCCCCCCCC"...The previous line (13 bytes) is merged into the result. Adding just one byte to the last line (1025 bytes) makes the bug disappear, which confirms the alignment condition.
Suggested fix
Only trim the trailing newline on the block that reaches the end of the file:
// only the tail block can end with the file's final newline;
// an interior block's trailing '\n' is the start boundary of the last line
if offset+bufLen == info.Size() && buf[n-1] == '\n' {
buf = buf[:n-1]
n--
} else {
buf = buf[:n]
}With this change, an interior block ending in '\n' is handled correctly by the existing backward scan: it finds the '\n' at i = n-1 and returns buf[n:] + last, which is exactly the accumulated last (the last line starts at the next block).
Environment
- go-zero: master (
core/filex/file.go) - go version go1.27.1 darwin/arm64
Source: zeromicro/go-zero