-
Notifications
You must be signed in to change notification settings - Fork 0
/
rar.go
160 lines (129 loc) · 3.39 KB
/
rar.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package compressor
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"
"time"
"github.com/nwaples/rardecode/v2"
"github.com/pchchv/golog"
)
type Rar struct {
// If true, errors that occurred while reading or writing a file in the archive
// will be logged and the operation will continue for the remaining files.
ContinueOnError bool
// Password to open archives.
Password string
}
// rarFileInfo satisfies the fs.FileInfo interface for RAR entries.
type rarFileInfo struct {
fh *rardecode.FileHeader
}
var (
rarHeaderV1_5 = []byte("Rar!\x1a\x07\x00") // v1.5
rarHeaderV5_0 = []byte("Rar!\x1a\x07\x01\x00") // v5.0
)
func init() {
RegisterFormat(Rar{})
}
func (Rar) Name() string {
return ".rar"
}
func (r Rar) Match(filename string, stream io.Reader) (MatchResult, error) {
var mr MatchResult
// match filename
if strings.Contains(strings.ToLower(filename), r.Name()) {
mr.ByName = true
}
// match file header (there are two versions; allocate buffer for larger one)
buf, err := readAtMost(stream, len(rarHeaderV5_0))
if err != nil {
return mr, err
}
matchedV1_5 := len(buf) >= len(rarHeaderV1_5) &&
bytes.Equal(rarHeaderV1_5, buf[:len(rarHeaderV1_5)])
matchedV5_0 := len(buf) >= len(rarHeaderV5_0) &&
bytes.Equal(rarHeaderV5_0, buf[:len(rarHeaderV5_0)])
mr.ByStream = matchedV1_5 || matchedV5_0
return mr, nil
}
// Archive is not implemented for RAR,
// but the method exists so that Rar satisfies the ArchiveFormat interface.
func (r Rar) Archive(_ context.Context, _ io.Writer, _ []File) error {
return fmt.Errorf("not implemented because RAR is a proprietary format")
}
func (r Rar) Extract(ctx context.Context, sourceArchive io.Reader, pathsInArchive []string, handleFile FileHandler) error {
var options []rardecode.Option
if r.Password != "" {
options = append(options, rardecode.Password(r.Password))
}
rr, err := rardecode.NewReader(sourceArchive, options...)
if err != nil {
return err
}
// important to initialize to non-nil, empty value due to how fileIsIncluded works
skipDirs := skipList{}
for {
if err := ctx.Err(); err != nil {
return err // honor context cancellation
}
hdr, err := rr.Next()
if err == io.EOF {
break
}
if err != nil {
if r.ContinueOnError {
golog.Info("[ERROR] Advancing to next file in rar archive: %v", err)
continue
}
return err
}
if !fileIsIncluded(pathsInArchive, hdr.Name) {
continue
}
if fileIsIncluded(skipDirs, hdr.Name) {
continue
}
file := File{
FileInfo: rarFileInfo{hdr},
Header: hdr,
FileName: hdr.Name,
Open: func() (io.ReadCloser, error) { return io.NopCloser(rr), nil },
}
err = handleFile(ctx, file)
if errors.Is(err, fs.SkipDir) {
// if a directory, skip this path; if a file, skip the folder path
dirPath := hdr.Name
if !hdr.IsDir {
dirPath = path.Dir(hdr.Name) + "/"
}
skipDirs.add(dirPath)
} else if err != nil {
return fmt.Errorf("handling file: %s: %w", hdr.Name, err)
}
}
return nil
}
func (rfi rarFileInfo) Name() string {
return path.Base(rfi.fh.Name)
}
func (rfi rarFileInfo) Size() int64 {
return rfi.fh.UnPackedSize
}
func (rfi rarFileInfo) Mode() os.FileMode {
return rfi.fh.Mode()
}
func (rfi rarFileInfo) ModTime() time.Time {
return rfi.fh.ModificationTime
}
func (rfi rarFileInfo) IsDir() bool {
return rfi.fh.IsDir
}
func (rfi rarFileInfo) Sys() interface{} {
return nil
}