mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 23:31:31 +02:00
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
184 lines
5.3 KiB
Python
184 lines
5.3 KiB
Python
"""The only reader and writer of wordlist files."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
from sqlalchemy import select
|
|
|
|
from shared.definitions.wordlists import (
|
|
BUILTIN_ROOT,
|
|
BUILTIN_WORDLISTS,
|
|
CUSTOM_ROOT,
|
|
MAX_WORD_LENGTH,
|
|
WordlistOrigin,
|
|
slugify,
|
|
)
|
|
from shared.logging import get_logger
|
|
from shared.models.wordlist import Wordlist
|
|
from shared.utils.datetime import utc_now
|
|
|
|
if TYPE_CHECKING:
|
|
from sqlalchemy.orm import Session
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class WordlistError(Exception):
|
|
"""An upload that cannot be stored."""
|
|
|
|
|
|
def builtin_root() -> Path:
|
|
return Path(BUILTIN_ROOT)
|
|
|
|
|
|
def custom_root() -> Path:
|
|
return Path(CUSTOM_ROOT)
|
|
|
|
|
|
def _root_for(origin: str) -> Path:
|
|
return builtin_root() if origin == WordlistOrigin.BUILTIN.value else custom_root()
|
|
|
|
|
|
def resolve_path(row: Wordlist) -> Path:
|
|
"""The file for a row, inside its root."""
|
|
root = _root_for(row.origin).resolve()
|
|
target = (root / row.filename).resolve()
|
|
if not target.is_relative_to(root):
|
|
msg = f"{row.slug} resolves outside the wordlist root"
|
|
raise WordlistError(msg)
|
|
return target
|
|
|
|
|
|
def clean_words(raw: str) -> list[str]:
|
|
"""One word per line, deduped, order kept."""
|
|
seen: set[str] = set()
|
|
words: list[str] = []
|
|
for line in raw.splitlines():
|
|
word = line.strip().lower()
|
|
if not word or word.startswith("#") or len(word) > MAX_WORD_LENGTH:
|
|
continue
|
|
if word in seen:
|
|
continue
|
|
seen.add(word)
|
|
words.append(word)
|
|
return words
|
|
|
|
|
|
def store_custom(filename: str, raw: str) -> tuple[str, list[str]]:
|
|
"""Validate an upload and write it under the custom root."""
|
|
words = clean_words(raw)
|
|
if not words:
|
|
msg = "No usable words. Every line is blank, a comment or over 63 characters."
|
|
raise WordlistError(msg)
|
|
stem = slugify(Path(filename).stem) or "wordlist"
|
|
relative = f"{stem}.txt"
|
|
root = custom_root()
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
target = (root / relative).resolve()
|
|
if not target.is_relative_to(root.resolve()):
|
|
msg = "The filename resolves outside the wordlist root."
|
|
raise WordlistError(msg)
|
|
target.write_text("\n".join(words) + "\n", encoding="utf-8")
|
|
return relative, words
|
|
|
|
|
|
def delete_custom(row: Wordlist) -> None:
|
|
"""Unlink a custom list."""
|
|
if row.origin != WordlistOrigin.CUSTOM.value:
|
|
msg = "Default wordlists are read-only."
|
|
raise WordlistError(msg)
|
|
path = resolve_path(row)
|
|
path.unlink(missing_ok=True)
|
|
|
|
|
|
def ensure_builtin(session: Session) -> int:
|
|
"""Index the shipped lists."""
|
|
root = builtin_root()
|
|
now = utc_now()
|
|
indexed = 0
|
|
for spec in BUILTIN_WORDLISTS:
|
|
path = root / spec.filename
|
|
if not path.is_file():
|
|
continue
|
|
words = len(clean_words(path.read_text(encoding="utf-8", errors="replace")))
|
|
row = session.scalar(select(Wordlist).where(Wordlist.slug == spec.slug))
|
|
values = {
|
|
"name": spec.name,
|
|
"description": spec.description,
|
|
"origin": WordlistOrigin.BUILTIN.value,
|
|
"kind": spec.kind,
|
|
"filename": spec.filename,
|
|
"words": words,
|
|
"bytes": path.stat().st_size,
|
|
"updated_at": now,
|
|
}
|
|
if row is None:
|
|
session.add(Wordlist(slug=spec.slug, **values))
|
|
else:
|
|
for key, value in values.items():
|
|
setattr(row, key, value)
|
|
session.add(row)
|
|
indexed += 1
|
|
session.commit()
|
|
return indexed
|
|
|
|
|
|
def _builtin_by_filename(value: str) -> str | None:
|
|
name = Path(value).name
|
|
for spec in BUILTIN_WORDLISTS:
|
|
if spec.filename == name:
|
|
return spec.slug
|
|
return None
|
|
|
|
|
|
def lookup(session: Session, reference: str) -> Wordlist | None:
|
|
"""Find a list by slug or by builtin filename."""
|
|
value = (reference or "").strip()
|
|
if not value:
|
|
return None
|
|
row = session.scalar(select(Wordlist).where(Wordlist.slug == value))
|
|
if row is not None:
|
|
return row
|
|
slug = _builtin_by_filename(value)
|
|
if slug is None:
|
|
return None
|
|
ensure_builtin(session)
|
|
return session.scalar(select(Wordlist).where(Wordlist.slug == slug))
|
|
|
|
|
|
def read_words(session: Session, reference: str, limit: int) -> tuple[list[str], str]:
|
|
"""The first `limit` words of a list, with the name to report it by."""
|
|
row = lookup(session, reference)
|
|
if row is None:
|
|
msg = f"No wordlist named {reference!r} is in the library."
|
|
raise WordlistError(msg)
|
|
path = resolve_path(row)
|
|
if not path.is_file():
|
|
msg = f"The file for {row.name} is missing."
|
|
raise WordlistError(msg)
|
|
words: list[str] = []
|
|
with path.open(encoding="utf-8", errors="replace") as handle:
|
|
for line in handle:
|
|
word = line.strip().lower()
|
|
if word and not word.startswith("#") and len(word) <= MAX_WORD_LENGTH:
|
|
words.append(word)
|
|
if len(words) >= limit:
|
|
break
|
|
return words, row.name
|
|
|
|
|
|
__all__ = [
|
|
"WordlistError",
|
|
"builtin_root",
|
|
"clean_words",
|
|
"custom_root",
|
|
"delete_custom",
|
|
"ensure_builtin",
|
|
"lookup",
|
|
"read_words",
|
|
"resolve_path",
|
|
"store_custom",
|
|
]
|