Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions Sources/TSCBasic/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -628,17 +628,47 @@ private struct LocalFileSystem: FileSystem {
}

func readFileContents(_ path: AbsolutePath) throws -> ByteString {
// stat the path so we can route to the right reader and produce accurate
// errors for directories. If stat itself fails (e.g. a path component is
// not a directory, or the path doesn't exist), propagate that error via
// the normal NSError mapping so callers get .noEntry / .notDirectory etc.
let fileType: FileAttributeType?
do {
let dataContent = try Data(contentsOf: URL(fileURLWithPath: path.pathString))
return dataContent.withUnsafeBytes { bytes in
ByteString(Array(bytes.bindMemory(to: UInt8.self)))
}
let attrs = try FileManager.default.attributesOfItem(atPath: path.pathString)
fileType = attrs[.type] as? FileAttributeType
} catch let error as NSError {
throw FileSystemError.from(nsError: error, path: path)
} catch {
// Handle any other error types (e.g., Swift errors)
throw FileSystemError(.unknownOSError, path)
}

if fileType == .typeDirectory {
throw FileSystemError(.isDirectory, path)
} else if fileType == .typeRegular {
// Data(contentsOf:) is used for regular files because it handles
// extended-length (\\?\) paths on Windows.
do {
let dataContent = try Data(contentsOf: URL(fileURLWithPath: path.pathString))
return dataContent.withUnsafeBytes { bytes in
ByteString(Array(bytes.bindMemory(to: UInt8.self)))
}
} catch let error as NSError {
throw FileSystemError.from(nsError: error, path: path)
} catch {
throw FileSystemError(.unknownOSError, path)
}
} else {
// Non-regular files (pipes, character devices, FIFOs) are rejected by
// Data(contentsOf:), so fall back to FileHandle which accepts them.
guard let handle = FileHandle(forReadingAtPath: path.pathString) else {
throw FileSystemError(.noEntry, path)
}
defer { try? handle.close() }
let dataContent = handle.readDataToEndOfFile()
return dataContent.withUnsafeBytes { bytes in
ByteString(Array(bytes.bindMemory(to: UInt8.self)))
}
}
}

func writeFileContents(_ path: AbsolutePath, bytes: ByteString) throws {
Expand Down
41 changes: 33 additions & 8 deletions Tests/TSCBasicTests/FileSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,7 @@ class FileSystemTests: XCTestCase {
XCTAssertEqual(try! fs.readFileContents(filePath), "Hello, new world!")

// Check read/write of a directory.
#if os(Windows)
var expectedError = FileSystemError(.invalidAccess, filePath.parentDirectory)
#else
var expectedError = FileSystemError(.isDirectory, filePath.parentDirectory)
#endif
XCTAssertThrows(expectedError) {
_ = try fs.readFileContents(filePath.parentDirectory)
}
Expand All @@ -376,11 +372,7 @@ class FileSystemTests: XCTestCase {
#else
let root = AbsolutePath("/")
#endif
#if os(Windows)
expectedError = FileSystemError(.invalidAccess, root)
#else
expectedError = FileSystemError(.isDirectory, root)
#endif
XCTAssertThrows(expectedError) {
_ = try fs.readFileContents(root)
}
Expand Down Expand Up @@ -1030,6 +1022,39 @@ class FileSystemTests: XCTestCase {
try XCTAssertEqual(fs.readFileContents(fileA), "100")
try XCTAssertEqual(fs.readFileContents(fileB), "100")
}

#if !os(Windows)
func testLocalReadNonRegularFileDevNull() throws {
let fs = TSCBasic.localFileSystem
let devNull = try AbsolutePath(validating: "/dev/null")
let contents = try fs.readFileContents(devNull)
XCTAssertEqual(contents, ByteString([]))
}

func testLocalReadFIFO() throws {
let fs = TSCBasic.localFileSystem
try withTemporaryDirectory(prefix: #function, removeTreeOnDeinit: true) { tmpDir in
let fifoPath = tmpDir.appending(component: "test.fifo")
guard mkfifo(fifoPath.pathString, 0o644) == 0 else {
XCTFail("mkfifo failed: \(String(cString: strerror(errno)))")
return
}

let writeContent = "hello from fifo"
// Open and write in a background thread; the open blocks until the
// read end is also opened, so the thread must start before the read.
let writer = Thread {
guard let handle = FileHandle(forWritingAtPath: fifoPath.pathString) else { return }
handle.write(writeContent.data(using: .utf8)!)
try? handle.close()
}
writer.start()

let contents = try fs.readFileContents(fifoPath)
XCTAssertEqual(contents, ByteString(writeContent))
}
}
#endif
}

/// Helper method to test file tree removal method on the given file system.
Expand Down
Loading