util.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. """
  2. Utility module.
  3. This module provides utility functions used across the coverage check scripts.
  4. """
  5. import os
  6. import sys
  7. import re
  8. import shlex
  9. import subprocess
  10. import traceback
  11. from typing import List, Tuple, Dict, Any, Optional, Union
  12. # List of allowed commands and their arguments
  13. ALLOWED_COMMANDS = {
  14. 'xvfb-run': ['-a'],
  15. 'npm': ['run', 'test:coverage', 'ci', 'install', '--no-save', '@vitest/coverage-v8', 'check-types', 'lint', 'format', 'compile'],
  16. 'cd': ['webview-ui'],
  17. 'python': ['-m', 'coverage_check'],
  18. 'git': ['fetch', 'checkout', 'origin'],
  19. }
  20. def is_safe_command(command: Union[str, List[str]]) -> bool:
  21. """
  22. Check if a command is safe to execute.
  23. Args:
  24. command: Command to check (string or list)
  25. Returns:
  26. True if command is safe, False otherwise
  27. """
  28. # Convert string command to list
  29. if isinstance(command, str):
  30. try:
  31. cmd_parts = shlex.split(command)
  32. except ValueError:
  33. return False
  34. else:
  35. cmd_parts = command
  36. if not cmd_parts:
  37. return False
  38. # Get base command
  39. base_cmd = os.path.basename(cmd_parts[0])
  40. # Check if command is in allowed list
  41. if base_cmd not in ALLOWED_COMMANDS:
  42. return False
  43. # For each argument, check for suspicious patterns
  44. for arg in cmd_parts[1:]:
  45. # Check for shell metacharacters
  46. if re.search(r'[;&|`$]', arg):
  47. return False
  48. # Check for path traversal
  49. if '..' in arg and not (base_cmd == 'npm' and arg.startswith('@')):
  50. return False
  51. return True
  52. def log(message: str) -> None:
  53. """
  54. Write a message to stdout and flush.
  55. Args:
  56. message: The message to write
  57. """
  58. sys.stdout.write(f"{message}\n")
  59. sys.stdout.flush()
  60. def file_exists(file_path: str) -> bool:
  61. """
  62. Check if a file exists.
  63. Args:
  64. file_path: Path to the file
  65. Returns:
  66. True if the file exists, False otherwise
  67. """
  68. return os.path.exists(file_path) and os.path.isfile(file_path)
  69. def get_file_size(file_path: str) -> int:
  70. """
  71. Get the size of a file in bytes.
  72. Args:
  73. file_path: Path to the file
  74. Returns:
  75. Size of the file in bytes, or 0 if the file doesn't exist
  76. """
  77. if file_exists(file_path):
  78. return os.path.getsize(file_path)
  79. return 0
  80. def list_directory(dir_path: str) -> List[Tuple[str, Union[int, str]]]:
  81. """
  82. List the contents of a directory.
  83. Args:
  84. dir_path: Path to the directory
  85. Returns:
  86. List of (name, size) tuples for each file/directory in the directory
  87. """
  88. if not os.path.exists(dir_path) or not os.path.isdir(dir_path):
  89. return []
  90. contents = []
  91. for item in os.listdir(dir_path):
  92. item_path = os.path.join(dir_path, item)
  93. if os.path.isfile(item_path):
  94. contents.append((item, os.path.getsize(item_path)))
  95. else:
  96. contents.append((item, "DIR"))
  97. return contents
  98. def read_file_content(file_path: str, default: str = "") -> str:
  99. """
  100. Read file content with error handling.
  101. Args:
  102. file_path: Path to the file
  103. default: Default value to return if file cannot be read
  104. Returns:
  105. File content or default value
  106. """
  107. if not file_exists(file_path):
  108. log(f"File does not exist: {file_path}")
  109. return default
  110. try:
  111. with open(file_path, 'r') as f:
  112. return f.read()
  113. except Exception as e:
  114. log(f"Error reading file {file_path}: {e}")
  115. return default
  116. def write_file_content(file_path: str, content: str) -> bool:
  117. """
  118. Write content to file with error handling.
  119. Args:
  120. file_path: Path to the file
  121. content: Content to write
  122. Returns:
  123. True if successful, False otherwise
  124. """
  125. try:
  126. # Create directory if it doesn't exist
  127. os.makedirs(os.path.dirname(file_path), exist_ok=True)
  128. with open(file_path, 'w') as f:
  129. f.write(content)
  130. return True
  131. except Exception as e:
  132. log(f"Error writing to file {file_path}: {e}")
  133. return False
  134. def run_command(command: Union[str, List[str]], capture_output: bool = True) -> Tuple[int, str, str]:
  135. """
  136. Run a command and return the result.
  137. Args:
  138. command: Command to run (string or list)
  139. capture_output: Whether to capture stdout/stderr
  140. Returns:
  141. Tuple of (returncode, stdout, stderr)
  142. """
  143. if not is_safe_command(command):
  144. error_msg = f"Unsafe command detected: {command}"
  145. log(error_msg)
  146. return 1, "", error_msg
  147. log(f"Running command: {command}")
  148. try:
  149. # Convert string command to list
  150. if isinstance(command, str):
  151. cmd_list = shlex.split(command)
  152. else:
  153. cmd_list = command
  154. result = subprocess.run(
  155. cmd_list,
  156. shell=False, # Never use shell=True for security
  157. capture_output=capture_output,
  158. text=True
  159. )
  160. log(f"Command exit code: {result.returncode}")
  161. return result.returncode, result.stdout, result.stderr
  162. except Exception as e:
  163. log(f"Error running command: {e}")
  164. log(traceback.format_exc())
  165. return 1, "", str(e)
  166. def find_pattern(content: str, pattern: str, group: int = 0,
  167. default: Optional[str] = None) -> Optional[str]:
  168. """
  169. Find a pattern in content and return the specified group.
  170. Args:
  171. content: Text content to search
  172. pattern: Regex pattern to search for
  173. group: Group number to return (default: 0 for entire match)
  174. default: Default value to return if pattern not found
  175. Returns:
  176. Matched text or default value
  177. """
  178. match = re.search(pattern, content, re.DOTALL)
  179. if match:
  180. return match.group(group)
  181. return default
  182. def get_env_var(name: str, default: Optional[str] = None) -> Optional[str]:
  183. """
  184. Get environment variable with default value.
  185. Args:
  186. name: Environment variable name
  187. default: Default value if not set
  188. Returns:
  189. Environment variable value or default
  190. """
  191. return os.environ.get(name, default)
  192. def format_exception(e: Exception) -> str:
  193. """
  194. Format an exception with traceback for logging.
  195. Args:
  196. e: Exception to format
  197. Returns:
  198. Formatted exception string
  199. """
  200. return f"{type(e).__name__}: {str(e)}\n{traceback.format_exc()}"