145 lines
5.1 KiB
Python
145 lines
5.1 KiB
Python
# file: scripts/_rust_audit_common.py
|
|
# version: 1
|
|
|
|
"""Small shared lexical and file-selection helpers for read-only Rust audits."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
|
|
_IGNORED = frozenset({".git", ".venv", "__pycache__", "target", "builds"})
|
|
|
|
|
|
def selected_files(raw_paths: list[str]) -> tuple[list[pathlib.Path], list[str]]:
|
|
"""Select explicit Rust files and directories, without following generated trees."""
|
|
|
|
files: set[pathlib.Path] = set()
|
|
errors: list[str] = []
|
|
for raw in raw_paths:
|
|
path = pathlib.Path(raw)
|
|
if path.is_dir():
|
|
files.update(
|
|
candidate
|
|
for candidate in path.rglob("*.rs")
|
|
if candidate.is_file() and not (set(candidate.parts) & _IGNORED)
|
|
)
|
|
elif path.is_file() and path.suffix == ".rs":
|
|
files.add(path)
|
|
else:
|
|
errors.append(f"unsupported or missing Rust path: {path}")
|
|
return sorted(files), errors
|
|
|
|
|
|
def mask_source(text: str) -> tuple[str, set[int]]:
|
|
"""Mask comments and literals, preserving offsets and structural punctuation.
|
|
|
|
Return line numbers whose first character is inside a multiline literal, so
|
|
an empty line in a raw string cannot be mistaken for an empty code line.
|
|
This is a lexical helper, not a full Rust parser or a compiler substitute.
|
|
"""
|
|
|
|
output = list(text)
|
|
protected: set[int] = set()
|
|
index = 0
|
|
line = 1
|
|
state = "code"
|
|
block_depth = 0
|
|
raw_hashes = 0
|
|
while index < len(text):
|
|
char = text[index]
|
|
nxt = text[index + 1] if index + 1 < len(text) else ""
|
|
if char == "\n":
|
|
line += 1
|
|
if state in {"string", "raw_string"}:
|
|
protected.add(line)
|
|
if state == "line_comment":
|
|
state = "code"
|
|
index += 1
|
|
continue
|
|
if state == "code":
|
|
if char == "/" and nxt in {"/", "*"}:
|
|
output[index:index + 2] = [" ", " "]
|
|
state = "line_comment" if nxt == "/" else "block_comment"
|
|
block_depth = 1 if nxt == "*" else 0
|
|
index += 2
|
|
continue
|
|
if char == '"':
|
|
output[index] = " "
|
|
state = "string"
|
|
index += 1
|
|
continue
|
|
if char == "'":
|
|
# Lifetimes have no nearby closing quote; char literals do.
|
|
probe = index + 1
|
|
escaped = False
|
|
found = False
|
|
while probe < min(len(text), index + 8) and text[probe] != "\n":
|
|
if text[probe] == "'" and not escaped:
|
|
found = True
|
|
break
|
|
escaped = text[probe] == "\\" and not escaped
|
|
probe += 1
|
|
if found:
|
|
output[index] = " "
|
|
state = "char"
|
|
index += 1
|
|
continue
|
|
# Also covers r#"..."#, br#"..."# and cr#"..."#.
|
|
if char == "r" and (index == 0 or text[index - 1] not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"):
|
|
probe = index + 1
|
|
while probe < len(text) and text[probe] == "#":
|
|
probe += 1
|
|
if probe < len(text) and text[probe] == '"':
|
|
output[index:probe + 1] = [" "] * (probe + 1 - index)
|
|
raw_hashes = probe - index - 1
|
|
state = "raw_string"
|
|
index = probe + 1
|
|
continue
|
|
index += 1
|
|
continue
|
|
if state == "line_comment":
|
|
output[index] = " "
|
|
elif state == "block_comment":
|
|
output[index] = " "
|
|
if char == "/" and nxt == "*":
|
|
output[index + 1] = " "
|
|
block_depth += 1
|
|
index += 2
|
|
continue
|
|
if char == "*" and nxt == "/":
|
|
output[index + 1] = " "
|
|
block_depth -= 1
|
|
if block_depth == 0:
|
|
state = "code"
|
|
index += 2
|
|
continue
|
|
elif state in {"string", "char"}:
|
|
output[index] = " "
|
|
if char == "\\" and index + 1 < len(text):
|
|
if text[index + 1] != "\n":
|
|
output[index + 1] = " "
|
|
index += 2
|
|
continue
|
|
if char == ('"' if state == "string" else "'"):
|
|
state = "code"
|
|
elif state == "raw_string":
|
|
output[index] = " "
|
|
if char == '"' and text.startswith("#" * raw_hashes, index + 1):
|
|
output[index + 1:index + 1 + raw_hashes] = [" "] * raw_hashes
|
|
index += 1 + raw_hashes
|
|
state = "code"
|
|
continue
|
|
index += 1
|
|
return "".join(output), protected
|
|
|
|
|
|
def line_depths(masked: str) -> list[int]:
|
|
"""Return brace depth at the start of each line, excluding literals."""
|
|
|
|
depths: list[int] = []
|
|
depth = 0
|
|
for line in masked.splitlines():
|
|
depths.append(depth)
|
|
depth = max(0, depth + line.count("{") - line.count("}"))
|
|
return depths
|