packages feed

hashed-storage 0.3.6 → 0.3.7

raw patch · 10 files changed

+595/−78 lines, 10 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Storage.Hashed.AnchoredPath: floatPath :: FilePath -> AnchoredPath
+ Storage.Hashed.Darcs: darcsEncodeWhite :: FilePath -> String
+ Storage.Hashed.Darcs: darcsParseDir :: ByteString -> [(ItemType, Name, Hash)]
+ Storage.Hashed.Packed: Block :: (Hash -> IO (Maybe FileSegment)) -> Int -> Format -> Block
+ Storage.Hashed.Packed: Compact :: Format
+ Storage.Hashed.Packed: Loose :: Format
+ Storage.Hashed.Packed: OS :: Block -> [Block] -> [Hash] -> (FileSegment -> IO [Hash]) -> FilePath -> OS
+ Storage.Hashed.Packed: Pack :: Format
+ Storage.Hashed.Packed: blockLookup :: Block -> Hash -> IO (Maybe FileSegment)
+ Storage.Hashed.Packed: blocksLookup :: [Block] -> Hash -> IO (Maybe (Hash, FileSegment))
+ Storage.Hashed.Packed: compact :: OS -> IO OS
+ Storage.Hashed.Packed: create :: FilePath -> Format -> IO OS
+ Storage.Hashed.Packed: createPack :: OS -> [(Hash, FileSegment)] -> IO Block
+ Storage.Hashed.Packed: darcsPristineRefs :: FileSegment -> IO [Hash]
+ Storage.Hashed.Packed: data Block
+ Storage.Hashed.Packed: data Format
+ Storage.Hashed.Packed: data OS
+ Storage.Hashed.Packed: format :: Block -> Format
+ Storage.Hashed.Packed: hatch :: OS -> [ByteString] -> IO OS
+ Storage.Hashed.Packed: hatchery :: OS -> Block
+ Storage.Hashed.Packed: instance Eq Format
+ Storage.Hashed.Packed: instance Show Format
+ Storage.Hashed.Packed: live :: OS -> [Block] -> IO (Map Hash FileSegment)
+ Storage.Hashed.Packed: lookup :: OS -> Hash -> IO (Maybe FileSegment)
+ Storage.Hashed.Packed: looseLookup :: OS -> Hash -> IO (Maybe FileSegment)
+ Storage.Hashed.Packed: loosePath :: OS -> Hash -> FilePath
+ Storage.Hashed.Packed: mature :: OS -> [Block]
+ Storage.Hashed.Packed: readOS :: FilePath -> IO OS
+ Storage.Hashed.Packed: readPackedDarcsPristine :: OS -> Hash -> IO Tree
+ Storage.Hashed.Packed: references :: OS -> FileSegment -> IO [Hash]
+ Storage.Hashed.Packed: repack :: OS -> Set Hash -> IO OS
+ Storage.Hashed.Packed: rootdir :: OS -> FilePath
+ Storage.Hashed.Packed: roots :: OS -> [Hash]
+ Storage.Hashed.Packed: size :: Block -> Int
+ Storage.Hashed.Packed: storePackedDarcsPristine :: Tree -> OS -> IO (OS, Hash)
+ Storage.Hashed.Packed: writePackedDarcsPristine :: Tree -> OS -> IO (OS, Hash)
+ Storage.Hashed.Tree: updateTreePostorder :: (Tree -> Tree) -> Tree -> Tree

Files

Storage/Hashed.hs view
@@ -30,8 +30,7 @@ import Storage.Hashed.Tree( Tree( listImmediate ), TreeItem(..), ItemType(..)                           , Blob(..), emptyTree, makeTree, makeTreeWithHash                           , list, read, find )-import System.FilePath( (</>), splitDirectories, normalise-                      , dropTrailingPathSeparator )+import System.FilePath( (</>) ) import System.Directory( getDirectoryContents, doesFileExist                        , doesDirectoryExist, createDirectoryIfMissing ) import Codec.Compression.GZip( decompress )@@ -42,14 +41,6 @@ -- For explorers -- --- | Take a relative FilePath and turn it into an AnchoredPath. The operation--- is unsafe and if you break it, you keep both pieces. More useful for--- exploratory purposes (ghci) than for serious programming.-floatPath :: FilePath -> AnchoredPath-floatPath = AnchoredPath . map (Name . BS.pack)-              . splitDirectories-              . normalise . dropTrailingPathSeparator- -- | Take a relative FilePath within a Tree and print the contents of the -- object there. Useful for exploration, less so for serious programming. printPath :: Tree -> FilePath -> IO ()@@ -100,20 +91,9 @@ readDarcsHashedDir dir h = do   compressed <- readSegment (dir </> BS.unpack (darcsFormatHash h), Nothing)   let content = decompress compressed-      lines = BL.split '\n' content   return $ if BL.null compressed               then []-              else parse lines-    where-      parse (t:n:h':r) = (parse' t,-                          Name $ BS.pack $ darcsDecodeWhite (BL.unpack n),-                          makeHash hash) : parse r-          where hash = BS.concat $ BL.toChunks h'-      parse _ = []-      parse' x-          | x == BL.pack "file:" = BlobType-          | x == BL.pack "directory:" = TreeType-          | otherwise = error $ "Error parsing darcs hashed dir: " ++ BL.unpack x+              else darcsParseDir content  -- | Read in a darcs-style hashed tree. This is mainly useful for reading -- \"pristine.hashed\". You need to provide the root hash you are interested in@@ -152,7 +132,12 @@                (pris_line:_) ->                    let hash = makeHash $ BS.drop 9 pris_line                     in readDarcsHashed (darcs </> "pristine.hashed") hash-     else readPlainTree $ darcs </> "pristine"+     else do have_pristine <- doesDirectoryExist $ darcs </> "pristine"+             have_current <- doesDirectoryExist $ darcs </> "current"+             case (have_pristine, have_current) of+               (True, _) -> readPlainTree $ darcs </> "pristine"+               (False, True) -> readPlainTree $ darcs </> "current"+               (_, _) -> fail "No pristine tree is available!"  -- | Write out *full* tree to a plain directory structure. If you instead want -- to make incremental updates, refer to "Monad.plainTreeIO".
Storage/Hashed/AnchoredPath.hs view
@@ -5,11 +5,11 @@     ( Name(..), AnchoredPath(..), appendPath, anchorPath     , isPrefix, parent, parents, catPaths, flatten, makeName     -- * Unsafe functions.-    , nameToFilePath, nameFromFilePath, floatBS ) where+    , nameToFilePath, nameFromFilePath, floatBS, floatPath ) where  import qualified Data.ByteString.Char8 as BS import Data.List( isPrefixOf, inits )-import System.FilePath( (</>) )+import System.FilePath( (</>), splitDirectories, normalise, dropTrailingPathSeparator )  ------------------------------- -- AnchoredPath utilities@@ -68,4 +68,11 @@  makeName :: String -> Name makeName = Name . BS.pack++-- | Take a relative FilePath and turn it into an AnchoredPath. The operation+-- is unsafe and if you break it, you keep both pieces. More useful for+-- exploratory purposes (ghci) than for serious programming.+floatPath :: FilePath -> AnchoredPath+floatPath = AnchoredPath . map (Name . BS.pack) . splitDirectories+            . normalise . dropTrailingPathSeparator 
Storage/Hashed/Darcs.hs view
@@ -10,7 +10,7 @@ import qualified Data.ByteString.Lazy.Char8 as BL  import Data.List( sortBy )-import Data.Char( chr )+import Data.Char( chr, ord, isSpace )  darcsFormatSize :: (Num a) => a -> BS.ByteString darcsFormatSize s = BS.pack $ replicate (10 - length n) '0' ++ n@@ -23,6 +23,12 @@               , h ] darcsFormatHash (Hash (Nothing, h)) = h +-- | 'decode_white' interprets the Darcs-specific \"encoded\" filenames+--   produced by 'darcsEncodeWhite'+--+--   > darcsDecodeWhite "hello\32\there" == "hello there"+--   > darcsDecodeWhite "hello\92\there" == "hello\there"+--   > darcsDecodeWhite "hello\there"    == error "malformed filename" darcsDecodeWhite :: String -> FilePath darcsDecodeWhite ('\\':cs) =     case break (=='\\') cs of@@ -32,6 +38,20 @@ darcsDecodeWhite (c:cs) = c: darcsDecodeWhite cs darcsDecodeWhite "" = "" +-- | 'encode_white' translates whitespace in filenames to a darcs-specific+--   format (backslash followed by numerical representation according to 'ord').+--   Note that backslashes are also escaped since they are used in the encoding.+--+--   > darcsEncodeWhite "hello there" == "hello\32\there"+--   > darcsEncodeWhite "hello\there" == "hello\92\there"+darcsEncodeWhite :: FilePath -> String+darcsEncodeWhite (c:cs) | isSpace c || c == '\\' =+    '\\' : (show $ ord c) ++ "\\" ++ darcsEncodeWhite cs+darcsEncodeWhite (c:cs) = c : darcsEncodeWhite cs+darcsEncodeWhite [] = []++darcsEncodeWhiteBS = BS.pack . darcsEncodeWhite . BS.unpack+ darcsFormatDir :: Tree -> BL.ByteString darcsFormatDir t = BL.fromChunks $ concatMap string                      (sortBy cmp $ listImmediate t)@@ -42,14 +62,30 @@                   SubTree _ -> BS.pack "directory:\n"                   Stub _ _ ->                       error "Trees with stubs not supported in darcsFormatDir.",-                name, BS.singleton '\n',+                darcsEncodeWhiteBS name, BS.singleton '\n',                 case itemHash item of                   Nothing -> error $ "darcsFormatDir: missing hash on "                                  ++ show name                   Just h -> darcsFormatHash h,                 BS.singleton '\n' ] +darcsParseDir :: BL.ByteString -> [(ItemType, Name, Hash)]+darcsParseDir content = parse (BL.split '\n' content)+    where+      parse (t:n:h':r) = (header t,+                          Name $ BS.pack $ darcsDecodeWhite (BL.unpack n),+                          makeHash hash) : parse r+          where hash = BS.concat $ BL.toChunks h'+      parse _ = []+      header x+          | x == BL.pack "file:" = BlobType+          | x == BL.pack "directory:" = TreeType+          | otherwise = error $ "Error parsing darcs hashed dir: " ++ BL.unpack x+ -- | Compute a darcs-compatible hash value for a tree-like structure. darcsTreeHash :: Tree -> Hash darcsTreeHash d = hashSetSize (sha256 bl) $ BL.length bl     where bl = darcsFormatDir d++darcsUpdateHashes tree =+    flip updateTreePostorder tree $ \ t -> t { treeHash = Just $ darcsTreeHash t }
Storage/Hashed/Index.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE ScopedTypeVariables, NoMonomorphismRestriction #-}+{-# LANGUAGE CPP, ScopedTypeVariables, NoMonomorphismRestriction #-}  -- | This module contains plain tree indexing code. The index itself is a -- CACHE: you should only ever use it as an optimisation and never as a primary@@ -9,7 +9,7 @@ -- your validity tracking code, you also check for format validity (see -- "indexFormatValid") and scrap and re-create index when needed. ----- The index is a binary file, that overlays a hashed tree over the working+-- The index is a binary file that overlays a hashed tree over the working -- copy. This means that every working file and directory has an entry in the -- index, that contains its path and hash and validity data. The validity data -- is a "last seen" timestamp plus the file size. The file hashes are sha256's@@ -53,7 +53,8 @@                       getFileStatus, fileSize, fileExists, EpochTime ) import System.IO.MMap( mmapFileForeignPtr, Mode(..) ) import System.IO( openBinaryFile, hGetChar, hClose, IOMode(..) )-import System.Directory( removeFile, doesFileExist )+import System.Directory( removeFile, doesFileExist, renameFile )+import System.FilePath( (<.>) ) import Control.Monad( when )  import qualified Data.ByteString.Char8 as BS@@ -289,7 +290,11 @@            len = 87 + sum [ typeLen typ + 84 + 1 + length (anchorPath "" p)                                 | (p, typ) <- paths ]        exist <- doesFileExist indexpath+#if mingw32_HOST_OS+       when exist $ renameFile indexpath (indexpath <.> "old")+#else        when exist $ removeFile indexpath -- to avoid clobbering oldidx+#endif        (mmap, _) <- mmapIndex indexpath len        let magic = fromForeignPtr (castForeignPtr mmap) 0 4            create (File _) path off =
Storage/Hashed/Monad.hs view
@@ -92,9 +92,12 @@                                            (cwd st `catPaths` path) item }  expandTo :: AnchoredPath -> TreeIO ()-expandTo p = do t <- gets tree-                t' <- liftIO $ expandPath t p `catch` \(_::SomeException) -> return t-                modify $ \st -> st { tree = t' }+expandTo p =+    do t <- gets tree+       case find t p of+         Nothing -> do t' <- liftIO $ expandPath t p `catch` \(_::SomeException) -> return t+                       modify $ \st -> st { tree = t' }+         _ -> return ()  -- | Run a 'TreeIO' @action@ in a hashed setting. The @initial@ tree is assumed -- to be fully available from the @directory@, and any changes will be written@@ -109,14 +112,15 @@     do runTreeIO action $ initialState t syncHashed     where syncHashed = do             ch <- gets changed-            modify $ \st -> st { changed = S.empty }+            modify $ \st -> st { changed = S.empty, changesize = 0 }+            modify $ \st -> st { tree = darcsUpdateHashes $ tree st }             forM_ (reverse $ S.toList ch) $ \c -> do                 let path = anchorPath "" c                 current <- gets tree                 case find current c of                   Just (File b) -> updateFile c b                   Just (SubTree s) -> updateSub c s-                  _ -> fail $ "Bar at " ++ path+                  _ -> return () -- the file could have disappeared in the meantime           updateFile path b@(Blob _ (Just !h)) = do             let fn = dir </> BS.unpack (darcsFormatHash h)                 nblob = File $ Blob (decompress `fmap` BL.readFile fn) (Just h)@@ -148,7 +152,7 @@ plainTreeIO action t dir = runTreeIO action $ initialState t syncPlain     where syncPlain = do             ch <- gets changed-            modify $ \st -> st { changed = S.empty }+            modify $ \st -> st { changed = S.empty, changesize = 0 }             current  <- gets tree             forM_ (S.toList ch) $ \c -> do                 let path = anchorPath dir c@@ -222,6 +226,9 @@ rename from to = do expandTo from                     tr <- gets tree                     let item = find tr from+                        found_to = find tr to+                    unless (isNothing found_to) $+                           fail $ "Error renaming: destination " ++ show to ++ " exists."                     unless (isNothing item) $ do                       replaceItem to item                       replaceItem from Nothing
+ Storage/Hashed/Packed.hs view
@@ -0,0 +1,225 @@+{-# LANGUAGE ParallelListComp #-}+module Storage.Hashed.Packed where++import Prelude hiding ( lookup, read )+import Storage.Hashed.AnchoredPath+import Storage.Hashed.Tree hiding ( lookup )+import Storage.Hashed.Utils+import Storage.Hashed.Darcs++import Control.Monad( forM, forM_, unless )+import Control.Applicative( (<$>) )+import System.FilePath( (</>), (<.>) )+import System.Directory( createDirectoryIfMissing, removeFile+                       , getDirectoryContents )++import Bundled.Posix( fileExists, isDirectory, getFileStatus )++import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Char8 as BS+import Data.Maybe( listToMaybe, catMaybes, fromJust, isNothing )++import qualified Data.Set as S+import qualified Data.Map as M+import Data.List( sort )++-- | On-disk format for object storage: we implement a completely loose format+-- (one file per object), a compact format stored in a single append-only file+-- and an immutable "pack" format.+data Format = Loose | Compact | Pack deriving (Show, Eq)++is_loose os = format (hatchery os) == Loose+is_compact os = format (hatchery os) == Compact++loose_dirs = let chars = ['0'..'9'] ++ ['a'..'f']+              in [ [a,b] | a <- chars, b <- chars ]++loosePath :: OS -> Hash -> FilePath+loosePath os (Hash (_,hash)) =+    let hash' = BS.unpack hash+     in rootdir os </> "hatchery" </> take 2 hash' </> drop 2 hash'++looseLookup :: OS -> Hash -> IO (Maybe FileSegment)+looseLookup os hash = do+  let path = loosePath os hash+  exist <- fileExists <$> getFileStatus path+  return $ if exist then Just (path, Nothing)+                    else Nothing++-- | Object storage block. When used as a hatchery, the loose or compact format+-- are preferable, while for mature space, the pack format is more useful.+data Block = Block { blockLookup :: Hash -> IO (Maybe FileSegment)+                   , size :: Int+                   , format :: Format }++-- | Object storage. Contains a single "hatchery" and possibly a number of+-- mature space blocks, usually in form of packs. It also keeps a list of root+-- pointers and has a way to extract pointers from objects (externally+-- supplied). These last two things are used to implement a simple GC.+data OS = OS { hatchery :: Block+             , mature :: [Block]+             , roots :: [Hash]+             , references :: FileSegment -> IO [Hash]+             , rootdir :: FilePath }++-- | Reduce number of packs in the object storage. This may both recombine+-- packs to eliminate dead objects and join some packs to form bigger+-- packs. The set of hashes given is used as roots for GC marking.+repack :: OS -> S.Set Hash -> IO OS+repack os roots = error "repack undefined"++-- | Add new objects to the object storage (i.e. put them into hatchery). It is+-- safe to call this even on objects that are already present in the storage:+-- such objects will be skipped.+hatch :: OS -> [BL.ByteString] -> IO OS+hatch os blobs =+    do processed <- mapM sieve blobs+       write [ (h, b) | (True, h, b) <- processed ]+    where write bits+              | is_loose os =+                  do forM bits $ \(hash, blob) -> do+                       BL.writeFile (loosePath os hash) blob+                     return os+              | is_compact os = error "hatch/compact undefined"+              | otherwise = fail "Hatchery must be either Loose or Compact."+          sieve blob = do let hash = sha256 blob+                          absent <- isNothing <$> lookup os hash+                          return (absent, hash, blob)++-- | Reduce hatchery size by moving things into packs.+compact :: OS -> IO OS+compact os = do objects <- live os [hatchery os]+                block <- createPack os (M.toList objects)+                cleanup+                return $ os { mature = block:mature os }+    where cleanup | is_loose os =+                      forM_ loose_dirs $ nuke . ((rootdir os </> "hatchery") </>)+                  | is_compact os =+                      removeFile (rootdir os </> "hatchery") >> return ()+          nuke dir = mapM (removeFile . (dir </>)) =<<+                       (Prelude.filter (`notElem` [".", ".."]) `fmap`+                               getDirectoryContents dir)++blocksLookup :: [Block] -> Hash -> IO (Maybe (Hash, FileSegment))+blocksLookup blocks hash =+    do segment <- cat `fmap` mapM (flip blockLookup hash) blocks+       return $ case segment of+                  Nothing -> Nothing+                  Just seg -> Just (hash, seg)+    where cat = listToMaybe . catMaybes++lookup :: OS -> Hash -> IO (Maybe FileSegment)+lookup os hash =+    do res <- blocksLookup (hatchery os : mature os) hash+       return $ case res of+                  Nothing -> Nothing+                  Just (_, seg) -> Just seg++-- | Create an empty object storage in given directory, with a hatchery of+-- given format. The directory is created if needed, but is assumed to be+-- empty.+create :: FilePath -> Format -> IO OS+create path fmt = do createDirectoryIfMissing True path+                     initHatchery+                     readOS path+    where initHatchery | fmt == Loose =+                           do mkdir hatchpath+                              forM loose_dirs $ mkdir . (hatchpath </>)+                       | fmt == Compact =+                           error "create/mkHatchery Compact undefined"+          mkdir = createDirectoryIfMissing False+          hatchpath = path </> "hatchery"++readOS :: FilePath -> IO OS+readOS path =+    do hatch_stat <- getFileStatus $ path </> "hatchery"+       let is_os = fileExists hatch_stat+           is_loose = isDirectory hatch_stat+       unless is_os $ fail $ path ++ " is not an object storage!"+       let _hatchery = Block { blockLookup = look os+                             , format = if is_loose then Loose else Compact }+           os = OS { hatchery = _hatchery+                   , rootdir = path+                   , mature = packs+                   , roots = _roots }+           look | format _hatchery == Loose = looseLookup+                | otherwise = undefined+           packs = [] -- FIXME read packs+           _roots = [] -- FIXME read packs+       return os++readPack = undefined++createPack :: OS -> [(Hash, FileSegment)] -> IO Block+createPack os bits =+    do contents <- mapM readSegment (map snd bits)+       let offsets = scanl (+) 0 $ map BL.length contents+           headerbits = [ BL.fromChunks [ hash+                                        , BS.pack ": ("+                                        , BS.pack (show $ offset)+                                        , BS.pack ", "+                                        , BS.pack (show $ BL.length string)+                                        , BS.pack ")\n" ]+                          | (Hash (_, hash), _) <- bits | string <- contents | offset <- offsets ]+           header = BL.concat $ sort headerbits+           blob = BL.concat $ header:contents+           hash@(Hash (_, hashhash)) = sha256 blob+           path = rootdir os </> BS.unpack hashhash <.> "bin"+       BL.writeFile path blob+       return $ readPack path++-- | Build a map of live objects (i.e. those reachable from the given roots) in+-- a given list of Blocks.+live :: OS -> [Block] -> IO (M.Map Hash FileSegment)+live os blocks =+    reachable (references os)+              (blocksLookup blocks)+              (S.fromList $ roots os)++--------------------------------------------------------------+-- Reading and writing darcs-style pristine+----++-- | Read a Tree in the darcs hashed format from an object storage. This is+-- basically the same as readDarcsHashed from Storage.Hashed, but uses an+-- object storage instead of traditional darcs filesystem layout. Requires the+-- tree root hash as a starting point.+readPackedDarcsPristine :: OS -> Hash -> IO Tree+readPackedDarcsPristine os root =+    do seg <- fromJust <$> lookup os root+       items <- darcsParseDir <$> readSegment seg+       subs <- sequence [+                case tp of+                  BlobType -> return (d, File $ file h)+                  TreeType -> let t = readPackedDarcsPristine os h+                               in return (d, Stub t $ Just h)+                | (tp, d, h) <- items ]+       return $ makeTreeWithHash subs root+    where file h = Blob ((fromJust <$> lookup os h) >>= readSegment) (Just h)++-- | Write a Tree into an object storage, using the darcs-style directory+-- formatting (and therefore darcs-style hashes). Gives back the object storage+-- and the root hash of the stored Tree. NB. The function expects that the Tree+-- comes equipped with darcs-style hashes already!+writePackedDarcsPristine :: Tree -> OS -> IO (OS, Hash)+writePackedDarcsPristine tree os =+    do t <- expand tree+       files <- sequence [ read b | (_, File b) <- list t ]+       let dirs = darcsFormatDir t : [ darcsFormatDir d | (_, SubTree d) <- list t ]+       os' <- hatch os $ files ++ dirs+       return (os', darcsTreeHash t)++storePackedDarcsPristine :: Tree -> OS -> IO (OS, Hash)+storePackedDarcsPristine tree os =+    do (os', root) <- writePackedDarcsPristine tree os+       return $ (os' { roots = root : roots os'+                     -- FIXME we probably don't want to override the references+                     -- thing completely here...+                     , references = darcsPristineRefs }, root)++darcsPristineRefs :: FileSegment -> IO [Hash]+darcsPristineRefs fs = do+  con <- (darcsParseDir <$> readSegment fs) `catch` \_ -> return []+  return $! [ hash | (_, _, hash) <- con, valid hash ]+    where valid (Hash (_, hash)) =+              BS.length hash == 64 && all (`elem` "0123456789abcdef") (BS.unpack hash)
Storage/Hashed/Test.hs view
@@ -1,26 +1,34 @@+{-# LANGUAGE ScopedTypeVariables, FlexibleInstances #-} module Storage.Hashed.Test( tests ) where  import Prelude hiding ( read, filter ) import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.ByteString.Char8 as BS+import Control.Exception( finally ) import System.Process import System.Directory( doesFileExist, removeFile ) import Control.Monad( forM_, when )+import Control.Monad.Identity import Data.Maybe import Data.Word import Data.Bits-import Data.List( (\\), sort )+import Data.List( (\\), sort, intercalate, nub ) import Storage.Hashed import Storage.Hashed.AnchoredPath import Storage.Hashed.Tree import Storage.Hashed.Index import Storage.Hashed.Utils import Storage.Hashed.Darcs+import Storage.Hashed.Packed import System.IO.Unsafe( unsafePerformIO ) import System.Mem( performGC ) -import Bundled.Posix( getFileStatus, getSymbolicLinkStatus, fileSize, fileExists )+import qualified Data.Set as S+import qualified Data.Map as M +import qualified Bundled.Posix as Posix+    ( getFileStatus, getSymbolicLinkStatus, fileSize, fileExists )+ import Test.HUnit import Test.Framework( testGroup ) import Test.QuickCheck@@ -35,12 +43,16 @@ blobs = [ (floatPath "foo_a", BL.pack "a\n")         , (floatPath "foo_dir/foo_a", BL.pack "a\n")         , (floatPath "foo_dir/foo_b", BL.pack "b\n")-        , (floatPath "foo_dir/foo_subdir/foo_a", BL.pack "a\n") ]+        , (floatPath "foo_dir/foo_subdir/foo_a", BL.pack "a\n")+        , (floatPath "foo space/foo\nnewline", BL.pack "newline\n")+        , (floatPath "foo space/foo\\backslash", BL.pack "backslash\n")+        , (floatPath "foo space/foo_a", BL.pack "a\n") ]  files = map fst blobs  dirs = [ floatPath "foo_dir"-       , floatPath "foo_dir/foo_subdir" ]+       , floatPath "foo_dir/foo_subdir"+       , floatPath "foo space" ]  emptyStub = Stub (return emptyTree) Nothing @@ -54,15 +66,22 @@           getsub = return sub           getsub2 = return $ makeTree [ (makeName "file", File emptyBlob) ] +equals_testdata t = sequence_ [+                     do ours <- read b+                        let Just stored = Prelude.lookup p blobs+                        assertEqual "contents match" ours stored+                     | (p, File b) <- list t ]+ --------------------------- -- Test list --  tests = [ testGroup "Bundled.Posix" posix-        , testGroup "Storage.Hashed" hashed-        , testGroup "Storage.Hashed.Index" index+        , testGroup "Storage.Hashed.Utils" utils         , testGroup "Storage.Hashed.Tree" tree-        , testGroup "Storage.Hashed.Utils" utils ]+        , testGroup "Storage.Hashed.Index" index+        , testGroup "Storage.Hashed.Packed" packed+        , testGroup "Storage.Hashed" hashed ]  -------------------------- -- Tests@@ -71,12 +90,15 @@ hashed = [ testCase "plain has all files" have_files          , testCase "pristine has all files" have_pristine_files          , testCase "pristine has no extras" pristine_no_extra-         , testCase "pristine file contents match" pristine_contents ]+         , testCase "pristine file contents match" pristine_contents+         , testCase "plain file contents match" plain_contents+         , testCase "writePlainTree works" write_plain ]     where       check_file t f = assertBool                        ("path " ++ show f ++ " is missing in tree")                        (isJust $ find t f)       check_files = forM_ files . check_file+       pristine_no_extra = do         t <- readDarcsPristine "." >>= expand         forM_ (list t) $ \(path,_) -> assertBool (show path ++ " is extraneous in tree")@@ -87,12 +109,18 @@        pristine_contents = do         t <- readDarcsPristine "." >>= expand-        sequence_ [-          do ours <- read b-             let Just stored = Prelude.lookup p blobs-             assertEqual "contents match" ours stored-         | (p, File b) <- list t ]+        equals_testdata t +      plain_contents = do+        t <- expand =<< filter nondarcs `fmap` readPlainTree "."+        equals_testdata t++      write_plain = do+        orig <- readDarcsPristine "." >>= expand+        writePlainTree orig "_darcs/plain"+        t <- readPlainTree "_darcs/plain"+        equals_testdata t+ index = [ testCase "index listing" check_index         , testCase "index content" check_index_content         , testCase "index versioning" check_index_versions ]@@ -126,12 +154,17 @@  tree = [ testCase "modifyTree" check_modify        , testCase "complex modifyTree" check_modify_complex+       , testCase "modifyTree removal" check_modify_remove        , testCase "expand" check_expand        , testCase "expandPath" check_expand_path-       , testProperty "treeEq" check_tree_eq-       , testProperty "expand is identity" check_expand_id-       , testProperty "filter True is identity" check_filter_id-       , testProperty "filter False is empty" check_filter_empty ]+       , testCase "diffTrees" check_diffTrees+       , testCase "diffTrees identical" check_diffTrees_ident+       , testProperty "treeEq" prop_tree_eq+       , testProperty "expand is identity" prop_expand_id+       , testProperty "filter True is identity" prop_filter_id+       , testProperty "filter False is empty" prop_filter_empty+       , testProperty "restrict both ways keeps shape" prop_restrict_shape_commutative+       , testProperty "restrict is a subtree of both" prop_restrict_subtree ]     where blob x = File $ Blob (return (BL.pack x)) (Just $ sha256 $ BL.pack x)           name = Name . BS.pack           check_modify =@@ -141,6 +174,9 @@                      y <- read $ fromJust $ findFile modify (floatPath "foo")                      assertEqual "old version" x (BL.pack "bar")                      assertEqual "new version" y (BL.pack "bla")+                     assertBool "list has foo" $+                                isJust (Prelude.lookup (floatPath "foo") $ list modify)+                     length (list modify) @?= 1           check_modify_complex =               let t = makeTree [ (name "foo", blob "bar")                                , (name "bar", SubTree t1) ]@@ -156,6 +192,22 @@                      assertEqual "old bar/foo" bar_foo (BL.pack "bar")                      assertEqual "new foo" foo' (BL.pack "bar")                      assertEqual "new bar/foo" bar_foo' (BL.pack "bla")+                     assertBool "list has bar/foo" $+                                isJust (Prelude.lookup (floatPath "bar/foo") $ list modify)+                     assertBool "list has foo" $+                                isJust (Prelude.lookup (floatPath "foo") $ list modify)+                     length (list modify) @?= length (list t)+          check_modify_remove =+              let t1 = makeTree [(name "foo", blob "bar")]+                  t2 = makeTree [ (name "foo", blob "bar")+                                , (name "bar", SubTree t1) ]+                  modify1 = modifyTree t1 (floatPath "foo") Nothing+                  modify2 = modifyTree t2 (floatPath "bar") Nothing+                  file = findFile modify1 (floatPath "foo")+                  subtree = findTree modify2 (floatPath "bar")+               in do assertBool "file is gone" (isNothing file)+                     assertBool "subtree is gone" (isNothing subtree)+           no_stubs t = null [ () | (_, Stub _ _) <- list t ]           path = floatPath "substub/substub/file"           badpath = floatPath "substub/substub/foo"@@ -167,33 +219,170 @@             assertBool "badpath not reachable" $                        badpath `notElem` (map fst $ list x)           check_expand_path = do+            test_exp <- expand testTree             t <- expandPath testTree path-            assertBool "path reachable" $ path `elem` (map fst $ list t)-            assertBool "badpath not reachable" $+            t' <- expandPath test_exp path+            t'' <- expandPath testTree $ floatPath "substub/x"+            assertBool "path not reachable in testTree" $ path `notElem` (map fst $ list testTree)+            assertBool "path reachable in t" $ path `elem` (map fst $ list t)+            assertBool "path reachable in t'" $ path `elem` (map fst $ list t')+            assertBool "path reachable in t (with findFile)" $+                       isJust $ findFile t path+            assertBool "path reachable in t' (with findFile)" $+                       isJust $ findFile t' path+            assertBool "path not reachable in t''" $ path `notElem` (map fst $ list t'')+            assertBool "badpath not reachable in t" $                        badpath `notElem` (map fst $ list t)-          check_tree_eq x = x `treeEq` x-          check_expand_id x = unsafePerformIO (expand x) `treeEq` x-          check_filter_id x = filter (\_ _ -> True) x `treeEq` x-          check_filter_empty x = filter (\_ _ -> False) x `treeEq` emptyTree+            assertBool "badpath not reachable in t'" $+                       badpath `notElem` (map fst $ list t') +          check_diffTrees =+            flip finally (writeFile "foo_dir/foo_a" "a\n") $+                 do writeFile "foo_dir/foo_a" "b\n"+                    working_plain <- filter nondarcs `fmap` readPlainTree "."+                    working <- expand =<< updateIndexFrom "_darcs/index" darcsTreeHash working_plain+                    pristine <- readDarcsPristine "."+                    (working', pristine') <- diffTrees working pristine+                    let foo_work = findFile working' (floatPath "foo_dir/foo_a")+                        foo_pris = findFile pristine' (floatPath "foo_dir/foo_a")+                    assertBool "trees have equal shapes" (working' `treeEq` pristine')+                    assertBool "foo_dir/foo_a is in working'" $ isJust foo_work+                    assertBool "foo_dir/foo_a is in pristine'" $ isJust foo_pris+                    foo_work_c <- read (fromJust foo_work)+                    foo_pris_c <- read (fromJust foo_pris)+                    BL.unpack foo_work_c @?= "b\n"+                    BL.unpack foo_pris_c @?= "a\n"+                    assertEqual "working' tree is minimal" 2 (length $ list working')+                    assertEqual "pristine' tree is minimal" 2 (length $ list pristine')++          check_diffTrees_ident = do+            pristine <- readDarcsPristine "."+            (t1, t2) <- diffTrees pristine pristine+            assertBool "t1 is empty" $ null (list t1)+            assertBool "t2 is empty" $ null (list t2)++          prop_tree_eq x = x `treeEq` x+          prop_expand_id x = unsafePerformIO (expand x) `treeEq` x+          prop_filter_id x = filter (\_ _ -> True) x `treeEq` x+          prop_filter_empty x = filter (\_ _ -> False) x `treeEq` emptyTree+          prop_restrict_shape_commutative (t1, t2) =+              not (restrict t1 t2 `treeEq` emptyTree) ==>+                  restrict t1 t2 `treeEq` restrict t2 t1+          prop_restrict_subtree (t1, t2) =+              not (restrict t1 t2 `treeEq` emptyTree) ==>+                  let restricted = S.fromList (map fst $ list $ restrict t1 t2)+                      orig1 = S.fromList (map fst $ list t1)+                      orig2 = S.fromList (map fst $ list t2)+                   in and [restricted `S.isSubsetOf` orig1, restricted `S.isSubsetOf` orig2]++packed = [ testCase "loose pristine tree" check_loose+         , testCase "readOS" check_readOS+         , testCase "live" check_live+         , testCase "compact" check_compact ]+    where check_loose = do x <- readDarcsPristine "."+                           os <- create "_darcs/loose" Loose+                           (os', root) <- writePackedDarcsPristine x os+                           y <- readPackedDarcsPristine os' root+                           equals_testdata y+          check_readOS = do os <- readOS "_darcs/loose"+                            format (hatchery os) @?= Loose+                            x <- readDarcsPristine "."+                            y <- readPackedDarcsPristine os (fromJust $ treeHash x)+                            equals_testdata y+          check_live = do os <- readOS "_darcs/loose"+                          x <- readDarcsPristine "."+                          alive <- live (os { roots = [ fromJust $ treeHash x ]+                                            , references = darcsPristineRefs } ) [hatchery os]+                          sequence_ [ assertBool "" $ (fromJust hash) `S.member` M.keysSet alive+                                      | hash <- map (itemHash . snd) $ list x ]+                          length (M.toList alive) @?= 1 + length (nub $ map snd blobs) + length dirs+          check_compact = do os <- readOS "_darcs/loose"+                             x <- darcsUpdateHashes `fmap` (expand =<< readDarcsPristine ".")+                             (os', root) <- storePackedDarcsPristine x os+                             hatch_root_old <- blockLookup (hatchery os') root+                             assertBool "bits in the old hatchery" $ isJust hatch_root_old++                             os'' <- compact os'+                             length (mature os'') @?= 1+                             hatch_root <- blockLookup (hatchery os'') root+                             assertBool "bits no longer in hatchery" $ isNothing hatch_root++                             -- TODO:+                             -- y <- readPackedDarcsPristine os'' (fromJust $ treeHash x)+                             -- equals_testdata y+ utils = [ testProperty "xlate32" prop_xlate32-        , testProperty "xlate64" prop_xlate64 ]+        , testProperty "xlate64" prop_xlate64+        , testProperty "reachable is a subset" prop_reach_subset+        , testProperty "roots are reachable" prop_reach_roots+        , testProperty "nonexistent roots are not reachable" prop_reach_nonroots+        , testCase "an example for reachable" check_reachable+        , testCase "fixFrom" check_fixFrom+        , testCase "mmap empty file" check_mmapEmpty ]     where prop_xlate32 x = (xlate32 . xlate32) x == x where types = x :: Word32           prop_xlate64 x = (xlate64 . xlate64) x == x where types = x :: Word64 -posix = [ testCase "getFileStatus" $ check_stat getFileStatus-        , testCase "getSymbolicLinkStatus" $ check_stat getSymbolicLinkStatus ]-    where check_stat fun = do-            x <- fileSize `fmap` fun "foo_a"-            writeFile "test_empty" ""-            y <- fileSize `fmap` fun "test_empty"-            exist_nonexistent <- fileExists `fmap` fun "test_does_not_exist"-            exist_existent <- fileExists `fmap` fun "test_empty"+          check_fixFrom = let f 0 = 0+                              f n = f (n - 1) in fixFrom f 5 @?= 0++          check_mmapEmpty = flip finally (removeFile "test_empty") $ do+                              Prelude.writeFile "test_empty" ""+                              x <- readSegment ("test_empty", Nothing)+                              x @?= BL.empty++          reachable' ref look roots = runIdentity $ reachable ref look roots++          check_reachable = let refs 0 = [1, 2]+                                refs 1 = [2]+                                refs 2 = [0, 4]+                                refs 3 = [4, 6, 7]+                                refs 4 = [0, 1]+                                set = S.fromList [1, 2]+                                map = M.fromList [ (n, refs n) | n <- [0..10] ]+                                reach = reachable' return (lookup map) set+                             in do M.keysSet reach @?= S.fromList [0, 1, 2, 4]++          prop_reach_subset (set :: S.Set Int, map :: M.Map Int [Int]) =+              M.keysSet (reachable' return (lookup map) set)+                   `S.isSubsetOf` M.keysSet map+          prop_reach_roots (set :: S.Set Int, map :: M.Map Int [Int]) =+              set `S.isSubsetOf` M.keysSet map+                      ==> set `S.isSubsetOf`+                            M.keysSet (reachable' return (lookup map) set)++          prop_reach_nonroots (set :: S.Set Int, map :: M.Map Int [Int]) =+              set `S.intersection` M.keysSet map+                      == M.keysSet (reachable' (return . const [])+                                   (lookup map) set)++          lookup :: (Ord a) => M.Map a [a] -> a -> Identity (Maybe (a, [a]))+          lookup m k = return $ case M.lookupIndex k m of+                                  Nothing -> Nothing+                                  Just i -> Just $ M.elemAt i m++posix = [ testCase "getFileStatus" $ check_stat Posix.getFileStatus+        , testCase "getSymbolicLinkStatus" $ check_stat Posix.getSymbolicLinkStatus ]+    where check_stat fun = flip finally (removeFile "test_empty") $ do+            x <- Posix.fileSize `fmap` fun "foo_a"+            Prelude.writeFile "test_empty" ""+            y <- Posix.fileSize `fmap` fun "test_empty"+            exist_nonexistent <- Posix.fileExists `fmap` fun "test_does_not_exist"+            exist_existent <- Posix.fileExists `fmap` fun "test_empty"             assertEqual "file size" x 2             assertEqual "file size" y 0             assertBool "existence check" $ not exist_nonexistent             assertBool "existence check" exist_existent +----------------------------------+-- Arbitrary instances+--++instance (Arbitrary a, Ord a) => Arbitrary (S.Set a)+    where arbitrary = S.fromList `fmap` arbitrary++instance (Arbitrary k, Arbitrary v, Ord k) => Arbitrary (M.Map k v)+    where arbitrary = M.fromList `fmap` arbitrary+ instance Arbitrary Word32 where     arbitrary = do x <- arbitrary :: Gen Int                    return $ fromIntegral x@@ -221,6 +410,18 @@                    File _ -> arbitrary                    SubTree t -> return t +---------------------------+-- Other instances+--++instance Show (Int -> Int) where+    show f = "[" ++ intercalate ", " (map val [1..20]) ++ " ...]"+        where val x = show x ++ " -> " ++ show (f x)++-----------------------+-- Test utilities+--+ treeItemEq (File _) (File _) = True treeItemEq (SubTree s) (SubTree p) = s `treeEq` p treeItemEq _ _ = False@@ -228,3 +429,6 @@ treeEq t r = and $ zipTrees cmp t r     where cmp _ (Just a) (Just b) = a `treeItemEq` b           cmp _ _ _ = False++nondarcs (AnchoredPath (Name x:_)) _ | x == BS.pack "_darcs" = False+                                     | otherwise = True
Storage/Hashed/Tree.hs view
@@ -20,7 +20,7 @@     , read      -- * Manipulating trees.-    , finish, filter, restrict, modifyTree ) where+    , finish, filter, restrict, modifyTree, updateTreePostorder ) where  import Prelude hiding( lookup, filter, read, all ) import Storage.Hashed.AnchoredPath@@ -218,17 +218,17 @@               let subs = (catMaybes [ (,) name `fmap` wibble path name item                                       | (name,item) <- listImmediate t ])               in t { items = M.mapMaybeWithKey (wibble path) $ items t-                   , listImmediate = subs-                   , treeHash = Nothing }+                   , listImmediate = subs }+                   -- , treeHash = Nothing }           wibble path name item =               let npath = path `appendPath` name in                   if predicate npath item                      then Just $ filterSub npath item                      else Nothing           filterSub npath (SubTree t) = SubTree $ filter' t npath-          filterSub npath (Stub stub _) =+          filterSub npath (Stub stub h) =               Stub (do x <- stub-                       return $ filter' x npath) Nothing+                       return $ filter' x npath) h -- Nothing           filterSub _ x = x  -- | Read a Blob into a Lazy ByteString. Might be backed by an mmap, use with@@ -265,7 +265,7 @@ -- high up the Tree branches as possible. The result is a pair of trees that do -- not share any identical subtrees. They are derived from the first and second -- parameters respectively and they are always fully expanded. It might be--- advantageous to feed the result into 'zipFiles'.+-- advantageous to feed the result into 'zipFiles' or 'zipTrees'. diffTrees :: Tree -> Tree -> IO (Tree, Tree) diffTrees left right =             if treeHash left `match` treeHash right@@ -347,3 +347,12 @@     error "Bug in descent in modifyTree." modifyTree _ (AnchoredPath []) Nothing =     error "Bug in descent in modifyTree."++updateTreePostorder :: (Tree -> Tree) -> Tree -> Tree+updateTreePostorder fun t =+    t { items = M.mapWithKey (curry $ snd . update) $ items t+      , listImmediate = map update $ listImmediate t+      , treeHash = Nothing }+  where update (k, SubTree s) = (k, SubTree $ fun $ updateTreePostorder fun s)+        update (k, File f) = (k, File f)+        update (k, Stub _ _) = error "Stubs not supported in updateTreePostorder"
Storage/Hashed/Utils.hs view
@@ -12,25 +12,34 @@ import System.Directory( getCurrentDirectory, setCurrentDirectory ) import System.FilePath( (</>), isAbsolute ) import Data.Int( Int64 )+import Data.Maybe( catMaybes ) import Control.Exception.Extensible( catch, bracket, SomeException(..) ) import Control.Monad( when )+import Control.Monad.Identity( runIdentity )+import Control.Applicative( (<$>) )  import Foreign.ForeignPtr( withForeignPtr ) import Foreign.Ptr( plusPtr ) import Data.ByteString.Internal( toForeignPtr, memcpy ) -import Data.Bits( (.&.), (.|.), shift, shiftL, shiftR, Bits )+import Data.Bits( Bits )+#ifdef BIGENDIAN+import Data.Bits( (.&.), (.|.), shift, shiftL, shiftR )+#endif  import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.ByteString.Char8 as BS -newtype Hash = Hash (Maybe Int64, BS.ByteString) deriving (Show, Eq, Read)+import qualified Data.Set as S+import qualified Data.Map as M +newtype Hash = Hash (Maybe Int64, BS.ByteString) deriving (Show, Eq, Ord, Read)+ makeHash :: BS.ByteString -> Hash makeHash str = case BS.split '-' str of                  [h] -> Hash (Nothing, h)                  [s, h] -> Hash (Just $ read $ BS.unpack s, h)-                 _ -> error $ "Bad hash string " ++ show str+                 _ -> Hash (Nothing, str) -- ...  hashSetSize :: Hash -> Int64 -> Hash hashSetSize (Hash (_,h)) s = Hash (Just s, h)@@ -115,3 +124,32 @@             ((a .&. (bytemask `shift` 48) `shiftR` 40)) .|.             ((a .&. (bytemask `shift` 56) `shiftR` 56)) #endif++-- | Find a monadic fixed point of @f@ that is the least above @i@. (Will+-- happily diverge if there is none.)+mfixFrom :: (Eq a, Functor m, Monad m) => (a -> m a) -> a -> m a+mfixFrom f i = do x <- f i+                  if x == i then return i+                            else mfixFrom f x++-- | Find a fixed point of @f@ that is the least above @i@. (Will happily+-- diverge if there is none.)+fixFrom :: (Eq a) => (a -> a) -> a -> a+fixFrom f i = runIdentity $ mfixFrom (return . f) i++-- | For a @refs@ function, a @map@ (@key@ -> @value@) and a @rootSet@, find a+-- submap of @map@ such that all items in @map@ are reachable, through @refs@+-- from @rootSet@.+reachable :: forall monad key value. (Functor monad, Monad monad, Ord key, Eq value) =>+              (value -> monad [key])+           -> (key -> monad (Maybe (key, value)))+           -> S.Set key -> monad (M.Map key value)+reachable refs lookup rootSet =+    do lookupSet rootSet >>= mfixFrom expand+    where lookupSet :: S.Set key -> monad (M.Map key value)+          expand :: M.Map key value -> monad (M.Map key value)++          lookupSet s = do list <- mapM lookup (S.toAscList s)+                           return $ M.fromAscList (catMaybes list)+          expand from = do refd <- concat <$> mapM refs (M.elems from)+                           M.union from <$> lookupSet (S.fromList refd)
hashed-storage.cabal view
@@ -1,5 +1,5 @@ name:          hashed-storage-version:       0.3.6+version:       0.3.7 synopsis:      Hashed file storage support code.  description:   Support code for reading and manipulating hashed file storage@@ -46,6 +46,7 @@         Storage.Hashed.Index         Storage.Hashed.Monad         Storage.Hashed.Tree+        Storage.Hashed.Packed         Storage.Hashed.Darcs      if flag(diff)