mirror of
https://github.com/samsonjs/FileOtter.git
synced 2026-03-25 08:25:49 +00:00
WIP: more File stuff
This commit is contained in:
parent
8b7910f165
commit
45409288d2
4 changed files with 554 additions and 148 deletions
|
|
@ -372,9 +372,10 @@ public extension File {
|
|||
}
|
||||
|
||||
static func isFile(_ url: URL) -> Bool {
|
||||
var isDirectory: ObjCBool = false
|
||||
let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
|
||||
return exists && !isDirectory.boolValue
|
||||
var statBuf = stat()
|
||||
let result = url.path.withCString { stat($0, &statBuf) }
|
||||
guard result == 0 else { return false }
|
||||
return (statBuf.st_mode & S_IFMT) == S_IFREG
|
||||
}
|
||||
|
||||
static func isDirectory(_ url: URL) -> Bool {
|
||||
|
|
@ -507,24 +508,100 @@ public extension File {
|
|||
// MARK: - Static File Operations
|
||||
|
||||
public extension File {
|
||||
static func chmod(_: URL, permissions _: Int) throws {
|
||||
fatalError("Not implemented")
|
||||
static func chmod(_ url: URL, permissions: Int) throws {
|
||||
let result = url.path.withCString { Darwin.chmod($0, mode_t(permissions)) }
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileWriteUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
}
|
||||
|
||||
static func chown(_: URL, owner _: Int? = nil, group _: Int? = nil) throws {
|
||||
fatalError("Not implemented")
|
||||
static func chown(_ url: URL, owner: Int? = nil, group: Int? = nil) throws {
|
||||
// Get current ownership if not changing both
|
||||
var currentOwner: uid_t = 0
|
||||
var currentGroup: gid_t = 0
|
||||
|
||||
if owner == nil || group == nil {
|
||||
var statBuf = stat()
|
||||
let result = url.path.withCString { stat($0, &statBuf) }
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
currentOwner = statBuf.st_uid
|
||||
currentGroup = statBuf.st_gid
|
||||
}
|
||||
|
||||
let newOwner = owner.map { uid_t($0) } ?? currentOwner
|
||||
let newGroup = group.map { gid_t($0) } ?? currentGroup
|
||||
|
||||
let result = url.path.withCString { Darwin.chown($0, newOwner, newGroup) }
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileWriteUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
}
|
||||
|
||||
static func lchmod(_: URL, permissions _: Int) throws {
|
||||
fatalError("Not implemented")
|
||||
static func lchmod(_ url: URL, permissions: Int) throws {
|
||||
// lchmod is not available on all systems, use lchflags as workaround
|
||||
// or fall back to fchmodat with AT_SYMLINK_NOFOLLOW
|
||||
#if os(macOS)
|
||||
// macOS doesn't have lchmod, permissions on symlinks are ignored
|
||||
// We could use fchmodat with AT_SYMLINK_NOFOLLOW but it's not always available
|
||||
// For now, this is a no-op on symlinks as per macOS behavior
|
||||
var statBuf = stat()
|
||||
let result = url.path.withCString { lstat($0, &statBuf) }
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
|
||||
// If it's not a symlink, use regular chmod
|
||||
if (statBuf.st_mode & S_IFMT) != S_IFLNK {
|
||||
try chmod(url, permissions: permissions)
|
||||
}
|
||||
// For symlinks, silently succeed (macOS behavior)
|
||||
#else
|
||||
let result = url.path.withCString { lchmod($0, mode_t(permissions)) }
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileWriteUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static func lchown(_: URL, owner _: Int? = nil, group _: Int? = nil) throws {
|
||||
fatalError("Not implemented")
|
||||
static func lchown(_ url: URL, owner: Int? = nil, group: Int? = nil) throws {
|
||||
// Get current ownership if not changing both
|
||||
var currentOwner: uid_t = 0
|
||||
var currentGroup: gid_t = 0
|
||||
|
||||
if owner == nil || group == nil {
|
||||
var statBuf = stat()
|
||||
let result = url.path.withCString { lstat($0, &statBuf) }
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
currentOwner = statBuf.st_uid
|
||||
currentGroup = statBuf.st_gid
|
||||
}
|
||||
|
||||
let newOwner = owner.map { uid_t($0) } ?? currentOwner
|
||||
let newGroup = group.map { gid_t($0) } ?? currentGroup
|
||||
|
||||
let result = url.path.withCString { Darwin.lchown($0, newOwner, newGroup) }
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileWriteUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
}
|
||||
|
||||
static func link(source _: URL, destination _: URL) throws {
|
||||
fatalError("Not implemented")
|
||||
static func link(source: URL, destination: URL) throws {
|
||||
// Create hard link
|
||||
let result = source.path.withCString { sourcePath in
|
||||
destination.path.withCString { destPath in
|
||||
Darwin.link(sourcePath, destPath)
|
||||
}
|
||||
}
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileWriteUnknown, userInfo: [
|
||||
NSFilePathErrorKey: destination.path,
|
||||
NSUnderlyingErrorKey: NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
static func symlink(source: URL, destination: URL) throws {
|
||||
|
|
@ -552,8 +629,11 @@ public extension File {
|
|||
)
|
||||
}
|
||||
|
||||
static func truncate(_: URL, to _: Int) throws {
|
||||
fatalError("Not implemented")
|
||||
static func truncate(_ url: URL, to size: Int) throws {
|
||||
let result = url.path.withCString { Darwin.truncate($0, off_t(size)) }
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileWriteUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
}
|
||||
|
||||
static func touch(_ url: URL) throws {
|
||||
|
|
@ -568,36 +648,98 @@ public extension File {
|
|||
}
|
||||
}
|
||||
|
||||
static func utime(_: URL, atime _: Date, mtime _: Date) throws {
|
||||
fatalError("Not implemented")
|
||||
static func utime(_ url: URL, atime: Date, mtime: Date) throws {
|
||||
var times = [timeval](repeating: timeval(), count: 2)
|
||||
|
||||
// Access time
|
||||
times[0].tv_sec = Int(atime.timeIntervalSince1970)
|
||||
times[0].tv_usec = Int32((atime.timeIntervalSince1970.truncatingRemainder(dividingBy: 1)) * 1_000_000)
|
||||
|
||||
// Modification time
|
||||
times[1].tv_sec = Int(mtime.timeIntervalSince1970)
|
||||
times[1].tv_usec = Int32((mtime.timeIntervalSince1970.truncatingRemainder(dividingBy: 1)) * 1_000_000)
|
||||
|
||||
let result = url.path.withCString { path in
|
||||
times.withUnsafeBufferPointer { timesPtr in
|
||||
Darwin.utimes(path, timesPtr.baseAddress)
|
||||
}
|
||||
}
|
||||
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileWriteUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
}
|
||||
|
||||
static func lutime(_: URL, atime _: Date, mtime _: Date) throws {
|
||||
fatalError("Not implemented")
|
||||
static func lutime(_ url: URL, atime: Date, mtime: Date) throws {
|
||||
// lutimes sets times on symlink itself (not the target)
|
||||
var times = [timeval](repeating: timeval(), count: 2)
|
||||
|
||||
// Access time
|
||||
times[0].tv_sec = Int(atime.timeIntervalSince1970)
|
||||
times[0].tv_usec = Int32((atime.timeIntervalSince1970.truncatingRemainder(dividingBy: 1)) * 1_000_000)
|
||||
|
||||
// Modification time
|
||||
times[1].tv_sec = Int(mtime.timeIntervalSince1970)
|
||||
times[1].tv_usec = Int32((mtime.timeIntervalSince1970.truncatingRemainder(dividingBy: 1)) * 1_000_000)
|
||||
|
||||
let result = url.path.withCString { path in
|
||||
times.withUnsafeBufferPointer { timesPtr in
|
||||
Darwin.lutimes(path, timesPtr.baseAddress)
|
||||
}
|
||||
}
|
||||
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileWriteUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
}
|
||||
|
||||
static func mkfifo(_: URL, permissions _: Int = 0o666) throws {
|
||||
fatalError("Not implemented")
|
||||
static func mkfifo(_ url: URL, permissions: Int = 0o666) throws {
|
||||
let result = url.path.withCString { Darwin.mkfifo($0, mode_t(permissions)) }
|
||||
guard result == 0 else {
|
||||
throw CocoaError(.fileWriteUnknown, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
}
|
||||
|
||||
static func identical(_: URL, _: URL) throws -> Bool {
|
||||
fatalError("Not implemented")
|
||||
static func identical(_ url1: URL, _ url2: URL) throws -> Bool {
|
||||
var stat1 = stat()
|
||||
var stat2 = stat()
|
||||
|
||||
let result1 = url1.path.withCString { stat($0, &stat1) }
|
||||
guard result1 == 0 else {
|
||||
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url1.path])
|
||||
}
|
||||
|
||||
let result2 = url2.path.withCString { stat($0, &stat2) }
|
||||
guard result2 == 0 else {
|
||||
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url2.path])
|
||||
}
|
||||
|
||||
// Files are identical if they have the same device and inode
|
||||
return stat1.st_dev == stat2.st_dev && stat1.st_ino == stat2.st_ino
|
||||
}
|
||||
|
||||
static func umask() -> Int {
|
||||
fatalError("Not implemented")
|
||||
// Get current umask by setting and restoring
|
||||
let current = Darwin.umask(0)
|
||||
Darwin.umask(current)
|
||||
return Int(current)
|
||||
}
|
||||
|
||||
static func umask(_: Int) -> Int {
|
||||
fatalError("Not implemented")
|
||||
static func umask(_ mask: Int) -> Int {
|
||||
let oldMask = Darwin.umask(mode_t(mask))
|
||||
return Int(oldMask)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Pattern Matching
|
||||
|
||||
public extension File {
|
||||
static func fnmatch(pattern _: String, path _: String, flags _: FnmatchFlags = []) -> Bool {
|
||||
fatalError("Not implemented")
|
||||
static func fnmatch(pattern: String, path: String, flags: FnmatchFlags = []) -> Bool {
|
||||
pattern.withCString { patternPtr in
|
||||
path.withCString { pathPtr in
|
||||
Darwin.fnmatch(patternPtr, pathPtr, flags.rawValue) == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -627,12 +769,11 @@ public struct FnmatchFlags: OptionSet, Sendable {
|
|||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let pathname = FnmatchFlags(rawValue: 1 << 0) // FNM_PATHNAME
|
||||
public static let noescape = FnmatchFlags(rawValue: 1 << 1) // FNM_NOESCAPE
|
||||
public static let period = FnmatchFlags(rawValue: 1 << 2) // FNM_PERIOD
|
||||
public static let casefold = FnmatchFlags(rawValue: 1 << 3) // FNM_CASEFOLD
|
||||
public static let extglob = FnmatchFlags(rawValue: 1 << 4) // FNM_EXTGLOB
|
||||
public static let dotmatch = FnmatchFlags(rawValue: 1 << 5) // FNM_DOTMATCH (custom)
|
||||
public static let pathname = FnmatchFlags(rawValue: FNM_PATHNAME)
|
||||
public static let noescape = FnmatchFlags(rawValue: FNM_NOESCAPE)
|
||||
public static let period = FnmatchFlags(rawValue: FNM_PERIOD)
|
||||
public static let casefold = FnmatchFlags(rawValue: FNM_CASEFOLD)
|
||||
public static let leadingDir = FnmatchFlags(rawValue: FNM_LEADING_DIR)
|
||||
}
|
||||
|
||||
public enum LockOperation {
|
||||
|
|
|
|||
|
|
@ -12,117 +12,148 @@ final class FileFnmatchTests: XCTestCase {
|
|||
// MARK: - Basic Pattern Tests
|
||||
|
||||
func testExactMatch() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("cat", "cat") => true
|
||||
// File.fnmatch("cat", "dog") => false
|
||||
XCTAssertTrue(File.fnmatch(pattern: "cat", path: "cat"))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "cat", path: "dog"))
|
||||
}
|
||||
|
||||
func testPartialMatch() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("cat", "category") => false (must match entire string)
|
||||
// Must match entire string
|
||||
XCTAssertFalse(File.fnmatch(pattern: "cat", path: "category"))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "cat", path: "bobcat"))
|
||||
}
|
||||
|
||||
// MARK: - Wildcard Tests
|
||||
|
||||
func testStarWildcard() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("c*", "cats") => true
|
||||
// File.fnmatch("c*t", "cat") => true
|
||||
// File.fnmatch("c*t", "c/a/b/t") => true
|
||||
XCTAssertTrue(File.fnmatch(pattern: "c*", path: "cats"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "c*t", path: "cat"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "c*t", path: "coat"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "*at", path: "cat"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "*at", path: "bat"))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "c*t", path: "dog"))
|
||||
|
||||
// Without pathname flag, * matches /
|
||||
XCTAssertTrue(File.fnmatch(pattern: "c*t", path: "c/a/b/t"))
|
||||
}
|
||||
|
||||
func testQuestionMarkWildcard() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("c?t", "cat") => true
|
||||
// File.fnmatch("c??t", "cat") => false
|
||||
}
|
||||
|
||||
func testDoubleStarWildcard() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("**/*.rb", "main.rb") => false
|
||||
// File.fnmatch("**/*.rb", "lib/song.rb") => true
|
||||
// File.fnmatch("**.rb", "main.rb") => true
|
||||
XCTAssertTrue(File.fnmatch(pattern: "c?t", path: "cat"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "c?t", path: "cot"))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "c??t", path: "cat"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "c??t", path: "coat"))
|
||||
|
||||
// ? doesn't match / with pathname flag
|
||||
XCTAssertFalse(File.fnmatch(pattern: "c?t", path: "c/t", flags: .pathname))
|
||||
}
|
||||
|
||||
// MARK: - Character Set Tests
|
||||
|
||||
func testCharacterSet() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("ca[a-z]", "cat") => true
|
||||
// File.fnmatch("ca[0-9]", "cat") => false
|
||||
XCTAssertTrue(File.fnmatch(pattern: "ca[a-z]", path: "cat"))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "ca[0-9]", path: "cat"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "ca[0-9]", path: "ca5"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "[abc]at", path: "cat"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "[abc]at", path: "bat"))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "[abc]at", path: "rat"))
|
||||
}
|
||||
|
||||
func testNegatedCharacterSet() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("ca[^t]", "cat") => false
|
||||
// File.fnmatch("ca[^t]", "cab") => true
|
||||
XCTAssertFalse(File.fnmatch(pattern: "ca[^t]", path: "cat"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "ca[^t]", path: "cab"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "ca[^t]", path: "can"))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "ca[^bcd]", path: "cab"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "ca[^bcd]", path: "cat"))
|
||||
}
|
||||
|
||||
// MARK: - Escape Tests
|
||||
|
||||
func testEscapedWildcard() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("\\?", "?") => true
|
||||
// File.fnmatch("\\*", "*") => true
|
||||
// With noescape flag, backslash is literal
|
||||
XCTAssertTrue(File.fnmatch(pattern: "\\*", path: "\\*", flags: .noescape))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "\\?", path: "\\?", flags: .noescape))
|
||||
|
||||
// Without noescape, backslash escapes the wildcard
|
||||
XCTAssertTrue(File.fnmatch(pattern: "\\*", path: "*"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "\\?", path: "?"))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "\\*", path: "anything"))
|
||||
}
|
||||
|
||||
func testEscapeInBrackets() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("[\\?]", "?") => true
|
||||
XCTAssertTrue(File.fnmatch(pattern: "[\\?]", path: "?"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "[\\*]", path: "*"))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "[\\?]", path: "a"))
|
||||
}
|
||||
|
||||
// MARK: - Flag Tests
|
||||
|
||||
func testCaseFoldFlag() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("cat", "CAT", flags: []) => false
|
||||
// File.fnmatch("cat", "CAT", flags: .casefold) => true
|
||||
XCTAssertFalse(File.fnmatch(pattern: "cat", path: "CAT", flags: []))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "cat", path: "CAT", flags: .casefold))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "CaT", path: "cat", flags: .casefold))
|
||||
}
|
||||
|
||||
func testPathnameFlag() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("*", "/", flags: []) => true
|
||||
// File.fnmatch("*", "/", flags: .pathname) => false
|
||||
// File.fnmatch("?", "/", flags: .pathname) => false
|
||||
// Without pathname flag, * matches /
|
||||
XCTAssertTrue(File.fnmatch(pattern: "*", path: "a/b", flags: []))
|
||||
|
||||
// With pathname flag, * doesn't match /
|
||||
XCTAssertFalse(File.fnmatch(pattern: "*", path: "a/b", flags: .pathname))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "a/*", path: "a/b", flags: .pathname))
|
||||
|
||||
// ? also doesn't match / with pathname flag
|
||||
XCTAssertFalse(File.fnmatch(pattern: "?", path: "/", flags: .pathname))
|
||||
}
|
||||
|
||||
func testPeriodFlag() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("*", ".profile", flags: []) => false (FNM_PERIOD by default)
|
||||
// File.fnmatch(".*", ".profile", flags: []) => true
|
||||
}
|
||||
|
||||
func testDotmatchFlag() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("*", ".profile", flags: .dotmatch) => true
|
||||
// By default, * doesn't match leading period (FNM_PERIOD is set)
|
||||
XCTAssertFalse(File.fnmatch(pattern: "*", path: ".profile", flags: .period))
|
||||
XCTAssertTrue(File.fnmatch(pattern: ".*", path: ".profile", flags: .period))
|
||||
|
||||
// Without period flag, * can match leading period
|
||||
XCTAssertTrue(File.fnmatch(pattern: "*", path: ".profile", flags: []))
|
||||
}
|
||||
|
||||
func testNoescapeFlag() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("\\a", "a", flags: []) => true
|
||||
// File.fnmatch("\\a", "\\a", flags: .noescape) => true
|
||||
// Without noescape, backslash escapes
|
||||
XCTAssertTrue(File.fnmatch(pattern: "\\a", path: "a", flags: []))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "\\a", path: "\\a", flags: []))
|
||||
|
||||
// With noescape, backslash is literal
|
||||
XCTAssertFalse(File.fnmatch(pattern: "\\a", path: "a", flags: .noescape))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "\\a", path: "\\a", flags: .noescape))
|
||||
}
|
||||
|
||||
func testExtglobFlag() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("c{at,ub}s", "cats", flags: []) => false
|
||||
// File.fnmatch("c{at,ub}s", "cats", flags: .extglob) => true
|
||||
// File.fnmatch("c{at,ub}s", "cubs", flags: .extglob) => true
|
||||
func testLeadingDirFlag() throws {
|
||||
// FNM_LEADING_DIR allows pattern to match a leading portion
|
||||
XCTAssertTrue(File.fnmatch(pattern: "*/foo", path: "bar/foo/baz", flags: .leadingDir))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "bar/foo", path: "bar/foo/baz", flags: .leadingDir))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "bar/foo", path: "bar/foo/baz", flags: []))
|
||||
}
|
||||
|
||||
// MARK: - Complex Pattern Tests
|
||||
|
||||
func testComplexGlobPatterns() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("**/foo", "a/b/c/foo", flags: .pathname) => true
|
||||
// File.fnmatch("**/foo", "/a/b/c/foo", flags: .pathname) => true
|
||||
// File.fnmatch("**/foo", "a/.b/c/foo", flags: .pathname) => false
|
||||
// File.fnmatch("**/foo", "a/.b/c/foo", flags: [.pathname, .dotmatch]) => true
|
||||
// Test various complex patterns
|
||||
XCTAssertTrue(File.fnmatch(pattern: "*.txt", path: "file.txt"))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "*.txt", path: "file.md"))
|
||||
|
||||
// Multiple wildcards
|
||||
XCTAssertTrue(File.fnmatch(pattern: "*.*", path: "file.txt"))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "test_*.rb", path: "test_file.rb"))
|
||||
XCTAssertFalse(File.fnmatch(pattern: "test_*.rb", path: "spec_file.rb"))
|
||||
}
|
||||
|
||||
func testHiddenFileMatching() throws {
|
||||
// TODO: Implement
|
||||
// File.fnmatch("*", "dave/.profile", flags: []) => true
|
||||
// File.fnmatch("**/.*", "a/.hidden", flags: .pathname) => true
|
||||
// Without special flags, * matches everything including paths with /
|
||||
XCTAssertTrue(File.fnmatch(pattern: "*", path: "dave/.profile", flags: []))
|
||||
|
||||
// With pathname flag, we need to be more specific
|
||||
XCTAssertFalse(File.fnmatch(pattern: "*", path: "dave/.profile", flags: .pathname))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "dave/*", path: "dave/.profile", flags: .pathname))
|
||||
XCTAssertTrue(File.fnmatch(pattern: "dave/.*", path: "dave/.profile", flags: .pathname))
|
||||
|
||||
// Hidden files in current directory
|
||||
XCTAssertFalse(File.fnmatch(pattern: "*", path: ".hidden", flags: .period))
|
||||
XCTAssertTrue(File.fnmatch(pattern: ".*", path: ".hidden", flags: .period))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,13 +33,29 @@ final class FileOperationTests: XCTestCase {
|
|||
// MARK: - Link Tests
|
||||
|
||||
func testLink() throws {
|
||||
// TODO: Implement
|
||||
// File.link(source, destination) creates hard link
|
||||
// Create hard link
|
||||
let hardLink = tempDir.appendingPathComponent("hardlink.txt")
|
||||
try File.link(source: sourceFile, destination: hardLink)
|
||||
|
||||
// Both files should exist
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: sourceFile.path))
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: hardLink.path))
|
||||
|
||||
// They should have the same content
|
||||
XCTAssertEqual(try String(contentsOf: hardLink), "Source content")
|
||||
|
||||
// They should be the same file (same inode)
|
||||
XCTAssertTrue(try File.identical(sourceFile, hardLink))
|
||||
}
|
||||
|
||||
func testLinkThrowsIfDestExists() throws {
|
||||
// TODO: Implement
|
||||
// File.link should not overwrite existing file
|
||||
try "Existing content".write(to: destFile, atomically: true, encoding: .utf8)
|
||||
|
||||
// Should throw because destination exists
|
||||
XCTAssertThrowsError(try File.link(source: sourceFile, destination: destFile))
|
||||
|
||||
// Destination should still have original content
|
||||
XCTAssertEqual(try String(contentsOf: destFile), "Existing content")
|
||||
}
|
||||
|
||||
func testSymlink() throws {
|
||||
|
|
@ -171,22 +187,51 @@ final class FileOperationTests: XCTestCase {
|
|||
// MARK: - Truncate Tests
|
||||
|
||||
func testTruncate() throws {
|
||||
// TODO: Implement
|
||||
// File.truncate(url, size) truncates file to size
|
||||
// Create file with content
|
||||
let file = tempDir.appendingPathComponent("truncate-test.txt")
|
||||
let originalContent = "This is a longer piece of content that will be truncated"
|
||||
try originalContent.write(to: file, atomically: true, encoding: .utf8)
|
||||
|
||||
// Truncate to 10 bytes
|
||||
try File.truncate(file, to: 10)
|
||||
|
||||
// File should be truncated
|
||||
XCTAssertEqual(try File.size(file), 10)
|
||||
XCTAssertEqual(try String(contentsOf: file), "This is a ")
|
||||
}
|
||||
|
||||
func testTruncateExpands() throws {
|
||||
// TODO: Implement
|
||||
// truncate can expand file with zero padding
|
||||
// Create small file
|
||||
let file = tempDir.appendingPathComponent("expand-test.txt")
|
||||
try "Short".write(to: file, atomically: true, encoding: .utf8)
|
||||
|
||||
// Expand to 20 bytes (should pad with zeros)
|
||||
try File.truncate(file, to: 20)
|
||||
|
||||
XCTAssertEqual(try File.size(file), 20)
|
||||
|
||||
// Read raw data to check padding
|
||||
let data = try Data(contentsOf: file)
|
||||
XCTAssertEqual(data.count, 20)
|
||||
|
||||
// First 5 bytes should be "Short"
|
||||
let shortData = "Short".data(using: .utf8)!
|
||||
XCTAssertEqual(data.prefix(5), shortData)
|
||||
|
||||
// Remaining bytes should be zeros
|
||||
for i in 5..<20 {
|
||||
XCTAssertEqual(data[i], 0)
|
||||
}
|
||||
}
|
||||
|
||||
func testTruncateThrowsForNonExistent() throws {
|
||||
// TODO: Implement
|
||||
let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt")
|
||||
XCTAssertThrowsError(try File.truncate(nonExistent, to: 10))
|
||||
}
|
||||
|
||||
func testInstanceTruncate() throws {
|
||||
// TODO: Implement
|
||||
// file.truncate(size) truncates open file
|
||||
// Skip for now as it requires File instance implementation
|
||||
throw XCTSkip("Instance methods not yet implemented")
|
||||
}
|
||||
|
||||
// MARK: - Touch Tests
|
||||
|
|
@ -222,60 +267,151 @@ final class FileOperationTests: XCTestCase {
|
|||
// MARK: - utime Tests
|
||||
|
||||
func testUtime() throws {
|
||||
// TODO: Implement
|
||||
// File.utime(url, atime, mtime) sets specific times
|
||||
// Create file
|
||||
let file = tempDir.appendingPathComponent("utime-test.txt")
|
||||
try "content".write(to: file, atomically: true, encoding: .utf8)
|
||||
|
||||
// Set specific times
|
||||
let atime = Date(timeIntervalSince1970: 1000000)
|
||||
let mtime = Date(timeIntervalSince1970: 2000000)
|
||||
|
||||
try File.utime(file, atime: atime, mtime: mtime)
|
||||
|
||||
// Verify times were set
|
||||
let newMtime = try File.mtime(file)
|
||||
XCTAssertEqual(newMtime.timeIntervalSince1970, mtime.timeIntervalSince1970, accuracy: 1.0)
|
||||
}
|
||||
|
||||
func testUtimeFollowsSymlinks() throws {
|
||||
// TODO: Implement
|
||||
// utime affects target of symlink
|
||||
// Create target and symlink
|
||||
let target = tempDir.appendingPathComponent("utime-target.txt")
|
||||
try "content".write(to: target, atomically: true, encoding: .utf8)
|
||||
|
||||
let link = tempDir.appendingPathComponent("utime-link.txt")
|
||||
try File.symlink(source: target, destination: link)
|
||||
|
||||
// Set times via symlink
|
||||
let atime = Date(timeIntervalSince1970: 1000000)
|
||||
let mtime = Date(timeIntervalSince1970: 2000000)
|
||||
|
||||
try File.utime(link, atime: atime, mtime: mtime)
|
||||
|
||||
// Target should have new times
|
||||
let targetMtime = try File.mtime(target)
|
||||
XCTAssertEqual(targetMtime.timeIntervalSince1970, mtime.timeIntervalSince1970, accuracy: 1.0)
|
||||
}
|
||||
|
||||
func testLutime() throws {
|
||||
// TODO: Implement
|
||||
// File.lutime(url, atime, mtime) sets symlink times
|
||||
// Create target and symlink
|
||||
let target = tempDir.appendingPathComponent("lutime-target.txt")
|
||||
try "content".write(to: target, atomically: true, encoding: .utf8)
|
||||
|
||||
let link = tempDir.appendingPathComponent("lutime-link.txt")
|
||||
try File.symlink(source: target, destination: link)
|
||||
|
||||
// Get original target mtime
|
||||
let originalTargetMtime = try File.mtime(target)
|
||||
|
||||
// Set times on symlink itself (not target)
|
||||
let atime = Date(timeIntervalSince1970: 1000000)
|
||||
let mtime = Date(timeIntervalSince1970: 2000000)
|
||||
|
||||
try File.lutime(link, atime: atime, mtime: mtime)
|
||||
|
||||
// Target should still have original time
|
||||
let targetMtime = try File.mtime(target)
|
||||
XCTAssertEqual(targetMtime.timeIntervalSince1970, originalTargetMtime.timeIntervalSince1970, accuracy: 1.0)
|
||||
|
||||
// Symlink should have new time (check with lstat)
|
||||
let linkStat = try File.linkStatus(link)
|
||||
XCTAssertEqual(linkStat.mtime.timeIntervalSince1970, mtime.timeIntervalSince1970, accuracy: 1.0)
|
||||
}
|
||||
|
||||
// MARK: - mkfifo Tests
|
||||
|
||||
func testMkfifo() throws {
|
||||
// TODO: Implement
|
||||
// File.mkfifo(url) creates named pipe
|
||||
// Create named pipe
|
||||
let fifo = tempDir.appendingPathComponent("test.fifo")
|
||||
try File.mkfifo(fifo)
|
||||
|
||||
// Verify it's a pipe
|
||||
XCTAssertTrue(File.isPipe(fifo))
|
||||
XCTAssertFalse(File.isFile(fifo))
|
||||
XCTAssertFalse(File.isDirectory(fifo))
|
||||
}
|
||||
|
||||
func testMkfifoWithPermissions() throws {
|
||||
// TODO: Implement
|
||||
// File.mkfifo(url, permissions) creates with specific perms
|
||||
// Create named pipe with specific permissions
|
||||
let fifo = tempDir.appendingPathComponent("test-perms.fifo")
|
||||
try File.mkfifo(fifo, permissions: 0o644)
|
||||
|
||||
// Verify it's a pipe
|
||||
XCTAssertTrue(File.isPipe(fifo))
|
||||
|
||||
// Verify permissions (masking out file type bits)
|
||||
let stat = try File.fileStatus(fifo)
|
||||
let perms = stat.mode & 0o777
|
||||
// Actual permissions may be affected by umask
|
||||
XCTAssertTrue(perms <= 0o644)
|
||||
}
|
||||
|
||||
func testMkfifoThrowsIfExists() throws {
|
||||
// TODO: Implement
|
||||
// Create a regular file
|
||||
let file = tempDir.appendingPathComponent("existing.txt")
|
||||
try "content".write(to: file, atomically: true, encoding: .utf8)
|
||||
|
||||
// Should throw when trying to create fifo with same name
|
||||
XCTAssertThrowsError(try File.mkfifo(file))
|
||||
|
||||
// Original file should still be a regular file
|
||||
XCTAssertTrue(File.isFile(file))
|
||||
XCTAssertFalse(File.isPipe(file))
|
||||
}
|
||||
|
||||
// MARK: - identical Tests
|
||||
|
||||
func testIdentical() throws {
|
||||
// TODO: Implement
|
||||
// File.identical(url1, url2) returns true for same file
|
||||
// Same file should be identical to itself
|
||||
XCTAssertTrue(try File.identical(sourceFile, sourceFile))
|
||||
}
|
||||
|
||||
func testIdenticalForHardLink() throws {
|
||||
// TODO: Implement
|
||||
// identical returns true for hard links to same file
|
||||
// Create hard link
|
||||
let hardLink = tempDir.appendingPathComponent("hardlink.txt")
|
||||
try File.link(source: sourceFile, destination: hardLink)
|
||||
|
||||
// Hard links point to same inode
|
||||
XCTAssertTrue(try File.identical(sourceFile, hardLink))
|
||||
XCTAssertTrue(try File.identical(hardLink, sourceFile))
|
||||
}
|
||||
|
||||
func testIdenticalForSymlink() throws {
|
||||
// TODO: Implement
|
||||
// identical returns true for symlink and target
|
||||
// Create symlink
|
||||
let symlink = tempDir.appendingPathComponent("symlink.txt")
|
||||
try File.symlink(source: sourceFile, destination: symlink)
|
||||
|
||||
// Symlink and target should be identical (stat follows symlinks)
|
||||
XCTAssertTrue(try File.identical(sourceFile, symlink))
|
||||
XCTAssertTrue(try File.identical(symlink, sourceFile))
|
||||
}
|
||||
|
||||
func testIdenticalForDifferent() throws {
|
||||
// TODO: Implement
|
||||
// identical returns false for different files
|
||||
// Create another file
|
||||
let otherFile = tempDir.appendingPathComponent("other.txt")
|
||||
try "Other content".write(to: otherFile, atomically: true, encoding: .utf8)
|
||||
|
||||
// Different files should not be identical
|
||||
XCTAssertFalse(try File.identical(sourceFile, otherFile))
|
||||
XCTAssertFalse(try File.identical(otherFile, sourceFile))
|
||||
}
|
||||
|
||||
func testIdenticalForSameContent() throws {
|
||||
// TODO: Implement
|
||||
// identical returns false for files with same content but different inodes
|
||||
// Create another file with same content
|
||||
let copyFile = tempDir.appendingPathComponent("copy.txt")
|
||||
try "Source content".write(to: copyFile, atomically: true, encoding: .utf8)
|
||||
|
||||
// Files with same content but different inodes are not identical
|
||||
XCTAssertFalse(try File.identical(sourceFile, copyFile))
|
||||
XCTAssertFalse(try File.identical(copyFile, sourceFile))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,56 +188,154 @@ final class FilePermissionTests: XCTestCase {
|
|||
// MARK: - chmod Tests
|
||||
|
||||
func testChmod() throws {
|
||||
// TODO: Implement
|
||||
// File.chmod(url, permissions) changes file permissions
|
||||
// Change file to read-only
|
||||
try File.chmod(testFile, permissions: 0o444)
|
||||
|
||||
// Verify permissions changed
|
||||
let stat = try File.fileStatus(testFile)
|
||||
let perms = stat.mode & 0o777
|
||||
XCTAssertEqual(perms, 0o444)
|
||||
|
||||
// File should still be readable but not writable
|
||||
XCTAssertTrue(File.isReadable(testFile))
|
||||
XCTAssertFalse(File.isWritable(testFile))
|
||||
|
||||
// Change back to read-write
|
||||
try File.chmod(testFile, permissions: 0o644)
|
||||
let stat2 = try File.fileStatus(testFile)
|
||||
let perms2 = stat2.mode & 0o777
|
||||
XCTAssertEqual(perms2, 0o644)
|
||||
}
|
||||
|
||||
func testChmodThrowsForNonExistent() throws {
|
||||
// TODO: Implement
|
||||
let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt")
|
||||
XCTAssertThrowsError(try File.chmod(nonExistent, permissions: 0o644))
|
||||
}
|
||||
|
||||
func testLchmod() throws {
|
||||
// TODO: Implement
|
||||
// File.lchmod(url, permissions) changes symlink permissions
|
||||
// Create a symlink
|
||||
let link = tempDir.appendingPathComponent("test-link")
|
||||
try File.symlink(source: testFile, destination: link)
|
||||
|
||||
// On macOS, lchmod is a no-op for symlinks
|
||||
// This should not throw but also won't change symlink permissions
|
||||
try File.lchmod(link, permissions: 0o777)
|
||||
|
||||
// The target file permissions should not be affected
|
||||
let targetStat = try File.fileStatus(testFile)
|
||||
let targetPerms = targetStat.mode & 0o777
|
||||
XCTAssertNotEqual(targetPerms, 0o777)
|
||||
}
|
||||
|
||||
func testInstanceChmod() throws {
|
||||
// TODO: Implement
|
||||
// file.chmod(permissions) changes open file permissions
|
||||
// Skip for now as it requires File instance implementation
|
||||
throw XCTSkip("Instance methods not yet implemented")
|
||||
}
|
||||
|
||||
// MARK: - chown Tests
|
||||
|
||||
func testChown() throws {
|
||||
// TODO: Implement
|
||||
// File.chown(url, owner, group) changes ownership
|
||||
// Note: May require special privileges
|
||||
// Get current ownership
|
||||
let stat = try File.fileStatus(testFile)
|
||||
let currentUid = stat.uid
|
||||
let currentGid = stat.gid
|
||||
|
||||
// Try to set to same owner/group (should always succeed)
|
||||
try File.chown(testFile, owner: currentUid, group: currentGid)
|
||||
|
||||
// Verify ownership unchanged
|
||||
let newStat = try File.fileStatus(testFile)
|
||||
XCTAssertEqual(newStat.uid, currentUid)
|
||||
XCTAssertEqual(newStat.gid, currentGid)
|
||||
|
||||
// Note: Changing to different owner usually requires root privileges
|
||||
// So we can't test that in normal unit tests
|
||||
}
|
||||
|
||||
func testChownWithNilValues() throws {
|
||||
// TODO: Implement
|
||||
// nil owner or group means don't change that value
|
||||
// Get current ownership
|
||||
let stat = try File.fileStatus(testFile)
|
||||
let currentUid = stat.uid
|
||||
let currentGid = stat.gid
|
||||
|
||||
// Change only owner (group stays same)
|
||||
try File.chown(testFile, owner: currentUid, group: nil)
|
||||
let stat1 = try File.fileStatus(testFile)
|
||||
XCTAssertEqual(stat1.uid, currentUid)
|
||||
XCTAssertEqual(stat1.gid, currentGid)
|
||||
|
||||
// Change only group (owner stays same)
|
||||
try File.chown(testFile, owner: nil, group: currentGid)
|
||||
let stat2 = try File.fileStatus(testFile)
|
||||
XCTAssertEqual(stat2.uid, currentUid)
|
||||
XCTAssertEqual(stat2.gid, currentGid)
|
||||
|
||||
// Change neither (no-op)
|
||||
try File.chown(testFile, owner: nil, group: nil)
|
||||
let stat3 = try File.fileStatus(testFile)
|
||||
XCTAssertEqual(stat3.uid, currentUid)
|
||||
XCTAssertEqual(stat3.gid, currentGid)
|
||||
}
|
||||
|
||||
func testLchown() throws {
|
||||
// TODO: Implement
|
||||
// File.lchown(url, owner, group) changes symlink ownership
|
||||
// Create a symlink
|
||||
let link = tempDir.appendingPathComponent("owner-link")
|
||||
try File.symlink(source: testFile, destination: link)
|
||||
|
||||
// Get current ownership of symlink
|
||||
let linkStat = try File.linkStatus(link)
|
||||
let currentUid = linkStat.uid
|
||||
let currentGid = linkStat.gid
|
||||
|
||||
// Try to set to same owner/group (should always succeed)
|
||||
try File.lchown(link, owner: currentUid, group: currentGid)
|
||||
|
||||
// Verify symlink ownership unchanged
|
||||
let newLinkStat = try File.linkStatus(link)
|
||||
XCTAssertEqual(newLinkStat.uid, currentUid)
|
||||
XCTAssertEqual(newLinkStat.gid, currentGid)
|
||||
|
||||
// Target file ownership should not be affected
|
||||
let targetStat = try File.fileStatus(testFile)
|
||||
XCTAssertEqual(targetStat.uid, currentUid)
|
||||
XCTAssertEqual(targetStat.gid, currentGid)
|
||||
}
|
||||
|
||||
func testInstanceChown() throws {
|
||||
// TODO: Implement
|
||||
// file.chown(owner, group) changes open file ownership
|
||||
// Skip for now as it requires File instance implementation
|
||||
throw XCTSkip("Instance methods not yet implemented")
|
||||
}
|
||||
|
||||
// MARK: - umask Tests
|
||||
|
||||
func testUmask() throws {
|
||||
// TODO: Implement
|
||||
// File.umask() returns current umask
|
||||
// Get current umask
|
||||
let currentMask = File.umask()
|
||||
|
||||
// Umask should be a reasonable value (typically 0o022 or 0o002)
|
||||
XCTAssertGreaterThanOrEqual(currentMask, 0)
|
||||
XCTAssertLessThan(currentMask, 0o777)
|
||||
}
|
||||
|
||||
func testUmaskSet() throws {
|
||||
// TODO: Implement
|
||||
// File.umask(mask) sets umask and returns previous value
|
||||
// Get current umask
|
||||
let originalMask = File.umask()
|
||||
|
||||
// Set new umask
|
||||
let newMask = 0o027
|
||||
let returnedMask = File.umask(newMask)
|
||||
|
||||
// Returned value should be the old mask
|
||||
XCTAssertEqual(returnedMask, originalMask)
|
||||
|
||||
// Current mask should be the new value
|
||||
let currentMask = File.umask()
|
||||
XCTAssertEqual(currentMask, newMask)
|
||||
|
||||
// Restore original umask
|
||||
_ = File.umask(originalMask)
|
||||
|
||||
// Verify restoration
|
||||
XCTAssertEqual(File.umask(), originalMask)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue