summaryrefslogtreecommitdiff
path: root/pkg/copy/copy.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/copy/copy.go')
-rw-r--r--pkg/copy/copy.go188
1 files changed, 188 insertions, 0 deletions
diff --git a/pkg/copy/copy.go b/pkg/copy/copy.go
new file mode 100644
index 000000000..0e68eb450
--- /dev/null
+++ b/pkg/copy/copy.go
@@ -0,0 +1,188 @@
+package copy
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ buildahCopiah "github.com/containers/buildah/copier"
+ "github.com/containers/storage/pkg/archive"
+ securejoin "github.com/cyphar/filepath-securejoin"
+ "github.com/pkg/errors"
+)
+
+// ********************************* NOTE *************************************
+//
+// Most security bugs are caused by attackers playing around with symlinks
+// trying to escape from the container onto the host and/or trick into data
+// corruption on the host. Hence, file operations on containers (including
+// *stat) should always be handled by `github.com/containers/buildah/copier`
+// which makes sure to evaluate files in a chroot'ed environment.
+//
+// Please make sure to add verbose comments when changing code to make the
+// lives of future readers easier.
+//
+// ****************************************************************************
+
+// Copy the source item to destination. Use extract to untar the source if
+// it's a tar archive.
+func Copy(source *CopyItem, destination *CopyItem, extract bool) error {
+ // First, do the man-page dance. See podman-cp(1) for details.
+ if err := enforceCopyRules(source, destination); err != nil {
+ return err
+ }
+
+ // Destination is a stream (e.g., stdout or an http body).
+ if destination.info.IsStream {
+ // Source is a stream (e.g., stdin or an http body).
+ if source.info.IsStream {
+ _, err := io.Copy(destination.writer, source.reader)
+ return err
+ }
+ root, glob, err := source.buildahGlobs()
+ if err != nil {
+ return err
+ }
+ return buildahCopiah.Get(root, "", source.getOptions(), []string{glob}, destination.writer)
+ }
+
+ // Destination is either a file or a directory.
+ if source.info.IsStream {
+ return buildahCopiah.Put(destination.root, destination.resolved, source.putOptions(), source.reader)
+ }
+
+ tarOptions := &archive.TarOptions{
+ Compression: archive.Uncompressed,
+ CopyPass: true,
+ }
+
+ root := destination.root
+ dir := destination.resolved
+ if !source.info.IsDir {
+ // When copying a file, make sure to rename the
+ // destination base path.
+ nameMap := make(map[string]string)
+ nameMap[filepath.Base(source.resolved)] = filepath.Base(destination.resolved)
+ tarOptions.RebaseNames = nameMap
+ dir = filepath.Dir(dir)
+ }
+
+ var tarReader io.ReadCloser
+ if extract && archive.IsArchivePath(source.resolved) {
+ if !destination.info.IsDir {
+ return errors.Errorf("cannot extract archive %q to file %q", source.original, destination.original)
+ }
+
+ reader, err := os.Open(source.resolved)
+ if err != nil {
+ return err
+ }
+ defer reader.Close()
+
+ // The stream from stdin may be compressed (e.g., via gzip).
+ decompressedStream, err := archive.DecompressStream(reader)
+ if err != nil {
+ return err
+ }
+
+ defer decompressedStream.Close()
+ tarReader = decompressedStream
+ } else {
+ reader, err := archive.TarWithOptions(source.resolved, tarOptions)
+ if err != nil {
+ return err
+ }
+ defer reader.Close()
+ tarReader = reader
+ }
+
+ return buildahCopiah.Put(root, dir, source.putOptions(), tarReader)
+}
+
+// enforceCopyRules enforces the rules for copying from a source to a
+// destination as mentioned in the podman-cp(1) man page. Please refer to the
+// man page and/or the inline comments for further details. Note that source
+// and destination are passed by reference and the their data may be changed.
+func enforceCopyRules(source, destination *CopyItem) error {
+ if source.statError != nil {
+ return source.statError
+ }
+
+ // We can copy everything to a stream.
+ if destination.info.IsStream {
+ return nil
+ }
+
+ // Source is a *stream*.
+ if source.info.IsStream {
+ if !(destination.info.IsDir || destination.info.IsStream) {
+ return errors.New("destination must be a directory or stream when copying from a stream")
+ }
+ return nil
+ }
+
+ // Source is a *directory*.
+ if source.info.IsDir {
+ if destination.statError != nil {
+ // It's okay if the destination does not exist. We
+ // made sure before that it's parent exists, so it
+ // would be created while copying.
+ if os.IsNotExist(destination.statError) {
+ return nil
+ }
+ // Could be a permission error.
+ return destination.statError
+ }
+
+ // If the destination exists and is not a directory, we have a
+ // problem.
+ if !destination.info.IsDir {
+ return errors.Errorf("cannot copy directory %q to file %q", source.original, destination.original)
+ }
+
+ // If the destination exists and is a directory, we need to
+ // append the source base directory to it. This makes sure
+ // that copying "/foo/bar" "/tmp" will copy to "/tmp/bar" (and
+ // not "/tmp").
+ newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved))
+ if err != nil {
+ return err
+ }
+ destination.resolved = newDestination
+ return nil
+ }
+
+ // Source is a *file*.
+ if destination.statError != nil {
+ // It's okay if the destination does not exist, unless it ends
+ // with "/".
+ if !os.IsNotExist(destination.statError) {
+ return destination.statError
+ } else if strings.HasSuffix(destination.resolved, "/") {
+ // Note: this is practically unreachable code as the
+ // existence of parent directories is enforced early
+ // on. It's left here as an extra security net.
+ return errors.Errorf("destination directory %q must exist (trailing %q)", destination.original, "/")
+ }
+ // Does not exist and does not end with "/".
+ return nil
+ }
+
+ // If the destination is a file, we're good. We will overwrite the
+ // contents while copying.
+ if !destination.info.IsDir {
+ return nil
+ }
+
+ // If the destination exists and is a directory, we need to append the
+ // source base directory to it. This makes sure that copying
+ // "/foo/bar" "/tmp" will copy to "/tmp/bar" (and not "/tmp").
+ newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved))
+ if err != nil {
+ return err
+ }
+
+ destination.resolved = newDestination
+ return nil
+}