1
0

Reconstruct this project from new.

This commit is contained in:
Ca2didi 2025-03-15 21:11:07 +08:00
parent d83ba632e0
commit d766aacecc
41 changed files with 1084 additions and 682 deletions

View File

@ -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

View File

@ -1,4 +1,18 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArgumentOutOfRangeException_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F82_003F5d81019e_003FArgumentOutOfRangeException_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssert_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea501b1a950043b99f3df638f1824d6143a18_003Fb8_003Fb16d6a68_003FAssert_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaimsPrincipal_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf5ce3f8ae0647439d514bb1a3c7f96b13600_003F20_003Fabeaf9ae_003FClaimsPrincipal_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaimTypes_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf5ce3f8ae0647439d514bb1a3c7f96b13600_003Fbd_003F4cde67a5_003FClaimTypes_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStringValue_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee39d1e9346e41aa9d44f0e1b1c6630f76268_003F49_003Fb92346b2_003FStringValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F28_003F6a41ec86_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStringValue_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee39d1e9346e41aa9d44f0e1b1c6630f76268_003F49_003Fb92346b2_003FStringValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATestMethodInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5ef53d675c5d34a6b85963919015dc0c1b06e5ea9834aac59ae6911f4c6f38_003FTestMethodInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5573ef9_002Db554_002D4a56_002D82c4_002D2531c8feef65/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=f3f8a684_002Dc08e_002D489f_002D949c_002D6c38a1ed63b0/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="MSTest" Version="3.6.4"/>
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Flawless.Abstraction\Flawless.Abstraction.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]

View File

@ -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<ArgumentException>("ab\\c/def", "Invalid work path character"));
Assert.IsTrue(InvalidPathVectorMatch<ArgumentException>("ab?c/def", "Invalid work path character"));
Assert.IsTrue(InvalidPathVectorMatch<ArgumentException>(" abc/def/ghi/k.lmn ", "Work path vector can not start or end with a space!"));
Assert.IsTrue(InvalidPathVectorMatch<ArgumentException>("/abc/def", "Work path cannot start with a DirectorySeparatorChar!"));
Assert.IsTrue(InvalidPathVectorMatch<ArgumentException>("/abc/def/", "Work path cannot start with a DirectorySeparatorChar!"));
Assert.IsTrue(InvalidPathVectorMatch<ArgumentNullException>("", "Not a valid work path!"));
Assert.IsTrue(InvalidPathVectorMatch<ArgumentNullException>(null!, "Not a valid work path!"));
Assert.IsTrue(InvalidPathVectorMatch<ArgumentException>("abc/ de f/ghi/k.lmn", "Work path vector can not start or end with a space!"));
Assert.IsTrue(InvalidPathVectorMatch<ArgumentException>("abc/def/", "Work path contains empty vector!"));
Assert.IsTrue(InvalidPathVectorMatch<ArgumentException>("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<string>();
WorkPath.GetPathVector(workPath, list);
return list.Count == subPaths.Length && count == subPaths.Length;
}
private bool InvalidPathVectorMatch<T>(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<T>(string workPath) where T : Exception
{
try
{
foreach (var _ in WorkPath.GetPathVector(workPath))
{
}
}
catch (T e)
{
return true;
}
catch (Exception)
{
// ignored
}
return false;
}
#endregion
}

View File

@ -0,0 +1,32 @@
namespace Flawless.Abstraction;
/// <summary>
/// An author setup to indicate who create a depot or identify a depot author when uploading it.
/// </summary>
public readonly struct Author : IEquatable<Author>
{
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);
}
}

View File

@ -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) {}
}

View File

@ -0,0 +1,14 @@
namespace Flawless.Abstraction.Exceptions;
/// <summary>
/// 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.
/// </summary>
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;
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@ -0,0 +1,50 @@
using System.Security.Cryptography;
namespace Flawless.Abstraction;
/// <summary>
/// An MD5 based hash code storage.
/// </summary>
[Serializable]
public readonly struct HashId : IEquatable<HashId>
{
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);
}
}

View File

@ -0,0 +1,14 @@
namespace Flawless.Abstraction;
/// <summary>
/// Standardized interface for depot to represent a depot inner data handles.
/// </summary>
public interface IDepotConnection : IDisposable
{
public IReadonlyRepository Repository { get; }
public IDepotLabel Label { get; }
public IDepotStorage Storage { get; }
}

View File

@ -0,0 +1,11 @@
namespace Flawless.Abstraction;
/// <summary>
/// Standardized interface for any platform to describe data block (Not the actual data) in repository.
/// </summary>
public interface IDepotLabel
{
public abstract HashId Id { get; }
public IEnumerable<HashId> Dependencies { get; }
}

View File

@ -0,0 +1,9 @@
namespace Flawless.Abstraction;
/// <summary>
/// Standardized interface to translate a depot to binary stream which also provides a stream lifetime management.
/// </summary>
public interface IDepotStorage
{
}

View File

@ -0,0 +1,9 @@
namespace Flawless.Abstraction;
/// <summary>
/// Standard interface for repository to store file/directory lock info in this repository.
/// </summary>
public interface IOccupationChart
{
}

View File

@ -0,0 +1,24 @@
namespace Flawless.Abstraction;
/// <summary>
/// Standardized interface to describe a place to store depots and how they connected with each other.
/// </summary>
public interface IReadonlyRepository
{
public bool IsReadonly { get; }
public uint GetLatestCommitId();
public IEnumerable<RepositoryCommit> GetCommits();
public RepositoryCommit? GetCommitById(uint commitId);
public Task<uint> GetLatestCommitIdAsync(CancellationToken cancellationToken = default);
public IAsyncEnumerable<RepositoryCommit> GetCommitsAsync(CancellationToken cancellationToken = default);
public Task<RepositoryCommit?> GetCommitByIdAsync(uint commitId, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,25 @@
namespace Flawless.Abstraction;
/// <summary>
/// Standardized interface to describe a place to store depots and how they connected with each other with write support.
/// </summary>
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<RepositoryCommit> SubmitWorkspaceAsync(CancellationToken cancellationToken = default);
public Task SyncOccupationChartAsync(CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,9 @@
namespace Flawless.Abstraction;
/// <summary>
/// Standardized interface for repository working area (Which means those changes are not committed yet.
/// </summary>
public interface IWorkspace
{
public string Message { get; set; }
}

View File

@ -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; }
}

View File

@ -0,0 +1,519 @@
using System.Text;
using Flawless.Abstraction.Exceptions;
namespace Flawless.Abstraction;
/// <summary>
/// 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 <see cref="Path"/> with ensure of correction.
/// </summary>
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<char> InvalidPathCharsQuickTest = new(InvalidPathChars);
public const char DirectorySeparatorChar = '/';
/// <summary>
/// Check if path contains any invalid characters. Do not guarantee path is valid.
/// </summary>
/// <param name="workPath">Tested path.</param>
/// <returns>If there has no invalid path, return true.</returns>
public static bool IsPathHasInvalidPathChars(string workPath)
{
foreach (var c in workPath)
{
if (!InvalidPathCharsQuickTest.Contains(c)) return true;
}
return false;
}
/// <summary>
/// Get an array of invalid characters. Do not guarantee path is valid.
/// </summary>
/// <returns>Array of invalid characters</returns>
public static char[] GetInvalidPathChars()
{
var r = new char[InvalidPathChars.Length];
InvalidPathChars.CopyTo(r, 0);
return r;
}
/// <summary>
/// Convert a work path into current platform file system path. Do not guarantee path is valid.
/// </summary>
/// <param name="workPath">A work path in this repository. This path will not do any validation.</param>
/// <param name="platformWorkingDirectory">The root path of repository in platform path.</param>
/// <returns>A platform path mapping from work path.</returns>
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();
}
/// <summary>
/// Convert a platform-specific file system path into work path. Do not guarantee path is valid.
/// </summary>
/// <param name="platformPath">A platform path.</param>
/// <param name="platformWorkingDirectory">The root path of repository in platform path.</param>
/// <returns>Work path mapping from platform path .</returns>
/// <exception cref="PlatformPathNonManagedException">If platform path is not a sub entity in platform working path,
/// this path will not being managed. So make error.</exception>
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();
}
/// <summary>
/// Split work path into path vector.
/// </summary>
/// <param name="workPath">The path will being split.</param>
/// <param name="result">The list to store result (Non-allocate)</param>
/// <returns>The count of added elements</returns>
/// <exception cref="ArgumentNullException">Argument is null</exception>
/// <exception cref="ArgumentException">Work path is invalid</exception>
public static int GetPathVector(string workPath, List<string> 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;
}
/// <summary>
/// Split work path into path vector.
/// </summary>
/// <param name="workPath">The path will being split.</param>
/// <returns>Enumerable of vector</returns>
/// <exception cref="ArgumentNullException">Argument is null</exception>
/// <exception cref="ArgumentException">Work path is invalid</exception>
public static IEnumerable<string> 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;
}
}
/// <summary>
/// Check work path is legal but not ensure that existed in platform.
/// </summary>
/// <param name="workPath">The path will being tested.</param>
/// <returns>True when path is valid. If you need more details, consider use
/// <see cref="WorkPath.GetPathVector(string)"/>.</returns>
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;
}
/// <summary>
/// Check if last vector has a valid extension.
/// </summary>
/// <param name="workPath">Targeting work path</param>
/// <returns>Is valid.</returns>
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;
}
/// <summary>
/// Get the last vector extension.
/// </summary>
/// <param name="workPath">Targeting work path</param>
/// <returns>The name of last vector extension. If the last vector is empty or no valid extension existed,
/// return empty.</returns>
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;
}
/// <summary>
/// Change the last vector extension.
/// </summary>
/// <param name="workPath">Targeting work path</param>
/// <param name="extension">Targeting extension, null means clean the extension</param>
/// <returns>Modified path.</returns>
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;
}
/// <summary>
/// Get the last vector name.
/// </summary>
/// <param name="workPath">Targeting work path</param>
/// <returns>The name of last vector name. If the last vector is empty, return empty.</returns>
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);
}
/// <summary>
/// Get the last vector name without extension.
/// </summary>
/// <param name="workPath">Targeting work path</param>
/// <returns>The name of last vector without extension. If the last vector is empty, return empty.</returns>
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);
}
/// <summary>
/// Check if path is a root path. Do not guarantee path is valid.
/// </summary>
/// <param name="workPath">Work path being tested.</param>
/// <returns>True when is root.</returns>
public static bool IsRootPath(string workPath)
{
return workPath.Contains(DirectorySeparatorChar);
}
/// <summary>
/// Check if path is ended with directory separator. Do not guarantee path is valid.
/// </summary>
/// <param name="workPath">Work path being tested.</param>
/// <returns>True when is ended with directory separator.</returns>
public static bool EndsInDirectorySeparator(string workPath)
{
return workPath.Length > 0 && workPath[^1] == DirectorySeparatorChar;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="relatedTo">The parent one.</param>
/// <param name="workPath">The child one.</param>
/// <returns>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.</returns>
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);
}
}
/// <summary>
/// Combine path one by one. Do not guarantee path is valid.
/// </summary>
/// <example>
/// It will connect those path in correct order and separator.
/// <code>
/// Combine("abc", "def") => "abc/def";
/// Combine("abc", "def/") => "abc/def/";
/// Combine("abc", "def/gg") => "abc/def/gg";
/// Combine("abc/", "/def/gg") => "abc/def/gg";
/// </code>
/// </example>
/// <returns>The connected path</returns>
public static string Combine(string path1, string path2)
{
var sb = new StringBuilder();
CombineInternal(sb, path1);
CombineInternal(sb, path2);
return sb.ToString();
}
/// <summary>
/// Combine path one by one. Do not guarantee path is valid.
/// </summary>
/// <example>
/// It will connect those path in correct order and separator.
/// <code>
/// Combine("abc", "def") => "abc/def";
/// Combine("abc", "def/") => "abc/def/";
/// Combine("abc", "def/gg") => "abc/def/gg";
/// Combine("abc/", "/def/gg") => "abc/def/gg";
/// </code>
/// </example>
/// <returns>The connected path</returns>
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();
}
/// <summary>
/// Combine path one by one. Do not guarantee path is valid.
/// </summary>
/// <example>
/// It will connect those path in correct order and separator.
/// <code>
/// Combine("abc", "def") => "abc/def";
/// Combine("abc", "def/") => "abc/def/";
/// Combine("abc", "def/gg") => "abc/def/gg";
/// Combine("abc/", "/def/gg") => "abc/def/gg";
/// </code>
/// </example>
/// <returns>The connected path</returns>
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();
}
/// <summary>
/// Combine path one by one. Do not guarantee path is valid.
/// </summary>
/// <example>
/// It will connect those path in correct order and separator.
/// <code>
/// Combine("abc", "def") => "abc/def";
/// Combine("abc", "def/") => "abc/def/";
/// Combine("abc", "def/gg") => "abc/def/gg";
/// Combine("abc/", "/def/gg") => "abc/def/gg";
/// </code>
/// </example>
/// <returns>The connected path</returns>
public static string Combine(params string[] paths)
{
var sb = new StringBuilder();
foreach (var path in paths) CombineInternal(sb, path);
return sb.ToString();
}
}

View File

@ -1,15 +0,0 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Flawless.Client.Avanonia.App"
xmlns:local="using:Flawless.Client.Avanonia"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

View File

@ -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();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

View File

@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\"/>
<AvaloniaResource Include="Assets\**"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.0"/>
<PackageReference Include="Avalonia.Desktop" Version="11.1.0"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.0"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.0"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.0"/>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1"/>
</ItemGroup>
</Project>

View File

@ -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<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -1,7 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Flawless.Client.Avanonia.ViewModels;
public class ViewModelBase : ObservableObject
{
}

View File

@ -1,20 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Flawless.Client.Avanonia.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Avanonia.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
Title="Flawless.Client.Avanonia">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainWindowViewModel/>
</Design.DataContext>
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Window>

View File

@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace Flawless.Client.Avanonia.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="Flawless.Client.Avanonia.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View File

@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="..\Flawless.Shared\Protos\*.proto" GrpcServices="Client"/>
<PackageReference Include="Google.Protobuf" Version="3.28.2" />
<PackageReference Include="Grpc.Net.Client" Version="2.66.0" />
<PackageReference Include="Grpc.Tools" Version="2.67.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<Protobuf Remove="..\Flawless.Shared\Protos\**" />
</ItemGroup>
</Project>

View File

@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<Protobuf Include="..\Flawless.Shared\Protos\*.proto" GrpcServices="Server"/>
<PackageReference Include="Grpc.AspNetCore" Version="2.57.0"/>
<Protobuf Update="..\Flawless.Shared\Protos\auth.proto">
<Link>Protos\auth.proto</Link>
</Protobuf>
<Protobuf Update="..\Flawless.Shared\Protos\auth_token.proto">
<Link>Protos\auth_token.proto</Link>
</Protobuf>
</ItemGroup>
</Project>

View File

@ -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<AuthService>();
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();
}

View File

@ -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"
}
}
}
}

View File

@ -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<AuthService> _logger;
public AuthService(ILogger<AuthService> logger)
{
_logger = logger;
}
public override Task<AuthResult> 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<AuthUserInfo> 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<Empty> Validate(Empty request, ServerCallContext context)
{
return Task.FromResult(new Empty());
}
}

View File

@ -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<Claim>
{
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);
}
}

View File

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -1,14 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
}
}

View File

@ -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;
}

View File

@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.28.2" />
<PackageReference Include="Grpc.Core.Api" Version="2.66.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.66.0" />
<PackageReference Include="Grpc.Tools" Version="2.67.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<Protobuf Include="..\Flawless.Shared\Protos\*.proto" GrpcServices="Client"/>
</ItemGroup>
</Project>

View File

@ -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}");
}