diff --git a/copier/copier.go b/copier/copier.go index c742601ea06..400d4cceb38 100644 --- a/copier/copier.go +++ b/copier/copier.go @@ -35,6 +35,8 @@ const ( cISUID = 0o4000 // Set uid, from archive/tar cISGID = 0o2000 // Set gid, from archive/tar cISVTX = 0o1000 // Save text (sticky bit), from archive/tar + // xattrs in the PAXRecords map are namespaced with this prefix + xattrPAXRecordNamespace = "SCHILY.xattr." ) func init() { @@ -1427,6 +1429,23 @@ func handleRename(rename map[string]string, name string) string { return name } +// mapWithPrefixedKeysWithoutKeyPrefix returns a map containing every element +// of m that had p as a prefix in its (string) key, with that prefix stripped +// from its key. items are shallow-copied using assignment. if m is nil, the +// returned map will be nil, otherwise it will at least have been allocated +func mapWithPrefixedKeysWithoutKeyPrefix[K any](m map[string]K, p string) map[string]K { + if m == nil { + return m + } + cloned := make(map[string]K, len(m)) + for k, v := range m { + if strings.HasPrefix(k, p) { + cloned[strings.TrimPrefix(k, p)] = v + } + } + return cloned +} + func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath string, options GetOptions, tw *tar.Writer, hardlinkChecker *hardlinkChecker, idMappings *idtools.IDMappings) error { // build the header using the name provided hdr, err := tar.FileInfoHeader(srcfi, symlinkTarget) @@ -1455,8 +1474,13 @@ func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath str if err != nil { return fmt.Errorf("getting extended attributes for %q: %w", contentPath, err) } + if len(xattrs) > 0 && hdr.PAXRecords == nil { + hdr.PAXRecords = make(map[string]string, len(xattrs)) + } + } + for k, v := range xattrs { + hdr.PAXRecords[xattrPAXRecordNamespace+k] = v } - hdr.Xattrs = xattrs // nolint:staticcheck if hdr.Typeflag == tar.TypeReg { // if it's an archive and we're extracting archives, read the // file and spool out its contents in-line. (if we just @@ -1959,7 +1983,8 @@ func copierHandlerPut(bulkReader io.Reader, req request, idMappings *idtools.IDM } // set xattrs, including some that might have been reset by chown() if !req.PutOptions.StripXattrs { - if err = Lsetxattrs(path, hdr.Xattrs); err != nil { // nolint:staticcheck + xattrs := mapWithPrefixedKeysWithoutKeyPrefix(hdr.PAXRecords, xattrPAXRecordNamespace) + if err = Lsetxattrs(path, xattrs); err != nil { // nolint:staticcheck if !req.PutOptions.IgnoreXattrErrors { return fmt.Errorf("copier: put: error setting extended attributes on %q: %w", path, err) } diff --git a/tests/conformance/conformance_test.go b/tests/conformance/conformance_test.go index decea79a80b..e12a7289894 100644 --- a/tests/conformance/conformance_test.go +++ b/tests/conformance/conformance_test.go @@ -61,6 +61,8 @@ const ( cISUID = 0o4000 // Set uid, from archive/tar cISGID = 0o2000 // Set gid, from archive/tar cISVTX = 0o1000 // Save text (sticky bit), from archive/tar + // xattrs in the PAXRecords map are namespaced with this prefix + xattrPAXRecordNamespace = "SCHILY.xattr." ) var ( @@ -2150,7 +2152,9 @@ var internalTestCases = []testCase{ Size: 8, Mode: 0o640, ModTime: testDate, - Xattrs: map[string]string{"user.a": "test"}, + PAXRecords: map[string]string{ + xattrPAXRecordNamespace + "user.a": "test", + }, } if err = tw.WriteHeader(&hdr); err != nil { return fmt.Errorf("writing tar archive header: %w", err)