From 7b2faf7051629e6047912fb5d0d9c3fbbca90afc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 Aug 2025 16:06:33 +0000 Subject: [PATCH 1/6] Initial plan From 110347c54b4ac27af45eb120ea486e485a9e5d26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 Aug 2025 16:18:16 +0000 Subject: [PATCH 2/6] Fix Swift test compilation by adding conditional compilation for xattr and sandbox functionality Co-authored-by: linhay <15262434+linhay@users.noreply.github.com> --- Sources/STFilePath/STPath+Metadata.swift | 21 +++++++++++++++++++++ Tests/STFilePathTests/STFolderTests.swift | 3 ++- Tests/STFilePathTests/Untils.swift | 6 ++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Sources/STFilePath/STPath+Metadata.swift b/Sources/STFilePath/STPath+Metadata.swift index 8f0178b..f4e6999 100644 --- a/Sources/STFilePath/STPath+Metadata.swift +++ b/Sources/STFilePath/STPath+Metadata.swift @@ -6,6 +6,11 @@ // import Foundation +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#endif public extension STPathProtocol { @@ -55,12 +60,16 @@ struct STExtendedAttributes { /// - forName: The name of the attribute. /// - Throws: An error if the attribute cannot be set. func set(name: String, value: Data) throws { + #if canImport(Darwin) try url.path.withCString { fileSystemPath in let status = setxattr(fileSystemPath, name, value.withUnsafeBytes { $0.baseAddress! }, value.count, 0, 0) if status == -1 { throw STPathError(message: "[en] Failed to set extended attribute \(name). \n [zh] 设置扩展属性 \(name) 失败。", code: Int(errno)) } } + #else + throw STPathError(message: "[en] Extended attributes are not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) + #endif } /// [en] Returns the value of an extended attribute. @@ -69,6 +78,7 @@ struct STExtendedAttributes { /// - Returns: The value of the attribute. /// - Throws: An error if the attribute cannot be retrieved. func value(of name: String) throws -> Data { + #if canImport(Darwin) try url.path.withCString { fileSystemPath in let length = getxattr(fileSystemPath, name, nil, 0, 0, 0) if length == -1 { @@ -83,6 +93,9 @@ struct STExtendedAttributes { } return data } + #else + throw STPathError(message: "[en] Extended attributes are not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) + #endif } /// [en] Removes an extended attribute from the file or folder. @@ -90,12 +103,16 @@ struct STExtendedAttributes { /// - Parameter forName: The name of the attribute to remove. /// - Throws: An error if the attribute cannot be removed. func remove(of name: String) throws { + #if canImport(Darwin) try url.path.withCString { fileSystemPath in let status = removexattr(fileSystemPath, name, 0) if status == -1 { throw STPathError(message: "[en] Failed to remove extended attribute \(name). \n [zh] 删除扩展属性 \(name) 失败。", code: Int(errno)) } } + #else + throw STPathError(message: "[en] Extended attributes are not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) + #endif } /// [en] Returns a list of all extended attributes. @@ -103,6 +120,7 @@ struct STExtendedAttributes { /// - Returns: A list of attribute names. /// - Throws: An error if the attributes cannot be retrieved. func list() throws -> [String] { + #if canImport(Darwin) try url.path.withCString { fileSystemPath in let length = listxattr(fileSystemPath, nil, 0, 0) if length == -1 { @@ -115,5 +133,8 @@ struct STExtendedAttributes { } return buffer.split(separator: 0).compactMap { String(cString: Array($0), encoding: .utf8) } } + #else + throw STPathError(message: "[en] Extended attributes are not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) + #endif } } \ No newline at end of file diff --git a/Tests/STFilePathTests/STFolderTests.swift b/Tests/STFilePathTests/STFolderTests.swift index 867cfcc..5007b44 100644 --- a/Tests/STFilePathTests/STFolderTests.swift +++ b/Tests/STFilePathTests/STFolderTests.swift @@ -56,9 +56,9 @@ struct STFolderTests { try testFolder.delete() } + #if !os(Linux) @Test("Sanbox Operations") func testSanboxOperations() throws { - let home = try STFolder(sanbox: .home) #expect(home.url.path == NSHomeDirectory()) @@ -66,5 +66,6 @@ struct STFolderTests { let expectedDocuments = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) #expect(documents.url == expectedDocuments) } + #endif } diff --git a/Tests/STFilePathTests/Untils.swift b/Tests/STFilePathTests/Untils.swift index 4e5b9ff..f5c6277 100644 --- a/Tests/STFilePathTests/Untils.swift +++ b/Tests/STFilePathTests/Untils.swift @@ -9,5 +9,11 @@ import STFilePath import Foundation func createTestFolder() throws -> STFolder { + #if os(Linux) + // On Linux, use the temp directory directly since sandbox functionality is not available + let tempDir = FileManager.default.temporaryDirectory + return STFolder(tempDir).folder("testings/\(UUID().uuidString)") + #else try STFolder(sanbox: .temporary).folder("testings/\(UUID().uuidString)") + #endif } From 597d8b09eeeef12094482e23d454d027cbb94a27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 Aug 2025 16:20:47 +0000 Subject: [PATCH 3/6] Fix home directory path standardization on Linux to use NSHomeDirectory() Co-authored-by: linhay <15262434+linhay@users.noreply.github.com> --- Sources/STFilePath/STPathProtocol.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/STFilePath/STPathProtocol.swift b/Sources/STFilePath/STPathProtocol.swift index f235416..ad0f772 100644 --- a/Sources/STFilePath/STPathProtocol.swift +++ b/Sources/STFilePath/STPathProtocol.swift @@ -237,7 +237,7 @@ public extension STPathProtocol { static func standardizedPath(_ path: String) -> URL { if path == "~" { #if os(Linux) - return URL(fileURLWithPath: "/") + return URL(fileURLWithPath: NSHomeDirectory()) #else return STFolder.Sanbox.home.url #endif @@ -245,7 +245,7 @@ public extension STPathProtocol { var components = path.split(separator: "/").map({ $0.description }) components = Array(components.dropFirst()) #if os(Linux) - let home = [String]() + let home = NSHomeDirectory().split(separator: "/").map(\.description) #else let home = STFolder.Sanbox.home.url.path.split(separator: "/").map(\.description) #endif From d97973db79195ff9c7f8478b3ac1f83fdf8ac2d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 04:47:15 +0000 Subject: [PATCH 4/6] Add comprehensive test suites for STFilePath components Co-authored-by: linhay <15262434+linhay@users.noreply.github.com> --- Tests/STFilePathTests/STComparatorTests.swift | 183 +++++++++++ .../STFile+CryptoKitTests.swift | 139 +++++++++ Tests/STFilePathTests/STFile+MMAPTests.swift | 216 +++++++++++++ .../STFolder+SearchTests.swift | 290 ++++++++++++++++++ Tests/STFilePathTests/STJSONLinesTests.swift | 235 ++++++++++++++ .../STPath+MetadataTests.swift | 233 +++++++++++++- .../STPathErrorHandlingTests.swift | 248 +++++++++++++++ 7 files changed, 1534 insertions(+), 10 deletions(-) create mode 100644 Tests/STFilePathTests/STComparatorTests.swift create mode 100644 Tests/STFilePathTests/STFile+CryptoKitTests.swift create mode 100644 Tests/STFilePathTests/STFile+MMAPTests.swift create mode 100644 Tests/STFilePathTests/STFolder+SearchTests.swift create mode 100644 Tests/STFilePathTests/STJSONLinesTests.swift create mode 100644 Tests/STFilePathTests/STPathErrorHandlingTests.swift diff --git a/Tests/STFilePathTests/STComparatorTests.swift b/Tests/STFilePathTests/STComparatorTests.swift new file mode 100644 index 0000000..736cd46 --- /dev/null +++ b/Tests/STFilePathTests/STComparatorTests.swift @@ -0,0 +1,183 @@ +import Testing +import STFilePath +import Foundation + +@Suite("STComparator Tests") +struct STComparatorTests { + + @Test("Basic Compression and Decompression") + func testBasicCompressionDecompression() throws { + #if canImport(Compression) + let originalData = "This is a test string for compression. It should compress well because it has repeating patterns and common words.".data(using: .utf8)! + + // Test ZLIB compression + let compressedData = try STComparator.compress(originalData, algorithm: .zlib) + #expect(compressedData.count > 0) + #expect(compressedData.count < originalData.count) // Should be smaller + + let decompressedData = try STComparator.decompress(compressedData, algorithm: .zlib) + #expect(decompressedData == originalData) + #endif + } + + @Test("Different Compression Algorithms") + func testDifferentCompressionAlgorithms() throws { + #if canImport(Compression) + let testData = String(repeating: "ABCDEFGHIJ", count: 100).data(using: .utf8)! + + let algorithms: [STComparator.Algorithm] = [.lz4, .zlib, .lzfse, .lzma] + + for algorithm in algorithms { + let compressed = try STComparator.compress(testData, algorithm: algorithm) + #expect(compressed.count > 0) + + let decompressed = try STComparator.decompress(compressed, algorithm: algorithm) + #expect(decompressed == testData) + } + #endif + } + + @Test("Empty Data Compression") + func testEmptyDataCompression() throws { + #if canImport(Compression) + let emptyData = Data() + + let compressed = try STComparator.compress(emptyData, algorithm: .zlib) + let decompressed = try STComparator.decompress(compressed, algorithm: .zlib) + + #expect(decompressed == emptyData) + #endif + } + + @Test("Small Data Compression") + func testSmallDataCompression() throws { + #if canImport(Compression) + let smallData = "Hi".data(using: .utf8)! + + let compressed = try STComparator.compress(smallData, algorithm: .lz4) + let decompressed = try STComparator.decompress(compressed, algorithm: .lz4) + + #expect(decompressed == smallData) + #endif + } + + @Test("Large Data Compression") + func testLargeDataCompression() throws { + #if canImport(Compression) + // Create a large dataset with patterns that should compress well + let pattern = "This is a repeating pattern that should compress very well. " + let largeData = String(repeating: pattern, count: 1000).data(using: .utf8)! + + let compressed = try STComparator.compress(largeData, algorithm: .zlib) + #expect(compressed.count > 0) + #expect(compressed.count < largeData.count) // Should achieve good compression ratio + + let decompressed = try STComparator.decompress(compressed, algorithm: .zlib) + #expect(decompressed == largeData) + + // Verify compression ratio is reasonable (should be much smaller) + let compressionRatio = Double(compressed.count) / Double(largeData.count) + #expect(compressionRatio < 0.5) // Should compress to less than 50% of original size + #endif + } + + @Test("Binary Data Compression") + func testBinaryDataCompression() throws { + #if canImport(Compression) + // Create some binary data + var binaryData = Data() + for i in 0..<1024 { + binaryData.append(UInt8(i % 256)) + } + + let compressed = try STComparator.compress(binaryData, algorithm: .lzfse) + let decompressed = try STComparator.decompress(compressed, algorithm: .lzfse) + + #expect(decompressed == binaryData) + #endif + } + + @Test("Random Data Compression") + func testRandomDataCompression() throws { + #if canImport(Compression) + // Random data typically doesn't compress well + var randomData = Data() + for _ in 0..<1024 { + randomData.append(UInt8.random(in: 0...255)) + } + + let compressed = try STComparator.compress(randomData, algorithm: .zlib) + let decompressed = try STComparator.decompress(compressed, algorithm: .zlib) + + #expect(decompressed == randomData) + + // Random data might actually get larger when compressed + // So we just verify it works, not that it's smaller + #expect(compressed.count > 0) + #endif + } + + @Test("Compression Algorithm Comparison") + func testCompressionAlgorithmComparison() throws { + #if canImport(Compression) + let testData = """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + """.data(using: .utf8)! + + let algorithms: [STComparator.Algorithm] = [.lz4, .zlib, .lzfse, .lzma] + var compressionResults: [STComparator.Algorithm: Int] = [:] + + for algorithm in algorithms { + let compressed = try STComparator.compress(testData, algorithm: algorithm) + let decompressed = try STComparator.decompress(compressed, algorithm: algorithm) + + #expect(decompressed == testData) + compressionResults[algorithm] = compressed.count + } + + // All algorithms should produce compressed data + for (algorithm, size) in compressionResults { + #expect(size > 0, "Algorithm \(algorithm) produced empty compressed data") + } + #endif + } + + @Test("Compression Error Handling") + func testCompressionErrorHandling() throws { + #if canImport(Compression) + // Test with malformed compressed data + let invalidCompressedData = Data([0xFF, 0xFF, 0xFF, 0xFF]) + + #expect { + try STComparator.decompress(invalidCompressedData, algorithm: .zlib) + } throws: { error in + return error is STComparator.Errors + } + #endif + } + + @Test("Compression Consistency") + func testCompressionConsistency() throws { + #if canImport(Compression) + let testData = "Consistency test data".data(using: .utf8)! + + // Compress the same data multiple times + let compressed1 = try STComparator.compress(testData, algorithm: .zlib) + let compressed2 = try STComparator.compress(testData, algorithm: .zlib) + + // The compressed data should be identical for the same input + #expect(compressed1 == compressed2) + + // Both should decompress to the original data + let decompressed1 = try STComparator.decompress(compressed1, algorithm: .zlib) + let decompressed2 = try STComparator.decompress(compressed2, algorithm: .zlib) + + #expect(decompressed1 == testData) + #expect(decompressed2 == testData) + #expect(decompressed1 == decompressed2) + #endif + } +} \ No newline at end of file diff --git a/Tests/STFilePathTests/STFile+CryptoKitTests.swift b/Tests/STFilePathTests/STFile+CryptoKitTests.swift new file mode 100644 index 0000000..adbc446 --- /dev/null +++ b/Tests/STFilePathTests/STFile+CryptoKitTests.swift @@ -0,0 +1,139 @@ +import Testing +import STFilePath +import Foundation + +@Suite("STFile CryptoKit Tests") +struct STFileCryptoKitTests { + + @Test("SHA256 Hash Operations") + func testSHA256Hash() throws { + #if canImport(CryptoKit) + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let file = testFolder.file("hash_test.txt") + let testData = "Hello, World!".data(using: .utf8)! + try file.create(with: testData) + + // Test SHA256 hash + let hash = try file.hash(with: .sha256) + #expect(!hash.isEmpty) + #expect(hash.count == 64) // SHA256 produces 32 bytes = 64 hex characters + + // Test consistency - same content should produce same hash + let hash2 = try file.hash(with: .sha256) + #expect(hash == hash2) + + // Test with different content + let file2 = testFolder.file("hash_test2.txt") + try file2.create(with: "Different content".data(using: .utf8)!) + let differentHash = try file2.hash(with: .sha256) + #expect(hash != differentHash) + #endif + } + + @Test("Multiple Hash Algorithms") + func testMultipleHashAlgorithms() throws { + #if canImport(CryptoKit) + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let file = testFolder.file("multi_hash_test.txt") + let testData = "Test data for multiple hash algorithms".data(using: .utf8)! + try file.create(with: testData) + + // Test different algorithms produce different results + let sha256Hash = try file.hash(with: .sha256) + let sha384Hash = try file.hash(with: .sha384) + let sha512Hash = try file.hash(with: .sha512) + let md5Hash = try file.hash(with: .md5) + + // Verify hash lengths + #expect(sha256Hash.count == 64) // 32 bytes * 2 hex chars + #expect(sha384Hash.count == 96) // 48 bytes * 2 hex chars + #expect(sha512Hash.count == 128) // 64 bytes * 2 hex chars + #expect(md5Hash.count == 32) // 16 bytes * 2 hex chars + + // Verify they're all different + #expect(sha256Hash != sha384Hash) + #expect(sha256Hash != sha512Hash) + #expect(sha256Hash != md5Hash) + #expect(sha384Hash != sha512Hash) + #endif + } + + @Test("Large File Hash Performance") + func testLargeFileHash() throws { + #if canImport(CryptoKit) + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let file = testFolder.file("large_file.txt") + + // Create a larger file (1MB of data) + let chunkSize = 1024 + let numChunks = 1024 + let chunk = String(repeating: "A", count: chunkSize).data(using: .utf8)! + + try file.create() + let handle = try file.handle(.writing) + defer { handle.closeFile() } + + for _ in 0.. 0 { + buffer[0] = 65 // Change 'H' to 'A' + } + + return buffer[0] + } + + #expect(result == 65) // Verify the modification + + // Verify the change persists in the mapped data + let modifiedData = mmap.read() + #expect(modifiedData.first == 65) + } + #endif + } + + @Test("MMAP Synchronization") + func testMMAPSynchronization() throws { + #if canImport(Darwin) + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let file = testFolder.file("sync_test.txt") + let testData = "Sync test data".data(using: .utf8)! + try file.create(with: testData) + + try file.withMmap { mmap in + // Write some data + let newData = "Synchronized".data(using: .utf8)! + try mmap.write(newData, at: 0) + + // Test sync operation (should not throw) + mmap.sync() + + // Verify data is still accessible + let readData = mmap.read(range: 0..= 4) // At least the files and some folders + + let fileItems = allItems.filter { $0.type.isFile } + #expect(fileItems.count == 3) + } + + @Test("Search with Skip Subdirectory Descendants") + func testSearchWithSkipSubdirectoryDescendants() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + // Create nested structure + let subFolder = testFolder.folder("subfolder") + try subFolder.create() + + let file1 = testFolder.file("root.txt") + let file2 = subFolder.file("nested.txt") + + try file1.create(with: "Root content".data(using: .utf8)!) + try file2.create(with: "Nested content".data(using: .utf8)!) + + // Search without descending into subdirectories + let shallowItems = try testFolder.allSubFilePaths([.skipsSubdirectoryDescendants]) + + // Should only find root.txt and subfolder, not nested.txt + let fileItems = shallowItems.filter { $0.type.isFile } + #expect(fileItems.count == 1) + #expect(fileItems[0].url.lastPathComponent == "root.txt") + } + + @Test("Search with Skip Hidden Files") + func testSearchWithSkipHiddenFiles() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + // Create normal and hidden files + let normalFile = testFolder.file("normal.txt") + let hiddenFile = testFolder.file(".hidden.txt") + + try normalFile.create(with: "Normal content".data(using: .utf8)!) + try hiddenFile.create(with: "Hidden content".data(using: .utf8)!) + + // Search skipping hidden files + let visibleFiles = try testFolder.files([.skipsHiddenFiles]) + #expect(visibleFiles.count == 1) + #expect(visibleFiles[0].url.lastPathComponent == "normal.txt") + + // Search including hidden files (default behavior) + let allFiles = try testFolder.files() + #expect(allFiles.count == 2) + } + + @Test("Custom Search Predicate") + func testCustomSearchPredicate() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + // Create files with different extensions + let txtFile = testFolder.file("document.txt") + let pdfFile = testFolder.file("document.pdf") + let jpgFile = testFolder.file("image.jpg") + + try txtFile.create(with: "Text content".data(using: .utf8)!) + try pdfFile.create(with: "PDF content".data(using: .utf8)!) + try jpgFile.create(with: "Image content".data(using: .utf8)!) + + // Custom predicate to find only .txt files + let txtOnlyPredicate: STFolder.SearchPredicate = .custom { path in + return path.url.pathExtension == "txt" + } + + let txtFiles = try testFolder.files([txtOnlyPredicate]) + #expect(txtFiles.count == 1) + #expect(txtFiles[0].url.lastPathComponent == "document.txt") + } + + @Test("Multiple Custom Predicates") + func testMultipleCustomPredicates() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + // Create files of different sizes + let smallFile = testFolder.file("small.txt") + let mediumFile = testFolder.file("medium.txt") + let largeFile = testFolder.file("large.txt") + + try smallFile.create(with: "Hi".data(using: .utf8)!) + try mediumFile.create(with: String(repeating: "A", count: 100).data(using: .utf8)!) + try largeFile.create(with: String(repeating: "B", count: 1000).data(using: .utf8)!) + + // Predicate for .txt files + let txtPredicate: STFolder.SearchPredicate = .custom { path in + return path.url.pathExtension == "txt" + } + + // Predicate for files larger than 50 bytes + let sizePredicate: STFolder.SearchPredicate = .custom { path in + guard let file = path.asFile else { return false } + do { + return file.attributes.size > 50 + } catch { + return false + } + } + + let filteredFiles = try testFolder.files([txtPredicate, sizePredicate]) + #expect(filteredFiles.count == 2) // medium.txt and large.txt + + let fileNames = filteredFiles.map { $0.url.lastPathComponent }.sorted() + #expect(fileNames == ["large.txt", "medium.txt"]) + } + + @Test("Search in Non-existent Folder") + func testSearchInNonExistentFolder() throws { + let testFolder = try createTestFolder() + let nonExistentFolder = testFolder.folder("does_not_exist") + + // Searching in non-existent folder should throw an error + #expect { + try nonExistentFolder.files() + } throws: { _ in true } + + #expect { + try nonExistentFolder.folders() + } throws: { _ in true } + } + + @Test("Empty Folder Search") + func testEmptyFolderSearch() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + // Search in empty folder + let files = try testFolder.files() + let folders = try testFolder.folders() + let allItems = try testFolder.allSubFilePaths() + + #expect(files.isEmpty) + #expect(folders.isEmpty) + #expect(allItems.isEmpty) + } + + @Test("Complex Nested Search") + func testComplexNestedSearch() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + // Create complex nested structure + let docs = testFolder.folder("documents") + let images = testFolder.folder("images") + let temp = testFolder.folder("temp") + + try docs.create() + try images.create() + try temp.create() + + let subDocs = docs.folder("subdocs") + try subDocs.create() + + // Create various files + try testFolder.file("readme.txt").create(with: "Root readme".data(using: .utf8)!) + try docs.file("doc1.txt").create(with: "Document 1".data(using: .utf8)!) + try docs.file("doc2.pdf").create(with: "Document 2".data(using: .utf8)!) + try subDocs.file("nested.txt").create(with: "Nested doc".data(using: .utf8)!) + try images.file("photo.jpg").create(with: "Photo".data(using: .utf8)!) + try temp.file(".hidden").create(with: "Hidden temp".data(using: .utf8)!) + + // Test various search combinations + let allFiles = try testFolder.files() + #expect(allFiles.count == 1) // Only readme.txt in root + + let allRecursiveFiles = try testFolder.allSubFilePaths().filter { $0.type.isFile } + #expect(allRecursiveFiles.count == 6) // All files including nested ones + + let noHiddenFiles = try testFolder.allSubFilePaths([.skipsHiddenFiles]).filter { $0.type.isFile } + #expect(noHiddenFiles.count == 5) // All except .hidden + + let txtOnly = try testFolder.allSubFilePaths([.custom { $0.url.pathExtension == "txt" }]).filter { $0.type.isFile } + #expect(txtOnly.count == 3) // readme.txt, doc1.txt, nested.txt + } + + @Test("Contains Predicate") + func testContainsPredicate() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let subFolder = testFolder.folder("subfolder") + try subFolder.create() + + let file1 = testFolder.file("file1.txt") + let file2 = subFolder.file("file2.txt") + + try file1.create() + try file2.create() + + // Test contains functionality + #expect(testFolder.contains(file1)) + #expect(testFolder.contains(file2)) // Should contain nested file too + #expect(testFolder.contains(subFolder)) + + // Test that subFolder doesn't contain file1 + #expect(!subFolder.contains(file1)) + #expect(subFolder.contains(file2)) + } +} \ No newline at end of file diff --git a/Tests/STFilePathTests/STJSONLinesTests.swift b/Tests/STFilePathTests/STJSONLinesTests.swift new file mode 100644 index 0000000..f2eaed7 --- /dev/null +++ b/Tests/STFilePathTests/STJSONLinesTests.swift @@ -0,0 +1,235 @@ +import Testing +import STFilePath +import Foundation + +@Suite("STJSONLines Tests") +struct STJSONLinesTests { + + @Test("Basic JSON Lines Operations") + func testBasicJSONLinesOperations() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let file = testFolder.file("jsonlines_test.jsonl") + try file.create() + + // Test writing JSON lines + let writer = try file.lineFile.newLineWriter + + struct TestModel: Codable, Equatable { + let id: Int + let name: String + let active: Bool + } + + let model1 = TestModel(id: 1, name: "Alice", active: true) + let model2 = TestModel(id: 2, name: "Bob", active: false) + let model3 = TestModel(id: 3, name: "Charlie", active: true) + + try writer.append(model: model1) + try writer.append(model: model2) + try writer.append(model: model3) + + // Test reading JSON lines + let readModels = try file.lineFile.lines(as: TestModel.self) + #expect(readModels.count == 3) + #expect(readModels[0] == model1) + #expect(readModels[1] == model2) + #expect(readModels[2] == model3) + } + + @Test("Raw Data JSON Lines") + func testRawDataJSONLines() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let file = testFolder.file("raw_jsonlines.jsonl") + try file.create() + + let writer = try file.lineFile.newLineWriter + + // Write raw JSON data + let jsonData1 = "{\"message\":\"Hello\",\"timestamp\":1234567890}".data(using: .utf8)! + let jsonData2 = "{\"message\":\"World\",\"timestamp\":1234567891}".data(using: .utf8)! + + try writer.append(jsonData1) + try writer.append(jsonData2) + + // Read back as raw data + let lines = try file.lineFile.lines() + #expect(lines.count == 2) + #expect(lines[0] == jsonData1) + #expect(lines[1] == jsonData2) + } + + @Test("Mixed Content JSON Lines") + func testMixedContentJSONLines() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let file = testFolder.file("mixed_jsonlines.jsonl") + try file.create() + + let writer = try file.lineFile.newLineWriter + + struct LogEntry: Codable { + let level: String + let message: String + let timestamp: Int + } + + struct UserAction: Codable { + let userId: Int + let action: String + let metadata: [String: String] + } + + let logEntry = LogEntry(level: "INFO", message: "User logged in", timestamp: 1234567890) + let userAction = UserAction(userId: 123, action: "click", metadata: ["button": "submit"]) + + try writer.append(model: logEntry) + try writer.append(model: userAction) + + // Read back as raw lines + let rawLines = try file.lineFile.lines() + #expect(rawLines.count == 2) + + // Verify each line is valid JSON by decoding + let decoder = JSONDecoder() + let decodedLog = try decoder.decode(LogEntry.self, from: rawLines[0]) + let decodedAction = try decoder.decode(UserAction.self, from: rawLines[1]) + + #expect(decodedLog.level == "INFO") + #expect(decodedAction.userId == 123) + } + + @Test("Large JSON Lines File") + func testLargeJSONLinesFile() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let file = testFolder.file("large_jsonlines.jsonl") + try file.create() + + let writer = try file.lineFile.newLineWriter + + struct DataPoint: Codable, Equatable { + let id: Int + let value: Double + let category: String + } + + let numEntries = 1000 + var expectedData: [DataPoint] = [] + + // Write many entries + for i in 0..= beforeCreate.addingTimeInterval(-1)) + #expect(creationDate <= afterCreate.addingTimeInterval(1)) + } catch { + // Some filesystems/platforms may not support creation date + // This is acceptable + } + + // Modification date should be supported on most systems + do { + let modificationDate = file.attributes.modificationDate + #expect(modificationDate >= beforeCreate.addingTimeInterval(-1)) + #expect(modificationDate <= afterCreate.addingTimeInterval(1)) + } catch { + // If modification date isn't supported, that's unusual but acceptable + } + } + + @Test("Complex Permission Combinations") + func testComplexPermissionCombinations() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let file = testFolder.file("complex_perms.txt") + try file.create(with: "Complex permissions test".data(using: .utf8)!) + + // Test various permission combinations + let permissionSets: [STPathPermission.Posix] = [ + [.ownerRead], + [.ownerWrite], + [.ownerExecute], + [.ownerRead, .ownerWrite], + [.ownerRead, .ownerExecute], + [.ownerWrite, .ownerExecute], + [.ownerRead, .ownerWrite, .ownerExecute], + [.groupRead, .groupWrite, .groupExecute], + [.othersRead, .othersWrite, .othersExecute] + ] + + for permSet in permissionSets { + try file.set(permissions: permSet) + let retrievedPerms = try file.permissions() + + // Check that the core permissions are set correctly + // Note: The retrieved permissions might include additional bits + // so we check if the intersection contains our original set + let intersection = STPathPermission.Posix(rawValue: retrievedPerms.rawValue & permSet.rawValue) + #expect(intersection == permSet) + } + } + + @Test("Metadata Error Handling") + func testMetadataErrorHandling() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let nonExistentFile = testFolder.file("does_not_exist.txt") + + // Test that operations on non-existent files throw appropriate errors + #expect { + try nonExistentFile.permissions() + } throws: { _ in true } + + #expect { + let _ = nonExistentFile.attributes.size + } throws: { _ in true } + + #if canImport(Darwin) + #expect { + try nonExistentFile.value(of: "com.test.nonexistent") + } throws: { _ in true } + #endif + } + + @Test("Large File Size Handling") + func testLargeFileSizeHandling() throws { + let testFolder = try createTestFolder() + defer { try? testFolder.delete() } + try testFolder.create() + + let file = testFolder.file("large_size_test.txt") + try file.create() + + // Create a moderately large file by appending data + let chunkSize = 1024 + let numChunks = 100 + let chunk = Data(repeating: 65, count: chunkSize) // 'A' repeated + + let handle = try file.handle(.writing) + defer { handle.closeFile() } + + for _ in 0.. Date: Sat, 2 Aug 2025 08:32:44 +0000 Subject: [PATCH 5/6] Revert STPath+Metadata.swift to original state as requested Co-authored-by: linhay <15262434+linhay@users.noreply.github.com> --- Sources/STFilePath/STPath+Metadata.swift | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/Sources/STFilePath/STPath+Metadata.swift b/Sources/STFilePath/STPath+Metadata.swift index f4e6999..8f0178b 100644 --- a/Sources/STFilePath/STPath+Metadata.swift +++ b/Sources/STFilePath/STPath+Metadata.swift @@ -6,11 +6,6 @@ // import Foundation -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#endif public extension STPathProtocol { @@ -60,16 +55,12 @@ struct STExtendedAttributes { /// - forName: The name of the attribute. /// - Throws: An error if the attribute cannot be set. func set(name: String, value: Data) throws { - #if canImport(Darwin) try url.path.withCString { fileSystemPath in let status = setxattr(fileSystemPath, name, value.withUnsafeBytes { $0.baseAddress! }, value.count, 0, 0) if status == -1 { throw STPathError(message: "[en] Failed to set extended attribute \(name). \n [zh] 设置扩展属性 \(name) 失败。", code: Int(errno)) } } - #else - throw STPathError(message: "[en] Extended attributes are not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) - #endif } /// [en] Returns the value of an extended attribute. @@ -78,7 +69,6 @@ struct STExtendedAttributes { /// - Returns: The value of the attribute. /// - Throws: An error if the attribute cannot be retrieved. func value(of name: String) throws -> Data { - #if canImport(Darwin) try url.path.withCString { fileSystemPath in let length = getxattr(fileSystemPath, name, nil, 0, 0, 0) if length == -1 { @@ -93,9 +83,6 @@ struct STExtendedAttributes { } return data } - #else - throw STPathError(message: "[en] Extended attributes are not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) - #endif } /// [en] Removes an extended attribute from the file or folder. @@ -103,16 +90,12 @@ struct STExtendedAttributes { /// - Parameter forName: The name of the attribute to remove. /// - Throws: An error if the attribute cannot be removed. func remove(of name: String) throws { - #if canImport(Darwin) try url.path.withCString { fileSystemPath in let status = removexattr(fileSystemPath, name, 0) if status == -1 { throw STPathError(message: "[en] Failed to remove extended attribute \(name). \n [zh] 删除扩展属性 \(name) 失败。", code: Int(errno)) } } - #else - throw STPathError(message: "[en] Extended attributes are not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) - #endif } /// [en] Returns a list of all extended attributes. @@ -120,7 +103,6 @@ struct STExtendedAttributes { /// - Returns: A list of attribute names. /// - Throws: An error if the attributes cannot be retrieved. func list() throws -> [String] { - #if canImport(Darwin) try url.path.withCString { fileSystemPath in let length = listxattr(fileSystemPath, nil, 0, 0) if length == -1 { @@ -133,8 +115,5 @@ struct STExtendedAttributes { } return buffer.split(separator: 0).compactMap { String(cString: Array($0), encoding: .utf8) } } - #else - throw STPathError(message: "[en] Extended attributes are not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) - #endif } } \ No newline at end of file From 0d53a4c6569bd12625d5257e900a4f14534befe4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 08:39:10 +0000 Subject: [PATCH 6/6] Fix Linux compilation error by adding conditional compilation for extended attributes Co-authored-by: linhay <15262434+linhay@users.noreply.github.com> --- Sources/STFilePath/STPath+Metadata.swift | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Sources/STFilePath/STPath+Metadata.swift b/Sources/STFilePath/STPath+Metadata.swift index 8f0178b..5a8a9eb 100644 --- a/Sources/STFilePath/STPath+Metadata.swift +++ b/Sources/STFilePath/STPath+Metadata.swift @@ -6,6 +6,9 @@ // import Foundation +#if canImport(Darwin) +import Darwin +#endif public extension STPathProtocol { @@ -48,6 +51,7 @@ struct STExtendedAttributes { let url: URL + #if canImport(Darwin) /// [en] Sets an extended attribute for the file or folder. /// [zh] 为文件或文件夹设置扩展属性。 /// - Parameters: @@ -116,4 +120,29 @@ struct STExtendedAttributes { return buffer.split(separator: 0).compactMap { String(cString: Array($0), encoding: .utf8) } } } + #else + /// [en] Extended attributes are not supported on this platform. + /// [zh] 此平台不支持扩展属性。 + func set(name: String, value: Data) throws { + throw STPathError(message: "[en] Extended attributes not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) + } + + /// [en] Extended attributes are not supported on this platform. + /// [zh] 此平台不支持扩展属性。 + func value(of name: String) throws -> Data { + throw STPathError(message: "[en] Extended attributes not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) + } + + /// [en] Extended attributes are not supported on this platform. + /// [zh] 此平台不支持扩展属性。 + func remove(of name: String) throws { + throw STPathError(message: "[en] Extended attributes not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) + } + + /// [en] Extended attributes are not supported on this platform. + /// [zh] 此平台不支持扩展属性。 + func list() throws -> [String] { + throw STPathError(message: "[en] Extended attributes not supported on this platform. \n [zh] 此平台不支持扩展属性。", code: -1) + } + #endif } \ No newline at end of file