fix(gopher): reject resume when caller supplies Writer

This commit is contained in:
2026-06-30 11:07:46 +02:00
parent dd46aa10f3
commit 535496e6e1
2 changed files with 60 additions and 0 deletions
+10
View File
@@ -165,6 +165,16 @@ func (p *Protocol) downloadText(ctx context.Context, reader *bufio.Reader, req *
// fresh downloads (create) and resume (append). When req.Writer is // fresh downloads (create) and resume (append). When req.Writer is
// set (caller manages the file), we use it as-is. // set (caller manages the file), we use it as-is.
if req.Output != "" && req.Output != "-" { if req.Output != "" && req.Output != "-" {
// Resume is meaningful only when we own the output file. If the
// caller already supplied a Writer we cannot append to Output
// behind their back, so disable resume to keep the partial-state
// sidecar consistent with what actually lands on disk.
if req.Resume && req.Writer != nil && req.Verbose {
fmt.Fprintf(os.Stderr, "[gopher] warning: --resume ignored because a Writer is set; output goes to the Writer only\n")
}
if req.Resume && req.Writer != nil {
req.Resume = false
}
if req.Resume { if req.Resume {
canResume, resumeMeta, rerr := output.CanResume(req.Output, req.URL.String()) canResume, resumeMeta, rerr := output.CanResume(req.Output, req.URL.String())
if rerr != nil { if rerr != nil {
+50
View File
@@ -438,3 +438,53 @@ func TestGopherResumeStaleMetadata(t *testing.T) {
t.Errorf("content = %q, want %q", string(data), "Fresh\n") t.Errorf("content = %q, want %q", string(data), "Fresh\n")
} }
} }
// TestGopherResumeIgnoredWhenWriterSet verifies that --resume is silently
// disabled when the caller also supplies a Writer: the resume metadata
// sidecar would otherwise lie about bytes written to Output, since the
// real data ends up in the Writer.
func TestGopherResumeIgnoredWhenWriterSet(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
port := listener.Addr().(*net.TCPAddr).Port
go func() {
conn, _ := listener.Accept()
if conn == nil {
return
}
defer conn.Close()
r := bufio.NewReader(conn)
r.ReadString('\n')
conn.Write([]byte("0Line one\t/foo\thost\t70\r\n.\r\n"))
}()
proto := NewProtocol()
u, _ := url.Parse("gopher://127.0.0.1:" + strconv.Itoa(port) + "/foo")
outPath := t.TempDir() + "/ignored-output.txt"
var buf strings.Builder
res, dlErr := proto.Download(context.Background(), &core.DownloadRequest{
URL: u,
Output: outPath,
Writer: &stringWriter{&buf},
Resume: true,
})
if dlErr != nil {
t.Fatalf("download: %v", dlErr)
}
if res.Resumed {
t.Error("expected Resumed=false when Writer is set (resume must be ignored)")
}
if !strings.Contains(buf.String(), "Line one") {
t.Errorf("writer should receive the body, got %q", buf.String())
}
// Output is supplied but a Writer takes precedence; the file must
// not be created or touched by resume machinery.
if _, statErr := os.Stat(outPath); statErr == nil {
t.Error("Output file should not be created when Writer is set")
}
}