Skip to content

Commit

Permalink
feat: Delete symbolic link files
Browse files Browse the repository at this point in the history
fix: More attributes provided for symbolic links
chore: Convenience open func for SMB2FileHandler
  • Loading branch information
amosavian committed Dec 22, 2023
1 parent 2751954 commit 3dcb33c
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 32 deletions.
43 changes: 20 additions & 23 deletions AMSMB2/AMSMB2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -490,30 +490,20 @@ 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: smb2_stat_64
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
stat = try context.stat(path)
} 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
// `libsmb2` can not read symlink attributes using `stat`, so if we get
// the related error, we simply open file as reparse point then use `fstat`.
let file = try SMB2FileHandle.open(path: path, flags: O_RDONLY | O_SYMLINK, on: context)
stat = try file.fstat()
}
var result = [URLResourceKey: Any]()
result[.nameKey] = path.fileURL().lastPathComponent
result[.pathKey] = path.fileURL(stat.isDirectory).path
stat.populateResourceValue(&result)
return result
}
}

Expand Down Expand Up @@ -751,7 +741,12 @@ public class SMB2Manager: NSObject, NSSecureCoding, Codable, NSCopying, CustomRe
@objc(removeFileAtPath:completionHandler:)
open func removeFile(atPath path: String, completionHandler: SimpleCompletionHandler) {
with(completionHandler: completionHandler) { context in
try context.unlink(path)
do {
try context.unlink(path)
} catch POSIXError.ENOLINK {
// Try to remove file as a symbolic link.
try context.unlink(path, flags: O_SYMLINK)
}
}
}

Expand Down Expand Up @@ -780,8 +775,10 @@ public class SMB2Manager: NSObject, NSSecureCoding, Codable, NSCopying, CustomRe
switch try Int32(context.stat(path).smb2_type) {
case SMB2_TYPE_DIRECTORY:
try self.removeDirectory(context: context, path: path, recursive: true)
case SMB2_TYPE_FILE, SMB2_TYPE_LINK:
case SMB2_TYPE_FILE:
try context.unlink(path)
case SMB2_TYPE_LINK:
try context.unlink(path, flags: O_SYMLINK)
default:
break
}
Expand Down
30 changes: 22 additions & 8 deletions AMSMB2/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,7 @@ extension SMB2Context {
}

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 file = try SMB2FileHandle.open(path: path, flags: O_RDWR | O_CREAT | O_EXCL | O_SYMLINK, on: self)
let reparse = IOCtl.SymbolicLinkReparse(path: destination, isRelative: true)
try file.fcntl(command: .setReparsePoint, args: reparse)
}
Expand All @@ -307,6 +300,27 @@ extension SMB2Context {
smb2_unlink_async(context, path.canonical, SMB2Context.generic_handler, cbPtr)
}
}

func unlink(_ path: String, flags: Int32) throws {
let file = try SMB2FileHandle.open(path: path, flags: O_RDWR | flags, on: self)
var inputBuffer = [UInt8](repeating: 0, count: 8)
inputBuffer[0] = 0x01 // DeletePending set to true
try withExtendedLifetime(file) {
try inputBuffer.withUnsafeMutableBytes { buf in
var req = smb2_set_info_request(
info_type: UInt8(SMB2_0_INFO_FILE),
file_info_class: 0x0D,
input_data: buf.baseAddress,
additional_information: 0,
file_id: file.fileId.uuid)

try async_await_pdu(dataHandler: EmptyReply.init) {
context, cbPtr -> UnsafeMutablePointer<smb2_pdu>? in
smb2_cmd_set_info_async(context, &req, SMB2Context.generic_handler, cbPtr)
}
}
}
}

func rename(_ path: String, to newPath: String) throws {
try async_await { context, cbPtr -> Int32 in
Expand Down
51 changes: 51 additions & 0 deletions AMSMB2/FileHandle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,57 @@ final class SMB2FileHandle {

return result
}

static func open(path: String, flags: Int32, on context: SMB2Context) throws -> SMB2FileHandle {
let desiredAccess: Int32
let shareAccess: Int32
let createDisposition: Int32
var createOptions: Int32 = 0

switch flags & O_ACCMODE {
case O_RDWR:
desiredAccess = .init(bitPattern: SMB2_GENERIC_READ) | SMB2_GENERIC_WRITE | SMB2_DELETE
shareAccess = SMB2_FILE_SHARE_READ | SMB2_FILE_SHARE_WRITE
case O_WRONLY:
desiredAccess = SMB2_GENERIC_WRITE | SMB2_DELETE
shareAccess = SMB2_FILE_SHARE_WRITE
default:
desiredAccess = .init(bitPattern: SMB2_GENERIC_READ)
shareAccess = SMB2_FILE_SHARE_READ
}

if (flags & O_CREAT) != 0 {
if (flags & O_EXCL) != 0 {
createDisposition = SMB2_FILE_CREATE
} else if(flags & O_TRUNC) != 0 {
createDisposition = SMB2_FILE_OVERWRITE_IF
} else {
createDisposition = SMB2_FILE_OPEN_IF
}
} else {
if (flags & O_TRUNC) != 0 {
createDisposition = SMB2_FILE_OVERWRITE
} else {
createDisposition = SMB2_FILE_OPEN
}
}

if (flags & O_DIRECTORY) != 0 {
createOptions |= SMB2_FILE_DIRECTORY_FILE
}
if (flags & O_SYMLINK) != 0 {
createOptions |= SMB2_FILE_OPEN_REPARSE_POINT
}

return try SMB2FileHandle.using(
path: path,
desiredAccess: desiredAccess,
shareAccess: shareAccess,
createDisposition: createDisposition,
createOptions: createOptions,
on: context
)
}

init(fileDescriptor: smb2_file_id, on context: SMB2Context) throws {
self.context = context
Expand Down
33 changes: 33 additions & 0 deletions AMSMB2/Fsctl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,39 @@ extension IOCtl.Command {
}

extension IOCtl {
struct Reparse: IOCtlReply, IOCtlArgument {
typealias Element = UInt8
private static let headerLength = 8

let reparseTag: UInt32
let buffer: Data

var regions: [Data] {
return [
.init(value: reparseTag),
.init(value: UInt16(buffer.count)),
.init(value: 0 as UInt16), // reserved
.init(buffer),
]
}

init(reparseTag: UInt32, buffer: Data = .init()) {
self.reparseTag = reparseTag
self.buffer = buffer
}

init(data: Data) throws {
guard data.count >= Self.headerLength else {
throw POSIXError(.EINVAL)
}
self.reparseTag = data.scanValue(offset: 0, as: UInt32.self) ?? SMB2_REPARSE_TAG_SYMLINK

let count = try data.scanInt(offset: 4, as: UInt16.self).unwrap()
guard count + 8 == data.count else { throw POSIXError(.EINVAL) }
self.buffer = data.dropFirst(Int(Self.headerLength))
}
}

struct SymbolicLinkReparse: IOCtlReply, IOCtlArgument {
typealias Element = UInt8

Expand Down
47 changes: 47 additions & 0 deletions AMSMB2Tests/SMB2ManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,53 @@ class SMB2ManagerTests: XCTestCase {
)
}
}

func testCreateSymlink() async throws {
let smb = SMB2Manager(url: server, credential: credential)!
let target = "testSymlinkTarget.dat"
let link = "testSymlink.dat"
let data = randomData(size: 0x000800)

addTeardownBlock {
try? await smb.removeFile(atPath: target)
try? await smb.removeFile(atPath: link)
}

try await smb.connectShare(name: share, encrypted: encrypted)
try await smb.write(data: data, toPath: target, progress: nil)
try await smb.createSymbolicLink(atPath: link, withDestinationPath: target)

let attribs = try await smb.attributesOfItem(atPath: link)
XCTAssertNotNil(attribs.contentModificationDate)
XCTAssertNotNil(attribs.creationDate)
XCTAssert(attribs.isSymbolicLink)
XCTAssertEqual(attribs.fileResourceType, URLFileResourceType.symbolicLink)

let destination = try await smb.destinationOfSymbolicLink(atPath: link)
XCTAssertEqual(destination, target)
}

func testRemoveSymlink() async throws {
let smb = SMB2Manager(url: server, credential: credential)!
let target = "testRemoveSymlinkTarget.dat"
let link = "testRemoveSymlink.dat"
let data = randomData(size: 0x000800)

addTeardownBlock {
try? await smb.removeFile(atPath: target)
try? await smb.removeFile(atPath: link)
}

try await smb.connectShare(name: share, encrypted: encrypted)
try await smb.write(data: data, toPath: target, progress: nil)
try await smb.createSymbolicLink(atPath: link, withDestinationPath: target)
try await smb.removeFile(atPath: link)

do {
_ = try await smb.destinationOfSymbolicLink(atPath: link)
XCTAssert(false, "Destination should not exist")
} catch {}
}

func testDirectoryOperation() async throws {
let smb = SMB2Manager(url: server, credential: credential)!
Expand Down
2 changes: 1 addition & 1 deletion Dependencies/libsmb2

0 comments on commit 3dcb33c

Please sign in to comment.