initial commit

This commit is contained in:
2025-10-18 10:26:43 +02:00
commit 43fba99802
87 changed files with 3696 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
namespace CertMgr.Core.Converters;
public class ConverterContext
{
public static readonly ConverterContext Empty = new ConverterContext();
}

View File

@@ -0,0 +1,15 @@
using CertMgr.Core.Utils;
namespace CertMgr.Core.Converters;
public sealed class ConverterException : Exception
{
internal ConverterException(string messageFormat, params object?[] messageArgs)
: base(StringFormatter.Format(messageFormat, messageArgs))
{
}
internal ConverterException(Exception? innerException, string messageFormat, params object?[] messageArgs)
: base(StringFormatter.Format(messageFormat, messageArgs), innerException)
{
}
}

View File

@@ -0,0 +1,19 @@
using CertMgr.Core.Converters.Impl;
namespace CertMgr.Core.Converters;
public sealed class ConverterFactory
{
internal ConverterFactory()
{
}
public ConverterStash CreateDefault()
{
ConverterStash stash = new ConverterStash();
stash.Set(typeof(int), typeof(IntConverter));
stash.Set(typeof(string), typeof(StringConverter));
stash.Set(typeof(Enum), typeof(EnumConverter));
return stash;
}
}

View File

@@ -0,0 +1,102 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace CertMgr.Core.Converters;
public sealed class ConverterStash
{
private readonly Dictionary<Type, ConverterDescriptor> _items;
internal ConverterStash()
{
_items = new Dictionary<Type, ConverterDescriptor>();
}
public bool Set(Type conversionType, Type converterType, bool replace = false)
{
bool added = false;
if (_items.ContainsKey(conversionType))
{
if (replace)
{
_items[conversionType] = new ConverterDescriptor(converterType);
added = true;
}
}
else
{
_items.Add(conversionType, new ConverterDescriptor(converterType));
added = true;
}
return added;
}
public bool TryGet(Type type, [NotNullWhen(true)] out IValueConverter? converter)
{
converter = null;
if (_items.TryGetValue(type, out ConverterDescriptor? descriptor))
{
converter = descriptor.Instance;
}
else if (type.IsEnum)
{
if (_items.TryGetValue(typeof(Enum), out descriptor))
{
converter = descriptor.Instance;
}
}
return converter != null;
}
public override string ToString()
{
return string.Format("count = {0}", _items.Count);
}
private sealed class ConverterDescriptor
{
private IValueConverter? _converter;
public ConverterDescriptor(Type converterType)
{
ConverterType = converterType;
}
public Type ConverterType { [DebuggerStepThrough] get; }
public IValueConverter Instance
{
get
{
if (_converter == null)
{
lock (this)
{
if (_converter == null)
{
try
{
_converter = (IValueConverter?)Activator.CreateInstance(ConverterType);
}
catch (Exception e)
{
throw new ConverterException(e, "Failed to create instance of converter of type '{0}'", ConverterType.Name);
}
}
}
}
if (_converter == null)
{
throw new ConverterException("Failed to create instance of converter of type '{0}'", ConverterType.Name);
}
return _converter;
}
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Diagnostics;
namespace CertMgr.Core.Converters;
public class EnumConverterContext : ConverterContext
{
internal EnumConverterContext(Type targetType)
{
TargetType = targetType;
}
public Type TargetType { [DebuggerStepThrough] get; }
}

View File

@@ -0,0 +1,6 @@
namespace CertMgr.Core.Converters;
public interface IValueConverter
{
Task<object?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,6 @@
namespace CertMgr.Core.Converters;
public interface IValueConverter<T> : IValueConverter
{
new Task<T?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,43 @@
using CertMgr.Core.Utils;
namespace CertMgr.Core.Converters.Impl;
public sealed class EnumConverter : ValueConverter<Enum>
{
protected override Task<Enum?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
Type resultType;
bool isNullable = targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>);
if (isNullable)
{
Type[] args = targetType.GetGenericArguments();
if (args.Length != 1)
{
throw new ConverterException("Cannot convert nullable type '{0}' to enum", targetType.ToString(false));
}
if (args[0].IsEnum)
{
resultType = args[0];
}
else
{
throw new ConverterException("Cannot convert nullable type '{0}' as enum as it is not enum", targetType.ToString(false));
}
}
else
{
if (!targetType.IsEnum)
{
resultType = targetType;
}
else
{
throw new ConverterException("Cannot convert type '{0}' as enum as it is not enum", targetType.ToString(false));
}
}
object? result = Enum.Parse(resultType, rawValue, true);
return Task.FromResult((Enum?)result);
}
}

View File

@@ -0,0 +1,15 @@
namespace CertMgr.Core.Converters.Impl;
public sealed class EnumNullableConverter : ValueConverter<Enum?>
{
protected override Task<Enum?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
if (!targetType.IsEnum)
{
throw new ConverterException("Cannot convert type '{0}' as enum as it is not enum", targetType.Name);
}
object? result = Enum.Parse(targetType, rawValue, true);
return Task.FromResult((Enum?)result);
}
}

View File

@@ -0,0 +1,10 @@
namespace CertMgr.Core.Converters.Impl;
public sealed class IntConverter : ValueConverter<int>
{
protected override Task<int> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
int result = int.Parse(rawValue);
return Task.FromResult(result);
}
}

View File

@@ -0,0 +1,121 @@
using System.Diagnostics.CodeAnalysis;
using CertMgr.Core.Log;
using CertMgr.Core.Storage;
namespace CertMgr.Core.Converters.Impl;
internal sealed class StorageConverter : ValueConverter<IStorage>
{
private const char Separator = '|';
protected override Task<IStorage?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
ReadOnlySpan<char> rawSpan = rawValue.AsSpan();
int storageTypeSplitIndex = rawSpan.IndexOf(Separator);
if (storageTypeSplitIndex == -1)
{
return Task.FromResult((IStorage?)EmptyStorage.Empty);
}
IStorage? storage;
ReadOnlySpan<char> storageTypeSpan = rawSpan.Slice(0, storageTypeSplitIndex);
ReadOnlySpan<char> storageDefinition = rawSpan.Slice(storageTypeSplitIndex + 1);
switch (storageTypeSpan)
{
case "file":
if (!TryGetFileStore(storageDefinition, out storage))
{
storage = EmptyStorage.Empty;
}
break;
default:
storage = EmptyStorage.Empty;
break;
}
return Task.FromResult((IStorage?)storage);
}
private bool TryGetFileStore(ReadOnlySpan<char> storageDefinition, [NotNullWhen(true)] out IStorage? storage)
{
// expecting that 'storageDefinition' is something like:
// <store-mode>|<file-path>
// or
// |<file-path>
// or
// <file-path>
//
// where <store-mode> can be:
// 'o' or 'overwrite' or 'overwriteornew'
// or
// 'a' or 'append' or 'appendornew'
// or
// 'c' or 'create' or 'createnew'
//
// where <file-path> can be:
// 'c:\path\myfile.txt'
// or
// '/path/myfile.txt'
// or
// './path/myfile.txt'
// in addition - <file-path> can be either quoted or double-quoted
storage = null;
FileStoreMode defaultStoreMode = FileStoreMode.OverwriteOrNew;
FileStoreMode storeMode;
ReadOnlySpan<char> filename;
int firstSplitIndex = storageDefinition.IndexOf(Separator);
if (firstSplitIndex != -1)
{
// there is a splitter. On the left side of it there must be <store-mode>. On the right there is a <file-path>
ReadOnlySpan<char> firstPart = storageDefinition.Slice(0, firstSplitIndex);
filename = storageDefinition.Slice(firstSplitIndex + 1);
if (firstPart.Length == 0)
{
storeMode = defaultStoreMode;
}
else if (Enum.TryParse(firstPart, true, out FileStoreMode tmpMode))
{
storeMode = tmpMode;
}
else if (firstPart.Equals("w", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("overwrite", StringComparison.OrdinalIgnoreCase))
{
storeMode = FileStoreMode.OverwriteOrNew;
}
else if (firstPart.Equals("c", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("createnew", StringComparison.OrdinalIgnoreCase))
{
storeMode = FileStoreMode.Create;
}
else if (firstPart.Equals("a", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("append", StringComparison.OrdinalIgnoreCase))
{
storeMode = FileStoreMode.AppendOrNew;
}
else if (firstPart.Equals("o", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("open", StringComparison.OrdinalIgnoreCase))
{
storeMode = FileStoreMode.Open;
}
else
{
// it is not store-mode or there is a typo or unsupported value
CLog.Error(string.Concat("Unsupported store-mode '", firstPart, "'"));
storeMode = defaultStoreMode;
}
}
else
{
// no split-char => just filename
storeMode = defaultStoreMode;
filename = storageDefinition;
}
storage = new FileStorage(filename.ToString(), storeMode);
return true;
}
}

View File

@@ -0,0 +1,10 @@
namespace CertMgr.Core.Converters.Impl;
public sealed class StringConverter : ValueConverter<string>
{
protected override Task<string?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
return Task.FromResult(rawValue);
}
}

View File

@@ -0,0 +1,49 @@
namespace CertMgr.Core.Converters.Impl;
public sealed class TimeSpanConverter : ValueConverter<TimeSpan?>
{
protected override Task<TimeSpan?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(rawValue) || rawValue.Length == 1)
{
return Task.FromResult((TimeSpan?)null);
}
ReadOnlySpan<char> rawSpan = rawValue.AsSpan();
ReadOnlySpan<char> valueSpan = rawSpan.Slice(0, rawSpan.Length - 1);
char unit = char.ToLower(rawValue[rawValue.Length - 1]);
TimeSpan? result;
if (int.TryParse(valueSpan, out int value))
{
switch (unit)
{
case 's':
result = TimeSpan.FromSeconds(value);
break;
case 'm':
result = TimeSpan.FromMinutes(value);
break;
case 'h':
result = TimeSpan.FromHours(value);
break;
case 'd':
result = TimeSpan.FromDays(value);
break;
case 'y':
result = TimeSpan.FromDays(value * 365);
break;
default:
result = null;
break;
}
}
else
{
result = null;
}
return Task.FromResult(result);
}
}

View File

@@ -0,0 +1,11 @@
namespace CertMgr.Core.Converters;
public abstract class ValueConverter : IValueConverter
{
public Task<object?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
return DoConvertAsync(rawValue, targetType, cancellationToken);
}
protected abstract Task<object?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,30 @@
namespace CertMgr.Core.Converters;
public abstract class ValueConverter<T> : IValueConverter<T>
{
public Task<T?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
if (typeof(T) != targetType)
{
throw new ConverterException("This converter ('{0}') converts string value to type '{1}' but type '{2}' is expected this time", GetType().Name, typeof(T), targetType.Name);
}
try
{
return DoConvertAsync(rawValue, targetType, cancellationToken);
}
catch (Exception e)
{
throw new ConverterException(e, "Failed to convert value '{0}' to type '{1}'", rawValue ?? "<null>", typeof(T).Name);
}
}
async Task<object?> IValueConverter.ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
T? typedValue = await DoConvertAsync(rawValue, targetType, cancellationToken);
return typedValue;
}
protected abstract Task<T?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
}