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