Skip to content

Files created from the XML's NFC paths on FSKit ExFAT cannot be unlinked by the name readdir lists (macOS 26): headroom backups, likely expressport --prune #154

Description

@M-Igashi

Found while chasing a Mac app bug (Delete Backup refused with a permission error on an ExFAT library volume, Playlist mode only). The cause is in how files are named, and it applies to anything baken-core or baken-export creates or renames on an ExFAT or FAT32 volume from a path that came out of collection.xml.

Updated 2026-09-24 after re-measuring on an FSKit ExFAT disk image, which mounts exactly like a real stick (exfat, noowners, fskit). The first version of the table said an NFD-stored file can be unlinked by its NFC name; it cannot. It also missed the rename and expressport consequences below. Fix: #159.

What happens

macOS 26 mounts ExFAT and FAT through FSKit (mount shows exfat, noowners, fskit). On such a volume a name is stored in the normalisation form it was created or renamed with (checked on the raw image bytes), and readdir lists every name in NFD.

Operation NFC-stored file NFD-stored file
readdir lists it as NFD NFD
open / stat by NFC or NFD ok ok
unlink / rmdir by the listed (NFD) name ENOENT ok
unlink / rmdir by the NFC name ok ENOENT
mkdir, open(O_CREAT) with the other form finds the existing entry finds the existing entry
rename onto it with the other form replaces it; stored in the new form replaces it; stored in the new form

realpath returns the form it was given and F_GETPATH always returns NFC, so nothing recovers the stored form. renamex_np(RENAME_SWAP) is not supported. The consequences on macOS: rm -rf leaves the file behind and ends with "Directory not empty", Foundation's removeItem fails with a no-such-file or no-permission error, and Finder manages.

Why baken hits it

rekordbox writes every Location in NFC: 593 of the 593 non-ASCII locations in the reporting collection, none in NFD. Files put on the volume by Finder are stored NFD. Lookup accepts either form, so the NFC path from the XML opens the NFD file, and everything baken creates or renames under that path is stored NFC:

  • headroom::processor::backup_file creates copies at backup_dir.join(relative) from the XML path, so backups made in the Mac app's Playlist mode could not be removed by their listed names. Backups made from a directory scan (NFD names) were fine; that was the whole Folder-versus-Playlist difference in the app.
  • headroom's lossless (ffmpeg) path writes a temp file and renames it onto the XML path, which re-stores the user's own track under NFC. The same goes for the atomic XML write rbsort and cdjsafe use, and for cdjsafe's output files.
  • expressport writes the stick in NFC, which is right: rekordbox's export.pdb holds NFC paths and the player looks files up by them. But its own cleanup removed files by their listed names. On 4.0.1 every export ends with No such file or directory (os error 2) as soon as an artist, album or file name is non-ASCII, because the AppleDouble cleanup cannot remove the ._ files macOS leaves beside new files and directories (it does so for directories too, com.apple.provenance). Everything is written by then, so the stick works, but the command reports failure. --prune fails the same way, and it compared the NFD listing against the NFC paths it meant to keep.

What to do (done in #159)

  • Create and rename under the NFD form of the name on macOS (baken_core::fsname::native), and leave names alone elsewhere, where two forms are two files. This covers headroom backups, temp files and the lossless replace, cdjsafe outputs and the atomic XML write. The replace stays a single atomic rename.
  • Keep the stick in NFC, and make --prune compare in NFC and remove by the listed name with an NFC retry (macOS only). The AppleDouble cleanup does the same and also takes ._Contents and ._PIONEER at the stick root.
  • unicode-normalization is the dependency this takes.
  • scripts/fskit-check.py reproduces all of the above on a disk image: 4.0.1 fails 8 of its 11 checks, Name files the macOS way and remove stick files by their stored name (#154) #159 passes all of them. It runs locally before a release, since CI has no FSKit.
  • The Mac app fixes deletion on its side regardless (unlink by listed name, retry in NFC, M-Igashi/baken-mac#46), because copies that already exist on users' drives have to be deletable.
  • Apple Feedback is worth filing with the table above; not blocking.

Discovered 2026-09-24 on macOS 26.0 (Darwin 27.0.0), baken-core 4.0.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions