From 50b716b8e354f305d9672915e17b9c24a7181993 Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Mon, 10 Mar 2025 00:36:15 +0200 Subject: [PATCH] v.util: fix stack overflow during parsing of `#flag -DName=$d(...)` (#23895) --- vlib/v/util/check_dflags_test.v | 78 +++++++++++++++++++ vlib/v/util/d_value.v | 106 ++++++++++++++++++++++++++ vlib/v/util/env_value.v | 50 ++++++++++++ vlib/v/util/util.v | 131 -------------------------------- vlib/v/util/vmod_value.v | 20 +++++ 5 files changed, 254 insertions(+), 131 deletions(-) create mode 100644 vlib/v/util/check_dflags_test.v create mode 100644 vlib/v/util/d_value.v create mode 100644 vlib/v/util/env_value.v create mode 100644 vlib/v/util/vmod_value.v diff --git a/vlib/v/util/check_dflags_test.v b/vlib/v/util/check_dflags_test.v new file mode 100644 index 000000000..c3340fc98 --- /dev/null +++ b/vlib/v/util/check_dflags_test.v @@ -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) +} diff --git a/vlib/v/util/d_value.v b/vlib/v/util/d_value.v new file mode 100644 index 000000000..2b5a7c1b4 --- /dev/null +++ b/vlib/v/util/d_value.v @@ -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 +} diff --git a/vlib/v/util/env_value.v b/vlib/v/util/env_value.v new file mode 100644 index 000000000..9c5eb34e3 --- /dev/null +++ b/vlib/v/util/env_value.v @@ -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 +} diff --git a/vlib/v/util/util.v b/vlib/v/util/util.v index 59ed7bae5..1bd894a63 100644 --- a/vlib/v/util/util.v +++ b/vlib/v/util/util.v @@ -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 @@ -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 { diff --git a/vlib/v/util/vmod_value.v b/vlib/v/util/vmod_value.v new file mode 100644 index 000000000..e02c7b485 --- /dev/null +++ b/vlib/v/util/vmod_value.v @@ -0,0 +1,20 @@ +// 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 +import v.vmod + +// 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)) +} -- 2.39.5