1
0
mirror of https://github.com/XFox111/SimpleOTP.git synced 2026-04-22 08:00:45 +03:00

Initial commit

This commit is contained in:
2021-05-28 18:36:54 +03:00
parent 890c34dd5b
commit 3a629ed8c5
29 changed files with 2047 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
// ------------------------------------------------------------
// Copyright ©2021 Eugene Fox. All rights reserved.
// Code by Eugene Fox (aka XFox)
//
// Licensed under MIT license (https://opensource.org/licenses/MIT)
// ------------------------------------------------------------
/*
* This file is used by Code Analysis to maintain SuppressMessage
* attributes that are applied to this project.
* Project-level suppressions either have no target or are given
* a specific target and scoped to a namespace, type, member, etc.
*/
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1625:Element documentation should not be copied and pasted", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1519:Braces should not be omitted from multi-line child statement", Justification = "Reviewd by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Reviewd by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
+37
View File
@@ -0,0 +1,37 @@
// ------------------------------------------------------------
// Copyright ©2021 Eugene Fox. All rights reserved.
// Code by Eugene Fox (aka XFox)
//
// Licensed under MIT license (https://opensource.org/licenses/MIT)
// ------------------------------------------------------------
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SimpleOTP.Helpers;
namespace SimpleOTP.Test.Helpers
{
/// <summary>
/// Unit-tests for Base32 encoder.
/// </summary>
[TestClass]
public class Base32UnitTest
{
/// <summary>
/// Test overall work of the encoder.
/// </summary>
[TestMethod("Overall Base32 encoder test")]
public void EncoderTest()
{
// byte[] bytes = new byte[new Random().Next(128, 161)]; // FIXME: See SimpleOTP.Helpers.Base32Encoder.Encode()
byte[] bytes = new byte[160];
new Random().NextBytes(bytes);
string str = Base32Encoder.Encode(bytes);
bytes = Base32Encoder.Decode(str);
string result = Base32Encoder.Encode(bytes);
Assert.AreEqual(str, result);
}
}
}
@@ -0,0 +1,37 @@
// ------------------------------------------------------------
// Copyright ©2021 Eugene Fox. All rights reserved.
// Code by Eugene Fox (aka XFox)
//
// Licensed under MIT license (https://opensource.org/licenses/MIT)
// ------------------------------------------------------------
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SimpleOTP.Helpers;
namespace SimpleOTP.Test.Helpers
{
/// <summary>
/// Unit-tests for key generator.
/// </summary>
[TestClass]
public class SecretGeneratorUnitTest
{
/// <summary>
/// Overall test of key generator.
/// </summary>
[TestMethod("Overall generator tests")]
public void Test_Generator()
{
Assert.ThrowsException<ArgumentOutOfRangeException>(() => SecretGenerator.GenerateSecret(64));
Assert.ThrowsException<ArgumentOutOfRangeException>(() => SecretGenerator.GenerateSecret(256));
string key = SecretGenerator.GenerateSecret();
Assert.IsFalse(string.IsNullOrWhiteSpace(key));
key = SecretGenerator.GenerateSecret(128);
Assert.IsFalse(string.IsNullOrWhiteSpace(key));
}
}
}
+160
View File
@@ -0,0 +1,160 @@
// ------------------------------------------------------------
// Copyright ©2021 Eugene Fox. All rights reserved.
// Code by Eugene Fox (aka XFox)
//
// Licensed under MIT license (https://opensource.org/licenses/MIT)
// ------------------------------------------------------------
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SimpleOTP.Models;
namespace SimpleOTP.Test.Helpers
{
/// <summary>
/// Unit-tests for OTP URI parser.
/// </summary>
[TestClass]
public class UriParserUnitTest
{
private static readonly Guid TestGuid = Guid.NewGuid();
private readonly string[] uriParts =
{
"otpauth",
"totp|hotp",
"FoxDev%20Studio",
"eugene@xfox111.net",
"secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
"issuer=FoxDev%20Studio%20Issuer",
"algorithm=SHA1|algorithm=SHA512",
"digits=8",
"period=10",
"counter=10000",
};
private readonly OTPConfiguration[] configs =
{
new ()
{
AccountName = "eugene@xfox111.net",
Algorithm = Enums.Algorithm.SHA512,
Digits = 8,
Type = Enums.OTPType.TOTP,
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
Issuer = "FoxDev Studio Issuer",
IssuerLabel = "FoxDev Studio",
Id = TestGuid,
Period = TimeSpan.FromSeconds(10),
},
new ()
{
AccountName = "eugene@xfox111.net",
Algorithm = Enums.Algorithm.SHA512,
Digits = 8,
Type = Enums.OTPType.HOTP,
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
Issuer = "FoxDev Studio Issuer",
IssuerLabel = "FoxDev Studio",
Id = TestGuid,
Counter = 10000,
},
new ()
{
AccountName = "eugene@xfox111.net",
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
Issuer = "FoxDev Studio",
IssuerLabel = "FoxDev Studio",
Id = TestGuid,
},
new ()
{
AccountName = "eugene@xfox111.net",
Type = Enums.OTPType.HOTP,
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
Issuer = "FoxDev Studio",
IssuerLabel = "FoxDev Studio",
Id = TestGuid,
Counter = 10000,
},
};
/// <summary>
/// Test parser with full TOTP URI.
/// </summary>
[TestMethod("Valid full URI (TOTP)")]
public void ParseValidUri_FullFormed_Totp()
{
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[5]}&{uriParts[6].Split('|')[1]}&{uriParts[7]}&{uriParts[8]}";
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
Assert.IsNotNull(config);
config.Id = TestGuid;
Assert.AreEqual(configs[0], config);
}
/// <summary>
/// Test parser with full HOTP URI.
/// </summary>
[TestMethod("Valid full URI (HOTP)")]
public void ParseValidUri_FullFormed_Hotp()
{
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[5]}&{uriParts[6].Split('|')[1]}&{uriParts[7]}&{uriParts[9]}";
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
Assert.IsNotNull(config);
config.Id = TestGuid;
Assert.AreEqual(configs[1], config);
}
/// <summary>
/// Test parser with TOTP URI. Minimal parameter set.
/// </summary>
[TestMethod("Valid minimal URI (TOTP)")]
public void ParseValidUri_Minimal_Totp()
{
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}";
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
Assert.IsNotNull(config);
config.Id = TestGuid;
Assert.AreEqual(configs[2], config);
config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri($"otpauth://totp/eugene@xfox111.net?{uriParts[4]}&{uriParts[5]}&algorithm=SHA256"));
Assert.IsNotNull(config);
config.Id = TestGuid;
Assert.AreEqual(configs[2] with { IssuerLabel = "FoxDev Studio Issuer", Issuer = "FoxDev Studio Issuer", Algorithm = Enums.Algorithm.SHA256 }, config);
}
/// <summary>
/// Test parser with HOTP URI. Minimal parameter set.
/// </summary>
[TestMethod("Valid minimal URI (HOTP)")]
public void ParseValidUri_Minimal_Hotp()
{
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}";
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
Assert.IsNotNull(config);
config.Id = TestGuid;
Assert.AreEqual(configs[3], config);
}
/// <summary>
/// Test parser with invalid OTP URIs.
/// </summary>
[TestMethod("Invalid URI")]
public void ParseInvalidUris()
{
string[] uris =
{
$"https://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
$"{uriParts[0]}://otp/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
$"{uriParts[0]}://{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
$"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
$"{uriParts[0]}://{uriParts[1].Split('|')[0]}/?{uriParts[4]}&{uriParts[9]}",
$"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[2]}:{uriParts[3]}",
$"{uriParts[0]}://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}",
};
foreach (string uri in uris)
Assert.ThrowsException<ArgumentException>(() => SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri)));
}
}
}
+37
View File
@@ -0,0 +1,37 @@
// ------------------------------------------------------------
// Copyright ©2021 Eugene Fox. All rights reserved.
// Code by Eugene Fox (aka XFox)
//
// Licensed under MIT license (https://opensource.org/licenses/MIT)
// ------------------------------------------------------------
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SimpleOTP.Models;
namespace SimpleOTP.Test.Models
{
/// <summary>
/// Unit-tests for OTP code model.
/// </summary>
[TestClass]
public class OTPCodeUnitTest
{
/// <summary>
/// Get formatted OTP code.
/// </summary>
[TestMethod("Code formatting")]
public void Test_GetFullCode()
{
OTPCode code = new ()
{
Code = 123,
Expiring = DateTime.UtcNow.AddSeconds(30),
};
Assert.AreEqual("000123", code.GetCode());
Assert.AreEqual("000 123", code.GetCode("000 000"));
}
}
}
@@ -0,0 +1,90 @@
// ------------------------------------------------------------
// Copyright ©2021 Eugene Fox. All rights reserved.
// Code by Eugene Fox (aka XFox)
//
// Licensed under MIT license (https://opensource.org/licenses/MIT)
// ------------------------------------------------------------
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SimpleOTP.Models;
namespace SimpleOTP.Test.Models
{
/// <summary>
/// Unit-tests for OTP generator configuration model.
/// </summary>
[TestClass]
public class OTPConfigurationUnitTest
{
/// <summary>
/// Get otpauth link from minimal configuration.
/// </summary>
[TestMethod("Link generator (short)")]
public void TestShortLinkGenerator()
{
OTPConfiguration config = OTPConfiguration.GenerateConfiguration("FoxDev Studio", "eugene@xfox111.net");
System.Diagnostics.Debug.WriteLine(config.Id);
Uri uri = config.GetUri();
Assert.AreEqual($"otpauth://totp/FoxDev+Studio:eugene@xfox111.net?secret={config.Secret}&issuer=FoxDev+Studio", uri.AbsoluteUri);
}
/// <summary>
/// Get otpauth link from complete configurations (TOTP + HOTP).
/// </summary>
[TestMethod("Link generator (full)")]
public void TestFullLinkGenerator()
{
OTPConfiguration config = OTPConfiguration.GetConfiguration("otpauth://totp/FoxDev%20Studio:eugene@xfox111.net?secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH&issuer=FoxDev%20Studio%20Issuer&algorithm=SHA512&digits=8&period=10");
Assert.AreEqual($"otpauth://totp/FoxDev+Studio:eugene@xfox111.net?secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH&issuer=FoxDev+Studio+Issuer&algorithm=SHA512&digits=8&period=10", config.GetUri().AbsoluteUri);
config.Type = Enums.OTPType.HOTP;
Assert.AreEqual($"otpauth://hotp/FoxDev+Studio:eugene@xfox111.net?secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH&issuer=FoxDev+Studio+Issuer&algorithm=SHA512&digits=8&counter=0", config.GetUri().AbsoluteUri);
}
/// <summary>
/// Test user-friendly secret formatting.
/// </summary>
[TestMethod("Fancy secret")]
public void GetFormattedSecret()
{
OTPConfiguration config = OTPConfiguration.GetConfiguration("ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH", "FoxDev Studio", "eugene@xfox111.net");
Assert.AreEqual($"ESQV TYRM 2CWZ C3NX 24GR RWIA UUWV HWQH", config.GetFancySecret());
}
/// <summary>
/// Test QR code generation for OTP config.
/// </summary>
/// <returns><see cref="Task"/>.</returns>
[TestMethod("QR code image")]
public async Task GetQrImage()
{
OTPConfiguration config = OTPConfiguration.GetConfiguration("ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH", "FoxDev Studio", "eugene@xfox111.net");
string imageStr = await config.GetQrImage();
Assert.AreEqual(
"data:image/png;base64,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", imageStr);
}
}
}
+42
View File
@@ -0,0 +1,42 @@
// ------------------------------------------------------------
// Copyright ©2021 Eugene Fox. All rights reserved.
// Code by Eugene Fox (aka XFox)
//
// Licensed under MIT license (https://opensource.org/licenses/MIT)
// ------------------------------------------------------------
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SimpleOTP.Models;
namespace SimpleOTP.Test
{
/// <summary>
/// OTP factory class unit-tests.
/// </summary>
[TestClass]
public class OTPFactoryUnitTest
{
/// <summary>
/// Complex test of OTP factory.
/// </summary>
/// <returns><see cref="Task"/>.</returns>
[TestMethod("Overall factory test")]
public async Task TestFactory()
{
OTPConfiguration config = OTPConfiguration.GetConfiguration("ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH", "FoxDev Studio", "eugene@xfox111.net");
config.Period = TimeSpan.FromSeconds(3);
using OTPFactory factory = new (config, 1500);
System.Diagnostics.Debug.WriteLine(factory.Configuration);
var code = factory.CurrentCode;
factory.Configuration = config;
factory.CurrentCode = code;
await Task.Delay(3500);
Assert.AreNotEqual(code.Code, factory.CurrentCode.Code);
}
}
}
+110
View File
@@ -0,0 +1,110 @@
// ------------------------------------------------------------
// Copyright ©2021 Eugene Fox. All rights reserved.
// Code by Eugene Fox (aka XFox)
//
// Licensed under MIT license (https://opensource.org/licenses/MIT)
// ------------------------------------------------------------
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SimpleOTP.Models;
namespace SimpleOTP.Test
{
/// <summary>
/// Unit-tests for OTP generator.
/// </summary>
[TestClass]
public class OTPServiceUnitTest
{
private readonly DateTime time = new (2021, 5, 28, 10, 47, 50, DateTimeKind.Utc);
private OTPConfiguration totpConfig = OTPConfiguration.GetConfiguration(new Uri("otpauth://totp/FoxDev%20Studio:eugene@xfox111.net?secret=JBSWY3DPEHPK3PXP&issuer=FoxDev%20Studio"));
private OTPConfiguration hotpConfig = OTPConfiguration.GetConfiguration(new Uri("otpauth://hotp/FoxDev%20Studio:eugene@xfox111.net?secret=JBSWY3DPEHPK3PXP&issuer=FoxDev%20Studio&counter=10000"));
/// <summary>
/// Test time-based OTP generator with pre-calculated code.
/// </summary>
[TestMethod("TOTP code generation")]
public void GenerateCode_Totp()
{
var config = totpConfig with { };
var code = OTPService.GenerateCode(ref config, time);
Assert.AreEqual(160102, code.Code);
config.Algorithm = Enums.Algorithm.SHA256;
code = OTPService.GenerateCode(ref config, time);
Assert.AreEqual(195671, code.Code);
config.Algorithm = Enums.Algorithm.SHA512;
code = OTPService.GenerateCode(ref config, time);
Assert.AreEqual(293657, code.Code);
}
/// <summary>
/// Test HOTP generator with pre-calculated code.
/// </summary>
[TestMethod("HOTP code generation")]
public void GenerateCode_Hotp()
{
var code = OTPService.GenerateCode(ref hotpConfig);
Assert.AreEqual(457608, code.Code);
Assert.AreEqual(10001, hotpConfig.Counter);
}
/// <summary>
/// Test time-based OTP validator with series of codes.
/// </summary>
[TestMethod("TOTP code validation")]
public void ValidateCode_Totp()
{
int[] codes =
{
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(-45)).Code,
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(-15)).Code,
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(0)).Code,
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(15)).Code,
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(45)).Code,
};
Assert.IsFalse(OTPService.ValidateCode(codes[0], totpConfig));
Assert.IsTrue(OTPService.ValidateCode(codes[1], totpConfig));
Assert.IsTrue(OTPService.ValidateCode(codes[2], totpConfig));
Assert.IsTrue(OTPService.ValidateCode(codes[3], totpConfig));
Assert.IsFalse(OTPService.ValidateCode(codes[4], totpConfig));
Assert.IsTrue(OTPService.ValidateCode(codes[0], totpConfig, TimeSpan.FromSeconds(60)));
}
/// <summary>
/// Test HOTP validator with series of codes.
/// </summary>
[TestMethod("HOTP code validation")]
public void ValidateCode_Hotp()
{
hotpConfig.Counter = 10000;
int[] codes =
{
OTPService.GenerateCode(ref hotpConfig).Code,
OTPService.GenerateCode(ref hotpConfig).Code,
OTPService.GenerateCode(ref hotpConfig).Code,
OTPService.GenerateCode(ref hotpConfig).Code,
OTPService.GenerateCode(ref hotpConfig).Code,
};
hotpConfig.Counter = 10002;
Assert.IsFalse(OTPService.ValidateCode(codes[0], ref hotpConfig, 1, true));
Assert.AreEqual(10002, hotpConfig.Counter);
Assert.IsTrue(OTPService.ValidateCode(codes[1], ref hotpConfig, 1, true));
Assert.AreEqual(10001, hotpConfig.Counter);
hotpConfig.Counter = 10002;
Assert.IsTrue(OTPService.ValidateCode(codes[2], ref hotpConfig, 1, true));
Assert.AreEqual(10002, hotpConfig.Counter);
hotpConfig.Counter = 10002;
Assert.IsTrue(OTPService.ValidateCode(codes[3], ref hotpConfig, 1, true));
Assert.AreEqual(10003, hotpConfig.Counter);
hotpConfig.Counter = 10002;
Assert.IsFalse(OTPService.ValidateCode(codes[4], ref hotpConfig, 1, true));
Assert.AreEqual(10002, hotpConfig.Counter);
}
}
}
+43
View File
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AssemblyName>SimpleOTP.Tests</AssemblyName>
<PackageId>SimpleOTP.Tests</PackageId>
</PropertyGroup>
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
</PropertyGroup>
<ItemGroup>
<None Remove="stylecop.json" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0-release-20210429-01" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.4-preview-20210513-02" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.4-preview-20210513-02" />
<PackageReference Include="coverlet.collector" Version="3.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.66">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SimpleOTP\SimpleOTP.csproj" />
</ItemGroup>
</Project>
+48
View File
@@ -0,0 +1,48 @@
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings":
{
"documentationRules":
{
"companyName": "Eugene Fox",
"copyrightText": "------------------------------------------------------------\nCopyright ©{year} {companyName}. All rights reserved.\nCode by {author}\n\nLicensed under MIT license (https://opensource.org/licenses/MIT)\n------------------------------------------------------------",
"xmlHeader": false,
"variables":
{
"year": "2021",
"author": "Eugene Fox (aka XFox)"
},
"headerDecoration": "------------------------------------------------------------",
"fileNamingConvention": "stylecop",
"documentationCulture": "en-US"
},
"indentation":
{
"useTabs": true
},
"layoutRules":
{
"newlineAtEndOfFile": "omit"
},
"maintainabilityRules":
{
"topLevelTypes": [ "class", "interface", "struct" ]
},
"namingRules":
{
"allowCommonHungarianPrefixes": false,
"allowedNamespaceComponents": [],
"tupleElementNameCasing": "PascalCase"
},
"orderingRules":
{
"blankLinesBetweenUsingGroups": "require",
"systemUsingDirectivesFirst": true,
"usingDirectivesPlacement": "outsideNamespace"
},
"readabilityRules":
{
"allowBuiltInTypeAliases": false
}
}
}