sochkeal/Satori/PersistentData.cs
2023-06-28 22:12:23 +02:00

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();
}
}
}