Extend managed macOS client toward feature parity
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
.NET port / cpp-conformance (push) Canceled after 0s

This commit is contained in:
2026-09-19 00:39:04 +02:00
parent 310c0f09dd
commit d0a72176ba
25 changed files with 1292 additions and 40 deletions
+42 -2
View File
@@ -5,7 +5,8 @@ namespace VoiceCat.Core;
public enum ServerAuthentication { Guest, Account }
public sealed record ServerProfile(Guid Id, string Host, ushort Port, ServerAuthentication Authentication, string? Username, string? Nickname)
public sealed record ServerProfile(Guid Id, string Host, ushort Port, ServerAuthentication Authentication, string? Username, string? Nickname,
[property: JsonIgnore] string? LegacyKeychainTag = null)
{
public static ServerProfile Create(string host, ushort port, ServerAuthentication authentication, string? username = null, string? nickname = null, Guid? id = null)
{
@@ -43,7 +44,11 @@ public sealed class ServerProfileStore(string path)
try
{
if (!File.Exists(path)) return [];
return (JsonSerializer.Deserialize<ServerProfile[]>(File.ReadAllBytes(path), Json) ?? [])
byte[] contents = File.ReadAllBytes(path);
using JsonDocument document = JsonDocument.Parse(contents);
if (document.RootElement.ValueKind == JsonValueKind.Array && document.RootElement.EnumerateArray().Any(LooksLegacy))
return LoadLegacy(document.RootElement);
return (JsonSerializer.Deserialize<ServerProfile[]>(contents, Json) ?? [])
.Where(profile => profile.IsValid).ToArray();
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException) { return []; }
@@ -55,6 +60,7 @@ public sealed class ServerProfileStore(string path)
ServerProfile[] valid = profiles.Where(profile => profile is not null && profile.IsValid).ToArray();
string fullPath = Path.GetFullPath(path);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
PreserveLegacyBackup(fullPath);
string temporary = fullPath + "." + Guid.NewGuid().ToString("N") + ".tmp";
try
{
@@ -63,4 +69,38 @@ public sealed class ServerProfileStore(string path)
}
finally { if (File.Exists(temporary)) File.Delete(temporary); }
}
private static bool LooksLegacy(JsonElement item) => item.ValueKind == JsonValueKind.Object && item.TryGetProperty("authMode", out _);
private static IReadOnlyList<ServerProfile> LoadLegacy(JsonElement root)
{
var profiles = new List<ServerProfile>();
foreach (JsonElement item in root.EnumerateArray())
{
if (!item.TryGetProperty("id", out JsonElement idValue) || !Guid.TryParse(idValue.GetString(), out Guid id) ||
!item.TryGetProperty("host", out JsonElement hostValue) || !item.TryGetProperty("port", out JsonElement portValue) ||
!portValue.TryGetUInt16(out ushort port)) continue;
string? mode = item.TryGetProperty("authMode", out JsonElement modeValue) ? modeValue.GetString() : null;
string? username = item.TryGetProperty("savedUsername", out JsonElement usernameValue) ? usernameValue.GetString() : null;
string? nickname = item.TryGetProperty("nickname", out JsonElement nicknameValue) ? nicknameValue.GetString() : null;
string? keychainTag = item.TryGetProperty("keychainTag", out JsonElement tagValue) ? tagValue.GetString() : null;
ServerAuthentication authentication = mode == "password" ? ServerAuthentication.Account : ServerAuthentication.Guest;
try { profiles.Add(ServerProfile.Create(hostValue.GetString() ?? "", port, authentication, username, nickname, id) with { LegacyKeychainTag = keychainTag }); }
catch (ArgumentException) { }
}
return profiles;
}
private static void PreserveLegacyBackup(string fullPath)
{
if (!File.Exists(fullPath)) return;
try
{
using JsonDocument document = JsonDocument.Parse(File.ReadAllBytes(fullPath));
if (document.RootElement.ValueKind != JsonValueKind.Array || !document.RootElement.EnumerateArray().Any(LooksLegacy)) return;
string backup = fullPath + ".swift-backup.json";
if (!File.Exists(backup)) File.Copy(fullPath, backup);
}
catch (JsonException) { }
}
}