559 lines
20 KiB
C
559 lines
20 KiB
C
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
// See LICENSE.md for license information.
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// amigaCheck.c - the Amiga gates' checks of a screenshot and the probes'
|
|
// logs (scripts/verify-amiga-icon.sh and verify-amiga-palette.sh).
|
|
//
|
|
// amigaCheck icon WORK STACK WBTOOL LABEL
|
|
// amigaCheck palette PNG LOG MINRATE LABEL
|
|
//
|
|
// icon: WORK holds WBLAUNCH's log (wblaunch.txt), Prog/Palprobe.info, and
|
|
// the screenshot (screen.png). Every line of the log with FAIL in it fails;
|
|
// the log must say icon.library read the icon as a tool's (type WBTOOL) of
|
|
// STACK bytes of stack and the file's image size and depth; and the
|
|
// screenshot must show the icon. The screen is Workbench's gray and the
|
|
// icon's colored pixels lie inside it: the file's colored pixels (index
|
|
// not 0) and the screenshot's (not gray) must have the same bounds, scaled,
|
|
// and each of the file's pixels is looked at in the middle of its cell.
|
|
// Only the gray's upper half is searched: FS-UAE prints its notices over
|
|
// the bottom. Then Palprobe's log must be in Prog/ with its last line, and
|
|
// no log in WORK: it ran in its own folder.
|
|
//
|
|
// palette: LOG must have PALPROBE's two rates (palette and SCB changes
|
|
// presented a second; the last of each the log has), each at least
|
|
// MINRATE, and the color bands down the screenshot's middle column, top to
|
|
// bottom, black (the border) dropped, must be red, green, yellow, and red.
|
|
//
|
|
// Prints LABEL: PASS or LABEL: FAIL - the reasons, as the gate's verdict;
|
|
// exits 0 or 1, or 2 on a bad command line.
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "gateArg.h"
|
|
#include "gateImage.h"
|
|
#include "gateText.h"
|
|
#include "toolBuf.h"
|
|
#include "toolFile.h"
|
|
|
|
#define TOOL_NAME "amigaCheck"
|
|
#define FAIL_EXIT 1
|
|
#define PATH_BYTES 4096u
|
|
#define NAME_BYTES 32u
|
|
#define SEPARATOR "; "
|
|
#define LIST_SEPARATOR ", "
|
|
// A pattern: its text, '#' standing for a run of digits (a number that
|
|
// fits a long, so of NUMBER_DIGITS_MAX digits at most).
|
|
#define PATTERN_NUMBER '#'
|
|
#define PATTERN_VALUES_MAX 8u
|
|
#define NUMBER_DIGITS_MAX 19u
|
|
|
|
// The icon file: a 78-byte DiskObject and the struct Image's first 4
|
|
// bytes, then its width, height, and depth (big-endian words); its planes
|
|
// follow the 20-byte struct Image, rows of whole 16-bit words.
|
|
#define INFO_SIZE_AT 82u
|
|
#define INFO_PLANES_AT 98u
|
|
#define INFO_DEPTH_MAX 8
|
|
#define WORD_PIXELS 16
|
|
#define WORD_BYTES 2
|
|
#define BYTE_BITS 8
|
|
#define TOP_BIT 0x80u
|
|
#define AMIGA_COLORS 4u
|
|
#define GRAY_STEP 2
|
|
#define HALF 0.5
|
|
#define ICON_PATTERN "icon type=# stack=# gadget=#x# image=#x#x#"
|
|
#define ICON_TYPE 0u
|
|
#define ICON_STACK 1u
|
|
#define ICON_IMAGE_W 4u
|
|
#define ICON_IMAGE_H 5u
|
|
#define ICON_IMAGE_DEPTH 6u
|
|
#define PALPROBE_DONE "PALPROBE: done"
|
|
#define FAIL_TEXT "FAIL"
|
|
|
|
// The palette probe: its two rates (the third number of its line), and
|
|
// the bands it draws.
|
|
#define RATE_KINDS 2u
|
|
#define RATE_VALUE 2u
|
|
#define BAND_COUNT 4u
|
|
#define BAND_NAMES 3u
|
|
#define BAND_OTHER BAND_NAMES
|
|
|
|
// The reasons a gate fails, "; " between them.
|
|
typedef struct FailsT {
|
|
ToolBufT text;
|
|
uint32_t count;
|
|
} FailsT;
|
|
|
|
// A box round some pixels, and whether there are any.
|
|
typedef struct BoundsT {
|
|
long x0;
|
|
long x1;
|
|
long y0;
|
|
long y1;
|
|
bool any;
|
|
} BoundsT;
|
|
|
|
// A Workbench icon's image: its size and depth as the file gives them, and
|
|
// each pixel's color index (no pixels when the size is not positive).
|
|
typedef struct IconT {
|
|
long width;
|
|
long height;
|
|
long depth;
|
|
long cols;
|
|
long rows;
|
|
uint8_t *indices;
|
|
} IconT;
|
|
|
|
static long be16(const uint8_t *p);
|
|
static void boundsAdd(BoundsT *bounds, long x, long y);
|
|
static int checkIcon(char **args);
|
|
static int checkPalette(char **args);
|
|
static void failAdd(FailsT *fails, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
|
|
static void failLine(FailsT *fails, const uint8_t *line, size_t len);
|
|
static void failSeparate(FailsT *fails);
|
|
static bool fileExists(const char *path);
|
|
static bool findPattern(const uint8_t *text, size_t len, size_t *at, const char *pattern, long *values);
|
|
static void iconRead(const char *path, IconT *icon);
|
|
static void iconShown(const char *path, const IconT *icon, const char *label, FailsT *fails);
|
|
static bool matchAt(const uint8_t *text, size_t len, size_t at, const char *pattern, long *values);
|
|
static int printVerdict(const FailsT *fails, const char *label);
|
|
static uint8_t *readText(const char *path, size_t *len);
|
|
|
|
static const GateModeT kModes[] = {
|
|
{ "icon", 4, checkIcon },
|
|
{ "palette", 4, checkPalette }
|
|
};
|
|
|
|
// Workbench's gray, black, white, and blue.
|
|
static const uint8_t kWorkbench[AMIGA_COLORS][GATE_RGB_BYTES] = { { 170, 170, 170 }, { 0, 0, 0 }, { 255, 255, 255 }, { 102, 136, 187 } };
|
|
|
|
static const char *const kRateKinds[RATE_KINDS] = { "palette", "scb" };
|
|
static const char *const kRatePatterns[RATE_KINDS] = { "PALPROBE: palette+present: # in # ms = #/s", "PALPROBE: scb+present: # in # ms = #/s" };
|
|
|
|
static const uint8_t kBandColors[BAND_NAMES][GATE_RGB_BYTES] = { { 255, 0, 0 }, { 0, 255, 0 }, { 255, 255, 0 } };
|
|
static const char *const kBandNames[BAND_NAMES] = { "red", "green", "yellow" };
|
|
static const uint32_t kWantBands[BAND_COUNT] = { 0u, 1u, 2u, 0u };
|
|
static const uint8_t kBlack[GATE_RGB_BYTES] = { 0, 0, 0 };
|
|
|
|
static const char *const kUsage[] = {
|
|
"usage:",
|
|
" amigaCheck icon WORK STACK WBTOOL LABEL WBLAUNCH read and drew the",
|
|
" icon; Palprobe ran in Prog/",
|
|
" amigaCheck palette PNG LOG MINRATE LABEL PALPROBE's rates and bands"
|
|
};
|
|
|
|
|
|
// A signed big-endian 16-bit number.
|
|
static long be16(const uint8_t *p) {
|
|
return (int16_t)(((uint32_t)p[0] << BYTE_BITS) | p[1]);
|
|
}
|
|
|
|
|
|
static void boundsAdd(BoundsT *bounds, long x, long y) {
|
|
bounds->x0 = (!bounds->any || x < bounds->x0) ? x : bounds->x0;
|
|
bounds->x1 = (!bounds->any || x > bounds->x1) ? x : bounds->x1;
|
|
bounds->y0 = (!bounds->any || y < bounds->y0) ? y : bounds->y0;
|
|
bounds->y1 = (!bounds->any || y > bounds->y1) ? y : bounds->y1;
|
|
bounds->any = true;
|
|
}
|
|
|
|
|
|
static int checkIcon(char **args) {
|
|
FailsT fails;
|
|
IconT icon;
|
|
char path[PATH_BYTES];
|
|
uint8_t *log;
|
|
uint8_t *palLog;
|
|
long got[PATTERN_VALUES_MAX];
|
|
size_t logLen;
|
|
size_t palLogLen;
|
|
size_t at;
|
|
size_t end;
|
|
size_t lineLen;
|
|
long wantStack;
|
|
long wbTool;
|
|
|
|
memset(&fails, 0, sizeof(fails));
|
|
wantStack = gateArgLong("stack", args[1]);
|
|
wbTool = gateArgLong("tool type", args[2]);
|
|
snprintf(path, sizeof(path), "%s/wblaunch.txt", args[0]);
|
|
log = readText(path, &logLen);
|
|
// Each line (a \r before its \n dropped) with FAIL in it.
|
|
for (at = 0; at < logLen; at = end + 1u) {
|
|
end = gateTextFind(log, logLen, at, "\n", 1u);
|
|
end = (end == GATE_TEXT_NOT_FOUND) ? logLen : end;
|
|
lineLen = (end > at && log[end - 1u] == '\r') ? end - at - 1u : end - at;
|
|
if (gateTextFind(log + at, lineLen, 0u, FAIL_TEXT, strlen(FAIL_TEXT)) != GATE_TEXT_NOT_FOUND) {
|
|
failLine(&fails, log + at, lineLen);
|
|
}
|
|
}
|
|
|
|
snprintf(path, sizeof(path), "%s/Prog/Palprobe.info", args[0]);
|
|
iconRead(path, &icon);
|
|
at = 0u;
|
|
if (!findPattern(log, logLen, &at, ICON_PATTERN, got)) {
|
|
failAdd(&fails, "icon.library never read the icon");
|
|
} else {
|
|
if (got[ICON_TYPE] != wbTool) {
|
|
failAdd(&fails, "icon type %ld, want WBTOOL (%ld)", got[ICON_TYPE], wbTool);
|
|
}
|
|
if (got[ICON_STACK] != wantStack) {
|
|
failAdd(&fails, "icon stack %ld, want %ld", got[ICON_STACK], wantStack);
|
|
}
|
|
if (got[ICON_IMAGE_W] != icon.width || got[ICON_IMAGE_H] != icon.height || got[ICON_IMAGE_DEPTH] != icon.depth) {
|
|
failAdd(&fails, "icon.library's image %ldx%ldx%ld, the file's %ldx%ldx%ld", got[ICON_IMAGE_W], got[ICON_IMAGE_H], got[ICON_IMAGE_DEPTH], icon.width, icon.height, icon.depth);
|
|
}
|
|
}
|
|
snprintf(path, sizeof(path), "%s/screen.png", args[0]);
|
|
iconShown(path, &icon, args[3], &fails);
|
|
|
|
snprintf(path, sizeof(path), "%s/Prog/joeylog.txt", args[0]);
|
|
palLog = fileExists(path) ? readText(path, &palLogLen) : NULL;
|
|
if (palLog == NULL || gateTextFind(palLog, palLogLen, 0u, PALPROBE_DONE, strlen(PALPROBE_DONE)) == GATE_TEXT_NOT_FOUND) {
|
|
failAdd(&fails, "Palprobe's log is not in Prog/ with its last line: it did not run in its own folder");
|
|
}
|
|
snprintf(path, sizeof(path), "%s/joeylog.txt", args[0]);
|
|
if (fileExists(path)) {
|
|
failAdd(&fails, "a joeylog.txt is in the drive's root: Palprobe ran outside its folder");
|
|
}
|
|
if (fails.count != 0u) {
|
|
return printVerdict(&fails, args[3]);
|
|
}
|
|
printf("%s: PASS (icon.library reads a tool's icon, %ld bytes of stack, %ldx%ldx%ld, and draws every pixel of it; started from it as Workbench does, Palprobe ran in its own folder and its message came back)\n", args[3], wantStack, icon.width, icon.height, icon.depth);
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int checkPalette(char **args) {
|
|
GateImageT img;
|
|
FailsT fails;
|
|
ToolBufT bands;
|
|
uint8_t *log;
|
|
const uint8_t *rgb;
|
|
long values[PATTERN_VALUES_MAX];
|
|
long rates[RATE_KINDS];
|
|
bool seen[RATE_KINDS];
|
|
uint32_t bandIds[BAND_COUNT];
|
|
uint32_t bandCount;
|
|
uint32_t band;
|
|
uint32_t k;
|
|
uint32_t y;
|
|
size_t logLen;
|
|
size_t at;
|
|
long minRate;
|
|
bool matched;
|
|
char name[NAME_BYTES];
|
|
char last[NAME_BYTES];
|
|
|
|
memset(&fails, 0, sizeof(fails));
|
|
memset(&bands, 0, sizeof(bands));
|
|
minRate = gateArgLong("minimum rate", args[2]);
|
|
log = readText(args[1], &logLen);
|
|
// Each kind's rate: the last the log has.
|
|
for (k = 0; k < RATE_KINDS; k++) {
|
|
seen[k] = false;
|
|
rates[k] = 0;
|
|
for (at = 0u; findPattern(log, logLen, &at, kRatePatterns[k], values); at++) {
|
|
seen[k] = true;
|
|
rates[k] = values[RATE_VALUE];
|
|
}
|
|
}
|
|
if (!seen[0] || !seen[1]) {
|
|
printf("%s: FAIL - the log does not have both rates (", args[3]);
|
|
for (k = 0; k < RATE_KINDS; k++) {
|
|
printf("%s%s ", (k == 0u) ? "" : LIST_SEPARATOR, kRateKinds[k]);
|
|
if (seen[k]) {
|
|
printf("%ld/s", rates[k]);
|
|
} else {
|
|
printf("missing");
|
|
}
|
|
}
|
|
printf(")\n");
|
|
return FAIL_EXIT;
|
|
}
|
|
|
|
// The bands down the middle column, top to bottom, black (the border)
|
|
// dropped: each run of one color is a band.
|
|
gateImageLoad(args[0], &img);
|
|
bandCount = 0u;
|
|
for (y = 0; y < img.height; y++) {
|
|
rgb = gateImagePixel(&img, img.width / 2u, y);
|
|
if (memcmp(rgb, kBlack, GATE_RGB_BYTES) == 0) {
|
|
continue;
|
|
}
|
|
band = BAND_OTHER;
|
|
for (k = 0; k < BAND_NAMES && band == BAND_OTHER; k++) {
|
|
band = (memcmp(rgb, kBandColors[k], GATE_RGB_BYTES) == 0) ? k : BAND_OTHER;
|
|
}
|
|
if (band != BAND_OTHER) {
|
|
snprintf(name, sizeof(name), "%s", kBandNames[band]);
|
|
} else {
|
|
snprintf(name, sizeof(name), "other (%u, %u, %u)", (unsigned)rgb[0], (unsigned)rgb[1], (unsigned)rgb[2]);
|
|
}
|
|
if (bandCount > 0u && strcmp(name, last) == 0) {
|
|
continue;
|
|
}
|
|
gateTextAdd(&bands, "%s%s", (bandCount == 0u) ? "" : LIST_SEPARATOR, name);
|
|
if (bandCount < BAND_COUNT) {
|
|
bandIds[bandCount] = band;
|
|
}
|
|
bandCount++;
|
|
memcpy(last, name, sizeof(last));
|
|
}
|
|
for (k = 0; k < RATE_KINDS; k++) {
|
|
if (rates[k] < minRate) {
|
|
failAdd(&fails, "%s changes presented %ld/s, want at least %ld", kRateKinds[k], rates[k], minRate);
|
|
}
|
|
}
|
|
matched = bandCount == BAND_COUNT;
|
|
for (k = 0; k < BAND_COUNT && matched; k++) {
|
|
matched = bandIds[k] == kWantBands[k];
|
|
}
|
|
if (!matched) {
|
|
failAdd(&fails, "bands %s, want red, green, yellow, red", (bandCount == 0u) ? "none" : gateTextString(&bands));
|
|
}
|
|
if (fails.count != 0u) {
|
|
return printVerdict(&fails, args[3]);
|
|
}
|
|
printf("%s: PASS (palette changes %ld/s, SCB changes %ld/s; bands red, green, yellow, red after a palette change and a same-layout SCB change)\n", args[3], rates[0], rates[1]);
|
|
return 0;
|
|
}
|
|
|
|
|
|
static void failAdd(FailsT *fails, const char *fmt, ...) {
|
|
va_list args;
|
|
|
|
failSeparate(fails);
|
|
va_start(args, fmt);
|
|
gateTextAddV(&fails->text, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
|
|
// A line of a log as a reason, its bytes as they are.
|
|
static void failLine(FailsT *fails, const uint8_t *line, size_t len) {
|
|
failSeparate(fails);
|
|
toolBufAdd(&fails->text, line, len);
|
|
}
|
|
|
|
|
|
// "; " before every reason but the first.
|
|
static void failSeparate(FailsT *fails) {
|
|
if (fails->count > 0u) {
|
|
toolBufAdd(&fails->text, SEPARATOR, strlen(SEPARATOR));
|
|
}
|
|
fails->count++;
|
|
}
|
|
|
|
|
|
static bool fileExists(const char *path) {
|
|
FILE *f;
|
|
|
|
f = fopen(path, "rb");
|
|
if (f == NULL) {
|
|
return false;
|
|
}
|
|
fclose(f);
|
|
return true;
|
|
}
|
|
|
|
|
|
// The first place at or after *at that the pattern matches: there, in *at,
|
|
// and its numbers in values.
|
|
static bool findPattern(const uint8_t *text, size_t len, size_t *at, const char *pattern, long *values) {
|
|
for (; *at < len; (*at)++) {
|
|
if (matchAt(text, len, *at, pattern, values)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// The image of the icon file at path: its size and depth, and its pixels'
|
|
// color indices, a bit of each from each plane.
|
|
static void iconRead(const char *path, IconT *icon) {
|
|
uint8_t *info;
|
|
size_t infoLen;
|
|
size_t byteAt;
|
|
long rowBytes;
|
|
long plane;
|
|
long x;
|
|
long y;
|
|
|
|
info = readText(path, &infoLen);
|
|
if (infoLen < INFO_PLANES_AT) {
|
|
toolFail("%s is too short for an icon", path);
|
|
}
|
|
icon->width = be16(info + INFO_SIZE_AT);
|
|
icon->height = be16(info + INFO_SIZE_AT + WORD_BYTES);
|
|
icon->depth = be16(info + INFO_SIZE_AT + 2u * WORD_BYTES);
|
|
if (icon->depth > INFO_DEPTH_MAX) {
|
|
toolFail("%s has %ld planes", path, icon->depth);
|
|
}
|
|
icon->cols = (icon->width > 0 && icon->height > 0) ? icon->width : 0;
|
|
icon->rows = (icon->cols > 0) ? icon->height : 0;
|
|
icon->indices = (uint8_t *)calloc((size_t)(icon->cols * icon->rows) + 1u, 1u);
|
|
if (icon->indices == NULL) {
|
|
toolFail("out of memory");
|
|
}
|
|
rowBytes = (icon->width + WORD_PIXELS - 1) / WORD_PIXELS * WORD_BYTES;
|
|
for (plane = 0; plane < icon->depth; plane++) {
|
|
for (y = 0; y < icon->rows; y++) {
|
|
for (x = 0; x < icon->cols; x++) {
|
|
byteAt = INFO_PLANES_AT + (size_t)(plane * rowBytes * icon->rows + y * rowBytes + x / BYTE_BITS);
|
|
if (byteAt >= infoLen) {
|
|
toolFail("%s's planes are truncated", path);
|
|
}
|
|
if (info[byteAt] & (TOP_BIT >> (x % BYTE_BITS))) {
|
|
icon->indices[y * icon->cols + x] |= (uint8_t)(1u << plane);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
free(info);
|
|
}
|
|
|
|
|
|
// Whether the screenshot at path shows the icon: the bounds of the gray
|
|
// (looked for at every other pixel), the non-gray inside its upper half,
|
|
// and each of the icon's pixels in the middle of its cell there. Prints
|
|
// where the icon is and how many of its pixels are right; adds what is
|
|
// wrong to fails.
|
|
static void iconShown(const char *path, const IconT *icon, const char *label, FailsT *fails) {
|
|
GateImageT shot;
|
|
BoundsT gray;
|
|
BoundsT shown;
|
|
BoundsT held;
|
|
const uint8_t *pixel;
|
|
long x;
|
|
long y;
|
|
long cells;
|
|
long wrong;
|
|
uint8_t index;
|
|
double scaleX;
|
|
double scaleY;
|
|
|
|
gateImageLoad(path, &shot);
|
|
memset(&gray, 0, sizeof(gray));
|
|
for (y = 0; y < (long)shot.height; y += GRAY_STEP) {
|
|
for (x = 0; x < (long)shot.width; x += GRAY_STEP) {
|
|
if (memcmp(gateImagePixel(&shot, (uint32_t)x, (uint32_t)y), kWorkbench[0], GATE_RGB_BYTES) == 0) {
|
|
boundsAdd(&gray, x, y);
|
|
}
|
|
}
|
|
}
|
|
if (!gray.any) {
|
|
failAdd(fails, "no Workbench gray on the screenshot");
|
|
return;
|
|
}
|
|
memset(&shown, 0, sizeof(shown));
|
|
for (y = gray.y0; y <= (gray.y0 + gray.y1) / 2; y++) {
|
|
for (x = gray.x0; x <= gray.x1; x++) {
|
|
if (memcmp(gateImagePixel(&shot, (uint32_t)x, (uint32_t)y), kWorkbench[0], GATE_RGB_BYTES) != 0) {
|
|
boundsAdd(&shown, x, y);
|
|
}
|
|
}
|
|
}
|
|
memset(&held, 0, sizeof(held));
|
|
for (y = 0; y < icon->rows; y++) {
|
|
for (x = 0; x < icon->cols; x++) {
|
|
if (icon->indices[y * icon->cols + x] != 0u) {
|
|
boundsAdd(&held, x, y);
|
|
}
|
|
}
|
|
}
|
|
if (!shown.any || !held.any) {
|
|
failAdd(fails, held.any ? "no icon on the screenshot" : "the icon file is empty");
|
|
return;
|
|
}
|
|
scaleX = (double)(shown.x1 + 1 - shown.x0) / (double)(held.x1 + 1 - held.x0);
|
|
scaleY = (double)(shown.y1 + 1 - shown.y0) / (double)(held.y1 + 1 - held.y0);
|
|
wrong = 0;
|
|
for (y = held.y0; y <= held.y1; y++) {
|
|
for (x = held.x0; x <= held.x1; x++) {
|
|
index = icon->indices[y * icon->cols + x];
|
|
if (index >= AMIGA_COLORS) {
|
|
toolFail("the icon's pixel %ld,%ld is color %u, past Workbench's four", x, y, (unsigned)index);
|
|
}
|
|
pixel = gateImagePixel(&shot, (uint32_t)(shown.x0 + (x - held.x0 + HALF) * scaleX), (uint32_t)(shown.y0 + (y - held.y0 + HALF) * scaleY));
|
|
if (memcmp(pixel, kWorkbench[index], GATE_RGB_BYTES) != 0) {
|
|
wrong++;
|
|
}
|
|
}
|
|
}
|
|
cells = (held.x1 + 1 - held.x0) * (held.y1 + 1 - held.y0);
|
|
printf("%s: icon shown at %ld,%ld scaled %.2fx%.2f: %ld/%ld pixels right\n", label, shown.x0, shown.y0, scaleX, scaleY, cells - wrong, cells);
|
|
if (wrong != 0) {
|
|
failAdd(fails, "%ld of the icon's %ld pixels wrong on the screen", wrong, cells);
|
|
}
|
|
gateImageFree(&shot);
|
|
}
|
|
|
|
|
|
// Whether the pattern matches text at at: its characters as they are, a
|
|
// PATTERN_NUMBER a run of digits that fits a long, each number in values.
|
|
static bool matchAt(const uint8_t *text, size_t len, size_t at, const char *pattern, long *values) {
|
|
char digits[NUMBER_DIGITS_MAX + 1u];
|
|
uint32_t count;
|
|
size_t n;
|
|
|
|
count = 0u;
|
|
for (; *pattern != '\0'; pattern++) {
|
|
if (*pattern != PATTERN_NUMBER) {
|
|
if (at >= len || text[at] != (uint8_t)*pattern) {
|
|
return false;
|
|
}
|
|
at++;
|
|
continue;
|
|
}
|
|
n = 0u;
|
|
while (at + n < len && text[at + n] >= '0' && text[at + n] <= '9') {
|
|
n++;
|
|
}
|
|
if (n == 0u || n > NUMBER_DIGITS_MAX) {
|
|
return false;
|
|
}
|
|
memcpy(digits, text + at, n);
|
|
digits[n] = '\0';
|
|
if (!gateArgParseLong(digits, &values[count])) {
|
|
return false;
|
|
}
|
|
count++;
|
|
at += n;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
static int printVerdict(const FailsT *fails, const char *label) {
|
|
printf("%s: FAIL - ", label);
|
|
fwrite(fails->text.data, 1u, fails->text.len, stdout);
|
|
printf("\n");
|
|
return FAIL_EXIT;
|
|
}
|
|
|
|
|
|
// A whole file; one that cannot be read is fatal.
|
|
static uint8_t *readText(const char *path, size_t *len) {
|
|
uint8_t *data;
|
|
|
|
if (!toolReadFile(path, &data, len)) {
|
|
toolFail("cannot read %s", path);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
int exitCode;
|
|
|
|
toolFileName(TOOL_NAME);
|
|
if (!gateModeRun(kModes, GATE_COUNT_OF(kModes), argc, argv, &exitCode)) {
|
|
return gateUsage(kUsage, GATE_COUNT_OF(kUsage));
|
|
}
|
|
return exitCode;
|
|
}
|