Coverage Report

Created: 2026-07-12 08:37

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/util/mustache/mustache.cc
Line
Count
Source
1
// Licensed under the Apache License, Version 2.0 (the "License");
2
// you may not use this file except in compliance with the License.
3
// You may obtain a copy of the License at
4
//
5
// http://www.apache.org/licenses/LICENSE-2.0
6
//
7
// Unless required by applicable law or agreed to in writing, software
8
// distributed under the License is distributed on an "AS IS" BASIS,
9
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
// See the License for the specific language governing permissions and
11
// limitations under the License.
12
13
#include "util/mustache/mustache.h"
14
15
#include <rapidjson/allocators.h>
16
#include <rapidjson/document.h>
17
#include <rapidjson/encodings.h>
18
#include <rapidjson/prettywriter.h>
19
#include <rapidjson/rapidjson.h>
20
#include <strings.h>
21
22
#include <algorithm>
23
#include <boost/algorithm/string/classification.hpp>
24
#include <boost/algorithm/string/detail/classification.hpp>
25
#include <boost/algorithm/string/join.hpp>
26
#include <boost/algorithm/string/predicate_facade.hpp>
27
#include <boost/algorithm/string/split.hpp>
28
#include <boost/algorithm/string/trim.hpp>
29
#include <boost/iterator/iterator_facade.hpp>
30
#include <boost/type_index/type_index_facade.hpp>
31
#include <fstream> // IWYU pragma: keep
32
#include <iostream>
33
#include <stack>
34
#include <vector>
35
36
#include "rapidjson/stringbuffer.h"
37
#include "rapidjson/writer.h"
38
39
using namespace rapidjson;
40
using namespace boost::algorithm;
41
42
namespace mustache {
43
44
// TODO:
45
// # Handle malformed templates better
46
// # Better support for reading templates from files
47
48
enum TagOperator {
49
    SUBSTITUTION,
50
    SECTION_START,
51
    NEGATED_SECTION_START,
52
    PREDICATE_SECTION_START,
53
    SECTION_END,
54
    PARTIAL,
55
    COMMENT,
56
    LENGTH,
57
    EQUALITY,
58
    INEQUALITY,
59
    LITERAL,
60
    NONE
61
};
62
63
struct OpCtx {
64
    TagOperator op;
65
    std::string tag_name;
66
    std::string tag_arg;
67
    bool escaped = false;
68
};
69
70
struct ContextStack {
71
    const Value* value;
72
    const ContextStack* parent;
73
};
74
75
272
TagOperator GetOperator(const std::string& tag) {
76
272
    if (tag.size() == 0) return SUBSTITUTION;
77
272
    switch (tag[0]) {
78
16
    case '#':
79
16
        return SECTION_START;
80
8
    case '^':
81
8
        return NEGATED_SECTION_START;
82
0
    case '?':
83
0
        return PREDICATE_SECTION_START;
84
72
    case '/':
85
72
        return SECTION_END;
86
8
    case '>':
87
8
        return PARTIAL;
88
8
    case '!':
89
8
        if (tag.size() == 1 || tag[1] != '=') return COMMENT;
90
0
        return INEQUALITY;
91
0
    case '%':
92
0
        return LENGTH;
93
0
    case '~':
94
0
        return LITERAL;
95
0
    case '=':
96
0
        return EQUALITY;
97
160
    default:
98
160
        return SUBSTITUTION;
99
272
    }
100
272
}
101
102
int EvaluateTag(const std::string& document, const std::string& document_root, int idx,
103
                const ContextStack* context, const OpCtx& op_ctx, std::stringstream* out);
104
105
static bool RenderTemplate(const std::string& document, const std::string& document_root,
106
                           const ContextStack* stack, std::stringstream* out);
107
108
144
void EscapeHtml(const std::string& in, std::stringstream* out) {
109
9.52k
    for (const char& c : in) {
110
9.52k
        switch (c) {
111
0
        case '&':
112
0
            (*out) << "&amp;";
113
0
            break;
114
0
        case '"':
115
0
            (*out) << "&quot;";
116
0
            break;
117
0
        case '\'':
118
0
            (*out) << "&apos;";
119
0
            break;
120
0
        case '<':
121
0
            (*out) << "&lt;";
122
0
            break;
123
128
        case '>':
124
128
            (*out) << "&gt;";
125
128
            break;
126
9.39k
        default:
127
9.39k
            (*out) << c;
128
9.39k
            break;
129
9.52k
        }
130
9.52k
    }
131
144
}
132
133
0
void Dump(const rapidjson::Value& v) {
134
0
    StringBuffer buffer;
135
0
    Writer<StringBuffer> writer(buffer);
136
0
    v.Accept(writer);
137
0
    std::cout << buffer.GetString() << std::endl;
138
0
}
139
140
// Breaks a dotted path into individual components. One wrinkle, which stops this from
141
// being a simple split() is that we allow path components to be quoted, e.g.: "foo".bar,
142
// and any '.' characters inside those quoted sections aren't considered to be
143
// delimiters. This is to allow Json keys that contain periods.
144
176
void FindJsonPathComponents(const std::string& path, std::vector<std::string>* components) {
145
176
    bool in_quote = false;
146
176
    bool escape_this_char = false;
147
176
    int start = 0;
148
1.33k
    for (int i = start; i < path.size(); ++i) {
149
1.16k
        if (path[i] == '"' && !escape_this_char) in_quote = !in_quote;
150
1.16k
        if (path[i] == '.' && !escape_this_char && !in_quote) {
151
            // Current char == delimiter and not escaped and not in a quote pair => found a
152
            // component
153
0
            if (i - start > 0) {
154
0
                if (path[start] == '"' && path[(i - 1) - start] == '"') {
155
0
                    if (i - start > 3) {
156
0
                        components->push_back(path.substr(start + 1, i - (start + 2)));
157
0
                    }
158
0
                } else {
159
0
                    components->push_back(path.substr(start, i - start));
160
0
                }
161
0
                start = i + 1;
162
0
            }
163
0
        }
164
165
1.16k
        escape_this_char = (path[i] == '\\' && !escape_this_char);
166
1.16k
    }
167
168
176
    if (path.size() - start > 0) {
169
176
        if (path[start] == '"' && path[(path.size() - 1) - start] == '"') {
170
0
            if (path.size() - start > 3) {
171
0
                components->push_back(path.substr(start + 1, path.size() - (start + 2)));
172
0
            }
173
176
        } else {
174
176
            components->push_back(path.substr(start, path.size() - start));
175
176
        }
176
176
    }
177
176
}
178
179
// Looks up the json entity at 'path' in 'parent_context', and places it in 'resolved'. If
180
// the entity does not exist (i.e. the path is invalid), 'resolved' will be set to nullptr.
181
void ResolveJsonContext(const std::string& path, const ContextStack* stack,
182
184
                        const Value** resolved) {
183
184
    if (path == ".") {
184
8
        *resolved = stack->value;
185
8
        return;
186
8
    }
187
176
    std::vector<std::string> components;
188
176
    FindJsonPathComponents(path, &components);
189
190
    // At each enclosing level of context, try to resolve the path.
191
176
    for (; stack != nullptr; stack = stack->parent) {
192
176
        const Value* cur = stack->value;
193
176
        bool match = true;
194
176
        for (const std::string& c : components) {
195
176
            if (cur->IsObject() && cur->HasMember(c.c_str())) {
196
176
                cur = &(*cur)[c.c_str()];
197
176
            } else {
198
0
                match = false;
199
0
                break;
200
0
            }
201
176
        }
202
176
        if (match) {
203
176
            *resolved = cur;
204
176
            return;
205
176
        }
206
176
    }
207
0
    *resolved = nullptr;
208
0
}
209
210
288
int FindNextTag(const std::string& document, int idx, OpCtx* op, std::stringstream* out) {
211
288
    op->op = NONE;
212
16.3k
    while (idx < document.size()) {
213
16.3k
        if (document[idx] == '{' && idx < (document.size() - 3) && document[idx + 1] == '{') {
214
272
            if (document[idx + 2] == '{') {
215
16
                idx += 3;
216
16
                op->escaped = true;
217
256
            } else {
218
256
                op->escaped = false;
219
256
                idx += 2; // Now at start of template expression
220
256
            }
221
272
            std::stringstream expr;
222
8.71k
            while (idx < document.size()) {
223
8.71k
                if (document[idx] != '}') {
224
8.44k
                    expr << document[idx];
225
8.44k
                    ++idx;
226
8.44k
                } else {
227
272
                    if (!op->escaped && idx < document.size() - 1 && document[idx + 1] == '}') {
228
256
                        ++idx;
229
256
                        break;
230
256
                    } else if (op->escaped && idx < document.size() - 2 &&
231
16
                               document[idx + 1] == '}' && document[idx + 2] == '}') {
232
16
                        idx += 2;
233
16
                        break;
234
16
                    } else {
235
0
                        expr << '}';
236
0
                    }
237
272
                }
238
8.71k
            }
239
240
272
            std::string key = expr.str();
241
272
            trim(key);
242
272
            if (key != ".") trim_if(key, is_any_of("."));
243
272
            if (key.size() == 0) continue;
244
272
            op->op = GetOperator(key);
245
272
            if (op->op != SUBSTITUTION) {
246
112
                int len = op->op == INEQUALITY ? 2 : 1;
247
112
                key = key.substr(len);
248
112
                trim(key);
249
112
            }
250
272
            if (key.size() == 0) continue;
251
252
272
            if (op->op == EQUALITY || op->op == INEQUALITY) {
253
                // Find an argument
254
0
                std::vector<std::string> components;
255
0
                split(components, key, is_any_of(" "));
256
0
                key = components[0];
257
0
                components.erase(components.begin());
258
0
                op->tag_arg = join(components, " ");
259
0
            }
260
261
272
            op->tag_name = key;
262
272
            return ++idx;
263
16.0k
        } else {
264
16.0k
            if (out != nullptr) (*out) << document[idx];
265
16.0k
        }
266
16.0k
        ++idx;
267
16.0k
    }
268
16
    return idx;
269
288
}
270
271
// Evaluates a [PREDICATE_|NEGATED_]SECTION_START / SECTION_END pair by evaluating the tag
272
// in 'parent_context'. False or non-existant values cause the entire section to be
273
// skipped. True values cause the section to be evaluated as though it were a normal
274
// section, but with the parent context being the root context for that section.
275
//
276
// If 'is_negation' is true, the behaviour is the opposite of the above: false values
277
// cause the section to be normally evaluated etc.
278
int EvaluateSection(const std::string& document, const std::string& document_root, int idx,
279
                    const ContextStack* context_stack, const OpCtx& op_ctx,
280
24
                    std::stringstream* out) {
281
    // Precondition: idx is the immediate next character after an opening {{ #tag_name }}
282
24
    const Value* context;
283
24
    ResolveJsonContext(op_ctx.tag_name, context_stack, &context);
284
285
    // If we a) cannot resolve the context from the tag name or b) the context evaluates to
286
    // false, we should skip the contents of the template until a closing {{/tag_name}}.
287
24
    bool skip_contents = false;
288
289
24
    if (op_ctx.op == NEGATED_SECTION_START || op_ctx.op == PREDICATE_SECTION_START ||
290
24
        op_ctx.op == SECTION_START) {
291
24
        skip_contents = (context == nullptr || context->IsFalse());
292
293
        // If the tag is a negative block (i.e. {{^tag_name}}), do the opposite: if the
294
        // context exists and is true, skip the contents, else echo them.
295
24
        if (op_ctx.op == NEGATED_SECTION_START) {
296
8
            context = context_stack->value;
297
8
            skip_contents = !skip_contents;
298
16
        } else if (op_ctx.op == PREDICATE_SECTION_START) {
299
0
            context = context_stack->value;
300
0
        }
301
24
    } else if (op_ctx.op == INEQUALITY || op_ctx.op == EQUALITY) {
302
0
        skip_contents = (context == nullptr || !context->IsString() ||
303
0
                         strcasecmp(context->GetString(), op_ctx.tag_arg.c_str()) != 0);
304
0
        if (op_ctx.op == INEQUALITY) skip_contents = !skip_contents;
305
0
        context = context_stack->value;
306
0
    }
307
308
24
    std::vector<const Value*> values;
309
24
    if (!skip_contents && context != nullptr && context->IsArray()) {
310
64
        for (int i = 0; i < context->Size(); ++i) {
311
56
            values.push_back(&(*context)[i]);
312
56
        }
313
16
    } else {
314
16
        values.push_back(skip_contents ? nullptr : context);
315
16
    }
316
24
    if (values.size() == 0) {
317
0
        skip_contents = true;
318
0
        values.push_back(nullptr);
319
0
    }
320
321
24
    int start_idx = idx;
322
72
    for (const Value* v : values) {
323
72
        idx = start_idx;
324
72
        std::stack<OpCtx> section_starts;
325
72
        section_starts.push(op_ctx);
326
192
        while (idx < document.size()) {
327
192
            OpCtx next_ctx;
328
192
            idx = FindNextTag(document, idx, &next_ctx, skip_contents ? nullptr : out);
329
192
            if (skip_contents &&
330
192
                (next_ctx.op == SECTION_START || next_ctx.op == PREDICATE_SECTION_START ||
331
8
                 next_ctx.op == NEGATED_SECTION_START)) {
332
0
                section_starts.push(next_ctx);
333
192
            } else if (next_ctx.op == SECTION_END) {
334
72
                if (next_ctx.tag_name != section_starts.top().tag_name) return -1;
335
72
                section_starts.pop();
336
72
            }
337
192
            if (section_starts.empty()) break;
338
339
            // Don't need to evaluate any templates if we're skipping the contents
340
120
            if (!skip_contents) {
341
120
                ContextStack new_context = {v, context_stack};
342
120
                idx = EvaluateTag(document, document_root, idx, &new_context, next_ctx, out);
343
120
            }
344
120
        }
345
72
    }
346
24
    return idx;
347
24
}
348
349
// Evaluates a SUBSTITUTION tag, by replacing its contents with the value of the tag's
350
// name in 'parent_context'.
351
int EvaluateSubstitution(const std::string& document, const int idx,
352
                         const ContextStack* context_stack, const OpCtx& op_ctx,
353
160
                         std::stringstream* out) {
354
160
    const Value* val;
355
160
    ResolveJsonContext(op_ctx.tag_name, context_stack, &val);
356
160
    if (val == nullptr) return idx;
357
160
    if (val->IsString()) {
358
160
        if (!op_ctx.escaped) {
359
144
            EscapeHtml(val->GetString(), out);
360
144
        } else {
361
            // TODO: Triple {{{ means don't escape
362
16
            (*out) << val->GetString();
363
16
        }
364
160
    } else if (val->IsInt64()) {
365
0
        (*out) << val->GetInt64();
366
0
    } else if (val->IsInt()) {
367
0
        (*out) << val->GetInt();
368
0
    } else if (val->IsDouble()) {
369
0
        (*out) << val->GetDouble();
370
0
    } else if (val->IsBool()) {
371
0
        (*out) << std::boolalpha << val->GetBool();
372
0
    }
373
160
    return idx;
374
160
}
375
376
// Evaluates a LENGTH tag by replacing its contents with the type-dependent 'size' of the
377
// value.
378
int EvaluateLength(const std::string& document, const int idx, const ContextStack* context_stack,
379
0
                   const std::string& tag_name, std::stringstream* out) {
380
0
    const Value* val;
381
0
    ResolveJsonContext(tag_name, context_stack, &val);
382
0
    if (val == nullptr) return idx;
383
0
    if (val->IsArray()) {
384
0
        (*out) << val->Size();
385
0
    } else if (val->IsString()) {
386
0
        (*out) << val->GetStringLength();
387
0
    };
388
389
0
    return idx;
390
0
}
391
392
int EvaluateLiteral(const std::string& document, const int idx, const ContextStack* context_stack,
393
0
                    const std::string& tag_name, std::stringstream* out) {
394
0
    const Value* val;
395
0
    ResolveJsonContext(tag_name, context_stack, &val);
396
0
    if (val == nullptr) return idx;
397
0
    if (!val->IsArray() && !val->IsObject()) return idx;
398
0
    StringBuffer strbuf;
399
0
    PrettyWriter<StringBuffer> writer(strbuf);
400
0
    val->Accept(writer);
401
0
    (*out) << strbuf.GetString();
402
0
    return idx;
403
0
}
404
405
// Evaluates a 'partial' template by reading it fully from disk, then rendering it
406
// directly into the current output with the current context.
407
//
408
// TODO: This could obviously be more efficient (and there are lots of file accesses in a
409
// long list context).
410
void EvaluatePartial(const std::string& tag_name, const std::string& document_root,
411
8
                     const ContextStack* stack, std::stringstream* out) {
412
8
    std::stringstream ss;
413
8
    ss << document_root << tag_name;
414
8
    std::ifstream tmpl(ss.str().c_str());
415
8
    if (!tmpl.is_open()) {
416
0
        ss << ".mustache";
417
0
        tmpl.open(ss.str().c_str());
418
0
        if (!tmpl.is_open()) return;
419
0
    }
420
8
    std::stringstream file_ss;
421
8
    file_ss << tmpl.rdbuf();
422
8
    RenderTemplate(file_ss.str(), document_root, stack, out);
423
8
}
424
425
// Given a tag name, and its operator, evaluate the tag in the given context and write the
426
// output to 'out'. The heavy-lifting is delegated to specific Evaluate*()
427
// methods. Returns the new cursor position within 'document', or -1 on error.
428
int EvaluateTag(const std::string& document, const std::string& document_root, int idx,
429
216
                const ContextStack* context, const OpCtx& op_ctx, std::stringstream* out) {
430
216
    if (idx == -1) return idx;
431
216
    switch (op_ctx.op) {
432
16
    case SECTION_START:
433
16
    case PREDICATE_SECTION_START:
434
24
    case NEGATED_SECTION_START:
435
24
    case EQUALITY:
436
24
    case INEQUALITY:
437
24
        return EvaluateSection(document, document_root, idx, context, op_ctx, out);
438
160
    case SUBSTITUTION:
439
160
        return EvaluateSubstitution(document, idx, context, op_ctx, out);
440
8
    case COMMENT:
441
8
        return idx; // Ignored
442
8
    case PARTIAL:
443
8
        EvaluatePartial(op_ctx.tag_name, document_root, context, out);
444
8
        return idx;
445
0
    case LENGTH:
446
0
        return EvaluateLength(document, idx, context, op_ctx.tag_name, out);
447
0
    case LITERAL:
448
0
        return EvaluateLiteral(document, idx, context, op_ctx.tag_name, out);
449
16
    case NONE:
450
16
        return idx; // No tag was found
451
0
    case SECTION_END:
452
0
        return idx;
453
0
    default:
454
0
        std::cout << "Unknown tag: " << op_ctx.op << std::endl;
455
0
        return -1;
456
216
    }
457
216
}
458
459
static bool RenderTemplate(const std::string& document, const std::string& document_root,
460
24
                           const ContextStack* stack, std::stringstream* out) {
461
24
    int idx = 0;
462
120
    while (idx < document.size() && idx != -1) {
463
96
        OpCtx op;
464
96
        idx = FindNextTag(document, idx, &op, out);
465
96
        idx = EvaluateTag(document, document_root, idx, stack, op, out);
466
96
    }
467
468
24
    return idx != -1;
469
24
}
470
471
bool RenderTemplate(const std::string& document, const std::string& document_root,
472
16
                    const Value& context, std::stringstream* out) {
473
16
    ContextStack stack = {&context, nullptr};
474
16
    return RenderTemplate(document, document_root, &stack, out);
475
16
}
476
477
} // namespace mustache