.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
104 lines
5.6 KiB
C#
104 lines
5.6 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace VoiceCat.Core;
|
|
|
|
public enum ServerAuthentication { Guest, Account }
|
|
|
|
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)
|
|
{
|
|
host = host.Trim(); username = Normalize(username); nickname = Normalize(nickname);
|
|
if (host.Length == 0) throw new ArgumentException("Server host is required.", nameof(host));
|
|
if (port == 0) throw new ArgumentOutOfRangeException(nameof(port));
|
|
if (authentication == ServerAuthentication.Account && username is null) throw new ArgumentException("Username is required for account authentication.", nameof(username));
|
|
return new(id.GetValueOrDefault(Guid.NewGuid()), host, port, authentication,
|
|
authentication == ServerAuthentication.Account ? username : null,
|
|
authentication == ServerAuthentication.Guest ? nickname : null);
|
|
}
|
|
|
|
[JsonIgnore]
|
|
public string DisplayName => Authentication == ServerAuthentication.Account
|
|
? $"{Username}@{Host}:{Port}"
|
|
: $"{Host}:{Port} (Guest{(Nickname is null ? "" : $": {Nickname}")})";
|
|
|
|
internal bool IsValid => Id != Guid.Empty && !string.IsNullOrWhiteSpace(Host) && Port != 0 &&
|
|
(Authentication == ServerAuthentication.Guest || !string.IsNullOrWhiteSpace(Username));
|
|
|
|
private static string? Normalize(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
}
|
|
|
|
public sealed class ServerProfileStore(string path)
|
|
{
|
|
public IReadOnlyList<ServerProfile> Load()
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(path)) return [];
|
|
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(contents, ServerProfileJsonContext.Default.ServerProfileArray) ?? [])
|
|
.Where(profile => profile.IsValid).ToArray();
|
|
}
|
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException) { return []; }
|
|
}
|
|
|
|
public void Save(IEnumerable<ServerProfile> profiles)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(profiles);
|
|
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
|
|
{
|
|
File.WriteAllBytes(temporary, JsonSerializer.SerializeToUtf8Bytes(valid, ServerProfileJsonContext.Default.ServerProfileArray));
|
|
File.Move(temporary, fullPath, true);
|
|
}
|
|
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) { }
|
|
}
|
|
}
|
|
|
|
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, WriteIndented = true, UseStringEnumConverter = true)]
|
|
[JsonSerializable(typeof(ServerProfile[]))]
|
|
internal sealed partial class ServerProfileJsonContext : JsonSerializerContext;
|