mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 20:45:11 +02:00
Merge reviewed append-assignment correction from PR2965
Source-PR: https://github.com/affaan-m/ECC/pull/2965
Source-Head: eca846248d
This commit is contained in:
@@ -244,6 +244,19 @@ function checkGitWords(words, budget, start = 0, environmentOverride = false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Keep literal outcomes and the empty result of an unresolved expansion. The
|
||||
// latter is a base for later visible += operands, not arbitrary evaluation.
|
||||
function assignmentValues(prior, operand, append, dynamic, budget) {
|
||||
const base = prior === undefined ? '' : prior;
|
||||
budget.spend((append ? base.length : 0) + operand.length + 1);
|
||||
const values = new Set([append ? base + operand : operand]);
|
||||
if (dynamic) {
|
||||
if (prior !== undefined) values.add(prior);
|
||||
values.add(append ? base : '');
|
||||
}
|
||||
return [...values];
|
||||
}
|
||||
|
||||
// Only explicit option grammars remove wrapper operands. Unknown launchers are
|
||||
// opaque/conservative, never guessed from a name found among data arguments.
|
||||
function executableWords(words, budget, inherited = new Map(), callerValues = inherited) {
|
||||
@@ -262,25 +275,30 @@ function executableWords(words, budget, inherited = new Map(), callerValues = in
|
||||
function assignment(token) {
|
||||
const { value, dynamic } = token;
|
||||
const equals = value.indexOf('=');
|
||||
const key = value.slice(0, equals);
|
||||
const append = !environmentAssignments && value[equals - 1] === '+';
|
||||
const key = value.slice(0, append ? equals - 1 : equals);
|
||||
if (/^GIT_CONFIG_(?:COUNT|PARAMETERS|(?:KEY|VALUE)_[0-9]+)$/.test(key)) {
|
||||
const assigned = value.slice(equals + 1);
|
||||
const operand = value.slice(equals + 1);
|
||||
const count = environments.length;
|
||||
budget.spend(count + 1);
|
||||
for (let n = 0; n < count; n++) {
|
||||
const environment = environments[n];
|
||||
// Expansion precedes env's reset. Caller values remain separate from
|
||||
// the child environment; keep both that possible value and the new
|
||||
// literal spelling without interpreting expansion syntax.
|
||||
if (dynamic && callerValues.has(key)) {
|
||||
// Shell prefix appends can see local values, even when not exported.
|
||||
// Repeated operands use the prior outcome in this same prefix.
|
||||
const prior = prefixAssignments.has(key) && environment.has(key)
|
||||
? environment.get(key) : callerValues.get(key);
|
||||
const values = assignmentValues(prior, operand, append, dynamic, budget);
|
||||
for (const alternative of values.slice(1)) {
|
||||
budget.spend(environment.size + 1);
|
||||
const prior = new Map(environment);
|
||||
prior.set(key, callerValues.get(key));
|
||||
environments.push(prior);
|
||||
const variant = new Map(environment);
|
||||
variant.set(key, alternative);
|
||||
environments.push(variant);
|
||||
}
|
||||
environment.set(key, assigned);
|
||||
environment.set(key, values[0]);
|
||||
}
|
||||
prefixAssignments.set(key, assigned);
|
||||
// Retain ordered operations so same-shell states apply each append once.
|
||||
if (!prefixAssignments.has(key)) prefixAssignments.set(key, []);
|
||||
prefixAssignments.get(key).push({ value: operand, append, dynamic });
|
||||
if (dynamic) dynamicAssignments.add(key);
|
||||
}
|
||||
}
|
||||
@@ -297,7 +315,7 @@ function executableWords(words, budget, inherited = new Map(), callerValues = in
|
||||
while (i < words.length) {
|
||||
const token = words[i];
|
||||
budget.spend(token.value.length + token.raw.length + 1);
|
||||
if (assignments && /^[A-Za-z_][A-Za-z0-9_]*=/.test(environmentAssignments ? token.value : token.raw)) { assignment(token); i++; continue; }
|
||||
if (assignments && /^[A-Za-z_][A-Za-z0-9_]*\+?=/.test(environmentAssignments ? token.value : token.raw)) { assignment(token); i++; continue; }
|
||||
if (!token.quoted && CONTROL_WORDS.has(token.value)) { i++; continue; }
|
||||
const name = basename(token.value);
|
||||
if (name === 'command') {
|
||||
@@ -479,21 +497,30 @@ function updateShellState(state, normalized, budget) {
|
||||
const states = [state];
|
||||
const result = (handled, changed, uncertain = false) => ({ handled, changed, uncertain, states });
|
||||
if (!local) return result(false, false);
|
||||
function assign(name, value, dynamic = false) {
|
||||
function assign(name, value, dynamic = false, append = false) {
|
||||
const count = states.length;
|
||||
budget.spend(count + 1);
|
||||
for (let n = 0; n < count; n++) {
|
||||
const current = states[n];
|
||||
if (current.readonly.has(name)) continue;
|
||||
// Preserve the known possible value AND the new literal spelling.
|
||||
// Both alternatives subsequently receive the declaration attributes.
|
||||
if (dynamic && current.variables.has(name)) states.push(copyShellState(current, budget));
|
||||
current.variables.set(name, value);
|
||||
const values = assignmentValues(current.variables.get(name), value, append, dynamic, budget);
|
||||
for (const alternative of values.slice(1)) {
|
||||
const variant = copyShellState(current, budget);
|
||||
variant.variables.set(name, alternative);
|
||||
states.push(variant);
|
||||
}
|
||||
current.variables.set(name, values[0]);
|
||||
}
|
||||
}
|
||||
function assignPrefixes() {
|
||||
budget.spend(prefixAssignments.size + 1);
|
||||
for (const [key, operations] of prefixAssignments) {
|
||||
budget.spend(operations.length + 1);
|
||||
for (const operation of operations) assign(key, operation.value, operation.dynamic, operation.append);
|
||||
}
|
||||
}
|
||||
if (assignmentOnly) {
|
||||
budget.spend(prefixAssignments.size + 1);
|
||||
for (const [name, value] of prefixAssignments) assign(name, value, dynamicAssignments.has(name));
|
||||
assignPrefixes();
|
||||
return result(true, prefixAssignments.size > 0, dynamicAssignments.size > 0);
|
||||
}
|
||||
// Exact builtin names only: /some/path/export is an external executable.
|
||||
@@ -521,17 +548,17 @@ function updateShellState(state, normalized, budget) {
|
||||
else uncertain = true;
|
||||
}
|
||||
if (passive && !uncertain) return result(true, false);
|
||||
let changed = false;
|
||||
budget.spend(prefixAssignments.size + 1);
|
||||
for (const [key, value] of prefixAssignments) { assign(key, value, dynamicAssignments.has(key)); changed = true; }
|
||||
let changed = prefixAssignments.size > 0;
|
||||
assignPrefixes();
|
||||
for (; i < words.length; i++) {
|
||||
const value = words[i].value;
|
||||
budget.spend(2 * value.length + 1);
|
||||
const equals = value.indexOf('=');
|
||||
const key = equals < 0 ? value : value.slice(0, equals);
|
||||
const append = equals > 0 && value[equals - 1] === '+';
|
||||
const key = equals < 0 ? value : value.slice(0, append ? equals - 1 : equals);
|
||||
if (!GIT_ENV_NAME.test(key)) continue;
|
||||
changed = true;
|
||||
if (name !== 'unset' && equals >= 0) assign(key, value.slice(equals + 1), words[i].dynamic);
|
||||
if (name !== 'unset' && equals >= 0) assign(key, value.slice(equals + 1), words[i].dynamic, append);
|
||||
budget.spend(states.length + 1);
|
||||
for (const current of states) {
|
||||
if (name === 'unset') {
|
||||
|
||||
@@ -1812,7 +1812,138 @@ const stickyEnvironmentCases = Object.freeze([
|
||||
["readonly print flag assignment prevents later reset", 2, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; readonly -p GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; unset GIT_CONFIG_PARAMETERS; git commit"],
|
||||
["readonly print flag safe Git control", 0, "readonly -p GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git status"]
|
||||
]);
|
||||
for (const [family, expected, command] of stickyEnvironmentCases) {
|
||||
// Append assignments are shell syntax before a command or in declarations.
|
||||
// env's NAME+=VALUE is a different, literal variable name, not shell append.
|
||||
const appendEnvironmentCases = [
|
||||
['prefix from unset', 'GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'"'],
|
||||
['export from unset', 'export GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'";'],
|
||||
['declare from unset', 'declare -x GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'";'],
|
||||
['typeset from unset', 'typeset -x GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'";'],
|
||||
['standalone then export', 'GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'"; export GIT_CONFIG_PARAMETERS;'],
|
||||
['append to exported safe parameters', 'export GIT_CONFIG_PARAMETERS="\'color.ui=false\'"; GIT_CONFIG_PARAMETERS+=" \'core.hooksPath=/dev/null\'";'],
|
||||
['append split declaration', 'export GIT_CONFIG_PARAMETERS="\'core.hooks"; export GIT_CONFIG_PARAMETERS+="Path=/dev/null\'";'],
|
||||
['append split standalone', 'GIT_CONFIG_PARAMETERS="\'core.hooks"; GIT_CONFIG_PARAMETERS+="Path=/dev/null\'"; export GIT_CONFIG_PARAMETERS;'],
|
||||
['append split prefix from local', 'GIT_CONFIG_PARAMETERS="\'core.hooks"; GIT_CONFIG_PARAMETERS+="Path=/dev/null\'"'],
|
||||
['multiple prefix operands', 'GIT_CONFIG_PARAMETERS="\'core.hooks" GIT_CONFIG_PARAMETERS+="Path=/dev/null\'"'],
|
||||
['count triplet prefix', 'GIT_CONFIG_COUNT+=1 GIT_CONFIG_KEY_0+=core.hooksPath GIT_CONFIG_VALUE_0+=/dev/null'],
|
||||
['count triplet declaration', 'export GIT_CONFIG_COUNT+=1 GIT_CONFIG_KEY_0+=core.hooksPath GIT_CONFIG_VALUE_0+=/dev/null;'],
|
||||
['split key declaration', 'export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core. GIT_CONFIG_VALUE_0=/dev/null; declare -x GIT_CONFIG_KEY_0+=hooksPath;'],
|
||||
['split key prefix', 'GIT_CONFIG_KEY_0=core.; GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0+=hooksPath GIT_CONFIG_VALUE_0=/dev/null'],
|
||||
['conditional append possible', 'false && export GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'";'],
|
||||
['append cannot erase prior unknown-value alternative', 'export GIT_CONFIG_PARAMETERS="\'core.hooksPath=/dev/null\'"; GIT_CONFIG_PARAMETERS+="$UNKNOWN";'],
|
||||
['dynamic prefix cannot erase prior alternative', 'export GIT_CONFIG_PARAMETERS="\'core.hooksPath=/dev/null\'"; GIT_CONFIG_PARAMETERS+="$UNKNOWN"'],
|
||||
['dynamic declaration retains new literal operand', 'export GIT_CONFIG_PARAMETERS=""; export GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'$UNKNOWN";'],
|
||||
['dynamic prefix retains new literal operand', 'GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'$UNKNOWN"'],
|
||||
];
|
||||
const appendCases = [];
|
||||
for (const [family, setup] of appendEnvironmentCases) {
|
||||
appendCases.push([`append ${family}`, 2, `${setup} git commit`]);
|
||||
appendCases.push([`append ${family} safe Git control`, 0, `${setup} git status`]);
|
||||
}
|
||||
appendCases.push(
|
||||
['unexported append stays local', 0, 'GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'"; git commit'],
|
||||
['export attribute removal remains effective', 0, 'export GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'"; export -n GIT_CONFIG_PARAMETERS; git commit'],
|
||||
['explicit unset removes appended state', 0, 'export GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'"; unset GIT_CONFIG_PARAMETERS; git commit'],
|
||||
['child environment reset removes appended state', 0, 'export GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'"; env -i git commit'],
|
||||
['env append-like name is literal data', 0, 'env GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'" git commit'],
|
||||
['env append-like name after reset is literal data', 0, 'export GIT_CONFIG_PARAMETERS="\'core.hooksPath=/dev/null\'"; env -i GIT_CONFIG_PARAMETERS+= git commit'],
|
||||
['env literal name does not reset real exported key', 2, 'export GIT_CONFIG_PARAMETERS="\'core.hooksPath=/dev/null\'"; env GIT_CONFIG_PARAMETERS+= git commit'],
|
||||
['quoted shell assignment name stays data', 0, '"GIT_CONFIG_PARAMETERS+=\'core.hooksPath=/dev/null\'" git commit'],
|
||||
['readonly safe value cannot acquire appended override', 0, 'declare -rx GIT_CONFIG_PARAMETERS=""; GIT_CONFIG_PARAMETERS+="\'core.hooksPath=/dev/null\'"; git commit'],
|
||||
['readonly unsafe value cannot lose override through append', 2, 'export GIT_CONFIG_PARAMETERS="\'core.hooksPath=/dev/null\'"; readonly GIT_CONFIG_PARAMETERS; GIT_CONFIG_PARAMETERS+="x"; git commit'],
|
||||
['ordinary append parameters do not disable hooks', 0, 'export GIT_CONFIG_PARAMETERS="\'color.ui="; GIT_CONFIG_PARAMETERS+="false\'"; git commit'],
|
||||
['empty appended count is not an override', 0, 'export GIT_CONFIG_COUNT=0; GIT_CONFIG_COUNT+=""; git commit'],
|
||||
);
|
||||
|
||||
appendCases.push(...[
|
||||
[
|
||||
"unknown prior export commit",
|
||||
2,
|
||||
"export GIT_CONFIG_PARAMETERS=\"$UNKNOWN\"; export GIT_CONFIG_PARAMETERS+=\"'core.hooksPath=/dev/null'\"; git commit"
|
||||
],
|
||||
[
|
||||
"unknown prior export status",
|
||||
0,
|
||||
"export GIT_CONFIG_PARAMETERS=\"$UNKNOWN\"; export GIT_CONFIG_PARAMETERS+=\"'core.hooksPath=/dev/null'\"; git status"
|
||||
],
|
||||
[
|
||||
"unknown prior standalone commit",
|
||||
2,
|
||||
"GIT_CONFIG_PARAMETERS=\"$UNKNOWN\"; GIT_CONFIG_PARAMETERS+=\"'core.hooksPath=/dev/null'\"; export GIT_CONFIG_PARAMETERS; git commit"
|
||||
],
|
||||
[
|
||||
"unknown prior standalone status",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\"$UNKNOWN\"; GIT_CONFIG_PARAMETERS+=\"'core.hooksPath=/dev/null'\"; export GIT_CONFIG_PARAMETERS; git status"
|
||||
],
|
||||
[
|
||||
"unknown prior prefix commit",
|
||||
2,
|
||||
"GIT_CONFIG_PARAMETERS=\"$UNKNOWN\"; GIT_CONFIG_PARAMETERS+=\"'core.hooksPath=/dev/null'\" git commit"
|
||||
],
|
||||
[
|
||||
"unknown prior prefix status",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\"$UNKNOWN\"; GIT_CONFIG_PARAMETERS+=\"'core.hooksPath=/dev/null'\" git status"
|
||||
],
|
||||
[
|
||||
"unknown repeated prefix commit",
|
||||
2,
|
||||
"GIT_CONFIG_PARAMETERS=\"$UNKNOWN\" GIT_CONFIG_PARAMETERS+=\"'core.hooksPath=/dev/null'\" git commit"
|
||||
],
|
||||
[
|
||||
"unknown repeated prefix status",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\"$UNKNOWN\" GIT_CONFIG_PARAMETERS+=\"'core.hooksPath=/dev/null'\" git status"
|
||||
],
|
||||
[
|
||||
"three ordered prefix operands commit",
|
||||
2,
|
||||
"GIT_CONFIG_PARAMETERS=\"'core.\" GIT_CONFIG_PARAMETERS+=\"hooks\" GIT_CONFIG_PARAMETERS+=\"Path=/dev/null'\" git commit"
|
||||
],
|
||||
[
|
||||
"three ordered prefix operands status",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\"'core.\" GIT_CONFIG_PARAMETERS+=\"hooks\" GIT_CONFIG_PARAMETERS+=\"Path=/dev/null'\" git status"
|
||||
],
|
||||
[
|
||||
"unknown count followed by literal append commit",
|
||||
2,
|
||||
"export GIT_CONFIG_COUNT=\"$UNKNOWN\" GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null; GIT_CONFIG_COUNT+=1; git commit"
|
||||
],
|
||||
[
|
||||
"unknown count followed by literal append status",
|
||||
0,
|
||||
"export GIT_CONFIG_COUNT=\"$UNKNOWN\" GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null; GIT_CONFIG_COUNT+=1; git status"
|
||||
],
|
||||
[
|
||||
"unknown prior child context commit",
|
||||
2,
|
||||
"export GIT_CONFIG_PARAMETERS=\"$UNKNOWN\"; sh -c \"GIT_CONFIG_PARAMETERS+=\\\"'core.hooksPath=/dev/null'\\\"; git commit\""
|
||||
],
|
||||
[
|
||||
"unknown prior child context status",
|
||||
0,
|
||||
"export GIT_CONFIG_PARAMETERS=\"$UNKNOWN\"; sh -c \"GIT_CONFIG_PARAMETERS+=\\\"'core.hooksPath=/dev/null'\\\"; git status\""
|
||||
],
|
||||
[
|
||||
"literal prior is not unresolved '$UNKNOWN'",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS='$UNKNOWN'; export GIT_CONFIG_PARAMETERS+=\"'core.hooksPath=/dev/null'\"; git commit"
|
||||
],
|
||||
[
|
||||
"literal prior is not unresolved \\$UNKNOWN",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\\$UNKNOWN; export GIT_CONFIG_PARAMETERS+=\"'core.hooksPath=/dev/null'\"; git commit"
|
||||
],
|
||||
[
|
||||
"literal prior is not unresolved \"\\$UNKNOWN\"",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\"\\$UNKNOWN\"; export GIT_CONFIG_PARAMETERS+=\"'core.hooksPath=/dev/null'\"; git commit"
|
||||
]
|
||||
]);
|
||||
|
||||
for (const [family, expected, command] of [...stickyEnvironmentCases, ...appendCases]) {
|
||||
if (test(`${family} ${expected}: ${JSON.stringify(command)}`, () => {
|
||||
const result = runHook(command);
|
||||
assert.strictEqual(result.code, expected, result.stderr);
|
||||
|
||||
Reference in New Issue
Block a user