diff --git a/Modules/MiscModule.cs b/Modules/MiscModule.cs index 692b891..06708f0 100644 --- a/Modules/MiscModule.cs +++ b/Modules/MiscModule.cs @@ -649,10 +649,9 @@ await message.ModifyAsync(msg => .WithCurrentTimestamp(); // Include image if the favicon exists - if (!string.IsNullOrEmpty(serverImageBase64)) + if (TryDecodeMinecraftFavicon(serverImageBase64, out byte[] imageBytes)) { embed.WithThumbnailUrl($"attachment://favicon.png"); // Point to an inline attachment URL - byte[] imageBytes = Convert.FromBase64String(serverImageBase64.Split(',')[1]); // Remove the data:image/png;base64, part MemoryStream stream = new(imageBytes); FileAttachment attachment = new(stream, "favicon.png"); @@ -677,6 +676,32 @@ await message.ModifyAsync(msg => } } + internal static bool TryDecodeMinecraftFavicon(string? favicon, out byte[] imageBytes) + { + imageBytes = []; + if (string.IsNullOrWhiteSpace(favicon)) + return false; + + const string base64Marker = ";base64,"; + int markerIndex = favicon.IndexOf(base64Marker, StringComparison.OrdinalIgnoreCase); + if (favicon.StartsWith("data:", StringComparison.OrdinalIgnoreCase) && markerIndex < 0) + return false; + + string encodedImage = markerIndex >= 0 + ? favicon[(markerIndex + base64Marker.Length)..] + : favicon; + + try + { + imageBytes = Convert.FromBase64String(encodedImage); + return imageBytes.Length > 0; + } + catch (FormatException) + { + return false; + } + } + [Name("Hash")] [Summary("Hashes a string using the specified algorithm.")] [Command("hash")] diff --git a/Morpheus.Tests/MiscModuleTests.cs b/Morpheus.Tests/MiscModuleTests.cs index 8459f83..49cae03 100644 --- a/Morpheus.Tests/MiscModuleTests.cs +++ b/Morpheus.Tests/MiscModuleTests.cs @@ -109,6 +109,31 @@ public void BuildUrbanDictionaryUrl_EncodesReservedQueryCharacters() Assert.Equal("https://api.urbandictionary.com/v0/define?term=C%23%20%26%20tea", result); } + [Theory] + [InlineData("data:image/png;base64,AQID")] + [InlineData("AQID")] + public void TryDecodeMinecraftFavicon_DecodesSupportedBase64Formats(string favicon) + { + bool decoded = MiscModule.TryDecodeMinecraftFavicon(favicon, out byte[] imageBytes); + + Assert.True(decoded); + Assert.Equal([1, 2, 3], imageBytes); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("data:image/png,not-base64")] + [InlineData("data:image/png;base64,")] + [InlineData("not-base64")] + public void TryDecodeMinecraftFavicon_RejectsMissingOrMalformedImages(string? favicon) + { + bool decoded = MiscModule.TryDecodeMinecraftFavicon(favicon, out byte[] imageBytes); + + Assert.False(decoded); + Assert.Empty(imageBytes); + } + [Fact] public async Task LoveCompatibilityCommand_AllowsComparingWithInvoker() {