Skip to content

Keep the seek offset out of memfs WriteAt - #239

Open
youdie006 wants to merge 2 commits into
go-git:mainfrom
youdie006:memfs-writeat-seek-offset
Open

Keep the seek offset out of memfs WriteAt#239
youdie006 wants to merge 2 commits into
go-git:mainfrom
youdie006:memfs-writeat-seek-offset

Conversation

@youdie006

Copy link
Copy Markdown

memfs's WriteAt moves the handle's seek offset, so a caller that patches an earlier region in
place and keeps writing overwrites its own data — with no error.

io.WriterAt, which billy.File embeds at fs.go:178:

WriteAt should not affect nor be affected by the underlying seek offset.

Same caller code through three implementations — write a placeholder header, write a body, patch the
header via WriteAt, then write a footer:

os.File      content="HDR!0020BODYBODYBODY<EOF>"   offset after WriteAt = 20
billy osfs   content="HDR!0020BODYBODYBODY<EOF>"   offset after WriteAt = 20
billy memfs  content="HDR!0020<EOF>ODYBODY"        offset after WriteAt =  8

Every call returned a nil error. memfs rewound to 8 and wrote the footer over the body.

memfs/file.go:87:

n, err := f.content.WriteAt(p, off)
f.position = off + int64(n)

The assignment cannot simply be dropped, because Write (memfs/file.go:70) is
return f.WriteAt(p, f.position) and relies on that side effect to advance. So the fix moves the
advance into Write, where it belongs, and leaves WriteAt offset-free.

osfs (a real os.File) and osfs/mmap_file.go already honour the contract, and the File
interface already carries a note requiring ReadAt to match io.ReaderAtWriteAt is the one
that drifted.

Test

Added to the shared conformance suite in test/basic_test.go, which eachBasicFS runs over both
osfs and memfs (test/common_posix.go:20), so osfs supplies the expected values rather than
me asserting them. Red with only memfs/file.go reverted — and only the memfs subtest fails:

--- FAIL: TestWriteAtDoesNotMoveSeekOffset/1-*chroot.ChrootHelper
    expected: 20
    actual  : 8

Green with the change; go test ./... passes across all packages and gofmt -l lists neither file.

grep WriteAt --include='*_test.go' finds a single hit, in osfs/mmap_file_test.go — memfs's
WriteAt had no test, and nothing anywhere interleaved it with Write, which is the combination
that fails.


Disclosure: prepared with AI assistance; I verified the three-way comparison and the red/green runs
myself.

io.WriterAt requires WriteAt not to affect the seek offset, but memfs set
position to off+n, so a caller that patched an earlier region in place
then kept writing overwrote its own data. osfs already behaves correctly;
Write now advances the offset itself.
Copilot AI lite review requested due to automatic review settings September 2, 2026 06:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new test leaks an opened file handle (and should follow the file’s established t.Helper() convention), which should be corrected before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes memfs WriteAt to honor the io.WriterAt contract (must not affect the underlying seek offset), preventing callers that interleave WriteAt and Write from accidentally overwriting data. Adds a conformance test that runs across both osfs and memfs via the shared eachBasicFS suite.

Changes:

  • Update memfs/file.go so WriteAt no longer mutates the handle seek position; advance is performed in Write instead.
  • Add a new shared conformance test asserting WriteAt does not move the seek offset and does not corrupt subsequent writes.
File summaries
File Description
memfs/file.go Stops WriteAt from changing file.position; moves offset advancement into Write.
test/basic_test.go Adds a cross-FS regression test validating WriteAt does not move the seek offset or corrupt data.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/basic_test.go
Comment on lines +838 to +842
read, err := fs.Open("foo")
require.NoError(t, err)
all, err := io.ReadAll(read)
require.NoError(t, err)
require.Equal(t, "HDR!0020BODYBODYBODY<EOF>", string(all))
Comment thread test/basic_test.go
Comment on lines +818 to +820
eachBasicFS(t, func(t *testing.T, fs Basic) {
f, err := fs.Create("foo")
require.NoError(t, err)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants