diff --git a/FileOtter.xcodeproj/project.pbxproj b/FileOtter.xcodeproj/project.pbxproj index b668c61..e1cdf55 100644 --- a/FileOtter.xcodeproj/project.pbxproj +++ b/FileOtter.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; - 7B5064B82BD9F236009CEFF9 /* FileOtter.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = FileOtter.docc; sourceTree = ""; }; 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 = ""; }; 7B5064CE2BD9F2C0009CEFF9 /* Readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Readme.md; sourceTree = ""; }; - 7B5064D02BD9F322009CEFF9 /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; - 7B5064D22BD9F339009CEFF9 /* Dir.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dir.swift; sourceTree = ""; }; /* 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 = ""; }; + 7B1B746F2E5E097B008EDC0E /* FileOtter */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (7B1B74752E5E097B008EDC0E /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = FileOtter; sourceTree = ""; }; +/* 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 = ""; @@ -77,26 +80,6 @@ name = Products; sourceTree = ""; }; - 7B5064B62BD9F236009CEFF9 /* FileOtter */ = { - isa = PBXGroup; - children = ( - 7B5064B72BD9F236009CEFF9 /* FileOtter.h */, - 7B5064B82BD9F236009CEFF9 /* FileOtter.docc */, - 7B5064D02BD9F322009CEFF9 /* File.swift */, - 7B5064D22BD9F339009CEFF9 /* Dir.swift */, - 7B1B71E02E52784D008EDC0E /* Glob.swift */, - ); - path = FileOtter; - sourceTree = ""; - }; - 7B5064C22BD9F236009CEFF9 /* FileOtterTests */ = { - isa = PBXGroup; - children = ( - 7B5064C32BD9F236009CEFF9 /* FileOtterTests.swift */, - ); - path = FileOtterTests; - sourceTree = ""; - }; /* 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; diff --git a/FileOtter/File.swift b/FileOtter/File.swift index b8bf2e7..56f00f8 100644 --- a/FileOtter/File.swift +++ b/FileOtter/File.swift @@ -149,7 +149,7 @@ public extension File { static func dirname(_ url: URL, level: Int = 1) -> URL { var result = url - for _ in 0.. (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") } -} \ No newline at end of file +} diff --git a/FileOtterTests/FileInfoTests.swift b/FileOtterTests/FileInfoTests.swift index 9d370fe..cfc499d 100644 --- a/FileOtterTests/FileInfoTests.swift +++ b/FileOtterTests/FileInfoTests.swift @@ -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 diff --git a/FileOtterTests/FilePathTests.swift b/FileOtterTests/FilePathTests.swift index c1cf347..551cd1d 100644 --- a/FileOtterTests/FilePathTests.swift +++ b/FileOtterTests/FilePathTests.swift @@ -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 } -} \ No newline at end of file +}