using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization; using System.Xml.Linq; namespace File_Folder_Helper.Day; internal static partial class Helper20231221 { // Folders with these names will be put in the root instead. private static readonly string[] _BlacklistedFolders = [ "KeePassHttp Passwords", "KeePassXC-Browser Passwords" ]; private static readonly string[] _BlacklistedFields = [ "KeePassXC-Browser Settings", "KeePassHttp Settings" ]; private static Func OtherFields() { return x => { string? key = x.Element("Key")?.Value; return key != "Title" && key != "Notes" && key != "UserName" && key != "Password" && key != "URL" && !_BlacklistedFields.Contains(key); }; } private record Field( [property: JsonPropertyName("name")] string? Name, [property: JsonPropertyName("value")] string? Value, [property: JsonPropertyName("type")] int? Type ); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Field))] private partial class FieldSourceGenerationContext : JsonSerializerContext { } private record Folder( [property: JsonPropertyName("id")] string? Id, [property: JsonPropertyName("name")] string? Name ); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Folder))] private partial class FolderSourceGenerationContext : JsonSerializerContext { } private record Item( [property: JsonPropertyName("revisionDate")] string? RevisionDate, [property: JsonPropertyName("creationDate")] string? CreationDate, [property: JsonPropertyName("folderId")] string? FolderId, [property: JsonPropertyName("type")] int Type, [property: JsonPropertyName("name")] string? Name, [property: JsonPropertyName("notes")] string? Notes, [property: JsonPropertyName("fields")] IReadOnlyList? Fields, [property: JsonPropertyName("login")] Login Login ); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Item))] private partial class ItemSourceGenerationContext : JsonSerializerContext { } private record Login( [property: JsonPropertyName("uris")] IReadOnlyList Uris, [property: JsonPropertyName("username")] string? Username, [property: JsonPropertyName("password")] string? Password ); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Login))] private partial class LoginSourceGenerationContext : JsonSerializerContext { } private record Root( [property: JsonPropertyName("folders")] IReadOnlyList Folders, [property: JsonPropertyName("items")] IReadOnlyList Items ); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Root))] private partial class RootSourceGenerationContext : JsonSerializerContext { } private record Uri( [property: JsonPropertyName("uri")] string? Value, [property: JsonPropertyName("host")] string? Host ); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Uri))] private partial class UriSourceGenerationContext : JsonSerializerContext { } private static Item? GetEntry(string folderId, XElement entry) { Item? result; XElement[] stringFields = entry.Elements("String").ToArray(); string? name = stringFields.Where(x => x.Element("Key")?.Value == "Title").Select(x => x.Element("Value")?.Value).FirstOrDefault(); if (_BlacklistedFields.Contains(name)) result = null; else { XElement[] timesFields = entry.Elements("Times").ToArray(); string? creationTime = timesFields.Elements("CreationTime").FirstOrDefault()?.Value; string? revisionDate = timesFields.Elements("LastModificationTime").FirstOrDefault()?.Value; string? uri = stringFields.Where(x => x.Element("Key")?.Value == "URL").Select(x => x.Element("Value")?.Value).FirstOrDefault(); string? notes = stringFields.Where(x => x.Element("Key")?.Value == "Notes").Select(x => x.Element("Value")?.Value).FirstOrDefault(); string? username = stringFields.Where(x => x.Element("Key")?.Value == "UserName").Select(x => x.Element("Value")?.Value).FirstOrDefault(); string? password = stringFields.Where(x => x.Element("Key")?.Value == "Password").Select(x => x.Element("Value")?.Value).FirstOrDefault(); string? host = string.IsNullOrEmpty(uri) || !uri.Contains(':') ? null : new System.Uri(uri).Host; Login login = new(new Uri[] { new(uri, host) }, username, password); List itemFields = stringFields.Where(OtherFields()).Select(x => new Field(x.Element("Key")?.Value, x.Element("Value")?.Value, 0)).ToList(); result = new(revisionDate, creationTime, folderId, 1, name, string.IsNullOrEmpty(notes) ? null : notes, itemFields.Count > 0 ? itemFields : null, login); } return result; } private static List Filter(List items) { List results = []; string key; Item result; Login login; string? uri; string? host; string? name; string? folderId; string? username; List? check; string? creationTime; string? revisionDate; List notes = []; string? password = null; string?[] checkPasswords; Dictionary> keyValuePairs = []; foreach (Item item in items) { key = string.Concat(item.Login.Username, '-', string.Join('-', item.Login.Uris.Select(l => l.Host))); if (!keyValuePairs.TryGetValue(key, out check)) { keyValuePairs.Add(key, []); if (!keyValuePairs.TryGetValue(key, out check)) throw new NotSupportedException(); } check.Add(item); } foreach (KeyValuePair> keyValuePair in keyValuePairs) { if (keyValuePair.Value.Count == 1) results.AddRange(keyValuePair.Value); else { checkPasswords = keyValuePair.Value.Select(l => l.Login.Password).Distinct().ToArray(); if (checkPasswords.Length == 1) results.Add(keyValuePair.Value[0]); else { uri = null; host = null; name = null; notes.Clear(); folderId = null; username = null; creationTime = null; revisionDate = null; notes.Add("Unset Password"); foreach (Item item in from l in keyValuePair.Value orderby l.RevisionDate, l.Login.Password?.Length descending select l) { if (item.Login.Uris.Count == 1) { uri = item.Login.Uris[0].Value; host = item.Login.Uris[0].Host; } name = item.Name; folderId = item.FolderId; username = item.Login.Username; creationTime = item.CreationDate; revisionDate = item.RevisionDate; notes.Add($"{item.Login.Password} on {item.RevisionDate}"); } login = new(new Uri[] { new(uri, host) }, username, password); result = new(revisionDate, creationTime, folderId, 1, name, string.Join(Environment.NewLine, notes), null, login); results.Add(result); } } } results = (from l in results orderby l.Login.Uris[0].Host, l.Login.Username select l).ToList(); return results; } private static void SaveTabSeparatedValueFiles(List items, string xmlFile) { List lines = []; foreach (Item item in items) lines.Add($"{item.Login.Uris[0].Host}\t{item.Login.Username}\t{item.Login.Password}"); File.WriteAllLines(Path.ChangeExtension(xmlFile, ".tvs"), lines); } internal static void ConvertKeePassExport(ILogger logger, List args) { Root root; Item? item; string json; string folderId; string groupKey; List items; XElement element; string newGroupKey; XDocument xDocument; List folders; string childGroupName; string outputPath = args[0]; string newExtension = args[3]; IEnumerable childEntries; IEnumerable childElements; Dictionary namesToIds; Queue> groupsToProcess; string[] xmlFiles = Directory.GetFiles(args[0], args[2]); foreach (string xmlFile in xmlFiles) { xDocument = XDocument.Load(xmlFile); if (xDocument.Root is null) throw new Exception("Root element missing"); items = []; folders = []; namesToIds = []; groupsToProcess = []; logger.LogInformation($"Loaded XML {xmlFile}.", xmlFile); groupsToProcess.Enqueue(new KeyValuePair("", xDocument.Root.Descendants("Group").First())); while (groupsToProcess.TryDequeue(out KeyValuePair valuePair)) { groupKey = valuePair.Key; element = valuePair.Value; folderId = Guid.NewGuid().ToString(); childElements = element.Elements("Group"); folders.Add(new Folder(folderId, groupKey)); foreach (XElement childElement in childElements) { childGroupName = (childElement.Element("Name")?.Value) ?? throw new Exception("Found group with no name, malformed file"); if (_BlacklistedFolders.Contains(childGroupName)) childGroupName = ""; newGroupKey = $"{groupKey}/{childGroupName}"; if (groupKey == "") newGroupKey = childGroupName; logger.LogInformation($"Found group '{newGroupKey}'"); groupsToProcess.Enqueue(new KeyValuePair(newGroupKey, childElement)); } childEntries = element.Elements("Entry"); foreach (XElement entry in childEntries) { item = GetEntry(folderId, entry); if (item is null) continue; items.Add(item); } } items = Filter(items); root = new(folders, items); logger.LogInformation("Serializing output file..."); json = JsonSerializer.Serialize(root, RootSourceGenerationContext.Default.Root); logger.LogInformation("Writing output file..."); File.WriteAllText(Path.ChangeExtension(xmlFile, newExtension), json); SaveTabSeparatedValueFiles(items, xmlFile); } logger.LogInformation("Done!"); } }