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 ]