Files
CherryHQ-cherry-studio/src/renderer/pages/code/cliConfig/dotenv.ts
eeee0717 e97806af16 fix(code-cli): fix dotenv escaping and redaction gaps found in re-review of 4fc8948
Re-review of the previous CLI-config fix batch (4fc8948) surfaced three
regressions introduced by that batch itself, plus two sinks the secret
redaction still missed:

- dotenv.ts: quoting a value that needed escaping (contains #, quotes, or
  leading/trailing whitespace) injected \\ and \" escapes that the real
  `dotenv` package (used by gemini-cli to read the file back) never
  unescapes — it only re-expands \n/\r inside double-quoted values. This
  silently corrupted Windows-style paths (doubled backslashes) and any
  value containing a literal double quote. Fixed by preferring a
  single-quoted value (read back 100% literally) whenever it has no
  embedded single quote, and only escaping \n/\r in the double-quote
  fallback.
- redact.ts: the value-matching regex stopped at the first embedded
  apostrophe in a double-quoted value, or at an already-broken/empty
  quoted pair, leaving the real secret exposed as an unredacted trailing
  fragment in both cases. Fixed by matching the rest of the source line
  (smol-toml embeds raw source lines verbatim in its own error messages)
  instead of a single quoted token.
- Centralized secret redaction inside parseTomlOrThrow/parseJsonOrThrow
  themselves (instead of only at the two call sites that remembered to
  wrap it), closing two previously-unredacted sinks:
  validateCliConfigDraftForWrite (manual config-text editing) and
  draftUpdater.ts's direct parse calls.
- Added missing assertCliConfigCredentials coverage for the
  missing-base-URL branches of OpenCode/Qwen/Kimi (only the missing-API-key
  branch had a test before).

Every fix has a regression test verified to fail without it; the dotenv
and redact tests exercise the real `dotenv`/`smol-toml` packages directly
rather than hand-crafted strings, to catch this class of drift going
forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: eeee0717 <chentao020717Work@outlook.com>
2026-07-05 17:54:14 +08:00

43 lines
1.9 KiB
TypeScript

/** Parse a dotenv file into an ordered key-value map, preserving entry order. */
export function parseDotenv(content: string): Map<string, string> {
const out = new Map<string, string>()
for (const raw of content.split(/\r?\n/)) {
const line = raw.trim()
if (!line || line.startsWith('#')) continue
const eq = line.indexOf('=')
if (eq === -1) continue
const key = line.slice(0, eq).trim()
let value = line.slice(eq + 1).trim()
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1).replace(/\\(["\\])/g, '$1')
} else if (value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1)
}
out.set(key, value)
}
return out
}
// Bare (unquoted) dotenv values are truncated by standard dotenv parsers at the
// first `#`, and lose leading/trailing whitespace — quote whenever that would
// otherwise corrupt the value on read-back by the CLI tool's own loader.
function needsDotenvQuoting(value: string): boolean {
return value === '' || /^\s|\s$|["'#\\]/.test(value)
}
// The real `dotenv` package (used by the CLI tools that read these files back) only re-expands
// literal `\n`/`\r` sequences inside a double-quoted value on read — it does NOT unescape `\\` or
// `\"`, so injecting those escapes here would corrupt the value on read-back instead of preserving
// it. A single-quoted value, by contrast, is taken back 100% literally with no escape processing at
// all, so prefer it whenever the value has no embedded single quote to conflict with the wrapper.
function quoteDotenvValue(value: string): string {
if (!value.includes("'")) return `'${value}'`
return `"${value.replace(/\r/g, '\\r').replace(/\n/g, '\\n')}"`
}
export function renderDotenvFile(envMap: Map<string, string>): string {
return `${[...envMap.entries()]
.map(([key, value]) => `${key}=${needsDotenvQuoting(value) ? quoteDotenvValue(value) : value}`)
.join('\n')}\n`
}