feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -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>
/// 解码字节数组为字符串,自动检测 BOMUTF-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
}
+150
View File
@@ -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&lt;T, E&gt; 转换为 Result&lt;T2, E&gt;,通过映射成功值。
/// </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&lt;T, E&gt; 转换为 Result&lt;T, E2&gt;,通过映射错误值。
/// </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";
}
}
}
+174
View File
@@ -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>
/// 当前剩余的配额数。如果 &gt; 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;
}
@@ -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;
}
@@ -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;
}
@@ -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&lt;ScheduledToastNotification&gt; 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;
}
}