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

namespace XFEEliteDangerous.NativeFormat.Replication;

/// <summary>
/// The pinned ServerAdmin secondary archive:12 fixed properties totalling448bits,
/// no optional mask,6 byte-string properties.6339D0 reads ordered fixed-bit runs
/// and length-delimited variable overrides;63B600 derives its9-bit index width.
/// This is per-peer control metadata, never a world or vessel state.
/// </summary>
public sealed class EdServerAdminSecondaryArchive:IDisposable
{
    public const int FixedBits=448,StringCount=6,IndexBits=9,MaximumStates=32;
    private readonly ulong reference;
    private readonly ushort version;
    private readonly byte[] fixedDefaults;
    private readonly bool defaultsVerified;
    private readonly byte defaultKnownStrings;
    private readonly Dictionary<ushort,State> states=[];
    private readonly Queue<ushort> order=[];
    private bool disposed;
    public int StateCount=>states.Count;
    public ushort LatestSequence{get;private set;}
    public EdServerAdminSecondaryArchive(ulong reference,ushort version,ReadOnlySpan<byte> verifiedFixedDefaults=default,byte verifiedEmptyStringMask=63)
    {
        if(reference is 0 or >0xFFFFFFFFFFFF||verifiedFixedDefaults.Length is not(0 or FixedBits/8)||verifiedEmptyStringMask>63)throw new ArgumentOutOfRangeException(nameof(reference));
        this.reference=reference;this.version=version;defaultsVerified=!verifiedFixedDefaults.IsEmpty;
        fixedDefaults=defaultsVerified?verifiedFixedDefaults.ToArray():new byte[FixedBits/8];
        defaultKnownStrings=verifiedEmptyStringMask;
    }
    public EdServerAdminSecondaryArchive Fork()
    {
        ObjectDisposedException.ThrowIf(disposed,this);var clone=new EdServerAdminSecondaryArchive(reference,version,defaultsVerified?fixedDefaults:[],defaultKnownStrings){LatestSequence=LatestSequence};
        foreach(var sequence in order){clone.states.Add(sequence,states[sequence].Copy());clone.order.Enqueue(sequence);}return clone;
    }
    public void Apply(EdServerObjectUpdate update)
    {
        ObjectDisposedException.ThrowIf(disposed,this);ArgumentNullException.ThrowIfNull(update);
        if(update.ObjectReference!=reference||!update.SecondaryArchive||update.ConstructionVersion!=version)
            throw new InvalidDataException("controller-secondary-schema-mismatch");
        var bytes=update.CopyPayload();byte[]? digest=null;State? candidate=null;
        try
        {
            digest=SHA256.HashData(bytes);
            states.TryGetValue(update.Sequence,out var prior);
            if(prior is not null&&CryptographicOperations.FixedTimeEquals(prior.Digest,digest))return;
            var p=0;var baseSequence=(ushort)Read(bytes,ref p,10,update.PayloadBits);
            if(baseSequence==0)candidate=new(){Fixed=fixedDefaults.ToArray(),KnownStrings=defaultKnownStrings};
            else if(states.TryGetValue(baseSequence,out var baseline))candidate=baseline.Copy();
            else throw new NotSupportedException("controller-secondary-base-unavailable");
            var previousEnd=-1;var seenVariables=new HashSet<int>();var suppliedFixedBits=0;
            while(p<update.PayloadBits)
            {
                var index=(int)Read(bytes,ref p,IndexBits,update.PayloadBits);
                if(index<FixedBits)
                {
                    var end=(int)Read(bytes,ref p,IndexBits,update.PayloadBits);
                    if(index<=previousEnd||end<=index||end>FixedBits)throw new InvalidDataException("controller-secondary-fixed-run");
                    for(var bit=index;bit<end;bit++)
                    {
                        var value=Read(bytes,ref p,1,update.PayloadBits);var mask=1<<(bit%8);
                        candidate.Fixed[bit/8]=(byte)((candidate.Fixed[bit/8]&~mask)|(value==0?0:mask));
                    }
                    previousEnd=end;
                    suppliedFixedBits+=end-index;
                }
                else
                {
                    var variable=index-FixedBits;
                    if(variable>=StringCount||!seenVariables.Add(variable))throw new InvalidDataException("controller-secondary-variable-index");
                    var payloadLength=(long)Read(bytes,ref p,32,update.PayloadBits);
                    if(payloadLength<32||payloadLength>update.PayloadBits-p)throw new InvalidDataException("controller-secondary-variable-length");
                    var end=p+(int)payloadLength;
                    var length=(long)Read(bytes,ref p,32,end);
                    if(length>1024||length*8!=end-p)throw new InvalidDataException("controller-secondary-string-length");
                    var value=new byte[(int)length];
                    for(var i=0;i<value.Length;i++)value[i]=(byte)Read(bytes,ref p,8,end);
                    CryptographicOperations.ZeroMemory(candidate.Strings[variable]);candidate.Strings[variable]=value;
                    candidate.KnownStrings|=(byte)(1<<variable);
                }
            }
            if(p!=update.PayloadBits)throw new InvalidDataException("controller-secondary-trailing-bits");
            if(baseSequence==0&&!defaultsVerified&&suppliedFixedBits!=FixedBits)
                throw new NotSupportedException("controller-secondary-defaults-unverified");
            if(candidate.KnownStrings!=63)throw new NotSupportedException("controller-secondary-string-defaults-unverified");
            if(prior is not null)
            {
                // UREP/CertREP can encode the same retained frame using different
                // baseline/override runs. Compare applied values, not wire bytes.
                if(!prior.Fixed.AsSpan().SequenceEqual(candidate.Fixed)||!Enumerable.Range(0,StringCount).All(index=>prior.Strings[index].AsSpan().SequenceEqual(candidate.Strings[index])))
                    throw new InvalidDataException("controller-secondary-changed-duplicate");
                return;
            }
            CryptographicOperations.ZeroMemory(candidate.Digest);candidate.Digest=digest;digest=null;
            states.Add(update.Sequence,candidate);order.Enqueue(update.Sequence);candidate=null;LatestSequence=update.Sequence;
            while(states.Count>MaximumStates){var sequence=order.Dequeue();states.Remove(sequence,out var expired);expired!.Dispose();}
        }
        finally{CryptographicOperations.ZeroMemory(bytes);if(digest is not null)CryptographicOperations.ZeroMemory(digest);candidate?.Dispose();}
    }
    public byte[] CopyFixedState(ushort sequence)
    {ObjectDisposedException.ThrowIf(disposed,this);return states[sequence].Fixed.ToArray();}
    public byte[] CopyString(ushort sequence,int index)
    {ObjectDisposedException.ThrowIf(disposed,this);if(index is <0 or >=StringCount)throw new ArgumentOutOfRangeException(nameof(index));return states[sequence].Strings[index].ToArray();}
    public void Dispose()
    {if(disposed)return;disposed=true;foreach(var state in states.Values)state.Dispose();states.Clear();order.Clear();CryptographicOperations.ZeroMemory(fixedDefaults);LatestSequence=0;}
    private static ulong Read(ReadOnlySpan<byte> bytes,ref int p,int count,int end)
    {
        if(count is <0 or >64||p<0||(long)p+count>end||(long)end>bytes.Length*8)throw new InvalidDataException("controller-secondary-truncated");
        ulong value=0;for(var i=0;i<count;i++,p++)value|=(ulong)(bytes[p/8]>>(p%8)&1)<<i;return value;
    }
    private sealed class State:IDisposable
    {
        internal byte[] Fixed=new byte[FixedBits/8],Digest=[];
        internal byte KnownStrings;
        internal byte[][] Strings=Enumerable.Range(0,StringCount).Select(_=>Array.Empty<byte>()).ToArray();
        internal State Copy()=>new(){Fixed=Fixed.ToArray(),Digest=Digest.ToArray(),KnownStrings=KnownStrings,Strings=Strings.Select(value=>value.ToArray()).ToArray()};
        public void Dispose(){CryptographicOperations.ZeroMemory(Fixed);CryptographicOperations.ZeroMemory(Digest);foreach(var value in Strings)CryptographicOperations.ZeroMemory(value);}
    }
}