diff --git a/include/asm/warning.h b/include/asm/warning.h index bae72e23..9df442cb 100644 --- a/include/asm/warning.h +++ b/include/asm/warning.h @@ -20,6 +20,7 @@ enum WarningID { WARNING_DIV, /* Division undefined behavior */ WARNING_EMPTY_DATA_DIRECTIVE, /* `db`, `dw` or `dl` with no directive in ROM */ WARNING_EMPTY_ENTRY, /* Empty entry in `db`, `dw` or `dl` */ + WARNING_EMPTY_STRRPL, /* Empty second argument in `STRRPL` */ WARNING_LARGE_CONSTANT, /* Constants too large */ WARNING_LONG_STR, /* String too long for internal buffers */ WARNING_NESTED_COMMENT, /* Comment-start delimiter in a block comment */ diff --git a/src/asm/charmap.c b/src/asm/charmap.c index 4fca8b07..ee8f8336 100644 --- a/src/asm/charmap.c +++ b/src/asm/charmap.c @@ -230,7 +230,7 @@ size_t charmap_Convert(char const *input, uint8_t *output) size_t codepointLen = readUTF8Char(output, input); if (codepointLen == 0) { - error("Input string is not valid UTF-8!"); + error("Input string is not valid UTF-8!\n"); break; } input += codepointLen; /* OK because UTF-8 has no NUL in multi-byte chars */ diff --git a/src/asm/lexer.c b/src/asm/lexer.c index 0df33fce..7743f90d 100644 --- a/src/asm/lexer.c +++ b/src/asm/lexer.c @@ -204,6 +204,7 @@ static struct KeywordMapping { {"STRCAT", T_OP_STRCAT}, {"STRUPR", T_OP_STRUPR}, {"STRLWR", T_OP_STRLWR}, + {"STRRPL", T_OP_STRRPL}, {"STRFMT", T_OP_STRFMT}, {"INCLUDE", T_POP_INCLUDE}, @@ -493,7 +494,7 @@ struct KeywordDictNode { uint16_t children[0x60 - ' ']; struct KeywordMapping const *keyword; /* Since the keyword structure is invariant, the min number of nodes is known at compile time */ -} keywordDict[353] = {0}; /* Make sure to keep this correct when adding keywords! */ +} keywordDict[355] = {0}; /* Make sure to keep this correct when adding keywords! */ /* Convert a char into its index into the dict */ static inline uint8_t dictIndex(char c) diff --git a/src/asm/parser.y b/src/asm/parser.y index 6560fd12..9c6a66b9 100644 --- a/src/asm/parser.y +++ b/src/asm/parser.y @@ -164,7 +164,46 @@ static void strsubUTF8(char *dest, const char *src, uint32_t pos, uint32_t len) dest[destIndex] = 0; } -static void initStrFmtArgList(struct StrFmtArgList *args) { +static void strrpl(char *dest, size_t destLen, char const *src, char const *old, char const *new) +{ + size_t oldLen = strlen(old); + size_t newLen = strlen(new); + size_t i = 0; + + if (!oldLen) { + warning(WARNING_EMPTY_STRRPL, "STRRPL: Cannot replace an empty string\n"); + strcpy(dest, src); + return; + } + + for (char const *next = strstr(src, old); next && *next; next = strstr(src, old)) { + memcpy(dest + i, src, next - src < destLen - i ? next - src : destLen - i); + i += next - src; + if (i >= destLen) + break; + + memcpy(dest + i, new, newLen < destLen - i ? newLen : destLen - i); + i += newLen; + if (i >= destLen) + break; + + src = next + oldLen; + } + + size_t srcLen = strlen(src); + + memcpy(dest + i, src, srcLen < destLen - i ? srcLen : destLen - i); + i += srcLen; + + if (i >= destLen) { + warning(WARNING_LONG_STR, "STRRPL: String too long, got truncated\n"); + i = destLen - 1; + } + dest[i] = '\0'; +} + +static void initStrFmtArgList(struct StrFmtArgList *args) +{ args->nbArgs = 0; args->capacity = INITIAL_STRFMT_ARG_SIZE; args->args = malloc(args->capacity * sizeof(*args->args)); @@ -173,7 +212,8 @@ static void initStrFmtArgList(struct StrFmtArgList *args) { strerror(errno)); } -static size_t nextStrFmtArgListIndex(struct StrFmtArgList *args) { +static size_t nextStrFmtArgListIndex(struct StrFmtArgList *args) +{ if (args->nbArgs == args->capacity) { args->capacity = (args->capacity + 1) * 2; args->args = realloc(args->args, args->capacity * sizeof(*args->args)); @@ -184,7 +224,8 @@ static size_t nextStrFmtArgListIndex(struct StrFmtArgList *args) { return args->nbArgs++; } -static void freeStrFmtArgList(struct StrFmtArgList *args) { +static void freeStrFmtArgList(struct StrFmtArgList *args) +{ free(args->format); for (size_t i = 0; i < args->nbArgs; i++) if (!args->args[i].isNumeric) @@ -192,11 +233,12 @@ static void freeStrFmtArgList(struct StrFmtArgList *args) { free(args->args); } -static void strfmt(char *dest, size_t destLen, char const *fmt, size_t nbArgs, struct StrFmtArg *args) { +static void strfmt(char *dest, size_t destLen, char const *fmt, size_t nbArgs, struct StrFmtArg *args) +{ size_t a = 0; - size_t i; + size_t i = 0; - for (i = 0; i < destLen;) { + while (i < destLen) { int c = *fmt++; if (c == '\0') { @@ -254,14 +296,14 @@ static void strfmt(char *dest, size_t destLen, char const *fmt, size_t nbArgs, s i += snprintf(&dest[i], destLen - i, "%s", buf); } + if (a < nbArgs) + error("STRFMT: %zu unformatted argument(s)\n", nbArgs - a); + if (i > destLen - 1) { warning(WARNING_LONG_STR, "STRFMT: String too long, got truncated\n"); i = destLen - 1; } dest[i] = '\0'; - - if (a < nbArgs) - error("STRFMT: %zu unformatted argument(s)\n", nbArgs - a); } static inline void failAssert(enum AssertionType type) @@ -387,6 +429,7 @@ static inline void failAssertMsg(enum AssertionType type, char const *msg) %left T_OP_STRCAT %left T_OP_STRUPR %left T_OP_STRLWR +%left T_OP_STRRPL %left T_OP_STRFMT %token T_LABEL @@ -1293,15 +1336,18 @@ string : T_STRING | T_OP_STRLWR T_LPAREN string T_RPAREN { lowerstring($$, $3); } + | T_OP_STRRPL T_LPAREN string T_COMMA string T_COMMA string T_RPAREN { + strrpl($$, sizeof($$), $3, $5, $7); + } | T_OP_STRFMT T_LPAREN strfmt_args T_RPAREN { - strfmt($$, MAXSTRLEN + 1, $3.format, $3.nbArgs, $3.args); + strfmt($$, sizeof($$), $3.format, $3.nbArgs, $3.args); freeStrFmtArgList(&$3); } ; strcat_args : string | strcat_args T_COMMA string { - if (snprintf($$, MAXSTRLEN + 1, "%s%s", $1, $3) > MAXSTRLEN) + if (snprintf($$, sizeof($$), "%s%s", $1, $3) > MAXSTRLEN) warning(WARNING_LONG_STR, "STRCAT: String too long '%s%s'\n", $1, $3); } diff --git a/src/asm/rgbasm.1 b/src/asm/rgbasm.1 index cd05c970..81946421 100644 --- a/src/asm/rgbasm.1 +++ b/src/asm/rgbasm.1 @@ -215,6 +215,12 @@ Warn when an empty entry is encountered in a list. This warning is enabled by .Fl Wextra . +.It Fl Wempty-strrpl +Warn when +.Fn STRRPL +is called with an empty string as its second argument (the substring to replace). +This warning is enabled by +.Fl Wall . .It Fl Wlarge-constant Warn when a constant too large to fit in a signed 32-bit integer is encountered. This warning is enabled by diff --git a/src/asm/rgbasm.5 b/src/asm/rgbasm.5 index ddee78d1..f805e96b 100644 --- a/src/asm/rgbasm.5 +++ b/src/asm/rgbasm.5 @@ -374,6 +374,7 @@ Most of them return a string, however some of these functions actually return an .It Fn STRSUB str pos len Ta Returns a substring from Ar str No starting at Ar pos Po first character is position 1 Pc and Ar len No characters long. .It Fn STRUPR str Ta Returns Ar str No with all letters in uppercase. .It Fn STRLWR str Ta Returns Ar str No with all letters in lowercase. +.It Fn STRRPL str old new Ta Returns Ar str No with each non-overlapping occurrence of the substring Ar old No replaced with Ar new . .It Fn STRFMT fmt args... Ta Returns the string Ar fmt No with each .Ql %spec pattern replaced by interpolating the format diff --git a/src/asm/warning.c b/src/asm/warning.c index b764fa6e..dcc166be 100644 --- a/src/asm/warning.c +++ b/src/asm/warning.c @@ -34,6 +34,7 @@ static enum WarningState const defaultWarnings[NB_WARNINGS] = { [WARNING_DIV] = WARNING_DISABLED, [WARNING_EMPTY_DATA_DIRECTIVE] = WARNING_DISABLED, [WARNING_EMPTY_ENTRY] = WARNING_DISABLED, + [WARNING_EMPTY_STRRPL] = WARNING_DISABLED, [WARNING_LARGE_CONSTANT] = WARNING_DISABLED, [WARNING_LONG_STR] = WARNING_DISABLED, [WARNING_NESTED_COMMENT] = WARNING_ENABLED, @@ -74,6 +75,7 @@ static char const *warningFlags[NB_WARNINGS_ALL] = { "div", "empty-data-directive", "empty-entry", + "empty-strrpl", "large-constant", "long-string", "nested-comment", @@ -98,6 +100,7 @@ static uint8_t const _wallCommands[] = { WARNING_BUILTIN_ARG, WARNING_CHARMAP_REDEF, WARNING_EMPTY_DATA_DIRECTIVE, + WARNING_EMPTY_STRRPL, WARNING_LARGE_CONSTANT, WARNING_LONG_STR, META_WARNING_DONE @@ -116,6 +119,7 @@ static uint8_t const _weverythingCommands[] = { WARNING_DIV, WARNING_EMPTY_DATA_DIRECTIVE, WARNING_EMPTY_ENTRY, + WARNING_EMPTY_STRRPL, WARNING_LARGE_CONSTANT, WARNING_LONG_STR, WARNING_NESTED_COMMENT, diff --git a/test/asm/strrpl.asm b/test/asm/strrpl.asm new file mode 100644 index 00000000..1f2a5f89 --- /dev/null +++ b/test/asm/strrpl.asm @@ -0,0 +1,6 @@ + println strrpl("\tld [hli], a", "[hli]", "[hl+]") + println strrpl("lolololol", "lol", "hah") + println strrpl("h e ll o", " ", "") + println strrpl("world", "", "x") + println strrpl("", "a", "b") + println strrpl("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "a", "[bbbbbbbb]") diff --git a/test/asm/strrpl.err b/test/asm/strrpl.err new file mode 100644 index 00000000..e893a298 --- /dev/null +++ b/test/asm/strrpl.err @@ -0,0 +1,4 @@ +warning: strrpl.asm(4): [-Wempty-strrpl] + STRRPL: Cannot replace an empty string +warning: strrpl.asm(6): [-Wlong-string] + STRRPL: String too long, got truncated diff --git a/test/asm/strrpl.out b/test/asm/strrpl.out new file mode 100644 index 00000000..5779fc82 --- /dev/null +++ b/test/asm/strrpl.out @@ -0,0 +1,6 @@ + ld [hl+], a +hahohahol +hello +world + +[bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbbbbbb][bbbb