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