Add sanitization to loaded GIF images

Parse the provided loaded background images and reconstruct a new GIF file on the fly to ensure they don't contain any extension that might not be supported by unalunch.
The generated GIF file will have a global color table, and single section, being the image descriptor. It also won't have any looping block.
This commit is contained in:
Edoardo Lolletti 2025-08-19 18:18:24 +02:00 committed by GitHub
parent 5bfa4b1a00
commit 6edcde25c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 282 additions and 68 deletions

View File

@ -1,13 +1,19 @@
#include "bgMenu.h"
#include "main.h"
#include "menu.h"
#include "gifConverter.h"
#include "message.h"
#include <algorithm>
#include <array>
#include <format>
#include <nds.h>
#include <dirent.h>
#include <string>
#include <vector>
static std::array<uint8_t, MAX_GIF_SIZE> currentlyLoadedGif;
static const auto& getBackgroundList()
{
static auto bgs = []{
@ -39,7 +45,7 @@ static const auto& getBackgroundList()
return bgs;
}
const char* backgroundMenu()
std::span<uint8_t> backgroundMenu()
{
//top screen
clearScreen(&topScreen);
@ -79,12 +85,17 @@ const char* backgroundMenu()
}
}
const char* result = nullptr;
if(static_cast<size_t>(m->cursor) < bgs.size())
result = bgs[m->cursor].second.data();
else if(static_cast<size_t>(m->cursor) == bgs.size())
result = "default";
auto selection = static_cast<size_t>(m->cursor);
freeMenu(m);
return result;
if(selection < bgs.size()) {
try {
return parseGif(bgs[selection].second.data(), currentlyLoadedGif);
} catch(const std::exception& e) {
messageBox(std::format("\x1B[31mError:\x1B[33m The image could not\n"
"be loaded: {}", e.what()).data());
}
}
return {};
}

View File

@ -1,14 +1,9 @@
#ifndef BGMENU_H
#define BGMENU_H
#ifdef __cplusplus
extern "C" {
#endif
#include <cstdint>
#include <span>
const char* backgroundMenu();
#ifdef __cplusplus
}
#endif
std::span<uint8_t> backgroundMenu();
#endif

225
arm9/src/gifConverter.cpp Normal file
View File

@ -0,0 +1,225 @@
#include <array>
#include <cstring>
#include <cstdio>
#include <cstdint>
#include <format>
#include <memory>
#include <stdexcept>
#include <vector>
#include "storage.h"
#include "unlaunch.h"
struct Gif {
struct Header {
char signature[6];
uint16_t width;
uint16_t height;
uint8_t gctSize: 3;
uint8_t sortFlag: 1;
uint8_t colorResolution: 3;
uint8_t gctFlag: 1;
uint8_t bgColor;
uint8_t pixelAspectRatio;
} __attribute__ ((__packed__)) header;
static_assert(sizeof(Header) == 13);
std::array<uint8_t, 255 * 3> colorTable;
size_t numColors;
struct Frame {
struct Descriptor {
uint16_t x;
uint16_t y;
uint16_t w;
uint16_t h;
uint8_t lctSize: 3;
uint8_t reserved: 2;
uint8_t sortFlag: 1;
uint8_t interlaceFlag: 1;
uint8_t lctFlag: 1;
} __attribute__ ((__packed__)) descriptor;
static_assert(sizeof(Descriptor) == 9);
struct Image {
uint8_t lzwMinimumCodeSize;
std::vector<std::vector<uint8_t>> imageDataChunks;
} image;
} frame;
};
static constexpr auto WIDTH = 256;
static constexpr auto HEIGHT = 192;
Gif getGif(std::string_view path) {
Gif gif;
auto file = fopen(path.data(), "rb");
if (!file)
throw std::runtime_error("Failed to open file");
auto fileSptr = std::shared_ptr<FILE>(file, fclose);
if(auto size = getFileSize(file); size < 7 || size > MAX_GIF_SIZE)
{
throw std::runtime_error("Gif file too big.\n");
}
auto& header = gif.header;
// Read header
fread(&header, 1, sizeof(header), file);
// Check that this is a GIF
if (memcmp(header.signature, "GIF87a", sizeof(header.signature)) != 0 && memcmp(header.signature, "GIF89a", sizeof(header.signature)) != 0) {
throw std::runtime_error("File not a gif");
}
if(header.width != WIDTH || header.height != HEIGHT) {
throw std::runtime_error("Invalid gif size");
}
auto& numColors = gif.numColors;
auto& color_table = gif.colorTable;
// Load global color table
if (header.gctFlag) {
numColors = (2 << header.gctSize);
fread(color_table.data(), 1, numColors * 3, file);
}
auto& frame = gif.frame;
bool gotImage = false;
while (1) {
switch (fgetc(file)) {
case 0x21: { // Extension
switch (fgetc(file)) {
case 0xF9: { // Graphics Control
uint8_t toRead = fgetc(file);
fseek(file, toRead, SEEK_CUR);
fgetc(file); // Terminator
break;
}
case 0x01: { // Plain text
throw std::runtime_error("Plain text found");
#if 0
fseek(file, 12, SEEK_CUR);
while (uint8_t size = fgetc(file)) {
fseek(file, size, SEEK_CUR);
}
break;
#endif
}
case 0xFF: { // Application extension
throw std::runtime_error("Application extension found");
#if 0
if (fgetc(file) == 0xB) {
char buffer[0xC] = {0};
fread(buffer, 1, 0xB, file);
if (strcmp(buffer, "NETSCAPE2.0") == 0) { // Check for Netscape loop count
fseek(file, 2, SEEK_CUR);
fseek(file, 2, SEEK_CUR);
fgetc(file); //terminator
break;
}
}
[[fallthrough]];
#endif
}
case 0xFE: { // Comment
// Skip comments and unsupported application extionsions
while (uint8_t size = fgetc(file)) {
fseek(file, size, SEEK_CUR);
}
break;
}
default: {
throw std::runtime_error("Unknown GIF extension found");
}
}
break;
} case 0x2C: { // Image descriptor
gotImage = true;
fread(&frame.descriptor, 1, sizeof(frame.descriptor), file);
if (frame.descriptor.lctFlag) {
header.gctFlag = 1;
header.gctSize = frame.descriptor.lctSize;
header.sortFlag = frame.descriptor.sortFlag;
numColors = 2 << header.gctSize;
fread(color_table.data(), 1, numColors * 3, file);
frame.descriptor.lctFlag = 0;
frame.descriptor.lctSize = 0;
frame.descriptor.sortFlag = 0;
}
if(frame.descriptor.w != WIDTH || frame.descriptor.h != HEIGHT) {
throw std::runtime_error("Wrong frame size");
}
if(frame.descriptor.x != 0 || frame.descriptor.y != 0) {
throw std::runtime_error("Wrong frame coordinates");
}
frame.image.lzwMinimumCodeSize = fgetc(file);
while (uint8_t size = fgetc(file)) {
std::vector<uint8_t> dataChunk;
dataChunk.resize(size);
fread(dataChunk.data(), 1, size, file);
frame.image.imageDataChunks.push_back(std::move(dataChunk));
}
goto breakWhile;
} case 0x3B: { // Trailer
goto breakWhile;
}
}
if(feof(file)){
throw std::runtime_error("Unexpected file termination");
}
}
breakWhile:
if(!gotImage){
throw std::runtime_error("Image data not found in gif");
}
if(!header.gctFlag) {
throw std::runtime_error("Invalid gif (missing color table)");
}
return gif;
}
std::span<uint8_t> writeGif(const Gif& gif, std::array<uint8_t, MAX_GIF_SIZE>& outArr) {
size_t totalWritten = 0;
auto writeArr = [&, ptr = outArr.data()](const void* data, size_t len) mutable {
if((totalWritten + len) > MAX_GIF_SIZE)
throw std::runtime_error("Gif too big");
memcpy(ptr, data, len);
ptr += len;
totalWritten += len;
};
auto writeCh = [&](char ch) mutable {
writeArr(&ch, 1);
};
writeArr(&gif.header, sizeof(gif.header));
writeArr(gif.colorTable.data(), gif.numColors * 3);
// write single image descriptor
writeCh(0x2C);
writeArr(&gif.frame.descriptor, sizeof(gif.frame.descriptor));
// write image data
writeCh(gif.frame.image.lzwMinimumCodeSize);
for(const auto& chunk : gif.frame.image.imageDataChunks){
writeCh(chunk.size());
writeArr(chunk.data(), chunk.size());
}
writeCh('\0');
// write trailer
writeCh(0x3B);
return std::span{outArr.data(), totalWritten};
}
std::span<uint8_t> parseGif(const char* path, std::array<uint8_t, MAX_GIF_SIZE>& outArr) {
const auto gif = getGif(path);
return writeGif(gif, outArr);
}

12
arm9/src/gifConverter.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef GIF_CONVERTER_H
#define GIF_CONVERTER_H
#include <array>
#include <cstdint>
#include <span>
#include "unlaunch.h"
std::span<uint8_t> parseGif(const char* path, std::array<uint8_t, MAX_GIF_SIZE>& outArr);
#endif //GIF_CONVERTER_H

View File

@ -26,7 +26,7 @@ static UNLAUNCH_VERSION foundUnlaunchInstallerVersion = INVALID;
static bool disableAllPatches = false;
static bool enableSoundAndSplash = false;
static const char* splashSoundBinaryPatchPath = NULL;
static const char* customBgPath = NULL;
static std::span<uint8_t> customBgSpan{};
volatile bool charging = false;
volatile u8 batteryLevel = 0;
static bool advancedOptionsUnlocked = false;
@ -590,7 +590,7 @@ void install(consoleInfo& info) {
}
if(installUnlaunch(info, disableAllPatches,
enableSoundAndSplash ? splashSoundBinaryPatchPath : NULL,
customBgPath))
customBgSpan))
{
messageBox("Install successful!\n");
info.tmdGood = false;
@ -615,19 +615,7 @@ void customBg() {
{
return;
}
const char* customBg = backgroundMenu();
if(!customBg)
{
return;
}
if(strcmp(customBg, "default") == 0)
{
customBgPath = NULL;
}
else
{
customBgPath = customBg;
}
customBgSpan = backgroundMenu();
}
void doMainMenu(consoleInfo& info) {

View File

@ -3,10 +3,12 @@
#include "storage.h"
#include "tonccpy.h"
#include "unlaunch.h"
#include <algorithm>
#include <nds/sha1.h>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <unistd.h>
#include <memory>
#include <format>
@ -425,38 +427,19 @@ static bool verifyUnlaunchInstaller(void)
return true;
}
static bool patchCustomBackground(const char* customBackgroundPath)
static bool patchCustomBackground(std::span<uint8_t> customBackground)
{
if(!customBackgroundPath)
auto size = customBackground.size();
if(size == 0)
{
return true;
}
auto bgGif = fopen(customBackgroundPath, "rb");
if(!bgGif)
if(size < 7 || size > MAX_GIF_SIZE)
{
messageBox("\x1B[31mError:\x1B[33m Failed to open custom bg gif.\n");
return false;
}
auto size = getFileSize(bgGif);
if(size < 7 || size > 0x3C70)
{
messageBox("\x1B[31mError:\x1B[33m Invalid gif file.\n");
fclose(bgGif);
return false;
}
u16 gifWidth;
u16 gifHeight;
if((fseek(bgGif, 6, SEEK_SET) != 0) || (fread(&gifWidth, 1, sizeof(u16), bgGif) != sizeof(u16)) || (fread(&gifHeight, 1, sizeof(u16), bgGif) != sizeof(u16)))
{
messageBox("\x1B[31mError:\x1B[33m Failed to parse gif file.\n");
fclose(bgGif);
return false;
}
if (gifWidth != 256 || gifHeight != 192) {
messageBox("\x1B[31mError:\x1B[33m Gif file has invalid dimensions.\n");
fclose(bgGif);
messageBox("\x1B[31mError:\x1B[33m Gif file too big.\n");
return false;
}
const u32 gifSignatureStart = 0x38464947;
const u32 gifSignatureEnd = 0x3B000044;
@ -468,14 +451,11 @@ static bool patchCustomBackground(const char* customBackgroundPath)
if(*gifStart != gifSignatureStart || *gifEnd != gifSignatureEnd)
{
messageBox("\x1B[31mError:\x1B[33m Gif offsets not matching.\n");
fclose(bgGif);
return false;
}
fseek(bgGif, 0, SEEK_SET);
//read the whole file, could be less than 0x3C70, but unlaunch should then just ignore the leftover data
fread(gifStart, 1, 0x3C70, bgGif);
std::memcpy(gifStart, customBackground.data(), size);
return true;
}
@ -507,7 +487,7 @@ static bool applyBinaryPatch(const char* path)
return true;
}
static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSoundBinaryPatchPath, const char* customBackgroundPath)
static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSoundBinaryPatchPath, std::span<uint8_t> customBackground)
{
tonccpy(unlaunchInstallerBuffer, ogUnlaunchInstallerBuffer, sizeof(unlaunchInstallerBuffer));
if (splashSoundBinaryPatchPath)
@ -530,7 +510,7 @@ static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSou
return false;
}
}
if(!patchCustomBackground(customBackgroundPath))
if(!patchCustomBackground(customBackground))
{
return false;
}
@ -559,9 +539,9 @@ const char* getUnlaunchVersionString(UNLAUNCH_VERSION version)
return unlaunchVersionStrings[version];
}
bool installUnlaunch(const consoleInfo& info, bool disableAllPatches, const char* splashSoundBinaryPatchPath, const char* customBackgroundPath)
bool installUnlaunch(const consoleInfo& info, bool disableAllPatches, const char* splashSoundBinaryPatchPath, std::span<uint8_t> customBackground)
{
if (installerVersion == INVALID || !patchUnlaunchInstaller(disableAllPatches, splashSoundBinaryPatchPath, customBackgroundPath))
if (installerVersion == INVALID || !patchUnlaunchInstaller(disableAllPatches, splashSoundBinaryPatchPath, customBackground))
return false;
// Treat protos differently

View File

@ -1,6 +1,7 @@
#ifndef UNLAUNCH_H
#define UNLAUNCH_H
#include <string_view>
#include <span>
#include "consoleInfo.h"
@ -9,10 +10,12 @@ typedef enum UNLAUNCH_VERSION {
INVALID,
} UNLAUNCH_VERSION;
static constexpr auto MAX_GIF_SIZE = 0x3C70;
const char* getUnlaunchVersionString(UNLAUNCH_VERSION);
bool uninstallUnlaunch(const consoleInfo& info, bool removeHNAABackup);
bool installUnlaunch(const consoleInfo& info, bool disableAllPatches, const char* splashSoundBinaryPatchPath, const char* customBackgroundPath);
bool installUnlaunch(const consoleInfo& info, bool disableAllPatches, const char* splashSoundBinaryPatchPath, std::span<uint8_t> customBackground);
UNLAUNCH_VERSION loadUnlaunchInstaller(std::string_view path);