diff --git a/Flawless-Version-Control.sln b/Flawless-Version-Control.sln index cad6658..cb390a4 100644 --- a/Flawless-Version-Control.sln +++ b/Flawless-Version-Control.sln @@ -1,14 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flawless.Shared", "Flawless.Shared\Flawless.Shared.csproj", "{825E512F-4283-4BE9-A88B-0316ED85796E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flawless.Abstraction", "Flawless.Abstraction\Flawless.Abstraction.csproj", "{2AF2AA78-8AFB-49B7-AE38-842D301A4DDE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flawless.Client", "Flawless.Client\Flawless.Client.csproj", "{7F847931-55E5-4F00-93F6-81881BFC61FE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flawless.Client.Avanonia", "Flawless.Client.Avanonia\Flawless.Client.Avanonia.csproj", "{3901AE7A-27DA-4A43-9E22-6F64D812AC3E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flawless.Server", "Flawless.Server\Flawless.Server.csproj", "{17EA1F4E-7D4E-485D-BDB5-18BBC3FB8DF9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flawless.Test.ConsoleApplication", "Flawless.Test.ConsoleApplication\Flawless.Test.ConsoleApplication.csproj", "{29FF9E82-23A4-4F3C-82B6-7ABC72D5990D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flawless.Abstract.Test", "Flawless.Abstract.Test\Flawless.Abstract.Test.csproj", "{5B1CB26D-99F5-491A-B368-7E3552FE67E9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -16,25 +10,13 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {825E512F-4283-4BE9-A88B-0316ED85796E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {825E512F-4283-4BE9-A88B-0316ED85796E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {825E512F-4283-4BE9-A88B-0316ED85796E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {825E512F-4283-4BE9-A88B-0316ED85796E}.Release|Any CPU.Build.0 = Release|Any CPU - {7F847931-55E5-4F00-93F6-81881BFC61FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7F847931-55E5-4F00-93F6-81881BFC61FE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7F847931-55E5-4F00-93F6-81881BFC61FE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7F847931-55E5-4F00-93F6-81881BFC61FE}.Release|Any CPU.Build.0 = Release|Any CPU - {3901AE7A-27DA-4A43-9E22-6F64D812AC3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3901AE7A-27DA-4A43-9E22-6F64D812AC3E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3901AE7A-27DA-4A43-9E22-6F64D812AC3E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3901AE7A-27DA-4A43-9E22-6F64D812AC3E}.Release|Any CPU.Build.0 = Release|Any CPU - {17EA1F4E-7D4E-485D-BDB5-18BBC3FB8DF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {17EA1F4E-7D4E-485D-BDB5-18BBC3FB8DF9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {17EA1F4E-7D4E-485D-BDB5-18BBC3FB8DF9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {17EA1F4E-7D4E-485D-BDB5-18BBC3FB8DF9}.Release|Any CPU.Build.0 = Release|Any CPU - {29FF9E82-23A4-4F3C-82B6-7ABC72D5990D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {29FF9E82-23A4-4F3C-82B6-7ABC72D5990D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29FF9E82-23A4-4F3C-82B6-7ABC72D5990D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {29FF9E82-23A4-4F3C-82B6-7ABC72D5990D}.Release|Any CPU.Build.0 = Release|Any CPU + {2AF2AA78-8AFB-49B7-AE38-842D301A4DDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AF2AA78-8AFB-49B7-AE38-842D301A4DDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AF2AA78-8AFB-49B7-AE38-842D301A4DDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AF2AA78-8AFB-49B7-AE38-842D301A4DDE}.Release|Any CPU.Build.0 = Release|Any CPU + {5B1CB26D-99F5-491A-B368-7E3552FE67E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B1CB26D-99F5-491A-B368-7E3552FE67E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B1CB26D-99F5-491A-B368-7E3552FE67E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B1CB26D-99F5-491A-B368-7E3552FE67E9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Flawless-Version-Control.sln.DotSettings.user b/Flawless-Version-Control.sln.DotSettings.user index 982ffe9..fa16bb2 100644 --- a/Flawless-Version-Control.sln.DotSettings.user +++ b/Flawless-Version-Control.sln.DotSettings.user @@ -1,4 +1,18 @@  + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded - ForceIncluded \ No newline at end of file + ForceIncluded + ForceIncluded + ForceIncluded + <SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId> + </TestAncestor> +</SessionState> + <SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId> + </TestAncestor> +</SessionState> \ No newline at end of file diff --git a/Flawless.Abstract.Test/Flawless.Abstract.Test.csproj b/Flawless.Abstract.Test/Flawless.Abstract.Test.csproj new file mode 100644 index 0000000..44d33b2 --- /dev/null +++ b/Flawless.Abstract.Test/Flawless.Abstract.Test.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + latest + enable + enable + + + + + + + + + + + + + + + + diff --git a/Flawless.Abstract.Test/MSTestSettings.cs b/Flawless.Abstract.Test/MSTestSettings.cs new file mode 100644 index 0000000..8b7de71 --- /dev/null +++ b/Flawless.Abstract.Test/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] \ No newline at end of file diff --git a/Flawless.Abstract.Test/WorkPathTestUnit.cs b/Flawless.Abstract.Test/WorkPathTestUnit.cs new file mode 100644 index 0000000..7fa3dbe --- /dev/null +++ b/Flawless.Abstract.Test/WorkPathTestUnit.cs @@ -0,0 +1,286 @@ +using Flawless.Abstraction; + +namespace Flawless.Abstract.Test; + +[TestClass] +public sealed class WorkPathTestUnit +{ + [TestMethod] + public void CombinationTest() + { + Assert.AreEqual("abc/def", WorkPath.Combine("abc", "def")); + Assert.AreEqual("/abc/def", WorkPath.Combine("/abc", "def")); + Assert.AreEqual("abc/def/", WorkPath.Combine("abc", "def/")); + Assert.AreEqual("/abc/def/", WorkPath.Combine("/abc", "def/")); + Assert.AreEqual("/abc/def/", WorkPath.Combine("/abc/", "/def/")); + Assert.AreEqual("abc/def", WorkPath.Combine("abc/", "def")); + Assert.AreEqual("abc/def", WorkPath.Combine("abc", "/def")); + Assert.AreEqual("abc/def", WorkPath.Combine("abc/", "/def")); + Assert.AreEqual("abc/", WorkPath.Combine("abc", "/")); + Assert.AreEqual("abc//", WorkPath.Combine("abc", "//")); + Assert.AreEqual("abc/def/ghi", WorkPath.Combine("abc", "def", "ghi")); + Assert.AreEqual("abc/def/ghi/nmp", WorkPath.Combine("abc", "def", "ghi", "nmp")); + Assert.AreEqual("abc/def/ghi/nmp/uvw", WorkPath.Combine("abc", "def", "ghi", "nmp", "uvw")); + Assert.AreEqual("abc/def/ghi/nmp/uvw", WorkPath.Combine("abc", "def/ghi/nmp/uvw")); + } + + [TestMethod] + public void ValidPathValidateTest() + { + Assert.IsTrue(WorkPath.IsPathValid("abc")); + Assert.IsTrue(WorkPath.IsPathValid("abc/def")); + Assert.IsTrue(WorkPath.IsPathValid("abc/d ef")); + Assert.IsTrue(WorkPath.IsPathValid("a b c/d ef")); + Assert.IsTrue(WorkPath.IsPathValid("abc/def/ghi/k.lmn")); + Assert.IsTrue(WorkPath.IsPathValid(".flawless.ignore")); + Assert.IsTrue(WorkPath.IsPathValid("sub/.flawless.ignore")); + } + + [TestMethod] + public void InvalidPathValidateTest() + { + Assert.IsFalse(WorkPath.IsPathValid("ab\\c/def")); + Assert.IsFalse(WorkPath.IsPathValid("ab?c/def")); + Assert.IsFalse(WorkPath.IsPathValid(" abc/def/ghi/k.lmn")); + Assert.IsFalse(WorkPath.IsPathValid("/abc/def")); + Assert.IsFalse(WorkPath.IsPathValid("/abc/def/")); + Assert.IsFalse(WorkPath.IsPathValid("")); + Assert.IsFalse(WorkPath.IsPathValid(null!)); + Assert.IsFalse(WorkPath.IsPathValid("abc/ de f/ghi/k.lmn")); + Assert.IsFalse(WorkPath.IsPathValid("abc/def/")); + Assert.IsFalse(WorkPath.IsPathValid("abc/def ")); + } + + + [TestMethod] + public void ValidPathSplitTest() + { + Assert.IsTrue(ValidPathVectorMatch("abc", "abc")); + Assert.IsTrue(ValidPathVectorMatch("abc/def", "abc", "def")); + Assert.IsTrue(ValidPathVectorMatch("abc/d ef", "abc", "d ef")); + Assert.IsTrue(ValidPathVectorMatch("a b c/d ef", "a b c", "d ef")); + Assert.IsTrue(ValidPathVectorMatch("abc/def/ghi/k.lmn", "abc", "def", "ghi", "k.lmn")); + } + + [TestMethod] + public void InvalidPathSplitTest() + { + Assert.IsTrue(InvalidPathVectorMatch("ab\\c/def", "Invalid work path character")); + Assert.IsTrue(InvalidPathVectorMatch("ab?c/def", "Invalid work path character")); + Assert.IsTrue(InvalidPathVectorMatch(" abc/def/ghi/k.lmn ", "Work path vector can not start or end with a space!")); + Assert.IsTrue(InvalidPathVectorMatch("/abc/def", "Work path cannot start with a DirectorySeparatorChar!")); + Assert.IsTrue(InvalidPathVectorMatch("/abc/def/", "Work path cannot start with a DirectorySeparatorChar!")); + Assert.IsTrue(InvalidPathVectorMatch("", "Not a valid work path!")); + Assert.IsTrue(InvalidPathVectorMatch(null!, "Not a valid work path!")); + Assert.IsTrue(InvalidPathVectorMatch("abc/ de f/ghi/k.lmn", "Work path vector can not start or end with a space!")); + Assert.IsTrue(InvalidPathVectorMatch("abc/def/", "Work path contains empty vector!")); + Assert.IsTrue(InvalidPathVectorMatch("abc/def ", "Work path vector can not start or end with a space!")); + } + + [TestMethod] + public void RelativePathTest() + { + Assert.AreEqual("def", WorkPath.GetRelativePath("abc", "abc/def")); + Assert.AreEqual("ghi", WorkPath.GetRelativePath("abc/def", "abc/def/ghi")); + Assert.AreEqual("def/ghi", WorkPath.GetRelativePath("abc", "abc/def/ghi")); + Assert.AreEqual(string.Empty, WorkPath.GetRelativePath("abc/def", "abc/def")); + Assert.AreEqual(string.Empty, WorkPath.GetRelativePath("abc", "abc")); + Assert.AreEqual(null, WorkPath.GetRelativePath("abc/def", "abc")); + } + + [TestMethod] + public void PlatformToWorkPathTransformTest() + { + Assert.AreEqual("abc/def", WorkPath.FromPlatformPath("/root/test/abc/def", "/root/test/")); + Assert.AreEqual("abc/def", WorkPath.FromPlatformPath("/root/test/abc/def", "/root/test")); + Assert.AreEqual("", WorkPath.FromPlatformPath("/root/test", "/root/test")); + Assert.AreEqual("", WorkPath.FromPlatformPath("/root/test/", "/root/test")); + Assert.AreEqual("", WorkPath.FromPlatformPath("/root/test", "/root/test/")); + Assert.AreEqual("", WorkPath.FromPlatformPath("/root/test/", "/root/test/")); + Assert.AreEqual("abc/def", WorkPath.FromPlatformPath("./root/test/abc/def", "./root/test")); + Assert.AreEqual("", WorkPath.FromPlatformPath(@".\root\test", @".\root\test")); + Assert.AreEqual("", WorkPath.FromPlatformPath(@".\root\test", @".\root/test")); + Assert.AreEqual("abc/def", WorkPath.FromPlatformPath(@".\root\test\abc\def", @".\root\test")); + Assert.AreEqual("abc/def", WorkPath.FromPlatformPath(@"C:\root\test\abc\def", @"C:\root\test\")); + Assert.AreEqual("abc/def", WorkPath.FromPlatformPath(@"C:\root\test\abc\def", @"C:\root\test")); + Assert.AreEqual("abc/def", WorkPath.FromPlatformPath(@"C:\root/test\abc/def", @"C:\root\test\")); + Assert.AreEqual("abc/def", WorkPath.FromPlatformPath(@"C:\root\test\abc\def", @"C:\root/test")); + } + + [TestMethod] + public void WorkToPlatformPathTransformTest() + { + Assert.AreEqual(".", Path.GetRelativePath("/root/test/abc/def", WorkPath.ToPlatformPath("abc/def", "/root/test"))); + Assert.AreEqual(".", Path.GetRelativePath("/root/test/abc/def", WorkPath.ToPlatformPath("/abc/def", "/root/test/"))); + Assert.AreEqual(".", Path.GetRelativePath("/root/test/abc/def", WorkPath.ToPlatformPath("/abc/def/", "/root/test"))); + Assert.AreEqual(".", Path.GetRelativePath("/root/test/abc/def", WorkPath.ToPlatformPath("/abc/def/", "/root/test/"))); + Assert.AreEqual(".", Path.GetRelativePath("/root/test/abc/def", WorkPath.ToPlatformPath("abc/def", "C:/root/test"))); + Assert.AreEqual(".", Path.GetRelativePath("/root/test/abc/def", WorkPath.ToPlatformPath("/abc/def", "C:/root/test/"))); + Assert.AreEqual(".", Path.GetRelativePath("/root/test/abc/def", WorkPath.ToPlatformPath("/abc/def/", "C:/root/test"))); + Assert.AreEqual(".", Path.GetRelativePath("/root/test/abc/def", WorkPath.ToPlatformPath("/abc/def/", "C:/root/test/"))); + Assert.AreEqual(".", Path.GetRelativePath("/root/test/abc/def", WorkPath.ToPlatformPath("abc/def", @"C:/root\test"))); + Assert.AreEqual(".", Path.GetRelativePath("/root/test/abc/def", WorkPath.ToPlatformPath("abc/def", @"/root\test"))); + } + + [TestMethod] + public void WorkPathGetExtensionTest() + { + Assert.AreEqual("", WorkPath.GetExtension("")); + Assert.AreEqual("", WorkPath.GetExtension("/")); + Assert.AreEqual("git", WorkPath.GetExtension(".git")); + Assert.AreEqual("git", WorkPath.GetExtension("abc.git")); + + Assert.AreEqual("", WorkPath.GetExtension("abc/def")); + Assert.AreEqual("", WorkPath.GetExtension("abc/")); + Assert.AreEqual("ght", WorkPath.GetExtension("abc/def.ght")); + Assert.AreEqual("ght", WorkPath.GetExtension(".abc/.ght")); + Assert.AreEqual("ght", WorkPath.GetExtension("abc/.ght")); + + Assert.AreEqual("", WorkPath.GetExtension("/abc/def")); + Assert.AreEqual("", WorkPath.GetExtension("/abc/")); + Assert.AreEqual("ght", WorkPath.GetExtension("/abc/def.ght")); + Assert.AreEqual("ght", WorkPath.GetExtension("/abc/.ght")); + Assert.AreEqual("ght", WorkPath.GetExtension("/.abc/.ght")); + } + + + [TestMethod] + public void WorkPathChangeExtensionTest() + { + Assert.AreEqual(".png", WorkPath.ChangeExtension("", "png")); + Assert.AreEqual("/.png", WorkPath.ChangeExtension("/", "png")); + Assert.AreEqual(".png", WorkPath.ChangeExtension(".git", "png")); + Assert.AreEqual("abc.png", WorkPath.ChangeExtension("abc.git", "png")); + + Assert.AreEqual("abc/def.png", WorkPath.ChangeExtension("abc/def", "png")); + Assert.AreEqual("abc/.png", WorkPath.ChangeExtension("abc/", "png")); + Assert.AreEqual("abc/def.png", WorkPath.ChangeExtension("abc/def.ght", "png")); + Assert.AreEqual(".abc/.png", WorkPath.ChangeExtension(".abc/.ght", "png")); + Assert.AreEqual("abc/.png", WorkPath.ChangeExtension("abc/.ght", "png")); + + Assert.AreEqual("/abc/def.png", WorkPath.ChangeExtension("/abc/def", "png")); + Assert.AreEqual("/abc/.png", WorkPath.ChangeExtension("/abc/", "png")); + Assert.AreEqual("/abc/def.png", WorkPath.ChangeExtension("/abc/def.ght", "png")); + Assert.AreEqual("/abc/.png", WorkPath.ChangeExtension("/abc/.ght", "png")); + Assert.AreEqual("/.abc/.png", WorkPath.ChangeExtension("/.abc/.ght", "png")); + } + + [TestMethod] + public void WorkPathExistExtensionTest() + { + Assert.IsFalse(WorkPath.HasExtension("")); + Assert.IsFalse(WorkPath.HasExtension("/")); + Assert.IsTrue(WorkPath.HasExtension(".git")); + Assert.IsTrue(WorkPath.HasExtension("abc.git")); + + Assert.IsFalse(WorkPath.HasExtension("abc/def")); + Assert.IsFalse(WorkPath.HasExtension("abc/")); + Assert.IsTrue(WorkPath.HasExtension("abc/def.ght")); + Assert.IsTrue(WorkPath.HasExtension(".abc/.ght")); + Assert.IsTrue(WorkPath.HasExtension("abc/.ght")); + + Assert.IsFalse(WorkPath.HasExtension("/abc/def")); + Assert.IsFalse(WorkPath.HasExtension("/abc/")); + Assert.IsTrue(WorkPath.HasExtension("/abc/def.ght")); + Assert.IsTrue(WorkPath.HasExtension("/abc/.ght")); + Assert.IsTrue(WorkPath.HasExtension("/.abc/.ght")); + } + + [TestMethod] + public void WorkPathGetLastVectorTest() + { + Assert.AreEqual("", WorkPath.GetName("")); + Assert.AreEqual("", WorkPath.GetName("/")); + Assert.AreEqual(".git", WorkPath.GetName(".git")); + Assert.AreEqual("abc.git", WorkPath.GetName("abc.git")); + + Assert.AreEqual("def", WorkPath.GetName("abc/def")); + Assert.AreEqual("", WorkPath.GetName("abc/")); + Assert.AreEqual("def.ght", WorkPath.GetName("abc/def.ght")); + Assert.AreEqual(".ght", WorkPath.GetName(".abc/.ght")); + Assert.AreEqual(".ght", WorkPath.GetName("abc/.ght")); + + Assert.AreEqual("def", WorkPath.GetName("/abc/def")); + Assert.AreEqual("", WorkPath.GetName("/abc/")); + Assert.AreEqual("def.ght", WorkPath.GetName("/abc/def.ght")); + Assert.AreEqual(".ght", WorkPath.GetName("/abc/.ght")); + Assert.AreEqual(".ght", WorkPath.GetName("/.abc/.ght")); + } + + [TestMethod] + public void WorkPathGetLastVectorNoExtensionTest() + { + Assert.AreEqual("", WorkPath.GetNameWithoutExtension("")); + Assert.AreEqual("", WorkPath.GetNameWithoutExtension("/")); + Assert.AreEqual("", WorkPath.GetNameWithoutExtension(".git")); + Assert.AreEqual("abc", WorkPath.GetNameWithoutExtension("abc.git")); + + Assert.AreEqual("def", WorkPath.GetNameWithoutExtension("abc/def")); + Assert.AreEqual("", WorkPath.GetNameWithoutExtension("abc/")); + Assert.AreEqual("def", WorkPath.GetNameWithoutExtension("abc/def.ght")); + Assert.AreEqual("", WorkPath.GetNameWithoutExtension(".abc/.ght")); + Assert.AreEqual("", WorkPath.GetNameWithoutExtension("abc/.ght")); + + Assert.AreEqual("def", WorkPath.GetNameWithoutExtension("/abc/def")); + Assert.AreEqual("", WorkPath.GetNameWithoutExtension("/abc/")); + Assert.AreEqual("def", WorkPath.GetNameWithoutExtension("/abc/def.ght")); + Assert.AreEqual("", WorkPath.GetNameWithoutExtension("/abc/.ght")); + Assert.AreEqual("", WorkPath.GetNameWithoutExtension("/.abc/.ght")); + } + + #region HelperMethod + + private bool ValidPathVectorMatch(string workPath, params string[] subPaths) + { + var count = 0; + if (WorkPath.GetPathVector(workPath).Any(se => se != subPaths[count++])) + return false; + + var list = new List(); + WorkPath.GetPathVector(workPath, list); + + return list.Count == subPaths.Length && count == subPaths.Length; + } + + private bool InvalidPathVectorMatch(string workPath, string errorMessageStarter) where T : Exception + { + try + { + foreach (var _ in WorkPath.GetPathVector(workPath)) + { + } + } + catch (T e) + { + return e.Message.StartsWith(errorMessageStarter); + } + catch (Exception) + { + // ignored + } + + return false; + } + + private bool InvalidPathVectorMatch(string workPath) where T : Exception + { + try + { + foreach (var _ in WorkPath.GetPathVector(workPath)) + { + } + } + catch (T e) + { + return true; + } + catch (Exception) + { + // ignored + } + + return false; + } + + #endregion +} \ No newline at end of file diff --git a/Flawless.Abstraction/Author.cs b/Flawless.Abstraction/Author.cs new file mode 100644 index 0000000..8052cbb --- /dev/null +++ b/Flawless.Abstraction/Author.cs @@ -0,0 +1,32 @@ +namespace Flawless.Abstraction; + +/// +/// An author setup to indicate who create a depot or identify a depot author when uploading it. +/// +public readonly struct Author : IEquatable +{ + public readonly string Name; + + public readonly string Email; + + public Author(string name, string email) + { + Name = name; + Email = email; + } + + public bool Equals(Author other) + { + return Name == other.Name && Email == other.Email; + } + + public override bool Equals(object? obj) + { + return obj is Author other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(Name, Email); + } +} \ No newline at end of file diff --git a/Flawless.Abstraction/Exceptions/FlawlessException.cs b/Flawless.Abstraction/Exceptions/FlawlessException.cs new file mode 100644 index 0000000..6146751 --- /dev/null +++ b/Flawless.Abstraction/Exceptions/FlawlessException.cs @@ -0,0 +1,16 @@ +using System.Runtime.Serialization; + +namespace Flawless.Abstraction.Exceptions; + +public class FlawlessException : Exception +{ + public FlawlessException() + { + } + + protected FlawlessException(SerializationInfo info, StreamingContext context) : base(info, context) {} + + public FlawlessException(string? message) : base(message) {} + + public FlawlessException(string? message, Exception? innerException) : base(message, innerException) {} +} \ No newline at end of file diff --git a/Flawless.Abstraction/Exceptions/PlatformPathNonManagedException.cs b/Flawless.Abstraction/Exceptions/PlatformPathNonManagedException.cs new file mode 100644 index 0000000..9597151 --- /dev/null +++ b/Flawless.Abstraction/Exceptions/PlatformPathNonManagedException.cs @@ -0,0 +1,14 @@ +namespace Flawless.Abstraction.Exceptions; + +/// +/// When doing path transformation between platform and work, if given path is not inside of working directory, trigger +/// this exception to let user know given path is incorrect. +/// +public class PlatformPathNonManagedException(string issuePlatformPath, string platformWorkingDirectory) + : FlawlessException( + $"Platform path '{issuePlatformPath}' is not inside of working directory '{platformWorkingDirectory}'") +{ + public readonly string IssuePlatformPath = issuePlatformPath; + + public readonly string PlatformWorkingDirectory = platformWorkingDirectory; +} \ No newline at end of file diff --git a/Flawless.Shared/Flawless.Shared.csproj b/Flawless.Abstraction/Flawless.Abstraction.csproj similarity index 77% rename from Flawless.Shared/Flawless.Shared.csproj rename to Flawless.Abstraction/Flawless.Abstraction.csproj index 3a63532..17b910f 100644 --- a/Flawless.Shared/Flawless.Shared.csproj +++ b/Flawless.Abstraction/Flawless.Abstraction.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable diff --git a/Flawless.Abstraction/HashID.cs b/Flawless.Abstraction/HashID.cs new file mode 100644 index 0000000..f11f73b --- /dev/null +++ b/Flawless.Abstraction/HashID.cs @@ -0,0 +1,50 @@ +using System.Security.Cryptography; + +namespace Flawless.Abstraction; + +/// +/// An MD5 based hash code storage. +/// +[Serializable] +public readonly struct HashId : IEquatable +{ + public static HashId Empty { get; } = new(0); + + private readonly UInt128 _hash; + + public HashId(UInt128 hash) + { + _hash = hash; + } + + public bool Equals(HashId other) + { + return _hash == other._hash; + } + + public override bool Equals(object? obj) + { + return obj is HashId other && Equals(other); + } + + public override int GetHashCode() + { + return _hash.GetHashCode(); + } +} + +public static class HashIdExtensions +{ + public static HashId ToHashId(Stream input) + { + UInt128 tmp = 0; + var d = MD5.HashData(input); + for (var i = 0; i < d.Length && i < MD5.HashSizeInBytes; i++) + { + UInt128 adder = d[i]; + tmp += adder << (i * 8); + } + + return new HashId(tmp); + } +} \ No newline at end of file diff --git a/Flawless.Abstraction/IDepotConnection.cs b/Flawless.Abstraction/IDepotConnection.cs new file mode 100644 index 0000000..64cd7a3 --- /dev/null +++ b/Flawless.Abstraction/IDepotConnection.cs @@ -0,0 +1,14 @@ +namespace Flawless.Abstraction; + +/// +/// Standardized interface for depot to represent a depot inner data handles. +/// +public interface IDepotConnection : IDisposable + +{ + public IReadonlyRepository Repository { get; } + + public IDepotLabel Label { get; } + + public IDepotStorage Storage { get; } +} \ No newline at end of file diff --git a/Flawless.Abstraction/IDepotLabel.cs b/Flawless.Abstraction/IDepotLabel.cs new file mode 100644 index 0000000..106ce1d --- /dev/null +++ b/Flawless.Abstraction/IDepotLabel.cs @@ -0,0 +1,11 @@ +namespace Flawless.Abstraction; + +/// +/// Standardized interface for any platform to describe data block (Not the actual data) in repository. +/// +public interface IDepotLabel +{ + public abstract HashId Id { get; } + + public IEnumerable Dependencies { get; } +} \ No newline at end of file diff --git a/Flawless.Abstraction/IDepotStorage.cs b/Flawless.Abstraction/IDepotStorage.cs new file mode 100644 index 0000000..0e90309 --- /dev/null +++ b/Flawless.Abstraction/IDepotStorage.cs @@ -0,0 +1,9 @@ +namespace Flawless.Abstraction; + +/// +/// Standardized interface to translate a depot to binary stream which also provides a stream lifetime management. +/// +public interface IDepotStorage +{ + +} \ No newline at end of file diff --git a/Flawless.Abstraction/IOccupationChart.cs b/Flawless.Abstraction/IOccupationChart.cs new file mode 100644 index 0000000..150250c --- /dev/null +++ b/Flawless.Abstraction/IOccupationChart.cs @@ -0,0 +1,9 @@ +namespace Flawless.Abstraction; + +/// +/// Standard interface for repository to store file/directory lock info in this repository. +/// +public interface IOccupationChart +{ + +} \ No newline at end of file diff --git a/Flawless.Abstraction/IReadonlyRepository.cs b/Flawless.Abstraction/IReadonlyRepository.cs new file mode 100644 index 0000000..7b1fe1f --- /dev/null +++ b/Flawless.Abstraction/IReadonlyRepository.cs @@ -0,0 +1,24 @@ +namespace Flawless.Abstraction; + +/// +/// Standardized interface to describe a place to store depots and how they connected with each other. +/// +public interface IReadonlyRepository +{ + public bool IsReadonly { get; } + + + public uint GetLatestCommitId(); + + public IEnumerable GetCommits(); + + public RepositoryCommit? GetCommitById(uint commitId); + + + public Task GetLatestCommitIdAsync(CancellationToken cancellationToken = default); + + public IAsyncEnumerable GetCommitsAsync(CancellationToken cancellationToken = default); + + public Task GetCommitByIdAsync(uint commitId, CancellationToken cancellationToken = default); + +} \ No newline at end of file diff --git a/Flawless.Abstraction/IRepository.cs b/Flawless.Abstraction/IRepository.cs new file mode 100644 index 0000000..54a4150 --- /dev/null +++ b/Flawless.Abstraction/IRepository.cs @@ -0,0 +1,25 @@ +namespace Flawless.Abstraction; + +/// +/// Standardized interface to describe a place to store depots and how they connected with each other with write support. +/// +public interface IRepository : IReadonlyRepository +{ + public IWorkspace Workspace { get; } + + public IOccupationChart OccupationChart { get; } + + + public uint GetActiveCommitId(); + + public RepositoryCommit SubmitWorkspace(); + + public void SyncOccupationChart(); + + + public Task GetActiveCommitIdAsync(CancellationToken cancellationToken = default); + + public Task SubmitWorkspaceAsync(CancellationToken cancellationToken = default); + + public Task SyncOccupationChartAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Flawless.Abstraction/IWorkspace.cs b/Flawless.Abstraction/IWorkspace.cs new file mode 100644 index 0000000..102b04a --- /dev/null +++ b/Flawless.Abstraction/IWorkspace.cs @@ -0,0 +1,9 @@ +namespace Flawless.Abstraction; + +/// +/// Standardized interface for repository working area (Which means those changes are not committed yet. +/// +public interface IWorkspace +{ + public string Message { get; set; } +} \ No newline at end of file diff --git a/Flawless.Abstraction/RepositoryCommit.cs b/Flawless.Abstraction/RepositoryCommit.cs new file mode 100644 index 0000000..9d9653c --- /dev/null +++ b/Flawless.Abstraction/RepositoryCommit.cs @@ -0,0 +1,16 @@ +namespace Flawless.Abstraction; + +public abstract class RepositoryCommit +{ + public abstract IReadonlyRepository Repository { get; } + + public abstract UInt64 Id { get; } + + public abstract Author Author { get; } + + public abstract DateTime CommitTime { get; } + + public abstract string Message { get; } + + public abstract IDepotLabel Depot { get; } +} \ No newline at end of file diff --git a/Flawless.Abstraction/WorkPath.cs b/Flawless.Abstraction/WorkPath.cs new file mode 100644 index 0000000..5f91e0d --- /dev/null +++ b/Flawless.Abstraction/WorkPath.cs @@ -0,0 +1,519 @@ +using System.Text; +using Flawless.Abstraction.Exceptions; + +namespace Flawless.Abstraction; + +/// +/// A platform-independent path system for version controlling which provides a safe and easy used path calculation. +/// Some of those function is a wrapper of with ensure of correction. +/// +public static class WorkPath +{ + + /* What is a valid work path? + * + * A worm path is something like this: + * + * root/subfolder1/subfolder2/file.txt + * + * 1. Should being trim at anytime + * 2. Every separated name should being trim at anytime + * 3. Should not start or end with '/' + * 4. Directory separator is '/' + * 5. Can be converted into and from any platform and self to be platform standalone + * 6. Without any irregular or invisible character. + */ + + private static readonly char[] InvalidPathChars = [ + '\"', '\\', '<', '>', '|', '?', '*', ':', '^', '%', + (char)0, (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, + (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, + (char)17, (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, + (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31]; + + private static readonly HashSet InvalidPathCharsQuickTest = new(InvalidPathChars); + + public const char DirectorySeparatorChar = '/'; + + /// + /// Check if path contains any invalid characters. Do not guarantee path is valid. + /// + /// Tested path. + /// If there has no invalid path, return true. + public static bool IsPathHasInvalidPathChars(string workPath) + { + foreach (var c in workPath) + { + if (!InvalidPathCharsQuickTest.Contains(c)) return true; + } + + return false; + } + + /// + /// Get an array of invalid characters. Do not guarantee path is valid. + /// + /// Array of invalid characters + public static char[] GetInvalidPathChars() + { + var r = new char[InvalidPathChars.Length]; + InvalidPathChars.CopyTo(r, 0); + return r; + } + + /// + /// Convert a work path into current platform file system path. Do not guarantee path is valid. + /// + /// A work path in this repository. This path will not do any validation. + /// The root path of repository in platform path. + /// A platform path mapping from work path. + public static string ToPlatformPath(string workPath, string platformWorkingDirectory) + { + var sb = new StringBuilder(workPath.Length + platformWorkingDirectory.Length); + sb.Append(platformWorkingDirectory); + + if (!Path.EndsInDirectorySeparator(platformWorkingDirectory)) sb.Append(Path.DirectorySeparatorChar); + foreach (var c in workPath) + sb.Append(c == DirectorySeparatorChar ? Path.DirectorySeparatorChar : c); + + return sb.ToString(); + } + + /// + /// Convert a platform-specific file system path into work path. Do not guarantee path is valid. + /// + /// A platform path. + /// The root path of repository in platform path. + /// Work path mapping from platform path . + /// If platform path is not a sub entity in platform working path, + /// this path will not being managed. So make error. + public static string FromPlatformPath(string platformPath, string platformWorkingDirectory) + { + var workPath = Path.GetRelativePath(platformWorkingDirectory, platformPath); + if (workPath == ".") return string.Empty; + if (workPath == platformPath) // If not share same root, it will return platform path. So compare it directly. + throw new PlatformPathNonManagedException(platformPath, platformWorkingDirectory); + + var sb = new StringBuilder(workPath.Length); + foreach (var c in workPath) + { + var isSplitChar = c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + if (sb.Length == 0 && isSplitChar) continue; + + sb.Append(isSplitChar ? DirectorySeparatorChar : c); + } + + return sb.ToString(); + } + + /// + /// Split work path into path vector. + /// + /// The path will being split. + /// The list to store result (Non-allocate) + /// The count of added elements + /// Argument is null + /// Work path is invalid + public static int GetPathVector(string workPath, List result) + { + ArgumentNullException.ThrowIfNull(result); + + if (string.IsNullOrWhiteSpace(workPath)) + throw new ArgumentNullException(nameof(workPath), "Not a valid work path!"); + + if (workPath[0] == DirectorySeparatorChar) + throw new ArgumentException("Work path cannot start with a DirectorySeparatorChar!", nameof(workPath)); + + var start = 0; + var end = 0; + var count = 0; + for (var i = 0; i <= workPath.Length; i++) + { + + if (i < workPath.Length) + { + var c = workPath[i]; + if (InvalidPathChars.Contains(c)) + throw new ArgumentException("Invalid work path character: " + c); + + if (c != DirectorySeparatorChar) + { + end = i; + continue; + } + } + + if (start >= end) throw new ArgumentException("Work path contains empty vector!"); + if (workPath[end] == ' ' || workPath[start] == ' ') + throw new ArgumentException("Work path vector can not start or end with a space!"); + + result.Add(workPath.Substring(start, end - start + 1)); + count++; + start = i + 1; + } + + return count; + } + + /// + /// Split work path into path vector. + /// + /// The path will being split. + /// Enumerable of vector + /// Argument is null + /// Work path is invalid + public static IEnumerable GetPathVector(string workPath) + { + if (string.IsNullOrWhiteSpace(workPath)) + throw new ArgumentNullException(nameof(workPath), "Not a valid work path!"); + + if (workPath[0] == DirectorySeparatorChar) + throw new ArgumentException("Work path cannot start with a DirectorySeparatorChar!", nameof(workPath)); + + var start = 0; + var end = 0; + for (var i = 0; i <= workPath.Length; i++) + { + + if (i < workPath.Length) + { + var c = workPath[i]; + if (InvalidPathChars.Contains(c)) + throw new ArgumentException("Invalid work path character: " + c); + + if (c != DirectorySeparatorChar) + { + end = i; + continue; + } + } + + if (start >= end) throw new ArgumentException("Work path contains empty vector!"); + if (workPath[end] == ' ' || workPath[start] == ' ') + throw new ArgumentException("Work path vector can not start or end with a space!"); + + yield return workPath.Substring(start, end - start + 1); + start = i + 1; + } + } + + /// + /// Check work path is legal but not ensure that existed in platform. + /// + /// The path will being tested. + /// True when path is valid. If you need more details, consider use + /// . + public static bool IsPathValid(string workPath) + { + if (string.IsNullOrWhiteSpace(workPath) || workPath[0] == DirectorySeparatorChar) return false; + + var start = 0; + var end = 0; + for (var i = 0; i <= workPath.Length; i++) + { + + if (i < workPath.Length) + { + var c = workPath[i]; + if (InvalidPathChars.Contains(c)) return false; + + if (c != DirectorySeparatorChar) + { + end = i; + continue; + } + } + + if (start >= end || end >= workPath.Length || start >= workPath.Length || + workPath[end] == ' ' || workPath[start] == ' ') + return false; + + start = i + 1; + } + + return true; + } + + /// + /// Check if last vector has a valid extension. + /// + /// Targeting work path + /// Is valid. + public static bool HasExtension(string workPath) + { + for (var i = workPath.Length - 1; i >= 0; i--) + { + var c = workPath[i]; + if (c == DirectorySeparatorChar) break; + if (c == '.') return true; + } + + return false; + } + + /// + /// Get the last vector extension. + /// + /// Targeting work path + /// The name of last vector extension. If the last vector is empty or no valid extension existed, + /// return empty. + public static string GetExtension(string workPath) + { + for (var i = workPath.Length - 1; i >= 0; i--) + { + var c = workPath[i]; + if (c == DirectorySeparatorChar) break; + if (c == '.') return i + 1 >= workPath.Length ? String.Empty : workPath.Substring(i + 1); + } + + return string.Empty; + } + + /// + /// Change the last vector extension. + /// + /// Targeting work path + /// Targeting extension, null means clean the extension + /// Modified path. + public static string ChangeExtension(string workPath, string? extension) + { + var hasExtension = extension != null; + for (var i = workPath.Length - 1; i >= 0; i--) + { + var c = workPath[i]; + if (c == DirectorySeparatorChar) break; + if (c == '.') return workPath.Substring(0, hasExtension ? i + 1 : i) + extension; + } + + return hasExtension ? workPath + "." + extension : workPath; + } + + /// + /// Get the last vector name. + /// + /// Targeting work path + /// The name of last vector name. If the last vector is empty, return empty. + public static string GetName(string workPath) + { + var start = workPath.Length - 1; + var length = 0; + for (var i = workPath.Length - 1; i >= 0; i--) + { + var c = workPath[i]; + if (c == DirectorySeparatorChar) + { + start = i + 1; + break; + } + + start = i; + length += 1; + } + + return start < 0 ? string.Empty : workPath.Substring(start, length); + } + + /// + /// Get the last vector name without extension. + /// + /// Targeting work path + /// The name of last vector without extension. If the last vector is empty, return empty. + public static string GetNameWithoutExtension(string workPath) + { + var start = workPath.Length - 1; + var end = workPath.Length; + for (var i = workPath.Length - 1; i >= 0; i--) + { + var c = workPath[i]; + start = i; + if (end == workPath.Length && c == '.') end = i; + if (c == DirectorySeparatorChar) + { + start += 1; + break; + } + } + + return start < 0 || end <= start ? string.Empty : workPath.Substring(start, end - start); + } + + + /// + /// Check if path is a root path. Do not guarantee path is valid. + /// + /// Work path being tested. + /// True when is root. + public static bool IsRootPath(string workPath) + { + return workPath.Contains(DirectorySeparatorChar); + } + + /// + /// Check if path is ended with directory separator. Do not guarantee path is valid. + /// + /// Work path being tested. + /// True when is ended with directory separator. + public static bool EndsInDirectorySeparator(string workPath) + { + return workPath.Length > 0 && workPath[^1] == DirectorySeparatorChar; + } + + /// + /// Get a relative work path from another work path in case first one is a sub path of second one. It will raise + /// exception if path is invalid. + /// + /// The parent one. + /// The child one. + /// Null if workPath is not a child of relatedTo, or empty if workPath equals to relatedTo, or a new path + /// when workPath is child of relatedTo. + public static string? GetRelativePath(string relatedTo, string workPath) + { + if (workPath.Length == 0 || relatedTo.Length == 0) return null; + using var parentTester = GetPathVector(relatedTo).GetEnumerator(); + using var childTester = GetPathVector(workPath).GetEnumerator(); + + while (true) + { + var parentMoveNext = parentTester.MoveNext(); + var childMoveNext = childTester.MoveNext(); + + // Both are not reach end + if (parentMoveNext && childMoveNext) + { + if (parentTester.Current == childTester.Current) continue; + // Or going to break if they are not equal. + } + // Only if child has left but parent has all pass away, this means they share a same parent. + else if (childMoveNext && !parentMoveNext) + { + var sb = new StringBuilder(); + + sb.Append(childTester.Current); + while (childTester.MoveNext()) + sb.Append(DirectorySeparatorChar).Append(childTester.Current); + + return sb.ToString(); + } + // If they are same path, return empty to indicate that. + else if (!childMoveNext && !parentMoveNext) return string.Empty; + + break; + } + + return null; + } + + private static void CombineInternal(StringBuilder sb, string addon) + { + if (addon.Length <= 0) return; + + if (sb.Length <= 0) + { + sb.Append(addon); + return; + } + + var endSep = sb[^1] == DirectorySeparatorChar; + var startSep = addon[0] == DirectorySeparatorChar; + + if (!endSep && !startSep) + { + sb.Append(DirectorySeparatorChar); + sb.Append(addon); + } + else if (endSep && startSep) + { + sb.Append(addon, 1, addon.Length - 1); + } + else + { + sb.Append(addon); + } + } + + /// + /// Combine path one by one. Do not guarantee path is valid. + /// + /// + /// It will connect those path in correct order and separator. + /// + /// Combine("abc", "def") => "abc/def"; + /// Combine("abc", "def/") => "abc/def/"; + /// Combine("abc", "def/gg") => "abc/def/gg"; + /// Combine("abc/", "/def/gg") => "abc/def/gg"; + /// + /// + /// The connected path + public static string Combine(string path1, string path2) + { + var sb = new StringBuilder(); + CombineInternal(sb, path1); + CombineInternal(sb, path2); + return sb.ToString(); + } + + /// + /// Combine path one by one. Do not guarantee path is valid. + /// + /// + /// It will connect those path in correct order and separator. + /// + /// Combine("abc", "def") => "abc/def"; + /// Combine("abc", "def/") => "abc/def/"; + /// Combine("abc", "def/gg") => "abc/def/gg"; + /// Combine("abc/", "/def/gg") => "abc/def/gg"; + /// + /// + /// The connected path + public static string Combine(string path1, string path2, string path3) + { + var sb = new StringBuilder(); + CombineInternal(sb, path1); + CombineInternal(sb, path2); + CombineInternal(sb, path3); + return sb.ToString(); + } + + /// + /// Combine path one by one. Do not guarantee path is valid. + /// + /// + /// It will connect those path in correct order and separator. + /// + /// Combine("abc", "def") => "abc/def"; + /// Combine("abc", "def/") => "abc/def/"; + /// Combine("abc", "def/gg") => "abc/def/gg"; + /// Combine("abc/", "/def/gg") => "abc/def/gg"; + /// + /// + /// The connected path + public static string Combine(string path1, string path2, string path3, string path4) + { + var sb = new StringBuilder(); + CombineInternal(sb, path1); + CombineInternal(sb, path2); + CombineInternal(sb, path3); + CombineInternal(sb, path4); + return sb.ToString(); + } + + /// + /// Combine path one by one. Do not guarantee path is valid. + /// + /// + /// It will connect those path in correct order and separator. + /// + /// Combine("abc", "def") => "abc/def"; + /// Combine("abc", "def/") => "abc/def/"; + /// Combine("abc", "def/gg") => "abc/def/gg"; + /// Combine("abc/", "/def/gg") => "abc/def/gg"; + /// + /// + /// The connected path + public static string Combine(params string[] paths) + { + var sb = new StringBuilder(); + foreach (var path in paths) CombineInternal(sb, path); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/Flawless.Client.Avanonia/App.axaml b/Flawless.Client.Avanonia/App.axaml deleted file mode 100644 index c887737..0000000 --- a/Flawless.Client.Avanonia/App.axaml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Flawless.Client.Avanonia/App.axaml.cs b/Flawless.Client.Avanonia/App.axaml.cs deleted file mode 100644 index 03f0f08..0000000 --- a/Flawless.Client.Avanonia/App.axaml.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Data.Core; -using Avalonia.Data.Core.Plugins; -using Avalonia.Markup.Xaml; -using Flawless.Client.Avanonia.ViewModels; -using Flawless.Client.Avanonia.Views; - -namespace Flawless.Client.Avanonia; - -public partial class App : Application -{ - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - // Line below is needed to remove Avalonia data validation. - // Without this line you will get duplicate validations from both Avalonia and CT - BindingPlugins.DataValidators.RemoveAt(0); - desktop.MainWindow = new MainWindow - { - DataContext = new MainWindowViewModel(), - }; - } - - base.OnFrameworkInitializationCompleted(); - } -} \ No newline at end of file diff --git a/Flawless.Client.Avanonia/Assets/avalonia-logo.ico b/Flawless.Client.Avanonia/Assets/avalonia-logo.ico deleted file mode 100644 index da8d49f..0000000 Binary files a/Flawless.Client.Avanonia/Assets/avalonia-logo.ico and /dev/null differ diff --git a/Flawless.Client.Avanonia/Flawless.Client.Avanonia.csproj b/Flawless.Client.Avanonia/Flawless.Client.Avanonia.csproj deleted file mode 100644 index 4616f60..0000000 --- a/Flawless.Client.Avanonia/Flawless.Client.Avanonia.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - WinExe - net8.0 - enable - true - app.manifest - true - - - - - - - - - - - - - - - - - diff --git a/Flawless.Client.Avanonia/Program.cs b/Flawless.Client.Avanonia/Program.cs deleted file mode 100644 index 1e9c4f5..0000000 --- a/Flawless.Client.Avanonia/Program.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia; -using System; - -namespace Flawless.Client.Avanonia; - -sealed class Program -{ - // Initialization code. Don't use any Avalonia, third-party APIs or any - // SynchronizationContext-reliant code before AppMain is called: things aren't initialized - // yet and stuff might break. - [STAThread] - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); - - // Avalonia configuration, don't remove; also used by visual designer. - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .WithInterFont() - .LogToTrace(); -} \ No newline at end of file diff --git a/Flawless.Client.Avanonia/ViewLocator.cs b/Flawless.Client.Avanonia/ViewLocator.cs deleted file mode 100644 index 61549b2..0000000 --- a/Flawless.Client.Avanonia/ViewLocator.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Controls.Templates; -using Flawless.Client.Avanonia.ViewModels; - -namespace Flawless.Client.Avanonia; - -public class ViewLocator : IDataTemplate -{ - public Control? Build(object? data) - { - if (data is null) - return null; - - var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); - var type = Type.GetType(name); - - if (type != null) - { - var control = (Control)Activator.CreateInstance(type)!; - control.DataContext = data; - return control; - } - - return new TextBlock { Text = "Not Found: " + name }; - } - - public bool Match(object? data) - { - return data is ViewModelBase; - } -} \ No newline at end of file diff --git a/Flawless.Client.Avanonia/ViewModels/MainWindowViewModel.cs b/Flawless.Client.Avanonia/ViewModels/MainWindowViewModel.cs deleted file mode 100644 index 08dc530..0000000 --- a/Flawless.Client.Avanonia/ViewModels/MainWindowViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Flawless.Client.Avanonia.ViewModels; - -public partial class MainWindowViewModel : ViewModelBase -{ -#pragma warning disable CA1822 // Mark members as static - public string Greeting => "Welcome to Avalonia!"; -#pragma warning restore CA1822 // Mark members as static -} \ No newline at end of file diff --git a/Flawless.Client.Avanonia/ViewModels/ViewModelBase.cs b/Flawless.Client.Avanonia/ViewModels/ViewModelBase.cs deleted file mode 100644 index 154c60b..0000000 --- a/Flawless.Client.Avanonia/ViewModels/ViewModelBase.cs +++ /dev/null @@ -1,7 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; - -namespace Flawless.Client.Avanonia.ViewModels; - -public class ViewModelBase : ObservableObject -{ -} \ No newline at end of file diff --git a/Flawless.Client.Avanonia/Views/MainWindow.axaml b/Flawless.Client.Avanonia/Views/MainWindow.axaml deleted file mode 100644 index ab1264d..0000000 --- a/Flawless.Client.Avanonia/Views/MainWindow.axaml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - diff --git a/Flawless.Client.Avanonia/Views/MainWindow.axaml.cs b/Flawless.Client.Avanonia/Views/MainWindow.axaml.cs deleted file mode 100644 index 9d1722d..0000000 --- a/Flawless.Client.Avanonia/Views/MainWindow.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace Flawless.Client.Avanonia.Views; - -public partial class MainWindow : Window -{ - public MainWindow() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/Flawless.Client.Avanonia/app.manifest b/Flawless.Client.Avanonia/app.manifest deleted file mode 100644 index ad7f15d..0000000 --- a/Flawless.Client.Avanonia/app.manifest +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - diff --git a/Flawless.Client/Flawless.Client.csproj b/Flawless.Client/Flawless.Client.csproj deleted file mode 100644 index e6faf30..0000000 --- a/Flawless.Client/Flawless.Client.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - diff --git a/Flawless.Server/Flawless.Server.csproj b/Flawless.Server/Flawless.Server.csproj deleted file mode 100644 index 6b34b82..0000000 --- a/Flawless.Server/Flawless.Server.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - Protos\auth.proto - - - Protos\auth_token.proto - - - - diff --git a/Flawless.Server/Program.cs b/Flawless.Server/Program.cs deleted file mode 100644 index 3bbd001..0000000 --- a/Flawless.Server/Program.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Flawless.Server.Services; -using Flawless.Server.Utility; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; - -bool init = true; -while (init) -{ - init = false; - var builder = WebApplication.CreateBuilder(args); - builder.Services.AddGrpc(x => - { - }); - - builder.Services.AddAuthentication(x => - { - x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }).AddJwtBearer(o => - { - o.TokenValidationParameters = new TokenValidationParameters() - { - ValidateIssuer = true, - RequireExpirationTime = true, - ValidateAudience = true, - ValidateIssuerSigningKey = true, - IssuerSigningKey = AuthUtility.SecurityKey, - ValidIssuer = AuthUtility.Issuer, - ValidAudience = AuthUtility.Audience, - ClockSkew = TimeSpan.Zero, - }; - }); - - builder.Services.AddAuthorization(); - - using var app = builder.Build(); - - // Enable call router - app.UseRouting(); - - // Enable authentication - app.UseAuthentication(); - app.UseAuthorization(); - - // Configure the HTTP request pipeline. - app.MapGrpcService(); - app.MapGet("/", - () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); - - app.Run(); -} diff --git a/Flawless.Server/Properties/launchSettings.json b/Flawless.Server/Properties/launchSettings.json deleted file mode 100644 index 7a3f517..0000000 --- a/Flawless.Server/Properties/launchSettings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5150", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "https://localhost:7004;http://localhost:5150", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/Flawless.Server/Services/AuthService.cs b/Flawless.Server/Services/AuthService.cs deleted file mode 100644 index ae71999..0000000 --- a/Flawless.Server/Services/AuthService.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Flawless.Api; -using Flawless.Server.Utility; -using Google.Protobuf.WellKnownTypes; -using Grpc.Core; -using Microsoft.AspNetCore.Authorization; - -namespace Flawless.Server.Services; - -public class AuthService : Auth.AuthBase -{ - - private ILogger _logger; - - public AuthService(ILogger logger) - { - _logger = logger; - } - - public override Task GainToken(AuthRequest request, ServerCallContext context) - { - - if (request.UserName != "admin") - { - return Task.FromResult(new AuthResult() - { - Token = "", - Result = -1, - Message = "Invalid username or password" - }); - } - - var token = AuthUtility.GenerateJwtToken(request.UserName, request.Expires); - - _logger.LogInformation($"User '{request.UserName}' has been login in.'"); - return Task.FromResult(new AuthResult - { - Token = token, - Result = 0 - }); - } - - [Authorize] - public override Task GetUserInfo(Empty request, ServerCallContext context) - { - return Task.FromResult(new AuthUserInfo - { - UserName = context.GetHttpContext().User.Identity?.Name ?? string.Empty, - IsSystemAdmin = true, - UserId = 1000 - }); - } - - [Authorize] - public override Task Validate(Empty request, ServerCallContext context) - { - return Task.FromResult(new Empty()); - } -} \ No newline at end of file diff --git a/Flawless.Server/Utility/AuthUtility.cs b/Flawless.Server/Utility/AuthUtility.cs deleted file mode 100644 index 650efb8..0000000 --- a/Flawless.Server/Utility/AuthUtility.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; -using Microsoft.IdentityModel.Tokens; - -namespace Flawless.Server.Utility; - -public static class AuthUtility -{ - private static JwtSecurityTokenHandler _tokenHandler = new(); - - private static SymmetricSecurityKey? _key; - - public static string GenerateSecret( - string randomRange = "abcdefghijklmnopqrstuvwxyz1234567890!@#$%^&*()_+=-", - int length = 256 / 8) - { - var rng = Random.Shared; - - String ran = ""; - for (int i = 0; i < length; i++) - { - int x = rng.Next(randomRange.Length); - ran += randomRange[x]; - } - - return ran; - } - - public static string JwtSecret { get; private set; } = GenerateSecret(); - - public static string Issuer { get; private set; } = Environment.GetEnvironmentVariable("issuer") ?? "jwt"; - - public static string Audience { get; private set; } = Environment.GetEnvironmentVariable("audience") ?? "jwt"; - - public static SymmetricSecurityKey SecurityKey - { - get - { - if (_key == null) _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret)); - return _key; - } - } - - public static void ResetKey(string issuer, string audience, string? keySecret = null) - { - JwtSecret = keySecret ?? GenerateSecret(); - Issuer = issuer; - Audience = audience; - _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret)); - } - - public static string GenerateJwtToken(string username, uint expires) - { - var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256Signature); - var claims = new List - { - new (ClaimTypes.Name, username), - }; - - var token = _tokenHandler.CreateJwtSecurityToken( - issuer: Issuer, - audience: Audience, - subject: new ClaimsIdentity(claims), - expires: DateTime.Now.AddSeconds(expires), - issuedAt: DateTime.Now, - notBefore: DateTime.Now, - signingCredentials: credentials); - - return _tokenHandler.WriteToken(token); - } -} \ No newline at end of file diff --git a/Flawless.Server/appsettings.Development.json b/Flawless.Server/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/Flawless.Server/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/Flawless.Server/appsettings.json b/Flawless.Server/appsettings.json deleted file mode 100644 index 1aef507..0000000 --- a/Flawless.Server/appsettings.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "Kestrel": { - "EndpointDefaults": { - "Protocols": "Http2" - } - } -} diff --git a/Flawless.Shared/Protos/auth.proto b/Flawless.Shared/Protos/auth.proto deleted file mode 100644 index 8290964..0000000 --- a/Flawless.Shared/Protos/auth.proto +++ /dev/null @@ -1,142 +0,0 @@ -syntax = "proto3"; -option csharp_namespace = "Flawless.Api"; -package Auth; - -import "google/protobuf/wrappers.proto"; -import "google/protobuf/empty.proto"; - -// Service state detector -service ServiceStatus { - rpc ValidateAuth(google.protobuf.Empty) returns (google.protobuf.Empty); - rpc AbleLogin(google.protobuf.Empty) returns (google.protobuf.BoolValue); - rpc AbleRegister(google.protobuf.Empty) returns (google.protobuf.BoolValue); -} - -//// TOKEN //// - -// Token management -service Token { - rpc Register(RegisterTokenRequest) returns (TokenResponse); - rpc Login(LoginTokenRequest) returns (TokenResponse); - rpc Refresh(RefreshTokenRequest) returns (TokenResponse); -} - -message RegisterTokenRequest { - string user_name = 1; - optional string nick_name = 2; - string mail_address = 3; - string password = 4; -} - -message LoginTokenRequest { - string user_name = 1; - string password = 2; - optional uint32 expired_timeout = 3; -} - -message RefreshTokenRequest { - uint64 user_id = 1; - string renew_token = 2; -} - -message TokenResponse { - TokenResponseResult type = 1; - optional string details = 2; - optional TokenDetails token = 3; -} - -message TokenDetails { - uint64 user_id = 1; - string jwt_token = 2; - string renew_token = 3; -} - -enum TokenResponseResult { - SUCCESS = 0; - UNTOLD = -1;; - - // Standard login errors - INVALID_USERNAME_OR_PASSWORD = -101; - REQUIRE_CHALLENGE = -102; - LIMIT_LOGIN_RATE = -103; - LIMIT_LOGIN_TIME_TODAY = -104; - - // For renew token - REQUIRE_LOGIN_AGAIN = -200; - - // For register - REGISTER_MAIL_OCCUPIED = -300; - REGISTER_USERNAME_OCCUPIED = -301; - REGISTER_STATE_CLOSED = -302; -} - -//// USER //// - -service User { - rpc GetAvatar(GetUserAvatarRequest) returns (stream GetUserAvatarResponse); - rpc GetInfo(GetUserInfoRequest) returns (GetUserInfoResponse); - rpc SetAvatar(stream SetUserAvatarRequest) returns (SetUserDataResponse); - rpc SetInfo(SetUserInfoRequest) returns (SetUserDataResponse); - rpc SetPassword(SetLoginUserPasswordRequest) returns (SetUserDataResponse); -} - -message GetUserInfoRequest { - oneof match_type { - string user_name = 1; - uint64 user_id = 2; - } -} - -message GetUserAvatarRequest { - uint64 user_id = 1; -} - -message SetUserAvatarRequest { - uint64 user_id = 1; - bytes data = 2; -} - -message SetUserInfoRequest { - optional string user_name = 1; - optional string mail_address = 2; - optional string phone_number = 3; - optional UserSex user_sex = 4; - optional string user_bio = 5; -} - -message SetLoginUserPasswordRequest { - string new_password = 1; - oneof ownership_validation { - string old_password = 2; - string temp_code = 3; - } -} - -message GetUserAvatarResponse { - uint64 user_id = 1; - string file_name = 2; - bytes data = 3; -} - -message GetUserInfoResponse { - uint64 user_id = 1; - string user_name = 2; - string mail_address = 3; - uint64 last_login = 4; - optional string phone_number = 6; - optional UserSex user_sex = 7; - optional string user_bio = 8; -} - -message SetUserDataResponse { - bool success = 1; - optional string details = 2; -} - -enum UserSex -{ - UNSET = 0; - MALE = 1; - FEMALE = 2; - WALMART_PLASTIC_BAG = 3; -} \ No newline at end of file diff --git a/Flawless.Test.ConsoleApplication/Flawless.Test.ConsoleApplication.csproj b/Flawless.Test.ConsoleApplication/Flawless.Test.ConsoleApplication.csproj deleted file mode 100644 index 45191a0..0000000 --- a/Flawless.Test.ConsoleApplication/Flawless.Test.ConsoleApplication.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - diff --git a/Flawless.Test.ConsoleApplication/Program.cs b/Flawless.Test.ConsoleApplication/Program.cs deleted file mode 100644 index ccfad5e..0000000 --- a/Flawless.Test.ConsoleApplication/Program.cs +++ /dev/null @@ -1,31 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -using Flawless.Api; -using Google.Protobuf.WellKnownTypes; -using Grpc.Core; -using Grpc.Net.Client; - -var path = "http://localhost:5150"; - -var rpcChannel = GrpcChannel.ForAddress(path); -var authService = new Auth.AuthClient(rpcChannel); - -var result = await authService.GainTokenAsync(new AuthRequest() -{ - UserName = "admin", - Expires = 10, - Password = "password" -}); - - -if (result.Result == 0) -{ - Console.WriteLine($"Token granted: {result.Token}"); - - // Thread.Sleep(5 * 1000); - var userInfo = await authService.GetUserInfoAsync(new Empty(), new Metadata() - { - { "Authorization", $"Bearer {result.Token}" } - }); - Console.WriteLine($"UserName: {userInfo.UserName}\nUID: {userInfo.UserId}\nIs Admin: {userInfo.IsSystemAdmin}"); -} \ No newline at end of file