using System.Security.Cryptography; namespace XFEEliteDangerous.NativeFormat.Replication; /// /// 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. /// 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 states=[]; private readonly Queue order=[]; private bool disposed; public int StateCount=>states.Count; public ushort LatestSequence{get;private set;} public EdServerAdminSecondaryArchive(ulong reference,ushort version,ReadOnlySpan 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();var suppliedFixedBits=0; while(pFixedBits)throw new InvalidDataException("controller-secondary-fixed-run"); for(var bit=index;bit=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;iprior.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 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>(p%8)&1)<Array.Empty()).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);} } }