diff --git a/C#/forSpbu/SimpleFtp.Client/Client.cs b/C#/forSpbu/SimpleFtp.Client/Client.cs new file mode 100644 index 0000000..cecee62 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Client/Client.cs @@ -0,0 +1,42 @@ +using System.Net.Sockets; +using SimpleFtp.Protocol; + +namespace SimpleFtp.Client; + +public class FtpClient +{ + private readonly TcpClient _client = new TcpClient(); + private StreamReader? _reader; + private StreamWriter? _writer; + + private FtpClient(string hostname, int port) + { + Hostname = hostname; + Port = port; + } + + public string Hostname { get; private set; } + public int Port { get; private set; } + + public static async Task Connect(string hostname, int port) + { + var client = new FtpClient(hostname, port); + await client._client.ConnectAsync(hostname, port); + client._reader = new StreamReader(client._client.GetStream()); + client._writer = new StreamWriter(client._client.GetStream()); + client._writer.AutoFlush = true; + return client; + } + + public Response SendRequest(Request request) + { + _writer?.Write(request.ToString()); + var data = _reader?.ReadLine() + "\n"; + return ResponseFactory.Create(data); + } + + public void Disconnect() + { + _client.Close(); + } +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Client/Program.cs b/C#/forSpbu/SimpleFtp.Client/Program.cs new file mode 100644 index 0000000..2969b0a --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Client/Program.cs @@ -0,0 +1,31 @@ +using SimpleFtp.Client; +using SimpleFtp.Protocol; + +var client = await FtpClient.Connect("localhost", 32768); +Console.WriteLine("Connected"); +while (true) +{ + var command = Console.ReadLine(); + if (command == "exit") + { + client.Disconnect(); + break; + } + + if (command == null) + { + Console.WriteLine("Incorrect command"); + continue; + } + + try + { + var request = RequestFactory.Create(command + "\n"); + var response = client.SendRequest(request); + Console.Write(response.ToString()); + } + catch (RequestParseException) + { + Console.WriteLine("Incorrect command"); + } +} diff --git a/C#/forSpbu/SimpleFtp.Client/SimpleFtp.Client.csproj b/C#/forSpbu/SimpleFtp.Client/SimpleFtp.Client.csproj new file mode 100644 index 0000000..6ce30c9 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Client/SimpleFtp.Client.csproj @@ -0,0 +1,15 @@ + + + + Exe + net7.0 + enable + enable + SimpleFtpClient + + + + + + + diff --git a/C#/forSpbu/SimpleFtp.Protocol/Request/GetRequest.cs b/C#/forSpbu/SimpleFtp.Protocol/Request/GetRequest.cs new file mode 100644 index 0000000..bc00baa --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Protocol/Request/GetRequest.cs @@ -0,0 +1,16 @@ +namespace SimpleFtp.Protocol; + +public class GetRequest : Request +{ + public string Path { get; private set; } + + public GetRequest(string path) + { + Path = path; + } + + public override string ToString() + { + return "2 " + Path + "\n"; + } +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Protocol/Request/ListRequest.cs b/C#/forSpbu/SimpleFtp.Protocol/Request/ListRequest.cs new file mode 100644 index 0000000..3f8e5e9 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Protocol/Request/ListRequest.cs @@ -0,0 +1,16 @@ +namespace SimpleFtp.Protocol; + +public class ListRequest : Request +{ + public string Path { get; private set; } + + public ListRequest(string path) + { + Path = path; + } + + public override string ToString() + { + return "1 " + Path + "\n"; + } +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Protocol/Request/Request.cs b/C#/forSpbu/SimpleFtp.Protocol/Request/Request.cs new file mode 100644 index 0000000..997b096 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Protocol/Request/Request.cs @@ -0,0 +1,6 @@ +namespace SimpleFtp.Protocol; + +public abstract class Request +{ + public abstract override string ToString(); +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Protocol/Request/RequestFactory.cs b/C#/forSpbu/SimpleFtp.Protocol/Request/RequestFactory.cs new file mode 100644 index 0000000..5fb72cb --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Protocol/Request/RequestFactory.cs @@ -0,0 +1,31 @@ +using System.Text.RegularExpressions; + +namespace SimpleFtp.Protocol; + +public static partial class RequestFactory +{ + private const string GetPattern = "2 (?[1-9a-zA-Z./\\\\]+)\n"; + private const string ListPattern = "1 (?[1-9a-zA-Z./\\\\]+)\n"; + + public static Request Create(string request) + { + if (GetRegex().IsMatch(request)) + { + var match = GetRegex().Match(request); + return new GetRequest(match.Groups["path"].Value); + } + if (ListRegex().IsMatch(request)) + { + var match = ListRegex().Match(request); + return new ListRequest(match.Groups["path"].Value); + } + + throw new RequestParseException(); + } + + [GeneratedRegex(GetPattern)] + private static partial Regex GetRegex(); + + [GeneratedRegex(ListPattern)] + private static partial Regex ListRegex(); +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Protocol/Request/RequestParseException.cs b/C#/forSpbu/SimpleFtp.Protocol/Request/RequestParseException.cs new file mode 100644 index 0000000..3d5753f --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Protocol/Request/RequestParseException.cs @@ -0,0 +1,5 @@ +namespace SimpleFtp.Protocol; + +public class RequestParseException : FormatException +{ +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Protocol/Response/GetResponse.cs b/C#/forSpbu/SimpleFtp.Protocol/Response/GetResponse.cs new file mode 100644 index 0000000..11458e4 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Protocol/Response/GetResponse.cs @@ -0,0 +1,18 @@ +namespace SimpleFtp.Protocol; + +public class GetResponse : Response +{ + private readonly byte[]? _file; + private int Size => _file?.Length ?? -1; + + public GetResponse(byte[] fileBytes) + { + _file = fileBytes; + } + + public GetResponse() + { + } + + public override string ToString() => Size + " " + System.Text.Encoding.UTF8.GetString(_file ?? Array.Empty()) + "\n"; +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Protocol/Response/ListResponse.cs b/C#/forSpbu/SimpleFtp.Protocol/Response/ListResponse.cs new file mode 100644 index 0000000..10e13a0 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Protocol/Response/ListResponse.cs @@ -0,0 +1,22 @@ +namespace SimpleFtp.Protocol; + +public class ListResponse : Response +{ + private readonly IEnumerable<(string name, bool isDir)>? _list; + + private int Size => _list?.Count() ?? -1; + + public ListResponse(IEnumerable<(string name, bool isDir)> dirList) + { + _list = dirList; + } + + public ListResponse() + { + } + + public override string ToString() => + Size + " " + + string.Join(' ', (_list ?? Array.Empty<(string name, bool isDir)>()) + .Select<(string name, bool isDir), string>(x => x.name + " " + x.isDir)) + "\n"; +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Protocol/Response/Response.cs b/C#/forSpbu/SimpleFtp.Protocol/Response/Response.cs new file mode 100644 index 0000000..9764824 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Protocol/Response/Response.cs @@ -0,0 +1,6 @@ +namespace SimpleFtp.Protocol; + +public abstract class Response +{ + public abstract override string ToString(); +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Protocol/Response/ResponseFactory.cs b/C#/forSpbu/SimpleFtp.Protocol/Response/ResponseFactory.cs new file mode 100644 index 0000000..8280cca --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Protocol/Response/ResponseFactory.cs @@ -0,0 +1,53 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace SimpleFtp.Protocol; + +public static partial class ResponseFactory +{ + private const string ListPattern = "(?[0-9]+) ((?[1-9a-zA-Z./\\\\]+) (?False|True) )*((?[1-9a-zA-Z./\\\\]+) (?False|True))+\n"; + private const string GetPattern = "(?[0-9]+) (?.+)\n"; + + public static Response Create(string response) + { + if (ListRegex().IsMatch(response)) + { + var match = ListRegex().Match(response); + + var isDirs = match.Groups["isDirs"].Captures.Select(x => x.Value == "True").ToArray(); + var names = match.Groups["names"].Captures.Select(x => x.Value).ToArray(); + if (!int.TryParse(match.Groups["size"].Value, out var size) || names.Length != size) + { + throw new ResponseParseException(); + } + + var list = new (string, bool)[size]; + for (int i = 0; i < size; i++) + { + list[i] = (names[i], isDirs[i]); + } + return new ListResponse(list); + } + if (GetRegex().IsMatch(response)) + { + var match = GetRegex().Match(response); + + var content = match.Groups["content"].Value; + if (!int.TryParse(match.Groups["size"].Value, out _)) + { + throw new ResponseParseException(); + } + + return new GetResponse(Encoding.ASCII.GetBytes(content)); + } + + throw new ResponseParseException(); + } + + + [GeneratedRegex(ListPattern)] + private static partial Regex ListRegex(); + + [GeneratedRegex(GetPattern)] + private static partial Regex GetRegex(); +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Protocol/Response/ResponseParseException.cs b/C#/forSpbu/SimpleFtp.Protocol/Response/ResponseParseException.cs new file mode 100644 index 0000000..40fedc6 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Protocol/Response/ResponseParseException.cs @@ -0,0 +1,5 @@ +namespace SimpleFtp.Protocol; + +public class ResponseParseException : FormatException +{ +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Protocol/SimpleFtp.Protocol.csproj b/C#/forSpbu/SimpleFtp.Protocol/SimpleFtp.Protocol.csproj new file mode 100644 index 0000000..6836c68 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Protocol/SimpleFtp.Protocol.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/C#/forSpbu/SimpleFtp.Server/Program.cs b/C#/forSpbu/SimpleFtp.Server/Program.cs new file mode 100644 index 0000000..ad03fb6 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Server/Program.cs @@ -0,0 +1,11 @@ +using SimpleFtp; +using SimpleFtp.Protocol; + +var server = new FtpServer(); +var cancellation = new CancellationTokenSource(); +Task.Run(() => server.Listen(cancellation)); +var input = Console.ReadLine(); +if (input == "exit") +{ + cancellation.Cancel(); +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Server/Server.cs b/C#/forSpbu/SimpleFtp.Server/Server.cs new file mode 100644 index 0000000..d78c21d --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Server/Server.cs @@ -0,0 +1,105 @@ +using System.Diagnostics; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Reflection.Metadata; +using SimpleFtp.Protocol; + +namespace SimpleFtp; +public class FtpServer +{ + private const int Port = 32768; + private readonly TcpListener _listener = new (IPAddress.Any, Port); + private CancellationTokenSource? _cancellation; + private readonly List _clients = new(); + + public async Task Listen(CancellationTokenSource cancellation) + { + _cancellation = cancellation; + _listener.Start(); + while (!_cancellation.IsCancellationRequested) + { + var client = await _listener.AcceptTcpClientAsync(_cancellation.Token); + _clients.Add(Task.Run(() => HandleClient(client))); + } + } + + private static async Task HandleClient(TcpClient client) + { + Console.WriteLine("Connected"); + using var reader = new StreamReader(client.GetStream()); + await using var writer = new StreamWriter(client.GetStream()); + writer.AutoFlush = true; + + + while (IsConnected(client)) + { + try + { + var data = await reader.ReadLineAsync(); + if (string.IsNullOrEmpty(data)) + { + continue; + } + data += "\n"; + var response = HandleRequest(RequestFactory.Create(data)); + await writer.WriteAsync(response.ToString()); + } + catch (Exception e) when (e is ArgumentOutOfRangeException or ObjectDisposedException or InvalidOperationException or RequestParseException) + { + } + } + Console.WriteLine("Disconnected"); + } + + private static bool IsConnected(TcpClient client) + { + var tcpConnections = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections().Where(x => x.LocalEndPoint.Equals(client.Client.LocalEndPoint) && x.RemoteEndPoint.Equals(client.Client.RemoteEndPoint)).ToArray(); + return tcpConnections.Length > 0 && tcpConnections.First().State == TcpState.Established; + } + + private static Response HandleRequest(Request request) + { + switch (request) + { + case ListRequest listRequest: + { + try + { + var files = Directory.GetFiles(listRequest.Path); + var directories = Directory.GetDirectories(listRequest.Path); + return new ListResponse(files.Select((string x) => (x, false)).Concat(directories.Select((string x) => (x, true)))); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException or ArgumentException) + { + } + + return new ListResponse(); + } + case GetRequest getRequest: + { + try + { + return new GetResponse(File.ReadAllBytes(getRequest.Path)); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException) + { + } + + return new GetResponse(); + } + default: + { + throw new UnreachableException(); + } + } + } + + private void WaitForClients() + { + foreach (var task in _clients) + { + task.Wait(); + } + } +} \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Server/SimpleFtp.Server.csproj b/C#/forSpbu/SimpleFtp.Server/SimpleFtp.Server.csproj new file mode 100644 index 0000000..aa1a880 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Server/SimpleFtp.Server.csproj @@ -0,0 +1,15 @@ + + + + Exe + net7.0 + enable + enable + SimpleFtp + + + + + + + diff --git a/C#/forSpbu/SimpleFtp.Tests/GlobalUsings.cs b/C#/forSpbu/SimpleFtp.Tests/GlobalUsings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/C#/forSpbu/SimpleFtp.Tests/SimpleFtp.Tests.csproj b/C#/forSpbu/SimpleFtp.Tests/SimpleFtp.Tests.csproj new file mode 100644 index 0000000..a7ac4a3 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Tests/SimpleFtp.Tests.csproj @@ -0,0 +1,26 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + diff --git a/C#/forSpbu/SimpleFtp.Tests/Tests.cs b/C#/forSpbu/SimpleFtp.Tests/Tests.cs new file mode 100644 index 0000000..9ce3696 --- /dev/null +++ b/C#/forSpbu/SimpleFtp.Tests/Tests.cs @@ -0,0 +1,67 @@ +using SimpleFtp.Client; +using SimpleFtp.Protocol; + +namespace SimpleFtp.Tests; + +public class Tests +{ + [Test] + public void TestListCommand() + { + var server = new FtpServer(); + var cancellation = new CancellationTokenSource(); + Task.Run(() => server.Listen(cancellation)); + + Directory.CreateDirectory("./tmp"); + File.WriteAllText("./tmp/a.txt", ""); + var request = RequestFactory.Create("1 ./tmp\n"); + var response = FtpClient.Connect("localhost", 32768).Result.SendRequest(request); + Assert.That(response.ToString(), Is.EqualTo("1 ./tmp\\a.txt False\n")); + } + + [Test] + public void TestGetCommand() + { + var server = new FtpServer(); + var cancellation = new CancellationTokenSource(); + Task.Run(() => server.Listen(cancellation)); + + var content = "asd@%!@# !@#%&*()"; + File.WriteAllText("./tmp.txt", content); + var request = RequestFactory.Create("2 ./tmp.txt\n"); + var response = FtpClient.Connect("localhost", 32768).Result.SendRequest(request); + Assert.That(response.ToString(), Is.EqualTo("17 " + content + "\n")); + } + + [Test] + public void TestConcurrentCommands() + { + var server = new FtpServer(); + var cancellation = new CancellationTokenSource(); + Task.Run(() => server.Listen(cancellation)); + + var content = "asd@%!@# !@#%&*()"; + File.WriteAllText("./tmp.txt", content); + var request = RequestFactory.Create("2 ./tmp.txt\n"); + var fstClient = FtpClient.Connect("localhost", 32768).Result; + var secClient = FtpClient.Connect("localhost", 32768).Result; + + var start = new ManualResetEvent(false); + + var fstThread = new Thread(() => + { + start.WaitOne(); + Assert.That(fstClient.SendRequest(request).ToString(), Is.EqualTo("17 " + content + "\n")); + }); + var secThread = new Thread(() => + { + start.WaitOne(); + Assert.That(secClient.SendRequest(request).ToString(), Is.EqualTo("17 " + content + "\n")); + }); + fstThread.Start(); + secThread.Start(); + start.Set(); + fstThread.Join(); + secThread.Join(); + } +} \ No newline at end of file diff --git a/C#/forSpbu/forSpbu.sln b/C#/forSpbu/forSpbu.sln index 527f400..156d553 100644 --- a/C#/forSpbu/forSpbu.sln +++ b/C#/forSpbu/forSpbu.sln @@ -10,6 +10,18 @@ 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", "{6C82FFDE-03BA-44ED-B5E8-C90579DCF56D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "12.10", "12.10", "{7A21E6C1-9694-4896-AD34-C0BDB62BF665}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleFtp.Server", "SimpleFtp.Server\SimpleFtp.Server.csproj", "{EE4BF01B-FD41-47A7-B2BF-F9604CFE50B5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleFtp.Client", "SimpleFtp.Client\SimpleFtp.Client.csproj", "{D5835D5B-B4F3-4AF6-8FDB-0133183D3304}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleFtp.Protocol", "SimpleFtp.Protocol\SimpleFtp.Protocol.csproj", "{888C5E3A-3DE2-4405-B4E3-24ABB9A473C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleFtp.Tests", "SimpleFtp.Tests\SimpleFtp.Tests.csproj", "{4B30B838-6F2B-48B2-BF87-16A9B003D12B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,12 +33,33 @@ 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 + {EE4BF01B-FD41-47A7-B2BF-F9604CFE50B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE4BF01B-FD41-47A7-B2BF-F9604CFE50B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE4BF01B-FD41-47A7-B2BF-F9604CFE50B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE4BF01B-FD41-47A7-B2BF-F9604CFE50B5}.Release|Any CPU.Build.0 = Release|Any CPU + {D5835D5B-B4F3-4AF6-8FDB-0133183D3304}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5835D5B-B4F3-4AF6-8FDB-0133183D3304}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5835D5B-B4F3-4AF6-8FDB-0133183D3304}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5835D5B-B4F3-4AF6-8FDB-0133183D3304}.Release|Any CPU.Build.0 = Release|Any CPU + {888C5E3A-3DE2-4405-B4E3-24ABB9A473C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {888C5E3A-3DE2-4405-B4E3-24ABB9A473C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {888C5E3A-3DE2-4405-B4E3-24ABB9A473C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {888C5E3A-3DE2-4405-B4E3-24ABB9A473C5}.Release|Any CPU.Build.0 = Release|Any CPU + {4B30B838-6F2B-48B2-BF87-16A9B003D12B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B30B838-6F2B-48B2-BF87-16A9B003D12B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B30B838-6F2B-48B2-BF87-16A9B003D12B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B30B838-6F2B-48B2-BF87-16A9B003D12B}.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} + {7A21E6C1-9694-4896-AD34-C0BDB62BF665} = {6C82FFDE-03BA-44ED-B5E8-C90579DCF56D} + {EE4BF01B-FD41-47A7-B2BF-F9604CFE50B5} = {7A21E6C1-9694-4896-AD34-C0BDB62BF665} + {D5835D5B-B4F3-4AF6-8FDB-0133183D3304} = {7A21E6C1-9694-4896-AD34-C0BDB62BF665} + {888C5E3A-3DE2-4405-B4E3-24ABB9A473C5} = {7A21E6C1-9694-4896-AD34-C0BDB62BF665} + {4B30B838-6F2B-48B2-BF87-16A9B003D12B} = {7A21E6C1-9694-4896-AD34-C0BDB62BF665} EndGlobalSection EndGlobal