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
28 changes: 28 additions & 0 deletions src/archive/zip/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,34 @@ func (w *Writer) CreateRaw(fh *FileHeader) (io.Writer, error) {
fh.CompressedSize = uint32(min(fh.CompressedSize64, uint32max))
fh.UncompressedSize = uint32(min(fh.UncompressedSize64, uint32max))

// If Modified is set, this takes precedence over MS-DOS timestamp fields.
if !fh.Modified.IsZero() {
// Contrary to the FileHeader.SetModTime method, we intentionally
// do not convert to UTC, because we assume the user intends to encode
// the date using the specified timezone. A user may want this control
// because many legacy ZIP readers interpret the timestamp according
// to the local timezone.
//
// The timezone is only non-UTC if a user directly sets the Modified
// field directly themselves. All other approaches sets UTC.
fh.ModifiedDate, fh.ModifiedTime = timeToMsDosTime(fh.Modified)

// Use "extended timestamp" format since this is what Info-ZIP uses.
// Nearly every major ZIP implementation uses a different format,
// but at least most seem to be able to understand the other formats.
//
// This format happens to be identical for both local and central header
// if modification time is the only timestamp being encoded.
var mbuf [9]byte // 2*SizeOf(uint16) + SizeOf(uint8) + SizeOf(uint32)
mt := uint32(fh.Modified.Unix())
eb := writeBuf(mbuf[:])
eb.uint16(extTimeExtraID)
eb.uint16(5) // Size: SizeOf(uint8) + SizeOf(uint32)
eb.uint8(1) // Flags: ModTime
eb.uint32(mt) // ModTime
fh.Extra = append(fh.Extra, mbuf[:]...)
}

h := &header{
FileHeader: fh,
offset: uint64(w.cw.count),
Expand Down
43 changes: 43 additions & 0 deletions src/archive/zip/writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,49 @@ func TestWriterCreateRaw(t *testing.T) {
}
}

func TestWriterCreateRawTime(t *testing.T) {
// Test that CreateRaw honors the Modified field in FileHeader.
// See https://go.dev/issue/76741
content := []byte("test content")
modified := time.Date(2023, 6, 15, 10, 30, 45, 0, time.UTC)

// Write a zip file using CreateRaw with Modified set.
var buf bytes.Buffer
w := NewWriter(&buf)

h := &FileHeader{
Name: "test.txt",
Method: Store,
CRC32: crc32.ChecksumIEEE(content),
CompressedSize64: uint64(len(content)),
UncompressedSize64: uint64(len(content)),
Modified: modified,
}
raw, err := w.CreateRaw(h)
if err != nil {
t.Fatalf("CreateRaw: %v", err)
}
if _, err := raw.Write(content); err != nil {
t.Fatalf("Write: %v", err)
}
if err := w.Close(); err != nil {
t.Fatalf("Close: %v", err)
}

// Read it back and verify the Modified time.
r, err := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
if err != nil {
t.Fatalf("NewReader: %v", err)
}
if len(r.File) != 1 {
t.Fatalf("got %d files; want 1", len(r.File))
}
got := r.File[0]
if !got.Modified.Equal(modified) {
t.Errorf("Modified = %v; want %v", got.Modified, modified)
}
}

func testCreate(t *testing.T, w *Writer, wt *WriteTest) {
header := &FileHeader{
Name: wt.Name,
Expand Down