From 275195436c9604668b5ab95bfb5a907c93ffd63c Mon Sep 17 00:00:00 2001 From: Amir Abbas Mousavian Date: Thu, 21 Dec 2023 19:17:42 +0330 Subject: [PATCH] feat: Create symbolic link fix: Get symbolic link attributes could not be fetched fix: Build issue on Swift 5.8 fix: Including windows error number in description when converting to posix error fails fix: Crash when IOCTL response is corrupt chore: Fsctl command organization --- AMSMB2/AMSMB2.swift | 69 +++++++++++++++++-- AMSMB2/Context.swift | 15 ++++- AMSMB2/Extensions.swift | 4 +- AMSMB2/FileHandle.swift | 23 ++++--- AMSMB2/Fsctl.swift | 103 ++++++++++++++++++++++------- AMSMB2/Parsers.swift | 12 +++- AMSMB2Tests/SMB2ManagerTests.swift | 8 +-- 7 files changed, 185 insertions(+), 49 deletions(-) diff --git a/AMSMB2/AMSMB2.swift b/AMSMB2/AMSMB2.swift index e45a67d..83ad419 100644 --- a/AMSMB2/AMSMB2.swift +++ b/AMSMB2/AMSMB2.swift @@ -490,12 +490,30 @@ public class SMB2Manager: NSObject, NSSecureCoding, Codable, NSCopying, CustomRe completionHandler: @Sendable @escaping (_ result: Result<[URLResourceKey: Any], any Error>) -> Void ) { with(completionHandler: completionHandler) { context in - let stat = try context.stat(path) - var result = [URLResourceKey: Any]() - result[.nameKey] = path.fileURL().lastPathComponent - result[.pathKey] = path.fileURL(stat.isDirectory).path - stat.populateResourceValue(&result) - return result + do { + let stat = try context.stat(path) + var result = [URLResourceKey: Any]() + result[.nameKey] = path.fileURL().lastPathComponent + result[.pathKey] = path.fileURL(stat.isDirectory).path + stat.populateResourceValue(&result) + return result + } catch POSIXError.ENOLINK { + // `libsmb2` can not read symlink attributes, so if we get + // the related error, we simply check if the given file is + // a symbolic link by `readlink`. + // If so, we set the attributes for a symbolic link. + _ = try context.readlink(path) + var result = [URLResourceKey: Any]() + result[.nameKey] = path.fileURL().lastPathComponent + result[.pathKey] = path.fileURL(false).path + result[.fileResourceTypeKey] = URLFileResourceType.symbolicLink + result[.isDirectoryKey] = NSNumber(false) + result[.isRegularFileKey] = NSNumber(false) + result[.isSymbolicLinkKey] = NSNumber(true) + return result + } catch { + throw error + } } } @@ -527,7 +545,11 @@ public class SMB2Manager: NSObject, NSSecureCoding, Codable, NSCopying, CustomRe - path: The path of a file or directory. - completionHandler: closure will be run after operation is completed. */ - open func setAttributes(attributes: [URLResourceKey: Any], ofItemAtPath path: String, completionHandler: SimpleCompletionHandler) { + open func setAttributes( + attributes: [URLResourceKey: Any], + ofItemAtPath path: String, + completionHandler: SimpleCompletionHandler + ) { var stat = smb2_stat_64() var smb2Attributes = SMB2FileAttributes() for attribute in attributes { @@ -594,6 +616,38 @@ public class SMB2Manager: NSObject, NSSecureCoding, Codable, NSCopying, CustomRe setAttributes(attributes: attributes, ofItemAtPath: path, completionHandler: asyncHandler(continuation)) } } + + /** + Creates a new symbolic link pointed to given destination. + + - Parameters: + - path: The path of a file or directory. + - destination: Item that symbolic link will point to. + - completionHandler: closure will be run after reading link is completed. + */ + func createSymbolicLink( + atPath path: String, withDestinationPath destination: String, + completionHandler: SimpleCompletionHandler + ) { + with(completionHandler: completionHandler) { context in + try context.symlink(path, to: destination) + } + } + + /** + Returns the path of the item pointed to by a symbolic link. + + - Parameters: + - atPath: The path of a file or directory. + - completionHandler: closure will be run after reading link is completed. + - Returns: An String object containing the path of the directory or file to which the symbolic link path refers. + If the symbolic link is specified as a relative path, that relative path is returned. + */ + func createSymbolicLink(atPath path: String, withDestinationPath destination: String) async throws { + try await withCheckedThrowingContinuation { continuation in + createSymbolicLink(atPath: path, withDestinationPath: destination, completionHandler: asyncHandler(continuation)) + } + } /** Returns the path of the item pointed to by a symbolic link. @@ -941,6 +995,7 @@ public class SMB2Manager: NSObject, NSSecureCoding, Codable, NSCopying, CustomRe - range: byte range that should be read, default value is whole file. e.g. `..<10` will read first ten bytes. - Returns: an async stream of `Data` object which contains file contents. */ + @available(swift 5.9) open func contents( atPath path: String, range: R? = Range?.none ) -> AsyncThrowingStream where R.Bound: FixedWidthInteger { diff --git a/AMSMB2/Context.swift b/AMSMB2/Context.swift index dcacffb..cd06e4b 100644 --- a/AMSMB2/Context.swift +++ b/AMSMB2/Context.swift @@ -2,7 +2,7 @@ // Context.swift // AMSMB2 // -// Created by Amir Abbas on 11/20/23. +// Created by Amir Abbas on 12/15/23. // Copyright © 2023 Mousavian. Distributed under MIT license. // All rights reserved. // @@ -272,6 +272,19 @@ extension SMB2Context { smb2_readlink_async(context, path.canonical, SMB2Context.generic_handler, cbPtr) }.data } + + func symlink(_ path: String, to destination: String) throws { + let file = try SMB2FileHandle.using( + path: path, + desiredAccess: .init(bitPattern: SMB2_GENERIC_READ) | SMB2_GENERIC_WRITE, + shareAccess: SMB2_FILE_SHARE_READ | SMB2_FILE_SHARE_WRITE, + createDisposition: SMB2_FILE_CREATE, + createOptions: SMB2_FILE_OPEN_REPARSE_POINT, + on: self + ) + let reparse = IOCtl.SymbolicLinkReparse(path: destination, isRelative: true) + try file.fcntl(command: .setReparsePoint, args: reparse) + } } // MARK: File operation diff --git a/AMSMB2/Extensions.swift b/AMSMB2/Extensions.swift index 663d840..3d13f31 100644 --- a/AMSMB2/Extensions.swift +++ b/AMSMB2/Extensions.swift @@ -51,8 +51,8 @@ extension POSIXError { static func throwIfErrorStatus(_ status: UInt32) throws { if status & SMB2_STATUS_SEVERITY_MASK == SMB2_STATUS_SEVERITY_ERROR { let errorNo = nterror_to_errno(status) - let description = nterror_to_str(status).map(String.init(cString:)) - try POSIXError.throwIfError(-errorNo, description: description) + let description = nterror_to_str(status).map(String.init(cString:)) ?? "Unknown" + throw POSIXError(.init(errorNo), description: "Error 0x\(String(status, radix: 16, uppercase: true)): \(description)") } } diff --git a/AMSMB2/FileHandle.swift b/AMSMB2/FileHandle.swift index a67620e..81ff481 100644 --- a/AMSMB2/FileHandle.swift +++ b/AMSMB2/FileHandle.swift @@ -2,7 +2,7 @@ // FileHandle.swift // AMSMB2 // -// Created by Amir Abbas on 11/20/23. +// Created by Amir Abbas on 12/15/23. // Copyright © 2023 Mousavian. Distributed under MIT license. // All rights reserved. // @@ -103,8 +103,8 @@ final class SMB2FileHandle { } catch {} } - var fileId: smb2_file_id { - (try? smb2_get_file_id(handle.unwrap()).unwrap().pointee) ?? compound_file_id + var fileId: UUID { + .init(uuid: (try? smb2_get_file_id(handle.unwrap()).unwrap().pointee) ?? compound_file_id) } func close() { @@ -131,17 +131,22 @@ final class SMB2FileHandle { var bfi = smb2_file_basic_info( creation_time: smb2_timeval( tv_sec: .init(stat.smb2_btime), - tv_usec: .init(stat.smb2_btime_nsec / 1000)), + tv_usec: .init(stat.smb2_btime_nsec / 1000) + ), last_access_time: smb2_timeval( tv_sec: .init(stat.smb2_atime), - tv_usec: .init(stat.smb2_atime_nsec / 1000)), + tv_usec: .init(stat.smb2_atime_nsec / 1000) + ), last_write_time: smb2_timeval( tv_sec: .init(stat.smb2_mtime), - tv_usec: .init(stat.smb2_mtime_nsec / 1000)), + tv_usec: .init(stat.smb2_mtime_nsec / 1000) + ), change_time: smb2_timeval( tv_sec: .init(stat.smb2_ctime), - tv_usec: .init(stat.smb2_ctime_nsec / 1000)), - file_attributes: attributes.rawValue) + tv_usec: .init(stat.smb2_ctime_nsec / 1000) + ), + file_attributes: attributes.rawValue + ) var req = smb2_set_info_request() req.file_id = smb2_get_file_id(handle).pointee @@ -266,7 +271,7 @@ final class SMB2FileHandle { var inputBuffer = [UInt8](args) return try inputBuffer.withUnsafeMutableBytes { buf in var req = smb2_ioctl_request( - ctl_code: command.rawValue, file_id: fileId, input_count: .init(buf.count), + ctl_code: command.rawValue, file_id: fileId.uuid, input_count: .init(buf.count), input: buf.baseAddress, flags: .init(SMB2_0_IOCTL_IS_FSCTL) ) return try context.async_await_pdu(dataHandler: R.init) { diff --git a/AMSMB2/Fsctl.swift b/AMSMB2/Fsctl.swift index cb35f4f..300ea1f 100644 --- a/AMSMB2/Fsctl.swift +++ b/AMSMB2/Fsctl.swift @@ -45,6 +45,7 @@ extension IOCtlArgument { protocol IOCtlReply { init(data: Data) throws + init(_ context: SMB2Context, _ dataPtr: UnsafeMutableRawPointer?) throws } struct AnyIOCtlReply: IOCtlReply { @@ -59,31 +60,43 @@ enum IOCtl { struct Command: RawRepresentable, Equatable, Hashable { var rawValue: UInt32 - static let dfsGetReferrals = Command(SMB2_FSCTL_DFS_GET_REFERRALS) - static let pipePeek = Command(SMB2_FSCTL_PIPE_PEEK) - static let pipeWait = Command(SMB2_FSCTL_PIPE_WAIT) - static let pipeTransceive = Command(SMB2_FSCTL_PIPE_TRANSCEIVE) - static let srvCopyChunk = Command(SMB2_FSCTL_SRV_COPYCHUNK) - static let srvCopyChunkWrite = Command(SMB2_FSCTL_SRV_COPYCHUNK_WRITE) static let srvEnumerateSnapshots = Command(SMB2_FSCTL_SRV_ENUMERATE_SNAPSHOTS) - static let srvRequestResumeKey = Command(SMB2_FSCTL_SRV_REQUEST_RESUME_KEY) static let srvReadHash = Command(SMB2_FSCTL_SRV_READ_HASH) static let lmrRequestResiliency = Command(SMB2_FSCTL_LMR_REQUEST_RESILIENCY) static let queryNetworkInterfaceInfo = Command(SMB2_FSCTL_QUERY_NETWORK_INTERFACE_INFO) - static let getReparsePoint = Command(SMB2_FSCTL_GET_REPARSE_POINT) - static let setReparsePoint = Command(SMB2_FSCTL_SET_REPARSE_POINT) - static let deleteReparsePoint = Command(0x0009_00ac) static let fileLevelTrim = Command(SMB2_FSCTL_FILE_LEVEL_TRIM) static let validateNegotiateInfo = Command(SMB2_FSCTL_VALIDATE_NEGOTIATE_INFO) } +} + +// - MARK: Pipe +extension IOCtl.Command { + static let pipePeek = Self(SMB2_FSCTL_PIPE_PEEK) + static let pipeWait = Self(SMB2_FSCTL_PIPE_WAIT) + static let pipeTransceive = Self(SMB2_FSCTL_PIPE_TRANSCEIVE) +} + +// - MARK: DFS +extension IOCtl.Command { + static let dfsGetReferrals = Self(SMB2_FSCTL_DFS_GET_REFERRALS) + static let dfsGetReferralsEx = Self(SMB2_FSCTL_DFS_GET_REFERRALS_EX) +} + +// - MARK: Server-side Copy +extension IOCtl.Command { + static let srvCopyChunk = Self(SMB2_FSCTL_SRV_COPYCHUNK) + static let srvCopyChunkWrite = Self(SMB2_FSCTL_SRV_COPYCHUNK_WRITE) + static let srvRequestResumeKey = Self(SMB2_FSCTL_SRV_REQUEST_RESUME_KEY) +} +extension IOCtl { struct SrvCopyChunk: IOCtlArgument { typealias Element = UInt8 - + let sourceOffset: UInt64 let targetOffset: UInt64 let length: UInt32 - + var regions: [Data] { [ .init(value: sourceOffset), @@ -92,20 +105,20 @@ enum IOCtl { .init(value: 0 as UInt32), ] } - + init(sourceOffset: UInt64, targetOffset: UInt64, length: UInt32) { self.sourceOffset = sourceOffset self.targetOffset = targetOffset self.length = length } } - + struct SrvCopyChunkCopy: IOCtlArgument { typealias Element = UInt8 - + let sourceKey: Data let chunks: [SrvCopyChunk] - + var regions: [Data] { [ sourceKey, @@ -113,16 +126,16 @@ enum IOCtl { .init(value: 0 as UInt32), ] + chunks.flatMap(\.regions) } - + init(sourceKey: Data, chunks: [SrvCopyChunk]) { self.sourceKey = sourceKey self.chunks = chunks } } - + struct RequestResumeKey: IOCtlReply { let resumeKey: Data - + init(data: Data) throws { guard data.count >= 24 else { throw POSIXError(.ENODATA) @@ -130,15 +143,31 @@ enum IOCtl { self.resumeKey = data.prefix(24) } } +} +// - MARK: Reparse Point +extension IOCtl.Command { + static let getReparsePoint = Self(SMB2_FSCTL_GET_REPARSE_POINT) + static let setReparsePoint = Self(SMB2_FSCTL_SET_REPARSE_POINT) + static let deleteReparsePoint = Self(0x0009_00ac) +} + +extension IOCtl { struct SymbolicLinkReparse: IOCtlReply, IOCtlArgument { typealias Element = UInt8 private static let headerLength = 20 - private let reparseTag: UInt32 = 0xa000_000c + private let reparseTag: UInt32 = SMB2_REPARSE_TAG_SYMLINK let substituteName: String let printName: String let isRelative: Bool + + init(path: String, printName: String? = nil, isRelative: Bool = false) { + let path = path.replacingOccurrences(of: "/", with: "\\") + self.substituteName = path + self.printName = printName ?? path + self.isRelative = isRelative + } init(data: Data) throws { guard data.scanValue(offset: 0, as: UInt32.self) == reparseTag else { @@ -174,15 +203,15 @@ enum IOCtl { let printLen = UInt16(printData.count) return [ .init(value: reparseTag), - .init(value: substituteLen + printLen), + .init(value: UInt16(substituteData.count + printData.count)), .init(value: 0 as UInt16), // reserved - .init(value: printLen), // substitute offset + .init(value: 0 as UInt16), // substitute offset .init(value: substituteLen), - .init(value: 0 as UInt16), + .init(value: UInt16(substituteData.count)), // print offset .init(value: printLen), .init(value: UInt32(isRelative ? 1 : 0)), - .init(printData), .init(substituteData), + .init(printData), ] } @@ -192,6 +221,32 @@ enum IOCtl { self.isRelative = false } } + + struct SymbolicLinkGUIDReparse: IOCtlArgument { + typealias Element = UInt8 + + // This reparse data buffer MUST be used only with reparse tag values + // whose high bit is set to 0. + private let reparseTag: UInt32 = SMB2_REPARSE_TAG_SYMLINK & 0x7fff_ffff + let fileId: UUID + + init(fileId: UUID = .init()) { + self.fileId = fileId + } + + var regions: [Data] { + [ + .init(value: reparseTag), + .init(value: 0 as UInt16), // buffer length + .init(value: 0 as UInt16), // reserved + .init(value: fileId), + ] + } + + private init() { + self.fileId = .init() + } + } struct MountPointReparse: IOCtlReply, IOCtlArgument { typealias Element = UInt8 diff --git a/AMSMB2/Parsers.swift b/AMSMB2/Parsers.swift index 403a673..6411470 100644 --- a/AMSMB2/Parsers.swift +++ b/AMSMB2/Parsers.swift @@ -11,8 +11,9 @@ import Foundation import SMB2 import SMB2.Raw -struct EmptyReply { - init(_: SMB2Context, _ dataPtr: UnsafeMutableRawPointer?) throws { } +struct EmptyReply: IOCtlReply { + init(data _: Data) throws {} + init(_: SMB2Context, _: UnsafeMutableRawPointer?) throws {} } extension String { @@ -63,6 +64,13 @@ extension IOCtlReply { self = try Self(data: .init()) return } + // Check memory validity in order to prevent crash on invalid pointers + let pageSize = sysconf(_SC_PAGESIZE) + let base = UnsafeMutableRawPointer(bitPattern: (size_t(bitPattern: output) / pageSize) * pageSize) + if msync(base, pageSize, MS_ASYNC) != 0 { + self = try Self(data: .init()) + return + } defer { smb2_free_data(context.unsafe, output) } let data = Data(bytes: output, count: Int(reply.output_count)) self = try Self(data: data) diff --git a/AMSMB2Tests/SMB2ManagerTests.swift b/AMSMB2Tests/SMB2ManagerTests.swift index f1e82d5..3f81f1a 100644 --- a/AMSMB2Tests/SMB2ManagerTests.swift +++ b/AMSMB2Tests/SMB2ManagerTests.swift @@ -2,7 +2,7 @@ // SMB2ManagerTests.swift // AMSMB2 // -// Created by Amir Abbas on 11/20/23. +// Created by Amir Abbas on 12/21/23. // Copyright © 2023 Mousavian. Distributed under MIT license. // All rights reserved. // @@ -407,7 +407,7 @@ class SMB2ManagerTests: XCTestCase { .forEach(FileManager.default.removeItem(at:)) await withTaskGroup(of: Void.self) { group in for file in files { - group.addTask{ + group.addTask { try? await smb.removeFile(atPath: file) } } @@ -420,7 +420,7 @@ class SMB2ManagerTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in for (file, url) in zip(files, urls) { - group.addTask{ + group.addTask { try await smb.uploadItem(at: url, toPath: file, progress: nil) } } @@ -431,7 +431,7 @@ class SMB2ManagerTests: XCTestCase { guard redownload else { return } try await withThrowingTaskGroup(of: Void.self) { group in for (file, url) in zip(files, urls) { - group.addTask{ + group.addTask { try await smb.downloadItem(atPath: file, to: url.appendingPathExtension("download"), progress: nil) } }