Skip to content

Commit 50b716b

Browse files
authored
v.util: fix stack overflow during parsing of #flag -DName=$d(...) (#23895)
1 parent 23c5bc7 commit 50b716b

File tree

5 files changed

+254
-131
lines changed

5 files changed

+254
-131
lines changed

vlib/v/util/check_dflags_test.v

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
module main
2+
3+
#flag -DCNUMBER1=$d('N',1231)
4+
#flag -DCNUMBER2=$d('N', 1232)
5+
#flag -DCNUMBER3=$d('N',1233 )
6+
#flag -DCNUMBER4=$d('N', 1234 )
7+
#flag -DCNUMBER5=$d('N', 1235 )
8+
#flag -DCNUMBER6=$d('N',1236) ##
9+
10+
fn test_if_compilation_with_custom_cflags_works_numbers_simple() {
11+
assert C.CNUMBER1 == 1231
12+
assert C.CNUMBER2 == 1232
13+
assert C.CNUMBER3 == 1233
14+
assert C.CNUMBER4 == 1234
15+
assert C.CNUMBER5 == 1235
16+
assert C.CNUMBER6 == 1236
17+
}
18+
19+
#flag -DCNUMBERS1=$d('N',123)+$d('N',123)
20+
#flag -DCNUMBERS2=$d('N', 123)-$d('N', 123)
21+
#flag -DCNUMBERS3=$d('N',123 )*$d('N',123 )
22+
#flag -DCNUMBERS4=$d('N', 123 )/$d('N', 123 )
23+
#flag -DCNUMBERS5=$d('N', 123 )+2*$d('N', 123 )
24+
#flag -DCNUMBERS6=$d('N',123)+1000*$d('N',123) ##
25+
26+
fn test_if_compilation_with_custom_cflags_works_numbers_composed_arithmetic() {
27+
assert C.CNUMBERS1 == 246
28+
assert C.CNUMBERS2 == 0
29+
assert C.CNUMBERS3 == 15129
30+
assert C.CNUMBERS4 == 1
31+
assert C.CNUMBERS5 == 369
32+
assert C.CNUMBERS6 == 123123
33+
}
34+
35+
#flag -DFNAME0=$d('A1','"printf"')
36+
#flag -DFNAME1=$d('A1','"print')$d('A2','f"')
37+
#flag -DFNAME2=$d('A1', 'print')$d('A2','f')
38+
#flag -DFNAME3=$d('A1','prin' )$d('A2','tf')
39+
#flag -DFNAME4=$d('A1', 'pri' )$d('A2','ntf')
40+
#flag -DFNAME5=$d('A1', 'pr' )$d('A2','intf') ##
41+
42+
fn test_custom_flags_with_composed_strings() {
43+
assert voidptr(C.FNAME0) == voidptr(C.printf)
44+
assert voidptr(C.FNAME1) == voidptr(C.printf)
45+
assert voidptr(C.FNAME2) == voidptr(C.printf)
46+
assert voidptr(C.FNAME3) == voidptr(C.printf)
47+
assert voidptr(C.FNAME4) == voidptr(C.printf)
48+
assert voidptr(C.FNAME5) == voidptr(C.printf)
49+
}
50+
51+
#flag -DCMIXED1=$d('A1','mixed')_$d('A2',1)
52+
#flag -DCMIXED2=$d('A1', 'mixed')_$d('A2',2 )
53+
#flag -DCMIXED3=$d('A1','mixed' )_$d('A2', 3)
54+
#flag -DCMIXED4=$d('A1', 'mixed' )_$d('A2', 4 )
55+
#flag -DCMIXED55=$d('A1', 'mixed' )_$d('A2',55) ##
56+
57+
@[export: 'mixed_1']
58+
pub fn f1() {}
59+
60+
@[export: 'mixed_2']
61+
pub fn f2() {}
62+
63+
@[export: 'mixed_3']
64+
pub fn f3() {}
65+
66+
@[export: 'mixed_4']
67+
pub fn f4() {}
68+
69+
@[export: 'mixed_55']
70+
pub fn f55() {}
71+
72+
fn test_custom_flags_that_are_a_mix() {
73+
assert voidptr(C.CMIXED1) == voidptr(C.mixed_1)
74+
assert voidptr(C.CMIXED2) == voidptr(C.mixed_2)
75+
assert voidptr(C.CMIXED3) == voidptr(C.mixed_3)
76+
assert voidptr(C.CMIXED4) == voidptr(C.mixed_4)
77+
assert voidptr(C.CMIXED55) == voidptr(C.mixed_55)
78+
}

vlib/v/util/d_value.v

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) 2025 Alexander Medvednikov. All rights reserved.
2+
// Use of this source code is governed by an MIT license
3+
// that can be found in the LICENSE file.
4+
module util
5+
6+
const d_sig = "\$d('"
7+
8+
// resolve_d_value replaces all occurrences of `$d('ident','value')`
9+
// in `str` with either the default `'value'` param or a compile value passed via `-d ident=value`.
10+
pub fn resolve_d_value(compile_values map[string]string, str string) !string {
11+
start := str.index(d_sig) or { return error('no "${d_sig}...\')" could be found in "${str}"') }
12+
mut i := 0
13+
mut ch := u8(`.`)
14+
mut bd_ident := []u8{cap: 20}
15+
mut blevel := 1
16+
for i = start + d_sig.len; i < str.len && ch != `'`; i++ {
17+
ch = str[i]
18+
if ch == `)` {
19+
blevel--
20+
} else if ch == `(` {
21+
blevel++
22+
}
23+
if ch.is_letter() || ch.is_digit() || ch == `_` {
24+
bd_ident << ch
25+
} else {
26+
if !(ch == `'`) {
27+
if ch == `$` {
28+
return error('cannot use string interpolation in compile time \$d() expression')
29+
}
30+
return error('invalid `\$d` identifier in "${str}", invalid character `${rune(ch)}`')
31+
}
32+
}
33+
}
34+
d_ident := bd_ident.bytestr().trim_space()
35+
if d_ident == '' {
36+
return error('first argument of `\$d` must be a string identifier')
37+
}
38+
39+
// At this point we should have a valid identifier in `d_ident`.
40+
// Next we parse out the default string value.
41+
42+
// Advance past the `,` and the opening `'` in second argument, or ... eat whatever is there:
43+
for ; i < str.len; i++ {
44+
ch = str[i]
45+
match ch {
46+
` `, `,` {
47+
continue
48+
}
49+
`'` {
50+
i++
51+
}
52+
else {}
53+
}
54+
break
55+
}
56+
// Rinse, repeat for the expected default value string
57+
ch = `.`
58+
dv_start := i
59+
mut dv_end := i
60+
for i < str.len {
61+
ch = str[i]
62+
dv_end++
63+
i++
64+
match ch {
65+
`'` {
66+
break
67+
}
68+
`(` {
69+
blevel++
70+
}
71+
`)` {
72+
blevel--
73+
if blevel <= 0 {
74+
break
75+
}
76+
}
77+
`$` {
78+
return error('cannot use string interpolation in compile time \$d() expression')
79+
}
80+
else {}
81+
}
82+
}
83+
if dv_end - dv_start == 0 {
84+
return error('second argument of `\$d` must be a pure literal')
85+
}
86+
for ; blevel > 0 && i < str.len; i++ {
87+
if str[i] == `)` {
88+
i++
89+
break
90+
}
91+
}
92+
d_default_value := str#[dv_start..dv_end - 1].trim_space() // last character is the closing `)`
93+
// at this point we have the identifier and the default value.
94+
// now we need to resolve which one to use from `compile_values`.
95+
d_value := compile_values[d_ident] or { d_default_value }
96+
original_expr_to_be_replaced := str#[start..i]
97+
if original_expr_to_be_replaced[original_expr_to_be_replaced.len - 1] != `)` {
98+
panic('the last character of `${original_expr_to_be_replaced}` should be `)`')
99+
}
100+
rep := str.replace_once(original_expr_to_be_replaced, d_value)
101+
if original_expr_to_be_replaced.len > 0 && rep.contains(d_sig) {
102+
// if more `$d()` calls remains, resolve those as well:
103+
return resolve_d_value(compile_values, rep)
104+
}
105+
return rep
106+
}

vlib/v/util/env_value.v

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) 2025 Alexander Medvednikov. All rights reserved.
2+
// Use of this source code is governed by an MIT license
3+
// that can be found in the LICENSE file.
4+
module util
5+
6+
import os
7+
8+
// resolve_env_value replaces all occurrences of `$env('ENV_VAR_NAME')`
9+
// in `str` with the value of the env variable `$ENV_VAR_NAME`.
10+
pub fn resolve_env_value(str string, check_for_presence bool) !string {
11+
env_ident := "\$env('"
12+
at := str.index(env_ident) or {
13+
return error('no "${env_ident}' + '...\')" could be found in "${str}".')
14+
}
15+
mut ch := u8(`.`)
16+
mut benv_lit := []u8{cap: 20}
17+
for i := at + env_ident.len; i < str.len && ch != `)`; i++ {
18+
ch = u8(str[i])
19+
if ch.is_letter() || ch.is_digit() || ch == `_` {
20+
benv_lit << ch
21+
} else {
22+
if !(ch == `'` || ch == `)`) {
23+
if ch == `$` {
24+
return error('cannot use string interpolation in compile time \$env() expression')
25+
}
26+
return error('invalid environment variable name in "${str}", invalid character "${rune(ch)}"')
27+
}
28+
}
29+
}
30+
env_lit := benv_lit.bytestr()
31+
if env_lit == '' {
32+
return error('supply an env variable name like HOME, PATH or USER')
33+
}
34+
mut env_value := ''
35+
if check_for_presence {
36+
env_value = os.environ()[env_lit] or {
37+
return error('the environment variable "${env_lit}" does not exist.')
38+
}
39+
if env_value == '' {
40+
return error('the environment variable "${env_lit}" is empty.')
41+
}
42+
} else {
43+
env_value = os.getenv(env_lit)
44+
}
45+
rep := str.replace_once(env_ident + env_lit + "'" + ')', env_value)
46+
if rep.contains(env_ident) {
47+
return resolve_env_value(rep, check_for_presence)
48+
}
49+
return rep
50+
}

vlib/v/util/util.v

-131
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import term
99
import rand
1010
import time
1111
import v.pref
12-
import v.vmod
1312
import v.util.recompilation
1413
import v.util.vflags
1514
import runtime
@@ -76,136 +75,6 @@ pub fn set_vroot_folder(vroot_path string) {
7675
os.setenv('VCHILD', 'true', true)
7776
}
7877

79-
// resolve_vmodroot replaces all occurences of `@VMODROOT` in `str`, with an absolute path,
80-
// formed by resolving, where the nearest `v.mod` is, given the folder `dir`.
81-
pub fn resolve_vmodroot(str string, dir string) !string {
82-
mut mcache := vmod.get_cache()
83-
vmod_file_location := mcache.get_by_folder(dir)
84-
if vmod_file_location.vmod_file.len == 0 {
85-
// There was no actual v.mod file found.
86-
return error('To use @VMODROOT, you need to have a "v.mod" file in ${dir}, or in one of its parent folders.')
87-
}
88-
vmod_path := vmod_file_location.vmod_folder
89-
return str.replace('@VMODROOT', os.real_path(vmod_path))
90-
}
91-
92-
// resolve_env_value replaces all occurrences of `$env('ENV_VAR_NAME')`
93-
// in `str` with the value of the env variable `$ENV_VAR_NAME`.
94-
pub fn resolve_env_value(str string, check_for_presence bool) !string {
95-
env_ident := "\$env('"
96-
at := str.index(env_ident) or {
97-
return error('no "${env_ident}' + '...\')" could be found in "${str}".')
98-
}
99-
mut ch := u8(`.`)
100-
mut env_lit := ''
101-
for i := at + env_ident.len; i < str.len && ch != `)`; i++ {
102-
ch = u8(str[i])
103-
if ch.is_letter() || ch.is_digit() || ch == `_` {
104-
env_lit += ch.ascii_str()
105-
} else {
106-
if !(ch == `'` || ch == `)`) {
107-
if ch == `$` {
108-
return error('cannot use string interpolation in compile time \$env() expression')
109-
}
110-
return error('invalid environment variable name in "${str}", invalid character "${ch.ascii_str()}"')
111-
}
112-
}
113-
}
114-
if env_lit == '' {
115-
return error('supply an env variable name like HOME, PATH or USER')
116-
}
117-
mut env_value := ''
118-
if check_for_presence {
119-
env_value = os.environ()[env_lit] or {
120-
return error('the environment variable "${env_lit}" does not exist.')
121-
}
122-
if env_value == '' {
123-
return error('the environment variable "${env_lit}" is empty.')
124-
}
125-
} else {
126-
env_value = os.getenv(env_lit)
127-
}
128-
rep := str.replace_once(env_ident + env_lit + "'" + ')', env_value)
129-
if rep.contains(env_ident) {
130-
return resolve_env_value(rep, check_for_presence)
131-
}
132-
return rep
133-
}
134-
135-
const d_sig = "\$d('"
136-
137-
// resolve_d_value replaces all occurrences of `$d('ident','value')`
138-
// in `str` with either the default `'value'` param or a compile value passed via `-d ident=value`.
139-
pub fn resolve_d_value(compile_values map[string]string, str string) !string {
140-
at := str.index(d_sig) or {
141-
return error('no "${d_sig}' + '...\')" could be found in "${str}".')
142-
}
143-
mut all_parsed := d_sig
144-
mut ch := u8(`.`)
145-
mut d_ident := ''
146-
mut i := 0
147-
for i = at + d_sig.len; i < str.len && ch != `'`; i++ {
148-
ch = u8(str[i])
149-
all_parsed += ch.ascii_str()
150-
if ch.is_letter() || ch.is_digit() || ch == `_` {
151-
d_ident += ch.ascii_str()
152-
} else {
153-
if !(ch == `'`) {
154-
if ch == `$` {
155-
return error('cannot use string interpolation in compile time \$d() expression')
156-
}
157-
return error('invalid `\$d` identifier in "${str}", invalid character "${ch.ascii_str()}"')
158-
}
159-
}
160-
}
161-
if d_ident == '' {
162-
return error('first argument of `\$d` must be a string identifier')
163-
}
164-
165-
// at this point we should have a valid identifier in `d_ident`.
166-
// Next we parse out the default string value
167-
168-
// advance past the `,` and the opening `'` in second argument, or ... eat whatever is there
169-
for i < str.len {
170-
ch = str[i]
171-
if ch in [` `, `,`] {
172-
i++
173-
all_parsed += u8(ch).ascii_str()
174-
continue
175-
}
176-
if ch == `'` {
177-
i++
178-
all_parsed += u8(ch).ascii_str()
179-
}
180-
break
181-
}
182-
// Rinse, repeat for the expected default value string
183-
ch = u8(`.`)
184-
mut d_default_value := ''
185-
for ; i < str.len && ch != `'`; i++ {
186-
ch = u8(str[i])
187-
all_parsed += ch.ascii_str()
188-
if !(ch == `'`) {
189-
d_default_value += ch.ascii_str()
190-
}
191-
if ch == `$` {
192-
return error('cannot use string interpolation in compile time \$d() expression')
193-
}
194-
}
195-
if d_default_value == '' {
196-
return error('second argument of `\$d` must be a pure literal')
197-
}
198-
// at this point we have the identifier and the default value.
199-
// now we need to resolve which one to use from `compile_values`.
200-
d_value := compile_values[d_ident] or { d_default_value }
201-
// if more `$d()` calls remains, resolve those as well:
202-
rep := str.replace_once(all_parsed + ')', d_value)
203-
if rep.contains(d_sig) {
204-
return resolve_d_value(compile_values, rep)
205-
}
206-
return rep
207-
}
208-
20978
// is_escape_sequence returns `true` if `c` is considered a valid escape sequence denoter.
21079
@[inline]
21180
pub fn is_escape_sequence(c u8) bool {

0 commit comments

Comments
 (0)