52 lines
3.6 KiB
C#
52 lines
3.6 KiB
C#
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<string>(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; }
|
|
}
|
|
}
|