first commit

This commit is contained in:
lihansani
2026-07-13 10:04:36 +08:00
commit 577aa128bd
870 changed files with 25849 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
using System;
using System.Text;
namespace DCIT.Core.Utilities
{
public static class DisplayWidthUtility
{
public static int Measure(string value)
{
if (string.IsNullOrEmpty(value))
{
return 0;
}
var width = 0;
foreach (var character in value)
{
width += GetCharacterWidth(character);
}
return width;
}
public static string TruncateToWidth(string value, int maxWidth, out bool truncated)
{
if (string.IsNullOrEmpty(value) || maxWidth <= 0)
{
truncated = !string.IsNullOrEmpty(value);
return string.Empty;
}
var builder = new StringBuilder();
var width = 0;
truncated = false;
foreach (var character in value)
{
var charWidth = GetCharacterWidth(character);
if (width + charWidth > maxWidth)
{
truncated = true;
break;
}
builder.Append(character);
width += charWidth;
}
if (!truncated && builder.Length < value.Length)
{
truncated = true;
}
return builder.ToString();
}
public static string PadToWidth(string value, int targetWidth, out bool padded)
{
var builder = new StringBuilder(value ?? string.Empty);
var currentWidth = Measure(builder.ToString());
padded = currentWidth < targetWidth;
while (currentWidth + 2 <= targetWidth)
{
builder.Append('\u3000');
currentWidth += 2;
}
while (currentWidth < targetWidth)
{
builder.Append(' ');
currentWidth += 1;
}
return builder.ToString();
}
private static int GetCharacterWidth(char character)
{
if (character == '\u3000')
{
return 2;
}
if (character >= 0x1100 &&
(character <= 0x115F ||
character == 0x2329 ||
character == 0x232A ||
(character >= 0x2E80 && character <= 0xA4CF && character != 0x303F) ||
(character >= 0xAC00 && character <= 0xD7A3) ||
(character >= 0xF900 && character <= 0xFAFF) ||
(character >= 0xFE10 && character <= 0xFE19) ||
(character >= 0xFE30 && character <= 0xFE6F) ||
(character >= 0xFF00 && character <= 0xFF60) ||
(character >= 0xFFE0 && character <= 0xFFE6)))
{
return 2;
}
return 1;
}
}
}

View File

@@ -0,0 +1,270 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace DCIT.Core.Utilities
{
public static class HashUtility
{
private static readonly byte[] EncryptionKey = ComputeSha256Bytes("DCIT::DiffEncryptionKey::20260424");
private static readonly byte[] TamperKey = ComputeSha256Bytes("DCIT::TamperProtectionKey::20260424");
public static string ComputeSha256Base64(byte[] bytes)
{
using (var sha = SHA256.Create())
{
return Convert.ToBase64String(sha.ComputeHash(bytes ?? new byte[0]));
}
}
public static string ComputeSha256Base64(string value)
{
return ComputeSha256Base64(Encoding.UTF8.GetBytes(value ?? string.Empty));
}
public static string ComputeFileSha256Base64(string path)
{
using (var stream = File.OpenRead(path))
using (var sha = SHA256.Create())
{
return Convert.ToBase64String(sha.ComputeHash(stream));
}
}
public static string ComputeHmacBase64(byte[] bytes)
{
using (var hmac = new HMACSHA256(TamperKey))
{
return Convert.ToBase64String(hmac.ComputeHash(bytes ?? new byte[0]));
}
}
public static string ComputeFileHmacBase64(string path)
{
using (var stream = File.OpenRead(path))
using (var hmac = new HMACSHA256(TamperKey))
{
return Convert.ToBase64String(hmac.ComputeHash(stream));
}
}
public static EncryptedPayload Encrypt(byte[] plaintextBytes)
{
var iv = new byte[16];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(iv);
}
byte[] cipherBytes;
using (var aes = Aes.Create())
{
aes.Key = EncryptionKey;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var encryptor = aes.CreateEncryptor())
{
cipherBytes = encryptor.TransformFinalBlock(plaintextBytes ?? new byte[0], 0, (plaintextBytes ?? new byte[0]).Length);
}
}
var tagInput = Combine(iv, cipherBytes);
using (var hmac = new HMACSHA256(TamperKey))
{
return new EncryptedPayload
{
CiphertextBase64 = Convert.ToBase64String(cipherBytes),
NonceBase64 = Convert.ToBase64String(iv),
TagBase64 = Convert.ToBase64String(hmac.ComputeHash(tagInput)),
};
}
}
public static EncryptedPayloadHeader EncryptStream(Stream plaintextStream, Stream ciphertextStream)
{
if (plaintextStream == null)
{
throw new ArgumentNullException("plaintextStream");
}
if (ciphertextStream == null)
{
throw new ArgumentNullException("ciphertextStream");
}
if (!ciphertextStream.CanSeek)
{
throw new ArgumentException("密文流必须支持 Seek 以便计算防篡改标签。", "ciphertextStream");
}
var iv = new byte[16];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(iv);
}
using (var aes = Aes.Create())
{
aes.Key = EncryptionKey;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var encryptor = aes.CreateEncryptor())
using (var cryptoStream = new CryptoStream(ciphertextStream, encryptor, CryptoStreamMode.Write, leaveOpen: true))
{
plaintextStream.CopyTo(cryptoStream);
cryptoStream.FlushFinalBlock();
}
}
ciphertextStream.Position = 0;
using (var hmac = new HMACSHA256(TamperKey))
{
hmac.TransformBlock(iv, 0, iv.Length, iv, 0);
var buffer = new byte[65536];
int read;
while ((read = ciphertextStream.Read(buffer, 0, buffer.Length)) > 0)
{
hmac.TransformBlock(buffer, 0, read, buffer, 0);
}
hmac.TransformFinalBlock(new byte[0], 0, 0);
return new EncryptedPayloadHeader
{
NonceBase64 = Convert.ToBase64String(iv),
TagBase64 = Convert.ToBase64String(hmac.Hash),
};
}
}
public static byte[] Decrypt(string ciphertextBase64, string nonceBase64, string tagBase64)
{
var cipherBytes = Convert.FromBase64String(ciphertextBase64 ?? string.Empty);
var iv = Convert.FromBase64String(nonceBase64 ?? string.Empty);
var providedTag = Convert.FromBase64String(tagBase64 ?? string.Empty);
var expectedTag = Convert.FromBase64String(ComputeHmacBase64(Combine(iv, cipherBytes)));
if (!FixedTimeEquals(providedTag, expectedTag))
{
throw new CryptographicException("差异载荷验证失败。");
}
using (var aes = Aes.Create())
{
aes.Key = EncryptionKey;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var decryptor = aes.CreateDecryptor())
{
return decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
}
}
}
public static void DecryptStream(Stream cipherStream, Stream plaintextStream, byte[] iv, byte[] expectedTag)
{
if (cipherStream == null)
{
throw new ArgumentNullException("cipherStream");
}
if (plaintextStream == null)
{
throw new ArgumentNullException("plaintextStream");
}
if (!cipherStream.CanSeek)
{
throw new ArgumentException("密文流必须支持 Seek 以便校验防篡改标签。", "cipherStream");
}
using (var hmac = new HMACSHA256(TamperKey))
{
hmac.TransformBlock(iv ?? new byte[0], 0, (iv ?? new byte[0]).Length, iv ?? new byte[0], 0);
cipherStream.Position = 0;
var buffer = new byte[65536];
int read;
while ((read = cipherStream.Read(buffer, 0, buffer.Length)) > 0)
{
hmac.TransformBlock(buffer, 0, read, buffer, 0);
}
hmac.TransformFinalBlock(new byte[0], 0, 0);
if (!FixedTimeEquals(hmac.Hash, expectedTag ?? new byte[0]))
{
throw new CryptographicException("差异载荷防篡改校验失败。");
}
}
cipherStream.Position = 0;
using (var aes = Aes.Create())
{
aes.Key = EncryptionKey;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var decryptor = aes.CreateDecryptor())
using (var cryptoStream = new CryptoStream(cipherStream, decryptor, CryptoStreamMode.Read, leaveOpen: true))
{
cryptoStream.CopyTo(plaintextStream);
}
}
}
private static byte[] ComputeSha256Bytes(string value)
{
using (var sha = SHA256.Create())
{
return sha.ComputeHash(Encoding.UTF8.GetBytes(value ?? string.Empty));
}
}
private static bool FixedTimeEquals(byte[] left, byte[] right)
{
if (left == null || right == null || left.Length != right.Length)
{
return false;
}
var diff = 0;
for (var index = 0; index < left.Length; index++)
{
diff |= left[index] ^ right[index];
}
return diff == 0;
}
private static byte[] Combine(byte[] left, byte[] right)
{
left = left ?? new byte[0];
right = right ?? new byte[0];
var bytes = new byte[left.Length + right.Length];
Buffer.BlockCopy(left, 0, bytes, 0, left.Length);
Buffer.BlockCopy(right, 0, bytes, left.Length, right.Length);
return bytes;
}
}
public class EncryptedPayload
{
public string CiphertextBase64 { get; set; }
public string NonceBase64 { get; set; }
public string TagBase64 { get; set; }
}
public class EncryptedPayloadHeader
{
public string NonceBase64 { get; set; }
public string TagBase64 { get; set; }
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace DCIT.Core.Utilities
{
public static class PathUtility
{
public static string NormalizeDirectory(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
var fullPath = Path.GetFullPath(path.Trim());
if (!fullPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
{
fullPath += Path.DirectorySeparatorChar;
}
return fullPath;
}
public static bool AreSameOrNestedDirectories(string left, string right)
{
var normalizedLeft = NormalizeDirectory(left);
var normalizedRight = NormalizeDirectory(right);
if (string.IsNullOrEmpty(normalizedLeft) || string.IsNullOrEmpty(normalizedRight))
{
return false;
}
return normalizedLeft.Equals(normalizedRight, StringComparison.OrdinalIgnoreCase)
|| normalizedLeft.StartsWith(normalizedRight, StringComparison.OrdinalIgnoreCase)
|| normalizedRight.StartsWith(normalizedLeft, StringComparison.OrdinalIgnoreCase);
}
public static string GetRelativePath(string relativeTo, string path)
{
var root = new Uri(NormalizeDirectory(relativeTo), UriKind.Absolute);
var target = new Uri(Path.GetFullPath(path), UriKind.Absolute);
var relative = root.MakeRelativeUri(target).ToString().Replace('/', Path.DirectorySeparatorChar);
return Uri.UnescapeDataString(relative);
}
public static IEnumerable<string> ValidateDirectoryPair(string firstLabel, string firstPath, string secondLabel, string secondPath)
{
if (string.IsNullOrWhiteSpace(firstPath) || string.IsNullOrWhiteSpace(secondPath))
{
yield break;
}
if (AreSameOrNestedDirectories(firstPath, secondPath))
{
yield return string.Format("{0} 与 {1} 不能相同,也不能互为父子目录。", firstLabel, secondLabel);
}
}
}
}

View File

@@ -0,0 +1,342 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using DocumentFormat.OpenXml;
namespace DCIT.Core.Utilities
{
internal static class VmlTextMetadataUtility
{
private static readonly Regex Base64WhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled);
public static bool IsVmlShapeElement(OpenXmlElement element)
{
return element != null
&& string.Equals(element.LocalName, "shape", StringComparison.OrdinalIgnoreCase)
&& string.Equals(element.NamespaceUri, "urn:schemas-microsoft-com:vml", StringComparison.OrdinalIgnoreCase);
}
public static IEnumerable<VmlTextBinding> CreateBindings(OpenXmlElement shape)
{
if (!IsVmlShapeElement(shape))
{
yield break;
}
foreach (var binding in CreateEquationBindings(shape))
{
yield return binding;
}
foreach (var binding in CreateChartBindings(shape))
{
yield return binding;
}
}
private static IEnumerable<VmlTextBinding> CreateEquationBindings(OpenXmlElement shape)
{
var attribute = FindAttribute(shape, "equationxml");
if (!attribute.HasValue || string.IsNullOrWhiteSpace(attribute.Value.Value))
{
yield break;
}
var actualAttribute = attribute.Value;
XDocument document;
try
{
var decoded = WebUtility.HtmlDecode(actualAttribute.Value);
if (string.IsNullOrWhiteSpace(decoded))
{
yield break;
}
document = XDocument.Parse(decoded, LoadOptions.PreserveWhitespace);
}
catch
{
yield break;
}
var textNodes = document
.Descendants()
.Where(item => string.Equals(item.Name.LocalName, "t", StringComparison.OrdinalIgnoreCase))
.ToList();
if (textNodes.Count == 0)
{
yield break;
}
foreach (var textNode in textNodes)
{
var initialText = textNode.Value ?? string.Empty;
if (initialText.Length == 0)
{
continue;
}
yield return new VmlTextBinding
{
Text = initialText,
IsEquationText = true,
Apply = updatedText =>
{
textNode.Value = updatedText ?? string.Empty;
SetAttribute(shape, actualAttribute, SerializeXml(document));
},
};
}
}
private static IEnumerable<VmlTextBinding> CreateChartBindings(OpenXmlElement shape)
{
var attribute = FindAttribute(shape, "gfxdata");
if (!attribute.HasValue || string.IsNullOrWhiteSpace(attribute.Value.Value))
{
yield break;
}
var actualAttribute = attribute.Value;
ChartPackageContext context;
try
{
context = ChartPackageContext.TryCreate(shape, actualAttribute);
}
catch
{
context = null;
}
if (context == null || context.XmlEntries.Count == 0)
{
yield break;
}
foreach (var entry in context.XmlEntries)
{
foreach (var textNode in entry.Document
.Descendants()
.Where(item => IsChartVisibleTextNode(item))
.ToList())
{
var initialText = textNode.Value ?? string.Empty;
if (initialText.Length == 0)
{
continue;
}
yield return new VmlTextBinding
{
Text = initialText,
IsChartText = true,
Apply = updatedText =>
{
textNode.Value = updatedText ?? string.Empty;
context.Save();
},
};
}
}
}
private static bool IsChartVisibleTextNode(XElement element)
{
if (element == null)
{
return false;
}
var localName = element.Name.LocalName;
return string.Equals(localName, "t", StringComparison.OrdinalIgnoreCase)
|| string.Equals(localName, "v", StringComparison.OrdinalIgnoreCase);
}
private static OpenXmlAttribute? FindAttribute(OpenXmlElement element, string localName)
{
if (element == null || string.IsNullOrWhiteSpace(localName))
{
return null;
}
foreach (var attribute in element.GetAttributes())
{
if (string.Equals(attribute.LocalName, localName, StringComparison.OrdinalIgnoreCase))
{
return attribute;
}
}
return null;
}
private static void SetAttribute(OpenXmlElement element, OpenXmlAttribute sourceAttribute, string value)
{
element.SetAttribute(new OpenXmlAttribute(
sourceAttribute.Prefix,
sourceAttribute.LocalName,
sourceAttribute.NamespaceUri,
value ?? string.Empty));
}
private static string SerializeXml(XDocument document)
{
using (var writer = new Utf8StringWriter())
using (var xmlWriter = XmlWriter.Create(writer, new XmlWriterSettings
{
OmitXmlDeclaration = document.Declaration == null,
Encoding = new UTF8Encoding(false),
Indent = false,
NewLineHandling = NewLineHandling.None,
}))
{
document.Save(xmlWriter);
xmlWriter.Flush();
return writer.ToString();
}
}
private sealed class ChartPackageContext
{
private readonly OpenXmlElement _shape;
private readonly OpenXmlAttribute _attribute;
private readonly List<EntryState> _allEntries;
private ChartPackageContext(OpenXmlElement shape, OpenXmlAttribute attribute, List<EntryState> allEntries, List<EntryState> xmlEntries)
{
_shape = shape;
_attribute = attribute;
_allEntries = allEntries;
XmlEntries = xmlEntries;
}
public List<EntryState> XmlEntries { get; private set; }
public static ChartPackageContext TryCreate(OpenXmlElement shape, OpenXmlAttribute attribute)
{
var decoded = WebUtility.HtmlDecode(attribute.Value ?? string.Empty);
if (string.IsNullOrWhiteSpace(decoded))
{
return null;
}
var normalizedBase64 = Base64WhitespaceRegex.Replace(decoded, string.Empty);
if (normalizedBase64.Length == 0)
{
return null;
}
var bytes = Convert.FromBase64String(normalizedBase64);
var allEntries = new List<EntryState>();
var xmlEntries = new List<EntryState>();
using (var input = new MemoryStream(bytes))
using (var zip = new ZipArchive(input, ZipArchiveMode.Read, false))
{
foreach (var entry in zip.Entries)
{
var state = new EntryState
{
FullName = entry.FullName,
LastWriteTime = entry.LastWriteTime,
};
using (var entryStream = entry.Open())
using (var memory = new MemoryStream())
{
entryStream.CopyTo(memory);
state.RawBytes = memory.ToArray();
}
if (entry.FullName.IndexOf("drs/charts/", StringComparison.OrdinalIgnoreCase) >= 0
&& entry.FullName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
try
{
state.Document = XDocument.Parse(
Encoding.UTF8.GetString(state.RawBytes),
LoadOptions.PreserveWhitespace);
}
catch
{
state.Document = null;
}
if (state.Document != null)
{
xmlEntries.Add(state);
}
}
allEntries.Add(state);
}
}
return new ChartPackageContext(shape, attribute, allEntries, xmlEntries);
}
public void Save()
{
using (var output = new MemoryStream())
{
using (var zip = new ZipArchive(output, ZipArchiveMode.Create, true))
{
foreach (var entry in _allEntries)
{
var targetEntry = zip.CreateEntry(entry.FullName, CompressionLevel.Optimal);
targetEntry.LastWriteTime = entry.LastWriteTime;
using (var targetStream = targetEntry.Open())
{
var bytes = entry.Document == null
? (entry.RawBytes ?? Array.Empty<byte>())
: Encoding.UTF8.GetBytes(SerializeXml(entry.Document));
targetStream.Write(bytes, 0, bytes.Length);
}
}
}
SetAttribute(_shape, _attribute, Convert.ToBase64String(output.ToArray()));
}
}
}
private sealed class Utf8StringWriter : StringWriter
{
public override Encoding Encoding
{
get { return new UTF8Encoding(false); }
}
}
private sealed class EntryState
{
public string FullName { get; set; }
public DateTimeOffset LastWriteTime { get; set; }
public byte[] RawBytes { get; set; }
public XDocument Document { get; set; }
}
}
internal sealed class VmlTextBinding
{
public string Text { get; set; }
public bool IsEquationText { get; set; }
public bool IsChartText { get; set; }
public Action<string> Apply { get; set; }
}
}