using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; namespace RBG_Server { /// /// Stores data in either memory or disk, depending on the required behaviour of the system, namely, if the /// byte array exceeds the size of the Memory Limit (objects can be required to stay in RAM) /// public struct CachedByteArray : IDisposable { // Static event, so that every instance of CachedByteArray is affected by a change in memory limit private static event EventHandler MemoryLimitModifiedEvent; // Properties to keep track of the actual allocated cache amounts private static long memoryAllocationTotal = 0; private static long memoryAllocationRAM = 0; // static fields private static int memoryLimit = 1024 * 1024 * 1; // Start with a memory limit of 1 MB private static readonly string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Remote Board Game\\Cache Objects"); // static properties public static long MemoryAllocationRAM { get { return memoryAllocationRAM; } } public static long MemoryAllocationTotal { get { return memoryAllocationTotal; } } public static long MemoryAllocationDisk { get { return memoryAllocationTotal - memoryAllocationRAM; } } private SemaphoreSlim accessSem; public bool IsInRam { get; private set; } // struct data private bool useOnlyRAM; private Stream dataStream; private string dataHashCode; public bool UseOnlyRAM { get => useOnlyRAM; set => useOnlyRAM = value; } // Properties from the array itself public long Length => dataStream.Length; /// /// Set the memory limit for keeping data in RAM, as opposed to storing on disk. /// Will raise events if modified, so all instances will be refactored to disk if appropriate /// public static int MemoryLimit { get { return memoryLimit; } set { memoryLimit = value; MemoryLimitModifiedEvent?.Invoke(null, EventArgs.Empty); } } public CachedByteArray(byte[] b) : this(b, false) { } public CachedByteArray(byte[] b, bool useOnlyRAM) { accessSem = new SemaphoreSlim(0,1); this.useOnlyRAM = useOnlyRAM; dataHashCode = GenerateDataHash(b); if (!useOnlyRAM && b.Length > MemoryLimit) { // Save to a file; load the data string path = Path.Combine(folderPath, dataHashCode); dataStream = CopyIntoFile(b, path); IsInRam = false; } else { dataStream = new MemoryStream(b); memoryAllocationRAM += dataStream.Length; IsInRam = true; } dataStream.Position = 0; memoryAllocationTotal += dataStream.Length; // Signal the semaphore is ready accessSem.Release(); // Listen to changes in the memory limit MemoryLimitModifiedEvent += CachedByteArray_memoryLimitModifiedEvent; } /// /// Generates a SHA256 hash of the data in the array, and returns it in a safe string format /// /// /// private static string GenerateDataHash(byte[] byteData) { using SHA256 dataHash = SHA256.Create(); byte[] hashCode = dataHash.ComputeHash(byteData); string generatedHashCode = ""; foreach (byte hashByte in hashCode) { generatedHashCode += $"{hashByte:X2}"; } return generatedHashCode; } /// /// Copies the given into the file at , opening the stream, and returning it /// /// /// /// private static Stream CopyIntoFile(byte[] byteData, string filePath) { FileStream s = File.OpenWrite(filePath); foreach (byte b in byteData) { s.WriteByte(b); } return s; } private void CachedByteArray_memoryLimitModifiedEvent(object sender, EventArgs e) { if (!useOnlyRAM) { if (dataStream.Length > MemoryLimit && IsInRam) { // Memory stream exceeds the limit; create an FS and copy accessSem.Wait(); // Get the underlying byte[] byte[] dataBytes = ((MemoryStream)dataStream).ToArray(); // Generate the hashcode from the data dataHashCode = GenerateDataHash(dataBytes); // Create the file, and copy the data dataStream = CopyIntoFile(dataBytes, Path.Combine(folderPath, dataHashCode)); // Disregard the allocation from RAM memoryAllocationRAM -= dataStream.Length; accessSem.Release(); } else if (dataStream.Length <= MemoryLimit && !IsInRam) { // Data stream is no larger than the limit, but the file is saved on disk; load to RAM instead // Obtain a lock on the stream accessSem.Wait(); // Copy data to memory dataStream.Position = 0; MemoryStream ms = new(); dataStream.CopyTo(ms); // Increase the amount noted in RAM memoryAllocationRAM += ms.Length; // Close the FileStream dataStream.Close(); // Delete the file DeleteFile(Path.Combine(folderPath, dataHashCode)); // Set the datastream to the memory location dataStream = ms; accessSem.Release(); // Check the loaded data matches the hashcode we already have string loadedHashCode = GenerateDataHash(ms.ToArray()); if (loadedHashCode != dataHashCode) { throw new UnauthorizedAccessException("Data was modified"); } } // Otherwise, the data is already in the correct spot, so we don't need to do anything else } } private static void DeleteFile(string filePath) { try { File.Delete(filePath); } catch (Exception ex) { Debug.WriteLine(ex); } } public static implicit operator byte[](CachedByteArray c) { byte[] dataBytes = new byte[c.dataStream.Length]; c.accessSem.Wait(); if (c.IsInRam) { // We can use the underlying array from the MemoryStream dataBytes = (c.dataStream as MemoryStream).ToArray(); } else { // Ensure we read from the beginning of the stream c.dataStream.Position = 0; c.dataStream.Read(dataBytes, 0, dataBytes.Length); } c.accessSem.Release(); return dataBytes; } public static explicit operator CachedByteArray(byte[] b) => new CachedByteArray(b); /// /// Array index operator, allows the CachedByteArray to act more completely as a byte[] /// /// /// public byte this[int index] { get { int b; // Aquire the semaphore, so that the read is effectively atomic (ensures the read is performed on the expected data) accessSem.Wait(); dataStream.Position = index; b = dataStream.ReadByte(); accessSem.Release(); if (b == -1) { throw new IndexOutOfRangeException(index.ToString()); } return (byte)b; } } public void Dispose() { accessSem.Wait(); // Remove the event listener MemoryLimitModifiedEvent -= CachedByteArray_memoryLimitModifiedEvent; // Remove the allocation from the counter memoryAllocationTotal -= dataStream.Length; memoryAllocationRAM -= dataStream.Length; // If the FileStream is opened, close and dispose it dataStream?.Dispose(); // If the file is on disk, remove it DeleteFile(Path.Combine(folderPath, dataHashCode)); // Release all resources obtained by the semaphore accessSem.Dispose(); } } }