Skip to content

Commit 2063169

Browse files
committed
Tighten up some logic around file permissions
Fix a regression in PbxCp which caused files to inappropriately gain the execute bit. `isExecutable` is conceptually a higher level operation which (per POSIX) can return true even if the file has no execute permission bits set: "If execute permission is requested, access shall be granted if execute permission is granted to at least one user by the file permission bits or by an alternate access control mechanism" No test because it's not clear what the concrete conditions are under which this occurs. That said, copying now checks against the owner permission bit specifically, as that's the actual intent of what the code is meaning to do and matches the original behavior. Additionally, fix some missing permission information in getFileInfo for the PseudoFS implementation and increase test coverage a bit. Avoids it coming back to bite later. rdar://154663442
1 parent bb039da commit 2063169

File tree

3 files changed

+54
-29
lines changed

3 files changed

+54
-29
lines changed

Sources/SWBUtil/FSProxy.swift

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,11 @@ public protocol FSProxy: AnyObject, Sendable {
160160
// FIXME: Need to document behavior w.r.t. error handling.
161161
func isDirectory(_ path: Path) -> Bool
162162

163-
/// Checks whether the given path has the execute bit (which on Windows is determined by the file extension).
163+
/// Checks whether the given path is executable.
164+
///
165+
/// On Windows, this is determined by the file extension (based on `SHGetFileInfo`), while on Unix it's determined by `access`, which means a file may be deemed executable even if no execute bit is set in the POSIX permissions.
166+
///
167+
/// - seealso: [_stat, _stat32, _stat64, _stati64, _stat32i64, _stat64i32, _wstat, _wstat32, _wstat64, _wstati64, _wstat32i64, _wstat64i32](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/stat-functions)
164168
func isExecutable(_ path: Path) throws -> Bool
165169

166170
/// Checks whether the given path is a symlink, also returning whether the linked file exists.
@@ -309,6 +313,10 @@ public extension FSProxy {
309313
func getFileSize(_ path: Path) throws -> ByteCount {
310314
try ByteCount(Int64(getFileInfo(path).size))
311315
}
316+
317+
func getFilePermissions(_ path: Path) throws -> FilePermissions {
318+
try FilePermissions(rawValue: CModeT(getFilePermissions(path)))
319+
}
312320
}
313321

314322
fileprivate extension FSProxy {
@@ -853,7 +861,7 @@ public class PseudoFS: FSProxy, @unchecked Sendable {
853861
}
854862

855863
// Write the symlink.
856-
directory.contents[path.basename] = Node(.symlink(target), permissions: 0o644, timestamp: getTimestamp(), inode: nextInode())
864+
directory.contents[path.basename] = Node(.symlink(target), permissions: 0o755, timestamp: getTimestamp(), inode: nextInode())
857865
parent.timestamp = getTimestamp()
858866
}
859867

@@ -1237,35 +1245,29 @@ public class PseudoFS: FSProxy, @unchecked Sendable {
12371245
public func getFileInfo(_ path: Path) throws -> FileInfo {
12381246
return try queue.blocking_sync {
12391247
guard let node = getNode(path) else { throw POSIXError(ENOENT) }
1248+
1249+
let type: FileAttributeType
1250+
let size: Int
12401251
switch node.contents {
12411252
case .file(let contents):
1242-
let info: [FileAttributeKey: any Sendable] = [
1243-
.modificationDate : Date(timeIntervalSince1970: TimeInterval(node.timestamp)),
1244-
.type: FileAttributeType.typeRegular,
1245-
.size: contents.bytes.count,
1246-
.posixPermissions: 0,
1247-
.systemNumber: node.device,
1248-
.systemFileNumber: node.inode]
1249-
return createFileInfo(info)
1253+
type = .typeRegular
1254+
size = contents.bytes.count
12501255
case .directory(let dir):
1251-
let info: [FileAttributeKey: any Sendable] = [
1252-
.modificationDate: Date(timeIntervalSince1970: TimeInterval(node.timestamp)),
1253-
.type: FileAttributeType.typeDirectory,
1254-
.size: dir.contents.count,
1255-
.posixPermissions: 0,
1256-
.systemNumber: node.device,
1257-
.systemFileNumber: node.inode]
1258-
return createFileInfo(info)
1259-
case .symlink(_):
1260-
let info: [FileAttributeKey: any Sendable] = [
1261-
.modificationDate: Date(timeIntervalSince1970: TimeInterval(node.timestamp)),
1262-
.type: FileAttributeType.typeSymbolicLink,
1263-
.size: 0,
1264-
.posixPermissions: 0,
1265-
.systemNumber: node.device,
1266-
.systemFileNumber: node.inode]
1267-
return createFileInfo(info)
1256+
type = .typeDirectory
1257+
size = dir.contents.count
1258+
case .symlink(let destination):
1259+
type = .typeSymbolicLink
1260+
size = destination.str.utf8.count
12681261
}
1262+
1263+
let info: [FileAttributeKey: any Sendable] = [
1264+
.modificationDate : Date(timeIntervalSince1970: TimeInterval(node.timestamp)),
1265+
.type: type,
1266+
.size: size,
1267+
.posixPermissions: node.permissions,
1268+
.systemNumber: node.device,
1269+
.systemFileNumber: node.inode]
1270+
return createFileInfo(info)
12691271
}
12701272
}
12711273

Sources/SWBUtil/PbxCp.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,9 @@ fileprivate func copyRegular(_ srcPath: Path, _ srcParentPath: Path, _ dstPath:
298298

299299
func _copyFile(_ srcPath: Path, _ dstPath: Path) throws {
300300
do {
301+
let existingPermissions: FilePermissions = try localFS.getFilePermissions(srcPath)
301302
var permissions: FilePermissions = [.ownerRead, .ownerWrite, .groupRead, .groupWrite, .otherRead, .otherWrite]
302-
if try localFS.isExecutable(srcPath) {
303+
if existingPermissions.contains(.ownerExecute) {
303304
permissions.insert([.ownerExecute, .groupExecute, .otherExecute])
304305
}
305306
let dstFd = try FileDescriptor.open(FilePath(dstPath.str), .writeOnly, options: [.create, .truncate], permissions: permissions)

Tests/SWBUtilTests/FSProxyTests.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,11 +350,21 @@ import SWBTestSupport
350350
// Test setting file permissions.
351351
let execPath = tmpDir.join("script.sh")
352352
try localFS.write(execPath, contents: [])
353+
#expect(try localFS.getFilePermissions(execPath) == 0o644)
354+
#expect(try localFS.getFileInfo(execPath).permissions == 0o644)
353355
#expect(try !localFS.isExecutable(execPath))
354-
355356
try localFS.setFilePermissions(execPath, permissions: 0o755)
356357
#expect(try localFS.getFilePermissions(execPath) == 0o755)
358+
#expect(try localFS.getFileInfo(execPath).permissions == 0o755)
357359
#expect(try localFS.isExecutable(execPath))
360+
361+
let linkPath = tmpDir.join("script")
362+
try localFS.symlink(linkPath, target: Path("script.sh"))
363+
#expect(try localFS.getFilePermissions(linkPath) == 0o755)
364+
#expect(try localFS.getFileInfo(linkPath).permissions == 0o755)
365+
try localFS.setFilePermissions(linkPath, permissions: 0o644)
366+
#expect(try localFS.getFilePermissions(linkPath) == 0o644)
367+
#expect(try localFS.getFileInfo(linkPath).permissions == 0o644)
358368
}
359369
}
360370

@@ -804,16 +814,28 @@ import SWBTestSupport
804814

805815
// Test default permissions.
806816
#expect(try fs.getFilePermissions(.root) == 0o755)
817+
#expect(try fs.getFileInfo(.root).permissions == 0o755)
807818
let filePath = Path.root.join("file.txt")
808819
try fs.write(filePath, contents: [])
809820
#expect(try fs.getFilePermissions(filePath) == 0o644)
821+
#expect(try fs.getFileInfo(filePath).permissions == 0o644)
810822

811823
// Test setting file permissions.
812824
let execPath = Path.root.join("script.sh")
813825
try fs.write(execPath, contents: [])
814826
#expect(try fs.getFilePermissions(execPath) == 0o644)
827+
#expect(try fs.getFileInfo(execPath).permissions == 0o644)
815828
try fs.setFilePermissions(execPath, permissions: 0o755)
816829
#expect(try fs.getFilePermissions(execPath) == 0o755)
830+
#expect(try fs.getFileInfo(execPath).permissions == 0o755)
831+
832+
let linkPath = Path.root.join("script")
833+
try fs.symlink(linkPath, target: Path("script.sh"))
834+
#expect(try fs.getFilePermissions(linkPath) == 0o755)
835+
#expect(try fs.getFileInfo(linkPath).permissions == 0o755)
836+
try fs.setFilePermissions(linkPath, permissions: 0o644)
837+
#expect(try fs.getFilePermissions(linkPath) == 0o644)
838+
#expect(try fs.getFileInfo(linkPath).permissions == 0o644)
817839
}
818840

819841
@Test

0 commit comments

Comments
 (0)