4
\$\begingroup\$

I've created the following module to allow me to generate a SHA256 signature for a file. (In real life this is used to verify an image file hasn't been amended). The cut down code looks like this:

open System.IO
open System.Text
open System.Security.Cryptography;


// Get the SHA256 hash of a file
let SHA256 (file:FileInfo) =
    let FileSHA256Wrap (hashFile : FileStream) (sha256Hash : SHA256)  : byte[] = 
        sha256Hash.ComputeHash(hashFile)
    let FileWrap (hashFile : FileStream) : byte[] =
        using (SHA256Managed.Create()) (FileSHA256Wrap hashFile)
    using (file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) FileWrap

// Convert the byte[] version of the hash to a printable encoded HEX string
let HexEncoded (hash:byte[]) :string =
    let sb = new StringBuilder(hash.Length * 2)
    hash |> Array.map (fun c -> sb.AppendFormat("{0:X2}",c)) |> ignore
    sb.ToString()

// Get the file hash and convert it to a HEX string
let HexEncodedSHA256 (file:FileInfo) =
    file |> SHA256 |> HexEncoded


let fi = new FileInfo("somefile.tif")
HexEncodedSHA256 fi

I've two questions here, with the double using statements I find the wrap functions help me work out what is going on - but is there a neater and more succinct way to write this without making it hard to work out what is happening?

The implementation of HexEncoded uses a string builder, is there a more functional way to do this.

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

The use binding is usually better than the using function. The object is disposed when leaving the scope of the use binding (when leaving the SHA256 function).

let SHA256 (file:FileInfo) =
    use hashFile = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
    use sha256Hash = SHA256Managed.Create()
    sha256Hash.ComputeHash(hashFile)

You can avoid the use of a string builder by using String.concat. You can also use the F# sprintf function, which has slightly different format specifier syntax.

let HexEncoded (hash:byte[]) :string =
    hash
    |> Array.map (sprintf "%02X")
    |> String.concat ""

This might be slightly slower than your string builder because it requires an intermediate array. (Note that your implementation could remove the intermediate array by using Array.iter instead of Array.map and moving the ignore to inside the lambda.)

\$\endgroup\$
1
  • \$\begingroup\$ The use binding is clearer, I was sort of trying to re-invent it with my wrap functions. \$\endgroup\$
    – Jackson
    Commented May 2, 2019 at 12:59
3
\$\begingroup\$

I can only agree with TheQuickBrownFox.

You can make even more dedicated functions and then compose them like:

let computeHash (dataStream: Stream) (hasher: HashAlgorithm) = hasher.ComputeHash(dataStream)
let openFile (fileInfo: FileInfo) = fileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)

let getHash algorithmFactory fileInfo = 
    use hasher = algorithmFactory()
    use stream = openFile fileInfo
    computeHash stream hasher

let hexEncode hash = String.Join ("", hash |> Array.map (sprintf "%02X"))

let fromAlgorithm algorithmFactory fileInfo = fileInfo |> getHash algorithmFactory |> hexEncode

let fromSHA256 = fromAlgorithm SHA256Managed.Create
let fromSHA512 = fromAlgorithm SHA512Managed.Create
let fromMD5 = fromAlgorithm MD5.Create

As shown, in this way it's easy to change the hash algorithm.

let test () =
    let fi = new FileInfo(fileName)
    printfn "%A" (fromSHA256 fi)
    printfn "%A" (fromSHA512 fi)
    printfn "%A" (fromMD5 fi)
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Note that F#'s String.concat calls .NET's String.Join, so you can use String.concat and maintain the pipeline style like in my answer, without changing behaviour or performance 🙂 \$\endgroup\$ Commented May 2, 2019 at 13:57
  • \$\begingroup\$ @TheQuickBrownFox: OK, thanks. I just thought I had to do something different than you :-) \$\endgroup\$
    – user73941
    Commented May 2, 2019 at 14:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.