Commit b9a79f32 authored by Joe Tsai's avatar Joe Tsai Committed by Joe Tsai

archive/tar: make Writer error handling consistent

The Writer logic was not consistent about when an IO error would
persist across multiple calls on Writer's methods.

Thus, to make the error handling more consistent we always check
the persistent state of the error prior to every exported method
call, and return an error if set. Otherwise, it is the responsibility
of every exported method to persist any fatal errors that may occur.

As a simplification, we can remove the close field since that
information can be represented by simply storing ErrWriteAfterClose
in the err field.

Change-Id: I8746ca36b3739803e0373253450db69b3bd12f38
Reviewed-on: https://go-review.googlesource.com/55590
Run-TryBot: Joe Tsai <joetsai@digital-static.net>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: default avatarIan Lance Taylor <iant@golang.org>
parent 5c20ffbb
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
package tar package tar
// TODO(dsymonds):
// - catch more errors (no first header, etc.)
import ( import (
"bytes" "bytes"
"fmt" "fmt"
...@@ -22,14 +19,16 @@ import ( ...@@ -22,14 +19,16 @@ import (
// Call WriteHeader to begin a new file, and then call Write to supply that file's data, // Call WriteHeader to begin a new file, and then call Write to supply that file's data,
// writing at most hdr.Size bytes in total. // writing at most hdr.Size bytes in total.
type Writer struct { type Writer struct {
w io.Writer w io.Writer
err error nb int64 // number of unwritten bytes for current file entry
nb int64 // number of unwritten bytes for current file entry pad int64 // amount of padding to write after current file entry
pad int64 // amount of padding to write after current file entry
closed bool
hdr Header // Shallow copy of Header that is safe for mutations hdr Header // Shallow copy of Header that is safe for mutations
blk block // Buffer to use as temporary local storage blk block // Buffer to use as temporary local storage
// err is a persistent error.
// It is only the responsibility of every exported method of Writer to
// ensure that this error is sticky.
err error
} }
// NewWriter creates a new Writer writing to w. // NewWriter creates a new Writer writing to w.
...@@ -41,10 +40,12 @@ func NewWriter(w io.Writer) *Writer { return &Writer{w: w} } ...@@ -41,10 +40,12 @@ func NewWriter(w io.Writer) *Writer { return &Writer{w: w} }
// Deprecated: This is unecessary as the next call to WriteHeader or Close // Deprecated: This is unecessary as the next call to WriteHeader or Close
// will implicitly flush out the file's padding. // will implicitly flush out the file's padding.
func (tw *Writer) Flush() error { func (tw *Writer) Flush() error {
if tw.nb > 0 { if tw.err != nil {
tw.err = fmt.Errorf("tar: missed writing %d bytes", tw.nb)
return tw.err return tw.err
} }
if tw.nb > 0 {
return fmt.Errorf("archive/tar: missed writing %d bytes", tw.nb)
}
if _, tw.err = tw.w.Write(zeroBlock[:tw.pad]); tw.err != nil { if _, tw.err = tw.w.Write(zeroBlock[:tw.pad]); tw.err != nil {
return tw.err return tw.err
} }
...@@ -63,13 +64,16 @@ func (tw *Writer) WriteHeader(hdr *Header) error { ...@@ -63,13 +64,16 @@ func (tw *Writer) WriteHeader(hdr *Header) error {
tw.hdr = *hdr // Shallow copy of Header tw.hdr = *hdr // Shallow copy of Header
switch allowedFormats, paxHdrs := tw.hdr.allowedFormats(); { switch allowedFormats, paxHdrs := tw.hdr.allowedFormats(); {
case allowedFormats&formatUSTAR != 0: case allowedFormats&formatUSTAR != 0:
return tw.writeUSTARHeader(&tw.hdr) tw.err = tw.writeUSTARHeader(&tw.hdr)
return tw.err
case allowedFormats&formatPAX != 0: case allowedFormats&formatPAX != 0:
return tw.writePAXHeader(&tw.hdr, paxHdrs) tw.err = tw.writePAXHeader(&tw.hdr, paxHdrs)
return tw.err
case allowedFormats&formatGNU != 0: case allowedFormats&formatGNU != 0:
return tw.writeGNUHeader(&tw.hdr) tw.err = tw.writeGNUHeader(&tw.hdr)
return tw.err
default: default:
return ErrHeader return ErrHeader // Non-fatal error
} }
} }
...@@ -273,45 +277,46 @@ func splitUSTARPath(name string) (prefix, suffix string, ok bool) { ...@@ -273,45 +277,46 @@ func splitUSTARPath(name string) (prefix, suffix string, ok bool) {
// Write writes to the current entry in the tar archive. // Write writes to the current entry in the tar archive.
// Write returns the error ErrWriteTooLong if more than // Write returns the error ErrWriteTooLong if more than
// hdr.Size bytes are written after WriteHeader. // Header.Size bytes are written after WriteHeader.
func (tw *Writer) Write(b []byte) (n int, err error) { //
if tw.closed { // Calling Write on special types like TypeLink, TypeSymLink, TypeChar,
err = ErrWriteAfterClose // TypeBlock, TypeDir, and TypeFifo returns (0, ErrWriteTooLong) regardless
return // of what the Header.Size claims.
func (tw *Writer) Write(b []byte) (int, error) {
if tw.err != nil {
return 0, tw.err
} }
overwrite := false
if int64(len(b)) > tw.nb { overwrite := int64(len(b)) > tw.nb
b = b[0:tw.nb] if overwrite {
overwrite = true b = b[:tw.nb]
} }
n, err = tw.w.Write(b) n, err := tw.w.Write(b)
tw.nb -= int64(n) tw.nb -= int64(n)
if err == nil && overwrite { if err == nil && overwrite {
err = ErrWriteTooLong return n, ErrWriteTooLong // Non-fatal error
return
} }
tw.err = err tw.err = err
return return n, err
} }
// Close closes the tar archive, flushing any unwritten // Close closes the tar archive, flushing any unwritten
// data to the underlying writer. // data to the underlying writer.
func (tw *Writer) Close() error { func (tw *Writer) Close() error {
if tw.err != nil || tw.closed { if tw.err == ErrWriteAfterClose {
return tw.err return nil
} }
tw.Flush()
tw.closed = true
if tw.err != nil { if tw.err != nil {
return tw.err return tw.err
} }
// trailer: two zero blocks // Trailer: two zero blocks.
for i := 0; i < 2; i++ { err := tw.Flush()
_, tw.err = tw.w.Write(zeroBlock[:]) for i := 0; i < 2 && err == nil; i++ {
if tw.err != nil { _, err = tw.w.Write(zeroBlock[:])
break
}
} }
return tw.err
// Ensure all future actions are invalid.
tw.err = ErrWriteAfterClose
return err // Report IO errors
} }
...@@ -576,40 +576,104 @@ func TestValidTypeflagWithPAXHeader(t *testing.T) { ...@@ -576,40 +576,104 @@ func TestValidTypeflagWithPAXHeader(t *testing.T) {
} }
} }
func TestWriteHeaderOnly(t *testing.T) { // failOnceWriter fails exactly once and then always reports success.
tw := NewWriter(new(bytes.Buffer)) type failOnceWriter bool
hdr := &Header{Name: "dir/", Typeflag: TypeDir}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatalf("WriteHeader() = %v, want nil", err)
}
if _, err := tw.Write([]byte{0x00}); err != ErrWriteTooLong {
t.Fatalf("Write() = %v, want %v", err, ErrWriteTooLong)
}
}
func TestWriteNegativeSize(t *testing.T) { func (w *failOnceWriter) Write(b []byte) (int, error) {
tw := NewWriter(new(bytes.Buffer)) if !*w {
hdr := &Header{Name: "small.txt", Size: -1} return 0, io.ErrShortWrite
if err := tw.WriteHeader(hdr); err != ErrHeader {
t.Fatalf("WriteHeader() = nil, want %v", ErrHeader)
} }
*w = true
return len(b), nil
} }
func TestWriteAfterClose(t *testing.T) { func TestWriterErrors(t *testing.T) {
var buffer bytes.Buffer t.Run("HeaderOnly", func(t *testing.T) {
tw := NewWriter(&buffer) tw := NewWriter(new(bytes.Buffer))
hdr := &Header{Name: "dir/", Typeflag: TypeDir}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatalf("WriteHeader() = %v, want nil", err)
}
if _, err := tw.Write([]byte{0x00}); err != ErrWriteTooLong {
t.Fatalf("Write() = %v, want %v", err, ErrWriteTooLong)
}
})
hdr := &Header{ t.Run("NegativeSize", func(t *testing.T) {
Name: "small.txt", tw := NewWriter(new(bytes.Buffer))
Size: 5, hdr := &Header{Name: "small.txt", Size: -1}
} if err := tw.WriteHeader(hdr); err != ErrHeader {
if err := tw.WriteHeader(hdr); err != nil { t.Fatalf("WriteHeader() = nil, want %v", ErrHeader)
t.Fatalf("Failed to write header: %s", err) }
} })
tw.Close()
if _, err := tw.Write([]byte("Kilts")); err != ErrWriteAfterClose { t.Run("BeforeHeader", func(t *testing.T) {
t.Fatalf("Write: got %v; want ErrWriteAfterClose", err) tw := NewWriter(new(bytes.Buffer))
} if _, err := tw.Write([]byte("Kilts")); err != ErrWriteTooLong {
t.Fatalf("Write() = %v, want %v", err, ErrWriteTooLong)
}
})
t.Run("AfterClose", func(t *testing.T) {
tw := NewWriter(new(bytes.Buffer))
hdr := &Header{Name: "small.txt"}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatalf("WriteHeader() = %v, want nil", err)
}
if err := tw.Close(); err != nil {
t.Fatalf("Close() = %v, want nil", err)
}
if _, err := tw.Write([]byte("Kilts")); err != ErrWriteAfterClose {
t.Fatalf("Write() = %v, want %v", err, ErrWriteAfterClose)
}
if err := tw.Flush(); err != ErrWriteAfterClose {
t.Fatalf("Flush() = %v, want %v", err, ErrWriteAfterClose)
}
if err := tw.Close(); err != nil {
t.Fatalf("Close() = %v, want nil", err)
}
})
t.Run("PrematureFlush", func(t *testing.T) {
tw := NewWriter(new(bytes.Buffer))
hdr := &Header{Name: "small.txt", Size: 5}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatalf("WriteHeader() = %v, want nil", err)
}
if err := tw.Flush(); err == nil {
t.Fatalf("Flush() = %v, want non-nil error", err)
}
})
t.Run("PrematureClose", func(t *testing.T) {
tw := NewWriter(new(bytes.Buffer))
hdr := &Header{Name: "small.txt", Size: 5}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatalf("WriteHeader() = %v, want nil", err)
}
if err := tw.Close(); err == nil {
t.Fatalf("Close() = %v, want non-nil error", err)
}
})
t.Run("Persistence", func(t *testing.T) {
tw := NewWriter(new(failOnceWriter))
if err := tw.WriteHeader(&Header{}); err != io.ErrShortWrite {
t.Fatalf("WriteHeader() = %v, want %v", err, io.ErrShortWrite)
}
if err := tw.WriteHeader(&Header{Name: "small.txt"}); err == nil {
t.Errorf("WriteHeader() = got %v, want non-nil error", err)
}
if _, err := tw.Write(nil); err == nil {
t.Errorf("Write() = %v, want non-nil error", err)
}
if err := tw.Flush(); err == nil {
t.Errorf("Flush() = %v, want non-nil error", err)
}
if err := tw.Close(); err == nil {
t.Errorf("Close() = %v, want non-nil error", err)
}
})
} }
func TestSplitUSTARPath(t *testing.T) { func TestSplitUSTARPath(t *testing.T) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment