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

T5996: selectively escape and restore single backslashes in config (backport #3035) #3334

Merged
merged 2 commits into from
Apr 20, 2024
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
24 changes: 20 additions & 4 deletions python/vyos/configtree.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,22 @@

LIBPATH = '/usr/lib/libvyosconfig.so.0'

def replace_backslash(s, search, replace):
"""Modify quoted strings containing backslashes not of escape sequences"""
def replace_method(match):
result = match.group().replace(search, replace)
return result
p = re.compile(r'("[^"]*[\\][^"]*"\n|\'[^\']*[\\][^\']*\'\n)')
return p.sub(replace_method, s)

def escape_backslash(string: str) -> str:
"""Escape single backslashes in string that are not in escape sequence"""
p = re.compile(r'(?<!\\)[\\](?!b|f|n|r|t|\\[^bfnrt])')
result = p.sub(r'\\\\', string)
"""Escape single backslashes in quoted strings"""
result = replace_backslash(string, '\\', '\\\\')
return result

def unescape_backslash(string: str) -> str:
"""Unescape backslashes in quoted strings"""
result = replace_backslash(string, '\\\\', '\\')
return result

def extract_version(s):
Expand Down Expand Up @@ -165,11 +177,14 @@ def get_version_string(self):

def to_string(self, ordered_values=False):
config_string = self.__to_string(self.__config, ordered_values).decode()
config_string = unescape_backslash(config_string)
config_string = "{0}\n{1}".format(config_string, self.__version)
return config_string

def to_commands(self, op="set"):
return self.__to_commands(self.__config, op.encode()).decode()
commands = self.__to_commands(self.__config, op.encode()).decode()
commands = unescape_backslash(commands)
return commands

def to_json(self):
return self.__to_json(self.__config).decode()
Expand Down Expand Up @@ -362,6 +377,7 @@ def show_diff(left, right, path=[], commands=False, libpath=LIBPATH):
msg = __get_error().decode()
raise ConfigTreeError(msg)

res = unescape_backslash(res)
return res

def union(left, right, libpath=LIBPATH):
Expand Down
68 changes: 68 additions & 0 deletions smoketest/scripts/cli/test_backslash_escape.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
#
# Copyright (C) 2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import unittest

from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configtree import ConfigTree

base_path = ['interfaces', 'ethernet', 'eth0', 'description']

cases_word = [r'fo\o', r'fo\\o', r'foço\o', r'foço\\o']
# legacy CLI output quotes only if whitespace present; this is a notable
# difference that confounds the translation legacy -> modern, hence
# determines the regex used in function replace_backslash
cases_phrase = [r'some fo\o', r'some fo\\o', r'some foço\o', r'some foço\\o']

case_save_config = '/tmp/smoketest-case-save'

class TestBackslashEscape(VyOSUnitTestSHIM.TestCase):
def test_backslash_escape_word(self):
for case in cases_word:
self.cli_set(base_path + [case])
self.cli_commit()
# save_config tests translation though subsystems:
# legacy output -> config -> configtree -> file
self._session.save_config(case_save_config)
# reload to configtree and confirm:
with open(case_save_config) as f:
config_string = f.read()
ct = ConfigTree(config_string)
res = ct.return_value(base_path)
self.assertEqual(case, res, msg=res)
print(f'description: {res}')
self.cli_delete(base_path)
self.cli_commit()

def test_backslash_escape_phrase(self):
for case in cases_phrase:
self.cli_set(base_path + [case])
self.cli_commit()
# save_config tests translation though subsystems:
# legacy output -> config -> configtree -> file
self._session.save_config(case_save_config)
# reload to configtree and confirm:
with open(case_save_config) as f:
config_string = f.read()
ct = ConfigTree(config_string)
res = ct.return_value(base_path)
self.assertEqual(case, res, msg=res)
print(f'description: {res}')
self.cli_delete(base_path)
self.cli_commit()

if __name__ == '__main__':
unittest.main(verbosity=2)
Loading