// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // obfuscate.c -- Release build name obfuscation // // See obfuscate.h for the high-level description. #include "obfuscate.h" #include "basEvents.h" #include "lexer.h" #include "../basRes.h" #include "../runtime/values.h" #include #include #include #define BAS_OBF_TOKEN_LEN BAS_MAX_IDENT // longest form/control/type token kept from a .frm line #define BAS_OBF_MAPPED_LEN 16 // "C" + decimal index + NUL #define BAS_OBF_MAP_INIT_CAP 16 // initial name-map capacity (doubles on growth) #define BAS_REM_KEYWORD_LEN 3 // "REM" // ============================================================ // Name map // ============================================================ typedef struct { char *orig; // original name (strdup'd, case-preserved) char *mapped; // new name (strdup'd, "C1" .. "Cn") } NameEntryT; typedef struct { NameEntryT *entries; int32_t count; int32_t cap; } NameMapT; // Function prototypes (alphabetical) void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms); int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap); static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved); static void emitBytes(uint8_t *out, int32_t outCap, int32_t *outLen, const char *src, int32_t len); static const char *nameMapAdd(NameMapT *m, const char *name); static void nameMapFree(NameMapT *m); static void nameMapInit(NameMapT *m); static const char *nameMapLookup(const NameMapT *m, const char *name); static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize); static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText); static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap); static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map); static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map); static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map); static const char *skipWhitespace(const char *p, const char *end); // ============================================================ // Top-level entry point // ============================================================ void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms) { if (!mod || frmCount < 0) { return; } // Pass 1: collect all names from all .frm texts. A control whose name // is also a control type or a property key (a TextBox named "Text", a // Timer named "Timer") is left alone: the constant pool cannot tell a // name reference apart from the identically spelled property/type name // used by OP_LOAD_PROP and friends. NameMapT names; NameMapT reserved; NameMapT map; nameMapInit(&names); nameMapInit(&reserved); nameMapInit(&map); for (int32_t i = 0; i < frmCount; i++) { if (frmTexts[i] && frmLens[i] > 0) { collectNamesFromFrm(frmTexts[i], frmLens[i], &names, &reserved); } } for (int32_t i = 0; i < names.count; i++) { if (!nameMapLookup(&reserved, names.entries[i].orig)) { nameMapAdd(&map, names.entries[i].orig); } } nameMapFree(&names); nameMapFree(&reserved); // Pass 2: rewrite each .frm, sized by a measuring pass so a form with // many short names that grow ("a" -> "C12") can never be truncated. for (int32_t i = 0; i < frmCount; i++) { outFrms[i].data = NULL; outFrms[i].len = 0; if (!frmTexts[i] || frmLens[i] <= 0) { continue; } int32_t strippedLen = basFindFormEndPos(frmTexts[i], frmLens[i]); int32_t outCap = rewriteFrmText(frmTexts[i], strippedLen, &map, NULL, 0) + 1; uint8_t *outBuf = malloc(outCap); if (!outBuf) { continue; } int32_t outLen = rewriteFrmText(frmTexts[i], strippedLen, &map, outBuf, outCap); // Ensure trailing newline if (outLen > 0 && outBuf[outLen - 1] != '\n') { outBuf[outLen++] = '\n'; } outFrms[i].data = outBuf; outFrms[i].len = outLen; } // Pass 3: rewrite module rewriteModuleConstants(mod, &map); rewriteModuleProcs(mod, &map); rewriteModuleFormVars(mod, &map); nameMapFree(&map); } int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap) { if (!src || srcLen <= 0 || !outBuf || outCap <= 0) { return 0; } int32_t outLen = 0; int32_t i = 0; while (i < srcLen) { int32_t lineStart = i; while (i < srcLen && src[i] != '\n' && src[i] != '\r') { i++; } int32_t lineEnd = i; if (i < srcLen && src[i] == '\r') { i++; } if (i < srcLen && src[i] == '\n') { i++; } // Scan for first unquoted ' (comment start). bool inStr = false; int32_t commentStart = -1; for (int32_t j = lineStart; j < lineEnd; j++) { char c = src[j]; if (c == '"') { inStr = !inStr; } else if (c == '\'' && !inStr) { commentStart = j; break; } } int32_t contentEnd = (commentStart >= 0) ? commentStart : lineEnd; // Check for whole-line REM. Find first non-whitespace position. int32_t firstNonWs = lineStart; while (firstNonWs < contentEnd && (src[firstNonWs] == ' ' || src[firstNonWs] == '\t')) { firstNonWs++; } if (contentEnd - firstNonWs >= BAS_REM_KEYWORD_LEN && strncasecmp(src + firstNonWs, "REM", BAS_REM_KEYWORD_LEN) == 0 && (contentEnd - firstNonWs == BAS_REM_KEYWORD_LEN || src[firstNonWs + BAS_REM_KEYWORD_LEN] == ' ' || src[firstNonWs + BAS_REM_KEYWORD_LEN] == '\t')) { contentEnd = firstNonWs; } // Trim trailing whitespace. while (contentEnd > lineStart && (src[contentEnd - 1] == ' ' || src[contentEnd - 1] == '\t')) { contentEnd--; } // Drop lines that have no non-whitespace content. if (contentEnd <= firstNonWs) { continue; } // Strip leading whitespace -- the form parser trims per line // anyway, so shipping indentation just bloats the embedded resource. int32_t writeLen = contentEnd - firstNonWs; if (outLen + writeLen + 1 >= outCap) { break; } memcpy(outBuf + outLen, src + firstNonWs, writeLen); outLen += writeLen; outBuf[outLen++] = '\n'; } return outLen; } // ============================================================ // Pass 1: collect form/control names and the identifiers they must not // collide with // ============================================================ // Scan a .frm text: every "Begin " adds Name to names, and // Type plus every " = ..." property key goes into reserved. static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved) { const char *p = text; const char *end = text + len; while (p < end) { // Read one line const char *lineStart = p; while (p < end && *p != '\n' && *p != '\r') { p++; } const char *lineEnd = p; if (p < end && *p == '\r') { p++; } if (p < end && *p == '\n') { p++; } // Trim leading whitespace const char *l = skipWhitespace(lineStart, lineEnd); char token[BAS_OBF_TOKEN_LEN]; if ((lineEnd - l) >= BAS_BEGIN_PREFIX_LEN && strncasecmp(l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) { l = skipWhitespace(l + BAS_BEGIN_PREFIX_LEN, lineEnd); l = readToken(l, lineEnd, token, sizeof(token)); if (token[0] == '\0') { continue; } nameMapAdd(reserved, token); l = skipWhitespace(l, lineEnd); readToken(l, lineEnd, token, sizeof(token)); if (token[0] && basIsValidIdent(token)) { nameMapAdd(names, token); } continue; } // " = value": the key is a property name. l = readToken(l, lineEnd, token, sizeof(token)); l = skipWhitespace(l, lineEnd); if (l < lineEnd && *l == '=' && token[0] && basIsValidIdent(token)) { nameMapAdd(reserved, token); } } } // ============================================================ // Pass 2: rewrite .frm text with mapped names // ============================================================ // Appends len bytes of src to out (bounded by outCap) and always advances // *outLen, so a NULL out measures the exact output size. static void emitBytes(uint8_t *out, int32_t outCap, int32_t *outLen, const char *src, int32_t len) { if (out && *outLen + len <= outCap) { memcpy(out + *outLen, src, len); } *outLen += len; } // Returns true if c is a valid identifier character. static const char *nameMapAdd(NameMapT *m, const char *name) { const char *existing = nameMapLookup(m, name); if (existing) { return existing; } if (m->count >= m->cap) { int32_t newCap = m->cap == 0 ? BAS_OBF_MAP_INIT_CAP : m->cap * 2; NameEntryT *newEntries = realloc(m->entries, newCap * sizeof(NameEntryT)); if (!newEntries) { return NULL; } m->entries = newEntries; m->cap = newCap; } char mapped[BAS_OBF_MAPPED_LEN]; snprintf(mapped, sizeof(mapped), "C%ld", (long)(m->count + 1)); m->entries[m->count].orig = strdup(name); m->entries[m->count].mapped = strdup(mapped); m->count++; return m->entries[m->count - 1].mapped; } static void nameMapFree(NameMapT *m) { for (int32_t i = 0; i < m->count; i++) { free(m->entries[i].orig); free(m->entries[i].mapped); } free(m->entries); m->entries = NULL; m->count = 0; m->cap = 0; } static void nameMapInit(NameMapT *m) { m->entries = NULL; m->count = 0; m->cap = 0; } // Look up an original name (case-insensitive). Returns mapped name or NULL. static const char *nameMapLookup(const NameMapT *m, const char *name) { for (int32_t i = 0; i < m->count; i++) { if (strcasecmp(m->entries[i].orig, name) == 0) { return m->entries[i].mapped; } } return NULL; } // Copy next whitespace-delimited token into buf. Returns pointer after token. static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize) { int32_t len = 0; while (p < end && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && len < bufSize - 1) { buf[len++] = *p++; } buf[len] = '\0'; return p; } // ============================================================ // Module rewriting // ============================================================ // Replace the contents of a constant pool entry with a new string. static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText) { BasStringT *newStr = basStringNew(newText, (int32_t)strlen(newText)); if (!newStr) { return; } basStringUnref(mod->constants[idx]); mod->constants[idx] = newStr; } // Rewrites the .frm text line by line. Only positions that hold a // form/control NAME are remapped: the name token of a "Begin " // line and a property value that is exactly a mapped name (bare, or the // whole content of a quoted string such as DataSource = "datCat"). Type // tokens and property keys are copied verbatim. Identifiers of any length // are copied from the source span, never through a fixed buffer. With out // NULL nothing is written and the return value is the required size. static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap) { int32_t outLen = 0; const char *p = src; const char *end = src + srcLen; while (p < end) { const char *lineStart = p; while (p < end && *p != '\n' && *p != '\r') { p++; } const char *lineEnd = p; if (p < end && *p == '\r') { p++; } if (p < end && *p == '\n') { p++; } const char *l = skipWhitespace(lineStart, lineEnd); const char *replaceStart = NULL; // span of the original name to swap const char *replaceEnd = NULL; const char *mapped = NULL; char token[BAS_OBF_TOKEN_LEN]; if ((lineEnd - l) >= BAS_BEGIN_PREFIX_LEN && strncasecmp(l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) { // Begin : only the name token is a candidate. const char *t = skipWhitespace(l + BAS_BEGIN_PREFIX_LEN, lineEnd); t = readToken(t, lineEnd, token, sizeof(token)); t = skipWhitespace(t, lineEnd); const char *nameStart = t; t = readToken(t, lineEnd, token, sizeof(token)); if (token[0] && t - nameStart == (int32_t)strlen(token)) { mapped = nameMapLookup(map, token); replaceStart = nameStart; replaceEnd = t; } } else { // = value: a value that is exactly a mapped name (bare or // quoted) refers to a control and follows the rename. const char *t = readToken(l, lineEnd, token, sizeof(token)); t = skipWhitespace(t, lineEnd); if (t < lineEnd && *t == '=') { t = skipWhitespace(t + 1, lineEnd); const char *valueStart = t; const char *valueEnd = lineEnd; while (valueEnd > valueStart && (valueEnd[-1] == ' ' || valueEnd[-1] == '\t')) { valueEnd--; } if (valueEnd - valueStart >= 2 && *valueStart == '"' && valueEnd[-1] == '"') { valueStart++; valueEnd--; } int32_t valueLen = (int32_t)(valueEnd - valueStart); bool isIdent = (valueLen > 0 && valueLen < (int32_t)sizeof(token)); for (int32_t k = 0; isIdent && k < valueLen; k++) { isIdent = basIsIdentChar(valueStart[k]); } if (isIdent) { memcpy(token, valueStart, valueLen); token[valueLen] = '\0'; mapped = nameMapLookup(map, token); replaceStart = valueStart; replaceEnd = valueEnd; } } } if (mapped) { emitBytes(out, outCap, &outLen, lineStart, (int32_t)(replaceStart - lineStart)); emitBytes(out, outCap, &outLen, mapped, (int32_t)strlen(mapped)); emitBytes(out, outCap, &outLen, replaceEnd, (int32_t)(p - replaceEnd)); } else { emitBytes(out, outCap, &outLen, lineStart, (int32_t)(p - lineStart)); } } return outLen; } static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) { // KNOWN LIMITATION: the constant pool dedupes a plain string literal and a // same-text control/form-name reference into ONE entry, so a release build // that does e.g. MsgBox "Save" while also having a control named "Save" // will see that literal rewritten to the obfuscated name ("C5"). Fully // separating name references from literals would give name refs their own // pool slots and change emitted bytecode for every release build, so it is // deferred; avoid string literals that exactly match a control/form name // in release builds. Names that collide with a property key or control // type are never mapped at all (see basObfuscateNames). for (int32_t i = 0; i < mod->constCount; i++) { const BasStringT *s = mod->constants[i]; if (!s) { continue; } const char *mapped = nameMapLookup(map, s->data); if (mapped) { replaceConstant(mod, i, mapped); continue; } // "_" handler names used as SetEvent targets follow the // procedure rename so basModuleFindProc still resolves them. const char *underscore = strrchr(s->data, '_'); if (!underscore || !basEventSuffixMatch(underscore + 1)) { continue; } int32_t prefixLen = (int32_t)(underscore - s->data); char prefix[BAS_MAX_IDENT]; if (prefixLen >= (int32_t)sizeof(prefix)) { continue; } memcpy(prefix, s->data, prefixLen); prefix[prefixLen] = '\0'; mapped = nameMapLookup(map, prefix); if (mapped) { char newName[BAS_MAX_IDENT]; snprintf(newName, sizeof(newName), "%s_%s", mapped, underscore + 1); replaceConstant(mod, i, newName); } } } static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map) { for (int32_t i = 0; i < mod->formVarInfoCount; i++) { BasFormVarInfoT *fv = &mod->formVarInfo[i]; const char *mapped = nameMapLookup(map, fv->formName); if (mapped) { snprintf(fv->formName, sizeof(fv->formName), "%s", mapped); } } } static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) { for (int32_t i = 0; i < mod->procCount; i++) { BasProcEntryT *proc = &mod->procs[i]; if (proc->name[0] == '\0') { continue; } // Remap the owning form name (used at runtime to bind form-scope // variables). The form itself gets renamed by the same pass. if (proc->formName[0]) { const char *mappedForm = nameMapLookup(map, proc->formName); if (mappedForm) { snprintf(proc->formName, sizeof(proc->formName), "%s", mappedForm); } } // Find last underscore char *underscore = strrchr(proc->name, '_'); if (!underscore) { continue; } const char *suffix = underscore + 1; if (!basEventSuffixMatch(suffix)) { continue; } // Split on underscore int32_t prefixLen = (int32_t)(underscore - proc->name); char prefix[BAS_MAX_IDENT]; if (prefixLen >= (int32_t)sizeof(prefix)) { prefixLen = (int32_t)sizeof(prefix) - 1; } memcpy(prefix, proc->name, prefixLen); prefix[prefixLen] = '\0'; const char *mapped = nameMapLookup(map, prefix); if (mapped) { char newName[BAS_MAX_IDENT]; snprintf(newName, sizeof(newName), "%s_%s", mapped, suffix); snprintf(proc->name, sizeof(proc->name), "%s", newName); } } } // ============================================================ // .frm parsing helpers // ============================================================ // Skip ASCII whitespace. Returns pointer past whitespace. static const char *skipWhitespace(const char *p, const char *end) { while (p < end && (*p == ' ' || *p == '\t')) { p++; } return p; }