diff --git a/System/OsString.hs b/System/OsString.hs
--- a/System/OsString.hs
+++ b/System/OsString.hs
@@ -21,6 +21,7 @@
 
   -- * OsString construction
   , encodeUtf
+  , unsafeEncodeUtf
   , encodeWith
   , encodeFS
   , osstr
@@ -130,6 +131,7 @@
     ( unsafeFromChar
     , toChar
     , encodeUtf
+    , unsafeEncodeUtf
     , encodeWith
     , encodeFS
     , osstr
diff --git a/System/OsString/Common.hs b/System/OsString/Common.hs
--- a/System/OsString/Common.hs
+++ b/System/OsString/Common.hs
@@ -3,6 +3,8 @@
 {-# LANGUAGE PatternSynonyms #-}
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskellQuotes #-}
+{-# LANGUAGE ViewPatterns #-}
 {-# OPTIONS_GHC -Wno-unused-imports #-}
 
 -- This template expects CPP definitions for:
@@ -28,6 +30,7 @@
 
   -- * String construction
   , encodeUtf
+  , unsafeEncodeUtf
   , encodeWith
   , encodeFS
   , fromBytes
@@ -175,7 +178,7 @@
 import qualified System.OsString.Data.ByteString.Short as BSP
 #endif
 import GHC.Stack (HasCallStack)
-import Prelude (Bool, Int, Maybe(..), IO, String, Either(..), fmap, ($), (.), mconcat, fromEnum, fromInteger, mempty, fromIntegral, fail, (<$>), show, either, pure, const, flip)
+import Prelude (Bool(..), Int, Maybe(..), IO, String, Either(..), fmap, ($), (.), mconcat, fromEnum, fromInteger, mempty, fromIntegral, fail, (<$>), show, either, pure, const, flip, error, id)
 import Data.Bifunctor ( bimap )
 import qualified System.OsString.Data.ByteString.Short.Word16 as BS16
 import qualified System.OsString.Data.ByteString.Short as BS8
@@ -187,13 +190,15 @@
 --
 -- This encodes as UTF16-LE (strictly), which is a pretty good guess.
 --
--- Throws an 'EncodingException' if encoding fails.
+-- Throws an 'EncodingException' if encoding fails. If the input does not
+-- contain surrogate chars, you can use @unsafeEncodeUtf@.
 #else
 -- | Partial unicode friendly encoding.
 --
 -- This encodes as UTF8 (strictly), which is a good guess.
 --
--- Throws an 'EncodingException' if encoding fails.
+-- Throws an 'EncodingException' if encoding fails. If the input does not
+-- contain surrogate chars, you can use 'unsafeEncodeUtf'.
 #endif
 encodeUtf :: MonadThrow m => String -> m PLATFORM_STRING
 #ifdef WINDOWS
@@ -202,6 +207,17 @@
 encodeUtf = either throwM pure . encodeWith utf8
 #endif
 
+-- | Unsafe unicode friendly encoding.
+--
+-- Like 'encodeUtf', except it crashes when the input contains
+-- surrogate chars. For sanitized input, this can be useful.
+unsafeEncodeUtf :: HasCallStack => String -> PLATFORM_STRING
+#ifdef WINDOWS
+unsafeEncodeUtf = either (error . displayException) id . encodeWith utf16le
+#else
+unsafeEncodeUtf = either (error . displayException) id . encodeWith utf8
+#endif
+
 -- | Encode a 'String' with the specified encoding.
 encodeWith :: TextEncoding
            -> String
@@ -345,23 +361,25 @@
   { quoteExp = \s -> do
       ps <- either (fail . show) pure $ encodeWith (mkUTF16le ErrorOnCodingFailure) s
       lift ps
-  , quotePat  = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a pattern)"
+  , quotePat = \s -> do
+      osp' <- either (fail . show) pure . encodeWith (mkUTF16le ErrorOnCodingFailure) $ s
+      [p|((==) osp' -> True)|]
   , quoteType = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a type)"
+      fail "illegal QuasiQuote (allowed as expression or pattern only, used as a type)"
   , quoteDec  = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a declaration)"
+      fail "illegal QuasiQuote (allowed as expression or pattern only, used as a declaration)"
   }
 #else
   { quoteExp = \s -> do
       ps <- either (fail . show) pure $ encodeWith (mkUTF8 ErrorOnCodingFailure) s
       lift ps
-  , quotePat  = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a pattern)"
+  , quotePat = \s -> do
+      osp' <- either (fail . show) pure . encodeWith (mkUTF8 ErrorOnCodingFailure) $ s
+      [p|((==) osp' -> True)|]
   , quoteType = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a type)"
+      fail "illegal QuasiQuote (allowed as expression or pattern only, used as a type)"
   , quoteDec  = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a declaration)"
+      fail "illegal QuasiQuote (allowed as expression or pattern only, used as a declaration)"
   }
 #endif
 
diff --git a/System/OsString/Data/ByteString/Short/Internal.hs b/System/OsString/Data/ByteString/Short/Internal.hs
--- a/System/OsString/Data/ByteString/Short/Internal.hs
+++ b/System/OsString/Data/ByteString/Short/Internal.hs
@@ -6,6 +6,9 @@
 {-# LANGUAGE ViewPatterns #-}
 {-# LANGUAGE UnliftedFFITypes         #-}
 
+-- Required for WORDS_BIGENDIAN
+#include <ghcautoconf.h>
+
 -- |
 -- Module      :  System.OsString.Data.ByteString.Short.Internal
 -- Copyright   :  © 2022 Julian Ospald
@@ -21,7 +24,6 @@
 
 import Control.Monad.ST
 import Control.Exception (assert, throwIO)
-import Data.Bits (Bits(..))
 import Data.ByteString.Short.Internal (ShortByteString(..), length)
 #if !MIN_VERSION_base(4,11,0)
 import Data.Semigroup
@@ -282,64 +284,38 @@
       go mba (i - 2) ws
 
 
--- | This isn't strictly Word16 array write. Instead it's two consecutive Word8 array
--- writes to avoid endianness issues due to primops doing automatic alignment based
--- on host platform. We want to always write LE to the byte array.
+-- | Encode Word16 as little-endian.
 writeWord16Array :: MBA s
                  -> Int      -- ^ Word8 index (not Word16)
                  -> Word16
                  -> ST s ()
-writeWord16Array (MBA# mba#) (I# i#) (W16# w#) =
-  case encodeWord16LE# w# of
-    (# lsb#, msb# #) ->
-      ST (\s -> case writeWord8Array# mba# i# lsb# s of
-          s' -> (# s', () #)) >>
-      ST (\s -> case writeWord8Array# mba# (i# +# 1#) msb# s of
-          s' -> (# s', () #))
+writeWord16Array (MBA# mba#) (I# i#) (W16# w#) = ST $ \s ->
+  case writeWord8ArrayAsWord16# mba# i# (word16ToLE# w#) s of
+    s' -> (# s', () #)
 
 indexWord8Array :: BA
                 -> Int      -- ^ Word8 index
                 -> Word8
 indexWord8Array (BA# ba#) (I# i#) = W8# (indexWord8Array# ba# i#)
 
--- | This isn't strictly Word16 array read. Instead it's two Word8 array reads
--- to avoid endianness issues due to primops doing automatic alignment based
--- on host platform. We expect the byte array to be LE always.
+-- | Decode Word16 from little-endian.
 indexWord16Array :: BA
                  -> Int      -- ^ Word8 index (not Word16)
                  -> Word16
-indexWord16Array ba i = fromIntegral lsb .|. (fromIntegral msb `shiftL` 8)
-  where
-    lsb = indexWord8Array ba i
-    msb = indexWord8Array ba (i + 1)
-
-#if !MIN_VERSION_base(4,16,0)
-
-encodeWord16LE# :: Word#              -- ^ Word16
-                -> (# Word#, Word# #) -- ^ Word8 (LSB, MSB)
-encodeWord16LE# x# = (# x# `and#` int2Word# 0xff#
-                     ,  x# `and#` int2Word# 0xff00# `shiftRL#` 8# #)
-
-decodeWord16LE# :: (# Word#, Word# #) -- ^ Word8 (LSB, MSB)
-                -> Word#              -- ^ Word16
-decodeWord16LE# (# lsb#, msb# #) = msb# `shiftL#` 8# `or#` lsb#
+indexWord16Array (BA# ba#) (I# i#) =
+  W16# (word16FromLE# (indexWord8ArrayAsWord16# ba# i#))
 
+#if MIN_VERSION_base(4,16,0)
+word16ToLE#, word16FromLE# :: Word16# -> Word16#
 #else
-
-encodeWord16LE# :: Word16#              -- ^ Word16
-                -> (# Word8#, Word8# #) -- ^ Word8 (LSB, MSB)
-encodeWord16LE# x# = (# word16ToWord8# x#
-                     ,  word16ToWord8# (x# `uncheckedShiftRLWord16#` 8#) #)
-  where
-    word16ToWord8# y = wordToWord8# (word16ToWord# y)
-
-decodeWord16LE# :: (# Word8#, Word8# #) -- ^ Word8 (LSB, MSB)
-                -> Word16#              -- ^ Word16
-decodeWord16LE# (# lsb#, msb# #) = ((word8ToWord16# msb# `uncheckedShiftLWord16#` 8#) `orWord16#` word8ToWord16# lsb#)
-  where
-    word8ToWord16# y = wordToWord16# (word8ToWord# y)
-
+word16ToLE#, word16FromLE# :: Word# -> Word#
 #endif
+#ifdef WORDS_BIGENDIAN
+word16ToLE# = byteSwap16#
+#else
+word16ToLE# w# = w#
+#endif
+word16FromLE# = word16ToLE#
 
 setByteArray :: MBA s -> Int -> Int -> Int -> ST s ()
 setByteArray (MBA# dst#) (I# off#) (I# len#) (I# c#) =
diff --git a/System/OsString/Internal.hs b/System/OsString/Internal.hs
--- a/System/OsString/Internal.hs
+++ b/System/OsString/Internal.hs
@@ -3,6 +3,8 @@
 {-# LANGUAGE UnliftedFFITypes #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TemplateHaskellQuotes #-}
+{-# LANGUAGE ViewPatterns #-}  -- needed to quote a view pattern
 
 module System.OsString.Internal where
 
@@ -40,10 +42,18 @@
 -- On windows this encodes as UTF16-LE (strictly), which is a pretty good guess.
 -- On unix this encodes as UTF8 (strictly), which is a good guess.
 --
--- Throws a 'EncodingException' if encoding fails.
+-- Throws an 'EncodingException' if encoding fails. If the input does not
+-- contain surrogate chars, you can use 'unsafeEncodeUtf'.
 encodeUtf :: MonadThrow m => String -> m OsString
 encodeUtf = fmap OsString . PF.encodeUtf
 
+-- | Unsafe unicode friendly encoding.
+--
+-- Like 'encodeUtf', except it crashes when the input contains
+-- surrogate chars. For sanitized input, this can be useful.
+unsafeEncodeUtf :: HasCallStack => String -> OsString
+unsafeEncodeUtf = OsString . PF.unsafeEncodeUtf
+
 -- | Encode an 'OsString' given the platform specific encodings.
 encodeWith :: TextEncoding  -- ^ unix text encoding
            -> TextEncoding  -- ^ windows text encoding
@@ -122,6 +132,7 @@
 
 -- | QuasiQuote an 'OsString'. This accepts Unicode characters
 -- and encodes as UTF-8 on unix and UTF-16 on windows.
+-- If used as pattern, requires turning on the @ViewPatterns@ extension.
 osstr :: QuasiQuoter
 osstr =
   QuasiQuoter
@@ -129,23 +140,25 @@
   { quoteExp = \s -> do
       osp <- either (fail . show) (pure . OsString) . PF.encodeWith (mkUTF16le ErrorOnCodingFailure) $ s
       lift osp
-  , quotePat  = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a pattern)"
+  , quotePat = \s -> do
+      osp' <- either (fail . show) (pure . OsString) . PF.encodeWith (mkUTF16le ErrorOnCodingFailure) $ s
+      [p|((==) osp' -> True)|]
   , quoteType = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a type)"
+      fail "illegal QuasiQuote (allowed as expression or pattern only, used as a type)"
   , quoteDec  = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a declaration)"
+      fail "illegal QuasiQuote (allowed as expression or pattern only, used as a declaration)"
   }
 #else
   { quoteExp = \s -> do
       osp <- either (fail . show) (pure . OsString) . PF.encodeWith (mkUTF8 ErrorOnCodingFailure) $ s
       lift osp
-  , quotePat  = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a pattern)"
+  , quotePat = \s -> do
+      osp' <- either (fail . show) (pure . OsString) . PF.encodeWith (mkUTF8 ErrorOnCodingFailure) $ s
+      [p|((==) osp' -> True)|]
   , quoteType = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a type)"
+      fail "illegal QuasiQuote (allowed as expression or pattern only, used as a type)"
   , quoteDec  = \_ ->
-      fail "illegal QuasiQuote (allowed as expression only, used as a declaration)"
+      fail "illegal QuasiQuote (allowed as expression or pattern only, used as a declaration)"
   }
 #endif
 
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,6 +1,15 @@
 # Changelog for [`os-string` package](http://hackage.haskell.org/package/os-string)
 
-## 1.0.0 *??? 2023*
+## 2.0.1 *Dec 2023*
 
-* Split out `OsString` from filepath package
+* add `unsafeEncodeUtf`, fixes #5
+
+## 2.0.0 *Nov 2023*
+
+* Split out `OsString` modules from filepath library
+* add more bytestring like functions (index/search/etc.)
+
+## 1.0.0 *Nov 2023*
+
+* dummy release to avoid name clashes with filepath <1.5
 
diff --git a/os-string.cabal b/os-string.cabal
--- a/os-string.cabal
+++ b/os-string.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               os-string
-version:            2.0.0
+version:            2.0.1
 
 -- NOTE: Don't forget to update ./changelog.md
 license:            BSD-3-Clause
@@ -16,14 +16,14 @@
 build-type:         Simple
 synopsis:           Library for manipulating Operating system strings.
 tested-with:
-  GHC ==8.0.2
-   || ==8.2.2
-   || ==8.4.4
-   || ==8.6.5
+  GHC ==8.6.5
    || ==8.8.4
    || ==8.10.7
    || ==9.0.2
-   || ==9.2.3
+   || ==9.2.8
+   || ==9.4.8
+   || ==9.6.3
+   || ==9.8.1
 
 description:
   This package provides functionality for manipulating @OsString@ values, and is shipped with <https://www.haskell.org/ghc/ GHC>.
@@ -63,7 +63,7 @@
 
   default-language: Haskell2010
   build-depends:
-    , base              >=4.9      && <4.20
+    , base              >=4.12.0.0      && <4.20
     , bytestring        >=0.11.3.0
     , deepseq
     , exceptions
@@ -90,6 +90,25 @@
     , bytestring  >=0.11.3.0
     , os-string
     , QuickCheck  >=2.7      && <2.15
+
+test-suite encoding-tests
+  default-language: Haskell2010
+  ghc-options:      -Wall
+  type:             exitcode-stdio-1.0
+  main-is:          Main.hs
+  hs-source-dirs:   tests tests/encoding
+  other-modules:
+    Arbitrary
+    EncodingSpec
+    TestUtil
+
+  build-depends:
+    , base
+    , bytestring  >=0.11.3.0
+    , deepseq
+    , os-string
+    , QuickCheck  >=2.7      && <2.15
+    , quickcheck-classes-base ^>=0.6.2
 
 benchmark bench
   main-is:          Bench.hs
diff --git a/tests/Arbitrary.hs b/tests/Arbitrary.hs
new file mode 100644
--- /dev/null
+++ b/tests/Arbitrary.hs
@@ -0,0 +1,69 @@
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module Arbitrary where
+
+import Data.Char
+import Data.Maybe
+import System.OsString
+import System.OsString.Internal.Types
+import qualified System.OsString.Posix as Posix
+import qualified System.OsString.Windows as Windows
+import Data.ByteString ( ByteString )
+import qualified Data.ByteString as ByteString
+import Test.QuickCheck
+
+
+instance Arbitrary OsString where
+  arbitrary = fmap fromJust $ encodeUtf <$> listOf filepathChar
+
+instance Arbitrary PosixString where
+  arbitrary = fmap fromJust $ Posix.encodeUtf <$> listOf filepathChar
+
+instance Arbitrary WindowsString where
+  arbitrary = fmap fromJust $ Windows.encodeUtf <$> listOf filepathChar
+
+
+newtype NonNullString = NonNullString { nonNullString :: String }
+  deriving Show
+
+instance Arbitrary NonNullString where
+  arbitrary = NonNullString <$> listOf filepathChar
+
+filepathChar :: Gen Char
+filepathChar = arbitraryUnicodeChar `suchThat` (\c -> not (isNull c) && isValidUnicode c)
+ where
+  isNull = (== '\NUL')
+  isValidUnicode c = case generalCategory c of
+      Surrogate -> False
+      NotAssigned -> False
+      _ -> True
+
+
+newtype NonNullAsciiString = NonNullAsciiString { nonNullAsciiString :: String }
+  deriving Show
+
+instance Arbitrary NonNullAsciiString where
+  arbitrary = NonNullAsciiString <$> listOf filepathAsciiChar
+
+filepathAsciiChar :: Gen Char
+filepathAsciiChar = arbitraryASCIIChar `suchThat` (\c -> not (isNull c))
+ where
+  isNull = (== '\NUL')
+
+newtype NonNullSurrogateString = NonNullSurrogateString { nonNullSurrogateString :: String }
+  deriving Show
+
+instance Arbitrary NonNullSurrogateString where
+  arbitrary = NonNullSurrogateString <$> listOf filepathWithSurrogates
+
+filepathWithSurrogates :: Gen Char
+filepathWithSurrogates =
+  frequency
+    [(3, arbitraryASCIIChar),
+     (1, arbitraryUnicodeChar),
+     (1, arbitraryBoundedEnum)
+    ]
+
+
+instance Arbitrary ByteString where arbitrary = ByteString.pack <$> arbitrary
+instance CoArbitrary ByteString where coarbitrary = coarbitrary . ByteString.unpack
diff --git a/tests/encoding/EncodingSpec.hs b/tests/encoding/EncodingSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/encoding/EncodingSpec.hs
@@ -0,0 +1,170 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE ViewPatterns #-}
+{-# LANGUAGE TypeApplications #-}
+
+module EncodingSpec where
+
+import Data.ByteString ( ByteString )
+import qualified Data.ByteString as BS
+
+import Arbitrary
+import Test.QuickCheck
+
+import Data.Either ( isRight )
+import qualified System.OsString.Data.ByteString.Short as BS8
+import qualified System.OsString.Data.ByteString.Short.Word16 as BS16
+import System.OsString.Encoding.Internal
+import GHC.IO (unsafePerformIO)
+import GHC.IO.Encoding ( setFileSystemEncoding )
+import System.IO
+    ( utf16le )
+import Control.Exception
+import Control.DeepSeq
+import Data.Bifunctor ( first )
+import GHC.IO.Encoding.Failure ( CodingFailureMode(..) )
+import GHC.IO.Encoding.UTF16 ( mkUTF16le )
+import GHC.IO.Encoding.UTF8 ( mkUTF8 )
+
+
+tests :: [(String, Property)]
+tests =
+  [ ("ucs2le_decode . ucs2le_encode == id",
+    property $ \(padEven -> ba) ->
+      let decoded = decodeWithTE ucs2le (BS8.toShort ba)
+          encoded = encodeWithTE ucs2le =<< decoded
+      in (BS8.fromShort <$> encoded) === Right ba)
+  , ("utf16 doesn't handle invalid surrogate pairs",
+     property $
+      let str = [toEnum 55296, toEnum 55297]
+          encoded = encodeWithTE utf16le str
+          decoded = decodeWithTE utf16le =<< encoded
+#if __GLASGOW_HASKELL__ >= 904
+      in decoded === Left (EncodingError ("recoverEncode: invalid argument (cannot encode character " <> show (head str) <> ")") Nothing))
+#else
+      in decoded === Left (EncodingError "recoverEncode: invalid argument (invalid character)" Nothing))
+#endif
+  , ("ucs2 handles invalid surrogate pairs",
+     property $
+      let str = [toEnum 55296, toEnum 55297]
+          encoded = encodeWithTE ucs2le str
+          decoded = decodeWithTE ucs2le =<< encoded
+      in decoded === Right str)
+  , ("can roundtrip arbitrary bytes through utf-8 (with RoundtripFailure)",
+     property $
+      \bs ->
+        let decoded = decodeWithTE (mkUTF8 RoundtripFailure) (BS8.toShort bs)
+            encoded = encodeWithTE (mkUTF8 RoundtripFailure) =<< decoded
+        in (either (const 0) BS8.length encoded, encoded) === (BS8.length (BS8.toShort bs), Right (BS8.toShort bs)))
+
+  , ("can decode arbitrary strings through utf-8 (with RoundtripFailure)",
+     property $
+      \(NonNullSurrogateString str) ->
+        let encoded = encodeWithTE (mkUTF8 RoundtripFailure) str
+            decoded = decodeWithTE (mkUTF8 RoundtripFailure) =<< encoded
+        in expectFailure $ (either (const 0) length decoded, decoded) === (length str, Right str))
+
+  , ("utf-8 roundtrip encode cannot deal with some surrogates",
+     property $
+      let str = [toEnum 0xDFF0, toEnum 0xDFF2]
+          encoded = encodeWithTE (mkUTF8 RoundtripFailure) str
+          decoded = decodeWithTE (mkUTF8 RoundtripFailure) =<< encoded
+#if __GLASGOW_HASKELL__ >= 904
+      in decoded === Left (EncodingError ("recoverEncode: invalid argument (cannot encode character " <> show (head str) <> ")") Nothing))
+#else
+      in decoded === Left (EncodingError "recoverEncode: invalid argument (invalid character)" Nothing))
+#endif
+
+  , ("cannot roundtrip arbitrary bytes through utf-16 (with RoundtripFailure)",
+     property $
+      \(padEven -> bs) ->
+        let decoded = decodeWithTE (mkUTF16le RoundtripFailure) (BS8.toShort bs)
+            encoded = encodeWithTE (mkUTF16le RoundtripFailure) =<< decoded
+        in expectFailure $ (either (const 0) BS8.length encoded, encoded) === (BS8.length (BS8.toShort bs), Right (BS8.toShort bs)))
+  , ("encodeWithTE/decodeWithTE ErrorOnCodingFailure fails (utf16le)",
+     property $
+      \(padEven -> bs) ->
+        let decoded = decodeWithTE (mkUTF16le ErrorOnCodingFailure) (BS8.toShort bs)
+            encoded = encodeWithTE (mkUTF16le ErrorOnCodingFailure) =<< decoded
+        in expectFailure $ (isRight encoded, isRight decoded) === (True, True))
+  , ("encodeWithTE/decodeWithTE ErrorOnCodingFailure fails (utf8)",
+     property $
+      \bs ->
+        let decoded = decodeWithTE (mkUTF8 ErrorOnCodingFailure) (BS8.toShort bs)
+            encoded = encodeWithTE (mkUTF8 ErrorOnCodingFailure) =<< decoded
+        in expectFailure $ (isRight encoded, isRight decoded) === (True, True))
+  , ("encodeWithTE/decodeWithTE TransliterateCodingFailure never fails (utf16le)",
+     property $
+      \(padEven -> bs) ->
+        let decoded = decodeWithTE (mkUTF16le TransliterateCodingFailure) (BS8.toShort bs)
+            encoded = encodeWithTE (mkUTF16le TransliterateCodingFailure) =<< decoded
+        in (isRight encoded, isRight decoded) === (True, True))
+  , ("encodeWithTE/decodeWithTE TransliterateCodingFailure never fails (utf8)",
+     property $
+      \bs ->
+        let decoded = decodeWithTE (mkUTF8 TransliterateCodingFailure) (BS8.toShort bs)
+            encoded = encodeWithTE (mkUTF8 TransliterateCodingFailure) =<< decoded
+        in (isRight encoded, isRight decoded) === (True, True))
+  , ("encodeWithBaseWindows/decodeWithBaseWindows never fails (utf16le)",
+     property $
+      \(padEven -> bs) ->
+        let decoded = decodeW' (BS8.toShort bs)
+            encoded = encodeW' =<< decoded
+        in (isRight encoded, isRight decoded) === (True, True))
+  , ("encodeWithBasePosix/decodeWithBasePosix never fails (utf8b)",
+     property $
+      \bs -> ioProperty $ do
+        setFileSystemEncoding (mkUTF8 TransliterateCodingFailure)
+        let decoded = decodeP' (BS8.toShort bs)
+            encoded = encodeP' =<< decoded
+        pure $ (isRight encoded, isRight decoded) === (True, True))
+
+  , ("decodeWithBaseWindows == utf16le_b",
+     property $
+      \(BS8.toShort . padEven -> bs) ->
+        let decoded  = decodeW' bs
+            decoded' = first displayException $ decodeWithTE (mkUTF16le_b ErrorOnCodingFailure) bs
+        in decoded === decoded')
+
+  , ("encodeWithBaseWindows == utf16le_b",
+     property $
+      \(NonNullSurrogateString str) ->
+        let decoded  = encodeW' str
+            decoded' = first displayException $ encodeWithTE (mkUTF16le_b ErrorOnCodingFailure) str
+        in decoded === decoded')
+
+  , ("encodeWithTE/decodeWithTE never fails (utf16le_b)",
+     property $
+      \(padEven -> bs) ->
+        let decoded = decodeWithTE (mkUTF16le_b ErrorOnCodingFailure) (BS8.toShort bs)
+            encoded = encodeWithTE (mkUTF16le_b ErrorOnCodingFailure) =<< decoded
+        in (isRight encoded, isRight decoded) === (True, True))
+  ]
+
+
+padEven :: ByteString -> ByteString
+padEven bs
+  | even (BS.length bs) = bs
+  | otherwise = bs `BS.append` BS.pack [70]
+
+
+decodeP' :: BS8.ShortByteString -> Either String String
+decodeP' ba = unsafePerformIO $ do
+  r <- try @SomeException $ decodeWithBasePosix ba
+  evaluate $ force $ first displayException r
+
+encodeP' :: String -> Either String BS8.ShortByteString
+encodeP' str = unsafePerformIO $ do
+  r <- try @SomeException $ encodeWithBasePosix str
+  evaluate $ force $ first displayException r
+
+decodeW' :: BS16.ShortByteString -> Either String String
+decodeW' ba = unsafePerformIO $ do
+  r <- try @SomeException $ decodeWithBaseWindows ba
+  evaluate $ force $ first displayException r
+
+encodeW' :: String -> Either String BS8.ShortByteString
+encodeW' str = unsafePerformIO $ do
+  r <- try @SomeException $ encodeWithBaseWindows str
+  evaluate $ force $ first displayException r
+
diff --git a/tests/encoding/Main.hs b/tests/encoding/Main.hs
new file mode 100644
--- /dev/null
+++ b/tests/encoding/Main.hs
@@ -0,0 +1,10 @@
+{-# LANGUAGE TypeApplications #-}
+
+module Main (main) where
+
+import qualified EncodingSpec as Spec
+import TestUtil
+
+main :: IO ()
+main = runTests (Spec.tests)
+
