1
0

check_stdlib_usage.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. #!/usr/bin/env python3
  2. #
  3. # Simple DirectMedia Layer
  4. # Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
  5. #
  6. # This software is provided 'as-is', without any express or implied
  7. # warranty. In no event will the authors be held liable for any damages
  8. # arising from the use of this software.
  9. #
  10. # Permission is granted to anyone to use this software for any purpose,
  11. # including commercial applications, and to alter it and redistribute it
  12. # freely, subject to the following restrictions:
  13. #
  14. # 1. The origin of this software must not be misrepresented; you must not
  15. # claim that you wrote the original software. If you use this software
  16. # in a product, an acknowledgment in the product documentation would be
  17. # appreciated but is not required.
  18. # 2. Altered source versions must be plainly marked as such, and must not be
  19. # misrepresented as being the original software.
  20. # 3. This notice may not be removed or altered from any source distribution.
  21. #
  22. # This script detects use of stdlib function in SDL code
  23. import argparse
  24. import os
  25. import pathlib
  26. import re
  27. import sys
  28. SDL_ROOT = pathlib.Path(__file__).resolve().parents[1]
  29. STDLIB_SDL_SYMBOLS = set((
  30. 'abs',
  31. 'acos',
  32. 'acosf',
  33. 'asin',
  34. 'asinf',
  35. 'asprintf',
  36. 'atan',
  37. 'atan2',
  38. 'atan2f',
  39. 'atanf',
  40. 'atof',
  41. 'atoi',
  42. 'bsearch',
  43. 'calloc',
  44. 'ceil',
  45. 'ceilf',
  46. 'copysign',
  47. 'copysignf',
  48. 'cos',
  49. 'cosf',
  50. 'crc32',
  51. 'exp',
  52. 'expf',
  53. 'fabs',
  54. 'fabsf',
  55. 'floor',
  56. 'floorf',
  57. 'fmod',
  58. 'fmodf',
  59. 'free',
  60. 'getenv',
  61. 'isalnum',
  62. 'isalpha',
  63. 'isblank',
  64. 'iscntrl',
  65. 'isdigit',
  66. 'isgraph',
  67. 'islower',
  68. 'isprint',
  69. 'ispunct',
  70. 'isspace',
  71. 'isupper',
  72. 'isxdigit',
  73. 'itoa',
  74. 'lltoa',
  75. 'log10',
  76. 'log10f',
  77. 'logf',
  78. 'lround',
  79. 'lroundf',
  80. 'ltoa',
  81. 'malloc',
  82. 'memalign',
  83. 'memcmp',
  84. 'memcpy',
  85. 'memcpy4',
  86. 'memmove',
  87. 'memset',
  88. 'pow',
  89. 'powf',
  90. 'qsort',
  91. 'qsort_r',
  92. 'qsort_s',
  93. 'realloc',
  94. 'round',
  95. 'roundf',
  96. 'scalbn',
  97. 'scalbnf',
  98. 'setenv',
  99. 'sin',
  100. 'sinf',
  101. 'snprintf',
  102. 'sqrt',
  103. 'sqrtf',
  104. 'sscanf',
  105. 'strcasecmp',
  106. 'strchr',
  107. 'strcmp',
  108. 'strdup',
  109. 'strlcat',
  110. 'strlcpy',
  111. 'strlen',
  112. 'strlwr',
  113. 'strncasecmp',
  114. 'strncmp',
  115. 'strrchr',
  116. 'strrev',
  117. 'strstr',
  118. 'strtod',
  119. 'strtokr',
  120. 'strtol',
  121. 'strtoll',
  122. 'strtoul',
  123. 'strtoull',
  124. 'strupr',
  125. 'tan',
  126. 'tanf',
  127. 'tolower',
  128. 'toupper',
  129. 'trunc',
  130. 'truncf',
  131. 'uitoa',
  132. 'ulltoa',
  133. 'ultoa',
  134. 'utf8strlcpy',
  135. 'utf8strlen',
  136. 'vasprintf',
  137. 'vsnprintf',
  138. 'vsscanf',
  139. 'wcscasecmp',
  140. 'wcscmp',
  141. 'wcsdup',
  142. 'wcslcat',
  143. 'wcslcpy',
  144. 'wcslen',
  145. 'wcsncasecmp',
  146. 'wcsncmp',
  147. 'wcsstr',
  148. 'wcstol',
  149. 'wcstoll',
  150. 'wcstoul',
  151. 'wcstoull',
  152. ))
  153. UNSAFE_STDLIB_SYMBOLS = set((
  154. 'putc',
  155. 'puts',
  156. 'printf',
  157. 'sprintf',
  158. 'vprintf',
  159. ))
  160. RE_STDLIB_SYMBOL = re.compile(rf"(?<!->)\b(?P<symbol>{'|'.join(STDLIB_SDL_SYMBOLS.union(UNSAFE_STDLIB_SYMBOLS))})\b\(")
  161. EXCLUDED_PATHS = (
  162. "src/core/windows/gameinput/gameinput.cpp",
  163. "src/stdlib",
  164. "src/libm",
  165. "src/hidapi",
  166. "src/video/khronos",
  167. "src/video/miniz.h",
  168. "src/video/stb_image.h",
  169. "include/SDL3",
  170. "build-scripts/gen_audio_resampler_filter.c",
  171. "build-scripts/gen_audio_channel_conversion.c",
  172. "test/win32/sdlprocdump.c",
  173. )
  174. def find_symbols_in_file(file: pathlib.Path, apply_exclude_paths: bool) -> int:
  175. match_count = 0
  176. allowed_extensions = [ ".c", ".cpp", ".m", ".h", ".hpp", ".cc" ]
  177. filename = pathlib.Path(file)
  178. if apply_exclude_paths:
  179. for ep in EXCLUDED_PATHS:
  180. if ep in filename.as_posix():
  181. # skip
  182. return 0
  183. if filename.suffix not in allowed_extensions:
  184. # skip
  185. return 0
  186. # print("Parse %s" % file)
  187. try:
  188. with file.open("r", encoding="UTF-8", newline="") as rfp:
  189. parsing_comment = False
  190. for line_i, original_line in enumerate(rfp, start=1):
  191. line = original_line.strip()
  192. line_comment = ""
  193. # Get the comment block /* ... */ across several lines
  194. while True:
  195. if parsing_comment:
  196. pos_end_comment = line.find("*/")
  197. if pos_end_comment >= 0:
  198. line = line[pos_end_comment+2:]
  199. parsing_comment = False
  200. else:
  201. break
  202. else:
  203. pos_start_comment = line.find("/*")
  204. if pos_start_comment >= 0:
  205. pos_end_comment = line.find("*/", pos_start_comment+2)
  206. if pos_end_comment >= 0:
  207. line_comment += line[pos_start_comment:pos_end_comment+2]
  208. line = line[:pos_start_comment] + line[pos_end_comment+2:]
  209. else:
  210. line_comment += line[pos_start_comment:]
  211. line = line[:pos_start_comment]
  212. parsing_comment = True
  213. break
  214. else:
  215. break
  216. if parsing_comment:
  217. continue
  218. pos_line_comment = line.find("//")
  219. if pos_line_comment >= 0:
  220. line_comment += line[pos_line_comment:]
  221. line = line[:pos_line_comment]
  222. if matches := tuple(RE_STDLIB_SYMBOL.finditer(line)):
  223. first_quote = line.find("\"")
  224. last_quote = line.rfind("\"")
  225. first_occurrence = min(m.span()[0] for m in matches)
  226. last_occurrence = max(m.span()[1] for m in matches)
  227. if first_quote == -1 or not (first_quote < first_occurrence and last_quote > last_occurrence):
  228. override_string = " or ".join(f"SDL_{m.group(1)}" for m in matches if m.group(1) in STDLIB_SDL_SYMBOLS)
  229. if override_string:
  230. override_string = f"This should NOT be {override_string}"
  231. if any(m for m in matches if m.group(1) in UNSAFE_STDLIB_SYMBOLS):
  232. if override_string:
  233. override_string += ". "
  234. override_string += "Allow unsafe stdlib"
  235. assert override_string
  236. if override_string not in line_comment:
  237. print(f"{filename}:{line_i}")
  238. print(f" {line}")
  239. print(f"")
  240. match_count += 1
  241. except UnicodeDecodeError:
  242. print(f"{file} is not text, skipping", file=sys.stderr)
  243. return match_count
  244. def find_symbols_in_dir(path: pathlib.Path, apply_exclude_paths: bool) -> int:
  245. match_count = 0
  246. for entry in path.iterdir():
  247. if entry.is_dir():
  248. match_count += find_symbols_in_dir(entry, apply_exclude_paths=apply_exclude_paths)
  249. else:
  250. match_count += find_symbols_in_file(entry, apply_exclude_paths=apply_exclude_paths)
  251. return match_count
  252. def main():
  253. parser = argparse.ArgumentParser(fromfile_prefix_chars="@")
  254. parser.add_argument("paths", default=[SDL_ROOT / "src", SDL_ROOT / "test"], nargs="*", type=pathlib.Path, help="Paths to look for stdlib symbols")
  255. parser.add_argument("--no-exclude", action="store_false", dest="apply_exclude_paths", help="Don't apply exclude paths")
  256. args = parser.parse_args()
  257. print(f"Looking for stdlib usage in {', '.join(str(p) for p in args.paths)}...")
  258. match_count = 0
  259. for path in args.paths:
  260. if path.is_file():
  261. match_count = find_symbols_in_file(path, apply_exclude_paths=args.apply_exclude_paths)
  262. else:
  263. match_count = find_symbols_in_dir(path, apply_exclude_paths=args.apply_exclude_paths)
  264. if match_count:
  265. print("If the stdlib usage is intentional, add a '// This should NOT be SDL_<symbol>()' line comment.")
  266. print("If there is no equivalent SDL function, add a '// Allow unsafe stdlib' line comment.")
  267. print("")
  268. print("NOT OK")
  269. else:
  270. print("OK")
  271. return 1 if match_count else 0
  272. if __name__ == "__main__":
  273. raise SystemExit(main())