github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/tarutil/tar.go (about) 1 // Copyright 2019-2021 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package tarutil 6 7 import ( 8 "archive/tar" 9 "bytes" 10 "fmt" 11 "io" 12 "log" 13 "os" 14 "path/filepath" 15 16 "github.com/mvdan/u-root-coreutils/pkg/upath" 17 ) 18 19 // Opts contains options for creating and extracting tar files. 20 type Opts struct { 21 // Filters are applied to each file while creating or extracting a tar 22 // archive. The filter can modify the tar.Header struct. If the filter 23 // returns false, the file is omitted. 24 Filters []Filter 25 26 // By default, when creating a tar archive, all directories are walked 27 // to include all sub-directories. Set to true to prevent this 28 // behavior. 29 NoRecursion bool 30 31 // Change to this directory before any operations. This is equivalent 32 // to "tar -C DIR". 33 ChangeDirectory string 34 } 35 36 // passesFilters returns true if the given file passes all filters, false otherwise. 37 func passesFilters(hdr *tar.Header, filters []Filter) bool { 38 for _, filter := range filters { 39 if !filter(hdr) { 40 return false 41 } 42 } 43 return true 44 } 45 46 // applyToArchive applies function f to all files in the given archive 47 func applyToArchive(tarFile io.Reader, f func(tr *tar.Reader, hdr *tar.Header) error) error { 48 tr := tar.NewReader(tarFile) 49 for { 50 hdr, err := tr.Next() 51 if err == io.EOF { 52 break 53 } 54 if err != nil { 55 return err 56 } 57 if err := f(tr, hdr); err != nil { 58 return err 59 } 60 } 61 return nil 62 } 63 64 // ListArchive lists the contents of the given tar archive. 65 func ListArchive(tarFile io.Reader) error { 66 return applyToArchive(tarFile, func(tr *tar.Reader, hdr *tar.Header) error { 67 fmt.Println(hdr.Name) 68 return nil 69 }) 70 } 71 72 // ExtractDir extracts all the contents of the tar file to the given directory. 73 func ExtractDir(tarFile io.Reader, dir string, opts *Opts) error { 74 if opts == nil { 75 opts = &Opts{} 76 } 77 78 // Simulate a "cd" to another directory. 79 if !filepath.IsAbs(dir) { 80 dir = filepath.Join(opts.ChangeDirectory, dir) 81 } 82 83 fi, err := os.Stat(dir) 84 if os.IsNotExist(err) { 85 if err := os.Mkdir(dir, os.ModePerm); err != nil { 86 return fmt.Errorf("could not create directory %s: %v", dir, err) 87 } 88 } else if err != nil || !fi.IsDir() { 89 return fmt.Errorf("could not stat directory %s: %v", dir, err) 90 } 91 92 return applyToArchive(tarFile, func(tr *tar.Reader, hdr *tar.Header) error { 93 if !passesFilters(hdr, opts.Filters) { 94 return nil 95 } 96 return createFileInRoot(hdr, tr, dir) 97 }) 98 } 99 100 // CreateTar creates a new tar file with all the contents of a directory. 101 func CreateTar(tarFile io.Writer, files []string, opts *Opts) error { 102 if opts == nil { 103 opts = &Opts{} 104 } 105 106 tw := tar.NewWriter(tarFile) 107 for _, bFile := range files { 108 // Simulate a "cd" to another directory. There are 3 parts to 109 // the file path: 110 // a) The path passed to ChangeDirectory 111 // b) The path passed in files 112 // c) The path in the current walk 113 // I prefixed corresponding a/b/c onto the variable name as an 114 // aid. For example abFile is the filepath of a+b. 115 abFile := filepath.Join(opts.ChangeDirectory, bFile) 116 if filepath.IsAbs(bFile) { 117 // "cd" does nothing if the file is absolute. 118 abFile = bFile 119 } 120 121 walk := filepath.Walk 122 if opts.NoRecursion { 123 // This "walk" function does not recurse. 124 walk = func(root string, walkFn filepath.WalkFunc) error { 125 fi, err := os.Lstat(root) 126 return walkFn(root, fi, err) 127 } 128 } 129 130 err := walk(abFile, func(abcPath string, info os.FileInfo, err error) error { 131 if err != nil { 132 return err 133 } 134 135 // The record should not contain the ChangeDirectory 136 // path, so we need to derive bc from abc. 137 bcPath, err := filepath.Rel(opts.ChangeDirectory, abcPath) 138 if err != nil { 139 return err 140 } 141 if filepath.IsAbs(bFile) { 142 // "cd" does nothing if the file is absolute. 143 bcPath = abcPath 144 } 145 146 var symlink string 147 if info.Mode()&os.ModeSymlink == os.ModeSymlink { 148 if symlink, err = os.Readlink(abcPath); err != nil { 149 return err 150 } 151 } 152 hdr, err := tar.FileInfoHeader(info, symlink) 153 if err != nil { 154 return err 155 } 156 hdr.Name = bcPath 157 if !passesFilters(hdr, opts.Filters) { 158 return nil 159 } 160 switch hdr.Typeflag { 161 case tar.TypeLink, tar.TypeSymlink, tar.TypeChar, tar.TypeBlock, tar.TypeDir, tar.TypeFifo: 162 if err := tw.WriteHeader(hdr); err != nil { 163 return err 164 } 165 default: 166 f, err := os.Open(abcPath) 167 if err != nil { 168 return err 169 } 170 171 var r io.Reader = f 172 if hdr.Size == 0 { 173 // Some files don't report their size correctly 174 // (ex: procfs), so we use an intermediary 175 // buffer to determine size. 176 b := &bytes.Buffer{} 177 if _, err := io.Copy(b, f); err != nil { 178 f.Close() 179 return err 180 } 181 f.Close() 182 hdr.Size = int64(b.Len()) 183 r = b 184 } 185 186 if err := tw.WriteHeader(hdr); err != nil { 187 return err 188 } 189 if _, err := io.Copy(tw, r); err != nil { 190 return err 191 } 192 } 193 return nil 194 }) 195 if err != nil { 196 return err 197 } 198 } 199 if err := tw.Close(); err != nil { 200 return err 201 } 202 return nil 203 } 204 205 func createFileInRoot(hdr *tar.Header, r io.Reader, rootDir string) error { 206 fi := hdr.FileInfo() 207 path, err := upath.SafeFilepathJoin(rootDir, hdr.Name) 208 if err != nil { 209 // The behavior is to skip files which are unsafe due to 210 // zipslip, but continue extracting everything else. 211 log.Printf("Warning: Skipping file %q due to: %v", hdr.Name, err) 212 return nil 213 } 214 215 switch fi.Mode() & os.ModeType { 216 case os.ModeSymlink: 217 // TODO: support symlinks 218 return fmt.Errorf("symlinks not yet supported: %q", path) 219 220 case os.FileMode(0): 221 f, err := os.Create(path) 222 if err != nil { 223 return err 224 } 225 if _, err := io.Copy(f, r); err != nil { 226 f.Close() 227 return err 228 } 229 if err := f.Close(); err != nil { 230 return err 231 } 232 233 case os.ModeDir: 234 if err := os.MkdirAll(path, fi.Mode()&os.ModePerm); err != nil { 235 return err 236 } 237 238 case os.ModeDevice: 239 // TODO: support block device 240 return fmt.Errorf("block device not yet supported: %q", path) 241 242 case os.ModeCharDevice: 243 // TODO: support char device 244 return fmt.Errorf("char device not yet supported: %q", path) 245 246 default: 247 return fmt.Errorf("%q: Unknown type %#o", path, fi.Mode()&os.ModeType) 248 } 249 250 if err := os.Chmod(path, fi.Mode()&os.ModePerm); err != nil { 251 return fmt.Errorf("error setting mode %#o on %q: %v", 252 fi.Mode()&os.ModePerm, path, err) 253 } 254 // TODO: also set ownership, etc... 255 return nil 256 } 257 258 // Filter is applied to each file while creating or extracting a tar archive. 259 // The filter can modify the tar.Header struct. If the filter returns false, 260 // the file is omitted. 261 type Filter func(hdr *tar.Header) bool 262 263 // NoFilter does not filter or modify any files. 264 func NoFilter(hdr *tar.Header) bool { 265 return true 266 } 267 268 // VerboseFilter prints the name of every file. 269 func VerboseFilter(hdr *tar.Header) bool { 270 fmt.Println(hdr.Name) 271 return true 272 } 273 274 // VerboseLogFilter logs the name of every file. 275 func VerboseLogFilter(hdr *tar.Header) bool { 276 log.Println(hdr.Name) 277 return true 278 } 279 280 // SafeFilter filters out all files which are not regular and not directories. 281 // It also sets appropriate permissions. 282 func SafeFilter(hdr *tar.Header) bool { 283 if hdr.Typeflag == tar.TypeDir { 284 hdr.Mode = 0o770 285 return true 286 } 287 if hdr.Typeflag == tar.TypeReg { 288 hdr.Mode = 0o660 289 return true 290 } 291 return false 292 }