chore: rename project coven → hack-house ⛧

Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"),
README, CLI strings, and branch (coven → hack-house). Gitea repo renamed
cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest +
golden-vector test still green, binary is now `hack-house`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-05-30 13:29:14 -07:00
parent 82a04f3e12
commit bb1d662ee1
2730 changed files with 712933 additions and 46 deletions
@@ -0,0 +1,11 @@
from .form import File, parse_multipart_form
from .parameters import RequestParameters
from .types import Request
__all__ = (
"File",
"parse_multipart_form",
"Request",
"RequestParameters",
)
@@ -0,0 +1,114 @@
from __future__ import annotations
import email.utils
import unicodedata
from typing import NamedTuple
from urllib.parse import unquote
from sanic.headers import parse_content_header
from sanic.log import logger
from .parameters import RequestParameters
class File(NamedTuple):
"""Model for defining a file.
It is a `namedtuple`, therefore you can iterate over the object, or
access the parameters by name.
Args:
type (str, optional): The mimetype, defaults to "text/plain".
body (bytes): Bytes of the file.
name (str): The filename.
"""
type: str
body: bytes
name: str
def parse_multipart_form(body, boundary):
"""Parse a request body and returns fields and files
Args:
body (bytes): Bytes request body.
boundary (bytes): Bytes multipart boundary.
Returns:
tuple[RequestParameters, RequestParameters]: A tuple containing fields and files as `RequestParameters`.
""" # noqa: E501
files = {}
fields = {}
form_parts = body.split(boundary)
for form_part in form_parts[1:-1]:
file_name = None
content_type = "text/plain"
content_charset = "utf-8"
field_name = None
line_index = 2
line_end_index = 0
while not line_end_index == -1:
line_end_index = form_part.find(b"\r\n", line_index)
form_line = form_part[line_index:line_end_index].decode("utf-8")
line_index = line_end_index + 2
if not form_line:
break
colon_index = form_line.index(":")
idx = colon_index + 2
form_header_field = form_line[0:colon_index].lower()
form_header_value, form_parameters = parse_content_header(
form_line[idx:]
)
if form_header_field == "content-disposition":
field_name = form_parameters.get("name")
file_name = form_parameters.get("filename")
# non-ASCII filenames in RFC2231, "filename*" format
if file_name is None and form_parameters.get("filename*"):
encoding, _, value = email.utils.decode_rfc2231(
form_parameters["filename*"]
)
file_name = unquote(value, encoding=encoding)
# Normalize to NFC (Apple MacOS/iOS send NFD)
# Notes:
# - No effect for Windows, Linux or Android clients which
# already send NFC
# - Python open() is tricky (creates files in NFC no matter
# which form you use)
if file_name is not None:
file_name = unicodedata.normalize("NFC", file_name)
elif form_header_field == "content-type":
content_type = form_header_value
content_charset = form_parameters.get("charset", "utf-8")
if field_name:
post_data = form_part[line_index:-4]
if file_name is None:
value = post_data.decode(content_charset)
if field_name in fields:
fields[field_name].append(value)
else:
fields[field_name] = [value]
else:
form_file = File(
type=content_type, name=file_name, body=post_data
)
if field_name in files:
files[field_name].append(form_file)
else:
files[field_name] = [form_file]
else:
logger.debug(
"Form-data field does not have a 'name' parameter "
"in the Content-Disposition header"
)
return RequestParameters(fields), RequestParameters(files)
@@ -0,0 +1,33 @@
from __future__ import annotations
from typing import Any
class RequestParameters(dict):
"""Hosts a dict with lists as values where get returns the first value of the list and getlist returns the whole shebang""" # noqa: E501
def get(self, name: str, default: Any | None = None) -> Any | None:
"""Return the first value, either the default or actual
Args:
name (str): The name of the parameter
default (Any | None, optional): The default value. Defaults to None.
Returns:
Any | None: The first value of the list
""" # noqa: E501
return super().get(name, [default])[0]
def getlist(
self, name: str, default: list[Any] | None = None
) -> list[Any]:
"""Return the entire list
Args:
name (str): The name of the parameter
default (list[Any] | None, optional): The default value. Defaults to None.
Returns:
list[Any]: The entire list of values or [] if not found
""" # noqa: E501
return super().get(name, default) or []
File diff suppressed because it is too large Load Diff