Skip to content

Commit 1cb83ea

Browse files
committed
Release DiskMount 0.2.9
1 parent 329ec4e commit 1cb83ea

13 files changed

Lines changed: 231 additions & 34 deletions

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 0.2.9 - 2026-08-04
4+
5+
- Supports Chinese and other non-ASCII NTFS volume names by assigning an ASCII-only custom NFS mount point while preserving the original display name in DiskMount.
6+
- Verifies that an exact device-backed NFS mount actually appears after anylinuxfs exits instead of treating its process exit code as proof of success.
7+
- Waits up to five seconds for the postcondition, reports a real failure when the mount is absent, and restores the native macOS read-only NTFS mount.
8+
- Saves Auto Read/Write only after the verified NFS mount succeeds, preventing failed mounts from being remembered as successful.
9+
- Adds regression coverage using the reported `大白菜U盘` name, safe-path sanitization, mount verification, and fallback restoration.
10+
311
## 0.2.8 - 2026-08-04
412

513
- Keeps a successfully ejected physical disk hidden across periodic refreshes while it remains connected, preventing the same unmounted device from reappearing in the panel.

‎DiskMount/DiskMount.xcodeproj/project.pbxproj‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@
324324
CODE_SIGN_IDENTITY = "Apple Development: samni728@gmail.com (4BVG532TG3)";
325325
CODE_SIGN_STYLE = Manual;
326326
COMBINE_HIDPI_IMAGES = YES;
327-
CURRENT_PROJECT_VERSION = 13;
327+
CURRENT_PROJECT_VERSION = 14;
328328
DEVELOPMENT_TEAM = YU9NH29226;
329329
ENABLE_APP_SANDBOX = NO;
330330
ENABLE_HARDENED_RUNTIME = YES;
@@ -334,7 +334,7 @@
334334
"$(inherited)",
335335
"@executable_path/../Frameworks",
336336
);
337-
MARKETING_VERSION = 0.2.8;
337+
MARKETING_VERSION = 0.2.9;
338338
PRODUCT_BUNDLE_IDENTIFIER = com.samni.DiskMount;
339339
PRODUCT_NAME = DiskMount;
340340
SDKROOT = macosx;
@@ -413,7 +413,7 @@
413413
CODE_SIGN_IDENTITY = "Apple Development: samni728@gmail.com (4BVG532TG3)";
414414
CODE_SIGN_STYLE = Manual;
415415
COMBINE_HIDPI_IMAGES = YES;
416-
CURRENT_PROJECT_VERSION = 13;
416+
CURRENT_PROJECT_VERSION = 14;
417417
DEVELOPMENT_TEAM = YU9NH29226;
418418
ENABLE_APP_SANDBOX = NO;
419419
ENABLE_HARDENED_RUNTIME = YES;
@@ -423,7 +423,7 @@
423423
"$(inherited)",
424424
"@executable_path/../Frameworks",
425425
);
426-
MARKETING_VERSION = 0.2.8;
426+
MARKETING_VERSION = 0.2.9;
427427
PRODUCT_BUNDLE_IDENTIFIER = com.samni.DiskMount;
428428
PRODUCT_NAME = DiskMount;
429429
SDKROOT = macosx;

‎DiskMount/Resources/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<img class="brand-icon" src="logo.png" alt="">
1515
<div>
1616
<div class="eyebrow" data-i18n="eyebrow">DISK CONTROL</div>
17-
<h1>DiskMount <button id="version-button" class="version-button" data-action="openUpdate" type="button" disabled><span id="version">0.2.8</span><span id="update-indicator" class="update-indicator hidden" aria-hidden="true"></span></button></h1>
17+
<h1>DiskMount <button id="version-button" class="version-button" data-action="openUpdate" type="button" disabled><span id="version">0.2.9</span><span id="update-indicator" class="update-indicator hidden" aria-hidden="true"></span></button></h1>
1818
</div>
1919
</div>
2020
<div class="toolbar">

‎DiskMount/Services/AnyLinuxFSService.swift‎

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,49 @@ final class AnyLinuxFSService {
2525
return DependencyState(available: true, path: path, version: rawVersion ?? "anylinuxfs", bundled: bundled)
2626
}
2727

28-
func mountReadWrite(devicePath: String) throws {
28+
func mountReadWrite(
29+
devicePath: String,
30+
deviceIdentifier: String,
31+
volumeName: String
32+
) throws {
2933
guard let path = executablePath else {
3034
throw AnyLinuxFSError.notInstalled
3135
}
36+
let arguments = Self.mountArguments(
37+
devicePath: devicePath,
38+
deviceIdentifier: deviceIdentifier,
39+
volumeName: volumeName
40+
)
3241
do {
3342
_ = try CommandRunner.runNTFSMountAsAdministrator(
3443
path,
3544
devicePath: devicePath,
36-
arguments: ["mount", devicePath, "--remount", "--ignore-permissions", "--window", "false"]
45+
arguments: arguments
3746
)
47+
let mounts = DiskService().activeAnyLinuxFSMounts()
48+
guard Self.hasVerifiedWritableMount(
49+
deviceIdentifier: deviceIdentifier,
50+
mounts: mounts
51+
) else {
52+
if mounts[deviceIdentifier] != nil {
53+
_ = try? CommandRunner.runAnyLinuxFSUnmountAsAdministrator(
54+
path,
55+
devicePath: devicePath
56+
)
57+
}
58+
_ = try? CommandRunner.run(
59+
"/usr/sbin/diskutil",
60+
arguments: ["mount", devicePath]
61+
)
62+
throw AnyLinuxFSError.mountVerificationFailed
63+
}
3864
} catch {
3965
if Self.isRawDiskPermissionError(error) {
4066
throw AnyLinuxFSError.fullDiskAccessRequired
4167
}
68+
if Self.isMissingMountVerificationError(error) {
69+
throw AnyLinuxFSError.mountVerificationFailed
70+
}
4271
throw error
4372
}
4473
}
@@ -57,18 +86,59 @@ final class AnyLinuxFSService {
5786
|| message.contains("operation not permitted")
5887
|| message.contains("file-read-data")
5988
}
89+
90+
static func mountArguments(
91+
devicePath: String,
92+
deviceIdentifier: String,
93+
volumeName: String
94+
) -> [String] {
95+
var arguments = ["mount", devicePath]
96+
if requiresASCIIMountPoint(volumeName) {
97+
arguments.append(safeMountPoint(deviceIdentifier: deviceIdentifier))
98+
}
99+
arguments.append(contentsOf: ["--remount", "--ignore-permissions", "--window", "false"])
100+
return arguments
101+
}
102+
103+
static func requiresASCIIMountPoint(_ volumeName: String) -> Bool {
104+
let forbidden = CharacterSet(charactersIn: "/:").union(.controlCharacters)
105+
return volumeName.isEmpty
106+
|| volumeName.data(using: .ascii, allowLossyConversion: false) == nil
107+
|| volumeName.rangeOfCharacter(from: forbidden) != nil
108+
}
109+
110+
static func safeMountPoint(deviceIdentifier: String) -> String {
111+
let allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
112+
let safeIdentifier = deviceIdentifier.filter { allowed.contains($0) }
113+
let suffix = safeIdentifier.isEmpty ? "External" : safeIdentifier
114+
return "/Volumes/DiskMount-\(suffix)"
115+
}
116+
117+
static func isMissingMountVerificationError(_ error: Error) -> Bool {
118+
error.localizedDescription.contains(CommandRunner.ntfsMountVerificationMarker)
119+
}
120+
121+
static func hasVerifiedWritableMount(
122+
deviceIdentifier: String,
123+
mounts: [String: DiskService.AnyLinuxFSMount]
124+
) -> Bool {
125+
mounts[deviceIdentifier]?.writable == true
126+
}
60127
}
61128

62129
enum AnyLinuxFSError: LocalizedError {
63130
case notInstalled
64131
case fullDiskAccessRequired
132+
case mountVerificationFailed
65133

66134
var errorDescription: String? {
67135
switch self {
68136
case .notInstalled:
69137
return "未检测到 anylinuxfs。请先执行:brew tap nohajc/anylinuxfs && brew install anylinuxfs"
70138
case .fullDiskAccessRequired:
71139
return "macOS 阻止了 NTFS 引擎读取原始磁盘。"
140+
case .mountVerificationFailed:
141+
return "NTFS 引擎未能建立可用的 NFS 挂载。"
72142
}
73143
}
74144
}

‎DiskMount/Services/CommandRunner.swift‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ enum CommandError: LocalizedError {
3838

3939
enum CommandRunner {
4040
private static let sudoSession = SudoSession()
41+
static let ntfsMountVerificationMarker = "DiskMount: NTFS engine exited without creating an NFS mount."
4142

4243
static func run(
4344
_ executable: String,
@@ -133,6 +134,18 @@ enum CommandRunner {
133134
devicePath: String,
134135
arguments: [String]
135136
) throws -> CommandResult {
137+
return try sudoSession.run(ntfsMountShellCommand(
138+
executable,
139+
devicePath: devicePath,
140+
arguments: arguments
141+
))
142+
}
143+
144+
static func ntfsMountShellCommand(
145+
_ executable: String,
146+
devicePath: String,
147+
arguments: [String]
148+
) -> String {
136149
let privilegedMount = ([executable] + arguments).map(shellQuote).joined(separator: " ")
137150
let fallbackMount = ["/usr/sbin/diskutil", "mount", devicePath]
138151
.map(shellQuote).joined(separator: " ")
@@ -144,11 +157,16 @@ enum CommandRunner {
144157
// unmounting the read-only volume, restore the normal macOS mount before returning.
145158
// Never create a duplicate read-only mount if a surviving anylinuxfs NFS mount still
146159
// owns this device after system wake.
147-
let command = "status=0; \(privilegedMount) || status=$?; "
160+
return "status=0; \(privilegedMount) || status=$?; "
161+
+ "if [ \"$status\" -eq 0 ]; then mounted=0; attempts=0; "
162+
+ "while [ \"$attempts\" -lt 20 ]; do "
163+
+ "if \(activeAnyLinuxFSMount); then mounted=1; break; fi; "
164+
+ "attempts=$((attempts + 1)); /bin/sleep 0.25; done; "
165+
+ "if [ \"$mounted\" -ne 1 ]; then "
166+
+ "/bin/echo \(shellQuote(ntfsMountVerificationMarker)) >&2; status=70; fi; fi; "
148167
+ "if [ \"$status\" -ne 0 ] && ! \(activeAnyLinuxFSMount); then "
149168
+ "\(fallbackMount) >/dev/null 2>&1 || true; fi; "
150169
+ "(exit \"$status\")"
151-
return try sudoSession.run(command)
152170
}
153171

154172
static func runAnyLinuxFSUnmountAsAdministrator(

‎DiskMount/Tests/CommandRunnerTests.swift‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,94 @@ final class CommandRunnerTests: XCTestCase {
6666
XCTAssertFalse(AnyLinuxFSService.isRawDiskPermissionError(error))
6767
}
6868

69+
func testChineseVolumeNameUsesASCIICustomMountPoint() {
70+
XCTAssertEqual(
71+
AnyLinuxFSService.mountArguments(
72+
devicePath: "/dev/disk35s1",
73+
deviceIdentifier: "disk35s1",
74+
volumeName: "大白菜U盘"
75+
),
76+
[
77+
"mount", "/dev/disk35s1", "/Volumes/DiskMount-disk35s1",
78+
"--remount", "--ignore-permissions", "--window", "false"
79+
]
80+
)
81+
}
82+
83+
func testASCIINameKeepsAnyLinuxFSDefaultMountPoint() {
84+
XCTAssertEqual(
85+
AnyLinuxFSService.mountArguments(
86+
devicePath: "/dev/disk66s1",
87+
deviceIdentifier: "disk66s1",
88+
volumeName: "SYSDISK"
89+
),
90+
[
91+
"mount", "/dev/disk66s1",
92+
"--remount", "--ignore-permissions", "--window", "false"
93+
]
94+
)
95+
}
96+
97+
func testUnsafeDeviceIdentifierIsSanitizedForMountPoint() {
98+
XCTAssertEqual(
99+
AnyLinuxFSService.safeMountPoint(deviceIdentifier: "磁盘 disk 35/s1;$()"),
100+
"/Volumes/DiskMount-disk35s1"
101+
)
102+
}
103+
104+
func testNTFSMountCommandVerifiesNFSPostconditionAndRestoresNativeMount() {
105+
let command = CommandRunner.ntfsMountShellCommand(
106+
"/Applications/Disk Mount/anylinuxfs",
107+
devicePath: "/dev/disk35s1",
108+
arguments: [
109+
"mount", "/dev/disk35s1", "/Volumes/DiskMount-disk35s1",
110+
"--remount", "--ignore-permissions", "--window", "false"
111+
]
112+
)
113+
114+
XCTAssertTrue(command.contains("'disk35s1.local:'"))
115+
XCTAssertTrue(command.contains("/bin/sleep 0.25"))
116+
XCTAssertTrue(command.contains(CommandRunner.ntfsMountVerificationMarker))
117+
XCTAssertTrue(command.contains("'/usr/sbin/diskutil' 'mount' '/dev/disk35s1'"))
118+
XCTAssertTrue(command.contains("'/Volumes/DiskMount-disk35s1'"))
119+
}
120+
121+
func testMissingMountVerificationErrorIsRecognized() {
122+
let error = CommandError.failed(
123+
executable: "/usr/bin/sudo",
124+
exitCode: 70,
125+
message: CommandRunner.ntfsMountVerificationMarker
126+
)
127+
128+
XCTAssertTrue(AnyLinuxFSService.isMissingMountVerificationError(error))
129+
}
130+
131+
func testNTFSSuccessRequiresExactWritableMount() {
132+
let writable = DiskService.AnyLinuxFSMount(
133+
deviceIdentifier: "disk35s1",
134+
mountPoint: "/Volumes/DiskMount-disk35s1",
135+
writable: true
136+
)
137+
let readOnly = DiskService.AnyLinuxFSMount(
138+
deviceIdentifier: "disk35s1",
139+
mountPoint: "/Volumes/DiskMount-disk35s1",
140+
writable: false
141+
)
142+
143+
XCTAssertTrue(AnyLinuxFSService.hasVerifiedWritableMount(
144+
deviceIdentifier: "disk35s1",
145+
mounts: ["disk35s1": writable]
146+
))
147+
XCTAssertFalse(AnyLinuxFSService.hasVerifiedWritableMount(
148+
deviceIdentifier: "disk35s1",
149+
mounts: ["disk35s1": readOnly]
150+
))
151+
XCTAssertFalse(AnyLinuxFSService.hasVerifiedWritableMount(
152+
deviceIdentifier: "disk35s1",
153+
mounts: ["disk66s1": writable]
154+
))
155+
}
156+
69157
func testParsesAnyLinuxFSMountByDeviceIdentifier() {
70158
let output = """
71159
/dev/disk3s1 on /System/Volumes/Data (apfs, local, journaled)

‎DiskMount/Web/WebPanelController.swift‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,11 @@ final class WebPanelController: NSViewController, WKScriptMessageHandler {
401401
case "mountNTFS":
402402
guard device.isNTFS else { throw PanelError.notNTFS }
403403
if !self.diskService.isMountedByAnyLinuxFS(deviceIdentifier: device.id) {
404-
try self.anyLinuxFS.mountReadWrite(devicePath: device.devicePath)
404+
try self.anyLinuxFS.mountReadWrite(
405+
devicePath: device.devicePath,
406+
deviceIdentifier: device.id,
407+
volumeName: device.name
408+
)
405409
}
406410
successMessage = self.localized(
407411
zh: "已将 \(device.name) 以 NTFS 读写模式加载",
@@ -568,6 +572,11 @@ final class WebPanelController: NSViewController, WKScriptMessageHandler {
568572
zh: "macOS 已阻止 NTFS 引擎读取原始磁盘。DiskMount 已尝试恢复普通只读挂载;请确认已为 DiskMount 开启“完全磁盘访问权限”和“可移动卷”权限,完全退出并重新打开 App 后再试。",
569573
en: "macOS blocked the NTFS engine from reading the raw disk. DiskMount attempted to restore the normal read-only mount. Enable Full Disk Access and Removable Volumes access for DiskMount, fully quit and reopen the app, then try again."
570574
)
575+
case .mountVerificationFailed:
576+
return localized(
577+
zh: "NTFS 引擎已经退出,但没有建立可用的 NFS 读写挂载。DiskMount 已尝试恢复 macOS 原生只读挂载;请刷新后重试。",
578+
en: "The NTFS engine exited without creating a usable NFS read/write mount. DiskMount attempted to restore the native macOS read-only mount; refresh and try again."
579+
)
571580
}
572581
}
573582
if let diskServiceError = error as? DiskServiceError {
@@ -599,7 +608,7 @@ final class WebPanelController: NSViewController, WKScriptMessageHandler {
599608

600609
private func publish() {
601610
let state = PanelState(
602-
version: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.2.8",
611+
version: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.2.9",
603612
devices: devices,
604613
dependency: anyLinuxFS.dependencyState(),
605614
proMode: proMode,

‎DiskMount/project.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ targets:
2424
PRODUCT_BUNDLE_IDENTIFIER: com.samni.DiskMount
2525
PRODUCT_NAME: DiskMount
2626
ARCHS: arm64
27-
MARKETING_VERSION: 0.2.8
28-
CURRENT_PROJECT_VERSION: 13
27+
MARKETING_VERSION: 0.2.9
28+
CURRENT_PROJECT_VERSION: 14
2929
INFOPLIST_FILE: Info.plist
3030
GENERATE_INFOPLIST_FILE: NO
3131
CODE_SIGN_STYLE: Manual

‎INSTALLATION.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44

55
## Before you install
66

7-
Download DiskMount only from the official [GitHub Releases page](https://github.com/samni728/diskmount/releases). Version 0.2.8 is self-contained and does not require Homebrew, Xcode, or a separate anylinuxfs installation.
7+
Download DiskMount only from the official [GitHub Releases page](https://github.com/samni728/diskmount/releases). Version 0.2.9 is self-contained and does not require Homebrew, Xcode, or a separate anylinuxfs installation.
88

99
The current package is signed with the developer's Apple Development certificate, but it is not signed with a Developer ID Application certificate and is not Apple-notarized. A Mac that downloads the DMG from the internet may therefore block the first launch. This Gatekeeper approval is separate from the disk permissions requested after DiskMount starts.
1010

1111
## Normal installation
1212

13-
1. Download `DiskMount-0.2.8-macOS26.dmg` and its `.sha256` file.
13+
1. Download `DiskMount-0.2.9-macOS26.dmg` and its `.sha256` file.
1414
2. Optionally verify the download in Terminal:
1515

1616
```bash
1717
cd ~/Downloads
18-
shasum -a 256 -c DiskMount-0.2.8-macOS26.dmg.sha256
18+
shasum -a 256 -c DiskMount-0.2.9-macOS26.dmg.sha256
1919
```
2020

2121
3. Open the DMG and drag `DiskMount.app` to `Applications`.
@@ -50,4 +50,4 @@ DiskMount contacts GitHub's public latest-Release API when it starts and when th
5050

5151
## Commercial distribution status
5252

53-
Version 0.2.8 is a development-signed community release. Warning-free public distribution requires a Developer ID Application certificate, hardened-runtime signing of the app and all bundled helpers, Apple notarization, and a stapled notarization ticket.
53+
Version 0.2.9 is a development-signed community release. Warning-free public distribution requires a Developer ID Application certificate, hardened-runtime signing of the app and all bundled helpers, Apple notarization, and a stapled notarization ticket.

0 commit comments

Comments
 (0)