diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index fc901c4..651d849 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -26,10 +26,24 @@ jobs: # skipped for the same reason: our CMakeLists declares akerror::akerror # first and libakstdlib guards on if(NOT TARGET ...). submodules: true + # moreutils is load-bearing, not incidental: it supplies errno(1), which + # deps/libakerror/scripts/generrno.sh shells out to at build time to + # generate its errno name table and to stamp AKERR_LAST_ERRNO_VALUE into + # the generated akerror.h. + # + # Its absence does not fail the build, which is what made this expensive to + # find. The script prints "errno: command not found" twice, emits an empty + # akerr_init_errno() so no errno ever gets a name, and substitutes an empty + # AKERR_LAST_ERRNO_VALUE -- so every code in libakerror's reserved band + # collapses from (134 + n) to ( + n), i.e. to n. AKERR_OUTOFBOUNDS is + # (AKERR_LAST_ERRNO_VALUE + 2), so it becomes literally 2, and ERR(2) + # answers "Out Of Bounds Error" where ENOENT's "No such file or directory" + # belongs. That is the docs_examples failure at docs/15-error-codes.md:105 + # that this job carried on every run since #2. - name: dependencies run: | sudo apt-get update -y - sudo apt-get install -y cmake gcc + sudo apt-get install -y cmake gcc moreutils - name: build run: | cmake -S . -B build @@ -74,7 +88,7 @@ jobs: - name: dependencies run: | sudo apt-get update -y - sudo apt-get install -y cmake gcc + sudo apt-get install -y cmake gcc moreutils # The whole suite under ASan and UBSan, golden files included. This is the # gate libakstdlib's TODO.md section 1 calls its highest-value missing item # -- worth having here from the start, because this library is all fixed @@ -98,7 +112,7 @@ jobs: - name: dependencies run: | sudo apt-get update -y - sudo apt-get install -y cmake gcc gcovr + sudo apt-get install -y cmake gcc gcovr moreutils # The gate is a ratchet, not a target: src/ sits at 94.6% of lines and # 98.6% of functions, so 90 fails on a real regression (a test deleted, or # new untested code added) without tripping over rounding. @@ -134,9 +148,21 @@ jobs: --fail-under-line 90 # Publish even when the threshold gate fails, so the uncovered lines are # visible -- each one is a missing test. + # + # @v3, not @v4, and that is Gitea rather than preference. @v4 bundles + # @actions/artifact v2, whose isGhes() treats any GITHUB_SERVER_URL that is + # not github.com as GitHub Enterprise Server and refuses outright: + # "GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+ + # and download-artifact@v4+ are not currently supported on GHES". @v3 uses + # the older artifact API, which this forge does implement. + # + # This never showed before because the step had nothing to upload: gcovr + # was never reached, so the step warned "No files were found" and passed. + # Fixing the suite is what first gave it a real file to refuse. Same class + # of accommodation as the annotate_only flag on the junit reporter above. - name: upload coverage reports if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: code-coverage path: build-coverage/coverage/ @@ -161,22 +187,56 @@ jobs: # embedded it takes a find_package path instead, so our CMakeLists declares # those targets first and needs the submodules present. Six of them, none # recursive. Filed upstream as libakgl API-gap item 5. + # + # Eight, not six: deps/libccd and deps/clay were missing and are not + # optional. libakgl does not add_subdirectory either of them -- their own + # CMakeLists are unusable as subprojects -- it compiles them into itself, + # so nothing declares them and configuration dies late with "File + # deps/libccd/src/ccd/config.h.cmake.in does not exist" at + # deps/libakgl/CMakeLists.txt:282. clay is the same shape one step later: + # deps/clay/clay.h is on the include path and installed. + # + # deps/tg is a real submodule of libakgl and is deliberately not here -- + # nothing in its CMakeLists references it. libakerror and libakstdlib are + # skipped for the reason the checkout note above gives. - name: libakgl dependencies run: | git -C deps/libakgl submodule update --init \ deps/SDL deps/SDL_image deps/SDL_mixer deps/SDL_ttf \ - deps/jansson deps/semver + deps/jansson deps/semver deps/libccd deps/clay # libfreetype-dev and libharfbuzz-dev are load-bearing, not incidental. # SDL_ttf prefers the system copies -- it reports "Using system freetype # library" and links libfreetype.so.6 -- and without them it would reach # for deps/SDL_ttf/external/freetype, which the checkout above # deliberately does not clone. Installing two dev packages is much cheaper # than cloning freetype and harfbuzz. + # + # The X11 dev packages are the other half, and they are what this job has + # been dying on since run #2 -- it has never once been green. SDL_X11 and + # its nine sub-options default ON on Linux, and CheckX11() in + # deps/SDL/cmake/sdlchecks.cmake calls SDL_missing_dependency() -- a hard + # CMake error, not a downgrade -- for any one of them whose header is + # absent. The runner image carries libx11-dev (X11/XKBlib.h resolved) but + # none of the extension packages, so configuration stopped at the first of + # the nine: "Couldn't find dependency package for XCURSOR". + # + # Turning the missing ones OFF would also configure, and is the wrong + # answer: the point of this job is that an AKGL build is a real SDL + # program, and a real SDL program on Linux builds the X11 backend. The + # tests still run headless under SDL_VIDEODRIVER=dummy -- see the env block + # below -- so this buys a faithful build, not a display. + # + # The list is SDL's own, from deps/SDL/docs/README-linux.md, reduced to the + # X11 entries: the audio, Wayland, KMSDRM and Vulkan backends all degrade + # to "not found" and skip themselves rather than erroring, so they cost + # nothing to leave out. Nine packages, all in noble. - name: dependencies run: | sudo apt-get update -y - sudo apt-get install -y cmake gcc g++ pkg-config \ - libfreetype-dev libharfbuzz-dev + sudo apt-get install -y cmake gcc g++ pkg-config moreutils \ + libfreetype-dev libharfbuzz-dev \ + libx11-dev libxext-dev libxcursor-dev libxi-dev libxfixes-dev \ + libxrandr-dev libxss-dev libxtst-dev libxkbcommon-dev # The akgl-backed half: the text sink and the graphics, audio and input # backends, the standalone SDL frontend, and the two suites that drive # them against a real software renderer and read the pixels back. @@ -256,7 +316,7 @@ jobs: - name: dependencies run: | sudo apt-get update -y - sudo apt-get install -y cmake gcc python3 + sudo apt-get install -y cmake gcc python3 moreutils # Verify the tests actually catch bugs: break the library many ways and # confirm the suite fails. This matters more here than in an ordinary C # library, because the akerror control-flow macros expand at their call diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index dac235e..f2e6537 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -70,10 +70,14 @@ jobs: # corpus is part of what kills mutants and it is checked in now, so # that is libakerror and libakstdlib and nothing else. submodules: true + # moreutils for errno(1), which deps/libakerror's scripts/generrno.sh needs + # to generate its errno table. Without it the generated table is empty and + # every AKERR_* code collapses onto a low integer -- see the long note on + # ci.yaml's cmake_build job for what that breaks and how it presents. - name: dependencies run: | sudo apt-get update -y - sudo apt-get install -y cmake gcc python3 + sudo apt-get install -y cmake gcc python3 moreutils # The whole akbasic-owned src/ tree. ci.yaml runs a two-file subset on every # push; this is the one that actually covers the interpreter. # diff --git a/deps/libakstdlib b/deps/libakstdlib index 669b2b3..2b79aca 160000 --- a/deps/libakstdlib +++ b/deps/libakstdlib @@ -1 +1 @@ -Subproject commit 669b2b395fda52167502e7326b9634bac7658004 +Subproject commit 2b79aca103d46f4ea4165172f5dc7f7c8dbaba75 diff --git a/include/akbasic/environment.h b/include/akbasic/environment.h index eb916ae..debbb2d 100644 --- a/include/akbasic/environment.h +++ b/include/akbasic/environment.h @@ -156,18 +156,32 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_new_leaf(akbasic_Environm */ akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_wait_for_command(akbasic_Environment *obj, const char *command); /** - * @brief True when this scope or any enclosing one is skipping forward. + * @brief Whether this scope or any enclosing one is skipping forward. + * + * The answer leaves through @p dest rather than the return value. Deciding it + * reads a recorded verb name, that read can fail, and a `bool` return has + * nowhere to report the failure -- so the signature changes rather than the + * error being swallowed. libakstdlib #38 is the ruling; `probe` in `symtab.c` + * and `loop_continues` in `runtime_structure.c` are the same shape. + * * @param obj Scope to inspect; NULL is not waiting. - * @return `true` when execution is currently suppressed. + * @param[out] dest `true` when execution is currently suppressed. Required. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When dest is NULL. */ -bool akbasic_environment_is_waiting_for_any(akbasic_Environment *obj); +akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_is_waiting_for_any(akbasic_Environment *obj, bool *dest); /** - * @brief True when this scope or an enclosing one is waiting for a given verb. + * @brief Whether this scope or an enclosing one is waiting for a given verb. + * + * Reports through @p dest for the same reason its sibling above does. + * * @param obj Scope to inspect; NULL is not waiting. - * @param command Verb to test for. - * @return `true` when that verb is what execution is waiting on. + * @param command Verb to test for; NULL is not waiting. + * @param[out] dest `true` when that verb is what execution is waiting on. Required. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When dest is NULL. */ -bool akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command); +akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command, bool *dest); /** * @brief Clear a pending wait, searching the parent chain for it. * diff --git a/src/data.c b/src/data.c index 46e90e6..2893bfa 100644 --- a/src/data.c +++ b/src/data.c @@ -8,7 +8,6 @@ #include #include #include -#include #include #include @@ -112,6 +111,7 @@ static akerr_ErrorContext *scan_line(akbasic_DataState *obj, const char *code, i const char *cursor = code; bool statementstart = true; bool instring = false; + int cmp = 0; while ( *cursor != '\0' ) { if ( instring ) { @@ -141,12 +141,14 @@ static akerr_ErrorContext *scan_line(akbasic_DataState *obj, const char *code, i } continue; } - if ( statementstart && strncasecmp(cursor, "DATA", 4) == 0 - && !isalnum((unsigned char)cursor[4]) ) { - cursor += 4; - PASS(errctx, collect_items(obj, &cursor, lineno)); - statementstart = false; - continue; + if ( statementstart ) { + PASS(errctx, aksl_strncasecmp(cursor, "DATA", 4, &cmp)); + if ( cmp == 0 && !isalnum((unsigned char)cursor[4]) ) { + cursor += 4; + PASS(errctx, collect_items(obj, &cursor, lineno)); + statementstart = false; + continue; + } } statementstart = false; cursor += 1; @@ -184,7 +186,7 @@ akerr_ErrorContext *akbasic_data_next(akbasic_Runtime *obj, akbasic_Type type, a PASS(errctx, akbasic_value_zero(dest)); if ( type == AKBASIC_TYPE_STRING ) { dest->valuetype = AKBASIC_TYPE_STRING; - snprintf(dest->stringval, sizeof(dest->stringval), "%s", item->text); + PASS(errctx, aksl_strcpy(dest->stringval, sizeof(dest->stringval), item->text)); SUCCEED_RETURN(errctx); } diff --git a/src/environment.c b/src/environment.c index a2f57e0..77a184a 100644 --- a/src/environment.c +++ b/src/environment.c @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -39,7 +40,7 @@ akerr_ErrorContext *akbasic_environment_init(akbasic_Environment *obj, akbasic_R obj->readIdentifierIdx = 0; obj->waitingForCommand[0] = '\0'; obj->errorToken = NULL; - memset(obj->readIdentifierLeaves, 0, sizeof(obj->readIdentifierLeaves)); + PASS(errctx, aksl_memset(obj->readIdentifierLeaves, 0, sizeof(obj->readIdentifierLeaves))); obj->doLeafPool.next = 0; obj->doLeafPool.capacity = AKBASIC_MAX_CONDITION_LEAVES; obj->doLeafPool.leaves = obj->doLeafStorage; @@ -123,6 +124,7 @@ akerr_ErrorContext *akbasic_environment_new_leaf(akbasic_Environment *obj, akbas akerr_ErrorContext *akbasic_environment_wait_for_command(akbasic_Environment *obj, const char *command) { PREPARE_ERROR(errctx); + size_t length = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL && command != NULL), AKERR_NULLPOINTER, "NULL argument in wait_for_command"); @@ -135,38 +137,56 @@ akerr_ErrorContext *akbasic_environment_wait_for_command(akbasic_Environment *ob FAIL_NONZERO_RETURN(errctx, (obj->waitingForCommand[0] != '\0'), AKBASIC_ERR_STATE, "Can't wait on multiple commands in the same environment : %s", obj->waitingForCommand); - FAIL_ZERO_RETURN(errctx, (strlen(command) < sizeof(obj->waitingForCommand)), + PASS(errctx, aksl_strlen(command, &length)); + FAIL_ZERO_RETURN(errctx, (length < sizeof(obj->waitingForCommand)), AKBASIC_ERR_BOUNDS, "Command name '%s' is too long to wait on", command); - strncpy(obj->waitingForCommand, command, sizeof(obj->waitingForCommand) - 1); - obj->waitingForCommand[sizeof(obj->waitingForCommand) - 1] = '\0'; + PASS(errctx, aksl_strcpy(obj->waitingForCommand, sizeof(obj->waitingForCommand), command)); SUCCEED_RETURN(errctx); } -bool akbasic_environment_is_waiting_for_any(akbasic_Environment *obj) +akerr_ErrorContext *akbasic_environment_is_waiting_for_any(akbasic_Environment *obj, bool *dest) { + PREPARE_ERROR(errctx); + + FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, + "NULL destination in is_waiting_for_any"); + *dest = false; if ( obj == NULL ) { - return false; + SUCCEED_RETURN(errctx); } if ( obj->waitingForCommand[0] != '\0' ) { - return true; + *dest = true; + SUCCEED_RETURN(errctx); } - return akbasic_environment_is_waiting_for_any(obj->parent); + /* The recursive tail becomes a PASS; dest carries the answer back up. */ + PASS(errctx, akbasic_environment_is_waiting_for_any(obj->parent, dest)); + SUCCEED_RETURN(errctx); } -bool akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command) +akerr_ErrorContext *akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command, bool *dest) { + PREPARE_ERROR(errctx); + int cmp = 0; + + FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, + "NULL destination in is_waiting_for"); + *dest = false; if ( obj == NULL || command == NULL ) { - return false; + SUCCEED_RETURN(errctx); } - if ( strcmp(obj->waitingForCommand, command) == 0 ) { - return true; + PASS(errctx, aksl_strcmp(obj->waitingForCommand, command, &cmp)); + if ( cmp == 0 ) { + *dest = true; + SUCCEED_RETURN(errctx); } - return akbasic_environment_is_waiting_for(obj->parent, command); + PASS(errctx, akbasic_environment_is_waiting_for(obj->parent, command, dest)); + SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_environment_stop_waiting(akbasic_Environment *obj, const char *command) { PREPARE_ERROR(errctx); + int cmp = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL && command != NULL), AKERR_NULLPOINTER, "NULL argument in stop_waiting"); @@ -179,7 +199,8 @@ akerr_ErrorContext *akbasic_environment_stop_waiting(akbasic_Environment *obj, c * a miss is silently tolerated, exactly as today. */ while ( obj != NULL ) { - if ( strcmp(obj->waitingForCommand, command) == 0 ) { + PASS(errctx, aksl_strcmp(obj->waitingForCommand, command, &cmp)); + if ( cmp == 0 ) { obj->waitingForCommand[0] = '\0'; SUCCEED_RETURN(errctx); } @@ -197,7 +218,7 @@ akerr_ErrorContext *akbasic_environment_get_function(akbasic_Environment *obj, c FAIL_ZERO_RETURN(errctx, (obj != NULL && fname != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in get_function"); - len = strlen(fname); + PASS(errctx, aksl_strlen(fname, &len)); FAIL_ZERO_RETURN(errctx, (len < sizeof(upper)), AKERR_KEY, "Function '%s' is not defined", fname); for ( i = 0; i < len; i++ ) { char c = fname[i]; @@ -291,6 +312,7 @@ akerr_ErrorContext *akbasic_environment_create(akbasic_Environment *obj, const c akbasic_Variable *variable = NULL; int64_t sizes[1] = { 1 }; void *slot = NULL; + size_t namelen = 0; akerr_ErrorContext *found = NULL; FAIL_ZERO_RETURN(errctx, (obj != NULL && varname != NULL && dest != NULL), AKERR_NULLPOINTER, @@ -312,10 +334,10 @@ akerr_ErrorContext *akbasic_environment_create(akbasic_Environment *obj, const c IGNORE(akerr_release_error(found)); PASS(errctx, akbasic_runtime_new_variable(obj->runtime, &variable)); - FAIL_ZERO_RETURN(errctx, (strlen(varname) < sizeof(variable->name)), AKBASIC_ERR_BOUNDS, + PASS(errctx, aksl_strlen(varname, &namelen)); + FAIL_ZERO_RETURN(errctx, (namelen < sizeof(variable->name)), AKBASIC_ERR_BOUNDS, "Variable name '%s' is too long", varname); - strncpy(variable->name, varname, sizeof(variable->name) - 1); - variable->name[sizeof(variable->name) - 1] = '\0'; + PASS(errctx, aksl_strcpy(variable->name, sizeof(variable->name), varname)); variable->valuetype = AKBASIC_TYPE_UNDEFINED; variable->mutable_ = true; PASS(errctx, akbasic_variable_init(variable, &obj->runtime->valuepool, sizes, 1)); diff --git a/src/format.c b/src/format.c index f5f269b..75a4238 100644 --- a/src/format.c +++ b/src/format.c @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -36,7 +37,7 @@ akerr_ErrorContext *akbasic_format_state_init(akbasic_FormatState *obj) PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL format state in init"); - memcpy(obj->chars, PUDEF_DEFAULTS, sizeof(obj->chars)); + PASS(errctx, aksl_memcpy(obj->chars, PUDEF_DEFAULTS, sizeof(obj->chars))); SUCCEED_RETURN(errctx); } @@ -161,11 +162,20 @@ static void measure_numeric(const char *field, size_t length, int *before, int * } } -/** @brief Fill @p dest with @p width copies of `*`, the overflow marker. */ -static void overflow(char *dest, size_t width) +/** + * @brief Fill @p dest with @p width copies of `*`, the overflow marker. + * + * Returns a context rather than `void` because the fill can fail and there was + * nowhere to say so. Both call sites are already inside `render_numeric`, which + * returns one. See libakstdlib #38. + */ +static akerr_ErrorContext *overflow(char *dest, size_t width) { - memset(dest, '*', width); + PREPARE_ERROR(errctx); + + PASS(errctx, aksl_memset(dest, '*', width)); dest[width] = '\0'; + SUCCEED_RETURN(errctx); } /** @@ -191,15 +201,21 @@ static akerr_ErrorContext *render_numeric(akbasic_FormatState *obj, const char * bool leadsign = false; bool trailsign = false; bool negative = (number < 0.0); + int written = 0; measure_numeric(field, length, &before, &after, &commas, &dollar, &leadsign, &trailsign); FAIL_ZERO_RETURN(errctx, (length + 1 <= len), AKBASIC_ERR_BOUNDS, "PRINT USING field of %zu characters does not fit", length); - snprintf(digits, sizeof(digits), "%.*f", after, (negative ? -number : number)); + PASS(errctx, aksl_snprintf(&written, digits, sizeof(digits), "%.*f", + after, (negative ? -number : number))); - point = strchr(digits, '.'); - intlen = (point != NULL ? (size_t)(point - digits) : strlen(digits)); + PASS(errctx, aksl_strchr(digits, '.', &point)); + if ( point != NULL ) { + intlen = (size_t)(point - digits); + } else { + PASS(errctx, aksl_strlen(digits, &intlen)); + } /* Group the integer part, if the field asked for separators. */ used = 0; @@ -213,7 +229,7 @@ static akerr_ErrorContext *render_numeric(akbasic_FormatState *obj, const char * used += 1; } } else { - memcpy(grouped, digits, intlen); + PASS(errctx, aksl_memcpy(grouped, digits, intlen)); used = intlen; } grouped[used] = '\0'; @@ -226,7 +242,7 @@ static akerr_ErrorContext *render_numeric(akbasic_FormatState *obj, const char * * printing wider than asked, which would misalign every later column. */ if ( (int)intlen > before ) { - overflow(dest, length); + PASS(errctx, overflow(dest, length)); SUCCEED_RETURN(errctx); } @@ -252,13 +268,13 @@ static akerr_ErrorContext *render_numeric(akbasic_FormatState *obj, const char * used += 1; } } - memcpy(dest + used, grouped, (size_t)group); + PASS(errctx, aksl_memcpy(dest + used, grouped, (size_t)group)); used += (size_t)group; if ( after > 0 ) { dest[used] = obj->chars[AKBASIC_PUDEF_POINT]; used += 1; - memcpy(dest + used, (point != NULL ? point + 1 : ""), (size_t)after); + PASS(errctx, aksl_memcpy(dest + used, (point != NULL ? point + 1 : ""), (size_t)after)); used += (size_t)after; } if ( trailsign ) { @@ -271,7 +287,7 @@ static akerr_ErrorContext *render_numeric(akbasic_FormatState *obj, const char * * because a printed -5 that reads as 5 is worse than a row of stars. */ if ( negative && !leadsign && !trailsign ) { - overflow(dest, length); + PASS(errctx, overflow(dest, length)); SUCCEED_RETURN(errctx); } dest[used] = '\0'; @@ -283,26 +299,27 @@ static akerr_ErrorContext *render_string(const char *field, size_t length, const char *dest, size_t len) { PREPARE_ERROR(errctx); - size_t textlen = strlen(text); + size_t textlen = 0; size_t pad = 0; + PASS(errctx, aksl_strlen(text, &textlen)); FAIL_ZERO_RETURN(errctx, (length + 1 <= len), AKBASIC_ERR_BOUNDS, "PRINT USING field of %zu characters does not fit", length); if ( textlen > length ) { /* Truncated, not starred: BASIC 7.0 cuts a string to its field. */ - memcpy(dest, text, length); + PASS(errctx, aksl_memcpy(dest, text, length)); dest[length] = '\0'; SUCCEED_RETURN(errctx); } - memset(dest, ' ', length); + PASS(errctx, aksl_memset(dest, ' ', length)); dest[length] = '\0'; if ( field[0] == '=' ) { pad = (length - textlen) / 2; } else { pad = length - textlen; } - memcpy(dest + pad, text, textlen); + PASS(errctx, aksl_memcpy(dest + pad, text, textlen)); SUCCEED_RETURN(errctx); } @@ -313,6 +330,9 @@ akerr_ErrorContext *akbasic_format_using(akbasic_FormatState *obj, const char *f size_t start = 0; size_t length = 0; size_t used = 0; + size_t renderedlen = 0; + size_t formatlen = 0; + int written = 0; bool numeric = false; FAIL_ZERO_RETURN(errctx, (obj != NULL && format != NULL && value != NULL && dest != NULL), @@ -343,12 +363,15 @@ akerr_ErrorContext *akbasic_format_using(akbasic_FormatState *obj, const char *f } /* Literal text before the field, the field, then literal text after it. */ - used = strlen(rendered) + strlen(format) - length; + PASS(errctx, aksl_strlen(rendered, &renderedlen)); + PASS(errctx, aksl_strlen(format, &formatlen)); + used = renderedlen + formatlen - length; FAIL_ZERO_RETURN(errctx, (used + 1 <= len), AKBASIC_ERR_BOUNDS, "PRINT USING result of %zu characters does not fit", used); - memcpy(dest, format, start); - memcpy(dest + start, rendered, strlen(rendered)); - snprintf(dest + start + strlen(rendered), len - start - strlen(rendered), - "%s", format + start + length); + PASS(errctx, aksl_memcpy(dest, format, start)); + PASS(errctx, aksl_memcpy(dest + start, rendered, renderedlen)); + /* The check above already proved the tail fits, so this cannot truncate. */ + PASS(errctx, aksl_snprintf(&written, dest + start + renderedlen, + len - start - renderedlen, "%s", format + start + length)); SUCCEED_RETURN(errctx); } diff --git a/src/frontend_akgl.c b/src/frontend_akgl.c index 6144be9..22c1887 100644 --- a/src/frontend_akgl.c +++ b/src/frontend_akgl.c @@ -23,9 +23,9 @@ */ #include -#include #include +#include #include #include @@ -55,7 +55,7 @@ akerr_ErrorContext *akbasic_frontend_akgl_init(akbasic_AkglFrontend *obj, const FAIL_ZERO_RETURN(errctx, (w > 0 && h > 0 && fontsize > 0), AKBASIC_ERR_VALUE, "A %dx%d window at %d points is not a window", w, h, fontsize); - memset(obj, 0, sizeof(*obj)); + PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); obj->width = w; obj->height = h; diff --git a/src/grammar.c b/src/grammar.c index f7b313e..66e719d 100644 --- a/src/grammar.c +++ b/src/grammar.c @@ -17,14 +17,19 @@ static akerr_ErrorContext *copy_bounded(char *dest, const char *src, const char *what) { PREPARE_ERROR(errctx); + size_t srclen = 0; FAIL_ZERO_RETURN(errctx, (src != NULL), AKERR_NULLPOINTER, "NULL %s", what); - FAIL_ZERO_RETURN(errctx, (strlen(src) < AKBASIC_MAX_STRING_LENGTH), + PASS(errctx, aksl_strlen(src, &srclen)); + FAIL_ZERO_RETURN(errctx, (srclen < AKBASIC_MAX_STRING_LENGTH), AKBASIC_ERR_VALUE, "%s of %zu characters exceeds the %d character limit", - what, strlen(src), AKBASIC_MAX_STRING_LENGTH - 1); - strncpy(dest, src, AKBASIC_MAX_STRING_LENGTH - 1); - dest[AKBASIC_MAX_STRING_LENGTH - 1] = '\0'; + what, srclen, AKBASIC_MAX_STRING_LENGTH - 1); + /* aksl_strcpy always terminates and refuses rather than truncates, which is + the behaviour this site already wanted. The length check above stays + because it names the limit in the message. Every destination is one of the + leaf's AKBASIC_MAX_STRING_LENGTH buffers, which is what sizes the copy. */ + PASS(errctx, aksl_strcpy(dest, AKBASIC_MAX_STRING_LENGTH, src)); SUCCEED_RETURN(errctx); } @@ -90,8 +95,8 @@ static akerr_ErrorContext *clone_into(akbasic_ASTLeaf *self, akbasic_LeafPool *p copy->parent = self->parent; copy->literal_int = self->literal_int; copy->literal_float = self->literal_float; - memcpy(copy->literal_string, self->literal_string, sizeof(copy->literal_string)); - memcpy(copy->identifier, self->identifier, sizeof(copy->identifier)); + PASS(errctx, aksl_memcpy(copy->literal_string, self->literal_string, sizeof(copy->literal_string))); + PASS(errctx, aksl_memcpy(copy->identifier, self->identifier, sizeof(copy->identifier))); copy->operator_ = self->operator_; PASS(errctx, clone_into(self->left, pool, ©->left)); @@ -289,7 +294,9 @@ akerr_ErrorContext *akbasic_leaf_new_literal_int(akbasic_ASTLeaf *obj, const cha { PREPARE_ERROR(errctx); long long value = 0; + size_t lexlen = 0; int base = 10; + int cmp = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_literal_int"); FAIL_ZERO_RETURN(errctx, (lexeme != NULL), AKERR_NULLPOINTER, "NULL lexeme in new_literal_int"); @@ -302,7 +309,9 @@ akerr_ErrorContext *akbasic_leaf_new_literal_int(akbasic_ASTLeaf *obj, const cha * Commodore BASIC has no octal literals, and a leading zero in a listing is * padding, not a radix. */ - if ( strlen(lexeme) > 2 && strncmp(lexeme, "0x", 2) == 0 ) { + PASS(errctx, aksl_strlen(lexeme, &lexlen)); + PASS(errctx, aksl_strncmp(lexeme, "0x", 2, &cmp)); + if ( lexlen > 2 && cmp == 0 ) { base = 16; } PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_LITERAL_INT)); @@ -388,6 +397,7 @@ akerr_ErrorContext *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, si PREPARE_ERROR(errctx); char sub1[AKBASIC_MAX_STRING_LENGTH]; char sub2[AKBASIC_MAX_STRING_LENGTH]; + int written = 0; FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL leaf in to_string"); FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in to_string"); @@ -395,40 +405,42 @@ akerr_ErrorContext *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, si switch ( self->leaftype ) { case AKBASIC_LEAF_LITERAL_INT: - snprintf(dest, len, "%" PRId64, self->literal_int); + PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, self->literal_int)); break; case AKBASIC_LEAF_LITERAL_FLOAT: - snprintf(dest, len, "%f", self->literal_float); + PASS(errctx, aksl_snprintf(&written, dest, len, "%f", self->literal_float)); break; case AKBASIC_LEAF_LITERAL_STRING: - snprintf(dest, len, "%s", self->literal_string); + PASS(errctx, aksl_snprintf(&written, dest, len, "%s", self->literal_string)); break; case AKBASIC_LEAF_IDENTIFIER_INT: case AKBASIC_LEAF_IDENTIFIER_FLOAT: case AKBASIC_LEAF_IDENTIFIER_STRING: case AKBASIC_LEAF_IDENTIFIER: - snprintf(dest, len, "%s", self->identifier); + PASS(errctx, aksl_snprintf(&written, dest, len, "%s", self->identifier)); break; case AKBASIC_LEAF_IDENTIFIER_STRUCT: - snprintf(dest, len, "%s", self->identifier); + PASS(errctx, aksl_snprintf(&written, dest, len, "%s", self->identifier)); break; case AKBASIC_LEAF_FIELD: PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1))); - snprintf(dest, len, "%s%s%s", sub1, - (self->operator_ == AKBASIC_TOK_ARROW ? "->" : "."), self->identifier); + PASS(errctx, aksl_snprintf(&written, dest, len, "%s%s%s", sub1, + (self->operator_ == AKBASIC_TOK_ARROW ? "->" : "."), self->identifier)); break; case AKBASIC_LEAF_UNARY: PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1))); - snprintf(dest, len, "(%s %s)", operator_to_str(self->operator_), sub1); + PASS(errctx, aksl_snprintf(&written, dest, len, "(%s %s)", + operator_to_str(self->operator_), sub1)); break; case AKBASIC_LEAF_BINARY: PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1))); PASS(errctx, akbasic_leaf_to_string(self->right, sub2, sizeof(sub2))); - snprintf(dest, len, "(%s %s %s)", operator_to_str(self->operator_), sub1, sub2); + PASS(errctx, aksl_snprintf(&written, dest, len, "(%s %s %s)", + operator_to_str(self->operator_), sub1, sub2)); break; case AKBASIC_LEAF_GROUPING: PASS(errctx, akbasic_leaf_to_string(self->expr, sub1, sizeof(sub1))); - snprintf(dest, len, "(group %s)", sub1); + PASS(errctx, aksl_snprintf(&written, dest, len, "(group %s)", sub1)); break; case AKBASIC_LEAF_COMMAND: case AKBASIC_LEAF_COMMAND_IMMEDIATE: @@ -437,10 +449,10 @@ akerr_ErrorContext *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, si * The reference falls through to Go's %+v struct dump here, which has no * useful C equivalent. Print something a test can assert on instead. */ - snprintf(dest, len, "(%s)", self->identifier); + PASS(errctx, aksl_snprintf(&written, dest, len, "(%s)", self->identifier)); break; default: - snprintf(dest, len, "(leaf %d)", (int)self->leaftype); + PASS(errctx, aksl_snprintf(&written, dest, len, "(leaf %d)", (int)self->leaftype)); break; } SUCCEED_RETURN(errctx); diff --git a/src/graphics_akgl.c b/src/graphics_akgl.c index 652ef66..6008686 100644 --- a/src/graphics_akgl.c +++ b/src/graphics_akgl.c @@ -12,9 +12,8 @@ * reaching for a global, which is exactly what goal 3 needs. */ -#include - #include +#include #include #include @@ -246,7 +245,7 @@ akerr_ErrorContext *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akb "NULL renderer in graphics_init_akgl: the host creates it, not this"); PASS(errctx, akgl_error_init()); - memset(state, 0, sizeof(*state)); + PASS(errctx, aksl_memset(state, 0, sizeof(*state))); state->renderer = renderer; obj->self = state; diff --git a/src/host.c b/src/host.c index 7d89808..2e62ed0 100644 --- a/src/host.c +++ b/src/host.c @@ -19,9 +19,9 @@ #include #include -#include #include +#include #include #include @@ -89,8 +89,14 @@ akerr_ErrorContext *akbasic_host_read_field(akbasic_Runtime *obj, akbasic_Struct * left it full: a BASIC string is inline and fixed, so there is nothing * to alias and nothing that can outlive the read. */ + /* + * Raw snprintf: the host's bytes need not be terminated, and `%.*s` + * bounded by hostwidth is what reads at most that many of them. A field + * wider than the destination truncates, which is the documented contract + * for reading a host string into a fixed BASIC one. libakstdlib #34. + */ snprintf(text, sizeof(text), "%.*s", (int)field->hostwidth, at); - snprintf(dest->stringval, sizeof(dest->stringval), "%s", text); + PASS(errctx, aksl_strcpy(dest->stringval, sizeof(dest->stringval), text)); break; default: FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, @@ -115,6 +121,7 @@ akerr_ErrorContext *akbasic_host_write_field(akbasic_Runtime *obj, akbasic_Struc char *at = (char *)hostbase + field->hostoffset; int64_t n = 0; double d = 0.0; + size_t len = 0; (void)obj; FAIL_ZERO_RETURN(errctx, (hostbase != NULL && src != NULL), AKERR_NULLPOINTER, @@ -166,12 +173,13 @@ akerr_ErrorContext *akbasic_host_write_field(akbasic_Runtime *obj, akbasic_Struc * eight of them. BASIC strings have no width, so this is the one place * the two models genuinely disagree and the disagreement is reported. */ - FAIL_ZERO_RETURN(errctx, (strlen(src->stringval) < field->hostwidth), + PASS(errctx, aksl_strlen(src->stringval, &len)); + FAIL_ZERO_RETURN(errctx, (len < field->hostwidth), AKBASIC_ERR_VALUE, "A string of %zu characters does not fit in %s, which holds %zu", - strlen(src->stringval), field->name, field->hostwidth - 1); - memset(at, 0, field->hostwidth); - memcpy(at, src->stringval, strlen(src->stringval)); + len, field->name, field->hostwidth - 1); + PASS(errctx, aksl_memset(at, 0, field->hostwidth)); + PASS(errctx, aksl_memcpy(at, src->stringval, len)); break; default: FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, @@ -226,7 +234,16 @@ akerr_ErrorContext *akbasic_host_register_type(akbasic_Runtime *obj, const akbas type->name, type->fieldcount, AKBASIC_MAX_STRUCT_FIELDS); dest = &obj->structtypes.types[obj->structtypes.count]; - memset(dest, 0, sizeof(*dest)); + PASS(errctx, aksl_memset(dest, 0, sizeof(*dest))); + /* + * Raw snprintf, and a latent defect rather than a settled decision: a host + * type name over 31 characters truncates silently here, and two that share a + * 31-character prefix then collide in akbasic_structtype_find. scan_names() + * in structtype.c already refuses the same case for a script-declared type + * with an explicit limit message, so the two paths disagree. Converting this + * to aksl_strcpy is the fix and it is a behaviour change on a public + * registration call, so it wants its own issue rather than this port. + */ snprintf(dest->name, sizeof(dest->name), "%s", type->name); dest->used = true; dest->ishost = true; @@ -241,7 +258,7 @@ akerr_ErrorContext *akbasic_host_register_type(akbasic_Runtime *obj, const akbas FAIL_ZERO_RETURN(errctx, (src->name != NULL), AKERR_NULLPOINTER, "%s field %d has no name", type->name, i); - len = strlen(src->name); + PASS(errctx, aksl_strlen(src->name, &len)); /* * The suffix has to agree with the C type. A host that writes "HP%" over * an int32_t has said two different things about one field, and the @@ -253,6 +270,7 @@ akerr_ErrorContext *akbasic_host_register_type(akbasic_Runtime *obj, const akbas "%s.%s must end in '%c' for the C type it describes", type->name, src->name, suffix_for(src->kind)); + /* Same silent truncation as the type name above, and the same fix. */ snprintf(field->name, sizeof(field->name), "%s", src->name); field->hostkind = src->kind; field->hostoffset = src->offset; @@ -296,7 +314,7 @@ akerr_ErrorContext *akbasic_host_bind(akbasic_Runtime *obj, const char *name, FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && typename_ != NULL && instance != NULL), AKERR_NULLPOINTER, "NULL argument in host bind"); - len = strlen(name); + PASS(errctx, aksl_strlen(name, &len)); FAIL_ZERO_RETURN(errctx, (len >= 2 && name[len - 1] == '@'), AKBASIC_ERR_VALUE, "A structure variable's name ends in '@', so \"%s\" cannot be bound", name); PASS(errctx, akbasic_structtype_find(&obj->structtypes, typename_, &typeindex)); diff --git a/src/main.c b/src/main.c index 5b9ab54..b80a316 100644 --- a/src/main.c +++ b/src/main.c @@ -157,6 +157,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_akgl(FILE *program, const char *pa const char *fontpath = getenv("AKBASIC_FONT"); FILE *input = program; int mode = AKBASIC_MODE_RUNSTREAM; + int fd = 0; if ( fontpath == NULL ) { fontpath = AKBASIC_FONT_PATH; @@ -169,7 +170,8 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_akgl(FILE *program, const char *pa * in an AKGL build, and the golden corpus can be driven through this * binary as well as through the stdio one. */ - input = (isatty(fileno(stdin)) ? NULL : stdin); + PASS(errctx, aksl_fileno(stdin, &fd)); + input = (isatty(fd) ? NULL : stdin); mode = AKBASIC_MODE_REPL; } diff --git a/src/parser.c b/src/parser.c index cc68806..92d15e6 100644 --- a/src/parser.c +++ b/src/parser.c @@ -10,9 +10,9 @@ */ #include -#include #include +#include #include #include @@ -206,8 +206,7 @@ akerr_ErrorContext *akbasic_parser_command(akbasic_Parser *obj, akbasic_ASTLeaf PASS(errctx, akbasic_parser_previous(obj, &operator_)); optype = operator_->tokentype; - strncpy(opname, operator_->lexeme, sizeof(opname) - 1); - opname[sizeof(opname) - 1] = '\0'; + PASS(errctx, aksl_strcpy(opname, sizeof(opname), operator_->lexeme)); /* Does this verb need its own parse path? */ PASS(errctx, akbasic_verb_lookup(opname, &verb)); @@ -596,8 +595,7 @@ static akerr_ErrorContext *function_call(akbasic_Parser *obj, akbasic_ASTLeaf ** } PASS(errctx, akbasic_parser_previous(obj, &operator_)); - strncpy(fname, operator_->lexeme, sizeof(fname) - 1); - fname[sizeof(fname) - 1] = '\0'; + PASS(errctx, aksl_strcpy(fname, sizeof(fname), operator_->lexeme)); PASS(errctx, akbasic_verb_lookup(fname, &verb)); if ( verb != NULL && verb->tokentype == AKBASIC_TOK_FUNCTION ) { diff --git a/src/parser_commands.c b/src/parser_commands.c index 082f143..e823a5d 100644 --- a/src/parser_commands.c +++ b/src/parser_commands.c @@ -10,10 +10,9 @@ */ #include -#include -#include #include +#include #include #include @@ -78,20 +77,23 @@ akerr_ErrorContext *akbasic_parse_graphic(akbasic_Parser *parser, akbasic_ASTLea akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *expr = NULL; akbasic_Token *peeked = NULL; + int cmp = 0; peeked = akbasic_parser_peek(parser); - if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND && - strcmp(peeked->lexeme, "CLR") == 0 ) { - (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND); - PASS(errctx, akbasic_parser_new_leaf(parser, &arglist)); - arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST; - arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT; - PASS(errctx, akbasic_parser_new_leaf(parser, &arglist->right)); - PASS(errctx, akbasic_leaf_new_literal_int(arglist->right, "5")); - PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); - PASS(errctx, akbasic_leaf_new_command(expr, "GRAPHIC", arglist)); - *dest = expr; - SUCCEED_RETURN(errctx); + if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) { + PASS(errctx, aksl_strcmp(peeked->lexeme, "CLR", &cmp)); + if ( cmp == 0 ) { + (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND); + PASS(errctx, akbasic_parser_new_leaf(parser, &arglist)); + arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST; + arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT; + PASS(errctx, akbasic_parser_new_leaf(parser, &arglist->right)); + PASS(errctx, akbasic_leaf_new_literal_int(arglist->right, "5")); + PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); + PASS(errctx, akbasic_leaf_new_command(expr, "GRAPHIC", arglist)); + *dest = expr; + SUCCEED_RETURN(errctx); + } } PASS(errctx, akbasic_parse_arglist(parser, dest)); SUCCEED_RETURN(errctx); @@ -105,6 +107,7 @@ akerr_ErrorContext *akbasic_parse_draw(akbasic_Parser *parser, akbasic_ASTLeaf * akbasic_ASTLeaf *tail = NULL; akbasic_Token *peeked = NULL; akbasic_Token *operator_ = NULL; + int cmp = 0; /* * DRAW source, x1,y1 TO x2,y2 TO x3,y3 -- a polyline, with TO between pairs @@ -125,9 +128,11 @@ akerr_ErrorContext *akbasic_parse_draw(akbasic_Parser *parser, akbasic_ASTLeaf * for ( ;; ) { peeked = akbasic_parser_peek(parser); - if ( peeked == NULL || - peeked->tokentype != AKBASIC_TOK_COMMAND || - strcmp(peeked->lexeme, "TO") != 0 ) { + if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND ) { + break; + } + PASS(errctx, aksl_strcmp(peeked->lexeme, "TO", &cmp)); + if ( cmp != 0 ) { break; } FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), @@ -259,6 +264,7 @@ static akerr_ErrorContext *parse_loop_condition(akbasic_Parser *parser, akbasic_ { PREPARE_ERROR(errctx); akbasic_Token *peeked = NULL; + int cmp = 0; *condition = NULL; *kind = AKBASIC_LOOPCOND_NONE; @@ -267,12 +273,15 @@ static akerr_ErrorContext *parse_loop_condition(akbasic_Parser *parser, akbasic_ if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND ) { SUCCEED_RETURN(errctx); } - if ( strcmp(peeked->lexeme, "WHILE") == 0 ) { + PASS(errctx, aksl_strcmp(peeked->lexeme, "WHILE", &cmp)); + if ( cmp == 0 ) { *kind = AKBASIC_LOOPCOND_WHILE; - } else if ( strcmp(peeked->lexeme, "UNTIL") == 0 ) { - *kind = AKBASIC_LOOPCOND_UNTIL; } else { - SUCCEED_RETURN(errctx); + PASS(errctx, aksl_strcmp(peeked->lexeme, "UNTIL", &cmp)); + if ( cmp != 0 ) { + SUCCEED_RETURN(errctx); + } + *kind = AKBASIC_LOOPCOND_UNTIL; } (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND); @@ -372,6 +381,7 @@ akerr_ErrorContext *akbasic_parse_on(akbasic_Parser *parser, akbasic_ASTLeaf **d akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *tail = NULL; akbasic_Token *operator_ = NULL; + int cmp = 0; bool gosub = false; PASS(errctx, akbasic_parser_new_leaf(parser, &arglist)); @@ -389,10 +399,12 @@ akerr_ErrorContext *akbasic_parse_on(akbasic_Parser *parser, akbasic_ASTLeaf **d FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX, "Expected GOTO or GOSUB after ON (expression)"); PASS(errctx, akbasic_parser_previous(parser, &operator_)); - if ( strcmp(operator_->lexeme, "GOSUB") == 0 ) { + PASS(errctx, aksl_strcmp(operator_->lexeme, "GOSUB", &cmp)); + if ( cmp == 0 ) { gosub = true; } else { - FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "GOTO"), AKBASIC_ERR_SYNTAX, + PASS(errctx, aksl_strcmp(operator_->lexeme, "GOTO", &cmp)); + FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX, "Expected GOTO or GOSUB after ON (expression)"); } marker->literal_int = (gosub ? 1 : 0); @@ -429,14 +441,20 @@ akerr_ErrorContext *akbasic_parse_resume(akbasic_Parser *parser, akbasic_ASTLeaf akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *target = NULL; akbasic_Token *peeked = NULL; + int cmp = 0; peeked = akbasic_parser_peek(parser); - if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND && - strcmp(peeked->lexeme, "NEXT") == 0 ) { - (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND); - PASS(errctx, akbasic_parser_new_leaf(parser, &target)); - PASS(errctx, akbasic_leaf_new_command(target, "NEXT", NULL)); - } else if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) { + if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) { + PASS(errctx, aksl_strcmp(peeked->lexeme, "NEXT", &cmp)); + if ( cmp == 0 ) { + (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND); + PASS(errctx, akbasic_parser_new_leaf(parser, &target)); + PASS(errctx, akbasic_leaf_new_command(target, "NEXT", NULL)); + } + } + /* The other two forms, taken only when the word was not NEXT -- which is + what a target already built says. */ + if ( target == NULL && peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) { PASS(errctx, akbasic_parser_expression(parser, &target)); } @@ -464,6 +482,8 @@ akerr_ErrorContext *akbasic_parse_print(akbasic_Parser *parser, akbasic_ASTLeaf akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *right = NULL; akbasic_Token *peeked = NULL; + int cmp = 0; + bool matched = false; peeked = akbasic_parser_peek(parser); @@ -499,8 +519,11 @@ akerr_ErrorContext *akbasic_parse_print(akbasic_Parser *parser, akbasic_ASTLeaf SUCCEED_RETURN(errctx); } - if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND || - strcmp(peeked->lexeme, "USING") != 0 ) { + if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) { + PASS(errctx, aksl_strcmp(peeked->lexeme, "USING", &cmp)); + matched = (cmp == 0); + } + if ( !matched ) { /* The ordinary PRINT: one expression, or none at all. */ if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) { PASS(errctx, akbasic_parser_expression(parser, &right)); @@ -627,12 +650,14 @@ akerr_ErrorContext *akbasic_parse_point(akbasic_Parser *parser, akbasic_ASTLeaf akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *command = NULL; akbasic_Token *word = NULL; + int cmp = 0; PASS(errctx, akbasic_parser_expression(parser, &pointer)); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), AKBASIC_ERR_SYNTAX, "Expected POINT AT "); PASS(errctx, akbasic_parser_previous(parser, &word)); - FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AT") == 0), AKBASIC_ERR_SYNTAX, + PASS(errctx, aksl_strcasecmp(word->lexeme, "AT", &cmp)); + FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_SYNTAX, "Expected AT after POINT , not %s", word->lexeme); PASS(errctx, akbasic_parser_expression(parser, &target)); @@ -668,6 +693,7 @@ akerr_ErrorContext *akbasic_parse_dim(akbasic_Parser *parser, akbasic_ASTLeaf ** PREPARE_ERROR(errctx); akbasic_ASTLeaf *command = NULL; akbasic_Token *word = NULL; + int cmp = 0; PASS(errctx, parse_verb_with_identifier(parser, "DIM", dest)); command = *dest; @@ -676,13 +702,15 @@ akerr_ErrorContext *akbasic_parse_dim(akbasic_Parser *parser, akbasic_ASTLeaf ** SUCCEED_RETURN(errctx); } PASS(errctx, akbasic_parser_previous(parser, &word)); - FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AS") == 0), AKBASIC_ERR_SYNTAX, + PASS(errctx, aksl_strcasecmp(word->lexeme, "AS", &cmp)); + FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_SYNTAX, "Expected AS after DIM %s, not %s", command->right->identifier, word->lexeme); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), AKBASIC_ERR_SYNTAX, "Expected a type name after AS"); PASS(errctx, akbasic_parser_previous(parser, &word)); - if ( strcasecmp(word->lexeme, "PTR") == 0 ) { + PASS(errctx, aksl_strcasecmp(word->lexeme, "PTR", &cmp)); + if ( cmp == 0 ) { command->literal_int = 1; /* * `TO` is already a verb -- FOR ... TO owns it -- so it arrives as a @@ -694,13 +722,14 @@ akerr_ErrorContext *akbasic_parse_dim(akbasic_Parser *parser, akbasic_ASTLeaf ** FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX, "Expected TO after PTR"); PASS(errctx, akbasic_parser_previous(parser, &word)); - FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "TO") == 0), AKBASIC_ERR_SYNTAX, + PASS(errctx, aksl_strcasecmp(word->lexeme, "TO", &cmp)); + FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_SYNTAX, "Expected PTR TO TYPENAME, not PTR %s", word->lexeme); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO"); PASS(errctx, akbasic_parser_previous(parser, &word)); } - snprintf(command->literal_string, sizeof(command->literal_string), "%s", word->lexeme); + PASS(errctx, aksl_strcpy(command->literal_string, sizeof(command->literal_string), word->lexeme)); SUCCEED_RETURN(errctx); } @@ -738,6 +767,7 @@ static akerr_ErrorContext *parse_def_parameters(akbasic_Parser *parser, akbasic_ akbasic_ASTLeaf *param = NULL; akbasic_ASTLeaf *tail = NULL; akbasic_Token *word = NULL; + int cmp = 0; FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_LEFT_PAREN), AKBASIC_ERR_SYNTAX, "Expected an argument list after DEF "); @@ -762,25 +792,28 @@ static akerr_ErrorContext *parse_def_parameters(akbasic_Parser *parser, akbasic_ "%s must name its type: %s AS TYPENAME, or %s AS PTR TO TYPENAME", param->identifier, param->identifier, param->identifier); PASS(errctx, akbasic_parser_previous(parser, &word)); - FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AS") == 0), AKBASIC_ERR_SYNTAX, + PASS(errctx, aksl_strcasecmp(word->lexeme, "AS", &cmp)); + FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_SYNTAX, "Expected AS after %s, not %s", param->identifier, word->lexeme); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), AKBASIC_ERR_SYNTAX, "Expected a type name after AS"); PASS(errctx, akbasic_parser_previous(parser, &word)); - if ( strcasecmp(word->lexeme, "PTR") == 0 ) { + PASS(errctx, aksl_strcasecmp(word->lexeme, "PTR", &cmp)); + if ( cmp == 0 ) { param->literal_int = 1; /* `TO` is already a verb -- FOR ... TO owns it -- so it arrives as a command token rather than an identifier. */ FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX, "Expected TO after PTR"); PASS(errctx, akbasic_parser_previous(parser, &word)); - FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "TO") == 0), AKBASIC_ERR_SYNTAX, + PASS(errctx, aksl_strcasecmp(word->lexeme, "TO", &cmp)); + FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_SYNTAX, "Expected PTR TO TYPENAME, not PTR %s", word->lexeme); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO"); PASS(errctx, akbasic_parser_previous(parser, &word)); } - snprintf(param->literal_string, sizeof(param->literal_string), "%s", word->lexeme); + PASS(errctx, aksl_strcpy(param->literal_string, sizeof(param->literal_string), word->lexeme)); } if ( tail == NULL ) { @@ -806,6 +839,7 @@ akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf ** akbasic_ASTLeaf *expression = NULL; akbasic_ASTLeaf *command = NULL; akbasic_FunctionDef *fndef = NULL; + size_t namelen = 0; size_t i = 0; PASS(errctx, akbasic_parser_primary(parser, &identifier)); @@ -817,13 +851,14 @@ akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf ** PASS(errctx, akbasic_runtime_new_function(runtime, &fndef)); /* Uppercase the name: verbs and function names are case-insensitive. */ - FAIL_ZERO_RETURN(errctx, (strlen(identifier->identifier) < sizeof(fndef->name)), + PASS(errctx, aksl_strlen(identifier->identifier, &namelen)); + FAIL_ZERO_RETURN(errctx, (namelen < sizeof(fndef->name)), AKBASIC_ERR_BOUNDS, "Function name '%s' is too long", identifier->identifier); - for ( i = 0; i < strlen(identifier->identifier); i++ ) { + for ( i = 0; i < namelen; i++ ) { char c = identifier->identifier[i]; fndef->name[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c); } - fndef->name[strlen(identifier->identifier)] = '\0'; + fndef->name[namelen] = '\0'; if ( akbasic_parser_match1(parser, AKBASIC_TOK_ASSIGNMENT) ) { PASS(errctx, akbasic_parser_expression(parser, &expression)); @@ -866,13 +901,15 @@ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf ** akbasic_Environment *parent = runtime->environment; akbasic_Environment *newenv = NULL; int64_t firstline = 0; + int cmp = 0; PASS(errctx, akbasic_parser_assignment(parser, &assignment)); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX, "Expected FOR (assignment) TO (expression) [STEP (expression)]"); PASS(errctx, akbasic_parser_previous(parser, &operator_)); - FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "TO"), AKBASIC_ERR_SYNTAX, + PASS(errctx, aksl_strcmp(operator_->lexeme, "TO", &cmp)); + FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX, "Expected FOR (assignment) TO (expression) [STEP (expression)]"); FAIL_ZERO_RETURN(errctx, (assignment != NULL && akbasic_leaf_is_identifier(assignment->left)), @@ -894,7 +931,8 @@ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf ** if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) { PASS(errctx, akbasic_parser_previous(parser, &operator_)); - FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "STEP"), AKBASIC_ERR_SYNTAX, + PASS(errctx, aksl_strcmp(operator_->lexeme, "STEP", &cmp)); + FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX, "Expected FOR (assignment) TO (expression) [STEP (expression)]"); PASS(errctx, akbasic_parser_expression(parser, &newenv->forStepLeaf)); } else { @@ -907,7 +945,8 @@ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf ** } /* A NEXT already being awaited means this is an inner loop over the same variable. */ - if ( strcmp(parent->waitingForCommand, "NEXT") == 0 ) { + PASS(errctx, aksl_strcmp(parent->waitingForCommand, "NEXT", &cmp)); + if ( cmp == 0 ) { newenv->forNextVariable = parent->forNextVariable; } newenv->loopFirstLine = firstline; @@ -1038,6 +1077,7 @@ akerr_ErrorContext *akbasic_parse_if(akbasic_Parser *parser, akbasic_ASTLeaf **d akbasic_ASTLeaf *else_command = NULL; akbasic_ASTLeaf *branch = NULL; akbasic_Token *operator_ = NULL; + int cmp = 0; /* * Everything from here to the THEN is a condition, so a lone `=` in it is an @@ -1057,14 +1097,16 @@ akerr_ErrorContext *akbasic_parse_if(akbasic_Parser *parser, akbasic_ASTLeaf **d FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX, "Incomplete IF statement"); PASS(errctx, akbasic_parser_previous(parser, &operator_)); - FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "THEN"), AKBASIC_ERR_SYNTAX, + PASS(errctx, aksl_strcmp(operator_->lexeme, "THEN", &cmp)); + FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX, "Expected IF ... THEN"); PASS(errctx, akbasic_parser_command(parser, &then_command)); if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) { PASS(errctx, akbasic_parser_previous(parser, &operator_)); - FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "ELSE"), AKBASIC_ERR_SYNTAX, + PASS(errctx, aksl_strcmp(operator_->lexeme, "ELSE", &cmp)); + FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX, "Expected IF ... THEN ... ELSE ..."); PASS(errctx, akbasic_parser_command(parser, &else_command)); } diff --git a/src/play.c b/src/play.c index e0de3e9..2f495fb 100644 --- a/src/play.c +++ b/src/play.c @@ -15,9 +15,9 @@ */ #include -#include #include +#include #include #include @@ -96,7 +96,7 @@ akerr_ErrorContext *akbasic_play_parse(akbasic_Runtime *obj, const char *notes) FAIL_ZERO_RETURN(errctx, (obj != NULL && notes != NULL), AKERR_NULLPOINTER, "NULL argument in play_parse"); audio = &obj->audio_state; - len = strlen(notes); + PASS(errctx, aksl_strlen(notes, &len)); /* * A loop, so no CATCH and no _BREAK macros in here -- they expand to a C diff --git a/src/renumber.c b/src/renumber.c index aa7d40e..692d23c 100644 --- a/src/renumber.c +++ b/src/renumber.c @@ -27,10 +27,9 @@ #include #include #include -#include -#include #include +#include #include #include @@ -103,7 +102,7 @@ static akerr_ErrorContext *rewrite_targets(akbasic_TargetWalk *walk, const char for ( ;; ) { char replacement[32]; int64_t line = 0; - int written = 0; + size_t written = 0; while ( *p == ' ' || *p == '\t' ) { FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS, @@ -126,9 +125,16 @@ static akerr_ErrorContext *rewrite_targets(akbasic_TargetWalk *walk, const char p += 1; } PASS(errctx, walk->visit(walk, line, replacement, sizeof(replacement))); - written = snprintf(out, (size_t)(limit - out), "%s", replacement); + /* + * The fit is decided before the copy rather than read back out of a + * return value afterwards: aksl_strcpy refuses rather than truncates, + * so the bounds check has to come first to keep the diagnosis this + * function already gives. + */ + PASS(errctx, aksl_strlen(replacement, &written)); FAIL_ZERO_RETURN(errctx, (written > 0 && out + written < limit), AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit"); + PASS(errctx, aksl_strcpy(out, (size_t)(limit - out), replacement)); out += written; /* @@ -176,6 +182,7 @@ static akerr_ErrorContext *rewrite_line(akbasic_TargetWalk *walk, const char *co const char *limit = dest + len - 1; bool statementstart = true; bool instring = false; + int cmp = 0; int i = 0; while ( *p != '\0' ) { @@ -227,12 +234,12 @@ static akerr_ErrorContext *rewrite_line(akbasic_TargetWalk *walk, const char *co * name in the language to reach it. */ for ( i = 0; i < BRANCH_VERB_COUNT; i++ ) { - verblen = strlen(BRANCH_VERBS[i]); + PASS(errctx, aksl_strlen(BRANCH_VERBS[i], &verblen)); if ( p > code && isalnum((unsigned char)p[-1]) ) { break; } - if ( strncasecmp(p, BRANCH_VERBS[i], verblen) == 0 && - !isalnum((unsigned char)p[verblen]) ) { + PASS(errctx, aksl_strncasecmp(p, BRANCH_VERBS[i], verblen, &cmp)); + if ( cmp == 0 && !isalnum((unsigned char)p[verblen]) ) { matched = true; break; } @@ -247,7 +254,7 @@ static akerr_ErrorContext *rewrite_line(akbasic_TargetWalk *walk, const char *co if ( matched ) { FAIL_ZERO_RETURN(errctx, (out + verblen < (size_t)(limit - dest) + dest), AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit"); - memcpy(out, p, verblen); + PASS(errctx, aksl_memcpy(out, p, verblen)); out += verblen; p += verblen; PASS(errctx, rewrite_targets(walk, &p, &out, limit)); @@ -260,9 +267,10 @@ static akerr_ErrorContext *rewrite_line(akbasic_TargetWalk *walk, const char *co * cannot be pointed at it directly. Copy the type and the comma, then * rewrite what follows. */ - if ( strncasecmp(p, "COLLISION", 9) == 0 && !isalnum((unsigned char)p[9]) + PASS(errctx, aksl_strncasecmp(p, "COLLISION", 9, &cmp)); + if ( cmp == 0 && !isalnum((unsigned char)p[9]) && !(p > code && isalnum((unsigned char)p[-1])) ) { - memcpy(out, p, 9); + PASS(errctx, aksl_memcpy(out, p, 9)); out += 9; p += 9; while ( *p != '\0' && *p != ',' && *p != ':' ) { @@ -299,8 +307,9 @@ static akerr_ErrorContext *visit_renumber(akbasic_TargetWalk *walk, int64_t targ { PREPARE_ERROR(errctx); const int64_t *map = (const int64_t *)walk->self; + int written = 0; - snprintf(dest, len, "%" PRId64, mapped(map, target)); + PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, mapped(map, target))); SUCCEED_RETURN(errctx); } @@ -347,7 +356,7 @@ akerr_ErrorContext *akbasic_renumber(akbasic_Runtime *obj, int64_t newstart, int next += increment; } - memset(rewritten, 0, sizeof(rewritten)); + PASS(errctx, aksl_memset(rewritten, 0, sizeof(rewritten))); for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { int64_t target = 0; @@ -371,7 +380,7 @@ akerr_ErrorContext *akbasic_renumber(akbasic_Runtime *obj, int64_t newstart, int rewritten[target].numbered = true; } - memcpy(obj->source, rewritten, sizeof(obj->source)); + PASS(errctx, aksl_memcpy(obj->source, rewritten, sizeof(obj->source))); SUCCEED_RETURN(errctx); } @@ -402,8 +411,9 @@ static akerr_ErrorContext *visit_check(akbasic_TargetWalk *walk, int64_t target, { PREPARE_ERROR(errctx); CheckState *state = (CheckState *)walk->self; + int written = 0; - snprintf(dest, len, "%" PRId64, target); + PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, target)); if ( target >= 0 && target < AKBASIC_MAX_SOURCE_LINES ) { FAIL_NONZERO_RETURN(errctx, (state->runtime->source[target].code[0] != '\0' diff --git a/src/runtime.c b/src/runtime.c index d1ff8e7..69983ae 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -6,10 +6,9 @@ #include #include #include -#include -#include #include +#include #include #include @@ -28,7 +27,7 @@ akerr_ErrorContext *akbasic_runtime_new_variable(akbasic_Runtime *obj, akbasic_V "NULL argument in new_variable"); for ( i = 0; i < AKBASIC_MAX_VARIABLES; i++ ) { if ( !obj->variables[i].used ) { - memset(&obj->variables[i], 0, sizeof(obj->variables[i])); + PASS(errctx, aksl_memset(&obj->variables[i], 0, sizeof(obj->variables[i]))); obj->variables[i].used = true; /* * Not zero: zero is a valid structure type index, so a memset alone @@ -102,7 +101,7 @@ akerr_ErrorContext *akbasic_runtime_new_function(akbasic_Runtime *obj, akbasic_F "NULL argument in new_function"); for ( i = 0; i < AKBASIC_MAX_FUNCTIONS; i++ ) { if ( !obj->functions[i].used ) { - memset(&obj->functions[i], 0, sizeof(obj->functions[i])); + PASS(errctx, aksl_memset(&obj->functions[i], 0, sizeof(obj->functions[i]))); obj->functions[i].used = true; obj->functions[i].leafpool.next = 0; obj->functions[i].leafpool.capacity = AKBASIC_MAX_LEAVES * 2; @@ -215,7 +214,7 @@ akerr_ErrorContext *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink */ PASS(errctx, akbasic_error_register()); - memset(obj, 0, sizeof(*obj)); + PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); obj->sink = sink; obj->environment = NULL; obj->autoLineNumber = 0; @@ -279,7 +278,7 @@ akerr_ErrorContext *akbasic_runtime_set_ui(akbasic_Runtime *obj, akbasic_UiBacke akerr_ErrorContext *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path) { PREPARE_ERROR(errctx); - const char *slash = NULL; + char *slash = NULL; size_t length = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in set_source_path"); @@ -292,18 +291,18 @@ akerr_ErrorContext *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const * would do it but it is allowed to modify its argument and two of the three * libcs this has to build on disagree about which one they implement. */ - slash = strrchr(path, '/'); + PASS(errctx, aksl_strrchr(path, '/', &slash)); length = (slash == NULL ? 0 : (size_t)(slash - path)); if ( length == 0 ) { /* Either no directory at all, or the root. */ - strncpy(obj->sourcepath, (slash == NULL ? "." : "/"), sizeof(obj->sourcepath) - 1); - obj->sourcepath[sizeof(obj->sourcepath) - 1] = '\0'; + PASS(errctx, aksl_strcpy(obj->sourcepath, sizeof(obj->sourcepath), + (slash == NULL ? "." : "/"))); SUCCEED_RETURN(errctx); } FAIL_ZERO_RETURN(errctx, (length < sizeof(obj->sourcepath)), AKBASIC_ERR_BOUNDS, "Program path of %zu characters exceeds the %d character limit", length, AKBASIC_MAX_LINE_LENGTH - 1); - memcpy(obj->sourcepath, path, length); + PASS(errctx, aksl_memcpy(obj->sourcepath, path, length)); obj->sourcepath[length] = '\0'; SUCCEED_RETURN(errctx); } @@ -393,6 +392,16 @@ akerr_ErrorContext *akbasic_runtime_error(akbasic_Runtime *obj, akbasic_ErrorCla * The format, the trailing \n inside the string, and the second newline * writeln adds are all part of the acceptance contract -- * tests/language/array_outofbounds.txt ends in 0a 0a. See TODO.md 1.8. + * + * **Raw snprintf on purpose, and this is the site where it matters most.** + * `line` is 512 bytes; `message` arrives from an akerr_ErrorContext, whose + * own buffer is AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH -- 12384. An ordinary + * long diagnostic therefore truncates, and truncating a report is correct + * here: this is the one function that tells the user *what went wrong*, and + * aksl_snprintf would turn a long message into a second, different failure + * that replaces the first. A report may be shortened; it may not be lost. + * libakstdlib #34 is the issue tracking the contract that would let a caller + * ask for the length instead. */ snprintf(line, sizeof(line), "? %" PRId64 " : %s %s\n", obj->environment->lineno, errclass_to_string(errclass), message); @@ -469,7 +478,15 @@ akerr_ErrorContext *akbasic_runtime_set_mode(akbasic_Runtime *obj, int mode) } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH]; - snprintf(message, sizeof(message), "%s", errctx->message); + /* + * Ignored rather than propagated: `errctx` is the error being + * handled, so a PASS here would overwrite it with the copy's own + * failure and the next line would read a released context. Same + * reason the two calls below are ignored. aksl_strcpy empties the + * destination before it copies, so a refusal leaves an empty + * message rather than an uninitialised one. + */ + IGNORE(aksl_strcpy(message, sizeof(message), errctx->message)); obj->lasterrorstatus = errctx->status; IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message)); IGNORE(akbasic_runtime_set_mode(obj, obj->run_finished_mode)); @@ -492,7 +509,14 @@ static akerr_ErrorContext *report_and_reraise(akbasic_Runtime *obj, akerr_ErrorC char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH]; int status = cause->status; - snprintf(message, sizeof(message), "%s", cause->message); + /* + * Ignored rather than passed: this function's whole contract is that the + * program's own error is the one that leaves, so a failure to copy the + * message must not become the error that gets raised. aksl_strcpy empties + * the destination first, so a refusal reports an empty message rather than + * an uninitialised one. + */ + IGNORE(aksl_strcpy(message, sizeof(message), cause->message)); /* What ER# reports, if a TRAP is armed. Recorded before the context goes. */ obj->lasterrorstatus = status; cause->handled = true; @@ -675,6 +699,7 @@ akerr_ErrorContext *akbasic_runtime_evaluate(akbasic_Runtime *obj, akbasic_ASTLe akbasic_Value *rval = NULL; akbasic_Value *scratch = NULL; const akbasic_Verb *verb = NULL; + int cmp = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in evaluate"); @@ -724,9 +749,11 @@ akerr_ErrorContext *akbasic_runtime_evaluate(akbasic_Runtime *obj, akbasic_ASTLe * reaches the end of this line. Arming the wait is what makes a * multi-line IF possible at all; BEND clears it. */ - if ( notaken != NULL && notaken->leaftype == AKBASIC_LEAF_COMMAND && - strcmp(notaken->identifier, "BEGIN") == 0 ) { - PASS(errctx, akbasic_environment_wait_for_command(obj->environment, "BEND")); + if ( notaken != NULL && notaken->leaftype == AKBASIC_LEAF_COMMAND ) { + PASS(errctx, aksl_strcmp(notaken->identifier, "BEGIN", &cmp)); + if ( cmp == 0 ) { + PASS(errctx, akbasic_environment_wait_for_command(obj->environment, "BEND")); + } } if ( taken ) { PASS(errctx, akbasic_runtime_evaluate(obj, expr->left, dest)); @@ -765,7 +792,7 @@ akerr_ErrorContext *akbasic_runtime_evaluate(akbasic_Runtime *obj, akbasic_ASTLe case AKBASIC_LEAF_LITERAL_STRING: lval->valuetype = AKBASIC_TYPE_STRING; - memcpy(lval->stringval, expr->literal_string, sizeof(lval->stringval)); + PASS(errctx, aksl_memcpy(lval->stringval, expr->literal_string, sizeof(lval->stringval))); SUCCEED_RETURN(errctx); case AKBASIC_LEAF_UNARY: @@ -816,6 +843,10 @@ akerr_ErrorContext *akbasic_runtime_evaluate(akbasic_Runtime *obj, akbasic_ASTLe akerr_ErrorContext *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest) { PREPARE_ERROR(errctx); + int cmp = 0; + bool waiting = false; + bool matchesverb = false; + bool waitingbend = false; FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in interpret"); @@ -824,9 +855,21 @@ akerr_ErrorContext *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTL * verb. This is what keeps a zero-iteration FOR body from executing, given * that the loop condition is evaluated at the bottom of the structure. */ - if ( akbasic_environment_is_waiting_for_any(obj->environment) ) { - if ( expr->leaftype != AKBASIC_LEAF_COMMAND || - !akbasic_environment_is_waiting_for(obj->environment, expr->identifier) ) { + PASS(errctx, akbasic_environment_is_waiting_for_any(obj->environment, &waiting)); + if ( waiting ) { + /* + * Hoisted out of the condition it used to be a term in: the test reads a + * recorded verb name, that read can fail, and the old `bool` return had + * nowhere to report it. `matchesverb` stays false unless the leaf really + * is a command whose name is what the scope is waiting for, which is what + * the `||` short-circuit used to say. See libakstdlib #38. + */ + matchesverb = false; + if ( expr->leaftype == AKBASIC_LEAF_COMMAND ) { + PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, + expr->identifier, &matchesverb)); + } + if ( !matchesverb ) { /* * **A skipped loop has already pushed its scope, and this is where it * comes back.** @@ -860,11 +903,18 @@ akerr_ErrorContext *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTL */ if ( expr->leaftype == AKBASIC_LEAF_COMMAND && obj->environment->loopFirstLine != 0 && - obj->environment->waitingForCommand[0] == '\0' && - akbasic_environment_is_waiting_for(obj->environment, "BEND") && - (strcmp(expr->identifier, "FOR") == 0 || - strcmp(expr->identifier, "DO") == 0) ) { - PASS(errctx, akbasic_runtime_prev_environment(obj)); + obj->environment->waitingForCommand[0] == '\0' ) { + PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "BEND", + &waitingbend)); + if ( waitingbend ) { + PASS(errctx, aksl_strcmp(expr->identifier, "FOR", &cmp)); + if ( cmp != 0 ) { + PASS(errctx, aksl_strcmp(expr->identifier, "DO", &cmp)); + } + if ( cmp == 0 ) { + PASS(errctx, akbasic_runtime_prev_environment(obj)); + } + } } *dest = &obj->staticTrueValue; SUCCEED_RETURN(errctx); @@ -1125,15 +1175,18 @@ int64_t akbasic_runtime_find_previous_lineno(akbasic_Runtime *obj) akerr_ErrorContext *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lineno, const char *code, bool numbered) { PREPARE_ERROR(errctx); + size_t length = 0; FAIL_ZERO_RETURN(errctx, (lineno >= 0 && lineno < AKBASIC_MAX_SOURCE_LINES), AKBASIC_ERR_BOUNDS, "Line number %" PRId64 " is outside 0..%d", lineno, AKBASIC_MAX_SOURCE_LINES - 1); - FAIL_ZERO_RETURN(errctx, (strlen(code) < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS, + PASS(errctx, aksl_strlen(code, &length)); + FAIL_ZERO_RETURN(errctx, (length < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS, "Source line exceeds the %d character limit", AKBASIC_MAX_LINE_LENGTH - 1); - strncpy(obj->source[lineno].code, code, AKBASIC_MAX_LINE_LENGTH - 1); - obj->source[lineno].code[AKBASIC_MAX_LINE_LENGTH - 1] = '\0'; + /* aksl_strcpy always terminates and refuses rather than truncates; the + length check above is what makes the refusal unreachable. */ + PASS(errctx, aksl_strcpy(obj->source[lineno].code, sizeof(obj->source[lineno].code), code)); obj->source[lineno].lineno = lineno; obj->source[lineno].numbered = numbered; SUCCEED_RETURN(errctx); @@ -1242,11 +1295,12 @@ akerr_ErrorContext *akbasic_runtime_process_line_repl(akbasic_Runtime *obj) akbasic_ASTLeaf *leaf = NULL; akbasic_Value *value = NULL; akbasic_Parser parser; + int written = 0; bool eof = false; if ( obj->autoLineNumber > 0 ) { - snprintf(prompt, sizeof(prompt), "%" PRId64 " ", - obj->environment->lineno + obj->autoLineNumber); + PASS(errctx, aksl_snprintf(&written, prompt, sizeof(prompt), "%" PRId64 " ", + obj->environment->lineno + obj->autoLineNumber)); PASS(errctx, akbasic_runtime_write(obj, prompt)); } @@ -1272,7 +1326,8 @@ akerr_ErrorContext *akbasic_runtime_process_line_repl(akbasic_Runtime *obj) } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH]; - snprintf(message, sizeof(message), "%s", errctx->message); + /* Ignored, not passed: `errctx` is the error being handled here. */ + IGNORE(aksl_strcpy(message, sizeof(message), errctx->message)); obj->lasterrorstatus = errctx->status; IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message)); } FINISH(errctx, false); @@ -1337,14 +1392,14 @@ akerr_ErrorContext *akbasic_runtime_process_line_run(akbasic_Runtime *obj) akbasic_ASTLeaf *leaf = NULL; akbasic_Value *value = NULL; akbasic_Parser parser; + int written = 0; if ( obj->environment->nextline >= AKBASIC_MAX_SOURCE_LINES ) { PASS(errctx, akbasic_runtime_set_mode(obj, obj->run_finished_mode)); SUCCEED_RETURN(errctx); } - strncpy(line, obj->source[obj->environment->nextline].code, sizeof(line) - 1); - line[sizeof(line) - 1] = '\0'; + PASS(errctx, aksl_strcpy(line, sizeof(line), obj->source[obj->environment->nextline].code)); obj->environment->lineno = obj->environment->nextline; obj->environment->nextline += 1; if ( line[0] == '\0' ) { @@ -1358,7 +1413,8 @@ akerr_ErrorContext *akbasic_runtime_process_line_run(akbasic_Runtime *obj) */ if ( obj->trace ) { char tracemark[32]; - snprintf(tracemark, sizeof(tracemark), "[%" PRId64 "]", obj->environment->lineno); + PASS(errctx, aksl_snprintf(&written, tracemark, sizeof(tracemark), "[%" PRId64 "]", + obj->environment->lineno)); PASS(errctx, akbasic_runtime_write(obj, tracemark)); } @@ -1373,7 +1429,8 @@ akerr_ErrorContext *akbasic_runtime_process_line_run(akbasic_Runtime *obj) } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH]; - snprintf(message, sizeof(message), "%s", errctx->message); + /* Ignored, not passed: `errctx` is the error being handled here. */ + IGNORE(aksl_strcpy(message, sizeof(message), errctx->message)); /* * What ER# reports, recorded before the context goes -- the same line * report_and_reraise() carries for a runtime error, and it was missing @@ -1442,6 +1499,7 @@ static akerr_ErrorContext *scan_line_labels(akbasic_Environment *root, const cha { PREPARE_ERROR(errctx); const char *cursor = code; + int cmp = 0; bool statementstart = true; bool instring = false; @@ -1479,31 +1537,33 @@ static akerr_ErrorContext *scan_line_labels(akbasic_Environment *root, const cha } continue; } - if ( statementstart && strncasecmp(cursor, "LABEL", 5) == 0 - && !isalnum((unsigned char)cursor[5]) ) { - char name[AKBASIC_SYMTAB_MAX_KEY]; - size_t used = 0; + if ( statementstart ) { + PASS(errctx, aksl_strncasecmp(cursor, "LABEL", 5, &cmp)); + if ( cmp == 0 && !isalnum((unsigned char)cursor[5]) ) { + char name[AKBASIC_SYMTAB_MAX_KEY]; + size_t used = 0; - cursor += 5; - while ( isspace((unsigned char)*cursor) ) { - cursor += 1; + cursor += 5; + while ( isspace((unsigned char)*cursor) ) { + cursor += 1; + } + /* + * Copied as written. Verbs are case-insensitive in this dialect and + * identifiers are not, so folding the name here would file a label + * under a spelling `LABEL` itself never uses. + */ + while ( isalnum((unsigned char)*cursor) && used < sizeof(name) - 1 ) { + name[used] = *cursor; + used += 1; + cursor += 1; + } + name[used] = '\0'; + if ( used > 0 ) { + PASS(errctx, akbasic_symtab_set(&root->labels, name, NULL, lineno)); + } + statementstart = false; + continue; } - /* - * Copied as written. Verbs are case-insensitive in this dialect and - * identifiers are not, so folding the name here would file a label - * under a spelling `LABEL` itself never uses. - */ - while ( isalnum((unsigned char)*cursor) && used < sizeof(name) - 1 ) { - name[used] = *cursor; - used += 1; - cursor += 1; - } - name[used] = '\0'; - if ( used > 0 ) { - PASS(errctx, akbasic_symtab_set(&root->labels, name, NULL, lineno)); - } - statementstart = false; - continue; } statementstart = false; cursor += 1; @@ -1536,6 +1596,7 @@ akerr_ErrorContext *akbasic_runtime_arm_interrupt(akbasic_Runtime *obj, akbasic_ { PREPARE_ERROR(errctx); akbasic_Interrupt *slot = NULL; + size_t length = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in arm_interrupt"); FAIL_ZERO_RETURN(errctx, (source >= 0 && source < AKBASIC_MAX_INTERRUPTS), @@ -1550,11 +1611,13 @@ akerr_ErrorContext *akbasic_runtime_arm_interrupt(akbasic_Runtime *obj, akbasic_ slot->line = line; slot->label[0] = '\0'; if ( label != NULL && label[0] != '\0' ) { - FAIL_ZERO_RETURN(errctx, (strlen(label) < sizeof(slot->label)), AKBASIC_ERR_BOUNDS, + PASS(errctx, aksl_strlen(label, &length)); + FAIL_ZERO_RETURN(errctx, (length < sizeof(slot->label)), AKBASIC_ERR_BOUNDS, "Handler label \"%s\" exceeds the %zu character limit", label, sizeof(slot->label) - 1); - strncpy(slot->label, label, sizeof(slot->label) - 1); - slot->label[sizeof(slot->label) - 1] = '\0'; + /* aksl_strcpy always terminates and refuses rather than truncates; the + length check above is what makes the refusal unreachable. */ + PASS(errctx, aksl_strcpy(slot->label, sizeof(slot->label), label)); } SUCCEED_RETURN(errctx); } @@ -1567,7 +1630,7 @@ akerr_ErrorContext *akbasic_runtime_disarm_interrupt(akbasic_Runtime *obj, akbas FAIL_ZERO_RETURN(errctx, (source >= 0 && source < AKBASIC_MAX_INTERRUPTS), AKBASIC_ERR_BOUNDS, "Interrupt source %d is outside 0..%d", (int)source, AKBASIC_MAX_INTERRUPTS - 1); - memset(&obj->interrupts[source], 0, sizeof(obj->interrupts[source])); + PASS(errctx, aksl_memset(&obj->interrupts[source], 0, sizeof(obj->interrupts[source]))); SUCCEED_RETURN(errctx); } @@ -1682,6 +1745,7 @@ akerr_ErrorContext *akbasic_runtime_load(akbasic_Runtime *obj, const char *sourc char scanned[AKBASIC_MAX_LINE_LENGTH]; const char *cursor = NULL; const char *eol = NULL; + char *match = NULL; size_t length = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in load"); @@ -1696,9 +1760,11 @@ akerr_ErrorContext *akbasic_runtime_load(akbasic_Runtime *obj, const char *sourc obj->environment->lineno = 0; for ( cursor = source; *cursor != '\0'; cursor = (*eol == '\0' ? eol : eol + 1) ) { - eol = strchr(cursor, '\n'); + PASS(errctx, aksl_strchr(cursor, '\n', &match)); + eol = match; if ( eol == NULL ) { - eol = cursor + strlen(cursor); + PASS(errctx, aksl_strlen(cursor, &length)); + eol = cursor + length; } length = (size_t)(eol - cursor); if ( length > 0 && cursor[length - 1] == '\r' ) { @@ -1707,7 +1773,7 @@ akerr_ErrorContext *akbasic_runtime_load(akbasic_Runtime *obj, const char *sourc FAIL_ZERO_RETURN(errctx, (length < sizeof(line)), AKBASIC_ERR_BOUNDS, "Source line of %zu characters exceeds the %d character limit", length, AKBASIC_MAX_LINE_LENGTH - 1); - memcpy(line, cursor, length); + PASS(errctx, aksl_memcpy(line, cursor, length)); line[length] = '\0'; if ( line[0] == '\0' ) { continue; @@ -1815,7 +1881,8 @@ akerr_ErrorContext *akbasic_runtime_step(akbasic_Runtime *obj) } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH]; - snprintf(message, sizeof(message), "%s", errctx->message); + /* Ignored, not passed: `errctx` is the error being handled here. */ + IGNORE(aksl_strcpy(message, sizeof(message), errctx->message)); IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_RUNTIME, message)); } FINISH(errctx, false); if ( obj->errclass != AKBASIC_ERRCLASS_NONE ) { diff --git a/src/runtime_commands.c b/src/runtime_commands.c index 4294e58..f9fb06d 100644 --- a/src/runtime_commands.c +++ b/src/runtime_commands.c @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -96,7 +95,7 @@ akerr_ErrorContext *akbasic_cmd_print(akbasic_Runtime *obj, akbasic_ASTLeaf *exp */ if ( (*dest)->valuetype == AKBASIC_TYPE_STRUCT || (*dest)->valuetype == AKBASIC_TYPE_POINTER ) { if ( (*dest)->structbase == NULL ) { - snprintf(rendered, sizeof(rendered), "NOTHING"); + PASS(errctx, aksl_strcpy(rendered, sizeof(rendered), "NOTHING")); } else { PASS(errctx, akbasic_struct_to_string(obj, (*dest)->structtype, (*dest)->structbase, 0, rendered, sizeof(rendered))); @@ -149,13 +148,15 @@ akerr_ErrorContext *akbasic_cmd_return(akbasic_Runtime *obj, akbasic_ASTLeaf *ex { PREPARE_ERROR(errctx); akbasic_Value *result = NULL; + bool waiting = false; (void)lval; (void)rval; /* * A RETURN reached while skipping forward to one is the end of a DEF body, * not a subroutine return. Stop waiting and carry on. */ - if ( akbasic_environment_is_waiting_for(obj->environment, "RETURN") ) { + PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "RETURN", &waiting)); + if ( waiting ) { PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "RETURN")); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); @@ -445,6 +446,7 @@ akerr_ErrorContext *akbasic_cmd_input(akbasic_Runtime *obj, akbasic_ASTLeaf *exp char rendered[AKBASIC_MAX_STRING_LENGTH]; char buffer[AKBASIC_MAX_LINE_LENGTH]; long long converted = 0; + size_t len = 0; bool eof = false; (void)lval; (void)rval; @@ -479,10 +481,11 @@ akerr_ErrorContext *akbasic_cmd_input(akbasic_Runtime *obj, akbasic_ASTLeaf *exp break; default: entered->valuetype = AKBASIC_TYPE_STRING; - FAIL_ZERO_RETURN(errctx, (strlen(buffer) < AKBASIC_MAX_STRING_LENGTH), AKBASIC_ERR_VALUE, + PASS(errctx, aksl_strlen(buffer, &len)); + FAIL_ZERO_RETURN(errctx, (len < AKBASIC_MAX_STRING_LENGTH), AKBASIC_ERR_VALUE, "Input line exceeds the %d character limit", AKBASIC_MAX_STRING_LENGTH - 1); - strncpy(entered->stringval, buffer, AKBASIC_MAX_STRING_LENGTH - 1); - entered->stringval[AKBASIC_MAX_STRING_LENGTH - 1] = '\0'; + /* aksl_strcpy always terminates, so the explicit terminator is gone. */ + PASS(errctx, aksl_strcpy(entered->stringval, sizeof(entered->stringval), buffer)); break; } PASS(errctx, akbasic_environment_assign(obj->environment, identifier, entered, &unused)); @@ -539,6 +542,7 @@ akerr_ErrorContext *akbasic_cmd_list(akbasic_Runtime *obj, akbasic_ASTLeaf *expr int64_t startidx = 0; int64_t endidx = 0; int64_t i = 0; + int written = 0; (void)lval; (void)rval; PASS(errctx, parse_line_range(obj, expr, &startidx, &endidx)); @@ -552,7 +556,8 @@ akerr_ErrorContext *akbasic_cmd_list(akbasic_Runtime *obj, akbasic_ASTLeaf *expr if ( obj->source[i].code[0] == '\0' ) { continue; } - snprintf(line, sizeof(line), "%" PRId64 " %s", i, obj->source[i].code); + PASS(errctx, aksl_snprintf(&written, line, sizeof(line), "%" PRId64 " %s", + i, obj->source[i].code)); PASS(errctx, akbasic_runtime_println(obj, line)); } SUCCEED_TRUE(obj, dest); @@ -588,6 +593,7 @@ static akerr_ErrorContext *filename_argument(akbasic_Runtime *obj, akbasic_ASTLe { PREPARE_ERROR(errctx); akbasic_Value *value = NULL; + size_t namelen = 0; FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX, "Expected a filename"); @@ -596,10 +602,107 @@ static akerr_ErrorContext *filename_argument(akbasic_Runtime *obj, akbasic_ASTLe "Expected a filename string"); FAIL_ZERO_RETURN(errctx, (value->stringval[0] != '\0'), AKBASIC_ERR_VALUE, "Filename must not be empty"); - FAIL_ZERO_RETURN(errctx, (strlen(value->stringval) < len), AKBASIC_ERR_BOUNDS, + PASS(errctx, aksl_strlen(value->stringval, &namelen)); + FAIL_ZERO_RETURN(errctx, (namelen < len), AKBASIC_ERR_BOUNDS, "Filename exceeds the %zu character limit", len - 1); - strncpy(dest, value->stringval, len - 1); - dest[len - 1] = '\0'; + /* aksl_strcpy always terminates, so the explicit terminator is gone. */ + PASS(errctx, aksl_strcpy(dest, len, value->stringval)); + SUCCEED_RETURN(errctx); +} + +/** + * @brief Read one line, reporting end of stream rather than raising it. + * + * aksl_fgets raises AKERR_EOF where fgets(3) returned NULL, so the end of an + * ordinary file arrives as an error and has to be handled. It is handled here, + * in a function whose ATTEMPT block contains no loop at all, because a CATCH + * inside a loop expands to a break that leaves the loop rather than the + * ATTEMPT. The caller reads @p eof instead. + */ +static akerr_ErrorContext *dload_read_line(FILE *fp, char *buffer, size_t len, bool *eof) +{ + PREPARE_ERROR(errctx); + size_t used = 0; + + *eof = false; + ATTEMPT { + CATCH(errctx, aksl_fgets(buffer, len, fp, &used)); + } CLEANUP { + } PROCESS(errctx) { + } HANDLE(errctx, AKERR_EOF) { + *eof = true; + used = 0; + } FINISH(errctx, true); + + while ( used > 0 && (buffer[used - 1] == '\n' || buffer[used - 1] == '\r') ) { + buffer[used - 1] = '\0'; + used -= 1; + } + SUCCEED_RETURN(errctx); +} + +/** + * @brief Read a whole program in, one line at a time. + * + * Hoisted out of akbasic_cmd_dload()'s ATTEMPT block because a loop inside one + * can use neither CATCH -- which breaks the loop -- nor PASS, which returns + * past CLEANUP and leaves the file open. Out here PASS is correct, and the + * caller CATCHes this one call. + */ +static akerr_ErrorContext *dload_read_program(akbasic_Runtime *obj, FILE *fp, + char *buffer, size_t buflen, + char *scanned, size_t scanlen) +{ + PREPARE_ERROR(errctx); + bool eof = false; + + for ( ;; ) { + PASS(errctx, dload_read_line(fp, buffer, buflen, &eof)); + if ( eof ) { + break; + } + if ( buffer[0] == '\0' ) { + continue; + } + PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, scanlen)); + PASS(errctx, akbasic_runtime_file_line(obj, scanned)); + } + SUCCEED_RETURN(errctx); +} + +/** + * @brief Write every stored line out, numbered. + * + * Hoisted out of akbasic_cmd_dsave()'s ATTEMPT block for the same reason + * dload_read_program() is. + */ +static akerr_ErrorContext *dsave_write_program(akbasic_Runtime *obj, FILE *fp, char *line, size_t len) +{ + PREPARE_ERROR(errctx); + size_t written = 0; + int64_t i = 0; + int count = 0; + + for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { + if ( obj->source[i].code[0] == '\0' ) { + continue; + } + /* + * aksl_snprintf's count is the length it wrote, and it treats + * truncation as an error rather than reporting a length it did not + * write -- so it is what goes to fwrite, and the strlen that used to + * recompute it is gone. + */ + PASS(errctx, aksl_snprintf(&count, line, len, "%" PRId64 " %s\n", i, obj->source[i].code)); + /* + * The written count is required by libakstdlib 0.2.0 and discarded + * here on purpose: a short write is no longer something a caller has + * to notice for itself, because that release made it an AKERR_IO + * rather than a silent success. Before it, a DSAVE onto a full disk + * reported nothing. + */ + PASS(errctx, aksl_fwrite(line, 1, (size_t)count, fp, &written)); + } SUCCEED_RETURN(errctx); } @@ -610,7 +713,6 @@ akerr_ErrorContext *akbasic_cmd_dload(akbasic_Runtime *obj, akbasic_ASTLeaf *exp char buffer[AKBASIC_MAX_LINE_LENGTH]; char scanned[AKBASIC_MAX_LINE_LENGTH]; FILE *fp = NULL; - size_t used = 0; int64_t i = 0; (void)lval; (void)rval; @@ -632,23 +734,14 @@ akerr_ErrorContext *akbasic_cmd_dload(akbasic_Runtime *obj, akbasic_ASTLeaf *exp ATTEMPT { CATCH(errctx, aksl_fopen(filename, "r", &fp)); - while ( fgets(buffer, sizeof(buffer), fp) != NULL ) { - used = strlen(buffer); - while ( used > 0 && (buffer[used - 1] == '\n' || buffer[used - 1] == '\r') ) { - buffer[used - 1] = '\0'; - used -= 1; - } - if ( buffer[0] == '\0' ) { - continue; - } - /* - * PASS inside the loop, never CATCH: CATCH expands to a break, which - * would leave this loop rather than the ATTEMPT and let the rest of - * the block run with an error pending. - */ - PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, sizeof(scanned))); - PASS(errctx, akbasic_runtime_file_line(obj, scanned)); - } + /* + * The read is one CATCH of one call, because the loop it used to be + * lives in dload_read_program() now: inside this block a loop can use + * neither CATCH nor PASS without either escaping the loop or returning + * past the fclose below. + */ + CATCH(errctx, dload_read_program(obj, fp, buffer, sizeof(buffer), + scanned, sizeof(scanned))); } CLEANUP { if ( fp != NULL ) { IGNORE(aksl_fclose(fp)); @@ -676,30 +769,13 @@ akerr_ErrorContext *akbasic_cmd_dsave(akbasic_Runtime *obj, akbasic_ASTLeaf *exp char filename[AKBASIC_MAX_STRING_LENGTH]; char line[AKBASIC_MAX_LINE_LENGTH * 2]; FILE *fp = NULL; - int64_t i = 0; - int count = 0; - size_t written = 0; (void)lval; (void)rval; PASS(errctx, filename_argument(obj, expr, filename, sizeof(filename))); ATTEMPT { CATCH(errctx, aksl_fopen(filename, "w", &fp)); - for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { - if ( obj->source[i].code[0] == '\0' ) { - continue; - } - snprintf(line, sizeof(line), "%" PRId64 " %s\n", i, obj->source[i].code); - /* - * The written count is required by libakstdlib 0.2.0 and discarded - * here on purpose: a short write is no longer something a caller has - * to notice for itself, because that release made it an AKERR_IO - * rather than a silent success. Before it, a DSAVE onto a full disk - * reported nothing. - */ - PASS(errctx, aksl_fwrite(line, 1, strlen(line), fp, &written)); - count += 1; - } + CATCH(errctx, dsave_write_program(obj, fp, line, sizeof(line))); } CLEANUP { if ( fp != NULL ) { IGNORE(aksl_fclose(fp)); @@ -807,6 +883,7 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr akbasic_Value *updated = NULL; akbasic_Value scratch; int64_t zerosubscript[1] = { 0 }; + int cmp = 0; bool met = false; (void)lval; (void)rval; @@ -842,7 +919,9 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr * A NEXT for someone else's loop variable: this environment is done, hand * the line back to the parent and pop. That is how nested loops unwind. */ - if ( strcmp(expr->right->identifier, obj->environment->forNextVariable->name) != 0 ) { + PASS(errctx, aksl_strcmp(expr->right->identifier, + obj->environment->forNextVariable->name, &cmp)); + if ( cmp != 0 ) { FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT, "NEXT in an orphaned environment"); obj->environment->parent->nextline = obj->environment->nextline; @@ -951,7 +1030,8 @@ akerr_ErrorContext *akbasic_cmd_read(akbasic_Runtime *obj, akbasic_ASTLeaf *expr PASS(errctx, akbasic_leaf_init(&literal, AKBASIC_LEAF_LITERAL_INT)); if ( value.valuetype == AKBASIC_TYPE_STRING ) { literal.leaftype = AKBASIC_LEAF_LITERAL_STRING; - snprintf(literal.literal_string, sizeof(literal.literal_string), "%s", value.stringval); + PASS(errctx, aksl_strcpy(literal.literal_string, sizeof(literal.literal_string), + value.stringval)); } else if ( value.valuetype == AKBASIC_TYPE_FLOAT ) { literal.leaftype = AKBASIC_LEAF_LITERAL_FLOAT; literal.literal_float = value.floatval; diff --git a/src/runtime_console.c b/src/runtime_console.c index e29c351..8a1d5e2 100644 --- a/src/runtime_console.c +++ b/src/runtime_console.c @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -114,6 +115,7 @@ akerr_ErrorContext *akbasic_cmd_key(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, /* Room for the longest macro plus `KEY n, ""` around it. */ char line[AKBASIC_MAX_STRING_LENGTH + 32]; int64_t number = 0; + int written = 0; int i = 0; (void)lval; (void)rval; @@ -124,8 +126,8 @@ akerr_ErrorContext *akbasic_cmd_key(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, if ( arg == NULL ) { /* Bare KEY lists the definitions, which is what a C128 does. */ for ( i = 0; i < AKBASIC_MAX_FUNCTION_KEYS; i++ ) { - snprintf(line, sizeof(line), "KEY %d, \"%s\"", - i + 1, obj->console_state.keys[i]); + PASS(errctx, aksl_snprintf(&written, line, sizeof(line), "KEY %d, \"%s\"", + i + 1, obj->console_state.keys[i])); PASS(errctx, akbasic_runtime_println(obj, line)); } SUCCEED_TRUE(obj, dest); @@ -146,8 +148,8 @@ akerr_ErrorContext *akbasic_cmd_key(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value)); FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE, "KEY expected a string"); - snprintf(obj->console_state.keys[number - 1], - sizeof(obj->console_state.keys[0]), "%s", value->stringval); + PASS(errctx, aksl_snprintf(&written, obj->console_state.keys[number - 1], + sizeof(obj->console_state.keys[0]), "%s", value->stringval)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } @@ -253,7 +255,7 @@ akerr_ErrorContext *akbasic_console_state_init(akbasic_ConsoleState *obj) PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL console state in init"); - memset(obj, 0, sizeof(*obj)); + PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); SUCCEED_RETURN(errctx); } @@ -302,6 +304,7 @@ akerr_ErrorContext *akbasic_console_update_clock(akbasic_Runtime *obj) int64_t jiffies = 0; int64_t seconds = 0; char text[16]; + int written = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in update_clock"); if ( obj->environment == NULL ) { @@ -324,8 +327,9 @@ akerr_ErrorContext *akbasic_console_update_clock(akbasic_Runtime *obj) PASS(errctx, akbasic_variable_set_integer(variable, jiffies, zerosubscript, 1)); seconds = obj->timems / 1000; - snprintf(text, sizeof(text), "%02" PRId64 "%02" PRId64 "%02" PRId64, - (seconds / 3600) % 24, (seconds / 60) % 60, seconds % 60); + PASS(errctx, aksl_snprintf(&written, text, sizeof(text), + "%02" PRId64 "%02" PRId64 "%02" PRId64, + (seconds / 3600) % 24, (seconds / 60) % 60, seconds % 60)); PASS(errctx, akbasic_runtime_global(obj, "TI$", &variable)); PASS(errctx, akbasic_variable_set_string(variable, text, zerosubscript, 1)); SUCCEED_RETURN(errctx); diff --git a/src/runtime_disk.c b/src/runtime_disk.c index a34b2f4..2abea6e 100644 --- a/src/runtime_disk.c +++ b/src/runtime_disk.c @@ -22,7 +22,6 @@ #include #include -#include #include #include @@ -44,7 +43,7 @@ akerr_ErrorContext *akbasic_disk_state_init(akbasic_DiskState *obj) PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL disk state in init"); - memset(obj, 0, sizeof(*obj)); + PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); SUCCEED_RETURN(errctx); } @@ -103,7 +102,12 @@ static akerr_ErrorContext *string_arg(akbasic_Runtime *obj, akbasic_ASTLeaf *arg PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value)); FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE, "%s expected a file name", verb); - snprintf(dest, len, "%s", value->stringval); + /* + * A plain copy, so aksl_strcpy rather than aksl_snprintf: it refuses rather + * than truncates, and every caller here hands in a buffer the size of a + * string value, so a name that did not fit could only ever be a bug. + */ + PASS(errctx, aksl_strcpy(dest, len, value->stringval)); SUCCEED_RETURN(errctx); } @@ -157,7 +161,7 @@ static akerr_ErrorContext *open_file(akbasic_Runtime *obj, akbasic_ASTLeaf *expr } PASS(errctx, aksl_fopen(name, mode, &channel->fp)); - snprintf(channel->name, sizeof(channel->name), "%s", name); + PASS(errctx, aksl_strcpy(channel->name, sizeof(channel->name), name)); channel->writing = writing; SUCCEED_RETURN(errctx); } @@ -265,13 +269,24 @@ akerr_ErrorContext *akbasic_cmd_record(akbasic_Runtime *obj, akbasic_ASTLeaf *ex akerr_ErrorContext *akbasic_cmd_scratch(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) { PREPARE_ERROR(errctx); + akerr_ErrorContext *failure = NULL; char name[AKBASIC_MAX_STRING_LENGTH]; (void)lval; (void)rval; FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in SCRATCH"); PASS(errctx, string_arg(obj, akbasic_leaf_first_argument(expr), "SCRATCH", name, sizeof(name))); - FAIL_ZERO_RETURN(errctx, (remove(name) == 0), AKERR_IO, - "SCRATCH could not delete \"%s\"", name); + /* + * Caught and restated rather than passed. aksl_remove raises the bare errno, + * and "No such file or directory" on its own does not say which file or + * which verb wanted it -- so the wrapper's error is retired here and the + * message the program sees keeps naming both. + */ + failure = aksl_remove(name); + if ( failure != NULL ) { + failure->handled = true; + IGNORE(akerr_release_error(failure)); + FAIL_RETURN(errctx, AKERR_IO, "SCRATCH could not delete \"%s\"", name); + } SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } @@ -279,6 +294,7 @@ akerr_ErrorContext *akbasic_cmd_scratch(akbasic_Runtime *obj, akbasic_ASTLeaf *e akerr_ErrorContext *akbasic_cmd_rename(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) { PREPARE_ERROR(errctx); + akerr_ErrorContext *failure = NULL; akbasic_ASTLeaf *arg = NULL; char from[AKBASIC_MAX_STRING_LENGTH]; char to[AKBASIC_MAX_STRING_LENGTH]; @@ -288,8 +304,13 @@ akerr_ErrorContext *akbasic_cmd_rename(akbasic_Runtime *obj, akbasic_ASTLeaf *ex arg = akbasic_leaf_first_argument(expr); PASS(errctx, string_arg(obj, arg, "RENAME", from, sizeof(from))); PASS(errctx, string_arg(obj, (arg != NULL ? arg->next : NULL), "RENAME", to, sizeof(to))); - FAIL_ZERO_RETURN(errctx, (rename(from, to) == 0), AKERR_IO, - "RENAME could not rename \"%s\" to \"%s\"", from, to); + /* Caught and restated, for the reason SCRATCH gives: the message names both files. */ + failure = aksl_rename(from, to); + if ( failure != NULL ) { + failure->handled = true; + IGNORE(akerr_release_error(failure)); + FAIL_RETURN(errctx, AKERR_IO, "RENAME could not rename \"%s\" to \"%s\"", from, to); + } SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } @@ -486,6 +507,7 @@ akerr_ErrorContext *akbasic_disk_write(akbasic_Runtime *obj, int64_t number, con { PREPARE_ERROR(errctx); akbasic_Channel *channel = NULL; + size_t length = 0; size_t put = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL && text != NULL), AKERR_NULLPOINTER, @@ -493,7 +515,8 @@ akerr_ErrorContext *akbasic_disk_write(akbasic_Runtime *obj, int64_t number, con PASS(errctx, open_channel(obj, number, &channel)); FAIL_ZERO_RETURN(errctx, channel->writing, AKBASIC_ERR_STATE, "Channel %" PRId64 " was opened for reading", number); - PASS(errctx, aksl_fwrite(text, 1, strlen(text), channel->fp, &put)); + PASS(errctx, aksl_strlen(text, &length)); + PASS(errctx, aksl_fwrite(text, 1, length, channel->fp, &put)); PASS(errctx, aksl_fwrite("\n", 1, 1, channel->fp, &put)); SUCCEED_RETURN(errctx); } @@ -525,7 +548,7 @@ akerr_ErrorContext *akbasic_disk_readline(akbasic_Runtime *obj, int64_t number, *eof = true; SUCCEED_RETURN(errctx); } - newline = strchr(dest, '\n'); + PASS(errctx, aksl_strchr(dest, '\n', &newline)); if ( newline != NULL ) { *newline = '\0'; } @@ -582,7 +605,7 @@ akerr_ErrorContext *akbasic_cmd_input_channel(akbasic_Runtime *obj, akbasic_ASTL * variable reading an empty line gets zero, the same as a C128. */ PASS(errctx, akbasic_leaf_init(&literal, AKBASIC_LEAF_LITERAL_STRING)); - snprintf(literal.literal_string, sizeof(literal.literal_string), "%s", text); + PASS(errctx, aksl_strcpy(literal.literal_string, sizeof(literal.literal_string), text)); if ( akbasic_leaf_identifier_type(identifier) == AKBASIC_TYPE_INTEGER ) { long long converted = 0; @@ -610,13 +633,69 @@ akerr_ErrorContext *akbasic_cmd_input_channel(akbasic_Runtime *obj, akbasic_ASTL /* ---------------------------------------------------------------- VERIFY -- */ +/** + * @brief Count the lines of @p fp that differ from the program in memory. + * + * Its own function so that the comparison can be reached with a single CATCH. + * Written inline it would be a loop inside VERIFY's ATTEMPT, where neither form + * is available: CATCH expands to a break that would escape only the loop, and + * PASS returns past the CLEANUP that closes the file. One call is neither, so + * the wrappers below can PASS in the ordinary way. + */ +static akerr_ErrorContext *verify_lines(akbasic_Runtime *obj, FILE *fp, int64_t *dest) +{ + PREPARE_ERROR(errctx); + akerr_ErrorContext *got = NULL; + char line[AKBASIC_MAX_LINE_LENGTH]; + const char *filetext = NULL; + char *newline = NULL; + int64_t lineno = 0; + size_t length = 0; + int cmp = 0; + + *dest = 0; + for ( lineno = 0; lineno < AKBASIC_MAX_SOURCE_LINES; lineno++ ) { + if ( obj->source[lineno].code[0] == '\0' ) { + continue; + } + got = aksl_fgets(line, sizeof(line), fp, &length); + if ( got != NULL ) { + got->handled = true; + IGNORE(akerr_release_error(got)); + *dest += 1; + break; + } + PASS(errctx, aksl_strchr(line, '\n', &newline)); + if ( newline != NULL ) { + *newline = '\0'; + } + /* + * The stored line has had its number stripped, so the file's copy is + * compared from past its own number. + */ + filetext = line; + while ( *filetext == ' ' ) { + filetext += 1; + } + while ( *filetext >= '0' && *filetext <= '9' ) { + filetext += 1; + } + while ( *filetext == ' ' ) { + filetext += 1; + } + PASS(errctx, aksl_strcmp(filetext, obj->source[lineno].code, &cmp)); + if ( cmp != 0 ) { + *dest += 1; + } + } + SUCCEED_RETURN(errctx); +} + akerr_ErrorContext *akbasic_cmd_dverify(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) { PREPARE_ERROR(errctx); FILE *fp = NULL; char name[AKBASIC_MAX_STRING_LENGTH]; - char line[AKBASIC_MAX_LINE_LENGTH]; - int64_t lineno = 0; int64_t mismatch = 0; (void)lval; (void)rval; @@ -632,47 +711,7 @@ akerr_ErrorContext *akbasic_cmd_dverify(akbasic_Runtime *obj, akbasic_ASTLeaf *e */ PASS(errctx, aksl_fopen(name, "r", &fp)); ATTEMPT { - for ( lineno = 0; lineno < AKBASIC_MAX_SOURCE_LINES; lineno++ ) { - size_t length = 0; - akerr_ErrorContext *got = NULL; - char *newline = NULL; - - if ( obj->source[lineno].code[0] == '\0' ) { - continue; - } - got = aksl_fgets(line, sizeof(line), fp, &length); - if ( got != NULL ) { - got->handled = true; - IGNORE(akerr_release_error(got)); - mismatch += 1; - break; - } - newline = strchr(line, '\n'); - if ( newline != NULL ) { - *newline = '\0'; - } - /* - * The stored line has had its number stripped, so the file's copy is - * compared from past its own number. PASS rather than CATCH: this is - * a loop. - */ - { - const char *filetext = line; - - while ( *filetext == ' ' ) { - filetext += 1; - } - while ( *filetext >= '0' && *filetext <= '9' ) { - filetext += 1; - } - while ( *filetext == ' ' ) { - filetext += 1; - } - if ( strcmp(filetext, obj->source[lineno].code) != 0 ) { - mismatch += 1; - } - } - } + CATCH(errctx, verify_lines(obj, fp, &mismatch)); } CLEANUP { IGNORE(aksl_fclose(fp)); } PROCESS(errctx) { diff --git a/src/runtime_functions.c b/src/runtime_functions.c index e63cb3b..31ebf2d 100644 --- a/src/runtime_functions.c +++ b/src/runtime_functions.c @@ -14,7 +14,6 @@ #include #include #include -#include #include #include @@ -189,13 +188,19 @@ akerr_ErrorContext *akbasic_fn_hex(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, PREPARE_ERROR(errctx); akbasic_Value *arg = NULL; akbasic_Value *out = NULL; + int written = 0; (void)lval; (void)rval; PASS(errctx, first_arg(obj, expr, "HEX", NULL, &arg, &out)); FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE, "HEX expected an integer"); out->valuetype = AKBASIC_TYPE_STRING; - snprintf(out->stringval, sizeof(out->stringval), "%" PRIx64, arg->intval); + /* + * aksl_snprintf refuses to truncate, which costs nothing here: sixteen hex + * digits is the longest an int64_t can be and a string value holds far more. + */ + PASS(errctx, aksl_snprintf(&written, out->stringval, sizeof(out->stringval), + "%" PRIx64, arg->intval)); *dest = out; SUCCEED_RETURN(errctx); } @@ -270,6 +275,7 @@ akerr_ErrorContext *akbasic_fn_len(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *strval = NULL; akbasic_Value *out = NULL; akbasic_Variable *variable = NULL; + size_t length = 0; (void)lval; (void)rval; /* @@ -291,7 +297,8 @@ akerr_ErrorContext *akbasic_fn_len(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, if ( argleaf->leaftype == AKBASIC_LEAF_LITERAL_STRING || argleaf->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING ) { PASS(errctx, akbasic_runtime_evaluate(obj, argleaf, &strval)); - out->intval = (int64_t)strlen(strval->stringval); + PASS(errctx, aksl_strlen(strval->stringval, &length)); + out->intval = (int64_t)length; } else { PASS(errctx, akbasic_environment_get(obj->environment, argleaf->identifier, &variable)); FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED, @@ -338,7 +345,8 @@ akerr_ErrorContext *akbasic_fn_instr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr PASS(errctx, akbasic_environment_new_value(obj->environment, &out)); PASS(errctx, akbasic_value_zero(out)); out->valuetype = AKBASIC_TYPE_INTEGER; - hit = strstr(haystack->stringval, needle->stringval); + /* Not finding it is a successful NULL, which is exactly the -1 answer below. */ + PASS(errctx, aksl_strstr(haystack->stringval, needle->stringval, &hit)); out->intval = (hit == NULL ? -1 : (int64_t)(hit - haystack->stringval)); *dest = out; SUCCEED_RETURN(errctx); @@ -360,7 +368,7 @@ akerr_ErrorContext *akbasic_fn_left(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, "LEFT expected a string"); FAIL_NONZERO_RETURN(errctx, (count->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE, "LEFT expected an integer count"); - sourcelen = strlen(source->stringval); + PASS(errctx, aksl_strlen(source->stringval, &sourcelen)); take = count->intval; if ( take < 0 ) { take = 0; @@ -371,7 +379,7 @@ akerr_ErrorContext *akbasic_fn_left(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, PASS(errctx, akbasic_environment_new_value(obj->environment, &out)); PASS(errctx, akbasic_value_zero(out)); out->valuetype = AKBASIC_TYPE_STRING; - memcpy(out->stringval, source->stringval, (size_t)take); + PASS(errctx, aksl_memcpy(out->stringval, source->stringval, (size_t)take)); out->stringval[take] = '\0'; *dest = out; SUCCEED_RETURN(errctx); @@ -393,7 +401,7 @@ akerr_ErrorContext *akbasic_fn_right(akbasic_Runtime *obj, akbasic_ASTLeaf *expr "RIGHT expected a string"); FAIL_NONZERO_RETURN(errctx, (count->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE, "RIGHT expected an integer count"); - sourcelen = strlen(source->stringval); + PASS(errctx, aksl_strlen(source->stringval, &sourcelen)); take = count->intval; if ( take < 0 ) { take = 0; @@ -404,7 +412,8 @@ akerr_ErrorContext *akbasic_fn_right(akbasic_Runtime *obj, akbasic_ASTLeaf *expr PASS(errctx, akbasic_environment_new_value(obj->environment, &out)); PASS(errctx, akbasic_value_zero(out)); out->valuetype = AKBASIC_TYPE_STRING; - memcpy(out->stringval, source->stringval + (sourcelen - (size_t)take), (size_t)take); + PASS(errctx, aksl_memcpy(out->stringval, source->stringval + (sourcelen - (size_t)take), + (size_t)take)); out->stringval[take] = '\0'; *dest = out; SUCCEED_RETURN(errctx); @@ -432,7 +441,7 @@ akerr_ErrorContext *akbasic_fn_mid(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, lengthval->valuetype == AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE, "MID expected integer start and length"); - sourcelen = strlen(source->stringval); + PASS(errctx, aksl_strlen(source->stringval, &sourcelen)); start = startval->intval; length = lengthval->intval; FAIL_ZERO_RETURN(errctx, (start >= 0 && (size_t)start <= sourcelen), AKBASIC_ERR_BOUNDS, @@ -446,7 +455,7 @@ akerr_ErrorContext *akbasic_fn_mid(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, PASS(errctx, akbasic_environment_new_value(obj->environment, &out)); PASS(errctx, akbasic_value_zero(out)); out->valuetype = AKBASIC_TYPE_STRING; - memcpy(out->stringval, source->stringval + start, (size_t)length); + PASS(errctx, aksl_memcpy(out->stringval, source->stringval + start, (size_t)length)); out->stringval[length] = '\0'; *dest = out; SUCCEED_RETURN(errctx); diff --git a/src/runtime_graphics.c b/src/runtime_graphics.c index d2eaf17..9771b9d 100644 --- a/src/runtime_graphics.c +++ b/src/runtime_graphics.c @@ -709,6 +709,7 @@ akerr_ErrorContext *akbasic_cmd_sshape(akbasic_Runtime *obj, akbasic_ASTLeaf *ex char encoded[AKBASIC_MAX_STRING_LENGTH]; int64_t subscripts[AKBASIC_MAX_ARRAY_DEPTH]; int subscriptcount = 0; + int written = 0; (void)lval; (void)rval; PASS(errctx, require_graphics(obj, "SSHAPE")); @@ -742,7 +743,7 @@ akerr_ErrorContext *akbasic_cmd_sshape(akbasic_Runtime *obj, akbasic_ASTLeaf *ex * BASIC ever does with it; what it can no longer do is save it to disk or * take its LEN and get a size. TODO.md section 5. */ - snprintf(encoded, sizeof(encoded), "%s%d", SHAPE_PREFIX, handle); + PASS(errctx, aksl_snprintf(&written, encoded, sizeof(encoded), "%s%d", SHAPE_PREFIX, handle)); PASS(errctx, akbasic_variable_set_string(variable, encoded, subscripts, subscriptcount)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); @@ -762,7 +763,9 @@ akerr_ErrorContext *akbasic_cmd_gshape(akbasic_Runtime *obj, akbasic_ASTLeaf *ex double x = 0.0; double y = 0.0; int64_t subscripts[AKBASIC_MAX_ARRAY_DEPTH]; + size_t prefixlen = 0; int subscriptcount = 0; + int cmp = 0; (void)lval; (void)rval; PASS(errctx, require_graphics(obj, "GSHAPE")); @@ -782,11 +785,11 @@ akerr_ErrorContext *akbasic_cmd_gshape(akbasic_Runtime *obj, akbasic_ASTLeaf *ex y = (count >= 2) ? coords[1] : obj->gfx.y; PASS(errctx, akbasic_variable_get_subscript(variable, subscripts, subscriptcount, &stored)); - FAIL_ZERO_RETURN(errctx, - (strncmp(stored->stringval, SHAPE_PREFIX, strlen(SHAPE_PREFIX)) == 0), - AKBASIC_ERR_VALUE, + PASS(errctx, aksl_strlen(SHAPE_PREFIX, &prefixlen)); + PASS(errctx, aksl_strncmp(stored->stringval, SHAPE_PREFIX, prefixlen, &cmp)); + FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_VALUE, "GSHAPE was given a string that did not come from SSHAPE"); - PASS(errctx, aksl_atoll(stored->stringval + strlen(SHAPE_PREFIX), &converted)); + PASS(errctx, aksl_atoll(stored->stringval + prefixlen, &converted)); handle = (int64_t)converted; scale_point(&obj->gfx, &x, &y); diff --git a/src/runtime_housekeeping.c b/src/runtime_housekeeping.c index 4639855..3b998e3 100644 --- a/src/runtime_housekeeping.c +++ b/src/runtime_housekeeping.c @@ -13,10 +13,9 @@ */ #include -#include -#include #include +#include #include #include @@ -322,13 +321,13 @@ akerr_ErrorContext *akbasic_cmd_swap(akbasic_Runtime *obj, akbasic_ASTLeaf *expr * exactly this reason. The names stay put: they are what the symbol table * points at. */ - memcpy(namea, a->name, sizeof(namea)); - memcpy(nameb, b->name, sizeof(nameb)); - memcpy(&swap, a, sizeof(swap)); - memcpy(a, b, sizeof(*a)); - memcpy(b, &swap, sizeof(*b)); - memcpy(a->name, namea, sizeof(a->name)); - memcpy(b->name, nameb, sizeof(b->name)); + PASS(errctx, aksl_memcpy(namea, a->name, sizeof(namea))); + PASS(errctx, aksl_memcpy(nameb, b->name, sizeof(nameb))); + PASS(errctx, aksl_memcpy(&swap, a, sizeof(swap))); + PASS(errctx, aksl_memcpy(a, b, sizeof(*a))); + PASS(errctx, aksl_memcpy(b, &swap, sizeof(*b))); + PASS(errctx, aksl_memcpy(a->name, namea, sizeof(a->name))); + PASS(errctx, aksl_memcpy(b->name, nameb, sizeof(b->name))); /* * A scalar's storage is *inside* the record, so the pointer that came over * with it names the other variable's inline slot -- which now holds this @@ -352,6 +351,7 @@ akerr_ErrorContext *akbasic_cmd_help(akbasic_Runtime *obj, akbasic_ASTLeaf *expr { PREPARE_ERROR(errctx); char line[AKBASIC_MAX_LINE_LENGTH * 2]; + int written = 0; (void)expr; (void)lval; (void)rval; FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in HELP"); @@ -369,8 +369,14 @@ akerr_ErrorContext *akbasic_cmd_help(akbasic_Runtime *obj, akbasic_ASTLeaf *expr SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } - snprintf(line, sizeof(line), "%" PRId64 " %s", - obj->errorline, obj->source[obj->errorline].code); + /* + * aksl_snprintf treats truncation as an error, and this destination is twice + * AKBASIC_MAX_LINE_LENGTH against a stored line that is at most one of them + * plus a line number, so the fit is a property of the buffer rather than + * something to check for. + */ + PASS(errctx, aksl_snprintf(&written, line, sizeof(line), "%" PRId64 " %s", + obj->errorline, obj->source[obj->errorline].code)); PASS(errctx, akbasic_runtime_println(obj, line)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); diff --git a/src/runtime_input.c b/src/runtime_input.c index beaa5c0..c93e3bc 100644 --- a/src/runtime_input.c +++ b/src/runtime_input.c @@ -14,9 +14,8 @@ * as sitting on the keyboard. See TODO.md section 5. */ -#include - #include +#include #include #include @@ -152,9 +151,8 @@ akerr_ErrorContext *akbasic_cmd_getkey(akbasic_Runtime *obj, akbasic_ASTLeaf *ex } obj->input_state.waiting = true; obj->input_state.numeric = numeric; - strncpy(obj->input_state.variable, arg->identifier, - sizeof(obj->input_state.variable) - 1); - obj->input_state.variable[sizeof(obj->input_state.variable) - 1] = '\0'; + PASS(errctx, aksl_strcpy(obj->input_state.variable, + sizeof(obj->input_state.variable), arg->identifier)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } diff --git a/src/runtime_machine.c b/src/runtime_machine.c index 535d197..232b0d2 100644 --- a/src/runtime_machine.c +++ b/src/runtime_machine.c @@ -14,9 +14,9 @@ #include #include -#include #include +#include #include #include @@ -69,7 +69,8 @@ static akerr_ErrorContext *copy_bytes(akbasic_Runtime *obj, akbasic_ASTLeaf *exp * POKE and PEEK. A BASIC integer here is a real address, so a wrong one is a * segmentation fault rather than an error message. See TODO.md section 5. */ - memmove((void *)(uintptr_t)args[2], (const void *)(uintptr_t)args[1], (size_t)length); + PASS(errctx, aksl_memmove((void *)(uintptr_t)args[2], (const void *)(uintptr_t)args[1], + (size_t)length)); SUCCEED_RETURN(errctx); } diff --git a/src/runtime_sprite.c b/src/runtime_sprite.c index 2da4a3f..9927587 100644 --- a/src/runtime_sprite.c +++ b/src/runtime_sprite.c @@ -31,6 +31,7 @@ #include #include +#include #include #include @@ -403,6 +404,9 @@ static akerr_ErrorContext *define_from_leaf(akbasic_Runtime *obj, akbasic_ASTLea akbasic_Color fg; akbasic_Color bg; uint8_t pattern[AKBASIC_SPRITE_PATTERN_BYTES]; + size_t prefixlen = 0; + int handle = 0; + int cmp = 0; if ( arg->leaftype == AKBASIC_LEAF_IDENTIFIER_INT && akbasic_leaf_first_subscript(arg) == NULL ) { @@ -428,11 +432,17 @@ static akerr_ErrorContext *define_from_leaf(akbasic_Runtime *obj, akbasic_ASTLea FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE, "SPRSAV expected a sprite number, an image path, a saved shape or an integer array"); - if ( strncmp(value->stringval, SHAPE_PREFIX, strlen(SHAPE_PREFIX)) == 0 ) { - int handle = atoi(value->stringval + strlen(SHAPE_PREFIX)); - + PASS(errctx, aksl_strlen(SHAPE_PREFIX, &prefixlen)); + PASS(errctx, aksl_strncmp(value->stringval, SHAPE_PREFIX, prefixlen, &cmp)); + if ( cmp == 0 ) { FAIL_ZERO_RETURN(errctx, (obj->sprites->define_shape != NULL), AKBASIC_ERR_DEVICE, "This sprite device cannot take a saved shape"); + /* + * aksl_atoi refuses a slot number that is not one, where atoi(3) answered + * a silent 0 -- so `SPRSAV "SHAPE:", 1` is now told what is wrong with it + * rather than being handed shape 0. + */ + PASS(errctx, aksl_atoi(value->stringval + prefixlen, &handle)); PASS(errctx, obj->sprites->define_shape(obj->sprites, index + 1, handle)); SUCCEED_RETURN(errctx); } @@ -779,8 +789,9 @@ akerr_ErrorContext *akbasic_cmd_solid(akbasic_Runtime *obj, akbasic_ASTLeaf *exp if ( !obj->sprite_state.solids[i].active ) { continue; } - memset(&obj->sprite_state.solids[i], 0, sizeof(obj->sprite_state.solids[i])); /* PASS rather than CATCH: this is a loop. */ + PASS(errctx, aksl_memset(&obj->sprite_state.solids[i], 0, + sizeof(obj->sprite_state.solids[i]))); PASS(errctx, obj->sprites->solid(obj->sprites, i + 1, false, 0.0, 0.0, 0.0, 0.0)); } SUCCEED_RETURN(errctx); @@ -792,7 +803,7 @@ akerr_ErrorContext *akbasic_cmd_solid(akbasic_Runtime *obj, akbasic_ASTLeaf *exp solid = &obj->sprite_state.solids[id - 1]; if ( count == 1 ) { - memset(solid, 0, sizeof(*solid)); + PASS(errctx, aksl_memset(solid, 0, sizeof(*solid))); PASS(errctx, obj->sprites->solid(obj->sprites, id, false, 0.0, 0.0, 0.0, 0.0)); SUCCEED_RETURN(errctx); } diff --git a/src/runtime_struct.c b/src/runtime_struct.c index 24304aa..37aa584 100644 --- a/src/runtime_struct.c +++ b/src/runtime_struct.c @@ -28,6 +28,7 @@ #include #include +#include #include #include @@ -194,7 +195,11 @@ akerr_ErrorContext *akbasic_struct_to_string(akbasic_Runtime *obj, int typeindex PREPARE_ERROR(errctx); char rendered[AKBASIC_MAX_STRING_LENGTH]; akbasic_StructType *type = NULL; + const char *separator = NULL; size_t used = 0; + size_t seplen = 0; + size_t namelen = 0; + size_t renderedlen = 0; int written = 0; int i = 0; @@ -204,6 +209,20 @@ akerr_ErrorContext *akbasic_struct_to_string(akbasic_Runtime *obj, int typeindex AKBASIC_ERR_BOUNDS, "Structure type index %d is out of range", typeindex); type = &obj->structtypes.types[typeindex]; + /* + * These two stay on raw snprintf on purpose, and are two of the six such + * sites left in src/. Both want truncation as an *answer*: `dest` and `len` + * come from the caller, this renders a value for display, and a name too long + * for the buffer should print short rather than fail the whole PRINT. The + * second one reads the return value to detect it -- on overflow `used` lands + * past `len`, which is exactly what makes the field loop below and the + * closing ")" both skip. + * + * aksl_snprintf treats truncation as AKERR_OUTOFBOUNDS and its `count` is 0 + * on that path, so it can express neither the tolerance nor the detection. + * libakstdlib #34 is the issue that would give `count` the required length + * back; until it lands there is nothing to convert these to. + */ if ( depth >= AKBASIC_MAX_STRUCT_DEPTH ) { snprintf(dest, len, "%s(...)", type->name); SUCCEED_RETURN(errctx); @@ -222,7 +241,7 @@ akerr_ErrorContext *akbasic_struct_to_string(akbasic_Runtime *obj, int typeindex break; case AKBASIC_FIELD_POINTER: if ( slot->structbase == NULL ) { - snprintf(rendered, sizeof(rendered), "NOTHING"); + PASS(errctx, aksl_snprintf(&written, rendered, sizeof(rendered), "NOTHING")); } else { PASS(errctx, akbasic_struct_to_string(obj, slot->structtype, slot->structbase, depth + 1, rendered, sizeof(rendered))); @@ -232,15 +251,24 @@ akerr_ErrorContext *akbasic_struct_to_string(akbasic_Runtime *obj, int typeindex PASS(errctx, akbasic_value_to_string(slot, rendered, sizeof(rendered))); break; } - written = snprintf(dest + used, len - used, "%s%s=%s", - (i == 0 ? "" : ", "), field->name, rendered); - if ( written < 0 || (size_t)written >= len - used ) { + separator = (i == 0 ? "" : ", "); + PASS(errctx, aksl_strlen(separator, &seplen)); + PASS(errctx, aksl_strlen(field->name, &namelen)); + PASS(errctx, aksl_strlen(rendered, &renderedlen)); + /* + * A render that does not fit ends the list rather than failing it, so the + * fit is decided here -- aksl_snprintf treats truncation as an error, and + * the one byte counted beyond the three lengths is the `=`. + */ + if ( used + seplen + namelen + 1 + renderedlen >= len ) { break; } + PASS(errctx, aksl_snprintf(&written, dest + used, len - used, "%s%s=%s", + separator, field->name, rendered)); used += (size_t)written; } if ( used + 1 < len ) { - snprintf(dest + used, len - used, ")"); + PASS(errctx, aksl_snprintf(&written, dest + used, len - used, ")")); } SUCCEED_RETURN(errctx); } diff --git a/src/runtime_structure.c b/src/runtime_structure.c index e860185..3f72484 100644 --- a/src/runtime_structure.c +++ b/src/runtime_structure.c @@ -173,6 +173,7 @@ akerr_ErrorContext *akbasic_cmd_begin(akbasic_Runtime *obj, akbasic_ASTLeaf *exp akerr_ErrorContext *akbasic_cmd_bend(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) { PREPARE_ERROR(errctx); + bool waiting = false; (void)expr; (void)lval; (void)rval; FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, @@ -182,7 +183,8 @@ akerr_ErrorContext *akbasic_cmd_bend(akbasic_Runtime *obj, akbasic_ASTLeaf *expr * to do, or it is the BEND a skipped block was skipping to, in which case * stopping the skip is the whole job. */ - if ( akbasic_environment_is_waiting_for(obj->environment, "BEND") ) { + PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "BEND", &waiting)); + if ( waiting ) { PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "BEND")); } SUCCEED_TRUE(obj, dest); diff --git a/src/runtime_trap.c b/src/runtime_trap.c index 007b8e1..4e929b2 100644 --- a/src/runtime_trap.c +++ b/src/runtime_trap.c @@ -16,10 +16,9 @@ */ #include -#include -#include #include +#include #include #include @@ -107,6 +106,8 @@ akerr_ErrorContext *akbasic_cmd_resume(akbasic_Runtime *obj, akbasic_ASTLeaf *ex akbasic_Variable *variable = NULL; int64_t zerosubscript[1] = { 0 }; int64_t resumeline = 0; + int cmp = 0; + bool isnext = false; (void)lval; (void)rval; FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, @@ -124,6 +125,10 @@ akerr_ErrorContext *akbasic_cmd_resume(akbasic_Runtime *obj, akbasic_ASTLeaf *ex resumeline = value->intval; target = (expr != NULL ? expr->right : NULL); + if ( target != NULL && target->leaftype == AKBASIC_LEAF_COMMAND ) { + PASS(errctx, aksl_strcmp(target->identifier, "NEXT", &cmp)); + isnext = (cmp == 0); + } if ( target == NULL ) { /* * Bare RESUME retries the line that failed. That is a loop unless the @@ -131,8 +136,7 @@ akerr_ErrorContext *akbasic_cmd_resume(akbasic_Runtime *obj, akbasic_ASTLeaf *ex * is exactly what the verb is for. */ obj->environment->parent->nextline = resumeline; - } else if ( target->leaftype == AKBASIC_LEAF_COMMAND && - strcmp(target->identifier, "NEXT") == 0 ) { + } else if ( isnext ) { /* RESUME NEXT carries on at the line after the one that failed. */ obj->environment->parent->nextline = resumeline + 1; } else { @@ -182,8 +186,8 @@ akerr_ErrorContext *akbasic_fn_err(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, * that ever decides otherwise. */ name = akerr_name_for_status((int)status, NULL); - snprintf(out->stringval, sizeof(out->stringval), "%s", - (name != NULL ? name : "Unknown Error")); + PASS(errctx, aksl_strcpy(out->stringval, sizeof(out->stringval), + (name != NULL ? name : "Unknown Error"))); *dest = out; SUCCEED_RETURN(errctx); } diff --git a/src/runtime_ui.c b/src/runtime_ui.c index 77093fb..19de8e4 100644 --- a/src/runtime_ui.c +++ b/src/runtime_ui.c @@ -19,9 +19,9 @@ */ #include -#include #include +#include #include #include @@ -153,8 +153,7 @@ static akerr_ErrorContext *nth_string(akbasic_Runtime *obj, akbasic_ASTLeaf *exp PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value)); FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE, "%s expected a string in argument %d", verb, n + 1); - strncpy(dest, value->stringval, len - 1); - dest[len - 1] = '\0'; + PASS(errctx, aksl_strcpy(dest, len, value->stringval)); SUCCEED_RETURN(errctx); } @@ -259,8 +258,7 @@ static akerr_ErrorContext *target_variable(akbasic_Runtime *obj, akbasic_ASTLeaf PASS(errctx, akbasic_environment_get(obj->environment, arg->identifier, dest)); FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKBASIC_ERR_UNDEFINED, "GETMENU could not reach the variable %s", arg->identifier); - strncpy(name, arg->identifier, len - 1); - name[len - 1] = '\0'; + PASS(errctx, aksl_strcpy(name, len, arg->identifier)); SUCCEED_RETURN(errctx); } @@ -309,8 +307,7 @@ akerr_ErrorContext *akbasic_cmd_getmenu(akbasic_Runtime *obj, akbasic_ASTLeaf *e */ obj->ui_state.waiting = true; obj->ui_state.waitmenu = slot; - strncpy(obj->ui_state.variable, name, sizeof(obj->ui_state.variable) - 1); - obj->ui_state.variable[sizeof(obj->ui_state.variable) - 1] = '\0'; + PASS(errctx, aksl_strcpy(obj->ui_state.variable, sizeof(obj->ui_state.variable), name)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } @@ -487,7 +484,7 @@ akerr_ErrorContext *akbasic_ui_state_init(akbasic_UiState *obj) FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL argument in ui_state_init"); - memset(obj, 0, sizeof(*obj)); + PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); SUCCEED_RETURN(errctx); } diff --git a/src/scanner.c b/src/scanner.c index 637e7ae..2f7ed40 100644 --- a/src/scanner.c +++ b/src/scanner.c @@ -26,27 +26,67 @@ akerr_ErrorContext *akbasic_scanner_zero(akbasic_Runtime *obj) SUCCEED_RETURN(errctx); } -static bool is_at_end(akbasic_Runtime *obj) +/** + * @brief Is the cursor past the end of the line? + * + * The answer leaves through @p dest rather than the return value because + * measuring the line can fail, and a `bool` has nowhere to put that. Same shape + * as symtab.c's `probe`, and for the same reason. See libakstdlib #38. + */ +static akerr_ErrorContext *is_at_end(akbasic_Runtime *obj, bool *dest) { - return (obj->current >= (int)strlen(obj->line)); + PREPARE_ERROR(errctx); + size_t linelen = 0; + + FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, + "NULL argument in is_at_end"); + PASS(errctx, aksl_strlen(obj->line, &linelen)); + *dest = (obj->current >= (int)linelen); + SUCCEED_RETURN(errctx); } -static bool peek(akbasic_Runtime *obj, char *dest) +/** + * @brief The character under the cursor. + * @param[out] dest The character. Untouched when there is none. + * @param[out] got Whether there was one. The old `bool` return. + */ +static akerr_ErrorContext *peek(akbasic_Runtime *obj, char *dest, bool *got) { - if ( is_at_end(obj) ) { - return false; + PREPARE_ERROR(errctx); + bool atend = false; + + FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL && got != NULL), AKERR_NULLPOINTER, + "NULL argument in peek"); + PASS(errctx, is_at_end(obj, &atend)); + if ( atend ) { + *got = false; + SUCCEED_RETURN(errctx); } *dest = obj->line[obj->current]; - return true; + *got = true; + SUCCEED_RETURN(errctx); } -static bool peek_next(akbasic_Runtime *obj, char *dest) +/** + * @brief The character one past the cursor. + * @param[out] dest The character. Untouched when there is none. + * @param[out] got Whether there was one. The old `bool` return. + */ +static akerr_ErrorContext *peek_next(akbasic_Runtime *obj, char *dest, bool *got) { - if ( (obj->current + 1) >= (int)strlen(obj->line) ) { - return false; + PREPARE_ERROR(errctx); + size_t linelen = 0; + + FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL && got != NULL), AKERR_NULLPOINTER, + "NULL argument in peek_next"); + PASS(errctx, aksl_strlen(obj->line, &linelen)); + if ( (obj->current + 1) >= (int)linelen ) { + *got = false; + SUCCEED_RETURN(errctx); } *dest = obj->line[obj->current + 1]; - return true; + *got = true; + SUCCEED_RETURN(errctx); } /* @@ -58,9 +98,12 @@ static bool peek_next(akbasic_Runtime *obj, char *dest) static akerr_ErrorContext *get_lexeme(akbasic_Runtime *obj, char *dest, size_t len) { PREPARE_ERROR(errctx); - int linelen = (int)strlen(obj->line); + size_t measured = 0; + int linelen = 0; int span = 0; + PASS(errctx, aksl_strlen(obj->line, &measured)); + linelen = (int)measured; if ( obj->current == linelen ) { span = linelen - obj->start; } else if ( obj->start == obj->current ) { @@ -74,7 +117,7 @@ static akerr_ErrorContext *get_lexeme(akbasic_Runtime *obj, char *dest, size_t l } FAIL_ZERO_RETURN(errctx, (span >= 0 && (size_t)span < len), AKBASIC_ERR_BOUNDS, "Lexeme of %d characters exceeds the %zu character limit", span, len - 1); - memcpy(dest, obj->line + obj->start, (size_t)span); + PASS(errctx, aksl_memcpy(dest, obj->line + obj->start, (size_t)span)); dest[span] = '\0'; SUCCEED_RETURN(errctx); } @@ -83,26 +126,39 @@ static akerr_ErrorContext *add_token(akbasic_Runtime *obj, akbasic_TokenType tok { PREPARE_ERROR(errctx); akbasic_Environment *env = obj->environment; + size_t lexemelen = 0; FAIL_ZERO_RETURN(errctx, (env->nexttoken < AKBASIC_MAX_TOKENS), AKBASIC_ERR_BOUNDS, "Line %" PRId64 " has more than %d tokens", env->lineno, AKBASIC_MAX_TOKENS); - FAIL_ZERO_RETURN(errctx, (strlen(lexeme) < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS, + PASS(errctx, aksl_strlen(lexeme, &lexemelen)); + FAIL_ZERO_RETURN(errctx, (lexemelen < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS, "Token lexeme exceeds the %d character limit", AKBASIC_MAX_LINE_LENGTH - 1); env->tokens[env->nexttoken].tokentype = token; env->tokens[env->nexttoken].lineno = env->lineno; - strncpy(env->tokens[env->nexttoken].lexeme, lexeme, AKBASIC_MAX_LINE_LENGTH - 1); - env->tokens[env->nexttoken].lexeme[AKBASIC_MAX_LINE_LENGTH - 1] = '\0'; + PASS(errctx, aksl_strcpy(env->tokens[env->nexttoken].lexeme, + sizeof(env->tokens[env->nexttoken].lexeme), lexeme)); env->nexttoken += 1; SUCCEED_RETURN(errctx); } -/* Consume one more character when it matches, choosing between two token types. */ -static bool match_next_char(akbasic_Runtime *obj, char cm, akbasic_TokenType truetype, akbasic_TokenType falsetype) +/** + * @brief Consume one more character when it matches, choosing between two token types. + * @param[out] matched Whether the character was consumed. The old `bool` return. + * + * On the chain below `peek`, so it reports the same way. See libakstdlib #38. + */ +static akerr_ErrorContext *match_next_char(akbasic_Runtime *obj, char cm, akbasic_TokenType truetype, + akbasic_TokenType falsetype, bool *matched) { + PREPARE_ERROR(errctx); char nc = '\0'; + bool got = false; - if ( !peek(obj, &nc) ) { + FAIL_ZERO_RETURN(errctx, (obj != NULL && matched != NULL), AKERR_NULLPOINTER, + "NULL argument in match_next_char"); + PASS(errctx, peek(obj, &nc, &got)); + if ( !got ) { /* * Nothing left to peek at, so the operator is whatever it is on its * own. The reference returns here *without* setting a type @@ -112,24 +168,34 @@ static bool match_next_char(akbasic_Runtime *obj, char cm, akbasic_TokenType tru * TODO.md section 6 item 14. */ obj->tokentype = falsetype; - return false; + *matched = false; + SUCCEED_RETURN(errctx); } if ( nc == cm ) { obj->current += 1; obj->tokentype = truetype; - return true; + *matched = true; + SUCCEED_RETURN(errctx); } obj->tokentype = falsetype; - return false; + *matched = false; + SUCCEED_RETURN(errctx); } static akerr_ErrorContext *match_string(akbasic_Runtime *obj) { PREPARE_ERROR(errctx); char c = '\0'; + bool atend = false; + bool got = false; - while ( !is_at_end(obj) ) { - if ( !peek(obj, &c) ) { + for ( ;; ) { + PASS(errctx, is_at_end(obj, &atend)); + if ( atend ) { + break; + } + PASS(errctx, peek(obj, &c, &got)); + if ( !got ) { PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, "UNTERMINATED STRING LITERAL\n")); obj->hasError = true; @@ -154,12 +220,19 @@ static akerr_ErrorContext *match_number(akbasic_Runtime *obj) int64_t lineno = 0; long long converted = 0; bool hex = false; + bool atend = false; + bool got = false; obj->tokentype = AKBASIC_TOK_LITERAL_INT; - while ( !is_at_end(obj) ) { - (void)peek(obj, &c); + for ( ;; ) { + PASS(errctx, is_at_end(obj, &atend)); + if ( atend ) { + break; + } + PASS(errctx, peek(obj, &c, &got)); if ( c == '.' ) { - if ( !peek_next(obj, &nc) || !isdigit((unsigned char)nc) ) { + PASS(errctx, peek_next(obj, &nc, &got)); + if ( !got || !isdigit((unsigned char)nc) ) { PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, "INVALID FLOATING POINT LITERAL\n")); obj->hasError = true; @@ -200,13 +273,17 @@ static akerr_ErrorContext *match_number(akbasic_Runtime *obj) } CLEANUP { } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { - char message[AKBASIC_MAX_LINE_LENGTH + 32]; - snprintf(message, sizeof(message), "INTEGER CONVERSION ON '%s'", lexeme); + char message[AKBASIC_MAX_LINE_LENGTH + 32] = "INTEGER CONVERSION"; + int written = 0; /* - * Reporting can itself fail if the sink is broken. Nothing useful - * remains to be done about that here, so record the flag and let the - * next operation surface it. + * Reporting can itself fail if the sink is broken, and so can + * formatting the message. Neither leaves anything useful to do here -- + * this block is already handling an error -- so both are ignored, the + * message keeps the initialiser above if the format fails, and the flag + * lets the next operation surface it. */ + IGNORE(aksl_snprintf(&written, message, sizeof(message), + "INTEGER CONVERSION ON '%s'", lexeme)); IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message)); obj->hasError = true; } FINISH(errctx, false); @@ -230,10 +307,16 @@ static akerr_ErrorContext *match_identifier(akbasic_Runtime *obj) bool userfunction = false; char c = '\0'; size_t used = 0; + bool atend = false; + bool got = false; obj->tokentype = AKBASIC_TOK_IDENTIFIER; - while ( !is_at_end(obj) ) { - (void)peek(obj, &c); + for ( ;; ) { + PASS(errctx, is_at_end(obj, &atend)); + if ( atend ) { + break; + } + PASS(errctx, peek(obj, &c, &got)); if ( isdigit((unsigned char)c) || isalpha((unsigned char)c) ) { obj->current += 1; continue; @@ -270,9 +353,8 @@ static akerr_ErrorContext *match_identifier(akbasic_Runtime *obj) * in variable name" branch below is dead code, and `PRINT$ = 1` is quietly * accepted as an ordinary string variable. TODO.md section 6 item 16. */ - strncpy(basename, lexeme, sizeof(basename) - 1); - basename[sizeof(basename) - 1] = '\0'; - used = strlen(basename); + PASS(errctx, aksl_strcpy(basename, sizeof(basename), lexeme)); + PASS(errctx, aksl_strlen(basename, &used)); if ( obj->tokentype != AKBASIC_TOK_IDENTIFIER && used > 0 ) { basename[used - 1] = '\0'; } @@ -311,15 +393,18 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line, char lexeme[AKBASIC_MAX_LINE_LENGTH]; char c = '\0'; bool done = false; + bool atend = false; + bool matched = false; + size_t linelen = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in scan"); FAIL_ZERO_RETURN(errctx, (line != NULL), AKERR_NULLPOINTER, "NULL line in scan"); - FAIL_ZERO_RETURN(errctx, (strlen(line) < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS, + PASS(errctx, aksl_strlen(line, &linelen)); + FAIL_ZERO_RETURN(errctx, (linelen < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS, "Source line of %zu characters exceeds the %d character limit", - strlen(line), AKBASIC_MAX_LINE_LENGTH - 1); + linelen, AKBASIC_MAX_LINE_LENGTH - 1); - strncpy(obj->line, line, AKBASIC_MAX_LINE_LENGTH - 1); - obj->line[AKBASIC_MAX_LINE_LENGTH - 1] = '\0'; + PASS(errctx, aksl_strcpy(obj->line, sizeof(obj->line), line)); PASS(errctx, akbasic_environment_zero_parser(obj->environment)); obj->current = 0; obj->start = 0; @@ -333,7 +418,11 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line, */ obj->hadlinenumber = false; - while ( !is_at_end(obj) && !done ) { + for ( ;; ) { + PASS(errctx, is_at_end(obj, &atend)); + if ( atend || done ) { + break; + } c = obj->line[obj->current]; obj->current += 1; @@ -350,7 +439,7 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line, * no spelling this makes ambiguous. */ case '-': - (void)match_next_char(obj, '>', AKBASIC_TOK_ARROW, AKBASIC_TOK_MINUS); + PASS(errctx, match_next_char(obj, '>', AKBASIC_TOK_ARROW, AKBASIC_TOK_MINUS, &matched)); break; case '/': obj->tokentype = AKBASIC_TOK_LEFT_SLASH; break; case '*': obj->tokentype = AKBASIC_TOK_STAR; break; @@ -373,15 +462,19 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line, case '[': obj->tokentype = AKBASIC_TOK_LEFT_SQUAREBRACKET; break; case ']': obj->tokentype = AKBASIC_TOK_RIGHT_SQUAREBRACKET; break; case '=': - (void)match_next_char(obj, '=', AKBASIC_TOK_EQUAL, AKBASIC_TOK_ASSIGNMENT); + PASS(errctx, match_next_char(obj, '=', AKBASIC_TOK_EQUAL, AKBASIC_TOK_ASSIGNMENT, &matched)); break; case '<': - if ( !match_next_char(obj, '=', AKBASIC_TOK_LESS_THAN_EQUAL, AKBASIC_TOK_LESS_THAN) ) { - (void)match_next_char(obj, '>', AKBASIC_TOK_NOT_EQUAL, AKBASIC_TOK_LESS_THAN); + PASS(errctx, match_next_char(obj, '=', AKBASIC_TOK_LESS_THAN_EQUAL, + AKBASIC_TOK_LESS_THAN, &matched)); + if ( !matched ) { + PASS(errctx, match_next_char(obj, '>', AKBASIC_TOK_NOT_EQUAL, + AKBASIC_TOK_LESS_THAN, &matched)); } break; case '>': - (void)match_next_char(obj, '=', AKBASIC_TOK_GREATER_THAN_EQUAL, AKBASIC_TOK_GREATER_THAN); + PASS(errctx, match_next_char(obj, '=', AKBASIC_TOK_GREATER_THAN_EQUAL, + AKBASIC_TOK_GREATER_THAN, &matched)); break; case '"': obj->start = obj->current; @@ -402,7 +495,10 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line, PASS(errctx, match_identifier(obj)); } else { char message[AKBASIC_MAX_LINE_LENGTH]; - snprintf(message, sizeof(message), "UNKNOWN TOKEN %c\n", c); + int written = 0; + + PASS(errctx, aksl_snprintf(&written, message, sizeof(message), + "UNKNOWN TOKEN %c\n", c)); PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message)); obj->hasError = true; obj->start = obj->current; @@ -426,10 +522,13 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line, * stores *that* as the program text. */ int skip = obj->current; + size_t tail = 0; + while ( obj->line[skip] == ' ' ) { skip += 1; } - memmove(obj->line, obj->line + skip, strlen(obj->line + skip) + 1); + PASS(errctx, aksl_strlen(obj->line + skip, &tail)); + PASS(errctx, aksl_memmove(obj->line, obj->line + skip, tail + 1)); obj->current = 0; } else { PASS(errctx, get_lexeme(obj, lexeme, sizeof(lexeme))); @@ -445,10 +544,10 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line, } if ( dest != NULL ) { - FAIL_ZERO_RETURN(errctx, (strlen(obj->line) < len), AKBASIC_ERR_BOUNDS, + PASS(errctx, aksl_strlen(obj->line, &linelen)); + FAIL_ZERO_RETURN(errctx, (linelen < len), AKBASIC_ERR_BOUNDS, "Scanned line does not fit the caller's buffer"); - strncpy(dest, obj->line, len - 1); - dest[len - 1] = '\0'; + PASS(errctx, aksl_strcpy(dest, len, obj->line)); } SUCCEED_RETURN(errctx); } diff --git a/src/sink_akgl.c b/src/sink_akgl.c index 0d3e2a7..86496a6 100644 --- a/src/sink_akgl.c +++ b/src/sink_akgl.c @@ -15,9 +15,8 @@ #include #include -#include - #include +#include #include #include @@ -32,15 +31,24 @@ /** @brief Bytes per row, including the terminator. */ #define SINK_MAX_COLUMNS 256 -/** @brief Scroll the grid up by one row, dropping the top one. */ -static void scroll(akbasic_AkglSink *state) +/** + * @brief Scroll the grid up by one row, dropping the top one. + * + * Returns a context rather than `void` because the row moves can fail and there + * was nowhere to say so. Everything on this chain -- scroll, newline, + * putchar_at, echo_line, edit_key -- returns one for the same reason, and the + * chain already ends at sink_write, sink_writeln and sink_readline, which + * returned one all along. See libakstdlib #38. + */ +static akerr_ErrorContext *scroll(akbasic_AkglSink *state) { + PREPARE_ERROR(errctx); int row = 0; for ( row = 0; row < SINK_MAX_ROWS - 1; row++ ) { - memcpy(state->text[row], state->text[row + 1], SINK_MAX_COLUMNS); + PASS(errctx, aksl_memcpy(state->text[row], state->text[row + 1], SINK_MAX_COLUMNS)); } - memset(state->text[SINK_MAX_ROWS - 1], 0, SINK_MAX_COLUMNS); + PASS(errctx, aksl_memset(state->text[SINK_MAX_ROWS - 1], 0, SINK_MAX_COLUMNS)); if ( state->cursorrow > 0 ) { state->cursorrow -= 1; } @@ -59,13 +67,16 @@ static void scroll(akbasic_AkglSink *state) } /** @brief Move to the start of the next row, scrolling if that runs off the end. */ -static void newline(akbasic_AkglSink *state) +static akerr_ErrorContext *newline(akbasic_AkglSink *state) { + PREPARE_ERROR(errctx); + state->cursorcol = 0; state->cursorrow += 1; if ( state->cursorrow >= state->rows || state->cursorrow >= SINK_MAX_ROWS ) { - scroll(state); + PASS(errctx, scroll(state)); } + SUCCEED_RETURN(errctx); } /** @@ -89,16 +100,17 @@ static void newline(akbasic_AkglSink *state) * cleared between rows, so the gap holds the tail of some longer row that used * to be here. */ -static void putchar_at(akbasic_AkglSink *state, char c) +static akerr_ErrorContext *putchar_at(akbasic_AkglSink *state, char c) { + PREPARE_ERROR(errctx); int col = 0; if ( c == '\n' ) { - newline(state); - return; + PASS(errctx, newline(state)); + SUCCEED_RETURN(errctx); } if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) { - newline(state); + PASS(errctx, newline(state)); } for ( col = 0; col < state->cursorcol; col++ ) { if ( state->text[state->cursorrow][col] == '\0' ) { @@ -111,6 +123,7 @@ static void putchar_at(akbasic_AkglSink *state, char c) state->text[state->cursorrow][state->cursorcol] = c; state->cursorcol += 1; state->text[state->cursorrow][state->cursorcol] = '\0'; + SUCCEED_RETURN(errctx); } static akerr_ErrorContext *sink_write(akbasic_TextSink *self, const char *text) @@ -125,7 +138,7 @@ static akerr_ErrorContext *sink_write(akbasic_TextSink *self, const char *text) FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state"); for ( i = 0; text[i] != '\0'; i++ ) { - putchar_at(state, text[i]); + PASS(errctx, putchar_at(state, text[i])); } SUCCEED_RETURN(errctx); } @@ -137,7 +150,7 @@ static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text PASS(errctx, sink_write(self, text)); state = (akbasic_AkglSink *)self->self; - newline(state); + PASS(errctx, newline(state)); SUCCEED_RETURN(errctx); } @@ -151,8 +164,9 @@ static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text * memory, and the host draws it when it draws its frame -- so the cost is a * couple of memcpy-sized loops per keystroke. */ -static void echo_line(akbasic_AkglSink *state) +static akerr_ErrorContext *echo_line(akbasic_AkglSink *state) { + PREPARE_ERROR(errctx); int i = 0; int erasedto = 0; int endedat = 0; @@ -160,14 +174,14 @@ static void echo_line(akbasic_AkglSink *state) state->cursorrow = state->editrow; state->cursorcol = state->editcol; for ( i = 0; i < state->echolen; i++ ) { - putchar_at(state, ' '); + PASS(errctx, putchar_at(state, ' ')); } erasedto = state->cursorrow; state->cursorrow = state->editrow; state->cursorcol = state->editcol; for ( i = 0; i < state->editlen; i++ ) { - putchar_at(state, state->editline[i]); + PASS(errctx, putchar_at(state, state->editline[i])); } endedat = state->cursorrow; state->echolen = state->editlen; @@ -186,6 +200,7 @@ static void echo_line(akbasic_AkglSink *state) for ( i = endedat + 1; i <= erasedto && i < SINK_MAX_ROWS; i++ ) { state->text[i][0] = '\0'; } + SUCCEED_RETURN(errctx); } /** @brief Append one byte to the line being typed, if there is room for it. */ @@ -215,14 +230,15 @@ static void edit_append(akbasic_AkglSink *state, char c) * Backspace and Escape are keys rather than characters and several of them * compose to text SDL would otherwise hand straight through. */ -static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *submitted) +static akerr_ErrorContext *edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *submitted) { + PREPARE_ERROR(errctx); size_t i = 0; if ( key->key == SDLK_RETURN || key->key == SDLK_KP_ENTER || key->key == '\r' || key->key == '\n' ) { *submitted = true; - return; + SUCCEED_RETURN(errctx); } if ( key->key == SDLK_BACKSPACE || key->key == '\b' || key->key == 0x7f ) { if ( state->editlen > 0 ) { @@ -235,15 +251,15 @@ static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *s */ state->editlen -= 1; state->editline[state->editlen] = '\0'; - echo_line(state); + PASS(errctx, echo_line(state)); } - return; + SUCCEED_RETURN(errctx); } if ( key->key == SDLK_ESCAPE || key->key == 0x1b ) { state->editlen = 0; state->editline[0] = '\0'; - echo_line(state); - return; + PASS(errctx, echo_line(state)); + SUCCEED_RETURN(errctx); } if ( key->text[0] != '\0' ) { @@ -257,8 +273,8 @@ static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *s edit_append(state, key->text[i]); } } - echo_line(state); - return; + PASS(errctx, echo_line(state)); + SUCCEED_RETURN(errctx); } /* @@ -277,14 +293,15 @@ static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *s */ if ( key->key >= 0x20 && key->key < 0x7f ) { edit_append(state, (char)toupper((unsigned char)key->key)); - echo_line(state); - return; + PASS(errctx, echo_line(state)); + SUCCEED_RETURN(errctx); } /* * Neither: a cursor key, a function key or a bare modifier. Not an editing * command here -- a script's own GET loop is what wants those. */ + SUCCEED_RETURN(errctx); } /** @@ -312,7 +329,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *edit_loop(akbasic_AkglSink *state, boo */ PASS(errctx, akgl_controller_poll_keystroke(&key, &available)); if ( available ) { - edit_key(state, &key, &submitted); + PASS(errctx, edit_key(state, &key, &submitted)); continue; } /* @@ -375,9 +392,8 @@ static akerr_ErrorContext *sink_readline(akbasic_TextSink *self, char *dest, siz if ( *eof ) { SUCCEED_RETURN(errctx); } - strncpy(dest, state->editline, len - 1); - dest[len - 1] = '\0'; - newline(state); + PASS(errctx, aksl_strncpy(dest, len, state->editline, len - 1)); + PASS(errctx, newline(state)); SUCCEED_RETURN(errctx); } @@ -391,7 +407,7 @@ static akerr_ErrorContext *sink_clear(akbasic_TextSink *self) state = (akbasic_AkglSink *)self->self; FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state"); - memset(state->text, 0, sizeof(state->text)); + PASS(errctx, aksl_memset(state->text, 0, sizeof(state->text))); state->cursorcol = 0; state->cursorrow = 0; SUCCEED_RETURN(errctx); @@ -600,7 +616,7 @@ akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSi "A %dx%d text area has no room for a %dx%d character", w, h, cellw, cellh); - memset(state, 0, sizeof(*state)); + PASS(errctx, aksl_memset(state, 0, sizeof(*state))); state->renderer = renderer; state->font = font; state->color.r = 0xff; diff --git a/src/sink_stdio.c b/src/sink_stdio.c index 3dffee7..1e50bf3 100644 --- a/src/sink_stdio.c +++ b/src/sink_stdio.c @@ -10,7 +10,6 @@ */ #include -#include #include #include @@ -59,8 +58,20 @@ static akerr_ErrorContext *stdio_readline(akbasic_TextSink *self, char *dest, si *eof = false; dest[0] = '\0'; - if ( fgets(dest, (int)len, state->in) == NULL ) { + /* + * The end of the stream is AKERR_EOF rather than a NULL return, so it is + * handled here and turned back into the *eof flag this sink promises. A + * genuine read error is left to propagate, which is what a NULL from + * fgets(3) could never be told apart from. + */ + ATTEMPT { + CATCH(errctx, aksl_fgets(dest, len, state->in, &used)); + } CLEANUP { + } PROCESS(errctx) { + } HANDLE(errctx, AKERR_EOF) { *eof = true; + } FINISH(errctx, true); + if ( *eof ) { SUCCEED_RETURN(errctx); } /* @@ -68,7 +79,6 @@ static akerr_ErrorContext *stdio_readline(akbasic_TextSink *self, char *dest, si * anyway, but leaving them on would make a stored source line differ from * the same line typed at the REPL. */ - used = strlen(dest); while ( used > 0 && (dest[used - 1] == '\n' || dest[used - 1] == '\r') ) { dest[used - 1] = '\0'; used -= 1; diff --git a/src/sprite_akgl.c b/src/sprite_akgl.c index 8ca94f2..0491c00 100644 --- a/src/sprite_akgl.c +++ b/src/sprite_akgl.c @@ -34,11 +34,11 @@ */ #include -#include #include #include +#include #include #include @@ -195,9 +195,9 @@ static akerr_ErrorContext *build_slot(akbasic_AkglSprites *state, int i, int wid oldvisible = state->actors[i]->visible; } - memset(spritename, 0, sizeof(spritename)); - memset(charname, 0, sizeof(charname)); - memset(actorname, 0, sizeof(actorname)); + PASS(errctx, aksl_memset(spritename, 0, sizeof(spritename))); + PASS(errctx, aksl_memset(charname, 0, sizeof(charname))); + PASS(errctx, aksl_memset(actorname, 0, sizeof(actorname))); SDL_snprintf(spritename, sizeof(spritename), "akbasic:sprite:%d", i + 1); SDL_snprintf(charname, sizeof(charname), "akbasic:character:%d", i + 1); SDL_snprintf(actorname, sizeof(actorname), "akbasic:actor:%d", i + 1); @@ -263,8 +263,8 @@ static akerr_ErrorContext *install_surface(akbasic_AkglSprites *state, int i, SD * which is what makes the texture's ownership one rule rather than two. */ PASS(errctx, akgl_heap_next_spritesheet(&state->sheets[i])); - memset(state->sheets[i], 0, sizeof(*state->sheets[i])); - memset(sheetname, 0, sizeof(sheetname)); + PASS(errctx, aksl_memset(state->sheets[i], 0, sizeof(*state->sheets[i]))); + PASS(errctx, aksl_memset(sheetname, 0, sizeof(sheetname))); SDL_snprintf(sheetname, sizeof(sheetname), "akbasic:sheet:%d", i + 1); SDL_strlcpy(state->sheets[i]->name, sheetname, sizeof(state->sheets[i]->name)); state->sheets[i]->texture = texture; @@ -533,7 +533,7 @@ static akerr_ErrorContext *spr_shape(akbasic_SpriteBackend *self, int n, int kin state->shapex2[i] = (float32_t)x2; state->shapey2[i] = (float32_t)y2; state->shapeexplicit[i] = (x2 > x1 && y2 > y1); - memset(&state->syncedbox[i], 0, sizeof(state->syncedbox[i])); + PASS(errctx, aksl_memset(&state->syncedbox[i], 0, sizeof(state->syncedbox[i]))); SUCCEED_RETURN(errctx); } @@ -703,7 +703,8 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_scan(akbasic_AkglSprites *state) for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { live[i] = slot_collidable(state, i); synced[i] = false; - memset(&box[i], 0, sizeof(box[i])); + /* PASS rather than CATCH, as everywhere else in this loop-heavy function. */ + PASS(errctx, aksl_memset(&box[i], 0, sizeof(box[i]))); if ( !live[i] ) { continue; } @@ -745,7 +746,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_scan(akbasic_AkglSprites *state) } state->lastmask = 0; state->lastsolidmask = 0; - memset(state->hascontact, 0, sizeof(state->hascontact)); + PASS(errctx, aksl_memset(state->hascontact, 0, sizeof(state->hascontact))); /* * **Two phases, and the split is the whole performance story.** @@ -970,7 +971,7 @@ akerr_ErrorContext *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic /* Before anything else in libakgl, so every AKGL_ERR_* has a name to print. */ PASS(errctx, akgl_error_init()); - memset(state, 0, sizeof(*state)); + PASS(errctx, aksl_memset(state, 0, sizeof(*state))); state->renderer = renderer; state->graphics = graphics; @@ -1012,7 +1013,7 @@ akerr_ErrorContext *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic &state->shapes[i], 0.0f, 0.0f, 0.0f)); } - memset(obj, 0, sizeof(*obj)); + PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); obj->self = state; obj->define = spr_define; obj->define_shape = spr_define_shape; diff --git a/src/sprite_tables.c b/src/sprite_tables.c index d7a0794..00c3527 100644 --- a/src/sprite_tables.c +++ b/src/sprite_tables.c @@ -9,9 +9,8 @@ * adaptor that has to turn a pattern into pixels. */ -#include - #include +#include #include #include @@ -40,7 +39,7 @@ akerr_ErrorContext *akbasic_sprite_state_init(akbasic_SpriteState *obj) int i = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL sprite state in init"); - memset(obj, 0, sizeof(*obj)); + PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { obj->sprites[i].colorindex = SPRITE_COLOR_DEFAULT; } diff --git a/src/structtype.c b/src/structtype.c index e333664..435bc15 100644 --- a/src/structtype.c +++ b/src/structtype.c @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -88,10 +89,22 @@ static const char *next_word(const char *cursor, char *dest, size_t len) return cursor; } -/** @brief Compare a word against a keyword. Verbs are case-insensitive here as everywhere. */ -static bool word_is(const char *word, const char *keyword) +/** + * @brief Compare a word against a keyword. Verbs are case-insensitive here as everywhere. + * + * The answer leaves through @p dest rather than the return value because the + * comparison itself can fail, and a `bool` has nowhere to put that. See + * libakstdlib #38. + */ +static akerr_ErrorContext *word_is(const char *word, const char *keyword, bool *dest) { - return (strcmp(word, keyword) == 0); + PREPARE_ERROR(errctx); + int cmp = 0; + + FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL dest in word_is"); + PASS(errctx, aksl_strcmp(word, keyword, &cmp)); + *dest = (cmp == 0); + SUCCEED_RETURN(errctx); } /** @@ -103,8 +116,9 @@ static bool word_is(const char *word, const char *keyword) static akerr_ErrorContext *type_from_suffix(const char *name, akbasic_Type *dest) { PREPARE_ERROR(errctx); - size_t len = strlen(name); + size_t len = 0; + PASS(errctx, aksl_strlen(name, &len)); FAIL_ZERO_RETURN(errctx, (len >= 2), AKBASIC_ERR_SYNTAX, "Field name \"%s\" carries no type suffix", name); switch ( name[len - 1] ) { @@ -125,7 +139,7 @@ akerr_ErrorContext *akbasic_structtype_table_init(akbasic_StructTypeTable *obj) PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL table in structtype init"); - memset(obj, 0, sizeof(*obj)); + PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); obj->count = 0; SUCCEED_RETURN(errctx); } @@ -134,12 +148,17 @@ akerr_ErrorContext *akbasic_structtype_find(akbasic_StructTypeTable *obj, const { PREPARE_ERROR(errctx); int i = 0; + int cmp = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in structtype find"); *dest = -1; for ( i = 0; i < obj->count; i++ ) { - if ( obj->types[i].used && strcmp(obj->types[i].name, name) == 0 ) { + if ( !obj->types[i].used ) { + continue; + } + PASS(errctx, aksl_strcmp(obj->types[i].name, name, &cmp)); + if ( cmp == 0 ) { *dest = i; SUCCEED_RETURN(errctx); } @@ -151,9 +170,14 @@ akerr_ErrorContext *akbasic_structtype_field(akbasic_StructTypeTable *obj, int t { PREPARE_ERROR(errctx); char known[AKBASIC_MAX_STRING_LENGTH]; + const char *separator = NULL; akbasic_StructType *type = NULL; size_t used = 0; + size_t namelen = 0; + size_t seplen = 0; + int written = 0; int i = 0; + int cmp = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in structtype field"); @@ -162,7 +186,8 @@ akerr_ErrorContext *akbasic_structtype_field(akbasic_StructTypeTable *obj, int t type = &obj->types[typeindex]; for ( i = 0; i < type->fieldcount; i++ ) { - if ( strcmp(type->fields[i].name, name) == 0 ) { + PASS(errctx, aksl_strcmp(type->fields[i].name, name, &cmp)); + if ( cmp == 0 ) { *dest = &type->fields[i]; SUCCEED_RETURN(errctx); } @@ -176,11 +201,20 @@ akerr_ErrorContext *akbasic_structtype_field(akbasic_StructTypeTable *obj, int t */ known[0] = '\0'; for ( i = 0; i < type->fieldcount; i++ ) { - int written = snprintf(known + used, sizeof(known) - used, "%s%s", - (i == 0 ? "" : ", "), type->fields[i].name); - if ( written < 0 || (size_t)written >= sizeof(known) - used ) { + separator = (i == 0 ? "" : ", "); + PASS(errctx, aksl_strlen(separator, &seplen)); + PASS(errctx, aksl_strlen(type->fields[i].name, &namelen)); + /* + * The list is advisory, so a full buffer ends it rather than failing the + * lookup. aksl_snprintf treats truncation as an error, which is right + * everywhere the output matters -- so the fit is decided here, before the + * call, instead of being read back out of a return value afterwards. + */ + if ( used + seplen + namelen >= sizeof(known) ) { break; } + PASS(errctx, aksl_snprintf(&written, known + used, sizeof(known) - used, "%s%s", + separator, type->fields[i].name)); used += (size_t)written; } FAIL_RETURN(errctx, AKBASIC_ERR_UNDEFINED, "%s has no field %s (%s)", @@ -223,6 +257,8 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj) int64_t i = 0; int open = -1; int existing = 0; + size_t namelen = 0; + bool matched = false; const akbasic_Verb *verb = NULL; for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { @@ -231,9 +267,11 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj) } cursor = next_word(skip_lineno(obj->source[i].code), word, sizeof(word)); - if ( word_is(word, "END") ) { + PASS(errctx, word_is(word, "END", &matched)); + if ( matched ) { (void)next_word(cursor, name, sizeof(name)); - if ( !word_is(name, "TYPE") ) { + PASS(errctx, word_is(name, "TYPE", &matched)); + if ( !matched ) { continue; /* an ordinary END, not our terminator */ } FAIL_ZERO_RETURN(errctx, (open >= 0), AKBASIC_ERR_SYNTAX, @@ -242,7 +280,8 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj) open = -1; continue; } - if ( !word_is(word, "TYPE") ) { + PASS(errctx, word_is(word, "TYPE", &matched)); + if ( !matched ) { continue; } @@ -268,16 +307,17 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj) FAIL_ZERO_RETURN(errctx, (table->count < AKBASIC_MAX_STRUCT_TYPES), AKBASIC_ERR_BOUNDS, "More than %d TYPE declarations", AKBASIC_MAX_STRUCT_TYPES); - FAIL_ZERO_RETURN(errctx, (strlen(name) < AKBASIC_MAX_STRUCT_NAME), AKBASIC_ERR_BOUNDS, + PASS(errctx, aksl_strlen(name, &namelen)); + FAIL_ZERO_RETURN(errctx, (namelen < AKBASIC_MAX_STRUCT_NAME), AKBASIC_ERR_BOUNDS, "TYPE name \"%s\" exceeds the %d character limit", name, AKBASIC_MAX_STRUCT_NAME - 1); open = table->count; - memset(&table->types[open], 0, sizeof(table->types[open])); - /* The memset is what terminates this: bounded at size - 1, the last byte - is the zero it already wrote. Refused above rather than truncated, - because two long names truncating to the same prefix would collide - silently -- and truncation is an error everywhere else here. */ - strncpy(table->types[open].name, name, sizeof(table->types[open].name) - 1); + PASS(errctx, aksl_memset(&table->types[open], 0, sizeof(table->types[open]))); + /* aksl_strcpy always terminates and refuses rather than truncates, which + is the behaviour this site already wanted -- two long names truncating + to the same prefix would collide silently. The length check above stays + because it names the limit in the message. */ + PASS(errctx, aksl_strcpy(table->types[open].name, sizeof(table->types[open].name), name)); table->types[open].used = true; table->types[open].firstline = i; table->types[open].lastline = -1; @@ -302,34 +342,38 @@ static akerr_ErrorContext *parse_field(akbasic_StructTypeTable *table, akbasic_S const char *cursor = NULL; akbasic_StructField *field = NULL; akbasic_Type valuetype = AKBASIC_TYPE_UNDEFINED; + size_t namelen = 0; + bool matched = false; int referenced = 0; + int cmp = 0; int i = 0; cursor = next_word(skip_lineno(line), fieldname, sizeof(fieldname)); if ( fieldname[0] == '\0' || fieldname[0] == '\'' ) { SUCCEED_RETURN(errctx); /* a blank line inside the block */ } - if ( word_is(fieldname, "REM") ) { + PASS(errctx, word_is(fieldname, "REM", &matched)); + if ( matched ) { SUCCEED_RETURN(errctx); } FAIL_ZERO_RETURN(errctx, (type->fieldcount < AKBASIC_MAX_STRUCT_FIELDS), AKBASIC_ERR_BOUNDS, "TYPE %s declares more than %d fields", type->name, AKBASIC_MAX_STRUCT_FIELDS); for ( i = 0; i < type->fieldcount; i++ ) { - FAIL_NONZERO_RETURN(errctx, (strcmp(type->fields[i].name, fieldname) == 0), - AKBASIC_ERR_VALUE, + PASS(errctx, aksl_strcmp(type->fields[i].name, fieldname, &cmp)); + FAIL_NONZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_VALUE, "TYPE %s declares %s twice", type->name, fieldname); } PASS(errctx, type_from_suffix(fieldname, &valuetype)); - FAIL_ZERO_RETURN(errctx, (strlen(fieldname) < AKBASIC_MAX_STRUCT_NAME), AKBASIC_ERR_BOUNDS, + PASS(errctx, aksl_strlen(fieldname, &namelen)); + FAIL_ZERO_RETURN(errctx, (namelen < AKBASIC_MAX_STRUCT_NAME), AKBASIC_ERR_BOUNDS, "Field name \"%s\" exceeds the %d character limit", fieldname, AKBASIC_MAX_STRUCT_NAME - 1); field = &type->fields[type->fieldcount]; - memset(field, 0, sizeof(*field)); - /* Terminated by the memset above; refused rather than truncated, for the - same reason a type name is. */ - strncpy(field->name, fieldname, sizeof(field->name) - 1); + PASS(errctx, aksl_memset(field, 0, sizeof(*field))); + /* Refused rather than truncated, for the same reason a type name is. */ + PASS(errctx, aksl_strcpy(field->name, sizeof(field->name), fieldname)); field->valuetype = valuetype; field->typeindex = -1; field->offset = -1; @@ -354,13 +398,16 @@ static akerr_ErrorContext *parse_field(akbasic_StructTypeTable *table, akbasic_S * not fit in one. So the declaration has to name it. */ cursor = next_word(cursor, word, sizeof(word)); - FAIL_ZERO_RETURN(errctx, word_is(word, "AS"), AKBASIC_ERR_SYNTAX, + PASS(errctx, word_is(word, "AS", &matched)); + FAIL_ZERO_RETURN(errctx, matched, AKBASIC_ERR_SYNTAX, "Line %" PRId64 ": %s must name its type -- %s AS TYPENAME, or %s AS PTR TO TYPENAME", lineno, fieldname, fieldname, fieldname); cursor = next_word(cursor, word, sizeof(word)); - if ( word_is(word, "PTR") ) { + PASS(errctx, word_is(word, "PTR", &matched)); + if ( matched ) { cursor = next_word(cursor, word, sizeof(word)); - FAIL_ZERO_RETURN(errctx, word_is(word, "TO"), AKBASIC_ERR_SYNTAX, + PASS(errctx, word_is(word, "TO", &matched)); + FAIL_ZERO_RETURN(errctx, matched, AKBASIC_ERR_SYNTAX, "Line %" PRId64 ": expected PTR TO TYPENAME", lineno); cursor = next_word(cursor, word, sizeof(word)); field->kind = AKBASIC_FIELD_POINTER; @@ -466,7 +513,7 @@ akerr_ErrorContext *akbasic_structtype_scan(akbasic_Runtime *obj) } } for ( i = t; i < table->count; i++ ) { - memset(&table->types[i], 0, sizeof(table->types[i])); + PASS(errctx, aksl_memset(&table->types[i], 0, sizeof(table->types[i]))); } table->count = t; diff --git a/src/symtab.c b/src/symtab.c index 1de9aa5..b1f9d81 100644 --- a/src/symtab.c +++ b/src/symtab.c @@ -3,8 +3,6 @@ * @brief Implements the fixed-capacity open-addressed symbol table. */ -#include - #include #include @@ -23,10 +21,13 @@ static akerr_ErrorContext *probe(akbasic_SymbolTable *obj, const char *key, int { PREPARE_ERROR(errctx); uint32_t hashval = 0; + size_t keylen = 0; int index = 0; + int cmp = 0; int i = 0; - PASS(errctx, aksl_strhash_djb2((char *)key, strlen(key), &hashval)); + PASS(errctx, aksl_strlen(key, &keylen)); + PASS(errctx, aksl_strhash_djb2((char *)key, keylen, &hashval)); *found = false; index = (int)(hashval % (uint32_t)obj->capacity); @@ -36,7 +37,8 @@ static akerr_ErrorContext *probe(akbasic_SymbolTable *obj, const char *key, int *slot = probeidx; SUCCEED_RETURN(errctx); } - if ( strcmp(obj->slots[probeidx].key, key) == 0 ) { + PASS(errctx, aksl_strcmp(obj->slots[probeidx].key, key, &cmp)); + if ( cmp == 0 ) { *slot = probeidx; *found = true; SUCCEED_RETURN(errctx); @@ -58,7 +60,7 @@ akerr_ErrorContext *akbasic_symtab_init(akbasic_SymbolTable *obj, int capacity) "Symbol table capacity %d out of range 1..%d", capacity, AKBASIC_SYMTAB_MAX_SLOTS); - memset(obj, 0, sizeof(*obj)); + PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); obj->capacity = capacity; obj->count = 0; SUCCEED_RETURN(errctx); @@ -67,6 +69,7 @@ akerr_ErrorContext *akbasic_symtab_init(akbasic_SymbolTable *obj, int capacity) akerr_ErrorContext *akbasic_symtab_set(akbasic_SymbolTable *obj, const char *key, void *value, int64_t ivalue) { PREPARE_ERROR(errctx); + size_t keylen = 0; int slot = 0; bool found = false; @@ -74,7 +77,8 @@ akerr_ErrorContext *akbasic_symtab_set(akbasic_SymbolTable *obj, const char *key "NULL symbol table in set"); FAIL_ZERO_RETURN(errctx, (key != NULL), AKERR_NULLPOINTER, "NULL key in symbol table set"); - FAIL_ZERO_RETURN(errctx, (strlen(key) < AKBASIC_SYMTAB_MAX_KEY), + PASS(errctx, aksl_strlen(key, &keylen)); + FAIL_ZERO_RETURN(errctx, (keylen < AKBASIC_SYMTAB_MAX_KEY), AKBASIC_ERR_BOUNDS, "Symbol name '%s' exceeds %d characters", key, AKBASIC_SYMTAB_MAX_KEY - 1); @@ -82,8 +86,10 @@ akerr_ErrorContext *akbasic_symtab_set(akbasic_SymbolTable *obj, const char *key PASS(errctx, probe(obj, key, &slot, &found)); if ( !found ) { - strncpy(obj->slots[slot].key, key, AKBASIC_SYMTAB_MAX_KEY - 1); - obj->slots[slot].key[AKBASIC_SYMTAB_MAX_KEY - 1] = '\0'; + /* aksl_strcpy always terminates, so the explicit terminator this + replaced is no longer needed. The length check above has already + refused anything that would not fit, so it cannot fail here. */ + PASS(errctx, aksl_strcpy(obj->slots[slot].key, sizeof(obj->slots[slot].key), key)); obj->slots[slot].used = true; obj->count += 1; } @@ -95,6 +101,7 @@ akerr_ErrorContext *akbasic_symtab_set(akbasic_SymbolTable *obj, const char *key akerr_ErrorContext *akbasic_symtab_get(akbasic_SymbolTable *obj, const char *key, void **value, int64_t *ivalue) { PREPARE_ERROR(errctx); + size_t keylen = 0; int slot = 0; bool found = false; @@ -102,7 +109,8 @@ akerr_ErrorContext *akbasic_symtab_get(akbasic_SymbolTable *obj, const char *key "NULL symbol table in get"); FAIL_ZERO_RETURN(errctx, (key != NULL), AKERR_NULLPOINTER, "NULL key in symbol table get"); - FAIL_ZERO_RETURN(errctx, (strlen(key) < AKBASIC_SYMTAB_MAX_KEY), + PASS(errctx, aksl_strlen(key, &keylen)); + FAIL_ZERO_RETURN(errctx, (keylen < AKBASIC_SYMTAB_MAX_KEY), AKERR_KEY, "Symbol '%s' is not present", key); @@ -127,7 +135,7 @@ akerr_ErrorContext *akbasic_symtab_clear(akbasic_SymbolTable *obj) "NULL symbol table in clear"); capacity = obj->capacity; - memset(obj, 0, sizeof(*obj)); + PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); obj->capacity = capacity; SUCCEED_RETURN(errctx); } diff --git a/src/ui_akgl.c b/src/ui_akgl.c index 66f7ccc..bc9de35 100644 --- a/src/ui_akgl.c +++ b/src/ui_akgl.c @@ -16,10 +16,8 @@ * string. */ -#include -#include - #include +#include #include #include @@ -94,8 +92,7 @@ static akerr_ErrorContext *ui_dialog(akbasic_UiBackend *self, const char *text) state->dialogtext[0] = '\0'; SUCCEED_RETURN(errctx); } - strncpy(state->dialogtext, text, sizeof(state->dialogtext) - 1); - state->dialogtext[sizeof(state->dialogtext) - 1] = '\0'; + PASS(errctx, aksl_strcpy(state->dialogtext, sizeof(state->dialogtext), text)); state->dialogopen = true; SUCCEED_RETURN(errctx); } @@ -113,8 +110,7 @@ static akerr_ErrorContext *ui_label(akbasic_UiBackend *self, int slot, int ancho state->labeltext[slot][0] = '\0'; SUCCEED_RETURN(errctx); } - strncpy(state->labeltext[slot], text, sizeof(state->labeltext[slot]) - 1); - state->labeltext[slot][sizeof(state->labeltext[slot]) - 1] = '\0'; + PASS(errctx, aksl_strcpy(state->labeltext[slot], sizeof(state->labeltext[slot]), text)); state->labelanchor[slot] = anchor; state->labelset[slot] = true; SUCCEED_RETURN(errctx); @@ -143,8 +139,8 @@ static akerr_ErrorContext *ui_menu(akbasic_UiBackend *self, int slot, const char for ( i = 0; i < count; i++ ) { FAIL_ZERO_RETURN(errctx, (items[i] != NULL), AKERR_NULLPOINTER, "MENU entry %d is NULL", i + 1); - strncpy(state->menuitems[slot][i], items[i], sizeof(state->menuitems[slot][i]) - 1); - state->menuitems[slot][i][sizeof(state->menuitems[slot][i]) - 1] = '\0'; + PASS(errctx, aksl_strcpy(state->menuitems[slot][i], sizeof(state->menuitems[slot][i]), + items[i])); state->menus[slot].items[i] = state->menuitems[slot][i]; } for ( i = count; i < AKBASIC_UI_MAX_MENU_ITEMS; i++ ) { @@ -348,9 +344,9 @@ akerr_ErrorContext *akbasic_ui_init_akgl(akbasic_UiBackend *obj, akbasic_AkglUi "A %dx%d UI at %d points is not a UI", width, height, fontsize); PASS(errctx, akgl_error_init()); - memset(state, 0, sizeof(*state)); + PASS(errctx, aksl_memset(state, 0, sizeof(*state))); state->renderer = renderer; - strncpy(state->fontname, UI_FONT_NAME, sizeof(state->fontname) - 1); + PASS(errctx, aksl_strcpy(state->fontname, sizeof(state->fontname), UI_FONT_NAME)); /* * The element ids, once. clay identifies an element by its string and keeps @@ -359,13 +355,14 @@ akerr_ErrorContext *akbasic_ui_init_akgl(akbasic_UiBackend *obj, akbasic_AkglUi * function that declares them. */ for ( i = 0; i < AKBASIC_UI_MAX_LABELS; i++ ) { - snprintf(state->labelid[i], sizeof(state->labelid[i]), "hud%d", i + 1); + PASS(errctx, aksl_snprintf(&count, state->labelid[i], sizeof(state->labelid[i]), + "hud%d", i + 1)); } for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) { - snprintf(state->menuid[i], sizeof(state->menuid[i]), "menu%d", i + 1); + PASS(errctx, aksl_snprintf(&count, state->menuid[i], sizeof(state->menuid[i]), + "menu%d", i + 1)); state->menus[i].id = state->menuid[i]; } - (void)count; /* * **akgl_registry_init() does not initialize the font registry** -- its own diff --git a/src/value.c b/src/value.c index ab30f8f..32a0131 100644 --- a/src/value.c +++ b/src/value.c @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -119,13 +120,17 @@ static akerr_ErrorContext *require_numeric(akbasic_Value *self, akbasic_Value *r static akerr_ErrorContext *set_string(akbasic_Value *dest, const char *src) { PREPARE_ERROR(errctx); + size_t srclen = 0; - FAIL_ZERO_RETURN(errctx, (strlen(src) < AKBASIC_MAX_STRING_LENGTH), + PASS(errctx, aksl_strlen(src, &srclen)); + FAIL_ZERO_RETURN(errctx, (srclen < AKBASIC_MAX_STRING_LENGTH), AKBASIC_ERR_VALUE, "String result of %zu characters exceeds the %d character limit", - strlen(src), AKBASIC_MAX_STRING_LENGTH - 1); - strncpy(dest->stringval, src, AKBASIC_MAX_STRING_LENGTH - 1); - dest->stringval[AKBASIC_MAX_STRING_LENGTH - 1] = '\0'; + srclen, AKBASIC_MAX_STRING_LENGTH - 1); + /* aksl_strcpy always terminates and refuses rather than truncates, which is + the behaviour this site already wanted. The length check above stays + because it names the limit in the message. */ + PASS(errctx, aksl_strcpy(dest->stringval, sizeof(dest->stringval), src)); SUCCEED_RETURN(errctx); } @@ -134,7 +139,7 @@ akerr_ErrorContext *akbasic_valuepool_init(akbasic_ValuePool *obj) PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL pool in init"); - memset(obj, 0, sizeof(*obj)); + PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); obj->next = 0; SUCCEED_RETURN(errctx); } @@ -201,7 +206,7 @@ akerr_ErrorContext *akbasic_value_clone(akbasic_Value *self, akbasic_Value *dest SUCCEED_RETURN(errctx); } dest->valuetype = self->valuetype; - memcpy(dest->stringval, self->stringval, sizeof(dest->stringval)); + PASS(errctx, aksl_memcpy(dest->stringval, self->stringval, sizeof(dest->stringval))); dest->intval = self->intval; dest->floatval = self->floatval; dest->boolvalue = self->boolvalue; @@ -223,6 +228,7 @@ akerr_ErrorContext *akbasic_value_clone(akbasic_Value *self, akbasic_Value *dest akerr_ErrorContext *akbasic_value_to_string(akbasic_Value *self, char *dest, size_t len) { PREPARE_ERROR(errctx); + int written = 0; FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL value in to_string"); FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in to_string"); @@ -230,20 +236,22 @@ akerr_ErrorContext *akbasic_value_to_string(akbasic_Value *self, char *dest, siz switch ( self->valuetype ) { case AKBASIC_TYPE_STRING: - snprintf(dest, len, "%s", self->stringval); + PASS(errctx, aksl_snprintf(&written, dest, len, "%s", self->stringval)); break; case AKBASIC_TYPE_INTEGER: - snprintf(dest, len, "%" PRId64, self->intval); + PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, self->intval)); break; case AKBASIC_TYPE_FLOAT: - snprintf(dest, len, "%f", self->floatval); + PASS(errctx, aksl_snprintf(&written, dest, len, "%f", self->floatval)); break; case AKBASIC_TYPE_BOOLEAN: /* Go's %t, which is "true"/"false" and not the numeric -1/0. */ - snprintf(dest, len, "%s", (self->boolvalue == AKBASIC_TRUE ? "true" : "false")); + PASS(errctx, aksl_snprintf(&written, dest, len, "%s", + (self->boolvalue == AKBASIC_TRUE ? "true" : "false"))); break; default: - snprintf(dest, len, "(UNDEFINED STRING REPRESENTATION FOR %d)", (int)self->valuetype); + PASS(errctx, aksl_snprintf(&written, dest, len, "(UNDEFINED STRING REPRESENTATION FOR %d)", + (int)self->valuetype)); break; } SUCCEED_RETURN(errctx); @@ -486,6 +494,7 @@ akerr_ErrorContext *akbasic_value_math_plus(akbasic_Value *self, akbasic_Value * PREPARE_ERROR(errctx); akbasic_Value *out = NULL; char buf[AKBASIC_MAX_STRING_LENGTH * 2]; + int written = 0; FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL value in math plus"); FAIL_ZERO_RETURN(errctx, (rval != NULL), AKERR_NULLPOINTER, "nil rval"); @@ -511,13 +520,16 @@ akerr_ErrorContext *akbasic_value_math_plus(akbasic_Value *self, akbasic_Value * } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { out->floatval = self->floatval + rval_as_float(rval); } else if ( self->valuetype == AKBASIC_TYPE_STRING && rval->valuetype == AKBASIC_TYPE_STRING ) { - snprintf(buf, sizeof(buf), "%s%s", self->stringval, rval->stringval); + PASS(errctx, aksl_snprintf(&written, buf, sizeof(buf), "%s%s", + self->stringval, rval->stringval)); PASS(errctx, set_string(out, buf)); } else if ( self->valuetype == AKBASIC_TYPE_STRING && rval->valuetype == AKBASIC_TYPE_INTEGER ) { - snprintf(buf, sizeof(buf), "%s%" PRId64, self->stringval, rval->intval); + PASS(errctx, aksl_snprintf(&written, buf, sizeof(buf), "%s%" PRId64, + self->stringval, rval->intval)); PASS(errctx, set_string(out, buf)); } else if ( self->valuetype == AKBASIC_TYPE_STRING && rval->valuetype == AKBASIC_TYPE_FLOAT ) { - snprintf(buf, sizeof(buf), "%s%f", self->stringval, rval->floatval); + PASS(errctx, aksl_snprintf(&written, buf, sizeof(buf), "%s%f", + self->stringval, rval->floatval)); PASS(errctx, set_string(out, buf)); } else { FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "Invalid arithmetic operation"); @@ -586,14 +598,14 @@ akerr_ErrorContext *akbasic_value_math_multiply(akbasic_Value *self, akbasic_Val */ FAIL_NONZERO_RETURN(errctx, (rval->intval < 0), AKBASIC_ERR_VALUE, "String multiplier %" PRId64 " must not be negative", rval->intval); - srclen = strlen((*dest)->stringval); + PASS(errctx, aksl_strlen((*dest)->stringval, &srclen)); FAIL_NONZERO_RETURN(errctx, (srclen != 0 && (uint64_t)rval->intval > (AKBASIC_MAX_STRING_LENGTH - 1) / srclen), AKBASIC_ERR_VALUE, "Repeated string of %zu x %" PRId64 " characters exceeds the %d character limit", srclen, rval->intval, AKBASIC_MAX_STRING_LENGTH - 1); for ( i = 0; i < rval->intval; i++ ) { - memcpy(buf + offset, (*dest)->stringval, srclen); + PASS(errctx, aksl_memcpy(buf + offset, (*dest)->stringval, srclen)); offset += srclen; } buf[offset] = '\0'; @@ -619,6 +631,7 @@ akerr_ErrorContext *akbasic_value_math_multiply(akbasic_Value *self, akbasic_Val akerr_ErrorContext *akbasic_value_less_than(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) { PREPARE_ERROR(errctx); + int cmp = 0; PASS(errctx, binary_prologue(self, rval, scratch, dest)); if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { @@ -626,7 +639,8 @@ akerr_ErrorContext *akbasic_value_less_than(akbasic_Value *self, akbasic_Value * } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { PASS(errctx, akbasic_value_set_bool(*dest, self->floatval < rval_as_float(rval))); } else { - PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) < 0)); + PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp)); + PASS(errctx, akbasic_value_set_bool(*dest, cmp < 0)); } SUCCEED_RETURN(errctx); } @@ -634,6 +648,7 @@ akerr_ErrorContext *akbasic_value_less_than(akbasic_Value *self, akbasic_Value * akerr_ErrorContext *akbasic_value_less_than_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) { PREPARE_ERROR(errctx); + int cmp = 0; PASS(errctx, binary_prologue(self, rval, scratch, dest)); if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { @@ -641,7 +656,8 @@ akerr_ErrorContext *akbasic_value_less_than_equal(akbasic_Value *self, akbasic_V } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { PASS(errctx, akbasic_value_set_bool(*dest, self->floatval <= rval_as_float(rval))); } else { - PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) <= 0)); + PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp)); + PASS(errctx, akbasic_value_set_bool(*dest, cmp <= 0)); } SUCCEED_RETURN(errctx); } @@ -649,6 +665,7 @@ akerr_ErrorContext *akbasic_value_less_than_equal(akbasic_Value *self, akbasic_V akerr_ErrorContext *akbasic_value_greater_than(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) { PREPARE_ERROR(errctx); + int cmp = 0; PASS(errctx, binary_prologue(self, rval, scratch, dest)); if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { @@ -656,7 +673,8 @@ akerr_ErrorContext *akbasic_value_greater_than(akbasic_Value *self, akbasic_Valu } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { PASS(errctx, akbasic_value_set_bool(*dest, self->floatval > rval_as_float(rval))); } else { - PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) > 0)); + PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp)); + PASS(errctx, akbasic_value_set_bool(*dest, cmp > 0)); } SUCCEED_RETURN(errctx); } @@ -664,6 +682,7 @@ akerr_ErrorContext *akbasic_value_greater_than(akbasic_Value *self, akbasic_Valu akerr_ErrorContext *akbasic_value_greater_than_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) { PREPARE_ERROR(errctx); + int cmp = 0; PASS(errctx, binary_prologue(self, rval, scratch, dest)); if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { @@ -671,7 +690,8 @@ akerr_ErrorContext *akbasic_value_greater_than_equal(akbasic_Value *self, akbasi } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { PASS(errctx, akbasic_value_set_bool(*dest, self->floatval >= rval_as_float(rval))); } else { - PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) >= 0)); + PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp)); + PASS(errctx, akbasic_value_set_bool(*dest, cmp >= 0)); } SUCCEED_RETURN(errctx); } @@ -679,6 +699,7 @@ akerr_ErrorContext *akbasic_value_greater_than_equal(akbasic_Value *self, akbasi akerr_ErrorContext *akbasic_value_is_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) { PREPARE_ERROR(errctx); + int cmp = 0; PASS(errctx, binary_prologue(self, rval, scratch, dest)); if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { @@ -686,7 +707,8 @@ akerr_ErrorContext *akbasic_value_is_equal(akbasic_Value *self, akbasic_Value *r } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { PASS(errctx, akbasic_value_set_bool(*dest, self->floatval == rval_as_float(rval))); } else { - PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) == 0)); + PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp)); + PASS(errctx, akbasic_value_set_bool(*dest, cmp == 0)); } SUCCEED_RETURN(errctx); } @@ -694,6 +716,7 @@ akerr_ErrorContext *akbasic_value_is_equal(akbasic_Value *self, akbasic_Value *r akerr_ErrorContext *akbasic_value_is_not_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) { PREPARE_ERROR(errctx); + int cmp = 0; PASS(errctx, binary_prologue(self, rval, scratch, dest)); if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { @@ -701,7 +724,8 @@ akerr_ErrorContext *akbasic_value_is_not_equal(akbasic_Value *self, akbasic_Valu } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { PASS(errctx, akbasic_value_set_bool(*dest, self->floatval != rval_as_float(rval))); } else { - PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) != 0)); + PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp)); + PASS(errctx, akbasic_value_set_bool(*dest, cmp != 0)); } SUCCEED_RETURN(errctx); } diff --git a/src/variable.c b/src/variable.c index 38cc7e9..f3d06d0 100644 --- a/src/variable.c +++ b/src/variable.c @@ -4,9 +4,9 @@ */ #include -#include #include +#include #include #include @@ -53,7 +53,7 @@ akerr_ErrorContext *akbasic_variable_init(akbasic_Variable *obj, akbasic_ValuePo "Array dimension count %d out of range 1..%d", sizecount, AKBASIC_MAX_ARRAY_DEPTH); - namelen = strlen(obj->name); + PASS(errctx, aksl_strlen(obj->name, &namelen)); FAIL_ZERO_RETURN(errctx, (namelen > 0), AKBASIC_ERR_VALUE, "Invalid variable name"); /* Type comes from the suffix. A bare name keeps whatever type it had. */ @@ -202,16 +202,17 @@ akerr_ErrorContext *akbasic_variable_set_string(akbasic_Variable *obj, const cha { PREPARE_ERROR(errctx); akbasic_Value tmp; + size_t valuelen = 0; FAIL_ZERO_RETURN(errctx, (value != NULL), AKERR_NULLPOINTER, "NULL string in set_string"); - FAIL_ZERO_RETURN(errctx, (strlen(value) < AKBASIC_MAX_STRING_LENGTH), + PASS(errctx, aksl_strlen(value, &valuelen)); + FAIL_ZERO_RETURN(errctx, (valuelen < AKBASIC_MAX_STRING_LENGTH), AKBASIC_ERR_VALUE, "String of %zu characters exceeds the %d character limit", - strlen(value), AKBASIC_MAX_STRING_LENGTH - 1); + valuelen, AKBASIC_MAX_STRING_LENGTH - 1); PASS(errctx, akbasic_value_zero(&tmp)); tmp.valuetype = AKBASIC_TYPE_STRING; - strncpy(tmp.stringval, value, AKBASIC_MAX_STRING_LENGTH - 1); - tmp.stringval[AKBASIC_MAX_STRING_LENGTH - 1] = '\0'; + PASS(errctx, aksl_strcpy(tmp.stringval, sizeof(tmp.stringval), value)); PASS(errctx, akbasic_variable_set_subscript(obj, &tmp, subscripts, subscriptcount)); SUCCEED_RETURN(errctx); } diff --git a/src/verbs.c b/src/verbs.c index 6f7bdaf..6af2a05 100644 --- a/src/verbs.c +++ b/src/verbs.c @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -197,6 +198,28 @@ static const akbasic_Verb VERBS[] = { #define VERB_COUNT ((int)(sizeof(VERBS) / sizeof(VERBS[0]))) +/** + * @brief bsearch(3) comparator for the verb table. + * + * **This is the one call in `src/` that stays on raw `strcmp` on purpose.** + * Everything else that reaches past libakstdlib to a function that library wraps + * has been converted to the wrapper and propagates the error context; the ruling + * that required it is libakstdlib #38, and the rule is that a function which + * cannot report an error changes its own signature rather than swallowing one. + * + * That remedy is unavailable here, and only here. `bsearch(3)` fixes the + * comparator's signature at `int (*)(const void *, const void *)`, so there is + * no out-parameter to hand an `akerr_ErrorContext *` back through and no return + * value that is not already the ordering. Converting this one site means + * dropping `bsearch` for an in-house binary search that can propagate -- a + * separate change to a lookup that runs once per scanned identifier, and a + * decision that belongs in its own issue rather than riding along with a port. + * + * The bypass is safe as written: `key` is the caller's NUL-terminated `upper` + * buffer and `element->name` is a string literal in the static table above, so + * neither argument can be NULL and neither can be unterminated -- which is the + * whole of what `aksl_strcmp` would have checked. + */ static int verb_compare(const void *key, const void *element) { const akbasic_Verb *verb = (const akbasic_Verb *)element; @@ -223,7 +246,7 @@ akerr_ErrorContext *akbasic_verb_lookup(const char *name, const akbasic_Verb **d FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in verb lookup"); *dest = NULL; - len = strlen(name); + PASS(errctx, aksl_strlen(name, &len)); if ( len == 0 || len >= sizeof(upper) ) { /* Too long to be any verb we know. Not an error, just a miss. */ SUCCEED_RETURN(errctx); diff --git a/tests/environment_scope.c b/tests/environment_scope.c index 4bf3ef5..c6115b9 100644 --- a/tests/environment_scope.c +++ b/tests/environment_scope.c @@ -13,6 +13,7 @@ int main(void) akbasic_Variable *again = NULL; akbasic_Value *value = NULL; int64_t lineno = 0; + bool waiting = false; int i = 0; TEST_REQUIRE_OK(harness_start(NULL)); @@ -57,24 +58,34 @@ int main(void) TEST_REQUIRE_STATUS(akbasic_runtime_prev_environment(&HARNESS_RUNTIME), AKBASIC_ERR_ENVIRONMENT); - /* The wait mechanism, including the parent-chain search. */ - TEST_REQUIRE(!akbasic_environment_is_waiting_for_any(root), "a fresh environment waits for nothing"); + /* + * The wait mechanism, including the parent-chain search. Both predicates + * report through an out-parameter and return an error context, so each + * assertion is a TEST_REQUIRE_OK on the call and a TEST_REQUIRE on the + * answer. See libakstdlib #38. + */ + TEST_REQUIRE_OK(akbasic_environment_is_waiting_for_any(root, &waiting)); + TEST_REQUIRE(!waiting, "a fresh environment waits for nothing"); TEST_REQUIRE_OK(akbasic_environment_wait_for_command(root, "NEXT")); - TEST_REQUIRE(akbasic_environment_is_waiting_for_any(root), "the wait must register"); - TEST_REQUIRE(akbasic_environment_is_waiting_for(root, "NEXT"), "the wait must match by name"); - TEST_REQUIRE(!akbasic_environment_is_waiting_for(root, "DATA"), "a different verb must not match"); + TEST_REQUIRE_OK(akbasic_environment_is_waiting_for_any(root, &waiting)); + TEST_REQUIRE(waiting, "the wait must register"); + TEST_REQUIRE_OK(akbasic_environment_is_waiting_for(root, "NEXT", &waiting)); + TEST_REQUIRE(waiting, "the wait must match by name"); + TEST_REQUIRE_OK(akbasic_environment_is_waiting_for(root, "DATA", &waiting)); + TEST_REQUIRE(!waiting, "a different verb must not match"); /* Two pending waits in one environment is a hard error, not a panic. */ TEST_REQUIRE_STATUS(akbasic_environment_wait_for_command(root, "DATA"), AKBASIC_ERR_STATE); TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME)); child = HARNESS_RUNTIME.environment; - TEST_REQUIRE(akbasic_environment_is_waiting_for(child, "NEXT"), - "a child must see its parent's wait"); + TEST_REQUIRE_OK(akbasic_environment_is_waiting_for(child, "NEXT", &waiting)); + TEST_REQUIRE(waiting, "a child must see its parent's wait"); TEST_REQUIRE_OK(akbasic_runtime_prev_environment(&HARNESS_RUNTIME)); TEST_REQUIRE_OK(akbasic_environment_stop_waiting(root, "NEXT")); - TEST_REQUIRE(!akbasic_environment_is_waiting_for_any(root), "stop_waiting must clear it"); + TEST_REQUIRE_OK(akbasic_environment_is_waiting_for_any(root, &waiting)); + TEST_REQUIRE(!waiting, "stop_waiting must clear it"); /* The per-line value pool is bounded and says so. */ for ( i = 0; i < AKBASIC_MAX_VALUES; i++ ) {