Skip to content

Releases: sohonetlabs/testfs

TestFS 0.1.16

Choose a tag to compare

@benroeder benroeder released this 29 Apr 13:03

What's new in 0.1.16

Closes the audit's #46: deterministic mount failures no longer wait the full 15-second confirmMountedOrRollback timeout before surfacing.

Fast-fail rollback (#46)

Previously, mounts that failed inside the FSKit extension's loadResource (malformed tree JSON, BuildError.duplicateName, BuildError.totalSizeOverflow, etc.) still took the full 15s before the host rolled back — mount(8) returns success synchronously and only the extension's async loadResource saw the error.

Cross-process failure marker channel:

  • Extension writes ~/Library/Containers/com.sohonet.testfsmount.appex/Data/Library/Application Support/TestFS/failed-<bsd>.json (a LoadFailureMarker Codable with {"error": "<localized description>"}) before calling replyHandler(nil, error).
  • Host's confirmMountedOrRollback polls statfs (success) AND the marker (failure) on each 100 ms tick. Whichever fires first wins.
  • Stage-time delete in prepareMount ensures a recycled /dev/diskN can't false-trigger off a stale marker from a prior failed mount.
  • Marker is removed in detach as part of normal rollback.

MountConfirmResult enum (.mounted / .failed(reason: String?)) carries the underlying error up to the UI, replacing the previous generic "the kernel accepted the mount but the FSKit extension didn't load the volume" with the actual decode/build error.

Auto-update

Existing 0.1.15 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….

Known follow-ups

  • #55 — re-add host-side pre-flight: TreeBuilder.parseAndBuild from MountManager.swift requires TreeBuilder/JSONTree/TreeIndex to be members of the host target (currently extension-only). Tracked separately, no user-facing impact (the marker channel covers the same failure modes via the extension round-trip).

TestFS 0.1.15

Choose a tag to compare

@benroeder benroeder released this 29 Apr 11:58

What's new in 0.1.15

A focus release covering 18 issues filed across three rounds of audits against the 0.1.14 codebase.

Reliability

  • Re-registration race: the FSKit toggle no longer races the user's first Mount click after an update (#21, #32).
  • Mount confirmation uses ContinuousClock with a 15s budget — sleep/wake mid-mount no longer skews the deadline (#20).
  • One wedged hdiutil/mount no longer hangs the whole MountManager actor — bounded subprocess timeout with SIGTERM→SIGKILL escalation (#42).

Correctness

  • Volume capability flipped to .sensitive so the runtime contract matches the personality plist and TreeIndex byte-keyed lookup. Closes the kernel-name-cache aliasing risk for case-distinct siblings (#17).
  • TreeBuilder rejects file sizes that would overflow the volume's block-count math instead of trapping at first statfs (#18).
  • parseSize returns nil on Int multiply overflow instead of crashing the host when an oversized value lands in the block size field (#19).
  • JSON loader rejects malformed top-level entries instead of silently truncating (#22).
  • Invalid mtime in the sidecar is rejected at load time, not silently rewritten to 1970-01-01 (#24).
  • Directory st_nlink is now 2 + immediate-subdirectory-count, matching the Unix convention (#25).
  • MountRegistry preserves app metadata across canonical-path variants (/tmp vs /private/tmp) and across /dev/diskN reuse (#31, #41).
  • lookupItem / enumerateDirectory return ENOTDIR (not ENOENT) when the target is a file (#43).

Tooling

  • scripts/install.sh always builds the current checkout into build/derived (was: could install another clone's app on a multi-clone machine) (#33).
  • scripts/mount.sh allocates a per-UUID image instead of the singleton dummy.img, so concurrent CLI mounts no longer stomp each other's sidecar state (#45).
  • scripts/mount.sh encodes the sidecar JSON via python3, surviving filenames containing ", \, or newlines (#34).
  • scripts/unmount.sh parses hdiutil info -plist instead of the brittle text format, mirroring the Swift sweep (#44).
  • scripts/update_appcast.rb is idempotent — re-running for the same version replaces in place rather than duplicating items (#35).
  • LaunchServices now gets pruned of stale TestFS.app registrations during install/release so extensionkitd can't resolve a phantom UUID.

Known issue (deferred to 0.1.16)

  • Mounts that fail at loadResource (malformed sidecar, missing config) still take the full 15s before the error surfaces. The fix needs a cross-process failure-marker channel whose lifecycle deserves more design than this release has time for. Tracked in #46.

Auto-update

Existing 0.1.14 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….

TestFS 0.1.14

Choose a tag to compare

@benroeder benroeder released this 28 Apr 16:53

What's new

Verify FSKit volume came up before recording the mount (#15)

mount(8) returning 0 only confirms the kernel queued the mount; the FSKit extension's loadResource runs asynchronously after that and could still fail (JSON parse error, missing config, the case-fold collision in 0.1.12 and earlier). The host app was recording the mount as successful and then the user would see the volume in the Live mounts list but Finder wouldn't show any files.

After mount(8), poll statfs(2) for f_fstypename == "testfs" with a 3-second timeout. On failure, unmount + detach so we don't leave an orphaned dev node. Status message now points at Show log… for the extension's actual error.

statfs over notification-watching: kernel VFS state is the authoritative source — log delivery is asynchronous and notifications routed through diskarbitrationd are opaque.

Auto-update

Existing 0.1.13 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….

TestFS 0.1.13

Choose a tag to compare

@benroeder benroeder released this 28 Apr 16:09

What's new

Match Python upstream: case-sensitive lookup, drop fold (#14)

The Swift port had been case-folding via TreeIndex.fold()String.lowercased(), deviating from the Python jsonfs.py upstream which is fully case-sensitive. Two compounding bugs:

  • Case-fold collapsed Foo.txt and foo.txt as siblings.
  • String.lowercased() is canonical-equivalence-aware, so even unicode_normalization=none couldn't keep NFC é (U+00E9) and NFD é (U+0065+U+0301) distinct. archive_torture_evil_filenames.json was unmountable as a result.

Drop the fold entirely. Switch the lookup map from [String: …] to [Data: …] keyed by raw UTF-8 bytes (Swift String Dictionary keys are also canonical-equivalence-hashed, so the byte-keyed form is required for upstream parity). Flip FSPersonalities.TestFSPersonality.FSfileObjectsAreCaseSensitive to true to tell the kernel the volume is now case-sensitive.

Renamed BuildError.caseFoldCollision.duplicateName since the only collisions left are literal duplicates after normalization.

89/89 SPM tests pass including 4 new tests in Tests/TestFSCoreTests/CaseSensitivityTests.swift that pin the contract.

Auto-update

Existing 0.1.12 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….

TestFS 0.1.12

Choose a tag to compare

@benroeder benroeder released this 28 Apr 14:00

What's new

Mount post-Sparkle-update — actually working this time

0.1.11 still failed live on the test machine. Diagnosis: when pluginkit is already at +, calling pluginkit -e use again is a no-op. There's no state transition, so extensionkitd doesn't drop its cached UUID and re-resolve against the post-update bundle.

The user's manual toggle off then on fix works because the transition is what kicks extensionkitd. Reproduced programmatically:

pluginkit -e ignore -i com.sohonet.testfsmount.appex
pluginkit -e use -i com.sohonet.testfsmount.appex

AppEnvironment.reregisterExtensionIfNeeded now does the four-step sequence (lsregister + pluginkit -a + pluginkit -e ignore + pluginkit -e use) on every version change. Same toggle cycle in scripts/install.sh. Confirmed live on the M4 Max test machine where 0.1.11 was still failing.

TestFS 0.1.11

Choose a tag to compare

@benroeder benroeder released this 28 Apr 13:43

What's new

Mount actually works after auto-update now

The 0.1.9 / 0.1.10 attempts at this fix were under-medicating. Diagnosed live: pluginkit -a registers the extension but leaves it in the default adjudication state (whitespace prefix in pluginkit -m -A). mount refuses non-+ extensions, hence the recurring extensionKit.errorDomain error 2 / File system named testfs not found failures.

pluginkit -e use is the programmatic equivalent of the System Settings toggle — flips + directly, runs without admin. 0.1.11 calls it as the third step of the auto-re-register sequence (after lsregister -f and pluginkit -a). Confirmed working on a fresh M4 Max / macOS 26.4.1 install where every prior version failed.

scripts/install.sh now also runs pluginkit -e use so manual install converges on the same end state as the Sparkle update path.

Internal hygiene

  • runSilently() now returns Bool. reregisterExtensionIfNeeded only stamps lastRegisteredVersion in UserDefaults if all three commands succeeded — a transient failure now retries on the next launch instead of locking us out until the next version bump.

TestFS 0.1.10

Choose a tag to compare

@benroeder benroeder released this 28 Apr 13:13

What's new

Bundle-version hygiene

The embedded TestFSExtension.appex was stuck at CFBundleVersion = 1 while the host kept incrementing — Apple requires the match (warning at build, hard ITMS-90473 error on App Store), and lsd's bookkeeping is more reliable when the two are synced. The 'Bump build number' Xcode phase now patches both plists with the same VERSION + build, then re-signs the appex in place preserving its identifier / entitlements / flags.

This complements 0.1.9's auto-re-register fix by removing one more variable from the extensionkitd adjudication path.

Auto-update

Existing 0.1.9 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….

TestFS 0.1.9

Choose a tag to compare

@benroeder benroeder released this 28 Apr 12:54

What's new

Auto-fix the post-update mount failures

The headline fix. After every Sparkle update, extensionkitd was holding a stale UUID for the just-replaced FSKit extension — System Settings would show the toggle still 'on' but mount would fail with com.apple.extensionKit.errorDomain error 2 / File system named testfs not found until the user toggled the switch off and back on.

0.1.9 detects a version change on launch and runs lsregister -f + pluginkit -a automatically (the same commands scripts/install.sh runs by hand). extensionkitd re-resolves the extension before you click Mount, breaking the staleness window.

This is the same class of issue macFUSE 5.2.0 worked around at the FSKit/PluginKit layer.

Fallback for users on older macOS / when the auto-fix fails

The targeted in-app status text from 0.1.8 still fires if extensionkitd somehow stays stale despite the auto-re-register — pointing you at the manual toggle-off-then-on as a backup.

Auto-update

Existing 0.1.8 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….

TestFS 0.1.8

Choose a tag to compare

@benroeder benroeder released this 28 Apr 12:37

What's new

Mount

  • Better diagnosis when mount fails with ExtensionKit error 2. macOS sometimes refuses to invoke the FSKit extension after an app update — the System Settings toggle still reads "on" but the adjudication is stale. The status line now identifies that signature specifically and tells you to toggle TestFS off and back on under General → Login Items & Extensions → File System Extensions. (Captured from a real failing mount thanks to 0.1.7's diagnostic logging.)
  • Fixes mount of archive_torture_format_sentinels.json and any other tree that ships its own .metadata_never_index file. The mount used to fail with a case-fold collision because we re-added our own copy on top; now we dedupe against the tree's contents.

Visual

  • Release builds: ladybug overlay renders solid black as the brand mark. The previous attempt at multicolor wasn't engaging via NSImage's symbol path. Debug builds still get a solid red ladybug, distinct at a glance.

Internal

  • Package.swift excludes build/ so swift test doesn't trip over Sparkle's localized resources staged there by scripts/release.sh. 85 SPM tests now run cleanly again, including a new regression test for the cache-files dedup.

TestFS 0.1.7

Choose a tag to compare

@benroeder benroeder released this 28 Apr 12:15

What's new

Mount error reporting

  • The status line now shows the actual mount(8) output verbatim, then a list of common causes — no more confidently mis-classifying every Operation not permitted as a TCC denial. The previous wording sent users hunting for privacy fences when the real cause was often something else (stale extension registration, fskitd state).
  • Drop the misleading "try under your home folder root" suggestion — macOS 26 appears to extend privacy fences into more of $HOME than just the named subdirs. /tmp/<name> is now the only path the message confidently calls safe.
  • Full mount(8) stdout / stderr / exit code is now logged to OSLog from MountManager so the in-app Show log… window captures it for post-hoc diagnosis. Until now, every failure dropped the raw bytes and only kept Foundation's localizedDescription.