diff --git a/Codec/Archive/Zip/Conduit/Internal.hs b/Codec/Archive/Zip/Conduit/Internal.hs
--- a/Codec/Archive/Zip/Conduit/Internal.hs
+++ b/Codec/Archive/Zip/Conduit/Internal.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE CPP #-}
+{-# LANGUAGE BangPatterns #-}
 module Codec.Archive.Zip.Conduit.Internal
   ( osVersion, zipVersion
   , zipError
@@ -42,14 +43,14 @@
 idConduit = C.awaitForever C.yield
 
 passthroughFold :: Monad m => (a -> b -> a) -> a -> C.ConduitM b b m a
-passthroughFold f z = C.await >>= maybe
+passthroughFold f !z = C.await >>= maybe
   (return z)
   (\x -> do
     C.yield x
     passthroughFold f (f z x))
 
 sizeCRC :: Monad m => C.ConduitM BS.ByteString BS.ByteString m (Word64, Word32)
-sizeCRC = passthroughFold (\(l, c) b -> (l + fromIntegral (BS.length b), crc32Update c b)) (0, 0)
+sizeCRC = passthroughFold (\(!l, !c) b -> (l + fromIntegral (BS.length b), crc32Update c b)) (0, 0)
 
 sizeC :: Monad m => C.ConduitM BS.ByteString BS.ByteString m Word64
 sizeC = passthroughFold (\l b -> l + fromIntegral (BS.length b)) 0 -- fst <$> sizeCRC
diff --git a/Codec/Archive/Zip/Conduit/Types.hs b/Codec/Archive/Zip/Conduit/Types.hs
--- a/Codec/Archive/Zip/Conduit/Types.hs
+++ b/Codec/Archive/Zip/Conduit/Types.hs
@@ -7,9 +7,10 @@
 import           Data.Conduit.Binary (sourceLbs)
 import           Data.Semigroup (Semigroup(..))
 import           Data.String (IsString(..))
+import qualified Data.Text as T
 import           Data.Time.LocalTime (LocalTime)
 import           Data.Typeable (Typeable)
-import           Data.Word (Word64)
+import           Data.Word (Word32, Word64)
 
 -- |Errors thrown during zip file processing
 newtype ZipError = ZipError String
@@ -29,9 +30,10 @@
 -- |(The beginning of) a single entry in a zip stream, which may be any file or directory.
 -- As per zip file conventions, directory names should end with a slash and have no data, but this library does not ensure that.
 data ZipEntry = ZipEntry
-  { zipEntryName :: ByteString -- ^File name (in posix format, no leading slashes), usually utf-8 encoded, with a trailing slash for directories
+  { zipEntryName :: Either T.Text ByteString -- ^File name (in posix format, no leading slashes), either UTF-8 encoded text or raw bytes (CP437), with a trailing slash for directories
   , zipEntryTime :: LocalTime -- ^Modification time
   , zipEntrySize :: Maybe Word64 -- ^Size of file data (if known); checked on zipping and also used as hint to enable zip64
+  , zipEntryExternalAttributes :: Maybe Word32 -- ^Host-dependent attributes, often MS-DOS directory attribute byte (only supported when zipping)
   } deriving (Eq, Show)
 
 -- |The data contents for a 'ZipEntry'. For empty entries (e.g., directories), use 'mempty'.
diff --git a/Codec/Archive/Zip/Conduit/UnZip.hs b/Codec/Archive/Zip/Conduit/UnZip.hs
--- a/Codec/Archive/Zip/Conduit/UnZip.hs
+++ b/Codec/Archive/Zip/Conduit/UnZip.hs
@@ -23,6 +23,8 @@
 import qualified Data.Conduit.List as CL
 import           Data.Conduit.Serialization.Binary (sinkGet)
 import qualified Data.Conduit.Zlib as CZ
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as TE
 import           Data.Time (LocalTime(..), TimeOfDay(..), fromGregorian)
 import           Data.Word (Word16, Word32, Word64)
 
@@ -136,7 +138,7 @@
               -- optional data description (possibly ambiguous!)
               sinkGet $ (guard =<< dataDesc h) <|> return ()
               return (size == usize && crc == fileCRC)
-        unless r $ zipError $ BSC.unpack (zipEntryName fileEntry) ++ ": data integrity check failed"
+        unless r $ zipError $ either T.unpack BSC.unpack (zipEntryName fileEntry) ++ ": data integrity check failed"
         next
       EndOfCentralDirectory{..} -> do
         return endInfo
@@ -166,7 +168,7 @@
     when (ver > zipVersion) $ fail $ "Unsupported version: " ++ show ver
     gpf <- G.getWord16le
     -- when (gpf .&. complement (bit 1 .|. bit 2 .|. bit 3) /= 0) $ fail $ "Unsupported flags: " ++ show gpf
-    when (gpf `clearBit` 1 `clearBit` 2 `clearBit` 3 /= 0) $ fail $ "Unsupported flags: " ++ show gpf
+    when (gpf `clearBit` 1 `clearBit` 2 `clearBit` 3 `clearBit` 11 /= 0) $ fail $ "Unsupported flags: " ++ show gpf
     comp <- G.getWord16le
     dcomp <- case comp of
       0 | testBit gpf 3 -> fail "Unsupported uncompressed streaming file data"
@@ -216,9 +218,10 @@
       }
     return FileHeader
       { fileEntry = ZipEntry
-        { zipEntryName = name
+        { zipEntryName = if testBit gpf 11 then Left (TE.decodeUtf8 name) else Right name
         , zipEntryTime = time
         , zipEntrySize = if testBit gpf 3 then Nothing else Just extZip64USize
+        , zipEntryExternalAttributes = Nothing
         }
       , fileDecompress = dcomp
       , fileCSize = extZip64CSize
diff --git a/Codec/Archive/Zip/Conduit/Zip.hs b/Codec/Archive/Zip/Conduit/Zip.hs
--- a/Codec/Archive/Zip/Conduit/Zip.hs
+++ b/Codec/Archive/Zip/Conduit/Zip.hs
@@ -35,6 +35,8 @@
 import           Data.Digest.CRC32 (crc32)
 import           Data.Either (isLeft)
 import           Data.Maybe (fromMaybe, fromJust)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as TE
 import           Data.Time (LocalTime(..), TimeOfDay(..), toGregorian)
 import           Data.Word (Word16, Word64)
 
@@ -127,12 +129,13 @@
           | otherwise = (left (fmap (id &&& fst)) sdat, usiz)
         z64 = maybe (zipOpt64 || any (maxBound32 <) zipEntrySize)
           (maxBound32 <) (max <$> usiz <*> csiz)
-        namelen = BS.length zipEntryName
+        name = either TE.encodeUtf8 id zipEntryName
+        namelen = BS.length name
         (time, date) = toDOSTime zipEntryTime
         mcrc = either (const Nothing) (Just . crc32) dat
-    when (namelen > maxBound16) $ zipError $ BSC.unpack zipEntryName ++ ": entry name too long"
+    when (namelen > maxBound16) $ zipError $ either T.unpack BSC.unpack zipEntryName ++ ": entry name too long"
     let common = do
-          P.putWord16le $ isLeft dat ?* bit 3
+          P.putWord16le $ isLeft dat ?* bit 3 .|. isLeft zipEntryName ?* bit 11
           P.putWord16le $ comp ?* 8
           P.putWord16le $ time
           P.putWord16le $ date
@@ -147,7 +150,7 @@
       P.putWord32le $ if z64 then maxBound32 else maybe 0 fromIntegral usiz
       P.putWord16le $ fromIntegral namelen
       P.putWord16le $ z64 ?* 20
-      P.putByteString zipEntryName
+      P.putByteString name
       when z64 $ do
         P.putWord16le 0x0001
         P.putWord16le 16
@@ -157,7 +160,7 @@
     ((usz, crc), csz) <- either
       (\cd -> do
         r@((usz, crc), csz) <- outsz cd -- write compressed data
-        when (not z64 && (usz > maxBound32 || csz > maxBound32)) $ zipError $ BSC.unpack zipEntryName ++ ": file too large and zipOpt64 disabled"
+        when (not z64 && (usz > maxBound32 || csz > maxBound32)) $ zipError $ either T.unpack BSC.unpack zipEntryName ++ ": file too large and zipOpt64 disabled"
         output $ do
           P.putWord32le 0x08074b50
           P.putWord32le crc
@@ -169,7 +172,7 @@
         return r)
       (\b -> outsz $ ((fromJust usiz, fromJust mcrc), fromJust csiz) <$ CB.sourceLbs b)
       cdat
-    when (any (usz /=) zipEntrySize) $ zipError $ BSC.unpack zipEntryName ++ ": incorrect zipEntrySize"
+    when (any (usz /=) zipEntrySize) $ zipError $ either T.unpack BSC.unpack zipEntryName ++ ": incorrect zipEntrySize"
     return $ do
       -- central directory
       let o64 = off >= maxBound32
@@ -189,9 +192,9 @@
       P.putWord16le 0 -- comment length
       P.putWord16le 0 -- disk number
       P.putWord16le 0 -- internal file attributes
-      P.putWord32le 0 -- external file attributes
+      P.putWord32le $ fromMaybe 0 zipEntryExternalAttributes
       P.putWord32le $ if o64 then maxBound32 else fromIntegral off
-      P.putByteString zipEntryName
+      P.putByteString name
       when a64 $ do
         P.putWord16le 0x0001
         P.putWord16le l64
diff --git a/cmd/unzip.hs b/cmd/unzip.hs
--- a/cmd/unzip.hs
+++ b/cmd/unzip.hs
@@ -6,6 +6,8 @@
 import qualified Data.ByteString.Char8 as BSC
 import qualified Data.Conduit as C
 import qualified Data.Conduit.Binary as CB
+import qualified Data.Text as T
+import qualified Data.Text.IO as TIO
 import           Data.Time.LocalTime (localTimeToUTC, utc)
 import           Data.Void (Void)
 import           System.Directory (createDirectoryIfMissing
@@ -23,9 +25,9 @@
 extract :: C.ConduitM (Either ZipEntry BS.ByteString) Void IO ()
 extract = C.awaitForever start where
   start (Left ZipEntry{..}) = do
-    liftIO $ BSC.putStrLn zipEntryName
+    liftIO $ either TIO.putStrLn BSC.putStrLn zipEntryName
     liftIO $ createDirectoryIfMissing True (takeDirectory name)
-    if BSC.last zipEntryName == '/'
+    if either T.last BSC.last zipEntryName == '/'
       then when ((0 /=) `any` zipEntrySize) $ fail $ name ++ ": non-empty directory"
       else do -- C.bracketP
         h <- liftIO $ openFile name WriteMode
@@ -35,7 +37,7 @@
 #if MIN_VERSION_directory(1,2,3)
     liftIO $ setModificationTime name $ localTimeToUTC utc zipEntryTime -- FIXME: timezone
 #endif
-    where name = BSC.unpack $ BSC.dropWhile ('/' ==) zipEntryName -- should we utf8 decode?
+    where name = either (T.unpack . T.dropWhile ('/' ==)) (BSC.unpack . BSC.dropWhile ('/' ==)) zipEntryName
   start (Right _) = fail "Unexpected leading or directory data contents"
   write = C.await >>= maybe
     (return ())
diff --git a/cmd/zip.hs b/cmd/zip.hs
--- a/cmd/zip.hs
+++ b/cmd/zip.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE CPP #-}
+import           Control.Arrow ((+++))
 import           Control.Monad (filterM, void)
 import           Control.Monad.IO.Class (MonadIO, liftIO)
 import           Control.Monad.Trans.Resource (MonadResource, runResourceT)
@@ -6,6 +7,7 @@
 import qualified Data.Conduit as C
 import qualified Data.Conduit.Binary as CB
 import           Data.List (foldl')
+import qualified Data.Text as T
 import           Data.Time.LocalTime (utcToLocalTime, utc)
 import qualified System.Console.GetOpt as Opt
 import           System.Directory (doesDirectoryExist, getModificationTime
@@ -43,9 +45,10 @@
 generate (p:paths) = do
   t <- liftIO $ getModificationTime p
   let e = ZipEntry
-        { zipEntryName = BSC.pack $ dropWhile ('/' ==) p
+        { zipEntryName = Right $ BSC.pack $ dropWhile ('/' ==) p
         , zipEntryTime = utcToLocalTime utc t -- FIXME: timezone
         , zipEntrySize = Nothing
+        , zipEntryExternalAttributes = Nothing
         }
   isd <- liftIO $ doesDirectoryExist p
   if isd
@@ -62,7 +65,7 @@
 #else
         filter (`notElem` [".",".."]) . map (p </>) <$> getDirectoryContents p
 #endif
-      C.yield (e{ zipEntryName = zipEntryName e `BSC.snoc` '/', zipEntrySize = Just 0 }, mempty)
+      C.yield (e{ zipEntryName = (`T.snoc` '/') +++ (`BSC.snoc` '/') $ zipEntryName e, zipEntrySize = Just 0 }, mempty)
       generate $ dl ++ paths
     else do
       C.yield (e, zipFileData p)
diff --git a/zip-stream.cabal b/zip-stream.cabal
--- a/zip-stream.cabal
+++ b/zip-stream.cabal
@@ -1,5 +1,5 @@
 name:                zip-stream
-version:             0.1.1
+version:             0.2.0.1
 synopsis:            ZIP archive streaming using conduits
 description:         Process (extract and create) zip files as streams (e.g., over the network), accessing contained files without having to write the zip file to disk (unlike zip-conduit).
 license:             BSD3
@@ -36,6 +36,7 @@
     mtl,
     primitive,
     resourcet,
+    text,
     time,
     transformers-base,
     zlib
@@ -52,6 +53,7 @@
     conduit-extra,
     directory,
     filepath,
+    text,
     time,
     transformers,
     zip-stream
@@ -69,6 +71,7 @@
     directory,
     filepath,
     resourcet,
+    text,
     time,
     transformers,
     zip-stream
