From 76baa4c50db1f0ffb1c13e0680c06f0d05e49dd3 Mon Sep 17 00:00:00 2001 From: Emily Date: Wed, 30 Jul 2025 17:40:16 +0100 Subject: [PATCH] libutil: extract Base32 helpers from `Hash` Change-Id: I6a6a6964f95aecf152090a3bf82b5ec287a21481 --- lix/libutil/hash.cc | 51 ++----------------------------- lix/libutil/hash.hh | 2 -- lix/libutil/references.cc | 1 + lix/libutil/strings.cc | 60 +++++++++++++++++++++++++++++++++++++ lix/libutil/strings.hh | 6 ++++ tests/unit/libutil/tests.cc | 47 +++++++++++++++++++++++++++++ 6 files changed, 117 insertions(+), 50 deletions(-) diff --git a/lix/libutil/hash.cc b/lix/libutil/hash.cc index d4771a177..68cb67dad 100644 --- a/lix/libutil/hash.cc +++ b/lix/libutil/hash.cc @@ -80,33 +80,6 @@ static std::string printHash16(const Hash & hash) } -// omitted: E O U T -const std::string base32Chars = "0123456789abcdfghijklmnpqrsvwxyz"; - - -static std::string printHash32(const Hash & hash) -{ - assert(hash.hashSize); - size_t len = hash.base32Len(); - assert(len); - - std::string s; - s.reserve(len); - - for (int n = (int) len - 1; n >= 0; n--) { - unsigned int b = n * 5; - unsigned int i = b / 8; - unsigned int j = b % 8; - unsigned char c = - (hash.hash[i] >> j) - | (i >= hash.hashSize - 1 ? 0 : hash.hash[i + 1] << (8 - j)); - s.push_back(base32Chars[c & 0x1f]); - } - - return s; -} - - std::string printHash16or32(const Hash & hash) { return hash.to_string(hash.type == HashType::MD5 ? Base::Base16 : Base::Base32, false); @@ -125,7 +98,7 @@ std::string Hash::to_string(Base base, bool includeType) const s += printHash16(*this); break; case Base::Base32: - s += printHash32(*this); + s += base32Encode(std::string_view(charptr_cast(hash), hashSize)); break; case Base::Base64: case Base::SRI: @@ -225,26 +198,8 @@ Hash::Hash(std::string_view rest, HashType type, bool isSRI) } else if (!isSRI && rest.size() == base32Len()) { - - for (unsigned int n = 0; n < rest.size(); ++n) { - char c = rest[rest.size() - n - 1]; - size_t digit; - for (digit = 0; digit < base32Chars.size(); ++digit) /* !!! slow */ - if (base32Chars[digit] == c) break; - if (digit >= 32) - throw BadHash("invalid base-32 hash '%s'", rest); - 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); - } - } + auto d = base32Decode(rest); + memcpy(hash, d.data(), hashSize); } else if (isSRI || rest.size() == base64Len()) { diff --git a/lix/libutil/hash.hh b/lix/libutil/hash.hh index 7e8910ed0..79858fd99 100644 --- a/lix/libutil/hash.hh +++ b/lix/libutil/hash.hh @@ -32,8 +32,6 @@ const int sha512HashSize = 64; extern std::set hashTypes; -extern const std::string base32Chars; - enum class Base : int { Base64, Base32, Base16, SRI }; diff --git a/lix/libutil/references.cc b/lix/libutil/references.cc index 1b9a2a955..5055d122b 100644 --- a/lix/libutil/references.cc +++ b/lix/libutil/references.cc @@ -1,4 +1,5 @@ #include "lix/libutil/references.hh" +#include "lix/libutil/strings.hh" #include "lix/libutil/hash.hh" #include "lix/libutil/logging.hh" diff --git a/lix/libutil/strings.cc b/lix/libutil/strings.cc index 1c32b871f..9349c1e25 100644 --- a/lix/libutil/strings.cc +++ b/lix/libutil/strings.cc @@ -195,6 +195,66 @@ std::string base64Decode(std::string_view s) return res; } +// omitted: E O U T +const std::string base32Chars = "0123456789abcdfghijklmnpqrsvwxyz"; + +std::string base32Encode(std::string_view s) +{ + if (s.empty()) { + return ""; + } + + size_t len = (s.size() * 8 - 1) / 5 + 1; + + std::string res; + res.reserve(len); + + for (int n = (int) len - 1; n >= 0; n--) { + unsigned int b = n * 5; + unsigned int i = b / 8; + unsigned int j = b % 8; + unsigned char c = (static_cast(s[i]) >> j) | (i >= s.size() - 1 ? 0 : static_cast(s[i + 1]) << (8 - j)); + res.push_back(base32Chars[c & 0x1f]); + } + + return res; +} + +std::string base32Decode(std::string_view s) +{ + if (s.empty()) { + return ""; + } + + std::string res(((s.size() - 1) * 5) / 8 + 1, 0); + + for (unsigned int n = 0; n < s.size(); ++n) { + char c = s[s.size() - n - 1]; + size_t digit; + for (digit = 0; digit < base32Chars.size(); ++digit) /* !!! slow */ { + if (base32Chars[digit] == c) { + break; + } + } + if (digit >= 32) { + throw Error("invalid character in base-32 string '%s'", s); + } + unsigned int b = n * 5; + unsigned int i = b / 8; + unsigned int j = b % 8; + res[i] |= digit << j; + + if (i < s.size() - 1) { + res[i + 1] |= digit >> (8 - j); + } else { + if (digit >> (8 - j)) { + throw Error("invalid base-32 string '%s'", s); + } + } + } + + return res; +} std::string stripIndentation(std::string_view s) { diff --git a/lix/libutil/strings.hh b/lix/libutil/strings.hh index 67238824a..de575b463 100644 --- a/lix/libutil/strings.hh +++ b/lix/libutil/strings.hh @@ -197,6 +197,12 @@ std::string shellEscape(const std::string_view s); std::string base64Encode(std::string_view s); std::string base64Decode(std::string_view s); +/** + * Base32 encoding/decoding. + */ +extern const std::string base32Chars; +std::string base32Encode(std::string_view s); +std::string base32Decode(std::string_view s); /** * Remove common leading whitespace from the lines in the string diff --git a/tests/unit/libutil/tests.cc b/tests/unit/libutil/tests.cc index 263fd7834..7982b87c3 100644 --- a/tests/unit/libutil/tests.cc +++ b/tests/unit/libutil/tests.cc @@ -364,6 +364,53 @@ namespace nix { ASSERT_THROW(base64Decode("cXVvZCBlcm_0IGRlbW9uc3RyYW5kdW0="), Error); } + /* ---------------------------------------------------------------------------- + * base32Encode + * --------------------------------------------------------------------------*/ + + TEST(base32Encode, emptyString) { + ASSERT_EQ(base32Encode(""), ""); + } + + TEST(base32Encode, encodesAString) { + ASSERT_EQ(base32Encode("quod erat demonstrandum"), "6sxb4drhp4x3kdrpnsrb441s62wk541j6yxbi"); + } + + TEST(base32Encode, encodeAndDecode) { + auto s = "quod erat demonstrandum"; + auto encoded = base32Encode(s); + auto decoded = base32Decode(encoded); + + ASSERT_EQ(decoded, s); + } + + TEST(base32Encode, encodeAndDecodeNonPrintable) { + char s[256]; + std::iota(std::rbegin(s), std::rend(s), 0); + + auto encoded = base32Encode(s); + auto decoded = base32Decode(encoded); + + EXPECT_EQ(decoded.length(), 255); + ASSERT_EQ(decoded, s); + } + + /* ---------------------------------------------------------------------------- + * base32Decode + * --------------------------------------------------------------------------*/ + + TEST(base32Decode, emptyString) { + ASSERT_EQ(base32Decode(""), ""); + } + + TEST(base32Decode, decodeAString) { + ASSERT_EQ(base32Decode("6sxb4drhp4x3kdrpnsrb441s62wk541j6yxbi"), "quod erat demonstrandum"); + } + + TEST(base32Decode, decodeThrowsOnInvalidChar) { + ASSERT_THROW(base32Decode("6sxb4drhp4x3kdrpnsrb441s62wk541j6yxbe"), Error); + } + /* ---------------------------------------------------------------------------- * getLine * --------------------------------------------------------------------------*/