1package archiver
2
3import (
4	"bytes"
5	"fmt"
6	"io"
7	"log"
8	"os"
9	"path"
10	"path/filepath"
11	"strings"
12	"time"
13
14	"github.com/nwaples/rardecode"
15)
16
17// Rar provides facilities for reading RAR archives.
18// See https://www.rarlab.com/technote.htm.
19type Rar struct {
20	// Whether to overwrite existing files; if false,
21	// an error is returned if the file exists.
22	OverwriteExisting bool
23
24	// Whether to make all the directories necessary
25	// to create a rar archive in the desired path.
26	MkdirAll bool
27
28	// A single top-level folder can be implicitly
29	// created by the Unarchive method if the files
30	// to be extracted from the archive do not all
31	// have a common root. This roughly mimics the
32	// behavior of archival tools integrated into OS
33	// file browsers which create a subfolder to
34	// avoid unexpectedly littering the destination
35	// folder with potentially many files, causing a
36	// problematic cleanup/organization situation.
37	// This feature is available for both creation
38	// and extraction of archives, but may be slightly
39	// inefficient with lots and lots of files,
40	// especially on extraction.
41	ImplicitTopLevelFolder bool
42
43	// If true, errors encountered during reading
44	// or writing a single file will be logged and
45	// the operation will continue on remaining files.
46	ContinueOnError bool
47
48	// The password to open archives (optional).
49	Password string
50
51	rr *rardecode.Reader     // underlying stream reader
52	rc *rardecode.ReadCloser // supports multi-volume archives (files only)
53}
54
55// CheckExt ensures the file extension matches the format.
56func (*Rar) CheckExt(filename string) error {
57	if !strings.HasSuffix(filename, ".rar") {
58		return fmt.Errorf("filename must have a .rar extension")
59	}
60	return nil
61}
62
63// Unarchive unpacks the .rar file at source to destination.
64// Destination will be treated as a folder name. It supports
65// multi-volume archives.
66func (r *Rar) Unarchive(source, destination string) error {
67	if !fileExists(destination) && r.MkdirAll {
68		err := mkdir(destination)
69		if err != nil {
70			return fmt.Errorf("preparing destination: %v", err)
71		}
72	}
73
74	// if the files in the archive do not all share a common
75	// root, then make sure we extract to a single subfolder
76	// rather than potentially littering the destination...
77	if r.ImplicitTopLevelFolder {
78		var err error
79		destination, err = r.addTopLevelFolder(source, destination)
80		if err != nil {
81			return fmt.Errorf("scanning source archive: %v", err)
82		}
83	}
84
85	err := r.OpenFile(source)
86	if err != nil {
87		return fmt.Errorf("opening rar archive for reading: %v", err)
88	}
89	defer r.Close()
90
91	for {
92		err := r.unrarNext(destination)
93		if err == io.EOF {
94			break
95		}
96		if err != nil {
97			if r.ContinueOnError {
98				log.Printf("[ERROR] Reading file in rar archive: %v", err)
99				continue
100			}
101			return fmt.Errorf("reading file in rar archive: %v", err)
102		}
103	}
104
105	return nil
106}
107
108// addTopLevelFolder scans the files contained inside
109// the tarball named sourceArchive and returns a modified
110// destination if all the files do not share the same
111// top-level folder.
112func (r *Rar) addTopLevelFolder(sourceArchive, destination string) (string, error) {
113	file, err := os.Open(sourceArchive)
114	if err != nil {
115		return "", fmt.Errorf("opening source archive: %v", err)
116	}
117	defer file.Close()
118
119	rc, err := rardecode.NewReader(file, r.Password)
120	if err != nil {
121		return "", fmt.Errorf("creating archive reader: %v", err)
122	}
123
124	var files []string
125	for {
126		hdr, err := rc.Next()
127		if err == io.EOF {
128			break
129		}
130		if err != nil {
131			return "", fmt.Errorf("scanning tarball's file listing: %v", err)
132		}
133		files = append(files, hdr.Name)
134	}
135
136	if multipleTopLevels(files) {
137		destination = filepath.Join(destination, folderNameFromFileName(sourceArchive))
138	}
139
140	return destination, nil
141}
142
143func (r *Rar) unrarNext(to string) error {
144	f, err := r.Read()
145	if err != nil {
146		return err // don't wrap error; calling loop must break on io.EOF
147	}
148	header, ok := f.Header.(*rardecode.FileHeader)
149	if !ok {
150		return fmt.Errorf("expected header to be *rardecode.FileHeader but was %T", f.Header)
151	}
152	return r.unrarFile(f, filepath.Join(to, header.Name))
153}
154
155func (r *Rar) unrarFile(f File, to string) error {
156	// do not overwrite existing files, if configured
157	if !f.IsDir() && !r.OverwriteExisting && fileExists(to) {
158		return fmt.Errorf("file already exists: %s", to)
159	}
160
161	hdr, ok := f.Header.(*rardecode.FileHeader)
162	if !ok {
163		return fmt.Errorf("expected header to be *rardecode.FileHeader but was %T", f.Header)
164	}
165
166	// if files come before their containing folders, then we must
167	// create their folders before writing the file
168	err := mkdir(filepath.Dir(to))
169	if err != nil {
170		return fmt.Errorf("making parent directories: %v", err)
171	}
172
173	return writeNewFile(to, r.rr, hdr.Mode())
174}
175
176// OpenFile opens filename for reading. This method supports
177// multi-volume archives, whereas Open does not (but Open
178// supports any stream, not just files).
179func (r *Rar) OpenFile(filename string) error {
180	if r.rr != nil {
181		return fmt.Errorf("rar archive is already open for reading")
182	}
183	var err error
184	r.rc, err = rardecode.OpenReader(filename, r.Password)
185	if err != nil {
186		return err
187	}
188	r.rr = &r.rc.Reader
189	return nil
190}
191
192// Open opens t for reading an archive from
193// in. The size parameter is not used.
194func (r *Rar) Open(in io.Reader, size int64) error {
195	if r.rr != nil {
196		return fmt.Errorf("rar archive is already open for reading")
197	}
198	var err error
199	r.rr, err = rardecode.NewReader(in, r.Password)
200	return err
201}
202
203// Read reads the next file from t, which must have
204// already been opened for reading. If there are no
205// more files, the error is io.EOF. The File must
206// be closed when finished reading from it.
207func (r *Rar) Read() (File, error) {
208	if r.rr == nil {
209		return File{}, fmt.Errorf("rar archive is not open")
210	}
211
212	hdr, err := r.rr.Next()
213	if err != nil {
214		return File{}, err // don't wrap error; preserve io.EOF
215	}
216
217	file := File{
218		FileInfo:   rarFileInfo{hdr},
219		Header:     hdr,
220		ReadCloser: ReadFakeCloser{r.rr},
221	}
222
223	return file, nil
224}
225
226// Close closes the rar archive(s) opened by Create and Open.
227func (r *Rar) Close() error {
228	var err error
229	if r.rc != nil {
230		rc := r.rc
231		r.rc = nil
232		err = rc.Close()
233	}
234	if r.rr != nil {
235		r.rr = nil
236	}
237	return err
238}
239
240// Walk calls walkFn for each visited item in archive.
241func (r *Rar) Walk(archive string, walkFn WalkFunc) error {
242	file, err := os.Open(archive)
243	if err != nil {
244		return fmt.Errorf("opening archive file: %v", err)
245	}
246	defer file.Close()
247
248	err = r.Open(file, 0)
249	if err != nil {
250		return fmt.Errorf("opening archive: %v", err)
251	}
252	defer r.Close()
253
254	for {
255		f, err := r.Read()
256		if err == io.EOF {
257			break
258		}
259		if err != nil {
260			if r.ContinueOnError {
261				log.Printf("[ERROR] Opening next file: %v", err)
262				continue
263			}
264			return fmt.Errorf("opening next file: %v", err)
265		}
266		err = walkFn(f)
267		if err != nil {
268			if err == ErrStopWalk {
269				break
270			}
271			if r.ContinueOnError {
272				log.Printf("[ERROR] Walking %s: %v", f.Name(), err)
273				continue
274			}
275			return fmt.Errorf("walking %s: %v", f.Name(), err)
276		}
277	}
278
279	return nil
280}
281
282// Extract extracts a single file from the rar archive.
283// If the target is a directory, the entire folder will
284// be extracted into destination.
285func (r *Rar) Extract(source, target, destination string) error {
286	// target refers to a path inside the archive, which should be clean also
287	target = path.Clean(target)
288
289	// if the target ends up being a directory, then
290	// we will continue walking and extracting files
291	// until we are no longer within that directory
292	var targetDirPath string
293
294	return r.Walk(source, func(f File) error {
295		th, ok := f.Header.(*rardecode.FileHeader)
296		if !ok {
297			return fmt.Errorf("expected header to be *rardecode.FileHeader but was %T", f.Header)
298		}
299
300		// importantly, cleaning the path strips tailing slash,
301		// which must be appended to folders within the archive
302		name := path.Clean(th.Name)
303		if f.IsDir() && target == name {
304			targetDirPath = path.Dir(name)
305		}
306
307		if within(target, th.Name) {
308			// either this is the exact file we want, or is
309			// in the directory we want to extract
310
311			// build the filename we will extract to
312			end, err := filepath.Rel(targetDirPath, th.Name)
313			if err != nil {
314				return fmt.Errorf("relativizing paths: %v", err)
315			}
316			joined := filepath.Join(destination, end)
317
318			err = r.unrarFile(f, joined)
319			if err != nil {
320				return fmt.Errorf("extracting file %s: %v", th.Name, err)
321			}
322
323			// if our target was not a directory, stop walk
324			if targetDirPath == "" {
325				return ErrStopWalk
326			}
327		} else if targetDirPath != "" {
328			// finished walking the entire directory
329			return ErrStopWalk
330		}
331
332		return nil
333	})
334}
335
336// Match returns true if the format of file matches this
337// type's format. It should not affect reader position.
338func (*Rar) Match(file io.ReadSeeker) (bool, error) {
339	currentPos, err := file.Seek(0, io.SeekCurrent)
340	if err != nil {
341		return false, err
342	}
343	_, err = file.Seek(0, 0)
344	if err != nil {
345		return false, err
346	}
347	defer file.Seek(currentPos, io.SeekStart)
348
349	buf := make([]byte, 8)
350	if n, err := file.Read(buf); err != nil || n < 8 {
351		return false, nil
352	}
353	hasTarHeader := bytes.Equal(buf[:7], []byte("Rar!\x1a\x07\x00")) || // ver 1.5
354		bytes.Equal(buf, []byte("Rar!\x1a\x07\x01\x00")) // ver 5.0
355	return hasTarHeader, nil
356}
357
358func (r *Rar) String() string { return "rar" }
359
360// NewRar returns a new, default instance ready to be customized and used.
361func NewRar() *Rar {
362	return &Rar{
363		MkdirAll: true,
364	}
365}
366
367type rarFileInfo struct {
368	fh *rardecode.FileHeader
369}
370
371func (rfi rarFileInfo) Name() string       { return rfi.fh.Name }
372func (rfi rarFileInfo) Size() int64        { return rfi.fh.UnPackedSize }
373func (rfi rarFileInfo) Mode() os.FileMode  { return rfi.fh.Mode() }
374func (rfi rarFileInfo) ModTime() time.Time { return rfi.fh.ModificationTime }
375func (rfi rarFileInfo) IsDir() bool        { return rfi.fh.IsDir }
376func (rfi rarFileInfo) Sys() interface{}   { return nil }
377
378// Compile-time checks to ensure type implements desired interfaces.
379var (
380	_ = Reader(new(Rar))
381	_ = Unarchiver(new(Rar))
382	_ = Walker(new(Rar))
383	_ = Extractor(new(Rar))
384	_ = Matcher(new(Rar))
385	_ = ExtensionChecker(new(Rar))
386	_ = os.FileInfo(rarFileInfo{})
387)
388
389// DefaultRar is a default instance that is conveniently ready to use.
390var DefaultRar = NewRar()
391