using System.Text.Json; using System.Xml; using System.Xml.Linq; namespace QSafe.Core; public sealed record ImportPreview(VaultData Data, int Added, int Duplicates, int Skipped, int Attachments); public static class XmlImport { public static ImportPreview Preview(byte[] bytes, VaultData current) { if (bytes.Length > 24 * 1024 * 1024) throw new InvalidDataException("XML превышает 24 МиБ."); using var stream = new MemoryStream(bytes); using var reader = XmlReader.Create(stream, new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null, MaxCharactersInDocument = 32 * 1024 * 1024 }); var doc = XDocument.Load(reader); if (doc.Root?.Name.LocalName != "database") throw new InvalidDataException("Это не XML-экспорт SafeInCloud."); var data = Vault.Copy(current); var labels = doc.Root.Elements("label").ToDictionary(e => (string?)e.Attribute("id") ?? "", e => (string?)e.Attribute("name") ?? ""); var fingerprints = new HashSet(data.Entries.Select(e => e.Extra != null && e.Extra.TryGetValue("safeInCloudFingerprint", out var fp) && fp.ValueKind == JsonValueKind.String ? fp.GetString() ?? "" : "")); int added = 0, duplicates = 0, skipped = 0, attachments = 0; foreach (var card in doc.Root.Elements("card")) { bool Flag(string name) => ((string?)card.Attribute(name)) is "true" or "1"; if (Flag("deleted") || Flag("template")) { skipped++; continue; } var xml = card.ToString(SaveOptions.DisableFormatting); // Preserve Android's original XML; XML serializers may use different formatting across platforms. var fingerprint = VaultCrypto.Hash(System.Text.Encoding.UTF8.GetBytes(xml)); bool sameSource = data.Entries.Any(e => e.Extra != null && e.Extra.TryGetValue("safeInCloudXml", out var source) && Equivalent(source.GetString(), card)); if (fingerprints.Contains(fingerprint) || sameSource) { duplicates++; continue; } var entry = new Entry { Title = (string?)card.Attribute("title") ?? "Без названия", Star = Flag("star"), Notes = (string?)card.Element("notes") ?? "" }; entry.Fields = card.Elements("field").Select(f => new Field { Name = (string?)f.Attribute("name") ?? "", Type = (string?)f.Attribute("type") ?? "text", Value = f.Value }).ToList(); entry.Tags = card.Elements("label_id").Select(e => labels.GetValueOrDefault(e.Value.Trim(), "Метка " + e.Value.Trim())).ToList(); var files = card.Elements().Where(e => e.Name.LocalName is "file" or "image").Select(e => new { kind = e.Name.LocalName, name = (string?)e.Attribute("name") ?? "", data = e.Value }).ToArray(); entry.Extra = new() { ["safeInCloudXml"] = JsonSerializer.SerializeToElement(xml), ["safeInCloudFingerprint"] = JsonSerializer.SerializeToElement(fingerprint), ["attachments"] = JsonSerializer.SerializeToElement(files) }; attachments += files.Length; data.Entries.Add(entry); fingerprints.Add(fingerprint); added++; } return new(data, added, duplicates, skipped, attachments); } private static bool Equivalent(string? source, XElement card) { if (source == null) return false; try { using var text = new StringReader(source); using var reader = XmlReader.Create(text, new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null, MaxCharactersInDocument = 32 * 1024 * 1024 }); return XNode.DeepEquals(XElement.Load(reader), card); } catch (XmlException) { return false; } } }