check_elf_log_strings.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. #!/usr/bin/env python
  2. # Copyright 2024 Google LLC
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import re
  16. import sys
  17. import argparse
  18. from elftools.elf.elffile import ELFFile
  19. from newlogging import get_log_dict_from_file
  20. FORMAT_SPECIFIER_REGEX = (r"(?P<flags>[-+ #0])?(?P<width>\*|\d*)?(?P<precision>\.\d+|\.\*)?"
  21. "(?P<length>hh|h|l|ll|j|z|t|L)?(?P<specifier>[diuoxXfFeEgGaAcspn%])")
  22. FORMAT_SPECIFIER_PATTERN = re.compile(FORMAT_SPECIFIER_REGEX)
  23. FLOAT_SPECIFIERS = "fFeEgGaA"
  24. LENGTH_64 = ['j', 'll', 'L'] # 'l', 'z', and 't' sizes confirmed to be 32 bits in logging.c
  25. LOG_LEVELS = [0, 1, 50, 100, 200, 255]
  26. def check_elf_log_strings(filename):
  27. """ Top Level API """
  28. log_dict = get_log_dict_from_file(filename)
  29. if not log_dict:
  30. return False, ['Unable to get log strings']
  31. return check_dict_log_strings(log_dict)
  32. def check_dict_log_strings(log_dict):
  33. """ Return complete error string rather than raise an exception on the first. """
  34. output = []
  35. for log_line in log_dict.itervalues():
  36. # Skip build_id and new_logging_version keys
  37. if 'file' not in log_line:
  38. continue
  39. file_line = ':'.join(log_line[x] for x in ['file', 'line', 'msg'])
  40. # Make sure that 'level' is being generated correctly
  41. if 'level' in log_line:
  42. if not log_line['level'].isdigit():
  43. output.append("'{}' PBL_LOG contains a non-constant LOG_LEVEL_ value '{}'".
  44. format(file_line, log_line['level']))
  45. break
  46. elif int(log_line['level']) not in LOG_LEVELS:
  47. output.append("'{}' PBL_LOG contains a non-constant LOG_LEVEL_ value '{}'".
  48. format(file_line, log_line['level']))
  49. break
  50. # Make sure that '`' isn't anywhere in the string
  51. if '`' in log_line['msg']:
  52. output.append("'{}' PBL_LOG contains '`'".format(file_line))
  53. # Now check the fmt string rules:
  54. # To ensure that we capture every %, find the '%' chars and then match on the remaining
  55. # string until we're done
  56. offset = 0
  57. num_conversions = 0
  58. num_str_conversions = 0
  59. while True:
  60. offset = log_line['msg'].find('%', offset)
  61. if offset == -1:
  62. break
  63. # Match starting immediately after the '%'
  64. match = FORMAT_SPECIFIER_PATTERN.match(log_line['msg'][offset + 1:])
  65. if not match:
  66. output.append("'{}' PBL_LOG contains unknown format specifier".format(file_line))
  67. break
  68. num_conversions += 1
  69. # RULE: no % literals.
  70. if match.group('specifier') == '%':
  71. output.append("'{}' PBL_LOG contains '%%'".format(file_line))
  72. break
  73. # RULE: no 64 bit values.
  74. if match.group('length') in LENGTH_64:
  75. output.append("'{}' PBL_LOG contains 64 bit value".format(file_line))
  76. break
  77. # RULE: no floats. VarArgs promotes to 64 bits, so this won't work, either
  78. if match.group('specifier') in FLOAT_SPECIFIERS:
  79. output.append("'{}' PBL_LOG contains floating point specifier".
  80. format(file_line))
  81. break
  82. # RULE: no flagged or formatted string conversions
  83. if match.group('specifier') == 's':
  84. num_str_conversions += 1
  85. if match.group('flags') or match.group('width') or match.group('precision'):
  86. output.append("'{}' PBL_LOG contains a formatted string conversion".
  87. format(file_line))
  88. break
  89. # RULE: no dynamic width specifiers. I.e., no * or .* widths. '.*' is already covered
  90. # above -- .* specifies precision for floats and # chars for strings. * remains.
  91. if '*' in match.group('width'):
  92. output.append("'{}' PBL_LOG contains a dynamic width".format(file_line))
  93. break
  94. # Consume this match by updating our offset
  95. for text in match.groups():
  96. if text:
  97. offset += len(text)
  98. # RULE: maximum of 7 format conversions
  99. if num_conversions > 7:
  100. output.append("'{}' PBL_LOG contains more than 7 format conversions".format(file_line))
  101. # RULE: maximum of 2 string conversions
  102. if num_str_conversions > 2:
  103. output.append("'{}' PBL_LOG contains more than 2 string conversions".format(file_line))
  104. if output:
  105. output.insert(0, 'NewLogging String Error{}:'.format('s' if len(output) > 1 else ''))
  106. output.append("See https://pebbletechnology.atlassian.net/wiki/display/DEV/New+Logging "
  107. "for help")
  108. return '\n'.join(output)
  109. if __name__ == '__main__':
  110. parser = argparse.ArgumentParser(description='Check .elf log strings for errors')
  111. parser.add_argument('elf_path', help='path to tintin_fw.elf to check')
  112. args = parser.parse_args()
  113. output = check_elf_log_strings(args.elf_path)
  114. if output:
  115. print(output)
  116. sys.exit(1)