Initial commit

This commit is contained in:
Hugo Mårdbrink 2025-11-03 13:50:38 +01:00
commit 1368b7b69b
10 changed files with 588 additions and 0 deletions

79
http/http.odin Normal file
View file

@ -0,0 +1,79 @@
package http
import "core:strconv"
RequestError :: enum {
InvalidRequestLine,
InvalidMethod,
InvalidHeaderFormat,
IncompleteRequest,
ContentLengthMismatch,
FailedParsing,
NetworkError,
}
ResponseError :: enum {
InvalidContentType,
InvalidStatus,
}
Header :: map[string]string
Status :: enum u32 {
Ok = 200,
BadRequest = 400,
NotFound = 404,
InternalServerError = 500,
}
ContentType :: enum {
Html,
Image,
}
Response :: struct {
header: Header,
proto_version: string,
status: Status,
body: []byte,
}
Method :: enum {
GET,
POST,
PUT,
DELETE,
}
Request :: struct {
header: Header,
proto_version: string,
method: Method,
path: string,
body: []byte,
path_variables: map[string]string,
}
make_response :: proc(
status: Status,
body: []byte,
content_type: ContentType,
allocator := context.temp_allocator,
) -> Response {
response := Response {
status = status,
proto_version = "HTTP/1.1",
header = make(Header, allocator),
body = body,
}
buf := make([]byte, 32, allocator)
response.header["Content-Length"] = strconv.write_uint(
buf[:],
u64(len(body)),
10,
)
response.header["Content-Type"] = str_from_content_type(content_type)
return response
}

188
http/marshaller.odin Normal file
View file

@ -0,0 +1,188 @@
package http
import "core:bufio"
import "core:bytes"
import "core:io"
import "core:log"
import "core:net"
import "core:strconv"
import "core:strings"
import "core:unicode"
@(private)
method_from_str :: proc(method_str: string) -> Method {
switch method_str {
case "GET":
return .GET
case "DELETE":
return .DELETE
case "PUT":
return .PUT
case "POST":
return .POST
}
unreachable()
}
@(private)
str_from_content_type :: proc(content_type: ContentType) -> string {
switch content_type {
case .Html:
return "text/html"
case .Image:
return "image/jpeg"
}
unreachable()
}
@(private)
status_text :: proc(status: Status) -> string {
switch status {
case .Ok:
return "OK"
case .NotFound:
return "Not found"
case .BadRequest:
return "Bad request"
case .InternalServerError:
return "Internal server error"
}
unreachable()
}
@(private)
unmarshall_request_line :: proc(
request: ^Request,
reader: ^bufio.Reader,
) -> RequestError {
request_line, io_err := bufio.reader_read_slice(reader, '\n')
if io_err != nil do return .InvalidRequestLine
request_line = sanitize_byte_slice(request_line)
parts := bytes.split(request_line, {' '})
if len(parts) != 3 do return .InvalidRequestLine
request.method = method_from_str(string(parts[0]))
request.path = string(parts[1])
request.proto_version = string(parts[2])
return nil
}
@(private)
unmarshall_request_headers :: proc(
request: ^Request,
reader: ^bufio.Reader,
allocator := context.temp_allocator,
) -> RequestError {
request.header = make(Header, allocator)
for {
header_line, io_err := bufio.reader_read_slice(reader, '\n')
if io_err != nil do return .InvalidHeaderFormat
header_line = sanitize_byte_slice(header_line)
if len(header_line) == 0 do break
parts := bytes.split_n(header_line, {':'}, 2)
if len(parts) != 2 do return .InvalidHeaderFormat
key := string(bytes.trim_space(parts[0]))
value := string(bytes.trim_space(parts[1]))
request.header[key] = value
}
return nil
}
@(private)
unmarshall_request_body :: proc(
request: ^Request,
reader: ^bufio.Reader,
allocator := context.temp_allocator,
) -> RequestError {
content_length_str, ok := request.header["Content-Length"]
if ok {
content_length, ok := strconv.parse_uint(content_length_str)
if !ok do return .InvalidHeaderFormat
request.body = make([]byte, content_length, allocator)
_, err := bufio.reader_read(reader, request.body)
if err != nil do return .FailedParsing
} else {
request.body = {}
}
return nil
}
@(private)
sanitize_byte_slice :: proc(slice: []byte) -> []byte {
return bytes.trim(slice, {'\n', '\r'})
}
unmarshall_request :: proc(
data: []byte,
allocator := context.temp_allocator,
) -> (
request: Request,
request_err: RequestError,
) {
if len(data) == 0 do return request, .IncompleteRequest
byte_reader: bytes.Reader
stream := bytes.reader_init(&byte_reader, data)
reader: bufio.Reader
bufio.reader_init(&reader, io.to_reader(stream), allocator = allocator)
defer bufio.reader_destroy(&reader)
unmarshall_request_line(&request, &reader) or_return
unmarshall_request_headers(&request, &reader, allocator) or_return
unmarshall_request_body(&request, &reader, allocator) or_return
return request, nil
}
marshall_response :: proc(
response: ^Response,
allocator := context.temp_allocator,
) -> []byte {
buffer: bytes.Buffer
bytes.buffer_init_allocator(&buffer, 0, 0, allocator)
bytes.buffer_write_string(&buffer, response.proto_version)
bytes.buffer_write_string(&buffer, " ")
status_code_buf := make([]byte, 4, allocator)
status_code_str := strconv.write_uint(
status_code_buf[:],
u64(response.status),
10,
)
bytes.buffer_write_string(&buffer, status_code_str)
bytes.buffer_write_string(&buffer, " ")
status_msg := status_text(response.status)
bytes.buffer_write_string(&buffer, status_msg)
bytes.buffer_write_string(&buffer, "\r\n")
for key, value in response.header {
bytes.buffer_write_string(&buffer, key)
bytes.buffer_write_string(&buffer, ": ")
bytes.buffer_write_string(&buffer, value)
bytes.buffer_write_string(&buffer, "\r\n")
}
bytes.buffer_write_string(&buffer, "\r\n")
bytes.buffer_write(&buffer, response.body)
return bytes.buffer_to_bytes(&buffer)
}