From e63d318573b49b05f343989233f55255d3729684 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 14:20:46 +0000 Subject: [PATCH 1/8] Initial plan From d01c195b516b25595b6909c076cd7c59f2c5a09c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 14:27:34 +0000 Subject: [PATCH 2/8] Migrate profile files from XML to JSON format Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- .../NETworkManager.Profiles/ProfileManager.cs | 277 +++++++++++++++--- 1 file changed, 238 insertions(+), 39 deletions(-) diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index 19784d812b..613289444b 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -9,6 +9,8 @@ using System.Linq; using System.Security; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Xml.Serialization; namespace NETworkManager.Profiles; @@ -31,13 +33,30 @@ public static class ProfileManager /// /// Profile file extension. /// - private const string ProfileFileExtension = ".xml"; + private const string ProfileFileExtension = ".json"; + + /// + /// Legacy XML profile file extension. + /// + [Obsolete("Legacy XML profile files are no longer used, but the extension is kept for migration purposes.")] + private const string LegacyProfileFileExtension = ".xml"; /// /// Profile file extension for encrypted files. /// private const string ProfileFileExtensionEncrypted = ".encrypted"; + /// + /// JSON serializer options for consistent serialization/deserialization. + /// + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + Converters = { new JsonStringEnumConverter() } + }; + /// /// ObservableCollection of all profile files. /// @@ -160,15 +179,17 @@ private static string GetProfilesDefaultFilePath() #region Get and load profile files /// - /// Get all files in the folder with the extension or - /// . + /// Get all files in the folder with the extension , + /// or . /// /// Path of the profile folder. /// List of profile files. private static IEnumerable GetProfileFiles(string location) { return Directory.GetFiles(location).Where(x => - Path.GetExtension(x) == ProfileFileExtension || Path.GetExtension(x) == ProfileFileExtensionEncrypted); + Path.GetExtension(x) == ProfileFileExtension || + Path.GetExtension(x) == LegacyProfileFileExtension || + Path.GetExtension(x) == ProfileFileExtensionEncrypted); } /// @@ -282,16 +303,24 @@ public static void EnableEncryption(ProfileFileInfo profileFileInfo, SecureStrin // Create a new profile info with the encryption infos var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name, - Path.ChangeExtension(profileFileInfo.Path, ProfileFileExtensionEncrypted), true) + Path.ChangeExtension(Path.ChangeExtension(profileFileInfo.Path, null), ProfileFileExtension + ProfileFileExtensionEncrypted), true) { Password = password, IsPasswordValid = true }; - // Load the profiles from the profile file - var profiles = DeserializeFromFile(profileFileInfo.Path); + // Load the profiles from the profile file (handle both XML and JSON) + List profiles; + if (Path.GetExtension(profileFileInfo.Path) == LegacyProfileFileExtension) + { + profiles = DeserializeFromXmlFile(profileFileInfo.Path); + } + else + { + profiles = DeserializeFromFile(profileFileInfo.Path); + } - // Save the encrypted file + // Save the encrypted file in JSON format var decryptedBytes = SerializeToByteArray(profiles); var encryptedBytes = CryptoHelper.Encrypt(decryptedBytes, SecureStringHelper.ConvertToString(newProfileFileInfo.Password), @@ -337,7 +366,7 @@ public static void ChangeMasterPassword(ProfileFileInfo profileFileInfo, SecureS // Create a new profile info with the encryption infos var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name, - Path.ChangeExtension(profileFileInfo.Path, ProfileFileExtensionEncrypted), true) + Path.ChangeExtension(Path.ChangeExtension(profileFileInfo.Path, null), ProfileFileExtension + ProfileFileExtensionEncrypted), true) { Password = newPassword, IsPasswordValid = true @@ -348,9 +377,19 @@ public static void ChangeMasterPassword(ProfileFileInfo profileFileInfo, SecureS var decryptedBytes = CryptoHelper.Decrypt(encryptedBytes, SecureStringHelper.ConvertToString(password), GlobalStaticConfiguration.Profile_EncryptionKeySize, GlobalStaticConfiguration.Profile_EncryptionIterations); - var profiles = DeserializeFromByteArray(decryptedBytes); - // Save the encrypted file + // Check if decrypted content is XML or JSON and deserialize accordingly + List profiles; + if (IsXmlContent(decryptedBytes)) + { + profiles = DeserializeFromXmlByteArray(decryptedBytes); + } + else + { + profiles = DeserializeFromByteArray(decryptedBytes); + } + + // Save the encrypted file in JSON format decryptedBytes = SerializeToByteArray(profiles); encryptedBytes = CryptoHelper.Encrypt(decryptedBytes, SecureStringHelper.ConvertToString(newProfileFileInfo.Password), @@ -392,16 +431,26 @@ public static void DisableEncryption(ProfileFileInfo profileFileInfo, SecureStri // Create a new profile info var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name, - Path.ChangeExtension(profileFileInfo.Path, ProfileFileExtension)); + Path.ChangeExtension(Path.ChangeExtension(profileFileInfo.Path, null), ProfileFileExtension)); // Load and decrypt the profiles from the profile file var encryptedBytes = File.ReadAllBytes(profileFileInfo.Path); var decryptedBytes = CryptoHelper.Decrypt(encryptedBytes, SecureStringHelper.ConvertToString(password), GlobalStaticConfiguration.Profile_EncryptionKeySize, GlobalStaticConfiguration.Profile_EncryptionIterations); - var profiles = DeserializeFromByteArray(decryptedBytes); - // Save the decrypted profiles to the profile file + // Check if decrypted content is XML or JSON and deserialize accordingly + List profiles; + if (IsXmlContent(decryptedBytes)) + { + profiles = DeserializeFromXmlByteArray(decryptedBytes); + } + else + { + profiles = DeserializeFromByteArray(decryptedBytes); + } + + // Save the decrypted profiles to the profile file in JSON format SerializeToFile(newProfileFileInfo.Path, profiles); // Add the new profile @@ -433,6 +482,9 @@ private static void Load(ProfileFileInfo profileFileInfo) if (File.Exists(profileFileInfo.Path)) { + // Detect if the file is a legacy XML file and needs migration + var isLegacyXmlFile = Path.GetExtension(profileFileInfo.Path) == LegacyProfileFileExtension; + if (profileFileInfo.IsEncrypted) { var encryptedBytes = File.ReadAllBytes(profileFileInfo.Path); @@ -441,7 +493,58 @@ private static void Load(ProfileFileInfo profileFileInfo) GlobalStaticConfiguration.Profile_EncryptionKeySize, GlobalStaticConfiguration.Profile_EncryptionIterations); - AddGroups(DeserializeFromByteArray(decryptedBytes)); + List groups; + + // Check if decrypted content is XML or JSON + if (IsXmlContent(decryptedBytes)) + { + Log.Info($"Legacy XML profile file found (encrypted): {profileFileInfo.Path}. Migrating to JSON format..."); + groups = DeserializeFromXmlByteArray(decryptedBytes); + + // Migrate to JSON format by saving the file + var newPath = Path.ChangeExtension(profileFileInfo.Path, ProfileFileExtension + ProfileFileExtensionEncrypted); + var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name, newPath, true) + { + Password = profileFileInfo.Password, + IsPasswordValid = true + }; + + var jsonBytes = SerializeToByteArray([.. groups]); + var encryptedJsonBytes = CryptoHelper.Encrypt(jsonBytes, + SecureStringHelper.ConvertToString(newProfileFileInfo.Password), + GlobalStaticConfiguration.Profile_EncryptionKeySize, + GlobalStaticConfiguration.Profile_EncryptionIterations); + + File.WriteAllBytes(newPath, encryptedJsonBytes); + + // Update ProfileFiles collection + ProfileFiles.Remove(profileFileInfo); + ProfileFiles.Add(newProfileFileInfo); + + // Update the reference + profileFileInfo = newProfileFileInfo; + + // Delete the old XML file + if (File.Exists(profileFileInfo.Path) && Path.GetExtension(profileFileInfo.Path) != newPath) + { + try + { + File.Delete(Path.ChangeExtension(newPath, LegacyProfileFileExtension + ProfileFileExtensionEncrypted)); + } + catch + { + // Ignore if file doesn't exist or can't be deleted + } + } + + Log.Info($"Profile migration from XML to JSON completed successfully: {newPath}"); + } + else + { + groups = DeserializeFromByteArray(decryptedBytes); + } + + AddGroups(groups); // Password is valid ProfileFiles.FirstOrDefault(x => x.Equals(profileFileInfo))!.IsPasswordValid = true; @@ -450,7 +553,39 @@ private static void Load(ProfileFileInfo profileFileInfo) } else { - AddGroups(DeserializeFromFile(profileFileInfo.Path)); + List groups; + + if (isLegacyXmlFile) + { + Log.Info($"Legacy XML profile file found: {profileFileInfo.Path}. Migrating to JSON format..."); + groups = DeserializeFromXmlFile(profileFileInfo.Path); + + // Migrate to JSON format by saving the file + var newPath = Path.ChangeExtension(profileFileInfo.Path, ProfileFileExtension); + SerializeToFile(newPath, groups); + + var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name, newPath); + + // Update ProfileFiles collection + ProfileFiles.Remove(profileFileInfo); + ProfileFiles.Add(newProfileFileInfo); + + // Delete the old XML file + File.Delete(profileFileInfo.Path); + + // Update the reference + profileFileInfo = newProfileFileInfo; + + Log.Info($"Profile migration from XML to JSON completed successfully: {newPath}"); + + loadedProfileUpdated = true; + } + else + { + groups = DeserializeFromFile(profileFileInfo.Path); + } + + AddGroups(groups); } } else @@ -468,6 +603,27 @@ private static void Load(ProfileFileInfo profileFileInfo) LoadedProfileFileChanged(LoadedProfileFile, true); } + /// + /// Method to check if the byte array content is XML. + /// + /// Byte array to check. + /// True if the content is XML. + private static bool IsXmlContent(byte[] data) + { + if (data == null || data.Length == 0) + return false; + + try + { + var text = Encoding.UTF8.GetString(data).TrimStart(); + return text.StartsWith(" /// Method to save the currently loaded profiles based on the infos provided in the . /// @@ -539,17 +695,15 @@ public static void Switch(ProfileFileInfo info, bool saveLoadedProfiles = true) #region Serialize and deserialize /// - /// Method to serialize a list of groups as to an xml file. + /// Method to serialize a list of groups as to a JSON file. /// - /// Path to an xml file. + /// Path to a JSON file. /// List of the groups as to serialize. private static void SerializeToFile(string filePath, List groups) { - var xmlSerializer = new XmlSerializer(typeof(List)); - - using var fileStream = new FileStream(filePath, FileMode.Create); + var jsonString = JsonSerializer.Serialize(SerializeGroup(groups), JsonOptions); - xmlSerializer.Serialize(fileStream, SerializeGroup(groups)); + File.WriteAllText(filePath, jsonString); } /// @@ -559,15 +713,9 @@ private static void SerializeToFile(string filePath, List groups) /// Serialized list of groups as as byte array. private static byte[] SerializeToByteArray(List groups) { - var xmlSerializer = new XmlSerializer(typeof(List)); - - using var memoryStream = new MemoryStream(); - - using var streamWriter = new StreamWriter(memoryStream, Encoding.UTF8); + var jsonString = JsonSerializer.Serialize(SerializeGroup(groups), JsonOptions); - xmlSerializer.Serialize(streamWriter, SerializeGroup(groups)); - - return memoryStream.ToArray(); + return Encoding.UTF8.GetBytes(jsonString); } /// @@ -629,39 +777,90 @@ private static List SerializeGroup(List groups } /// - /// Method to deserialize a list of groups as from an xml file. + /// Method to deserialize a list of groups as from a JSON file. /// - /// Path to an xml file. + /// Path to a JSON file. /// List of groups as . private static List DeserializeFromFile(string filePath) + { + var jsonString = File.ReadAllText(filePath); + + return DeserializeFromJson(jsonString); + } + + /// + /// Method to deserialize a list of groups as from a legacy XML file. + /// + /// Path to an XML file. + /// List of groups as . + [Obsolete("Legacy XML profile files are no longer used, but the method is kept for migration purposes.")] + private static List DeserializeFromXmlFile(string filePath) { using FileStream fileStream = new(filePath, FileMode.Open); - return DeserializeGroup(fileStream); + return DeserializeFromXmlStream(fileStream); } /// /// Method to deserialize a list of groups as from a byte array. /// - /// Serialized list of groups as as byte array. + /// Serialized list of groups as as byte array. + /// List of groups as . + private static List DeserializeFromByteArray(byte[] data) + { + var jsonString = Encoding.UTF8.GetString(data); + + return DeserializeFromJson(jsonString); + } + + /// + /// Method to deserialize a list of groups as from a legacy XML byte array. + /// + /// Serialized list of groups as as XML byte array. /// List of groups as . - private static List DeserializeFromByteArray(byte[] xml) + [Obsolete("Legacy XML profile files are no longer used, but the method is kept for migration purposes.")] + private static List DeserializeFromXmlByteArray(byte[] xml) { using MemoryStream memoryStream = new(xml); - return DeserializeGroup(memoryStream); + return DeserializeFromXmlStream(memoryStream); } /// - /// Method to deserialize a list of groups as . + /// Method to deserialize a list of groups as from JSON string. + /// + /// JSON string to deserialize. + /// List of groups as . + private static List DeserializeFromJson(string jsonString) + { + var groupsSerializable = JsonSerializer.Deserialize>(jsonString, JsonOptions); + + return DeserializeGroup(groupsSerializable); + } + + /// + /// Method to deserialize a list of groups as from an XML stream. /// /// Stream to deserialize. /// List of groups as . - private static List DeserializeGroup(Stream stream) + [Obsolete("Legacy XML profile files are no longer used, but the method is kept for migration purposes.")] + private static List DeserializeFromXmlStream(Stream stream) { XmlSerializer xmlSerializer = new(typeof(List)); - return (from groupSerializable in ((List)xmlSerializer.Deserialize(stream))! + var groupsSerializable = (List)xmlSerializer.Deserialize(stream); + + return DeserializeGroup(groupsSerializable); + } + + /// + /// Method to deserialize a list of groups as . + /// + /// List of serializable groups to deserialize. + /// List of groups as . + private static List DeserializeGroup(List groupsSerializable) + { + return (from groupSerializable in groupsSerializable! let profiles = groupSerializable.Profiles.Select(profileSerializable => new ProfileInfo(profileSerializable) { // Migrate old tags to new tags list From 2989d87e369b4e566d38d5499307a3bc3a819577 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 14:32:16 +0000 Subject: [PATCH 3/8] Fix code review issues: file deletion logic, XML detection, and comments Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- .../NETworkManager.Profiles/ProfileManager.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index 613289444b..848ae74d57 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -501,6 +501,9 @@ private static void Load(ProfileFileInfo profileFileInfo) Log.Info($"Legacy XML profile file found (encrypted): {profileFileInfo.Path}. Migrating to JSON format..."); groups = DeserializeFromXmlByteArray(decryptedBytes); + // Store the original path before updating + var originalPath = profileFileInfo.Path; + // Migrate to JSON format by saving the file var newPath = Path.ChangeExtension(profileFileInfo.Path, ProfileFileExtension + ProfileFileExtensionEncrypted); var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name, newPath, true) @@ -525,16 +528,13 @@ private static void Load(ProfileFileInfo profileFileInfo) profileFileInfo = newProfileFileInfo; // Delete the old XML file - if (File.Exists(profileFileInfo.Path) && Path.GetExtension(profileFileInfo.Path) != newPath) + try + { + File.Delete(originalPath); + } + catch { - try - { - File.Delete(Path.ChangeExtension(newPath, LegacyProfileFileExtension + ProfileFileExtensionEncrypted)); - } - catch - { - // Ignore if file doesn't exist or can't be deleted - } + // Ignore if file doesn't exist or can't be deleted } Log.Info($"Profile migration from XML to JSON completed successfully: {newPath}"); @@ -616,7 +616,8 @@ private static bool IsXmlContent(byte[] data) try { var text = Encoding.UTF8.GetString(data).TrimStart(); - return text.StartsWith(" Date: Sun, 28 Dec 2025 14:33:55 +0000 Subject: [PATCH 4/8] Fix null checks, exception handling, and unnecessary collection copy Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- Source/NETworkManager.Profiles/ProfileManager.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index 848ae74d57..63a420386d 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -512,7 +512,7 @@ private static void Load(ProfileFileInfo profileFileInfo) IsPasswordValid = true }; - var jsonBytes = SerializeToByteArray([.. groups]); + var jsonBytes = SerializeToByteArray(groups); var encryptedJsonBytes = CryptoHelper.Encrypt(jsonBytes, SecureStringHelper.ConvertToString(newProfileFileInfo.Password), GlobalStaticConfiguration.Profile_EncryptionKeySize, @@ -532,9 +532,9 @@ private static void Load(ProfileFileInfo profileFileInfo) { File.Delete(originalPath); } - catch + catch (Exception ex) { - // Ignore if file doesn't exist or can't be deleted + Log.Warn($"Failed to delete old XML profile file: {originalPath}. Error: {ex.Message}"); } Log.Info($"Profile migration from XML to JSON completed successfully: {newPath}"); @@ -849,7 +849,10 @@ private static List DeserializeFromXmlStream(Stream stream) { XmlSerializer xmlSerializer = new(typeof(List)); - var groupsSerializable = (List)xmlSerializer.Deserialize(stream); + var groupsSerializable = xmlSerializer.Deserialize(stream) as List; + + if (groupsSerializable == null) + throw new InvalidOperationException("Failed to deserialize XML profile file."); return DeserializeGroup(groupsSerializable); } @@ -861,7 +864,10 @@ private static List DeserializeFromXmlStream(Stream stream) /// List of groups as . private static List DeserializeGroup(List groupsSerializable) { - return (from groupSerializable in groupsSerializable! + if (groupsSerializable == null) + throw new ArgumentNullException(nameof(groupsSerializable)); + + return (from groupSerializable in groupsSerializable let profiles = groupSerializable.Profiles.Select(profileSerializable => new ProfileInfo(profileSerializable) { // Migrate old tags to new tags list From 39e6d8bb25747d341f6cc31e1d93b8c041ed1e19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 14:35:46 +0000 Subject: [PATCH 5/8] Add error handling for unencrypted file deletion and null check for JSON deserialization Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- Source/NETworkManager.Profiles/ProfileManager.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index 63a420386d..0d0e33fa14 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -571,7 +571,14 @@ private static void Load(ProfileFileInfo profileFileInfo) ProfileFiles.Add(newProfileFileInfo); // Delete the old XML file - File.Delete(profileFileInfo.Path); + try + { + File.Delete(profileFileInfo.Path); + } + catch (Exception ex) + { + Log.Warn($"Failed to delete old XML profile file: {profileFileInfo.Path}. Error: {ex.Message}"); + } // Update the reference profileFileInfo = newProfileFileInfo; @@ -836,6 +843,9 @@ private static List DeserializeFromJson(string jsonString) { var groupsSerializable = JsonSerializer.Deserialize>(jsonString, JsonOptions); + if (groupsSerializable == null) + throw new InvalidOperationException("Failed to deserialize JSON profile file."); + return DeserializeGroup(groupsSerializable); } From 054fc6e361b5472552ba06b1f966c95be0a28c71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 14:37:53 +0000 Subject: [PATCH 6/8] Refactor path manipulation with helper method and fix code style Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- .../NETworkManager.Profiles/ProfileManager.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index 0d0e33fa14..c4d04ac74c 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -156,6 +156,20 @@ public static string GetProfilesFolderLocation() AssemblyManager.Current.Name, ProfilesFolderName); } + /// + /// Method to get a JSON profile file path with optional encryption extension. + /// + /// Original file path. + /// Whether the file should have the encrypted extension. + /// JSON profile file path. + private static string GetJsonProfilePath(string originalPath, bool encrypted) + { + var basePath = Path.ChangeExtension(originalPath, null); + return encrypted + ? basePath + ProfileFileExtension + ProfileFileExtensionEncrypted + : basePath + ProfileFileExtension; + } + /// /// Method to get the default profile file name. /// @@ -303,7 +317,7 @@ public static void EnableEncryption(ProfileFileInfo profileFileInfo, SecureStrin // Create a new profile info with the encryption infos var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name, - Path.ChangeExtension(Path.ChangeExtension(profileFileInfo.Path, null), ProfileFileExtension + ProfileFileExtensionEncrypted), true) + GetJsonProfilePath(profileFileInfo.Path, true), true) { Password = password, IsPasswordValid = true @@ -366,7 +380,7 @@ public static void ChangeMasterPassword(ProfileFileInfo profileFileInfo, SecureS // Create a new profile info with the encryption infos var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name, - Path.ChangeExtension(Path.ChangeExtension(profileFileInfo.Path, null), ProfileFileExtension + ProfileFileExtensionEncrypted), true) + GetJsonProfilePath(profileFileInfo.Path, true), true) { Password = newPassword, IsPasswordValid = true @@ -431,7 +445,7 @@ public static void DisableEncryption(ProfileFileInfo profileFileInfo, SecureStri // Create a new profile info var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name, - Path.ChangeExtension(Path.ChangeExtension(profileFileInfo.Path, null), ProfileFileExtension)); + GetJsonProfilePath(profileFileInfo.Path, false)); // Load and decrypt the profiles from the profile file var encryptedBytes = File.ReadAllBytes(profileFileInfo.Path); @@ -860,7 +874,7 @@ private static List DeserializeFromXmlStream(Stream stream) XmlSerializer xmlSerializer = new(typeof(List)); var groupsSerializable = xmlSerializer.Deserialize(stream) as List; - + if (groupsSerializable == null) throw new InvalidOperationException("Failed to deserialize XML profile file."); From 56d5d2722a2379179066627dc32261382908327d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 14:39:57 +0000 Subject: [PATCH 7/8] Final improvements: null-safe Profiles access, optimize IsXmlContent, improve readability Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- Source/NETworkManager.Profiles/ProfileManager.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index c4d04ac74c..19f1c407a0 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -165,9 +165,11 @@ public static string GetProfilesFolderLocation() private static string GetJsonProfilePath(string originalPath, bool encrypted) { var basePath = Path.ChangeExtension(originalPath, null); - return encrypted - ? basePath + ProfileFileExtension + ProfileFileExtensionEncrypted - : basePath + ProfileFileExtension; + + if (encrypted) + return basePath + ProfileFileExtension + ProfileFileExtensionEncrypted; + + return basePath + ProfileFileExtension; } /// @@ -636,7 +638,9 @@ private static bool IsXmlContent(byte[] data) try { - var text = Encoding.UTF8.GetString(data).TrimStart(); + // Only check the first 200 bytes for performance + var bytesToCheck = Math.Min(200, data.Length); + var text = Encoding.UTF8.GetString(data, 0, bytesToCheck).TrimStart(); // Check for XML declaration or root element that matches profile structure return text.StartsWith(" DeserializeGroup(List grou throw new ArgumentNullException(nameof(groupsSerializable)); return (from groupSerializable in groupsSerializable - let profiles = groupSerializable.Profiles.Select(profileSerializable => new ProfileInfo(profileSerializable) + let profiles = (groupSerializable.Profiles ?? []).Select(profileSerializable => new ProfileInfo(profileSerializable) { // Migrate old tags to new tags list // if TagsList is null or empty and Tags is not null or empty, split Tags by ';' and create a new ObservableSetCollection From 84c845e979648268950e823e3232540cf164e241 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 14:41:52 +0000 Subject: [PATCH 8/8] Final polish: add constant for buffer size, use string interpolation, fix compatibility Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- .../NETworkManager.Profiles/ProfileManager.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index 19f1c407a0..9de74f1087 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -57,6 +57,11 @@ public static class ProfileManager Converters = { new JsonStringEnumConverter() } }; + /// + /// Maximum number of bytes to check for XML content detection. + /// + private const int XmlDetectionBufferSize = 200; + /// /// ObservableCollection of all profile files. /// @@ -167,9 +172,9 @@ private static string GetJsonProfilePath(string originalPath, bool encrypted) var basePath = Path.ChangeExtension(originalPath, null); if (encrypted) - return basePath + ProfileFileExtension + ProfileFileExtensionEncrypted; + return $"{basePath}{ProfileFileExtension}{ProfileFileExtensionEncrypted}"; - return basePath + ProfileFileExtension; + return $"{basePath}{ProfileFileExtension}"; } /// @@ -521,7 +526,7 @@ private static void Load(ProfileFileInfo profileFileInfo) var originalPath = profileFileInfo.Path; // Migrate to JSON format by saving the file - var newPath = Path.ChangeExtension(profileFileInfo.Path, ProfileFileExtension + ProfileFileExtensionEncrypted); + var newPath = GetJsonProfilePath(profileFileInfo.Path, true); var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name, newPath, true) { Password = profileFileInfo.Password, @@ -638,8 +643,8 @@ private static bool IsXmlContent(byte[] data) try { - // Only check the first 200 bytes for performance - var bytesToCheck = Math.Min(200, data.Length); + // Only check the first few bytes for performance + var bytesToCheck = Math.Min(XmlDetectionBufferSize, data.Length); var text = Encoding.UTF8.GetString(data, 0, bytesToCheck).TrimStart(); // Check for XML declaration or root element that matches profile structure return text.StartsWith(" DeserializeGroup(List grou throw new ArgumentNullException(nameof(groupsSerializable)); return (from groupSerializable in groupsSerializable - let profiles = (groupSerializable.Profiles ?? []).Select(profileSerializable => new ProfileInfo(profileSerializable) + let profiles = (groupSerializable.Profiles ?? new List()).Select(profileSerializable => new ProfileInfo(profileSerializable) { // Migrate old tags to new tags list // if TagsList is null or empty and Tags is not null or empty, split Tags by ';' and create a new ObservableSetCollection