namespace Git open System.IO open System.Security.Cryptography open Ionic.Zlib type EncodedObject = { Header : Header Content : byte array } [] module EncodedObject = let encode (o : Git.Object) : EncodedObject = let contents = match o with | Object.Blob c -> Blob.encode c | Object.Tree entries -> Tree.encode entries { Header = match o with | Object.Blob _ -> Header.Blob contents.Length | Object.Tree _ -> Header.Tree contents.Length Content = contents } let decode (e : EncodedObject) : Git.Object = match e.Header with | Header.Tree _ -> Tree.decode e.Content |> Object.Tree | Header.Blob _ -> Blob.decode e.Content |> Object.Blob let hash (o : EncodedObject) : Hash = use hasher = SHA1.Create () let content = Array.concat [| Header.toBytes o.Header ; o.Content |] hasher.ComputeHash content |> Hash.ofBytes let private compress (o : EncodedObject) (dest : Stream) : unit = let toWrite = [| Header.toBytes o.Header ; o.Content |] |> Array.concat use ms = new MemoryStream(toWrite) use ds = new Ionic.Zlib.ZlibStream(dest, CompressionMode.Compress) ms.CopyTo ds /// Read the header of the stream seeked to the beginning of the content. let private consumeHeader (s : BinaryReader) : Header = let rec bytes () : byte seq = seq { let newByte = s.Read () if newByte < 0 then failwith "ran out of bytes" elif newByte > 0 then yield (byte newByte) yield! bytes () // stop reading the header at the 0 byte } match bytes () |> Seq.toArray |> Header.ofBytes with | None -> failwith "malformed header" | Some b -> b let private uncompress (s : Stream) : EncodedObject = use ms = new MemoryStream () use ds = new Ionic.Zlib.ZlibStream(s, CompressionMode.Decompress) ds.CopyTo ms ms.Seek(0L, SeekOrigin.Begin) |> ignore use r = new BinaryReader(ms) let header = consumeHeader r let expectedLength = match header with | Header.Blob i -> i | Header.Tree i -> i let result = { Header = header Content = r.ReadBytes expectedLength } if r.PeekChar () <> -1 then failwith "unexpectedly not at end" result let write (r : Repository) (o : EncodedObject) : Hash = let hash = hash o let hashStr = Hash.toString hash let objectName = hashStr.[2..] let subdir = hashStr.[0..1] let d = Repository.createSubdir (Repository.objectDir r) subdir use filestream = r.Fs.File.Create (r.Fs.Path.Combine (d.FullName, objectName)) compress o filestream hash let catFile (r : Repository) (hash : Hash) : EncodedObject = let hash = hash |> Hash.toString let objectName = hash.[2..] let subdir = hash.[0..1] use filestream = r.Fs.Path.Combine ((Repository.objectDir r).FullName, subdir, objectName) |> r.Fs.File.OpenRead uncompress filestream