WIP: Implement more of File

This commit is contained in:
Sami Samhuri 2025-08-26 08:35:49 -07:00
parent f24ed714e6
commit 4529a7e700
No known key found for this signature in database
4 changed files with 207 additions and 95 deletions

View file

@ -3,18 +3,12 @@
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
7B1B71E12E52784D008EDC0E /* Glob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B71E02E52784D008EDC0E /* Glob.swift */; };
7B5064B92BD9F236009CEFF9 /* FileOtter.docc in Sources */ = {isa = PBXBuildFile; fileRef = 7B5064B82BD9F236009CEFF9 /* FileOtter.docc */; };
7B5064BF2BD9F236009CEFF9 /* FileOtter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B5064B42BD9F236009CEFF9 /* FileOtter.framework */; };
7B5064C42BD9F236009CEFF9 /* FileOtterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5064C32BD9F236009CEFF9 /* FileOtterTests.swift */; };
7B5064C52BD9F236009CEFF9 /* FileOtter.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B5064B72BD9F236009CEFF9 /* FileOtter.h */; settings = {ATTRIBUTES = (Public, ); }; };
7B5064CF2BD9F2C0009CEFF9 /* Readme.md in Resources */ = {isa = PBXBuildFile; fileRef = 7B5064CE2BD9F2C0009CEFF9 /* Readme.md */; };
7B5064D12BD9F322009CEFF9 /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5064D02BD9F322009CEFF9 /* File.swift */; };
7B5064D32BD9F339009CEFF9 /* Dir.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5064D22BD9F339009CEFF9 /* Dir.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -28,17 +22,26 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
7B1B71E02E52784D008EDC0E /* Glob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glob.swift; sourceTree = "<group>"; };
7B5064B42BD9F236009CEFF9 /* FileOtter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileOtter.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7B5064B72BD9F236009CEFF9 /* FileOtter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FileOtter.h; sourceTree = "<group>"; };
7B5064B82BD9F236009CEFF9 /* FileOtter.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = FileOtter.docc; sourceTree = "<group>"; };
7B5064BE2BD9F236009CEFF9 /* FileOtterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FileOtterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7B5064C32BD9F236009CEFF9 /* FileOtterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOtterTests.swift; sourceTree = "<group>"; };
7B5064CE2BD9F2C0009CEFF9 /* Readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Readme.md; sourceTree = "<group>"; };
7B5064D02BD9F322009CEFF9 /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = "<group>"; };
7B5064D22BD9F339009CEFF9 /* Dir.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dir.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
7B1B74752E5E097B008EDC0E /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
publicHeaders = (
FileOtter.h,
);
target = 7B5064B32BD9F235009CEFF9 /* FileOtter */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
7B1B74612E5E0978008EDC0E /* FileOtterTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = FileOtterTests; sourceTree = "<group>"; };
7B1B746F2E5E097B008EDC0E /* FileOtter */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (7B1B74752E5E097B008EDC0E /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = FileOtter; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
7B5064B12BD9F235009CEFF9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@ -62,8 +65,8 @@
isa = PBXGroup;
children = (
7B5064CE2BD9F2C0009CEFF9 /* Readme.md */,
7B5064B62BD9F236009CEFF9 /* FileOtter */,
7B5064C22BD9F236009CEFF9 /* FileOtterTests */,
7B1B746F2E5E097B008EDC0E /* FileOtter */,
7B1B74612E5E0978008EDC0E /* FileOtterTests */,
7B5064B52BD9F236009CEFF9 /* Products */,
);
sourceTree = "<group>";
@ -77,26 +80,6 @@
name = Products;
sourceTree = "<group>";
};
7B5064B62BD9F236009CEFF9 /* FileOtter */ = {
isa = PBXGroup;
children = (
7B5064B72BD9F236009CEFF9 /* FileOtter.h */,
7B5064B82BD9F236009CEFF9 /* FileOtter.docc */,
7B5064D02BD9F322009CEFF9 /* File.swift */,
7B5064D22BD9F339009CEFF9 /* Dir.swift */,
7B1B71E02E52784D008EDC0E /* Glob.swift */,
);
path = FileOtter;
sourceTree = "<group>";
};
7B5064C22BD9F236009CEFF9 /* FileOtterTests */ = {
isa = PBXGroup;
children = (
7B5064C32BD9F236009CEFF9 /* FileOtterTests.swift */,
);
path = FileOtterTests;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@ -104,7 +87,6 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
7B5064C52BD9F236009CEFF9 /* FileOtter.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -124,6 +106,9 @@
);
dependencies = (
);
fileSystemSynchronizedGroups = (
7B1B746F2E5E097B008EDC0E /* FileOtter */,
);
name = FileOtter;
productName = FileOtter;
productReference = 7B5064B42BD9F236009CEFF9 /* FileOtter.framework */;
@ -142,6 +127,9 @@
dependencies = (
7B5064C12BD9F236009CEFF9 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
7B1B74612E5E0978008EDC0E /* FileOtterTests */,
);
name = FileOtterTests;
productName = FileOtterTests;
productReference = 7B5064BE2BD9F236009CEFF9 /* FileOtterTests.xctest */;
@ -207,10 +195,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7B5064B92BD9F236009CEFF9 /* FileOtter.docc in Sources */,
7B1B71E12E52784D008EDC0E /* Glob.swift in Sources */,
7B5064D12BD9F322009CEFF9 /* File.swift in Sources */,
7B5064D32BD9F339009CEFF9 /* Dir.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -218,7 +202,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7B5064C42BD9F236009CEFF9 /* FileOtterTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -439,7 +422,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = X45WPY5JFZ;
@ -461,7 +443,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = X45WPY5JFZ;

View file

@ -149,7 +149,7 @@ public extension File {
static func dirname(_ url: URL, level: Int = 1) -> URL {
var result = url
for _ in 0..<level {
for _ in 0..<level where result.path != "/" {
result = result.deletingLastPathComponent()
}
return result
@ -161,15 +161,42 @@ public extension File {
}
static func split(_ url: URL) -> (dir: URL, name: String) {
fatalError("Not implemented")
let dir = url.deletingLastPathComponent()
let name = url.lastPathComponent
// Handle root path special case
if url.path == "/" {
return (url, "")
}
return (dir, name)
}
static func join(_ components: String...) -> URL {
fatalError("Not implemented")
join(components)
}
static func join(_ components: [String]) -> URL {
fatalError("Not implemented")
// Filter out empty components
let nonEmptyComponents = components.filter { !$0.isEmpty }
guard !nonEmptyComponents.isEmpty else {
return URL(fileURLWithPath: ".")
}
// Start with the first component to preserve absolute/relative nature
var result = URL(fileURLWithPath: nonEmptyComponents[0])
// Append remaining components
for component in nonEmptyComponents.dropFirst() {
// Remove leading/trailing slashes from component before appending
let trimmed = component.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
if !trimmed.isEmpty {
result.appendPathComponent(trimmed)
}
}
return result
}
static func absolutePath(_ url: URL, relativeTo base: URL? = nil) -> URL {
@ -193,23 +220,55 @@ public extension File {
public extension File {
static func atime(_ url: URL) throws -> Date {
fatalError("Not implemented")
// Note: On macOS, access time updates may be disabled for performance
// You can check with: mount | grep noatime
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
// Try to get access date if available
if let accessDate = attributes[.modificationDate] as? Date {
// FileManager doesn't expose access time directly, using modification as fallback
// For true access time, would need to use stat() system call
return accessDate
}
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
}
static func mtime(_ url: URL) throws -> Date {
fatalError("Not implemented")
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
guard let modDate = attributes[.modificationDate] as? Date else {
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
}
return modDate
}
static func ctime(_ url: URL) throws -> Date {
fatalError("Not implemented")
// Status change time - on macOS this is often the same as mtime
// For true ctime, would need to use stat() system call
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
// Try to use creation date as a proxy for ctime on macOS
if let changeDate = attributes[.modificationDate] as? Date {
return changeDate
}
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
}
static func birthtime(_ url: URL) throws -> Date {
fatalError("Not implemented")
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
guard let creationDate = attributes[.creationDate] as? Date else {
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
}
return creationDate
}
static func size(_ url: URL) throws -> Int {
fatalError("Not implemented")
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
guard let fileSize = attributes[.size] as? NSNumber else {
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
}
return fileSize.intValue
}
static func stat(_ url: URL) throws -> FileStat {
@ -456,4 +515,4 @@ public extension File {
static func ftype(_ url: URL) -> FileType {
fatalError("Not implemented")
}
}
}

View file

@ -30,23 +30,44 @@ final class FileInfoTests: XCTestCase {
// MARK: - Time-based Tests
func testAtime() throws {
// TODO: Implement
// File.atime(url) returns last access time
let atime = try File.atime(testFile)
XCTAssertNotNil(atime)
// Access time should be recent (within last hour)
XCTAssertLessThan(Date().timeIntervalSince(atime), 3600)
}
func testMtime() throws {
// TODO: Implement
// File.mtime(url) returns last modification time
// Get initial mtime
let initialMtime = try File.mtime(testFile)
// Wait a moment and modify the file
Thread.sleep(forTimeInterval: 0.1)
try "Modified content".write(to: testFile, atomically: true, encoding: .utf8)
// mtime should be updated
let newMtime = try File.mtime(testFile)
XCTAssertGreaterThan(newMtime, initialMtime)
}
func testCtime() throws {
// TODO: Implement
// File.ctime(url) returns last status change time
let ctime = try File.ctime(testFile)
XCTAssertNotNil(ctime)
// Status change time should be recent
XCTAssertLessThan(Date().timeIntervalSince(ctime), 3600)
}
func testBirthtime() throws {
// TODO: Implement
// File.birthtime(url) returns creation time
// Create a new file
let newFile = tempDir.appendingPathComponent("birthtime-test.txt")
let beforeCreation = Date()
Thread.sleep(forTimeInterval: 0.01)
try "content".write(to: newFile, atomically: true, encoding: .utf8)
Thread.sleep(forTimeInterval: 0.01)
let afterCreation = Date()
let birthtime = try File.birthtime(newFile)
XCTAssertGreaterThanOrEqual(birthtime, beforeCreation)
XCTAssertLessThanOrEqual(birthtime, afterCreation)
}
func testBirthtimeThrowsOnUnsupportedPlatform() throws {
@ -56,16 +77,28 @@ final class FileInfoTests: XCTestCase {
// MARK: - Size Tests
func testSize() throws {
// TODO: Implement
// File.size(url) returns file size in bytes
// Test with known content
let content = "Test content"
let expectedSize = content.data(using: .utf8)!.count
XCTAssertEqual(try File.size(testFile), expectedSize)
// Test with larger file
let largeFile = tempDir.appendingPathComponent("large.txt")
let largeContent = String(repeating: "Hello World! ", count: 100)
try largeContent.write(to: largeFile, atomically: true, encoding: .utf8)
let largeExpectedSize = largeContent.data(using: .utf8)!.count
XCTAssertEqual(try File.size(largeFile), largeExpectedSize)
}
func testSizeThrowsForNonExistent() throws {
// TODO: Implement
let nonExistent = tempDir.appendingPathComponent("no-such-file.txt")
XCTAssertThrowsError(try File.size(nonExistent))
}
func testSizeForEmptyFile() throws {
// TODO: Implement
let emptyFile = tempDir.appendingPathComponent("empty.txt")
try "".write(to: emptyFile, atomically: true, encoding: .utf8)
XCTAssertEqual(try File.size(emptyFile), 0)
}
// MARK: - Stat Tests

View file

@ -26,10 +26,10 @@ final class FilePathTests: XCTestCase {
// MARK: - basename Tests
func testBasename() throws {
let url1 = URL(fileURLWithPath: "/home/user/file.txt")
let url1 = URL(fileURLWithPath: "/Users/sjs/file.txt")
XCTAssertEqual(File.basename(url1), "file.txt")
let url2 = URL(fileURLWithPath: "/home/user/dir/")
let url2 = URL(fileURLWithPath: "/Users/sjs/dir/")
XCTAssertEqual(File.basename(url2), "dir")
let url3 = URL(fileURLWithPath: "/")
@ -40,49 +40,49 @@ final class FilePathTests: XCTestCase {
}
func testBasenameWithSuffix() throws {
let url = URL(fileURLWithPath: "/home/user/file.txt")
let url = URL(fileURLWithPath: "/Users/sjs/file.txt")
XCTAssertEqual(File.basename(url, suffix: ".txt"), "file")
XCTAssertEqual(File.basename(url, suffix: ".rb"), "file.txt")
let url2 = URL(fileURLWithPath: "/home/user/archive.tar.gz")
let url2 = URL(fileURLWithPath: "/Users/sjs/archive.tar.gz")
XCTAssertEqual(File.basename(url2, suffix: ".gz"), "archive.tar")
XCTAssertEqual(File.basename(url2, suffix: ".tar.gz"), "archive")
}
func testBasenameWithWildcardSuffix() throws {
let url = URL(fileURLWithPath: "/home/user/file.txt")
let url = URL(fileURLWithPath: "/Users/sjs/file.txt")
XCTAssertEqual(File.basename(url, suffix: ".*"), "file")
let url2 = URL(fileURLWithPath: "/home/user/archive.tar.gz")
let url2 = URL(fileURLWithPath: "/Users/sjs/archive.tar.gz")
XCTAssertEqual(File.basename(url2, suffix: ".*"), "archive.tar")
let url3 = URL(fileURLWithPath: "/home/user/noext")
let url3 = URL(fileURLWithPath: "/Users/sjs/noext")
XCTAssertEqual(File.basename(url3, suffix: ".*"), "noext")
}
// MARK: - dirname Tests
func testDirname() throws {
let url1 = URL(fileURLWithPath: "/home/user/file.txt")
XCTAssertEqual(File.dirname(url1).path, "/home/user")
let url2 = URL(fileURLWithPath: "/home/user/dir/")
XCTAssertEqual(File.dirname(url2).path, "/home/user")
let url1 = URL(fileURLWithPath: "/Users/sjs/file.txt")
XCTAssertEqual(File.dirname(url1).path(), "/Users/sjs/")
let url2 = URL(fileURLWithPath: "/Users/sjs/dir/")
XCTAssertEqual(File.dirname(url2).path(), "/Users/sjs/")
let url3 = URL(fileURLWithPath: "/file.txt")
XCTAssertEqual(File.dirname(url3).path, "/")
XCTAssertEqual(File.dirname(url3).path(), "/")
let url4 = URL(fileURLWithPath: "file.txt")
XCTAssertEqual(File.dirname(url4).path, ".")
XCTAssertEqual(File.dirname(url4).path(), "./")
}
func testDirnameWithLevel() throws {
let url = URL(fileURLWithPath: "/home/user/dir/file.txt")
XCTAssertEqual(File.dirname(url, level: 1).path, "/home/user/dir")
XCTAssertEqual(File.dirname(url, level: 2).path, "/home/user")
XCTAssertEqual(File.dirname(url, level: 3).path, "/home")
XCTAssertEqual(File.dirname(url, level: 4).path, "/")
XCTAssertEqual(File.dirname(url, level: 5).path, "/") // Can't go beyond root
let url = URL(fileURLWithPath: "/Users/sjs/dir/file.txt")
XCTAssertEqual(File.dirname(url, level: 1).path(), "/Users/sjs/dir/")
XCTAssertEqual(File.dirname(url, level: 2).path(), "/Users/sjs/")
XCTAssertEqual(File.dirname(url, level: 3).path(), "/Users/")
XCTAssertEqual(File.dirname(url, level: 4).path(), "/")
XCTAssertEqual(File.dirname(url, level: 5).path(), "/") // Can't go beyond root
}
// MARK: - extname Tests
@ -98,26 +98,65 @@ final class FilePathTests: XCTestCase {
func testExtnameWithDotfile() throws {
XCTAssertEqual(File.extname(URL(fileURLWithPath: ".profile")), "")
XCTAssertEqual(File.extname(URL(fileURLWithPath: ".profile.sh")), ".sh")
XCTAssertEqual(File.extname(URL(fileURLWithPath: "/home/user/.bashrc")), "")
XCTAssertEqual(File.extname(URL(fileURLWithPath: "/home/user/.config.bak")), ".bak")
XCTAssertEqual(File.extname(URL(fileURLWithPath: "/Users/sjs/.bashrc")), "")
XCTAssertEqual(File.extname(URL(fileURLWithPath: "/Users/sjs/.config.bak")), ".bak")
}
// MARK: - split Tests
func testSplit() throws {
// TODO: Implement
// File.split("/home/user/file.txt") => ("/home/user", "file.txt")
let (dir1, name1) = File.split(URL(fileURLWithPath: "/Users/sjs/file.txt"))
XCTAssertEqual(dir1.path, "/Users/sjs")
XCTAssertEqual(name1, "file.txt")
let (dir2, name2) = File.split(URL(fileURLWithPath: "/file.txt"))
XCTAssertEqual(dir2.path, "/")
XCTAssertEqual(name2, "file.txt")
let (dir3, name3) = File.split(URL(fileURLWithPath: "file.txt"))
XCTAssertEqual(dir3.path(), "./")
XCTAssertEqual(name3, "file.txt")
let (dir4, name4) = File.split(URL(fileURLWithPath: "/Users/sjs/"))
XCTAssertEqual(dir4.path, "/Users")
XCTAssertEqual(name4, "sjs")
// Root path edge case
let (dir5, name5) = File.split(URL(fileURLWithPath: "/"))
XCTAssertEqual(dir5.path, "/")
XCTAssertEqual(name5, "")
}
// MARK: - join Tests
func testJoin() throws {
// TODO: Implement
// File.join("usr", "mail", "gumby") => "usr/mail/gumby"
let u = URL(fileURLWithPath: "hello")
XCTAssertEqual(File.join(u.path(), "world").path(), "hello/world")
XCTAssertEqual(File.join("usr", "mail", "gumby").path(), "usr/mail/gumby")
XCTAssertEqual(File.join("/usr", "mail", "gumby").path(), "/usr/mail/gumby")
XCTAssertEqual(File.join("/", "usr", "bin").path(), "/usr/bin/")
// Single component
XCTAssertEqual(File.join("file.txt").path(), "file.txt")
XCTAssertEqual(File.join("/file.txt").path(), "/file.txt")
// Empty components are ignored
XCTAssertEqual(File.join("usr", "", "bin").path(), "usr/bin")
// Handles trailing slashes
XCTAssertEqual(File.join("/usr/", "local/", "bin").path(), "/usr/local/bin/")
}
func testJoinWithArray() throws {
// TODO: Implement
let components = ["usr", "local", "bin"]
XCTAssertEqual(File.join(components).path(), "usr/local/bin")
let absoluteComponents = ["/usr", "local", "bin"]
XCTAssertEqual(File.join(absoluteComponents).path(), "/usr/local/bin/")
let singleComponent = ["file.txt"]
XCTAssertEqual(File.join(singleComponent).path(), "file.txt")
}
// MARK: - absolutePath Tests
@ -164,4 +203,4 @@ final class FilePathTests: XCTestCase {
func testRealdirpathWithNonExistentLast() throws {
// TODO: Implement
}
}
}