-- | Module : Data.BaseSystem.Internal -- Description : Provides common resources for multibase -- Copyright : Zoey McBride (c) 2026 -- License : BSD-3-Clause -- Maintainer : zoeymcbride@mailbox.org -- Stability : experimental -- -- Common data definitions and helper functions for this library. module Data.BaseSystem.Internal ( -- * BaseSystem data structures RadixSystem (..), PaddingMethod (..), BitwiseSystem (..), -- * List utilities replaceNull, iterateInit, -- * ByteString utilities bytesToInteger, packInteger, fitsGroup, -- * Decoder implementations binaryDecoder, ) where import Control.Monad (foldM) import Data.BaseSystem.Alphabet (Alphabet) import Data.BaseSystem.Alphabet qualified as Alpha import Data.Bits ((.&.), (.<<.), (.>>.), (.|.)) import Data.ByteString (ByteString) import Data.ByteString qualified as Bytes import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text qualified as Text -- | Implements BaseSystem over base radix modulus. data RadixSystem = RadixSystem { -- | Used to implement instance of Show. radixShow :: String, -- | Alphabet for the base system. radixAlpha :: Alphabet } instance Show RadixSystem where show = radixShow -- | Implements padding for encoding BitwiseSystem. data PaddingMethod = PaddingMethod { -- | Gives the char to use as padding. paddingChar :: Char, -- | Provides the callback to resolve the padding from # of bytes. paddingResolve :: Int -> String } -- | Implements BaseSystem over series of bit groups. data BitwiseSystem = BitwiseSystem { -- | String used to implement instance of Show. bitwiseShow :: String, -- | Base system alphabet. bitwiseAlpha :: Alphabet, -- | # of bits per symbol bitwiseSymBits :: Int, -- | # of bytes to be processed into symbols in encoding. bitwiseGroupBytes :: Int, -- | # of bytes forming a group to find padding in decoding. bitwiseGroupSymbols :: Int, -- | Gives the method for padding. bitwisePadMethod :: Maybe PaddingMethod } instance Show BitwiseSystem where show = bitwiseShow -- | Return the the first param if the second param is empty. {-# INLINE replaceNull #-} replaceNull :: [a] -> [a] -> [a] replaceNull replace xs | null xs = replace | otherwise = xs -- | Wraps iterate to compose with an intermediary type constructor. iterateInit :: (b -> b) -> (a -> b) -> a -> [b] iterateInit f iterinit = drop 1 . iterate f . iterinit -- | Builds an Integer from ByteString. bytesToInteger :: ByteString -> Integer bytesToInteger = Bytes.foldl' (\acc x -> (acc .<<. 8) .|. fromIntegral x) 0 . Bytes.dropWhile (== 0) -- | Encode an Integer's numeric value in a ByteString as raw base2 binary -- values from the the Integer's bit values. packInteger :: Integer -> ByteString packInteger = -- This code can be optimized; if we precalculate the length of the integer -- in bits we can mask from the other direction using left shifts, and remove -- the call to reverse, maybe we can even use a list comprehension to build -- in place for Bytes.pack let maskbyte = (.&. 0xFF) shiftbyte = (.>>. 8) in Bytes.pack . replaceNull [0] . reverse . map (\(_, byte) -> fromIntegral byte) . takeWhile (\(rest, byte) -> rest > 0 || byte > 0) . iterateInit (\(input, _) -> (shiftbyte input, maskbyte input)) (,0) -- | Checks if an Integral a fits within a BitwiseSystem's group. {-# INLINE fitsGroup #-} fitsGroup :: Int -> ByteString -> Integer -> Bool fitsGroup groupsize groupbytes groupbits = Bytes.length groupbytes <= groupsize && minInt <= groupbits && groupbits <= maxInt where minInt = fromIntegral (minBound :: Int) maxInt = fromIntegral (maxBound :: Int) -- | Function to cleanup data in decoding. type FinalizeBits = Maybe (Integer -> Integer) -- Builds symbols from a number in decoding. type DecoderBuilder = Integer -> Alpha.Symbol -> Maybe Integer -- | Provides generic structure for decoding symbols into binary data -- ByteString. {-# INLINE binaryDecoder #-} binaryDecoder :: FinalizeBits -> Text -> DecoderBuilder -> Maybe ByteString binaryDecoder finalize text builder | Text.null text = Nothing | otherwise = fmap (packInteger . applyFinalize) . foldM builder 0 $ Text.chunksOf 1 text where -- If finalize exists, apply it, otherwise return the unchanged value. applyFinalize value = fromMaybe value $ finalize <*> Just value