Skip to content

Commit 7e988fc

Browse files
bpamiriclaude
andauthored
refactor(cli): unify path normalization, add tests + Windows install docs (#2863)
* refactor(cli): unify path normalization, add tests + Windows install docs Follow-up to #2835, which fixed the Windows "no Resource provider [c]" crash (#2841). This does not change the runtime fix #2835 shipped: - Move the canonical normalizePath() into Helpers.cfc (its natural home beside capitalize/pluralize/stripSpecialChars) and reduce Module.$normalizePath() to a one-line delegating wrapper. Helpers is a dependency-free leaf utility, so it is safe to construct during init(). The 6 call sites and the $safeDirExists() fallback are unchanged. - Add 7 normalizePath() regression specs to HelpersSpec.cfc. Because Module delegates to Helpers now, these exercise the real bootstrap path rather than a parallel copy. - Document the Windows "there is no Resource provider available with the name [c]" failure in the CLI installation guide. Supersedes #2843. Verified locally: 667/667 cli/lucli specs pass. Signed-off-by: Peter Amiri <peter@alurium.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(cli): guard normalizePath mid() edge and broaden Windows update docs Address Reviewer A on #2863: - Guard the mid(rv, 3, len(rv) - 2) call in Helpers.normalizePath() against a count of 0 when rv is exactly "//" — cf. CLAUDE.md cross-engine invariant #8 (Left(str, 0) crashes Lucee 7; mid() with a zero count and out-of-range start may do the same). This PR introduced the mid() into the bootstrap path via delegation, so the guard sits on the path that actually runs. Output is unchanged for every real input. - Add a regression spec for the degenerate "//" root. - Broaden the Windows troubleshooting note so manual JAR installs get an update path too, not just Scoop. Verified locally: 668/668 cli/lucli specs pass. Signed-off-by: Peter Amiri <peter@alurium.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5661859 commit 7e988fc

5 files changed

Lines changed: 112 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo
2929
### Changed
3030

3131
- Version switcher now labels the 4.0 stable docs "v4.0 (current)" (was "v4.0.0"); the vestigial pre-GA `v4-0-1-snapshot` guides tree is removed and its one unique page, "Reading the Changelog", is salvaged into `v4-0-0/upgrading/`. Both sites deploy from `develop`, so in-progress patch docs already live in the `v4-0-0` tree; a separate `*-snapshot` tree is only warranted when a different minor/major (e.g. `v4-1-snapshot`) is under development. Courtesy redirects cover the high-traffic `/v4-0-1-snapshot/*` paths (#2827)
32+
- CLI path normalisation now lives in a single, unit-tested `Helpers.normalizePath()`; `Module.$normalizePath()` (added in #2835 to fix the Windows `Resource provider [c]` crash) delegates to it instead of carrying a private copy, so the regression coverage exercises the real bootstrap path rather than a decoy. The CLI installation guide also gains a Windows troubleshooting entry for the original `there is no Resource provider available with the name [c]` error (#2841)
3233

3334
### Fixed
3435

cli/lucli/Module.cfc

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,19 @@ component extends="modules.BaseModule" {
4949
}
5050

5151
/**
52-
* Normalize a filesystem path for safe handoff to Lucee file APIs on
53-
* Windows. Replaces all backslashes with forward slashes — Lucee
54-
* accepts both on Windows, but mixed-slash strings can trip
55-
* ResourceUtil's URI scheme detection (see init() comment).
52+
* Bootstrap-safe wrapper around `Helpers.normalizePath()` — the single
53+
* source of truth for path normalisation (GH #2841). Collapses Windows
54+
* backslashes to forward slashes so a mixed-slash path like
55+
* `C:\Users\cy/blog` can't trip Lucee's Resource API into reading `c:`
56+
* as a URI scheme (see init() comment).
5657
*
57-
* No-op on a path that already uses forward slashes (Mac/Linux,
58-
* already-normalized Windows paths).
58+
* Helpers is instantiated directly rather than via `getService()`
59+
* because `$normalizePath()` runs inside `init()` before
60+
* `variables.services` exists. Helpers is a dependency-free leaf
61+
* utility, so constructing it at bootstrap is cheap and safe.
5962
*/
6063
private string function $normalizePath(required string p) {
61-
return replace(arguments.p, "\", "/", "all");
64+
return new services.Helpers().normalizePath(arguments.p);
6265
}
6366

6467
/**

cli/lucli/services/Helpers.cfc

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,37 @@ component {
104104
return trim(reReplace(str, "[{}()^$&%##!@=<>:;,~`'*?/+|\[\]\-\\]", "", "all"));
105105
}
106106

107+
/**
108+
* Convert any filesystem path to a single-slash, forward-slash form so
109+
* it is safe to hand to Lucee's file APIs on Windows.
110+
*
111+
* Regression: GH #2841 — `wheels new` / `wheels start` on Windows blew
112+
* up with `lucee.runtime.exp.NativeException: there is no Resource
113+
* provider available with the name [c]`. The bootstrap handed
114+
* `java.io.File.getCanonicalPath()` output (e.g. `C:\Users\tim\Projects`)
115+
* to `directoryExists(... & "/vendor/wheels")`, producing the mixed-slash
116+
* string `C:\Users\tim\Projects/vendor/wheels`. Lucee's Resource API
117+
* parsed `c:` as a URI scheme and bailed because no `c` provider is
118+
* registered. Normalising to pure forward slashes keeps the path
119+
* unambiguous on Windows while being a no-op on POSIX.
120+
*
121+
* `Module.$normalizePath()` delegates here so the bootstrap path and the
122+
* unit tests exercise one implementation (#2835 originally carried a
123+
* private copy inside Module.cfc).
124+
*/
125+
public string function normalizePath(required string path) {
126+
if (!len(arguments.path)) return "";
127+
var rv = replace(arguments.path, "\", "/", "all");
128+
// Collapse doubled slashes from naïve concatenation, but preserve a
129+
// leading `//` (UNC / network-share prefix on Windows). Guard the
130+
// mid() against a count of 0 (when rv is exactly "//"), which can
131+
// trip Lucee 7's string-range handling (cf. CLAUDE.md cross-engine #8).
132+
var leading = left(rv, 2) == "//" ? "//" : "";
133+
var body = len(leading) ? (len(rv) > 2 ? mid(rv, 3, len(rv) - 2) : "") : rv;
134+
body = reReplace(body, "/{2,}", "/", "all");
135+
return leading & body;
136+
}
137+
107138
/**
108139
* Generate a migration timestamp (YYYYMMDDHHMMSS)
109140
*/

cli/lucli/tests/specs/services/HelpersSpec.cfc

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,64 @@ component extends="wheels.wheelstest.system.BaseSpec" {
9797

9898
});
9999

100+
describe("normalizePath()", () => {
101+
102+
// Regression: GH #2841 — `wheels new`/`wheels start` on Windows
103+
// failed with "lucee.runtime.exp.NativeException: there is no
104+
// Resource provider available with the name [c]". The CLI
105+
// concatenated a Windows-form path (backslashes from
106+
// java.io.File.getCanonicalPath()) with "/vendor/wheels" and
107+
// fed the mixed-slash result to Lucee's Resource API, which
108+
// then parsed "c:" as a URI scheme. Forward-slash normalization
109+
// makes the path unambiguous on Windows while being a no-op on
110+
// POSIX. Module.$normalizePath() delegates to this method, so
111+
// these cases cover the real bootstrap path — not a copy.
112+
113+
it("converts Windows backslashes to forward slashes", () => {
114+
expect(helpers.normalizePath("C:\Users\tim\Projects"))
115+
.toBe("C:/Users/tim/Projects");
116+
});
117+
118+
it("leaves POSIX paths unchanged", () => {
119+
expect(helpers.normalizePath("/home/runner/work/wheels"))
120+
.toBe("/home/runner/work/wheels");
121+
});
122+
123+
it("returns an empty string for empty input", () => {
124+
expect(helpers.normalizePath("")).toBe("");
125+
});
126+
127+
it("collapses doubled forward slashes from concatenation", () => {
128+
expect(helpers.normalizePath("/a/b//c")).toBe("/a/b/c");
129+
});
130+
131+
it("preserves a Windows drive-letter prefix after normalization", () => {
132+
var normalized = helpers.normalizePath("C:\Users\tim\Projects");
133+
expect(normalized & "/vendor/wheels")
134+
.toBe("C:/Users/tim/Projects/vendor/wheels");
135+
// Sanity: no remaining backslash means downstream
136+
// directoryExists() won't trip Lucee's scheme parser.
137+
expect(find("\", normalized)).toBe(0);
138+
});
139+
140+
it("preserves a UNC network-share prefix", () => {
141+
expect(helpers.normalizePath("//server/share/path"))
142+
.toBe("//server/share/path");
143+
});
144+
145+
it("collapses doubled slashes inside a UNC path without eating the prefix", () => {
146+
expect(helpers.normalizePath("//server//share"))
147+
.toBe("//server/share");
148+
});
149+
150+
it("handles a bare double-slash root without a mid() range error", () => {
151+
// Degenerate UNC root: rv === "//" makes the internal mid()
152+
// count 0. Guarded so it can't trip Lucee 7 (cross-engine #8).
153+
expect(helpers.normalizePath("//")).toBe("//");
154+
});
155+
156+
});
157+
100158
});
101159

102160
}

web/sites/guides/src/content/docs/v4-0-0/command-line-tools/installation.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,18 @@ On Linux, the `.deb`/`.rpm` package installs `/usr/bin/wheels`, which should be
233233

234234
The wheels formula deliberately isolates runtime state under `~/.wheels/` (via `LUCLI_HOME`) so a standalone `lucli` install — which uses `~/.lucli/` — stays out of the way. If you previously had LuCLI installed directly and see odd module-resolution errors, check that the wrapper set `LUCLI_HOME` correctly (`wheels system env` will dump the resolved environment) and that `~/.wheels/modules/wheels/` contains a current `Module.cfc` and `.module-version` file.
235235

236+
### Windows: `there is no Resource provider available with the name [c]`
237+
238+
On Windows, `wheels new`, `wheels start`, and most other subcommands crashed before any work could happen with:
239+
240+
```
241+
lucee.runtime.exp.NativeException: there is no Resource provider available
242+
with the name [c], available resource providers are [ftp, zip, tar, tgz,
243+
http, https, ram, s3]
244+
```
245+
246+
The cause was mixed-slash paths: `java.io.File.getCanonicalPath()` on Windows returns backslash form (`C:\Users\tim\Projects`), which — when concatenated with a forward-slash suffix — produced a string like `C:\Users\tim\Projects/vendor/wheels`. Lucee's Resource API parsed `c:` as a URI scheme and bailed because no `c` provider is registered. This is fixed in the release that includes #2841. Update to the latest version and the error will not recur — `scoop update wheels` for Scoop installs, or re-fetch the latest Wheels Module (see **Manual JAR install** above) if you wired it up by hand. `wheels --version` was unaffected because LuCLI handles that flag before dispatching to the module.
247+
236248
## Related commands
237249

238250
<CardGrid>

0 commit comments

Comments
 (0)