192 lines
6.2 KiB
C#
192 lines
6.2 KiB
C#
|
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<byte> ArrayPool { get; } = ArrayPool<byte>.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();
|
|||
|
}
|
|||
|
}
|
|||
|
}
|