diff --git a/CHANGELOG.md b/CHANGELOG.md index 99e5ec8d3f..ee35d59ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo ### Changed - 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) +- 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) ### Fixed diff --git a/cli/lucli/Module.cfc b/cli/lucli/Module.cfc index d91faee0ed..6f8b1d9daf 100644 --- a/cli/lucli/Module.cfc +++ b/cli/lucli/Module.cfc @@ -49,16 +49,19 @@ component extends="modules.BaseModule" { } /** - * Normalize a filesystem path for safe handoff to Lucee file APIs on - * Windows. Replaces all backslashes with forward slashes — Lucee - * accepts both on Windows, but mixed-slash strings can trip - * ResourceUtil's URI scheme detection (see init() comment). + * Bootstrap-safe wrapper around `Helpers.normalizePath()` — the single + * source of truth for path normalisation (GH #2841). Collapses Windows + * backslashes to forward slashes so a mixed-slash path like + * `C:\Users\cy/blog` can't trip Lucee's Resource API into reading `c:` + * as a URI scheme (see init() comment). * - * No-op on a path that already uses forward slashes (Mac/Linux, - * already-normalized Windows paths). + * Helpers is instantiated directly rather than via `getService()` + * because `$normalizePath()` runs inside `init()` before + * `variables.services` exists. Helpers is a dependency-free leaf + * utility, so constructing it at bootstrap is cheap and safe. */ private string function $normalizePath(required string p) { - return replace(arguments.p, "\", "/", "all"); + return new services.Helpers().normalizePath(arguments.p); } /** diff --git a/cli/lucli/services/Helpers.cfc b/cli/lucli/services/Helpers.cfc index 982a58b206..110ee927ca 100644 --- a/cli/lucli/services/Helpers.cfc +++ b/cli/lucli/services/Helpers.cfc @@ -104,6 +104,37 @@ component { return trim(reReplace(str, "[{}()^$&%##!@=<>:;,~`'*?/+|\[\]\-\\]", "", "all")); } + /** + * Convert any filesystem path to a single-slash, forward-slash form so + * it is safe to hand to Lucee's file APIs on Windows. + * + * Regression: GH #2841 — `wheels new` / `wheels start` on Windows blew + * up with `lucee.runtime.exp.NativeException: there is no Resource + * provider available with the name [c]`. The bootstrap handed + * `java.io.File.getCanonicalPath()` output (e.g. `C:\Users\tim\Projects`) + * to `directoryExists(... & "/vendor/wheels")`, producing the mixed-slash + * string `C:\Users\tim\Projects/vendor/wheels`. Lucee's Resource API + * parsed `c:` as a URI scheme and bailed because no `c` provider is + * registered. Normalising to pure forward slashes keeps the path + * unambiguous on Windows while being a no-op on POSIX. + * + * `Module.$normalizePath()` delegates here so the bootstrap path and the + * unit tests exercise one implementation (#2835 originally carried a + * private copy inside Module.cfc). + */ + public string function normalizePath(required string path) { + if (!len(arguments.path)) return ""; + var rv = replace(arguments.path, "\", "/", "all"); + // Collapse doubled slashes from naïve concatenation, but preserve a + // leading `//` (UNC / network-share prefix on Windows). Guard the + // mid() against a count of 0 (when rv is exactly "//"), which can + // trip Lucee 7's string-range handling (cf. CLAUDE.md cross-engine #8). + var leading = left(rv, 2) == "//" ? "//" : ""; + var body = len(leading) ? (len(rv) > 2 ? mid(rv, 3, len(rv) - 2) : "") : rv; + body = reReplace(body, "/{2,}", "/", "all"); + return leading & body; + } + /** * Generate a migration timestamp (YYYYMMDDHHMMSS) */ diff --git a/cli/lucli/tests/specs/services/HelpersSpec.cfc b/cli/lucli/tests/specs/services/HelpersSpec.cfc index d7e374b3e0..7c92090398 100644 --- a/cli/lucli/tests/specs/services/HelpersSpec.cfc +++ b/cli/lucli/tests/specs/services/HelpersSpec.cfc @@ -97,6 +97,64 @@ component extends="wheels.wheelstest.system.BaseSpec" { }); + describe("normalizePath()", () => { + + // Regression: GH #2841 — `wheels new`/`wheels start` on Windows + // failed with "lucee.runtime.exp.NativeException: there is no + // Resource provider available with the name [c]". The CLI + // concatenated a Windows-form path (backslashes from + // java.io.File.getCanonicalPath()) with "/vendor/wheels" and + // fed the mixed-slash result to Lucee's Resource API, which + // then parsed "c:" as a URI scheme. Forward-slash normalization + // makes the path unambiguous on Windows while being a no-op on + // POSIX. Module.$normalizePath() delegates to this method, so + // these cases cover the real bootstrap path — not a copy. + + it("converts Windows backslashes to forward slashes", () => { + expect(helpers.normalizePath("C:\Users\tim\Projects")) + .toBe("C:/Users/tim/Projects"); + }); + + it("leaves POSIX paths unchanged", () => { + expect(helpers.normalizePath("/home/runner/work/wheels")) + .toBe("/home/runner/work/wheels"); + }); + + it("returns an empty string for empty input", () => { + expect(helpers.normalizePath("")).toBe(""); + }); + + it("collapses doubled forward slashes from concatenation", () => { + expect(helpers.normalizePath("/a/b//c")).toBe("/a/b/c"); + }); + + it("preserves a Windows drive-letter prefix after normalization", () => { + var normalized = helpers.normalizePath("C:\Users\tim\Projects"); + expect(normalized & "/vendor/wheels") + .toBe("C:/Users/tim/Projects/vendor/wheels"); + // Sanity: no remaining backslash means downstream + // directoryExists() won't trip Lucee's scheme parser. + expect(find("\", normalized)).toBe(0); + }); + + it("preserves a UNC network-share prefix", () => { + expect(helpers.normalizePath("//server/share/path")) + .toBe("//server/share/path"); + }); + + it("collapses doubled slashes inside a UNC path without eating the prefix", () => { + expect(helpers.normalizePath("//server//share")) + .toBe("//server/share"); + }); + + it("handles a bare double-slash root without a mid() range error", () => { + // Degenerate UNC root: rv === "//" makes the internal mid() + // count 0. Guarded so it can't trip Lucee 7 (cross-engine #8). + expect(helpers.normalizePath("//")).toBe("//"); + }); + + }); + }); } diff --git a/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/installation.mdx b/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/installation.mdx index 17efd895a5..7497f24ec8 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/installation.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/command-line-tools/installation.mdx @@ -233,6 +233,18 @@ On Linux, the `.deb`/`.rpm` package installs `/usr/bin/wheels`, which should be 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. +### Windows: `there is no Resource provider available with the name [c]` + +On Windows, `wheels new`, `wheels start`, and most other subcommands crashed before any work could happen with: + +``` +lucee.runtime.exp.NativeException: there is no Resource provider available +with the name [c], available resource providers are [ftp, zip, tar, tgz, +http, https, ram, s3] +``` + +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. + ## Related commands