diff --git a/Sources/ContainerizationArchive/ArchiveReader.swift b/Sources/ContainerizationArchive/ArchiveReader.swift index 3c291d1a5..c130200fb 100644 --- a/Sources/ContainerizationArchive/ArchiveReader.swift +++ b/Sources/ContainerizationArchive/ArchiveReader.swift @@ -398,10 +398,10 @@ extension ArchiveReader { } private func setFileAttributes(fd: Int32, entry: WriteEntry) { - fchmod(fd, entry.permissions & 0o777) if let owner = entry.owner, let group = entry.group { fchown(fd, owner, group) } + fchmod(fd, entry.permissions & 0o7777) } private static func copyDataReaderToFd(dataReader: ArchiveEntryReader, fileFd: Int32, memberPath: FilePath) throws { diff --git a/Tests/ContainerizationArchiveTests/ArchiveReaderTests.swift b/Tests/ContainerizationArchiveTests/ArchiveReaderTests.swift index c7c0e563a..e9da47cb2 100644 --- a/Tests/ContainerizationArchiveTests/ArchiveReaderTests.swift +++ b/Tests/ContainerizationArchiveTests/ArchiveReaderTests.swift @@ -438,6 +438,52 @@ struct ArchiveReaderTests { #expect((perms & permMask) == 0o755, "Permissions should be preserved") } + @Test func preserveSpecialPermissionBits() throws { + let testDirectory = createTemporaryDirectory(baseName: "ArchiveReaderTests")! + let archiveURL = testDirectory.appendingPathComponent("special-permissions.tar") + let archiver = try ArchiveWriter(format: .paxRestricted, filter: .none, file: archiveURL) + + let writeEntry = WriteEntry() + writeEntry.path = "sticky" + writeEntry.fileType = .directory + writeEntry.permissions = 0o1777 + writeEntry.size = 0 + try archiver.writeEntry(entry: writeEntry, data: nil) + + let setIDEntry = WriteEntry() + setIDEntry.path = "set-id" + setIDEntry.fileType = .regular + setIDEntry.permissions = 0o6755 + setIDEntry.owner = getuid() + setIDEntry.group = getgid() + let setIDData = Data("set-id".utf8) + setIDEntry.size = numericCast(setIDData.count) + try archiver.writeEntry(entry: setIDEntry, data: setIDData) + + try archiver.finishEncoding() + + defer { try? FileManager.default.removeItem(at: testDirectory) } + + let extractDir = try createExtractionDirectory(name: "special-permissions") + defer { try? FileManager.default.removeItem(at: extractDir.deletingLastPathComponent()) } + + let reader = try ArchiveReader(format: .paxRestricted, filter: .none, file: archiveURL) + let rejectedPaths = try reader.extractContents(to: extractDir) + + #expect(rejectedPaths.isEmpty) + + let directoryPath = extractDir.appendingPathComponent("sticky").path + let attrs = try FileManager.default.attributesOfItem(atPath: directoryPath) + let perms = (attrs[.posixPermissions] as? NSNumber)?.uint16Value ?? 0 + let permMask: UInt16 = 0o7777 + #expect((perms & permMask) == 0o1777, "Special permission bits should be preserved") + + let setIDPath = extractDir.appendingPathComponent("set-id").path + let setIDAttrs = try FileManager.default.attributesOfItem(atPath: setIDPath) + let setIDPerms = (setIDAttrs[.posixPermissions] as? NSNumber)?.uint16Value ?? 0 + #expect((setIDPerms & permMask) == 0o6755, "Set-ID permission bits should be preserved after ownership") + } + // MARK: - Duplicate Entry Tests @Test func duplicateRegularFiles() throws {