ppad-base58 0.2.3 → 0.2.4
raw patch · 5 files changed
+121/−55 lines, 5 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG +5/−0
- lib/Data/ByteString/Base58.hs +13/−30
- lib/Data/ByteString/Base58Check.hs +2/−3
- ppad-base58.cabal +2/−2
- test/Main.hs +99/−20
CHANGELOG view
@@ -1,5 +1,10 @@ # Changelog +- 0.2.4 (2026-10-10)+ * Decoding now validates and converts input in a single pass, and is+ somewhat faster on longer inputs.+ * Expands the test suite with decoding vectors and invalid inputs.+ - 0.2.3 (2026-01-10) * Bumps the ppad-sha256 dependency.
lib/Data/ByteString/Base58.hs view
@@ -1,6 +1,5 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ViewPatterns #-} -- | -- Module: Data.ByteString.Base58@@ -15,7 +14,6 @@ , decode ) where -import Control.Monad (guard) import qualified Data.Bits as B import Data.Bits ((.|.)) import qualified Data.ByteString as BS@@ -56,37 +54,20 @@ -- Nothing decode :: BS.ByteString -> Maybe BS.ByteString decode bs = do- guard (verify_base58 bs)+ n <- roll_base58 bs let ls = leading_zeros bs- pure $ ls <> unroll_base256 (roll_base58 bs)--verify_base58 :: BS.ByteString -> Bool-verify_base58 bs = case BS.uncons bs of- Nothing -> True- Just (h, t)- | BS.elem h base58_charset -> verify_base58 t- | otherwise -> False+ pure $ ls <> unroll_base256 n base58_charset :: BS.ByteString base58_charset = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" -- produce leading ones from leading zeros leading_ones :: BS.ByteString -> BS.ByteString-leading_ones = go mempty where- go acc bs = case BS.uncons bs of- Nothing -> acc- Just (h, t)- | h == 0 -> go (BS.cons 0x31 acc) t- | otherwise -> acc+leading_ones bs = BS.replicate (BS.length (BS.takeWhile (== 0x00) bs)) 0x31 -- produce leading zeros from leading ones leading_zeros :: BS.ByteString -> BS.ByteString-leading_zeros = go mempty where- go acc bs = case BS.uncons bs of- Nothing -> acc- Just (h, t)- | h == 0x31 -> go (BS.cons 0x00 acc) t- | otherwise -> acc+leading_zeros bs = BS.replicate (BS.length (BS.takeWhile (== 0x31) bs)) 0x00 -- to base256 unroll_base256 :: Integer -> BS.ByteString@@ -111,11 +92,13 @@ let (b, c) = quotRem a 58 in (BU.unsafeIndex base58_charset (fi c), b) --- from base58-roll_base58 :: BS.ByteString -> Integer-roll_base58 bs = BS.foldl' alg 0 bs where- alg !b !a = case word6 a of- Just w -> b * 58 + fi w- Nothing ->- error "ppad-base58 (roll_base58): internal error"+-- from base58, failing on any non-base58 character+roll_base58 :: BS.ByteString -> Maybe Integer+roll_base58 bs = go 0 0 where+ l = BS.length bs+ go !acc !i+ | i == l = Just acc+ | otherwise = case word6 (BU.unsafeIndex bs i) of+ Nothing -> Nothing+ Just w -> go (acc * 58 + fi w) (i + 1)
lib/Data/ByteString/Base58Check.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE ViewPatterns #-}- -- | -- Module: Data.ByteString.Base58Check -- Copyright: (c) 2024 Jared Tobin@@ -42,7 +40,8 @@ decode mb = do bs <- B58.decode mb let len = BS.length bs- (pay, kek) = BS.splitAt (len - 4) bs+ guard (len >= 4)+ let (pay, kek) = BS.splitAt (len - 4) bs man = BS.take 4 (SHA256.hash (SHA256.hash pay)) guard (kek == man) pure pay
ppad-base58.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: ppad-base58-version: 0.2.3+version: 0.2.4 synopsis: base58 and base58check encoding/decoding. license: MIT license-file: LICENSE@@ -35,7 +35,7 @@ build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13- , ppad-sha256 > 0.3 && < 0.4+ , ppad-sha256 >= 0.3 && < 0.4 test-suite base58-tests type: exitcode-stdio-1.0
test/Main.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -6,7 +5,9 @@ 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 @@ , 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 @@ , 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,14 +75,60 @@ 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 @@ 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 @@ 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 ]