packages feed

byteslice-0.2.15.0: src/Data/Bytes/Chunks.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UnboxedTuples #-}

{- | Chunks of bytes. This is useful as a target for a builder
or as a way to read a large amount of whose size is unknown
in advance. Structurally, this type is similar to
@Data.ByteString.Lazy.ByteString@. However, the type in this
module is strict in its spine. Additionally, none of the
@Handle@ functions perform lazy I\/O.
-}
module Data.Bytes.Chunks
  ( -- * Types
    Chunks (..)

    -- * Properties
  , length
  , null

    -- * Manipulate
  , cons
  , concat
  , concatPinned
  , concatU
  , concatPinnedU
  , concatByteString
  , reverse
  , reverseOnto
  , replicate
  , replicateByte

    -- * Folds
  , foldl'

    -- * Splitting
  , split

    -- * Hashing
  , fnv1a32
  , fnv1a64

    -- * Create
  , fromBytes
  , fromByteArray

    -- * Copy to buffer
  , unsafeCopy

    -- * I\/O with Handles
  , hGetContents
  , readFile
  , hPut
  , writeFile
  ) where

import Prelude hiding (Foldable (..), concat, readFile, replicate, reverse, writeFile)

import Control.Exception (IOException, catch)
import Control.Monad.ST.Run (runIntByteArrayST)
import Data.Bits (xor)
import Data.ByteString (ByteString)
import Data.Bytes.Types (Bytes (Bytes))
import Data.Primitive (ByteArray (..), MutableByteArray (..))
import Data.Word (Word32, Word64, Word8)
import GHC.Exts (ByteArray#, Int (I#), Int#, MutableByteArray#, State#, (+#))
import GHC.ST (ST (..))
import System.IO (Handle, IOMode (ReadMode, WriteMode), hFileSize, withBinaryFile)

import qualified Data.Bytes.Byte as Byte
import qualified Data.Bytes.IO as IO
import qualified Data.Bytes.Pure as Bytes
import qualified Data.Bytes.Types as B
import qualified Data.Primitive as PM
import qualified GHC.Exts as Exts

-- | A cons-list of byte sequences.
data Chunks
  = ChunksCons {-# UNPACK #-} !Bytes !Chunks
  | ChunksNil
  deriving stock (Show)

instance Semigroup Chunks where
  ChunksNil <> a = a
  cs@(ChunksCons _ _) <> ChunksNil = cs
  as@(ChunksCons _ _) <> bs@(ChunksCons _ _) =
    reverseOnto bs (reverse as)

instance Monoid Chunks where
  mempty = ChunksNil

-- | This uses @concat@ to form an equivalence class.
instance Eq Chunks where
  -- TODO: There is a more efficient way to do this, but
  -- it is tedious.
  a == b = concat a == concat b

-- | Add a byte sequence to the beginning.
cons :: Bytes -> Chunks -> Chunks
{-# INLINE cons #-}
cons = ChunksCons

{- | Repeat the byte sequence over and over. Returns empty chunks when given
a negative repetition count.
-}
replicate ::
  Bytes ->
  -- | Number of times to repeat the sequence.
  Int ->
  Chunks
replicate !b@(Bytes _ _ len) !n
  | n <= 0 = ChunksNil
  | len == 0 = ChunksNil
  | otherwise = go n ChunksNil
 where
  -- Implementation note: We do not have to reverse the chunks at the end.
  go i !acc = case i of
    0 -> acc
    _ -> go (i - 1) (ChunksCons b acc)

{- | Repeat the byte over and over. This builds a single byte array that
is at most 64KiB and shares that across every @ChunksCons@ cell.

An as example, creating a 2GiB chunks this way would use 64KiB for the
byte array, and there would be the additional overhead of the 2^15
@ChunksCons@ data constructors. On a 64-bit platform, @ChunksCons@
takes 40 bytes, so the total memory consumption would be
@2^16 + 40 * 2^15@, which is roughly 1.37MB. The same reasoning
shows that it takes about 83.95MB to represent a 128GiB chunks.

The size of the shared payload is an implementation detail. Do not
rely on this function producing 64KiB chunks. The implementation might
one day change to something smarter that minimizes the memory footprint
for very large chunks.
-}
replicateByte ::
  Word8 ->
  -- | Number of times to replicate the byte
  Int ->
  Chunks
replicateByte !w !n
  | n <= 0 = ChunksNil
  | n < 65536 = ChunksCons (Bytes.replicate n w) ChunksNil
  | otherwise = go (Bytes.replicateU 65536 w) n ChunksNil
 where
  go !shared !remaining !acc
    | remaining == 0 = acc
    | remaining < 65536 = ChunksCons (Bytes shared 0 remaining) acc
    | otherwise = go shared (remaining - 65536) (ChunksCons (Bytes shared 0 65536) acc)

-- | Are there any bytes in the chunked byte sequences?
null :: Chunks -> Bool
null = go
 where
  go ChunksNil = True
  go (ChunksCons (Bytes _ _ len) xs) = case len of
    0 -> go xs
    _ -> False

{- | Variant of 'concat' that ensure that the resulting byte
sequence is pinned memory.
-}
concatPinned :: Chunks -> Bytes
concatPinned x = case x of
  ChunksNil -> Bytes.emptyPinned
  ChunksCons b y -> case y of
    ChunksNil -> Bytes.pin b
    ChunksCons c z -> case concatPinnedFollowing2 b c z of
      (# len, r #) -> Bytes (ByteArray r) 0 (I# len)

-- | Concatenate chunks into a strict bytestring.
concatByteString :: Chunks -> ByteString
concatByteString c = Bytes.pinnedToByteString (concatPinned c)

-- | Concatenate chunks into a single contiguous byte sequence.
concat :: Chunks -> Bytes
concat x = case x of
  ChunksNil -> Bytes.empty
  ChunksCons b y -> case y of
    ChunksNil -> b
    ChunksCons c z -> case concatFollowing2 b c z of
      (# len, r #) -> Bytes (ByteArray r) 0 (I# len)

-- | Variant of 'concat' that returns an unsliced byte sequence.
concatU :: Chunks -> ByteArray
concatU x = case x of
  ChunksNil -> mempty
  ChunksCons b y -> case y of
    ChunksNil -> Bytes.toByteArray b
    ChunksCons c z -> case concatFollowing2 b c z of
      (# _, r #) -> ByteArray r

-- | Variant of 'concatPinned' that returns an unsliced pinned byte sequence.
concatPinnedU :: Chunks -> ByteArray
concatPinnedU x = case x of
  ChunksNil -> Bytes.emptyPinnedU
  ChunksCons b y -> case y of
    ChunksNil -> Bytes.toPinnedByteArray b
    ChunksCons c z -> case concatPinnedFollowing2 b c z of
      (# _, r #) -> ByteArray r

concatFollowing2 :: Bytes -> Bytes -> Chunks -> (# Int#, ByteArray# #)
concatFollowing2 = internalConcatFollowing2 PM.newByteArray

concatPinnedFollowing2 :: Bytes -> Bytes -> Chunks -> (# Int#, ByteArray# #)
concatPinnedFollowing2 = internalConcatFollowing2 PM.newPinnedByteArray

internalConcatFollowing2 ::
  (forall s. Int -> ST s (MutableByteArray s)) ->
  Bytes ->
  Bytes ->
  Chunks ->
  (# Int#, ByteArray# #)
{-# INLINE internalConcatFollowing2 #-}
internalConcatFollowing2
  allocate
  (Bytes {array = c, offset = coff, length = szc})
  (Bytes {array = d, offset = doff, length = szd})
  ds =
    let !(I# x, ByteArray y) = runIntByteArrayST $ do
          let !szboth = szc + szd
              !len = chunksLengthGo szboth ds
          dst <- allocate len
          PM.copyByteArray dst 0 c coff szc
          PM.copyByteArray dst szc d doff szd
          -- Note: len2 will always be the same as len.
          !len2 <- unsafeCopy dst szboth ds
          result <- PM.unsafeFreezeByteArray dst
          pure (len2, result)
     in (# x, y #)

-- | The total number of bytes in all the chunks.
length :: Chunks -> Int
length = chunksLengthGo 0

chunksLengthGo :: Int -> Chunks -> Int
chunksLengthGo !n ChunksNil = n
chunksLengthGo !n (ChunksCons (Bytes {B.length = len}) cs) =
  chunksLengthGo (n + len) cs

{- | Copy the contents of the chunks into a mutable array.
Precondition: The destination must have enough space to
house the contents. This is not checked.
-}
unsafeCopy ::
  -- | Destination
  MutableByteArray s ->
  -- | Destination offset
  Int ->
  -- | Source
  Chunks ->
  -- | Returns the next index into the destination after the payload
  ST s Int
{-# INLINE unsafeCopy #-}
unsafeCopy (MutableByteArray dst) (I# off) cs =
  ST
    ( \s0 -> case copy# dst off cs s0 of
        (# s1, nextOff #) -> (# s1, I# nextOff #)
    )

copy# :: MutableByteArray# s -> Int# -> Chunks -> State# s -> (# State# s, Int# #)
copy# _ off ChunksNil s0 = (# s0, off #)
copy# marr off (ChunksCons (Bytes {B.array, B.offset, B.length = len}) cs) s0 =
  case Exts.copyByteArray# (unBa array) (unI offset) marr off (unI len) s0 of
    s1 -> copy# marr (off +# unI len) cs s1

-- | Reverse chunks but not the bytes within each chunk.
reverse :: Chunks -> Chunks
reverse = reverseOnto ChunksNil

{- | Variant of 'reverse' that allows the caller to provide
an initial list of chunks that the reversed chunks will
be pushed onto.
-}
reverseOnto :: Chunks -> Chunks -> Chunks
reverseOnto !x ChunksNil = x
reverseOnto !x (ChunksCons y ys) =
  reverseOnto (ChunksCons y x) ys

unI :: Int -> Int#
{-# INLINE unI #-}
unI (I# i) = i

unBa :: ByteArray -> ByteArray#
{-# INLINE unBa #-}
unBa (ByteArray x) = x

-- | Read a handle's entire contents strictly into chunks.
hGetContents :: Handle -> IO Chunks
hGetContents !h = hGetContentsCommon ChunksNil h

-- | Read a handle's entire contents strictly into chunks.
hGetContentsHint :: Int -> Handle -> IO Chunks
hGetContentsHint !hint !h = do
  c <- IO.hGet h hint
  let !r = ChunksCons c ChunksNil
  if Bytes.length c == hint
    then pure r
    else hGetContentsCommon r h

hGetContentsCommon ::
  Chunks -> -- reversed chunks
  Handle ->
  IO Chunks
hGetContentsCommon !acc0 !h = go acc0
 where
  go !acc = do
    c <- IO.hGet h chunkSize
    let !r = ChunksCons c acc
    if Bytes.length c == chunkSize
      then go r
      else pure $! reverse r

{- | Read an entire file strictly into chunks. If reading from a
regular file, this makes an effort read the file into a single
chunk.
-}
readFile :: FilePath -> IO Chunks
readFile f = withBinaryFile f ReadMode $ \h -> do
  -- Implementation copied from bytestring.
  -- hFileSize fails if file is not regular file (like
  -- /dev/null). Catch exception and try reading anyway.
  filesz <- catch (hFileSize h) useZeroIfNotRegularFile
  let hint = (fromIntegral filesz `max` 255) + 1
  hGetContentsHint hint h
 where
  -- Our initial size is one bigger than the file size so that in the
  -- typical case we will read the whole file in one go and not have
  -- to allocate any more chunks. We'll still do the right thing if the
  -- file size is 0 or is changed before we do the read.

  useZeroIfNotRegularFile :: IOException -> IO Integer
  useZeroIfNotRegularFile _ = return 0

chunkSize :: Int
chunkSize = 16384 - 16

-- | Create a list of chunks with a single chunk.
fromBytes :: Bytes -> Chunks
fromBytes !b = ChunksCons b ChunksNil

-- | Variant of 'fromBytes' where the single chunk is unsliced.
fromByteArray :: ByteArray -> Chunks
fromByteArray !b = fromBytes (Bytes.fromByteArray b)

-- | Left fold over all bytes in the chunks, strict in the accumulator.
foldl' :: (a -> Word8 -> a) -> a -> Chunks -> a
{-# INLINE foldl' #-}
foldl' g = go
 where
  go !a ChunksNil = a
  go !a (ChunksCons c cs) = go (Bytes.foldl' g a c) cs

-- | Hash byte sequence with 32-bit variant of FNV-1a.
fnv1a32 :: Chunks -> Word32
fnv1a32 !b =
  foldl'
    ( \acc w -> (fromIntegral @Word8 @Word32 w `xor` acc) * 0x01000193
    )
    0x811c9dc5
    b

-- | Hash byte sequence with 64-bit variant of FNV-1a.
fnv1a64 :: Chunks -> Word64
fnv1a64 !b =
  foldl'
    ( \acc w -> (fromIntegral @Word8 @Word64 w `xor` acc) * 0x00000100000001B3
    )
    0xcbf29ce484222325
    b

{- | Outputs 'Chunks' to the specified 'Handle'. This is implemented
with 'IO.hPut'.
-}
hPut :: Handle -> Chunks -> IO ()
hPut h = go
 where
  go ChunksNil = pure ()
  go (ChunksCons c cs) = IO.hPut h c *> go cs

{- | Write 'Chunks' to a file, replacing the previous contents of
the file.
-}
writeFile :: FilePath -> Chunks -> IO ()
writeFile path cs = withBinaryFile path WriteMode (\h -> hPut h cs)

{- | Break chunks of bytes into contiguous pieces separated by the
byte argument. This is a good producer for list fusion. For this
function to perform well, each chunk should contain multiple separators.
Any piece that spans multiple chunks must be copied.
-}
split :: Word8 -> Chunks -> [Bytes]
{-# INLINE split #-}
split !w !cs0 =
  Exts.build
    ( \g x0 ->
        -- It is possible to optimize for the common case where a
        -- piece does not span multiple chunks. However, such an
        -- optimization would actually cause this to tail call in
        -- two places rather than one and may actually adversely
        -- affect performance. It hasn't been benchmarked.
        let go !cs = case splitOnto ChunksNil w cs of
              (hd, tl) ->
                let !x = concat (reverse hd)
                 in case tl of
                      ChunksNil -> x0
                      _ -> g x (go tl)
         in go cs0
    )

splitOnto :: Chunks -> Word8 -> Chunks -> (Chunks, Chunks)
{-# INLINE splitOnto #-}
splitOnto !acc0 !w !cs0 = go acc0 cs0
 where
  go !acc ChunksNil = (acc, ChunksNil)
  go !acc (ChunksCons b bs) = case Byte.split1 w b of
    Nothing -> go (ChunksCons b acc) bs
    Just (hd, tl) ->
      let !r1 = ChunksCons hd acc
          !r2 = ChunksCons tl bs
       in (r1, r2)