Skip to content

Commit

Permalink
Updated FileCache to remove BinaryFormatter.
Browse files Browse the repository at this point in the history
FileCacheManager:
  - Added magic header number `CACHE_VERSION`.
    + `HeaderVersionValid()` and `HeaderVersionWrite()` use it in
      writing/reading to figure out if the files were created using the
      old (BinaryFormatter) or new (BinaryWrite) way.

  - Removed `ReadSysFile()`.
    + Replaced it with:
      - `ReadSysValue<T>(string, out T)`
        + Just throws an Exception - below two functions are the
          actually used ones.
      - `ReadSysValue(string, out long)`
      - `ReadSysValue(string, out DateTime)`

  - Removed `WriteSysFile()`.
    + Replaced it with:
      - `WriteSysValue<T>(string, T)`
        + Just throws an Exception - below two functions are the
          actually used ones.
      - `WriteSysValue(string, long)`
      - `WriteSysValue(string, DateTime)`

FileCache:
  - Implemented `FileCache.PayloadMode.RawBytes` for read & write.
    + Removed code in `PayloadReadMode` property to prevent its use.
    + Implemented the `ReadFile()` function helper:
      `LoadRawPayloadData()`.
    + Updated `WriteFile()` (PayloadMode.RawBytes path) to use
      BinaryWriter instead of BinaryFormatter.
  • Loading branch information
cole-brown committed Jul 28, 2021
1 parent 80fd36d commit d738c50
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 62 deletions.
2 changes: 0 additions & 2 deletions src/FileCache/BasicFileCacheManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,5 @@ public override string GetPolicyPath(string key, string regionName = null)
}
return filePath;
}


}
}
32 changes: 10 additions & 22 deletions src/FileCache/FileCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public class FileCache : ObjectCache
// this is a file used to prevent multiple processes from trying to "clean" at the same time
private const string SemaphoreFile = "cache.sem";
private long _currentCacheSize = 0;
private PayloadMode _readMode = PayloadMode.Serializable;
public string CacheDir { get; protected set; }

/// <summary>
Expand Down Expand Up @@ -88,16 +87,7 @@ public enum PayloadMode
/// <summary>
/// Specified whether the payload is deserialized or just the file name.
/// </summary>
public PayloadMode PayloadReadMode {
get => _readMode;
set {
if (value == PayloadMode.RawBytes)
{
throw new ArgumentException("The read mode cannot be set to RawBytes. Use the file name please.");
}
_readMode = value;
}
}
public PayloadMode PayloadReadMode { get; set; } = PayloadMode.Serializable;

/// <summary>
/// Specified how the payload is to be handled on add operations.
Expand Down Expand Up @@ -144,12 +134,11 @@ public long CurrentCacheSize
if (_currentCacheSize == 0)
{
// Read the system file for cache size
object cacheSizeObj = CacheManager.ReadSysFile(CacheSizeFile);

// Did we successfully get data from the file?
if (cacheSizeObj != null)
long cacheSize;
if (CacheManager.ReadSysValue(CacheSizeFile, out cacheSize))
{
_currentCacheSize = (long) cacheSizeObj;
// Did we successfully get data from the file? Write it to our member var.
_currentCacheSize = cacheSize;
}
}

Expand All @@ -160,7 +149,7 @@ private set
// no need to do a pointless re-store of the same value
if (_currentCacheSize != value || value == 0)
{
CacheManager.WriteSysFile(CacheSizeFile, value);
CacheManager.WriteSysValue(CacheSizeFile, value);
_currentCacheSize = value;
}
}
Expand Down Expand Up @@ -395,11 +384,10 @@ private bool ShouldClean()
try
{
// if the file can't be found, or is corrupt this will throw an exception
DateTime? lastClean = CacheManager.ReadSysFile(LastCleanedDateFile) as DateTime?;

//AC: rewrote to be safer in null cases
if (lastClean == null)
DateTime lastClean;
if (!CacheManager.ReadSysValue(LastCleanedDateFile, out lastClean))
{
//AC: rewrote to be safer in cases where no value obtained.
return true;
}

Expand Down Expand Up @@ -514,7 +502,7 @@ public long CleanCache(string regionName = null)
}

// mark that we've cleaned the cache
CacheManager.WriteSysFile(LastCleanedDateFile, DateTime.Now);
CacheManager.WriteSysValue(LastCleanedDateFile, DateTime.Now);

// unlock
cLock.Close();
Expand Down
224 changes: 186 additions & 38 deletions src/FileCache/FileCacheManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ namespace System.Runtime.Caching
{
public abstract class FileCacheManager
{
// Magic version for new sysfiles: 3.3.0 packed into a long.
protected const ulong CACHE_VERSION = ( 3 << 16
+ 3 << 8
+ 0 << 0);

public string CacheDir { get; set; }
public string CacheSubFolder { get; set; }
public string PolicySubFolder { get; set; }
Expand All @@ -24,6 +29,40 @@ public abstract class FileCacheManager
/// </summary>
public TimeSpan AccessTimeout { get; set; }


/// <summary>
/// Differentiate outdated cache formats from newer.
///
/// Older caches use "BinaryFormatter", which is a security risk:
/// https://docs.microsoft.com/nl-nl/dotnet/standard/serialization/binaryformatter-security-guide#preferred-alternatives
///
/// The newer caches have a 'magic' header we'll look for.
/// </summary>
/// <param name="reader">BinaryReader opened to stream containing the file contents.</param>
/// <returns>boolean indicating validity</returns>
protected bool HeaderVersionValid(BinaryReader reader)
{
// Don't much care about exceptions here - let them bubble up.
ulong version = reader.ReadUInt64();
// Valid if magic header version matches.
return (version == CACHE_VERSION);
}

/// <summary>
/// Differentiate outdated cache formats from newer.
///
/// Older caches use "BinaryFormatter", which is a security risk:
/// https://docs.microsoft.com/nl-nl/dotnet/standard/serialization/binaryformatter-security-guide#preferred-alternatives
///
/// The newer caches have a 'magic' header we'll look for.
/// </summary>
/// <param name="reader">BinaryWriter opened to stream that will contain the file contents.</param>
protected void HeaderVersionWrite(BinaryWriter writer)
{
// Don't much care about exceptions here - let them bubble up.
writer.Write(CACHE_VERSION);
}

protected virtual object DeserializePayloadData(string fileName, SerializationBinder objectBinder = null)
{
object data = null;
Expand Down Expand Up @@ -98,9 +137,30 @@ public virtual FileCachePayload ReadFile(FileCache.PayloadMode mode, string key,
return payload;
}

private object LoadRawPayloadData(string cachePath)
private byte[] LoadRawPayloadData(string fileName)
{
throw new NotSupportedException("Reading raw payload is not currently supported.");
byte[] data = null;
if (File.Exists(fileName))
{
using (FileStream stream = GetStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (BinaryReader reader = new BinaryReader(stream))
{
// Check if it's valid version first.
if (!HeaderVersionValid(reader))
{
// Failure - return invalid data.
return null;

// `using` statements will clean up for us.
}

// Valid - read entire file.
data = reader.ReadBytes(int.MaxValue);
}
}
}
return data;
}

protected virtual object Deserialize(string fileName, SerializationBinder objectBinder = null)
Expand Down Expand Up @@ -165,25 +225,27 @@ public virtual long WriteFile(FileCache.PayloadMode mode, string key, FileCacheP
case FileCache.PayloadMode.Serializable:
using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None))
{

BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, data.Payload);
}
break;
case FileCache.PayloadMode.RawBytes:
using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None))
{

if (data.Payload is byte[])
{
byte[] dataPayload = (byte[])data.Payload;
stream.Write(dataPayload, 0, dataPayload.Length);
}
else if (data.Payload is Stream)
using (BinaryWriter writer = new BinaryWriter(stream))
{
Stream dataPayload = (Stream)data.Payload;
dataPayload.CopyTo(stream);
// no close or the like, we are not the owner
if (data.Payload is byte[])
{
byte[] dataPayload = (byte[])data.Payload;
writer.Write(dataPayload);
}
else if (data.Payload is Stream)
{
Stream dataPayload = (Stream)data.Payload;
byte[] bytePayload = new byte[dataPayload.Length - dataPayload.Position];
dataPayload.Read(bytePayload, (int)dataPayload.Position, bytePayload.Length);
// no close or the like for data.Payload - we are not the owner
}
}
}
break;
Expand Down Expand Up @@ -261,17 +323,56 @@ public IEnumerable<string> GetRegions()
public abstract string GetPolicyPath(string key, string regionName = null);

/// <summary>
/// Reads data in from a system file. System files are not part of the
/// cache itself, but serve as a way for the cache to store data it
/// needs to operate.
/// Generic version of ReadSysValue just throws an ArgumentException to error on unknown new types.
/// </summary>
/// <param name="filename">The name of the sysfile (without directory)</param>
/// <returns>The data from the file</returns>
public object ReadSysFile(string filename)
/// <param name="value">The value read</param>
/// <returns>success/failure boolean</returns>
public bool ReadSysValue<T>(string filename, out T value) where T : struct
{
throw new ArgumentException(string.Format("Type is currently unsupported: {0}", typeof(T).ToString()), "value");

// These types could be easily implemented following the `long` function as a template:
// - bool:
// + reader.ReadBoolean();
// - byte:
// + reader.ReadByte();
// - char:
// + reader.ReadChar();
// - decimal:
// + reader.ReadDecimal();
// - double:
// + reader.ReadDouble();
// - short:
// + reader.ReadInt16();
// - int:
// + reader.ReadInt32();
// - long:
// + reader.ReadInt64();
// - sbyte:
// + reader.ReadSbyte();
// - ushort:
// + reader.ReadUInt16();
// - uint:
// + reader.ReadUInt32();
// - ulong:
// + reader.ReadUInt64();
}

/// <summary>
/// Read a `long` (64 bit signed int) from a sysfile.
/// </summary>
/// <param name="filename">The name of the sysfile (without directory)</param>
/// <param name="value">The value read or long.MinValue</param>
/// <returns>success/failure boolean</returns>
public bool ReadSysValue(string filename, out long value)
{
// Return min value on fail. Success/fail will be either exception or bool return.
value = long.MinValue;
bool success = false;

// sys files go in the root directory
string path = Path.Combine(CacheDir, filename);
object data = null;

if (File.Exists(path))
{
Expand All @@ -281,21 +382,33 @@ public object ReadSysFile(string filename)
{
using (FileStream stream = GetStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
BinaryFormatter formatter = new BinaryFormatter();
try
{
data = formatter.Deserialize(stream);
}
catch (Exception)
using(BinaryReader reader = new BinaryReader(stream))
{
data = null;
}
finally
{
stream.Close();
try
{
// The old "BinaryFormatter" sysfiles will fail this check.
if (HeaderVersionValid(reader))
{
value = reader.ReadInt64();
}
else
{
// Invalid version - return invalid value & failure.
value = long.MinValue;
success = false;
break;
}
}
catch (Exception)
{
value = long.MinValue;
// DriveCommerce: Need to rethrow to get the IOException caught.
throw;
}
}
success = true;
break;
}
break;
}
catch (IOException)
{
Expand All @@ -304,26 +417,61 @@ public object ReadSysFile(string filename)
}
}

return data;
// `value` already set correctly.
return success;
}

/// <summary>
/// Writes data to a system file that is not part of the cache itself,
/// Read a `DateTime` struct from a sysfile using `DateTime.FromBinary()`.
/// </summary>
/// <param name="filename">The name of the sysfile (without directory)</param>
/// <param name="value">The value read or DateTime.MinValue</param>
/// <returns>success/failure boolean</returns>
public bool ReadSysValue(string filename, out DateTime value)
{
// DateTime is serialized as a long, so use that `ReadSysValue()` function.
long serialized;
if (ReadSysValue(filename, out serialized))
{
value = DateTime.FromBinary(serialized);
return true;
}
// else failed:
value = DateTime.MinValue;
return false;
}

/// <summary>
/// Generic version of `WriteSysValue` just throws an ArgumentException on unknown new types.
/// </summary>
/// <param name="filename">The name of the sysfile (without directory)</param>
/// <param name="data">The data to write to the sysfile</param>
public void WriteSysValue<T>(string filename, T data) where T : struct
{
throw new ArgumentException(string.Format("Type is currently unsupported: {0}", typeof(T).ToString()), "data");
}

/// <summary>
/// Writes a long to a system file that is not part of the cache itself,
/// but is used to help it function.
/// </summary>
/// <param name="filename">The name of the sysfile (without directory)</param>
/// <param name="data">The data to write to the file</param>
public void WriteSysFile(string filename, object data)
/// <param name="data">The long to write to the file</param>
public void WriteSysValue(string filename, long data)
{
// sys files go in the root directory
string path = Path.Combine(CacheDir, filename);

// write the data to the file
using (FileStream stream = GetStream(path, FileMode.Create, FileAccess.Write, FileShare.Write))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, data);
stream.Close();
using (BinaryWriter writer = new BinaryWriter(stream))
{
// Must write the magic version header first.
HeaderVersionWrite(writer);

writer.Write(data);
}
}
}

Expand Down

0 comments on commit d738c50

Please sign in to comment.