refactor: split node agent and Go control plane
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
using System.Globalization;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace WxAgent.Core;
|
||||
|
||||
public sealed record WechatMergedMessage(
|
||||
string Path, string? DataId, int DataType, string? SenderName, string? SenderHash,
|
||||
string? SourceLocalId, string? SourceServerId, DateTimeOffset? Timestamp, string? DisplayTime,
|
||||
string? Text, string? Title, string? Format, long? SizeBytes, WechatMergedChat? NestedRecord);
|
||||
|
||||
public sealed record WechatMergedChat(string? Title, string? Description, IReadOnlyList<WechatMergedMessage> Messages);
|
||||
|
||||
public sealed record DbMergedChat(string DatabaseRelativePath, DbMessage Parent, WechatMergedChat Record);
|
||||
|
||||
public static class WechatMergedChatParser
|
||||
{
|
||||
public const int MaxXmlCharacters = 4 * 1024 * 1024;
|
||||
public const int MaxMessages = 5000;
|
||||
public const int MaxNesting = 8;
|
||||
|
||||
public static WechatMergedChat Parse(string xml)
|
||||
{
|
||||
var budget = MaxXmlCharacters * 2;
|
||||
var remaining = MaxMessages;
|
||||
try
|
||||
{
|
||||
var root = Load(xml, ref budget);
|
||||
if (root.Name == "msg")
|
||||
{
|
||||
var app = root.Element("appmsg") ?? throw Invalid("Missing appmsg.");
|
||||
if (app.Element("type")?.Value != "19") throw Invalid("Not a merged-chat app message.");
|
||||
root = ReadRecord(app.Element("recorditem") ?? throw Invalid("Missing recorditem."), ref budget);
|
||||
}
|
||||
return ParseRecord(root, "", 0, ref remaining, ref budget);
|
||||
}
|
||||
catch (XmlException ex) { throw new InvalidDataException("Invalid merged-chat XML.", ex); }
|
||||
catch (ArgumentOutOfRangeException ex) { throw new InvalidDataException("Invalid merged-chat timestamp.", ex); }
|
||||
}
|
||||
|
||||
private static WechatMergedChat ParseRecord(XElement record, string parentPath, int depth, ref int remaining, ref int budget)
|
||||
{
|
||||
if (depth >= MaxNesting) throw Invalid("Nested record limit exceeded.");
|
||||
if (record.Name != "recordinfo") throw Invalid("Expected recordinfo.");
|
||||
var list = record.Element("datalist") ?? throw Invalid("Missing datalist.");
|
||||
var items = list.Elements().ToArray();
|
||||
if (items.Any(item => item.Name != "dataitem")) throw Invalid("Unknown datalist element.");
|
||||
if (items.Length > remaining) throw Invalid("Merged-message count limit exceeded.");
|
||||
remaining -= items.Length;
|
||||
var count = (string?)list.Attribute("count");
|
||||
if (count is not null && (!int.TryParse(count, NumberStyles.None, CultureInfo.InvariantCulture, out var expected) || expected != items.Length))
|
||||
throw Invalid("Declared merged-message count does not match the embedded record.");
|
||||
var messages = new List<WechatMergedMessage>(items.Length);
|
||||
for (var index = 0; index < items.Length; index++)
|
||||
{
|
||||
var item = items[index];
|
||||
if (!int.TryParse((string?)item.Attribute("datatype"), NumberStyles.None, CultureInfo.InvariantCulture, out var type))
|
||||
throw Invalid("Missing or invalid merged-message datatype.");
|
||||
var path = parentPath.Length == 0 ? index.ToString(CultureInfo.InvariantCulture) : $"{parentPath}/{index}";
|
||||
var text = Value(item, "datadesc");
|
||||
if (type == 1 && text is null) throw Invalid("Text record has no datadesc.");
|
||||
var seconds = Integer(item, "srcMsgCreateTime");
|
||||
var source = item.Element("dataitemsource");
|
||||
var nestedElement = item.Element("recordinfo") ?? item.Element("recorditem");
|
||||
var nested = nestedElement is null ? null : ParseRecord(ReadRecord(nestedElement, ref budget), path, depth + 1, ref remaining, ref budget);
|
||||
messages.Add(new WechatMergedMessage(path, (string?)item.Attribute("dataid"), type,
|
||||
Value(item, "sourcename"), source is null ? null : Value(source, "hashusername"),
|
||||
Value(item, "srcMsgLocalId"), Value(item, "fromnewmsgid"),
|
||||
seconds is null ? null : DateTimeOffset.FromUnixTimeSeconds(seconds.Value), Value(item, "sourcetime"),
|
||||
text, Value(item, "datatitle"), Value(item, "datafmt"), Integer(item, "datasize"), nested));
|
||||
}
|
||||
return new WechatMergedChat(Value(record, "title"), Value(record, "desc"), messages);
|
||||
}
|
||||
|
||||
private static XElement ReadRecord(XElement element, ref int budget)
|
||||
{
|
||||
if (element.Name == "recordinfo" && element.HasElements) return element;
|
||||
if (element.HasElements)
|
||||
{
|
||||
var children = element.Elements().ToArray();
|
||||
if (children.Length != 1 || children[0].Name != "recordinfo") throw Invalid("Ambiguous embedded record.");
|
||||
return children[0];
|
||||
}
|
||||
return Load(element.Value, ref budget);
|
||||
}
|
||||
|
||||
private static XElement Load(string xml, ref int budget)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(xml) || xml.Length > MaxXmlCharacters || xml.Length > budget)
|
||||
throw Invalid("Merged-chat XML is empty or exceeds the size limit.");
|
||||
budget -= xml.Length;
|
||||
var settings = new XmlReaderSettings
|
||||
{
|
||||
DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null,
|
||||
MaxCharactersInDocument = MaxXmlCharacters, MaxCharactersFromEntities = 0
|
||||
};
|
||||
// Preflight depth before building a DOM; untrusted forwarded XML must stay bounded.
|
||||
using (var check = XmlReader.Create(new StringReader(xml), settings))
|
||||
while (check.Read())
|
||||
if (check.Depth > 64) throw Invalid("XML depth limit exceeded.");
|
||||
using var reader = XmlReader.Create(new StringReader(xml), settings);
|
||||
return XElement.Load(reader, LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
|
||||
private static string? Value(XElement element, string name) => element.Element(name)?.Value;
|
||||
|
||||
private static long? Integer(XElement element, string name)
|
||||
{
|
||||
var text = Value(element, name);
|
||||
if (string.IsNullOrEmpty(text)) return null;
|
||||
if (!long.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out var number))
|
||||
throw Invalid($"Invalid {name}.");
|
||||
return number;
|
||||
}
|
||||
|
||||
private static InvalidDataException Invalid(string message) => new(message);
|
||||
}
|
||||
Reference in New Issue
Block a user