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

bodgit / sevenzip / 11888809181

18 Nov 2024 08:46AM UTC coverage: 73.339% (-0.06%) from 73.403%
11888809181

push

github

web-flow
refactor: Make use of 1.20+ standard library features (#286)

* ci: Bump golangci-lint

* chore: Drop compat `min()` & `max()` functions

* refactor: Use `sync.OnceValues` instead of `syncutil.Once`

* refactor: Use `errors.Join` instead of `github.com/hashicorp/go-multierror`

18 of 29 new or added lines in 4 files covered. (62.07%)

1 existing line in 1 file now uncovered.

1678 of 2288 relevant lines covered (73.34%)

1.32 hits per line

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

77.99
/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
        "io/fs"
13
        "os"
14
        "path"
15
        "path/filepath"
16
        "sort"
17
        "strings"
18
        "sync"
19
        "time"
20

21
        "github.com/bodgit/plumbing"
22
        "github.com/bodgit/sevenzip/internal/pool"
23
        "github.com/bodgit/sevenzip/internal/util"
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 []*os.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() (fs.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.FileHeader.isEmptyStream || f.FileHeader.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
// OpenReaderWithPassword will open the 7-zip file specified by name using
191
// password as the basis of the decryption key and return a [*ReadCloser]. If
192
// name has a ".001" suffix it is assumed there are multiple volumes and each
193
// sequential volume will be opened.
194
//
195
//nolint:cyclop,funlen
196
func OpenReaderWithPassword(name, password string) (*ReadCloser, error) {
2✔
197
        f, err := os.Open(filepath.Clean(name))
2✔
198
        if err != nil {
2✔
199
                return nil, fmt.Errorf("sevenzip: error opening: %w", err)
×
200
        }
×
201

202
        info, err := f.Stat()
2✔
203
        if err != nil {
2✔
NEW
204
                err = errors.Join(err, f.Close())
×
205

×
206
                return nil, fmt.Errorf("sevenzip: error retrieving file info: %w", err)
×
207
        }
×
208

209
        var reader io.ReaderAt = f
2✔
210

2✔
211
        size := info.Size()
2✔
212
        files := []*os.File{f}
2✔
213

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

2✔
217
                for i := 2; true; i++ {
4✔
218
                        f, err := os.Open(fmt.Sprintf("%s.%03d", strings.TrimSuffix(name, ext), i))
2✔
219
                        if err != nil {
4✔
220
                                if errors.Is(err, fs.ErrNotExist) {
4✔
221
                                        break
2✔
222
                                }
223

NEW
224
                                errs := make([]error, 0, len(files)+1)
×
NEW
225
                                errs = append(errs, err)
×
NEW
226

×
227
                                for _, file := range files {
×
NEW
228
                                        errs = append(errs, file.Close())
×
229
                                }
×
230

NEW
231
                                return nil, fmt.Errorf("sevenzip: error opening: %w", errors.Join(errs...))
×
232
                        }
233

234
                        files = append(files, f)
2✔
235

2✔
236
                        info, err = f.Stat()
2✔
237
                        if err != nil {
2✔
NEW
238
                                errs := make([]error, 0, len(files)+1)
×
NEW
239
                                errs = append(errs, err)
×
NEW
240

×
241
                                for _, file := range files {
×
NEW
242
                                        errs = append(errs, file.Close())
×
243
                                }
×
244

NEW
245
                                return nil, fmt.Errorf("sevenzip: error retrieving file info: %w", errors.Join(errs...))
×
246
                        }
247

248
                        sr = append(sr, io.NewSectionReader(f, 0, info.Size()))
2✔
249
                }
250

251
                mr := readerutil.NewMultiReaderAt(sr...)
2✔
252
                reader, size = mr, mr.Size()
2✔
253
        }
254

255
        r := new(ReadCloser)
2✔
256
        r.p = password
2✔
257

2✔
258
        if err := r.init(reader, size); err != nil {
4✔
259
                errs := make([]error, 0, len(files)+1)
2✔
260
                errs = append(errs, err)
2✔
261

2✔
262
                for _, file := range files {
4✔
263
                        errs = append(errs, file.Close())
2✔
264
                }
2✔
265

266
                return nil, fmt.Errorf("sevenzip: error initialising: %w", errors.Join(errs...))
2✔
267
        }
268

269
        r.f = files
2✔
270

2✔
271
        return r, nil
2✔
272
}
273

274
// OpenReader will open the 7-zip file specified by name and return a
275
// [*ReadCloser]. If name has a ".001" suffix it is assumed there are multiple
276
// volumes and each sequential volume will be opened.
277
func OpenReader(name string) (*ReadCloser, error) {
2✔
278
        return OpenReaderWithPassword(name, "")
2✔
279
}
2✔
280

281
// NewReaderWithPassword returns a new [*Reader] reading from r using password
282
// as the basis of the decryption key, which is assumed to have the given size
283
// in bytes.
284
func NewReaderWithPassword(r io.ReaderAt, size int64, password string) (*Reader, error) {
×
285
        if size < 0 {
×
286
                return nil, errNegativeSize
×
287
        }
×
288

289
        zr := new(Reader)
×
290
        zr.p = password
×
291

×
292
        if err := zr.init(r, size); err != nil {
×
293
                return nil, err
×
294
        }
×
295

296
        return zr, nil
×
297
}
298

299
// NewReader returns a new [*Reader] reading from r, which is assumed to have
300
// the given size in bytes.
301
func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
×
302
        return NewReaderWithPassword(r, size, "")
×
303
}
×
304

305
func (z *Reader) folderReader(si *streamsInfo, f int) (*folderReadCloser, uint32, bool, error) {
2✔
306
        // Create a SectionReader covering all of the streams data
2✔
307
        return si.FolderReader(io.NewSectionReader(z.r, z.start, z.end-z.start), f, z.p)
2✔
308
}
2✔
309

310
const (
311
        chunkSize   = 4096
312
        searchLimit = 1 << 20 // 1 MiB
313
)
314

315
func findSignature(r io.ReaderAt, search []byte) ([]int64, error) {
2✔
316
        chunk := make([]byte, chunkSize+len(search))
2✔
317
        offsets := make([]int64, 0, 2)
2✔
318

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

2✔
322
                for i := 0; ; {
4✔
323
                        idx := bytes.Index(chunk[i:n], search)
2✔
324
                        if idx == -1 {
4✔
325
                                break
2✔
326
                        }
327

328
                        offsets = append(offsets, offset+int64(i+idx))
2✔
329
                        if offsets[0] == 0 {
4✔
330
                                // If signature is at the beginning, return immediately, it's a regular archive
2✔
331
                                return offsets, nil
2✔
332
                        }
2✔
333

334
                        i += idx + 1
2✔
335
                }
336

337
                if err != nil {
4✔
338
                        if errors.Is(err, io.EOF) {
4✔
339
                                break
2✔
340
                        }
341

342
                        return nil, fmt.Errorf("sevenzip: error reading chunk: %w", err)
×
343
                }
344
        }
345

346
        return offsets, nil
2✔
347
}
348

349
//nolint:cyclop,funlen,gocognit,gocyclo,maintidx
350
func (z *Reader) init(r io.ReaderAt, size int64) (err error) {
2✔
351
        h := crc32.NewIEEE()
2✔
352
        tra := plumbing.TeeReaderAt(r, h)
2✔
353

2✔
354
        var (
2✔
355
                signature = []byte{'7', 'z', 0xbc, 0xaf, 0x27, 0x1c}
2✔
356
                offsets   []int64
2✔
357
        )
2✔
358

2✔
359
        offsets, err = findSignature(r, signature)
2✔
360
        if err != nil {
2✔
361
                return err
×
362
        }
×
363

364
        if len(offsets) == 0 {
2✔
365
                return errFormat
×
366
        }
×
367

368
        var (
2✔
369
                sr    *io.SectionReader
2✔
370
                off   int64
2✔
371
                start startHeader
2✔
372
        )
2✔
373

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

2✔
377
                var sh signatureHeader
2✔
378
                if err = binary.Read(sr, binary.LittleEndian, &sh); err != nil {
2✔
379
                        return fmt.Errorf("sevenzip: error reading signature header: %w", err)
×
380
                }
×
381

382
                z.r = r
2✔
383

2✔
384
                h.Reset()
2✔
385

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

390
                // CRC of the start header should match
391
                if util.CRC32Equal(h.Sum(nil), sh.CRC) {
4✔
392
                        break
2✔
393
                }
394

395
                err = errChecksum
2✔
396
        }
397

398
        if err != nil {
2✔
399
                return err
×
400
        }
×
401

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

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

412
        z.start += off
2✔
413
        z.end += off
2✔
414

2✔
415
        h.Reset()
2✔
416

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

2✔
420
        var (
2✔
421
                id          byte
2✔
422
                header      *header
2✔
423
                streamsInfo *streamsInfo
2✔
424
        )
2✔
425

2✔
426
        if id, err = br.ReadByte(); err != nil {
2✔
427
                return fmt.Errorf("sevenzip: error reading header id: %w", err)
×
428
        }
×
429

430
        switch id {
2✔
431
        case idHeader:
2✔
432
                if header, err = readHeader(br); err != nil {
4✔
433
                        return err
2✔
434
                }
2✔
435
        case idEncodedHeader:
2✔
436
                if streamsInfo, err = readStreamsInfo(br); err != nil {
2✔
437
                        return err
×
438
                }
×
439
        default:
×
440
                return errUnexpectedID
×
441
        }
442

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

449
        // CRC should match the one from the start header
450
        if !util.CRC32Equal(h.Sum(nil), start.CRC) {
2✔
451
                return errChecksum
×
452
        }
×
453

454
        // If the header was encoded we should have sufficient information now
455
        // to decode it
456
        if streamsInfo != nil {
4✔
457
                if streamsInfo.Folders() != 1 {
2✔
458
                        return errOneHeaderStream
×
459
                }
×
460

461
                var (
2✔
462
                        fr        *folderReadCloser
2✔
463
                        crc       uint32
2✔
464
                        encrypted bool
2✔
465
                )
2✔
466

2✔
467
                fr, crc, encrypted, err = z.folderReader(streamsInfo, 0)
2✔
468
                if err != nil {
2✔
469
                        return &ReadError{
×
470
                                Encrypted: encrypted,
×
471
                                Err:       err,
×
472
                        }
×
473
                }
×
474

475
                defer func() {
4✔
476
                        err = errors.Join(err, fr.Close())
2✔
477
                }()
2✔
478

479
                if header, err = readEncodedHeader(util.ByteReadCloser(fr)); err != nil {
4✔
480
                        return &ReadError{
2✔
481
                                Encrypted: fr.hasEncryption,
2✔
482
                                Err:       err,
2✔
483
                        }
2✔
484
                }
2✔
485

486
                if crc != 0 && !util.CRC32Equal(fr.Checksum(), crc) {
2✔
487
                        return errChecksum
×
488
                }
×
489
        }
490

491
        z.si = header.streamsInfo
2✔
492

2✔
493
        // spew.Dump(header)
2✔
494
        filesPerStream := make(map[int]int, z.si.Folders())
2✔
495

2✔
496
        if header.filesInfo != nil {
4✔
497
                folder, offset := 0, int64(0)
2✔
498
                z.File = make([]*File, 0, len(header.filesInfo.file))
2✔
499
                j := 0
2✔
500

2✔
501
                for _, fh := range header.filesInfo.file {
4✔
502
                        f := new(File)
2✔
503
                        f.zip = z
2✔
504
                        f.FileHeader = fh
2✔
505

2✔
506
                        if f.FileHeader.FileInfo().IsDir() && !strings.HasSuffix(f.FileHeader.Name, "/") {
4✔
507
                                f.FileHeader.Name += "/"
2✔
508
                        }
2✔
509

510
                        if !fh.isEmptyStream && !fh.isEmptyFile {
4✔
511
                                f.folder, _ = header.streamsInfo.FileFolderAndSize(j)
2✔
512

2✔
513
                                // Make an exported copy of the folder index
2✔
514
                                f.Stream = f.folder
2✔
515

2✔
516
                                filesPerStream[f.folder]++
2✔
517

2✔
518
                                if f.folder != folder {
4✔
519
                                        offset = 0
2✔
520
                                }
2✔
521

522
                                f.offset = offset
2✔
523
                                offset += int64(f.UncompressedSize) //nolint:gosec
2✔
524
                                folder = f.folder
2✔
525
                                j++
2✔
526
                        }
527

528
                        z.File = append(z.File, f)
2✔
529
                }
530
        }
531

532
        // spew.Dump(filesPerStream)
533

534
        z.pool = make([]pool.Pooler, z.si.Folders())
2✔
535
        for i := range z.pool {
4✔
536
                var newPool pool.Constructor = pool.NewNoopPool
2✔
537

2✔
538
                if filesPerStream[i] > 1 {
4✔
539
                        newPool = pool.NewPool
2✔
540
                }
2✔
541

542
                if z.pool[i], err = newPool(); err != nil {
2✔
543
                        return err
×
544
                }
×
545
        }
546

547
        return nil
2✔
548
}
549

550
// Volumes returns the list of volumes that have been opened as part of the
551
// current archive.
552
func (rc *ReadCloser) Volumes() []string {
2✔
553
        volumes := make([]string, len(rc.f))
2✔
554
        for idx, f := range rc.f {
4✔
555
                volumes[idx] = f.Name()
2✔
556
        }
2✔
557

558
        return volumes
2✔
559
}
560

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

2✔
565
        for _, f := range rc.f {
4✔
566
                errs = append(errs, f.Close())
2✔
567
        }
2✔
568

569
        err := errors.Join(errs...)
2✔
570
        if err != nil {
2✔
571
                err = fmt.Errorf("sevenzip: error closing: %w", err)
×
572
        }
×
573

574
        return err
2✔
575
}
576

577
type fileListEntry struct {
578
        name  string
579
        file  *File
580
        isDir bool
581
        isDup bool
582
}
583

584
type fileInfoDirEntry interface {
585
        fs.FileInfo
586
        fs.DirEntry
587
}
588

589
func (e *fileListEntry) stat() (fileInfoDirEntry, error) {
2✔
590
        if e.isDup {
2✔
591
                return nil, errors.New(e.name + ": duplicate entries in 7-zip file") //nolint:err113
×
592
        }
×
593

594
        if !e.isDir {
4✔
595
                return headerFileInfo{&e.file.FileHeader}, nil
2✔
596
        }
2✔
597

598
        return e, nil
2✔
599
}
600

601
func (e *fileListEntry) Name() string {
2✔
602
        _, elem := split(e.name)
2✔
603

2✔
604
        return elem
2✔
605
}
2✔
606

607
func (e *fileListEntry) Size() int64       { return 0 }
2✔
608
func (e *fileListEntry) Mode() fs.FileMode { return fs.ModeDir | 0o555 }
2✔
609
func (e *fileListEntry) Type() fs.FileMode { return fs.ModeDir }
2✔
610
func (e *fileListEntry) IsDir() bool       { return true }
2✔
611
func (e *fileListEntry) Sys() interface{}  { return nil }
×
612

613
func (e *fileListEntry) ModTime() time.Time {
2✔
614
        if e.file == nil {
4✔
615
                return time.Time{}
2✔
616
        }
2✔
617

618
        return e.file.FileHeader.Modified.UTC()
×
619
}
620

621
func (e *fileListEntry) Info() (fs.FileInfo, error) { return e, nil }
2✔
622

623
func toValidName(name string) string {
2✔
624
        name = strings.ReplaceAll(name, `\`, `/`)
2✔
625

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

2✔
628
        for strings.HasPrefix(p, "../") {
2✔
629
                p = p[len("../"):]
×
630
        }
×
631

632
        return p
2✔
633
}
634

635
//nolint:cyclop,funlen
636
func (z *Reader) initFileList() {
2✔
637
        z.fileListOnce.Do(func() {
4✔
638
                files := make(map[string]int)
2✔
639
                knownDirs := make(map[string]int)
2✔
640

2✔
641
                dirs := make(map[string]struct{})
2✔
642

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

2✔
646
                        name := toValidName(file.Name)
2✔
647
                        if name == "" {
2✔
648
                                continue
×
649
                        }
650

651
                        if idx, ok := files[name]; ok {
2✔
652
                                z.fileList[idx].isDup = true
×
653

×
654
                                continue
×
655
                        }
656

657
                        if idx, ok := knownDirs[name]; ok {
2✔
658
                                z.fileList[idx].isDup = true
×
659

×
660
                                continue
×
661
                        }
662

663
                        for dir := path.Dir(name); dir != "."; dir = path.Dir(dir) {
4✔
664
                                dirs[dir] = struct{}{}
2✔
665
                        }
2✔
666

667
                        idx := len(z.fileList)
2✔
668
                        entry := fileListEntry{
2✔
669
                                name:  name,
2✔
670
                                file:  file,
2✔
671
                                isDir: isDir,
2✔
672
                        }
2✔
673
                        z.fileList = append(z.fileList, entry)
2✔
674

2✔
675
                        if isDir {
2✔
676
                                knownDirs[name] = idx
×
677
                        } else {
2✔
678
                                files[name] = idx
2✔
679
                        }
2✔
680
                }
681

682
                for dir := range dirs {
4✔
683
                        if _, ok := knownDirs[dir]; !ok {
4✔
684
                                if idx, ok := files[dir]; ok {
2✔
685
                                        z.fileList[idx].isDup = true
×
686
                                } else {
2✔
687
                                        entry := fileListEntry{
2✔
688
                                                name:  dir,
2✔
689
                                                file:  nil,
2✔
690
                                                isDir: true,
2✔
691
                                        }
2✔
692
                                        z.fileList = append(z.fileList, entry)
2✔
693
                                }
2✔
694
                        }
695
                }
696

697
                sort.Slice(z.fileList, func(i, j int) bool { return fileEntryLess(z.fileList[i].name, z.fileList[j].name) })
4✔
698
        })
699
}
700

701
func fileEntryLess(x, y string) bool {
2✔
702
        xdir, xelem := split(x)
2✔
703
        ydir, yelem := split(y)
2✔
704

2✔
705
        return xdir < ydir || xdir == ydir && xelem < yelem
2✔
706
}
2✔
707

708
// Open opens the named file in the 7-zip archive, using the semantics of
709
// [fs.FS.Open]: paths are always slash separated, with no leading / or ../
710
// elements.
711
func (z *Reader) Open(name string) (fs.File, error) {
2✔
712
        z.initFileList()
2✔
713

2✔
714
        if !fs.ValidPath(name) {
4✔
715
                return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
2✔
716
        }
2✔
717

718
        e := z.openLookup(name)
2✔
719
        if e == nil {
4✔
720
                return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
2✔
721
        }
2✔
722

723
        if e.isDir {
4✔
724
                return &openDir{e, z.openReadDir(name), 0}, nil
2✔
725
        }
2✔
726

727
        rc, err := e.file.Open()
2✔
728
        if err != nil {
2✔
729
                return nil, err
×
730
        }
×
731

732
        return rc.(fs.File), nil //nolint:forcetypeassert
2✔
733
}
734

735
func split(name string) (dir, elem string) {
2✔
736
        if len(name) > 0 && name[len(name)-1] == '/' {
2✔
737
                name = name[:len(name)-1]
×
738
        }
×
739

740
        i := len(name) - 1
2✔
741
        for i >= 0 && name[i] != '/' {
4✔
742
                i--
2✔
743
        }
2✔
744

745
        if i < 0 {
4✔
746
                return ".", name
2✔
747
        }
2✔
748

749
        return name[:i], name[i+1:]
2✔
750
}
751

752
//nolint:gochecknoglobals
753
var dotFile = &fileListEntry{name: "./", isDir: true}
754

755
func (z *Reader) openLookup(name string) *fileListEntry {
2✔
756
        if name == "." {
4✔
757
                return dotFile
2✔
758
        }
2✔
759

760
        dir, elem := split(name)
2✔
761

2✔
762
        files := z.fileList
2✔
763
        i := sort.Search(len(files), func(i int) bool {
4✔
764
                idir, ielem := split(files[i].name)
2✔
765

2✔
766
                return idir > dir || idir == dir && ielem >= elem
2✔
767
        })
2✔
768

769
        if i < len(files) {
4✔
770
                fname := files[i].name
2✔
771
                if fname == name || len(fname) == len(name)+1 && fname[len(name)] == '/' && fname[:len(name)] == name {
4✔
772
                        return &files[i]
2✔
773
                }
2✔
774
        }
775

776
        return nil
2✔
777
}
778

779
func (z *Reader) openReadDir(dir string) []fileListEntry {
2✔
780
        files := z.fileList
2✔
781

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

2✔
785
                return idir >= dir
2✔
786
        })
2✔
787

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

2✔
791
                return jdir > dir
2✔
792
        })
2✔
793

794
        return files[i:j]
2✔
795
}
796

797
type openDir struct {
798
        e      *fileListEntry
799
        files  []fileListEntry
800
        offset int
801
}
802

803
func (d *openDir) Close() error               { return nil }
2✔
804
func (d *openDir) Stat() (fs.FileInfo, error) { return d.e.stat() }
2✔
805

806
var errIsDirectory = errors.New("is a directory")
807

808
func (d *openDir) Read([]byte) (int, error) {
×
809
        return 0, &fs.PathError{Op: "read", Path: d.e.name, Err: errIsDirectory}
×
810
}
×
811

812
func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) {
2✔
813
        n := len(d.files) - d.offset
2✔
814
        if count > 0 && n > count {
4✔
815
                n = count
2✔
816
        }
2✔
817

818
        if n == 0 {
4✔
819
                if count <= 0 {
4✔
820
                        return nil, nil
2✔
821
                }
2✔
822

823
                return nil, io.EOF
2✔
824
        }
825

826
        list := make([]fs.DirEntry, n)
2✔
827
        for i := range list {
4✔
828
                s, err := d.files[d.offset+i].stat()
2✔
829
                if err != nil {
2✔
830
                        return nil, err
×
831
                }
×
832

833
                list[i] = s
2✔
834
        }
835

836
        d.offset += n
2✔
837

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

© 2026 Coveralls, Inc