Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion src/archive/zip/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package zip

import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"hash"
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down
36 changes: 36 additions & 0 deletions src/archive/zip/writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}