Skip to content

Commit

Permalink
configure_file: update \@ escape logic
Browse files Browse the repository at this point in the history
Update meson logic to only handle matched pairs of \@ when
escaping variables in 'meson' and 'cmake@' style configure_file
invocations.

The previous approach, using a regex to handle a single  \@
to escape a variable has undesirable side effects,
including mangling valid perl that needs to be configured.

Closes: mesonbuild#7165
  • Loading branch information
Kangie committed Jun 6, 2024
1 parent dfd22db commit 1f4624f
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 26 deletions.
39 changes: 24 additions & 15 deletions mesonbuild/utils/universal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1182,24 +1182,21 @@ def do_replacement(regex: T.Pattern[str], line: str,
variable_format: Literal['meson', 'cmake', 'cmake@'],
confdata: T.Union[T.Dict[str, T.Tuple[str, T.Optional[str]]], 'ConfigurationData']) -> T.Tuple[str, T.Set[str]]:
missing_variables: T.Set[str] = set()
if variable_format == 'cmake':
start_tag = '${'
backslash_tag = '\\${'
else:
start_tag = '@'
backslash_tag = '\\@'

def variable_replace(match: T.Match[str]) -> str:
# Pairs of escape characters before '@' or '\@'
# Pairs of escape characters before '@', '\@', '${' or '\${'
if match.group(0).endswith('\\'):
num_escapes = match.end(0) - match.start(0)
return '\\' * (num_escapes // 2)
# Single escape character and '@'
elif match.group(0) == backslash_tag:
return start_tag
# Template variable to be replaced
# Handle cmake escaped \${} tags
elif variable_format == 'cmake' and match.group(0) == '\\${':
return '${'
# \@escaped\@ variables
elif match.groupdict().get('escaped') is not None:
return match.group('escaped')[1:-2]+'@'
else:
varname = match.group(1)
# Template variable to be replaced
varname = match.group('variable')
var_str = ''
if varname in confdata:
var, _ = confdata.get(varname)
Expand Down Expand Up @@ -1280,11 +1277,23 @@ def get_cmake_define(line: str, confdata: 'ConfigurationData') -> str:

def get_variable_regex(variable_format: Literal['meson', 'cmake', 'cmake@'] = 'meson') -> T.Pattern[str]:
# Only allow (a-z, A-Z, 0-9, _, -) as valid characters for a define
# Also allow escaping '@' with '\@'
if variable_format in {'meson', 'cmake@'}:
regex = re.compile(r'(?:\\\\)+(?=\\?@)|\\@|@([-a-zA-Z0-9_]+)@')
# Also allow escaping pairs of '@' with '\@'
regex = re.compile(r'''
(?:\\\\)+(?=\\?@) # Matches multiple backslashes followed by an @ symbol
| # OR
@(?P<variable>[-a-zA-Z0-9_]+)@ # Matches a variable enclosed in @ symbols and captures the variable name
| # OR
(?P<escaped>\\@[-a-zA-Z0-9_]+\\@) # Matches an escaped variable enclosed in @ symbols
''', re.VERBOSE)
else:
regex = re.compile(r'(?:\\\\)+(?=\\?\$)|\\\${|\${([-a-zA-Z0-9_]+)}')
regex = re.compile(r'''
(?:\\\\)+(?=\\?\$) # Matches multiple backslashes followed by a dollar sign
| # OR
\\\${ # Matches a backslash followed by a dollar sign and an opening curly brace
| # OR
\${(?P<variable>[-a-zA-Z0-9_]+)} # Matches a variable enclosed in curly braces and captures the variable name
''', re.VERBOSE)
return regex

def do_conf_str(src: str, data: T.List[str], confdata: 'ConfigurationData',
Expand Down
13 changes: 5 additions & 8 deletions test cases/common/14 configure file/config6.h.in
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
/* No escape */
#define MESSAGE1 "@var1@"

/* Single escape means no replace */
#define MESSAGE2 "\@var1@"
/* Escaped whole variable */
#define MESSAGE2 "\\@var1\\@"

/* Replace pairs of escapes before '@' or '\@' with escape characters
* (note we have to double number of pairs due to C string escaping)
*/
#define MESSAGE3 "\\\\@var1@"

/* Pairs of escapes and then single escape to avoid replace */
#define MESSAGE4 "\\\\\@var1@"

/* Check escaped variable does not overlap following variable */
#define MESSAGE5 "\@var1@var2@"
/* Pairs of escapes and then an escaped variable */
#define MESSAGE4 "\\\\\@var1\@"

/* Check escape character outside variables */
#define MESSAGE6 "\\ @ \@ \\\\@ \\\\\@"
#define MESSAGE5 "\\ @ \\\\@"
8 changes: 8 additions & 0 deletions test cases/common/14 configure file/dummy-perl.pl.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env perl

use strict;
use warnings;

my @array = qw(1 2);
use Data::Dumper;
say Dumper(\@array);
9 changes: 9 additions & 0 deletions test cases/common/14 configure file/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,15 @@ configure_file(
configuration: conf
)

# Do not mangle (e.g.) perl files when input legitimately contains '\@'
configure_file(
input: 'dummy-perl.pl.in',
output: 'dummy-perl.pl',
configuration: conf,
)
run_command('cmp', files('dummy-perl.pl.in'),
join_paths(meson.current_build_dir(), 'dummy-perl.pl'), check: true)

test('configure-file', test_file)

# Dictionaries
Expand Down
10 changes: 8 additions & 2 deletions test cases/common/14 configure file/prog6.c
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
#include <string.h>
#include <config6.h>
#include <stdio.h>

int main(void) {
fprintf(stderr, "MESSAGE1 %s\n", MESSAGE1);
fprintf(stderr, "MESSAGE2 %s\n", MESSAGE2);
fprintf(stderr, "MESSAGE3 %s\n", MESSAGE3);
fprintf(stderr, "MESSAGE4 %s\n", MESSAGE4);
fprintf(stderr, "MESSAGE5 %s\n", MESSAGE5);

return strcmp(MESSAGE1, "foo")
|| strcmp(MESSAGE2, "@var1@")
|| strcmp(MESSAGE3, "\\foo")
|| strcmp(MESSAGE4, "\\@var1@")
|| strcmp(MESSAGE5, "@var1bar")
|| strcmp(MESSAGE6, "\\ @ @ \\@ \\@");
|| strcmp(MESSAGE5, "\\ @ \\@");
}
2 changes: 1 addition & 1 deletion test cases/common/14 configure file/subdir/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ configure_file(
input: '../test.py.in',
output: 'no_write_conflict.txt',
configuration: conf
)
)

0 comments on commit 1f4624f

Please sign in to comment.