libutil: extract Base32 helpers from Hash

base32Encode now takes std::span<std::byte>, with a base32EncodeStr
convenience wrapper which takes std::string_view.

Co-authored-by: Qyriad <qyriad@qyriad.me>

Change-Id: I6a6a6964f799dc84ecbfb55c7ca03a064cff71d9
This commit is contained in:
Emily
2025-11-14 14:58:15 +01:00
committed by Qyriad
parent 2fa40c9de4
commit 9f3ba30783
6 changed files with 146 additions and 50 deletions
+3 -48
View File
@@ -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 += base32EncodeStr(std::string_view(charptr_cast<const char *>(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()) {
-2
View File
@@ -32,8 +32,6 @@ const int sha512HashSize = 64;
extern std::set<std::string> hashTypes;
extern const std::string base32Chars;
enum class Base : int { Base64, Base32, Base16, SRI };
+1
View File
@@ -1,4 +1,5 @@
#include "lix/libutil/references.hh"
#include "lix/libutil/strings.hh"
#include "lix/libutil/hash.hh"
#include "lix/libutil/logging.hh"
+77
View File
@@ -2,6 +2,7 @@
#include "lix/libutil/strings.hh"
#include "lix/libutil/references.hh"
#include <boost/lexical_cast.hpp>
#include <ranges>
#include <stdint.h>
namespace nix {
@@ -231,6 +232,82 @@ std::string base64Decode(std::string_view s)
return res;
}
// omitted: E O U T
const std::string base32Chars = "0123456789abcdfghijklmnpqrsvwxyz";
std::string base32EncodeStr(std::string_view s)
{
std::span<std::byte const> sp = std::as_bytes(std::span(s));
return base32Encode(sp);
}
std::string base32Encode(std::span<std::byte const> bytes)
{
// log2(32) == 5.
constexpr int B32_BITS_PER_DIGIT = 5;
// We need to do arithmetic.
auto const s = std::views::transform(bytes, std::to_integer<std::uint32_t>);
if (s.empty()) {
return "";
}
ssize_t len = (s.size() * CHAR_BIT - 1) / B32_BITS_PER_DIGIT + 1;
std::string res;
res.reserve(len);
for (ssize_t const n : std::views::iota(0, len) | std::views::reverse) {
unsigned int b = n * B32_BITS_PER_DIGIT;
unsigned int i = b / CHAR_BIT;
unsigned int j = b % CHAR_BIT;
auto const curChar = s[i];
auto const second = i >= s.size() - 1 ? 0 : s[i + 1] << (CHAR_BIT - j);
auto const c = (curChar >> j) | second;
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 < res.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)
{
+7
View File
@@ -202,6 +202,13 @@ std::string bashEscape(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 base32EncodeStr(std::string_view s);
std::string base32Encode(std::span<std::byte const> const s);
std::string base32Decode(std::string_view s);
/**
* Remove common leading whitespace from the lines in the string
+58
View File
@@ -365,6 +365,64 @@ namespace nix {
ASSERT_THROW(base64Decode("cXVvZCBlcm_0IGRlbW9uc3RyYW5kdW0="), Error);
}
/* ----------------------------------------------------------------------------
* base32Encode
* --------------------------------------------------------------------------*/
TEST(base32Encode, emptyString) {
ASSERT_EQ(base32EncodeStr(""), "");
}
TEST(base32Encode, encodesAString) {
ASSERT_EQ(base32EncodeStr("quod erat demonstrandum"), "6sxb4drhp4x3kdrpnsrb441s62wk541j6yxbi");
}
TEST(base32Encode, encodeAndDecode) {
auto s = "quod erat demonstrandum";
auto encoded = base32EncodeStr(s);
auto decoded = base32Decode(encoded);
ASSERT_EQ(decoded, s);
}
TEST(base32Encode, encodeAndDecodeNonPrintable) {
std::string s(257, '\0');
std::iota(std::rbegin(s), std::rend(s), 0);
auto encoded = base32EncodeStr(s);
auto decoded = base32Decode(encoded);
EXPECT_EQ(decoded.length(), 257);
ASSERT_EQ(decoded, s);
}
TEST(base32Encode, handleNulChars) {
std::string s = "cat girls say meow even with NULs";
// Just throw a NUL in there somewhere.
s[5] = '\0';
auto encoded = base32EncodeStr(s);
auto decoded = base32Decode(encoded);
EXPECT_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
* --------------------------------------------------------------------------*/