初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
public class ArgumentsBuilder
|
||||
{
|
||||
private readonly List<Argument> _args = [];
|
||||
|
||||
private enum ArgumentStyle
|
||||
{
|
||||
Flag,
|
||||
|
||||
/// <summary>
|
||||
/// Concated by '=' symbol.
|
||||
/// </summary>
|
||||
Equals,
|
||||
|
||||
/// <summary>
|
||||
/// Concated by space.
|
||||
/// </summary>
|
||||
Space
|
||||
}
|
||||
|
||||
private readonly struct Argument(string key, string? value, ArgumentStyle style)
|
||||
{
|
||||
public readonly string Key = key ?? throw new ArgumentNullException(nameof(key));
|
||||
public readonly string? Value = value;
|
||||
public readonly ArgumentStyle Style = style;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加键值对参数(自动处理空格转义)
|
||||
/// </summary>
|
||||
/// <param name="key">参数名(不带前缀)</param>
|
||||
/// <param name="value">参数值</param>
|
||||
public ArgumentsBuilder Add(string key, string value)
|
||||
{
|
||||
if (key is null) throw new NullReferenceException(nameof(key));
|
||||
if (value is null) throw new NullReferenceException(nameof(value));
|
||||
_args.Add(new Argument(key, _HandleValue(value), ArgumentStyle.Equals));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加由空格连接到键值对参数(自动处理空格转义)
|
||||
/// </summary>
|
||||
/// <param name="key">参数名(不带前缀)</param>
|
||||
/// <param name="value">参数值</param>
|
||||
public ArgumentsBuilder AddWithSpace(string key, string value)
|
||||
{
|
||||
if (key is null) throw new NullReferenceException(nameof(key));
|
||||
if (value is null) throw new NullReferenceException(nameof(value));
|
||||
_args.Add(new Argument(key, _HandleValue(value), ArgumentStyle.Space));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加标志参数(无值参数)
|
||||
/// </summary>
|
||||
/// <param name="flag">标志名(不带前缀)</param>
|
||||
public ArgumentsBuilder AddFlag(string flag)
|
||||
{
|
||||
if (flag is null) throw new NullReferenceException(nameof(flag));
|
||||
_args.Add(new Argument(flag, null, ArgumentStyle.Flag));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 条件添加参数(仅当condition为true时添加)
|
||||
/// </summary>
|
||||
public ArgumentsBuilder AddIf(bool condition, string key, string value)
|
||||
{
|
||||
if (condition) Add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 条件添加由空格连接的参数(仅当condition为true时添加)
|
||||
/// </summary>
|
||||
public ArgumentsBuilder AddWithSpaceIf(bool condition, string key, string value)
|
||||
{
|
||||
if (condition) AddWithSpace(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 条件添加标志(仅当condition为true时添加)
|
||||
/// </summary>
|
||||
public ArgumentsBuilder AddFlagIf(bool condition, string flag)
|
||||
{
|
||||
if (condition) AddFlag(flag);
|
||||
return this;
|
||||
}
|
||||
|
||||
public enum PrefixStyle
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动(单字符用-,多字符用--)
|
||||
/// </summary>
|
||||
Auto,
|
||||
/// <summary>
|
||||
/// 强制单横线
|
||||
/// </summary>
|
||||
SingleLine,
|
||||
/// <summary>
|
||||
/// 强制双横线
|
||||
/// </summary>
|
||||
DoubleLine
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建参数字符串
|
||||
/// </summary>
|
||||
/// <param name="prefixStyle">前缀样式</param>
|
||||
public string GetResult(PrefixStyle prefixStyle = 0)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var arg in _args)
|
||||
{
|
||||
if (sb.Length > 0) sb.Append(' ');
|
||||
|
||||
// 添加前缀
|
||||
switch (prefixStyle)
|
||||
{
|
||||
case PrefixStyle.SingleLine: // 强制单横线
|
||||
sb.Append('-').Append(arg.Key);
|
||||
break;
|
||||
case PrefixStyle.DoubleLine: // 强制双横线
|
||||
sb.Append("--").Append(arg.Key);
|
||||
break;
|
||||
default: // 自动判断
|
||||
sb.Append(arg.Key.Length == 1 ? "-" : "--").Append(arg.Key);
|
||||
break;
|
||||
}
|
||||
|
||||
// 添加值(如果有)
|
||||
if (arg.Value is not null)
|
||||
{
|
||||
switch (arg.Style)
|
||||
{
|
||||
case ArgumentStyle.Equals:
|
||||
sb.Append('=')
|
||||
.Append(arg.Value);
|
||||
break;
|
||||
case ArgumentStyle.Space:
|
||||
sb.Append(' ')
|
||||
.Append(arg.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有参数
|
||||
/// </summary>
|
||||
public void Clear() => _args.Clear();
|
||||
|
||||
private static readonly char[] _CharNeedToQute = [' ', '=', '|', '"'];
|
||||
|
||||
// 转义包含空格的值(用双引号包裹)
|
||||
private static string _HandleValue(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return $"\"{value}\"";
|
||||
return value.All(x => !_CharNeedToQute.Contains(x))
|
||||
? value
|
||||
: $"\"{value.Replace("\"", "\\\"")}\""; // 处理双引号转义
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
public sealed class AtomicVariable<T>
|
||||
{
|
||||
private T? _value;
|
||||
|
||||
public T? Value {
|
||||
get => _value;
|
||||
set => SetValue(value);
|
||||
}
|
||||
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
public bool Nullable { get; set; }
|
||||
|
||||
public void SetValue(in T? value)
|
||||
{
|
||||
if (ReadOnly) throw new NotSupportedException("Read-only variable");
|
||||
if (!Nullable && value is null) throw new NotSupportedException("Non-null variable");
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public AtomicVariable(T? value = default, bool readOnly = false, bool? nullable = null)
|
||||
{
|
||||
Nullable = nullable ?? value is null;
|
||||
SetValue(value);
|
||||
ReadOnly = readOnly;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace PCL.Core.Utils.Codecs;
|
||||
|
||||
public static class EncodingDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// 检测流中的文本编码方式(支持 Seek 的流)
|
||||
/// </summary>
|
||||
/// <param name="stream">输入流,必须支持 Seek</param>
|
||||
/// <param name="readFromBegin">是否将流重置到起始点</param>
|
||||
/// <returns>检测到的编码,未识别时返回 UTF-8 或系统默认</returns>
|
||||
public static Encoding DetectEncoding(Stream stream, bool readFromBegin = false)
|
||||
{
|
||||
if (!stream.CanRead)
|
||||
throw new ArgumentException("流必须支持读操作");
|
||||
if (!stream.CanSeek)
|
||||
throw new ArgumentException("流必须支持 Seek 操作");
|
||||
|
||||
var originalPosition = stream.Position;
|
||||
if (readFromBegin) stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
try
|
||||
{
|
||||
return _DetectByBom(stream, originalPosition) ?? _DetectWithoutBOM(stream, originalPosition) ?? Encoding.Default;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stream.Position = originalPosition;
|
||||
}
|
||||
}
|
||||
|
||||
public static Encoding DetectEncoding(byte[] bytes)
|
||||
{
|
||||
return DetectEncoding(new MemoryStream(bytes), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 BOM 判断编码
|
||||
/// </summary>
|
||||
private static Encoding? _DetectByBom(Stream stream, long originalPosition)
|
||||
{
|
||||
stream.Position = originalPosition;
|
||||
// 获取最长样本长度
|
||||
var readableLength = stream.Length - stream.Position;
|
||||
var sampleLength = Math.Min(readableLength, 4);
|
||||
var buffer = new byte[sampleLength];
|
||||
var actualRead = stream.Read(buffer, 0, buffer.Length);
|
||||
if (actualRead != sampleLength) throw new Exception("无法获取样本长度");
|
||||
|
||||
// 对样本进行分析
|
||||
if (sampleLength >= 3 && buffer is [0xef, 0xbb, 0xbf])
|
||||
return Encoding.UTF8; // UTF-8
|
||||
|
||||
if (sampleLength >= 2)
|
||||
{
|
||||
if (buffer is [0xfe, 0xff])
|
||||
return Encoding.BigEndianUnicode; // UTF-16 BE
|
||||
if (buffer is [0xff, 0xfe])
|
||||
{
|
||||
if (sampleLength >= 4 && buffer is [_, _, 0x00, 0x00])
|
||||
return Encoding.UTF32; // UTF-32 LE
|
||||
return Encoding.Unicode; // UTF-16 LE
|
||||
}
|
||||
}
|
||||
|
||||
if (sampleLength >= 4)
|
||||
{
|
||||
if (buffer is [0x00, 0x00, 0xfe, 0xff])
|
||||
return Encoding.GetEncoding("utf-32BE"); // UTF-32 BE
|
||||
if (buffer is [0xff, 0xfe, 0x00, 0x00])
|
||||
return Encoding.UTF32; // UTF-32 LE
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BOM 不存在时的备用检测策略
|
||||
/// </summary>
|
||||
private static Encoding? _DetectWithoutBOM(Stream stream, long originalPosition)
|
||||
{
|
||||
// 尝试验证是否为有效 UTF-8
|
||||
return _IsValidUtf8(stream, originalPosition) ? Encoding.UTF8 : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证流内容是否为合法 UTF-8(通过 round-trip 验证)
|
||||
/// </summary>
|
||||
private static bool _IsValidUtf8(Stream stream, long originalPosition)
|
||||
{
|
||||
const int sampleSize = 1024;
|
||||
var buffer = new byte[sampleSize];
|
||||
stream.Position = originalPosition;
|
||||
|
||||
try
|
||||
{
|
||||
var decoded = Encoding.UTF8.GetString(buffer);
|
||||
var roundTrip = Encoding.UTF8.GetBytes(decoded);
|
||||
return roundTrip.SequenceEqual(buffer);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace PCL.Core.Utils.Codecs;
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
public static class EncodingUtils {
|
||||
public static bool IsDefaultEncodingUtf8() => Encoding.Default.CodePage == 65001;
|
||||
|
||||
public static bool IsDefaultEncodingGbk() => Encoding.Default.CodePage == 936;
|
||||
|
||||
/// <summary>
|
||||
/// 解码字节数组为字符串,自动检测 BOM(UTF-8、UTF-16 LE/BE、UTF-32 LE/BE)或回退到 GB18030。
|
||||
/// </summary>
|
||||
/// <param name="bytes">要解码的字节数组。</param>
|
||||
/// <returns>解码后的字符串,失败时返回空字符串。</returns>
|
||||
public static string DecodeBytes(byte[] bytes) {
|
||||
if (bytes.Length == 0) return "";
|
||||
|
||||
// 使用 EncodingDetector 检测编码
|
||||
var encoding = EncodingDetector.DetectEncoding(bytes);
|
||||
|
||||
// 如果检测到有效编码(非 Encoding.Default),直接解码
|
||||
if (!encoding.Equals(Encoding.Default)) {
|
||||
ReadOnlySpan<byte> span = bytes.AsSpan();
|
||||
if (encoding.Equals(Encoding.UTF8) && span.Length >= 3 && span[0] == 0xEF && span[1] == 0xBB && span[2] == 0xBF) {
|
||||
return encoding.GetString(span[3..]); // 跳过 UTF-8 BOM
|
||||
}
|
||||
if (encoding.Equals(Encoding.BigEndianUnicode) && span.Length >= 2 && span[0] == 0xFE && span[1] == 0xFF) {
|
||||
return encoding.GetString(span[2..]); // 跳过 UTF-16 BE BOM
|
||||
}
|
||||
if (encoding.Equals(Encoding.Unicode) && span.Length >= 2 && span[0] == 0xFF && span[1] == 0xFE) {
|
||||
return encoding.GetString(span[2..]); // 跳过 UTF-16 LE BOM
|
||||
}
|
||||
if (encoding.Equals(Encoding.UTF32) && span.Length >= 4 && span[0] == 0xFF && span[1] == 0xFE && span[2] == 0x00 && span[3] == 0x00) {
|
||||
return encoding.GetString(span[4..]); // 跳过 UTF-32 LE BOM
|
||||
}
|
||||
if (encoding.CodePage == Encoding.GetEncoding("utf-32BE").CodePage && span.Length >= 4 && span[0] == 0x00 && span[1] == 0x00 && span[2] == 0xFE && span[3] == 0xFF) {
|
||||
return encoding.GetString(span[4..]); // 跳过 UTF-32 BE BOM
|
||||
}
|
||||
return encoding.GetString(span); // 无 BOM 或其他编码
|
||||
}
|
||||
|
||||
// 无 BOM 或检测为 Encoding.Default,尝试 UTF-8
|
||||
try {
|
||||
var utf8Result = Encoding.UTF8.GetString(bytes);
|
||||
return utf8Result.Contains('\uFFFD') ? Encodings.GB18030.GetString(bytes) : utf8Result; // 无效 UTF-8,回退到 GB18030
|
||||
} catch (DecoderFallbackException) {
|
||||
return Encodings.GB18030.GetString(bytes); // UTF-8 解码失败,回退到 GB18030
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.Text;
|
||||
|
||||
namespace PCL.Core.Utils.Codecs;
|
||||
|
||||
public static class Encodings {
|
||||
public static readonly Encoding GB18030 = Encoding.GetEncoding("GB18030");
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
public sealed class ConcurrentSet<T> : IProducerConsumerCollection<T>, ICollection<T> where T: notnull
|
||||
{
|
||||
private readonly ConcurrentDictionary<T, object?> _dictionary = new();
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
public int Count => _dictionary.Count;
|
||||
object ICollection.SyncRoot => ((ICollection)_dictionary).SyncRoot;
|
||||
bool ICollection.IsSynchronized => ((ICollection)_dictionary).IsSynchronized;
|
||||
public bool IgnoreDuplicated { get; init; } = false;
|
||||
|
||||
public bool TryAdd(T item)
|
||||
{
|
||||
if (!_dictionary.TryAdd(item, null) && !IgnoreDuplicated)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTake([UnscopedRef] out T item)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var keys = _dictionary.Keys;
|
||||
if (keys.Count == 0)
|
||||
{
|
||||
item = default!;
|
||||
return false;
|
||||
}
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (_dictionary.TryRemove(key, out _))
|
||||
{
|
||||
item = key;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(T item) => _dictionary.TryRemove(item, out _);
|
||||
|
||||
public void Add(T item)
|
||||
{
|
||||
if (!_dictionary.TryAdd(item, null) && !IgnoreDuplicated)
|
||||
throw new ArgumentException(nameof(ConcurrentSet<T>) + " 中已存在该元素");
|
||||
}
|
||||
|
||||
public void Clear() => _dictionary.Clear();
|
||||
|
||||
public bool Contains(T item) => _dictionary.ContainsKey(item);
|
||||
|
||||
// ReSharper disable once NotDisposedResourceIsReturned
|
||||
public IEnumerator<T> GetEnumerator() => _dictionary.Keys.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public void CopyTo(T[] array, int index) => _dictionary.Keys.CopyTo(array, index);
|
||||
|
||||
void ICollection.CopyTo(Array array, int index) => CopyTo((T[])array, index);
|
||||
|
||||
void ICollection<T>.CopyTo(T[] array, int arrayIndex) => CopyTo(array, arrayIndex);
|
||||
|
||||
public T[] ToArray() => _dictionary.Keys.ToArray();
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace PCL.Core.Utils.Diagnostics;
|
||||
|
||||
public static class StackHelper
|
||||
{
|
||||
private const string Unknown = "<unknown>";
|
||||
|
||||
// 返回“直接调用者”的可读名称(例如 Namespace.Type.Method(paramTypes))
|
||||
// includeNamespace: 是否包含命名空间
|
||||
// includeParameters: 是否包含参数类型列表
|
||||
// skipAppFrames: 额外跳过的应用层帧数量(例如你自己的日志包装器)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static string GetDirectCallerName(
|
||||
bool includeNamespace = true,
|
||||
bool includeParameters = false,
|
||||
int skipAppFrames = 0)
|
||||
{
|
||||
var st = new StackTrace(skipFrames: 1, fNeedFileInfo: false);
|
||||
var frame = st.GetFrame(skipAppFrames);
|
||||
var method = frame?.GetMethod();
|
||||
if (method is null) return Unknown;
|
||||
|
||||
method = _TryMapAsyncOrIterator(method);
|
||||
return _FormatMethod(method, includeNamespace, includeParameters);
|
||||
}
|
||||
|
||||
// 获取前 maxFrames 层调用栈,返回格式化后的每一帧字符串
|
||||
// needFileInfo=true 时会解析 PDB 以拿到文件名与行号(昂贵)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static IReadOnlyList<string> GetStack(
|
||||
int maxFrames = 10,
|
||||
bool includeNamespace = true,
|
||||
bool includeParameters = false,
|
||||
bool needFileInfo = false)
|
||||
{
|
||||
if (maxFrames <= 0) return [];
|
||||
|
||||
var st = new StackTrace(skipFrames: 1, fNeedFileInfo: needFileInfo);
|
||||
var list = new List<string>(capacity: Math.Min(maxFrames, st.FrameCount));
|
||||
|
||||
for (int i = 0, added = 0; i < st.FrameCount && added < maxFrames; i++)
|
||||
{
|
||||
var method = st.GetFrame(i)?.GetMethod();
|
||||
if (method is null) continue;
|
||||
|
||||
method = _TryMapAsyncOrIterator(method);
|
||||
var sig = _FormatMethod(method, includeNamespace, includeParameters);
|
||||
|
||||
if (needFileInfo)
|
||||
{
|
||||
var f = st.GetFrame(i);
|
||||
var file = f?.GetFileName();
|
||||
var line = f?.GetFileLineNumber() ?? 0;
|
||||
if (!string.IsNullOrEmpty(file) && line > 0)
|
||||
{
|
||||
sig = $"{sig} ({System.IO.Path.GetFileName(file)}:{line})";
|
||||
}
|
||||
}
|
||||
|
||||
list.Add(sig);
|
||||
added++;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
// 将 async/iterator 的 MoveNext 映射回原始方法名(尽力而为的启发式)
|
||||
private static MethodBase _TryMapAsyncOrIterator(MethodBase method)
|
||||
{
|
||||
if (method.Name != "MoveNext") return method;
|
||||
|
||||
var dt = method.DeclaringType;
|
||||
if (dt is null) return method;
|
||||
|
||||
// 典型生成类型名:<MethodName>d__12 或 <MethodName>g__Local|12_0
|
||||
var name = dt.Name;
|
||||
var lt = name.IndexOf('<');
|
||||
var gt = name.IndexOf('>');
|
||||
if (lt >= 0 && gt > lt + 1)
|
||||
{
|
||||
var originalName = name.Substring(lt + 1, gt - lt - 1);
|
||||
// 在声明类型的外层类型里查找同名方法(可能存在重载,选第一个匹配)
|
||||
var parent = dt.DeclaringType ?? dt; // 迭代器常为嵌套到原类型里
|
||||
foreach (var m in parent.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
if (string.Equals(m.Name, originalName, StringComparison.Ordinal))
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
private static string _FormatMethod(MethodBase method, bool includeNamespace, bool includeParameters, bool includeParameterNamespace = false)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var type = method.DeclaringType;
|
||||
if (type is not null)
|
||||
{
|
||||
sb.Append(_FormatTypeName(type, includeNamespace));
|
||||
sb.Append('.');
|
||||
}
|
||||
|
||||
sb.Append(method.Name);
|
||||
|
||||
if (method is MethodInfo { IsGenericMethod: true } mi)
|
||||
{
|
||||
sb.Append('[');
|
||||
var args = mi.GetGenericArguments();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(", ");
|
||||
sb.Append(_FormatTypeName(args[i], false));
|
||||
}
|
||||
sb.Append(']');
|
||||
}
|
||||
|
||||
if (includeParameters)
|
||||
{
|
||||
var ps = method.GetParameters();
|
||||
sb.Append('(');
|
||||
for (var i = 0; i < ps.Length; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(", ");
|
||||
sb.Append(_FormatTypeName(ps[i].ParameterType, includeParameterNamespace));
|
||||
if (!string.IsNullOrEmpty(ps[i].Name))
|
||||
{
|
||||
sb.Append(' ').Append(ps[i].Name);
|
||||
}
|
||||
}
|
||||
sb.Append(')');
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string _FormatTypeName(Type t, bool includeNamespace)
|
||||
{
|
||||
if (t.IsGenericType)
|
||||
{
|
||||
var def = t.GetGenericTypeDefinition();
|
||||
var name = def.Name;
|
||||
var tick = name.IndexOf('`');
|
||||
if (tick >= 0) name = name[..tick];
|
||||
|
||||
var ns = includeNamespace ? (def.Namespace is null ? "" : def.Namespace + ".") : "";
|
||||
var sb = new StringBuilder(ns).Append(name).Append('[');
|
||||
var args = t.GetGenericArguments();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(", ");
|
||||
sb.Append(_FormatTypeName(args[i], false));
|
||||
}
|
||||
sb.Append(']');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
return includeNamespace && t.Namespace is not null
|
||||
? t.Namespace + "." + t.Name
|
||||
: t.Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
部分内容参考了 https://github.com/LogosBible/bsdiff.net 的实现
|
||||
|
||||
Copyright 2010-2024 Logos Bible Software
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
Copyright 2003-2005 Colin Percival
|
||||
All rights reserved
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted providing that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using ICSharpCode.SharpZipLib.BZip2;
|
||||
|
||||
namespace PCL.Core.Utils.Diff;
|
||||
|
||||
|
||||
public class BsDiff : IBinaryDiff
|
||||
{
|
||||
private const int HeaderSize = 32; // 32-byte header
|
||||
private const int HeaderVersionIndex = 0;
|
||||
private const long HeaderVersion = 0x3034464649445342; // "BSDIFF40" in little-endian
|
||||
private const int HeaderCtrlIndex = 8;
|
||||
private const int HeaderDiffIndex = 16;
|
||||
private const int HeaderNewSizeIndex = 24;
|
||||
|
||||
/*
|
||||
File format:
|
||||
0 8 "BSDIFF40"
|
||||
8 8 X
|
||||
16 8 Y
|
||||
24 8 sizeof(newfile)
|
||||
32 X bzip2(control block)
|
||||
32+X Y bzip2(diff block)
|
||||
32+X+Y ??? bzip2(extra block)
|
||||
with control block a set of triples (x,y,z) meaning "add x bytes
|
||||
from oldfile to x bytes from the diff block; copy y bytes from the
|
||||
extra block; seek forwards in oldfile by z bytes".
|
||||
*/
|
||||
|
||||
public async Task<byte[]> ApplyAsync(byte[] originData, byte[] diffData)
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
if (diffData.Length < HeaderSize)
|
||||
throw new Exception("Diff file size is less than the header size");
|
||||
if (BitConverter.ToInt64(diffData, HeaderVersionIndex) != HeaderVersion)
|
||||
throw new Exception("Diff file version is wrong");
|
||||
// 读取 Header 信息
|
||||
var ctrlLen = BitConverter.ToInt64(diffData, HeaderCtrlIndex);
|
||||
var diffLen = BitConverter.ToInt64(diffData, HeaderDiffIndex);
|
||||
var newLen = BitConverter.ToInt64(diffData, HeaderNewSizeIndex);
|
||||
var extraLen = diffData.Length - HeaderSize - ctrlLen - diffLen;
|
||||
|
||||
if (ctrlLen < 0 || diffLen < 0 || extraLen < 0)
|
||||
throw new Exception("Block size is negative");
|
||||
if (newLen < 0)
|
||||
throw new Exception("Final file size info is negative");
|
||||
if (HeaderSize + ctrlLen + diffLen + extraLen > diffData.Length)
|
||||
throw new Exception("Diff file size info is not correct");
|
||||
|
||||
Console.WriteLine(
|
||||
$"Got diff-data-len = {diffData.Length}, ctrllen = {ctrlLen}, difflen = {diffLen}, extralen = {extraLen}, totallen = {newLen}");
|
||||
|
||||
var ctrlContent = new byte[ctrlLen];
|
||||
// 获取 Control 数据
|
||||
long curOffset = HeaderSize;
|
||||
Array.Copy(diffData, curOffset, ctrlContent, 0, ctrlLen);
|
||||
using var ctrlStream = new BZip2InputStream(new MemoryStream(ctrlContent));
|
||||
using var ctrlReader = new BinaryReader(ctrlStream);
|
||||
// 获取 Diff 数据
|
||||
curOffset += ctrlLen;
|
||||
var diffContent = new byte[diffLen];
|
||||
Array.Copy(diffData, curOffset, diffContent, 0, diffLen);
|
||||
using var diffStream = new BZip2InputStream(new MemoryStream(diffContent));
|
||||
using var diffReader = new BinaryReader(diffStream);
|
||||
// 获取 Extra 数据
|
||||
curOffset += diffLen;
|
||||
var extraContent = new byte[extraLen];
|
||||
Array.Copy(diffData, curOffset, extraContent, 0, extraLen);
|
||||
using var extraStream = new BZip2InputStream(new MemoryStream(extraContent));
|
||||
using var extraReader = new BinaryReader(extraStream);
|
||||
|
||||
var ret = new byte[newLen];
|
||||
|
||||
long newDataPos = 0;
|
||||
long oldDataPos = 0;
|
||||
while (newDataPos < newLen)
|
||||
{
|
||||
var addRange = ReadInt64(ctrlReader.ReadBytes(8));
|
||||
var copyRange = ReadInt64(ctrlReader.ReadBytes(8));
|
||||
var seekPos = ReadInt64(ctrlReader.ReadBytes(8));
|
||||
|
||||
Console.WriteLine($"Round add-range = {addRange}, copy-range = {copyRange}, seek-pos = {seekPos}");
|
||||
|
||||
// 新加入的
|
||||
if (newDataPos + addRange > newLen)
|
||||
throw new Exception(
|
||||
$"Add range overflows, want add {addRange.ToString()}, but only have {newLen - newDataPos} left");
|
||||
|
||||
for (long i = 0; i < addRange; i++)
|
||||
{
|
||||
var readedByte = diffReader.ReadByte();
|
||||
if (oldDataPos + i < originData.Length)
|
||||
ret[newDataPos + i] = (byte)(readedByte + originData[oldDataPos + i]);
|
||||
else
|
||||
ret[newDataPos + i] = readedByte;
|
||||
}
|
||||
|
||||
newDataPos += addRange;
|
||||
oldDataPos += addRange;
|
||||
|
||||
// 原有的
|
||||
if (newDataPos + copyRange > newLen)
|
||||
throw new Exception(
|
||||
$"Copy range overflows, want copy {copyRange.ToString()}, but only have {newLen - newDataPos} left");
|
||||
|
||||
for (var i = 0; i < copyRange; i++)
|
||||
{
|
||||
ret[newDataPos + i] = extraReader.ReadByte();
|
||||
}
|
||||
|
||||
newDataPos += copyRange;
|
||||
|
||||
// 原有的切换到指定位置继续读取
|
||||
oldDataPos += seekPos;
|
||||
if (oldDataPos > originData.Length)
|
||||
throw new Exception(
|
||||
$"Old data pos overflows, current old data length = {originData.Length}, but want {oldDataPos}");
|
||||
}
|
||||
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<byte[]> MakeAsync(byte[] originData, byte[] newData)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
internal static long ReadInt64(byte[] buffer, int offset = 0)
|
||||
{
|
||||
// 手动组合小端序的 long 值
|
||||
var value = ((long)buffer[offset] << 0) | ((long)buffer[offset + 1] << 8) |
|
||||
((long)buffer[offset + 2] << 16) | ((long)buffer[offset + 3] << 24) |
|
||||
((long)buffer[offset + 4] << 32) | ((long)buffer[offset + 5] << 40) |
|
||||
((long)buffer[offset + 6] << 48) | ((long)buffer[offset + 7] << 56);
|
||||
|
||||
// 原始位运算逻辑保持不变
|
||||
var mask = value >> 63;
|
||||
return (~mask & value) |
|
||||
(((value & unchecked((long)0x8000000000000000)) - value) & mask);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Diff;
|
||||
|
||||
public interface IBinaryDiff
|
||||
{
|
||||
public Task<byte[]> MakeAsync(byte[] originData, byte[] newData);
|
||||
public Task<byte[]> ApplyAsync(byte[] originData, byte[] diffData);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace PCL.Core.Utils.Diff
|
||||
{
|
||||
public class ItemDiff
|
||||
{
|
||||
public static ItemDiffResult<T> ComputeDiff<T, TKey>(
|
||||
IEnumerable<T> oldItems,
|
||||
IEnumerable<T> newItems,
|
||||
Func<T, TKey> keySelector,
|
||||
IEqualityComparer<TKey>? keyComparer = null)
|
||||
where TKey : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(oldItems);
|
||||
ArgumentNullException.ThrowIfNull(newItems);
|
||||
ArgumentNullException.ThrowIfNull(keySelector);
|
||||
|
||||
var comparer = keyComparer ?? EqualityComparer<TKey>.Default;
|
||||
|
||||
var oldDict = oldItems.ToDictionary(keySelector, comparer);
|
||||
var newDict = newItems.ToDictionary(keySelector, comparer);
|
||||
|
||||
var oldKeys = new HashSet<TKey>(oldDict.Keys, comparer);
|
||||
var newKeys = new HashSet<TKey>(newDict.Keys, comparer);
|
||||
|
||||
var addedKeys = new HashSet<TKey>(newKeys.Except(oldKeys, comparer), comparer);
|
||||
var removedKeys = new HashSet<TKey>(oldKeys.Except(newKeys, comparer), comparer);
|
||||
var commonKeys = new HashSet<TKey>(oldKeys.Intersect(newKeys, comparer), comparer);
|
||||
|
||||
var added = addedKeys.Select(k => newDict[k]).ToList();
|
||||
var removed = removedKeys.Select(k => oldDict[k]).ToList();
|
||||
var unchanged = commonKeys.Select(k => newDict[k]).ToList();
|
||||
|
||||
return new ItemDiffResult<T>(added, removed, unchanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PCL.Core.Utils.Diff
|
||||
{
|
||||
public class ItemDiffResult<T>
|
||||
{
|
||||
public IReadOnlyList<T> Added { get; }
|
||||
public IReadOnlyList<T> Removed { get; }
|
||||
public IReadOnlyList<T> Unchanged { get; }
|
||||
|
||||
public ItemDiffResult(IReadOnlyList<T> added, IReadOnlyList<T> removed, IReadOnlyList<T> unchanged)
|
||||
{
|
||||
Added = added ?? throw new ArgumentNullException(nameof(added));
|
||||
Removed = removed ?? throw new ArgumentNullException(nameof(removed));
|
||||
Unchanged = unchanged ?? throw new ArgumentNullException(nameof(unchanged));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
internal static class EaseUtils
|
||||
{
|
||||
// 预计算常量
|
||||
private const double BounceN1 = 7.5625;
|
||||
private const double BounceInvD1 = 0.3636363636363636; // 1 / 2.75
|
||||
private const double BounceThreshold2 = 0.7272727272727273; // 2 / 2.75
|
||||
private const double BounceThreshold3 = 0.9090909090909091; // 2.5 / 2.75
|
||||
private const double BounceOffset1 = 0.5454545454545454; // 1.5 / 2.75
|
||||
private const double BounceOffset2 = 0.8181818181818182; // 2.25 / 2.75
|
||||
private const double BounceOffset3 = 0.9545454545454546; // 2.625 / 2.75
|
||||
|
||||
internal const double ElasticLn2Times10 = 6.931471805599453; // Math.Log(2d) * 10d
|
||||
internal const double ElasticPiTimes6Point5 = 20.420352248333657; // Math.PI * 6.5d
|
||||
|
||||
internal static double Bounce(double progress)
|
||||
{
|
||||
switch (progress)
|
||||
{
|
||||
case < BounceInvD1:
|
||||
return BounceN1 * progress * progress;
|
||||
case < BounceThreshold2:
|
||||
progress -= BounceOffset1;
|
||||
return BounceN1 * progress * progress + 0.75;
|
||||
case < BounceThreshold3:
|
||||
progress -= BounceOffset2;
|
||||
return BounceN1 * progress * progress + 0.9375;
|
||||
default:
|
||||
progress -= BounceOffset3;
|
||||
return BounceN1 * progress * progress + 0.984375;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace PCL.Core.Utils.Encryption
|
||||
{
|
||||
[Obsolete("Do not use this AES mode for Encryption")]
|
||||
public sealed class AesCbcProvider : IEncryptionProvider
|
||||
{
|
||||
public static AesCbcProvider Instance { get; } = new();
|
||||
|
||||
private const int SaltSize = 32;
|
||||
private const int IvSize = 16;
|
||||
|
||||
public byte[] Decrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.KeySize = 256;
|
||||
aes.BlockSize = 128;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
var salt = data[..SaltSize];
|
||||
|
||||
var iv = data[SaltSize..(SaltSize + IvSize)];
|
||||
aes.IV = iv.ToArray();
|
||||
|
||||
if (data.Length < salt.Length + iv.Length)
|
||||
{
|
||||
throw new ArgumentException("AES-CBC: Can not decrypt data, the encrypted data is broken");
|
||||
}
|
||||
|
||||
#pragma warning disable SYSLIB0041
|
||||
using (var deriveBytes = new Rfc2898DeriveBytes(key.ToArray(), salt.ToArray(), 1000))
|
||||
{
|
||||
aes.Key = deriveBytes.GetBytes(aes.KeySize / 8);
|
||||
}
|
||||
#pragma warning restore SYSLIB0041
|
||||
|
||||
using var ret = new MemoryStream();
|
||||
using var ms = new MemoryStream(data[(SaltSize + IvSize)..].ToArray());
|
||||
using var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read);
|
||||
cs.CopyTo(ret);
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
public byte[] Encrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key)
|
||||
{
|
||||
throw new NotSupportedException("You should no longer use AES-CBC as your encryption method");
|
||||
}
|
||||
|
||||
public bool IsSupported { get => false; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace PCL.Core.Utils.Encryption
|
||||
{
|
||||
public class AesGcmProvider : IEncryptionProvider
|
||||
{
|
||||
public static AesGcmProvider Instance { get; } = new();
|
||||
|
||||
private const int NonceSize = 12; // 96 bits
|
||||
private const int TagSize = 16; // 128 bits
|
||||
|
||||
public byte[] Encrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key)
|
||||
{
|
||||
byte[] nonce = new byte[NonceSize];
|
||||
RandomNumberGenerator.Fill(nonce);
|
||||
|
||||
byte[] tag = new byte[TagSize];
|
||||
byte[] ciphertext = new byte[data.Length];
|
||||
|
||||
using (var aesGcm = new AesGcm(key, TagSize))
|
||||
{
|
||||
aesGcm.Encrypt(nonce, data, ciphertext, tag);
|
||||
}
|
||||
|
||||
// [Nonce(12)] [Tag(16)] [Ciphertext(n)]
|
||||
byte[] result = new byte[NonceSize + TagSize + ciphertext.Length];
|
||||
Buffer.BlockCopy(nonce, 0, result, 0, NonceSize);
|
||||
Buffer.BlockCopy(tag, 0, result, NonceSize, TagSize);
|
||||
Buffer.BlockCopy(ciphertext, 0, result, NonceSize + TagSize, ciphertext.Length);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public byte[] Decrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key)
|
||||
{
|
||||
if (data.Length < NonceSize + TagSize)
|
||||
throw new ArgumentException("加密数据长度不足。");
|
||||
|
||||
ReadOnlySpan<byte> nonce = data.Slice(0, NonceSize);
|
||||
ReadOnlySpan<byte> tag = data.Slice(NonceSize, TagSize);
|
||||
ReadOnlySpan<byte> ciphertext = data.Slice(NonceSize + TagSize);
|
||||
|
||||
byte[] plaintext = new byte[ciphertext.Length];
|
||||
|
||||
using (var aesGcm = new AesGcm(key, TagSize))
|
||||
{
|
||||
aesGcm.Decrypt(nonce, ciphertext, tag, plaintext);
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
public bool IsSupported { get => AesGcm.IsSupported; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace PCL.Core.Utils.Encryption;
|
||||
|
||||
public sealed class ChaCha20Poly1305Provider : IEncryptionProvider
|
||||
{
|
||||
public static ChaCha20Poly1305Provider Instance { get; } = new();
|
||||
|
||||
private const int NonceSize = 12; // 96-bit nonce for ChaCha20Poly1305
|
||||
private const int TagSize = 16; // 128-bit authentication tag
|
||||
private const int KeySize = 32; // 256-bit key
|
||||
private const int SaltSize = 16; // 128-bit salt for HKDF
|
||||
|
||||
public byte[] Encrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key)
|
||||
{
|
||||
// Generate random salt, nonce and the tag
|
||||
var salt = new byte[SaltSize];
|
||||
var nonce = new byte[NonceSize];
|
||||
var tag = new byte[TagSize];
|
||||
RandomNumberGenerator.Fill(salt);
|
||||
RandomNumberGenerator.Fill(nonce);
|
||||
RandomNumberGenerator.Fill(tag);
|
||||
|
||||
// Derive key using the salt
|
||||
Span<byte> outputKey = stackalloc byte[KeySize];
|
||||
_DeriveKey(key, salt, outputKey);
|
||||
using var chacha = new ChaCha20Poly1305(outputKey);
|
||||
|
||||
// Prepare output arrays
|
||||
var ciphertext = new byte[data.Length];
|
||||
|
||||
// Perform encryption
|
||||
chacha.Encrypt(nonce, data, ciphertext, tag);
|
||||
|
||||
// Make the encryption data: salt + nonce + tag + ciphertext
|
||||
var result = new byte[SaltSize + NonceSize + ciphertext.Length + TagSize];
|
||||
var resultSpan = result.AsSpan();
|
||||
|
||||
salt.CopyTo(resultSpan[..SaltSize]);
|
||||
nonce.CopyTo(resultSpan.Slice(SaltSize, NonceSize));
|
||||
tag.CopyTo(resultSpan.Slice(SaltSize + NonceSize, TagSize));
|
||||
ciphertext.CopyTo(resultSpan.Slice(SaltSize + NonceSize + TagSize, ciphertext.Length));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public byte[] Decrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key)
|
||||
{
|
||||
// Verify minimum data length
|
||||
if (data.Length < SaltSize + NonceSize + TagSize)
|
||||
throw new ArgumentException("Invalid encrypted data length");
|
||||
|
||||
// Encryption data: salt + nonce + tag + ciphertext
|
||||
var salt = data[..SaltSize];
|
||||
var nonce = data.Slice(SaltSize, NonceSize);
|
||||
var tag = data.Slice(SaltSize + NonceSize, TagSize);
|
||||
var ciphertext = data[(SaltSize + NonceSize + TagSize)..];
|
||||
|
||||
// Derive key using the extracted salt
|
||||
Span<byte> outputKey = stackalloc byte[KeySize];
|
||||
_DeriveKey(key, salt, outputKey);
|
||||
using var chacha = new ChaCha20Poly1305(outputKey);
|
||||
|
||||
// Perform decryption
|
||||
var plaintext = new byte[ciphertext.Length];
|
||||
chacha.Decrypt(nonce, ciphertext, tag, plaintext);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
private static readonly byte[] _Info = "PCL.Core.Utils.Encryption.ChaCha20"u8.ToArray();
|
||||
private static void _DeriveKey(ReadOnlySpan<byte> ikm, ReadOnlySpan<byte> salt, Span<byte> outputKey)
|
||||
{
|
||||
HKDF.DeriveKey(
|
||||
HashAlgorithmName.SHA256,
|
||||
ikm,
|
||||
outputKey,
|
||||
salt,
|
||||
_Info.AsSpan());
|
||||
}
|
||||
|
||||
public bool IsSupported { get => ChaCha20Poly1305.IsSupported; }
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace PCL.Core.Utils.Encryption;
|
||||
|
||||
public class ChaCha20SoftwareProvider : IEncryptionProvider
|
||||
{
|
||||
public static ChaCha20SoftwareProvider Instance { get; } = new();
|
||||
|
||||
// 常量:expand 32-byte k
|
||||
private static ReadOnlySpan<uint> Sigma => new[] { 0x61707865u, 0x3320646eu, 0x79622d32u, 0x6b206574u };
|
||||
|
||||
public static bool IsSupported => true;
|
||||
|
||||
public byte[] Encrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key)
|
||||
{
|
||||
// 预留 12 字节 Nonce 空间
|
||||
var result = new byte[data.Length + 12];
|
||||
var nonce = result.AsSpan(0, 12);
|
||||
|
||||
// 生成随机 Nonce
|
||||
RandomNumberGenerator.Fill(nonce);
|
||||
|
||||
_Process(data, result.AsSpan(12), key, nonce);
|
||||
return result;
|
||||
}
|
||||
|
||||
public byte[] Decrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key)
|
||||
{
|
||||
if (data.Length < 12) throw new ArgumentException("数据长度不足以包含 Nonce");
|
||||
|
||||
var nonce = data[..12];
|
||||
var ciphertext = data[12..];
|
||||
var plaintext = new byte[ciphertext.Length];
|
||||
|
||||
_Process(ciphertext, plaintext, key, nonce);
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
private static void _Process(ReadOnlySpan<byte> input, Span<byte> output, ReadOnlySpan<byte> key, ReadOnlySpan<byte> nonce)
|
||||
{
|
||||
if (key.Length != 32) throw new ArgumentException("Key 必须为 32 字节");
|
||||
if (nonce.Length != 12) throw new ArgumentException("Nonce 必须为 12 字节");
|
||||
|
||||
// 在栈上分配状态矩阵和工作块,避免 GC
|
||||
Span<uint> state = stackalloc uint[16];
|
||||
|
||||
// 1. 初始化状态
|
||||
Sigma.CopyTo(state[..4]);
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
state[4 + i] = BinaryPrimitives.ReadUInt32LittleEndian(key.Slice(i * 4, 4));
|
||||
}
|
||||
state[12] = 0; // 计数器初始值
|
||||
state[13] = BinaryPrimitives.ReadUInt32LittleEndian(nonce[..4]);
|
||||
state[14] = BinaryPrimitives.ReadUInt32LittleEndian(nonce[4..8]);
|
||||
state[15] = BinaryPrimitives.ReadUInt32LittleEndian(nonce[8..12]);
|
||||
|
||||
Span<uint> workingBlock = stackalloc uint[16];
|
||||
var offset = 0;
|
||||
var length = input.Length;
|
||||
|
||||
while (offset < length)
|
||||
{
|
||||
_GenerateBlock(workingBlock, state);
|
||||
state[12]++; // 递增计数器
|
||||
|
||||
var remaining = Math.Min(64, length - offset);
|
||||
var inputPart = input.Slice(offset, remaining);
|
||||
var outputPart = output.Slice(offset, remaining);
|
||||
|
||||
// 将 uint 块视为字节流进行异或
|
||||
var blockBytes = MemoryMarshal.AsBytes(workingBlock);
|
||||
for (var i = 0; i < remaining; i++)
|
||||
{
|
||||
outputPart[i] = (byte)(inputPart[i] ^ blockBytes[i]);
|
||||
}
|
||||
|
||||
offset += 64;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void _GenerateBlock(Span<uint> x, ReadOnlySpan<uint> input)
|
||||
{
|
||||
input.CopyTo(x);
|
||||
|
||||
for (var i = 0; i < 10; i++) // 20 轮计算
|
||||
{
|
||||
// 列变换
|
||||
_QuarterRound(x, 0, 4, 8, 12);
|
||||
_QuarterRound(x, 1, 5, 9, 13);
|
||||
_QuarterRound(x, 2, 6, 10, 14);
|
||||
_QuarterRound(x, 3, 7, 11, 15);
|
||||
// 对角线变换
|
||||
_QuarterRound(x, 0, 5, 10, 15);
|
||||
_QuarterRound(x, 1, 6, 11, 12);
|
||||
_QuarterRound(x, 2, 7, 8, 13);
|
||||
_QuarterRound(x, 3, 4, 9, 14);
|
||||
}
|
||||
|
||||
for (var i = 0; i < 16; i++)
|
||||
{
|
||||
x[i] += input[i];
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void _QuarterRound(Span<uint> x, int a, int b, int c, int d)
|
||||
{
|
||||
x[a] += x[b]; x[d] ^= x[a]; x[d] = BitOperations.RotateLeft(x[d], 16);
|
||||
x[c] += x[d]; x[b] ^= x[c]; x[b] = BitOperations.RotateLeft(x[b], 12);
|
||||
x[a] += x[b]; x[d] ^= x[a]; x[d] = BitOperations.RotateLeft(x[d], 8);
|
||||
x[c] += x[d]; x[b] ^= x[c]; x[b] = BitOperations.RotateLeft(x[b], 7);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Utils.Encryption;
|
||||
|
||||
public interface IEncryptionProvider
|
||||
{
|
||||
public byte[] Encrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key);
|
||||
public byte[] Decrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key);
|
||||
|
||||
public static bool IsSupported { get; }
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
// Partly generated by o4-mini (20250611)
|
||||
// ReSharper disable All
|
||||
#nullable disable
|
||||
|
||||
public sealed class ExpandoObjectConverter : JsonConverter<ExpandoObject>
|
||||
{
|
||||
public static readonly ExpandoObjectConverter Default = new ExpandoObjectConverter();
|
||||
|
||||
public ExpandoObjectConverter()
|
||||
{
|
||||
}
|
||||
|
||||
public override ExpandoObject Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using (JsonDocument document = JsonDocument.ParseValue(ref reader))
|
||||
{
|
||||
JsonElement root = document.RootElement;
|
||||
return Read(root, options);
|
||||
}
|
||||
}
|
||||
|
||||
public static ExpandoObject Read(
|
||||
JsonElement element,
|
||||
JsonSerializerOptions options)
|
||||
{
|
||||
ExpandoObject expandoObject = new ExpandoObject();
|
||||
IDictionary<string, object> dict = expandoObject;
|
||||
foreach (JsonProperty property in element.EnumerateObject())
|
||||
{
|
||||
dict.Add(property.Name, _ConvertElement(property.Value, options));
|
||||
}
|
||||
|
||||
return expandoObject;
|
||||
}
|
||||
|
||||
private static object _ConvertElement(JsonElement element, JsonSerializerOptions options)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => Read(element, options),
|
||||
JsonValueKind.Array => element.EnumerateArray()
|
||||
.Select(item => _ConvertElement(item, options))
|
||||
.ToList(),
|
||||
JsonValueKind.String => element.GetString(),
|
||||
JsonValueKind.Number when element.TryGetInt64(out var integer) => integer,
|
||||
JsonValueKind.Number when element.TryGetDouble(out var number) => number,
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.Null => null,
|
||||
JsonValueKind.Undefined => null,
|
||||
_ => element.GetRawText()
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(
|
||||
Utf8JsonWriter writer,
|
||||
ExpandoObject value,
|
||||
JsonSerializerOptions options)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStartObject();
|
||||
|
||||
foreach (KeyValuePair<string, object> kvp in value)
|
||||
{
|
||||
writer.WritePropertyName(kvp.Key);
|
||||
JsonSerializer.Serialize(writer, kvp.Value, options);
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace PCL.Core.Utils.Exts;
|
||||
|
||||
public static class AsyncEnumerableExtensions
|
||||
{
|
||||
/// <param name="source">源集合</param>
|
||||
/// <typeparam name="T">集合元素类型</typeparam>
|
||||
extension<T>(IEnumerable<T> source)
|
||||
{
|
||||
/// <summary>
|
||||
/// 对集合中的每个元素异步执行指定操作,最多同时运行 maxDegreeOfParallelism 个任务。
|
||||
/// </summary>
|
||||
/// <param name="action">对每个元素执行的异步操作</param>
|
||||
/// <param name="maxDegreeOfParallelism">最大并发数,默认为 10</param>
|
||||
/// <returns>所有任务完成后的任务</returns>
|
||||
public async Task ForEachAsync(
|
||||
Func<T, Task> action,
|
||||
int maxDegreeOfParallelism = 10)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxDegreeOfParallelism);
|
||||
|
||||
var semaphore = new SemaphoreSlim(maxDegreeOfParallelism);
|
||||
|
||||
var tasks = source.Select(async item =>
|
||||
{
|
||||
await semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
await action(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对集合中的每个元素异步执行指定操作,最多同时运行 maxDegreeOfParallelism 个任务。
|
||||
/// </summary>
|
||||
/// <param name="action">对每个元素执行的异步操作</param>
|
||||
/// <param name="maxDegreeOfParallelism">最大并发数,默认为 10</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>所有任务完成后的任务</returns>
|
||||
public async Task ForEachAsync(
|
||||
Func<T, CancellationToken, Task> action,
|
||||
int maxDegreeOfParallelism = 10,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxDegreeOfParallelism);
|
||||
|
||||
var semaphore = new SemaphoreSlim(maxDegreeOfParallelism);
|
||||
|
||||
var tasks = source.Select(async item =>
|
||||
{
|
||||
await semaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await action(item, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对集合中的每个元素异步执行指定操作,限制并发数,并返回所有操作的结果。
|
||||
/// 等同于:source.Select(x => action(x)).WhenAll(),但有并发控制。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">操作返回类型</typeparam>
|
||||
/// <param name="selector">异步选择器函数</param>
|
||||
/// <param name="maxDegreeOfParallelism">最大并发数,默认为 10</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>包含所有操作结果的集合</returns>
|
||||
public async Task<IEnumerable<TResult>> SelectAsync<TResult>(
|
||||
Func<T, Task<TResult>> selector,
|
||||
int maxDegreeOfParallelism = 10,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(selector);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxDegreeOfParallelism);
|
||||
|
||||
var semaphore = new SemaphoreSlim(maxDegreeOfParallelism);
|
||||
|
||||
var tasks = source.Select(async item =>
|
||||
{
|
||||
await semaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
return await selector(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
});
|
||||
|
||||
return await Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Utils.Exts;
|
||||
|
||||
public static class ByteExtension
|
||||
{
|
||||
extension(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
public string ToHexString() => Convert.ToHexString(bytes).ToLower();
|
||||
public string FromByteToB64() => Convert.ToBase64String(bytes);
|
||||
public string FromBytesToB64UrlSafe() => bytes.FromByteToB64().FromB64ToB64UrlSafe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PCL.Core.Utils.Exts;
|
||||
|
||||
public static class ConcurrentDictionaryExtension
|
||||
{
|
||||
extension<TKey, TValue>(ConcurrentDictionary<TKey, TValue> dict) where TKey: notnull
|
||||
{
|
||||
public bool CompareAndRemove(TKey key,
|
||||
TValue comparison) => ((ICollection<KeyValuePair<TKey, TValue>>)dict).Remove(new KeyValuePair<TKey, TValue>(key, comparison));
|
||||
|
||||
public TValue? UpdateAndGetPrevious(TKey key,
|
||||
TValue value)
|
||||
{
|
||||
TValue? prevValue = default;
|
||||
dict.AddOrUpdate(key, _ =>
|
||||
{
|
||||
prevValue = default;
|
||||
return value;
|
||||
}, (_, existingValue) =>
|
||||
{
|
||||
prevValue = existingValue;
|
||||
return value;
|
||||
});
|
||||
return prevValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Exts;
|
||||
|
||||
public static class HttpRequestExtension
|
||||
{
|
||||
public static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessage request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var clone = new HttpRequestMessage(request.Method, request.RequestUri)
|
||||
{
|
||||
Version = request.Version,
|
||||
VersionPolicy = request.VersionPolicy
|
||||
};
|
||||
|
||||
if (request.Content is not null)
|
||||
{
|
||||
clone.Content = await request.Content._DeepCloneAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var header in request.Headers)
|
||||
{
|
||||
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
foreach (var option in request.Options)
|
||||
{
|
||||
clone.Options.TryAdd(option.Key, option.Value);
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static async Task<HttpContent?> _DeepCloneAsync(this HttpContent content)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
await content.CopyToAsync(ms).ConfigureAwait(false);
|
||||
ms.Position = 0;
|
||||
|
||||
var clone = new StreamContent(ms);
|
||||
|
||||
// 复制内容头(如 Content-Type, Content-Length 等)
|
||||
foreach (var header in content.Headers)
|
||||
{
|
||||
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace PCL.Core.Utils.Exts;
|
||||
|
||||
public static class LanguageSpecificStringDictionaryExtensions
|
||||
{
|
||||
public static string GetForCurrentUiCulture(this LanguageSpecificStringDictionary dict, string? fallback = null)
|
||||
{
|
||||
var ui = CultureInfo.CurrentUICulture;
|
||||
|
||||
// 1) 精确匹配,如 zh-Hans-CN
|
||||
if (TryFromTag(dict, ui.IetfLanguageTag, out var v))
|
||||
return v;
|
||||
|
||||
// 2) 逐级回退,如 zh-Hans-CN -> zh-Hans -> zh
|
||||
var tag = ui.IetfLanguageTag;
|
||||
for (var dash = tag.LastIndexOf('-'); dash > 0; dash = tag.LastIndexOf('-'))
|
||||
{
|
||||
tag = tag.Substring(0, dash);
|
||||
if (TryFromTag(dict, tag, out v))
|
||||
return v;
|
||||
}
|
||||
|
||||
// 3) 再尝试父文化(当 CurrentUICulture 是特定文化时)
|
||||
if (!ui.IsNeutralCulture && TryFromTag(dict, ui.Parent.IetfLanguageTag, out v))
|
||||
return v;
|
||||
|
||||
// 4) 兜底:取字典中的第一个值,或使用传入的 fallback,或空字符串
|
||||
if (dict.Count > 0) return dict.Values!.First();
|
||||
return fallback ?? string.Empty;
|
||||
|
||||
static bool TryFromTag(LanguageSpecificStringDictionary d, string ietf, out string value)
|
||||
=> d.TryGetValue(XmlLanguage.GetLanguage(ietf), out value!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PCL.Core.Utils.Exts;
|
||||
|
||||
public static class ListUtils
|
||||
{
|
||||
extension<T>(IEnumerable<T> source)
|
||||
{
|
||||
/// <summary>
|
||||
/// 选择最大值对应的对象。
|
||||
/// 若没有元素则返回 default(T)。
|
||||
/// </summary>
|
||||
public T? MaxOrDefault<C>(Func<T, C> selector) where C : IComparable<C>
|
||||
{
|
||||
using var enumerator = source.GetEnumerator();
|
||||
if (!enumerator.MoveNext()) return default;
|
||||
var maxItem = enumerator.Current;
|
||||
var maxValue = selector(maxItem);
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var value = selector(enumerator.Current);
|
||||
if (value.CompareTo(maxValue) <= 0) continue;
|
||||
maxItem = enumerator.Current;
|
||||
maxValue = value;
|
||||
}
|
||||
return maxItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选择最小值对应的对象。
|
||||
/// 若没有元素则返回 default(T)。
|
||||
/// </summary>
|
||||
public T? MinOrDefault<C>(Func<T, C> selector) where C : IComparable<C>
|
||||
{
|
||||
using var enumerator = source.GetEnumerator();
|
||||
if (!enumerator.MoveNext()) return default;
|
||||
var minItem = enumerator.Current;
|
||||
var minValue = selector(minItem);
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var value = selector(enumerator.Current);
|
||||
if (value.CompareTo(minValue) >= 0) continue;
|
||||
minItem = enumerator.Current;
|
||||
minValue = value;
|
||||
}
|
||||
return minItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class SortUtils {
|
||||
/// <summary>
|
||||
/// 对列表进行稳定排序,返回新列表。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">列表元素类型。</typeparam>
|
||||
/// <param name="list">要排序的列表。</param>
|
||||
/// <param name="comparison">比较器,接收两个对象,若第一个对象应排在前面,则返回 true。</param>
|
||||
/// <returns>排序后的新列表。</returns>
|
||||
public static List<T> Sort<T>(this IList<T> list, Func<T, T, bool> comparison) {
|
||||
// 创建新列表以避免修改原始列表
|
||||
var result = new List<T>(list);
|
||||
result.Sort(new StableComparer<T>(comparison));
|
||||
return result;
|
||||
}
|
||||
|
||||
private class StableComparer<T>(Func<T, T, bool> comparison) : IComparer<T> {
|
||||
private readonly Func<T, T, bool> _comparison = comparison ?? throw new ArgumentNullException(nameof(comparison));
|
||||
|
||||
public int Compare(T? x, T? y) {
|
||||
if (x is null && y is null) return 0;
|
||||
if (x is null) return -1;
|
||||
if (y is null) return 1;
|
||||
|
||||
var xComesFirst = _comparison(x, y);
|
||||
var yComesFirst = _comparison(y, x);
|
||||
|
||||
if (!xComesFirst && !yComesFirst) return 0; // 相等,保持稳定
|
||||
return xComesFirst ? -1 : 1; // x 在前返回 -1,否则返回 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PCL.Core.Utils.Exts;
|
||||
|
||||
public static class StringConvertExtension
|
||||
{
|
||||
public static object? Convert(string? value, Type targetType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(targetType);
|
||||
|
||||
if (targetType == typeof(string)) return value;
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
if (!targetType.IsValueType || Nullable.GetUnderlyingType(targetType) is not null) return null;
|
||||
return Activator.CreateInstance(targetType);
|
||||
}
|
||||
|
||||
var converter = TypeDescriptor.GetConverter(targetType);
|
||||
|
||||
if (converter.CanConvertFrom(typeof(string)))
|
||||
{
|
||||
var c = converter.ConvertFromInvariantString(value);
|
||||
return c;
|
||||
}
|
||||
|
||||
if (typeof(IConvertible).IsAssignableFrom(targetType))
|
||||
{
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression
|
||||
var changed = System.Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)!;
|
||||
return changed;
|
||||
}
|
||||
|
||||
if (targetType.IsEnum) return Enum.Parse(targetType, value, ignoreCase: true);
|
||||
|
||||
var parse = targetType.GetMethod("Parse",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
binder: null, types: [typeof(string)], modifiers: null);
|
||||
if (parse is not null) return parse.Invoke(null, [value]);
|
||||
|
||||
throw new NotSupportedException($"无法将字符串转换为类型 {targetType.FullName}");
|
||||
}
|
||||
|
||||
public static T? Convert<T>(this string? value)
|
||||
{
|
||||
var obj = Convert(value, typeof(T));
|
||||
if (obj is null) return default;
|
||||
return (T)obj;
|
||||
}
|
||||
}
|
||||
|
||||
public static class StringExtension
|
||||
{
|
||||
public static string? ConvertToString(object? obj)
|
||||
{
|
||||
if (obj is null) return null;
|
||||
if (obj is string s) return s;
|
||||
|
||||
var converter = TypeDescriptor.GetConverter(obj.GetType());
|
||||
if (converter.CanConvertTo(typeof(string)))
|
||||
{
|
||||
object? o = converter.ConvertToInvariantString(obj);
|
||||
return o as string;
|
||||
}
|
||||
|
||||
if (obj is IFormattable fmt) return fmt.ToString(null, CultureInfo.InvariantCulture);
|
||||
|
||||
return obj.ToString();
|
||||
}
|
||||
|
||||
public static string? ConvertToString<T>(this T? value) => ConvertToString((object?)value);
|
||||
|
||||
private static readonly char[] _B36Map = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();
|
||||
|
||||
extension(string input)
|
||||
{
|
||||
public string FromB10ToB36()
|
||||
{
|
||||
var n = BigInteger.Parse(input);
|
||||
var s = new List<char>();
|
||||
while (n > 0)
|
||||
{
|
||||
var i = (n % 36).ToByteArray()[0];
|
||||
s.Add(_B36Map[i]);
|
||||
n /= 36;
|
||||
}
|
||||
s.Reverse();
|
||||
return string.Join("", s);
|
||||
}
|
||||
|
||||
public string FromB36ToB10()
|
||||
{
|
||||
var ns = input.Select(c => (c is >= '0' and <= '9') ? c - '0' : c - 'A' + 10).ToArray();
|
||||
var nb = ns.Aggregate(new BigInteger(0), (n, i) => n * 36 + i);
|
||||
return nb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly char[] _B32Map = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ".ToCharArray();
|
||||
|
||||
extension(string input)
|
||||
{
|
||||
/// <summary>
|
||||
/// 将 Base10 文本重新编码为 Base32 文本。
|
||||
/// </summary>
|
||||
public string FromB10ToB32()
|
||||
{
|
||||
var n = BigInteger.Parse(input);
|
||||
var s = new List<char>();
|
||||
while (n > 0)
|
||||
{
|
||||
var i = (n % 32).ToByteArray()[0];
|
||||
s.Add(_B32Map[i]);
|
||||
n /= 32;
|
||||
}
|
||||
s.Reverse();
|
||||
return string.Join("", s);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Base32 文本重新编码为 Base10 文本。
|
||||
/// </summary>
|
||||
public string FromB32ToB10()
|
||||
{
|
||||
var ns = input.Select(Parse).ToArray();
|
||||
var nb = ns.Aggregate(new BigInteger(0), (n, i) => n * 32 + i);
|
||||
return nb.ToString();
|
||||
|
||||
int Parse(char c) => c switch
|
||||
{
|
||||
>= '2' and <= '9' => c - '2',
|
||||
>= 'A' and <= 'H' => c - 'A' + 8,
|
||||
>= 'J' and <= 'N' => c - 'J' + 16,
|
||||
>= 'P' and <= 'Z' => c - 'P' + 21,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(input), $"Character '{c}' out of Base32 range")
|
||||
};
|
||||
}
|
||||
|
||||
public string FromB64ToB64UrlSafe() => input.Replace("+", "-").Replace("/", "_");
|
||||
public string FromB64UrlSafeToB64() => input.Replace("-", "+").Replace("_", "/");
|
||||
|
||||
public byte[] FromB64ToBytes()
|
||||
{
|
||||
switch (input.Length % 4)
|
||||
{
|
||||
case 3:
|
||||
input += "===";
|
||||
break;
|
||||
case 2:
|
||||
input += "==";
|
||||
break;
|
||||
case 1:
|
||||
input += "=";
|
||||
break;
|
||||
}
|
||||
|
||||
return Convert.FromBase64String(input);
|
||||
}
|
||||
|
||||
public byte[] FromB64UrlSafeToBytes() => input.FromB64UrlSafeToB64().FromB64ToBytes();
|
||||
}
|
||||
|
||||
extension(string input)
|
||||
{
|
||||
|
||||
public T ParseToEnum<T>() where T : struct, Enum
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return (T)(object)0;
|
||||
}
|
||||
else if (int.TryParse(input, out int numericValue))
|
||||
{
|
||||
return (T)(object)numericValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Enum.Parse<T>(input, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension([NotNullWhen(false)] string? value)
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="string.IsNullOrEmpty"/> 的扩展方法。
|
||||
/// </summary>
|
||||
public bool IsNullOrEmpty() => string.IsNullOrEmpty(value);
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="string.IsNullOrWhiteSpace"/> 的扩展方法。
|
||||
/// </summary>
|
||||
public bool IsNullOrWhiteSpace() => string.IsNullOrWhiteSpace(value);
|
||||
}
|
||||
|
||||
/// <param name="input">文本</param>
|
||||
extension(string? input)
|
||||
{
|
||||
/// <summary>
|
||||
/// 当文本为空时返回替代文本,否则返回原来的文本。
|
||||
/// </summary>
|
||||
/// <param name="replacement">替代文本</param>
|
||||
public string ReplaceNullOrEmpty(string? replacement = null)
|
||||
=> string.IsNullOrEmpty(input) ? (replacement ?? string.Empty) : input;
|
||||
|
||||
/// <summary>
|
||||
/// 替换指定文本中的所有换行符。
|
||||
/// </summary>
|
||||
/// <param name="replacement">用于替换的文本</param>
|
||||
/// <returns>替换后的文本</returns>
|
||||
public string ReplaceLineBreak(string replacement = " ")
|
||||
=> input?.Replace(RegexPatterns.NewLine, replacement) ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 替换指定文本中所有匹配正则表达式的部分。
|
||||
/// </summary>
|
||||
/// <param name="regex">正则表达式</param>
|
||||
/// <param name="replacement">用于替换的文本</param>
|
||||
/// <returns>替换后的文本</returns>
|
||||
[return: NotNullIfNotNull(nameof(input))]
|
||||
public string? Replace(Regex regex, string replacement)
|
||||
=> input is null ? null : regex.Replace(input, replacement);
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定文本是否能成功匹配正则表达式。
|
||||
/// </summary>
|
||||
/// <param name="regex">正则表达式</param>
|
||||
/// <returns>若匹配成功则为 <c>true</c>,若文本为 <c>null</c> 或匹配不成功则为 <c>false</c></returns>
|
||||
public bool IsMatch(Regex regex)
|
||||
=> input is not null && regex.IsMatch(input);
|
||||
}
|
||||
|
||||
extension(string str)
|
||||
{
|
||||
/// <summary>
|
||||
/// 查找并返回指定文本中所有与正则表达式匹配的部分。
|
||||
/// </summary>
|
||||
public List<string> RegexSearch(Regex regex)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var regexSearchRes = regex.Matches(str);
|
||||
if (regexSearchRes.Count == 0) return result;
|
||||
result.AddRange(from Match item in regexSearchRes select item.Value);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定文本是否在 ASCII 范围内。
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public bool IsASCII()
|
||||
{
|
||||
return str.All(c => c < 128);
|
||||
}
|
||||
|
||||
public bool StartsWithF(string prefix, bool ignoreCase = false)
|
||||
=> str.StartsWith(prefix, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
|
||||
public bool EndsWithF(string suffix, bool ignoreCase = false)
|
||||
=> str.EndsWith(suffix, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
|
||||
public bool ContainsF(string subStr, bool ignoreCase = false)
|
||||
=> str.Contains(subStr, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
|
||||
public int IndexOfF(string subStr, bool ignoreCase = false)
|
||||
=> str.IndexOf(subStr, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
|
||||
public int IndexOfF(string subStr, int startIndex, bool ignoreCase = false)
|
||||
=> str.IndexOf(subStr, startIndex, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
|
||||
public int LastIndexOfF(string subStr, bool ignoreCase = false)
|
||||
=> str.LastIndexOf(subStr, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
|
||||
public int LastIndexOfF(string subStr, int startIndex, bool ignoreCase = false)
|
||||
=> str.LastIndexOf(subStr, startIndex, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
extension(string hex)
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte[] HexToBytes() => Convert.FromHexString(hex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Exts;
|
||||
public static class TaskExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回第一个成功完成的 Task 的结果。
|
||||
/// 如果所有 Task 都失败或被取消,则抛出 AggregateException。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Task 的返回类型</typeparam>
|
||||
/// <param name="tasks">要等待的任务集合</param>
|
||||
/// <returns>第一个成功完成的 Task 的结果</returns>
|
||||
/// <exception cref="ArgumentException">任务集合为空</exception>
|
||||
/// <exception cref="AggregateException">所有任务均未成功完成</exception>
|
||||
public static async Task<Task<T>> WhenAnySuccessAsync<T>(this IEnumerable<Task<T>> tasks)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tasks, nameof(tasks));
|
||||
var taskList = tasks.ToList();
|
||||
if (taskList.Count == 0)
|
||||
throw new ArgumentException("Task collection is empty.", nameof(tasks));
|
||||
|
||||
var remaining = new HashSet<Task<T>>(taskList);
|
||||
|
||||
while (remaining.Count > 0)
|
||||
{
|
||||
var completed = await Task.WhenAny(remaining);
|
||||
remaining.Remove(completed);
|
||||
if (completed.IsCompletedSuccessfully)
|
||||
{
|
||||
return completed;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有任务都失败或被取消
|
||||
var faultedExceptions = taskList
|
||||
.Where(t => t.IsFaulted)
|
||||
.SelectMany(t => t.Exception?.Flatten().InnerExceptions ?? Enumerable.Empty<Exception>())
|
||||
.ToList();
|
||||
|
||||
if (faultedExceptions.Count > 0)
|
||||
{
|
||||
throw new AggregateException("All connection attempts failed.", faultedExceptions);
|
||||
}
|
||||
|
||||
// 如果没有失败任务,但还有任务,为 canceled
|
||||
if (taskList.Any(t => t.IsCanceled))
|
||||
{
|
||||
throw new OperationCanceledException("All connection attempts were canceled.");
|
||||
}
|
||||
|
||||
// Task 要么成功,要么 Faulted,要么 Canceled,大概率走不到这
|
||||
throw new InvalidOperationException("All tasks completed but none succeeded, failed, or canceled.");
|
||||
}
|
||||
|
||||
extension(Task task)
|
||||
{
|
||||
/// <summary>
|
||||
/// 不关心 Task 后续的异常,但是需要观察下
|
||||
/// </summary>
|
||||
public void Forget()
|
||||
{
|
||||
_ = task.ContinueWith(t => _ = t.Exception, TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace PCL.Core.Utils.Exts;
|
||||
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
/// <summary>
|
||||
/// 提供 WPF UI 控件的扩展方法。
|
||||
/// </summary>
|
||||
public static class UiExtension {
|
||||
/// <summary>
|
||||
/// 检查控件是否在指定窗口的可视区域内,且控件本身可见。
|
||||
/// </summary>
|
||||
/// <param name="element">要检查的 FrameworkElement。</param>
|
||||
/// <param name="mainWindow">主窗口,用于确定可视区域。</param>
|
||||
/// <returns>如果控件部分或完全在窗口可视区域内且可见,则返回 true;否则返回 false。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="element"/> 或 <paramref name="mainWindow"/> 为 null 时抛出。</exception>
|
||||
public static bool IsVisibleInWindow(this FrameworkElement element, Window mainWindow) {
|
||||
if (!element.IsVisible) return false;
|
||||
|
||||
try {
|
||||
var transform = element.TransformToAncestor(mainWindow);
|
||||
var bounds = transform.TransformBounds(new Rect(0, 0, element.ActualWidth, element.ActualHeight));
|
||||
var windowRect = new Rect(0, 0, mainWindow.ActualWidth, mainWindow.ActualHeight);
|
||||
return windowRect.IntersectsWith(bounds);
|
||||
} catch (InvalidOperationException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查 TextBlock 是否因 TextTrimming 属性导致文本被截断。
|
||||
/// </summary>
|
||||
/// <param name="textBlock">要检查的 TextBlock。</param>
|
||||
/// <returns>如果文本被截断,则返回 true;否则返回 false。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="textBlock"/> 为 null 时抛出。</exception>
|
||||
public static bool IsTextTrimmed(this TextBlock textBlock) {
|
||||
if (textBlock.TextTrimming == TextTrimming.None) return false;
|
||||
|
||||
try {
|
||||
var formattedText = new FormattedText(
|
||||
textBlock.Text,
|
||||
System.Globalization.CultureInfo.CurrentCulture,
|
||||
textBlock.FlowDirection,
|
||||
new Typeface(textBlock.FontFamily, textBlock.FontStyle, textBlock.FontWeight, textBlock.FontStretch),
|
||||
textBlock.FontSize,
|
||||
textBlock.Foreground,
|
||||
VisualTreeHelper.GetDpi(textBlock).PixelsPerDip
|
||||
);
|
||||
|
||||
return formattedText.Width > textBlock.ActualWidth;
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// 帮助动画帧的工具类。
|
||||
/// </summary>
|
||||
public static class FrameUtils
|
||||
{
|
||||
public static long Frequency { get; }
|
||||
|
||||
static FrameUtils()
|
||||
{
|
||||
Frequency = Stopwatch.Frequency;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取现在的时间戳。
|
||||
/// </summary>
|
||||
/// <returns>时间戳。</returns>
|
||||
public static long NowStamp() => Stopwatch.GetTimestamp();
|
||||
|
||||
/// <summary>
|
||||
/// 将时间戳转换为帧索引。
|
||||
/// </summary>
|
||||
/// <param name="startStamp">开始时的时间戳。</param>
|
||||
/// <param name="fps">帧率。</param>
|
||||
/// <returns>帧索引。</returns>
|
||||
public static long StampToFrameIndex(long startStamp, int fps)
|
||||
{
|
||||
// 计算经过的时间戳
|
||||
var durationStamp = NowStamp() - startStamp;
|
||||
if (durationStamp <= 0) return 0;
|
||||
|
||||
// 计算帧索引
|
||||
var index = durationStamp * fps / Frequency;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将时间跨度转换为帧索引。
|
||||
/// </summary>
|
||||
/// <param name="startTime">开始时的时间。</param>
|
||||
/// <param name="currentTime">当前时间。</param>
|
||||
/// <param name="fps">帧率。</param>
|
||||
/// <returns>帧索引。</returns>
|
||||
public static long TimeSpanToFrameIndex(TimeSpan startTime, TimeSpan currentTime, int fps)
|
||||
{
|
||||
// 如果当前时间早于开始时间,则返回0
|
||||
if (currentTime < startTime) return 0;
|
||||
|
||||
// 计算经过的时间
|
||||
var duration = currentTime - startTime;
|
||||
// 计算帧索引
|
||||
var index = (long)(duration.TotalSeconds * fps);
|
||||
|
||||
return index;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class HashCache
|
||||
{
|
||||
private readonly string _dbPath;
|
||||
|
||||
public HashCache(string dbPath)
|
||||
{
|
||||
_dbPath = dbPath ?? throw new ArgumentNullException(nameof(dbPath));
|
||||
var dir = Path.GetDirectoryName(Path.GetFullPath(_dbPath));
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
_Initialize();
|
||||
}
|
||||
|
||||
private void _Initialize()
|
||||
{
|
||||
using var connection = _CreateConnection();
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = """
|
||||
CREATE TABLE IF NOT EXISTS HashCache (
|
||||
FilePath TEXT NOT NULL PRIMARY KEY,
|
||||
FileSize INTEGER NOT NULL,
|
||||
LastWriteTime TEXT NOT NULL,
|
||||
MD5 TEXT NULL,
|
||||
SHA1 TEXT NULL,
|
||||
SHA256 TEXT NULL,
|
||||
SHA512 TEXT NULL,
|
||||
MurmurHash2 TEXT NULL
|
||||
)
|
||||
""";
|
||||
cmd.ExecuteNonQuery();
|
||||
|
||||
using var setCmd = connection.CreateCommand();
|
||||
setCmd.CommandText = "PRAGMA journal_mode=WAL";
|
||||
setCmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private SqliteConnection _CreateConnection()
|
||||
{
|
||||
var connection = new SqliteConnection($"Data Source={_dbPath};Pooling=True");
|
||||
connection.Open();
|
||||
return connection;
|
||||
}
|
||||
|
||||
public Task<string> GetMD5Async(string filePath) =>
|
||||
_GetHashWithPending(filePath, MD5Provider.Instance, "MD5");
|
||||
|
||||
public Task<string> GetSHA1Async(string filePath) =>
|
||||
_GetHashWithPending(filePath, SHA1Provider.Instance, "SHA1");
|
||||
|
||||
public Task<string> GetSHA256Async(string filePath) =>
|
||||
_GetHashWithPending(filePath, SHA256Provider.Instance, "SHA256");
|
||||
|
||||
public Task<string> GetSHA512Async(string filePath) =>
|
||||
_GetHashWithPending(filePath, SHA512Provider.Instance, "SHA512");
|
||||
|
||||
public Task<string> GetMurmurHash2Async(string filePath) =>
|
||||
_GetHashWithPending(filePath, MurmurHash2Provider.Instance, "MurmurHash2");
|
||||
|
||||
private static readonly ConcurrentDictionary<string, Task<string>> _FileHashComputePending = new();
|
||||
|
||||
public async Task<string> GetHashAsync(string filePath, IHashProvider provider)
|
||||
{
|
||||
var algoName = provider switch
|
||||
{
|
||||
MD5Provider => "MD5",
|
||||
SHA1Provider => "SHA1",
|
||||
SHA256Provider => "SHA256",
|
||||
SHA512Provider => "SHA512",
|
||||
MurmurHash2Provider => "MurmurHash2",
|
||||
_ => throw new ArgumentException($"不支持的哈希算法: {provider.GetType().Name}")
|
||||
};
|
||||
var computeKey = $"{filePath}:{algoName}";
|
||||
return await _GetHashWithPending(filePath, provider, algoName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task<string> _GetHashWithPending(string filePath, IHashProvider provider, string algoName)
|
||||
{
|
||||
var computeKey = $"{filePath}:{algoName}";
|
||||
return _FileHashComputePending.GetOrAdd(computeKey, key =>
|
||||
{
|
||||
var computeTask = _GetHashAsync(filePath, provider, algoName);
|
||||
_ = computeTask.ContinueWith(t =>
|
||||
{
|
||||
_FileHashComputePending.TryRemove(computeKey, out _);
|
||||
}, TaskContinuationOptions.ExecuteSynchronously);
|
||||
return computeTask;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<string> _GetHashAsync(string filePath, IHashProvider provider, string algoName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
|
||||
var fullPath = Path.GetFullPath(filePath);
|
||||
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(fullPath);
|
||||
var fileSize = fileInfo.Length;
|
||||
var lastWrite = fileInfo.LastWriteTimeUtc.ToString("O");
|
||||
|
||||
var cached = await _FindCacheEntryAsync(fullPath).ConfigureAwait(false);
|
||||
|
||||
if (cached != null)
|
||||
{
|
||||
if (cached.FileSize == fileSize && cached.LastWriteTime == lastWrite)
|
||||
{
|
||||
var hash = _GetHashFromEntry(cached, algoName);
|
||||
if (hash != null)
|
||||
return hash;
|
||||
|
||||
var computedHash = await _ComputeHashAsync(fullPath, provider).ConfigureAwait(false);
|
||||
await _InsertOrUpdateHashAsync(fullPath, fileSize, lastWrite, algoName, computedHash).ConfigureAwait(false);
|
||||
return computedHash;
|
||||
}
|
||||
else
|
||||
{
|
||||
await _DeleteCacheEntryAsync(fullPath).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
var computed = await _ComputeHashAsync(fullPath, provider).ConfigureAwait(false);
|
||||
await _InsertOrUpdateHashAsync(fullPath, fileSize, lastWrite, algoName, computed).ConfigureAwait(false);
|
||||
return computed;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
await _DeleteCacheEntryAsync(fullPath).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> _ComputeHashAsync(string fullPath, IHashProvider provider)
|
||||
{
|
||||
using FileStream fs = new(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
return (await provider.ComputeHashAsync(fs).ConfigureAwait(false)).ToHexString();
|
||||
}
|
||||
|
||||
private async Task<CacheEntry?> _FindCacheEntryAsync(string fullPath)
|
||||
{
|
||||
using var conn = _CreateConnection();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT FilePath, FileSize, LastWriteTime, MD5, SHA1, SHA256, SHA512, MurmurHash2 FROM HashCache WHERE FilePath = @FilePath";
|
||||
cmd.Parameters.AddWithValue("@FilePath", fullPath);
|
||||
|
||||
using var reader = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync().ConfigureAwait(false))
|
||||
return null;
|
||||
|
||||
return new CacheEntry
|
||||
{
|
||||
FilePath = reader.GetString(0),
|
||||
FileSize = reader.GetInt64(1),
|
||||
LastWriteTime = reader.GetString(2),
|
||||
MD5 = reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||
SHA1 = reader.IsDBNull(4) ? null : reader.GetString(4),
|
||||
SHA256 = reader.IsDBNull(5) ? null : reader.GetString(5),
|
||||
SHA512 = reader.IsDBNull(6) ? null : reader.GetString(6),
|
||||
MurmurHash2 = reader.IsDBNull(7) ? null : reader.GetString(7)
|
||||
};
|
||||
}
|
||||
|
||||
private static string? _GetHashFromEntry(CacheEntry entry, string algoName) => algoName switch
|
||||
{
|
||||
"MD5" => entry.MD5,
|
||||
"SHA1" => entry.SHA1,
|
||||
"SHA256" => entry.SHA256,
|
||||
"SHA512" => entry.SHA512,
|
||||
"MurmurHash2" => entry.MurmurHash2,
|
||||
_ => null
|
||||
};
|
||||
|
||||
private async Task _InsertOrUpdateHashAsync(string fullPath, long fileSize, string lastWrite, string algoName, string hash)
|
||||
{
|
||||
if (hash.IsNullOrWhiteSpace()) return;
|
||||
using var conn = _CreateConnection();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = """
|
||||
INSERT INTO HashCache (FilePath, FileSize, LastWriteTime, MD5, SHA1, SHA256, SHA512, MurmurHash2)
|
||||
VALUES (@FilePath, @FileSize, @LastWriteTime, @MD5, @SHA1, @SHA256, @SHA512, @MurmurHash2)
|
||||
ON CONFLICT(FilePath) DO UPDATE SET
|
||||
FileSize = excluded.FileSize,
|
||||
LastWriteTime = excluded.LastWriteTime,
|
||||
MD5 = COALESCE(excluded.MD5, HashCache.MD5),
|
||||
SHA1 = COALESCE(excluded.SHA1, HashCache.SHA1),
|
||||
SHA256 = COALESCE(excluded.SHA256, HashCache.SHA256),
|
||||
SHA512 = COALESCE(excluded.SHA512, HashCache.SHA512),
|
||||
MurmurHash2 = COALESCE(excluded.MurmurHash2, HashCache.MurmurHash2)
|
||||
""";
|
||||
cmd.Parameters.AddWithValue("@FilePath", fullPath);
|
||||
cmd.Parameters.AddWithValue("@FileSize", fileSize);
|
||||
cmd.Parameters.AddWithValue("@LastWriteTime", lastWrite);
|
||||
cmd.Parameters.AddWithValue("@MD5", algoName == "MD5" ? hash : DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@SHA1", algoName == "SHA1" ? hash : DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@SHA256", algoName == "SHA256" ? hash : DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@SHA512", algoName == "SHA512" ? hash : DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@MurmurHash2", algoName == "MurmurHash2" ? hash : DBNull.Value);
|
||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task _DeleteCacheEntryAsync(string fullPath)
|
||||
{
|
||||
using var conn = _CreateConnection();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "DELETE FROM HashCache WHERE FilePath = @FilePath";
|
||||
cmd.Parameters.AddWithValue("@FilePath", fullPath);
|
||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private sealed class CacheEntry
|
||||
{
|
||||
public string FilePath { get; init; } = "";
|
||||
public long FileSize { get; init; }
|
||||
public string LastWriteTime { get; init; } = "";
|
||||
public string? MD5 { get; init; }
|
||||
public string? SHA1 { get; init; }
|
||||
public string? SHA256 { get; init; }
|
||||
public string? SHA512 { get; init; }
|
||||
public string? MurmurHash2 { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public interface IHashProvider
|
||||
{
|
||||
ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default);
|
||||
byte[] ComputeHash(Stream input);
|
||||
byte[] ComputeHash(ReadOnlySpan<byte> input);
|
||||
byte[] ComputeHash(string input, Encoding? en = null);
|
||||
int Length { get; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class MD5Provider : IHashProvider {
|
||||
public static MD5Provider Instance { get; } = new();
|
||||
public int Length => 32;
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> input)
|
||||
{
|
||||
return MD5.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string input, Encoding? en = null)
|
||||
{
|
||||
return ComputeHash(en is null ? Encoding.UTF8.GetBytes(input) : en.GetBytes(input));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(Stream input)
|
||||
{
|
||||
return MD5.HashData(input);
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await MD5.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class MurmurHash2Provider : IHashProvider
|
||||
{
|
||||
public static MurmurHash2Provider Instance { get; } = new();
|
||||
public int Length => 8;
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var filtered = new List<byte>(data.Length);
|
||||
foreach (var b in data)
|
||||
if (b != 9 && b != 10 && b != 13 && b != 32)
|
||||
filtered.Add(b);
|
||||
return _ComputeHash(CollectionsMarshal.AsSpan(filtered));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string input, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
return ComputeHash(encoding.GetBytes(input));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(Stream input)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
input.CopyTo(ms);
|
||||
return ComputeHash(ms.GetBuffer().AsSpan(0, (int)ms.Length));
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await input.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||
return ComputeHash(ms.GetBuffer().AsSpan(0, (int)ms.Length));
|
||||
}
|
||||
|
||||
private static byte[] _ComputeHash(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var h = (uint)(1 ^ data.Length);
|
||||
var i = 0;
|
||||
var loopTo = data.Length - 4;
|
||||
|
||||
for (; i <= loopTo; i += 4)
|
||||
{
|
||||
var k = data[i]
|
||||
| ((uint)data[i + 1] << 8)
|
||||
| ((uint)data[i + 2] << 16)
|
||||
| ((uint)data[i + 3] << 24);
|
||||
k *= 0x5BD1E995;
|
||||
k ^= k >> 24;
|
||||
k *= 0x5BD1E995;
|
||||
h *= 0x5BD1E995;
|
||||
h ^= k;
|
||||
}
|
||||
|
||||
switch (data.Length - i)
|
||||
{
|
||||
case 3:
|
||||
h ^= (uint)(data[i] | ((uint)data[i + 1] << 8));
|
||||
h ^= (uint)data[i + 2] << 16;
|
||||
h *= 0x5BD1E995;
|
||||
break;
|
||||
case 2:
|
||||
h ^= (uint)(data[i] | ((uint)data[i + 1] << 8));
|
||||
h *= 0x5BD1E995;
|
||||
break;
|
||||
case 1:
|
||||
h ^= data[i];
|
||||
h *= 0x5BD1E995;
|
||||
break;
|
||||
}
|
||||
|
||||
h ^= h >> 13;
|
||||
h *= 0x5BD1E995;
|
||||
h ^= h >> 15;
|
||||
|
||||
return BitConverter.GetBytes(h);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class SHA1Provider : IHashProvider
|
||||
{
|
||||
public static SHA1Provider Instance { get; } = new();
|
||||
public int Length => 40;
|
||||
|
||||
public byte[] ComputeHash(byte[] input)
|
||||
{
|
||||
return SHA1.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> input)
|
||||
{
|
||||
return SHA1.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string input, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
return ComputeHash(encoding.GetBytes(input));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(Stream input)
|
||||
{
|
||||
return SHA1.HashData(input);
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await SHA1.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class SHA256Provider : IHashProvider
|
||||
{
|
||||
public static SHA256Provider Instance { get; } = new();
|
||||
public int Length => 64;
|
||||
|
||||
public byte[] ComputeHash(byte[] input)
|
||||
{
|
||||
return SHA256.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> input)
|
||||
{
|
||||
return SHA256.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string input, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
return ComputeHash(encoding.GetBytes(input));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(Stream input)
|
||||
{
|
||||
return SHA256.HashData(input);
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await SHA256.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class SHA512Provider : IHashProvider
|
||||
{
|
||||
public static SHA512Provider Instance { get; } = new();
|
||||
public int Length => 128;
|
||||
|
||||
public byte[] ComputeHash(byte[] input)
|
||||
{
|
||||
return SHA512.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> input)
|
||||
{
|
||||
return SHA512.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string input, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
return ComputeHash(encoding.GetBytes(input));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(Stream input)
|
||||
{
|
||||
return SHA512.HashData(input);
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await SHA512.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using PCL.Core.App;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
public static class IconHelper
|
||||
{
|
||||
public static string GetIconPath()
|
||||
{
|
||||
var paths = Path.Combine(Paths.Temp, "icon.png");
|
||||
if (!File.Exists(paths))
|
||||
{
|
||||
CreateIcon();
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
private static void CreateIcon()
|
||||
{
|
||||
using var icon = Icon.ExtractAssociatedIcon(Basics.ExecutablePath) ??
|
||||
throw new InvalidOperationException("无法提取程序图标。");
|
||||
using var bitmap = icon.ToBitmap();
|
||||
bitmap.Save(Path.Combine(Paths.Temp, "icon.png"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// System.Text.Json 兼容 Newtonsoft.Json 宽松行为的统一入口。
|
||||
/// </summary>
|
||||
public static class JsonCompat
|
||||
{
|
||||
public static readonly JsonNodeOptions NodeOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public static readonly JsonDocumentOptions DocumentOptions = new()
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 统一的宽松 JSON 序列化配置。该实例在静态初始化时已被冻结,调用方不能修改全局行为。
|
||||
/// 如需追加调用点专用设置,请使用 <c>new JsonSerializerOptions(JsonCompat.SerializerOptions)</c> 克隆后修改。
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions SerializerOptions { get; } = _CreateSerializerOptions();
|
||||
|
||||
private static JsonSerializerOptions _CreateSerializerOptions()
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters =
|
||||
{
|
||||
new FlexibleDateTimeConverter(),
|
||||
new FlexibleBoolConverter(),
|
||||
new FlexibleStringConverter(),
|
||||
new JsonStringEnumConverter()
|
||||
}
|
||||
};
|
||||
|
||||
options.MakeReadOnly(true);
|
||||
return options;
|
||||
}
|
||||
|
||||
public static JsonNode ParseNode(string text)
|
||||
{
|
||||
return JsonNode.Parse(text, NodeOptions, DocumentOptions)!;
|
||||
}
|
||||
|
||||
public static T? ToObject<T>(this JsonNode? node)
|
||||
{
|
||||
return node is null ? default : node.Deserialize<T>(SerializerOptions);
|
||||
}
|
||||
|
||||
public static JsonArray FromObject<T>(IEnumerable<T> items)
|
||||
{
|
||||
var arr = new JsonArray();
|
||||
foreach (var item in items)
|
||||
arr.Add(JsonSerializer.SerializeToNode(item, SerializerOptions));
|
||||
return arr;
|
||||
}
|
||||
|
||||
public static bool TryGetDateTime(JsonNode? node, out DateTime dateTime)
|
||||
{
|
||||
dateTime = default;
|
||||
switch (node)
|
||||
{
|
||||
case null:
|
||||
return false;
|
||||
case JsonValue value when value.TryGetValue<DateTime>(out var rawDateTime):
|
||||
dateTime = NormalizeDateTime(rawDateTime);
|
||||
return true;
|
||||
case JsonValue value
|
||||
when value.TryGetValue<string>(out var rawText) && TryParseDateTime(rawText, out dateTime):
|
||||
return true;
|
||||
default:
|
||||
try
|
||||
{
|
||||
dateTime = NormalizeDateTime(node.Deserialize<DateTime>(SerializerOptions));
|
||||
return true;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryParseDateTime(string? value, out DateTime dateTime)
|
||||
{
|
||||
dateTime = default;
|
||||
if (string.IsNullOrWhiteSpace(value)) return false;
|
||||
|
||||
// ISO 8601 允许用 24:00:00 表示当日终点(语义等于次日零点),但 .NET 的日期解析器不接受小时为 24,
|
||||
// 需先把小时 24 归一化为 00,再在解析成功后补一天。例如社区版本清单中的 2009-10-24T24:00:00+00:00。
|
||||
var addDay = false;
|
||||
var endOfDay = RegexPatterns.Iso8601EndOfDay.Match(value);
|
||||
if (endOfDay.Success)
|
||||
{
|
||||
value = value.Remove(endOfDay.Index + 1, 2).Insert(endOfDay.Index + 1, "00");
|
||||
addDay = true;
|
||||
}
|
||||
|
||||
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind,
|
||||
out var dateTimeOffset))
|
||||
{
|
||||
if (addDay) dateTimeOffset = dateTimeOffset.AddDays(1);
|
||||
dateTime = dateTimeOffset.LocalDateTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var parsed))
|
||||
return false;
|
||||
|
||||
dateTime = NormalizeDateTime(addDay ? parsed.AddDays(1) : parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static DateTime NormalizeDateTime(DateTime dateTime)
|
||||
{
|
||||
return dateTime.Kind == DateTimeKind.Utc ? dateTime.ToLocalTime() : dateTime;
|
||||
}
|
||||
|
||||
public static void Merge(this JsonObject target, JsonNode? source)
|
||||
{
|
||||
if (source is not JsonObject sourceObj) return;
|
||||
|
||||
foreach (var prop in sourceObj.ToArray())
|
||||
switch (target[prop.Key])
|
||||
{
|
||||
case JsonObject targetChild when
|
||||
prop.Value is JsonObject sourceChild:
|
||||
targetChild.Merge(sourceChild);
|
||||
break;
|
||||
case JsonArray targetArray when
|
||||
prop.Value is JsonArray sourceArray:
|
||||
targetArray.Merge(sourceArray);
|
||||
break;
|
||||
default:
|
||||
target[prop.Key] = prop.Value?.DeepClone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Merge(this JsonArray target, JsonNode? source)
|
||||
{
|
||||
if (source is not JsonArray sourceArr) return;
|
||||
foreach (var item in sourceArr)
|
||||
target.Add(item?.DeepClone());
|
||||
}
|
||||
|
||||
private sealed class FlexibleDateTimeConverter : JsonConverter<DateTime>
|
||||
{
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.String)
|
||||
return NormalizeDateTime(reader.GetDateTime());
|
||||
|
||||
var value = reader.GetString();
|
||||
return TryParseDateTime(value, out var dateTime)
|
||||
? dateTime
|
||||
: NormalizeDateTime(reader.GetDateTime());
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FlexibleBoolConverter : JsonConverter<bool>
|
||||
{
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.String when bool.TryParse(reader.GetString(), out var value) => value,
|
||||
JsonTokenType.String when int.TryParse(reader.GetString(), NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture, out var number) => number != 0,
|
||||
JsonTokenType.Number when reader.TryGetInt64(out var number) => number != 0,
|
||||
_ => throw new JsonException($"Can not convert JSON token {reader.TokenType} to Boolean.")
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteBooleanValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FlexibleStringConverter : JsonConverter<string>
|
||||
{
|
||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.Null => null,
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => _ReadRawJsonValue(ref reader),
|
||||
JsonTokenType.True => bool.TrueString,
|
||||
JsonTokenType.False => bool.FalseString,
|
||||
_ => _ReadRawJsonValue(ref reader)
|
||||
};
|
||||
}
|
||||
|
||||
private static string _ReadRawJsonValue(ref Utf8JsonReader reader)
|
||||
{
|
||||
using var document = JsonDocument.ParseValue(ref reader);
|
||||
return document.RootElement.GetRawText();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static class AumidHelper
|
||||
{
|
||||
public const string Aumid = "PCLCommunity.PCLCE";
|
||||
|
||||
public static bool HasAumid()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(string.Concat(@"Software\Classes\AppUserModelId\", Aumid));
|
||||
return key is not null;
|
||||
}
|
||||
|
||||
public static void RegisterAumid()
|
||||
{
|
||||
// .NET 8 在正常情况下不可能返回 null,如果炸了不应该包住而是让他炸下去
|
||||
using var key = Registry.CurrentUser.CreateSubKey(string.Concat(@"Software\Classes\AppUserModelId\", Aumid));
|
||||
key.SetValue("DisplayName", "Plain Craft Launcher Community Edition");
|
||||
key.SetValue("IconUri", IconHelper.GetIconPath());
|
||||
key.SetValue("IconBackgroundColor", "FFDDDD");
|
||||
}
|
||||
|
||||
public static void UnregisterAumid()
|
||||
{
|
||||
Registry.CurrentUser.DeleteSubKey(string.Concat(@"Software\Classes\AppUserModelId\", Aumid), false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
public static class ClipboardUtils {
|
||||
/// <summary>
|
||||
/// 将剪贴板内容设置为用于复制/粘贴操作的文件或文件夹路径列表。
|
||||
/// </summary>
|
||||
/// <param name="paths">要设置到剪贴板的文件或文件夹路径数组。</param>
|
||||
public static void SetClipboardFiles(string[] paths) {
|
||||
if (paths is null || paths.Length == 0) {
|
||||
throw new ArgumentException("Paths cannot be null or empty.", nameof(paths));
|
||||
}
|
||||
|
||||
var dataObject = new DataObject();
|
||||
dataObject.SetData(DataFormats.FileDrop, paths);
|
||||
Clipboard.SetDataObject(dataObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
public partial class DragHelper
|
||||
{
|
||||
public event EventHandler? DragDrop;
|
||||
|
||||
public string[]? DropFilePaths { get; private set; }
|
||||
public Point DropDragPoint { get; private set; }
|
||||
|
||||
public HwndSource? HwndSource { get; set; }
|
||||
|
||||
#region Public API
|
||||
|
||||
public void AddHook()
|
||||
{
|
||||
if (HwndSource is null)
|
||||
throw new InvalidOperationException("HwndSource 未设置");
|
||||
|
||||
RemoveHook();
|
||||
|
||||
HwndSource.AddHook(WndProc);
|
||||
IntPtr hwnd = HwndSource.Handle;
|
||||
|
||||
if (IsUserAnAdmin())
|
||||
RevokeDragDrop(hwnd);
|
||||
|
||||
DragAcceptFiles(hwnd, true);
|
||||
ChangeMessageFilter(hwnd);
|
||||
}
|
||||
|
||||
public void RemoveHook()
|
||||
{
|
||||
if (HwndSource is null)
|
||||
return;
|
||||
|
||||
HwndSource.RemoveHook(WndProc);
|
||||
DragAcceptFiles(HwndSource.Handle, false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WndProc
|
||||
|
||||
private IntPtr WndProc(
|
||||
IntPtr hwnd,
|
||||
int msg,
|
||||
IntPtr wParam,
|
||||
IntPtr lParam,
|
||||
ref bool handled)
|
||||
{
|
||||
if (TryGetDropInfo(msg, wParam, out var files, out var pt))
|
||||
{
|
||||
DropFilePaths = files;
|
||||
DropDragPoint = new Point(pt.X, pt.Y);
|
||||
DragDrop?.Invoke(this, EventArgs.Empty);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Message filter (UAC)
|
||||
|
||||
private static unsafe void ChangeMessageFilter(IntPtr hwnd)
|
||||
{
|
||||
var ver = Environment.OSVersion.Version;
|
||||
if (ver < new Version(6, 0))
|
||||
return;
|
||||
|
||||
var win7OrHigher = ver >= new Version(6, 1);
|
||||
|
||||
var filter = new CHANGEFILTERSTRUCT
|
||||
{
|
||||
cbSize = (uint)sizeof(CHANGEFILTERSTRUCT)
|
||||
};
|
||||
|
||||
uint[] messages = [
|
||||
WM_DROPFILES,
|
||||
WM_COPYGLOBALDATA,
|
||||
WM_COPYDATA
|
||||
];
|
||||
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
var ok = win7OrHigher
|
||||
? ChangeWindowMessageFilterEx(hwnd, msg, MSGFLT_ALLOW, ref filter)
|
||||
: ChangeWindowMessageFilter(msg, MSGFLT_ADD);
|
||||
|
||||
if (!ok) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Drop parsing
|
||||
|
||||
private static bool TryGetDropInfo(
|
||||
int msg,
|
||||
IntPtr hDrop,
|
||||
out string[]? filePaths,
|
||||
out DragPoint dropPoint)
|
||||
{
|
||||
filePaths = null;
|
||||
dropPoint = default;
|
||||
|
||||
if (msg != WM_DROPFILES)
|
||||
return false;
|
||||
|
||||
var count = DragQueryFile(hDrop, uint.MaxValue, IntPtr.Zero, 0);
|
||||
filePaths = new string[count];
|
||||
|
||||
const int maxPath = 32768, smallerMaxPath = 1024;
|
||||
|
||||
Span<char> gBuffer = stackalloc char[smallerMaxPath];
|
||||
for (uint i = 0; i < count; i++)
|
||||
{
|
||||
var len = DragQueryFile(hDrop, i, IntPtr.Zero, 0) + 1;
|
||||
if (len > maxPath) len = maxPath;
|
||||
var buffer = len <= smallerMaxPath ? gBuffer[..(int)len] : new char[len];
|
||||
_ = DragQueryFile(hDrop, i, buffer, len);
|
||||
filePaths[i] = new string(buffer[..(int)(len - 1)]);
|
||||
}
|
||||
|
||||
DragFinish(hDrop);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Win32
|
||||
|
||||
private const uint WM_COPYGLOBALDATA = 0x0049;
|
||||
private const uint WM_COPYDATA = 0x004A;
|
||||
private const uint WM_DROPFILES = 0x0233;
|
||||
|
||||
private const uint MSGFLT_ALLOW = 1;
|
||||
private const uint MSGFLT_ADD = 1;
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ChangeWindowMessageFilter(
|
||||
uint msg,
|
||||
uint flags);
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ChangeWindowMessageFilterEx(
|
||||
IntPtr hwnd,
|
||||
uint msg,
|
||||
uint action,
|
||||
ref CHANGEFILTERSTRUCT filter);
|
||||
|
||||
[LibraryImport("shell32.dll")]
|
||||
private static partial void DragAcceptFiles(
|
||||
IntPtr hwnd,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool accept);
|
||||
|
||||
[LibraryImport("shell32.dll", EntryPoint = "DragQueryFileW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
private static partial uint DragQueryFile(IntPtr hDrop, uint iFile, Span<char> lpszFile, uint cch);
|
||||
|
||||
[LibraryImport("shell32.dll", EntryPoint = "DragQueryFileW")]
|
||||
private static partial uint DragQueryFile(IntPtr hDrop, uint iFile, IntPtr lpszFile, uint cch);
|
||||
|
||||
[LibraryImport("shell32.dll")]
|
||||
private static partial void DragFinish(IntPtr hDrop);
|
||||
|
||||
[LibraryImport("ole32.dll")]
|
||||
private static partial int RevokeDragDrop(IntPtr hwnd);
|
||||
|
||||
[LibraryImport("shell32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool IsUserAnAdmin();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Structs
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct DragPoint
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct CHANGEFILTERSTRUCT
|
||||
{
|
||||
public uint cbSize;
|
||||
public uint ExtStatus;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static partial class EnvironmentInterop
|
||||
{
|
||||
private const string LogModule = "Environment";
|
||||
|
||||
/// <summary>
|
||||
/// 读取环境变量并使用 <see cref="StringExtension.Convert{T}"/> 将其转换为指定类型并写入目标引用。
|
||||
/// </summary>
|
||||
/// <param name="key">环境变量名</param>
|
||||
/// <param name="target">需要写入的目标引用 (不存在该环境变量或转换失败时不会写入)</param>
|
||||
/// <param name="detailLog">是否在日志中输出变量值</param>
|
||||
/// <typeparam name="TValue">目标引用的类型</typeparam>
|
||||
/// <returns>是否成功写入目标引用</returns>
|
||||
public static bool ReadVariable<TValue>(string key, ref TValue target, bool detailLog = true)
|
||||
{
|
||||
var envValue = Environment.GetEnvironmentVariable(key);
|
||||
if (envValue is null) return false;
|
||||
var valueLog = detailLog ? $" = {envValue}" : string.Empty;
|
||||
LogWrapper.Debug(LogModule, $"读取到环境变量 {key}{valueLog}");
|
||||
var value = envValue.Convert<TValue>();
|
||||
if (value is null)
|
||||
{
|
||||
LogWrapper.Warn(LogModule, $"环境变量 {key} 类型转换失败");
|
||||
return false;
|
||||
}
|
||||
target = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string? GetSecret(string key, bool readEnv = true, bool readEnvDebugOnly = false)
|
||||
{
|
||||
if (!SecretDictionary.TryGetValue(key, out var value) &&
|
||||
readEnv &&
|
||||
#if !DEBUG
|
||||
!readEnvDebugOnly &&
|
||||
#endif
|
||||
ReadVariable($"PCL_{key}", ref value, false)
|
||||
) SecretDictionary[key] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前操作系统名称。
|
||||
/// </summary>
|
||||
/// <returns>返回小写的操作系统名称,如 "windows", "linux", "osx"。</returns>
|
||||
public static string GetCurrentOsName() {
|
||||
if (OperatingSystem.IsWindows())
|
||||
return "windows";
|
||||
if (OperatingSystem.IsLinux())
|
||||
return "linux";
|
||||
return OperatingSystem.IsMacOS()
|
||||
? "osx"
|
||||
: "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Management;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static class HardwareInfo
|
||||
{
|
||||
private static readonly object _Lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 系统 CPU 信息
|
||||
/// </summary>
|
||||
public static string CPUName = "Unknown";
|
||||
|
||||
/// <summary>
|
||||
/// 系统 GPU 信息
|
||||
/// </summary>
|
||||
public static IReadOnlyList<GPUInfo> GPUs { get; private set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 已安装物理内存大小,单位 MiB
|
||||
/// </summary>
|
||||
public static long SystemMemorySize = (long)KernelInterop.GetPhysicalMemoryBytes().Total / 1024 / 1024;
|
||||
|
||||
public readonly record struct GPUInfo(string Name, string DriverVersion, long Memory);
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统信息,例如 CPU 与 GPU,并存储到 CPUName 和 GPUs
|
||||
/// </summary>
|
||||
public static void GetHardwareInfo()
|
||||
{
|
||||
// CPU
|
||||
var cpuName = (string?)null;
|
||||
try
|
||||
{
|
||||
using var searcher = new ManagementObjectSearcher(@"root\CIMV2", "SELECT * FROM Win32_Processor");
|
||||
foreach (ManagementObject queryObj in searcher.Get())
|
||||
{
|
||||
cpuName = queryObj["Name"]?.ToString()?.Trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "获取 CPU 信息时出错");
|
||||
}
|
||||
|
||||
// GPU
|
||||
var gpuList = new List<GPUInfo>();
|
||||
try
|
||||
{
|
||||
using var searcher =
|
||||
new ManagementObjectSearcher(@"root\CIMV2", "SELECT * FROM Win32_VideoController");
|
||||
foreach (ManagementObject queryObj in searcher.Get())
|
||||
{
|
||||
var gpuInfo = new GPUInfo
|
||||
{
|
||||
Name = queryObj["Name"]?.ToString() ?? "",
|
||||
DriverVersion = queryObj["DriverVersion"]?.ToString() ?? "",
|
||||
Memory = queryObj["AdapterRAM"] is not null and not DBNull
|
||||
? Convert.ToInt64(queryObj["AdapterRAM"]) / (1024 * 1024)
|
||||
: 0
|
||||
};
|
||||
gpuList.Add(gpuInfo);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "获取 GPU 信息时出错");
|
||||
}
|
||||
|
||||
lock (_Lock)
|
||||
{
|
||||
if (cpuName is not null)
|
||||
CPUName = cpuName;
|
||||
if (gpuList.Count > 0)
|
||||
GPUs = gpuList;
|
||||
}
|
||||
LogWrapper.Info("已获取系统硬件信息");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static partial class KernelInterop
|
||||
{
|
||||
// ReSharper disable InconsistentNaming, UnusedMember.Local
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", SetLastError = true)]
|
||||
private static partial uint _GetCurrentThreadId();
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "ExitProcess", SetLastError = false)]
|
||||
private static partial void _ExitProcess(uint statusCode);
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "GetNamedPipeClientProcessId", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool _GetNamedPipeClientProcessId(IntPtr pipeHandle, out uint clientProcessId);
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "GetLogicalProcessorInformationEx", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool _GetLogicalProcessorInformationEx(
|
||||
LOGICAL_PROCESSOR_RELATIONSHIP relationshipType,
|
||||
IntPtr buffer,
|
||||
ref uint returnLength);
|
||||
|
||||
private const int ERROR_INSUFFICIENT_BUFFER = 122;
|
||||
|
||||
private enum LOGICAL_PROCESSOR_RELATIONSHIP : uint
|
||||
{
|
||||
RelationProcessorCore = 0,
|
||||
RelationNumaNode = 1,
|
||||
RelationCache = 2,
|
||||
RelationProcessorPackage = 3,
|
||||
RelationGroup = 4,
|
||||
RelationAll = 0xffff
|
||||
}
|
||||
|
||||
private static MEMORYSTATUSEX CreateStatus() => new() { dwLength = (uint)Marshal.SizeOf<MEMORYSTATUSEX>() };
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct MEMORYSTATUSEX
|
||||
{
|
||||
public uint dwLength;
|
||||
public uint dwMemoryLoad;
|
||||
public ulong ullTotalPhys;
|
||||
public ulong ullAvailPhys;
|
||||
public ulong ullTotalPageFile;
|
||||
public ulong ullAvailPageFile;
|
||||
public ulong ullTotalVirtual;
|
||||
public ulong ullAvailVirtual;
|
||||
public ulong ullAvailExtendedVirtual;
|
||||
}
|
||||
|
||||
private const int ERROR_ACCESS_DENIED = 5;
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "AllocConsole")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool _AllocConsole();
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "FreeConsole")]
|
||||
private static partial void _FreeConsole();
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "GetConsoleWindow")]
|
||||
private static partial nint _GetConsoleWindow();
|
||||
|
||||
// ReSharper restore InconsistentNaming, UnusedMember.Local
|
||||
|
||||
private static void _ThrowLastWin32Error(int? errorCode = null) => throw new Win32Exception(errorCode ?? Marshal.GetLastWin32Error());
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前线程的 Win32 Thread ID。若无特殊情况请用 <see cref="Thread.ManagedThreadId"/> 而不是这个方法。
|
||||
/// </summary>
|
||||
public static uint CurrentNativeThreadId => _GetCurrentThreadId();
|
||||
|
||||
/// <summary>
|
||||
/// 直接结束当前进程。若无特殊情况请使用 <see cref="PCL.Core.App.IoC.Lifecycle.Shutdown"/>
|
||||
/// </summary>
|
||||
/// <param name="statusCode">退出状态码 (返回值)</param>
|
||||
public static void ExitProcess(int statusCode = 0) => _ExitProcess((uint)statusCode);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定命名管道当前连接的客户端进程 ID
|
||||
/// </summary>
|
||||
/// <param name="pipeHandle">命名管道句柄</param>
|
||||
/// <returns>获取到的进程 ID</returns>
|
||||
public static uint GetNamedPipeClientProcessId(IntPtr pipeHandle)
|
||||
{
|
||||
if (!_GetNamedPipeClientProcessId(pipeHandle, out var clientProcessId)) _ThrowLastWin32Error();
|
||||
return clientProcessId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取仅包含性能核(P-core)的逻辑处理器数量。
|
||||
/// 在不支持 EfficiencyClass(旧 OS 或非混合架构)时,会退回到 Environment.ProcessorCount。
|
||||
/// </summary>
|
||||
public static int GetPerformanceLogicalProcessorCount()
|
||||
{
|
||||
var cores = QueryProcessorCoreRelationships();
|
||||
if (cores.Count == 0)
|
||||
{
|
||||
// 不支持 EfficiencyClass
|
||||
return Environment.ProcessorCount;
|
||||
}
|
||||
|
||||
// 原理:性能核的 EfficiencyClass 一定比能效核大
|
||||
var maxEff = cores.Max(c => c.EfficiencyClass);
|
||||
|
||||
// 统计所有效率等级为 maxEff 的核心的掩码位数
|
||||
return cores
|
||||
.Where(c => c.EfficiencyClass == maxEff)
|
||||
.Sum(c => CountSetBits(c.Mask));
|
||||
|
||||
static int CountSetBits(ulong v)
|
||||
{
|
||||
var cnt = 0;
|
||||
while (v != 0)
|
||||
{
|
||||
cnt += (int)(v & 1);
|
||||
v >>= 1;
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅承载 EfficiencyClass 和 Mask 的简单 CPU 核心信息
|
||||
/// </summary>
|
||||
public sealed record ProcessorCore(byte EfficiencyClass, ulong Mask);
|
||||
|
||||
/// <summary>
|
||||
/// 枚举 RelationProcessorCore 返回的所有物理核心关系信息
|
||||
/// </summary>
|
||||
// Partly generated by o4-mini-high (20250709)
|
||||
public static List<ProcessorCore> QueryProcessorCoreRelationships()
|
||||
{
|
||||
uint returnedLength = 0;
|
||||
|
||||
// 第一次调用仅为了获取所需缓冲区大小
|
||||
if (!_GetLogicalProcessorInformationEx(
|
||||
LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore,
|
||||
IntPtr.Zero,
|
||||
ref returnedLength)
|
||||
&& Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
var list = new List<ProcessorCore>();
|
||||
var buffer = Marshal.AllocHGlobal((int)returnedLength);
|
||||
try
|
||||
{
|
||||
if (!_GetLogicalProcessorInformationEx(
|
||||
LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore,
|
||||
buffer,
|
||||
ref returnedLength))
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
var ptr = buffer;
|
||||
var end = IntPtr.Add(buffer, (int)returnedLength);
|
||||
|
||||
// SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX 头部:Relationship (4 字节) + Size (4 字节)
|
||||
const int headerSize = sizeof(uint) + sizeof(uint);
|
||||
// GROUP_AFFINITY 大小 = KAFFINITY (平台指针大小) + WORD Group + WORD[3] Reserved
|
||||
var groupAffinitySize = IntPtr.Size + 8;
|
||||
|
||||
while (ptr.ToInt64() < end.ToInt64())
|
||||
{
|
||||
var relationship = (uint)Marshal.ReadInt32(ptr);
|
||||
var size = (uint)Marshal.ReadInt32(ptr, sizeof(uint));
|
||||
|
||||
if (relationship == (uint)LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore)
|
||||
{
|
||||
// PROCESSOR_RELATIONSHIP 结构:
|
||||
// Flags BYTE @ offset 8
|
||||
// EfficiencyClass BYTE @ offset 9
|
||||
// Reserved[20] BYTE[20]
|
||||
// GroupCount WORD @ offset 30
|
||||
// GroupMask[ANYSIZE] GROUP_AFFINITY 从 offset 32 开始
|
||||
|
||||
var efficiencyClass = Marshal.ReadByte(ptr, headerSize + 1);
|
||||
var groupCount = (ushort)Marshal.ReadInt16(ptr, headerSize + 2 + 20);
|
||||
var maskBase = IntPtr.Add(ptr, headerSize + 2 + 20 + sizeof(ushort));
|
||||
|
||||
for (var i = 0; i < groupCount; i++)
|
||||
{
|
||||
var affinityPtr = IntPtr.Add(maskBase, i * groupAffinitySize);
|
||||
// 只读取 Mask 部分,统计位数
|
||||
var mask = (IntPtr.Size == 8 ? (ulong)Marshal.ReadInt64(affinityPtr) : (uint)Marshal.ReadInt32(affinityPtr));
|
||||
list.Add(new ProcessorCore(efficiencyClass, mask));
|
||||
}
|
||||
}
|
||||
|
||||
// 移动到下一个记录
|
||||
ptr = IntPtr.Add(ptr, (int)size);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统可用物理内存 (<c>ullAvailPhys</c>) 的字节数
|
||||
/// </summary>
|
||||
public static ulong GetAvailablePhysicalMemoryBytes()
|
||||
{
|
||||
var status = CreateStatus();
|
||||
if (!GlobalMemoryStatusEx(ref status)) _ThrowLastWin32Error();
|
||||
return status.ullAvailPhys;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统可用物理内存 (<c>ullAvailPhys</c>) 和总物理内存 (<c>ullTotalPhys</c>) 的字节数
|
||||
/// </summary>
|
||||
public static (ulong Total, ulong Available) GetPhysicalMemoryBytes()
|
||||
{
|
||||
var status = CreateStatus();
|
||||
if (!GlobalMemoryStatusEx(ref status)) _ThrowLastWin32Error();
|
||||
return (status.ullTotalPhys, status.ullAvailPhys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取以百分比表示的系统内存占用 (范围 0.0 ~ 100.0)
|
||||
/// </summary>
|
||||
public static double GetMemoryLoadPercent()
|
||||
{
|
||||
var status = CreateStatus();
|
||||
if (!GlobalMemoryStatusEx(ref status)) _ThrowLastWin32Error();
|
||||
return status.dwMemoryLoad;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为当前进程新建终端窗口。<br/>
|
||||
/// 若进程已拥有终端窗口,该方法将无任何作用。若有需要,可在调用前使用
|
||||
/// <see cref="GetConsoleWindow"/> 来确认进程是否存在关联的终端窗口。
|
||||
/// </summary>
|
||||
public static void AllocateConsole()
|
||||
{
|
||||
if (_AllocConsole()) return;
|
||||
var lastError = Marshal.GetLastWin32Error();
|
||||
if (lastError != ERROR_ACCESS_DENIED) _ThrowLastWin32Error(lastError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前进程的终端窗口。<br/>
|
||||
/// 若进程不存在关联的终端窗口,该方法将无任何作用。
|
||||
/// </summary>
|
||||
public static void FreeConsole() => _FreeConsole();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前进程关联的终端窗口句柄。
|
||||
/// </summary>
|
||||
/// <returns>代表终端窗口的 HWND,若当前进程无关联的终端窗口,则该值为 <see cref="nint.Zero"/></returns>
|
||||
public static nint GetConsoleWindow() => _GetConsoleWindow();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static class NetworkUtils
|
||||
{
|
||||
private static readonly IPAddress[] _LocalIpAddresses = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(x => x is { OperationalStatus: OperationalStatus.Up })
|
||||
.SelectMany(x => x.GetIPProperties().UnicastAddresses)
|
||||
.Where(ua => ua.Address is { AddressFamily: AddressFamily.InterNetwork or AddressFamily.InterNetworkV6 } &&
|
||||
!ua.Address.Equals(IPAddress.Any) &&
|
||||
!ua.Address.Equals(IPAddress.IPv6Any) &&
|
||||
!ua.Address.Equals(IPAddress.Loopback) &&
|
||||
!ua.Address.Equals(IPAddress.IPv6Loopback))
|
||||
.Select(ua => ua.Address)
|
||||
.ToArray();
|
||||
|
||||
public static IPAddress[] GetAllLocalAddress() => _LocalIpAddresses;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static partial class NtInterop
|
||||
{
|
||||
[LibraryImport("ntdll.dll")]
|
||||
private static partial void RtlGetNtVersionNumbers(
|
||||
out int major,
|
||||
out int minor,
|
||||
out int build);
|
||||
|
||||
private static void _ThrowLastWin32Error(int? errorCode = null) => throw new Win32Exception(errorCode ?? Marshal.GetLastWin32Error());
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the kernel version number of the current operating system (unaffected by compatibility settings)
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Version"/> instance, used to represent the current operating system kernel version number.</returns>
|
||||
public static Version GetCurrentOsVersion()
|
||||
{
|
||||
RtlGetNtVersionNumbers(out var major, out var minor, out var build);
|
||||
build &= 0xFFFF;
|
||||
return new Version(major, minor, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Management;
|
||||
using System.Security;
|
||||
using System.Security.Principal;
|
||||
using Microsoft.Win32;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public class ProcessInterop {
|
||||
/// <summary>
|
||||
/// 检查当前程序是否以管理员权限运行。
|
||||
/// </summary>
|
||||
/// <returns>如果当前用户具有管理员权限,则返回 true;否则返回 false。</returns>
|
||||
public static bool IsAdmin() =>
|
||||
new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定进程 ID 的命令行参数。
|
||||
/// </summary>
|
||||
/// <param name="processId">进程 ID</param>
|
||||
/// <returns>命令行参数文本</returns>
|
||||
public static string? GetCommandLine(int processId) {
|
||||
var query = $"SELECT CommandLine FROM Win32_Process WHERE ProcessId = {processId}";
|
||||
using var searcher = new ManagementObjectSearcher(query);
|
||||
return searcher.Get().GetEnumerator().Current["CommandLine"].ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从本地可执行文件启动新的进程。
|
||||
/// </summary>
|
||||
/// <param name="path">可执行文件路径</param>
|
||||
/// <param name="arguments">程序参数</param>
|
||||
/// <param name="runAsAdmin">指定是否以管理员身份启动该进程</param>
|
||||
/// <returns>新的进程实例</returns>
|
||||
public static Process? Start(string path, string? arguments = null, bool runAsAdmin = false) {
|
||||
var psi = new ProcessStartInfo(path);
|
||||
if (arguments is not null) psi.Arguments = arguments;
|
||||
if (runAsAdmin)
|
||||
{
|
||||
psi.UseShellExecute = true;
|
||||
psi.Verb = "runas";
|
||||
}
|
||||
if (Directory.Exists(path))
|
||||
psi.UseShellExecute = true;
|
||||
|
||||
return Process.Start(psi);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定进程的可执行文件路径
|
||||
/// </summary>
|
||||
/// <param name="process">进程实例</param>
|
||||
/// <returns>可执行文件路径,若无法获取则为 <c>null</c></returns>
|
||||
public static string? GetExecutablePath(Process process) {
|
||||
try {
|
||||
var path = process.MainModule?.FileName;
|
||||
return (path is null) ? null : Path.GetFullPath(path);
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从本地可执行文件以管理员身份启动新的进程。<see cref="Start"/> 的套壳。
|
||||
/// </summary>
|
||||
/// <param name="path">可执行文件路径</param>
|
||||
/// <param name="arguments">程序参数</param>
|
||||
/// <returns>新的进程实例</returns>
|
||||
public static Process? StartAsAdmin(string path, string? arguments = null) => Start(path, arguments, true);
|
||||
|
||||
/// <summary>
|
||||
/// 结束指定进程。
|
||||
/// </summary>
|
||||
/// <param name="process">要结束的进程实例</param>
|
||||
/// <param name="timeout">等待进程退出超时,以毫秒为单位,-1 表示无限制</param>
|
||||
/// <param name="force">指定是否强制结束,若为 <c>true</c> 将通过带 <c>/F</c> 参数的 <c>TASKKILL.EXE</c> 结束进程</param>
|
||||
/// <returns>进程返回值,若等待超时将返回 <see cref="int.MinValue"/></returns>
|
||||
public static int Kill(Process process, int timeout = 3000, bool force = false) {
|
||||
if (force) Process.Start(new ProcessStartInfo("TASKKILL.EXE", $"/PID {process.Id} /F") { UseShellExecute = false });
|
||||
else process.Kill();
|
||||
if (timeout == -1) process.WaitForExit();
|
||||
else if (timeout != 0) process.WaitForExit(timeout);
|
||||
return process.HasExited ? process.ExitCode : int.MinValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将特定程序设置为使用高性能显卡启动。
|
||||
/// </summary>
|
||||
/// <param name="executable">可执行文件路径。</param>
|
||||
/// <param name="wantHighPerformance">是否使用高性能显卡,默认为 true。</param>
|
||||
/// <exception cref="ArgumentException">当可执行文件路径无效时抛出</exception>
|
||||
/// <exception cref="UnauthorizedAccessException">当没有足够权限访问注册表时抛出</exception>
|
||||
/// <exception cref="SecurityException">当安全策略不允许访问注册表时抛出</exception>
|
||||
/// <exception cref="InvalidOperationException">当注册表操作失败时抛出</exception>
|
||||
public static void SetGpuPreference(string executable, bool wantHighPerformance = true) {
|
||||
// 参数验证
|
||||
if (string.IsNullOrWhiteSpace(executable)) {
|
||||
throw new ArgumentException("可执行文件路径不能为空或仅包含空白字符", nameof(executable));
|
||||
}
|
||||
|
||||
// 验证文件路径格式
|
||||
try {
|
||||
var fullPath = Path.GetFullPath(executable);
|
||||
if (!File.Exists(fullPath)) {
|
||||
LogWrapper.Warn("System", $"指定的可执行文件不存在: {executable}");
|
||||
}
|
||||
} catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException) {
|
||||
throw new ArgumentException($"无效的可执行文件路径: {executable}", nameof(executable), ex);
|
||||
}
|
||||
|
||||
const string gpuPreferenceRegKey = @"Software\Microsoft\DirectX\UserGpuPreferences";
|
||||
const string gpuPreferenceRegValueHigh = "GpuPreference=2;";
|
||||
const string gpuPreferenceRegValueDefault = "GpuPreference=0;";
|
||||
|
||||
try {
|
||||
var isCurrentHighPerformance = _GetCurrentGpuPreference(executable, gpuPreferenceRegKey, gpuPreferenceRegValueHigh);
|
||||
|
||||
LogWrapper.Info("System", $"当前程序 ({executable}) 的显卡设置为高性能: {isCurrentHighPerformance}");
|
||||
|
||||
// 如果当前设置已经是期望的设置,则无需修改
|
||||
if (isCurrentHighPerformance == wantHighPerformance) {
|
||||
LogWrapper.Info("System", $"程序 ({executable}) 的显卡设置已经是期望的设置,无需修改");
|
||||
return;
|
||||
}
|
||||
|
||||
// 写入新设置
|
||||
_SetGpuPreferenceValue(executable, wantHighPerformance, gpuPreferenceRegKey,
|
||||
gpuPreferenceRegValueHigh, gpuPreferenceRegValueDefault);
|
||||
} catch (UnauthorizedAccessException ex) {
|
||||
var errorMsg = "没有足够的权限访问注册表。请以管理员身份运行程序或检查用户权限设置。";
|
||||
LogWrapper.Error(ex, "System", errorMsg);
|
||||
throw new UnauthorizedAccessException(errorMsg, ex);
|
||||
} catch (SecurityException ex) {
|
||||
var errorMsg = "安全策略不允许访问注册表。请联系系统管理员检查安全设置。";
|
||||
LogWrapper.Error(ex, "System", errorMsg);
|
||||
throw new SecurityException(errorMsg, ex);
|
||||
} catch (Exception ex) {
|
||||
var errorMsg = $"设置 GPU 偏好时发生未预期的错误: {ex.Message}";
|
||||
LogWrapper.Error(ex, "System", errorMsg);
|
||||
throw new InvalidOperationException(errorMsg, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前程序的GPU偏好设置
|
||||
/// </summary>
|
||||
private static bool _GetCurrentGpuPreference(string executable, string regKey, string highPerfValue) {
|
||||
try {
|
||||
using var readOnlyKey = Registry.CurrentUser.OpenSubKey(regKey, false);
|
||||
if (readOnlyKey is null) {
|
||||
LogWrapper.Info("System", "GPU 偏好注册表键不存在,将在需要时创建");
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentValue = readOnlyKey.GetValue(executable)?.ToString();
|
||||
return string.Equals(currentValue, highPerfValue, StringComparison.OrdinalIgnoreCase);
|
||||
} catch (Exception ex) {
|
||||
LogWrapper.Warn(ex, "System", $"读取当前 GPU 偏好设置时出现错误: {ex.Message}");
|
||||
return false; // 假设当前不是高性能模式
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置GPU偏好值到注册表
|
||||
/// </summary>
|
||||
private static bool _SetGpuPreferenceValue(string executable, bool wantHighPerformance,
|
||||
string regKey, string highPerfValue, string defaultValue) {
|
||||
RegistryKey? writeKey = null;
|
||||
try {
|
||||
// 尝试打开现有键进行写入
|
||||
writeKey = Registry.CurrentUser.OpenSubKey(regKey, true);
|
||||
|
||||
// 如果键不存在,创建它
|
||||
if (writeKey is null) {
|
||||
LogWrapper.Info("System", "创建 GPU 偏好注册表键");
|
||||
writeKey = Registry.CurrentUser.CreateSubKey(regKey);
|
||||
|
||||
if (writeKey is null) {
|
||||
throw new InvalidOperationException($"无法创建注册表键: {regKey}");
|
||||
}
|
||||
}
|
||||
|
||||
var valueToSet = wantHighPerformance ? highPerfValue : defaultValue;
|
||||
writeKey.SetValue(executable, valueToSet, RegistryValueKind.String);
|
||||
|
||||
LogWrapper.Info("System", $"成功设置程序 ({executable}) 的GPU偏好: {(wantHighPerformance ? "高性能" : "默认")}");
|
||||
return true;
|
||||
} catch (UnauthorizedAccessException) {
|
||||
// 重新抛出,让上层处理
|
||||
throw;
|
||||
} catch (SecurityException) {
|
||||
// 重新抛出,让上层处理
|
||||
throw;
|
||||
} catch (Exception ex) {
|
||||
var errorMsg = $"写入注册表时发生错误: {ex.Message}";
|
||||
LogWrapper.Error(ex, "System", errorMsg);
|
||||
throw new InvalidOperationException(errorMsg, ex);
|
||||
} finally {
|
||||
writeKey?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ProcessExitCode {
|
||||
/// <summary>
|
||||
/// Indicates that the process completed successfully.
|
||||
/// </summary>
|
||||
TaskDone = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a general failure of the process.
|
||||
/// </summary>
|
||||
Failed = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the process was canceled.
|
||||
/// </summary>
|
||||
Canceled = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the process failed due to insufficient permissions.
|
||||
/// </summary>
|
||||
AccessDenied = 5
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public partial class RegistryChangeMonitor : IDisposable
|
||||
{
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
private const int REG_NOTIFY_CHANGE_LAST_SET = 0x00000004;
|
||||
private const int KEY_NOTIFY = 0x0010;
|
||||
private const int KEY_QUERY_VALUE = 0x0001;
|
||||
private const int KEY_READ = (KEY_QUERY_VALUE | KEY_NOTIFY);
|
||||
private const UIntPtr HKEY_CURRENT_USER = 0x80000001;
|
||||
|
||||
[LibraryImport("advapi32.dll", EntryPoint = "RegOpenKeyExW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
|
||||
private static partial int _RegOpenKeyEx(UIntPtr hKey, string subKey, uint options, int samDesired, out IntPtr phkResult);
|
||||
|
||||
[LibraryImport("advapi32.dll", EntryPoint = "RegNotifyChangeKeyValue", SetLastError = true)]
|
||||
private static partial int _RegNotifyChangeKeyValue(IntPtr hKey, [MarshalAs(UnmanagedType.Bool)] bool bWatchSubtree, int dwNotifyFilter, IntPtr hEvent, [MarshalAs(UnmanagedType.Bool)] bool fAsynchronous);
|
||||
|
||||
[LibraryImport("advapi32.dll", EntryPoint = "RegCloseKey", SetLastError = true)]
|
||||
private static partial int _RegCloseKey(IntPtr hKey);
|
||||
|
||||
// ReSharper restore InconsistentNaming
|
||||
|
||||
private readonly IntPtr _hKey;
|
||||
private readonly ManualResetEvent _stopEvent = new(false);
|
||||
private readonly ManualResetEvent _registryEvent = new(false);
|
||||
private readonly Thread _monitorThread;
|
||||
|
||||
public event EventHandler? Changed;
|
||||
|
||||
public RegistryChangeMonitor(string keyPath)
|
||||
{
|
||||
// Open registry key with proper access rights
|
||||
var result = _RegOpenKeyEx(HKEY_CURRENT_USER, keyPath, 0, KEY_READ, out _hKey);
|
||||
if (result != 0) throw new Win32Exception(result);
|
||||
|
||||
// Start monitoring thread
|
||||
_monitorThread = new Thread(_MonitorThread) { IsBackground = true };
|
||||
_monitorThread.Start();
|
||||
}
|
||||
|
||||
private void _MonitorThread()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Initial registration
|
||||
_RegisterForNotification();
|
||||
|
||||
while (!_stopEvent.WaitOne(0))
|
||||
{
|
||||
// Wait for either registry change or stop signal
|
||||
var index = WaitHandle.WaitAny(
|
||||
[_registryEvent, _stopEvent],
|
||||
TimeSpan.FromSeconds(1)); // Timeout to check for stop periodically
|
||||
|
||||
if (index == 1) break; // Stop requested
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
_registryEvent.Reset();
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
_RegisterForNotification(); // Re-register for next change
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_registryEvent.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void _RegisterForNotification()
|
||||
{
|
||||
var result = _RegNotifyChangeKeyValue(
|
||||
_hKey,
|
||||
true,
|
||||
REG_NOTIFY_CHANGE_LAST_SET,
|
||||
_registryEvent.SafeWaitHandle.DangerousGetHandle(),
|
||||
true); // Must be asynchronous to allow graceful shutdown
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
// Handle error - key might have been deleted
|
||||
_stopEvent.Set();
|
||||
throw new Win32Exception(result);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_stopEvent.Set();
|
||||
|
||||
// Give thread a chance to exit gracefully
|
||||
if (_monitorThread is {IsAlive: true})
|
||||
_monitorThread.Join(1000);
|
||||
|
||||
if (_hKey != IntPtr.Zero)
|
||||
_ = _RegCloseKey(_hKey);
|
||||
|
||||
_stopEvent.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static class SystemInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否为 32 位系统。
|
||||
/// </summary>
|
||||
public static readonly bool Is32BitSystem = !Environment.Is64BitOperatingSystem;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为 ARM64 架构。
|
||||
/// </summary>
|
||||
public static readonly bool IsArm64System = RuntimeInformation.OSArchitecture == Architecture.Arm64;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用 GBK 编码。
|
||||
/// </summary>
|
||||
public static readonly bool IsGBKEncoding = Encoding.Default.CodePage == 936;
|
||||
|
||||
/// <summary>
|
||||
/// 系统信息描述,例如 Microsoft Windows 11 专业工作站版 10.0.22635.0
|
||||
/// </summary>
|
||||
public static readonly string OSInfo = $"{RuntimeInformation.OSDescription} {Environment.OSVersion.Version}";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static class SystemPaths {
|
||||
/// <summary>
|
||||
/// 系统盘符(含冒号和反斜杠),例如 "C:\"。
|
||||
/// </summary>
|
||||
public static string DriveLetter { get; } = Path.GetPathRoot(Environment.SystemDirectory)!;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using Microsoft.Win32;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public class SystemTheme {
|
||||
private const string ThemeRegistryPath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
|
||||
private const string AppsUseLightThemeKey = "AppsUseLightTheme";
|
||||
|
||||
/// <summary>
|
||||
/// 检查系统是否处于深色模式。
|
||||
/// </summary>
|
||||
/// <returns>如果系统使用深色模式,则返回 true;否则返回 false(包括注册表不可访问的情况)。</returns>
|
||||
public static bool IsSystemInDarkMode() {
|
||||
try {
|
||||
using var registryKey = Registry.CurrentUser.OpenSubKey(ThemeRegistryPath);
|
||||
if (registryKey is null) {
|
||||
LogWrapper.Warn($"注册表键 {ThemeRegistryPath} 不存在");
|
||||
return false;
|
||||
}
|
||||
|
||||
var value = registryKey.GetValue(AppsUseLightThemeKey) as int?;
|
||||
return value == 0; // 0 表示深色模式(AppsUseLightTheme = false)
|
||||
} catch (Exception ex) when (ex is SecurityException or IOException) {
|
||||
LogWrapper.Warn(ex, $"无法访问注册表键 {ThemeRegistryPath}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static partial class WindowInterop
|
||||
{
|
||||
// ReSharper disable InconsistentNaming UnusedMember.Local
|
||||
|
||||
// DWM 外边缘结构定义
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MARGINS { public int leftWidth, rightWidth, topHeight, bottomHeight; }
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static partial int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset);
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static partial int DwmIsCompositionEnabled([MarshalAs(UnmanagedType.Bool)] out bool pfEnabled);
|
||||
|
||||
// Win32 矩形结构定义
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct RECT { public int left; public int top; public int right; public int bottom; }
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||
|
||||
// MONITOR_DPI_TYPE enum
|
||||
private enum MONITOR_DPI_TYPE {
|
||||
MDT_EFFECTIVE_DPI = 0,
|
||||
MDT_ANGULAR_DPI = 1,
|
||||
MDT_RAW_DPI = 2,
|
||||
MDT_DEFAULT = MDT_EFFECTIVE_DPI
|
||||
}
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
private static partial IntPtr MonitorFromWindow(IntPtr hWnd, uint dwFlags);
|
||||
|
||||
// Get the primary monitor handle
|
||||
private const int MONITOR_DEFAULTTOPRIMARY = 1;
|
||||
|
||||
[LibraryImport("shcore.dll", EntryPoint = "GetDpiForMonitor")]
|
||||
private static partial int GetDpiForMonitor(
|
||||
IntPtr hMonitor,
|
||||
MONITOR_DPI_TYPE dpiType,
|
||||
out uint dpiX,
|
||||
out uint dpiY
|
||||
);
|
||||
|
||||
// ReSharper enable InconsistentNaming UnusedMember.Local
|
||||
|
||||
/// <summary>
|
||||
/// 检测 DWM 组合是否可用
|
||||
/// </summary>
|
||||
public static bool IsCompositionEnabled()
|
||||
{
|
||||
var hResult = DwmIsCompositionEnabled(out var enabled);
|
||||
return hResult != 0 ? throw new Win32Exception(hResult, "Failed to check DWM status") : enabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 DWM 窗口边框到客户区域的扩展大小
|
||||
/// </summary>
|
||||
public static void ExtendFrameIntoClientArea(
|
||||
IntPtr hWnd, int marginLeft, int marginTop, int marginRight, int marginBottom)
|
||||
{
|
||||
MARGINS margins = new()
|
||||
{
|
||||
leftWidth = marginLeft,
|
||||
rightWidth = marginRight,
|
||||
topHeight = marginTop,
|
||||
bottomHeight = marginBottom
|
||||
};
|
||||
if (!IsCompositionEnabled()) return;
|
||||
var hResult = DwmExtendFrameIntoClientArea(hWnd, ref margins);
|
||||
if (hResult != 0) throw new Win32Exception(hResult, "Failed to extend frame into client area");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="ExtendFrameIntoClientArea(IntPtr, int, int, int, int)"/>
|
||||
/// </summary>
|
||||
public static void ExtendFrameIntoClientArea(IntPtr hWnd, int margin)
|
||||
=> ExtendFrameIntoClientArea(hWnd, margin, margin, margin, margin);
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Win32 窗口矩形定义
|
||||
/// </summary>
|
||||
public static (int Left, int Top, int Right, int Bottom) GetWindowRectangle(IntPtr hWnd)
|
||||
{
|
||||
var hResult = GetWindowRect(hWnd, out var rect);
|
||||
return hResult ? (rect.left, rect.top, rect.right, rect.bottom)
|
||||
: throw new Win32Exception("Failed to get window rectangle");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Win32 窗口位置与大小
|
||||
/// </summary>
|
||||
public static (int X, int Y, int Width, int Height) ToWindowBounds(
|
||||
this (int Left, int Top, int Right, int Bottom) rect)
|
||||
{
|
||||
var (l, t, r, b) = rect;
|
||||
var x = l;
|
||||
var y = t;
|
||||
var width = r - l;
|
||||
var height = b - t;
|
||||
return (x, y, width, height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定屏幕的系统 DPI
|
||||
/// </summary>
|
||||
/// <param name="hWnd">位于指定屏幕上的任意窗口句柄,默认指定主屏</param>
|
||||
public static int GetSystemDpi(IntPtr hWnd = 0) {
|
||||
// Get the monitor handle
|
||||
var hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTOPRIMARY);
|
||||
// 0 is S_OK
|
||||
var hr = GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
|
||||
if (hr == 0)
|
||||
return (int)dpiX;
|
||||
// fallback to default DPI (96)
|
||||
return 96;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
/// <summary>
|
||||
/// 通用化 PE 文件头读取器
|
||||
/// </summary>
|
||||
public static class PEHeaderReader
|
||||
{
|
||||
private const int PE_POINTER_OFFSET = 0x3C;
|
||||
private const uint PE_SIGNATURE = 0x00004550; // "PE\0\0"
|
||||
|
||||
/// <summary>
|
||||
/// 读取并解析 PE 文件头结构
|
||||
/// </summary>
|
||||
public static PEStruct ReadPEHeader(string filePath)
|
||||
{
|
||||
var result = new PEStruct();
|
||||
|
||||
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
|
||||
{
|
||||
result.ErrorMessage = "文件不存在或路径无效";
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
// 验证 DOS 头
|
||||
if (!_IsValidDosHeader(fs))
|
||||
{
|
||||
result.ErrorMessage = "无效的DOS头(MZ签名)";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 获取 NT 头偏移量
|
||||
var peHeaderOffset = _GetPEOffset(fs);
|
||||
if (peHeaderOffset <= 0 || peHeaderOffset >= fs.Length - 24)
|
||||
{
|
||||
result.ErrorMessage = "无效的PE头偏移量";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 定位并验证 PE 签名
|
||||
fs.Seek(peHeaderOffset, SeekOrigin.Begin);
|
||||
if (!_IsValidPESignature(fs))
|
||||
{
|
||||
result.ErrorMessage = "无效的PE签名";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 读取完整的 IMAGE_FILE_HEADER 结构
|
||||
result = _ParseImageFileHeader(fs);
|
||||
result.IsValid = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.IsValid = false;
|
||||
result.ErrorMessage = $"读取失败: {ex.Message}";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool _IsValidDosHeader(FileStream fs)
|
||||
{
|
||||
if (fs.Length < 2) return false;
|
||||
fs.Seek(0, SeekOrigin.Begin);
|
||||
return fs.ReadByte() == 'M' && fs.ReadByte() == 'Z';
|
||||
}
|
||||
|
||||
private static long _GetPEOffset(FileStream fs)
|
||||
{
|
||||
fs.Seek(PE_POINTER_OFFSET, SeekOrigin.Begin);
|
||||
using var reader = new BinaryReader(fs, Encoding.Default, true);
|
||||
return reader.ReadInt32();
|
||||
}
|
||||
|
||||
private static bool _IsValidPESignature(FileStream fs)
|
||||
{
|
||||
using var reader = new BinaryReader(fs, Encoding.Default, true);
|
||||
return reader.ReadUInt32() == PE_SIGNATURE;
|
||||
}
|
||||
|
||||
private static PEStruct _ParseImageFileHeader(FileStream fs)
|
||||
{
|
||||
using var reader = new BinaryReader(fs, Encoding.Default, true);
|
||||
return new PEStruct
|
||||
{
|
||||
// 将读取的ushort值转换为MachineType枚举
|
||||
Machine = (MachineType)reader.ReadUInt16(),
|
||||
NumberOfSections = reader.ReadUInt16(),
|
||||
TimeDateStamp = reader.ReadUInt32(),
|
||||
PointerToSymbolTable = reader.ReadUInt32(),
|
||||
NumberOfSymbols = reader.ReadUInt32(),
|
||||
SizeOfOptionalHeader = reader.ReadUInt16(),
|
||||
Characteristics = reader.ReadUInt16()
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsMachine64Bit(MachineType machine)
|
||||
{
|
||||
return new List<MachineType> { MachineType.IA64, MachineType.ARM64, MachineType.AMD64 }.Contains(machine);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PE 文件机器架构类型
|
||||
/// </summary>
|
||||
public enum MachineType : ushort
|
||||
{
|
||||
Unknown = 0x0,
|
||||
I386 = 0x14C, // x86
|
||||
IA64 = 0x200, // Intel Itanium
|
||||
AMD64 = 0x8664, // x64 (AMD or Intel)
|
||||
ARM = 0x1C0, // ARM little endian
|
||||
ARM64 = 0xAA64, // ARM64 little endian
|
||||
ARMNT = 0x1C4, // ARM Thumb-2 little endian
|
||||
EFI_BYTECODE = 0xEBC, // EFI byte code
|
||||
M32R = 0x9041, // Mitsubishi M32R little endian
|
||||
MIPS16 = 0x266, // MIPS16
|
||||
MIPSFPU = 0x366, // MIPS with FPU
|
||||
MIPSFPU16 = 0x466, // MIPS16 with FPU
|
||||
POWERPC = 0x1F0, // Power PC little endian
|
||||
POWERPCFP = 0x1F1, // Power PC with floating point support
|
||||
R4000 = 0x166, // MIPS little endian
|
||||
SH3 = 0x1A2, // Hitachi SH3
|
||||
SH3DSP = 0x1A3, // Hitachi SH3 DSP
|
||||
SH4 = 0x1A6, // Hitachi SH4
|
||||
SH5 = 0x1A8, // Hitachi SH5
|
||||
THUMB = 0x1C2, // Thumb
|
||||
WCEMIPSV2 = 0x169, // MIPS little-endian WCE v2
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct PEStruct
|
||||
{
|
||||
public MachineType Machine;
|
||||
public ushort NumberOfSections;
|
||||
public uint TimeDateStamp;
|
||||
public uint PointerToSymbolTable;
|
||||
public uint NumberOfSymbols;
|
||||
public ushort SizeOfOptionalHeader;
|
||||
public ushort Characteristics;
|
||||
public bool IsValid;
|
||||
public string ErrorMessage;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// 用来模拟带一个参数的属性
|
||||
/// </summary>
|
||||
/// <typeparam name="TParam">参数值类型</typeparam>
|
||||
/// <typeparam name="TValue">属性值类型</typeparam>
|
||||
public class ParameterizedProperty<TParam, TValue>
|
||||
{
|
||||
public Func<TParam, TValue> GetValue { private get; init; } = null!;
|
||||
public Action<TParam, TValue> SetValue { private get; init; } = null!;
|
||||
|
||||
public TValue this[TParam param]
|
||||
{
|
||||
get => GetValue.Invoke(param);
|
||||
set => SetValue.Invoke(param, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
/// <summary>
|
||||
/// 提供随机数和集合随机操作的实用方法。
|
||||
/// </summary>
|
||||
public static class RandomUtils {
|
||||
private static readonly Random _SharedRandom = Random.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 从集合中随机选择一个元素。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">集合元素类型。</typeparam>
|
||||
/// <param name="collection">要从中选择元素的集合。</param>
|
||||
/// <returns>随机选择的元素。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="collection"/> 为 null 时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="collection"/> 为空时抛出。</exception>
|
||||
public static T PickRandom<T>(ICollection<T> collection) {
|
||||
if (collection.Count == 0)
|
||||
throw new ArgumentException("集合不能为空", nameof(collection));
|
||||
var index = _SharedRandom.Next(collection.Count);
|
||||
if (collection is IList<T> list)
|
||||
return list[index];
|
||||
return collection.Skip(index).First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成指定范围内的随机整数(包含 min 和 max)。
|
||||
/// </summary>
|
||||
/// <param name="min">范围下限(包含)。</param>
|
||||
/// <param name="max">范围上限(包含)。</param>
|
||||
/// <returns>随机整数,范围为 [min, max]。</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="min"/> 大于 <paramref name="max"/> 时抛出。</exception>
|
||||
public static int NextInt(int min, int max) {
|
||||
return min > max ? throw new ArgumentOutOfRangeException(nameof(min), "最小值不能大于最大值") : _SharedRandom.Next(min, max + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 随机打乱列表的元素,返回新列表。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">列表元素类型。</typeparam>
|
||||
/// <param name="list">要打乱的列表。</param>
|
||||
/// <returns>包含随机顺序元素的新列表。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="list"/> 为 null 时抛出。</exception>
|
||||
public static List<T> Shuffle<T>(IList<T> list) {
|
||||
var result = new List<T>(list);
|
||||
var n = result.Count;
|
||||
for (var i = n - 1; i > 0; i--) {
|
||||
var j = _SharedRandom.Next(0, i + 1);
|
||||
(result[i], result[j]) = (result[j], result[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 原地随机打乱列表的元素。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">列表元素类型。</typeparam>
|
||||
/// <param name="list">要打乱的列表。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="list"/> 为 null 时抛出。</exception>
|
||||
public static void ShuffleInPlace<T>(IList<T> list) {
|
||||
var n = list.Count;
|
||||
for (var i = n - 1; i > 0; i--) {
|
||||
var j = _SharedRandom.Next(0, i + 1);
|
||||
(list[i], list[j]) = (list[j], list[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// 基于代码生成优化的正则表达式实例。
|
||||
/// </summary>
|
||||
public static partial class RegexPatterns
|
||||
{
|
||||
/// <summary>
|
||||
/// 陶瓦联机 ID。
|
||||
/// </summary>
|
||||
public static readonly Regex TerracottaId = _TerracottaId();
|
||||
[GeneratedRegex("([0-9A-Z]{5}-){4}[0-9A-Z]{5}", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex _TerracottaId();
|
||||
|
||||
/// <summary>
|
||||
/// 换行符,包括 <c>\r\n</c> <c>\n</c> <c>\r</c> 三种。
|
||||
/// </summary>
|
||||
public static readonly Regex NewLine = _NewLine();
|
||||
[GeneratedRegex(@"\r\n|\n|\r")]
|
||||
private static partial Regex _NewLine();
|
||||
|
||||
/// <summary>
|
||||
/// ISO 8601 当日终点时间写法 24:00(:00)(.0…),语义等于次日零点。
|
||||
/// </summary>
|
||||
public static readonly Regex Iso8601EndOfDay = _Iso8601EndOfDay();
|
||||
[GeneratedRegex(@"[Tt]24:00(?::00(?:\.0+)?)?(?=[Zz+\-]|$)")]
|
||||
private static partial Regex _Iso8601EndOfDay();
|
||||
|
||||
/// <summary>
|
||||
/// Semantic Versioning (SemVer) 规范的版本号,包含可选的 v 前缀。
|
||||
/// </summary>
|
||||
public static readonly Regex SemVer = _SemVer();
|
||||
private const string PatternSemVer =
|
||||
@"^v?(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)" +
|
||||
@"(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" +
|
||||
@"(?:\+(?<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$";
|
||||
[GeneratedRegex(PatternSemVer, RegexOptions.ExplicitCapture)]
|
||||
private static partial Regex _SemVer();
|
||||
|
||||
/// <summary>
|
||||
/// 简单匹配 HTTP(S) URI,若需严格检查请使用 <see cref="FullHttpUri"/>。
|
||||
/// </summary>
|
||||
public static readonly Regex HttpUri = _HttpUri();
|
||||
private const string PatternHttpUri = @"^https?://(?:\[[^\]\s]+\]|[^/\s?#:]+)(?::\d{1,5})?(?:/[^\s?#]*)?(?:\?[^\s#]*)?(?:#\S*)?$";
|
||||
[GeneratedRegex(PatternHttpUri, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _HttpUri();
|
||||
|
||||
/// <summary>
|
||||
/// 包含完整规则的 HTTP(S) URI,含有 <c>scheme</c> <c>host</c> <c>ipv6</c>
|
||||
/// <c>port</c> <c>path</c> <c>query</c> <c>fragment</c> 分组。
|
||||
/// </summary>
|
||||
public static readonly Regex FullHttpUri = _FullHttpUri();
|
||||
private const string PatternFullHttpUri =
|
||||
@"^(?<scheme>https?)://(?<host>localhost|(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3})" +
|
||||
@"|\[(?<ipv6>[0-9A-Fa-f:.]+)\]|(?:(?:[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?\.)+(?:[A-Za-z]{2,63}|xn--[" +
|
||||
@"A-Za-z0-9\-]{2,59})))(?::(?<port>6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{0,3}))?" +
|
||||
@"(?<path>/[^\s?#]*)?(?:\?(?<query>[^\s#]*))?(?:#(?<fragment>[^\s]*))?$";
|
||||
[GeneratedRegex(PatternFullHttpUri, RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _FullHttpUri();
|
||||
|
||||
/// <summary>
|
||||
/// LastPending(_Xxx).log 路径。
|
||||
/// </summary>
|
||||
public static readonly Regex LastPendingLogPath = _LastPendingLogPath();
|
||||
[GeneratedRegex(@"\\LastPending[_]?[^\\]*\.log$", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex _LastPendingLogPath();
|
||||
|
||||
/// <summary>
|
||||
/// Mod Loader 不兼容的错误提示。
|
||||
/// </summary>
|
||||
public static readonly Regex IncompatibleModLoaderErrorHint = _IncompatibleModLoaderErrorHint();
|
||||
[GeneratedRegex(@"(incompatible[\s\S]+'Fabric Loader' \(fabricloader\)|Mod ID: '(?:neo)?forge', Requested by '([^']+)')")]
|
||||
private static partial Regex _IncompatibleModLoaderErrorHint();
|
||||
|
||||
/// <summary>
|
||||
/// Minecraft 颜色代码,为 Hex 颜色代码,格式为 <c>#RRGGBB</c>。
|
||||
/// </summary>
|
||||
public static readonly Regex HexColor = _HexColor();
|
||||
[GeneratedRegex("^#[0-9A-Fa-f]{6}$")]
|
||||
private static partial Regex _HexColor();
|
||||
|
||||
/// <summary>
|
||||
/// A compiled regular expression for matching Minecraft MOTD formatting codes.
|
||||
/// Matches legacy color/format codes (e.g., §a, §b, §k) and hexadecimal color codes (e.g., #FF0000).
|
||||
/// </summary>
|
||||
public static readonly Regex MotdCode = _MotdCode();
|
||||
[GeneratedRegex("(§[0-9a-fk-oAr]|#[0-9A-Fa-f]{6})")]
|
||||
private static partial Regex _MotdCode();
|
||||
|
||||
public static readonly Regex BroadcastMotd = _BroadcastMotd();
|
||||
[GeneratedRegex(@"\[MOTD\](.*?)\[/MOTD\]", RegexOptions.Compiled)]
|
||||
private static partial Regex _BroadcastMotd();
|
||||
|
||||
public static readonly Regex BroadcastAd = _BroadcastAd();
|
||||
[GeneratedRegex(@"\[AD\](.*?)\[/AD\]", RegexOptions.Compiled)]
|
||||
private static partial Regex _BroadcastAd();
|
||||
|
||||
/// <summary>
|
||||
/// 匹配 Minecraft 正常版本号,如 1.20.4、1.19.3 等。
|
||||
/// </summary>
|
||||
public static readonly Regex McNormalVersion = _McNormalVersion();
|
||||
[GeneratedRegex(@"^\d+\.\d+\.\d+$|^\d+\.\d+$")]
|
||||
private static partial Regex _McNormalVersion();
|
||||
|
||||
/// <summary>
|
||||
/// 匹配 Minecraft 快照版本号,如 24w14a 等。
|
||||
/// </summary>
|
||||
public static readonly Regex McSnapshotVersion = _McSnapshotVersion();
|
||||
[GeneratedRegex(@"(\d+)w(\d+)([a-z]?)")]
|
||||
private static partial Regex _McSnapshotVersion();
|
||||
|
||||
/// <summary>
|
||||
/// 匹配 Minecraft Indev 版本号,如 in-20091231-2、in-20100130 等。
|
||||
/// </summary>
|
||||
public static readonly Regex McIndevVersion = _McIndevVersion();
|
||||
[GeneratedRegex(@"^in-(\d{8})(-(\d+))?$")]
|
||||
private static partial Regex _McIndevVersion();
|
||||
|
||||
/// <summary>
|
||||
/// 匹配 Minecraft Infdev 版本号,如 inf-20100611 等。
|
||||
/// </summary>
|
||||
public static readonly Regex McInfdevVersion = _McInfdevVersion();
|
||||
[GeneratedRegex(@"^inf-(\d{8})(-(\d+))?$")]
|
||||
private static partial Regex _McInfdevVersion();
|
||||
|
||||
/// <summary>
|
||||
/// 匹配 accessToken 内容。
|
||||
/// </summary>
|
||||
public static readonly Regex AccessToken = _AccessToken();
|
||||
[GeneratedRegex("(?<=accessToken ([^ ]{5}))[^ ]+(?=[^ ]{5})")]
|
||||
private static partial Regex _AccessToken();
|
||||
|
||||
/// <summary>
|
||||
/// 使用 IsMatch 检查是否存在中文字符
|
||||
/// </summary>
|
||||
public static readonly Regex HasChineseChar = _HasChineseChar();
|
||||
[GeneratedRegex("[\u4e00-\u9fbb]")]
|
||||
private static partial Regex _HasChineseChar();
|
||||
|
||||
/// <summary>
|
||||
/// 用 Replace 替换英文中的分隔特征
|
||||
/// </summary>
|
||||
public static readonly Regex EnglishSpacedKeywords = _EnglishSpacedKeywords();
|
||||
[GeneratedRegex("([A-Z]+|[a-z]+?)(?=[A-Z]+[a-z]+[a-z ]*)")]
|
||||
private static partial Regex _EnglishSpacedKeywords();
|
||||
|
||||
/// <summary>
|
||||
/// NTFS 8.3 文件名格式
|
||||
/// </summary>
|
||||
public static readonly Regex Ntfs83FileName = _Ntfs83FileName();
|
||||
[GeneratedRegex(@".{2,}~\d")]
|
||||
private static partial Regex _Ntfs83FileName();
|
||||
|
||||
public static readonly Regex UncPath = _UncPath();
|
||||
[GeneratedRegex("""^\\\\[^\\/:*?"<>|]+\\[^\\/:*?"<>|]+(\\[^\\/:*?"<>|]+)*\\?$""")]
|
||||
private static partial Regex _UncPath();
|
||||
|
||||
#region Minecraft 实例解析
|
||||
|
||||
public static readonly Regex OptiFineVersion = _OptiFineVersion();
|
||||
[GeneratedRegex(@"(?<=HD_U_)[^"":/]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _OptiFineVersion();
|
||||
|
||||
public static readonly Regex OptiFineLibVersion = _OptiFineLibVersion();
|
||||
[GeneratedRegex(@"(?<=HD_U_)[^"":/]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _OptiFineLibVersion();
|
||||
|
||||
public static readonly Regex LegacyFabricVersion = _LegacyFabricVersion();
|
||||
[GeneratedRegex(@"(?<=(net.fabricmc:fabric-loader:))[0-9\.]+(\+build.[0-9]+)?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _LegacyFabricVersion();
|
||||
|
||||
public static readonly Regex FabricVersion = _FabricVersion();
|
||||
[GeneratedRegex(@"(?<=(net.fabricmc:fabric-loader:))[0-9\.]+(\+build.[0-9]+)?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _FabricVersion();
|
||||
|
||||
public static readonly Regex QuiltVersion = _QuiltVersion();
|
||||
[GeneratedRegex(@"(?<=(org.quiltmc:quilt-loader:))[0-9\.]+(\+build.[0-9]+)?((-beta.)[0-9]([0-9]?))", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _QuiltVersion();
|
||||
|
||||
public static readonly Regex CleanroomVersion = _CleanroomVersion();
|
||||
[GeneratedRegex(@"(?<=(com.cleanroommc:cleanroom:))[0-9\.]+(\+build.[0-9]+)?(-alpha)?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _CleanroomVersion();
|
||||
|
||||
/// <summary>
|
||||
/// 匹配 Forge 主版本号(位于 "forge:X.Y.Z-" 之后)。
|
||||
/// </summary>
|
||||
public static readonly Regex ForgeMainVersion = _ForgeMainVersion();
|
||||
[GeneratedRegex(@"(?<=forge:[0-9\.]+(_pre[0-9]*)?\-)[0-9\.]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _ForgeMainVersion();
|
||||
|
||||
/// <summary>
|
||||
/// 匹配 Forge Maven 坐标中的版本号(net.minecraftforge:minecraftforge:X.Y.Z)。
|
||||
/// </summary>
|
||||
public static readonly Regex ForgeLibVersion = _ForgeLibVersion();
|
||||
[GeneratedRegex(@"(?<=net\.minecraftforge:(?:forge|fmlloader):[0-9.]+-)[0-9a-zA-Z._+-]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _ForgeLibVersion();
|
||||
|
||||
/// <summary>
|
||||
/// 匹配 NeoForge 版本号(从 JSON 参数中提取,如 "--fml.neoForgeVersion", "20.6.119-beta")。
|
||||
/// </summary>
|
||||
public static readonly Regex NeoForgeVersion = _NeoForgeVersion();
|
||||
[GeneratedRegex(@"(?<=orgeVersion"",[^""]*?"")[^""]+(?="",)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _NeoForgeVersion();
|
||||
|
||||
public static readonly Regex FabricLikeLibVersion = _FabricLikeLibVersion();
|
||||
[GeneratedRegex(@"(?<=((fabricmc)|(quiltmc)|(legacyfabric)):intermediary:)[^""]*", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _FabricLikeLibVersion();
|
||||
|
||||
public static readonly Regex LabyModVersion = _LabyModVersion();
|
||||
[GeneratedRegex(@"(?<=-Dnet.labymod.running-version=)1.[0-9+.]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _LabyModVersion();
|
||||
|
||||
public static readonly Regex MinecraftJsonVersion = _MinecraftJsonVersion();
|
||||
[GeneratedRegex(@"(([1-9][0-9]w[0-9]{2}[a-g])|((1|[2-9][0-9])\.[0-9]+(\.[0-9]+)?(-(pre|rc|snapshot-?)[1-9]*| Pre-Release( [1-9])?)?))(_unobfuscated)?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _MinecraftJsonVersion();
|
||||
|
||||
public static readonly Regex MinecraftDownloadUrlVersion = _MinecraftDownloadUrlVersion();
|
||||
[GeneratedRegex(@"(?<=launcher.mojang.com/mc/game/)[^/]*", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _MinecraftDownloadUrlVersion();
|
||||
|
||||
public static readonly Regex CatchLwjglInLib = _CatchLwjglInLib();
|
||||
[GeneratedRegex(@"(?<=org.lwjgl:)lwjgl(-[a-z._.\-.0-9]*)(?=(:[0-9].[0-9].[0-9](-[a-z.0-9._.\-]*)?:([a-z._.\-.0-9]*)?))", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _CatchLwjglInLib();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Minecraft 下载信息
|
||||
|
||||
/// <summary>
|
||||
/// 匹配 NeoForge 版本列表 JSON 中的版本号
|
||||
/// </summary>
|
||||
public static readonly Regex DlNeoForgeVersion = _DlNeoForgeVersion();
|
||||
[GeneratedRegex(@"(?<="")(1\.20\.1-)?\d+\.[^\.]+\.\d+(\.\d+)?(-(beta|alpha)(\.\d+)?)?(\+snapshot-\d+)?(\+pre-\d+)?(?="")", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _DlNeoForgeVersion();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 外部组件
|
||||
|
||||
public static readonly Regex ModIdMatch = _ModIdMatch();
|
||||
[GeneratedRegex(@"[0-9a-zA-Z_-]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex _ModIdMatch();
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个可能成功或失败的操作结果。
|
||||
/// 类似于 Rust 的 Result。
|
||||
/// </summary>
|
||||
/// <typeparam name="TOk">成功时返回的值类型</typeparam>
|
||||
/// <typeparam name="TErr">失败时返回的错误类型</typeparam>
|
||||
[DebuggerDisplay("{" + nameof(_DebuggerDisplay) + "}")]
|
||||
public sealed class Result<TOk, TErr>
|
||||
{
|
||||
private readonly TOk? _value;
|
||||
private readonly TErr? _error;
|
||||
private readonly bool _isSuccess;
|
||||
|
||||
private Result(TOk value)
|
||||
{
|
||||
_value = value;
|
||||
_isSuccess = true;
|
||||
}
|
||||
|
||||
private Result(TErr error)
|
||||
{
|
||||
_error = error;
|
||||
_isSuccess = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个成功的结果。
|
||||
/// </summary>
|
||||
public static Result<TOk, TErr> Ok(TOk value) => new(value);
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个失败的结果。
|
||||
/// </summary>
|
||||
public static Result<TOk, TErr> Err(TErr error) => new(error);
|
||||
|
||||
/// <summary>
|
||||
/// 是否为成功状态。
|
||||
/// </summary>
|
||||
public bool IsSuccess => _isSuccess;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为失败状态。
|
||||
/// </summary>
|
||||
public bool IsFailure => !_isSuccess;
|
||||
|
||||
/// <summary>
|
||||
/// 获取成功值(仅在 IsSuccess 为 true 时有效)。
|
||||
/// </summary>
|
||||
public TOk Value => IsSuccess ? _value! : throw new InvalidOperationException("Cannot access Value of a failed Result.");
|
||||
|
||||
/// <summary>
|
||||
/// 获取错误值(仅在 IsFailure 为 true 时有效)。
|
||||
/// </summary>
|
||||
public TErr Error => IsFailure ? _error! : throw new InvalidOperationException("Cannot access Error of a successful Result.");
|
||||
|
||||
/// <summary>
|
||||
/// 使用模式匹配处理成功和失败情况。
|
||||
/// </summary>
|
||||
public TR Match<TR>(Func<TOk, TR> onSuccess, Func<TErr, TR> onError)
|
||||
=> IsSuccess ? onSuccess(_value!) : onError(_error!);
|
||||
|
||||
/// <summary>
|
||||
/// 如果成功,执行一个动作。
|
||||
/// </summary>
|
||||
public void IfSuccess(Action<TOk> action)
|
||||
{
|
||||
if (IsSuccess) action(_value!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 如果失败,执行一个动作。
|
||||
/// </summary>
|
||||
public void IfFailure(Action<TErr> action)
|
||||
{
|
||||
if (IsFailure) action(_error!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Result<T, E> 转换为 Result<T2, E>,通过映射成功值。
|
||||
/// </summary>
|
||||
public Result<T2, TErr> Map<T2>(Func<TOk, T2> mapper)
|
||||
=> IsSuccess ? Result<T2, TErr>.Ok(mapper(_value!)) : Result<T2, TErr>.Err(_error!);
|
||||
|
||||
/// <summary>
|
||||
/// 将 Result<T, E> 转换为 Result<T, E2>,通过映射错误值。
|
||||
/// </summary>
|
||||
public Result<TOk, TErr2> MapError<TErr2>(Func<TErr, TErr2> mapper)
|
||||
=> IsSuccess ? Result<TOk, TErr2>.Ok(_value!) : Result<TOk, TErr2>.Err(mapper(_error!));
|
||||
|
||||
/// <summary>
|
||||
/// 如果成功,使用函数返回另一个 Result,实现链式调用。
|
||||
/// 类似于 Rust 的 and_then。
|
||||
/// </summary>
|
||||
public Result<T2, TErr> Bind<T2>(Func<TOk, Result<T2, TErr>> mapper)
|
||||
=> IsSuccess ? mapper(_value!) : Result<T2, TErr>.Err(_error!);
|
||||
|
||||
/// <summary>
|
||||
/// 如果失败,使用函数返回另一个 Result。
|
||||
/// 类似于 Rust 的 or_else。
|
||||
/// </summary>
|
||||
public Result<TOk, TErr2> BindError<TErr2>(Func<TErr, Result<TOk, TErr2>> mapper)
|
||||
=> IsFailure ? mapper(_error!) : Result<TOk, TErr2>.Ok(_value!);
|
||||
|
||||
/// <summary>
|
||||
/// 如果失败,提供一个默认值。
|
||||
/// </summary>
|
||||
public TOk OrElse(TOk defaultValue) => IsSuccess ? _value! : defaultValue;
|
||||
|
||||
/// <summary>
|
||||
/// 如果失败,提供一个函数生成默认值。
|
||||
/// </summary>
|
||||
public TOk OrElse(Func<TOk> defaultValueFactory) => IsSuccess ? _value! : defaultValueFactory();
|
||||
|
||||
/// <summary>
|
||||
/// 如果失败,抛出异常。
|
||||
/// </summary>
|
||||
public TOk Expect(string message) => IsSuccess ? _value! : throw new InvalidOperationException(message);
|
||||
|
||||
/// <summary>
|
||||
/// 如果失败,抛出指定的异常。
|
||||
/// </summary>
|
||||
public TOk UnwrapOrThrow<TException>(Func<TErr, TException> exceptionFactory) where TException : Exception
|
||||
=> IsSuccess ? _value! : throw exceptionFactory(_error!);
|
||||
|
||||
// 重写 ToString 用于调试
|
||||
private string _DebuggerDisplay => IsSuccess ? $"Ok({_value})" : $"Err({_error})";
|
||||
|
||||
public override string ToString() => _DebuggerDisplay;
|
||||
|
||||
// 重写 Equals 和 GetHashCode 以支持比较
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not Result<TOk, TErr> other) return false;
|
||||
if (IsSuccess != other.IsSuccess) return false;
|
||||
return IsSuccess
|
||||
? EqualityComparer<TOk>.Default.Equals(_value, other._value)
|
||||
: EqualityComparer<TErr>.Default.Equals(_error, other._error);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(IsSuccess, _value, _error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Utils.Encryption;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using PlainToolkit.CngProtectedData;
|
||||
using DataProtectionScope = System.Security.Cryptography.DataProtectionScope;
|
||||
using CngDataProtectionScope = PlainToolkit.CngProtectedData.DataProtectionScope;
|
||||
|
||||
|
||||
namespace PCL.Core.Utils.Secret;
|
||||
|
||||
public static class EncryptHelper
|
||||
{
|
||||
private static readonly byte[] Key = "PCL CE Encryption Key"u8.ToArray();
|
||||
public static (IEncryptionProvider Provider, uint Version) DefaultProvider => _DefaultProvider.Value;
|
||||
private static readonly Lazy<(IEncryptionProvider Provider, uint Version)> _DefaultProvider = new(_SelectBestEncryption);
|
||||
|
||||
private static (IEncryptionProvider Provider, uint Version) _SelectBestEncryption()
|
||||
{
|
||||
var aesHardwareSupport = System.Runtime.Intrinsics.X86.Aes.IsSupported ||
|
||||
System.Runtime.Intrinsics.Arm.Aes.IsSupported;
|
||||
if (aesHardwareSupport && AesGcmProvider.Instance.IsSupported) return (AesGcmProvider.Instance, 2);
|
||||
if (ChaCha20Poly1305Provider.Instance.IsSupported) return (ChaCha20Poly1305Provider.Instance, 1);
|
||||
return (ChaCha20SoftwareProvider.Instance, 0);
|
||||
}
|
||||
|
||||
public static string SecretEncrypt(string? data)
|
||||
{
|
||||
if (data.IsNullOrEmpty()) return string.Empty;
|
||||
var rawData = Encoding.UTF8.GetBytes(data);
|
||||
|
||||
return Convert.ToBase64String(EncryptionData.ToBytes(new EncryptionData
|
||||
{ Version = DefaultProvider.Version, Data = DefaultProvider.Provider.Encrypt(rawData, EncryptionKey) }));
|
||||
}
|
||||
|
||||
public static string SecretDecrypt(string? data)
|
||||
{
|
||||
if (data.IsNullOrEmpty()) return string.Empty;
|
||||
var rawData = Convert.FromBase64String(data);
|
||||
Exception? decryptError;
|
||||
if (EncryptionData.IsValid(rawData))
|
||||
{
|
||||
try
|
||||
{
|
||||
var encryptionData = EncryptionData.FromBytes(rawData);
|
||||
IEncryptionProvider provider = encryptionData.Version switch
|
||||
{
|
||||
0 => ChaCha20SoftwareProvider.Instance,
|
||||
1 => ChaCha20Poly1305Provider.Instance,
|
||||
2 => AesGcmProvider.Instance,
|
||||
_ => throw new NotSupportedException("Unsupported encryption version")
|
||||
};
|
||||
var decryptedData = provider.Decrypt(encryptionData.Data, EncryptionKey);
|
||||
return Encoding.UTF8.GetString(decryptedData);
|
||||
}
|
||||
catch (Exception ex) { decryptError = ex; }
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
#pragma warning disable CS0612,CS0618 // Type or member is obsolete
|
||||
var decryptedData = AesCbcProvider.Instance.Decrypt(rawData, Encoding.UTF8.GetBytes(IdentifyOld.EncryptKey));
|
||||
#pragma warning restore CS0612,CS0618 // Type or member is obsolete
|
||||
return Encoding.UTF8.GetString(decryptedData);
|
||||
}
|
||||
catch (Exception ex) { decryptError = ex; }
|
||||
}
|
||||
|
||||
throw new Exception($"Unknown Encryption data, the data may broken", decryptError);
|
||||
}
|
||||
|
||||
#region "加密存储信息数据"
|
||||
|
||||
|
||||
public struct EncryptionData
|
||||
{
|
||||
public uint Version;
|
||||
public byte[] Data;
|
||||
|
||||
private const uint MagicNumber = 0x454E4321;
|
||||
|
||||
public static EncryptionData FromBase64(string base64)
|
||||
{
|
||||
return FromBytes(Convert.FromBase64String(base64));
|
||||
}
|
||||
|
||||
public static EncryptionData FromBytes(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
// 0 - 4 MagicNumber | 4 - 8 version || 8 - 12 bytes rData length | n bytes rData
|
||||
if (bytes.Length < 12)
|
||||
throw new ArgumentException("No enough data for EncryptionData", nameof(bytes));
|
||||
|
||||
if (BinaryPrimitives.ReadUInt32BigEndian(bytes[..4]) != MagicNumber)
|
||||
throw new ArgumentException("Unknown data for EncryptionData", nameof(bytes));
|
||||
|
||||
var dataLength = BinaryPrimitives.ReadInt32BigEndian(bytes[8..12]);
|
||||
if (dataLength > bytes.Length - 12)
|
||||
throw new ArgumentException("No enough data for EncryptionData", nameof(bytes));
|
||||
if (dataLength < 0)
|
||||
throw new ArgumentException("Invalid data length for EncryptionData", nameof(bytes));
|
||||
|
||||
var rData = bytes[12..(12 + dataLength)];
|
||||
|
||||
return new EncryptionData
|
||||
{
|
||||
Version = BinaryPrimitives.ReadUInt32BigEndian(bytes[4..8]),
|
||||
Data = rData.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public static byte[] ToBytes(EncryptionData encryptionData)
|
||||
{
|
||||
var length = 12 + encryptionData.Data.Length;
|
||||
var bytes = new byte[length];
|
||||
var bytesSpan = bytes.AsSpan();
|
||||
BinaryPrimitives.WriteUInt32BigEndian(bytesSpan[..4], MagicNumber);
|
||||
BinaryPrimitives.WriteUInt32BigEndian(bytesSpan[4..8], encryptionData.Version);
|
||||
BinaryPrimitives.WriteInt32BigEndian(bytesSpan[8..12], encryptionData.Data.Length);
|
||||
encryptionData.Data.CopyTo(bytesSpan[12..]);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static bool IsValid(ReadOnlySpan<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
return data.Length >= 12 && BinaryPrimitives.ReadUInt32BigEndian(data[..4]) == MagicNumber;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region "密钥存储和获取"
|
||||
|
||||
internal static byte[] EncryptionKey { get => _EncryptionKey.Value; }
|
||||
private static readonly Lazy<byte[]> _EncryptionKey = new(_GetKey);
|
||||
|
||||
private static byte[] _GetKey()
|
||||
{
|
||||
var keyFile = Path.Combine(Paths.SharedData, "UserKey.bin");
|
||||
if (File.Exists(keyFile))
|
||||
{
|
||||
var buf = File.ReadAllBytes(keyFile);
|
||||
var data = EncryptionData.FromBytes(buf);
|
||||
return data.Version switch
|
||||
{
|
||||
1 => ProtectedData.Unprotect(data.Data, Key, DataProtectionScope.CurrentUser),
|
||||
2 => CngProtectedData.Unprotect(data.Data, Key, CngDataProtectionScope.CurrentUser),
|
||||
_ => throw new NotSupportedException("Unsupported key version")
|
||||
};
|
||||
}
|
||||
|
||||
var randomKey = new byte[32];
|
||||
RandomNumberGenerator.Fill(randomKey);
|
||||
var storeData = EncryptionData.ToBytes(new EncryptionData
|
||||
{
|
||||
Version = 2,
|
||||
Data = CngProtectedData.Protect(randomKey, Key, CngDataProtectionScope.CurrentUser)
|
||||
});
|
||||
|
||||
var tmpFile = $"{keyFile}.tmp{RandomUtils.NextInt(10000, 99999)}";
|
||||
using (var fs = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
|
||||
{
|
||||
fs.Write(storeData);
|
||||
fs.Flush(true);
|
||||
}
|
||||
|
||||
File.Move(tmpFile, keyFile, true);
|
||||
|
||||
return randomKey;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Management;
|
||||
using System.Text;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using PCL.Core.Utils.Hash;
|
||||
|
||||
namespace PCL.Core.Utils.Secret;
|
||||
|
||||
public class Identify
|
||||
{
|
||||
public static byte[] RawId { get => field ??= _GetRawId(); } = null!;
|
||||
public static string LauncherId { get => field ??= _getLauncherId(); } = null!;
|
||||
|
||||
private static byte[] _GetRawId()
|
||||
{
|
||||
var code = new StringBuilder();
|
||||
try
|
||||
{
|
||||
code.Append("UUID:").Append(_GetWmiProperty("Win32_ComputerSystemProduct", "UUID"))
|
||||
.Append("|MB_Prod:").Append(_GetWmiProperty("Win32_BaseBoard", "Product"))
|
||||
.Append("|MB_SN:").Append(_GetWmiProperty("Win32_BaseBoard", "SerialNumber"))
|
||||
.Append("|CPU:").Append(_GetWmiProperty("Win32_Processor", "ProcessorId"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", "获取设备基础信息失败");
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetBytes(SHA512Provider.Instance.ComputeHash(code.ToString()).ToHexString());
|
||||
}
|
||||
|
||||
private static string _GetWmiProperty(string className, string propertyName)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var searcher =
|
||||
new ManagementObjectSearcher($"SELECT {propertyName} FROM {className}");
|
||||
using var results = searcher.Get();
|
||||
foreach (var obj in results)
|
||||
{
|
||||
if (obj[propertyName] is not null)
|
||||
return (obj[propertyName].ToString() ?? string.Empty).Trim();
|
||||
}
|
||||
}
|
||||
catch { /* Ignore */ }
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string _getLauncherId()
|
||||
{
|
||||
try
|
||||
{
|
||||
var prefix = "PCL-CE|"u8.ToArray();
|
||||
var ctx = RawId;
|
||||
var suffix = "|LauncherId"u8.ToArray();
|
||||
|
||||
var buffer = new byte[prefix.Length + ctx.Length + suffix.Length];
|
||||
var bufferSpan = buffer.AsSpan();
|
||||
prefix.CopyTo(bufferSpan[..prefix.Length]);
|
||||
ctx.CopyTo(bufferSpan.Slice(prefix.Length, ctx.Length));
|
||||
suffix.CopyTo(bufferSpan.Slice(prefix.Length + ctx.Length, suffix.Length));
|
||||
|
||||
Array.Clear(ctx);
|
||||
var sample = SHA512Provider.Instance.ComputeHash(bufferSpan).ToHexString();
|
||||
bufferSpan.Clear();
|
||||
|
||||
// 16 in length, 8 bytes, 64 bits, enough for us
|
||||
return sample.Substring(64, 16)
|
||||
.ToUpper()
|
||||
.Insert(4, "-")
|
||||
.Insert(9, "-")
|
||||
.Insert(14, "-");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", "无法获取识别码");
|
||||
return "PCL2-CECE-GOOD-2025";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Management;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.Hash;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Utils.Secret;
|
||||
|
||||
[Obsolete("Use PCL.Core.Utils.Secret.Identify instead")]
|
||||
public static class IdentifyOld
|
||||
{
|
||||
private const string DefaultRawCode = "B09675A9351CBD1FD568056781FE3966DD936CC9B94E51AB5CF67EEB7E74C075";
|
||||
private static readonly Lazy<string?> _LazyCpuId = new(_GetCpuId);
|
||||
|
||||
private static readonly Lazy<string> _LazyRawCode =
|
||||
new(() => CpuId is null ? DefaultRawCode : SHA256Provider.Instance.ComputeHash(CpuId).ToHexString().ToUpper());
|
||||
|
||||
private static readonly Lazy<string> _LaunchId = new(_GetLaunchId);
|
||||
|
||||
private static readonly Lazy<string> _LazyEncryptKey =
|
||||
new(() => SHA512Provider.Instance.ComputeHash(RawCode).ToHexString().Substring(4, 32).ToUpper());
|
||||
|
||||
public static string GetGuid() => Guid.NewGuid().ToString();
|
||||
[Obsolete]
|
||||
public static string? CpuId => _LazyCpuId.Value;
|
||||
[Obsolete]
|
||||
public static string RawCode => _LazyRawCode.Value;
|
||||
[Obsolete]
|
||||
public static string LaunchId => _LaunchId.Value;
|
||||
[Obsolete]
|
||||
public static string EncryptKey => _LazyEncryptKey.Value;
|
||||
|
||||
private static string? _GetCpuId()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var searcher = new ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor");
|
||||
using var collection = searcher.Get();
|
||||
|
||||
foreach (var item in collection)
|
||||
{
|
||||
try
|
||||
{
|
||||
return item["ProcessorId"]?.ToString();
|
||||
}
|
||||
catch (ManagementException ex)
|
||||
{
|
||||
LogWrapper.Warn("Identify", $"WMI属性读取失败: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
item.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
LogWrapper.Warn("Identify", "未找到有效的CPU ID");
|
||||
return null;
|
||||
}
|
||||
catch (ManagementException ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", $"WMI查询失败");
|
||||
}
|
||||
catch (System.Runtime.InteropServices.COMException ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", $"COM异常,请确保WMI服务正在运行");
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", "访问被拒绝,请以管理员权限运行");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", $"意外的系统异常");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetMachineId(string randomId)
|
||||
{
|
||||
return SHA512Provider.Instance.ComputeHash($"{randomId}|{CpuId}").ToHexString().ToUpper();
|
||||
}
|
||||
|
||||
private static string _GetLaunchId()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(States.System.LaunchUuid)) States.System.LaunchUuid = GetGuid();
|
||||
var hashCode = GetMachineId(States.System.LaunchUuid)
|
||||
.Substring(6, 16)
|
||||
.Insert(4, "-")
|
||||
.Insert(9, "-")
|
||||
.Insert(14, "-");
|
||||
return hashCode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", "无法获取短识别码");
|
||||
return "PCL2-CECE-GOOD-2025";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
[Serializable]
|
||||
public class SemVer(int major, int minor, int patch, string? prerelease = null, string? buildMetadata = null)
|
||||
: IComparable<SemVer>, IEquatable<SemVer>
|
||||
{
|
||||
public int Major => major;
|
||||
public int Minor => minor;
|
||||
public int Patch => patch;
|
||||
public string Prerelease => prerelease ?? string.Empty;
|
||||
public string BuildMetadata => buildMetadata ?? string.Empty;
|
||||
|
||||
public static SemVer Parse(string version)
|
||||
{
|
||||
if (!TryParse(version, out var result))
|
||||
{
|
||||
throw new ArgumentException("Invalid semantic version format");
|
||||
}
|
||||
return result!;
|
||||
}
|
||||
|
||||
public static bool TryParse(string version, out SemVer? result)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var match = RegexPatterns.SemVer.Match(version);
|
||||
if (!match.Success)
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = _CreateFromMatch(match);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static SemVer _CreateFromMatch(Match match)
|
||||
{
|
||||
var major = int.Parse(match.Groups["major"].Value);
|
||||
var minor = int.Parse(match.Groups["minor"].Value);
|
||||
var patch = int.Parse(match.Groups["patch"].Value);
|
||||
var prerelease = match.Groups["prerelease"].Value;
|
||||
var build = match.Groups["build"].Value;
|
||||
|
||||
return new SemVer(major, minor, patch, prerelease, build);
|
||||
}
|
||||
|
||||
public int CompareTo(SemVer? other)
|
||||
{
|
||||
if (other is null) return 1;
|
||||
|
||||
var compare = Major.CompareTo(other.Major);
|
||||
if (compare != 0) return compare;
|
||||
|
||||
compare = Minor.CompareTo(other.Minor);
|
||||
if (compare != 0) return compare;
|
||||
|
||||
compare = Patch.CompareTo(other.Patch);
|
||||
if (compare != 0) return compare;
|
||||
|
||||
return _ComparePrerelease(Prerelease, other.Prerelease);
|
||||
}
|
||||
|
||||
private static int _ComparePrerelease(string a, string b)
|
||||
{
|
||||
if (string.Equals(a, b, StringComparison.Ordinal))
|
||||
return 0;
|
||||
|
||||
// 正式版优先级高于预发布版
|
||||
if (string.IsNullOrEmpty(a)) return 1;
|
||||
if (string.IsNullOrEmpty(b)) return -1;
|
||||
|
||||
var identifiersA = a.Split('.');
|
||||
var identifiersB = b.Split('.');
|
||||
|
||||
var minLength = Math.Min(identifiersA.Length, identifiersB.Length);
|
||||
for (var i = 0; i < minLength; i++)
|
||||
{
|
||||
var idA = identifiersA[i];
|
||||
var idB = identifiersB[i];
|
||||
|
||||
var aIsNumeric = int.TryParse(idA, out var numA);
|
||||
var bIsNumeric = int.TryParse(idB, out var numB);
|
||||
|
||||
int result;
|
||||
if (aIsNumeric && bIsNumeric)
|
||||
{
|
||||
result = numA.CompareTo(numB);
|
||||
}
|
||||
else if (aIsNumeric || bIsNumeric)
|
||||
{
|
||||
// 数值标识符比非数值标识符优先级低
|
||||
result = aIsNumeric ? -1 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = string.Compare(idA, idB, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (result != 0)
|
||||
return result;
|
||||
}
|
||||
|
||||
return identifiersA.Length.CompareTo(identifiersB.Length);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var version = $"{Major}.{Minor}.{Patch}";
|
||||
|
||||
if (!string.IsNullOrEmpty(Prerelease))
|
||||
version += $"-{Prerelease}";
|
||||
|
||||
if (!string.IsNullOrEmpty(BuildMetadata))
|
||||
version += $"+{BuildMetadata}";
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
// 实现相等性比较
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return Equals(obj as SemVer);
|
||||
}
|
||||
|
||||
public bool Equals(SemVer? other)
|
||||
{
|
||||
return other is not null &&
|
||||
Major == other.Major &&
|
||||
Minor == other.Minor &&
|
||||
Patch == other.Patch &&
|
||||
string.Equals(Prerelease, other.Prerelease, StringComparison.Ordinal) &&
|
||||
string.Equals(BuildMetadata, other.BuildMetadata, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hash = 17;
|
||||
hash = hash * 23 + Major.GetHashCode();
|
||||
hash = hash * 23 + Minor.GetHashCode();
|
||||
hash = hash * 23 + Patch.GetHashCode();
|
||||
hash = hash * 23 + Prerelease.GetHashCode();
|
||||
hash = hash * 23 + BuildMetadata.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
// 运算符重载
|
||||
public static bool operator ==(SemVer? left, SemVer? right)
|
||||
{
|
||||
if (ReferenceEquals(left, right)) return true;
|
||||
if (left is null || right is null) return false;
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(SemVer? left, SemVer? right) => !(left == right);
|
||||
public static bool operator <(SemVer? left, SemVer? right) =>
|
||||
left is null ? right is not null : left.CompareTo(right) < 0;
|
||||
public static bool operator >(SemVer? left, SemVer? right) =>
|
||||
left is not null && left.CompareTo(right) > 0;
|
||||
public static bool operator <=(SemVer? left, SemVer? right) =>
|
||||
left is null || left.CompareTo(right) <= 0;
|
||||
public static bool operator >=(SemVer? left, SemVer? right) =>
|
||||
left is null ? right is null : left.CompareTo(right) >= 0;
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
/// <summary>
|
||||
/// 提供文本相似度搜索功能。
|
||||
/// </summary>
|
||||
public static class SimilaritySearch {
|
||||
//region: SearchSimilarity Constants
|
||||
// 这些常量来自原始算法,用于调整评分权重。
|
||||
|
||||
/// <summary>
|
||||
/// 匹配长度权重计算的指数底数。越长匹配的得分呈指数增长。
|
||||
/// </summary>
|
||||
private const double LengthPowerBase = 1.4;
|
||||
|
||||
/// <summary>
|
||||
/// 匹配长度权重计算的偏移量。
|
||||
/// </summary>
|
||||
private const double LengthWeightOffset = 3.6;
|
||||
|
||||
/// <summary>
|
||||
/// 匹配位置邻近度的奖励因子。
|
||||
/// </summary>
|
||||
private const double PositionBonusFactor = 0.3;
|
||||
|
||||
/// <summary>
|
||||
/// 计算位置奖励时,允许的最大位置差异。
|
||||
/// </summary>
|
||||
private const int MaxPositionBonusDistance = 3;
|
||||
|
||||
/// <summary>
|
||||
/// 源文本长度对最终得分的影响因子。
|
||||
/// </summary>
|
||||
private const double SourceLengthImpactFactor = 3.0;
|
||||
|
||||
/// <summary>
|
||||
/// 源文本长度惩罚的平滑参数。
|
||||
/// </summary>
|
||||
private const double SourceLengthSmoothing = 15.0;
|
||||
|
||||
/// <summary>
|
||||
/// 对长度为1的查询的得分奖励。
|
||||
/// </summary>
|
||||
private const double ShortQueryBonusFactor = 2.0;
|
||||
//endregion
|
||||
|
||||
/// <summary>
|
||||
/// 获取搜索文本的相似度。(已优化)
|
||||
/// </summary>
|
||||
/// <param name="source">被搜索的长内容。</param>
|
||||
/// <param name="query">用户输入的搜索文本。</param>
|
||||
/// <returns>一个表示相似度的 double 值。</returns>
|
||||
private static double _SearchSimilarity(string source, string query) {
|
||||
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(query)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// 预处理:转为小写并移除空格,然后转换为 ReadOnlySpan 以提高性能。
|
||||
var sourceSpan = source.ToLower().Replace(" ", "").AsSpan();
|
||||
var querySpan = query.ToLower().Replace(" ", "").AsSpan();
|
||||
|
||||
if (sourceSpan.IsEmpty || querySpan.IsEmpty) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// 使用布尔数组来跟踪源文本中已被匹配的字符,避免重复分配字符串。
|
||||
var usedSourceIndices = new bool[sourceSpan.Length];
|
||||
double weightedLengthSum = 0;
|
||||
var queryIndex = 0;
|
||||
|
||||
while (queryIndex < querySpan.Length) {
|
||||
var longestMatchLength = 0;
|
||||
var bestMatchSourceStartIndex = -1;
|
||||
|
||||
// 寻找以当前 queryIndex 为起点的最长匹配子串
|
||||
for (var sourceIndex = 0; sourceIndex < sourceSpan.Length; sourceIndex++) {
|
||||
var currentMatchLength = 0;
|
||||
// 计算从当前 sourceIndex 和 queryIndex 开始的匹配长度
|
||||
// 同时确保源字符未被使用
|
||||
while ((queryIndex + currentMatchLength) < querySpan.Length &&
|
||||
(sourceIndex + currentMatchLength) < sourceSpan.Length &&
|
||||
!usedSourceIndices[sourceIndex + currentMatchLength] &&
|
||||
sourceSpan[sourceIndex + currentMatchLength] == querySpan[queryIndex + currentMatchLength]) {
|
||||
currentMatchLength++;
|
||||
}
|
||||
|
||||
// 卫语句:如果当前匹配不比最长匹配更长,则直接继续下一次循环
|
||||
if (currentMatchLength <= longestMatchLength) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果满足条件,更新最长匹配信息
|
||||
longestMatchLength = currentMatchLength;
|
||||
bestMatchSourceStartIndex = sourceIndex;
|
||||
}
|
||||
|
||||
if (longestMatchLength > 0) {
|
||||
// 标记源中对应的字符为已使用
|
||||
for (var i = 0; i < longestMatchLength; i++) {
|
||||
usedSourceIndices[bestMatchSourceStartIndex + i] = true;
|
||||
}
|
||||
|
||||
// 根据长度加成
|
||||
var incrementWeight = Math.Pow(LengthPowerBase, 3 + longestMatchLength) - LengthWeightOffset;
|
||||
|
||||
// 根据位置加成
|
||||
var positionDifference = Math.Abs(queryIndex - bestMatchSourceStartIndex);
|
||||
var positionBonus = 1.0 + PositionBonusFactor * Math.Max(0, MaxPositionBonusDistance - positionDifference);
|
||||
incrementWeight *= positionBonus;
|
||||
|
||||
weightedLengthSum += incrementWeight;
|
||||
}
|
||||
|
||||
// 推进查询指针
|
||||
queryIndex += Math.Max(1, longestMatchLength);
|
||||
}
|
||||
|
||||
// 计算最终结果:(加权匹配总和 / 查询长度) * 源长度影响比例 * 短查询奖励
|
||||
var normalizedScore = weightedLengthSum / querySpan.Length;
|
||||
var sourceLengthPenalty = SourceLengthImpactFactor / Math.Sqrt(sourceSpan.Length + SourceLengthSmoothing);
|
||||
var shortQueryBonus = query.Length == 1 ? ShortQueryBonusFactor : 1.0;
|
||||
|
||||
return normalizedScore * sourceLengthPenalty * shortQueryBonus;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取多段文本加权后的相似度。
|
||||
/// </summary>
|
||||
private static double _SearchSimilarityWeighted(List<KeyValuePair<string, double>> source, string query) {
|
||||
if (source.Count == 0) return 0.0;
|
||||
|
||||
var totalWeight = source.Sum(pair => pair.Value);
|
||||
if (totalWeight == 0) return 0.0;
|
||||
|
||||
var weightedSum = source.Sum(pair => _SearchSimilarity(pair.Key, query) * pair.Value);
|
||||
|
||||
return weightedSum / totalWeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查一个条目的所有搜索源是否完全匹配查询的所有部分。
|
||||
/// </summary>
|
||||
private static bool _IsAbsoluteMatch(IEnumerable<KeyValuePair<string, double>> searchSources, string[] queryParts) {
|
||||
// 预处理搜索源:转小写并移除空格,避免在循环中重复操作
|
||||
var processedSources = searchSources
|
||||
.Select(s => s.Key.Replace(" ", "").ToLower())
|
||||
.ToList();
|
||||
|
||||
// 必须所有查询词都在至少一个源中找到
|
||||
return queryParts.All(queryPart => processedSources.Any(source => source.Contains(queryPart)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进行多段文本加权搜索,获取相似度较高的数项结果。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">搜索条目的泛型类型。</typeparam>
|
||||
/// <param name="entries">要搜索的条目列表。</param>
|
||||
/// <param name="query">用户输入的查询字符串。</param>
|
||||
/// <param name="maxBlurCount">返回的最大模糊结果数。</param>
|
||||
/// <param name="minBlurSimilarity">返回结果要求的最低相似度。</param>
|
||||
/// <returns>排序和过滤后的搜索结果列表。</returns>
|
||||
public static List<SearchEntry<T>> Search<T>(
|
||||
List<SearchEntry<T>> entries,
|
||||
string query,
|
||||
int maxBlurCount = 5,
|
||||
double minBlurSimilarity = 0.1) {
|
||||
if (entries.Count == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query)) {
|
||||
return entries; // 或者返回空列表,取决于业务需求
|
||||
}
|
||||
|
||||
var queryParts = query.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(q => q.ToLower())
|
||||
.ToArray();
|
||||
|
||||
if (queryParts.Length == 0) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
// 1. 计算每个条目的相似度和是否完全匹配
|
||||
foreach (var entry in entries) {
|
||||
entry.Similarity = _SearchSimilarityWeighted(entry.SearchSource, query);
|
||||
entry.AbsoluteRight = _IsAbsoluteMatch(entry.SearchSource, queryParts);
|
||||
}
|
||||
|
||||
// 2. 排序:完全匹配的优先,其次按相似度降序
|
||||
var sortedEntries = entries
|
||||
.OrderByDescending(e => e.AbsoluteRight)
|
||||
.ThenByDescending(e => e.Similarity);
|
||||
|
||||
// 3. 构建最终结果列表
|
||||
var sortedEntriesList = sortedEntries.ToList();
|
||||
|
||||
var absoluteMatches = sortedEntriesList.Where(e => e.AbsoluteRight);
|
||||
|
||||
var blurMatches = sortedEntriesList
|
||||
.Where(e => !e.AbsoluteRight && e.Similarity >= minBlurSimilarity)
|
||||
.Take(maxBlurCount);
|
||||
|
||||
return absoluteMatches.Concat(blurMatches).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于搜索的项目。使用主构造函数 (C# 12+)。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">该项目对应的源数据类型。</typeparam>
|
||||
public class SearchEntry<T>(T item, List<KeyValuePair<string, double>> searchSource) {
|
||||
/// <summary>
|
||||
/// 该项目对应的源数据。
|
||||
/// </summary>
|
||||
public T Item { get; set; } = item;
|
||||
|
||||
/// <summary>
|
||||
/// 该项目用于搜索的源文本及其权重。
|
||||
/// </summary>
|
||||
public List<KeyValuePair<string, double>> SearchSource { get; set; } = searchSource;
|
||||
|
||||
/// <summary>
|
||||
/// 计算出的相似度。
|
||||
/// </summary>
|
||||
public double Similarity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否完全匹配。
|
||||
/// 如果查询的所有部分都能在搜索源中找到,则为 true。
|
||||
/// </summary>
|
||||
public bool AbsoluteRight { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
public class StringStream : Stream
|
||||
{
|
||||
private readonly MemoryStream _innerStream;
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定编码初始化 StringStream。
|
||||
/// </summary>
|
||||
public StringStream(string source, Encoding? encoding = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
encoding ??= Encoding.UTF8;
|
||||
|
||||
var buffer = encoding.GetBytes(source);
|
||||
_innerStream = new MemoryStream(buffer);
|
||||
}
|
||||
|
||||
public override bool CanRead => _innerStream.CanRead;
|
||||
public override bool CanSeek => _innerStream.CanSeek;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => _innerStream.Length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _innerStream.Position;
|
||||
set => _innerStream.Position = value;
|
||||
}
|
||||
|
||||
public override void Flush() { /* 只读流 无需实现 */ }
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException("StringStream 是只读流,不支持 SetLength。");
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException("StringStream 是只读流,不支持 Write。");
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing) _innerStream.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Threading;
|
||||
|
||||
// 使用 AI 生成的代码
|
||||
// 时间: 2025/9/2
|
||||
// 模型: GPT-5
|
||||
|
||||
/// <summary>
|
||||
/// 一个带配额(Permit)的 <see cref="System.Threading.AutoResetEvent"/> 变体。
|
||||
/// 支持一次性释放多个等待任务。
|
||||
/// 类似于 <see cref="System.Threading.SemaphoreSlim"/>,但语义更接近 AutoResetEvent。
|
||||
/// </summary>
|
||||
public sealed class AsyncCountResetEvent : IDisposable
|
||||
{
|
||||
private readonly Queue<TaskCompletionSource<bool>> _waiters = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 当前剩余的配额数。如果 > 0,新的等待者会立即通过。
|
||||
/// </summary>
|
||||
private int _permits;
|
||||
|
||||
/// <summary>
|
||||
/// 标记当前对象是否已释放。
|
||||
/// </summary>
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 析构函数。
|
||||
/// </summary>
|
||||
~AsyncCountResetEvent()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待一个信号。当信号可用时返回完成的 <see cref="Task"/>。
|
||||
/// 如果没有信号,则进入队列等待。
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// 一个 <see cref="Task"/>,表示等待操作。
|
||||
/// 如果对象被释放,则返回的 Task 会异常结束。
|
||||
/// </returns>
|
||||
public Task WaitAsync()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, nameof(AsyncCountResetEvent));
|
||||
|
||||
if (_permits > 0)
|
||||
{
|
||||
_permits--;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_waiters.Enqueue(tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放一个或多个信号,让等待者继续执行。
|
||||
/// </summary>
|
||||
/// <param name="count">要释放的配额数量,默认为 1。</param>
|
||||
public void Set(int count = 1)
|
||||
{
|
||||
if (count <= 0) return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
while (count > 0 && _waiters.Count > 0)
|
||||
{
|
||||
var tcs = _waiters.Dequeue();
|
||||
tcs.TrySetResult(true);
|
||||
count--;
|
||||
}
|
||||
|
||||
// 如果没有等待者,就累积到配额里
|
||||
_permits += count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前对象。
|
||||
/// 会让所有等待中的任务异常完成。
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
while (_waiters.Count > 0)
|
||||
{
|
||||
var tcs = _waiters.Dequeue();
|
||||
tcs.TrySetException(new ObjectDisposedException(nameof(AsyncCountResetEvent)));
|
||||
}
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 可重置的异步延时器。在指定延时后执行异步任务并等待下一次重置后重复该逻辑,指定延时未到达时重置将会重新开始计时。
|
||||
/// <p>实例创建后并不会立即开始计时,而是等待第一次 <see cref="ResetAsync"/>
|
||||
/// 调用。因此,若有特殊需求,请不要忘了创建实例后调用一次 <see cref="ResetAsync"/>。</p>
|
||||
/// </summary>
|
||||
public class AsyncDebounce(CancellationToken cancelToken = default) : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行延迟。
|
||||
/// </summary>
|
||||
public required TimeSpan Delay { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 异步任务实例。
|
||||
/// </summary>
|
||||
public required Func<Task> ScheduledTask { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 指示本次延迟任务是否已经完成。
|
||||
/// </summary>
|
||||
public bool IsCurrentTaskCompleted { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指示本次延迟任务是否正在运行。
|
||||
/// </summary>
|
||||
public bool IsCurrentTaskRunning => _currentTask is not null;
|
||||
|
||||
private Task? _currentTask;
|
||||
private Task? _worker; // 跟踪最近一次 worker
|
||||
private CancellationTokenSource? _currentDelayCts;
|
||||
private readonly CancellationTokenSource _cts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken);
|
||||
private readonly object _resetLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 重置延时。
|
||||
/// </summary>
|
||||
public async Task ResetAsync()
|
||||
{
|
||||
IsCurrentTaskCompleted = false;
|
||||
|
||||
CancellationTokenSource? capturedCts;
|
||||
Task? runningToAwait;
|
||||
|
||||
#pragma warning disable VSTHRD103 // 禁用检查 避免智障警告
|
||||
lock (_resetLock)
|
||||
{
|
||||
// 只取消,不在这里 Dispose
|
||||
try { _currentDelayCts?.Cancel(); }
|
||||
catch(ObjectDisposedException) { /* ignored */ }
|
||||
|
||||
_currentDelayCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
|
||||
capturedCts = _currentDelayCts;
|
||||
|
||||
// 记录当前运行中的 ScheduledTask,稍后在锁外等待,避免重叠
|
||||
runningToAwait = _currentTask;
|
||||
|
||||
_worker = Task.Run(async () =>
|
||||
{
|
||||
try { await Task.Delay(Delay, capturedCts.Token).ConfigureAwait(false); }
|
||||
catch (OperationCanceledException) { return; }
|
||||
finally
|
||||
{
|
||||
// 仅由使用者自己释放,避免跨线程 Dispose 竞态
|
||||
// 注意:不要在这里释放 _cancelToken
|
||||
capturedCts.Dispose();
|
||||
}
|
||||
|
||||
// 身份校验,确保自己仍是“当前”那一次
|
||||
if (
|
||||
!ReferenceEquals(_currentDelayCts, capturedCts) ||
|
||||
capturedCts.IsCancellationRequested ||
|
||||
_cts.IsCancellationRequested
|
||||
) return;
|
||||
|
||||
if (_currentTask is not null) await _currentTask.ConfigureAwait(false);
|
||||
|
||||
var task = ScheduledTask();
|
||||
lock (_resetLock) _currentTask = task;
|
||||
|
||||
try
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_resetLock)
|
||||
{
|
||||
_currentTask = null;
|
||||
IsCurrentTaskCompleted = true;
|
||||
}
|
||||
}
|
||||
}, _cts.Token);
|
||||
}
|
||||
#pragma warning restore VSTHRD103
|
||||
|
||||
// 避免 ScheduledTask 并发
|
||||
if (runningToAwait is not null) await runningToAwait.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
try { _worker?.Wait(); } catch { /* ignored */ }
|
||||
_currentDelayCts?.Dispose();
|
||||
_cts.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Threading;
|
||||
|
||||
// Partly generated by gpt-5-mini (20250808)
|
||||
public sealed class AsyncManualResetEvent : IDisposable
|
||||
{
|
||||
private readonly object _syncLock = new();
|
||||
private TaskCompletionSource<bool> _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly ManualResetEventSlim _mre = new(false);
|
||||
private bool _disposed;
|
||||
|
||||
public AsyncManualResetEvent(bool initialState = false)
|
||||
{
|
||||
if (!initialState) return;
|
||||
_tcs.SetResult(true);
|
||||
_mre.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 事件是否已触发。
|
||||
/// </summary>
|
||||
public bool IsSet
|
||||
{
|
||||
get { lock (_syncLock) { return _tcs.Task.IsCompleted; } }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步等待。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">用于结束等待的取消信号</param>
|
||||
public Task WaitAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
TaskCompletionSource<bool> t;
|
||||
lock (_syncLock) { t = _tcs; }
|
||||
if (!cancellationToken.CanBeCanceled || t.Task.IsCompleted) return t.Task;
|
||||
return _WaitWithCancellationAsync(t.Task, cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task _WaitWithCancellationAsync(Task waitTask, CancellationToken ct)
|
||||
{
|
||||
var cancelTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
using (ct.Register(s => ((TaskCompletionSource<bool>)s!).TrySetResult(true), cancelTcs))
|
||||
{
|
||||
var completed = await Task.WhenAny(waitTask, cancelTcs.Task).ConfigureAwait(false);
|
||||
if (completed == cancelTcs.Task) ct.ThrowIfCancellationRequested();
|
||||
await waitTask.ConfigureAwait(false); // propagate exceptions if any
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步等待。
|
||||
/// </summary>
|
||||
public void Wait() => _mre.Wait();
|
||||
|
||||
/// <summary>
|
||||
/// 同步等待,并在超时后结束。
|
||||
/// </summary>
|
||||
/// <param name="millisecondsTimeout">等待超时的毫秒数</param>
|
||||
/// <returns>若已触发事件则为 <c>true</c>,否则为 <c>false</c></returns>
|
||||
public bool Wait(int millisecondsTimeout) => _mre.Wait(millisecondsTimeout);
|
||||
|
||||
/// <summary>
|
||||
/// 同步等待,并在超时后结束。
|
||||
/// </summary>
|
||||
/// <param name="timeout">等待超时</param>
|
||||
/// <returns>若已触发事件则为 <c>true</c>,否则为 <c>false</c></returns>
|
||||
public bool Wait(TimeSpan timeout) => _mre.Wait(timeout);
|
||||
|
||||
/// <summary>
|
||||
/// 同步等待,并传递用于结束等待的取消信号。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">用于结束等待的取消信号</param>
|
||||
public void Wait(CancellationToken cancellationToken) => _mre.Wait(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 触发事件。
|
||||
/// </summary>
|
||||
public void Set()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_tcs.TrySetResult(true); // Use TrySetResult to avoid exceptions on repeated Set
|
||||
_mre.Set();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置事件。
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_tcs.Task.IsCompleted) return; // already reset
|
||||
_tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_mre.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_mre.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Threading;
|
||||
|
||||
// Partly generated by o4-mini-high (20250709)
|
||||
|
||||
/// <summary>
|
||||
/// 使用两个线程池的调度器,分为 CPU 线程池和 IO 线程池,分别负责 CPU 密集型任务和 IO 密集型任务
|
||||
/// </summary>
|
||||
public class DualThreadPool
|
||||
{
|
||||
/// <summary>
|
||||
/// 两线程池分别计算的最大线程数
|
||||
/// </summary>
|
||||
public int MaxThread { get; }
|
||||
|
||||
private readonly TaskFactory _ioFactory;
|
||||
private readonly TaskFactory _cpuFactory;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 <see cref="DualThreadPool"/> 实例
|
||||
/// </summary>
|
||||
/// <param name="maxThread">参考 <see cref="MaxThread"/>,最小为 1</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">最大线程数小于 1</exception>
|
||||
public DualThreadPool(int maxThread)
|
||||
{
|
||||
if (maxThread < 1) throw new ArgumentOutOfRangeException(nameof(maxThread));
|
||||
|
||||
MaxThread = maxThread;
|
||||
|
||||
var ioScheduler = new LimitedConcurrencyLevelTaskScheduler(maxThread);
|
||||
var cpuScheduler = new LimitedConcurrencyLevelTaskScheduler(maxThread);
|
||||
var cancellationToken = _cts.Token;
|
||||
|
||||
// DenyChildAttach 防止子任务跑到外层 scheduler
|
||||
_ioFactory = new TaskFactory(
|
||||
cancellationToken,
|
||||
TaskCreationOptions.DenyChildAttach,
|
||||
TaskContinuationOptions.None,
|
||||
ioScheduler);
|
||||
|
||||
_cpuFactory = new TaskFactory(
|
||||
cancellationToken,
|
||||
TaskCreationOptions.DenyChildAttach,
|
||||
TaskContinuationOptions.None,
|
||||
cpuScheduler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交一段 IO 密集工作
|
||||
/// </summary>
|
||||
public Task QueueIo(Action work) => _ioFactory.StartNew(work);
|
||||
|
||||
/// <summary>
|
||||
/// 提交一段异步 IO 密集工作
|
||||
/// </summary>
|
||||
public Task QueueIo(Func<Task> work) => _ioFactory.StartNew(work).Unwrap();
|
||||
|
||||
/// <summary>
|
||||
/// 提交一段 CPU 密集工作
|
||||
/// </summary>
|
||||
public Task QueueCpu(Action work) => _cpuFactory.StartNew(work);
|
||||
|
||||
/// <summary>
|
||||
/// 提交一段异步 CPU 密集工作
|
||||
/// </summary>
|
||||
public Task QueueCpu(Func<Task> work) => _cpuFactory.StartNew(work).Unwrap();
|
||||
|
||||
/// <summary>
|
||||
/// 取消所有正在执行的工作
|
||||
/// </summary>
|
||||
public void CancelAll() => _cts.Cancel();
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Threading;
|
||||
|
||||
// Partly generated by o4-mini-high (20250709)
|
||||
|
||||
/// <summary>
|
||||
/// 允许限制最大并发度的 TaskScheduler
|
||||
/// </summary>
|
||||
public sealed class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static bool _currentThreadIsProcessingItems;
|
||||
|
||||
private readonly LinkedList<Task> _tasks = [];
|
||||
private int _delegatesQueuedOrRunning = 0;
|
||||
private readonly int _maxDegreeOfParallelism;
|
||||
|
||||
public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism)
|
||||
{
|
||||
if (maxDegreeOfParallelism < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism));
|
||||
_maxDegreeOfParallelism = maxDegreeOfParallelism;
|
||||
}
|
||||
|
||||
public override int MaximumConcurrencyLevel => _maxDegreeOfParallelism;
|
||||
|
||||
protected override IEnumerable<Task> GetScheduledTasks()
|
||||
{
|
||||
var lockTaken = false;
|
||||
try
|
||||
{
|
||||
Monitor.TryEnter(_tasks, ref lockTaken);
|
||||
if (lockTaken) return _tasks.ToArray();
|
||||
else throw new NotSupportedException();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockTaken) Monitor.Exit(_tasks);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void QueueTask(Task task)
|
||||
{
|
||||
lock (_tasks)
|
||||
{
|
||||
_tasks.AddLast(task);
|
||||
if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism)
|
||||
{
|
||||
_delegatesQueuedOrRunning++;
|
||||
ThreadPool.UnsafeQueueUserWorkItem(_ => _ProcessTasks(), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void _ProcessTasks()
|
||||
{
|
||||
_currentThreadIsProcessingItems = true;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Task item;
|
||||
lock (_tasks)
|
||||
{
|
||||
if (_tasks.Count == 0)
|
||||
{
|
||||
_delegatesQueuedOrRunning--;
|
||||
break;
|
||||
}
|
||||
item = _tasks.First!.Value;
|
||||
_tasks.RemoveFirst();
|
||||
}
|
||||
TryExecuteTask(item);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentThreadIsProcessingItems = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
|
||||
{
|
||||
if (!_currentThreadIsProcessingItems) return false;
|
||||
|
||||
if (taskWasPreviouslyQueued)
|
||||
{
|
||||
return TryDequeue(task) && TryExecuteTask(task);
|
||||
}
|
||||
else
|
||||
{
|
||||
return TryExecuteTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool TryDequeue(Task task)
|
||||
{
|
||||
lock (_tasks)
|
||||
{
|
||||
return _tasks.Remove(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Threading;
|
||||
|
||||
// Partly generated by o4-mini-high (20250709)
|
||||
|
||||
/// <summary>
|
||||
/// 可限制并发线程数量的任务池。
|
||||
/// </summary>
|
||||
public class LimitedTaskPool
|
||||
{
|
||||
/// <summary>
|
||||
/// 最大并发线程数。
|
||||
/// </summary>
|
||||
public int MaxThread { get; }
|
||||
|
||||
private readonly TaskFactory _factory;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 <see cref="LimitedTaskPool"/> 实例
|
||||
/// </summary>
|
||||
/// <param name="maxThread">参考 <see cref="MaxThread"/>,最小为 1</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="maxThread"/> 小于 1</exception>
|
||||
public LimitedTaskPool(int maxThread)
|
||||
{
|
||||
if (maxThread < 1) throw new ArgumentOutOfRangeException(nameof(maxThread));
|
||||
|
||||
MaxThread = maxThread;
|
||||
|
||||
var scheduler = new LimitedConcurrencyLevelTaskScheduler(maxThread);
|
||||
var cancellationToken = _cts.Token;
|
||||
|
||||
_factory = new TaskFactory(
|
||||
cancellationToken,
|
||||
TaskCreationOptions.DenyChildAttach,
|
||||
TaskContinuationOptions.None,
|
||||
scheduler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交一个任务。
|
||||
/// </summary>
|
||||
public Task Submit(Action work) => _factory.StartNew(work);
|
||||
|
||||
/// <summary>
|
||||
/// 提交一个异步任务。
|
||||
/// </summary>
|
||||
public Task Submit(Func<Task> work) => _factory.StartNew(work).Unwrap();
|
||||
|
||||
/// <summary>
|
||||
/// 取消所有任务。
|
||||
/// </summary>
|
||||
public void CancelAll() => _cts.Cancel();
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using PCL.Core.App.Localization;
|
||||
using System.Globalization;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// 提供与时间相关的实用方法。
|
||||
/// </summary>
|
||||
public static class TimeUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取格式类似于“11:08:52.037”的当前时间字符串。
|
||||
/// </summary>
|
||||
/// <returns>格式化后的时间字符串。</returns>
|
||||
public static string GetTimeNow()
|
||||
{
|
||||
return DateTime.Now.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统运行时间(毫秒),保证为正长整型,且大于 1。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 此方法基于 Environment.TickCount,但在 .NET 框架中,我们有更可靠的替代方案。
|
||||
/// </remarks>
|
||||
/// <returns>系统运行毫秒数。</returns>
|
||||
public static long GetTimeTick()
|
||||
{
|
||||
// 原始代码处理了 Environment.TickCount 的符号溢出问题(在 24.8 天后),
|
||||
// 但在现代 .NET 中,我们有更可靠、更精确的 Stopwatch 类。
|
||||
// 为了保持原函数意图,这里直接返回 Environment.TickCount。
|
||||
// 注意:Environment.TickCount 在64位系统中可能为负,不推荐在重要场景使用。
|
||||
return Environment.TickCount64;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取十进制 Unix 时间戳(秒)。
|
||||
/// </summary>
|
||||
/// <returns>当前时间的 Unix 时间戳。</returns>
|
||||
public static long GetUnixTimestamp()
|
||||
{
|
||||
return DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Unix 时间戳(秒)转换为本地时区的日期时间。
|
||||
/// </summary>
|
||||
/// <param name="unixTimestamp">Unix 时间戳(秒),表示自 1970-01-01 00:00:00 UTC 起的秒数。</param>
|
||||
/// <returns>转换后的本地日期时间。</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="unixTimestamp" /> 为负数或过大时抛出。</exception>
|
||||
public static DateTimeOffset FromUnixTimestamp(long unixTimestamp)
|
||||
{
|
||||
if (unixTimestamp < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(unixTimestamp), "Unix 时间戳不能为负数。");
|
||||
|
||||
try
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).ToLocalTime();
|
||||
}
|
||||
catch (ArgumentOutOfRangeException ex)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(unixTimestamp), "Unix 时间戳超出有效范围。", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 UTC 时间转换为当前时区的时间。
|
||||
/// </summary>
|
||||
/// <param name="utcDate">UTC 日期时间。</param>
|
||||
/// <returns>转换后的本地日期时间。</returns>
|
||||
public static DateTimeOffset ToLocalTime(DateTimeOffset utcDate)
|
||||
{
|
||||
return utcDate.ToLocalTime();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Unix 时间戳(秒)转换为当前展示区域性格式的本地时间字符串。
|
||||
/// </summary>
|
||||
/// <param name="unixTimestamp">Unix 时间戳(秒),表示自 1970-01-01 00:00:00 UTC 起的秒数。</param>
|
||||
/// <returns>使用当前展示区域性格式化后的本地时间字符串。</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="unixTimestamp" /> 为负数或过大时抛出。</exception>
|
||||
public static string FormatUnixTimestamp(long unixTimestamp)
|
||||
{
|
||||
return Lang.Date(FromUnixTimestamp(unixTimestamp), "g");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public class BlacklistValidator(List<string> contains) : AbstractValidator<string>
|
||||
{
|
||||
public List<string> Blacklist { get; set; } = contains;
|
||||
|
||||
public BlacklistValidator() : this([])
|
||||
{
|
||||
}
|
||||
|
||||
private void _BuildRules()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.Custom((input, context) =>
|
||||
{
|
||||
foreach (var items in Blacklist)
|
||||
{
|
||||
if (input.Contains(items))
|
||||
{
|
||||
context.AddFailure($"输入内容不能包含 {items}!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
|
||||
{
|
||||
_BuildRules();
|
||||
return base.PreValidate(context, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public class FileNameValidator(
|
||||
string? parentFolder = null,
|
||||
bool ignoreCase = true,
|
||||
bool useMinecraftCharCheck = true,
|
||||
bool requireParentFolderExists = true)
|
||||
: FileSystemValidator
|
||||
{
|
||||
public bool UseMinecraftCharCheck { get; set; } = useMinecraftCharCheck;
|
||||
public bool IgnoreCase { get; set; } = ignoreCase;
|
||||
public string? ParentFolder { get; set; } = parentFolder;
|
||||
public bool RequireParentFolderExists { get; set; } = requireParentFolderExists;
|
||||
|
||||
private bool? _isParentFolderExists;
|
||||
|
||||
public FileNameValidator() : this(null)
|
||||
{
|
||||
}
|
||||
|
||||
private void _BuildRules()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.Must(x => !string.IsNullOrWhiteSpace(x)).WithMessage("输入内容不能为空!")
|
||||
.Must(x => !x.StartsWith(' ')).WithMessage("文件名不能以空格开头!")
|
||||
.Must(x => !x.EndsWith(' ')).WithMessage("文件名不能以空格结尾!")
|
||||
.Must(x => !x.EndsWith('.')).WithMessage("文件名不能以小数点结尾!")
|
||||
.Custom((fileName, context) =>
|
||||
{
|
||||
var invalidChar = CheckInvalidStrings(fileName, UseMinecraftCharCheck ? ["!;"] : []);
|
||||
if (invalidChar is not null)
|
||||
{
|
||||
context.AddFailure($"文件名不可包含 {invalidChar} 字符!");
|
||||
}
|
||||
})
|
||||
.Custom((fileName, context) =>
|
||||
{
|
||||
var reservedWord = CheckReservedWord(fileName, []);
|
||||
if (reservedWord is not null)
|
||||
{
|
||||
context.AddFailure($"文件名不可为 {reservedWord}!");
|
||||
}
|
||||
})
|
||||
.Must(x => !x.IsMatch(RegexPatterns.Ntfs83FileName)).WithMessage("文件名不能包含这一特殊格式!")
|
||||
.Must(x =>
|
||||
{
|
||||
if (ParentFolder is null) return true;
|
||||
|
||||
var dirInfo = new DirectoryInfo(ParentFolder);
|
||||
if (dirInfo.Exists)
|
||||
{
|
||||
return !dirInfo.EnumerateFiles().Select(f => f.Name).Contains(x,
|
||||
IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
_isParentFolderExists = false;
|
||||
return !RequireParentFolderExists;
|
||||
|
||||
}).WithMessage(_isParentFolderExists is not null ? $"父文件夹不存在:{ParentFolder}" : "不可与现有文件重名!");
|
||||
}
|
||||
|
||||
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
|
||||
{
|
||||
_BuildRules();
|
||||
return base.PreValidate(context, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public abstract class FileSystemValidator : AbstractValidator<string>
|
||||
{
|
||||
protected static string? CheckInvalidStrings(string input, string[] extraInvalidStrings)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input)) return null;
|
||||
|
||||
var invalidStrings = Path.GetInvalidFileNameChars().Select(c => c.ToString());
|
||||
invalidStrings = invalidStrings.Concat(extraInvalidStrings);
|
||||
|
||||
// 找出字符串中包含的非法字符
|
||||
var found = invalidStrings
|
||||
.Where(input.Contains)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
return found.Length != 0 ? string.Join(" ", found) : null;
|
||||
}
|
||||
|
||||
protected static string? CheckReservedWord(string input, string[] extraReservedWords)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input)) return null;
|
||||
|
||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(input);
|
||||
|
||||
IEnumerable<string> reserved =
|
||||
[
|
||||
"CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
|
||||
"COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7",
|
||||
"LPT8", "LPT9", "LPT¹", "LPT²", "LPT³"
|
||||
];
|
||||
reserved = reserved.Concat(extraReservedWords);
|
||||
|
||||
// 找出匹配的保留字
|
||||
var matched = reserved.FirstOrDefault(r => r.Equals(nameWithoutExtension));
|
||||
|
||||
return matched ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public class FolderNameValidator(
|
||||
string? parentFolder = null,
|
||||
bool useMinecraftCharCheck = true,
|
||||
bool ignoreCase = true,
|
||||
bool ignoreSameNameInParentFolder = false)
|
||||
: FileSystemValidator
|
||||
{
|
||||
public bool UseMinecraftCharCheck { get; set; } = useMinecraftCharCheck;
|
||||
public bool IgnoreCase { get; set; } = ignoreCase;
|
||||
public bool IgnoreSameNameInParentFolder { get; set; } = ignoreSameNameInParentFolder;
|
||||
public string? ParentFolder { get; set; } = parentFolder;
|
||||
|
||||
public FolderNameValidator() : this(null)
|
||||
{
|
||||
}
|
||||
|
||||
private void _BuildRules()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.Must(x => !string.IsNullOrWhiteSpace(x)).WithMessage("输入内容不能为空!")
|
||||
.Must(x => !x.StartsWith(' ')).WithMessage("文件名不能以空格开头!")
|
||||
.Must(x => !x.EndsWith(' ')).WithMessage("文件名不能以空格结尾!")
|
||||
.Must(x => !x.EndsWith('.')).WithMessage("文件名不能以小数点结尾!")
|
||||
.Custom((fileName, context) =>
|
||||
{
|
||||
var invalidChar = CheckInvalidStrings(fileName, UseMinecraftCharCheck ? ["!;"] : []);
|
||||
if (invalidChar is not null)
|
||||
{
|
||||
context.AddFailure($"文件名不可包含 {invalidChar} 字符!");
|
||||
}
|
||||
})
|
||||
.Custom((fileName, context) =>
|
||||
{
|
||||
var reservedWord = CheckReservedWord(fileName, []);
|
||||
if (reservedWord is not null)
|
||||
{
|
||||
context.AddFailure($"文件名不可为 {reservedWord}!");
|
||||
}
|
||||
})
|
||||
.Must(x => !x.IsMatch(RegexPatterns.Ntfs83FileName)).WithMessage("文件名不能包含这一特殊格式!")
|
||||
.Must(x =>
|
||||
{
|
||||
if (ParentFolder is null) return true;
|
||||
|
||||
var dirInfo = new DirectoryInfo(ParentFolder);
|
||||
if (!dirInfo.Exists) return true;
|
||||
if (IgnoreSameNameInParentFolder) return true;
|
||||
|
||||
return !dirInfo.EnumerateDirectories().Select(f => f.Name).Contains(x,
|
||||
IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
|
||||
|
||||
}).WithMessage("不可与现有文件夹重名!");
|
||||
}
|
||||
|
||||
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
|
||||
{
|
||||
_BuildRules();
|
||||
return base.PreValidate(context, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public class FolderPathValidator(bool useMinecraftCharCheck) : FileSystemValidator
|
||||
{
|
||||
public bool UseMinecraftCharCheck { get; set; } = useMinecraftCharCheck;
|
||||
|
||||
public FolderPathValidator() : this(true)
|
||||
{
|
||||
}
|
||||
|
||||
private void _BuildRules()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.NotEmpty().WithMessage("输入内容不能为空!")
|
||||
.Must(x => !x.EndsWith(' ')).WithMessage("文件夹名不能以空格结尾!")
|
||||
.Must(x => !x.EndsWith('.')).WithMessage("文件夹名不能以小数点结尾!");
|
||||
|
||||
RuleForEach(x => _GetSubPaths(x))
|
||||
.Must(x => !string.IsNullOrWhiteSpace(x)).WithMessage("文件夹路径存在错误!")
|
||||
.Must(x => !x.StartsWith(' ')).WithMessage("文件夹名不能以空格开头!")
|
||||
.Must(x => !x.EndsWith(' ')).WithMessage("文件夹名不能以空格结尾!")
|
||||
.Must(x => !x.EndsWith('.')).WithMessage("文件夹名不能以小数点结尾!")
|
||||
.Custom((fileName, context) =>
|
||||
{
|
||||
var invalidChar = CheckInvalidStrings(fileName, UseMinecraftCharCheck ? ["!;"] : []);
|
||||
if (invalidChar is not null)
|
||||
{
|
||||
context.AddFailure($"文件夹名不可包含 {invalidChar} 字符!");
|
||||
}
|
||||
})
|
||||
.Custom((fileName, context) =>
|
||||
{
|
||||
var reservedWord = CheckReservedWord(fileName, []);
|
||||
if (reservedWord is not null)
|
||||
{
|
||||
context.AddFailure($"文件夹名不可为 {reservedWord}!");
|
||||
}
|
||||
})
|
||||
.Must(x => !x.IsMatch(RegexPatterns.Ntfs83FileName)).WithMessage("文件夹名不能包含这一特殊格式!")
|
||||
.OverridePropertyName("PathSegments");
|
||||
}
|
||||
|
||||
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
|
||||
{
|
||||
_BuildRules();
|
||||
return base.PreValidate(context, result);
|
||||
}
|
||||
|
||||
private static string[] _GetSubPaths(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var fullPath = new DirectoryInfo(path).FullName;
|
||||
return fullPath[Path.GetPathRoot(fullPath)!.Length..]
|
||||
.TrimEnd(Path.DirectorySeparatorChar)
|
||||
.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public class HttpAndUncValidator(bool allowNullOrEmpty) : AbstractValidator<string>
|
||||
{
|
||||
public bool AllowsNullOrEmpty { get; set; } = allowNullOrEmpty;
|
||||
|
||||
public HttpAndUncValidator() : this(false)
|
||||
{
|
||||
}
|
||||
|
||||
private void _BuildRules()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.Must(x =>
|
||||
{
|
||||
if (AllowsNullOrEmpty && string.IsNullOrEmpty(x))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return x.IsMatch(RegexPatterns.HttpUri) || x.IsMatch(RegexPatterns.UncPath);
|
||||
}).WithMessage("输入的网址无效!");
|
||||
}
|
||||
|
||||
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
|
||||
{
|
||||
_BuildRules();
|
||||
return base.PreValidate(context, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public class HttpValidator(bool allowNullOrEmpty) : AbstractValidator<string>
|
||||
{
|
||||
public bool AllowsNullOrEmpty { get; set; } = allowNullOrEmpty;
|
||||
|
||||
public HttpValidator() : this(false)
|
||||
{
|
||||
}
|
||||
|
||||
private void _BuildRules()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.Must(x =>
|
||||
{
|
||||
if (AllowsNullOrEmpty && string.IsNullOrEmpty(x))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return x.IsMatch(RegexPatterns.HttpUri);
|
||||
}).WithMessage("输入的网址无效!");
|
||||
}
|
||||
|
||||
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
|
||||
{
|
||||
_BuildRules();
|
||||
return base.PreValidate(context, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public class IntValidator(int max = int.MaxValue, int min = int.MinValue) : AbstractValidator<string>
|
||||
{
|
||||
public int Max { get; set; } = max;
|
||||
public int Min { get; set; } = min;
|
||||
|
||||
public IntValidator() : this(int.MaxValue)
|
||||
{
|
||||
}
|
||||
|
||||
private void _BuildRules()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.Must(x => x.Length < 9).WithMessage("请输入一个大小合理的数字!")
|
||||
.Must(x => int.TryParse(x, out _)).WithMessage("请输入一个整数!")
|
||||
.Must(x => int.TryParse(x, out var value) && value <= Max).WithMessage($"不可超过 {Max}!")
|
||||
.Must(x => int.TryParse(x, out var value) && value >= Min).WithMessage($"不可低于 {Min}!");
|
||||
}
|
||||
|
||||
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
|
||||
{
|
||||
_BuildRules();
|
||||
return base.PreValidate(context, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public class NullOrEmptyValidator : AbstractValidator<string>
|
||||
{
|
||||
public NullOrEmptyValidator()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.Must(x => !string.IsNullOrEmpty(x)).WithMessage("输入内容不能为空!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public class NullOrWhiteSpaceValidator : AbstractValidator<string>
|
||||
{
|
||||
public NullOrWhiteSpaceValidator()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.Must(x => !string.IsNullOrWhiteSpace(x)).WithMessage("输入内容不能为空!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using PCL.Core.App.Localization;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public class RegexValidator(string pattern = "", string errorMessage = "正则检查失败!") : AbstractValidator<string>
|
||||
{
|
||||
public RegexValidator() : this(string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public string Pattern { get; set; } = pattern;
|
||||
public string ErrorMessage { get; set; } = errorMessage;
|
||||
public string ErrorKey { get; set; } = "";
|
||||
|
||||
private void _BuildRules()
|
||||
{
|
||||
var message = string.IsNullOrEmpty(ErrorKey) ? ErrorMessage : Lang.Text(ErrorKey);
|
||||
RuleFor(x => x)
|
||||
.Must(x => Regex.IsMatch(x, Pattern)).WithMessage(message);
|
||||
}
|
||||
|
||||
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
|
||||
{
|
||||
_BuildRules();
|
||||
return base.PreValidate(context, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
|
||||
namespace PCL.Core.Utils.Validate;
|
||||
|
||||
public class StringLengthValidator(int min = 0, int max = int.MaxValue) : AbstractValidator<string>
|
||||
{
|
||||
public int Min { get; set; } = min;
|
||||
public int Max { get; set; } = max;
|
||||
|
||||
public StringLengthValidator() : this(0)
|
||||
{
|
||||
}
|
||||
|
||||
private void _BuildRules()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.Must(x => x.Length != Max || Max == Min).WithMessage($"长度必须为 {Max} 个字符!")
|
||||
.Must(x => x.Length >= Min).WithMessage($"长度至少为 {Min} 个字符!")
|
||||
.Must(x => x.Length <= Max).WithMessage($"长度最长为 {Max} 个字符!");
|
||||
}
|
||||
|
||||
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
|
||||
{
|
||||
_BuildRules();
|
||||
return base.PreValidate(context, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
public static class VarIntHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 将无符号长整数编码为VarInt字节序列
|
||||
/// </summary>
|
||||
/// <param name="value">要编码的64位无符号整数</param>
|
||||
/// <returns>VarInt字节数组</returns>
|
||||
public static byte[] Encode(ulong value)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
do
|
||||
{
|
||||
var temp = (byte)(value & 0x7F); // 取低7位
|
||||
value >>= 7; // 右移7位
|
||||
if (value != 0) // 如果还有后续数据
|
||||
temp |= 0x80; // 设置最高位为1
|
||||
stream.WriteByte(temp);
|
||||
} while (value != 0);
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将无符号整数编码为VarInt字节序列
|
||||
/// </summary>
|
||||
/// <param name="value">要编码的32位无符号整数</param>
|
||||
/// <returns>VarInt字节数组</returns>
|
||||
public static byte[] Encode(uint value) => Encode((ulong)value);
|
||||
|
||||
/// <summary>
|
||||
/// 从字节数组中解码无符号长整数
|
||||
/// </summary>
|
||||
/// <param name="bytes">包含VarInt编码的字节数组</param>
|
||||
/// <param name="readLength">读取的字节长度</param>
|
||||
/// <returns>解码后的64位无符号整数</returns>
|
||||
/// <exception cref="ArgumentNullException">输入字节数组为空</exception>
|
||||
/// <exception cref="FormatException">VarInt格式无效或超过最大长度</exception>
|
||||
public static ulong Decode(byte[] bytes, out int readLength)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bytes);
|
||||
|
||||
ulong result = 0;
|
||||
var shift = 0;
|
||||
var bytesRead = 0;
|
||||
const int maxBytes = 10; // ulong最大需要10字节
|
||||
|
||||
foreach (var b in bytes)
|
||||
{
|
||||
if (bytesRead >= maxBytes)
|
||||
throw new FormatException("VarInt exceeds maximum length");
|
||||
|
||||
// 取低7位并移位合并
|
||||
result |= (ulong)(b & 0x7F) << shift;
|
||||
bytesRead++;
|
||||
|
||||
// 检查是否结束
|
||||
if ((b & 0x80) == 0)
|
||||
{
|
||||
readLength = bytesRead;
|
||||
return result;
|
||||
}
|
||||
|
||||
shift += 7;
|
||||
}
|
||||
|
||||
throw new FormatException("Incomplete VarInt encoding");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从字节数组中解码无符号整数
|
||||
/// </summary>
|
||||
/// <param name="bytes">包含VarInt编码的字节数组</param>
|
||||
/// <param name="readLength">读取的字节长度</param>
|
||||
/// <returns>解码后的32位无符号整数</returns>
|
||||
public static uint DecodeUInt(byte[] bytes, out int readLength)
|
||||
{
|
||||
var result = Decode(bytes, out readLength);
|
||||
if (result > uint.MaxValue)
|
||||
throw new OverflowException("Decoded value exceeds UInt32 range");
|
||||
return (uint)result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从流中读取并解码无符号长整数,并将流前进所读取的字节数
|
||||
/// </summary>
|
||||
/// <param name="stream">输入流</param>
|
||||
/// <param name="cancellationToken">要监视取消请求的标记</param>
|
||||
/// <returns>解码后的64位无符号整数</returns>
|
||||
/// <exception cref="EndOfStreamException">流提前结束</exception>
|
||||
/// <exception cref="FormatException">VarInt格式无效</exception>
|
||||
public static async Task<ulong> ReadFromStreamAsync(Stream stream, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ulong result = 0;
|
||||
var shift = 0;
|
||||
var bytesRead = 0;
|
||||
const int maxBytes = 10;
|
||||
var buffer = new byte[1];
|
||||
while (true)
|
||||
{
|
||||
var readLength = await stream.ReadAsync(buffer, 0, 1, cancellationToken);
|
||||
if (readLength == 0)
|
||||
throw new EndOfStreamException();
|
||||
|
||||
var b = buffer[0];
|
||||
bytesRead++;
|
||||
|
||||
if (bytesRead > maxBytes)
|
||||
throw new FormatException("VarInt exceeds maximum length");
|
||||
|
||||
result |= (ulong)(b & 0x7F) << shift;
|
||||
|
||||
if ((b & 0x80) == 0)
|
||||
return result;
|
||||
|
||||
shift += 7;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从流中读取并解码无符号整数,并将流前进所读取的字节数
|
||||
/// </summary>
|
||||
/// <param name="stream">输入流</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>解码后的32位无符号整数</returns>
|
||||
public static async Task<uint> ReadUIntFromStreamAsync(Stream stream, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await ReadFromStreamAsync(stream, cancellationToken);
|
||||
if (result > uint.MaxValue)
|
||||
throw new OverflowException("Decoded value exceeds UInt32 range");
|
||||
return (uint)result;
|
||||
}
|
||||
|
||||
public static long DecodeZigZag(ulong value) => (long)(value >> 1) ^ -(long)(value & 1);
|
||||
|
||||
public static ulong EncodeZigZag(ulong value) => ((value << 1) ^ (value >> 63));
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
public class VersionRange(Version? minVersion, Version? maxVersion)
|
||||
{
|
||||
public Version? MinVersion { get; set; } = minVersion;
|
||||
|
||||
public Version? MaxVersion { get; set; } = maxVersion;
|
||||
|
||||
public bool IsInRange(Version target) => (MinVersion is not null || MaxVersion is not null)
|
||||
&& (MinVersion ?? target) <= target
|
||||
&& (MaxVersion ?? target) >= target;
|
||||
|
||||
/// <summary>
|
||||
/// 缩减版本号范围,如果 <paramref name="target"/> 比当 <see cref="MinVersion"/> 大则应用
|
||||
/// </summary>
|
||||
/// <param name="target">目标版本号</param>
|
||||
/// <returns>是否使用</returns>
|
||||
public bool SetMin(Version target) => (MinVersion = (MinVersion is null || target > MinVersion) ? target : MinVersion) == target;
|
||||
|
||||
/// <summary>
|
||||
/// 缩减版本号范围,如果 <paramref name="target"/> 比当 <see cref="MaxVersion"/> 小则应用
|
||||
/// </summary>
|
||||
/// <param name="target">目标版本号</param>
|
||||
/// <returns>是否使用</returns>
|
||||
public bool SetMax(Version target) => (MaxVersion = (MaxVersion is null || target < MaxVersion) ? target : MaxVersion) == target;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Utils.WinRT;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个 WinRT HSTRING 句柄的只读包装。
|
||||
/// </summary>
|
||||
/// <param name="Value">HSTRING 的底层指针句柄(<see cref="nint"/>)。</param>
|
||||
public readonly record struct HString(IntPtr Value)
|
||||
{
|
||||
public readonly IntPtr Value = Value;
|
||||
public bool IsNull => Value == IntPtr.Zero;
|
||||
public static implicit operator IntPtr(HString h) => h.Value;
|
||||
public static explicit operator HString(IntPtr ptr) => new(ptr);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace PCL.Core.Utils.WinRT;
|
||||
|
||||
public class HStringHelper
|
||||
{
|
||||
public static unsafe HString ToHString(string? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return new HString(IntPtr.Zero);
|
||||
}
|
||||
|
||||
IntPtr handle;
|
||||
fixed (char* lpValue = value)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(
|
||||
WinRTInterop.WindowsCreateString((ushort*)lpValue, value.Length, &handle));
|
||||
}
|
||||
|
||||
return new HString(handle);
|
||||
}
|
||||
|
||||
public static unsafe string ToManagedString(HString value)
|
||||
{
|
||||
if (value == IntPtr.Zero)
|
||||
return "";
|
||||
uint length;
|
||||
var buffer = WinRTInterop.WindowsGetStringRawBuffer(value, &length);
|
||||
return length != 0 ? new string(buffer, 0, (int)length) : string.Empty;
|
||||
}
|
||||
|
||||
public static void DeleteHString(HString value)
|
||||
{
|
||||
if (!value.IsNull)
|
||||
WinRTInterop.WindowsDeleteString(value);
|
||||
}
|
||||
|
||||
public static unsafe void WithReference(
|
||||
ReadOnlySpan<char> value,
|
||||
Action<HString> action)
|
||||
{
|
||||
fixed (char* p = value)
|
||||
{
|
||||
WinRTInterop.HStringHeader header;
|
||||
IntPtr hstring;
|
||||
|
||||
Marshal.ThrowExceptionForHR(
|
||||
WinRTInterop.WindowsCreateStringReference(
|
||||
(ushort*)p,
|
||||
value.Length,
|
||||
(IntPtr*)&header,
|
||||
&hstring));
|
||||
|
||||
action((HString)hstring);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Utils.WinRT.Interface;
|
||||
|
||||
public unsafe struct IInspectable
|
||||
{
|
||||
public IInspectableVtbl* lpVtbl;
|
||||
}
|
||||
|
||||
public unsafe struct IInspectableVtbl
|
||||
{
|
||||
// IUnknown
|
||||
public delegate* unmanaged<void*, Guid*, void**, int> QueryInterface;
|
||||
public delegate* unmanaged<void*, uint> AddRef;
|
||||
public delegate* unmanaged<void*, uint> Release;
|
||||
|
||||
// IInspectable
|
||||
public delegate* unmanaged<void*, uint*, Guid**, int> GetIids;
|
||||
public delegate* unmanaged<void*, IntPtr*, int> GetRuntimeClassName;
|
||||
public delegate* unmanaged<void*, int*, int> GetTrustLevel;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Utils.WinRT.Interface.Windows.Data.Xml.Dom;
|
||||
|
||||
public static class IXmlDocumentIOInfo
|
||||
{
|
||||
public static readonly string ActivatableClassId = "Windows.Data.Xml.Dom.XmlDocument";
|
||||
public static readonly Guid Iid = new("6cd0e74e-ee65-4489-9ebf-ca43e87ba637");
|
||||
}
|
||||
|
||||
public unsafe struct IXmlDocumentIO
|
||||
{
|
||||
public IXmlDocumentIOVtbl* lpVtbl;
|
||||
}
|
||||
|
||||
public unsafe struct IXmlDocumentIOVtbl
|
||||
{
|
||||
// IUnknown
|
||||
public delegate* unmanaged<void*, Guid*, void**, int> QueryInterface;
|
||||
public delegate* unmanaged<void*, uint> AddRef;
|
||||
public delegate* unmanaged<void*, uint> Release;
|
||||
|
||||
// IInspectable
|
||||
public delegate* unmanaged<void*, uint*, Guid**, int> GetIids;
|
||||
public delegate* unmanaged<void*, IntPtr*, int> GetRuntimeClassName;
|
||||
public delegate* unmanaged<void*, int*, int> GetTrustLevel;
|
||||
|
||||
// IXmlDocumentIO
|
||||
|
||||
/// <summary>
|
||||
/// void LoadXml(string xml)
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, IntPtr, int> LoadXml;
|
||||
|
||||
/// <summary>
|
||||
/// void LoadXml(string xml, XmlLoadSettings settings)
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, IntPtr, void*, int> LoadXmlWithSettings;
|
||||
|
||||
/// <summary>
|
||||
/// IAsyncAction SaveToFileAsync(IStorageFile file)
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, void*, void**, int> SaveToFileAsync;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Utils.WinRT.Interface.Windows.UI.Notifications;
|
||||
|
||||
public static class IToastNotificationFactoryInfo
|
||||
{
|
||||
public static readonly string ActivatableClassId = "Windows.UI.Notifications.ToastNotification";
|
||||
public static readonly Guid Iid = new("04124b20-82c6-4229-b109-fd9ed4662b53");
|
||||
}
|
||||
|
||||
public unsafe struct IToastNotificationFactory
|
||||
{
|
||||
public IToastNotificationFactoryVtbl* lpVtbl;
|
||||
}
|
||||
|
||||
public unsafe struct IToastNotificationFactoryVtbl
|
||||
{
|
||||
// IUnknown
|
||||
public delegate* unmanaged<void*, Guid*, void**, int> QueryInterface;
|
||||
public delegate* unmanaged<void*, uint> AddRef;
|
||||
public delegate* unmanaged<void*, uint> Release;
|
||||
|
||||
// IInspectable
|
||||
public delegate* unmanaged<void*, uint*, Guid**, int> GetIids;
|
||||
public delegate* unmanaged<void*, IntPtr*, int> GetRuntimeClassName;
|
||||
public delegate* unmanaged<void*, int*, int> GetTrustLevel;
|
||||
|
||||
// IToastNotificationFactory
|
||||
|
||||
/// <summary>
|
||||
/// ToastNotification CreateToastNotification(XmlDocument content)
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, void*, void**, int> CreateToastNotification;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Utils.WinRT.Interface.Windows.UI.Notifications;
|
||||
|
||||
public static class IToastNotificationManagerStaticsInfo
|
||||
{
|
||||
public static readonly string ActivatableClassId = "Windows.UI.Notifications.ToastNotificationManager";
|
||||
public static readonly Guid Iid = new("50ac103f-d235-4598-bbef-98fe4d1a3ad4");
|
||||
}
|
||||
|
||||
public unsafe struct IToastNotificationManagerStatics
|
||||
{
|
||||
public IToastNotificationManagerStaticsVtbl* lpVtbl;
|
||||
}
|
||||
|
||||
public unsafe struct IToastNotificationManagerStaticsVtbl
|
||||
{
|
||||
// IUnknown
|
||||
public delegate* unmanaged<void*, Guid*, void**, int> QueryInterface;
|
||||
public delegate* unmanaged<void*, uint> AddRef;
|
||||
public delegate* unmanaged<void*, uint> Release;
|
||||
|
||||
// IInspectable
|
||||
public delegate* unmanaged<void*, uint*, Guid**, int> GetIids;
|
||||
public delegate* unmanaged<void*, IntPtr*, int> GetRuntimeClassName;
|
||||
public delegate* unmanaged<void*, int*, int> GetTrustLevel;
|
||||
|
||||
// IToastNotificationManagerStatics
|
||||
|
||||
/// <summary>
|
||||
/// ToastNotifier CreateToastNotifier()
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, void**, int> CreateToastNotifier;
|
||||
|
||||
/// <summary>
|
||||
/// ToastNotifier CreateToastNotifier(string applicationId)
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, IntPtr, void**, int> CreateToastNotifierWithId;
|
||||
|
||||
/// <summary>
|
||||
/// XmlDocument GetTemplateContent(ToastTemplateType type)
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, int, void**, int> GetTemplateContent;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Utils.WinRT.Interface.Windows.UI.Notifications;
|
||||
|
||||
public static class IToastNotifierInfo
|
||||
{
|
||||
public static readonly string ActivatableClassId = "Windows.UI.Notifications.ToastNotifier";
|
||||
public static readonly Guid Iid = new("75927b93-03f3-41ec-91d3-6e5bac1b38e7");
|
||||
}
|
||||
|
||||
public unsafe struct IToastNotifier
|
||||
{
|
||||
public IToastNotifierVtbl* lpVtbl;
|
||||
}
|
||||
|
||||
public unsafe struct IToastNotifierVtbl
|
||||
{
|
||||
// IUnknown
|
||||
public delegate* unmanaged<void*, Guid*, void**, int> QueryInterface;
|
||||
public delegate* unmanaged<void*, uint> AddRef;
|
||||
public delegate* unmanaged<void*, uint> Release;
|
||||
|
||||
// IInspectable
|
||||
public delegate* unmanaged<void*, uint*, Guid**, int> GetIids;
|
||||
public delegate* unmanaged<void*, IntPtr*, int> GetRuntimeClassName;
|
||||
public delegate* unmanaged<void*, int*, int> GetTrustLevel;
|
||||
|
||||
// IToastNotifier
|
||||
|
||||
/// <summary>
|
||||
/// void Show(ToastNotification notification)
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, void*, int> Show;
|
||||
|
||||
/// <summary>
|
||||
/// void Hide(ToastNotification notification)
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, void*, int> Hide;
|
||||
|
||||
/// <summary>
|
||||
/// NotificationSetting Setting { get; }
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, int, int> get_Setting;
|
||||
|
||||
/// <summary>
|
||||
/// void AddToSchedule(ScheduledToastNotification scheduledToast)
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, void*, int> AddToSchedule;
|
||||
|
||||
/// <summary>
|
||||
/// void RemoveFromSchedule(ScheduledToastNotification scheduledToast)
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, void*, int> RemoveFromSchedule;
|
||||
|
||||
/// <summary>
|
||||
/// IVectorView<ScheduledToastNotification> GetScheduledToastNotifications()
|
||||
/// </summary>
|
||||
public delegate* unmanaged<void*, void**, int> GetScheduledToastNotifications;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace PCL.Core.Utils.WinRT;
|
||||
|
||||
public static partial class WinRTInterop
|
||||
{
|
||||
// roapi.h
|
||||
[LibraryImport("combase.dll")]
|
||||
private static unsafe partial int RoGetActivationFactory(IntPtr activatableClassId, Guid* iid, IntPtr* factory);
|
||||
[LibraryImport("combase.dll")]
|
||||
private static unsafe partial int RoActivateInstance(IntPtr activatableClassId, IntPtr* instance);
|
||||
|
||||
public static unsafe IntPtr ActivateInstance(ReadOnlySpan<char> activatableClassId)
|
||||
{
|
||||
var instance = IntPtr.Zero;
|
||||
|
||||
fixed (char* pName = activatableClassId)
|
||||
{
|
||||
HStringHeader header;
|
||||
IntPtr hstring;
|
||||
|
||||
Marshal.ThrowExceptionForHR(
|
||||
WindowsCreateStringReference(
|
||||
(ushort*)pName,
|
||||
activatableClassId.Length,
|
||||
(IntPtr*)&header,
|
||||
&hstring));
|
||||
|
||||
Marshal.ThrowExceptionForHR(
|
||||
RoActivateInstance(
|
||||
hstring,
|
||||
&instance));
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static unsafe IntPtr GetActivationFactory(ReadOnlySpan<char> activatableClassId, Guid iid)
|
||||
{
|
||||
var factory = IntPtr.Zero;
|
||||
|
||||
fixed (char* pName = activatableClassId)
|
||||
{
|
||||
HStringHeader header;
|
||||
IntPtr hstring;
|
||||
|
||||
Marshal.ThrowExceptionForHR(
|
||||
WindowsCreateStringReference(
|
||||
(ushort*)pName,
|
||||
activatableClassId.Length,
|
||||
(IntPtr*)&header,
|
||||
&hstring));
|
||||
|
||||
Marshal.ThrowExceptionForHR(
|
||||
RoGetActivationFactory(
|
||||
hstring,
|
||||
&iid,
|
||||
&factory));
|
||||
}
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
// winstring.h
|
||||
[LibraryImport("combase.dll")]
|
||||
internal static unsafe partial int WindowsCreateString(ushort* sourceString, int length, IntPtr* hstring);
|
||||
[LibraryImport("combase.dll")]
|
||||
internal static unsafe partial int WindowsCreateStringReference(ushort* sourceString, int length,
|
||||
IntPtr* hstringHeader, IntPtr* hstring);
|
||||
[LibraryImport("combase.dll")]
|
||||
internal static unsafe partial int WindowsDeleteString(IntPtr hstring);
|
||||
[LibraryImport("combase.dll")]
|
||||
internal static unsafe partial char* WindowsGetStringRawBuffer(IntPtr hstring, uint* length);
|
||||
|
||||
// hstring.h
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal unsafe struct HStringHeader
|
||||
{
|
||||
private fixed byte _data[24];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
|
||||
|
||||
public static class WpfUtils
|
||||
{
|
||||
public static bool IsDependencyPropertySet(DependencyObject obj, DependencyProperty dp)
|
||||
{
|
||||
return obj.ReadLocalValue(dp) != DependencyProperty.UnsetValue;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user