diff --git a/src/archive/zip/writer.go b/src/archive/zip/writer.go index 0a310054e37678..a19bf47657c1f5 100644 --- a/src/archive/zip/writer.go +++ b/src/archive/zip/writer.go @@ -6,6 +6,7 @@ package zip import ( "bufio" + "bytes" "encoding/binary" "errors" "hash" @@ -30,6 +31,10 @@ type Writer struct { compressors map[uint16]Compressor comment string + // wa holds the underlying writer if it supports WriteAt, used for + // writing local file header versions for ZIP64 entries. + wa io.WriterAt + // testHookCloseSizeOffset if non-nil is called with the size // of offset of the central directory at Close. testHookCloseSizeOffset func(size, offset uint64) @@ -41,9 +46,29 @@ type header struct { raw bool } +// bufferWriterAt wraps a *bytes.Buffer to implement io.WriterAt. +// This enables ZIP64 local file header writing for in-memory ZIP creation. +type bufferWriterAt struct { + buf *bytes.Buffer +} + +func (b *bufferWriterAt) WriteAt(p []byte, off int64) (int, error) { + data := b.buf.Bytes() + if int(off)+len(p) > len(data) { + return 0, errors.New("zip: write beyond buffer length") + } + return copy(data[off:], p), nil +} + // NewWriter returns a new [Writer] writing a zip file to w. func NewWriter(w io.Writer) *Writer { - return &Writer{cw: &countWriter{w: bufio.NewWriter(w)}} + zw := &Writer{cw: &countWriter{w: bufio.NewWriter(w)}} + if wa, ok := w.(io.WriterAt); ok { + zw.wa = wa + } else if buf, ok := w.(*bytes.Buffer); ok { + zw.wa = &bufferWriterAt{buf: buf} + } + return zw } // SetOffset sets the offset of the beginning of the zip data within the @@ -57,6 +82,33 @@ func (w *Writer) SetOffset(n int64) { w.cw.count = n } +// writeZip64LFH writes the local file header (LFH) version field for ZIP64 +// entries to ensure consistency with the central directory. The LFH is +// written before the file data when the final size is unknown, so it uses +// version 20. After the data is written, if the entry exceeds 4GB, the +// central directory uses version 45. This method write each ZIP64 entry's +// LFH to update the version to 45. +func (w *Writer) writeZip64LFH() error { + if w.wa == nil { + return nil + } + if err := w.cw.w.(*bufio.Writer).Flush(); err != nil { + return err + } + var versionBuf [2]byte + binary.LittleEndian.PutUint16(versionBuf[:], zipVersion45) + for _, h := range w.dir { + if h.isZip64() { + // LFH structure: signature(4) + version(2) + ... + // Write version field at offset + 4 + if _, err := w.wa.WriteAt(versionBuf[:], int64(h.offset)+4); err != nil { + return err + } + } + } + return nil +} + // Flush flushes any buffered data to the underlying writer. // Calling Flush is not normally necessary; calling Close is sufficient. func (w *Writer) Flush() error { @@ -87,6 +139,13 @@ func (w *Writer) Close() error { } w.closed = true + // write local file header versions for ZIP64 entries to ensure + // consistency with the central directory. This is required by strict + // ZIP readers. + if err := w.writeZip64LFH(); err != nil { + return err + } + // write central directory start := w.cw.count for _, h := range w.dir { @@ -446,6 +505,11 @@ func (w *Writer) CreateRaw(fh *FileHeader) (io.Writer, error) { fh.CompressedSize = uint32(min(fh.CompressedSize64, uint32max)) fh.UncompressedSize = uint32(min(fh.UncompressedSize64, uint32max)) + // Set version 45 for ZIP64 entries since sizes are known upfront + if fh.isZip64() { + fh.ReaderVersion = zipVersion45 + } + h := &header{ FileHeader: fh, offset: uint64(w.cw.count), diff --git a/src/archive/zip/writer_test.go b/src/archive/zip/writer_test.go index 44592ce8318826..fe9f29ee0c2fb0 100644 --- a/src/archive/zip/writer_test.go +++ b/src/archive/zip/writer_test.go @@ -671,3 +671,39 @@ func TestIssue61875(t *testing.T) { t.Errorf("expected error, got nil") } } + +// TestZip64LFHVersion tests that the Local File Header version is written +// to 45 for ZIP64 entries to match the Central Directory version. +func TestZip64LFHVersion(t *testing.T) { + buf := new(bytes.Buffer) + w := NewWriter(buf) + fh := &FileHeader{ + Name: "large_file.bin", + Method: Store, + CRC32: 0x12345678, + CompressedSize64: uint32max + 1, + UncompressedSize64: uint32max + 1, + } + if _, err := w.CreateRaw(fh); err != nil { + t.Fatalf("CreateRaw failed: %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("Close failed: %v", err) + } + data := buf.Bytes() + // Find Local File Header (signature 0x04034b50) + lfhIdx := bytes.Index(data, []byte{0x50, 0x4b, 0x03, 0x04}) + if lfhIdx == -1 { + t.Fatal("local file header not found") + } + lfhVersion := binary.LittleEndian.Uint16(data[lfhIdx+4 : lfhIdx+6]) + // Find Central Directory Header (signature 0x02014b50) + cdIdx := bytes.Index(data, []byte{0x50, 0x4b, 0x01, 0x02}) + if cdIdx == -1 { + t.Fatal("central directory header not found") + } + cdVersion := binary.LittleEndian.Uint16(data[cdIdx+6 : cdIdx+8]) + if lfhVersion != 45 || cdVersion != 45 { + t.Errorf("version mismatch: LFH=%d, CD=%d, want both 45", lfhVersion, cdVersion) + } +}