mirror of
https://github.com/samsonjs/FileOtter.git
synced 2026-04-27 14:57:41 +00:00
WIP: Implement more of File
This commit is contained in:
parent
ab21045498
commit
993cc73ec0
11 changed files with 925 additions and 543 deletions
|
|
@ -98,7 +98,7 @@ public extension Dir {
|
||||||
try FileManager.default.createDirectory(
|
try FileManager.default.createDirectory(
|
||||||
at: url,
|
at: url,
|
||||||
withIntermediateDirectories: false,
|
withIntermediateDirectories: false,
|
||||||
attributes: attributes
|
attributes: attributes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,7 +110,7 @@ public extension Dir {
|
||||||
try FileManager.default.createDirectory(
|
try FileManager.default.createDirectory(
|
||||||
at: tmpDir,
|
at: tmpDir,
|
||||||
withIntermediateDirectories: true,
|
withIntermediateDirectories: true,
|
||||||
attributes: [.posixPermissions: 0o700]
|
attributes: [.posixPermissions: 0o700],
|
||||||
)
|
)
|
||||||
return tmpDir
|
return tmpDir
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +119,7 @@ public extension Dir {
|
||||||
static func mktmpdir<T>(
|
static func mktmpdir<T>(
|
||||||
prefix: String = "d",
|
prefix: String = "d",
|
||||||
suffix: String = "",
|
suffix: String = "",
|
||||||
_ block: (URL) throws -> T
|
_ block: (URL) throws -> T,
|
||||||
) throws -> T {
|
) throws -> T {
|
||||||
let tmpDir = try mktmpdir(prefix: prefix, suffix: suffix)
|
let tmpDir = try mktmpdir(prefix: prefix, suffix: suffix)
|
||||||
defer {
|
defer {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
// Created by Sami Samhuri on 2025-08-19.
|
// Created by Sami Samhuri on 2025-08-19.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Darwin
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
// MARK: - File Class
|
// MARK: - File Class
|
||||||
|
|
@ -30,10 +31,10 @@ public class File: CustomStringConvertible, CustomDebugStringConvertible {
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
public init(url: URL, mode: Mode = .read, permissions: Int = 0o666) throws {
|
public init(url: URL, mode: Mode = .read, permissions _: Int = 0o666) throws {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.handle = FileHandle() // TODO: Implement proper opening
|
handle = FileHandle() // TODO: Implement proper opening
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,12 +44,12 @@ public class File: CustomStringConvertible, CustomDebugStringConvertible {
|
||||||
|
|
||||||
// MARK: - Opening with blocks
|
// MARK: - Opening with blocks
|
||||||
|
|
||||||
public static func open(url: URL, mode: Mode = .read, permissions: Int = 0o666) throws -> File {
|
public static func open(url _: URL, mode _: Mode = .read, permissions _: Int = 0o666) throws -> File {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public static func open<T>(url: URL, mode: Mode = .read, permissions: Int = 0o666, _ block: (File) throws -> T) rethrows -> T {
|
public static func open<T>(url _: URL, mode _: Mode = .read, permissions _: Int = 0o666, _: (File) throws -> T) rethrows -> T {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,27 +77,27 @@ public class File: CustomStringConvertible, CustomDebugStringConvertible {
|
||||||
|
|
||||||
// MARK: - Instance Methods
|
// MARK: - Instance Methods
|
||||||
|
|
||||||
public func chmod(_ permissions: Int) throws {
|
public func chmod(_: Int) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func chown(owner: Int? = nil, group: Int? = nil) throws {
|
public func chown(owner _: Int? = nil, group _: Int? = nil) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func truncate(to size: Int) throws {
|
public func truncate(to _: Int) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func flock(_ operation: LockOperation) throws {
|
public func flock(_: LockOperation) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func stat() throws -> FileStat {
|
public func fileStat() throws -> FileStat {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func lstat() throws -> FileStat {
|
public func fileLstat() throws -> FileStat {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,7 +129,7 @@ public extension File {
|
||||||
let base = url.lastPathComponent
|
let base = url.lastPathComponent
|
||||||
|
|
||||||
// If no suffix specified, return the base
|
// If no suffix specified, return the base
|
||||||
guard let suffix = suffix else {
|
guard let suffix else {
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,20 +200,59 @@ public extension File {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
static func absolutePath(_ url: URL, relativeTo base: URL? = nil) -> URL {
|
static func absolutePath(_ url: URL) -> URL {
|
||||||
fatalError("Not implemented")
|
// URL(fileURLWithPath:) already makes relative paths absolute using current directory
|
||||||
|
// We just need to normalize the path (resolves . and .. but NOT symlinks)
|
||||||
|
url.standardized
|
||||||
}
|
}
|
||||||
|
|
||||||
static func expandPath(_ path: String) -> URL {
|
static func expandPath(_ path: String) -> URL {
|
||||||
fatalError("Not implemented")
|
// Expand tilde to home directory
|
||||||
|
let expanded = (path as NSString).expandingTildeInPath
|
||||||
|
return URL(fileURLWithPath: expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func realpath(_ url: URL) throws -> URL {
|
static func realpath(_ url: URL) throws -> URL {
|
||||||
fatalError("Not implemented")
|
// Resolve all symbolic links in the path
|
||||||
|
// All components must exist for this to work
|
||||||
|
let path = url.path
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
guard FileManager.default.fileExists(atPath: path) else {
|
||||||
|
throw CocoaError(.fileNoSuchFile, userInfo: [NSFilePathErrorKey: path])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use standardizedFileURL to resolve symlinks and normalize the path
|
||||||
|
// This resolves .., ., and symlinks
|
||||||
|
return url.resolvingSymlinksInPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func realdirpath(_ url: URL) throws -> URL {
|
static func realdirpath(_ url: URL) throws -> URL {
|
||||||
fatalError("Not implemented")
|
// Similar to realpath but the last component may not exist
|
||||||
|
let parentURL = url.deletingLastPathComponent()
|
||||||
|
let lastComponent = url.lastPathComponent
|
||||||
|
|
||||||
|
// If we're at root or parent doesn't exist, just standardize
|
||||||
|
if parentURL.path == "/" || parentURL.path.isEmpty {
|
||||||
|
return url.standardizedFileURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the full path exists (including the last component)
|
||||||
|
if FileManager.default.fileExists(atPath: url.path) {
|
||||||
|
// If the full path exists, resolve all symlinks
|
||||||
|
return url.resolvingSymlinksInPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the parent needs to exist, last component may not
|
||||||
|
let resolvedParent: URL = if FileManager.default.fileExists(atPath: parentURL.path) {
|
||||||
|
parentURL.resolvingSymlinksInPath()
|
||||||
|
} else {
|
||||||
|
// Parent doesn't exist, try to resolve what we can recursively
|
||||||
|
try realdirpath(parentURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the last component (which may not exist)
|
||||||
|
return resolvedParent.appendingPathComponent(lastComponent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,12 +311,56 @@ public extension File {
|
||||||
return fileSize.intValue
|
return fileSize.intValue
|
||||||
}
|
}
|
||||||
|
|
||||||
static func stat(_ url: URL) throws -> FileStat {
|
static func fileStatus(_ url: URL) throws -> FileStat {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
|
||||||
|
guard result == 0 else {
|
||||||
|
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||||
}
|
}
|
||||||
|
|
||||||
static func lstat(_ url: URL) throws -> FileStat {
|
return FileStat(
|
||||||
fatalError("Not implemented")
|
dev: Int(statBuf.st_dev),
|
||||||
|
ino: Int(statBuf.st_ino),
|
||||||
|
mode: Int(statBuf.st_mode),
|
||||||
|
nlink: Int(statBuf.st_nlink),
|
||||||
|
uid: Int(statBuf.st_uid),
|
||||||
|
gid: Int(statBuf.st_gid),
|
||||||
|
rdev: Int(statBuf.st_rdev),
|
||||||
|
size: Int64(statBuf.st_size),
|
||||||
|
blksize: Int(statBuf.st_blksize),
|
||||||
|
blocks: Int64(statBuf.st_blocks),
|
||||||
|
atime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_atimespec.tv_sec)),
|
||||||
|
mtime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_mtimespec.tv_sec)),
|
||||||
|
ctime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_ctimespec.tv_sec)),
|
||||||
|
birthtime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_birthtimespec.tv_sec)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func linkStatus(_ url: URL) throws -> FileStat {
|
||||||
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { lstat($0, &statBuf) }
|
||||||
|
|
||||||
|
guard result == 0 else {
|
||||||
|
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileStat(
|
||||||
|
dev: Int(statBuf.st_dev),
|
||||||
|
ino: Int(statBuf.st_ino),
|
||||||
|
mode: Int(statBuf.st_mode),
|
||||||
|
nlink: Int(statBuf.st_nlink),
|
||||||
|
uid: Int(statBuf.st_uid),
|
||||||
|
gid: Int(statBuf.st_gid),
|
||||||
|
rdev: Int(statBuf.st_rdev),
|
||||||
|
size: Int64(statBuf.st_size),
|
||||||
|
blksize: Int(statBuf.st_blksize),
|
||||||
|
blocks: Int64(statBuf.st_blocks),
|
||||||
|
atime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_atimespec.tv_sec)),
|
||||||
|
mtime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_mtimespec.tv_sec)),
|
||||||
|
ctime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_ctimespec.tv_sec)),
|
||||||
|
birthtime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_birthtimespec.tv_sec)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,27 +384,42 @@ public extension File {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isSymlink(_ url: URL) -> Bool {
|
static func isSymlink(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { lstat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return false }
|
||||||
|
return (statBuf.st_mode & S_IFMT) == S_IFLNK
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isBlockDevice(_ url: URL) -> Bool {
|
static func isBlockDevice(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return false }
|
||||||
|
return (statBuf.st_mode & S_IFMT) == S_IFBLK
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isCharDevice(_ url: URL) -> Bool {
|
static func isCharDevice(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return false }
|
||||||
|
return (statBuf.st_mode & S_IFMT) == S_IFCHR
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isPipe(_ url: URL) -> Bool {
|
static func isPipe(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return false }
|
||||||
|
return (statBuf.st_mode & S_IFMT) == S_IFIFO
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isSocket(_ url: URL) -> Bool {
|
static func isSocket(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return false }
|
||||||
|
return (statBuf.st_mode & S_IFMT) == S_IFSOCK
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isEmpty(_ url: URL) throws -> Bool {
|
static func isEmpty(_ url: URL) throws -> Bool {
|
||||||
let size = try self.size(url)
|
let size = try size(url)
|
||||||
return size == 0
|
return size == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,66 +433,97 @@ public extension File {
|
||||||
|
|
||||||
public extension File {
|
public extension File {
|
||||||
static func isReadable(_ url: URL) -> Bool {
|
static func isReadable(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
url.path.withCString { access($0, R_OK) } == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isWritable(_ url: URL) -> Bool {
|
static func isWritable(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
url.path.withCString { access($0, W_OK) } == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isExecutable(_ url: URL) -> Bool {
|
static func isExecutable(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
url.path.withCString { access($0, X_OK) } == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isOwned(_ url: URL) -> Bool {
|
static func isOwned(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return false }
|
||||||
|
return statBuf.st_uid == getuid()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isGroupOwned(_ url: URL) -> Bool {
|
static func isGroupOwned(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return false }
|
||||||
|
return statBuf.st_gid == getgid()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isWorldReadable(_ url: URL) -> Int? {
|
static func isWorldReadable(_ url: URL) -> Int? {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return nil }
|
||||||
|
|
||||||
|
// Check if world readable (other read bit)
|
||||||
|
if (statBuf.st_mode & S_IROTH) != 0 {
|
||||||
|
return Int(statBuf.st_mode & 0o777)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isWorldWritable(_ url: URL) -> Int? {
|
static func isWorldWritable(_ url: URL) -> Int? {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return nil }
|
||||||
|
|
||||||
|
// Check if world writable (other write bit)
|
||||||
|
if (statBuf.st_mode & S_IWOTH) != 0 {
|
||||||
|
return Int(statBuf.st_mode & 0o777)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isSetuid(_ url: URL) -> Bool {
|
static func isSetuid(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return false }
|
||||||
|
return (statBuf.st_mode & S_ISUID) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isSetgid(_ url: URL) -> Bool {
|
static func isSetgid(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return false }
|
||||||
|
return (statBuf.st_mode & S_ISGID) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isSticky(_ url: URL) -> Bool {
|
static func isSticky(_ url: URL) -> Bool {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { stat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return false }
|
||||||
|
return (statBuf.st_mode & S_ISVTX) != 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Static File Operations
|
// MARK: - Static File Operations
|
||||||
|
|
||||||
public extension File {
|
public extension File {
|
||||||
static func chmod(_ url: URL, permissions: Int) throws {
|
static func chmod(_: URL, permissions _: Int) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func chown(_ url: URL, owner: Int? = nil, group: Int? = nil) throws {
|
static func chown(_: URL, owner _: Int? = nil, group _: Int? = nil) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func lchmod(_ url: URL, permissions: Int) throws {
|
static func lchmod(_: URL, permissions _: Int) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func lchown(_ url: URL, owner: Int? = nil, group: Int? = nil) throws {
|
static func lchown(_: URL, owner _: Int? = nil, group _: Int? = nil) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func link(source: URL, destination: URL) throws {
|
static func link(source _: URL, destination _: URL) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -418,11 +548,11 @@ public extension File {
|
||||||
// Use replaceItem - it works whether destination exists or not
|
// Use replaceItem - it works whether destination exists or not
|
||||||
// and provides atomic replacement when it does exist
|
// and provides atomic replacement when it does exist
|
||||||
_ = try FileManager.default.replaceItem(
|
_ = try FileManager.default.replaceItem(
|
||||||
at: destination, withItemAt: source, backupItemName: nil, resultingItemURL: nil
|
at: destination, withItemAt: source, backupItemName: nil, resultingItemURL: nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func truncate(_ url: URL, to size: Int) throws {
|
static func truncate(_: URL, to _: Int) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -438,19 +568,19 @@ public extension File {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func utime(_ url: URL, atime: Date, mtime: Date) throws {
|
static func utime(_: URL, atime _: Date, mtime _: Date) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func lutime(_ url: URL, atime: Date, mtime: Date) throws {
|
static func lutime(_: URL, atime _: Date, mtime _: Date) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func mkfifo(_ url: URL, permissions: Int = 0o666) throws {
|
static func mkfifo(_: URL, permissions _: Int = 0o666) throws {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func identical(_ url1: URL, _ url2: URL) throws -> Bool {
|
static func identical(_: URL, _: URL) throws -> Bool {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -458,7 +588,7 @@ public extension File {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func umask(_ mask: Int) -> Int {
|
static func umask(_: Int) -> Int {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -466,7 +596,7 @@ public extension File {
|
||||||
// MARK: - Pattern Matching
|
// MARK: - Pattern Matching
|
||||||
|
|
||||||
public extension File {
|
public extension File {
|
||||||
static func fnmatch(pattern: String, path: String, flags: FnmatchFlags = []) -> Bool {
|
static func fnmatch(pattern _: String, path _: String, flags _: FnmatchFlags = []) -> Bool {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -515,18 +645,39 @@ public enum LockOperation {
|
||||||
// MARK: - File Type enum
|
// MARK: - File Type enum
|
||||||
|
|
||||||
public enum FileType: String {
|
public enum FileType: String {
|
||||||
case file = "file"
|
case file
|
||||||
case directory = "directory"
|
case directory
|
||||||
case characterSpecial = "characterSpecial"
|
case characterSpecial
|
||||||
case blockSpecial = "blockSpecial"
|
case blockSpecial
|
||||||
case fifo = "fifo"
|
case fifo
|
||||||
case link = "link"
|
case link
|
||||||
case socket = "socket"
|
case socket
|
||||||
case unknown = "unknown"
|
case unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension File {
|
public extension File {
|
||||||
static func ftype(_ url: URL) -> FileType {
|
static func ftype(_ url: URL) -> FileType {
|
||||||
fatalError("Not implemented")
|
var statBuf = stat()
|
||||||
|
let result = url.path.withCString { lstat($0, &statBuf) }
|
||||||
|
guard result == 0 else { return .unknown }
|
||||||
|
|
||||||
|
switch statBuf.st_mode & S_IFMT {
|
||||||
|
case S_IFREG:
|
||||||
|
return .file
|
||||||
|
case S_IFDIR:
|
||||||
|
return .directory
|
||||||
|
case S_IFLNK:
|
||||||
|
return .link
|
||||||
|
case S_IFBLK:
|
||||||
|
return .blockSpecial
|
||||||
|
case S_IFCHR:
|
||||||
|
return .characterSpecial
|
||||||
|
case S_IFIFO:
|
||||||
|
return .fifo
|
||||||
|
case S_IFSOCK:
|
||||||
|
return .socket
|
||||||
|
default:
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// FileOtterTests.swift
|
// DirTests.swift
|
||||||
// FileOtterTests
|
// FileOtterTests
|
||||||
//
|
//
|
||||||
// Created by Sami Samhuri on 2024-04-24.
|
// Created by Sami Samhuri on 2024-04-24.
|
||||||
|
|
@ -145,7 +145,7 @@ final class DirTests: XCTestCase {
|
||||||
let children = try Dir.children(tempDir)
|
let children = try Dir.children(tempDir)
|
||||||
XCTAssertEqual(children.count, 3)
|
XCTAssertEqual(children.count, 3)
|
||||||
|
|
||||||
let childNames = children.map { $0.lastPathComponent }.sorted()
|
let childNames = children.map(\.lastPathComponent).sorted()
|
||||||
XCTAssertEqual(childNames, ["file1.txt", "file2.txt", "subdir"])
|
XCTAssertEqual(childNames, ["file1.txt", "file2.txt", "subdir"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,7 +240,7 @@ final class DirTests: XCTestCase {
|
||||||
let results = Dir.glob(base: tempDir, "*.txt")
|
let results = Dir.glob(base: tempDir, "*.txt")
|
||||||
XCTAssertEqual(results.count, 2)
|
XCTAssertEqual(results.count, 2)
|
||||||
|
|
||||||
let filenames = results.map { $0.lastPathComponent }.sorted()
|
let filenames = results.map(\.lastPathComponent).sorted()
|
||||||
XCTAssertEqual(filenames, ["file1.txt", "file2.txt"])
|
XCTAssertEqual(filenames, ["file1.txt", "file2.txt"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,7 +266,7 @@ final class DirTests: XCTestCase {
|
||||||
let results = Dir.glob(base: tempDir, "??.txt")
|
let results = Dir.glob(base: tempDir, "??.txt")
|
||||||
XCTAssertEqual(results.count, 2)
|
XCTAssertEqual(results.count, 2)
|
||||||
|
|
||||||
let filenames = results.map { $0.lastPathComponent }.sorted()
|
let filenames = results.map(\.lastPathComponent).sorted()
|
||||||
XCTAssertEqual(filenames, ["a1.txt", "b2.txt"])
|
XCTAssertEqual(filenames, ["a1.txt", "b2.txt"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -464,7 +464,7 @@ final class DirTests: XCTestCase {
|
||||||
XCTAssertEqual(result, "block result")
|
XCTAssertEqual(result, "block result")
|
||||||
XCTAssertTrue(fileCreated)
|
XCTAssertTrue(fileCreated)
|
||||||
|
|
||||||
if let tmpDirInBlock = tmpDirInBlock {
|
if let tmpDirInBlock {
|
||||||
XCTAssertFalse(FileManager.default.fileExists(atPath: tmpDirInBlock.path))
|
XCTAssertFalse(FileManager.default.fileExists(atPath: tmpDirInBlock.path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
final class FileFnmatchTests: XCTestCase {
|
final class FileFnmatchTests: XCTestCase {
|
||||||
|
|
||||||
// MARK: - Basic Pattern Tests
|
// MARK: - Basic Pattern Tests
|
||||||
|
|
||||||
func testExactMatch() throws {
|
func testExactMatch() throws {
|
||||||
|
|
|
||||||
|
|
@ -104,22 +104,56 @@ final class FileInfoTests: XCTestCase {
|
||||||
// MARK: - Stat Tests
|
// MARK: - Stat Tests
|
||||||
|
|
||||||
func testStat() throws {
|
func testStat() throws {
|
||||||
// TODO: Implement
|
let stat = try File.fileStatus(testFile)
|
||||||
// File.stat(url) returns FileStat object
|
|
||||||
|
// Verify basic properties
|
||||||
|
XCTAssertGreaterThan(stat.ino, 0) // inode should be positive
|
||||||
|
XCTAssertGreaterThan(stat.uid, 0) // uid should be positive
|
||||||
|
XCTAssertGreaterThan(stat.gid, 0) // gid should be positive
|
||||||
|
XCTAssertEqual(stat.size, 12) // "Test content" is 12 bytes
|
||||||
|
|
||||||
|
// Verify times are reasonable
|
||||||
|
XCTAssertLessThan(Date().timeIntervalSince(stat.mtime), 3600)
|
||||||
|
XCTAssertLessThan(Date().timeIntervalSince(stat.atime), 3600)
|
||||||
|
XCTAssertNotNil(stat.birthtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testStatThrowsForNonExistent() throws {
|
func testStatThrowsForNonExistent() throws {
|
||||||
// TODO: Implement
|
let nonExistent = tempDir.appendingPathComponent("nonexistent")
|
||||||
|
XCTAssertThrowsError(try File.fileStatus(nonExistent))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLstat() throws {
|
func testLstat() throws {
|
||||||
// TODO: Implement
|
// For regular files, lstat should be same as stat
|
||||||
// File.lstat(url) doesn't follow symlinks
|
let lstat = try File.linkStatus(testFile)
|
||||||
|
let stat = try File.fileStatus(testFile)
|
||||||
|
|
||||||
|
XCTAssertEqual(lstat.size, stat.size)
|
||||||
|
XCTAssertEqual(lstat.ino, stat.ino)
|
||||||
|
XCTAssertEqual(lstat.mode, stat.mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLstatForSymlink() throws {
|
func testLstatForSymlink() throws {
|
||||||
// TODO: Implement
|
// Create a larger target file
|
||||||
// Create symlink and verify lstat returns symlink stats, not target
|
let targetFile = tempDir.appendingPathComponent("target.txt")
|
||||||
|
let targetContent = "This is the target file content"
|
||||||
|
try targetContent.write(to: targetFile, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
|
// Create symlink
|
||||||
|
let symlinkURL = tempDir.appendingPathComponent("symlink.txt")
|
||||||
|
try FileManager.default.createSymbolicLink(at: symlinkURL, withDestinationURL: targetFile)
|
||||||
|
|
||||||
|
let lstat = try File.linkStatus(symlinkURL)
|
||||||
|
let stat = try File.fileStatus(symlinkURL)
|
||||||
|
|
||||||
|
// lstat should return symlink's own stats (smaller size)
|
||||||
|
// stat should follow the symlink to the target (larger size)
|
||||||
|
XCTAssertNotEqual(lstat.size, stat.size)
|
||||||
|
XCTAssertEqual(stat.size, Int64(targetContent.data(using: .utf8)!.count))
|
||||||
|
|
||||||
|
// lstat should indicate it's a symlink via mode
|
||||||
|
let isLink = (lstat.mode & 0o170000) == 0o120000 // S_IFLNK
|
||||||
|
XCTAssertTrue(isLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Instance Method Tests
|
// MARK: - Instance Method Tests
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,17 @@ import XCTest
|
||||||
|
|
||||||
final class FilePathTests: XCTestCase {
|
final class FilePathTests: XCTestCase {
|
||||||
var tempDir: URL!
|
var tempDir: URL!
|
||||||
|
var originalWorkingDirectory: String!
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
override func setUpWithError() throws {
|
||||||
|
originalWorkingDirectory = FileManager.default.currentDirectoryPath
|
||||||
tempDir = URL.temporaryDirectory
|
tempDir = URL.temporaryDirectory
|
||||||
.appendingPathComponent("FilePathTests-\(UUID().uuidString)")
|
.appendingPathComponent("FilePathTests-\(UUID().uuidString)")
|
||||||
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
override func tearDownWithError() throws {
|
||||||
|
FileManager.default.changeCurrentDirectoryPath(originalWorkingDirectory)
|
||||||
if FileManager.default.fileExists(atPath: tempDir.path) {
|
if FileManager.default.fileExists(atPath: tempDir.path) {
|
||||||
try FileManager.default.removeItem(at: tempDir)
|
try FileManager.default.removeItem(at: tempDir)
|
||||||
}
|
}
|
||||||
|
|
@ -162,45 +165,110 @@ final class FilePathTests: XCTestCase {
|
||||||
// MARK: - absolutePath Tests
|
// MARK: - absolutePath Tests
|
||||||
|
|
||||||
func testAbsolutePath() throws {
|
func testAbsolutePath() throws {
|
||||||
// TODO: Implement
|
// Test already absolute path
|
||||||
// Converts relative to absolute
|
let absoluteURL = URL(fileURLWithPath: "/usr/bin/swift")
|
||||||
|
XCTAssertEqual(File.absolutePath(absoluteURL).path, "/usr/bin/swift")
|
||||||
|
|
||||||
|
// Test relative path - URL constructor will use current directory
|
||||||
|
FileManager.default.changeCurrentDirectoryPath(tempDir.path)
|
||||||
|
let relativeURL = URL(fileURLWithPath: "file.txt")
|
||||||
|
let absPath = File.absolutePath(relativeURL)
|
||||||
|
// The URL is already absolute at this point, we just normalize it
|
||||||
|
XCTAssertTrue(absPath.path.hasSuffix("file.txt"))
|
||||||
|
XCTAssertTrue(absPath.path.hasPrefix("/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAbsolutePathWithBase() throws {
|
func testAbsolutePathNormalization() throws {
|
||||||
// TODO: Implement
|
// Test that .. and . are resolved
|
||||||
|
let pathWithDots = URL(fileURLWithPath: "/usr/../bin/./swift")
|
||||||
|
XCTAssertEqual(File.absolutePath(pathWithDots).path, "/bin/swift")
|
||||||
|
|
||||||
|
// Test multiple .. segments
|
||||||
|
let pathWithMultipleDots = URL(fileURLWithPath: "/usr/local/../../bin")
|
||||||
|
XCTAssertEqual(File.absolutePath(pathWithMultipleDots).path, "/bin")
|
||||||
|
|
||||||
|
// Test trailing slash removal
|
||||||
|
let pathWithTrailingSlash = URL(fileURLWithPath: "/usr/bin/")
|
||||||
|
XCTAssertEqual(File.absolutePath(pathWithTrailingSlash).path, "/usr/bin")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - expandPath Tests
|
// MARK: - expandPath Tests
|
||||||
|
|
||||||
func testExpandPath() throws {
|
func testExpandPath() throws {
|
||||||
// TODO: Implement
|
let homeDir = FileManager.default.homeDirectoryForCurrentUser
|
||||||
// File.expandPath("~") => home directory
|
|
||||||
// File.expandPath("~/Documents") => home/Documents
|
|
||||||
}
|
|
||||||
|
|
||||||
func testExpandPathWithRelative() throws {
|
// Test expanding ~
|
||||||
// TODO: Implement
|
let expanded1 = File.expandPath("~")
|
||||||
|
XCTAssertEqual(expanded1.path, homeDir.path)
|
||||||
|
|
||||||
|
// Test expanding ~/Documents
|
||||||
|
let expanded2 = File.expandPath("~/Documents")
|
||||||
|
XCTAssertEqual(expanded2.path, homeDir.appendingPathComponent("Documents").path)
|
||||||
|
|
||||||
|
// Test regular path (no expansion needed)
|
||||||
|
let expanded3 = File.expandPath("/usr/bin")
|
||||||
|
XCTAssertEqual(expanded3.path, "/usr/bin")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - realpath Tests
|
// MARK: - realpath Tests
|
||||||
|
|
||||||
func testRealpath() throws {
|
func testRealpath() throws {
|
||||||
// TODO: Implement
|
// Create a real file
|
||||||
// Resolves symlinks, all components must exist
|
let fileURL = tempDir.appendingPathComponent("realfile.txt")
|
||||||
|
try "test content".write(to: fileURL, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
|
// Test resolving a real file
|
||||||
|
let resolved = try File.realpath(fileURL)
|
||||||
|
XCTAssertEqual(resolved.path, fileURL.path)
|
||||||
|
|
||||||
|
// Create a symlink
|
||||||
|
let symlinkURL = tempDir.appendingPathComponent("symlink.txt")
|
||||||
|
try FileManager.default.createSymbolicLink(at: symlinkURL, withDestinationURL: fileURL)
|
||||||
|
|
||||||
|
// Test resolving symlink
|
||||||
|
let resolvedSymlink = try File.realpath(symlinkURL)
|
||||||
|
XCTAssertEqual(resolvedSymlink.path, fileURL.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRealpathThrowsForNonExistent() throws {
|
func testRealpathThrowsForNonExistent() throws {
|
||||||
// TODO: Implement
|
let nonExistentURL = tempDir.appendingPathComponent("nonexistent.txt")
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try File.realpath(nonExistentURL)) { error in
|
||||||
|
// Should throw file not found error
|
||||||
|
let nsError = error as NSError
|
||||||
|
XCTAssertEqual(nsError.domain, NSCocoaErrorDomain)
|
||||||
|
XCTAssertEqual(nsError.code, CocoaError.fileNoSuchFile.rawValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - realdirpath Tests
|
// MARK: - realdirpath Tests
|
||||||
|
|
||||||
func testRealdirpath() throws {
|
func testRealdirpath() throws {
|
||||||
// TODO: Implement
|
// Create a directory
|
||||||
// Resolves symlinks, last component may not exist
|
let dirURL = tempDir.appendingPathComponent("realdir")
|
||||||
|
try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
// Test resolving existing directory
|
||||||
|
let resolved = try File.realdirpath(dirURL)
|
||||||
|
XCTAssertEqual(resolved.path, dirURL.path)
|
||||||
|
|
||||||
|
// Create a symlink to the directory
|
||||||
|
let symlinkDirURL = tempDir.appendingPathComponent("symlinkdir")
|
||||||
|
try FileManager.default.createSymbolicLink(at: symlinkDirURL, withDestinationURL: dirURL)
|
||||||
|
|
||||||
|
// Test resolving symlinked directory
|
||||||
|
let resolvedSymlink = try File.realdirpath(symlinkDirURL)
|
||||||
|
XCTAssertEqual(resolvedSymlink.path, dirURL.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRealdirpathWithNonExistentLast() throws {
|
func testRealdirpathWithNonExistentLast() throws {
|
||||||
// TODO: Implement
|
// Create a real directory
|
||||||
|
let dirURL = tempDir.appendingPathComponent("realdir")
|
||||||
|
try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
// Test with non-existent file in existing directory
|
||||||
|
let nonExistentFile = dirURL.appendingPathComponent("future-file.txt")
|
||||||
|
let resolved = try File.realdirpath(nonExistentFile)
|
||||||
|
XCTAssertEqual(resolved.path, nonExistentFile.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,74 +38,151 @@ final class FilePermissionTests: XCTestCase {
|
||||||
// MARK: - Basic Permission Tests
|
// MARK: - Basic Permission Tests
|
||||||
|
|
||||||
func testIsReadable() throws {
|
func testIsReadable() throws {
|
||||||
// TODO: Implement
|
// Normal files should be readable
|
||||||
// File.isReadable(url) returns true for readable files
|
XCTAssertTrue(File.isReadable(testFile))
|
||||||
|
|
||||||
|
// System files are generally readable
|
||||||
|
XCTAssertTrue(File.isReadable(URL(fileURLWithPath: "/etc/hosts")))
|
||||||
|
|
||||||
|
// Non-existent files are not readable
|
||||||
|
let nonExistent = tempDir.appendingPathComponent("nonexistent")
|
||||||
|
XCTAssertFalse(File.isReadable(nonExistent))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsWritable() throws {
|
func testIsWritable() throws {
|
||||||
// TODO: Implement
|
// Files we created should be writable
|
||||||
// File.isWritable(url) returns true for writable files
|
XCTAssertTrue(File.isWritable(testFile))
|
||||||
|
|
||||||
|
// System files are generally not writable
|
||||||
|
XCTAssertFalse(File.isWritable(URL(fileURLWithPath: "/etc/hosts")))
|
||||||
|
|
||||||
|
// Non-existent files are not writable
|
||||||
|
let nonExistent = tempDir.appendingPathComponent("nonexistent")
|
||||||
|
XCTAssertFalse(File.isWritable(nonExistent))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsExecutable() throws {
|
func testIsExecutable() throws {
|
||||||
// TODO: Implement
|
// Make the script executable
|
||||||
// File.isExecutable(url) returns true for executable files
|
try FileManager.default.setAttributes(
|
||||||
|
[.posixPermissions: 0o755],
|
||||||
|
ofItemAtPath: executableFile.path,
|
||||||
|
)
|
||||||
|
XCTAssertTrue(File.isExecutable(executableFile))
|
||||||
|
|
||||||
|
// System executables
|
||||||
|
XCTAssertTrue(File.isExecutable(URL(fileURLWithPath: "/bin/ls")))
|
||||||
|
XCTAssertTrue(File.isExecutable(URL(fileURLWithPath: "/usr/bin/swift")))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsExecutableForNonExecutable() throws {
|
func testIsExecutableForNonExecutable() throws {
|
||||||
// TODO: Implement
|
// Regular text files are not executable
|
||||||
// File.isExecutable(url) returns false for non-executable
|
XCTAssertFalse(File.isExecutable(testFile))
|
||||||
|
XCTAssertFalse(File.isExecutable(readOnlyFile))
|
||||||
|
|
||||||
|
// Non-existent files are not executable
|
||||||
|
let nonExistent = tempDir.appendingPathComponent("nonexistent")
|
||||||
|
XCTAssertFalse(File.isExecutable(nonExistent))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Ownership Tests
|
// MARK: - Ownership Tests
|
||||||
|
|
||||||
func testIsOwned() throws {
|
func testIsOwned() throws {
|
||||||
// TODO: Implement
|
// Files we create should be owned by us
|
||||||
// File.isOwned(url) returns true for files owned by effective user
|
XCTAssertTrue(File.isOwned(testFile))
|
||||||
|
XCTAssertTrue(File.isOwned(readOnlyFile))
|
||||||
|
XCTAssertTrue(File.isOwned(executableFile))
|
||||||
|
|
||||||
|
// System files may not be owned by us
|
||||||
|
// This depends on the user running the test
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsGroupOwned() throws {
|
func testIsGroupOwned() throws {
|
||||||
// TODO: Implement
|
// Files we create should be owned by our effective group
|
||||||
// File.isGroupOwned(url) returns true for files owned by effective group
|
XCTAssertTrue(File.isGroupOwned(testFile))
|
||||||
|
XCTAssertTrue(File.isGroupOwned(readOnlyFile))
|
||||||
|
XCTAssertTrue(File.isGroupOwned(executableFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - World Permission Tests
|
// MARK: - World Permission Tests
|
||||||
|
|
||||||
func testIsWorldReadable() throws {
|
func testIsWorldReadable() throws {
|
||||||
// TODO: Implement
|
// Make file world readable
|
||||||
// File.isWorldReadable(url) returns permission bits if world readable
|
try FileManager.default.setAttributes(
|
||||||
|
[.posixPermissions: 0o644],
|
||||||
|
ofItemAtPath: testFile.path,
|
||||||
|
)
|
||||||
|
|
||||||
|
let perms = File.isWorldReadable(testFile)
|
||||||
|
XCTAssertNotNil(perms)
|
||||||
|
if let perms {
|
||||||
|
XCTAssertEqual(perms & 0o004, 0o004) // Check world read bit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsWorldReadableForPrivate() throws {
|
func testIsWorldReadableForPrivate() throws {
|
||||||
// TODO: Implement
|
// Make file not world readable
|
||||||
// File.isWorldReadable(url) returns nil if not world readable
|
try FileManager.default.setAttributes(
|
||||||
|
[.posixPermissions: 0o640],
|
||||||
|
ofItemAtPath: readOnlyFile.path,
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertNil(File.isWorldReadable(readOnlyFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsWorldWritable() throws {
|
func testIsWorldWritable() throws {
|
||||||
// TODO: Implement
|
// Make file world writable (dangerous in practice!)
|
||||||
// File.isWorldWritable(url) returns permission bits if world writable
|
try FileManager.default.setAttributes(
|
||||||
|
[.posixPermissions: 0o666],
|
||||||
|
ofItemAtPath: testFile.path,
|
||||||
|
)
|
||||||
|
|
||||||
|
let perms = File.isWorldWritable(testFile)
|
||||||
|
XCTAssertNotNil(perms)
|
||||||
|
if let perms {
|
||||||
|
XCTAssertEqual(perms & 0o002, 0o002) // Check world write bit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsWorldWritableForProtected() throws {
|
func testIsWorldWritableForProtected() throws {
|
||||||
// TODO: Implement
|
// Make file not world writable
|
||||||
// File.isWorldWritable(url) returns nil if not world writable
|
try FileManager.default.setAttributes(
|
||||||
|
[.posixPermissions: 0o644],
|
||||||
|
ofItemAtPath: readOnlyFile.path,
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertNil(File.isWorldWritable(readOnlyFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Special Bit Tests
|
// MARK: - Special Bit Tests
|
||||||
|
|
||||||
func testIsSetuid() throws {
|
func testIsSetuid() throws {
|
||||||
// TODO: Implement
|
// Setuid is rarely used on regular files
|
||||||
// File.isSetuid(url) returns true if setuid bit is set
|
// Most files should not have setuid
|
||||||
|
XCTAssertFalse(File.isSetuid(testFile))
|
||||||
|
|
||||||
|
// /usr/bin/sudo typically has setuid (if it exists)
|
||||||
|
let sudo = URL(fileURLWithPath: "/usr/bin/sudo")
|
||||||
|
if FileManager.default.fileExists(atPath: sudo.path) {
|
||||||
|
// This might be true on some systems
|
||||||
|
_ = File.isSetuid(sudo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsSetgid() throws {
|
func testIsSetgid() throws {
|
||||||
// TODO: Implement
|
// Setgid is rarely used on regular files
|
||||||
// File.isSetgid(url) returns true if setgid bit is set
|
XCTAssertFalse(File.isSetgid(testFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsSticky() throws {
|
func testIsSticky() throws {
|
||||||
// TODO: Implement
|
// Sticky bit is typically set on /tmp
|
||||||
// File.isSticky(url) returns true if sticky bit is set
|
let tmpDir = URL(fileURLWithPath: "/tmp")
|
||||||
|
if FileManager.default.fileExists(atPath: tmpDir.path) {
|
||||||
|
// /tmp usually has sticky bit
|
||||||
|
_ = File.isSticky(tmpDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular files should not have sticky bit
|
||||||
|
XCTAssertFalse(File.isSticky(testFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - chmod Tests
|
// MARK: - chmod Tests
|
||||||
|
|
|
||||||
|
|
@ -101,33 +101,70 @@ final class FileTypeTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsSymlink() throws {
|
func testIsSymlink() throws {
|
||||||
// TODO: Implement
|
// Create symlink to test file
|
||||||
// Create symlink and test File.isSymlink(url)
|
let symlinkURL = tempDir.appendingPathComponent("symlink.txt")
|
||||||
|
try FileManager.default.createSymbolicLink(at: symlinkURL, withDestinationURL: testFile)
|
||||||
|
XCTAssertTrue(File.isSymlink(symlinkURL))
|
||||||
|
|
||||||
|
// Create symlink to directory
|
||||||
|
let dirSymlinkURL = tempDir.appendingPathComponent("dirlink")
|
||||||
|
try FileManager.default.createSymbolicLink(at: dirSymlinkURL, withDestinationURL: testDir)
|
||||||
|
XCTAssertTrue(File.isSymlink(dirSymlinkURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsSymlinkForRegularFile() throws {
|
func testIsSymlinkForRegularFile() throws {
|
||||||
// TODO: Implement
|
// Regular files are not symlinks
|
||||||
// File.isSymlink(url) returns false for regular files
|
XCTAssertFalse(File.isSymlink(testFile))
|
||||||
|
|
||||||
|
// Directories are not symlinks
|
||||||
|
XCTAssertFalse(File.isSymlink(testDir))
|
||||||
|
|
||||||
|
// Non-existent paths are not symlinks
|
||||||
|
let nonExistent = tempDir.appendingPathComponent("nonexistent")
|
||||||
|
XCTAssertFalse(File.isSymlink(nonExistent))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsBlockDevice() throws {
|
func testIsBlockDevice() throws {
|
||||||
// TODO: Implement
|
// Block devices are rare on macOS, but /dev/disk* exists
|
||||||
// File.isBlockDevice(url) - may need special test file
|
// This test might fail in sandboxed environments
|
||||||
|
if FileManager.default.fileExists(atPath: "/dev/disk0") {
|
||||||
|
XCTAssertTrue(File.isBlockDevice(URL(fileURLWithPath: "/dev/disk0")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular files are not block devices
|
||||||
|
XCTAssertFalse(File.isBlockDevice(testFile))
|
||||||
|
XCTAssertFalse(File.isBlockDevice(testDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsCharDevice() throws {
|
func testIsCharDevice() throws {
|
||||||
// TODO: Implement
|
// /dev/null is always a character device
|
||||||
// File.isCharDevice(url) - test with /dev/null or similar
|
let devNull = URL(fileURLWithPath: "/dev/null")
|
||||||
|
XCTAssertTrue(File.isCharDevice(devNull))
|
||||||
|
|
||||||
|
// /dev/random is also a character device
|
||||||
|
let devRandom = URL(fileURLWithPath: "/dev/random")
|
||||||
|
if FileManager.default.fileExists(atPath: devRandom.path) {
|
||||||
|
XCTAssertTrue(File.isCharDevice(devRandom))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular files are not character devices
|
||||||
|
XCTAssertFalse(File.isCharDevice(testFile))
|
||||||
|
XCTAssertFalse(File.isCharDevice(testDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsPipe() throws {
|
func testIsPipe() throws {
|
||||||
// TODO: Implement
|
// Creating FIFOs requires mkfifo system call
|
||||||
// File.isPipe(url) - create FIFO and test
|
// Skip this test for now as it requires additional implementation
|
||||||
|
// Regular files are not pipes
|
||||||
|
XCTAssertFalse(File.isPipe(testFile))
|
||||||
|
XCTAssertFalse(File.isPipe(testDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsSocket() throws {
|
func testIsSocket() throws {
|
||||||
// TODO: Implement
|
// Unix domain sockets are rare and hard to create in tests
|
||||||
// File.isSocket(url) - create socket and test
|
// Regular files are not sockets
|
||||||
|
XCTAssertFalse(File.isSocket(testFile))
|
||||||
|
XCTAssertFalse(File.isSocket(testDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Empty/Zero Tests
|
// MARK: - Empty/Zero Tests
|
||||||
|
|
@ -167,42 +204,58 @@ final class FileTypeTests: XCTestCase {
|
||||||
// MARK: - ftype Tests
|
// MARK: - ftype Tests
|
||||||
|
|
||||||
func testFtypeForFile() throws {
|
func testFtypeForFile() throws {
|
||||||
// TODO: Implement
|
XCTAssertEqual(File.ftype(testFile), .file)
|
||||||
// File.ftype(url) returns .file
|
|
||||||
|
// Create another file to test
|
||||||
|
let anotherFile = tempDir.appendingPathComponent("another.dat")
|
||||||
|
try Data().write(to: anotherFile)
|
||||||
|
XCTAssertEqual(File.ftype(anotherFile), .file)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFtypeForDirectory() throws {
|
func testFtypeForDirectory() throws {
|
||||||
// TODO: Implement
|
XCTAssertEqual(File.ftype(testDir), .directory)
|
||||||
// File.ftype(url) returns .directory
|
XCTAssertEqual(File.ftype(tempDir), .directory)
|
||||||
|
XCTAssertEqual(File.ftype(URL(fileURLWithPath: "/")), .directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFtypeForSymlink() throws {
|
func testFtypeForSymlink() throws {
|
||||||
// TODO: Implement
|
let symlinkURL = tempDir.appendingPathComponent("link.txt")
|
||||||
// File.ftype(url) returns .link
|
try FileManager.default.createSymbolicLink(at: symlinkURL, withDestinationURL: testFile)
|
||||||
|
XCTAssertEqual(File.ftype(symlinkURL), .link)
|
||||||
|
|
||||||
|
// Symlink to directory
|
||||||
|
let dirSymlinkURL = tempDir.appendingPathComponent("dirlink")
|
||||||
|
try FileManager.default.createSymbolicLink(at: dirSymlinkURL, withDestinationURL: testDir)
|
||||||
|
XCTAssertEqual(File.ftype(dirSymlinkURL), .link)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFtypeForCharDevice() throws {
|
func testFtypeForCharDevice() throws {
|
||||||
// TODO: Implement
|
// /dev/null is a character device
|
||||||
// File.ftype(url) returns .characterSpecial
|
let devNull = URL(fileURLWithPath: "/dev/null")
|
||||||
|
XCTAssertEqual(File.ftype(devNull), .characterSpecial)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFtypeForBlockDevice() throws {
|
func testFtypeForBlockDevice() throws {
|
||||||
// TODO: Implement
|
// Block devices are rare on macOS
|
||||||
// File.ftype(url) returns .blockSpecial
|
// This test might fail in sandboxed environments
|
||||||
|
if FileManager.default.fileExists(atPath: "/dev/disk0") {
|
||||||
|
XCTAssertEqual(File.ftype(URL(fileURLWithPath: "/dev/disk0")), .blockSpecial)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFtypeForFifo() throws {
|
func testFtypeForFifo() throws {
|
||||||
// TODO: Implement
|
// FIFOs require special creation
|
||||||
// File.ftype(url) returns .fifo
|
// Skip for now
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFtypeForSocket() throws {
|
func testFtypeForSocket() throws {
|
||||||
// TODO: Implement
|
// Sockets require special creation
|
||||||
// File.ftype(url) returns .socket
|
// Skip for now
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFtypeForUnknown() throws {
|
func testFtypeForUnknown() throws {
|
||||||
// TODO: Implement
|
// Non-existent files return unknown
|
||||||
// File.ftype(url) returns .unknown for unrecognized types
|
let nonExistent = tempDir.appendingPathComponent("nonexistent")
|
||||||
|
XCTAssertEqual(File.ftype(nonExistent), .unknown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue