diff --git a/Package.swift b/Package.swift index d49419fc..1a7cf381 100644 --- a/Package.swift +++ b/Package.swift @@ -108,7 +108,22 @@ var targets: [PackageDescription.Target] = [ "NIOSOCKS", .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOEmbedded", package: "swift-nio"), - ]) + ]), + .target( + name: "NIONFS3", + dependencies: [ + .product(name: "NIO", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "InstrumentationBaggage", package: "swift-distributed-tracing-baggage"), + ]), + .testTarget( + name: "NIONFS3Tests", + dependencies: [ + "NIONFS3", + .product(name: "NIO", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOTestUtils", package: "swift-nio"), + ]), ] let package = Package( @@ -120,6 +135,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.32.0"), + .package(url: "https://github.com/apple/swift-distributed-tracing-baggage", .upToNextMajor(from: "0.2.1")), ], targets: targets ) diff --git a/Sources/NIONFS3/DummyFS.swift b/Sources/NIONFS3/DummyFS.swift new file mode 100644 index 00000000..07d8a99f --- /dev/null +++ b/Sources/NIONFS3/DummyFS.swift @@ -0,0 +1,265 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 InstrumentationBaggage + +final class DummyFS: NFS3FileSystemNoAuth { + struct ChildEntry { + var name: String + var index: Int + } + + struct InodeEntry { + var type: NFS3FileType + var children: [ChildEntry] + } + + private var files: [InodeEntry] = [] + private var root: Int = 7 + private let fileContent: ByteBuffer = { + var buffer = ByteBuffer(repeating: UInt8(ascii: "A"), count: 1 * 1024 * 1024) + buffer.setInteger(UInt8(ascii: "H"), at: 0) + buffer.setInteger(UInt8(ascii: "L"), at: buffer.writerIndex - 2) + buffer.setInteger(UInt8(ascii: "L"), at: buffer.writerIndex - 3) + buffer.setInteger(UInt8(ascii: "O"), at: buffer.writerIndex - 1) + return buffer + }() + + init() { + // 0 doesn't exist? + self.files.append(.init(type: .regular, children: [])) + + let idDirFileA = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDirFileB = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDirFileC = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDirFileD = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDirFileE = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDirFileF = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDir = self.files.count + self.files.append(.init(type: .directory, + children: [ + .init(name: ".", index: idDir), + .init(name: "file", index: idDirFileA), + .init(name: "file1", index: idDirFileB), + .init(name: "file2", index: idDirFileC), + .init(name: "file3", index: idDirFileD), + .init(name: "file4", index: idDirFileE), + .init(name: "file5", index: idDirFileF), + ])) + + let idRoot = self.files.count + self.files.append(.init(type: .directory, + children: [ + .init(name: ".", index: idRoot), + .init(name: "dir", index: idDir), + ])) + + self.files[idDir].children.append(.init(name: "..", index: idRoot)) + self.files[idRoot].children.append(.init(name: "..", index: idRoot)) + + self.root = idRoot + } + + func mount(_ call: NFS3CallMount, baggage: Baggage, promise: EventLoopPromise) { + promise.succeed(.init(result: .okay(.init(fileHandle: NFS3FileHandle(UInt64(self.root)))))) + } + + func unmount(_ call: NFS3CallUnmount, baggage: Baggage, promise: EventLoopPromise) { + promise.succeed(.init()) + } + + func getattr(_ call: NFS3CallGetAttr, baggage: Baggage, promise: EventLoopPromise) { + if let result = self.getFile(call.fileHandle) { + promise.succeed(.init(result: .okay(.init(attributes: result)))) + } else { + promise.succeed(.init(result: .fail(.errorBADHANDLE, NFS3Nothing()))) + } + } + + func lookup(fileName: String, inDirectory dirHandle: NFS3FileHandle) -> (NFS3FileHandle, NFS3FileAttr)? { + guard let dirEntry = self.getEntry(fileHandle: dirHandle) else { + return nil + } + + guard let index = self.files[dirEntry.0].children.first(where: { $0.name == fileName })?.index else { + return nil + } + let fileHandle = NFS3FileHandle(UInt64(index)) + + return (fileHandle, self.getFile(fileHandle)!) + } + + func getEntry(index: Int) -> InodeEntry? { + guard index >= 0 && index < self.files.count else { + return nil + } + return self.files[index] + } + + func getEntry(fileHandle: NFS3FileHandle) -> (Int, InodeEntry)? { + return UInt64(fileHandle).flatMap { + Int(exactly: $0) + }.flatMap { index in + self.getEntry(index: index).map { + (index, $0) + } + } + } + + func getFile(_ fileHandle: NFS3FileHandle) -> NFS3FileAttr? { + guard let entry = self.getEntry(fileHandle: fileHandle) else { + return nil + } + + return .init(type: entry.1.type, + mode: 0o777, + nlink: 1, + uid: 1, + gid: 1, + size: 1 * 1024 * 1024, + used: 1, + rdev: 1, + fsid: 1, + fileid: .init(entry.0), + atime: .init(seconds: 0, nanoSeconds: 0), + mtime: .init(seconds: 0, nanoSeconds: 0), + ctime: .init(seconds: 0, nanoSeconds: 0)) + } + + func fsinfo(_ call: NFS3CallFSInfo, baggage: Baggage, promise: EventLoopPromise) { + promise.succeed(NFS3ReplyFSInfo(result: .okay(.init(attributes: nil, + rtmax: 1_000_000, + rtpref: 128_000, + rtmult: 4096, + wtmax: 1_000_000, + wtpref: 128_000, + wtmult: 4096, + dtpref: 128_000, + maxFileSize: UInt64(Int.max), + timeDelta: NFS3Time(seconds: 0, nanoSeconds: 0), + properties: .default)))) + } + + func pathconf(_ call: NFS3CallPathConf, baggage: Baggage, promise: EventLoopPromise) { + promise.succeed(.init(result: .okay(.init(attributes: nil, + linkMax: 1_000_000, + nameMax: 4096, + noTrunc: false, + chownRestricted: false, + caseInsensitive: false, + casePreserving: true)))) + } + + func fsstat(_ call: NFS3CallFSStat, baggage: Baggage, promise: EventLoopPromise) { + promise.succeed(.init(result: .okay(.init(attributes: nil, + tbytes: 0x10000000000, + fbytes: 0, + abytes: 0, + tfiles: 0x10000000, + ffiles: 0, + afiles: 0, + invarsec: 0)))) + } + + func access(_ call: NFS3CallAccess, baggage: Baggage, promise: EventLoopPromise) { + promise.succeed(.init(result: .okay(.init(dirAttributes: nil, access: .allReadOnly)))) + } + + func lookup(_ call: NFS3CallLookup, baggage: Baggage, promise: EventLoopPromise) { + if let entry = self.lookup(fileName: call.name, inDirectory: call.dir) { + promise.succeed(.init(result: .okay(.init(fileHandle: entry.0, + attributes: entry.1, + dirAttributes: nil)))) + } else { + promise.succeed(.init(result: .fail(.errorNOENT, .init(dirAttributes: nil)))) + + } + } + + func readdirplus(_ call: NFS3CallReadDirPlus, baggage: Baggage, promise: EventLoopPromise) { + if let entry = self.getEntry(fileHandle: call.fileHandle) { + var entries: [NFS3ReplyReadDirPlus.Entry] = [] + for fileIndex in entry.1.children.enumerated().dropFirst(Int(min(UInt64(Int.max), call.cookie))) { + entries.append(.init(fileID: UInt64(fileIndex.element.index), + fileName: fileIndex.element.name, + cookie: NFS3Cookie(fileIndex.offset), + nameAttributes: nil, + nameHandle: nil)) + } + promise.succeed(.init(result: .okay(.init(dirAttributes: nil, + cookieVerifier: call.cookieVerifier, + entries: entries, + eof: true)))) + } else { + promise.succeed(.init(result: .fail(.errorNOENT, .init(dirAttributes: nil)))) + + } + } + + func read(_ call: NFS3CallRead, baggage: Baggage, promise: EventLoopPromise) { + if let file = self.getFile(call.fileHandle) { + if file.type == .regular { + var slice = self.fileContent + guard call.offset <= .init(Int.max) else { + promise.succeed(.init(result: .fail(.errorFBIG, .init(attributes: nil)))) + return + } + let offsetLegal = slice.readSlice(length: Int(call.offset)) != nil + if offsetLegal { + let actualSlice = slice.readSlice(length: min(slice.readableBytes, Int(call.count)))! + let isEOF = slice.readableBytes == 0 + + promise.succeed(.init(result: .okay(.init(attributes: nil, + count: .init(actualSlice.readableBytes), + eof: isEOF, + data: actualSlice)))) + } else { + promise.succeed(.init(result: .okay(.init(attributes: nil, + count: 0, + eof: true, + data: ByteBuffer())))) + } + } else { + promise.succeed(.init(result: .fail(.errorISDIR, .init(attributes: nil)))) + } + } else { + promise.succeed(.init(result: .fail(.errorNOENT, .init(attributes: nil)))) + } + } + + func readlink(_ call: NFS3CallReadlink, baggage: Baggage, promise: EventLoopPromise) { + promise.succeed(.init(result: .fail(.errorNOENT, .init(symlinkAttributes: nil)))) + } + + func setattr(_ call: NFS3CallSetattr, baggage: Baggage, promise: EventLoopPromise) { + promise.succeed(.init(result: .fail(.errorROFS, .init(wcc: .init(before: nil, after: nil))))) + } + + func shutdown(promise: EventLoopPromise) { + promise.succeed(()) + } +} diff --git a/Sources/NIONFS3/NFSCallDecoder.swift b/Sources/NIONFS3/NFSCallDecoder.swift new file mode 100644 index 00000000..12e52047 --- /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-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 + +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.readNFSCall(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..bb82a313 --- /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-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 + +public struct NFS3CallEncoder: MessageToByteEncoder { + public typealias OutboundIn = RPCNFS3Call + + public init() {} + + public func encode(data: RPCNFS3Call, out: inout ByteBuffer) throws { + out.writeRPCNFSCall(data) + } +} diff --git a/Sources/NIONFS3/NFSFileSystem+FuturesAPI.swift b/Sources/NIONFS3/NFSFileSystem+FuturesAPI.swift new file mode 100644 index 00000000..b202184d --- /dev/null +++ b/Sources/NIONFS3/NFSFileSystem+FuturesAPI.swift @@ -0,0 +1,175 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 InstrumentationBaggage + +extension NFS3FileSystemNoAuth { + public func mount(_ call: NFS3CallMount, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyMount.self) + if eventLoop.inEventLoop { + self.mount(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.mount(call, baggage: baggage, promise: promise) + } + } + return promise.futureResult + } + + public func unmount(_ call: NFS3CallUnmount, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyUnmount.self) + if eventLoop.inEventLoop { + self.unmount(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.unmount(call, baggage: baggage, promise: promise) + } + } + return promise.futureResult + } + + public func getattr(_ call: NFS3CallGetAttr, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyGetAttr.self) + if eventLoop.inEventLoop { + self.getattr(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.getattr(call, baggage: baggage, promise: promise) + } + } + return promise.futureResult + } + + public func fsinfo(_ call: NFS3CallFSInfo, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyFSInfo.self) + if eventLoop.inEventLoop { + self.fsinfo(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.fsinfo(call, baggage: baggage, promise: promise) + } + } + return promise.futureResult + } + + public func pathconf(_ call: NFS3CallPathConf, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyPathConf.self) + if eventLoop.inEventLoop { + self.pathconf(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.pathconf(call, baggage: baggage, promise: promise) + } + } + return promise.futureResult + } + + public func fsstat(_ call: NFS3CallFSStat, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyFSStat.self) + if eventLoop.inEventLoop { + self.fsstat(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.fsstat(call, baggage: baggage, promise: promise) + } + } + return promise.futureResult + } + + public func access(_ call: NFS3CallAccess, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyAccess.self) + if eventLoop.inEventLoop { + self.access(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.access(call, baggage: baggage, promise: promise) + } + } + return promise.futureResult + } + + public func lookup(_ call: NFS3CallLookup, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyLookup.self) + if eventLoop.inEventLoop { + self.lookup(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.lookup(call, baggage: baggage, promise: promise) + } + } + return promise.futureResult + } + + public func readdirplus(_ call: NFS3CallReadDirPlus, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyReadDirPlus.self) + if eventLoop.inEventLoop { + self.readdirplus(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.readdirplus(call, baggage: baggage, promise: promise) + } + } + return promise.futureResult + } + + public func read(_ call: NFS3CallRead, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyRead.self) + if eventLoop.inEventLoop { + self.read(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.read(call, baggage: baggage, promise: promise) + } + } + return promise.futureResult + } + + public func readlink(_ call: NFS3CallReadlink, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplyReadlink.self) + if eventLoop.inEventLoop { + self.readlink(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.readlink(call, baggage: baggage, promise: promise) + } + } + return promise.futureResult + } + + public func setattr(_ call: NFS3CallSetattr, baggage: Baggage, eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NFS3ReplySetattr.self) + if eventLoop.inEventLoop { + self.setattr(call, baggage: baggage, promise: promise) + } else { + eventLoop.execute { + self.setattr(call, baggage: baggage, 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 + } + +} diff --git a/Sources/NIONFS3/NFSFileSystem.swift b/Sources/NIONFS3/NFSFileSystem.swift new file mode 100644 index 00000000..83b6a464 --- /dev/null +++ b/Sources/NIONFS3/NFSFileSystem.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 InstrumentationBaggage + +public protocol NFS3FileSystemNoAuth { + func mount(_ call: NFS3CallMount, baggage: Baggage, promise: EventLoopPromise) + func unmount(_ call: NFS3CallUnmount, baggage: Baggage, promise: EventLoopPromise) + func getattr(_ call: NFS3CallGetAttr, baggage: Baggage, promise: EventLoopPromise) + func fsinfo(_ call: NFS3CallFSInfo, baggage: Baggage, promise: EventLoopPromise) + func pathconf(_ call: NFS3CallPathConf, baggage: Baggage, promise: EventLoopPromise) + func fsstat(_ call: NFS3CallFSStat, baggage: Baggage, promise: EventLoopPromise) + func access(_ call: NFS3CallAccess, baggage: Baggage, promise: EventLoopPromise) + func lookup(_ call: NFS3CallLookup, baggage: Baggage, promise: EventLoopPromise) + func readdirplus(_ call: NFS3CallReadDirPlus, baggage: Baggage, promise: EventLoopPromise) + func read(_ call: NFS3CallRead, baggage: Baggage, promise: EventLoopPromise) + func readlink(_ call: NFS3CallReadlink, baggage: Baggage, promise: EventLoopPromise) + func setattr(_ call: NFS3CallSetattr, baggage: Baggage, promise: EventLoopPromise) + + func shutdown(promise: EventLoopPromise) +} diff --git a/Sources/NIONFS3/NFSFileSystemHandler.swift b/Sources/NIONFS3/NFSFileSystemHandler.swift new file mode 100644 index 00000000..658d2523 --- /dev/null +++ b/Sources/NIONFS3/NFSFileSystemHandler.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 InstrumentationBaggage + +/// `ChannelHandler` which implements NFS calls & replies the user implements as a `NFS3FileSystemNoAuth`. +/// +/// `NFS3FileSystemNoAuthHandler` is a 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 + private var baggage: Baggage + + public init(_ fs: FS, baggage: Baggage) { + self.filesystem = fs + self.baggage = baggage + } + + 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 { + context.writeAndFlush(self.wrapOutboundOut(.init(rpcReply: .init(xid: call.rpcCall.xid, + status: self.rpcReplySuccess), + nfsReply: reply)), + promise: nil) + } + } + + func sendError(_ error: Error, call: RPCNFS3Call) { + if let context = self.context { + context.fireErrorCaught(error) + context.writeAndFlush(self.wrapOutboundOut(.init(rpcReply: .init(xid: call.rpcCall.xid, + status: self.rpcReplySuccess), + nfsReply: .mount(.init(result: .fail(.errorSERVERFAULT, + NFS3Nothing()))))), + promise: nil) + } + } + + + public func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let call = self.unwrapInboundIn(data) + self.invoker!.handleNFSCall(call, baggage: self.baggage) + } + + public func errorCaught(context: ChannelHandlerContext, error: Error) { + switch error as? NFS3Error { + case .unknownProgramOrProcedure(.call(let call)): + print("UNKNOWN CALL: \(call)") + context.writeAndFlush(self.wrapOutboundOut(.init(rpcReply: .init(xid: call.xid, + status: .messageAccepted(.init(verifier: .init(flavor: .noAuth, opaque: nil), + status: .procedureUnavailable))), + nfsReply: .null)), 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..dae892d2 --- /dev/null +++ b/Sources/NIONFS3/NFSFileSystemInvoker.swift @@ -0,0 +1,202 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 InstrumentationBaggage + +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 { + let promise = self.eventLoop.makePromise(of: Void.self) + self.fs.shutdown(promise: promise) + return promise.futureResult + } + + func handleNFSCall(_ callMessage: RPCNFS3Call, baggage: Baggage) { + switch callMessage.nfsCall { + case .mount(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyMount.self) + + self.fs.mount(call, baggage: baggage, promise: promise) + + promise.futureResult.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): + let promise = self.eventLoop.makePromise(of: NFS3ReplyUnmount.self) + + self.fs.unmount(call, baggage: baggage, promise: promise) + + promise.futureResult.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): + let promise = self.eventLoop.makePromise(of: NFS3ReplyGetAttr.self) + + self.fs.getattr(call, baggage: baggage, promise: promise) + + promise.futureResult.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): + let promise = self.eventLoop.makePromise(of: NFS3ReplyFSInfo.self) + + self.fs.fsinfo(call, baggage: baggage, promise: promise) + + promise.futureResult.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): + let promise = self.eventLoop.makePromise(of: NFS3ReplyPathConf.self) + + self.fs.pathconf(call, baggage: baggage, promise: promise) + + promise.futureResult.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): + let promise = self.eventLoop.makePromise(of: NFS3ReplyFSStat.self) + + self.fs.fsstat(call, baggage: baggage, promise: promise) + + promise.futureResult.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): + let promise = self.eventLoop.makePromise(of: NFS3ReplyAccess.self) + + self.fs.access(call, baggage: baggage, promise: promise) + + promise.futureResult.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): + let promise = self.eventLoop.makePromise(of: NFS3ReplyLookup.self) + + self.fs.lookup(call, baggage: baggage, promise: promise) + + promise.futureResult.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): + let promise = self.eventLoop.makePromise(of: NFS3ReplyReadDirPlus.self) + + self.fs.readdirplus(call, baggage: baggage, promise: promise) + + promise.futureResult.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): + let promise = self.eventLoop.makePromise(of: NFS3ReplyRead.self) + + self.fs.read(call, baggage: baggage, promise: promise) + + promise.futureResult.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 .readlink(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyReadlink.self) + + self.fs.readlink(call, baggage: baggage, promise: promise) + + promise.futureResult.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): + let promise = self.eventLoop.makePromise(of: NFS3ReplySetattr.self) + + self.fs.setattr(call, baggage: baggage, promise: promise) + + promise.futureResult.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) + } + } + } + } +} diff --git a/Sources/NIONFS3/NFSFileSystemServerHandler.swift b/Sources/NIONFS3/NFSFileSystemServerHandler.swift new file mode 100644 index 00000000..fcf1436b --- /dev/null +++ b/Sources/NIONFS3/NFSFileSystemServerHandler.swift @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 InstrumentationBaggage + +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) + private var baggage: Baggage + + public init(_ fs: FS, baggage: Baggage) { + self.filesystem = fs + self.baggage = baggage + } +} + +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?.handleNFSCall(nfsCall, baggage: self.baggage) + } + } 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)): + print("UNKNOWN CALL: \(call)") + let reply = RPCNFS3Reply(rpcReply: .init(xid: call.xid, + status: .messageAccepted(.init(verifier: .init(flavor: .noAuth, opaque: nil), + status: .procedureUnavailable))), + nfsReply: .null) + self.writeBuffer.clear() + self.writeBuffer.writeRPCNFSReply(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.writeRPCNFSReplyPartially(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.writeRPCNFSReply(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..67f26b22 --- /dev/null +++ b/Sources/NIONFS3/NFSReplyDecoder.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +public struct NFS3ReplyDecoder: WriteObservingByteToMessageDecoder { + public typealias OutboundIn = RPCNFS3Call + public typealias InboundOut = RPCNFS3Reply + + private var procedures: [UInt32: RPCNFSProcedureID] + private let allowDuplicateReplies: Bool + + public init(prepopulatedProcecedures: [UInt32: RPCNFSProcedureID]? = 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: RPCNFSProcedureID + 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.readNFSReply(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..b44f0d87 --- /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-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 + +public struct NFS3ReplyEncoder: MessageToByteEncoder { + public typealias OutboundIn = RPCNFS3Reply + + public init() {} + + public func encode(data: RPCNFS3Reply, out: inout ByteBuffer) throws { + out.writeRPCNFSReply(data) + } +} diff --git a/Sources/NIONFS3/NFSTypes+Access.swift b/Sources/NIONFS3/NFSTypes+Access.swift new file mode 100644 index 00000000..78ebf730 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Access.swift @@ -0,0 +1,104 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - Access +public struct NFS3CallAccess: Equatable { + public init(object: NFS3FileHandle, access: NFS3Access) { + self.object = object + self.access = access + } + + public var object: NFS3FileHandle + public var access: NFS3Access +} + +public struct NFS3ReplyAccess: Equatable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Equatable { + public init(dirAttributes: NFS3FileAttr?, access: NFS3Access) { + self.dirAttributes = dirAttributes + self.access = access + } + + public var dirAttributes: NFS3FileAttr? + public var access: NFS3Access + } + + public struct Fail: Equatable { + public init(dirAttributes: NFS3FileAttr?) { + self.dirAttributes = dirAttributes + } + + public var dirAttributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFSCallAccess() throws -> NFS3CallAccess { + let fileHandle = try self.readNFSFileHandle() + let access = try self.readNFSInteger(as: UInt32.self) + return NFS3CallAccess(object: fileHandle, access: .init(rawValue: access)) + } + + public mutating func writeNFSCallAccess(_ call: NFS3CallAccess) { + self.writeNFSFileHandle(call.object) + self.writeInteger(call.access.rawValue, endianness: .big) + } + + public mutating func readNFSReplyAccess() throws -> NFS3ReplyAccess { + return NFS3ReplyAccess(result: try self.readNFSResult( + readOkay: { buffer in + let attrs = try buffer.readNFSOptional { buffer in + try buffer.readNFSFileAttr() + } + let rawValue = try buffer.readNFSInteger(as: UInt32.self) + return NFS3ReplyAccess.Okay(dirAttributes: attrs, access: NFS3Access(rawValue: rawValue)) + } + , readFail: { buffer in + return NFS3ReplyAccess.Fail(dirAttributes: try buffer.readNFSOptional { buffer in + try buffer.readNFSFileAttr() + }) + })) + } + + public mutating func writeNFSReplyAccess(_ accessResult: NFS3ReplyAccess) { + switch accessResult.result { + case .okay(let result): + self.writeInteger(NFS3Status.ok.rawValue, endianness: .big) + if let attrs = result.dirAttributes { + self.writeInteger(1, endianness: .big, as: UInt32.self) + self.writeNFSFileAttr(attrs) + } else { + self.writeInteger(0, endianness: .big, as: UInt32.self) + } + self.writeInteger(result.access.rawValue, endianness: .big) + case .fail(let status, let fail): + precondition(status != .ok) + self.writeInteger(status.rawValue, endianness: .big) + if let attrs = fail.dirAttributes { + self.writeInteger(1, endianness: .big, as: UInt32.self) + self.writeNFSFileAttr(attrs) + } else { + self.writeInteger(0, endianness: .big, as: UInt32.self) + } + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+Common.swift b/Sources/NIONFS3/NFSTypes+Common.swift new file mode 100644 index 00000000..8ad0e255 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Common.swift @@ -0,0 +1,593 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - SwiftNFS Specifics +@available(*, deprecated, renamed: "RPCNFS3Call") +public typealias RPCNFSCall = RPCNFS3Call + +public struct RPCNFS3Call: Equatable { + 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 .mount: + rpcCall.programAndProcedure = .mount + case .unmount: + rpcCall.programAndProcedure = .unmount + case .null: + rpcCall.programAndProcedure = .null + case .getattr: + rpcCall.programAndProcedure = .getattr + case .fsinfo: + rpcCall.programAndProcedure = .fsinfo + case .pathconf: + rpcCall.programAndProcedure = .pathconf + case .fsstat: + rpcCall.programAndProcedure = .fsstat + case .access: + rpcCall.programAndProcedure = .access + case .lookup: + rpcCall.programAndProcedure = .lookup + case .readdirplus: + rpcCall.programAndProcedure = .readdirplus + case .read: + rpcCall.programAndProcedure = .read + case .readlink: + rpcCall.programAndProcedure = .readlink + case .setattr: + rpcCall.programAndProcedure = .setattr + } + + 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 + } +} + +@available(*, deprecated, renamed: "RPCNFS3Reply") +public typealias RPCNFSReply = RPCNFS3Reply + +public struct RPCNFS3Reply: Equatable { + 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: Equatable where Okay: Equatable, Fail: Equatable { +} + +extension NFS3Result { + public var status: NFS3Status { + switch self { + case .okay: + return .ok + case .fail(let status, _): + assert(status != .ok) + return status + } + } +} + +// MARK: - General +public typealias NFS3FileMode = UInt32 +public typealias NFS3UID = UInt32 +public typealias NFS3GID = UInt32 +public typealias NFS3Size = UInt64 +public typealias NFS3SpecData = UInt64 +public typealias NFS3FileID = UInt64 +public typealias NFS3Cookie = UInt64 +public typealias NFS3CookieVerifier = UInt64 +public typealias NFS3Offset = UInt64 +public typealias NFS3Count = UInt32 + +public struct NFS3Nothing: Equatable { + public init() {} +} + +public enum NFS3Status: UInt32 { + 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 +} + +public struct NFS3Access: OptionSet { + public typealias RawValue = UInt32 + + public var rawValue: UInt32 + + 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] +} + +public enum NFS3FileType: UInt32 { + 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, CustomStringConvertible { + @usableFromInline + internal var _value: UInt64 + + public init(_ value: UInt64) { + self._value = value + } + + 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(endianness: .big, as: UInt64.self)!) + } + + public var description: String { + return "NFS3FileHandle(\(self._value))" + } +} + +extension UInt64 { + // This initialiser is fallible because we're 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: Equatable { + public init(seconds: UInt32, nanoSeconds: UInt32) { + self.seconds = seconds + self.nanoSeconds = nanoSeconds + } + + public var seconds: UInt32 + public var nanoSeconds: UInt32 +} + +public struct NFS3FileAttr: Equatable { + 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: Equatable { + 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: Equatable { + 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 readNFSWeakCacheConsistencyAttr() throws -> NFS3WeakCacheConsistencyAttr { + let size = try self.readNFSInteger(as: NFS3Size.self) + let mtime = try self.readNFSTime() + let ctime = try self.readNFSTime() + + return .init(size: size, mtime: mtime, ctime: ctime) + } + + public mutating func writeNFSWeakCacheConsistencyAttr(_ wccAttr: NFS3WeakCacheConsistencyAttr) { + self.writeInteger(wccAttr.size, endianness: .big) + self.writeNFSTime(wccAttr.mtime) + self.writeNFSTime(wccAttr.ctime) + } + + public mutating func readNFSWeakCacheConsistencyData() throws -> NFS3WeakCacheConsistencyData { + let before = try self.readNFSOptional { try $0.readNFSWeakCacheConsistencyAttr() } + let after = try self.readNFSOptional { try $0.readNFSFileAttr() } + + return .init(before: before, after: after) + } + + public mutating func writeNFSWeakCacheConsistencyData(_ wccData: NFS3WeakCacheConsistencyData) { + self.writeNFSOptional(wccData.before, writer: { $0.writeNFSWeakCacheConsistencyAttr($1) }) + self.writeNFSOptional(wccData.after, writer: { $0.writeNFSFileAttr($1) }) + } + + public mutating func readNFSInteger(as: I.Type = I.self) throws -> I { + if let value = self.readInteger(endianness: .big, as: I.self) { + return value + } else { + throw NFS3Error.illegalRPCTooShort + } + } + + public mutating func readNFSBlob() throws -> ByteBuffer { + let length = try self.readNFSInteger(as: UInt32.self) + guard let blob = self.readSlice(length: Int(length)), + let _ = self.readSlice(length: nfsStringFillBytes(Int(length))) else { + throw NFS3Error.illegalRPCTooShort + } + return blob + } + + public mutating func writeNFSBlob(_ blob: ByteBuffer) { + let byteCount = blob.readableBytes + self.writeInteger(UInt32(byteCount), endianness: .big) + self.writeImmutableBuffer(blob) + self.writeRepeatingByte(0x42, count: nfsStringFillBytes(byteCount)) + } + + public mutating func readNFSString() throws -> String { + let blob = try self.readNFSBlob() + return String(buffer: blob) + } + + public mutating func writeNFSString(_ string: String) { + let byteCount = string.utf8.count + self.writeInteger(UInt32(byteCount), endianness: .big) + self.writeString(string) + self.writeRepeatingByte(0x42, count: nfsStringFillBytes(byteCount)) + } + + public mutating func readNFSFileHandle() throws -> NFS3FileHandle { + guard let values = self.readMultipleIntegers(endianness: .big, 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) + } + + public mutating func writeNFSFileHandle(_ fileHandle: NFS3FileHandle) { + // TODO: This ! is safe at the moment until the file handle == 64 bits limitation is lifted + let id = UInt64(fileHandle)! + self.writeMultipleIntegers(UInt32(MemoryLayout.size(ofValue: id)), id, endianness: .big) + } + + public mutating func writeNFSFileType(_ fileType: NFS3FileType) { + self.writeInteger(fileType.rawValue, endianness: .big) + } + + public mutating func writeNFSTime(_ time: NFS3Time) { + self.writeMultipleIntegers(time.seconds, time.nanoSeconds, endianness: .big) + } + + public mutating func read3NFSTimes() throws -> (NFS3Time, NFS3Time, NFS3Time) { + guard let values = self.readMultipleIntegers(endianness: .big, + 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)) + } + + public mutating func write3NFSTimes(_ time1: NFS3Time, _ time2: NFS3Time, _ time3: NFS3Time) { + self.writeMultipleIntegers(time1.seconds, time1.nanoSeconds, + time2.seconds, time2.nanoSeconds, + time3.seconds, time3.nanoSeconds) + } + + public mutating func readNFSTime() throws -> NFS3Time { + guard let values = self.readMultipleIntegers(endianness: .big, as: (UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + + return .init(seconds: values.0, nanoSeconds: values.1) + } + + public mutating func readNFSFileType() throws -> NFS3FileType { + let typeRaw = try self.readNFSInteger(as: UInt32.self) + if let type = NFS3FileType(rawValue: typeRaw) { + return type + } else { + throw NFS3Error.invalidFileType(typeRaw) + } + } + + public mutating func readNFSFileAttr() throws -> NFS3FileAttr { + let type = try self.readNFSFileType() + guard let values = self.readMultipleIntegers(endianness: .big, + as: (UInt32, UInt32, UInt32, UInt32, NFS3Size, + NFS3Size, UInt64, UInt64, NFS3FileID, + 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: mode, nlink: nlink, + uid: uid, gid: gid, + size: size, used: used, + rdev: rdev, fsid: fsid, fileid: fileid, + atime: atime, mtime: mtime, ctime: ctime) + } + + public mutating func writeNFSFileAttr(_ attributes: NFS3FileAttr) { + self.writeNFSFileType(attributes.type) + self.writeMultipleIntegers( + attributes.mode, + attributes.nlink, + attributes.uid, + attributes.gid, + attributes.size, + attributes.used, + attributes.rdev, + attributes.fsid, + attributes.fileid, + attributes.atime.seconds, + attributes.atime.nanoSeconds, + attributes.mtime.seconds, + attributes.mtime.nanoSeconds, + attributes.ctime.seconds, + attributes.ctime.nanoSeconds, + endianness: .big) + } + + public mutating func writeNFSBool(_ bool: NFS3Bool) { + self.writeInteger(bool == true ? 1 : 0, endianness: .big, as: UInt32.self) + } + + public mutating func readNFSBool() throws -> Bool { + let rawValue = try self.readNFSInteger(as: UInt32.self) + return rawValue != 0 + } + + public mutating func readNFSOptional(_ reader: (inout ByteBuffer) throws -> T) rethrows -> T? { + if self.readInteger(endianness: .big, as: UInt32.self) == 1 { + return try reader(&self) + } else { + return nil + } + } + + public mutating func writeNFSOptional(_ value: T?, writer: (inout ByteBuffer, T) -> Void) { + if let value = value { + self.writeInteger(1, endianness: .big, as: UInt32.self) + writer(&self, value) + } else { + self.writeInteger(0, endianness: .big, as: UInt32.self) + } + } + + public mutating func readNFSCount() throws -> NFS3Count { + return try self.readNFSInteger(as: NFS3Count.self) + } + + public mutating func readNFSList(readEntry: (inout ByteBuffer) throws -> Element) throws -> [Element] { + let count = try self.readNFSCount() + var result: [Element] = [] + result.reserveCapacity(Int(count)) + + for _ in 0..(_ result: NFS3Result) { + self.writeInteger(result.status.rawValue, endianness: .big, as: UInt32.self) + } + + public mutating func readNFSStatus() throws -> NFS3Status { + let rawValue = try self.readNFSInteger(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.readNFSInteger(as: UInt32.self) + if let flavor = RPCAuthFlavor(rawValue: rawValue) { + return flavor + } else { + throw RPCErrors.invalidAuthFlavor(rawValue) + } + } + + public mutating func readNFSResult(readOkay: (inout ByteBuffer) throws -> O, + readFail: (inout ByteBuffer) throws -> F) throws -> NFS3Result { + let status = try self.readNFSStatus() + switch status { + case .ok: + return .okay(try readOkay(&self)) + default: + return .fail(status, try readFail(&self)) + } + } + + public mutating func writeNFSSize(_ size: NFS3Size) { + self.writeInteger(size, endianness: .big) + } + + public mutating func readNFSSize() throws -> NFS3Size { + return try self.readNFSInteger() + } +} + +public enum NFS3PartialWriteNextStep { + 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..887d607b --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Containers.swift @@ -0,0 +1,470 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +public struct RPCNFSProcedureID: Equatable { + public internal(set) var program: UInt32 + public internal(set) var procedure: UInt32 + + public static let mount: Self = .init(program: 100005, procedure: 1) + public static let unmount: Self = .init(program: 100005, procedure: 3) + public static let null: Self = .init(program: 100003, procedure: 0) + public static let getattr: Self = .init(program: 100003, procedure: 1) + public static let fsinfo: Self = .init(program: 100003, procedure: 19) + public static let pathconf: Self = .init(program: 100003, procedure: 20) + public static let fsstat: Self = .init(program: 100003, procedure: 18) + public static let access: Self = .init(program: 100003, procedure: 4) + public static let lookup: Self = .init(program: 100003, procedure: 3) + public static let readdirplus: Self = .init(program: 100003, procedure: 17) + public static let read: Self = .init(program: 100003, procedure: 6) + public static let readlink: Self = .init(program: 100003, procedure: 5) + public static let setattr: Self = .init(program: 100003, procedure: 2) +} + +extension RPCNFSProcedureID { + public init(_ nfsReply: NFS3Reply) { + switch nfsReply { + case .mount: + self = .mount + case .unmount: + self = .unmount + case .null: + self = .null + case .getattr: + self = .getattr + case .fsinfo: + self = .fsinfo + case .pathconf: + self = .pathconf + case .fsstat: + self = .fsstat + case .access: + self = .access + case .lookup: + self = .lookup + case .readdirplus: + self = .readdirplus + case .read: + self = .read + case .readlink: + self = .readlink + case .setattr: + self = .setattr + } + } +} + +// FIXME: PUBLIC API: Problematic, can we keep this private? +public enum NFS3Call: Equatable { + case mount(NFS3CallMount) + case unmount(NFS3CallUnmount) + 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) +} + +// FIXME: PUBLIC API: Problematic, can we keep this private? +public enum NFS3Reply: Equatable { + case mount(NFS3ReplyMount) + case unmount(NFS3ReplyUnmount) + case null + case getattr(NFS3ReplyGetAttr) + case fsinfo(NFS3ReplyFSInfo) + case pathconf(NFS3ReplyPathConf) + case fsstat(NFS3ReplyFSStat) + case access(NFS3ReplyAccess) + case lookup(NFS3ReplyLookup) + case readdirplus(NFS3ReplyReadDirPlus) + case read(NFS3ReplyRead) + case readlink(NFS3ReplyReadlink) + case setattr(NFS3ReplySetattr) +} + +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) +} + +internal func nfsStringFillBytes(_ byteCount: Int) -> Int { + return (4 - (byteCount % 4)) % 4 +} + +extension ByteBuffer { + mutating func readRPCVerifier() throws -> RPCOpaqueAuth { + guard let flavor = self.readInteger(endianness: .big, as: UInt32.self), + let length = self.readInteger(endianness: .big, as: 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) + } + + public mutating func writeRPCVerifier(_ verifier: RPCOpaqueAuth) { + self.writeInteger(verifier.flavor.rawValue, endianness: .big) + if let opaqueBlob = verifier.opaque { + self.writeNFSBlob(opaqueBlob) + } else { + self.writeInteger(0, endianness: .big, as: UInt32.self) + } + } + + public mutating func readRPCCredentials() throws -> RPCCredentials { + guard let flavor = self.readInteger(endianness: .big, as: UInt32.self) else { + throw NFS3Error.illegalRPCTooShort + } + let blob = try self.readNFSBlob() + return RPCCredentials(flavor: flavor, length: UInt32(blob.readableBytes), otherBytes: blob) + } + + public mutating func writeRPCCredentials(_ credentials: RPCCredentials) { + self.writeInteger(credentials.flavor) + self.writeNFSBlob(credentials.otherBytes) + } + + public mutating func readRPCFragmentHeader() throws -> RPCFragmentHeader? { + let save = self + guard let lastAndLength = self.readInteger(endianness: .big, 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, endianness: .big) + } + + public mutating func writeRPCFragmentHeader(_ header: RPCFragmentHeader) { + let bytesWritten = self.setRPCFragmentHeader(header, at: self.writerIndex) + self.moveWriterIndex(forwardBy: bytesWritten) + } + + mutating func readRPCReply(xid: UInt32) throws -> RPCReply { + let acceptedOrDenied = try self.readNFSInteger(as: UInt32.self) + switch acceptedOrDenied { + case 0: // MSG_ACCEPTED + let verifier = try self.readRPCVerifier() + let status = try self.readNFSInteger(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.readNFSInteger(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.readNFSInteger(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) + } + } + + public mutating func writeRPCCall(_ call: RPCCall) { + self.writeMultipleIntegers( + RPCMessageType.call.rawValue, + call.rpcVersion, + call.program, + call.programVersion, + call.procedure, + endianness: .big) + self.writeRPCCredentials(call.credentials) + self.writeRPCVerifier(call.verifier) + } + + public mutating func writeRPCReply(_ reply: RPCReply) { + self.writeInteger(RPCMessageType.reply.rawValue, endianness: .big) + + switch reply.status { + case .messageAccepted(_): + self.writeInteger(0 /* accepted */, endianness: .big, as: UInt32.self) + case .messageDenied(_): + // FIXME: MSG_DENIED (spec name) isn't actually handled correctly here. + self.writeInteger(1 /* denied */, endianness: .big, as: UInt32.self) + } + self.writeInteger(0 /* verifier */, endianness: .big, as: UInt64.self) + self.writeInteger(0 /* executed successfully */, endianness: .big, as: UInt32.self) + } + + + public mutating func readRPCCall(xid: UInt32) throws -> RPCCall { + guard let version = self.readInteger(endianness: .big, as: UInt32.self), + let program = self.readInteger(endianness: .big, as: UInt32.self), + let programVersion = self.readInteger(endianness: .big, as: UInt32.self), + let procedure = self.readInteger(endianness: .big, as: UInt32.self) else { + throw NFS3Error.illegalRPCTooShort + } + 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 readNFSReply(programAndProcedure: RPCNFSProcedureID, rpcReply: RPCReply) throws -> RPCNFS3Reply { + switch programAndProcedure { + case .mount: + return .init(rpcReply: rpcReply, nfsReply: .mount(try self.readNFSReplyMount())) + case .unmount: + return .init(rpcReply: rpcReply, nfsReply: .unmount(try self.readNFSReplyUnmount())) + case .null: + return .init(rpcReply: rpcReply, nfsReply: .null) + case .getattr: + return .init(rpcReply: rpcReply, nfsReply: .getattr(try self.readNFSReplyGetAttr())) + case .fsinfo: + return .init(rpcReply: rpcReply, nfsReply: .fsinfo(try self.readNFSReplyFSInfo())) + case .pathconf: + return .init(rpcReply: rpcReply, nfsReply: .pathconf(try self.readNFSReplyPathConf())) + case .fsstat: + return .init(rpcReply: rpcReply, nfsReply: .fsstat(try self.readNFSReplyFSStat())) + case .access: + return .init(rpcReply: rpcReply, nfsReply: .access(try self.readNFSReplyAccess())) + case .lookup: + return .init(rpcReply: rpcReply, nfsReply: .lookup(try self.readNFSReplyLookup())) + case .readdirplus: + return .init(rpcReply: rpcReply, nfsReply: .readdirplus(try self.readNFSReplyReadDirPlus())) + case .read: + return .init(rpcReply: rpcReply, nfsReply: .read(try self.readNFSReplyRead())) + case .readlink: + return .init(rpcReply: rpcReply, nfsReply: .readlink(try self.readNFSReplyReadlink())) + case .setattr: + return .init(rpcReply: rpcReply, nfsReply: .setattr(try self.readNFSReplySetattr())) + default: + throw NFS3Error.unknownProgramOrProcedure(.reply(rpcReply)) + } + } + + mutating func readNFSCall(rpc: RPCCall) throws -> RPCNFS3Call { + switch RPCNFSProcedureID(program: rpc.program, procedure: rpc.procedure) { + case .mount: + return .init(rpcCall: rpc, nfsCall: .mount(try self.readNFSCallMount())) + case .unmount: + return .init(rpcCall: rpc, nfsCall: .unmount(try self.readNFSCallUnmount())) + case .null: + return .init(rpcCall: rpc, nfsCall: .null(try self.readNFSCallNull())) + case .getattr: + return .init(rpcCall: rpc, nfsCall: .getattr(try self.readNFSCallGetattr())) + case .fsinfo: + return .init(rpcCall: rpc, nfsCall: .fsinfo(try self.readNFSCallFSInfo())) + case .pathconf: + return .init(rpcCall: rpc, nfsCall: .pathconf(try self.readNFSCallPathConf())) + case .fsstat: + return .init(rpcCall: rpc, nfsCall: .fsstat(try self.readNFSCallFSStat())) + case .access: + return .init(rpcCall: rpc, nfsCall: .access(try self.readNFSCallAccess())) + case .lookup: + return .init(rpcCall: rpc, nfsCall: .lookup(try self.readNFSCallLookup())) + case .readdirplus: + return .init(rpcCall: rpc, nfsCall: .readdirplus(try self.readNFSCallReadDirPlus())) + case .read: + return .init(rpcCall: rpc, nfsCall: .read(try self.readNFSCallRead())) + case .readlink: + return .init(rpcCall: rpc, nfsCall: .readlink(try self.readNFSCallReadlink())) + case .setattr: + return .init(rpcCall: rpc, nfsCall: .setattr(try self.readNFSCallSetattr())) + default: + throw NFS3Error.unknownProgramOrProcedure(.call(rpc)) + } + } + + @discardableResult + public mutating func writeRPCNFSCall(_ rpcNFSCall: RPCNFS3Call) -> Int { + let startWriterIndex = self.writerIndex + self.writeRPCFragmentHeader(.init(length: 12345678, last: false)) // placeholder, overwritten later + self.writeInteger(rpcNFSCall.rpcCall.xid, endianness: .big) + + self.writeRPCCall(rpcNFSCall.rpcCall) + + switch rpcNFSCall.nfsCall { + case .mount(let nfsCallMount): + self.writeNFSCallMount(nfsCallMount) + case .unmount(let nfsCallUnmount): + self.writeNFSCallUnmount(nfsCallUnmount) + case .null: + () // noop + case .getattr(let nfsCallGetAttr): + self.writeNFSCallGetattr(nfsCallGetAttr) + case .fsinfo(let nfsCallFSInfo): + self.writeNFSCallFSInfo(nfsCallFSInfo) + case .pathconf(let nfsCallPathConf): + self.writeNFSCallPathConf(nfsCallPathConf) + case .fsstat(let nfsCallFSStat): + self.writeNFSCallFSStat(nfsCallFSStat) + case .access(let nfsCallAccess): + self.writeNFSCallAccess(nfsCallAccess) + case .lookup(let nfsCallLookup): + self.writeNFSCallLookup(nfsCallLookup) + case .readdirplus(let nfsCallReadDirPlus): + self.writeNFSCallReadDirPlus(nfsCallReadDirPlus) + case .read(let nfsCallRead): + self.writeNFSCallRead(nfsCallRead) + case .readlink(let nfsCallReadlink): + self.writeNFSCallReadlink(nfsCallReadlink) + case .setattr(let nfsCallSetattr): + self.writeNFSCallSetattr(nfsCallSetattr) + } + + self.setRPCFragmentHeader(.init(length: UInt32(self.writerIndex - startWriterIndex - 4), + last: true), + at: startWriterIndex) + return self.writerIndex - startWriterIndex + } + + public mutating func writeRPCNFSReplyPartially(_ rpcNFSReply: RPCNFS3Reply) -> (Int, NFS3PartialWriteNextStep) { + var nextStep: NFS3PartialWriteNextStep = .doNothing + + let startWriterIndex = self.writerIndex + self.writeRPCFragmentHeader(.init(length: 12345678, last: false)) // placeholder, overwritten later + self.writeInteger(rpcNFSReply.rpcReply.xid, endianness: .big) + + self.writeRPCReply(rpcNFSReply.rpcReply) + + switch rpcNFSReply.nfsReply { + case .mount(let nfsReplyMount): + self.writeNFSReplyMount(nfsReplyMount) + case .unmount(let nfsReplyUnmount): + self.writeNFSReplyUnmount(nfsReplyUnmount) + case .null: + () + case .getattr(let nfsReplyGetAttr): + self.writeNFSReplyGetAttr(nfsReplyGetAttr) + case .fsinfo(let nfsReplyFSInfo): + self.writeNFSReplyFSInfo(nfsReplyFSInfo) + case .pathconf(let nfsReplyPathConf): + self.writeNFSReplyPathConf(nfsReplyPathConf) + case .fsstat(let nfsReplyFSStat): + self.writeNFSReplyFSStat(nfsReplyFSStat) + case .access(let nfsReplyAccess): + self.writeNFSReplyAccess(nfsReplyAccess) + case .lookup(let nfsReplyLookup): + self.writeNFSReplyLookup(nfsReplyLookup) + case .readdirplus(let nfsReplyReadDirPlus): + self.writeNFSReplyReadDirPlus(nfsReplyReadDirPlus) + case .read(let nfsReplyRead): + nextStep = self.writeNFSReplyReadPartially(nfsReplyRead) + case .readlink(let nfsReplyReadlink): + self.writeNFSReplyReadlink(nfsReplyReadlink) + case .setattr(let nfsReplySetattr): + self.writeNFSReplySetattr(nfsReplySetattr) + } + + self.setRPCFragmentHeader(.init(length: UInt32(self.writerIndex - startWriterIndex - 4 + nextStep.bytesToFollow), + last: true), + at: startWriterIndex) + return (self.writerIndex - startWriterIndex, nextStep) + } + + @discardableResult + public mutating func writeRPCNFSReply(_ reply: RPCNFS3Reply) -> Int { + let (bytesWritten, nextStep) = self.writeRPCNFSReplyPartially(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(endianness: .big, as: UInt32.self), + let messageType = self.readInteger(endianness: .big, 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..3c772a74 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+FSInfo.swift @@ -0,0 +1,160 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - FSInfo +public struct NFS3CallFSInfo: Equatable { + public init(fsroot: NFS3FileHandle) { + self.fsroot = fsroot + } + + public var fsroot: NFS3FileHandle +} + +public struct NFS3ReplyFSInfo: Equatable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Properties: OptionSet { + 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: Equatable { + 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: Equatable { + public init(attributes: NFS3FileAttr?) { + self.attributes = attributes + } + + public var attributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFSCallFSInfo() throws -> NFS3CallFSInfo { + let fileHandle = try self.readNFSFileHandle() + return NFS3CallFSInfo(fsroot: fileHandle) + } + + public mutating func writeNFSCallFSInfo(_ call: NFS3CallFSInfo) { + self.writeNFSFileHandle(call.fsroot) + } + + private mutating func readNFSCallFSInfoProperties() throws -> NFS3ReplyFSInfo.Properties { + let rawValue = try self.readNFSInteger(as: UInt32.self) + return NFS3ReplyFSInfo.Properties(rawValue: rawValue) + } + + public mutating func writeNFSReplyFSInfo(_ reply: NFS3ReplyFSInfo) { + self.writeNFSResultStatus(reply.result) + + switch reply.result { + case .okay(let reply): + self.writeNFSOptional(reply.attributes, writer: { $0.writeNFSFileAttr($1) }) + self.writeMultipleIntegers( + reply.rtmax, + reply.rtpref, + reply.rtmult, + reply.wtmax, + reply.wtpref, + reply.wtmult, + reply.dtpref, + reply.maxFileSize, + endianness: .big) + self.writeNFSTime(reply.timeDelta) + self.writeInteger(reply.properties.rawValue, endianness: .big) + case .fail(_, let fail): + self.writeNFSOptional(fail.attributes, writer: { $0.writeNFSFileAttr($1) }) + } + } + + private mutating func readNFSReplyFSInfoOkay() throws -> NFS3ReplyFSInfo.Okay { + let fileAttr = try self.readNFSOptional { try $0.readNFSFileAttr() } + guard let values = self.readMultipleIntegers(endianness: .big, + 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.readNFSSize() + let timeDelta = try self.readNFSTime() + let properties = try self.readNFSCallFSInfoProperties() + + 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 readNFSReplyFSInfo() throws -> NFS3ReplyFSInfo { + return NFS3ReplyFSInfo(result: try self.readNFSResult( + readOkay: { try $0.readNFSReplyFSInfoOkay() }, + readFail: { NFS3ReplyFSInfo.Fail(attributes: try $0.readNFSFileAttr()) } + )) + } +} diff --git a/Sources/NIONFS3/NFSTypes+FSStat.swift b/Sources/NIONFS3/NFSTypes+FSStat.swift new file mode 100644 index 00000000..c32098fd --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+FSStat.swift @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - FSStat +public struct NFS3CallFSStat: Equatable { + public init(fsroot: NFS3FileHandle) { + self.fsroot = fsroot + } + + public var fsroot: NFS3FileHandle +} + +public struct NFS3ReplyFSStat: Equatable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Equatable { + 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: Equatable { + public init(attributes: NFS3FileAttr?) { + self.attributes = attributes + } + + public var attributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFSCallFSStat() throws -> NFS3CallFSStat { + let fileHandle = try self.readNFSFileHandle() + return NFS3CallFSStat(fsroot: fileHandle) + } + + public mutating func writeNFSCallFSStat(_ call: NFS3CallFSStat) { + self.writeNFSFileHandle(call.fsroot) + } + + private mutating func readNFSReplyFSStatOkay() throws -> NFS3ReplyFSStat.Okay { + let attrs = try self.readNFSOptional { buffer in + try buffer.readNFSFileAttr() + } + if let values = self.readMultipleIntegers(as: (NFS3Size, NFS3Size, NFS3Size, NFS3Size, NFS3Size, NFS3Size, UInt32).self) { + return .init(attributes: attrs, + tbytes: values.0, fbytes: values.1, abytes: values.2, + tfiles: values.3, ffiles: values.4, afiles: values.5, + invarsec: values.6) + } else { + throw NFS3Error.illegalRPCTooShort + } + } + + public mutating func readNFSReplyFSStat() throws -> NFS3ReplyFSStat { + return NFS3ReplyFSStat( + result: try self.readNFSResult( + readOkay: { buffer in + try buffer.readNFSReplyFSStatOkay() + }, + readFail: { buffer in + NFS3ReplyFSStat.Fail( + attributes: try buffer.readNFSOptional { buffer in + try buffer.readNFSFileAttr() + } + ) + }) + ) + } + + public mutating func writeNFSReplyFSStat(_ reply: NFS3ReplyFSStat) { + self.writeNFSResultStatus(reply.result) + + switch reply.result { + case .okay(let okay): + self.writeNFSOptional(okay.attributes, writer: { $0.writeNFSFileAttr($1) }) + self.writeMultipleIntegers( + okay.tbytes, + okay.fbytes, + okay.abytes, + okay.tfiles, + okay.ffiles, + okay.afiles, + okay.invarsec, + endianness: .big) + case .fail(_, let fail): + self.writeNFSOptional(fail.attributes, writer: { $0.writeNFSFileAttr($1) }) + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+Getattr.swift b/Sources/NIONFS3/NFSTypes+Getattr.swift new file mode 100644 index 00000000..48835eea --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Getattr.swift @@ -0,0 +1,74 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - Getattr +public struct NFS3CallGetAttr: Equatable { + public init(fileHandle: NFS3FileHandle) { + self.fileHandle = fileHandle + } + + public var fileHandle: NFS3FileHandle +} + +public struct NFS3ReplyGetAttr: Equatable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Equatable { + public init(attributes: NFS3FileAttr) { + self.attributes = attributes + } + + public var attributes: NFS3FileAttr + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFSCallGetattr() throws -> NFS3CallGetAttr { + let fileHandle = try self.readNFSFileHandle() + return NFS3CallGetAttr(fileHandle: fileHandle) + } + + public mutating func writeNFSCallGetattr(_ call: NFS3CallGetAttr) { + self.writeNFSFileHandle(call.fileHandle) + } + + public mutating func readNFSReplyGetAttr() throws -> NFS3ReplyGetAttr { + return NFS3ReplyGetAttr( + result: try self.readNFSResult( + readOkay: { buffer in + return NFS3ReplyGetAttr.Okay(attributes: try buffer.readNFSFileAttr()) + }, + readFail: { _ in + return NFS3Nothing() + }) + ) + } + + public mutating func writeNFSReplyGetAttr(_ reply: NFS3ReplyGetAttr) { + self.writeNFSResultStatus(reply.result) + + switch reply.result { + case .okay(let okay): + self.writeNFSFileAttr(okay.attributes) + case .fail(_, _): + () + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+Lookup.swift b/Sources/NIONFS3/NFSTypes+Lookup.swift new file mode 100644 index 00000000..76dd1025 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Lookup.swift @@ -0,0 +1,120 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 Foundation + +// MARK: - Lookup +public struct NFS3CallLookup: Equatable { + public init(dir: NFS3FileHandle, name: String) { + self.dir = dir + self.name = name + } + + public var dir: NFS3FileHandle + public var name: String +} + +public struct NFS3ReplyLookup: Equatable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Equatable { + 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: Equatable { + public init(dirAttributes: NFS3FileAttr? = nil) { + self.dirAttributes = dirAttributes + } + + public var dirAttributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFSCallLookup() throws -> NFS3CallLookup { + let dir = try self.readNFSFileHandle() + let name = try self.readNFSString() + return NFS3CallLookup(dir: dir, name: name) + } + + public mutating func writeNFSCallLookup(_ call: NFS3CallLookup) { + self.writeNFSFileHandle(call.dir) + self.writeNFSString(call.name) + } + + public mutating func readNFSReplyLookup() throws -> NFS3ReplyLookup { + return NFS3ReplyLookup( + result: try self.readNFSResult( + readOkay: { buffer in + let fileHandle = try buffer.readNFSFileHandle() + let attrs = try buffer.readNFSOptional { buffer in + try buffer.readNFSFileAttr() + } + let dirAttrs = try buffer.readNFSOptional { buffer in + try buffer.readNFSFileAttr() + } + + return NFS3ReplyLookup.Okay(fileHandle: fileHandle, attributes: attrs, dirAttributes: dirAttrs) + }, + readFail: { buffer in + let attrs = try buffer.readNFSOptional { buffer in + try buffer.readNFSFileAttr() + } + return NFS3ReplyLookup.Fail(dirAttributes: attrs) + }) + ) + } + + public mutating func writeNFSReplyLookup(_ lookupResult: NFS3ReplyLookup) { + switch lookupResult.result { + case .okay(let result): + self.writeInteger(NFS3Status.ok.rawValue, endianness: .big) + self.writeNFSFileHandle(result.fileHandle) + if let attrs = result.attributes { + self.writeInteger(1, endianness: .big, as: UInt32.self) + self.writeNFSFileAttr(attrs) + } else { + self.writeInteger(0, endianness: .big, as: UInt32.self) + } + if let attrs = result.dirAttributes { + self.writeInteger(1, endianness: .big, as: UInt32.self) + self.writeNFSFileAttr(attrs) + } else { + self.writeInteger(0, endianness: .big, as: UInt32.self) + } + case .fail(let status, let fail): + precondition(status != .ok) + self.writeInteger(status.rawValue, endianness: .big) + if let attrs = fail.dirAttributes { + self.writeInteger(1, endianness: .big, as: UInt32.self) + self.writeNFSFileAttr(attrs) + } else { + self.writeInteger(0, endianness: .big, as: UInt32.self) + } + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+Mount.swift b/Sources/NIONFS3/NFSTypes+Mount.swift new file mode 100644 index 00000000..b64bf806 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Mount.swift @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - Mount +public struct NFS3CallMount: Equatable { + public init(dirPath: String) { + self.dirPath = dirPath + } + + public var dirPath: String +} + +public struct NFS3ReplyMount: Equatable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Equatable { + 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 readNFSCallMount() throws -> NFS3CallMount { + let dirPath = try self.readNFSString() + return NFS3CallMount(dirPath: dirPath) + } + + public mutating func writeNFSCallMount(_ call: NFS3CallMount) { + self.writeNFSString(call.dirPath) + } + + public mutating func writeNFSReplyMount(_ reply: NFS3ReplyMount) { + self.writeNFSResultStatus(reply.result) + + switch reply.result { + case .okay(let reply): + self.writeNFSFileHandle(reply.fileHandle) + precondition(reply.authFlavors == [.unix] || reply.authFlavors == [.noAuth], + "Sorry, anything but [.unix] / [.system] / [.noAuth] unimplemented.") + self.writeInteger(UInt32(reply.authFlavors.count), endianness: .big, as: UInt32.self) + for flavor in reply.authFlavors { + self.writeInteger(flavor.rawValue, endianness: .big, as: UInt32.self) + } + case .fail(_, _): + () + } + } + + public mutating func readNFSReplyMount() throws -> NFS3ReplyMount { + return NFS3ReplyMount(result: try self.readNFSResult(readOkay: { buffer in + let fileHandle = try buffer.readNFSFileHandle() + let authFlavors = try buffer.readNFSList(readEntry: { buffer in + try buffer.readRPCAuthFlavor() + }) + return .init(fileHandle: fileHandle, authFlavors: authFlavors) + + }, + readFail: { _ in NFS3Nothing() })) + } +} diff --git a/Sources/NIONFS3/NFSTypes+Null.swift b/Sources/NIONFS3/NFSTypes+Null.swift new file mode 100644 index 00000000..1c6040de --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Null.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - Null +public struct NFS3CallNull: Equatable { + public init() {} +} + +extension ByteBuffer { + public mutating func readNFSCallNull() throws -> NFS3CallNull { + return NFS3CallNull() + } + + public mutating func writeNFSCallNull(_ call: NFS3CallNull) { + } +} diff --git a/Sources/NIONFS3/NFSTypes+PathConf.swift b/Sources/NIONFS3/NFSTypes+PathConf.swift new file mode 100644 index 00000000..c8844874 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+PathConf.swift @@ -0,0 +1,118 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - PathConf +public struct NFS3CallPathConf: Equatable { + public init(object: NFS3FileHandle) { + self.object = object + } + + public var object: NFS3FileHandle +} + +public struct NFS3ReplyPathConf: Equatable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Equatable { + 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: Equatable { + public init(attributes: NFS3FileAttr?) { + self.attributes = attributes + } + + public var attributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFSCallPathConf() throws -> NFS3CallPathConf { + let fileHandle = try self.readNFSFileHandle() + return NFS3CallPathConf(object: fileHandle) + } + + public mutating func writeNFSCallPathConf(_ call: NFS3CallPathConf) { + self.writeNFSFileHandle(call.object) + } + + public mutating func readNFSReplyPathConf() throws -> NFS3ReplyPathConf { + return NFS3ReplyPathConf( + result: try self.readNFSResult( + readOkay: { buffer in + let attrs = try buffer.readNFSOptional { buffer in + try buffer.readNFSFileAttr() + } + 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.readNFSOptional { buffer in + try buffer.readNFSFileAttr() + } + return NFS3ReplyPathConf.Fail(attributes: attrs) + }) + ) + } + + public mutating func writeNFSReplyPathConf(_ pathconf: NFS3ReplyPathConf) { + self.writeNFSResultStatus(pathconf.result) + + switch pathconf.result { + case .okay(let pathconf): + self.writeNFSOptional(pathconf.attributes, writer: { $0.writeNFSFileAttr($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): + self.writeNFSOptional(fail.attributes, writer: { $0.writeNFSFileAttr($1) }) + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+Read.swift b/Sources/NIONFS3/NFSTypes+Read.swift new file mode 100644 index 00000000..e4d7ef94 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Read.swift @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - Read +public struct NFS3CallRead: Equatable { + 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: Equatable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Equatable { + 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: Equatable { + public init(attributes: NFS3FileAttr? = nil) { + self.attributes = attributes + } + + public var attributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFSCallRead() throws -> NFS3CallRead { + let fileHandle = try self.readNFSFileHandle() + guard let values = self.readMultipleIntegers(as: (NFS3Offset, NFS3Count).self) else { + throw NFS3Error.illegalRPCTooShort + } + + return NFS3CallRead(fileHandle: fileHandle, offset: values.0, count: values.1) + } + + public mutating func writeNFSCallRead(_ call: NFS3CallRead) { + self.writeNFSFileHandle(call.fileHandle) + self.writeMultipleIntegers(call.offset, call.count, endianness: .big) + } + + public mutating func readNFSReplyRead() throws -> NFS3ReplyRead { + return NFS3ReplyRead( + result: try self.readNFSResult( + readOkay: { buffer in + let attrs = try buffer.readNFSOptional { buffer in + try buffer.readNFSFileAttr() + } + guard let values = buffer.readMultipleIntegers(as: (UInt32, UInt32).self) else { + throw NFS3Error.illegalRPCTooShort + } + let bytes = try buffer.readNFSBlob() + return NFS3ReplyRead.Okay(attributes: attrs, + count: values.0, + eof: values.1 == 0 ? false : true, + data: bytes) + }, + readFail: { buffer in + let attrs = try buffer.readNFSOptional { buffer in + try buffer.readNFSFileAttr() + } + return NFS3ReplyRead.Fail(attributes: attrs) + }) + ) + } + + public mutating func writeNFSReplyReadPartially(_ read: NFS3ReplyRead) -> NFS3PartialWriteNextStep { + switch read.result { + case .okay(let result): + self.writeInteger(NFS3Status.ok.rawValue, endianness: .big) + self.writeNFSOptional(result.attributes, writer: { $0.writeNFSFileAttr($1) }) + self.writeMultipleIntegers( + result.count, + 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, endianness: .big) + self.writeNFSOptional(fail.attributes, writer: { $0.writeNFSFileAttr($1) }) + return .doNothing + } + } + + public mutating func writeNFSReplyRead(_ read: NFS3ReplyRead) { + switch self.writeNFSReplyReadPartially(read) { + case .doNothing: + () + case .writeBlob(let blob, numberOfFillBytes: let fillBytes): + self.writeImmutableBuffer(blob) + self.writeRepeatingByte(0x41, count: fillBytes) + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+ReadDirPlus.swift b/Sources/NIONFS3/NFSTypes+ReadDirPlus.swift new file mode 100644 index 00000000..6cc4088a --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+ReadDirPlus.swift @@ -0,0 +1,171 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - ReadDirPlus +public struct NFS3CallReadDirPlus: Equatable { + public init(fileHandle: NFS3FileHandle, cookie: NFS3Cookie, cookieVerifier: NFS3CookieVerifier, dirCount: UInt32, maxCount: UInt32) { + 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: UInt32 + public var maxCount: UInt32 +} + +public struct NFS3ReplyReadDirPlus: Equatable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Entry: Equatable { + 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: Equatable { + 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: Equatable { + public init(dirAttributes: NFS3FileAttr? = nil) { + self.dirAttributes = dirAttributes + } + + public var dirAttributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFSCallReadDirPlus() throws -> NFS3CallReadDirPlus { + let dir = try self.readNFSFileHandle() + let cookie = try self.readNFSInteger(as: UInt64.self) + let cookieVerifier = try self.readNFSInteger(as: UInt64.self) + let dirCount = try self.readNFSInteger(as: UInt32.self) + let maxCount = try self.readNFSInteger(as: UInt32.self) + + return NFS3CallReadDirPlus(fileHandle: dir, + cookie: cookie, + cookieVerifier: cookieVerifier, + dirCount: dirCount, + maxCount: maxCount) + } + + public mutating func writeNFSCallReadDirPlus(_ call: NFS3CallReadDirPlus) { + self.writeNFSFileHandle(call.fileHandle) + self.writeMultipleIntegers( + call.cookie, + call.cookieVerifier, + call.dirCount, + call.maxCount + ) + } + + private mutating func readReadDirPlusEntry() throws -> NFS3ReplyReadDirPlus.Entry { + let fileID = try self.readNFSInteger(as: NFS3FileID.self) + let fileName = try self.readNFSString() + let cookie = try self.readNFSInteger(as: NFS3Cookie.self) + let nameAttrs = try self.readNFSOptional { try $0.readNFSFileAttr() } + let nameHandle = try self.readNFSOptional { try $0.readNFSFileHandle() } + + return NFS3ReplyReadDirPlus.Entry(fileID: fileID, + fileName: fileName, + cookie: cookie, + nameAttributes: nameAttrs, + nameHandle: nameHandle) + } + + private mutating func writeReadDirPlusEntry(_ entry: NFS3ReplyReadDirPlus.Entry) { + self.writeNFSFileID(entry.fileID) + self.writeNFSString(entry.fileName) + self.writeNFSCookie(entry.cookie) + self.writeNFSOptional(entry.nameAttributes, writer: { $0.writeNFSFileAttr($1) }) + self.writeNFSOptional(entry.nameHandle, writer: { $0.writeNFSFileHandle($1) }) + } + + public mutating func readNFSReplyReadDirPlus() throws -> NFS3ReplyReadDirPlus { + return NFS3ReplyReadDirPlus( + result: try self.readNFSResult( + readOkay: { buffer in + let attrs = try buffer.readNFSOptional { try $0.readNFSFileAttr() } + let cookieVerifier = try buffer.readNFSInteger(as: NFS3CookieVerifier.self) + + var entries: [NFS3ReplyReadDirPlus.Entry] = [] + while let entry = try buffer.readNFSOptional({ try $0.readReadDirPlusEntry() }) { + entries.append(entry) + } + let eof = try buffer.readNFSBool() + + return NFS3ReplyReadDirPlus.Okay(dirAttributes: attrs, + cookieVerifier: cookieVerifier, + entries: entries, + eof: eof) + }, + readFail: { buffer in + let attrs = try buffer.readNFSOptional { try $0.readNFSFileAttr() } + + return NFS3ReplyReadDirPlus.Fail(dirAttributes: attrs) + }) + ) + } + + public mutating func writeNFSReplyReadDirPlus(_ rdp: NFS3ReplyReadDirPlus) { + switch rdp.result { + case .okay(let result): + self.writeInteger(NFS3Status.ok.rawValue, endianness: .big) + self.writeNFSOptional(result.dirAttributes, writer: { $0.writeNFSFileAttr($1) }) + self.writeNFSCookieVerifier(result.cookieVerifier) + for entry in result.entries { + self.writeInteger(1, endianness: .big, as: UInt32.self) + self.writeReadDirPlusEntry(entry) + } + self.writeInteger(0, endianness: .big, as: UInt32.self) + self.writeInteger(result.eof == true ? 1 : 0, endianness: .big, as: UInt32.self) + case .fail(let status, let fail): + precondition(status != .ok) + self.writeInteger(status.rawValue, endianness: .big) + self.writeNFSOptional(fail.dirAttributes, writer: { $0.writeNFSFileAttr($1) }) + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+Readlink.swift b/Sources/NIONFS3/NFSTypes+Readlink.swift new file mode 100644 index 00000000..d6ddb99d --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Readlink.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - Readlink +public struct NFS3CallReadlink: Equatable { + public init(symlink: NFS3FileHandle) { + self.symlink = symlink + } + + public var symlink: NFS3FileHandle +} + +public struct NFS3ReplyReadlink: Equatable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Equatable { + public init(symlinkAttributes: NFS3FileAttr? = nil, target: String) { + self.symlinkAttributes = symlinkAttributes + self.target = target + } + + public var symlinkAttributes: NFS3FileAttr? + public var target: String + } + + public struct Fail: Equatable { + public init(symlinkAttributes: NFS3FileAttr? = nil) { + self.symlinkAttributes = symlinkAttributes + } + + public var symlinkAttributes: NFS3FileAttr? + } + + public var result: NFS3Result +} + +extension ByteBuffer { + public mutating func readNFSCallReadlink() throws -> NFS3CallReadlink { + let symlink = try self.readNFSFileHandle() + + return .init(symlink: symlink) + } + + public mutating func writeNFSCallReadlink(_ call: NFS3CallReadlink) { + self.writeNFSFileHandle(call.symlink) + } + + public mutating func readNFSReplyReadlink() throws -> NFS3ReplyReadlink { + return NFS3ReplyReadlink( + result: try self.readNFSResult( + readOkay: { buffer in + let attrs = try buffer.readNFSOptional { try $0.readNFSFileAttr() } + let target = try buffer.readNFSString() + + return NFS3ReplyReadlink.Okay(symlinkAttributes: attrs, target: target) + }, + readFail: { buffer in + let attrs = try buffer.readNFSOptional { try $0.readNFSFileAttr() } + return NFS3ReplyReadlink.Fail(symlinkAttributes: attrs) + })) + } + + public mutating func writeNFSReplyReadlink(_ reply: NFS3ReplyReadlink) { + self.writeNFSResultStatus(reply.result) + + switch reply.result { + case .okay(let okay): + self.writeNFSOptional(okay.symlinkAttributes, writer: { $0.writeNFSFileAttr($1) }) + self.writeNFSString(okay.target) + case .fail(_, let fail): + self.writeNFSOptional(fail.symlinkAttributes, writer: { $0.writeNFSFileAttr($1) }) + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+SetAttr.swift b/Sources/NIONFS3/NFSTypes+SetAttr.swift new file mode 100644 index 00000000..9af03af5 --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+SetAttr.swift @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - Setattr +public struct NFS3CallSetattr: Equatable { + public init(object: NFS3FileHandle, newAttributes: NFS3CallSetattr.Attributes, guard: NFS3Time? = nil) { + self.object = object + self.newAttributes = newAttributes + self.guard = `guard` + } + + public struct Attributes: Equatable { + 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: Equatable { + public init(result: NFS3Result) { + self.result = result + } + + public struct Okay: Equatable { + public init(wcc: NFS3WeakCacheConsistencyData) { + self.wcc = wcc + } + + public var wcc: NFS3WeakCacheConsistencyData + } + + public struct Fail: Equatable { + public init(wcc: NFS3WeakCacheConsistencyData) { + self.wcc = wcc + } + + public var wcc: NFS3WeakCacheConsistencyData + } + + public var result: NFS3Result +} + +extension ByteBuffer { + private mutating func readNFSCallSetattrAttributes() throws -> NFS3CallSetattr.Attributes { + let mode = try self.readNFSOptional { try $0.readNFSInteger(as: UInt32.self) } + let uid = try self.readNFSOptional { try $0.readNFSInteger(as: UInt32.self) } + let gid = try self.readNFSOptional { try $0.readNFSInteger(as: UInt32.self) } + let size = try self.readNFSOptional { try $0.readNFSInteger(as: UInt64.self) } + let atime = try self.readNFSOptional { try $0.readNFSTime() } + let mtime = try self.readNFSOptional { try $0.readNFSTime() } + + return .init(mode: mode, uid: uid, gid: gid, size: size, atime: atime, mtime: mtime) + } + + private mutating func writeNFSCallSetattrAttributes(_ attrs: NFS3CallSetattr.Attributes) { + self.writeNFSOptional(attrs.mode, writer: { $0.writeInteger($1, endianness: .big) }) + self.writeNFSOptional(attrs.uid, writer: { $0.writeInteger($1, endianness: .big) }) + self.writeNFSOptional(attrs.gid, writer: { $0.writeInteger($1, endianness: .big) }) + self.writeNFSOptional(attrs.size, writer: { $0.writeInteger($1, endianness: .big) }) + self.writeNFSOptional(attrs.atime, writer: { $0.writeNFSTime($1) }) + self.writeNFSOptional(attrs.mtime, writer: { $0.writeNFSTime($1) }) + } + + public mutating func readNFSCallSetattr() throws -> NFS3CallSetattr { + let object = try self.readNFSFileHandle() + let attributes = try self.readNFSCallSetattrAttributes() + let `guard` = try self.readNFSOptional { try $0.readNFSTime() } + + return .init(object: object, newAttributes: attributes, guard: `guard`) + } + + public mutating func writeNFSCallSetattr(_ call: NFS3CallSetattr) { + self.writeNFSFileHandle(call.object) + self.writeNFSCallSetattrAttributes(call.newAttributes) + self.writeNFSOptional(call.guard, writer: { $0.writeNFSTime($1) }) + } + + public mutating func readNFSReplySetattr() throws -> NFS3ReplySetattr { + return NFS3ReplySetattr( + result: try self.readNFSResult( + readOkay: { buffer in + return NFS3ReplySetattr.Okay(wcc: try buffer.readNFSWeakCacheConsistencyData()) + }, + readFail: { buffer in + return NFS3ReplySetattr.Fail(wcc: try buffer.readNFSWeakCacheConsistencyData()) + })) + } + + public mutating func writeNFSReplySetattr(_ reply: NFS3ReplySetattr) { + self.writeNFSResultStatus(reply.result) + + switch reply.result { + case .okay(let okay): + self.writeNFSWeakCacheConsistencyData(okay.wcc) + case .fail(_, let fail): + self.writeNFSWeakCacheConsistencyData(fail.wcc) + } + } +} diff --git a/Sources/NIONFS3/NFSTypes+Unmount.swift b/Sources/NIONFS3/NFSTypes+Unmount.swift new file mode 100644 index 00000000..1348176d --- /dev/null +++ b/Sources/NIONFS3/NFSTypes+Unmount.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 + +// MARK: - Unmount +public struct NFS3CallUnmount: Equatable { + public init(dirPath: String) { + self.dirPath = dirPath + } + + public var dirPath: String +} + +public struct NFS3ReplyUnmount: Equatable { + public init() {} +} + +extension ByteBuffer { + public mutating func readNFSCallUnmount() throws -> NFS3CallUnmount { + let dirPath = try self.readNFSString() + return NFS3CallUnmount(dirPath: dirPath) + } + + public mutating func writeNFSCallUnmount(_ call: NFS3CallUnmount) { + self.writeNFSString(call.dirPath) + } + + public mutating func writeNFSReplyUnmount(_ reply: NFS3ReplyUnmount) { + } + + public mutating func readNFSReplyUnmount() throws -> NFS3ReplyUnmount { + return NFS3ReplyUnmount() + } +} diff --git a/Sources/NIONFS3/RPCTypes.swift b/Sources/NIONFS3/RPCTypes.swift new file mode 100644 index 00000000..223951be --- /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-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 + +public struct RPCFragmentHeader { + 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 { + case call = 0 + case reply = 1 +} + +/// RFC 5531: struct rpc_msg +public enum RPCMessage { + 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: Equatable { + 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: RPCNFSProcedureID { + get { + return RPCNFSProcedureID(program: self.program, procedure: self.procedure) + } + set { + self.program = newValue.program + self.procedure = newValue.procedure + } + } +} + +public enum RPCReplyStatus: Equatable { + case messageAccepted(RPCAcceptedReply) + case messageDenied(RPCRejectedReply) +} + +public struct RPCReply: Equatable { + public var xid: UInt32 + public var status: RPCReplyStatus + + public init(xid: UInt32, status: RPCReplyStatus) { + self.xid = xid + self.status = status + } +} + +public enum RPCAcceptedReplyStatus: Equatable { + case success + case programUnavailable + case programMismatch(low: UInt32, high: UInt32) + case procedureUnavailable + case garbageArguments + case systemError +} + +public struct RPCOpaqueAuth: Equatable { + 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: Equatable { + public var verifier: RPCOpaqueAuth + public var status: RPCAcceptedReplyStatus + + public init(verifier: RPCOpaqueAuth, status: RPCAcceptedReplyStatus) { + self.verifier = verifier + self.status = status + } +} + +public enum RPCAuthStatus: UInt32 { + 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: Equatable { + 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: Equatable { + 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 { + 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/NIONFS3Tests/NFS3ReplyEncoderTest.swift b/Tests/NIONFS3Tests/NFS3ReplyEncoderTest.swift new file mode 100644 index 00000000..6c52e7a6 --- /dev/null +++ b/Tests/NIONFS3Tests/NFS3ReplyEncoderTest.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 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: 7, + eof: false, + data: expectedPayload))))) + + var partialSerialisation = ByteBuffer() + let (bytesWritten, nextStep) = partialSerialisation.writeRPCNFSReplyPartially(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() + fullSerialisation.writeRPCNFSReply(reply) + + 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, + endianness: .big, + 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: 7, + eof: false, + data: expectedPayload))))) + + var fullSerialisation = ByteBuffer() + fullSerialisation.writeRPCNFSReply(expectedReply) + guard var actualReply = try? fullSerialisation.readRPCMessage() else { + XCTFail("could not read RPC message") + return + } + XCTAssertEqual(0, fullSerialisation.readableBytes) + var actualNFSReply: NFS3ReplyRead? = nil + XCTAssertNoThrow(actualNFSReply = try actualReply.1.readNFSReplyRead()) + XCTAssertEqual(0, actualReply.1.readableBytes) + XCTAssertEqual(expectedReply.nfsReply, + actualNFSReply.map { .read($0) }, + "parsing failed for payload length \(payloadLength)") + } + } +} diff --git a/Tests/NIONFS3Tests/NFS3RoundtripTests.swift b/Tests/NIONFS3Tests/NFS3RoundtripTests.swift new file mode 100644 index 00000000..6611f5bf --- /dev/null +++ b/Tests/NIONFS3Tests/NFS3RoundtripTests.swift @@ -0,0 +1,225 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-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 NIOTestUtils +import XCTest +import NIONFS3 + +final class NFS3RoundtripTests: XCTestCase { + func testRegularCallsRoundtrip() { + let mountCall1 = NFS3Call.mount(NFS3CallMount(dirPath: "/hellö/this is/a cOmplicatedPath⚠️")) + let mountCall2 = NFS3Call.mount(NFS3CallMount(dirPath: "")) + let unmountCall1 = NFS3Call.unmount(NFS3CallUnmount(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 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 rpcNFSCall = RPCNFS3Call(nfsCall: nfsCall, xid: xid) + buffer.writeRPCNFSCall(rpcNFSCall) + + return (buffer, [rpcNFSCall]) + } + + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder(inputOutputPairs: [ + makeInputOutputPair(mountCall1), + makeInputOutputPair(mountCall2), + makeInputOutputPair(unmountCall1), + makeInputOutputPair(accessCall1), + makeInputOutputPair(fsInfoCall1), + makeInputOutputPair(fsStatCall1), + makeInputOutputPair(getattrCall1), + makeInputOutputPair(lookupCall1), + makeInputOutputPair(nullCall1), + makeInputOutputPair(pathConfCall1), + makeInputOutputPair(readCall1), + 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: .max, count: .max)) + let readDirPlusCall1 = NFS3Call.readdirplus(.init(fileHandle: NFS3FileHandle(.max), cookie: .max, cookieVerifier: .max, dirCount: .max, maxCount: .max)) + let readlinkCall1 = NFS3Call.readlink(.init(symlink: NFS3FileHandle(.max))) + let setattrCall1 = NFS3Call.setattr(.init(object: NFS3FileHandle(.max), + newAttributes: .init(mode: .max, + uid: .max, gid: .max, + size: .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 rpcNFSCall = RPCNFS3Call(nfsCall: nfsCall, xid: xid) + buffer.writeRPCNFSCall(rpcNFSCall) + + return (buffer, [rpcNFSCall]) + } + + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder(inputOutputPairs: [ + makeInputOutputPair(accessCall1), + makeInputOutputPair(fsInfoCall1), + makeInputOutputPair(fsStatCall1), + makeInputOutputPair(getattrCall1), + makeInputOutputPair(lookupCall1), + makeInputOutputPair(pathConfCall1), + makeInputOutputPair(readCall1), + makeInputOutputPair(readDirPlusCall1), + makeInputOutputPair(readlinkCall1), + makeInputOutputPair(setattrCall1), + ], + decoderFactory: { NFS3CallDecoder() })) + } + + func testRegularOkayRepliesRoundtrip() { + func makeRandomFileAttr() -> NFS3FileAttr { + return .init(type: .init(rawValue: .random(in: 1 ... 7))!, + mode: .random(in: 0o000 ... 0o777), + nlink: .random(in: .min ... .max), + uid: .random(in: .min ... .max), + gid: .random(in: .min ... .max), + size: .random(in: .min ... .max), + used: .random(in: .min ... .max), + rdev: .random(in: .min ... .max), + fsid: .random(in: .min ... .max), + fileid: .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 mountReply1 = NFS3Reply.mount(NFS3ReplyMount(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: .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: .random(in: .min ... .max), + fbytes: .random(in: .min ... .max), + abytes: .random(in: .min ... .max), + tfiles: .random(in: .min ... .max), + ffiles: .random(in: .min ... .max), + afiles: .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: .random(in: .min ... .max), + eof: .random(), + data: ByteBuffer(string: "abc"))))) + let readDirPlusReply1 = NFS3Reply.readdirplus(.init(result: .okay(.init(dirAttributes: makeRandomFileAttr(), + cookieVerifier: .random(in: .min ... .max), + entries: [.init(fileID: .random(in: .min ... .max), + fileName: "asd", + cookie: .random(in: .min ... .max), + nameAttributes: makeRandomFileAttr(), + nameHandle: NFS3FileHandle(.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: .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: RPCNFSProcedureID] = [:] + func makeInputOutputPair(_ nfsReply: NFS3Reply) -> (ByteBuffer, [RPCNFS3Reply]) { + var buffer = ByteBuffer() + xid += 1 + let rpcNFSReply = RPCNFS3Reply(rpcReply: .init(xid: xid, status: .messageAccepted(.init(verifier: .init(flavor: .noAuth, opaque: nil), status: .success))), + nfsReply: nfsReply) + prepopulatedProcs[xid] = .init(nfsReply) + buffer.writeRPCNFSReply(rpcNFSReply) + + return (buffer, [rpcNFSReply]) + } + + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder(inputOutputPairs: [ + 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(readlinkReply1), + makeInputOutputPair(setattrReply1), + ], + decoderFactory: { NFS3ReplyDecoder(prepopulatedProcecedures: prepopulatedProcs, + allowDuplicateReplies: true) })) + } + + +}