github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/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  }