summaryrefslogtreecommitdiff
path: root/vendor/github.com/moby
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-05-14 13:18:54 -0600
committermo khan <mo@mokhan.ca>2025-05-14 13:18:54 -0600
commit4b2d609a0efcc1d9b2f1a08f954d067ad1d9cd1e (patch)
tree0afacf9217b0569130da6b97d4197331681bf119 /vendor/github.com/moby
parentab373d1fe698cd3f53258c09bc8515d88a6d0b9e (diff)
test: use playwright to test out an OIDC login
Diffstat (limited to 'vendor/github.com/moby')
-rw-r--r--vendor/github.com/moby/go-archive/.gitattributes2
-rw-r--r--vendor/github.com/moby/go-archive/.gitignore1
-rw-r--r--vendor/github.com/moby/go-archive/.golangci.yml33
-rw-r--r--vendor/github.com/moby/go-archive/LICENSE202
-rw-r--r--vendor/github.com/moby/go-archive/archive.go1169
-rw-r--r--vendor/github.com/moby/go-archive/archive_linux.go107
-rw-r--r--vendor/github.com/moby/go-archive/archive_other.go7
-rw-r--r--vendor/github.com/moby/go-archive/archive_unix.go86
-rw-r--r--vendor/github.com/moby/go-archive/archive_windows.go62
-rw-r--r--vendor/github.com/moby/go-archive/changes.go430
-rw-r--r--vendor/github.com/moby/go-archive/changes_linux.go274
-rw-r--r--vendor/github.com/moby/go-archive/changes_other.go95
-rw-r--r--vendor/github.com/moby/go-archive/changes_unix.go43
-rw-r--r--vendor/github.com/moby/go-archive/changes_windows.go33
-rw-r--r--vendor/github.com/moby/go-archive/compression/compression.go263
-rw-r--r--vendor/github.com/moby/go-archive/compression/compression_detect.go65
-rw-r--r--vendor/github.com/moby/go-archive/copy.go496
-rw-r--r--vendor/github.com/moby/go-archive/copy_unix.go11
-rw-r--r--vendor/github.com/moby/go-archive/copy_windows.go9
-rw-r--r--vendor/github.com/moby/go-archive/dev_freebsd.go9
-rw-r--r--vendor/github.com/moby/go-archive/dev_unix.go9
-rw-r--r--vendor/github.com/moby/go-archive/diff.go261
-rw-r--r--vendor/github.com/moby/go-archive/diff_unix.go21
-rw-r--r--vendor/github.com/moby/go-archive/diff_windows.go6
-rw-r--r--vendor/github.com/moby/go-archive/path.go20
-rw-r--r--vendor/github.com/moby/go-archive/path_unix.go9
-rw-r--r--vendor/github.com/moby/go-archive/path_windows.go22
-rw-r--r--vendor/github.com/moby/go-archive/tarheader/tarheader.go67
-rw-r--r--vendor/github.com/moby/go-archive/tarheader/tarheader_unix.go46
-rw-r--r--vendor/github.com/moby/go-archive/tarheader/tarheader_windows.go12
-rw-r--r--vendor/github.com/moby/go-archive/time.go38
-rw-r--r--vendor/github.com/moby/go-archive/time_nonwindows.go41
-rw-r--r--vendor/github.com/moby/go-archive/time_windows.go32
-rw-r--r--vendor/github.com/moby/go-archive/whiteouts.go23
-rw-r--r--vendor/github.com/moby/go-archive/wrap.go59
-rw-r--r--vendor/github.com/moby/go-archive/xattr_supported.go52
-rw-r--r--vendor/github.com/moby/go-archive/xattr_supported_linux.go5
-rw-r--r--vendor/github.com/moby/go-archive/xattr_supported_unix.go7
-rw-r--r--vendor/github.com/moby/go-archive/xattr_unsupported.go11
-rw-r--r--vendor/github.com/moby/sys/sequential/sequential_unix.go27
-rw-r--r--vendor/github.com/moby/sys/sequential/sequential_windows.go89
-rw-r--r--vendor/github.com/moby/sys/user/idtools.go141
-rw-r--r--vendor/github.com/moby/sys/user/idtools_unix.go143
-rw-r--r--vendor/github.com/moby/sys/user/idtools_windows.go13
-rw-r--r--vendor/github.com/moby/sys/user/user.go1
-rw-r--r--vendor/github.com/moby/term/term_unix.go2
46 files changed, 4485 insertions, 69 deletions
diff --git a/vendor/github.com/moby/go-archive/.gitattributes b/vendor/github.com/moby/go-archive/.gitattributes
new file mode 100644
index 0000000..4bb139d
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/.gitattributes
@@ -0,0 +1,2 @@
+*.go -text diff=golang
+*.go text eol=lf
diff --git a/vendor/github.com/moby/go-archive/.gitignore b/vendor/github.com/moby/go-archive/.gitignore
new file mode 100644
index 0000000..3f2bc47
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/.gitignore
@@ -0,0 +1 @@
+/coverage.txt
diff --git a/vendor/github.com/moby/go-archive/.golangci.yml b/vendor/github.com/moby/go-archive/.golangci.yml
new file mode 100644
index 0000000..21439e5
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/.golangci.yml
@@ -0,0 +1,33 @@
+version: "2"
+
+issues:
+ # Disable maximum issues count per one linter.
+ max-issues-per-linter: 0
+ # Disable maximum count of issues with the same text.
+ max-same-issues: 0
+
+linters:
+ enable:
+ - errorlint
+ - unconvert
+ - unparam
+ exclusions:
+ generated: disable
+ presets:
+ - comments
+ - std-error-handling
+ settings:
+ staticcheck:
+ # Enable all options, with some exceptions.
+ # For defaults, see https://golangci-lint.run/usage/linters/#staticcheck
+ checks:
+ - all
+ - -QF1008 # Omit embedded fields from selector expression; https://staticcheck.dev/docs/checks/#QF1008
+ - -ST1003 # Poorly chosen identifier; https://staticcheck.dev/docs/checks/#ST1003
+
+formatters:
+ enable:
+ - gofumpt
+ - goimports
+ exclusions:
+ generated: disable
diff --git a/vendor/github.com/moby/go-archive/LICENSE b/vendor/github.com/moby/go-archive/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/vendor/github.com/moby/go-archive/archive.go b/vendor/github.com/moby/go-archive/archive.go
new file mode 100644
index 0000000..7a105ae
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/archive.go
@@ -0,0 +1,1169 @@
+// Package archive provides helper functions for dealing with archive files.
+package archive
+
+import (
+ "archive/tar"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/containerd/log"
+ "github.com/moby/patternmatcher"
+ "github.com/moby/sys/sequential"
+ "github.com/moby/sys/user"
+
+ "github.com/moby/go-archive/compression"
+ "github.com/moby/go-archive/tarheader"
+)
+
+// ImpliedDirectoryMode represents the mode (Unix permissions) applied to directories that are implied by files in a
+// tar, but that do not have their own header entry.
+//
+// The permissions mask is stored in a constant instead of locally to ensure that magic numbers do not
+// proliferate in the codebase. The default value 0755 has been selected based on the default umask of 0022, and
+// a convention of mkdir(1) calling mkdir(2) with permissions of 0777, resulting in a final value of 0755.
+//
+// This value is currently implementation-defined, and not captured in any cross-runtime specification. Thus, it is
+// subject to change in Moby at any time -- image authors who require consistent or known directory permissions
+// should explicitly control them by ensuring that header entries exist for any applicable path.
+const ImpliedDirectoryMode = 0o755
+
+type (
+ // Compression is the state represents if compressed or not.
+ //
+ // Deprecated: use [compression.Compression].
+ Compression = compression.Compression
+ // WhiteoutFormat is the format of whiteouts unpacked
+ WhiteoutFormat int
+
+ ChownOpts struct {
+ UID int
+ GID int
+ }
+
+ // TarOptions wraps the tar options.
+ TarOptions struct {
+ IncludeFiles []string
+ ExcludePatterns []string
+ Compression compression.Compression
+ NoLchown bool
+ IDMap user.IdentityMapping
+ ChownOpts *ChownOpts
+ IncludeSourceDir bool
+ // WhiteoutFormat is the expected on disk format for whiteout files.
+ // This format will be converted to the standard format on pack
+ // and from the standard format on unpack.
+ WhiteoutFormat WhiteoutFormat
+ // When unpacking, specifies whether overwriting a directory with a
+ // non-directory is allowed and vice versa.
+ NoOverwriteDirNonDir bool
+ // For each include when creating an archive, the included name will be
+ // replaced with the matching name from this map.
+ RebaseNames map[string]string
+ InUserNS bool
+ // Allow unpacking to succeed in spite of failures to set extended
+ // attributes on the unpacked files due to the destination filesystem
+ // not supporting them or a lack of permissions. Extended attributes
+ // were probably in the archive for a reason, so set this option at
+ // your own peril.
+ BestEffortXattrs bool
+ }
+)
+
+// Archiver implements the Archiver interface and allows the reuse of most utility functions of
+// this package with a pluggable Untar function. Also, to facilitate the passing of specific id
+// mappings for untar, an Archiver can be created with maps which will then be passed to Untar operations.
+type Archiver struct {
+ Untar func(io.Reader, string, *TarOptions) error
+ IDMapping user.IdentityMapping
+}
+
+// NewDefaultArchiver returns a new Archiver without any IdentityMapping
+func NewDefaultArchiver() *Archiver {
+ return &Archiver{Untar: Untar}
+}
+
+// breakoutError is used to differentiate errors related to breaking out
+// When testing archive breakout in the unit tests, this error is expected
+// in order for the test to pass.
+type breakoutError error
+
+const (
+ Uncompressed = compression.None // Deprecated: use [compression.None].
+ Bzip2 = compression.Bzip2 // Deprecated: use [compression.Bzip2].
+ Gzip = compression.Gzip // Deprecated: use [compression.Gzip].
+ Xz = compression.Xz // Deprecated: use [compression.Xz].
+ Zstd = compression.Zstd // Deprecated: use [compression.Zstd].
+)
+
+const (
+ AUFSWhiteoutFormat WhiteoutFormat = 0 // AUFSWhiteoutFormat is the default format for whiteouts
+ OverlayWhiteoutFormat WhiteoutFormat = 1 // OverlayWhiteoutFormat formats whiteout according to the overlay standard.
+)
+
+// IsArchivePath checks if the (possibly compressed) file at the given path
+// starts with a tar file header.
+func IsArchivePath(path string) bool {
+ file, err := os.Open(path)
+ if err != nil {
+ return false
+ }
+ defer file.Close()
+ rdr, err := compression.DecompressStream(file)
+ if err != nil {
+ return false
+ }
+ defer rdr.Close()
+ r := tar.NewReader(rdr)
+ _, err = r.Next()
+ return err == nil
+}
+
+// DetectCompression detects the compression algorithm of the source.
+//
+// Deprecated: use [compression.Detect].
+func DetectCompression(source []byte) compression.Compression {
+ return compression.Detect(source)
+}
+
+// DecompressStream decompresses the archive and returns a ReaderCloser with the decompressed archive.
+//
+// Deprecated: use [compression.DecompressStream].
+func DecompressStream(archive io.Reader) (io.ReadCloser, error) {
+ return compression.DecompressStream(archive)
+}
+
+// CompressStream compresses the dest with specified compression algorithm.
+//
+// Deprecated: use [compression.CompressStream].
+func CompressStream(dest io.Writer, comp compression.Compression) (io.WriteCloser, error) {
+ return compression.CompressStream(dest, comp)
+}
+
+// TarModifierFunc is a function that can be passed to ReplaceFileTarWrapper to
+// modify the contents or header of an entry in the archive. If the file already
+// exists in the archive the TarModifierFunc will be called with the Header and
+// a reader which will return the files content. If the file does not exist both
+// header and content will be nil.
+type TarModifierFunc func(path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error)
+
+// ReplaceFileTarWrapper converts inputTarStream to a new tar stream. Files in the
+// tar stream are modified if they match any of the keys in mods.
+func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModifierFunc) io.ReadCloser {
+ pipeReader, pipeWriter := io.Pipe()
+
+ go func() {
+ tarReader := tar.NewReader(inputTarStream)
+ tarWriter := tar.NewWriter(pipeWriter)
+ defer inputTarStream.Close()
+ defer tarWriter.Close()
+
+ modify := func(name string, original *tar.Header, modifier TarModifierFunc, tarReader io.Reader) error {
+ header, data, err := modifier(name, original, tarReader)
+ switch {
+ case err != nil:
+ return err
+ case header == nil:
+ return nil
+ }
+
+ if header.Name == "" {
+ header.Name = name
+ }
+ header.Size = int64(len(data))
+ if err := tarWriter.WriteHeader(header); err != nil {
+ return err
+ }
+ if len(data) != 0 {
+ if _, err := tarWriter.Write(data); err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+
+ var err error
+ var originalHeader *tar.Header
+ for {
+ originalHeader, err = tarReader.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ pipeWriter.CloseWithError(err)
+ return
+ }
+
+ modifier, ok := mods[originalHeader.Name]
+ if !ok {
+ // No modifiers for this file, copy the header and data
+ if err := tarWriter.WriteHeader(originalHeader); err != nil {
+ pipeWriter.CloseWithError(err)
+ return
+ }
+ if err := copyWithBuffer(tarWriter, tarReader); err != nil {
+ pipeWriter.CloseWithError(err)
+ return
+ }
+ continue
+ }
+ delete(mods, originalHeader.Name)
+
+ if err := modify(originalHeader.Name, originalHeader, modifier, tarReader); err != nil {
+ pipeWriter.CloseWithError(err)
+ return
+ }
+ }
+
+ // Apply the modifiers that haven't matched any files in the archive
+ for name, modifier := range mods {
+ if err := modify(name, nil, modifier, nil); err != nil {
+ pipeWriter.CloseWithError(err)
+ return
+ }
+ }
+
+ pipeWriter.Close()
+ }()
+ return pipeReader
+}
+
+// FileInfoHeaderNoLookups creates a partially-populated tar.Header from fi.
+//
+// Deprecated: use [tarheader.FileInfoHeaderNoLookups].
+func FileInfoHeaderNoLookups(fi os.FileInfo, link string) (*tar.Header, error) {
+ return tarheader.FileInfoHeaderNoLookups(fi, link)
+}
+
+// FileInfoHeader creates a populated Header from fi.
+//
+// Compared to the archive/tar package, this function fills in less information
+// but is safe to call from a chrooted process. The AccessTime and ChangeTime
+// fields are not set in the returned header, ModTime is truncated to one-second
+// precision, and the Uname and Gname fields are only set when fi is a FileInfo
+// value returned from tar.Header.FileInfo().
+func FileInfoHeader(name string, fi os.FileInfo, link string) (*tar.Header, error) {
+ hdr, err := tarheader.FileInfoHeaderNoLookups(fi, link)
+ if err != nil {
+ return nil, err
+ }
+ hdr.Format = tar.FormatPAX
+ hdr.ModTime = hdr.ModTime.Truncate(time.Second)
+ hdr.AccessTime = time.Time{}
+ hdr.ChangeTime = time.Time{}
+ hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode)))
+ hdr.Name = canonicalTarName(name, fi.IsDir())
+ return hdr, nil
+}
+
+const paxSchilyXattr = "SCHILY.xattr."
+
+// ReadSecurityXattrToTarHeader reads security.capability xattr from filesystem
+// to a tar header
+func ReadSecurityXattrToTarHeader(path string, hdr *tar.Header) error {
+ const (
+ // Values based on linux/include/uapi/linux/capability.h
+ xattrCapsSz2 = 20
+ versionOffset = 3
+ vfsCapRevision2 = 2
+ vfsCapRevision3 = 3
+ )
+ capability, _ := lgetxattr(path, "security.capability")
+ if capability != nil {
+ if capability[versionOffset] == vfsCapRevision3 {
+ // Convert VFS_CAP_REVISION_3 to VFS_CAP_REVISION_2 as root UID makes no
+ // sense outside the user namespace the archive is built in.
+ capability[versionOffset] = vfsCapRevision2
+ capability = capability[:xattrCapsSz2]
+ }
+ if hdr.PAXRecords == nil {
+ hdr.PAXRecords = make(map[string]string)
+ }
+ hdr.PAXRecords[paxSchilyXattr+"security.capability"] = string(capability)
+ }
+ return nil
+}
+
+type tarWhiteoutConverter interface {
+ ConvertWrite(*tar.Header, string, os.FileInfo) (*tar.Header, error)
+ ConvertRead(*tar.Header, string) (bool, error)
+}
+
+type tarAppender struct {
+ TarWriter *tar.Writer
+
+ // for hardlink mapping
+ SeenFiles map[uint64]string
+ IdentityMapping user.IdentityMapping
+ ChownOpts *ChownOpts
+
+ // For packing and unpacking whiteout files in the
+ // non standard format. The whiteout files defined
+ // by the AUFS standard are used as the tar whiteout
+ // standard.
+ WhiteoutConverter tarWhiteoutConverter
+}
+
+func newTarAppender(idMapping user.IdentityMapping, writer io.Writer, chownOpts *ChownOpts) *tarAppender {
+ return &tarAppender{
+ SeenFiles: make(map[uint64]string),
+ TarWriter: tar.NewWriter(writer),
+ IdentityMapping: idMapping,
+ ChownOpts: chownOpts,
+ }
+}
+
+// canonicalTarName provides a platform-independent and consistent POSIX-style
+// path for files and directories to be archived regardless of the platform.
+func canonicalTarName(name string, isDir bool) string {
+ name = filepath.ToSlash(name)
+
+ // suffix with '/' for directories
+ if isDir && !strings.HasSuffix(name, "/") {
+ name += "/"
+ }
+ return name
+}
+
+// addTarFile adds to the tar archive a file from `path` as `name`
+func (ta *tarAppender) addTarFile(path, name string) error {
+ fi, err := os.Lstat(path)
+ if err != nil {
+ return err
+ }
+
+ var link string
+ if fi.Mode()&os.ModeSymlink != 0 {
+ var err error
+ link, err = os.Readlink(path)
+ if err != nil {
+ return err
+ }
+ }
+
+ hdr, err := FileInfoHeader(name, fi, link)
+ if err != nil {
+ return err
+ }
+ if err := ReadSecurityXattrToTarHeader(path, hdr); err != nil {
+ return err
+ }
+
+ // if it's not a directory and has more than 1 link,
+ // it's hard linked, so set the type flag accordingly
+ if !fi.IsDir() && hasHardlinks(fi) {
+ inode, err := getInodeFromStat(fi.Sys())
+ if err != nil {
+ return err
+ }
+ // a link should have a name that it links too
+ // and that linked name should be first in the tar archive
+ if oldpath, ok := ta.SeenFiles[inode]; ok {
+ hdr.Typeflag = tar.TypeLink
+ hdr.Linkname = oldpath
+ hdr.Size = 0 // This Must be here for the writer math to add up!
+ } else {
+ ta.SeenFiles[inode] = name
+ }
+ }
+
+ // check whether the file is overlayfs whiteout
+ // if yes, skip re-mapping container ID mappings.
+ isOverlayWhiteout := fi.Mode()&os.ModeCharDevice != 0 && hdr.Devmajor == 0 && hdr.Devminor == 0
+
+ // handle re-mapping container ID mappings back to host ID mappings before
+ // writing tar headers/files. We skip whiteout files because they were written
+ // by the kernel and already have proper ownership relative to the host
+ if !isOverlayWhiteout && !strings.HasPrefix(filepath.Base(hdr.Name), WhiteoutPrefix) && !ta.IdentityMapping.Empty() {
+ uid, gid, err := getFileUIDGID(fi.Sys())
+ if err != nil {
+ return err
+ }
+ hdr.Uid, hdr.Gid, err = ta.IdentityMapping.ToContainer(uid, gid)
+ if err != nil {
+ return err
+ }
+ }
+
+ // explicitly override with ChownOpts
+ if ta.ChownOpts != nil {
+ hdr.Uid = ta.ChownOpts.UID
+ hdr.Gid = ta.ChownOpts.GID
+ }
+
+ if ta.WhiteoutConverter != nil {
+ wo, err := ta.WhiteoutConverter.ConvertWrite(hdr, path, fi)
+ if err != nil {
+ return err
+ }
+
+ // If a new whiteout file exists, write original hdr, then
+ // replace hdr with wo to be written after. Whiteouts should
+ // always be written after the original. Note the original
+ // hdr may have been updated to be a whiteout with returning
+ // a whiteout header
+ if wo != nil {
+ if err := ta.TarWriter.WriteHeader(hdr); err != nil {
+ return err
+ }
+ if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 {
+ return fmt.Errorf("tar: cannot use whiteout for non-empty file")
+ }
+ hdr = wo
+ }
+ }
+
+ if err := ta.TarWriter.WriteHeader(hdr); err != nil {
+ return err
+ }
+
+ if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 {
+ // We use sequential file access to avoid depleting the standby list on
+ // Windows. On Linux, this equates to a regular os.Open.
+ file, err := sequential.Open(path)
+ if err != nil {
+ return err
+ }
+
+ err = copyWithBuffer(ta.TarWriter, file)
+ file.Close()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, opts *TarOptions) error {
+ var (
+ Lchown = true
+ inUserns, bestEffortXattrs bool
+ chownOpts *ChownOpts
+ )
+
+ // TODO(thaJeztah): make opts a required argument.
+ if opts != nil {
+ Lchown = !opts.NoLchown
+ inUserns = opts.InUserNS // TODO(thaJeztah): consider deprecating opts.InUserNS and detect locally.
+ chownOpts = opts.ChownOpts
+ bestEffortXattrs = opts.BestEffortXattrs
+ }
+
+ // hdr.Mode is in linux format, which we can use for sycalls,
+ // 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 := os.Mkdir(path, hdrInfo.Mode()); err != nil {
+ return err
+ }
+ }
+
+ case tar.TypeReg:
+ // Source is regular file. We use sequential file access to avoid depleting
+ // the standby list on Windows. On Linux, this equates to a regular os.OpenFile.
+ file, err := sequential.OpenFile(path, os.O_CREATE|os.O_WRONLY, hdrInfo.Mode())
+ if err != nil {
+ return err
+ }
+ if err := copyWithBuffer(file, reader); err != nil {
+ _ = file.Close()
+ return err
+ }
+ _ = file.Close()
+
+ case tar.TypeBlock, tar.TypeChar:
+ if inUserns { // cannot create devices in a userns
+ log.G(context.TODO()).WithFields(log.Fields{"path": path, "type": hdr.Typeflag}).Debug("skipping device nodes in a userns")
+ return nil
+ }
+ // 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 {
+ if inUserns && errors.Is(err, syscall.EPERM) {
+ // In most cases, cannot create a fifo if running in user namespace
+ log.G(context.TODO()).WithFields(log.Fields{"error": err, "path": path, "type": hdr.Typeflag}).Debug("creating fifo node in a userns")
+ return nil
+ }
+ return err
+ }
+
+ case tar.TypeLink:
+ // #nosec G305 -- The target path is checked for path traversal.
+ targetPath := filepath.Join(extractDir, hdr.Linkname)
+ // check for hardlink breakout
+ if !strings.HasPrefix(targetPath, extractDir) {
+ return breakoutError(fmt.Errorf("invalid hardlink %q -> %q", targetPath, hdr.Linkname))
+ }
+ if err := os.Link(targetPath, path); err != nil {
+ return err
+ }
+
+ case tar.TypeSymlink:
+ // path -> hdr.Linkname = targetPath
+ // e.g. /extractDir/path/to/symlink -> ../2/file = /extractDir/path/2/file
+ targetPath := filepath.Join(filepath.Dir(path), hdr.Linkname) // #nosec G305 -- The target path is checked for path traversal.
+
+ // the reason we don't need to check symlinks in the path (with FollowSymlinkInScope) is because
+ // that symlink would first have to be created, which would be caught earlier, at this very check:
+ if !strings.HasPrefix(targetPath, extractDir) {
+ return breakoutError(fmt.Errorf("invalid symlink %q -> %q", path, hdr.Linkname))
+ }
+ if err := os.Symlink(hdr.Linkname, path); err != nil {
+ return err
+ }
+
+ case tar.TypeXGlobalHeader:
+ log.G(context.TODO()).Debug("PAX Global Extended Headers found and ignored")
+ return nil
+
+ default:
+ return fmt.Errorf("unhandled tar header type %d", hdr.Typeflag)
+ }
+
+ // Lchown is not supported on Windows.
+ if Lchown && runtime.GOOS != "windows" {
+ if chownOpts == nil {
+ chownOpts = &ChownOpts{UID: hdr.Uid, GID: hdr.Gid}
+ }
+ if err := os.Lchown(path, chownOpts.UID, chownOpts.GID); err != nil {
+ var msg string
+ if inUserns && errors.Is(err, syscall.EINVAL) {
+ msg = " (try increasing the number of subordinate IDs in /etc/subuid and /etc/subgid)"
+ }
+ return fmt.Errorf("failed to Lchown %q for UID %d, GID %d%s: %w", path, hdr.Uid, hdr.Gid, msg, err)
+ }
+ }
+
+ var xattrErrs []string
+ for key, value := range hdr.PAXRecords {
+ xattr, ok := strings.CutPrefix(key, paxSchilyXattr)
+ if !ok {
+ continue
+ }
+ if err := lsetxattr(path, xattr, []byte(value), 0); err != nil {
+ if bestEffortXattrs && errors.Is(err, syscall.ENOTSUP) || errors.Is(err, syscall.EPERM) {
+ // EPERM occurs if modifying xattrs is not allowed. This can
+ // happen when running in userns with restrictions (ChromeOS).
+ xattrErrs = append(xattrErrs, err.Error())
+ continue
+ }
+ return err
+ }
+ }
+
+ if len(xattrErrs) > 0 {
+ log.G(context.TODO()).WithFields(log.Fields{
+ "errors": xattrErrs,
+ }).Warn("ignored xattrs in archive: underlying filesystem doesn't support them")
+ }
+
+ // There is no LChmod, so ignore mode for symlink. Also, this
+ // must happen after chown, as that can modify the file mode
+ if err := handleLChmod(hdr, path, hdrInfo); err != nil {
+ return err
+ }
+
+ aTime := boundTime(latestTime(hdr.AccessTime, hdr.ModTime))
+ mTime := boundTime(hdr.ModTime)
+
+ // chtimes doesn't support a NOFOLLOW flag atm
+ if hdr.Typeflag == tar.TypeLink {
+ if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) {
+ if err := chtimes(path, aTime, mTime); err != nil {
+ return err
+ }
+ }
+ } else if hdr.Typeflag != tar.TypeSymlink {
+ if err := chtimes(path, aTime, mTime); err != nil {
+ return err
+ }
+ } else {
+ if err := lchtimes(path, aTime, mTime); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Tar creates an archive from the directory at `path`, and returns it as a
+// stream of bytes.
+func Tar(path string, comp compression.Compression) (io.ReadCloser, error) {
+ return TarWithOptions(path, &TarOptions{Compression: comp})
+}
+
+// TarWithOptions creates an archive from the directory at `path`, only including files whose relative
+// paths are included in `options.IncludeFiles` (if non-nil) or not in `options.ExcludePatterns`.
+func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) {
+ tb, err := NewTarballer(srcPath, options)
+ if err != nil {
+ return nil, err
+ }
+ go tb.Do()
+ return tb.Reader(), nil
+}
+
+// Tarballer is a lower-level interface to TarWithOptions which gives the caller
+// control over which goroutine the archiving operation executes on.
+type Tarballer struct {
+ srcPath string
+ options *TarOptions
+ pm *patternmatcher.PatternMatcher
+ pipeReader *io.PipeReader
+ pipeWriter *io.PipeWriter
+ compressWriter io.WriteCloser
+ whiteoutConverter tarWhiteoutConverter
+}
+
+// NewTarballer constructs a new tarballer. The arguments are the same as for
+// TarWithOptions.
+func NewTarballer(srcPath string, options *TarOptions) (*Tarballer, error) {
+ pm, err := patternmatcher.New(options.ExcludePatterns)
+ if err != nil {
+ return nil, err
+ }
+
+ pipeReader, pipeWriter := io.Pipe()
+
+ compressWriter, err := compression.CompressStream(pipeWriter, options.Compression)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Tarballer{
+ // Fix the source path to work with long path names. This is a no-op
+ // on platforms other than Windows.
+ srcPath: addLongPathPrefix(srcPath),
+ options: options,
+ pm: pm,
+ pipeReader: pipeReader,
+ pipeWriter: pipeWriter,
+ compressWriter: compressWriter,
+ whiteoutConverter: getWhiteoutConverter(options.WhiteoutFormat),
+ }, nil
+}
+
+// Reader returns the reader for the created archive.
+func (t *Tarballer) Reader() io.ReadCloser {
+ return t.pipeReader
+}
+
+// Do performs the archiving operation in the background. The resulting archive
+// can be read from t.Reader(). Do should only be called once on each Tarballer
+// instance.
+func (t *Tarballer) Do() {
+ ta := newTarAppender(
+ t.options.IDMap,
+ t.compressWriter,
+ t.options.ChownOpts,
+ )
+ ta.WhiteoutConverter = t.whiteoutConverter
+
+ defer func() {
+ // Make sure to check the error on Close.
+ if err := ta.TarWriter.Close(); err != nil {
+ log.G(context.TODO()).Errorf("Can't close tar writer: %s", err)
+ }
+ if err := t.compressWriter.Close(); err != nil {
+ log.G(context.TODO()).Errorf("Can't close compress writer: %s", err)
+ }
+ if err := t.pipeWriter.Close(); err != nil {
+ log.G(context.TODO()).Errorf("Can't close pipe writer: %s", err)
+ }
+ }()
+
+ // In general we log errors here but ignore them because
+ // during e.g. a diff operation the container can continue
+ // mutating the filesystem and we can see transient errors
+ // from this
+
+ stat, err := os.Lstat(t.srcPath)
+ if err != nil {
+ return
+ }
+
+ if !stat.IsDir() {
+ // We can't later join a non-dir with any includes because the
+ // 'walk' will error if "file/." is stat-ed and "file" is not a
+ // directory. So, we must split the source path and use the
+ // basename as the include.
+ if len(t.options.IncludeFiles) > 0 {
+ log.G(context.TODO()).Warn("Tar: Can't archive a file with includes")
+ }
+
+ dir, base := SplitPathDirEntry(t.srcPath)
+ t.srcPath = dir
+ t.options.IncludeFiles = []string{base}
+ }
+
+ if len(t.options.IncludeFiles) == 0 {
+ t.options.IncludeFiles = []string{"."}
+ }
+
+ seen := make(map[string]bool)
+
+ for _, include := range t.options.IncludeFiles {
+ rebaseName := t.options.RebaseNames[include]
+
+ var (
+ parentMatchInfo []patternmatcher.MatchInfo
+ parentDirs []string
+ )
+
+ walkRoot := getWalkRoot(t.srcPath, include)
+ // TODO(thaJeztah): should this error be handled?
+ _ = filepath.WalkDir(walkRoot, func(filePath string, f os.DirEntry, err error) error {
+ if err != nil {
+ log.G(context.TODO()).Errorf("Tar: Can't stat file %s to tar: %s", t.srcPath, err)
+ return nil
+ }
+
+ relFilePath, err := filepath.Rel(t.srcPath, filePath)
+ if err != nil || (!t.options.IncludeSourceDir && relFilePath == "." && f.IsDir()) {
+ // Error getting relative path OR we are looking
+ // at the source directory path. Skip in both situations.
+ return nil
+ }
+
+ if t.options.IncludeSourceDir && include == "." && relFilePath != "." {
+ relFilePath = strings.Join([]string{".", relFilePath}, string(filepath.Separator))
+ }
+
+ skip := false
+
+ // If "include" is an exact match for the current file
+ // then even if there's an "excludePatterns" pattern that
+ // matches it, don't skip it. IOW, assume an explicit 'include'
+ // is asking for that file no matter what - which is true
+ // for some files, like .dockerignore and Dockerfile (sometimes)
+ if include != relFilePath {
+ for len(parentDirs) != 0 {
+ lastParentDir := parentDirs[len(parentDirs)-1]
+ if strings.HasPrefix(relFilePath, lastParentDir+string(os.PathSeparator)) {
+ break
+ }
+ parentDirs = parentDirs[:len(parentDirs)-1]
+ parentMatchInfo = parentMatchInfo[:len(parentMatchInfo)-1]
+ }
+
+ var matchInfo patternmatcher.MatchInfo
+ if len(parentMatchInfo) != 0 {
+ skip, matchInfo, err = t.pm.MatchesUsingParentResults(relFilePath, parentMatchInfo[len(parentMatchInfo)-1])
+ } else {
+ skip, matchInfo, err = t.pm.MatchesUsingParentResults(relFilePath, patternmatcher.MatchInfo{})
+ }
+ if err != nil {
+ log.G(context.TODO()).Errorf("Error matching %s: %v", relFilePath, err)
+ return err
+ }
+
+ if f.IsDir() {
+ parentDirs = append(parentDirs, relFilePath)
+ parentMatchInfo = append(parentMatchInfo, matchInfo)
+ }
+ }
+
+ if skip {
+ // If we want to skip this file and its a directory
+ // then we should first check to see if there's an
+ // excludes pattern (e.g. !dir/file) that starts with this
+ // dir. If so then we can't skip this dir.
+
+ // Its not a dir then so we can just return/skip.
+ if !f.IsDir() {
+ return nil
+ }
+
+ // No exceptions (!...) in patterns so just skip dir
+ if !t.pm.Exclusions() {
+ return filepath.SkipDir
+ }
+
+ dirSlash := relFilePath + string(filepath.Separator)
+
+ for _, pat := range t.pm.Patterns() {
+ if !pat.Exclusion() {
+ continue
+ }
+ if strings.HasPrefix(pat.String()+string(filepath.Separator), dirSlash) {
+ // found a match - so can't skip this dir
+ return nil
+ }
+ }
+
+ // No matching exclusion dir so just skip dir
+ return filepath.SkipDir
+ }
+
+ if seen[relFilePath] {
+ return nil
+ }
+ seen[relFilePath] = true
+
+ // Rename the base resource.
+ if rebaseName != "" {
+ var replacement string
+ if rebaseName != string(filepath.Separator) {
+ // Special case the root directory to replace with an
+ // empty string instead so that we don't end up with
+ // double slashes in the paths.
+ replacement = rebaseName
+ }
+
+ relFilePath = strings.Replace(relFilePath, include, replacement, 1)
+ }
+
+ if err := ta.addTarFile(filePath, relFilePath); err != nil {
+ log.G(context.TODO()).Errorf("Can't add file %s to tar: %s", filePath, err)
+ // if pipe is broken, stop writing tar stream to it
+ if errors.Is(err, io.ErrClosedPipe) {
+ return err
+ }
+ }
+ return nil
+ })
+ }
+}
+
+// Unpack unpacks the decompressedArchive to dest with options.
+func Unpack(decompressedArchive io.Reader, dest string, options *TarOptions) error {
+ tr := tar.NewReader(decompressedArchive)
+
+ var dirs []*tar.Header
+ whiteoutConverter := getWhiteoutConverter(options.WhiteoutFormat)
+
+ // Iterate through the files in the archive.
+loop:
+ for {
+ hdr, err := tr.Next()
+ if errors.Is(err, io.EOF) {
+ // end of tar archive
+ break
+ }
+ if err != nil {
+ return err
+ }
+
+ // ignore XGlobalHeader early to avoid creating parent directories for them
+ if hdr.Typeflag == tar.TypeXGlobalHeader {
+ log.G(context.TODO()).Debugf("PAX Global Extended Headers found for %s and ignored", hdr.Name)
+ continue
+ }
+
+ // Normalize name, for safety and for a simple is-root check
+ // This keeps "../" as-is, but normalizes "/../" to "/". Or Windows:
+ // This keeps "..\" as-is, but normalizes "\..\" to "\".
+ hdr.Name = filepath.Clean(hdr.Name)
+
+ for _, exclude := range options.ExcludePatterns {
+ if strings.HasPrefix(hdr.Name, exclude) {
+ continue loop
+ }
+ }
+
+ // Ensure that the parent directory exists.
+ err = createImpliedDirectories(dest, hdr, options)
+ if err != nil {
+ return err
+ }
+
+ // #nosec G305 -- The joined path is checked for path traversal.
+ path := filepath.Join(dest, hdr.Name)
+ rel, err := filepath.Rel(dest, path)
+ if err != nil {
+ return err
+ }
+ if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
+ return breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest))
+ }
+
+ // 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 options.NoOverwriteDirNonDir && fi.IsDir() && hdr.Typeflag != tar.TypeDir {
+ // If NoOverwriteDirNonDir is true then we cannot replace
+ // an existing directory with a non-directory from the archive.
+ return fmt.Errorf("cannot overwrite directory %q with non-directory %q", path, dest)
+ }
+
+ if options.NoOverwriteDirNonDir && !fi.IsDir() && hdr.Typeflag == tar.TypeDir {
+ // If NoOverwriteDirNonDir is true then we cannot replace
+ // an existing non-directory with a directory from the archive.
+ return fmt.Errorf("cannot overwrite non-directory %q with directory %q", path, dest)
+ }
+
+ if fi.IsDir() && hdr.Name == "." {
+ continue
+ }
+
+ if !fi.IsDir() || hdr.Typeflag != tar.TypeDir {
+ if err := os.RemoveAll(path); err != nil {
+ return err
+ }
+ }
+ }
+
+ if err := remapIDs(options.IDMap, hdr); err != nil {
+ return err
+ }
+
+ if whiteoutConverter != nil {
+ writeFile, err := whiteoutConverter.ConvertRead(hdr, path)
+ if err != nil {
+ return err
+ }
+ if !writeFile {
+ continue
+ }
+ }
+
+ if err := createTarFile(path, dest, hdr, tr, options); err != nil {
+ return 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)
+ }
+ }
+
+ for _, hdr := range dirs {
+ // #nosec G305 -- The header was checked for path traversal before it was appended to the dirs slice.
+ path := filepath.Join(dest, hdr.Name)
+
+ if err := chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// createImpliedDirectories will create all parent directories of the current path with default permissions, if they do
+// not already exist. This is possible as the tar format supports 'implicit' directories, where their existence is
+// defined by the paths of files in the tar, but there are no header entries for the directories themselves, and thus
+// we most both create them and choose metadata like permissions.
+//
+// The caller should have performed filepath.Clean(hdr.Name), so hdr.Name will now be in the filepath format for the OS
+// on which the daemon is running. This precondition is required because this function assumes a OS-specific path
+// separator when checking that a path is not the root.
+func createImpliedDirectories(dest string, hdr *tar.Header, options *TarOptions) error {
+ // Not the root directory, ensure that the parent directory exists
+ if !strings.HasSuffix(hdr.Name, string(os.PathSeparator)) {
+ parent := filepath.Dir(hdr.Name)
+ parentPath := filepath.Join(dest, parent)
+ if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) {
+ // RootPair() is confined inside this loop as most cases will not require a call, so we can spend some
+ // unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche
+ // usage that reduces the portability of an image.
+ uid, gid := options.IDMap.RootPair()
+
+ err = user.MkdirAllAndChown(parentPath, ImpliedDirectoryMode, uid, gid, user.WithOnlyNew)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+// Untar reads a stream of bytes from `archive`, parses it as a tar archive,
+// and unpacks it into the directory at `dest`.
+// The archive may be compressed with one of the following algorithms:
+// identity (uncompressed), gzip, bzip2, xz.
+//
+// FIXME: specify behavior when target path exists vs. doesn't exist.
+func Untar(tarArchive io.Reader, dest string, options *TarOptions) error {
+ return untarHandler(tarArchive, dest, options, true)
+}
+
+// UntarUncompressed reads a stream of bytes from `archive`, parses it as a tar archive,
+// and unpacks it into the directory at `dest`.
+// The archive must be an uncompressed stream.
+func UntarUncompressed(tarArchive io.Reader, dest string, options *TarOptions) error {
+ return untarHandler(tarArchive, dest, options, false)
+}
+
+// Handler for teasing out the automatic decompression
+func untarHandler(tarArchive io.Reader, dest string, options *TarOptions, decompress bool) error {
+ if tarArchive == nil {
+ return errors.New("empty archive")
+ }
+ dest = filepath.Clean(dest)
+ if options == nil {
+ options = &TarOptions{}
+ }
+ if options.ExcludePatterns == nil {
+ options.ExcludePatterns = []string{}
+ }
+
+ r := tarArchive
+ if decompress {
+ decompressedArchive, err := compression.DecompressStream(tarArchive)
+ if err != nil {
+ return err
+ }
+ defer decompressedArchive.Close()
+ r = decompressedArchive
+ }
+
+ return Unpack(r, dest, options)
+}
+
+// TarUntar is a convenience function which calls Tar and Untar, with the output of one piped into the other.
+// If either Tar or Untar fails, TarUntar aborts and returns the error.
+func (archiver *Archiver) TarUntar(src, dst string) error {
+ archive, err := Tar(src, compression.None)
+ if err != nil {
+ return err
+ }
+ defer archive.Close()
+ return archiver.Untar(archive, dst, &TarOptions{
+ IDMap: archiver.IDMapping,
+ })
+}
+
+// UntarPath untar a file from path to a destination, src is the source tar file path.
+func (archiver *Archiver) UntarPath(src, dst string) error {
+ archive, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer archive.Close()
+ return archiver.Untar(archive, dst, &TarOptions{
+ IDMap: archiver.IDMapping,
+ })
+}
+
+// CopyWithTar creates a tar archive of filesystem path `src`, and
+// unpacks it at filesystem path `dst`.
+// The archive is streamed directly with fixed buffering and no
+// intermediary disk IO.
+func (archiver *Archiver) CopyWithTar(src, dst string) error {
+ srcSt, err := os.Stat(src)
+ if err != nil {
+ return err
+ }
+ if !srcSt.IsDir() {
+ return archiver.CopyFileWithTar(src, dst)
+ }
+
+ // if this Archiver is set up with ID mapping we need to create
+ // the new destination directory with the remapped root UID/GID pair
+ // as owner
+ uid, gid := archiver.IDMapping.RootPair()
+ // Create dst, copy src's content into it
+ if err := user.MkdirAllAndChown(dst, 0o755, uid, gid, user.WithOnlyNew); err != nil {
+ return err
+ }
+ return archiver.TarUntar(src, dst)
+}
+
+// CopyFileWithTar emulates the behavior of the 'cp' command-line
+// for a single file. It copies a regular file from path `src` to
+// path `dst`, and preserves all its metadata.
+func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) {
+ srcSt, err := os.Stat(src)
+ if err != nil {
+ return err
+ }
+
+ if srcSt.IsDir() {
+ return errors.New("can't copy a directory")
+ }
+
+ // Clean up the trailing slash. This must be done in an operating
+ // system specific manner.
+ if dst[len(dst)-1] == os.PathSeparator {
+ dst = filepath.Join(dst, filepath.Base(src))
+ }
+ // Create the holding directory if necessary
+ if err := os.MkdirAll(filepath.Dir(dst), 0o700); err != nil {
+ return err
+ }
+
+ r, w := io.Pipe()
+ errC := make(chan error, 1)
+
+ go func() {
+ defer close(errC)
+
+ errC <- func() error {
+ defer w.Close()
+
+ srcF, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer srcF.Close()
+
+ hdr, err := tarheader.FileInfoHeaderNoLookups(srcSt, "")
+ if err != nil {
+ return err
+ }
+ hdr.Format = tar.FormatPAX
+ hdr.ModTime = hdr.ModTime.Truncate(time.Second)
+ hdr.AccessTime = time.Time{}
+ hdr.ChangeTime = time.Time{}
+ hdr.Name = filepath.Base(dst)
+ hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode)))
+
+ if err := remapIDs(archiver.IDMapping, hdr); err != nil {
+ return err
+ }
+
+ tw := tar.NewWriter(w)
+ defer tw.Close()
+ if err := tw.WriteHeader(hdr); err != nil {
+ return err
+ }
+ if err := copyWithBuffer(tw, srcF); err != nil {
+ return err
+ }
+ return nil
+ }()
+ }()
+ defer func() {
+ if er := <-errC; err == nil && er != nil {
+ err = er
+ }
+ }()
+
+ err = archiver.Untar(r, filepath.Dir(dst), nil)
+ if err != nil {
+ r.CloseWithError(err)
+ }
+ return err
+}
+
+// IdentityMapping returns the IdentityMapping of the archiver.
+func (archiver *Archiver) IdentityMapping() user.IdentityMapping {
+ return archiver.IDMapping
+}
+
+func remapIDs(idMapping user.IdentityMapping, hdr *tar.Header) error {
+ uid, gid, err := idMapping.ToHost(hdr.Uid, hdr.Gid)
+ hdr.Uid, hdr.Gid = uid, gid
+ return err
+}
diff --git a/vendor/github.com/moby/go-archive/archive_linux.go b/vendor/github.com/moby/go-archive/archive_linux.go
new file mode 100644
index 0000000..7b6c3e0
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/archive_linux.go
@@ -0,0 +1,107 @@
+package archive
+
+import (
+ "archive/tar"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/moby/sys/userns"
+ "golang.org/x/sys/unix"
+)
+
+func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
+ if format == OverlayWhiteoutFormat {
+ return overlayWhiteoutConverter{}
+ }
+ return nil
+}
+
+type overlayWhiteoutConverter struct{}
+
+func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os.FileInfo) (wo *tar.Header, _ error) {
+ // convert whiteouts to AUFS format
+ if fi.Mode()&os.ModeCharDevice != 0 && hdr.Devmajor == 0 && hdr.Devminor == 0 {
+ // we just rename the file and make it normal
+ dir, filename := filepath.Split(hdr.Name)
+ hdr.Name = filepath.Join(dir, WhiteoutPrefix+filename)
+ hdr.Mode = 0o600
+ hdr.Typeflag = tar.TypeReg
+ hdr.Size = 0
+ }
+
+ if fi.Mode()&os.ModeDir == 0 {
+ // FIXME(thaJeztah): return a sentinel error instead of nil, nil
+ return nil, nil
+ }
+
+ opaqueXattrName := "trusted.overlay.opaque"
+ if userns.RunningInUserNS() {
+ opaqueXattrName = "user.overlay.opaque"
+ }
+
+ // convert opaque dirs to AUFS format by writing an empty file with the prefix
+ opaque, err := lgetxattr(path, opaqueXattrName)
+ if err != nil {
+ return nil, err
+ }
+ if len(opaque) != 1 || opaque[0] != 'y' {
+ // FIXME(thaJeztah): return a sentinel error instead of nil, nil
+ return nil, nil
+ }
+ delete(hdr.PAXRecords, paxSchilyXattr+opaqueXattrName)
+
+ // create a header for the whiteout file
+ // it should inherit some properties from the parent, but be a regular file
+ return &tar.Header{
+ Typeflag: tar.TypeReg,
+ Mode: hdr.Mode & int64(os.ModePerm),
+ Name: filepath.Join(hdr.Name, WhiteoutOpaqueDir), // #nosec G305 -- An archive is being created, not extracted.
+ Size: 0,
+ Uid: hdr.Uid,
+ Uname: hdr.Uname,
+ Gid: hdr.Gid,
+ Gname: hdr.Gname,
+ AccessTime: hdr.AccessTime,
+ ChangeTime: hdr.ChangeTime,
+ }, nil
+}
+
+func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, path string) (bool, error) {
+ base := filepath.Base(path)
+ dir := filepath.Dir(path)
+
+ // if a directory is marked as opaque by the AUFS special file, we need to translate that to overlay
+ if base == WhiteoutOpaqueDir {
+ opaqueXattrName := "trusted.overlay.opaque"
+ if userns.RunningInUserNS() {
+ opaqueXattrName = "user.overlay.opaque"
+ }
+
+ err := unix.Setxattr(dir, opaqueXattrName, []byte{'y'}, 0)
+ if err != nil {
+ return false, fmt.Errorf("setxattr('%s', %s=y): %w", dir, opaqueXattrName, err)
+ }
+ // don't write the file itself
+ return false, err
+ }
+
+ // if a file was deleted and we are using overlay, we need to create a character device
+ if strings.HasPrefix(base, WhiteoutPrefix) {
+ originalBase := base[len(WhiteoutPrefix):]
+ originalPath := filepath.Join(dir, originalBase)
+
+ if err := unix.Mknod(originalPath, unix.S_IFCHR, 0); err != nil {
+ return false, fmt.Errorf("failed to mknod('%s', S_IFCHR, 0): %w", originalPath, err)
+ }
+ if err := os.Chown(originalPath, hdr.Uid, hdr.Gid); err != nil {
+ return false, err
+ }
+
+ // don't write the file itself
+ return false, nil
+ }
+
+ return true, nil
+}
diff --git a/vendor/github.com/moby/go-archive/archive_other.go b/vendor/github.com/moby/go-archive/archive_other.go
new file mode 100644
index 0000000..6495549
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/archive_other.go
@@ -0,0 +1,7 @@
+//go:build !linux
+
+package archive
+
+func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
+ return nil
+}
diff --git a/vendor/github.com/moby/go-archive/archive_unix.go b/vendor/github.com/moby/go-archive/archive_unix.go
new file mode 100644
index 0000000..3a9f5b0
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/archive_unix.go
@@ -0,0 +1,86 @@
+//go:build !windows
+
+package archive
+
+import (
+ "archive/tar"
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+ "syscall"
+
+ "golang.org/x/sys/unix"
+)
+
+// addLongPathPrefix adds the Windows long path prefix to the path provided if
+// it does not already have it. It is a no-op on platforms other than Windows.
+func addLongPathPrefix(srcPath string) string {
+ return srcPath
+}
+
+// getWalkRoot calculates the root path when performing a TarWithOptions.
+// We use a separate function as this is platform specific. On Linux, we
+// can't use filepath.Join(srcPath,include) because this will clean away
+// a trailing "." or "/" which may be important.
+func getWalkRoot(srcPath string, include string) string {
+ return strings.TrimSuffix(srcPath, string(filepath.Separator)) + string(filepath.Separator) + include
+}
+
+// chmodTarEntry is used to adjust the file permissions used in tar header based
+// on the platform the archival is done.
+func chmodTarEntry(perm os.FileMode) os.FileMode {
+ return perm // noop for unix as golang APIs provide perm bits correctly
+}
+
+func getInodeFromStat(stat interface{}) (uint64, error) {
+ s, ok := stat.(*syscall.Stat_t)
+ if !ok {
+ // FIXME(thaJeztah): this should likely return an error; see https://github.com/moby/moby/pull/49493#discussion_r1979152897
+ return 0, nil
+ }
+ return s.Ino, nil
+}
+
+func getFileUIDGID(stat interface{}) (int, int, error) {
+ s, ok := stat.(*syscall.Stat_t)
+
+ if !ok {
+ return 0, 0, errors.New("cannot convert stat value to syscall.Stat_t")
+ }
+ return int(s.Uid), int(s.Gid), nil
+}
+
+// handleTarTypeBlockCharFifo is an OS-specific helper function used by
+// createTarFile to handle the following types of header: Block; Char; Fifo.
+//
+// Creating device nodes is not supported when running in a user namespace,
+// produces a [syscall.EPERM] in most cases.
+func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
+ mode := uint32(hdr.Mode & 0o7777)
+ switch hdr.Typeflag {
+ case tar.TypeBlock:
+ mode |= unix.S_IFBLK
+ case tar.TypeChar:
+ mode |= unix.S_IFCHR
+ case tar.TypeFifo:
+ mode |= unix.S_IFIFO
+ }
+
+ return mknod(path, mode, unix.Mkdev(uint32(hdr.Devmajor), uint32(hdr.Devminor)))
+}
+
+func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
+ if hdr.Typeflag == tar.TypeLink {
+ if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) {
+ if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
+ return err
+ }
+ }
+ } else if hdr.Typeflag != tar.TypeSymlink {
+ if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/vendor/github.com/moby/go-archive/archive_windows.go b/vendor/github.com/moby/go-archive/archive_windows.go
new file mode 100644
index 0000000..0e3e316
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/archive_windows.go
@@ -0,0 +1,62 @@
+package archive
+
+import (
+ "archive/tar"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// longPathPrefix is the longpath prefix for Windows file paths.
+const longPathPrefix = `\\?\`
+
+// addLongPathPrefix adds the Windows long path prefix to the path provided if
+// it does not already have it. It is a no-op on platforms other than Windows.
+//
+// addLongPathPrefix is a copy of [github.com/docker/docker/pkg/longpath.AddPrefix].
+func addLongPathPrefix(srcPath string) string {
+ if strings.HasPrefix(srcPath, longPathPrefix) {
+ return srcPath
+ }
+ if strings.HasPrefix(srcPath, `\\`) {
+ // This is a UNC path, so we need to add 'UNC' to the path as well.
+ return longPathPrefix + `UNC` + srcPath[1:]
+ }
+ return longPathPrefix + srcPath
+}
+
+// getWalkRoot calculates the root path when performing a TarWithOptions.
+// We use a separate function as this is platform specific.
+func getWalkRoot(srcPath string, include string) string {
+ return filepath.Join(srcPath, include)
+}
+
+// chmodTarEntry is used to adjust the file permissions used in tar header based
+// on the platform the archival is done.
+func chmodTarEntry(perm os.FileMode) os.FileMode {
+ // Remove group- and world-writable bits.
+ perm &= 0o755
+
+ // Add the x bit: make everything +x on Windows
+ return perm | 0o111
+}
+
+func getInodeFromStat(stat interface{}) (uint64, error) {
+ // do nothing. no notion of Inode in stat on Windows
+ return 0, nil
+}
+
+// handleTarTypeBlockCharFifo is an OS-specific helper function used by
+// createTarFile to handle the following types of header: Block; Char; Fifo
+func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
+ return nil
+}
+
+func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
+ return nil
+}
+
+func getFileUIDGID(stat interface{}) (int, int, error) {
+ // no notion of file ownership mapping yet on Windows
+ return 0, 0, nil
+}
diff --git a/vendor/github.com/moby/go-archive/changes.go b/vendor/github.com/moby/go-archive/changes.go
new file mode 100644
index 0000000..02a0372
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/changes.go
@@ -0,0 +1,430 @@
+package archive
+
+import (
+ "archive/tar"
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/containerd/log"
+ "github.com/moby/sys/user"
+)
+
+// ChangeType represents the change type.
+type ChangeType int
+
+const (
+ ChangeModify = 0 // ChangeModify represents the modify operation.
+ ChangeAdd = 1 // ChangeAdd represents the add operation.
+ ChangeDelete = 2 // ChangeDelete represents the delete operation.
+)
+
+func (c ChangeType) String() string {
+ switch c {
+ case ChangeModify:
+ return "C"
+ case ChangeAdd:
+ return "A"
+ case ChangeDelete:
+ return "D"
+ }
+ return ""
+}
+
+// Change represents a change, it wraps the change type and path.
+// It describes changes of the files in the path respect to the
+// parent layers. The change could be modify, add, delete.
+// This is used for layer diff.
+type Change struct {
+ Path string
+ Kind ChangeType
+}
+
+func (change *Change) String() string {
+ return fmt.Sprintf("%s %s", change.Kind, change.Path)
+}
+
+// for sort.Sort
+type changesByPath []Change
+
+func (c changesByPath) Less(i, j int) bool { return c[i].Path < c[j].Path }
+func (c changesByPath) Len() int { return len(c) }
+func (c changesByPath) Swap(i, j int) { c[j], c[i] = c[i], c[j] }
+
+// Gnu tar doesn't have sub-second mtime precision. The go tar
+// writer (1.10+) does when using PAX format, but we round times to seconds
+// to ensure archives have the same hashes for backwards compatibility.
+// See https://github.com/moby/moby/pull/35739/commits/fb170206ba12752214630b269a40ac7be6115ed4.
+//
+// Non-sub-second is problematic when we apply changes via tar
+// files. We handle this by comparing for exact times, *or* same
+// second count and either a or b having exactly 0 nanoseconds
+func sameFsTime(a, b time.Time) bool {
+ return a.Equal(b) ||
+ (a.Unix() == b.Unix() &&
+ (a.Nanosecond() == 0 || b.Nanosecond() == 0))
+}
+
+// Changes walks the path rw and determines changes for the files in the path,
+// with respect to the parent layers
+func Changes(layers []string, rw string) ([]Change, error) {
+ return collectChanges(layers, rw, aufsDeletedFile, aufsMetadataSkip)
+}
+
+func aufsMetadataSkip(path string) (skip bool, err error) {
+ skip, err = filepath.Match(string(os.PathSeparator)+WhiteoutMetaPrefix+"*", path)
+ if err != nil {
+ skip = true
+ }
+ return skip, err
+}
+
+func aufsDeletedFile(root, path string, fi os.FileInfo) (string, error) {
+ f := filepath.Base(path)
+
+ // If there is a whiteout, then the file was removed
+ if strings.HasPrefix(f, WhiteoutPrefix) {
+ originalFile := f[len(WhiteoutPrefix):]
+ return filepath.Join(filepath.Dir(path), originalFile), nil
+ }
+
+ return "", nil
+}
+
+type (
+ skipChange func(string) (bool, error)
+ deleteChange func(string, string, os.FileInfo) (string, error)
+)
+
+func collectChanges(layers []string, rw string, dc deleteChange, sc skipChange) ([]Change, error) {
+ var (
+ changes []Change
+ changedDirs = make(map[string]struct{})
+ )
+
+ err := filepath.Walk(rw, func(path string, f os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ // Rebase path
+ path, err = filepath.Rel(rw, path)
+ if err != nil {
+ return err
+ }
+
+ // As this runs on the daemon side, file paths are OS specific.
+ path = filepath.Join(string(os.PathSeparator), path)
+
+ // Skip root
+ if path == string(os.PathSeparator) {
+ return nil
+ }
+
+ if sc != nil {
+ if skip, err := sc(path); skip {
+ return err
+ }
+ }
+
+ change := Change{
+ Path: path,
+ }
+
+ deletedFile, err := dc(rw, path, f)
+ if err != nil {
+ return err
+ }
+
+ // Find out what kind of modification happened
+ if deletedFile != "" {
+ change.Path = deletedFile
+ change.Kind = ChangeDelete
+ } else {
+ // Otherwise, the file was added
+ change.Kind = ChangeAdd
+
+ // ...Unless it already existed in a top layer, in which case, it's a modification
+ for _, layer := range layers {
+ stat, err := os.Stat(filepath.Join(layer, path))
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ if err == nil {
+ // The file existed in the top layer, so that's a modification
+
+ // However, if it's a directory, maybe it wasn't actually modified.
+ // If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar
+ if stat.IsDir() && f.IsDir() {
+ if f.Size() == stat.Size() && f.Mode() == stat.Mode() && sameFsTime(f.ModTime(), stat.ModTime()) {
+ // Both directories are the same, don't record the change
+ return nil
+ }
+ }
+ change.Kind = ChangeModify
+ break
+ }
+ }
+ }
+
+ // If /foo/bar/file.txt is modified, then /foo/bar must be part of the changed files.
+ // This block is here to ensure the change is recorded even if the
+ // modify time, mode and size of the parent directory in the rw and ro layers are all equal.
+ // Check https://github.com/docker/docker/pull/13590 for details.
+ if f.IsDir() {
+ changedDirs[path] = struct{}{}
+ }
+ if change.Kind == ChangeAdd || change.Kind == ChangeDelete {
+ parent := filepath.Dir(path)
+ if _, ok := changedDirs[parent]; !ok && parent != "/" {
+ changes = append(changes, Change{Path: parent, Kind: ChangeModify})
+ changedDirs[parent] = struct{}{}
+ }
+ }
+
+ // Record change
+ changes = append(changes, change)
+ return nil
+ })
+ if err != nil && !os.IsNotExist(err) {
+ return nil, err
+ }
+ return changes, nil
+}
+
+// FileInfo describes the information of a file.
+type FileInfo struct {
+ parent *FileInfo
+ name string
+ stat fs.FileInfo
+ children map[string]*FileInfo
+ capability []byte
+ added bool
+}
+
+// LookUp looks up the file information of a file.
+func (info *FileInfo) LookUp(path string) *FileInfo {
+ // As this runs on the daemon side, file paths are OS specific.
+ parent := info
+ if path == string(os.PathSeparator) {
+ return info
+ }
+
+ pathElements := strings.Split(path, string(os.PathSeparator))
+ for _, elem := range pathElements {
+ if elem != "" {
+ child := parent.children[elem]
+ if child == nil {
+ return nil
+ }
+ parent = child
+ }
+ }
+ return parent
+}
+
+func (info *FileInfo) path() string {
+ if info.parent == nil {
+ // As this runs on the daemon side, file paths are OS specific.
+ return string(os.PathSeparator)
+ }
+ return filepath.Join(info.parent.path(), info.name)
+}
+
+func (info *FileInfo) addChanges(oldInfo *FileInfo, changes *[]Change) {
+ sizeAtEntry := len(*changes)
+
+ if oldInfo == nil {
+ // add
+ change := Change{
+ Path: info.path(),
+ Kind: ChangeAdd,
+ }
+ *changes = append(*changes, change)
+ info.added = true
+ }
+
+ // We make a copy so we can modify it to detect additions
+ // also, we only recurse on the old dir if the new info is a directory
+ // otherwise any previous delete/change is considered recursive
+ oldChildren := make(map[string]*FileInfo)
+ if oldInfo != nil && info.isDir() {
+ for k, v := range oldInfo.children {
+ oldChildren[k] = v
+ }
+ }
+
+ for name, newChild := range info.children {
+ oldChild := oldChildren[name]
+ if oldChild != nil {
+ // change?
+ oldStat := oldChild.stat
+ newStat := newChild.stat
+ // Note: We can't compare inode or ctime or blocksize here, because these change
+ // when copying a file into a container. However, that is not generally a problem
+ // because any content change will change mtime, and any status change should
+ // be visible when actually comparing the stat fields. The only time this
+ // breaks down is if some code intentionally hides a change by setting
+ // back mtime
+ if statDifferent(oldStat, newStat) ||
+ !bytes.Equal(oldChild.capability, newChild.capability) {
+ change := Change{
+ Path: newChild.path(),
+ Kind: ChangeModify,
+ }
+ *changes = append(*changes, change)
+ newChild.added = true
+ }
+
+ // Remove from copy so we can detect deletions
+ delete(oldChildren, name)
+ }
+
+ newChild.addChanges(oldChild, changes)
+ }
+ for _, oldChild := range oldChildren {
+ // delete
+ change := Change{
+ Path: oldChild.path(),
+ Kind: ChangeDelete,
+ }
+ *changes = append(*changes, change)
+ }
+
+ // If there were changes inside this directory, we need to add it, even if the directory
+ // itself wasn't changed. This is needed to properly save and restore filesystem permissions.
+ // As this runs on the daemon side, file paths are OS specific.
+ if len(*changes) > sizeAtEntry && info.isDir() && !info.added && info.path() != string(os.PathSeparator) {
+ change := Change{
+ Path: info.path(),
+ Kind: ChangeModify,
+ }
+ // Let's insert the directory entry before the recently added entries located inside this dir
+ *changes = append(*changes, change) // just to resize the slice, will be overwritten
+ copy((*changes)[sizeAtEntry+1:], (*changes)[sizeAtEntry:])
+ (*changes)[sizeAtEntry] = change
+ }
+}
+
+// Changes add changes to file information.
+func (info *FileInfo) Changes(oldInfo *FileInfo) []Change {
+ var changes []Change
+
+ info.addChanges(oldInfo, &changes)
+
+ return changes
+}
+
+func newRootFileInfo() *FileInfo {
+ // As this runs on the daemon side, file paths are OS specific.
+ root := &FileInfo{
+ name: string(os.PathSeparator),
+ children: make(map[string]*FileInfo),
+ }
+ return root
+}
+
+// ChangesDirs compares two directories and generates an array of Change objects describing the changes.
+// If oldDir is "", then all files in newDir will be Add-Changes.
+func ChangesDirs(newDir, oldDir string) ([]Change, error) {
+ var oldRoot, newRoot *FileInfo
+ if oldDir == "" {
+ emptyDir, err := os.MkdirTemp("", "empty")
+ if err != nil {
+ return nil, err
+ }
+ defer os.Remove(emptyDir)
+ oldDir = emptyDir
+ }
+ oldRoot, newRoot, err := collectFileInfoForChanges(oldDir, newDir)
+ if err != nil {
+ return nil, err
+ }
+
+ return newRoot.Changes(oldRoot), nil
+}
+
+// ChangesSize calculates the size in bytes of the provided changes, based on newDir.
+func ChangesSize(newDir string, changes []Change) int64 {
+ var (
+ size int64
+ sf = make(map[uint64]struct{})
+ )
+ for _, change := range changes {
+ if change.Kind == ChangeModify || change.Kind == ChangeAdd {
+ file := filepath.Join(newDir, change.Path)
+ fileInfo, err := os.Lstat(file)
+ if err != nil {
+ log.G(context.TODO()).Errorf("Can not stat %q: %s", file, err)
+ continue
+ }
+
+ if fileInfo != nil && !fileInfo.IsDir() {
+ if hasHardlinks(fileInfo) {
+ inode := getIno(fileInfo)
+ if _, ok := sf[inode]; !ok {
+ size += fileInfo.Size()
+ sf[inode] = struct{}{}
+ }
+ } else {
+ size += fileInfo.Size()
+ }
+ }
+ }
+ }
+ return size
+}
+
+// ExportChanges produces an Archive from the provided changes, relative to dir.
+func ExportChanges(dir string, changes []Change, idMap user.IdentityMapping) (io.ReadCloser, error) {
+ reader, writer := io.Pipe()
+ go func() {
+ ta := newTarAppender(idMap, writer, nil)
+
+ sort.Sort(changesByPath(changes))
+
+ // In general we log errors here but ignore them because
+ // during e.g. a diff operation the container can continue
+ // mutating the filesystem and we can see transient errors
+ // from this
+ for _, change := range changes {
+ if change.Kind == ChangeDelete {
+ whiteOutDir := filepath.Dir(change.Path)
+ whiteOutBase := filepath.Base(change.Path)
+ whiteOut := filepath.Join(whiteOutDir, WhiteoutPrefix+whiteOutBase)
+ timestamp := time.Now()
+ hdr := &tar.Header{
+ Name: whiteOut[1:],
+ Size: 0,
+ ModTime: timestamp,
+ AccessTime: timestamp,
+ ChangeTime: timestamp,
+ }
+ if err := ta.TarWriter.WriteHeader(hdr); err != nil {
+ log.G(context.TODO()).Debugf("Can't write whiteout header: %s", err)
+ }
+ } else {
+ path := filepath.Join(dir, change.Path)
+ if err := ta.addTarFile(path, change.Path[1:]); err != nil {
+ log.G(context.TODO()).Debugf("Can't add file %s to tar: %s", path, err)
+ }
+ }
+ }
+
+ // Make sure to check the error on Close.
+ if err := ta.TarWriter.Close(); err != nil {
+ log.G(context.TODO()).Debugf("Can't close layer: %s", err)
+ }
+ if err := writer.Close(); err != nil {
+ log.G(context.TODO()).Debugf("failed close Changes writer: %s", err)
+ }
+ }()
+ return reader, nil
+}
diff --git a/vendor/github.com/moby/go-archive/changes_linux.go b/vendor/github.com/moby/go-archive/changes_linux.go
new file mode 100644
index 0000000..8289fe1
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/changes_linux.go
@@ -0,0 +1,274 @@
+package archive
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "syscall"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+)
+
+// walker is used to implement collectFileInfoForChanges on linux. Where this
+// method in general returns the entire contents of two directory trees, we
+// optimize some FS calls out on linux. In particular, we take advantage of the
+// fact that getdents(2) returns the inode of each file in the directory being
+// walked, which, when walking two trees in parallel to generate a list of
+// changes, can be used to prune subtrees without ever having to lstat(2) them
+// directly. Eliminating stat calls in this way can save up to seconds on large
+// images.
+type walker struct {
+ dir1 string
+ dir2 string
+ root1 *FileInfo
+ root2 *FileInfo
+}
+
+// collectFileInfoForChanges returns a complete representation of the trees
+// rooted at dir1 and dir2, with one important exception: any subtree or
+// leaf where the inode and device numbers are an exact match between dir1
+// and dir2 will be pruned from the results. This method is *only* to be used
+// to generating a list of changes between the two directories, as it does not
+// reflect the full contents.
+func collectFileInfoForChanges(dir1, dir2 string) (*FileInfo, *FileInfo, error) {
+ w := &walker{
+ dir1: dir1,
+ dir2: dir2,
+ root1: newRootFileInfo(),
+ root2: newRootFileInfo(),
+ }
+
+ i1, err := os.Lstat(w.dir1)
+ if err != nil {
+ return nil, nil, err
+ }
+ i2, err := os.Lstat(w.dir2)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if err := w.walk("/", i1, i2); err != nil {
+ return nil, nil, err
+ }
+
+ return w.root1, w.root2, nil
+}
+
+// Given a FileInfo, its path info, and a reference to the root of the tree
+// being constructed, register this file with the tree.
+func walkchunk(path string, fi os.FileInfo, dir string, root *FileInfo) error {
+ if fi == nil {
+ return nil
+ }
+ parent := root.LookUp(filepath.Dir(path))
+ if parent == nil {
+ return fmt.Errorf("walkchunk: Unexpectedly no parent for %s", path)
+ }
+ info := &FileInfo{
+ name: filepath.Base(path),
+ children: make(map[string]*FileInfo),
+ parent: parent,
+ }
+ cpath := filepath.Join(dir, path)
+ info.stat = fi
+ info.capability, _ = lgetxattr(cpath, "security.capability") // lgetxattr(2): fs access
+ parent.children[info.name] = info
+ return nil
+}
+
+// Walk a subtree rooted at the same path in both trees being iterated. For
+// example, /docker/overlay/1234/a/b/c/d and /docker/overlay/8888/a/b/c/d
+func (w *walker) walk(path string, i1, i2 os.FileInfo) (err error) {
+ // Register these nodes with the return trees, unless we're still at the
+ // (already-created) roots:
+ if path != "/" {
+ if err := walkchunk(path, i1, w.dir1, w.root1); err != nil {
+ return err
+ }
+ if err := walkchunk(path, i2, w.dir2, w.root2); err != nil {
+ return err
+ }
+ }
+
+ is1Dir := i1 != nil && i1.IsDir()
+ is2Dir := i2 != nil && i2.IsDir()
+
+ sameDevice := false
+ if i1 != nil && i2 != nil {
+ si1 := i1.Sys().(*syscall.Stat_t)
+ si2 := i2.Sys().(*syscall.Stat_t)
+ if si1.Dev == si2.Dev {
+ sameDevice = true
+ }
+ }
+
+ // If these files are both non-existent, or leaves (non-dirs), we are done.
+ if !is1Dir && !is2Dir {
+ return nil
+ }
+
+ // Fetch the names of all the files contained in both directories being walked:
+ var names1, names2 []nameIno
+ if is1Dir {
+ names1, err = readdirnames(filepath.Join(w.dir1, path)) // getdents(2): fs access
+ if err != nil {
+ return err
+ }
+ }
+ if is2Dir {
+ names2, err = readdirnames(filepath.Join(w.dir2, path)) // getdents(2): fs access
+ if err != nil {
+ return err
+ }
+ }
+
+ // We have lists of the files contained in both parallel directories, sorted
+ // in the same order. Walk them in parallel, generating a unique merged list
+ // of all items present in either or both directories.
+ var names []string
+ ix1 := 0
+ ix2 := 0
+
+ for ix1 < len(names1) && ix2 < len(names2) {
+ ni1 := names1[ix1]
+ ni2 := names2[ix2]
+
+ switch strings.Compare(ni1.name, ni2.name) {
+ case -1: // ni1 < ni2 -- advance ni1
+ // we will not encounter ni1 in names2
+ names = append(names, ni1.name)
+ ix1++
+ case 0: // ni1 == ni2
+ if ni1.ino != ni2.ino || !sameDevice {
+ names = append(names, ni1.name)
+ }
+ ix1++
+ ix2++
+ case 1: // ni1 > ni2 -- advance ni2
+ // we will not encounter ni2 in names1
+ names = append(names, ni2.name)
+ ix2++
+ }
+ }
+ for ix1 < len(names1) {
+ names = append(names, names1[ix1].name)
+ ix1++
+ }
+ for ix2 < len(names2) {
+ names = append(names, names2[ix2].name)
+ ix2++
+ }
+
+ // For each of the names present in either or both of the directories being
+ // iterated, stat the name under each root, and recurse the pair of them:
+ for _, name := range names {
+ fname := filepath.Join(path, name)
+ var cInfo1, cInfo2 os.FileInfo
+ if is1Dir {
+ cInfo1, err = os.Lstat(filepath.Join(w.dir1, fname)) // lstat(2): fs access
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ }
+ if is2Dir {
+ cInfo2, err = os.Lstat(filepath.Join(w.dir2, fname)) // lstat(2): fs access
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ }
+ if err = w.walk(fname, cInfo1, cInfo2); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// {name,inode} pairs used to support the early-pruning logic of the walker type
+type nameIno struct {
+ name string
+ ino uint64
+}
+
+type nameInoSlice []nameIno
+
+func (s nameInoSlice) Len() int { return len(s) }
+func (s nameInoSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s nameInoSlice) Less(i, j int) bool { return s[i].name < s[j].name }
+
+// readdirnames is a hacked-apart version of the Go stdlib code, exposing inode
+// numbers further up the stack when reading directory contents. Unlike
+// os.Readdirnames, which returns a list of filenames, this function returns a
+// list of {filename,inode} pairs.
+func readdirnames(dirname string) (names []nameIno, err error) {
+ var (
+ size = 100
+ buf = make([]byte, 4096)
+ nbuf int
+ bufp int
+ nb int
+ )
+
+ f, err := os.Open(dirname)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ names = make([]nameIno, 0, size) // Empty with room to grow.
+ for {
+ // Refill the buffer if necessary
+ if bufp >= nbuf {
+ bufp = 0
+ nbuf, err = unix.ReadDirent(int(f.Fd()), buf) // getdents on linux
+ if nbuf < 0 {
+ nbuf = 0
+ }
+ if err != nil {
+ return nil, os.NewSyscallError("readdirent", err)
+ }
+ if nbuf <= 0 {
+ break // EOF
+ }
+ }
+
+ // Drain the buffer
+ nb, names = parseDirent(buf[bufp:nbuf], names)
+ bufp += nb
+ }
+
+ sl := nameInoSlice(names)
+ sort.Sort(sl)
+ return sl, nil
+}
+
+// parseDirent is a minor modification of unix.ParseDirent (linux version)
+// which returns {name,inode} pairs instead of just names.
+func parseDirent(buf []byte, names []nameIno) (consumed int, newnames []nameIno) {
+ origlen := len(buf)
+ for len(buf) > 0 {
+ dirent := (*unix.Dirent)(unsafe.Pointer(&buf[0])) // #nosec G103 -- Ignore "G103: Use of unsafe calls should be audited"
+ buf = buf[dirent.Reclen:]
+ if dirent.Ino == 0 { // File absent in directory.
+ continue
+ }
+ b := (*[10000]byte)(unsafe.Pointer(&dirent.Name[0])) // #nosec G103 -- Ignore "G103: Use of unsafe calls should be audited"
+ name := string(b[0:clen(b[:])])
+ if name == "." || name == ".." { // Useless names
+ continue
+ }
+ names = append(names, nameIno{name, dirent.Ino})
+ }
+ return origlen - len(buf), names
+}
+
+func clen(n []byte) int {
+ for i := 0; i < len(n); i++ {
+ if n[i] == 0 {
+ return i
+ }
+ }
+ return len(n)
+}
diff --git a/vendor/github.com/moby/go-archive/changes_other.go b/vendor/github.com/moby/go-archive/changes_other.go
new file mode 100644
index 0000000..a8a3a5a
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/changes_other.go
@@ -0,0 +1,95 @@
+//go:build !linux
+
+package archive
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+func collectFileInfoForChanges(oldDir, newDir string) (*FileInfo, *FileInfo, error) {
+ var (
+ oldRoot, newRoot *FileInfo
+ err1, err2 error
+ errs = make(chan error, 2)
+ )
+ go func() {
+ oldRoot, err1 = collectFileInfo(oldDir)
+ errs <- err1
+ }()
+ go func() {
+ newRoot, err2 = collectFileInfo(newDir)
+ errs <- err2
+ }()
+
+ // block until both routines have returned
+ for i := 0; i < 2; i++ {
+ if err := <-errs; err != nil {
+ return nil, nil, err
+ }
+ }
+
+ return oldRoot, newRoot, nil
+}
+
+func collectFileInfo(sourceDir string) (*FileInfo, error) {
+ root := newRootFileInfo()
+
+ err := filepath.WalkDir(sourceDir, func(path string, _ os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+
+ // Rebase path
+ relPath, err := filepath.Rel(sourceDir, path)
+ if err != nil {
+ return err
+ }
+
+ // As this runs on the daemon side, file paths are OS specific.
+ relPath = filepath.Join(string(os.PathSeparator), relPath)
+
+ // See https://github.com/golang/go/issues/9168 - bug in filepath.Join.
+ // Temporary workaround. If the returned path starts with two backslashes,
+ // trim it down to a single backslash. Only relevant on Windows.
+ if runtime.GOOS == "windows" {
+ if strings.HasPrefix(relPath, `\\`) {
+ relPath = relPath[1:]
+ }
+ }
+
+ if relPath == string(os.PathSeparator) {
+ return nil
+ }
+
+ parent := root.LookUp(filepath.Dir(relPath))
+ if parent == nil {
+ return fmt.Errorf("collectFileInfo: Unexpectedly no parent for %s", relPath)
+ }
+
+ s, err := os.Lstat(path)
+ if err != nil {
+ return err
+ }
+
+ info := &FileInfo{
+ name: filepath.Base(relPath),
+ children: make(map[string]*FileInfo),
+ parent: parent,
+ stat: s,
+ }
+
+ info.capability, _ = lgetxattr(path, "security.capability")
+
+ parent.children[info.name] = info
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return root, nil
+}
diff --git a/vendor/github.com/moby/go-archive/changes_unix.go b/vendor/github.com/moby/go-archive/changes_unix.go
new file mode 100644
index 0000000..4dd98bd
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/changes_unix.go
@@ -0,0 +1,43 @@
+//go:build !windows
+
+package archive
+
+import (
+ "io/fs"
+ "os"
+ "syscall"
+)
+
+func statDifferent(oldStat fs.FileInfo, newStat fs.FileInfo) bool {
+ oldSys := oldStat.Sys().(*syscall.Stat_t)
+ newSys := newStat.Sys().(*syscall.Stat_t)
+ // Don't look at size for dirs, its not a good measure of change
+ if oldStat.Mode() != newStat.Mode() ||
+ oldSys.Uid != newSys.Uid ||
+ oldSys.Gid != newSys.Gid ||
+ oldSys.Rdev != newSys.Rdev ||
+ // Don't look at size or modification time for dirs, its not a good
+ // measure of change. See https://github.com/moby/moby/issues/9874
+ // for a description of the issue with modification time, and
+ // https://github.com/moby/moby/pull/11422 for the change.
+ // (Note that in the Windows implementation of this function,
+ // modification time IS taken as a change). See
+ // https://github.com/moby/moby/pull/37982 for more information.
+ (!oldStat.Mode().IsDir() &&
+ (!sameFsTime(oldStat.ModTime(), newStat.ModTime()) || (oldStat.Size() != newStat.Size()))) {
+ return true
+ }
+ return false
+}
+
+func (info *FileInfo) isDir() bool {
+ return info.parent == nil || info.stat.Mode().IsDir()
+}
+
+func getIno(fi os.FileInfo) uint64 {
+ return fi.Sys().(*syscall.Stat_t).Ino
+}
+
+func hasHardlinks(fi os.FileInfo) bool {
+ return fi.Sys().(*syscall.Stat_t).Nlink > 1
+}
diff --git a/vendor/github.com/moby/go-archive/changes_windows.go b/vendor/github.com/moby/go-archive/changes_windows.go
new file mode 100644
index 0000000..c89605c
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/changes_windows.go
@@ -0,0 +1,33 @@
+package archive
+
+import (
+ "io/fs"
+ "os"
+)
+
+func statDifferent(oldStat fs.FileInfo, newStat fs.FileInfo) bool {
+ // Note there is slight difference between the Linux and Windows
+ // implementations here. Due to https://github.com/moby/moby/issues/9874,
+ // and the fix at https://github.com/moby/moby/pull/11422, Linux does not
+ // consider a change to the directory time as a change. Windows on NTFS
+ // does. See https://github.com/moby/moby/pull/37982 for more information.
+
+ if !sameFsTime(oldStat.ModTime(), newStat.ModTime()) ||
+ oldStat.Mode() != newStat.Mode() ||
+ oldStat.Size() != newStat.Size() && !oldStat.Mode().IsDir() {
+ return true
+ }
+ return false
+}
+
+func (info *FileInfo) isDir() bool {
+ return info.parent == nil || info.stat.Mode().IsDir()
+}
+
+func getIno(fi os.FileInfo) (inode uint64) {
+ return
+}
+
+func hasHardlinks(fi os.FileInfo) bool {
+ return false
+}
diff --git a/vendor/github.com/moby/go-archive/compression/compression.go b/vendor/github.com/moby/go-archive/compression/compression.go
new file mode 100644
index 0000000..e298cef
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/compression/compression.go
@@ -0,0 +1,263 @@
+package compression
+
+import (
+ "bufio"
+ "bytes"
+ "compress/bzip2"
+ "compress/gzip"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "strconv"
+ "sync"
+
+ "github.com/containerd/log"
+ "github.com/klauspost/compress/zstd"
+)
+
+// Compression is the state represents if compressed or not.
+type Compression int
+
+const (
+ None Compression = 0 // None represents the uncompressed.
+ Bzip2 Compression = 1 // Bzip2 is bzip2 compression algorithm.
+ Gzip Compression = 2 // Gzip is gzip compression algorithm.
+ Xz Compression = 3 // Xz is xz compression algorithm.
+ Zstd Compression = 4 // Zstd is zstd compression algorithm.
+)
+
+// Extension returns the extension of a file that uses the specified compression algorithm.
+func (c *Compression) Extension() string {
+ switch *c {
+ case None:
+ return "tar"
+ case Bzip2:
+ return "tar.bz2"
+ case Gzip:
+ return "tar.gz"
+ case Xz:
+ return "tar.xz"
+ case Zstd:
+ return "tar.zst"
+ default:
+ return ""
+ }
+}
+
+type readCloserWrapper struct {
+ io.Reader
+ closer func() error
+}
+
+func (r *readCloserWrapper) Close() error {
+ if r.closer != nil {
+ return r.closer()
+ }
+ return nil
+}
+
+type nopWriteCloser struct {
+ io.Writer
+}
+
+func (nopWriteCloser) Close() error { return nil }
+
+var bufioReader32KPool = &sync.Pool{
+ New: func() interface{} { return bufio.NewReaderSize(nil, 32*1024) },
+}
+
+type bufferedReader struct {
+ buf *bufio.Reader
+}
+
+func newBufferedReader(r io.Reader) *bufferedReader {
+ buf := bufioReader32KPool.Get().(*bufio.Reader)
+ buf.Reset(r)
+ return &bufferedReader{buf}
+}
+
+func (r *bufferedReader) Read(p []byte) (int, error) {
+ if r.buf == nil {
+ return 0, io.EOF
+ }
+ n, err := r.buf.Read(p)
+ if errors.Is(err, io.EOF) {
+ r.buf.Reset(nil)
+ bufioReader32KPool.Put(r.buf)
+ r.buf = nil
+ }
+ return n, err
+}
+
+func (r *bufferedReader) Peek(n int) ([]byte, error) {
+ if r.buf == nil {
+ return nil, io.EOF
+ }
+ return r.buf.Peek(n)
+}
+
+// DecompressStream decompresses the archive and returns a ReaderCloser with the decompressed archive.
+func DecompressStream(archive io.Reader) (io.ReadCloser, error) {
+ buf := newBufferedReader(archive)
+ bs, err := buf.Peek(10)
+ if err != nil && !errors.Is(err, io.EOF) {
+ // Note: we'll ignore any io.EOF error because there are some odd
+ // cases where the layer.tar file will be empty (zero bytes) and
+ // that results in an io.EOF from the Peek() call. So, in those
+ // cases we'll just treat it as a non-compressed stream and
+ // that means just create an empty layer.
+ // See Issue 18170
+ return nil, err
+ }
+
+ switch compression := Detect(bs); compression {
+ case None:
+ return &readCloserWrapper{
+ Reader: buf,
+ }, nil
+ case Gzip:
+ ctx, cancel := context.WithCancel(context.Background())
+ gzReader, err := gzipDecompress(ctx, buf)
+ if err != nil {
+ cancel()
+ return nil, err
+ }
+
+ return &readCloserWrapper{
+ Reader: gzReader,
+ closer: func() error {
+ cancel()
+ return gzReader.Close()
+ },
+ }, nil
+ case Bzip2:
+ bz2Reader := bzip2.NewReader(buf)
+ return &readCloserWrapper{
+ Reader: bz2Reader,
+ }, nil
+ case Xz:
+ ctx, cancel := context.WithCancel(context.Background())
+
+ xzReader, err := xzDecompress(ctx, buf)
+ if err != nil {
+ cancel()
+ return nil, err
+ }
+
+ return &readCloserWrapper{
+ Reader: xzReader,
+ closer: func() error {
+ cancel()
+ return xzReader.Close()
+ },
+ }, nil
+ case Zstd:
+ zstdReader, err := zstd.NewReader(buf)
+ if err != nil {
+ return nil, err
+ }
+ return &readCloserWrapper{
+ Reader: zstdReader,
+ closer: func() error {
+ zstdReader.Close()
+ return nil
+ },
+ }, nil
+
+ default:
+ return nil, fmt.Errorf("unsupported compression format (%d)", compression)
+ }
+}
+
+// CompressStream compresses the dest with specified compression algorithm.
+func CompressStream(dest io.Writer, compression Compression) (io.WriteCloser, error) {
+ switch compression {
+ case None:
+ return nopWriteCloser{dest}, nil
+ case Gzip:
+ return gzip.NewWriter(dest), nil
+ case Bzip2:
+ // archive/bzip2 does not support writing.
+ return nil, errors.New("unsupported compression format: tar.bz2")
+ case Xz:
+ // there is no xz support at all
+ // However, this is not a problem as docker only currently generates gzipped tars
+ return nil, errors.New("unsupported compression format: tar.xz")
+ default:
+ return nil, fmt.Errorf("unsupported compression format (%d)", compression)
+ }
+}
+
+func xzDecompress(ctx context.Context, archive io.Reader) (io.ReadCloser, error) {
+ args := []string{"xz", "-d", "-c", "-q"}
+
+ return cmdStream(exec.CommandContext(ctx, args[0], args[1:]...), archive)
+}
+
+func gzipDecompress(ctx context.Context, buf io.Reader) (io.ReadCloser, error) {
+ if noPigzEnv := os.Getenv("MOBY_DISABLE_PIGZ"); noPigzEnv != "" {
+ noPigz, err := strconv.ParseBool(noPigzEnv)
+ if err != nil {
+ log.G(ctx).WithError(err).Warn("invalid value in MOBY_DISABLE_PIGZ env var")
+ }
+ if noPigz {
+ log.G(ctx).Debugf("Use of pigz is disabled due to MOBY_DISABLE_PIGZ=%s", noPigzEnv)
+ return gzip.NewReader(buf)
+ }
+ }
+
+ unpigzPath, err := exec.LookPath("unpigz")
+ if err != nil {
+ log.G(ctx).Debugf("unpigz binary not found, falling back to go gzip library")
+ return gzip.NewReader(buf)
+ }
+
+ log.G(ctx).Debugf("Using %s to decompress", unpigzPath)
+
+ return cmdStream(exec.CommandContext(ctx, unpigzPath, "-d", "-c"), buf)
+}
+
+// cmdStream executes a command, and returns its stdout as a stream.
+// If the command fails to run or doesn't complete successfully, an error
+// will be returned, including anything written on stderr.
+func cmdStream(cmd *exec.Cmd, in io.Reader) (io.ReadCloser, error) {
+ reader, writer := io.Pipe()
+
+ cmd.Stdin = in
+ cmd.Stdout = writer
+
+ var errBuf bytes.Buffer
+ cmd.Stderr = &errBuf
+
+ // Run the command and return the pipe
+ if err := cmd.Start(); err != nil {
+ return nil, err
+ }
+
+ // Ensure the command has exited before we clean anything up
+ done := make(chan struct{})
+
+ // Copy stdout to the returned pipe
+ go func() {
+ if err := cmd.Wait(); err != nil {
+ _ = writer.CloseWithError(fmt.Errorf("%w: %s", err, errBuf.String()))
+ } else {
+ _ = writer.Close()
+ }
+ close(done)
+ }()
+
+ return &readCloserWrapper{
+ Reader: reader,
+ closer: func() error {
+ // Close pipeR, and then wait for the command to complete before returning. We have to close pipeR first, as
+ // cmd.Wait waits for any non-file stdout/stderr/stdin to close.
+ err := reader.Close()
+ <-done
+ return err
+ },
+ }, nil
+}
diff --git a/vendor/github.com/moby/go-archive/compression/compression_detect.go b/vendor/github.com/moby/go-archive/compression/compression_detect.go
new file mode 100644
index 0000000..85eda92
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/compression/compression_detect.go
@@ -0,0 +1,65 @@
+package compression
+
+import (
+ "bytes"
+ "encoding/binary"
+)
+
+const (
+ zstdMagicSkippableStart = 0x184D2A50
+ zstdMagicSkippableMask = 0xFFFFFFF0
+)
+
+var (
+ bzip2Magic = []byte{0x42, 0x5A, 0x68}
+ gzipMagic = []byte{0x1F, 0x8B, 0x08}
+ xzMagic = []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}
+ zstdMagic = []byte{0x28, 0xb5, 0x2f, 0xfd}
+)
+
+type matcher = func([]byte) bool
+
+// Detect detects the compression algorithm of the source.
+func Detect(source []byte) Compression {
+ compressionMap := map[Compression]matcher{
+ Bzip2: magicNumberMatcher(bzip2Magic),
+ Gzip: magicNumberMatcher(gzipMagic),
+ Xz: magicNumberMatcher(xzMagic),
+ Zstd: zstdMatcher(),
+ }
+ for _, compression := range []Compression{Bzip2, Gzip, Xz, Zstd} {
+ fn := compressionMap[compression]
+ if fn(source) {
+ return compression
+ }
+ }
+ return None
+}
+
+func magicNumberMatcher(m []byte) matcher {
+ return func(source []byte) bool {
+ return bytes.HasPrefix(source, m)
+ }
+}
+
+// zstdMatcher detects zstd compression algorithm.
+// Zstandard compressed data is made of one or more frames.
+// There are two frame formats defined by Zstandard: Zstandard frames and Skippable frames.
+// See https://datatracker.ietf.org/doc/html/rfc8878#section-3 for more details.
+func zstdMatcher() matcher {
+ return func(source []byte) bool {
+ if bytes.HasPrefix(source, zstdMagic) {
+ // Zstandard frame
+ return true
+ }
+ // skippable frame
+ if len(source) < 8 {
+ return false
+ }
+ // magic number from 0x184D2A50 to 0x184D2A5F.
+ if binary.LittleEndian.Uint32(source[:4])&zstdMagicSkippableMask == zstdMagicSkippableStart {
+ return true
+ }
+ return false
+ }
+}
diff --git a/vendor/github.com/moby/go-archive/copy.go b/vendor/github.com/moby/go-archive/copy.go
new file mode 100644
index 0000000..77d038c
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/copy.go
@@ -0,0 +1,496 @@
+package archive
+
+import (
+ "archive/tar"
+ "context"
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/containerd/log"
+)
+
+// Errors used or returned by this file.
+var (
+ ErrNotDirectory = errors.New("not a directory")
+ ErrDirNotExists = errors.New("no such directory")
+ ErrCannotCopyDir = errors.New("cannot copy directory")
+ ErrInvalidCopySource = errors.New("invalid copy source content")
+)
+
+var copyPool = sync.Pool{
+ New: func() interface{} { s := make([]byte, 32*1024); return &s },
+}
+
+func copyWithBuffer(dst io.Writer, src io.Reader) error {
+ buf := copyPool.Get().(*[]byte)
+ _, err := io.CopyBuffer(dst, src, *buf)
+ copyPool.Put(buf)
+ return err
+}
+
+// PreserveTrailingDotOrSeparator returns the given cleaned path (after
+// processing using any utility functions from the path or filepath stdlib
+// packages) and appends a trailing `/.` or `/` if its corresponding original
+// path (from before being processed by utility functions from the path or
+// filepath stdlib packages) ends with a trailing `/.` or `/`. If the cleaned
+// path already ends in a `.` path segment, then another is not added. If the
+// clean path already ends in a path separator, then another is not added.
+func PreserveTrailingDotOrSeparator(cleanedPath string, originalPath string) string {
+ // Ensure paths are in platform semantics
+ cleanedPath = normalizePath(cleanedPath)
+ originalPath = normalizePath(originalPath)
+
+ if !specifiesCurrentDir(cleanedPath) && specifiesCurrentDir(originalPath) {
+ if !hasTrailingPathSeparator(cleanedPath) {
+ // Add a separator if it doesn't already end with one (a cleaned
+ // path would only end in a separator if it is the root).
+ cleanedPath += string(filepath.Separator)
+ }
+ cleanedPath += "."
+ }
+
+ if !hasTrailingPathSeparator(cleanedPath) && hasTrailingPathSeparator(originalPath) {
+ cleanedPath += string(filepath.Separator)
+ }
+
+ return cleanedPath
+}
+
+// assertsDirectory returns whether the given path is
+// asserted to be a directory, i.e., the path ends with
+// a trailing '/' or `/.`, assuming a path separator of `/`.
+func assertsDirectory(path string) bool {
+ return hasTrailingPathSeparator(path) || specifiesCurrentDir(path)
+}
+
+// hasTrailingPathSeparator returns whether the given
+// path ends with the system's path separator character.
+func hasTrailingPathSeparator(path string) bool {
+ return len(path) > 0 && path[len(path)-1] == filepath.Separator
+}
+
+// specifiesCurrentDir returns whether the given path specifies
+// a "current directory", i.e., the last path segment is `.`.
+func specifiesCurrentDir(path string) bool {
+ return filepath.Base(path) == "."
+}
+
+// SplitPathDirEntry splits the given path between its directory name and its
+// basename by first cleaning the path but preserves a trailing "." if the
+// original path specified the current directory.
+func SplitPathDirEntry(path string) (dir, base string) {
+ cleanedPath := filepath.Clean(filepath.FromSlash(path))
+
+ if specifiesCurrentDir(path) {
+ cleanedPath += string(os.PathSeparator) + "."
+ }
+
+ return filepath.Dir(cleanedPath), filepath.Base(cleanedPath)
+}
+
+// TarResource archives the resource described by the given CopyInfo to a Tar
+// archive. A non-nil error is returned if sourcePath does not exist or is
+// asserted to be a directory but exists as another type of file.
+//
+// This function acts as a convenient wrapper around TarWithOptions, which
+// requires a directory as the source path. TarResource accepts either a
+// directory or a file path and correctly sets the Tar options.
+func TarResource(sourceInfo CopyInfo) (content io.ReadCloser, err error) {
+ return TarResourceRebase(sourceInfo.Path, sourceInfo.RebaseName)
+}
+
+// TarResourceRebase is like TarResource but renames the first path element of
+// items in the resulting tar archive to match the given rebaseName if not "".
+func TarResourceRebase(sourcePath, rebaseName string) (content io.ReadCloser, _ error) {
+ sourcePath = normalizePath(sourcePath)
+ if _, err := os.Lstat(sourcePath); err != nil {
+ // Catches the case where the source does not exist or is not a
+ // directory if asserted to be a directory, as this also causes an
+ // error.
+ return nil, err
+ }
+
+ // Separate the source path between its directory and
+ // the entry in that directory which we are archiving.
+ sourceDir, sourceBase := SplitPathDirEntry(sourcePath)
+ opts := TarResourceRebaseOpts(sourceBase, rebaseName)
+
+ log.G(context.TODO()).Debugf("copying %q from %q", sourceBase, sourceDir)
+ return TarWithOptions(sourceDir, opts)
+}
+
+// TarResourceRebaseOpts does not preform the Tar, but instead just creates the rebase
+// parameters to be sent to TarWithOptions (the TarOptions struct)
+func TarResourceRebaseOpts(sourceBase string, rebaseName string) *TarOptions {
+ filter := []string{sourceBase}
+ return &TarOptions{
+ IncludeFiles: filter,
+ IncludeSourceDir: true,
+ RebaseNames: map[string]string{
+ sourceBase: rebaseName,
+ },
+ }
+}
+
+// CopyInfo holds basic info about the source
+// or destination path of a copy operation.
+type CopyInfo struct {
+ Path string
+ Exists bool
+ IsDir bool
+ RebaseName string
+}
+
+// CopyInfoSourcePath stats the given path to create a CopyInfo
+// struct representing that resource for the source of an archive copy
+// operation. The given path should be an absolute local path. A source path
+// has all symlinks evaluated that appear before the last path separator ("/"
+// on Unix). As it is to be a copy source, the path must exist.
+func CopyInfoSourcePath(path string, followLink bool) (CopyInfo, error) {
+ // normalize the file path and then evaluate the symbol link
+ // we will use the target file instead of the symbol link if
+ // followLink is set
+ path = normalizePath(path)
+
+ resolvedPath, rebaseName, err := ResolveHostSourcePath(path, followLink)
+ if err != nil {
+ return CopyInfo{}, err
+ }
+
+ stat, err := os.Lstat(resolvedPath)
+ if err != nil {
+ return CopyInfo{}, err
+ }
+
+ return CopyInfo{
+ Path: resolvedPath,
+ Exists: true,
+ IsDir: stat.IsDir(),
+ RebaseName: rebaseName,
+ }, nil
+}
+
+// CopyInfoDestinationPath stats the given path to create a CopyInfo
+// struct representing that resource for the destination of an archive copy
+// operation. The given path should be an absolute local path.
+func CopyInfoDestinationPath(path string) (info CopyInfo, err error) {
+ maxSymlinkIter := 10 // filepath.EvalSymlinks uses 255, but 10 already seems like a lot.
+ path = normalizePath(path)
+ originalPath := path
+
+ stat, err := os.Lstat(path)
+
+ if err == nil && stat.Mode()&os.ModeSymlink == 0 {
+ // The path exists and is not a symlink.
+ return CopyInfo{
+ Path: path,
+ Exists: true,
+ IsDir: stat.IsDir(),
+ }, nil
+ }
+
+ // While the path is a symlink.
+ for n := 0; err == nil && stat.Mode()&os.ModeSymlink != 0; n++ {
+ if n > maxSymlinkIter {
+ // Don't follow symlinks more than this arbitrary number of times.
+ return CopyInfo{}, errors.New("too many symlinks in " + originalPath)
+ }
+
+ // The path is a symbolic link. We need to evaluate it so that the
+ // destination of the copy operation is the link target and not the
+ // link itself. This is notably different than CopyInfoSourcePath which
+ // only evaluates symlinks before the last appearing path separator.
+ // Also note that it is okay if the last path element is a broken
+ // symlink as the copy operation should create the target.
+ var linkTarget string
+
+ linkTarget, err = os.Readlink(path)
+ if err != nil {
+ return CopyInfo{}, err
+ }
+
+ if !filepath.IsAbs(linkTarget) {
+ // Join with the parent directory.
+ dstParent, _ := SplitPathDirEntry(path)
+ linkTarget = filepath.Join(dstParent, linkTarget)
+ }
+
+ path = linkTarget
+ stat, err = os.Lstat(path)
+ }
+
+ if err != nil {
+ // It's okay if the destination path doesn't exist. We can still
+ // continue the copy operation if the parent directory exists.
+ if !os.IsNotExist(err) {
+ return CopyInfo{}, err
+ }
+
+ // Ensure destination parent dir exists.
+ dstParent, _ := SplitPathDirEntry(path)
+
+ parentDirStat, err := os.Stat(dstParent)
+ if err != nil {
+ return CopyInfo{}, err
+ }
+ if !parentDirStat.IsDir() {
+ return CopyInfo{}, ErrNotDirectory
+ }
+
+ return CopyInfo{Path: path}, nil
+ }
+
+ // The path exists after resolving symlinks.
+ return CopyInfo{
+ Path: path,
+ Exists: true,
+ IsDir: stat.IsDir(),
+ }, nil
+}
+
+// PrepareArchiveCopy prepares the given srcContent archive, which should
+// contain the archived resource described by srcInfo, to the destination
+// described by dstInfo. Returns the possibly modified content archive along
+// with the path to the destination directory which it should be extracted to.
+func PrepareArchiveCopy(srcContent io.Reader, srcInfo, dstInfo CopyInfo) (dstDir string, content io.ReadCloser, err error) {
+ // Ensure in platform semantics
+ srcInfo.Path = normalizePath(srcInfo.Path)
+ dstInfo.Path = normalizePath(dstInfo.Path)
+
+ // Separate the destination path between its directory and base
+ // components in case the source archive contents need to be rebased.
+ dstDir, dstBase := SplitPathDirEntry(dstInfo.Path)
+ _, srcBase := SplitPathDirEntry(srcInfo.Path)
+
+ switch {
+ case dstInfo.Exists && dstInfo.IsDir:
+ // The destination exists as a directory. No alteration
+ // to srcContent is needed as its contents can be
+ // simply extracted to the destination directory.
+ return dstInfo.Path, io.NopCloser(srcContent), nil
+ case dstInfo.Exists && srcInfo.IsDir:
+ // The destination exists as some type of file and the source
+ // content is a directory. This is an error condition since
+ // you cannot copy a directory to an existing file location.
+ return "", nil, ErrCannotCopyDir
+ case dstInfo.Exists:
+ // The destination exists as some type of file and the source content
+ // is also a file. The source content entry will have to be renamed to
+ // have a basename which matches the destination path's basename.
+ if len(srcInfo.RebaseName) != 0 {
+ srcBase = srcInfo.RebaseName
+ }
+ return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil
+ case srcInfo.IsDir:
+ // The destination does not exist and the source content is an archive
+ // of a directory. The archive should be extracted to the parent of
+ // the destination path instead, and when it is, the directory that is
+ // created as a result should take the name of the destination path.
+ // The source content entries will have to be renamed to have a
+ // basename which matches the destination path's basename.
+ if len(srcInfo.RebaseName) != 0 {
+ srcBase = srcInfo.RebaseName
+ }
+ return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil
+ case assertsDirectory(dstInfo.Path):
+ // The destination does not exist and is asserted to be created as a
+ // directory, but the source content is not a directory. This is an
+ // error condition since you cannot create a directory from a file
+ // source.
+ return "", nil, ErrDirNotExists
+ default:
+ // The last remaining case is when the destination does not exist, is
+ // not asserted to be a directory, and the source content is not an
+ // archive of a directory. It this case, the destination file will need
+ // to be created when the archive is extracted and the source content
+ // entry will have to be renamed to have a basename which matches the
+ // destination path's basename.
+ if len(srcInfo.RebaseName) != 0 {
+ srcBase = srcInfo.RebaseName
+ }
+ return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil
+ }
+}
+
+// RebaseArchiveEntries rewrites the given srcContent archive replacing
+// an occurrence of oldBase with newBase at the beginning of entry names.
+func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.ReadCloser {
+ if oldBase == string(os.PathSeparator) {
+ // If oldBase specifies the root directory, use an empty string as
+ // oldBase instead so that newBase doesn't replace the path separator
+ // that all paths will start with.
+ oldBase = ""
+ }
+
+ rebased, w := io.Pipe()
+
+ go func() {
+ srcTar := tar.NewReader(srcContent)
+ rebasedTar := tar.NewWriter(w)
+
+ for {
+ hdr, err := srcTar.Next()
+ if errors.Is(err, io.EOF) {
+ // Signals end of archive.
+ rebasedTar.Close()
+ w.Close()
+ return
+ }
+ if err != nil {
+ w.CloseWithError(err)
+ return
+ }
+
+ // srcContent tar stream, as served by TarWithOptions(), is
+ // definitely in PAX format, but tar.Next() mistakenly guesses it
+ // as USTAR, which creates a problem: if the newBase is >100
+ // characters long, WriteHeader() returns an error like
+ // "archive/tar: cannot encode header: Format specifies USTAR; and USTAR cannot encode Name=...".
+ //
+ // To fix, set the format to PAX here. See docker/for-linux issue #484.
+ hdr.Format = tar.FormatPAX
+ hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1)
+ if hdr.Typeflag == tar.TypeLink {
+ hdr.Linkname = strings.Replace(hdr.Linkname, oldBase, newBase, 1)
+ }
+
+ if err = rebasedTar.WriteHeader(hdr); err != nil {
+ w.CloseWithError(err)
+ return
+ }
+
+ // Ignoring GoSec G110. See https://github.com/securego/gosec/pull/433
+ // and https://cure53.de/pentest-report_opa.pdf, which recommends to
+ // replace io.Copy with io.CopyN7. The latter allows to specify the
+ // maximum number of bytes that should be read. By properly defining
+ // the limit, it can be assured that a GZip compression bomb cannot
+ // easily cause a Denial-of-Service.
+ // After reviewing with @tonistiigi and @cpuguy83, this should not
+ // affect us, because here we do not read into memory, hence should
+ // not be vulnerable to this code consuming memory.
+ //nolint:gosec // G110: Potential DoS vulnerability via decompression bomb (gosec)
+ if _, err = io.Copy(rebasedTar, srcTar); err != nil {
+ w.CloseWithError(err)
+ return
+ }
+ }
+ }()
+
+ return rebased
+}
+
+// CopyResource performs an archive copy from the given source path to the
+// given destination path. The source path MUST exist and the destination
+// path's parent directory must exist.
+func CopyResource(srcPath, dstPath string, followLink bool) error {
+ var (
+ srcInfo CopyInfo
+ err error
+ )
+
+ // Ensure in platform semantics
+ srcPath = normalizePath(srcPath)
+ dstPath = normalizePath(dstPath)
+
+ // Clean the source and destination paths.
+ srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath)
+ dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath)
+
+ if srcInfo, err = CopyInfoSourcePath(srcPath, followLink); err != nil {
+ return err
+ }
+
+ content, err := TarResource(srcInfo)
+ if err != nil {
+ return err
+ }
+ defer content.Close()
+
+ return CopyTo(content, srcInfo, dstPath)
+}
+
+// CopyTo handles extracting the given content whose
+// entries should be sourced from srcInfo to dstPath.
+func CopyTo(content io.Reader, srcInfo CopyInfo, dstPath string) error {
+ // The destination path need not exist, but CopyInfoDestinationPath will
+ // ensure that at least the parent directory exists.
+ dstInfo, err := CopyInfoDestinationPath(normalizePath(dstPath))
+ if err != nil {
+ return err
+ }
+
+ dstDir, copyArchive, err := PrepareArchiveCopy(content, srcInfo, dstInfo)
+ if err != nil {
+ return err
+ }
+ defer copyArchive.Close()
+
+ options := &TarOptions{
+ NoLchown: true,
+ NoOverwriteDirNonDir: true,
+ }
+
+ return Untar(copyArchive, dstDir, options)
+}
+
+// ResolveHostSourcePath decides real path need to be copied with parameters such as
+// whether to follow symbol link or not, if followLink is true, resolvedPath will return
+// link target of any symbol link file, else it will only resolve symlink of directory
+// but return symbol link file itself without resolving.
+func ResolveHostSourcePath(path string, followLink bool) (resolvedPath, rebaseName string, _ error) {
+ if followLink {
+ var err error
+ resolvedPath, err = filepath.EvalSymlinks(path)
+ if err != nil {
+ return "", "", err
+ }
+
+ resolvedPath, rebaseName = GetRebaseName(path, resolvedPath)
+ } else {
+ dirPath, basePath := filepath.Split(path)
+
+ // if not follow symbol link, then resolve symbol link of parent dir
+ resolvedDirPath, err := filepath.EvalSymlinks(dirPath)
+ if err != nil {
+ return "", "", err
+ }
+ // resolvedDirPath will have been cleaned (no trailing path separators) so
+ // we can manually join it with the base path element.
+ resolvedPath = resolvedDirPath + string(filepath.Separator) + basePath
+ if hasTrailingPathSeparator(path) &&
+ filepath.Base(path) != filepath.Base(resolvedPath) {
+ rebaseName = filepath.Base(path)
+ }
+ }
+ return resolvedPath, rebaseName, nil
+}
+
+// GetRebaseName normalizes and compares path and resolvedPath,
+// return completed resolved path and rebased file name
+func GetRebaseName(path, resolvedPath string) (string, string) {
+ // linkTarget will have been cleaned (no trailing path separators and dot) so
+ // we can manually join it with them
+ var rebaseName string
+ if specifiesCurrentDir(path) &&
+ !specifiesCurrentDir(resolvedPath) {
+ resolvedPath += string(filepath.Separator) + "."
+ }
+
+ if hasTrailingPathSeparator(path) &&
+ !hasTrailingPathSeparator(resolvedPath) {
+ resolvedPath += string(filepath.Separator)
+ }
+
+ if filepath.Base(path) != filepath.Base(resolvedPath) {
+ // In the case where the path had a trailing separator and a symlink
+ // evaluation has changed the last path component, we will need to
+ // rebase the name in the archive that is being copied to match the
+ // originally requested name.
+ rebaseName = filepath.Base(path)
+ }
+ return resolvedPath, rebaseName
+}
diff --git a/vendor/github.com/moby/go-archive/copy_unix.go b/vendor/github.com/moby/go-archive/copy_unix.go
new file mode 100644
index 0000000..f579282
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/copy_unix.go
@@ -0,0 +1,11 @@
+//go:build !windows
+
+package archive
+
+import (
+ "path/filepath"
+)
+
+func normalizePath(path string) string {
+ return filepath.ToSlash(path)
+}
diff --git a/vendor/github.com/moby/go-archive/copy_windows.go b/vendor/github.com/moby/go-archive/copy_windows.go
new file mode 100644
index 0000000..2b775b4
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/copy_windows.go
@@ -0,0 +1,9 @@
+package archive
+
+import (
+ "path/filepath"
+)
+
+func normalizePath(path string) string {
+ return filepath.FromSlash(path)
+}
diff --git a/vendor/github.com/moby/go-archive/dev_freebsd.go b/vendor/github.com/moby/go-archive/dev_freebsd.go
new file mode 100644
index 0000000..b3068fc
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/dev_freebsd.go
@@ -0,0 +1,9 @@
+//go:build freebsd
+
+package archive
+
+import "golang.org/x/sys/unix"
+
+func mknod(path string, mode uint32, dev uint64) error {
+ return unix.Mknod(path, mode, dev)
+}
diff --git a/vendor/github.com/moby/go-archive/dev_unix.go b/vendor/github.com/moby/go-archive/dev_unix.go
new file mode 100644
index 0000000..dffc596
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/dev_unix.go
@@ -0,0 +1,9 @@
+//go:build !windows && !freebsd
+
+package archive
+
+import "golang.org/x/sys/unix"
+
+func mknod(path string, mode uint32, dev uint64) error {
+ return unix.Mknod(path, mode, int(dev))
+}
diff --git a/vendor/github.com/moby/go-archive/diff.go b/vendor/github.com/moby/go-archive/diff.go
new file mode 100644
index 0000000..96db972
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/diff.go
@@ -0,0 +1,261 @@
+package archive
+
+import (
+ "archive/tar"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/containerd/log"
+
+ "github.com/moby/go-archive/compression"
+)
+
+// UnpackLayer unpack `layer` to a `dest`. The stream `layer` can be
+// compressed or uncompressed.
+// Returns the size in bytes of the contents of the layer.
+func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, err error) {
+ tr := tar.NewReader(layer)
+
+ var dirs []*tar.Header
+ unpackedPaths := make(map[string]struct{})
+
+ if options == nil {
+ options = &TarOptions{}
+ }
+ if options.ExcludePatterns == nil {
+ options.ExcludePatterns = []string{}
+ }
+
+ aufsTempdir := ""
+ aufsHardlinks := make(map[string]*tar.Header)
+
+ // Iterate through the files in the archive.
+ for {
+ hdr, err := tr.Next()
+ if errors.Is(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)
+
+ // Windows does not support filenames with colons in them. Ignore
+ // these files. This is not a problem though (although it might
+ // appear that it is). Let's suppose a client is running docker pull.
+ // The daemon it points to is Windows. Would it make sense for the
+ // client to be doing a docker pull Ubuntu for example (which has files
+ // with colons in the name under /usr/share/man/man3)? No, absolutely
+ // not as it would really only make sense that they were pulling a
+ // Windows image. However, for development, it is necessary to be able
+ // to pull Linux images which are in the repository.
+ //
+ // TODO Windows. Once the registry is aware of what images are Windows-
+ // specific or Linux-specific, this warning should be changed to an error
+ // to cater for the situation where someone does manage to upload a Linux
+ // image but have it tagged as Windows inadvertently.
+ if runtime.GOOS == "windows" {
+ if strings.Contains(hdr.Name, ":") {
+ log.G(context.TODO()).Warnf("Windows: Ignoring %s (is this a Linux image?)", hdr.Name)
+ continue
+ }
+ }
+
+ // Ensure that the parent directory exists.
+ err = createImpliedDirectories(dest, hdr, options)
+ if err != nil {
+ return 0, err
+ }
+
+ // Skip AUFS metadata dirs
+ if strings.HasPrefix(hdr.Name, WhiteoutMetaPrefix) {
+ // Regular files inside /.wh..wh.plnk can be used as hardlink targets
+ // We don't want this directory, but we need the files in them so that
+ // such hardlinks can be resolved.
+ if strings.HasPrefix(hdr.Name, WhiteoutLinkDir) && hdr.Typeflag == tar.TypeReg {
+ basename := filepath.Base(hdr.Name)
+ aufsHardlinks[basename] = hdr
+ if aufsTempdir == "" {
+ if aufsTempdir, err = os.MkdirTemp(dest, "dockerplnk"); err != nil {
+ return 0, err
+ }
+ defer os.RemoveAll(aufsTempdir)
+ }
+ if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, options); err != nil {
+ return 0, err
+ }
+ }
+
+ if hdr.Name != WhiteoutOpaqueDir {
+ continue
+ }
+ }
+ // #nosec G305 -- The joined path is guarded against path traversal.
+ path := filepath.Join(dest, hdr.Name)
+ rel, err := filepath.Rel(dest, path)
+ if err != nil {
+ return 0, err
+ }
+
+ // Note as these operations are platform specific, so must the slash be.
+ if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
+ return 0, breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest))
+ }
+ base := filepath.Base(path)
+
+ if strings.HasPrefix(base, WhiteoutPrefix) {
+ dir := filepath.Dir(path)
+ if base == WhiteoutOpaqueDir {
+ _, err := os.Lstat(dir)
+ if err != nil {
+ return 0, err
+ }
+ err = filepath.WalkDir(dir, func(path string, info os.DirEntry, 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 {
+ return os.RemoveAll(path)
+ }
+ return nil
+ })
+ if err != nil {
+ return 0, err
+ }
+ } else {
+ originalBase := base[len(WhiteoutPrefix):]
+ originalPath := filepath.Join(dir, originalBase)
+ if err := os.RemoveAll(originalPath); err != nil {
+ return 0, err
+ }
+ }
+ } else {
+ // 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
+
+ // Hard links into /.wh..wh.plnk don't work, as we don't extract that directory, so
+ // we manually retarget these into the temporary files we extracted them into
+ if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(filepath.Clean(hdr.Linkname), WhiteoutLinkDir) {
+ linkBasename := filepath.Base(hdr.Linkname)
+ srcHdr = aufsHardlinks[linkBasename]
+ if srcHdr == nil {
+ return 0, errors.New("invalid aufs hardlink")
+ }
+ tmpFile, err := os.Open(filepath.Join(aufsTempdir, linkBasename))
+ if err != nil {
+ return 0, err
+ }
+ defer tmpFile.Close()
+ srcData = tmpFile
+ }
+
+ if err := remapIDs(options.IDMap, srcHdr); err != nil {
+ return 0, err
+ }
+
+ if err := createTarFile(path, dest, srcHdr, srcData, options); 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 {
+ // #nosec G305 -- The header was checked for path traversal before it was appended to the dirs slice.
+ path := filepath.Join(dest, hdr.Name)
+ if err := chtimes(path, hdr.AccessTime, hdr.ModTime); err != nil {
+ return 0, err
+ }
+ }
+
+ return size, nil
+}
+
+// ApplyLayer parses a diff in the standard layer format from `layer`,
+// and applies it to the directory `dest`. The stream `layer` can be
+// compressed or uncompressed.
+// Returns the size in bytes of the contents of the layer.
+func ApplyLayer(dest string, layer io.Reader) (int64, error) {
+ return applyLayerHandler(dest, layer, &TarOptions{}, true)
+}
+
+// ApplyUncompressedLayer parses a diff in the standard layer format from
+// `layer`, and applies it to the directory `dest`. The stream `layer`
+// can only be uncompressed.
+// Returns the size in bytes of the contents of the layer.
+func ApplyUncompressedLayer(dest string, layer io.Reader, options *TarOptions) (int64, error) {
+ return applyLayerHandler(dest, layer, options, false)
+}
+
+// IsEmpty checks if the tar archive is empty (doesn't contain any entries).
+func IsEmpty(rd io.Reader) (bool, error) {
+ decompRd, err := compression.DecompressStream(rd)
+ if err != nil {
+ return true, fmt.Errorf("failed to decompress archive: %w", err)
+ }
+ defer decompRd.Close()
+
+ tarReader := tar.NewReader(decompRd)
+ if _, err := tarReader.Next(); err != nil {
+ if errors.Is(err, io.EOF) {
+ return true, nil
+ }
+ return false, fmt.Errorf("failed to read next archive header: %w", err)
+ }
+
+ return false, nil
+}
+
+// do the bulk load of ApplyLayer, but allow for not calling DecompressStream
+func applyLayerHandler(dest string, layer io.Reader, options *TarOptions, decompress bool) (int64, error) {
+ dest = filepath.Clean(dest)
+
+ // We need to be able to set any perms
+ restore := overrideUmask(0)
+ defer restore()
+
+ if decompress {
+ decompLayer, err := compression.DecompressStream(layer)
+ if err != nil {
+ return 0, err
+ }
+ defer decompLayer.Close()
+ layer = decompLayer
+ }
+ return UnpackLayer(dest, layer, options)
+}
diff --git a/vendor/github.com/moby/go-archive/diff_unix.go b/vendor/github.com/moby/go-archive/diff_unix.go
new file mode 100644
index 0000000..7216f2f
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/diff_unix.go
@@ -0,0 +1,21 @@
+//go:build !windows
+
+package archive
+
+import "golang.org/x/sys/unix"
+
+// overrideUmask sets current process's file mode creation mask to newmask
+// and returns a function to restore it.
+//
+// WARNING for readers stumbling upon this code. Changing umask in a multi-
+// threaded environment isn't safe. Don't use this without understanding the
+// risks, and don't export this function for others to use (we shouldn't even
+// be using this ourself).
+//
+// FIXME(thaJeztah): we should get rid of these hacks if possible.
+func overrideUmask(newMask int) func() {
+ oldMask := unix.Umask(newMask)
+ return func() {
+ unix.Umask(oldMask)
+ }
+}
diff --git a/vendor/github.com/moby/go-archive/diff_windows.go b/vendor/github.com/moby/go-archive/diff_windows.go
new file mode 100644
index 0000000..d28f5b2
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/diff_windows.go
@@ -0,0 +1,6 @@
+package archive
+
+// overrideUmask is a no-op on windows.
+func overrideUmask(newmask int) func() {
+ return func() {}
+}
diff --git a/vendor/github.com/moby/go-archive/path.go b/vendor/github.com/moby/go-archive/path.go
new file mode 100644
index 0000000..888a697
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/path.go
@@ -0,0 +1,20 @@
+package archive
+
+// CheckSystemDriveAndRemoveDriveLetter verifies that a path, if it includes a drive letter,
+// is the system drive.
+// On Linux: this is a no-op.
+// On Windows: this does the following>
+// CheckSystemDriveAndRemoveDriveLetter verifies and manipulates a Windows path.
+// This is used, for example, when validating a user provided path in docker cp.
+// If a drive letter is supplied, it must be the system drive. The drive letter
+// is always removed. Also, it translates it to OS semantics (IOW / to \). We
+// need the path in this syntax so that it can ultimately be concatenated with
+// a Windows long-path which doesn't support drive-letters. Examples:
+// C: --> Fail
+// C:\ --> \
+// a --> a
+// /a --> \a
+// d:\ --> Fail
+func CheckSystemDriveAndRemoveDriveLetter(path string) (string, error) {
+ return checkSystemDriveAndRemoveDriveLetter(path)
+}
diff --git a/vendor/github.com/moby/go-archive/path_unix.go b/vendor/github.com/moby/go-archive/path_unix.go
new file mode 100644
index 0000000..390264b
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/path_unix.go
@@ -0,0 +1,9 @@
+//go:build !windows
+
+package archive
+
+// checkSystemDriveAndRemoveDriveLetter is the non-Windows implementation
+// of CheckSystemDriveAndRemoveDriveLetter
+func checkSystemDriveAndRemoveDriveLetter(path string) (string, error) {
+ return path, nil
+}
diff --git a/vendor/github.com/moby/go-archive/path_windows.go b/vendor/github.com/moby/go-archive/path_windows.go
new file mode 100644
index 0000000..7e18c8e
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/path_windows.go
@@ -0,0 +1,22 @@
+package archive
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+)
+
+// checkSystemDriveAndRemoveDriveLetter is the Windows implementation
+// of CheckSystemDriveAndRemoveDriveLetter
+func checkSystemDriveAndRemoveDriveLetter(path string) (string, error) {
+ if len(path) == 2 && string(path[1]) == ":" {
+ return "", fmt.Errorf("no relative path specified in %q", path)
+ }
+ if !filepath.IsAbs(path) || len(path) < 2 {
+ return filepath.FromSlash(path), nil
+ }
+ if string(path[1]) == ":" && !strings.EqualFold(string(path[0]), "c") {
+ return "", fmt.Errorf("the specified path is not on the system drive (C:)")
+ }
+ return filepath.FromSlash(path[2:]), nil
+}
diff --git a/vendor/github.com/moby/go-archive/tarheader/tarheader.go b/vendor/github.com/moby/go-archive/tarheader/tarheader.go
new file mode 100644
index 0000000..03732a4
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/tarheader/tarheader.go
@@ -0,0 +1,67 @@
+package tarheader
+
+import (
+ "archive/tar"
+ "os"
+)
+
+// assert that we implement [tar.FileInfoNames].
+var _ tar.FileInfoNames = (*nosysFileInfo)(nil)
+
+// nosysFileInfo hides the system-dependent info of the wrapped FileInfo to
+// prevent tar.FileInfoHeader from introspecting it and potentially calling into
+// glibc.
+//
+// It implements [tar.FileInfoNames] to further prevent [tar.FileInfoHeader]
+// from performing any lookups on go1.23 and up. see https://go.dev/issue/50102
+type nosysFileInfo struct {
+ os.FileInfo
+}
+
+// Uname stubs out looking up username. It implements [tar.FileInfoNames]
+// to prevent [tar.FileInfoHeader] from loading libraries to perform
+// username lookups.
+func (fi nosysFileInfo) Uname() (string, error) {
+ return "", nil
+}
+
+// Gname stubs out looking up group-name. It implements [tar.FileInfoNames]
+// to prevent [tar.FileInfoHeader] from loading libraries to perform
+// username lookups.
+func (fi nosysFileInfo) Gname() (string, error) {
+ return "", nil
+}
+
+func (fi nosysFileInfo) Sys() interface{} {
+ // A Sys value of type *tar.Header is safe as it is system-independent.
+ // The tar.FileInfoHeader function copies the fields into the returned
+ // header without performing any OS lookups.
+ if sys, ok := fi.FileInfo.Sys().(*tar.Header); ok {
+ return sys
+ }
+ return nil
+}
+
+// FileInfoHeaderNoLookups creates a partially-populated tar.Header from fi.
+//
+// Compared to the archive/tar.FileInfoHeader function, this function is safe to
+// call from a chrooted process as it does not populate fields which would
+// require operating system lookups. It behaves identically to
+// tar.FileInfoHeader when fi is a FileInfo value returned from
+// tar.Header.FileInfo().
+//
+// When fi is a FileInfo for a native file, such as returned from os.Stat() and
+// os.Lstat(), the returned Header value differs from one returned from
+// tar.FileInfoHeader in the following ways. The Uname and Gname fields are not
+// set as OS lookups would be required to populate them. The AccessTime and
+// ChangeTime fields are not currently set (not yet implemented) although that
+// is subject to change. Callers which require the AccessTime or ChangeTime
+// fields to be zeroed should explicitly zero them out in the returned Header
+// value to avoid any compatibility issues in the future.
+func FileInfoHeaderNoLookups(fi os.FileInfo, link string) (*tar.Header, error) {
+ hdr, err := tar.FileInfoHeader(nosysFileInfo{fi}, link)
+ if err != nil {
+ return nil, err
+ }
+ return hdr, sysStat(fi, hdr)
+}
diff --git a/vendor/github.com/moby/go-archive/tarheader/tarheader_unix.go b/vendor/github.com/moby/go-archive/tarheader/tarheader_unix.go
new file mode 100644
index 0000000..9c3311c
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/tarheader/tarheader_unix.go
@@ -0,0 +1,46 @@
+//go:build !windows
+
+package tarheader
+
+import (
+ "archive/tar"
+ "os"
+ "runtime"
+ "syscall"
+
+ "golang.org/x/sys/unix"
+)
+
+// sysStat populates hdr from system-dependent fields of fi without performing
+// any OS lookups.
+func sysStat(fi os.FileInfo, hdr *tar.Header) error {
+ // Devmajor and Devminor are only needed for special devices.
+
+ // In FreeBSD, RDev for regular files is -1 (unless overridden by FS):
+ // https://cgit.freebsd.org/src/tree/sys/kern/vfs_default.c?h=stable/13#n1531
+ // (NODEV is -1: https://cgit.freebsd.org/src/tree/sys/sys/param.h?h=stable/13#n241).
+
+ // ZFS in particular does not override the default:
+ // https://cgit.freebsd.org/src/tree/sys/contrib/openzfs/module/os/freebsd/zfs/zfs_vnops_os.c?h=stable/13#n2027
+
+ // Since `Stat_t.Rdev` is uint64, the cast turns -1 into (2^64 - 1).
+ // Such large values cannot be encoded in a tar header.
+ if runtime.GOOS == "freebsd" && hdr.Typeflag != tar.TypeBlock && hdr.Typeflag != tar.TypeChar {
+ return nil
+ }
+ s, ok := fi.Sys().(*syscall.Stat_t)
+ if !ok {
+ return nil
+ }
+
+ hdr.Uid = int(s.Uid)
+ hdr.Gid = int(s.Gid)
+
+ if s.Mode&unix.S_IFBLK != 0 ||
+ s.Mode&unix.S_IFCHR != 0 {
+ hdr.Devmajor = int64(unix.Major(uint64(s.Rdev))) //nolint: unconvert
+ hdr.Devminor = int64(unix.Minor(uint64(s.Rdev))) //nolint: unconvert
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/moby/go-archive/tarheader/tarheader_windows.go b/vendor/github.com/moby/go-archive/tarheader/tarheader_windows.go
new file mode 100644
index 0000000..5d4483c
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/tarheader/tarheader_windows.go
@@ -0,0 +1,12 @@
+package tarheader
+
+import (
+ "archive/tar"
+ "os"
+)
+
+// sysStat populates hdr from system-dependent fields of fi without performing
+// any OS lookups. It is a no-op on Windows.
+func sysStat(os.FileInfo, *tar.Header) error {
+ return nil
+}
diff --git a/vendor/github.com/moby/go-archive/time.go b/vendor/github.com/moby/go-archive/time.go
new file mode 100644
index 0000000..4e9ae95
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/time.go
@@ -0,0 +1,38 @@
+package archive
+
+import (
+ "syscall"
+ "time"
+ "unsafe"
+)
+
+var (
+ minTime = time.Unix(0, 0)
+ maxTime time.Time
+)
+
+func init() {
+ if unsafe.Sizeof(syscall.Timespec{}.Nsec) == 8 {
+ // This is a 64 bit timespec
+ // os.Chtimes limits time to the following
+ maxTime = time.Unix(0, 1<<63-1)
+ } else {
+ // This is a 32 bit timespec
+ maxTime = time.Unix(1<<31-1, 0)
+ }
+}
+
+func boundTime(t time.Time) time.Time {
+ if t.Before(minTime) || t.After(maxTime) {
+ return minTime
+ }
+
+ return t
+}
+
+func latestTime(t1, t2 time.Time) time.Time {
+ if t1.Before(t2) {
+ return t2
+ }
+ return t1
+}
diff --git a/vendor/github.com/moby/go-archive/time_nonwindows.go b/vendor/github.com/moby/go-archive/time_nonwindows.go
new file mode 100644
index 0000000..5bfdfa2
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/time_nonwindows.go
@@ -0,0 +1,41 @@
+//go:build !windows
+
+package archive
+
+import (
+ "os"
+ "time"
+
+ "golang.org/x/sys/unix"
+)
+
+// chtimes changes the access time and modified time of a file at the given path.
+// If the modified time is prior to the Unix Epoch (unixMinTime), or after the
+// end of Unix Time (unixEpochTime), os.Chtimes has undefined behavior. In this
+// case, Chtimes defaults to Unix Epoch, just in case.
+func chtimes(name string, atime time.Time, mtime time.Time) error {
+ return os.Chtimes(name, atime, mtime)
+}
+
+func timeToTimespec(time time.Time) unix.Timespec {
+ if time.IsZero() {
+ // Return UTIME_OMIT special value
+ return unix.Timespec{
+ Sec: 0,
+ Nsec: (1 << 30) - 2,
+ }
+ }
+ return unix.NsecToTimespec(time.UnixNano())
+}
+
+func lchtimes(name string, atime time.Time, mtime time.Time) error {
+ utimes := [2]unix.Timespec{
+ timeToTimespec(atime),
+ timeToTimespec(mtime),
+ }
+ err := unix.UtimesNanoAt(unix.AT_FDCWD, name, utimes[0:], unix.AT_SYMLINK_NOFOLLOW)
+ if err != nil && err != unix.ENOSYS {
+ return err
+ }
+ return err
+}
diff --git a/vendor/github.com/moby/go-archive/time_windows.go b/vendor/github.com/moby/go-archive/time_windows.go
new file mode 100644
index 0000000..af1f7c8
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/time_windows.go
@@ -0,0 +1,32 @@
+package archive
+
+import (
+ "os"
+ "time"
+
+ "golang.org/x/sys/windows"
+)
+
+func chtimes(name string, atime time.Time, mtime time.Time) error {
+ if err := os.Chtimes(name, atime, mtime); err != nil {
+ return err
+ }
+
+ pathp, err := windows.UTF16PtrFromString(name)
+ if err != nil {
+ return err
+ }
+ h, err := windows.CreateFile(pathp,
+ windows.FILE_WRITE_ATTRIBUTES, windows.FILE_SHARE_WRITE, nil,
+ windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0)
+ if err != nil {
+ return err
+ }
+ defer windows.Close(h)
+ c := windows.NsecToFiletime(mtime.UnixNano())
+ return windows.SetFileTime(h, &c, nil, nil)
+}
+
+func lchtimes(name string, atime time.Time, mtime time.Time) error {
+ return nil
+}
diff --git a/vendor/github.com/moby/go-archive/whiteouts.go b/vendor/github.com/moby/go-archive/whiteouts.go
new file mode 100644
index 0000000..d20478a
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/whiteouts.go
@@ -0,0 +1,23 @@
+package archive
+
+// Whiteouts are files with a special meaning for the layered filesystem.
+// Docker uses AUFS whiteout files inside exported archives. In other
+// filesystems these files are generated/handled on tar creation/extraction.
+
+// 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.
+const 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.
+const WhiteoutMetaPrefix = WhiteoutPrefix + WhiteoutPrefix
+
+// WhiteoutLinkDir is a directory AUFS uses for storing hardlink links to other
+// layers. Normally these should not go into exported archives and all changed
+// hardlinks should be copied to the top layer.
+const WhiteoutLinkDir = WhiteoutMetaPrefix + "plnk"
+
+// WhiteoutOpaqueDir file means directory has been made opaque - meaning
+// readdir calls to this directory do not follow to lower layers.
+const WhiteoutOpaqueDir = WhiteoutMetaPrefix + ".opq"
diff --git a/vendor/github.com/moby/go-archive/wrap.go b/vendor/github.com/moby/go-archive/wrap.go
new file mode 100644
index 0000000..f8a9725
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/wrap.go
@@ -0,0 +1,59 @@
+package archive
+
+import (
+ "archive/tar"
+ "bytes"
+ "io"
+)
+
+// Generate generates a new archive from the content provided
+// as input.
+//
+// `files` is a sequence of path/content pairs. A new file is
+// added to the archive for each pair.
+// If the last pair is incomplete, the file is created with an
+// empty content. For example:
+//
+// Generate("foo.txt", "hello world", "emptyfile")
+//
+// The above call will return an archive with 2 files:
+// - ./foo.txt with content "hello world"
+// - ./empty with empty content
+//
+// FIXME: stream content instead of buffering
+// FIXME: specify permissions and other archive metadata
+func Generate(input ...string) (io.Reader, error) {
+ files := parseStringPairs(input...)
+ buf := new(bytes.Buffer)
+ tw := tar.NewWriter(buf)
+ for _, file := range files {
+ name, content := file[0], file[1]
+ hdr := &tar.Header{
+ Name: name,
+ Size: int64(len(content)),
+ }
+ if err := tw.WriteHeader(hdr); err != nil {
+ return nil, err
+ }
+ if _, err := tw.Write([]byte(content)); err != nil {
+ return nil, err
+ }
+ }
+ if err := tw.Close(); err != nil {
+ return nil, err
+ }
+ return buf, nil
+}
+
+func parseStringPairs(input ...string) [][2]string {
+ output := make([][2]string, 0, len(input)/2+1)
+ for i := 0; i < len(input); i += 2 {
+ var pair [2]string
+ pair[0] = input[i]
+ if i+1 < len(input) {
+ pair[1] = input[i+1]
+ }
+ output = append(output, pair)
+ }
+ return output
+}
diff --git a/vendor/github.com/moby/go-archive/xattr_supported.go b/vendor/github.com/moby/go-archive/xattr_supported.go
new file mode 100644
index 0000000..652a1f0
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/xattr_supported.go
@@ -0,0 +1,52 @@
+//go:build linux || darwin || freebsd || netbsd
+
+package archive
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+
+ "golang.org/x/sys/unix"
+)
+
+// lgetxattr retrieves the value of the extended attribute identified by attr
+// and associated with the given path in the file system.
+// It returns a nil slice and nil error if the xattr is not set.
+func lgetxattr(path string, attr string) ([]byte, error) {
+ // Start with a 128 length byte array
+ dest := make([]byte, 128)
+ sz, err := unix.Lgetxattr(path, attr, dest)
+
+ for errors.Is(err, unix.ERANGE) {
+ // Buffer too small, use zero-sized buffer to get the actual size
+ sz, err = unix.Lgetxattr(path, attr, []byte{})
+ if err != nil {
+ return nil, wrapPathError("lgetxattr", path, attr, err)
+ }
+ dest = make([]byte, sz)
+ sz, err = unix.Lgetxattr(path, attr, dest)
+ }
+
+ if err != nil {
+ if errors.Is(err, noattr) {
+ return nil, nil
+ }
+ return nil, wrapPathError("lgetxattr", path, attr, err)
+ }
+
+ return dest[:sz], nil
+}
+
+// lsetxattr sets the value of the extended attribute identified by attr
+// and associated with the given path in the file system.
+func lsetxattr(path string, attr string, data []byte, flags int) error {
+ return wrapPathError("lsetxattr", path, attr, unix.Lsetxattr(path, attr, data, flags))
+}
+
+func wrapPathError(op, path, attr string, err error) error {
+ if err == nil {
+ return nil
+ }
+ return &fs.PathError{Op: op, Path: path, Err: fmt.Errorf("xattr %q: %w", attr, err)}
+}
diff --git a/vendor/github.com/moby/go-archive/xattr_supported_linux.go b/vendor/github.com/moby/go-archive/xattr_supported_linux.go
new file mode 100644
index 0000000..f2e7646
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/xattr_supported_linux.go
@@ -0,0 +1,5 @@
+package archive
+
+import "golang.org/x/sys/unix"
+
+var noattr = unix.ENODATA
diff --git a/vendor/github.com/moby/go-archive/xattr_supported_unix.go b/vendor/github.com/moby/go-archive/xattr_supported_unix.go
new file mode 100644
index 0000000..4d88241
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/xattr_supported_unix.go
@@ -0,0 +1,7 @@
+//go:build !linux && !windows
+
+package archive
+
+import "golang.org/x/sys/unix"
+
+var noattr = unix.ENOATTR
diff --git a/vendor/github.com/moby/go-archive/xattr_unsupported.go b/vendor/github.com/moby/go-archive/xattr_unsupported.go
new file mode 100644
index 0000000..b0d9165
--- /dev/null
+++ b/vendor/github.com/moby/go-archive/xattr_unsupported.go
@@ -0,0 +1,11 @@
+//go:build !linux && !darwin && !freebsd && !netbsd
+
+package archive
+
+func lgetxattr(path string, attr string) ([]byte, error) {
+ return nil, nil
+}
+
+func lsetxattr(path string, attr string, data []byte, flags int) error {
+ return nil
+}
diff --git a/vendor/github.com/moby/sys/sequential/sequential_unix.go b/vendor/github.com/moby/sys/sequential/sequential_unix.go
index a3c7340..278cdfb 100644
--- a/vendor/github.com/moby/sys/sequential/sequential_unix.go
+++ b/vendor/github.com/moby/sys/sequential/sequential_unix.go
@@ -5,41 +5,22 @@ package sequential
import "os"
-// Create creates the named file with mode 0666 (before umask), truncating
-// it if it already exists. If successful, methods on the returned
-// File can be used for I/O; the associated file descriptor has mode
-// O_RDWR.
-// If there is an error, it will be of type *PathError.
+// Create is an alias for [os.Create] on non-Windows platforms.
func Create(name string) (*os.File, error) {
return os.Create(name)
}
-// Open opens the named file for reading. If successful, methods on
-// the returned file can be used for reading; the associated file
-// descriptor has mode O_RDONLY.
-// If there is an error, it will be of type *PathError.
+// Open is an alias for [os.Open] on non-Windows platforms.
func Open(name string) (*os.File, error) {
return os.Open(name)
}
-// OpenFile is the generalized open call; most users will use Open
-// or Create instead. It opens the named file with specified flag
-// (O_RDONLY etc.) and perm, (0666 etc.) if applicable. If successful,
-// methods on the returned File can be used for I/O.
-// If there is an error, it will be of type *PathError.
+// OpenFile is an alias for [os.OpenFile] on non-Windows platforms.
func OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) {
return os.OpenFile(name, flag, perm)
}
-// CreateTemp creates a new temporary file in the directory dir
-// with a name beginning with prefix, opens the file for reading
-// and writing, and returns the resulting *os.File.
-// If dir is the empty string, TempFile uses the default directory
-// for temporary files (see os.TempDir).
-// Multiple programs calling TempFile simultaneously
-// will not choose the same file. The caller can use f.Name()
-// to find the pathname of the file. It is the caller's responsibility
-// to remove the file when no longer needed.
+// CreateTemp is an alias for [os.CreateTemp] on non-Windows platforms.
func CreateTemp(dir, prefix string) (f *os.File, err error) {
return os.CreateTemp(dir, prefix)
}
diff --git a/vendor/github.com/moby/sys/sequential/sequential_windows.go b/vendor/github.com/moby/sys/sequential/sequential_windows.go
index 3f7f0d8..3500ecc 100644
--- a/vendor/github.com/moby/sys/sequential/sequential_windows.go
+++ b/vendor/github.com/moby/sys/sequential/sequential_windows.go
@@ -5,48 +5,52 @@ import (
"path/filepath"
"strconv"
"sync"
- "syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
-// Create creates the named file with mode 0666 (before umask), truncating
-// it if it already exists. If successful, methods on the returned
-// File can be used for I/O; the associated file descriptor has mode
-// O_RDWR.
-// If there is an error, it will be of type *PathError.
+// Create is a copy of [os.Create], modified to use sequential file access.
+//
+// It uses [windows.FILE_FLAG_SEQUENTIAL_SCAN] rather than [windows.FILE_ATTRIBUTE_NORMAL]
+// as implemented in golang. Refer to the [Win32 API documentation] for details
+// on sequential file access.
+//
+// [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
func Create(name string) (*os.File, error) {
- return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0)
+ return openFileSequential(name, windows.O_RDWR|windows.O_CREAT|windows.O_TRUNC)
}
-// Open opens the named file for reading. If successful, methods on
-// the returned file can be used for reading; the associated file
-// descriptor has mode O_RDONLY.
-// If there is an error, it will be of type *PathError.
+// Open is a copy of [os.Open], modified to use sequential file access.
+//
+// It uses [windows.FILE_FLAG_SEQUENTIAL_SCAN] rather than [windows.FILE_ATTRIBUTE_NORMAL]
+// as implemented in golang. Refer to the [Win32 API documentation] for details
+// on sequential file access.
+//
+// [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
func Open(name string) (*os.File, error) {
- return OpenFile(name, os.O_RDONLY, 0)
+ return openFileSequential(name, windows.O_RDONLY)
}
-// OpenFile is the generalized open call; most users will use Open
-// or Create instead.
-// If there is an error, it will be of type *PathError.
+// OpenFile is a copy of [os.OpenFile], modified to use sequential file access.
+//
+// It uses [windows.FILE_FLAG_SEQUENTIAL_SCAN] rather than [windows.FILE_ATTRIBUTE_NORMAL]
+// as implemented in golang. Refer to the [Win32 API documentation] for details
+// on sequential file access.
+//
+// [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
func OpenFile(name string, flag int, _ os.FileMode) (*os.File, error) {
- if name == "" {
- return nil, &os.PathError{Op: "open", Path: name, Err: syscall.ENOENT}
- }
- r, err := openFileSequential(name, flag, 0)
- if err == nil {
- return r, nil
- }
- return nil, &os.PathError{Op: "open", Path: name, Err: err}
+ return openFileSequential(name, flag)
}
-func openFileSequential(name string, flag int, _ os.FileMode) (file *os.File, err error) {
- r, e := openSequential(name, flag|windows.O_CLOEXEC, 0)
+func openFileSequential(name string, flag int) (file *os.File, err error) {
+ if name == "" {
+ return nil, &os.PathError{Op: "open", Path: name, Err: windows.ERROR_FILE_NOT_FOUND}
+ }
+ r, e := openSequential(name, flag|windows.O_CLOEXEC)
if e != nil {
- return nil, e
+ return nil, &os.PathError{Op: "open", Path: name, Err: e}
}
return os.NewFile(uintptr(r), name), nil
}
@@ -58,7 +62,7 @@ func makeInheritSa() *windows.SecurityAttributes {
return &sa
}
-func openSequential(path string, mode int, _ uint32) (fd windows.Handle, err error) {
+func openSequential(path string, mode int) (fd windows.Handle, err error) {
if len(path) == 0 {
return windows.InvalidHandle, windows.ERROR_FILE_NOT_FOUND
}
@@ -101,15 +105,16 @@ func openSequential(path string, mode int, _ uint32) (fd windows.Handle, err err
createmode = windows.OPEN_EXISTING
}
// Use FILE_FLAG_SEQUENTIAL_SCAN rather than FILE_ATTRIBUTE_NORMAL as implemented in golang.
- // https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx
- const fileFlagSequentialScan = 0x08000000 // FILE_FLAG_SEQUENTIAL_SCAN
- h, e := windows.CreateFile(pathp, access, sharemode, sa, createmode, fileFlagSequentialScan, 0)
+ // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
+ h, e := windows.CreateFile(pathp, access, sharemode, sa, createmode, windows.FILE_FLAG_SEQUENTIAL_SCAN, 0)
return h, e
}
// Helpers for CreateTemp
-var rand uint32
-var randmu sync.Mutex
+var (
+ rand uint32
+ randmu sync.Mutex
+)
func reseed() uint32 {
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
@@ -127,17 +132,13 @@ func nextSuffix() string {
return strconv.Itoa(int(1e9 + r%1e9))[1:]
}
-// CreateTemp is a copy of os.CreateTemp, modified to use sequential
-// file access. Below is the original comment from golang:
-// TempFile creates a new temporary file in the directory dir
-// with a name beginning with prefix, opens the file for reading
-// and writing, and returns the resulting *os.File.
-// If dir is the empty string, TempFile uses the default directory
-// for temporary files (see os.TempDir).
-// Multiple programs calling TempFile simultaneously
-// will not choose the same file. The caller can use f.Name()
-// to find the pathname of the file. It is the caller's responsibility
-// to remove the file when no longer needed.
+// CreateTemp is a copy of [os.CreateTemp], modified to use sequential file access.
+//
+// It uses [windows.FILE_FLAG_SEQUENTIAL_SCAN] rather than [windows.FILE_ATTRIBUTE_NORMAL]
+// as implemented in golang. Refer to the [Win32 API documentation] for details
+// on sequential file access.
+//
+// [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
func CreateTemp(dir, prefix string) (f *os.File, err error) {
if dir == "" {
dir = os.TempDir()
@@ -146,7 +147,7 @@ func CreateTemp(dir, prefix string) (f *os.File, err error) {
nconflict := 0
for i := 0; i < 10000; i++ {
name := filepath.Join(dir, prefix+nextSuffix())
- f, err = OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
+ f, err = openFileSequential(name, windows.O_RDWR|windows.O_CREAT|windows.O_EXCL)
if os.IsExist(err) {
if nconflict++; nconflict > 10 {
randmu.Lock()
diff --git a/vendor/github.com/moby/sys/user/idtools.go b/vendor/github.com/moby/sys/user/idtools.go
new file mode 100644
index 0000000..595b7a9
--- /dev/null
+++ b/vendor/github.com/moby/sys/user/idtools.go
@@ -0,0 +1,141 @@
+package user
+
+import (
+ "fmt"
+ "os"
+)
+
+// MkdirOpt is a type for options to pass to Mkdir calls
+type MkdirOpt func(*mkdirOptions)
+
+type mkdirOptions struct {
+ onlyNew bool
+}
+
+// WithOnlyNew is an option for MkdirAllAndChown that will only change ownership and permissions
+// on newly created directories. If the directory already exists, it will not be modified
+func WithOnlyNew(o *mkdirOptions) {
+ o.onlyNew = true
+}
+
+// MkdirAllAndChown creates a directory (include any along the path) and then modifies
+// ownership to the requested uid/gid. By default, if the directory already exists, this
+// function will still change ownership and permissions. If WithOnlyNew is passed as an
+// option, then only the newly created directories will have ownership and permissions changed.
+func MkdirAllAndChown(path string, mode os.FileMode, uid, gid int, opts ...MkdirOpt) error {
+ var options mkdirOptions
+ for _, opt := range opts {
+ opt(&options)
+ }
+
+ return mkdirAs(path, mode, uid, gid, true, options.onlyNew)
+}
+
+// MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid.
+// By default, if the directory already exists, this function still changes ownership and permissions.
+// If WithOnlyNew is passed as an option, then only the newly created directory will have ownership
+// and permissions changed.
+// Note that unlike os.Mkdir(), this function does not return IsExist error
+// in case path already exists.
+func MkdirAndChown(path string, mode os.FileMode, uid, gid int, opts ...MkdirOpt) error {
+ var options mkdirOptions
+ for _, opt := range opts {
+ opt(&options)
+ }
+ return mkdirAs(path, mode, uid, gid, false, options.onlyNew)
+}
+
+// getRootUIDGID retrieves the remapped root uid/gid pair from the set of maps.
+// If the maps are empty, then the root uid/gid will default to "real" 0/0
+func getRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) {
+ uid, err := toHost(0, uidMap)
+ if err != nil {
+ return -1, -1, err
+ }
+ gid, err := toHost(0, gidMap)
+ if err != nil {
+ return -1, -1, err
+ }
+ return uid, gid, nil
+}
+
+// toContainer takes an id mapping, and uses it to translate a
+// host ID to the remapped ID. If no map is provided, then the translation
+// assumes a 1-to-1 mapping and returns the passed in id
+func toContainer(hostID int, idMap []IDMap) (int, error) {
+ if idMap == nil {
+ return hostID, nil
+ }
+ for _, m := range idMap {
+ if (int64(hostID) >= m.ParentID) && (int64(hostID) <= (m.ParentID + m.Count - 1)) {
+ contID := int(m.ID + (int64(hostID) - m.ParentID))
+ return contID, nil
+ }
+ }
+ return -1, fmt.Errorf("host ID %d cannot be mapped to a container ID", hostID)
+}
+
+// toHost takes an id mapping and a remapped ID, and translates the
+// ID to the mapped host ID. If no map is provided, then the translation
+// assumes a 1-to-1 mapping and returns the passed in id #
+func toHost(contID int, idMap []IDMap) (int, error) {
+ if idMap == nil {
+ return contID, nil
+ }
+ for _, m := range idMap {
+ if (int64(contID) >= m.ID) && (int64(contID) <= (m.ID + m.Count - 1)) {
+ hostID := int(m.ParentID + (int64(contID) - m.ID))
+ return hostID, nil
+ }
+ }
+ return -1, fmt.Errorf("container ID %d cannot be mapped to a host ID", contID)
+}
+
+// IdentityMapping contains a mappings of UIDs and GIDs.
+// The zero value represents an empty mapping.
+type IdentityMapping struct {
+ UIDMaps []IDMap `json:"UIDMaps"`
+ GIDMaps []IDMap `json:"GIDMaps"`
+}
+
+// RootPair returns a uid and gid pair for the root user. The error is ignored
+// because a root user always exists, and the defaults are correct when the uid
+// and gid maps are empty.
+func (i IdentityMapping) RootPair() (int, int) {
+ uid, gid, _ := getRootUIDGID(i.UIDMaps, i.GIDMaps)
+ return uid, gid
+}
+
+// ToHost returns the host UID and GID for the container uid, gid.
+// Remapping is only performed if the ids aren't already the remapped root ids
+func (i IdentityMapping) ToHost(uid, gid int) (int, int, error) {
+ var err error
+ ruid, rgid := i.RootPair()
+
+ if uid != ruid {
+ ruid, err = toHost(uid, i.UIDMaps)
+ if err != nil {
+ return ruid, rgid, err
+ }
+ }
+
+ if gid != rgid {
+ rgid, err = toHost(gid, i.GIDMaps)
+ }
+ return ruid, rgid, err
+}
+
+// ToContainer returns the container UID and GID for the host uid and gid
+func (i IdentityMapping) ToContainer(uid, gid int) (int, int, error) {
+ ruid, err := toContainer(uid, i.UIDMaps)
+ if err != nil {
+ return -1, -1, err
+ }
+ rgid, err := toContainer(gid, i.GIDMaps)
+ return ruid, rgid, err
+}
+
+// Empty returns true if there are no id mappings
+func (i IdentityMapping) Empty() bool {
+ return len(i.UIDMaps) == 0 && len(i.GIDMaps) == 0
+}
diff --git a/vendor/github.com/moby/sys/user/idtools_unix.go b/vendor/github.com/moby/sys/user/idtools_unix.go
new file mode 100644
index 0000000..4e39d24
--- /dev/null
+++ b/vendor/github.com/moby/sys/user/idtools_unix.go
@@ -0,0 +1,143 @@
+//go:build !windows
+
+package user
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "syscall"
+)
+
+func mkdirAs(path string, mode os.FileMode, uid, gid int, mkAll, onlyNew bool) error {
+ path, err := filepath.Abs(path)
+ if err != nil {
+ return err
+ }
+
+ stat, err := os.Stat(path)
+ if err == nil {
+ if !stat.IsDir() {
+ return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
+ }
+ if onlyNew {
+ return nil
+ }
+
+ // short-circuit -- we were called with an existing directory and chown was requested
+ return setPermissions(path, mode, uid, gid, stat)
+ }
+
+ // make an array containing the original path asked for, plus (for mkAll == true)
+ // all path components leading up to the complete path that don't exist before we MkdirAll
+ // so that we can chown all of them properly at the end. If onlyNew is true, we won't
+ // chown the full directory path if it exists
+ var paths []string
+ if os.IsNotExist(err) {
+ paths = append(paths, path)
+ }
+
+ if mkAll {
+ // walk back to "/" looking for directories which do not exist
+ // and add them to the paths array for chown after creation
+ dirPath := path
+ for {
+ dirPath = filepath.Dir(dirPath)
+ if dirPath == "/" {
+ break
+ }
+ if _, err = os.Stat(dirPath); os.IsNotExist(err) {
+ paths = append(paths, dirPath)
+ }
+ }
+ if err = os.MkdirAll(path, mode); err != nil {
+ return err
+ }
+ } else if err = os.Mkdir(path, mode); err != nil {
+ return err
+ }
+ // even if it existed, we will chown the requested path + any subpaths that
+ // didn't exist when we called MkdirAll
+ for _, pathComponent := range paths {
+ if err = setPermissions(pathComponent, mode, uid, gid, nil); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// setPermissions performs a chown/chmod only if the uid/gid don't match what's requested
+// Normally a Chown is a no-op if uid/gid match, but in some cases this can still cause an error, e.g. if the
+// dir is on an NFS share, so don't call chown unless we absolutely must.
+// Likewise for setting permissions.
+func setPermissions(p string, mode os.FileMode, uid, gid int, stat os.FileInfo) error {
+ if stat == nil {
+ var err error
+ stat, err = os.Stat(p)
+ if err != nil {
+ return err
+ }
+ }
+ if stat.Mode().Perm() != mode.Perm() {
+ if err := os.Chmod(p, mode.Perm()); err != nil {
+ return err
+ }
+ }
+ ssi := stat.Sys().(*syscall.Stat_t)
+ if ssi.Uid == uint32(uid) && ssi.Gid == uint32(gid) {
+ return nil
+ }
+ return os.Chown(p, uid, gid)
+}
+
+// LoadIdentityMapping takes a requested username and
+// using the data from /etc/sub{uid,gid} ranges, creates the
+// proper uid and gid remapping ranges for that user/group pair
+func LoadIdentityMapping(name string) (IdentityMapping, error) {
+ // TODO: Consider adding support for calling out to "getent"
+ usr, err := LookupUser(name)
+ if err != nil {
+ return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %w", name, err)
+ }
+
+ subuidRanges, err := lookupSubRangesFile("/etc/subuid", usr)
+ if err != nil {
+ return IdentityMapping{}, err
+ }
+ subgidRanges, err := lookupSubRangesFile("/etc/subgid", usr)
+ if err != nil {
+ return IdentityMapping{}, err
+ }
+
+ return IdentityMapping{
+ UIDMaps: subuidRanges,
+ GIDMaps: subgidRanges,
+ }, nil
+}
+
+func lookupSubRangesFile(path string, usr User) ([]IDMap, error) {
+ uidstr := strconv.Itoa(usr.Uid)
+ rangeList, err := ParseSubIDFileFilter(path, func(sid SubID) bool {
+ return sid.Name == usr.Name || sid.Name == uidstr
+ })
+ if err != nil {
+ return nil, err
+ }
+ if len(rangeList) == 0 {
+ return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name)
+ }
+
+ idMap := []IDMap{}
+
+ var containerID int64
+ for _, idrange := range rangeList {
+ idMap = append(idMap, IDMap{
+ ID: containerID,
+ ParentID: idrange.SubID,
+ Count: idrange.Count,
+ })
+ containerID = containerID + idrange.Count
+ }
+ return idMap, nil
+}
diff --git a/vendor/github.com/moby/sys/user/idtools_windows.go b/vendor/github.com/moby/sys/user/idtools_windows.go
new file mode 100644
index 0000000..9de730c
--- /dev/null
+++ b/vendor/github.com/moby/sys/user/idtools_windows.go
@@ -0,0 +1,13 @@
+package user
+
+import (
+ "os"
+)
+
+// This is currently a wrapper around [os.MkdirAll] since currently
+// permissions aren't set through this path, the identity isn't utilized.
+// Ownership is handled elsewhere, but in the future could be support here
+// too.
+func mkdirAs(path string, _ os.FileMode, _, _ int, _, _ bool) error {
+ return os.MkdirAll(path, 0)
+}
diff --git a/vendor/github.com/moby/sys/user/user.go b/vendor/github.com/moby/sys/user/user.go
index 984466d..198c493 100644
--- a/vendor/github.com/moby/sys/user/user.go
+++ b/vendor/github.com/moby/sys/user/user.go
@@ -197,7 +197,6 @@ func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) {
for {
var line []byte
line, isPrefix, err = rd.ReadLine()
-
if err != nil {
// We should return no error if EOF is reached
// without a match.
diff --git a/vendor/github.com/moby/term/term_unix.go b/vendor/github.com/moby/term/term_unix.go
index 2ec7706..579ce55 100644
--- a/vendor/github.com/moby/term/term_unix.go
+++ b/vendor/github.com/moby/term/term_unix.go
@@ -81,7 +81,7 @@ func setRawTerminal(fd uintptr) (*State, error) {
return makeRaw(fd)
}
-func setRawTerminalOutput(fd uintptr) (*State, error) {
+func setRawTerminalOutput(uintptr) (*State, error) {
return nil, nil
}