Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v.util: fix stack overflow due to unlimited recursion in resolve_d_value #23895

Merged
merged 1 commit into from
Mar 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions vlib/v/util/check_dflags_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
module main

#flag -DCNUMBER1=$d('N',1231)
#flag -DCNUMBER2=$d('N', 1232)
#flag -DCNUMBER3=$d('N',1233 )
#flag -DCNUMBER4=$d('N', 1234 )
#flag -DCNUMBER5=$d('N', 1235 )
#flag -DCNUMBER6=$d('N',1236) ##

fn test_if_compilation_with_custom_cflags_works_numbers_simple() {
assert C.CNUMBER1 == 1231
assert C.CNUMBER2 == 1232
assert C.CNUMBER3 == 1233
assert C.CNUMBER4 == 1234
assert C.CNUMBER5 == 1235
assert C.CNUMBER6 == 1236
}

#flag -DCNUMBERS1=$d('N',123)+$d('N',123)
#flag -DCNUMBERS2=$d('N', 123)-$d('N', 123)
#flag -DCNUMBERS3=$d('N',123 )*$d('N',123 )
#flag -DCNUMBERS4=$d('N', 123 )/$d('N', 123 )
#flag -DCNUMBERS5=$d('N', 123 )+2*$d('N', 123 )
#flag -DCNUMBERS6=$d('N',123)+1000*$d('N',123) ##

fn test_if_compilation_with_custom_cflags_works_numbers_composed_arithmetic() {
assert C.CNUMBERS1 == 246
assert C.CNUMBERS2 == 0
assert C.CNUMBERS3 == 15129
assert C.CNUMBERS4 == 1
assert C.CNUMBERS5 == 369
assert C.CNUMBERS6 == 123123
}

#flag -DFNAME0=$d('A1','"printf"')
#flag -DFNAME1=$d('A1','"print')$d('A2','f"')
#flag -DFNAME2=$d('A1', 'print')$d('A2','f')
#flag -DFNAME3=$d('A1','prin' )$d('A2','tf')
#flag -DFNAME4=$d('A1', 'pri' )$d('A2','ntf')
#flag -DFNAME5=$d('A1', 'pr' )$d('A2','intf') ##

fn test_custom_flags_with_composed_strings() {
assert voidptr(C.FNAME0) == voidptr(C.printf)
assert voidptr(C.FNAME1) == voidptr(C.printf)
assert voidptr(C.FNAME2) == voidptr(C.printf)
assert voidptr(C.FNAME3) == voidptr(C.printf)
assert voidptr(C.FNAME4) == voidptr(C.printf)
assert voidptr(C.FNAME5) == voidptr(C.printf)
}

#flag -DCMIXED1=$d('A1','mixed')_$d('A2',1)
#flag -DCMIXED2=$d('A1', 'mixed')_$d('A2',2 )
#flag -DCMIXED3=$d('A1','mixed' )_$d('A2', 3)
#flag -DCMIXED4=$d('A1', 'mixed' )_$d('A2', 4 )
#flag -DCMIXED55=$d('A1', 'mixed' )_$d('A2',55) ##

@[export: 'mixed_1']
pub fn f1() {}

@[export: 'mixed_2']
pub fn f2() {}

@[export: 'mixed_3']
pub fn f3() {}

@[export: 'mixed_4']
pub fn f4() {}

@[export: 'mixed_55']
pub fn f55() {}

fn test_custom_flags_that_are_a_mix() {
assert voidptr(C.CMIXED1) == voidptr(C.mixed_1)
assert voidptr(C.CMIXED2) == voidptr(C.mixed_2)
assert voidptr(C.CMIXED3) == voidptr(C.mixed_3)
assert voidptr(C.CMIXED4) == voidptr(C.mixed_4)
assert voidptr(C.CMIXED55) == voidptr(C.mixed_55)
}
106 changes: 106 additions & 0 deletions vlib/v/util/d_value.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) 2025 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module util

const d_sig = "\$d('"

// resolve_d_value replaces all occurrences of `$d('ident','value')`
// in `str` with either the default `'value'` param or a compile value passed via `-d ident=value`.
pub fn resolve_d_value(compile_values map[string]string, str string) !string {
start := str.index(d_sig) or { return error('no "${d_sig}...\')" could be found in "${str}"') }
mut i := 0
mut ch := u8(`.`)
mut bd_ident := []u8{cap: 20}
mut blevel := 1
for i = start + d_sig.len; i < str.len && ch != `'`; i++ {
ch = str[i]
if ch == `)` {
blevel--
} else if ch == `(` {
blevel++
}
if ch.is_letter() || ch.is_digit() || ch == `_` {
bd_ident << ch
} else {
if !(ch == `'`) {
if ch == `$` {
return error('cannot use string interpolation in compile time \$d() expression')
}
return error('invalid `\$d` identifier in "${str}", invalid character `${rune(ch)}`')
}
}
}
d_ident := bd_ident.bytestr().trim_space()
if d_ident == '' {
return error('first argument of `\$d` must be a string identifier')
}

// At this point we should have a valid identifier in `d_ident`.
// Next we parse out the default string value.

// Advance past the `,` and the opening `'` in second argument, or ... eat whatever is there:
for ; i < str.len; i++ {
ch = str[i]
match ch {
` `, `,` {
continue
}
`'` {
i++
}
else {}
}
break
}
// Rinse, repeat for the expected default value string
ch = `.`
dv_start := i
mut dv_end := i
for i < str.len {
ch = str[i]
dv_end++
i++
match ch {
`'` {
break
}
`(` {
blevel++
}
`)` {
blevel--
if blevel <= 0 {
break
}
}
`$` {
return error('cannot use string interpolation in compile time \$d() expression')
}
else {}
}
}
if dv_end - dv_start == 0 {
return error('second argument of `\$d` must be a pure literal')
}
for ; blevel > 0 && i < str.len; i++ {
if str[i] == `)` {
i++
break
}
}
d_default_value := str#[dv_start..dv_end - 1].trim_space() // last character is the closing `)`
// at this point we have the identifier and the default value.
// now we need to resolve which one to use from `compile_values`.
d_value := compile_values[d_ident] or { d_default_value }
original_expr_to_be_replaced := str#[start..i]
if original_expr_to_be_replaced[original_expr_to_be_replaced.len - 1] != `)` {
panic('the last character of `${original_expr_to_be_replaced}` should be `)`')
}
rep := str.replace_once(original_expr_to_be_replaced, d_value)
if original_expr_to_be_replaced.len > 0 && rep.contains(d_sig) {
// if more `$d()` calls remains, resolve those as well:
return resolve_d_value(compile_values, rep)
}
return rep
}
50 changes: 50 additions & 0 deletions vlib/v/util/env_value.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2025 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module util

import os

// resolve_env_value replaces all occurrences of `$env('ENV_VAR_NAME')`
// in `str` with the value of the env variable `$ENV_VAR_NAME`.
pub fn resolve_env_value(str string, check_for_presence bool) !string {
env_ident := "\$env('"
at := str.index(env_ident) or {
return error('no "${env_ident}' + '...\')" could be found in "${str}".')
}
mut ch := u8(`.`)
mut benv_lit := []u8{cap: 20}
for i := at + env_ident.len; i < str.len && ch != `)`; i++ {
ch = u8(str[i])
if ch.is_letter() || ch.is_digit() || ch == `_` {
benv_lit << ch
} else {
if !(ch == `'` || ch == `)`) {
if ch == `$` {
return error('cannot use string interpolation in compile time \$env() expression')
}
return error('invalid environment variable name in "${str}", invalid character "${rune(ch)}"')
}
}
}
env_lit := benv_lit.bytestr()
if env_lit == '' {
return error('supply an env variable name like HOME, PATH or USER')
}
mut env_value := ''
if check_for_presence {
env_value = os.environ()[env_lit] or {
return error('the environment variable "${env_lit}" does not exist.')
}
if env_value == '' {
return error('the environment variable "${env_lit}" is empty.')
}
} else {
env_value = os.getenv(env_lit)
}
rep := str.replace_once(env_ident + env_lit + "'" + ')', env_value)
if rep.contains(env_ident) {
return resolve_env_value(rep, check_for_presence)
}
return rep
}
131 changes: 0 additions & 131 deletions vlib/v/util/util.v
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import term
import rand
import time
import v.pref
import v.vmod
import v.util.recompilation
import v.util.vflags
import runtime
Expand Down Expand Up @@ -76,136 +75,6 @@ pub fn set_vroot_folder(vroot_path string) {
os.setenv('VCHILD', 'true', true)
}

// resolve_vmodroot replaces all occurences of `@VMODROOT` in `str`, with an absolute path,
// formed by resolving, where the nearest `v.mod` is, given the folder `dir`.
pub fn resolve_vmodroot(str string, dir string) !string {
mut mcache := vmod.get_cache()
vmod_file_location := mcache.get_by_folder(dir)
if vmod_file_location.vmod_file.len == 0 {
// There was no actual v.mod file found.
return error('To use @VMODROOT, you need to have a "v.mod" file in ${dir}, or in one of its parent folders.')
}
vmod_path := vmod_file_location.vmod_folder
return str.replace('@VMODROOT', os.real_path(vmod_path))
}

// resolve_env_value replaces all occurrences of `$env('ENV_VAR_NAME')`
// in `str` with the value of the env variable `$ENV_VAR_NAME`.
pub fn resolve_env_value(str string, check_for_presence bool) !string {
env_ident := "\$env('"
at := str.index(env_ident) or {
return error('no "${env_ident}' + '...\')" could be found in "${str}".')
}
mut ch := u8(`.`)
mut env_lit := ''
for i := at + env_ident.len; i < str.len && ch != `)`; i++ {
ch = u8(str[i])
if ch.is_letter() || ch.is_digit() || ch == `_` {
env_lit += ch.ascii_str()
} else {
if !(ch == `'` || ch == `)`) {
if ch == `$` {
return error('cannot use string interpolation in compile time \$env() expression')
}
return error('invalid environment variable name in "${str}", invalid character "${ch.ascii_str()}"')
}
}
}
if env_lit == '' {
return error('supply an env variable name like HOME, PATH or USER')
}
mut env_value := ''
if check_for_presence {
env_value = os.environ()[env_lit] or {
return error('the environment variable "${env_lit}" does not exist.')
}
if env_value == '' {
return error('the environment variable "${env_lit}" is empty.')
}
} else {
env_value = os.getenv(env_lit)
}
rep := str.replace_once(env_ident + env_lit + "'" + ')', env_value)
if rep.contains(env_ident) {
return resolve_env_value(rep, check_for_presence)
}
return rep
}

const d_sig = "\$d('"

// resolve_d_value replaces all occurrences of `$d('ident','value')`
// in `str` with either the default `'value'` param or a compile value passed via `-d ident=value`.
pub fn resolve_d_value(compile_values map[string]string, str string) !string {
at := str.index(d_sig) or {
return error('no "${d_sig}' + '...\')" could be found in "${str}".')
}
mut all_parsed := d_sig
mut ch := u8(`.`)
mut d_ident := ''
mut i := 0
for i = at + d_sig.len; i < str.len && ch != `'`; i++ {
ch = u8(str[i])
all_parsed += ch.ascii_str()
if ch.is_letter() || ch.is_digit() || ch == `_` {
d_ident += ch.ascii_str()
} else {
if !(ch == `'`) {
if ch == `$` {
return error('cannot use string interpolation in compile time \$d() expression')
}
return error('invalid `\$d` identifier in "${str}", invalid character "${ch.ascii_str()}"')
}
}
}
if d_ident == '' {
return error('first argument of `\$d` must be a string identifier')
}

// at this point we should have a valid identifier in `d_ident`.
// Next we parse out the default string value

// advance past the `,` and the opening `'` in second argument, or ... eat whatever is there
for i < str.len {
ch = str[i]
if ch in [` `, `,`] {
i++
all_parsed += u8(ch).ascii_str()
continue
}
if ch == `'` {
i++
all_parsed += u8(ch).ascii_str()
}
break
}
// Rinse, repeat for the expected default value string
ch = u8(`.`)
mut d_default_value := ''
for ; i < str.len && ch != `'`; i++ {
ch = u8(str[i])
all_parsed += ch.ascii_str()
if !(ch == `'`) {
d_default_value += ch.ascii_str()
}
if ch == `$` {
return error('cannot use string interpolation in compile time \$d() expression')
}
}
if d_default_value == '' {
return error('second argument of `\$d` must be a pure literal')
}
// at this point we have the identifier and the default value.
// now we need to resolve which one to use from `compile_values`.
d_value := compile_values[d_ident] or { d_default_value }
// if more `$d()` calls remains, resolve those as well:
rep := str.replace_once(all_parsed + ')', d_value)
if rep.contains(d_sig) {
return resolve_d_value(compile_values, rep)
}
return rep
}

// is_escape_sequence returns `true` if `c` is considered a valid escape sequence denoter.
@[inline]
pub fn is_escape_sequence(c u8) bool {
Expand Down
Loading
Loading