XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
UTF-8
using System.Security.Cryptography;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace XFEEliteDangerous.Bootstrapper;

public sealed record PreparedProfileSetting(string RelativePath,int Bytes,string Sha256);
public sealed record OwnedProfileSettingsReceipt(DateTimeOffset PreparedAt,IReadOnlyList<PreparedProfileSetting> Files,
    bool ReadBackVerified,bool ExistingFilesOverwritten)
{
    public bool Persistent { get; init; }
    public int ExistingFilesPreserved { get; init; }
    public int MigratedSettingsFiles { get; init; }
}

/// <summary>Only two native, non-identity defaults inside one new owned diagnostic profile.
/// This is not a general profile copier, arbitrary settings writer or completion-flag editor.</summary>
internal static class OwnedProfileSettings
{
    internal const string StartupRelativePath=@"LocalAppData\Frontier Developments\Elite Dangerous\Options\Startup\Settings.xml";
    internal const string GraphicsRelativePath=@"LocalAppData\Frontier Developments\Elite Dangerous\Options\Graphics\StartPreset.start";
    private const string StartupXml="<?xml version=\"1.0\" encoding=\"UTF-8\"?><Startup><RenderVersion>2</RenderVersion></Startup>";

    internal static async Task<OwnedProfileSettingsReceipt> InitializePersistentAsync(PersistentGameProfile profile,CancellationToken cancellation=default)
    {
        var prepared=new List<PreparedProfileSetting>(2);var preserved=0;
        foreach(var definition in new[]{(Relative:StartupRelativePath,Bytes:Encoding.UTF8.GetBytes(StartupXml)),
                    (Relative:GraphicsRelativePath,Bytes:Encoding.ASCII.GetBytes("High"))})
        {
            cancellation.ThrowIfCancellationRequested();
            var path=Path.Combine(profile.DirectoryPath,definition.Relative);SafePaths.NoReparseAncestors(path);
            if(!File.Exists(path))
            {
                Directory.CreateDirectory(Path.GetDirectoryName(path)!);SafePaths.NoReparseAncestors(path);
                await using var output=new FileStream(path,FileMode.CreateNew,FileAccess.Write,FileShare.Read,4096,FileOptions.Asynchronous);
                await output.WriteAsync(definition.Bytes,cancellation).ConfigureAwait(false);await output.FlushAsync(cancellation).ConfigureAwait(false);
            }
            else preserved++;
            SafePaths.NoReparseAncestors(path);
            if(new FileInfo(path).Length is <1 or >1048576)throw new InvalidDataException("Existing game preferences are invalid; they were preserved for repair.");
            var actual=await File.ReadAllBytesAsync(path,cancellation).ConfigureAwait(false);
            if(definition.Relative==StartupRelativePath)
            {
                using var reader=XmlReader.Create(new MemoryStream(actual),new XmlReaderSettings{DtdProcessing=DtdProcessing.Prohibit,XmlResolver=null});
                if(XDocument.Load(reader).Root?.Name!="Startup")throw new InvalidDataException("Existing startup settings have an unexpected root; no settings were overwritten.");
            }
            prepared.Add(new(definition.Relative,actual.Length,Convert.ToHexStringLower(SHA256.HashData(actual))));
        }
        return new(DateTimeOffset.UtcNow,prepared.AsReadOnly(),true,false)
        {Persistent=true,ExistingFilesPreserved=preserved,MigratedSettingsFiles=profile.ImportedSettingsFiles};
    }

    internal static async Task<OwnedProfileSettingsReceipt> InitializeAsync(string runtimeDirectory,CancellationToken cancellation=default)
    {
        var runtime=SafePaths.LocalAbsolute(runtimeDirectory);
        var artifacts=SafePaths.ValidateArtifactRoot(null);
        var leaf=Path.GetFileName(runtime);
        if(!string.Equals(Path.GetDirectoryName(runtime),artifacts,StringComparison.OrdinalIgnoreCase)
            ||!leaf.StartsWith("native-session-",StringComparison.Ordinal)
            ||!Guid.TryParseExact(leaf["native-session-".Length..],"N",out _))
            throw new ArgumentException("Fixed defaults may be prepared only in a new owned diagnostic runtime.");
        SafePaths.NoReparseAncestors(runtime);
        var profile=Path.Combine(runtime,"profile");
        SafePaths.NoReparseAncestors(profile);
        if(!Directory.Exists(runtime)||!Directory.Exists(profile)) throw new DirectoryNotFoundException("The owned profile must exist before preparing settings.");
        var definitions=new[]{(Relative:StartupRelativePath,Bytes:Encoding.UTF8.GetBytes(StartupXml)),
            (Relative:GraphicsRelativePath,Bytes:Encoding.ASCII.GetBytes("High"))};
        // Preflight both fixed paths before creating either file. FileMode.CreateNew also closes an
        // overwrite race; any failure aborts session creation rather than reusing a partial profile.
        foreach(var definition in definitions)
        {
            var path=Path.Combine(profile,definition.Relative);
            SafePaths.NoReparseAncestors(path);
            if(!SafePaths.Within(profile,path)||File.Exists(path)||Directory.Exists(path))
                throw new IOException("Existing profile settings are never overwritten.");
        }
        var prepared=new List<PreparedProfileSetting>(2);
        foreach(var definition in definitions)
        {
            cancellation.ThrowIfCancellationRequested();
            var path=Path.Combine(profile,definition.Relative);
            var parent=Path.GetDirectoryName(path)!;
            SafePaths.NoReparseAncestors(parent); Directory.CreateDirectory(parent); SafePaths.NoReparseAncestors(path);
            await using(var output=new FileStream(path,FileMode.CreateNew,FileAccess.Write,FileShare.Read,4096,FileOptions.Asynchronous))
            {
                await output.WriteAsync(definition.Bytes,cancellation).ConfigureAwait(false);
                await output.FlushAsync(cancellation).ConfigureAwait(false);
            }
            SafePaths.NoReparseAncestors(path);
            var actual=await File.ReadAllBytesAsync(path,cancellation).ConfigureAwait(false);
            if(!actual.AsSpan().SequenceEqual(definition.Bytes)) throw new InvalidDataException("A fixed profile default failed read-back validation.");
            if(definition.Relative==StartupRelativePath)
            {
                using var reader=XmlReader.Create(new MemoryStream(actual),new XmlReaderSettings{DtdProcessing=DtdProcessing.Prohibit,XmlResolver=null});
                var document=XDocument.Load(reader);
                if(document.Root is not {Name:var name}||name!="Startup"||document.Root.HasAttributes
                    ||document.Root.Elements().Count()!=1||document.Root.Element("RenderVersion") is not {Value:"2",HasAttributes:false} render||render.HasElements)
                    throw new InvalidDataException("The fixed startup default has an unexpected schema.");
            }
            prepared.Add(new(definition.Relative,actual.Length,Convert.ToHexString(SHA256.HashData(actual)).ToLowerInvariant()));
        }
        return new(DateTimeOffset.UtcNow,prepared.AsReadOnly(),true,false);
    }
}