diff --git a/C#/forSpbu/Md5Calculator.Tests/GlobalUsings.cs b/C#/forSpbu/Md5Calculator.Tests/GlobalUsings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/C#/forSpbu/Md5Calculator.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/C#/forSpbu/Md5Calculator.Tests/Md5Calculator.Tests.csproj b/C#/forSpbu/Md5Calculator.Tests/Md5Calculator.Tests.csproj new file mode 100644 index 0000000..a7fc770 --- /dev/null +++ b/C#/forSpbu/Md5Calculator.Tests/Md5Calculator.Tests.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + diff --git a/C#/forSpbu/Md5Calculator.Tests/Md5CalculatorTests.cs b/C#/forSpbu/Md5Calculator.Tests/Md5CalculatorTests.cs new file mode 100644 index 0000000..8eb4b10 --- /dev/null +++ b/C#/forSpbu/Md5Calculator.Tests/Md5CalculatorTests.cs @@ -0,0 +1,160 @@ +namespace Md5Calculator.Tests; + +public class Md5CalculatorTests +{ + private static IEnumerable> ComputeInstances() + { + yield return Md5Calculator.Compute; + yield return path => Md5Calculator.ComputeAsync(path).Result; + } + + [Test] + public void AsyncEqualsSyncTest() + { + var dirNames = new [] + { + "./AsyncEqualsSyncTest", + "./AsyncEqualsSyncTest/Dir1", + }; + (string path, string data)[] files = new [] + { + ("./AsyncEqualsSyncTest/Dir1/file1.txt", "Asd"), + ("./AsyncEqualsSyncTest/file1.txt", "Asd"), + }; + dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path)); + files.AsParallel() + .ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data))); + + Assert.That(Md5Calculator.Compute(dirNames[0]), Is.EqualTo(Md5Calculator.ComputeAsync(dirNames[0]).Result)); + + files.AsParallel() + .ForAll(file => File.Delete(file.path)); + Directory.Delete(dirNames[1]); + } + + [Test, TestCaseSource(nameof(ComputeInstances))] + public void RecalculateEqualsTest(Func compute) + { + var dirNames = new [] + { + "./RecalculateEqualsTest", + "./RecalculateEqualsTest/Dir1", + }; + (string path, string data)[] files = new [] + { + ("./RecalculateEqualsTest/Dir1/file1.txt", "Asd"), + ("./RecalculateEqualsTest/file1.txt", "Asd"), + }; + dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path)); + files.AsParallel() + .ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data))); + + Assert.That(compute(dirNames[0]), Is.EqualTo(compute(dirNames[0]))); + + files.AsParallel() + .ForAll(file => File.Delete(file.path)); + Directory.Delete(dirNames[1]); + } + + [Test, TestCaseSource(nameof(ComputeInstances))] + public void FileNameMattersTest(Func compute) + { + var dirNames = new [] + { + "./FileNameMattersTest", + }; + (string path, string data)[] files = new [] + { + ("./FileNameMattersTest/file2.txt", "Asd"), + ("./FileNameMattersTest/file1.txt", "Asd"), + }; + dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path)); + files.AsParallel() + .ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data))); + + Assert.That(compute(files[0].path), Is.Not.EqualTo(compute(files[1].path))); + + files.AsParallel() + .ForAll(file => File.Delete(file.path)); + } + + [Test, TestCaseSource(nameof(ComputeInstances))] + public void DirNameMattersTest(Func compute) + { + var dirNames = new [] + { + "./DirNameMattersTest/Dir1", + "./DirNameMattersTest/Dirs5", + }; + (string path, string data)[] files = new [] + { + ("./DirNameMattersTest/Dir1/file1.txt", "Asd"), + ("./DirNameMattersTest/Dirs5/file1.txt", "Asd"), + }; + dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path)); + files.AsParallel() + .ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data))); + + Assert.That(compute(dirNames[0]), Is.Not.EqualTo(compute(dirNames[1]))); + + files.AsParallel() + .ForAll(file => File.Delete(file.path)); + dirNames.AsParallel().ForAll(path => Directory.Delete(path)); + } + + [Test, TestCaseSource(nameof(ComputeInstances))] + public void DirFilesMatterTest(Func compute) + { + var dirNames = new [] + { + "./DirFilesMatterTest/Dir1", + "./DirFilesMatterTest/Dir2", + }; + (string path, string data)[] files = new [] + { + ("./DirFilesMatterTest/Dir1/file1.txt", "Asd"), + ("./DirFilesMatterTest/Dir2/file1.txt", "Asde"), + }; + dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path)); + files.AsParallel() + .ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data))); + + Assert.That(compute(dirNames[0]), Is.Not.EqualTo(compute(dirNames[1]))); + + files.AsParallel() + .ForAll(file => File.Delete(file.path)); + dirNames.AsParallel().ForAll(path => Directory.Delete(path)); + } + + [Test, TestCaseSource(nameof(ComputeInstances))] + public void SubDirsMatterTest(Func compute) + { + var dirNames = new [] + { + "./SubDirsMatterTest/Dir1", + "./SubDirsMatterTest/Dir2", + "./SubDirsMatterTest/Dir1/Dir11", + "./SubDirsMatterTest/Dir2/Dir21", + }; + (string path, string data)[] files = new [] + { + ("./SubDirsMatterTest/Dir1/Dir11/file1.txt", "Asd"), + ("./SubDirsMatterTest/Dir2/Dir21/file1.txt", "Asde"), + }; + dirNames.AsParallel().ForAll(path => Directory.CreateDirectory(path)); + files.AsParallel() + .ForAll(file => File.WriteAllBytes(file.path, System.Text.Encoding.ASCII.GetBytes(file.data))); + + Assert.That(compute(dirNames[0]), Is.Not.EqualTo(compute(dirNames[1]))); + + files.AsParallel() + .ForAll(file => File.Delete(file.path)); + Directory.Delete(dirNames[3]); + Directory.Delete(dirNames[2]); + Directory.Delete(dirNames[1]); + Directory.Delete(dirNames[0]); + + + + } +} \ No newline at end of file diff --git a/C#/forSpbu/Md5Calculator/CalculatorException.cs b/C#/forSpbu/Md5Calculator/CalculatorException.cs new file mode 100644 index 0000000..da260d8 --- /dev/null +++ b/C#/forSpbu/Md5Calculator/CalculatorException.cs @@ -0,0 +1,22 @@ +namespace Md5Calculator; + +/// +/// Represents exception thrown in Md5 calculator +/// +public class CalculatorException : ArgumentException +{ + /// + /// Creates exception with message + /// + /// Exception message + public CalculatorException(string message) : base(message) + { + } + + /// + /// Creates empty exception + /// + public CalculatorException() + { + } +} \ No newline at end of file diff --git a/C#/forSpbu/Md5Calculator/Md5Calculator.cs b/C#/forSpbu/Md5Calculator/Md5Calculator.cs new file mode 100644 index 0000000..af3796a --- /dev/null +++ b/C#/forSpbu/Md5Calculator/Md5Calculator.cs @@ -0,0 +1,83 @@ +namespace Md5Calculator; + +using System.Security.Cryptography; + +/// +/// Represents md5 hash calculator +/// +public static class Md5Calculator +{ + /// + /// Asynchronously computes md5 hash of given path + /// + /// Path to compute + /// Hash as byte array + /// If given does not exist + public static async Task ComputeAsync(string path) + { + if (!Directory.Exists(path) && !File.Exists(path)) + { + throw new CalculatorException("Path doesnt exist"); + } + + return !Directory.Exists(path) ? await ComputeFileAsync(path) : await ComputeDirAsync(path); + } + + /// + /// Synchronously computes md5 hash of given path + /// + /// Path to compute + /// Hash as byte array + /// If given does not exist + public static byte[] Compute(string path) + { + if (!Directory.Exists(path) && !File.Exists(path)) + { + throw new CalculatorException("Path doesnt exist"); + } + + return !Directory.Exists(path) ? ComputeFile(path) : ComputeDir(path); + } + + + private static async Task ComputeDirAsync(string path) + { + var fileTasks = Directory.GetFiles(path).Select(ComputeFileAsync); + + var dirTasks = Directory.GetDirectories(path).Select(ComputeDirAsync); + + var tasks = fileTasks.Concat(dirTasks).ToArray(); + + var overallBytes = (await Task.WhenAll(tasks)) + .SelectMany(byteArray => byteArray) + .Concat(ComputeString(new DirectoryInfo(path).Name)) + .ToArray(); + return MD5.HashData(overallBytes); + } + + private static byte[] ComputeDir(string path) + { + var fileBytes = Directory.GetFiles(path).SelectMany(ComputeFile); + + var dirBytes = Directory.GetDirectories(path).SelectMany(ComputeDir); + + var overallBytes = fileBytes.Concat(dirBytes).Concat(ComputeString(new DirectoryInfo(path).Name)).ToArray(); + + return MD5.HashData(overallBytes); + } + + private static async Task ComputeFileAsync(string path) + { + var byteArray = (await MD5.HashDataAsync(new FileInfo(path).OpenRead())).Concat(ComputeString(new FileInfo(path).Name)).ToArray(); + return MD5.HashData(byteArray); + } + + private static byte[] ComputeFile(string path) + { + var byteArray = MD5.HashData(new FileInfo(path).OpenRead()).Concat(ComputeString(new FileInfo(path).Name)).ToArray(); + return MD5.HashData(byteArray); + } + + private static byte[] ComputeString(string @string) => + MD5.HashData(System.Text.Encoding.ASCII.GetBytes(@string)); +} \ No newline at end of file diff --git a/C#/forSpbu/Md5Calculator/Md5Calculator.csproj b/C#/forSpbu/Md5Calculator/Md5Calculator.csproj new file mode 100644 index 0000000..151d8d4 --- /dev/null +++ b/C#/forSpbu/Md5Calculator/Md5Calculator.csproj @@ -0,0 +1,11 @@ + + + + Exe + net8.0 + Md5_calculator + enable + enable + + + diff --git a/C#/forSpbu/Md5Calculator/Program.cs b/C#/forSpbu/Md5Calculator/Program.cs new file mode 100644 index 0000000..4da3a7c --- /dev/null +++ b/C#/forSpbu/Md5Calculator/Program.cs @@ -0,0 +1,29 @@ +using Md5Calculator; +using System.Diagnostics; + +if (args.Length != 1 || string.IsNullOrEmpty(args[0])) +{ + Console.WriteLine("Should be 1 argument with path"); + return; +} + +try +{ + var startTime = Stopwatch.GetTimestamp(); + var hash = Md5Calculator.Md5Calculator.Compute(args[0]); + var endTime = Stopwatch.GetTimestamp(); + Console.WriteLine($"Single thread: {Convert.ToBase64String(hash)}, {Stopwatch.GetElapsedTime(startTime, endTime)}"); + + startTime = Stopwatch.GetTimestamp(); + hash = await Md5Calculator.Md5Calculator.ComputeAsync(args[0]); + endTime = Stopwatch.GetTimestamp(); + Console.WriteLine($"Multi thread: {Convert.ToBase64String(hash)}, {Stopwatch.GetElapsedTime(startTime, endTime)}"); +} +catch (CalculatorException e) +{ + Console.WriteLine(e.Message); +} +catch (IOException e) +{ + Console.WriteLine(e.Message); +} \ No newline at end of file diff --git a/C#/forSpbu/forSpbu.sln b/C#/forSpbu/forSpbu.sln index 527f400..d68c9cf 100644 --- a/C#/forSpbu/forSpbu.sln +++ b/C#/forSpbu/forSpbu.sln @@ -10,6 +10,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "03.03", "03.03", "{882A9B9C EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "10.03", "10.03", "{EA6FC7D9-BDFB-49CD-AC00-FC5DDC5274B0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2023", "2023", "{318796AF-A927-4A13-BAEE-FD13551DE91A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "30.11", "30.11", "{47EB60F8-9C89-4D13-8243-B77B01A4BA53}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Md5Calculator", "Md5Calculator\Md5Calculator.csproj", "{AA2AD188-B903-46DF-AE43-4769DBF788E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Md5Calculator.Tests", "Md5Calculator.Tests\Md5Calculator.Tests.csproj", "{6A71CD77-320C-458F-97B3-BA8F4CB530C6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,12 +29,23 @@ Global {E007586F-9760-4744-BB25-EDEFD6BA860C}.Release|Any CPU.ActiveCfg = Release|Any CPU {E007586F-9760-4744-BB25-EDEFD6BA860C}.Release|Any CPU.Build.0 = Release|Any CPU {A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Release|Any CPU.Build.0 = Release|Any CPU + {A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E}.Release|Any CPU.Build.0 = Release|Any CPU + {AA2AD188-B903-46DF-AE43-4769DBF788E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA2AD188-B903-46DF-AE43-4769DBF788E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA2AD188-B903-46DF-AE43-4769DBF788E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA2AD188-B903-46DF-AE43-4769DBF788E9}.Release|Any CPU.Build.0 = Release|Any CPU + {6A71CD77-320C-458F-97B3-BA8F4CB530C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A71CD77-320C-458F-97B3-BA8F4CB530C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A71CD77-320C-458F-97B3-BA8F4CB530C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A71CD77-320C-458F-97B3-BA8F4CB530C6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution - {E007586F-9760-4744-BB25-EDEFD6BA860C} = {D3FCB669-E93F-4F0B-B9C5-6592CE93AC7F} + {E007586F-9760-4744-BB25-EDEFD6BA860C} = {D3FCB669-E93F-4F0B-B9C5-6592CE93AC7F} {A4F6ADD5-85FD-4F67-8B29-549DDDF6F82E} = {D3FCB669-E93F-4F0B-B9C5-6592CE93AC7F} + {47EB60F8-9C89-4D13-8243-B77B01A4BA53} = {318796AF-A927-4A13-BAEE-FD13551DE91A} + {AA2AD188-B903-46DF-AE43-4769DBF788E9} = {47EB60F8-9C89-4D13-8243-B77B01A4BA53} + {6A71CD77-320C-458F-97B3-BA8F4CB530C6} = {47EB60F8-9C89-4D13-8243-B77B01A4BA53} EndGlobalSection EndGlobal