From 65f0fa625575592639a24a9b39330e4a6b500f22 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:36:31 -0400 Subject: add scaffolding for web UI (#358) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 162 +++++++++++++++++++++++++ src/SMAPI.Web/Controllers/ModsController.cs | 162 ------------------------- 2 files changed, 162 insertions(+), 162 deletions(-) create mode 100644 src/SMAPI.Web/Controllers/ModsApiController.cs delete mode 100644 src/SMAPI.Web/Controllers/ModsController.cs (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs new file mode 100644 index 00000000..1db5b59e --- /dev/null +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.Framework.ModRepositories; + +namespace StardewModdingAPI.Web.Controllers +{ + /// Provides an API to perform mod update checks. + [Produces("application/json")] + [Route("api/{version:semanticVersion}/mods")] + internal class ModsApiController : Controller + { + /********* + ** Properties + *********/ + /// The mod repositories which provide mod metadata. + private readonly IDictionary Repositories; + + /// The cache in which to store mod metadata. + private readonly IMemoryCache Cache; + + /// The number of minutes update checks should be cached before refetching them. + private readonly int CacheMinutes; + + /// A regex which matches SMAPI-style semantic version. + private readonly string VersionRegex; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The cache in which to store mod metadata. + /// The config settings for mod update checks. + public ModsApiController(IMemoryCache cache, IOptions configProvider) + { + ModUpdateCheckConfig config = configProvider.Value; + + this.Cache = cache; + this.CacheMinutes = config.CacheMinutes; + this.VersionRegex = config.SemanticVersionRegex; + + string version = this.GetType().Assembly.GetName().Version.ToString(3); + this.Repositories = + new IModRepository[] + { + new ChucklefishRepository( + vendorKey: config.ChucklefishKey, + userAgent: string.Format(config.ChucklefishUserAgent, version), + baseUrl: config.ChucklefishBaseUrl, + modPageUrlFormat: config.ChucklefishModPageUrlFormat + ), + new GitHubRepository( + vendorKey: config.GitHubKey, + baseUrl: config.GitHubBaseUrl, + releaseUrlFormat: config.GitHubReleaseUrlFormat, + userAgent: string.Format(config.GitHubUserAgent, version), + acceptHeader: config.GitHubAcceptHeader, + username: config.GitHubUsername, + password: config.GitHubPassword + ), + new NexusRepository( + vendorKey: config.NexusKey, + userAgent: config.NexusUserAgent, + baseUrl: config.NexusBaseUrl, + modUrlFormat: config.NexusModUrlFormat + ) + } + .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); + } + + /// Fetch version metadata for the given mods. + /// The namespaced mod keys to search as a comma-delimited array. + [HttpGet] + public async Task> GetAsync(string modKeys) + { + string[] modKeysArray = modKeys?.Split(',').ToArray(); + if (modKeysArray == null || !modKeysArray.Any()) + return new Dictionary(); + + return await this.PostAsync(new ModSearchModel(modKeysArray)); + } + + /// Fetch version metadata for the given mods. + /// The mod search criteria. + [HttpPost] + public async Task> PostAsync([FromBody] ModSearchModel search) + { + // sort & filter keys + string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) + .Distinct(StringComparer.CurrentCultureIgnoreCase) + .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) + .ToArray(); + + // fetch mod info + IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + foreach (string modKey in modKeys) + { + // parse mod key + if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) + { + result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + continue; + } + + // get matching repository + if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + { + result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + continue; + } + + // fetch mod info + result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); + + ModInfoModel info = await repository.GetModInfoAsync(modID); + if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))) + info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'."); + + return info; + }); + } + + return result; + } + + + /********* + ** Private methods + *********/ + /// Parse a namespaced mod ID. + /// The raw mod ID to parse. + /// The parsed vendor key. + /// The parsed mod ID. + /// Returns whether the value could be parsed. + private bool TryParseModKey(string raw, out string vendorKey, out string modID) + { + // split parts + string[] parts = raw?.Split(':'); + if (parts == null || parts.Length != 2) + { + vendorKey = null; + modID = null; + return false; + } + + // parse + vendorKey = parts[0].Trim(); + modID = parts[1].Trim(); + return true; + } + } +} diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs deleted file mode 100644 index a671ddca..00000000 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using StardewModdingAPI.Common.Models; -using StardewModdingAPI.Web.Framework.ConfigModels; -using StardewModdingAPI.Web.Framework.ModRepositories; - -namespace StardewModdingAPI.Web.Controllers -{ - /// Provides an API to perform mod update checks. - [Produces("application/json")] - [Route("api/{version:semanticVersion}/[controller]")] - internal class ModsController : Controller - { - /********* - ** Properties - *********/ - /// The mod repositories which provide mod metadata. - private readonly IDictionary Repositories; - - /// The cache in which to store mod metadata. - private readonly IMemoryCache Cache; - - /// The number of minutes update checks should be cached before refetching them. - private readonly int CacheMinutes; - - /// A regex which matches SMAPI-style semantic version. - private readonly string VersionRegex; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The cache in which to store mod metadata. - /// The config settings for mod update checks. - public ModsController(IMemoryCache cache, IOptions configProvider) - { - ModUpdateCheckConfig config = configProvider.Value; - - this.Cache = cache; - this.CacheMinutes = config.CacheMinutes; - this.VersionRegex = config.SemanticVersionRegex; - - string version = this.GetType().Assembly.GetName().Version.ToString(3); - this.Repositories = - new IModRepository[] - { - new ChucklefishRepository( - vendorKey: config.ChucklefishKey, - userAgent: string.Format(config.ChucklefishUserAgent, version), - baseUrl: config.ChucklefishBaseUrl, - modPageUrlFormat: config.ChucklefishModPageUrlFormat - ), - new GitHubRepository( - vendorKey: config.GitHubKey, - baseUrl: config.GitHubBaseUrl, - releaseUrlFormat: config.GitHubReleaseUrlFormat, - userAgent: string.Format(config.GitHubUserAgent, version), - acceptHeader: config.GitHubAcceptHeader, - username: config.GitHubUsername, - password: config.GitHubPassword - ), - new NexusRepository( - vendorKey: config.NexusKey, - userAgent: config.NexusUserAgent, - baseUrl: config.NexusBaseUrl, - modUrlFormat: config.NexusModUrlFormat - ) - } - .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); - } - - /// Fetch version metadata for the given mods. - /// The namespaced mod keys to search as a comma-delimited array. - [HttpGet] - public async Task> GetAsync(string modKeys) - { - string[] modKeysArray = modKeys?.Split(',').ToArray(); - if (modKeysArray == null || !modKeysArray.Any()) - return new Dictionary(); - - return await this.PostAsync(new ModSearchModel(modKeysArray)); - } - - /// Fetch version metadata for the given mods. - /// The mod search criteria. - [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel search) - { - // sort & filter keys - string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) - .ToArray(); - - // fetch mod info - IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); - foreach (string modKey in modKeys) - { - // parse mod key - if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) - { - result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); - continue; - } - - // get matching repository - if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) - { - result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); - continue; - } - - // fetch mod info - result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => - { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); - - ModInfoModel info = await repository.GetModInfoAsync(modID); - if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))) - info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'."); - - return info; - }); - } - - return result; - } - - - /********* - ** Private methods - *********/ - /// Parse a namespaced mod ID. - /// The raw mod ID to parse. - /// The parsed vendor key. - /// The parsed mod ID. - /// Returns whether the value could be parsed. - private bool TryParseModKey(string raw, out string vendorKey, out string modID) - { - // split parts - string[] parts = raw?.Split(':'); - if (parts == null || parts.Length != 2) - { - vendorKey = null; - modID = null; - return false; - } - - // parse - vendorKey = parts[0].Trim(); - modID = parts[1].Trim(); - return true; - } - } -} -- cgit From e75aef8634f9edb8ac385b8d7308b40ed3269cbc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:36:52 -0400 Subject: add placeholder for new log parser (#358) --- src/SMAPI.Web/Controllers/LogParserController.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/SMAPI.Web/Controllers/LogParserController.cs (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs new file mode 100644 index 00000000..4ed8898a --- /dev/null +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; + +namespace StardewModdingAPI.Web.Controllers +{ + /// Provides a web UI and API for parsing SMAPI log files. + [Route("log")] + internal class LogParserController : Controller + { + /********* + ** Public methods + *********/ + /// Render the web UI to upload a log file. + [HttpGet] + public ViewResult Index() + { + return this.View("Index"); + } + } +} -- cgit From ad5bb5b49af49c4668fd30fb2a0e606dcefe4ec0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:39:13 -0400 Subject: proxy Pastebin requests through our API instead of third parties, improve error-handling (#358) --- src/SMAPI.Web/Controllers/LogParserController.cs | 54 +++++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 4ed8898a..893d9a52 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -1,19 +1,69 @@ +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.Framework.LogParser; namespace StardewModdingAPI.Web.Controllers { /// Provides a web UI and API for parsing SMAPI log files. - [Route("log")] internal class LogParserController : Controller { + /********* + ** Properties + *********/ + /// The underlying Pastebin client. + private readonly PastebinClient PastebinClient; + + /********* ** Public methods *********/ - /// Render the web UI to upload a log file. + /*** + ** Constructor + ***/ + /// Construct an instance. + /// The log parser config settings. + public LogParserController(IOptions configProvider) + { + // init Pastebin client + LogParserConfig config = configProvider.Value; + string version = this.GetType().Assembly.GetName().Version.ToString(3); + string userAgent = string.Format(config.PastebinUserAgent, version); + this.PastebinClient = new PastebinClient(config.PastebinBaseUrl, userAgent, config.PastebinDevKey); + } + + /*** + ** Web UI + ***/ + /// Render the log parser UI. [HttpGet] + [Route("log")] public ViewResult Index() { return this.View("Index"); } + + /*** + ** JSON + ***/ + /// Fetch raw text from Pastebin. + /// The Pastebin paste ID. + [HttpGet, Produces("application/json")] + [Route("log/fetch/{id}")] + public async Task GetAsync(string id) + { + return await this.PastebinClient.GetAsync(id); + } + + /// Save raw log data. + /// The log content to save. + [HttpPost, Produces("application/json"), AllowLargePosts] + [Route("log/save")] + public async Task PostAsync([FromBody] string content) + { + return await this.PastebinClient.PostAsync(content); + } } } -- cgit From 3f43ebcc0e31db523fa82a163374cebf2f577cde Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 21:10:36 -0400 Subject: fix issues with subdomain routing in log UI (#358) --- src/SMAPI.Web/Controllers/LogParserController.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 893d9a52..ee1d51cd 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.LogParser; +using StardewModdingAPI.Web.ViewModels; namespace StardewModdingAPI.Web.Controllers { @@ -13,6 +14,9 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Properties *********/ + /// The log parser config settings. + private readonly LogParserConfig Config; + /// The underlying Pastebin client. private readonly PastebinClient PastebinClient; @@ -28,10 +32,10 @@ namespace StardewModdingAPI.Web.Controllers public LogParserController(IOptions configProvider) { // init Pastebin client - LogParserConfig config = configProvider.Value; + this.Config = configProvider.Value; string version = this.GetType().Assembly.GetName().Version.ToString(3); - string userAgent = string.Format(config.PastebinUserAgent, version); - this.PastebinClient = new PastebinClient(config.PastebinBaseUrl, userAgent, config.PastebinDevKey); + string userAgent = string.Format(this.Config.PastebinUserAgent, version); + this.PastebinClient = new PastebinClient(this.Config.PastebinBaseUrl, userAgent, this.Config.PastebinDevKey); } /*** @@ -42,7 +46,7 @@ namespace StardewModdingAPI.Web.Controllers [Route("log")] public ViewResult Index() { - return this.View("Index"); + return this.View("Index", new LogParserModel(this.Config.SectionUrl)); } /*** -- cgit From 9a091bd961ed6dca221316a50c04b91318514ce3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Oct 2017 11:51:25 -0400 Subject: fix API version format --- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 1db5b59e..a600662c 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -14,7 +14,7 @@ namespace StardewModdingAPI.Web.Controllers { /// Provides an API to perform mod update checks. [Produces("application/json")] - [Route("api/{version:semanticVersion}/mods")] + [Route("api/v{version:semanticVersion}/mods")] internal class ModsApiController : Controller { /********* -- cgit From fe5b2f62da27ac0e2ddf60240f2b19cf2a404f69 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Oct 2017 12:38:30 -0400 Subject: prettify log URL, read paste ID serverside (#358) --- src/SMAPI.Web/Controllers/LogParserController.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index ee1d51cd..f9943707 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -42,11 +42,13 @@ namespace StardewModdingAPI.Web.Controllers ** Web UI ***/ /// Render the log parser UI. + /// The paste ID. [HttpGet] [Route("log")] - public ViewResult Index() + [Route("log/{id}")] + public ViewResult Index(string id = null) { - return this.View("Index", new LogParserModel(this.Config.SectionUrl)); + return this.View("Index", new LogParserModel(this.Config.SectionUrl, id)); } /*** -- cgit From 790a62920b15f1f948724f5b2a70a937829355dd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Oct 2017 14:05:29 -0400 Subject: link pastes to Pastebin account & tweak paste options (#358) --- src/SMAPI.Web/Controllers/LogParserController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index f9943707..8e986196 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI.Web.Controllers this.Config = configProvider.Value; string version = this.GetType().Assembly.GetName().Version.ToString(3); string userAgent = string.Format(this.Config.PastebinUserAgent, version); - this.PastebinClient = new PastebinClient(this.Config.PastebinBaseUrl, userAgent, this.Config.PastebinDevKey); + this.PastebinClient = new PastebinClient(this.Config.PastebinBaseUrl, userAgent, this.Config.PastebinUserKey, this.Config.PastebinDevKey); } /*** -- cgit From 7ed1fbf0aac5a3b777e3ed5b8f104bd27ce4bf0c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Oct 2017 15:28:07 -0400 Subject: defer log compression to backend and significantly improve compression (#358) --- src/SMAPI.Web/Controllers/LogParserController.cs | 84 +++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 8e986196..f143bc5c 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -1,3 +1,7 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -20,6 +24,10 @@ namespace StardewModdingAPI.Web.Controllers /// The underlying Pastebin client. private readonly PastebinClient PastebinClient; + /// The first bytes in a valid zip file. + /// See . + private const uint GzipLeadBytes = 0x8b1f; + /********* ** Public methods @@ -60,7 +68,9 @@ namespace StardewModdingAPI.Web.Controllers [Route("log/fetch/{id}")] public async Task GetAsync(string id) { - return await this.PastebinClient.GetAsync(id); + GetPasteResponse response = await this.PastebinClient.GetAsync(id); + response.Content = this.DecompressString(response.Content); + return response; } /// Save raw log data. @@ -69,7 +79,79 @@ namespace StardewModdingAPI.Web.Controllers [Route("log/save")] public async Task PostAsync([FromBody] string content) { + content = this.CompressString(content); return await this.PastebinClient.PostAsync(content); } + + + /********* + ** Private methods + *********/ + /// Compress a string. + /// The text to compress. + /// Derived from . + private string CompressString(string text) + { + // get raw bytes + byte[] buffer = Encoding.UTF8.GetBytes(text); + + // compressed + byte[] compressedData; + using (MemoryStream stream = new MemoryStream()) + { + using (GZipStream zipStream = new GZipStream(stream, CompressionLevel.Optimal, leaveOpen: true)) + zipStream.Write(buffer, 0, buffer.Length); + + stream.Position = 0; + compressedData = new byte[stream.Length]; + stream.Read(compressedData, 0, compressedData.Length); + } + + // prefix length + var zipBuffer = new byte[compressedData.Length + 4]; + Buffer.BlockCopy(compressedData, 0, zipBuffer, 4, compressedData.Length); + Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, zipBuffer, 0, 4); + + // return string representation + return Convert.ToBase64String(zipBuffer); + } + + /// Decompress a string. + /// The compressed text. + /// Derived from . + private string DecompressString(string rawText) + { + // get raw bytes + byte[] zipBuffer; + try + { + zipBuffer = Convert.FromBase64String(rawText); + } + catch + { + return rawText; // not valid base64, wasn't compressed by the log parser + } + + // skip if not gzip + if (BitConverter.ToUInt16(zipBuffer, 4) != LogParserController.GzipLeadBytes) + return rawText; + + // decompress + using (MemoryStream memoryStream = new MemoryStream()) + { + // read length prefix + int dataLength = BitConverter.ToInt32(zipBuffer, 0); + memoryStream.Write(zipBuffer, 4, zipBuffer.Length - 4); + + // read data + var buffer = new byte[dataLength]; + memoryStream.Position = 0; + using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + gZipStream.Read(buffer, 0, buffer.Length); + + // return original string + return Encoding.UTF8.GetString(buffer); + } + } } } -- cgit