核心功能: - ✅ Categories/Series双视图管理(category_view.rs + import_markdown.rs) - ✅ FUSE Multi-Volume支持(tree_type参数) - ✅ SSH/SFTP/SCP/rsync协议完整实现(4042行) - ✅ NFS/SMB Module Phase 1-3完成 - ✅ Archive Module Phase 1-4完成(2916行) - ✅ Download Center API完整实现 - ✅ S3兼容API实现(560行) Git配置修正: - ✅ 删除错误origin(gitea.momentry.ddns.net) - ✅ 删除m5max128(指向机器名) - ✅ 设置origin = m5max128gitea.momentry.ddns.net/admin/markbase - ✅ 设置m4minigitea = m4minigitea.momentry.ddns.net/warren/markbase 数据清理: - ✅ 删除38个临时SQLite(保留accusys.sqlite、demo.sqlite) - ✅ 删除.bak、test_*.bin、调试脚本等临时文件 - ✅ 删除临时目录(build/、download files/、raid_test/等) - ✅ 更新.gitignore排除临时文件 架构优化: - 52个文件修改,2434行新增,4739行删除 - Workspace成员整合(16个crate) - 数据库状态:accusys.sqlite保留(主demo测试) 远程同步: - ✅ 准备推送到m5max128gitea(远程Gitea) - ✅ 准备推送到m4minigitea(本地Gitea)
1018 lines
26 KiB
Markdown
1018 lines
26 KiB
Markdown
# FSKit API Real-World Analysis
|
|
**Source**: KhaosT/FSKitSample GitHub Repository
|
|
**Date**: 2026-06-11
|
|
**Purpose**: Fix HelloFS compilation errors with actual FSKit API
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
**FSKit is the new macOS Sequoia 15.4+ framework for user-space filesystem implementations.** This analysis extracts real working code patterns from the FSKitSample repository to fix compilation errors in HelloFS.
|
|
|
|
---
|
|
|
|
## 1. Repository Structure
|
|
|
|
```
|
|
FSKitSample/
|
|
├── FSKitExp/ # Main Application (SwiftUI)
|
|
│ ├── FSKitExpApp.swift # App entry point
|
|
│ ├── ContentView.swift # UI for managing extensions
|
|
│ ├── ViewModel.swift # FSClient interaction
|
|
│ ├── Info.plist
|
|
│ └── FSKitExp.entitlements
|
|
│
|
|
├── FSKitExpExtension/ # File System Extension (Target)
|
|
│ ├── FSKitExpExtension.swift # Extension entry point ⭐
|
|
│ ├── MyFS.swift # FSUnaryFileSystem implementation ⭐
|
|
│ ├── MyFSVolume.swift # FSVolume implementation ⭐
|
|
│ ├── MyFSItem.swift # FSItem implementation ⭐
|
|
│ ├── Constants.swift # UUID constants
|
|
│ ├── Info.plist # Extension configuration ⭐
|
|
│ └── FSKitExpExtension.entitlements ⭐
|
|
│
|
|
└── FSKitExp.xcodeproj/ # Xcode project
|
|
```
|
|
|
|
**Key Insight**: FSKit requires **two targets**:
|
|
1. **Main App** - For UI and extension management
|
|
2. **File System Extension** - The actual filesystem implementation
|
|
|
|
---
|
|
|
|
## 2. FSKit Extension Entry Point
|
|
|
|
### FSKitExpExtension.swift (Complete Code)
|
|
|
|
```swift
|
|
import Foundation
|
|
import FSKit
|
|
|
|
@main
|
|
struct FSKitExpExtension : UnaryFileSystemExtension {
|
|
|
|
var fileSystem : FSUnaryFileSystem & FSUnaryFileSystemOperations {
|
|
MyFS()
|
|
}
|
|
}
|
|
```
|
|
|
|
**Critical Finding**:
|
|
- Entry point is `UnaryFileSystemExtension` (NOT `FSUnaryFileSystemExtension`!)
|
|
- Returns `FSUnaryFileSystem & FSUnaryFileSystemOperations`
|
|
- Uses `@main` attribute for Swift 5.7+ entry point
|
|
|
|
---
|
|
|
|
## 3. Filesystem Implementation
|
|
|
|
### MyFS.swift (Complete Code)
|
|
|
|
```swift
|
|
import Foundation
|
|
import FSKit
|
|
import os
|
|
|
|
final class MyFS: FSUnaryFileSystem, FSUnaryFileSystemOperations {
|
|
|
|
private let logger = Logger(subsystem: "FSKitExp", category: "MyFS")
|
|
|
|
func probeResource(
|
|
resource: FSResource,
|
|
replyHandler: @escaping (FSProbeResult?, (any Error)?) -> Void
|
|
) {
|
|
logger.debug("probeResource: \(resource, privacy: .public)")
|
|
|
|
replyHandler(
|
|
FSProbeResult.usable(
|
|
name: "Test1",
|
|
containerID: FSContainerIdentifier(uuid: Constants.containerIdentifier)
|
|
),
|
|
nil
|
|
)
|
|
}
|
|
|
|
func loadResource(
|
|
resource: FSResource,
|
|
options: FSTaskOptions,
|
|
replyHandler: @escaping (FSVolume?, (any Error)?) -> Void
|
|
) {
|
|
containerStatus = .ready
|
|
logger.debug("loadResource: \(resource, privacy: .public)")
|
|
replyHandler(
|
|
MyFSVolume(resource: resource),
|
|
nil
|
|
)
|
|
}
|
|
|
|
func unloadResource(
|
|
resource: FSResource,
|
|
options: FSTaskOptions,
|
|
replyHandler reply: @escaping ((any Error)?) -> Void
|
|
) {
|
|
logger.debug("unloadResource: \(resource, privacy: .public)")
|
|
reply(nil)
|
|
}
|
|
|
|
func didFinishLoading() {
|
|
logger.debug("didFinishLoading")
|
|
}
|
|
}
|
|
```
|
|
|
|
**Key API Differences from HelloFS**:
|
|
|
|
| HelloFS (Wrong) | FSKitSample (Correct) |
|
|
|----------------|----------------------|
|
|
| `FSUnaryFileSystemExtension` | `UnaryFileSystemExtension` |
|
|
| `probeResource()` sync | `probeResource()` with replyHandler |
|
|
| `loadResource()` sync | `loadResource()` with replyHandler |
|
|
| `unloadResource()` sync | `unloadResource()` with replyHandler |
|
|
| No `didFinishLoading()` | Has `didFinishLoading()` |
|
|
|
|
---
|
|
|
|
## 4. Volume Implementation
|
|
|
|
### MyFSVolume.swift (Key Extracts)
|
|
|
|
```swift
|
|
final class MyFSVolume: FSVolume {
|
|
|
|
private let resource: FSResource
|
|
private let logger = Logger(subsystem: "FSKitExp", category: "MyFSVolume")
|
|
|
|
private let root: MyFSItem = {
|
|
let item = MyFSItem(name: FSFileName(string: "/"))
|
|
item.attributes.parentID = .parentOfRoot
|
|
item.attributes.fileID = .rootDirectory
|
|
item.attributes.uid = 0
|
|
item.attributes.gid = 0
|
|
item.attributes.linkCount = 1
|
|
item.attributes.type = .directory
|
|
item.attributes.mode = UInt32(S_IFDIR | 0b111_000_000)
|
|
item.attributes.allocSize = 1
|
|
item.attributes.size = 1
|
|
return item
|
|
}()
|
|
|
|
init(resource: FSResource) {
|
|
self.resource = resource
|
|
|
|
super.init(
|
|
volumeID: FSVolume.Identifier(uuid: Constants.volumeIdentifier),
|
|
volumeName: FSFileName(string: "Test1")
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Volume Protocols
|
|
|
|
**FSVolume.PathConfOperations**:
|
|
```swift
|
|
extension MyFSVolume: FSVolume.PathConfOperations {
|
|
var maximumLinkCount: Int { return -1 }
|
|
var maximumNameLength: Int { return -1 }
|
|
var restrictsOwnershipChanges: Bool { return false }
|
|
var truncatesLongNames: Bool { return false }
|
|
var maximumXattrSize: Int { return Int.max }
|
|
var maximumFileSize: UInt64 { return UInt64.max }
|
|
}
|
|
```
|
|
|
|
**FSVolume.Operations** (Core Operations):
|
|
```swift
|
|
extension MyFSVolume: FSVolume.Operations {
|
|
|
|
var supportedVolumeCapabilities: FSVolume.SupportedCapabilities {
|
|
let capabilities = FSVolume.SupportedCapabilities()
|
|
capabilities.supportsHardLinks = true
|
|
capabilities.supportsSymbolicLinks = true
|
|
capabilities.supportsPersistentObjectIDs = true
|
|
capabilities.doesNotSupportVolumeSizes = true
|
|
capabilities.supportsHiddenFiles = true
|
|
capabilities.supports64BitObjectIDs = true
|
|
capabilities.caseFormat = .insensitiveCasePreserving
|
|
return capabilities
|
|
}
|
|
|
|
var volumeStatistics: FSStatFSResult {
|
|
let result = FSStatFSResult(fileSystemTypeName: "MyFS")
|
|
result.blockSize = 1024000
|
|
result.ioSize = 1024000
|
|
result.totalBlocks = 1024000
|
|
result.availableBlocks = 1024000
|
|
result.freeBlocks = 1024000
|
|
result.totalFiles = 1024000
|
|
result.freeFiles = 1024000
|
|
return result
|
|
}
|
|
|
|
func activate(options: FSTaskOptions) async throws -> FSItem {
|
|
return root
|
|
}
|
|
|
|
func deactivate(options: FSDeactivateOptions = []) async throws {
|
|
}
|
|
|
|
func mount(options: FSTaskOptions) async throws {
|
|
}
|
|
|
|
func unmount() async {
|
|
}
|
|
|
|
func synchronize(flags: FSSyncFlags) async throws {
|
|
}
|
|
|
|
func attributes(
|
|
_ desiredAttributes: FSItem.GetAttributesRequest,
|
|
of item: FSItem
|
|
) async throws -> FSItem.Attributes {
|
|
if let item = item as? MyFSItem {
|
|
return item.attributes
|
|
} else {
|
|
throw fs_errorForPOSIXError(POSIXError.EIO.rawValue)
|
|
}
|
|
}
|
|
|
|
func setAttributes(
|
|
_ newAttributes: FSItem.SetAttributesRequest,
|
|
on item: FSItem
|
|
) async throws -> FSItem.Attributes {
|
|
if let item = item as? MyFSItem {
|
|
mergeAttributes(item.attributes, request: newAttributes)
|
|
return item.attributes
|
|
} else {
|
|
throw fs_errorForPOSIXError(POSIXError.EIO.rawValue)
|
|
}
|
|
}
|
|
|
|
func lookupItem(
|
|
named name: FSFileName,
|
|
inDirectory directory: FSItem
|
|
) async throws -> (FSItem, FSFileName) {
|
|
guard let directory = directory as? MyFSItem else {
|
|
throw fs_errorForPOSIXError(POSIXError.ENOENT.rawValue)
|
|
}
|
|
|
|
if let item = directory.children[name] {
|
|
return (item, name)
|
|
} else {
|
|
throw fs_errorForPOSIXError(POSIXError.ENOENT.rawValue)
|
|
}
|
|
}
|
|
|
|
func reclaimItem(_ item: FSItem) async throws {
|
|
}
|
|
|
|
func readSymbolicLink(_ item: FSItem) async throws -> FSFileName {
|
|
throw fs_errorForPOSIXError(POSIXError.EIO.rawValue)
|
|
}
|
|
|
|
func createItem(
|
|
named name: FSFileName,
|
|
type: FSItem.ItemType,
|
|
inDirectory directory: FSItem,
|
|
attributes newAttributes: FSItem.SetAttributesRequest
|
|
) async throws -> (FSItem, FSFileName) {
|
|
guard let directory = directory as? MyFSItem else {
|
|
throw fs_errorForPOSIXError(POSIXError.EIO.rawValue)
|
|
}
|
|
|
|
let item = MyFSItem(name: name)
|
|
mergeAttributes(item.attributes, request: newAttributes)
|
|
item.attributes.parentID = directory.attributes.fileID
|
|
item.attributes.type = type
|
|
directory.addItem(item)
|
|
|
|
return (item, name)
|
|
}
|
|
|
|
func createSymbolicLink(
|
|
named name: FSFileName,
|
|
inDirectory directory: FSItem,
|
|
attributes newAttributes: FSItem.SetAttributesRequest,
|
|
linkContents contents: FSFileName
|
|
) async throws -> (FSItem, FSFileName) {
|
|
throw fs_errorForPOSIXError(POSIXError.EIO.rawValue)
|
|
}
|
|
|
|
func createLink(
|
|
to item: FSItem,
|
|
named name: FSFileName,
|
|
inDirectory directory: FSItem
|
|
) async throws -> FSFileName {
|
|
throw fs_errorForPOSIXError(POSIXError.EIO.rawValue)
|
|
}
|
|
|
|
func removeItem(
|
|
_ item: FSItem,
|
|
named name: FSFileName,
|
|
fromDirectory directory: FSItem
|
|
) async throws {
|
|
if let item = item as? MyFSItem, let directory = directory as? MyFSItem {
|
|
directory.removeItem(item)
|
|
} else {
|
|
throw fs_errorForPOSIXError(POSIXError.EIO.rawValue)
|
|
}
|
|
}
|
|
|
|
func renameItem(
|
|
_ item: FSItem,
|
|
inDirectory sourceDirectory: FSItem,
|
|
named sourceName: FSFileName,
|
|
to destinationName: FSFileName,
|
|
inDirectory destinationDirectory: FSItem,
|
|
overItem: FSItem?
|
|
) async throws -> FSFileName {
|
|
throw fs_errorForPOSIXError(POSIXError.EIO.rawValue)
|
|
}
|
|
|
|
func enumerateDirectory(
|
|
_ directory: FSItem,
|
|
startingAt cookie: FSDirectoryCookie,
|
|
verifier: FSDirectoryVerifier,
|
|
attributes: FSItem.GetAttributesRequest?,
|
|
packer: FSDirectoryEntryPacker
|
|
) async throws -> FSDirectoryVerifier {
|
|
guard let directory = directory as? MyFSItem else {
|
|
throw fs_errorForPOSIXError(POSIXError.ENOENT.rawValue)
|
|
}
|
|
|
|
for (idx, item) in directory.children.values.enumerated() {
|
|
let isLast = (idx == directory.children.count - 1)
|
|
|
|
let v = packer.packEntry(
|
|
name: item.name,
|
|
itemType: item.attributes.type,
|
|
itemID: item.attributes.fileID,
|
|
nextCookie: FSDirectoryCookie(UInt64(idx)),
|
|
attributes: attributes != nil ? item.attributes : nil
|
|
)
|
|
}
|
|
|
|
return FSDirectoryVerifier(0)
|
|
}
|
|
}
|
|
```
|
|
|
|
**FSVolume.OpenCloseOperations**:
|
|
```swift
|
|
extension MyFSVolume: FSVolume.OpenCloseOperations {
|
|
|
|
func openItem(_ item: FSItem, modes: FSVolume.OpenModes) async throws {
|
|
}
|
|
|
|
func closeItem(_ item: FSItem, modes: FSVolume.OpenModes) async throws {
|
|
}
|
|
}
|
|
```
|
|
|
|
**FSVolume.XattrOperations**:
|
|
```swift
|
|
extension MyFSVolume: FSVolume.XattrOperations {
|
|
|
|
func xattr(named name: FSFileName, of item: FSItem) async throws -> Data {
|
|
if let item = item as? MyFSItem {
|
|
return item.xattrs[name] ?? Data()
|
|
} else {
|
|
return Data()
|
|
}
|
|
}
|
|
|
|
func setXattr(named name: FSFileName, to value: Data?, on item: FSItem, policy: FSVolume.SetXattrPolicy) async throws {
|
|
if let item = item as? MyFSItem {
|
|
item.xattrs[name] = value
|
|
}
|
|
}
|
|
|
|
func xattrs(of item: FSItem) async throws -> [FSFileName] {
|
|
if let item = item as? MyFSItem {
|
|
return Array(item.xattrs.keys)
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**FSVolume.ReadWriteOperations**:
|
|
```swift
|
|
extension MyFSVolume: FSVolume.ReadWriteOperations {
|
|
|
|
func read(from item: FSItem, at offset: off_t, length: Int, into buffer: FSMutableFileDataBuffer) async throws -> Int {
|
|
var bytesRead = 0
|
|
|
|
if let item = item as? MyFSItem, let data = item.data {
|
|
bytesRead = data.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in
|
|
let length = min(buffer.length, data.count)
|
|
_ = buffer.withUnsafeMutableBytes { dst in
|
|
memcpy(dst.baseAddress, ptr.baseAddress, length)
|
|
}
|
|
return length
|
|
}
|
|
}
|
|
|
|
return bytesRead
|
|
}
|
|
|
|
func write(contents: Data, to item: FSItem, at offset: off_t) async throws -> Int {
|
|
if let item = item as? MyFSItem {
|
|
item.data = contents
|
|
item.attributes.size = UInt64(contents.count)
|
|
item.attributes.allocSize = UInt64(contents.count)
|
|
}
|
|
|
|
return contents.count
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. FSItem Implementation
|
|
|
|
### MyFSItem.swift (Complete Code)
|
|
|
|
```swift
|
|
import Foundation
|
|
import FSKit
|
|
|
|
final class MyFSItem: FSItem {
|
|
|
|
private static var id: UInt64 = FSItem.Identifier.rootDirectory.rawValue + 1
|
|
static func getNextID() -> UInt64 {
|
|
let current = id
|
|
id += 1
|
|
return current
|
|
}
|
|
|
|
let name: FSFileName
|
|
let id = MyFSItem.getNextID()
|
|
|
|
var attributes = FSItem.Attributes()
|
|
var xattrs: [FSFileName: Data] = [:]
|
|
var data: Data?
|
|
|
|
private(set) var children: [FSFileName: MyFSItem] = [:]
|
|
|
|
init(name: FSFileName) {
|
|
self.name = name
|
|
attributes.fileID = FSItem.Identifier(rawValue: id) ?? .invalid
|
|
attributes.size = 0
|
|
attributes.allocSize = 0
|
|
attributes.flags = 0
|
|
|
|
var timespec = timespec()
|
|
timespec_get(×pec, TIME_UTC)
|
|
|
|
attributes.addedTime = timespec
|
|
attributes.birthTime = timespec
|
|
attributes.changeTime = timespec
|
|
attributes.modifyTime = timespec
|
|
attributes.accessTime = timespec
|
|
}
|
|
|
|
func addItem(_ item: MyFSItem) {
|
|
children[item.name] = item
|
|
}
|
|
|
|
func removeItem(_ item: MyFSItem) {
|
|
children[item.name] = nil
|
|
}
|
|
}
|
|
```
|
|
|
|
**Key Points**:
|
|
- Subclasses `FSItem` directly
|
|
- Manages file ID allocation
|
|
- Stores attributes, xattrs, data, and children
|
|
- Helper methods for adding/removing children
|
|
|
|
---
|
|
|
|
## 6. Info.plist Configuration (CRITICAL)
|
|
|
|
### FSKitExpExtension/Info.plist
|
|
|
|
```xml
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>EXAppExtensionAttributes</key>
|
|
<dict>
|
|
<key>EXExtensionPointIdentifier</key>
|
|
<string>com.apple.fskit.fsmodule</string>
|
|
|
|
<key>FSShortName</key>
|
|
<string>MyFS</string>
|
|
|
|
<key>FSPersonalities</key>
|
|
<dict>
|
|
<key>FSKitExpExtensionPersonality</key>
|
|
<dict>
|
|
<key>FSName</key>
|
|
<string>MyFS</string>
|
|
<key>FSfileObjectsAreCaseSensitive</key>
|
|
<false/>
|
|
</dict>
|
|
</dict>
|
|
|
|
<key>FSSupportsBlockResources</key>
|
|
<true/>
|
|
|
|
<key>FSSupportsGenericURLResources</key>
|
|
<false/>
|
|
|
|
<key>FSSupportsPathURLs</key>
|
|
<false/>
|
|
|
|
<key>FSSupportsServerURLs</key>
|
|
<false/>
|
|
|
|
<key>FSRequiresSecurityScopedPathURLResources</key>
|
|
<false/>
|
|
|
|
<key>FSMediaTypes</key>
|
|
<dict/>
|
|
|
|
<key>FSActivateOptionSyntax</key>
|
|
<dict>
|
|
<key>shortOptions</key>
|
|
<string>g:m:o:u:</string>
|
|
</dict>
|
|
|
|
<key>FSCheckOptionSyntax</key>
|
|
<dict>
|
|
<key>shortOptions</key>
|
|
<string>nqy</string>
|
|
</dict>
|
|
|
|
<key>FSFormatOptionSyntax</key>
|
|
<dict>
|
|
<key>shortOptions</key>
|
|
<string>v</string>
|
|
</dict>
|
|
</dict>
|
|
</dict>
|
|
</plist>
|
|
```
|
|
|
|
**Critical Configuration Keys**:
|
|
|
|
| Key | Value | Purpose |
|
|
|-----|-------|---------|
|
|
| `EXExtensionPointIdentifier` | `com.apple.fskit.fsmodule` | FSKit extension type |
|
|
| `FSShortName` | `MyFS` | Filesystem short name |
|
|
| `FSName` | `MyFS` | Filesystem display name |
|
|
| `FSSupportsBlockResources` | `true` | Supports block devices |
|
|
| `FSSupportsGenericURLResources` | `false` | No generic URL support |
|
|
| `FSSupportsPathURLs` | `false` | No path URL support |
|
|
| `FSSupportsServerURLs` | `false` | No server URL support |
|
|
| `FSActivateOptionSyntax` | `-g:m:o:u:` | Mount options syntax |
|
|
| `FSCheckOptionSyntax` | `-nqy` | Check options syntax |
|
|
| `FSFormatOptionSyntax` | `-v` | Format options syntax |
|
|
|
|
---
|
|
|
|
## 7. Entitlements Configuration
|
|
|
|
### FSKitExpExtension.entitlements
|
|
|
|
```xml
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>com.apple.security.app-sandbox</key>
|
|
<true/>
|
|
<key>com.apple.developer.fskit.fsmodule</key>
|
|
<true/>
|
|
</dict>
|
|
</plist>
|
|
```
|
|
|
|
**Required Entitlements**:
|
|
1. `com.apple.security.app-sandbox` - Must be enabled
|
|
2. `com.apple.developer.fskit.fsmodule` - FSKit module capability
|
|
|
|
---
|
|
|
|
## 8. Main App Structure
|
|
|
|
### FSKitExpApp.swift
|
|
|
|
```swift
|
|
import SwiftUI
|
|
|
|
@main
|
|
struct FSKitExpApp: App {
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView()
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### ContentView.swift
|
|
|
|
```swift
|
|
import SwiftUI
|
|
|
|
struct ContentView: View {
|
|
|
|
@State
|
|
private var viewModel = ViewModel()
|
|
|
|
var body: some View {
|
|
List {
|
|
ForEach(viewModel.modules, id: \.self) { module in
|
|
VStack(alignment: .leading) {
|
|
Text(module.bundleIdentifier)
|
|
.bold()
|
|
Text(module.url.absoluteString)
|
|
Text("\(module.isEnabled)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### ViewModel.swift
|
|
|
|
```swift
|
|
import Foundation
|
|
import FSKit
|
|
import Observation
|
|
|
|
@Observable
|
|
@MainActor
|
|
final class ViewModel {
|
|
|
|
private var client: FSClient?
|
|
private(set) var modules: [FSModuleIdentity] = []
|
|
|
|
init() {
|
|
client = FSClient.shared
|
|
client?.fetchInstalledExtensions { modules, errors in
|
|
if let modules {
|
|
self.modules = modules
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Purpose**: Main app lists installed FSKit extensions and their status.
|
|
|
|
---
|
|
|
|
## 9. Testing and Mounting
|
|
|
|
### Create Dummy Block Device
|
|
|
|
```bash
|
|
# Create a 100MB dummy file
|
|
mkfile -n 100m dummy
|
|
|
|
# Mount it as a raw block device
|
|
hdiutil attach -imagekey diskimage-class=CRawDiskImage -nomount dummy
|
|
# Output: /dev/disk18
|
|
```
|
|
|
|
### Mount Filesystem
|
|
|
|
```bash
|
|
# Create mount point
|
|
mkdir /tmp/TestVol
|
|
|
|
# Mount the filesystem
|
|
mount -F -t MyFS disk18 /tmp/TestVol
|
|
```
|
|
|
|
### Unmount Filesystem
|
|
|
|
```bash
|
|
umount /tmp/TestVol
|
|
```
|
|
|
|
### Enable Extension
|
|
|
|
After building and running the app:
|
|
1. Open System Settings
|
|
2. Navigate to General → Login Items & Extensions
|
|
3. Enable the File System Extension under "File System Extensions"
|
|
|
|
---
|
|
|
|
## 10. Critical API Differences from HelloFS
|
|
|
|
### Entry Point
|
|
|
|
**HelloFS (Wrong)**:
|
|
```swift
|
|
@main
|
|
class HelloFSExtension: FSUnaryFileSystemExtension {
|
|
// ERROR: FSUnaryFileSystemExtension doesn't exist
|
|
}
|
|
```
|
|
|
|
**FSKitSample (Correct)**:
|
|
```swift
|
|
@main
|
|
struct FSKitExpExtension : UnaryFileSystemExtension {
|
|
var fileSystem : FSUnaryFileSystem & FSUnaryFileSystemOperations {
|
|
MyFS()
|
|
}
|
|
}
|
|
```
|
|
|
|
### Probe Resource
|
|
|
|
**HelloFS (Wrong)**:
|
|
```swift
|
|
func probeResource(_ resource: FSResource) -> FSProbeResult {
|
|
// Sync return
|
|
}
|
|
```
|
|
|
|
**FSKitSample (Correct)**:
|
|
```swift
|
|
func probeResource(
|
|
resource: FSResource,
|
|
replyHandler: @escaping (FSProbeResult?, (any Error)?) -> Void
|
|
) {
|
|
// Async with replyHandler
|
|
replyHandler(
|
|
FSProbeResult.usable(
|
|
name: "MyFS",
|
|
containerID: FSContainerIdentifier(uuid: Constants.containerIdentifier)
|
|
),
|
|
nil
|
|
)
|
|
}
|
|
```
|
|
|
|
### Load Resource
|
|
|
|
**HelloFS (Wrong)**:
|
|
```swift
|
|
func loadResource(_ resource: FSResource) -> FSVolume {
|
|
// Sync return
|
|
}
|
|
```
|
|
|
|
**FSKitSample (Correct)**:
|
|
```swift
|
|
func loadResource(
|
|
resource: FSResource,
|
|
options: FSTaskOptions,
|
|
replyHandler: @escaping (FSVolume?, (any Error)?) -> Void
|
|
) {
|
|
// Async with replyHandler
|
|
replyHandler(MyFSVolume(resource: resource), nil)
|
|
}
|
|
```
|
|
|
|
### Volume Operations
|
|
|
|
**HelloFS (Wrong)**:
|
|
```swift
|
|
func activate() -> FSItem {
|
|
// Sync return
|
|
}
|
|
```
|
|
|
|
**FSKitSample (Correct)**:
|
|
```swift
|
|
func activate(options: FSTaskOptions) async throws -> FSItem {
|
|
// Async with options parameter
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Protocol Hierarchy
|
|
|
|
```
|
|
UnaryFileSystemExtension (Entry Point)
|
|
└─ Returns: FSUnaryFileSystem & FSUnaryFileSystemOperations
|
|
|
|
FSUnaryFileSystemOperations:
|
|
├─ probeResource(_:replyHandler:)
|
|
├─ loadResource(_:options:replyHandler:)
|
|
├─ unloadResource(_:options:replyHandler:)
|
|
└─ didFinishLoading()
|
|
|
|
FSVolume:
|
|
├─ FSVolume.Operations (Required)
|
|
├─ FSVolume.PathConfOperations
|
|
├─ FSVolume.OpenCloseOperations
|
|
├─ FSVolume.XattrOperations
|
|
└─ FSVolume.ReadWriteOperations
|
|
|
|
FSVolume.Operations:
|
|
├─ supportedVolumeCapabilities
|
|
├─ volumeStatistics
|
|
├─ activate(options:)
|
|
├─ deactivate(options:)
|
|
├─ mount(options:)
|
|
├─ unmount()
|
|
├─ synchronize(flags:)
|
|
├─ attributes(_:of:)
|
|
├─ setAttributes(_:on:)
|
|
├─ lookupItem(named:inDirectory:)
|
|
├─ reclaimItem(_:)
|
|
├─ readSymbolicLink(_:)
|
|
├─ createItem(named:type:inDirectory:attributes:)
|
|
├─ createSymbolicLink(named:inDirectory:attributes:linkContents:)
|
|
├─ createLink(to:named:inDirectory:)
|
|
├─ removeItem(_:named:fromDirectory:)
|
|
├─ renameItem(_:inDirectory:named:to:inDirectory:overItem:)
|
|
└─ enumerateDirectory(_:startingAt:verifier:attributes:packer:)
|
|
|
|
FSVolume.PathConfOperations:
|
|
├─ maximumLinkCount
|
|
├─ maximumNameLength
|
|
├─ restrictsOwnershipChanges
|
|
├─ truncatesLongNames
|
|
├─ maximumXattrSize
|
|
└─ maximumFileSize
|
|
|
|
FSVolume.OpenCloseOperations:
|
|
├─ openItem(_:modes:)
|
|
└─ closeItem(_:modes:)
|
|
|
|
FSVolume.XattrOperations:
|
|
├─ xattr(named:of:)
|
|
├─ setXattr(named:to:on:policy:)
|
|
└─ xattrs(of:)
|
|
|
|
FSVolume.ReadWriteOperations:
|
|
├─ read(from:at:length:into:)
|
|
└─ write(contents:to:at:)
|
|
```
|
|
|
|
---
|
|
|
|
## 12. FSItem Structure
|
|
|
|
```swift
|
|
FSItem.Attributes:
|
|
├─ fileID: FSItem.Identifier
|
|
├─ parentID: FSItem.Identifier
|
|
├─ uid: uid_t
|
|
├─ gid: gid_t
|
|
├─ type: FSItem.ItemType
|
|
├─ mode: UInt32
|
|
├─ linkCount: UInt32
|
|
├─ flags: UInt32
|
|
├─ size: UInt64
|
|
├─ allocSize: UInt64
|
|
├─ accessTime: timespec
|
|
├─ modifyTime: timespec
|
|
├─ changeTime: timespec
|
|
├─ birthTime: timespec
|
|
├─ addedTime: timespec
|
|
└─ backupTime: timespec
|
|
|
|
FSItem.GetAttributesRequest:
|
|
└─ isValid(_ attribute: FSItem.Attribute) -> Bool
|
|
|
|
FSItem.SetAttributesRequest:
|
|
├─ All attributes from FSItem.Attributes
|
|
└─ isValid(_ attribute: FSItem.Attribute) -> Bool
|
|
|
|
FSItem.Identifier:
|
|
├─ rootDirectory
|
|
├─ invalid
|
|
├─ parentOfRoot
|
|
└─ init(rawValue: UInt64)
|
|
|
|
FSItem.ItemType:
|
|
├─ directory
|
|
├─ file
|
|
├─ symbolicLink
|
|
└─ etc.
|
|
|
|
FSFileName:
|
|
├─ init(string: String)
|
|
└─ string: String?
|
|
|
|
FSDirectoryCookie:
|
|
└─ init(UInt64)
|
|
|
|
FSDirectoryVerifier:
|
|
└─ init(UInt64)
|
|
|
|
FSDirectoryEntryPacker:
|
|
└─ packEntry(name:itemType:itemID:nextCookie:attributes:) -> Bool
|
|
```
|
|
|
|
---
|
|
|
|
## 13. Error Handling
|
|
|
|
```swift
|
|
// POSIX error helper
|
|
fs_errorForPOSIXError(POSIXError.ENOENT.rawValue) // File not found
|
|
fs_errorForPOSIXError(POSIXError.EIO.rawValue) // I/O error
|
|
fs_errorForPOSIXError(POSIXError.EACCES.rawValue) // Permission denied
|
|
```
|
|
|
|
---
|
|
|
|
## 14. Build Configuration
|
|
|
|
### Xcode Project Structure
|
|
|
|
- **Main App Target**: FSKitExp (macOS App)
|
|
- **Extension Target**: FSKitExpExtension (File System Extension)
|
|
- **Platform**: macOS Sequoia 15.4+
|
|
- **Language**: Swift 5.7+
|
|
- **Frameworks**:
|
|
- SwiftUI (Main App)
|
|
- FSKit (Extension)
|
|
|
|
### Build Phases
|
|
|
|
**Main App**:
|
|
- Compile Sources: FSKitExpApp.swift, ContentView.swift, ViewModel.swift
|
|
- Link Binary: SwiftUI.framework
|
|
- Copy Bundle Resources: Assets.xcassets
|
|
|
|
**Extension**:
|
|
- Compile Sources: FSKitExpExtension.swift, MyFS.swift, MyFSVolume.swift, MyFSItem.swift, Constants.swift
|
|
- Link Binary: FSKit.framework
|
|
- Copy Bundle Resources: None
|
|
|
|
---
|
|
|
|
## 15. Key Takeaways for HelloFS
|
|
|
|
### 1. Fix Entry Point
|
|
```swift
|
|
// Change from:
|
|
@main
|
|
class HelloFSExtension: FSUnaryFileSystemExtension
|
|
|
|
// To:
|
|
@main
|
|
struct HelloFSExtension: UnaryFileSystemExtension {
|
|
var fileSystem: FSUnaryFileSystem & FSUnaryFileSystemOperations {
|
|
HelloFS()
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Fix Filesystem Class
|
|
```swift
|
|
// Change from:
|
|
class HelloFS: FSUnaryFileSystemExtension
|
|
|
|
// To:
|
|
final class HelloFS: FSUnaryFileSystem, FSUnaryFileSystemOperations
|
|
```
|
|
|
|
### 3. Fix Probe/Load/Unload Methods
|
|
- Change sync methods to async with replyHandler
|
|
- Add `options: FSTaskOptions` parameter
|
|
- Add `didFinishLoading()` method
|
|
|
|
### 4. Fix Volume Operations
|
|
- Add `options: FSTaskOptions` parameter to async methods
|
|
- Use proper async/await syntax
|
|
- Implement all required protocols
|
|
|
|
### 5. Add Info.plist Configuration
|
|
- Add `EXAppExtensionAttributes` with FSKit configuration
|
|
- Set `FSShortName` and `FSName`
|
|
- Configure `FSSupportsBlockResources` = true
|
|
|
|
### 6. Add Entitlements
|
|
- Enable `com.apple.security.app-sandbox`
|
|
- Enable `com.apple.developer.fskit.fsmodule`
|
|
|
|
---
|
|
|
|
## 16. Summary of Compilation Errors to Fix
|
|
|
|
| Error | Cause | Fix |
|
|
|-------|-------|-----|
|
|
| `Cannot find type 'FSUnaryFileSystemExtension' in scope` | Wrong class name | Use `UnaryFileSystemExtension` |
|
|
| `Method signature mismatch` | Sync vs async | Use replyHandler pattern |
|
|
| `Missing parameter` | Missing `options` | Add `FSTaskOptions` parameter |
|
|
| `Missing method` | No `didFinishLoading()` | Add method |
|
|
| `Missing Info.plist keys` | No FSKit config | Add `EXAppExtensionAttributes` |
|
|
| `Missing entitlements` | No FSKit capability | Add entitlements |
|
|
|
|
---
|
|
|
|
**Last Updated**: 2026-06-11
|
|
**Status**: Complete Analysis
|
|
**Next Steps**: Apply fixes to HelloFS implementation
|