""" Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 """ import logging from json.decoder import JSONDecodeError from typing import Callable, List, Optional, Tuple, Union from yaml import YAMLError from yaml.parser import ParserError from yaml.scanner import ScannerError from cfnlint.decode import cfn_json, cfn_yaml from cfnlint.match import Match LOGGER = logging.getLogger(__name__) Matches = List[Match] Decode = Tuple[Union[str, None], Matches] def decode_str(s: str) -> Decode: """Decode the string s into an object.""" return _decode(cfn_yaml.loads, cfn_json.loads, s, None) def decode(filename: str) -> Decode: """Decode filename into an object.""" return _decode(cfn_yaml.load, cfn_json.load, filename, filename) def _decode( yaml_f: Callable, json_f: Callable, payload: str, filename: Optional[str] ) -> Decode: """Decode payload using yaml_f and json_f, using filename for log output.""" template = None matches = [] try: template = yaml_f(payload) except IOError as e: if e.errno == 2: LOGGER.error("Template file not found: %s", filename) matches.append( create_match_file_error( filename, f"Template file not found: {filename}" ) ) elif e.errno == 21: LOGGER.error("Template references a directory, not a file: %s", filename) matches.append( create_match_file_error( filename, f"Template references a directory, not a file: {filename}" ) ) elif e.errno == 13: LOGGER.error("Permission denied when accessing template file: %s", filename) matches.append( create_match_file_error( filename, f"Permission denied when accessing template file: {filename}", ) ) if matches: return (None, matches) except UnicodeDecodeError as _: LOGGER.error("Cannot read file contents: %s", filename) matches.append( create_match_file_error(filename, "Cannot read file contents: {filename}") ) except cfn_yaml.CfnParseError as err: matches = err.matches except ParserError as err: matches = [create_match_yaml_parser_error(err, filename)] except ScannerError as err: if err.problem and ( err.problem in [ "found character '\\t' that cannot start any token", "found unknown escape character", ] or err.problem.startswith("found unknown escape character") ): try: template = json_f(payload) except cfn_json.JSONDecodeError as json_errs: for json_err in json_errs.matches: json_err.filename = filename matches = json_errs.matches except JSONDecodeError as json_err: if hasattr(json_err, "msg"): if json_err.msg in [ "No JSON object could be decoded", "Expecting value", ]: # pylint: disable=no-member matches = [create_match_yaml_parser_error(err, filename)] else: matches = [create_match_json_parser_error(json_err, filename)] except Exception as json_err: # pylint: disable=W0703 LOGGER.error("Template %s is malformed: %s", filename, err.problem) LOGGER.error( "Tried to parse %s as JSON but got error: %s", filename, str(json_err), ) return ( None, [ create_match_file_error( filename, f"Tried to parse {filename} as JSON but got error: {str(json_err)}", ) ], ) else: matches = [create_match_yaml_parser_error(err, filename)] except YAMLError as err: matches = [create_match_file_error(filename, str(err))] if not isinstance(template, dict) and not matches: # pylint: disable=import-outside-toplevel from cfnlint.rules import ParseError # Template isn't a dict which means nearly nothing will work matches = [ Match( 1, 1, 1, 1, filename, ParseError(), message="Template needs to be an object.", ) ] return (template, matches) def create_match_yaml_parser_error(parser_error, filename): """Create a Match for a parser error""" # pylint: disable=import-outside-toplevel from cfnlint.rules import ParseError lineno = parser_error.problem_mark.line + 1 colno = parser_error.problem_mark.column + 1 msg = parser_error.problem return Match(lineno, colno, lineno, colno + 1, filename, ParseError(), message=msg) def create_match_file_error(filename, msg): """Create a Match for a parser error""" # pylint: disable=import-outside-toplevel from cfnlint.rules import ParseError return Match( linenumber=1, columnnumber=1, linenumberend=1, columnnumberend=2, filename=filename, rule=ParseError(), message=msg, ) def create_match_json_parser_error(parser_error, filename): """Create a Match for a parser error""" # pylint: disable=import-outside-toplevel from cfnlint.rules import ParseError lineno = parser_error.lineno colno = parser_error.colno msg = parser_error.msg return Match(lineno, colno, lineno, colno + 1, filename, ParseError(), message=msg)