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