Move archive to pkg/archive
Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
		
							
								
								
									
										816
									
								
								pkg/archive/tar.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										816
									
								
								pkg/archive/tar.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,816 @@ | ||||
| /* | ||||
|    Copyright The containerd Authors. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package archive | ||||
|  | ||||
| import ( | ||||
| 	"archive/tar" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/containerd/containerd/v2/pkg/archive/tarheader" | ||||
| 	"github.com/containerd/containerd/v2/pkg/epoch" | ||||
| 	"github.com/containerd/containerd/v2/pkg/userns" | ||||
| 	"github.com/containerd/continuity/fs" | ||||
| 	"github.com/containerd/log" | ||||
| ) | ||||
|  | ||||
| var bufPool = &sync.Pool{ | ||||
| 	New: func() interface{} { | ||||
| 		buffer := make([]byte, 32*1024) | ||||
| 		return &buffer | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| var errInvalidArchive = errors.New("invalid archive") | ||||
|  | ||||
| // Diff returns a tar stream of the computed filesystem | ||||
| // difference between the provided directories. | ||||
| // | ||||
| // Produces a tar using OCI style file markers for deletions. Deleted | ||||
| // files will be prepended with the prefix ".wh.". This style is | ||||
| // based off AUFS whiteouts. | ||||
| // See https://github.com/opencontainers/image-spec/blob/main/layer.md | ||||
| func Diff(ctx context.Context, a, b string, opts ...WriteDiffOpt) io.ReadCloser { | ||||
| 	r, w := io.Pipe() | ||||
|  | ||||
| 	go func() { | ||||
| 		err := WriteDiff(ctx, w, a, b, opts...) | ||||
| 		if err != nil { | ||||
| 			log.G(ctx).WithError(err).Debugf("write diff failed") | ||||
| 		} | ||||
| 		if err = w.CloseWithError(err); err != nil { | ||||
| 			log.G(ctx).WithError(err).Debugf("closing tar pipe failed") | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	return r | ||||
| } | ||||
|  | ||||
| // WriteDiff writes a tar stream of the computed difference between the | ||||
| // provided paths. | ||||
| // | ||||
| // Produces a tar using OCI style file markers for deletions. Deleted | ||||
| // files will be prepended with the prefix ".wh.". This style is | ||||
| // based off AUFS whiteouts. | ||||
| // See https://github.com/opencontainers/image-spec/blob/main/layer.md | ||||
| func WriteDiff(ctx context.Context, w io.Writer, a, b string, opts ...WriteDiffOpt) error { | ||||
| 	var options WriteDiffOptions | ||||
| 	for _, opt := range opts { | ||||
| 		if err := opt(&options); err != nil { | ||||
| 			return fmt.Errorf("failed to apply option: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 	if tm := epoch.FromContext(ctx); tm != nil && options.SourceDateEpoch == nil { | ||||
| 		options.SourceDateEpoch = tm | ||||
| 	} | ||||
|  | ||||
| 	if options.writeDiffFunc == nil { | ||||
| 		options.writeDiffFunc = writeDiffNaive | ||||
| 	} | ||||
|  | ||||
| 	return options.writeDiffFunc(ctx, w, a, b, options) | ||||
| } | ||||
|  | ||||
| // writeDiffNaive writes a tar stream of the computed difference between the | ||||
| // provided directories on disk. | ||||
| // | ||||
| // Produces a tar using OCI style file markers for deletions. Deleted | ||||
| // files will be prepended with the prefix ".wh.". This style is | ||||
| // based off AUFS whiteouts. | ||||
| // See https://github.com/opencontainers/image-spec/blob/main/layer.md | ||||
| func writeDiffNaive(ctx context.Context, w io.Writer, a, b string, o WriteDiffOptions) error { | ||||
| 	var opts []ChangeWriterOpt | ||||
| 	if o.SourceDateEpoch != nil { | ||||
| 		opts = append(opts, WithModTimeUpperBound(*o.SourceDateEpoch)) | ||||
| 		// Since containerd v2.0, the whiteout timestamps are set to zero (1970-01-01), | ||||
| 		// not to the source date epoch | ||||
| 	} | ||||
| 	cw := NewChangeWriter(w, b, opts...) | ||||
| 	err := fs.Changes(ctx, a, b, cw.HandleChange) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to create diff tar stream: %w", err) | ||||
| 	} | ||||
| 	return cw.Close() | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	// whiteoutPrefix prefix means file is a whiteout. If this is followed by a | ||||
| 	// filename this means that file has been removed from the base layer. | ||||
| 	// See https://github.com/opencontainers/image-spec/blob/main/layer.md#whiteouts | ||||
| 	whiteoutPrefix = ".wh." | ||||
|  | ||||
| 	// whiteoutMetaPrefix prefix means whiteout has a special meaning and is not | ||||
| 	// for removing an actual file. Normally these files are excluded from exported | ||||
| 	// archives. | ||||
| 	whiteoutMetaPrefix = whiteoutPrefix + whiteoutPrefix | ||||
|  | ||||
| 	// whiteoutOpaqueDir file means directory has been made opaque - meaning | ||||
| 	// readdir calls to this directory do not follow to lower layers. | ||||
| 	whiteoutOpaqueDir = whiteoutMetaPrefix + ".opq" | ||||
|  | ||||
| 	paxSchilyXattr = "SCHILY.xattr." | ||||
|  | ||||
| 	userXattrPrefix = "user." | ||||
| ) | ||||
|  | ||||
| // Apply applies a tar stream of an OCI style diff tar. | ||||
| // See https://github.com/opencontainers/image-spec/blob/main/layer.md#applying-changesets | ||||
| func Apply(ctx context.Context, root string, r io.Reader, opts ...ApplyOpt) (int64, error) { | ||||
| 	root = filepath.Clean(root) | ||||
|  | ||||
| 	var options ApplyOptions | ||||
| 	for _, opt := range opts { | ||||
| 		if err := opt(&options); err != nil { | ||||
| 			return 0, fmt.Errorf("failed to apply option: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 	if options.Filter == nil { | ||||
| 		options.Filter = all | ||||
| 	} | ||||
| 	if options.applyFunc == nil { | ||||
| 		options.applyFunc = applyNaive | ||||
| 	} | ||||
|  | ||||
| 	return options.applyFunc(ctx, root, r, options) | ||||
| } | ||||
|  | ||||
| // applyNaive applies a tar stream of an OCI style diff tar to a directory | ||||
| // applying each file as either a whole file or whiteout. | ||||
| // See https://github.com/opencontainers/image-spec/blob/main/layer.md#applying-changesets | ||||
| func applyNaive(ctx context.Context, root string, r io.Reader, options ApplyOptions) (size int64, err error) { | ||||
| 	var ( | ||||
| 		dirs []*tar.Header | ||||
|  | ||||
| 		tr = tar.NewReader(r) | ||||
|  | ||||
| 		// Used for handling opaque directory markers which | ||||
| 		// may occur out of order | ||||
| 		unpackedPaths = make(map[string]struct{}) | ||||
|  | ||||
| 		convertWhiteout = options.ConvertWhiteout | ||||
| 	) | ||||
|  | ||||
| 	if convertWhiteout == nil { | ||||
| 		// handle whiteouts by removing the target files | ||||
| 		convertWhiteout = func(hdr *tar.Header, path string) (bool, error) { | ||||
| 			base := filepath.Base(path) | ||||
| 			dir := filepath.Dir(path) | ||||
| 			if base == whiteoutOpaqueDir { | ||||
| 				_, err := os.Lstat(dir) | ||||
| 				if err != nil { | ||||
| 					return false, err | ||||
| 				} | ||||
| 				err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { | ||||
| 					if err != nil { | ||||
| 						if os.IsNotExist(err) { | ||||
| 							err = nil // parent was deleted | ||||
| 						} | ||||
| 						return err | ||||
| 					} | ||||
| 					if path == dir { | ||||
| 						return nil | ||||
| 					} | ||||
| 					if _, exists := unpackedPaths[path]; !exists { | ||||
| 						err := os.RemoveAll(path) | ||||
| 						return err | ||||
| 					} | ||||
| 					return nil | ||||
| 				}) | ||||
| 				return false, err | ||||
| 			} | ||||
|  | ||||
| 			if strings.HasPrefix(base, whiteoutPrefix) { | ||||
| 				originalBase := base[len(whiteoutPrefix):] | ||||
| 				originalPath := filepath.Join(dir, originalBase) | ||||
|  | ||||
| 				return false, os.RemoveAll(originalPath) | ||||
| 			} | ||||
|  | ||||
| 			return true, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Iterate through the files in the archive. | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			return 0, ctx.Err() | ||||
| 		default: | ||||
| 		} | ||||
|  | ||||
| 		hdr, err := tr.Next() | ||||
| 		if err == io.EOF { | ||||
| 			// end of tar archive | ||||
| 			break | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
|  | ||||
| 		size += hdr.Size | ||||
|  | ||||
| 		// Normalize name, for safety and for a simple is-root check | ||||
| 		hdr.Name = filepath.Clean(hdr.Name) | ||||
|  | ||||
| 		accept, err := options.Filter(hdr) | ||||
| 		if err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
| 		if !accept { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if skipFile(hdr) { | ||||
| 			log.G(ctx).Warnf("file %q ignored: archive may not be supported on system", hdr.Name) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// Split name and resolve symlinks for root directory. | ||||
| 		ppath, base := filepath.Split(hdr.Name) | ||||
| 		ppath, err = fs.RootPath(root, ppath) | ||||
| 		if err != nil { | ||||
| 			return 0, fmt.Errorf("failed to get root path: %w", err) | ||||
| 		} | ||||
|  | ||||
| 		// Join to root before joining to parent path to ensure relative links are | ||||
| 		// already resolved based on the root before adding to parent. | ||||
| 		path := filepath.Join(ppath, filepath.Join("/", base)) | ||||
| 		if path == root { | ||||
| 			log.G(ctx).Debugf("file %q ignored: resolved to root", hdr.Name) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// If file is not directly under root, ensure parent directory | ||||
| 		// exists or is created. | ||||
| 		if ppath != root { | ||||
| 			parentPath := ppath | ||||
| 			if base == "" { | ||||
| 				parentPath = filepath.Dir(path) | ||||
| 			} | ||||
| 			if err := mkparent(ctx, parentPath, root, options.Parents); err != nil { | ||||
| 				return 0, err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Naive whiteout convert function which handles whiteout files by | ||||
| 		// removing the target files. | ||||
| 		if err := validateWhiteout(path); err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
| 		writeFile, err := convertWhiteout(hdr, path) | ||||
| 		if err != nil { | ||||
| 			return 0, fmt.Errorf("failed to convert whiteout file %q: %w", hdr.Name, err) | ||||
| 		} | ||||
| 		if !writeFile { | ||||
| 			continue | ||||
| 		} | ||||
| 		// If path exits we almost always just want to remove and replace it. | ||||
| 		// The only exception is when it is a directory *and* the file from | ||||
| 		// the layer is also a directory. Then we want to merge them (i.e. | ||||
| 		// just apply the metadata from the layer). | ||||
| 		if fi, err := os.Lstat(path); err == nil { | ||||
| 			if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) { | ||||
| 				if err := os.RemoveAll(path); err != nil { | ||||
| 					return 0, err | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		srcData := io.Reader(tr) | ||||
| 		srcHdr := hdr | ||||
|  | ||||
| 		if err := createTarFile(ctx, path, root, srcHdr, srcData, options.NoSameOwner); err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
|  | ||||
| 		// Directory mtimes must be handled at the end to avoid further | ||||
| 		// file creation in them to modify the directory mtime | ||||
| 		if hdr.Typeflag == tar.TypeDir { | ||||
| 			dirs = append(dirs, hdr) | ||||
| 		} | ||||
| 		unpackedPaths[path] = struct{}{} | ||||
| 	} | ||||
|  | ||||
| 	for _, hdr := range dirs { | ||||
| 		path, err := fs.RootPath(root, hdr.Name) | ||||
| 		if err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
| 		if err := chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)); err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return size, nil | ||||
| } | ||||
|  | ||||
| func createTarFile(ctx context.Context, path, extractDir string, hdr *tar.Header, reader io.Reader, noSameOwner bool) error { | ||||
| 	// hdr.Mode is in linux format, which we can use for syscalls, | ||||
| 	// but for os.Foo() calls we need the mode converted to os.FileMode, | ||||
| 	// so use hdrInfo.Mode() (they differ for e.g. setuid bits) | ||||
| 	hdrInfo := hdr.FileInfo() | ||||
|  | ||||
| 	switch hdr.Typeflag { | ||||
| 	case tar.TypeDir: | ||||
| 		// Create directory unless it exists as a directory already. | ||||
| 		// In that case we just want to merge the two | ||||
| 		if fi, err := os.Lstat(path); !(err == nil && fi.IsDir()) { | ||||
| 			if err := mkdir(path, hdrInfo.Mode()); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	//nolint:staticcheck // TypeRegA is deprecated but we may still receive an external tar with TypeRegA | ||||
| 	case tar.TypeReg, tar.TypeRegA: | ||||
| 		file, err := openFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, hdrInfo.Mode()) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		_, err = copyBuffered(ctx, file, reader) | ||||
| 		if err1 := file.Close(); err == nil { | ||||
| 			err = err1 | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 	case tar.TypeBlock, tar.TypeChar: | ||||
| 		// Handle this is an OS-specific way | ||||
| 		if err := handleTarTypeBlockCharFifo(hdr, path); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 	case tar.TypeFifo: | ||||
| 		// Handle this is an OS-specific way | ||||
| 		if err := handleTarTypeBlockCharFifo(hdr, path); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 	case tar.TypeLink: | ||||
| 		targetPath, err := hardlinkRootPath(extractDir, hdr.Linkname) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if err := link(targetPath, path); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 	case tar.TypeSymlink: | ||||
| 		if err := os.Symlink(hdr.Linkname, path); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 	case tar.TypeXGlobalHeader: | ||||
| 		log.G(ctx).Debug("PAX Global Extended Headers found and ignored") | ||||
| 		return nil | ||||
|  | ||||
| 	default: | ||||
| 		return fmt.Errorf("unhandled tar header type %d", hdr.Typeflag) | ||||
| 	} | ||||
|  | ||||
| 	if !noSameOwner { | ||||
| 		if err := os.Lchown(path, hdr.Uid, hdr.Gid); err != nil { | ||||
| 			err = fmt.Errorf("failed to Lchown %q for UID %d, GID %d: %w", path, hdr.Uid, hdr.Gid, err) | ||||
| 			if errors.Is(err, syscall.EINVAL) && userns.RunningInUserNS() { | ||||
| 				err = fmt.Errorf("%w (Hint: try increasing the number of subordinate IDs in /etc/subuid and /etc/subgid)", err) | ||||
| 			} | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for key, value := range hdr.PAXRecords { | ||||
| 		if strings.HasPrefix(key, paxSchilyXattr) { | ||||
| 			key = key[len(paxSchilyXattr):] | ||||
| 			if err := setxattr(path, key, value); err != nil { | ||||
| 				if errors.Is(err, syscall.EPERM) && strings.HasPrefix(key, userXattrPrefix) { | ||||
| 					// In the user.* namespace, only regular files and directories can have extended attributes. | ||||
| 					// See https://man7.org/linux/man-pages/man7/xattr.7.html for details. | ||||
| 					if fi, err := os.Lstat(path); err == nil && (!fi.Mode().IsRegular() && !fi.Mode().IsDir()) { | ||||
| 						log.G(ctx).WithError(err).Warnf("ignored xattr %s in archive", key) | ||||
| 						continue | ||||
| 					} | ||||
| 				} | ||||
| 				if errors.Is(err, syscall.ENOTSUP) { | ||||
| 					log.G(ctx).WithError(err).Warnf("ignored xattr %s in archive", key) | ||||
| 					continue | ||||
| 				} | ||||
| 				return fmt.Errorf("failed to setxattr %q for key %q: %w", path, key, err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// call lchmod after lchown since lchown can modify the file mode | ||||
| 	if err := lchmod(path, hdrInfo.Mode()); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)) | ||||
| } | ||||
|  | ||||
| func mkparent(ctx context.Context, path, root string, parents []string) error { | ||||
| 	if dir, err := os.Lstat(path); err == nil { | ||||
| 		if dir.IsDir() { | ||||
| 			return nil | ||||
| 		} | ||||
| 		return &os.PathError{ | ||||
| 			Op:   "mkparent", | ||||
| 			Path: path, | ||||
| 			Err:  syscall.ENOTDIR, | ||||
| 		} | ||||
| 	} else if !os.IsNotExist(err) { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	i := len(path) | ||||
| 	for i > len(root) && !os.IsPathSeparator(path[i-1]) { | ||||
| 		i-- | ||||
| 	} | ||||
|  | ||||
| 	if i > len(root)+1 { | ||||
| 		if err := mkparent(ctx, path[:i-1], root, parents); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := mkdir(path, 0755); err != nil { | ||||
| 		// Check that still doesn't exist | ||||
| 		dir, err1 := os.Lstat(path) | ||||
| 		if err1 == nil && dir.IsDir() { | ||||
| 			return nil | ||||
| 		} | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, p := range parents { | ||||
| 		ppath, err := fs.RootPath(p, path[len(root):]) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		dir, err := os.Lstat(ppath) | ||||
| 		if err == nil { | ||||
| 			if !dir.IsDir() { | ||||
| 				// Replaced, do not copy attributes | ||||
| 				break | ||||
| 			} | ||||
| 			if err := copyDirInfo(dir, path); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			return copyUpXAttrs(path, ppath) | ||||
| 		} else if !os.IsNotExist(err) { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	log.G(ctx).Debugf("parent directory %q not found: default permissions(0755) used", path) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // ChangeWriter provides tar stream from filesystem change information. | ||||
| // The privided tar stream is styled as an OCI layer. Change information | ||||
| // (add/modify/delete/unmodified) for each file needs to be passed to this | ||||
| // writer through HandleChange method. | ||||
| // | ||||
| // This should be used combining with continuity's diff computing functionality | ||||
| // (e.g. `fs.Change` of github.com/containerd/continuity/fs). | ||||
| // | ||||
| // See also https://github.com/opencontainers/image-spec/blob/main/layer.md for details | ||||
| // about OCI layers | ||||
| type ChangeWriter struct { | ||||
| 	tw                *tar.Writer | ||||
| 	source            string | ||||
| 	modTimeUpperBound *time.Time | ||||
| 	inodeSrc          map[uint64]string | ||||
| 	inodeRefs         map[uint64][]string | ||||
| 	addedDirs         map[string]struct{} | ||||
| } | ||||
|  | ||||
| // ChangeWriterOpt can be specified in NewChangeWriter. | ||||
| type ChangeWriterOpt func(cw *ChangeWriter) | ||||
|  | ||||
| // WithModTimeUpperBound sets the mod time upper bound. | ||||
| func WithModTimeUpperBound(tm time.Time) ChangeWriterOpt { | ||||
| 	return func(cw *ChangeWriter) { | ||||
| 		cw.modTimeUpperBound = &tm | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // NewChangeWriter returns ChangeWriter that writes tar stream of the source directory | ||||
| // to the privided writer. Change information (add/modify/delete/unmodified) for each | ||||
| // file needs to be passed through HandleChange method. | ||||
| func NewChangeWriter(w io.Writer, source string, opts ...ChangeWriterOpt) *ChangeWriter { | ||||
| 	cw := &ChangeWriter{ | ||||
| 		tw:        tar.NewWriter(w), | ||||
| 		source:    source, | ||||
| 		inodeSrc:  map[uint64]string{}, | ||||
| 		inodeRefs: map[uint64][]string{}, | ||||
| 		addedDirs: map[string]struct{}{}, | ||||
| 	} | ||||
| 	for _, o := range opts { | ||||
| 		o(cw) | ||||
| 	} | ||||
| 	return cw | ||||
| } | ||||
|  | ||||
| // HandleChange receives filesystem change information and reflect that information to | ||||
| // the result tar stream. This function implements `fs.ChangeFunc` of continuity | ||||
| // (github.com/containerd/continuity/fs) and should be used with that package. | ||||
| func (cw *ChangeWriter) HandleChange(k fs.ChangeKind, p string, f os.FileInfo, err error) error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if k == fs.ChangeKindDelete { | ||||
| 		whiteOutDir := filepath.Dir(p) | ||||
| 		whiteOutBase := filepath.Base(p) | ||||
| 		whiteOut := filepath.Join(whiteOutDir, whiteoutPrefix+whiteOutBase) | ||||
| 		// Since containerd v2.0, the whiteout timestamps are set to zero (1970-01-01), | ||||
| 		// not to the source date epoch. | ||||
| 		whiteOutT := time.Unix(0, 0).UTC() | ||||
| 		hdr := &tar.Header{ | ||||
| 			Typeflag:   tar.TypeReg, | ||||
| 			Name:       whiteOut[1:], | ||||
| 			Size:       0, | ||||
| 			ModTime:    whiteOutT, | ||||
| 			AccessTime: whiteOutT, | ||||
| 			ChangeTime: whiteOutT, | ||||
| 		} | ||||
| 		if err := cw.includeParents(hdr); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err := cw.tw.WriteHeader(hdr); err != nil { | ||||
| 			return fmt.Errorf("failed to write whiteout header: %w", err) | ||||
| 		} | ||||
| 	} else { | ||||
| 		var ( | ||||
| 			link   string | ||||
| 			err    error | ||||
| 			source = filepath.Join(cw.source, p) | ||||
| 		) | ||||
|  | ||||
| 		switch { | ||||
| 		case f.Mode()&os.ModeSocket != 0: | ||||
| 			return nil // ignore sockets | ||||
| 		case f.Mode()&os.ModeSymlink != 0: | ||||
| 			if link, err = os.Readlink(source); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Use FileInfoHeaderNoLookups to avoid propagating user names and group names from the host | ||||
| 		hdr, err := tarheader.FileInfoHeaderNoLookups(f, link) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode))) | ||||
|  | ||||
| 		// truncate timestamp for compatibility. without PAX stdlib rounds timestamps instead | ||||
| 		hdr.Format = tar.FormatPAX | ||||
| 		if cw.modTimeUpperBound != nil && hdr.ModTime.After(*cw.modTimeUpperBound) { | ||||
| 			hdr.ModTime = *cw.modTimeUpperBound | ||||
| 		} | ||||
| 		hdr.ModTime = hdr.ModTime.Truncate(time.Second) | ||||
| 		hdr.AccessTime = time.Time{} | ||||
| 		hdr.ChangeTime = time.Time{} | ||||
|  | ||||
| 		name := p | ||||
| 		if strings.HasPrefix(name, string(filepath.Separator)) { | ||||
| 			name, err = filepath.Rel(string(filepath.Separator), name) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("failed to make path relative: %w", err) | ||||
| 			} | ||||
| 		} | ||||
| 		// Canonicalize to POSIX-style paths using forward slashes. Directory | ||||
| 		// entries must end with a slash. | ||||
| 		name = filepath.ToSlash(name) | ||||
| 		if f.IsDir() && !strings.HasSuffix(name, "/") { | ||||
| 			name += "/" | ||||
| 		} | ||||
| 		hdr.Name = name | ||||
|  | ||||
| 		if err := setHeaderForSpecialDevice(hdr, name, f); err != nil { | ||||
| 			return fmt.Errorf("failed to set device headers: %w", err) | ||||
| 		} | ||||
|  | ||||
| 		// additionalLinks stores file names which must be linked to | ||||
| 		// this file when this file is added | ||||
| 		var additionalLinks []string | ||||
| 		inode, isHardlink := fs.GetLinkInfo(f) | ||||
| 		if isHardlink { | ||||
| 			// If the inode has a source, always link to it | ||||
| 			if source, ok := cw.inodeSrc[inode]; ok { | ||||
| 				hdr.Typeflag = tar.TypeLink | ||||
| 				hdr.Linkname = source | ||||
| 				hdr.Size = 0 | ||||
| 			} else { | ||||
| 				if k == fs.ChangeKindUnmodified { | ||||
| 					cw.inodeRefs[inode] = append(cw.inodeRefs[inode], name) | ||||
| 					return nil | ||||
| 				} | ||||
| 				cw.inodeSrc[inode] = name | ||||
| 				additionalLinks = cw.inodeRefs[inode] | ||||
| 				delete(cw.inodeRefs, inode) | ||||
| 			} | ||||
| 		} else if k == fs.ChangeKindUnmodified { | ||||
| 			// Nothing to write to diff | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		if capability, err := getxattr(source, "security.capability"); err != nil { | ||||
| 			return fmt.Errorf("failed to get capabilities xattr: %w", err) | ||||
| 		} else if len(capability) > 0 { | ||||
| 			if hdr.PAXRecords == nil { | ||||
| 				hdr.PAXRecords = map[string]string{} | ||||
| 			} | ||||
| 			hdr.PAXRecords[paxSchilyXattr+"security.capability"] = string(capability) | ||||
| 		} | ||||
|  | ||||
| 		if err := cw.includeParents(hdr); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err := cw.tw.WriteHeader(hdr); err != nil { | ||||
| 			return fmt.Errorf("failed to write file header: %w", err) | ||||
| 		} | ||||
|  | ||||
| 		if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 { | ||||
| 			file, err := open(source) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("failed to open path: %v: %w", source, err) | ||||
| 			} | ||||
| 			defer file.Close() | ||||
|  | ||||
| 			n, err := copyBuffered(context.TODO(), cw.tw, file) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("failed to copy: %w", err) | ||||
| 			} | ||||
| 			if n != hdr.Size { | ||||
| 				return errors.New("short write copying file") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if additionalLinks != nil { | ||||
| 			source = hdr.Name | ||||
| 			for _, extra := range additionalLinks { | ||||
| 				hdr.Name = extra | ||||
| 				hdr.Typeflag = tar.TypeLink | ||||
| 				hdr.Linkname = source | ||||
| 				hdr.Size = 0 | ||||
|  | ||||
| 				if err := cw.includeParents(hdr); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 				if err := cw.tw.WriteHeader(hdr); err != nil { | ||||
| 					return fmt.Errorf("failed to write file header: %w", err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Close closes this writer. | ||||
| func (cw *ChangeWriter) Close() error { | ||||
| 	if err := cw.tw.Close(); err != nil { | ||||
| 		return fmt.Errorf("failed to close tar writer: %w", err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (cw *ChangeWriter) includeParents(hdr *tar.Header) error { | ||||
| 	if cw.addedDirs == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	name := strings.TrimRight(hdr.Name, "/") | ||||
| 	fname := filepath.Join(cw.source, name) | ||||
| 	parent := filepath.Dir(name) | ||||
| 	pname := filepath.Join(cw.source, parent) | ||||
|  | ||||
| 	// Do not include root directory as parent | ||||
| 	if fname != cw.source && pname != cw.source { | ||||
| 		_, ok := cw.addedDirs[parent] | ||||
| 		if !ok { | ||||
| 			cw.addedDirs[parent] = struct{}{} | ||||
| 			fi, err := os.Stat(pname) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			if err := cw.HandleChange(fs.ChangeKindModify, parent, fi, nil); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if hdr.Typeflag == tar.TypeDir { | ||||
| 		cw.addedDirs[name] = struct{}{} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func copyBuffered(ctx context.Context, dst io.Writer, src io.Reader) (written int64, err error) { | ||||
| 	buf := bufPool.Get().(*[]byte) | ||||
| 	defer bufPool.Put(buf) | ||||
|  | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			err = ctx.Err() | ||||
| 			return | ||||
| 		default: | ||||
| 		} | ||||
|  | ||||
| 		nr, er := src.Read(*buf) | ||||
| 		if nr > 0 { | ||||
| 			nw, ew := dst.Write((*buf)[0:nr]) | ||||
| 			if nw > 0 { | ||||
| 				written += int64(nw) | ||||
| 			} | ||||
| 			if ew != nil { | ||||
| 				err = ew | ||||
| 				break | ||||
| 			} | ||||
| 			if nr != nw { | ||||
| 				err = io.ErrShortWrite | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if er != nil { | ||||
| 			if er != io.EOF { | ||||
| 				err = er | ||||
| 			} | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	return written, err | ||||
|  | ||||
| } | ||||
|  | ||||
| // hardlinkRootPath returns target linkname, evaluating and bounding any | ||||
| // symlink to the parent directory. | ||||
| // | ||||
| // NOTE: Allow hardlink to the softlink, not the real one. For example, | ||||
| // | ||||
| //	touch /tmp/zzz | ||||
| //	ln -s /tmp/zzz /tmp/xxx | ||||
| //	ln /tmp/xxx /tmp/yyy | ||||
| // | ||||
| // /tmp/yyy should be softlink which be same of /tmp/xxx, not /tmp/zzz. | ||||
| func hardlinkRootPath(root, linkname string) (string, error) { | ||||
| 	ppath, base := filepath.Split(linkname) | ||||
| 	ppath, err := fs.RootPath(root, ppath) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	targetPath := filepath.Join(ppath, base) | ||||
| 	if !strings.HasPrefix(targetPath, root) { | ||||
| 		targetPath = root | ||||
| 	} | ||||
| 	return targetPath, nil | ||||
| } | ||||
|  | ||||
| func validateWhiteout(path string) error { | ||||
| 	base := filepath.Base(path) | ||||
| 	dir := filepath.Dir(path) | ||||
|  | ||||
| 	if base == whiteoutOpaqueDir { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if strings.HasPrefix(base, whiteoutPrefix) { | ||||
| 		originalBase := base[len(whiteoutPrefix):] | ||||
| 		originalPath := filepath.Join(dir, originalBase) | ||||
|  | ||||
| 		// Ensure originalPath is under dir | ||||
| 		if dir[len(dir)-1] != filepath.Separator { | ||||
| 			dir += string(filepath.Separator) | ||||
| 		} | ||||
| 		if !strings.HasPrefix(originalPath, dir) { | ||||
| 			return fmt.Errorf("invalid whiteout name: %v: %w", base, errInvalidArchive) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Derek McGowan
					Derek McGowan