Skip to content

Bug:filex.LastLine returns a wrong line when the content after the last separator is a multiple of 1024 bytes #5787

Open

Description

@purplecity

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 merged

Precise 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions