1package archiver 2 3import ( 4 "archive/tar" 5 "bytes" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "strconv" 11 "strings" 12) 13 14// Tar is for Tar format 15var Tar tarFormat 16 17func init() { 18 RegisterFormat("Tar", Tar) 19} 20 21type tarFormat struct{} 22 23func (tarFormat) Match(filename string) bool { 24 return strings.HasSuffix(strings.ToLower(filename), ".tar") || isTar(filename) 25} 26 27const tarBlockSize int = 512 28 29// isTar checks the file has the Tar format header by reading its beginning 30// block. 31func isTar(tarPath string) bool { 32 f, err := os.Open(tarPath) 33 if err != nil { 34 return false 35 } 36 defer f.Close() 37 38 buf := make([]byte, tarBlockSize) 39 if _, err = io.ReadFull(f, buf); err != nil { 40 return false 41 } 42 43 return hasTarHeader(buf) 44} 45 46// hasTarHeader checks passed bytes has a valid tar header or not. buf must 47// contain at least 512 bytes and if not, it always returns false. 48func hasTarHeader(buf []byte) bool { 49 if len(buf) < tarBlockSize { 50 return false 51 } 52 53 b := buf[148:156] 54 b = bytes.Trim(b, " \x00") // clean up all spaces and null bytes 55 if len(b) == 0 { 56 return false // unknown format 57 } 58 hdrSum, err := strconv.ParseUint(string(b), 8, 64) 59 if err != nil { 60 return false 61 } 62 63 // According to the go official archive/tar, Sun tar uses signed byte 64 // values so this calcs both signed and unsigned 65 var usum uint64 66 var sum int64 67 for i, c := range buf { 68 if 148 <= i && i < 156 { 69 c = ' ' // checksum field itself is counted as branks 70 } 71 usum += uint64(uint8(c)) 72 sum += int64(int8(c)) 73 } 74 75 if hdrSum != usum && int64(hdrSum) != sum { 76 return false // invalid checksum 77 } 78 79 return true 80} 81 82// Write outputs a .tar file to a Writer containing the 83// contents of files listed in filePaths. File paths can 84// be those of regular files or directories. Regular 85// files are stored at the 'root' of the archive, and 86// directories are recursively added. 87func (tarFormat) Write(output io.Writer, filePaths []string) error { 88 return writeTar(filePaths, output, "") 89} 90 91// Make creates a .tar file at tarPath containing the 92// contents of files listed in filePaths. File paths can 93// be those of regular files or directories. Regular 94// files are stored at the 'root' of the archive, and 95// directories are recursively added. 96func (tarFormat) Make(tarPath string, filePaths []string) error { 97 out, err := os.Create(tarPath) 98 if err != nil { 99 return fmt.Errorf("error creating %s: %v", tarPath, err) 100 } 101 defer out.Close() 102 103 return writeTar(filePaths, out, tarPath) 104} 105 106func writeTar(filePaths []string, output io.Writer, dest string) error { 107 tarWriter := tar.NewWriter(output) 108 defer tarWriter.Close() 109 110 return tarball(filePaths, tarWriter, dest) 111} 112 113// tarball writes all files listed in filePaths into tarWriter, which is 114// writing into a file located at dest. 115func tarball(filePaths []string, tarWriter *tar.Writer, dest string) error { 116 for _, fpath := range filePaths { 117 err := tarFile(tarWriter, fpath, dest) 118 if err != nil { 119 return err 120 } 121 } 122 return nil 123} 124 125// tarFile writes the file at source into tarWriter. It does so 126// recursively for directories. 127func tarFile(tarWriter *tar.Writer, source, dest string) error { 128 sourceInfo, err := os.Stat(source) 129 if err != nil { 130 return fmt.Errorf("%s: stat: %v", source, err) 131 } 132 133 var baseDir string 134 if sourceInfo.IsDir() { 135 baseDir = filepath.Base(source) 136 } 137 138 return filepath.Walk(source, func(path string, info os.FileInfo, err error) error { 139 if err != nil { 140 return fmt.Errorf("error walking to %s: %v", path, err) 141 } 142 143 header, err := tar.FileInfoHeader(info, path) 144 if err != nil { 145 return fmt.Errorf("%s: making header: %v", path, err) 146 } 147 148 if baseDir != "" { 149 header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source)) 150 } 151 152 if header.Name == dest { 153 // our new tar file is inside the directory being archived; skip it 154 return nil 155 } 156 157 if info.IsDir() { 158 header.Name += "/" 159 } 160 161 err = tarWriter.WriteHeader(header) 162 if err != nil { 163 return fmt.Errorf("%s: writing header: %v", path, err) 164 } 165 166 if info.IsDir() { 167 return nil 168 } 169 170 if header.Typeflag == tar.TypeReg { 171 file, err := os.Open(path) 172 if err != nil { 173 return fmt.Errorf("%s: open: %v", path, err) 174 } 175 defer file.Close() 176 177 _, err = io.CopyN(tarWriter, file, info.Size()) 178 if err != nil && err != io.EOF { 179 return fmt.Errorf("%s: copying contents: %v", path, err) 180 } 181 } 182 return nil 183 }) 184} 185 186// Read untars a .tar file read from a Reader and puts 187// the contents into destination. 188func (tarFormat) Read(input io.Reader, destination string) error { 189 return untar(tar.NewReader(input), destination) 190} 191 192// Open untars source and puts the contents into destination. 193func (tarFormat) Open(source, destination string) error { 194 f, err := os.Open(source) 195 if err != nil { 196 return fmt.Errorf("%s: failed to open archive: %v", source, err) 197 } 198 defer f.Close() 199 200 return Tar.Read(f, destination) 201} 202 203// untar un-tarballs the contents of tr into destination. 204func untar(tr *tar.Reader, destination string) error { 205 for { 206 header, err := tr.Next() 207 if err == io.EOF { 208 break 209 } else if err != nil { 210 return err 211 } 212 213 if err := untarFile(tr, header, destination); err != nil { 214 return err 215 } 216 } 217 return nil 218} 219 220// untarFile untars a single file from tr with header header into destination. 221func untarFile(tr *tar.Reader, header *tar.Header, destination string) error { 222 err := sanitizeExtractPath(header.Name, destination) 223 if err != nil { 224 return err 225 } 226 227 destpath := filepath.Join(destination, header.Name) 228 229 switch header.Typeflag { 230 case tar.TypeDir: 231 return mkdir(destpath) 232 case tar.TypeReg, tar.TypeRegA, tar.TypeChar, tar.TypeBlock, tar.TypeFifo: 233 return writeNewFile(destpath, tr, header.FileInfo().Mode()) 234 case tar.TypeSymlink: 235 return writeNewSymbolicLink(destpath, header.Linkname) 236 case tar.TypeLink: 237 return writeNewHardLink(destpath, filepath.Join(destination, header.Linkname)) 238 case tar.TypeXGlobalHeader: 239 // ignore the pax global header from git generated tarballs 240 return nil 241 default: 242 return fmt.Errorf("%s: unknown type flag: %c", header.Name, header.Typeflag) 243 } 244} 245