diff --git a/Sources/NIOFileSystem/ByteCount.swift b/Sources/NIOFileSystem/ByteCount.swift index 64fe76a309..f6b957700d 100644 --- a/Sources/NIOFileSystem/ByteCount.swift +++ b/Sources/NIOFileSystem/ByteCount.swift @@ -80,6 +80,21 @@ public struct ByteCount: Hashable, Sendable { } } +extension ByteCount { + /// A ``ByteCount`` for the maximum amount of bytes that can be written to `ByteBuffer`. + internal static var byteBufferCapacity: ByteCount { + #if arch(arm) || arch(i386) || arch(arm64_32) + // on 32-bit platforms we can't make use of a whole UInt32.max (as it doesn't fit in an Int) + let byteBufferMaxIndex = UInt32(Int.max) + #else + // on 64-bit platforms we're good + let byteBufferMaxIndex = UInt32.max + #endif + + return ByteCount(bytes: Int64(byteBufferMaxIndex)) + } +} + extension ByteCount: AdditiveArithmetic { public static var zero: ByteCount { ByteCount(bytes: 0) } diff --git a/Sources/NIOFileSystem/FileHandleProtocol.swift b/Sources/NIOFileSystem/FileHandleProtocol.swift index d58e33065a..b8d2791c4c 100644 --- a/Sources/NIOFileSystem/FileHandleProtocol.swift +++ b/Sources/NIOFileSystem/FileHandleProtocol.swift @@ -328,10 +328,9 @@ extension ReadableFileHandleProtocol { /// - offset: The absolute offset into the file to read from. Defaults to zero. /// - maximumSizeAllowed: The maximum size of file to read, as a ``ByteCount``. /// - Returns: The bytes read from the file. - /// - Throws: ``FileSystemError`` with code ``FileSystemError/Code-swift.struct/resourceExhausted`` if there - /// are more bytes to read than `maximumBytesAllowed`. - /// ``FileSystemError/Code-swift.struct/unsupported`` if file is unseekable and - /// `offset` is not 0. + /// - Throws: ``FileSystemError`` with code ``FileSystemError/Code-swift.struct/resourceExhausted`` + /// if `maximumSizeAllowed` is more than can be written to `ByteBuffer`. Or if there are more bytes to read than + /// `maximumBytesAllowed`. public func readToEnd( fromAbsoluteOffset offset: Int64 = 0, maximumSizeAllowed: ByteCount @@ -340,6 +339,20 @@ extension ReadableFileHandleProtocol { let fileSize = Int64(info.size) let readSize = max(Int(fileSize - offset), 0) + if maximumSizeAllowed > .byteBufferCapacity { + throw FileSystemError( + code: .resourceExhausted, + message: """ + The maximum size allowed (\(maximumSizeAllowed)) is more than the maximum \ + amount of bytes that can be written to ByteBuffer \ + (\(ByteCount.byteBufferCapacity)). You can read the file in smaller chunks by \ + calling readChunks(). + """, + cause: nil, + location: .here() + ) + } + if readSize > maximumSizeAllowed.bytes { throw FileSystemError( code: .resourceExhausted, diff --git a/Tests/NIOFileSystemIntegrationTests/FileSystemTests.swift b/Tests/NIOFileSystemIntegrationTests/FileSystemTests.swift index 397e9b76ca..f0e6aee74a 100644 --- a/Tests/NIOFileSystemIntegrationTests/FileSystemTests.swift +++ b/Tests/NIOFileSystemIntegrationTests/FileSystemTests.swift @@ -1803,6 +1803,21 @@ extension FileSystemTests { XCTAssertEqual(byteCount, Int(size)) } } + + func testReadMoreThanByteBufferCapacity() async throws { + let path = try await self.fs.temporaryFilePath() + + try await self.fs.withFileHandle(forReadingAndWritingAt: path) { fileHandle in + await XCTAssertThrowsFileSystemErrorAsync { + // Set `maximumSizeAllowed` to 1 byte more than can be written to `ByteBuffer`. + try await fileHandle.readToEnd( + maximumSizeAllowed: .byteBufferCapacity + .bytes(1) + ) + } onError: { error in + XCTAssertEqual(error.code, .resourceExhausted) + } + } + } } #if !canImport(Darwin) && swift(<5.9.2)