namespace DodoSSH.Client.Session;
///
/// Where this machine keeps its profile.
///
///
///
/// A record with an explicit directory rather than a static lookup, so a test — or a portable install —
/// can point it somewhere else without an environment variable.
///
///
/// The choice of directory matters more than it looks. The cache is a SQLite file written by one
/// process, and the whole design assumes each machine has its own: the outbox holds changes this machine
/// has made and not yet pushed, and two machines sharing one file through a cloud sync client corrupts
/// it. So this deliberately picks a local, non-roaming location on every platform. On Windows
/// that means %LOCALAPPDATA% and never %APPDATA%, which roams in a domain environment and
/// would do exactly the wrong thing.
///
///
/// The profile directory. Created on demand.
public sealed record ClientPaths(string DataDirectory)
{
private const string WindowsFolderName = "DodoSSH";
private const string UnixFolderName = "dodossh";
/// The conventional location for this platform.
public static ClientPaths Default { get; } = new(ResolveDataDirectory());
/// The encrypted local cache.
public string CacheFile => Path.Combine(DataDirectory, "cache.db");
///
/// This machine's device key, encrypted to a key it cannot export.
///
///
/// Local and non-roaming for a stronger reason than the cache is: the file is decryptable only by a
/// key held in this machine's TPM, so a copy of it on another machine is bytes nothing can open. It
/// following a user to a second computer would be useless rather than dangerous — but a roaming
/// profile that overwrote one machine's blob with another's would break both.
///
public string DeviceKeyFile => Path.Combine(DataDirectory, "device.key");
/// Creates the profile directory if it is not there yet.
///
/// Separate from resolving the path, because resolving must never have a side effect: it is read
/// during startup diagnostics and by tests that have no business creating directories.
///
public void EnsureCreated() => Directory.CreateDirectory(DataDirectory);
///
/// The platform branches are explicit rather than delegating to
/// everywhere. That enumeration does
/// the right thing on Windows, but on macOS the runtime maps it to ~/.local/share rather than
/// to ~/Library/Application Support, and relying on framework behaviour that differs per
/// platform for a path users will look at is how a file ends up somewhere nobody expects.
///
/// XDG_DATA_HOME is honoured explicitly for the same reason: it is the spec, and reading it
/// here is one line versus depending on whether the runtime happens to.
///
///
private static string ResolveDataDirectory()
{
if (OperatingSystem.IsWindows())
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
WindowsFolderName);
}
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (OperatingSystem.IsMacOS())
{
return Path.Combine(home, "Library", "Application Support", WindowsFolderName);
}
var xdgDataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
var root = string.IsNullOrWhiteSpace(xdgDataHome)
? Path.Combine(home, ".local", "share")
: xdgDataHome;
return Path.Combine(root, UnixFolderName);
}
}