mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 18:59:33 +01:00
fix: refuse a value ending in a backslash on the first run (release review)
In a single-quoted dotenv value a backslash escapes the closing quote, so a password or data path ending in one passed validation and produced a docker/.env Compose cannot parse. The error named an unrelated line, printed the password, and a rerun never asked again. The env_quote comment claimed a single quote was the only character such a value cannot hold, and the test stub's comment said bash reads these values the way Compose does: both corrected, and a test now reads generated values back by Compose's rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
af6fcb624b
commit
d539a7a3a2
2 files changed
+46
-2
No files matched your search
+11
-2
@@ -209,8 +209,9 @@ generate_password() {
|
||||
|
||||
# Compose reads .env values with its own dotenv rules: `$` interpolates and an
|
||||
# unquoted ` #` starts a comment. A single-quoted value is taken literally, so
|
||||
# anything beyond plain path characters is written that way (the accept_*
|
||||
# checks below refuse the one character it cannot hold, a single quote).
|
||||
# anything beyond plain path characters is written that way. It cannot hold a
|
||||
# single quote, and a backslash just before the closing quote escapes that quote,
|
||||
# so the whole file fails to parse: the accept_* checks below refuse both.
|
||||
env_quote() {
|
||||
local re='^[A-Za-z0-9._/@:+-]*$'
|
||||
if [[ "$1" =~ $re ]]; then
|
||||
@@ -237,6 +238,10 @@ accept_appdata_path() {
|
||||
printf ' The path cannot contain a single quote or a line break.\n' >&2
|
||||
return 1
|
||||
;;
|
||||
*\\)
|
||||
printf ' The path cannot end in a backslash.\n' >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [[ "$p" != /* ]]; then
|
||||
printf ' Use an absolute path, one that starts with /.\n' >&2
|
||||
@@ -277,6 +282,10 @@ accept_password() {
|
||||
printf ' The password cannot contain a single quote or a line break.\n' >&2
|
||||
return 1
|
||||
;;
|
||||
*\\)
|
||||
printf ' The password cannot end in a backslash.\n' >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if ((${#1} < 8)); then
|
||||
printf ' Use at least 8 characters.\n' >&2
|
||||
|
||||
@@ -228,6 +228,21 @@ function envValue(text: string, key: string): string | undefined {
|
||||
return raw.startsWith("'") && raw.endsWith("'") ? raw.slice(1, -1) : raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* What Compose reads from a single-quoted dotenv value, or null when it refuses
|
||||
* the file with `unterminated quoted value`. A backslash there escapes the next
|
||||
* character, so `'longpass\\'` never closes, while bash (and so the stub) ends
|
||||
* the same value at that quote. Measured on Compose 2.33.0 and 5.5.0.
|
||||
*/
|
||||
function composeReads(quoted: string): string | null {
|
||||
let escaped = false;
|
||||
for (let i = 1; i < quoted.length; i++) {
|
||||
if (quoted[i] === "'" && !escaped) return i === quoted.length - 1 ? quoted.slice(1, i) : null;
|
||||
escaped = quoted[i] === '\\' && !escaped;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
describe('Start-Codeman.sh first run (no docker/.env yet)', () => {
|
||||
it('parses under bash -n', () => {
|
||||
execFileSync('bash', ['-n', join(ROOT, 'docker/Start-Codeman.sh')]);
|
||||
@@ -316,12 +331,32 @@ describe('Start-Codeman.sh first run (no docker/.env yet)', () => {
|
||||
['a single quote', "it's-a-password"],
|
||||
['the example placeholder', 'changeme'],
|
||||
['fewer than 8 characters', 'short'],
|
||||
['a trailing backslash, which would escape the closing quote', 'longpass\\'],
|
||||
])('refuses a preset password with %s', (_name, password) => {
|
||||
const r = runStart(['--setup-only'], { env: { CODEMAN_PASSWORD: password } });
|
||||
expect(r.status).toBe(1);
|
||||
expect(r.env).toBeNull();
|
||||
});
|
||||
|
||||
it('refuses a data folder ending in a backslash, even behind a trailing slash', () => {
|
||||
for (const path of ['/srv/data\\', '/srv/data\\/']) {
|
||||
const r = runStart(['--setup-only'], { env: { CODEMAN_APPDATA_PATH: path } });
|
||||
expect(r.status).toBe(1);
|
||||
expect(r.stderr).toMatch(/cannot end in a backslash/);
|
||||
expect(r.env).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(['mid\\dle-pass', 'pa$$ #word', 'back`tick"quote', 'tab\there'])(
|
||||
'writes %j so Compose reads it back exactly',
|
||||
(password) => {
|
||||
const r = runStart(['--setup-only'], { env: { CODEMAN_PASSWORD: password } });
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
const line = (r.env as string).split('\n').find((l) => l.startsWith('CODEMAN_PASSWORD=')) as string;
|
||||
expect(composeReads(line.slice('CODEMAN_PASSWORD='.length))).toBe(password);
|
||||
}
|
||||
);
|
||||
|
||||
it('never rewrites an existing docker/.env', () => {
|
||||
const existing = '# hand-written\nCODEMAN_PASSWORD=mine-and-only-mine\nCODEMAN_APPDATA_PATH=/x\n';
|
||||
const r = runStart(['--setup-only'], { existingEnv: existing });
|
||||
|
||||
Reference in new issue
Block a user