base58

Pure Haskell base58, base58check encoding/decoding (docs.ppad.tech/base58).
git clone git://git.ppad.tech/base58.git
Log | Files | Refs | README | LICENSE

commit 9778ead4667bd6290f3c117eb7a523038d23a396
parent b36e36e9dfbd793dec2908b3640394b8f47860db
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 15:24:32 +0400

test: decode vectors, invalid inputs, labels

Diffstat:
Mtest/Main.hs | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
1 file changed, 99 insertions(+), 20 deletions(-)

diff --git a/test/Main.hs b/test/Main.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -6,7 +5,9 @@ module Main where import Data.Aeson ((.:)) import qualified Data.Aeson as A +import qualified Data.Aeson.Types as AT import qualified Data.ByteString as BS +import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Base16 as B16 import qualified Data.ByteString.Base58 as B58 import qualified Data.ByteString.Base58Check as B58Check @@ -21,15 +22,15 @@ data Valid_Base58Check = Valid_Base58Check { , vc_payload :: !BS.ByteString } deriving Show -decodeLenient :: BS.ByteString -> BS.ByteString -decodeLenient bs = case B16.decode bs of - Nothing -> error "boom" - Just v -> v +hex :: BS.ByteString -> AT.Parser BS.ByteString +hex bs = case B16.decode bs of + Nothing -> fail "invalid hex" + Just v -> pure v instance A.FromJSON Valid_Base58Check where parseJSON = A.withObject "Valid_Base58Check" $ \m -> Valid_Base58Check <$> fmap TE.encodeUtf8 (m .: "string") - <*> fmap (decodeLenient . TE.encodeUtf8) (m .: "payload") + <*> (hex . TE.encodeUtf8 =<< (m .: "payload")) data Invalid_Base58Check = Invalid_Base58Check { ic_string :: !BS.ByteString @@ -56,16 +57,16 @@ execute_base58check Base58Check {..} = testGroup "base58check" [ , testGroup "invalid" (fmap execute_invalid b58c_invalid) ] where - execute_valid Valid_Base58Check {..} = testCase "valid" $ do -- label - let enc = B58Check.encode vc_payload - assertEqual mempty enc vc_string + execute_valid Valid_Base58Check {..} = testGroup (label vc_string) [ + testCase "encode" $ + assertEqual mempty vc_string (B58Check.encode vc_payload) + , testCase "decode" $ + assertEqual mempty (Just vc_payload) (B58Check.decode vc_string) + ] - execute_invalid Invalid_Base58Check {..} = testCase "invalid" $ do -- label - let dec = B58Check.decode ic_string - is_just = \case - Nothing -> False - Just _ -> True - assertBool mempty (not (is_just dec)) + execute_invalid Invalid_Base58Check {..} = + testCase (label ic_string) $ + assertEqual mempty Nothing (B58Check.decode ic_string) data Valid_Base58 = Valid_Base58 { vb_decodedHex :: !BS.ByteString @@ -74,13 +75,59 @@ data Valid_Base58 = Valid_Base58 { instance A.FromJSON Valid_Base58 where parseJSON = A.withObject "Valid_Base58" $ \m -> Valid_Base58 - <$> fmap (decodeLenient . TE.encodeUtf8) (m .: "decodedHex") + <$> (hex . TE.encodeUtf8 =<< (m .: "decodedHex")) <*> fmap TE.encodeUtf8 (m .: "encoded") -execute_base58 :: Valid_Base58 -> TestTree -- XX label -execute_base58 Valid_Base58 {..} = testCase "base58" $ do - let enc = B58.encode vb_decodedHex - assertEqual mempty enc vb_encoded +execute_base58 :: Valid_Base58 -> TestTree +execute_base58 Valid_Base58 {..} = testGroup (label vb_encoded) [ + testCase "encode" $ + assertEqual mempty vb_encoded (B58.encode vb_decodedHex) + , testCase "decode" $ + assertEqual mempty (Just vb_decodedHex) (B58.decode vb_encoded) + ] + +-- test label for an encoded string +label :: BS.ByteString -> String +label bs + | BS.null bs = "(empty)" + | otherwise = show (B8.unpack bs) + +-- strings containing non-base58 characters +invalid_base58 :: [BS.ByteString] +invalid_base58 = [ + "0" + , "O" + , "I" + , "l" + , " " + , "\NUL" + , "+" + , "/" + , "\xff" + , "\xc3\xa9" + , "StV1DL0CwTryKyV" + , "StV1DLOCwTryKyV" + , "StV1DLICwTryKyV" + , "StV1DLlCwTryKyV" + , " StV1DL6CwTryKyV" + , "StV1DL6CwTryKyV " + , "StV1DL6CwTryKyV\n" + , "11110" + , "111l" + , "11 1" + ] + +execute_invalid_base58 :: BS.ByteString -> TestTree +execute_invalid_base58 bs = testCase (label bs) $ + assertEqual mempty Nothing (B58.decode bs) + +-- strings that base58-decode to fewer than four bytes +short_base58check :: [BS.ByteString] +short_base58check = ["", "1", "11", "111", "2g", "a3gV"] + +execute_short_base58check :: BS.ByteString -> TestTree +execute_short_base58check bs = testCase (label bs) $ + assertEqual mempty Nothing (B58Check.decode bs) newtype BS = BS BS.ByteString deriving (Eq, Show) @@ -96,6 +143,32 @@ instance Q.Arbitrary BS where b <- bytes 1024 pure (BS b) +-- arbitrary strings over the base58 alphabet, plus some invalid +-- characters +newtype B58Str = B58Str BS.ByteString + deriving (Eq, Show) + +instance Q.Arbitrary B58Str where + arbitrary = do + l <- Q.chooseInt (0, 64) + v <- Q.vectorOf l $ Q.frequency [ + (16, Q.elements alphabet) + , (1, Q.elements (BS.unpack "0OIl +/\NUL\xff")) + ] + pure (B58Str (BS.pack v)) + where + alphabet = BS.unpack + "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + +-- decoding succeeds exactly on strings over the alphabet, and every +-- such string is canonical +base58_decode_canonical :: B58Str -> Bool +base58_decode_canonical (B58Str bs) = case B58.decode bs of + Nothing -> BS.any (`BS.notElem` alphabet) bs + Just d -> B58.encode d == bs + where + alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + base58_decode_inverts_encode :: BS -> Bool base58_decode_inverts_encode (BS bs) = case B58.decode (B58.encode bs) of Nothing -> False @@ -120,11 +193,17 @@ main = do Just (b58, b58c) -> defaultMain $ testGroup "ppad-base58" [ testGroup "unit tests" [ testGroup "base58" (fmap execute_base58 b58) + , testGroup "base58 (invalid)" + (fmap execute_invalid_base58 invalid_base58) , execute_base58check b58c + , testGroup "base58check (short)" + (fmap execute_short_base58check short_base58check) ] , testGroup "property tests" [ Q.testProperty "(base58) decode . encode ~ id" $ Q.withMaxSuccess 250 base58_decode_inverts_encode + , Q.testProperty "(base58) decode succeeds iff canonical" $ + Q.withMaxSuccess 1000 base58_decode_canonical , Q.testProperty "(base58check) decode . encode ~ id" $ Q.withMaxSuccess 250 base58check_decode_inverts_encode ]