From 991831227e7ce9de0f06c4b6331a455b49e3e168 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Tue, 5 Aug 2025 14:12:00 -0400 Subject: [PATCH] Clean up Base* code Make it separate from Hash, since other things can be base-encoded too. This isn't really needed for Nix, but it makes the code easier to read e.g. for someone reimplementing this stuff in a different language. (Of course, Base16/Base64 should be gotten off-the-shelf, but now the hash code, which is more bespoke, is less cluttered with the parts that would be from some library.) Many reimplementations of "Nix32" and our hash type already exist, so this cleanup is coming years too late, but I say better late than never / it is always good to nudge the code in the direction of being a "living spec". Co-authored-by: Sergei Zimmerman --- src/libfetchers/git-utils.cc | 3 +- src/libstore/machines.cc | 3 +- src/libstore/ssh.cc | 3 +- src/libutil-tests/base-n.cc | 68 +++++++++++ src/libutil-tests/meson.build | 1 + src/libutil-tests/util.cc | 55 +-------- src/libutil/base-n.cc | 114 ++++++++++++++++++ src/libutil/base-nix-32.cc | 43 ++++++- src/libutil/hash.cc | 109 ++++++----------- .../nix/util/array-from-string-literal.hh | 27 +++++ src/libutil/include/nix/util/base-n.hh | 53 ++++++++ src/libutil/include/nix/util/base-nix-32.hh | 12 +- src/libutil/include/nix/util/hash.hh | 24 ---- src/libutil/include/nix/util/meson.build | 2 + src/libutil/include/nix/util/util.hh | 10 -- src/libutil/meson.build | 1 + src/libutil/signature/local-keys.cc | 9 +- src/libutil/util.cc | 64 ---------- 18 files changed, 357 insertions(+), 244 deletions(-) create mode 100644 src/libutil-tests/base-n.cc create mode 100644 src/libutil/base-n.cc create mode 100644 src/libutil/include/nix/util/array-from-string-literal.hh create mode 100644 src/libutil/include/nix/util/base-n.hh diff --git a/src/libfetchers/git-utils.cc b/src/libfetchers/git-utils.cc index a758848b2..993d7fb08 100644 --- a/src/libfetchers/git-utils.cc +++ b/src/libfetchers/git-utils.cc @@ -2,6 +2,7 @@ #include "nix/fetchers/git-lfs-fetch.hh" #include "nix/fetchers/cache.hh" #include "nix/fetchers/fetch-settings.hh" +#include "nix/util/base-n.hh" #include "nix/util/finally.hh" #include "nix/util/processes.hh" #include "nix/util/signals.hh" @@ -608,7 +609,7 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this // Calculate sha256 fingerprint from public key and escape the regex symbol '+' to match the key literally std::string keyDecoded; try { - keyDecoded = base64Decode(k.key); + keyDecoded = base64::decode(k.key); } catch (Error & e) { e.addTrace({}, "while decoding public key '%s' used for git signature", k.key); } diff --git a/src/libstore/machines.cc b/src/libstore/machines.cc index 4ae5cd206..d61467666 100644 --- a/src/libstore/machines.cc +++ b/src/libstore/machines.cc @@ -1,3 +1,4 @@ +#include "nix/util/base-n.hh" #include "nix/store/machines.hh" #include "nix/store/globals.hh" #include "nix/store/store-open.hh" @@ -158,7 +159,7 @@ static Machine parseBuilderLine(const StringSet & defaultSystems, const std::str auto ensureBase64 = [&](size_t fieldIndex) { const auto & str = tokens[fieldIndex]; try { - base64Decode(str); + base64::decode(str); } catch (FormatError & e) { e.addTrace({}, "while parsing machine specification at a column #%lu in a row: '%s'", fieldIndex, line); throw; diff --git a/src/libstore/ssh.cc b/src/libstore/ssh.cc index e53c4b336..474b3622a 100644 --- a/src/libstore/ssh.cc +++ b/src/libstore/ssh.cc @@ -4,13 +4,14 @@ #include "nix/util/environment-variables.hh" #include "nix/util/util.hh" #include "nix/util/exec.hh" +#include "nix/util/base-n.hh" namespace nix { static std::string parsePublicHostKey(std::string_view host, std::string_view sshPublicHostKey) { try { - return base64Decode(sshPublicHostKey); + return base64::decode(sshPublicHostKey); } catch (Error & e) { e.addTrace({}, "while decoding ssh public host key for host '%s'", host); throw; diff --git a/src/libutil-tests/base-n.cc b/src/libutil-tests/base-n.cc new file mode 100644 index 000000000..8de78b55d --- /dev/null +++ b/src/libutil-tests/base-n.cc @@ -0,0 +1,68 @@ +#include +#include + +#include "nix/util/base-n.hh" +#include "nix/util/error.hh" + +namespace nix { + +static const std::span stringToByteSpan(const std::string_view s) +{ + return {(const std::byte *) s.data(), s.size()}; +} + +/* ---------------------------------------------------------------------------- + * base64::encode + * --------------------------------------------------------------------------*/ + +TEST(base64Encode, emptyString) +{ + ASSERT_EQ(base64::encode(stringToByteSpan("")), ""); +} + +TEST(base64Encode, encodesAString) +{ + ASSERT_EQ(base64::encode(stringToByteSpan("quod erat demonstrandum")), "cXVvZCBlcmF0IGRlbW9uc3RyYW5kdW0="); +} + +TEST(base64Encode, encodeAndDecode) +{ + auto s = "quod erat demonstrandum"; + auto encoded = base64::encode(stringToByteSpan(s)); + auto decoded = base64::decode(encoded); + + ASSERT_EQ(decoded, s); +} + +TEST(base64Encode, encodeAndDecodeNonPrintable) +{ + char s[256]; + std::iota(std::rbegin(s), std::rend(s), 0); + + auto encoded = base64::encode(std::as_bytes(std::span{std::string_view{s}})); + auto decoded = base64::decode(encoded); + + EXPECT_EQ(decoded.length(), 255u); + ASSERT_EQ(decoded, s); +} + +/* ---------------------------------------------------------------------------- + * base64::decode + * --------------------------------------------------------------------------*/ + +TEST(base64Decode, emptyString) +{ + ASSERT_EQ(base64::decode(""), ""); +} + +TEST(base64Decode, decodeAString) +{ + ASSERT_EQ(base64::decode("cXVvZCBlcmF0IGRlbW9uc3RyYW5kdW0="), "quod erat demonstrandum"); +} + +TEST(base64Decode, decodeThrowsOnInvalidChar) +{ + ASSERT_THROW(base64::decode("cXVvZCBlcm_0IGRlbW9uc3RyYW5kdW0="), Error); +} + +} // namespace nix diff --git a/src/libutil-tests/meson.build b/src/libutil-tests/meson.build index b3776e094..0097611c6 100644 --- a/src/libutil-tests/meson.build +++ b/src/libutil-tests/meson.build @@ -44,6 +44,7 @@ subdir('nix-meson-build-support/common') sources = files( 'args.cc', + 'base-n.cc', 'canon-path.cc', 'checked-arithmetic.cc', 'chunked-vector.cc', diff --git a/src/libutil-tests/util.cc b/src/libutil-tests/util.cc index 534731c6c..c48b97e8e 100644 --- a/src/libutil-tests/util.cc +++ b/src/libutil-tests/util.cc @@ -3,6 +3,7 @@ #include "nix/util/file-system.hh" #include "nix/util/terminal.hh" #include "nix/util/strings.hh" +#include "nix/util/base-n.hh" #include #include @@ -48,60 +49,6 @@ TEST(hasSuffix, trivialCase) ASSERT_TRUE(hasSuffix("foobar", "bar")); } -/* ---------------------------------------------------------------------------- - * base64Encode - * --------------------------------------------------------------------------*/ - -TEST(base64Encode, emptyString) -{ - ASSERT_EQ(base64Encode(""), ""); -} - -TEST(base64Encode, encodesAString) -{ - ASSERT_EQ(base64Encode("quod erat demonstrandum"), "cXVvZCBlcmF0IGRlbW9uc3RyYW5kdW0="); -} - -TEST(base64Encode, encodeAndDecode) -{ - auto s = "quod erat demonstrandum"; - auto encoded = base64Encode(s); - auto decoded = base64Decode(encoded); - - ASSERT_EQ(decoded, s); -} - -TEST(base64Encode, encodeAndDecodeNonPrintable) -{ - char s[256]; - std::iota(std::rbegin(s), std::rend(s), 0); - - auto encoded = base64Encode(s); - auto decoded = base64Decode(encoded); - - EXPECT_EQ(decoded.length(), 255u); - ASSERT_EQ(decoded, s); -} - -/* ---------------------------------------------------------------------------- - * base64Decode - * --------------------------------------------------------------------------*/ - -TEST(base64Decode, emptyString) -{ - ASSERT_EQ(base64Decode(""), ""); -} - -TEST(base64Decode, decodeAString) -{ - ASSERT_EQ(base64Decode("cXVvZCBlcmF0IGRlbW9uc3RyYW5kdW0="), "quod erat demonstrandum"); -} - -TEST(base64Decode, decodeThrowsOnInvalidChar) -{ - ASSERT_THROW(base64Decode("cXVvZCBlcm_0IGRlbW9uc3RyYW5kdW0="), Error); -} - /* ---------------------------------------------------------------------------- * getLine * --------------------------------------------------------------------------*/ diff --git a/src/libutil/base-n.cc b/src/libutil/base-n.cc new file mode 100644 index 000000000..4c9726ad2 --- /dev/null +++ b/src/libutil/base-n.cc @@ -0,0 +1,114 @@ +#include + +#include "nix/util/array-from-string-literal.hh" +#include "nix/util/util.hh" +#include "nix/util/base-n.hh" + +using namespace std::literals; + +namespace nix { + +constexpr static const std::array base16Chars = "0123456789abcdef"_arrayNoNull; + +std::string base16::encode(std::span b) +{ + std::string buf; + buf.reserve(b.size() * 2); + for (size_t i = 0; i < b.size(); i++) { + buf.push_back(base16Chars[(uint8_t) b.data()[i] >> 4]); + buf.push_back(base16Chars[(uint8_t) b.data()[i] & 0x0f]); + } + return buf; +} + +std::string base16::decode(std::string_view s) +{ + auto parseHexDigit = [&](char c) { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + throw FormatError("invalid character in Base16 string: '%c'", c); + }; + + assert(s.size() % 2 == 0); + auto decodedSize = s.size() / 2; + + std::string res; + res.reserve(decodedSize); + + for (unsigned int i = 0; i < decodedSize; i++) { + res.push_back(parseHexDigit(s[i * 2]) << 4 | parseHexDigit(s[i * 2 + 1])); + } + + return res; +} + +constexpr static const std::array base64Chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"_arrayNoNull; + +std::string base64::encode(std::span s) +{ + std::string res; + res.reserve((s.size() + 2) / 3 * 4); + int data = 0, nbits = 0; + + for (std::byte c : s) { + data = data << 8 | (uint8_t) c; + nbits += 8; + while (nbits >= 6) { + nbits -= 6; + res.push_back(base64Chars[data >> nbits & 0x3f]); + } + } + + if (nbits) + res.push_back(base64Chars[data << (6 - nbits) & 0x3f]); + while (res.size() % 4) + res.push_back('='); + + return res; +} + +std::string base64::decode(std::string_view s) +{ + constexpr char npos = -1; + constexpr std::array base64DecodeChars = [&] { + std::array result{}; + for (auto & c : result) + c = npos; + for (int i = 0; i < 64; i++) + result[base64Chars[i]] = i; + return result; + }(); + + std::string res; + // Some sequences are missing the padding consisting of up to two '='. + // vvv + res.reserve((s.size() + 2) / 4 * 3); + unsigned int d = 0, bits = 0; + + for (char c : s) { + if (c == '=') + break; + if (c == '\n') + continue; + + char digit = base64DecodeChars[(unsigned char) c]; + if (digit == npos) + throw FormatError("invalid character in Base64 string: '%c'", c); + + bits += 6; + d = d << 6 | digit; + if (bits >= 8) { + res.push_back(d >> (bits - 8) & 0xff); + bits -= 8; + } + } + + return res; +} + +} // namespace nix diff --git a/src/libutil/base-nix-32.cc b/src/libutil/base-nix-32.cc index dec5cd7d7..4f5af462d 100644 --- a/src/libutil/base-nix-32.cc +++ b/src/libutil/base-nix-32.cc @@ -1,6 +1,7 @@ #include #include "nix/util/base-nix-32.hh" +#include "nix/util/util.hh" namespace nix { @@ -16,12 +17,12 @@ constexpr const std::array BaseNix32::reverseMap = [] { return map; }(); -std::string BaseNix32::encode(std::span originalData) +std::string BaseNix32::encode(std::span bs) { - if (originalData.size() == 0) + if (bs.size() == 0) return {}; - size_t len = encodedLength(originalData.size()); + size_t len = encodedLength(bs.size()); assert(len); std::string s; @@ -31,12 +32,42 @@ std::string BaseNix32::encode(std::span originalData) unsigned int b = n * 5; unsigned int i = b / 8; unsigned int j = b % 8; - unsigned char c = - (originalData.data()[i] >> j) | (i >= originalData.size() - 1 ? 0 : originalData.data()[i + 1] << (8 - j)); - s.push_back(characters[c & 0x1f]); + std::byte c = (bs.data()[i] >> j) | (i >= bs.size() - 1 ? std::byte{0} : bs.data()[i + 1] << (8 - j)); + s.push_back(characters[uint8_t(c & std::byte{0x1f})]); } return s; } +std::string BaseNix32::decode(std::string_view s) +{ + std::string res; + res.reserve((s.size() * 5 + 7) / 8); // ceiling(size * 5/8) + + for (unsigned int n = 0; n < s.size(); ++n) { + char c = s[s.size() - n - 1]; + auto digit_opt = BaseNix32::lookupReverse(c); + + if (!digit_opt) + throw FormatError("invalid character in Nix32 (Nix's Base32 variation) string: '%c'", c); + + uint8_t digit = *digit_opt; + + unsigned int b = n * 5; + unsigned int i = b / 8; + unsigned int j = b % 8; + + // Ensure res has enough space + res.resize(i + 1); + res[i] |= digit << j; + + if (digit >> (8 - j)) { + res.resize(i + 2); + res[i + 1] |= digit >> (8 - j); + } + } + + return res; +} + } // namespace nix diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc index 03003c689..1319924bf 100644 --- a/src/libutil/hash.cc +++ b/src/libutil/hash.cc @@ -11,6 +11,7 @@ #include "nix/util/archive.hh" #include "nix/util/configuration.hh" #include "nix/util/split.hh" +#include "nix/util/base-n.hh" #include "nix/util/base-nix-32.hh" #include @@ -59,25 +60,6 @@ std::strong_ordering Hash::operator<=>(const Hash & h) const noexcept return std::strong_ordering::equivalent; } -const std::string base16Chars = "0123456789abcdef"; - -static std::string printHash16(const Hash & hash) -{ - std::string buf; - buf.reserve(hash.hashSize * 2); - for (unsigned int i = 0; i < hash.hashSize; i++) { - buf.push_back(base16Chars[hash.hash[i] >> 4]); - buf.push_back(base16Chars[hash.hash[i] & 0x0f]); - } - return buf; -} - -static std::string printHash32(const Hash & hash) -{ - assert(hash.hashSize); - return BaseNix32::encode({&hash.hash[0], hash.hashSize}); -} - std::string printHash16or32(const Hash & hash) { assert(static_cast(hash.algo)); @@ -91,16 +73,20 @@ std::string Hash::to_string(HashFormat hashFormat, bool includeAlgo) const s += printHashAlgo(algo); s += hashFormat == HashFormat::SRI ? '-' : ':'; } + const auto bytes = std::as_bytes(std::span{&hash[0], hashSize}); switch (hashFormat) { case HashFormat::Base16: - s += printHash16(*this); + assert(hashSize); + s += base16::encode(bytes); break; case HashFormat::Nix32: - s += printHash32(*this); + assert(hashSize); + s += BaseNix32::encode(bytes); break; case HashFormat::Base64: case HashFormat::SRI: - s += base64Encode(std::string_view((const char *) hash, hashSize)); + assert(hashSize); + s += base64::encode(bytes); break; } return s; @@ -180,63 +166,38 @@ Hash Hash::parseNonSRIUnprefixed(std::string_view s, HashAlgorithm algo) Hash::Hash(std::string_view rest, HashAlgorithm algo, bool isSRI) : Hash(algo) { - if (!isSRI && rest.size() == base16Len()) { + auto [decode, formatName] = [&]() -> std::pair { + if (isSRI) { + /* In the SRI case, we always are using Base64. If the + length is wrong, get an error later. */ + return {base64::decode, "SRI"}; + } else { + /* Otherwise, decide via the length of the hash (for the + given algorithm) what base encoding it is. */ - auto parseHexDigit = [&](char c) { - if (c >= '0' && c <= '9') - return c - '0'; - if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - throw BadHash("invalid base-16 hash '%s'", rest); - }; + if (rest.size() == base16::encodedLength(hashSize)) + return {base16::decode, "base16"}; - for (unsigned int i = 0; i < hashSize; i++) { - hash[i] = parseHexDigit(rest[i * 2]) << 4 | parseHexDigit(rest[i * 2 + 1]); + if (rest.size() == BaseNix32::encodedLength(hashSize)) + return {BaseNix32::decode, "nix32"}; + + if (rest.size() == base64::encodedLength(hashSize)) + return {base64::decode, "Base64"}; } - } - else if (!isSRI && rest.size() == base32Len()) { - - for (unsigned int n = 0; n < rest.size(); ++n) { - char c = rest[rest.size() - n - 1]; - auto digit_opt = BaseNix32::lookupReverse(c); - - if (!digit_opt) - throw BadHash("invalid base-32 hash: '%s'", rest); - - uint8_t digit = std::move(*digit_opt); - - unsigned int b = n * 5; - unsigned int i = b / 8; - unsigned int j = b % 8; - hash[i] |= digit << j; - - if (i < hashSize - 1) { - hash[i + 1] |= digit >> (8 - j); - } else { - if (digit >> (8 - j)) - throw BadHash("invalid base-32 hash '%s'", rest); - } - } - } - - else if (isSRI || rest.size() == base64Len()) { - std::string d; - try { - d = base64Decode(rest); - } catch (Error & e) { - e.addTrace({}, "While decoding hash '%s'", rest); - } - if (d.size() != hashSize) - throw BadHash("invalid %s hash '%s'", isSRI ? "SRI" : "base-64", rest); - assert(hashSize); - memcpy(hash, d.data(), hashSize); - } - - else throw BadHash("hash '%s' has wrong length for hash algorithm '%s'", rest, printHashAlgo(this->algo)); + }(); + + std::string d; + try { + d = decode(rest); + } catch (Error & e) { + e.addTrace({}, "While decoding hash '%s'", rest); + } + if (d.size() != hashSize) + throw BadHash("invalid %s hash '%s' %d %d", formatName, rest); + assert(hashSize); + memcpy(hash, d.data(), hashSize); } Hash Hash::random(HashAlgorithm algo) diff --git a/src/libutil/include/nix/util/array-from-string-literal.hh b/src/libutil/include/nix/util/array-from-string-literal.hh new file mode 100644 index 000000000..a4a137609 --- /dev/null +++ b/src/libutil/include/nix/util/array-from-string-literal.hh @@ -0,0 +1,27 @@ +#pragma once +///@file + +#include +#include + +namespace nix { + +template +struct ArrayNoNullAdaptor +{ + std::array data; + + constexpr ArrayNoNullAdaptor(const char (&init)[sizeWithNull]) + { + static_assert(sizeWithNull > 0); + std::copy_n(init, sizeWithNull - 1, data.data()); + } +}; + +template +constexpr auto operator""_arrayNoNull() +{ + return str.data; +} + +} // namespace nix diff --git a/src/libutil/include/nix/util/base-n.hh b/src/libutil/include/nix/util/base-n.hh new file mode 100644 index 000000000..637a06f3f --- /dev/null +++ b/src/libutil/include/nix/util/base-n.hh @@ -0,0 +1,53 @@ +#pragma once +///@file + +#include +#include + +namespace nix { + +namespace base16 { + +/** + * Returns the length of a base-16 representation of this many bytes. + */ +[[nodiscard]] constexpr static inline size_t encodedLength(size_t origSize) +{ + return origSize * 2; +} + +/** + * Encode arbitrary bytes as Base16. + */ +std::string encode(std::span b); + +/** + * Decode arbitrary Base16 string to bytes. + */ +std::string decode(std::string_view s); + +} // namespace base16 + +namespace base64 { + +/** + * Returns the length of a base-64 representation of this many bytes. + */ +[[nodiscard]] constexpr static inline size_t encodedLength(size_t origSize) +{ + return ((4 * origSize / 3) + 3) & ~3; +} + +/** + * Encode arbitrary bytes as Base64. + */ +std::string encode(std::span b); + +/** + * Decode arbitrary Base64 string to bytes. + */ +std::string decode(std::string_view s); + +} // namespace base64 + +} // namespace nix diff --git a/src/libutil/include/nix/util/base-nix-32.hh b/src/libutil/include/nix/util/base-nix-32.hh index 37b23a2bb..28095e92c 100644 --- a/src/libutil/include/nix/util/base-nix-32.hh +++ b/src/libutil/include/nix/util/base-nix-32.hh @@ -7,14 +7,14 @@ #include #include +#include "nix/util/array-from-string-literal.hh" + namespace nix { struct BaseNix32 { /// omitted: E O U T - constexpr static std::array characters = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', - 'b', 'c', 'd', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', - 'n', 'p', 'q', 'r', 's', 'v', 'w', 'x', 'y', 'z'}; + constexpr static std::array characters = "0123456789abcdfghijklmnpqrsvwxyz"_arrayNoNull; private: static const std::array reverseMap; @@ -34,12 +34,14 @@ public: /** * Returns the length of a base-32 representation of this hash. */ - static size_t encodedLength(size_t originalLength) + [[nodiscard]] constexpr static inline size_t encodedLength(size_t originalLength) { return (originalLength * 8 - 1) / 5 + 1; } - static std::string encode(std::span originalData); + static std::string encode(std::span originalData); + + static std::string decode(std::string_view s); }; } // namespace nix diff --git a/src/libutil/include/nix/util/hash.hh b/src/libutil/include/nix/util/hash.hh index 7b76095cf..fdd4c6fa7 100644 --- a/src/libutil/include/nix/util/hash.hh +++ b/src/libutil/include/nix/util/hash.hh @@ -110,30 +110,6 @@ public: */ std::strong_ordering operator<=>(const Hash & h2) const noexcept; - /** - * Returns the length of a base-16 representation of this hash. - */ - [[nodiscard]] size_t base16Len() const - { - return hashSize * 2; - } - - /** - * Returns the length of a base-32 representation of this hash. - */ - [[nodiscard]] size_t base32Len() const - { - return (hashSize * 8 - 1) / 5 + 1; - } - - /** - * Returns the length of a base-64 representation of this hash. - */ - [[nodiscard]] size_t base64Len() const - { - return ((4 * hashSize / 3) + 3) & ~3; - } - /** * Return a string representation of the hash, in base-16, base-32 * or base-64. By default, this is prefixed by the hash algo diff --git a/src/libutil/include/nix/util/meson.build b/src/libutil/include/nix/util/meson.build index b7d4d761d..bc58b4d5e 100644 --- a/src/libutil/include/nix/util/meson.build +++ b/src/libutil/include/nix/util/meson.build @@ -8,6 +8,8 @@ headers = files( 'archive.hh', 'args.hh', 'args/root.hh', + 'array-from-string-literal.hh', + 'base-n.hh', 'base-nix-32.hh', 'callback.hh', 'canon-path.hh', diff --git a/src/libutil/include/nix/util/util.hh b/src/libutil/include/nix/util/util.hh index 015086d39..56041a112 100644 --- a/src/libutil/include/nix/util/util.hh +++ b/src/libutil/include/nix/util/util.hh @@ -179,16 +179,6 @@ constexpr char treeLast[] = "└───"; constexpr char treeLine[] = "│ "; constexpr char treeNull[] = " "; -/** - * Encode arbitrary bytes as Base64. - */ -std::string base64Encode(std::string_view s); - -/** - * Decode arbitrary bytes to Base64. - */ -std::string base64Decode(std::string_view s); - /** * Remove common leading whitespace from the lines in the string * 's'. For example, if every line is indented by at least 3 spaces, diff --git a/src/libutil/meson.build b/src/libutil/meson.build index fb3e98e1d..ea2cb679e 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -112,6 +112,7 @@ subdir('nix-meson-build-support/common') sources = [config_priv_h] + files( 'archive.cc', 'args.cc', + 'base-n.cc', 'base-nix-32.cc', 'canon-path.cc', 'compression.cc', diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index 374b5569d..1541aed2f 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -1,6 +1,7 @@ #include "nix/util/signature/local-keys.hh" #include "nix/util/file-system.hh" +#include "nix/util/base-n.hh" #include "nix/util/util.hh" #include @@ -25,7 +26,7 @@ Key::Key(std::string_view s, bool sensitiveValue) if (name == "" || key == "") throw FormatError("key is corrupt"); - key = base64Decode(key); + key = base64::decode(key); } catch (Error & e) { std::string extra; if (!sensitiveValue) @@ -37,7 +38,7 @@ Key::Key(std::string_view s, bool sensitiveValue) std::string Key::to_string() const { - return name + ":" + base64Encode(key); + return name + ":" + base64::encode(std::as_bytes(std::span{key})); } SecretKey::SecretKey(std::string_view s) @@ -52,7 +53,7 @@ std::string SecretKey::signDetached(std::string_view data) const unsigned char sig[crypto_sign_BYTES]; unsigned long long sigLen; crypto_sign_detached(sig, &sigLen, (unsigned char *) data.data(), data.size(), (unsigned char *) key.data()); - return name + ":" + base64Encode(std::string((char *) sig, sigLen)); + return name + ":" + base64::encode(std::as_bytes(std::span{sig, sigLen})); } PublicKey SecretKey::toPublicKey() const @@ -93,7 +94,7 @@ bool PublicKey::verifyDetachedAnon(std::string_view data, std::string_view sig) { std::string sig2; try { - sig2 = base64Decode(sig); + sig2 = base64::decode(sig); } catch (Error & e) { e.addTrace({}, "while decoding signature '%s'", sig); } diff --git a/src/libutil/util.cc b/src/libutil/util.cc index 5cbbb80ee..383a904ad 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -204,70 +204,6 @@ void ignoreExceptionExceptInterrupt(Verbosity lvl) } } -constexpr char base64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -std::string base64Encode(std::string_view s) -{ - std::string res; - res.reserve((s.size() + 2) / 3 * 4); - int data = 0, nbits = 0; - - for (char c : s) { - data = data << 8 | (unsigned char) c; - nbits += 8; - while (nbits >= 6) { - nbits -= 6; - res.push_back(base64Chars[data >> nbits & 0x3f]); - } - } - - if (nbits) - res.push_back(base64Chars[data << (6 - nbits) & 0x3f]); - while (res.size() % 4) - res.push_back('='); - - return res; -} - -std::string base64Decode(std::string_view s) -{ - constexpr char npos = -1; - constexpr std::array base64DecodeChars = [&] { - std::array result{}; - for (auto & c : result) - c = npos; - for (int i = 0; i < 64; i++) - result[base64Chars[i]] = i; - return result; - }(); - - std::string res; - // Some sequences are missing the padding consisting of up to two '='. - // vvv - res.reserve((s.size() + 2) / 4 * 3); - unsigned int d = 0, bits = 0; - - for (char c : s) { - if (c == '=') - break; - if (c == '\n') - continue; - - char digit = base64DecodeChars[(unsigned char) c]; - if (digit == npos) - throw FormatError("invalid character in Base64 string: '%c'", c); - - bits += 6; - d = d << 6 | digit; - if (bits >= 8) { - res.push_back(d >> (bits - 8) & 0xff); - bits -= 8; - } - } - - return res; -} - std::string stripIndentation(std::string_view s) { size_t minIndent = 10000;