Skip to content

Commit

Permalink
feat: Create symbolic link
Browse files Browse the repository at this point in the history
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
  • Loading branch information
amosavian committed Dec 21, 2023
1 parent 75c198c commit 2751954
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 49 deletions.
69 changes: 62 additions & 7 deletions AMSMB2/AMSMB2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<R: RangeExpression>(
atPath path: String, range: R? = Range<UInt64>?.none
) -> AsyncThrowingStream<Data, any Error> where R.Bound: FixedWidthInteger {
Expand Down
15 changes: 14 additions & 1 deletion AMSMB2/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions AMSMB2/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}

Expand Down
23 changes: 14 additions & 9 deletions AMSMB2/FileHandle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down Expand Up @@ -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() {
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
103 changes: 79 additions & 24 deletions AMSMB2/Fsctl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ extension IOCtlArgument {

protocol IOCtlReply {
init(data: Data) throws
init(_ context: SMB2Context, _ dataPtr: UnsafeMutableRawPointer?) throws
}

struct AnyIOCtlReply: IOCtlReply {
Expand All @@ -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),
Expand All @@ -92,53 +105,69 @@ 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,
.init(value: UInt32(chunks.count)),
.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)
}
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 {
Expand Down Expand Up @@ -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),
]
}

Expand All @@ -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
Expand Down
Loading

0 comments on commit 2751954

Please sign in to comment.