One Extra Slash: Hunting a Windows Path Bug Down to a Single Formatter
DEV Community

One Extra Slash: Hunting a Windows Path Bug Down to a Single Formatter

This is my entry for DEV's Summer Bug Smash - Smash Stories. Not every good bug is an epic. Some of them are one character long, and the interesting part is entirely in the hunt.

I was consuming the JSON output of a developer CLI - machine-readable mode, the structured projection you're supposed to build tooling against. My code was doing something completely ordinary: taking a file path out of that JSON and comparing it against a path I already had. The comparison kept failing on paths that were obviously, visibly identical.

They weren't identical. The JSON contained:

C://Users/me/project/src/index.ts

Two separators after the drive letter. One too many.

Why this is worse than it looks

Your first reaction to a doubled slash is probably "so what?" - and for a lot of purposes, you're right. Hand C://Users/me/file.ts to most filesystem APIs on Windows and it will happily open the file. The OS and most path libraries collapse redundant separators. Nothing crashes. Nothing errors. You can go a long time without noticing.

The damage shows up the moment anything treats a path as a string rather than as a path:

  • String equality between two paths silently returns false
  • Anything used as a map key or cache key produces a duplicate entry for the same physical file
  • Deduplication logic keeps both variants
  • Diffs and logs show two "different" files that are the same file
  • Any downstream consumer that splits on the separator gets a phantom empty segment

That's the real cost. It doesn't fail loudly at the source. It fails quietly, later, in whatever unlucky code is doing the comparing - which in this case was mine. This is a bug that spends its whole life framing someone else.

Step one: is it actually them, or is it me?

This is the step I think people skip too often, and it's the one that earns you the right to file the report. My first assumption was that I had mangled the path myself. Something in my own pipeline joining, normalising, or concatenating badly.

So before looking anywhere else, I went and checked the raw bytes coming out of the CLI, before my code touched them at all. The doubled separator was already there. It arrived that way. That reframed the problem from "fix my code" to "characterise someone else's bug" - and those need very different work.

A bug report that says "your tool is broken" is worth almost nothing. A bug report that says "your tool is broken here specifically, and not there" is worth a lot.

Step two: scope it against its siblings

The key question was: how wide is this? Is every path this tool emits affected, or just one? So I ran the sibling commands - the other subcommands that emit paths in the same structured mode - and compared their output against the one that was misbehaving.

The result was clean and specific: the human-readable output was correct. The other commands' structured output was correct. Only one particular projection in one particular command emitted the doubled separator.

That single fact does enormous work:

  • It rules out the underlying path library. If the shared library were broken, every command would be wrong.
  • It rules out platform-level normalisation and my shell environment, for the same reason.
  • It localises the defect to one formatting site - one place where a path gets assembled for output.

By the end of this step I hadn't seen a line of their source, but I could say with confidence that this was a local formatting bug in a single code path, not a systemic one. That's a genuinely useful thing to be able to hand a maintainer.

Step three: the mechanism

The shape of this bug is a classic, and once you've seen it once you'll recognise it forever. Somewhere, a path is being assembled roughly like:

root + separator + relativePart

On POSIX that's usually harmless-looking, because a root like /home/me doesn't end in a separator. On Windows, a drive root is different: C:/ (or C:\) already terminates with a separator. It has to - C: alone means "the current directory on drive C", which is a different thing entirely. The trailing separator isn't decoration, it's semantic.

So the naive join appends a separator to a string that already ends in one, and you get C://. The bug only manifests when the root is a drive root, which is why it hides so well: any path assembled relative to a normal subdirectory looks perfectly fine, and only the drive-root case exposes it.

This is why the correct tool is the platform's path-join function rather than string concatenation. Join functions exist precisely to know that a root may already carry its separator. Every time someone hand-rolls a join with +, this bug is one Windows user away.

Step four: report it properly, and bring the test

I filed it upstream with the things I'd want if I were the maintainer:

  • A minimal reproduction - the exact command, on the exact platform, with the exact offending output.
  • The scoping evidence - that sibling commands and the human-readable output are all correct, so the fault is localised to that one projection rather than the shared path handling. This is the part that saves a maintainer an hour of hunting.
  • The proposed mechanism - drive-root joins double the separator, with the reasoning above.
  • A suggested regression test.

That last one matters more than people give it credit for. The test I proposed asserts that the emitted path for a file at a drive root contains no doubled separator - specifically the drive-root case, not a generic path case. That specificity is the whole point. A test using a normal nested path passes happily against the broken code and protects nothing. The bug lives only at the drive root, so the test has to live at the drive root too. A regression test that doesn't reproduce the original bug is decoration.

What I took from it

  1. Prove it isn't yours before you file. Checking the raw output before my own code touched it took two minutes and completely changed what I was doing. Half of "their bug" reports are ours.
  2. Scope before you speculate. Running the sibling commands was the highest-value five minutes of the whole hunt. It converted "something's wrong with this tool" into "this one formatter is wrong and the shared library is fine" - without reading their source at all. You can characterise a bug from the outside far more precisely than most people attempt.
  3. Windows drive roots are a genuine edge case, not a nuisance. C:/ ending in a separator is correct and load-bearing. Any hand-rolled path concatenation that assumes roots don't end in separators is already broken; it just hasn't met a Windows user yet.
  4. Ship the test with the report. A regression test that fails on the current code and passes after the fix is the single most useful artifact you can attach to a bug report - and it forces you to state precisely what the correct behaviour is, which is a good discipline even when nobody else ever reads it.

One extra slash. Nothing crashed, nothing errored, and it still cost real time - because the failure surfaced in a completely different codebase from the one that caused it.

Comments

No comments yet. Start the discussion.