diff --git a/Changelog.md b/Changelog.md
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,30 +1,35 @@
-[0.11.5.4] — January 2025
+[0.12.0.0] — July 2023
 
+* __Breaking Changes__:
+  * [`readInt` returns `Nothing`, if the sequence of digits cannot be represented by an `Int`, instead of overflowing silently](https://github.com/haskell/bytestring/pull/309)
+  * [Remove `zipWith` rewrite rule](https://github.com/haskell/bytestring/pull/387)
+  * [`ShortByteString` is now a wrapper around `Data.Array.Byte.ByteArray` instead of `ByteArray#` directly](https://github.com/haskell/bytestring/pull/410)
+    * As a compatibility measure, `SBS` remains available as a pattern synonym.
+    * The compatibility package `data-array-byte` is used when `base` does not provide `Data.Array.Byte`.
+  * [`fromListN` from `instance IsList ShortByteString` now throws an exception if the first argument does not match the length of the second](https://github.com/haskell/bytestring/pull/410)
+    * Previously, it would ignore the first argument entirely.
 * Bug fixes:
-  * [`Builder`: avoid unsound buffer reuse, introduced in `bytestring-0.11.5.0`](https://github.com/haskell/bytestring/pull/691)
+  * Size-related calculations are more resistant to `Int` overflow in the following places:
+    * [`Data.ByteString.intercalate`](https://github.com/haskell/bytestring/pull/468)
+    * [`stimes @StrictByteString`](https://github.com/haskell/bytestring/pull/443)
+    * [`Data.ByteString.Short.concat`](https://github.com/haskell/bytestring/pull/443)
+    * [`Data.ByteString.Short.append`](https://github.com/haskell/bytestring/pull/443)
+    * [`Data.ByteString.Short.snoc`](https://github.com/haskell/bytestring/pull/599)
+    * [`Data.ByteString.Short.cons`](https://github.com/haskell/bytestring/pull/599)
+* API additions:
+  * [New sized and/or unsigned variants of `readInt` and `readInteger`](https://github.com/haskell/bytestring/pull/438)
+  * [`Data.ByteString.Internal` now provides `SizeOverflowException`, `overflowError`, and `checkedMultiply`](https://github.com/haskell/bytestring/pull/443)
+* Deprecations:
+  * `Data.ByteString.getLine`: prefer `Data.ByteString.Char8.getLine`
+  * `Data.ByteString.hGetLine`: prefer `Data.ByteString.Char8.hGetLine`
+<!--
+* Performance improvements:
+* Miscellaneous:
+* Internal stuff:
+-->
 
-[0.11.5.4]: https://github.com/haskell/bytestring/compare/0.11.5.3...0.11.5.4
 
-[0.11.5.3] — October 2023
-
-* Bug fixes:
-  * [Fix a bug in `isValidUtf8`](https://github.com/haskell/bytestring/pull/621)
-
-[0.11.5.3]: https://github.com/haskell/bytestring/compare/0.11.5.2...0.11.5.3
-
-[0.11.5.2] — August 2023
-
-* Bug fixes:
-  * [Fix `clockid_t`-related build failures on some platforms](https://github.com/haskell/bytestring/pull/607)
-
-[0.11.5.2]: https://github.com/haskell/bytestring/compare/0.11.5.1...0.11.5.2
-
-[0.11.5.1] — August 2023
-
-* Bug fixes:
-  * [Work around a GHC runtime linker issue on i386/PowerPC](https://github.com/haskell/bytestring/pull/604)
-
-[0.11.5.1]: https://github.com/haskell/bytestring/compare/0.11.5.0...0.11.5.1
+[0.12.0.0]: https://github.com/haskell/bytestring/compare/0.11.5.0...0.12.0.0
 
 [0.11.5.0] — July 2023
 
diff --git a/Data/ByteString.hs b/Data/ByteString.hs
--- a/Data/ByteString.hs
+++ b/Data/ByteString.hs
@@ -380,16 +380,16 @@
 -- | /O(n)/ 'cons' is analogous to (:) for lists, but of different
 -- complexity, as it requires making a copy.
 cons :: Word8 -> ByteString -> ByteString
-cons c (BS x l) = unsafeCreateFp (l+1) $ \p -> do
+cons c (BS x len) = unsafeCreateFp (checkedAdd "cons" len 1) $ \p -> do
         pokeFp p c
-        memcpyFp (p `plusForeignPtr` 1) x l
+        memcpyFp (p `plusForeignPtr` 1) x len
 {-# INLINE cons #-}
 
 -- | /O(n)/ Append a byte to the end of a 'ByteString'
 snoc :: ByteString -> Word8 -> ByteString
-snoc (BS x l) c = unsafeCreateFp (l+1) $ \p -> do
-        memcpyFp p x l
-        pokeFp (p `plusForeignPtr` l) c
+snoc (BS x len) c = unsafeCreateFp (checkedAdd "snoc" len 1) $ \p -> do
+        memcpyFp p x len
+        pokeFp (p `plusForeignPtr` len) c
 {-# INLINE snoc #-}
 
 -- | /O(1)/ Extract the first element of a ByteString, which must be non-empty.
@@ -773,7 +773,7 @@
     -- ^ input of length n
     -> ByteString
     -- ^ output of length n+1
-scanl f v = \(BS a len) -> unsafeCreateFp (len+1) $ \q -> do
+scanl f v = \(BS a len) -> unsafeCreateFp (checkedAdd "scanl" len 1) $ \q -> do
          -- see fold inlining
         pokeFp q v
         let
@@ -817,7 +817,7 @@
     -- ^ input of length n
     -> ByteString
     -- ^ output of length n+1
-scanr f v = \(BS a len) -> unsafeCreateFp (len+1) $ \b -> do
+scanr f v = \(BS a len) -> unsafeCreateFp (checkedAdd "scanr" len 1) $ \b -> do
          -- see fold inlining
         pokeFpByteOff b len v
         let
@@ -1228,8 +1228,9 @@
             go (destPtr' `plusForeignPtr` chunkLen) chunks
       go (dstPtr0 `plusForeignPtr` hLen) t
   where
-  totalLen = List.foldl' (\acc (BS _ chunkLen) -> acc + chunkLen + sepLen) hLen t
-{-# INLINE intercalate #-}
+  totalLen = List.foldl' (\acc chunk -> acc +! sepLen +! length chunk) hLen t
+  (+!) = checkedAdd "intercalate"
+{-# INLINABLE intercalate #-}
 
 -- ---------------------------------------------------------------------
 -- Indexing ByteStrings
@@ -1680,11 +1681,6 @@
     len = min l m
 {-# INLINE packZipWith #-}
 
-{-# RULES
-"ByteString specialise zipWith" forall (f :: Word8 -> Word8 -> Word8) p q .
-    zipWith f p q = unpack (packZipWith f p q)
-  #-}
-
 -- | /O(n)/ 'unzip' transforms a list of pairs of bytes into a pair of
 -- ByteStrings. Note that this performs two 'pack' operations.
 unzip :: [(Word8,Word8)] -> (ByteString,ByteString)
@@ -1830,8 +1826,11 @@
 getLine :: IO ByteString
 getLine = hGetLine stdin
 
--- | Read a line from a handle
+{-# DEPRECATED getLine
+     "Deprecated since @bytestring-0.12@. Use 'Data.ByteString.Char8.getLine' instead. (Functions that rely on ASCII encodings belong in \"Data.ByteString.Char8\")"
+  #-}
 
+-- | Read a line from a handle
 hGetLine :: Handle -> IO ByteString
 hGetLine h =
   wantReadableHandle_ "Data.ByteString.hGetLine" h $
@@ -1877,6 +1876,10 @@
             if c == fromIntegral (ord '\n')
                 then return r -- NB. not r+1: don't include the '\n'
                 else findEOL (r+1) w raw
+
+{-# DEPRECATED hGetLine
+     "Deprecated since @bytestring-0.12@. Use 'Data.ByteString.Char8.hGetLine' instead. (Functions that rely on ASCII encodings belong in \"Data.ByteString.Char8\")"
+  #-}
 
 mkPS :: RawBuffer Word8 -> Int -> Int -> IO ByteString
 mkPS buf start end =
diff --git a/Data/ByteString/Builder/Internal.hs b/Data/ByteString/Builder/Internal.hs
--- a/Data/ByteString/Builder/Internal.hs
+++ b/Data/ByteString/Builder/Internal.hs
@@ -1096,7 +1096,7 @@
                 -- Checking for empty case avoids allocating 'n-1' empty
                 -- buffers for 'n' insertChunkH right after each other.
                 if isEmpty
-                  then fill nextStep buf
+                  then fill nextStep (Buffer fpbuf (BufferRange pbuf pe))
                   else do buf' <- nextBuffer (Just (buf, bufSize))
                           fill nextStep buf'
 
@@ -1108,9 +1108,9 @@
           | trim chunkSize size = do
               bs <- S.createFp chunkSize $ \fpbuf' ->
                         S.memcpyFp fpbuf' fpbuf chunkSize
-              -- It is not safe to re-use the old buffer (see #690),
-              -- so we allocate a new buffer after trimming.
-              return $ Yield1 bs (mkCIOS False)
+              -- Instead of allocating a new buffer after trimming,
+              -- we re-use the old buffer and consider it empty.
+              return $ Yield1 bs (mkCIOS True)
           | otherwise            =
               return $ Yield1 (S.BS fpbuf chunkSize) (mkCIOS False)
           where
diff --git a/Data/ByteString/Char8.hs b/Data/ByteString/Char8.hs
--- a/Data/ByteString/Char8.hs
+++ b/Data/ByteString/Char8.hs
@@ -2,6 +2,7 @@
 {-# LANGUAGE MagicHash #-}
 {-# OPTIONS_HADDOCK prune #-}
 {-# LANGUAGE Trustworthy #-}
+{-# OPTIONS_GHC -Wno-deprecations #-}
 
 -- |
 -- Module      : Data.ByteString.Char8
@@ -187,7 +188,19 @@
 
         -- * Reading from ByteStrings
         readInt,
+        readInt64,
+        readInt32,
+        readInt16,
+        readInt8,
+
+        readWord,
+        readWord64,
+        readWord32,
+        readWord16,
+        readWord8,
+
         readInteger,
+        readNatural,
 
         -- * Low level CString conversions
 
@@ -257,15 +270,17 @@
                        ,isInfixOf,stripPrefix,stripSuffix
                        ,breakSubstring,copy,group
 
-                       ,getLine, getContents, putStr, interact
+                       ,getContents, putStr, interact
                        ,readFile, writeFile, appendFile
                        ,hGetContents, hGet, hGetSome, hPut, hPutStr
-                       ,hGetLine, hGetNonBlocking, hPutNonBlocking
+                       ,hGetNonBlocking, hPutNonBlocking
                        ,packCString,packCStringLen
                        ,useAsCString,useAsCStringLen
                        )
 
 import Data.ByteString.Internal.Type
+import Data.ByteString.ReadInt
+import Data.ByteString.ReadNat
 
 import Data.Char    ( isSpace )
 -- See bytestring #70
@@ -845,14 +860,6 @@
 unzip ls = (pack (P.map fst ls), pack (P.map snd ls))
 {-# INLINE unzip #-}
 
--- | A variety of 'head' for non-empty ByteStrings. 'unsafeHead' omits
--- the check for the empty case, which is good for performance, but
--- there is an obligation on the programmer to provide a proof that the
--- ByteString is non-empty.
-unsafeHead :: ByteString -> Char
-unsafeHead  = w2c . B.unsafeHead
-{-# INLINE unsafeHead #-}
-
 -- ---------------------------------------------------------------------
 -- Things that depend on the encoding
 
@@ -988,84 +995,16 @@
 unwords = intercalate (singleton ' ')
 {-# INLINE unwords #-}
 
--- ---------------------------------------------------------------------
--- Reading from ByteStrings
-
--- | readInt reads an Int from the beginning of the ByteString.  If there is no
--- integer at the beginning of the string, it returns Nothing, otherwise
--- it just returns the int read, and the rest of the string.
---
--- Note: This function will overflow the Int for large integers.
-readInt :: ByteString -> Maybe (Int, ByteString)
-readInt as
-    | null as   = Nothing
-    | otherwise =
-        case unsafeHead as of
-            '-' -> loop True  0 0 (B.unsafeTail as)
-            '+' -> loop False 0 0 (B.unsafeTail as)
-            _   -> loop False 0 0 as
-
-    where loop :: Bool -> Int -> Int -> ByteString -> Maybe (Int, ByteString)
-          loop neg !i !n !ps
-              | null ps   = end neg i n ps
-              | otherwise =
-                  case B.unsafeHead ps of
-                    w | w >= 0x30
-                     && w <= 0x39 -> loop neg (i+1)
-                                          (n * 10 + (fromIntegral w - 0x30))
-                                          (B.unsafeTail ps)
-                      | otherwise -> end neg i n ps
-
-          end _    0 _ _  = Nothing
-          end True _ n ps = Just (negate n, ps)
-          end _    _ n ps = Just (n, ps)
-
--- | readInteger reads an Integer from the beginning of the ByteString.  If
--- there is no integer at the beginning of the string, it returns Nothing,
--- otherwise it just returns the int read, and the rest of the string.
-readInteger :: ByteString -> Maybe (Integer, ByteString)
-readInteger as
-    | null as   = Nothing
-    | otherwise =
-        case unsafeHead as of
-            '-' -> first (B.unsafeTail as) >>= \(n, bs) -> return (-n, bs)
-            '+' -> first (B.unsafeTail as)
-            _   -> first as
-
-    where first ps | null ps   = Nothing
-                   | otherwise =
-                       case B.unsafeHead ps of
-                        w | w >= 0x30 && w <= 0x39 -> Just $
-                            loop 1 (fromIntegral w - 0x30) [] (B.unsafeTail ps)
-                          | otherwise              -> Nothing
-
-          loop :: Int -> Int -> [Integer]
-               -> ByteString -> (Integer, ByteString)
-          loop !d !acc ns !ps
-              | null ps   = combine d acc ns empty
-              | otherwise =
-                  case B.unsafeHead ps of
-                   w | w >= 0x30 && w <= 0x39 ->
-                       if d == 9 then loop 1 (fromIntegral w - 0x30)
-                                           (toInteger acc : ns)
-                                           (B.unsafeTail ps)
-                                 else loop (d+1)
-                                           (10*acc + (fromIntegral w - 0x30))
-                                           ns (B.unsafeTail ps)
-                     | otherwise -> combine d acc ns ps
-
-          combine _ acc [] ps = (toInteger acc, ps)
-          combine d acc ns ps =
-              (10^d * combine1 1000000000 ns + toInteger acc, ps)
-
-          combine1 _ [n] = n
-          combine1 b ns  = combine1 (b*b) $ combine2 b ns
-
-          combine2 b (n:m:ns) = let !t = m*b + n in t : combine2 b ns
-          combine2 _ ns       = ns
-
 ------------------------------------------------------------------------
 -- For non-binary text processing:
+
+-- | Read a line from stdin.
+getLine :: IO ByteString
+getLine = B.getLine
+
+-- | Read a line from a handle
+hGetLine :: Handle -> IO ByteString
+hGetLine = B.hGetLine
 
 -- | Write a ByteString to a handle, appending a newline byte.
 --
diff --git a/Data/ByteString/Internal.hs b/Data/ByteString/Internal.hs
--- a/Data/ByteString/Internal.hs
+++ b/Data/ByteString/Internal.hs
@@ -60,7 +60,10 @@
         -- * Utilities
         nullForeignPtr,
         deferForeignPtrAvailability,
+        SizeOverflowException,
+        overflowError,
         checkedAdd,
+        checkedMultiply,
 
         -- * Standard C Functions
         c_strlen,
diff --git a/Data/ByteString/Internal/Type.hs b/Data/ByteString/Internal/Type.hs
--- a/Data/ByteString/Internal/Type.hs
+++ b/Data/ByteString/Internal/Type.hs
@@ -78,7 +78,10 @@
         memcpyFp,
         deferForeignPtrAvailability,
         unsafeDupablePerformIO,
+        SizeOverflowException,
+        overflowError,
         checkedAdd,
+        checkedMultiply,
 
         -- * Standard C Functions
         c_strlen,
@@ -118,18 +121,17 @@
 import Foreign.C.String         (CString)
 import Foreign.Marshal.Utils
 
-#if MIN_VERSION_base(4,13,0)
-import Data.Semigroup           (Semigroup (sconcat, stimes))
-#else
-import Data.Semigroup           (Semigroup ((<>), sconcat, stimes))
+#if !MIN_VERSION_base(4,13,0)
+import Data.Semigroup           (Semigroup ((<>)))
 #endif
+import Data.Semigroup           (Semigroup (sconcat, stimes))
 import Data.List.NonEmpty       (NonEmpty ((:|)))
 
 import Control.DeepSeq          (NFData(rnf))
 
 import Data.String              (IsString(..))
 
-import Control.Exception        (assert)
+import Control.Exception        (assert, throw, Exception)
 
 import Data.Bits                ((.&.))
 import Data.Char                (ord)
@@ -142,6 +144,19 @@
 import GHC.CString              (unpackCString#)
 import GHC.Magic                (runRW#, lazy)
 
+#define TIMES_INT_2_AVAILABLE MIN_VERSION_ghc_prim(0,7,0)
+#if TIMES_INT_2_AVAILABLE
+import GHC.Prim                (timesInt2#)
+#else
+import GHC.Prim                ( timesWord2#
+                               , or#
+                               , uncheckedShiftRL#
+                               , int2Word#
+                               , word2Int#
+                               )
+import Data.Bits               (finiteBitSize)
+#endif
+
 import GHC.IO                   (IO(IO))
 import GHC.ForeignPtr           (ForeignPtr(ForeignPtr)
 #if __GLASGOW_HASKELL__ < 900
@@ -308,7 +323,8 @@
 instance Semigroup ByteString where
     (<>)    = append
     sconcat (b:|bs) = concat (b:bs)
-    stimes  = times
+    {-# INLINE stimes #-}
+    stimes  = stimesPolymorphic
 
 instance Monoid ByteString where
     mempty  = empty
@@ -637,30 +653,30 @@
 
 -- | Create ByteString of size @l@ and use action @f@ to fill its contents.
 createFp :: Int -> (ForeignPtr Word8 -> IO ()) -> IO ByteString
-createFp l action = do
-    fp <- mallocByteString l
+createFp len action = assert (len >= 0) $ do
+    fp <- mallocByteString len
     action fp
-    mkDeferredByteString fp l
+    mkDeferredByteString fp len
 {-# INLINE createFp #-}
 
 -- | Given a maximum size @l@ and an action @f@ that fills the 'ByteString'
 -- starting at the given 'Ptr' and returns the actual utilized length,
 -- @`createFpUptoN'` l f@ returns the filled 'ByteString'.
 createFpUptoN :: Int -> (ForeignPtr Word8 -> IO Int) -> IO ByteString
-createFpUptoN l action = do
-    fp <- mallocByteString l
-    l' <- action fp
-    assert (l' <= l) $ mkDeferredByteString fp l'
+createFpUptoN maxLen action = assert (maxLen >= 0) $ do
+    fp <- mallocByteString maxLen
+    len <- action fp
+    assert (0 <= len && len <= maxLen) $ mkDeferredByteString fp len
 {-# INLINE createFpUptoN #-}
 
 -- | Like 'createFpUptoN', but also returns an additional value created by the
 -- action.
 createFpUptoN' :: Int -> (ForeignPtr Word8 -> IO (Int, a)) -> IO (ByteString, a)
-createFpUptoN' l action = do
-    fp <- mallocByteString l
-    (l', res) <- action fp
-    bs <- mkDeferredByteString fp l'
-    assert (l' <= l) $ pure (bs, res)
+createFpUptoN' maxLen action = assert (maxLen >= 0) $ do
+    fp <- mallocByteString maxLen
+    (len, res) <- action fp
+    bs <- mkDeferredByteString fp len
+    assert (0 <= len && len <= maxLen) $ pure (bs, res)
 {-# INLINE createFpUptoN' #-}
 
 -- | Given the maximum size needed and a function to make the contents
@@ -672,22 +688,26 @@
 -- ByteString functions, using Haskell or C functions to fill the space.
 --
 createFpAndTrim :: Int -> (ForeignPtr Word8 -> IO Int) -> IO ByteString
-createFpAndTrim l action = do
-    fp <- mallocByteString l
-    l' <- action fp
-    if assert (0 <= l' && l' <= l) $ l' >= l
-        then mkDeferredByteString fp l
-        else createFp l' $ \dest -> memcpyFp dest fp l'
+createFpAndTrim maxLen action = assert (maxLen >= 0) $ do
+    fp <- mallocByteString maxLen
+    len <- action fp
+    if assert (0 <= len && len <= maxLen) $ len >= maxLen
+        then mkDeferredByteString fp maxLen
+        else createFp len $ \dest -> memcpyFp dest fp len
 {-# INLINE createFpAndTrim #-}
 
 createFpAndTrim' :: Int -> (ForeignPtr Word8 -> IO (Int, Int, a)) -> IO (ByteString, a)
-createFpAndTrim' l action = do
-    fp <- mallocByteString l
-    (off, l', res) <- action fp
-    bs <- if assert (0 <= l' && l' <= l) $ l' >= l
-        then mkDeferredByteString fp l -- entire buffer used => offset is zero
-        else createFp l' $ \dest ->
-               memcpyFp dest (fp `plusForeignPtr` off) l'
+createFpAndTrim' maxLen action = assert (maxLen >= 0) $ do
+    fp <- mallocByteString maxLen
+    (off, len, res) <- action fp
+    assert (
+      0 <= len && len <= maxLen && -- length OK
+      (len == 0 || (0 <= off && off <= maxLen - len)) -- offset OK
+      ) $ pure ()
+    bs <- if len >= maxLen
+        then mkDeferredByteString fp maxLen -- entire buffer used => offset is zero
+        else createFp len $ \dest ->
+               memcpyFp dest (fp `plusForeignPtr` off) len
     return (bs, res)
 {-# INLINE createFpAndTrim' #-}
 
@@ -791,7 +811,7 @@
 append (BS _   0)    b                  = b
 append a             (BS _   0)    = a
 append (BS fp1 len1) (BS fp2 len2) =
-    unsafeCreateFp (len1+len2) $ \destptr1 -> do
+    unsafeCreateFp (checkedAdd "append" len1 len2) $ \destptr1 -> do
       let destptr2 = destptr1 `plusForeignPtr` len1
       memcpyFp destptr1 fp1 len1
       memcpyFp destptr2 fp2 len2
@@ -847,37 +867,57 @@
    concat [x] = x
  #-}
 
--- | /O(log n)/ Repeats the given ByteString n times.
-times :: Integral a => a -> ByteString -> ByteString
-times n (BS fp len)
-  | n < 0 = error "stimes: non-negative multiplier expected"
+-- | Repeats the given ByteString n times.
+-- Polymorphic wrapper to make sure any generated
+-- specializations are reasonably small.
+stimesPolymorphic :: Integral a => a -> ByteString -> ByteString
+{-# INLINABLE stimesPolymorphic #-}
+stimesPolymorphic nRaw !bs = case checkedIntegerToInt n of
+  Just nInt
+    | nInt >= 0  -> stimesNonNegativeInt nInt bs
+    | otherwise  -> stimesNegativeErr
+  Nothing
+    | n < 0  -> stimesNegativeErr
+    | BS _ 0 <- bs  -> empty
+    | otherwise     -> stimesOverflowErr
+  where  n = toInteger nRaw
+  -- By exclusively using n instead of nRaw, the semantics are kept simple
+  -- and the likelihood of potentially dangerous mistakes minimized.
+
+
+stimesNegativeErr :: ByteString
+stimesNegativeErr
+  = error "stimes @ByteString: non-negative multiplier expected"
+
+stimesOverflowErr :: ByteString
+-- Although this only appears once, it is extracted here to prevent it
+-- from being duplicated in specializations of 'stimesPolymorphic'
+stimesOverflowErr = overflowError "stimes"
+
+-- | Repeats the given ByteString n times.
+stimesNonNegativeInt :: Int -> ByteString -> ByteString
+stimesNonNegativeInt n (BS fp len)
   | n == 0 = empty
   | n == 1 = BS fp len
   | len == 0 = empty
-  | len == 1 = unsafeCreateFp size $ \destfptr -> do
+  | len == 1 = unsafeCreateFp n $ \destfptr -> do
       byte <- peekFp fp
       unsafeWithForeignPtr destfptr $ \destptr ->
-        fillBytes destptr byte (fromIntegral n)
+        fillBytes destptr byte n
   | otherwise = unsafeCreateFp size $ \destptr -> do
       memcpyFp destptr fp len
       fillFrom destptr len
   where
-    size = len * fromIntegral n
+    size = checkedMultiply "stimes" n len
+    halfSize = (size - 1) `div` 2 -- subtraction and division won't overflow
 
     fillFrom :: ForeignPtr Word8 -> Int -> IO ()
     fillFrom destptr copied
-      | 2 * copied <= size = do
+      | copied <= halfSize = do
         memcpyFp (destptr `plusForeignPtr` copied) destptr copied
         fillFrom destptr (copied * 2)
       | otherwise = memcpyFp (destptr `plusForeignPtr` copied) destptr (size - copied)
 
--- | Add two non-negative numbers. Errors out on overflow.                                   ...
-checkedAdd :: String -> Int -> Int -> Int
-checkedAdd fun x y
-  | r >= 0    = r
-  | otherwise = overflowError fun
-  where r = x + y
-{-# INLINE checkedAdd #-}
 
 ------------------------------------------------------------------------
 
@@ -912,8 +952,66 @@
 isSpaceChar8 = isSpaceWord8 . c2w
 {-# INLINE isSpaceChar8 #-}
 
+------------------------------------------------------------------------
+
+-- | The type of exception raised by 'overflowError'
+-- and on failure by overflow-checked arithmetic operations.
+newtype SizeOverflowException
+  = SizeOverflowException String
+
+instance Show SizeOverflowException where
+  show (SizeOverflowException err) = err
+
+instance Exception SizeOverflowException
+
+-- | Raises a 'SizeOverflowException',
+-- with a message using the given function name.
 overflowError :: String -> a
-overflowError fun = error $ "Data.ByteString." ++ fun ++ ": size overflow"
+overflowError fun = throw $ SizeOverflowException msg
+  where msg = "Data.ByteString." ++ fun ++ ": size overflow"
+
+-- | Add two non-negative numbers.
+-- Calls 'overflowError' on overflow.
+checkedAdd :: String -> Int -> Int -> Int
+{-# INLINE checkedAdd #-}
+checkedAdd fun x y
+  -- checking "r < 0" here matches the condition in mallocPlainForeignPtrBytes,
+  -- helping the compiler see the latter is redundant in some places
+  | r < 0     = overflowError fun
+  | otherwise = r
+  where r = assert (min x y >= 0) $ x + y
+
+-- | Multiplies two non-negative numbers.
+-- Calls 'overflowError' on overflow.
+checkedMultiply :: String -> Int -> Int -> Int
+{-# INLINE checkedMultiply #-}
+checkedMultiply fun !x@(I# x#) !y@(I# y#) = assert (min x y >= 0) $
+#if TIMES_INT_2_AVAILABLE
+  case timesInt2# x# y# of
+    (# 0#, _, result #) -> I# result
+    _ -> overflowError fun
+#else
+  case timesWord2# (int2Word# x#) (int2Word# y#) of
+    (# hi, lo #) -> case or# hi (uncheckedShiftRL# lo shiftAmt) of
+      0## -> I# (word2Int# lo)
+      _   -> overflowError fun
+  where !(I# shiftAmt) = finiteBitSize (0 :: Word) - 1
+#endif
+
+
+-- | Attempts to convert an 'Integer' value to an 'Int', returning
+-- 'Nothing' if doing so would result in an overflow.
+checkedIntegerToInt :: Integer -> Maybe Int
+{-# INLINE checkedIntegerToInt #-}
+-- We could use Data.Bits.toIntegralSized, but this hand-rolled
+-- version is currently a bit faster as of GHC 9.2.
+-- It's even faster to just match on the Integer constructors, but
+-- we'd still need a fallback implementation for integer-simple.
+checkedIntegerToInt x
+  | x == toInteger res = Just res
+  | otherwise = Nothing
+  where  res = fromInteger x :: Int
+
 
 ------------------------------------------------------------------------
 
diff --git a/Data/ByteString/Lazy/Char8.hs b/Data/ByteString/Lazy/Char8.hs
--- a/Data/ByteString/Lazy/Char8.hs
+++ b/Data/ByteString/Lazy/Char8.hs
@@ -1,6 +1,7 @@
 {-# LANGUAGE BangPatterns #-}
-{-# OPTIONS_HADDOCK prune #-}
 {-# LANGUAGE Trustworthy #-}
+{-# LANGUAGE TypeApplications #-}
+{-# OPTIONS_HADDOCK prune #-}
 
 -- |
 -- Module      : Data.ByteString.Lazy.Char8
@@ -179,8 +180,28 @@
         copy,
 
         -- * Reading from ByteStrings
+        -- | Note that a lazy 'ByteString' may hold an unbounded stream of
+        -- @\'0\'@ digits, in which case the functions below may never return.
+        -- If that's a concern, you can use 'take' to first truncate the input
+        -- to an acceptable length.  Non-termination is also possible when
+        -- reading arbitrary precision numbers via 'readInteger' or
+        -- 'readNatural', if the input is an unbounded stream of arbitrary
+        -- decimal digits.
+        --
         readInt,
+        readInt64,
+        readInt32,
+        readInt16,
+        readInt8,
+
+        readWord,
+        readWord64,
+        readWord32,
+        readWord16,
+        readWord8,
+
         readInteger,
+        readNatural,
 
         -- * I\/O with 'ByteString's
         -- | ByteString I/O uses binary mode, without any character decoding
@@ -228,8 +249,10 @@
 import qualified Data.ByteString.Unsafe as B
 import Data.List.NonEmpty (NonEmpty(..))
 import Data.ByteString.Lazy.Internal
+import Data.ByteString.Lazy.ReadInt
+import Data.ByteString.Lazy.ReadNat
 
-import Data.ByteString.Internal (w2c, c2w, isSpaceWord8)
+import Data.ByteString.Internal (c2w,w2c,isSpaceWord8)
 
 import Data.Int (Int64)
 import qualified Data.List as List
@@ -899,96 +922,6 @@
 unwords :: [ByteString] -> ByteString
 unwords = intercalate (singleton ' ')
 {-# INLINE unwords #-}
-
--- | readInt reads an Int from the beginning of the ByteString.  If
--- there is no integer at the beginning of the string, it returns
--- Nothing, otherwise it just returns the int read, and the rest of the
--- string.
---
--- Note: This function will overflow the Int for large integers.
-
-readInt :: ByteString -> Maybe (Int, ByteString)
-{-# INLINE readInt #-}
-readInt Empty        = Nothing
-readInt (Chunk x xs) = case w2c (B.unsafeHead x) of
-    '-' -> loop True  0 0 (B.unsafeTail x) xs
-    '+' -> loop False 0 0 (B.unsafeTail x) xs
-    _   -> loop False 0 0 x xs
-
-    where loop :: Bool -> Int -> Int
-                -> S.ByteString -> ByteString -> Maybe (Int, ByteString)
-          loop neg !i !n !c cs
-              | B.null c = case cs of
-                             Empty          -> end  neg i n c  cs
-                             (Chunk c' cs') -> loop neg i n c' cs'
-              | otherwise =
-                  case B.unsafeHead c of
-                    w | w >= 0x30
-                     && w <= 0x39 -> loop neg (i+1)
-                                          (n * 10 + (fromIntegral w - 0x30))
-                                          (B.unsafeTail c) cs
-                      | otherwise -> end neg i n c cs
-
-          {-# INLINE end #-}
-          end _   0 _ _  _ = Nothing
-          end neg _ n c cs = e
-                where n' = if neg then negate n else n
-                      c' = chunk c cs
-                      e  = n' `seq` c' `seq` Just (n',c')
-         --                  in n' `seq` c' `seq` JustS n' c'
-
--- | readInteger reads an Integer from the beginning of the ByteString.  If
--- there is no integer at the beginning of the string, it returns Nothing,
--- otherwise it just returns the int read, and the rest of the string.
-readInteger :: ByteString -> Maybe (Integer, ByteString)
-readInteger Empty = Nothing
-readInteger (Chunk c0 cs0) =
-        case w2c (B.unsafeHead c0) of
-            '-' -> first (B.unsafeTail c0) cs0 >>= \(n, cs') -> return (-n, cs')
-            '+' -> first (B.unsafeTail c0) cs0
-            _   -> first c0 cs0
-
-    where first c cs
-              | B.null c = case cs of
-                  Empty          -> Nothing
-                  (Chunk c' cs') -> first' c' cs'
-              | otherwise = first' c cs
-
-          first' c cs = case B.unsafeHead c of
-              w | w >= 0x30 && w <= 0x39 -> Just $
-                  loop 1 (fromIntegral w - 0x30) [] (B.unsafeTail c) cs
-                | otherwise              -> Nothing
-
-          loop :: Int -> Int -> [Integer]
-               -> S.ByteString -> ByteString -> (Integer, ByteString)
-          loop !d !acc ns !c cs
-              | B.null c = case cs of
-                             Empty          -> combine d acc ns c cs
-                             (Chunk c' cs') -> loop d acc ns c' cs'
-              | otherwise =
-                  case B.unsafeHead c of
-                   w | w >= 0x30 && w <= 0x39 ->
-                       if d < 9 then loop (d+1)
-                                          (10*acc + (fromIntegral w - 0x30))
-                                          ns (B.unsafeTail c) cs
-                                else loop 1 (fromIntegral w - 0x30)
-                                          (fromIntegral acc : ns)
-                                          (B.unsafeTail c) cs
-                     | otherwise -> combine d acc ns c cs
-
-          combine _ acc [] c cs = end (fromIntegral acc) c cs
-          combine d acc ns c cs =
-              end (10^d * combine1 1000000000 ns + fromIntegral acc) c cs
-
-          combine1 _ [n] = n
-          combine1 b ns  = combine1 (b*b) $ combine2 b ns
-
-          combine2 b (n:m:ns) = let !t = n+m*b in t : combine2 b ns
-          combine2 _ ns       = ns
-
-          end n c cs = let !c' = chunk c cs
-                        in (n, c')
-
 
 -- | Write a ByteString to a handle, appending a newline byte.
 --
diff --git a/Data/ByteString/Lazy/Internal.hs b/Data/ByteString/Lazy/Internal.hs
--- a/Data/ByteString/Lazy/Internal.hs
+++ b/Data/ByteString/Lazy/Internal.hs
@@ -337,11 +337,11 @@
     -- It's still possible that the result is a single chunk
     goLen1 _   bs Empty = bs
     goLen1 cs0 (S.BS _ bl) (Chunk (S.BS _ cl) cs) =
-        goLen cs0 (S.checkedAdd "Lazy.concat" bl cl) cs
+        goLen cs0 (S.checkedAdd "Lazy.toStrict" bl cl) cs
 
     -- General case, just find the total length we'll need
     goLen cs0 !total (Chunk (S.BS _ cl) cs) =
-      goLen cs0 (S.checkedAdd "Lazy.concat" total cl) cs
+      goLen cs0 (S.checkedAdd "Lazy.toStrict" total cl) cs
     goLen cs0 total Empty =
       S.unsafeCreateFp total $ \ptr -> goCopy cs0 ptr
 
diff --git a/Data/ByteString/Lazy/ReadInt.hs b/Data/ByteString/Lazy/ReadInt.hs
new file mode 100644
--- /dev/null
+++ b/Data/ByteString/Lazy/ReadInt.hs
@@ -0,0 +1,264 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- This file is also included by "Data.ByteString.ReadInt", after defining
+-- "BYTESTRING_STRICT".  The two modules share much of their code, but
+-- the lazy version adds an outer loop over the chunks.
+
+#ifdef BYTESTRING_STRICT
+module Data.ByteString.ReadInt
+#else
+module Data.ByteString.Lazy.ReadInt
+#endif
+    ( readInt
+    , readInt8
+    , readInt16
+    , readInt32
+    , readWord
+    , readWord8
+    , readWord16
+    , readWord32
+    , readInt64
+    , readWord64
+    ) where
+
+import qualified Data.ByteString.Internal as BI
+#ifdef BYTESTRING_STRICT
+import Data.ByteString
+#else
+import Data.ByteString.Lazy
+import Data.ByteString.Lazy.Internal
+#endif
+import Data.Bits (FiniteBits, isSigned)
+import Data.ByteString.Internal (pattern BS, plusForeignPtr)
+import Data.Int
+import Data.Word
+import Foreign.ForeignPtr (ForeignPtr)
+import Foreign.Ptr (minusPtr, plusPtr)
+import Foreign.Storable (Storable(..))
+
+----- Public API
+
+-- | Try to read a signed 'Int' value from the 'ByteString', returning
+-- @Just (val, str)@ on success, where @val@ is the value read and @str@ is the
+-- rest of the input string.  If the sequence of digits decodes to a value
+-- larger than can be represented by an 'Int', the returned value will be
+-- 'Nothing'.
+--
+-- 'readInt' does not ignore leading whitespace, the value must start
+-- immediately at the beginning of the input string.
+--
+-- ==== __Examples__
+-- >>> readInt "-1729 sum of cubes"
+-- Just (-1729," sum of cubes")
+-- >>> readInt "+1: readInt also accepts a leading '+'"
+-- Just (1, ": readInt also accepts a leading '+'")
+-- >>> readInt "not a decimal number"
+-- Nothing
+-- >>> readInt "12345678901234567890 overflows maxBound"
+-- Nothing
+-- >>> readInt "-12345678901234567890 underflows minBound"
+-- Nothing
+--
+readInt :: ByteString -> Maybe (Int, ByteString)
+readInt = _read
+
+-- | A variant of 'readInt' specialised to 'Int32'.
+readInt32 :: ByteString -> Maybe (Int32, ByteString)
+readInt32 = _read
+
+-- | A variant of 'readInt' specialised to 'Int16'.
+readInt16 :: ByteString -> Maybe (Int16, ByteString)
+readInt16 = _read
+
+-- | A variant of 'readInt' specialised to 'Int8'.
+readInt8 :: ByteString -> Maybe (Int8, ByteString)
+readInt8 = _read
+
+-- | Try to read a 'Word' value from the 'ByteString', returning
+-- @Just (val, str)@ on success, where @val@ is the value read and @str@ is the
+-- rest of the input string.  If the sequence of digits decodes to a value
+-- larger than can be represented by a 'Word', the returned value will be
+-- 'Nothing'.
+--
+-- 'readWord' does not ignore leading whitespace, the value must start with a
+-- decimal digit immediately at the beginning of the input string.  Leading @+@
+-- signs are not accepted.
+--
+-- ==== __Examples__
+-- >>> readWord "1729 sum of cubes"
+-- Just (1729," sum of cubes")
+-- >>> readWord "+1729 has an explicit sign"
+-- Nothing
+-- >>> readWord "not a decimal number"
+-- Nothing
+-- >>> readWord "98765432109876543210 overflows maxBound"
+-- Nothing
+--
+readWord :: ByteString -> Maybe (Word, ByteString)
+readWord = _read
+
+-- | A variant of 'readWord' specialised to 'Word32'.
+readWord32 :: ByteString -> Maybe (Word32, ByteString)
+readWord32 = _read
+
+-- | A variant of 'readWord' specialised to 'Word16'.
+readWord16 :: ByteString -> Maybe (Word16, ByteString)
+readWord16 = _read
+
+-- | A variant of 'readWord' specialised to 'Word8'.
+readWord8 :: ByteString -> Maybe (Word8, ByteString)
+readWord8 = _read
+
+-- | A variant of 'readInt' specialised to 'Int64'.
+readInt64 :: ByteString -> Maybe (Int64, ByteString)
+readInt64 = _read
+
+-- | A variant of 'readWord' specialised to 'Word64'.
+readWord64 :: ByteString -> Maybe (Word64, ByteString)
+readWord64 = _read
+
+-- | Polymorphic Int*/Word* reader
+_read :: forall a. (Integral a, FiniteBits a, Bounded a)
+      => ByteString  -> Maybe (a, ByteString)
+{-# INLINE _read #-}
+_read
+    | isSigned @a 0
+      = \ bs -> signed bs >>= \ (r, s, d1) -> _readDecimal r s d1
+    | otherwise
+      -- When the input is @16^n-1@, as is the case with 'maxBound' for
+      -- all the Word* types, the last decimal digit of 'maxBound' is 5.
+      = \ bs -> unsigned 5 bs >>= \ (r, s, d1) -> _readDecimal r s d1
+  where
+    -- Returns:
+    --  * Mod 10 min/max bound remainder
+    --  * 2nd and later digits
+    --  * 1st digit
+    --
+    -- When the input is @8*16^n-1@, as is the case with 'maxBound' for
+    -- all the Int* types, the last decimal digit of 'maxBound' is 7.
+    --
+    signed :: ByteString -> Maybe (Word64, ByteString, Word64)
+    signed bs = do
+        (w, s) <- uncons bs
+        let d1 = fromDigit w
+        if | d1 <= 9   -> Just (7, s, d1) -- leading digit
+           | w == 0x2d -> unsigned 8 s    -- minus sign
+           | w == 0x2b -> unsigned 7 s    -- plus sign
+           | otherwise -> Nothing         -- not a number
+
+    unsigned :: Word64 -> ByteString -> Maybe (Word64, ByteString, Word64)
+    unsigned r bs = do
+        (w, s) <- uncons bs
+        let d1 = fromDigit w
+        if | d1 <= 9   -> Just (r, s, d1) -- leading digit
+           | otherwise -> Nothing         -- not a number
+
+----- Fixed-width unsigned reader
+
+-- | Intermediate result from scanning a chunk, final output is
+-- converted to the requested type once all chunks are processed.
+--
+data Result = Overflow
+            | Result !Int    -- number of bytes (digits) read
+                     !Word64 -- accumulator value
+
+_readDecimal :: forall a. (Integral a, Bounded a)
+             => Word64     -- ^ abs(maxBound/minBound) `mod` 10
+             -> ByteString -- ^ Input string
+             -> Word64     -- ^ First digit value
+             -> Maybe (a, ByteString)
+{-# INLINE _readDecimal #-}
+_readDecimal !r = consume
+  where
+    consume :: ByteString -> Word64 -> Maybe (a, ByteString)
+#ifdef BYTESTRING_STRICT
+    consume (BS fp len) a = case _digits q r fp len a of
+        Result used acc
+            | used == len
+              -> convert acc empty
+            | otherwise
+              -> convert acc $ BS (fp `plusForeignPtr` used) (len - used)
+        _   -> Nothing
+#else
+    -- All done
+    consume Empty acc = convert acc Empty
+    -- Process next chunk
+    consume (Chunk (BS fp len) cs) acc
+        = case _digits q r fp len acc of
+            Result used acc'
+                | used == len
+                  -- process remaining chunks
+                  -> consume cs acc'
+                | otherwise
+                  -- ran into a non-digit
+                  -> convert acc' $
+                     Chunk (BS (fp `plusForeignPtr` used) (len - used)) cs
+            _     -> Nothing
+#endif
+    convert :: Word64 -> ByteString -> Maybe (a, ByteString)
+    convert !acc rest =
+        let !i = case r of
+                -- minBound @Int* `mod` 10 == 8
+                8 -> negate $ fromIntegral @Word64 @a acc
+                _ -> fromIntegral @Word64 @a acc
+         in Just (i, rest)
+
+    -- The quotient of 'maxBound' divided by 10 is needed for
+    -- overflow checks, once the accumulator exceeds this value
+    -- no further digits can be added.  If equal, the last digit
+    -- must not exceed the `r` value (max/min bound `mod` 10).
+    --
+    q = fromIntegral @a @Word64 maxBound `div` 10
+
+----- Per chunk decoder
+
+-- | Process as many digits as we can, returning the additional
+-- number of digits found and the updated accumulator.  If the
+-- accumulator would overflow return 'Overflow'.
+--
+_digits :: Word64           -- ^ maximum non-overflow value `div` 10
+        -> Word64           -- ^ maximum non-overflow vavlue `mod` 10
+        -> ForeignPtr Word8 -- ^ Input buffer
+        -> Int              -- ^ Input length
+        -> Word64           -- ^ Accumulated value of leading digits
+        -> Result           -- ^ Bytes read and final accumulator,
+                            -- or else overflow indication
+{-# INLINE _digits #-}
+_digits !q !r fp len a = BI.accursedUnutterablePerformIO $
+    BI.unsafeWithForeignPtr fp $ \ ptr -> do
+        let end = ptr `plusPtr` len
+        go ptr end ptr a
+  where
+    go !start !end = loop
+      where
+        loop !ptr !acc = getDigit >>= \ !d ->
+            if | d > 9
+                 -> return $ Result (ptr `minusPtr` start) acc
+               | acc < q || acc == q && d <= r
+                 -> loop (ptr `plusPtr` 1) (acc * 10 + d)
+               | otherwise
+                 -> return Overflow
+          where
+            getDigit :: IO Word64
+            getDigit
+                | ptr /= end = fromDigit <$> peek ptr
+                | otherwise  = pure 10  -- End of input
+            {-# NOINLINE getDigit #-}
+            -- 'getDigit' makes it possible to implement a single success
+            -- exit point from the loop.  If instead we return 'Result'
+            -- from multiple places, when '_digits' is inlined we get (at
+            -- least GHC 8.10 through 9.2) for each exit path a separate
+            -- join point implementing the continuation code.  GHC ticket
+            -- <https://gitlab.haskell.org/ghc/ghc/-/issues/20739>.
+            --
+            -- The NOINLINE pragma is required to avoid inlining branches
+            -- that would restore multiple exit points.
+
+fromDigit :: Word8 -> Word64
+{-# INLINE fromDigit #-}
+fromDigit = \ !w -> fromIntegral w - 0x30 -- i.e. w - '0'
diff --git a/Data/ByteString/Lazy/ReadNat.hs b/Data/ByteString/Lazy/ReadNat.hs
new file mode 100644
--- /dev/null
+++ b/Data/ByteString/Lazy/ReadNat.hs
@@ -0,0 +1,257 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- This file is included by "Data.ByteString.ReadInt", after defining
+-- "BYTESTRING_STRICT".  The two modules are largely identical, except for the
+-- choice of ByteString type and the loops in `readNatural`, where the lazy
+-- version needs to nest the inner loop inside a loop over the constituent
+-- chunks.
+
+#ifdef BYTESTRING_STRICT
+module Data.ByteString.ReadNat
+#else
+module Data.ByteString.Lazy.ReadNat
+#endif
+    ( readInteger
+    , readNatural
+    ) where
+
+import qualified Data.ByteString.Internal as BI
+#ifdef BYTESTRING_STRICT
+import Data.ByteString
+#else
+import Data.ByteString.Lazy
+import Data.ByteString.Lazy.Internal
+#endif
+import Data.Bits (finiteBitSize)
+import Data.ByteString.Internal (pattern BS, plusForeignPtr)
+import Data.Word
+import Foreign.ForeignPtr (ForeignPtr)
+import Foreign.Ptr (Ptr, minusPtr, plusPtr)
+import Foreign.Storable (Storable(..))
+import Numeric.Natural (Natural)
+
+----- Public API
+
+-- | 'readInteger' reads an 'Integer' from the beginning of the 'ByteString'.
+-- If there is no 'Integer' at the beginning of the string, it returns
+-- 'Nothing', otherwise it just returns the 'Integer' read, and the rest of
+-- the string.
+--
+-- 'readInteger' does not ignore leading whitespace, the value must start
+-- immediately at the beginning of the input string.
+--
+-- ==== __Examples__
+-- >>> readInteger "-000111222333444555666777888999 all done"
+-- Just (-111222333444555666777888999," all done")
+-- >>> readInteger "+1: readInteger also accepts a leading '+'"
+-- Just (1, ": readInteger also accepts a leading '+'")
+-- >>> readInteger "not a decimal number"
+-- Nothing
+--
+readInteger :: ByteString -> Maybe (Integer, ByteString)
+readInteger = \ bs -> do
+    (w, s) <- uncons bs
+    let d = fromDigit w
+    if | d <=    9 -> unsigned d s -- leading digit
+       | w == 0x2d -> negative s   -- minus sign
+       | w == 0x2b -> positive s   -- plus sign
+       | otherwise -> Nothing      -- not a number
+  where
+    unsigned :: Word -> ByteString -> Maybe (Integer, ByteString)
+    unsigned d s =
+         let (!n, rest) = _readDecimal d s
+             !i = toInteger n
+          in Just (i, rest)
+
+    positive :: ByteString -> Maybe (Integer, ByteString)
+    positive bs = do
+        (w, s) <- uncons bs
+        let d = fromDigit w
+        if | d <=    9 -> unsigned d s
+           | otherwise -> Nothing
+
+    negative :: ByteString -> Maybe (Integer, ByteString)
+    negative bs = do
+        (w, s) <- uncons bs
+        let d = fromDigit w
+        if | d >     9 -> Nothing
+           | otherwise -> let (n, rest) = _readDecimal d s
+                              !i = negate $ toInteger n
+                           in Just (i, rest)
+
+-- | 'readNatural' reads a 'Natural' number from the beginning of the
+-- 'ByteString'.  If there is no 'Natural' number at the beginning of the
+-- string, it returns 'Nothing', otherwise it just returns the number read, and
+-- the rest of the string.
+--
+-- 'readNatural' does not ignore leading whitespace, the value must start with
+-- a decimal digit immediately at the beginning of the input string.  Leading
+-- @+@ signs are not accepted.
+--
+-- ==== __Examples__
+-- >>> readNatural "000111222333444555666777888999 all done"
+-- Just (111222333444555666777888999," all done")
+-- >>> readNatural "+000111222333444555666777888999 explicit sign"
+-- Nothing
+-- >>> readNatural "not a decimal number"
+-- Nothing
+--
+readNatural :: ByteString -> Maybe (Natural, ByteString)
+readNatural bs = do
+    (w, s) <- uncons bs
+    let d = fromDigit w
+    if | d <=    9 -> Just $! _readDecimal d s
+       | otherwise -> Nothing
+
+----- Internal implementation
+
+-- | Intermediate result from scanning a chunk, final output is
+-- obtained via `convert` after all the chunks are processed.
+--
+data Result = Result !Int      -- Bytes consumed
+                     !Word     -- Value of LSW
+                     !Int      -- Digits in LSW
+                     [Natural] -- Little endian MSW list
+
+_readDecimal :: Word -> ByteString -> (Natural, ByteString)
+_readDecimal =
+    -- Having read one digit, we're about to read the 2nd So the digit count
+    -- up to 'safeLog' starts at 2.
+    consume [] 2
+  where
+    consume :: [Natural] -> Int -> Word -> ByteString
+            -> (Natural, ByteString)
+#ifdef BYTESTRING_STRICT
+    consume ns cnt acc (BS fp len) =
+        -- Having read one digit, we're about to read the 2nd
+        -- So the digit count up to 'safeLog' starts at 2.
+        case natdigits fp len acc cnt ns of
+            Result used acc' cnt' ns'
+                | used == len
+                  -> convert acc' cnt' ns' $ empty
+                | otherwise
+                  -> convert acc' cnt' ns' $
+                     BS (fp `plusForeignPtr` used) (len - used)
+#else
+    -- All done
+    consume ns cnt acc Empty = convert acc cnt ns Empty
+    -- Process next chunk
+    consume ns cnt acc (Chunk (BS fp len) cs)
+        = case natdigits fp len acc cnt ns of
+            Result used acc' cnt' ns'
+                | used == len -- process more chunks
+                  -> consume ns' cnt' acc' cs
+                | otherwise   -- ran into a non-digit
+                  -> let c = Chunk (BS (fp `plusForeignPtr` used) (len - used)) cs
+                      in convert acc' cnt' ns' c
+#endif
+    convert !acc !cnt !ns rest =
+        let !n = combine acc cnt ns
+         in (n, rest)
+
+    -- | Merge least-significant word with reduction of of little-endian tail.
+    --
+    -- The input is:
+    --
+    -- * Least significant digits as a 'Word' (LSW)
+    -- * The number of digits that went into the LSW
+    -- * All the remaining digit groups ('safeLog' digits each),
+    --   in little-endian order
+    --
+    -- The result is obtained by pairwise recursive combining of all the
+    -- full size digit groups, followed by multiplication by @10^cnt@ and
+    -- addition of the LSW.
+    combine :: Word      -- ^ value of LSW
+            -> Int       -- ^ count of digits in LSW
+            -> [Natural] -- ^ tail elements (base @10^'safeLog'@)
+            -> Natural
+    {-# INLINE combine #-}
+    combine !acc !_   [] = wordToNatural acc
+    combine !acc !cnt ns =
+        wordToNatural (10^cnt) * combine1 safeBase ns + wordToNatural acc
+
+    -- | Recursive reduction of little-endian sequence of 'Natural'-valued
+    -- /digits/ in base @base@ (a power of 10).  The base is squared after
+    -- each round.  This shows better asymptotic performance than one word
+    -- at a time multiply-add folds.  See:
+    -- <https://gmplib.org/manual/Multiplication-Algorithms>
+    --
+    combine1 :: Natural -> [Natural] -> Natural
+    combine1 _    [n] = n
+    combine1 base ns  = combine1 (base * base) (combine2 base ns)
+
+    -- | One round pairwise merge of numbers in base @base@.
+    combine2 :: Natural -> [Natural] -> [Natural]
+    combine2 base (n:m:ns) = let !t = m * base + n in t : combine2 base ns
+    combine2 _    ns       = ns
+
+-- The intermediate representation is a little-endian sequence in base
+-- @10^'safeLog'@, prefixed by an initial element in base @10^cnt@ for some
+-- @cnt@ between 1 and 'safeLog'.  The final result is obtained by recursive
+-- pairwise merging of the tail followed by a final multiplication by @10^cnt@
+-- and addition of the head.
+--
+natdigits :: ForeignPtr Word8 -- ^ Input chunk
+          -> Int              -- ^ Chunk length
+          -> Word             -- ^ accumulated element
+          -> Int              -- ^ partial digit count
+          -> [Natural]        -- ^ accumulated MSB elements
+          -> Result
+{-# INLINE natdigits #-}
+natdigits fp len = \ acc cnt ns ->
+    BI.accursedUnutterablePerformIO $
+        BI.unsafeWithForeignPtr fp $ \ ptr -> do
+            let end = ptr `plusPtr` len
+            go ptr end acc cnt ns ptr
+  where
+    go !start !end = loop
+      where
+        loop :: Word -> Int -> [Natural] -> Ptr Word8 -> IO Result
+        loop !acc !cnt ns !ptr = getDigit >>= \ !d ->
+            if | d > 9
+                 -> return $ Result (ptr `minusPtr` start) acc cnt ns
+               | cnt < safeLog
+                 -> loop (10*acc + d) (cnt+1) ns $ ptr `plusPtr` 1
+               | otherwise
+                 -> let !acc' = wordToNatural acc
+                     in loop d 1 (acc' : ns) $ ptr `plusPtr` 1
+          where
+            getDigit | ptr /= end = fromDigit <$> peek ptr
+                     | otherwise  = pure 10  -- End of input
+            {-# NOINLINE getDigit #-}
+            -- 'getDigit' makes it possible to implement a single success
+            -- exit point from the loop.  If instead we return 'Result'
+            -- from multiple places, when 'natdigits' is inlined we get (at
+            -- least GHC 8.10 through 9.2) for each exit path a separate
+            -- join point implementing the continuation code.  GHC ticket
+            -- <https://gitlab.haskell.org/ghc/ghc/-/issues/20739>.
+            --
+            -- The NOINLINE pragma is required to avoid inlining branches
+            -- that would restore multiple exit points.
+
+----- Misc functions
+
+-- | Largest decimal digit count that never overflows the accumulator
+-- The base 10 logarithm of 2 is ~0.30103, therefore 2^n has at least
+-- @1 + floor (0.3 n)@ decimal digits.  Therefore @floor (0.3 n)@,
+-- digits cannot overflow the upper bound of an @n-bit@ word.
+--
+safeLog :: Int
+safeLog = 3 * finiteBitSize @Word 0 `div` 10
+
+-- | 10-power base for little-endian sequence of ~Word-sized "digits"
+safeBase :: Natural
+safeBase = 10 ^ safeLog
+
+fromDigit :: Word8 -> Word
+{-# INLINE fromDigit #-}
+fromDigit = \ !w -> fromIntegral w - 0x30 -- i.e. w - '0'
+
+wordToNatural :: Word -> Natural
+{-# INLINE wordToNatural #-}
+wordToNatural  = fromIntegral
diff --git a/Data/ByteString/ReadInt.hs b/Data/ByteString/ReadInt.hs
new file mode 100644
--- /dev/null
+++ b/Data/ByteString/ReadInt.hs
@@ -0,0 +1,3 @@
+{-# LANGUAGE CPP #-}
+#define BYTESTRING_STRICT
+#include "Lazy/ReadInt.hs"
diff --git a/Data/ByteString/ReadNat.hs b/Data/ByteString/ReadNat.hs
new file mode 100644
--- /dev/null
+++ b/Data/ByteString/ReadNat.hs
@@ -0,0 +1,3 @@
+{-# LANGUAGE CPP #-}
+#define BYTESTRING_STRICT
+#include "Lazy/ReadNat.hs"
diff --git a/Data/ByteString/Short/Internal.hs b/Data/ByteString/Short/Internal.hs
--- a/Data/ByteString/Short/Internal.hs
+++ b/Data/ByteString/Short/Internal.hs
@@ -1,10 +1,13 @@
 {-# LANGUAGE BangPatterns             #-}
 {-# LANGUAGE CPP                      #-}
 {-# LANGUAGE DeriveDataTypeable       #-}
+{-# LANGUAGE DeriveLift               #-}
 {-# LANGUAGE ForeignFunctionInterface #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE LambdaCase               #-}
 {-# LANGUAGE MagicHash                #-}
 {-# LANGUAGE MultiWayIf               #-}
+{-# LANGUAGE PatternSynonyms          #-}
 {-# LANGUAGE RankNTypes               #-}
 {-# LANGUAGE ScopedTypeVariables      #-}
 {-# LANGUAGE TemplateHaskellQuotes    #-}
@@ -42,7 +45,7 @@
 module Data.ByteString.Short.Internal (
 
     -- * The @ShortByteString@ type and representation
-    ShortByteString(..),
+    ShortByteString(.., SBS),
 
     -- * Introducing and eliminating 'ShortByteString's
     empty,
@@ -165,6 +168,8 @@
   , checkedAdd
   )
 
+import Data.Array.Byte
+  ( ByteArray(..) )
 import Data.Bits
   ( FiniteBits (finiteBitSize)
   , shiftL
@@ -175,21 +180,17 @@
   , (.|.)
   )
 import Data.Data
-  ( Data(..)
-  , mkNoRepType
-  )
+  ( Data(..) )
 import Data.Monoid
   ( Monoid(..) )
 import Data.Semigroup
   ( Semigroup((<>)) )
 import Data.String
   ( IsString(..) )
-import Data.Typeable
-  ( Typeable )
 import Control.Applicative
   ( pure )
 import Control.DeepSeq
-  ( NFData(..) )
+  ( NFData )
 import Control.Exception
   ( assert )
 import Control.Monad
@@ -272,7 +273,6 @@
 
 import qualified Data.List as List
 import qualified GHC.Exts
-import qualified Language.Haskell.TH.Lib as TH
 import qualified Language.Haskell.TH.Syntax as TH
 
 -- | A compact representation of a 'Word8' vector.
@@ -282,43 +282,35 @@
 -- 'ByteString' (at the cost of copying the string data). It supports very few
 -- other operations.
 --
-data ShortByteString = SBS ByteArray#
-    deriving Typeable
-
--- | @since 0.11.2.0
-instance TH.Lift ShortByteString where
-#if MIN_VERSION_template_haskell(2,16,0)
-  lift sbs = [| unsafePackLenLiteral |]
-    `TH.appE` TH.litE (TH.integerL (fromIntegral len))
-    `TH.appE` TH.litE (TH.BytesPrimL $ TH.Bytes ptr 0 (fromIntegral len))
-    where
-      BS ptr len = fromShort sbs
-#else
-  lift sbs = [| unsafePackLenLiteral |]
-    `TH.appE` TH.litE (TH.integerL (fromIntegral len))
-    `TH.appE` TH.litE (TH.StringPrimL $ BS.unpackBytes bs)
-    where
-      bs@(BS _ len) = fromShort sbs
-#endif
+newtype ShortByteString =
+  -- | @since 0.12.0.0
+  ShortByteString
+  { unShortByteString :: ByteArray
+  -- ^ @since 0.12.0.0
+  }
+  deriving (Eq, TH.Lift, Data, NFData)
 
-#if MIN_VERSION_template_haskell(2,17,0)
-  liftTyped = TH.unsafeCodeCoerce . TH.lift
-#elif MIN_VERSION_template_haskell(2,16,0)
-  liftTyped = TH.unsafeTExpCoerce . TH.lift
+-- | Prior to @bytestring-0.12@ 'SBS' was a genuine constructor of 'ShortByteString',
+-- but now it is a bundled pattern synonym, provided as a compatibility shim.
+pattern SBS :: ByteArray# -> ShortByteString
+pattern SBS x = ShortByteString (ByteArray x)
+#if __GLASGOW_HASKELL__ >= 802
+{-# COMPLETE SBS #-}
+-- To avoid spurious warnings from CI with ghc-8.0, we internally
+-- use view patterns like (unSBS -> ba#) instead of using (SBS ba#)
 #endif
 
--- The ByteArray# representation is always word sized and aligned but with a
--- known byte length. Our representation choice for ShortByteString is to leave
--- the 0--3 trailing bytes undefined. This means we can use word-sized writes,
--- but we have to be careful with reads, see equateBytes and compareBytes below.
-
-
-instance Eq ShortByteString where
-    (==)    = equateBytes
-
+-- | Lexicographic order.
 instance Ord ShortByteString where
     compare = compareBytes
 
+-- Instead of deriving Semigroup / Monoid , we stick to our own implementations
+-- of mappend / mconcat, because they are safer with regards to overflows
+-- (see prop_32bitOverflow_Short_mconcat test).
+-- ByteArray is likely to catch up starting from GHC 9.6:
+-- * https://gitlab.haskell.org/ghc/ghc/-/merge_requests/8272
+-- * https://gitlab.haskell.org/ghc/ghc/-/merge_requests/9128
+
 instance Semigroup ShortByteString where
     (<>)    = append
 
@@ -327,9 +319,6 @@
     mappend = (<>)
     mconcat = concat
 
-instance NFData ShortByteString where
-    rnf SBS{} = ()
-
 instance Show ShortByteString where
     showsPrec p ps r = showsPrec p (unpackChars ps) r
 
@@ -339,20 +328,15 @@
 -- | @since 0.10.12.0
 instance GHC.Exts.IsList ShortByteString where
   type Item ShortByteString = Word8
-  fromList = packBytes
-  toList   = unpack
+  fromList  = ShortByteString . GHC.Exts.fromList
+  fromListN = (ShortByteString .) . GHC.Exts.fromListN
+  toList    = GHC.Exts.toList . unShortByteString
 
 -- | Beware: 'fromString' truncates multi-byte characters to octets.
 -- e.g. "枯朶に烏のとまりけり秋の暮" becomes �6k�nh~�Q��n�
 instance IsString ShortByteString where
     fromString = packChars
 
-instance Data ShortByteString where
-  gfoldl f z txt = z packBytes `f` unpack txt
-  toConstr _     = error "Data.ByteString.Short.ShortByteString.toConstr"
-  gunfold _ _    = error "Data.ByteString.Short.ShortByteString.gunfold"
-  dataTypeOf _   = mkNoRepType "Data.ByteString.Short.ShortByteString"
-
 ------------------------------------------------------------------------
 -- Simple operations
 
@@ -405,12 +389,6 @@
   moduleError "index" $ "error in array index: " ++ show i
                         ++ " not in range [0.." ++ show (length sbs) ++ "]"
 
--- | @since 0.11.2.0
-unsafePackLenLiteral :: Int -> Addr# -> ShortByteString
-unsafePackLenLiteral len addr# =
-    -- createFromPtr allocates, so accursedUnutterablePerformIO is wrong
-    unsafeDupablePerformIO $ createFromPtr (Ptr addr#) len
-
 ------------------------------------------------------------------------
 -- Internal utils
 
@@ -418,11 +396,11 @@
 asBA (unSBS -> ba#) = BA# ba#
 
 unSBS :: ShortByteString -> ByteArray#
-unSBS (SBS ba#) = ba#
+unSBS (ShortByteString (ByteArray ba#)) = ba#
 
 create :: Int -> (forall s. MBA s -> ST s ()) -> ShortByteString
 create len fill =
-    runST $ do
+    assert (len >= 0) $ runST $ do
       mba <- newByteArray len
       fill mba
       BA# ba# <- unsafeFreezeByteArray mba
@@ -435,59 +413,60 @@
 -- (<= the maximum size) and the result value. The resulting byte array
 -- is realloced to this size.
 createAndTrim :: Int -> (forall s. MBA s -> ST s (Int, a)) -> (ShortByteString, a)
-createAndTrim l fill =
-    runST $ do
-      mba <- newByteArray l
-      (l', res) <- fill mba
-      if assert (l' <= l) $ l' >= l
+createAndTrim maxLen fill =
+    assert (maxLen >= 0) $ runST $ do
+      mba <- newByteArray maxLen
+      (len, res) <- fill mba
+      if assert (0 <= len && len <= maxLen) $ len >= maxLen
           then do
             BA# ba# <- unsafeFreezeByteArray mba
             return (SBS ba#, res)
           else do
-            mba2 <- newByteArray l'
-            copyMutableByteArray mba 0 mba2 0 l'
+            mba2 <- newByteArray len
+            copyMutableByteArray mba 0 mba2 0 len
             BA# ba# <- unsafeFreezeByteArray mba2
             return (SBS ba#, res)
 {-# INLINE createAndTrim #-}
 
 createAndTrim' :: Int -> (forall s. MBA s -> ST s Int) -> ShortByteString
-createAndTrim' l fill =
-    runST $ do
-      mba <- newByteArray l
-      l' <- fill mba
-      if assert (l' <= l) $ l' >= l
+createAndTrim' maxLen fill =
+    assert (maxLen >= 0) $ runST $ do
+      mba <- newByteArray maxLen
+      len <- fill mba
+      if assert (0 <= len && len <= maxLen) $ len >= maxLen
           then do
             BA# ba# <- unsafeFreezeByteArray mba
             return (SBS ba#)
           else do
-            mba2 <- newByteArray l'
-            copyMutableByteArray mba 0 mba2 0 l'
+            mba2 <- newByteArray len
+            copyMutableByteArray mba 0 mba2 0 len
             BA# ba# <- unsafeFreezeByteArray mba2
             return (SBS ba#)
 {-# INLINE createAndTrim' #-}
 
-createAndTrim'' :: Int -> (forall s. MBA s -> MBA s -> ST s (Int, Int)) -> (ShortByteString, ShortByteString)
-createAndTrim'' l fill =
+-- | Like createAndTrim, but with two buffers at once
+createAndTrim2 :: Int -> Int -> (forall s. MBA s -> MBA s -> ST s (Int, Int)) -> (ShortByteString, ShortByteString)
+createAndTrim2 maxLen1 maxLen2 fill =
     runST $ do
-      mba1 <- newByteArray l
-      mba2 <- newByteArray l
-      (l1, l2) <- fill mba1 mba2
-      sbs1 <- freeze' l1 mba1
-      sbs2 <- freeze' l2 mba2
+      mba1 <- newByteArray maxLen1
+      mba2 <- newByteArray maxLen2
+      (len1, len2) <- fill mba1 mba2
+      sbs1 <- freeze' len1 maxLen1 mba1
+      sbs2 <- freeze' len2 maxLen2 mba2
       pure (sbs1, sbs2)
   where
-    freeze' :: Int -> MBA s -> ST s ShortByteString
-    freeze' l' mba =
-      if assert (l' <= l) $ l' >= l
+    freeze' :: Int -> Int -> MBA s -> ST s ShortByteString
+    freeze' len maxLen mba =
+      if assert (0 <= len && len <= maxLen) $ len >= maxLen
           then do
             BA# ba# <- unsafeFreezeByteArray mba
             return (SBS ba#)
           else do
-            mba2 <- newByteArray l'
-            copyMutableByteArray mba 0 mba2 0 l'
+            mba2 <- newByteArray len
+            copyMutableByteArray mba 0 mba2 0 len
             BA# ba# <- unsafeFreezeByteArray mba2
             return (SBS ba#)
-{-# INLINE createAndTrim'' #-}
+{-# INLINE createAndTrim2 #-}
 
 isPinned :: ByteArray# -> Bool
 #if MIN_VERSION_base(4,10,0)
@@ -648,13 +627,6 @@
 ------------------------------------------------------------------------
 -- Eq and Ord implementations
 
-equateBytes :: ShortByteString -> ShortByteString -> Bool
-equateBytes sbs1 sbs2 =
-    let !len1 = length sbs1
-        !len2 = length sbs2
-     in len1 == len2
-     && 0 == compareByteArrays (asBA sbs1) (asBA sbs2) len1
-
 compareBytes :: ShortByteString -> ShortByteString -> Ordering
 compareBytes sbs1 sbs2 =
     let !len1 = length sbs1
@@ -667,7 +639,6 @@
             | len2 < len1 -> GT
             | otherwise   -> EQ
 
-
 ------------------------------------------------------------------------
 -- Appending and concatenation
 
@@ -675,7 +646,7 @@
 append src1 src2 =
   let !len1 = length src1
       !len2 = length src2
-   in create (len1 + len2) $ \dst -> do
+   in create (checkedAdd "Short.append" len1 len2) $ \dst -> do
         copyByteArray (asBA src1) 0 dst 0    len1
         copyByteArray (asBA src2) 0 dst len1 len2
 
@@ -683,8 +654,9 @@
 concat = \sbss ->
     create (totalLen 0 sbss) (\dst -> copy dst 0 sbss)
   where
-    totalLen !acc []          = acc
-    totalLen !acc (sbs: sbss) = totalLen (acc + length sbs) sbss
+    totalLen !acc [] = acc
+    totalLen !acc (curr : rest)
+      = totalLen (checkedAdd "Short.concat" acc $ length curr) rest
 
     copy :: MBA s -> Int -> [ShortByteString] -> ST s ()
     copy !_   !_   []                           = return ()
@@ -705,11 +677,11 @@
 --
 -- @since 0.11.3.0
 snoc :: ShortByteString -> Word8 -> ShortByteString
-snoc = \sbs c -> let l  = length sbs
-                     nl = l + 1
-  in create nl $ \mba -> do
-      copyByteArray (asBA sbs) 0 mba 0 l
-      writeWord8Array mba l c
+snoc = \sbs c -> let len    = length sbs
+                     newLen = checkedAdd "Short.snoc" len 1
+  in create newLen $ \mba -> do
+      copyByteArray (asBA sbs) 0 mba 0 len
+      writeWord8Array mba len c
 
 -- | /O(n)/ 'cons' is analogous to (:) for lists.
 --
@@ -717,11 +689,11 @@
 --
 -- @since 0.11.3.0
 cons :: Word8 -> ShortByteString -> ShortByteString
-cons c = \sbs -> let l  = length sbs
-                     nl = l + 1
-  in create nl $ \mba -> do
+cons c = \sbs -> let len    = length sbs
+                     newLen = checkedAdd "Short.cons" len 1
+  in create newLen $ \mba -> do
       writeWord8Array mba 0 c
-      copyByteArray (asBA sbs) 0 mba 1 l
+      copyByteArray (asBA sbs) 0 mba 1 len
 
 -- | /O(1)/ Extract the last element of a ShortByteString, which must be finite and non-empty.
 -- An exception will be thrown in the case of an empty ShortByteString.
@@ -1513,9 +1485,9 @@
 --
 -- @since 0.11.3.0
 partition :: (Word8 -> Bool) -> ShortByteString -> (ShortByteString, ShortByteString)
-partition k = \sbs -> let l = length sbs
-                   in if | l <= 0    -> (sbs, sbs)
-                         | otherwise -> createAndTrim'' l $ \mba1 mba2 -> go mba1 mba2 (asBA sbs) l
+partition k = \sbs -> let len = length sbs
+                   in if | len <= 0  -> (sbs, sbs)
+                         | otherwise -> createAndTrim2 len len $ \mba1 mba2 -> go mba1 mba2 (asBA sbs) len
   where
     go :: forall s.
           MBA s           -- mutable output bytestring1
@@ -1602,8 +1574,6 @@
             | otherwise = go (n + 1)
   in go 0
 
-
-
 ------------------------------------------------------------------------
 -- Exported low level operations
 
@@ -1645,12 +1615,14 @@
 #endif
 
 newByteArray :: Int -> ST s (MBA s)
-newByteArray (I# len#) =
+newByteArray len@(I# len#) =
+  assert (len >= 0) $
     ST $ \s -> case newByteArray# len# s of
                  (# s', mba# #) -> (# s', MBA# mba# #)
 
 newPinnedByteArray :: Int -> ST s (MBA s)
-newPinnedByteArray (I# len#) =
+newPinnedByteArray len@(I# len#) =
+  assert (len >= 0) $
     ST $ \s -> case newPinnedByteArray# len# s of
                  (# s', mba# #) -> (# s', MBA# mba# #)
 
diff --git a/bench/BenchAll.hs b/bench/BenchAll.hs
--- a/bench/BenchAll.hs
+++ b/bench/BenchAll.hs
@@ -16,6 +16,7 @@
 
 import           Data.Foldable                         (foldMap)
 import           Data.Monoid
+import           Data.Semigroup
 import           Data.String
 import           Test.Tasty.Bench
 import           Prelude                               hiding (words)
@@ -43,6 +44,7 @@
 import BenchCount
 import BenchCSV
 import BenchIndices
+import BenchReadInt
 import BenchShort
 
 ------------------------------------------------------------------------------
@@ -418,6 +420,11 @@
       , bench "lazy"   $ nf L.tails lazyByteStringData
       ]
     , bgroup "sort" $ map (\s -> bench (S8.unpack s) $ nf S.sort s) sortInputs
+    , bgroup "stimes" $ let  st = stimes :: Int -> S.ByteString -> S.ByteString
+     in
+      [ bench "strict (tiny)" $ whnf (st 4) (S8.pack "test")
+      , bench "strict (large)" $ whnf (st 50) byteStringData
+      ]
     , bgroup "words"
       [ bench "lorem ipsum" $ nf S8.words loremIpsum
       , bench "one huge word" $ nf S8.words byteStringData
@@ -490,5 +497,6 @@
     , benchCount
     , benchCSV
     , benchIndices
+    , benchReadInt
     , benchShort
     ]
diff --git a/bench/BenchReadInt.hs b/bench/BenchReadInt.hs
new file mode 100644
--- /dev/null
+++ b/bench/BenchReadInt.hs
@@ -0,0 +1,144 @@
+-- |
+-- Copyright   : (c) 2021 Viktor Dukhovni
+-- License     : BSD3-style (see LICENSE)
+--
+-- Maintainer  : Viktor Dukhovni <ietf-dane@dukhovni.org>
+--
+-- Benchmark readInt and variants, readWord and variants,
+-- readInteger and readNatural
+
+{-# LANGUAGE
+    CPP
+  , BangPatterns
+  , OverloadedStrings
+  , TypeApplications
+  , ScopedTypeVariables
+  #-}
+
+module BenchReadInt (benchReadInt) where
+
+import qualified Data.ByteString.Builder               as B
+import qualified Data.ByteString.Char8                 as S
+import qualified Data.ByteString.Lazy.Char8            as L
+import Test.Tasty.Bench
+import Data.Int
+import Data.Word
+import Numeric.Natural
+#if !(MIN_VERSION_base(4,11,0))
+import Data.Semigroup (Semigroup((<>)))
+#endif
+import Data.Monoid (mconcat)
+
+------------------------------------------------------------------------------
+-- Benchmark
+------------------------------------------------------------------------------
+
+-- Sum space-separated integers in a ByteString.
+loopS :: Integral a
+      => (S.ByteString -> Maybe (a, S.ByteString)) -> S.ByteString -> a
+loopS rd = go 0
+  where
+    go !acc !bs = case rd bs of
+        Just (i, t) -> case S.uncons t of
+            Just (_, t') -> go (acc + i) t'
+            Nothing      -> acc + i
+        Nothing          -> acc
+
+-- Sum space-separated integers in a ByteString.
+loopL :: Integral a
+      => (L.ByteString -> Maybe (a, L.ByteString)) -> L.ByteString -> a
+loopL rd = go 0
+  where
+    go !acc !bs = case rd bs of
+        Just (i, t) -> case L.uncons t of
+            Just (_, t') -> go (acc + i) t'
+            Nothing      -> acc + i
+        Nothing          -> acc
+
+benchReadInt :: Benchmark
+benchReadInt = bgroup "Read Integral"
+    [ bgroup "Strict"
+        [ bench "ReadInt"     $ nf (loopS S.readInt)     intS
+        , bench "ReadInt8"    $ nf (loopS S.readInt8)    int8S
+        , bench "ReadInt16"   $ nf (loopS S.readInt16)   int16S
+        , bench "ReadInt32"   $ nf (loopS S.readInt32)   int32S
+        , bench "ReadInt64"   $ nf (loopS S.readInt64)   int64S
+        , bench "ReadWord"    $ nf (loopS S.readWord)    wordS
+        , bench "ReadWord8"   $ nf (loopS S.readWord8)   word8S
+        , bench "ReadWord16"  $ nf (loopS S.readWord16)  word16S
+        , bench "ReadWord32"  $ nf (loopS S.readWord32)  word32S
+        , bench "ReadWord64"  $ nf (loopS S.readWord64)  word64S
+        , bench "ReadInteger" $ nf (loopS S.readInteger) bignatS
+        , bench "ReadNatural" $ nf (loopS S.readNatural) bignatS
+        , bench "ReadInteger small" $ nf (loopS S.readInteger) intS
+        , bench "ReadNatural small" $ nf (loopS S.readNatural) wordS
+        ]
+
+    , bgroup "Lazy"
+        [ bench "ReadInt"     $ nf (loopL L.readInt)     intL
+        , bench "ReadInt8"    $ nf (loopL L.readInt8)    int8L
+        , bench "ReadInt16"   $ nf (loopL L.readInt16)   int16L
+        , bench "ReadInt32"   $ nf (loopL L.readInt32)   int32L
+        , bench "ReadInt64"   $ nf (loopL L.readInt64)   int64L
+        , bench "ReadWord"    $ nf (loopL L.readWord)    wordL
+        , bench "ReadWord8"   $ nf (loopL L.readWord8)   word8L
+        , bench "ReadWord16"  $ nf (loopL L.readWord16)  word16L
+        , bench "ReadWord32"  $ nf (loopL L.readWord32)  word32L
+        , bench "ReadWord64"  $ nf (loopL L.readWord64)  word64L
+        , bench "ReadInteger" $ nf (loopL L.readInteger) bignatL
+        , bench "ReadNatural" $ nf (loopL L.readNatural) bignatL
+        , bench "ReadInteger small" $ nf (loopL L.readInteger) intL
+        , bench "ReadNatural small" $ nf (loopL L.readNatural) wordL
+        ]
+    ]
+  where
+    mkWordL :: forall a. (Integral a, Bounded a)
+            => (a -> B.Builder) -> L.ByteString
+    mkWordL f = B.toLazyByteString b
+      where b = mconcat [f i <> B.char8 ' ' | i <- [n-255..n]]
+            n = maxBound @a
+    mkWordS f = S.toStrict $ mkWordL f
+
+    mkIntL :: forall a. (Integral a, Bounded a)
+           => (a -> B.Builder) -> L.ByteString
+    mkIntL f = B.toLazyByteString b
+      where b = mconcat [f (i + 128) <> B.char8 ' ' | i <- [n-255..n]]
+            n = maxBound @a
+    mkIntS f = S.toStrict $ mkIntL f
+
+    wordS, word8S, word16S, word32S, word64S :: S.ByteString
+    !wordS = mkWordS B.wordDec
+    !word8S = mkWordS B.word8Dec
+    !word16S = mkWordS B.word16Dec
+    !word32S = mkWordS B.word32Dec
+    !word64S = mkWordS B.word64Dec
+
+    intS, int8S, int16S, int32S, int64S :: S.ByteString
+    !intS =  mkIntS B.intDec
+    !int8S = mkIntS B.int8Dec
+    !int16S = mkIntS B.int16Dec
+    !int32S = mkIntS B.int32Dec
+    !int64S = mkIntS B.int64Dec
+
+    word8L, word16L, word32L, word64L :: L.ByteString
+    !wordL = mkWordL B.wordDec
+    !word8L = mkWordL B.word8Dec
+    !word16L = mkWordL B.word16Dec
+    !word32L = mkWordL B.word32Dec
+    !word64L = mkWordL B.word64Dec
+
+    intL, int8L, int16L, int32L, int64L :: L.ByteString
+    !intL =  mkIntL B.intDec
+    !int8L = mkIntL B.int8Dec
+    !int16L = mkIntL B.int16Dec
+    !int32L = mkIntL B.int32Dec
+    !int64L = mkIntL B.int64Dec
+
+    bignatL :: L.ByteString
+    !bignatL = B.toLazyByteString b
+      where b = mconcat [B.integerDec (powpow i) <> B.char8 ' ' | i <- [0..13]]
+            powpow :: Word -> Integer
+            powpow n = 2^(2^n :: Word)
+
+    bignatS :: S.ByteString
+    !bignatS = S.toStrict bignatL
diff --git a/bytestring.cabal b/bytestring.cabal
--- a/bytestring.cabal
+++ b/bytestring.cabal
@@ -1,5 +1,5 @@
 Name:                bytestring
-Version:             0.11.5.4
+Version:             0.12.0.0
 Synopsis:            Fast, compact, strict and lazy byte strings with a list interface
 Description:
     An efficient compact, immutable byte string type (both strict and lazy)
@@ -74,6 +74,9 @@
 library
   build-depends:     base >= 4.9 && < 5, ghc-prim, deepseq, template-haskell
 
+  if impl(ghc < 9.4)
+    build-depends: data-array-byte >= 0.1 && < 0.2
+
   exposed-modules:   Data.ByteString
                      Data.ByteString.Char8
                      Data.ByteString.Unsafe
@@ -103,6 +106,10 @@
                      Data.ByteString.Builder.RealFloat.TableGenerator
                      Data.ByteString.Internal.Type
                      Data.ByteString.Lazy.Internal.Deque
+                     Data.ByteString.Lazy.ReadInt
+                     Data.ByteString.Lazy.ReadNat
+                     Data.ByteString.ReadInt
+                     Data.ByteString.ReadNat
 
   default-language:  Haskell2010
   other-extensions:  CPP,
@@ -182,6 +189,7 @@
                     BenchCount
                     BenchCSV
                     BenchIndices
+                    BenchReadInt
                     BenchShort
   type:             exitcode-stdio-1.0
   hs-source-dirs:   bench
diff --git a/cbits/aarch64/is-valid-utf8.c b/cbits/aarch64/is-valid-utf8.c
--- a/cbits/aarch64/is-valid-utf8.c
+++ b/cbits/aarch64/is-valid-utf8.c
@@ -260,24 +260,20 @@
   //'Roll back' our pointer a little to prepare for a slow search of the rest.
   uint32_t token;
   vst1q_lane_u32(&token, vreinterpretq_u32_u8(prev_input), 3);
-  uint8_t const *token_ptr = (uint8_t const *)&token;
-  ptrdiff_t rollback = 0;
-  // We must not roll back if no big blocks were processed, as then
-  // the fallback function would examine out-of-bounds data (#620).
-  // In that case, prev_input contains only nulls and we skip the if body.
-  if (token_ptr[3] >= 0x80u) {
-    // Look for an incomplete multi-byte code point
-    if (token_ptr[3] >= 0xC0u) {
-      rollback = 1;
-    } else if (token_ptr[2] >= 0xE0u) {
-      rollback = 2;
-    } else if (token_ptr[1] >= 0xF0u) {
-      rollback = 3;
-    }
+  // We cast this pointer to avoid a redundant check against < 127, as any such
+  // value would be negative in signed form.
+  int8_t const *token_ptr = (int8_t const *)&token;
+  ptrdiff_t lookahead = 0;
+  if (token_ptr[3] > (int8_t)0xBF) {
+    lookahead = 1;
+  } else if (token_ptr[2] > (int8_t)0xBF) {
+    lookahead = 2;
+  } else if (token_ptr[1] > (int8_t)0xBF) {
+    lookahead = 3;
   }
   // Finish the job.
-  uint8_t const *const small_ptr = ptr - rollback;
-  size_t const small_len = remaining + rollback;
+  uint8_t const *const small_ptr = ptr - lookahead;
+  size_t const small_len = remaining + lookahead;
   return is_valid_utf8_fallback(small_ptr, small_len);
 }
 
diff --git a/cbits/is-valid-utf8.c b/cbits/is-valid-utf8.c
--- a/cbits/is-valid-utf8.c
+++ b/cbits/is-valid-utf8.c
@@ -50,7 +50,6 @@
 #endif
 
 #include <MachDeps.h>
-#include "ghcplatform.h"
 
 #ifdef WORDS_BIGENDIAN
 #define to_little_endian(x) __builtin_bswap64(x)
@@ -67,29 +66,6 @@
   return r;
 }
 
-// stand-in for __builtin_ctzll, used because __builtin_ctzll can
-// cause runtime linker issues for GHC in some exotic situations (#601)
-//
-// See also these ghc issues:
-//  * https://gitlab.haskell.org/ghc/ghc/-/issues/21787
-//  * https://gitlab.haskell.org/ghc/ghc/-/issues/22011
-static inline int hs_bytestring_ctz64(const uint64_t x) {
-  // These CPP conditions are taken from ghc-prim:
-  // https://gitlab.haskell.org/ghc/ghc/-/blob/73b5c7ce33929e1f7c9283ed7c2860aa40f6d0ec/libraries/ghc-prim/cbits/ctz.c#L31-57
-  // credit to Herbert Valerio Riedel, Erik de Castro Lopo
-#if defined(__GNUC__) && (defined(i386_HOST_ARCH) || defined(powerpc_HOST_ARCH))
-  uint32_t xhi = (uint32_t)(x >> 32);
-  uint32_t xlo = (uint32_t) x;
-  return xlo ? __builtin_ctz(xlo) : 32 + __builtin_ctz(xhi);
-#elif SIZEOF_UNSIGNED_LONG == 8
-  return __builtin_ctzl(x);
-#elif SIZEOF_UNSIGNED_LONG_LONG == 8
-  return __builtin_ctzll(x);
-#else
-# error no suitable __builtin_ctz() found
-#endif
-}
-
 static inline int is_valid_utf8_fallback(uint8_t const *const src,
                                          size_t const len) {
   uint8_t const *ptr = (uint8_t const *)src;
@@ -124,16 +100,16 @@
               if (results[3] == 0) {
                 ptr += 8;
               } else {
-                ptr += (hs_bytestring_ctz64(results[3]) / 8);
+                ptr += (__builtin_ctzll(results[3]) / 8);
               }
             } else {
-              ptr += (hs_bytestring_ctz64(results[2]) / 8);
+              ptr += (__builtin_ctzll(results[2]) / 8);
             }
           } else {
-            ptr += (hs_bytestring_ctz64(results[1]) / 8);
+            ptr += (__builtin_ctzll(results[1]) / 8);
           }
         } else {
-          ptr += (hs_bytestring_ctz64(results[0]) / 8);
+          ptr += (__builtin_ctzll(results[0]) / 8);
         }
       }
     }
@@ -231,16 +207,16 @@
               if (result == 0) {
                 ptr += 16;
               } else {
-                ptr += __builtin_ctz(result);
+                ptr += __builtin_ctzll(result);
               }
             } else {
-              ptr += __builtin_ctz(result);
+              ptr += __builtin_ctzll(result);
             }
           } else {
-            ptr += __builtin_ctz(result);
+            ptr += __builtin_ctzll(result);
           }
         } else {
-          ptr += __builtin_ctz(result);
+          ptr += __builtin_ctzll(result);
         }
       }
     }
@@ -346,7 +322,7 @@
 // +------------+---------------+------------------+----------------+
 // | F0         | 3             | 3                | 6              |
 // +------------+---------------+------------------+----------------+
-// | F4         | 3             | 4                | 7              |
+// | F4         | 4             | 4                | 8              |
 // +------------+---------------+------------------+----------------+
 // index1 -> E0, index14 -> ED
 static int8_t const df_ee_lookup[16] = {
@@ -498,27 +474,20 @@
     return 0;
   }
   // 'Roll back' our pointer a little to prepare for a slow search of the rest.
-  uint16_t tokens[2];
+  int16_t tokens[2];
   tokens[0] = _mm_extract_epi16(prev_input, 6);
   tokens[1] = _mm_extract_epi16(prev_input, 7);
-  uint8_t const *token_ptr = (uint8_t const *)tokens;
-  ptrdiff_t rollback = 0;
-  // We must not roll back if no big blocks were processed, as then
-  // the fallback function would examine out-of-bounds data (#620).
-  // In that case, prev_input contains only nulls and we skip the if body.
-  if (token_ptr[3] >= 0x80u) {
-    // Look for an incomplete multi-byte code point
-    if (token_ptr[3] >= 0xC0u) {
-      rollback = 1;
-    } else if (token_ptr[2] >= 0xE0u) {
-      rollback = 2;
-    } else if (token_ptr[1] >= 0xF0u) {
-      rollback = 3;
-    }
+  int8_t const *token_ptr = (int8_t const *)tokens;
+  ptrdiff_t lookahead = 0;
+  if (token_ptr[3] > (int8_t)0xBF) {
+    lookahead = 1;
+  } else if (token_ptr[2] > (int8_t)0xBF) {
+    lookahead = 2;
+  } else if (token_ptr[1] > (int8_t)0xBF) {
+    lookahead = 3;
   }
-  // Finish the job.
-  uint8_t const *const small_ptr = ptr - rollback;
-  size_t const small_len = remaining + rollback;
+  uint8_t const *const small_ptr = ptr - lookahead;
+  size_t const small_len = remaining + lookahead;
   return is_valid_utf8_fallback(small_ptr, small_len);
 }
 
@@ -711,24 +680,17 @@
   }
   // 'Roll back' our pointer a little to prepare for a slow search of the rest.
   uint32_t tokens_blob = _mm256_extract_epi32(prev_input, 7);
-  uint8_t const *token_ptr = (uint8_t const *)&tokens_blob;
-  ptrdiff_t rollback = 0;
-  // We must not roll back if no big blocks were processed, as then
-  // the fallback function would examine out-of-bounds data (#620).
-  // In that case, prev_input contains only nulls and we skip the if body.
-  if (token_ptr[3] >= 0x80u) {
-    // Look for an incomplete multi-byte code point
-    if (token_ptr[3] >= 0xC0u) {
-      rollback = 1;
-    } else if (token_ptr[2] >= 0xE0u) {
-      rollback = 2;
-    } else if (token_ptr[1] >= 0xF0u) {
-      rollback = 3;
-    }
+  int8_t const *tokens = (int8_t const *)&tokens_blob;
+  ptrdiff_t lookahead = 0;
+  if (tokens[3] > (int8_t)0xBF) {
+    lookahead = 1;
+  } else if (tokens[2] > (int8_t)0xBF) {
+    lookahead = 2;
+  } else if (tokens[1] > (int8_t)0xBF) {
+    lookahead = 3;
   }
-  // Finish the job.
-  uint8_t const *const small_ptr = ptr - rollback;
-  size_t const small_len = remaining + rollback;
+  uint8_t const *const small_ptr = ptr - lookahead;
+  size_t const small_len = remaining + lookahead;
   return is_valid_utf8_fallback(small_ptr, small_len);
 }
 
diff --git a/tests/IsValidUtf8.hs b/tests/IsValidUtf8.hs
--- a/tests/IsValidUtf8.hs
+++ b/tests/IsValidUtf8.hs
@@ -50,8 +50,7 @@
   testProperty "Three invalid bytes between spaces" $
     not $ B.isValidUtf8 threeBytesBetweenSpaces,
   testProperty "ASCII stride and invalid multibyte sequence" $
-    not $ B.isValidUtf8 asciiAndInvalidMultiByte,
-  testProperty "Splitting valid in two" splitValid
+    not $ B.isValidUtf8 asciiAndInvalidMultiByte
   ]
   where
     tooHigh :: ByteString
@@ -69,21 +68,13 @@
     threeBytesBetweenSpaces = fromList $ replicate 125 32 ++ [242, 134, 159] ++ replicate 128 32
 
     badBlockEnd :: Property
-    badBlockEnd =
-      forAllShrinkShow genBadBlock shrinkBadBlock showBadBlock $ \(BadBlock bs) ->
+    badBlockEnd = 
+      forAllShrinkShow genBadBlock shrinkBadBlock showBadBlock $ \(BadBlock bs) -> 
         not . B.isValidUtf8 $ bs
 
     asciiAndInvalidMultiByte :: ByteString
     asciiAndInvalidMultiByte = fromList $ replicate 32 48 ++ [235, 185]
 
-    splitValid :: Property
-    splitValid = forAll genValidUtf8 $ \bs ->
-      forAll (choose (0, B.length bs)) $ \k ->
-        case B.splitAt k bs of
-          -- q may have non-zero offset, which
-          -- allows this property test to tickle #620
-          (p, q) -> B.isValidUtf8 p == B.isValidUtf8 q
-
 -- Helpers
 
 -- A 128-byte sequence with a single bad byte at the end, with the rest being
@@ -107,7 +98,7 @@
 showBadBlock (BadBlock bs) = let asList = toList bs in
   foldr showHex "" asList
 
-data Utf8Sequence =
+data Utf8Sequence = 
   One Word8 |
   Two Word8 Word8 |
   Three Word8 Word8 Word8 |
@@ -125,7 +116,7 @@
       genThree :: Gen Utf8Sequence
       genThree = do
         w1 <- elements [0xE0 .. 0xED]
-        w2 <- elements $ case w1 of
+        w2 <- elements $ case w1 of 
           0xE0 -> [0xA0 .. 0xBF]
           0xED -> [0x80 .. 0x9F]
           _ -> [0x80 .. 0xBF]
@@ -134,7 +125,7 @@
       genFour :: Gen Utf8Sequence
       genFour = do
         w1 <- elements [0xF0 .. 0xF4]
-        w2 <- elements $ case w1 of
+        w2 <- elements $ case w1 of 
           0xF0 -> [0x90 .. 0xBF]
           0xF4 -> [0x80 .. 0x8F]
           _ -> [0x80 .. 0xBF]
@@ -142,46 +133,46 @@
         w4 <- elements [0x80 .. 0xBF]
         pure . Four w1 w2 w3 $ w4
   shrink = \case
-    One w1 -> One <$> case w1 of
+    One w1 -> One <$> case w1 of 
       0x00 -> []
       _ -> [0x00 .. (w1 - 1)]
-    Two w1 w2 -> case (w1, w2) of
+    Two w1 w2 -> case (w1, w2) of 
       (0xC2, 0x80) -> allOnes
       _ -> (Two <$> [0xC2 .. (w1 - 1)] <*> [0x80 .. (w2 - 1)]) ++ allOnes
-    Three w1 w2 w3 -> case (w1, w2, w3) of
+    Three w1 w2 w3 -> case (w1, w2, w3) of 
       (0xE0, 0xA0, 0x80) -> allTwos ++ allOnes
       (0xE0, 0xA0, _) -> (Three 0xE0 0xA0 <$> [0x80 .. (w3 - 1)]) ++ allTwos ++ allOnes
-      (0xE0, _, _) ->
+      (0xE0, _, _) -> 
         (Three 0xE0 <$> [0xA0 .. (w2 - 1)] <*> [0x80 .. (w3 - 1)]) ++ allTwos ++ allOnes
       _ -> do
         w1' <- [0xE0 .. (w1 - 1)]
-        case w1' of
-          0xE0 -> (Three 0xE0 <$> [0xA0 .. 0xBF] <*> [0x80 .. 0xBF]) ++
-                  allTwos ++
+        case w1' of 
+          0xE0 -> (Three 0xE0 <$> [0xA0 .. 0xBF] <*> [0x80 .. 0xBF]) ++ 
+                  allTwos ++ 
                   allOnes
-          _ -> (Three w1' <$> [0x80 .. 0xBF] <*> [0x80 .. 0xBF]) ++
-               allTwos ++
+          _ -> (Three w1' <$> [0x80 .. 0xBF] <*> [0x80 .. 0xBF]) ++ 
+               allTwos ++ 
                allOnes
-    Four w1 w2 w3 w4 -> case (w1, w2, w3, w4) of
+    Four w1 w2 w3 w4 -> case (w1, w2, w3, w4) of 
       (0xF0, 0x90, 0x80, 0x80) -> allThrees ++ allTwos ++ allOnes
-      (0xF0, 0x90, 0x80, _) ->
-        (Four 0xF0 0x90 0x80 <$> [0x80 .. (w4 - 1)]) ++
+      (0xF0, 0x90, 0x80, _) -> 
+        (Four 0xF0 0x90 0x80 <$> [0x80 .. (w4 - 1)]) ++ 
         allThrees ++
         allTwos ++
         allOnes
-      (0xF0, 0x90, _, _) ->
+      (0xF0, 0x90, _, _) -> 
         (Four 0xF0 0x90 <$> [0x80 .. (w3 - 1)] <*> [0x80 .. (w4 - 1)]) ++
         allThrees ++
         allTwos ++
         allOnes
-      (0xF0, _, _, _) ->
+      (0xF0, _, _, _) -> 
         (Four 0xF0 <$> [0x90 .. (w2 - 1)] <*> [0x80 .. (w3 - 1)] <*> [0x80 .. (w4 - 1)]) ++
         allThrees ++
         allTwos ++
         allOnes
       _ -> do
         w1' <- [0xF0 .. (w1 - 1)]
-        case w1' of
+        case w1' of 
           0xF0 -> (Four 0xF0 <$> [0x90 .. 0xBF] <*> [0x80 .. 0xBF] <*> [0x80 .. 0xBF]) ++
                   allThrees ++
                   allTwos ++
@@ -198,7 +189,7 @@
 allTwos = Two <$> [0xC2 .. 0xDF] <*> [0x80 .. 0xBF]
 
 allThrees :: [Utf8Sequence]
-allThrees = (Three 0xE0 <$> [0xA0 .. 0xBF] <*> [0x80 .. 0xBF]) ++
+allThrees = (Three 0xE0 <$> [0xA0 .. 0xBF] <*> [0x80 .. 0xBF]) ++ 
             (Three 0xED <$> [0x80 .. 0x9F] <*> [0x80 .. 0xBF]) ++
             (Three <$> [0xE1 .. 0xEC] <*> [0x80 .. 0xBF] <*> [0x80 .. 0xBF]) ++
             (Three <$> [0xEE .. 0xEF] <*> [0x80 .. 0xBF] <*> [0x80 .. 0xBF])
@@ -242,7 +233,7 @@
     , InvalidUtf8 <$> genValidUtf8 <*> genInvalidUtf8 <*> pure mempty
     , InvalidUtf8 <$> genValidUtf8 <*> genInvalidUtf8 <*> genValidUtf8
     ]
-  shrink (InvalidUtf8 p i s) =
+  shrink (InvalidUtf8 p i s) = 
     (InvalidUtf8 p i <$> shrinkValidBS s) ++
     ((\p' -> InvalidUtf8 p' i s) <$> shrinkValidBS p)
 
@@ -271,7 +262,7 @@
     -- overlong encoding
   , do k <- choose (0, 0xFFFF)
        let c = chr k
-       case k of
+       case k of 
         _ | k < 0x80    -> oneof [ let (w, x)       = ord2 c in pure [w, x]
                                  , let (w, x, y)    = ord3 c in pure [w, x, y]
                                  , let (w, x, y, z) = ord4 c in pure [w, x, y, z] ]
@@ -288,7 +279,7 @@
       vectorOf k gen
 
 genValidUtf8 :: Gen ByteString
-genValidUtf8 = sized $ \size ->
+genValidUtf8 = sized $ \size -> 
   if size <= 0
   then pure mempty
   else oneof [
@@ -309,7 +300,7 @@
     gen3Byte :: Gen ByteString
     gen3Byte = do
       b1 <- elements [0xE0 .. 0xED]
-      b2 <- elements $ case b1 of
+      b2 <- elements $ case b1 of 
         0xE0 -> [0xA0 .. 0xBF]
         0xED -> [0x80 .. 0x9F]
         _ -> [0x80 .. 0xBF]
@@ -318,7 +309,7 @@
     gen4Byte :: Gen ByteString
     gen4Byte = do
       b1 <- elements [0xF0 .. 0xF4]
-      b2 <- elements $ case b1 of
+      b2 <- elements $ case b1 of 
         0xF0 -> [0x90 .. 0xBF]
         0xF4 -> [0x80 .. 0x8F]
         _ -> [0x80 .. 0xBF]
diff --git a/tests/Properties.hs b/tests/Properties.hs
--- a/tests/Properties.hs
+++ b/tests/Properties.hs
@@ -1,7 +1,16 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE MagicHash #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE UnboxedTuples #-}
 
+-- We need @AllowAmbiguousTypes@ in order to be able to use @TypeApplications@
+-- to disambiguate the desired instance of class methods whose instance cannot
+-- be inferred from the caller's context.  We would otherwise have to use
+-- proxy arguments.  Here the 'RdInt' class methods used to generate tests for
+-- all the various 'readInt' types require explicit type applications.
+
 module Properties (testSuite) where
 
 import Prelude hiding (head, tail)
@@ -22,7 +31,9 @@
 import Data.Char
 import Data.Word
 import Data.Maybe
-import Data.Int (Int64)
+import Data.Either (isLeft)
+import Data.Bits (finiteBitSize, bit)
+import Data.Int (Int8, Int16, Int32, Int64)
 import Data.Semigroup
 import GHC.Exts (Int(..), newPinnedByteArray#, unsafeFreezeByteArray#)
 import GHC.ST (ST(..), runST)
@@ -47,7 +58,6 @@
 import qualified Data.ByteString.Lazy.Char8 as D
 
 import qualified Data.ByteString.Lazy.Internal as L
-import Prelude hiding (abs)
 
 import QuickCheckUtils
 import Test.Tasty
@@ -97,15 +107,228 @@
 
 prop_strip x = C.strip x == (C.dropSpace . C.reverse . C.dropSpace . C.reverse) x
 
--- Ensure that readInt and readInteger over lazy ByteStrings are not
+class (Bounded a, Integral a, Show a) => RdInt a where
+    rdIntC :: C.ByteString -> Maybe (a, C.ByteString)
+    rdIntD :: D.ByteString -> Maybe (a, D.ByteString)
+
+instance RdInt Int    where { rdIntC = C.readInt;    rdIntD = D.readInt }
+instance RdInt Int8   where { rdIntC = C.readInt8;   rdIntD = D.readInt8 }
+instance RdInt Int16  where { rdIntC = C.readInt16;  rdIntD = D.readInt16 }
+instance RdInt Int32  where { rdIntC = C.readInt32;  rdIntD = D.readInt32 }
+instance RdInt Int64  where { rdIntC = C.readInt64;  rdIntD = D.readInt64 }
+--
+instance RdInt Word   where { rdIntC = C.readWord;   rdIntD = D.readWord }
+instance RdInt Word8  where { rdIntC = C.readWord8;  rdIntD = D.readWord8 }
+instance RdInt Word16 where { rdIntC = C.readWord16; rdIntD = D.readWord16 }
+instance RdInt Word32 where { rdIntC = C.readWord32; rdIntD = D.readWord32 }
+instance RdInt Word64 where { rdIntC = C.readWord64; rdIntD = D.readWord64 }
+
+smax :: forall a. (Bounded a, Show a) => String
+smax = show $ maxBound @a
+smax1 :: forall a. (Bounded a, Integral a) => String
+smax1 = show $ fromIntegral @a @Integer maxBound + 1
+smax10 :: forall a. (Bounded a, Integral a) => String
+smax10 = show $ fromIntegral @a @Integer maxBound + 10
+
+smin :: forall a. (Bounded a, Show a) => String
+smin = show (minBound @a)
+smin1 :: forall a. (Bounded a, Integral a) => String
+smin1 = show $ fromIntegral @a @Integer minBound - 1
+smin10 :: forall a. (Bounded a, Integral a) => String
+smin10 = show $ fromIntegral @a @Integer minBound - 10
+
+-- Ensure that readWord64 and readInteger over lazy ByteStrings are not
 -- excessively strict.
-prop_readIntSafe         = (fst . fromJust . D.readInt) (Chunk (C.pack "1z") Empty)         == 1
-prop_readIntUnsafe       = (fst . fromJust . D.readInt) (Chunk (C.pack "2z") undefined)     == 2
+prop_readWordSafe        = (fst . fromJust . D.readWord64) (Chunk (C.pack "1z") Empty)      == 1
+prop_readWordUnsafe      = (fst . fromJust . D.readWord64) (Chunk (C.pack "2z") undefined)  == 2
 prop_readIntegerSafe     = (fst . fromJust . D.readInteger) (Chunk (C.pack "1z") Empty)     == 1
 prop_readIntegerUnsafe   = (fst . fromJust . D.readInteger) (Chunk (C.pack "2z") undefined) == 2
+prop_readNaturalSafe     = (fst . fromJust . D.readNatural) (Chunk (C.pack "1z") Empty)     == 1
+prop_readNaturalUnsafe   = (fst . fromJust . D.readNatural) (Chunk (C.pack "2z") undefined) == 2
+prop_readIntBoundsCC     =     rdWordBounds @Word
+                            && rdWordBounds @Word8
+                            && rdWordBounds @Word16
+                            && rdWordBounds @Word32
+                            && rdWordBounds @Word64
+                            && rdIntBounds  @Int
+                            && rdIntBounds  @Int8
+                            && rdIntBounds  @Int16
+                            && rdIntBounds  @Int32
+                            && rdIntBounds  @Int64
+  where
+    tailStr      = " tail"
+    zeroStr      = "000000000000000000000000000"
+    spack s      = C.pack $ s ++ tailStr
+    spackPlus s  = C.pack $ '+' : (s ++ tailStr)
+    spackMinus s = C.pack $ '-' : (s ++ tailStr)
+    spackLong s  = C.pack $ s ++ zeroStr ++ tailStr
+    spackZeros s = case s of
+                    '+':num -> C.pack $ '+' : zeroStr ++ num ++ tailStr
+                    '-':num -> C.pack $ '-' : zeroStr ++ num ++ tailStr
+                    num     -> C.pack $ zeroStr ++ num ++ tailStr
+    good i       = Just (i, C.pack tailStr)
+    --
+    rdWordBounds :: forall a. RdInt a => Bool
+    rdWordBounds =
+        -- Upper bound
+        rdIntC @a (spack (smax @a)) == good maxBound
+        -- With leading zeros
+        && rdIntC @a (spackZeros (smax @a)) == good maxBound
+        -- Overflow in last digit
+        && rdIntC @a (spack (smax1 @a)) == Nothing
+        -- Overflow in 2nd-last digit
+        && rdIntC @a (spack (smax10 @a)) == Nothing
+        -- Trailing zeros
+        && rdIntC @a (spackLong (smax @a)) == Nothing
+    --
+    rdIntBounds :: forall a. RdInt a => Bool
+    rdIntBounds =
+        rdWordBounds @a
+        -- Lower bound
+        && rdIntC @a (spack (smin @a)) == good minBound
+        -- With leading signs
+        && rdIntC @a (spackPlus (smax @a)) == good maxBound
+        && rdIntC @a (spackMinus (smax @a)) == good (negate maxBound)
+        -- With leading zeros
+        && rdIntC @a (spackZeros (smax @a)) == good maxBound
+        -- Underflow in last digit
+        && rdIntC @a (spack (smin1 @a)) == Nothing
+        -- Underflow in 2nd-last digit
+        && rdIntC @a (spack (smin10 @a)) == Nothing
+        -- Trailing zeros
+        && rdIntC @a (spackLong (smin @a)) == Nothing
 
+prop_readIntBoundsLC     =     rdWordBounds @Word
+                            && rdWordBounds @Word8
+                            && rdWordBounds @Word16
+                            && rdWordBounds @Word32
+                            && rdWordBounds @Word64
+                            && rdIntBounds  @Int
+                            && rdIntBounds  @Int8
+                            && rdIntBounds  @Int16
+                            && rdIntBounds  @Int32
+                            && rdIntBounds  @Int64
+  where
+    tailStr      = " tail"
+    zeroStr      = "000000000000000000000000000"
+    spack s      = LC.pack $ s ++ tailStr
+    spackPlus s  = LC.singleton '+' `D.append` LC.pack s `D.append` LC.pack tailStr
+    spackMinus s = LC.singleton '-' `D.append` LC.pack s `D.append` LC.pack tailStr
+    spackLong1 s = LC.pack s `D.append` LC.pack zeroStr `D.append` LC.pack tailStr
+    spackLong2 s = LC.pack (s ++ zeroStr) `D.append` LC.pack tailStr
+    spackZeros s = case s of
+                    '+':num -> LC.pack ('+' : zeroStr) `D.append` LC.pack (num ++ tailStr)
+                    '-':num -> LC.pack ('-' : zeroStr) `D.append` LC.pack (num ++ tailStr)
+                    num     -> LC.pack $ zeroStr ++ num ++ tailStr
+    good i       = Just (i, LC.pack tailStr)
+    --
+    rdWordBounds :: forall a. RdInt a => Bool
+    rdWordBounds =
+        -- Upper bound
+        rdIntD @a (spack (smax @a)) == good maxBound
+        -- With leading zeros
+        && rdIntD @a (spackZeros (smax @a)) == good maxBound
+        -- Overflow in last digit
+        && rdIntD @a (spack (smax1 @a)) == Nothing
+        -- Overflow in 2nd-last digit
+        && rdIntD @a (spack (smax10 @a)) == Nothing
+        -- Overflow across chunk boundary
+        && rdIntD @a (spackLong1 (smax @a)) == Nothing
+        -- Overflow within chunk
+        && rdIntD @a (spackLong2 (smax @a)) == Nothing
+        -- Sign with no digits
+        && rdIntD @a (LC.pack "+ foo") == Nothing
+        && rdIntD @a (LC.pack "-bar") == Nothing
+    --
+    rdIntBounds :: forall a. RdInt a => Bool
+    rdIntBounds =
+        rdWordBounds @a
+        -- Lower bound
+        && rdIntD @a (spack (smin @a)) == good minBound
+        -- With leading signs
+        && rdIntD @a (spackPlus (smax @a)) == good maxBound
+        && rdIntD @a (spackMinus (smax @a)) == good (negate maxBound)
+        -- With leading zeros
+        && rdIntD @a (spackZeros (smin @a)) == good minBound
+        -- Overflow in last digit
+        && rdIntD @a (spack (smin1 @a)) == Nothing
+        -- Overflow in 2nd-last digit
+        && rdIntD @a (spack (smin10 @a)) == Nothing
+        -- Overflow across chunk boundary
+        && rdIntD @a (spackLong1 (smin @a)) == Nothing
+        -- Overflow within chunk
+        && rdIntD @a (spackLong2 (smin @a)) == Nothing
+
 ------------------------------------------------------------------------
 
+expectSizeOverflow :: a -> Property
+expectSizeOverflow val = ioProperty $ do
+  isLeft <$> try @P.SizeOverflowException (evaluate val)
+
+prop_checkedAdd = forAll (vectorOf 2 nonNeg) $ \[x, y] -> if oflo x y
+  then expectSizeOverflow (P.checkedAdd "" x y)
+  else property $ P.checkedAdd "" x y == x + y
+  where nonNeg = choose (0, (maxBound @Int))
+        oflo x y = toInteger x + toInteger y /= toInteger @Int (x + y)
+
+multCompl :: Int -> Gen Int
+multCompl x = choose (0, fromInteger @Int maxc)
+  -- This choice creates products with magnitude roughly in the range
+  -- [0..5*(maxBound @Int)], which results in a roughly even split
+  -- between positive and negative overflowed Int results, while still
+  -- producing a fair number of non-overflowing products.
+  where maxc = toInteger (maxBound @Int) * 5 `quot` max 5 (abs $ toInteger x)
+
+prop_checkedMultiply = forAll genScale $ \scale ->
+  forAll (genVal scale) $ \x ->
+    forAll (multCompl x) $ \y -> if oflo x y
+      then expectSizeOverflow (P.checkedMultiply "" x y)
+      else property $ P.checkedMultiply "" x y == x * y
+  where genScale = choose (0, finiteBitSize @Int 0 - 1)
+        genVal scale = choose (0, bit scale - 1)
+        oflo x y = toInteger x * toInteger y /= toInteger @Int (x * y)
+
+prop_stimesOverflowBasic bs = forAll (multCompl len) $ \n ->
+  toInteger n * toInteger len > maxInt ==> expectSizeOverflow (stimes n bs)
+  where
+    maxInt = toInteger @Int (maxBound @Int)
+    len = P.length bs
+
+prop_stimesOverflowScary bs =
+  -- "Scary" because this test will cause heap corruption
+  -- (not just memory exhaustion) with the old stimes implementation.
+  n > 1 ==> expectSizeOverflow (stimes reps bs)
+  where
+    n = P.length bs
+    reps = maxBound @Word `quot` fromIntegral @Int @Word n + 1
+
+prop_stimesOverflowEmpty = forAll (choose (0, maxBound @Word)) $ \n ->
+  stimes n mempty === mempty @P.ByteString
+
+concat32bitOverflow :: (Int -> a) -> ([a] -> a) -> Property
+concat32bitOverflow replicateLike concatLike = let
+  intBits = finiteBitSize @Int 0
+  largeBS = concatLike $ replicate (bit 14) $ replicateLike (bit 17)
+  in if intBits /= 32
+     then label "skipped due to non-32-bit Int" True
+     else expectSizeOverflow largeBS
+
+prop_32bitOverflow_Strict_mconcat :: Property
+prop_32bitOverflow_Strict_mconcat =
+  concat32bitOverflow (`P.replicate` 0) mconcat
+
+prop_32bitOverflow_Lazy_toStrict :: Property
+prop_32bitOverflow_Lazy_toStrict =
+  concat32bitOverflow (`P.replicate` 0) (L.toStrict . L.fromChunks)
+
+prop_32bitOverflow_Short_mconcat :: Property
+prop_32bitOverflow_Short_mconcat =
+  concat32bitOverflow makeShort mconcat
+  where makeShort n = Short.toShort $ P.replicate n 0
+
+
+------------------------------------------------------------------------
+
 prop_packUptoLenBytes cs =
     forAll (choose (0, length cs + 1)) $ \n ->
       let (bs, cs') = P.packUptoLenBytes n cs
@@ -417,6 +640,7 @@
   , testGroup "StrictChar8"     PropBS8.tests
   , testGroup "LazyWord8"       PropBL.tests
   , testGroup "LazyChar8"       PropBL8.tests
+  , testGroup "Overflow"        overflow_tests
   , testGroup "Misc"            misc_tests
   , testGroup "IO"              io_tests
   , testGroup "Short"           short_tests
@@ -437,6 +661,17 @@
     , testProperty "packAddress       " prop_packAddress
     ]
 
+overflow_tests =
+    [ testProperty "checkedAdd" prop_checkedAdd
+    , testProperty "checkedMultiply" prop_checkedMultiply
+    , testProperty "StrictByteString stimes (basic)" prop_stimesOverflowBasic
+    , testProperty "StrictByteString stimes (scary)" prop_stimesOverflowScary
+    , testProperty "StrictByteString stimes (empty)" prop_stimesOverflowEmpty
+    , testProperty "StrictByteString mconcat" prop_32bitOverflow_Strict_mconcat
+    , testProperty "LazyByteString toStrict"  prop_32bitOverflow_Lazy_toStrict
+    , testProperty "ShortByteString mconcat"  prop_32bitOverflow_Short_mconcat
+    ]
+
 misc_tests =
     [ testProperty "packUptoLenBytes"       prop_packUptoLenBytes
     , testProperty "packUptoLenChars"       prop_packUptoLenChars
@@ -475,10 +710,14 @@
     , testProperty "strip"          prop_strip
     , testProperty "isSpace"        prop_isSpaceWord8
 
-    , testProperty "readIntSafe"       prop_readIntSafe
-    , testProperty "readIntUnsafe"     prop_readIntUnsafe
+    , testProperty "readWordSafe"      prop_readWordSafe
+    , testProperty "readWordUnsafe"    prop_readWordUnsafe
+    , testProperty "readIntBoundsCC"   prop_readIntBoundsCC
+    , testProperty "readIntBoundsLC"   prop_readIntBoundsLC
     , testProperty "readIntegerSafe"   prop_readIntegerSafe
     , testProperty "readIntegerUnsafe" prop_readIntegerUnsafe
+    , testProperty "readNaturalSafe"   prop_readNaturalSafe
+    , testProperty "readNaturalUnsafe" prop_readNaturalUnsafe
     ]
 
 strictness_checks =
diff --git a/tests/Properties/ByteString.hs b/tests/Properties/ByteString.hs
--- a/tests/Properties/ByteString.hs
+++ b/tests/Properties/ByteString.hs
@@ -4,8 +4,17 @@
 -- License     : BSD-style
 
 {-# LANGUAGE CPP #-}
+{-# LANGUAGE AllowAmbiguousTypes #-}
 {-# LANGUAGE ViewPatterns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
 
+-- We need @AllowAmbiguousTypes@ in order to be able to use @TypeApplications@
+-- to disambiguate the desired instance of class methods whose instance cannot
+-- be inferred from the caller's context.  We would otherwise have to use
+-- proxy arguments.  Here the 'RdInt' class methods used to generate tests for
+-- all the various 'readInt' types require explicit type applications.
+
 -- We are happy to sacrifice optimizations in exchange for faster compilation,
 -- but need to test rewrite rules. As one can check using -ddump-rule-firings,
 -- rewrite rules do not fire in -O0 mode, so we use -O1, but disable almost all
@@ -36,8 +45,6 @@
 import qualified Data.ByteString.Lazy as B
 #endif
 
-import Data.Word
-
 #else
 
 #ifndef BYTESTRING_LAZY
@@ -50,6 +57,9 @@
 #define BYTESTRING_TYPE B.ByteString
 #endif
 
+import Data.Int
+import Numeric.Natural (Natural)
+
 import Text.Read
 
 #endif
@@ -63,6 +73,7 @@
 import Data.Semigroup
 import Data.String
 import Data.Tuple
+import Data.Word
 import Test.Tasty
 import Test.Tasty.QuickCheck
 import QuickCheckUtils
@@ -73,6 +84,42 @@
 #else
 toElem :: Char8 -> Char
 toElem (Char8 c) = c
+
+class (Integral a, Show a) => RdInt a where
+    bread :: BYTESTRING_TYPE -> Maybe (a, BYTESTRING_TYPE)
+    sread :: String -> Maybe (a, String)
+
+instance RdInt Int    where { bread = B.readInt;     sread = readInt }
+instance RdInt Int8   where { bread = B.readInt8;    sread = readInt8 }
+instance RdInt Int16  where { bread = B.readInt16;   sread = readInt16 }
+instance RdInt Int32  where { bread = B.readInt32;   sread = readInt32 }
+instance RdInt Int64  where { bread = B.readInt64;   sread = readInt64 }
+--
+instance RdInt Word   where { bread = B.readWord;    sread = readWord }
+instance RdInt Word8  where { bread = B.readWord8;   sread = readWord8 }
+instance RdInt Word16 where { bread = B.readWord16;  sread = readWord16 }
+instance RdInt Word32 where { bread = B.readWord32;  sread = readWord32 }
+instance RdInt Word64 where { bread = B.readWord64;  sread = readWord64 }
+--
+instance RdInt Integer where { bread = B.readInteger; sread = readInteger }
+instance RdInt Natural where { bread = B.readNatural; sread = readNatural }
+
+instance Arbitrary Natural where
+    arbitrary = i2n <$> arbitrary
+      where i2n :: Integer -> Natural
+            i2n i | i >= 0 = fromIntegral i
+                  | otherwise = fromIntegral $ negate i
+
+testRdInt :: forall a. (Arbitrary a, RdInt a) => String -> TestTree
+testRdInt s = testGroup s $
+    [ testProperty "from string" $ \ prefix value suffix ->
+        let si = show @a value
+            b  = prefix <> B.pack si <> suffix
+         in fmap (second B.unpack) (bread @a b)
+            === sread @a (B.unpack prefix ++ si ++ B.unpack suffix)
+    , testProperty "from number" $ \n ->
+        bread @a (B.pack (show n)) === Just (n, B.empty)
+    ]
 #endif
 
 tests :: [TestTree]
@@ -558,14 +605,18 @@
 #ifdef BYTESTRING_CHAR8
   , testProperty "isString" $
     \x -> x === fromString (B.unpack x)
-  , testProperty "readInt 1" $
-    \x -> fmap (second B.unpack) (B.readInt x) === readInt (B.unpack x)
-  , testProperty "readInt 2" $
-    \n -> B.readInt (B.pack (show n)) === Just (n, B.empty)
-  , testProperty "readInteger 1" $
-    \x -> fmap (second B.unpack) (B.readInteger x) === readInteger (B.unpack x)
-  , testProperty "readInteger 2" $
-    \n -> B.readInteger (B.pack (show n)) === Just (n, B.empty)
+  , testRdInt @Int    "readInt"
+  , testRdInt @Int8   "readInt8"
+  , testRdInt @Int16  "readInt16"
+  , testRdInt @Int32  "readInt32"
+  , testRdInt @Int64  "readInt64"
+  , testRdInt @Word   "readWord"
+  , testRdInt @Word8  "readWord8"
+  , testRdInt @Word16 "readWord16"
+  , testRdInt @Word32 "readWord32"
+  , testRdInt @Word64 "readWord64"
+  , testRdInt @Integer "readInteger"
+  , testRdInt @Natural "readNatural"
   , testProperty "lines" $
     \x -> map B.unpack (B.lines x) === lines (B.unpack x)
   , testProperty "lines \\n" $ once $
@@ -648,10 +699,70 @@
     | y' <- fromInteger y, toInteger y' == y -> Just (y', zs)
   otherwise -> Nothing
 
+readWord :: String -> Maybe (Word, String)
+readWord xs = case readIntegerUnsigned xs of
+  Just (y, zs)
+    | y' <- fromInteger y, toInteger y' == y -> Just (y', zs)
+  otherwise -> Nothing
+
+readInt8 :: String -> Maybe (Int8, String)
+readInt8 xs = case readInteger xs of
+  Just (y, zs)
+    | y' <- fromInteger y, toInteger y' == y -> Just (y', zs)
+  otherwise -> Nothing
+
+readWord8 :: String -> Maybe (Word8, String)
+readWord8 xs = case readIntegerUnsigned xs of
+  Just (y, zs)
+    | y' <- fromInteger y, toInteger y' == y -> Just (y', zs)
+  otherwise -> Nothing
+
+readInt16 :: String -> Maybe (Int16, String)
+readInt16 xs = case readInteger xs of
+  Just (y, zs)
+    | y' <- fromInteger y, toInteger y' == y -> Just (y', zs)
+  otherwise -> Nothing
+
+readWord16 :: String -> Maybe (Word16, String)
+readWord16 xs = case readIntegerUnsigned xs of
+  Just (y, zs)
+    | y' <- fromInteger y, toInteger y' == y -> Just (y', zs)
+  otherwise -> Nothing
+
+readInt32 :: String -> Maybe (Int32, String)
+readInt32 xs = case readInteger xs of
+  Just (y, zs)
+    | y' <- fromInteger y, toInteger y' == y -> Just (y', zs)
+  otherwise -> Nothing
+
+readWord32 :: String -> Maybe (Word32, String)
+readWord32 xs = case readIntegerUnsigned xs of
+  Just (y, zs)
+    | y' <- fromInteger y, toInteger y' == y -> Just (y', zs)
+  otherwise -> Nothing
+
+readInt64 :: String -> Maybe (Int64, String)
+readInt64 xs = case readInteger xs of
+  Just (y, zs)
+    | y' <- fromInteger y, toInteger y' == y -> Just (y', zs)
+  otherwise -> Nothing
+
+readWord64 :: String -> Maybe (Word64, String)
+readWord64 xs = case readIntegerUnsigned xs of
+  Just (y, zs)
+    | y' <- fromInteger y, toInteger y' == y -> Just (y', zs)
+  otherwise -> Nothing
+
 readInteger :: String -> Maybe (Integer, String)
 readInteger ('+' : xs) = readIntegerUnsigned xs
 readInteger ('-' : xs) = fmap (first negate) (readIntegerUnsigned xs)
 readInteger xs = readIntegerUnsigned xs
+
+readNatural :: String -> Maybe (Natural, String)
+readNatural xs = case readIntegerUnsigned xs of
+  Just (y, zs)
+    | y >= 0 -> Just (fromIntegral @Integer @Natural y, zs)
+  _          -> Nothing
 
 readIntegerUnsigned :: String -> Maybe (Integer, String)
 readIntegerUnsigned xs = case readMaybe ys of
