packages feed

byteslice 0.2.2.0 → 0.2.3.0

raw patch · 12 files changed

+554/−234 lines, 12 filesdep +tuplesdep +vectorPVP ok

version bump matches the API change (PVP)

Dependencies added: tuples, vector

API changes (from Hackage documentation)

+ Data.Bytes: fnv1a32 :: Bytes -> Word32
+ Data.Bytes: fnv1a64 :: Bytes -> Word64
+ Data.Bytes: fromCString# :: Addr# -> Bytes
+ Data.Bytes: readFile :: FilePath -> IO Bytes
+ Data.Bytes: splitStream :: forall m. Applicative m => Word8 -> Bytes -> Stream m Bytes
+ Data.Bytes.Chunks: fnv1a32 :: Chunks -> Word32
+ Data.Bytes.Chunks: fnv1a64 :: Chunks -> Word64
+ Data.Bytes.Chunks: foldl' :: (a -> Word8 -> a) -> a -> Chunks -> a
+ Data.Bytes.Chunks: hPut :: Handle -> Chunks -> IO ()
+ Data.Bytes.Chunks: null :: Chunks -> Bool
+ Data.Bytes.Chunks: readFile :: FilePath -> IO Chunks
+ Data.Bytes.Chunks: split :: Word8 -> Chunks -> [Bytes]
+ Data.Bytes.Chunks: writeFile :: FilePath -> Chunks -> IO ()
+ Data.Bytes.Types: type family Bytes# :: TYPE ( 'TupleRep '[ 'UnliftedRep, 'IntRep, 'IntRep])

Files

CHANGELOG.md view
@@ -1,5 +1,19 @@ # Revision history for byteslice +## 0.2.3.0 -- 2020-04-30++* Add `fnv1a32` and `fnv1a64`, implementations of the 32-bit and+  64-bit variants of the FNV-1a hash algorithm, to both `Data.Bytes`+  and `Data.Bytes.Chunks`.+* Add `Data.Bytes.Chunks.null`.+* Add `readFile` to both `Data.Bytes` and `Data.Bytes.Chunks`.+* Add `foldl'` to `Data.Bytes.Chunks`.+* Add `split` to `Data.Bytes.Chunks`.+* Add `splitStream` for splitting as a good stream-fusion producer.+* Add `hPut` and `writeFile` to `Data.Bytes.Chunks`.+* Add `fromCString#`.+* Add `Bytes#` newtype on GHC 8.10 and up.+ ## 0.2.2.0 -- 2020-02-27  * Add `split4`.
byteslice.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: byteslice-version: 0.2.2.0+version: 0.2.3.0 synopsis: Slicing managed and unmanaged memory description:   This library provides types that allow the user to talk about a slice of@@ -24,15 +24,26 @@     Data.Bytes.Mutable     Data.Bytes.Types   other-modules:+    Data.Bytes.Compat     Data.Bytes.Byte+    Data.Bytes.Pure+    Data.Bytes.IO+    UnliftedBytes   build-depends:     , base >=4.11.1 && <5     , primitive >=0.7 && <0.8     , primitive-unlifted >=0.1.2 && <0.2     , primitive-addr >=0.1 && <0.2     , run-st >=0.1.1 && <0.2+    , tuples >=0.1 && <0.2+    , vector >=0.12 && <0.13   hs-source-dirs: src   ghc-options: -Wall -O2+  if impl(ghc>=8.10)+    hs-source-dirs: src-unlifted-newtypes+  else+    hs-source-dirs: src-no-unlifted-newtypes+    ghc-options: -fno-warn-dodgy-imports -fno-warn-dodgy-exports   default-language: Haskell2010   include-dirs: include   includes: bs_custom.h
+ src-no-unlifted-newtypes/UnliftedBytes.hs view
@@ -0,0 +1,14 @@+{-# language MagicHash #-}+{-# language TypeFamilies #-}+{-# language TypeInType #-}+{-# language UndecidableInstances #-}++module UnliftedBytes+  ( Bytes#+  ) where++import GHC.TypeLits+import GHC.Exts (RuntimeRep(..),TYPE)++type family Bytes# :: TYPE ('TupleRep '[ 'UnliftedRep,'IntRep,'IntRep]) where+  Bytes# = TypeError ('Text "Bytes# not available before GHC 8.10")
+ src-unlifted-newtypes/UnliftedBytes.hs view
@@ -0,0 +1,15 @@+{-# language GADTSyntax #-}+{-# language KindSignatures #-}+{-# language MagicHash #-}+{-# language TypeInType #-}+{-# language UnboxedTuples #-}+{-# language UnliftedNewtypes #-}++module UnliftedBytes+  ( Bytes#(..)+  ) where++import GHC.Exts (ByteArray#,Int#,RuntimeRep(..),TYPE)++newtype Bytes# :: TYPE ('TupleRep '[ 'UnliftedRep,'IntRep,'IntRep]) where+  Bytes# :: (# ByteArray#, Int#, Int# #) -> Bytes#
src/Data/Bytes.hs view
@@ -9,11 +9,11 @@   ( -- * Types     Bytes     -- * Constants-  , empty-  , emptyPinned+  , Pure.empty+  , Pure.emptyPinned     -- * Properties   , null-  , length+  , Pure.length     -- * Decompose   , uncons   , unsnoc@@ -38,7 +38,7 @@   , dropWhileEnd     -- * Folds   , foldl-  , foldl'+  , Pure.foldl'   , foldr   , foldr'     -- * Folds with Indices@@ -51,10 +51,11 @@   , Byte.splitInit   , Byte.splitInitU   , Byte.splitNonEmpty-  , split1-  , split2-  , split3-  , split4+  , Byte.splitStream+  , Byte.split1+  , Byte.split2+  , Byte.split3+  , Byte.split4     -- * Combining   , intercalate     -- * Counting@@ -85,59 +86,62 @@   , equalsLatin8     -- ** C Strings   , equalsCString+    -- * Hashing+  , Pure.fnv1a32+  , Pure.fnv1a64     -- * Unsafe Slicing   , unsafeTake   , unsafeDrop   , unsafeIndex     -- * Copying-  , unsafeCopy+  , Pure.unsafeCopy     -- * Pointers-  , pin-  , contents+  , Pure.pin+  , Pure.contents   , touch     -- * Conversion-  , toByteArray-  , toByteArrayClone+  , Pure.toByteArray+  , Pure.toByteArrayClone   , fromAsciiString   , fromLatinString-  , fromByteArray+  , Pure.fromByteArray   , toLatinString+  , fromCString#     -- * I\/O with Handles-  , hGet-  , hPut+  , BIO.hGet+  , readFile+  , BIO.hPut   ) where -import Prelude hiding (length,takeWhile,dropWhile,null,foldl,foldr,elem,replicate,any,all)+import Prelude hiding (length,takeWhile,dropWhile,null,foldl,foldr,elem,replicate,any,all,readFile) -import Control.Monad.Primitive (PrimMonad,PrimState,primitive_,unsafeIOToPrim)+import Control.Monad.Primitive (PrimMonad,primitive_,unsafeIOToPrim) import Control.Monad.ST.Run (runByteArrayST)+import Data.Bytes.Compat (cstringLength#)+import Data.Bytes.Pure (length,fromByteArray) import Data.Bytes.Types (Bytes(Bytes,array,offset)) import Data.Char (ord)-import Data.Primitive (ByteArray(ByteArray),MutableByteArray)+import Data.Primitive (ByteArray(ByteArray)) import Foreign.C.String (CString) import Foreign.Ptr (Ptr,plusPtr,castPtr)-import GHC.Exts (Int(I#),Char(C#),word2Int#,chr#)-import GHC.Exts (Word#,Int#)-import GHC.IO (IO(IO))+import GHC.Exts (Int(I#),Char(C#),Ptr(Ptr),word2Int#,chr#)+import GHC.Exts (Addr#,Word#,Int#) import GHC.Word (Word8(W8#))-import System.IO (Handle)  import qualified Data.Bytes.Byte as Byte+import qualified Data.Bytes.Chunks as Chunks+import qualified Data.Bytes.IO as BIO+import qualified Data.Bytes.Pure as Pure import qualified Data.Foldable as F import qualified Data.List as List import qualified Data.Primitive as PM import qualified Data.Primitive.Ptr as PM import qualified GHC.Exts as Exts-import qualified System.IO as IO  -- | Is the byte sequence empty? null :: Bytes -> Bool null (Bytes _ _ len) = len == 0 --- | The length of a slice of bytes.-length :: Bytes -> Int-length (Bytes _ _ len) = len- -- | Extract the head and tail of the 'Bytes', returning 'Nothing' if -- it is empty. uncons :: Bytes -> Maybe (Word8, Bytes)@@ -280,98 +284,6 @@   then Bytes (array str) (offset str) (length str - length suf)   else str --- | Split a byte sequence on the first occurrence of the target--- byte. The target is removed from the result. For example:------ >>> split1 0xA [0x1,0x2,0xA,0xB]--- Just ([0x1,0x2],[0xB])-split1 :: Word8 -> Bytes -> Maybe (Bytes,Bytes)-{-# inline split1 #-}-split1 w b@(Bytes arr off len) = case elemIndexLoop# w b of-  (-1#) -> Nothing-  i# -> let i = I# i# in-    Just (Bytes arr off (i - off), Bytes arr (i + 1) (len - (1 + i - off)))---- | Split a byte sequence on the first and second occurrences--- of the target byte. The target is removed from the result.--- For example:------ >>> split2 0xA [0x1,0x2,0xA,0xB,0xA,0xA,0xA]--- Just ([0x1,0x2],[0xB],[0xA,0xA])-split2 :: Word8 -> Bytes -> Maybe (Bytes,Bytes,Bytes)-{-# inline split2 #-}-split2 w b@(Bytes arr off len) = case elemIndexLoop# w b of-  (-1#) -> Nothing-  i# -> let i = I# i# in-    case elemIndexLoop# w (Bytes arr (i + 1) (len - (1 + i - off))) of-      (-1#) -> Nothing-      j# -> let j = I# j# in Just-        ( Bytes arr off (i - off)-        , Bytes arr (i + 1) (j - (i + 1))-        , Bytes arr (j + 1) (len - (1 + j - off))-        )---- | Split a byte sequence on the first, second, and third occurrences--- of the target byte. The target is removed from the result.--- For example:------ >>> split3 0xA [0x1,0x2,0xA,0xB,0xA,0xA,0xA]--- Just ([0x1,0x2],[0xB],[],[0xA])-split3 :: Word8 -> Bytes -> Maybe (Bytes,Bytes,Bytes,Bytes)-{-# inline split3 #-}-split3 w b@(Bytes arr off len) = case elemIndexLoop# w b of-  (-1#) -> Nothing-  i# -> let i = I# i# in-    case elemIndexLoop# w (Bytes arr (i + 1) (len - (1 + i - off))) of-      (-1#) -> Nothing-      j# -> let j = I# j# in-        case elemIndexLoop# w (Bytes arr (j + 1) (len - (1 + j - off))) of-          (-1#) -> Nothing-          k# -> let k = I# k# in Just-            ( Bytes arr off (i - off)-            , Bytes arr (i + 1) (j - (i + 1))-            , Bytes arr (j + 1) (k - (j + 1))-            , Bytes arr (k + 1) (len - (1 + k - off))-            )---- | Split a byte sequence on the first, second, third, and fourth--- occurrences of the target byte. The target is removed from the result.--- For example:------ >>> split4 0xA [0x1,0x2,0xA,0xB,0xA,0xA,0xA]--- Just ([0x1,0x2],[0xB],[],[],[])-split4 :: Word8 -> Bytes -> Maybe (Bytes,Bytes,Bytes,Bytes,Bytes)-{-# inline split4 #-}-split4 w b@(Bytes arr off len) = case elemIndexLoop# w b of-  (-1#) -> Nothing-  i# -> let i = I# i# in-    case elemIndexLoop# w (Bytes arr (i + 1) (len - (1 + i - off))) of-      (-1#) -> Nothing-      j# -> let j = I# j# in-        case elemIndexLoop# w (Bytes arr (j + 1) (len - (1 + j - off))) of-          (-1#) -> Nothing-          k# -> let k = I# k# in-            case elemIndexLoop# w (Bytes arr (k + 1) (len - (1 + k - off))) of-              (-1#) -> Nothing-              m# -> let m = I# m# in Just-                ( Bytes arr off (i - off)-                , Bytes arr (i + 1) (j - (i + 1))-                , Bytes arr (j + 1) (k - (j + 1))-                , Bytes arr (k + 1) (m - (k + 1))-                , Bytes arr (m + 1) (len - (1 + m - off))-                )---- This returns the offset into the byte array. This is not an index--- that will mean anything to the end user, so it cannot be returned--- to them.-elemIndexLoop# :: Word8 -> Bytes -> Int#-elemIndexLoop# !w (Bytes arr off@(I# off# ) len) = case len of-  0 -> (-1#)-  _ -> if PM.indexByteArray arr off == w-    then off#-    else elemIndexLoop# w (Bytes arr (off + 1) (len - 1))-- -- | Is the byte a member of the byte sequence? elem :: Word8 -> Bytes -> Bool elem (W8# w) b = case elemLoop 0# w b of@@ -467,14 +379,6 @@     0 -> a0     _ -> f (PM.indexByteArray arr off) (go (off + 1) (len - 1)) --- | Left fold over bytes, strict in the accumulator.-foldl' :: (a -> Word8 -> a) -> a -> Bytes -> a-{-# inline foldl' #-}-foldl' f a0 (Bytes arr off0 len0) = go a0 off0 len0 where-  go !a !off !len = case len of-    0 -> a-    _ -> go (f a (PM.indexByteArray arr off)) (off + 1) (len - 1)- -- | Left fold over bytes, strict in the accumulator. The reduction function -- is applied to each element along with its index. ifoldl' :: (a -> Int -> Word8 -> a) -> a -> Bytes -> a@@ -494,23 +398,6 @@     (-1) -> a     _ -> go (f (PM.indexByteArray arr off) a) (off - 1) (ix - 1) --- | Convert the sliced 'Bytes' to an unsliced 'ByteArray'. This--- reuses the array backing the sliced 'Bytes' if the slicing metadata--- implies that all of the bytes are used. Otherwise, it makes a copy.-toByteArray :: Bytes -> ByteArray-toByteArray b@(Bytes arr off len)-  | off == 0, PM.sizeofByteArray arr == len = arr-  | otherwise = toByteArrayClone b---- | Variant of 'toByteArray' that unconditionally makes a copy of--- the array backing the sliced 'Bytes' even if the original array--- could be reused. Prefer 'toByteArray'.-toByteArrayClone :: Bytes -> ByteArray-toByteArrayClone (Bytes arr off len) = runByteArrayST $ do-  m <- PM.newByteArray len-  PM.copyByteArray m 0 arr off len-  PM.unsafeFreezeByteArray m- -- | Convert a 'String' consisting of only characters in the ASCII block -- to a byte sequence. Any character with a codepoint above @U+007F@ is -- replaced by @U+0000@.@@ -530,9 +417,17 @@ toLatinString :: Bytes -> String toLatinString = foldr (\(W8# w) xs -> C# (chr# (word2Int# w)) : xs) [] --- | Create a slice of 'Bytes' that spans the entire argument array.-fromByteArray :: ByteArray -> Bytes-fromByteArray b = Bytes b 0 (PM.sizeofByteArray b)+-- | Copy a primitive string literal into managed memory.+fromCString# :: Addr# -> Bytes+fromCString# a = Bytes+  ( runByteArrayST $ do+      dst@(PM.MutableByteArray dst# ) <- PM.newByteArray len+      PM.copyPtrToMutablePrimArray+        (PM.MutablePrimArray dst# ) 0 (Ptr a :: Ptr Word8) len+      PM.unsafeFreezeByteArray dst+  ) 0 len+  where+  len = I# (cstringLength# a)  compareByteArrays :: ByteArray -> Int -> ByteArray -> Int -> Int -> Ordering {-# INLINE compareByteArrays #-}@@ -647,37 +542,6 @@         True -> go (plusPtr ptr 1) (off + 1) (len - 1)         False -> Nothing --- | Copy the byte sequence into a mutable buffer. The buffer must have--- enough space to accomodate the byte sequence, but this this is not--- checked.-unsafeCopy :: PrimMonad m-  => MutableByteArray (PrimState m) -- ^ Destination-  -> Int -- ^ Destination Offset-  -> Bytes -- ^ Source-  -> m ()-{-# inline unsafeCopy #-}-unsafeCopy dst dstIx (Bytes src srcIx len) =-  PM.copyByteArray dst dstIx src srcIx len---- | Yields a pinned byte sequence whose contents are identical to those--- of the original byte sequence. If the @ByteArray@ backing the argument--- was already pinned, this simply aliases the argument and does not perform--- any copying.-pin :: Bytes -> Bytes-pin b@(Bytes arr _ len) = case PM.isByteArrayPinned arr of-  True -> b-  False -> Bytes-    ( runByteArrayST do-        dst <- PM.newPinnedByteArray len-        unsafeCopy dst 0 b-        PM.unsafeFreezeByteArray dst-    ) 0 len---- | Yields a pointer to the beginning of the byte sequence. It is only safe--- to call this on a 'Bytes' backed by a pinned @ByteArray@.-contents :: Bytes -> Ptr Word8-contents (Bytes arr off _) = plusPtr (PM.byteArrayContents arr) off- -- | Touch the byte array backing the byte sequence. This sometimes needed -- after calling 'contents' so that the @ByteArray@ does not get garbage -- collected.@@ -688,48 +552,9 @@ indexCharArray :: ByteArray -> Int -> Char indexCharArray (ByteArray arr) (I# off) = C# (Exts.indexCharArray# arr off) --- | The empty byte sequence.-empty :: Bytes-empty = Bytes mempty 0 0---- | The empty byte sequence.-emptyPinned :: Bytes-emptyPinned = Bytes-  ( runByteArrayST-    (PM.newPinnedByteArray 0 >>= PM.unsafeFreezeByteArray)-  ) 0 0---- | Read 'Bytes' directly from the specified 'Handle'. The resulting--- 'Bytes' are pinned. This is implemented with 'hGetBuf'.-hGet :: Handle -> Int -> IO Bytes-hGet h i = createPinnedAndTrim i (\p -> IO.hGetBuf h p i)---- | Outputs 'Bytes' to the specified 'Handle'. This is implemented--- with 'hPutBuf'.-hPut :: Handle -> Bytes -> IO ()-hPut h b0 = do-  let b1@(Bytes arr _ len) = pin b0-  IO.hPutBuf h (contents b1) len-  touchByteArrayIO arr---- Only used internally.-createPinnedAndTrim :: Int -> (Ptr Word8 -> IO Int) -> IO Bytes-{-# inline createPinnedAndTrim #-}-createPinnedAndTrim maxSz f = do-  arr@(PM.MutableByteArray arr#) <- PM.newPinnedByteArray maxSz-  sz <- f (PM.mutableByteArrayContents arr)-  touchMutableByteArrayIO arr-  PM.shrinkMutablePrimArray (PM.MutablePrimArray @Exts.RealWorld @Word8 arr#) sz-  r <- PM.unsafeFreezeByteArray arr-  pure (Bytes r 0 sz)--touchByteArrayIO :: ByteArray -> IO ()-touchByteArrayIO (ByteArray x) =-  IO (\s -> (# Exts.touch# x s, () #))--touchMutableByteArrayIO :: MutableByteArray s -> IO ()-touchMutableByteArrayIO (PM.MutableByteArray x) =-  IO (\s -> (# Exts.touch# x s, () #))+-- | Read an entire file strictly into a 'Bytes'.+readFile :: FilePath -> IO Bytes+readFile f = Chunks.concat <$> Chunks.readFile f  -- | /O(n)/ The intercalate function takes a separator 'Bytes' and a list of -- 'Bytes' and concatenates the list elements by interspersing the separator
src/Data/Bytes/Byte.hs view
@@ -15,8 +15,13 @@   , split   , splitU   , splitNonEmpty+  , splitStream   , splitInit   , splitInitU+  , split1+  , split2+  , split3+  , split4   ) where  import Prelude hiding (length)@@ -27,8 +32,10 @@ import Data.List.NonEmpty (NonEmpty((:|))) import Data.Primitive (PrimArray(..),MutablePrimArray(..),ByteArray(..)) import Data.Primitive.Unlifted.Array (UnliftedArray)+import Data.Tuple.Types (IntPair(IntPair))+import Data.Vector.Fusion.Stream.Monadic (Stream(Stream),Step(Yield,Done)) import Data.Word (Word8)-import GHC.Exts (ByteArray#,MutableByteArray#)+import GHC.Exts (ByteArray#,MutableByteArray#,Int#,Int(I#)) import GHC.IO (unsafeIOToST)  import qualified Data.Primitive as PM@@ -104,6 +111,24 @@   !lens = splitLengthsAlt w bs   !lensSz = PM.sizeofPrimArray lens +-- | Variant of 'split' that intended for use with stream fusion rather+-- than @build@-@foldr@ fusion.+splitStream :: forall m. Applicative m => Word8 -> Bytes -> Stream m Bytes+{-# inline [1] splitStream #-}+splitStream !w !bs@Bytes{array,offset=arrIx0} = Stream step (IntPair 0 arrIx0)+  where+  !lens = splitLengthsAlt w bs+  !lensSz = PM.sizeofPrimArray lens+  {-# inline [0] step #-}+  step :: IntPair -> m (Step IntPair Bytes)+  step (IntPair lenIx arrIx) = if lenIx < lensSz+    then do+      let !len = PM.indexPrimArray lens lenIx+          !element = Bytes array arrIx len+          !acc = IntPair (lenIx + 1) (arrIx + len + 1)+      pure (Yield element acc)+    else pure Done+ -- | Variant of 'split' that returns the result as a 'NonEmpty' -- instead of @[]@. This is also eligible for stream fusion. splitNonEmpty :: Word8 -> Bytes -> NonEmpty Bytes@@ -162,3 +187,95 @@  foreign import ccall unsafe "bs_custom.h count_ba" count_ba   :: ByteArray# -> Int -> Int -> Word8 -> Int++-- | Split a byte sequence on the first occurrence of the target+-- byte. The target is removed from the result. For example:+--+-- >>> split1 0xA [0x1,0x2,0xA,0xB]+-- Just ([0x1,0x2],[0xB])+split1 :: Word8 -> Bytes -> Maybe (Bytes,Bytes)+{-# inline split1 #-}+split1 w b@(Bytes arr off len) = case elemIndexLoop# w b of+  (-1#) -> Nothing+  i# -> let i = I# i# in+    Just (Bytes arr off (i - off), Bytes arr (i + 1) (len - (1 + i - off)))++-- | Split a byte sequence on the first and second occurrences+-- of the target byte. The target is removed from the result.+-- For example:+--+-- >>> split2 0xA [0x1,0x2,0xA,0xB,0xA,0xA,0xA]+-- Just ([0x1,0x2],[0xB],[0xA,0xA])+split2 :: Word8 -> Bytes -> Maybe (Bytes,Bytes,Bytes)+{-# inline split2 #-}+split2 w b@(Bytes arr off len) = case elemIndexLoop# w b of+  (-1#) -> Nothing+  i# -> let i = I# i# in+    case elemIndexLoop# w (Bytes arr (i + 1) (len - (1 + i - off))) of+      (-1#) -> Nothing+      j# -> let j = I# j# in Just+        ( Bytes arr off (i - off)+        , Bytes arr (i + 1) (j - (i + 1))+        , Bytes arr (j + 1) (len - (1 + j - off))+        )++-- | Split a byte sequence on the first, second, and third occurrences+-- of the target byte. The target is removed from the result.+-- For example:+--+-- >>> split3 0xA [0x1,0x2,0xA,0xB,0xA,0xA,0xA]+-- Just ([0x1,0x2],[0xB],[],[0xA])+split3 :: Word8 -> Bytes -> Maybe (Bytes,Bytes,Bytes,Bytes)+{-# inline split3 #-}+split3 w b@(Bytes arr off len) = case elemIndexLoop# w b of+  (-1#) -> Nothing+  i# -> let i = I# i# in+    case elemIndexLoop# w (Bytes arr (i + 1) (len - (1 + i - off))) of+      (-1#) -> Nothing+      j# -> let j = I# j# in+        case elemIndexLoop# w (Bytes arr (j + 1) (len - (1 + j - off))) of+          (-1#) -> Nothing+          k# -> let k = I# k# in Just+            ( Bytes arr off (i - off)+            , Bytes arr (i + 1) (j - (i + 1))+            , Bytes arr (j + 1) (k - (j + 1))+            , Bytes arr (k + 1) (len - (1 + k - off))+            )++-- | Split a byte sequence on the first, second, third, and fourth+-- occurrences of the target byte. The target is removed from the result.+-- For example:+--+-- >>> split4 0xA [0x1,0x2,0xA,0xB,0xA,0xA,0xA]+-- Just ([0x1,0x2],[0xB],[],[],[])+split4 :: Word8 -> Bytes -> Maybe (Bytes,Bytes,Bytes,Bytes,Bytes)+{-# inline split4 #-}+split4 w b@(Bytes arr off len) = case elemIndexLoop# w b of+  (-1#) -> Nothing+  i# -> let i = I# i# in+    case elemIndexLoop# w (Bytes arr (i + 1) (len - (1 + i - off))) of+      (-1#) -> Nothing+      j# -> let j = I# j# in+        case elemIndexLoop# w (Bytes arr (j + 1) (len - (1 + j - off))) of+          (-1#) -> Nothing+          k# -> let k = I# k# in+            case elemIndexLoop# w (Bytes arr (k + 1) (len - (1 + k - off))) of+              (-1#) -> Nothing+              m# -> let m = I# m# in Just+                ( Bytes arr off (i - off)+                , Bytes arr (i + 1) (j - (i + 1))+                , Bytes arr (j + 1) (k - (j + 1))+                , Bytes arr (k + 1) (m - (k + 1))+                , Bytes arr (m + 1) (len - (1 + m - off))+                )++-- This returns the offset into the byte array. This is not an index+-- that will mean anything to the end user, so it cannot be returned+-- to them.+elemIndexLoop# :: Word8 -> Bytes -> Int#+elemIndexLoop# !w (Bytes arr off@(I# off# ) len) = case len of+  0 -> (-1#)+  _ -> if PM.indexByteArray arr off == w+    then off#+    else elemIndexLoop# w (Bytes arr (off + 1) (len - 1))+
src/Data/Bytes/Chunks.hs view
@@ -1,11 +1,12 @@ {-# language BangPatterns #-} {-# language DerivingStrategies #-} {-# language DuplicateRecordFields #-}-{-# language TypeFamilies #-} {-# language MagicHash #-}-{-# language UnboxedTuples #-} {-# 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@@ -18,12 +19,20 @@     Chunks(..)     -- * Properties   , length+  , null     -- * Manipulate   , concat   , concatPinned   , concatU   , reverse   , reverseOnto+    -- * Folds+  , foldl'+    -- * Splitting+  , split+    -- * Hashing+  , fnv1a32+  , fnv1a64     -- * Create   , fromBytes   , fromByteArray@@ -31,22 +40,30 @@   , unsafeCopy     -- * I\/O with Handles   , hGetContents+  , readFile+  , hPut+  , writeFile   ) where -import Prelude hiding (length,concat,reverse)+import Prelude hiding (length,concat,reverse,readFile,writeFile,null) +import Control.Exception (IOException,catch) import Control.Monad.ST.Run (runIntByteArrayST)+import Data.Bits (xor) import Data.Bytes.Types (Bytes(Bytes))+import Data.Word (Word8,Word32,Word64) import Data.Primitive (ByteArray(..),MutableByteArray(..)) import GHC.Exts (ByteArray#,MutableByteArray#) import GHC.Exts (Int#,State#,Int(I#),(+#)) import GHC.ST (ST(..))-import System.IO (Handle)+import System.IO (Handle,hFileSize,IOMode(ReadMode,WriteMode),withBinaryFile)  import qualified GHC.Exts as Exts import qualified Data.Primitive as PM import qualified Data.Bytes.Types as B-import qualified Data.Bytes as Bytes+import qualified Data.Bytes.Pure as Bytes+import qualified Data.Bytes.Byte as Byte+import qualified Data.Bytes.IO as IO  -- | A cons-list of byte sequences. data Chunks@@ -69,6 +86,14 @@   -- it is tedious.   a == b = concat a == concat b +-- | 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@@ -174,17 +199,48 @@  -- | Read a handle's entire contents strictly into chunks. hGetContents :: Handle -> IO Chunks-hGetContents !h = do-  result <- go ChunksNil-  pure $! reverse result-  where+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 <- Bytes.hGet h chunkSize+    c <- IO.hGet h chunkSize     let !r = ChunksCons c acc     if Bytes.length c == chunkSize       then go r-      else pure 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+  -- 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.+  where+    useZeroIfNotRegularFile :: IOException -> IO Integer+    useZeroIfNotRegularFile _ = return 0+ chunkSize :: Int chunkSize = 16384 - 16 @@ -195,3 +251,66 @@ -- | 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 = foldl'+  (\acc w -> (fromIntegral @Word8 @Word32 w `xor` acc) * 0x01000193+  ) 0x811c9dc5++-- | Hash byte sequence with 64-bit variant of FNV-1a.+fnv1a64 :: Chunks -> Word64+fnv1a64 = foldl'+  (\acc w -> (fromIntegral @Word8 @Word64 w `xor` acc) * 0x00000100000001B3+  ) 0xcbf29ce484222325++-- | Outputs 'Chunks' to the specified 'Handle'. This is implemented+-- with 'hPutBuf'.+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)
+ src/Data/Bytes/Compat.hs view
@@ -0,0 +1,16 @@+{-# language MagicHash #-}+{-# language UnliftedFFITypes #-}++module Data.Bytes.Compat+  ( cstringLength#+  ) where++import GHC.Exts++-- GHC 8.12 comes with a known-key implementation of strlen that supports+-- constant folding. Here we define a shim for older GHCs.++foreign import ccall unsafe "strlen" c_strlen :: Addr# -> Int#++cstringLength# :: Addr# -> Int#+cstringLength# = c_strlen
+ src/Data/Bytes/IO.hs view
@@ -0,0 +1,55 @@+{-# language BangPatterns #-}+{-# language BlockArguments #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language TypeApplications #-}+{-# language UnboxedTuples #-}++module Data.Bytes.IO+  ( hGet+  , hPut+  ) where++import Data.Primitive (MutableByteArray,ByteArray(..))+import Data.Word (Word8)+import Data.Bytes.Types (Bytes(Bytes))+import Data.Bytes.Pure (pin,contents)+import System.IO (Handle)+import Foreign.Ptr (Ptr)+import GHC.IO (IO(IO))+import qualified System.IO as IO+import qualified GHC.Exts as Exts+import qualified Data.Primitive as PM++-- | Read 'Bytes' directly from the specified 'Handle'. The resulting+-- 'Bytes' are pinned. This is implemented with 'hGetBuf'.+hGet :: Handle -> Int -> IO Bytes+hGet h i = createPinnedAndTrim i (\p -> IO.hGetBuf h p i)++-- | Outputs 'Bytes' to the specified 'Handle'. This is implemented+-- with 'hPutBuf'.+hPut :: Handle -> Bytes -> IO ()+hPut h b0 = do+  let b1@(Bytes arr _ len) = pin b0+  IO.hPutBuf h (contents b1) len+  touchByteArrayIO arr++-- Only used internally.+createPinnedAndTrim :: Int -> (Ptr Word8 -> IO Int) -> IO Bytes+{-# inline createPinnedAndTrim #-}+createPinnedAndTrim maxSz f = do+  arr@(PM.MutableByteArray arr#) <- PM.newPinnedByteArray maxSz+  sz <- f (PM.mutableByteArrayContents arr)+  touchMutableByteArrayIO arr+  PM.shrinkMutablePrimArray (PM.MutablePrimArray @Exts.RealWorld @Word8 arr#) sz+  r <- PM.unsafeFreezeByteArray arr+  pure (Bytes r 0 sz)++touchMutableByteArrayIO :: MutableByteArray s -> IO ()+touchMutableByteArrayIO (PM.MutableByteArray x) =+  IO (\s -> (# Exts.touch# x s, () #))++touchByteArrayIO :: ByteArray -> IO ()+touchByteArrayIO (ByteArray x) =+  IO (\s -> (# Exts.touch# x s, () #))+
+ src/Data/Bytes/Pure.hs view
@@ -0,0 +1,120 @@+{-# language BangPatterns #-}+{-# language BlockArguments #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language TypeApplications #-}+{-# language UnboxedTuples #-}++module Data.Bytes.Pure+  ( empty+  , emptyPinned+  , pin+  , contents+  , unsafeCopy+  , toByteArray+  , toByteArrayClone+  , fromByteArray+  , length+  , foldl'+  , fnv1a32+  , fnv1a64+  ) where++import Prelude hiding (length)++import Control.Monad.Primitive (PrimState,PrimMonad)+import Control.Monad.ST.Run (runByteArrayST)+import Data.Bits (xor)+import Data.Bytes.Types (Bytes(Bytes))+import Data.Primitive (ByteArray,MutableByteArray)+import Data.Word (Word64,Word32,Word8)+import Foreign.Ptr (Ptr,plusPtr)++import qualified Data.Primitive as PM++-- | The empty byte sequence.+empty :: Bytes+empty = Bytes mempty 0 0++-- | The empty byte sequence.+emptyPinned :: Bytes+emptyPinned = Bytes+  ( runByteArrayST+    (PM.newPinnedByteArray 0 >>= PM.unsafeFreezeByteArray)+  ) 0 0++-- | Yields a pinned byte sequence whose contents are identical to those+-- of the original byte sequence. If the @ByteArray@ backing the argument+-- was already pinned, this simply aliases the argument and does not perform+-- any copying.+pin :: Bytes -> Bytes+pin b@(Bytes arr _ len) = case PM.isByteArrayPinned arr of+  True -> b+  False -> Bytes+    ( runByteArrayST do+        dst <- PM.newPinnedByteArray len+        unsafeCopy dst 0 b+        PM.unsafeFreezeByteArray dst+    ) 0 len++-- | Convert the sliced 'Bytes' to an unsliced 'ByteArray'. This+-- reuses the array backing the sliced 'Bytes' if the slicing metadata+-- implies that all of the bytes are used. Otherwise, it makes a copy.+toByteArray :: Bytes -> ByteArray+toByteArray b@(Bytes arr off len)+  | off == 0, PM.sizeofByteArray arr == len = arr+  | otherwise = toByteArrayClone b++-- | Variant of 'toByteArray' that unconditionally makes a copy of+-- the array backing the sliced 'Bytes' even if the original array+-- could be reused. Prefer 'toByteArray'.+toByteArrayClone :: Bytes -> ByteArray+toByteArrayClone (Bytes arr off len) = runByteArrayST $ do+  m <- PM.newByteArray len+  PM.copyByteArray m 0 arr off len+  PM.unsafeFreezeByteArray m++-- | Copy the byte sequence into a mutable buffer. The buffer must have+-- enough space to accomodate the byte sequence, but this this is not+-- checked.+unsafeCopy :: PrimMonad m+  => MutableByteArray (PrimState m) -- ^ Destination+  -> Int -- ^ Destination Offset+  -> Bytes -- ^ Source+  -> m ()+{-# inline unsafeCopy #-}+unsafeCopy dst dstIx (Bytes src srcIx len) =+  PM.copyByteArray dst dstIx src srcIx len++-- | Create a slice of 'Bytes' that spans the entire argument array.+fromByteArray :: ByteArray -> Bytes+fromByteArray b = Bytes b 0 (PM.sizeofByteArray b)++-- | The length of a slice of bytes.+length :: Bytes -> Int+length (Bytes _ _ len) = len++-- | Hash byte sequence with 32-bit variant of FNV-1a.+fnv1a32 :: Bytes -> Word32+fnv1a32 = foldl'+  (\acc w -> (fromIntegral @Word8 @Word32 w `xor` acc) * 0x01000193+  ) 0x811c9dc5++-- | Hash byte sequence with 64-bit variant of FNV-1a.+fnv1a64 :: Bytes -> Word64+fnv1a64 = foldl'+  (\acc w -> (fromIntegral @Word8 @Word64 w `xor` acc) * 0x00000100000001B3+  ) 0xcbf29ce484222325++-- | Left fold over bytes, strict in the accumulator.+foldl' :: (a -> Word8 -> a) -> a -> Bytes -> a+{-# inline foldl' #-}+foldl' f a0 (Bytes arr off0 len0) = go a0 off0 len0 where+  go !a !off !len = case len of+    0 -> a+    _ -> go (f a (PM.indexByteArray arr off)) (off + 1) (len - 1)++-- | Yields a pointer to the beginning of the byte sequence. It is only safe+-- to call this on a 'Bytes' backed by a pinned @ByteArray@.+contents :: Bytes -> Ptr Word8+contents (Bytes arr off _) = plusPtr (PM.byteArrayContents arr) off
src/Data/Bytes/Types.hs view
@@ -5,6 +5,7 @@  module Data.Bytes.Types   ( Bytes(..)+  , Bytes#(..)   , MutableBytes(..)   , UnmanagedBytes(..)   ) where@@ -19,6 +20,7 @@ import GHC.Base (unsafeChr) import GHC.Exts (Int(I#),unsafeCoerce#,sameMutableByteArray#) import GHC.Exts (isTrue#,compareByteArrays#,IsList(..))+import UnliftedBytes (Bytes#(..))  import qualified Data.List as L import qualified Data.Foldable as F
test/Main.hs view
@@ -174,6 +174,18 @@       case Bytes.split4 0xEF (slicedPack (ws ++ [0xEF] ++ xs ++ [0xEF] ++ ys ++ [0xEF] ++ zs ++ [0xEF] ++ ps)) of         Just r -> r === (slicedPack ws, slicedPack xs, slicedPack ys, slicedPack zs, slicedPack ps)         Nothing -> property False+  , testGroup "FNV-1a"+    [ testGroup "32-bit"+      [ testCase "empty" (Bytes.fnv1a32 Bytes.empty @=? 0x811c9dc5)+      , testCase "a" (Bytes.fnv1a32 (bytes "a") @=? 0xe40c292c)+      , testCase "foobar" (Bytes.fnv1a32 (bytes "foobar") @=? 0xbf9cf968)+      ]+    , testGroup "64-bit"+      [ testCase "empty" (Bytes.fnv1a64 Bytes.empty @=? 0xcbf29ce484222325)+      , testCase "a" (Bytes.fnv1a64 (bytes "a") @=? 0xaf63dc4c8601ec8c)+      , testCase "foobar" (Bytes.fnv1a64 (bytes "foobar") @=? 0x85944171f73967e8)+      ]+    ]   , testGroup "Chunks"     [ lawsToTest (QCC.eqLaws (Proxy :: Proxy Chunks))     , lawsToTest (QCC.semigroupLaws (Proxy :: Proxy Chunks))