• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bodgit / sevenzip / 18628837276

19 Oct 2025 10:03AM UTC coverage: 74.173%. Remained the same
18628837276

push

github

web-flow
chore(deps): bump golangci/golangci-lint-action from 6.1.1 to 8.0.0 (#352)

* chore(deps): bump golangci/golangci-lint-action from 6.1.1 to 8.0.0

Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.1.1 to 8.0.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/971e284b6...4afd733a8)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* style: Lint fixes

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matt Dainty <matt@bodgit-n-scarper.com>

37 of 49 new or added lines in 4 files covered. (75.51%)

1726 of 2327 relevant lines covered (74.17%)

1.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.76
/reader.go
1
// Package sevenzip provides read access to 7-zip archives.
2
package sevenzip
3

4
import (
5
        "bufio"
6
        "bytes"
7
        "encoding/binary"
8
        "errors"
9
        "fmt"
10
        "hash/crc32"
11
        "io"
12
        iofs "io/fs"
13
        "path"
14
        "path/filepath"
15
        "sort"
16
        "strings"
17
        "sync"
18
        "time"
19

20
        "github.com/bodgit/plumbing"
21
        "github.com/bodgit/sevenzip/internal/pool"
22
        "github.com/bodgit/sevenzip/internal/util"
23
        "github.com/spf13/afero"
24
        "go4.org/readerutil"
25
)
26

27
var (
28
        errFormat          = errors.New("sevenzip: not a valid 7-zip file")
29
        errChecksum        = errors.New("sevenzip: checksum error")
30
        errTooMuch         = errors.New("sevenzip: too much data")
31
        errNegativeSize    = errors.New("sevenzip: size cannot be negative")
32
        errOneHeaderStream = errors.New("sevenzip: expected only one folder in header stream")
33
)
34

35
// ReadError is used to wrap read I/O errors.
36
type ReadError struct {
37
        // Encrypted is a hint that there is encryption involved.
38
        Encrypted bool
39
        Err       error
40
}
41

42
func (e ReadError) Error() string {
2✔
43
        return fmt.Sprintf("sevenzip: read error: %v", e.Err)
2✔
44
}
2✔
45

46
func (e ReadError) Unwrap() error {
×
47
        return e.Err
×
48
}
×
49

50
// A Reader serves content from a 7-Zip archive.
51
type Reader struct {
52
        r     io.ReaderAt
53
        start int64
54
        end   int64
55
        si    *streamsInfo
56
        p     string
57
        File  []*File
58
        pool  []pool.Pooler
59

60
        fileListOnce sync.Once
61
        fileList     []fileListEntry
62
}
63

64
// A ReadCloser is a [Reader] that must be closed when no longer needed.
65
type ReadCloser struct {
66
        f []afero.File
67
        Reader
68
}
69

70
// A File is a single file in a 7-Zip archive. The file information is in the
71
// embedded [FileHeader]. The file content can be accessed by calling
72
// [File.Open].
73
type File struct {
74
        FileHeader
75
        zip    *Reader
76
        folder int
77
        offset int64
78
}
79

80
type fileReader struct {
81
        rc util.SizeReadSeekCloser
82
        f  *File
83
        n  int64
84
}
85

86
func (fr *fileReader) Stat() (iofs.FileInfo, error) {
2✔
87
        return headerFileInfo{&fr.f.FileHeader}, nil
2✔
88
}
2✔
89

90
func (fr *fileReader) Read(p []byte) (int, error) {
2✔
91
        if len(p) == 0 {
4✔
92
                return 0, nil
2✔
93
        }
2✔
94

95
        if fr.n <= 0 {
4✔
96
                return 0, io.EOF
2✔
97
        }
2✔
98

99
        if int64(len(p)) > fr.n {
4✔
100
                p = p[0:fr.n]
2✔
101
        }
2✔
102

103
        n, err := fr.rc.Read(p)
2✔
104
        fr.n -= int64(n)
2✔
105

2✔
106
        if err != nil && !errors.Is(err, io.EOF) {
4✔
107
                e := &ReadError{
2✔
108
                        Err: err,
2✔
109
                }
2✔
110

2✔
111
                if frc, ok := fr.rc.(*folderReadCloser); ok {
4✔
112
                        e.Encrypted = frc.hasEncryption
2✔
113
                }
2✔
114

115
                return n, e
2✔
116
        }
117

118
        return n, err //nolint:wrapcheck
2✔
119
}
120

121
func (fr *fileReader) Close() error {
2✔
122
        if fr.rc == nil {
4✔
123
                return nil
2✔
124
        }
2✔
125

126
        offset, err := fr.rc.Seek(0, io.SeekCurrent)
2✔
127
        if err != nil {
2✔
128
                return fmt.Errorf("sevenzip: error seeking current position: %w", err)
×
129
        }
×
130

131
        if offset == fr.rc.Size() { // EOF reached
4✔
132
                if err := fr.rc.Close(); err != nil {
2✔
133
                        return fmt.Errorf("sevenzip: error closing: %w", err)
×
134
                }
×
135
        } else {
2✔
136
                f := fr.f
2✔
137
                if _, err := f.zip.pool[f.folder].Put(offset, fr.rc); err != nil {
2✔
138
                        return fmt.Errorf("sevenzip: error adding to pool: %w", err)
×
139
                }
×
140
        }
141

142
        fr.rc = nil
2✔
143

2✔
144
        return nil
2✔
145
}
146

147
// Open returns an [io.ReadCloser] that provides access to the [File]'s
148
// contents. Multiple files may be read concurrently.
149
func (f *File) Open() (io.ReadCloser, error) {
2✔
150
        if f.isEmptyStream || f.isEmptyFile {
4✔
151
                // Return empty reader for directory or empty file
2✔
152
                return io.NopCloser(bytes.NewReader(nil)), nil
2✔
153
        }
2✔
154

155
        rc, _ := f.zip.pool[f.folder].Get(f.offset)
2✔
156
        if rc == nil {
4✔
157
                var (
2✔
158
                        encrypted bool
2✔
159
                        err       error
2✔
160
                )
2✔
161

2✔
162
                rc, _, encrypted, err = f.zip.folderReader(f.zip.si, f.folder)
2✔
163
                if err != nil {
2✔
164
                        return nil, &ReadError{
×
165
                                Encrypted: encrypted,
×
166
                                Err:       err,
×
167
                        }
×
168
                }
×
169
        }
170

171
        if _, err := rc.Seek(f.offset, io.SeekStart); err != nil {
2✔
172
                e := &ReadError{
×
173
                        Err: err,
×
174
                }
×
175

×
176
                if fr, ok := rc.(*folderReadCloser); ok {
×
177
                        e.Encrypted = fr.hasEncryption
×
178
                }
×
179

180
                return nil, e
×
181
        }
182

183
        return &fileReader{
2✔
184
                rc: rc,
2✔
185
                f:  f,
2✔
186
                n:  int64(f.UncompressedSize), //nolint:gosec
2✔
187
        }, nil
2✔
188
}
189

190
func openReader(fs afero.Fs, name string) (io.ReaderAt, int64, []afero.File, error) {
2✔
191
        f, err := fs.Open(filepath.Clean(name))
2✔
192
        if err != nil {
4✔
193
                return nil, 0, nil, fmt.Errorf("sevenzip: error opening: %w", err)
2✔
194
        }
2✔
195

196
        info, err := f.Stat()
2✔
197
        if err != nil {
4✔
198
                err = errors.Join(err, f.Close())
2✔
199

2✔
200
                return nil, 0, nil, fmt.Errorf("sevenzip: error retrieving file info: %w", err)
2✔
201
        }
2✔
202

203
        var reader io.ReaderAt = f
2✔
204

2✔
205
        size := info.Size()
2✔
206
        files := []afero.File{f}
2✔
207

2✔
208
        if ext := filepath.Ext(name); ext == ".001" {
4✔
209
                sr := []readerutil.SizeReaderAt{io.NewSectionReader(f, 0, size)}
2✔
210

2✔
211
                for i := 2; true; i++ {
4✔
212
                        f, err := fs.Open(fmt.Sprintf("%s.%03d", strings.TrimSuffix(name, ext), i))
2✔
213
                        if err != nil {
4✔
214
                                if errors.Is(err, iofs.ErrNotExist) {
4✔
215
                                        break
2✔
216
                                }
217

218
                                errs := make([]error, 0, len(files)+1)
2✔
219
                                errs = append(errs, err)
2✔
220

2✔
221
                                for _, file := range files {
4✔
222
                                        errs = append(errs, file.Close())
2✔
223
                                }
2✔
224

225
                                return nil, 0, nil, fmt.Errorf("sevenzip: error opening: %w", errors.Join(errs...))
2✔
226
                        }
227

228
                        files = append(files, f)
2✔
229

2✔
230
                        info, err = f.Stat()
2✔
231
                        if err != nil {
4✔
232
                                errs := make([]error, 0, len(files)+1)
2✔
233
                                errs = append(errs, err)
2✔
234

2✔
235
                                for _, file := range files {
4✔
236
                                        errs = append(errs, file.Close())
2✔
237
                                }
2✔
238

239
                                return nil, 0, nil, fmt.Errorf("sevenzip: error retrieving file info: %w", errors.Join(errs...))
2✔
240
                        }
241

242
                        sr = append(sr, io.NewSectionReader(f, 0, info.Size()))
2✔
243
                }
244

245
                mr := readerutil.NewMultiReaderAt(sr...)
2✔
246
                reader, size = mr, mr.Size()
2✔
247
        }
248

249
        return reader, size, files, nil
2✔
250
}
251

252
// OpenReaderWithPassword will open the 7-zip file specified by name using
253
// password as the basis of the decryption key and return a [*ReadCloser]. If
254
// name has a ".001" suffix it is assumed there are multiple volumes and each
255
// sequential volume will be opened.
256
func OpenReaderWithPassword(name, password string) (*ReadCloser, error) {
2✔
257
        reader, size, files, err := openReader(afero.NewOsFs(), name)
2✔
258
        if err != nil {
2✔
259
                return nil, err
×
260
        }
×
261

262
        r := new(ReadCloser)
2✔
263
        r.p = password
2✔
264

2✔
265
        if err := r.init(reader, size); err != nil {
4✔
266
                errs := make([]error, 0, len(files)+1)
2✔
267
                errs = append(errs, err)
2✔
268

2✔
269
                for _, file := range files {
4✔
270
                        errs = append(errs, file.Close())
2✔
271
                }
2✔
272

273
                return nil, fmt.Errorf("sevenzip: error initialising: %w", errors.Join(errs...))
2✔
274
        }
275

276
        r.f = files
2✔
277

2✔
278
        return r, nil
2✔
279
}
280

281
// OpenReader will open the 7-zip file specified by name and return a
282
// [*ReadCloser]. If name has a ".001" suffix it is assumed there are multiple
283
// volumes and each sequential volume will be opened.
284
func OpenReader(name string) (*ReadCloser, error) {
2✔
285
        return OpenReaderWithPassword(name, "")
2✔
286
}
2✔
287

288
// NewReaderWithPassword returns a new [*Reader] reading from r using password
289
// as the basis of the decryption key, which is assumed to have the given size
290
// in bytes.
291
func NewReaderWithPassword(r io.ReaderAt, size int64, password string) (*Reader, error) {
2✔
292
        if size < 0 {
4✔
293
                return nil, errNegativeSize
2✔
294
        }
2✔
295

296
        zr := new(Reader)
2✔
297
        zr.p = password
2✔
298

2✔
299
        if err := zr.init(r, size); err != nil {
2✔
300
                return nil, err
×
301
        }
×
302

303
        return zr, nil
2✔
304
}
305

306
// NewReader returns a new [*Reader] reading from r, which is assumed to have
307
// the given size in bytes.
308
func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
2✔
309
        return NewReaderWithPassword(r, size, "")
2✔
310
}
2✔
311

312
// Open opens the named file in the 7-zip archive, using the semantics of
313
// [fs.FS.Open]: paths are always slash separated, with no leading / or ../
314
// elements.
315
func (z *Reader) Open(name string) (iofs.File, error) {
2✔
316
        z.initFileList()
2✔
317

2✔
318
        if !iofs.ValidPath(name) {
4✔
319
                return nil, &iofs.PathError{Op: "open", Path: name, Err: iofs.ErrInvalid}
2✔
320
        }
2✔
321

322
        e := z.openLookup(name)
2✔
323
        if e == nil {
4✔
324
                return nil, &iofs.PathError{Op: "open", Path: name, Err: iofs.ErrNotExist}
2✔
325
        }
2✔
326

327
        if e.isDir {
4✔
328
                return &openDir{e, z.openReadDir(name), 0}, nil
2✔
329
        }
2✔
330

331
        rc, err := e.file.Open()
2✔
332
        if err != nil {
2✔
NEW
333
                return nil, err
×
NEW
334
        }
×
335

336
        return rc.(iofs.File), nil //nolint:forcetypeassert
2✔
337
}
338

339
func (z *Reader) folderReader(si *streamsInfo, f int) (*folderReadCloser, uint32, bool, error) {
2✔
340
        // Create a SectionReader covering all of the streams data
2✔
341
        return si.folderReader(io.NewSectionReader(z.r, z.start, z.end-z.start), f, z.p)
2✔
342
}
2✔
343

344
const (
345
        chunkSize   = 4096
346
        searchLimit = 1 << 20 // 1 MiB
347
)
348

349
func findSignature(r io.ReaderAt, search []byte) ([]int64, error) {
2✔
350
        chunk := make([]byte, chunkSize+len(search))
2✔
351
        offsets := make([]int64, 0, 2)
2✔
352

2✔
353
        for offset := int64(0); offset < searchLimit; offset += chunkSize {
4✔
354
                n, err := r.ReadAt(chunk, offset)
2✔
355

2✔
356
                for i := 0; ; {
4✔
357
                        idx := bytes.Index(chunk[i:n], search)
2✔
358
                        if idx == -1 {
4✔
359
                                break
2✔
360
                        }
361

362
                        offsets = append(offsets, offset+int64(i+idx))
2✔
363
                        if offsets[0] == 0 {
4✔
364
                                // If signature is at the beginning, return immediately, it's a regular archive
2✔
365
                                return offsets, nil
2✔
366
                        }
2✔
367

368
                        i += idx + 1
2✔
369
                }
370

371
                if err != nil {
4✔
372
                        if errors.Is(err, io.EOF) {
4✔
373
                                break
2✔
374
                        }
375

376
                        return nil, fmt.Errorf("sevenzip: error reading chunk: %w", err)
×
377
                }
378
        }
379

380
        return offsets, nil
2✔
381
}
382

383
//nolint:cyclop,funlen,gocognit,gocyclo,maintidx
384
func (z *Reader) init(r io.ReaderAt, size int64) (err error) {
2✔
385
        h := crc32.NewIEEE()
2✔
386
        tra := plumbing.TeeReaderAt(r, h)
2✔
387

2✔
388
        var (
2✔
389
                signature = []byte{'7', 'z', 0xbc, 0xaf, 0x27, 0x1c}
2✔
390
                offsets   []int64
2✔
391
        )
2✔
392

2✔
393
        offsets, err = findSignature(r, signature)
2✔
394
        if err != nil {
2✔
395
                return err
×
396
        }
×
397

398
        if len(offsets) == 0 {
2✔
399
                return errFormat
×
400
        }
×
401

402
        var (
2✔
403
                sr    *io.SectionReader
2✔
404
                off   int64
2✔
405
                start startHeader
2✔
406
        )
2✔
407

2✔
408
        for _, off = range offsets {
4✔
409
                sr = io.NewSectionReader(tra, off, size-off) // Will only read first 32 bytes
2✔
410

2✔
411
                var sh signatureHeader
2✔
412
                if err = binary.Read(sr, binary.LittleEndian, &sh); err != nil {
2✔
413
                        return fmt.Errorf("sevenzip: error reading signature header: %w", err)
×
414
                }
×
415

416
                z.r = r
2✔
417

2✔
418
                h.Reset()
2✔
419

2✔
420
                if err = binary.Read(sr, binary.LittleEndian, &start); err != nil {
2✔
421
                        return fmt.Errorf("sevenzip: error reading start header: %w", err)
×
422
                }
×
423

424
                // CRC of the start header should match
425
                if util.CRC32Equal(h.Sum(nil), sh.CRC) {
4✔
426
                        break
2✔
427
                }
428

429
                err = errChecksum
2✔
430
        }
431

432
        if err != nil {
2✔
433
                return err
×
434
        }
×
435

436
        // Work out where we are in the file (32, avoiding magic numbers)
437
        if z.start, err = sr.Seek(0, io.SeekCurrent); err != nil {
2✔
438
                return fmt.Errorf("sevenzip: error seeking current position: %w", err)
×
439
        }
×
440

441
        // Seek over the streams
442
        if z.end, err = sr.Seek(int64(start.Offset), io.SeekCurrent); err != nil { //nolint:gosec
2✔
443
                return fmt.Errorf("sevenzip: error seeking over streams: %w", err)
×
444
        }
×
445

446
        z.start += off
2✔
447
        z.end += off
2✔
448

2✔
449
        h.Reset()
2✔
450

2✔
451
        // Bound bufio.Reader otherwise it can read trailing garbage which screws up the CRC check
2✔
452
        br := bufio.NewReader(io.NewSectionReader(tra, z.end, int64(start.Size))) //nolint:gosec
2✔
453

2✔
454
        var (
2✔
455
                id          byte
2✔
456
                header      *header
2✔
457
                streamsInfo *streamsInfo
2✔
458
        )
2✔
459

2✔
460
        if id, err = br.ReadByte(); err != nil {
2✔
461
                return fmt.Errorf("sevenzip: error reading header id: %w", err)
×
462
        }
×
463

464
        switch id {
2✔
465
        case idHeader:
2✔
466
                if header, err = readHeader(br); err != nil {
4✔
467
                        return err
2✔
468
                }
2✔
469
        case idEncodedHeader:
2✔
470
                if streamsInfo, err = readStreamsInfo(br); err != nil {
2✔
471
                        return err
×
472
                }
×
473
        default:
×
474
                return errUnexpectedID
×
475
        }
476

477
        // If there's more data to read, we've not parsed this correctly. This
478
        // won't break with trailing data as the bufio.Reader was bounded
479
        if n, _ := io.CopyN(io.Discard, br, 1); n != 0 {
2✔
480
                return errTooMuch
×
481
        }
×
482

483
        // CRC should match the one from the start header
484
        if !util.CRC32Equal(h.Sum(nil), start.CRC) {
2✔
485
                return errChecksum
×
486
        }
×
487

488
        // If the header was encoded we should have sufficient information now
489
        // to decode it
490
        if streamsInfo != nil {
4✔
491
                if streamsInfo.Folders() != 1 {
2✔
492
                        return errOneHeaderStream
×
493
                }
×
494

495
                var (
2✔
496
                        fr        *folderReadCloser
2✔
497
                        crc       uint32
2✔
498
                        encrypted bool
2✔
499
                )
2✔
500

2✔
501
                fr, crc, encrypted, err = z.folderReader(streamsInfo, 0)
2✔
502
                if err != nil {
2✔
503
                        return &ReadError{
×
504
                                Encrypted: encrypted,
×
505
                                Err:       err,
×
506
                        }
×
507
                }
×
508

509
                defer func() {
4✔
510
                        err = errors.Join(err, fr.Close())
2✔
511
                }()
2✔
512

513
                if header, err = readEncodedHeader(util.ByteReadCloser(fr)); err != nil {
4✔
514
                        return &ReadError{
2✔
515
                                Encrypted: fr.hasEncryption,
2✔
516
                                Err:       err,
2✔
517
                        }
2✔
518
                }
2✔
519

520
                if crc != 0 && !util.CRC32Equal(fr.Checksum(), crc) {
2✔
521
                        return errChecksum
×
522
                }
×
523
        }
524

525
        z.si = header.streamsInfo
2✔
526

2✔
527
        // spew.Dump(header)
2✔
528
        filesPerStream := make(map[int]int, z.si.Folders())
2✔
529

2✔
530
        if header.filesInfo != nil {
4✔
531
                folder, offset := 0, int64(0)
2✔
532
                z.File = make([]*File, 0, len(header.filesInfo.file))
2✔
533
                j := 0
2✔
534

2✔
535
                for _, fh := range header.filesInfo.file {
4✔
536
                        f := new(File)
2✔
537
                        f.zip = z
2✔
538
                        f.FileHeader = fh
2✔
539

2✔
540
                        if f.FileHeader.FileInfo().IsDir() && !strings.HasSuffix(f.Name, "/") {
4✔
541
                                f.Name += "/"
2✔
542
                        }
2✔
543

544
                        if !fh.isEmptyStream && !fh.isEmptyFile {
4✔
545
                                f.folder, _, _ = header.streamsInfo.FileFolderAndSize(j)
2✔
546

2✔
547
                                // Make an exported copy of the folder index
2✔
548
                                f.Stream = f.folder
2✔
549

2✔
550
                                filesPerStream[f.folder]++
2✔
551

2✔
552
                                if f.folder != folder {
4✔
553
                                        offset = 0
2✔
554
                                }
2✔
555

556
                                f.offset = offset
2✔
557
                                offset += int64(f.UncompressedSize) //nolint:gosec
2✔
558
                                folder = f.folder
2✔
559
                                j++
2✔
560
                        }
561

562
                        z.File = append(z.File, f)
2✔
563
                }
564
        }
565

566
        // spew.Dump(filesPerStream)
567

568
        z.pool = make([]pool.Pooler, z.si.Folders())
2✔
569
        for i := range z.pool {
4✔
570
                var newPool pool.Constructor = pool.NewNoopPool
2✔
571

2✔
572
                if filesPerStream[i] > 1 {
4✔
573
                        newPool = pool.NewPool
2✔
574
                }
2✔
575

576
                if z.pool[i], err = newPool(); err != nil {
2✔
577
                        return err
×
578
                }
×
579
        }
580

581
        return nil
2✔
582
}
583

584
// Volumes returns the list of volumes that have been opened as part of the
585
// current archive.
586
func (rc *ReadCloser) Volumes() []string {
2✔
587
        volumes := make([]string, len(rc.f))
2✔
588
        for idx, f := range rc.f {
4✔
589
                volumes[idx] = f.Name()
2✔
590
        }
2✔
591

592
        return volumes
2✔
593
}
594

595
// Close closes the 7-zip file or volumes, rendering them unusable for I/O.
596
func (rc *ReadCloser) Close() error {
2✔
597
        errs := make([]error, 0, len(rc.f))
2✔
598

2✔
599
        for _, f := range rc.f {
4✔
600
                errs = append(errs, f.Close())
2✔
601
        }
2✔
602

603
        err := errors.Join(errs...)
2✔
604
        if err != nil {
2✔
605
                err = fmt.Errorf("sevenzip: error closing: %w", err)
×
606
        }
×
607

608
        return err
2✔
609
}
610

611
type fileListEntry struct {
612
        name  string
613
        file  *File
614
        isDir bool
615
        isDup bool
616
}
617

618
type fileInfoDirEntry interface {
619
        iofs.FileInfo
620
        iofs.DirEntry
621
}
622

623
func (e *fileListEntry) Name() string {
2✔
624
        _, elem := split(e.name)
2✔
625

2✔
626
        return elem
2✔
627
}
2✔
628

629
func (e *fileListEntry) Size() int64         { return 0 }
2✔
630
func (e *fileListEntry) Mode() iofs.FileMode { return iofs.ModeDir | 0o555 }
2✔
631
func (e *fileListEntry) Type() iofs.FileMode { return iofs.ModeDir }
2✔
632
func (e *fileListEntry) IsDir() bool         { return true }
2✔
633
func (e *fileListEntry) Sys() interface{}    { return nil }
×
634

635
func (e *fileListEntry) ModTime() time.Time {
2✔
636
        if e.file == nil {
4✔
637
                return time.Time{}
2✔
638
        }
2✔
639

NEW
640
        return e.file.Modified.UTC()
×
641
}
642

643
func (e *fileListEntry) Info() (iofs.FileInfo, error) { return e, nil }
2✔
644

645
func (e *fileListEntry) stat() (fileInfoDirEntry, error) {
2✔
646
        if e.isDup {
2✔
NEW
647
                return nil, errors.New(e.name + ": duplicate entries in 7-zip file") //nolint:err113
×
NEW
648
        }
×
649

650
        if !e.isDir {
4✔
651
                return headerFileInfo{&e.file.FileHeader}, nil
2✔
652
        }
2✔
653

654
        return e, nil
2✔
655
}
656

657
func toValidName(name string) string {
2✔
658
        name = strings.ReplaceAll(name, `\`, `/`)
2✔
659

2✔
660
        p := strings.TrimPrefix(path.Clean(name), "/")
2✔
661

2✔
662
        for strings.HasPrefix(p, "../") {
2✔
663
                p = p[len("../"):]
×
664
        }
×
665

666
        return p
2✔
667
}
668

669
//nolint:cyclop,funlen
670
func (z *Reader) initFileList() {
2✔
671
        z.fileListOnce.Do(func() {
4✔
672
                files := make(map[string]int)
2✔
673
                knownDirs := make(map[string]int)
2✔
674

2✔
675
                dirs := make(map[string]struct{})
2✔
676

2✔
677
                for _, file := range z.File {
4✔
678
                        isDir := len(file.Name) > 0 && file.Name[len(file.Name)-1] == '/'
2✔
679

2✔
680
                        name := toValidName(file.Name)
2✔
681
                        if name == "" {
2✔
682
                                continue
×
683
                        }
684

685
                        if idx, ok := files[name]; ok {
2✔
686
                                z.fileList[idx].isDup = true
×
687

×
688
                                continue
×
689
                        }
690

691
                        if idx, ok := knownDirs[name]; ok {
2✔
692
                                z.fileList[idx].isDup = true
×
693

×
694
                                continue
×
695
                        }
696

697
                        for dir := path.Dir(name); dir != "."; dir = path.Dir(dir) {
4✔
698
                                dirs[dir] = struct{}{}
2✔
699
                        }
2✔
700

701
                        idx := len(z.fileList)
2✔
702
                        entry := fileListEntry{
2✔
703
                                name:  name,
2✔
704
                                file:  file,
2✔
705
                                isDir: isDir,
2✔
706
                        }
2✔
707
                        z.fileList = append(z.fileList, entry)
2✔
708

2✔
709
                        if isDir {
2✔
710
                                knownDirs[name] = idx
×
711
                        } else {
2✔
712
                                files[name] = idx
2✔
713
                        }
2✔
714
                }
715

716
                for dir := range dirs {
4✔
717
                        if _, ok := knownDirs[dir]; !ok {
4✔
718
                                if idx, ok := files[dir]; ok {
2✔
719
                                        z.fileList[idx].isDup = true
×
720
                                } else {
2✔
721
                                        entry := fileListEntry{
2✔
722
                                                name:  dir,
2✔
723
                                                file:  nil,
2✔
724
                                                isDir: true,
2✔
725
                                        }
2✔
726
                                        z.fileList = append(z.fileList, entry)
2✔
727
                                }
2✔
728
                        }
729
                }
730

731
                sort.Slice(z.fileList, func(i, j int) bool { return fileEntryLess(z.fileList[i].name, z.fileList[j].name) })
4✔
732
        })
733
}
734

735
func fileEntryLess(x, y string) bool {
2✔
736
        xdir, xelem := split(x)
2✔
737
        ydir, yelem := split(y)
2✔
738

2✔
739
        return xdir < ydir || xdir == ydir && xelem < yelem
2✔
740
}
2✔
741

742
func split(name string) (dir, elem string) {
2✔
743
        if len(name) > 0 && name[len(name)-1] == '/' {
2✔
744
                name = name[:len(name)-1]
×
745
        }
×
746

747
        i := len(name) - 1
2✔
748
        for i >= 0 && name[i] != '/' {
4✔
749
                i--
2✔
750
        }
2✔
751

752
        if i < 0 {
4✔
753
                return ".", name
2✔
754
        }
2✔
755

756
        return name[:i], name[i+1:]
2✔
757
}
758

759
//nolint:gochecknoglobals
760
var dotFile = &fileListEntry{name: "./", isDir: true}
761

762
func (z *Reader) openLookup(name string) *fileListEntry {
2✔
763
        if name == "." {
4✔
764
                return dotFile
2✔
765
        }
2✔
766

767
        dir, elem := split(name)
2✔
768

2✔
769
        files := z.fileList
2✔
770
        i := sort.Search(len(files), func(i int) bool {
4✔
771
                idir, ielem := split(files[i].name)
2✔
772

2✔
773
                return idir > dir || idir == dir && ielem >= elem
2✔
774
        })
2✔
775

776
        if i < len(files) {
4✔
777
                fname := files[i].name
2✔
778
                if fname == name || len(fname) == len(name)+1 && fname[len(name)] == '/' && fname[:len(name)] == name {
4✔
779
                        return &files[i]
2✔
780
                }
2✔
781
        }
782

783
        return nil
2✔
784
}
785

786
func (z *Reader) openReadDir(dir string) []fileListEntry {
2✔
787
        files := z.fileList
2✔
788

2✔
789
        i := sort.Search(len(files), func(i int) bool {
4✔
790
                idir, _ := split(files[i].name)
2✔
791

2✔
792
                return idir >= dir
2✔
793
        })
2✔
794

795
        j := sort.Search(len(files), func(j int) bool {
4✔
796
                jdir, _ := split(files[j].name)
2✔
797

2✔
798
                return jdir > dir
2✔
799
        })
2✔
800

801
        return files[i:j]
2✔
802
}
803

804
type openDir struct {
805
        e      *fileListEntry
806
        files  []fileListEntry
807
        offset int
808
}
809

810
func (d *openDir) Close() error                 { return nil }
2✔
811
func (d *openDir) Stat() (iofs.FileInfo, error) { return d.e.stat() }
2✔
812

813
var errIsDirectory = errors.New("is a directory")
814

815
func (d *openDir) Read([]byte) (int, error) {
×
816
        return 0, &iofs.PathError{Op: "read", Path: d.e.name, Err: errIsDirectory}
×
817
}
×
818

819
func (d *openDir) ReadDir(count int) ([]iofs.DirEntry, error) {
2✔
820
        n := len(d.files) - d.offset
2✔
821
        if count > 0 && n > count {
4✔
822
                n = count
2✔
823
        }
2✔
824

825
        if n == 0 {
4✔
826
                if count <= 0 {
4✔
827
                        return nil, nil
2✔
828
                }
2✔
829

830
                return nil, io.EOF
2✔
831
        }
832

833
        list := make([]iofs.DirEntry, n)
2✔
834
        for i := range list {
4✔
835
                s, err := d.files[d.offset+i].stat()
2✔
836
                if err != nil {
2✔
837
                        return nil, err
×
838
                }
×
839

840
                list[i] = s
2✔
841
        }
842

843
        d.offset += n
2✔
844

2✔
845
        return list, nil
2✔
846
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc