archive/zip: honor Modified field in CreateRaw
CreateRaw was ignoring the Modified field in FileHeader, requiring users to call the deprecated SetModTime method as a workaround.
This change adds the same timestamp handling logic to CreateRaw that CreateHeader already uses.
Fixes #76741
diff --git a/src/archive/zip/writer.go b/src/archive/zip/writer.go
index 0a31005..b55b516 100644
--- a/src/archive/zip/writer.go
+++ b/src/archive/zip/writer.go
@@ -446,6 +446,34 @@
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),
diff --git a/src/archive/zip/writer_test.go b/src/archive/zip/writer_test.go
index 44592ce..4125502 100644
--- a/src/archive/zip/writer_test.go
+++ b/src/archive/zip/writer_test.go
@@ -533,6 +533,49 @@
}
}
+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,
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
I spotted some possible problems with your PR:
1. You have a long 130 character line in the commit message body. Please add line breaks to long lines that should be wrapped. Lines in the commit message body should be wrapped at ~76 characters unless needed for things like URLs or tables. (Note: GitHub might render long lines as soft-wrapped, so double-check in the Gerrit commit message shown above.)
Please address any problems by updating the GitHub PR.
When complete, mark this comment as 'Done' and click the [blue 'Reply' button](https://go.dev/wiki/GerritBot#i-left-a-reply-to-a-comment-in-gerrit-but-no-one-but-me-can-see-it) above. These findings are based on heuristics; if a finding does not apply, briefly reply here saying so.
To update the commit title or commit message body shown here in Gerrit, you must edit the GitHub PR title and PR description (the first comment) in the GitHub web interface using the 'Edit' button or 'Edit' menu entry there. Note: pushing a new commit to the PR will not automatically update the commit message used by Gerrit.
For more details, see:
(In general for Gerrit code reviews, the change author is expected to [log in to Gerrit](https://go-review.googlesource.com/login/) with a Gmail or other Google account and then close out each piece of feedback by marking it as 'Done' if implemented as suggested or otherwise reply to each review comment. See the [Review](https://go.dev/doc/contribute#review) section of the Contributing Guide for details.)
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |