From be1bb60110fa6359a792986e46549ed15c15c2b0 Mon Sep 17 00:00:00 2001
From: kylepritchard
Date: Thu, 8 Aug 2024 20:41:09 +0100
Subject: [PATCH] parser: update `@include` in templates, to work with relative
paths & prevent recursive calls (#21943)
---
vlib/v/TEMPLATES.md | 3 +-
vlib/v/parser/tests/tmpl/recursive_fail.html | 2 +
vlib/v/parser/tests/tmpl/recursive_fail2.html | 2 +
vlib/v/parser/tests/tmpl_recursion.out | 5 +
vlib/v/parser/tests/tmpl_recursion.vv | 3 +
vlib/v/parser/tmpl.v | 213 +++++++++++++-----
vlib/v/parser/tmpl_test.v | 12 -
vlib/v/tests/tmpl/base.html | 1 +
.../templates => tests/tmpl}/index.html | 2 +-
vlib/v/tests/tmpl/nested/child.html | 2 +
.../tmpl/nested/nested_deeper/grandchild.html | 3 +
vlib/v/tests/tmpl/parent.html | 4 +
vlib/v/tests/tmpl_test.v | 42 ++++
13 files changed, 223 insertions(+), 71 deletions(-)
create mode 100644 vlib/v/parser/tests/tmpl/recursive_fail.html
create mode 100644 vlib/v/parser/tests/tmpl/recursive_fail2.html
create mode 100644 vlib/v/parser/tests/tmpl_recursion.out
create mode 100644 vlib/v/parser/tests/tmpl_recursion.vv
delete mode 100644 vlib/v/parser/tmpl_test.v
create mode 100644 vlib/v/tests/tmpl/base.html
rename vlib/v/{parser/templates => tests/tmpl}/index.html (98%)
create mode 100644 vlib/v/tests/tmpl/nested/child.html
create mode 100644 vlib/v/tests/tmpl/nested/nested_deeper/grandchild.html
create mode 100644 vlib/v/tests/tmpl/parent.html
diff --git a/vlib/v/TEMPLATES.md b/vlib/v/TEMPLATES.md
index 28d0b9bac46382..791a545103d763 100644
--- a/vlib/v/TEMPLATES.md
+++ b/vlib/v/TEMPLATES.md
@@ -116,7 +116,7 @@ You can also write (and all other for condition syntaxes that are allowed in V):
The include directive is for including other html files (which will be processed as well)
and consists of two parts, the `@include` tag and a following `''` string.
-The path parameter is relative to the `/templates` directory in the corresponding project.
+The path parameter is relative to the template file being called.
### Example for the folder structure of a project using templates:
@@ -138,6 +138,7 @@ Project root
> Note that there shouldn't be a file suffix,
> it is automatically appended and only allows `html` files.
+
## js
The js directive consists of two parts, the `@js` tag and `''` string,
diff --git a/vlib/v/parser/tests/tmpl/recursive_fail.html b/vlib/v/parser/tests/tmpl/recursive_fail.html
new file mode 100644
index 00000000000000..78f3cb28258e36
--- /dev/null
+++ b/vlib/v/parser/tests/tmpl/recursive_fail.html
@@ -0,0 +1,2 @@
+This makes a recursive call
+@include 'recursive_fail2'
\ No newline at end of file
diff --git a/vlib/v/parser/tests/tmpl/recursive_fail2.html b/vlib/v/parser/tests/tmpl/recursive_fail2.html
new file mode 100644
index 00000000000000..f753e5b2127a02
--- /dev/null
+++ b/vlib/v/parser/tests/tmpl/recursive_fail2.html
@@ -0,0 +1,2 @@
+This makes a recursive call
+@include 'recursive_fail'
\ No newline at end of file
diff --git a/vlib/v/parser/tests/tmpl_recursion.out b/vlib/v/parser/tests/tmpl_recursion.out
new file mode 100644
index 00000000000000..b48765a11e5a6a
--- /dev/null
+++ b/vlib/v/parser/tests/tmpl_recursion.out
@@ -0,0 +1,5 @@
+vlib/v/parser/tests/tmpl/recursive_fail2.html:2:1: error: A recursive call is being made on template recursive_fail
+ 1 | This makes a recursive call
+ 2 | @include 'recursive_fail'
+ | ~~~~~~~~~
+
diff --git a/vlib/v/parser/tests/tmpl_recursion.vv b/vlib/v/parser/tests/tmpl_recursion.vv
new file mode 100644
index 00000000000000..1425b464ff2d13
--- /dev/null
+++ b/vlib/v/parser/tests/tmpl_recursion.vv
@@ -0,0 +1,3 @@
+fn test_tmpl_catch_recursion() {
+ recurse := $tmpl('tmpl/recursive_fail.html')
+}
\ No newline at end of file
diff --git a/vlib/v/parser/tmpl.v b/vlib/v/parser/tmpl.v
index 9dc5066ac830f3..817e8fd235fa50 100644
--- a/vlib/v/parser/tmpl.v
+++ b/vlib/v/parser/tmpl.v
@@ -90,6 +90,128 @@ fn insert_template_code(fn_name string, tmpl_str_start string, line string) stri
return rline
}
+// struct to track dependecies and cache templates for reuse without io
+struct DependencyCache {
+pub mut:
+ dependencies map[string][]string
+ cache map[string][]string
+}
+
+// custom error to handle issues when including template files
+struct IncludeError {
+ Error
+pub:
+ calling_file string
+ line_nr int
+ position int
+ col int
+ message string
+}
+
+fn (err IncludeError) msg() string {
+ return err.message
+}
+
+fn (err IncludeError) line_nr() int {
+ return err.line_nr
+}
+
+fn (err IncludeError) pos() int {
+ return err.position
+}
+
+fn (err IncludeError) calling_file() string {
+ return err.calling_file
+}
+
+fn (err IncludeError) col() int {
+ return err.col
+}
+
+fn (mut p Parser) process_includes(calling_file string, line_number int, line string, mut dc DependencyCache) ![]string {
+ base_path := os.dir(calling_file)
+ mut tline_number := line_number
+ mut file_name := if line.contains('"') {
+ line.split('"')[1]
+ } else if line.contains("'") {
+ line.split("'")[1]
+ } else {
+ position := line.index('@include ') or { 0 }
+ return &IncludeError{
+ calling_file: calling_file
+ line_nr: tline_number // line_number
+ position: position + '@include '.len
+ col: position + '@include '.len
+ message: 'path for @include must be quoted with \' or "'
+ }
+ }
+ mut file_ext := os.file_ext(file_name)
+ if file_ext == '' {
+ file_ext = '.html'
+ }
+ file_name = file_name.replace(file_ext, '')
+ mut file_path := os.real_path(os.join_path_single(base_path, '${file_name}${file_ext}'))
+
+ if !os.exists(file_path) && !file_name.contains('../') {
+ // the calling file is probably original way (relative to calling file) and works from the root folder
+ path_arr := base_path.split_any('/\\')
+ idx := path_arr.index('templates')
+ root_path := path_arr[..idx + 1].join('/')
+ file_name = file_name.rsplit('../')[0]
+ file_path = os.real_path(os.join_path_single(root_path, '${file_name}${file_ext}'))
+ }
+
+ // If file hasnt been called before then add to dependency tree
+ if !dc.dependencies[file_path].contains(calling_file) {
+ dc.dependencies[file_path] << calling_file
+ }
+
+ // Circular import detection
+ for callee in dc.dependencies[file_path] {
+ if dc.dependencies[callee].contains(file_path) {
+ return &IncludeError{
+ calling_file: calling_file
+ line_nr: tline_number
+ position: line.index('@include ') or { 0 }
+ message: 'A recursive call is being made on template ${file_name}'
+ }
+ }
+ }
+ mut file_content := []string{}
+ if file_path in dc.cache {
+ file_content = dc.cache[file_path]
+ } else {
+ file_content = os.read_lines(file_path) or {
+ position := line.index('@include ') or { 0 } + '@include '.len
+ return &IncludeError{
+ calling_file: calling_file
+ line_nr: tline_number // line_number
+ position: position
+ message: 'Reading file `${file_name}` from path: ${file_path} failed'
+ }
+ }
+ }
+ // no errors detected in calling file - reset tline_number (error reporting)
+ tline_number = 1
+
+ // loop over the imported file
+ for i, l in file_content {
+ if l.contains('@include ') {
+ processed := p.process_includes(file_path, tline_number, l, mut dc) or { return err }
+ file_content.delete(i) // remove the include line
+ for processed_line in processed.reverse() {
+ file_content.insert(i, processed_line)
+ tline_number--
+ }
+ }
+ }
+ // Add template to parser for reloading
+ p.template_paths << file_path
+ // Add the imported template to the cache
+ dc.cache[file_path] = file_content
+ return file_content
+}
+
// compile_file compiles the content of a file by the given path as a template
pub fn (mut p Parser) compile_template_file(template_file string, fn_name string) string {
mut lines := os.read_lines(template_file) or {
@@ -97,7 +219,8 @@ pub fn (mut p Parser) compile_template_file(template_file string, fn_name string
return ''
}
p.template_paths << template_file
- basepath := os.dir(template_file)
+ // create a new Dependency tree & cache any templates to avoid further io
+ mut dc := DependencyCache{}
lstartlength := lines.len * 30
tmpl_str_start := "\tsb_${fn_name}.write_string('"
mut source := strings.new_builder(1000)
@@ -163,65 +286,41 @@ fn vweb_tmpl_${fn_name}() string {
}
if line.contains('@include ') {
lines.delete(i)
- // Allow single or double quoted paths.
- mut file_name := if line.contains('"') {
- line.split('"')[1]
- } else if line.contains("'") {
- line.split("'")[1]
- } else {
- s := '@include '
- position := line.index(s) or { 0 }
- p.error_with_error(errors.Error{
- message: 'path for @include must be quoted with \' or "'
- file_path: template_file
- pos: token.Pos{
- len: s.len
- line_nr: tline_number
- pos: start_of_line_pos + position + s.len
- col: position + s.len
- last_line: lines.len + 1
- }
- reporter: .parser
- })
- ''
- }
- mut file_ext := os.file_ext(file_name)
- if file_ext == '' {
- file_ext = '.html'
- }
- file_name = file_name.replace(file_ext, '')
- // relative path, starting with the current folder
- mut templates_folder := os.real_path(basepath)
- if file_name.contains('/') && file_name.starts_with('/') {
- // an absolute path
- templates_folder = ''
- }
- file_path := os.real_path(os.join_path_single(templates_folder, '${file_name}${file_ext}'))
- $if trace_tmpl ? {
- eprintln('>>> basepath: "${basepath}" , template_file: "${template_file}" , fn_name: "${fn_name}" , @include line: "${line}" , file_name: "${file_name}" , file_ext: "${file_ext}" , templates_folder: "${templates_folder}" , file_path: "${file_path}"')
- }
- file_content := os.read_file(file_path) or {
- position := line.index('@include ') or { 0 } + '@include '.len
- p.error_with_error(errors.Error{
- message: 'Reading file ${file_name} from path: ${file_path} failed'
- details: "Failed to @include '${file_name}'"
- file_path: template_file
- pos: token.Pos{
- len: '@include '.len + file_name.len
- line_nr: tline_number
- pos: start_of_line_pos + position
- last_line: lines.len
- }
- reporter: .parser
- })
- ''
+ resolved := p.process_includes(template_file, tline_number, line, mut &dc) or {
+ if err is IncludeError {
+ p.error_with_error(errors.Error{
+ message: err.msg()
+ file_path: err.calling_file()
+ pos: token.Pos{
+ len: '@include '.len
+ line_nr: err.line_nr()
+ pos: start_of_line_pos + err.pos()
+ col: err.col()
+ last_line: lines.len
+ }
+ reporter: .parser
+ })
+ []string{}
+ } else {
+ p.error_with_error(errors.Error{
+ message: 'An unknown error has occurred'
+ file_path: template_file
+ pos: token.Pos{
+ len: '@include '.len
+ line_nr: tline_number
+ pos: start_of_line_pos
+ last_line: lines.len
+ }
+ reporter: .parser
+ })
+ []string{}
+ }
}
- p.template_paths << file_path
- file_splitted := file_content.split_into_lines().reverse()
- for f in file_splitted {
+ for resolved_line in resolved.reverse() {
tline_number--
- lines.insert(i, f)
+ lines.insert(i, resolved_line)
}
+
i--
continue
}
diff --git a/vlib/v/parser/tmpl_test.v b/vlib/v/parser/tmpl_test.v
deleted file mode 100644
index 1cc893bff9832d..00000000000000
--- a/vlib/v/parser/tmpl_test.v
+++ /dev/null
@@ -1,12 +0,0 @@
-// Add more examples of potentially buggy patterns in vlib/v/parser/templates/index.html
-fn test_tmpl_comptime() {
- index := $tmpl('templates/index.html').trim_space()
- // dump(index)
- assert index.contains('
Line ending with percent %\n')
- assert index.contains('
Line ending with at $\n')
- assert index.contains('
Line ending with ampersand &\n')
- assert index.contains('
Line ending with hash #\n')
- assert index.contains('
Line ending with slash /\n')
- assert index.contains('
Line ending with dollar $\n')
- assert index.contains('
Line ending with caret ^\n')
-}
diff --git a/vlib/v/tests/tmpl/base.html b/vlib/v/tests/tmpl/base.html
new file mode 100644
index 00000000000000..69fbe2b88e307e
--- /dev/null
+++ b/vlib/v/tests/tmpl/base.html
@@ -0,0 +1 @@
+This is the base file
\ No newline at end of file
diff --git a/vlib/v/parser/templates/index.html b/vlib/v/tests/tmpl/index.html
similarity index 98%
rename from vlib/v/parser/templates/index.html
rename to vlib/v/tests/tmpl/index.html
index c20d6a5638bd97..e2496f6fe00c1e 100644
--- a/vlib/v/parser/templates/index.html
+++ b/vlib/v/tests/tmpl/index.html
@@ -8,5 +8,5 @@
Line ending with slash /
Line ending with dollar $
Line ending with caret ^
-
+
Last line.
diff --git a/vlib/v/tests/tmpl/nested/child.html b/vlib/v/tests/tmpl/nested/child.html
new file mode 100644
index 00000000000000..ad015e14341067
--- /dev/null
+++ b/vlib/v/tests/tmpl/nested/child.html
@@ -0,0 +1,2 @@
+@include '../base'
+This is the child file
diff --git a/vlib/v/tests/tmpl/nested/nested_deeper/grandchild.html b/vlib/v/tests/tmpl/nested/nested_deeper/grandchild.html
new file mode 100644
index 00000000000000..117e2d585921c2
--- /dev/null
+++ b/vlib/v/tests/tmpl/nested/nested_deeper/grandchild.html
@@ -0,0 +1,3 @@
+@include '../../base'
+@include '../child'
+This is the grandchild file
diff --git a/vlib/v/tests/tmpl/parent.html b/vlib/v/tests/tmpl/parent.html
new file mode 100644
index 00000000000000..1a52eac2be1008
--- /dev/null
+++ b/vlib/v/tests/tmpl/parent.html
@@ -0,0 +1,4 @@
+@include 'base'
+@include 'nested/child'
+@include 'nested/nested_deeper/grandchild'
+This is the parent file
\ No newline at end of file
diff --git a/vlib/v/tests/tmpl_test.v b/vlib/v/tests/tmpl_test.v
index 9b3ff34223625a..a80f6cdecd4b9b 100644
--- a/vlib/v/tests/tmpl_test.v
+++ b/vlib/v/tests/tmpl_test.v
@@ -102,3 +102,45 @@ fn test_tmpl_interpolation() {
fn my_fn(s string) string {
return s
}
+
+// Add more examples of potentially buggy patterns in vlib/v/tests/tmpl/index.html
+fn test_tmpl_comptime() {
+ index := $tmpl('tmpl/index.html').trim_space()
+ // dump(index)
+ assert index.contains('
Line ending with percent %\n')
+ assert index.contains('
Line ending with at $\n')
+ assert index.contains('
Line ending with ampersand &\n')
+ assert index.contains('
Line ending with hash #\n')
+ assert index.contains('
Line ending with slash /\n')
+ assert index.contains('
Line ending with dollar $\n')
+ assert index.contains('
Line ending with caret ^\n')
+}
+
+// Add a tests for @include
+
+// File contents for building repsonse
+const base = 'This is the base file
'
+const child = 'This is the child file
'
+const grandchild = 'This is the grandchild file
'
+const parent = 'This is the parent file
'
+
+// Call the parent file which contains all child templates
+fn test_tmpl_include_parent() {
+ expected := [base, base, child, base, base, child, grandchild, parent].join('\n')
+ parent_tmpl := $tmpl('tmpl/parent.html')
+ assert parent_tmpl.contains(expected)
+}
+
+// Test the child template which calls parent template
+fn test_tmpl_include_child() {
+ expected := [base, child].join('\n')
+ child_tmpl := $tmpl('tmpl/nested/child.html')
+ assert child_tmpl.contains(expected)
+}
+
+// Test the grandchild templates calling both parent and grandparent templates
+fn test_tmpl_include_grandchild() {
+ expected := [base, base, child, grandchild].join('\n')
+ child_tmpl := $tmpl('tmpl/nested/nested_deeper/grandchild.html')
+ assert child_tmpl.contains(expected)
+}