ggml-org/llama.cpp · #31153
common, grammar: add recursive depth limit, make it a requirement
common/common.cpp44 + / 38 −
@@ -742,50 +742,56 @@ static inline bool glob_class_match(const char c, const char * pattern, const ch } // simple glob: * matches non-/ chars, ** matches anything including /, [] matches character class-static inline bool glob_match(const char * pattern, const char * str) {- if (*pattern == '\0') {- return *str == '\0';- }- if (pattern[0] == '*' && pattern[1] == '*') {- const char * p = pattern + 2;- if (glob_match(p, str)) return true;- if (*str != '\0') return glob_match(pattern, str + 1);+// only wildcards recurse, too many wildcards: no match+static bool glob_match(const char * pattern, const char * str, int depth = 0) {+ if (depth >= COMMON_RECURSIVE_LIMIT) { return false; }- if (*pattern == '*') {- const char * p = pattern + 1;- for (; *str != '\0' && *str != '/'; str++) {- if (glob_match(p, str)) return true;- }- return glob_match(p, str);- }- if (*pattern == '?' && *str != '\0' && *str != '/') {- return glob_match(pattern + 1, str + 1);- }- if (*pattern == '[') {- const char * class_end = pattern + 1;- // If first character after '[' is ']' or '-', treat it as literal- if (*class_end == ']' || *class_end == '-') {- class_end++;- }- while (*class_end != '\0' && *class_end != ']') {- class_end++;+ while (*pattern != '\0') {+ if (pattern[0] == '*' && pattern[1] == '*') {+ const char * p = pattern + 2;+ for (;; str++) {+ if (glob_match(p, str, depth + 1)) return true;+ if (*str == '\0') return false;+ } }- if (*class_end == ']') {- if (*str == '\0') return false;- bool matched = glob_class_match(*str, pattern + 1, class_end);- return matched && glob_match(class_end + 1, str + 1);- } else {- if (*str == '[') {- return glob_match(pattern + 1, str + 1);+ if (*pattern == '*') {+ const char * p = pattern + 1;+ for (; *str != '\0' && *str != '/'; str++) {+ if (glob_match(p, str, depth + 1)) return true; }- return false;+ return glob_match(p, str, depth + 1);+ }+ if (*pattern == '?' && *str != '\0' && *str != '/') {+ pattern++;+ str++;+ continue;+ }+ if (*pattern == '[') {+ const char * class_end = pattern + 1;+ // If first character after '[' is ']' or '-', treat it as literal+ if (*class_end == ']' || *class_end == '-') {+ class_end++;+ }+ while (*class_end != '\0' && *class_end != ']') {+ class_end++;+ }+ if (*class_end == ']') {+ if (*str == '\0' || !glob_class_match(*str, pattern + 1, class_end)) return false;+ pattern = class_end + 1;+ str++;+ continue;+ }+ if (*str != '[') return false;+ pattern++;+ str++;+ continue; }+ if (*pattern != *str) return false;+ pattern++;+ str++; }- if (*pattern == *str) {- return glob_match(pattern + 1, str + 1);- }- return false;+ return *str == '\0'; } bool glob_match(const std::string & pattern, const std::string & str) {common/common.h2 + / 0 −
@@ -31,6 +31,8 @@ #define DIRECTORY_SEPARATOR '/' #endif // _WIN32 +#define COMMON_RECURSIVE_LIMIT 128+ #define COM_DBG(fmt, ...) LOG_DBG("cmn %12.*s: " fmt, 12, __func__, __VA_ARGS__) #define COM_TRC(fmt, ...) LOG_TRC("cmn %12.*s: " fmt, 12, __func__, __VA_ARGS__) #define COM_INF(fmt, ...) LOG_INF("cmn %12.*s: " fmt, 12, __func__, __VA_ARGS__)common/jinja/parser.cpp21 + / 3 −
@@ -21,6 +21,17 @@ static bool is_type(const statement_ptr & ptr) { class parser { const std::vector<token> & tokens; size_t current = 0;+ int depth = 0;++ struct depth_guard {+ parser & p;+ depth_guard(parser & p) : p(p) {+ if (++p.depth > JINJA_RECURSIVE_LIMIT) {+ throw parser_exception("Parser Error: template is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep", p.source, p.peek().pos);+ }+ }+ ~depth_guard() { p.depth--; }+ }; std::string source; // for error reporting @@ -94,6 +105,7 @@ class parser { } statement_ptr parse_any() {+ depth_guard guard(*this); size_t start_pos = current; switch (peek().t) { case token::comment:@@ -254,6 +266,7 @@ class parser { size_t pos0 = current; ++current; // consume {% ++current; // consume 'elif'+ depth_guard guard(*this); alternate.push_back(parse_if_statement(pos0)); // nested If } else if (is_statement({"else"})) { ++current; // consume {%@@ -330,6 +343,7 @@ class parser { } statement_ptr parse_if_expression() {+ depth_guard guard(*this); auto a = parse_logical_or_expression(); if (is_identifier("if")) { // Ternary expression@@ -373,6 +387,7 @@ class parser { statement_ptr parse_logical_negation_expression() { // Try parse unary operators if (is_identifier("not")) {+ depth_guard guard(*this); size_t start_pos = current; auto op = next(); return mk_stmt<unary_expression>(start_pos, op, parse_logical_negation_expression());@@ -458,6 +473,7 @@ class parser { statement_ptr parse_unary_expression() { if (is(token::unary_operator)) {+ depth_guard guard(*this); size_t start_pos = current; auto op = next(); return mk_stmt<unary_expression>(start_pos, op, parse_unary_expression());@@ -477,9 +493,11 @@ class parser { size_t start_pos = current; auto expr = mk_stmt<call_expression>(start_pos, std::move(callee), parse_args()); auto member = parse_member_expression(std::move(expr)); // foo.x().y- return is(token::open_paren)- ? parse_call_expression(std::move(member)) // foo.x()()- : std::move(member);+ if (!is(token::open_paren)) {+ return member;+ }+ depth_guard guard(*this);+ return parse_call_expression(std::move(member)); // foo.x()() } statements parse_args() {common/jinja/runtime.cpp23 + / 7 −
@@ -3,6 +3,7 @@ #include "value.h" #include "utils.h" +#include <exception> #include <string> #include <vector> #include <memory>@@ -52,32 +53,39 @@ static void ensure_key_type_allowed(const value & val) { // execute with error handling value statement::execute(context & ctx) const {+ std::exception_ptr pending;+ std::string message; try { return execute_impl(ctx); } catch (const continue_statement::signal & /* ex */) {- throw;+ pending = std::current_exception(); } catch (const break_statement::signal & /* ex */) {- throw;+ pending = std::current_exception(); } catch (const rethrown_exception & /* ex */) {- throw;+ pending = std::current_exception(); } catch (const not_implemented_exception & /* ex */) {- throw;+ pending = std::current_exception(); } catch (const std::exception & e) { const std::string & source = *ctx.src; if (source.empty()) { std::ostringstream oss; oss << "\nError executing " << type() << " at position " << pos << ": " << e.what();- throw rethrown_exception(oss.str());+ message = oss.str(); } else { std::ostringstream oss; oss << "\n------------\n"; oss << "While executing " << type() << " at " << get_line_col(source, pos) << " in source:\n"; oss << peak_source(source, pos) << "\n"; oss << "Error: " << e.what();- // throw as another exception to avoid repeated formatting- throw rethrown_exception(oss.str());+ message = oss.str(); } }+ // throw outside the catch: MSVC keeps the unwound frames on the stack while a catch block runs+ if (pending) {+ std::rethrow_exception(pending);+ }+ // throw as another exception to avoid repeated formatting+ throw rethrown_exception(message); } value identifier::execute_impl(context & ctx) const {@@ -747,7 +755,11 @@ value macro_statement::execute_impl(context & ctx) const { std::string name = cast_stmt<identifier>(this->name)->val; const func_handler func = [this, name](const func_args & args) -> value {+ if (args.ctx.macro_depth >= JINJA_RECURSIVE_LIMIT) {+ throw std::runtime_error("macro calls are nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep");+ } context macro_ctx(args.ctx); // new scope for macro execution+ macro_ctx.macro_depth = args.ctx.macro_depth + 1; bind_parameters(name, this->args, args, macro_ctx); @@ -778,7 +790,11 @@ value call_statement::execute_impl(context & ctx) const { context caller_ctx(ctx); // new scope for caller execution const func_handler func = [this, caller_ctx = std::move(caller_ctx)](const func_args & args) -> value {+ if (args.ctx.macro_depth >= JINJA_RECURSIVE_LIMIT) {+ throw std::runtime_error("macro calls are nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep");+ } context block_ctx(caller_ctx); // new scope for block execution+ block_ctx.macro_depth = args.ctx.macro_depth + 1; bind_parameters("caller", this->caller_args, args, block_ctx); common/jinja/runtime.h7 + / 2 −
@@ -58,6 +58,8 @@ struct context { bool is_get_stats = false; // whether to collect stats + int macro_depth = 0;+ visitor_fn visitor; // src is optional, used for error reporting@@ -78,6 +80,7 @@ struct context { src(parent.src), current_time(parent.current_time), is_get_stats(parent.is_get_stats),+ macro_depth(parent.macro_depth), // inherit variables (for example, when entering a new scope) env(mk_val<value_object>(*parent.env)) {} @@ -710,18 +713,20 @@ struct rethrown_exception : public std::exception { ////////////////////// -static void gather_string_parts_recursive(const value & val, value_string & parts) {+static void gather_string_parts_recursive(const value & val, value_string & parts, int depth = 0) { // TODO: probably allow print value_none as "None" string? currently this breaks some templates if (is_val<value_string>(val)) { const auto & str_val = cast_val<value_string>(val)->val_str; parts->val_str.append(str_val); } else if (is_val<value_int>(val) || is_val<value_float>(val) || is_val<value_bool>(val)) { std::string str_val = val->as_string().str(); parts->val_str.append(str_val);+ } else if (is_val<value_array>(val) && depth >= JINJA_RECURSIVE_LIMIT) {+ throw std::runtime_error("value is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep"); } else if (is_val<value_array>(val)) { auto items = cast_val<value_array>(val)->as_array(); for (const auto & item : items) {- gather_string_parts_recursive(item, parts);+ gather_string_parts_recursive(item, parts, depth + 1); } } }common/jinja/utils.h2 + / 0 −
@@ -6,6 +6,8 @@ #include <cstdint> #include <cstring> +#define JINJA_RECURSIVE_LIMIT 128+ namespace jinja { static void string_replace_all(std::string & s, const std::string & search, const std::string & replace) {common/jinja/value.cpp11 + / 8 −
@@ -1441,8 +1441,6 @@ const func_builtins & value_undefined_t::get_builtins() const { // the runtime walks values with recursion, a deeper input overflows the stack-static constexpr int FROM_JSON_MAX_DEPTH = 128;- static value from_json(const common_json & j, bool mark_input, int depth = 0) { if (j.is_null()) { return mk_val<value_none>();@@ -1458,8 +1456,8 @@ static value from_json(const common_json & j, bool mark_input, int depth = 0) { str->mark_input(); } return str;- } else if ((j.is_array() || j.is_object()) && depth >= FROM_JSON_MAX_DEPTH) {- throw std::invalid_argument("JSON is nested more than " + std::to_string(FROM_JSON_MAX_DEPTH) + " levels deep");+ } else if ((j.is_array() || j.is_object()) && depth >= JINJA_RECURSIVE_LIMIT) {+ throw std::invalid_argument("JSON is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep"); } else if (j.is_array()) { auto arr = mk_val<value_array>(); for (const auto & item : j) {@@ -1664,6 +1662,8 @@ static void value_to_json_internal(std::ostringstream & oss, const value & val, } } oss << "\"";+ } else if ((is_val<value_array>(val) || is_val<value_object>(val)) && curr_lvl >= JINJA_RECURSIVE_LIMIT) {+ throw std::runtime_error("value is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep"); } else if (is_val<value_array>(val)) { const auto & arr = val->as_array(); oss << "[";@@ -1729,17 +1729,20 @@ std::string value_to_string_repr(const value & val) { } // stats utility-void value_t::stats_t::mark_used(value & val, bool deep) {+void value_t::stats_t::mark_used(value & val, bool deep, int depth) { val->stats.used = true;+ if (deep && (is_val<value_array>(val) || is_val<value_object>(val)) && depth >= JINJA_RECURSIVE_LIMIT) {+ throw std::runtime_error("value is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep");+ } if (deep) { if (is_val<value_array>(val)) { for (auto & item : val->val_arr) {- mark_used(item, deep);+ mark_used(item, deep, depth + 1); } } else if (is_val<value_object>(val)) { for (auto & pair : val->val_obj) {- mark_used(pair.first, deep);- mark_used(pair.second, deep);+ mark_used(pair.first, deep, depth + 1);+ mark_used(pair.second, deep, depth + 1); } } }common/jinja/value.h1 + / 1 −
@@ -119,7 +119,7 @@ struct value_t { // ops can be builtin calls or operators: "array_access", "object_access" std::set<std::string> ops; // utility to recursively mark value and its children as used- static void mark_used(value & val, bool deep = false);+ static void mark_used(value & val, bool deep = false, int depth = 0); } stats; value_t() = default;common/json-schema-to-grammar.cpp46 + / 14 −
@@ -272,15 +272,12 @@ static bool is_reserved_name(const std::string & name) { return RESERVED_NAMES.find(name) != RESERVED_NAMES.end(); } -static std::regex INVALID_RULE_CHARS_RE("[^a-zA-Z0-9-]+"); static std::regex GRAMMAR_LITERAL_ESCAPE_RE("[\r\n\"\\\\]"); static std::regex GRAMMAR_RANGE_LITERAL_ESCAPE_RE("[\r\n\"\\]\\-\\\\]"); static std::unordered_map<char, std::string> GRAMMAR_LITERAL_ESCAPES = { {'\r', "\\r"}, {'\n', "\\n"}, {'"', "\\\""}, {'-', "\\-"}, {']', "\\]"}, {'\\', "\\\\"} }; -static const int MAX_PATTERN_DEPTH = 100;- static std::unordered_set<char> NON_LITERAL_SET = {'|', '.', '(', ')', '[', ']', '{', '}', '*', '+', '?', '^', '$'}; static std::unordered_set<char> ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = {'^', '$', '.', '[', ']', '(', ')', '|', '{', '}', '*', '+', '?'}; @@ -312,6 +309,22 @@ static std::string format_literal(const std::string & literal) { std::string gbnf_format_literal(const std::string & literal) { return format_literal(literal); } +// same as std::regex_replace with [^a-zA-Z0-9-]+ -> "-", no std::regex: libstdc++ recurses per char+std::string gbnf_format_rule_name(const std::string & name) {+ std::string out;+ bool in_invalid = false;+ for (char c : name) {+ if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-') {+ out += c;+ in_invalid = false;+ } else if (!in_invalid) {+ out += '-';+ in_invalid = true;+ }+ }+ return out;+}+ static size_t gbnf_escape_length(const std::string & pattern, size_t pos) { if (pos + 1 >= pattern.length() || pattern[pos] != '\\') { return 0;@@ -347,14 +360,25 @@ class common_chat_schema_converter { std::unordered_set<std::string> _refs_being_resolved; std::vector<std::string> _errors; std::vector<std::string> _warnings;+ int _depth = 0;++ struct depth_guard {+ int & depth;+ depth_guard(int & depth) : depth(depth) {+ if (++depth > COMMON_RECURSIVE_LIMIT) {+ throw std::invalid_argument("JSON schema is nested more than " + std::to_string(COMMON_RECURSIVE_LIMIT) + " levels deep");+ }+ }+ ~depth_guard() { depth--; }+ }; template <typename T> static const T & as(const common_chat_schema & node) { return static_cast<const T &>(node); } std::string _add_rule(const std::string & name, const std::string & rule) {- std::string esc_name = regex_replace(name, INVALID_RULE_CHARS_RE, "-");+ std::string esc_name = gbnf_format_rule_name(name); if (_rules.find(esc_name) == _rules.end() || _rules[esc_name] == rule) { _rules[esc_name] = rule; return esc_name;@@ -482,7 +506,7 @@ class common_chat_schema_converter { } } paren_depth++;- if (paren_depth > MAX_PATTERN_DEPTH) {+ if (paren_depth > COMMON_RECURSIVE_LIMIT) { throw unsupported_pattern("pattern nesting too deep"); } seq.emplace_back("(" + to_rule(transform()) + ")", false);@@ -644,7 +668,11 @@ class common_chat_schema_converter { std::string char_rule = _add_primitive("char", PRIMITIVE_RULES.at("char")); std::ostringstream out; out << "[\"] ( ";- std::function<void(size_t)> visit = [&](size_t idx) {+ // one level per char of the longest name+ std::function<void(size_t, int)> visit = [&](size_t idx, int depth) {+ if (depth >= COMMON_RECURSIVE_LIMIT) {+ throw std::invalid_argument("JSON schema property name is longer than " + std::to_string(COMMON_RECURSIVE_LIMIT) + " chars");+ } const auto & node = trie.nodes[idx]; std::string rejects; auto first = true;@@ -659,7 +687,7 @@ class common_chat_schema_converter { out << "[" << c << "]"; if (!trie.nodes[child].children.empty()) { out << " (";- visit(child);+ visit(child, depth + 1); out << ")"; } else { out << " " << char_rule << "+";@@ -669,7 +697,7 @@ class common_chat_schema_converter { out << " | [^\"" << rejects << "] " << char_rule << "*"; } };- visit(0);+ visit(0, 0); out << " )"; if (trie.nodes[0].pattern < 0) {@@ -682,8 +710,7 @@ class common_chat_schema_converter { std::string _resolve_ref(const common_chat_schema_ref & schema) { auto it = schema.ref.find('#'); std::string ref_fragment = it != std::string::npos ? schema.ref.substr(it + 1) : schema.ref;- static const std::regex nonalphanumeric_regex(R"([^a-zA-Z0-9-]+)");- std::string ref_name = "ref" + std::regex_replace(ref_fragment, nonalphanumeric_regex, "-");+ std::string ref_name = "ref" + gbnf_format_rule_name(ref_fragment); if (_rules.find(ref_name) == _rules.end() && _refs_being_resolved.find(schema.ref) == _refs_being_resolved.end()) { if (!schema.target) { _errors.push_back("Unresolved $ref " + schema.ref);@@ -833,10 +860,13 @@ class common_chat_schema_converter { std::unordered_set<std::string> required; std::vector<std::pair<std::string, const common_chat_schema *>> properties; std::map<std::string, size_t> enum_values;- std::function<void(const common_chat_schema &, bool)> add_component = [&](const common_chat_schema & comp, bool is_required) {+ std::function<void(const common_chat_schema &, bool, int)> add_component = [&](const common_chat_schema & comp, bool is_required, int depth) {+ if (depth >= COMMON_RECURSIVE_LIMIT) {+ throw std::invalid_argument("JSON schema is nested more than " + std::to_string(COMMON_RECURSIVE_LIMIT) + " levels deep");+ } if (comp.kind() == common_chat_schema::KIND_REF) { if (const auto * target = as<common_chat_schema_ref>(comp).target) {- add_component(*target, is_required);+ add_component(*target, is_required, depth + 1); } } else if (comp.kind() == common_chat_schema::KIND_OBJECT) { for (const auto & prop : as<common_chat_schema_object>(comp).properties) {@@ -854,10 +884,10 @@ class common_chat_schema_converter { for (const auto & child : schema.children) { if (child->kind() == common_chat_schema::KIND_ANY_OF) { for (const auto & alt : as<common_chat_schema_any_of>(*child).children) {- add_component(*alt, false);+ add_component(*alt, false, 0); } } else {- add_component(*child, true);+ add_component(*child, true, 0); } } if (!enum_values.empty()) {@@ -875,6 +905,8 @@ class common_chat_schema_converter { } std::string visit(const common_chat_schema & schema, const std::string & name) {+ // the $ref targets are visited too, a $ref chain is not bounded by the builder+ depth_guard guard(_depth); std::string rule_name = is_reserved_name(name) ? name + "-" : name.empty() ? "root" : name; std::string sub_name = name + (name.empty() ? "" : "-"); common/json-schema-to-grammar.h1 + / 0 −
@@ -19,5 +19,6 @@ struct common_grammar_options { }; std::string gbnf_format_literal(const std::string & literal);+std::string gbnf_format_rule_name(const std::string & name); std::string build_grammar(const std::function<void(const common_grammar_builder &)> & cb, const common_grammar_options & options = {});common/json-schema.cpp30 + / 20 −
@@ -9,6 +9,18 @@ #include <utility> #include <vector> +namespace {+struct depth_guard {+ int & depth;+ depth_guard(int & depth) : depth(depth) {+ if (++depth > COMMON_RECURSIVE_LIMIT) {+ throw std::invalid_argument("JSON schema is nested more than " + std::to_string(COMMON_RECURSIVE_LIMIT) + " levels deep");+ }+ }+ ~depth_guard() { depth--; }+};+} // namespace+ class common_chat_schema_builder { const common_json & root_; common_chat_schema_document & doc_;@@ -224,18 +236,11 @@ class common_chat_schema_builder { return node; } - // the builder and the code that walks the built schema recurse once per level, a deeper schema overflows the stack- static constexpr int max_depth = 128;- int depth_ = 0;+ int depth_ = 0; common_chat_schema_ptr build_node(const common_json & schema, const std::string & path) {- if (depth_ >= max_depth) {- throw std::invalid_argument("JSON schema is nested more than " + std::to_string(max_depth) + " levels deep");- }- depth_++;- auto node = build_node_impl(schema, path);- depth_--;- return node;+ depth_guard guard(depth_);+ return build_node_impl(schema, path); } common_chat_schema_ptr build_node_impl(const common_json & schema, const std::string & path) {@@ -379,7 +384,9 @@ static common_chat_schema::value_type json_type(const common_json & value) { return common_chat_schema::TYPE_OBJECT; } -static common_chat_schema::type_set value_types_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited) {+// depth counts the $ref, anyOf and allOf hops, a $ref chain is not bounded by the builder+static common_chat_schema::type_set value_types_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited, int & depth) {+ depth_guard guard(depth); switch (s.kind()) { case common_chat_schema::KIND_ANY: return common_chat_schema::type_set::all();@@ -413,21 +420,21 @@ static common_chat_schema::type_set value_types_impl(const common_chat_schema & // a cycle contributes no type, to be safe return {}; }- auto types = value_types_impl(*target, visited);+ auto types = value_types_impl(*target, visited, depth); visited.erase(target); return types; } case common_chat_schema::KIND_ANY_OF: { common_chat_schema::type_set types; for (const auto & child : static_cast<const common_chat_schema_any_of &>(s).children) {- types |= value_types_impl(*child, visited);+ types |= value_types_impl(*child, visited, depth); } return types; } case common_chat_schema::KIND_ALL_OF: { auto types = common_chat_schema::type_set::all(); for (const auto & child : static_cast<const common_chat_schema_all_of &>(s).children) {- types &= value_types_impl(*child, visited);+ types &= value_types_impl(*child, visited, depth); } return types; }@@ -437,10 +444,12 @@ static common_chat_schema::type_set value_types_impl(const common_chat_schema & common_chat_schema::type_set common_chat_schema::value_types() const { std::unordered_set<const common_chat_schema *> visited;- return value_types_impl(*this, visited);+ int depth = 0;+ return value_types_impl(*this, visited, depth); } -static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited) {+static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited, int & depth) {+ depth_guard guard(depth); switch (s.kind()) { case common_chat_schema::KIND_STRING: return true;@@ -459,13 +468,13 @@ static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set< if (!target || !visited.insert(target).second) { return false; }- bool result = may_be_string_impl(*target, visited);+ bool result = may_be_string_impl(*target, visited, depth); visited.erase(target); return result; } case common_chat_schema::KIND_ANY_OF: for (const auto & child : static_cast<const common_chat_schema_any_of &>(s).children) {- if (may_be_string_impl(*child, visited)) {+ if (may_be_string_impl(*child, visited, depth)) { return true; } }@@ -477,7 +486,7 @@ static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set< if (child->kind() == common_chat_schema::KIND_ANY) { continue; }- if (!may_be_string_impl(*child, visited)) {+ if (!may_be_string_impl(*child, visited, depth)) { return false; } any_string = true;@@ -491,7 +500,8 @@ static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set< bool common_chat_schema::may_be_string() const { std::unordered_set<const common_chat_schema *> visited;- return may_be_string_impl(*this, visited);+ int depth = 0;+ return may_be_string_impl(*this, visited, depth); } const char * common_chat_schema::kind_name(node_kind kind) {common/peg-parser.cpp27 + / 15 −
@@ -10,10 +10,12 @@ #include <initializer_list> #include <map> #include <memory>-#include <regex> #include <set> #include <stdexcept> +// The parser can run with a lower limit than COMMON_RECURSIVE_LIMIT+static constexpr int RULE_RECURSIVE_LIMIT = 64;+ // Trick to catch missing branches template <typename T> inline constexpr bool is_always_false_v = false;@@ -186,13 +188,17 @@ std::string common_peg_ast_node::sanitized_text() const { } void common_peg_ast_arena::visit(common_peg_ast_id id, const common_peg_ast_visitor & visitor) const {- if (id == COMMON_PEG_INVALID_AST_ID) {- return;- }- const auto & node = get(id);- visitor(node);- for (const auto & child : node.children) {- visit(child, visitor);+ // no recursion: the AST is several nodes deep per level of the input+ std::vector<common_peg_ast_id> stack = { id };+ while (!stack.empty()) {+ const auto cur = stack.back();+ stack.pop_back();+ if (cur == COMMON_PEG_INVALID_AST_ID) {+ continue;+ }+ const auto & node = get(cur);+ visitor(node);+ stack.insert(stack.end(), node.children.rbegin(), node.children.rend()); } } @@ -736,8 +742,15 @@ struct parser_executor { } common_peg_parse_result operator()(const common_peg_rule_parser & p) {+ // only rules recurse, a recursive rule is entered once per input level+ if (ctx.rule_depth[p.child] >= RULE_RECURSIVE_LIMIT) {+ throw common_peg_recursion_error("input is nested more than " + std::to_string(RULE_RECURSIVE_LIMIT) + " levels deep");+ }+ // Parse the child+ ctx.rule_depth[p.child]++; auto result = arena.parse(p.child, ctx, start_pos);+ ctx.rule_depth[p.child]--; if (!result.fail()) { std::string_view text;@@ -819,6 +832,10 @@ common_peg_parse_result common_peg_arena::parse(common_peg_parse_context & ctx, if (root_ == COMMON_PEG_INVALID_PARSER_ID) { throw std::runtime_error("No root parser set"); }++ // Initialize rule depth for all parsers+ ctx.rule_depth.assign(parsers_.size(), 0);+ return parse(root_, ctx, start); } @@ -1069,11 +1086,6 @@ common_peg_parser operator|(const std::string & str, const common_peg_parser & p return operator|(str.c_str(), p); } -static std::string rule_name(const std::string & name) {- static const std::regex invalid_rule_chars_re("[^a-zA-Z0-9-]+");- return std::regex_replace(name, invalid_rule_chars_re, "-");-}- common_peg_parser_builder::common_peg_parser_builder() {} common_peg_parser common_peg_parser_builder::sequence(const std::vector<common_peg_parser_id> & parsers) {@@ -1155,14 +1167,14 @@ common_peg_parser common_peg_parser_builder::schema(const common_peg_parser & p, } common_peg_parser common_peg_parser_builder::rule(const std::string & name, const common_peg_parser & p, bool trigger) {- auto clean_name = rule_name(name);+ auto clean_name = gbnf_format_rule_name(name); auto rule_id = arena_.add_parser(common_peg_rule_parser{clean_name, p.id(), trigger}); arena_.add_rule(clean_name, rule_id); return ref(clean_name); } common_peg_parser common_peg_parser_builder::rule(const std::string & name, const std::function<common_peg_parser()> & builder_fn, bool trigger) {- auto clean_name = rule_name(name);+ auto clean_name = gbnf_format_rule_name(name); if (arena_.has_rule(clean_name)) { return ref(clean_name); }common/peg-parser.h6 + / 0 −
@@ -6,6 +6,7 @@ #include <memory> #include <set>+#include <stdexcept> #include <unordered_map> #include <string> #include <string_view>@@ -182,13 +183,18 @@ inline common_peg_parse_flags operator~(common_peg_parse_flags a) { return static_cast<common_peg_parse_flags>(~int(a)); } +struct common_peg_recursion_error : std::invalid_argument {+ using std::invalid_argument::invalid_argument;+};+ struct common_peg_parse_context { std::string input; // [h, e, l, l, o, _, w, o, r, l, d] std::vector<llama_token> tokens; // [id, -1, -1, -1, -1, id, -1, -1, -1, -1, -1] common_peg_parse_flags flags; common_peg_ast_arena ast; int parse_depth;+ std::vector<int> rule_depth; // index: rule child id common_peg_parse_context(common_peg_parse_flags flags = COMMON_PEG_PARSE_FLAG_NONE) : flags(flags), parse_depth(0) {}skills/code-review/SKILL.md1 + / 0 −
@@ -58,6 +58,7 @@ Mandatory on every review; any finding here is **blocking**. Rule of thumb: GGUF - **Reused/reserved buffers:** recheck bounds after a buffer is shrunk or reused; watch `reserve()` then index-by-assumed-size, and header fields read before their length is checked. - **Server JSON ints:** clamp client-supplied integers (token/discard counts, offsets) to non-negative and an upper bound before they reach index/pointer arithmetic. - **RPC-deserialized fields:** treat every field (type/buffer/data/ne/nb/op_params) as hostile - validate before use. Null/zero buffers skipping validation, attacker data pointers, out-of-range type indices, and negative strides sign-extending past a corner-only assert all give arbitrary read/write.+- **Recursion on untrusted input:** a function that recurses once per level (or per char) of request JSON, model output, a grammar, a template or a regex pattern overflows the stack - the HTTP threads have 512 KB on macOS, so a few hundred levels are enough. Every such function needs its own depth bound, checked in the function itself, not a pre-walk at a caller (the next caller skips it): libcommon uses `COMMON_RECURSIVE_LIMIT` (`common/common.h`), jinja `JINJA_RECURSIVE_LIMIT` (`common/jinja/utils.h`), the grammar `GRAMMAR_RECURSIVE_LIMIT` (`src/llama-grammar.h`). Count the levels of the input, not the frames, and throw `std::invalid_argument` for request input so the server answers 400. Look for recursion the code does not spell out: mutual recursion, recursive `std::function` lambdas, visitor/virtual dispatch, `std::regex` (libstdc++ recurses per char of the input), and per-char helpers that recurse where a loop would do. - **Lifetime/UAF:** flag stored raw pointers to caller/temporary storage, cached pointers to buffers a later free releases, async ops whose source may drop before completion, and structures not invalidated on free/realloc. Null-check conditionally-built or "not required" tensors before dereferencing. ## Approach and design (when a new component/infra is introduced)src/llama-grammar.cpp130 + / 64 −
@@ -8,10 +8,11 @@ #include <algorithm> #include <cstdint> #include <set>+#include <numeric> #include <stdexcept> #define MAX_REPETITION_THRESHOLD 2000-#define MAX_GRAMMAR_RECURSION_DEPTH 256+#define GRAMMAR_RECURSIVE_LIMIT 256 // // helpers //@@ -598,8 +599,8 @@ const char * llama_grammar_parser::parse_sequence( n_prev_rules = 1; rule.push_back({LLAMA_GRETYPE_RULE_REF, ref_rule_id}); } else if (*pos == '(') { // grouping- if (depth >= MAX_GRAMMAR_RECURSION_DEPTH) {- throw std::runtime_error("grammar recursion depth limit exceeded (max " + std::to_string(MAX_GRAMMAR_RECURSION_DEPTH) + "), please reduce nesting depth");+ if (depth >= GRAMMAR_RECURSIVE_LIMIT) {+ throw std::runtime_error("grammar recursion depth limit exceeded (max " + std::to_string(GRAMMAR_RECURSIVE_LIMIT) + "), please reduce nesting depth"); } // parse nested alternates into synthesized rule pos = parse_space(pos + 1, true);@@ -947,34 +948,19 @@ static void llama_grammar_advance_stack( } } -static llama_grammar_candidates llama_grammar_reject_candidates(- const llama_grammar_rules & rules,- const llama_grammar_stacks & stacks,- const llama_grammar_candidates & candidates) {- GGML_ASSERT(!stacks.empty()); // REVIEW-- if (candidates.empty()) {- return {};- }-- auto rejects = llama_grammar_reject_candidates_for_stack(rules, stacks.front(), candidates);-- for (size_t i = 1, size = stacks.size(); i < size; ++i) {- rejects = llama_grammar_reject_candidates_for_stack(rules, stacks[i], rejects);- }-- return rejects;-}- static bool llama_grammar_detect_left_recursion( const llama_grammar_rules & rules, size_t rule_index, std::vector<bool> * rules_visited, std::vector<bool> * rules_in_progress,- std::vector<bool> * rules_may_be_empty) {+ std::vector<bool> * rules_may_be_empty,+ uint32_t depth = 0) { if ((*rules_in_progress)[rule_index]) { return true; }+ if (depth > GRAMMAR_RECURSIVE_LIMIT) {+ throw std::runtime_error("grammar rules are chained more than " + std::to_string(GRAMMAR_RECURSIVE_LIMIT) + " levels deep");+ } (*rules_in_progress)[rule_index] = true; @@ -1000,7 +986,7 @@ static bool llama_grammar_detect_left_recursion( bool recurse_into_nonterminal = true; for (size_t i = 0; i < rule.size(); i++) { if (rule[i].type == LLAMA_GRETYPE_RULE_REF && recurse_into_nonterminal) {- if (llama_grammar_detect_left_recursion(rules, (size_t)rule[i].value, rules_visited, rules_in_progress, rules_may_be_empty)) {+ if (llama_grammar_detect_left_recursion(rules, (size_t)rule[i].value, rules_visited, rules_in_progress, rules_may_be_empty, depth + 1)) { return true; } if (!((*rules_may_be_empty)[(size_t)rule[i].value])) {@@ -1064,56 +1050,63 @@ void llama_grammar_accept(struct llama_grammar * grammar, uint32_t chr) { grammar->stacks = std::move(stacks_new); } -llama_grammar_candidates llama_grammar_reject_candidates_for_stack(+// checks the code point at depth of each candidate (an index into the input) against the stack+// returns true when some match: they go to next, and the stacks that follow to next_stacks+static bool llama_grammar_reject_step( const llama_grammar_rules & rules, const llama_grammar_stack & stack,- const llama_grammar_candidates & candidates) {-- llama_grammar_candidates rejects;- rejects.reserve(candidates.size());+ const llama_grammar_candidates & candidates,+ size_t depth,+ const std::vector<uint32_t> & cur,+ std::vector<uint32_t> & rejects,+ std::vector<uint32_t> & next,+ llama_grammar_stacks & next_stacks) {+ rejects.reserve(cur.size()); if (stack.empty()) {- for (const auto & tok : candidates) {- if (*tok.code_points != 0 || tok.partial_utf8.n_remain != 0) {- rejects.push_back(tok);+ for (const uint32_t i : cur) {+ const auto & tok = candidates[i];+ if (tok.code_points[depth] != 0 || tok.partial_utf8.n_remain != 0) {+ rejects.push_back(i); } }- return rejects;+ return false; } const llama_grammar_element * stack_pos = stack.back(); // if the top of the stack is a token rule, then we only need to check the token id if (stack_pos->type == LLAMA_GRETYPE_TOKEN || stack_pos->type == LLAMA_GRETYPE_TOKEN_NOT) {- for (const auto & tok : candidates) {- if (*tok.code_points == 0) {+ for (const uint32_t i : cur) {+ const auto & tok = candidates[i];+ if (tok.code_points[depth] == 0) { // reached the end of a token consumed by char rules, reject iff it ended // in a partial response if (tok.partial_utf8.n_remain != 0) {- rejects.push_back(tok);+ rejects.push_back(i); } } else if (!llama_grammar_match_token(stack_pos, tok.id)) {- rejects.push_back(tok);+ rejects.push_back(i); } }- return rejects;+ return false; } - llama_grammar_candidates next_candidates;- next_candidates.reserve(candidates.size());+ next.reserve(cur.size()); - for (const auto & tok : candidates) {- if (*tok.code_points == 0) {+ for (const uint32_t i : cur) {+ const auto & tok = candidates[i];+ if (tok.code_points[depth] == 0) { // reached end of full codepoints in token, reject iff it ended in a partial sequence // that cannot satisfy this position in grammar if (tok.partial_utf8.n_remain != 0 && !llama_grammar_match_partial_char(stack_pos, tok.partial_utf8)) {- rejects.push_back(tok);+ rejects.push_back(i); }- } else if (llama_grammar_match_char(stack_pos, *tok.code_points).first) {- next_candidates.push_back({ tok.index, tok.code_points + 1, tok.partial_utf8, tok.id });+ } else if (llama_grammar_match_char(stack_pos, tok.code_points[depth]).first) {+ next.push_back(i); } else {- rejects.push_back(tok);+ rejects.push_back(i); } } @@ -1124,17 +1117,80 @@ llama_grammar_candidates llama_grammar_reject_candidates_for_stack( if (!llama_grammar_is_end_of_sequence(stack_pos_after)) { stack_after.push_back(stack_pos_after); }- llama_grammar_stacks next_stacks; llama_grammar_advance_stack(rules, stack_after, next_stacks); - auto next_rejects = llama_grammar_reject_candidates(rules, next_stacks, next_candidates);- for (const auto & tok : next_rejects) {- rejects.push_back({ tok.index, tok.code_points - 1, tok.partial_utf8, tok.id });+ return true;+}++// the candidates that no stack accepts, each stack narrows the rejects of the previous one+// no recursion: a level per code point, a token from the GGUF can be long+static llama_grammar_candidates llama_grammar_reject_candidates(+ const llama_grammar_rules & rules,+ const llama_grammar_stacks & stacks,+ const llama_grammar_candidates & candidates) {+ GGML_ASSERT(!stacks.empty()); // REVIEW++ // a level checks the candidates that reach its code point against its stacks in turn+ struct level {+ llama_grammar_stacks stacks;+ size_t i; // the stack in progress+ std::vector<uint32_t> cur; // the candidates rejected by stacks[0, i)+ std::vector<uint32_t> rejects; // the rejects of stacks[i] at this code point+ };++ std::vector<uint32_t> all(candidates.size());+ std::iota(all.begin(), all.end(), 0u);++ std::vector<level> levels;+ levels.push_back({ stacks, 0, std::move(all), {} });++ while (true) {+ level & lv = levels.back();++ if (lv.i == lv.stacks.size() || lv.cur.empty()) {+ if (levels.size() == 1) {+ break;+ }+ // the rejects at the next code point belong to stacks[i] of the parent level+ level & parent = levels[levels.size() - 2];+ parent.rejects.insert(parent.rejects.end(), lv.cur.begin(), lv.cur.end());+ levels.pop_back();+ parent.cur = std::move(parent.rejects);+ parent.rejects = {};+ parent.i++;+ continue;+ }++ std::vector<uint32_t> rejects;+ std::vector<uint32_t> next;+ llama_grammar_stacks next_stacks;++ if (!llama_grammar_reject_step(rules, lv.stacks[lv.i], candidates, levels.size() - 1, lv.cur, rejects, next, next_stacks)) {+ lv.cur = std::move(rejects);+ lv.i++;+ continue;+ }++ GGML_ASSERT(!next_stacks.empty());+ lv.rejects = std::move(rejects);+ levels.push_back({ std::move(next_stacks), 0, std::move(next), {} }); } + llama_grammar_candidates rejects;+ rejects.reserve(levels[0].cur.size());+ for (const uint32_t i : levels[0].cur) {+ rejects.push_back(candidates[i]);+ } return rejects; } +llama_grammar_candidates llama_grammar_reject_candidates_for_stack(+ const llama_grammar_rules & rules,+ const llama_grammar_stack & stack,+ const llama_grammar_candidates & candidates) {+ return llama_grammar_reject_candidates(rules, llama_grammar_stacks{ stack }, candidates);+}+ //////////////////// struct llama_grammar * llama_grammar_init_impl(@@ -1169,14 +1225,19 @@ struct llama_grammar * llama_grammar_init_impl( std::vector<bool> rules_visited(n_rules); std::vector<bool> rules_in_progress(n_rules); std::vector<bool> rules_may_be_empty(n_rules);- for (size_t i = 0; i < n_rules; i++) {- if (rules_visited[i]) {- continue;- }- if (llama_grammar_detect_left_recursion(vec_rules, i, &rules_visited, &rules_in_progress, &rules_may_be_empty)) {- LLAMA_LOG_ERROR("unsupported grammar, left recursion detected for nonterminal at index %zu", i);- return nullptr;+ try {+ for (size_t i = 0; i < n_rules; i++) {+ if (rules_visited[i]) {+ continue;+ }+ if (llama_grammar_detect_left_recursion(vec_rules, i, &rules_visited, &rules_in_progress, &rules_may_be_empty)) {+ LLAMA_LOG_ERROR("unsupported grammar, left recursion detected for nonterminal at index %zu", i);+ return nullptr;+ } }+ } catch (const std::exception & err) {+ LLAMA_LOG_ERROR("unsupported grammar: %s\n", err.what());+ return nullptr; } // loop over alternates of start rule to build initial stacks@@ -1262,14 +1323,19 @@ struct llama_grammar * llama_grammar_init_impl( std::vector<bool> rules_visited(n_rules); std::vector<bool> rules_in_progress(n_rules); std::vector<bool> rules_may_be_empty(n_rules);- for (size_t i = 0; i < n_rules; i++) {- if (rules_visited[i]) {- continue;- }- if (llama_grammar_detect_left_recursion(vec_rules, i, &rules_visited, &rules_in_progress, &rules_may_be_empty)) {- LLAMA_LOG_ERROR("unsupported grammar, left recursion detected for nonterminal at index %zu\n", i);- return nullptr;+ try {+ for (size_t i = 0; i < n_rules; i++) {+ if (rules_visited[i]) {+ continue;+ }+ if (llama_grammar_detect_left_recursion(vec_rules, i, &rules_visited, &rules_in_progress, &rules_may_be_empty)) {+ LLAMA_LOG_ERROR("unsupported grammar, left recursion detected for nonterminal at index %zu\n", i);+ return nullptr;+ } }+ } catch (const std::exception & err) {+ LLAMA_LOG_ERROR("unsupported grammar: %s\n", err.what());+ return nullptr; } // loop over alternates of start rule to build initial stackssrc/llama-vocab.cpp39 + / 38 −
@@ -29,31 +29,25 @@ struct naive_trie { naive_trie() : has_value(false), value(0) { }+ // no recursion: a token from the GGUF can be long void insert(const char * key, size_t len, int32_t value = 0) {- if (len == 0) {- this->has_value = true;- this->value = value;- return;- }- char c = key[0];- auto res = children.find(c);- if (res != children.end()) {- res->second.insert(key + 1, len - 1, value);- } else {- auto res = children.insert(std::make_pair(c, naive_trie()));- res.first->second.insert(key + 1, len - 1, value);+ naive_trie * node = this;+ for (size_t i = 0; i < len; i++) {+ node = &node->children[key[i]]; }+ node->has_value = true;+ node->value = value; } std::pair<const char *, size_t> get_longest_prefix(const char * key, size_t len, size_t offset = 0) const {- if (len == 0 || offset == len) {- return std::make_pair(key, offset);- }- char c = key[offset];- auto res = children.find(c);- if (res != children.end()) {- return res->second.get_longest_prefix(key, len, offset + 1);+ const naive_trie * node = this;+ while (offset < len) {+ auto res = node->children.find(key[offset]);+ if (res == node->children.end()) {+ break;+ }+ node = &res->second;+ offset++; }- return std::make_pair(key, offset); } const struct naive_trie * traverse(const char c) const {@@ -174,30 +168,37 @@ struct llm_tokenizer_spm_session { } private:+ // no recursion: the merges of a long token can be deep void resegment(llm_symbol & symbol, std::vector<llama_token> & output) {- auto text = std::string(symbol.text, symbol.n);- auto token = vocab.text_to_token(text);+ std::vector<const llm_symbol *> todo = { &symbol };+ while (!todo.empty()) {+ const llm_symbol & cur = *todo.back();+ todo.pop_back(); - // Do we need to support is_unused?- if (token != LLAMA_TOKEN_NULL) {- output.push_back(token);- return;- }+ auto text = std::string(cur.text, cur.n);+ auto token = vocab.text_to_token(text);++ // Do we need to support is_unused?+ if (token != LLAMA_TOKEN_NULL) {+ output.push_back(token);+ continue;+ } - const auto p = rev_merge.find(text);+ const auto p = rev_merge.find(text); - if (p == rev_merge.end()) {- // output any symbols that did not form tokens as bytes.- output.reserve(output.size() + symbol.n);- for (int j = 0; j < (int)symbol.n; ++j) {- llama_token id = vocab.byte_to_token(symbol.text[j]);- output.push_back(id);+ if (p == rev_merge.end()) {+ // output any symbols that did not form tokens as bytes.+ output.reserve(output.size() + cur.n);+ for (int j = 0; j < (int)cur.n; ++j) {+ llama_token id = vocab.byte_to_token(cur.text[j]);+ output.push_back(id);+ }+ continue; }- return;- } - resegment(symbols[p->second.first], output);- resegment(symbols[p->second.second], output);+ todo.push_back(&symbols[p->second.second]);+ todo.push_back(&symbols[p->second.first]);+ } } void try_add_bigram(int left, int right) {tests/peg-parser/test-json-parser.cpp19 + / 0 −
@@ -130,4 +130,23 @@ void test_json_parser(testing &t) { t.assert_true("fail", result.fail()); }); });++ // json-value is entered once per level+ t.test("deep JSON", [](testing &t) {+ auto json = build_peg_parser([](common_peg_parser_builder & p) { return p.json(); });++ const auto parse = [&](size_t depth) {+ std::string input = std::string(depth, '[') + std::string(depth, ']');+ common_peg_parse_context ctx(input);+ try {+ return json.parse(ctx).success() ? std::string() : std::string("fail");+ } catch (const common_peg_recursion_error & e) {+ return std::string(e.what());+ }+ };++ t.assert_equal("64 levels", std::string(), parse(64));+ t.assert_true("65 levels", parse(65).find("nested more than 64 levels deep") != std::string::npos);+ t.assert_true("100000 levels", parse(100000).find("nested more than 64 levels deep") != std::string::npos);+ }); }tests/test-grammar-integration.cpp31 + / 0 −
@@ -879,6 +879,18 @@ static void test_failure_left_recursion() { empty ::= "blah" | )"""; assert(test_build_grammar_fails(hardest_str)); + const auto chain = [](int n) {+ std::string s = "root ::= r0\n";+ for (int i = 0; i < n; i++) {+ s += "r" + std::to_string(i) + " ::= r" + std::to_string(i + 1) + "\n";+ }+ return s + "r" + std::to_string(n) + " ::= \"a\"\n";+ };+ llama_grammar * grammar = build_grammar(chain(100));+ assert(grammar != nullptr);+ llama_grammar_free_impl(grammar);+ assert(test_build_grammar_fails(chain(1000)));+ fprintf(stderr, " ✅︎ Passed\n"); } @@ -1476,6 +1488,25 @@ static void test_json_schema() { // R"""({"productId": 1, "productName": "A green door", "price": 12.50, "tags": ["home", "green", "home"]})""", } );++ // the deepest schema of json_schema_to_grammar must give a valid grammar+ std::string deep_schema;+ for (int i = 0; i < 127; i++) {+ deep_schema += R"({"type": "array", "items": )";+ }+ deep_schema += R"({"type": "string"})" + std::string(127, '}');+ test_schema(+ "127 nested arrays",+ deep_schema,+ // Passing strings+ {+ std::string(127, '[') + R"("a")" + std::string(127, ']'),+ },+ // Failing strings+ {+ std::string(126, '[') + R"("a")" + std::string(126, ']'),+ }+ ); } int main() {tests/test-jinja.cpp44 + / 0 −
@@ -17,6 +17,7 @@ using json = common_json; static void test_template(testing & t, const std::string & name, const std::string & tmpl, const json & vars, const std::string & expect);+static void test_template_error(testing & t, const std::string & name, const std::string & tmpl, const std::string & expect); static void test_whitespace_control(testing & t); static void test_conditionals(testing & t);@@ -606,6 +607,17 @@ static void test_expressions(testing & t) { {{"cond", true}}, "yes" );++ test_template(t, "nested parentheses",+ "{{ " + std::string(30, '(') + "1" + std::string(30, ')') + " }}",+ json::object(),+ "1"+ );++ test_template_error(t, "too deeply nested parentheses",+ "{{ " + std::string(1000, '(') + "1" + std::string(1000, ')') + " }}",+ "template is nested more than 128 levels deep"+ ); } static void test_set_statement(testing & t) {@@ -1029,6 +1041,11 @@ static void test_filters(testing & t) { {{"x", nullptr}}, "None" );++ test_template_error(t, "tojson of a list that contains itself",+ "{% set arr = [] %}{% set _ = arr.append(arr) %}{{ arr|tojson }}",+ "value is nested more than 128 levels deep"+ ); } static void test_literals(testing & t) {@@ -1291,6 +1308,17 @@ static void test_macros(testing & t) { json::object(), "Dict {\n root1: 1\n root2: {\n nest1: 1\n nest2: {\n nest3: 2\n }\n }\n}" );++ test_template(t, "macro recursion",+ "{% macro f(n) %}{% if n < 100 %}{{ f(n + 1) }}{% else %}{{ n }}{% endif %}{% endmacro %}{{ f(0) }}",+ json::object(),+ "100"+ );++ test_template_error(t, "endless macro recursion",+ "{% macro f(n) %}{{ f(n + 1) }}{% endmacro %}{{ f(0) }}",+ "macro calls are nested more than 128 levels deep"+ ); } static void test_namespace(testing & t) {@@ -2554,6 +2582,22 @@ static void test_string_parts(testing & t) { } +static void test_template_error(testing & t, const std::string & name, const std::string & tmpl, const std::string & expect) {+ t.test(name, [&tmpl, &expect](testing & t) {+ std::string error;+ try {+ jinja::lexer lexer;+ jinja::program ast = jinja::parse_from_tokens(lexer.tokenize(tmpl));+ jinja::context ctx(tmpl);+ jinja::runtime runtime(ctx);+ runtime.gather_string_parts(runtime.execute(ast));+ } catch (const std::exception & e) {+ error = e.what();+ }+ t.assert_true("expected error: " + expect + ", got: " + error, error.find(expect) != std::string::npos);+ });+}+ static void test_template_cpp(testing & t, const std::string & name, const std::string & tmpl, const json & vars, const std::string & expect) { t.test(name, [&tmpl, &vars, &expect](testing & t) { jinja::lexer lexer;tests/test-json-schema-to-grammar.cpp17 + / 0 −
@@ -1513,6 +1513,23 @@ static void test_all(const std::string & title, std::function<void(const TestCas "" }); + test({+ FAILURE,+ "allOf with a $ref cycle",+ R"""({+ "allOf": [{"$ref": "#/$defs/a"}],+ "$defs": {"a": {"$ref": "#/$defs/a"}}+ })""",+ ""+ });++ test({+ FAILURE,+ "long property name with additionalProperties",+ R"""({"type": "object", "properties": {")""" + std::string(1000, 'a') + R"""(": {"type": "string"}}, "additionalProperties": true})""",+ ""+ });+ // only the property with the bad pattern degrades test({ SUCCESS,tests/test-server-component.cpp27 + / 0 −
@@ -527,6 +527,32 @@ static void test_dry_sequence_breakers(testing & t) { } } +//+// glob_match+//++static void test_glob_match(testing & t) {+ t.test("patterns", [](testing & t) {+ t.assert_true("literal", glob_match("src/main.cpp", "src/main.cpp"));+ t.assert_true("star", glob_match("src/*.cpp", "src/main.cpp"));+ t.assert_true("star stops at /", !glob_match("*.cpp", "src/main.cpp"));+ t.assert_true("double star", glob_match("**/*.cpp", "a/b/c/main.cpp"));+ t.assert_true("double star at end", glob_match("src/**", "src/a/b"));+ t.assert_true("question mark", glob_match("ma?n.cpp", "main.cpp"));+ t.assert_true("question mark not /", !glob_match("a?b", "a/b"));+ t.assert_true("class", glob_match("[a-c]x", "bx"));+ t.assert_true("negated class", !glob_match("[!a-c]x", "bx"));+ t.assert_true("unclosed class", glob_match("[ab", "[ab"));+ t.assert_true("mismatch", !glob_match("main.cpp", "main.c"));+ });+ t.test("long input", [](testing & t) {+ const std::string path(100000, 'a');+ t.assert_true("long literal", glob_match(path, path));+ t.assert_true("long path", glob_match("**", path));+ t.assert_true("too many wildcards", !glob_match(std::string(1000, '*'), "a"));+ });+}+ int main(int argc, char ** argv) { testing t; @@ -548,6 +574,7 @@ int main(int argc, char ** argv) { t.test("decision_escape", test_decision_escape); t.test("rerank_template", test_rerank_template); t.test("decision_depth", test_decision_depth);+ t.test("glob_match", test_glob_match); t.test("dry_sequence_breakers", test_dry_sequence_breakers); return t.summary();