Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/merve/parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ enum lexer_error {

// Resource limit errors
TEMPLATE_NEST_OVERFLOW, ///< Template literal nesting too deep
OPEN_TOKEN_OVERFLOW, ///< Parenthesis or brace nesting too deep
};

/**
Expand Down
1 change: 1 addition & 0 deletions include/merve_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ typedef struct {
#define MERVE_ERROR_UNEXPECTED_ESM_IMPORT 10
#define MERVE_ERROR_UNEXPECTED_ESM_EXPORT 11
#define MERVE_ERROR_TEMPLATE_NEST_OVERFLOW 12
#define MERVE_ERROR_OPEN_TOKEN_OVERFLOW 13

#ifdef __cplusplus
extern "C" {
Expand Down
28 changes: 23 additions & 5 deletions rust/deps/merve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,20 @@ class CJSLexer {
}

// Parsing utilities

// STACK_DEPTH slots. A deeper '(' or '{' is a parse error.
bool pushOpenToken(char type, const char* tokenPos, bool isClass = false) {
if (openTokenDepth >= STACK_DEPTH) {
syntaxError(lexer_error::OPEN_TOKEN_OVERFLOW, tokenPos);
return false;
}
if (type == '{')
openClassPosStack[openTokenDepth] = isClass;
openTokenTypeStack_[openTokenDepth] = type;
openTokenPosStack_[openTokenDepth++] = tokenPos;
return true;
}

void syntaxError(lexer_error code, const char* at = nullptr) {
if (!last_error) {
last_error = code;
Expand Down Expand Up @@ -633,6 +647,10 @@ class CJSLexer {
syntaxError(lexer_error::TEMPLATE_NEST_OVERFLOW);
return;
}
if (openTokenDepth >= STACK_DEPTH) {
syntaxError(lexer_error::OPEN_TOKEN_OVERFLOW);
return;
}
templateStack_[templateStackDepth++] = templateDepth;
templateDepth = ++openTokenDepth;
return;
Expand Down Expand Up @@ -1682,8 +1700,8 @@ class CJSLexer {
tryParseObjectDefineOrKeys(openTokenDepth == 0);
break;
case '(':
openTokenTypeStack_[openTokenDepth] = '(';
openTokenPosStack_[openTokenDepth++] = lastTokenPos;
if (!pushOpenToken('(', lastTokenPos))
return false;
break;
case ')':
if (openTokenDepth == 0) {
Expand All @@ -1693,10 +1711,9 @@ class CJSLexer {
openTokenDepth--;
break;
case '{':
openClassPosStack[openTokenDepth] = nextBraceIsClass;
if (!pushOpenToken('{', lastTokenPos, nextBraceIsClass))
return false;
nextBraceIsClass = false;
openTokenTypeStack_[openTokenDepth] = '{';
openTokenPosStack_[openTokenDepth++] = lastTokenPos;
break;
case '}':
if (openTokenDepth == 0) {
Expand Down Expand Up @@ -1880,6 +1897,7 @@ typedef struct {
#define MERVE_ERROR_UNEXPECTED_ESM_IMPORT 10
#define MERVE_ERROR_UNEXPECTED_ESM_EXPORT 11
#define MERVE_ERROR_TEMPLATE_NEST_OVERFLOW 12
#define MERVE_ERROR_OPEN_TOKEN_OVERFLOW 13

#ifdef __cplusplus
extern "C" {
Expand Down
1 change: 1 addition & 0 deletions rust/deps/merve.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ enum lexer_error {

// Resource limit errors
TEMPLATE_NEST_OVERFLOW, ///< Template literal nesting too deep
OPEN_TOKEN_OVERFLOW, ///< Parenthesis or brace nesting too deep
};

/**
Expand Down
1 change: 1 addition & 0 deletions rust/deps/merve_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ typedef struct {
#define MERVE_ERROR_UNEXPECTED_ESM_IMPORT 10
#define MERVE_ERROR_UNEXPECTED_ESM_EXPORT 11
#define MERVE_ERROR_TEMPLATE_NEST_OVERFLOW 12
#define MERVE_ERROR_OPEN_TOKEN_OVERFLOW 13

#ifdef __cplusplus
extern "C" {
Expand Down
12 changes: 11 additions & 1 deletion rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub enum LexerError {
UnexpectedEsmImport,
UnexpectedEsmExport,
TemplateNestOverflow,
OpenTokenOverflow,
/// An error code not recognized by these bindings.
Unknown(i32),
}
Expand All @@ -74,6 +75,7 @@ impl LexerError {
10 => Self::UnexpectedEsmImport,
11 => Self::UnexpectedEsmExport,
12 => Self::TemplateNestOverflow,
13 => Self::OpenTokenOverflow,
other => Self::Unknown(other),
}
}
Expand All @@ -95,6 +97,7 @@ impl LexerError {
Self::UnexpectedEsmImport => "unexpected ESM import",
Self::UnexpectedEsmExport => "unexpected ESM export",
Self::TemplateNestOverflow => "template nesting overflow",
Self::OpenTokenOverflow => "open token nesting overflow",
Self::Unknown(_) => "unknown error",
}
}
Expand Down Expand Up @@ -530,6 +533,13 @@ mod tests {
assert_eq!(err.kind, LexerError::UnexpectedEsmExport);
}

#[test]
fn open_token_overflow() {
let source = "(".repeat(2048 * 4);
let err = parse_commonjs(&source).unwrap_err();
assert_eq!(err.kind, LexerError::OpenTokenOverflow);
}

#[test]
fn empty_input() {
let result = parse_commonjs("");
Expand Down Expand Up @@ -694,7 +704,7 @@ mod tests {

#[test]
fn error_from_code_roundtrip() {
for code in 0..=12 {
for code in 0..=13 {
let err = LexerError::from_code(code);
assert_ne!(err, LexerError::Unknown(code));
}
Expand Down
27 changes: 22 additions & 5 deletions src/parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,20 @@ class CJSLexer {
}

// Parsing utilities

// STACK_DEPTH slots. A deeper '(' or '{' is a parse error.
bool pushOpenToken(char type, const char* tokenPos, bool isClass = false) {
if (openTokenDepth >= STACK_DEPTH) {
syntaxError(lexer_error::OPEN_TOKEN_OVERFLOW, tokenPos);
return false;
}
if (type == '{')
openClassPosStack[openTokenDepth] = isClass;
openTokenTypeStack_[openTokenDepth] = type;
openTokenPosStack_[openTokenDepth++] = tokenPos;
return true;
}

void syntaxError(lexer_error code, const char* at = nullptr) {
if (!last_error) {
last_error = code;
Expand Down Expand Up @@ -631,6 +645,10 @@ class CJSLexer {
syntaxError(lexer_error::TEMPLATE_NEST_OVERFLOW);
return;
}
if (openTokenDepth >= STACK_DEPTH) {
syntaxError(lexer_error::OPEN_TOKEN_OVERFLOW);
return;
}
templateStack_[templateStackDepth++] = templateDepth;
templateDepth = ++openTokenDepth;
return;
Expand Down Expand Up @@ -1680,8 +1698,8 @@ class CJSLexer {
tryParseObjectDefineOrKeys(openTokenDepth == 0);
break;
case '(':
openTokenTypeStack_[openTokenDepth] = '(';
openTokenPosStack_[openTokenDepth++] = lastTokenPos;
if (!pushOpenToken('(', lastTokenPos))
return false;
break;
case ')':
if (openTokenDepth == 0) {
Expand All @@ -1691,10 +1709,9 @@ class CJSLexer {
openTokenDepth--;
break;
case '{':
openClassPosStack[openTokenDepth] = nextBraceIsClass;
if (!pushOpenToken('{', lastTokenPos, nextBraceIsClass))
return false;
nextBraceIsClass = false;
openTokenTypeStack_[openTokenDepth] = '{';
openTokenPosStack_[openTokenDepth++] = lastTokenPos;
break;
case '}':
if (openTokenDepth == 0) {
Expand Down
1 change: 1 addition & 0 deletions tests/c_api_compile_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ static void check_types(void) {
MERVE_ERROR_UNEXPECTED_ESM_IMPORT,
MERVE_ERROR_UNEXPECTED_ESM_EXPORT,
MERVE_ERROR_TEMPLATE_NEST_OVERFLOW,
MERVE_ERROR_OPEN_TOKEN_OVERFLOW,
};
(void)errors;
}
Expand Down
10 changes: 10 additions & 0 deletions tests/c_api_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "gtest/gtest.h"
#include <cstring>
#include <string>

// Helper: compare merve_string to a C string literal.
static bool merve_string_eq(merve_string s, const char* expected) {
Expand Down Expand Up @@ -482,3 +483,12 @@ TEST(c_api_tests, spread_reexports) {
ASSERT_TRUE(merve_string_eq(merve_get_reexport_name(result, 1), "dep2"));
merve_free(result);
}

TEST(c_api_tests, open_token_overflow) {
const std::string source(2048 * 4, '(');
merve_analysis result = merve_parse_commonjs(source.data(), source.size());
ASSERT_NE(result, nullptr);
ASSERT_FALSE(merve_is_valid(result));
ASSERT_EQ(merve_get_last_error(), MERVE_ERROR_OPEN_TOKEN_OVERFLOW);
merve_free(result);
}
28 changes: 28 additions & 0 deletions tests/real_world_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1306,6 +1306,34 @@ TEST(real_world_tests, require_at_start_of_input) {
ASSERT_TRUE(result2.has_value());
}

// Matches lexer::STACK_DEPTH in src/parser.cpp.
constexpr size_t kOpenTokenStackDepth = 2048;

TEST(real_world_tests, open_token_stack_depth_still_parses) {
std::string source(kOpenTokenStackDepth, '(');
source.append(kOpenTokenStackDepth, ')');
source += "exports.depth = 1;";
auto result = lexer::parse_commonjs(source);
ASSERT_TRUE(result.has_value());
ASSERT_EQ(result->exports.size(), 1);
ASSERT_EQ(lexer::get_string_view(result->exports[0]), "depth");
}

TEST(real_world_tests, open_token_overflow) {
std::string source(kOpenTokenStackDepth * 4, '(');
auto result = lexer::parse_commonjs(source);
ASSERT_FALSE(result.has_value());
ASSERT_EQ(lexer::get_last_error(), lexer::lexer_error::OPEN_TOKEN_OVERFLOW);
}

TEST(real_world_tests, open_token_overflow_inside_template) {
std::string source(kOpenTokenStackDepth, '(');
source += "`${";
auto result = lexer::parse_commonjs(source);
ASSERT_FALSE(result.has_value());
ASSERT_EQ(lexer::get_last_error(), lexer::lexer_error::OPEN_TOKEN_OVERFLOW);
}

TEST(real_world_tests, line_numbers_after_block_comment) {
auto result = lexer::parse_commonjs(
"/*\n"
Expand Down