base32 0.4 → 1.0
raw patch · 35 files changed
+4591/−1753 lines, 35 filesdep +primitivedep −random-bytestringdep ~basedep ~bytestringdep ~deepseqPVP ok
version bump matches the API change (PVP)
Dependencies added: primitive
Dependencies removed: random-bytestring
Dependency ranges changed: base, bytestring, deepseq, text, text-short
API changes (from Hackage documentation)
- Data.ByteString.Base32: isBase32 :: ByteString -> Bool
- Data.ByteString.Base32: isValidBase32 :: ByteString -> Bool
- Data.ByteString.Base32.Hex: isBase32Hex :: ByteString -> Bool
- Data.ByteString.Base32.Hex: isValidBase32Hex :: ByteString -> Bool
- Data.ByteString.Base32.Internal: decodeBase32_ :: Ptr Word8 -> ByteString -> IO (Either Text ByteString)
- Data.ByteString.Base32.Internal: encodeBase32NoPad_ :: Addr# -> ByteString -> ByteString
- Data.ByteString.Base32.Internal: encodeBase32_ :: Addr# -> ByteString -> ByteString
- Data.ByteString.Base32.Internal: validateBase32 :: ByteString -> ByteString -> Bool
- Data.ByteString.Base32.Internal: validateLastNPads :: Int -> ByteString -> IO (Either Text ByteString) -> Either Text ByteString
- Data.ByteString.Lazy.Base32: isBase32 :: ByteString -> Bool
- Data.ByteString.Lazy.Base32: isValidBase32 :: ByteString -> Bool
- Data.ByteString.Lazy.Base32.Hex: isBase32Hex :: ByteString -> Bool
- Data.ByteString.Lazy.Base32.Hex: isValidBase32Hex :: ByteString -> Bool
- Data.ByteString.Short.Base32: isBase32 :: ShortByteString -> Bool
- Data.ByteString.Short.Base32: isValidBase32 :: ShortByteString -> Bool
- Data.ByteString.Short.Base32.Hex: isBase32Hex :: ShortByteString -> Bool
- Data.ByteString.Short.Base32.Hex: isValidBase32Hex :: ShortByteString -> Bool
- Data.Text.Encoding.Base32: isBase32 :: Text -> Bool
- Data.Text.Encoding.Base32: isValidBase32 :: Text -> Bool
- Data.Text.Encoding.Base32.Hex: isBase32Hex :: Text -> Bool
- Data.Text.Encoding.Base32.Hex: isValidBase32Hex :: Text -> Bool
- Data.Text.Lazy.Encoding.Base32: isBase32 :: Text -> Bool
- Data.Text.Lazy.Encoding.Base32: isValidBase32 :: Text -> Bool
- Data.Text.Lazy.Encoding.Base32.Hex: isBase32Hex :: Text -> Bool
- Data.Text.Lazy.Encoding.Base32.Hex: isValidBase32Hex :: Text -> Bool
- Data.Text.Short.Encoding.Base32: isBase32 :: ShortText -> Bool
- Data.Text.Short.Encoding.Base32: isValidBase32 :: ShortText -> Bool
- Data.Text.Short.Encoding.Base32.Hex: isBase32Hex :: ShortText -> Bool
- Data.Text.Short.Encoding.Base32.Hex: isValidBase32Hex :: ShortText -> Bool
+ Data.Base32.Types: HexPadded :: Alphabet
+ Data.Base32.Types: HexUnpadded :: Alphabet
+ Data.Base32.Types: StdPadded :: Alphabet
+ Data.Base32.Types: StdUnpadded :: Alphabet
+ Data.Base32.Types: assertBase32 :: forall (alphabet :: Alphabet) a. a -> Base32 alphabet a
+ Data.Base32.Types: data Alphabet
+ Data.Base32.Types: data Base32 (alphabet :: Alphabet) a
+ Data.Base32.Types: extractBase32 :: Base32 alphabet a -> a
+ Data.Base32.Types: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Base32.Types.Base32 alphabet a)
+ Data.Base32.Types: instance GHC.Base.Applicative (Data.Base32.Types.Base32 alphabet)
+ Data.Base32.Types: instance GHC.Base.Functor (Data.Base32.Types.Base32 alphabet)
+ Data.Base32.Types: instance GHC.Base.Monad (Data.Base32.Types.Base32 alphabet)
+ Data.Base32.Types: instance GHC.Classes.Eq Data.Base32.Types.Alphabet
+ Data.Base32.Types: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.Base32.Types.Base32 alphabet a)
+ Data.Base32.Types: instance GHC.Classes.Ord Data.Base32.Types.Alphabet
+ Data.Base32.Types: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.Base32.Types.Base32 alphabet a)
+ Data.Base32.Types: instance GHC.Show.Show Data.Base32.Types.Alphabet
+ Data.Base32.Types: instance GHC.Show.Show a => GHC.Show.Show (Data.Base32.Types.Base32 alphabet a)
+ Data.Base32.Types: type family HexAlphabet (k :: Alphabet)
+ Data.ByteString.Base32: decodeBase32PaddedUntyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32: decodeBase32UnpaddedUntyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32: decodeBase32Untyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32: parseBase32Padded :: ByteString -> Either Text (Base32 'StdPadded ByteString)
+ Data.ByteString.Base32: parseBase32Unpadded :: ByteString -> Either Text (Base32 'StdUnpadded ByteString)
+ Data.ByteString.Base32.Hex: decodeBase32PaddedUntyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32.Hex: decodeBase32UnpaddedUntyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32.Hex: decodeBase32Untyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32.Hex: parseBase32Padded :: ByteString -> Either Text (Base32 'HexPadded ByteString)
+ Data.ByteString.Base32.Hex: parseBase32Unpadded :: ByteString -> Either Text (Base32 'HexUnpadded ByteString)
+ Data.ByteString.Base32.Internal.Head: decodeBase32Typed_ :: forall (alphabet :: Alphabet). Ptr Word8 -> Base32 alphabet ByteString -> ByteString
+ Data.ByteString.Base32.Internal.Head: decodeBase32_ :: Ptr Word8 -> ByteString -> IO (Either Text ByteString)
+ Data.ByteString.Base32.Internal.Head: encodeBase32NoPad_ :: Addr# -> ByteString -> ByteString
+ Data.ByteString.Base32.Internal.Head: encodeBase32_ :: Addr# -> ByteString -> ByteString
+ Data.ByteString.Base32.Internal.Loop: decodeLoop :: Addr# -> ForeignPtr Word8 -> Ptr Word8 -> Ptr Word8 -> Ptr Word8 -> IO (Either Text ByteString)
+ Data.ByteString.Base32.Internal.Loop: decodeLoopTyped :: Addr# -> Ptr Word8 -> Ptr Word8 -> Int -> IO ()
+ Data.ByteString.Base32.Internal.Loop: decodeQuantum :: Bool -> Int -> Word64 -> Word64 -> Word64 -> Word64 -> Word64 -> Word64 -> Word64 -> Word64 -> Either Text Int
+ Data.ByteString.Base32.Internal.Loop: innerLoop :: Addr# -> Ptr Word64 -> Ptr Word8 -> Ptr Word8 -> (Ptr Word8 -> Ptr Word8 -> IO ByteString) -> IO ByteString
+ Data.ByteString.Base32.Internal.Short: decodeShort :: Ptr Word8 -> ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Base32.Internal.Short: decodeShortTyped :: forall (alphabet :: Alphabet). Ptr Word8 -> Base32 alphabet ShortByteString -> ShortByteString
+ Data.ByteString.Base32.Internal.Short: encodeShort :: Addr# -> Bool -> ShortByteString -> ShortByteString
+ Data.ByteString.Base32.Internal.Tables: hexDecodeTable :: Ptr Word8
+ Data.ByteString.Base32.Internal.Tables: stdDecodeTable :: Ptr Word8
+ Data.ByteString.Base32.Internal.Tail: loopTail :: Addr# -> ForeignPtr Word8 -> Ptr Word8 -> Ptr Word8 -> Ptr Word8 -> Ptr Word8 -> IO ByteString
+ Data.ByteString.Base32.Internal.Tail: loopTailNoPad :: Addr# -> ForeignPtr Word8 -> Ptr Word8 -> Ptr Word8 -> Ptr Word8 -> Ptr Word8 -> IO ByteString
+ Data.ByteString.Base32.Internal.Utils: aix :: Word8 -> Addr# -> Word8
+ Data.ByteString.Base32.Internal.Utils: encodedLength :: Int -> Int
+ Data.ByteString.Base32.Internal.Utils: peekWord32BE :: Ptr Word32 -> IO Word32
+ Data.ByteString.Base32.Internal.Utils: peekWord64BE :: Ptr Word64 -> IO Word64
+ Data.ByteString.Base32.Internal.Utils: reChunkN :: Int -> [ByteString] -> [ByteString]
+ Data.ByteString.Base32.Internal.Utils: validateLastNPads :: Int -> ByteString -> IO (Either Text ByteString) -> Either Text ByteString
+ Data.ByteString.Base32.Internal.Utils: w32 :: Word8 -> Word32
+ Data.ByteString.Base32.Internal.Utils: w64 :: Word8 -> Word64
+ Data.ByteString.Base32.Internal.Utils: w64_32 :: Word32 -> Word64
+ Data.ByteString.Base32.Internal.Utils: writeNPlainPtrBytes :: Storable a => Int -> [a] -> Ptr a
+ Data.ByteString.Lazy.Base32: decodeBase32PaddedUntyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32: decodeBase32UnpaddedUntyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32: decodeBase32Untyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32: parseBase32Padded :: ByteString -> Either Text (Base32 'StdPadded ByteString)
+ Data.ByteString.Lazy.Base32: parseBase32Unpadded :: ByteString -> Either Text (Base32 'StdUnpadded ByteString)
+ Data.ByteString.Lazy.Base32.Hex: decodeBase32PaddedUntyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32.Hex: decodeBase32UnpaddedUntyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32.Hex: decodeBase32Untyped :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32.Hex: parseBase32Padded :: ByteString -> Either Text (Base32 'HexPadded ByteString)
+ Data.ByteString.Lazy.Base32.Hex: parseBase32Unpadded :: ByteString -> Either Text (Base32 'HexUnpadded ByteString)
+ Data.ByteString.Short.Base32: decodeBase32PaddedUntyped :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32: decodeBase32UnpaddedUntyped :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32: decodeBase32Untyped :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32: parseBase32Padded :: ShortByteString -> Either Text (Base32 'StdPadded ShortByteString)
+ Data.ByteString.Short.Base32: parseBase32Unpadded :: ShortByteString -> Either Text (Base32 'StdUnpadded ShortByteString)
+ Data.ByteString.Short.Base32.Hex: decodeBase32PaddedUntyped :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32.Hex: decodeBase32UnpaddedUntyped :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32.Hex: decodeBase32Untyped :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32.Hex: parseBase32Padded :: ShortByteString -> Either Text (Base32 'HexPadded ShortByteString)
+ Data.ByteString.Short.Base32.Hex: parseBase32Unpadded :: ShortByteString -> Either Text (Base32 'HexUnpadded ShortByteString)
+ Data.Text.Encoding.Base32: decodeBase32PaddedUntyped :: Text -> Either Text Text
+ Data.Text.Encoding.Base32: decodeBase32UnpaddedUntyped :: Text -> Either Text Text
+ Data.Text.Encoding.Base32: decodeBase32Untyped :: Text -> Either Text Text
+ Data.Text.Encoding.Base32: parseBase32Padded :: Text -> Either Text (Base32 'StdPadded Text)
+ Data.Text.Encoding.Base32: parseBase32Unpadded :: Text -> Either Text (Base32 'StdUnpadded Text)
+ Data.Text.Encoding.Base32.Hex: decodeBase32PaddedUntyped :: Text -> Either Text Text
+ Data.Text.Encoding.Base32.Hex: decodeBase32UnpaddedUntyped :: Text -> Either Text Text
+ Data.Text.Encoding.Base32.Hex: decodeBase32Untyped :: Text -> Either Text Text
+ Data.Text.Encoding.Base32.Hex: parseBase32Padded :: Text -> Either Text (Base32 'HexPadded Text)
+ Data.Text.Encoding.Base32.Hex: parseBase32Unpadded :: Text -> Either Text (Base32 'HexUnpadded Text)
+ Data.Text.Lazy.Encoding.Base32: decodeBase32PaddedUntyped :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32: decodeBase32UnpaddedUntyped :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32: decodeBase32Untyped :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32: parseBase32Padded :: Text -> Either Text (Base32 'StdPadded Text)
+ Data.Text.Lazy.Encoding.Base32: parseBase32Unpadded :: Text -> Either Text (Base32 'StdUnpadded Text)
+ Data.Text.Lazy.Encoding.Base32.Hex: decodeBase32PaddedUntyped :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32.Hex: decodeBase32UnpaddedUntyped :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32.Hex: decodeBase32Untyped :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32.Hex: parseBase32Padded :: Text -> Either Text (Base32 'HexPadded Text)
+ Data.Text.Lazy.Encoding.Base32.Hex: parseBase32Unpadded :: Text -> Either Text (Base32 'HexUnpadded Text)
+ Data.Text.Short.Encoding.Base32: decodeBase32PaddedUntyped :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32: decodeBase32UnpaddedUntyped :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32: decodeBase32Untyped :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32: parseBase32Padded :: ShortText -> Either Text (Base32 'StdPadded ShortText)
+ Data.Text.Short.Encoding.Base32: parseBase32Unpadded :: ShortText -> Either Text (Base32 'StdUnpadded ShortText)
+ Data.Text.Short.Encoding.Base32.Hex: decodeBase32PaddedUntyped :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32.Hex: decodeBase32UnpaddedUntyped :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32.Hex: decodeBase32Untyped :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32.Hex: parseBase32Padded :: ShortText -> Either Text (Base32 'HexPadded ShortText)
+ Data.Text.Short.Encoding.Base32.Hex: parseBase32Unpadded :: ShortText -> Either Text (Base32 'HexUnpadded ShortText)
- Data.ByteString.Base32: decodeBase32 :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32: decodeBase32 :: forall (k :: Alphabet). StdAlphabet k => Base32 k ByteString -> ByteString
- Data.ByteString.Base32: decodeBase32Padded :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32: decodeBase32Padded :: Base32 'StdPadded ByteString -> ByteString
- Data.ByteString.Base32: decodeBase32Unpadded :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32: decodeBase32Unpadded :: Base32 'StdUnpadded ByteString -> ByteString
- Data.ByteString.Base32: encodeBase32 :: ByteString -> Text
+ Data.ByteString.Base32: encodeBase32 :: ByteString -> Base32 'StdPadded Text
- Data.ByteString.Base32: encodeBase32' :: ByteString -> ByteString
+ Data.ByteString.Base32: encodeBase32' :: ByteString -> Base32 'StdPadded ByteString
- Data.ByteString.Base32: encodeBase32Unpadded :: ByteString -> Text
+ Data.ByteString.Base32: encodeBase32Unpadded :: ByteString -> Base32 'StdUnpadded Text
- Data.ByteString.Base32: encodeBase32Unpadded' :: ByteString -> ByteString
+ Data.ByteString.Base32: encodeBase32Unpadded' :: ByteString -> Base32 'StdUnpadded ByteString
- Data.ByteString.Base32.Hex: decodeBase32 :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32.Hex: decodeBase32 :: forall (k :: Alphabet). HexAlphabet k => Base32 k ByteString -> ByteString
- Data.ByteString.Base32.Hex: decodeBase32Padded :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32.Hex: decodeBase32Padded :: Base32 'HexPadded ByteString -> ByteString
- Data.ByteString.Base32.Hex: decodeBase32Unpadded :: ByteString -> Either Text ByteString
+ Data.ByteString.Base32.Hex: decodeBase32Unpadded :: Base32 'HexUnpadded ByteString -> ByteString
- Data.ByteString.Base32.Hex: encodeBase32 :: ByteString -> Text
+ Data.ByteString.Base32.Hex: encodeBase32 :: ByteString -> Base32 'HexPadded Text
- Data.ByteString.Base32.Hex: encodeBase32' :: ByteString -> ByteString
+ Data.ByteString.Base32.Hex: encodeBase32' :: ByteString -> Base32 'HexPadded ByteString
- Data.ByteString.Base32.Hex: encodeBase32Unpadded :: ByteString -> Text
+ Data.ByteString.Base32.Hex: encodeBase32Unpadded :: ByteString -> Base32 'HexUnpadded Text
- Data.ByteString.Base32.Hex: encodeBase32Unpadded' :: ByteString -> ByteString
+ Data.ByteString.Base32.Hex: encodeBase32Unpadded' :: ByteString -> Base32 'HexUnpadded ByteString
- Data.ByteString.Lazy.Base32: decodeBase32 :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32: decodeBase32 :: forall (k :: Alphabet). StdAlphabet k => Base32 k ByteString -> ByteString
- Data.ByteString.Lazy.Base32: decodeBase32Padded :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32: decodeBase32Padded :: Base32 'StdPadded ByteString -> ByteString
- Data.ByteString.Lazy.Base32: decodeBase32Unpadded :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32: decodeBase32Unpadded :: Base32 'StdUnpadded ByteString -> ByteString
- Data.ByteString.Lazy.Base32: encodeBase32 :: ByteString -> Text
+ Data.ByteString.Lazy.Base32: encodeBase32 :: ByteString -> Base32 'StdPadded Text
- Data.ByteString.Lazy.Base32: encodeBase32' :: ByteString -> ByteString
+ Data.ByteString.Lazy.Base32: encodeBase32' :: ByteString -> Base32 'StdPadded ByteString
- Data.ByteString.Lazy.Base32: encodeBase32Unpadded :: ByteString -> Text
+ Data.ByteString.Lazy.Base32: encodeBase32Unpadded :: ByteString -> Base32 'StdUnpadded Text
- Data.ByteString.Lazy.Base32: encodeBase32Unpadded' :: ByteString -> ByteString
+ Data.ByteString.Lazy.Base32: encodeBase32Unpadded' :: ByteString -> Base32 'StdUnpadded ByteString
- Data.ByteString.Lazy.Base32.Hex: decodeBase32 :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32.Hex: decodeBase32 :: forall (k :: Alphabet). HexAlphabet k => Base32 k ByteString -> ByteString
- Data.ByteString.Lazy.Base32.Hex: decodeBase32Padded :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32.Hex: decodeBase32Padded :: Base32 'HexPadded ByteString -> ByteString
- Data.ByteString.Lazy.Base32.Hex: decodeBase32Unpadded :: ByteString -> Either Text ByteString
+ Data.ByteString.Lazy.Base32.Hex: decodeBase32Unpadded :: Base32 'HexUnpadded ByteString -> ByteString
- Data.ByteString.Lazy.Base32.Hex: encodeBase32 :: ByteString -> Text
+ Data.ByteString.Lazy.Base32.Hex: encodeBase32 :: ByteString -> Base32 'HexPadded Text
- Data.ByteString.Lazy.Base32.Hex: encodeBase32' :: ByteString -> ByteString
+ Data.ByteString.Lazy.Base32.Hex: encodeBase32' :: ByteString -> Base32 'HexPadded ByteString
- Data.ByteString.Lazy.Base32.Hex: encodeBase32Unpadded :: ByteString -> Text
+ Data.ByteString.Lazy.Base32.Hex: encodeBase32Unpadded :: ByteString -> Base32 'HexUnpadded Text
- Data.ByteString.Lazy.Base32.Hex: encodeBase32Unpadded' :: ByteString -> ByteString
+ Data.ByteString.Lazy.Base32.Hex: encodeBase32Unpadded' :: ByteString -> Base32 'HexUnpadded ByteString
- Data.ByteString.Short.Base32: decodeBase32 :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32: decodeBase32 :: forall (k :: Alphabet). StdAlphabet k => Base32 k ShortByteString -> ShortByteString
- Data.ByteString.Short.Base32: decodeBase32Padded :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32: decodeBase32Padded :: Base32 'StdPadded ShortByteString -> ShortByteString
- Data.ByteString.Short.Base32: decodeBase32Unpadded :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32: decodeBase32Unpadded :: Base32 'StdUnpadded ShortByteString -> ShortByteString
- Data.ByteString.Short.Base32: encodeBase32 :: ShortByteString -> ShortText
+ Data.ByteString.Short.Base32: encodeBase32 :: ShortByteString -> Base32 'StdPadded ShortText
- Data.ByteString.Short.Base32: encodeBase32' :: ShortByteString -> ShortByteString
+ Data.ByteString.Short.Base32: encodeBase32' :: ShortByteString -> Base32 'StdPadded ShortByteString
- Data.ByteString.Short.Base32: encodeBase32Unpadded :: ShortByteString -> ShortText
+ Data.ByteString.Short.Base32: encodeBase32Unpadded :: ShortByteString -> Base32 'StdUnpadded ShortText
- Data.ByteString.Short.Base32: encodeBase32Unpadded' :: ShortByteString -> ShortByteString
+ Data.ByteString.Short.Base32: encodeBase32Unpadded' :: ShortByteString -> Base32 'StdUnpadded ShortByteString
- Data.ByteString.Short.Base32.Hex: decodeBase32 :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32.Hex: decodeBase32 :: forall (k :: Alphabet). HexAlphabet k => Base32 k ShortByteString -> ShortByteString
- Data.ByteString.Short.Base32.Hex: decodeBase32Padded :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32.Hex: decodeBase32Padded :: Base32 'HexPadded ShortByteString -> ShortByteString
- Data.ByteString.Short.Base32.Hex: decodeBase32Unpadded :: ShortByteString -> Either Text ShortByteString
+ Data.ByteString.Short.Base32.Hex: decodeBase32Unpadded :: Base32 'HexUnpadded ShortByteString -> ShortByteString
- Data.ByteString.Short.Base32.Hex: encodeBase32 :: ShortByteString -> ShortText
+ Data.ByteString.Short.Base32.Hex: encodeBase32 :: ShortByteString -> Base32 'HexPadded ShortText
- Data.ByteString.Short.Base32.Hex: encodeBase32' :: ShortByteString -> ShortByteString
+ Data.ByteString.Short.Base32.Hex: encodeBase32' :: ShortByteString -> Base32 'HexPadded ShortByteString
- Data.ByteString.Short.Base32.Hex: encodeBase32Unpadded :: ShortByteString -> ShortText
+ Data.ByteString.Short.Base32.Hex: encodeBase32Unpadded :: ShortByteString -> Base32 'HexUnpadded ShortText
- Data.ByteString.Short.Base32.Hex: encodeBase32Unpadded' :: ShortByteString -> ShortByteString
+ Data.ByteString.Short.Base32.Hex: encodeBase32Unpadded' :: ShortByteString -> Base32 'HexUnpadded ShortByteString
- Data.Text.Encoding.Base32: decodeBase32 :: Text -> Either Text Text
+ Data.Text.Encoding.Base32: decodeBase32 :: forall (k :: Alphabet). StdAlphabet k => Base32 k Text -> Text
- Data.Text.Encoding.Base32: decodeBase32Padded :: Text -> Either Text Text
+ Data.Text.Encoding.Base32: decodeBase32Padded :: Base32 'StdPadded Text -> Text
- Data.Text.Encoding.Base32: decodeBase32Unpadded :: Text -> Either Text Text
+ Data.Text.Encoding.Base32: decodeBase32Unpadded :: Base32 'StdUnpadded Text -> Text
- Data.Text.Encoding.Base32: encodeBase32 :: Text -> Text
+ Data.Text.Encoding.Base32: encodeBase32 :: Text -> Base32 'StdPadded Text
- Data.Text.Encoding.Base32: encodeBase32Unpadded :: Text -> Text
+ Data.Text.Encoding.Base32: encodeBase32Unpadded :: Text -> Base32 'StdUnpadded Text
- Data.Text.Encoding.Base32.Hex: decodeBase32 :: Text -> Either Text Text
+ Data.Text.Encoding.Base32.Hex: decodeBase32 :: forall (k :: Alphabet). HexAlphabet k => Base32 k Text -> Text
- Data.Text.Encoding.Base32.Hex: decodeBase32Padded :: Text -> Either Text Text
+ Data.Text.Encoding.Base32.Hex: decodeBase32Padded :: Base32 'HexPadded Text -> Text
- Data.Text.Encoding.Base32.Hex: decodeBase32Unpadded :: Text -> Either Text Text
+ Data.Text.Encoding.Base32.Hex: decodeBase32Unpadded :: Base32 'HexUnpadded Text -> Text
- Data.Text.Encoding.Base32.Hex: encodeBase32 :: Text -> Text
+ Data.Text.Encoding.Base32.Hex: encodeBase32 :: Text -> Base32 'HexPadded Text
- Data.Text.Encoding.Base32.Hex: encodeBase32Unpadded :: Text -> Text
+ Data.Text.Encoding.Base32.Hex: encodeBase32Unpadded :: Text -> Base32 'HexUnpadded Text
- Data.Text.Lazy.Encoding.Base32: decodeBase32 :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32: decodeBase32 :: forall (k :: Alphabet). StdAlphabet k => Base32 k Text -> Text
- Data.Text.Lazy.Encoding.Base32: decodeBase32Padded :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32: decodeBase32Padded :: Base32 'StdPadded Text -> Text
- Data.Text.Lazy.Encoding.Base32: decodeBase32Unpadded :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32: decodeBase32Unpadded :: Base32 'StdUnpadded Text -> Text
- Data.Text.Lazy.Encoding.Base32: encodeBase32 :: Text -> Text
+ Data.Text.Lazy.Encoding.Base32: encodeBase32 :: Text -> Base32 'StdPadded Text
- Data.Text.Lazy.Encoding.Base32: encodeBase32Unpadded :: Text -> Text
+ Data.Text.Lazy.Encoding.Base32: encodeBase32Unpadded :: Text -> Base32 'StdUnpadded Text
- Data.Text.Lazy.Encoding.Base32.Hex: decodeBase32 :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32.Hex: decodeBase32 :: forall (k :: Alphabet). HexAlphabet k => Base32 k Text -> Text
- Data.Text.Lazy.Encoding.Base32.Hex: decodeBase32Padded :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32.Hex: decodeBase32Padded :: Base32 'HexPadded Text -> Text
- Data.Text.Lazy.Encoding.Base32.Hex: decodeBase32Unpadded :: Text -> Either Text Text
+ Data.Text.Lazy.Encoding.Base32.Hex: decodeBase32Unpadded :: Base32 'HexUnpadded Text -> Text
- Data.Text.Lazy.Encoding.Base32.Hex: encodeBase32 :: Text -> Text
+ Data.Text.Lazy.Encoding.Base32.Hex: encodeBase32 :: Text -> Base32 'HexPadded Text
- Data.Text.Lazy.Encoding.Base32.Hex: encodeBase32Unpadded :: Text -> Text
+ Data.Text.Lazy.Encoding.Base32.Hex: encodeBase32Unpadded :: Text -> Base32 'HexUnpadded Text
- Data.Text.Short.Encoding.Base32: decodeBase32 :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32: decodeBase32 :: forall (k :: Alphabet). StdAlphabet k => Base32 k ShortText -> ShortText
- Data.Text.Short.Encoding.Base32: decodeBase32Padded :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32: decodeBase32Padded :: Base32 'StdPadded ShortText -> ShortText
- Data.Text.Short.Encoding.Base32: decodeBase32Unpadded :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32: decodeBase32Unpadded :: Base32 'StdUnpadded ShortText -> ShortText
- Data.Text.Short.Encoding.Base32: encodeBase32 :: ShortText -> ShortText
+ Data.Text.Short.Encoding.Base32: encodeBase32 :: ShortText -> Base32 'StdPadded ShortText
- Data.Text.Short.Encoding.Base32: encodeBase32Unpadded :: ShortText -> ShortText
+ Data.Text.Short.Encoding.Base32: encodeBase32Unpadded :: ShortText -> Base32 'StdUnpadded ShortText
- Data.Text.Short.Encoding.Base32.Hex: decodeBase32 :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32.Hex: decodeBase32 :: forall (k :: Alphabet). HexAlphabet k => Base32 k ShortText -> ShortText
- Data.Text.Short.Encoding.Base32.Hex: decodeBase32Padded :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32.Hex: decodeBase32Padded :: Base32 'HexPadded ShortText -> ShortText
- Data.Text.Short.Encoding.Base32.Hex: decodeBase32Unpadded :: ShortText -> Either Text ShortText
+ Data.Text.Short.Encoding.Base32.Hex: decodeBase32Unpadded :: Base32 'HexUnpadded ShortText -> ShortText
- Data.Text.Short.Encoding.Base32.Hex: encodeBase32 :: ShortText -> ShortText
+ Data.Text.Short.Encoding.Base32.Hex: encodeBase32 :: ShortText -> Base32 'HexPadded ShortText
- Data.Text.Short.Encoding.Base32.Hex: encodeBase32Unpadded :: ShortText -> ShortText
+ Data.Text.Short.Encoding.Base32.Hex: encodeBase32Unpadded :: ShortText -> Base32 'HexUnpadded ShortText
Files
- CHANGELOG.md +23/−0
- LICENSE +1/−1
- MIGRATION-1.0.md +95/−0
- README.md +89/−15
- base32.cabal +44/−18
- benchmarks/Base32Bench.hs +122/−89
- cbits/base32_simd.c +466/−0
- cbits/base32_simd.h +15/−0
- src/Data/Base32/Types.hs +113/−0
- src/Data/ByteString/Base32.hs +149/−81
- src/Data/ByteString/Base32/Hex.hs +145/−72
- src/Data/ByteString/Base32/Internal.hs +0/−120
- src/Data/ByteString/Base32/Internal/Head.hs +53/−4
- src/Data/ByteString/Base32/Internal/Loop.hs +187/−126
- src/Data/ByteString/Base32/Internal/SIMD.hs +153/−0
- src/Data/ByteString/Base32/Internal/Short.hs +237/−0
- src/Data/ByteString/Base32/Internal/Tables.hs +21/−0
- src/Data/ByteString/Base32/Internal/Tail.hs +17/−0
- src/Data/ByteString/Base32/Internal/Utils.hs +72/−0
- src/Data/ByteString/Lazy/Base32.hs +143/−99
- src/Data/ByteString/Lazy/Base32/Hex.hs +141/−98
- src/Data/ByteString/Short/Base32.hs +175/−81
- src/Data/ByteString/Short/Base32/Hex.hs +178/−82
- src/Data/Text/Encoding/Base32.hs +167/−103
- src/Data/Text/Encoding/Base32/Error.hs +7/−1
- src/Data/Text/Encoding/Base32/Hex.hs +163/−98
- src/Data/Text/Lazy/Encoding/Base32.hs +164/−106
- src/Data/Text/Lazy/Encoding/Base32/Hex.hs +163/−105
- src/Data/Text/Short/Encoding/Base32.hs +198/−123
- src/Data/Text/Short/Encoding/Base32/Hex.hs +194/−117
- test/Internal.hs +182/−90
- test/Main.hs +163/−124
- test/Regression.hs +270/−0
- test/simd-native.c +184/−0
- test/typecheck.sh +97/−0
CHANGELOG.md view
@@ -1,5 +1,28 @@ # Revision history for base32 +## 1.0++This release introduces a typed encoding API with breaking signature changes.+See [MIGRATION-1.0.md](MIGRATION-1.0.md) for the changes from 0.4.++* Introduce alphabet- and padding-tagged `Base32` values in `Data.Base32.Types`.+ Encoders return wrapped values; typed decoders consume them. Checked decoders+ now use the `Untyped` suffix for consuming raw Base32 encoded bytes.++ Typed decoding uses unchecked scalar and SIMD loops, with streaming lazy decoding.+ `StdAlphabet` and `HexAlphabet` admit both padding forms in general typed decoders;+ padding-specific typed decoders require exact tags.++* Replace `isBase32`, `isValidBase32`, `isBase32Hex`, and `isValidBase32Hex` with+ `parseBase32Padded` and `parseBase32Unpadded` in all twelve APIs.+ Parsers preserve input, accept the decoder's existing case-insensitive alphabets,+ and check unused pad bits. Noncanonical pad bits are now rejected by decoders too. These were removed because they had no measurable downstream usage, and were of questionable utility. +* Added an optional, default-off `simd` flag with NEON, SSSE3, and AVX2 prefixes+ and a portable scalar fallback, for both standard and extended-hex alphabets.+* Remove `Data.ByteString.Base32.Internal`; use `Internal.Head` for codec entry+ points and `Internal.Utils` for `validateLastNPads`. They were relics of older development. +* Support GHC 9.6–9.14; update copyright through 2026, CI, bounds, and benchmarks.+ ## 0.4 * Support for GHC 9.8
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2020, Emily Pillmore+Copyright (c) 2020-2026, Emily Pillmore All rights reserved.
+ MIGRATION-1.0.md view
@@ -0,0 +1,95 @@+# Migrating from base32 0.4 to 1.0++## Encoded values carry their alphabet and padding++Encoding now produces type-tagged values that carry the alphabet and padding provenance +carried out by the encoding. You can extract these bytes or assert their provenance by importing `Data.Base32.Types` and manipulating the wrapper through the supplied functions. `Alphabet(..)` is also included, and can be used in conjunction with `-XDataKinds` to assert what you need. Standard encoders return `Base32 'StdPadded a`+or `Base32 'StdUnpadded a`, while extended-hex encoders return `Base32 'HexPadded a` or+`Base32 'HexUnpadded a`. For example:++```haskell+-- Before+rawb32bytes = B32.encodeBase32' bytes++-- After, when the surrounding API expects a plain ByteString+rawb32bytes = extractBase32 (B32.encodeBase32' unencodedbytes)++-- A typed roundtrip no longer returns Either+unencodedBytesAgain = B32.decodeBase32 (B32.encodeBase32' unencodedbytes)+```++The reasoning behind this wrapper is clear from a performance perspective: by shoving the information about the bytes into the typed wrapper and shunting the contract to the user +or the program, we have all the info we need to forego validity checks during the decode +process. This removes most branching from the procedure and hotpath detection is both +faster and easier, resulting in a significant speedup. Go Haskell!++## Checked decoding keeps an explicit name++| 0.4 | 1.0 checked equivalent |+| --- | --- |+| `decodeBase32` | `decodeBase32Untyped` |+| `decodeBase32Padded` | `decodeBase32PaddedUntyped` |+| `decodeBase32Unpadded` | `decodeBase32UnpaddedUntyped` |++These replacements apply to all twelve ByteString/Text modules. The old names+now consume `Base32` values and return the decoded result directly. The+`decodeBase32With`, `decodeBase32PaddedWith`, and `decodeBase32UnpaddedWith`+functions keep their checked signatures and names, and are `Text` only, because +decoding is separate from utf8 conversion and errors are surfaced accordingly for each step. ++Typed decoders use dedicated unchecked loops. They trust the established+provenance and do not check the alphabet, input length, padding, or unused bits.+`decodeBase32` accepts both padding forms within its alphabet through the+`StdAlphabet k` or `HexAlphabet k` constraint. The padding-specific decoders+require the corresponding exact tag, so mixing their API paths is a type error.+For example, `decodeBase32Padded (encodeBase32Unpadded' bytes)` cannot compile.+Use `TypeFamilies` when writing signatures with these constraints.+Callers using `assertBase32`, record updates, or mapping instances must preserve+validity themselves.++Typed lazy decoding streams through complete quanta. Checked lazy decoding and+parsing still validate a complete finite input.++## Replace predicates with parsers++`isBase32`, `isValidBase32`, `isBase32Hex`, and `isValidBase32Hex` are removed.+Use `parseBase32Padded` or `parseBase32Unpadded` from the corresponding standard+or `.Hex` module. Each returns the precise padding tag; there is no permissive+`parseBase32`. `Data.Either.isRight . parseBase32Padded` provides a Boolean check+for padded input when needed. Prefer retaining the successful parsed value:++```haskell+case B32.parseBase32Padded wire of+ Left err -> handleError err+ Right encoded -> consumeBytes (B32.decodeBase32 encoded)+```++Parsers return the original encoded representation, including case, padding,+and lazy chunk structure. Unlike the old shape predicates, they perform full+validation. The decoders' existing lowercase acceptance is preserved; encoders+produce uppercase. Inputs with nonzero unused bits, such as `MZ======`, are now+rejected instead of silently decoding as `f`.++## Text decoding and binary payloads++The wrapper establishes Base32 syntax, not the decoded payload's character+encoding. `parseBase32Padded (Text.pack "74======")` succeeds, since it encodes+byte `0xff`.+Checked Text decoding now reports invalid UTF-8 as `Left`, including short and+lazy Text. Typed Text decoding requires valid UTF-8 and fails otherwise. Use+ByteString decoding for binary data, or a `decode*With` function for explicit+character conversion.++## Build and compatibility++`Data.ByteString.Base32.Internal` is removed. Import codec entry points from+`Data.ByteString.Base32.Internal.Head` and `validateLastNPads` from+`Data.ByteString.Base32.Internal.Utils`.++The compiler range is GHC 9.6–9.14. Dependencies include `primitive` for direct+short byte-array codecs. SIMD is optional and remains disabled unless selected+with `-fsimd`; both modes use identical validation rules. The source archive+contains the bundled C sources, headers, tests, and this migration guide.++Update downstream libraries, including `base32-lens`, before widening their+bounds to accept 1.0.
README.md view
@@ -3,25 +3,99 @@  [](https://hackage.haskell.org/package/base32) -Padded and unpadded base32 and base32hex encoding and decoding for `Text` and `ByteString` values.+RFC 4648 standard and extended-hex Base32 encoding, with padded and unpadded+variants for strict, lazy, and short `ByteString` and `Text`. -For the companion optics and pattern synonyms, see [base32-lens](https://hackage.haskell.org/package/base32-lens).+```haskell+import Data.Base32.Types+import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Base32 as B32 -### Summary+encoded = B32.encodeBase32' (BS.pack "Sun")+-- encoded :: Base32 'StdPadded ByteString+-- extractBase32 encoded == BS.pack "KN2W4==="+-- B32.decodeBase32 encoded == BS.pack "Sun" -The following types are supported for both padded and unpadded std and extended hex alphabets:+parsed = B32.parseBase32Unpadded (BS.pack "KN2W4")+-- Right "KN2W4"; the original encoded representation is retained -- `Data.ByteString`-- `Data.ByteString.Lazy`-- `Data.ByteString.Short`-- `Data.Text`-- `Data.Text.Lazy`-- `Data.Text.Short`+raw = B32.decodeBase32Untyped (BS.pack "KN2W4")+-- Right "Sun"+``` -Additionally this library has+Use `.Hex` modules for the extended-hex alphabet. Values carry `'StdPadded`,+`'StdUnpadded`, `'HexPadded`, or `'HexUnpadded`, preventing accidental mixing of+alphabets or padding-specific APIs. See+[MIGRATION-1.0.md](MIGRATION-1.0.md) for the changes from 0.4. -- Better performance than `memory` for encode and decode 3-4x.-- Optics for handling more complex structures with Base32 representations via the `base32-lens` package-- Checks for both validity and correctness of Base32 and Base32hex encodings+## Validation -There are no dependencies aside from those bundled with GHC, `text-short`.+`parseBase32Padded` and `parseBase32Unpadded` establish the corresponding padding+tag. Both check the alphabet, input length, padding placement, and zero unused+trailing bits.+They accept upper- and lowercase letters, preserve the original case and+representation, and accept empty input. Whitespace is rejected.++Typed decoders use dedicated unchecked loops: provenance establishes validity,+so decoding does not repeat alphabet, length, padding, or canonical-bit checks.+`decodeBase32` uses the `StdAlphabet` or `HexAlphabet` constraint to accept both+padding forms of its alphabet. `decodeBase32Padded` and `decodeBase32Unpadded`+require their exact tags; calling them with the wrong padding form is a type error.++The wrapper identifies the alphabet; it does not prove that decoded bytes form UTF-8.+Checked Text decoders return `Left` for invalid UTF-8; `decodeBase32With` and+its variants let you choose a character conversion. For arbitrary binary+payloads, use ByteString decoders.++`assertBase32`, record updates, and the mapping instances+can bypass validation. Typed decoding requires a truthful encoded value and,+for Text, valid UTF-8 payload bytes. Use parsers and checked decoders for+external input.++Lazy encoding and typed decoding preserve streaming through rechunking.+Checked lazy decoding and parsing validate a complete finite input.++## Optional SIMD++```sh+cabal build -fsimd+cabal test -fsimd+```++The `simd` flag defaults to **off**. It enables bundled NEON kernels on AArch64+and runtime-dispatched SSSE3/AVX2 kernels on x86-64, with scalar fallback on+other targets. Small inputs and final quanta use Haskell scalar code. No+external native library is required.++Strict and short values use SIMD directly. Short codecs use byte arrays without+pinning; typed ShortText additionally converts the decoded UTF-8 payload. Lazy+variants reach the same kernels through their strict chunks or checked input.++## Development++```sh+cabal test -f-simd+cabal test -fsimd --ghc-options='-fcheck-prim-bounds -fno-ignore-asserts'+cabal exec -- sh test/typecheck.sh+cabal haddock -fsimd+cabal check+```++CI tests both flag configurations across GHC 9.6–9.14, builds from the source+archive, and runs native sanitizer and guard-page tests on x86-64 and ARM64.++Compare optimized scalar and SIMD benchmark builds separately:++```sh+cabal bench -f-simd --builddir=dist-scalar -O2 --benchmark-options='--csv scalar.csv'+cabal bench -fsimd --builddir=dist-simd -O2 --benchmark-options='--csv simd.csv'+```++Benchmark names identify the selected flag configuration. Both builds measure+the same deterministic inputs, with setup excluded from timing. The suite covers+strict and short codecs, both alphabets and padding forms, encoding, typed and+untyped decoding, parsing, sizes around dispatch thresholds, large buffers, and+sliced strict inputs.++The companion [base32-lens](https://hackage.haskell.org/package/base32-lens)+package needs an update for the 1.0 API before it can be used with this release.
base32.cabal view
@@ -1,10 +1,11 @@ cabal-version: 3.0 name: base32-version: 0.4+version: 1.0 synopsis: Fast RFC 4648-compliant Base32 encoding description: RFC 4648-compliant Base32 encodings and decodings.- This library provides performant encoding and decoding primitives, as well as support for textual values.+ This library provides performant encoding and decoding primitives,+ as well as support for textual values. homepage: https://github.com/emilypi/base32 bug-reports: https://github.com/emilypi/base32/issues@@ -12,25 +13,36 @@ license-file: LICENSE author: Emily Pillmore maintainer: emilypi@cohomolo.gy-copyright: (c) 2020-2023 Emily Pillmore+copyright: (c) 2020-2026 Emily Pillmore category: Data build-type: Simple-extra-source-files:+extra-doc-files: CHANGELOG.md README.md+ MIGRATION-1.0.md tested-with:- GHC ==9.0.2 || ==9.2.8 || ==9.4.7 || ==9.6.3 || ==9.8.1+ GHC ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.4 || ==9.14.1 +extra-source-files:+ cbits/base32_simd.h+ test/simd-native.c+ test/typecheck.sh++flag simd+ description: Enable bundled SIMD kernels with portable scalar fallback+ default: False+ manual: True+ source-repository head type: git location: https://github.com/emilypi/base32.git library exposed-modules:+ Data.Base32.Types Data.ByteString.Base32 Data.ByteString.Base32.Hex- Data.ByteString.Base32.Internal Data.ByteString.Lazy.Base32 Data.ByteString.Lazy.Base32.Hex Data.ByteString.Short.Base32@@ -43,37 +55,47 @@ Data.Text.Short.Encoding.Base32 Data.Text.Short.Encoding.Base32.Hex - other-modules:+ Data.ByteString.Base32.Internal.Short Data.ByteString.Base32.Internal.Head Data.ByteString.Base32.Internal.Loop Data.ByteString.Base32.Internal.Tables Data.ByteString.Base32.Internal.Tail Data.ByteString.Base32.Internal.Utils + if flag(simd)+ exposed-modules: Data.ByteString.Base32.Internal.SIMD+ cpp-options: -DBASE32_SIMD+ c-sources: cbits/base32_simd.c+ include-dirs: cbits+ cc-options: -O2 -std=c99+ build-depends:- base >=4.15 && <4.20- , bytestring ^>=0.11+ base >=4.18 && <4.23+ , bytestring >=0.11 && <0.13 , deepseq >=1.4.4.0 && <1.6- , text >=2.0 && <2.2+ , primitive >=0.6 && <0.10+ , text >=2.0 && <2.3 , text-short ^>=0.1 hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall -test-suite tasty+test-suite base32-tests default-language: Haskell2010 type: exitcode-stdio-1.0 hs-source-dirs: test other-modules: Internal+ Regression main-is: Main.hs+ ghc-options: -Wall -threaded -rtsopts build-depends:- base >=4.15 && <4.20+ base >=4.18 && <4.23 , base32- , bytestring ^>=0.11+ , bytestring >=0.11 && <0.13+ , deepseq , memory , QuickCheck- , random-bytestring , tasty , tasty-hunit , tasty-quickcheck@@ -86,11 +108,15 @@ hs-source-dirs: benchmarks main-is: Base32Bench.hs build-depends:- base >=4.15 && <4.20+ base >=4.18 && <4.23 , base32- , bytestring ^>=0.11+ , bytestring >=0.11 && <0.13 , criterion , deepseq- , memory- , random-bytestring , text >=2.0+ , text-short++ if flag(simd)+ cpp-options: -DBASE32_SIMD++ ghc-options: -Wall -rtsopts
benchmarks/Base32Bench.hs view
@@ -1,96 +1,129 @@-{-# LANGUAGE PackageImports #-}-module Main-( main-)where-+{-# LANGUAGE CPP #-}+-- |+-- Module : Main+-- Copyright : (c) 2020-2026 Emily Pillmore+-- License : BSD-style+--+-- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>+-- Stability : Experimental+-- Portability : portable+--+-- Build once with -f-simd and once with -fsimd to compare the same cases.+-- Sizes describe raw input bytes; sliced inputs also apply to decode buffers.+--+module Main (main) where -import Criterion import Criterion.Main--import "memory" Data.ByteArray.Encoding as Mem-import Data.ByteString-import "base32" Data.ByteString.Base32 as B32-import Data.ByteString.Random (random)-+import Data.Base32.Types (assertBase32, extractBase32)+import qualified Data.ByteString as B+import qualified Data.ByteString.Base32 as B32+import qualified Data.ByteString.Base32.Hex as H32+import qualified Data.ByteString.Short as S+import qualified Data.ByteString.Short.Base32 as S32+import qualified Data.ByteString.Short.Base32.Hex as SH32+import qualified Data.Text.Short as T+import qualified Data.Text.Short.Encoding.Base32 as T32 main :: IO ()-main =- defaultMain- [ env bs $ \ ~(bs25,bs100,bs1k,bs10k,bs100k,bs1mm) ->- bgroup "encode"- [ bgroup "25"- [ bench "memory" $ whnf ctob bs25- , bench "base32" $ whnf B32.encodeBase32' bs25- ]- , bgroup "100"- [ bench "memory" $ whnf ctob bs100- , bench "base32" $ whnf B32.encodeBase32' bs100- ]- , bgroup "1k"- [ bench "memory" $ whnf ctob bs1k- , bench "base32" $ whnf B32.encodeBase32' bs1k- ]- , bgroup "10k"- [ bench "memory" $ whnf ctob bs10k- , bench "base32" $ whnf B32.encodeBase32' bs10k- ]- , bgroup "100k"- [ bench "memory" $ whnf ctob bs100k- , bench "base32" $ whnf B32.encodeBase32' bs100k- ]- , bgroup "1mm"- [ bench "memory" $ whnf ctob bs1mm- , bench "base32" $ whnf B32.encodeBase32' bs1mm- ]- ]- , env bs' $ \ ~(bs25,bs100,bs1k,bs10k,bs100k,bs1mm) ->- bgroup "decode"- [ bgroup "25"- [ bench "memory" $ whnf btoc bs25- , bench "base32" $ whnf B32.decodeBase32 bs25- ]- , bgroup "100"- [ bench "memory" $ whnf btoc bs100- , bench "base32" $ whnf B32.decodeBase32 bs100- ]- , bgroup "1k"- [ bench "memory" $ whnf btoc bs1k- , bench "base32" $ whnf B32.decodeBase32 bs1k- ]- , bgroup "10k"- [ bench "memory" $ whnf btoc bs10k- , bench "base32" $ whnf B32.decodeBase32 bs10k- ]- , bgroup "100k"- [ bench "memory" $ whnf btoc bs100k- , bench "base32" $ whnf B32.decodeBase32 bs100k- ]- , bgroup "1mm"- [ bench "memory" $ whnf btoc bs1mm- , bench "base32" $ whnf B32.decodeBase32 bs1mm- ]+main = defaultMain+ [ bgroup implementation+ [ bgroup (show size)+ [ bgroup "strict"+ [ env (inputs id size offset) $+ \ ~(raw, std, stdUnpadded, hex, hexUnpadded) ->+ bgroup ("offset-" ++ show offset)+ [ bgroup "standard-padded"+ [ bench "encode" $ nf B32.encodeBase32' raw+ , bench "decode-typed" $+ nf (B32.decodeBase32Padded . assertBase32) std+ , bench "decode-untyped" $ nf B32.decodeBase32PaddedUntyped std+ , bench "parse" $ nf B32.parseBase32Padded std+ ]+ , bgroup "standard-unpadded"+ [ bench "encode" $ nf B32.encodeBase32Unpadded' raw+ , bench "decode-typed" $+ nf (B32.decodeBase32Unpadded . assertBase32) stdUnpadded+ , bench "decode-untyped" $+ nf B32.decodeBase32UnpaddedUntyped stdUnpadded+ , bench "parse" $ nf B32.parseBase32Unpadded stdUnpadded+ ]+ , bgroup "hex-padded"+ [ bench "encode" $ nf H32.encodeBase32' raw+ , bench "decode-typed" $+ nf (H32.decodeBase32Padded . assertBase32) hex+ , bench "decode-untyped" $ nf H32.decodeBase32PaddedUntyped hex+ , bench "parse" $ nf H32.parseBase32Padded hex+ ]+ , bgroup "hex-unpadded"+ [ bench "encode" $ nf H32.encodeBase32Unpadded' raw+ , bench "decode-typed" $+ nf (H32.decodeBase32Unpadded . assertBase32) hexUnpadded+ , bench "decode-untyped" $+ nf H32.decodeBase32UnpaddedUntyped hexUnpadded+ , bench "parse" $ nf H32.parseBase32Unpadded hexUnpadded+ ]+ ]+ | offset <- [0, 1]+ ]+ , env (inputs S.toShort size 0) $+ \ ~(raw, std, stdUnpadded, hex, hexUnpadded) ->+ bgroup "short"+ [ bgroup "standard-padded"+ [ bench "encode" $ nf S32.encodeBase32' raw+ , bench "decode-typed" $+ nf (S32.decodeBase32Padded . assertBase32) std+ , bench "decode-untyped" $ nf S32.decodeBase32PaddedUntyped std+ , bench "parse" $ nf S32.parseBase32Padded std+ ]+ , bgroup "standard-unpadded"+ [ bench "encode" $ nf S32.encodeBase32Unpadded' raw+ , bench "decode-typed" $+ nf (S32.decodeBase32Unpadded . assertBase32) stdUnpadded+ , bench "decode-untyped" $+ nf S32.decodeBase32UnpaddedUntyped stdUnpadded+ , bench "parse" $ nf S32.parseBase32Unpadded stdUnpadded+ ]+ , bgroup "hex-padded"+ [ bench "encode" $ nf SH32.encodeBase32' raw+ , bench "decode-typed" $+ nf (SH32.decodeBase32Padded . assertBase32) hex+ , bench "decode-untyped" $ nf SH32.decodeBase32PaddedUntyped hex+ , bench "parse" $ nf SH32.parseBase32Padded hex+ ]+ , bgroup "hex-unpadded"+ [ bench "encode" $ nf SH32.encodeBase32Unpadded' raw+ , bench "decode-typed" $+ nf (SH32.decodeBase32Unpadded . assertBase32) hexUnpadded+ , bench "decode-untyped" $+ nf SH32.decodeBase32UnpaddedUntyped hexUnpadded+ , bench "parse" $ nf SH32.parseBase32Unpadded hexUnpadded+ ]+ ]+ ]+ | size <- [25, 39, 40, 41, 79, 80, 81, 4096, 1048576] ]- ]- where- ctob :: ByteString -> ByteString- ctob = Mem.convertToBase Mem.Base32- btoc :: ByteString -> Either String ByteString- btoc = Mem.convertFromBase Mem.Base32+ , env (pure (T.pack "Base32 benchmark λ")) $ \shortText ->+ bgroup implementation [bench "short-text" $ nf T32.encodeBase32 shortText]+ ] - bs = do- a <- random 25- b <- random 100- c <- random 1000- d <- random 10000- e <- random 100000- f <- random 1000000- return (a,b,c,d,e,f)+implementation :: String+#ifdef BASE32_SIMD+implementation = "simd"+#else+implementation = "scalar"+#endif - bs' = do- a <- B32.encodeBase32' <$> random 25- b <- B32.encodeBase32' <$> random 100- c <- B32.encodeBase32' <$> random 1000- d <- B32.encodeBase32' <$> random 10000- e <- B32.encodeBase32' <$> random 100000- f <- B32.encodeBase32' <$> random 1000000- return (a,b,c,d,e,f)+-- Criterion forces this environment before timing. Both builds use the same+-- deterministic data; encoding, slicing, and conversion are excluded from timing.+inputs :: (B.ByteString -> a) -> Int -> Int -> IO (a, a, a, a, a)+inputs convert size offset = pure+ ( prepared raw+ , prepared (extractBase32 (B32.encodeBase32' raw))+ , prepared (extractBase32 (B32.encodeBase32Unpadded' raw))+ , prepared (extractBase32 (H32.encodeBase32' raw))+ , prepared (extractBase32 (H32.encodeBase32Unpadded' raw))+ )+ where+ raw = B.pack (take size (cycle [0 .. 255]))+ prepared bytes = convert . B.take (B.length bytes) . B.drop offset $+ B.replicate offset 0xe5 <> bytes <> B.replicate 32 0xff
+ cbits/base32_simd.c view
@@ -0,0 +1,466 @@+/* Copyright (c) 2026 Emily Pillmore. BSD-3-Clause; see LICENSE. */+#include "base32_simd.h"+#include <string.h>++/* Each lane encodes ten bytes or decodes sixteen. All loads/stores are+ * bounded; invalid decode blocks are not committed. Target attributes keep+ * optional x86 instructions out of the baseline dispatcher. Force macros+ * only cap backend selection; they never bypass hardware/OS checks. */+#if !defined(HS_BASE32_SIMD_FORCE_SCALAR)+#if defined(__aarch64__) && defined(__ARM_NEON)+#define HS_BASE32_NEON+#include <arm_neon.h>+#elif defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__))+#define HS_BASE32_X86+#include <immintrin.h>+#endif+#endif++#if defined(HS_BASE32_NEON) || defined(HS_BASE32_X86)+static const uint8_t enc_a[16] = {+ 0, 0, 1, 1, 2, 3, 3, 4, 5, 5, 6, 6, 7, 8, 8, 9+};+static const uint8_t enc_b[16] = {+ 255, 1, 255, 2, 3, 255, 4, 255, 255, 6, 255, 7, 8, 255, 9, 255+};+static const uint8_t dec_a[16] = {+ 0, 1, 3, 4, 6, 8, 9, 11, 12, 14, 255, 255, 255, 255, 255, 255+};+static const uint8_t dec_b[16] = {+ 1, 2, 4, 5, 7, 9, 10, 12, 13, 15, 255, 255, 255, 255, 255, 255+};+static const uint8_t dec_c[16] = {+ 255, 3, 255, 6, 255, 255, 11, 255, 14, 255, 255, 255, 255, 255, 255, 255+};+#endif++#if defined(HS_BASE32_NEON)++static uint8x16_t load10(const uint8_t *src)+{+ uint8_t bytes[16] = {0};+ memcpy(bytes, src, 10);+ return vld1q_u8(bytes);+}++static void store10(uint8_t *dst, uint8x16_t value)+{+ uint8_t bytes[16];+ vst1q_u8(bytes, value);+ memcpy(dst, bytes, 10);+}++static size_t neon_encode(const uint8_t *src, size_t len, uint8_t *dst, int hex)+{+ static const int8_t shifts_a[16] = {+ -3,2,-1,4,1,-2,3,0, -3,2,-1,4,1,-2,3,0+ };+ static const int8_t shifts_b[16] = {+ 0,-6,0,-4,-7,0,-5,0, 0,-6,0,-4,-7,0,-5,0+ };+ size_t done = 0;+ while (len - done >= 10) {+ const uint8x16_t input = load10(src + done);+ const uint8x16_t a = vqtbl1q_u8(input, vld1q_u8(enc_a));+ const uint8x16_t b = vqtbl1q_u8(input, vld1q_u8(enc_b));+ const uint8x16_t index = vandq_u8(vdupq_n_u8(31), vorrq_u8(+ vshlq_u8(a, vld1q_s8(shifts_a)), vshlq_u8(b, vld1q_s8(shifts_b))));+ const uint8x16_t high = vcgtq_u8(index, vdupq_n_u8(hex ? 9 : 25));+ const uint8x16_t offset = vbslq_u8(high,+ vdupq_n_u8(hex ? 55 : 24), vdupq_n_u8(hex ? 48 : 65));+ vst1q_u8(dst, vaddq_u8(index, offset));+ dst += 16;+ done += 10;+ }+ return done;+}++static size_t neon_decode(const uint8_t *src, size_t len, uint8_t *dst,+ int hex, int checked)+{+ static const int8_t shifts_a[16] = {3,6,4,7,5,3,6,4,7,5,0,0,0,0,0,0};+ static const int8_t shifts_b[16] = {-2,1,-1,2,0,-2,1,-1,2,0,0,0,0,0,0,0};+ static const int8_t shifts_c[16] = {0,-4,0,-3,0,0,-4,0,-3,0,0,0,0,0,0,0};+ size_t done = 0;+ while (len - done >= 16) {+ const uint8x16_t input = vld1q_u8(src + done);+ const uint8x16_t upper = vandq_u8(input, vdupq_n_u8(0xdf));+ const uint8x16_t letter = vsubq_u8(upper, vdupq_n_u8('A'));+ const uint8x16_t digit = vsubq_u8(input, vdupq_n_u8(hex ? '0' : '2'));+ const uint8x16_t is_letter = vcleq_u8(letter, vdupq_n_u8(hex ? 21 : 25));+ const uint8x16_t is_digit = vcleq_u8(digit, vdupq_n_u8(hex ? 9 : 5));+ if (checked && vminvq_u8(vorrq_u8(is_letter, is_digit)) == 0) {+ break;+ }+ const uint8x16_t value = vbslq_u8(is_letter,+ vaddq_u8(letter, vdupq_n_u8(hex ? 10 : 0)),+ vaddq_u8(digit, vdupq_n_u8(hex ? 0 : 26)));+ const uint8x16_t a = vqtbl1q_u8(value, vld1q_u8(dec_a));+ const uint8x16_t b = vqtbl1q_u8(value, vld1q_u8(dec_b));+ const uint8x16_t c = vqtbl1q_u8(value, vld1q_u8(dec_c));+ store10(dst, vorrq_u8(vshlq_u8(a, vld1q_s8(shifts_a)),+ vorrq_u8(vshlq_u8(b, vld1q_s8(shifts_b)),+ vshlq_u8(c, vld1q_s8(shifts_c)))));+ dst += 10;+ done += 16;+ }+ return done;+}+#elif defined(HS_BASE32_X86)++__attribute__((target("ssse3")))+static __m128i ssse3_mask(const uint8_t *bytes)+{+ const __m128i value = _mm_loadu_si128((const __m128i *)bytes);+ return value;+}++__attribute__((target("ssse3")))+static size_t ssse3_encode(const uint8_t *src, size_t len, uint8_t *dst, int hex)+{+ size_t done = 0;+ while (len - done >= 10) {+ uint8_t bytes[16] = {0};+ memcpy(bytes + 0, src + done + 0, 10);+ const __m128i value = _mm_loadu_si128((const __m128i *)bytes);+ __m128i output = _mm_setzero_si128();+ const __m128i a = _mm_shuffle_epi8(value, ssse3_mask(enc_a));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_srli_epi16(a, 3),+ ssse3_mask((const uint8_t[16]){31,0,0,0,0,0,0,0,31,0,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_srli_epi16(a, 2),+ ssse3_mask((const uint8_t[16]){0,0,0,0,0,31,0,0,0,0,0,0,0,31,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_srli_epi16(a, 1),+ ssse3_mask((const uint8_t[16]){0,0,31,0,0,0,0,0,0,0,31,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ a,+ ssse3_mask((const uint8_t[16]){0,0,0,0,0,0,0,31,0,0,0,0,0,0,0,31})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_slli_epi16(a, 1),+ ssse3_mask((const uint8_t[16]){0,0,0,0,30,0,0,0,0,0,0,0,30,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_slli_epi16(a, 2),+ ssse3_mask((const uint8_t[16]){0,28,0,0,0,0,0,0,0,28,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_slli_epi16(a, 3),+ ssse3_mask((const uint8_t[16]){0,0,0,0,0,0,24,0,0,0,0,0,0,0,24,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_slli_epi16(a, 4),+ ssse3_mask((const uint8_t[16]){0,0,0,16,0,0,0,0,0,0,0,16,0,0,0,0})));+ const __m128i b = _mm_shuffle_epi8(value, ssse3_mask(enc_b));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_srli_epi16(b, 7),+ ssse3_mask((const uint8_t[16]){0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_srli_epi16(b, 6),+ ssse3_mask((const uint8_t[16]){0,3,0,0,0,0,0,0,0,3,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_srli_epi16(b, 5),+ ssse3_mask((const uint8_t[16]){0,0,0,0,0,0,7,0,0,0,0,0,0,0,7,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_srli_epi16(b, 4),+ ssse3_mask((const uint8_t[16]){0,0,0,15,0,0,0,0,0,0,0,15,0,0,0,0})));+ const __m128i high = _mm_cmpgt_epi8(output, _mm_set1_epi8(hex ? 9 : 25));+ const __m128i offset = _mm_or_si128(+ _mm_and_si128(high, _mm_set1_epi8(hex ? 55 : 24)),+ _mm_andnot_si128(high, _mm_set1_epi8(hex ? 48 : 65)));+ _mm_storeu_si128((__m128i *)dst, _mm_add_epi8(output, offset));+ dst += 16;+ done += 10;+ }+ return done;+}++__attribute__((target("ssse3")))+static size_t ssse3_decode(const uint8_t *src, size_t len, uint8_t *dst,+ int hex, int checked)+{+ size_t done = 0;+ while (len - done >= 16) {+ const __m128i input = _mm_loadu_si128((const __m128i *)(src + done));+ const __m128i upper = _mm_and_si128(input, _mm_set1_epi8((char)0xdf));+ const __m128i letters = _mm_and_si128(+ _mm_cmpgt_epi8(upper, _mm_set1_epi8('A' - 1)),+ _mm_cmpgt_epi8(_mm_set1_epi8(hex ? 'V' + 1 : 'Z' + 1), upper));+ const __m128i digits = _mm_and_si128(+ _mm_cmpgt_epi8(input, _mm_set1_epi8(hex ? '0' - 1 : '2' - 1)),+ _mm_cmpgt_epi8(_mm_set1_epi8(hex ? '9' + 1 : '7' + 1), input));+ if (checked && (unsigned)_mm_movemask_epi8(_mm_or_si128(letters, digits))+ != 0xffffU) {+ break;+ }+ const __m128i value = _mm_or_si128(+ _mm_and_si128(letters,+ _mm_add_epi8(upper, _mm_set1_epi8(hex ? 10 - 'A' : -'A'))),+ _mm_and_si128(digits,+ _mm_add_epi8(input, _mm_set1_epi8(hex ? -'0' : 26 - '2'))));+ __m128i output = _mm_setzero_si128();+ const __m128i a = _mm_shuffle_epi8(value, ssse3_mask(dec_a));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_slli_epi16(a, 3),+ ssse3_mask((const uint8_t[16]){248,0,0,0,0,248,0,0,0,0,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_slli_epi16(a, 4),+ ssse3_mask((const uint8_t[16]){0,0,240,0,0,0,0,240,0,0,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_slli_epi16(a, 5),+ ssse3_mask((const uint8_t[16]){0,0,0,0,224,0,0,0,0,224,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_slli_epi16(a, 6),+ ssse3_mask((const uint8_t[16]){0,192,0,0,0,0,192,0,0,0,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_slli_epi16(a, 7),+ ssse3_mask((const uint8_t[16]){0,0,0,128,0,0,0,0,128,0,0,0,0,0,0,0})));+ const __m128i b = _mm_shuffle_epi8(value, ssse3_mask(dec_b));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_srli_epi16(b, 2),+ ssse3_mask((const uint8_t[16]){7,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_srli_epi16(b, 1),+ ssse3_mask((const uint8_t[16]){0,0,15,0,0,0,0,15,0,0,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ b,+ ssse3_mask((const uint8_t[16]){0,0,0,0,31,0,0,0,0,31,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_slli_epi16(b, 1),+ ssse3_mask((const uint8_t[16]){0,62,0,0,0,0,62,0,0,0,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_slli_epi16(b, 2),+ ssse3_mask((const uint8_t[16]){0,0,0,124,0,0,0,0,124,0,0,0,0,0,0,0})));+ const __m128i c = _mm_shuffle_epi8(value, ssse3_mask(dec_c));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_srli_epi16(c, 4),+ ssse3_mask((const uint8_t[16]){0,15,0,0,0,0,15,0,0,0,0,0,0,0,0,0})));+ output = _mm_or_si128(output, _mm_and_si128(+ _mm_srli_epi16(c, 3),+ ssse3_mask((const uint8_t[16]){0,0,0,7,0,0,0,0,7,0,0,0,0,0,0,0})));+ uint8_t bytes[16];+ _mm_storeu_si128((__m128i *)bytes, output);+ memcpy(dst + 0, bytes + 0, 10);+ dst += 10;+ done += 16;+ }+ return done;+}++__attribute__((target("avx2")))+static __m256i avx2_mask(const uint8_t *bytes)+{+ const __m128i value = _mm_loadu_si128((const __m128i *)bytes);+ return _mm256_broadcastsi128_si256(value);+}++__attribute__((target("avx2")))+static size_t avx2_encode(const uint8_t *src, size_t len, uint8_t *dst, int hex)+{+ size_t done = 0;+ while (len - done >= 20) {+ uint8_t bytes[32] = {0};+ memcpy(bytes + 0, src + done + 0, 10);+ memcpy(bytes + 16, src + done + 10, 10);+ const __m256i value = _mm256_loadu_si256((const __m256i *)bytes);+ __m256i output = _mm256_setzero_si256();+ const __m256i a = _mm256_shuffle_epi8(value, avx2_mask(enc_a));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_srli_epi16(a, 3),+ avx2_mask((const uint8_t[16]){31,0,0,0,0,0,0,0,31,0,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_srli_epi16(a, 2),+ avx2_mask((const uint8_t[16]){0,0,0,0,0,31,0,0,0,0,0,0,0,31,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_srli_epi16(a, 1),+ avx2_mask((const uint8_t[16]){0,0,31,0,0,0,0,0,0,0,31,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ a,+ avx2_mask((const uint8_t[16]){0,0,0,0,0,0,0,31,0,0,0,0,0,0,0,31})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_slli_epi16(a, 1),+ avx2_mask((const uint8_t[16]){0,0,0,0,30,0,0,0,0,0,0,0,30,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_slli_epi16(a, 2),+ avx2_mask((const uint8_t[16]){0,28,0,0,0,0,0,0,0,28,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_slli_epi16(a, 3),+ avx2_mask((const uint8_t[16]){0,0,0,0,0,0,24,0,0,0,0,0,0,0,24,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_slli_epi16(a, 4),+ avx2_mask((const uint8_t[16]){0,0,0,16,0,0,0,0,0,0,0,16,0,0,0,0})));+ const __m256i b = _mm256_shuffle_epi8(value, avx2_mask(enc_b));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_srli_epi16(b, 7),+ avx2_mask((const uint8_t[16]){0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_srli_epi16(b, 6),+ avx2_mask((const uint8_t[16]){0,3,0,0,0,0,0,0,0,3,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_srli_epi16(b, 5),+ avx2_mask((const uint8_t[16]){0,0,0,0,0,0,7,0,0,0,0,0,0,0,7,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_srli_epi16(b, 4),+ avx2_mask((const uint8_t[16]){0,0,0,15,0,0,0,0,0,0,0,15,0,0,0,0})));+ const __m256i high = _mm256_cmpgt_epi8(output, _mm256_set1_epi8(hex ? 9 : 25));+ const __m256i offset = _mm256_or_si256(+ _mm256_and_si256(high, _mm256_set1_epi8(hex ? 55 : 24)),+ _mm256_andnot_si256(high, _mm256_set1_epi8(hex ? 48 : 65)));+ _mm256_storeu_si256((__m256i *)dst, _mm256_add_epi8(output, offset));+ dst += 32;+ done += 20;+ }+ return done;+}++__attribute__((target("avx2")))+static size_t avx2_decode(const uint8_t *src, size_t len, uint8_t *dst,+ int hex, int checked)+{+ size_t done = 0;+ while (len - done >= 32) {+ const __m256i input = _mm256_loadu_si256((const __m256i *)(src + done));+ const __m256i upper = _mm256_and_si256(input, _mm256_set1_epi8((char)0xdf));+ const __m256i letters = _mm256_and_si256(+ _mm256_cmpgt_epi8(upper, _mm256_set1_epi8('A' - 1)),+ _mm256_cmpgt_epi8(_mm256_set1_epi8(hex ? 'V' + 1 : 'Z' + 1), upper));+ const __m256i digits = _mm256_and_si256(+ _mm256_cmpgt_epi8(input, _mm256_set1_epi8(hex ? '0' - 1 : '2' - 1)),+ _mm256_cmpgt_epi8(_mm256_set1_epi8(hex ? '9' + 1 : '7' + 1), input));+ if (checked && (unsigned)_mm256_movemask_epi8(_mm256_or_si256(letters, digits))+ != 0xffffffffU) {+ break;+ }+ const __m256i value = _mm256_or_si256(+ _mm256_and_si256(letters,+ _mm256_add_epi8(upper, _mm256_set1_epi8(hex ? 10 - 'A' : -'A'))),+ _mm256_and_si256(digits,+ _mm256_add_epi8(input, _mm256_set1_epi8(hex ? -'0' : 26 - '2'))));+ __m256i output = _mm256_setzero_si256();+ const __m256i a = _mm256_shuffle_epi8(value, avx2_mask(dec_a));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_slli_epi16(a, 3),+ avx2_mask((const uint8_t[16]){248,0,0,0,0,248,0,0,0,0,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_slli_epi16(a, 4),+ avx2_mask((const uint8_t[16]){0,0,240,0,0,0,0,240,0,0,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_slli_epi16(a, 5),+ avx2_mask((const uint8_t[16]){0,0,0,0,224,0,0,0,0,224,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_slli_epi16(a, 6),+ avx2_mask((const uint8_t[16]){0,192,0,0,0,0,192,0,0,0,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_slli_epi16(a, 7),+ avx2_mask((const uint8_t[16]){0,0,0,128,0,0,0,0,128,0,0,0,0,0,0,0})));+ const __m256i b = _mm256_shuffle_epi8(value, avx2_mask(dec_b));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_srli_epi16(b, 2),+ avx2_mask((const uint8_t[16]){7,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_srli_epi16(b, 1),+ avx2_mask((const uint8_t[16]){0,0,15,0,0,0,0,15,0,0,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ b,+ avx2_mask((const uint8_t[16]){0,0,0,0,31,0,0,0,0,31,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_slli_epi16(b, 1),+ avx2_mask((const uint8_t[16]){0,62,0,0,0,0,62,0,0,0,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_slli_epi16(b, 2),+ avx2_mask((const uint8_t[16]){0,0,0,124,0,0,0,0,124,0,0,0,0,0,0,0})));+ const __m256i c = _mm256_shuffle_epi8(value, avx2_mask(dec_c));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_srli_epi16(c, 4),+ avx2_mask((const uint8_t[16]){0,15,0,0,0,0,15,0,0,0,0,0,0,0,0,0})));+ output = _mm256_or_si256(output, _mm256_and_si256(+ _mm256_srli_epi16(c, 3),+ avx2_mask((const uint8_t[16]){0,0,0,7,0,0,0,0,7,0,0,0,0,0,0,0})));+ uint8_t bytes[32];+ _mm256_storeu_si256((__m256i *)bytes, output);+ memcpy(dst + 0, bytes + 0, 10);+ memcpy(dst + 10, bytes + 16, 10);+ dst += 20;+ done += 32;+ }+ return done;+}++#endif++int hs_base32_simd_backend(void)+{+#if defined(HS_BASE32_NEON)+ return 2;+#elif defined(HS_BASE32_X86)+#if !defined(HS_BASE32_SIMD_FORCE_SSSE3)+ /* Compiler CPU queries include OS support for AVX register state. */+ if (__builtin_cpu_supports("avx2")) {+ return 3;+ }+#endif+ return __builtin_cpu_supports("ssse3") ? 1 : 0;+#else+ return 0;+#endif+}++size_t hs_base32_simd_encode(const uint8_t *src, size_t len, uint8_t *dst, int hex)+{+#if defined(HS_BASE32_NEON)+ return neon_encode(src, len, dst, hex);+#elif defined(HS_BASE32_X86)+ const int backend = hs_base32_simd_backend();+ if (backend == 3) {+ return avx2_encode(src, len, dst, hex);+ }+ if (backend == 1) {+ return ssse3_encode(src, len, dst, hex);+ }+#endif+ (void)src;+ (void)len;+ (void)dst;+ (void)hex;+ return 0;+}++size_t hs_base32_simd_decode(const uint8_t *src, size_t len, uint8_t *dst, int hex)+{+#if defined(HS_BASE32_NEON)+ return neon_decode(src, len, dst, hex, 1);+#elif defined(HS_BASE32_X86)+ const int backend = hs_base32_simd_backend();+ if (backend == 3) {+ return avx2_decode(src, len, dst, hex, 1);+ }+ if (backend == 1) {+ return ssse3_decode(src, len, dst, hex, 1);+ }+#endif+ (void)src;+ (void)len;+ (void)dst;+ (void)hex;+ return 0;+}++/* Typed callers establish validity before entering these loops. */+size_t hs_base32_simd_decode_typed(const uint8_t *src, size_t len,+ uint8_t *dst, int hex)+{+#if defined(HS_BASE32_NEON)+ return neon_decode(src, len, dst, hex, 0);+#elif defined(HS_BASE32_X86)+ const int backend = hs_base32_simd_backend();+ if (backend == 3) {+ return avx2_decode(src, len, dst, hex, 0);+ }+ if (backend == 1) {+ return ssse3_decode(src, len, dst, hex, 0);+ }+#endif+ (void)src;+ (void)len;+ (void)dst;+ (void)hex;+ return 0;+}
+ cbits/base32_simd.h view
@@ -0,0 +1,15 @@+/* Copyright (c) 2026 Emily Pillmore. BSD-3-Clause; see LICENSE. */+#ifndef HS_BASE32_SIMD_H+#define HS_BASE32_SIMD_H+#include <stddef.h>+#include <stdint.h>+/* Backends: 0 scalar fallback, 1 SSSE3, 2 NEON, 3 AVX2. */+int hs_base32_simd_backend(void);+/* Return consumed input bytes. No tail, padding, or invalid block is written.+ * Source and destination must be distinct, nonoverlapping buffers; destination+ * capacities are len/5*8 (encode) and len/8*5 (decode). hex is 0 or 1. */+size_t hs_base32_simd_encode(const uint8_t *, size_t, uint8_t *, int hex);+size_t hs_base32_simd_decode(const uint8_t *, size_t, uint8_t *, int hex);+/* Known-valid symbols only, with trailing padding excluded. No validation. */+size_t hs_base32_simd_decode_typed(const uint8_t *, size_t, uint8_t *, int hex);+#endif
+ src/Data/Base32/Types.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE ExplicitForAll #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE RoleAnnotations #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE Trustworthy #-}+-- |+-- Module : Data.Base32.Types+-- Copyright : (c) 2026 Emily Pillmore+-- License : BSD-style+--+-- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>+-- Stability : stable+-- Portability : non-portable+--+-- Encoded values tagged with their alphabet and padding policy.+--+-- @since 1.0+--+module Data.Base32.Types+ ( Alphabet (..)+ , StdAlphabet+ , HexAlphabet+ , Base32+ , extractBase32+ , assertBase32+ ) where++import Control.DeepSeq (NFData)+import Data.Functor.Identity (Identity (..))+import Data.Kind (Constraint)+import GHC.TypeLits (ErrorMessage (..), TypeError)++-- | The alphabets and padding forms from RFC 4648 sections 6 and 7.+--+-- @since 1.0+--+data Alphabet = StdPadded | StdUnpadded | HexPadded | HexUnpadded+ deriving+ ( Eq -- ^ @since 1.0+ , Ord -- ^ @since 1.0+ , Show -- ^ @since 1.0+ )++-- | Padded and unpadded forms of the standard alphabet.+--+-- @since 1.0+--+type family StdAlphabet (k :: Alphabet) :: Constraint where+ StdAlphabet 'StdPadded = ()+ StdAlphabet 'StdUnpadded = ()+ StdAlphabet _ = TypeError+ ( 'Text "Cannot prove base32 value is encoded using the standard alphabet. "+ ':<>: 'Text "Use the standard encoders or padding-specific parsers."+ )++-- | Padded and unpadded forms of the extended-hex alphabet.+--+-- >>> let hex :: HexAlphabet k => Base32 k a -> a; hex = extractBase32+-- >>> hex (assertBase32 @'HexPadded "ADQMS===")+-- "ADQMS==="+-- >>> hex (assertBase32 @'HexUnpadded "ADQMS")+-- "ADQMS"+--+-- @since 1.0+--+type family HexAlphabet (k :: Alphabet) :: Constraint where+ HexAlphabet 'HexPadded = ()+ HexAlphabet 'HexUnpadded = ()+ HexAlphabet _ = TypeError+ ( 'Text "Cannot prove base32 value is encoded using the extended-hex alphabet. "+ ':<>: 'Text "Use the hex encoders or padding-specific parsers."+ )++-- | A value encoded with the given alphabet and padding. Encoding and parsing establish+-- this provenance. 'assertBase32', record updates, and the derived mapping+-- instances do not validate their results.+--+-- @since 1.0+--+type role Base32 nominal representational+newtype Base32 (alphabet :: Alphabet) a = Base32+ { extractBase32 :: a+ -- ^ Extract the encoded representation without decoding it.+ --+ -- @since 1.0+ }+ deriving+ ( Eq -- ^ @since 1.0+ , Ord -- ^ @since 1.0+ , Show -- ^ @since 1.0+ , NFData -- ^ @since 1.0+ ) via a+ deriving+ ( Functor -- ^ @since 1.0+ , Applicative -- ^ @since 1.0+ , Monad -- ^ @since 1.0+ ) via Identity++-- | Assert that a value is Base32-encoded. +--+-- @since 1.0+--+assertBase32 :: forall alphabet a. a -> Base32 alphabet a+assertBase32 = Base32+{-# INLINE assertBase32 #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> :seti -XDataKinds -XTypeApplications -XTypeFamilies -XFlexibleContexts
src/Data/ByteString/Base32.hs view
@@ -1,10 +1,11 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE MagicHash #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Trustworthy #-} -- | -- Module : Data.ByteString.Base32--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -23,22 +24,27 @@ , encodeBase32Unpadded , encodeBase32Unpadded' -- * Decoding+, decodeBase32Untyped+, decodeBase32UnpaddedUntyped+, decodeBase32PaddedUntyped+ -- * Validation , decodeBase32-, decodeBase32Unpadded , decodeBase32Padded--- , decodeBase32Lenient- -- * Validation-, isBase32-, isValidBase32+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where +import Data.Base32.Types+ import qualified Data.ByteString as BS import Data.ByteString.Internal (ByteString(..))-import Data.ByteString.Base32.Internal+import Data.ByteString.Base32.Internal.Head import Data.ByteString.Base32.Internal.Tables-import Data.Either (isRight)+import Data.ByteString.Base32.Internal.Utils (validateLastNPads) import Data.Text (Text)+import qualified Data.Text as Text import qualified Data.Text.Encoding as T import System.IO.Unsafe (unsafeDupablePerformIO)@@ -53,8 +59,10 @@ -- >>> encodeBase32 "Sun" -- "KN2W4===" ---encodeBase32 :: ByteString -> Text-encodeBase32 = T.decodeUtf8 . encodeBase32'+-- @since 1.0+--+encodeBase32 :: ByteString -> Base32 'StdPadded Text+encodeBase32 = fmap T.decodeUtf8 . encodeBase32' {-# INLINE encodeBase32 #-} -- | Encode a 'ByteString' value as a Base32 'ByteString' value with padding.@@ -66,8 +74,10 @@ -- >>> encodeBase32' "Sun" -- "KN2W4===" ---encodeBase32' :: ByteString -> ByteString-encodeBase32' = encodeBase32_ "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"#+-- @since 1.0+--+encodeBase32' :: ByteString -> Base32 'StdPadded ByteString+encodeBase32' = assertBase32 . encodeBase32_ "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"# {-# INLINE encodeBase32' #-} -- | Decode an arbitrarily padded Base32-encoded 'ByteString' value. If its length@@ -79,27 +89,33 @@ -- -- === __Examples__: ----- >>> decodeBase32 "KN2W4==="+-- >>> decodeBase32Untyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32 "KN2W4"+-- >>> decodeBase32Untyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32 "KN2W==="+-- >>> decodeBase32Untyped "KN2W===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: ByteString -> Either Text ByteString-decodeBase32 bs@(BS _ !l)+-- @since 1.0+--+decodeBase32Untyped :: ByteString -> Either Text ByteString+decodeBase32Untyped bs@(BS _ !l) | l == 0 = Right bs | r == 0 = unsafeDupablePerformIO $ decodeBase32_ stdDecodeTable bs- | r == 2 = unsafeDupablePerformIO $ decodeBase32_ stdDecodeTable (BS.append bs "======")- | r == 4 = validateLastNPads 2 bs $ decodeBase32_ stdDecodeTable (BS.append bs "====")- | r == 5 = validateLastNPads 3 bs $ decodeBase32_ stdDecodeTable (BS.append bs "===")- | r == 7 = validateLastNPads 5 bs $ decodeBase32_ stdDecodeTable (BS.append bs "=")- | otherwise = Left "Base32-encoded bytestring has invalid size"+ | r == 2 = unsafeDupablePerformIO $+ decodeBase32_ stdDecodeTable (BS.append bs (BS.replicate 6 0x3d))+ | r == 4 = validateLastNPads 2 bs $+ decodeBase32_ stdDecodeTable (BS.append bs (BS.replicate 4 0x3d))+ | r == 5 = validateLastNPads 3 bs $+ decodeBase32_ stdDecodeTable (BS.append bs (BS.replicate 3 0x3d))+ | r == 7 = validateLastNPads 5 bs $+ decodeBase32_ stdDecodeTable (BS.append bs (BS.replicate 1 0x3d))+ | otherwise = Left (Text.pack "Base32-encoded bytestring has invalid size") where !r = l `rem` 8-{-# INLINE decodeBase32 #-}+{-# INLINE decodeBase32Untyped #-} -- | Encode a 'ByteString' value as a Base32 'Text' value without padding. --@@ -110,8 +126,10 @@ -- >>> encodeBase32Unpadded "Sun" -- "KN2W4" ---encodeBase32Unpadded :: ByteString -> Text-encodeBase32Unpadded = T.decodeUtf8 . encodeBase32Unpadded'+-- @since 1.0+--+encodeBase32Unpadded :: ByteString -> Base32 'StdUnpadded Text+encodeBase32Unpadded = fmap T.decodeUtf8 . encodeBase32Unpadded' {-# INLINE encodeBase32Unpadded #-} -- | Encode a 'ByteString' value as a Base32 'ByteString' value without padding.@@ -123,8 +141,11 @@ -- >>> encodeBase32Unpadded' "Sun" -- "KN2W4" ---encodeBase32Unpadded' :: ByteString -> ByteString-encodeBase32Unpadded' = encodeBase32NoPad_ "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"#+-- @since 1.0+--+encodeBase32Unpadded' :: ByteString -> Base32 'StdUnpadded ByteString+encodeBase32Unpadded' = assertBase32+ . encodeBase32NoPad_ "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"# {-# INLINE encodeBase32Unpadded' #-} -- | Decode an unpadded Base32-encoded 'ByteString' value.@@ -133,24 +154,30 @@ -- -- === __Examples__: ----- >>> decodeBase32Unpadded "KN2W4"+-- >>> decodeBase32UnpaddedUntyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32Unpadded "KN2W4==="+-- >>> decodeBase32UnpaddedUntyped "KN2W4===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: ByteString -> Either Text ByteString-decodeBase32Unpadded bs@(BS _ !l)+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: ByteString -> Either Text ByteString+decodeBase32UnpaddedUntyped bs@(BS _ !l) | l == 0 = Right bs | r == 0 = validateLastNPads 1 bs $ decodeBase32_ stdDecodeTable bs- | r == 2 = unsafeDupablePerformIO $ decodeBase32_ stdDecodeTable (BS.append bs "======")- | r == 4 = validateLastNPads 1 bs $ decodeBase32_ stdDecodeTable (BS.append bs "====")- | r == 5 = validateLastNPads 1 bs $ decodeBase32_ stdDecodeTable (BS.append bs "===")- | r == 7 = validateLastNPads 1 bs $ decodeBase32_ stdDecodeTable (BS.append bs "=")- | otherwise = Left "Base32-encoded bytestring has invalid size"+ | r == 2 = unsafeDupablePerformIO $+ decodeBase32_ stdDecodeTable (BS.append bs (BS.replicate 6 0x3d))+ | r == 4 = validateLastNPads 1 bs $+ decodeBase32_ stdDecodeTable (BS.append bs (BS.replicate 4 0x3d))+ | r == 5 = validateLastNPads 1 bs $+ decodeBase32_ stdDecodeTable (BS.append bs (BS.replicate 3 0x3d))+ | r == 7 = validateLastNPads 1 bs $+ decodeBase32_ stdDecodeTable (BS.append bs (BS.replicate 1 0x3d))+ | otherwise = Left (Text.pack "Base32-encoded bytestring has invalid size") where !r = l `rem` 8-{-# INLINE decodeBase32Unpadded #-}+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Decode a padded Base32-encoded 'ByteString' value. --@@ -158,68 +185,109 @@ -- -- === __Examples__: ----- >>> decodeBase32Padded "KN2W4==="+-- >>> decodeBase32PaddedUntyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32Padded "KN2W4"+-- >>> decodeBase32PaddedUntyped "KN2W4" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: ByteString -> Either Text ByteString-decodeBase32Padded bs@(BS _ !l)+-- @since 1.0+--+decodeBase32PaddedUntyped :: ByteString -> Either Text ByteString+decodeBase32PaddedUntyped bs@(BS _ !l) | l == 0 = Right bs- | r == 1 = Left "Base32-encoded bytestring has invalid size"- | r == 3 = Left "Base32-encoded bytestring has invalid size"- | r == 6 = Left "Base32-encoded bytestring has invalid size"- | r /= 0 = Left "Base32-encoded bytestring requires padding"+ | r == 1 = Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r == 3 = Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r == 6 = Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r /= 0 = Left (Text.pack "Base32-encoded bytestring requires padding") | otherwise = unsafeDupablePerformIO $ decodeBase32_ stdDecodeTable bs where !r = l `rem` 8-{-# INLINE decodeBase32Padded #-}+{-# INLINE decodeBase32PaddedUntyped #-} --- -- | Leniently decode an unpadded Base32-encoded 'ByteString' value. This function--- -- will not generate parse errors. If input data contains padding chars,--- -- then the input will be parsed up until the first pad character.--- ----- -- __Note:__ This is not RFC 4648-compliant.--- ----- decodeBase32Lenient :: ByteString -> ByteString--- decodeBase32Lenient = decodeBase32Lenient_ decodeB32Table--- {-# INLINE decodeBase32Lenient #-}+-- | Decode a typed Base32-encoded 'ByteString' value.+--+-- This function is padding-agnostic.+--+-- >>> decodeBase32 (encodeBase32' "Sun")+-- "Sun"+--+-- >>> decodeBase32 (encodeBase32Unpadded' "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: StdAlphabet k => Base32 k ByteString -> ByteString+decodeBase32 = decodeBase32Typed_ stdDecodeTable+{-# INLINE decodeBase32 #-} --- | Tell whether a 'ByteString' value is encoded in padded or unpadded Base32 format+-- | Decode a typed, padded Base32-encoded 'ByteString' value. ----- === __Examples__:+-- >>> decodeBase32Padded (encodeBase32' "Sun")+-- "Sun" ----- >>> isBase32 "KN2W4"--- True+-- @since 1.0 ----- >>> isBase32 "KN2W4==="--- True+decodeBase32Padded :: Base32 'StdPadded ByteString -> ByteString+decodeBase32Padded = decodeBase32+{-# INLINE decodeBase32Padded #-}++-- | Parse and validate a base32-encoded value, assuming that it has padding. ----- >>> isBase32 "KN2W4=="--- False+-- >>> parseBase32Padded "kn2w4==="+-- Right "kn2w4===" ---isBase32 :: ByteString -> Bool-isBase32 bs = isValidBase32 bs && isRight (decodeBase32 bs)-{-# INLINE isBase32 #-}+-- >>> fmap decodeBase32Padded (parseBase32Padded "KN2W4===")+-- Right "Sun"+--+-- >>> parseBase32Padded ""+-- Right ""+--+-- >>> parseBase32Padded "KN2W4"+-- Left "Base32-encoded bytestring requires padding"+--+-- @since 1.0+--+parseBase32Padded :: ByteString -> Either Text (Base32 'StdPadded ByteString)+parseBase32Padded value = assertBase32 value+ <$ (decodeBase32PaddedUntyped) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'ByteString' value is a valid Base32 format.+-- | Decode a typed, unpadded Base32-encoded value. ----- This will not tell you whether or not this is a correct Base32 representation,--- only that it conforms to the correct shape (including padding/size etc.).--- To check whether it is a true Base32 encoded 'ByteString' value, use 'isBase32'.+-- >>> decodeBase32Unpadded (encodeBase32Unpadded' "Sun")+-- "Sun" ----- === __Examples__:+-- @since 1.0 ----- >>> isValidBase32 "KN2W4"--- True+decodeBase32Unpadded :: Base32 'StdUnpadded ByteString -> ByteString+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32-encoded value, assuming that it is unpadded. ----- >>> isValidBase32 "KN2W4="--- False+-- >>> parseBase32Unpadded "kn2w4"+-- Right "kn2w4" ----- >>> isValidBase32 "KN2W4%"--- False+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "KN2W4")+-- Right "Sun" ---isValidBase32 :: ByteString -> Bool-isValidBase32 = validateBase32 "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"-{-# INLINE isValidBase32 #-}+-- >>> parseBase32Unpadded ""+-- Right ""+--+-- >>> parseBase32Unpadded "KN2W4==="+-- Left "Base32-encoded bytestring has invalid padding"+--+-- >>> isLeft (parseBase32Unpadded "MZ")+-- True+--+-- @since 1.0+--+parseBase32Unpadded :: ByteString -> Either Text (Base32 'StdUnpadded ByteString)+parseBase32Unpadded value = assertBase32 value+ <$ (decodeBase32UnpaddedUntyped) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import Data.Either (isLeft)
src/Data/ByteString/Base32/Hex.hs view
@@ -1,10 +1,11 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE MagicHash #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Trustworthy #-} -- | -- Module : Data.ByteString.Base32.Hex--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -23,22 +24,27 @@ , encodeBase32Unpadded , encodeBase32Unpadded' -- * Decoding+, decodeBase32Untyped+, decodeBase32UnpaddedUntyped+, decodeBase32PaddedUntyped+ -- * Validation , decodeBase32-, decodeBase32Unpadded , decodeBase32Padded--- , decodeBase32Lenient- -- * Validation-, isBase32Hex-, isValidBase32Hex+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where +import Data.Base32.Types+ import qualified Data.ByteString as BS import Data.ByteString.Internal (ByteString(..))-import Data.ByteString.Base32.Internal+import Data.ByteString.Base32.Internal.Head import Data.ByteString.Base32.Internal.Tables-import Data.Either (isRight)+import Data.ByteString.Base32.Internal.Utils (validateLastNPads) import Data.Text (Text)+import qualified Data.Text as Text import qualified Data.Text.Encoding as T import System.IO.Unsafe (unsafeDupablePerformIO)@@ -53,8 +59,10 @@ -- >>> encodeBase32 "Sun" -- "ADQMS===" ---encodeBase32 :: ByteString -> Text-encodeBase32 = T.decodeUtf8 . encodeBase32'+-- @since 1.0+--+encodeBase32 :: ByteString -> Base32 'HexPadded Text+encodeBase32 = fmap T.decodeUtf8 . encodeBase32' {-# INLINE encodeBase32 #-} -- | Encode a 'ByteString' value as a Base32hex 'ByteString' value with padding.@@ -66,8 +74,10 @@ -- >>> encodeBase32' "Sun" -- "ADQMS===" ---encodeBase32' :: ByteString -> ByteString-encodeBase32' = encodeBase32_ "0123456789ABCDEFGHIJKLMNOPQRSTUV"#+-- @since 1.0+--+encodeBase32' :: ByteString -> Base32 'HexPadded ByteString+encodeBase32' = assertBase32 . encodeBase32_ "0123456789ABCDEFGHIJKLMNOPQRSTUV"# {-# INLINE encodeBase32' #-} -- | Decode an arbitrarily padded Base32hex-encoded 'ByteString' value. If its length@@ -79,27 +89,33 @@ -- -- === __Examples__: ----- >>> decodeBase32 "ADQMS==="+-- >>> decodeBase32Untyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32 "ADQMS"+-- >>> decodeBase32Untyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32 "ADQM==="+-- >>> decodeBase32Untyped "ADQM===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: ByteString -> Either Text ByteString-decodeBase32 bs@(BS _ !l)+-- @since 1.0+--+decodeBase32Untyped :: ByteString -> Either Text ByteString+decodeBase32Untyped bs@(BS _ !l) | l == 0 = Right bs | r == 0 = unsafeDupablePerformIO $ decodeBase32_ hexDecodeTable bs- | r == 2 = unsafeDupablePerformIO $ decodeBase32_ hexDecodeTable (BS.append bs "======")- | r == 4 = validateLastNPads 2 bs $ decodeBase32_ hexDecodeTable (BS.append bs "====")- | r == 5 = validateLastNPads 3 bs $ decodeBase32_ hexDecodeTable (BS.append bs "===")- | r == 7 = validateLastNPads 5 bs $ decodeBase32_ hexDecodeTable (BS.append bs "=")- | otherwise = Left "Base32-encoded bytestring has invalid size"+ | r == 2 = unsafeDupablePerformIO $+ decodeBase32_ hexDecodeTable (BS.append bs (BS.replicate 6 0x3d))+ | r == 4 = validateLastNPads 2 bs $+ decodeBase32_ hexDecodeTable (BS.append bs (BS.replicate 4 0x3d))+ | r == 5 = validateLastNPads 3 bs $+ decodeBase32_ hexDecodeTable (BS.append bs (BS.replicate 3 0x3d))+ | r == 7 = validateLastNPads 5 bs $+ decodeBase32_ hexDecodeTable (BS.append bs (BS.replicate 1 0x3d))+ | otherwise = Left (Text.pack "Base32-encoded bytestring has invalid size") where !r = l `rem` 8-{-# INLINE decodeBase32 #-}+{-# INLINE decodeBase32Untyped #-} -- | Encode a 'ByteString' value as a Base32hex 'Text' value without padding. --@@ -107,11 +123,13 @@ -- -- === __Examples__: ----- >>> encodeBase32Unpadded' "Sun"+-- >>> encodeBase32Unpadded "Sun" -- "ADQMS" ---encodeBase32Unpadded :: ByteString -> Text-encodeBase32Unpadded = T.decodeUtf8 . encodeBase32Unpadded'+-- @since 1.0+--+encodeBase32Unpadded :: ByteString -> Base32 'HexUnpadded Text+encodeBase32Unpadded = fmap T.decodeUtf8 . encodeBase32Unpadded' {-# INLINE encodeBase32Unpadded #-} -- | Encode a 'ByteString' value as a Base32hex 'ByteString' value without padding.@@ -123,8 +141,11 @@ -- >>> encodeBase32Unpadded' "Sun" -- "ADQMS" ---encodeBase32Unpadded' :: ByteString -> ByteString-encodeBase32Unpadded' = encodeBase32NoPad_ "0123456789ABCDEFGHIJKLMNOPQRSTUV"#+-- @since 1.0+--+encodeBase32Unpadded' :: ByteString -> Base32 'HexUnpadded ByteString+encodeBase32Unpadded' = assertBase32+ . encodeBase32NoPad_ "0123456789ABCDEFGHIJKLMNOPQRSTUV"# {-# INLINE encodeBase32Unpadded' #-} -- | Decode an unpadded Base32hex-encoded 'ByteString' value.@@ -133,24 +154,30 @@ -- -- === __Examples__: ----- >>> decodeBase32Unpadded "ADQMS"+-- >>> decodeBase32UnpaddedUntyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32Unpadded "ADQMS==="+-- >>> decodeBase32UnpaddedUntyped "ADQMS===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: ByteString -> Either Text ByteString-decodeBase32Unpadded bs@(BS _ !l)+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: ByteString -> Either Text ByteString+decodeBase32UnpaddedUntyped bs@(BS _ !l) | l == 0 = Right bs | r == 0 = validateLastNPads 1 bs $ decodeBase32_ hexDecodeTable bs- | r == 2 = unsafeDupablePerformIO $ decodeBase32_ hexDecodeTable (BS.append bs "======")- | r == 4 = validateLastNPads 1 bs $ decodeBase32_ hexDecodeTable (BS.append bs "====")- | r == 5 = validateLastNPads 1 bs $ decodeBase32_ hexDecodeTable (BS.append bs "===")- | r == 7 = validateLastNPads 1 bs $ decodeBase32_ hexDecodeTable (BS.append bs "=")- | otherwise = Left "Base32-encoded bytestring has invalid size"+ | r == 2 = unsafeDupablePerformIO $+ decodeBase32_ hexDecodeTable (BS.append bs (BS.replicate 6 0x3d))+ | r == 4 = validateLastNPads 1 bs $+ decodeBase32_ hexDecodeTable (BS.append bs (BS.replicate 4 0x3d))+ | r == 5 = validateLastNPads 1 bs $+ decodeBase32_ hexDecodeTable (BS.append bs (BS.replicate 3 0x3d))+ | r == 7 = validateLastNPads 1 bs $+ decodeBase32_ hexDecodeTable (BS.append bs (BS.replicate 1 0x3d))+ | otherwise = Left (Text.pack "Base32-encoded bytestring has invalid size") where !r = l `rem` 8-{-# INLINE decodeBase32Unpadded #-}+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Decode a padded Base32hex-encoded 'ByteString' value. --@@ -158,58 +185,104 @@ -- -- === __Examples__: ----- >>> decodeBase32Padded "ADQMS==="+-- >>> decodeBase32PaddedUntyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32Padded "ADQMS"+-- >>> decodeBase32PaddedUntyped "ADQMS" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: ByteString -> Either Text ByteString-decodeBase32Padded bs@(BS _ !l)+-- @since 1.0+--+decodeBase32PaddedUntyped :: ByteString -> Either Text ByteString+decodeBase32PaddedUntyped bs@(BS _ !l) | l == 0 = Right bs- | r == 1 = Left "Base32-encoded bytestring has invalid size"- | r == 3 = Left "Base32-encoded bytestring has invalid size"- | r == 6 = Left "Base32-encoded bytestring has invalid size"- | r /= 0 = Left "Base32-encoded bytestring requires padding"+ | r == 1 = Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r == 3 = Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r == 6 = Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r /= 0 = Left (Text.pack "Base32-encoded bytestring requires padding") | otherwise = unsafeDupablePerformIO $ decodeBase32_ hexDecodeTable bs where !r = l `rem` 8+{-# INLINE decodeBase32PaddedUntyped #-}++-- | Decode a typed Base32hex-encoded 'ByteString' value. +-- +-- This function is padding-agnostic.+--+-- >>> decodeBase32 (encodeBase32' "Sun")+-- "Sun"+-- >>> decodeBase32 (encodeBase32Unpadded' "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: HexAlphabet k => Base32 k ByteString -> ByteString+decodeBase32 = decodeBase32Typed_ hexDecodeTable+{-# INLINE decodeBase32 #-}++-- | Decode a typed, padded Base32hex-encoded 'ByteString' value +--+-- >>> decodeBase32Padded (encodeBase32' "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32Padded :: Base32 'HexPadded ByteString -> ByteString+decodeBase32Padded = decodeBase32 {-# INLINE decodeBase32Padded #-} --- | Tell whether a 'ByteString' value is encoded in padded or unpadded Base32hex format+-- | Parse and validate a base32hex-encoded value, assuming that it has padding. ----- === __Examples__:+-- >>> parseBase32Padded "adqms==="+-- Right "adqms===" ----- >>> isBase32Hex "ADQMS"--- True+-- >>> fmap decodeBase32Padded (parseBase32Padded "ADQMS===")+-- Right "Sun"+-- +-- >>> parseBase32Padded ""+-- Right "" ----- >>> isBase32Hex "ADQMS==="--- True+-- >>> parseBase32Padded "ADQMS"+-- Left "Base32-encoded bytestring requires padding" ----- >>> isBase32Hex "ADQMS=="--- False+-- @since 1.0 ---isBase32Hex :: ByteString -> Bool-isBase32Hex bs = isValidBase32Hex bs && isRight (decodeBase32 bs)-{-# INLINE isBase32Hex #-}+parseBase32Padded :: ByteString -> Either Text (Base32 'HexPadded ByteString)+parseBase32Padded value = assertBase32 value+ <$ (decodeBase32PaddedUntyped) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'ByteString' value is a valid Base32hex format.+-- | Decode a typed, unpadded Base32hex-encoded value. ----- This will not tell you whether or not this is a correct Base32hex representation,--- only that it conforms to the correct shape (including padding/size etc.).--- To check whether it is a true Base32hex encoded 'ByteString' value, use 'isBase32'.+-- >>> decodeBase32Unpadded (encodeBase32Unpadded' "Sun")+-- "Sun" ----- === __Examples__:+-- @since 1.0 ----- >>> isValidBase32Hex "ADQMS"--- True+decodeBase32Unpadded :: Base32 'HexUnpadded ByteString -> ByteString+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32hex-encoded value, assuming that is unpadded. ----- >>> isValidBase32Hex "ADQMS="--- False+-- >>> parseBase32Unpadded "adqms"+-- Right "adqms"+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "ADQMS")+-- Right "Sun"+-- >>> parseBase32Unpadded ""+-- Right ""+-- >>> parseBase32Unpadded "ADQMS==="+-- Left "Base32-encoded bytestring has invalid padding"+-- >>> isLeft (parseBase32Unpadded "CP")+-- True ----- >>> isValidBase32Hex "ADQMS%"--- False+-- @since 1.0 ---isValidBase32Hex :: ByteString -> Bool-isValidBase32Hex = validateBase32 "0123456789ABCDEFGHIJKLMNOPQRSTUV"-{-# INLINE isValidBase32Hex #-}+parseBase32Unpadded :: ByteString -> Either Text (Base32 'HexUnpadded ByteString)+parseBase32Unpadded value = assertBase32 value+ <$ (decodeBase32UnpaddedUntyped) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import Data.Either (isLeft)
− src/Data/ByteString/Base32/Internal.hs
@@ -1,120 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE OverloadedStrings #-}--- |--- Module : Data.ByteString.Base32.Internal--- Copyright : (c) 2020 Emily Pillmore--- License : BSD-style------ Maintainer : Emily Pillmore <emilypi@cohomolo.gy>--- Stability : Experimental--- Portability : portable------ Internal module defining the encoding and decoding--- processes and tables.----module Data.ByteString.Base32.Internal-( encodeBase32_-, encodeBase32NoPad_-, decodeBase32_-, validateBase32-, validateLastNPads-) where---import qualified Data.ByteString as BS-import Data.ByteString.Internal-import Data.ByteString.Base32.Internal.Head-import Data.Text (Text)--import Foreign.ForeignPtr-import Foreign.Ptr-import Foreign.Storable--import GHC.Word--import System.IO.Unsafe---- -------------------------------------------------------------------------- ----- Validating Base32---- | Validate a base32-encoded bytestring against some alphabet.----validateBase32 :: ByteString -> ByteString -> Bool-validateBase32 !alphabet bs@(BS _ l)- | l == 0 = True- | r == 0 = f bs- | r == 2 = f (BS.append bs "======")- | r == 4 = f (BS.append bs "====")- | r == 5 = f (BS.append bs "===")- | r == 7 = f (BS.append bs "=")- | otherwise = False- where- r = l `rem` 8-- f (BS fp l') = accursedUnutterablePerformIO $ withForeignPtr fp $ \p ->- go p (plusPtr p l')-- go !p !end- | p == end = return True- | otherwise = do- w <- peek p-- let check a- | a == 0x3d, plusPtr p 1 == end = True- | a == 0x3d, plusPtr p 2 == end = True- | a == 0x3d, plusPtr p 3 == end = True- | a == 0x3d, plusPtr p 4 == end = True- | a == 0x3d, plusPtr p 5 == end = True- | a == 0x3d, plusPtr p 6 == end = True- | a == 0x3d = False- | otherwise = BS.elem a alphabet-- if check w then go (plusPtr p 1) end else return False-{-# INLINE validateBase32 #-}---- | This function checks that the last N-chars of a bytestring are '='--- and, if true, fails with a message or completes some io action.------ This is necessary to check when decoding permissively (i.e. filling in padding chars).--- Consider the following 8 cases of a string of length l:------ - @l = 0 mod 8@: No pad chars are added, since the input is assumed to be good.--- - @l = 1 mod 8@: Never an admissible length in base32--- - @l = 2 mod 8@: 6 padding chars are added. If padding chars are present in the string, they will fail as to decode as final quanta--- - @l = 3 mod 8@: Never an admissible length in base32--- - @l = 4 mod 8@: 4 padding chars are added. If 2 padding chars are present in the string this can be "completed" in the sense that--- it now acts like a string `l == 2 mod 8` with 6 padding chars, and could potentially form corrupted data.--- - @l = 5 mod 8@: 3 padding chars are added. If 3 padding chars are present in the string, this could form corrupted data like in the--- previous case.--- - @l = 6 mod 8@: Never an admissible length in base32--- - @l = 7 mod 8@: 1 padding char is added. If 5 padding chars are present in the string, this could form corrupted data like the--- previous cases.------ Hence, permissive decodes should only fill in padding chars when it makes sense to add them. That is,--- if an input is degenerate, it should never succeed when we add padding chars. We need the following invariant to hold:------ @--- B32.decodeUnpadded <|> B32.decodePadded ~ B32.decode--- @----validateLastNPads- :: Int- -> ByteString- -> IO (Either Text ByteString)- -> Either Text ByteString-validateLastNPads !n (BS !fp !l) io- | not valid = Left "Base32-encoded bytestring has invalid padding"- | otherwise = unsafeDupablePerformIO io- where- valid = accursedUnutterablePerformIO $ withForeignPtr fp $ \p -> do- let end = plusPtr p l-- let go :: Ptr Word8 -> IO Bool- go !q- | q == end = return True- | otherwise = do- a <- peek q- if a == 0x3d then return False else go (plusPtr q 1)-- go (plusPtr p (l - n))-{-# INLINE validateLastNPads #-}
src/Data/ByteString/Base32/Internal/Head.hs view
@@ -1,17 +1,37 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE MagicHash #-} {-# LANGUAGE TypeApplications #-}+-- |+-- Module : Data.ByteString.Base32.Internal.Head+-- Copyright : (c) 2020-2026 Emily Pillmore+-- License : BSD-style+--+-- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>+-- Stability : Experimental+-- Portability : portable+--+-- Allocation and entry points for the strict encoding and decoding loops.+--+-- @since 1.0+-- module Data.ByteString.Base32.Internal.Head ( encodeBase32_ , encodeBase32NoPad_ , decodeBase32_+, decodeBase32Typed_ ) where +import Prelude hiding (length, null)+import Data.Base32.Types (Base32, extractBase32)+import qualified Data.ByteString as BS+import Data.ByteString (length, null) import Data.ByteString.Internal+import Data.ByteString.Base32.Internal.Utils (encodedLength) import Data.ByteString.Base32.Internal.Loop import Data.ByteString.Base32.Internal.Tail import Data.Text (Text)+import qualified Data.Text as Text import Foreign.Ptr import Foreign.ForeignPtr@@ -29,6 +49,8 @@ -- allocates the correct number of bytes that will be written, and -- executes the inner encoding loop against that data. --+-- @since 1.0+-- encodeBase32_ :: Addr# -> ByteString -> ByteString encodeBase32_ !lut (BS !sfp !l) = unsafeDupablePerformIO $ do dfp <- mallocPlainForeignPtrBytes dlen@@ -39,7 +61,7 @@ (castPtr dptr) sptr end (loopTail lut dfp dptr end) where- !dlen = ceiling (fromIntegral @_ @Double l / 5) * 8+ !dlen = encodedLength l -- | Head of the unpadded base32 encoding loop. --@@ -47,6 +69,8 @@ -- allocates the correct number of bytes that will be written, and -- executes the inner encoding loop against that data. --+-- @since 1.0+-- encodeBase32NoPad_ :: Addr# -> ByteString -> ByteString encodeBase32NoPad_ !lut (BS !sfp !l) = unsafeDupablePerformIO $ do !dfp <- mallocPlainForeignPtrBytes dlen@@ -57,7 +81,7 @@ (castPtr dptr) sptr end (loopTailNoPad lut dfp dptr end) where- !dlen = ceiling (fromIntegral @_ @Double l / 5) * 8+ !dlen = encodedLength l -- | Head of the base32 decoding loop. --@@ -65,12 +89,37 @@ -- bytestring, allocates the correct number of bytes that will be written, -- and executes the inner decoding loop against that data. --+-- @since 1.0+-- decodeBase32_ :: Ptr Word8 -> ByteString -> IO (Either Text ByteString)-decodeBase32_ (Ptr !dtable) (BS !sfp !slen) =+decodeBase32_ table bs+ | null bs = pure (Right bs)+ | length bs `rem` 8 /= 0 =+ pure (Left (Text.pack "Base32 input requires complete quanta"))+ | otherwise = decodeComplete table bs++decodeComplete :: Ptr Word8 -> ByteString -> IO (Either Text ByteString)+decodeComplete (Ptr !dtable) (BS !sfp !slen) = withForeignPtr sfp $ \sptr -> do dfp <- mallocPlainForeignPtrBytes dlen withForeignPtr dfp $ \dptr -> do let !end = plusPtr sptr slen decodeLoop dtable dfp dptr sptr end where- !dlen = ceiling (fromIntegral @_ @Double slen / 1.6)+ !dlen = (slen `quot` 8) * 5++-- | Decode established Base32 provenance without validation.+-- Padding contributes no payload; partial final quanta need no copying.+--+-- @since 1.0+--+decodeBase32Typed_ :: Ptr Word8 -> Base32 alphabet ByteString -> ByteString+decodeBase32Typed_ (Ptr table) encoded = unsafeDupablePerformIO $ do+ dfp <- mallocPlainForeignPtrBytes dlen+ withForeignPtr dfp $ \dst ->+ withForeignPtr sfp $ \src -> decodeLoopTyped table dst src slen+ pure (BS dfp dlen)+ where+ BS sfp slen = BS.dropWhileEnd (== 61) (extractBase32 encoded)+ dlen = (slen `quot` 8) * 5 + (slen `rem` 8) * 5 `quot` 8+{-# INLINE decodeBase32Typed_ #-}
src/Data/ByteString/Base32/Internal/Loop.hs view
@@ -1,11 +1,31 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE MagicHash #-} {-# LANGUAGE TypeApplications #-}+-- |+-- Module : Data.ByteString.Base32.Internal.Loop+-- Copyright : (c) 2020-2026 Emily Pillmore+-- License : BSD-style+--+-- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>+-- Stability : Experimental+-- Portability : portable+--+-- Encoding loops, checked and typed decoding loops, and quantum validation.+--+-- @since 1.0+-- module Data.ByteString.Base32.Internal.Loop ( innerLoop , decodeLoop+, decodeLoopTyped+, decodeQuantum ) where +#ifdef BASE32_SIMD+import qualified Data.ByteString.Base32.Internal.SIMD as SIMD+#endif+ import Data.Bits import Data.ByteString.Internal (ByteString(..)) import Data.ByteString.Base32.Internal.Utils@@ -16,6 +36,7 @@ import Foreign.ForeignPtr import Foreign.Storable +import GHC.ByteOrder import GHC.Exts import GHC.Word @@ -23,6 +44,10 @@ -- ------------------------------------------------------------------------ -- -- Encoding loops +-- | Encode complete quanta, then run the supplied tail handler.+--+-- @since 1.0+-- innerLoop :: Addr# -> Ptr Word64@@ -30,7 +55,14 @@ -> Ptr Word8 -> (Ptr Word8 -> Ptr Word8 -> IO ByteString) -> IO ByteString-innerLoop !lut !dptr !sptr !end finish = go dptr sptr+innerLoop !lut !dptr !sptr !end finish = do+#ifdef BASE32_SIMD+ consumed <- SIMD.encodePrefix lut sptr+ (end `minusPtr` sptr) (castPtr dptr)+ go (plusPtr dptr (consumed `quot` 5 * 8)) (plusPtr sptr consumed)+#else+ go dptr sptr+#endif where lix a = w64 (aix (fromIntegral a .&. 0x1f) lut) {-# INLINE lix #-}@@ -59,13 +91,19 @@ .|. unsafeShiftL g 48 .|. unsafeShiftL h 56 - poke dst w+ poke dst (case targetByteOrder of+ LittleEndian -> w+ BigEndian -> byteSwap64 w) go (plusPtr dst 8) (plusPtr src 5) {-# INLINE innerLoop #-} -- ------------------------------------------------------------------------ -- -- Decoding loops +-- | Decode and validate complete quanta, including the final padding.+--+-- @since 1.0+-- decodeLoop :: Addr# -> ForeignPtr Word8@@ -73,132 +111,155 @@ -> Ptr Word8 -> Ptr Word8 -> IO (Either Text ByteString)-decodeLoop !lut !dfp !dptr !sptr !end = go dptr sptr+decodeLoop !lut !dfp !dptr !sptr !end = do+#ifdef BASE32_SIMD+ consumed <- SIMD.decodePrefix lut sptr+ (max 0 (end `minusPtr` sptr - 8)) dptr+ go (plusPtr dptr (consumed `quot` 8 * 5)) (plusPtr sptr consumed)+#else+ go dptr sptr+#endif where- lix a = w64 (aix (fromIntegral a) lut)-- err :: Ptr Word8 -> IO (Either Text ByteString)- err p = return . Left . T.pack- $ "invalid character at offset: "- ++ show (p `minusPtr` sptr)-- padErr :: Ptr Word8 -> IO (Either Text ByteString)- padErr p = return . Left . T.pack- $ "invalid padding at offset: "- ++ show (p `minusPtr` sptr)-- look :: Ptr Word8 -> IO Word64- look !p = lix <$> peek @Word8 p-+ look value = w64 (aix (fromIntegral value) lut) go !dst !src- | plusPtr src 8 >= end = do-- a <- look src- b <- look (plusPtr src 1)- c <- look (plusPtr src 2)- d <- look (plusPtr src 3)- e <- look (plusPtr src 4)- f <- look (plusPtr src 5)- g <- look (plusPtr src 6)- h <- look (plusPtr src 7)- finalChunk dst src a b c d e f g h-- | otherwise = do- !t <- peekWord64BE (castPtr src)-- let a = lix (unsafeShiftR t 56)- b = lix (unsafeShiftR t 48)- c = lix (unsafeShiftR t 40)- d = lix (unsafeShiftR t 32)- e = lix (unsafeShiftR t 24)- f = lix (unsafeShiftR t 16)- g = lix (unsafeShiftR t 8)- h = lix t-- decodeChunk dst src a b c d e f g h-- finalChunk !dst !src !a !b !c !d !e !f !g !h- | a == 0x63 = padErr src- | b == 0x63 = padErr (plusPtr src 1)- | a == 0xff = err src- | b == 0xff = err (plusPtr src 1)- | c == 0xff = err (plusPtr src 2)- | d == 0xff = err (plusPtr src 3)- | e == 0xff = err (plusPtr src 4)- | f == 0xff = err (plusPtr src 5)- | g == 0xff = err (plusPtr src 6)- | h == 0xff = err (plusPtr src 7)- | otherwise = do-- let !o1 = (fromIntegral a `unsafeShiftL` 3) .|. (fromIntegral b `unsafeShiftR` 2)- !o2 = (fromIntegral b `unsafeShiftL` 6)- .|. (fromIntegral c `unsafeShiftL` 1)- .|. (fromIntegral d `unsafeShiftR` 4)- !o3 = (fromIntegral d `unsafeShiftL` 4) .|. (fromIntegral e `unsafeShiftR` 1)- !o4 = (fromIntegral e `unsafeShiftL` 7)- .|. (fromIntegral f `unsafeShiftL` 2)- .|. (fromIntegral g `unsafeShiftR` 3)- !o5 = (fromIntegral g `unsafeShiftL` 5) .|. fromIntegral h-- poke @Word8 dst o1- poke @Word8 (plusPtr dst 1) o2-- case (c,d,e,f,g,h) of- (0x63,0x63,0x63,0x63,0x63,0x63) ->- return (Right (BS dfp (1 + minusPtr dst dptr)))- (0x63,_,_,_,_,_) -> padErr (plusPtr src 3)- (_,0x63,0x63,0x63,0x63,0x63) -> padErr (plusPtr src 3)- (_,0x63,_,_,_,_) -> padErr (plusPtr src 4)- (_,_,0x63,0x63,0x63,0x63) -> do- poke @Word8 (plusPtr dst 2) o3- return (Right (BS dfp (2 + minusPtr dst dptr)))- (_,_,0x63,_,_,_) -> padErr (plusPtr src 5)- (_,_,_,0x63,0x63,0x63) -> do- poke @Word8 (plusPtr dst 2) o3- poke @Word8 (plusPtr dst 3) o4- return (Right (BS dfp (3 + minusPtr dst dptr)))- (_,_,_,0x63,_,_) -> padErr (plusPtr src 6)- (_,_,_,_,0x63,0x63) -> padErr (plusPtr src 6)- (_,_,_,_,0x63,_) -> padErr (plusPtr src 7)- (_,_,_,_,_,0x63) -> do- poke @Word8 (plusPtr dst 2) o3- poke @Word8 (plusPtr dst 3) o4- poke @Word8 (plusPtr dst 4) o5- return (Right (BS dfp (4 + minusPtr dst dptr)))- (_,_,_,_,_,_) -> do- poke @Word8 (plusPtr dst 2) o3- poke @Word8 (plusPtr dst 3) o4- poke @Word8 (plusPtr dst 4) o5- return (Right (BS dfp (5 + minusPtr dst dptr)))-- decodeChunk !dst !src !a !b !c !d !e !f !g !h- | a == 0x63 = padErr src- | b == 0x63 = padErr (plusPtr src 1)- | c == 0x63 = padErr (plusPtr src 2)- | d == 0x63 = padErr (plusPtr src 3)- | e == 0x63 = padErr (plusPtr src 4)- | f == 0x63 = padErr (plusPtr src 5)- | g == 0x63 = padErr (plusPtr src 6)- | h == 0x63 = padErr (plusPtr src 7)- | a == 0xff = err src- | b == 0xff = err (plusPtr src 1)- | c == 0xff = err (plusPtr src 2)- | d == 0xff = err (plusPtr src 3)- | e == 0xff = err (plusPtr src 4)- | f == 0xff = err (plusPtr src 5)- | g == 0xff = err (plusPtr src 6)- | h == 0xff = err (plusPtr src 7)+ | src == end = pure (Right (BS dfp (dst `minusPtr` dptr))) | otherwise = do+ t <- peekWord64BE (castPtr src)+ let !a = look (unsafeShiftR t 56)+ !b = look (unsafeShiftR t 48)+ !c = look (unsafeShiftR t 40)+ !d = look (unsafeShiftR t 32)+ !e = look (unsafeShiftR t 24)+ !f = look (unsafeShiftR t 16)+ !g = look (unsafeShiftR t 8)+ !h = look t+ final = plusPtr src 8 == end+ if not final && (a .|. b .|. c .|. d .|. e .|. f .|. g .|. h) < 32+ then do+ writeFull dst (pack a b c d e f g h)+ go (plusPtr dst 5) (plusPtr src 8)+ else case decodeQuantum final (src `minusPtr` sptr) a b c d e f g h of+ Left err -> pure (Left err)+ Right count -> do+ let clean v = if v == 0x63 then 0 else v+ w = pack a b (clean c) (clean d) (clean e)+ (clean f) (clean g) (clean h)+ write i+ | i == count = pure ()+ | otherwise = do+ poke @Word8 (plusPtr dst i)+ (fromIntegral (unsafeShiftR w (32 - 8 * i)))+ write (i + 1)+ write 0+ if final+ then pure (Right (BS dfp (dst `minusPtr` dptr + count)))+ else go (plusPtr dst 5) (plusPtr src 8)+ pack a b c d e f g h =+ unsafeShiftL a 35 .|. unsafeShiftL b 30+ .|. unsafeShiftL c 25 .|. unsafeShiftL d 20+ .|. unsafeShiftL e 15 .|. unsafeShiftL f 10+ .|. unsafeShiftL g 5 .|. h+ writeFull dst w = do+ let first = fromIntegral (w `unsafeShiftR` 8) :: Word32+ poke (castPtr dst) (case targetByteOrder of+ LittleEndian -> byteSwap32 first+ BigEndian -> first)+ poke @Word8 (plusPtr dst 4) (fromIntegral w)+{-# INLINE decodeLoop #-} - let !w = (unsafeShiftL a 35- .|. unsafeShiftL b 30- .|. unsafeShiftL c 25- .|. unsafeShiftL d 20- .|. unsafeShiftL e 15- .|. unsafeShiftL f 10- .|. unsafeShiftL g 5- .|. h) :: Word64+-- | Validate a decoded quantum before any output is written. Return its+-- payload length. Both pointer and byte-array loops use the same rules.+--+-- @since 1.0+--+decodeQuantum+ :: Bool -> Int+ -> Word64 -> Word64 -> Word64 -> Word64+ -> Word64 -> Word64 -> Word64 -> Word64+ -> Either Text Int+decodeQuantum !final !offset !a !b !c !d !e !f !g !h+ | a == 0x63 = padErr 0+ | b == 0x63 = padErr 1+ | not final, c == 0x63 = padErr 2+ | not final, d == 0x63 = padErr 3+ | not final, e == 0x63 = padErr 4+ | not final, f == 0x63 = padErr 5+ | not final, g == 0x63 = padErr 6+ | not final, h == 0x63 = padErr 7+ | a == 0xff = charErr 0+ | b == 0xff = charErr 1+ | c == 0xff = charErr 2+ | d == 0xff = charErr 3+ | e == 0xff = charErr 4+ | f == 0xff = charErr 5+ | g == 0xff = charErr 6+ | h == 0xff = charErr 7+ | not final = Right 5+ | otherwise = case (c,d,e,f,g,h) of+ (0x63,0x63,0x63,0x63,0x63,0x63) -> canonical b 3 1 1+ (0x63,_,_,_,_,_) -> padErr 3+ (_,0x63,0x63,0x63,0x63,0x63) -> padErr 3+ (_,0x63,_,_,_,_) -> padErr 4+ (_,_,0x63,0x63,0x63,0x63) -> canonical d 15 3 2+ (_,_,0x63,_,_,_) -> padErr 5+ (_,_,_,0x63,0x63,0x63) -> canonical e 1 4 3+ (_,_,_,0x63,_,_) -> padErr 6+ (_,_,_,_,0x63,0x63) -> padErr 6+ (_,_,_,_,0x63,_) -> padErr 7+ (_,_,_,_,_,0x63) -> canonical g 7 6 4+ _ -> Right 5+ where+ failure message i = Left (T.pack (message ++ show (offset + i)))+ padErr = failure "invalid padding at offset: "+ charErr = failure "invalid character at offset: "+ canonical value mask i count+ | value .&. mask == 0 = Right count+ | otherwise = failure "non-canonical encoding at offset: " i+{-# INLINE decodeQuantum #-} - poke @Word32 (castPtr dst) (byteSwap32 (fromIntegral (unsafeShiftR w 8)))- poke @Word8 (plusPtr dst 4) (fromIntegral w)- go (plusPtr dst 5) (plusPtr src 8)+-- | Decode known-valid symbols. Alphabet, size, padding placement and unused+-- bits have already been established by the producer of the typed value.+-- The caller excludes trailing padding from the source length.+--+-- @since 1.0+--+decodeLoopTyped :: Addr# -> Ptr Word8 -> Ptr Word8 -> Int -> IO ()+decodeLoopTyped table dptr sptr slen = do+#ifdef BASE32_SIMD+ consumed <- SIMD.decodeTypedPrefix table sptr slen dptr+#else+ let consumed = 0+#endif+ go (plusPtr dptr (consumed `quot` 8 * 5)) consumed+ where+ look value = w64 (aix (fromIntegral value) table)+ go !dst !i+ | slen - i >= 8 = do+ t <- peekWord64BE (castPtr (plusPtr sptr i))+ let w = pack (look (t `unsafeShiftR` 56)) (look (t `unsafeShiftR` 48))+ (look (t `unsafeShiftR` 40)) (look (t `unsafeShiftR` 32))+ (look (t `unsafeShiftR` 24)) (look (t `unsafeShiftR` 16))+ (look (t `unsafeShiftR` 8)) (look t)+ first = fromIntegral (w `unsafeShiftR` 8) :: Word32+ poke (castPtr dst) (case targetByteOrder of+ LittleEndian -> byteSwap32 first+ BigEndian -> first)+ poke @Word8 (plusPtr dst 4) (fromIntegral w)+ go (plusPtr dst 5) (i + 8)+ | otherwise = tailBytes dst i 0 0 (0 :: Word16)+ tailBytes !dst !i !bits !j !buffer+ | bits >= 8 = do+ let rest = bits - 8+ poke @Word8 (plusPtr dst j) (fromIntegral (buffer `unsafeShiftR` rest))+ tailBytes dst i rest (j + 1) buffer+ | i < slen = do+ symbol <- peek @Word8 (plusPtr sptr i)+ tailBytes dst (i + 1) (bits + 5) j+ (buffer `unsafeShiftL` 5 .|. fromIntegral (aix symbol table))+ | otherwise = pure ()+ pack a b c d e f g h =+ a `unsafeShiftL` 35 .|. b `unsafeShiftL` 30+ .|. c `unsafeShiftL` 25 .|. d `unsafeShiftL` 20+ .|. e `unsafeShiftL` 15 .|. f `unsafeShiftL` 10+ .|. g `unsafeShiftL` 5 .|. h+{-# INLINE decodeLoopTyped #-}
+ src/Data/ByteString/Base32/Internal/SIMD.hs view
@@ -0,0 +1,153 @@+{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE UnliftedFFITypes #-}+-- |+-- Module : Data.ByteString.Base32.Internal.SIMD+-- Copyright : (c) 2026 Emily Pillmore+-- License : BSD-style+--+-- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>+-- Stability : Experimental+-- Portability : portable+--+-- Bounded native prefixes. Callers retain pointer buffers with withForeignPtr.+-- Byte arrays are passed directly to unsafe FFI: GHC cannot collect during+-- the call, and the kernels retain no pointers and make no callbacks.+--+-- @since 1.0+--+module Data.ByteString.Base32.Internal.SIMD+ ( encodePrefix+ , decodePrefix+ , decodeTypedPrefix+ , encodeByteArrayPrefix+ , decodeByteArrayPrefix+ , decodeByteArrayTypedPrefix+ ) where++import Control.Monad.ST (ST)+import Control.Monad.ST.Unsafe (unsafeIOToST)+import Data.ByteString.Base32.Internal.Tables (hexDecodeTable, stdDecodeTable)+import Data.ByteString.Base32.Internal.Utils (aix)+import Data.Primitive.ByteArray (ByteArray (..), MutableByteArray (..))+import Data.Word (Word8)+import Foreign.C.Types (CInt (..), CSize (..))+import GHC.Exts (Addr#, ByteArray#, MutableByteArray#)+import GHC.Ptr (Ptr (..))++foreign import ccall unsafe "hs_base32_simd_encode"+ c_encode :: Ptr Word8 -> CSize -> Ptr Word8 -> CInt -> IO CSize+foreign import ccall unsafe "hs_base32_simd_decode"+ c_decode :: Ptr Word8 -> CSize -> Ptr Word8 -> CInt -> IO CSize+foreign import ccall unsafe "hs_base32_simd_encode"+ c_encodeArray :: ByteArray# -> CSize -> MutableByteArray# s -> CInt -> IO CSize+foreign import ccall unsafe "hs_base32_simd_decode"+ c_decodeArray :: ByteArray# -> CSize -> MutableByteArray# s -> CInt -> IO CSize++foreign import ccall unsafe "hs_base32_simd_decode_typed"+ c_decodeTyped :: Ptr Word8 -> CSize -> Ptr Word8 -> CInt -> IO CSize+foreign import ccall unsafe "hs_base32_simd_decode_typed"+ c_decodeArrayTyped :: ByteArray# -> CSize -> MutableByteArray# s -> CInt -> IO CSize++-- | Encode complete quanta. Destination capacity must be at least len/5*8.+--+-- @since 1.0+--+encodePrefix :: Addr# -> Ptr Word8 -> Int -> Ptr Word8 -> IO Int+encodePrefix alphabet src len dst+ | len < 80 = pure 0+ | Just hex <- encodingAlphabet alphabet =+ fromIntegral <$> c_encode src (fromIntegral len) dst hex+ | otherwise = pure 0+{-# INLINE encodePrefix #-}++-- | Decode valid complete quanta; leave padding/invalid blocks for scalar code.+-- Destination capacity must be at least len/8*5.+--+-- @since 1.0+--+decodePrefix :: Addr# -> Ptr Word8 -> Int -> Ptr Word8 -> IO Int+decodePrefix table src len dst+ | len < 64 = pure 0+ | Just hex <- decodingAlphabet table =+ fromIntegral <$> c_decode src (fromIntegral len) dst hex+ | otherwise = pure 0+{-# INLINE decodePrefix #-}++-- | Decode known-valid symbols without validation; excludes trailing padding.+-- Destination capacity must be at least len/8*5.+--+-- @since 1.0+--+decodeTypedPrefix :: Addr# -> Ptr Word8 -> Int -> Ptr Word8 -> IO Int+decodeTypedPrefix table src len dst+ | len < 64 = pure 0+ | Just hex <- decodingAlphabet table =+ fromIntegral <$> c_decodeTyped src (fromIntegral len) dst hex+ | otherwise = pure 0+{-# INLINE decodeTypedPrefix #-}++-- | Direct unpinned-array encoding into a distinct mutable output buffer.+--+-- @since 1.0+--+encodeByteArrayPrefix ::+ Addr# -> ByteArray -> Int -> MutableByteArray s -> ST s Int+encodeByteArrayPrefix alphabet (ByteArray src) len (MutableByteArray dst)+ | len < 40 = pure 0+ | Just hex <- encodingAlphabet alphabet =+ unsafeIOToST $+ fromIntegral <$> c_encodeArray src (fromIntegral len) dst hex+ | otherwise = pure 0+{-# INLINE encodeByteArrayPrefix #-}++-- | Direct unpinned-array decoding into a distinct mutable output buffer.+--+-- @since 1.0+--+decodeByteArrayPrefix ::+ Addr# -> ByteArray -> Int -> MutableByteArray s -> ST s Int+decodeByteArrayPrefix table (ByteArray src) len (MutableByteArray dst)+ | len < 64 = pure 0+ | Just hex <- decodingAlphabet table =+ unsafeIOToST $+ fromIntegral <$> c_decodeArray src (fromIntegral len) dst hex+ | otherwise = pure 0+{-# INLINE decodeByteArrayPrefix #-}++-- | Unchecked array decoding of known-valid symbols, excluding padding.+--+-- @since 1.0+--+decodeByteArrayTypedPrefix ::+ Addr# -> ByteArray -> Int -> MutableByteArray s -> ST s Int+decodeByteArrayTypedPrefix table (ByteArray src) len (MutableByteArray dst)+ | len < 64 = pure 0+ | Just hex <- decodingAlphabet table =+ unsafeIOToST $+ fromIntegral <$> c_decodeArrayTyped src (fromIntegral len) dst hex+ | otherwise = pure 0+{-# INLINE decodeByteArrayTypedPrefix #-}++-- Arbitrary alphabets are part of the exposed low-level API. Only exact+-- built-in alphabets can use these kernels; other alphabets stay scalar.+encodingAlphabet :: Addr# -> Maybe CInt+encodingAlphabet alphabet+ | matches "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"# = Just 0+ | matches "0123456789ABCDEFGHIJKLMNOPQRSTUV"# = Just 1+ | otherwise = Nothing+ where+ matches expected = go 0+ where+ go i+ | i == 32 = True+ | aix i alphabet == aix i expected = go (i + 1)+ | otherwise = False+{-# INLINE encodingAlphabet #-}++decodingAlphabet :: Addr# -> Maybe CInt+decodingAlphabet table+ | Ptr table == stdDecodeTable = Just 0+ | Ptr table == hexDecodeTable = Just 1+ | otherwise = Nothing+{-# INLINE decodingAlphabet #-}
+ src/Data/ByteString/Base32/Internal/Short.hs view
@@ -0,0 +1,237 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE MagicHash #-}+-- |+-- Module : Data.ByteString.Base32.Internal.Short+-- Copyright : (c) 2026 Emily Pillmore+-- License : BSD-style+--+-- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>+-- Stability : Experimental+-- Portability : portable+--+-- Direct byte-array codecs; no conversion through strict ByteString.+--+-- @since 1.0+--+module Data.ByteString.Base32.Internal.Short+ ( encodeShort+ , decodeShort+ , decodeShortTyped+ ) where++import Control.Monad.ST (runST)+import Data.Base32.Types (Base32, extractBase32)+import Data.Bits+import Data.ByteString.Base32.Internal.Loop (decodeQuantum)+import Data.ByteString.Base32.Internal.Utils (aix, encodedLength)+import Data.ByteString.Short.Internal (ShortByteString(..))+import qualified Data.ByteString.Short as SBS+import Data.Primitive.ByteArray+import Data.Text (Text)+import Data.Word+import GHC.Exts (Addr#)+import GHC.ByteOrder+import GHC.Ptr (Ptr(..))++#ifdef BASE32_SIMD+import qualified Data.ByteString.Base32.Internal.SIMD as SIMD+#endif++-- | Encode with the supplied 32-character ASCII alphabet.+--+-- @since 1.0+--+encodeShort :: Addr# -> Bool -> ShortByteString -> ShortByteString+encodeShort alphabet padded input@(SBS source) = runST $ do+ dst <- newByteArray capacity+#ifdef BASE32_SIMD+ consumed <- SIMD.encodeByteArrayPrefix alphabet src len dst+#else+ let consumed = 0+#endif+ let emit !i !j !bits !buffer+ | bits >= 5 = do+ let next = bits - 5+ writeByteArray dst j+ (aix (fromIntegral (buffer `unsafeShiftR` next) .&. 31) alphabet)+ emit i (j + 1) next buffer+ | i < len = emit (i + 1) j (bits + 8)+ ((buffer `unsafeShiftL` 8) .|. fromIntegral (byte i))+ | bits > 0 = do+ writeByteArray dst j+ (aix (fromIntegral (buffer `unsafeShiftL` (5 - bits)) .&. 31)+ alphabet)+ finish (j + 1)+ | otherwise = finish j+ finish !j+ | padded && j < capacity = do+ writeByteArray dst j (61 :: Word8)+ finish (j + 1)+ | otherwise = do+ shrinkMutableByteArray dst j+ ByteArray result <- unsafeFreezeByteArray dst+ pure (SBS result)+ let full !i !j+ | i + 5 > len = emit i j 0 (0 :: Word16)+ | otherwise = do+ let t = fromIntegral (byte i) `unsafeShiftL` 32+ .|. fromIntegral (byte (i + 1)) `unsafeShiftL` 24+ .|. fromIntegral (byte (i + 2)) `unsafeShiftL` 16+ .|. fromIntegral (byte (i + 3)) `unsafeShiftL` 8+ .|. fromIntegral (byte (i + 4)) :: Word64+ at shiftCount = fromIntegral+ (aix (fromIntegral (t `unsafeShiftR` shiftCount) .&. 31) alphabet)+ w = at 35 .|. at 30 `unsafeShiftL` 8+ .|. at 25 `unsafeShiftL` 16 .|. at 20 `unsafeShiftL` 24+ .|. at 15 `unsafeShiftL` 32 .|. at 10 `unsafeShiftL` 40+ .|. at 5 `unsafeShiftL` 48 .|. at 0 `unsafeShiftL` 56 :: Word64+ writeByteArray dst (j `quot` 8) (case targetByteOrder of+ LittleEndian -> w+ BigEndian -> byteSwap64 w)+ full (i + 5) (j + 8)+ full consumed (consumed `quot` 5 * 8)+ where+ src = ByteArray source+ len = SBS.length input+ capacity = encodedLength len+ byte i = indexByteArray src i :: Word8+{-# INLINE encodeShort #-}++-- | Decode with an ASCII lookup table. The caller checks the input length+-- and final padding before entering the loop.+--+-- @since 1.0+--+decodeShort+ :: Ptr Word8 -> ShortByteString -> Either Text ShortByteString+decodeShort (Ptr table) input@(SBS source) = runST $ do+ dst <- newByteArray capacity+#ifdef BASE32_SIMD+ consumed <- SIMD.decodeByteArrayPrefix table+ src (max 0 (len - 8)) dst+#else+ let consumed = 0+#endif+ let go !i !j+ | i + 8 >= len = final i j+ | otherwise = do+ let word = indexByteArray src (i `quot` 8) :: Word64+ t = case targetByteOrder of+ LittleEndian -> byteSwap64 word+ BigEndian -> word+ from shiftCount = fromIntegral+ (aix (fromIntegral (t `unsafeShiftR` shiftCount)) table) :: Word64+ !a = from 56+ !b = from 48+ !c = from 40+ !d = from 32+ !e = from 24+ !f = from 16+ !g = from 8+ !h = from 0+ if (a .|. b .|. c .|. d .|. e .|. f .|. g .|. h) < 32+ then do+ writeFull j (pack a b c d e f g h)+ go (i + 8) (j + 5)+ else finishQuantum False i j a b c d e f g h+ final !i !j+ | i >= len = freeze j+ | otherwise = finishQuantum True i j+ (look i) (look (i + 1)) (look (i + 2)) (look (i + 3))+ (look (i + 4)) (look (i + 5)) (look (i + 6)) (look (i + 7))+ finishQuantum lastQuantum i j a b c d e f g h =+ case decodeQuantum lastQuantum i a b c d e f g h of+ Left err -> pure (Left err)+ Right count -> do+ let clean v = if v == 0x63 then 0 else v+ w = pack a b (clean c) (clean d) (clean e)+ (clean f) (clean g) (clean h)+ write !k+ | k == count = pure ()+ | otherwise = do+ writeByteArray dst (j + k)+ (fromIntegral (w `unsafeShiftR` (32 - 8 * k)) :: Word8)+ write (k + 1)+ write 0+ if lastQuantum then freeze (j + count) else go (i + 8) (j + count)+ pack a b c d e f g h =+ unsafeShiftL a 35 .|. unsafeShiftL b 30+ .|. unsafeShiftL c 25 .|. unsafeShiftL d 20+ .|. unsafeShiftL e 15 .|. unsafeShiftL f 10+ .|. unsafeShiftL g 5 .|. h+ writeFull j w = do+ writeByteArray dst j (fromIntegral (w `unsafeShiftR` 32) :: Word8)+ writeByteArray dst (j + 1) (fromIntegral (w `unsafeShiftR` 24) :: Word8)+ writeByteArray dst (j + 2) (fromIntegral (w `unsafeShiftR` 16) :: Word8)+ writeByteArray dst (j + 3) (fromIntegral (w `unsafeShiftR` 8) :: Word8)+ writeByteArray dst (j + 4) (fromIntegral w :: Word8)+ freeze n = do+ shrinkMutableByteArray dst n+ ByteArray result <- unsafeFreezeByteArray dst+ pure (Right (SBS result))+ go consumed (consumed `quot` 8 * 5)+ where+ src = ByteArray source+ len = SBS.length input+ remainder = len `rem` 8+ capacity = (len `quot` 8) * 5 + (remainder * 5 `quot` 8)+ look i+ | i >= len = 0x63+ | otherwise = fromIntegral (aix (indexByteArray src i) table) :: Word64+{-# INLINE decodeShort #-}++-- | Decode established provenance directly from a byte array, without+-- alphabet, size, padding-policy or canonical-bit validation.+--+-- @since 1.0+--+decodeShortTyped+ :: Ptr Word8 -> Base32 alphabet ShortByteString -> ShortByteString+decodeShortTyped (Ptr table) encoded = runST $ do+ dst <- newByteArray capacity+#ifdef BASE32_SIMD+ consumed <- SIMD.decodeByteArrayTypedPrefix table src len dst+#else+ let consumed = 0+#endif+ let go !i !j+ | len - i >= 8 = do+ let word = indexByteArray src (i `quot` 8) :: Word64+ t = case targetByteOrder of+ LittleEndian -> byteSwap64 word+ BigEndian -> word+ at n = fromIntegral+ (aix (fromIntegral (t `unsafeShiftR` n)) table) :: Word64+ w = at 56 `unsafeShiftL` 35 .|. at 48 `unsafeShiftL` 30+ .|. at 40 `unsafeShiftL` 25 .|. at 32 `unsafeShiftL` 20+ .|. at 24 `unsafeShiftL` 15 .|. at 16 `unsafeShiftL` 10+ .|. at 8 `unsafeShiftL` 5 .|. at 0+ writeByteArray dst j (fromIntegral (w `unsafeShiftR` 32) :: Word8)+ writeByteArray dst (j + 1) (fromIntegral (w `unsafeShiftR` 24) :: Word8)+ writeByteArray dst (j + 2) (fromIntegral (w `unsafeShiftR` 16) :: Word8)+ writeByteArray dst (j + 3) (fromIntegral (w `unsafeShiftR` 8) :: Word8)+ writeByteArray dst (j + 4) (fromIntegral w :: Word8)+ go (i + 8) (j + 5)+ | otherwise = tailBytes i j 0 (0 :: Word16)+ tailBytes !i !j !bits !buffer+ | bits >= 8 = do+ let rest = bits - 8+ writeByteArray dst j (fromIntegral (buffer `unsafeShiftR` rest) :: Word8)+ tailBytes i (j + 1) rest buffer+ | i < len = tailBytes (i + 1) j (bits + 5)+ (buffer `unsafeShiftL` 5 .|. fromIntegral (aix (byte i) table))+ | otherwise = pure ()+ go consumed (consumed `quot` 8 * 5)+ ByteArray result <- unsafeFreezeByteArray dst+ pure (SBS result)+ where+ !input@(SBS source) = extractBase32 encoded+ src = ByteArray source+ byte i = indexByteArray src i :: Word8+ payloadEnd n+ | n > 0 && byte (n - 1) == 61 = payloadEnd (n - 1)+ | otherwise = n+ len = payloadEnd (SBS.length input)+ capacity = (len `quot` 8) * 5 + (len `rem` 8) * 5 `quot` 8+{-# INLINE decodeShortTyped #-}
src/Data/ByteString/Base32/Internal/Tables.hs view
@@ -1,4 +1,17 @@ {-# LANGUAGE TypeApplications #-}+-- |+-- Module : Data.ByteString.Base32.Internal.Tables+-- Copyright : (c) 2020-2026 Emily Pillmore+-- License : BSD-style+--+-- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>+-- Stability : Experimental+-- Portability : portable+--+-- Decoding tables for the standard and extended-hex alphabets.+--+-- @since 1.0+-- module Data.ByteString.Base32.Internal.Tables ( stdDecodeTable , hexDecodeTable@@ -11,6 +24,10 @@ import GHC.Ptr (Ptr) +-- | ASCII decoding table for the standard alphabet.+--+-- @since 1.0+-- stdDecodeTable :: Ptr Word8 stdDecodeTable = writeNPlainPtrBytes @Word8 256 [ 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff@@ -32,6 +49,10 @@ ] {-# NOINLINE stdDecodeTable #-} +-- | ASCII decoding table for the extended-hex alphabet.+--+-- @since 1.0+-- hexDecodeTable :: Ptr Word8 hexDecodeTable = writeNPlainPtrBytes @Word8 256 [ 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
src/Data/ByteString/Base32/Internal/Tail.hs view
@@ -1,5 +1,18 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE MagicHash #-}+-- |+-- Module : Data.ByteString.Base32.Internal.Tail+-- Copyright : (c) 2020-2026 Emily Pillmore+-- License : BSD-style+--+-- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>+-- Stability : Experimental+-- Portability : portable+--+-- Final quantum encoding for padded and unpadded output.+--+-- @since 1.0+-- module Data.ByteString.Base32.Internal.Tail ( loopTail , loopTailNoPad@@ -20,6 +33,8 @@ -- | Unroll final quantum encoding for base32 --+-- @since 1.0+-- loopTail :: Addr# -> ForeignPtr Word8@@ -108,6 +123,8 @@ {-# INLINE loopTail #-} -- | Unroll final quantum encoding for base32+--+-- @since 1.0 -- loopTailNoPad :: Addr#
src/Data/ByteString/Base32/Internal/Utils.hs view
@@ -1,5 +1,18 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE MagicHash #-}+-- |+-- Module : Data.ByteString.Base32.Internal.Utils+-- Copyright : (c) 2020-2026 Emily Pillmore+-- License : BSD-style+--+-- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>+-- Stability : Experimental+-- Portability : portable+--+-- Shared allocation, byte-order, table lookup, rechunking, and padding utilities.+--+-- @since 1.0+-- module Data.ByteString.Base32.Internal.Utils ( aix , peekWord32BE@@ -8,12 +21,16 @@ , w32 , w64 , w64_32+, encodedLength , writeNPlainPtrBytes+, validateLastNPads ) where import Data.ByteString (ByteString) import qualified Data.ByteString as BS+import Data.Text (Text)+import qualified Data.Text as Text import Foreign.Ptr import Foreign.Storable@@ -28,26 +45,42 @@ -- | Read 'Word8' index off alphabet addr --+-- @since 1.0+-- aix :: Word8 -> Addr# -> Word8 aix w8 alpha = W8# (indexWord8OffAddr# alpha i) where !(I# i) = fromIntegral w8 {-# INLINE aix #-} +-- | Widen a 'Word8' to a 'Word32'.+--+-- @since 1.0+-- w32 :: Word8 -> Word32 w32 = fromIntegral {-# INLINE w32 #-} +-- | Widen a 'Word32' to a 'Word64'.+--+-- @since 1.0+-- w64_32 :: Word32 -> Word64 w64_32 = fromIntegral {-# INLINE w64_32 #-} +-- | Widen a 'Word8' to a 'Word64'.+--+-- @since 1.0+-- w64 :: Word8 -> Word64 w64 = fromIntegral {-# INLINE w64 #-} -- | Allocate and fill @n@ bytes with some data --+-- @since 1.0+-- writeNPlainPtrBytes :: Storable a => Int@@ -62,12 +95,20 @@ go !p (x:xs) = poke p x >> go (plusPtr p 1) xs {-# INLINE writeNPlainPtrBytes #-} +-- | Read a 32-bit word in big-endian byte order.+--+-- @since 1.0+-- peekWord32BE :: Ptr Word32 -> IO Word32 peekWord32BE p = case targetByteOrder of LittleEndian -> byteSwap32 <$> peek p BigEndian -> peek p {-# inline peekWord32BE #-} +-- | Read a 64-bit word in big-endian byte order.+--+-- @since 1.0+-- peekWord64BE :: Ptr Word64 -> IO Word64 peekWord64BE p = case targetByteOrder of LittleEndian -> byteSwap64 <$> peek p@@ -76,6 +117,8 @@ -- | Rechunk a list of bytestrings in multiples of @n@ --+-- @since 1.0+-- reChunkN :: Int -> [ByteString] -> [ByteString] reChunkN n = go where@@ -96,3 +139,32 @@ in acc' : go cs' else accum acc' cs {-# INLINE reChunkN #-}++-- | Padded output capacity, using checked integer arithmetic.+--+-- @since 1.0+--+encodedLength :: Int -> Int+encodedLength n+ | n < 0 || blocks > maxBound `quot` 8 = error "Base32 output is too large"+ | otherwise = blocks * 8+ where+ blocks = n `quot` 5 + if n `rem` 5 == 0 then 0 else 1+{-# INLINE encodedLength #-}++-- | Reject '=' in the final @n@ bytes before running the decoder.+-- The caller selects @n@ from the input length and padding policy, preventing+-- existing padding from being accepted when missing padding is added.+--+-- @since 1.0+--+validateLastNPads+ :: Int+ -> ByteString+ -> IO (Either Text ByteString)+ -> Either Text ByteString+validateLastNPads n bs io+ | BS.elem 0x3d (BS.drop (BS.length bs - n) bs) =+ Left (Text.pack "Base32-encoded bytestring has invalid padding")+ | otherwise = unsafeDupablePerformIO io+{-# INLINE validateLastNPads #-}
src/Data/ByteString/Lazy/Base32.hs view
@@ -1,8 +1,10 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE Trustworthy #-} -- | -- Module : Data.ByteString.Lazy.Base32--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -21,30 +23,32 @@ , encodeBase32Unpadded , encodeBase32Unpadded' -- * Decoding+, decodeBase32Untyped+, decodeBase32UnpaddedUntyped+, decodeBase32PaddedUntyped+ -- * Validation , decodeBase32-, decodeBase32Unpadded , decodeBase32Padded---, decodeBase32Lenient- -- * Validation-, isBase32-, isValidBase32+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where -import Prelude hiding (all, elem) +import Data.Base32.Types+ import qualified Data.ByteString as BS import qualified Data.ByteString.Base32 as B32 import Data.ByteString.Base32.Internal.Utils (reChunkN)-import Data.ByteString.Lazy (elem, fromChunks, toChunks)+import Data.ByteString.Lazy (fromChunks, toChunks) import Data.ByteString.Lazy.Internal (ByteString(..))-import Data.Either (isRight) import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL --- | Encode a 'ByteString' value as a Base32 'Text' value with padding.+-- | Encode a 'ByteString' value as a Base32 'TL.Text' value with padding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> --@@ -53,8 +57,10 @@ -- >>> encodeBase32 "Sun" -- "KN2W4===" ---encodeBase32 :: ByteString -> TL.Text-encodeBase32 = TL.decodeUtf8 . encodeBase32'+-- @since 1.0+--+encodeBase32 :: ByteString -> Base32 'StdPadded TL.Text+encodeBase32 = fmap TL.decodeUtf8 . encodeBase32' {-# INLINE encodeBase32 #-} -- | Encode a 'ByteString' as a Base32 'ByteString' value with padding.@@ -63,40 +69,46 @@ -- -- === __Examples__: ----- >>> encodeBase32 "Sun"+-- >>> encodeBase32' "Sun" -- "KN2W4===" ---encodeBase32' :: ByteString -> ByteString-encodeBase32' = fromChunks- . fmap B32.encodeBase32'+-- @since 1.0+--+encodeBase32' :: ByteString -> Base32 'StdPadded ByteString+encodeBase32' = assertBase32 . fromChunks+ . fmap (extractBase32 . B32.encodeBase32') . reChunkN 5 . toChunks --- | Decode an arbitrarily padded Base32 encoded 'ByteString' value. If its length is not a multiple--- of 4, then padding chars will be added to fill out the input to a multiple of--- 4 for safe decoding as Base32-encoded values are optionally padded.+-- | Decode an arbitrarily padded Base32 encoded 'ByteString' value. If its length is not+-- a multiple+-- of 8, then padding chars will be added to fill out the input to a multiple of+-- 8 for safe decoding as Base32-encoded values are optionally padded. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32 "KN2W4==="+-- >>> decodeBase32Untyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32 "KN2W4"+-- >>> decodeBase32Untyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32 "KN2W==="+-- >>> decodeBase32Untyped "KN2W===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: ByteString -> Either T.Text ByteString-decodeBase32 = fmap (fromChunks . (:[]))- . B32.decodeBase32+-- @since 1.0+--+decodeBase32Untyped :: ByteString -> Either T.Text ByteString+decodeBase32Untyped = fmap (fromChunks . (:[]))+ . B32.decodeBase32Untyped . BS.concat . toChunks-{-# INLINE decodeBase32 #-}+{-# INLINE decodeBase32Untyped #-} --- | Encode a 'ByteString' value as Base32 'Text' without padding. Note that for Base32,+-- | Encode a 'ByteString' value as Base32 'TL.Text' without padding.+-- Note that for Base32, -- padding is optional. If you call this function, you will simply be encoding -- as Base32 and stripping padding chars from the output. --@@ -107,8 +119,10 @@ -- >>> encodeBase32Unpadded "Sun" -- "KN2W4" ---encodeBase32Unpadded :: ByteString -> TL.Text-encodeBase32Unpadded = TL.decodeUtf8 . encodeBase32Unpadded'+-- @since 1.0+--+encodeBase32Unpadded :: ByteString -> Base32 'StdUnpadded TL.Text+encodeBase32Unpadded = fmap TL.decodeUtf8 . encodeBase32Unpadded' {-# INLINE encodeBase32Unpadded #-} -- | Encode a 'ByteString' value as Base32 without padding. Note that for Base32,@@ -122,9 +136,11 @@ -- >>> encodeBase32Unpadded' "Sun" -- "KN2W4" ---encodeBase32Unpadded' :: ByteString -> ByteString-encodeBase32Unpadded' = fromChunks- . fmap B32.encodeBase32Unpadded'+-- @since 1.0+--+encodeBase32Unpadded' :: ByteString -> Base32 'StdUnpadded ByteString+encodeBase32Unpadded' = assertBase32 . fromChunks+ . fmap (extractBase32 . B32.encodeBase32Unpadded') . reChunkN 5 . toChunks @@ -133,111 +149,139 @@ -- confirm. -- -- In general, unless unpadded Base32 is explicitly required, it is--- safer to call 'decodeBase32'.+-- safer to call 'decodeBase32Untyped'. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Unpadded "KN2W4"+-- >>> decodeBase32UnpaddedUntyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32Unpadded "KN2W4==="+-- >>> decodeBase32UnpaddedUntyped "KN2W4===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: ByteString -> Either T.Text ByteString-decodeBase32Unpadded = fmap (fromChunks . (:[]))- . B32.decodeBase32Unpadded+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: ByteString -> Either T.Text ByteString+decodeBase32UnpaddedUntyped = fmap (fromChunks . (:[]))+ . B32.decodeBase32UnpaddedUntyped . BS.concat . toChunks-{-# INLINE decodeBase32Unpadded #-}+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Decode a padded Base32-encoded 'ByteString' value. Input strings are -- required to be correctly padded, and will be validated prior to decoding -- to confirm. -- -- In general, unless padded Base32 is explicitly required, it is--- safer to call 'decodeBase32'.+-- safer to call 'decodeBase32Untyped'. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Padded "KN2W4==="+-- >>> decodeBase32PaddedUntyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32Padded "KN2W4"+-- >>> decodeBase32PaddedUntyped "KN2W4" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: ByteString -> Either T.Text ByteString-decodeBase32Padded = fmap (fromChunks . (:[]))- . B32.decodeBase32Padded+-- @since 1.0+--+decodeBase32PaddedUntyped :: ByteString -> Either T.Text ByteString+decodeBase32PaddedUntyped = fmap (fromChunks . (:[]))+ . B32.decodeBase32PaddedUntyped . BS.concat . toChunks-{-# INLINE decodeBase32Padded #-}+{-# INLINE decodeBase32PaddedUntyped #-} --- -- | Leniently decode an unpadded Base32-encoded 'ByteString'. This function--- -- will not generate parse errors. If input data contains padding chars,--- -- then the input will be parsed up until the first pad character.--- ----- -- __Note:__ This is not RFC 4648-compliant.--- ----- -- === __Examples__:--- ----- -- >>> decodeBase32Lenient "PDw_Pj4="--- -- "<<?>>"--- ----- -- >>> decodeBase32Lenient "PDw_%%%$}Pj4"--- -- "<<?>>"--- ----- decodeBase32Lenient :: ByteString -> ByteString--- decodeBase32Lenient = fromChunks--- . fmap B32.decodeBase32Lenient--- . reChunkN 8--- . fmap (BS.filter (flip elem "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567="))--- . toChunks--- {-# INLINE decodeBase32Lenient #-}+-- | Decode a typed Base32-encoded 'ByteString' value.+--+-- This function is padding-agnostic.+--+-- >>> decodeBase32 (encodeBase32' "Sun")+-- "Sun"+--+-- >>> decodeBase32 (encodeBase32Unpadded' "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: forall k. StdAlphabet k => Base32 k ByteString -> ByteString+decodeBase32 = fromChunks+ . fmap (B32.decodeBase32+ . (assertBase32 :: BS.ByteString -> Base32 k BS.ByteString))+ . reChunkN 8 . toChunks . extractBase32+{-# INLINE decodeBase32 #-} --- | Tell whether a 'ByteString' is Base32-encoded.+-- | Decode a typed, padded Base32-encoded 'ByteString' value. ----- === __Examples__:+-- >>> decodeBase32Padded (encodeBase32' "Sun")+-- "Sun" ----- >>> isBase32 "KN2W4"--- True+-- @since 1.0 ----- >>> isBase32 "KN2W4==="--- True+decodeBase32Padded :: Base32 'StdPadded ByteString -> ByteString+decodeBase32Padded = decodeBase32+{-# INLINE decodeBase32Padded #-}++-- | Parse and validate a base32-encoded value, assuming that it has padding. ----- >>> isBase32 "KN2W4=="--- False+-- >>> parseBase32Padded "kn2w4==="+-- Right "kn2w4===" ---isBase32 :: ByteString -> Bool-isBase32 bs = isValidBase32 bs && isRight (decodeBase32 bs)-{-# INLINE isBase32 #-}+-- >>> fmap decodeBase32Padded (parseBase32Padded "KN2W4===")+-- Right "Sun"+--+-- >>> parseBase32Padded ""+-- Right ""+--+-- >>> parseBase32Padded "KN2W4"+-- Left "Base32-encoded bytestring requires padding"+--+-- @since 1.0+--+parseBase32Padded :: ByteString -> Either T.Text (Base32 'StdPadded ByteString)+parseBase32Padded value = assertBase32 value+ <$ (decodeBase32PaddedUntyped) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'ByteString' is a valid Base32 format.+-- | Decode a typed, unpadded Base32-encoded value. ----- This will not tell you whether or not this is a correct Base32 representation,--- only that it conforms to the correct shape. To check whether it is a true--- Base32 encoded 'ByteString' value, use 'isBase32'.+-- >>> decodeBase32Unpadded (encodeBase32Unpadded' "Sun")+-- "Sun" ----- === __Examples__:+-- @since 1.0 ----- >>> isValidBase32 "KN2W4"--- True+decodeBase32Unpadded :: Base32 'StdUnpadded ByteString -> ByteString+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32-encoded value, assuming that it is unpadded. ----- >>> isValidBase32 "KN2W4="--- False+-- >>> parseBase32Unpadded "kn2w4"+-- Right "kn2w4" ----- >>> isValidBase32 "KN2W4%"--- False+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "KN2W4")+-- Right "Sun" ---isValidBase32 :: ByteString -> Bool-isValidBase32 = go . toChunks- where- go [] = True- go [c] = B32.isValidBase32 c- go (c:cs) = -- note the lack of padding char- BS.all (flip elem "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567") c- && go cs-{-# INLINE isValidBase32 #-}+-- >>> parseBase32Unpadded ""+-- Right ""+--+-- >>> parseBase32Unpadded "KN2W4==="+-- Left "Base32-encoded bytestring has invalid padding"+--+-- >>> isLeft (parseBase32Unpadded "MZ")+-- True+--+-- @since 1.0+--+parseBase32Unpadded :: ByteString -> Either T.Text (Base32 'StdUnpadded ByteString)+parseBase32Unpadded value = assertBase32 value+ <$ (decodeBase32UnpaddedUntyped) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import Data.Either (isLeft)
src/Data/ByteString/Lazy/Base32/Hex.hs view
@@ -1,8 +1,10 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE Trustworthy #-} -- | -- Module : Data.ByteString.Lazy.Base32.Hex--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -21,30 +23,32 @@ , encodeBase32Unpadded , encodeBase32Unpadded' -- * Decoding+, decodeBase32Untyped+, decodeBase32UnpaddedUntyped+, decodeBase32PaddedUntyped+ -- * Validation , decodeBase32-, decodeBase32Unpadded , decodeBase32Padded---, decodeBase32Lenient- -- * Validation-, isBase32Hex-, isValidBase32Hex+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where -import Prelude hiding (all, elem) +import Data.Base32.Types+ import qualified Data.ByteString as BS import qualified Data.ByteString.Base32.Hex as B32H import Data.ByteString.Base32.Internal.Utils (reChunkN)-import Data.ByteString.Lazy (elem, fromChunks, toChunks)+import Data.ByteString.Lazy (fromChunks, toChunks) import Data.ByteString.Lazy.Internal (ByteString(..))-import Data.Either (isRight) import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL --- | Encode a 'ByteString' value as a Base32hex 'Text' value with padding.+-- | Encode a 'ByteString' value as a Base32hex 'TL.Text' value with padding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> --@@ -53,8 +57,10 @@ -- >>> encodeBase32 "Sun" -- "ADQMS===" ---encodeBase32 :: ByteString -> TL.Text-encodeBase32 = TL.decodeUtf8 . encodeBase32'+-- @since 1.0+--+encodeBase32 :: ByteString -> Base32 'HexPadded TL.Text+encodeBase32 = fmap TL.decodeUtf8 . encodeBase32' {-# INLINE encodeBase32 #-} -- | Encode a 'ByteString' as a Base32hex 'ByteString' value with padding.@@ -66,37 +72,43 @@ -- >>> encodeBase32' "Sun" -- "ADQMS===" ---encodeBase32' :: ByteString -> ByteString-encodeBase32' = fromChunks- . fmap B32H.encodeBase32'+-- @since 1.0+--+encodeBase32' :: ByteString -> Base32 'HexPadded ByteString+encodeBase32' = assertBase32 . fromChunks+ . fmap (extractBase32 . B32H.encodeBase32') . reChunkN 5 . toChunks --- | Decode an arbitrarily padded Base32hex encoded 'ByteString' value. If its length is not a multiple--- of 4, then padding chars will be added to fill out the input to a multiple of--- 4 for safe decoding as Base32hex-encoded values are optionally padded.+-- | Decode an arbitrarily padded Base32hex encoded 'ByteString' value. If its length is+-- not a multiple+-- of 8, then padding chars will be added to fill out the input to a multiple of+-- 8 for safe decoding as Base32hex-encoded values are optionally padded. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32 "ADQMS==="+-- >>> decodeBase32Untyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32 "ADQMS"+-- >>> decodeBase32Untyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32 "ADQMS==="+-- >>> decodeBase32Untyped "ADQM===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: ByteString -> Either T.Text ByteString-decodeBase32 = fmap (fromChunks . (:[]))- . B32H.decodeBase32+-- @since 1.0+--+decodeBase32Untyped :: ByteString -> Either T.Text ByteString+decodeBase32Untyped = fmap (fromChunks . (:[]))+ . B32H.decodeBase32Untyped . BS.concat . toChunks-{-# INLINE decodeBase32 #-}+{-# INLINE decodeBase32Untyped #-} --- | Encode a 'ByteString' value as Base32hex 'Text' without padding. Note that for Base32hex,+-- | Encode a 'ByteString' value as Base32hex 'TL.Text' without padding. Note that for+-- Base32hex, -- padding is optional. If you call this function, you will simply be encoding -- as Base32hex and stripping padding chars from the output. --@@ -107,8 +119,10 @@ -- >>> encodeBase32Unpadded "Sun" -- "ADQMS" ---encodeBase32Unpadded :: ByteString -> TL.Text-encodeBase32Unpadded = TL.decodeUtf8 . encodeBase32Unpadded'+-- @since 1.0+--+encodeBase32Unpadded :: ByteString -> Base32 'HexUnpadded TL.Text+encodeBase32Unpadded = fmap TL.decodeUtf8 . encodeBase32Unpadded' {-# INLINE encodeBase32Unpadded #-} -- | Encode a 'ByteString' value as Base32hex without padding. Note that for Base32hex,@@ -122,9 +136,11 @@ -- >>> encodeBase32Unpadded' "Sun" -- "ADQMS" ---encodeBase32Unpadded' :: ByteString -> ByteString-encodeBase32Unpadded' = fromChunks- . fmap B32H.encodeBase32Unpadded'+-- @since 1.0+--+encodeBase32Unpadded' :: ByteString -> Base32 'HexUnpadded ByteString+encodeBase32Unpadded' = assertBase32 . fromChunks+ . fmap (extractBase32 . B32H.encodeBase32Unpadded') . reChunkN 5 . toChunks @@ -133,112 +149,139 @@ -- confirm. -- -- In general, unless unpadded Base32hex is explicitly required, it is--- safer to call 'decodeBase32'.+-- safer to call 'decodeBase32Untyped'. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32Unpadded "ADQMS"+-- >>> decodeBase32UnpaddedUntyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32Unpadded "ADQMS==="+-- >>> decodeBase32UnpaddedUntyped "ADQMS===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: ByteString -> Either T.Text ByteString-decodeBase32Unpadded = fmap (fromChunks . (:[]))- . B32H.decodeBase32Unpadded+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: ByteString -> Either T.Text ByteString+decodeBase32UnpaddedUntyped = fmap (fromChunks . (:[]))+ . B32H.decodeBase32UnpaddedUntyped . BS.concat . toChunks-{-# INLINE decodeBase32Unpadded #-}+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Decode a padded Base32hex-encoded 'ByteString' value. Input strings are -- required to be correctly padded, and will be validated prior to decoding -- to confirm. -- -- In general, unless padded Base32hex is explicitly required, it is--- safer to call 'decodeBase32'.+-- safer to call 'decodeBase32Untyped'. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32Padded "ADQMS==="+-- >>> decodeBase32PaddedUntyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32Padded "ADQMS"+-- >>> decodeBase32PaddedUntyped "ADQMS" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: ByteString -> Either T.Text ByteString-decodeBase32Padded = fmap (fromChunks . (:[]))- . B32H.decodeBase32Padded+-- @since 1.0+--+decodeBase32PaddedUntyped :: ByteString -> Either T.Text ByteString+decodeBase32PaddedUntyped = fmap (fromChunks . (:[]))+ . B32H.decodeBase32PaddedUntyped . BS.concat . toChunks-{-# INLINE decodeBase32Padded #-}+{-# INLINE decodeBase32PaddedUntyped #-} --- -- | Leniently decode an unpadded Base32hex-encoded 'ByteString'. This function--- -- will not generate parse errors. If input data contains padding chars,--- -- then the input will be parsed up until the first pad character.--- ----- -- __Note:__ This is not RFC 4648-compliant.--- ----- -- === __Examples__:--- ----- -- >>> decodeBase32Lenient "PDw_Pj4="--- -- "<<?>>"--- ----- -- >>> decodeBase32Lenient "PDw_%%%$}Pj4"--- -- "<<?>>"--- ----- decodeBase32Lenient :: ByteString -> ByteString--- decodeBase32Lenient = fromChunks--- . fmap B32H.decodeBase32Lenient--- . reChunkN 8--- . fmap (BS.filter (flip elem "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567="))--- . toChunks--- {-# INLINE decodeBase32Lenient #-}+-- | Decode a typed Base32hex-encoded 'ByteString' value.+--+-- This function is padding-agnostic.+--+-- >>> decodeBase32 (encodeBase32' "Sun")+-- "Sun"+--+-- >>> decodeBase32 (encodeBase32Unpadded' "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: forall k. HexAlphabet k => Base32 k ByteString -> ByteString+decodeBase32 = fromChunks+ . fmap (B32H.decodeBase32+ . (assertBase32 :: BS.ByteString -> Base32 k BS.ByteString))+ . reChunkN 8 . toChunks . extractBase32+{-# INLINE decodeBase32 #-} --- | Tell whether a 'ByteString' is Base32hex-encoded.+-- | Decode a typed, padded Base32hex-encoded 'ByteString' value. ----- === __Examples__:+-- >>> decodeBase32Padded (encodeBase32' "Sun")+-- "Sun" ----- >>> isBase32Hex "ADQMS"--- True+-- @since 1.0 ----- >>> isBase32Hex "ADQMS==="--- True+decodeBase32Padded :: Base32 'HexPadded ByteString -> ByteString+decodeBase32Padded = decodeBase32+{-# INLINE decodeBase32Padded #-}++-- | Parse and validate a base32hex-encoded value, assuming that it has padding. ----- >>> isBase32Hex "ADQMS=="--- False+-- >>> parseBase32Padded "adqms==="+-- Right "adqms===" ---isBase32Hex :: ByteString -> Bool-isBase32Hex bs = isValidBase32Hex bs && isRight (decodeBase32 bs)-{-# INLINE isBase32Hex #-}+-- >>> fmap decodeBase32Padded (parseBase32Padded "ADQMS===")+-- Right "Sun"+--+-- >>> parseBase32Padded ""+-- Right ""+--+-- >>> parseBase32Padded "ADQMS"+-- Left "Base32-encoded bytestring requires padding"+--+-- @since 1.0+--+parseBase32Padded :: ByteString -> Either T.Text (Base32 'HexPadded ByteString)+parseBase32Padded value = assertBase32 value+ <$ (decodeBase32PaddedUntyped) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'ByteString' is a valid Base32hex format.+-- | Decode a typed, unpadded Base32hex-encoded value. ----- This will not tell you whether or not this is a correct Base32hex representation,--- only that it conforms to the correct shape. To check whether it is a true--- Base32hex encoded 'ByteString' value, use 'isBase32Hex'.+-- >>> decodeBase32Unpadded (encodeBase32Unpadded' "Sun")+-- "Sun" ----- === __Examples__:+-- @since 1.0 --+decodeBase32Unpadded :: Base32 'HexUnpadded ByteString -> ByteString+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32hex-encoded value, assuming that it is unpadded. ----- >>> isValidBase32Hex "ADQMS"--- True+-- >>> parseBase32Unpadded "adqms"+-- Right "adqms" ----- >>> isValidBase32Hex "ADQMS="--- False+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "ADQMS")+-- Right "Sun" ----- >>> isValidBase32Hex "ADQMS%"--- False+-- >>> parseBase32Unpadded ""+-- Right "" ---isValidBase32Hex :: ByteString -> Bool-isValidBase32Hex = go . toChunks- where- go [] = True- go [c] = B32H.isValidBase32Hex c- go (c:cs) = -- note the lack of padding char- BS.all (flip elem "0123456789ABCDEFGHIJKLMNOPQRSTUV") c- && go cs-{-# INLINE isValidBase32Hex #-}+-- >>> parseBase32Unpadded "ADQMS==="+-- Left "Base32-encoded bytestring has invalid padding"+--+-- >>> isLeft (parseBase32Unpadded "CP")+-- True+--+-- @since 1.0+--+parseBase32Unpadded :: ByteString -> Either T.Text (Base32 'HexUnpadded ByteString)+parseBase32Unpadded value = assertBase32 value+ <$ (decodeBase32UnpaddedUntyped) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import Data.Either (isLeft)
src/Data/ByteString/Short/Base32.hs view
@@ -1,7 +1,10 @@+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE Trustworthy #-} -- | -- Module : Data.ByteString.Short.Base32--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -20,19 +23,26 @@ , encodeBase32Unpadded , encodeBase32Unpadded' -- * Decoding+, decodeBase32Untyped+, decodeBase32UnpaddedUntyped+, decodeBase32PaddedUntyped+ -- * Validation , decodeBase32-, decodeBase32Unpadded , decodeBase32Padded---, decodeBase32Lenient- -- * Validation-, isBase32-, isValidBase32+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where -import qualified Data.ByteString.Base32 as B32-import Data.ByteString.Short (ShortByteString, fromShort, toShort)+import Data.ByteString.Base32.Internal.Short+import Data.ByteString.Base32.Internal.Tables+import Data.ByteString.Short (ShortByteString)+import qualified Data.ByteString.Short as SBS+import Data.Base32.Types+ import Data.Text (Text)+import qualified Data.Text as Text import Data.Text.Short (ShortText) import Data.Text.Short.Unsafe (fromShortByteStringUnsafe) @@ -46,8 +56,10 @@ -- >>> encodeBase32 "Sun" -- "KN2W4===" ---encodeBase32 :: ShortByteString -> ShortText-encodeBase32 = fromShortByteStringUnsafe . encodeBase32'+-- @since 1.0+--+encodeBase32 :: ShortByteString -> Base32 'StdPadded ShortText+encodeBase32 = fmap fromShortByteStringUnsafe . encodeBase32' {-# INLINE encodeBase32 #-} -- | Encode a 'ShortByteString' as a Base32 'ShortByteString' value with padding.@@ -59,32 +71,53 @@ -- >>> encodeBase32' "Sun" -- "KN2W4===" ---encodeBase32' :: ShortByteString -> ShortByteString-encodeBase32' = toShort . B32.encodeBase32' . fromShort+-- @since 1.0+--+encodeBase32' :: ShortByteString -> Base32 'StdPadded ShortByteString+encodeBase32' =+ assertBase32+ . encodeShort+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"#+ True --- | Decode an arbitrarily padded Base32 encoded 'ShortByteString' value. If its length is not a multiple--- of 4, then padding chars will be added to fill out the input to a multiple of--- 4 for safe decoding as Base32-encoded values are optionally padded.+-- | Decode an arbitrarily padded Base32 encoded 'ShortByteString' value. If its length is+-- not a multiple+-- of 8, then padding chars will be added to fill out the input to a multiple of+-- 8 for safe decoding as Base32-encoded values are optionally padded. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32 "KN2W4==="+-- >>> decodeBase32Untyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32 "KN2W4"+-- >>> decodeBase32Untyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32 "KN2W==="+-- >>> decodeBase32Untyped "KN2W===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: ShortByteString -> Either Text ShortByteString-decodeBase32 = fmap toShort . B32.decodeBase32 . fromShort--{-# INLINE decodeBase32 #-}+-- @since 1.0+--+decodeBase32Untyped :: ShortByteString -> Either Text ShortByteString+decodeBase32Untyped bs+ | l == 0 = Right bs+ | r == 1 || r == 3 || r == 6 =+ Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r == 4 && any isPad [1 .. 2]+ || r == 5 && any isPad [1 .. 3]+ || r == 7 && any isPad [1 .. 5] =+ Left (Text.pack "Base32-encoded bytestring has invalid padding")+ | otherwise = decodeShort stdDecodeTable bs+ where+ l = SBS.length bs+ r = l `rem` 8+ isPad i = SBS.index bs (l - i) == 0x3d+{-# INLINE decodeBase32Untyped #-} --- | Encode a 'ShortByteString' value as Base32 'Text' without padding. Note that for Base32,+-- | Encode a 'ShortByteString' value as Base32 'Text' without padding. Note that for+-- Base32, -- padding is optional. If you call this function, you will simply be encoding -- as Base32 and stripping padding chars from the output. --@@ -95,8 +128,10 @@ -- >>> encodeBase32Unpadded "Sun" -- "KN2W4" ---encodeBase32Unpadded :: ShortByteString -> ShortText-encodeBase32Unpadded = fromShortByteStringUnsafe . encodeBase32Unpadded'+-- @since 1.0+--+encodeBase32Unpadded :: ShortByteString -> Base32 'StdUnpadded ShortText+encodeBase32Unpadded = fmap fromShortByteStringUnsafe . encodeBase32Unpadded' {-# INLINE encodeBase32Unpadded #-} -- | Encode a 'ShortByteString' value as Base32 without padding. Note that for Base32,@@ -110,103 +145,162 @@ -- >>> encodeBase32Unpadded' "Sun" -- "KN2W4" ---encodeBase32Unpadded' :: ShortByteString -> ShortByteString-encodeBase32Unpadded' = toShort . B32.encodeBase32Unpadded' . fromShort+-- @since 1.0+--+encodeBase32Unpadded' :: ShortByteString -> Base32 'StdUnpadded ShortByteString+encodeBase32Unpadded' =+ assertBase32+ . encodeShort+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"#+ False -- | Decode an unpadded Base32-encoded 'ShortByteString' value. Input strings are -- required to be unpadded, and will undergo validation prior to decoding to -- confirm. -- -- In general, unless unpadded Base32 is explicitly required, it is--- safer to call 'decodeBase32'.+-- safer to call 'decodeBase32Untyped'. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Unpadded "KN2W4"+-- >>> decodeBase32UnpaddedUntyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32Unpadded "KN2W4==="+-- >>> decodeBase32UnpaddedUntyped "KN2W4===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: ShortByteString -> Either Text ShortByteString-decodeBase32Unpadded = fmap toShort . B32.decodeBase32Unpadded . fromShort-{-# INLINE decodeBase32Unpadded #-}+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: ShortByteString -> Either Text ShortByteString+decodeBase32UnpaddedUntyped bs+ | l == 0 = Right bs+ | r == 1 || r == 3 || r == 6 =+ Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r /= 2 && SBS.index bs (l - 1) == 0x3d =+ Left (Text.pack "Base32-encoded bytestring has invalid padding")+ | otherwise = decodeShort stdDecodeTable bs+ where+ l = SBS.length bs+ r = l `rem` 8+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Decode a padded Base32-encoded 'ShortByteString' value. Input strings are -- required to be correctly padded, and will be validated prior to decoding -- to confirm. -- -- In general, unless padded Base32 is explicitly required, it is--- safer to call 'decodeBase32'.+-- safer to call 'decodeBase32Untyped'. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Padded "KN2W4==="+-- >>> decodeBase32PaddedUntyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32Padded "KN2W4"+-- >>> decodeBase32PaddedUntyped "KN2W4" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: ShortByteString -> Either Text ShortByteString-decodeBase32Padded = fmap toShort . B32.decodeBase32Padded . fromShort-{-# INLINE decodeBase32Padded #-}+-- @since 1.0+--+decodeBase32PaddedUntyped :: ShortByteString -> Either Text ShortByteString+decodeBase32PaddedUntyped bs+ | l == 0 = Right bs+ | r == 1 || r == 3 || r == 6 =+ Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r /= 0 = Left (Text.pack "Base32-encoded bytestring requires padding")+ | otherwise = decodeShort stdDecodeTable bs+ where+ l = SBS.length bs+ r = l `rem` 8+{-# INLINE decodeBase32PaddedUntyped #-} --- -- | Leniently decode an unpadded Base32-encoded 'ShortByteString'. This function--- -- will not generate parse errors. If input data contains padding chars,--- -- then the input will be parsed up until the first pad character.--- ----- -- __Note:__ This is not RFC 4648-compliant.--- ----- -- === __Examples__:--- ----- -- >>> decodeBase32Lenient "PDw_Pj4="--- -- "<<?>>"--- ----- -- >>> decodeBase32Lenient "PDw_%%%$}Pj4"--- -- "<<?>>"--- ----- decodeBase32Lenient :: ShortByteString -> ShortByteString--- decodeBase32Lenient = toShort . B32.decodeBase32Lenient . fromShort--- {-# INLINE decodeBase32Lenient #-}+-- | Decode a typed Base32-encoded 'ShortByteString' value.+--+-- This function is padding-agnostic.+--+-- >>> decodeBase32 (encodeBase32' "Sun")+-- "Sun"+--+-- >>> decodeBase32 (encodeBase32Unpadded' "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: StdAlphabet k => Base32 k ShortByteString -> ShortByteString+decodeBase32 = decodeShortTyped stdDecodeTable+{-# INLINE decodeBase32 #-} --- | Tell whether a 'ShortByteString' is Base32-encoded.+-- | Decode a typed, padded Base32-encoded 'ShortByteString' value. ----- === __Examples__:+-- >>> decodeBase32Padded (encodeBase32' "Sun")+-- "Sun" ----- >>> isBase32 "KN2W4"--- True+-- @since 1.0 ----- >>> isBase32 "KN2W4==="--- True+decodeBase32Padded :: Base32 'StdPadded ShortByteString -> ShortByteString+decodeBase32Padded = decodeBase32+{-# INLINE decodeBase32Padded #-}++-- | Parse and validate a base32-encoded value, assuming that it has padding. ----- >>> isBase32 "KN2W4=="--- False+-- >>> parseBase32Padded "kn2w4==="+-- Right "kn2w4===" ---isBase32 :: ShortByteString -> Bool-isBase32 = B32.isBase32 . fromShort-{-# INLINE isBase32 #-}+-- >>> fmap decodeBase32Padded (parseBase32Padded "KN2W4===")+-- Right "Sun"+--+-- >>> parseBase32Padded ""+-- Right ""+--+-- >>> parseBase32Padded "KN2W4"+-- Left "Base32-encoded bytestring requires padding"+--+-- @since 1.0+--+parseBase32Padded :: ShortByteString -> Either Text (Base32 'StdPadded ShortByteString)+parseBase32Padded value = assertBase32 value+ <$ (decodeBase32PaddedUntyped) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'ShortByteString' is a valid Base32 format.+-- | Decode a typed, unpadded Base32-encoded value. ----- This will not tell you whether or not this is a correct Base32 representation,--- only that it conforms to the correct shape. To check whether it is a true--- Base32 encoded 'ShortByteString' value, use 'isBase32'.+-- >>> decodeBase32Unpadded (encodeBase32Unpadded' "Sun")+-- "Sun" ----- === __Examples__:+-- @since 1.0 ----- >>> isValidBase32 "KN2W4"--- True+decodeBase32Unpadded :: Base32 'StdUnpadded ShortByteString -> ShortByteString+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32-encoded value, assuming that it is unpadded. ----- >>> isValidBase32 "KN2W4="--- False+-- >>> parseBase32Unpadded "kn2w4"+-- Right "kn2w4" ----- >>> isValidBase32 "KN2W4%"--- False+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "KN2W4")+-- Right "Sun" ---isValidBase32 :: ShortByteString -> Bool-isValidBase32 = B32.isValidBase32 . fromShort-{-# INLINE isValidBase32 #-}+-- >>> parseBase32Unpadded ""+-- Right ""+--+-- >>> parseBase32Unpadded "KN2W4==="+-- Left "Base32-encoded bytestring has invalid padding"+--+-- >>> isLeft (parseBase32Unpadded "MZ")+-- True+--+-- @since 1.0+--+parseBase32Unpadded+ :: ShortByteString -> Either Text (Base32 'StdUnpadded ShortByteString)+parseBase32Unpadded value = assertBase32 value+ <$ (decodeBase32UnpaddedUntyped) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import Data.Either (isLeft)
src/Data/ByteString/Short/Base32/Hex.hs view
@@ -1,7 +1,10 @@+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE Trustworthy #-} -- | -- Module : Data.ByteString.Short.Base32.Hex--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -20,19 +23,26 @@ , encodeBase32Unpadded , encodeBase32Unpadded' -- * Decoding+, decodeBase32Untyped+, decodeBase32UnpaddedUntyped+, decodeBase32PaddedUntyped+ -- * Validation , decodeBase32-, decodeBase32Unpadded , decodeBase32Padded---, decodeBase32Lenient- -- * Validation-, isBase32Hex-, isValidBase32Hex+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where -import qualified Data.ByteString.Base32.Hex as B32H-import Data.ByteString.Short (ShortByteString, fromShort, toShort)+import Data.ByteString.Base32.Internal.Short+import Data.ByteString.Base32.Internal.Tables+import Data.ByteString.Short (ShortByteString)+import qualified Data.ByteString.Short as SBS+import Data.Base32.Types+ import Data.Text (Text)+import qualified Data.Text as Text import Data.Text.Short (ShortText) import Data.Text.Short.Unsafe (fromShortByteStringUnsafe) @@ -45,8 +55,10 @@ -- >>> encodeBase32 "Sun" -- "ADQMS===" ---encodeBase32 :: ShortByteString -> ShortText-encodeBase32 = fromShortByteStringUnsafe . encodeBase32'+-- @since 1.0+--+encodeBase32 :: ShortByteString -> Base32 'HexPadded ShortText+encodeBase32 = fmap fromShortByteStringUnsafe . encodeBase32' {-# INLINE encodeBase32 #-} -- | Encode a 'ShortByteString' as a Base32hex 'ShortByteString' value with padding.@@ -58,31 +70,53 @@ -- >>> encodeBase32' "Sun" -- "ADQMS===" ---encodeBase32' :: ShortByteString -> ShortByteString-encodeBase32' = toShort . B32H.encodeBase32' . fromShort+-- @since 1.0+--+encodeBase32' :: ShortByteString -> Base32 'HexPadded ShortByteString+encodeBase32' =+ assertBase32+ . encodeShort+ "0123456789ABCDEFGHIJKLMNOPQRSTUV"#+ True --- | Decode an arbitrarily padded Base32hex encoded 'ShortByteString' value. If its length is not a multiple--- of 4, then padding chars will be added to fill out the input to a multiple of--- 4 for safe decoding as Base32hex-encoded values are optionally padded.+-- | Decode an arbitrarily padded Base32hex encoded 'ShortByteString' value. If its length+-- is not a multiple+-- of 8, then padding chars will be added to fill out the input to a multiple of+-- 8 for safe decoding as Base32hex-encoded values are optionally padded. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32 "ADQMS==="+-- >>> decodeBase32Untyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32 "ADQMS"+-- >>> decodeBase32Untyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32 "ADQM==="+-- >>> decodeBase32Untyped "ADQM===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: ShortByteString -> Either Text ShortByteString-decodeBase32 = fmap toShort . B32H.decodeBase32 . fromShort-{-# INLINE decodeBase32 #-}+-- @since 1.0+--+decodeBase32Untyped :: ShortByteString -> Either Text ShortByteString+decodeBase32Untyped bs+ | l == 0 = Right bs+ | r == 1 || r == 3 || r == 6 =+ Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r == 4 && any isPad [1 .. 2]+ || r == 5 && any isPad [1 .. 3]+ || r == 7 && any isPad [1 .. 5] =+ Left (Text.pack "Base32-encoded bytestring has invalid padding")+ | otherwise = decodeShort hexDecodeTable bs+ where+ l = SBS.length bs+ r = l `rem` 8+ isPad i = SBS.index bs (l - i) == 0x3d+{-# INLINE decodeBase32Untyped #-} --- | Encode a 'ShortByteString' value as Base32hex 'Text' without padding. Note that for Base32hex,+-- | Encode a 'ShortByteString' value as Base32hex 'Text' without padding. Note that for+-- Base32hex, -- padding is optional. If you call this function, you will simply be encoding -- as Base32hex and stripping padding chars from the output. --@@ -90,14 +124,17 @@ -- -- === __Examples__: ----- >>> encodeBase32Unpadded' "Sun"+-- >>> encodeBase32Unpadded "Sun" -- "ADQMS" ---encodeBase32Unpadded :: ShortByteString -> ShortText-encodeBase32Unpadded = fromShortByteStringUnsafe . encodeBase32Unpadded'+-- @since 1.0+--+encodeBase32Unpadded :: ShortByteString -> Base32 'HexUnpadded ShortText+encodeBase32Unpadded = fmap fromShortByteStringUnsafe . encodeBase32Unpadded' {-# INLINE encodeBase32Unpadded #-} --- | Encode a 'ShortByteString' value as Base32hex without padding. Note that for Base32hex,+-- | Encode a 'ShortByteString' value as Base32hex without padding. Note that for+-- Base32hex, -- padding is optional. If you call this function, you will simply be encoding -- as Base32hex and stripping padding chars from the output. --@@ -108,103 +145,162 @@ -- >>> encodeBase32Unpadded' "Sun" -- "ADQMS" ---encodeBase32Unpadded' :: ShortByteString -> ShortByteString-encodeBase32Unpadded' = toShort . B32H.encodeBase32Unpadded' . fromShort+-- @since 1.0+--+encodeBase32Unpadded' :: ShortByteString -> Base32 'HexUnpadded ShortByteString+encodeBase32Unpadded' =+ assertBase32+ . encodeShort+ "0123456789ABCDEFGHIJKLMNOPQRSTUV"#+ False -- | Decode an unpadded Base32hex-encoded 'ShortByteString' value. Input strings are -- required to be unpadded, and will undergo validation prior to decoding to -- confirm. -- -- In general, unless unpadded Base32hex is explicitly required, it is--- safer to call 'decodeBase32'.+-- safer to call 'decodeBase32Untyped'. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32Unpadded "ADQMS"+-- >>> decodeBase32UnpaddedUntyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32Unpadded "ADQMS==="+-- >>> decodeBase32UnpaddedUntyped "ADQMS===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: ShortByteString -> Either Text ShortByteString-decodeBase32Unpadded = fmap toShort . B32H.decodeBase32Unpadded . fromShort-{-# INLINE decodeBase32Unpadded #-}+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: ShortByteString -> Either Text ShortByteString+decodeBase32UnpaddedUntyped bs+ | l == 0 = Right bs+ | r == 1 || r == 3 || r == 6 =+ Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r /= 2 && SBS.index bs (l - 1) == 0x3d =+ Left (Text.pack "Base32-encoded bytestring has invalid padding")+ | otherwise = decodeShort hexDecodeTable bs+ where+ l = SBS.length bs+ r = l `rem` 8+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Decode a padded Base32hex-encoded 'ShortByteString' value. Input strings are -- required to be correctly padded, and will be validated prior to decoding -- to confirm. -- -- In general, unless padded Base32hex is explicitly required, it is--- safer to call 'decodeBase32'.+-- safer to call 'decodeBase32Untyped'. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32Padded "ADQMS==="+-- >>> decodeBase32PaddedUntyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32Padded "ADQMS"+-- >>> decodeBase32PaddedUntyped "ADQMS" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: ShortByteString -> Either Text ShortByteString-decodeBase32Padded = fmap toShort . B32H.decodeBase32Padded . fromShort-{-# INLINE decodeBase32Padded #-}+-- @since 1.0+--+decodeBase32PaddedUntyped :: ShortByteString -> Either Text ShortByteString+decodeBase32PaddedUntyped bs+ | l == 0 = Right bs+ | r == 1 || r == 3 || r == 6 =+ Left (Text.pack "Base32-encoded bytestring has invalid size")+ | r /= 0 = Left (Text.pack "Base32-encoded bytestring requires padding")+ | otherwise = decodeShort hexDecodeTable bs+ where+ l = SBS.length bs+ r = l `rem` 8+{-# INLINE decodeBase32PaddedUntyped #-} --- -- | Leniently decode an unpadded Base32hex-encoded 'ShortByteString'. This function--- -- will not generate parse errors. If input data contains padding chars,--- -- then the input will be parsed up until the first pad character.--- ----- -- __Note:__ This is not RFC 4648-compliant.--- ----- -- === __Examples__:--- ----- -- >>> decodeBase32Lenient "PDw_Pj4="--- -- "<<?>>"--- ----- -- >>> decodeBase32Lenient "PDw_%%%$}Pj4"--- -- "<<?>>"--- ----- decodeBase32Lenient :: ShortByteString -> ShortByteString--- decodeBase32Lenient = toShort . B32H.decodeBase32Lenient . fromShort--- {-# INLINE decodeBase32Lenient #-}+-- | Decode a typed Base32hex-encoded 'ShortByteString' value.+--+-- This function is padding-agnostic.+--+-- >>> decodeBase32 (encodeBase32' "Sun")+-- "Sun"+--+-- >>> decodeBase32 (encodeBase32Unpadded' "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: HexAlphabet k => Base32 k ShortByteString -> ShortByteString+decodeBase32 = decodeShortTyped hexDecodeTable+{-# INLINE decodeBase32 #-} --- | Tell whether a 'ShortByteString' is Base32hex-encoded.+-- | Decode a typed, padded Base32hex-encoded 'ShortByteString' value. ----- === __Examples__:+-- >>> decodeBase32Padded (encodeBase32' "Sun")+-- "Sun" ----- >>> isBase32Hex "ADQMS"--- True+-- @since 1.0 ----- >>> isBase32Hex "ADQMS==="--- True+decodeBase32Padded :: Base32 'HexPadded ShortByteString -> ShortByteString+decodeBase32Padded = decodeBase32+{-# INLINE decodeBase32Padded #-}++-- | Parse and validate a base32hex-encoded value, assuming that it has padding. ----- >>> isBase32Hex "ADQMS=="--- False+-- >>> parseBase32Padded "adqms==="+-- Right "adqms===" ---isBase32Hex :: ShortByteString -> Bool-isBase32Hex = B32H.isBase32Hex . fromShort-{-# INLINE isBase32Hex #-}+-- >>> fmap decodeBase32Padded (parseBase32Padded "ADQMS===")+-- Right "Sun"+--+-- >>> parseBase32Padded ""+-- Right ""+--+-- >>> parseBase32Padded "ADQMS"+-- Left "Base32-encoded bytestring requires padding"+--+-- @since 1.0+--+parseBase32Padded :: ShortByteString -> Either Text (Base32 'HexPadded ShortByteString)+parseBase32Padded value = assertBase32 value+ <$ (decodeBase32PaddedUntyped) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'ShortByteString' is a valid Base32hex format.+-- | Decode a typed, unpadded Base32hex-encoded value. ----- This will not tell you whether or not this is a correct Base32hex representation,--- only that it conforms to the correct shape. To check whether it is a true--- Base32 encoded 'ShortByteString' value, use 'isBase32Hex'.+-- >>> decodeBase32Unpadded (encodeBase32Unpadded' "Sun")+-- "Sun" ----- === __Examples__:+-- @since 1.0 ----- >>> isValidBase32Hex "ADQMS"--- True+decodeBase32Unpadded :: Base32 'HexUnpadded ShortByteString -> ShortByteString+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32hex-encoded value, assuming that it is unpadded. ----- >>> isValidBase32Hex "ADQMS="--- False+-- >>> parseBase32Unpadded "adqms"+-- Right "adqms" ----- >>> isValidBase32Hex "ADQMS%"--- False+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "ADQMS")+-- Right "Sun" ---isValidBase32Hex :: ShortByteString -> Bool-isValidBase32Hex = B32H.isValidBase32Hex . fromShort-{-# INLINE isValidBase32Hex #-}+-- >>> parseBase32Unpadded ""+-- Right ""+--+-- >>> parseBase32Unpadded "ADQMS==="+-- Left "Base32-encoded bytestring has invalid padding"+--+-- >>> isLeft (parseBase32Unpadded "CP")+-- True+--+-- @since 1.0+--+parseBase32Unpadded+ :: ShortByteString -> Either Text (Base32 'HexUnpadded ShortByteString)+parseBase32Unpadded value = assertBase32 value+ <$ (decodeBase32UnpaddedUntyped) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import Data.Either (isLeft)
src/Data/Text/Encoding/Base32.hs view
@@ -1,7 +1,9 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE Safe #-} -- | -- Module : Data.Text.Encoding.Base32--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -15,19 +17,24 @@ -- module Data.Text.Encoding.Base32 ( encodeBase32-, decodeBase32+, decodeBase32Untyped , decodeBase32With , encodeBase32Unpadded-, decodeBase32Unpadded+, decodeBase32UnpaddedUntyped , decodeBase32UnpaddedWith-, decodeBase32Padded+, decodeBase32PaddedUntyped , decodeBase32PaddedWith--- , decodeBase32Lenient-, isBase32-, isValidBase32+, decodeBase32+, decodeBase32Padded+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where +import Data.Base32.Types+import qualified Data.Text as ErrorText+ import Data.Bifunctor (first) import Data.ByteString (ByteString) import qualified Data.ByteString.Base32 as B32@@ -45,59 +52,65 @@ -- >>> encodeBase32 "Sun" -- "KN2W4===" ---encodeBase32 :: Text -> Text+-- @since 1.0+--+encodeBase32 :: Text -> Base32 'StdPadded Text encodeBase32 = B32.encodeBase32 . T.encodeUtf8 {-# INLINE encodeBase32 #-} --- | Decode an arbitrarily padded Base32-encoded 'Text' value. If its length is not a multiple--- of 4, then padding chars will be added to fill out the input to a multiple of--- 4 for safe decoding as base32 encodings are optionally padded.+-- | Decode an arbitrarily padded Base32-encoded 'Text' value. If its length is not a+-- multiple+-- of 8, then padding chars will be added to fill out the input to a multiple of+-- 8 for safe decoding as base32 encodings are optionally padded. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'T.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32With`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32 "KN2W4==="+-- >>> decodeBase32Untyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32 "KN2W4"+-- >>> decodeBase32Untyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32 "KN2W==="+-- >>> decodeBase32Untyped "KN2W===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: Text -> Either Text Text-decodeBase32 = fmap T.decodeUtf8- . B32.decodeBase32- . T.encodeUtf8-{-# INLINE decodeBase32 #-}+-- @since 1.0+--+decodeBase32Untyped :: Text -> Either Text Text+decodeBase32Untyped value = B32.decodeBase32Untyped (T.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . T.decodeUtf8'+{-# INLINE decodeBase32Untyped #-} -- | Attempt to decode a 'Text' value as Base32, converting from -- 'ByteString' to 'Text' according to some encoding function. In practice,--- This is something like 'decodeUtf8'', which may produce an error.+-- This is something like 'T.decodeUtf8'', which may produce an error. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Example__: ----- @--- 'decodeBase32With' 'T.decodeUtf8''--- :: 'ByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'Text'--- @+-- >>> decodeBase32With UTF8.decodeUtf8' "KN2W4==="+-- Right "Sun" --+-- >>> decodeBase32With UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32With (const (Left "unsupported encoding")) "KN2W4==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32With :: (ByteString -> Either err Text) -- ^ convert a bytestring to text (e.g. 'T.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) Text-decodeBase32With f t = case B32.decodeBase32 t of+decodeBase32With f t = case B32.decodeBase32Untyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32With #-}@@ -111,54 +124,59 @@ -- >>> encodeBase32Unpadded "Sun" -- "KN2W4" ---encodeBase32Unpadded :: Text -> Text+-- @since 1.0+--+encodeBase32Unpadded :: Text -> Base32 'StdUnpadded Text encodeBase32Unpadded = B32.encodeBase32Unpadded . T.encodeUtf8 {-# INLINE encodeBase32Unpadded #-} -- | Decode an unpadded Base32 encoded 'Text' value. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'T.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32WUnpaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Unpadded "KN2W4"+-- >>> decodeBase32UnpaddedUntyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32Unpadded "KN2W4==="+-- >>> decodeBase32UnpaddedUntyped "KN2W4===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: Text -> Either Text Text-decodeBase32Unpadded = fmap T.decodeUtf8- . B32.decodeBase32Unpadded- . T.encodeUtf8-{-# INLINE decodeBase32Unpadded #-}+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: Text -> Either Text Text+decodeBase32UnpaddedUntyped value = B32.decodeBase32UnpaddedUntyped (T.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . T.decodeUtf8'+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Attempt to decode an unpadded 'ByteString' value as Base32, converting from -- 'ByteString' to 'Text' according to some encoding function. In practice,--- This is something like 'decodeUtf8'', which may produce an error.+-- This is something like 'T.decodeUtf8'', which may produce an error. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Example__: ----- @--- 'decodeBase32UnpaddedWith' 'T.decodeUtf8''--- :: 'ByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'Text'--- @+-- >>> decodeBase32UnpaddedWith UTF8.decodeUtf8' "KN2W4"+-- Right "Sun" --+-- >>> decodeBase32UnpaddedWith UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32UnpaddedWith (const (Left "unsupported encoding")) "KN2W4"+-- Left (ConversionError "unsupported encoding")+-- decodeBase32UnpaddedWith :: (ByteString -> Either err Text) -- ^ convert a bytestring to text (e.g. 'T.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) Text-decodeBase32UnpaddedWith f t = case B32.decodeBase32Unpadded t of+decodeBase32UnpaddedWith f t = case B32.decodeBase32UnpaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32UnpaddedWith #-}@@ -166,98 +184,144 @@ -- | Decode an padded Base32 encoded 'Text' value ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'T.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32PaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Padded "KN2W4==="+-- >>> decodeBase32PaddedUntyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32Padded "KN2W4"+-- >>> decodeBase32PaddedUntyped "KN2W4" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: Text -> Either Text Text-decodeBase32Padded = fmap T.decodeUtf8- . B32.decodeBase32Padded- . T.encodeUtf8-{-# INLINE decodeBase32Padded #-}+-- @since 1.0+--+decodeBase32PaddedUntyped :: Text -> Either Text Text+decodeBase32PaddedUntyped value = B32.decodeBase32PaddedUntyped (T.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . T.decodeUtf8'+{-# INLINE decodeBase32PaddedUntyped #-} -- | Attempt to decode a padded 'ByteString' value as Base32, converting from -- 'ByteString' to 'Text' according to some encoding function. In practice,--- This is something like 'decodeUtf8'', which may produce an error.+-- This is something like 'T.decodeUtf8'', which may produce an error. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Example__: ----- @--- 'decodeBase32PaddedWith' 'T.decodeUtf8''--- :: 'ByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'Text'--- @+-- >>> decodeBase32PaddedWith UTF8.decodeUtf8' "KN2W4==="+-- Right "Sun" --+-- >>> decodeBase32PaddedWith UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32PaddedWith (const (Left "unsupported encoding")) "KN2W4==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32PaddedWith :: (ByteString -> Either err Text) -- ^ convert a bytestring to text (e.g. 'T.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) Text-decodeBase32PaddedWith f t = case B32.decodeBase32Padded t of+decodeBase32PaddedWith f t = case B32.decodeBase32PaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32PaddedWith #-} --- -- | Leniently decode a Base32-encoded 'Text' value. This function--- -- will not generate parse errors. If input data contains padding chars,--- -- then the input will be parsed up until the first pad character.--- ----- -- __Note:__ This is not RFC 4648-compliant.--- ----- decodeBase32Lenient :: Text -> Text--- decodeBase32Lenient = T.decodeUtf8--- . B32.decodeBase32Lenient--- . T.encodeUtf8--- {-# INLINE decodeBase32Lenient #-}+-- | Decode a typed Base32-encoded 'Text' value.+--+-- This function is padding-agnostic.+-- The decoded payload must be valid UTF-8.+--+-- >>> decodeBase32 (encodeBase32 "Sun")+-- "Sun"+--+-- >>> decodeBase32 (encodeBase32Unpadded "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: StdAlphabet k => Base32 k Text -> Text+decodeBase32 = T.decodeUtf8 . B32.decodeBase32 . fmap T.encodeUtf8+{-# INLINE decodeBase32 #-} --- | Tell whether a 'Text' value is Base32-encoded+-- | Decode a typed, padded Base32-encoded 'Text' value. ----- === __Examples__:+-- The decoded payload must be valid UTF-8. ----- >>> isBase32 "KN2W4"--- True+-- >>> decodeBase32Padded (encodeBase32 "Sun")+-- "Sun" ----- >>> isBase32 "KN2W4==="--- True+-- @since 1.0 ----- >>> isBase32 "KN2W4=="--- False+decodeBase32Padded :: Base32 'StdPadded Text -> Text+decodeBase32Padded = decodeBase32+{-# INLINE decodeBase32Padded #-}++-- | Parse and validate a base32-encoded value, assuming that it has padding. ---isBase32 :: Text -> Bool-isBase32 = B32.isBase32 . T.encodeUtf8-{-# INLINE isBase32 #-}+-- >>> parseBase32Padded "kn2w4==="+-- Right "kn2w4==="+--+-- >>> fmap decodeBase32Padded (parseBase32Padded "KN2W4===")+-- Right "Sun"+--+-- >>> parseBase32Padded ""+-- Right ""+--+-- >>> parseBase32Padded "KN2W4"+-- Left "Base32-encoded bytestring requires padding"+--+-- @since 1.0+--+parseBase32Padded :: Text -> Either Text (Base32 'StdPadded Text)+parseBase32Padded value = assertBase32 value+ <$ (B32.decodeBase32PaddedUntyped . T.encodeUtf8) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'Text' value is a valid Base32 format.+-- | Decode a typed, unpadded Base32-encoded value. ----- This will not tell you whether or not this is a correct Base32 representation,--- only that it conforms to the correct shape. To check whether it is a true--- Base32 encoded 'Text' value, use 'isBase32'.+-- The decoded payload must be valid UTF-8. ----- === __Examples__:+-- >>> decodeBase32Unpadded (encodeBase32Unpadded "Sun")+-- "Sun" ----- >>> isValidBase32 "KN2W4"--- True+-- @since 1.0 ----- >>> isValidBase32 "KN2W4="--- False+decodeBase32Unpadded :: Base32 'StdUnpadded Text -> Text+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32-encoded value, assuming that it is unpadded. ----- >>> isValidBase32 "KN2W4%"--- False+-- >>> parseBase32Unpadded "kn2w4"+-- Right "kn2w4" ---isValidBase32 :: Text -> Bool-isValidBase32 = B32.isValidBase32 . T.encodeUtf8-{-# INLINE isValidBase32 #-}+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "KN2W4")+-- Right "Sun"+--+-- >>> parseBase32Unpadded ""+-- Right ""+--+-- >>> parseBase32Unpadded "KN2W4==="+-- Left "Base32-encoded bytestring has invalid padding"+--+-- >>> isLeft (parseBase32Unpadded "MZ")+-- True+--+-- @since 1.0+--+parseBase32Unpadded :: Text -> Either Text (Base32 'StdUnpadded Text)+parseBase32Unpadded value = assertBase32 value+ <$ (B32.decodeBase32UnpaddedUntyped . T.encodeUtf8) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import Data.Either (isLeft)+-- >>> import qualified Data.Text.Encoding as UTF8
src/Data/Text/Encoding/Base32/Error.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE Safe #-} -- | -- Module : Data.Text.Encoding.Base32.Error--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -29,6 +29,11 @@ -- Namely, to distinguish between decoding errors from opaque -- unicode exceptions caught in the unicode decoding process. --+-- >>> DecodeError "invalid padding" :: Base32Error String+-- DecodeError "invalid padding"+-- >>> ConversionError "invalid UTF-8" :: Base32Error String+-- ConversionError "invalid UTF-8"+-- data Base32Error e = DecodeError Text -- ^ The error associated with decoding failure@@ -54,3 +59,4 @@ -- @since 0.2.0.0 -- instance NFData e => NFData (Base32Error e)+
src/Data/Text/Encoding/Base32/Hex.hs view
@@ -1,7 +1,9 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE Safe #-} -- | -- Module : Data.Text.Encoding.Base32.Hex--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -16,19 +18,24 @@ -- module Data.Text.Encoding.Base32.Hex ( encodeBase32-, decodeBase32+, decodeBase32Untyped , decodeBase32With , encodeBase32Unpadded-, decodeBase32Unpadded+, decodeBase32UnpaddedUntyped , decodeBase32UnpaddedWith-, decodeBase32Padded+, decodeBase32PaddedUntyped , decodeBase32PaddedWith--- , decodeBase32Lenient-, isBase32Hex-, isValidBase32Hex+, decodeBase32+, decodeBase32Padded+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where +import Data.Base32.Types+import qualified Data.Text as ErrorText+ import Data.Bifunctor (first) import Data.ByteString (ByteString) import qualified Data.ByteString.Base32.Hex as B32H@@ -46,36 +53,39 @@ -- >>> encodeBase32 "Sun" -- "ADQMS===" ---encodeBase32 :: Text -> Text+-- @since 1.0+--+encodeBase32 :: Text -> Base32 'HexPadded Text encodeBase32 = B32H.encodeBase32 . T.encodeUtf8 {-# INLINE encodeBase32 #-} -- | Decode a padded Base32hex-encoded 'Text' value. If its length is not a multiple--- of 4, then padding chars will be added to fill out the input to a multiple of--- 4 for safe decoding as base32hex encodings are optionally padded.+-- of 8, then padding chars will be added to fill out the input to a multiple of+-- 8 for safe decoding as base32hex encodings are optionally padded. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'T.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32With`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32 "ADQMS==="+-- >>> decodeBase32Untyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32 "ADQMS"+-- >>> decodeBase32Untyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32 "ADQMS==="+-- >>> decodeBase32Untyped "ADQM===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: Text -> Either Text Text-decodeBase32 = fmap T.decodeUtf8 . B32H.decodeBase32 . T.encodeUtf8-{-# INLINE decodeBase32 #-}+-- @since 1.0+--+decodeBase32Untyped :: Text -> Either Text Text+decodeBase32Untyped value = B32H.decodeBase32Untyped (T.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . T.decodeUtf8'+{-# INLINE decodeBase32Untyped #-} -- | Attempt to decode a 'ByteString' value as Base32hex, converting from -- 'ByteString' to 'Text' according to some encoding function. In practice,@@ -85,18 +95,22 @@ -- -- === __Examples__: ----- @--- 'decodeBase32With' 'T.decodeUtf8''--- :: 'Text' -> 'Either' ('Base32Error' 'UnicodeException') 'Text'--- @+-- >>> decodeBase32With UTF8.decodeUtf8' "ADQMS==="+-- Right "Sun" --+-- >>> decodeBase32With UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32With (const (Left "unsupported encoding")) "ADQMS==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32With :: (ByteString -> Either err Text) -- ^ convert a bytestring to text (e.g. 'T.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) Text-decodeBase32With f t = case B32H.decodeBase32 t of+decodeBase32With f t = case B32H.decodeBase32Untyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32With #-}@@ -112,56 +126,61 @@ -- >>> encodeBase32Unpadded "Sun" -- "ADQMS" ---encodeBase32Unpadded :: Text -> Text+-- @since 1.0+--+encodeBase32Unpadded :: Text -> Base32 'HexUnpadded Text encodeBase32Unpadded = B32H.encodeBase32Unpadded . T.encodeUtf8 {-# INLINE encodeBase32Unpadded #-} -- | Decode an unpadded Base32hex encoded 'Text' value. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'T.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32WUnpaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Unpadded "ADQMS"+-- >>> decodeBase32UnpaddedUntyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32Unpadded "ADQMS==="+-- >>> decodeBase32UnpaddedUntyped "ADQMS===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: Text -> Either Text Text-decodeBase32Unpadded = fmap T.decodeUtf8- . B32H.decodeBase32Unpadded- . T.encodeUtf8-{-# INLINE decodeBase32Unpadded #-}+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: Text -> Either Text Text+decodeBase32UnpaddedUntyped value = B32H.decodeBase32UnpaddedUntyped (T.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . T.decodeUtf8'+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Attempt to decode an unpadded 'ByteString' value as Base32hex, converting from -- 'ByteString' to 'Text' according to some encoding function. In practice,--- This is something like 'decodeUtf8'', which may produce an error.+-- This is something like 'T.decodeUtf8'', which may produce an error. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Example__: ----- @--- 'decodeBase32UnpaddedWith' 'T.decodeUtf8''--- :: 'ByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'Text'--- @+-- >>> decodeBase32UnpaddedWith UTF8.decodeUtf8' "ADQMS"+-- Right "Sun" --+-- >>> decodeBase32UnpaddedWith UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32UnpaddedWith (const (Left "unsupported encoding")) "ADQMS"+-- Left (ConversionError "unsupported encoding")+-- decodeBase32UnpaddedWith :: (ByteString -> Either err Text) -- ^ convert a bytestring to text (e.g. 'T.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) Text-decodeBase32UnpaddedWith f t = case B32H.decodeBase32Unpadded t of+decodeBase32UnpaddedWith f t = case B32H.decodeBase32UnpaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32UnpaddedWith #-}@@ -169,27 +188,26 @@ -- | Decode an padded Base32hex encoded 'Text' value ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'T.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32PaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Padded "ADQMS==="+-- >>> decodeBase32PaddedUntyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32Padded "ADQMS"+-- >>> decodeBase32PaddedUntyped "ADQMS" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: Text -> Either Text Text-decodeBase32Padded = fmap T.decodeUtf8- . B32H.decodeBase32Padded- . T.encodeUtf8-{-# INLINE decodeBase32Padded #-}+-- @since 1.0+--+decodeBase32PaddedUntyped :: Text -> Either Text Text+decodeBase32PaddedUntyped value = B32H.decodeBase32PaddedUntyped (T.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . T.decodeUtf8'+{-# INLINE decodeBase32PaddedUntyped #-} -- | Attempt to decode a padded 'ByteString' value as Base32hex, converting from -- 'ByteString' to 'Text' according to some encoding function. In practice,@@ -199,68 +217,115 @@ -- -- === __Example__: ----- @--- 'decodeBase32PaddedWith' 'T.decodeUtf8''--- :: 'ByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'Text'--- @+-- >>> decodeBase32PaddedWith UTF8.decodeUtf8' "ADQMS==="+-- Right "Sun" --+-- >>> decodeBase32PaddedWith UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32PaddedWith (const (Left "unsupported encoding")) "ADQMS==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32PaddedWith :: (ByteString -> Either err Text) -- ^ convert a bytestring to text (e.g. 'T.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) Text-decodeBase32PaddedWith f t = case B32H.decodeBase32Padded t of+decodeBase32PaddedWith f t = case B32H.decodeBase32PaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32PaddedWith #-} --- -- -- | Leniently decode an unpadded Base32hex-encoded 'Text'. This function--- -- -- will not generate parse errors. If input data contains padding chars,--- -- -- then the input will be parsed up until the first pad character.--- -- ----- -- -- __Note:__ This is not RFC 4648-compliant.--- -- ----- -- decodeBase32Lenient :: Text -> Text--- -- decodeBase32Lenient = T.decodeUtf8--- -- . B32H.decodeBase32Lenient--- -- . T.encodeUtf8--- -- {-# INLINE decodeBase32Lenient #-}+-- | Decode a typed Base32hex-encoded 'Text' value.+--+-- This function is padding-agnostic.+-- The decoded payload must be valid UTF-8.+--+-- >>> decodeBase32 (encodeBase32 "Sun")+-- "Sun"+--+-- >>> decodeBase32 (encodeBase32Unpadded "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: HexAlphabet k => Base32 k Text -> Text+decodeBase32 = T.decodeUtf8 . B32H.decodeBase32 . fmap T.encodeUtf8+{-# INLINE decodeBase32 #-} --- | Tell whether a 'Text' value is Base32hex-encoded.+-- | Decode a typed, padded Base32hex-encoded 'Text' value. ----- === __Examples__:+-- The decoded payload must be valid UTF-8. ----- >>> isBase32Hex "ADQMS"--- True+-- >>> decodeBase32Padded (encodeBase32 "Sun")+-- "Sun" ----- >>> isBase32Hex "ADQMS==="--- True+-- @since 1.0 ----- >>> isBase32Hex "ADQMS=="--- False+decodeBase32Padded :: Base32 'HexPadded Text -> Text+decodeBase32Padded = decodeBase32+{-# INLINE decodeBase32Padded #-}++-- | Parse and validate a base32hex-encoded value, assuming that it has padding. ---isBase32Hex :: Text -> Bool-isBase32Hex = B32H.isBase32Hex . T.encodeUtf8-{-# INLINE isBase32Hex #-}+-- >>> parseBase32Padded "adqms==="+-- Right "adqms==="+--+-- >>> fmap decodeBase32Padded (parseBase32Padded "ADQMS===")+-- Right "Sun"+--+-- >>> parseBase32Padded ""+-- Right ""+--+-- >>> parseBase32Padded "ADQMS"+-- Left "Base32-encoded bytestring requires padding"+--+-- @since 1.0+--+parseBase32Padded :: Text -> Either Text (Base32 'HexPadded Text)+parseBase32Padded value = assertBase32 value+ <$ (B32H.decodeBase32PaddedUntyped . T.encodeUtf8) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'Text' value is a valid Base32hex format.+-- | Decode a typed, unpadded Base32hex-encoded value. ----- This will not tell you whether or not this is a correct Base32hex representation,--- only that it conforms to the correct shape. To check whether it is a true--- Base32 encoded 'Text' value, use 'isBase32Hex'.+-- The decoded payload must be valid UTF-8. ----- === __Examples__:+-- >>> decodeBase32Unpadded (encodeBase32Unpadded "Sun")+-- "Sun" ----- >>> isValidBase32Hex "ADQMS"--- True+-- @since 1.0 ----- >>> isValidBase32Hex "ADQMS="--- False+decodeBase32Unpadded :: Base32 'HexUnpadded Text -> Text+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32hex-encoded value, assuming that it is unpadded. ----- >>> isValidBase32Hex "ADQMS%"--- False+-- >>> parseBase32Unpadded "adqms"+-- Right "adqms" ---isValidBase32Hex :: Text -> Bool-isValidBase32Hex = B32H.isValidBase32Hex . T.encodeUtf8-{-# INLINE isValidBase32Hex #-}+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "ADQMS")+-- Right "Sun"+--+-- >>> parseBase32Unpadded ""+-- Right ""+--+-- >>> parseBase32Unpadded "ADQMS==="+-- Left "Base32-encoded bytestring has invalid padding"+--+-- >>> isLeft (parseBase32Unpadded "CP")+-- True+--+-- @since 1.0+--+parseBase32Unpadded :: Text -> Either Text (Base32 'HexUnpadded Text)+parseBase32Unpadded value = assertBase32 value+ <$ (B32H.decodeBase32UnpaddedUntyped . T.encodeUtf8) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import Data.Either (isLeft)+-- >>> import qualified Data.Text.Encoding as UTF8
src/Data/Text/Lazy/Encoding/Base32.hs view
@@ -1,7 +1,9 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE Safe #-} -- | -- Module : Data.Text.Lazy.Encoding.Base32--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -18,19 +20,24 @@ encodeBase32 , encodeBase32Unpadded -- * Decoding-, decodeBase32+, decodeBase32Untyped , decodeBase32With-, decodeBase32Unpadded+, decodeBase32UnpaddedUntyped , decodeBase32UnpaddedWith-, decodeBase32Padded+, decodeBase32PaddedUntyped , decodeBase32PaddedWith---, decodeBase32Lenient -- * Validation-, isBase32-, isValidBase32+, decodeBase32+, decodeBase32Padded+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where +import Data.Base32.Types+import qualified Data.Text as ErrorText+ import Data.Bifunctor (first) import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy.Base32 as BL32@@ -49,36 +56,40 @@ -- >>> encodeBase32 "Sun" -- "KN2W4===" ---encodeBase32 :: TL.Text -> TL.Text+-- @since 1.0+--+encodeBase32 :: TL.Text -> Base32 'StdPadded TL.Text encodeBase32 = BL32.encodeBase32 . TL.encodeUtf8 {-# INLINE encodeBase32 #-} --- | Decode an arbitrarily padded Base32-encoded 'TL.Text' value. If its length is not a multiple--- of 4, then padding chars will be added to fill out the input to a multiple of--- 4 for safe decoding as base32 encodings are optionally padded.+-- | Decode an arbitrarily padded Base32-encoded 'TL.Text' value. If its length is not a+-- multiple+-- of 8, then padding chars will be added to fill out the input to a multiple of+-- 8 for safe decoding as base32 encodings are optionally padded. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'TL.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32With`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32 "KN2W4==="+-- >>> decodeBase32Untyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32 "KN2W4"+-- >>> decodeBase32Untyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32 "KN2W==="+-- >>> decodeBase32Untyped "KN2W===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: TL.Text -> Either T.Text TL.Text-decodeBase32 = fmap TL.decodeUtf8 . BL32.decodeBase32 . TL.encodeUtf8-{-# INLINE decodeBase32 #-}+-- @since 1.0+--+decodeBase32Untyped :: TL.Text -> Either T.Text TL.Text+decodeBase32Untyped value = BL32.decodeBase32Untyped (TL.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . TL.decodeUtf8'+{-# INLINE decodeBase32Untyped #-} -- | Attempt to decode a lazy 'ByteString' value as Base32, converting from -- 'ByteString' to 'TL.Text' according to some encoding function. In practice,@@ -88,18 +99,22 @@ -- -- === __Examples__: ----- @--- 'decodeBase32With' 'TL.decodeUtf8''--- :: 'ByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'TL.Text'--- @+-- >>> decodeBase32With UTF8.decodeUtf8' "KN2W4==="+-- Right "Sun" --+-- >>> decodeBase32With UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32With (const (Left "unsupported encoding")) "KN2W4==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32With :: (ByteString -> Either err TL.Text) -- ^ convert a bytestring to text (e.g. 'TL.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) TL.Text-decodeBase32With f t = case BL32.decodeBase32 t of+decodeBase32With f t = case BL32.decodeBase32Untyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32With #-}@@ -115,33 +130,34 @@ -- >>> encodeBase32Unpadded "Sun" -- "KN2W4" ---encodeBase32Unpadded :: TL.Text -> TL.Text+-- @since 1.0+--+encodeBase32Unpadded :: TL.Text -> Base32 'StdUnpadded TL.Text encodeBase32Unpadded = BL32.encodeBase32Unpadded . TL.encodeUtf8 {-# INLINE encodeBase32Unpadded #-} -- | Decode an unpadded Base32 encoded 'TL.Text' value. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'TL.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32WUnpaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Unpadded "KN2W4"+-- >>> decodeBase32UnpaddedUntyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32Unpadded "KN2W4==="+-- >>> decodeBase32UnpaddedUntyped "KN2W4===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: TL.Text -> Either T.Text TL.Text-decodeBase32Unpadded = fmap TL.decodeUtf8- . BL32.decodeBase32Unpadded- . TL.encodeUtf8-{-# INLINE decodeBase32Unpadded #-}+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: TL.Text -> Either T.Text TL.Text+decodeBase32UnpaddedUntyped value = BL32.decodeBase32UnpaddedUntyped (TL.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . TL.decodeUtf8'+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Attempt to decode an unpadded lazy 'ByteString' value as Base32, converting from -- 'ByteString' to 'TL.Text' according to some encoding function. In practice,@@ -151,45 +167,48 @@ -- -- === __Examples__: ----- @--- 'decodeBase32UnpaddedWith' 'TL.decodeUtf8''--- :: 'ByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'TL.Text'--- @+-- >>> decodeBase32UnpaddedWith UTF8.decodeUtf8' "KN2W4"+-- Right "Sun" --+-- >>> decodeBase32UnpaddedWith UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32UnpaddedWith (const (Left "unsupported encoding")) "KN2W4"+-- Left (ConversionError "unsupported encoding")+-- decodeBase32UnpaddedWith :: (ByteString -> Either err TL.Text) -- ^ convert a bytestring to text (e.g. 'TL.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) TL.Text-decodeBase32UnpaddedWith f t = case BL32.decodeBase32Unpadded t of+decodeBase32UnpaddedWith f t = case BL32.decodeBase32UnpaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32UnpaddedWith #-} -- | Decode an padded Base32 encoded 'TL.Text' value ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'TL.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32PaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Padded "KN2W4==="+-- >>> decodeBase32PaddedUntyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32Padded "KN2W4"+-- >>> decodeBase32PaddedUntyped "KN2W4" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: TL.Text -> Either T.Text TL.Text-decodeBase32Padded = fmap TL.decodeUtf8- . BL32.decodeBase32Padded- . TL.encodeUtf8-{-# INLINE decodeBase32Padded #-}+-- @since 1.0+--+decodeBase32PaddedUntyped :: TL.Text -> Either T.Text TL.Text+decodeBase32PaddedUntyped value = BL32.decodeBase32PaddedUntyped (TL.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . TL.decodeUtf8'+{-# INLINE decodeBase32PaddedUntyped #-} -- | Attempt to decode a padded lazy 'ByteString' value as Base32, converting from -- 'ByteString' to 'TL.Text' according to some encoding function. In practice,@@ -199,76 +218,115 @@ -- -- === __Example__: ----- @--- 'decodeBase32PaddedWith' 'TL.decodeUtf8''--- :: 'ByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'Text'--- @+-- >>> decodeBase32PaddedWith UTF8.decodeUtf8' "KN2W4==="+-- Right "Sun" --+-- >>> decodeBase32PaddedWith UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32PaddedWith (const (Left "unsupported encoding")) "KN2W4==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32PaddedWith :: (ByteString -> Either err TL.Text) -- ^ convert a bytestring to text (e.g. 'TL.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) TL.Text-decodeBase32PaddedWith f t = case BL32.decodeBase32Padded t of+decodeBase32PaddedWith f t = case BL32.decodeBase32PaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32PaddedWith #-} --- -- | Leniently decode an unpadded Base32-encoded 'TL.Text'. This function--- -- will not generate parse errors. If input data contains padding chars,--- -- then the input will be parsed up until the first pad character.--- ----- -- __Note:__ This is not RFC 4648-compliant.--- ----- -- === __Examples__:--- ----- -- >>> decodeBase32Lenient "PDw_Pj4="--- -- "<<?>>"--- ----- -- >>> decodeBase32Lenient "PDw_%%%$}Pj4"--- -- "<<?>>"--- ----- decodeBase32Lenient :: TL.Text -> TL.Text--- decodeBase32Lenient = TL.decodeUtf8--- . BL32.decodeBase32Lenient--- . TL.encodeUtf8--- {-# INLINE decodeBase32Lenient #-}+-- | Decode a typed Base32-encoded 'TL.Text' value.+--+-- This function is padding-agnostic.+-- The decoded payload must be valid UTF-8.+--+-- >>> decodeBase32 (encodeBase32 "Sun")+-- "Sun"+--+-- >>> decodeBase32 (encodeBase32Unpadded "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: StdAlphabet k => Base32 k TL.Text -> TL.Text+decodeBase32 = TL.decodeUtf8 . BL32.decodeBase32 . fmap TL.encodeUtf8+{-# INLINE decodeBase32 #-} --- | Tell whether a 'TL.Text' value is Base32-encoded+-- | Decode a typed, padded Base32-encoded 'TL.Text' value. ----- === __Examples__:+-- The decoded payload must be valid UTF-8. ----- >>> isBase32 "KN2W4"--- True+-- >>> decodeBase32Padded (encodeBase32 "Sun")+-- "Sun" ----- >>> isBase32 "KN2W4==="--- True+-- @since 1.0 ----- >>> isBase32 "KN2W4=="--- False+decodeBase32Padded :: Base32 'StdPadded TL.Text -> TL.Text+decodeBase32Padded = decodeBase32+{-# INLINE decodeBase32Padded #-}++-- | Parse and validate a base32-encoded value, assuming that it has padding. ---isBase32 :: TL.Text -> Bool-isBase32 = BL32.isBase32 . TL.encodeUtf8-{-# INLINE isBase32 #-}+-- >>> parseBase32Padded "kn2w4==="+-- Right "kn2w4==="+--+-- >>> fmap decodeBase32Padded (parseBase32Padded "KN2W4===")+-- Right "Sun"+--+-- >>> parseBase32Padded ""+-- Right ""+--+-- >>> parseBase32Padded "KN2W4"+-- Left "Base32-encoded bytestring requires padding"+--+-- @since 1.0+--+parseBase32Padded :: TL.Text -> Either T.Text (Base32 'StdPadded TL.Text)+parseBase32Padded value = assertBase32 value+ <$ (BL32.decodeBase32PaddedUntyped . TL.encodeUtf8) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'TL.Text' value is a valid Base32 format.+-- | Decode a typed, unpadded Base32-encoded value. ----- This will not tell you whether or not this is a correct Base32 representation,--- only that it conforms to the correct shape. To check whether it is a true--- Base32 encoded 'TL.Text' value, use 'isBase32'.+-- The decoded payload must be valid UTF-8. ----- === __Examples__:+-- >>> decodeBase32Unpadded (encodeBase32Unpadded "Sun")+-- "Sun" ----- >>> isValidBase32 "KN2W4"--- True+-- @since 1.0 ----- >>> isValidBase32 "KN2W4="--- False+decodeBase32Unpadded :: Base32 'StdUnpadded TL.Text -> TL.Text+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32-encoded value, assuming that it is unpadded. ----- >>> isValidBase32 "KN2W4%"--- False+-- >>> parseBase32Unpadded "kn2w4"+-- Right "kn2w4" ---isValidBase32 :: TL.Text -> Bool-isValidBase32 = BL32.isValidBase32 . TL.encodeUtf8-{-# INLINE isValidBase32 #-}+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "KN2W4")+-- Right "Sun"+--+-- >>> parseBase32Unpadded ""+-- Right ""+--+-- >>> parseBase32Unpadded "KN2W4==="+-- Left "Base32-encoded bytestring has invalid padding"+--+-- >>> isLeft (parseBase32Unpadded "MZ")+-- True+--+-- @since 1.0+--+parseBase32Unpadded :: TL.Text -> Either T.Text (Base32 'StdUnpadded TL.Text)+parseBase32Unpadded value = assertBase32 value+ <$ (BL32.decodeBase32UnpaddedUntyped . TL.encodeUtf8) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import Data.Either (isLeft)+-- >>> import qualified Data.Text.Lazy.Encoding as UTF8
src/Data/Text/Lazy/Encoding/Base32/Hex.hs view
@@ -1,7 +1,9 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE Safe #-} -- | -- Module : Data.Text.Lazy.Encoding.Base32.Hex--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -18,19 +20,24 @@ encodeBase32 , encodeBase32Unpadded -- * Decoding-, decodeBase32+, decodeBase32Untyped , decodeBase32With-, decodeBase32Unpadded+, decodeBase32UnpaddedUntyped , decodeBase32UnpaddedWith-, decodeBase32Padded+, decodeBase32PaddedUntyped , decodeBase32PaddedWith---, decodeBase32Lenient -- * Validation-, isBase32Hex-, isValidBase32Hex+, decodeBase32+, decodeBase32Padded+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where +import Data.Base32.Types+import qualified Data.Text as ErrorText+ import Data.Bifunctor (first) import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy.Base32.Hex as BL32H@@ -49,36 +56,39 @@ -- >>> encodeBase32 "Sun" -- "ADQMS===" ---encodeBase32 :: TL.Text -> TL.Text+-- @since 1.0+--+encodeBase32 :: TL.Text -> Base32 'HexPadded TL.Text encodeBase32 = BL32H.encodeBase32 . TL.encodeUtf8 {-# INLINE encodeBase32 #-} -- | Decode a padded Base32hex-encoded 'TL.Text' value. If its length is not a multiple--- of 4, then padding chars will be added to fill out the input to a multiple of--- 4 for safe decoding as base32hex encodings are optionally padded.+-- of 8, then padding chars will be added to fill out the input to a multiple of+-- 8 for safe decoding as base32hex encodings are optionally padded. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'TL.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32With`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32 "ADQMS==="+-- >>> decodeBase32Untyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32 "ADQMS"+-- >>> decodeBase32Untyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32 "ADQMS==="+-- >>> decodeBase32Untyped "ADQM===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: TL.Text -> Either T.Text TL.Text-decodeBase32 = fmap TL.decodeUtf8 . BL32H.decodeBase32 . TL.encodeUtf8-{-# INLINE decodeBase32 #-}+-- @since 1.0+--+decodeBase32Untyped :: TL.Text -> Either T.Text TL.Text+decodeBase32Untyped value = BL32H.decodeBase32Untyped (TL.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . TL.decodeUtf8'+{-# INLINE decodeBase32Untyped #-} -- | Attempt to decode a lazy 'ByteString' value as Base32hex, converting from -- 'ByteString' to 'TL.Text' according to some encoding function. In practice,@@ -88,18 +98,22 @@ -- -- === __Examples__: ----- @--- 'decodeBase32With' 'TL.decodeUtf8''--- :: 'ByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'TL.Text'--- @+-- >>> decodeBase32With UTF8.decodeUtf8' "ADQMS==="+-- Right "Sun" --+-- >>> decodeBase32With UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32With (const (Left "unsupported encoding")) "ADQMS==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32With :: (ByteString -> Either err TL.Text) -- ^ convert a bytestring to text (e.g. 'TL.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) TL.Text-decodeBase32With f t = case BL32H.decodeBase32 t of+decodeBase32With f t = case BL32H.decodeBase32Untyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32With #-}@@ -115,33 +129,35 @@ -- >>> encodeBase32Unpadded "Sun" -- "ADQMS" ---encodeBase32Unpadded :: TL.Text -> TL.Text+-- @since 1.0+--+encodeBase32Unpadded :: TL.Text -> Base32 'HexUnpadded TL.Text encodeBase32Unpadded = BL32H.encodeBase32Unpadded . TL.encodeUtf8 {-# INLINE encodeBase32Unpadded #-} -- | Decode an unpadded Base32hex encoded 'TL.Text' value. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'TL.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32WUnpaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32Unpadded "ADQMS"+-- >>> decodeBase32UnpaddedUntyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32Unpadded "ADQMS==="+-- >>> decodeBase32UnpaddedUntyped "ADQMS===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: TL.Text -> Either T.Text TL.Text-decodeBase32Unpadded = fmap TL.decodeUtf8- . BL32H.decodeBase32Unpadded- . TL.encodeUtf8-{-# INLINE decodeBase32Unpadded #-}+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: TL.Text -> Either T.Text TL.Text+decodeBase32UnpaddedUntyped value =+ BL32H.decodeBase32UnpaddedUntyped (TL.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . TL.decodeUtf8'+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Attempt to decode an unpadded lazy 'ByteString' value as Base32hex, converting from -- 'ByteString' to 'TL.Text' according to some encoding function. In practice,@@ -151,45 +167,48 @@ -- -- === __Examples__: ----- @--- 'decodeBase32UnpaddedWith' 'TL.decodeUtf8''--- :: 'ByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'TL.Text'--- @+-- >>> decodeBase32UnpaddedWith UTF8.decodeUtf8' "ADQMS"+-- Right "Sun" --+-- >>> decodeBase32UnpaddedWith UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32UnpaddedWith (const (Left "unsupported encoding")) "ADQMS"+-- Left (ConversionError "unsupported encoding")+-- decodeBase32UnpaddedWith :: (ByteString -> Either err TL.Text) -- ^ convert a bytestring to text (e.g. 'TL.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) TL.Text-decodeBase32UnpaddedWith f t = case BL32H.decodeBase32Unpadded t of+decodeBase32UnpaddedWith f t = case BL32H.decodeBase32UnpaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32UnpaddedWith #-} -- | Decode an padded Base32hex encoded 'TL.Text' value ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'TL.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32PaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32Padded "ADQMS==="+-- >>> decodeBase32PaddedUntyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32Padded "ADQMS"+-- >>> decodeBase32PaddedUntyped "ADQMS" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: TL.Text -> Either T.Text TL.Text-decodeBase32Padded = fmap TL.decodeUtf8- . BL32H.decodeBase32Padded- . TL.encodeUtf8-{-# INLINE decodeBase32Padded #-}+-- @since 1.0+--+decodeBase32PaddedUntyped :: TL.Text -> Either T.Text TL.Text+decodeBase32PaddedUntyped value = BL32H.decodeBase32PaddedUntyped (TL.encodeUtf8 value)+ >>= first (ErrorText.pack . show) . TL.decodeUtf8'+{-# INLINE decodeBase32PaddedUntyped #-} -- | Attempt to decode a padded lazy 'ByteString' value as Base32hex, converting from -- 'ByteString' to 'TL.Text' according to some encoding function. In practice,@@ -199,76 +218,115 @@ -- -- === __Example__: ----- @--- 'decodeBase32PaddedWith' 'TL.decodeUtf8''--- :: 'ByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'Text'--- @+-- >>> decodeBase32PaddedWith UTF8.decodeUtf8' "ADQMS==="+-- Right "Sun" --+-- >>> decodeBase32PaddedWith UTF8.decodeUtf8' "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32PaddedWith (const (Left "unsupported encoding")) "ADQMS==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32PaddedWith :: (ByteString -> Either err TL.Text) -- ^ convert a bytestring to text (e.g. 'TL.decodeUtf8'') -> ByteString -- ^ Input text to decode -> Either (Base32Error err) TL.Text-decodeBase32PaddedWith f t = case BL32H.decodeBase32Padded t of+decodeBase32PaddedWith f t = case BL32H.decodeBase32PaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32PaddedWith #-} --- -- | Leniently decode an unpadded Base32hex-encoded 'TL.Text'. This function--- -- will not generate parse errors. If input data contains padding chars,--- -- then the input will be parsed up until the first pad character.--- ----- -- __Note:__ This is not RFC 4648-compliant.--- ----- -- === __Examples__:--- ----- -- >>> decodeBase32Lenient "PDw_Pj4="--- -- "<<?>>"--- ----- -- >>> decodeBase32Lenient "PDw_%%%$}Pj4"--- -- "<<?>>"--- ----- decodeBase32Lenient :: TL.Text -> TL.Text--- decodeBase32Lenient = TL.decodeUtf8--- . BL32H.decodeBase32Lenient--- . TL.encodeUtf8--- {-# INLINE decodeBase32Lenient #-}+-- | Decode a typed Base32hex-encoded 'TL.Text' value.+--+-- This function is padding-agnostic.+-- The decoded payload must be valid UTF-8.+--+-- >>> decodeBase32 (encodeBase32 "Sun")+-- "Sun"+--+-- >>> decodeBase32 (encodeBase32Unpadded "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: HexAlphabet k => Base32 k TL.Text -> TL.Text+decodeBase32 = TL.decodeUtf8 . BL32H.decodeBase32 . fmap TL.encodeUtf8+{-# INLINE decodeBase32 #-} --- | Tell whether a 'TL.Text' value is Base32hex-encoded+-- | Decode a typed, padded Base32hex-encoded 'TL.Text' value. ----- === __Examples__:+-- The decoded payload must be valid UTF-8. ----- >>> isBase32Hex "ADQMS"--- True+-- >>> decodeBase32Padded (encodeBase32 "Sun")+-- "Sun" ----- >>> isBase32Hex "ADQMS==="--- True+-- @since 1.0 ----- >>> isBase32Hex "ADQMS=="--- False+decodeBase32Padded :: Base32 'HexPadded TL.Text -> TL.Text+decodeBase32Padded = decodeBase32+{-# INLINE decodeBase32Padded #-}++-- | Parse and validate a base32hex-encoded value, assuming that it has padding. ---isBase32Hex :: TL.Text -> Bool-isBase32Hex = BL32H.isBase32Hex . TL.encodeUtf8-{-# INLINE isBase32Hex #-}+-- >>> parseBase32Padded "adqms==="+-- Right "adqms==="+--+-- >>> fmap decodeBase32Padded (parseBase32Padded "ADQMS===")+-- Right "Sun"+--+-- >>> parseBase32Padded ""+-- Right ""+--+-- >>> parseBase32Padded "ADQMS"+-- Left "Base32-encoded bytestring requires padding"+--+-- @since 1.0+--+parseBase32Padded :: TL.Text -> Either T.Text (Base32 'HexPadded TL.Text)+parseBase32Padded value = assertBase32 value+ <$ (BL32H.decodeBase32PaddedUntyped . TL.encodeUtf8) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'TL.Text' value is a valid Base32hex format.+-- | Decode a typed, unpadded Base32hex-encoded value. ----- This will not tell you whether or not this is a correct Base32hex representation,--- only that it conforms to the correct shape. To check whether it is a true--- Base32 encoded 'TL.Text' value, use 'isBase32Hex'.+-- The decoded payload must be valid UTF-8. ----- === __Examples__:+-- >>> decodeBase32Unpadded (encodeBase32Unpadded "Sun")+-- "Sun" ----- >>> isValidBase32Hex "ADQMS"--- True+-- @since 1.0 ----- >>> isValidBase32Hex "ADQMS="--- False+decodeBase32Unpadded :: Base32 'HexUnpadded TL.Text -> TL.Text+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32hex-encoded value, assuming that it is unpadded. ----- >>> isValidBase32Hex "ADQMS%"--- False+-- >>> parseBase32Unpadded "adqms"+-- Right "adqms" ---isValidBase32Hex :: TL.Text -> Bool-isValidBase32Hex = BL32H.isValidBase32Hex . TL.encodeUtf8-{-# INLINE isValidBase32Hex #-}+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "ADQMS")+-- Right "Sun"+--+-- >>> parseBase32Unpadded ""+-- Right ""+--+-- >>> parseBase32Unpadded "ADQMS==="+-- Left "Base32-encoded bytestring has invalid padding"+--+-- >>> isLeft (parseBase32Unpadded "CP")+-- True+--+-- @since 1.0+--+parseBase32Unpadded :: TL.Text -> Either T.Text (Base32 'HexUnpadded TL.Text)+parseBase32Unpadded value = assertBase32 value+ <$ (BL32H.decodeBase32UnpaddedUntyped . TL.encodeUtf8) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import Data.Either (isLeft)+-- >>> import qualified Data.Text.Lazy.Encoding as UTF8
src/Data/Text/Short/Encoding/Base32.hs view
@@ -1,7 +1,10 @@-{-# LANGUAGE Trustworthy #-}+{-# LANGUAGE Safe #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+ -- | -- Module : Data.Text.Short.Encoding.Base32--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -18,91 +21,107 @@ encodeBase32 , encodeBase32Unpadded -- * Decoding-, decodeBase32+, decodeBase32Untyped , decodeBase32With-, decodeBase32Unpadded+, decodeBase32UnpaddedUntyped , decodeBase32UnpaddedWith-, decodeBase32Padded+, decodeBase32PaddedUntyped , decodeBase32PaddedWith---, decodeBase32Lenient -- * Validation-, isBase32-, isValidBase32+, decodeBase32+, decodeBase32Padded+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where +import Data.Base32.Types+import qualified Data.Text as ErrorText+ import Data.Bifunctor (first)-import qualified Data.ByteString.Base32 as B32 import Data.ByteString.Short (ShortByteString)+import qualified Data.ByteString.Short as SBS import qualified Data.ByteString.Short.Base32 as BS32 import Data.Text (Text)-import qualified Data.Text.Encoding.Base32 as B32T+import qualified Data.Text.Encoding as TE import Data.Text.Encoding.Base32.Error import Data.Text.Short-import Data.Text.Short.Unsafe + -- | Encode a 'ShortText' value in Base32 with padding. ----- See: <https://tools.ietf.org/html/rfc4328#section-6 RFC-4328 section 6>+-- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: -- -- >>> encodeBase32 "Sun" -- "KN2W4===" ---encodeBase32 :: ShortText -> ShortText-encodeBase32 = fromByteStringUnsafe- . B32.encodeBase32'- . toByteString+-- @since 1.0+--+encodeBase32 :: ShortText -> Base32 'StdPadded ShortText+encodeBase32 =+ BS32.encodeBase32+ . toShortByteString {-# INLINE encodeBase32 #-} --- | Decode an arbitrarily padded Base32-encoded 'ShortText' value. If its length is not a multiple--- of 4, then padding chars will be added to fill out the input to a multiple of--- 4 for safe decoding as base32 encodings are optionally padded.+-- | Decode an arbitrarily padded Base32-encoded 'ShortText' value. If its length is not a+-- multiple+-- of 8, then padding chars will be added to fill out the input to a multiple of+-- 8 for safe decoding as base32 encodings are optionally padded. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'Data.Text.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32With`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. ----- See: <https://tools.ietf.org/html/rfc4328#section-6 RFC-4328 section 6>+-- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32 "KN2W4==="+-- >>> decodeBase32Untyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32 "KN2W4"+-- >>> decodeBase32Untyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32 "KN2W==="+-- >>> decodeBase32Untyped "KN2W===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: ShortText -> Either Text ShortText-decodeBase32 = fmap fromText . B32T.decodeBase32 . toText-{-# INLINE decodeBase32 #-}+-- @since 1.0+--+decodeBase32Untyped :: ShortText -> Either Text ShortText+decodeBase32Untyped value =+ BS32.decodeBase32Untyped (toShortByteString value)+ >>= maybe (Left (ErrorText.pack "invalid UTF-8 payload")) Right+ . fromShortByteString+{-# INLINE decodeBase32Untyped #-} -- | Attempt to decode a 'ShortByteString' value as Base32, converting from--- 'ByteString' to 'ShortText' according to some encoding function. In practice,--- This is something like 'decodeUtf8'', which may produce an error.+-- 'ShortByteString' to 'ShortText' according to some encoding function. In practice,+-- The conversion can report a character-encoding error. ----- See: <https://tools.ietf.org/html/rfc4328#section-6 RFC-4328 section 6>+-- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- @--- 'decodeBase32With' '(fmap fromText . T.decodeUtf8' . toText)'--- :: 'ShortByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'ShortText'--- @+-- >>> let utf8 = maybe (Left "invalid UTF-8") Right . Text.fromShortByteString+-- >>> decodeBase32With utf8 "KN2W4==="+-- Right "Sun" --+-- >>> decodeBase32With utf8 "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32With (const (Left "unsupported encoding")) "KN2W4==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32With :: (ShortByteString -> Either err ShortText) -- ^ convert a bytestring to text (e.g. 'T.decodeUtf8'') -> ShortByteString -- ^ Input text to decode -> Either (Base32Error err) ShortText-decodeBase32With f t = case BS32.decodeBase32 t of+decodeBase32With f t = case BS32.decodeBase32Untyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32With #-}@@ -111,163 +130,219 @@ -- padding is optional. If you call this function, you will simply be encoding -- as Base32 and stripping padding chars from the output. ----- See: <https://tools.ietf.org/html/rfc4328#section-6 RFC-4328 section 6>+-- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: -- -- >>> encodeBase32Unpadded "Sun" -- "KN2W4" ---encodeBase32Unpadded :: ShortText -> ShortText-encodeBase32Unpadded = fromByteStringUnsafe- . B32.encodeBase32Unpadded'- . toByteString+-- @since 1.0+--+encodeBase32Unpadded :: ShortText -> Base32 'StdUnpadded ShortText+encodeBase32Unpadded =+ BS32.encodeBase32Unpadded+ . toShortByteString {-# INLINE encodeBase32Unpadded #-} -- | Decode an unpadded Base32 encoded 'ShortText' value. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'T.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32UnpaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. ----- See: <https://tools.ietf.org/html/rfc4328#section-6 RFC-4328 section 6>+-- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Unpadded "KN2W4"+-- >>> decodeBase32UnpaddedUntyped "KN2W4" -- Right "Sun" ----- >>> decodeBase32Unpadded "KN2W4==="+-- >>> decodeBase32UnpaddedUntyped "KN2W4===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: ShortText -> Either Text ShortText-decodeBase32Unpadded = fmap fromText . B32T.decodeBase32Unpadded . toText-{-# INLINE decodeBase32Unpadded #-}+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: ShortText -> Either Text ShortText+decodeBase32UnpaddedUntyped value =+ BS32.decodeBase32UnpaddedUntyped (toShortByteString value)+ >>= maybe (Left (ErrorText.pack "invalid UTF-8 payload")) Right+ . fromShortByteString+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Attempt to decode an unpadded 'ShortByteString' value as Base32, converting from -- 'ShortByteString' to 'ShortText' according to some encoding function. In practice,--- This is something like 'decodeUtf8'', which may produce an error.+-- The conversion can report a character-encoding error. ----- See: <https://tools.ietf.org/html/rfc4328#section-6 RFC-4328 section 6>+-- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- @--- 'decodeBase32UnpaddedWith' '(fmap fromText . T.decodeUtf8' . toText)'--- :: 'ShortByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'ShortText'--- @+-- >>> let utf8 = maybe (Left "invalid UTF-8") Right . Text.fromShortByteString+-- >>> decodeBase32UnpaddedWith utf8 "KN2W4"+-- Right "Sun" --+-- >>> decodeBase32UnpaddedWith utf8 "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32UnpaddedWith (const (Left "unsupported encoding")) "KN2W4"+-- Left (ConversionError "unsupported encoding")+-- decodeBase32UnpaddedWith :: (ShortByteString -> Either err ShortText) -- ^ convert a bytestring to text (e.g. 'T.decodeUtf8'') -> ShortByteString -- ^ Input text to decode -> Either (Base32Error err) ShortText-decodeBase32UnpaddedWith f t = case BS32.decodeBase32Unpadded t of+decodeBase32UnpaddedWith f t = case BS32.decodeBase32UnpaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32UnpaddedWith #-} -- | Decode an padded Base32 encoded 'ShortText' value ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'T.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32PaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. ----- See: <https://tools.ietf.org/html/rfc4328#section-6 RFC-4328 section 6>+-- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- >>> decodeBase32Padded "KN2W4==="+-- >>> decodeBase32PaddedUntyped "KN2W4===" -- Right "Sun" ----- >>> decodeBase32Padded "KN2W4"+-- >>> decodeBase32PaddedUntyped "KN2W4" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: ShortText -> Either Text ShortText-decodeBase32Padded = fmap fromText . B32T.decodeBase32Padded . toText-{-# INLINE decodeBase32Padded #-}+-- @since 1.0+--+decodeBase32PaddedUntyped :: ShortText -> Either Text ShortText+decodeBase32PaddedUntyped value =+ BS32.decodeBase32PaddedUntyped (toShortByteString value)+ >>= maybe (Left (ErrorText.pack "invalid UTF-8 payload")) Right+ . fromShortByteString+{-# INLINE decodeBase32PaddedUntyped #-} -- | Attempt to decode a padded 'ShortByteString' value as Base32, converting from--- 'ByteString' to 'ShortText' according to some encoding function. In practice,--- This is something like 'decodeUtf8'', which may produce an error.+-- 'ShortByteString' to 'ShortText' according to some encoding function. In practice,+-- The conversion can report a character-encoding error. ----- See: <https://tools.ietf.org/html/rfc4328#section-6 RFC-4328 section 6>+-- See: <https://tools.ietf.org/html/rfc4648#section-6 RFC-4648 section 6> -- -- === __Examples__: ----- @--- 'decodeBase32With' '(fmap fromText . T.decodeUtf8' . toText)'--- :: 'ShortByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'ShortText'--- @+-- >>> let utf8 = maybe (Left "invalid UTF-8") Right . Text.fromShortByteString+-- >>> decodeBase32PaddedWith utf8 "KN2W4==="+-- Right "Sun" --+-- >>> decodeBase32PaddedWith utf8 "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32PaddedWith (const (Left "unsupported encoding")) "KN2W4==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32PaddedWith :: (ShortByteString -> Either err ShortText) -- ^ convert a bytestring to text (e.g. 'T.decodeUtf8'') -> ShortByteString -- ^ Input text to decode -> Either (Base32Error err) ShortText-decodeBase32PaddedWith f t = case BS32.decodeBase32Padded t of+decodeBase32PaddedWith f t = case BS32.decodeBase32PaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32PaddedWith #-} --- -- | Leniently decode an unpadded Base32-encoded 'ShortText'. This function--- -- will not generate parse errors. If input data contains padding chars,--- -- then the input will be parsed up until the first pad character.--- ----- -- __Note:__ This is not RFC 4328-compliant.--- ----- -- === __Examples__:--- ----- -- >>> decodeBase32Lenient "PDw_Pj4="--- -- "<<?>>"--- ----- -- >>> decodeBase32Lenient "PDw_%%%$}Pj4"--- -- "<<?>>"--- ----- decodeBase32Lenient :: ShortText -> ShortText--- decodeBase32Lenient = fromText . B32T.decodeBase32Lenient . toText--- {-# INLINE decodeBase32Lenient #-}+-- | Decode a typed Base32-encoded 'ShortText' value.+--+-- This function is padding-agnostic.+-- The decoded payload must be valid UTF-8.+--+-- >>> decodeBase32 (encodeBase32 "Sun")+-- "Sun"+--+-- >>> decodeBase32 (encodeBase32Unpadded "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: StdAlphabet k => Base32 k ShortText -> ShortText+decodeBase32 = fromText . TE.decodeUtf8 . SBS.fromShort+ . BS32.decodeBase32 . fmap toShortByteString+{-# INLINE decodeBase32 #-} --- | Tell whether a 'ShortText' value is Base32-encoded.+-- | Decode a typed, padded Base32-encoded 'ShortText' value. ----- === __Examples__:+-- The decoded payload must be valid UTF-8. ----- >>> isBase32 "KN2W4"--- True+-- >>> decodeBase32Padded (encodeBase32 "Sun")+-- "Sun" ----- >>> isBase32 "KN2W4==="--- True+-- @since 1.0 ----- >>> isBase32 "KN2W4=="--- False+decodeBase32Padded :: Base32 'StdPadded ShortText -> ShortText+decodeBase32Padded = decodeBase32+{-# INLINE decodeBase32Padded #-}++-- | Parse and validate a base32-encoded value, assuming that it has padding. ---isBase32 :: ShortText -> Bool-isBase32 = B32.isBase32 . toByteString-{-# INLINE isBase32 #-}+-- >>> parseBase32Padded "kn2w4==="+-- Right "kn2w4==="+--+-- >>> fmap decodeBase32Padded (parseBase32Padded "KN2W4===")+-- Right "Sun"+--+-- >>> parseBase32Padded ""+-- Right ""+--+-- >>> parseBase32Padded "KN2W4"+-- Left "Base32-encoded bytestring requires padding"+--+-- @since 1.0+--+parseBase32Padded :: ShortText -> Either Text (Base32 'StdPadded ShortText)+parseBase32Padded value = assertBase32 value+ <$ (BS32.decodeBase32PaddedUntyped . toShortByteString) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'ShortText' value is a valid Base32 format.+-- | Decode a typed, unpadded Base32-encoded value. ----- This will not tell you whether or not this is a correct Base32 representation,--- only that it conforms to the correct shape. To check whether it is a true--- Base32 encoded 'ShortText' value, use 'isBase32'.+-- The decoded payload must be valid UTF-8. ----- === __Examples__:+-- >>> decodeBase32Unpadded (encodeBase32Unpadded "Sun")+-- "Sun" ----- >>> isValidBase32 "KN2W4"--- True+-- @since 1.0 ----- >>> isValidBase32 "KN2W4="--- False+decodeBase32Unpadded :: Base32 'StdUnpadded ShortText -> ShortText+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32-encoded value, assuming that it is unpadded. ----- >>> isValidBase32 "KN2W4%"--- False+-- >>> parseBase32Unpadded "kn2w4"+-- Right "kn2w4" ---isValidBase32 :: ShortText -> Bool-isValidBase32 = B32.isValidBase32 . toByteString-{-# INLINE isValidBase32 #-}+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "KN2W4")+-- Right "Sun"+--+-- >>> parseBase32Unpadded ""+-- Right ""+--+-- >>> parseBase32Unpadded "KN2W4==="+-- Left "Base32-encoded bytestring has invalid padding"+--+-- >>> isLeft (parseBase32Unpadded "MZ")+-- True+--+-- @since 1.0+--+parseBase32Unpadded :: ShortText -> Either Text (Base32 'StdUnpadded ShortText)+parseBase32Unpadded value = assertBase32 value+ <$ (BS32.decodeBase32UnpaddedUntyped . toShortByteString) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import qualified Data.Text.Short as Text+-- >>> import Data.Either (isLeft)
src/Data/Text/Short/Encoding/Base32/Hex.hs view
@@ -1,7 +1,10 @@-{-# LANGUAGE Trustworthy #-}+{-# LANGUAGE Safe #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+ -- | -- Module : Data.Text.Short.Encoding.Base32.Hex--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -18,29 +21,34 @@ encodeBase32 , encodeBase32Unpadded -- * Decoding-, decodeBase32+, decodeBase32Untyped , decodeBase32With-, decodeBase32Unpadded+, decodeBase32UnpaddedUntyped , decodeBase32UnpaddedWith-, decodeBase32Padded+, decodeBase32PaddedUntyped , decodeBase32PaddedWith---, decodeBase32Lenient -- * Validation-, isBase32Hex-, isValidBase32Hex+, decodeBase32+, decodeBase32Padded+, decodeBase32Unpadded+, parseBase32Padded+, parseBase32Unpadded ) where +import Data.Base32.Types+import qualified Data.Text as ErrorText+ import Data.Bifunctor (first)-import qualified Data.ByteString.Base32.Hex as B32H import Data.ByteString.Short (ShortByteString)+import qualified Data.ByteString.Short as SBS import qualified Data.ByteString.Short.Base32.Hex as BS32H import Data.Text (Text)-import qualified Data.Text.Encoding.Base32.Hex as B32TH+import qualified Data.Text.Encoding as TE import Data.Text.Encoding.Base32.Error import Data.Text.Short-import Data.Text.Short.Unsafe + -- | Encode a 'ShortText' value in Base32hex with padding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7>@@ -50,59 +58,71 @@ -- >>> encodeBase32 "Sun" -- "ADQMS===" ---encodeBase32 :: ShortText -> ShortText-encodeBase32 = fromByteStringUnsafe- . B32H.encodeBase32'- . toByteString+-- @since 1.0+--+encodeBase32 :: ShortText -> Base32 'HexPadded ShortText+encodeBase32 =+ BS32H.encodeBase32+ . toShortByteString {-# INLINE encodeBase32 #-} --- | Decode an arbitrarily padded Base32hex-encoded 'ShortText' value. If its length is not a multiple--- of 4, then padding chars will be added to fill out the input to a multiple of--- 4 for safe decoding as base32hex encodings are optionally padded.+-- | Decode an arbitrarily padded Base32hex-encoded 'ShortText' value. If its length is+-- not a multiple+-- of 8, then padding chars will be added to fill out the input to a multiple of+-- 8 for safe decoding as base32hex encodings are optionally padded. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'Data.Text.Encoding.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32With`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32 "ADQMS==="+-- >>> decodeBase32Untyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32 "ADQMS"+-- >>> decodeBase32Untyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32 "ADQMS==="+-- >>> decodeBase32Untyped "ADQM===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32 :: ShortText -> Either Text ShortText-decodeBase32 = fmap fromText . B32TH.decodeBase32 . toText-{-# INLINE decodeBase32 #-}+-- @since 1.0+--+decodeBase32Untyped :: ShortText -> Either Text ShortText+decodeBase32Untyped value =+ BS32H.decodeBase32Untyped (toShortByteString value)+ >>= maybe (Left (ErrorText.pack "invalid UTF-8 payload")) Right+ . fromShortByteString+{-# INLINE decodeBase32Untyped #-} -- | Attempt to decode a 'ShortByteString' value as Base32hex, converting from--- 'ByteString' to 'ShortText' according to some encoding function. In practice,--- This is something like 'decodeUtf8'', which may produce an error.+-- 'ShortByteString' to 'ShortText' according to some encoding function. In practice,+-- The conversion can report a character-encoding error. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- @--- 'decodeBase32With' '(fmap fromText . Data.Text.Encoding.decodeUtf8' . toText)'--- :: 'ShortByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'ShortText'--- @+-- >>> let utf8 = maybe (Left "invalid UTF-8") Right . Text.fromShortByteString+-- >>> decodeBase32With utf8 "ADQMS==="+-- Right "Sun" --+-- >>> decodeBase32With utf8 "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32With (const (Left "unsupported encoding")) "ADQMS==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32With :: (ShortByteString -> Either err ShortText)- -- ^ convert a bytestring to text (e.g. '(fmap fromText . Data.Text.Encoding.decodeUtf8' . toText)')+ -- ^ convert a bytestring to text (e.g. '(fmap fromText .+ -- Data.Text.Encoding.decodeUtf8' . toText)') -> ShortByteString -- ^ Input text to decode -> Either (Base32Error err) ShortText-decodeBase32With f t = case BS32H.decodeBase32 t of+decodeBase32With f t = case BS32H.decodeBase32Untyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32With #-}@@ -118,156 +138,213 @@ -- >>> encodeBase32Unpadded "Sun" -- "ADQMS" ---encodeBase32Unpadded :: ShortText -> ShortText-encodeBase32Unpadded = fromByteStringUnsafe- . B32H.encodeBase32Unpadded'- . toByteString+-- @since 1.0+--+encodeBase32Unpadded :: ShortText -> Base32 'HexUnpadded ShortText+encodeBase32Unpadded =+ BS32H.encodeBase32Unpadded+ . toShortByteString {-# INLINE encodeBase32Unpadded #-} -- | Decode an unpadded Base32hex encoded 'ShortText' value. ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'Data.Text.Encoding.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32UnpaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32Unpadded "ADQMS"+-- >>> decodeBase32UnpaddedUntyped "ADQMS" -- Right "Sun" ----- >>> decodeBase32Unpadded "ADQMS==="+-- >>> decodeBase32UnpaddedUntyped "ADQMS===" -- Left "Base32-encoded bytestring has invalid padding" ---decodeBase32Unpadded :: ShortText -> Either Text ShortText-decodeBase32Unpadded = fmap fromText . B32TH.decodeBase32Unpadded . toText-{-# INLINE decodeBase32Unpadded #-}+-- @since 1.0+--+decodeBase32UnpaddedUntyped :: ShortText -> Either Text ShortText+decodeBase32UnpaddedUntyped value =+ BS32H.decodeBase32UnpaddedUntyped (toShortByteString value)+ >>= maybe (Left (ErrorText.pack "invalid UTF-8 payload")) Right+ . fromShortByteString+{-# INLINE decodeBase32UnpaddedUntyped #-} -- | Attempt to decode an unpadded 'ShortByteString' value as Base32hex, converting from -- 'ShortByteString' to 'ShortText' according to some encoding function. In practice,--- This is something like 'decodeUtf8'', which may produce an error.+-- The conversion can report a character-encoding error. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- @--- 'decodeBase32UnpaddedWith' '(fmap fromText . Data.Text.Encoding.decodeUtf8' . toText)'--- :: 'ShortByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'ShortText'--- @+-- >>> let utf8 = maybe (Left "invalid UTF-8") Right . Text.fromShortByteString+-- >>> decodeBase32UnpaddedWith utf8 "ADQMS"+-- Right "Sun" --+-- >>> decodeBase32UnpaddedWith utf8 "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32UnpaddedWith (const (Left "unsupported encoding")) "ADQMS"+-- Left (ConversionError "unsupported encoding")+-- decodeBase32UnpaddedWith :: (ShortByteString -> Either err ShortText)- -- ^ convert a bytestring to text (e.g. '(fmap fromText . Data.Text.Encoding.decodeUtf8' . toText)')+ -- ^ convert a bytestring to text (e.g. '(fmap fromText .+ -- Data.Text.Encoding.decodeUtf8' . toText)') -> ShortByteString -- ^ Input text to decode -> Either (Base32Error err) ShortText-decodeBase32UnpaddedWith f t = case BS32H.decodeBase32Unpadded t of+decodeBase32UnpaddedWith f t = case BS32H.decodeBase32UnpaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32UnpaddedWith #-} -- | Decode an padded Base32hex encoded 'ShortText' value ----- /Note:/ This function makes sure that decoding is total by deferring to--- 'Data.Text.Encoding.decodeUtf8'. This will always round trip for any valid Base32-encoded--- text value, but it may not round trip for bad inputs. The onus is on the--- caller to make sure inputs are valid. If unsure, defer to `decodeBase32PaddedWith`--- and pass in a custom decode function.+-- Decoded bytes are interpreted as UTF-8. Invalid Base32 or UTF-8+-- returns 'Left'. Use the corresponding @decode*With@ function+-- to choose a different character encoding. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- >>> decodeBase32Padded "ADQMS==="+-- >>> decodeBase32PaddedUntyped "ADQMS===" -- Right "Sun" ----- >>> decodeBase32Padded "ADQMS"+-- >>> decodeBase32PaddedUntyped "ADQMS" -- Left "Base32-encoded bytestring requires padding" ---decodeBase32Padded :: ShortText -> Either Text ShortText-decodeBase32Padded = fmap fromText . B32TH.decodeBase32Padded . toText-{-# INLINE decodeBase32Padded #-}+-- @since 1.0+--+decodeBase32PaddedUntyped :: ShortText -> Either Text ShortText+decodeBase32PaddedUntyped value =+ BS32H.decodeBase32PaddedUntyped (toShortByteString value)+ >>= maybe (Left (ErrorText.pack "invalid UTF-8 payload")) Right+ . fromShortByteString+{-# INLINE decodeBase32PaddedUntyped #-} -- | Attempt to decode a padded 'ShortByteString' value as Base32hex, converting from--- 'ByteString' to 'ShortText' according to some encoding function. In practice,--- This is something like 'decodeUtf8'', which may produce an error.+-- 'ShortByteString' to 'ShortText' according to some encoding function. In practice,+-- The conversion can report a character-encoding error. -- -- See: <https://tools.ietf.org/html/rfc4648#section-7 RFC-4648 section 7> -- -- === __Examples__: ----- @--- 'decodeBase32With' '(fmap fromText . Data.Text.Encoding.decodeUtf8' . toText)'--- :: 'ShortByteString' -> 'Either' ('Base32Error' 'UnicodeException') 'ShortText'--- @+-- >>> let utf8 = maybe (Left "invalid UTF-8") Right . Text.fromShortByteString+-- >>> decodeBase32PaddedWith utf8 "ADQMS==="+-- Right "Sun" --+-- >>> decodeBase32PaddedWith utf8 "!"+-- Left (DecodeError "Base32-encoded bytestring has invalid size")+--+-- >>> decodeBase32PaddedWith (const (Left "unsupported encoding")) "ADQMS==="+-- Left (ConversionError "unsupported encoding")+-- decodeBase32PaddedWith :: (ShortByteString -> Either err ShortText) -- ^ convert a bytestring to text (e.g. 'Data.Text.Encoding.decodeUtf8'') -> ShortByteString -- ^ Input text to decode -> Either (Base32Error err) ShortText-decodeBase32PaddedWith f t = case BS32H.decodeBase32Padded t of+decodeBase32PaddedWith f t = case BS32H.decodeBase32PaddedUntyped t of Left de -> Left $ DecodeError de Right a -> first ConversionError (f a) {-# INLINE decodeBase32PaddedWith #-} --- -- | Leniently decode an unpadded Base32hex-encoded 'ShortText'. This function--- -- will not generate parse errors. If input data contains padding chars,--- -- then the input will be parsed up until the first pad character.--- ----- -- __Note:__ This is not RFC 4648-compliant.--- ----- -- === __Examples__:--- ----- -- >>> decodeBase32Lenient "PDw_Pj4="--- -- "<<?>>"--- ----- -- >>> decodeBase32Lenient "PDw_%%%$}Pj4"--- -- "<<?>>"--- ----- decodeBase32Lenient :: ShortText -> ShortText--- decodeBase32Lenient = fromText . B32TH.decodeBase32Lenient . toText--- {-# INLINE decodeBase32Lenient #-}+-- | Decode a typed Base32hex-encoded 'ShortText' value.+--+-- This function is padding-agnostic.+-- The decoded payload must be valid UTF-8.+--+-- >>> decodeBase32 (encodeBase32 "Sun")+-- "Sun"+--+-- >>> decodeBase32 (encodeBase32Unpadded "Sun")+-- "Sun"+--+-- @since 1.0+--+decodeBase32 :: HexAlphabet k => Base32 k ShortText -> ShortText+decodeBase32 = fromText . TE.decodeUtf8 . SBS.fromShort+ . BS32H.decodeBase32 . fmap toShortByteString+{-# INLINE decodeBase32 #-} --- | Tell whether a 'ShortText' value is Base32hex-encoded.+-- | Decode a typed, padded Base32hex-encoded 'ShortText' value. ----- === __Examples__:+-- The decoded payload must be valid UTF-8. ----- >>> isBase32Hex "ADQMS"--- True+-- >>> decodeBase32Padded (encodeBase32 "Sun")+-- "Sun" ----- >>> isBase32Hex "ADQMS==="--- True+-- @since 1.0 ----- >>> isBase32Hex "ADQMS=="--- False+decodeBase32Padded :: Base32 'HexPadded ShortText -> ShortText+decodeBase32Padded = decodeBase32+{-# INLINE decodeBase32Padded #-}++-- | Parse and validate a base32hex-encoded value, assuming that it has padding. ---isBase32Hex :: ShortText -> Bool-isBase32Hex = B32H.isBase32Hex . toByteString-{-# INLINE isBase32Hex #-}+-- >>> parseBase32Padded "adqms==="+-- Right "adqms==="+--+-- >>> fmap decodeBase32Padded (parseBase32Padded "ADQMS===")+-- Right "Sun"+--+-- >>> parseBase32Padded ""+-- Right ""+--+-- >>> parseBase32Padded "ADQMS"+-- Left "Base32-encoded bytestring requires padding"+--+-- @since 1.0+--+parseBase32Padded :: ShortText -> Either Text (Base32 'HexPadded ShortText)+parseBase32Padded value = assertBase32 value+ <$ (BS32H.decodeBase32PaddedUntyped . toShortByteString) value+{-# INLINE parseBase32Padded #-} --- | Tell whether a 'ShortText' value is a valid Base32hex format.+-- | Decode a typed, unpadded Base32hex-encoded value. ----- This will not tell you whether or not this is a correct Base32hex representation,--- only that it conforms to the correct shape. To check whether it is a true--- Base32 encoded 'ShortText' value, use 'isBase32Hex'.+-- The decoded payload must be valid UTF-8. ----- === __Examples__:+-- >>> decodeBase32Unpadded (encodeBase32Unpadded "Sun")+-- "Sun" ----- >>> isValidBase32Hex "ADQMS"--- True+-- @since 1.0 ----- >>> isValidBase32Hex "ADQMS="--- False+decodeBase32Unpadded :: Base32 'HexUnpadded ShortText -> ShortText+decodeBase32Unpadded = decodeBase32+{-# INLINE decodeBase32Unpadded #-}++-- | Parse and validate a base32hex-encoded value, assuming that it is unpadded. ----- >>> isValidBase32Hex "ADQMS%"--- False+-- >>> parseBase32Unpadded "adqms"+-- Right "adqms" ---isValidBase32Hex :: ShortText -> Bool-isValidBase32Hex = B32H.isValidBase32Hex . toByteString-{-# INLINE isValidBase32Hex #-}+-- >>> fmap decodeBase32Unpadded (parseBase32Unpadded "ADQMS")+-- Right "Sun"+--+-- >>> parseBase32Unpadded ""+-- Right ""+--+-- >>> parseBase32Unpadded "ADQMS==="+-- Left "Base32-encoded bytestring has invalid padding"+--+-- >>> isLeft (parseBase32Unpadded "CP")+-- True+--+-- @since 1.0+--+parseBase32Unpadded :: ShortText -> Either Text (Base32 'HexUnpadded ShortText)+parseBase32Unpadded value = assertBase32 value+ <$ (BS32H.decodeBase32UnpaddedUntyped . toShortByteString) value+{-# INLINE parseBase32Unpadded #-}++-- $setup+-- >>> :seti -XOverloadedStrings+-- >>> import qualified Data.Text.Short as Text+-- >>> import Data.Either (isLeft)
test/Internal.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE KindSignatures #-}@@ -9,7 +10,7 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} -- | -- Module : Main--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -33,6 +34,8 @@ ) where +import Data.Base32.Types (Alphabet(..), Base32, StdAlphabet, HexAlphabet, extractBase32)+import Data.Either (isRight) import Test.QuickCheck hiding (label) import Data.Text import qualified Data.ByteString as BS@@ -69,12 +72,24 @@ , encodeNopad :: bs -> bs , encodeHex :: bs -> bs , encodeHexNopad :: bs -> bs+ , decodeTyped :: forall k. StdAlphabet k => Base32 k bs -> bs+ , decodePaddedTyped :: Base32 'StdPadded bs -> bs+ , decodeUnpaddedTyped :: Base32 'StdUnpadded bs -> bs+ , decodeHexTyped :: forall k. HexAlphabet k => Base32 k bs -> bs+ , decodeHexPaddedTyped :: Base32 'HexPadded bs -> bs+ , decodeHexUnpaddedTyped :: Base32 'HexUnpadded bs -> bs , decode :: bs -> Either Text bs , decodeHex :: bs -> Either Text bs , decodePad :: bs -> Either Text bs , decodeHexPad :: bs -> Either Text bs , decodeNopad :: bs -> Either Text bs , decodeHexNopad :: bs -> Either Text bs+ , parseValue :: bs -> Either Text bs+ , parseHexValue :: bs -> Either Text bs+ , parsePaddedValue :: bs -> Either Text bs+ , parseUnpaddedValue :: bs -> Either Text bs+ , parseHexPaddedValue :: bs -> Either Text bs+ , parseHexUnpaddedValue :: bs -> Either Text bs , correct :: bs -> Bool , correctHex :: bs -> Bool , validate :: bs -> Bool@@ -84,115 +99,187 @@ b32 :: Harness BS.ByteString b32 = Harness { label = "ByteString"- , encode = B32.encodeBase32'- , encodeNopad = B32.encodeBase32Unpadded'- , decode = B32.decodeBase32- , decodePad = B32.decodeBase32Padded- , decodeNopad = B32.decodeBase32Unpadded- , correct = B32.isBase32- , validate = B32.isValidBase32- , encodeHex = B32H.encodeBase32'- , encodeHexNopad = B32H.encodeBase32Unpadded'- , decodeHex = B32H.decodeBase32- , decodeHexPad = B32H.decodeBase32Padded- , decodeHexNopad = B32H.decodeBase32Unpadded- , correctHex = B32H.isBase32Hex- , validateHex = B32H.isValidBase32Hex+ , encode = extractBase32 . B32.encodeBase32'+ , encodeNopad = extractBase32 . B32.encodeBase32Unpadded'+ , decodeTyped = B32.decodeBase32+ , decodePaddedTyped = B32.decodeBase32Padded+ , decodeUnpaddedTyped = B32.decodeBase32Unpadded+ , decodeHexTyped = B32H.decodeBase32+ , decodeHexPaddedTyped = B32H.decodeBase32Padded+ , decodeHexUnpaddedTyped = B32H.decodeBase32Unpadded+ , decode = B32.decodeBase32Untyped+ , decodePad = B32.decodeBase32PaddedUntyped+ , decodeNopad = B32.decodeBase32UnpaddedUntyped+ , parseValue = fmap extractBase32 . B32.parseBase32Padded+ , parseHexValue = fmap extractBase32 . B32H.parseBase32Padded+ , parsePaddedValue = fmap extractBase32 . B32.parseBase32Padded+ , parseUnpaddedValue = fmap extractBase32 . B32.parseBase32Unpadded+ , parseHexPaddedValue = fmap extractBase32 . B32H.parseBase32Padded+ , parseHexUnpaddedValue = fmap extractBase32 . B32H.parseBase32Unpadded+ , correct = isRight . B32.decodeBase32Untyped+ , validate = isRight . B32.decodeBase32Untyped+ , encodeHex = extractBase32 . B32H.encodeBase32'+ , encodeHexNopad = extractBase32 . B32H.encodeBase32Unpadded'+ , decodeHex = B32H.decodeBase32Untyped+ , decodeHexPad = B32H.decodeBase32PaddedUntyped+ , decodeHexNopad = B32H.decodeBase32UnpaddedUntyped+ , correctHex = isRight . B32H.decodeBase32Untyped+ , validateHex = isRight . B32H.decodeBase32Untyped } lb32 :: Harness LBS.ByteString lb32 = Harness { label = "Lazy ByteString"- , encode = BL32.encodeBase32'- , encodeNopad = BL32.encodeBase32Unpadded'- , decode = BL32.decodeBase32- , decodePad = BL32.decodeBase32Padded- , decodeNopad = BL32.decodeBase32Unpadded- , correct = BL32.isBase32- , validate = BL32.isValidBase32- , encodeHex = BL32H.encodeBase32'- , encodeHexNopad = BL32H.encodeBase32Unpadded'- , decodeHex = BL32H.decodeBase32- , decodeHexPad = BL32H.decodeBase32Padded- , decodeHexNopad = BL32H.decodeBase32Unpadded- , correctHex = BL32H.isBase32Hex- , validateHex = BL32H.isValidBase32Hex+ , encode = extractBase32 . BL32.encodeBase32'+ , encodeNopad = extractBase32 . BL32.encodeBase32Unpadded'+ , decodeTyped = BL32.decodeBase32+ , decodePaddedTyped = BL32.decodeBase32Padded+ , decodeUnpaddedTyped = BL32.decodeBase32Unpadded+ , decodeHexTyped = BL32H.decodeBase32+ , decodeHexPaddedTyped = BL32H.decodeBase32Padded+ , decodeHexUnpaddedTyped = BL32H.decodeBase32Unpadded+ , decode = BL32.decodeBase32Untyped+ , decodePad = BL32.decodeBase32PaddedUntyped+ , decodeNopad = BL32.decodeBase32UnpaddedUntyped+ , parseValue = fmap extractBase32 . BL32.parseBase32Padded+ , parseHexValue = fmap extractBase32 . BL32H.parseBase32Padded+ , parsePaddedValue = fmap extractBase32 . BL32.parseBase32Padded+ , parseUnpaddedValue = fmap extractBase32 . BL32.parseBase32Unpadded+ , parseHexPaddedValue = fmap extractBase32 . BL32H.parseBase32Padded+ , parseHexUnpaddedValue = fmap extractBase32 . BL32H.parseBase32Unpadded+ , correct = isRight . BL32.decodeBase32Untyped+ , validate = isRight . BL32.decodeBase32Untyped+ , encodeHex = extractBase32 . BL32H.encodeBase32'+ , encodeHexNopad = extractBase32 . BL32H.encodeBase32Unpadded'+ , decodeHex = BL32H.decodeBase32Untyped+ , decodeHexPad = BL32H.decodeBase32PaddedUntyped+ , decodeHexNopad = BL32H.decodeBase32UnpaddedUntyped+ , correctHex = isRight . BL32H.decodeBase32Untyped+ , validateHex = isRight . BL32H.decodeBase32Untyped } sb32 :: Harness SBS.ShortByteString sb32 = Harness { label = "Short ByteString"- , encode = BS32.encodeBase32'- , encodeNopad = BS32.encodeBase32Unpadded'- , decode = BS32.decodeBase32- , decodePad = BS32.decodeBase32Padded- , decodeNopad = BS32.decodeBase32Unpadded- , correct = BS32.isBase32- , validate = BS32.isValidBase32- , encodeHex = BS32H.encodeBase32'- , encodeHexNopad = BS32H.encodeBase32Unpadded'- , decodeHex = BS32H.decodeBase32- , decodeHexPad = BS32H.decodeBase32Padded- , decodeHexNopad = BS32H.decodeBase32Unpadded- , correctHex = BS32H.isBase32Hex- , validateHex = BS32H.isValidBase32Hex+ , encode = extractBase32 . BS32.encodeBase32'+ , encodeNopad = extractBase32 . BS32.encodeBase32Unpadded'+ , decodeTyped = BS32.decodeBase32+ , decodePaddedTyped = BS32.decodeBase32Padded+ , decodeUnpaddedTyped = BS32.decodeBase32Unpadded+ , decodeHexTyped = BS32H.decodeBase32+ , decodeHexPaddedTyped = BS32H.decodeBase32Padded+ , decodeHexUnpaddedTyped = BS32H.decodeBase32Unpadded+ , decode = BS32.decodeBase32Untyped+ , decodePad = BS32.decodeBase32PaddedUntyped+ , decodeNopad = BS32.decodeBase32UnpaddedUntyped+ , parseValue = fmap extractBase32 . BS32.parseBase32Padded+ , parseHexValue = fmap extractBase32 . BS32H.parseBase32Padded+ , parsePaddedValue = fmap extractBase32 . BS32.parseBase32Padded+ , parseUnpaddedValue = fmap extractBase32 . BS32.parseBase32Unpadded+ , parseHexPaddedValue = fmap extractBase32 . BS32H.parseBase32Padded+ , parseHexUnpaddedValue = fmap extractBase32 . BS32H.parseBase32Unpadded+ , correct = isRight . BS32.decodeBase32Untyped+ , validate = isRight . BS32.decodeBase32Untyped+ , encodeHex = extractBase32 . BS32H.encodeBase32'+ , encodeHexNopad = extractBase32 . BS32H.encodeBase32Unpadded'+ , decodeHex = BS32H.decodeBase32Untyped+ , decodeHexPad = BS32H.decodeBase32PaddedUntyped+ , decodeHexNopad = BS32H.decodeBase32UnpaddedUntyped+ , correctHex = isRight . BS32H.decodeBase32Untyped+ , validateHex = isRight . BS32H.decodeBase32Untyped } t32 :: Harness T.Text t32 = Harness { label = "Text"- , encode = T32.encodeBase32- , encodeNopad = T32.encodeBase32Unpadded- , decode = T32.decodeBase32- , decodePad = T32.decodeBase32Padded- , decodeNopad = T32.decodeBase32Unpadded- , correct = T32.isBase32- , validate = T32.isValidBase32- , encodeHex = T32H.encodeBase32- , encodeHexNopad = T32H.encodeBase32Unpadded- , decodeHex = T32H.decodeBase32- , decodeHexPad = T32H.decodeBase32Padded- , decodeHexNopad = T32H.decodeBase32Unpadded- , correctHex = T32H.isBase32Hex- , validateHex = T32H.isValidBase32Hex+ , encode = extractBase32 . T32.encodeBase32+ , encodeNopad = extractBase32 . T32.encodeBase32Unpadded+ , decodeTyped = T32.decodeBase32+ , decodePaddedTyped = T32.decodeBase32Padded+ , decodeUnpaddedTyped = T32.decodeBase32Unpadded+ , decodeHexTyped = T32H.decodeBase32+ , decodeHexPaddedTyped = T32H.decodeBase32Padded+ , decodeHexUnpaddedTyped = T32H.decodeBase32Unpadded+ , decode = T32.decodeBase32Untyped+ , decodePad = T32.decodeBase32PaddedUntyped+ , decodeNopad = T32.decodeBase32UnpaddedUntyped+ , parseValue = fmap extractBase32 . T32.parseBase32Padded+ , parseHexValue = fmap extractBase32 . T32H.parseBase32Padded+ , parsePaddedValue = fmap extractBase32 . T32.parseBase32Padded+ , parseUnpaddedValue = fmap extractBase32 . T32.parseBase32Unpadded+ , parseHexPaddedValue = fmap extractBase32 . T32H.parseBase32Padded+ , parseHexUnpaddedValue = fmap extractBase32 . T32H.parseBase32Unpadded+ , correct = isRight . T32.decodeBase32Untyped+ , validate = isRight . T32.decodeBase32Untyped+ , encodeHex = extractBase32 . T32H.encodeBase32+ , encodeHexNopad = extractBase32 . T32H.encodeBase32Unpadded+ , decodeHex = T32H.decodeBase32Untyped+ , decodeHexPad = T32H.decodeBase32PaddedUntyped+ , decodeHexNopad = T32H.decodeBase32UnpaddedUntyped+ , correctHex = isRight . T32H.decodeBase32Untyped+ , validateHex = isRight . T32H.decodeBase32Untyped } tl32 :: Harness TL.Text tl32 = Harness { label = "Lazy Text"- , encode = TL32.encodeBase32- , encodeNopad = TL32.encodeBase32Unpadded- , decode = TL32.decodeBase32- , decodePad = TL32.decodeBase32Padded- , decodeNopad = TL32.decodeBase32Unpadded- , correct = TL32.isBase32- , validate = TL32.isValidBase32- , encodeHex = TL32H.encodeBase32- , encodeHexNopad = TL32H.encodeBase32Unpadded- , decodeHex = TL32H.decodeBase32- , decodeHexPad = TL32H.decodeBase32Padded- , decodeHexNopad = TL32H.decodeBase32Unpadded- , correctHex = TL32H.isBase32Hex- , validateHex = TL32H.isValidBase32Hex+ , encode = extractBase32 . TL32.encodeBase32+ , encodeNopad = extractBase32 . TL32.encodeBase32Unpadded+ , decodeTyped = TL32.decodeBase32+ , decodePaddedTyped = TL32.decodeBase32Padded+ , decodeUnpaddedTyped = TL32.decodeBase32Unpadded+ , decodeHexTyped = TL32H.decodeBase32+ , decodeHexPaddedTyped = TL32H.decodeBase32Padded+ , decodeHexUnpaddedTyped = TL32H.decodeBase32Unpadded+ , decode = TL32.decodeBase32Untyped+ , decodePad = TL32.decodeBase32PaddedUntyped+ , decodeNopad = TL32.decodeBase32UnpaddedUntyped+ , parseValue = fmap extractBase32 . TL32.parseBase32Padded+ , parseHexValue = fmap extractBase32 . TL32H.parseBase32Padded+ , parsePaddedValue = fmap extractBase32 . TL32.parseBase32Padded+ , parseUnpaddedValue = fmap extractBase32 . TL32.parseBase32Unpadded+ , parseHexPaddedValue = fmap extractBase32 . TL32H.parseBase32Padded+ , parseHexUnpaddedValue = fmap extractBase32 . TL32H.parseBase32Unpadded+ , correct = isRight . TL32.decodeBase32Untyped+ , validate = isRight . TL32.decodeBase32Untyped+ , encodeHex = extractBase32 . TL32H.encodeBase32+ , encodeHexNopad = extractBase32 . TL32H.encodeBase32Unpadded+ , decodeHex = TL32H.decodeBase32Untyped+ , decodeHexPad = TL32H.decodeBase32PaddedUntyped+ , decodeHexNopad = TL32H.decodeBase32UnpaddedUntyped+ , correctHex = isRight . TL32H.decodeBase32Untyped+ , validateHex = isRight . TL32H.decodeBase32Untyped } ts32 :: Harness TS.ShortText ts32 = Harness { label = "Short Lazy Text"- , encode = TS32.encodeBase32- , encodeNopad = TS32.encodeBase32Unpadded- , decode = TS32.decodeBase32- , decodePad = TS32.decodeBase32Padded- , decodeNopad = TS32.decodeBase32Unpadded- , correct = TS32.isBase32- , validate = TS32.isValidBase32- , encodeHex = TS32H.encodeBase32- , encodeHexNopad = TS32H.encodeBase32Unpadded- , decodeHex = TS32H.decodeBase32- , decodeHexPad = TS32H.decodeBase32Padded- , decodeHexNopad = TS32H.decodeBase32Unpadded- , correctHex = TS32H.isBase32Hex- , validateHex = TS32H.isValidBase32Hex+ , encode = extractBase32 . TS32.encodeBase32+ , encodeNopad = extractBase32 . TS32.encodeBase32Unpadded+ , decodeTyped = TS32.decodeBase32+ , decodePaddedTyped = TS32.decodeBase32Padded+ , decodeUnpaddedTyped = TS32.decodeBase32Unpadded+ , decodeHexTyped = TS32H.decodeBase32+ , decodeHexPaddedTyped = TS32H.decodeBase32Padded+ , decodeHexUnpaddedTyped = TS32H.decodeBase32Unpadded+ , decode = TS32.decodeBase32Untyped+ , decodePad = TS32.decodeBase32PaddedUntyped+ , decodeNopad = TS32.decodeBase32UnpaddedUntyped+ , parseValue = fmap extractBase32 . TS32.parseBase32Padded+ , parseHexValue = fmap extractBase32 . TS32H.parseBase32Padded+ , parsePaddedValue = fmap extractBase32 . TS32.parseBase32Padded+ , parseUnpaddedValue = fmap extractBase32 . TS32.parseBase32Unpadded+ , parseHexPaddedValue = fmap extractBase32 . TS32H.parseBase32Padded+ , parseHexUnpaddedValue = fmap extractBase32 . TS32H.parseBase32Unpadded+ , correct = isRight . TS32.decodeBase32Untyped+ , validate = isRight . TS32.decodeBase32Untyped+ , encodeHex = extractBase32 . TS32H.encodeBase32+ , encodeHexNopad = extractBase32 . TS32H.encodeBase32Unpadded+ , decodeHex = TS32H.decodeBase32Untyped+ , decodeHexPad = TS32H.decodeBase32PaddedUntyped+ , decodeHexNopad = TS32H.decodeBase32UnpaddedUntyped+ , correctHex = isRight . TS32H.decodeBase32Untyped+ , validateHex = isRight . TS32H.decodeBase32Untyped } -- -------------------------------------------------------------------- --@@ -200,11 +287,16 @@ data TextHarness bs cs = TextHarness { decodeWith_ :: forall err. (bs -> Either err cs) -> bs -> Either (Base32Error err) cs- , decodePaddedWith_ :: forall err. (bs -> Either err cs) -> bs -> Either (Base32Error err) cs- , decodeUnpaddedWith_ :: forall err. (bs -> Either err cs) -> bs -> Either (Base32Error err) cs- , decodeHexWith_ :: forall err. (bs -> Either err cs) -> bs -> Either (Base32Error err) cs- , decodeHexPaddedWith_ :: forall err. (bs -> Either err cs) -> bs -> Either (Base32Error err) cs- , decodeHexUnpaddedWith_ :: forall err. (bs -> Either err cs) -> bs -> Either (Base32Error err) cs+ , decodePaddedWith_+ :: forall err. (bs -> Either err cs) -> bs -> Either (Base32Error err) cs+ , decodeUnpaddedWith_+ :: forall err. (bs -> Either err cs) -> bs -> Either (Base32Error err) cs+ , decodeHexWith_+ :: forall err. (bs -> Either err cs) -> bs -> Either (Base32Error err) cs+ , decodeHexPaddedWith_+ :: forall err. (bs -> Either err cs) -> bs -> Either (Base32Error err) cs+ , decodeHexUnpaddedWith_+ :: forall err. (bs -> Either err cs) -> bs -> Either (Base32Error err) cs } tt32 :: TextHarness BS.ByteString Text
test/Main.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PackageImports #-} {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -8,7 +7,7 @@ {-# LANGUAGE RecordWildCards #-} -- | -- Module : Main--- Copyright : (c) 2019-2023 Emily Pillmore+-- Copyright : (c) 2019-2026 Emily Pillmore -- License : BSD-style -- -- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>@@ -22,24 +21,21 @@ ) where +import Data.Base32.Types (extractBase32) import Data.Bifunctor (second) import qualified "memory" Data.ByteArray.Encoding as Mem import qualified Data.ByteString as BS-import Data.ByteString.Internal (c2w) import "base32" Data.ByteString.Base32 as B32-import "base32" Data.ByteString.Base32.Hex as B32H-import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Short as SBS-import Data.String (IsString)-import qualified Data.Text as T+import Data.String (IsString, fromString)+import qualified Data.Text as Text import qualified Data.Text.Encoding as T import Data.Text.Encoding.Base32.Error (Base32Error(..))-import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL import qualified Data.Text.Short as TS-import Data.Word import Internal+import Regression (regressions) import Test.Tasty import Test.Tasty.HUnit@@ -54,7 +50,8 @@ tests :: TestTree tests = testGroup "Base32 Tests"- [ mkTree b32+ [ regressions+ , mkTree b32 [ mkPropTree , mkUnitTree ]@@ -206,8 +203,9 @@ prop_mem_coherence :: TestTree prop_mem_coherence = testGroup "prop_mem_coherence" [ testProperty "prop_std_mem_coherence" $ \bs ->- Right bs == B32.decodeBase32 (B32.encodeBase32' bs)- && Right bs == Mem.convertFromBase Mem.Base32 (Mem.convertToBase @BS.ByteString @BS.ByteString Mem.Base32 bs)+ Right bs == B32.decodeBase32Untyped (extractBase32 (B32.encodeBase32' bs))+ && Right bs == Mem.convertFromBase Mem.Base32+ (Mem.convertToBase @BS.ByteString @BS.ByteString Mem.Base32 bs) ] -- ---------------------------------------------------------------- --@@ -240,18 +238,18 @@ testCaseStd s t = testCaseSteps (show $ if s == "" then "empty" else s) $ \step -> do step "encode is sound"- t @=? encode s+ fromString t @=? encode (fromString s) step "decode is sound"- Right s @=? decode (encode s)+ Right (fromString s) @=? decode (encode (fromString s)) testCaseHex s t = testCaseSteps (show $ if s == "" then "empty" else s) $ \step -> do step "encode is sound"- t @=? encodeHex s+ fromString t @=? encodeHex (fromString s) step "decode is sound"- Right s @=? decodeHexPad t+ Right (fromString s) @=? decodeHexPad (fromString t) -- | Unit test trees for the `decode*With` family of text-valued functions --@@ -268,78 +266,87 @@ decodeWithVectors utf8 TextHarness{..} s t = testGroup "DecodeWith* unit tests" [ testGroup "decodeWith negative tests" [ testCase "decodeWith non-utf8 inputs on decodeUtf8" $ do- case decodeWith_ utf8 "\1079743" of+ case decodeWith_ utf8 (fromString "\1079743") of Left (DecodeError _) -> return () _ -> assertFailure "decoding phase" , testCase "decodeWith valid utf8 inputs on decodeUtf8" $ do- case decodeWith_ utf8 (encode s "\1079743") of+ case decodeWith_ utf8 (encode s (fromString "\1079743")) of Left (ConversionError _) -> return () _ -> assertFailure "conversion phase" , testCase "decodeHexWith non-utf8 inputs on decodeUtf8" $ do- case decodeHexWith_ utf8 "\1079743" of+ case decodeHexWith_ utf8 (fromString "\1079743") of Left (DecodeError _) -> return () _ -> assertFailure "decoding phase" , testCase "decodePaddedWith non-utf8 inputs on decodeUtf8" $ do- case decodePaddedWith_ utf8 "\1079743" of+ case decodePaddedWith_ utf8 (fromString "\1079743") of Left (DecodeError _) -> return () _ -> assertFailure "decoding phase" , testCase "decodePaddedWith valid utf8 inputs on decodeUtf8" $ do- case decodePaddedWith_ utf8 (encode s "\1079743") of+ case decodePaddedWith_ utf8 (encode s (fromString "\1079743")) of Left (ConversionError _) -> return () _ -> assertFailure "conversion phase" , testCase "decodeUnpaddedWith non-utf8 inputs on decodeUtf8" $ do- case decodeUnpaddedWith_ utf8 "\1079743" of+ case decodeUnpaddedWith_ utf8 (fromString "\1079743") of Left (DecodeError _) -> return () _ -> assertFailure "decoding phase" , testCase "decodeUnpaddedWith valid utf8 inputs on decodeUtf8" $ do- case decodeUnpaddedWith_ utf8 (encodeNopad s "\1079743") of+ case decodeUnpaddedWith_ utf8 (encodeNopad s (fromString "\1079743")) of Left (ConversionError _) -> return () _ -> assertFailure "conversion phase" , testCase "decodeHexWith valid utf8 inputs on decodeUtf8" $ do- case decodeHexWith_ utf8 (encodeHex s "\1079743") of+ case decodeHexWith_ utf8 (encodeHex s (fromString "\1079743")) of Left (ConversionError _) -> return () _ -> assertFailure "conversion phase" , testCase "decodeHexPaddedWith non-utf8 inputs on decodeUtf8" $ do- case decodeHexPaddedWith_ utf8 "\1079743" of+ case decodeHexPaddedWith_ utf8 (fromString "\1079743") of Left (DecodeError _) -> return () _ -> assertFailure "decoding phase" , testCase "decodeHexPaddedWith valid utf8 inputs on decodeUtf8" $ do- case decodeHexPaddedWith_ utf8 (encodeHex s "\1079743") of+ case decodeHexPaddedWith_ utf8 (encodeHex s (fromString "\1079743")) of Left (ConversionError _) -> return () _ -> assertFailure "conversion phase" , testCase "decodeHexUnpaddedWith non-utf8 inputs on decodeUtf8" $ do- case decodeHexUnpaddedWith_ utf8 "\1079743" of+ case decodeHexUnpaddedWith_ utf8 (fromString "\1079743") of Left (DecodeError _) -> return () _ -> assertFailure "decoding phase" , testCase "decodeHexUnpaddedWith valid utf8 inputs on decodeUtf8" $ do- case decodeHexUnpaddedWith_ utf8 (encodeHexNopad s "\1079743") of+ case decodeHexUnpaddedWith_ utf8 (encodeHexNopad s (fromString "\1079743")) of Left (ConversionError _) -> return () _ -> assertFailure "conversion phase" ] , testGroup "decodeWith positive tests" [ testCase "decodeWith utf8 inputs on decodeUtf8" $ do- a <- either (assertFailure . show) pure $ decode t "MZXW6YTBOI======"- b <- either (assertFailure . show) pure $ decodeWith_ utf8 "MZXW6YTBOI======"+ a <- either (assertFailure . show) pure $ decode t (fromString "MZXW6YTBOI======")+ b <- either (assertFailure . show) pure $+ decodeWith_ utf8 (fromString "MZXW6YTBOI======") a @=? b , testCase "decodePaddedWith utf8 inputs on decodeUtf8" $ do- a <- either (assertFailure . show) pure $ decodePad t "MZXW6YTBOI======"- b <- either (assertFailure . show) pure $ decodePaddedWith_ utf8 "MZXW6YTBOI======"+ a <- either (assertFailure . show) pure $+ decodePad t (fromString "MZXW6YTBOI======")+ b <- either (assertFailure . show) pure $+ decodePaddedWith_ utf8 (fromString "MZXW6YTBOI======") a @=? b , testCase "decodeUnpaddedWith utf8 inputs on decodeUtf8" $ do- a <- either (assertFailure . show) pure $ decodeNopad t "MZXW6YTBOI"- b <- either (assertFailure . show) pure $ decodeUnpaddedWith_ utf8 "MZXW6YTBOI"+ a <- either (assertFailure . show) pure $ decodeNopad t (fromString "MZXW6YTBOI")+ b <- either (assertFailure . show) pure $+ decodeUnpaddedWith_ utf8 (fromString "MZXW6YTBOI") a @=? b , testCase "decodeHexWith utf8 inputs on decodeUtf8" $ do- a <- either (assertFailure . show) pure $ decodeHex t "CPNMUOJ1E8======"- b <- either (assertFailure . show) pure $ decodeHexWith_ utf8 "CPNMUOJ1E8======"+ a <- either (assertFailure . show) pure $+ decodeHex t (fromString "CPNMUOJ1E8======")+ b <- either (assertFailure . show) pure $+ decodeHexWith_ utf8 (fromString "CPNMUOJ1E8======") a @=? b , testCase "decodeHexPaddedWith utf8 inputs on decodeUtf8" $ do- a <- either (assertFailure . show) pure $ decodeHexPad t "CPNMUOJ1E8======"- b <- either (assertFailure . show) pure $ decodeHexPaddedWith_ utf8 "CPNMUOJ1E8======"+ a <- either (assertFailure . show) pure $+ decodeHexPad t (fromString "CPNMUOJ1E8======")+ b <- either (assertFailure . show) pure $+ decodeHexPaddedWith_ utf8 (fromString "CPNMUOJ1E8======") a @=? b , testCase "decodeHexUnpaddedWith utf8 inputs on decodeUtf8" $ do- a <- either (assertFailure . show) pure $ decodeHexNopad t "CPNMUOJ1"- b <- either (assertFailure . show) pure $ decodeHexUnpaddedWith_ utf8 "CPNMUOJ1"+ a <- either (assertFailure . show) pure $ decodeHexNopad t (fromString "CPNMUOJ1")+ b <- either (assertFailure . show) pure $+ decodeHexUnpaddedWith_ utf8 (fromString "CPNMUOJ1") a @=? b ] ]@@ -350,63 +357,63 @@ validityTests Harness{..} = testGroup "Validity and correctness unit tests" [ testGroup "Validity unit tests" [ testCase "Hex padding tests" $ do- not (validateHex "C") @? "C"- validateHex "CO" @? "CO"- validateHex "CPNG" @? "CPNG"- validateHex "CPNMU" @? "CPNMU"- validateHex "CPNMUOG" @? "CPNMUOG"- validateHex "CPNMUOJ1" @? "CPNMUOJ1"- validateHex "CPNMUOJ1E8" @? "CPNMUOJ1E8"- validateHex "CO======" @? "CO======"- validateHex "CPNG====" @? "CPNG===="- validateHex "CPNMU===" @? "CPNMU==="- validateHex "CPNMUOG=" @? "CPNMUOG="- validateHex "CPNMUOJ1" @? "CPNMUOJ1"- validateHex "CPNMUOJ1E8======" @? "CPNMUOJ1E8======"+ not (validateHex (fromString "C")) @? "C"+ validateHex (fromString "CO") @? "CO"+ validateHex (fromString "CPNG") @? "CPNG"+ validateHex (fromString "CPNMU") @? "CPNMU"+ validateHex (fromString "CPNMUOG") @? "CPNMUOG"+ validateHex (fromString "CPNMUOJ1") @? "CPNMUOJ1"+ validateHex (fromString "CPNMUOJ1E8") @? "CPNMUOJ1E8"+ validateHex (fromString "CO======") @? "CO======"+ validateHex (fromString "CPNG====") @? "CPNG===="+ validateHex (fromString "CPNMU===") @? "CPNMU==="+ validateHex (fromString "CPNMUOG=") @? "CPNMUOG="+ validateHex (fromString "CPNMUOJ1") @? "CPNMUOJ1"+ validateHex (fromString "CPNMUOJ1E8======") @? "CPNMUOJ1E8======" , testCase "Std padding tests" $ do- not (validate "M") @? "M"- validate "MY" @? "MY"- validate "MZXQ" @? "MZXQ"- validate "MZXW6" @? "MZXW6"- validate "MZXW6YQ" @? "MZXW6YQ"- validate "MZXW6YTB" @? "MZXW6YTB"- validate "MZXW6YTBOI" @? "MZXW6YTBOI"- validate "MY======" @? "MY======"- validate "MZXQ====" @? "MZXQ===="- validate "MZXW6===" @? "MZXW6==="- validate "MZXW6YQ=" @? "MZXW6YQ="- validate "MZXW6YTB" @? "MZXW6YTB"- validate "MZXW6YTBOI======" @? "MZXW6YTBOI======"+ not (validate (fromString "M")) @? "M"+ validate (fromString "MY") @? "MY"+ validate (fromString "MZXQ") @? "MZXQ"+ validate (fromString "MZXW6") @? "MZXW6"+ validate (fromString "MZXW6YQ") @? "MZXW6YQ"+ validate (fromString "MZXW6YTB") @? "MZXW6YTB"+ validate (fromString "MZXW6YTBOI") @? "MZXW6YTBOI"+ validate (fromString "MY======") @? "MY======"+ validate (fromString "MZXQ====") @? "MZXQ===="+ validate (fromString "MZXW6===") @? "MZXW6==="+ validate (fromString "MZXW6YQ=") @? "MZXW6YQ="+ validate (fromString "MZXW6YTB") @? "MZXW6YTB"+ validate (fromString "MZXW6YTBOI======") @? "MZXW6YTBOI======" ] , testGroup "Correctness unit tests" [ testCase "Hex tests" $ do- not (correctHex "C") @? "C"- correctHex "CO" @? "CO"- correctHex "CPNG" @? "CPNG"- correctHex "CPNMU" @? "CPNMU"- correctHex "CPNMUOG" @? "CPNMUOG"- correctHex "CPNMUOJ1" @? "CPNMUOJ1"- correctHex "CPNMUOJ1E8" @? "CPNMUOJ1E8"- correctHex "CO======" @? "CO======"- correctHex "CPNG====" @? "CPNG===="- correctHex "CPNMU===" @? "CPNMU==="- correctHex "CPNMUOG=" @? "CPNMUOG="- correctHex "CPNMUOJ1" @? "CPNMUOJ1"- correctHex "CPNMUOJ1E8======" @? "CPNMUOJ1E8======"+ not (correctHex (fromString "C")) @? "C"+ correctHex (fromString "CO") @? "CO"+ correctHex (fromString "CPNG") @? "CPNG"+ correctHex (fromString "CPNMU") @? "CPNMU"+ correctHex (fromString "CPNMUOG") @? "CPNMUOG"+ correctHex (fromString "CPNMUOJ1") @? "CPNMUOJ1"+ correctHex (fromString "CPNMUOJ1E8") @? "CPNMUOJ1E8"+ correctHex (fromString "CO======") @? "CO======"+ correctHex (fromString "CPNG====") @? "CPNG===="+ correctHex (fromString "CPNMU===") @? "CPNMU==="+ correctHex (fromString "CPNMUOG=") @? "CPNMUOG="+ correctHex (fromString "CPNMUOJ1") @? "CPNMUOJ1"+ correctHex (fromString "CPNMUOJ1E8======") @? "CPNMUOJ1E8======" , testCase "Std tests" $ do- not (correct "M") @? "M"- correct "MY" @? "MY"- correct "MZXQ" @? "MZXQ"- correct "MZXW6" @? "MZXW6"- correct "MZXW6YQ" @? "MZXW6YQ"- correct "MZXW6YTB" @? "MZXW6YTB"- correct "MZXW6YTBOI" @? "MZXW6YTBOI"- correct "MY======" @? "MY======"- correct "MZXQ====" @? "MZXQ===="- correct "MZXW6===" @? "MZXW6==="- correct "MZXW6YQ=" @? "MZXW6YQ="- correct "MZXW6YTB" @? "MZXW6YTB"- correct "MZXW6YTBOI======" @? "MZXW6YTBOI======"+ not (correct (fromString "M")) @? "M"+ correct (fromString "MY") @? "MY"+ correct (fromString "MZXQ") @? "MZXQ"+ correct (fromString "MZXW6") @? "MZXW6"+ correct (fromString "MZXW6YQ") @? "MZXW6YQ"+ correct (fromString "MZXW6YTB") @? "MZXW6YTB"+ correct (fromString "MZXW6YTBOI") @? "MZXW6YTBOI"+ correct (fromString "MY======") @? "MY======"+ correct (fromString "MZXQ====") @? "MZXQ===="+ correct (fromString "MZXW6===") @? "MZXW6==="+ correct (fromString "MZXW6YQ=") @? "MZXW6YQ="+ correct (fromString "MZXW6YTB") @? "MZXW6YTB"+ correct (fromString "MZXW6YTBOI======") @? "MZXW6YTBOI======" ] ] @@ -417,42 +424,74 @@ offsetVectors Harness{..} = testGroup "Offset tests" [ testGroup "Hex - Invalid padding" [ testCase "Invalid staggered padding" $ do- decodeHex "=PNMUOJ1E8======" @?= Left "invalid padding at offset: 0"- decodeHex "C=NMUOJ1E8======" @?= Left "invalid padding at offset: 1"- decodeHex "CP=MUOJ1E8======" @?= Left "invalid padding at offset: 2"- decodeHex "CPN=UOJ1E8======" @?= Left "invalid padding at offset: 3"- decodeHex "CPNM=OJ1E8======" @?= Left "invalid padding at offset: 4"- decodeHex "CPNMU=J1E8======" @?= Left "invalid padding at offset: 5"- decodeHex "CPNMUO=1E8======" @?= Left "invalid padding at offset: 6"- decodeHex "CPNMUOJ=E8======" @?= Left "invalid padding at offset: 7"+ decodeHex (fromString "=PNMUOJ1E8======")+ @?= Left (Text.pack "invalid padding at offset: 0")+ decodeHex (fromString "C=NMUOJ1E8======")+ @?= Left (Text.pack "invalid padding at offset: 1")+ decodeHex (fromString "CP=MUOJ1E8======")+ @?= Left (Text.pack "invalid padding at offset: 2")+ decodeHex (fromString "CPN=UOJ1E8======")+ @?= Left (Text.pack "invalid padding at offset: 3")+ decodeHex (fromString "CPNM=OJ1E8======")+ @?= Left (Text.pack "invalid padding at offset: 4")+ decodeHex (fromString "CPNMU=J1E8======")+ @?= Left (Text.pack "invalid padding at offset: 5")+ decodeHex (fromString "CPNMUO=1E8======")+ @?= Left (Text.pack "invalid padding at offset: 6")+ decodeHex (fromString "CPNMUOJ=E8======")+ @?= Left (Text.pack "invalid padding at offset: 7") , testCase "Invalid character coverage" $ do- decodeHex "%PNMUOJ1E8======" @?= Left "invalid character at offset: 0"- decodeHex "C%NMUOJ1E8======" @?= Left "invalid character at offset: 1"- decodeHex "CP%MUOJ1E8======" @?= Left "invalid character at offset: 2"- decodeHex "CPN%UOJ1E8======" @?= Left "invalid character at offset: 3"- decodeHex "CPNM%OJ1E8======" @?= Left "invalid character at offset: 4"- decodeHex "CPNMU%J1E8======" @?= Left "invalid character at offset: 5"- decodeHex "CPNMUO%1E8======" @?= Left "invalid character at offset: 6"- decodeHex "CPNMUOJ%E8======" @?= Left "invalid character at offset: 7"+ decodeHex (fromString "%PNMUOJ1E8======")+ @?= Left (Text.pack "invalid character at offset: 0")+ decodeHex (fromString "C%NMUOJ1E8======")+ @?= Left (Text.pack "invalid character at offset: 1")+ decodeHex (fromString "CP%MUOJ1E8======")+ @?= Left (Text.pack "invalid character at offset: 2")+ decodeHex (fromString "CPN%UOJ1E8======")+ @?= Left (Text.pack "invalid character at offset: 3")+ decodeHex (fromString "CPNM%OJ1E8======")+ @?= Left (Text.pack "invalid character at offset: 4")+ decodeHex (fromString "CPNMU%J1E8======")+ @?= Left (Text.pack "invalid character at offset: 5")+ decodeHex (fromString "CPNMUO%1E8======")+ @?= Left (Text.pack "invalid character at offset: 6")+ decodeHex (fromString "CPNMUOJ%E8======")+ @?= Left (Text.pack "invalid character at offset: 7") ] , testGroup "Std - Invalid padding" [ testCase "Invalid staggered padding" $ do- decode "=ZXW6YTBOI======" @?= Left "invalid padding at offset: 0"- decode "M=XW6YTBOI======" @?= Left "invalid padding at offset: 1"- decode "MZ=W6YTBOI======" @?= Left "invalid padding at offset: 2"- decode "MZX=6YTBOI======" @?= Left "invalid padding at offset: 3"- decode "MZXW=YTBOI======" @?= Left "invalid padding at offset: 4"- decode "MZXW6=TBOI======" @?= Left "invalid padding at offset: 5"- decode "MZXW6Y=BOI======" @?= Left "invalid padding at offset: 6"- decode "MZXW6YT=OI======" @?= Left "invalid padding at offset: 7"+ decode (fromString "=ZXW6YTBOI======")+ @?= Left (Text.pack "invalid padding at offset: 0")+ decode (fromString "M=XW6YTBOI======")+ @?= Left (Text.pack "invalid padding at offset: 1")+ decode (fromString "MZ=W6YTBOI======")+ @?= Left (Text.pack "invalid padding at offset: 2")+ decode (fromString "MZX=6YTBOI======")+ @?= Left (Text.pack "invalid padding at offset: 3")+ decode (fromString "MZXW=YTBOI======")+ @?= Left (Text.pack "invalid padding at offset: 4")+ decode (fromString "MZXW6=TBOI======")+ @?= Left (Text.pack "invalid padding at offset: 5")+ decode (fromString "MZXW6Y=BOI======")+ @?= Left (Text.pack "invalid padding at offset: 6")+ decode (fromString "MZXW6YT=OI======")+ @?= Left (Text.pack "invalid padding at offset: 7") , testCase "Invalid character coverage" $ do- decode "%ZXW6YTBOI======" @?= Left "invalid character at offset: 0"- decode "M%XW6YTBOI======" @?= Left "invalid character at offset: 1"- decode "MZ%W6YTBOI======" @?= Left "invalid character at offset: 2"- decode "MZX%6YTBOI======" @?= Left "invalid character at offset: 3"- decode "MZXW%YTBOI======" @?= Left "invalid character at offset: 4"- decode "MZXW6%TBOI======" @?= Left "invalid character at offset: 5"- decode "MZXW6Y%BOI======" @?= Left "invalid character at offset: 6"- decode "MZXW6YT%OI======" @?= Left "invalid character at offset: 7"+ decode (fromString "%ZXW6YTBOI======")+ @?= Left (Text.pack "invalid character at offset: 0")+ decode (fromString "M%XW6YTBOI======")+ @?= Left (Text.pack "invalid character at offset: 1")+ decode (fromString "MZ%W6YTBOI======")+ @?= Left (Text.pack "invalid character at offset: 2")+ decode (fromString "MZX%6YTBOI======")+ @?= Left (Text.pack "invalid character at offset: 3")+ decode (fromString "MZXW%YTBOI======")+ @?= Left (Text.pack "invalid character at offset: 4")+ decode (fromString "MZXW6%TBOI======")+ @?= Left (Text.pack "invalid character at offset: 5")+ decode (fromString "MZXW6Y%BOI======")+ @?= Left (Text.pack "invalid character at offset: 6")+ decode (fromString "MZXW6YT%OI======")+ @?= Left (Text.pack "invalid character at offset: 7") ] ]
+ test/Regression.hs view
@@ -0,0 +1,270 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- |+-- Module : Regression+-- Copyright : (c) 2026 Emily Pillmore+-- License : BSD-style+--+-- Maintainer : Emily Pillmore <emilypi@cohomolo.gy>+-- Stability : Experimental+-- Portability : portable+--+-- Regression tests for typed codecs, parsing, and buffer boundaries.+--+module Regression (regressions) where++import Control.Concurrent (forkIO, killThread, threadDelay)+import Control.DeepSeq (force)+import Control.Exception (ErrorCall, bracket, evaluate, try)+import Control.Monad (forM_, forever, replicateM)+import Data.Base32.Types+import Data.Bits (xor)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BS8+import qualified Data.ByteString.Base32 as B+import qualified Data.ByteString.Base32.Hex as H+import Data.ByteString.Base32.Internal.Head (decodeBase32_, encodeBase32_)+import Data.ByteString.Base32.Internal.Short (decodeShort, encodeShort)+import Data.ByteString.Base32.Internal.Tables (stdDecodeTable)+import Data.ByteString.Base32.Internal.Utils (encodedLength)+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Base32 as L+import qualified Data.ByteString.Short as SBS+import qualified Data.ByteString.Short.Base32 as S+import qualified Data.ByteString.Short.Base32.Hex as SH+import Data.Either (isLeft, isRight)+import Data.String (IsString, fromString)+import qualified Data.Text as T+import qualified Data.Text.Encoding.Base32 as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Encoding.Base32 as TL+import qualified Data.Text.Short as TS+import qualified Data.Text.Short.Encoding.Base32 as TS+import Foreign.Marshal.Array (peekArray, withArray)+import Internal+import System.Mem (performGC)+import System.Timeout (timeout)+import Test.QuickCheck hiding (label)+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck (testProperty)++regressions :: TestTree+regressions =+ testGroup+ "Release regressions"+ [ testCase "custom alphabets and tables retain scalar semantics" $ do+ let input = BS.pack (take 1000 (cycle [0 .. 255]))+ alphabet = "ACBDEFGHIJKLMNOPQRSTUVWXYZ234567"#+ swap w+ | w == 66 = 67+ | w == 67 = 66+ | otherwise = w+ expected = BS.map swap (extractBase32 (B.encodeBase32' input))+ encodeBase32_ alphabet input @?= expected+ encodeShort alphabet True (SBS.toShort input) @?= SBS.toShort expected+ table <- peekArray 256 stdDecodeTable+ let replace i w+ | i == 66 = 2+ | i == 67 = 1+ | otherwise = w+ withArray (zipWith replace [0 :: Int ..] table) $ \custom -> do+ decodeBase32_ custom expected >>= (@?= Right input)+ decodeShort custom (SBS.toShort expected)+ @?= Right (SBS.toShort input)+ , testCase "allocation lengths reject overflow before allocation" $ do+ forM_ [-1, maxBound] $ \n -> do+ result <- try (evaluate (encodedLength n)) :: IO (Either ErrorCall Int)+ assertBool "expected checked capacity failure" (isLeft result)+ map encodedLength [0 .. 5] @?= [0, 8, 8, 8, 8, 8]+ , testCase "strict codecs handle sliced inputs" $+ forM_ [0 .. 31] $ \offset -> do+ let payload = BS.pack (take 1001 (cycle [0 .. 255]))+ input = BS.drop offset (BS.replicate offset 99 <> payload)+ encoded = extractBase32 (B.encodeBase32' input)+ sliced = BS.drop offset (BS.replicate offset 99 <> encoded)+ B.decodeBase32Untyped sliced @?= Right payload+ B.decodeBase32 (assertBase32 @'StdPadded sliced) @?= payload+ , parserTests b32+ , parserTests lb32+ , parserTests sb32+ , parserTests t32+ , parserTests tl32+ , parserTests ts32+ , testCase "strict and short SIMD boundaries, both alphabets" $+ forM_ ([0 .. 170] ++ [255, 256, 257, 1023, 1024, 1025, 100000]) $ \n -> do+ let input = BS.pack (take n (cycle [0 .. 255]))+ short = SBS.toShort input+ encoded = B.encodeBase32' input+ encodedHex = H.encodeBase32' input+ extractBase32 (S.encodeBase32' short) @?= SBS.toShort (extractBase32 encoded)+ extractBase32 (SH.encodeBase32' short)+ @?= SBS.toShort (extractBase32 encodedHex)+ B.decodeBase32 encoded @?= input+ H.decodeBase32 encodedHex @?= input+ S.decodeBase32 (S.encodeBase32' short) @?= short+ SH.decodeBase32 (SH.encodeBase32' short) @?= short+ B.decodeBase32 (B.encodeBase32Unpadded' input) @?= input+ S.decodeBase32 (S.encodeBase32Unpadded' short) @?= short+ , testCase "typed lazy decoding streams without whole-input validation" $ do+ output <- timeout 1000000 $ evaluate $ force $+ BL.take 16 (L.decodeBase32 (L.encodeBase32' (BL.repeat 255)))+ output @?= Just (BL.replicate 16 255)+ , testCase "parsed padding provenance selects the matching typed decoder" $ do+ let padded = BS8.pack "MY======"+ unpaddedHex = BS8.pack "CO"+ decoded = BS8.pack "f"+ fmap B.decodeBase32Padded (B.parseBase32Padded padded) @?= Right decoded+ fmap H.decodeBase32Unpadded (H.parseBase32Unpadded unpaddedHex)+ @?= Right decoded+ fmap S.decodeBase32Padded (S.parseBase32Padded (SBS.toShort padded))+ @?= Right (SBS.toShort decoded)+ fmap SH.decodeBase32Unpadded (SH.parseBase32Unpadded (SBS.toShort unpaddedHex))+ @?= Right (SBS.toShort decoded)+ , testCase "lazy padding at every chunk boundary" $ do+ let encoded = extractBase32 (B.encodeBase32' (BS.replicate 81 102))+ forM_ [0 .. BS.length encoded] $ \i -> do+ let chunks = BL.fromChunks [BS.take i encoded, BS.drop i encoded]+ fmap extractBase32 (L.parseBase32Padded chunks) @?= Right chunks+ L.decodeBase32Untyped chunks @?= Right (BL.replicate 81 102)+ L.decodeBase32 (assertBase32 @'StdPadded chunks) @?= BL.replicate 81 102+ , testCase "invalid bytes after SIMD prefixes keep global offsets" $+ forM_ [0 .. 159] $ \i -> do+ let input = BS.replicate i 65 <> BS.singleton 33 <> BS.replicate (159 - i) 65+ expected = Left (T.pack ("invalid character at offset: " ++ show i))+ B.decodeBase32Untyped input @?= expected+ S.decodeBase32Untyped (SBS.toShort input) @?= expected+ , testCase "strict and short decoders agree on final padding" $+ forM_ [0, 80] $ \prefixLength ->+ forM_ [0 .. 8] $ \tailLength ->+ forM_ (replicateM tailLength [False, True]) $ \padding -> do+ let input symbol = BS.replicate prefixLength symbol+ <> BS.pack (map (\pad -> if pad then 0x3d else symbol) padding)+ standard = input 65+ hex = input 48+ check raw decodeStrict decodeSmall =+ decodeSmall (SBS.toShort raw) @?= fmap SBS.toShort (decodeStrict raw)+ check standard B.decodeBase32Untyped S.decodeBase32Untyped+ check standard B.decodeBase32PaddedUntyped S.decodeBase32PaddedUntyped+ check standard B.decodeBase32UnpaddedUntyped S.decodeBase32UnpaddedUntyped+ check hex H.decodeBase32Untyped SH.decodeBase32Untyped+ check hex H.decodeBase32PaddedUntyped SH.decodeBase32PaddedUntyped+ check hex H.decodeBase32UnpaddedUntyped SH.decodeBase32UnpaddedUntyped+ , testProperty "strict and short malformed inputs agree" $ \(bytes :: [Word]) ->+ let input = BS.pack (map fromIntegral bytes)+ short = SBS.toShort input+ in conjoin+ [ fmap SBS.toShort (B.decodeBase32Untyped input)+ === S.decodeBase32Untyped short+ , fmap SBS.toShort (H.decodeBase32Untyped input)+ === SH.decodeBase32Untyped short+ , fmap SBS.toShort (B.decodeBase32PaddedUntyped input)+ === S.decodeBase32PaddedUntyped short+ , fmap SBS.toShort (B.decodeBase32UnpaddedUntyped input)+ === S.decodeBase32UnpaddedUntyped short+ ]+ , testCase "text parsers accept binary; decoders check UTF-8" $ do+ assertBool "binary parser" (isRight (T.parseBase32Padded (T.pack "74======")))+ assertBool "invalid UTF-8" (isLeft (T.decodeBase32Untyped (T.pack "74======")))+ assertBool "lazy invalid UTF-8"+ (isLeft (TL.decodeBase32Untyped (TL.pack "74======")))+ assertBool "short invalid UTF-8"+ (isLeft (TS.decodeBase32Untyped (TS.pack "74======")))+ , testCase "short arrays remain valid during concurrent collection" $+ bracket (forkIO (forever (performGC >> threadDelay 1000))) killThread $ \_ ->+ forM_ [0 .. 99 :: Int] $ \salt -> do+ let input =+ SBS.toShort+ ( BS.pack+ [fromIntegral (i `xor` salt) | i <- [0 .. 8191 :: Int]]+ )+ output <- evaluate (force (S.decodeBase32 (S.encodeBase32' input)))+ output @?= input+ , testCase "typed text roundtrip" $ do+ T.decodeBase32 (T.encodeBase32 (T.pack "λ☃")) @?= T.pack "λ☃"+ TS.decodeBase32 (TS.encodeBase32 (TS.pack "λ☃")) @?= TS.pack "λ☃"+ ]++parserTests ::+ (Arbitrary a, IsString a, Eq a, Show a) =>+ Harness a ->+ TestTree+parserTests Harness{..} =+ testGroup+ (label ++ " parsers")+ [ testProperty "typed standard decoding preserves padding provenance" $ \input ->+ let padded = assertBase32 @'StdPadded (encode input)+ unpadded = assertBase32 @'StdUnpadded (encodeNopad input)+ in conjoin+ [ decodeTyped padded === input+ , decodeTyped unpadded === input+ , decodePaddedTyped padded === input+ , decodeUnpaddedTyped unpadded === input+ ]+ , testProperty "typed hex decoding preserves padding provenance" $ \input ->+ let padded = assertBase32 @'HexPadded (encodeHex input)+ unpadded = assertBase32 @'HexUnpadded (encodeHexNopad input)+ in conjoin+ [ decodeHexTyped padded === input+ , decodeHexTyped unpadded === input+ , decodeHexPaddedTyped padded === input+ , decodeHexUnpaddedTyped unpadded === input+ ]+ , testProperty "preserve padded standard input" $ \input ->+ parsePaddedValue (encode input) === Right (encode input)+ , testProperty "preserve unpadded hex input" $ \input ->+ parseHexUnpaddedValue (encodeHexNopad input) === Right (encodeHexNopad input)+ , testCase "preserve original letter case" $ do+ parseValue (fromString "my======") @?= Right (fromString "my======")+ parseHexValue (fromString "co======") @?= Right (fromString "co======")+ decodeTyped (assertBase32 @'StdPadded (fromString "my======")) @?= fromString "f"+ decodeHexTyped (assertBase32 @'HexPadded (fromString "co======"))+ @?= fromString "f"+ , testCase "enforce padding policies" $ do+ assertBool "padded requires padding" (isLeft (parsePaddedValue (fromString "MY")))+ assertBool "unpadded rejects padding"+ (isLeft (parseUnpaddedValue (fromString "MY======")))+ assertBool "hex padded requires padding"+ (isLeft (parseHexPaddedValue (fromString "CO")))+ assertBool+ "hex unpadded rejects padding"+ (isLeft (parseHexUnpaddedValue (fromString "CO======")))+ parseValue (fromString "") @?= Right (fromString "")+ parseHexValue (fromString "") @?= Right (fromString "")+ , testCase "reject nonzero unused bits in all tail lengths" $ do+ forM_ (map fromString+ [ "MZ"+ , "MZXR"+ , "MZXW7"+ , "MZXW6YR"+ , "MZ======"+ , "MZXR===="+ , "MZXW7==="+ , "MZXW6YR="+ ])+ $ \value ->+ assertBool (show value)+ (isLeft (parsePaddedValue value) && isLeft (parseUnpaddedValue value))+ forM_ (map fromString+ [ "CP"+ , "CPNH"+ , "CPNMV"+ , "CPNMUOH"+ , "CP======"+ , "CPNH===="+ , "CPNMV==="+ , "CPNMUOH="+ ])+ $ \value ->+ assertBool (show value)+ (isLeft (parseHexPaddedValue value) && isLeft (parseHexUnpaddedValue value))+ , testCase "parser and checked decoder report the same errors" $+ forM_ (map fromString+ ["M", "!Y======", "MZ======", "MY====", "MY======MY======"]) $ \value ->+ case decodePad value of+ Left err -> parseValue value @?= Left err+ Right _ -> assertFailure "expected malformed fixture"+ ]
+ test/simd-native.c view
@@ -0,0 +1,184 @@+/* Copyright (c) 2026 Emily Pillmore. BSD-3-Clause; see LICENSE. */+#include "base32_simd.h"+#include <assert.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#if defined(__unix__) || defined(__APPLE__)+#include <sys/mman.h>+#include <unistd.h>+#endif++static const char *alphabets[2] = {+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", "0123456789ABCDEFGHIJKLMNOPQRSTUV"+};++static void reference(const uint8_t *src, size_t len, uint8_t *dst, int hex)+{+ unsigned bits = 0, buffer = 0;+ size_t out = 0;+ for (size_t i = 0; i < len; ++i) {+ buffer = (buffer << 8) | src[i];+ bits += 8;+ while (bits >= 5) {+ bits -= 5;+ dst[out++] = (uint8_t)alphabets[hex][(buffer >> bits) & 31];+ }+ }+ if (bits) {+ dst[out] = (uint8_t)alphabets[hex][(buffer << (5 - bits)) & 31];+ }+}++static void boundaries(int hex)+{+ for (size_t len = 0; len <= 1024; ++len) {+ for (size_t offset = 0; offset < 16; ++offset) {+ size_t capacity = (len / 5) * 8;+ uint8_t *allocation = malloc(len + offset + (len == 0));+ uint8_t *output = malloc(capacity + offset + 1);+ uint8_t *expected = malloc((len * 8 + 4) / 5 + 1);+ assert(allocation && output && expected);+ uint8_t *src = allocation + offset;+ uint8_t *dst = output + offset;+ for (size_t i = 0; i < len; ++i) {+ src[i] = (uint8_t)(i * 131 + len * 17);+ }+ memset(output, 0xa5, capacity + offset + 1);+ reference(src, len, expected, hex);+ size_t read = hs_base32_simd_encode(src, len, dst, hex);+ assert(read <= len && read % 5 == 0);+ if (hs_base32_simd_backend() && len >= 20) {+ assert(read != 0);+ }+ size_t written = read / 5 * 8;+ assert(memcmp(dst, expected, written) == 0);+ for (size_t i = written; i <= capacity; ++i) {+ assert(dst[i] == 0xa5);+ }+ for (size_t i = 0; i < offset; ++i) {+ assert(output[i] == 0xa5);+ }+ /* Decode from an allocation that ends exactly at the input end. */+ uint8_t *encoded = malloc(written + offset + (written == 0));+ uint8_t *decoded = malloc(read + offset + 1);+ assert(encoded && decoded);+ memcpy(encoded + offset, expected, written);+ memset(decoded, 0xa5, read + offset + 1);+ size_t used = hs_base32_simd_decode(encoded + offset, written,+ decoded + offset, hex);+ assert(used <= written && used % 8 == 0);+ assert(used == (hs_base32_simd_backend() ? written : 0));+ assert(memcmp(decoded + offset, src, used / 8 * 5) == 0);+ for (size_t i = used / 8 * 5; i <= read; ++i) {+ assert(decoded[offset + i] == 0xa5);+ }+ for (size_t i = 0; i < offset; ++i) {+ assert(decoded[i] == 0xa5);+ }+ memset(decoded, 0xa5, read + offset + 1);+ used = hs_base32_simd_decode_typed(encoded + offset, written,+ decoded + offset, hex);+ assert(used == (hs_base32_simd_backend() ? written : 0));+ assert(memcmp(decoded + offset, src, used / 8 * 5) == 0);+ for (size_t i = used / 8 * 5; i <= read; ++i) {+ assert(decoded[offset + i] == 0xa5);+ }+ for (size_t i = 0; i < offset; ++i) {+ assert(decoded[i] == 0xa5);+ }+ free(encoded);+ free(decoded);+ free(allocation);+ free(output);+ free(expected);+ }+ }+}++static void malformed(int hex)+{+ uint8_t payload[80], encoded[128], changed[128], decoded[80];+ for (size_t i = 0; i < sizeof payload; ++i) {+ payload[i] = (uint8_t)(i * 73);+ }+ reference(payload, sizeof payload, encoded, hex);+ for (size_t pos = 0; pos < sizeof encoded; ++pos) {+ for (unsigned byte = 0; byte < 256; ++byte) {+ unsigned upper = byte & ~32U;+ int valid = (upper >= 'A' && upper <= (hex ? 'V' : 'Z')) ||+ (byte >= (hex ? '0' : '2') && byte <= (hex ? '9' : '7'));+ if (valid) {+ continue;+ }+ memcpy(changed, encoded, sizeof changed);+ changed[pos] = (uint8_t)byte;+ memset(decoded, 0xa5, sizeof decoded);+ size_t used = hs_base32_simd_decode(changed, sizeof changed, decoded, hex);+ assert(used <= pos && used % 8 == 0);+ assert(memcmp(decoded, payload, used / 8 * 5) == 0);+ for (size_t i = used / 8 * 5; i < sizeof decoded; ++i) {+ assert(decoded[i] == 0xa5);+ }+ }+ }+ for (size_t i = 0; i < sizeof encoded; ++i) {+ if (encoded[i] >= 'A' && encoded[i] <= 'Z') {+ encoded[i] |= 32;+ }+ }+ size_t used = hs_base32_simd_decode(encoded, sizeof encoded, decoded, hex);+ assert(used == (hs_base32_simd_backend() ? sizeof encoded : 0));+ assert(memcmp(decoded, payload, used / 8 * 5) == 0);+ used = hs_base32_simd_decode_typed(encoded, sizeof encoded, decoded, hex);+ assert(used == (hs_base32_simd_backend() ? sizeof encoded : 0));+ assert(memcmp(decoded, payload, used / 8 * 5) == 0);++ /* White-box check: the typed entry point must bypass validation. */+ memset(encoded, '!', sizeof encoded);+ used = hs_base32_simd_decode_typed(encoded, sizeof encoded, decoded, hex);+ assert(used == (hs_base32_simd_backend() ? sizeof encoded : 0));+}++static void guarded(int hex)+{+#if defined(__unix__) || defined(__APPLE__)+ size_t page = (size_t)sysconf(_SC_PAGESIZE);+ uint8_t *src = mmap(NULL, page * 2, PROT_READ | PROT_WRITE,+ MAP_PRIVATE | MAP_ANON, -1, 0);+ uint8_t *dst = mmap(NULL, page * 2, PROT_READ | PROT_WRITE,+ MAP_PRIVATE | MAP_ANON, -1, 0);+ assert(src != MAP_FAILED && dst != MAP_FAILED);+ assert(mprotect(src + page, page, PROT_NONE) == 0);+ assert(mprotect(dst + page, page, PROT_NONE) == 0);+ for (size_t len = 0; len <= 160; ++len) {+ size_t capacity = len / 5 * 8;+ memset(src + page - len, 42, len);+ hs_base32_simd_encode(src + page - len, len, dst + page - capacity, hex);+ capacity = len / 8 * 5;+ memset(src + page - len, hex ? '0' : 'A', len);+ hs_base32_simd_decode(src + page - len, len, dst + page - capacity, hex);+ hs_base32_simd_decode_typed(src + page - len, len, dst + page - capacity, hex);+ }+ assert(munmap(src, page * 2) == 0);+ assert(munmap(dst, page * 2) == 0);+#else+ (void)hex;+#endif+}++int main(int argc, char **argv)+{+ if (argc == 2) {+ assert(hs_base32_simd_backend() == atoi(argv[1]));+ } else {+ assert(argc == 1);+ }+ for (int hex = 0; hex < 2; ++hex) {+ boundaries(hex);+ malformed(hex);+ guarded(hex);+ }+ printf("Native Base32 tests passed (backend %d)\n", hs_base32_simd_backend());+ return 0;+}
+ test/typecheck.sh view
@@ -0,0 +1,97 @@+#!/bin/sh+# Run inside the package environment: cabal exec -- sh test/typecheck.sh+set -eu++compiler=${GHC:-ghc}+work=$(mktemp -d)+trap 'rm -rf "$work"' EXIT HUP INT TERM++compile() {+ "$compiler" -v0 -fno-code -fforce-recomp -package base32 \+ -odir "$work" -hidir "$work" "$work/Api.hs" > "$work/result" 2>&1+}++reject() {+ cat "$work/header" > "$work/Api.hs"+ printf '%s\n' "$1" >> "$work/Api.hs"+ if compile; then+ printf 'Unexpectedly accepted in %s: %s\n' "$api" "$1" >&2+ exit 1+ fi+ if ! grep -F "$2" "$work/result" > /dev/null; then+ cat "$work/result" >&2+ exit 1+ fi+}++while read -r api representation value suffix; do+ [ "$suffix" != - ] || suffix=+ padded="encodeBase32$suffix"+ unpadded="encodeBase32Unpadded$suffix"+ cat > "$work/header" <<EOF+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+module Api where+import Data.Base32.Types+import Data.Coerce (coerce)+import qualified $representation as R+import qualified Data.Text as Error+import qualified $api as S+import qualified $api.Hex as H+type Value = R.$value+EOF+ cat "$work/header" > "$work/Api.hs"+ cat >> "$work/Api.hs" <<EOF+standard :: StdAlphabet k => Base32 k Value -> Value+standard = S.decodeBase32+hexadecimal :: HexAlphabet k => Base32 k Value -> Value+hexadecimal = H.decodeBase32+roundtrips :: Value -> [Value]+roundtrips value =+ [ standard (S.$padded value), standard (S.$unpadded value)+ , S.decodeBase32Padded (S.$padded value)+ , S.decodeBase32Unpadded (S.$unpadded value)+ , hexadecimal (H.$padded value), hexadecimal (H.$unpadded value)+ , H.decodeBase32Padded (H.$padded value)+ , H.decodeBase32Unpadded (H.$unpadded value)+ ]+stdPadded :: Value -> Either Error.Text (Base32 'StdPadded Value)+stdPadded = S.parseBase32Padded+stdUnpadded :: Value -> Either Error.Text (Base32 'StdUnpadded Value)+stdUnpadded = S.parseBase32Unpadded+hexPadded :: Value -> Either Error.Text (Base32 'HexPadded Value)+hexPadded = H.parseBase32Padded+hexUnpadded :: Value -> Either Error.Text (Base32 'HexUnpadded Value)+hexUnpadded = H.parseBase32Unpadded+EOF+ if ! compile; then+ cat "$work/result" >&2+ exit 1+ fi+ for encoder in "$padded" "$unpadded"; do+ reject "bad = S.decodeBase32 . H.$encoder" 'standard alphabet'+ reject "bad = H.decodeBase32 . S.$encoder" 'extended-hex alphabet'+ done+ for alphabet in S H; do+ reject "bad = $alphabet.decodeBase32Padded . $alphabet.$unpadded" \+ "Couldn't match type"+ reject "bad = $alphabet.decodeBase32Unpadded . $alphabet.$padded" \+ "Couldn't match type"+ reject "bad = fmap $alphabet.decodeBase32Padded . $alphabet.parseBase32Unpadded" \+ "Couldn't match type"+ reject "bad = fmap $alphabet.decodeBase32Unpadded . $alphabet.parseBase32Padded" \+ "Couldn't match type"+ done+ reject "bad :: Base32 'StdPadded Value -> Base32 'StdUnpadded Value+bad = coerce" "Couldn't match type"+ reject "bad :: Base32 'StdPadded Value -> Base32 'HexPadded Value+bad = coerce" "Couldn't match type"+ printf 'Type checks passed: %s\n' "$api"+done <<'EOF'+Data.ByteString.Base32 Data.ByteString ByteString '+Data.ByteString.Lazy.Base32 Data.ByteString.Lazy ByteString '+Data.ByteString.Short.Base32 Data.ByteString.Short ShortByteString '+Data.Text.Encoding.Base32 Data.Text Text -+Data.Text.Lazy.Encoding.Base32 Data.Text.Lazy Text -+Data.Text.Short.Encoding.Base32 Data.Text.Short ShortText -+EOF