Skip to content

Commit 420d802

Browse files
Match docker cp entry naming and compression for tar streams
The stream copyOut path reused archiveDirectory for directories, which emits a leading "./" and names entries relative to the directory itself, and always compressed with gzip. Docker and podman name entries relative to the source's parent, so the source's own basename is the top level entry, and emit an uncompressed tar. The difference is visible in a round trip: with contents-relative naming, `cp ctr:/dir - | cp - other:/dest` scatters dir's children into /dest instead of producing /dest/dir. The gzip framing also breaks consumers that read the stream with a plain tar reader rather than shelling out to tar. Use the parent-relative form for both files and directories whenever request.isArchive marks the stream path, with no compression filter. The internal directory copyOut, where isArchive is derived from the path being a directory, keeps shipping pax+gzip of the directory's contents, which is what the host side extracts into the destination directory.
1 parent 35f5d9b commit 420d802

1 file changed

Lines changed: 13 additions & 9 deletions

File tree

vminitd/Sources/VminitdCore/Server+GRPC.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -597,17 +597,21 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ
597597
defer { try? sock.close() }
598598

599599
if isArchive {
600-
let fileURL = URL(fileURLWithPath: path)
601-
let writer = try ArchiveWriter(configuration: .init(format: .pax, filter: .gzip))
600+
// `request.isArchive` marks the `docker cp CONTAINER:/path -`
601+
// stream path. Docker emits an uncompressed tar whose entries are
602+
// named relative to the source's parent, so the source's own
603+
// basename is the top level entry, for both files and directories.
604+
// The internal directory copyOut instead ships pax+gzip of the
605+
// directory's contents, which is what the host extracts into the
606+
// destination directory.
607+
let filter: Filter = request.isArchive ? Filter.none : Filter.gzip
608+
let writer = try ArchiveWriter(configuration: .init(format: .pax, filter: filter))
602609
try writer.open(fileDescriptor: sock.fileDescriptor)
603-
if isDirectory.boolValue {
604-
try writer.archiveDirectory(fileURL)
605-
} else {
606-
// Forced single-file archive: emit one entry named after the
607-
// file's basename, relative to its parent directory.
610+
if request.isArchive {
608611
let filePath = FilePath(path)
609-
let base = filePath.removingLastComponent()
610-
try writer.archive([filePath], base: base)
612+
try writer.archive([filePath], base: filePath.removingLastComponent())
613+
} else {
614+
try writer.archiveDirectory(URL(fileURLWithPath: path))
611615
}
612616
try writer.finishEncoding()
613617
} else {

0 commit comments

Comments
 (0)