| // Copyright 2026 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package zip | |
| import ( | |
| "bytes" | |
| "encoding/binary" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "path/filepath" | |
| "slices" | |
| "strings" | |
| "testing" | |
| ) | |
| // TestZip64WriterCDGoldens checks that the archive/zip Writer emits a Central | |
| // Directory that matches the Zip64 conventions used by Info-ZIP, libarchive, | |
| // and the pre-CL archive/zip writer (go126-*), for archives at or above 4 GiB, | |
| // except where we intentionally diverged. | |
| // | |
| // For each golden in testdata/zip64/*.zsparse (see [sparseFile] for the | |
| // committed format), the test: | |
| // 1. Parses the golden's CD into a producer-independent snapshot — which | |
| // fields hold 0xFFFFFFFF placeholders, which Zip64 extra sub-fields are | |
| // present and in what order, and the EOCD/EOCD64 values. | |
| // 2. Verifies the production [NewReader] parses the same archive. | |
| // 3. Replays the same entries through a fresh [Writer] into a [sparseBuffer] | |
| // and parses our own CD. | |
| // 4. Verifies the production [NewReader] parses our reproduced archive too. | |
| // 5. Compares the two snapshots field-by-field, ignoring producer-specific | |
| // details (creator version, external attrs, non-Zip64 extras, absolute | |
| // byte offsets that depend on LFH/data-descriptor layout). | |
| func TestZip64WriterCDGoldens(t *testing.T) { | |
| if testing.Short() { | |
| t.Skip("skipping in short mode; each golden replays a multi-GiB write") | |
| } | |
| matches, err := filepath.Glob("testdata/zip64/*.zsparse") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(matches) == 0 { | |
| t.Fatal("missing Zip64 goldens in testdata/zip64") | |
| } | |
| // Tail materialized for parseCD. Goldens have ≤ 2 entries; their CD | |
| // plus EOCD records fits in well under 1 MiB. | |
| const tailKeep = 1 << 20 | |
| // archive/zip's writer takes the most defensive position on every | |
| // spec-fuzzy point: it always emits the Zip64 extra at the 0xFFFFFFFF | |
| // boundary (matching libarchive but more conservative than Info-ZIP) AND | |
| // emits EOCD64 whenever any entry has a Zip64 extra in its CD record | |
| // (matching Info-ZIP but more conservative than libarchive). The go126- | |
| // goldens are output of an older archive/zip writer, and the format | |
| // deliberately diverges; they are kept here so the reader-side check | |
| // enforces backwards compatibility with archives produced by our own past | |
| // writer, and to ensure we only diverge where intended. | |
| expectedDiff := map[string]bool{ | |
| // Info-ZIP treats a CD size field of exactly 0xFFFFFFFF as a real | |
| // value and omits the Zip64 extra; archive/zip defensively emits | |
| // the Zip64 extra with USize64+CSize64. | |
| "infozip-store-4g-minus-1": true, | |
| // Info-ZIP treats a CD offset field of exactly 0xFFFFFFFF as a real | |
| // value and omits the Zip64 extra for offset; archive/zip defensively | |
| // emits the Zip64 extra with the offset sub-field. | |
| "infozip-offset-eq-4g": true, | |
| // libarchive's writer emits EOCD64 only on EOCD-level overflow (CD | |
| // size/offset > 4GiB, records > 0xFFFF); archive/zip also emits | |
| // EOCD64 when any per-entry CD record uses a Zip64 extra, even if | |
| // the EOCD fields fit in 32 bits. | |
| "libarchive-deflate-zeros-5g": true, | |
| // libarchive's LFH always carries a UT timestamp extra (~9 bytes), | |
| // so its dirOffset for a body of 4GiB-59 lands just past 0xFFFFFFFF | |
| // and it emits EOCD64. archive/zip's streaming LFH has no such | |
| // extras and stays under uint32max. | |
| "libarchive-store-just-under-4g": true, | |
| // The old archive/zip writer differs from the current writer on | |
| // every Zip64-using entry: it always wrote a fixed 24-byte Zip64 | |
| // extra with all three sub-fields (usize, csize, offset) and set | |
| // both 32-bit size fields to 0xFFFFFFFF whenever the per-entry | |
| // trigger fired; it also set the EOCD records/size/offset to the | |
| // placeholder values whenever EOCD64 was present. | |
| "go126-store-5g": true, | |
| "go126-deflate-zeros-5g": true, | |
| "go126-store-4g-minus-1": true, | |
| "go126-store-4g-minus-2": true, | |
| "go126-store-exact-4g": true, | |
| "go126-offset-past-4g": true, | |
| "go126-offset-eq-4g": true, | |
| "go126-store-just-under-4g": false, | |
| } | |
| for _, path := range matches { | |
| name := strings.TrimSuffix(filepath.Base(path), ".zsparse") | |
| t.Run(name, func(t *testing.T) { | |
| t.Parallel() | |
| goldenSF, err := readSparseFile(path) | |
| if err != nil { | |
| t.Fatalf("read golden: %v", err) | |
| } | |
| goldenData, goldenBase := goldenSF.materializeTail(tailKeep) | |
| golden, err := parseCD(goldenData, goldenBase) | |
| if err != nil { | |
| t.Fatalf("parse golden CD: %v", err) | |
| } | |
| // Verify the production Reader can parse the full golden. | |
| checkReaderMatchesSnapshot(t, "golden", goldenSF, golden) | |
| oursSF := reproduceCD(t, golden) | |
| oursData, oursBase := oursSF.materializeTail(tailKeep) | |
| got, err := parseCD(oursData, oursBase) | |
| if err != nil { | |
| t.Fatalf("parse reproduced CD: %v\nbytes:\n%s", err, hexDump(oursData)) | |
| } | |
| // Verify the production Reader can parse archive/zip's own | |
| // output and gets the same view of the entries. | |
| checkReaderMatchesSnapshot(t, "reproduced", oursSF, got) | |
| if expectedDiff[name] { | |
| var cap captureReporter | |
| compareCDSnapshots(&cap, golden, got) | |
| if !cap.failed { | |
| t.Errorf("expected this golden to fail equivalence, but it passed") | |
| } else { | |
| t.Logf("expected mismatch:\n%s", indent(cap.msg.String(), " ")) | |
| } | |
| return | |
| } | |
| compareCDSnapshots(t, golden, got) | |
| }) | |
| } | |
| } | |
| // errReporter is the subset of [testing.TB] that [compareCDSnapshots] uses. | |
| // The captureReporter implementation lets the test capture mismatches for | |
| // expected-failure cases instead of propagating them to the outer t. | |
| type errReporter interface { | |
| Errorf(format string, args ...any) | |
| Helper() | |
| } | |
| type captureReporter struct { | |
| failed bool | |
| msg strings.Builder | |
| } | |
| func (c *captureReporter) Errorf(format string, args ...any) { | |
| c.failed = true | |
| fmt.Fprintf(&c.msg, format+"\n", args...) | |
| } | |
| func (c *captureReporter) Helper() {} | |
| // checkReaderMatchesSnapshot opens the archive backed by the sparseFile | |
| // using the production [NewReader] and asserts that the entry list it | |
| // returns matches the [cdSnapshot] (entry count, names, resolved 64-bit | |
| // sizes). | |
| func checkReaderMatchesSnapshot(t *testing.T, label string, f *sparseFile, snap *cdSnapshot) { | |
| t.Helper() | |
| zr, err := NewReader(f, f.Size) | |
| if err != nil { | |
| t.Fatalf("%s: NewReader: %v", label, err) | |
| } | |
| if g, w := len(zr.File), len(snap.Entries); g != w { | |
| t.Errorf("%s: NewReader returned %d files, parseCD found %d", label, g, w) | |
| return | |
| } | |
| for i, f := range zr.File { | |
| want := &snap.Entries[i] | |
| if f.Name != want.Name { | |
| t.Errorf("%s entry %d: Name = %q, want %q", label, i, f.Name, want.Name) | |
| } | |
| if f.UncompressedSize64 != want.USize64 { | |
| t.Errorf("%s entry %d %q: UncompressedSize64 = %d, want %d", label, i, want.Name, f.UncompressedSize64, want.USize64) | |
| } | |
| if f.CompressedSize64 != want.CSize64 { | |
| t.Errorf("%s entry %d %q: CompressedSize64 = %d, want %d", label, i, want.Name, f.CompressedSize64, want.CSize64) | |
| } | |
| } | |
| } | |
| // indent prefixes every line of s with prefix. | |
| func indent(s, prefix string) string { | |
| if s == "" { | |
| return s | |
| } | |
| lines := strings.Split(strings.TrimRight(s, "\n"), "\n") | |
| for i, l := range lines { | |
| lines[i] = prefix + l | |
| } | |
| return strings.Join(lines, "\n") + "\n" | |
| } | |
| // reproduceCD writes a zip archive with the same logical entries as golden | |
| // into a [sparseBuffer] (which drops all-zero chunks, so pushing multi-GiB | |
| // streams of zeros through the writer is essentially free) and returns the | |
| // resulting [sparseFile]. | |
| // | |
| // For entries where compressed == uncompressed (Store, or other 1:1 cases) | |
| // we drive the Writer through [Writer.CreateHeader] so that the data | |
| // descriptor, offset accounting, and Close-time CD emission all exercise | |
| // the production streaming path. The CRC32 hasher is replaced with | |
| // [fakeHash32] to avoid hashing many GiB of zeros. | |
| // | |
| // For entries where compressed ≪ uncompressed (Method=Deflate over zeros), | |
| // actually deflating multi-GiB streams at test time is prohibitively slow, | |
| // so we fall back to [Writer.CreateRaw] and declare the sizes directly. | |
| // The Central Directory output is identical either way. | |
| func reproduceCD(t *testing.T, golden *cdSnapshot) *sparseFile { | |
| t.Helper() | |
| sb := &sparseBuffer{} | |
| w := NewWriter(sb) | |
| for i, e := range golden.Entries { | |
| if e.CSize64 == e.USize64 { | |
| fh := &FileHeader{Name: e.Name, Method: e.Method} | |
| fw, err := w.CreateHeader(fh) | |
| if err != nil { | |
| t.Fatalf("CreateHeader[%d %q]: %v", i, e.Name, err) | |
| } | |
| fw.(*fileWriter).crc32 = fakeHash32{} | |
| if _, err := io.CopyN(fw, zeros{}, int64(e.USize64)); err != nil { | |
| t.Fatalf("CopyN[%d %q]: %v", i, e.Name, err) | |
| } | |
| continue | |
| } | |
| fh := &FileHeader{ | |
| Name: e.Name, | |
| Method: e.Method, | |
| CompressedSize64: e.CSize64, | |
| UncompressedSize64: e.USize64, | |
| } | |
| fw, err := w.CreateRaw(fh) | |
| if err != nil { | |
| t.Fatalf("CreateRaw[%d %q]: %v", i, e.Name, err) | |
| } | |
| if _, err := io.CopyN(fw, zeros{}, int64(e.CSize64)); err != nil { | |
| t.Fatalf("CopyN[%d %q]: %v", i, e.Name, err) | |
| } | |
| } | |
| if err := w.Close(); err != nil { | |
| t.Fatalf("Close: %v", err) | |
| } | |
| return &sb.f | |
| } | |
| // compareCDSnapshots asserts that got matches want on Zip64-relevant fields. | |
| // | |
| // Per-entry size fields (RawCSize, RawUSize, CSize64, USize64) are compared | |
| // exactly — we feed them in from the golden when reproducing, so the writer | |
| // has no excuse to disagree. Per-entry RawOffset and the EOCD records/size/ | |
| // offset fields are compared only as placeholder-or-not: their absolute | |
| // values depend on producer-specific LFH layout (Info-ZIP packs sizes into | |
| // the LFH; archive/zip's streaming path uses a data descriptor; libarchive | |
| // adds UT extras) and that's not what this test is pinning down. | |
| func compareCDSnapshots(t errReporter, want, got *cdSnapshot) { | |
| t.Helper() | |
| if g, w := len(got.Entries), len(want.Entries); g != w { | |
| t.Errorf("entry count = %d, want %d", g, w) | |
| return | |
| } | |
| for i := range want.Entries { | |
| we, ge := &want.Entries[i], &got.Entries[i] | |
| // csize and usize come from the declared FileHeader values, so the | |
| // raw 32-bit fields must match exactly (real value vs. placeholder | |
| // choice and, when not placeholder, the value itself). | |
| if we.RawCSize != ge.RawCSize { | |
| t.Errorf("entry %d %q: RawCSize = %#08x, want %#08x", i, we.Name, ge.RawCSize, we.RawCSize) | |
| } | |
| if we.RawUSize != ge.RawUSize { | |
| t.Errorf("entry %d %q: RawUSize = %#08x, want %#08x", i, we.Name, ge.RawUSize, we.RawUSize) | |
| } | |
| // Resolved csize/usize must match — we fed them in from the golden. | |
| if we.CSize64 != ge.CSize64 { | |
| t.Errorf("entry %d %q: CSize64 = %d, want %d", i, we.Name, ge.CSize64, we.CSize64) | |
| } | |
| if we.USize64 != ge.USize64 { | |
| t.Errorf("entry %d %q: USize64 = %d, want %d", i, we.Name, ge.USize64, we.USize64) | |
| } | |
| // Offset is layout-dependent. Compare placeholder-or-not, not value. | |
| if isPlaceholder32(we.RawOffset) != isPlaceholder32(ge.RawOffset) { | |
| t.Errorf("entry %d %q: RawOffset placeholder = %#08x, want %#08x", i, we.Name, ge.RawOffset, we.RawOffset) | |
| } | |
| // Zip64 sub-field presence/order, must match exactly. | |
| if !slices.Equal(we.Z64ExtraFields, ge.Z64ExtraFields) { | |
| t.Errorf("entry %d %q: Zip64 sub-field order = %v, want %v", i, we.Name, ge.Z64ExtraFields, we.Z64ExtraFields) | |
| } | |
| // ReaderVersion ≥ 45 whenever a Zip64 extra is present. | |
| if len(we.Z64ExtraFields) > 0 && ge.ReaderVersion < zipVersion45 { | |
| t.Errorf("entry %d %q: ReaderVersion = %d, want ≥ %d (Zip64 extra present)", i, we.Name, ge.ReaderVersion, zipVersion45) | |
| } | |
| } | |
| // EOCD: compare placeholder-or-not for each field. Exact values are | |
| // layout-dependent. | |
| if isPlaceholder16(want.EOCD.Records) != isPlaceholder16(got.EOCD.Records) { | |
| t.Errorf("EOCD records placeholder = %#x, want %#x", got.EOCD.Records, want.EOCD.Records) | |
| } | |
| if isPlaceholder32(want.EOCD.Size) != isPlaceholder32(got.EOCD.Size) { | |
| t.Errorf("EOCD size placeholder = %#x, want %#x", got.EOCD.Size, want.EOCD.Size) | |
| } | |
| if isPlaceholder32(want.EOCD.Offset) != isPlaceholder32(got.EOCD.Offset) { | |
| t.Errorf("EOCD offset placeholder = %#x, want %#x", got.EOCD.Offset, want.EOCD.Offset) | |
| } | |
| if got.HasEOCD64 != want.HasEOCD64 { | |
| t.Errorf("EOCD64 present = %v, want %v", got.HasEOCD64, want.HasEOCD64) | |
| } | |
| if want.HasEOCD64 && got.HasEOCD64 { | |
| if got.EOCD64.Records != want.EOCD64.Records { | |
| t.Errorf("EOCD64 records = %d, want %d", got.EOCD64.Records, want.EOCD64.Records) | |
| } | |
| // EOCD64.Size and EOCD64.Offset are layout-dependent. | |
| } | |
| } | |
| func isPlaceholder32(v uint32) bool { return v == uint32max } | |
| func isPlaceholder16(v uint16) bool { return v == uint16max } | |
| // CD snapshot types and parser | |
| // zip64SubID identifies one of the three sub-fields that may appear in a | |
| // Zip64 extended-information extra field, in the spec-defined order. | |
| type zip64SubID int | |
| const ( | |
| z64USize zip64SubID = iota + 1 | |
| z64CSize | |
| z64Offset | |
| ) | |
| func (s zip64SubID) String() string { | |
| switch s { | |
| case z64USize: | |
| return "usize" | |
| case z64CSize: | |
| return "csize" | |
| case z64Offset: | |
| return "offset" | |
| } | |
| return fmt.Sprintf("zip64SubID(%d)", int(s)) | |
| } | |
| type cdEntry struct { | |
| Name string | |
| Method uint16 | |
| ReaderVersion uint16 | |
| // Raw 32-bit fields from the CD record. A value of 0xFFFFFFFF indicates | |
| // the real value is in the Zip64 extended-information extra field. | |
| RawCSize uint32 | |
| RawUSize uint32 | |
| RawOffset uint32 | |
| // Resolved 64-bit values (from the 32-bit field if not a placeholder, | |
| // otherwise from the Zip64 extra). | |
| CSize64 uint64 | |
| USize64 uint64 | |
| Offset64 uint64 | |
| // Sub-fields present in the Zip64 extra, in the order they appear. | |
| Z64ExtraFields []zip64SubID | |
| } | |
| type eocdRec struct { | |
| Records uint16 // 0xFFFF if placeholder | |
| Size uint32 // 0xFFFFFFFF if placeholder | |
| Offset uint32 // 0xFFFFFFFF if placeholder | |
| } | |
| type eocd64Rec struct { | |
| Records uint64 | |
| Size uint64 | |
| Offset uint64 | |
| } | |
| type cdSnapshot struct { | |
| Entries []cdEntry | |
| EOCD eocdRec | |
| HasEOCD64 bool | |
| EOCD64 eocd64Rec | |
| } | |
| var le = binary.LittleEndian | |
| // parseCD parses the Central Directory and EOCD records of a zip archive | |
| // from its raw bytes. data must be the tail of the archive, with baseOffset | |
| // indicating where data[0] sits in the original archive (0 for whole-archive | |
| // input). | |
| func parseCD(data []byte, baseOffset uint64) (*cdSnapshot, error) { | |
| sigOff, err := findEOCD(data) | |
| if err != nil { | |
| return nil, err | |
| } | |
| snap := &cdSnapshot{} | |
| snap.EOCD.Records = le.Uint16(data[sigOff+10:]) | |
| snap.EOCD.Size = le.Uint32(data[sigOff+12:]) | |
| snap.EOCD.Offset = le.Uint32(data[sigOff+16:]) | |
| dirOffset := uint64(snap.EOCD.Offset) | |
| nRecords := uint64(snap.EOCD.Records) | |
| // toData converts an absolute archive offset to a data slice offset, | |
| // returning false if it lies before our captured tail. | |
| toData := func(absOff uint64) (uint64, bool) { | |
| if absOff < baseOffset { | |
| return 0, false | |
| } | |
| return absOff - baseOffset, true | |
| } | |
| // Look for an EOCD64 locator immediately preceding the EOCD record. | |
| if sigOff >= directory64LocLen { | |
| locOff := sigOff - directory64LocLen | |
| if le.Uint32(data[locOff:]) == directory64LocSignature { | |
| eocd64Off := le.Uint64(data[locOff+8:]) | |
| eocd64DataOff, ok := toData(eocd64Off) | |
| if !ok { | |
| return nil, fmt.Errorf("zip: EOCD64 at %#x before captured tail (base %#x)", eocd64Off, baseOffset) | |
| } | |
| if eocd64DataOff+directory64EndLen > uint64(len(data)) { | |
| return nil, errors.New("zip: EOCD64 offset out of range") | |
| } | |
| if le.Uint32(data[eocd64DataOff:]) != directory64EndSignature { | |
| return nil, errors.New("zip: EOCD64 signature mismatch") | |
| } | |
| snap.HasEOCD64 = true | |
| snap.EOCD64.Records = le.Uint64(data[eocd64DataOff+32:]) | |
| snap.EOCD64.Size = le.Uint64(data[eocd64DataOff+40:]) | |
| snap.EOCD64.Offset = le.Uint64(data[eocd64DataOff+48:]) | |
| dirOffset = snap.EOCD64.Offset | |
| nRecords = snap.EOCD64.Records | |
| } | |
| } | |
| off, ok := toData(dirOffset) | |
| if !ok { | |
| return nil, fmt.Errorf("zip: CD at %#x before captured tail (base %#x)", dirOffset, baseOffset) | |
| } | |
| for i := uint64(0); i < nRecords; i++ { | |
| if off+directoryHeaderLen > uint64(len(data)) { | |
| return nil, fmt.Errorf("zip: CD entry %d out of range", i) | |
| } | |
| rec := data[off:] | |
| if le.Uint32(rec) != directoryHeaderSignature { | |
| return nil, fmt.Errorf("zip: bad CD signature at offset %d", off) | |
| } | |
| var e cdEntry | |
| e.ReaderVersion = le.Uint16(rec[6:]) | |
| e.Method = le.Uint16(rec[10:]) | |
| e.RawCSize = le.Uint32(rec[20:]) | |
| e.RawUSize = le.Uint32(rec[24:]) | |
| nameLen := uint64(le.Uint16(rec[28:])) | |
| extraLen := uint64(le.Uint16(rec[30:])) | |
| commLen := uint64(le.Uint16(rec[32:])) | |
| e.RawOffset = le.Uint32(rec[42:]) | |
| recLen := uint64(directoryHeaderLen) + nameLen + extraLen + commLen | |
| if off+recLen > uint64(len(data)) { | |
| return nil, fmt.Errorf("zip: CD entry %d truncated", i) | |
| } | |
| nameOff := off + directoryHeaderLen | |
| extraOff := nameOff + nameLen | |
| e.Name = string(data[nameOff:extraOff]) | |
| extra := data[extraOff : extraOff+extraLen] | |
| e.CSize64 = uint64(e.RawCSize) | |
| e.USize64 = uint64(e.RawUSize) | |
| e.Offset64 = uint64(e.RawOffset) | |
| // Walk extra fields; consume the Zip64 sub-field if present. | |
| // Per the spec and Info-ZIP convention, the Zip64 extra contains | |
| // 8-byte values for exactly the size/offset fields whose 32-bit | |
| // counterpart is 0xFFFFFFFF, in the order: USize, CSize, Offset. | |
| for len(extra) >= 4 { | |
| tag := le.Uint16(extra) | |
| size := uint64(le.Uint16(extra[2:])) | |
| if 4+size > uint64(len(extra)) { | |
| break | |
| } | |
| field := extra[4 : 4+size] | |
| extra = extra[4+size:] | |
| if tag != zip64ExtraID { | |
| continue | |
| } | |
| if e.RawUSize == uint32max && len(field) >= 8 { | |
| e.USize64 = le.Uint64(field) | |
| e.Z64ExtraFields = append(e.Z64ExtraFields, z64USize) | |
| field = field[8:] | |
| } | |
| if e.RawCSize == uint32max && len(field) >= 8 { | |
| e.CSize64 = le.Uint64(field) | |
| e.Z64ExtraFields = append(e.Z64ExtraFields, z64CSize) | |
| field = field[8:] | |
| } | |
| if e.RawOffset == uint32max && len(field) >= 8 { | |
| e.Offset64 = le.Uint64(field) | |
| e.Z64ExtraFields = append(e.Z64ExtraFields, z64Offset) | |
| field = field[8:] | |
| } | |
| } | |
| snap.Entries = append(snap.Entries, e) | |
| off += recLen | |
| } | |
| return snap, nil | |
| } | |
| // findEOCD locates the EOCD record by scanning back from the end of data, | |
| // matching both the signature and the trailing comment-length field. | |
| func findEOCD(data []byte) (uint64, error) { | |
| if len(data) < directoryEndLen { | |
| return 0, errors.New("zip: too short for EOCD") | |
| } | |
| maxComment := uint16max | |
| lo := len(data) - directoryEndLen | |
| hi := lo | |
| if hi > maxComment { | |
| lo = hi - maxComment | |
| } else { | |
| lo = 0 | |
| } | |
| for i := hi; i >= lo; i-- { | |
| if le.Uint32(data[i:]) != directoryEndSignature { | |
| continue | |
| } | |
| cl := int(le.Uint16(data[i+20:])) | |
| if i+directoryEndLen+cl == len(data) { | |
| return uint64(i), nil | |
| } | |
| } | |
| return 0, errors.New("zip: EOCD not found") | |
| } | |
| // hexDump returns a short hex dump of data for failure messages. | |
| func hexDump(data []byte) string { | |
| if len(data) > 4096 { | |
| data = data[len(data)-4096:] | |
| } | |
| var b strings.Builder | |
| for i := 0; i < len(data); i += 16 { | |
| end := min(i+16, len(data)) | |
| fmt.Fprintf(&b, "%04x % x\n", i, data[i:end]) | |
| } | |
| return b.String() | |
| } | |
| // TestZip64LFHBothPlaceholders covers the [Writer.CreateRaw] + no-data- | |
| // descriptor path where the entry's uncompressed or compressed size exceeds | |
| // 4 GiB. The Local File Header carries a Zip64 extra with both 8-byte | |
| // USize64 and CSize64 sub-fields (matching Info-ZIP), so per APPNOTE 4.5.3 | |
| // both 32-bit size fields in the LFH must be the 0xFFFFFFFF placeholder — | |
| // even if only one of the sizes actually overflows. | |
| func TestZip64LFHBothPlaceholders(t *testing.T) { | |
| var buf bytes.Buffer | |
| w := NewWriter(&buf) | |
| fh := &FileHeader{ | |
| Name: "x", | |
| Method: Deflate, | |
| CompressedSize64: 1024, | |
| UncompressedSize64: 5 << 30, // > 4 GiB | |
| } | |
| fw, err := w.CreateRaw(fh) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if _, err := io.CopyN(fw, zeros{}, int64(fh.CompressedSize64)); err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := w.Close(); err != nil { | |
| t.Fatal(err) | |
| } | |
| b := buf.Bytes() | |
| if got := le.Uint32(b[14:18]); got != fh.CRC32 { | |
| t.Errorf("LFH CRC32 = %#x, want %#x", got, fh.CRC32) | |
| } | |
| if got := le.Uint32(b[18:22]); got != uint32max { | |
| t.Errorf("LFH CompressedSize = %#x, want %#x (placeholder)", got, uint32(uint32max)) | |
| } | |
| if got := le.Uint32(b[22:26]); got != uint32max { | |
| t.Errorf("LFH UncompressedSize = %#x, want %#x (placeholder)", got, uint32(uint32max)) | |
| } | |
| // The Zip64 LFH extra should carry both 64-bit sub-fields in | |
| // USize64-then-CSize64 order. | |
| nameLen := uint64(le.Uint16(b[26:28])) | |
| extraLen := uint64(le.Uint16(b[28:30])) | |
| if want := uint64(20); extraLen != want { | |
| t.Fatalf("LFH extra length = %d, want %d", extraLen, want) | |
| } | |
| extra := b[30+nameLen : 30+nameLen+extraLen] | |
| if tag := le.Uint16(extra[:2]); tag != zip64ExtraID { | |
| t.Errorf("Zip64 extra tag = %#x, want %#x", tag, zip64ExtraID) | |
| } | |
| if dataLen := le.Uint16(extra[2:4]); dataLen != 16 { | |
| t.Errorf("Zip64 extra data length = %d, want 16", dataLen) | |
| } | |
| if got := le.Uint64(extra[4:12]); got != fh.UncompressedSize64 { | |
| t.Errorf("Zip64 USize64 = %d, want %d", got, fh.UncompressedSize64) | |
| } | |
| if got := le.Uint64(extra[12:20]); got != fh.CompressedSize64 { | |
| t.Errorf("Zip64 CSize64 = %d, want %d", got, fh.CompressedSize64) | |
| } | |
| } |