diff --git a/NOTICE.txt b/NOTICE.txt index cf22b8e7..7640185f 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -32,3 +32,14 @@ This product contains a derivation of the Tony Stone's 'process_test_files.rb'. * https://www.apache.org/licenses/LICENSE-2.0 * HOMEPAGE: * https://codegists.com/snippet/ruby/generate_xctest_linux_runnerrb_tonystone_ruby + +--- + +This product contains a derivation of "HTTP1ProxyConnectHandler.swift" and accompanying tests from AsyncHTTPClient. + + * LICENSE (Apache License 2.0): + * https://www.apache.org/licenses/LICENSE-2.0 + * HOMEPAGE: + * https://github.com/swift-server/async-http-client + +--- diff --git a/Package.swift b/Package.swift index 1b61ba73..8d69b8ef 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,7 @@ var targets: [PackageDescription.Target] = [ "NIOPCAP", .product(name: "NIO", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOHTTP1", package: "swift-nio"), ]), .target( name: "NIOHTTPCompression", @@ -116,6 +117,18 @@ var targets: [PackageDescription.Target] = [ .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), ]), + name: "NIONFS3", + dependencies: [ + .product(name: "NIOCore", package: "swift-nio"), + ]), + .testTarget( + name: "NIONFS3Tests", + dependencies: [ + "NIONFS3", + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOEmbedded", package: "swift-nio"), + .product(name: "NIOTestUtils", package: "swift-nio"), + ]), ] let package = Package( diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift index ef408b52..2163959d 100644 --- a/Package@swift-5.5.swift +++ b/Package@swift-5.5.swift @@ -22,6 +22,7 @@ var targets: [PackageDescription.Target] = [ "NIOPCAP", .product(name: "NIO", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOHTTP1", package: "swift-nio") ]), .target( name: "NIOHTTPCompression", @@ -116,6 +117,17 @@ var targets: [PackageDescription.Target] = [ .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), ]), + name: "NIONFS3", + dependencies: [ + .product(name: "NIOCore", package: "swift-nio"), + ]), + .testTarget( + name: "NIONFS3Tests", + dependencies: [ + "NIONFS3", + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOTestUtils", package: "swift-nio"), + ]), ] let package = Package( diff --git a/Sources/NIOExtras/HTTP1ProxyConnectHandler.swift b/Sources/NIOExtras/HTTP1ProxyConnectHandler.swift new file mode 100644 index 00000000..7663bdd8 --- /dev/null +++ b/Sources/NIOExtras/HTTP1ProxyConnectHandler.swift @@ -0,0 +1,396 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2022 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOHTTP1 + +public final class NIOHTTP1ProxyConnectHandler: ChannelDuplexHandler, RemovableChannelHandler { + public typealias OutboundIn = Never + public typealias OutboundOut = HTTPClientRequestPart + public typealias InboundIn = HTTPClientResponsePart + + /// Whether we've already seen the first request. + private var seenFirstRequest = false + private var bufferedWrittenMessages: MarkedCircularBuffer + + struct BufferedWrite { + var data: NIOAny + var promise: EventLoopPromise? + } + + private enum State { + // transitions to `.connectSent` or `.failed` + case initialized + // transitions to `.headReceived` or `.failed` + case connectSent(Scheduled) + // transitions to `.completed` or `.failed` + case headReceived(Scheduled) + // final error state + case failed(Error) + // final success state + case completed + } + + private var state: State = .initialized + + private let targetHost: String + private let targetPort: Int + private let headers: HTTPHeaders + private let deadline: NIODeadline + private let promise: EventLoopPromise? + + /// Creates a new ``NIOHTTP1ProxyConnectHandler`` that issues a CONNECT request to a proxy server + /// and instructs the server to connect to `targetHost`. + /// - Parameters: + /// - targetHost: The desired end point host + /// - targetPort: The port to be used when connecting to `targetHost` + /// - headers: Headers to supply to the proxy server as part of the CONNECT request + /// - deadline: Deadline for the CONNECT request + /// - promise: Promise with which the result of the connect operation is communicated + public init(targetHost: String, + targetPort: Int, + headers: HTTPHeaders, + deadline: NIODeadline, + promise: EventLoopPromise?) { + self.targetHost = targetHost + self.targetPort = targetPort + self.headers = headers + self.deadline = deadline + self.promise = promise + + self.bufferedWrittenMessages = MarkedCircularBuffer(initialCapacity: 16) // matches CircularBuffer default + } + + public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + switch self.state { + case .initialized, .connectSent, .headReceived, .completed: + self.bufferedWrittenMessages.append(BufferedWrite(data: data, promise: promise)) + case .failed(let error): + promise?.fail(error) + } + } + + public func flush(context: ChannelHandlerContext) { + self.bufferedWrittenMessages.mark() + } + + public func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) { + // We have been formally removed from the pipeline. We should send any buffered data we have. + switch self.state { + case .initialized, .connectSent, .headReceived, .failed: + self.failWithError(.noResult(), context: context) + + case .completed: + while let (bufferedPart, isMarked) = self.bufferedWrittenMessages.popFirstCheckMarked() { + context.write(bufferedPart.data, promise: bufferedPart.promise) + if isMarked { + context.flush() + } + } + + } + + context.leavePipeline(removalToken: removalToken) + } + + public func handlerAdded(context: ChannelHandlerContext) { + if context.channel.isActive { + self.sendConnect(context: context) + } + } + + public func handlerRemoved(context: ChannelHandlerContext) { + switch self.state { + case .failed, .completed: + guard self.bufferedWrittenMessages.isEmpty else { + self.failWithError(Error.droppedWrites(), context: context) + return + } + break + + case .initialized, .connectSent, .headReceived: + self.failWithError(Error.noResult(), context: context) + } + } + + public func channelActive(context: ChannelHandlerContext) { + self.sendConnect(context: context) + context.fireChannelActive() + } + + public func channelInactive(context: ChannelHandlerContext) { + switch self.state { + case .initialized: + self.failWithError(Error.channelUnexpectedlyInactive(), context: context, closeConnection: false) + case .connectSent(let timeout), .headReceived(let timeout): + timeout.cancel() + self.failWithError(Error.remoteConnectionClosed(), context: context, closeConnection: false) + + case .failed, .completed: + break + } + context.fireChannelInactive() + } + + public func channelRead(context: ChannelHandlerContext, data: NIOAny) { + switch self.unwrapInboundIn(data) { + case .head(let head): + self.handleHTTPHeadReceived(head, context: context) + case .body: + self.handleHTTPBodyReceived(context: context) + case .end: + self.handleHTTPEndReceived(context: context) + } + } + + private func sendConnect(context: ChannelHandlerContext) { + guard case .initialized = self.state else { + // we might run into this handler twice, once in handlerAdded and once in channelActive. + return + } + + let timeout = context.eventLoop.scheduleTask(deadline: self.deadline) { + switch self.state { + case .initialized: + preconditionFailure("How can we have a scheduled timeout, if the connection is not even up?") + + case .connectSent, .headReceived: + self.failWithError(Error.httpProxyHandshakeTimeout(), context: context) + + case .failed, .completed: + break + } + } + + self.state = .connectSent(timeout) + + let head = HTTPRequestHead( + version: .init(major: 1, minor: 1), + method: .CONNECT, + uri: "\(self.targetHost):\(self.targetPort)", + headers: self.headers + ) + + context.write(self.wrapOutboundOut(.head(head)), promise: nil) + context.write(self.wrapOutboundOut(.end(nil)), promise: nil) + context.flush() + } + + private func handleHTTPHeadReceived(_ head: HTTPResponseHead, context: ChannelHandlerContext) { + switch self.state { + case .connectSent(let scheduled): + switch head.status.code { + case 200..<300: + // Any 2xx (Successful) response indicates that the sender (and all + // inbound proxies) will switch to tunnel mode immediately after the + // blank line that concludes the successful response's header section + self.state = .headReceived(scheduled) + case 407: + self.failWithError(Error.proxyAuthenticationRequired(), context: context) + + default: + // Any response other than a successful response indicates that the tunnel + // has not yet been formed and that the connection remains governed by HTTP. + self.failWithError(Error.invalidProxyResponseHead(head), context: context) + } + case .failed: + break + case .initialized, .headReceived, .completed: + preconditionFailure("Invalid state: \(self.state)") + } + } + + private func handleHTTPBodyReceived(context: ChannelHandlerContext) { + switch self.state { + case .headReceived(let timeout): + timeout.cancel() + // we don't expect a body + self.failWithError(Error.invalidProxyResponse(), context: context) + case .failed: + // ran into an error before... ignore this one + break + case .completed, .connectSent, .initialized: + preconditionFailure("Invalid state: \(self.state)") + } + } + + private func handleHTTPEndReceived(context: ChannelHandlerContext) { + switch self.state { + case .headReceived(let timeout): + timeout.cancel() + self.state = .completed + case .failed: + // ran into an error before... ignore this one + return + case .initialized, .connectSent, .completed: + preconditionFailure("Invalid state: \(self.state)") + } + + // Ok, we've set up the proxy connection. We can now remove ourselves, which should happen synchronously. + context.pipeline.removeHandler(context: context, promise: nil) + + self.promise?.succeed(()) + } + + private func failWithError(_ error: Error, context: ChannelHandlerContext, closeConnection: Bool = true) { + switch self.state { + case .failed: + return + case .initialized, .connectSent, .headReceived, .completed: + self.state = .failed(error) + self.promise?.fail(error) + context.fireErrorCaught(error) + if closeConnection { + context.close(mode: .all, promise: nil) + } + while let bufferedWrite = self.bufferedWrittenMessages.popFirst() { + bufferedWrite.promise?.fail(error) + } + } + } + + /// Error types for ``HTTP1ProxyConnectHandler`` + public struct Error: Swift.Error { + fileprivate enum Details { + case proxyAuthenticationRequired + case invalidProxyResponseHead(head: HTTPResponseHead) + case invalidProxyResponse + case remoteConnectionClosed + case httpProxyHandshakeTimeout + case noResult + case channelUnexpectedlyInactive + case droppedWrites + } + + final class Storage: Sendable { + fileprivate let details: Details + public let file: String + public let line: UInt + + fileprivate init(error details: Details, file: String, line: UInt) { + self.details = details + self.file = file + self.line = line + } + } + + fileprivate let store: Storage + + fileprivate init(error: Details, file: String, line: UInt) { + self.store = Storage(error: error, file: file, line: line) + } + + /// Proxy response status `407` indicates that authentication is required + public static func proxyAuthenticationRequired(file: String = #file, line: UInt = #line) -> Error { + Error(error: .proxyAuthenticationRequired, file: file, line: line) + } + + /// Proxy response contains unexpected status + public static func invalidProxyResponseHead(_ head: HTTPResponseHead, file: String = #file, line: UInt = #line) -> Error { + Error(error: .invalidProxyResponseHead(head: head), file: file, line: line) + } + + /// Proxy response contains unexpected body + public static func invalidProxyResponse(file: String = #file, line: UInt = #line) -> Error { + Error(error: .invalidProxyResponse, file: file, line: line) + } + + /// Connection has been closed for ongoing request + public static func remoteConnectionClosed(file: String = #file, line: UInt = #line) -> Error { + Error(error: .remoteConnectionClosed, file: file, line: line) + } + + /// Proxy connection handshake has timed out + public static func httpProxyHandshakeTimeout(file: String = #file, line: UInt = #line) -> Error { + Error(error: .httpProxyHandshakeTimeout, file: file, line: line) + } + + /// Handler was removed before we received a result for the request + public static func noResult(file: String = #file, line: UInt = #line) -> Error { + Error(error: .noResult, file: file, line: line) + } + + /// Handler became unexpectedly inactive before a connection was made + public static func channelUnexpectedlyInactive(file: String = #file, line: UInt = #line) -> Error { + Error(error: .channelUnexpectedlyInactive, file: file, line: line) + } + + public static func droppedWrites(file: String = #file, line: UInt = #line) -> Error { + Error(error: .droppedWrites, file: file, line: line) + } + + fileprivate var errorCode: Int { + switch self.store.details { + case .proxyAuthenticationRequired: + return 0 + case .invalidProxyResponseHead: + return 1 + case .invalidProxyResponse: + return 2 + case .remoteConnectionClosed: + return 3 + case .httpProxyHandshakeTimeout: + return 4 + case .noResult: + return 5 + case .channelUnexpectedlyInactive: + return 6 + case .droppedWrites: + return 7 + } + } + } + +} + +extension NIOHTTP1ProxyConnectHandler.Error: Hashable { + // compare only the kind of error, not the associated response head + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.errorCode == rhs.errorCode + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(self.errorCode) + } +} + + +extension NIOHTTP1ProxyConnectHandler.Error: CustomStringConvertible { + public var description: String { + "\(self.store.details.description) (\(self.store.file): \(self.store.line))" + } +} + +extension NIOHTTP1ProxyConnectHandler.Error.Details: CustomStringConvertible { + public var description: String { + switch self { + case .proxyAuthenticationRequired: + return "Proxy Authentication Required" + case .invalidProxyResponseHead(let head): + return "Invalid Proxy Response Head: \(head)" + case .invalidProxyResponse: + return "Invalid Proxy Response" + case .remoteConnectionClosed: + return "Remote Connection Closed" + case .httpProxyHandshakeTimeout: + return "HTTP Proxy Handshake Timeout" + case .noResult: + return "No Result" + case .channelUnexpectedlyInactive: + return "Channel Unexpectedly Inactive" + case .droppedWrites: + return "Handler Was Removed with Writes Left in the Buffer" + } + } +} diff --git a/Sources/NIOExtras/MarkedCircularBuffer+PopFirstCheckMarked.swift b/Sources/NIOExtras/MarkedCircularBuffer+PopFirstCheckMarked.swift new file mode 100644 index 00000000..03e1825b --- /dev/null +++ b/Sources/NIOExtras/MarkedCircularBuffer+PopFirstCheckMarked.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +extension MarkedCircularBuffer { + @inlinable + internal mutating func popFirstCheckMarked() -> (Element, Bool)? { + let marked = self.markedElementIndex == self.startIndex + return self.popFirst().map { ($0, marked) } + } +} diff --git a/Sources/NIOExtras/QuiescingHelper.swift b/Sources/NIOExtras/QuiescingHelper.swift index 846fe177..e66de895 100644 --- a/Sources/NIOExtras/QuiescingHelper.swift +++ b/Sources/NIOExtras/QuiescingHelper.swift @@ -23,23 +23,25 @@ private enum ShutdownError: Error { /// `channelAdded` method in the same event loop tick as the `Channel` is actually created. private final class ChannelCollector { enum LifecycleState { - case upAndRunning - case shuttingDown + case upAndRunning( + openChannels: [ObjectIdentifier: Channel], + serverChannel: Channel + ) + case shuttingDown( + openChannels: [ObjectIdentifier: Channel], + fullyShutdownPromise: EventLoopPromise + ) case shutdownCompleted } - private var openChannels: [ObjectIdentifier: Channel] = [:] - private let serverChannel: Channel - private var fullyShutdownPromise: EventLoopPromise? = nil - private var lifecycleState = LifecycleState.upAndRunning + private var lifecycleState: LifecycleState - private var eventLoop: EventLoop { - return self.serverChannel.eventLoop - } + private let eventLoop: EventLoop /// Initializes a `ChannelCollector` for `Channel`s accepted by `serverChannel`. init(serverChannel: Channel) { - self.serverChannel = serverChannel + self.eventLoop = serverChannel.eventLoop + self.lifecycleState = .upAndRunning(openChannels: [:], serverChannel: serverChannel) } /// Add a channel to the `ChannelCollector`. @@ -51,30 +53,64 @@ private final class ChannelCollector { func channelAdded(_ channel: Channel) throws { self.eventLoop.assertInEventLoop() - guard self.lifecycleState != .shutdownCompleted else { + switch self.lifecycleState { + case .upAndRunning(var openChannels, let serverChannel): + openChannels[ObjectIdentifier(channel)] = channel + self.lifecycleState = .upAndRunning(openChannels: openChannels, serverChannel: serverChannel) + + case .shuttingDown(var openChannels, let fullyShutdownPromise): + openChannels[ObjectIdentifier(channel)] = channel + channel.eventLoop.execute { + channel.pipeline.fireUserInboundEventTriggered(ChannelShouldQuiesceEvent()) + } + self.lifecycleState = .shuttingDown(openChannels: openChannels, fullyShutdownPromise: fullyShutdownPromise) + + case .shutdownCompleted: channel.close(promise: nil) throw ShutdownError.alreadyShutdown } - - self.openChannels[ObjectIdentifier(channel)] = channel } private func shutdownCompleted() { self.eventLoop.assertInEventLoop() - assert(self.lifecycleState == .shuttingDown) - self.lifecycleState = .shutdownCompleted - self.fullyShutdownPromise?.succeed(()) + switch self.lifecycleState { + case .upAndRunning: + preconditionFailure("This can never happen because we transition to shuttingDown first") + + case .shuttingDown(_, let fullyShutdownPromise): + self.lifecycleState = .shutdownCompleted + fullyShutdownPromise.succeed(()) + + case .shutdownCompleted: + preconditionFailure("We should only complete the shutdown once") + } } private func channelRemoved0(_ channel: Channel) { self.eventLoop.assertInEventLoop() - precondition(self.openChannels.keys.contains(ObjectIdentifier(channel)), - "channel \(channel) not in ChannelCollector \(self.openChannels)") - self.openChannels.removeValue(forKey: ObjectIdentifier(channel)) - if self.lifecycleState != .upAndRunning && self.openChannels.isEmpty { - shutdownCompleted() + switch self.lifecycleState { + case .upAndRunning(var openChannels, let serverChannel): + let removedChannel = openChannels.removeValue(forKey: ObjectIdentifier(channel)) + + precondition(removedChannel != nil, "channel \(channel) not in ChannelCollector \(openChannels)") + + self.lifecycleState = .upAndRunning(openChannels: openChannels, serverChannel: serverChannel) + + case .shuttingDown(var openChannels, let fullyShutdownPromise): + let removedChannel = openChannels.removeValue(forKey: ObjectIdentifier(channel)) + + precondition(removedChannel != nil, "channel \(channel) not in ChannelCollector \(openChannels)") + + if openChannels.isEmpty { + self.shutdownCompleted() + } else { + self.lifecycleState = .shuttingDown(openChannels: openChannels, fullyShutdownPromise: fullyShutdownPromise) + } + + case .shutdownCompleted: + preconditionFailure("We should not have channels removed after transitioned to completed") } } @@ -96,44 +132,39 @@ private final class ChannelCollector { private func initiateShutdown0(promise: EventLoopPromise?) { self.eventLoop.assertInEventLoop() - precondition(self.lifecycleState == .upAndRunning) - self.lifecycleState = .shuttingDown + switch self.lifecycleState { + case .upAndRunning(let openChannels, let serverChannel): + let fullyShutdownPromise = promise ?? serverChannel.eventLoop.makePromise(of: Void.self) - if let promise = promise { - if let alreadyExistingPromise = self.fullyShutdownPromise { - alreadyExistingPromise.futureResult.cascade(to: promise) - } else { - self.fullyShutdownPromise = promise - } - } + self.lifecycleState = .shuttingDown(openChannels: openChannels, fullyShutdownPromise: fullyShutdownPromise) - self.serverChannel.close().cascadeFailure(to: self.fullyShutdownPromise) + serverChannel.pipeline.fireUserInboundEventTriggered(ChannelShouldQuiesceEvent()) + serverChannel.close().cascadeFailure(to: fullyShutdownPromise) - for channel in self.openChannels.values { - channel.eventLoop.execute { - channel.pipeline.fireUserInboundEventTriggered(ChannelShouldQuiesceEvent()) + for channel in openChannels.values { + channel.eventLoop.execute { + channel.pipeline.fireUserInboundEventTriggered(ChannelShouldQuiesceEvent()) + } + } + + if openChannels.isEmpty { + self.shutdownCompleted() } - } - if self.openChannels.isEmpty { - shutdownCompleted() + case .shuttingDown(_, let fullyShutdownPromise): + fullyShutdownPromise.futureResult.cascade(to: promise) + + case .shutdownCompleted: + promise?.succeed(()) } } /// Initiate the shutdown fulfilling `promise` when all the previously registered `Channel`s have been closed. /// /// - parameters: - /// - promise: The `EventLoopPromise` to fulfill when the shutdown of all previously registered `Channel`s has been completed. + /// - promise: The `EventLoopPromise` to fulfil when the shutdown of all previously registered `Channel`s has been completed. func initiateShutdown(promise: EventLoopPromise?) { - if self.serverChannel.eventLoop.inEventLoop { - self.serverChannel.pipeline.fireUserInboundEventTriggered(ChannelShouldQuiesceEvent()) - } else { - self.eventLoop.execute { - self.serverChannel.pipeline.fireUserInboundEventTriggered(ChannelShouldQuiesceEvent()) - } - } - if self.eventLoop.inEventLoop { self.initiateShutdown0(promise: promise) } else { @@ -144,7 +175,6 @@ private final class ChannelCollector { } } - extension ChannelCollector: @unchecked Sendable {} /// A `ChannelHandler` that adds all channels that it receives through the `ChannelPipeline` to a `ChannelCollector`. @@ -173,7 +203,7 @@ private final class CollectAcceptedChannelsHandler: ChannelInboundHandler { do { try self.channelCollector.channelAdded(channel) let closeFuture = channel.closeFuture - closeFuture.whenComplete { (_: Result<(), Error>) in + closeFuture.whenComplete { (_: Result) in self.channelCollector.channelRemoved(channel) } context.fireChannelRead(data) @@ -216,6 +246,8 @@ private final class CollectAcceptedChannelsHandler: ChannelInboundHandler { /// try fullyShutdownPromise.futureResult.wait() /// public final class ServerQuiescingHelper { + /// The `ServerQuiescingHelper` was never used to create a channel handler. + public struct UnusedQuiescingHelperError: Error {} private let channelCollectorPromise: EventLoopPromise /// Initialize with a given `EventLoopGroup`. @@ -226,6 +258,10 @@ public final class ServerQuiescingHelper { self.channelCollectorPromise = group.next().makePromise() } + deinit { + self.channelCollectorPromise.fail(UnusedQuiescingHelperError()) + } + /// Create the `ChannelHandler` for the server `channel` to collect all accepted child `Channel`s. /// /// - parameters: @@ -256,6 +292,4 @@ public final class ServerQuiescingHelper { } } -extension ServerQuiescingHelper: Sendable { - -} +extension ServerQuiescingHelper: Sendable {} diff --git a/Sources/NIOExtras/WritePCAPHandler.swift b/Sources/NIOExtras/WritePCAPHandler.swift index 0865d926..9f535b70 100644 --- a/Sources/NIOExtras/WritePCAPHandler.swift +++ b/Sources/NIOExtras/WritePCAPHandler.swift @@ -414,7 +414,7 @@ extension NIOWritePCAPHandler { /// A synchronised file sink that uses a `DispatchQueue` to do all the necessary write synchronously. /// /// A `SynchronizedFileSink` is thread-safe so can be used from any thread/`EventLoop`. After use, you - /// _must_ call `syncClose` on the `SynchronizedFileSink` to shut it and all the associated resources down. Failing + /// _must_ call `syncClose` or `close` on the `SynchronizedFileSink` to shut it and all the associated resources down. Failing /// to do so triggers undefined behaviour. public final class SynchronizedFileSink { private let fileHandle: NIOFileHandle @@ -497,17 +497,44 @@ extension NIOWritePCAPHandler { self.workQueue = DispatchQueue(label: "io.swiftnio.extras.WritePCAPHandler.SynchronizedFileSink.workQueue") self.errorHandler = errorHandler } - + + #if swift(>=5.7) + /// Synchronously close this `SynchronizedFileSink` and any associated resources. + /// + /// After use, it is mandatory to close a `SynchronizedFileSink` exactly once. `syncClose` may be called from + /// any thread but not from an `EventLoop` as it will block, and may not be called from an async context. + @available(*, noasync, message: "syncClose() can block indefinitely, prefer close()", renamed: "close()") + public func syncClose() throws { + try self._syncClose() + } + #else /// Synchronously close this `SynchronizedFileSink` and any associated resources. /// /// After use, it is mandatory to close a `SynchronizedFileSink` exactly once. `syncClose` may be called from - /// any thread but not from an `EventLoop` as it will block. + /// any thread but not from an `EventLoop` as it will block, and may not be called from an async context. public func syncClose() throws { + try self._syncClose() + } + #endif + + private func _syncClose() throws { self.writesGroup.wait() try self.workQueue.sync { try self.fileHandle.close() } } + + /// Close this `SynchronizedFileSink` and any associated resources. + /// + /// After use, it is mandatory to close a `SynchronizedFileSink` exactly once. + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func close() async throws { + try await withCheckedThrowingContinuation { continuation in + self.workQueue.async { + continuation.resume(with: Result { try self.fileHandle.close() }) + } + } + } public func write(buffer: ByteBuffer) { self.workQueue.async(group: self.writesGroup) { diff --git a/Sources/NIONFS3/MountTypes+Mount.swift b/Sources/NIONFS3/MountTypes+Mount.swift new file mode 100644 index 00000000..2ada4c70 --- /dev/null +++ b/Sources/NIONFS3/MountTypes+Mount.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - Mount +public struct MountCallMount: Hashable & Sendable { + public init(dirPath: String) { + self.dirPath = dirPath + } + + public var dirPath: String +} + +public struct MountReplyMount: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Hashable & Sendable { + public init(fileHandle: NFS3FileHandle, authFlavors: [RPCAuthFlavor] = [.unix]) { + self.fileHandle = fileHandle + self.authFlavors = authFlavors + } + + public var fileHandle: NFS3FileHandle + public var authFlavors: [RPCAuthFlavor] = [.unix] + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFS3CallMount() throws -> MountCallMount { + let dirPath = try self.readNFS3String() + return MountCallMount(dirPath: dirPath) + } + + @discardableResult public mutating func writeNFS3CallMount(_ call: MountCallMount) -> Int { + self.writeNFS3String(call.dirPath) + } + + @discardableResult public mutating func writeNFS3ReplyMount(_ reply: MountReplyMount) -> Int { + var bytesWritten = self.writeNFS3ResultStatus(reply.result) + + switch reply.result { + case .okay(let reply): + bytesWritten += self.writeNFS3FileHandle(reply.fileHandle) + precondition(reply.authFlavors == [.unix] || reply.authFlavors == [.noAuth], + "Sorry, anything but [.unix] / [.system] / [.noAuth] unimplemented.") + bytesWritten += self.writeInteger(UInt32(reply.authFlavors.count), as: UInt32.self) + for flavor in reply.authFlavors { + bytesWritten += self.writeInteger(flavor.rawValue, as: UInt32.self) + } + case .fail(_, _): + () + } + + return bytesWritten + } + + public mutating func readNFS3ReplyMount() throws -> MountReplyMount { + let result = try self.readNFS3Result(readOkay: { buffer -> MountReplyMount.Okay in + let fileHandle = try buffer.readNFS3FileHandle() + let authFlavors = try buffer.readNFS3List(readEntry: { buffer in + try buffer.readRPCAuthFlavor() + }) + return MountReplyMount.Okay(fileHandle: fileHandle, authFlavors: authFlavors) + + }, + readFail: { _ in NFS3Nothing() }) + return MountReplyMount(result: result) + } +} diff --git a/Sources/NIONFS3/MountTypes+Null.swift b/Sources/NIONFS3/MountTypes+Null.swift new file mode 100644 index 00000000..8dc5f98d --- /dev/null +++ b/Sources/NIONFS3/MountTypes+Null.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - Null +public struct MountCallNull: Hashable & Sendable { + public init() {} +} + +extension ByteBuffer { + public mutating func readMountCallNull() throws -> MountCallNull { + return MountCallNull() + } + + @discardableResult public mutating func writeMountCallNull(_ call: MountCallNull) -> Int { + return 0 + } +} diff --git a/Sources/NIONFS3/MountTypes+Unmount.swift b/Sources/NIONFS3/MountTypes+Unmount.swift new file mode 100644 index 00000000..d1122dd4 --- /dev/null +++ b/Sources/NIONFS3/MountTypes+Unmount.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - Unmount +public struct MountCallUnmount: Hashable & Sendable { + public init(dirPath: String) { + self.dirPath = dirPath + } + + public var dirPath: String +} + +public struct MountReplyUnmount: Hashable & Sendable { + public init() {} +} + +extension ByteBuffer { + public mutating func readNFS3CallUnmount() throws -> MountCallUnmount { + let dirPath = try self.readNFS3String() + return MountCallUnmount(dirPath: dirPath) + } + + @discardableResult public mutating func writeNFS3CallUnmount(_ call: MountCallUnmount) -> Int { + self.writeNFS3String(call.dirPath) + } + + @discardableResult public mutating func writeNFS3ReplyUnmount(_ reply: MountReplyUnmount) -> Int { + return 0 + } + + public mutating func readNFS3ReplyUnmount() throws -> MountReplyUnmount { + return MountReplyUnmount() + } +} diff --git a/Sources/NIONFS3/NFSCallDecoder.swift b/Sources/NIONFS3/NFSCallDecoder.swift new file mode 100644 index 00000000..25616655 --- /dev/null +++ b/Sources/NIONFS3/NFSCallDecoder.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +public struct NFS3CallDecoder: NIOSingleStepByteToMessageDecoder { + public typealias InboundOut = RPCNFS3Call + + public init() {} + + public mutating func decode(buffer: inout ByteBuffer) throws -> RPCNFS3Call? { + guard let message = try buffer.readRPCMessage() else { + return nil + } + + guard case (.call(let call), var body) = message else { + throw NFS3Error.wrongMessageType(message.0) + } + + return try body.readNFS3Call(rpc: call) + } + + public mutating func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> RPCNFS3Call? { + return try self.decode(buffer: &buffer) + } +} diff --git a/Sources/NIONFS3/NFSCallEncoder.swift b/Sources/NIONFS3/NFSCallEncoder.swift new file mode 100644 index 00000000..6867a8a9 --- /dev/null +++ b/Sources/NIONFS3/NFSCallEncoder.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +public struct NFS3CallEncoder: MessageToByteEncoder { + public typealias OutboundIn = RPCNFS3Call + + public init() {} + + public func encode(data: RPCNFS3Call, out: inout ByteBuffer) throws { + out.writeRPCNFS3Call(data) + } +} diff --git a/Sources/NIONFS3/NFSFileSystem+FuturesAPI.swift b/Sources/NIONFS3/NFSFileSystem+FuturesAPI.swift new file mode 100644 index 00000000..7929e6e4 --- /dev/null +++ b/Sources/NIONFS3/NFSFileSystem+FuturesAPI.swift @@ -0,0 +1,185 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +extension NFS3FileSystemNoAuth { + public func mount(_ call: MountCallMount, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: MountReplyMount.self) + if eventLoop.inEventLoop { + self.mount(call, promise: promise) + } else { + eventLoop.execute { + self.mount(call, promise: promise) + } + } + return promise.futureResult + } + + public func unmount(_ call: MountCallUnmount, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: MountReplyUnmount.self) + if eventLoop.inEventLoop { + self.unmount(call, promise: promise) + } else { + eventLoop.execute { + self.unmount(call, promise: promise) + } + } + return promise.futureResult + } + + public func getattr(_ call: NFS3CallGetAttr, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyGetAttr.self) + if eventLoop.inEventLoop { + self.getattr(call, promise: promise) + } else { + eventLoop.execute { + self.getattr(call, promise: promise) + } + } + return promise.futureResult + } + + public func fsinfo(_ call: NFS3CallFSInfo, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyFSInfo.self) + if eventLoop.inEventLoop { + self.fsinfo(call, promise: promise) + } else { + eventLoop.execute { + self.fsinfo(call, promise: promise) + } + } + return promise.futureResult + } + + public func pathconf(_ call: NFS3CallPathConf, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyPathConf.self) + if eventLoop.inEventLoop { + self.pathconf(call, promise: promise) + } else { + eventLoop.execute { + self.pathconf(call, promise: promise) + } + } + return promise.futureResult + } + + public func fsstat(_ call: NFS3CallFSStat, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyFSStat.self) + if eventLoop.inEventLoop { + self.fsstat(call, promise: promise) + } else { + eventLoop.execute { + self.fsstat(call, promise: promise) + } + } + return promise.futureResult + } + + public func access(_ call: NFS3CallAccess, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyAccess.self) + if eventLoop.inEventLoop { + self.access(call, promise: promise) + } else { + eventLoop.execute { + self.access(call, promise: promise) + } + } + return promise.futureResult + } + + public func lookup(_ call: NFS3CallLookup, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyLookup.self) + if eventLoop.inEventLoop { + self.lookup(call, promise: promise) + } else { + eventLoop.execute { + self.lookup(call, promise: promise) + } + } + return promise.futureResult + } + + public func readdirplus(_ call: NFS3CallReadDirPlus, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyReadDirPlus.self) + if eventLoop.inEventLoop { + self.readdirplus(call, promise: promise) + } else { + eventLoop.execute { + self.readdirplus(call, promise: promise) + } + } + return promise.futureResult + } + + public func read(_ call: NFS3CallRead, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyRead.self) + if eventLoop.inEventLoop { + self.read(call, promise: promise) + } else { + eventLoop.execute { + self.read(call, promise: promise) + } + } + return promise.futureResult + } + + public func readlink(_ call: NFS3CallReadlink, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyReadlink.self) + if eventLoop.inEventLoop { + self.readlink(call, promise: promise) + } else { + eventLoop.execute { + self.readlink(call, promise: promise) + } + } + return promise.futureResult + } + + public func setattr(_ call: NFS3CallSetattr, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplySetattr.self) + if eventLoop.inEventLoop { + self.setattr(call, promise: promise) + } else { + eventLoop.execute { + self.setattr(call, promise: promise) + } + } + return promise.futureResult + } + + public func shutdown(eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: Void.self) + if eventLoop.inEventLoop { + self.shutdown(promise: promise) + } else { + eventLoop.execute { + self.shutdown(promise: promise) + } + } + return promise.futureResult + } + + public func readdir(_ call: NFS3CallReadDir, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyReadDir.self) + if eventLoop.inEventLoop { + self.readdir(call, promise: promise) + } else { + eventLoop.execute { + self.readdir(call, promise: promise) + } + } + return promise.futureResult + } +} diff --git a/Sources/NIONFS3/NFSFileSystem.swift b/Sources/NIONFS3/NFSFileSystem.swift new file mode 100644 index 00000000..3b07648b --- /dev/null +++ b/Sources/NIONFS3/NFSFileSystem.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +public protocol NFS3FileSystemNoAuth { + func mount(_ call: MountCallMount, promise: EventLoopPromise) + func unmount(_ call: MountCallUnmount, promise: EventLoopPromise) + func getattr(_ call: NFS3CallGetAttr, promise: EventLoopPromise) + func fsinfo(_ call: NFS3CallFSInfo, promise: EventLoopPromise) + func pathconf(_ call: NFS3CallPathConf, promise: EventLoopPromise) + func fsstat(_ call: NFS3CallFSStat, promise: EventLoopPromise) + func access(_ call: NFS3CallAccess, promise: EventLoopPromise) + func lookup(_ call: NFS3CallLookup, promise: EventLoopPromise) + func readdirplus(_ call: NFS3CallReadDirPlus, promise: EventLoopPromise) + func read(_ call: NFS3CallRead, promise: EventLoopPromise) + func readlink(_ call: NFS3CallReadlink, promise: EventLoopPromise) + func setattr(_ call: NFS3CallSetattr, promise: EventLoopPromise) + func readdir(_ call: NFS3CallReadDir, promise: EventLoopPromise) + + func shutdown(promise: EventLoopPromise) +} + +extension NFS3FileSystemNoAuth { + public func readdir(_ call: NFS3CallReadDir, promise originalPromise: EventLoopPromise) { + let promise = originalPromise.futureResult.eventLoop.makePromise(of: NFS3ReplyReadDirPlus.self) + self.readdirplus(NFS3CallReadDirPlus(fileHandle: call.fileHandle, + cookie: call.cookie, + cookieVerifier: call.cookieVerifier, + dirCount: NFS3Count(rawValue: .max), + maxCount: call.maxResultByteCount), + + promise: promise) + + promise.futureResult.whenComplete { readDirPlusResult in + switch readDirPlusResult { + case .success(let readDirPlusSuccessResult): + switch readDirPlusSuccessResult.result { + case .okay(let readDirPlusOkay): + originalPromise.succeed(NFS3ReplyReadDir(result: .okay(.init(cookieVerifier: readDirPlusOkay.cookieVerifier, + entries: readDirPlusOkay.entries.map { readDirPlusEntry in + NFS3ReplyReadDir.Entry(fileID: readDirPlusEntry.fileID, + fileName: readDirPlusEntry.fileName, + cookie: readDirPlusEntry.cookie) + }, eof: readDirPlusOkay.eof)))) + case .fail(let nfsStatus, let readDirPlusFailure): + originalPromise.succeed(NFS3ReplyReadDir(result: .fail(nfsStatus, + .init(dirAttributes: readDirPlusFailure.dirAttributes)))) + + } + case .failure(let error): + originalPromise.fail(error) + } + } + } +} diff --git a/Sources/NIONFS3/NFSFileSystemHandler.swift b/Sources/NIONFS3/NFSFileSystemHandler.swift new file mode 100644 index 00000000..cc2739ee --- /dev/null +++ b/Sources/NIONFS3/NFSFileSystemHandler.swift @@ -0,0 +1,96 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +/// `ChannelHandler` which implements NFS calls & replies the user implements as a `NFS3FileSystemNoAuth`. +/// +/// `NFS3FileSystemNoAuthHandler` is an all-in-one SwiftNIO `ChannelHandler` that implements an NFS3 server. Every call +/// it receives will be forwarded to the user-provided `FS` file system implementation. +/// +/// `NFS3FileSystemNoAuthHandler` ignores any [SUN RPC](https://datatracker.ietf.org/doc/html/rfc5531) credentials / +/// verifiers and always replies with `AUTH_NONE`. If you need to implement access control via UNIX user/group, this +/// handler will not be enough. It assumes that every call is allowed. Please note that this is not a security risk +/// because NFS3 tranditionally just trusts the UNIX uid/gid that the client provided. So there's no security value +/// added by verifying them. However, the client may rely on the server to check the UNIX permissions (whilst trusting +/// the uid/gid) which cannot be done with this handler. +public final class NFS3FileSystemNoAuthHandler: ChannelDuplexHandler, NFS3FileSystemResponder { + public typealias OutboundIn = Never + public typealias InboundIn = RPCNFS3Call + public typealias OutboundOut = RPCNFS3Reply + + private let filesystem: FS + private let rpcReplySuccess: RPCReplyStatus = .messageAccepted(.init(verifier: .init(flavor: .noAuth, + opaque: nil), + status: .success)) + private var invoker: NFS3FileSystemInvoker>? + private var context: ChannelHandlerContext? = nil + + public init(_ fs: FS) { + self.filesystem = fs + } + + public func handlerAdded(context: ChannelHandlerContext) { + self.context = context + self.invoker = NFS3FileSystemInvoker(sink: self, fileSystem: self.filesystem, eventLoop: context.eventLoop) + } + + public func handlerRemoved(context: ChannelHandlerContext) { + self.invoker = nil + self.context = nil + } + + func sendSuccessfulReply(_ reply: NFS3Reply, call: RPCNFS3Call) { + if let context = self.context { + let reply = RPCNFS3Reply(rpcReply: .init(xid: call.rpcCall.xid, + status: self.rpcReplySuccess), + nfsReply: reply) + context.writeAndFlush(self.wrapOutboundOut(reply), promise: nil) + } + } + + func sendError(_ error: Error, call: RPCNFS3Call) { + if let context = self.context { + let nfsErrorReply = RPCNFS3Reply(rpcReply: .init(xid: call.rpcCall.xid, + status: self.rpcReplySuccess), + nfsReply: .mount(.init(result: .fail(.errorSERVERFAULT, + NFS3Nothing())))) + context.writeAndFlush(self.wrapOutboundOut(nfsErrorReply), promise: nil) + context.fireErrorCaught(error) + } + } + + + public func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let call = self.unwrapInboundIn(data) + // ! is safe here because it's set on `handlerAdded` (and unset in `handlerRemoved`). Calling this outside that + // is programmer error. + self.invoker!.handleNFS3Call(call) + } + + public func errorCaught(context: ChannelHandlerContext, error: Error) { + switch error as? NFS3Error { + case .unknownProgramOrProcedure(.call(let call)): + let acceptedReply = RPCAcceptedReply(verifier: RPCOpaqueAuth(flavor: .noAuth, opaque: nil), + status: .procedureUnavailable) + let reply = RPCNFS3Reply(rpcReply: RPCReply(xid: call.xid, status: .messageAccepted(acceptedReply)), + nfsReply: .null) + context.writeAndFlush(self.wrapOutboundOut(reply), promise: nil) + return + default: + () + } + context.fireErrorCaught(error) + } +} diff --git a/Sources/NIONFS3/NFSFileSystemInvoker.swift b/Sources/NIONFS3/NFSFileSystemInvoker.swift new file mode 100644 index 00000000..f3cba7df --- /dev/null +++ b/Sources/NIONFS3/NFSFileSystemInvoker.swift @@ -0,0 +1,165 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +internal protocol NFS3FileSystemResponder { + func sendSuccessfulReply(_ reply: NFS3Reply, call: RPCNFS3Call) + func sendError(_ error: Error, call: RPCNFS3Call) +} + +internal struct NFS3FileSystemInvoker { + private let sink: Sink + private let fs: FS + private let eventLoop: EventLoop + + internal init(sink: Sink, fileSystem: FS, eventLoop: EventLoop) { + self.sink = sink + self.fs = fileSystem + self.eventLoop = eventLoop + } + + func shutdown() -> EventLoopFuture { + return self.fs.shutdown(eventLoop: self.eventLoop) + } + + func handleNFS3Call(_ callMessage: RPCNFS3Call) { + switch callMessage.nfsCall { + case .mountNull: + self.sink.sendSuccessfulReply(.mountNull, call: callMessage) + case .mount(let call): + self.fs.mount(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.mount(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .unmount(let call): + self.fs.unmount(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.unmount(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .null: + self.sink.sendSuccessfulReply(.null, call: callMessage) + case .getattr(let call): + self.fs.getattr(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.getattr(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .fsinfo(let call): + self.fs.fsinfo(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.fsinfo(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .pathconf(let call): + self.fs.pathconf(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.pathconf(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .fsstat(let call): + self.fs.fsstat(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.fsstat(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .access(let call): + self.fs.access(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.access(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .lookup(let call): + self.fs.lookup(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.lookup(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .readdirplus(let call): + self.fs.readdirplus(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.readdirplus(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .read(let call): + self.fs.read(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.read(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .readdir(let call): + self.fs.readdir(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.readdir(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .readlink(let call): + self.fs.readlink(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.readlink(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .setattr(let call): + self.fs.setattr(call, eventLoop: self.eventLoop).whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.setattr(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case ._PLEASE_DO_NOT_EXHAUSTIVELY_MATCH_THIS_ENUM_NEW_CASES_MIGHT_BE_ADDED_IN_THE_FUTURE: + // inside the module, matching exhaustively is okay + preconditionFailure("unknown NFS3 call, this should never happen. Please report a bug.") + } + } +} diff --git a/Sources/NIONFS3/NFSFileSystemServerHandler.swift b/Sources/NIONFS3/NFSFileSystemServerHandler.swift new file mode 100644 index 00000000..97b3f923 --- /dev/null +++ b/Sources/NIONFS3/NFSFileSystemServerHandler.swift @@ -0,0 +1,122 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +public final class NFS3FileSystemServerHandler { + public typealias InboundIn = ByteBuffer + public typealias OutboundOut = ByteBuffer + + private var error: Error? = nil + private var b2md = NIOSingleStepByteToMessageProcessor(NFS3CallDecoder(), + maximumBufferSize: 4 * 1024 * 1024) + private let filesystem: FS + private let rpcReplySuccess: RPCReplyStatus = .messageAccepted(.init(verifier: .init(flavor: .noAuth, + opaque: nil), + status: .success)) + private var invoker: NFS3FileSystemInvoker>? + private var context: ChannelHandlerContext? = nil + private var writeBuffer = ByteBuffer() + private let fillByteBuffer = ByteBuffer(repeating: 0x41, count: 4) + + public init(_ fs: FS) { + self.filesystem = fs + } +} + +extension NFS3FileSystemServerHandler: ChannelInboundHandler { + public func handlerAdded(context: ChannelHandlerContext) { + self.context = context + self.invoker = NFS3FileSystemInvoker(sink: self, fileSystem: self.filesystem, eventLoop: context.eventLoop) + } + + public func handlerRemoved(context: ChannelHandlerContext) { + self.invoker = nil + self.context = nil + } + + public func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let data = self.unwrapInboundIn(data) + guard self.error == nil else { + context.fireErrorCaught(ByteToMessageDecoderError.dataReceivedInErrorState(self.error!, + data)) + return + } + + do { + try self.b2md.process(buffer: data) { nfsCall in + self.invoker?.handleNFS3Call(nfsCall) + } + } catch { + self.error = error + self.invoker = nil + context.fireErrorCaught(error) + } + } + + public func errorCaught(context: ChannelHandlerContext, error: Error) { + switch error as? NFS3Error { + case .unknownProgramOrProcedure(.call(let call)): + let acceptedReply = RPCAcceptedReply(verifier: .init(flavor: .noAuth, opaque: nil), + status: .procedureUnavailable) + let reply = RPCNFS3Reply(rpcReply: RPCReply(xid: call.xid, status: .messageAccepted(acceptedReply)), + nfsReply: .null) + self.writeBuffer.clear() + self.writeBuffer.writeRPCNFS3Reply(reply) + return + default: + () + } + context.fireErrorCaught(error) + } +} + +extension NFS3FileSystemServerHandler: NFS3FileSystemResponder { + func sendSuccessfulReply(_ reply: NFS3Reply, call: RPCNFS3Call) { + if let context = self.context { + let reply = RPCNFS3Reply(rpcReply: .init(xid: call.rpcCall.xid, + status: self.rpcReplySuccess), + nfsReply: reply) + + self.writeBuffer.clear() + switch self.writeBuffer.writeRPCNFS3ReplyPartially(reply).1 { + case .doNothing: + context.writeAndFlush(self.wrapOutboundOut(self.writeBuffer), promise: nil) + case .writeBlob(let buffer, numberOfFillBytes: let fillBytes): + context.write(self.wrapOutboundOut(self.writeBuffer), promise: nil) + context.write(self.wrapOutboundOut(buffer), promise: nil) + if fillBytes > 0 { + var fillers = self.fillByteBuffer + context.write(self.wrapOutboundOut(fillers.readSlice(length: fillBytes)!), promise: nil) + } + context.flush() + } + } + } + + func sendError(_ error: Error, call: RPCNFS3Call) { + if let context = self.context { + let reply = RPCNFS3Reply(rpcReply: .init(xid: call.rpcCall.xid, + status: self.rpcReplySuccess), + nfsReply: .mount(.init(result: .fail(.errorSERVERFAULT, + NFS3Nothing())))) + + self.writeBuffer.clear() + self.writeBuffer.writeRPCNFS3Reply(reply) + + context.fireErrorCaught(error) + context.writeAndFlush(self.wrapOutboundOut(self.writeBuffer), promise: nil) + } + } +} diff --git a/Sources/NIONFS3/NFSReplyDecoder.swift b/Sources/NIONFS3/NFSReplyDecoder.swift new file mode 100644 index 00000000..5c346424 --- /dev/null +++ b/Sources/NIONFS3/NFSReplyDecoder.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +public struct NFS3ReplyDecoder: WriteObservingByteToMessageDecoder { + public typealias OutboundIn = RPCNFS3Call + public typealias InboundOut = RPCNFS3Reply + + private var procedures: [UInt32: RPCNFS3ProcedureID] + private let allowDuplicateReplies: Bool + + /// Initialize the `NFS3ReplyDecoder`. + /// + /// - Parameters: + /// - prepopulatedProcecedures: For testing and other more obscure purposes it might be useful to pre-seed the + /// decoder with some RPC numbers and their respective type. + /// - allowDuplicateReplies: Whether to fail when receiving more than one response for a given call. + public init(prepopulatedProcecedures: [UInt32: RPCNFS3ProcedureID]? = nil, + allowDuplicateReplies: Bool = false) { + self.procedures = prepopulatedProcecedures ?? [:] + self.allowDuplicateReplies = allowDuplicateReplies + } + + public mutating func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState { + guard let message = try buffer.readRPCMessage() else { + return .needMoreData + } + + guard case (.reply(let reply), var body) = message else { + throw NFS3Error.wrongMessageType(message.0) + } + + let progAndProc: RPCNFS3ProcedureID + if allowDuplicateReplies { + // for tests mainly + guard let p = self.procedures[reply.xid] else { + throw NFS3Error.unknownXID(reply.xid) + } + progAndProc = p + } else { + guard let p = self.procedures.removeValue(forKey: reply.xid) else { + throw NFS3Error.unknownXID(reply.xid) + } + progAndProc = p + } + + let nfsReply = try body.readNFS3Reply(programAndProcedure: progAndProc, rpcReply: reply) + context.fireChannelRead(self.wrapInboundOut(nfsReply)) + return .continue + } + + public mutating func write(data: RPCNFS3Call) { + self.procedures[data.rpcCall.xid] = data.rpcCall.programAndProcedure + } +} diff --git a/Sources/NIONFS3/NFSReplyEncoder.swift b/Sources/NIONFS3/NFSReplyEncoder.swift new file mode 100644 index 00000000..ff1a4964 --- /dev/null +++ b/Sources/NIONFS3/NFSReplyEncoder.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +public struct NFS3ReplyEncoder: MessageToByteEncoder { + public typealias OutboundIn = RPCNFS3Reply + + public init() {} + + public func encode(data: RPCNFS3Reply, out: inout ByteBuffer) throws { + out.writeRPCNFS3Reply(data) + } +} diff --git a/Sources/NIONFS3/NFSTypes+Access.swift b/Sources/NIONFS3/NFSTypes+Access.swift new file mode 100644 index 00000000..ddcff0e1 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Access.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - Access +public struct NFS3CallAccess: Hashable & Sendable { + public init(object: NFS3FileHandle, access: NFS3Access) { + self.object = object + self.access = access + } + + public var object: NFS3FileHandle + public var access: NFS3Access +} + +public struct NFS3ReplyAccess: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Hashable & Sendable { + public init(dirAttributes: NFS3FileAttr?, access: NFS3Access) { + self.dirAttributes = dirAttributes + self.access = access + } + + public var dirAttributes: NFS3FileAttr? + public var access: NFS3Access + } + + public struct Fail: Hashable & Sendable { + public init(dirAttributes: NFS3FileAttr?) { + self.dirAttributes = dirAttributes + } + + public var dirAttributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFS3CallAccess() throws -> NFS3CallAccess { + let fileHandle = try self.readNFS3FileHandle() + let access = try self.readNFS3Access() + return NFS3CallAccess(object: fileHandle, access: access) + } + + @discardableResult public mutating func writeNFS3CallAccess(_ call: NFS3CallAccess) -> Int { + return self.writeNFS3FileHandle(call.object) + + self.writeInteger(call.access.rawValue) + } + + public mutating func readNFS3ReplyAccess() throws -> NFS3ReplyAccess { + return NFS3ReplyAccess(result: try self.readNFS3Result( + readOkay: { buffer in + let attrs = try buffer.readNFS3Optional { buffer in + try buffer.readNFS3FileAttr() + } + let access = try buffer.readNFS3Access() + return NFS3ReplyAccess.Okay(dirAttributes: attrs, access: access) + }, + readFail: { buffer in + return NFS3ReplyAccess.Fail(dirAttributes: try buffer.readNFS3Optional { buffer in + try buffer.readNFS3FileAttr() + }) + })) + } + + @discardableResult public mutating func writeNFS3ReplyAccess(_ accessResult: NFS3ReplyAccess) -> Int { + var bytesWritten = 0 + + switch accessResult.result { + case .okay(let result): + bytesWritten += self.writeInteger(NFS3Status.ok.rawValue) + if let attrs = result.dirAttributes { + bytesWritten += self.writeInteger(1, as: UInt32.self) + + self.writeNFS3FileAttr(attrs) + } else { + bytesWritten += self.writeInteger(0, as: UInt32.self) + } + bytesWritten += self.writeInteger(result.access.rawValue) + case .fail(let status, let fail): + precondition(status != .ok) + bytesWritten += self.writeInteger(status.rawValue) + if let attrs = fail.dirAttributes { + bytesWritten += self.writeInteger(1, as: UInt32.self) + + self.writeNFS3FileAttr(attrs) + } else { + bytesWritten += self.writeInteger(0, as: UInt32.self) + } + } + return bytesWritten + } +} diff --git a/Sources/NIONFS3/NFSTypes+Common.swift b/Sources/NIONFS3/NFSTypes+Common.swift new file mode 100644 index 00000000..d927ea2e --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Common.swift @@ -0,0 +1,879 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - NIONFS3 Specifics +public struct RPCNFS3Call: Hashable & Sendable { + public init(rpcCall: RPCCall, nfsCall: NFS3Call) { + self.rpcCall = rpcCall + self.nfsCall = nfsCall + } + + public init(nfsCall: NFS3Call, + xid: UInt32, + credentials: RPCCredentials = .init(flavor: 0, length: 0, otherBytes: ByteBuffer()), + verifier: RPCOpaqueAuth = RPCOpaqueAuth(flavor: .noAuth)) { + var rpcCall = RPCCall(xid: xid, + rpcVersion: 2, + program: .max, // placeholder, overwritten below + programVersion: 3, + procedure: .max, // placeholder, overwritten below + credentials: credentials, + verifier: verifier) + + switch nfsCall { + case .mountNull: + rpcCall.programAndProcedure = .mountNull + case .mount: + rpcCall.programAndProcedure = .mountMount + case .unmount: + rpcCall.programAndProcedure = .mountUnmount + case .null: + rpcCall.programAndProcedure = .nfsNull + case .getattr: + rpcCall.programAndProcedure = .nfsGetAttr + case .fsinfo: + rpcCall.programAndProcedure = .nfsFSInfo + case .pathconf: + rpcCall.programAndProcedure = .nfsPathConf + case .fsstat: + rpcCall.programAndProcedure = .nfsFSStat + case .access: + rpcCall.programAndProcedure = .nfsAccess + case .lookup: + rpcCall.programAndProcedure = .nfsLookup + case .readdirplus: + rpcCall.programAndProcedure = .nfsReadDirPlus + case .readdir: + rpcCall.programAndProcedure = .nfsReadDir + case .read: + rpcCall.programAndProcedure = .nfsRead + case .readlink: + rpcCall.programAndProcedure = .nfsReadLink + case .setattr: + rpcCall.programAndProcedure = .nfsSetAttr + case ._PLEASE_DO_NOT_EXHAUSTIVELY_MATCH_THIS_ENUM_NEW_CASES_MIGHT_BE_ADDED_IN_THE_FUTURE: + // inside the module, matching exhaustively is okay + preconditionFailure("unknown NFS3 call, this should never happen. Please report a bug.") + } + + self = .init(rpcCall: rpcCall, nfsCall: nfsCall) + } + + public var rpcCall: RPCCall + public var nfsCall: NFS3Call +} + +extension RPCNFS3Call: Identifiable { + public typealias ID = UInt32 + + public var id: ID { + return self.rpcCall.xid + } +} + +public struct RPCNFS3Reply: Hashable & Sendable { + public init(rpcReply: RPCReply, nfsReply: NFS3Reply) { + self.rpcReply = rpcReply + self.nfsReply = nfsReply + } + + public var rpcReply: RPCReply + public var nfsReply: NFS3Reply +} + +extension RPCNFS3Reply: Identifiable { + public typealias ID = UInt32 + + public var id: ID { + return self.rpcReply.xid + } +} + +public enum NFS3Result { + case okay(Okay) + case fail(NFS3Status, Fail) +} + +extension NFS3Result: Hashable where Okay: Hashable, Fail: Hashable { +} + +extension NFS3Result: Equatable where Okay: Equatable, Fail: Equatable { +} + +extension NFS3Result: Sendable where Okay: Sendable, Fail: Sendable { +} + +extension NFS3Result { + public var status: NFS3Status { + switch self { + case .okay: + return .ok + case .fail(let status, _): + assert(status != .ok) + return status + } + } +} + +// MARK: - General +public struct NFS3FileMode: Hashable & Sendable { + public typealias RawValue = UInt32 + + public var rawValue: RawValue + + public init(rawValue: NFS3FileMode.RawValue) { + self.rawValue = rawValue + } +} + +extension NFS3FileMode: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = RawValue + + public init(integerLiteral value: RawValue) { + self = .init(rawValue: value) + } +} + +extension ByteBuffer { + public mutating func readNFS3FileMode() throws -> NFS3FileMode { + return NFS3FileMode(rawValue: try self.readNFS3Integer(as: NFS3FileMode.RawValue.self)) + } + + @discardableResult + public mutating func writeNFS3FileMode(_ value: NFS3FileMode) -> Int { + return self.writeInteger(value.rawValue) + } +} + +public struct NFS3UID: Hashable & Sendable { + public typealias RawValue = UInt32 + + public var rawValue: RawValue + + public init(rawValue: NFS3UID.RawValue) { + self.rawValue = rawValue + } +} + +extension NFS3UID: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = RawValue + + public init(integerLiteral value: RawValue) { + self = .init(rawValue: value) + } +} + +extension ByteBuffer { + public mutating func readNFS3UID() throws -> NFS3UID { + return NFS3UID(rawValue: try self.readNFS3Integer(as: NFS3UID.RawValue.self)) + } + + @discardableResult + public mutating func writeNFS3UID(_ value: NFS3UID) -> Int { + return self.writeInteger(value.rawValue) + } +} + +public struct NFS3GID: Hashable & Sendable { + public typealias RawValue = UInt32 + + public var rawValue: RawValue + + public init(rawValue: NFS3GID.RawValue) { + self.rawValue = rawValue + } +} + +extension NFS3GID: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = RawValue + + public init(integerLiteral value: RawValue) { + self = .init(rawValue: value) + } +} + +extension ByteBuffer { + public mutating func readNFS3GID() throws -> NFS3GID { + return NFS3GID(rawValue: try self.readNFS3Integer(as: NFS3GID.RawValue.self)) + } + + @discardableResult + public mutating func writeNFS3GID(_ value: NFS3GID) -> Int { + return self.writeInteger(value.rawValue) + } +} + +public struct NFS3Size: Hashable & Sendable { + public typealias RawValue = UInt64 + + public var rawValue: RawValue + + public init(rawValue: NFS3Size.RawValue) { + self.rawValue = rawValue + } +} + +extension NFS3Size: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = RawValue + + public init(integerLiteral value: RawValue) { + self = .init(rawValue: value) + } +} + +extension ByteBuffer { + public mutating func readNFS3Size() throws -> NFS3Size { + return NFS3Size(rawValue: try self.readNFS3Integer(as: NFS3Size.RawValue.self)) + } + + @discardableResult + public mutating func writeNFS3Size(_ value: NFS3Size) -> Int { + return self.writeInteger(value.rawValue) + } +} + +public struct NFS3SpecData: Hashable & Sendable { + public typealias RawValue = UInt64 + + public var rawValue: RawValue + + public init(rawValue: NFS3SpecData.RawValue) { + self.rawValue = rawValue + } +} + +extension NFS3SpecData: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = RawValue + + public init(integerLiteral value: RawValue) { + self = .init(rawValue: value) + } +} + +extension ByteBuffer { + public mutating func readNFS3SpecData() throws -> NFS3SpecData { + return NFS3SpecData(rawValue: try self.readNFS3Integer(as: NFS3SpecData.RawValue.self)) + } + + @discardableResult + public mutating func writeNFS3SpecData(_ value: NFS3SpecData) -> Int { + return self.writeInteger(value.rawValue) + } +} + +public struct NFS3FileID: Hashable & Sendable { + public typealias RawValue = UInt64 + + public var rawValue: RawValue + + public init(rawValue: NFS3FileID.RawValue) { + self.rawValue = rawValue + } +} + +extension NFS3FileID: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = RawValue + + public init(integerLiteral value: RawValue) { + self = .init(rawValue: value) + } +} + +extension ByteBuffer { + public mutating func readNFS3FileID() throws -> NFS3FileID { + return NFS3FileID(rawValue: try self.readNFS3Integer(as: NFS3FileID.RawValue.self)) + } + + @discardableResult + public mutating func writeNFS3FileID(_ value: NFS3FileID) -> Int { + return self.writeInteger(value.rawValue) + } +} + +public struct NFS3Cookie: Hashable & Sendable { + public typealias RawValue = UInt64 + + public var rawValue: RawValue + + public init(rawValue: NFS3Cookie.RawValue) { + self.rawValue = rawValue + } +} + +extension NFS3Cookie: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = RawValue + + public init(integerLiteral value: RawValue) { + self = .init(rawValue: value) + } +} + +extension ByteBuffer { + public mutating func readNFS3Cookie() throws -> NFS3Cookie { + return NFS3Cookie(rawValue: try self.readNFS3Integer(as: NFS3Cookie.RawValue.self)) + } + + @discardableResult + public mutating func writeNFS3Cookie(_ value: NFS3Cookie) -> Int { + return self.writeInteger(value.rawValue) + } +} + +public struct NFS3CookieVerifier: Hashable & Sendable { + public typealias RawValue = UInt64 + + public var rawValue: RawValue + + public init(rawValue: NFS3CookieVerifier.RawValue) { + self.rawValue = rawValue + } +} + +extension NFS3CookieVerifier: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = RawValue + + public init(integerLiteral value: RawValue) { + self = .init(rawValue: value) + } +} + +extension ByteBuffer { + public mutating func readNFS3CookieVerifier() throws -> NFS3CookieVerifier { + return NFS3CookieVerifier(rawValue: try self.readNFS3Integer(as: NFS3CookieVerifier.RawValue.self)) + } + + @discardableResult + public mutating func writeNFS3CookieVerifier(_ value: NFS3CookieVerifier) -> Int { + return self.writeInteger(value.rawValue) + } +} + +public struct NFS3Offset: Hashable & Sendable { + public typealias RawValue = UInt64 + + public var rawValue: RawValue + + public init(rawValue: NFS3Offset.RawValue) { + self.rawValue = rawValue + } +} + +extension NFS3Offset: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = RawValue + + public init(integerLiteral value: RawValue) { + self = .init(rawValue: value) + } +} + +extension ByteBuffer { + public mutating func readNFS3Offset() throws -> NFS3Offset { + return NFS3Offset(rawValue: try self.readNFS3Integer(as: NFS3Offset.RawValue.self)) + } + + @discardableResult + public mutating func writeNFS3Offset(_ value: NFS3Offset) -> Int { + return self.writeInteger(value.rawValue) + } +} + +public struct NFS3Count: Hashable & Sendable { + public typealias RawValue = UInt32 + + public var rawValue: RawValue + + public init(rawValue: NFS3Count.RawValue) { + self.rawValue = rawValue + } +} + +extension NFS3Count: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = RawValue + + public init(integerLiteral value: RawValue) { + self = .init(rawValue: value) + } +} + +extension ByteBuffer { + public mutating func readNFS3Count() throws -> NFS3Count { + return NFS3Count(rawValue: try self.readNFS3Integer(as: NFS3Count.RawValue.self)) + } + + @discardableResult + public mutating func writeNFS3Count(_ value: NFS3Count) -> Int { + return self.writeInteger(value.rawValue) + } +} + + +public struct NFS3Nothing: Hashable & Sendable { + public init() {} +} + +/// The status of an NFS3 operation. +/// +/// - seealso: https://www.rfc-editor.org/rfc/rfc1813#page-16 +public enum NFS3Status: UInt32, Sendable { + case ok = 0 + case errorPERM = 1 + case errorNOENT = 2 + case errorIO = 5 + case errorNXIO = 6 + case errorACCES = 13 + case errorEXIST = 17 + case errorXDEV = 18 + case errorNODEV = 19 + case errorNOTDIR = 20 + case errorISDIR = 21 + case errorINVAL = 22 + case errorFBIG = 27 + case errorNOSPC = 28 + case errorROFS = 30 + case errorMLINK = 31 + case errorNAMETOOLONG = 63 + case errorNOTEMPTY = 66 + case errorDQUOT = 69 + case errorSTALE = 70 + case errorREMOTE = 71 + case errorBADHANDLE = 10001 + case errorNOT_SYNC = 10002 + case errorBAD_COOKIE = 10003 + case errorNOTSUPP = 10004 + case errorTOOSMALL = 10005 + case errorSERVERFAULT = 10006 + case errorBADTYPE = 10007 + case errorJUKEBOX = 10008 +} + +/// Check the access rights to a file. +/// +/// - seealso: https://www.rfc-editor.org/rfc/rfc1813#page-40 +public struct NFS3Access: OptionSet & Hashable & Sendable { + public typealias RawValue = UInt32 + + public var rawValue: RawValue + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let read: NFS3Access = .init(rawValue: 0x0001) + public static let lookup: NFS3Access = .init(rawValue: 0x0002) + public static let modify: NFS3Access = .init(rawValue: 0x0004) + public static let extend: NFS3Access = .init(rawValue: 0x0008) + public static let delete: NFS3Access = .init(rawValue: 0x0010) + public static let execute: NFS3Access = .init(rawValue: 0x0020) + + public static let all: NFS3Access = [.read, .lookup, .modify, .extend, .delete, .execute] + public static let allReadOnly: NFS3Access = [.read, .lookup, .execute] +} + +extension ByteBuffer { + public mutating func readNFS3Access() throws -> NFS3Access { + return NFS3Access(rawValue: try self.readNFS3Integer(as: UInt32.self)) + } + + public mutating func writeNFS3Access(_ access: NFS3Access) { + self.writeInteger(access.rawValue) + } +} + +/// The filetype as defined in NFS3. +/// +/// - seealso: https://www.rfc-editor.org/rfc/rfc1813#page-20 +public enum NFS3FileType: UInt32, Sendable { + case regular = 1 + case directory = 2 + case blockDevice = 3 + case characterDevice = 4 + case link = 5 + case socket = 6 + case fifo = 7 +} + +public typealias NFS3Bool = Bool + +public struct NFS3FileHandle: Hashable & Sendable & CustomStringConvertible { + @usableFromInline + internal var _value: UInt64 + + public init(_ value: UInt64) { + self._value = value + } + + /// Initialize an ``NFS3FileHandle`` with the raw representation. + /// + /// The spec requires the representation to take up 64 bytes or fewer. + /// + /// - seealso: https://www.rfc-editor.org/rfc/rfc1813#page-106 + public init(_ bytes: ByteBuffer) { + precondition(bytes.readableBytes <= 64, "NFS3 mandates that file handles are NFS3_FHSIZE (64) bytes or less.") + precondition(bytes.readableBytes == MemoryLayout.size, + "Sorry, at the moment only file handles with exactly 8 bytes are implemented.") + var bytes = bytes + self = NFS3FileHandle(bytes.readInteger(as: UInt64.self)!) + } + + public var description: String { + return "NFS3FileHandle(\(self._value))" + } +} + +extension UInt64 { + // This initialiser is fallible because we only _currently_ require that all file handles be exactly 8 bytes + // long. This limitation should be removed in the future. + @inlinable + public init?(_ fileHandle: NFS3FileHandle) { + self = fileHandle._value + } +} + +extension UInt32 { + @inlinable + public init?(_ fileHandle: NFS3FileHandle) { + if let value = UInt32(exactly: fileHandle._value) { + self = value + } else { + return nil + } + } +} + + +public struct NFS3Time: Hashable & Sendable { + public init(seconds: UInt32, nanoseconds: UInt32) { + self.seconds = seconds + self.nanoseconds = nanoseconds + } + + public var seconds: UInt32 + public var nanoseconds: UInt32 +} + +public struct NFS3FileAttr: Hashable & Sendable { + public init(type: NFS3FileType, mode: NFS3FileMode, nlink: UInt32, uid: NFS3UID, gid: NFS3GID, size: NFS3Size, + used: NFS3Size, rdev: NFS3SpecData, fsid: UInt64, fileid: NFS3FileID, atime: NFS3Time, mtime: NFS3Time, + ctime: NFS3Time) { + self.type = type + self.mode = mode + self.nlink = nlink + self.uid = uid + self.gid = gid + self.size = size + self.used = used + self.rdev = rdev + self.fsid = fsid + self.fileid = fileid + self.atime = atime + self.mtime = mtime + self.ctime = ctime + } + + public var type: NFS3FileType + public var mode: NFS3FileMode + public var nlink: UInt32 + public var uid: NFS3UID + public var gid: NFS3GID + public var size: NFS3Size + public var used: NFS3Size + public var rdev: NFS3SpecData + public var fsid: UInt64 + public var fileid: NFS3FileID + public var atime: NFS3Time + public var mtime: NFS3Time + public var ctime: NFS3Time +} + +public struct NFS3WeakCacheConsistencyAttr: Hashable & Sendable { + public init(size: NFS3Size, mtime: NFS3Time, ctime: NFS3Time) { + self.size = size + self.mtime = mtime + self.ctime = ctime + } + + public var size: NFS3Size + public var mtime: NFS3Time + public var ctime: NFS3Time +} + +public struct NFS3WeakCacheConsistencyData: Hashable & Sendable { + public init(before: NFS3WeakCacheConsistencyAttr? = nil, after: NFS3FileAttr? = nil) { + self.before = before + self.after = after + } + + public var before: NFS3WeakCacheConsistencyAttr? + public var after: NFS3FileAttr? +} + +extension ByteBuffer { + public mutating func readNFS3WeakCacheConsistencyAttr() throws -> NFS3WeakCacheConsistencyAttr { + let size = try self.readNFS3Size() + let mtime = try self.readNFS3Time() + let ctime = try self.readNFS3Time() + + return .init(size: size, mtime: mtime, ctime: ctime) + } + + @discardableResult public mutating func writeNFS3WeakCacheConsistencyAttr(_ wccAttr: NFS3WeakCacheConsistencyAttr) -> Int { + return self.writeNFS3Size(wccAttr.size) + + self.writeNFS3Time(wccAttr.mtime) + + self.writeNFS3Time(wccAttr.ctime) + } + + public mutating func readNFS3WeakCacheConsistencyData() throws -> NFS3WeakCacheConsistencyData { + let before = try self.readNFS3Optional { try $0.readNFS3WeakCacheConsistencyAttr() } + let after = try self.readNFS3Optional { try $0.readNFS3FileAttr() } + + return .init(before: before, after: after) + } + + @discardableResult public mutating func writeNFS3WeakCacheConsistencyData(_ wccData: NFS3WeakCacheConsistencyData) -> Int { + return self.writeNFS3Optional(wccData.before, writer: { $0.writeNFS3WeakCacheConsistencyAttr($1) }) + + self.writeNFS3Optional(wccData.after, writer: { $0.writeNFS3FileAttr($1) }) + } + + public mutating func readNFS3Integer(as: I.Type = I.self) throws -> I { + if let value = self.readInteger(as: I.self) { + return value + } else { + throw NFS3Error.illegalRPCTooShort + } + } + + public mutating func readNFS3Blob() throws -> ByteBuffer { + let length = try self.readNFS3Integer(as: UInt32.self) + guard let blob = self.readSlice(length: Int(length)), + let _ = self.readSlice(length: nfsStringFillBytes(Int(length))) else { + throw NFS3Error.illegalRPCTooShort + } + return blob + } + + @discardableResult public mutating func writeNFS3Blob(_ blob: ByteBuffer) -> Int { + let byteCount = blob.readableBytes + return self.writeInteger(UInt32(byteCount)) + + self.writeImmutableBuffer(blob) + + self.writeRepeatingByte(0x42, count: nfsStringFillBytes(byteCount)) + } + + public mutating func readNFS3String() throws -> String { + let blob = try self.readNFS3Blob() + return String(buffer: blob) + } + + @discardableResult public mutating func writeNFS3String(_ string: String) -> Int { + let byteCount = string.utf8.count + return self.writeInteger(UInt32(byteCount)) + + self.writeString(string) + + self.writeRepeatingByte(0x42, count: nfsStringFillBytes(byteCount)) + } + + public mutating func readNFS3FileHandle() throws -> NFS3FileHandle { + guard let values = self.readMultipleIntegers(as: (UInt32, UInt64).self) else { + throw NFS3Error.illegalRPCTooShort + } + let length = values.0 + let id = values.1 + + // TODO: This is a temporary limitation to be lifted later. + guard length == MemoryLayout.size else { + throw NFS3Error.invalidFileHandleFormat(length: length) + } + return NFS3FileHandle(id) + } + + @discardableResult public mutating func writeNFS3FileHandle(_ fileHandle: NFS3FileHandle) -> Int { + // TODO: This ! is safe at the moment until the file handle == 64 bits limitation is lifted + let id = UInt64(fileHandle)! + return self.writeMultipleIntegers(UInt32(MemoryLayout.size(ofValue: id)), id) + } + + @discardableResult public mutating func writeNFS3FileType(_ fileType: NFS3FileType) -> Int { + self.writeInteger(fileType.rawValue) + } + + @discardableResult public mutating func writeNFS3Time(_ time: NFS3Time) -> Int { + self.writeMultipleIntegers(time.seconds, time.nanoseconds) + } + + public mutating func read3NFS3Times() throws -> (NFS3Time, NFS3Time, NFS3Time) { + guard let values = self.readMultipleIntegers(as: (UInt32, UInt32, UInt32, UInt32, UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + return (NFS3Time(seconds: values.0, nanoseconds: values.1), + NFS3Time(seconds: values.2, nanoseconds: values.3), + NFS3Time(seconds: values.4, nanoseconds: values.5)) + } + + @discardableResult public mutating func write3NFS3Times(_ time1: NFS3Time, _ time2: NFS3Time, _ time3: NFS3Time) -> Int { + self.writeMultipleIntegers(time1.seconds, time1.nanoseconds, + time2.seconds, time2.nanoseconds, + time3.seconds, time3.nanoseconds) + } + + public mutating func readNFS3Time() throws -> NFS3Time { + guard let values = self.readMultipleIntegers(as: (UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + + return .init(seconds: values.0, nanoseconds: values.1) + } + + public mutating func readNFS3FileType() throws -> NFS3FileType { + let typeRaw = try self.readNFS3Integer(as: UInt32.self) + if let type = NFS3FileType(rawValue: typeRaw) { + return type + } else { + throw NFS3Error.invalidFileType(typeRaw) + } + } + + public mutating func readNFS3FileAttr() throws -> NFS3FileAttr { + let type = try self.readNFS3FileType() + guard let values = self.readMultipleIntegers(as: (UInt32, UInt32, UInt32, UInt32, NFS3Size.RawValue, + NFS3Size.RawValue, UInt64, UInt64, NFS3FileID.RawValue, + UInt32, UInt32, UInt32, UInt32, UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + let mode = values.0 + let nlink = values.1 + let uid = values.2 + let gid = values.3 + let size = values.4 + let used = values.5 + let rdev = values.6 + let fsid = values.7 + let fileid = values.8 + let atime = NFS3Time(seconds: values.9, nanoseconds: values.10) + let mtime = NFS3Time(seconds: values.11, nanoseconds: values.12) + let ctime = NFS3Time(seconds: values.13, nanoseconds: values.14) + + return .init(type: type, mode: NFS3FileMode(rawValue: mode), nlink: nlink, + uid: NFS3UID(rawValue: uid), gid: NFS3GID(rawValue: gid), + size: NFS3Size(rawValue: size), used: NFS3Size(rawValue: used), + rdev: NFS3SpecData(rawValue: rdev), fsid: fsid, fileid: NFS3FileID(rawValue: fileid), + atime: atime, mtime: mtime, ctime: ctime) + } + + @discardableResult public mutating func writeNFS3FileAttr(_ attributes: NFS3FileAttr) -> Int { + return self.writeNFS3FileType(attributes.type) + + self.writeMultipleIntegers( + attributes.mode.rawValue, + attributes.nlink, + attributes.uid.rawValue, + attributes.gid.rawValue, + attributes.size.rawValue, + attributes.used.rawValue, + attributes.rdev.rawValue, + attributes.fsid, + attributes.fileid.rawValue, + attributes.atime.seconds, + attributes.atime.nanoseconds, + attributes.mtime.seconds, + attributes.mtime.nanoseconds, + attributes.ctime.seconds, + attributes.ctime.nanoseconds) + } + + @discardableResult public mutating func writeNFS3Bool(_ bool: NFS3Bool) -> Int { + self.writeInteger(bool == true ? 1 : 0, as: UInt32.self) + } + + public mutating func readNFS3Bool() throws -> Bool { + let rawValue = try self.readNFS3Integer(as: UInt32.self) + return rawValue != 0 + } + + public mutating func readNFS3Optional(_ reader: (inout ByteBuffer) throws -> T) rethrows -> T? { + if self.readInteger(as: UInt32.self) == 1 { + return try reader(&self) + } else { + return nil + } + } + + @discardableResult public mutating func writeNFS3Optional(_ value: T?, writer: (inout ByteBuffer, T) -> Int) -> Int { + if let value = value { + return self.writeInteger(1, as: UInt32.self) + + writer(&self, value) + } else { + return self.writeInteger(0, as: UInt32.self) + } + } + + public mutating func readNFS3List(readEntry: (inout ByteBuffer) throws -> Element) throws -> [Element] { + let count = try self.readNFS3Count().rawValue + var result: [Element] = [] + result.reserveCapacity(Int(count)) + + for _ in 0..(_ result: NFS3Result) -> Int { + self.writeInteger(result.status.rawValue, as: UInt32.self) + } + + public mutating func readNFS3Status() throws -> NFS3Status { + let rawValue = try self.readNFS3Integer(as: UInt32.self) + if let status = NFS3Status(rawValue: rawValue) { + return status + } else { + throw NFS3Error.invalidStatus(rawValue) + } + } + + public mutating func readRPCAuthFlavor() throws -> RPCAuthFlavor { + let rawValue = try self.readNFS3Integer(as: UInt32.self) + if let flavor = RPCAuthFlavor(rawValue: rawValue) { + return flavor + } else { + throw RPCErrors.invalidAuthFlavor(rawValue) + } + } + + public mutating func readNFS3Result(readOkay: (inout ByteBuffer) throws -> O, + readFail: (inout ByteBuffer) throws -> F) throws -> NFS3Result { + let status = try self.readNFS3Status() + switch status { + case .ok: + return .okay(try readOkay(&self)) + default: + return .fail(status, try readFail(&self)) + } + } +} + +public enum NFS3PartialWriteNextStep: Hashable & Sendable { + case doNothing + case writeBlob(ByteBuffer, numberOfFillBytes: Int) +} + +extension NFS3PartialWriteNextStep { + var bytesToFollow: Int { + switch self { + case .doNothing: + return 0 + case .writeBlob(let bytes, numberOfFillBytes: let fillBytes): + return bytes.readableBytes &+ fillBytes + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+Containers.swift b/Sources/NIONFS3/NFSTypes+Containers.swift new file mode 100644 index 00000000..60c5354d --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Containers.swift @@ -0,0 +1,527 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +public struct RPCNFS3ProcedureID: Hashable & Sendable { + public internal(set) var program: UInt32 + public internal(set) var procedure: UInt32 + + public static let mountNull: Self = .init(program: 100005, procedure: 0) + public static let mountMount: Self = .init(program: 100005, procedure: 1) + public static let mountDump: Self = .init(program: 100005, procedure: 2) // unimplemented + public static let mountUnmount: Self = .init(program: 100005, procedure: 3) + public static let mountUnmountAll: Self = .init(program: 100005, procedure: 4) // unimplemented + public static let mountExport: Self = .init(program: 100005, procedure: 5) // unimplemented + + // The source of truth for the values in the NFS program (`1000003`) can be found in the NFS RFC at + // https://www.rfc-editor.org/rfc/rfc1813#page-28 + public static let nfsNull: Self = .init(program: 100003, procedure: 0) + public static let nfsGetAttr: Self = .init(program: 100003, procedure: 1) + public static let nfsSetAttr: Self = .init(program: 100003, procedure: 2) + public static let nfsLookup: Self = .init(program: 100003, procedure: 3) + public static let nfsAccess: Self = .init(program: 100003, procedure: 4) + public static let nfsReadLink: Self = .init(program: 100003, procedure: 5) + public static let nfsRead: Self = .init(program: 100003, procedure: 6) + + public static let nfsWrite: Self = .init(program: 100003, procedure: 7) // unimplemented + public static let nfsCreate: Self = .init(program: 100003, procedure: 8) // unimplemented + public static let nfsMkDir: Self = .init(program: 100003, procedure: 9) // unimplemented + public static let nfsSymlink: Self = .init(program: 100003, procedure: 10) // unimplemented + public static let nfsMkNod: Self = .init(program: 100003, procedure: 11) // unimplemented + public static let nfsRemove: Self = .init(program: 100003, procedure: 12) // unimplemented + public static let nfsRmDir: Self = .init(program: 100003, procedure: 13) // unimplemented + public static let nfsRename: Self = .init(program: 100003, procedure: 14) // unimplemented + public static let nfsLink: Self = .init(program: 100003, procedure: 15) // unimplemented + public static let nfsReadDir: Self = .init(program: 100003, procedure: 16) + + public static let nfsReadDirPlus: Self = .init(program: 100003, procedure: 17) + public static let nfsFSStat: Self = .init(program: 100003, procedure: 18) + public static let nfsFSInfo: Self = .init(program: 100003, procedure: 19) + public static let nfsPathConf: Self = .init(program: 100003, procedure: 20) + + public static let nfsCommit: Self = .init(program: 100003, procedure: 21) // unimplemented +} + +extension RPCNFS3ProcedureID { + public init(_ nfsReply: NFS3Reply) { + switch nfsReply { + case .mountNull: + self = .mountNull + case .mount: + self = .mountMount + case .unmount: + self = .mountUnmount + case .null: + self = .nfsNull + case .getattr: + self = .nfsGetAttr + case .fsinfo: + self = .nfsFSInfo + case .pathconf: + self = .nfsPathConf + case .fsstat: + self = .nfsFSStat + case .access: + self = .nfsAccess + case .lookup: + self = .nfsLookup + case .readdirplus: + self = .nfsReadDirPlus + case .readdir: + self = .nfsReadDir + case .read: + self = .nfsRead + case .readlink: + self = .nfsReadLink + case .setattr: + self = .nfsSetAttr + case ._PLEASE_DO_NOT_EXHAUSTIVELY_MATCH_THIS_ENUM_NEW_CASES_MIGHT_BE_ADDED_IN_THE_FUTURE: + // inside the module, matching exhaustively is okay + preconditionFailure("unknown NFS3 reply, this should never happen. Please report a bug.") + } + } +} + +public enum NFS3Call: Hashable & Sendable { + case mountNull(MountCallNull) + case mount(MountCallMount) + case unmount(MountCallUnmount) + case null(NFS3CallNull) + case getattr(NFS3CallGetAttr) + case fsinfo(NFS3CallFSInfo) + case pathconf(NFS3CallPathConf) + case fsstat(NFS3CallFSStat) + case access(NFS3CallAccess) + case lookup(NFS3CallLookup) + case readdirplus(NFS3CallReadDirPlus) + case read(NFS3CallRead) + case readlink(NFS3CallReadlink) + case setattr(NFS3CallSetattr) + case readdir(NFS3CallReadDir) + + case _PLEASE_DO_NOT_EXHAUSTIVELY_MATCH_THIS_ENUM_NEW_CASES_MIGHT_BE_ADDED_IN_THE_FUTURE +} + +public enum NFS3Reply: Hashable & Sendable { + case mountNull + case mount(MountReplyMount) + case unmount(MountReplyUnmount) + case null + case getattr(NFS3ReplyGetAttr) + case fsinfo(NFS3ReplyFSInfo) + case pathconf(NFS3ReplyPathConf) + case fsstat(NFS3ReplyFSStat) + case access(NFS3ReplyAccess) + case lookup(NFS3ReplyLookup) + case readdirplus(NFS3ReplyReadDirPlus) + case readdir(NFS3ReplyReadDir) + case read(NFS3ReplyRead) + case readlink(NFS3ReplyReadlink) + case setattr(NFS3ReplySetattr) + + case _PLEASE_DO_NOT_EXHAUSTIVELY_MATCH_THIS_ENUM_NEW_CASES_MIGHT_BE_ADDED_IN_THE_FUTURE +} + +public enum NFS3Error: Error { + case wrongMessageType(RPCMessage) + case unknownProgramOrProcedure(RPCMessage) + case invalidFileHandleFormat(length: UInt32) + case illegalRPCTooShort + case invalidFileType(UInt32) + case invalidStatus(UInt32) + case invalidFSInfoProperties(NFS3ReplyFSInfo.Properties) + case unknownXID(UInt32) + + case _PLEASE_DO_NOT_EXHAUSTIVELY_MATCH_THIS_ENUM_NEW_CASES_MIGHT_BE_ADDED_IN_THE_FUTURE +} + +internal func nfsStringFillBytes(_ byteCount: Int) -> Int { + return (4 - (byteCount % 4)) % 4 +} + +extension ByteBuffer { + public mutating func readRPCVerifier() throws -> RPCOpaqueAuth { + guard let (flavor, length) = self.readMultipleIntegers(as: (UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + guard (flavor == RPCAuthFlavor.system.rawValue || flavor == RPCAuthFlavor.noAuth.rawValue) && length == 0 else { + throw RPCErrors.unknownVerifier(flavor) + } + return RPCOpaqueAuth(flavor: .noAuth, opaque: nil) + } + + @discardableResult public mutating func writeRPCVerifier(_ verifier: RPCOpaqueAuth) -> Int { + var bytesWritten = self.writeInteger(verifier.flavor.rawValue) + if let opaqueBlob = verifier.opaque { + bytesWritten += self.writeNFS3Blob(opaqueBlob) + } else { + bytesWritten += self.writeInteger(0, as: UInt32.self) + } + return bytesWritten + } + + public mutating func readRPCCredentials() throws -> RPCCredentials { + guard let flavor = self.readInteger(as: UInt32.self) else { + throw NFS3Error.illegalRPCTooShort + } + let blob = try self.readNFS3Blob() + return RPCCredentials(flavor: flavor, length: UInt32(blob.readableBytes), otherBytes: blob) + } + + @discardableResult public mutating func writeRPCCredentials(_ credentials: RPCCredentials) -> Int { + return self.writeInteger(credentials.flavor) + + self.writeNFS3Blob(credentials.otherBytes) + } + + public mutating func readRPCFragmentHeader() throws -> RPCFragmentHeader? { + let save = self + guard let lastAndLength = self.readInteger(as: UInt32.self) else { + self = save + return nil + } + return .init(rawValue: lastAndLength) + } + + @discardableResult + public mutating func setRPCFragmentHeader(_ header: RPCFragmentHeader, at index: Int) -> Int { + return self.setInteger(header.rawValue, at: index) + } + + @discardableResult public mutating func writeRPCFragmentHeader(_ header: RPCFragmentHeader) -> Int { + let bytesWritten = self.setRPCFragmentHeader(header, at: self.writerIndex) + self.moveWriterIndex(forwardBy: bytesWritten) + return bytesWritten + } + + mutating func readRPCReply(xid: UInt32) throws -> RPCReply { + let acceptedOrDenied = try self.readNFS3Integer(as: UInt32.self) + switch acceptedOrDenied { + case 0: // MSG_ACCEPTED + let verifier = try self.readRPCVerifier() + let status = try self.readNFS3Integer(as: UInt32.self) + let acceptedReplyStatus: RPCAcceptedReplyStatus + + switch status { + case 0: // SUCCESS + acceptedReplyStatus = .success + case 1: //PROG_UNAVAIL + acceptedReplyStatus = .programUnavailable + case 2: //PROG_MISMATCH + guard let values = self.readMultipleIntegers(as: (UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + acceptedReplyStatus = .programMismatch(low: values.0, high: values.1) + case 3: //PROC_UNAVAIL + acceptedReplyStatus = .procedureUnavailable + case 4: //GARBAGE_ARGS + acceptedReplyStatus = .garbageArguments + case 5: //SYSTEM_ERR + acceptedReplyStatus = .systemError + default: + throw RPCErrors.illegalReplyAcceptanceStatus(status) + } + return RPCReply(xid: xid, status: .messageAccepted(.init(verifier: verifier, + status: acceptedReplyStatus))) + case 1: // MSG_DENIED + let rejectionKind = try self.readNFS3Integer(as: UInt32.self) + switch rejectionKind { + case 0: // RPC_MISMATCH: RPC version number != 2 + guard let values = self.readMultipleIntegers(as: (UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + return RPCReply(xid: xid, status: .messageDenied(.rpcMismatch(low: values.0, high: values.1))) + case 1: // AUTH_ERROR + let rawValue = try self.readNFS3Integer(as: UInt32.self) + if let value = RPCAuthStatus(rawValue: rawValue) { + return RPCReply(xid: xid, status: .messageDenied(.authError(value))) + } else { + throw RPCErrors.illegalAuthStatus(rawValue) + } + default: + throw RPCErrors.illegalReplyRejectionStatus(rejectionKind) + } + default: + throw RPCErrors.illegalReplyStatus(acceptedOrDenied) + } + } + + @discardableResult public mutating func writeRPCCall(_ call: RPCCall) -> Int { + return self.writeMultipleIntegers( + RPCMessageType.call.rawValue, + call.rpcVersion, + call.program, + call.programVersion, + call.procedure) + + self.writeRPCCredentials(call.credentials) + + self.writeRPCVerifier(call.verifier) + } + + @discardableResult public mutating func writeRPCReply(_ reply: RPCReply) -> Int { + var bytesWritten = self.writeInteger(RPCMessageType.reply.rawValue) + + switch reply.status { + case .messageAccepted(_): + bytesWritten += self.writeInteger(0 /* accepted */, as: UInt32.self) + case .messageDenied(_): + // FIXME: MSG_DENIED (spec name) isn't actually handled correctly here. + bytesWritten += self.writeInteger(1 /* denied */, as: UInt32.self) + } + bytesWritten += self.writeInteger(0 /* verifier */, as: UInt64.self) + + self.writeInteger(0 /* executed successfully */, as: UInt32.self) + return bytesWritten + } + + + public mutating func readRPCCall(xid: UInt32) throws -> RPCCall { + guard let values = self.readMultipleIntegers(as: (UInt32, UInt32, UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + + let (version, program, programVersion, procedure) = values + let credentials = try self.readRPCCredentials() + let verifier = try self.readRPCVerifier() + + guard version == 2 else { + throw RPCErrors.unknownVersion(version) + } + + return RPCCall(xid: xid, + rpcVersion: version, + program: program, + programVersion: programVersion, + procedure: procedure, + credentials: credentials, + verifier: verifier) + } + + public mutating func readNFS3Reply(programAndProcedure: RPCNFS3ProcedureID, rpcReply: RPCReply) throws -> RPCNFS3Reply { + switch programAndProcedure { + case .mountNull: + return .init(rpcReply: rpcReply, nfsReply: .mountNull) + case .mountMount: + return .init(rpcReply: rpcReply, nfsReply: .mount(try self.readNFS3ReplyMount())) + case .mountUnmount: + return .init(rpcReply: rpcReply, nfsReply: .unmount(try self.readNFS3ReplyUnmount())) + case .nfsNull: + return .init(rpcReply: rpcReply, nfsReply: .null) + case .nfsGetAttr: + return .init(rpcReply: rpcReply, nfsReply: .getattr(try self.readNFS3ReplyGetAttr())) + case .nfsFSInfo: + return .init(rpcReply: rpcReply, nfsReply: .fsinfo(try self.readNFS3ReplyFSInfo())) + case .nfsPathConf: + return .init(rpcReply: rpcReply, nfsReply: .pathconf(try self.readNFS3ReplyPathConf())) + case .nfsFSStat: + return .init(rpcReply: rpcReply, nfsReply: .fsstat(try self.readNFS3ReplyFSStat())) + case .nfsAccess: + return .init(rpcReply: rpcReply, nfsReply: .access(try self.readNFS3ReplyAccess())) + case .nfsLookup: + return .init(rpcReply: rpcReply, nfsReply: .lookup(try self.readNFS3ReplyLookup())) + case .nfsReadDirPlus: + return .init(rpcReply: rpcReply, nfsReply: .readdirplus(try self.readNFS3ReplyReadDirPlus())) + case .nfsReadDir: + return .init(rpcReply: rpcReply, nfsReply: .readdir(try self.readNFS3ReplyReadDir())) + case .nfsRead: + return .init(rpcReply: rpcReply, nfsReply: .read(try self.readNFS3ReplyRead())) + case .nfsReadLink: + return .init(rpcReply: rpcReply, nfsReply: .readlink(try self.readNFS3ReplyReadlink())) + case .nfsSetAttr: + return .init(rpcReply: rpcReply, nfsReply: .setattr(try self.readNFS3ReplySetattr())) + default: + throw NFS3Error.unknownProgramOrProcedure(.reply(rpcReply)) + } + } + + mutating func readNFS3Call(rpc: RPCCall) throws -> RPCNFS3Call { + switch RPCNFS3ProcedureID(program: rpc.program, procedure: rpc.procedure) { + case .mountNull: + return .init(rpcCall: rpc, nfsCall: .mountNull(try self.readMountCallNull())) + case .mountMount: + return .init(rpcCall: rpc, nfsCall: .mount(try self.readNFS3CallMount())) + case .mountUnmount: + return .init(rpcCall: rpc, nfsCall: .unmount(try self.readNFS3CallUnmount())) + case .nfsNull: + return .init(rpcCall: rpc, nfsCall: .null(try self.readNFS3CallNull())) + case .nfsGetAttr: + return .init(rpcCall: rpc, nfsCall: .getattr(try self.readNFS3CallGetattr())) + case .nfsFSInfo: + return .init(rpcCall: rpc, nfsCall: .fsinfo(try self.readNFS3CallFSInfo())) + case .nfsPathConf: + return .init(rpcCall: rpc, nfsCall: .pathconf(try self.readNFS3CallPathConf())) + case .nfsFSStat: + return .init(rpcCall: rpc, nfsCall: .fsstat(try self.readNFS3CallFSStat())) + case .nfsAccess: + return .init(rpcCall: rpc, nfsCall: .access(try self.readNFS3CallAccess())) + case .nfsLookup: + return .init(rpcCall: rpc, nfsCall: .lookup(try self.readNFS3CallLookup())) + case .nfsReadDirPlus: + return .init(rpcCall: rpc, nfsCall: .readdirplus(try self.readNFS3CallReadDirPlus())) + case .nfsReadDir: + return .init(rpcCall: rpc, nfsCall: .readdir(try self.readNFS3CallReadDir())) + case .nfsRead: + return .init(rpcCall: rpc, nfsCall: .read(try self.readNFS3CallRead())) + case .nfsReadLink: + return .init(rpcCall: rpc, nfsCall: .readlink(try self.readNFS3CallReadlink())) + case .nfsSetAttr: + return .init(rpcCall: rpc, nfsCall: .setattr(try self.readNFS3CallSetattr())) + default: + throw NFS3Error.unknownProgramOrProcedure(.call(rpc)) + } + } + + @discardableResult public mutating func writeRPCNFS3Call(_ rpcNFS3Call: RPCNFS3Call) -> Int { + let startWriterIndex = self.writerIndex + self.writeRPCFragmentHeader(.init(length: 12345678, last: false)) // placeholder, overwritten later + self.writeInteger(rpcNFS3Call.rpcCall.xid) + + self.writeRPCCall(rpcNFS3Call.rpcCall) + + switch rpcNFS3Call.nfsCall { + case .mountNull: + () // noop + case .mount(let nfsCallMount): + self.writeNFS3CallMount(nfsCallMount) + case .unmount(let nfsCallUnmount): + self.writeNFS3CallUnmount(nfsCallUnmount) + case .null: + () // noop + case .getattr(let nfsCallGetAttr): + self.writeNFS3CallGetattr(nfsCallGetAttr) + case .fsinfo(let nfsCallFSInfo): + self.writeNFS3CallFSInfo(nfsCallFSInfo) + case .pathconf(let nfsCallPathConf): + self.writeNFS3CallPathConf(nfsCallPathConf) + case .fsstat(let nfsCallFSStat): + self.writeNFS3CallFSStat(nfsCallFSStat) + case .access(let nfsCallAccess): + self.writeNFS3CallAccess(nfsCallAccess) + case .lookup(let nfsCallLookup): + self.writeNFS3CallLookup(nfsCallLookup) + case .readdirplus(let nfsCallReadDirPlus): + self.writeNFS3CallReadDirPlus(nfsCallReadDirPlus) + case .readdir(let nfsCallReadDir): + self.writeNFS3CallReadDir(nfsCallReadDir) + case .read(let nfsCallRead): + self.writeNFS3CallRead(nfsCallRead) + case .readlink(let nfsCallReadlink): + self.writeNFS3CallReadlink(nfsCallReadlink) + case .setattr(let nfsCallSetattr): + self.writeNFS3CallSetattr(nfsCallSetattr) + case ._PLEASE_DO_NOT_EXHAUSTIVELY_MATCH_THIS_ENUM_NEW_CASES_MIGHT_BE_ADDED_IN_THE_FUTURE: + // inside the module, matching exhaustively is okay + preconditionFailure("unknown NFS3 call, this should never happen. Please report a bug.") + } + + self.setRPCFragmentHeader(.init(length: UInt32(self.writerIndex - startWriterIndex - 4), + last: true), + at: startWriterIndex) + return self.writerIndex - startWriterIndex + } + + @discardableResult public mutating func writeRPCNFS3ReplyPartially(_ rpcNFS3Reply: RPCNFS3Reply) -> (Int, NFS3PartialWriteNextStep) { + var nextStep: NFS3PartialWriteNextStep = .doNothing + + let startWriterIndex = self.writerIndex + self.writeRPCFragmentHeader(.init(length: 12345678, last: false)) // placeholder, overwritten later + self.writeInteger(rpcNFS3Reply.rpcReply.xid) + + self.writeRPCReply(rpcNFS3Reply.rpcReply) + + switch rpcNFS3Reply.nfsReply { + case .mountNull: + () // noop + case .mount(let nfsReplyMount): + self.writeNFS3ReplyMount(nfsReplyMount) + case .unmount(let nfsReplyUnmount): + self.writeNFS3ReplyUnmount(nfsReplyUnmount) + case .null: + () // noop + case .getattr(let nfsReplyGetAttr): + self.writeNFS3ReplyGetAttr(nfsReplyGetAttr) + case .fsinfo(let nfsReplyFSInfo): + self.writeNFS3ReplyFSInfo(nfsReplyFSInfo) + case .pathconf(let nfsReplyPathConf): + self.writeNFS3ReplyPathConf(nfsReplyPathConf) + case .fsstat(let nfsReplyFSStat): + self.writeNFS3ReplyFSStat(nfsReplyFSStat) + case .access(let nfsReplyAccess): + self.writeNFS3ReplyAccess(nfsReplyAccess) + case .lookup(let nfsReplyLookup): + self.writeNFS3ReplyLookup(nfsReplyLookup) + case .readdirplus(let nfsReplyReadDirPlus): + self.writeNFS3ReplyReadDirPlus(nfsReplyReadDirPlus) + case .readdir(let nfsReplyReadDir): + self.writeNFS3ReplyReadDir(nfsReplyReadDir) + case .read(let nfsReplyRead): + nextStep = self.writeNFS3ReplyReadPartially(nfsReplyRead) + case .readlink(let nfsReplyReadlink): + self.writeNFS3ReplyReadlink(nfsReplyReadlink) + case .setattr(let nfsReplySetattr): + self.writeNFS3ReplySetattr(nfsReplySetattr) + case ._PLEASE_DO_NOT_EXHAUSTIVELY_MATCH_THIS_ENUM_NEW_CASES_MIGHT_BE_ADDED_IN_THE_FUTURE: + // inside the module, matching exhaustively is okay + preconditionFailure("unknown NFS3 reply, this should never happen. Please report a bug.") + } + + self.setRPCFragmentHeader(.init(length: UInt32(self.writerIndex - startWriterIndex - 4 + nextStep.bytesToFollow), + last: true), + at: startWriterIndex) + return (self.writerIndex - startWriterIndex, nextStep) + } + + @discardableResult + public mutating func writeRPCNFS3Reply(_ reply: RPCNFS3Reply) -> Int { + let (bytesWritten, nextStep) = self.writeRPCNFS3ReplyPartially(reply) + switch nextStep { + case .doNothing: + return bytesWritten + case .writeBlob(let buffer, numberOfFillBytes: let fillBytes): + return bytesWritten + &+ self.writeImmutableBuffer(buffer) + &+ self.writeRepeatingByte(0x41, count: fillBytes) + } + } + + public mutating func readRPCMessage() throws -> (RPCMessage, ByteBuffer)? { + let save = self + guard let fragmentHeader = try self.readRPCFragmentHeader(), + let xid = self.readInteger(as: UInt32.self), + let messageType = self.readInteger(as: UInt32.self) else { + self = save + return nil + } + + if fragmentHeader.length > 1 * 1024 * 1024 { + throw RPCErrors.tooLong(fragmentHeader, xid: xid, messageType: messageType) + } + + guard fragmentHeader.length >= 8 else { + throw RPCErrors.fragementHeaderLengthTooShort(fragmentHeader.length) + } + + guard var body = self.readSlice(length: Int(fragmentHeader.length - 8)) else { + self = save + return nil + } + + switch RPCMessageType(rawValue: messageType) { + case .some(.call): + return (.call(try body.readRPCCall(xid: xid)), body) + case .some(.reply): + return (.reply(try body.readRPCReply(xid: xid)), body) + case .none: + throw RPCErrors.unknownType(messageType) + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+FSInfo.swift b/Sources/NIONFS3/NFSTypes+FSInfo.swift new file mode 100644 index 00000000..26aba1ce --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+FSInfo.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - FSInfo +public struct NFS3CallFSInfo: Hashable & Sendable { + public init(fsroot: NFS3FileHandle) { + self.fsroot = fsroot + } + + public var fsroot: NFS3FileHandle +} + +public struct NFS3ReplyFSInfo: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Properties: OptionSet & Hashable & Sendable { + public typealias RawValue = UInt32 + + public var rawValue: RawValue + + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + + public static let supportsHardlinks: Self = .init(rawValue: (1 << 0)) + public static let supportsSoftlinks: Self = .init(rawValue: (1 << 1)) + public static let isHomogenous: Self = .init(rawValue: (1 << 2)) + public static let canSetTime: Self = .init(rawValue: (1 << 3)) + public static let `default`: Self = [.supportsSoftlinks, .supportsHardlinks, .isHomogenous, .canSetTime] + } + + public struct Okay: Hashable & Sendable { + public init(attributes: NFS3FileAttr?, + rtmax: UInt32, rtpref: UInt32, rtmult: UInt32, + wtmax: UInt32, wtpref: UInt32, wtmult: UInt32, + dtpref: UInt32, + maxFileSize: NFS3Size, + timeDelta: NFS3Time, + properties: NFS3ReplyFSInfo.Properties) { + self.attributes = attributes + self.rtmax = rtmax + self.rtpref = rtpref + self.rtmult = rtmult + self.wtmax = wtmax + self.wtpref = wtpref + self.wtmult = wtmult + self.dtpref = dtpref + self.maxFileSize = maxFileSize + self.timeDelta = timeDelta + self.properties = properties + } + + public var attributes: NFS3FileAttr? + public var rtmax: UInt32 + public var rtpref: UInt32 + public var rtmult: UInt32 + public var wtmax: UInt32 + public var wtpref: UInt32 + public var wtmult: UInt32 + public var dtpref: UInt32 + public var maxFileSize: NFS3Size + public var timeDelta: NFS3Time + public var properties: Properties = .default + } + + public struct Fail: Hashable & Sendable { + public init(attributes: NFS3FileAttr?) { + self.attributes = attributes + } + + public var attributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFS3CallFSInfo() throws -> NFS3CallFSInfo { + let fileHandle = try self.readNFS3FileHandle() + return NFS3CallFSInfo(fsroot: fileHandle) + } + + @discardableResult public mutating func writeNFS3CallFSInfo(_ call: NFS3CallFSInfo) -> Int { + self.writeNFS3FileHandle(call.fsroot) + } + + private mutating func readNFS3CallFSInfoProperties() throws -> NFS3ReplyFSInfo.Properties { + let rawValue = try self.readNFS3Integer(as: UInt32.self) + return NFS3ReplyFSInfo.Properties(rawValue: rawValue) + } + + @discardableResult public mutating func writeNFS3ReplyFSInfo(_ reply: NFS3ReplyFSInfo) -> Int { + var bytesWritten = self.writeNFS3ResultStatus(reply.result) + + switch reply.result { + case .okay(let reply): + bytesWritten += self.writeNFS3Optional(reply.attributes, writer: { $0.writeNFS3FileAttr($1) }) + + self.writeMultipleIntegers( + reply.rtmax, + reply.rtpref, + reply.rtmult, + reply.wtmax, + reply.wtpref, + reply.wtmult, + reply.dtpref, + reply.maxFileSize.rawValue) + + self.writeNFS3Time(reply.timeDelta) + + self.writeInteger(reply.properties.rawValue) + case .fail(_, let fail): + bytesWritten += self.writeNFS3Optional(fail.attributes, writer: { $0.writeNFS3FileAttr($1) }) + } + return bytesWritten + } + + private mutating func readNFS3ReplyFSInfoOkay() throws -> NFS3ReplyFSInfo.Okay { + let fileAttr = try self.readNFS3Optional { try $0.readNFS3FileAttr() } + guard let values = self.readMultipleIntegers(as: (UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + let rtmax = values.0 + let rtpref = values.1 + let rtmult = values.2 + let wtmax = values.3 + let wtpref = values.4 + let wtmult = values.5 + let dtpref = values.6 + let maxFileSize = try self.readNFS3Size() + let timeDelta = try self.readNFS3Time() + let properties = try self.readNFS3CallFSInfoProperties() + + return .init(attributes: fileAttr, + rtmax: rtmax, rtpref: rtpref, rtmult: rtmult, + wtmax: wtmax, wtpref: wtpref, wtmult: wtmult, + dtpref: dtpref, + maxFileSize: maxFileSize, timeDelta: timeDelta, properties: properties) + } + + public mutating func readNFS3ReplyFSInfo() throws -> NFS3ReplyFSInfo { + return NFS3ReplyFSInfo(result: try self.readNFS3Result( + readOkay: { try $0.readNFS3ReplyFSInfoOkay() }, + readFail: { NFS3ReplyFSInfo.Fail(attributes: try $0.readNFS3FileAttr()) } + )) + } +} diff --git a/Sources/NIONFS3/NFSTypes+FSStat.swift b/Sources/NIONFS3/NFSTypes+FSStat.swift new file mode 100644 index 00000000..5a6b6d94 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+FSStat.swift @@ -0,0 +1,130 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - FSStat +public struct NFS3CallFSStat: Hashable & Sendable { + public init(fsroot: NFS3FileHandle) { + self.fsroot = fsroot + } + + public var fsroot: NFS3FileHandle +} + +public struct NFS3ReplyFSStat: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Hashable & Sendable { + public init(attributes: NFS3FileAttr?, + tbytes: NFS3Size, fbytes: NFS3Size, abytes: NFS3Size, + tfiles: NFS3Size, ffiles: NFS3Size, afiles: NFS3Size, + invarsec: UInt32) { + self.attributes = attributes + self.tbytes = tbytes + self.fbytes = fbytes + self.abytes = abytes + self.tfiles = tfiles + self.ffiles = ffiles + self.afiles = afiles + self.invarsec = invarsec + } + + public var attributes: NFS3FileAttr? + public var tbytes: NFS3Size + public var fbytes: NFS3Size + public var abytes: NFS3Size + public var tfiles: NFS3Size + public var ffiles: NFS3Size + public var afiles: NFS3Size + public var invarsec: UInt32 + } + + public struct Fail: Hashable & Sendable { + public init(attributes: NFS3FileAttr?) { + self.attributes = attributes + } + + public var attributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFS3CallFSStat() throws -> NFS3CallFSStat { + let fileHandle = try self.readNFS3FileHandle() + return NFS3CallFSStat(fsroot: fileHandle) + } + + @discardableResult public mutating func writeNFS3CallFSStat(_ call: NFS3CallFSStat) -> Int { + self.writeNFS3FileHandle(call.fsroot) + } + + private mutating func readNFS3ReplyFSStatOkay() throws -> NFS3ReplyFSStat.Okay { + let attrs = try self.readNFS3Optional { buffer in + try buffer.readNFS3FileAttr() + } + if let values = self.readMultipleIntegers(as: (UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt32).self) { + return .init(attributes: attrs, + tbytes: NFS3Size(rawValue: values.0), + fbytes: NFS3Size(rawValue: values.1), + abytes: NFS3Size(rawValue: values.2), + tfiles: NFS3Size(rawValue: values.3), + ffiles: NFS3Size(rawValue: values.4), + afiles: NFS3Size(rawValue: values.5), + invarsec: values.6) + } else { + throw NFS3Error.illegalRPCTooShort + } + } + + public mutating func readNFS3ReplyFSStat() throws -> NFS3ReplyFSStat { + return NFS3ReplyFSStat( + result: try self.readNFS3Result( + readOkay: { buffer in + try buffer.readNFS3ReplyFSStatOkay() + }, + readFail: { buffer in + NFS3ReplyFSStat.Fail( + attributes: try buffer.readNFS3Optional { buffer in + try buffer.readNFS3FileAttr() + } + ) + }) + ) + } + + @discardableResult public mutating func writeNFS3ReplyFSStat(_ reply: NFS3ReplyFSStat) -> Int { + var bytesWritten = self.writeNFS3ResultStatus(reply.result) + + switch reply.result { + case .okay(let okay): + bytesWritten += self.writeNFS3Optional(okay.attributes, writer: { $0.writeNFS3FileAttr($1) }) + + self.writeMultipleIntegers( + okay.tbytes.rawValue, + okay.fbytes.rawValue, + okay.abytes.rawValue, + okay.tfiles.rawValue, + okay.ffiles.rawValue, + okay.afiles.rawValue, + okay.invarsec) + case .fail(_, let fail): + bytesWritten += self.writeNFS3Optional(fail.attributes, writer: { $0.writeNFS3FileAttr($1) }) + } + return bytesWritten + } +} diff --git a/Sources/NIONFS3/NFSTypes+Getattr.swift b/Sources/NIONFS3/NFSTypes+Getattr.swift new file mode 100644 index 00000000..6863a0c0 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Getattr.swift @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - Getattr +public struct NFS3CallGetAttr: Hashable & Sendable { + public init(fileHandle: NFS3FileHandle) { + self.fileHandle = fileHandle + } + + public var fileHandle: NFS3FileHandle +} + +public struct NFS3ReplyGetAttr: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Hashable & Sendable { + public init(attributes: NFS3FileAttr) { + self.attributes = attributes + } + + public var attributes: NFS3FileAttr + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFS3CallGetattr() throws -> NFS3CallGetAttr { + let fileHandle = try self.readNFS3FileHandle() + return NFS3CallGetAttr(fileHandle: fileHandle) + } + + @discardableResult public mutating func writeNFS3CallGetattr(_ call: NFS3CallGetAttr) -> Int { + self.writeNFS3FileHandle(call.fileHandle) + } + + public mutating func readNFS3ReplyGetAttr() throws -> NFS3ReplyGetAttr { + return NFS3ReplyGetAttr( + result: try self.readNFS3Result( + readOkay: { buffer in + return NFS3ReplyGetAttr.Okay(attributes: try buffer.readNFS3FileAttr()) + }, + readFail: { _ in + return NFS3Nothing() + }) + ) + } + + @discardableResult public mutating func writeNFS3ReplyGetAttr(_ reply: NFS3ReplyGetAttr) -> Int { + var bytesWritten = self.writeNFS3ResultStatus(reply.result) + + switch reply.result { + case .okay(let okay): + bytesWritten += self.writeNFS3FileAttr(okay.attributes) + case .fail(_, _): + () + } + return bytesWritten + } +} diff --git a/Sources/NIONFS3/NFSTypes+Lookup.swift b/Sources/NIONFS3/NFSTypes+Lookup.swift new file mode 100644 index 00000000..c81170a0 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Lookup.swift @@ -0,0 +1,122 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - Lookup +public struct NFS3CallLookup: Hashable & Sendable { + public init(dir: NFS3FileHandle, name: String) { + self.dir = dir + self.name = name + } + + public var dir: NFS3FileHandle + public var name: String +} + +public struct NFS3ReplyLookup: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Hashable & Sendable { + public init(fileHandle: NFS3FileHandle, attributes: NFS3FileAttr?, dirAttributes: NFS3FileAttr?) { + self.fileHandle = fileHandle + self.attributes = attributes + self.dirAttributes = dirAttributes + } + + public var fileHandle: NFS3FileHandle + public var attributes: NFS3FileAttr? + public var dirAttributes: NFS3FileAttr? + } + + public struct Fail: Hashable & Sendable { + public init(dirAttributes: NFS3FileAttr? = nil) { + self.dirAttributes = dirAttributes + } + + public var dirAttributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFS3CallLookup() throws -> NFS3CallLookup { + let dir = try self.readNFS3FileHandle() + let name = try self.readNFS3String() + return NFS3CallLookup(dir: dir, name: name) + } + + @discardableResult public mutating func writeNFS3CallLookup(_ call: NFS3CallLookup) -> Int { + return self.writeNFS3FileHandle(call.dir) + + self.writeNFS3String(call.name) + } + + public mutating func readNFS3ReplyLookup() throws -> NFS3ReplyLookup { + return NFS3ReplyLookup( + result: try self.readNFS3Result( + readOkay: { buffer in + let fileHandle = try buffer.readNFS3FileHandle() + let attrs = try buffer.readNFS3Optional { buffer in + try buffer.readNFS3FileAttr() + } + let dirAttrs = try buffer.readNFS3Optional { buffer in + try buffer.readNFS3FileAttr() + } + + return NFS3ReplyLookup.Okay(fileHandle: fileHandle, attributes: attrs, dirAttributes: dirAttrs) + }, + readFail: { buffer in + let attrs = try buffer.readNFS3Optional { buffer in + try buffer.readNFS3FileAttr() + } + return NFS3ReplyLookup.Fail(dirAttributes: attrs) + }) + ) + } + + @discardableResult public mutating func writeNFS3ReplyLookup(_ lookupResult: NFS3ReplyLookup) -> Int { + var bytesWritten = 0 + + switch lookupResult.result { + case .okay(let result): + bytesWritten += self.writeInteger(NFS3Status.ok.rawValue) + + self.writeNFS3FileHandle(result.fileHandle) + if let attrs = result.attributes { + bytesWritten += self.writeInteger(1, as: UInt32.self) + + self.writeNFS3FileAttr(attrs) + } else { + bytesWritten += self.writeInteger(0, as: UInt32.self) + } + if let attrs = result.dirAttributes { + bytesWritten += self.writeInteger(1, as: UInt32.self) + + self.writeNFS3FileAttr(attrs) + } else { + bytesWritten += self.writeInteger(0, as: UInt32.self) + } + case .fail(let status, let fail): + precondition(status != .ok) + bytesWritten += self.writeInteger(status.rawValue) + if let attrs = fail.dirAttributes { + bytesWritten += self.writeInteger(1, as: UInt32.self) + + self.writeNFS3FileAttr(attrs) + } else { + bytesWritten += self.writeInteger(0, as: UInt32.self) + } + } + return bytesWritten + } +} diff --git a/Sources/NIONFS3/NFSTypes+Null.swift b/Sources/NIONFS3/NFSTypes+Null.swift new file mode 100644 index 00000000..da0703fd --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Null.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - Null +public struct NFS3CallNull: Hashable & Sendable { + public init() {} +} + +extension ByteBuffer { + public mutating func readNFS3CallNull() throws -> NFS3CallNull { + return NFS3CallNull() + } + + @discardableResult public mutating func writeNFS3CallNull(_ call: NFS3CallNull) -> Int { + return 0 + } +} diff --git a/Sources/NIONFS3/NFSTypes+PathConf.swift b/Sources/NIONFS3/NFSTypes+PathConf.swift new file mode 100644 index 00000000..afb0fd70 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+PathConf.swift @@ -0,0 +1,119 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - PathConf +public struct NFS3CallPathConf: Hashable & Sendable { + public init(object: NFS3FileHandle) { + self.object = object + } + + public var object: NFS3FileHandle +} + +public struct NFS3ReplyPathConf: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Hashable & Sendable { + public init(attributes: NFS3FileAttr?, linkMax: UInt32, nameMax: UInt32, noTrunc: NFS3Bool, chownRestricted: NFS3Bool, caseInsensitive: NFS3Bool, casePreserving: NFS3Bool) { + self.attributes = attributes + self.linkMax = linkMax + self.nameMax = nameMax + self.noTrunc = noTrunc + self.chownRestricted = chownRestricted + self.caseInsensitive = caseInsensitive + self.casePreserving = casePreserving + } + + public var attributes: NFS3FileAttr? + public var linkMax: UInt32 + public var nameMax: UInt32 + public var noTrunc: NFS3Bool + public var chownRestricted: NFS3Bool + public var caseInsensitive: NFS3Bool + public var casePreserving: NFS3Bool + } + + public struct Fail: Hashable & Sendable { + public init(attributes: NFS3FileAttr?) { + self.attributes = attributes + } + + public var attributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFS3CallPathConf() throws -> NFS3CallPathConf { + let fileHandle = try self.readNFS3FileHandle() + return NFS3CallPathConf(object: fileHandle) + } + + @discardableResult public mutating func writeNFS3CallPathConf(_ call: NFS3CallPathConf) -> Int { + self.writeNFS3FileHandle(call.object) + } + + public mutating func readNFS3ReplyPathConf() throws -> NFS3ReplyPathConf { + return NFS3ReplyPathConf( + result: try self.readNFS3Result( + readOkay: { buffer in + let attrs = try buffer.readNFS3Optional { buffer in + try buffer.readNFS3FileAttr() + } + guard let values = buffer.readMultipleIntegers(as: (UInt32, UInt32, UInt32, UInt32, UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + + return NFS3ReplyPathConf.Okay(attributes: attrs, + linkMax: values.0, + nameMax: values.1, + noTrunc: values.2 == 0 ? false : true, + chownRestricted: values.3 == 0 ? false : true, + caseInsensitive: values.4 == 0 ? false : true, + casePreserving: values.5 == 0 ? false : true) + }, + readFail: { buffer in + let attrs = try buffer.readNFS3Optional { buffer in + try buffer.readNFS3FileAttr() + } + return NFS3ReplyPathConf.Fail(attributes: attrs) + }) + ) + } + + @discardableResult public mutating func writeNFS3ReplyPathConf(_ pathconf: NFS3ReplyPathConf) -> Int { + var bytesWritten = self.writeNFS3ResultStatus(pathconf.result) + + switch pathconf.result { + case .okay(let pathconf): + bytesWritten += self.writeNFS3Optional(pathconf.attributes, writer: { $0.writeNFS3FileAttr($1) }) + + self.writeMultipleIntegers( + pathconf.linkMax, + pathconf.nameMax, + pathconf.noTrunc ? UInt32(1) : 0, + pathconf.chownRestricted ? UInt32(1) : 0, + pathconf.caseInsensitive ? UInt32(1) : 0, + pathconf.casePreserving ? UInt32(1) : 0 + ) + case .fail(_, let fail): + bytesWritten += self.writeNFS3Optional(fail.attributes, writer: { $0.writeNFS3FileAttr($1) }) + } + return bytesWritten + } +} diff --git a/Sources/NIONFS3/NFSTypes+Read.swift b/Sources/NIONFS3/NFSTypes+Read.swift new file mode 100644 index 00000000..c43b3940 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Read.swift @@ -0,0 +1,130 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - Read +public struct NFS3CallRead: Hashable & Sendable { + public init(fileHandle: NFS3FileHandle, offset: NFS3Offset, count: NFS3Count) { + self.fileHandle = fileHandle + self.offset = offset + self.count = count + } + + public var fileHandle: NFS3FileHandle + public var offset: NFS3Offset + public var count: NFS3Count +} + +public struct NFS3ReplyRead: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Hashable & Sendable { + public init(attributes: NFS3FileAttr? = nil, count: NFS3Count, eof: NFS3Bool, data: ByteBuffer) { + self.attributes = attributes + self.count = count + self.eof = eof + self.data = data + } + + public var attributes: NFS3FileAttr? + public var count: NFS3Count + public var eof: NFS3Bool + public var data: ByteBuffer + } + + public struct Fail: Hashable & Sendable { + public init(attributes: NFS3FileAttr? = nil) { + self.attributes = attributes + } + + public var attributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFS3CallRead() throws -> NFS3CallRead { + let fileHandle = try self.readNFS3FileHandle() + guard let values = self.readMultipleIntegers(as: (NFS3Offset.RawValue, NFS3Count.RawValue).self) else { + throw NFS3Error.illegalRPCTooShort + } + + return NFS3CallRead(fileHandle: fileHandle, + offset: .init(rawValue: values.0), + count: .init(rawValue: values.1)) + } + + @discardableResult public mutating func writeNFS3CallRead(_ call: NFS3CallRead) -> Int { + return self.writeNFS3FileHandle(call.fileHandle) + + self.writeMultipleIntegers(call.offset.rawValue, call.count.rawValue) + } + + public mutating func readNFS3ReplyRead() throws -> NFS3ReplyRead { + return NFS3ReplyRead( + result: try self.readNFS3Result( + readOkay: { buffer in + let attrs = try buffer.readNFS3Optional { buffer in + try buffer.readNFS3FileAttr() + } + guard let values = buffer.readMultipleIntegers(as: (UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + let bytes = try buffer.readNFS3Blob() + return NFS3ReplyRead.Okay(attributes: attrs, + count: NFS3Count(rawValue: values.0), + eof: values.1 == 0 ? false : true, + data: bytes) + }, + readFail: { buffer in + let attrs = try buffer.readNFS3Optional { buffer in + try buffer.readNFS3FileAttr() + } + return NFS3ReplyRead.Fail(attributes: attrs) + }) + ) + } + + public mutating func writeNFS3ReplyReadPartially(_ read: NFS3ReplyRead) -> NFS3PartialWriteNextStep { + switch read.result { + case .okay(let result): + self.writeInteger(NFS3Status.ok.rawValue) + self.writeNFS3Optional(result.attributes, writer: { $0.writeNFS3FileAttr($1) }) + self.writeMultipleIntegers( + result.count.rawValue, + result.eof ? UInt32(1) : 0, + UInt32(result.data.readableBytes) + ) + return .writeBlob(result.data, numberOfFillBytes: nfsStringFillBytes(result.data.readableBytes)) + case .fail(let status, let fail): + precondition(status != .ok) + self.writeInteger(status.rawValue) + self.writeNFS3Optional(fail.attributes, writer: { $0.writeNFS3FileAttr($1) }) + return .doNothing + } + } + + @discardableResult public mutating func writeNFS3ReplyRead(_ read: NFS3ReplyRead) -> Int { + switch self.writeNFS3ReplyReadPartially(read) { + case .doNothing: + return 0 + case .writeBlob(let blob, numberOfFillBytes: let fillBytes): + return self.writeImmutableBuffer(blob) + + self.writeRepeatingByte(0x41, count: fillBytes) + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+ReadDir.swift b/Sources/NIONFS3/NFSTypes+ReadDir.swift new file mode 100644 index 00000000..4b4f2205 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+ReadDir.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - ReadDir +public struct NFS3CallReadDir: Hashable & Sendable { + public init(fileHandle: NFS3FileHandle, cookie: NFS3Cookie, cookieVerifier: NFS3CookieVerifier, + maxResultByteCount: NFS3Count) { + self.fileHandle = fileHandle + self.cookie = cookie + self.cookieVerifier = cookieVerifier + self.maxResultByteCount = maxResultByteCount + } + + public var fileHandle: NFS3FileHandle + public var cookie: NFS3Cookie + public var cookieVerifier: NFS3CookieVerifier + public var maxResultByteCount: NFS3Count +} + +public struct NFS3ReplyReadDir: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Entry: Hashable & Sendable { + public init(fileID: NFS3FileID, fileName: String, cookie: NFS3Cookie) { + self.fileID = fileID + self.fileName = fileName + self.cookie = cookie + } + + public var fileID: NFS3FileID + public var fileName: String + public var cookie: NFS3Cookie + } + + public struct Okay: Hashable & Sendable { + public init(dirAttributes: NFS3FileAttr? = nil, cookieVerifier: NFS3CookieVerifier, entries: [NFS3ReplyReadDir.Entry], eof: NFS3Bool) { + self.dirAttributes = dirAttributes + self.cookieVerifier = cookieVerifier + self.entries = entries + self.eof = eof + } + + public var dirAttributes: NFS3FileAttr? + public var cookieVerifier: NFS3CookieVerifier + public var entries: [Entry] + public var eof: NFS3Bool + } + + public struct Fail: Hashable & Sendable { + public init(dirAttributes: NFS3FileAttr? = nil) { + self.dirAttributes = dirAttributes + } + + public var dirAttributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFS3CallReadDir() throws -> NFS3CallReadDir { + let dir = try self.readNFS3FileHandle() + let cookie = try self.readNFS3Cookie() + let cookieVerifier = try self.readNFS3CookieVerifier() + let maxResultByteCount = try self.readNFS3Count() + + return NFS3CallReadDir(fileHandle: dir, + cookie: cookie, + cookieVerifier: cookieVerifier, + maxResultByteCount: maxResultByteCount) + } + + @discardableResult public mutating func writeNFS3CallReadDir(_ call: NFS3CallReadDir) -> Int { + return self.writeNFS3FileHandle(call.fileHandle) + + self.writeMultipleIntegers( + call.cookie.rawValue, + call.cookieVerifier.rawValue, + call.maxResultByteCount.rawValue + ) + } + + private mutating func readReadDirEntry() throws -> NFS3ReplyReadDir.Entry { + let fileID = try self.readNFS3FileID() + let fileName = try self.readNFS3String() + let cookie = try self.readNFS3Cookie() + + return NFS3ReplyReadDir.Entry(fileID: fileID, + fileName: fileName, + cookie: cookie) + } + + private mutating func writeReadDirEntry(_ entry: NFS3ReplyReadDir.Entry) -> Int { + return self.writeNFS3FileID(entry.fileID) + + self.writeNFS3String(entry.fileName) + + self.writeNFS3Cookie(entry.cookie) + } + + public mutating func readNFS3ReplyReadDir() throws -> NFS3ReplyReadDir { + return NFS3ReplyReadDir( + result: try self.readNFS3Result( + readOkay: { buffer in + let dirAttributes = try buffer.readNFS3Optional { try $0.readNFS3FileAttr() } + let cookieVerifier = try buffer.readNFS3CookieVerifier() + + var entries: [NFS3ReplyReadDir.Entry] = [] + while let entry = try buffer.readNFS3Optional({ try $0.readReadDirEntry() }) { + entries.append(entry) + } + let eof = try buffer.readNFS3Bool() + + return NFS3ReplyReadDir.Okay(dirAttributes: dirAttributes, + cookieVerifier: cookieVerifier, + entries: entries, + eof: eof) + }, + readFail: { buffer in + let attrs = try buffer.readNFS3Optional { try $0.readNFS3FileAttr() } + + return NFS3ReplyReadDir.Fail(dirAttributes: attrs) + }) + ) + } + + @discardableResult public mutating func writeNFS3ReplyReadDir(_ rd: NFS3ReplyReadDir) -> Int { + var bytesWritten = 0 + switch rd.result { + case .okay(let result): + bytesWritten += self.writeInteger(NFS3Status.ok.rawValue) + + self.writeNFS3Optional(result.dirAttributes, writer: { $0.writeNFS3FileAttr($1) }) + + self.writeNFS3CookieVerifier(result.cookieVerifier) + for entry in result.entries { + bytesWritten += self.writeInteger(1, as: UInt32.self) + + self.writeReadDirEntry(entry) + } + bytesWritten += self.writeInteger(0, as: UInt32.self) + + self.writeInteger(result.eof == true ? 1 : 0, as: UInt32.self) + case .fail(let status, let fail): + precondition(status != .ok) + bytesWritten += self.writeInteger(status.rawValue) + + self.writeNFS3Optional(fail.dirAttributes, writer: { $0.writeNFS3FileAttr($1) }) + } + return bytesWritten + } +} diff --git a/Sources/NIONFS3/NFSTypes+ReadDirPlus.swift b/Sources/NIONFS3/NFSTypes+ReadDirPlus.swift new file mode 100644 index 00000000..17757d39 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+ReadDirPlus.swift @@ -0,0 +1,176 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - ReadDirPlus +public struct NFS3CallReadDirPlus: Hashable & Sendable { + public init(fileHandle: NFS3FileHandle, cookie: NFS3Cookie, cookieVerifier: NFS3CookieVerifier, + dirCount: NFS3Count, maxCount: NFS3Count) { + self.fileHandle = fileHandle + self.cookie = cookie + self.cookieVerifier = cookieVerifier + self.dirCount = dirCount + self.maxCount = maxCount + } + + public var fileHandle: NFS3FileHandle + public var cookie: NFS3Cookie + public var cookieVerifier: NFS3CookieVerifier + public var dirCount: NFS3Count + public var maxCount: NFS3Count +} + +public struct NFS3ReplyReadDirPlus: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Entry: Hashable & Sendable { + public init(fileID: NFS3FileID, fileName: String, cookie: NFS3Cookie, nameAttributes: NFS3FileAttr? = nil, nameHandle: NFS3FileHandle? = nil) { + self.fileID = fileID + self.fileName = fileName + self.cookie = cookie + self.nameAttributes = nameAttributes + self.nameHandle = nameHandle + } + + public var fileID: NFS3FileID + public var fileName: String + public var cookie: NFS3Cookie + public var nameAttributes: NFS3FileAttr? + public var nameHandle: NFS3FileHandle? + } + + public struct Okay: Hashable & Sendable { + public init(dirAttributes: NFS3FileAttr? = nil, cookieVerifier: NFS3CookieVerifier, entries: [NFS3ReplyReadDirPlus.Entry], eof: NFS3Bool) { + self.dirAttributes = dirAttributes + self.cookieVerifier = cookieVerifier + self.entries = entries + self.eof = eof + } + + public var dirAttributes: NFS3FileAttr? + public var cookieVerifier: NFS3CookieVerifier + public var entries: [Entry] + public var eof: NFS3Bool + } + + public struct Fail: Hashable & Sendable { + public init(dirAttributes: NFS3FileAttr? = nil) { + self.dirAttributes = dirAttributes + } + + public var dirAttributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFS3CallReadDirPlus() throws -> NFS3CallReadDirPlus { + let dir = try self.readNFS3FileHandle() + let cookie = try self.readNFS3Cookie() + let cookieVerifier = try self.readNFS3CookieVerifier() + let dirCount = try self.readNFS3Count() + let maxCount = try self.readNFS3Count() + + return NFS3CallReadDirPlus(fileHandle: dir, + cookie: cookie, + cookieVerifier: cookieVerifier, + dirCount: dirCount, + maxCount: maxCount) + } + + @discardableResult public mutating func writeNFS3CallReadDirPlus(_ call: NFS3CallReadDirPlus) -> Int { + return self.writeNFS3FileHandle(call.fileHandle) + + self.writeMultipleIntegers( + call.cookie.rawValue, + call.cookieVerifier.rawValue, + call.dirCount.rawValue, + call.maxCount.rawValue + ) + } + + private mutating func readReadDirPlusEntry() throws -> NFS3ReplyReadDirPlus.Entry { + let fileID = try self.readNFS3FileID() + let fileName = try self.readNFS3String() + let cookie = try self.readNFS3Cookie() + let nameAttrs = try self.readNFS3Optional { try $0.readNFS3FileAttr() } + let nameHandle = try self.readNFS3Optional { try $0.readNFS3FileHandle() } + + return NFS3ReplyReadDirPlus.Entry(fileID: fileID, + fileName: fileName, + cookie: cookie, + nameAttributes: nameAttrs, + nameHandle: nameHandle) + } + + private mutating func writeReadDirPlusEntry(_ entry: NFS3ReplyReadDirPlus.Entry) -> Int { + return self.writeNFS3FileID(entry.fileID) + + self.writeNFS3String(entry.fileName) + + self.writeNFS3Cookie(entry.cookie) + + self.writeNFS3Optional(entry.nameAttributes, writer: { $0.writeNFS3FileAttr($1) }) + + self.writeNFS3Optional(entry.nameHandle, writer: { $0.writeNFS3FileHandle($1) }) + } + + public mutating func readNFS3ReplyReadDirPlus() throws -> NFS3ReplyReadDirPlus { + return NFS3ReplyReadDirPlus( + result: try self.readNFS3Result( + readOkay: { buffer in + let attrs = try buffer.readNFS3Optional { try $0.readNFS3FileAttr() } + let cookieVerifier = try buffer.readNFS3CookieVerifier() + + var entries: [NFS3ReplyReadDirPlus.Entry] = [] + while let entry = try buffer.readNFS3Optional({ try $0.readReadDirPlusEntry() }) { + entries.append(entry) + } + let eof = try buffer.readNFS3Bool() + + return NFS3ReplyReadDirPlus.Okay(dirAttributes: attrs, + cookieVerifier: cookieVerifier, + entries: entries, + eof: eof) + }, + readFail: { buffer in + let attrs = try buffer.readNFS3Optional { try $0.readNFS3FileAttr() } + + return NFS3ReplyReadDirPlus.Fail(dirAttributes: attrs) + }) + ) + } + + @discardableResult public mutating func writeNFS3ReplyReadDirPlus(_ rdp: NFS3ReplyReadDirPlus) -> Int { + var bytesWritten = 0 + + switch rdp.result { + case .okay(let result): + bytesWritten += self.writeInteger(NFS3Status.ok.rawValue) + + self.writeNFS3Optional(result.dirAttributes, writer: { $0.writeNFS3FileAttr($1) }) + + self.writeNFS3CookieVerifier(result.cookieVerifier) + for entry in result.entries { + bytesWritten += self.writeInteger(1, as: UInt32.self) + + self.writeReadDirPlusEntry(entry) + } + bytesWritten += self.writeInteger(0, as: UInt32.self) + + self.writeInteger(result.eof == true ? 1 : 0, as: UInt32.self) + case .fail(let status, let fail): + precondition(status != .ok) + bytesWritten += self.writeInteger(status.rawValue) + + self.writeNFS3Optional(fail.dirAttributes, writer: { $0.writeNFS3FileAttr($1) }) + } + + return bytesWritten + } +} diff --git a/Sources/NIONFS3/NFSTypes+Readlink.swift b/Sources/NIONFS3/NFSTypes+Readlink.swift new file mode 100644 index 00000000..230195e8 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Readlink.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - Readlink +public struct NFS3CallReadlink: Hashable & Sendable { + public init(symlink: NFS3FileHandle) { + self.symlink = symlink + } + + public var symlink: NFS3FileHandle +} + +public struct NFS3ReplyReadlink: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Hashable & Sendable { + public init(symlinkAttributes: NFS3FileAttr? = nil, target: String) { + self.symlinkAttributes = symlinkAttributes + self.target = target + } + + public var symlinkAttributes: NFS3FileAttr? + public var target: String + } + + public struct Fail: Hashable & Sendable { + public init(symlinkAttributes: NFS3FileAttr? = nil) { + self.symlinkAttributes = symlinkAttributes + } + + public var symlinkAttributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFS3CallReadlink() throws -> NFS3CallReadlink { + let symlink = try self.readNFS3FileHandle() + + return .init(symlink: symlink) + } + + @discardableResult public mutating func writeNFS3CallReadlink(_ call: NFS3CallReadlink) -> Int { + self.writeNFS3FileHandle(call.symlink) + } + + public mutating func readNFS3ReplyReadlink() throws -> NFS3ReplyReadlink { + return NFS3ReplyReadlink( + result: try self.readNFS3Result( + readOkay: { buffer in + let attrs = try buffer.readNFS3Optional { try $0.readNFS3FileAttr() } + let target = try buffer.readNFS3String() + + return NFS3ReplyReadlink.Okay(symlinkAttributes: attrs, target: target) + }, + readFail: { buffer in + let attrs = try buffer.readNFS3Optional { try $0.readNFS3FileAttr() } + return NFS3ReplyReadlink.Fail(symlinkAttributes: attrs) + })) + } + + @discardableResult public mutating func writeNFS3ReplyReadlink(_ reply: NFS3ReplyReadlink) -> Int { + var bytesWritten = self.writeNFS3ResultStatus(reply.result) + + switch reply.result { + case .okay(let okay): + bytesWritten += self.writeNFS3Optional(okay.symlinkAttributes, writer: { $0.writeNFS3FileAttr($1) }) + + self.writeNFS3String(okay.target) + case .fail(_, let fail): + bytesWritten += self.writeNFS3Optional(fail.symlinkAttributes, writer: { $0.writeNFS3FileAttr($1) }) + } + return bytesWritten + } +} diff --git a/Sources/NIONFS3/NFSTypes+SetAttr.swift b/Sources/NIONFS3/NFSTypes+SetAttr.swift new file mode 100644 index 00000000..4ee43cfe --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+SetAttr.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// MARK: - Setattr +public struct NFS3CallSetattr: Hashable & Sendable { + public init(object: NFS3FileHandle, newAttributes: NFS3CallSetattr.Attributes, guard: NFS3Time? = nil) { + self.object = object + self.newAttributes = newAttributes + self.guard = `guard` + } + + public struct Attributes: Hashable & Sendable { + public init(mode: NFS3FileMode? = nil, uid: NFS3UID? = nil, gid: NFS3GID? = nil, size: NFS3Size? = nil, atime: NFS3Time? = nil, mtime: NFS3Time? = nil) { + self.mode = mode + self.uid = uid + self.gid = gid + self.size = size + self.atime = atime + self.mtime = mtime + } + + public var mode: NFS3FileMode? + public var uid: NFS3UID? + public var gid: NFS3GID? + public var size: NFS3Size? + public var atime: NFS3Time? + public var mtime: NFS3Time? + + } + public var object: NFS3FileHandle + public var newAttributes: Attributes + public var `guard`: NFS3Time? +} + +public struct NFS3ReplySetattr: Hashable & Sendable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Hashable & Sendable { + public init(wcc: NFS3WeakCacheConsistencyData) { + self.wcc = wcc + } + + public var wcc: NFS3WeakCacheConsistencyData + } + + public struct Fail: Hashable & Sendable { + public init(wcc: NFS3WeakCacheConsistencyData) { + self.wcc = wcc + } + + public var wcc: NFS3WeakCacheConsistencyData + } + + public var result: NFS3Result +} + +extension ByteBuffer { + private mutating func readNFS3CallSetattrAttributes() throws -> NFS3CallSetattr.Attributes { + let mode = try self.readNFS3Optional { try $0.readNFS3FileMode() } + let uid = try self.readNFS3Optional { try $0.readNFS3UID() } + let gid = try self.readNFS3Optional { try $0.readNFS3GID() } + let size = try self.readNFS3Optional { try $0.readNFS3Size() } + let atime = try self.readNFS3Optional { try $0.readNFS3Time() } + let mtime = try self.readNFS3Optional { try $0.readNFS3Time() } + + return .init(mode: mode, uid: uid, gid: gid, size: size, atime: atime, mtime: mtime) + } + + private mutating func writeNFS3CallSetattrAttributes(_ attrs: NFS3CallSetattr.Attributes) -> Int { + return self.writeNFS3Optional(attrs.mode, writer: { $0.writeNFS3FileMode($1) }) + + self.writeNFS3Optional(attrs.uid, writer: { $0.writeNFS3UID($1) }) + + self.writeNFS3Optional(attrs.gid, writer: { $0.writeNFS3GID($1) }) + + self.writeNFS3Optional(attrs.size, writer: { $0.writeNFS3Size($1) }) + + self.writeNFS3Optional(attrs.atime, writer: { $0.writeNFS3Time($1) }) + + self.writeNFS3Optional(attrs.mtime, writer: { $0.writeNFS3Time($1) }) + } + + public mutating func readNFS3CallSetattr() throws -> NFS3CallSetattr { + let object = try self.readNFS3FileHandle() + let attributes = try self.readNFS3CallSetattrAttributes() + let `guard` = try self.readNFS3Optional { try $0.readNFS3Time() } + + return .init(object: object, newAttributes: attributes, guard: `guard`) + } + + @discardableResult public mutating func writeNFS3CallSetattr(_ call: NFS3CallSetattr) -> Int { + return self.writeNFS3FileHandle(call.object) + + self.writeNFS3CallSetattrAttributes(call.newAttributes) + + self.writeNFS3Optional(call.guard, writer: { $0.writeNFS3Time($1) }) + } + + public mutating func readNFS3ReplySetattr() throws -> NFS3ReplySetattr { + return NFS3ReplySetattr( + result: try self.readNFS3Result( + readOkay: { buffer in + return NFS3ReplySetattr.Okay(wcc: try buffer.readNFS3WeakCacheConsistencyData()) + }, + readFail: { buffer in + return NFS3ReplySetattr.Fail(wcc: try buffer.readNFS3WeakCacheConsistencyData()) + })) + } + + @discardableResult public mutating func writeNFS3ReplySetattr(_ reply: NFS3ReplySetattr) -> Int { + var bytesWritten = self.writeNFS3ResultStatus(reply.result) + + switch reply.result { + case .okay(let okay): + bytesWritten += self.writeNFS3WeakCacheConsistencyData(okay.wcc) + case .fail(_, let fail): + bytesWritten += self.writeNFS3WeakCacheConsistencyData(fail.wcc) + } + return bytesWritten + } +} diff --git a/Sources/NIONFS3/RPCTypes.swift b/Sources/NIONFS3/RPCTypes.swift new file mode 100644 index 00000000..6f4aa364 --- /dev/null +++ b/Sources/NIONFS3/RPCTypes.swift @@ -0,0 +1,205 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +public struct RPCFragmentHeader: Hashable & Sendable { + public var length: UInt32 + public var last: Bool + + public init(length: UInt32, last: Bool) { + self.length = length + self.last = last + } + + public init(rawValue: UInt32) { + let last = rawValue & (1 << 31) == 0 ? false : true + let length = rawValue & (UInt32.max ^ (1 << 31)) + + self = .init(length: length, last: last) + } + + public var rawValue: UInt32 { + var rawValue = self.length + rawValue |= ((self.last ? 1 : 0) << 31) + return rawValue + } +} + +public enum RPCMessageType: UInt32, Hashable & Sendable { + case call = 0 + case reply = 1 +} + +/// RFC 5531: struct rpc_msg +public enum RPCMessage: Hashable & Sendable { + case call(RPCCall) + case reply(RPCReply) + + var xid: UInt32 { + get { + switch self { + case .call(let call): + return call.xid + case .reply(let reply): + return reply.xid + } + } + set { + switch self { + case .call(var call): + call.xid = newValue + self = .call(call) + case .reply(var reply): + reply.xid = newValue + self = .reply(reply) + } + } + } +} + +/// RFC 5531: struct call_body +public struct RPCCall: Hashable & Sendable { + public init(xid: UInt32, rpcVersion: UInt32, program: UInt32, programVersion: UInt32, procedure: UInt32, credentials: RPCCredentials, verifier: RPCOpaqueAuth) { + self.xid = xid + self.rpcVersion = rpcVersion + self.program = program + self.programVersion = programVersion + self.procedure = procedure + self.credentials = credentials + self.verifier = verifier + } + + public var xid: UInt32 + public var rpcVersion: UInt32 // must be 2 + public var program: UInt32 + public var programVersion: UInt32 + public var procedure: UInt32 + public var credentials: RPCCredentials + public var verifier: RPCOpaqueAuth +} + +extension RPCCall { + public var programAndProcedure: RPCNFS3ProcedureID { + get { + return RPCNFS3ProcedureID(program: self.program, procedure: self.procedure) + } + set { + self.program = newValue.program + self.procedure = newValue.procedure + } + } +} + +public enum RPCReplyStatus: Hashable & Sendable { + case messageAccepted(RPCAcceptedReply) + case messageDenied(RPCRejectedReply) +} + +public struct RPCReply: Hashable & Sendable { + public var xid: UInt32 + public var status: RPCReplyStatus + + public init(xid: UInt32, status: RPCReplyStatus) { + self.xid = xid + self.status = status + } +} + +public enum RPCAcceptedReplyStatus: Hashable & Sendable { + case success + case programUnavailable + case programMismatch(low: UInt32, high: UInt32) + case procedureUnavailable + case garbageArguments + case systemError +} + +public struct RPCOpaqueAuth: Hashable & Sendable { + public var flavor: RPCAuthFlavor + public var opaque: ByteBuffer? = nil + + public init(flavor: RPCAuthFlavor, opaque: ByteBuffer? = nil) { + self.flavor = flavor + self.opaque = opaque + } +} + +public struct RPCAcceptedReply: Hashable & Sendable { + public var verifier: RPCOpaqueAuth + public var status: RPCAcceptedReplyStatus + + public init(verifier: RPCOpaqueAuth, status: RPCAcceptedReplyStatus) { + self.verifier = verifier + self.status = status + } +} + +public enum RPCAuthStatus: UInt32, Hashable & Sendable { + case ok = 0 /* success */ + case badCredentials = 1 /* bad credential (seal broken) */ + case rejectedCredentials = 2 /* client must begin new session */ + case badVerifier = 3 /* bad verifier (seal broken) */ + case rejectedVerifier = 4 /* verifier expired or replayed */ + case rejectedForSecurityReasons = 5 /* rejected for security reasons */ + case invalidResponseVerifier = 6 /* bogus response verifier */ + case failedForUnknownReason = 7 /* reason unknown */ + case kerberosError = 8 /* kerberos generic error */ + case credentialExpired = 9 /* time of credential expired */ + case ticketFileProblem = 10 /* problem with ticket file */ + case cannotDecodeAuthenticator = 11 /* can't decode authenticator */ + case illegalNetworkAddressInTicket = 12 /* wrong net address in ticket */ + case noCredentialsForUser = 13 /* no credentials for user */ + case problemWithGSSContext = 14 /* problem with context */ +} + +public enum RPCRejectedReply: Hashable & Sendable { + case rpcMismatch(low: UInt32, high: UInt32) + case authError(RPCAuthStatus) +} + +public enum RPCErrors: Error { + case unknownType(UInt32) + case tooLong(RPCFragmentHeader, xid: UInt32, messageType: UInt32) + case fragementHeaderLengthTooShort(UInt32) + case unknownVerifier(UInt32) + case unknownVersion(UInt32) + case invalidAuthFlavor(UInt32) + case illegalReplyStatus(UInt32) + case illegalReplyAcceptanceStatus(UInt32) + case illegalReplyRejectionStatus(UInt32) + case illegalAuthStatus(UInt32) +} + +public struct RPCCredentials: Hashable & Sendable { + internal var flavor: UInt32 + internal var length: UInt32 + internal var otherBytes: ByteBuffer + + public init(flavor: UInt32, length: UInt32, otherBytes: ByteBuffer) { + self.flavor = flavor + self.length = length + self.otherBytes = otherBytes + } +} + +public enum RPCAuthFlavor: UInt32, Hashable & Sendable { + case noAuth = 0 + case system = 1 + case short = 2 + case dh = 3 + case rpcSecGSS = 6 + + public static let unix: Self = .system +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 4bfb3d66..b96c849f 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2018-2022 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2018-2023 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -26,6 +26,7 @@ import XCTest #if os(Linux) || os(FreeBSD) || os(Android) @testable import NIOExtrasTests @testable import NIOHTTPCompressionTests + @testable import NIONFS3Tests @testable import NIOSOCKSTests @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") @@ -40,6 +41,7 @@ class LinuxMainRunner { testCase(DebugInboundEventsHandlerTest.allTests), testCase(DebugOutboundEventsHandlerTest.allTests), testCase(FixedLengthFrameDecoderTest.allTests), + testCase(HTTP1ProxyConnectHandlerTests.allTests), testCase(HTTPRequestCompressorTest.allTests), testCase(HTTPRequestDecompressorTest.allTests), testCase(HTTPResponseCompressorTest.allTests), @@ -51,6 +53,9 @@ class LinuxMainRunner { testCase(LengthFieldPrependerTest.allTests), testCase(LineBasedFrameDecoderTest.allTests), testCase(MethodSelectionTests.allTests), + testCase(NFS3FileSystemTests.allTests), + testCase(NFS3ReplyEncoderTest.allTests), + testCase(NFS3RoundtripTests.allTests), testCase(PCAPRingBufferTest.allTests), testCase(QuiescingHelperTest.allTests), testCase(RequestResponseHandlerTest.allTests), @@ -58,6 +63,7 @@ class LinuxMainRunner { testCase(ServerResponseTests.allTests), testCase(ServerStateMachineTests.allTests), testCase(SocksClientHandlerTests.allTests), + testCase(SynchronizedFileSinkTests.allTests), testCase(WritePCAPHandlerTest.allTests), ]) } diff --git a/Tests/NIOExtrasTests/HTTP1ProxyConnectHandlerTests+XCTest.swift b/Tests/NIOExtrasTests/HTTP1ProxyConnectHandlerTests+XCTest.swift new file mode 100644 index 00000000..e7a2d53a --- /dev/null +++ b/Tests/NIOExtrasTests/HTTP1ProxyConnectHandlerTests+XCTest.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2023 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 +// +//===----------------------------------------------------------------------===// +// +// HTTP1ProxyConnectHandlerTests+XCTest.swift +// +import XCTest + +/// +/// NOTE: This file was generated by generate_linux_tests.rb +/// +/// Do NOT edit this file directly as it will be regenerated automatically when needed. +/// + +extension HTTP1ProxyConnectHandlerTests { + + @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") + static var allTests : [(String, (HTTP1ProxyConnectHandlerTests) -> () throws -> Void)] { + return [ + ("testProxyConnectWithoutAuthorizationSuccess", testProxyConnectWithoutAuthorizationSuccess), + ("testProxyConnectWithAuthorization", testProxyConnectWithAuthorization), + ("testProxyConnectWithoutAuthorizationFailure500", testProxyConnectWithoutAuthorizationFailure500), + ("testProxyConnectWithoutAuthorizationButAuthorizationNeeded", testProxyConnectWithoutAuthorizationButAuthorizationNeeded), + ("testProxyConnectReceivesBody", testProxyConnectReceivesBody), + ("testProxyConnectWithoutAuthorizationBufferedWrites", testProxyConnectWithoutAuthorizationBufferedWrites), + ("testProxyConnectFailsBufferedWritesAreFailed", testProxyConnectFailsBufferedWritesAreFailed), + ] + } +} + diff --git a/Tests/NIOExtrasTests/HTTP1ProxyConnectHandlerTests.swift b/Tests/NIOExtrasTests/HTTP1ProxyConnectHandlerTests.swift new file mode 100644 index 00000000..f2457d18 --- /dev/null +++ b/Tests/NIOExtrasTests/HTTP1ProxyConnectHandlerTests.swift @@ -0,0 +1,363 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2022 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 +// +//===----------------------------------------------------------------------===// + +@testable import NIOExtras +import NIOCore +import NIOEmbedded +import NIOHTTP1 +import XCTest + +class HTTP1ProxyConnectHandlerTests: XCTestCase { + func testProxyConnectWithoutAuthorizationSuccess() throws { + let embedded = EmbeddedChannel() + defer { XCTAssertNoThrow(try embedded.finish(acceptAlreadyClosed: false)) } + + let socketAddress = try! SocketAddress.makeAddressResolvingHost("localhost", port: 0) + XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait()) + + let promise: EventLoopPromise = embedded.eventLoop.makePromise() + let proxyConnectHandler = NIOHTTP1ProxyConnectHandler( + targetHost: "swift.org", + targetPort: 443, + headers: [:], + deadline: .now() + .seconds(10), + promise: promise + ) + + XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler)) + + let head = try XCTUnwrap(try embedded.readOutbound(as: HTTPClientRequestPart.self)).assertHead() + XCTAssertEqual(head.method, .CONNECT) + XCTAssertEqual(head.uri, "swift.org:443") + XCTAssertNil(head.headers["proxy-authorization"].first) + XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil)) + + let responseHead = HTTPResponseHead(version: .http1_1, status: .ok) + XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead))) + XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil))) + + XCTAssertNoThrow(try promise.futureResult.wait()) + } + + func testProxyConnectWithAuthorization() throws { + let embedded = EmbeddedChannel() + + let socketAddress = try! SocketAddress.makeAddressResolvingHost("localhost", port: 0) + XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait()) + + let promise: EventLoopPromise = embedded.eventLoop.makePromise() + let proxyConnectHandler = NIOHTTP1ProxyConnectHandler( + targetHost: "swift.org", + targetPort: 443, + headers: ["proxy-authorization" : "Basic abc123"], + deadline: .now() + .seconds(10), + promise: promise + ) + + XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler)) + + let head = try XCTUnwrap(try embedded.readOutbound(as: HTTPClientRequestPart.self)).assertHead() + XCTAssertEqual(head.method, .CONNECT) + XCTAssertEqual(head.uri, "swift.org:443") + XCTAssertEqual(head.headers["proxy-authorization"].first, "Basic abc123") + XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil)) + + let responseHead = HTTPResponseHead(version: .http1_1, status: .ok) + XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead))) + XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil))) + + XCTAssertNoThrow(try promise.futureResult.wait()) + } + + func testProxyConnectWithoutAuthorizationFailure500() throws { + let embedded = EmbeddedChannel() + + let socketAddress = try! SocketAddress.makeAddressResolvingHost("localhost", port: 0) + XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait()) + + let promise: EventLoopPromise = embedded.eventLoop.makePromise() + let proxyConnectHandler = NIOHTTP1ProxyConnectHandler( + targetHost: "swift.org", + targetPort: 443, + headers: [:], + deadline: .now() + .seconds(10), + promise: promise + ) + + XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler)) + + let head = try XCTUnwrap(try embedded.readOutbound(as: HTTPClientRequestPart.self)).assertHead() + XCTAssertEqual(head.method, .CONNECT) + XCTAssertEqual(head.uri, "swift.org:443") + XCTAssertNil(head.headers["proxy-authorization"].first) + XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil)) + + let responseHead = HTTPResponseHead(version: .http1_1, status: .internalServerError) + // answering with 500 should lead to a triggered error in pipeline + XCTAssertThrowsError(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead))) { + XCTAssertEqual($0 as? NIOHTTP1ProxyConnectHandler.Error, .invalidProxyResponseHead(responseHead)) + } + XCTAssertFalse(embedded.isActive, "Channel should be closed in response to the error") + XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil))) + + XCTAssertThrowsError(try promise.futureResult.wait()) { + XCTAssertEqual($0 as? NIOHTTP1ProxyConnectHandler.Error, .invalidProxyResponseHead(responseHead)) + } + } + + func testProxyConnectWithoutAuthorizationButAuthorizationNeeded() throws { + let embedded = EmbeddedChannel() + + let socketAddress = try! SocketAddress.makeAddressResolvingHost("localhost", port: 0) + XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait()) + + let promise: EventLoopPromise = embedded.eventLoop.makePromise() + let proxyConnectHandler = NIOHTTP1ProxyConnectHandler( + targetHost: "swift.org", + targetPort: 443, + headers: [:], + deadline: .now() + .seconds(10), + promise: promise + ) + + XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler)) + + let head = try XCTUnwrap(try embedded.readOutbound(as: HTTPClientRequestPart.self)).assertHead() + XCTAssertEqual(head.method, .CONNECT) + XCTAssertEqual(head.uri, "swift.org:443") + XCTAssertNil(head.headers["proxy-authorization"].first) + XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil)) + + let responseHead = HTTPResponseHead(version: .http1_1, status: .proxyAuthenticationRequired) + // answering with 500 should lead to a triggered error in pipeline + XCTAssertThrowsError(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead))) { + XCTAssertEqual($0 as? NIOHTTP1ProxyConnectHandler.Error, .proxyAuthenticationRequired()) + } + XCTAssertFalse(embedded.isActive, "Channel should be closed in response to the error") + XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil))) + + XCTAssertThrowsError(try promise.futureResult.wait()) { + XCTAssertEqual($0 as? NIOHTTP1ProxyConnectHandler.Error, .proxyAuthenticationRequired()) + } + } + + func testProxyConnectReceivesBody() { + let embedded = EmbeddedChannel() + + let socketAddress = try! SocketAddress.makeAddressResolvingHost("localhost", port: 0) + XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait()) + + let promise: EventLoopPromise = embedded.eventLoop.makePromise() + let proxyConnectHandler = NIOHTTP1ProxyConnectHandler( + targetHost: "swift.org", + targetPort: 443, + headers: [:], + deadline: .now() + .seconds(10), + promise: promise + ) + + XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler)) + + var maybeHead: HTTPClientRequestPart? + XCTAssertNoThrow(maybeHead = try embedded.readOutbound(as: HTTPClientRequestPart.self)) + guard case .some(.head(let head)) = maybeHead else { + return XCTFail("Expected the proxy connect handler to first send a http head part") + } + + XCTAssertEqual(head.method, .CONNECT) + XCTAssertEqual(head.uri, "swift.org:443") + XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil)) + + let responseHead = HTTPResponseHead(version: .http1_1, status: .ok) + XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead))) + // answering with a body should lead to a triggered error in pipeline + XCTAssertThrowsError(try embedded.writeInbound(HTTPClientResponsePart.body(ByteBuffer(bytes: [0, 1, 2, 3])))) { + XCTAssertEqual($0 as? NIOHTTP1ProxyConnectHandler.Error, .invalidProxyResponse()) + } + XCTAssertEqual(embedded.isActive, false) + XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil))) + + XCTAssertThrowsError(try promise.futureResult.wait()) { + XCTAssertEqual($0 as? NIOHTTP1ProxyConnectHandler.Error, .invalidProxyResponse()) + } + } + + func testProxyConnectWithoutAuthorizationBufferedWrites() throws { + let embedded = EmbeddedChannel() + defer { XCTAssertNoThrow(try embedded.finish(acceptAlreadyClosed: false)) } + + let socketAddress = try! SocketAddress.makeAddressResolvingHost("localhost", port: 0) + XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait()) + + let proxyConnectPromise: EventLoopPromise = embedded.eventLoop.makePromise() + let proxyConnectHandler = NIOHTTP1ProxyConnectHandler( + targetHost: "swift.org", + targetPort: 443, + headers: [:], + deadline: .now() + .seconds(10), + promise: proxyConnectPromise + ) + + XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler)) + + // write a request to be buffered inside the ProxyConnectHandler + // it will be unbuffered when the handler completes and removes itself + let requestHead = HTTPRequestHead(version: HTTPVersion(major: 1, minor: 1), method: .GET, uri: "http://apple.com") + var promises: [EventLoopPromise] = [] + promises.append(embedded.eventLoop.makePromise()) + embedded.pipeline.write(NIOAny(HTTPClientRequestPart.head(requestHead)), promise: promises.last) + + promises.append(embedded.eventLoop.makePromise()) + embedded.pipeline.write(NIOAny(HTTPClientRequestPart.body(.byteBuffer(ByteBuffer(string: "Test")))), promise: promises.last) + + promises.append(embedded.eventLoop.makePromise()) + embedded.pipeline.write(NIOAny(HTTPClientRequestPart.end(nil)), promise: promises.last) + embedded.pipeline.flush() + + // read the connect header back + let connectHead = try XCTUnwrap(try embedded.readOutbound(as: HTTPClientRequestPart.self)).assertHead() + + XCTAssertEqual(connectHead.method, .CONNECT) + XCTAssertEqual(connectHead.uri, "swift.org:443") + XCTAssertNil(connectHead.headers["proxy-authorization"].first) + + let connectTrailers = try XCTUnwrap(try embedded.readOutbound(as: HTTPClientRequestPart.self)).assertEnd() + XCTAssertNil(connectTrailers) + + // ensure that nothing has been unbuffered by mistake + XCTAssertNil(try embedded.readOutbound(as: HTTPClientRequestPart.self)) + + let responseHead = HTTPResponseHead(version: .http1_1, status: .ok) + XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead))) + XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil))) + + XCTAssertNoThrow(try proxyConnectPromise.futureResult.wait()) + + // read the buffered write back + let bufferedHead = try XCTUnwrap(try embedded.readOutbound(as: HTTPClientRequestPart.self)).assertHead() + + XCTAssertEqual(bufferedHead.method, .GET) + XCTAssertEqual(bufferedHead.uri, "http://apple.com") + XCTAssertNil(bufferedHead.headers["proxy-authorization"].first) + + let bufferedBody = try XCTUnwrap(try embedded.readOutbound(as: HTTPClientRequestPart.self)).assertBody() + XCTAssertEqual(bufferedBody, ByteBuffer(string: "Test")) + + let bufferedTrailers = try XCTUnwrap(try embedded.readOutbound(as: HTTPClientRequestPart.self)).assertEnd() + XCTAssertNil(bufferedTrailers) + + let resultFutures = promises.map { $0.futureResult } + XCTAssertNoThrow(_ = try EventLoopFuture.whenAllComplete(resultFutures, on: embedded.eventLoop).wait()) + } + + func testProxyConnectFailsBufferedWritesAreFailed() throws { + let embedded = EmbeddedChannel() + + let socketAddress = try! SocketAddress.makeAddressResolvingHost("localhost", port: 0) + XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait()) + + let proxyConnectPromise: EventLoopPromise = embedded.eventLoop.makePromise() + let proxyConnectHandler = NIOHTTP1ProxyConnectHandler( + targetHost: "swift.org", + targetPort: 443, + headers: [:], + deadline: .now() + .seconds(10), + promise: proxyConnectPromise + ) + + XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler)) + + // write a request to be buffered inside the ProxyConnectHandler + // it will be unbuffered when the handler completes and removes itself + let requestHead = HTTPRequestHead(version: HTTPVersion(major: 1, minor: 1), method: .GET, uri: "apple.com") + var promises: [EventLoopPromise] = [] + promises.append(embedded.eventLoop.makePromise()) + embedded.pipeline.write(NIOAny(HTTPClientRequestPart.head(requestHead)), promise: promises.last) + + promises.append(embedded.eventLoop.makePromise()) + embedded.pipeline.write(NIOAny(HTTPClientRequestPart.body(.byteBuffer(ByteBuffer(string: "Test")))), promise: promises.last) + + promises.append(embedded.eventLoop.makePromise()) + embedded.pipeline.write(NIOAny(HTTPClientRequestPart.end(nil)), promise: promises.last) + embedded.pipeline.flush() + + // read the connect header back + let connectHead = try XCTUnwrap(try embedded.readOutbound(as: HTTPClientRequestPart.self)).assertHead() + + XCTAssertEqual(connectHead.method, .CONNECT) + XCTAssertEqual(connectHead.uri, "swift.org:443") + XCTAssertNil(connectHead.headers["proxy-authorization"].first) + + let connectTrailers = try XCTUnwrap(try embedded.readOutbound(as: HTTPClientRequestPart.self)).assertEnd() + XCTAssertNil(connectTrailers) + + // ensure that nothing has been unbuffered by mistake + XCTAssertNil(try embedded.readOutbound(as: HTTPClientRequestPart.self)) + + let responseHead = HTTPResponseHead(version: .http1_1, status: .internalServerError) + XCTAssertThrowsError(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead))) { + XCTAssertEqual($0 as? NIOHTTP1ProxyConnectHandler.Error, .invalidProxyResponseHead(responseHead)) + } + XCTAssertFalse(embedded.isActive, "Channel should be closed in response to the error") + XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil))) + + XCTAssertThrowsError(try proxyConnectPromise.futureResult.wait()) { + XCTAssertEqual($0 as? NIOHTTP1ProxyConnectHandler.Error, .invalidProxyResponseHead(responseHead)) + } + + // buffered writes are dropped + XCTAssertNil(try embedded.readOutbound(as: HTTPClientRequestPart.self)) + + // all outstanding buffered write promises should be completed + let resultFutures = promises.map { $0.futureResult } + XCTAssertNoThrow(_ = try EventLoopFuture.whenAllComplete(resultFutures, on: embedded.eventLoop).wait()) + } +} + +struct HTTPRequestPartMismatch: Error {} + +extension HTTPClientRequestPart { + @discardableResult + func assertHead(file: StaticString = #file, line: UInt = #line) throws -> HTTPRequestHead { + switch self { + case .head(let head): + return head + default: + XCTFail("Expected .head but got \(self)", file: file, line: line) + throw HTTPRequestPartMismatch() + } + } + + @discardableResult + func assertBody(file: StaticString = #file, line: UInt = #line) throws -> ByteBuffer { + switch self { + case .body(.byteBuffer(let body)): + return body + default: + XCTFail("Expected .body but got \(self)", file: file, line: line) + throw HTTPRequestPartMismatch() + } + } + + @discardableResult + func assertEnd(file: StaticString = #file, line: UInt = #line) throws -> HTTPHeaders? { + switch self { + case .end(let trailers): + return trailers + default: + XCTFail("Expected .end but got \(self)", file: file, line: line) + throw HTTPRequestPartMismatch() + } + } +} diff --git a/Tests/NIOExtrasTests/QuiescingHelperTest+XCTest.swift b/Tests/NIOExtrasTests/QuiescingHelperTest+XCTest.swift index 91d64281..de40aef4 100644 --- a/Tests/NIOExtrasTests/QuiescingHelperTest+XCTest.swift +++ b/Tests/NIOExtrasTests/QuiescingHelperTest+XCTest.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2018-2022 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2018-2023 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -30,6 +30,12 @@ extension QuiescingHelperTest { ("testShutdownIsImmediateWhenNoChannelsCollected", testShutdownIsImmediateWhenNoChannelsCollected), ("testQuiesceUserEventReceivedOnShutdown", testQuiesceUserEventReceivedOnShutdown), ("testQuiescingDoesNotSwallowCloseErrorsFromAcceptHandler", testQuiescingDoesNotSwallowCloseErrorsFromAcceptHandler), + ("testShutdownIsImmediateWhenPromiseDoesNotSucceed", testShutdownIsImmediateWhenPromiseDoesNotSucceed), + ("testShutdown_whenAlreadyShutdown", testShutdown_whenAlreadyShutdown), + ("testShutdown_whenNoOpenChild", testShutdown_whenNoOpenChild), + ("testChannelClose_whenRunning", testChannelClose_whenRunning), + ("testChannelAdded_whenShuttingDown", testChannelAdded_whenShuttingDown), + ("testChannelAdded_whenShutdown", testChannelAdded_whenShutdown), ] } } diff --git a/Tests/NIOExtrasTests/QuiescingHelperTest.swift b/Tests/NIOExtrasTests/QuiescingHelperTest.swift index fccec295..ac62e78b 100644 --- a/Tests/NIOExtrasTests/QuiescingHelperTest.swift +++ b/Tests/NIOExtrasTests/QuiescingHelperTest.swift @@ -12,12 +12,27 @@ // //===----------------------------------------------------------------------===// -import XCTest import NIOCore import NIOEmbedded +@testable import NIOExtras import NIOPosix import NIOTestUtils -@testable import NIOExtras +import XCTest + +private final class WaitForQuiesceUserEvent: ChannelInboundHandler { + typealias InboundIn = Never + private let promise: EventLoopPromise + + init(promise: EventLoopPromise) { + self.promise = promise + } + + func userInboundEventTriggered(context _: ChannelHandlerContext, event: Any) { + if event is ChannelShouldQuiesceEvent { + self.promise.succeed(()) + } + } +} public class QuiescingHelperTest: XCTestCase { func testShutdownIsImmediateWhenNoChannelsCollected() throws { @@ -35,21 +50,6 @@ public class QuiescingHelperTest: XCTestCase { } func testQuiesceUserEventReceivedOnShutdown() throws { - class WaitForQuiesceUserEvent: ChannelInboundHandler { - typealias InboundIn = Never - private let promise: EventLoopPromise - - init(promise: EventLoopPromise) { - self.promise = promise - } - - func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { - if event is ChannelShouldQuiesceEvent { - self.promise.succeed(()) - } - } - } - let el = EmbeddedEventLoop() let allShutdownPromise: EventLoopPromise = el.makePromise() let serverChannel = EmbeddedChannel(handler: nil, loop: el) @@ -63,7 +63,7 @@ public class QuiescingHelperTest: XCTestCase { // add a bunch of channels for pretendPort in 1...128 { - let waitForPromise: EventLoopPromise<()> = el.makePromise() + let waitForPromise: EventLoopPromise = el.makePromise() let channel = EmbeddedChannel(handler: WaitForQuiesceUserEvent(promise: waitForPromise), loop: el) // activate the child chan XCTAssertNoThrow(try channel.connect(to: .init(ipAddress: "1.2.3.4", port: pretendPort)).wait()) @@ -136,4 +136,179 @@ public class QuiescingHelperTest: XCTestCase { XCTAssert(error is DummyError) } } + + /// verifying that the promise fails when goes out of scope for shutdown + func testShutdownIsImmediateWhenPromiseDoesNotSucceed() throws { + let el = EmbeddedEventLoop() + + let p: EventLoopPromise = el.makePromise() + + do { + let quiesce = ServerQuiescingHelper(group: el) + quiesce.initiateShutdown(promise: p) + } + XCTAssertThrowsError(try p.futureResult.wait()) { error in + XCTAssertTrue(error is ServerQuiescingHelper.UnusedQuiescingHelperError) + } + } + + func testShutdown_whenAlreadyShutdown() throws { + let el = EmbeddedEventLoop() + let channel = EmbeddedChannel(handler: nil, loop: el) + // let's activate the server channel, nothing actually happens as this is an EmbeddedChannel + XCTAssertNoThrow(try channel.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 1)).wait()) + XCTAssertTrue(channel.isActive) + let quiesce = ServerQuiescingHelper(group: el) + _ = quiesce.makeServerChannelHandler(channel: channel) + let p1: EventLoopPromise = el.makePromise() + quiesce.initiateShutdown(promise: p1) + XCTAssertNoThrow(try p1.futureResult.wait()) + XCTAssertFalse(channel.isActive) + + let p2: EventLoopPromise = el.makePromise() + quiesce.initiateShutdown(promise: p2) + XCTAssertNoThrow(try p2.futureResult.wait()) + } + + func testShutdown_whenNoOpenChild() throws { + let el = EmbeddedEventLoop() + let channel = EmbeddedChannel(handler: nil, loop: el) + // let's activate the server channel, nothing actually happens as this is an EmbeddedChannel + XCTAssertNoThrow(try channel.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 1)).wait()) + XCTAssertTrue(channel.isActive) + let quiesce = ServerQuiescingHelper(group: el) + _ = quiesce.makeServerChannelHandler(channel: channel) + let p1: EventLoopPromise = el.makePromise() + quiesce.initiateShutdown(promise: p1) + el.run() + XCTAssertNoThrow(try p1.futureResult.wait()) + XCTAssertFalse(channel.isActive) + } + + func testChannelClose_whenRunning() throws { + let el = EmbeddedEventLoop() + let allShutdownPromise: EventLoopPromise = el.makePromise() + let serverChannel = EmbeddedChannel(handler: nil, loop: el) + // let's activate the server channel, nothing actually happens as this is an EmbeddedChannel + XCTAssertNoThrow(try serverChannel.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 1)).wait()) + let quiesce = ServerQuiescingHelper(group: el) + let collectionHandler = quiesce.makeServerChannelHandler(channel: serverChannel) + XCTAssertNoThrow(try serverChannel.pipeline.addHandler(collectionHandler).wait()) + + // let's one channels + let eventCounterHandler = EventCounterHandler() + let childChannel1 = EmbeddedChannel(handler: eventCounterHandler, loop: el) + // activate the child channel + XCTAssertNoThrow(try childChannel1.connect(to: .init(ipAddress: "1.2.3.4", port: 1)).wait()) + serverChannel.pipeline.fireChannelRead(NIOAny(childChannel1)) + + // check that the server channel and channel are active before initiating the shutdown + XCTAssertTrue(serverChannel.isActive) + XCTAssertTrue(childChannel1.isActive) + + XCTAssertEqual(eventCounterHandler.userInboundEventTriggeredCalls, 0) + + // now close the first child channel + childChannel1.close(promise: nil) + el.run() + + // check that the server is active and child is not + XCTAssertTrue(serverChannel.isActive) + XCTAssertFalse(childChannel1.isActive) + + quiesce.initiateShutdown(promise: allShutdownPromise) + el.run() + + // check that the server channel is closed as the first thing + XCTAssertFalse(serverChannel.isActive) + + el.run() + + // check that the shutdown has completed + XCTAssertNoThrow(try allShutdownPromise.futureResult.wait()) + } + + func testChannelAdded_whenShuttingDown() throws { + let el = EmbeddedEventLoop() + let allShutdownPromise: EventLoopPromise = el.makePromise() + let serverChannel = EmbeddedChannel(handler: nil, loop: el) + // let's activate the server channel, nothing actually happens as this is an EmbeddedChannel + XCTAssertNoThrow(try serverChannel.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 1)).wait()) + let quiesce = ServerQuiescingHelper(group: el) + let collectionHandler = quiesce.makeServerChannelHandler(channel: serverChannel) + XCTAssertNoThrow(try serverChannel.pipeline.addHandler(collectionHandler).wait()) + + // let's add one channel + let waitForPromise1: EventLoopPromise = el.makePromise() + let childChannel1 = EmbeddedChannel(handler: WaitForQuiesceUserEvent(promise: waitForPromise1), loop: el) + // activate the child channel + XCTAssertNoThrow(try childChannel1.connect(to: .init(ipAddress: "1.2.3.4", port: 1)).wait()) + serverChannel.pipeline.fireChannelRead(NIOAny(childChannel1)) + + el.run() + + // check that the server and channel are running + XCTAssertTrue(serverChannel.isActive) + XCTAssertTrue(childChannel1.isActive) + + // let's shut down + quiesce.initiateShutdown(promise: allShutdownPromise) + + // let's add one more channel + let waitForPromise2: EventLoopPromise = el.makePromise() + let childChannel2 = EmbeddedChannel(handler: WaitForQuiesceUserEvent(promise: waitForPromise2), loop: el) + // activate the child channel + XCTAssertNoThrow(try childChannel2.connect(to: .init(ipAddress: "1.2.3.4", port: 2)).wait()) + serverChannel.pipeline.fireChannelRead(NIOAny(childChannel2)) + el.run() + + // Check that we got all quiescing events + XCTAssertNoThrow(try waitForPromise1.futureResult.wait() as Void) + XCTAssertNoThrow(try waitForPromise2.futureResult.wait() as Void) + + // check that the server is closed and the children are running + XCTAssertFalse(serverChannel.isActive) + XCTAssertTrue(childChannel1.isActive) + XCTAssertTrue(childChannel2.isActive) + + // let's close the children + childChannel1.close(promise: nil) + childChannel2.close(promise: nil) + el.run() + + // check that everything is closed + XCTAssertFalse(serverChannel.isActive) + XCTAssertFalse(childChannel1.isActive) + XCTAssertFalse(childChannel2.isActive) + + XCTAssertNoThrow(try allShutdownPromise.futureResult.wait() as Void) + } + + func testChannelAdded_whenShutdown() throws { + let el = EmbeddedEventLoop() + let allShutdownPromise: EventLoopPromise = el.makePromise() + let serverChannel = EmbeddedChannel(handler: nil, loop: el) + // let's activate the server channel, nothing actually happens as this is an EmbeddedChannel + XCTAssertNoThrow(try serverChannel.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 1)).wait()) + let quiesce = ServerQuiescingHelper(group: el) + let collectionHandler = quiesce.makeServerChannelHandler(channel: serverChannel) + XCTAssertNoThrow(try serverChannel.pipeline.addHandler(collectionHandler).wait()) + + // check that the server is running + XCTAssertTrue(serverChannel.isActive) + + // let's shut down + quiesce.initiateShutdown(promise: allShutdownPromise) + + // check that the shutdown has completed + XCTAssertNoThrow(try allShutdownPromise.futureResult.wait()) + + // let's add one channel + let childChannel1 = EmbeddedChannel(loop: el) + // activate the child channel + XCTAssertNoThrow(try childChannel1.connect(to: .init(ipAddress: "1.2.3.4", port: 1)).wait()) + serverChannel.pipeline.fireChannelRead(NIOAny(childChannel1)) + + el.run() + } } diff --git a/Tests/NIOExtrasTests/SynchronizedFileSinkTests+XCTest.swift b/Tests/NIOExtrasTests/SynchronizedFileSinkTests+XCTest.swift new file mode 100644 index 00000000..3ed6e8fb --- /dev/null +++ b/Tests/NIOExtrasTests/SynchronizedFileSinkTests+XCTest.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2018-2023 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 +// +//===----------------------------------------------------------------------===// +// +// SynchronizedFileSinkTests+XCTest.swift +// +import XCTest + +/// +/// NOTE: This file was generated by generate_linux_tests.rb +/// +/// Do NOT edit this file directly as it will be regenerated automatically when needed. +/// + +extension SynchronizedFileSinkTests { + + @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") + static var allTests : [(String, (SynchronizedFileSinkTests) -> () throws -> Void)] { + return [ + ("testSimpleFileSink", testSimpleFileSink), + ("testSimpleFileSinkAsyncShutdown", testSimpleFileSinkAsyncShutdown), + ] + } +} + diff --git a/Tests/NIOExtrasTests/SynchronizedFileSinkTests.swift b/Tests/NIOExtrasTests/SynchronizedFileSinkTests.swift new file mode 100644 index 00000000..d05210cb --- /dev/null +++ b/Tests/NIOExtrasTests/SynchronizedFileSinkTests.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2023 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 +// +//===----------------------------------------------------------------------===// + +import Foundation +import XCTest + +import NIOCore +import NIOEmbedded +@testable import NIOExtras + +final class SynchronizedFileSinkTests: XCTestCase { + func testSimpleFileSink() throws { + try withTemporaryFile { file, path in + let sink = try NIOWritePCAPHandler.SynchronizedFileSink.fileSinkWritingToFile(path: path, errorHandler: { XCTFail("Caught error \($0)") }) + + sink.write(buffer: ByteBuffer(string: "Hello, ")) + sink.write(buffer: ByteBuffer(string: "world!")) + try sink.syncClose() + + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + XCTAssertEqual(data, Data(NIOWritePCAPHandler.pcapFileHeader.readableBytesView) + Data("Hello, world!".utf8)) + } + } + + func testSimpleFileSinkAsyncShutdown() throws { + guard #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) else { return } + XCTAsyncTest { + try await withTemporaryFile { file, path in + let sink = try NIOWritePCAPHandler.SynchronizedFileSink.fileSinkWritingToFile(path: path, errorHandler: { XCTFail("Caught error \($0)") }) + + sink.write(buffer: ByteBuffer(string: "Hello, ")) + sink.write(buffer: ByteBuffer(string: "world!")) + try await sink.close() + + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + XCTAssertEqual(data, Data(NIOWritePCAPHandler.pcapFileHeader.readableBytesView) + Data("Hello, world!".utf8)) + } + } + } +} + +fileprivate func withTemporaryFile(content: String? = nil, _ body: (NIOCore.NIOFileHandle, String) throws -> T) throws -> T { + let temporaryFilePath = "\(temporaryDirectory)/nio_extras_\(UUID())" + FileManager.default.createFile(atPath: temporaryFilePath, contents: content?.data(using: .utf8)) + defer { + XCTAssertNoThrow(try FileManager.default.removeItem(atPath: temporaryFilePath)) + } + + let fileHandle = try NIOFileHandle(path: temporaryFilePath, mode: [.read, .write]) + defer { + XCTAssertNoThrow(try fileHandle.close()) + } + + return try body(fileHandle, temporaryFilePath) +} + +fileprivate func withTemporaryFile(content: String? = nil, _ body: (NIOCore.NIOFileHandle, String) async throws -> T) async throws -> T { + let temporaryFilePath = "\(temporaryDirectory)/nio_extras_\(UUID())" + FileManager.default.createFile(atPath: temporaryFilePath, contents: content?.data(using: .utf8)) + defer { + XCTAssertNoThrow(try FileManager.default.removeItem(atPath: temporaryFilePath)) + } + + let fileHandle = try NIOFileHandle(path: temporaryFilePath, mode: [.read, .write]) + defer { + XCTAssertNoThrow(try fileHandle.close()) + } + + return try await body(fileHandle, temporaryFilePath) +} + +fileprivate var temporaryDirectory: String { +#if os(Linux) + return "/tmp" +#else + if #available(macOS 10.12, iOS 10, tvOS 10, watchOS 3, *) { + return FileManager.default.temporaryDirectory.path + } else { + return "/tmp" + } +#endif // os +} + +extension XCTestCase { + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + /// Cross-platform XCTest support for async-await tests. + /// + /// Currently the Linux implementation of XCTest doesn't have async-await support. + /// Until it does, we make use of this shim which uses a detached `Task` along with + /// `XCTest.wait(for:timeout:)` to wrap the operation. + /// + /// - NOTE: Support for Linux is tracked by https://bugs.swift.org/browse/SR-14403. + /// - NOTE: Implementation currently in progress: https://github.com/apple/swift-corelibs-xctest/pull/326 + func XCTAsyncTest( + expectationDescription: String = "Async operation", + timeout: TimeInterval = 30, + file: StaticString = #filePath, + line: UInt = #line, + function: StaticString = #function, + operation: @escaping @Sendable () async throws -> Void + ) { + let expectation = self.expectation(description: expectationDescription) + Task { + do { + try await operation() + } catch { + XCTFail("Error thrown while executing \(function): \(error)", file: file, line: line) + Thread.callStackSymbols.forEach { print($0) } + } + expectation.fulfill() + } + self.wait(for: [expectation], timeout: timeout) + } +} diff --git a/Tests/NIONFS3Tests/NFS3FileSystemTests+XCTest.swift b/Tests/NIONFS3Tests/NFS3FileSystemTests+XCTest.swift new file mode 100644 index 00000000..a45d480d --- /dev/null +++ b/Tests/NIONFS3Tests/NFS3FileSystemTests+XCTest.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2018-2022 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 +// +//===----------------------------------------------------------------------===// +// +// NFS3FileSystemTests+XCTest.swift +// +import XCTest + +/// +/// NOTE: This file was generated by generate_linux_tests.rb +/// +/// Do NOT edit this file directly as it will be regenerated automatically when needed. +/// + +extension NFS3FileSystemTests { + + @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") + static var allTests : [(String, (NFS3FileSystemTests) -> () throws -> Void)] { + return [ + ("testReadDirDefaultImplementation", testReadDirDefaultImplementation), + ] + } +} + diff --git a/Tests/NIONFS3Tests/NFS3FileSystemTests.swift b/Tests/NIONFS3Tests/NFS3FileSystemTests.swift new file mode 100644 index 00000000..2f95200a --- /dev/null +++ b/Tests/NIONFS3Tests/NFS3FileSystemTests.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOEmbedded +import NIONFS3 +import XCTest + +final class NFS3FileSystemTests: XCTestCase { + func testReadDirDefaultImplementation() throws { + class MyOnlyReadDirPlusFS: NFS3FileSystemNoAuth { + func shutdown(promise: EventLoopPromise) { + promise.succeed(()) + } + + func readdirplus(_ call: NFS3CallReadDirPlus, promise: EventLoopPromise) { + promise.succeed(.init(result: .okay(.init(cookieVerifier: .init(rawValue: 11111), + entries: [.init(fileID: .init(rawValue: 22222), + fileName: "file", + cookie: .init(rawValue: 33333))], + eof: true)))) + } + + func mount(_ call: MountCallMount, promise: EventLoopPromise) { + fatalError("shouldn't be called") + } + + func unmount(_ call: MountCallUnmount, promise: EventLoopPromise) { + fatalError("shouldn't be called") + } + + func getattr(_ call: NFS3CallGetAttr, promise: EventLoopPromise) { + fatalError("shouldn't be called") + } + + func fsinfo(_ call: NFS3CallFSInfo, promise: EventLoopPromise) { + fatalError("shouldn't be called") + } + + func pathconf(_ call: NFS3CallPathConf, promise: EventLoopPromise) { + fatalError("shouldn't be called") + } + + func fsstat(_ call: NFS3CallFSStat, promise: EventLoopPromise) { + fatalError("shouldn't be called") + } + + func access(_ call: NFS3CallAccess, promise: EventLoopPromise) { + fatalError("shouldn't be called") + } + + func lookup(_ call: NFS3CallLookup, promise: EventLoopPromise) { + fatalError("shouldn't be called") + } + + func read(_ call: NFS3CallRead, promise: EventLoopPromise) { + fatalError("shouldn't be called") + } + + func readlink(_ call: NFS3CallReadlink, promise: EventLoopPromise) { + fatalError("shouldn't be called") + } + + func setattr(_ call: NFS3CallSetattr, promise: EventLoopPromise) { + fatalError("shouldn't be called") + } + } + + let eventLoop = EmbeddedEventLoop() + defer { + XCTAssertNoThrow(try eventLoop.syncShutdownGracefully()) + } + let fs = MyOnlyReadDirPlusFS() + let promise = eventLoop.makePromise(of: NFS3ReplyReadDir.self) + fs.readdir(.init(fileHandle: .init(123), + cookie: .init(rawValue: 234), + cookieVerifier: .init(rawValue: 345), + maxResultByteCount: .init(rawValue: 456)), + promise: promise) + let actualResult = try promise.futureResult.wait() + let expectedResult = NFS3ReplyReadDir(result: .okay(.init(cookieVerifier: .init(rawValue: 11111), + entries: [.init(fileID: .init(rawValue: 22222), + fileName: "file", + cookie: .init(rawValue: 33333))], + eof: true))) + XCTAssertEqual(expectedResult, actualResult) + } +} diff --git a/Tests/NIONFS3Tests/NFS3ReplyEncoderTest+XCTest.swift b/Tests/NIONFS3Tests/NFS3ReplyEncoderTest+XCTest.swift new file mode 100644 index 00000000..f2c5309d --- /dev/null +++ b/Tests/NIONFS3Tests/NFS3ReplyEncoderTest+XCTest.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2018-2022 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 +// +//===----------------------------------------------------------------------===// +// +// NFS3ReplyEncoderTest+XCTest.swift +// +import XCTest + +/// +/// NOTE: This file was generated by generate_linux_tests.rb +/// +/// Do NOT edit this file directly as it will be regenerated automatically when needed. +/// + +extension NFS3ReplyEncoderTest { + + @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") + static var allTests : [(String, (NFS3ReplyEncoderTest) -> () throws -> Void)] { + return [ + ("testPartialReadEncoding", testPartialReadEncoding), + ("testFullReadEncodingParses", testFullReadEncodingParses), + ] + } +} + diff --git a/Tests/NIONFS3Tests/NFS3ReplyEncoderTest.swift b/Tests/NIONFS3Tests/NFS3ReplyEncoderTest.swift new file mode 100644 index 00000000..ed9c12c2 --- /dev/null +++ b/Tests/NIONFS3Tests/NFS3ReplyEncoderTest.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +import NIONFS3 +import NIOCore + +final class NFS3ReplyEncoderTest: XCTestCase { + func testPartialReadEncoding() { + for payloadLength in 0..<100 { + let expectedPayload = ByteBuffer(repeating: UInt8(ascii: "j"), count: payloadLength) + let expectedFillBytes = (4 - (payloadLength % 4)) % 4 + + let reply = RPCNFS3Reply(rpcReply: RPCReply(xid: 12345, + status: .messageAccepted(.init(verifier: .init(flavor: .noAuth, + opaque: nil), + status: .success))), + nfsReply: .read(.init(result: .okay(.init(attributes: nil, + count: .init(rawValue: 7), + eof: false, + data: expectedPayload))))) + + var partialSerialisation = ByteBuffer() + let (bytesWritten, nextStep) = partialSerialisation.writeRPCNFS3ReplyPartially(reply) + XCTAssertEqual(partialSerialisation.readableBytes, bytesWritten) + switch nextStep { + case .doNothing: + XCTFail("we need to write more bytes here") + case .writeBlob(let actualPayload, numberOfFillBytes: let fillBytes): + XCTAssertEqual(expectedPayload, actualPayload) + XCTAssertEqual(expectedFillBytes, fillBytes) + } + + var fullSerialisation = ByteBuffer() + let bytesWruttenFull = fullSerialisation.writeRPCNFS3Reply(reply) + XCTAssertEqual(bytesWruttenFull, fullSerialisation.readableBytes) + + XCTAssert(fullSerialisation.readableBytesView.starts(with: partialSerialisation.readableBytesView)) + XCTAssert(fullSerialisation.readableBytesView + .dropFirst(partialSerialisation.readableBytes) + .prefix(expectedPayload.readableBytes) + .elementsEqual(expectedPayload.readableBytesView)) + + XCTAssertEqual(partialSerialisation.readableBytes + payloadLength + expectedFillBytes, + fullSerialisation.readableBytes) + XCTAssertEqual(UInt32(payloadLength), + partialSerialisation.getInteger(at: partialSerialisation.writerIndex - 4, + as: UInt32.self)) + } + } + + func testFullReadEncodingParses() { + for payloadLength in 0..<1 { + let expectedPayload = ByteBuffer(repeating: UInt8(ascii: "j"), count: payloadLength) + + let expectedReply = RPCNFS3Reply(rpcReply: RPCReply(xid: 12345, + status: .messageAccepted(.init(verifier: .init(flavor: .noAuth, + opaque: nil), + status: .success))), + nfsReply: .read(.init(result: .okay(.init(attributes: nil, + count: .init(rawValue: 7), + eof: false, + data: expectedPayload))))) + + var fullSerialisation = ByteBuffer() + let bytesWrittenFull = fullSerialisation.writeRPCNFS3Reply(expectedReply) + XCTAssertEqual(bytesWrittenFull, fullSerialisation.readableBytes) + guard var actualReply = try? fullSerialisation.readRPCMessage() else { + XCTFail("could not read RPC message") + return + } + XCTAssertEqual(0, fullSerialisation.readableBytes) + var actualNFS3Reply: NFS3ReplyRead? = nil + XCTAssertNoThrow(actualNFS3Reply = try actualReply.1.readNFS3ReplyRead()) + XCTAssertEqual(0, actualReply.1.readableBytes) + XCTAssertEqual(expectedReply.nfsReply, + actualNFS3Reply.map { NFS3Reply.read($0) }, + "parsing failed for payload length \(payloadLength)") + } + } +} diff --git a/Tests/NIONFS3Tests/NFS3RoundtripTests+XCTest.swift b/Tests/NIONFS3Tests/NFS3RoundtripTests+XCTest.swift new file mode 100644 index 00000000..8dff52ce --- /dev/null +++ b/Tests/NIONFS3Tests/NFS3RoundtripTests+XCTest.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2018-2022 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 +// +//===----------------------------------------------------------------------===// +// +// NFS3RoundtripTests+XCTest.swift +// +import XCTest + +/// +/// NOTE: This file was generated by generate_linux_tests.rb +/// +/// Do NOT edit this file directly as it will be regenerated automatically when needed. +/// + +extension NFS3RoundtripTests { + + @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") + static var allTests : [(String, (NFS3RoundtripTests) -> () throws -> Void)] { + return [ + ("testRegularCallsRoundtrip", testRegularCallsRoundtrip), + ("testCallsWithMaxIntegersRoundtrip", testCallsWithMaxIntegersRoundtrip), + ("testRegularOkayRepliesRoundtrip", testRegularOkayRepliesRoundtrip), + ] + } +} + diff --git a/Tests/NIONFS3Tests/NFS3RoundtripTests.swift b/Tests/NIONFS3Tests/NFS3RoundtripTests.swift new file mode 100644 index 00000000..e78e9fa6 --- /dev/null +++ b/Tests/NIONFS3Tests/NFS3RoundtripTests.swift @@ -0,0 +1,266 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOTestUtils +import XCTest +import NIONFS3 + +final class NFS3RoundtripTests: XCTestCase { + func testRegularCallsRoundtrip() { + let mountCallNull = NFS3Call.mountNull(.init()) + let mountCall1 = NFS3Call.mount(MountCallMount(dirPath: "/hellö/this is/a cOmplicatedPath⚠️")) + let mountCall2 = NFS3Call.mount(MountCallMount(dirPath: "")) + let unmountCall1 = NFS3Call.unmount(MountCallUnmount(dirPath: "/hellö/this is/a cOmplicatedPath⚠️")) + let accessCall1 = NFS3Call.access(NFS3CallAccess(object: NFS3FileHandle(#line), access: .all)) + let fsInfoCall1 = NFS3Call.fsinfo(.init(fsroot: NFS3FileHandle(#line))) + let fsStatCall1 = NFS3Call.fsstat(.init(fsroot: NFS3FileHandle(#line))) + let getattrCall1 = NFS3Call.getattr(.init(fileHandle: NFS3FileHandle(#line))) + let lookupCall1 = NFS3Call.lookup(.init(dir: NFS3FileHandle(#line), name: "⚠️")) + let nullCall1 = NFS3Call.null(.init()) + let pathConfCall1 = NFS3Call.pathconf(.init(object: NFS3FileHandle(#line))) + let readCall1 = NFS3Call.read(.init(fileHandle: NFS3FileHandle(#line), offset: 123, count: 456)) + let readDirPlusCall1 = NFS3Call.readdirplus(.init(fileHandle: NFS3FileHandle(#line), cookie: 345, + cookieVerifier: 879, dirCount: 23488, maxCount: 2342888)) + let readDirCall1 = NFS3Call.readdir(.init(fileHandle: NFS3FileHandle(#line), cookie: 345, cookieVerifier: 879, maxResultByteCount: 234797)) + let readlinkCall1 = NFS3Call.readlink(.init(symlink: NFS3FileHandle(#line))) + let setattrCall1 = NFS3Call.setattr(.init(object: NFS3FileHandle(#line), + newAttributes: .init(mode: 0o146, + uid: 1, gid: 2, + size: 3, + atime: .init(seconds: 4, nanoseconds: 5), + mtime: .init(seconds: 6, nanoseconds: 7)), + guard: .init(seconds: 8, nanoseconds: 0))) + + var xid: UInt32 = 0 + func makeInputOutputPair(_ nfsCall: NFS3Call) -> (ByteBuffer, [RPCNFS3Call]) { + var buffer = ByteBuffer() + xid += 1 + let rpcNFS3Call = RPCNFS3Call(nfsCall: nfsCall, xid: xid) + let oldReadableBytes = buffer.readableBytes + let bytesWritten = buffer.writeRPCNFS3Call(rpcNFS3Call) + XCTAssertEqual(oldReadableBytes + bytesWritten, buffer.readableBytes) + + return (buffer, [rpcNFS3Call]) + } + + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder(inputOutputPairs: [ + makeInputOutputPair(mountCall1), + makeInputOutputPair(mountCall2), + makeInputOutputPair(unmountCall1), + makeInputOutputPair(accessCall1), + makeInputOutputPair(fsInfoCall1), + makeInputOutputPair(fsStatCall1), + makeInputOutputPair(getattrCall1), + makeInputOutputPair(lookupCall1), + makeInputOutputPair(nullCall1), + makeInputOutputPair(mountCallNull), + makeInputOutputPair(pathConfCall1), + makeInputOutputPair(readCall1), + makeInputOutputPair(readDirCall1), + makeInputOutputPair(readDirPlusCall1), + makeInputOutputPair(readlinkCall1), + makeInputOutputPair(setattrCall1), + ], + decoderFactory: { NFS3CallDecoder() })) + } + + func testCallsWithMaxIntegersRoundtrip() { + let accessCall1 = NFS3Call.access(NFS3CallAccess(object: NFS3FileHandle(.max), + access: NFS3Access(rawValue: .max))) + let fsInfoCall1 = NFS3Call.fsinfo(.init(fsroot: NFS3FileHandle(.max))) + let fsStatCall1 = NFS3Call.fsstat(.init(fsroot: NFS3FileHandle(.max))) + let getattrCall1 = NFS3Call.getattr(.init(fileHandle: NFS3FileHandle(.max))) + let lookupCall1 = NFS3Call.lookup(.init(dir: NFS3FileHandle(.max), name: "⚠️")) + let pathConfCall1 = NFS3Call.pathconf(.init(object: NFS3FileHandle(.max))) + let readCall1 = NFS3Call.read(.init(fileHandle: NFS3FileHandle(.max), + offset: .init(rawValue: .max), count: .init(rawValue: .max))) + let readDirPlusCall1 = NFS3Call.readdirplus(.init(fileHandle: NFS3FileHandle(.max), + cookie: .init(rawValue: .max), + cookieVerifier: .init(rawValue: .max), + dirCount: .init(rawValue: .max), + maxCount: .init(rawValue: .max))) + let readDirCall1 = NFS3Call.readdir(.init(fileHandle: NFS3FileHandle(.max), + cookie: .init(rawValue: .max), + cookieVerifier: .init(rawValue: .max), + maxResultByteCount: .init(rawValue: .max))) + let readlinkCall1 = NFS3Call.readlink(.init(symlink: NFS3FileHandle(.max))) + let setattrCall1 = NFS3Call.setattr(.init(object: NFS3FileHandle(.max), + newAttributes: .init(mode: .init(rawValue: .max), + uid: .init(rawValue: .max), + gid: .init(rawValue: .max), + size: .init(rawValue: .max), + atime: .init(seconds: .max, nanoseconds: .max), + mtime: .init(seconds: .max, nanoseconds: .max)), + guard: .init(seconds: .max, nanoseconds: .max))) + + var xid: UInt32 = 0 + func makeInputOutputPair(_ nfsCall: NFS3Call) -> (ByteBuffer, [RPCNFS3Call]) { + var buffer = ByteBuffer() + xid += 1 + let rpcNFS3Call = RPCNFS3Call(nfsCall: nfsCall, xid: xid) + let oldReadableBytes = buffer.readableBytes + let bytesWritten = buffer.writeRPCNFS3Call(rpcNFS3Call) + XCTAssertEqual(oldReadableBytes + bytesWritten, buffer.readableBytes) + + return (buffer, [rpcNFS3Call]) + } + + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder(inputOutputPairs: [ + makeInputOutputPair(accessCall1), + makeInputOutputPair(fsInfoCall1), + makeInputOutputPair(fsStatCall1), + makeInputOutputPair(getattrCall1), + makeInputOutputPair(lookupCall1), + makeInputOutputPair(pathConfCall1), + makeInputOutputPair(readCall1), + makeInputOutputPair(readDirPlusCall1), + makeInputOutputPair(readDirCall1), + makeInputOutputPair(readlinkCall1), + makeInputOutputPair(setattrCall1), + ], + decoderFactory: { NFS3CallDecoder() })) + } + + func testRegularOkayRepliesRoundtrip() { + func makeRandomFileAttr() -> NFS3FileAttr { + return .init(type: .init(rawValue: .random(in: 1 ... 7))!, + mode: .init(rawValue: .random(in: 0o000 ... 0o777)), + nlink: .random(in: .min ... .max), + uid: .init(rawValue: .random(in: .min ... .max)), + gid: .init(rawValue: .random(in: .min ... .max)), + size: .init(rawValue: .random(in: .min ... .max)), + used: .init(rawValue: .random(in: .min ... .max)), + rdev: .init(rawValue: .random(in: .min ... .max)), + fsid: .random(in: .min ... .max), + fileid: .init(rawValue: .random(in: .min ... .max)), + atime: .init(seconds: .random(in: .min ... .max), + nanoseconds: .random(in: .min ... .max)), + mtime: .init(seconds: .random(in: .min ... .max), + nanoseconds: .random(in: .min ... .max)), + ctime: .init(seconds: .random(in: .min ... .max), + nanoseconds: .random(in: .min ... .max))) + } + let mountNullReply1 = NFS3Reply.mountNull + let mountReply1 = NFS3Reply.mount(MountReplyMount(result: .okay(.init(fileHandle: NFS3FileHandle(#line))))) + let mountReply2 = NFS3Reply.mount(.init(result: .okay(.init(fileHandle: NFS3FileHandle(#line))))) + let unmountReply1 = NFS3Reply.unmount(.init()) + let accessReply1 = NFS3Reply.access(.init(result: .okay(.init(dirAttributes: makeRandomFileAttr(), access: .allReadOnly)))) + let fsInfoReply1 = NFS3Reply.fsinfo(.init(result: + .okay(.init(attributes: makeRandomFileAttr(), + rtmax: .random(in: .min ... .max), + rtpref: .random(in: .min ... .max), + rtmult: .random(in: .min ... .max), + wtmax: .random(in: .min ... .max), + wtpref: .random(in: .min ... .max), + wtmult: .random(in: .min ... .max), + dtpref: .random(in: .min ... .max), + maxFileSize: .init(rawValue: .random(in: .min ... .max)), + timeDelta: .init(seconds: .random(in: .min ... .max), + nanoseconds: .random(in: .min ... .max)), + properties: .init(rawValue: .random(in: .min ... .max)))))) + let fsStatReply1 = NFS3Reply.fsstat(.init(result: + .okay(.init(attributes: makeRandomFileAttr(), + tbytes: .init(rawValue: .random(in: .min ... .max)), + fbytes: .init(rawValue: .random(in: .min ... .max)), + abytes: .init(rawValue: .random(in: .min ... .max)), + tfiles: .init(rawValue: .random(in: .min ... .max)), + ffiles: .init(rawValue: .random(in: .min ... .max)), + afiles: .init(rawValue: .random(in: .min ... .max)), + invarsec: .random(in: .min ... .max))))) + let getattrReply1 = NFS3Reply.getattr(.init(result: .okay(.init(attributes: makeRandomFileAttr())))) + let lookupReply1 = NFS3Reply.lookup(.init(result: + .okay(.init(fileHandle: NFS3FileHandle(.random(in: .min ... .max)), + attributes: makeRandomFileAttr(), + dirAttributes: makeRandomFileAttr())))) + let nullReply1 = NFS3Reply.null + let pathConfReply1 = NFS3Reply.pathconf(.init(result: .okay(.init(attributes: makeRandomFileAttr(), + linkMax: .random(in: .min ... .max), + nameMax: .random(in: .min ... .max), + noTrunc: .random(), + chownRestricted: .random(), + caseInsensitive: .random(), + casePreserving: .random())))) + let readReply1 = NFS3Reply.read(.init(result: .okay(.init(attributes: makeRandomFileAttr(), + count: .init(rawValue: .random(in: .min ... .max)), + eof: .random(), + data: ByteBuffer(string: "abc"))))) + let readDirPlusReply1 = NFS3Reply.readdirplus(.init(result: + .okay(.init(dirAttributes: makeRandomFileAttr(), + cookieVerifier: .init(rawValue: .random(in: .min ... .max)), + entries: [.init(fileID: .init(rawValue: .random(in: .min ... .max)), + fileName: "asd", + cookie: .init(rawValue: .random(in: .min ... .max)), + nameAttributes: makeRandomFileAttr(), + nameHandle: NFS3FileHandle(.random(in: .min ... .max)))], + eof: .random())))) + let readDirReply1 = NFS3Reply.readdir(.init(result: + .okay(.init(dirAttributes: makeRandomFileAttr(), + cookieVerifier: .init(rawValue: .random(in: .min ... .max)), + entries: [ + .init(fileID: .init(rawValue: .random(in: .min ... .max)), + fileName: "asd", + cookie: .init(rawValue: .random(in: .min ... .max)))], + eof: .random())))) + let readlinkReply1 = NFS3Reply.readlink(.init(result: .okay(.init(symlinkAttributes: makeRandomFileAttr(), + target: "he")))) + let setattrReply1 = NFS3Reply.setattr(.init(result: + .okay(.init(wcc: .init(before: .some(.init(size: .init(rawValue: .random(in: .min ... .max)), + mtime: .init(seconds: .random(in: .min ... .max), + nanoseconds: .random(in: .min ... .max)), + ctime: .init(seconds: .random(in: .min ... .max), + nanoseconds: .random(in: .min ... .max)))), + after: makeRandomFileAttr()))))) + + var xid: UInt32 = 0 + var prepopulatedProcs: [UInt32: RPCNFS3ProcedureID] = [:] + func makeInputOutputPair(_ nfsReply: NFS3Reply) -> (ByteBuffer, [RPCNFS3Reply]) { + var buffer = ByteBuffer() + xid += 1 + let rpcNFS3Reply = RPCNFS3Reply(rpcReply: + .init(xid: xid, + status: .messageAccepted(.init(verifier: .init(flavor: .noAuth, opaque: nil), + status: .success))), + nfsReply: nfsReply) + prepopulatedProcs[xid] = .init(nfsReply) + let oldReadableBytes = buffer.readableBytes + let writtenBytes = buffer.writeRPCNFS3Reply(rpcNFS3Reply) + XCTAssertEqual(oldReadableBytes + writtenBytes, buffer.readableBytes) + + return (buffer, [rpcNFS3Reply]) + } + + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [ + makeInputOutputPair(mountNullReply1), + makeInputOutputPair(mountReply1), + makeInputOutputPair(mountReply2), + makeInputOutputPair(unmountReply1), + makeInputOutputPair(accessReply1), + makeInputOutputPair(fsInfoReply1), + makeInputOutputPair(fsStatReply1), + makeInputOutputPair(getattrReply1), + makeInputOutputPair(lookupReply1), + makeInputOutputPair(nullReply1), + makeInputOutputPair(pathConfReply1), + makeInputOutputPair(readReply1), + makeInputOutputPair(readDirPlusReply1), + makeInputOutputPair(readDirReply1), + makeInputOutputPair(readlinkReply1), + makeInputOutputPair(setattrReply1), + ], + decoderFactory: { NFS3ReplyDecoder(prepopulatedProcecedures: prepopulatedProcs, + allowDuplicateReplies: true) })) + } +} diff --git a/docker/Dockerfile b/docker/Dockerfile index 62c76d82..2d3e61c7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -17,19 +17,9 @@ RUN apt-get update && apt-get install -y wget RUN apt-get update && apt-get install -y lsof dnsutils netcat-openbsd net-tools curl jq # used by integration tests RUN apt-get update && apt-get install -y zlib1g-dev -# ruby and jazzy for docs generation +# ruby for soundness RUN apt-get update && apt-get install -y ruby ruby-dev libsqlite3-dev build-essential -# jazzy no longer works on xenial as ruby is too old. -RUN if [ "${ubuntu_version}" = "focal" ] ; then echo "gem: --no-document" > ~/.gemrc ; fi -RUN if [ "${ubuntu_version}" = "focal" ] ; then gem install jazzy ; fi # tools RUN mkdir -p $HOME/.tools RUN echo 'export PATH="$HOME/.tools:$PATH"' >> $HOME/.profile - -# swiftformat (until part of the toolchain) - -ARG swiftformat_version=0.40.12 -RUN git clone --branch $swiftformat_version --depth 1 https://github.com/nicklockwood/SwiftFormat $HOME/.tools/swift-format -RUN cd $HOME/.tools/swift-format && swift build -c release -RUN ln -s $HOME/.tools/swift-format/.build/release/swiftformat $HOME/.tools/swiftformat diff --git a/docker/docker-compose.2004.main.yaml b/docker/docker-compose.2204.58.yaml similarity index 50% rename from docker/docker-compose.2004.main.yaml rename to docker/docker-compose.2204.58.yaml index 6e9fe8c6..a7804722 100644 --- a/docker/docker-compose.2004.main.yaml +++ b/docker/docker-compose.2204.58.yaml @@ -3,15 +3,15 @@ version: "3" services: runtime-setup: - image: swift-nio-extras:20.04-main + image: swift-nio-extras:22.04-5.8 build: args: - base_image: "swiftlang/swift:nightly-main-focal" + base_image: "swiftlang/swift:nightly-main-jammy" test: - image: swift-nio-extras:20.04-main + image: swift-nio-extras:22.04-5.8 environment: - IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error shell: - image: swift-nio-extras:20.04-main + image: swift-nio-extras:22.04-5.8 diff --git a/docker/docker-compose.2204.main.yaml b/docker/docker-compose.2204.main.yaml new file mode 100644 index 00000000..ee845e1c --- /dev/null +++ b/docker/docker-compose.2204.main.yaml @@ -0,0 +1,17 @@ +version: "3" + +services: + + runtime-setup: + image: swift-nio-extras:22.04-main + build: + args: + base_image: "swiftlang/swift:nightly-main-jammy" + + test: + image: swift-nio-extras:22.04-main + environment: + - IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error + + shell: + image: swift-nio-extras:22.04-main diff --git a/scripts/generate_docs.sh b/scripts/generate_docs.sh deleted file mode 100755 index adff6ab0..00000000 --- a/scripts/generate_docs.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftNIO open source project -## -## Copyright (c) 2017-2018 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 -## -##===----------------------------------------------------------------------===## - -set -e - -my_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -root_path="$my_path/.." -version=$(git describe --abbrev=0 --tags || echo "0.0.0") -modules=(NIOExtras NIOHTTPCompression NIOSOCKS) - -if [[ "$(uname -s)" == "Linux" ]]; then - # build code if required - if [[ ! -d "$root_path/.build/x86_64-unknown-linux" ]]; then - swift build - fi - # setup source-kitten if required - mkdir -p "$root_path/.build/sourcekitten" - source_kitten_source_path="$root_path/.build/sourcekitten/source" - if [[ ! -d "$source_kitten_source_path" ]]; then - git clone https://github.com/jpsim/SourceKitten.git "$source_kitten_source_path" - fi - source_kitten_path="$source_kitten_source_path/.build/debug" - if [[ ! -d "$source_kitten_path" ]]; then - rm -rf "$source_kitten_source_path/.swift-version" - cd "$source_kitten_source_path" && swift build && cd "$root_path" - fi - # generate - for module in "${modules[@]}"; do - if [[ ! -f "$root_path/.build/sourcekitten/$module.json" ]]; then - "$source_kitten_path/sourcekitten" doc --spm --module-name $module > "$root_path/.build/sourcekitten/$module.json" - fi - done -fi - -[[ -d docs/$version ]] || mkdir -p docs/$version -[[ -d swift-nio-extras.xcodeproj ]] || swift package generate-xcodeproj - -# run jazzy -if ! command -v jazzy > /dev/null; then - gem install jazzy --no-ri --no-rdoc -fi - -jazzy_dir="$root_path/.build/jazzy" -rm -rf "$jazzy_dir" -mkdir -p "$jazzy_dir" - -module_switcher="$jazzy_dir/README.md" -jazzy_args=(--clean - --author 'swift-nio team' - --readme "$module_switcher" - --author_url https://github.com/apple/swift-nio-extras - --github_url https://github.com/apple/swift-nio-extras - --theme fullwidth - --xcodebuild-arguments -scheme,swift-nio-extras-Package) -cat > "$module_switcher" <<"EOF" -# swift-nio-extras Docs - -swift-nio-extras is a good place for code that is related to NIO but not core. -It can also be used to incubate APIs for tasks that are possible with core-NIO but are cumbersome today. - -swift-nio-extras contains multiple modules: - ---- - -For the API documentation of the other repositories in the SwiftNIO family check: - -- [`swift-nio` API docs](https://apple.github.io/swift-nio) -- [`swift-nio-ssl` API docs](https://apple.github.io/swift-nio-ssl) -- [`swift-nio-http2` API docs](https://apple.github.io/swift-nio-http2) -- [`swift-nio-extras` API docs](https://apple.github.io/swift-nio-extras/docs/current/NIOExtras/index.html) - -EOF - -for module in "${modules[@]}"; do - args=("${jazzy_args[@]}" --output "$jazzy_dir/docs/$version/$module" --docset-path "$jazzy_dir/docset/$version/$module" - --module "$module" --module-version $version - --root-url "https://apple.github.io/swift-nio-extras/docs/$version/$module/") - if [[ -f "$root_path/.build/sourcekitten/$module.json" ]]; then - args+=(--sourcekitten-sourcefile "$root_path/.build/sourcekitten/$module.json") - fi - jazzy "${args[@]}" -done - -# push to github pages -if [[ $PUSH == true ]]; then - BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) - GIT_AUTHOR=$(git --no-pager show -s --format='%an <%ae>' HEAD) - git fetch origin +gh-pages:gh-pages - git checkout gh-pages - rm -rf "docs/$version" - rm -rf "docs/current" - cp -r "$jazzy_dir/docs/$version" docs/ - cp -r "docs/$version" docs/current - git add --all docs - echo '' > index.html - git add index.html - touch .nojekyll - git add .nojekyll - changes=$(git diff-index --name-only HEAD) - if [[ -n "$changes" ]]; then - echo -e "changes detected\n$changes" - git commit --author="$GIT_AUTHOR" -m "publish $version docs" - git push origin gh-pages - else - echo "no changes detected" - fi - git checkout -f $BRANCH_NAME -fi diff --git a/scripts/soundness.sh b/scripts/soundness.sh index 574d3bac..59dc5ea5 100755 --- a/scripts/soundness.sh +++ b/scripts/soundness.sh @@ -18,7 +18,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" function replace_acceptable_years() { # this needs to replace all acceptable forms with 'YEARS' - sed -e 's/20[12][789012]-20[12][789012]/YEARS/' -e 's/20[12][89012]/YEARS/' + sed -e 's/20[12][7890123]-20[12][7890123]/YEARS/' -e 's/20[12][890123]/YEARS/' } printf "=> Checking linux tests... "