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

bodgit / sevenzip / 12000514469

24 Nov 2024 11:28PM UTC coverage: 74.182% (+0.8%) from 73.339%
12000514469

push

github

web-flow
test: Improve test coverage (#289)

* test: Test `NewReader()`

* test: Test `fileReadCloser.Seek()`

* refactor: Change `io/fs` import

* refactor: Use afero for filesystem interactions

* test: Test `openReader()`

41 of 93 new or added lines in 3 files covered. (44.09%)

22 existing lines in 1 file now uncovered.

1724 of 2324 relevant lines covered (74.18%)

1.35 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.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
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✔
NEW
259
                return nil, err
×
NEW
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
func (z *Reader) folderReader(si *streamsInfo, f int) (*folderReadCloser, uint32, bool, error) {
2✔
313
        // Create a SectionReader covering all of the streams data
2✔
314
        return si.FolderReader(io.NewSectionReader(z.r, z.start, z.end-z.start), f, z.p)
2✔
315
}
2✔
316

317
const (
318
        chunkSize   = 4096
319
        searchLimit = 1 << 20 // 1 MiB
320
)
321

322
func findSignature(r io.ReaderAt, search []byte) ([]int64, error) {
2✔
323
        chunk := make([]byte, chunkSize+len(search))
2✔
324
        offsets := make([]int64, 0, 2)
2✔
325

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

2✔
329
                for i := 0; ; {
4✔
330
                        idx := bytes.Index(chunk[i:n], search)
2✔
331
                        if idx == -1 {
4✔
332
                                break
2✔
333
                        }
334

335
                        offsets = append(offsets, offset+int64(i+idx))
2✔
336
                        if offsets[0] == 0 {
4✔
337
                                // If signature is at the beginning, return immediately, it's a regular archive
2✔
338
                                return offsets, nil
2✔
339
                        }
2✔
340

341
                        i += idx + 1
2✔
342
                }
343

344
                if err != nil {
4✔
345
                        if errors.Is(err, io.EOF) {
4✔
346
                                break
2✔
347
                        }
348

349
                        return nil, fmt.Errorf("sevenzip: error reading chunk: %w", err)
×
350
                }
351
        }
352

353
        return offsets, nil
2✔
354
}
355

356
//nolint:cyclop,funlen,gocognit,gocyclo,maintidx
357
func (z *Reader) init(r io.ReaderAt, size int64) (err error) {
2✔
358
        h := crc32.NewIEEE()
2✔
359
        tra := plumbing.TeeReaderAt(r, h)
2✔
360

2✔
361
        var (
2✔
362
                signature = []byte{'7', 'z', 0xbc, 0xaf, 0x27, 0x1c}
2✔
363
                offsets   []int64
2✔
364
        )
2✔
365

2✔
366
        offsets, err = findSignature(r, signature)
2✔
367
        if err != nil {
2✔
368
                return err
×
369
        }
×
370

371
        if len(offsets) == 0 {
2✔
372
                return errFormat
×
373
        }
×
374

375
        var (
2✔
376
                sr    *io.SectionReader
2✔
377
                off   int64
2✔
378
                start startHeader
2✔
379
        )
2✔
380

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

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

389
                z.r = r
2✔
390

2✔
391
                h.Reset()
2✔
392

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

397
                // CRC of the start header should match
398
                if util.CRC32Equal(h.Sum(nil), sh.CRC) {
4✔
399
                        break
2✔
400
                }
401

402
                err = errChecksum
2✔
403
        }
404

405
        if err != nil {
2✔
406
                return err
×
407
        }
×
408

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

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

419
        z.start += off
2✔
420
        z.end += off
2✔
421

2✔
422
        h.Reset()
2✔
423

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

2✔
427
        var (
2✔
428
                id          byte
2✔
429
                header      *header
2✔
430
                streamsInfo *streamsInfo
2✔
431
        )
2✔
432

2✔
433
        if id, err = br.ReadByte(); err != nil {
2✔
434
                return fmt.Errorf("sevenzip: error reading header id: %w", err)
×
435
        }
×
436

437
        switch id {
2✔
438
        case idHeader:
2✔
439
                if header, err = readHeader(br); err != nil {
4✔
440
                        return err
2✔
441
                }
2✔
442
        case idEncodedHeader:
2✔
443
                if streamsInfo, err = readStreamsInfo(br); err != nil {
2✔
444
                        return err
×
445
                }
×
446
        default:
×
447
                return errUnexpectedID
×
448
        }
449

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

456
        // CRC should match the one from the start header
457
        if !util.CRC32Equal(h.Sum(nil), start.CRC) {
2✔
458
                return errChecksum
×
459
        }
×
460

461
        // If the header was encoded we should have sufficient information now
462
        // to decode it
463
        if streamsInfo != nil {
4✔
464
                if streamsInfo.Folders() != 1 {
2✔
465
                        return errOneHeaderStream
×
466
                }
×
467

468
                var (
2✔
469
                        fr        *folderReadCloser
2✔
470
                        crc       uint32
2✔
471
                        encrypted bool
2✔
472
                )
2✔
473

2✔
474
                fr, crc, encrypted, err = z.folderReader(streamsInfo, 0)
2✔
475
                if err != nil {
2✔
476
                        return &ReadError{
×
477
                                Encrypted: encrypted,
×
478
                                Err:       err,
×
479
                        }
×
480
                }
×
481

482
                defer func() {
4✔
483
                        err = errors.Join(err, fr.Close())
2✔
484
                }()
2✔
485

486
                if header, err = readEncodedHeader(util.ByteReadCloser(fr)); err != nil {
4✔
487
                        return &ReadError{
2✔
488
                                Encrypted: fr.hasEncryption,
2✔
489
                                Err:       err,
2✔
490
                        }
2✔
491
                }
2✔
492

493
                if crc != 0 && !util.CRC32Equal(fr.Checksum(), crc) {
2✔
494
                        return errChecksum
×
495
                }
×
496
        }
497

498
        z.si = header.streamsInfo
2✔
499

2✔
500
        // spew.Dump(header)
2✔
501
        filesPerStream := make(map[int]int, z.si.Folders())
2✔
502

2✔
503
        if header.filesInfo != nil {
4✔
504
                folder, offset := 0, int64(0)
2✔
505
                z.File = make([]*File, 0, len(header.filesInfo.file))
2✔
506
                j := 0
2✔
507

2✔
508
                for _, fh := range header.filesInfo.file {
4✔
509
                        f := new(File)
2✔
510
                        f.zip = z
2✔
511
                        f.FileHeader = fh
2✔
512

2✔
513
                        if f.FileHeader.FileInfo().IsDir() && !strings.HasSuffix(f.FileHeader.Name, "/") {
4✔
514
                                f.FileHeader.Name += "/"
2✔
515
                        }
2✔
516

517
                        if !fh.isEmptyStream && !fh.isEmptyFile {
4✔
518
                                f.folder, _ = header.streamsInfo.FileFolderAndSize(j)
2✔
519

2✔
520
                                // Make an exported copy of the folder index
2✔
521
                                f.Stream = f.folder
2✔
522

2✔
523
                                filesPerStream[f.folder]++
2✔
524

2✔
525
                                if f.folder != folder {
4✔
526
                                        offset = 0
2✔
527
                                }
2✔
528

529
                                f.offset = offset
2✔
530
                                offset += int64(f.UncompressedSize) //nolint:gosec
2✔
531
                                folder = f.folder
2✔
532
                                j++
2✔
533
                        }
534

535
                        z.File = append(z.File, f)
2✔
536
                }
537
        }
538

539
        // spew.Dump(filesPerStream)
540

541
        z.pool = make([]pool.Pooler, z.si.Folders())
2✔
542
        for i := range z.pool {
4✔
543
                var newPool pool.Constructor = pool.NewNoopPool
2✔
544

2✔
545
                if filesPerStream[i] > 1 {
4✔
546
                        newPool = pool.NewPool
2✔
547
                }
2✔
548

549
                if z.pool[i], err = newPool(); err != nil {
2✔
550
                        return err
×
551
                }
×
552
        }
553

554
        return nil
2✔
555
}
556

557
// Volumes returns the list of volumes that have been opened as part of the
558
// current archive.
559
func (rc *ReadCloser) Volumes() []string {
2✔
560
        volumes := make([]string, len(rc.f))
2✔
561
        for idx, f := range rc.f {
4✔
562
                volumes[idx] = f.Name()
2✔
563
        }
2✔
564

565
        return volumes
2✔
566
}
567

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

2✔
572
        for _, f := range rc.f {
4✔
573
                errs = append(errs, f.Close())
2✔
574
        }
2✔
575

576
        err := errors.Join(errs...)
2✔
577
        if err != nil {
2✔
578
                err = fmt.Errorf("sevenzip: error closing: %w", err)
×
579
        }
×
580

581
        return err
2✔
582
}
583

584
type fileListEntry struct {
585
        name  string
586
        file  *File
587
        isDir bool
588
        isDup bool
589
}
590

591
type fileInfoDirEntry interface {
592
        iofs.FileInfo
593
        iofs.DirEntry
594
}
595

596
func (e *fileListEntry) stat() (fileInfoDirEntry, error) {
2✔
597
        if e.isDup {
2✔
598
                return nil, errors.New(e.name + ": duplicate entries in 7-zip file") //nolint:err113
×
599
        }
×
600

601
        if !e.isDir {
4✔
602
                return headerFileInfo{&e.file.FileHeader}, nil
2✔
603
        }
2✔
604

605
        return e, nil
2✔
606
}
607

608
func (e *fileListEntry) Name() string {
2✔
609
        _, elem := split(e.name)
2✔
610

2✔
611
        return elem
2✔
612
}
2✔
613

614
func (e *fileListEntry) Size() int64         { return 0 }
2✔
615
func (e *fileListEntry) Mode() iofs.FileMode { return iofs.ModeDir | 0o555 }
2✔
616
func (e *fileListEntry) Type() iofs.FileMode { return iofs.ModeDir }
2✔
617
func (e *fileListEntry) IsDir() bool         { return true }
2✔
NEW
618
func (e *fileListEntry) Sys() interface{}    { return nil }
×
619

620
func (e *fileListEntry) ModTime() time.Time {
2✔
621
        if e.file == nil {
4✔
622
                return time.Time{}
2✔
623
        }
2✔
624

625
        return e.file.FileHeader.Modified.UTC()
×
626
}
627

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

630
func toValidName(name string) string {
2✔
631
        name = strings.ReplaceAll(name, `\`, `/`)
2✔
632

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

2✔
635
        for strings.HasPrefix(p, "../") {
2✔
636
                p = p[len("../"):]
×
637
        }
×
638

639
        return p
2✔
640
}
641

642
//nolint:cyclop,funlen
643
func (z *Reader) initFileList() {
2✔
644
        z.fileListOnce.Do(func() {
4✔
645
                files := make(map[string]int)
2✔
646
                knownDirs := make(map[string]int)
2✔
647

2✔
648
                dirs := make(map[string]struct{})
2✔
649

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

2✔
653
                        name := toValidName(file.Name)
2✔
654
                        if name == "" {
2✔
655
                                continue
×
656
                        }
657

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

×
661
                                continue
×
662
                        }
663

664
                        if idx, ok := knownDirs[name]; ok {
2✔
665
                                z.fileList[idx].isDup = true
×
666

×
667
                                continue
×
668
                        }
669

670
                        for dir := path.Dir(name); dir != "."; dir = path.Dir(dir) {
4✔
671
                                dirs[dir] = struct{}{}
2✔
672
                        }
2✔
673

674
                        idx := len(z.fileList)
2✔
675
                        entry := fileListEntry{
2✔
676
                                name:  name,
2✔
677
                                file:  file,
2✔
678
                                isDir: isDir,
2✔
679
                        }
2✔
680
                        z.fileList = append(z.fileList, entry)
2✔
681

2✔
682
                        if isDir {
2✔
683
                                knownDirs[name] = idx
×
684
                        } else {
2✔
685
                                files[name] = idx
2✔
686
                        }
2✔
687
                }
688

689
                for dir := range dirs {
4✔
690
                        if _, ok := knownDirs[dir]; !ok {
4✔
691
                                if idx, ok := files[dir]; ok {
2✔
692
                                        z.fileList[idx].isDup = true
×
693
                                } else {
2✔
694
                                        entry := fileListEntry{
2✔
695
                                                name:  dir,
2✔
696
                                                file:  nil,
2✔
697
                                                isDir: true,
2✔
698
                                        }
2✔
699
                                        z.fileList = append(z.fileList, entry)
2✔
700
                                }
2✔
701
                        }
702
                }
703

704
                sort.Slice(z.fileList, func(i, j int) bool { return fileEntryLess(z.fileList[i].name, z.fileList[j].name) })
4✔
705
        })
706
}
707

708
func fileEntryLess(x, y string) bool {
2✔
709
        xdir, xelem := split(x)
2✔
710
        ydir, yelem := split(y)
2✔
711

2✔
712
        return xdir < ydir || xdir == ydir && xelem < yelem
2✔
713
}
2✔
714

715
// Open opens the named file in the 7-zip archive, using the semantics of
716
// [fs.FS.Open]: paths are always slash separated, with no leading / or ../
717
// elements.
718
func (z *Reader) Open(name string) (iofs.File, error) {
2✔
719
        z.initFileList()
2✔
720

2✔
721
        if !iofs.ValidPath(name) {
4✔
722
                return nil, &iofs.PathError{Op: "open", Path: name, Err: iofs.ErrInvalid}
2✔
723
        }
2✔
724

725
        e := z.openLookup(name)
2✔
726
        if e == nil {
4✔
727
                return nil, &iofs.PathError{Op: "open", Path: name, Err: iofs.ErrNotExist}
2✔
728
        }
2✔
729

730
        if e.isDir {
4✔
731
                return &openDir{e, z.openReadDir(name), 0}, nil
2✔
732
        }
2✔
733

734
        rc, err := e.file.Open()
2✔
735
        if err != nil {
2✔
736
                return nil, err
×
737
        }
×
738

739
        return rc.(iofs.File), nil //nolint:forcetypeassert
2✔
740
}
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) {
×
NEW
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 · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc