Skip to content

Commit 408135c

Browse files
committed
Fix FileSystem read/writeFileContents to use Data API
- using the Data API fixes issues with long filenames on Windows
1 parent c4309ae commit 408135c

File tree

2 files changed

+102
-48
lines changed

2 files changed

+102
-48
lines changed

Sources/TSCBasic/FileSystem.swift

Lines changed: 100 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,84 @@ public extension FileSystemError {
104104
case TSCLibc.ENOTDIR:
105105
self.init(.notDirectory, path)
106106
case TSCLibc.EEXIST:
107-
self.init(.alreadyExistsAtDestination, path)
107+
self.init(.noEntry, path)
108108
default:
109109
self.init(.ioError(code: errno), path)
110110
}
111111
}
112+
113+
init(error: POSIXError, _ path: AbsolutePath) {
114+
switch error.code {
115+
case .ENOENT:
116+
self.init(.noEntry, path)
117+
case .EACCES:
118+
self.init(.invalidAccess, path)
119+
case .EISDIR:
120+
self.init(.isDirectory, path)
121+
case .ENOTDIR:
122+
self.init(.notDirectory, path)
123+
case .EEXIST:
124+
self.init(.alreadyExistsAtDestination, path)
125+
default:
126+
self.init(.ioError(code: error.code.rawValue), path)
127+
}
128+
}
129+
}
130+
131+
// MARK: - NSError to FileSystemError Mapping
132+
private extension FileSystemError {
133+
/// Maps NSError codes to appropriate FileSystemError kinds
134+
/// This centralizes error mapping logic and ensures consistency across file operations
135+
///
136+
/// - Parameters:
137+
/// - error: The NSError to map
138+
/// - path: The file path associated with the error
139+
/// - Returns: A FileSystemError with appropriate semantic mapping
140+
static func from(nsError error: NSError, path: AbsolutePath) -> FileSystemError {
141+
// First, check for POSIX errors in the underlying error chain
142+
// POSIX errors provide more precise semantic information
143+
if let posixError = error.userInfo[NSUnderlyingErrorKey] as? POSIXError {
144+
return FileSystemError(error: posixError, path)
145+
}
146+
147+
// Handle Cocoa domain errors with proper semantic mapping
148+
guard error.domain == NSCocoaErrorDomain else {
149+
// For non-Cocoa errors, preserve the original error information
150+
return FileSystemError(.ioError(code: Int32(error.code)), path)
151+
}
152+
153+
// Map common Cocoa error codes to semantic FileSystemError kinds
154+
switch error.code {
155+
// File not found errors
156+
case NSFileReadNoSuchFileError, NSFileNoSuchFileError:
157+
return FileSystemError(.noEntry, path)
158+
159+
// Permission denied errors
160+
case NSFileReadNoPermissionError, NSFileWriteNoPermissionError:
161+
return FileSystemError(.invalidAccess, path)
162+
163+
// File already exists errors
164+
case NSFileWriteFileExistsError:
165+
return FileSystemError(.alreadyExistsAtDestination, path)
166+
167+
// Read-only volume errors
168+
case NSFileWriteVolumeReadOnlyError:
169+
return FileSystemError(.invalidAccess, path)
170+
171+
// File corruption or invalid format errors
172+
case NSFileReadCorruptFileError:
173+
return FileSystemError(.ioError(code: Int32(error.code)), path)
174+
175+
// Directory-related errors
176+
case NSFileReadInvalidFileNameError:
177+
return FileSystemError(.notDirectory, path)
178+
179+
default:
180+
// For any other Cocoa error, wrap it as an IO error preserving the original code
181+
// This ensures we don't lose diagnostic information
182+
return FileSystemError(.ioError(code: Int32(error.code)), path)
183+
}
184+
}
112185
}
113186

114187
/// Defines the file modes.
@@ -513,64 +586,45 @@ private struct LocalFileSystem: FileSystem {
513586
}
514587

515588
func readFileContents(_ path: AbsolutePath) throws -> ByteString {
516-
// Open the file.
517-
guard let fp = fopen(path.pathString, "rb") else {
518-
throw FileSystemError(errno: errno, path)
519-
}
520-
defer { fclose(fp) }
521-
522-
// Read the data one block at a time.
523-
let data = BufferedOutputByteStream()
524-
var tmpBuffer = [UInt8](repeating: 0, count: 1 << 12)
525-
while true {
526-
let n = fread(&tmpBuffer, 1, tmpBuffer.count, fp)
527-
if n < 0 {
528-
if errno == EINTR { continue }
529-
throw FileSystemError(.ioError(code: errno), path)
530-
}
531-
if n == 0 {
532-
let errno = ferror(fp)
533-
if errno != 0 {
534-
throw FileSystemError(.ioError(code: errno), path)
535-
}
536-
break
589+
do {
590+
let dataContent = try Data(contentsOf: URL(fileURLWithPath: path.pathString))
591+
return dataContent.withUnsafeBytes { bytes in
592+
ByteString(Array(bytes.bindMemory(to: UInt8.self)))
537593
}
538-
data.send(tmpBuffer[0..<n])
594+
} catch let error as NSError {
595+
throw FileSystemError.from(nsError: error, path: path)
596+
} catch {
597+
// Handle any other error types (e.g., Swift errors)
598+
throw FileSystemError(.unknownOSError, path)
539599
}
540-
541-
return data.bytes
542600
}
543601

544602
func writeFileContents(_ path: AbsolutePath, bytes: ByteString) throws {
545-
// Open the file.
546-
guard let fp = fopen(path.pathString, "wb") else {
547-
throw FileSystemError(errno: errno, path)
548-
}
549-
defer { fclose(fp) }
550-
551-
// Write the data in one chunk.
552-
var contents = bytes.contents
553-
while true {
554-
let n = fwrite(&contents, 1, contents.count, fp)
555-
if n < 0 {
556-
if errno == EINTR { continue }
557-
throw FileSystemError(.ioError(code: errno), path)
558-
}
559-
if n != contents.count {
560-
throw FileSystemError(.mismatchedByteCount(expected: contents.count, actual: n), path)
603+
do {
604+
try bytes.withData {
605+
try $0.write(to: URL(fileURLWithPath: path.pathString))
561606
}
562-
break
607+
} catch let error as NSError {
608+
throw FileSystemError.from(nsError: error, path: path)
609+
} catch {
610+
// Handle any other error types (e.g., Swift errors)
611+
throw FileSystemError(.unknownOSError, path)
563612
}
564613
}
565614

566615
func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws {
567-
// Perform non-atomic writes using the fast path.
568616
if !atomically {
569617
return try writeFileContents(path, bytes: bytes)
570618
}
571-
572-
try bytes.withData {
573-
try $0.write(to: URL(fileURLWithPath: path.pathString), options: .atomic)
619+
do {
620+
try bytes.withData {
621+
try $0.write(to: URL(fileURLWithPath: path.pathString), options: .atomic)
622+
}
623+
} catch let error as NSError {
624+
throw FileSystemError.from(nsError: error, path: path)
625+
} catch {
626+
// Handle any other error types (e.g., Swift errors)
627+
throw FileSystemError(.unknownOSError, path)
574628
}
575629
}
576630

Tests/TSCBasicTests/FileSystemTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ class FileSystemTests: XCTestCase {
313313
XCTAssertEqual(try! fs.readFileContents(filePath), "Hello, new world!")
314314

315315
// Check read/write of a directory.
316-
XCTAssertThrows(FileSystemError(.ioError(code: TSCLibc.EPERM), filePath.parentDirectory)) {
316+
XCTAssertThrows(FileSystemError(.isDirectory, filePath.parentDirectory)) {
317317
_ = try fs.readFileContents(filePath.parentDirectory)
318318
}
319319
XCTAssertThrows(FileSystemError(.isDirectory, filePath.parentDirectory)) {
@@ -327,7 +327,7 @@ class FileSystemTests: XCTestCase {
327327
#else
328328
let root = AbsolutePath("/")
329329
#endif
330-
XCTAssertThrows(FileSystemError(.ioError(code: TSCLibc.EPERM), root)) {
330+
XCTAssertThrows(FileSystemError(.isDirectory, root)) {
331331
_ = try fs.readFileContents(root)
332332

333333
}

0 commit comments

Comments
 (0)