From d2bb7898fe3a5e259d83e4c90e71d7a15fc0a511 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Sun, 28 Jun 2026 17:31:30 +0900 Subject: [PATCH] fix string literal escapes --- tests/test_lexer.c | 24 +++++++++++++++++++++ tests/test_parser.c | 48 +++++++++++++++++++++++++++++++++++++++++ wirelog/parser/lexer.c | 23 +++++++++++++++++--- wirelog/parser/parser.c | 12 +---------- 4 files changed, 93 insertions(+), 14 deletions(-) diff --git a/tests/test_lexer.c b/tests/test_lexer.c index fed9b301..6c335cf0 100644 --- a/tests/test_lexer.c +++ b/tests/test_lexer.c @@ -232,6 +232,28 @@ test_string_empty(void) PASS(); } +static void +test_string_escaped_quote(void) +{ + TEST("string literal with escaped quote"); + wl_parser_lexer_t lex; + wl_parser_lexer_init(&lex, "\"a\\\"b\""); + ASSERT_TOK_STR(lex, WL_PARSER_LEXER_TOK_STRING, "a\"b"); + ASSERT_TOK(lex, WL_PARSER_LEXER_TOK_EOF); + PASS(); +} + +static void +test_string_escaped_backslash(void) +{ + TEST("string literal with escaped backslash"); + wl_parser_lexer_t lex; + wl_parser_lexer_init(&lex, "\"a\\\\b\""); + ASSERT_TOK_STR(lex, WL_PARSER_LEXER_TOK_STRING, "a\\b"); + ASSERT_TOK(lex, WL_PARSER_LEXER_TOK_EOF); + PASS(); +} + /* ======================================================================== */ /* Lexer: Boolean Literals */ /* ======================================================================== */ @@ -881,6 +903,8 @@ main(void) test_integer_large(); test_string_literal(); test_string_empty(); + test_string_escaped_quote(); + test_string_escaped_backslash(); printf("\n--- Boolean Literals ---\n"); test_true_literal(); diff --git a/tests/test_parser.c b/tests/test_parser.c index ad4656ee..19014a27 100644 --- a/tests/test_parser.c +++ b/tests/test_parser.c @@ -1510,6 +1510,52 @@ test_parse_fact_string_constant(void) PASS(); } +static void +test_parse_fact_string_escaped_quote(void) +{ + TEST("fact string constant with escaped quote"); + PARSE("relation(\"s\", \"r\", \"a\\\"b\")."); + ASSERT_PARSED(); + const wl_parser_ast_node_t *fact = child(program, 0); + if (fact->type != WL_PARSER_AST_NODE_FACT || fact->child_count != 3) { + CLEANUP(); + FAIL("expected relation fact with 3 args"); + return; + } + const wl_parser_ast_node_t *arg2 = child(fact, 2); + if (arg2->type != WL_PARSER_AST_NODE_STRING || !arg2->str_value + || strcmp(arg2->str_value, "a\"b") != 0) { + CLEANUP(); + FAIL("arg 2 should be STRING 'a\"b'"); + return; + } + CLEANUP(); + PASS(); +} + +static void +test_parse_fact_string_escaped_backslash(void) +{ + TEST("fact string constant with escaped backslash"); + PARSE("path(\"a\\\\b\")."); + ASSERT_PARSED(); + const wl_parser_ast_node_t *fact = child(program, 0); + if (fact->type != WL_PARSER_AST_NODE_FACT || fact->child_count != 1) { + CLEANUP(); + FAIL("expected path fact with 1 arg"); + return; + } + const wl_parser_ast_node_t *arg0 = child(fact, 0); + if (arg0->type != WL_PARSER_AST_NODE_STRING || !arg0->str_value + || strcmp(arg0->str_value, "a\\b") != 0) { + CLEANUP(); + FAIL("arg 0 should be STRING 'a\\b'"); + return; + } + CLEANUP(); + PASS(); +} + /* ======================================================================== */ /* Parser: Compound Terms (Issue #530) */ /* ======================================================================== */ @@ -1991,6 +2037,8 @@ main(void) test_parse_facts_mixed_with_rules(); test_parse_fact_rejects_variables(); test_parse_fact_string_constant(); + test_parse_fact_string_escaped_quote(); + test_parse_fact_string_escaped_backslash(); printf("\n--- Compound Terms (Issue #530) ---\n"); test_parser_compound_basic(); diff --git a/wirelog/parser/lexer.c b/wirelog/parser/lexer.c index e9ffe19c..11d2e3eb 100644 --- a/wirelog/parser/lexer.c +++ b/wirelog/parser/lexer.c @@ -138,6 +138,12 @@ scan_string(wl_parser_lexer_t *lexer) { /* Opening quote already consumed */ while (!is_at_end(lexer) && peek(lexer) != '"') { + if (peek(lexer) == '\\' + && (peek_next(lexer) == '"' || peek_next(lexer) == '\\')) { + advance(lexer); + advance(lexer); + continue; + } advance(lexer); } @@ -649,13 +655,24 @@ char * wl_parser_lexer_token_to_string(const wl_parser_lexer_token_t *token) { if (token->type == WL_PARSER_LEXER_TOK_STRING) { - /* Strip surrounding quotes */ + /* Strip surrounding quotes and decode supported string escapes. */ if (token->length >= 2) { uint32_t inner_len = token->length - 2; char *str = (char *)malloc(inner_len + 1); if (str) { - memcpy(str, token->start + 1, inner_len); - str[inner_len] = '\0'; + const char *p = token->start + 1; + const char *end = p + inner_len; + char *out = str; + while (p < end) { + if (*p == '\\' && p + 1 < end + && (p[1] == '"' || p[1] == '\\')) { + *out++ = p[1]; + p += 2; + continue; + } + *out++ = *p++; + } + *out = '\0'; } return str; } diff --git a/wirelog/parser/parser.c b/wirelog/parser/parser.c index 28b249cf..f03bab97 100644 --- a/wirelog/parser/parser.c +++ b/wirelog/parser/parser.c @@ -143,17 +143,7 @@ token_to_name(const wl_parser_lexer_token_t *token) static char * token_to_str_value(const wl_parser_lexer_token_t *token) { - /* Strip quotes from string token */ - if (token->length >= 2) { - uint32_t inner = token->length - 2; - char *s = (char *)malloc(inner + 1); - if (s) { - memcpy(s, token->start + 1, inner); - s[inner] = '\0'; - } - return s; - } - return (char *)calloc(1, 1); + return wl_parser_lexer_token_to_string(token); } static bool