Use UpperMap for rgblink -S scramble spec matching

This also makes invalid RGBLINK CLI options into fatal errors
like the other programs
This commit is contained in:
Rangi42
2025-07-29 03:37:32 -04:00
parent cecbf0aa0e
commit 308d488833
5 changed files with 100 additions and 142 deletions

View File

@@ -8,6 +8,7 @@
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <utility>
#include "diagnostics.hpp"
#include "extern/getopt.hpp"
@@ -16,6 +17,7 @@
#include "platform.hpp"
#include "script.hpp" // Generated from script.y
#include "usage.hpp"
#include "util.hpp" // UpperMap, printChar
#include "version.hpp"
#include "link/assign.hpp"
@@ -100,145 +102,111 @@ static Usage usage(
);
// clang-format on
enum ScrambledRegion {
SCRAMBLE_ROMX,
SCRAMBLE_SRAM,
SCRAMBLE_WRAMX,
static void parseScrambleSpec(char *spec) {
static UpperMap<std::pair<uint16_t *, uint16_t>> scrambleSpecs{
{"ROMX", std::pair{&options.scrambleROMX, 65535}},
{"SRAM", std::pair{&options.scrambleSRAM, 255} },
{"WRAMX", std::pair{&options.scrambleWRAMX, 7} },
};
SCRAMBLE_UNK, // Used for errors
};
struct {
char const *name;
uint16_t max;
} scrambleSpecs[SCRAMBLE_UNK] = {
{"romx", 65535}, // SCRAMBLE_ROMX
{"sram", 255 }, // SCRAMBLE_SRAM
{"wramx", 7 }, // SCRAMBLE_WRAMX
};
static void parseScrambleSpec(char const *spec) {
// Skip any leading whitespace
// Skip leading whitespace before the regions.
spec += strspn(spec, " \t");
// The argument to `-S` should be a comma-separated list of sections followed by an '='
// indicating their scramble limit.
while (spec) {
// Invariant: we should not be pointing at whitespace at this point
assume(*spec != ' ' && *spec != '\t');
// The argument to `-S` should be a comma-separated list of regions, allowing a trailing comma.
// Each region name is optionally followed by an '=' and a region size.
while (*spec) {
char *regionName = spec;
// Remember where the region's name begins and ends
char const *regionName = spec;
size_t regionNameLen = strcspn(spec, "=, \t");
// Length of region name string slice for print formatting, truncated if too long
int regionNameFmtLen = regionNameLen > INT_MAX ? INT_MAX : static_cast<int>(regionNameLen);
ScrambledRegion region = SCRAMBLE_UNK;
// The region name continues (skipping any whitespace) until a ',' (next region),
// '=' (region size), or the end of the string.
size_t regionNameLen = strcspn(regionName, "=, \t");
// Skip trailing whitespace after the region name.
size_t regionNameSkipLen = regionNameLen + strspn(regionName + regionNameLen, " \t");
spec = regionName + regionNameSkipLen;
// If this trips, `spec` must be pointing at a ',' or '=' (or NUL) due to the assumption
if (regionNameLen == 0) {
argError('S', "Missing region name");
if (*spec == '\0') {
break;
}
if (*spec == '=') { // Skip the limit, too
spec = strchr(&spec[1], ','); // Skip to next comma, if any
}
goto next;
}
// Find the next non-blank char after the region name's end
spec += regionNameLen + strspn(&spec[regionNameLen], " \t");
if (*spec != '\0' && *spec != ',' && *spec != '=') {
argError(
'S',
"Unexpected '%c' after region name \"%.*s\"",
*spec,
regionNameFmtLen,
regionName
);
// Skip to next ',' or '=' (or NUL) and keep parsing
spec += 1 + strcspn(&spec[1], ",=");
}
// Now, determine which region type this is
for (ScrambledRegion r : EnumSeq(SCRAMBLE_UNK)) {
// If the strings match (case-insensitively), we got it!
// `strncasecmp` must be used here since `regionName` points
// to the entire remaining argument.
if (!strncasecmp(scrambleSpecs[r].name, regionName, regionNameLen)) {
region = r;
break;
}
}
if (region == SCRAMBLE_UNK) {
argError('S', "Unknown region \"%.*s\"", regionNameFmtLen, regionName);
if (*spec != '=' && *spec != ',' && *spec != '\0') {
fatal("Unexpected character %s in spec for option 'S'", printChar(*spec));
}
char *regionSize = nullptr;
size_t regionSizeLen = 0;
// The '=' region size limit is optional.
if (*spec == '=') {
++spec; // `strtoul` will skip the whitespace on its own
unsigned long limit;
regionSize = spec + 1; // Skip the '='
// Skip leading whitespace before the region size.
regionSize += strspn(regionSize, " \t");
// The region size continues (skipping any whitespace) until a ',' (next region)
// or the end of the string.
regionSizeLen = strcspn(regionSize, ", \t");
// Skip trailing whitespace after the region size.
size_t regionSizeSkipLen = regionSizeLen + strspn(regionSize + regionSizeLen, " \t");
spec = regionSize + regionSizeSkipLen;
if (*spec != ',' && *spec != '\0') {
fatal("Unexpected character %s in spec for option 'S'", printChar(*spec));
}
}
// Skip trailing comma after the region.
if (*spec == ',') {
++spec;
}
// Skip trailing whitespace after the region.
// `spec` will be the next region name, or the end of the string.
spec += strspn(spec, " \t");
// Terminate the `regionName` and `regionSize` strings.
regionName[regionNameLen] = '\0';
if (regionSize) {
regionSize[regionSizeLen] = '\0';
}
// Check for an empty region name or limit.
// Note that by skipping leading whitespace before the loop, and skipping a trailing comma
// and whitespace before the next iteration, we guarantee that the region name will not be
// empty if it is present at all.
if (*regionName == '\0') {
fatal("Empty region name in spec for option 'S'");
}
if (regionSize && *regionSize == '\0') {
fatal("Empty region size limit in spec for option 'S'");
}
// Determine which region type this is.
auto search = scrambleSpecs.find(regionName);
if (search == scrambleSpecs.end()) {
fatal("Unknown region name \"%s\" in spec for option 'S'", regionName);
}
uint16_t limit = search->second.second;
if (regionSize) {
char *endptr;
unsigned long value = strtoul(regionSize, &endptr, 0);
if (*spec == '\0' || *spec == ',') {
argError('S', "Empty limit for region \"%.*s\"", regionNameFmtLen, regionName);
goto next;
if (*endptr != '\0') {
fatal("Invalid region size limit \"%s\" for option 'S'", regionSize);
}
limit = strtoul(spec, &endptr, 10);
endptr += strspn(endptr, " \t");
if (*endptr != '\0' && *endptr != ',') {
argError(
'S',
"Invalid non-numeric limit for region \"%.*s\"",
regionNameFmtLen,
regionName
if (value > limit) {
fatal(
"%s region size for option 'S' must be between 0 and %" PRIu16,
search->first.c_str(),
limit
);
endptr = strchr(endptr, ',');
}
spec = endptr;
if (region != SCRAMBLE_UNK && limit > scrambleSpecs[region].max) {
argError(
'S',
"Limit for region \"%.*s\" may not exceed %" PRIu16,
regionNameFmtLen,
regionName,
scrambleSpecs[region].max
);
limit = scrambleSpecs[region].max;
}
switch (region) {
case SCRAMBLE_ROMX:
options.scrambleROMX = limit;
break;
case SCRAMBLE_SRAM:
options.scrambleSRAM = limit;
break;
case SCRAMBLE_WRAMX:
options.scrambleWRAMX = limit;
break;
case SCRAMBLE_UNK: // The error has already been reported, do nothing
break;
}
} else if (region == SCRAMBLE_WRAMX) {
// Only WRAMX can be implied, since ROMX and SRAM size may vary
options.scrambleWRAMX = 7;
} else {
argError('S', "Cannot imply limit for region \"%.*s\"", regionNameFmtLen, regionName);
limit = value;
} else if (search->second.first != &options.scrambleWRAMX) {
// Only WRAMX limit can be implied, since ROMX and SRAM size may vary.
fatal("Missing %s region size limit for option 'S'", search->first.c_str());
}
next: // Can't `continue` a `for` loop with this nontrivial iteration logic
if (spec) {
assume(*spec == ',' || *spec == '\0');
if (*spec == ',') {
spec += 1 + strspn(&spec[1], " \t");
}
if (*spec == '\0') {
break;
}
if (*search->second.first != limit && *search->second.first != 0) {
warnx("Overriding %s region size limit for option 'S'", search->first.c_str());
}
// Update the scrambling region size limit.
*search->second.first = limit;
}
}
@@ -291,10 +259,13 @@ int main(int argc, char *argv[]) {
char *endptr;
unsigned long value = strtoul(musl_optarg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0' || value > 0xFF) {
argError('p', "Argument for 'p' must be a byte (between 0 and 0xFF)");
value = 0xFF;
if (musl_optarg[0] == '\0' || *endptr != '\0') {
fatal("Invalid argument for option 'p'");
}
if (value > 0xFF) {
fatal("Argument for option 'p' must be between 0 and 0xFF");
}
options.padValue = value;
options.hasPadValue = true;
break;