1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
|
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StardewModdingAPI.Web.Framework;
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.Framework.LogParsing;
using StardewModdingAPI.Web.Framework.LogParsing.Models;
using StardewModdingAPI.Web.ViewModels;
namespace StardewModdingAPI.Web.Controllers
{
/// <summary>Provides a web UI and API for parsing SMAPI log files.</summary>
internal class LogParserController : Controller
{
/*********
** Fields
*********/
/// <summary>The site config settings.</summary>
private readonly SiteConfig Config;
/// <summary>The underlying Pastebin client.</summary>
private readonly IPastebinClient Pastebin;
/// <summary>The first bytes in a valid zip file.</summary>
/// <remarks>See <a href="https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers"/>.</remarks>
private const uint GzipLeadBytes = 0x8b1f;
/*********
** Public methods
*********/
/***
** Constructor
***/
/// <summary>Construct an instance.</summary>
/// <param name="siteConfig">The context config settings.</param>
/// <param name="pastebin">The Pastebin API client.</param>
public LogParserController(IOptions<SiteConfig> siteConfig, IPastebinClient pastebin)
{
this.Config = siteConfig.Value;
this.Pastebin = pastebin;
}
/***
** Web UI
***/
/// <summary>Render the log parser UI.</summary>
/// <param name="id">The paste ID.</param>
/// <param name="raw">Whether to display the raw unparsed log.</param>
[HttpGet]
[Route("log")]
[Route("log/{id}")]
public async Task<ViewResult> Index(string id = null, bool raw = false)
{
// fresh page
if (string.IsNullOrWhiteSpace(id))
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id));
// log page
PasteInfo paste = await this.GetAsync(id);
ParsedLog log = paste.Success
? new LogParser().Parse(paste.Content)
: new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error };
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log, raw));
}
/***
** JSON
***/
/// <summary>Save raw log data.</summary>
[HttpPost, AllowLargePosts]
[Route("log")]
public async Task<ActionResult> PostAsync()
{
// get raw log text
string input = this.Request.Form["input"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(input))
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null) { UploadError = "The log file seems to be empty." });
// upload log
input = this.CompressString(input);
SavePasteResult result = await this.Pastebin.PostAsync(input);
// handle errors
if (!result.Success)
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" });
// redirect to view
UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl));
uri.Path = uri.Path.TrimEnd('/') + '/' + result.ID;
return this.Redirect(uri.Uri.ToString());
}
/*********
** Private methods
*********/
/// <summary>Fetch raw text from Pastebin.</summary>
/// <param name="id">The Pastebin paste ID.</param>
private async Task<PasteInfo> GetAsync(string id)
{
PasteInfo response = await this.Pastebin.GetAsync(id);
response.Content = this.DecompressString(response.Content);
return response;
}
/// <summary>Compress a string.</summary>
/// <param name="text">The text to compress.</param>
/// <remarks>Derived from <a href="https://stackoverflow.com/a/17993002/262123"/>.</remarks>
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
byte[] 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);
}
/// <summary>Decompress a string.</summary>
/// <param name="rawText">The compressed text.</param>
/// <remarks>Derived from <a href="https://stackoverflow.com/a/17993002/262123"/>.</remarks>
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
byte[] 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);
}
}
}
}
|