From 53dc27f752a9ef6b2d7e5c48124cdd9c8223c3f4 Mon Sep 17 00:00:00 2001 From: Qyriad Date: Thu, 11 Sep 2025 18:32:24 +0200 Subject: [PATCH] nix3-develop: correctly escape ASCII control characters for JSON Fixes #991. (cherry picked from commit 138c7161be82d3a240ba3a59cbdd93eef069b448) Change-Id: Iafc7d9603fbc3615393d32d9630f0e8fe548950b --- doc/manual/rl-next/fix-develop-esc.md | 9 +++++++++ lix/nix/develop.cc | 23 +++++++++++++---------- lix/nix/get-env.sh | 26 ++++++++++++++++++++++++++ tests/functional/nix-shell/basic.sh | 1 + tests/functional/nix-shell/shell.nix | 1 + 5 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 doc/manual/rl-next/fix-develop-esc.md diff --git a/doc/manual/rl-next/fix-develop-esc.md b/doc/manual/rl-next/fix-develop-esc.md new file mode 100644 index 000000000..85c2ff99c --- /dev/null +++ b/doc/manual/rl-next/fix-develop-esc.md @@ -0,0 +1,9 @@ +--- +synopsis: Fix develop shells for derivations with escape codes +issues: [fj#991] +cls: [4154, 4155] +category: Fixes +credits: [Qyriad] +--- + +ASCII control characters (including `\e`, used for ANSI escape codes) in derivation variables are now correctly escaped for `nix develop` and `nix print-dev-env`, instead of erroring. diff --git a/lix/nix/develop.cc b/lix/nix/develop.cc index 4b3dee50b..9a22cef4e 100644 --- a/lix/nix/develop.cc +++ b/lix/nix/develop.cc @@ -142,20 +142,20 @@ struct BuildEnvironment for (auto & [name, value] : vars) { if (!ignoreVars.count(name)) { if (auto str = std::get_if(&value)) { - out << fmt("%s=%s\n", name, shellEscape(str->value)); + out << fmt("%s=%s\n", name, bashEscape(str->value)); if (str->exported) out << fmt("export %s\n", name); } else if (auto arr = std::get_if(&value)) { out << "declare -a " << name << "=("; for (auto & s : *arr) - out << shellEscape(s) << " "; + out << bashEscape(s) << " "; out << ")\n"; } else if (auto arr = std::get_if(&value)) { out << "declare -A " << name << "=("; for (auto & [n, v] : *arr) - out << "[" << shellEscape(n) << "]=" << shellEscape(v) << " "; + out << "[" << bashEscape(n) << "]=" << bashEscape(v) << " "; out << ")\n"; } } @@ -599,21 +599,24 @@ struct CmdDevelop : Common, MixEnvironment else if (!command.empty()) { std::vector args; for (auto s : command) - args.push_back(shellEscape(s)); + args.push_back(bashEscape(s)); script += fmt("exec %s\n", concatStringsSep(" ", args)); } else { script = "[ -n \"$PS1\" ] && [ -e ~/.bashrc ] && source ~/.bashrc;\n" + script; if (developSettings.bashPrompt != "") - script += fmt("[ -n \"$PS1\" ] && PS1=%s;\n", - shellEscape(developSettings.bashPrompt.get())); + script += + fmt("[ -n \"$PS1\" ] && PS1=%s;\n", + bashEscape(developSettings.bashPrompt.get())); if (developSettings.bashPromptPrefix != "") - script += fmt("[ -n \"$PS1\" ] && PS1=%s\"$PS1\";\n", - shellEscape(developSettings.bashPromptPrefix.get())); + script += + fmt("[ -n \"$PS1\" ] && PS1=%s\"$PS1\";\n", + bashEscape(developSettings.bashPromptPrefix.get())); if (developSettings.bashPromptSuffix != "") - script += fmt("[ -n \"$PS1\" ] && PS1+=%s;\n", - shellEscape(developSettings.bashPromptSuffix.get())); + script += + fmt("[ -n \"$PS1\" ] && PS1+=%s;\n", + bashEscape(developSettings.bashPromptSuffix.get())); } writeFull(rcFileFd.get(), script); diff --git a/lix/nix/get-env.sh b/lix/nix/get-env.sh index 832cc2f11..368351ad7 100644 --- a/lix/nix/get-env.sh +++ b/lix/nix/get-env.sh @@ -12,6 +12,31 @@ fi __vars="$(declare -p)" __functions="$(declare -F)" +# Literal control characters (ASCII 0-31) aren't valid JSON. +__escapeCtrl() { + local escaped="$1" + + # I don't know if NUL bytes are at ALL possible in here, + # but covering them is free. + local i=0 + # NOTE: safe input `i` to arithmetic expansion. + while [[ "$i" -le 32 ]]; do + # Convert the decimal ASCII value to its actual string. + local asHex; printf -v asHex "%02x" "$i" + local asStr; printf -v asStr "%b" "\x$asHex" + + # Format it to \uXXXX. + # All control characters fit within four hex digits. + local asUni; printf -v asUni '\\u%04x' "$i" + + escaped="${escaped//"$asStr"/"$asUni"}" + + i="$((i + 1))" + done + + printf "%s" "$escaped" +} + __dumpEnv() { printf '{\n' @@ -125,6 +150,7 @@ __escapeString() { __s="${__s//$'\n'/\\n}" __s="${__s//$'\r'/\\r}" __s="${__s//$'\t'/\\t}" + __s="$(__escapeCtrl "$__s")" printf '"%s"' "$__s" } diff --git a/tests/functional/nix-shell/basic.sh b/tests/functional/nix-shell/basic.sh index 5c6e81ec2..032639f9a 100644 --- a/tests/functional/nix-shell/basic.sh +++ b/tests/functional/nix-shell/basic.sh @@ -132,6 +132,7 @@ set -u [[ ${arr2[1]} = $'\n' ]] [[ ${arr2[2]} = $'x\ny' ]] [[ $(fun) = blabla ]] + [[ "$ASCII_ESC" = "$(printf "\e")" ]] [[ $PATH = $(jq -r .variables.PATH.value $TEST_ROOT/dev-env.json):$path ]] ) diff --git a/tests/functional/nix-shell/shell.nix b/tests/functional/nix-shell/shell.nix index 92d94fbc2..1beeb3357 100644 --- a/tests/functional/nix-shell/shell.nix +++ b/tests/functional/nix-shell/shell.nix @@ -50,6 +50,7 @@ let pkgs = rec { VAR_FROM_NIX = "bar"; ASCII_PERCENT = "%"; ASCII_AT = "@"; + ASCII_ESC = ""; TEST_inNixShell = if inNixShell then "true" else "false"; inherit stdenv; outputs = ["dev" "out"];