diff --git a/packages/code-analyzer-flow-engine/FlowScanner/public/parse_utils.py b/packages/code-analyzer-flow-engine/FlowScanner/public/parse_utils.py index afaba974..7c06d28c 100644 --- a/packages/code-analyzer-flow-engine/FlowScanner/public/parse_utils.py +++ b/packages/code-analyzer-flow-engine/FlowScanner/public/parse_utils.py @@ -1415,19 +1415,30 @@ def quick_validate(flow_path: str) -> bool | None: has_start = False has_banned = False try: - with open(flow_path, 'r') as fp: - flow_data = fp.read() - for start_tag in START_ELEMS_TAGGED: - if start_tag in flow_data: - has_start = True - break - - for banned_tag in BANNED_ELEMS_TAGGED: - if banned_tag in flow_data: - has_banned = True - break - - return has_start and not has_banned + # Flow XML is authored/declared UTF-8, so read it as UTF-8 explicitly + # rather than relying on the platform locale encoding (e.g. cp1252 on + # Windows), which would raise UnicodeDecodeError on valid flows and + # cause them to be silently dropped. Fall back to cp1252 for legacy + # files, matching the pattern used in flow_scanner/__main__.py. + try: + with open(flow_path, 'r', encoding='utf-8') as fp: + flow_data = fp.read() + except UnicodeDecodeError: + # cp1252 is used on older Windows systems + with open(flow_path, 'r', encoding='cp1252') as fp: + flow_data = fp.read() + + for start_tag in START_ELEMS_TAGGED: + if start_tag in flow_data: + has_start = True + break + + for banned_tag in BANNED_ELEMS_TAGGED: + if banned_tag in flow_data: + has_banned = True + break + + return has_start and not has_banned except FileNotFoundError: logger.critical(f"Could not find file {flow_path}") diff --git a/packages/code-analyzer-flow-engine/FlowScanner/tests/test_parse_utils.py b/packages/code-analyzer-flow-engine/FlowScanner/tests/test_parse_utils.py new file mode 100644 index 00000000..eb7fc58e --- /dev/null +++ b/packages/code-analyzer-flow-engine/FlowScanner/tests/test_parse_utils.py @@ -0,0 +1,75 @@ +"""Tests for public.parse_utils. + +Run from the FlowScanner directory with:: + + python3 -m unittest tests.test_parse_utils +""" +import builtins +import os +import tempfile +import unittest + +import public.parse_utils as pu + + +class QuickValidateEncodingTest(unittest.TestCase): + """Regression tests for issue #2074. + + Flow XML is authored/declared UTF-8, but quick_validate historically opened + files using the platform locale encoding. On Windows (cp1252) any flow + containing a non-cp1252 UTF-8 byte (e.g. 0x9D from U+201D smart quote) + raised UnicodeDecodeError, the flow was silently dropped, and the scanner + ultimately failed to write its results file. + """ + + #: A valid flow whose description contains a right double quotation mark + #: (U+201D). This encodes to byte 0x9D in UTF-8's continuation, which maps + #: to in cp1252 and would raise UnicodeDecodeError. + UTF8_FLOW = ( + '\n' + '' + 'smart ”quote' + '' + '' + ) + + def setUp(self): + fd, self.flow_path = tempfile.mkstemp(suffix='.flow') + os.close(fd) + with open(self.flow_path, 'w', encoding='utf-8') as fp: + fp.write(self.UTF8_FLOW) + + def tearDown(self): + if os.path.exists(self.flow_path): + os.unlink(self.flow_path) + + def test_utf8_flow_validates_under_cp1252_locale(self): + """quick_validate must read UTF-8 flows even when the locale is cp1252.""" + orig_open = builtins.open + + def cp1252_default_open(file, mode='r', *args, **kwargs): + # Emulate a Windows cp1252 locale: text reads with no explicit + # encoding fall back to cp1252. + if 'b' not in mode and 'encoding' not in kwargs: + kwargs['encoding'] = 'cp1252' + return orig_open(file, mode, *args, **kwargs) + + builtins.open = cp1252_default_open + try: + result = pu.quick_validate(self.flow_path) + finally: + builtins.open = orig_open + + self.assertTrue(result) + + def test_utf8_flow_validates_under_utf8_locale(self): + """quick_validate still works under a normal UTF-8 locale.""" + self.assertTrue(pu.quick_validate(self.flow_path)) + + def test_missing_file_returns_none(self): + """A missing file still returns None (unchanged behavior).""" + self.assertIsNone(pu.quick_validate(self.flow_path + '.does-not-exist')) + + +if __name__ == '__main__': + unittest.main() diff --git a/packages/code-analyzer-flow-engine/package.json b/packages/code-analyzer-flow-engine/package.json index ec8b3659..9814f183 100644 --- a/packages/code-analyzer-flow-engine/package.json +++ b/packages/code-analyzer-flow-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-flow-engine", "description": "Plugin package that adds 'Flow Scanner' as an engine into Salesforce Code Analyzer", - "version": "0.40.0", + "version": "0.40.1-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",