mirror of
https://github.com/tbamud/tbamud.git
synced 2026-04-30 04:41:51 +02:00
* Fix stack buffer overflow in perform_complex_alias() - add bounds checks * Refactor: use return value instead of char_data param in perform_complex_alias() * Add unit tests for perform_complex_alias() via perform_alias() Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: welcor <357770+welcor@users.noreply.github.com>
503 lines
14 KiB
C
503 lines
14 KiB
C
/**
|
||
* @file test_interpreter.c
|
||
* Unit tests for pure string-handling functions in src/interpreter.c:
|
||
* is_number, is_abbrev, delete_doubledollar, any_one_arg, one_word
|
||
* and for the alias expansion path perform_complex_alias() (via perform_alias()).
|
||
*/
|
||
|
||
#include "unity.h"
|
||
|
||
#include "conf.h"
|
||
#include "sysdep.h"
|
||
#include "structs.h"
|
||
#include "utils.h"
|
||
#include "comm.h"
|
||
#include "interpreter.h"
|
||
|
||
extern FILE *logfile;
|
||
|
||
void setUp(void) { logfile = stderr; }
|
||
void tearDown(void) { logfile = NULL; }
|
||
|
||
/* =========================================================
|
||
* write_to_q — real implementation for alias tests.
|
||
* Overrides the __attribute__((weak)) stub in test_stubs.c so that
|
||
* the queue is actually populated and we can inspect its contents.
|
||
* ========================================================= */
|
||
|
||
void write_to_q(const char *txt, struct txt_q *queue, int aliased)
|
||
{
|
||
struct txt_block *newt;
|
||
CREATE(newt, struct txt_block, 1);
|
||
newt->text = strdup(txt);
|
||
newt->aliased = aliased;
|
||
if (!queue->head) {
|
||
newt->next = NULL;
|
||
queue->head = queue->tail = newt;
|
||
} else {
|
||
queue->tail->next = newt;
|
||
queue->tail = newt;
|
||
newt->next = NULL;
|
||
}
|
||
}
|
||
|
||
/* =========================================================
|
||
* is_number
|
||
* ========================================================= */
|
||
|
||
void test_is_number_digits_only(void)
|
||
{
|
||
TEST_ASSERT_TRUE(is_number("42"));
|
||
}
|
||
|
||
void test_is_number_zero(void)
|
||
{
|
||
TEST_ASSERT_TRUE(is_number("0"));
|
||
}
|
||
|
||
void test_is_number_negative(void)
|
||
{
|
||
TEST_ASSERT_TRUE(is_number("-5"));
|
||
}
|
||
|
||
void test_is_number_empty_string(void)
|
||
{
|
||
TEST_ASSERT_FALSE(is_number(""));
|
||
}
|
||
|
||
void test_is_number_contains_letter(void)
|
||
{
|
||
TEST_ASSERT_FALSE(is_number("12x3"));
|
||
}
|
||
|
||
void test_is_number_minus_only(void)
|
||
{
|
||
TEST_ASSERT_FALSE(is_number("-"));
|
||
}
|
||
|
||
void test_is_number_float(void)
|
||
{
|
||
TEST_ASSERT_FALSE(is_number("3.14"));
|
||
}
|
||
|
||
/* =========================================================
|
||
* is_abbrev
|
||
* ========================================================= */
|
||
|
||
void test_is_abbrev_exact_match(void)
|
||
{
|
||
TEST_ASSERT_TRUE(is_abbrev("north", "north"));
|
||
}
|
||
|
||
void test_is_abbrev_valid_prefix(void)
|
||
{
|
||
TEST_ASSERT_TRUE(is_abbrev("nort", "north"));
|
||
TEST_ASSERT_TRUE(is_abbrev("n", "north"));
|
||
}
|
||
|
||
void test_is_abbrev_non_prefix(void)
|
||
{
|
||
TEST_ASSERT_FALSE(is_abbrev("south", "north"));
|
||
}
|
||
|
||
void test_is_abbrev_empty_arg1(void)
|
||
{
|
||
TEST_ASSERT_FALSE(is_abbrev("", "north"));
|
||
}
|
||
|
||
void test_is_abbrev_arg1_longer_than_arg2(void)
|
||
{
|
||
TEST_ASSERT_FALSE(is_abbrev("northward", "north"));
|
||
}
|
||
|
||
void test_is_abbrev_case_insensitive(void)
|
||
{
|
||
TEST_ASSERT_TRUE(is_abbrev("Nor", "north"));
|
||
TEST_ASSERT_TRUE(is_abbrev("NOR", "NORTH"));
|
||
}
|
||
|
||
/* =========================================================
|
||
* delete_doubledollar
|
||
* ========================================================= */
|
||
|
||
void test_delete_doubledollar_no_dollars(void)
|
||
{
|
||
char s[] = "hello";
|
||
delete_doubledollar(s);
|
||
TEST_ASSERT_EQUAL_STRING("hello", s);
|
||
}
|
||
|
||
void test_delete_doubledollar_double_at_start(void)
|
||
{
|
||
char s[] = "$$hello";
|
||
delete_doubledollar(s);
|
||
TEST_ASSERT_EQUAL_STRING("$hello", s);
|
||
}
|
||
|
||
void test_delete_doubledollar_double_in_middle(void)
|
||
{
|
||
char s[] = "hello$$world";
|
||
delete_doubledollar(s);
|
||
TEST_ASSERT_EQUAL_STRING("hello$world", s);
|
||
}
|
||
|
||
void test_delete_doubledollar_four_dollars(void)
|
||
{
|
||
char s[] = "$$$$";
|
||
delete_doubledollar(s);
|
||
TEST_ASSERT_EQUAL_STRING("$$", s);
|
||
}
|
||
|
||
void test_delete_doubledollar_single_dollar_unchanged(void)
|
||
{
|
||
char s[] = "hello$world";
|
||
delete_doubledollar(s);
|
||
TEST_ASSERT_EQUAL_STRING("hello$world", s);
|
||
}
|
||
|
||
/* =========================================================
|
||
* any_one_arg
|
||
* ========================================================= */
|
||
|
||
void test_any_one_arg_basic(void)
|
||
{
|
||
char first[64];
|
||
char *rest = any_one_arg("hello world", first);
|
||
TEST_ASSERT_EQUAL_STRING("hello", first);
|
||
TEST_ASSERT_EQUAL_STRING(" world", rest);
|
||
}
|
||
|
||
void test_any_one_arg_leading_spaces(void)
|
||
{
|
||
char first[64];
|
||
any_one_arg(" hello world", first);
|
||
TEST_ASSERT_EQUAL_STRING("hello", first);
|
||
}
|
||
|
||
void test_any_one_arg_single_word(void)
|
||
{
|
||
char first[64];
|
||
char *rest = any_one_arg("hello", first);
|
||
TEST_ASSERT_EQUAL_STRING("hello", first);
|
||
TEST_ASSERT_EQUAL_STRING("", rest);
|
||
}
|
||
|
||
void test_any_one_arg_empty_string(void)
|
||
{
|
||
char first[64];
|
||
any_one_arg("", first);
|
||
TEST_ASSERT_EQUAL_STRING("", first);
|
||
}
|
||
|
||
void test_any_one_arg_lowercases(void)
|
||
{
|
||
char first[64];
|
||
any_one_arg("HELLO", first);
|
||
TEST_ASSERT_EQUAL_STRING("hello", first);
|
||
}
|
||
|
||
/* =========================================================
|
||
* one_word
|
||
* ========================================================= */
|
||
|
||
void test_one_word_unquoted_like_any_one_arg(void)
|
||
{
|
||
char first[64];
|
||
char *rest = one_word("hello world", first);
|
||
TEST_ASSERT_EQUAL_STRING("hello", first);
|
||
TEST_ASSERT_EQUAL_STRING(" world", rest);
|
||
}
|
||
|
||
void test_one_word_quoted_string(void)
|
||
{
|
||
char first[64];
|
||
char *rest = one_word("\"hello world\" rest", first);
|
||
TEST_ASSERT_EQUAL_STRING("hello world", first);
|
||
/* rest points just past the closing quote */
|
||
TEST_ASSERT_EQUAL_STRING(" rest", rest);
|
||
}
|
||
|
||
void test_one_word_empty_quoted(void)
|
||
{
|
||
char first[64];
|
||
one_word("\"\" rest", first);
|
||
TEST_ASSERT_EQUAL_STRING("", first);
|
||
}
|
||
|
||
void test_one_word_leading_spaces_skipped(void)
|
||
{
|
||
char first[64];
|
||
one_word(" hello", first);
|
||
TEST_ASSERT_EQUAL_STRING("hello", first);
|
||
}
|
||
|
||
/* =========================================================
|
||
* Helpers for perform_complex_alias tests
|
||
* ========================================================= */
|
||
|
||
/* Release every txt_block in a queue and reset head/tail to NULL. */
|
||
static void free_txt_q(struct txt_q *q)
|
||
{
|
||
struct txt_block *b = q->head, *n;
|
||
while (b) { n = b->next; free(b->text); free(b); b = n; }
|
||
q->head = q->tail = NULL;
|
||
}
|
||
|
||
/* Build a heap-allocated alias_data with type ALIAS_COMPLEX. */
|
||
static struct alias_data *make_test_alias(const char *name, const char *repl)
|
||
{
|
||
struct alias_data *a;
|
||
CREATE(a, struct alias_data, 1);
|
||
a->alias = strdup(name);
|
||
a->replacement = strdup(repl);
|
||
a->type = ALIAS_COMPLEX;
|
||
a->next = NULL;
|
||
return a;
|
||
}
|
||
|
||
/* Free a single alias_data allocated by make_test_alias(). */
|
||
static void destroy_test_alias(struct alias_data *a)
|
||
{
|
||
free(a->alias);
|
||
free(a->replacement);
|
||
free(a);
|
||
}
|
||
|
||
/* Zero-initialise all three objects and wire them together so that
|
||
* IS_NPC() returns false and GET_ALIASES() returns a. */
|
||
static void alias_env_init(struct descriptor_data *d,
|
||
struct char_data *ch,
|
||
struct player_special_data *psd,
|
||
struct alias_data *a)
|
||
{
|
||
memset(d, 0, sizeof(*d));
|
||
memset(ch, 0, sizeof(*ch));
|
||
memset(psd, 0, sizeof(*psd));
|
||
ch->player_specials = psd;
|
||
psd->aliases = a;
|
||
d->character = ch;
|
||
}
|
||
|
||
/* =========================================================
|
||
* perform_complex_alias — tested via the public perform_alias()
|
||
* ========================================================= */
|
||
|
||
/* Literal replacement with no variable tokens. */
|
||
void test_pca_literal(void)
|
||
{
|
||
struct alias_data *a = make_test_alias("lit", "hello world");
|
||
struct descriptor_data d;
|
||
struct char_data ch;
|
||
struct player_special_data psd;
|
||
char orig[] = "lit";
|
||
|
||
alias_env_init(&d, &ch, &psd, a);
|
||
perform_alias(&d, orig, sizeof(orig));
|
||
|
||
TEST_ASSERT_NOT_NULL(d.input.head);
|
||
TEST_ASSERT_EQUAL_STRING("hello world", d.input.head->text);
|
||
TEST_ASSERT_NULL(d.input.head->next);
|
||
|
||
free_txt_q(&d.input);
|
||
destroy_test_alias(a);
|
||
}
|
||
|
||
/* $* glob expands to the full argument string after the alias trigger. */
|
||
void test_pca_glob_expansion(void)
|
||
{
|
||
struct alias_data *a = make_test_alias("say", "say $*");
|
||
struct descriptor_data d;
|
||
struct char_data ch;
|
||
struct player_special_data psd;
|
||
char orig[] = "say hello there";
|
||
|
||
alias_env_init(&d, &ch, &psd, a);
|
||
perform_alias(&d, orig, sizeof(orig));
|
||
|
||
TEST_ASSERT_NOT_NULL(d.input.head);
|
||
TEST_ASSERT_EQUAL_STRING("say hello there", d.input.head->text);
|
||
|
||
free_txt_q(&d.input);
|
||
destroy_test_alias(a);
|
||
}
|
||
|
||
/* $1 expands to the first whitespace-delimited token of the arguments. */
|
||
void test_pca_token1_expansion(void)
|
||
{
|
||
struct alias_data *a = make_test_alias("s1", "say $1");
|
||
struct descriptor_data d;
|
||
struct char_data ch;
|
||
struct player_special_data psd;
|
||
char orig[] = "s1 hello world";
|
||
|
||
alias_env_init(&d, &ch, &psd, a);
|
||
perform_alias(&d, orig, sizeof(orig));
|
||
|
||
TEST_ASSERT_NOT_NULL(d.input.head);
|
||
TEST_ASSERT_EQUAL_STRING("say hello", d.input.head->text);
|
||
|
||
free_txt_q(&d.input);
|
||
destroy_test_alias(a);
|
||
}
|
||
|
||
/* Semicolon separator (;) produces two independent queue entries. */
|
||
void test_pca_separator(void)
|
||
{
|
||
struct alias_data *a = make_test_alias("seq", "go north;go east");
|
||
struct descriptor_data d;
|
||
struct char_data ch;
|
||
struct player_special_data psd;
|
||
char orig[] = "seq";
|
||
|
||
alias_env_init(&d, &ch, &psd, a);
|
||
perform_alias(&d, orig, sizeof(orig));
|
||
|
||
TEST_ASSERT_NOT_NULL(d.input.head);
|
||
TEST_ASSERT_EQUAL_STRING("go north", d.input.head->text);
|
||
TEST_ASSERT_NOT_NULL(d.input.head->next);
|
||
TEST_ASSERT_EQUAL_STRING("go east", d.input.head->next->text);
|
||
|
||
free_txt_q(&d.input);
|
||
destroy_test_alias(a);
|
||
}
|
||
|
||
/* $$ in the replacement is preserved as $$ in the output (act-safety doubling). */
|
||
void test_pca_dollar_dollar(void)
|
||
{
|
||
struct alias_data *a = make_test_alias("dol", "cost $$5");
|
||
struct descriptor_data d;
|
||
struct char_data ch;
|
||
struct player_special_data psd;
|
||
char orig[] = "dol";
|
||
|
||
alias_env_init(&d, &ch, &psd, a);
|
||
perform_alias(&d, orig, sizeof(orig));
|
||
|
||
TEST_ASSERT_NOT_NULL(d.input.head);
|
||
TEST_ASSERT_EQUAL_STRING("cost $$5", d.input.head->text);
|
||
|
||
free_txt_q(&d.input);
|
||
destroy_test_alias(a);
|
||
}
|
||
|
||
/* Overflow via $*: 255 "$*" tokens × 50-char argument exceeds
|
||
* MAX_RAW_INPUT_LENGTH. The queue must be empty after the call. */
|
||
void test_pca_overflow_glob(void)
|
||
{
|
||
/* 255 × "$*" = 510 bytes + NUL */
|
||
char repl[511];
|
||
char orig[64];
|
||
struct alias_data *a;
|
||
struct descriptor_data d;
|
||
struct char_data ch;
|
||
struct player_special_data psd;
|
||
int i;
|
||
|
||
for (i = 0; i < 255; i++) { repl[i * 2] = '$'; repl[i * 2 + 1] = '*'; }
|
||
repl[510] = '\0';
|
||
|
||
/* "al " + 50 'x' chars */
|
||
memcpy(orig, "al ", 3);
|
||
memset(orig + 3, 'x', 50);
|
||
orig[53] = '\0';
|
||
|
||
a = make_test_alias("al", repl);
|
||
alias_env_init(&d, &ch, &psd, a);
|
||
perform_alias(&d, orig, sizeof(orig));
|
||
|
||
/* No partial results must leak into the queue on overflow. */
|
||
TEST_ASSERT_NULL(d.input.head);
|
||
|
||
free_txt_q(&d.input);
|
||
destroy_test_alias(a);
|
||
}
|
||
|
||
/* Overflow via $1: 255 "$1" tokens × 50-char first token exceeds
|
||
* MAX_RAW_INPUT_LENGTH. The queue must be empty after the call. */
|
||
void test_pca_overflow_token(void)
|
||
{
|
||
/* 255 × "$1" = 510 bytes + NUL */
|
||
char repl[511];
|
||
char orig[64];
|
||
struct alias_data *a;
|
||
struct descriptor_data d;
|
||
struct char_data ch;
|
||
struct player_special_data psd;
|
||
int i;
|
||
|
||
for (i = 0; i < 255; i++) { repl[i * 2] = '$'; repl[i * 2 + 1] = '1'; }
|
||
repl[510] = '\0';
|
||
|
||
/* "al " + 50 'y' chars */
|
||
memcpy(orig, "al ", 3);
|
||
memset(orig + 3, 'y', 50);
|
||
orig[53] = '\0';
|
||
|
||
a = make_test_alias("al", repl);
|
||
alias_env_init(&d, &ch, &psd, a);
|
||
perform_alias(&d, orig, sizeof(orig));
|
||
|
||
/* No partial results must leak into the queue on overflow. */
|
||
TEST_ASSERT_NULL(d.input.head);
|
||
|
||
free_txt_q(&d.input);
|
||
destroy_test_alias(a);
|
||
}
|
||
|
||
/* =========================================================
|
||
* main
|
||
* ========================================================= */
|
||
|
||
int main(void)
|
||
{
|
||
UNITY_BEGIN();
|
||
|
||
/* is_number */
|
||
RUN_TEST(test_is_number_digits_only);
|
||
RUN_TEST(test_is_number_zero);
|
||
RUN_TEST(test_is_number_negative);
|
||
RUN_TEST(test_is_number_empty_string);
|
||
RUN_TEST(test_is_number_contains_letter);
|
||
RUN_TEST(test_is_number_minus_only);
|
||
RUN_TEST(test_is_number_float);
|
||
|
||
/* is_abbrev */
|
||
RUN_TEST(test_is_abbrev_exact_match);
|
||
RUN_TEST(test_is_abbrev_valid_prefix);
|
||
RUN_TEST(test_is_abbrev_non_prefix);
|
||
RUN_TEST(test_is_abbrev_empty_arg1);
|
||
RUN_TEST(test_is_abbrev_arg1_longer_than_arg2);
|
||
RUN_TEST(test_is_abbrev_case_insensitive);
|
||
|
||
/* delete_doubledollar */
|
||
RUN_TEST(test_delete_doubledollar_no_dollars);
|
||
RUN_TEST(test_delete_doubledollar_double_at_start);
|
||
RUN_TEST(test_delete_doubledollar_double_in_middle);
|
||
RUN_TEST(test_delete_doubledollar_four_dollars);
|
||
RUN_TEST(test_delete_doubledollar_single_dollar_unchanged);
|
||
|
||
/* any_one_arg */
|
||
RUN_TEST(test_any_one_arg_basic);
|
||
RUN_TEST(test_any_one_arg_leading_spaces);
|
||
RUN_TEST(test_any_one_arg_single_word);
|
||
RUN_TEST(test_any_one_arg_empty_string);
|
||
RUN_TEST(test_any_one_arg_lowercases);
|
||
|
||
/* one_word */
|
||
RUN_TEST(test_one_word_unquoted_like_any_one_arg);
|
||
RUN_TEST(test_one_word_quoted_string);
|
||
RUN_TEST(test_one_word_empty_quoted);
|
||
RUN_TEST(test_one_word_leading_spaces_skipped);
|
||
|
||
/* perform_complex_alias */
|
||
RUN_TEST(test_pca_literal);
|
||
RUN_TEST(test_pca_glob_expansion);
|
||
RUN_TEST(test_pca_token1_expansion);
|
||
RUN_TEST(test_pca_separator);
|
||
RUN_TEST(test_pca_dollar_dollar);
|
||
RUN_TEST(test_pca_overflow_glob);
|
||
RUN_TEST(test_pca_overflow_token);
|
||
|
||
return UNITY_END();
|
||
}
|