From cc1c57c2912a058870f4f47d84a001aeb7be39ac Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:41:10 +0100 Subject: [PATCH] Provide APIs to read file into more data types (#2923) Motivation: As requested in issues [#2875](https://github.com/apple/swift-nio/issues/2875) and [#2876](https://github.com/apple/swift-nio/issues/2876), it would be convenient to be able to read the contents of a file into more data types such as `Array`, `ArraySlice` & Foundation's `Data`. Modifications: - Extend `Array`, `ArraySlice` & `Data` to be initialisable with the contents of a file. Result: The contents of a file can be read into more data types. --------- Co-authored-by: George Barnett --- Package.swift | 3 +- Sources/NIOFileSystem/Array+FileSystem.swift | 54 ++++++++++++++++++ .../NIOFileSystem/ArraySlice+FileSystem.swift | 56 ++++++++++++++++++ .../Data+FileSystem.swift | 57 +++++++++++++++++++ .../FileSystemFoundationCompatTests.swift | 44 ++++++++++++++ .../FileSystemTests.swift | 24 ++++++++ 6 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 Sources/NIOFileSystem/Array+FileSystem.swift create mode 100644 Sources/NIOFileSystem/ArraySlice+FileSystem.swift create mode 100644 Sources/NIOFileSystemFoundationCompat/Data+FileSystem.swift diff --git a/Package.swift b/Package.swift index 99842452b9..bb2eaeddcb 100644 --- a/Package.swift +++ b/Package.swift @@ -259,7 +259,8 @@ let package = Package( .target( name: "_NIOFileSystemFoundationCompat", dependencies: [ - "_NIOFileSystem" + "_NIOFileSystem", + "NIOFoundationCompat", ], path: "Sources/NIOFileSystemFoundationCompat" ), diff --git a/Sources/NIOFileSystem/Array+FileSystem.swift b/Sources/NIOFileSystem/Array+FileSystem.swift new file mode 100644 index 0000000000..b8707fbbd5 --- /dev/null +++ b/Sources/NIOFileSystem/Array+FileSystem.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) || os(Android) +import NIOCore + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension Array where Element == UInt8 { + /// Reads the contents of the file at the path. + /// + /// - Parameters: + /// - path: The path of the file to read. + /// - maximumSizeAllowed: The maximum size of file which can be read, in bytes, as a ``ByteCount``. + /// - fileSystem: The ``FileSystemProtocol`` instance to use to read the file. + public init( + contentsOf path: FilePath, + maximumSizeAllowed: ByteCount, + fileSystem: some FileSystemProtocol + ) async throws { + let byteBuffer = try await fileSystem.withFileHandle(forReadingAt: path) { handle in + try await handle.readToEnd(maximumSizeAllowed: maximumSizeAllowed) + } + + self = Self(buffer: byteBuffer) + } + + /// Reads the contents of the file at the path using ``FileSystem``. + /// + /// - Parameters: + /// - path: The path of the file to read. + /// - maximumSizeAllowed: The maximum size of file which can be read, as a ``ByteCount``. + public init( + contentsOf path: FilePath, + maximumSizeAllowed: ByteCount + ) async throws { + self = try await Self( + contentsOf: path, + maximumSizeAllowed: maximumSizeAllowed, + fileSystem: .shared + ) + } +} +#endif diff --git a/Sources/NIOFileSystem/ArraySlice+FileSystem.swift b/Sources/NIOFileSystem/ArraySlice+FileSystem.swift new file mode 100644 index 0000000000..37e324acfc --- /dev/null +++ b/Sources/NIOFileSystem/ArraySlice+FileSystem.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) || os(Android) +import NIOCore + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension ArraySlice where Element == UInt8 { + /// Reads the contents of the file at the path. + /// + /// - Parameters: + /// - path: The path of the file to read. + /// - maximumSizeAllowed: The maximum size of file which can be read, in bytes, as a ``ByteCount``. + /// - fileSystem: The ``FileSystemProtocol`` instance to use to read the file. + public init( + contentsOf path: FilePath, + maximumSizeAllowed: ByteCount, + fileSystem: some FileSystemProtocol + ) async throws { + let bytes = try await Array( + contentsOf: path, + maximumSizeAllowed: maximumSizeAllowed, + fileSystem: fileSystem + ) + + self = Self(bytes) + } + + /// Reads the contents of the file at the path using ``FileSystem``. + /// + /// - Parameters: + /// - path: The path of the file to read. + /// - maximumSizeAllowed: The maximum size of file which can be read, as a ``ByteCount``. + public init( + contentsOf path: FilePath, + maximumSizeAllowed: ByteCount + ) async throws { + self = try await Self( + contentsOf: path, + maximumSizeAllowed: maximumSizeAllowed, + fileSystem: .shared + ) + } +} +#endif diff --git a/Sources/NIOFileSystemFoundationCompat/Data+FileSystem.swift b/Sources/NIOFileSystemFoundationCompat/Data+FileSystem.swift new file mode 100644 index 0000000000..7e0652f24d --- /dev/null +++ b/Sources/NIOFileSystemFoundationCompat/Data+FileSystem.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) || os(Android) +import _NIOFileSystem +import NIOCore +import NIOFoundationCompat +import struct Foundation.Data + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension Data { + /// Reads the contents of the file at the path. + /// + /// - Parameters: + /// - path: The path of the file to read. + /// - maximumSizeAllowed: The maximum size of file which can be read, in bytes, as a ``ByteCount``. + /// - fileSystem: The ``FileSystemProtocol`` instance to use to read the file. + public init( + contentsOf path: FilePath, + maximumSizeAllowed: ByteCount, + fileSystem: some FileSystemProtocol + ) async throws { + let byteBuffer = try await fileSystem.withFileHandle(forReadingAt: path) { handle in + try await handle.readToEnd(maximumSizeAllowed: maximumSizeAllowed) + } + + self = Data(buffer: byteBuffer) + } + + /// Reads the contents of the file at the path using ``FileSystem``. + /// + /// - Parameters: + /// - path: The path of the file to read. + /// - maximumSizeAllowed: The maximum size of file which can be read, as a ``ByteCount``. + public init( + contentsOf path: FilePath, + maximumSizeAllowed: ByteCount + ) async throws { + self = try await Self( + contentsOf: path, + maximumSizeAllowed: maximumSizeAllowed, + fileSystem: .shared + ) + } +} +#endif diff --git a/Tests/NIOFileSystemFoundationCompatTests/FileSystemFoundationCompatTests.swift b/Tests/NIOFileSystemFoundationCompatTests/FileSystemFoundationCompatTests.swift index 2f6d0e68cf..717b9762dd 100644 --- a/Tests/NIOFileSystemFoundationCompatTests/FileSystemFoundationCompatTests.swift +++ b/Tests/NIOFileSystemFoundationCompatTests/FileSystemFoundationCompatTests.swift @@ -17,6 +17,37 @@ import _NIOFileSystem import _NIOFileSystemFoundationCompat import XCTest +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension FileSystem { + func temporaryFilePath( + _ function: String = #function, + inTemporaryDirectory: Bool = true + ) async throws -> FilePath { + if inTemporaryDirectory { + let directory = try await self.temporaryDirectory + return self.temporaryFilePath(function, inDirectory: directory) + } else { + return self.temporaryFilePath(function, inDirectory: nil) + } + } + + func temporaryFilePath( + _ function: String = #function, + inDirectory directory: FilePath? + ) -> FilePath { + let index = function.firstIndex(of: "(")! + let functionName = function.prefix(upTo: index) + let random = UInt32.random(in: .min ... .max) + let fileName = "\(functionName)-\(random)" + + if let directory = directory { + return directory.appending(fileName) + } else { + return FilePath(fileName) + } + } +} + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) final class FileSystemBytesConformanceTests: XCTestCase { func testTimepecToDate() async throws { @@ -33,5 +64,18 @@ final class FileSystemBytesConformanceTests: XCTestCase { Date(timeIntervalSince1970: 1.000000001) ) } + + func testReadFileIntoData() async throws { + let fs = FileSystem.shared + let path = try await fs.temporaryFilePath() + + try await fs.withFileHandle(forReadingAndWritingAt: path) { fileHandle in + _ = try await fileHandle.write(contentsOf: [0, 1, 2], toAbsoluteOffset: 0) + } + + let contents = try await Data(contentsOf: path, maximumSizeAllowed: .bytes(1024)) + + XCTAssertEqual(contents, Data([0, 1, 2])) + } } #endif diff --git a/Tests/NIOFileSystemIntegrationTests/FileSystemTests.swift b/Tests/NIOFileSystemIntegrationTests/FileSystemTests.swift index 409ef26cef..a7c2786a27 100644 --- a/Tests/NIOFileSystemIntegrationTests/FileSystemTests.swift +++ b/Tests/NIOFileSystemIntegrationTests/FileSystemTests.swift @@ -1828,6 +1828,30 @@ extension FileSystemTests { ) } } + + func testReadIntoArray() async throws { + let path = try await self.fs.temporaryFilePath() + + try await self.fs.withFileHandle(forReadingAndWritingAt: path) { fileHandle in + _ = try await fileHandle.write(contentsOf: [0, 1, 2], toAbsoluteOffset: 0) + } + + let contents = try await Array(contentsOf: path, maximumSizeAllowed: .bytes(1024)) + + XCTAssertEqual(contents, [0, 1, 2]) + } + + func testReadIntoArraySlice() async throws { + let path = try await self.fs.temporaryFilePath() + + try await self.fs.withFileHandle(forReadingAndWritingAt: path) { fileHandle in + _ = try await fileHandle.write(contentsOf: [0, 1, 2], toAbsoluteOffset: 0) + } + + let contents = try await ArraySlice(contentsOf: path, maximumSizeAllowed: .bytes(1024)) + + XCTAssertEqual(contents, [0, 1, 2]) + } } #if !canImport(Darwin) && swift(<5.9.2)