github.com/dtroyer-salad/og2/v2@v2.0.0-20240412154159-c47231610877/content/file/utils.go (about) 1 /* 2 Copyright The ORAS Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package file 17 18 import ( 19 "archive/tar" 20 "compress/gzip" 21 "errors" 22 "fmt" 23 "io" 24 "os" 25 "path/filepath" 26 "strings" 27 "time" 28 29 "github.com/opencontainers/go-digest" 30 ) 31 32 // tarDirectory walks the directory specified by path, and tar those files with a new 33 // path prefix. 34 func tarDirectory(root, prefix string, w io.Writer, removeTimes bool, buf []byte) (err error) { 35 tw := tar.NewWriter(w) 36 defer func() { 37 closeErr := tw.Close() 38 if err == nil { 39 err = closeErr 40 } 41 }() 42 43 return filepath.Walk(root, func(path string, info os.FileInfo, err error) (returnErr error) { 44 if err != nil { 45 return err 46 } 47 48 // Rename path 49 name, err := filepath.Rel(root, path) 50 if err != nil { 51 return err 52 } 53 name = filepath.Join(prefix, name) 54 name = filepath.ToSlash(name) 55 56 // Generate header 57 var link string 58 mode := info.Mode() 59 if mode&os.ModeSymlink != 0 { 60 if link, err = os.Readlink(path); err != nil { 61 return err 62 } 63 } 64 header, err := tar.FileInfoHeader(info, link) 65 if err != nil { 66 return fmt.Errorf("%s: %w", path, err) 67 } 68 header.Name = name 69 header.Uid = 0 70 header.Gid = 0 71 header.Uname = "" 72 header.Gname = "" 73 74 if removeTimes { 75 header.ModTime = time.Time{} 76 header.AccessTime = time.Time{} 77 header.ChangeTime = time.Time{} 78 } 79 80 // Write file 81 if err := tw.WriteHeader(header); err != nil { 82 return fmt.Errorf("tar: %w", err) 83 } 84 if mode.IsRegular() { 85 fp, err := os.Open(path) 86 if err != nil { 87 return err 88 } 89 defer func() { 90 closeErr := fp.Close() 91 if returnErr == nil { 92 returnErr = closeErr 93 } 94 }() 95 96 if _, err := io.CopyBuffer(tw, fp, buf); err != nil { 97 return fmt.Errorf("failed to copy to %s: %w", path, err) 98 } 99 } 100 101 return nil 102 }) 103 } 104 105 // extractTarGzip decompresses the gzip 106 // and extracts tar file to a directory specified by the `dir` parameter. 107 func extractTarGzip(dir, prefix, filename, checksum string, buf []byte) (err error) { 108 fp, err := os.Open(filename) 109 if err != nil { 110 return err 111 } 112 defer func() { 113 closeErr := fp.Close() 114 if err == nil { 115 err = closeErr 116 } 117 }() 118 119 gzr, err := gzip.NewReader(fp) 120 if err != nil { 121 return err 122 } 123 defer func() { 124 closeErr := gzr.Close() 125 if err == nil { 126 err = closeErr 127 } 128 }() 129 130 var r io.Reader = gzr 131 var verifier digest.Verifier 132 if checksum != "" { 133 if digest, err := digest.Parse(checksum); err == nil { 134 verifier = digest.Verifier() 135 r = io.TeeReader(r, verifier) 136 } 137 } 138 if err := extractTarDirectory(dir, prefix, r, buf); err != nil { 139 return err 140 } 141 if verifier != nil && !verifier.Verified() { 142 return errors.New("content digest mismatch") 143 } 144 return nil 145 } 146 147 // extractTarDirectory extracts tar file to a directory specified by the `dir` 148 // parameter. The file name prefix is ensured to be the string specified by the 149 // `prefix` parameter and is trimmed. 150 func extractTarDirectory(dir, prefix string, r io.Reader, buf []byte) error { 151 tr := tar.NewReader(r) 152 for { 153 header, err := tr.Next() 154 if err != nil { 155 if err == io.EOF { 156 return nil 157 } 158 return err 159 } 160 161 // Name check 162 name := header.Name 163 path, err := ensureBasePath(dir, prefix, name) 164 if err != nil { 165 return err 166 } 167 path = filepath.Join(dir, path) 168 169 // Create content 170 switch header.Typeflag { 171 case tar.TypeReg: 172 err = writeFile(path, tr, header.FileInfo().Mode(), buf) 173 case tar.TypeDir: 174 err = os.MkdirAll(path, header.FileInfo().Mode()) 175 case tar.TypeLink: 176 var target string 177 if target, err = ensureLinkPath(dir, prefix, path, header.Linkname); err == nil { 178 err = os.Link(target, path) 179 } 180 case tar.TypeSymlink: 181 var target string 182 if target, err = ensureLinkPath(dir, prefix, path, header.Linkname); err == nil { 183 err = os.Symlink(target, path) 184 } 185 default: 186 continue // Non-regular files are skipped 187 } 188 if err != nil { 189 return err 190 } 191 192 // Change access time and modification time if possible (error ignored) 193 os.Chtimes(path, header.AccessTime, header.ModTime) 194 } 195 } 196 197 // ensureBasePath ensures the target path is in the base path, 198 // returning its relative path to the base path. 199 // target can be either an absolute path or a relative path. 200 func ensureBasePath(baseAbs, baseRel, target string) (string, error) { 201 base := baseRel 202 if filepath.IsAbs(target) { 203 // ensure base and target are consistent 204 base = baseAbs 205 } 206 path, err := filepath.Rel(base, target) 207 if err != nil { 208 return "", err 209 } 210 cleanPath := filepath.ToSlash(filepath.Clean(path)) 211 if cleanPath == ".." || strings.HasPrefix(cleanPath, "../") { 212 return "", fmt.Errorf("%q is outside of %q", target, baseRel) 213 } 214 215 // No symbolic link allowed in the relative path 216 dir := filepath.Dir(path) 217 for dir != "." { 218 if info, err := os.Lstat(filepath.Join(baseAbs, dir)); err != nil { 219 if !os.IsNotExist(err) { 220 return "", err 221 } 222 } else if info.Mode()&os.ModeSymlink != 0 { 223 return "", fmt.Errorf("no symbolic link allowed between %q and %q", baseRel, target) 224 } 225 dir = filepath.Dir(dir) 226 } 227 228 return path, nil 229 } 230 231 // ensureLinkPath ensures the target path pointed by the link is in the base 232 // path. It returns target path if validated. 233 func ensureLinkPath(baseAbs, baseRel, link, target string) (string, error) { 234 // resolve link 235 path := target 236 if !filepath.IsAbs(target) { 237 path = filepath.Join(filepath.Dir(link), target) 238 } 239 // ensure path is under baseAbs or baseRel 240 if _, err := ensureBasePath(baseAbs, baseRel, path); err != nil { 241 return "", err 242 } 243 return target, nil 244 } 245 246 // writeFile writes content to the file specified by the `path` parameter. 247 func writeFile(path string, r io.Reader, perm os.FileMode, buf []byte) (err error) { 248 file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) 249 if err != nil { 250 return err 251 } 252 defer func() { 253 closeErr := file.Close() 254 if err == nil { 255 err = closeErr 256 } 257 }() 258 259 _, err = io.CopyBuffer(file, r, buf) 260 return err 261 }