diff --git a/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs b/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs index 9a078331..ce83418c 100644 --- a/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs +++ b/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.IO; @@ -6,16 +7,15 @@ using System.Numerics; using System.Text; using System.Xml; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using NLog; namespace VRCX { internal static class ScreenshotHelper { - private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - private static readonly ScreenshotMetadataDatabase cacheDatabase = new ScreenshotMetadataDatabase(Path.Join(Program.AppDataDirectory, "metadataCache.db")); - private static readonly Dictionary metadataCache = new Dictionary(); + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static readonly ScreenshotMetadataDatabase CacheDatabase = new(Path.Join(Program.AppDataDirectory, "metadataCache.db")); + private static readonly Dictionary MetadataCache = new(); public enum ScreenshotSearchType { @@ -25,40 +25,32 @@ namespace VRCX WorldID, } - public static bool TryGetCachedMetadata(string filePath, out ScreenshotMetadata metadata) + public static bool TryGetCachedMetadata(string filePath, out ScreenshotMetadata? metadata) { - if (metadataCache.TryGetValue(filePath, out metadata)) + if (MetadataCache.TryGetValue(filePath, out metadata)) return true; - int id = cacheDatabase.IsFileCached(filePath); + var id = CacheDatabase.IsFileCached(filePath); + if (id == -1) + return false; + + var metadataStr = CacheDatabase.GetMetadataById(id); + var metadataObj = metadataStr == null ? null : JsonConvert.DeserializeObject(metadataStr); + MetadataCache.Add(filePath, metadataObj); - if (id != -1) - { - string metadataStr = cacheDatabase.GetMetadataById(id); - var metadataObj = metadataStr == null ? null : JsonConvert.DeserializeObject(metadataStr); - - metadataCache.Add(filePath, metadataObj); - - metadata = metadataObj; - return true; - } - - return false; + metadata = metadataObj; + return true; } public static List FindScreenshots(string query, string directory, ScreenshotSearchType searchType) { var result = new List(); - var files = Directory.GetFiles(directory, "*.png", SearchOption.AllDirectories); - var addToCache = new List(); - - int amtFromCache = 0; + var amtFromCache = 0; foreach (var file in files) { - ScreenshotMetadata metadata = null; - + ScreenshotMetadata? metadata; if (TryGetCachedMetadata(file, out metadata)) { amtFromCache++; @@ -76,13 +68,13 @@ namespace VRCX if (metadata == null || metadata.Error != null) { addToCache.Add(dbEntry); - metadataCache.TryAdd(file, null); + MetadataCache.TryAdd(file, null); continue; } dbEntry.Metadata = JsonConvert.SerializeObject(metadata); addToCache.Add(dbEntry); - metadataCache.TryAdd(file, metadata); + MetadataCache.TryAdd(file, metadata); } if (metadata == null) @@ -113,14 +105,13 @@ namespace VRCX result.Add(metadata); break; - } } if (addToCache.Count > 0) - cacheDatabase.BulkAddMetadataCache(addToCache); + CacheDatabase.BulkAddMetadataCache(addToCache); - logger.ConditionalDebug("Found {0}/{1} screenshots matching query '{2}' of type '{3}'. {4}/{5} pulled from cache.", result.Count, files.Length, query, searchType, amtFromCache, files.Length); + Logger.ConditionalDebug("Found {0}/{1} screenshots matching query '{2}' of type '{3}'. {4}/{5} pulled from cache.", result.Count, files.Length, query, searchType, amtFromCache, files.Length); return result; } @@ -134,7 +125,7 @@ namespace VRCX // if (metadataCache.TryGetValue(path, out var cachedMetadata)) // return cachedMetadata; - string metadataString; + string? metadataString; // Get the metadata string from the PNG file try @@ -143,7 +134,7 @@ namespace VRCX } catch (Exception ex) { - logger.Error(ex, "Failed to read PNG description for file '{0}'", path); + Logger.Error(ex, "Failed to read PNG description for file '{0}'", path); return ScreenshotMetadata.JustError(path, "Failed to read PNG description. Check logs."); } @@ -163,13 +154,13 @@ namespace VRCX } catch (Exception ex) { - logger.Error(ex, "Failed to parse LFS/ScreenshotManager metadata for file '{0}'", path); + Logger.Error(ex, "Failed to parse LFS/ScreenshotManager metadata for file '{0}'", path); return ScreenshotMetadata.JustError(path, "Failed to parse LFS/ScreenshotManager metadata."); } } // If not JSON metadata, return early so we're not throwing/catching pointless exceptions - if (!metadataString.StartsWith("{")) + if (!metadataString.StartsWith('{')) { // parse VRC prints var xmlIndex = metadataString.IndexOf(" /// The text description that is read from the PNG file. /// - public static string ReadPNGDescription(string path) + public static string? ReadPNGDescription(string path) { if (!File.Exists(path) || !IsPNGFile(path)) return null; - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512)) - { - var existingiTXt = FindChunk(stream, "iTXt", true); - if (existingiTXt == null) return null; + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512); + var existingiTXt = FindChunk(stream, "iTXt", true); - return existingiTXt.GetText("Description"); - } + return existingiTXt?.GetText("Description"); } public static bool HasTXt(string path) { if (!File.Exists(path) || !IsPNGFile(path)) return false; - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512)) - { - var existingiTXt = FindChunk(stream, "iTXt", true); + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512); + var existingiTXt = FindChunk(stream, "iTXt", true); - return existingiTXt != null; - } + return existingiTXt != null; } - public static PNGChunk ReadTXt(string path) + public static PNGChunk? ReadTXt(string path) { if (!File.Exists(path) || !IsPNGFile(path)) return null; - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512)) - { - var existingiTXt = FindChunk(stream, "iTXt", true); + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512); + var existingiTXt = FindChunk(stream, "iTXt", true); - return existingiTXt; - } + return existingiTXt; } /// @@ -363,17 +347,14 @@ namespace VRCX /// /// The path. /// - public static string ReadPNGResolution(string path) + public static string? ReadPNGResolution(string path) { if (!File.Exists(path) || !IsPNGFile(path)) return null; - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512)) - { - var existingiHDR = FindChunk(stream, "IHDR", false); - if (existingiHDR == null) return null; - - return existingiHDR.GetResolution(); - } + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512); + var existingiHDR = FindChunk(stream, "IHDR", false); + + return existingiHDR?.GetResolution(); } /// @@ -386,14 +367,12 @@ namespace VRCX var pngSignatureBytes = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; // Read only the first 8 bytes of the file to check if it's a PNG file instead of reading the entire thing into memory just to see check a couple bytes. - using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - { - if (fs.Length < 33) return false; // I don't remember how I came up with this number, but a PNG file below this size is not going to be valid for our purposes. + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + if (fs.Length < 33) return false; // I don't remember how I came up with this number, but a PNG file below this size is not going to be valid for our purposes. - var signature = new byte[8]; - fs.Read(signature, 0, 8); - return signature.SequenceEqual(pngSignatureBytes); - } + var signature = new byte[8]; + fs.ReadExactly(signature, 0, 8); + return signature.SequenceEqual(pngSignatureBytes); } /// @@ -404,10 +383,9 @@ namespace VRCX /// private static int FindChunkIndex(byte[] png, string type) { - int chunksProcessed = 0; - int chunkSeekLimit = 5; - - bool isLittleEndian = BitConverter.IsLittleEndian; + var chunksProcessed = 0; + var chunkSeekLimit = 5; + var isLittleEndian = BitConverter.IsLittleEndian; // The first 8 bytes of the file are the png signature, so we can skip them. var index = 8; @@ -450,26 +428,22 @@ namespace VRCX private static int FindChunkIndex(FileStream fs, string type, bool seekEnd) { - int chunksProcessed = 0; - int chunkSeekLimit = 5; - - bool isLittleEndian = BitConverter.IsLittleEndian; + var chunksProcessed = 0; + var chunkSeekLimit = 5; + var isLittleEndian = BitConverter.IsLittleEndian; fs.Seek(8, SeekOrigin.Begin); - - byte[] buffer = new byte[8]; - + var buffer = new byte[8]; while (fs.Position < fs.Length) { - int chunkIndex = (int)fs.Position; - - fs.Read(buffer, 0, 8); // Read both chunkLength and chunkName at once into this buffer + var chunkIndex = (int)fs.Position; + fs.ReadExactly(buffer, 0, 8); // Read both chunkLength and chunkName at once into this buffer // BitConverter wants little endian(unless your system is big endian for some reason), PNG multi-byte integers are big endian. So we reverse the array. if (isLittleEndian) Array.Reverse(buffer, 0, 4); // Only reverse the chunkLength part - int chunkLength = BitConverter.ToInt32(buffer, 0); - string chunkType = Encoding.UTF8.GetString(buffer, 4, 4); // We don't need to reverse strings since UTF-8 strings aren't affected by endianess, given that they're a sequence of bytes. + var chunkLength = BitConverter.ToInt32(buffer, 0); + var chunkType = Encoding.UTF8.GetString(buffer, 4, 4); // We don't need to reverse strings since UTF-8 strings aren't affected by endianess, given that they're a sequence of bytes. if (chunkType == type) return chunkIndex; if (chunkType == "IEND") return -1; // Nothing should exist past IEND in a normal png file, so we should stop parsing here to avoid trying to parse junk data. @@ -493,14 +467,14 @@ namespace VRCX var chunkNameBytes = Encoding.UTF8.GetBytes(type); fs.Seek(-4096, SeekOrigin.Current); - byte[] trailingBytes = new byte[4096]; - fs.Read(trailingBytes, 0, 4096); + var trailingBytes = new byte[4096]; + fs.ReadExactly(trailingBytes, 0, 4096); // At this scale we can just brute force/naive search for the chunk name in the trailing bytes and performance will be fine. - for (int i = 0; i <= trailingBytes.Length - chunkNameBytes.Length; i++) + for (var i = 0; i <= trailingBytes.Length - chunkNameBytes.Length; i++) { - bool isMatch = true; - for (int j = 0; j < chunkNameBytes.Length; j++) + var isMatch = true; + for (var j = 0; j < chunkNameBytes.Length; j++) { if (trailingBytes[i + j] != chunkNameBytes[j]) { @@ -526,7 +500,8 @@ namespace VRCX private static int FindEndOfChunk(byte[] png, string type) { var index = FindChunkIndex(png, type); - if (index == -1) return index; + if (index == -1) + return index; var chunkLength = new byte[4]; Array.Copy(png, index, chunkLength, 0, 4); @@ -542,7 +517,7 @@ namespace VRCX /// Array of bytes representing a PNG file /// Type of PMG chunk to find /// PNGChunk - private static PNGChunk FindChunk(byte[] png, string type) + private static PNGChunk? FindChunk(byte[] png, string type) { var index = FindChunkIndex(png, type); if (index == -1) return null; @@ -564,16 +539,17 @@ namespace VRCX /// FileStream of a PNG file. /// Type of PMG chunk to find /// PNGChunk - private static PNGChunk FindChunk(FileStream fs, string type, bool seekFromEnd) + private static PNGChunk? FindChunk(FileStream fs, string type, bool seekFromEnd) { var index = FindChunkIndex(fs, type, seekFromEnd); - if (index == -1) return null; + if (index == -1) + return null; // Seek back to start of found chunk fs.Seek(index, SeekOrigin.Begin); var chunkLength = new byte[4]; - fs.Read(chunkLength, 0, 4); + fs.ReadExactly(chunkLength, 0, 4); Array.Reverse(chunkLength); var length = BitConverter.ToInt32(chunkLength, 0); @@ -581,7 +557,7 @@ namespace VRCX fs.Seek(4, SeekOrigin.Current); var chunkData = new byte[length]; - fs.Read(chunkData, 0, length); + fs.ReadExactly(chunkData, 0, length); return new PNGChunk(type, chunkData); } @@ -616,7 +592,7 @@ namespace VRCX metadata.Application = application; metadata.Version = version; - bool isCVR = application == "cvr"; + var isCVR = application == "cvr"; if (application == "screenshotmanager") { @@ -641,11 +617,10 @@ namespace VRCX var key = split[0]; var value = split[1]; - if (String.IsNullOrEmpty(value)) // One of my LFS files had an empty value for 'players:'. not pog + if (string.IsNullOrEmpty(value)) // One of my LFS files had an empty value for 'players:'. not pog continue; var parts = value.Split(','); - switch (key) { case "author": @@ -718,9 +693,9 @@ namespace VRCX // init lookup table and store crc for iTXt private static readonly uint iTXtCrc = Crc32(new[] { (byte)'i', (byte)'T', (byte)'X', (byte)'t' }, 0, 4, 0); private readonly Encoding keywordEncoding = Encoding.GetEncoding("ISO-8859-1"); // ISO-8859-1/Latin1 is the encoding used for the keyword in text chunks. - public List ChunkDataBytes; - public int ChunkDataLength; - public string ChunkType; + private List ChunkDataBytes; + private int ChunkDataLength; + private string ChunkType; public PNGChunk(string chunkType) {