Add HW 6
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"""HW6 library rules; inspect student source without importing it.
|
||||
|
||||
The general shared-state checker is passed in by the embedded Rust wrapper so
|
||||
its import resolution is shared without changing the policy of other homework.
|
||||
This checks common compression APIs, not arbitrary compression algorithms.
|
||||
"""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
COMPRESSION_APIS = {
|
||||
"zlib": {"compress", "compressobj", "decompress", "decompressobj"},
|
||||
"gzip": {"compress", "decompress", "open", "GzipFile"},
|
||||
"bz2": {"compress", "decompress", "open", "BZ2File", "BZ2Compressor", "BZ2Decompressor"},
|
||||
"lzma": {"compress", "decompress", "open", "LZMAFile", "LZMACompressor", "LZMADecompressor"},
|
||||
}
|
||||
|
||||
|
||||
def _check_file(tree, current, root, checker):
|
||||
violations = set()
|
||||
pending = checker._local_imports(tree, current, root)
|
||||
modules, functions = checker._dynamic_import_names(tree)
|
||||
strings = checker._static_string_values(tree)
|
||||
symbols = {}
|
||||
|
||||
def report(node, message):
|
||||
violations.add(f"{current}:{node.lineno}:{node.col_offset + 1}: {message}")
|
||||
|
||||
def check_module(name, node, base=root):
|
||||
parts = name.split(".")
|
||||
local = checker._candidate_module(base, parts)
|
||||
# Inspect package initializers as well as the explicitly imported file.
|
||||
for index in range(1, len(parts) + 1):
|
||||
candidate = checker._candidate_module(base, parts[:index])
|
||||
if candidate is not None:
|
||||
pending.add(candidate)
|
||||
namespace = base.joinpath(*parts).resolve()
|
||||
local_namespace = namespace.is_relative_to(root) and namespace.is_dir()
|
||||
if local is None and not local_namespace and parts[0] not in sys.stdlib_module_names and parts[0] != "anysystem":
|
||||
report(node, f"library '{name}' is not allowed: use AnySystem and the Python standard library")
|
||||
|
||||
def dynamic_name(node):
|
||||
argument = node.args[0] if node.args else next(
|
||||
(item.value for item in node.keywords if item.arg == "name"), None
|
||||
)
|
||||
return checker._static_string(argument, strings) if argument is not None else None
|
||||
|
||||
def symbol(node):
|
||||
if isinstance(node, ast.Name):
|
||||
return symbols.get(node.id)
|
||||
if isinstance(node, ast.Call) and checker._is_dynamic_import_call(node, modules, functions):
|
||||
return dynamic_name(node)
|
||||
resolved = checker._resolved_attribute(node)
|
||||
if resolved is not None:
|
||||
parent = symbol(resolved[0])
|
||||
if parent is not None:
|
||||
return f"{parent}.{resolved[1]}"
|
||||
return None
|
||||
|
||||
def check_compression(name, node):
|
||||
parts = name.split(".")
|
||||
if len(parts) >= 2 and parts[1] in COMPRESSION_APIS.get(parts[0], set()):
|
||||
report(node, f"message compression is forbidden in HW6: '{parts[0]}.{parts[1]}'")
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
check_module(alias.name, node)
|
||||
symbols[alias.asname or alias.name.split(".")[0]] = alias.name if alias.asname else alias.name.split(".")[0]
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
base = root
|
||||
if node.level:
|
||||
base = current.parent
|
||||
for _ in range(node.level - 1):
|
||||
base = base.parent
|
||||
if not base.is_relative_to(root):
|
||||
report(node, "local imports must stay inside the solution directory")
|
||||
continue
|
||||
if node.module:
|
||||
check_module(node.module, node, base)
|
||||
for alias in node.names:
|
||||
name = f"{node.module}.{alias.name}" if node.module else alias.name
|
||||
if checker._candidate_module(base, name.split(".")) is not None:
|
||||
check_module(name, node, base)
|
||||
if alias.name == "*" and node.module in COMPRESSION_APIS:
|
||||
report(node, f"message compression is forbidden in HW6: star import from '{node.module}'")
|
||||
else:
|
||||
check_compression(name, node)
|
||||
symbols[alias.asname or alias.name] = name
|
||||
elif isinstance(node, ast.Call) and checker._is_dynamic_import_call(node, modules, functions):
|
||||
name = dynamic_name(node)
|
||||
if name is None or name.startswith("."):
|
||||
report(node, "dynamic imports must use a statically known absolute module name")
|
||||
else:
|
||||
check_module(name, node)
|
||||
|
||||
# Follow aliases, including compressor functions saved for later use.
|
||||
assignments = [parts for node in ast.walk(tree) if (parts := checker._assignment_parts(node)) is not None]
|
||||
for _ in range(len(assignments) + 1):
|
||||
changed = False
|
||||
for value, targets in assignments:
|
||||
name = symbol(value)
|
||||
if name is None:
|
||||
continue
|
||||
for target in targets:
|
||||
if isinstance(target, ast.Name) and target.id not in symbols:
|
||||
symbols[target.id] = name
|
||||
changed = True
|
||||
if not changed:
|
||||
break
|
||||
for node in ast.walk(tree):
|
||||
name = symbol(node)
|
||||
if name is not None:
|
||||
check_compression(name, node)
|
||||
return violations, pending
|
||||
|
||||
|
||||
def validate_solution(path, checker):
|
||||
entrypoint = Path(path).resolve()
|
||||
root = entrypoint.parent
|
||||
pending = {entrypoint}
|
||||
visited = set()
|
||||
violations = set()
|
||||
while pending:
|
||||
current = pending.pop()
|
||||
if current in visited:
|
||||
continue
|
||||
visited.add(current)
|
||||
tree = ast.parse(current.read_text(encoding="utf-8-sig"), filename=str(current))
|
||||
errors, imports = _check_file(tree, current, root, checker)
|
||||
violations.update(errors)
|
||||
pending.update(imports - visited)
|
||||
return sorted(violations)
|
||||
Reference in New Issue
Block a user