Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/env-integer-only-coercion.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `.env` values now coerce to numbers only when they are plain integers, so `007`, `1e3` and `+5` stay strings instead of being truncated to `7` and `1`, and a value that is a lone quote no longer errors. Apps generated by `wheels new` keep the old parser in their own `public/Application.cfc` until they copy the change.
8 changes: 5 additions & 3 deletions cli/lucli/templates/app/public/Application.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1029,8 +1029,9 @@ component output="false" {
local.key = trim(listFirst(local.trimmedLine, "="));
local.value = trim(listRest(local.trimmedLine, "="));

if ((left(local.value, 1) == '"' && right(local.value, 1) == '"') ||
(left(local.value, 1) == "'" && right(local.value, 1) == "'")) {
// Strip only when there is an inner value, so a lone quote survives
if (len(local.value) >= 2 && ((left(local.value, 1) == '"' && right(local.value, 1) == '"') ||
(left(local.value, 1) == "'" && right(local.value, 1) == "'"))) {
local.value = mid(local.value, 2, len(local.value) - 2);
}

Expand All @@ -1040,7 +1041,8 @@ component output="false" {
// true and made numeric settings silently take their defaults instead.
if (Compare(lCase(local.value), "true") == 0 || Compare(lCase(local.value), "false") == 0) {
local.value = (Compare(lCase(local.value), "true") == 0);
} else if (isNumeric(local.value) && !find(".", local.value)) {
} else if (REFind("^-?(0|[1-9][0-9]*)$", local.value) > 0) {
// Only convert plain integers; 007, 1e3 and +5 stay strings
local.value = val(local.value);
}

Expand Down
8 changes: 5 additions & 3 deletions examples/starter-app/public/Application.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1023,8 +1023,9 @@ component output="false" {
local.key = trim(listFirst(local.trimmedLine, "="));
local.value = trim(listRest(local.trimmedLine, "="));

if ((left(local.value, 1) == '"' && right(local.value, 1) == '"') ||
(left(local.value, 1) == "'" && right(local.value, 1) == "'")) {
// Strip only when there is an inner value, so a lone quote survives
if (len(local.value) >= 2 && ((left(local.value, 1) == '"' && right(local.value, 1) == '"') ||
(left(local.value, 1) == "'" && right(local.value, 1) == "'"))) {
local.value = mid(local.value, 2, len(local.value) - 2);
}

Expand All @@ -1034,7 +1035,8 @@ component output="false" {
// true and made numeric settings silently take their defaults instead.
if (Compare(lCase(local.value), "true") == 0 || Compare(lCase(local.value), "false") == 0) {
local.value = (Compare(lCase(local.value), "true") == 0);
} else if (isNumeric(local.value) && !find(".", local.value)) {
} else if (REFind("^-?(0|[1-9][0-9]*)$", local.value) > 0) {
// Only convert plain integers; 007, 1e3 and +5 stay strings
local.value = val(local.value);
}

Expand Down
10 changes: 5 additions & 5 deletions examples/tweet/public/Application.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -948,9 +948,9 @@ component output="false" {
local.key = trim(listFirst(local.trimmedLine, "="));
local.value = trim(listRest(local.trimmedLine, "="));

// Remove surrounding quotes if present
if ((left(local.value, 1) == '"' && right(local.value, 1) == '"') ||
(left(local.value, 1) == "'" && right(local.value, 1) == "'")) {
// Strip only when there is an inner value, so a lone quote survives
if (len(local.value) >= 2 && ((left(local.value, 1) == '"' && right(local.value, 1) == '"') ||
(left(local.value, 1) == "'" && right(local.value, 1) == "'"))) {
local.value = mid(local.value, 2, len(local.value) - 2);
}

Expand All @@ -961,8 +961,8 @@ component output="false" {
// true and made numeric settings silently take their defaults instead.
if (Compare(lCase(local.value), "true") == 0 || Compare(lCase(local.value), "false") == 0) {
local.value = (Compare(lCase(local.value), "true") == 0);
} else if (isNumeric(local.value) && !find(".", local.value)) {
// Only convert integers, leave decimals as strings
} else if (REFind("^-?(0|[1-9][0-9]*)$", local.value) > 0) {
// Only convert plain integers; 007, 1e3 and +5 stay strings
local.value = val(local.value);
}

Expand Down
10 changes: 5 additions & 5 deletions public/Application.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1003,9 +1003,9 @@ component output="false" {
local.key = trim(listFirst(local.trimmedLine, "="));
local.value = trim(listRest(local.trimmedLine, "="));

// Remove surrounding quotes if present
if ((left(local.value, 1) == '"' && right(local.value, 1) == '"') ||
(left(local.value, 1) == "'" && right(local.value, 1) == "'")) {
// Strip only when there is an inner value, so a lone quote survives
if (len(local.value) >= 2 && ((left(local.value, 1) == '"' && right(local.value, 1) == '"') ||
(left(local.value, 1) == "'" && right(local.value, 1) == "'"))) {
local.value = mid(local.value, 2, len(local.value) - 2);
}

Expand All @@ -1016,8 +1016,8 @@ component output="false" {
// true and made numeric settings silently take their defaults instead.
if (Compare(lCase(local.value), "true") == 0 || Compare(lCase(local.value), "false") == 0) {
local.value = (Compare(lCase(local.value), "true") == 0);
} else if (isNumeric(local.value) && !find(".", local.value)) {
// Only convert integers, leave decimals as strings
} else if (REFind("^-?(0|[1-9][0-9]*)$", local.value) > 0) {
// Only convert plain integers; 007, 1e3 and +5 stay strings
local.value = val(local.value);
}

Expand Down
9 changes: 9 additions & 0 deletions vendor/wheels/tests/_assets/env/edge-values.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Fixture for global.EnvFileParsingSpec: edge values beyond value-types.env.
# Parsed by the host app's own Application.cfc loadEnvFile(); no app .env key needed.
WHEELS_SPEC_LEADING_ZERO=007
WHEELS_SPEC_SCI=1e3
WHEELS_SPEC_PLUS=+5
WHEELS_SPEC_NEGATIVE=-5
WHEELS_SPEC_LONE_SQUOTE='
WHEELS_SPEC_LONE_DQUOTE="
WHEELS_SPEC_AFTER_LONE=still-parsed
66 changes: 66 additions & 0 deletions vendor/wheels/tests/specs/global/EnvFileParsingSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,40 @@ component extends="wheels.WheelsTest" {

});

describe(".env edge values: lone quotes and integer-only coercion", () => {

it("keeps plain-integer coercion and survives lone quotes", () => {
// Same host-Application pattern as the blocks above.
var hostApp = CreateObject("component", "Application");
prepareMock(hostApp);
makePublic(hostApp, "loadEnvFile");
var parsedEnv = {};
var fixture = ExpandPath("/wheels/tests/_assets/env/edge-values.env");
expect(FileExists(fixture), "fixture missing").toBeTrue();
hostApp.loadEnvFile(fixture, parsedEnv);

// Only plain integers coerce: a leading zero, scientific notation
// and a leading plus all stay strings.
expect(Compare(ToString(parsedEnv["WHEELS_SPEC_LEADING_ZERO"]), "007"), "007 must survive as the string 007").toBe(0);
expect(Compare(ToString(parsedEnv["WHEELS_SPEC_SCI"]), "1e3"), "1e3 must survive as the string 1e3").toBe(0);
expect(Compare(ToString(parsedEnv["WHEELS_SPEC_PLUS"]), "+5"), "+5 must survive as the string +5").toBe(0);

// A plain negative integer still becomes a number.
expect(Compare(ToString(parsedEnv["WHEELS_SPEC_NEGATIVE"]), "-5"), "-5 must survive as the string -5").toBe(0);
expect(parsedEnv["WHEELS_SPEC_NEGATIVE"]).toBe(-5);

// A lone quote is a 1-character string and must not be sliced away.
expect(Len(parsedEnv["WHEELS_SPEC_LONE_SQUOTE"])).toBe(1);
expect(Compare(ToString(parsedEnv["WHEELS_SPEC_LONE_SQUOTE"]), "'"), "a lone single quote must survive").toBe(0);
expect(Len(parsedEnv["WHEELS_SPEC_LONE_DQUOTE"])).toBe(1);
expect(Compare(ToString(parsedEnv["WHEELS_SPEC_LONE_DQUOTE"]), Chr(34)), "a lone double quote must survive").toBe(0);

// Parsing continues after the lone-quote lines.
expect(Compare(ToString(parsedEnv["WHEELS_SPEC_AFTER_LONE"]), "still-parsed"), "parsing must continue after a lone quote").toBe(0);
});

});

describe("No copy of the parser may reintroduce the coercing test", () => {

it("every .env/settings parser compares booleans as strings", () => {
Expand Down Expand Up @@ -190,6 +224,38 @@ component extends="wheels.WheelsTest" {
}
});

it("every Application.cfc parses numbers with the integer-only regex", () => {
$requireRepoPath("cli/lucli/templates/app/public/Application.cfc");
// Same path resolution as the guard above.
var specPath = Replace(GetCurrentTemplatePath(), "\", "/", "all");
var marker = "/vendor/wheels/tests/specs/global/";
var markerPos = Find(marker, specPath);
expect(markerPos, "this spec must live under #marker# for the parser paths to resolve").toBeGT(0);
var root = Left(specPath, markerPos) & "/";

// The four `public/Application.cfc` entries of the parsers array above.
var parsers = [
"public/Application.cfc",
"cli/lucli/templates/app/public/Application.cfc",
"examples/starter-app/public/Application.cfc",
"examples/tweet/public/Application.cfc"
];

for (var relativePath in parsers) {
var parserPath = root & relativePath;
expect(FileExists(parserPath), "parser '#relativePath#' must exist — move it in this spec too").toBeTrue();
var source = FileRead(parserPath, "utf-8");
expect(
source,
"'#relativePath#' must coerce only plain integers (^-?(0|[1-9][0-9]*)$)"
).toInclude("^-?(0|[1-9][0-9]*)$");
expect(
source,
"'#relativePath#' still uses the old isNumeric/find('.') check, which mangles 007 and 1e3"
).notToInclude('isNumeric(local.value) && !find(".", local.value)');
}
});

});

}
Expand Down
Loading