From ec2f9ca50429da502ea0d4feb0754bbcb6e8a21c Mon Sep 17 00:00:00 2001 From: Max Murshed Date: Fri, 12 Jun 2026 15:28:48 -0700 Subject: [PATCH 1/4] Limb move and byte serialization APIs --- include/biginteger/BigInteger.h | 92 +++++++++++++++++++++++++++++++- tests/unit/test_construction.cpp | 75 ++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) diff --git a/include/biginteger/BigInteger.h b/include/biginteger/BigInteger.h index c71ba8a..067b6af 100644 --- a/include/biginteger/BigInteger.h +++ b/include/biginteger/BigInteger.h @@ -7,6 +7,8 @@ #ifndef BIGINTEGER #define BIGINTEGER +#include +#include #include #include "common/Util.h" @@ -23,7 +25,7 @@ namespace BigMath // True if the number is negative bool isNegative; - // Constructor, desctructor, and assignment operator + // Constructor, destructor, and assignment operator public: explicit BigInteger(SizeT size = 0, bool negative = false) : theInteger(size == 0 ? 1 : size, 0), isNegative(negative) { @@ -40,6 +42,15 @@ namespace BigMath } } + BigInteger(std::vector&& aInt, bool negative) : theInteger(std::move(aInt)), isNegative(negative) + { + TrimZerosToOne(theInteger); + if (isNegative && Zero()) + { + isNegative = false; + } + } + // Filled with specified data BigInteger(SizeT size, bool negative, DataT fill) : theInteger(size), isNegative(negative) { @@ -65,6 +76,85 @@ namespace BigMath return theInteger; } + std::vector ReleaseInteger() + { + std::vector r = std::move(theInteger); + theInteger = {0}; + isNegative = false; + return r; + } + + std::vector ToByteArray(bool bigEndian = true) const + { + if (Zero()) + { + return {}; + } + + constexpr SizeT limbBytes = LimbBits / 8; + std::vector bytes; + bytes.reserve(theInteger.size() * limbBytes); + + for (DataT val : theInteger) + { + for (SizeT b = 0; b < limbBytes; ++b) + { + bytes.push_back(static_cast((val >> (b * 8)) & 0xFF)); + } + } + + while (!bytes.empty() && bytes.back() == 0) + { + bytes.pop_back(); + } + + if (bigEndian) + { + std::reverse(bytes.begin(), bytes.end()); + } + + return bytes; + } + + static BigInteger FromByteArray(std::span bytes, bool negative, bool bigEndian = true) + { + if (bytes.empty()) + { + return BigInteger(); + } + + constexpr SizeT limbBytes = LimbBits / 8; + const size_t numLimbs = (bytes.size() + limbBytes - 1) / limbBytes; + std::vector limbs(numLimbs, 0); + + auto getByte = [&](size_t idx) -> uint8_t { + if (bigEndian) + { + return bytes[bytes.size() - 1 - idx]; + } + else + { + return bytes[idx]; + } + }; + + for (size_t i = 0; i < numLimbs; ++i) + { + DataT limbVal = 0; + for (size_t b = 0; b < limbBytes; ++b) + { + size_t byteIdx = i * limbBytes + b; + if (byteIdx < bytes.size()) + { + limbVal |= (static_cast(getByte(byteIdx)) << (b * 8)); + } + } + limbs[i] = limbVal; + } + + return BigInteger(std::move(limbs), negative); + } + DataT operator[](const SizeT i) const { return theInteger[i]; diff --git a/tests/unit/test_construction.cpp b/tests/unit/test_construction.cpp index 92fe585..3ee1a24 100644 --- a/tests/unit/test_construction.cpp +++ b/tests/unit/test_construction.cpp @@ -198,3 +198,78 @@ REGISTER_TEST(Builder, VectorFromULong) ASSERT_EQ(v[1], 1u); } #endif + +REGISTER_TEST(Construction, MoveLimbVector) +{ + std::vector limbs = {12345, 67890}; + BigInteger x(std::move(limbs), true); + ASSERT_TRUE(x.IsNegative()); + ASSERT_EQ(x.size(), 2u); + ASSERT_EQ(x[0], 12345u); + ASSERT_EQ(x[1], 67890u); + ASSERT_TRUE(limbs.empty()); // Check that vector was moved from +} + +REGISTER_TEST(Construction, ReleaseInteger) +{ + BigInteger x(std::vector{100, 200}, true); + std::vector limbs = x.ReleaseInteger(); + ASSERT_EQ(limbs.size(), 2u); + ASSERT_EQ(limbs[0], 100u); + ASSERT_EQ(limbs[1], 200u); + + // x should be left in a valid positive zero state + ASSERT_TRUE(x.Zero()); + ASSERT_FALSE(x.IsNegative()); + ASSERT_EQ(x.size(), 1u); + ASSERT_EQ(x[0], 0u); +} + +REGISTER_TEST(Serialization, ByteArrays) +{ + // Test value 0 + { + BigInteger z; + auto bytes = z.ToByteArray(true); + ASSERT_TRUE(bytes.empty()); + + BigInteger z2 = BigInteger::FromByteArray(bytes, false, true); + ASSERT_TRUE(z2.Zero()); + ASSERT_FALSE(z2.IsNegative()); + } + + // Test standard value, big-endian and little-endian + { + // 0x1234567890abcdef + ULong val = 0x1234567890abcdefULL; + BigInteger x = BigIntegerBuilder::From(val); + + // Big Endian bytes + std::vector expectedBE = { + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef + }; + auto bytesBE = x.ToByteArray(true); + ASSERT_EQ(bytesBE.size(), expectedBE.size()); + for (size_t i = 0; i < bytesBE.size(); ++i) { + ASSERT_EQ(bytesBE[i], expectedBE[i]); + } + + BigInteger x2 = BigInteger::FromByteArray(bytesBE, true, true); + ASSERT_TRUE(x2.IsNegative()); + ASSERT_EQ(x2, -x); + + // Little Endian bytes + std::vector expectedLE = { + 0xef, 0xcd, 0xab, 0x90, 0x78, 0x56, 0x34, 0x12 + }; + auto bytesLE = x.ToByteArray(false); + ASSERT_EQ(bytesLE.size(), expectedLE.size()); + for (size_t i = 0; i < bytesLE.size(); ++i) { + ASSERT_EQ(bytesLE[i], expectedLE[i]); + } + + BigInteger x3 = BigInteger::FromByteArray(bytesLE, false, false); + ASSERT_FALSE(x3.IsNegative()); + ASSERT_EQ(x3, x); + } +} From aec256c4d4bdcf041584148bc6318fb6e64d9512 Mon Sep 17 00:00:00 2001 From: Max Murshed Date: Fri, 12 Jun 2026 15:32:37 -0700 Subject: [PATCH 2/4] Fix compiler warnings and sign comparison mismatches --- .../multiplication/ClassicMultiplication.h | 12 +++--- .../algorithms/multiplication/NTTCore.h | 3 +- include/biginteger/common/Util.h | 2 +- src/algorithms/Addition.cpp | 41 ++++++++++--------- src/algorithms/Subtraction.cpp | 33 ++++++++------- 5 files changed, 48 insertions(+), 43 deletions(-) diff --git a/include/biginteger/algorithms/multiplication/ClassicMultiplication.h b/include/biginteger/algorithms/multiplication/ClassicMultiplication.h index d8fffbc..ab1543a 100644 --- a/include/biginteger/algorithms/multiplication/ClassicMultiplication.h +++ b/include/biginteger/algorithms/multiplication/ClassicMultiplication.h @@ -117,7 +117,7 @@ namespace BigMath ULong carry = 0; - for (Int j = aStart; j <= aEnd; j++) + for (SizeT j = aStart; j <= aEnd; j++) { ULong multiply = a[j]; multiply *= b; @@ -127,7 +127,7 @@ namespace BigMath carry = NextCarry(multiply, base); } - Int j = aEnd + 1; + SizeT j = aEnd + 1; while (carry > 0) { SetOrPush(a, j, LowDigit(carry, base)); @@ -240,7 +240,7 @@ namespace BigMath ULong carry = 0; - for (Int j = 0; j < len; j++) + for (SizeT j = 0; j < len; j++) { ULong multiply = 0; SizeT aPos = aStart + j; @@ -341,13 +341,13 @@ namespace BigMath SizeT jStart = rStart + (i - bStart); for (SizeT j = aStart; j <= aEnd; j++) { - SizeT k = jStart + (j - aStart); + SizeT kk = jStart + (j - aStart); ULong multiply = a[j]; multiply *= b[i]; - multiply += result[k]; + multiply += result[kk]; multiply += carry; - result[k] = LowDigit(multiply, base); + result[kk] = LowDigit(multiply, base); carry = NextCarry(multiply, base); } k = jStart + lenA; diff --git a/include/biginteger/algorithms/multiplication/NTTCore.h b/include/biginteger/algorithms/multiplication/NTTCore.h index 7a8de76..7541ae6 100644 --- a/include/biginteger/algorithms/multiplication/NTTCore.h +++ b/include/biginteger/algorithms/multiplication/NTTCore.h @@ -377,8 +377,7 @@ namespace BigMath Int stride = n / outerLen; Int stride4 = stride << 2; Int numBlocks = n / outerLen; - Int omega4_off = n / 4; - auto body = [a, halflen, qlen4, qlen8, outerLen, stride, stride4, omega4_off, roots](Int bStart, Int bEnd) { + auto body = [a, halflen, qlen4, qlen8, outerLen, stride, stride4, roots](Int bStart, Int bEnd) { for (Int b = bStart; b < bEnd; ++b) { Int i = b * outerLen; diff --git a/include/biginteger/common/Util.h b/include/biginteger/common/Util.h index 8725af3..af3b99e 100644 --- a/include/biginteger/common/Util.h +++ b/include/biginteger/common/Util.h @@ -61,7 +61,7 @@ namespace BigMath return std::vector{0}; } - inline SizeT FindNonZeroByte(std::vector const &a, Int start = 0, Int end = -1) + inline Int FindNonZeroByte(std::vector const &a, Int start = 0, Int end = -1) { Int i = (end == -1 ? (Int)a.size() : end + 1); while (i > start && a[i - 1] == 0) diff --git a/src/algorithms/Addition.cpp b/src/algorithms/Addition.cpp index 71e8074..7534b9b 100644 --- a/src/algorithms/Addition.cpp +++ b/src/algorithms/Addition.cpp @@ -95,30 +95,33 @@ namespace BigMath aEnd = std::min(aEnd, (SizeT)(a.size() - 1)); bEnd = std::min(bEnd, (SizeT)(b.size() - 1)); - Int size = std::max(Len(aStart, aEnd), Len(bStart, bEnd)); + Int len = std::max(Len(aStart, aEnd), Len(bStart, bEnd)); + if (len <= 0) + return; + SizeT size = (SizeT)len; if (base == Base2_64) { ULong128 carry = 0; - for (Int i = 0; i < size; i++) + for (SizeT i = 0; i < size; i++) { ULong128 digitOps = carry; - Int aPos = i + aStart; - if (aPos <= aEnd && aPos < (Int)a.size()) + SizeT aPos = i + aStart; + if (aPos <= aEnd && aPos < a.size()) digitOps += a[aPos]; - Int bPos = i + bStart; - if (bPos <= bEnd && bPos < (Int)b.size()) + SizeT bPos = i + bStart; + if (bPos <= bEnd && bPos < b.size()) digitOps += b[bPos]; - Int rPos = rStart + i; - if (rPos < (Int)result.size()) + SizeT rPos = rStart + i; + if (rPos < result.size()) result[rPos] = (DataT)(digitOps & 0xFFFFFFFFFFFFFFFFULL); carry = digitOps >> 64; } // Propagate the final carry: a bare += can itself overflow the slot. // (If the result window ends here the carry is dropped — this is a // fixed-width window primitive; whole-vector AddTo grows `a` first.) - Int rPos = rStart + size; - while (carry > 0 && rPos < (Int)result.size()) + SizeT rPos = rStart + size; + while (carry > 0 && rPos < result.size()) { carry += result[rPos]; result[rPos] = (DataT)(carry & 0xFFFFFFFFFFFFFFFFULL); @@ -129,28 +132,28 @@ namespace BigMath } Long carry = 0; - for (Int i = 0; i < size; i++) + for (SizeT i = 0; i < size; i++) { Long digitOps = 0; - Int aPos = i + aStart; - if (aPos <= aEnd && aPos < (Int)a.size()) + SizeT aPos = i + aStart; + if (aPos <= aEnd && aPos < a.size()) digitOps = a[aPos]; digitOps += carry; - Int bPos = i + bStart; - if (bPos <= bEnd && bPos < (Int)b.size()) + SizeT bPos = i + bStart; + if (bPos <= bEnd && bPos < b.size()) digitOps += b[bPos]; - Int rPos = rStart + i; - if (rPos < (Int)result.size()) + SizeT rPos = rStart + i; + if (rPos < result.size()) result[rPos] = (DataT)(digitOps % base); carry = digitOps / base; } // Propagate the final carry (see the Base2_64 branch above). - Int rPos = rStart + size; - while (carry > 0 && rPos < (Int)result.size()) + SizeT rPos = rStart + size; + while (carry > 0 && rPos < result.size()) { Long digitOps = (Long)result[rPos] + carry; result[rPos] = (DataT)(digitOps % base); diff --git a/src/algorithms/Subtraction.cpp b/src/algorithms/Subtraction.cpp index d084d9f..ceb2dcb 100644 --- a/src/algorithms/Subtraction.cpp +++ b/src/algorithms/Subtraction.cpp @@ -72,7 +72,10 @@ namespace BigMath aEnd = std::min(aEnd, (SizeT)(a.size() - 1)); bEnd = std::min(bEnd, (SizeT)(b.size() - 1)); - Int size = std::max(Len(aStart, aEnd), Len(bStart, bEnd)); + Int len = std::max(Len(aStart, aEnd), Len(bStart, bEnd)); + if (len <= 0) + return; + SizeT size = (SizeT)len; if (base == Base2_64) { @@ -80,16 +83,16 @@ namespace BigMath // can't hold a 64-bit limb, so do unsigned arithmetic and detect borrow // via comparison. ULong borrow = 0; - for (Int i = 0; i < size; i++) + for (SizeT i = 0; i < size; i++) { ULong ai = 0; - Int aPos = aStart + i; - if (aPos <= aEnd && aPos < (Int)a.size()) + SizeT aPos = aStart + i; + if (aPos <= aEnd && aPos < a.size()) ai = a[aPos]; ULong bi = 0; - Int bPos = bStart + i; - if (bPos <= bEnd && bPos < (Int)b.size()) + SizeT bPos = bStart + i; + if (bPos <= bEnd && bPos < b.size()) bi = b[bPos]; // Compute ai - bi - borrow with two-step borrow detection. @@ -99,26 +102,26 @@ namespace BigMath ULong borrow2 = (t1 < bi) ? 1 : 0; borrow = borrow1 + borrow2; - Int rPos = rStart + i; - if (rPos < (Int)result.size()) + SizeT rPos = rStart + i; + if (rPos < result.size()) result[rPos] = (DataT)diff; } return; } Long carry = 0; - for (Int i = 0; i < size; i++) + for (SizeT i = 0; i < size; i++) { Long digitOps = 0; - Int aPos = aStart + i; - if (aPos <= aEnd && aPos < (Int)a.size()) + SizeT aPos = aStart + i; + if (aPos <= aEnd && aPos < a.size()) digitOps = a[aPos]; digitOps -= carry; - Int bPos = bStart + i; - if (bPos <= bEnd && bPos < (Int)b.size()) + SizeT bPos = bStart + i; + if (bPos <= bEnd && bPos < b.size()) digitOps -= b[bPos]; carry = 0; @@ -128,8 +131,8 @@ namespace BigMath carry = 1; } - Int rPos = rStart + i; - if (rPos < (Int)result.size()) + SizeT rPos = rStart + i; + if (rPos < result.size()) result[rPos] = (DataT)digitOps; } } From bc92f517515fba2bae9d2a7b9f907b1822224404 Mon Sep 17 00:00:00 2001 From: Max Murshed Date: Fri, 12 Jun 2026 15:37:59 -0700 Subject: [PATCH 3/4] Fix 64-bit limb formatting bug in calculator REPL --- calculator/calculator.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/calculator/calculator.cpp b/calculator/calculator.cpp index 6b6a0a0..564f653 100755 --- a/calculator/calculator.cpp +++ b/calculator/calculator.cpp @@ -57,12 +57,14 @@ namespace auto const &v = x.GetInteger(); int hi = (int)v.size() - 1; while (hi > 0 && v[hi] == 0) --hi; - char buf[16]; - std::snprintf(buf, sizeof(buf), "%x", (unsigned)v[hi]); + char buf[32]; + std::snprintf(buf, sizeof(buf), "%llx", (unsigned long long)v[hi]); out += buf; + + const char *fmt = (BigInteger::Base() == Base2_64) ? "%016llx" : "%08llx"; for (int i = hi - 1; i >= 0; --i) { - std::snprintf(buf, sizeof(buf), "%08x", (unsigned)v[i]); + std::snprintf(buf, sizeof(buf), fmt, (unsigned long long)v[i]); out += buf; } return out; @@ -77,19 +79,21 @@ namespace int hi = (int)v.size() - 1; while (hi > 0 && v[hi] == 0) --hi; - auto pushBits = [&](uint32_t w, int bits) { + auto pushBits = [&](ULong w, int bits) { for (int i = bits - 1; i >= 0; --i) - out.push_back(((w >> i) & 1u) ? '1' : '0'); + out.push_back(((w >> i) & 1ULL) ? '1' : '0'); }; + const int limbBits = (BigInteger::Base() == Base2_64) ? 64 : 32; + // Top limb: strip leading zero bits (but keep at least one). - uint32_t top = (uint32_t)v[hi]; - int topBits = 32; - while (topBits > 1 && ((top >> (topBits - 1)) & 1u) == 0) + ULong top = v[hi]; + int topBits = limbBits; + while (topBits > 1 && ((top >> (topBits - 1)) & 1ULL) == 0) --topBits; pushBits(top, topBits); for (int i = hi - 1; i >= 0; --i) - pushBits((uint32_t)v[i], 32); + pushBits(v[i], limbBits); return out; } From bfa3bb071f10bc780f671ac0abdfb4171a2a6439 Mon Sep 17 00:00:00 2001 From: Max Murshed Date: Fri, 12 Jun 2026 15:47:52 -0700 Subject: [PATCH 4/4] Address review: ByteOrder enum, ctor precondition doc, serialization test coverage - Replace bool bigEndian parameter with BigInteger::ByteOrder enum for unambiguous call sites next to FromByteArray's negative flag - Document zero-copy limb ctor precondition (limbs must be canonical for the current base; no validation performed) - Drop assertion on moved-from vector state (unspecified per standard) - Add serialization tests: odd-length inputs (5/9 bytes, partial top limb), 17-byte multi-limb round trip with both endiannesses, leading/trailing zero-byte inputs, canonical (trimmed) output Co-Authored-By: Claude Fable 5 --- include/biginteger/BigInteger.h | 20 ++++-- tests/unit/test_construction.cpp | 112 +++++++++++++++++++++++++++---- 2 files changed, 116 insertions(+), 16 deletions(-) diff --git a/include/biginteger/BigInteger.h b/include/biginteger/BigInteger.h index 067b6af..088bfa6 100644 --- a/include/biginteger/BigInteger.h +++ b/include/biginteger/BigInteger.h @@ -42,6 +42,10 @@ namespace BigMath } } + // Zero-copy adoption of a raw limb vector (little-endian limb order). + // Precondition: every limb must be canonical for the current base — + // < 2^32 when built with BIGMATH_LIMB_64=0. No validation is performed; + // non-canonical limbs silently corrupt downstream arithmetic. BigInteger(std::vector&& aInt, bool negative) : theInteger(std::move(aInt)), isNegative(negative) { TrimZerosToOne(theInteger); @@ -84,7 +88,15 @@ namespace BigMath return r; } - std::vector ToByteArray(bool bigEndian = true) const + // Byte order for magnitude serialization. Named enum instead of a bool so + // call sites read unambiguously next to FromByteArray's `negative` flag. + enum class ByteOrder + { + BigEndian, + LittleEndian + }; + + std::vector ToByteArray(ByteOrder order = ByteOrder::BigEndian) const { if (Zero()) { @@ -108,7 +120,7 @@ namespace BigMath bytes.pop_back(); } - if (bigEndian) + if (order == ByteOrder::BigEndian) { std::reverse(bytes.begin(), bytes.end()); } @@ -116,7 +128,7 @@ namespace BigMath return bytes; } - static BigInteger FromByteArray(std::span bytes, bool negative, bool bigEndian = true) + static BigInteger FromByteArray(std::span bytes, bool negative, ByteOrder order = ByteOrder::BigEndian) { if (bytes.empty()) { @@ -128,7 +140,7 @@ namespace BigMath std::vector limbs(numLimbs, 0); auto getByte = [&](size_t idx) -> uint8_t { - if (bigEndian) + if (order == ByteOrder::BigEndian) { return bytes[bytes.size() - 1 - idx]; } diff --git a/tests/unit/test_construction.cpp b/tests/unit/test_construction.cpp index 3ee1a24..9a9fa3a 100644 --- a/tests/unit/test_construction.cpp +++ b/tests/unit/test_construction.cpp @@ -207,7 +207,8 @@ REGISTER_TEST(Construction, MoveLimbVector) ASSERT_EQ(x.size(), 2u); ASSERT_EQ(x[0], 12345u); ASSERT_EQ(x[1], 67890u); - ASSERT_TRUE(limbs.empty()); // Check that vector was moved from + // No assertion on `limbs` here: a moved-from vector is in a valid but + // unspecified state per the standard. } REGISTER_TEST(Construction, ReleaseInteger) @@ -227,13 +228,15 @@ REGISTER_TEST(Construction, ReleaseInteger) REGISTER_TEST(Serialization, ByteArrays) { + using ByteOrder = BigInteger::ByteOrder; + // Test value 0 { BigInteger z; - auto bytes = z.ToByteArray(true); + auto bytes = z.ToByteArray(ByteOrder::BigEndian); ASSERT_TRUE(bytes.empty()); - - BigInteger z2 = BigInteger::FromByteArray(bytes, false, true); + + BigInteger z2 = BigInteger::FromByteArray(bytes, false, ByteOrder::BigEndian); ASSERT_TRUE(z2.Zero()); ASSERT_FALSE(z2.IsNegative()); } @@ -243,33 +246,118 @@ REGISTER_TEST(Serialization, ByteArrays) // 0x1234567890abcdef ULong val = 0x1234567890abcdefULL; BigInteger x = BigIntegerBuilder::From(val); - + // Big Endian bytes std::vector expectedBE = { 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef }; - auto bytesBE = x.ToByteArray(true); + auto bytesBE = x.ToByteArray(ByteOrder::BigEndian); ASSERT_EQ(bytesBE.size(), expectedBE.size()); for (size_t i = 0; i < bytesBE.size(); ++i) { ASSERT_EQ(bytesBE[i], expectedBE[i]); } - - BigInteger x2 = BigInteger::FromByteArray(bytesBE, true, true); + + BigInteger x2 = BigInteger::FromByteArray(bytesBE, true, ByteOrder::BigEndian); ASSERT_TRUE(x2.IsNegative()); ASSERT_EQ(x2, -x); - + // Little Endian bytes std::vector expectedLE = { 0xef, 0xcd, 0xab, 0x90, 0x78, 0x56, 0x34, 0x12 }; - auto bytesLE = x.ToByteArray(false); + auto bytesLE = x.ToByteArray(ByteOrder::LittleEndian); ASSERT_EQ(bytesLE.size(), expectedLE.size()); for (size_t i = 0; i < bytesLE.size(); ++i) { ASSERT_EQ(bytesLE[i], expectedLE[i]); } - - BigInteger x3 = BigInteger::FromByteArray(bytesLE, false, false); + + BigInteger x3 = BigInteger::FromByteArray(bytesLE, false, ByteOrder::LittleEndian); ASSERT_FALSE(x3.IsNegative()); ASSERT_EQ(x3, x); } } + +REGISTER_TEST(Serialization, OddLengthByteArrays) +{ + using ByteOrder = BigInteger::ByteOrder; + + // 5 bytes: partial top limb in both limb configurations. + { + std::vector be = {0x01, 0x02, 0x03, 0x04, 0x05}; + BigInteger x = BigInteger::FromByteArray(be, false); + ASSERT_EQ(x, BigIntegerBuilder::From(0x0102030405ULL)); + + auto rt = x.ToByteArray(); + ASSERT_EQ(rt.size(), be.size()); + for (size_t i = 0; i < rt.size(); ++i) + ASSERT_EQ(rt[i], be[i]); + } + + // 9 bytes: partial top limb above a full 64-bit limb. + { + std::vector be = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; + BigInteger x = BigInteger::FromByteArray(be, false); + // 0x010203040506070809 + ASSERT_EQ(ToString(x), std::string("18591708106338011145")); + + auto rt = x.ToByteArray(); + ASSERT_EQ(rt.size(), be.size()); + for (size_t i = 0; i < rt.size(); ++i) + ASSERT_EQ(rt[i], be[i]); + + BigInteger x2 = BigInteger::FromByteArray(x.ToByteArray(ByteOrder::LittleEndian), false, ByteOrder::LittleEndian); + ASSERT_EQ(x2, x); + } +} + +REGISTER_TEST(Serialization, MultiLimbRoundTrip) +{ + using ByteOrder = BigInteger::ByteOrder; + + // 17 bytes: 3 limbs at 64-bit limbs, 5 limbs at 32-bit limbs; the top limb + // is partial in both configurations. + std::vector be; + for (uint8_t v = 1; v <= 17; ++v) + be.push_back(v); + + BigInteger x = BigInteger::FromByteArray(be, false); + // 0x0102030405060708090a0b0c0d0e0f1011 + ASSERT_EQ(ToString(x), std::string("342956481330728537355412814650493833233")); + ASSERT_EQ(x.size(), (be.size() + LimbBits / 8 - 1) / (LimbBits / 8)); + + auto rtBE = x.ToByteArray(ByteOrder::BigEndian); + ASSERT_EQ(rtBE.size(), be.size()); + for (size_t i = 0; i < rtBE.size(); ++i) + ASSERT_EQ(rtBE[i], be[i]); + + auto le = x.ToByteArray(ByteOrder::LittleEndian); + ASSERT_EQ(le.size(), be.size()); + for (size_t i = 0; i < le.size(); ++i) + ASSERT_EQ(le[i], be[be.size() - 1 - i]); + + BigInteger x2 = BigInteger::FromByteArray(le, false, ByteOrder::LittleEndian); + ASSERT_EQ(x2, x); +} + +REGISTER_TEST(Serialization, LeadingZeroBytes) +{ + using ByteOrder = BigInteger::ByteOrder; + + BigInteger expected = BigIntegerBuilder::From(0x0102ULL); + + // Big-endian input with leading zero bytes parses to the same value. + std::vector paddedBE = {0x00, 0x00, 0x00, 0x01, 0x02}; + BigInteger x = BigInteger::FromByteArray(paddedBE, false, ByteOrder::BigEndian); + ASSERT_EQ(x, expected); + + // Little-endian input with trailing zero bytes parses to the same value. + std::vector paddedLE = {0x02, 0x01, 0x00, 0x00, 0x00}; + BigInteger y = BigInteger::FromByteArray(paddedLE, false, ByteOrder::LittleEndian); + ASSERT_EQ(y, expected); + + // Serialization is canonical: zero padding never round-trips. + auto bytes = x.ToByteArray(ByteOrder::BigEndian); + ASSERT_EQ(bytes.size(), 2u); + ASSERT_EQ(bytes[0], 0x01u); + ASSERT_EQ(bytes[1], 0x02u); +}