This commit is contained in:
2025-04-09 16:54:34 +08:00
parent 7cf1c7741d
commit 1d9ab14b01
11 changed files with 650 additions and 0 deletions

152
app/subroutines/_c_route.c Normal file
View File

@@ -0,0 +1,152 @@
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define SEP 0x01
#define FORMAT_START 0x03
#define FORMAT_END 0x00
#define DOUBLE_STAR 0x09
void free_parsed_route(char *parsed) {
free(parsed);
}
// Helper function to write an Exact component to the output buffer
static void write_exact(const char *value, size_t value_len, char **output, size_t *output_len) {
memcpy(*output, value, value_len);
*output += value_len;
**output = SEP;
(*output)++;
*output_len += value_len + 1;
}
// Helper function to write a Format component to the output buffer
static void write_format(const char *name, size_t name_len, size_t minl, size_t maxl, int ignore_sep, char **output, size_t *output_len) {
// Write FORMAT_START marker
**output = FORMAT_START;
(*output)++;
*output_len += 1;
// Write the name
memcpy(*output, name, name_len);
*output += name_len;
**output = FORMAT_END;
(*output)++;
*output_len += name_len + 1;
// Write minl and maxl as size_t values
memcpy(*output, &minl, sizeof(size_t));
*output += sizeof(size_t);
memcpy(*output, &maxl, sizeof(size_t));
*output += sizeof(size_t);
*output_len += 2 * sizeof(size_t);
// Write ignore_sep as a single byte
**output = (char)ignore_sep;
(*output)++;
**output = SEP;
(*output)++;
*output_len += 2;
}
// Main parsing function
char *parse_route(const char *route, size_t route_len, size_t *output_len) {
char *output = malloc(route_len * 8); // Allocate enough space for the output
if (!output) return NULL;
char *output_start = output;
*output_len = 0;
const char *p = route;
const char *end = route + route_len;
while (p < end) {
if (*p == '/') {
// Skip leading slashes
write_exact("/", 1, &output, output_len);
p++;
continue;
} else if (*p == '{') {
// Handle Format component
const char *start = p + 1; // Skip '{'
const char *end_brace = memchr(start, '}', end - start);
if (!end_brace) break; // Invalid format, stop parsing
// Extract name and constraints
const char *colon = memchr(start, ':', end_brace - start);
size_t name_len = colon ? (size_t)(colon - start) : (size_t)(end_brace - start);
const char *name = start;
size_t minl = 0, maxl = (size_t)-1, ignore_sep = 0;
if (colon) {
const char *constraint = colon + 1;
if (memchr(constraint, '-', end_brace - constraint)) {
// Parse "minl-maxl"
sscanf(constraint, "%zu-%zu", &minl, &maxl);
} else {
// Parse "minl" (fixed length)
sscanf(constraint, "%zu", &minl);
maxl = minl;
}
}
// Write the Format component
write_format(name, name_len, minl, maxl, ignore_sep, &output, output_len);
p = end_brace + 1; // Move past '}'
} else if (*p == '*' && *(p + 1) == '*') {
// Handle "**" as a special marker (\x09)
*output = DOUBLE_STAR;
output++;
*output = SEP;
output++;
*output_len += 2;
p += 2; // Move past "**"
} else {
// Handle Exact component
const char *next_slash = memchr(p, '/', end - p);
size_t segment_len = next_slash ? (size_t)(next_slash - p) : (size_t)(end - p);
write_exact(p, segment_len, &output, output_len);
p += segment_len;
}
}
// Null-terminate the output
*output = '\0';
*output_len += 1;
return output_start;
}
// // Example usage
// int main() {
// const char *route = "/foo/{n:5}/bar/**/baz";
// size_t route_len = strlen(route);
// size_t output_len;
// char *result = parse_route(route, route_len, &output_len);
// if (!result) {
// fprintf(stderr, "Memory allocation failed\n");
// return 1;
// }
// printf("Parsed result (%zu bytes):\n", output_len);
// for (size_t i = 0; i < output_len; i++) {
// if (result[i] == SEP) {
// printf("\\x01");
// } else if (result[i] == FORMAT_START) {
// printf("\\x03");
// } else if (result[i] == FORMAT_END) {
// printf("\\x00");
// } else if (result[i] == DOUBLE_STAR) {
// printf("\\x09");
// } else {
// printf("%c", result[i]);
// }
// }
// printf("\n");
// free(result);
// return 0;
// }

138
app/subroutines/http.py Normal file
View File

@@ -0,0 +1,138 @@
from collections.abc import MutableMapping
from dataclasses import InitVar, dataclass, field
from http.cookies import BaseCookie
from types import TracebackType
from typing import Any, Self
from app.exceptions import ConnectionClosed
from app.types_ import CommonMapping, ReceiveHTTP, Send
class SimpleRequest:
body: bytes
body_parts: list[bytes]
body_complete: bool
done: bool
keep_unset_body: bool = False
def __init__(self, keep_unset_body: bool | None = None) -> None:
self.body = b""
self.body_parts = []
self.body_complete = False
self.done = False
if keep_unset_body is not None:
self.keep_unset_body = keep_unset_body
def receive(self, received: ReceiveHTTP) -> bool:
if received["type"] == "http.disconnect":
self.done = True
return True
if "body" in received:
self.body_parts.append(received["body"])
elif self.keep_unset_body:
self.body_parts.append(b"")
if not ("more_body" in received and received["more_body"]):
self.body = b"".join(self.body_parts)
self.body_complete = True
return self.body_complete or self.done
class SimpleResponse:
send: Send
headers: list[tuple[bytes, bytes]]
status: int
trailers: bool
done: bool
def __init__(self, send: Send) -> None:
self.send = send
self.headers = []
self.status = 200
self.trailers = False
self.done = False
def add_header(self, name: str, value: object) -> None:
self.headers.append((name.lower().encode(), str(value).encode()))
def prepare(self, status: int = 200, trailers: bool = False, headers: MutableMapping[str, str] | None = None) -> Self:
self.status = status
self.trailers = trailers
if headers:
for k, v in headers.items():
self.add_header(k, v)
return self
async def start(self) -> None:
if self.done:
raise ConnectionClosed
await self.send({
"type": "http.response.start",
"status": self.status,
"headers": self.headers,
"trailers": self.trailers
})
async def body(self, data: bytes = b"", *, done: bool = True) -> None:
if self.done:
raise ConnectionClosed
await self.send({
"type": "http.response.body",
"body": data,
"more_body": not done
})
self.done = done and not self.trailers
async def part(self, data: bytes = b"") -> None:
"""Same as `Response.body(data, done=False)`"""
return await self.body(data, done=False)
async def finish(self, data: bytes = b"") -> None:
"""Same as `Response.body(data, done=True)`"""
return await self.body(data, done=True)
async def trail(self) -> None:
if self.done:
raise ConnectionClosed
await self.send({"type": "http.response.trailers"})
self.done = True
async def __aenter__(self) -> Self:
await self.start()
return self
async def __aexit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /) -> bool | None:
if not (self.done or exc_type):
await self.finish()
if exc_type and self.trailers:
await self.trail()
return None
@dataclass
class Response:
status: int = 200
body: bytes | None = None
headers: CommonMapping = field(default_factory=dict[str, Any])
content_type: InitVar[str | None] = None
cookies: InitVar[BaseCookie[bytes] | None] = None
def __post_init__(self, content_type: str | None, cookies: BaseCookie[bytes] | None) -> None:
if self.body is not None:
self.headers["content-length"] = len(self.body)
if content_type is not None:
self.headers["content-type"] = content_type
if cookies is not None:
self.headers["set-cookie"] = cookies.output(header="").strip()
@dataclass
class HTMLResponse(Response):
content: InitVar[str] = ""
encoding: InitVar[str] = "utf-8"
def __post_init__(self, content_type: str | None, cookies: BaseCookie[bytes] | None, content: str, encoding: str) -> None:
self.body: bytes | None = content.encode(encoding)
return super().__post_init__("text/html", cookies)

48
app/subroutines/route.py Normal file
View File

@@ -0,0 +1,48 @@
# / -> Exact("/")
# /foo -> Exact("/foo")
# /foo/ -> Exact("/foo/")
# /foo/bar -> Exact("/foo/bar")
# /foo/bar/{n} -> Exact("/foo/bar/"), Format("n")
# /foo/{n}/bar -> Exact("/foo/"), Format("n"), Exact("/bar")
# /foo{n:5}/bar -> Exact("/foo"), Format("n", minl=5, maxl=5), Exact("/bar")
# /foo/{m:1-}/{n:3-5}bar -> Exact("/foo/"), Format("m", minl=1, maxl=-1), Exact("/"), Format("n", minl=3, maxl=5), Exact("bar")
# /* -> Exact("/"), Format("_")
# /foo/*bar -> Exact("/foo/"), Format("_"), Exact("bar")
# /foo/*/bar -> Exact("/foo/"), Format("_"), Exact("/bar")
# /foo/**/bar -> Exact("/foo/"), Format("_", ignore_sep=True), Exact("/bar")
# ["foo", Format("m", minl=1, maxl=-1), [Format("n", minl=3, maxl=5), "bar"], "*"]
import ctypes
dll = ctypes.CDLL("./c_route.so")
dll.parse_route.restype = type("c_char_alloc", (ctypes.c_char_p,), {})
dll.free_parsed_route.argtypes = (ctypes.c_void_p,)
dll.free_parsed_route.restype = None
def compile_route(pattern: str) -> list[bytes]:
if pattern[0] != "/":
from app.exceptions import ParseError
raise ParseError("The first character in route pattern must be '/'.")
# res: list[str] = []
# for path in pattern[1:].split("/"):
# if (star_idx := path.find("*")) != -1:
# path = "".join((path[:star_idx], "{_}", path[star_idx + 1:]))
# if "{" not in path and "}" not in path and "*" not in path:
# res.append(path)
outlen = ctypes.c_size_t()
data = pattern[1:].encode()
ret = dll.parse_route(data, len(data), ctypes.byref(outlen))
res = ctypes.string_at(ret, outlen.value - 2).split(b"\x01")
dll.free_parsed_route(ret)
return res
if __name__ == "__main__":
print(compile_route("/foo/{n:5}foo/bar/**/baz"))
print(compile_route("/"))