Move archive to pkg/archive
Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
80
pkg/archive/compression/benchmark_test.go
Normal file
80
pkg/archive/compression/benchmark_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compression
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const benchmarkTestDataURL = "https://git.io/fADcl"
|
||||
|
||||
func BenchmarkDecompression(b *testing.B) {
|
||||
resp, err := http.Get(benchmarkTestDataURL)
|
||||
require.NoError(b, err)
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
require.NoError(b, err)
|
||||
resp.Body.Close()
|
||||
|
||||
const mib = 1024 * 1024
|
||||
sizes := []int{32, 64, 128, 256}
|
||||
|
||||
for _, sizeInMiB := range sizes {
|
||||
size := sizeInMiB * mib
|
||||
for len(data) < size {
|
||||
data = append(data, data...)
|
||||
}
|
||||
data = data[0:size]
|
||||
|
||||
gz := testCompress(b, data, Gzip)
|
||||
zstd := testCompress(b, data, Zstd)
|
||||
|
||||
b.Run(fmt.Sprintf("size=%dMiB", sizeInMiB), func(b *testing.B) {
|
||||
original := gzipPath
|
||||
defer func() {
|
||||
gzipPath = original
|
||||
}()
|
||||
|
||||
b.Run("zstd", func(b *testing.B) {
|
||||
testDecompress(b, zstd)
|
||||
})
|
||||
|
||||
gzipPath = ""
|
||||
b.Run("gzipPureGo", func(b *testing.B) {
|
||||
testDecompress(b, gz)
|
||||
})
|
||||
gzipPath, err = exec.LookPath("igzip")
|
||||
if err == nil {
|
||||
b.Run("igzip", func(b *testing.B) {
|
||||
testDecompress(b, gz)
|
||||
})
|
||||
}
|
||||
gzipPath, err = exec.LookPath("unpigz")
|
||||
if err == nil {
|
||||
b.Run("unpigz", func(b *testing.B) {
|
||||
testDecompress(b, gz)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
326
pkg/archive/compression/compression.go
Normal file
326
pkg/archive/compression/compression.go
Normal file
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compression
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
)
|
||||
|
||||
type (
|
||||
// Compression is the state represents if compressed or not.
|
||||
Compression int
|
||||
)
|
||||
|
||||
const (
|
||||
// Uncompressed represents the uncompressed.
|
||||
Uncompressed Compression = iota
|
||||
// Gzip is gzip compression algorithm.
|
||||
Gzip
|
||||
// Zstd is zstd compression algorithm.
|
||||
Zstd
|
||||
)
|
||||
|
||||
const (
|
||||
disablePigzEnv = "CONTAINERD_DISABLE_PIGZ"
|
||||
disableIgzipEnv = "CONTAINERD_DISABLE_IGZIP"
|
||||
)
|
||||
|
||||
var (
|
||||
initGzip sync.Once
|
||||
gzipPath string
|
||||
)
|
||||
|
||||
var (
|
||||
bufioReader32KPool = &sync.Pool{
|
||||
New: func() interface{} { return bufio.NewReaderSize(nil, 32*1024) },
|
||||
}
|
||||
)
|
||||
|
||||
// DecompressReadCloser include the stream after decompress and the compress method detected.
|
||||
type DecompressReadCloser interface {
|
||||
io.ReadCloser
|
||||
// GetCompression returns the compress method which is used before decompressing
|
||||
GetCompression() Compression
|
||||
}
|
||||
|
||||
type readCloserWrapper struct {
|
||||
io.Reader
|
||||
compression Compression
|
||||
closer func() error
|
||||
}
|
||||
|
||||
func (r *readCloserWrapper) Close() error {
|
||||
if r.closer != nil {
|
||||
return r.closer()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *readCloserWrapper) GetCompression() Compression {
|
||||
return r.compression
|
||||
}
|
||||
|
||||
type writeCloserWrapper struct {
|
||||
io.Writer
|
||||
closer func() error
|
||||
}
|
||||
|
||||
func (w *writeCloserWrapper) Close() error {
|
||||
if w.closer != nil {
|
||||
w.closer()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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) (n int, err error) {
|
||||
if r.buf == nil {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n, err = r.buf.Read(p)
|
||||
if err == io.EOF {
|
||||
r.buf.Reset(nil)
|
||||
bufioReader32KPool.Put(r.buf)
|
||||
r.buf = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *bufferedReader) Peek(n int) ([]byte, error) {
|
||||
if r.buf == nil {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return r.buf.Peek(n)
|
||||
}
|
||||
|
||||
const (
|
||||
zstdMagicSkippableStart = 0x184D2A50
|
||||
zstdMagicSkippableMask = 0xFFFFFFF0
|
||||
)
|
||||
|
||||
var (
|
||||
gzipMagic = []byte{0x1F, 0x8B, 0x08}
|
||||
zstdMagic = []byte{0x28, 0xb5, 0x2f, 0xfd}
|
||||
)
|
||||
|
||||
type matcher = func([]byte) bool
|
||||
|
||||
func magicNumberMatcher(m []byte) matcher {
|
||||
return func(source []byte) bool {
|
||||
return bytes.HasPrefix(source, m)
|
||||
}
|
||||
}
|
||||
|
||||
// zstdMatcher detects zstd compression algorithm.
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// DetectCompression detects the compression algorithm of the source.
|
||||
func DetectCompression(source []byte) Compression {
|
||||
for compression, fn := range map[Compression]matcher{
|
||||
Gzip: magicNumberMatcher(gzipMagic),
|
||||
Zstd: zstdMatcher(),
|
||||
} {
|
||||
if fn(source) {
|
||||
return compression
|
||||
}
|
||||
}
|
||||
return Uncompressed
|
||||
}
|
||||
|
||||
// DecompressStream decompresses the archive and returns a ReaderCloser with the decompressed archive.
|
||||
func DecompressStream(archive io.Reader) (DecompressReadCloser, error) {
|
||||
buf := newBufferedReader(archive)
|
||||
bs, err := buf.Peek(10)
|
||||
if err != nil && 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 docker/docker#18170
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch compression := DetectCompression(bs); compression {
|
||||
case Uncompressed:
|
||||
return &readCloserWrapper{
|
||||
Reader: buf,
|
||||
compression: compression,
|
||||
}, 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,
|
||||
compression: compression,
|
||||
closer: func() error {
|
||||
cancel()
|
||||
return gzReader.Close()
|
||||
},
|
||||
}, nil
|
||||
case Zstd:
|
||||
zstdReader, err := zstd.NewReader(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &readCloserWrapper{
|
||||
Reader: zstdReader,
|
||||
compression: compression,
|
||||
closer: func() error {
|
||||
zstdReader.Close()
|
||||
return nil
|
||||
},
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported compression format %s", (&compression).Extension())
|
||||
}
|
||||
}
|
||||
|
||||
// CompressStream compresses the dest with specified compression algorithm.
|
||||
func CompressStream(dest io.Writer, compression Compression) (io.WriteCloser, error) {
|
||||
switch compression {
|
||||
case Uncompressed:
|
||||
return &writeCloserWrapper{dest, nil}, nil
|
||||
case Gzip:
|
||||
return gzip.NewWriter(dest), nil
|
||||
case Zstd:
|
||||
return zstd.NewWriter(dest)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported compression format %s", (&compression).Extension())
|
||||
}
|
||||
}
|
||||
|
||||
// Extension returns the extension of a file that uses the specified compression algorithm.
|
||||
func (compression *Compression) Extension() string {
|
||||
switch *compression {
|
||||
case Gzip:
|
||||
return "gz"
|
||||
case Zstd:
|
||||
return "zst"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func gzipDecompress(ctx context.Context, buf io.Reader) (io.ReadCloser, error) {
|
||||
initGzip.Do(func() {
|
||||
if gzipPath = detectCommand("igzip", disableIgzipEnv); gzipPath != "" {
|
||||
log.L.Debug("using igzip for decompression")
|
||||
return
|
||||
}
|
||||
if gzipPath = detectCommand("unpigz", disablePigzEnv); gzipPath != "" {
|
||||
log.L.Debug("using unpigz for decompression")
|
||||
}
|
||||
})
|
||||
|
||||
if gzipPath == "" {
|
||||
return gzip.NewReader(buf)
|
||||
}
|
||||
return cmdStream(exec.CommandContext(ctx, gzipPath, "-d", "-c"), buf)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
writer.CloseWithError(fmt.Errorf("%s: %s", err, errBuf.String()))
|
||||
} else {
|
||||
writer.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func detectCommand(path, disableEnvName string) string {
|
||||
// Check if this command is disabled via the env variable
|
||||
value := os.Getenv(disableEnvName)
|
||||
if value != "" {
|
||||
disable, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
log.L.WithError(err).Warnf("could not parse %s: %s", disableEnvName, value)
|
||||
}
|
||||
|
||||
if disable {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
path, err := exec.LookPath(path)
|
||||
if err != nil {
|
||||
log.L.WithError(err).Debugf("%s not found", path)
|
||||
return ""
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
28
pkg/archive/compression/compression_fuzzer.go
Normal file
28
pkg/archive/compression/compression_fuzzer.go
Normal file
@@ -0,0 +1,28 @@
|
||||
//go:build gofuzz
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compression
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
func FuzzDecompressStream(data []byte) int {
|
||||
_, _ = DecompressStream(bytes.NewReader(data))
|
||||
return 1
|
||||
}
|
||||
228
pkg/archive/compression/compression_test.go
Normal file
228
pkg/archive/compression/compression_test.go
Normal file
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compression
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Force initPigz to be called, so tests start with the same initial state
|
||||
gzipDecompress(context.Background(), strings.NewReader(""))
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// generateData generates data that composed of 2 random parts
|
||||
// and single zero-filled part within them.
|
||||
// Typically, the compression ratio would be about 67%.
|
||||
func generateData(t testing.TB, size int) []byte {
|
||||
part0 := size / 3 // random
|
||||
part2 := size / 3 // random
|
||||
part1 := size - part0 - part2 // zero-filled
|
||||
part0Data := make([]byte, part0)
|
||||
if _, err := rand.Read(part0Data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
part1Data := make([]byte, part1)
|
||||
part2Data := make([]byte, part2)
|
||||
if _, err := rand.Read(part2Data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return append(part0Data, append(part1Data, part2Data...)...)
|
||||
}
|
||||
|
||||
func testCompress(t testing.TB, orig []byte, compression Compression) []byte {
|
||||
size := len(orig)
|
||||
var b bytes.Buffer
|
||||
compressor, err := CompressStream(&b, compression)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, err := compressor.Write(orig); err != nil || n != size {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compressor.Close()
|
||||
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func testDecompress(t testing.TB, compressed []byte) ([]byte, DecompressReadCloser) {
|
||||
decompressor, err := DecompressStream(bytes.NewReader(compressed))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
decompressed, err := io.ReadAll(decompressor)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return decompressed, decompressor
|
||||
}
|
||||
|
||||
func testCompressDecompress(t testing.TB, size int, compression Compression) DecompressReadCloser {
|
||||
orig := generateData(t, size)
|
||||
compressed := testCompress(t, orig, compression)
|
||||
t.Logf("compressed %d bytes to %d bytes (%.2f%%)",
|
||||
len(orig), len(compressed), 100.0*float32(len(compressed))/float32(len(orig)))
|
||||
if compared := bytes.Compare(orig, compressed); (compression == Uncompressed && compared != 0) ||
|
||||
(compression != Uncompressed && compared == 0) {
|
||||
t.Fatal("strange compressed data")
|
||||
}
|
||||
|
||||
decompressed, decompressor := testDecompress(t, compressed)
|
||||
if !bytes.Equal(orig, decompressed) {
|
||||
t.Fatal("strange decompressed data")
|
||||
}
|
||||
|
||||
return decompressor
|
||||
}
|
||||
|
||||
func TestCompressDecompressGzip(t *testing.T) {
|
||||
oldUnpigzPath := gzipPath
|
||||
gzipPath = ""
|
||||
defer func() { gzipPath = oldUnpigzPath }()
|
||||
|
||||
decompressor := testCompressDecompress(t, 1024*1024, Gzip)
|
||||
wrapper := decompressor.(*readCloserWrapper)
|
||||
_, ok := wrapper.Reader.(*gzip.Reader)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected compressor type: %T", wrapper.Reader)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompressDecompressPigz(t *testing.T) {
|
||||
if _, err := exec.LookPath("unpigz"); err != nil {
|
||||
t.Skip("pigz not installed")
|
||||
}
|
||||
|
||||
decompressor := testCompressDecompress(t, 1024*1024, Gzip)
|
||||
wrapper := decompressor.(*readCloserWrapper)
|
||||
_, ok := wrapper.Reader.(*io.PipeReader)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected compressor type: %T", wrapper.Reader)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompressDecompressUncompressed(t *testing.T) {
|
||||
testCompressDecompress(t, 1024*1024, Uncompressed)
|
||||
}
|
||||
|
||||
func TestDetectPigz(t *testing.T) {
|
||||
// Create fake PATH with unpigz executable, make sure detectPigz can find it
|
||||
tempPath := t.TempDir()
|
||||
|
||||
filename := "unpigz"
|
||||
if runtime.GOOS == "windows" {
|
||||
filename = "unpigz.exe"
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(tempPath, filename)
|
||||
|
||||
if err := os.WriteFile(fullPath, []byte(""), 0111); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Setenv("PATH", tempPath)
|
||||
|
||||
if pigzPath := detectCommand("unpigz", disablePigzEnv); pigzPath == "" {
|
||||
t.Fatal("failed to detect pigz path")
|
||||
} else if pigzPath != fullPath {
|
||||
t.Fatalf("wrong pigz found: %s != %s", pigzPath, fullPath)
|
||||
}
|
||||
|
||||
t.Setenv(disablePigzEnv, "1")
|
||||
|
||||
if pigzPath := detectCommand("unpigz", disablePigzEnv); pigzPath != "" {
|
||||
t.Fatalf("disable via %s doesn't work", disablePigzEnv)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdStream(t *testing.T) {
|
||||
out, err := cmdStream(exec.Command("sh", "-c", "echo hello; exit 0"), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf, err := io.ReadAll(out)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read from stdout: %s", err)
|
||||
}
|
||||
|
||||
if string(buf) != "hello\n" {
|
||||
t.Fatalf("unexpected command output ('%s' != '%s')", string(buf), "hello\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdStreamBad(t *testing.T) {
|
||||
out, err := cmdStream(exec.Command("sh", "-c", "echo hello; echo >&2 bad result; exit 1"), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to start command: %v", err)
|
||||
}
|
||||
|
||||
if buf, err := io.ReadAll(out); err == nil {
|
||||
t.Fatal("command should have failed")
|
||||
} else if err.Error() != "exit status 1: bad result\n" {
|
||||
t.Fatalf("wrong error: %s", err.Error())
|
||||
} else if string(buf) != "hello\n" {
|
||||
t.Fatalf("wrong output: %s", string(buf))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectCompressionZstd(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
source []byte
|
||||
expected Compression
|
||||
}{
|
||||
{
|
||||
// test zstd compression without skippable frames.
|
||||
source: []byte{
|
||||
0x28, 0xb5, 0x2f, 0xfd, // magic number of Zstandard frame: 0xFD2FB528
|
||||
0x04, 0x00, 0x31, 0x00, 0x00, // frame header
|
||||
0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, // data block "docker"
|
||||
0x16, 0x0e, 0x21, 0xc3, // content checksum
|
||||
},
|
||||
expected: Zstd,
|
||||
},
|
||||
{
|
||||
// test zstd compression with skippable frames.
|
||||
source: []byte{
|
||||
0x50, 0x2a, 0x4d, 0x18, // magic number of skippable frame: 0x184D2A50 to 0x184D2A5F
|
||||
0x04, 0x00, 0x00, 0x00, // frame size
|
||||
0x5d, 0x00, 0x00, 0x00, // user data
|
||||
0x28, 0xb5, 0x2f, 0xfd, // magic number of Zstandard frame: 0xFD2FB528
|
||||
0x04, 0x00, 0x31, 0x00, 0x00, // frame header
|
||||
0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, // data block "docker"
|
||||
0x16, 0x0e, 0x21, 0xc3, // content checksum
|
||||
},
|
||||
expected: Zstd,
|
||||
},
|
||||
} {
|
||||
compression := DetectCompression(tc.source)
|
||||
if compression != tc.expected {
|
||||
t.Fatalf("Unexpected compression %v, expected %v", compression, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
58
pkg/archive/issues_test.go
Normal file
58
pkg/archive/issues_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/archive/compression"
|
||||
"github.com/containerd/containerd/v2/pkg/testutil"
|
||||
)
|
||||
|
||||
// TestPrefixHeaderReadable tests that files that could be created with the
|
||||
// version of this package that was built with <=go17 are still readable.
|
||||
func TestPrefixHeaderReadable(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
// https://gist.github.com/stevvooe/e2a790ad4e97425896206c0816e1a882#file-out-go
|
||||
var testFile = []byte("\x1f\x8b\x08\x08\x44\x21\x68\x59\x00\x03\x74\x2e\x74\x61\x72\x00\x4b\xcb\xcf\x67\xa0\x35\x30\x80\x00\x86\x06\x10\x47\x01\xc1\x37\x40\x00\x54\xb6\xb1\xa1\xa9\x99\x09\x48\x25\x1d\x40\x69\x71\x49\x62\x91\x02\xe5\x76\xa1\x79\x84\x21\x91\xd6\x80\x72\xaf\x8f\x82\x51\x30\x0a\x46\x36\x00\x00\xf0\x1c\x1e\x95\x00\x06\x00\x00")
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
r, err := compression.DecompressStream(bytes.NewReader(testFile))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
_, err = Apply(context.Background(), tmpDir, r, WithNoSameOwner())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
baseName := "foo"
|
||||
pth := strings.Repeat("a", 100-len(baseName)) + "/" + baseName
|
||||
|
||||
_, err = os.Lstat(filepath.Join(tmpDir, pth))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
25
pkg/archive/link_default.go
Normal file
25
pkg/archive/link_default.go
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build !freebsd
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import "os"
|
||||
|
||||
func link(oldname, newname string) error {
|
||||
return os.Link(oldname, newname)
|
||||
}
|
||||
82
pkg/archive/link_freebsd.go
Normal file
82
pkg/archive/link_freebsd.go
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func link(oldname, newname string) error {
|
||||
e := ignoringEINTR(func() error {
|
||||
return unix.Linkat(unix.AT_FDCWD, oldname, unix.AT_FDCWD, newname, 0)
|
||||
})
|
||||
if e != nil {
|
||||
return &os.LinkError{Op: "link", Old: oldname, New: newname, Err: e}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// The following contents were copied from Go 1.18.2.
|
||||
// Use of this source code is governed by the following
|
||||
// BSD-style license:
|
||||
//
|
||||
// Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
// ignoringEINTR makes a function call and repeats it if it returns an
|
||||
// EINTR error. This appears to be required even though we install all
|
||||
// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.
|
||||
// Also #20400 and #36644 are issues in which a signal handler is
|
||||
// installed without setting SA_RESTART. None of these are the common case,
|
||||
// but there are enough of them that it seems that we can't avoid
|
||||
// an EINTR loop.
|
||||
func ignoringEINTR(fn func() error) error {
|
||||
for {
|
||||
err := fn()
|
||||
if err != syscall.EINTR {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
816
pkg/archive/tar.go
Normal file
816
pkg/archive/tar.go
Normal file
@@ -0,0 +1,816 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/archive/tarheader"
|
||||
"github.com/containerd/containerd/v2/pkg/epoch"
|
||||
"github.com/containerd/containerd/v2/pkg/userns"
|
||||
"github.com/containerd/continuity/fs"
|
||||
"github.com/containerd/log"
|
||||
)
|
||||
|
||||
var bufPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
buffer := make([]byte, 32*1024)
|
||||
return &buffer
|
||||
},
|
||||
}
|
||||
|
||||
var errInvalidArchive = errors.New("invalid archive")
|
||||
|
||||
// Diff returns a tar stream of the computed filesystem
|
||||
// difference between the provided directories.
|
||||
//
|
||||
// Produces a tar using OCI style file markers for deletions. Deleted
|
||||
// files will be prepended with the prefix ".wh.". This style is
|
||||
// based off AUFS whiteouts.
|
||||
// See https://github.com/opencontainers/image-spec/blob/main/layer.md
|
||||
func Diff(ctx context.Context, a, b string, opts ...WriteDiffOpt) io.ReadCloser {
|
||||
r, w := io.Pipe()
|
||||
|
||||
go func() {
|
||||
err := WriteDiff(ctx, w, a, b, opts...)
|
||||
if err != nil {
|
||||
log.G(ctx).WithError(err).Debugf("write diff failed")
|
||||
}
|
||||
if err = w.CloseWithError(err); err != nil {
|
||||
log.G(ctx).WithError(err).Debugf("closing tar pipe failed")
|
||||
}
|
||||
}()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// WriteDiff writes a tar stream of the computed difference between the
|
||||
// provided paths.
|
||||
//
|
||||
// Produces a tar using OCI style file markers for deletions. Deleted
|
||||
// files will be prepended with the prefix ".wh.". This style is
|
||||
// based off AUFS whiteouts.
|
||||
// See https://github.com/opencontainers/image-spec/blob/main/layer.md
|
||||
func WriteDiff(ctx context.Context, w io.Writer, a, b string, opts ...WriteDiffOpt) error {
|
||||
var options WriteDiffOptions
|
||||
for _, opt := range opts {
|
||||
if err := opt(&options); err != nil {
|
||||
return fmt.Errorf("failed to apply option: %w", err)
|
||||
}
|
||||
}
|
||||
if tm := epoch.FromContext(ctx); tm != nil && options.SourceDateEpoch == nil {
|
||||
options.SourceDateEpoch = tm
|
||||
}
|
||||
|
||||
if options.writeDiffFunc == nil {
|
||||
options.writeDiffFunc = writeDiffNaive
|
||||
}
|
||||
|
||||
return options.writeDiffFunc(ctx, w, a, b, options)
|
||||
}
|
||||
|
||||
// writeDiffNaive writes a tar stream of the computed difference between the
|
||||
// provided directories on disk.
|
||||
//
|
||||
// Produces a tar using OCI style file markers for deletions. Deleted
|
||||
// files will be prepended with the prefix ".wh.". This style is
|
||||
// based off AUFS whiteouts.
|
||||
// See https://github.com/opencontainers/image-spec/blob/main/layer.md
|
||||
func writeDiffNaive(ctx context.Context, w io.Writer, a, b string, o WriteDiffOptions) error {
|
||||
var opts []ChangeWriterOpt
|
||||
if o.SourceDateEpoch != nil {
|
||||
opts = append(opts, WithModTimeUpperBound(*o.SourceDateEpoch))
|
||||
// Since containerd v2.0, the whiteout timestamps are set to zero (1970-01-01),
|
||||
// not to the source date epoch
|
||||
}
|
||||
cw := NewChangeWriter(w, b, opts...)
|
||||
err := fs.Changes(ctx, a, b, cw.HandleChange)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create diff tar stream: %w", err)
|
||||
}
|
||||
return cw.Close()
|
||||
}
|
||||
|
||||
const (
|
||||
// whiteoutPrefix prefix means file is a whiteout. If this is followed by a
|
||||
// filename this means that file has been removed from the base layer.
|
||||
// See https://github.com/opencontainers/image-spec/blob/main/layer.md#whiteouts
|
||||
whiteoutPrefix = ".wh."
|
||||
|
||||
// whiteoutMetaPrefix prefix means whiteout has a special meaning and is not
|
||||
// for removing an actual file. Normally these files are excluded from exported
|
||||
// archives.
|
||||
whiteoutMetaPrefix = whiteoutPrefix + whiteoutPrefix
|
||||
|
||||
// whiteoutOpaqueDir file means directory has been made opaque - meaning
|
||||
// readdir calls to this directory do not follow to lower layers.
|
||||
whiteoutOpaqueDir = whiteoutMetaPrefix + ".opq"
|
||||
|
||||
paxSchilyXattr = "SCHILY.xattr."
|
||||
|
||||
userXattrPrefix = "user."
|
||||
)
|
||||
|
||||
// Apply applies a tar stream of an OCI style diff tar.
|
||||
// See https://github.com/opencontainers/image-spec/blob/main/layer.md#applying-changesets
|
||||
func Apply(ctx context.Context, root string, r io.Reader, opts ...ApplyOpt) (int64, error) {
|
||||
root = filepath.Clean(root)
|
||||
|
||||
var options ApplyOptions
|
||||
for _, opt := range opts {
|
||||
if err := opt(&options); err != nil {
|
||||
return 0, fmt.Errorf("failed to apply option: %w", err)
|
||||
}
|
||||
}
|
||||
if options.Filter == nil {
|
||||
options.Filter = all
|
||||
}
|
||||
if options.applyFunc == nil {
|
||||
options.applyFunc = applyNaive
|
||||
}
|
||||
|
||||
return options.applyFunc(ctx, root, r, options)
|
||||
}
|
||||
|
||||
// applyNaive applies a tar stream of an OCI style diff tar to a directory
|
||||
// applying each file as either a whole file or whiteout.
|
||||
// See https://github.com/opencontainers/image-spec/blob/main/layer.md#applying-changesets
|
||||
func applyNaive(ctx context.Context, root string, r io.Reader, options ApplyOptions) (size int64, err error) {
|
||||
var (
|
||||
dirs []*tar.Header
|
||||
|
||||
tr = tar.NewReader(r)
|
||||
|
||||
// Used for handling opaque directory markers which
|
||||
// may occur out of order
|
||||
unpackedPaths = make(map[string]struct{})
|
||||
|
||||
convertWhiteout = options.ConvertWhiteout
|
||||
)
|
||||
|
||||
if convertWhiteout == nil {
|
||||
// handle whiteouts by removing the target files
|
||||
convertWhiteout = func(hdr *tar.Header, path string) (bool, error) {
|
||||
base := filepath.Base(path)
|
||||
dir := filepath.Dir(path)
|
||||
if base == whiteoutOpaqueDir {
|
||||
_, err := os.Lstat(dir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = nil // parent was deleted
|
||||
}
|
||||
return err
|
||||
}
|
||||
if path == dir {
|
||||
return nil
|
||||
}
|
||||
if _, exists := unpackedPaths[path]; !exists {
|
||||
err := os.RemoveAll(path)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return false, err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(base, whiteoutPrefix) {
|
||||
originalBase := base[len(whiteoutPrefix):]
|
||||
originalPath := filepath.Join(dir, originalBase)
|
||||
|
||||
return false, os.RemoveAll(originalPath)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through the files in the archive.
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
// end of tar archive
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
size += hdr.Size
|
||||
|
||||
// Normalize name, for safety and for a simple is-root check
|
||||
hdr.Name = filepath.Clean(hdr.Name)
|
||||
|
||||
accept, err := options.Filter(hdr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !accept {
|
||||
continue
|
||||
}
|
||||
|
||||
if skipFile(hdr) {
|
||||
log.G(ctx).Warnf("file %q ignored: archive may not be supported on system", hdr.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Split name and resolve symlinks for root directory.
|
||||
ppath, base := filepath.Split(hdr.Name)
|
||||
ppath, err = fs.RootPath(root, ppath)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get root path: %w", err)
|
||||
}
|
||||
|
||||
// Join to root before joining to parent path to ensure relative links are
|
||||
// already resolved based on the root before adding to parent.
|
||||
path := filepath.Join(ppath, filepath.Join("/", base))
|
||||
if path == root {
|
||||
log.G(ctx).Debugf("file %q ignored: resolved to root", hdr.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// If file is not directly under root, ensure parent directory
|
||||
// exists or is created.
|
||||
if ppath != root {
|
||||
parentPath := ppath
|
||||
if base == "" {
|
||||
parentPath = filepath.Dir(path)
|
||||
}
|
||||
if err := mkparent(ctx, parentPath, root, options.Parents); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// Naive whiteout convert function which handles whiteout files by
|
||||
// removing the target files.
|
||||
if err := validateWhiteout(path); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
writeFile, err := convertWhiteout(hdr, path)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to convert whiteout file %q: %w", hdr.Name, err)
|
||||
}
|
||||
if !writeFile {
|
||||
continue
|
||||
}
|
||||
// If path exits we almost always just want to remove and replace it.
|
||||
// The only exception is when it is a directory *and* the file from
|
||||
// the layer is also a directory. Then we want to merge them (i.e.
|
||||
// just apply the metadata from the layer).
|
||||
if fi, err := os.Lstat(path); err == nil {
|
||||
if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) {
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
srcData := io.Reader(tr)
|
||||
srcHdr := hdr
|
||||
|
||||
if err := createTarFile(ctx, path, root, srcHdr, srcData, options.NoSameOwner); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Directory mtimes must be handled at the end to avoid further
|
||||
// file creation in them to modify the directory mtime
|
||||
if hdr.Typeflag == tar.TypeDir {
|
||||
dirs = append(dirs, hdr)
|
||||
}
|
||||
unpackedPaths[path] = struct{}{}
|
||||
}
|
||||
|
||||
for _, hdr := range dirs {
|
||||
path, err := fs.RootPath(root, hdr.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func createTarFile(ctx context.Context, path, extractDir string, hdr *tar.Header, reader io.Reader, noSameOwner bool) error {
|
||||
// hdr.Mode is in linux format, which we can use for syscalls,
|
||||
// but for os.Foo() calls we need the mode converted to os.FileMode,
|
||||
// so use hdrInfo.Mode() (they differ for e.g. setuid bits)
|
||||
hdrInfo := hdr.FileInfo()
|
||||
|
||||
switch hdr.Typeflag {
|
||||
case tar.TypeDir:
|
||||
// Create directory unless it exists as a directory already.
|
||||
// In that case we just want to merge the two
|
||||
if fi, err := os.Lstat(path); !(err == nil && fi.IsDir()) {
|
||||
if err := mkdir(path, hdrInfo.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:staticcheck // TypeRegA is deprecated but we may still receive an external tar with TypeRegA
|
||||
case tar.TypeReg, tar.TypeRegA:
|
||||
file, err := openFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, hdrInfo.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = copyBuffered(ctx, file, reader)
|
||||
if err1 := file.Close(); err == nil {
|
||||
err = err1
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case tar.TypeBlock, tar.TypeChar:
|
||||
// Handle this is an OS-specific way
|
||||
if err := handleTarTypeBlockCharFifo(hdr, path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case tar.TypeFifo:
|
||||
// Handle this is an OS-specific way
|
||||
if err := handleTarTypeBlockCharFifo(hdr, path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case tar.TypeLink:
|
||||
targetPath, err := hardlinkRootPath(extractDir, hdr.Linkname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := link(targetPath, path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case tar.TypeSymlink:
|
||||
if err := os.Symlink(hdr.Linkname, path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case tar.TypeXGlobalHeader:
|
||||
log.G(ctx).Debug("PAX Global Extended Headers found and ignored")
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unhandled tar header type %d", hdr.Typeflag)
|
||||
}
|
||||
|
||||
if !noSameOwner {
|
||||
if err := os.Lchown(path, hdr.Uid, hdr.Gid); err != nil {
|
||||
err = fmt.Errorf("failed to Lchown %q for UID %d, GID %d: %w", path, hdr.Uid, hdr.Gid, err)
|
||||
if errors.Is(err, syscall.EINVAL) && userns.RunningInUserNS() {
|
||||
err = fmt.Errorf("%w (Hint: try increasing the number of subordinate IDs in /etc/subuid and /etc/subgid)", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range hdr.PAXRecords {
|
||||
if strings.HasPrefix(key, paxSchilyXattr) {
|
||||
key = key[len(paxSchilyXattr):]
|
||||
if err := setxattr(path, key, value); err != nil {
|
||||
if errors.Is(err, syscall.EPERM) && strings.HasPrefix(key, userXattrPrefix) {
|
||||
// In the user.* namespace, only regular files and directories can have extended attributes.
|
||||
// See https://man7.org/linux/man-pages/man7/xattr.7.html for details.
|
||||
if fi, err := os.Lstat(path); err == nil && (!fi.Mode().IsRegular() && !fi.Mode().IsDir()) {
|
||||
log.G(ctx).WithError(err).Warnf("ignored xattr %s in archive", key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if errors.Is(err, syscall.ENOTSUP) {
|
||||
log.G(ctx).WithError(err).Warnf("ignored xattr %s in archive", key)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to setxattr %q for key %q: %w", path, key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// call lchmod after lchown since lchown can modify the file mode
|
||||
if err := lchmod(path, hdrInfo.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime))
|
||||
}
|
||||
|
||||
func mkparent(ctx context.Context, path, root string, parents []string) error {
|
||||
if dir, err := os.Lstat(path); err == nil {
|
||||
if dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return &os.PathError{
|
||||
Op: "mkparent",
|
||||
Path: path,
|
||||
Err: syscall.ENOTDIR,
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
i := len(path)
|
||||
for i > len(root) && !os.IsPathSeparator(path[i-1]) {
|
||||
i--
|
||||
}
|
||||
|
||||
if i > len(root)+1 {
|
||||
if err := mkparent(ctx, path[:i-1], root, parents); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := mkdir(path, 0755); err != nil {
|
||||
// Check that still doesn't exist
|
||||
dir, err1 := os.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range parents {
|
||||
ppath, err := fs.RootPath(p, path[len(root):])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dir, err := os.Lstat(ppath)
|
||||
if err == nil {
|
||||
if !dir.IsDir() {
|
||||
// Replaced, do not copy attributes
|
||||
break
|
||||
}
|
||||
if err := copyDirInfo(dir, path); err != nil {
|
||||
return err
|
||||
}
|
||||
return copyUpXAttrs(path, ppath)
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.G(ctx).Debugf("parent directory %q not found: default permissions(0755) used", path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangeWriter provides tar stream from filesystem change information.
|
||||
// The privided tar stream is styled as an OCI layer. Change information
|
||||
// (add/modify/delete/unmodified) for each file needs to be passed to this
|
||||
// writer through HandleChange method.
|
||||
//
|
||||
// This should be used combining with continuity's diff computing functionality
|
||||
// (e.g. `fs.Change` of github.com/containerd/continuity/fs).
|
||||
//
|
||||
// See also https://github.com/opencontainers/image-spec/blob/main/layer.md for details
|
||||
// about OCI layers
|
||||
type ChangeWriter struct {
|
||||
tw *tar.Writer
|
||||
source string
|
||||
modTimeUpperBound *time.Time
|
||||
inodeSrc map[uint64]string
|
||||
inodeRefs map[uint64][]string
|
||||
addedDirs map[string]struct{}
|
||||
}
|
||||
|
||||
// ChangeWriterOpt can be specified in NewChangeWriter.
|
||||
type ChangeWriterOpt func(cw *ChangeWriter)
|
||||
|
||||
// WithModTimeUpperBound sets the mod time upper bound.
|
||||
func WithModTimeUpperBound(tm time.Time) ChangeWriterOpt {
|
||||
return func(cw *ChangeWriter) {
|
||||
cw.modTimeUpperBound = &tm
|
||||
}
|
||||
}
|
||||
|
||||
// NewChangeWriter returns ChangeWriter that writes tar stream of the source directory
|
||||
// to the privided writer. Change information (add/modify/delete/unmodified) for each
|
||||
// file needs to be passed through HandleChange method.
|
||||
func NewChangeWriter(w io.Writer, source string, opts ...ChangeWriterOpt) *ChangeWriter {
|
||||
cw := &ChangeWriter{
|
||||
tw: tar.NewWriter(w),
|
||||
source: source,
|
||||
inodeSrc: map[uint64]string{},
|
||||
inodeRefs: map[uint64][]string{},
|
||||
addedDirs: map[string]struct{}{},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(cw)
|
||||
}
|
||||
return cw
|
||||
}
|
||||
|
||||
// HandleChange receives filesystem change information and reflect that information to
|
||||
// the result tar stream. This function implements `fs.ChangeFunc` of continuity
|
||||
// (github.com/containerd/continuity/fs) and should be used with that package.
|
||||
func (cw *ChangeWriter) HandleChange(k fs.ChangeKind, p string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if k == fs.ChangeKindDelete {
|
||||
whiteOutDir := filepath.Dir(p)
|
||||
whiteOutBase := filepath.Base(p)
|
||||
whiteOut := filepath.Join(whiteOutDir, whiteoutPrefix+whiteOutBase)
|
||||
// Since containerd v2.0, the whiteout timestamps are set to zero (1970-01-01),
|
||||
// not to the source date epoch.
|
||||
whiteOutT := time.Unix(0, 0).UTC()
|
||||
hdr := &tar.Header{
|
||||
Typeflag: tar.TypeReg,
|
||||
Name: whiteOut[1:],
|
||||
Size: 0,
|
||||
ModTime: whiteOutT,
|
||||
AccessTime: whiteOutT,
|
||||
ChangeTime: whiteOutT,
|
||||
}
|
||||
if err := cw.includeParents(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cw.tw.WriteHeader(hdr); err != nil {
|
||||
return fmt.Errorf("failed to write whiteout header: %w", err)
|
||||
}
|
||||
} else {
|
||||
var (
|
||||
link string
|
||||
err error
|
||||
source = filepath.Join(cw.source, p)
|
||||
)
|
||||
|
||||
switch {
|
||||
case f.Mode()&os.ModeSocket != 0:
|
||||
return nil // ignore sockets
|
||||
case f.Mode()&os.ModeSymlink != 0:
|
||||
if link, err = os.Readlink(source); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Use FileInfoHeaderNoLookups to avoid propagating user names and group names from the host
|
||||
hdr, err := tarheader.FileInfoHeaderNoLookups(f, link)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode)))
|
||||
|
||||
// truncate timestamp for compatibility. without PAX stdlib rounds timestamps instead
|
||||
hdr.Format = tar.FormatPAX
|
||||
if cw.modTimeUpperBound != nil && hdr.ModTime.After(*cw.modTimeUpperBound) {
|
||||
hdr.ModTime = *cw.modTimeUpperBound
|
||||
}
|
||||
hdr.ModTime = hdr.ModTime.Truncate(time.Second)
|
||||
hdr.AccessTime = time.Time{}
|
||||
hdr.ChangeTime = time.Time{}
|
||||
|
||||
name := p
|
||||
if strings.HasPrefix(name, string(filepath.Separator)) {
|
||||
name, err = filepath.Rel(string(filepath.Separator), name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make path relative: %w", err)
|
||||
}
|
||||
}
|
||||
// Canonicalize to POSIX-style paths using forward slashes. Directory
|
||||
// entries must end with a slash.
|
||||
name = filepath.ToSlash(name)
|
||||
if f.IsDir() && !strings.HasSuffix(name, "/") {
|
||||
name += "/"
|
||||
}
|
||||
hdr.Name = name
|
||||
|
||||
if err := setHeaderForSpecialDevice(hdr, name, f); err != nil {
|
||||
return fmt.Errorf("failed to set device headers: %w", err)
|
||||
}
|
||||
|
||||
// additionalLinks stores file names which must be linked to
|
||||
// this file when this file is added
|
||||
var additionalLinks []string
|
||||
inode, isHardlink := fs.GetLinkInfo(f)
|
||||
if isHardlink {
|
||||
// If the inode has a source, always link to it
|
||||
if source, ok := cw.inodeSrc[inode]; ok {
|
||||
hdr.Typeflag = tar.TypeLink
|
||||
hdr.Linkname = source
|
||||
hdr.Size = 0
|
||||
} else {
|
||||
if k == fs.ChangeKindUnmodified {
|
||||
cw.inodeRefs[inode] = append(cw.inodeRefs[inode], name)
|
||||
return nil
|
||||
}
|
||||
cw.inodeSrc[inode] = name
|
||||
additionalLinks = cw.inodeRefs[inode]
|
||||
delete(cw.inodeRefs, inode)
|
||||
}
|
||||
} else if k == fs.ChangeKindUnmodified {
|
||||
// Nothing to write to diff
|
||||
return nil
|
||||
}
|
||||
|
||||
if capability, err := getxattr(source, "security.capability"); err != nil {
|
||||
return fmt.Errorf("failed to get capabilities xattr: %w", err)
|
||||
} else if len(capability) > 0 {
|
||||
if hdr.PAXRecords == nil {
|
||||
hdr.PAXRecords = map[string]string{}
|
||||
}
|
||||
hdr.PAXRecords[paxSchilyXattr+"security.capability"] = string(capability)
|
||||
}
|
||||
|
||||
if err := cw.includeParents(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cw.tw.WriteHeader(hdr); err != nil {
|
||||
return fmt.Errorf("failed to write file header: %w", err)
|
||||
}
|
||||
|
||||
if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 {
|
||||
file, err := open(source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open path: %v: %w", source, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
n, err := copyBuffered(context.TODO(), cw.tw, file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy: %w", err)
|
||||
}
|
||||
if n != hdr.Size {
|
||||
return errors.New("short write copying file")
|
||||
}
|
||||
}
|
||||
|
||||
if additionalLinks != nil {
|
||||
source = hdr.Name
|
||||
for _, extra := range additionalLinks {
|
||||
hdr.Name = extra
|
||||
hdr.Typeflag = tar.TypeLink
|
||||
hdr.Linkname = source
|
||||
hdr.Size = 0
|
||||
|
||||
if err := cw.includeParents(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cw.tw.WriteHeader(hdr); err != nil {
|
||||
return fmt.Errorf("failed to write file header: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes this writer.
|
||||
func (cw *ChangeWriter) Close() error {
|
||||
if err := cw.tw.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close tar writer: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cw *ChangeWriter) includeParents(hdr *tar.Header) error {
|
||||
if cw.addedDirs == nil {
|
||||
return nil
|
||||
}
|
||||
name := strings.TrimRight(hdr.Name, "/")
|
||||
fname := filepath.Join(cw.source, name)
|
||||
parent := filepath.Dir(name)
|
||||
pname := filepath.Join(cw.source, parent)
|
||||
|
||||
// Do not include root directory as parent
|
||||
if fname != cw.source && pname != cw.source {
|
||||
_, ok := cw.addedDirs[parent]
|
||||
if !ok {
|
||||
cw.addedDirs[parent] = struct{}{}
|
||||
fi, err := os.Stat(pname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cw.HandleChange(fs.ChangeKindModify, parent, fi, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if hdr.Typeflag == tar.TypeDir {
|
||||
cw.addedDirs[name] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyBuffered(ctx context.Context, dst io.Writer, src io.Reader) (written int64, err error) {
|
||||
buf := bufPool.Get().(*[]byte)
|
||||
defer bufPool.Put(buf)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
nr, er := src.Read(*buf)
|
||||
if nr > 0 {
|
||||
nw, ew := dst.Write((*buf)[0:nr])
|
||||
if nw > 0 {
|
||||
written += int64(nw)
|
||||
}
|
||||
if ew != nil {
|
||||
err = ew
|
||||
break
|
||||
}
|
||||
if nr != nw {
|
||||
err = io.ErrShortWrite
|
||||
break
|
||||
}
|
||||
}
|
||||
if er != nil {
|
||||
if er != io.EOF {
|
||||
err = er
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return written, err
|
||||
|
||||
}
|
||||
|
||||
// hardlinkRootPath returns target linkname, evaluating and bounding any
|
||||
// symlink to the parent directory.
|
||||
//
|
||||
// NOTE: Allow hardlink to the softlink, not the real one. For example,
|
||||
//
|
||||
// touch /tmp/zzz
|
||||
// ln -s /tmp/zzz /tmp/xxx
|
||||
// ln /tmp/xxx /tmp/yyy
|
||||
//
|
||||
// /tmp/yyy should be softlink which be same of /tmp/xxx, not /tmp/zzz.
|
||||
func hardlinkRootPath(root, linkname string) (string, error) {
|
||||
ppath, base := filepath.Split(linkname)
|
||||
ppath, err := fs.RootPath(root, ppath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(ppath, base)
|
||||
if !strings.HasPrefix(targetPath, root) {
|
||||
targetPath = root
|
||||
}
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
func validateWhiteout(path string) error {
|
||||
base := filepath.Base(path)
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
if base == whiteoutOpaqueDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(base, whiteoutPrefix) {
|
||||
originalBase := base[len(whiteoutPrefix):]
|
||||
originalPath := filepath.Join(dir, originalBase)
|
||||
|
||||
// Ensure originalPath is under dir
|
||||
if dir[len(dir)-1] != filepath.Separator {
|
||||
dir += string(filepath.Separator)
|
||||
}
|
||||
if !strings.HasPrefix(originalPath, dir) {
|
||||
return fmt.Errorf("invalid whiteout name: %v: %w", base, errInvalidArchive)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
46
pkg/archive/tar_freebsd.go
Normal file
46
pkg/archive/tar_freebsd.go
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// mknod wraps unix.Mknod. FreeBSD's unix.Mknod signature is different from
|
||||
// other Unix and Unix-like operating systems.
|
||||
func mknod(path string, mode uint32, dev uint64) error {
|
||||
return unix.Mknod(path, mode, dev)
|
||||
}
|
||||
|
||||
// lsetxattrCreate wraps unix.Lsetxattr with FreeBSD-specific flags and errors
|
||||
func lsetxattrCreate(link string, attr string, data []byte) error {
|
||||
err := unix.Lsetxattr(link, attr, data, 0)
|
||||
if err == unix.ENOTSUP || err == unix.EEXIST {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func lchmod(path string, mode os.FileMode) error {
|
||||
err := unix.Fchmodat(unix.AT_FDCWD, path, uint32(mode), unix.AT_SYMLINK_NOFOLLOW)
|
||||
if err != nil {
|
||||
err = &os.PathError{Op: "lchmod", Path: path, Err: err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
171
pkg/archive/tar_linux_test.go
Normal file
171
pkg/archive/tar_linux_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/containerd/v2/pkg/testutil"
|
||||
"github.com/containerd/containerd/v2/plugins/snapshots/overlay/overlayutils"
|
||||
"github.com/containerd/continuity/fs"
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
"github.com/containerd/log/logtest"
|
||||
)
|
||||
|
||||
func TestOverlayApply(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
base := t.TempDir()
|
||||
|
||||
if err := overlayutils.Supported(base); err != nil {
|
||||
t.Skipf("skipping because overlay is not supported %v", err)
|
||||
}
|
||||
fstest.FSSuite(t, overlayDiffApplier{
|
||||
tmp: base,
|
||||
diff: WriteDiff,
|
||||
t: t,
|
||||
})
|
||||
}
|
||||
|
||||
func TestOverlayApplyNoParents(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
base := t.TempDir()
|
||||
|
||||
if err := overlayutils.Supported(base); err != nil {
|
||||
t.Skipf("skipping because overlay is not supported %v", err)
|
||||
}
|
||||
fstest.FSSuite(t, overlayDiffApplier{
|
||||
tmp: base,
|
||||
diff: func(ctx context.Context, w io.Writer, a, b string, _ ...WriteDiffOpt) error {
|
||||
cw := NewChangeWriter(w, b)
|
||||
cw.addedDirs = nil
|
||||
err := fs.Changes(ctx, a, b, cw.HandleChange)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create diff tar stream: %w", err)
|
||||
}
|
||||
return cw.Close()
|
||||
},
|
||||
t: t,
|
||||
})
|
||||
}
|
||||
|
||||
type overlayDiffApplier struct {
|
||||
tmp string
|
||||
diff func(context.Context, io.Writer, string, string, ...WriteDiffOpt) error
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
type overlayContext struct {
|
||||
merged string
|
||||
lowers []string
|
||||
mounted bool
|
||||
}
|
||||
|
||||
type contextKey struct{}
|
||||
|
||||
func (d overlayDiffApplier) TestContext(ctx context.Context) (context.Context, func(), error) {
|
||||
merged, err := os.MkdirTemp(d.tmp, "merged")
|
||||
if err != nil {
|
||||
return ctx, nil, fmt.Errorf("failed to make merged dir: %w", err)
|
||||
}
|
||||
|
||||
oc := &overlayContext{
|
||||
merged: merged,
|
||||
}
|
||||
|
||||
ctx = logtest.WithT(ctx, d.t)
|
||||
|
||||
return context.WithValue(ctx, contextKey{}, oc), func() {
|
||||
if oc.mounted {
|
||||
mount.Unmount(oc.merged, 0)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d overlayDiffApplier) Apply(ctx context.Context, a fstest.Applier) (string, func(), error) {
|
||||
oc := ctx.Value(contextKey{}).(*overlayContext)
|
||||
|
||||
applyCopy, err := os.MkdirTemp(d.tmp, "apply-copy-")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(applyCopy)
|
||||
|
||||
base := oc.merged
|
||||
if len(oc.lowers) == 1 {
|
||||
base = oc.lowers[0]
|
||||
}
|
||||
|
||||
if err = fs.CopyDir(applyCopy, base); err != nil {
|
||||
return "", nil, fmt.Errorf("failed to copy base: %w", err)
|
||||
}
|
||||
|
||||
if err := a.Apply(applyCopy); err != nil {
|
||||
return "", nil, fmt.Errorf("failed to apply changes to copy of base: %w", err)
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
if err := d.diff(ctx, buf, base, applyCopy); err != nil {
|
||||
return "", nil, fmt.Errorf("failed to create diff: %w", err)
|
||||
}
|
||||
|
||||
if oc.mounted {
|
||||
if err := mount.Unmount(oc.merged, 0); err != nil {
|
||||
return "", nil, fmt.Errorf("failed to unmount: %w", err)
|
||||
}
|
||||
oc.mounted = false
|
||||
}
|
||||
|
||||
next, err := os.MkdirTemp(d.tmp, "lower-")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
|
||||
if _, err = Apply(ctx, next, buf, WithConvertWhiteout(OverlayConvertWhiteout), WithParents(oc.lowers)); err != nil {
|
||||
return "", nil, fmt.Errorf("failed to apply tar stream: %w", err)
|
||||
}
|
||||
|
||||
oc.lowers = append([]string{next}, oc.lowers...)
|
||||
|
||||
if len(oc.lowers) == 1 {
|
||||
return oc.lowers[0], nil, nil
|
||||
}
|
||||
|
||||
m := mount.Mount{
|
||||
Type: "overlay",
|
||||
Source: "overlay",
|
||||
Options: []string{
|
||||
fmt.Sprintf("lowerdir=%s", strings.Join(oc.lowers, ":")),
|
||||
},
|
||||
}
|
||||
|
||||
if err := m.Mount(oc.merged); err != nil {
|
||||
return "", nil, fmt.Errorf("failed to mount: %v: %w", m, err)
|
||||
}
|
||||
oc.mounted = true
|
||||
|
||||
return oc.merged, nil, nil
|
||||
}
|
||||
55
pkg/archive/tar_mostunix.go
Normal file
55
pkg/archive/tar_mostunix.go
Normal file
@@ -0,0 +1,55 @@
|
||||
//go:build !windows && !freebsd
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// mknod wraps Unix.Mknod and casts dev to int
|
||||
func mknod(path string, mode uint32, dev uint64) error {
|
||||
return unix.Mknod(path, mode, int(dev))
|
||||
}
|
||||
|
||||
// lsetxattrCreate wraps unix.Lsetxattr, passes the unix.XATTR_CREATE flag on
|
||||
// supported operating systems,and ignores appropriate errors
|
||||
func lsetxattrCreate(link string, attr string, data []byte) error {
|
||||
err := unix.Lsetxattr(link, attr, data, unix.XATTR_CREATE)
|
||||
if err == unix.ENOTSUP || err == unix.ENODATA || err == unix.EEXIST {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// lchmod checks for symlink and changes the mode if not a symlink
|
||||
func lchmod(path string, mode os.FileMode) error {
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.Mode()&os.ModeSymlink == 0 {
|
||||
if err := os.Chmod(path, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
111
pkg/archive/tar_opts.go
Normal file
111
pkg/archive/tar_opts.go
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ApplyOptions provides additional options for an Apply operation
|
||||
type ApplyOptions struct {
|
||||
Filter Filter // Filter tar headers
|
||||
ConvertWhiteout ConvertWhiteout // Convert whiteout files
|
||||
Parents []string // Parent directories to handle inherited attributes without CoW
|
||||
NoSameOwner bool // NoSameOwner will not attempt to preserve the owner specified in the tar archive.
|
||||
|
||||
applyFunc func(context.Context, string, io.Reader, ApplyOptions) (int64, error)
|
||||
}
|
||||
|
||||
// ApplyOpt allows setting mutable archive apply properties on creation
|
||||
type ApplyOpt func(options *ApplyOptions) error
|
||||
|
||||
// Filter specific files from the archive
|
||||
type Filter func(*tar.Header) (bool, error)
|
||||
|
||||
// ConvertWhiteout converts whiteout files from the archive
|
||||
type ConvertWhiteout func(*tar.Header, string) (bool, error)
|
||||
|
||||
// all allows all files
|
||||
func all(_ *tar.Header) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// WithFilter uses the filter to select which files are to be extracted.
|
||||
func WithFilter(f Filter) ApplyOpt {
|
||||
return func(options *ApplyOptions) error {
|
||||
options.Filter = f
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithConvertWhiteout uses the convert function to convert the whiteout files.
|
||||
func WithConvertWhiteout(c ConvertWhiteout) ApplyOpt {
|
||||
return func(options *ApplyOptions) error {
|
||||
options.ConvertWhiteout = c
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNoSameOwner is same as '--no-same-owner` in 'tar' command.
|
||||
// It'll skip attempt to preserve the owner specified in the tar archive.
|
||||
func WithNoSameOwner() ApplyOpt {
|
||||
return func(options *ApplyOptions) error {
|
||||
options.NoSameOwner = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithParents provides parent directories for resolving inherited attributes
|
||||
// directory from the filesystem.
|
||||
// Inherited attributes are searched from first to last, making the first
|
||||
// element in the list the most immediate parent directory.
|
||||
// NOTE: When applying to a filesystem which supports CoW, file attributes
|
||||
// should be inherited by the filesystem.
|
||||
func WithParents(p []string) ApplyOpt {
|
||||
return func(options *ApplyOptions) error {
|
||||
options.Parents = p
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WriteDiffOptions provides additional options for a WriteDiff operation
|
||||
type WriteDiffOptions struct {
|
||||
ParentLayers []string // Windows needs the full list of parent layers
|
||||
|
||||
writeDiffFunc func(context.Context, io.Writer, string, string, WriteDiffOptions) error
|
||||
|
||||
// SourceDateEpoch specifies the following timestamps to provide control for reproducibility.
|
||||
// - The upper bound timestamp of the diff contents
|
||||
// - The timestamp of the whiteouts
|
||||
//
|
||||
// See also https://reproducible-builds.org/docs/source-date-epoch/ .
|
||||
SourceDateEpoch *time.Time
|
||||
}
|
||||
|
||||
// WriteDiffOpt allows setting mutable archive write properties on creation
|
||||
type WriteDiffOpt func(options *WriteDiffOptions) error
|
||||
|
||||
// WithSourceDateEpoch specifies the SOURCE_DATE_EPOCH without touching the env vars.
|
||||
func WithSourceDateEpoch(tm *time.Time) WriteDiffOpt {
|
||||
return func(options *WriteDiffOptions) error {
|
||||
options.SourceDateEpoch = tm
|
||||
return nil
|
||||
}
|
||||
}
|
||||
52
pkg/archive/tar_opts_linux.go
Normal file
52
pkg/archive/tar_opts_linux.go
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// OverlayConvertWhiteout converts whiteout files for overlay.
|
||||
func OverlayConvertWhiteout(hdr *tar.Header, path string) (bool, error) {
|
||||
base := filepath.Base(path)
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
// if a directory is marked as opaque, we need to translate that to overlay
|
||||
if base == whiteoutOpaqueDir {
|
||||
// don't write the file itself
|
||||
return false, unix.Setxattr(dir, "trusted.overlay.opaque", []byte{'y'}, 0)
|
||||
}
|
||||
|
||||
// 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, err
|
||||
}
|
||||
// don't write the file itself
|
||||
return false, os.Chown(originalPath, hdr.Uid, hdr.Gid)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
93
pkg/archive/tar_opts_windows.go
Normal file
93
pkg/archive/tar_opts_windows.go
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
"github.com/Microsoft/hcsshim/pkg/ociwclayer"
|
||||
ocicimlayer "github.com/Microsoft/hcsshim/pkg/ociwclayer/cim"
|
||||
)
|
||||
|
||||
// applyWindowsLayer applies a tar stream of an OCI style diff tar of a Windows layer
|
||||
// See https://github.com/opencontainers/image-spec/blob/main/layer.md#applying-changesets
|
||||
func applyWindowsLayer(ctx context.Context, root string, r io.Reader, options ApplyOptions) (size int64, err error) {
|
||||
// It seems that in certain situations, like having the containerd root and state on a file system hosted on a
|
||||
// mounted VHDX, we need SeSecurityPrivilege when opening a file with winio.ACCESS_SYSTEM_SECURITY. This happens
|
||||
// in the base layer writer in hcsshim when adding a new file.
|
||||
err = winio.RunWithPrivileges([]string{winio.SeSecurityPrivilege}, func() error {
|
||||
var innerErr error
|
||||
size, innerErr = ociwclayer.ImportLayerFromTar(ctx, r, root, options.Parents)
|
||||
return innerErr
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// AsWindowsContainerLayer indicates that the tar stream to apply is that of
|
||||
// a Windows Container Layer. The caller must be holding SeBackupPrivilege and
|
||||
// SeRestorePrivilege.
|
||||
func AsWindowsContainerLayer() ApplyOpt {
|
||||
return func(options *ApplyOptions) error {
|
||||
options.applyFunc = applyWindowsLayer
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// writeDiffWindowsLayers writes a tar stream of the computed difference between the
|
||||
// provided Windows layers
|
||||
//
|
||||
// Produces a tar using OCI style file markers for deletions. Deleted
|
||||
// files will be prepended with the prefix ".wh.". This style is
|
||||
// based off AUFS whiteouts.
|
||||
// See https://github.com/opencontainers/image-spec/blob/main/layer.md
|
||||
func writeDiffWindowsLayers(ctx context.Context, w io.Writer, _, layer string, options WriteDiffOptions) error {
|
||||
return ociwclayer.ExportLayerToTar(ctx, w, layer, options.ParentLayers)
|
||||
}
|
||||
|
||||
// AsWindowsContainerLayerPair indicates that the paths to diff are a pair of
|
||||
// Windows Container Layers. The caller must be holding SeBackupPrivilege.
|
||||
func AsWindowsContainerLayerPair() WriteDiffOpt {
|
||||
return func(options *WriteDiffOptions) error {
|
||||
options.writeDiffFunc = writeDiffWindowsLayers
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithParentLayers provides the Windows Container Layers that are the parents
|
||||
// of the target (right-hand, "upper") layer, if any. The source (left-hand, "lower")
|
||||
// layer passed to WriteDiff should be "" in this case.
|
||||
func WithParentLayers(p []string) WriteDiffOpt {
|
||||
return func(options *WriteDiffOptions) error {
|
||||
options.ParentLayers = p
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func applyWindowsCimLayer(ctx context.Context, root string, r io.Reader, options ApplyOptions) (size int64, err error) {
|
||||
return ocicimlayer.ImportCimLayerFromTar(ctx, r, root, options.Parents)
|
||||
}
|
||||
|
||||
// AsCimContainerLayer indicates that the tar stream to apply is that of a Windows container Layer written in
|
||||
// the cim format.
|
||||
func AsCimContainerLayer() ApplyOpt {
|
||||
return func(options *ApplyOptions) error {
|
||||
options.applyFunc = applyWindowsCimLayer
|
||||
return nil
|
||||
}
|
||||
}
|
||||
1436
pkg/archive/tar_test.go
Normal file
1436
pkg/archive/tar_test.go
Normal file
File diff suppressed because it is too large
Load Diff
202
pkg/archive/tar_unix.go
Normal file
202
pkg/archive/tar_unix.go
Normal file
@@ -0,0 +1,202 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/userns"
|
||||
"github.com/containerd/continuity/fs"
|
||||
"github.com/containerd/continuity/sysx"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func chmodTarEntry(perm os.FileMode) os.FileMode {
|
||||
return perm
|
||||
}
|
||||
|
||||
func setHeaderForSpecialDevice(hdr *tar.Header, name string, fi os.FileInfo) 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 errors.New("unsupported stat type")
|
||||
}
|
||||
|
||||
rdev := uint64(s.Rdev) //nolint:nolintlint,unconvert // rdev is int32 on darwin/bsd, int64 on linux/solaris
|
||||
|
||||
// Currently go does not fill in the major/minors
|
||||
if s.Mode&syscall.S_IFBLK != 0 ||
|
||||
s.Mode&syscall.S_IFCHR != 0 {
|
||||
hdr.Devmajor = int64(unix.Major(rdev))
|
||||
hdr.Devminor = int64(unix.Minor(rdev))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func open(p string) (*os.File, error) {
|
||||
return os.Open(p)
|
||||
}
|
||||
|
||||
func openFile(name string, flag int, perm os.FileMode) (*os.File, error) {
|
||||
f, err := os.OpenFile(name, flag, perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Call chmod to avoid permission mask
|
||||
if err := os.Chmod(name, perm); err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
func mkdir(path string, perm os.FileMode) error {
|
||||
if err := os.Mkdir(path, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
// Only final created directory gets explicit permission
|
||||
// call to avoid permission mask
|
||||
return os.Chmod(path, perm)
|
||||
}
|
||||
|
||||
func skipFile(hdr *tar.Header) bool {
|
||||
switch hdr.Typeflag {
|
||||
case tar.TypeBlock, tar.TypeChar:
|
||||
// cannot create a device if running in user namespace
|
||||
return userns.RunningInUserNS()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// handleTarTypeBlockCharFifo is an OS-specific helper function used by
|
||||
// createTarFile to handle the following types of header: Block; Char; Fifo.
|
||||
// This function must not be called for Block and Char when running in userns.
|
||||
// (skipFile() should return true for them.)
|
||||
func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
|
||||
mode := uint32(hdr.Mode & 07777)
|
||||
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 getxattr(path, attr string) ([]byte, error) {
|
||||
b, err := sysx.LGetxattr(path, attr)
|
||||
if err == unix.ENOTSUP || err == sysx.ENODATA {
|
||||
return nil, nil
|
||||
}
|
||||
return b, err
|
||||
}
|
||||
|
||||
func setxattr(path, key, value string) error {
|
||||
// Do not set trusted attributes
|
||||
if strings.HasPrefix(key, "trusted.") {
|
||||
return fmt.Errorf("admin attributes from archive not supported: %w", unix.ENOTSUP)
|
||||
}
|
||||
return unix.Lsetxattr(path, key, []byte(value), 0)
|
||||
}
|
||||
|
||||
func copyDirInfo(fi os.FileInfo, path string) error {
|
||||
st := fi.Sys().(*syscall.Stat_t)
|
||||
if err := os.Lchown(path, int(st.Uid), int(st.Gid)); err != nil {
|
||||
if os.IsPermission(err) {
|
||||
// Normally if uid/gid are the same this would be a no-op, but some
|
||||
// filesystems may still return EPERM... for instance NFS does this.
|
||||
// In such a case, this is not an error.
|
||||
if dstStat, err2 := os.Lstat(path); err2 == nil {
|
||||
st2 := dstStat.Sys().(*syscall.Stat_t)
|
||||
if st.Uid == st2.Uid && st.Gid == st2.Gid {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to chown %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Chmod(path, fi.Mode()); err != nil {
|
||||
return fmt.Errorf("failed to chmod %s: %w", path, err)
|
||||
}
|
||||
|
||||
timespec := []unix.Timespec{
|
||||
unix.NsecToTimespec(syscall.TimespecToNsec(fs.StatAtime(st))),
|
||||
unix.NsecToTimespec(syscall.TimespecToNsec(fs.StatMtime(st))),
|
||||
}
|
||||
if err := unix.UtimesNanoAt(unix.AT_FDCWD, path, timespec, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
||||
return fmt.Errorf("failed to utime %s: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyUpXAttrs(dst, src string) error {
|
||||
xattrKeys, err := sysx.LListxattr(src)
|
||||
if err != nil {
|
||||
if err == unix.ENOTSUP || err == sysx.ENODATA {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to list xattrs on %s: %w", src, err)
|
||||
}
|
||||
for _, xattr := range xattrKeys {
|
||||
// Do not copy up trusted attributes
|
||||
if strings.HasPrefix(xattr, "trusted.") {
|
||||
continue
|
||||
}
|
||||
data, err := sysx.LGetxattr(src, xattr)
|
||||
if err != nil {
|
||||
if err == unix.ENOTSUP || err == sysx.ENODATA {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to get xattr %q on %s: %w", xattr, src, err)
|
||||
}
|
||||
if err := lsetxattrCreate(dst, xattr, data); err != nil {
|
||||
return fmt.Errorf("failed to set xattr %q on %s: %w", xattr, dst, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
107
pkg/archive/tar_windows.go
Normal file
107
pkg/archive/tar_windows.go
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/sys/sequential"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
perm &= 0755
|
||||
// Add the x bit: make everything +x from windows
|
||||
perm |= 0111
|
||||
|
||||
return perm
|
||||
}
|
||||
|
||||
func setHeaderForSpecialDevice(*tar.Header, string, os.FileInfo) error {
|
||||
// do nothing. no notion of Rdev, Inode, Nlink in stat on Windows
|
||||
return nil
|
||||
}
|
||||
|
||||
func open(p string) (*os.File, error) {
|
||||
// We use sequential file access to avoid depleting the standby list on
|
||||
// Windows.
|
||||
return sequential.Open(p)
|
||||
}
|
||||
|
||||
func openFile(name string, flag int, perm os.FileMode) (*os.File, error) {
|
||||
// Source is regular file. We use sequential file access to avoid depleting
|
||||
// the standby list on Windows.
|
||||
return sequential.OpenFile(name, flag, perm)
|
||||
}
|
||||
|
||||
func mkdir(path string, perm os.FileMode) error {
|
||||
return os.Mkdir(path, perm)
|
||||
}
|
||||
|
||||
func skipFile(hdr *tar.Header) bool {
|
||||
// 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.
|
||||
return strings.Contains(hdr.Name, ":")
|
||||
}
|
||||
|
||||
// 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 lchmod(path string, mode os.FileMode) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getxattr(path, attr string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func setxattr(path, key, value string) error {
|
||||
// Return not support error, do not wrap underlying not supported
|
||||
// since xattrs should not exist in windows diff archives
|
||||
return errors.New("xattrs not supported on Windows")
|
||||
}
|
||||
|
||||
func copyDirInfo(fi os.FileInfo, path string) error {
|
||||
if err := os.Chmod(path, fi.Mode()); err != nil {
|
||||
return fmt.Errorf("failed to chmod %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyUpXAttrs(dst, src string) error {
|
||||
return nil
|
||||
}
|
||||
82
pkg/archive/tarheader/tarheader.go
Normal file
82
pkg/archive/tarheader/tarheader.go
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
Portions from https://github.com/moby/moby/blob/v23.0.1/pkg/archive/archive.go#L419-L464
|
||||
Copyright (C) Docker/Moby authors.
|
||||
Licensed under the Apache License, Version 2.0
|
||||
NOTICE: https://github.com/moby/moby/blob/v23.0.1/NOTICE
|
||||
*/
|
||||
|
||||
package tarheader
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"os"
|
||||
)
|
||||
|
||||
// nosysFileInfo hides the system-dependent info of the wrapped FileInfo to
|
||||
// prevent tar.FileInfoHeader from introspecting it and potentially calling into
|
||||
// glibc.
|
||||
//
|
||||
// From https://github.com/moby/moby/blob/v23.0.1/pkg/archive/archive.go#L419-L434 .
|
||||
type nosysFileInfo struct {
|
||||
os.FileInfo
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// sysStat, if non-nil, populates hdr from system-dependent fields of fi.
|
||||
//
|
||||
// From https://github.com/moby/moby/blob/v23.0.1/pkg/archive/archive.go#L436-L437 .
|
||||
var sysStat func(fi os.FileInfo, hdr *tar.Header) error
|
||||
|
||||
// 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.
|
||||
//
|
||||
// From https://github.com/moby/moby/blob/v23.0.1/pkg/archive/archive.go#L439-L464 .
|
||||
func FileInfoHeaderNoLookups(fi os.FileInfo, link string) (*tar.Header, error) {
|
||||
hdr, err := tar.FileInfoHeader(nosysFileInfo{fi}, link)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sysStat != nil {
|
||||
return hdr, sysStat(fi, hdr)
|
||||
}
|
||||
return hdr, nil
|
||||
}
|
||||
75
pkg/archive/tarheader/tarheader_unix.go
Normal file
75
pkg/archive/tarheader/tarheader_unix.go
Normal file
@@ -0,0 +1,75 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
Portions from https://github.com/moby/moby/blob/v23.0.1/pkg/archive/archive_unix.go#L52-L70
|
||||
Copyright (C) Docker/Moby authors.
|
||||
Licensed under the Apache License, Version 2.0
|
||||
NOTICE: https://github.com/moby/moby/blob/v23.0.1/NOTICE
|
||||
*/
|
||||
|
||||
package tarheader
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func init() {
|
||||
sysStat = statUnix
|
||||
}
|
||||
|
||||
// statUnix populates hdr from system-dependent fields of fi without performing
|
||||
// any OS lookups.
|
||||
// From https://github.com/moby/moby/blob/2cccb1f02c83aaacef3c15fa43f3b64d57f315f8/pkg/archive/archive_unix.go#L46-L76
|
||||
func statUnix(fi os.FileInfo, hdr *tar.Header) error {
|
||||
s, ok := fi.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
hdr.Uid = int(s.Uid)
|
||||
hdr.Gid = int(s.Gid)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if s.Mode&unix.S_IFBLK != 0 ||
|
||||
s.Mode&unix.S_IFCHR != 0 {
|
||||
hdr.Devmajor = int64(unix.Major(uint64(s.Rdev)))
|
||||
hdr.Devminor = int64(unix.Minor(uint64(s.Rdev)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
210
pkg/archive/tartest/tar.go
Normal file
210
pkg/archive/tartest/tar.go
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package tartest
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WriterToTar is an type which writes to a tar writer
|
||||
type WriterToTar interface {
|
||||
WriteTo(*tar.Writer) error
|
||||
}
|
||||
|
||||
type writerToFn func(*tar.Writer) error
|
||||
|
||||
func (w writerToFn) WriteTo(tw *tar.Writer) error {
|
||||
return w(tw)
|
||||
}
|
||||
|
||||
// TarAll creates a WriterToTar which calls all the provided writers
|
||||
// in the order in which they are provided.
|
||||
func TarAll(wt ...WriterToTar) WriterToTar {
|
||||
return writerToFn(func(tw *tar.Writer) error {
|
||||
for _, w := range wt {
|
||||
if err := w.WriteTo(tw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// TarFromWriterTo is used to create a tar stream from a tar record
|
||||
// creator. This can be used to manufacture more specific tar records
|
||||
// which allow testing specific tar cases which may be encountered
|
||||
// by the untar process.
|
||||
func TarFromWriterTo(wt WriterToTar) io.ReadCloser {
|
||||
r, w := io.Pipe()
|
||||
go func() {
|
||||
tw := tar.NewWriter(w)
|
||||
if err := wt.WriteTo(tw); err != nil {
|
||||
w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
w.CloseWithError(tw.Close())
|
||||
}()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// TarContext is used to create tar records
|
||||
type TarContext struct {
|
||||
UID int
|
||||
GID int
|
||||
|
||||
// ModTime sets the modtimes for all files, if nil the current time
|
||||
// is used for each file when it was written
|
||||
ModTime *time.Time
|
||||
|
||||
Xattrs map[string]string
|
||||
}
|
||||
|
||||
func (tc TarContext) newHeader(mode os.FileMode, name, link string, size int64) *tar.Header {
|
||||
ti := tarInfo{
|
||||
name: name,
|
||||
mode: mode,
|
||||
size: size,
|
||||
modt: tc.ModTime,
|
||||
hdr: &tar.Header{
|
||||
Uid: tc.UID,
|
||||
Gid: tc.GID,
|
||||
Xattrs: tc.Xattrs,
|
||||
},
|
||||
}
|
||||
|
||||
if mode&os.ModeSymlink == 0 && link != "" {
|
||||
ti.hdr.Typeflag = tar.TypeLink
|
||||
ti.hdr.Linkname = link
|
||||
}
|
||||
|
||||
hdr, err := tar.FileInfoHeader(ti, link)
|
||||
if err != nil {
|
||||
// Only returns an error on bad input mode
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return hdr
|
||||
}
|
||||
|
||||
type tarInfo struct {
|
||||
name string
|
||||
mode os.FileMode
|
||||
size int64
|
||||
modt *time.Time
|
||||
hdr *tar.Header
|
||||
}
|
||||
|
||||
func (ti tarInfo) Name() string {
|
||||
return ti.name
|
||||
}
|
||||
|
||||
func (ti tarInfo) Size() int64 {
|
||||
return ti.size
|
||||
}
|
||||
func (ti tarInfo) Mode() os.FileMode {
|
||||
return ti.mode
|
||||
}
|
||||
|
||||
func (ti tarInfo) ModTime() time.Time {
|
||||
if ti.modt != nil {
|
||||
return *ti.modt
|
||||
}
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
func (ti tarInfo) IsDir() bool {
|
||||
return (ti.mode & os.ModeDir) != 0
|
||||
}
|
||||
func (ti tarInfo) Sys() interface{} {
|
||||
return ti.hdr
|
||||
}
|
||||
|
||||
// WithUIDGID sets the UID and GID for tar entries
|
||||
func (tc TarContext) WithUIDGID(uid, gid int) TarContext {
|
||||
ntc := tc
|
||||
ntc.UID = uid
|
||||
ntc.GID = gid
|
||||
return ntc
|
||||
}
|
||||
|
||||
// WithModTime sets the ModTime for tar entries
|
||||
func (tc TarContext) WithModTime(modtime time.Time) TarContext {
|
||||
ntc := tc
|
||||
ntc.ModTime = &modtime
|
||||
return ntc
|
||||
}
|
||||
|
||||
// WithXattrs adds these xattrs to all files, merges with any
|
||||
// previously added xattrs
|
||||
func (tc TarContext) WithXattrs(xattrs map[string]string) TarContext {
|
||||
ntc := tc
|
||||
if ntc.Xattrs == nil {
|
||||
ntc.Xattrs = map[string]string{}
|
||||
}
|
||||
for k, v := range xattrs {
|
||||
ntc.Xattrs[k] = v
|
||||
}
|
||||
return ntc
|
||||
}
|
||||
|
||||
// File returns a regular file tar entry using the provided bytes
|
||||
func (tc TarContext) File(name string, content []byte, perm os.FileMode) WriterToTar {
|
||||
return writerToFn(func(tw *tar.Writer) error {
|
||||
return writeHeaderAndContent(tw, tc.newHeader(perm, name, "", int64(len(content))), content)
|
||||
})
|
||||
}
|
||||
|
||||
// Dir returns a directory tar entry
|
||||
func (tc TarContext) Dir(name string, perm os.FileMode) WriterToTar {
|
||||
return writerToFn(func(tw *tar.Writer) error {
|
||||
return writeHeaderAndContent(tw, tc.newHeader(perm|os.ModeDir, name, "", 0), nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Symlink returns a symlink tar entry
|
||||
func (tc TarContext) Symlink(oldname, newname string) WriterToTar {
|
||||
return writerToFn(func(tw *tar.Writer) error {
|
||||
return writeHeaderAndContent(tw, tc.newHeader(0777|os.ModeSymlink, newname, oldname, 0), nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Link returns a hard link tar entry
|
||||
func (tc TarContext) Link(oldname, newname string) WriterToTar {
|
||||
return writerToFn(func(tw *tar.Writer) error {
|
||||
return writeHeaderAndContent(tw, tc.newHeader(0777, newname, oldname, 0), nil)
|
||||
})
|
||||
}
|
||||
|
||||
func writeHeaderAndContent(tw *tar.Writer, h *tar.Header, b []byte) error {
|
||||
if h.Size != int64(len(b)) {
|
||||
return errors.New("bad content length")
|
||||
}
|
||||
if err := tw.WriteHeader(h); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(b) > 0 {
|
||||
if _, err := tw.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
54
pkg/archive/time.go
Normal file
54
pkg/archive/time.go
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"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
|
||||
}
|
||||
38
pkg/archive/time_unix.go
Normal file
38
pkg/archive/time_unix.go
Normal file
@@ -0,0 +1,38 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func chtimes(path string, atime, mtime time.Time) error {
|
||||
var utimes [2]unix.Timespec
|
||||
utimes[0] = unix.NsecToTimespec(atime.UnixNano())
|
||||
utimes[1] = unix.NsecToTimespec(mtime.UnixNano())
|
||||
|
||||
if err := unix.UtimesNanoAt(unix.AT_FDCWD, path, utimes[0:], unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
||||
return fmt.Errorf("failed call to UtimesNanoAt for %s: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
41
pkg/archive/time_windows.go
Normal file
41
pkg/archive/time_windows.go
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// chtimes will set the create time on a file using the given modtime.
|
||||
// This requires calling SetFileTime and explicitly including the create time.
|
||||
func chtimes(path string, atime, mtime time.Time) error {
|
||||
pathp, err := windows.UTF16PtrFromString(path)
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user