Add db merged with exact account/chat/local-ID selection and shard ambiguity checks. Preserve Int64 composite message types and parse bounded, DTD-free embedded record XML, including duplicate messages and source metadata. Revalidate cached keys against current page-1 HMAC; replace the SQL write probe with sqlite3_db_readonly. Core tests: 129 passed. Windows Session 0 database validation: 5/5; all 16 text/sender/source-ID/timestamp entries matched independent XML parsing. Read-only UI baseline: 3/3; zero sends.
85 lines
4.9 KiB
C#
85 lines
4.9 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Xml.Linq;
|
|
using WxAgent.Core;
|
|
using Xunit;
|
|
using ZstdSharp;
|
|
|
|
namespace WxAgent.Core.Tests;
|
|
|
|
public sealed class WechatMergedChatTests
|
|
{
|
|
[Fact]
|
|
public void ParsesEmbeddedTextMetadataWithoutDeduplicatingRepeatedMessages()
|
|
{
|
|
var result = WechatMergedChatParser.Parse(Wrap(Record(Item("same\n\ntext"), Item("same\n\ntext"))));
|
|
Assert.Equal(2, result.Messages.Count);
|
|
Assert.Equal("0", result.Messages[0].Path);
|
|
Assert.Equal("1", result.Messages[1].Path);
|
|
Assert.Equal("same\n\ntext", result.Messages[0].Text);
|
|
Assert.Equal("Synthetic sender", result.Messages[0].SenderName);
|
|
Assert.Equal("opaque-hash", result.Messages[0].SenderHash);
|
|
Assert.Equal("18446744073709551615", result.Messages[0].SourceServerId);
|
|
Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1700000000), result.Messages[0].Timestamp);
|
|
}
|
|
|
|
[Fact]
|
|
public void SupportsNestedRecordsAndExplicitAttachmentMetadata()
|
|
{
|
|
var attachment = new XElement("dataitem", new XAttribute("datatype", 8),
|
|
new XElement("datatitle", "example.txt"), new XElement("datafmt", "txt"), new XElement("datasize", 42),
|
|
new XElement("cdn_dataurl", "must-not-be-exported"), new XElement("aeskey", "must-not-be-exported"));
|
|
var nested = new XElement("dataitem", new XAttribute("datatype", 17), Record(Item("nested")));
|
|
var result = WechatMergedChatParser.Parse(Record(attachment, nested).ToString());
|
|
Assert.Null(result.Messages[0].Text);
|
|
Assert.Equal(42, result.Messages[0].SizeBytes);
|
|
Assert.Equal("1/0", result.Messages[1].NestedRecord!.Messages[0].Path);
|
|
Assert.DoesNotContain("must-not-be-exported", JsonSerializer.Serialize(result));
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectsTruncationMissingPayloadAndInvalidTimestamp()
|
|
{
|
|
var record = Record(Item("hello"));
|
|
record.Element("datalist")!.SetAttributeValue("count", 2);
|
|
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(record.ToString()));
|
|
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(Record(new XElement("dataitem", new XAttribute("datatype", 1))).ToString()));
|
|
var item = Item("hello");
|
|
item.Element("srcMsgCreateTime")!.Value = long.MaxValue.ToString();
|
|
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(Record(item).ToString()));
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectsDtdAndBoundViolations()
|
|
{
|
|
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse("<!DOCTYPE recordinfo [<!ENTITY x SYSTEM 'file:///not-read'>]><recordinfo>&x;</recordinfo>"));
|
|
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(new string('x', WechatMergedChatParser.MaxXmlCharacters + 1)));
|
|
var nested = Record(Item("innermost"));
|
|
for (var i = 0; i < WechatMergedChatParser.MaxNesting; i++)
|
|
nested = Record(new XElement("dataitem", new XAttribute("datatype", 17), nested));
|
|
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(nested.ToString()));
|
|
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(Record(Enumerable.Range(0, WechatMergedChatParser.MaxMessages + 1).Select(_ => Item("x")).ToArray()).ToString()));
|
|
}
|
|
|
|
[Fact]
|
|
public void PreservesComposite64BitMessageTypeAndBoundsZstdDecoding()
|
|
{
|
|
var message = new DbMessage(149, 123, "filehelper", null, null, null, 81604378673L, "xml", DateTimeOffset.UnixEpoch, null);
|
|
using var json = JsonDocument.Parse(JsonSerializer.Serialize(message));
|
|
Assert.Equal(81604378673L, json.RootElement.GetProperty("Type").GetInt64());
|
|
using var output = new MemoryStream();
|
|
using (var compressed = new CompressionStream(output))
|
|
compressed.Write(Encoding.UTF8.GetBytes(new string('x', WechatMergedChatParser.MaxXmlCharacters + 1)));
|
|
Assert.Throws<InvalidDataException>(() => WechatDbMessage.DecodeContent(Convert.ToHexString(output.ToArray()), true));
|
|
}
|
|
|
|
private static XElement Item(string text) => new("dataitem", new XAttribute("datatype", 1), new XAttribute("dataid", "repeated-id"),
|
|
new XElement("datadesc", text), new XElement("sourcename", "Synthetic sender"), new XElement("srcMsgLocalId", "7"),
|
|
new XElement("srcMsgCreateTime", "1700000000"), new XElement("fromnewmsgid", "18446744073709551615"),
|
|
new XElement("dataitemsource", new XElement("hashusername", "opaque-hash")));
|
|
private static XElement Record(params XElement[] items) => new("recordinfo", new XElement("title", "Synthetic record"),
|
|
new XElement("datalist", new XAttribute("count", items.Length), items));
|
|
private static string Wrap(XElement record) => new XElement("msg", new XElement("appmsg", new XElement("type", 19),
|
|
new XElement("recorditem", new XCData(record.ToString(SaveOptions.DisableFormatting))))).ToString();
|
|
}
|