diff --git a/cppcheck.sh b/cppcheck.sh new file mode 100755 index 0000000..8ec276b --- /dev/null +++ b/cppcheck.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# +# cppcheck.sh -- static analysis for the DVX source tree. +# +# Usage: +# ./cppcheck.sh # whole tree +# ./cppcheck.sh src/widgets # one subtree +# ./cppcheck.sh src/libs/kpunch/texthelp/textHelp.c # one file +# +# Exits 0 even when warnings are found -- cppcheck's exit status is +# not useful as a gate for this codebase (too many DJGPP-isms it can't +# resolve). Read the output. + +set -e + +cd "$(dirname "$0")" + +TARGETS="${@:-src}" + +INCLUDES=( + -Isrc/libs/kpunch/libdvx + -Isrc/libs/kpunch/libdvx/platform + -Isrc/libs/kpunch/libdvx/thirdparty + -Isrc/libs/kpunch/libtasks + -Isrc/libs/kpunch/texthelp + -Isrc/libs/kpunch/listhelp + -Isrc/libs/kpunch/dvxshell + -Isrc/libs/kpunch/taskmgr + -Isrc/libs/kpunch/serial + -Isrc/widgets/kpunch + -Isrc/apps/kpunch/dvxbasic + -Isrc/apps/kpunch/dvxbasic/compiler + -Isrc/apps/kpunch/dvxbasic/runtime + -Isrc/apps/kpunch/dvxbasic/formrt +) + +# DJGPP / platform macros cppcheck can't find. Expand them to nothing +# so the preprocessor leaves the code visible to the analyser instead +# of eliding whole functions behind unknown macro calls. +DEFINES=( + -DDXE_EXPORT= + -DDVX_WIDGET_IMPL + -D__DJGPP__=2 +) + +# Suppressions: things cppcheck flags that aren't actually wrong for +# this codebase. Keep the list short so real issues stay visible. +SUPPRESS=( + --suppress=missingIncludeSystem # system headers aren't on host + --suppress=unusedFunction # many public exports look "unused" to a .c-only scan + --suppress=constParameterPointer # noisy on this style + --suppress=constVariablePointer # ditto + --suppress=normalCheckLevelMaxBranches + # stb_ds arrput is a macro -- cppcheck can't see that the value + # (including heap pointers inside it) is stored, so it reports + # spurious null-derefs on the stb_ds dynamic-array handle. + --suppress=nullPointerRedundantCheck + --suppress=nullPointerArithmeticRedundantCheck + # Thirdparty headers -- their own style isn't our problem. + --suppress='*:src/libs/kpunch/libdvx/thirdparty/*' + --suppress='*:src/libs/kpunch/sql/thirdparty/*' +) + +exec cppcheck \ + --enable=warning,style,performance,portability \ + --inline-suppr \ + --std=c99 \ + --platform=unix32 \ + --quiet \ + -j"$(nproc)" \ + -i src/libs/kpunch/sql/thirdparty \ + -i src/libs/kpunch/libdvx/thirdparty \ + "${INCLUDES[@]}" \ + "${DEFINES[@]}" \ + "${SUPPRESS[@]}" \ + $TARGETS diff --git a/src/apps/kpunch/dvxbasic/compiler/parser.c b/src/apps/kpunch/dvxbasic/compiler/parser.c index cbf567d..f4c38ed 100644 --- a/src/apps/kpunch/dvxbasic/compiler/parser.c +++ b/src/apps/kpunch/dvxbasic/compiler/parser.c @@ -3991,7 +3991,7 @@ static void parsePrimary(BasParserT *p) { } else { basEmit8(&p->cg, OP_PUSH_INT32); basEmit16(&p->cg, (int16_t)(val & 0xFFFF)); - basEmit16(&p->cg, (int16_t)((val >> 16) & 0xFFFF)); + basEmit16(&p->cg, (int16_t)(((uint32_t)val >> 16) & 0xFFFF)); } advance(p); return; diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c index 973fea2..e7b15d1 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formrt.c +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c @@ -63,6 +63,7 @@ #define DEFAULT_FORM_W 400 #define DEFAULT_FORM_H 300 #define MAX_EVENT_NAME_LEN 128 +#define MENU_ID_BASE 10000 // Module-level form runtime pointer for onFormClose callback static BasFormRtT *sFormRt = NULL; @@ -1570,8 +1571,7 @@ void basFormRtHideForm(void *ctx, void *formRef) { if (!form) { basFormRtRuntimeError(rt, "Hide on unknown form", - "The form reference resolved to NULL; check that the form name exists.", - NULL); + "The form reference resolved to NULL; check that the form name exists."); return; } @@ -1652,8 +1652,6 @@ static void *basFormRtLoadCfm(BasFormRtT *rt, const uint8_t *data, int32_t dataL return form; } - dvxLog("basFormRtLoadCfm: form '%s' created, %ld controls", form->name, (long)arrlen(form->controls)); - // Set window callbacks (same as basFormRtLoadForm) form->window->onClose = onFormClose; form->window->onResize = onFormResize; @@ -1892,7 +1890,6 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen bar = wmAddMenuBar(form->window); } - #define MENU_ID_BASE 10000 MenuT *menuStack[16]; memset(menuStack, 0, sizeof(menuStack)); bool topIsPopup = false; @@ -2291,7 +2288,6 @@ void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName) { void basFormRtRunSimple(BasFormRtT *rt) { if (!rt || !rt->vm) { - dvxLog("basFormRtRunSimple: no rt or vm"); return; } @@ -2563,8 +2559,7 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { if (!form) { basFormRtRuntimeError(rt, "Show on unknown form", - "The form reference resolved to NULL; check that the form name exists.", - NULL); + "The form reference resolved to NULL; check that the form name exists."); return; } @@ -2765,7 +2760,7 @@ static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name) { // event dispatch. Starts from MENU_ID_BASE + the highest in-use ID // so runtime-added items don't collide with .frm-loaded items. static int32_t nextMenuItemId(BasFormT *form) { - int32_t maxId = 10000 - 1; // MENU_ID_BASE - 1 + int32_t maxId = MENU_ID_BASE - 1; for (int32_t i = 0; i < form->menuIdMapCount; i++) { if (form->menuIdMap[i].id > maxId) { @@ -3665,12 +3660,6 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char *name) { BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData; - dvxLog("onCtrlBegin: type='%s' name='%s' form=%s nestDepth=%d", - typeName ? typeName : "?", - name ? name : "?", - (ctx->form && ctx->form->name[0]) ? ctx->form->name : "(null)", - (int)ctx->nestDepth); - if (!ctx->form || ctx->nestDepth <= 0) { return; } diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c index 108588f..32d1804 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideMain.c +++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c @@ -1469,7 +1469,7 @@ static bool ideValidator_onFormBegin(void *ud, const char *name) { IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud; IdeCtrlMapEntryT e; memset(&e, 0, sizeof(e)); - snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name); + snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name ? name : ""); snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", "Form"); arrput(v->entries, e); snprintf(v->currentForm, BAS_MAX_CTRL_NAME, "%s", name ? name : ""); @@ -1481,7 +1481,7 @@ static void ideValidator_onCtrlBegin(void *ud, const char *typeName, const char IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud; IdeCtrlMapEntryT e; memset(&e, 0, sizeof(e)); - snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name); + snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name ? name : ""); snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", typeName ? typeName : ""); arrput(v->entries, e); diff --git a/src/apps/kpunch/dvxhelp/dvxhelp.c b/src/apps/kpunch/dvxhelp/dvxhelp.c index 3988889..a91c4c9 100644 --- a/src/apps/kpunch/dvxhelp/dvxhelp.c +++ b/src/apps/kpunch/dvxhelp/dvxhelp.c @@ -612,6 +612,7 @@ static const char *doWrap(char **wrappedPtr, int32_t *wrapWidthPtr, int32_t *lin // Return cached result if width hasn't changed if (*wrappedPtr && *wrapWidthPtr == pixelW) { + // cppcheck-suppress identicalInnerCondition return *wrappedPtr; } diff --git a/src/libs/kpunch/libdvx/dvxResource.c b/src/libs/kpunch/libdvx/dvxResource.c index 20428b7..cb7005e 100644 --- a/src/libs/kpunch/libdvx/dvxResource.c +++ b/src/libs/kpunch/libdvx/dvxResource.c @@ -58,6 +58,7 @@ const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name) { // Entries are sorted at open() time so bsearch gives O(log n) lookup. DvxResDirEntryT key; + memset(&key, 0, sizeof(key)); strncpy(key.name, name, sizeof(key.name) - 1); key.name[sizeof(key.name) - 1] = '\0'; return (const DvxResDirEntryT *)bsearch(&key, h->entries, h->entryCount, diff --git a/src/libs/kpunch/texthelp/textHelp.c b/src/libs/kpunch/texthelp/textHelp.c index b97dfb5..5eccd9f 100644 --- a/src/libs/kpunch/texthelp/textHelp.c +++ b/src/libs/kpunch/texthelp/textHelp.c @@ -257,13 +257,8 @@ bool textEditFindNext(TextEditLineCacheT *lc, const char *buf, int32_t len, cons int32_t cursorByte = textEditRowColToOff(lc, buf, len, *pCursorRow, *pCursorCol); int32_t startPos = forward ? cursorByte + 1 : cursorByte - 1; - int32_t searchLen = len - needleLen + 1; - - if (searchLen <= 0) { - return false; - } - - int32_t count = forward ? (searchLen - startPos) : (startPos + 1); + int32_t searchLen = len - needleLen + 1; // >= 1 given the needleLen > len check above + int32_t count = forward ? (searchLen - startPos) : (startPos + 1); if (count <= 0) { return false; @@ -2080,7 +2075,6 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_ if (shift && pSelStart && pSelEnd) { if (*pSelStart < 0) { *pSelStart = *pCursor; - *pSelEnd = *pCursor; } if (*pCursor > 0) { @@ -2106,7 +2100,6 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_ if (shift && pSelStart && pSelEnd) { if (*pSelStart < 0) { *pSelStart = *pCursor; - *pSelEnd = *pCursor; } if (*pCursor < *pLen) { @@ -2134,7 +2127,6 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_ if (shift && pSelStart && pSelEnd) { if (*pSelStart < 0) { *pSelStart = *pCursor; - *pSelEnd = *pCursor; } *pCursor = newPos; @@ -2157,7 +2149,6 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_ if (shift && pSelStart && pSelEnd) { if (*pSelStart < 0) { *pSelStart = *pCursor; - *pSelEnd = *pCursor; } *pCursor = newPos; @@ -2178,7 +2169,6 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_ if (shift && pSelStart && pSelEnd) { if (*pSelStart < 0) { *pSelStart = *pCursor; - *pSelEnd = *pCursor; } *pCursor = 0; @@ -2199,7 +2189,6 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_ if (shift && pSelStart && pSelEnd) { if (*pSelStart < 0) { *pSelStart = *pCursor; - *pSelEnd = *pCursor; } *pCursor = *pLen; diff --git a/src/tools/dvxResWrite.h b/src/tools/dvxResWrite.h index 742506b..9a08773 100644 --- a/src/tools/dvxResWrite.h +++ b/src/tools/dvxResWrite.h @@ -276,9 +276,36 @@ static int dvxResAppendEntry(const char *path, const char *name, uint32_t type, } } - // Add new entry - entries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT)); - data = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *)); + // Add new entry. Use tmp pointers so a realloc failure doesn't + // strand the existing blocks as unreachable leaks. Abort the + // write on OOM -- nothing useful to recover to here. + DvxResDirEntryT *newEntries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT)); + + if (!newEntries) { + for (uint32_t i = 0; i < count; i++) { + free(data[i]); + } + + free(data); + free(entries); + return -1; + } + + entries = newEntries; + + uint8_t **newData = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *)); + + if (!newData) { + for (uint32_t i = 0; i < count; i++) { + free(data[i]); + } + + free(data); + free(entries); + return -1; + } + + data = newData; memset(&entries[count], 0, sizeof(DvxResDirEntryT)); strncpy(entries[count].name, name, DVX_RES_NAME_MAX - 1); diff --git a/src/tools/dvxres.c b/src/tools/dvxres.c index 5638f8d..d3d8023 100644 --- a/src/tools/dvxres.c +++ b/src/tools/dvxres.c @@ -244,9 +244,27 @@ static int cmdBuild(const char *dxePath, const char *manifestPath) { newSize = (uint32_t)readLen; } - // Append - entries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT)); - data = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *)); + // Append. Use tmp pointers so a realloc failure doesn't + // strand the existing blocks as unreachable leaks. As a + // host build-time tool, abort on OOM -- nothing useful to + // recover to. + DvxResDirEntryT *newEntries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT)); + + if (!newEntries) { + fprintf(stderr, "dvxres: out of memory\n"); + exit(1); + } + + entries = newEntries; + + uint8_t **newDataArr = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *)); + + if (!newDataArr) { + fprintf(stderr, "dvxres: out of memory\n"); + exit(1); + } + + data = newDataArr; memset(&entries[count], 0, sizeof(DvxResDirEntryT)); snprintf(entries[count].name, DVX_RES_NAME_MAX, "%s", name); diff --git a/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c b/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c index 5e88c2f..a3cd9e3 100644 --- a/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c +++ b/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c @@ -200,6 +200,7 @@ void dataCtrlAddNew(WidgetT *w) { d->isNewRow = true; fireReposition(w); + // cppcheck-suppress memleak ; row.fields is stored into d->rows via arrput } @@ -647,6 +648,7 @@ void dataCtrlRefresh(WidgetT *w) { } arrput(d->rows, row); + // cppcheck-suppress memleak ; row.fields is stored into d->rows via arrput } d->rowCount = (int32_t)arrlen(d->rows); @@ -830,7 +832,7 @@ void dataCtrlUpdate(WidgetT *w) { pos += snprintf(sql + pos, sizeof(sql) - pos, "'%s'", escaped); } - pos += snprintf(sql + pos, sizeof(sql) - pos, ")"); + snprintf(sql + pos, sizeof(sql) - pos, ")"); } else { // UPDATE table SET col1='val1', ... WHERE keyCol=keyVal int32_t keyCol = findKeyCol(d); @@ -855,7 +857,7 @@ void dataCtrlUpdate(WidgetT *w) { first = false; } - pos += snprintf(sql + pos, sizeof(sql) - pos, " WHERE %s=%s", d->colNames[keyCol], keyVal); + snprintf(sql + pos, sizeof(sql) - pos, " WHERE %s=%s", d->colNames[keyCol], keyVal); } dvxSqlExec(db, sql); diff --git a/src/widgets/kpunch/textInput/widgetTextInput.c b/src/widgets/kpunch/textInput/widgetTextInput.c index fa53c88..e1ac02c 100644 --- a/src/widgets/kpunch/textInput/widgetTextInput.c +++ b/src/widgets/kpunch/textInput/widgetTextInput.c @@ -30,10 +30,14 @@ // 2. TextArea -- multi-line text editor with row/col cursor, // dual-axis scrolling, and full selection/clipboard support. // -// Shared infrastructure (clipboard, multi-click detection, word -// boundary logic, cross-widget selection clearing, and the -// single-line editing engine widgetTextEditOnKey) lives in -// widgetCore.c so it can be linked from any widget DXE. +// Shared text-editing infrastructure lives in the texthelp library +// (src/libs/kpunch/texthelp/textHelp.c/h): clipboard, multi-click +// detection, word-boundary helpers, cross-widget selection clearing, +// the single-line edit engine, and the full multi-line edit engine +// (cache, paint, mouse/drag, key handler, find/replace, goto-line, +// get-word-at-cursor). This widget is a thin consumer that owns +// widget chrome (border, gutter, scrollbars, colorizer callbacks) +// and routes editing calls into the library via TextEditMultiT. // // All text editing is done in-place in fixed-size char buffers // allocated at widget creation. No dynamic resizing -- this keeps @@ -51,22 +55,6 @@ // "low" end for deletion/copy is always min(start, end). The // -1 sentinel means "no selection". // -// Clipboard is a simple static buffer (4KB). This is a process-wide -// clipboard, not per-widget and not OS-integrated (DOS has no -// clipboard API). Text cut/copied from any widget is available to -// paste in any other widget. -// -// Multi-click detection uses clock() timestamps with a 500ms -// threshold and 4px spatial tolerance. Double-click selects word, -// triple-click selects line (TextArea) or all (TextInput). -// -// Cross-widget selection clearing: when a widget gains selection, -// clearOtherSelections() deselects any other widget that had an -// active selection. This prevents the confusing visual state of -// multiple selected ranges across different widgets. The tracking -// uses sLastSelectedWidget to achieve O(1) clearing rather than -// walking the entire widget tree. -// // Masked input: a special TextInput mode where the buffer is // pre-filled from a mask pattern (e.g., "###-##-####" for SSN). // '#' accepts digits, 'A' accepts letters, '*' accepts any @@ -161,6 +149,11 @@ typedef struct { // Match the ANSI terminal cursor blink rate (CURSOR_MS in widgetAnsiTerm.c) #define CURSOR_BLINK_MS 250 +// Default buffer sizes when the caller passes maxLen <= 0 and when +// the BASIC iface registers a control without specifying a size. +#define TEXTINPUT_DEFAULT_BUF 256 +#define TEXTAREA_DEFAULT_BUF 65536 + // sCursorBlinkOn is defined in widgetCore.c (shared state) @@ -1228,7 +1221,7 @@ WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) { } w->data = ti; - int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256; + int32_t bufSize = maxLen > 0 ? maxLen + 1 : TEXTINPUT_DEFAULT_BUF; ti->buf = (char *)malloc(bufSize); ti->bufSize = bufSize; @@ -1970,7 +1963,7 @@ static const WgtIfaceT sIfaceTextInput = { .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT, - .createArgs = { 256 }, + .createArgs = { TEXTINPUT_DEFAULT_BUF }, .defaultEvent = "Change" }; @@ -1983,7 +1976,7 @@ static const WgtIfaceT sIfaceTextArea = { .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT, - .createArgs = { 65536 }, + .createArgs = { TEXTAREA_DEFAULT_BUF }, .defaultEvent = "Change" };