using System; using System.Buffers; using System.IO; using System.Text; namespace SockChatKeepAlive { public class PersistentData : IDisposable { private const int MAGIC = 0x0BB0AFDE; private const byte VERSION = 1; private Stream Stream { get; } private bool OwnsStream { get; } private const int HEADER_SIZE = 0x10; private const int SAT_TOTAL_UPTIME = 0x10; private const int SAT_TOTAL_UPTIME_LENGTH = 0x08; private const int AFK_STR = SAT_TOTAL_UPTIME + SAT_TOTAL_UPTIME_LENGTH; private const int AFK_STR_LENGTH = 0x14; private const int GRACEFUL_DISCON = AFK_STR + AFK_STR_LENGTH; private readonly long OffsetStart; public long SatoriTotalUptime { get => ReadI64(SAT_TOTAL_UPTIME); set => WriteI64(SAT_TOTAL_UPTIME, value); } public string AFKString { get => ReadStr(AFK_STR, AFK_STR_LENGTH); set => WriteStr(AFK_STR, AFK_STR_LENGTH, value); } public bool WasGracefulDisconnect { get => ReadU1(GRACEFUL_DISCON); set => WriteU1(GRACEFUL_DISCON, value); } private ArrayPool ArrayPool { get; } = ArrayPool.Shared; public PersistentData(string fileName) : this( new FileStream( fileName ?? throw new ArgumentNullException(nameof(fileName)), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read ), true ) { } public PersistentData(Stream stream, bool ownsStream = false) { Stream = stream ?? throw new ArgumentNullException(nameof(stream)); OwnsStream = ownsStream; if(!stream.CanRead) throw new ArgumentException("Stream must be readable.", nameof(stream)); if(!stream.CanSeek) throw new ArgumentException("Stream must be seekable.", nameof(stream)); if(!stream.CanWrite) throw new ArgumentException("Stream must be writable.", nameof(stream)); OffsetStart = stream.Position; byte[] buffer = ArrayPool.Rent(HEADER_SIZE); int read; try { read = stream.Read(buffer, 0, HEADER_SIZE); if(read > 0) { if(BitConverter.ToInt32(buffer, 0) != MAGIC) throw new ArgumentException("Stream does not contain a valid persistent data file structure: invalid magic number.", nameof(stream)); if(buffer[5] is < 1 or > VERSION) throw new ArgumentException("Stream does not contain a valid persistent data file structure: unsupported version.", nameof(stream)); } } finally { ArrayPool.Return(buffer); } if(read < 1) { Stream.Seek(OffsetStart, SeekOrigin.Begin); Stream.Write(BitConverter.GetBytes(MAGIC)); // intentionally incompatible with satori Stream.WriteByte(0); Stream.WriteByte(VERSION); // lol Stream.WriteByte(0); Stream.WriteByte(0); Stream.WriteByte(0); Stream.WriteByte(0); Stream.WriteByte(0); Stream.WriteByte(0); Stream.WriteByte(0); Stream.WriteByte(0); Stream.WriteByte(0); Stream.WriteByte(0); Stream.Flush(); } else if(read < HEADER_SIZE) throw new ArgumentException("Stream does not contain a valid persistent data file structure: not enough data.", nameof(stream)); } private void Seek(long address) { Stream.Seek(OffsetStart + address, SeekOrigin.Begin); } private string ReadStr(long address, int length) { byte[] buffer = ArrayPool.Rent(length); try { Seek(address); Stream.Read(buffer, 0, length); return Encoding.UTF8.GetString(buffer).Trim('\0'); // retarded } finally { ArrayPool.Return(buffer); } } private bool ReadU1(long address) { return ReadU8(address) > 0; } private byte ReadU8(long address) { Seek(address); int value = Stream.ReadByte(); return (byte)(value < 1 ? 0 : value); } private long ReadI64(long address) { byte[] buffer = ArrayPool.Rent(8); try { Seek(address); return Stream.Read(buffer) < 8 ? 0 : BitConverter.ToInt64(buffer); } finally { ArrayPool.Return(buffer); } } private void WriteStr(long address, int length, string value) { value ??= string.Empty; if(Encoding.UTF8.GetByteCount(value) > length) throw new ArgumentException("Value exceeds maximum length."); byte[] buffer = Encoding.UTF8.GetBytes(value); int difference = length - buffer.Length; Seek(address); Stream.Write(Encoding.UTF8.GetBytes(value)); while(difference-- > 0) Stream.WriteByte(0); } private void WriteU1(long address, bool value) { WriteU8(address, (byte)(value ? 0x35 : 0)); } private void WriteU8(long address, byte number) { Seek(address); Stream.WriteByte(number); } private void WriteI64(long address, long number) { Seek(address); Stream.Write(BitConverter.GetBytes(number)); } public void Flush() { Stream.Flush(); } private bool IsDisposed; ~PersistentData() { DoDispose(); } public void Dispose() { DoDispose(); GC.SuppressFinalize(this); } private void DoDispose() { if(IsDisposed) return; IsDisposed = true; Flush(); if(OwnsStream) Stream.Dispose(); } } }