packages feed

streamly-zip (empty) → 0.0.1

raw patch · 13 files changed

+611/−0 lines, 13 filesdep +QuickCheckdep +basedep +base16-bytestringsetup-changedbinary-added

Dependencies added: QuickCheck, base, base16-bytestring, bytestring, containers, cryptohash, directory, filepath, streamly, streamly-core, streamly-zip, tar, tasty, tasty-hunit, tasty-quickcheck, temporary, zlib

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+## 0.0.1++* Initial release.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Shlok Datye (c) 2024++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Shlok Datye nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,36 @@+# streamly-zip++Stream data from zip archives using the Haskell [streamly](https://hackage.haskell.org/package/streamly) library.++## Comparison with streamly-archive++This library was created because libarchive (which [streamly-archive](https://hackage.haskell.org/package/streamly-archive) relies on) does not seem to have support for jumping to specific files even when the format supports it.++## Requirements++Install libzip on your system.++* Debian Linux: `sudo apt-get install libzip-dev`.+* macOS: `brew install libzip`.++## Quick start++```haskell+module Main where++import qualified Data.ByteString as B+import Data.Function+import qualified Streamly.Data.Fold as F+import qualified Streamly.Data.Stream.Prelude as S+import Streamly.External.Zip++main :: IO ()+main = do+  -- Obtain an archive.+  z <- openZip "/path/to/archive.zip" []++  -- Output a particular file to stdout.+  S.unfold unfoldFileAtPath (z, [], "file.txt")+    & S.mapM B.putStr+    & S.fold F.drain+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Streamly/External/Zip.hs view
@@ -0,0 +1,23 @@+-- The documentation in this module has at times been copy-pasted directly from the [libzip+-- documentation](https://libzip.org/documentation/).+module Streamly.External.Zip+  ( -- ** Open archive+    Zip,+    OpenFlag (..),+    openZip,++    -- ** Archive information+    getNumEntries,++    -- ** Paths+    PathFlag (..),+    getPathByIndex,++    -- ** Streamly+    GetFileFlag (..),+    unfoldFileAtPath,+    unfoldFileAtIndex,+  )+where++import Streamly.External.Zip.Internal
+ src/Streamly/External/Zip/Internal.hs view
@@ -0,0 +1,243 @@+{-# LANGUAGE TypeApplications #-}++module Streamly.External.Zip.Internal where++import Control.Exception+import Control.Monad+import Control.Monad.IO.Class+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import Data.List+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as M+import Data.Maybe+import Foreign+import Foreign.C.String+import Foreign.C.Types+import Streamly.Data.Unfold (Unfold)+import Streamly.External.Zip.Internal.Error+import Streamly.External.Zip.Internal.Foreign+import Streamly.Internal.Data.IOFinalizer+import qualified Streamly.Internal.Data.Unfold as U+import Text.Printf++-- | A zip archive.+newtype Zip+  = -- A ForeignPtr works because zip_discard() returns void.+    Zip (ForeignPtr Zip_t)++-- (*) Certain libzip functionality (e.g., flags) has been commented out because it is currently not+-- applicable for this library, e.g., because this library is currently read-only.++data OpenFlag+  = -- Most libzip flags not included; see (*).++    -- | Perform additional stricter consistency checks on the archive, and error if they fail.+    O_CHECKCONS+  deriving (Eq, Ord)++-- | /Internal/.+openFlags :: Map OpenFlag CInt+openFlags =+  M.fromList+    [ (O_CHECKCONS, zip_checkcons)+    -- (O_CREATE, zip_create), -- See (*).+    -- (O_EXCL, zip_excl),+    -- (O_TRUNCATE, zip_truncate)+    -- (O_RDONLY, zip_rdonly)+    ]++-- | Opens the zip archive at the given file path.+--+-- /Warning/: To satisfy low-level libzip requirements, please use each 'Zip' from one thread+-- only—or make sure to synchronize its use. Note that it is perfectly fine to open multiple 'Zip's+-- for a single zip file on disk.+openZip :: FilePath -> [OpenFlag] -> IO Zip+openZip fp flags =+  -- This library is currently read-only; always open the archive in read-only mode; see (*).+  let flags' = zip_rdonly .|. combineFlags openFlags flags+   in withCString fp $ \fpc -> alloca $ \errp -> mask_ $ do+        zipp <- c_zip_open fpc flags' errp+        if zipp == nullPtr+          then do+            err <- libzipErrToString =<< peek errp+            throwError "openZip" err+          else Zip <$> newForeignPtr c_zip_discard_ptr zipp++-- See (*).+-- data NumEntriesFlag+--   = -- | The original number of entries is returned.+--     NE_FL_UNCHANGED+--   deriving (Eq, Ord)++-- numEntriesFlags :: Map NumEntriesFlag Zip_flags_t+-- numEntriesFlags =+--   M.fromList+--     [(NEF_FL_UNCHANGED, zip_fl_unchanged)]++-- getNumEntries :: Zip -> [NumEntriesFlag] -> IO Int++-- | Gets the number of entries in the given archive.+getNumEntries :: Zip -> IO Int+getNumEntries (Zip zipfp) =+  let flags' = 0 -- combineFlags numEntriesFlags flags; see (*).+   in do+        num <- withForeignPtr zipfp $ \zipp -> c_zip_get_num_entries zipp flags'+        if num < 0+          then -- c_zip_get_num_entries should not return -1 here, for zipp is known to be non-NULL.+            throwError "getNumEntries" "unexpected"+          else return $ fromIntegral num++data PathFlag+  = --+    --   -- | The original unchanged filename is returned. -- See (*).+    --   P_FL_UNCHANGED++    -- | Return the unmodified names as it is in the ZIP archive.+    P_FL_ENC_RAW+  | -- | (Default.) Guess the encoding of the name in the ZIP archive and convert it to UTF-8, if+    -- necessary. (Only CP-437 and UTF-8 are recognized.)+    P_FL_ENC_GUESS+  | -- | Follow the ZIP specification and expect CP-437 encoded names in the ZIP archive (except if+    -- they are explicitly marked as UTF-8). Convert it to UTF-8.+    P_FL_ENC_STRICT+  deriving (Eq, Ord)++-- | /Internal/.+pathFlags :: Map PathFlag Zip_flags_t+pathFlags =+  M.fromList+    [ -- (P_FL_UNCHANGED, zip_fl_unchanged) -- See (*).+      (P_FL_ENC_RAW, zip_fl_enc_raw),+      (P_FL_ENC_GUESS, zip_fl_enc_guess),+      (P_FL_ENC_STRICT, zip_fl_enc_strict)+    ]++-- | Gets the path (e.g., @"foo.txt"@, @"foo/"@, or @"foo/bar.txt"@) of the file at the given+-- 0-based index in the given zip archive. Please use 'getNumEntries' to find the upper bound for+-- the index.+getPathByIndex :: Zip -> Int -> [PathFlag] -> IO ByteString+getPathByIndex (Zip zipfp) idx flags =+  let flags' = combineFlags pathFlags flags+   in withForeignPtr zipfp $ \zipp -> do+        name <- c_zip_get_name zipp (fromIntegral idx) flags'+        if name == nullPtr+          then do+            err <- BC.unpack <$> (B.packCString =<< c_zip_strerror zipp)+            throwError "getPathByIndex" err+          else B.packCString name++-- | A file inside of a 'Zip' archive.+--+-- /Internal/.+data File+  = File+      !(Ptr Zip_file_t) -- ForeignPtr does not work because zip_fclose() does not return void.+      !IOFinalizer++data GetFileFlag+  = -- | Read the compressed data. Otherwise the data is uncompressed when reading.+    GF_FL_COMPRESSED+  -- -- | Read the original data from the zip archive, ignoring any changes made to the file.+  --  GF_FL_UNCHANGED+  deriving (Eq, Ord)++-- /Internal/.+getFileFlags :: Map GetFileFlag Zip_flags_t+getFileFlags =+  M.fromList+    [ (GF_FL_COMPRESSED, zip_fl_compressed)+    -- (GF_FL_UNCHANGED, zip_fl_unchanged)+    ]++-- | We don't publicly expose getting a 'File' (and then unfolding from it) because we don't want+-- users to unfold from the same 'File' more than once. (libzip’s @c_zip_fread@ isn’t designed for+-- iterating through a file more than once.)+--+-- /Internal/.+getFileByPathOrIndex :: Zip -> [GetFileFlag] -> Either String Int -> IO File+getFileByPathOrIndex (Zip zipfp) flags pathOrIdx = mask_ $ do+  let flags' = combineFlags getFileFlags flags+  filep <- case pathOrIdx of+    Left path ->+      withCString path $ \pathc -> withForeignPtr zipfp $ \zipp -> do+        filep <- c_zip_fopen zipp pathc flags'+        when (filep == nullPtr) $ do+          err <- BC.unpack <$> (B.packCString =<< c_zip_strerror zipp)+          -- Keep zip alive (at least) until we have gotten the above error. This should take care+          -- of touchForeignPtr’s “divergence” caveat. Ref (**).+          touchForeignPtr zipfp+          throwError "Error opening file at path" err+        return filep+    Right idx ->+      withForeignPtr zipfp $ \zipp -> do+        filep <- c_zip_fopen_index zipp (fromIntegral idx) flags'+        when (filep == nullPtr) $ do+          err <- BC.unpack <$> (B.packCString =<< c_zip_strerror zipp)+          touchForeignPtr zipfp -- See (**).+          throwError "Error opening file at index" err+        return filep+  ref <- newIOFinalizer $ do+    ret <- c_zip_fclose filep+    touchForeignPtr zipfp -- See (**).+    when (ret /= 0) $+      throwError+        "Error closing file"+        (printf "zip_fclose() return code: %d" (fromIntegral @_ @Int ret))+  return $ File filep ref++-- /Internal/.+{-# INLINE unfoldFile #-}+unfoldFile :: (MonadIO m) => Unfold m (Zip, [GetFileFlag], Either String Int) ByteString+unfoldFile =+  U.mkUnfoldM+    ( \(z@(Zip zipfp), file@(File filep _), bufp, ref) -> liftIO $ do+        bytesRead <- c_zip_fread filep bufp chunkSize+        if bytesRead < 0+          then+            throwError+              "Error reading file"+              (printf "zip_fread() return value: %d" (fromIntegral @_ @Int bytesRead))+          else+            if bytesRead == 0+              then do+                runIOFinalizer ref+                -- Keep zip alive for (at least) the duration of the unfold. See also (**).+                touchForeignPtr zipfp+                return U.Stop+              else do+                bs <- B.packCStringLen (bufp, fromIntegral bytesRead)+                return $ U.Yield bs (z, file, bufp, ref)+    )+    ( \(z@(Zip zipfp), flags, pathOrIndex) -> liftIO $ mask_ $ do+        file@(File _ fileFinalizer) <- getFileByPathOrIndex z flags pathOrIndex+        bufp <- mallocBytes $ fromIntegral chunkSize+        ref <- newIOFinalizer $ do+          free bufp+          runIOFinalizer fileFinalizer+          touchForeignPtr zipfp -- See also (**).+        return (z, file, bufp, ref)+    )++-- /Internal/.+{-# INLINE chunkSize #-}+chunkSize :: Zip_uint64_t+chunkSize = 64000++-- /Internal/.+combineFlags :: (Ord flagType, Bits a, Num a) => Map flagType a -> [flagType] -> a+combineFlags allFlags =+  foldl'+    (\acc chosenFlag -> acc .|. fromMaybe (error "flag expected") (M.lookup chosenFlag allFlags))+    0++-- | Creates an @Unfold@ with which one can stream data out of the entry at the given path (e.g.,+-- @"foo.txt"@, @"foo/"@, or @"foo/bar.txt"@).+unfoldFileAtPath :: (MonadIO m) => Unfold m (Zip, [GetFileFlag], String) ByteString+unfoldFileAtPath = U.lmap (\(z, fl, p) -> (z, fl, Left p)) unfoldFile++-- | Creates an @Unfold@ with which one can stream data out of the entry at the given index. Please+-- use 'getNumEntries' to find the upper bound for the index.+unfoldFileAtIndex :: (MonadIO m) => Unfold m (Zip, [GetFileFlag], Int) ByteString+unfoldFileAtIndex = U.lmap (\(z, fl, idx) -> (z, fl, Right idx)) unfoldFile
+ src/Streamly/External/Zip/Internal/Error.hs view
@@ -0,0 +1,26 @@+module Streamly.External.Zip.Internal.Error where++import Control.Exception+import qualified Data.ByteString.Char8 as BC+import Foreign.C.Types+import Foreign.Marshal+import Streamly.External.Zip.Internal.Foreign+import Text.Printf++data Error = Error !String !String++instance Show Error where+  show (Error ctx msg) = printf "streamly-zip; %s; %s" ctx msg++instance Exception Error++throwError :: String -> String -> m a+throwError ctx msg = throw $ Error ctx msg++libzipErrToString :: CInt -> IO String+libzipErrToString err = mask_ $ do+  errp <- mallocBytes zip_error_t_size+  c_zip_error_init_with_code errp err+  str <- BC.unpack <$> (c_zip_error_strerror errp >>= BC.packCString)+  c_zip_error_fini errp+  return str
+ src/Streamly/External/Zip/Internal/Foreign.hsc view
@@ -0,0 +1,84 @@+module Streamly.External.Zip.Internal.Foreign where++import Data.Int+import Foreign+import Foreign.C.String+import Foreign.C.Types++#include <zip.h>+type Zip_flags_t = #type zip_flags_t+type Zip_int64_t = #type zip_int64_t+type Zip_uint64_t = #type zip_uint64_t++zip_error_t_size :: Int+zip_error_t_size = #size zip_error_t++data Zip_t++data Zip_file_t++data Zip_error_t++foreign import ccall safe "zip.h zip_open"+  c_zip_open :: CString -> CInt -> Ptr CInt -> IO (Ptr Zip_t)++foreign import ccall safe "zip.h &zip_discard"+  c_zip_discard_ptr :: FunPtr (Ptr Zip_t -> IO ())++-- As this library currently only does reading, we don’t export zip_close().++foreign import ccall safe "zip.h zip_get_num_entries"+  c_zip_get_num_entries :: Ptr Zip_t -> Zip_flags_t -> IO Zip_int64_t++foreign import ccall safe "zip.h zip_get_name"+  c_zip_get_name :: Ptr Zip_t -> Zip_uint64_t -> Zip_flags_t -> IO (Ptr CChar)++foreign import ccall safe "zip.h zip_fopen"+  c_zip_fopen :: Ptr Zip_t -> CString -> Zip_flags_t -> IO (Ptr Zip_file_t)++foreign import ccall safe "zip.h zip_fopen_index"+  c_zip_fopen_index :: Ptr Zip_t -> Zip_uint64_t -> Zip_flags_t -> IO (Ptr Zip_file_t)++foreign import ccall safe "zip.h zip_fclose"+  c_zip_fclose :: Ptr Zip_file_t -> IO CInt++foreign import ccall safe "zip.h zip_fread"+  c_zip_fread :: Ptr Zip_file_t -> Ptr CChar -> Zip_uint64_t -> IO Zip_int64_t++foreign import ccall safe "zip.h zip_strerror"+  c_zip_strerror :: Ptr Zip_t -> IO (Ptr CChar)++-- foreign import ccall safe "zip.h zip_file_strerror"+--   c_zip_file_strerror :: Ptr Zip_file_t -> IO (Ptr CChar)++foreign import ccall safe "zip.h zip_error_init_with_code"+  c_zip_error_init_with_code :: Ptr Zip_error_t -> CInt -> IO ()++foreign import ccall safe "zip.h zip_error_strerror"+  c_zip_error_strerror :: Ptr Zip_error_t -> IO (Ptr CChar)++foreign import ccall safe "zip.h zip_error_fini"+  c_zip_error_fini :: Ptr Zip_error_t -> IO ()++-- All flags relevant for the libzip functions we use.+zip_checkcons,+  zip_create,+  zip_excl,+  zip_truncate,+  zip_rdonly,+  zip_fl_compressed,+  zip_fl_unchanged,+  zip_fl_enc_raw,+  zip_fl_enc_guess,+  zip_fl_enc_strict ::+    (Num a) => a+zip_checkcons = #const ZIP_CHECKCONS+zip_create = #const ZIP_CREATE+zip_excl = #const ZIP_EXCL+zip_truncate = #const ZIP_TRUNCATE+zip_rdonly = #const ZIP_RDONLY+zip_fl_compressed = #const ZIP_FL_COMPRESSED+zip_fl_unchanged = #const ZIP_FL_UNCHANGED+zip_fl_enc_raw = #const ZIP_FL_ENC_RAW+zip_fl_enc_guess = #const ZIP_FL_ENC_GUESS+zip_fl_enc_strict = #const ZIP_FL_ENC_STRICT
+ streamly-zip.cabal view
@@ -0,0 +1,80 @@+cabal-version:    3.0+name:             streamly-zip+version:          0.0.1+synopsis:         Stream data from zip archives using the streamly library.+description:      Please see the README on GitHub at <https://github.com/shlok/streamly-zip#readme>+category:         Archive, Codec, Streaming, Streamly+homepage:         https://github.com/shlok/streamly-zip+bug-reports:      https://github.com/shlok/streamly-zip/issues+author:           Shlok Datye+maintainer:       sd-haskell@quant.is+copyright:        2024 Shlok Datye+license:          BSD-3-Clause+license-file:     LICENSE+build-type:       Simple+extra-doc-files:+  ChangeLog.md+  README.md+data-files:+  test/data/data.zip++source-repository head+  type: git+  location: https://github.com/shlok/streamly-zip++library+  exposed-modules:+      Streamly.External.Zip+      Streamly.External.Zip.Internal+      Streamly.External.Zip.Internal.Error+      Streamly.External.Zip.Internal.Foreign+  other-modules:+      Paths_streamly_zip+  autogen-modules:+      Paths_streamly_zip+  hs-source-dirs:+      src+  ghc-options: -Wall+  extra-libraries:+      zip+  build-depends:+      base >= 4.14 && < 5+    , bytestring >= 0.10 && < 0.12+    , containers >=0.6 && <0.7+    , streamly >=0.10.0 && <0.11+    , streamly-core >=0.2.0 && <0.3+  default-language: Haskell2010++test-suite streamly-zip-test+  type: exitcode-stdio-1.0+  main-is: TestSuite.hs+  other-modules:+      ReadmeMain+      Streamly.External.Zip.Tests+      Paths_streamly_zip+  autogen-modules:+      Paths_streamly_zip+  hs-source-dirs:+      test+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+  extra-libraries:+      zip+  build-depends: +      base >= 4.14 && <5+    , base16-bytestring >=1.0.1.0 && <1.1+    , bytestring >= 0.10 && < 0.12+    , containers >=0.6 && <0.7+    , cryptohash >=0.11.9 && <0.12+    , directory >=1.3.6.0 && <1.4+    , filepath >=1.4.2.1 && <1.5+    , QuickCheck >=2.13.2 && <2.15+    , streamly >=0.10.0 && <0.11+    , streamly-core >=0.2.0 && <0.3+    , streamly-zip+    , tar >=0.5.1.1 && <0.6+    , tasty >=1.2.3 && <1.5+    , tasty-hunit >=0.10.0.2 && <0.11+    , tasty-quickcheck >=0.10.1.1 && <0.11+    , temporary >=1.3 && <1.4+    , zlib >=0.6.2.1 && <0.7+  default-language: Haskell2010
+ test/ReadmeMain.hs view
@@ -0,0 +1,17 @@+module ReadmeMain where++import qualified Data.ByteString as B+import Data.Function+import qualified Streamly.Data.Fold as F+import qualified Streamly.Data.Stream.Prelude as S+import Streamly.External.Zip++main :: IO ()+main = do+  -- Obtain an archive.+  z <- openZip "/path/to/archive.zip" []++  -- Output a particular file to stdout.+  S.unfold unfoldFileAtPath (z, [], "file.txt")+    & S.mapM B.putStr+    & S.fold F.drain
+ test/Streamly/External/Zip/Tests.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE OverloadedStrings #-}++module Streamly.External.Zip.Tests (tests) where++import Control.Monad+import qualified Crypto.Hash.SHA256 as SHA256+import qualified Data.ByteString as B+import qualified Data.ByteString.Base16 as Base16+import qualified Data.ByteString.Char8 as BC8+import Data.Function+import qualified Streamly.Data.Fold as F+import qualified Streamly.Data.Stream.Prelude as S+import Streamly.External.Zip+import Test.Tasty+import Test.Tasty.HUnit++tests :: [TestTree]+tests =+  [testDataZip]++testDataZip :: TestTree+testDataZip = testCase "testDataZip" $ do+  z <- openZip "test/data/data.zip" []++  numEntries <- getNumEntries z+  assertEqual "" numEntries 4++  paths <- forM [0 .. numEntries - 1] $ \idx -> getPathByIndex z idx []+  assertEqual "" paths ["1byte", "60kilobytes", "larger/", "larger/1megabyte"]+  let indexedPaths = zip [0 ..] paths++  let fol = S.fold (F.foldl' B.append B.empty)+      pathToBs p = S.unfold unfoldFileAtPath (z, [], p) & fol+      idxToBs idx = S.unfold unfoldFileAtIndex (z, [], idx) & fol++  fileBytestrings2 <- forM indexedPaths $ \(idx, path) -> do+    (,) <$> pathToBs (BC8.unpack path) <*> idxToBs idx++  let fileBytestrings = map fst fileBytestrings2+  assertEqual "" fileBytestrings (map snd fileBytestrings2)++  let hashes = map (Base16.encode . SHA256.hash) fileBytestrings+      expectedHashes =+        [ "2d3193691934124461809fb9bc7e671215099fc7d961bfbe31943d40d477c890",+          "8ef26f35dd26d5852a145e8ed64dad4db69820556fb267c23d76a513deaaaa80",+          -- SHA256 of empty string.+          "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",+          "b9a25f19d36f7e39ceff25e1463eeaa9800a8fae12551d46f826ae87ccba4a40"+        ]++  assertEqual "" hashes expectedHashes
+ test/TestSuite.hs view
@@ -0,0 +1,16 @@+module Main where++import qualified Streamly.External.Zip.Tests+import Test.Tasty++main :: IO ()+main = defaultMain tests++tests :: TestTree+tests =+  testGroup+    "Tests"+    [ testGroup+        "Streamly.External.Zip.Tests"+        Streamly.External.Zip.Tests.tests+    ]
+ test/data/data.zip view

binary file changed (absent → 1060549 bytes)