From c50a3a426fc5686e480bf75bc3417d3459e3f2ea Mon Sep 17 00:00:00 2001 From: eldritch horrors Date: Sun, 25 Jan 2026 17:29:32 +0100 Subject: [PATCH] libutil: add simple NUL escaping/unescaping functions Change-Id: Ia4cc7b8f1058439f312066422eebfaff1c2c0c6c --- lix/libutil/strings.cc | 23 +++++++++++++++++++++++ lix/libutil/strings.hh | 10 +++++++++- tests/unit/libutil/tests.cc | 17 +++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lix/libutil/strings.cc b/lix/libutil/strings.cc index 8ea3e5132..9eb2842b0 100644 --- a/lix/libutil/strings.cc +++ b/lix/libutil/strings.cc @@ -366,4 +366,27 @@ std::string showBytes(uint64_t bytes) return fmt("%.2f MiB", bytes / (1024.0 * 1024.0)); } +std::string escapeNul(const std::string & in) +{ + using std::operator""sv; + return replaceStrings(replaceStrings(std::move(in), R"(\)", R"(\\)"), "\0"sv, R"(\0)"); +} + +std::string unescapeNul(const std::string & in) +{ + std::string result; + result.reserve(in.size()); + bool inSequence = false; + for (auto c : in) { + if (inSequence) { + result += c == '0' ? '\0' : c; + inSequence = false; + } else if (c == '\\') { + inSequence = true; + } else { + result += c; + } + } + return result; +} } diff --git a/lix/libutil/strings.hh b/lix/libutil/strings.hh index bc3d9285b..03f32ce14 100644 --- a/lix/libutil/strings.hh +++ b/lix/libutil/strings.hh @@ -141,7 +141,15 @@ inline std::string rewriteStrings(std::string s, const StringMap & rewrites) return Rewriter(rewrites)(s); } - +/** + * Escape NUL bytes and `\` in a string by replacing them with `\0` and `\\` respectively. + */ +std::string escapeNul(const std::string & in); +/** + * Undo replacements done by `escapeNul`. Other escape sequences (eg `\n`) are replaced by + * the second character in the sequence (eg `n`), `\` at the end of the string is dropped. + */ +std::string unescapeNul(const std::string & in); /** * Parse a string into an integer. diff --git a/tests/unit/libutil/tests.cc b/tests/unit/libutil/tests.cc index ff96b565b..24ddbb56f 100644 --- a/tests/unit/libutil/tests.cc +++ b/tests/unit/libutil/tests.cc @@ -817,4 +817,21 @@ namespace nix { ASSERT_TRUE(e.is()); ASSERT_NE(e.as(), nullptr); } + + TEST(NulEscaping, function) + { + for (int c1 = CHAR_MIN; c1 < CHAR_MAX; c1++) { + for (int c2 = CHAR_MIN; c2 < CHAR_MAX; c2++) { + auto sequence = std::format("{}{}", char(c1), char(c2)); + for (auto str : {sequence, "a" + sequence + "b"}) { + ASSERT_EQ(unescapeNul(escapeNul(str)), str); + } + } + } + + // unknown escapes are character identity for simplicity of use + ASSERT_EQ(unescapeNul("\\a"), "a"); + // escape of end-of-string is dropped silently + ASSERT_EQ(unescapeNul("a\\"), "a"); + } }