base16

Fast Haskell base16 encoding/decoding (docs.ppad.tech/base16).
git clone git://git.ppad.tech/base16.git
Log | Files | Refs | README | LICENSE

Main.hs (5568B)


      1 {-# OPTIONS_GHC -fno-warn-missing-signatures #-}
      2 {-# LANGUAGE OverloadedStrings #-}
      3 {-# LANGUAGE PackageImports #-}
      4 
      5 module Main where
      6 
      7 import Control.Monad (forM_)
      8 import qualified Data.ByteString as BS
      9 import qualified Data.ByteString.Char8 as B8
     10 import qualified "ppad-base16" Data.ByteString.Base16 as B16
     11 import qualified "ppad-base16" Data.ByteString.Base16.Pure as Pure
     12 import qualified "base16-bytestring" Data.ByteString.Base16 as R0
     13 import Data.Word (Word8)
     14 import Test.Tasty
     15 import qualified Test.Tasty.QuickCheck as Q
     16 import qualified Test.Tasty.HUnit as H
     17 
     18 -- generators -----------------------------------------------------------------
     19 
     20 -- random bytes, as a slice at a random offset into a larger buffer, so
     21 -- that inputs with a non-zero 'ByteString' offset are exercised
     22 bytes :: Int -> Q.Gen BS.ByteString
     23 bytes k = do
     24   l <- Q.chooseInt (0, k)
     25   o <- Q.chooseInt (0, 32)
     26   v <- Q.vectorOf (o + l) Q.arbitrary
     27   pure (BS.drop o (BS.pack v))
     28 
     29 newtype BS = BS BS.ByteString
     30   deriving (Eq, Show)
     31 
     32 instance Q.Arbitrary BS where
     33   arbitrary = do
     34     b <- bytes 1024
     35     pure (BS b)
     36 
     37 hex_chars :: BS.ByteString
     38 hex_chars = "0123456789abcdefABCDEF"
     39 
     40 -- mostly-hex strings of arbitrary (including odd) length: each byte is
     41 -- a hex char with high probability, otherwise an arbitrary byte
     42 newtype Hexish = Hexish BS.ByteString
     43   deriving (Eq, Show)
     44 
     45 instance Q.Arbitrary Hexish where
     46   arbitrary = do
     47     l <- Q.chooseInt (0, 256)
     48     o <- Q.chooseInt (0, 32)
     49     junk <- Q.elements [0, 1, 50 :: Int]
     50     v <- Q.vectorOf (o + l) $ do
     51       p <- Q.chooseInt (1, 100)
     52       if   p <= junk
     53       then Q.arbitrary
     54       else Q.elements (BS.unpack hex_chars)
     55     pure (Hexish (BS.drop o (BS.pack v)))
     56 
     57 -- decoders under test --------------------------------------------------------
     58 
     59 decoders :: [(String, BS.ByteString -> Maybe BS.ByteString)]
     60 decoders = [("public", B16.decode), ("pure", Pure.decode)]
     61 
     62 ref_decode :: BS.ByteString -> Maybe BS.ByteString
     63 ref_decode bs = case R0.decode bs of
     64   Left _  -> Nothing
     65   Right d -> Just d
     66 
     67 is_hex :: Word8 -> Bool
     68 is_hex c = BS.elem c hex_chars
     69 
     70 -- properties -----------------------------------------------------------------
     71 
     72 encode_matches_reference :: BS -> Bool
     73 encode_matches_reference (BS bs) =
     74   let r0 = R0.encode bs
     75   in  B16.encode bs == r0 && Pure.encode bs == r0
     76 
     77 decode_inverts_encode :: BS -> Bool
     78 decode_inverts_encode (BS bs) =
     79   let enc = B16.encode bs
     80   in  all (\(_, dec) -> dec enc == Just bs) decoders
     81 
     82 decode_matches_reference :: Hexish -> Bool
     83 decode_matches_reference (Hexish bs) =
     84   let r0 = ref_decode bs
     85   in  B16.decode bs == r0 && Pure.decode bs == r0
     86 
     87 -- unit tests -----------------------------------------------------------------
     88 
     89 case_handled :: TestTree
     90 case_handled = H.testCase "decodes uppercase and mixed-case hex" $
     91   forM_ decoders $ \(nam, dec) -> do
     92     let l = dec "deadbeef"
     93     H.assertEqual nam (Just "\xde\xad\xbe\xef") l
     94     H.assertEqual nam l (dec "DEADBEEF")
     95     H.assertEqual nam l (dec "DeAdBeEf")
     96 
     97 -- every byte value, at every position of a string spanning one full NEON
     98 -- block plus a scalar tail, is accepted iff it is a hex char
     99 every_byte_every_position :: TestTree
    100 every_byte_every_position =
    101   H.testCase "every byte at every position" $ do
    102     let base = B8.replicate 34 '0'
    103     forM_ [0 .. 255 :: Int] $ \c ->
    104       forM_ [0 .. BS.length base - 1] $ \i -> do
    105         let w   = fromIntegral c :: Word8
    106             inp = BS.take i base <> BS.singleton w <> BS.drop (i + 1) base
    107             pec = if is_hex w then ref_decode inp else Nothing
    108             msg = "byte " <> show c <> ", position " <> show i
    109         forM_ decoders $ \(nam, dec) ->
    110           H.assertEqual (nam <> ", " <> msg) pec (dec inp)
    111 
    112 -- for every length spanning several NEON blocks and every tail length,
    113 -- a single invalid byte at any position causes decoding to fail
    114 single_corruption :: TestTree
    115 single_corruption = H.testCase "single invalid byte anywhere" $ do
    116   let bad = BS.pack [0x00, 0x20, 0x7f, 0x80, 0xff] <> "/:@G`g"
    117   forM_ [1 .. 80 :: Int] $ \n -> do
    118     let raw = BS.pack (fmap fromIntegral [7 * j + 3 | j <- [1 .. n]])
    119         enc = upper_odd (B16.encode raw)
    120         upper_odd = BS.pack . zipWith up [0 :: Int ..] . BS.unpack
    121         up j c | odd j && c >= 0x61 = c - 0x20
    122                | otherwise          = c
    123     forM_ decoders $ \(nam, dec) ->
    124       H.assertEqual (nam <> ", intact, n = " <> show n) (Just raw) (dec enc)
    125     forM_ [0 .. BS.length enc - 1] $ \i ->
    126       forM_ (BS.unpack bad) $ \w -> do
    127         let inp = BS.take i enc <> BS.singleton w <> BS.drop (i + 1) enc
    128             msg = "n = " <> show n <> ", position " <> show i
    129                   <> ", byte " <> show w
    130         forM_ decoders $ \(nam, dec) ->
    131           H.assertEqual (nam <> ", " <> msg) Nothing (dec inp)
    132 
    133 odd_lengths :: TestTree
    134 odd_lengths = H.testCase "odd-length inputs" $
    135   forM_ [1, 3 .. 99 :: Int] $ \l ->
    136     forM_ decoders $ \(nam, dec) ->
    137       H.assertEqual (nam <> ", length " <> show l) Nothing
    138         (dec (B8.replicate l 'a'))
    139 
    140 main :: IO ()
    141 main = defaultMain $
    142   testGroup "ppad-base16" [
    143     testGroup "property tests" [
    144       Q.testProperty "encode matches reference" $
    145         Q.withMaxSuccess 5000 encode_matches_reference
    146     , Q.testProperty "decode . encode ~ id" $
    147         Q.withMaxSuccess 5000 decode_inverts_encode
    148     , Q.testProperty "decode matches reference" $
    149         Q.withMaxSuccess 5000 decode_matches_reference
    150     ]
    151   , testGroup "unit tests" [
    152       case_handled
    153     , every_byte_every_position
    154     , single_corruption
    155     , odd_lengths
    156     ]
    157   ]