diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index fe5f05ab8..21dd5a294 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -3185,12 +3185,16 @@ std::ostream & operator << (std::ostream & str, const ExternalValueBase & v) { return v.print(str); } -void forceNoNullByte(std::string_view s) +void forceNoNullByte(std::string_view s, std::function pos) { if (s.find('\0') != s.npos) { using namespace std::string_view_literals; auto str = replaceStrings(std::string(s), "\0"sv, "␀"sv); - throw Error("input string '%s' cannot be represented as Nix string because it contains null bytes", str); + Error error("input string '%s' cannot be represented as Nix string because it contains null bytes", str); + if (pos) { + error.atPos(pos()); + } + throw error; } } diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l index a7e44cb72..067f86e01 100644 --- a/src/libexpr/lexer.l +++ b/src/libexpr/lexer.l @@ -41,16 +41,18 @@ namespace nix { // we make use of the fact that the parser receives a private copy of the input // string and can munge around in it. -static StringToken unescapeStr(SymbolTable & symbols, char * s, size_t length) +// getting the position is expensive and thus it is implemented lazily. +static StringToken unescapeStr(char * const s, size_t length, std::function && pos) { - char * result = s; + bool noNullByte = true; char * t = s; - char c; // the input string is terminated with *two* NULs, so we can safely take // *one* character after the one being checked against. - while ((c = *s++)) { + for (size_t i = 0; i < length; t++) { + char c = s[i++]; + noNullByte &= c != '\0'; if (c == '\\') { - c = *s++; + c = s[i++]; if (c == 'n') *t = '\n'; else if (c == 'r') *t = '\r'; else if (c == 't') *t = '\t'; @@ -59,12 +61,14 @@ static StringToken unescapeStr(SymbolTable & symbols, char * s, size_t length) else if (c == '\r') { /* Normalise CR and CR/LF into LF. */ *t = '\n'; - if (*s == '\n') s++; /* cr/lf */ + if (s[i] == '\n') i++; /* cr/lf */ } else *t = c; - t++; } - return {result, size_t(t - result)}; + if (!noNullByte) { + forceNoNullByte({s, size_t(t - s)}, std::move(pos)); + } + return {s, size_t(t - s)}; } static void requireExperimentalFeature(const ExperimentalFeature & feature, const Pos & pos) @@ -175,7 +179,7 @@ or { return OR_KW; } /* It is impossible to match strings ending with '$' with one regex because trailing contexts are only valid at the end of a rule. (A sane but undocumented limitation.) */ - yylval->str = unescapeStr(state->symbols, yytext, yyleng); + yylval->str = unescapeStr(yytext, yyleng, [&]() { return state->positions[CUR_POS]; }); return STR; } \$\{ { PUSH_STATE(DEFAULT); return DOLLAR_CURLY; } @@ -191,6 +195,7 @@ or { return OR_KW; } \'\'(\ *\n)? { PUSH_STATE(IND_STRING); return IND_STRING_OPEN; } ([^\$\']|\$[^\{\']|\'[^\'\$])+ { yylval->str = {yytext, (size_t) yyleng, true}; + forceNoNullByte(yylval->str, [&]() { return state->positions[CUR_POS]; }); return IND_STR; } \'\'\$ | @@ -203,7 +208,7 @@ or { return OR_KW; } return IND_STR; } \'\'\\{ANY} { - yylval->str = unescapeStr(state->symbols, yytext + 2, yyleng - 2); + yylval->str = unescapeStr(yytext + 2, yyleng - 2, [&]() { return state->positions[CUR_POS]; }); return IND_STR; } \$\{ { PUSH_STATE(DEFAULT); return DOLLAR_CURLY; } diff --git a/src/libexpr/value.hh b/src/libexpr/value.hh index 88fcae986..8925693e3 100644 --- a/src/libexpr/value.hh +++ b/src/libexpr/value.hh @@ -510,6 +510,6 @@ typedef std::shared_ptr RootValue; RootValue allocRootValue(Value * v); -void forceNoNullByte(std::string_view s); +void forceNoNullByte(std::string_view s, std::function = nullptr); } diff --git a/tests/functional/lang/eval-fail-string-nul-1.err.exp b/tests/functional/lang/eval-fail-string-nul-1.err.exp new file mode 100644 index 000000000..2dfbea063 Binary files /dev/null and b/tests/functional/lang/eval-fail-string-nul-1.err.exp differ diff --git a/tests/functional/lang/eval-fail-string-nul-1.nix b/tests/functional/lang/eval-fail-string-nul-1.nix new file mode 100644 index 000000000..368940917 Binary files /dev/null and b/tests/functional/lang/eval-fail-string-nul-1.nix differ diff --git a/tests/functional/lang/eval-fail-string-nul-2.err.exp b/tests/functional/lang/eval-fail-string-nul-2.err.exp new file mode 100644 index 000000000..b1cae5325 Binary files /dev/null and b/tests/functional/lang/eval-fail-string-nul-2.err.exp differ diff --git a/tests/functional/lang/eval-fail-string-nul-2.nix b/tests/functional/lang/eval-fail-string-nul-2.nix new file mode 100644 index 000000000..fd6b3258a Binary files /dev/null and b/tests/functional/lang/eval-fail-string-nul-2.nix differ