fec-0.2.0: haskell/Codec/FEC.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE NamedFieldPuns #-}
{- |
Module: Codec.FEC
Copyright: Adam Langley
License: GPLv2+|TGPPLv1+ (see README.rst for details)
Stability: experimental
The module provides k of n encoding - a way to generate (n - k) secondary
blocks of data from k primary blocks such that any k blocks (primary or
secondary) are sufficient to regenerate all blocks.
All blocks must be the same length and you need to keep track of which
blocks you have in order to tell decode. By convention, the blocks are
numbered 0..(n - 1) and blocks numbered < k are the primary blocks.
-}
module Codec.FEC (
FECParams (paramK, paramN),
initialize,
fec,
encode,
decode,
-- * Utility functions
secureDivide,
secureCombine,
enFEC,
deFEC,
) where
import Control.Concurrent.Extra (Lock, newLock, withLock)
import Control.DeepSeq (NFData (rnf))
import Control.Exception (Exception, throwIO)
import Data.Bits (xor)
import qualified Data.ByteString as B
import qualified Data.ByteString.Unsafe as BU
import Data.List (nub, partition, sortBy, (\\))
import Data.Word (Word8)
import Foreign.C.Types (CSize (..), CUInt (..))
import Foreign.ForeignPtr (
ForeignPtr,
newForeignPtr,
withForeignPtr,
)
import Foreign.Marshal.Alloc (allocaBytes)
import Foreign.Marshal.Array (advancePtr, withArray)
import Foreign.Ptr (FunPtr, Ptr, castPtr, nullPtr)
import Foreign.Storable (poke, sizeOf)
import GHC.Generics (Generic)
import System.IO (IOMode (..), withFile)
import System.IO.Unsafe (unsafePerformIO)
data CFEC
data FECParams = FECParams
{ _cfec :: !(ForeignPtr CFEC)
, paramK :: Int
, paramN :: Int
}
deriving (Generic)
-- Provide an NFData instance so it's possible to use a FECParams in a
-- Criterion benchmark.
instance NFData FECParams where
rnf FECParams{_cfec, paramK, paramN} =
-- ForeignPtr has no NFData instance and I don't know how to implement
-- one for it so we punt on it here. We do make it strict in the
-- record definition which at least shallowly evaluates the
-- ForeignPtr which is ... part of the job?
rnf paramK `seq` rnf paramN
instance Show FECParams where
show (FECParams _ k n) = "FEC (" ++ show k ++ ", " ++ show n ++ ")"
foreign import ccall unsafe "fec_init"
_init :: IO ()
foreign import ccall unsafe "fec_new"
_new ::
-- | k
CUInt ->
-- | n
CUInt ->
IO (Ptr CFEC)
foreign import ccall unsafe "&fec_free" _free :: FunPtr (Ptr CFEC -> IO ())
foreign import ccall unsafe "fec_encode"
_encode ::
Ptr CFEC ->
-- | primary blocks
Ptr (Ptr Word8) ->
-- | (output) secondary blocks
Ptr (Ptr Word8) ->
-- | array of secondary block ids
Ptr CUInt ->
-- | length of previous
CSize ->
-- | block length
CSize ->
IO ()
foreign import ccall unsafe "fec_decode"
_decode ::
Ptr CFEC ->
-- | input blocks
Ptr (Ptr Word8) ->
-- | output blocks
Ptr (Ptr Word8) ->
-- | array of input indexes
Ptr CUInt ->
-- | block length
CSize ->
IO ()
-- | Return true if the given @k@ and @n@ values are valid
isValidConfig :: Int -> Int -> Bool
isValidConfig k n
| k > n = False
| k < 1 = False
| n < 1 = False
| n > 255 = False
| otherwise = True
{- | The underlying library signaled that it has not been properly initialized
yet. Use @initialize@ to initialize it.
-}
data Uninitialized = Uninitialized deriving (Ord, Eq, Show)
instance Exception Uninitialized
-- A lock to ensure at most one thread attempts to initialize the underlying
-- library at a time. Multiple initializations are harmless but concurrent
-- initializations are disallowed.
_initializationLock :: Lock
{-# NOINLINE _initializationLock #-}
_initializationLock = unsafePerformIO newLock
-- | Initialize the library. This must be done before other APIs can succeed.
initialize :: IO ()
initialize = withLock _initializationLock _init
-- | Return a FEC with the given parameters.
fec ::
-- | the number of primary blocks
Int ->
-- | the total number blocks, must be < 256
Int ->
FECParams
fec k n =
if not (isValidConfig k n)
then error $ "Invalid FEC parameters: " ++ show k ++ " " ++ show n
else
unsafePerformIO
( do
cfec' <- _new (fromIntegral k) (fromIntegral n)
-- new will return null if the library hasn't been
-- initialized.
if cfec' == nullPtr
then throwIO Uninitialized
else do
params <- newForeignPtr _free cfec'
return $ FECParams params k n
)
-- | Create a C array of unsigned from an input array
uintCArray :: [Int] -> (Ptr CUInt -> IO a) -> IO a
uintCArray = withArray . map fromIntegral
-- | Convert a list of ByteStrings to an array of pointers to their data
byteStringsToArray :: [B.ByteString] -> (Ptr (Ptr Word8) -> IO a) -> IO a
byteStringsToArray inputs f = do
let l = length inputs
allocaBytes
(l * sizeOf (undefined :: Ptr Word8))
( \array -> do
let inner _ [] = f array
inner array' (bs : bss) =
BU.unsafeUseAsCString
bs
( \ptr -> do
poke array' $ castPtr ptr
inner (advancePtr array' 1) bss
)
inner array inputs
)
-- | Return True iff all the given ByteStrings are the same length
allByteStringsSameLength :: [B.ByteString] -> Bool
allByteStringsSameLength [] = True
allByteStringsSameLength (bs : bss) = all ((==) (B.length bs) . B.length) bss
{- | Run the given function with a pointer to an array of @n@ pointers to
buffers of size @size@. Return these buffers as a list of ByteStrings
-}
createByteStringArray ::
-- | the number of buffers requested
Int ->
-- | the size of each buffer
Int ->
(Ptr (Ptr Word8) -> IO ()) ->
IO [B.ByteString]
createByteStringArray n size f = do
allocaBytes
(n * sizeOf (undefined :: Ptr Word8))
( \array -> do
allocaBytes
(n * size)
( \ptr -> do
mapM_ (\i -> poke (advancePtr array i) (advancePtr ptr (size * i))) [0 .. (n - 1)]
f array
mapM (\i -> B.packCStringLen (castPtr $ advancePtr ptr (i * size), size)) [0 .. (n - 1)]
)
)
{- | Generate the secondary blocks from a list of the primary blocks. The
primary blocks must be in order and all of the same size. There must be
@k@ primary blocks.
-}
encode ::
FECParams ->
-- | a list of @k@ input blocks
[B.ByteString] ->
-- | (n - k) output blocks
[B.ByteString]
encode (FECParams params k n) inblocks
| length inblocks /= k = error "Wrong number of blocks to FEC encode"
| not (allByteStringsSameLength inblocks) = error "Not all inputs to FEC encode are the same length"
| otherwise =
unsafePerformIO
( do
let sz = B.length $ head inblocks
withForeignPtr
params
( \cfec' -> do
byteStringsToArray
inblocks
( \src -> do
createByteStringArray
(n - k)
sz
( \fecs -> do
uintCArray
[k .. (n - 1)]
( \block_nums -> do
_encode cfec' src fecs block_nums (fromIntegral (n - k)) $ fromIntegral sz
)
)
)
)
)
-- | A sort function for tagged assoc lists
sortTagged :: [(Int, a)] -> [(Int, a)]
sortTagged = sortBy (\a b -> compare (fst a) (fst b))
{- | Reorder the given list so that elements with tag numbers < the first
argument have an index equal to their tag number (if possible)
-}
reorderPrimaryBlocks :: Int -> [(Int, a)] -> [(Int, a)]
reorderPrimaryBlocks n blocks = inner (sortTagged pBlocks) sBlocks []
where
(pBlocks, sBlocks) = partition (\(tag, _) -> tag < n) blocks
inner [] sBlocks' acc = acc ++ sBlocks'
inner pBlocks' [] acc = acc ++ pBlocks'
inner pBlocks'@((tag, a) : ps) sBlocks'@(s : ss) acc =
if length acc == tag
then inner ps sBlocks' (acc ++ [(tag, a)])
else inner pBlocks' ss (acc ++ [s])
{- | Recover the primary blocks from a list of @k@ blocks. Each block must be
tagged with its number (see the module comments about block numbering)
-}
decode ::
FECParams ->
-- | a list of @k@ blocks and their index
[(Int, B.ByteString)] ->
-- | a list the @k@ primary blocks
[B.ByteString]
decode (FECParams params k n) inblocks
| length (nub $ map fst inblocks) /= length inblocks = error "Duplicate input blocks in FEC decode"
| any ((\f -> f < 0 || f >= n) . fst) inblocks = error "Invalid block numbers in FEC decode"
| length inblocks /= k = error "Wrong number of blocks to FEC decode"
| not (allByteStringsSameLength $ map snd inblocks) = error "Not all inputs to FEC decode are same length"
| otherwise =
unsafePerformIO
( do
let sz = B.length $ snd $ head inblocks
inblocks' = reorderPrimaryBlocks k inblocks
presentBlocks = map fst inblocks'
withForeignPtr
params
( \cfec' -> do
byteStringsToArray
(map snd inblocks')
( \src -> do
b <-
createByteStringArray
(n - k)
sz
( \out -> do
uintCArray
presentBlocks
( \block_nums -> do
_decode cfec' src out block_nums $ fromIntegral sz
)
)
let blocks = [0 .. (n - 1)] \\ presentBlocks
tagged = zip blocks b
allBlocks = sortTagged $ tagged ++ inblocks'
return $ take k $ map snd allBlocks
)
)
)
{- | Break a ByteString into @n@ parts, equal in length to the original, such
that all @n@ are required to reconstruct the original, but having less
than @n@ parts reveals no information about the orginal.
This code works in IO monad because it needs a source of random bytes,
which it gets from /dev/urandom. If this file doesn't exist an
exception results
Not terribly fast - probably best to do it with short inputs (e.g. an
encryption key)
-}
secureDivide ::
-- | the number of parts requested
Int ->
-- | the data to be split
B.ByteString ->
IO [B.ByteString]
secureDivide n input
| n < 0 = error "secureDivide called with negative number of parts"
| otherwise =
withFile
"/dev/urandom"
ReadMode
( \handle -> do
let inner 1 bs = return [bs]
inner n' bs = do
mask <- B.hGet handle (B.length bs)
let masked = B.pack $ B.zipWith xor bs mask
rest <- inner (n' - 1) masked
return (mask : rest)
inner n input
)
{- | Reverse the operation of secureDivide. The order of the inputs doesn't
matter, but they must all be the same length
-}
secureCombine :: [B.ByteString] -> B.ByteString
secureCombine [] = error "Passed empty list of inputs to secureCombine"
secureCombine [a] = a
secureCombine [a, b] = B.pack $ B.zipWith xor a b
secureCombine (a : rest) = B.pack $ B.zipWith xor a $ secureCombine rest
{- | A utility function which takes an arbitary input and FEC encodes it into a
number of blocks. The order the resulting blocks doesn't matter so long
as you have enough to present to @deFEC@.
-}
enFEC ::
-- | the number of blocks required to reconstruct
Int ->
-- | the total number of blocks
Int ->
-- | the data to divide
B.ByteString ->
-- | the resulting blocks
[B.ByteString]
enFEC k n input = taggedPrimaryBlocks ++ taggedSecondaryBlocks
where
taggedPrimaryBlocks = zipWith B.cons [0 ..] primaryBlocks
taggedSecondaryBlocks = zipWith B.cons [(fromIntegral k) ..] secondaryBlocks
remainder = B.length input `mod` k
paddingLength = if remainder >= 1 then k - remainder else k
paddingBytes = B.replicate (paddingLength - 1) 0 `B.append` B.singleton (fromIntegral paddingLength)
divide a bs
| B.null bs = []
| otherwise = B.take a bs : divide a (B.drop a bs)
input' = input `B.append` paddingBytes
blockSize = B.length input' `div` k
primaryBlocks = divide blockSize input'
secondaryBlocks = encode params primaryBlocks
params = fec k n
-- | Reverses the operation of @enFEC@.
deFEC ::
-- | the number of blocks required (matches call to @enFEC@)
Int ->
-- | the total number of blocks (matches call to @enFEC@)
Int ->
-- | a list of k, or more, blocks from @enFEC@
[B.ByteString] ->
B.ByteString
deFEC k n inputs
| length inputs < k = error "Too few inputs to deFEC"
| otherwise = B.take (B.length fecOutput - paddingLength) fecOutput
where
paddingLength = fromIntegral $ B.last fecOutput
inputs' = take k inputs
taggedInputs = map (\bs -> (fromIntegral $ B.head bs, B.tail bs)) inputs'
fecOutput = B.concat $ decode params taggedInputs
params = fec k n