diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,9 @@
 # Revision history for Z-Data
 
+## 0.1.7.1  -- 2020-10-17
+
+* Add `singleton` to `Z.IO.CBytes`, fix `unpack` when there're trailing illegal UTF8 codepoints.
+
 ## 0.1.7.0  -- 2020-10-15
 
 * Change `defaultChunkSize` from 32K to 16K to reduce memory overhead.
diff --git a/Z-Data.cabal b/Z-Data.cabal
--- a/Z-Data.cabal
+++ b/Z-Data.cabal
@@ -1,6 +1,6 @@
 cabal-version:              2.4
 name:                       Z-Data
-version:                    0.1.7.0
+version:                    0.1.7.1
 synopsis:                   Array, vector and text
 description:                This package provides array, slice and text operations
 license:                    BSD-3-Clause
diff --git a/Z/Data/CBytes.hs b/Z/Data/CBytes.hs
--- a/Z/Data/CBytes.hs
+++ b/Z/Data/CBytes.hs
@@ -19,8 +19,8 @@
   , toBytes, fromBytes, toText, toTextMaybe, fromText, toBuilder, buildCBytes
   , pack
   , unpack
-  , null , length
-  , empty, append, concat, intercalate, intercalateElem
+  , null, length
+  , empty, singleton, append, concat, intercalate, intercalateElem
   , fromCString, fromCStringN
   , withCBytesUnsafe, withCBytes, allocCBytesUnsafe, allocCBytes
   -- * re-export
@@ -59,6 +59,7 @@
 import qualified Z.Data.Builder        as B
 import qualified Z.Data.Text           as T
 import qualified Z.Data.Text.ShowT     as T
+import qualified Z.Data.Text.UTF8Codec as T
 import qualified Z.Data.JSON.Base      as JSON
 import           Z.Data.JSON.Base      ((<?>))
 import           Z.Data.Text.UTF8Codec (encodeCharModifiedUTF8, decodeChar)
@@ -112,6 +113,7 @@
 
 -- | Use this pattern to match or construct 'CBytes', result will be trimmed down to first @\\NUL@ byte if there's any.
 pattern CB :: V.Bytes -> CBytes
+{-# COMPLETE CB #-}
 pattern CB bs <- (toBytes -> bs) where
     CB bs = fromBytes bs
 
@@ -228,6 +230,7 @@
         Just t -> JSON.encodeJSON t
         Nothing -> B.square . JSON.commaVec' . toBytes $ cbytes
 
+-- | Concatenate two 'CBytes'.
 append :: CBytes -> CBytes -> CBytes
 {-# INLINABLE append #-}
 append strA@(CBytes pa) strB@(CBytes pb)
@@ -244,11 +247,23 @@
     lenA = length strA
     lenB = length strB
 
--- | An empty 'CBytes'
+-- | Empty 'CBytes'
 empty :: CBytes
 {-# NOINLINE empty #-}
 empty = CBytes (V.singleton 0)
 
+-- | Singleton 'CBytes'.
+singleton :: Word8 -> CBytes
+{-# INLINE singleton #-}
+singleton w = runST (do
+    buf <- newPrimArray 2
+    writePrimArray buf 0 w
+    writePrimArray buf 1 0
+    pa <- unsafeFreezePrimArray buf
+    return (CBytes pa))
+
+-- | /O(n)/ Concatenate a list of 'CBytes'.
+--
 concat :: [CBytes] -> CBytes
 {-# INLINABLE concat #-}
 concat bss = case pre 0 0 bss of
@@ -385,6 +400,7 @@
     !end = sizeofPrimArray arr - 1
     go !idx
         | idx >= end = []
+        | idx + T.decodeCharLen arr idx > end = [T.replacementChar]
         | otherwise = let (# c, i #) = decodeChar arr idx in c : go (idx + i)
 
 unpackFB :: CBytes -> (Char -> a -> a) -> a -> a
@@ -394,6 +410,7 @@
     !end = sizeofPrimArray arr - 1
     go !idx
         | idx >= end = z
+        | idx + T.decodeCharLen arr idx > end = T.replacementChar `k` z
         | otherwise = let (# c, i #) = decodeChar arr idx in c `k` go (idx + i)
 
 {-# RULES
@@ -409,7 +426,7 @@
 {-# INLINE null #-}
 null (CBytes pa) = indexPrimArray pa 0 == 0
 
--- | Return the BTYE length of 'CBytes'.
+-- | /O(1)/, Return the BTYE length of 'CBytes'.
 --
 length :: CBytes -> Int
 {-# INLINE length #-}
