Simplified several functions by replacing task continuations with async/await
[pithos-ms-client] / trunk / Pithos.Network / Signature.cs
1 using System;
2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Diagnostics.Contracts;
5 using System.IO;
6 using System.Runtime.Remoting.Metadata.W3cXsd2001;
7 using System.Security.Cryptography;
8 using System.Text;
9 using System.Threading.Tasks;
10 using System.Linq;
11
12 namespace Pithos.Network
13 {
14     public static class Signature
15     {
16         public static string CalculateMD5(FileInfo info)
17         {
18             if (info==null)
19                 throw new ArgumentNullException("info");
20             if (String.IsNullOrWhiteSpace(info.FullName))
21                 throw new ArgumentException("info.FullName is empty","info");
22             Contract.EndContractBlock();
23
24             return CalculateMD5(info.FullName);
25         }
26
27         public static string CalculateMD5(string path)
28         {
29             if (String.IsNullOrWhiteSpace(path))
30                 throw new ArgumentNullException("path");
31             Contract.EndContractBlock();
32
33             //DON'T calculate hashes for folders
34             if (Directory.Exists(path))
35                 return "";
36
37             string hash;
38             using (var hasher = MD5.Create())
39             using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
40             {
41                 var hashBytes = hasher.ComputeHash(stream);
42                 hash = hashBytes.ToHashString();
43             }
44             return hash;
45         }
46
47 /*
48         public static string BytesToString(byte[] hashBytes)
49         {
50             var shb=new SoapHexBinary(hashBytes);
51             return shb.ToString();
52             
53         }
54
55
56         public static byte[] StringToBytes(string hash)
57         {
58             var shb=SoapHexBinary.Parse(hash);
59             return shb.Value;
60         }
61 */
62
63         public static byte[] ToBytes(this string hash)
64         {
65             var shb = SoapHexBinary.Parse(hash);
66             return shb.Value;
67         }
68
69         public static string ToHashString(this byte[] hashBytes)
70         {
71             var shb = new SoapHexBinary(hashBytes);
72             return shb.ToString().ToLower();
73         }
74
75         public static TreeHash CalculateTreeHash(FileInfo fileInfo, int blockSize, string algorithm)
76         {
77             if (fileInfo==null)
78                 throw new ArgumentNullException("fileInfo");
79             if (String.IsNullOrWhiteSpace(fileInfo.FullName))
80                 throw new ArgumentException("fileInfo.FullName is empty","fileInfo");
81             if (blockSize <= 0)
82                 throw new ArgumentOutOfRangeException("blockSize", "blockSize must be a value greater than zero ");
83             if (String.IsNullOrWhiteSpace(algorithm))
84                 throw new ArgumentNullException("algorithm");
85             Contract.EndContractBlock();
86
87             return CalculateTreeHash(fileInfo.FullName, blockSize, algorithm);
88         }
89
90         /// <summary>
91         /// Calculates a file's tree hash synchronously, using the specified block size
92         /// </summary>
93         /// <param name="filePath">Path to an existing file</param>
94         /// <param name="blockSize">Block size used to calculate leaf hashes</param>
95         /// <param name="algorithm"></param>
96         /// <returns>A <see cref="TreeHash"/> with the block hashes and top hash</returns>
97         public static TreeHash CalculateTreeHash(string filePath, int blockSize, string algorithm)
98         {
99             if (String.IsNullOrWhiteSpace(filePath))
100                 throw new ArgumentNullException("filePath");
101             if (blockSize<=0)
102                 throw new ArgumentOutOfRangeException("blockSize","blockSize must be a value greater than zero ");
103             if (String.IsNullOrWhiteSpace(algorithm))
104                 throw new ArgumentNullException("algorithm");
105             Contract.EndContractBlock();
106
107             var hash=CalculateTreeHashAsync(filePath, blockSize, algorithm);
108             return hash.Result;
109         }
110         
111         public static async Task<TreeHash> CalculateTreeHashAsync(FileInfo fileInfo, int blockSize, string algorithm)
112         {
113             if (fileInfo == null)
114                 throw new ArgumentNullException("fileInfo");
115             if (String.IsNullOrWhiteSpace(fileInfo.FullName))
116                 throw new ArgumentNullException("fileInfo.FullName is empty","fileInfo");
117             if (blockSize <= 0)
118                 throw new ArgumentOutOfRangeException("blockSize", "blockSize must be a value greater than zero ");
119             if (String.IsNullOrWhiteSpace(algorithm))
120                 throw new ArgumentNullException("algorithm");
121             Contract.EndContractBlock();
122
123             return await CalculateTreeHashAsync(fileInfo.FullName, blockSize, algorithm);
124         }
125
126
127         public static async Task<TreeHash> CalculateTreeHashAsync(string filePath, int blockSize,string algorithm)
128         {
129             if (String.IsNullOrWhiteSpace(filePath))
130                 throw new ArgumentNullException("filePath");
131             if (blockSize <= 0)
132                 throw new ArgumentOutOfRangeException("blockSize", "blockSize must be a value greater than zero ");
133             if (String.IsNullOrWhiteSpace(algorithm))
134                 throw new ArgumentNullException("algorithm");
135             Contract.EndContractBlock();
136
137             //DON'T calculate hashes for folders
138             if (Directory.Exists(filePath))
139                 return new TreeHash(algorithm);
140             //The hash of a non-existent file is the empty hash
141             if (!File.Exists(filePath))
142                 return new TreeHash(algorithm);
143
144             //Calculate the hash of all blocks using a blockhash iterator
145             using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, blockSize, true))
146             {
147                 //Calculate the blocks asyncrhonously
148                 var hashes = await CalculateBlockHashesAsync(stream, blockSize, algorithm);                
149
150                 //And then proceed with creating and returning a TreeHash
151                 var length = stream.Length;
152                 var list = hashes.OrderBy(pair => pair.Key).Select(pair => pair.Value).ToList();
153
154                 var treeHash = new TreeHash(algorithm)
155                 {
156                     Bytes = length,
157                     BlockSize = blockSize,
158                     Hashes = list
159                 };
160
161                 return treeHash;
162             }
163         }
164
165         
166         public static byte[] CalculateTopHash(IList<byte[]> hashMap, string algorithm)
167         {
168             if (hashMap == null)
169                 throw new ArgumentNullException("hashMap");
170             if (String.IsNullOrWhiteSpace(algorithm))
171                 throw new ArgumentNullException("algorithm");
172             Contract.EndContractBlock();            
173
174             var hashCount = hashMap.Count;
175             //The tophash of an empty hashmap is an empty array
176             if (hashCount == 0)
177                 return new byte[0];
178             //The tophash of a one-item hashmap is the hash itself
179             if (hashCount == 1)
180                 return hashMap[0];
181
182             //Calculate the required number of leaf nodes
183             var leafs =(int)Math.Pow(2, Math.Ceiling(Math.Log(hashCount,2)));
184             //The size of all nodes is the same and equal to the size of the input hashes
185             var hashSize = hashMap[0].Length;
186
187             //If the hashmap containes fewer nodes than the required leaf count, we need to fill
188             //the rest with empty blocks
189             byte[] empty=null;            
190             if (hashCount < leafs)
191                 empty = new byte[hashSize];
192
193             //New hashes will be stored in a dictionary keyed by their step to preserve order
194             var newHashes=new ConcurrentDictionary<int, byte[]>();            
195             
196             Parallel.For(0, leafs/2,
197                 (step, state) =>
198                 {
199                     using (var hasher = HashAlgorithm.Create(algorithm))
200                     {
201                         var i = step*2;
202                         var block1 = i <= hashCount - 1 ? hashMap[i] : empty;
203                         var block2 = i <= hashCount - 2 ? hashMap[i + 1] : empty;
204
205                         hasher.TransformBlock(block1, 0, block1.Length, null, 0);
206                         hasher.TransformFinalBlock(block2, 0, block2.Length);
207
208                         var finalHash = hasher.Hash;
209                         //Store the final value in its proper place
210                         newHashes[step] = finalHash;
211                     }
212                 });
213 /*
214             Parallel.For(0, leafs/2,
215                 () => HashAlgorithm.Create(algorithm),
216                 (step, state, hasher) =>
217                 {
218                                  
219                     var i = step*2;
220                     var block1 = i <= hashCount - 1 ? hashMap[i]: empty;
221                     var block2 = i <= hashCount - 2 ? hashMap[i + 1] : empty;
222
223                     hasher.TransformBlock(block1, 0, block1.Length, null, 0);
224                     hasher.TransformFinalBlock(block2, 0, block2.Length);
225                     
226                     var finalHash = hasher.Hash;
227                     //Store the final value in its proper place
228                     newHashes[step] = finalHash;
229                                         
230                     return hasher;
231                 },
232                 hasher => hasher.Dispose());
233 */
234             //Extract the hashes to a list ordered by their step 
235             var hashes = newHashes.OrderBy(pair => pair.Key).Select(pair => pair.Value).ToList();
236             return CalculateTopHash(hashes, algorithm);                   
237         }
238
239         
240
241       /*  public static string CalculateTopHash(string hashString, string algorithm)
242         {
243             if (String.IsNullOrWhiteSpace(algorithm))
244                 throw new ArgumentNullException("algorithm");
245             Contract.EndContractBlock();
246             if (String.IsNullOrWhiteSpace(hashString))
247                 return String.Empty;
248
249             using (var hasher = HashAlgorithm.Create(algorithm))
250             {
251                 var bytes=Encoding.ASCII.GetBytes(hashString.ToLower());
252                 var hash=hasher.ComputeHash(bytes);
253                 return hash.ToHashString();
254             }
255         }*/
256
257         private static Task<ConcurrentDictionary<int, byte[]>> CalculateBlockHashesAsync(FileStream stream, int blockSize, string algorithm, ConcurrentDictionary<int, byte[]> hashes=null, int index = 0)
258         {
259             if (stream==null)
260                 throw new ArgumentNullException("stream");
261             if (String.IsNullOrWhiteSpace(algorithm))
262                 throw new ArgumentNullException("algorithm");
263             if (blockSize <= 0)
264                 throw new ArgumentOutOfRangeException("blockSize", "blockSize must be a value greater than zero ");
265             if (index< 0)
266                 throw new ArgumentOutOfRangeException("index", "index must be a non-negative value");
267             Contract.EndContractBlock();
268
269
270             if (hashes==null)
271                 hashes= new ConcurrentDictionary<int, byte[]>();
272
273             var buffer = new byte[blockSize];
274             return stream.ReadAsync(buffer, 0, blockSize).ContinueWith(t =>
275             {
276                 var read = t.Result;
277
278                 var nextTask = read == blockSize
279                                     ? CalculateBlockHashesAsync(stream, blockSize, algorithm, hashes, index + 1) 
280                                     : Task.Factory.StartNew(() => hashes);
281                 if (read>0)
282                     using (var hasher = HashAlgorithm.Create(algorithm))
283                     {
284                         //This code was added for compatibility with the way Pithos calculates the last hash
285                         //We calculate the hash only up to the last non-null byte
286                         //TODO: Remove if the server starts using the full block instead of the trimmed block
287                         var lastByteIndex = Array.FindLastIndex(buffer, read - 1, aByte => aByte != 0);
288
289                         var hash = hasher.ComputeHash(buffer, 0, lastByteIndex+1);
290                         hashes[index]=hash;
291                     }
292                 return nextTask;
293             }).Unwrap();
294         }
295
296     
297
298         public static byte[] CalculateHash(byte[] buffer,string algorithm)
299         {
300             if (buffer == null)
301                 throw new ArgumentNullException("buffer");
302             if (String.IsNullOrWhiteSpace(algorithm))
303                 throw new ArgumentNullException("algorithm");
304             Contract.EndContractBlock();
305
306             using (var hasher = HashAlgorithm.Create(algorithm))
307             {
308                 var hash = hasher.ComputeHash(buffer, 0, buffer.Length);
309                 return hash;
310             }        
311         }
312     }
313 }
314
315
316