Skip to content

Workspace: on two kinds of folding disk, listedName keeps a spelling disk doesn't list, so one file gets two workspace entries #662

Open

Description

@nissa-seru

diskSpelling gives each segment of a path the name disk lists for it, through listedName (src/modules/workspace/observe.ts at main 97ca7eba). When the segment as given isn't listed, listedName looks up its variants: its letters' case flipped, NFC and NFD. If a variant finds the same file as the segment (sameFile, by device and inode), the directory folds. It then takes a listed name equal to the segment in lower-case NFC, or failing that one equal by foldedLoosely (NFC, then lower, upper and lower case) that is the same file. Otherwise the segment keeps the spelling it was given, and the workspace holds the file under that spelling beside the one disk lists. Two kinds of folding disk get there, each shown by a run on main's sources at 97ca7eba (Linux, Node 24.22.0). The disks are FUSE passthroughs that list each name as their backing directory spells it.

1. A disk that drops default-ignorable code points. These are characters such as a soft hyphen (U+00AD), a zero-width joiner (U+200D) or a variation selector (U+FE0F). Linux's casefold, which ext4 and f2fs set per directory, drops them. The kernel kept that in 231825b2, which reverts 5c26d2f1, because on-disk name hashes depend on it, and stable kernels have it from v6.12.5. HFS Plus's case-insensitive comparison skips some of them too (Apple TN1150). Neither of listedName's comparisons drops them.

Run: the passthrough's lookups fold as utf8_casefold does by the kernel's description (default-ignorables dropped, NFD, full case folding, NFD). A mount on it holds gate.txt.

  • write outer/ga\u00adte.txt (a soft hyphen after ga) succeeds.
  • materialize writes nothing, and skips it: "ga\u00adte.txt: disk holds a different copy, and nothing records which is newer (conflict: store-origin-collision)".
  • read outer/gate.txt gives the file's old content, and read outer/ga\u00adte.txt gives the new content.
  • ls outer lists both gate.txt and ga\u00adte.txt. Disk lists only gate.txt, with its old content.

2. A disk that folds case but numbers a file anew for each spelling it's looked up by. One example is a filesystem on libfuse's high-level API mounted without use_ino. There, the flipped variant finds the same file under another inode number, so listedName reads the directory as one that tells case apart.

Run: the passthrough folds case and Unicode normalization (Python's casefold, NFC), and is mounted without use_ino. One directory is inner at inode 29 and INNER at inode 32. A mount on it holds gate.txt.

  • write outer/GATE.TXT succeeds.
  • materialize skips it with the same conflict.
  • read outer/gate.txt gives the old content, and read outer/GATE.TXT the new.
  • ls outer lists both gate.txt and GATE.TXT.

Likely fixes, by our reading, unrun:

What would test it: CI has no disk of either kind, and we haven't seen whether APFS drops default-ignorables. So a test would need readdir and lstat replaced, as workspace-path-spelling.test.ts's withLstat replaces lstat.

Found in review of #624, which compares a path with a nested mount's directory in these ways and leaves listedName as it is.

Written by Nell-1783.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions