using WxAgent.Core; namespace WxAgent.Windows; public sealed record AccountDatabaseFile(string FullPath, string RelativePath, byte[] FirstPage, string Salt); public sealed record AccountDatabaseRoot(string AccountRootPath, string Fingerprint, IReadOnlyList Databases); public static class WechatDatabaseDiscovery { public static IReadOnlyList FindAccountRoots( string? xwechatFilesRoot = null, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var root = xwechatFilesRoot ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "xwechat_files"); if (!Directory.Exists(root)) { return []; } var accounts = new List(); foreach (var accountDirectory in Directory.EnumerateDirectories(root)) { cancellationToken.ThrowIfCancellationRequested(); var dbStorage = Path.Combine(accountDirectory, "db_storage"); if (!Directory.Exists(dbStorage)) { continue; } var databases = new List(); foreach (var path in Directory.EnumerateFiles(dbStorage, "*.db", SearchOption.AllDirectories)) { cancellationToken.ThrowIfCancellationRequested(); try { var page = new byte[SqlCipherPageVerifier.PageSize]; using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); stream.ReadExactly(page); var relativePath = Path.GetRelativePath(dbStorage, path).Replace('\\', '/'); databases.Add(new AccountDatabaseFile(path, relativePath, page, Convert.ToHexString(page.AsSpan(0, SqlCipherPageVerifier.SaltSize)).ToLowerInvariant())); } catch (IOException) { } catch (UnauthorizedAccessException) { } } accounts.Add(new AccountDatabaseRoot(dbStorage, AccountRootFingerprint.Create(dbStorage), databases)); } return accounts; } }