Compare commits
7 Commits
712e214f61
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dc839e098c | |||
| 380145a986 | |||
| 0cce9d9bce | |||
| 572f4be896 | |||
| 8d45a93e6e | |||
| 09266e678f | |||
| cbf0d6a9da |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.zig-cache
|
||||
zig-out
|
||||
zig-pkg
|
||||
|
||||
88
README.md
Normal file
88
README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Building my own castle
|
||||
|
||||
I'm building my own castle in Zig ⚡.
|
||||
|
||||
This is a collection of experimental, recreational or actually useful libraries,
|
||||
which I may or may not use as a part of some other projects. Most of them are
|
||||
work in progress and might remain so indefinitely.
|
||||
|
||||
## cjit
|
||||
|
||||
JIT compiler for the C language inspired by Fabrice Bellard's Tiny C Compiler
|
||||
(TCC). Meant to compile x86_64 and aarch64 for Windows and Linux straight to
|
||||
virtual memory, without the ability to output an executable file or library. C
|
||||
standard library is not fully supported, only some includes and builtins (see
|
||||
[src/includes](packages/cjit/src/includes)).
|
||||
|
||||
It's in very early stage and cannot be used as of now. Currently, parts of the
|
||||
tokenizer, x86_64 emit code and relocation logic are implemented. Aarch64 would
|
||||
come after x86_64 is fully implemented and actually works.
|
||||
|
||||
You can see an example of how this library is supposed to be used in
|
||||
[test/root.zig](packages/cjit/test/root.zig).
|
||||
|
||||
## js
|
||||
|
||||
Zig bindings for Fabrice Bellard's QuickJS. Original C sources are included and
|
||||
compiled with Zig's build system. The bindings are not complete, but usable.
|
||||
|
||||
## media
|
||||
|
||||
Set of utilities for decoding and encoding media files. As of now, no purely Zig
|
||||
decoder/encoder exists, which would be the goal. There is fairly comprehensive
|
||||
PNG chunk decoder, though.
|
||||
|
||||
Bindings for stb_image.h from Sean T. Barrett's single-file C libraries are
|
||||
included. They can be used to decode PNG, JPEG and HDR images and support Zig's
|
||||
allocator interface in a thread-safe manner.
|
||||
|
||||
This package is actually used by my other project, codenamed
|
||||
[voxel-game](/:root/renati/voxel-game).
|
||||
|
||||
## myid
|
||||
|
||||
Remnants of a project meant to provide a simpler alternative to OpenID Connect
|
||||
(OIDC). It contains an HTTP server, which has since been improved and resides
|
||||
in *web* package.
|
||||
|
||||
The ideas behind *myid* were developed further in another project I created
|
||||
using TypeScript and Bun as a runtime. I might backport them back to Zig when I
|
||||
find the time and motivation, which would be implemented on top of the HTTP
|
||||
server from the *web* package.
|
||||
|
||||
## vecmath
|
||||
|
||||
Vector math library with support for vectors, matrices and ×8 SIMD operations
|
||||
(utilizing SOA layout).
|
||||
|
||||
This package is actually used by my other project, codenamed
|
||||
[voxel-game](/:root/renati/voxel-game) and is probably the most complete package
|
||||
in this repository.
|
||||
|
||||
## web
|
||||
|
||||
An HTTP server library and collection of other utilities commonly associated
|
||||
with web technologies.
|
||||
|
||||
The HTTP server implements HTTP version 1.1 using worker threads and supports
|
||||
SSL via OpenSSL library. The server is very low level and does not provide any
|
||||
logic to handle proper HTTP semantics. It is up to the user of the HTTP server
|
||||
library to respect (or not) all HTTP methods and headers. The server is designed
|
||||
or Linux only and uses Linux syscalls directly in its implementation.
|
||||
|
||||
The other utilities include:
|
||||
|
||||
- HTTP/1.1 request parser, which is part of the HTTP server, but can be used
|
||||
independently,
|
||||
- OpenSSL bindings, which are used by the HTTP server, but can be used
|
||||
independently (full mechanical C translation is provided and partial manual
|
||||
Zig translation),
|
||||
- UUID type and generators for v4, v5 and v7,
|
||||
- Generic ID type, a wrapper around UUID (or anything that is 16 bytes long)
|
||||
that can be instantiated for each distinct use of ID with a tag to help avoid
|
||||
type confusion at compile time.
|
||||
|
||||
## x11
|
||||
|
||||
Zig bindings for Xlib (aka libX11), a C library implementing a client of the X
|
||||
Window System protocol. The bindings are not complete.
|
||||
@@ -195,7 +195,7 @@ const import = struct {
|
||||
no_add: bool = false,
|
||||
/// Internal use.
|
||||
no_exotic: bool = false,
|
||||
_pad14: u18 = 0,
|
||||
_pad18: u14 = 0,
|
||||
};
|
||||
|
||||
pub const JS_EVAL = packed struct(u32) {
|
||||
@@ -1168,7 +1168,7 @@ pub const Value = extern struct {
|
||||
}
|
||||
|
||||
pub fn getClassId(self: Value) ClassId {
|
||||
return .{ .class_id = import.JS_GetClassID(self) };
|
||||
return .{ .class_id = import.JS_GetClassID(self.value) };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1182,7 +1182,7 @@ pub const ClassId = extern struct {
|
||||
pub const invalid: ClassId = .{ .class_id = 0 };
|
||||
|
||||
pub fn new() ClassId {
|
||||
var class_id: import.JSClassId = 0;
|
||||
var class_id: import.JSClassID = 0;
|
||||
return .{ .class_id = import.JS_NewClassID(&class_id) };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const vm_dep = b.dependency("vecmath", .{});
|
||||
const vm_module = vm_dep.module("vecmath");
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
const root_module = b.addModule("media", .{
|
||||
const vm_dep = b.dependency("vecmath", .{
|
||||
.target = target,
|
||||
});
|
||||
const vm_mod = vm_dep.module("vecmath");
|
||||
|
||||
const mod = b.addModule("media", .{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.link_libc = true,
|
||||
.imports = &.{
|
||||
.{ .name = "vecmath", .module = vm_mod },
|
||||
},
|
||||
});
|
||||
|
||||
root_module.addCSourceFile(.{
|
||||
mod.addCSourceFile(.{
|
||||
.file = b.path("src/stbi/stb_image.c"),
|
||||
.flags = &.{
|
||||
"-std=c17",
|
||||
@@ -19,5 +27,12 @@ pub fn build(b: *std.Build) void {
|
||||
.language = .c,
|
||||
});
|
||||
|
||||
root_module.addImport("vecmath", vm_module);
|
||||
const mod_tests = b.addTest(.{
|
||||
.root_module = mod,
|
||||
});
|
||||
|
||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_mod_tests.step);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.{
|
||||
.name = .media,
|
||||
.version = "0.0.0",
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.minimum_zig_version = "0.16.0",
|
||||
.paths = .{
|
||||
"src",
|
||||
"build.zig",
|
||||
|
||||
@@ -19,3 +19,7 @@ media_type: []const u8,
|
||||
/// to contain the entire file, only its beginning. The caller asserts that
|
||||
/// `buffer` is at least `magic_length` bytes long.
|
||||
isFormat: *const fn (buffer: []const u8) bool,
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -9,14 +9,26 @@ pub const Static = struct {
|
||||
const sample_rate: f64 = @floatFromInt(self.sample_rate);
|
||||
return @floatCast(samples / sample_rate);
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
pub const Sample = extern struct {
|
||||
left: i16,
|
||||
right: i16,
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
pub const Stream = struct {
|
||||
source: *std.Io.Reader,
|
||||
sample_rate: u32,
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,11 +19,15 @@ pub fn Static(comptime W: u32, comptime H: u32) type {
|
||||
}
|
||||
|
||||
pub fn fill(self: *@This(), color: vm.Color) void {
|
||||
@memset(self.data, color);
|
||||
@memset(&self.data, color);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "Static - refAllDecls" {
|
||||
std.testing.refAllDecls(Static(16, 16));
|
||||
}
|
||||
|
||||
pub const Dynamic = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
@@ -65,6 +69,10 @@ pub const Dynamic = struct {
|
||||
pub fn fill(self: *@This(), color: vm.Color) void {
|
||||
@memset(self.data[0 .. self.width * self.height], color);
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
pub const Hdr = struct {
|
||||
@@ -108,4 +116,8 @@ pub const Hdr = struct {
|
||||
pub fn fill(self: *@This(), color: vm.ColorHdr) void {
|
||||
@memset(self.data[0 .. self.width * self.height], color);
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,6 +20,10 @@ const Info = union(enum) {
|
||||
pub fn makeFull(full: Header) Info {
|
||||
return .{ .full = full };
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
const Header = struct {
|
||||
@@ -99,14 +103,18 @@ const Marker = enum(u8) {
|
||||
return if (@intFromEnum(self) & 0b1111_0000 == 0xE0) @intCast(@intFromEnum(self) & 0b0000_1111) else null;
|
||||
}
|
||||
|
||||
/// A standalone marker has no content and are not followed by segment
|
||||
/// length parameter.
|
||||
/// A standalone marker has no content and is not followed by segment length
|
||||
/// parameter.
|
||||
pub fn isStandalone(self: Marker) bool {
|
||||
return self.isRST() != null or
|
||||
self == .SOI or
|
||||
self == .EOI or
|
||||
self == .TEM;
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
/// The caller asserts that the buffer is at least `format.magic_length` bytes
|
||||
@@ -137,3 +145,7 @@ pub fn info(buffer: []const u8) ?Info {
|
||||
|
||||
@panic("TODO");
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -35,3 +35,7 @@ pub fn info(buffer: []const u8) ?Header {
|
||||
|
||||
@panic("TODO");
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ pub const Header = struct {
|
||||
filter_method: FilterMethod,
|
||||
interlace_method: InterlaceMethod,
|
||||
|
||||
pub fn decode(chunk: Chunk) Header {
|
||||
pub fn decode(chunk: Chunk) !Header {
|
||||
std.debug.assert(chunk.chunk_type == .IHDR);
|
||||
|
||||
if (chunk.data.len != 13) return error.InvalidPng;
|
||||
@@ -79,6 +79,10 @@ pub const Header = struct {
|
||||
.interlace_method = @enumFromInt(interlace_method),
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
pub const BitDepth = enum(u8) {
|
||||
@@ -89,7 +93,11 @@ pub const BitDepth = enum(u8) {
|
||||
@"16" = 16,
|
||||
|
||||
pub fn range(self: BitDepth) usize {
|
||||
return @as(usize, 2) << @intFromEnum(@intFromEnum(self));
|
||||
return @as(usize, 2) << @intCast(@intFromEnum(self));
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,6 +119,10 @@ pub const ColorType = enum(u8) {
|
||||
pub fn alphaChannelUsed(self: ColorType) bool {
|
||||
return @intFromEnum(self) & 0b0000_0100 != 0;
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
pub const CompressionMethod = enum(u8) {
|
||||
@@ -156,6 +168,10 @@ pub const Palette = struct {
|
||||
.entries = entries,
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
// --- tRNS --------------------------------------------------------------------
|
||||
@@ -209,6 +225,10 @@ pub const Transparency = union(enum) {
|
||||
.rgba => error.InvalidPng,
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
const TransparencyPalette = struct {
|
||||
@@ -218,6 +238,10 @@ const TransparencyPalette = struct {
|
||||
pub fn asSlice(self: *const TransparencyPalette) []const u8 {
|
||||
return self.entries[0..self.len];
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
// --- gAMA --------------------------------------------------------------------
|
||||
@@ -245,6 +269,10 @@ const Gamma = struct {
|
||||
const gamma = std.mem.readInt(u32, chunk.data[0..4], .big);
|
||||
return .{ .gamma = gamma };
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
// --- cHRM --------------------------------------------------------------------
|
||||
@@ -274,6 +302,10 @@ const Chromaticities = struct {
|
||||
.blue_y = std.mem.readInt(u32, chunk.data[28..32], .big),
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
// --- sRGB --------------------------------------------------------------------
|
||||
@@ -301,6 +333,10 @@ const RenderingIntent = enum(u8) {
|
||||
if (rendering_intent > 3) return error.InvalidPng;
|
||||
return @enumFromInt(rendering_intent);
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
// --- pHYs --------------------------------------------------------------------
|
||||
@@ -310,7 +346,7 @@ pub const PhysicalPixelDimensions = struct {
|
||||
pixels_per_unit_y: u32,
|
||||
unit: PhysicalUnit,
|
||||
|
||||
pub fn decode(chunk: Chunk) PhysicalPixelDimensions {
|
||||
pub fn decode(chunk: Chunk) !PhysicalPixelDimensions {
|
||||
std.debug.assert(chunk.chunk_type == .pHYs);
|
||||
|
||||
if (chunk.data.len != 9) return error.InvalidPng;
|
||||
@@ -328,6 +364,10 @@ pub const PhysicalPixelDimensions = struct {
|
||||
.unit = @enumFromInt(unit),
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
pub const PhysicalUnit = enum(u8) {
|
||||
@@ -381,41 +421,61 @@ pub const LastModificationTime = struct {
|
||||
.second = second,
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
pub const ChunkType = enum(u32) {
|
||||
IHDR = @bitCast("IHDR".*),
|
||||
PLTE = @bitCast("PLTE".*),
|
||||
IDAT = @bitCast("IDAT".*),
|
||||
IEND = @bitCast("IEND".*),
|
||||
tRNS = @bitCast("tRNS".*),
|
||||
gAMA = @bitCast("gAMA".*),
|
||||
cHRM = @bitCast("cHRM".*),
|
||||
sRGB = @bitCast("sRGB".*),
|
||||
iCCP = @bitCast("iCCP".*),
|
||||
tEXt = @bitCast("tEXt".*),
|
||||
zTXt = @bitCast("zTXt".*),
|
||||
iTXt = @bitCast("iTXt".*),
|
||||
bKGD = @bitCast("bKGD".*),
|
||||
pHYs = @bitCast("pHYs".*),
|
||||
sBIT = @bitCast("sBIT".*),
|
||||
sPLT = @bitCast("sPLT".*),
|
||||
hIST = @bitCast("hIST".*),
|
||||
tIME = @bitCast("tIME".*),
|
||||
IHDR = fromName("IHDR"),
|
||||
PLTE = fromName("PLTE"),
|
||||
IDAT = fromName("IDAT"),
|
||||
IEND = fromName("IEND"),
|
||||
tRNS = fromName("tRNS"),
|
||||
gAMA = fromName("gAMA"),
|
||||
cHRM = fromName("cHRM"),
|
||||
sRGB = fromName("sRGB"),
|
||||
iCCP = fromName("iCCP"),
|
||||
tEXt = fromName("tEXt"),
|
||||
zTXt = fromName("zTXt"),
|
||||
iTXt = fromName("iTXt"),
|
||||
bKGD = fromName("bKGD"),
|
||||
pHYs = fromName("pHYs"),
|
||||
sBIT = fromName("sBIT"),
|
||||
sPLT = fromName("sPLT"),
|
||||
hIST = fromName("hIST"),
|
||||
tIME = fromName("tIME"),
|
||||
_,
|
||||
|
||||
fn fromName(name: *const [4]u8) u32 {
|
||||
return @bitCast(name.*);
|
||||
}
|
||||
|
||||
pub fn fromBytes(bytes: *const [4]u8) ChunkType {
|
||||
return @enumFromInt(fromName(bytes));
|
||||
}
|
||||
|
||||
pub fn toBytes(self: ChunkType) [4]u8 {
|
||||
return @bitCast(@intFromEnum(self));
|
||||
}
|
||||
|
||||
pub fn ancillary(self: ChunkType) bool {
|
||||
return @as([4]u8, @bitCast(self))[0] & 0b0010_0000 != 0;
|
||||
return self.toBytes()[0] & 0b0010_0000 != 0;
|
||||
}
|
||||
|
||||
pub fn private(self: ChunkType) bool {
|
||||
return @as([4]u8, @bitCast(self))[1] & 0b0010_0000 != 0;
|
||||
return self.toBytes()[1] & 0b0010_0000 != 0;
|
||||
}
|
||||
|
||||
pub fn safeToCopy(self: ChunkType) bool {
|
||||
return @as([4]u8, @bitCast(self))[3] & 0b0010_0000 != 0;
|
||||
return self.toBytes()[3] & 0b0010_0000 != 0;
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -460,6 +520,10 @@ pub const StandardKeyword = enum {
|
||||
pub fn isStandardKeyword(keyword: []const u8) ?StandardKeyword {
|
||||
return map.get(keyword);
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
/// The caller asserts that the buffer is at least `format.magic_length` bytes
|
||||
@@ -477,7 +541,7 @@ pub fn info(buffer: []const u8) ?Header {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chunk, _ = decodeChunk(buffer[format.magic_length..]) catch return null orelse return null;
|
||||
const chunk, _ = (decodeChunk(buffer[format.magic_length..]) catch return null) orelse return null;
|
||||
if (chunk.chunk_type != .IHDR) {
|
||||
return null;
|
||||
}
|
||||
@@ -495,7 +559,7 @@ pub fn decodeChunk(buffer: []const u8) !?struct { Chunk, []const u8 } {
|
||||
}
|
||||
|
||||
const length = std.mem.readInt(u32, rest[0..4], .big);
|
||||
const chunk_type: ChunkType = @bitCast(rest[4..8].*);
|
||||
const chunk_type: ChunkType = .fromBytes(rest[4..8]);
|
||||
rest = rest[8..];
|
||||
|
||||
if (length > 0x7FFF_FFFF) {
|
||||
@@ -575,3 +639,7 @@ pub fn encodeChunks(chunks: []const Chunk, writer: *std.Io.Writer) !void {
|
||||
try writer.writeInt(u32, crc.final(), .big);
|
||||
}
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ const Header = union(enum) {
|
||||
pub fn initStatic(static: HeaderStatic) Header {
|
||||
return .{ .static = static };
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
const HeaderStreaming = void;
|
||||
@@ -36,6 +40,10 @@ const HeaderStatic = struct {
|
||||
const sample_rate: f64 = @floatFromInt(self.sample_rate);
|
||||
return @floatCast(samples / sample_rate);
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
/// The caller asserts that the buffer is at least `format.magic_length` bytes
|
||||
@@ -73,3 +81,7 @@ pub fn info(buffer: []const u8) ?Header {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -68,3 +68,7 @@ pub fn info(buffer: []const u8) ?Header {
|
||||
.color_space = @enumFromInt(color_space),
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const audio = @import("audio.zig");
|
||||
pub const Format = @import("Format.zig");
|
||||
pub const image = @import("image.zig");
|
||||
@@ -7,3 +9,7 @@ pub const png = @import("png.zig");
|
||||
pub const qoa = @import("qoa.zig");
|
||||
pub const qoi = @import("qoi.zig");
|
||||
pub const stbi = @import("stbi.zig");
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -5,17 +5,20 @@ const image = @import("image.zig");
|
||||
const vm = @import("vecmath");
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
io: std.Io,
|
||||
|
||||
mutex: std.Io.Mutex = .init,
|
||||
allocations: std.AutoHashMapUnmanaged(*anyopaque, usize) = .empty,
|
||||
mutex: std.Thread.Mutex = .{},
|
||||
allocated_bytes: usize = 0,
|
||||
|
||||
const alignment: std.mem.Alignment = .@"16";
|
||||
const VoidPtr = ?*align(alignment.toByteUnits()) anyopaque;
|
||||
const log = std.log.scoped(.stbi);
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Self {
|
||||
pub fn init(allocator: std.mem.Allocator, io: std.Io) Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.io = io,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,7 +79,7 @@ pub fn loadStaticBuf(self: *Self, comptime W: u32, comptime H: u32, buf: []const
|
||||
return .{ .data = @as(*const [W * H]vm.Color, @ptrCast(@alignCast(res))).* };
|
||||
}
|
||||
|
||||
pub fn loadStaticIo(self: *Self, comptime W: u32, comptime H: u32, reader: *std.io.Reader) !image.Static(W, H) {
|
||||
pub fn loadStaticIo(self: *Self, comptime W: u32, comptime H: u32, reader: *std.Io.Reader) !image.Static(W, H) {
|
||||
current_self = self;
|
||||
defer current_self = undefined;
|
||||
|
||||
@@ -109,7 +112,7 @@ pub fn loadDynamicBuf(self: *Self, buf: []const u8) !image.Dynamic {
|
||||
}
|
||||
|
||||
/// On success, must free memory by calling `freeDynamic` method.
|
||||
pub fn loadDynamicIo(self: *Self, reader: *std.io.Reader) !image.Dynamic {
|
||||
pub fn loadDynamicIo(self: *Self, reader: *std.Io.Reader) !image.Dynamic {
|
||||
current_self = self;
|
||||
defer current_self = undefined;
|
||||
|
||||
@@ -155,7 +158,7 @@ pub fn loadHdrBuf(self: *Self, buf: []const u8) !image.Hdr {
|
||||
}
|
||||
|
||||
/// On success, must free memory by calling `freeHdr` method.
|
||||
pub fn loadHdrIo(self: *Self, reader: *std.io.Reader) !image.Hdr {
|
||||
pub fn loadHdrIo(self: *Self, reader: *std.Io.Reader) !image.Hdr {
|
||||
current_self = self;
|
||||
defer current_self = undefined;
|
||||
|
||||
@@ -243,8 +246,8 @@ threadlocal var current_self: *Self = undefined;
|
||||
export fn castle_media_stbi_malloc(size: usize) callconv(.c) VoidPtr {
|
||||
const self = current_self;
|
||||
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
self.mutex.lock(self.io) catch return null;
|
||||
defer self.mutex.unlock(self.io);
|
||||
|
||||
self.allocations.ensureUnusedCapacity(self.allocator, 1) catch return null;
|
||||
const memory = self.allocator.alignedAlloc(u8, alignment, size) catch return null;
|
||||
@@ -259,8 +262,8 @@ export fn castle_media_stbi_malloc(size: usize) callconv(.c) VoidPtr {
|
||||
export fn castle_media_stbi_realloc(maybe_ptr: VoidPtr, size: usize) callconv(.c) VoidPtr {
|
||||
const self = current_self;
|
||||
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
self.mutex.lock(self.io) catch return null;
|
||||
defer self.mutex.unlock(self.io);
|
||||
|
||||
// NOTE If we were pedantic, we would consider the fact that we might not
|
||||
// need unused capacity if the memory doesn't get relocated.
|
||||
@@ -291,8 +294,8 @@ export fn castle_media_stbi_free(maybe_ptr: VoidPtr) callconv(.c) void {
|
||||
const self = current_self;
|
||||
|
||||
if (maybe_ptr) |ptr| {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
self.mutex.lockUncancelable(self.io);
|
||||
defer self.mutex.unlock(self.io);
|
||||
|
||||
const size = self.allocations.fetchRemove(ptr).?.value;
|
||||
self.allocated_bytes -= size;
|
||||
@@ -301,3 +304,7 @@ export fn castle_media_stbi_free(maybe_ptr: VoidPtr) callconv(.c) void {
|
||||
self.allocator.free(memory);
|
||||
}
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -11,11 +11,19 @@ pub fn build(b: *std.Build) void {
|
||||
|
||||
const sqlite_mod = sqlite_dep.module("sqlite");
|
||||
|
||||
const web_dep = b.dependency("web", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const web_mod = web_dep.module("web");
|
||||
|
||||
const myid_mod = b.addModule("myid", .{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.imports = &.{
|
||||
.{ .name = "sqlite", .module = sqlite_mod },
|
||||
.{ .name = "web", .module = web_mod },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -13,5 +13,8 @@
|
||||
.url = "git+https://github.com/vrischmann/zig-sqlite#6d90ee900d186a7fbb6066f28ee13beeaf8be345",
|
||||
.hash = "sqlite-3.48.0-F2R_a5yODgDFvwwsytm7ZONcSqYBo3qv1PmXOtw3tqLA",
|
||||
},
|
||||
.web = .{
|
||||
.path = "../web",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
259
packages/myid/src/data.zig
Normal file
259
packages/myid/src/data.zig
Normal file
@@ -0,0 +1,259 @@
|
||||
const std = @import("std");
|
||||
const sqlite = @import("sqlite");
|
||||
const web = @import("web");
|
||||
|
||||
const id = @import("id.zig");
|
||||
|
||||
pub const callback_regex = "^https?://([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}/";
|
||||
|
||||
pub const App = struct {
|
||||
aid: id.App,
|
||||
name: []const u8,
|
||||
callbacks: []const []const u8,
|
||||
};
|
||||
|
||||
pub const AppWithSecret = struct {
|
||||
aid: id.App,
|
||||
name: []const u8,
|
||||
callbacks: []const []const u8,
|
||||
secret: []const u8,
|
||||
};
|
||||
|
||||
pub const CreateAppData = struct {
|
||||
name: []const u8,
|
||||
callback: ?[]const []const u8,
|
||||
};
|
||||
|
||||
pub const CreateAppResult = struct {
|
||||
aid: id.App,
|
||||
plainSecret: []const u8,
|
||||
};
|
||||
|
||||
pub const User = struct {
|
||||
uid: id.User,
|
||||
name: []const u8,
|
||||
email: []const u8,
|
||||
admin: bool,
|
||||
};
|
||||
|
||||
pub const UserWithPassword = struct {
|
||||
uid: id.User,
|
||||
name: []const u8,
|
||||
email: []const u8,
|
||||
admin: bool,
|
||||
passworD: []const u8,
|
||||
};
|
||||
|
||||
pub const InviteUserData = struct {
|
||||
name: []const u8,
|
||||
email: []const u8,
|
||||
admin: bool,
|
||||
};
|
||||
|
||||
pub const CreateUserData = struct {
|
||||
name: []const u8,
|
||||
email: []const u8,
|
||||
plainPassword: []const u8,
|
||||
admin: bool,
|
||||
};
|
||||
|
||||
pub const All = struct {
|
||||
users: []const User,
|
||||
apps: []const App,
|
||||
};
|
||||
|
||||
pub const Database = struct {
|
||||
db: sqlite.Db,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn init(path: [:0]const u8, allocator: std.mem.Allocator) !Database {
|
||||
var db = try sqlite.Db.init(.{
|
||||
.mode = .{ .File = path },
|
||||
.open_flags = .{
|
||||
.create = true,
|
||||
.write = true,
|
||||
},
|
||||
.threading_mode = .MultiThread,
|
||||
});
|
||||
errdefer db.deinit();
|
||||
|
||||
_ = try db.one(void, "PRAGMA journal_mode = WAL", .{}, .{});
|
||||
_ = try db.one(void, "PRAGMA foreign_keys = ON", .{}, .{});
|
||||
|
||||
return .{
|
||||
.db = db,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Database) void {
|
||||
self.db.deinit();
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
// --- MIGRATION -----------------------------------------------------------
|
||||
|
||||
fn getUserVersion(self: *Database) !i32 {
|
||||
const version = try self.db.one(i32, "PRAGMA user_version", .{}, .{});
|
||||
return version.?;
|
||||
}
|
||||
|
||||
fn setUserVersion(self: *Database, version: i32) !void {
|
||||
var buf: [100]u8 = undefined;
|
||||
const query = std.fmt.bufPrint(&buf, "PRAGMA user_version = {d}", .{version}) catch unreachable;
|
||||
_ = try self.db.oneDynamic(void, query, .{}, .{});
|
||||
}
|
||||
|
||||
pub fn migrate(self: *Database) !void {
|
||||
var user_version = try self.getUserVersion();
|
||||
|
||||
if (user_version == 0) {
|
||||
_ = try self.db.exec(
|
||||
\\CREATE TABLE users (
|
||||
\\ uid BLOB NOT NULL,
|
||||
\\ name TEXT NOT NULL,
|
||||
\\ email TEXT NOT NULL UNIQUE,
|
||||
\\ password TEXT NOT NULL,
|
||||
\\ PRIMARY KEY (uid)
|
||||
\\)
|
||||
, .{}, .{});
|
||||
_ = try self.db.exec(
|
||||
\\CREATE TABLE apps (
|
||||
\\ aid BLOB NOT NULL,
|
||||
\\ name TEXT NOT NULL,
|
||||
\\ secret TEXT NOT NULL,
|
||||
\\ PRIMARY KEY (aid)
|
||||
\\)
|
||||
, .{}, .{});
|
||||
_ = try self.db.exec(
|
||||
\\CREATE TABLE app_callbacks (
|
||||
\\ aid BLOB NOT NULL,
|
||||
\\ callback TEXT NOT NULL,
|
||||
\\ PRIMARY KEY (aid, callback),
|
||||
\\ FOREIGN KEY (aid) REFERENCES apps (aid)
|
||||
\\ ON UPDATE CASCADE
|
||||
\\ ON DELETE CASCADE
|
||||
\\)
|
||||
, .{}, .{});
|
||||
user_version += 1;
|
||||
try self.setUserVersion(user_version);
|
||||
}
|
||||
}
|
||||
|
||||
// --- USERS ---------------------------------------------------------------
|
||||
|
||||
pub fn createUser(self: *Database, name: []const u8, email: []const u8, plain_password: []const u8) !id.User {
|
||||
const uid = id.User.init(web.UUID.v7());
|
||||
var password_buf: [1000]u8 = undefined;
|
||||
const password = try std.crypto.pwhash.argon2.strHash(plain_password, .{
|
||||
.allocator = self.allocator,
|
||||
.mode = .argon2id,
|
||||
.params = .owasp_2id,
|
||||
}, &password_buf);
|
||||
|
||||
try self.db.exec("INSERT INTO users (uid, name, email, password) VALUES (?, ?, ?, ?)", .{}, .{
|
||||
.uid = uid.bytes,
|
||||
.name = name,
|
||||
.email = email,
|
||||
.password = password,
|
||||
});
|
||||
|
||||
return uid;
|
||||
}
|
||||
|
||||
// --- APPS ----------------------------------------------------------------
|
||||
|
||||
pub fn createApp(self: *Database, name: []const u8, callbacks: []const []const u8) !CreateAppResult {
|
||||
_ = try self.db.exec("BEGIN", .{}, .{});
|
||||
errdefer self.db.exec("ROLLBACK", .{}, .{}) catch unreachable;
|
||||
|
||||
const secret_bytes = blk: {
|
||||
var bytes: [24]u8 = undefined;
|
||||
std.crypto.random.bytes(&bytes);
|
||||
break :blk bytes;
|
||||
};
|
||||
|
||||
var plain_secret_buf: [32]u8 = undefined;
|
||||
const plain_secret = std.base64.url_safe_no_pad.Encoder.encode(&plain_secret_buf, &secret_bytes);
|
||||
std.debug.assert(plain_secret_buf.len == plain_secret.len);
|
||||
|
||||
const aid = id.App.init(web.UUID.v7());
|
||||
var secret_buf: [1000]u8 = undefined;
|
||||
const secret = try std.crypto.pwhash.argon2.strHash(plain_secret, .{
|
||||
.allocator = self.allocator,
|
||||
.mode = .argon2id,
|
||||
.params = .owasp_2id,
|
||||
}, &secret_buf);
|
||||
|
||||
try self.db.exec("INSERT INTO apps (aid, name, secret) VALUES (?, ?, ?)", .{}, .{
|
||||
.aid = aid.bytes,
|
||||
.name = name,
|
||||
.secret = secret,
|
||||
});
|
||||
|
||||
var insert_callback = try self.db.prepare("INSERT INTO app_callbacks (aid, callback) VALUES (?, ?)");
|
||||
defer insert_callback.deinit();
|
||||
for (callbacks) |callback| {
|
||||
try insert_callback.exec(.{}, .{ .aid = aid.bytes, .callback = callback });
|
||||
insert_callback.reset();
|
||||
}
|
||||
|
||||
try self.db.exec("COMMIT", .{}, .{});
|
||||
return .{
|
||||
.aid = aid,
|
||||
.plain_secret = plain_secret_buf,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
test "user version" {
|
||||
var db = try Database.init(":memory:", std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try std.testing.expectEqual(0, try db.getUserVersion());
|
||||
try db.setUserVersion(1);
|
||||
try std.testing.expectEqual(1, try db.getUserVersion());
|
||||
}
|
||||
|
||||
test "migrate" {
|
||||
var db = try Database.init(":memory:", std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try db.migrate();
|
||||
}
|
||||
|
||||
test "create user" {
|
||||
var db = try Database.init(":memory:", std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try db.migrate();
|
||||
|
||||
const uid = try db.createUser("admin", "admin@test.invalid", "admin");
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
const maybe_user = try db.db.oneAlloc(struct {
|
||||
name: []const u8,
|
||||
email: []const u8,
|
||||
password: []const u8,
|
||||
}, arena.allocator(), "SELECT name, email, password FROM users WHERE uid = ?", .{}, .{
|
||||
.uid = uid.bytes,
|
||||
});
|
||||
defer arena.deinit();
|
||||
|
||||
try std.testing.expect(maybe_user != null);
|
||||
if (maybe_user) |user| {
|
||||
try std.testing.expectEqualSlices(u8, "admin", user.name);
|
||||
}
|
||||
}
|
||||
|
||||
test "create app" {
|
||||
var db = try Database.init(":memory:", std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try db.migrate();
|
||||
|
||||
_ = try db.createApp("app", &.{
|
||||
"http://localhost:3000/callback",
|
||||
"https://example.com/callback",
|
||||
});
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub const database_path = "db.sqlite3";
|
||||
pub const socket_path = "myid.sock";
|
||||
@@ -1,206 +0,0 @@
|
||||
const std = @import("std");
|
||||
const main = @import("main.zig");
|
||||
|
||||
const Parser = @import("http/Parser.zig");
|
||||
|
||||
threadlocal var read_buffer: [2 * 1024 * 1024]u8 = undefined;
|
||||
threadlocal var write_buffer: [2 * 1024 * 1024]u8 = undefined;
|
||||
|
||||
const log = std.log.scoped(.http);
|
||||
|
||||
const status = struct {
|
||||
pub const ok = "HTTP/1.1 200 OK\r\n";
|
||||
pub const created = "HTTP/1.1 201 Created\r\n";
|
||||
pub const accepted = "HTTP/1.1 202 Accepted\r\n";
|
||||
pub const non_authoritative_information = "HTTP/1.1 203 Non-Authoritative Information\r\n";
|
||||
pub const no_content = "HTTP/1.1 204 No Content\r\n";
|
||||
pub const reset_content = "HTTP/1.1 205 Reset Content\r\n";
|
||||
pub const partial_content = "HTTP/1.1 206 Partial Content\r\n";
|
||||
pub const multi_status = "HTTP/1.1 207 Multi-Status\r\n";
|
||||
pub const already_reported = "HTTP/1.1 208 Already Reported\r\n";
|
||||
|
||||
pub const multiple_choices = "HTTP/1.1 300 Multiple Choices\r\n";
|
||||
pub const moved_permanently = "HTTP/1.1 301 Moved Permanently\r\n";
|
||||
pub const found = "HTTP/1.1 302 Found\r\n";
|
||||
pub const see_other = "HTTP/1.1 303 See Other\r\n";
|
||||
pub const not_modified = "HTTP/1.1 304 Not Modified\r\n";
|
||||
pub const temporary_redirect = "HTTP/1.1 307 Temporary Redirect\r\n";
|
||||
pub const permanent_redirect = "HTTP/1.1 308 Permanent Redirect\r\n";
|
||||
|
||||
pub const bad_request = "HTTP/1.1 400 Bad Request\r\n";
|
||||
pub const unauthorized = "HTTP/1.1 401 Unauthorized\r\n";
|
||||
pub const payment_required = "HTTP/1.1 402 Payment Required\r\n";
|
||||
pub const forbidden = "HTTP/1.1 403 Forbidden\r\n";
|
||||
pub const not_found = "HTTP/1.1 404 Not Found\r\n";
|
||||
pub const method_not_allowed = "HTTP/1.1 405 Method Not Allowed\r\n";
|
||||
pub const not_acceptable = "HTTP/1.1 406 Not Acceptable\r\n";
|
||||
pub const proxy_authentication_required = "HTTP/1.1 407 Proxy Authentication Required\r\n";
|
||||
pub const request_timeout = "HTTP/1.1 408 Request Timeout\r\n";
|
||||
pub const conflict = "HTTP/1.1 409 Conflict\r\n";
|
||||
pub const gone = "HTTP/1.1 410 Gone\r\n";
|
||||
pub const length_required = "HTTP/1.1 411 Length Required\r\n";
|
||||
pub const precondition_failed = "HTTP/1.1 412 Precondition Failed\r\n";
|
||||
pub const content_too_large = "HTTP/1.1 413 Content Too Large\r\n";
|
||||
pub const uri_too_long = "HTTP/1.1 414 URI Too Long\r\n";
|
||||
pub const unsupported_media_type = "HTTP/1.1 415 Unsupported Media Type\r\n";
|
||||
pub const range_not_satisfiable = "HTTP/1.1 416 Range Not Satisfiable\r\n";
|
||||
pub const expectation_failed = "HTTP/1.1 417 Expectation Failed\r\n";
|
||||
pub const im_a_teapot = "HTTP/1.1 418 I'm a teapot\r\n";
|
||||
pub const misdirected_request = "HTTP/1.1 421 Misdirected Request\r\n";
|
||||
pub const unprocessable_content = "HTTP/1.1 422 Unprocessable Content\r\n";
|
||||
pub const locked = "HTTP/1.1 423 Locked\r\n";
|
||||
pub const failed_dependency = "HTTP/1.1 424 Failed Dependency\r\n";
|
||||
pub const upgrade_required = "HTTP/1.1 426 Upgrade Required\r\n";
|
||||
pub const precondition_required = "HTTP/1.1 428 Precondition Required\r\n";
|
||||
pub const too_many_requests = "HTTP/1.1 429 Too Many Requests\r\n";
|
||||
pub const request_header_fields_too_large = "HTTP/1.1 431 Request Header Fields Too Large\r\n";
|
||||
pub const unavailable_for_legal_reasons = "HTTP/1.1 451 Unavailable For Legal Reasons\r\n";
|
||||
|
||||
pub const internal_server_error = "HTTP/1.1 500 Internal Server Error\r\n";
|
||||
pub const not_implemented = "HTTP/1.1 501 Not Implemented\r\n";
|
||||
pub const bad_gateway = "HTTP/1.1 502 Bad Gateway\r\n";
|
||||
pub const service_unavailable = "HTTP/1.1 503 Service Unavailable\r\n";
|
||||
pub const gateway_timeout = "HTTP/1.1 504 Gateway Timeout\r\n";
|
||||
pub const http_version_not_supported = "HTTP/1.1 505 HTTP Version Not Supported\r\n";
|
||||
pub const variant_also_negotiates = "HTTP/1.1 506 Variant Also Negotiates\r\n";
|
||||
pub const insufficient_storage = "HTTP/1.1 507 Insufficient Storage\r\n";
|
||||
pub const loop_detected = "HTTP/1.1 508 Loop Detected\r\n";
|
||||
pub const not_extended = "HTTP/1.1 510 Not Extended\r\n";
|
||||
pub const network_authentication_required = "HTTP/1.1 511 Network Authentication Required\r\n";
|
||||
};
|
||||
|
||||
const ResponseEmptyOptions = struct {
|
||||
status_text: []const u8 = status.ok,
|
||||
};
|
||||
|
||||
const ResponseOptions = struct {
|
||||
status_text: []const u8 = status.ok,
|
||||
media_type: []const u8 = "text/plain; charset=utf-8",
|
||||
response_body: []const u8,
|
||||
};
|
||||
|
||||
fn makeResponseEmpty(options: ResponseEmptyOptions) ![]const u8 {
|
||||
var fbs = std.io.fixedBufferStream(&write_buffer);
|
||||
const writer = fbs.writer();
|
||||
|
||||
try writer.print("{s}", .{options.status_text});
|
||||
try writer.print("\r\n", .{});
|
||||
|
||||
return fbs.getWritten();
|
||||
}
|
||||
|
||||
fn makeResponseClose(options: ResponseEmptyOptions) ![]const u8 {
|
||||
var fbs = std.io.fixedBufferStream(&write_buffer);
|
||||
const writer = fbs.writer();
|
||||
|
||||
try writer.print("{s}", .{options.status_text});
|
||||
try writer.print("Connection: close\r\n", .{});
|
||||
try writer.print("\r\n", .{});
|
||||
|
||||
return fbs.getWritten();
|
||||
}
|
||||
|
||||
fn makeResponse(options: ResponseOptions) ![]const u8 {
|
||||
var fbs = std.io.fixedBufferStream(&write_buffer);
|
||||
const writer = fbs.writer();
|
||||
|
||||
try writer.print("{s}", .{options.status_text});
|
||||
try writer.print("Content-Type: {s}\r\n", .{options.media_type});
|
||||
try writer.print("Content-Length: {d}\r\n", .{options.response_body.len});
|
||||
try writer.print("\r\n", .{});
|
||||
try writer.print("{s}", .{options.response_body});
|
||||
|
||||
return fbs.getWritten();
|
||||
}
|
||||
|
||||
pub fn process(conn: std.net.Server.Connection) !void {
|
||||
defer conn.stream.close();
|
||||
|
||||
var leftover_bytes: usize = 0;
|
||||
|
||||
while (true) {
|
||||
const start = try std.time.Instant.now();
|
||||
|
||||
var route: Parser.Route = undefined;
|
||||
|
||||
var parser = Parser.init(.{
|
||||
.self = &route,
|
||||
.route = routeCallback,
|
||||
});
|
||||
var total_bytes_read: usize = 0;
|
||||
|
||||
while (true) {
|
||||
var bytes_read: usize = undefined;
|
||||
var chars: []const u8 = undefined;
|
||||
|
||||
if (leftover_bytes > 0) {
|
||||
bytes_read = leftover_bytes;
|
||||
chars = read_buffer[0..leftover_bytes];
|
||||
leftover_bytes = 0;
|
||||
} else {
|
||||
bytes_read = try conn.stream.read(read_buffer[total_bytes_read..]);
|
||||
chars = read_buffer[total_bytes_read .. total_bytes_read + bytes_read];
|
||||
}
|
||||
|
||||
total_bytes_read += bytes_read;
|
||||
|
||||
const res = parser.consume(chars) catch |err| switch (err) {
|
||||
error.MethodNotSupported => {
|
||||
const response = try makeResponseClose(.{ .status_text = status.method_not_allowed });
|
||||
try conn.stream.writeAll(response);
|
||||
return;
|
||||
},
|
||||
error.HttpVersionNotSupported => {
|
||||
const response = try makeResponseClose(.{ .status_text = status.http_version_not_supported });
|
||||
try conn.stream.writeAll(response);
|
||||
return;
|
||||
},
|
||||
error.MissingLineFeed => {
|
||||
const response = try makeResponseClose(.{ .status_text = status.bad_request });
|
||||
try conn.stream.writeAll(response);
|
||||
return;
|
||||
},
|
||||
error.InvalidContentLength => {
|
||||
const response = try makeResponseClose(.{ .status_text = status.bad_request });
|
||||
try conn.stream.writeAll(response);
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
if (total_bytes_read >= read_buffer.len and !res.done) {
|
||||
if (parser.state == .body) {
|
||||
const response = try makeResponseClose(.{ .status_text = status.content_too_large });
|
||||
try conn.stream.writeAll(response);
|
||||
return;
|
||||
} else {
|
||||
const response = try makeResponseClose(.{ .status_text = status.request_header_fields_too_large });
|
||||
try conn.stream.writeAll(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.done) {
|
||||
leftover_bytes = bytes_read - res.consumed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const response = try makeResponse(.{ .response_body = "PONG\n" });
|
||||
|
||||
try conn.stream.writeAll(response);
|
||||
|
||||
if (leftover_bytes > 0) {
|
||||
@memmove(&read_buffer, read_buffer[total_bytes_read - leftover_bytes .. total_bytes_read]);
|
||||
}
|
||||
|
||||
const end = try std.time.Instant.now();
|
||||
const time_ns = end.since(start);
|
||||
const time_us = @divFloor(time_ns, std.time.ns_per_us);
|
||||
|
||||
log.info("{s} {s} ({} μs)", .{ @tagName(route.method), route.pathname, time_us });
|
||||
}
|
||||
}
|
||||
|
||||
fn routeCallback(self: ?*anyopaque, route: Parser.Route) void {
|
||||
@as(*Parser.Route, @alignCast(@ptrCast(self))).* = route;
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Parser = @This();
|
||||
|
||||
const Callbacks = struct {
|
||||
self: ?*anyopaque = null,
|
||||
|
||||
route: ?*const fn (self: ?*anyopaque, route: Route) void = null,
|
||||
header: ?*const fn (self: ?*anyopaque, name: []const u8, value: []const u8) void = null,
|
||||
body: ?*const fn (self: ?*anyopaque, body: []const u8) void = null,
|
||||
|
||||
pub const init: Callbacks = .{};
|
||||
};
|
||||
|
||||
const Error = error{
|
||||
MethodNotSupported,
|
||||
HttpVersionNotSupported,
|
||||
MissingLineFeed,
|
||||
InvalidContentLength,
|
||||
};
|
||||
|
||||
const State = union(enum) {
|
||||
pub fn methodComplete(method: Method) State {
|
||||
return .{
|
||||
.method_complete = .{
|
||||
.method = method,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn pathname(method: Method, p: []const u8) State {
|
||||
return .{
|
||||
.pathname_state = .{
|
||||
.method = method,
|
||||
.pathname = p,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn headerValue(name: []const u8, value: []const u8) State {
|
||||
return .{
|
||||
.header_value = .{
|
||||
.name = name,
|
||||
.value = value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
init: void,
|
||||
method_d: void,
|
||||
method_g: void,
|
||||
method_h: void,
|
||||
method_p: void,
|
||||
method_de: void,
|
||||
method_ge: void,
|
||||
method_he: void,
|
||||
method_pa: void,
|
||||
method_po: void,
|
||||
method_pu: void,
|
||||
method_del: void,
|
||||
method_hea: void,
|
||||
method_pat: void,
|
||||
method_pos: void,
|
||||
method_dele: void,
|
||||
method_patc: void,
|
||||
method_delet: void,
|
||||
method_complete: struct { method: Method },
|
||||
pathname_state: struct { method: Method, pathname: []const u8 },
|
||||
pathname_complete: void,
|
||||
version_h: void,
|
||||
version_ht: void,
|
||||
version_htt: void,
|
||||
version_http: void,
|
||||
@"version_http/@": void,
|
||||
@"version_http/1@": void,
|
||||
@"version_http/1.@": void,
|
||||
version_complete: void,
|
||||
start_line_end: void,
|
||||
header_name_start: void,
|
||||
header_name: []const u8,
|
||||
header_value: struct { name: []const u8, value: []const u8 },
|
||||
header_line_end: void,
|
||||
headers_end: void,
|
||||
body: []const u8,
|
||||
};
|
||||
|
||||
const ConsumeResult = struct {
|
||||
consumed: usize,
|
||||
done: bool,
|
||||
};
|
||||
|
||||
const ConsumeCharResult = enum {
|
||||
not_done,
|
||||
done,
|
||||
};
|
||||
|
||||
pub const Method = enum {
|
||||
DELETE,
|
||||
GET,
|
||||
HEAD,
|
||||
PATCH,
|
||||
POST,
|
||||
PUT,
|
||||
};
|
||||
|
||||
pub const Route = struct {
|
||||
method: Method,
|
||||
pathname: []const u8,
|
||||
};
|
||||
|
||||
callbacks: Callbacks,
|
||||
state: State,
|
||||
current_header_is_content_length: bool,
|
||||
content_length: usize,
|
||||
|
||||
pub fn init(callbacks: Callbacks) Parser {
|
||||
return .{
|
||||
.callbacks = callbacks,
|
||||
.state = .init,
|
||||
.current_header_is_content_length = false,
|
||||
.content_length = 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
|
||||
var i: usize = 0;
|
||||
while (i < chars.len) {
|
||||
switch (self.state) {
|
||||
.body => |body| {
|
||||
const to_consume = @min(chars.len - i, self.content_length - body.len);
|
||||
const new_body = body.ptr[0 .. body.len + to_consume];
|
||||
self.state = .{ .body = new_body };
|
||||
i += to_consume;
|
||||
|
||||
const done = new_body.len >= self.content_length;
|
||||
|
||||
if (done) {
|
||||
if (self.callbacks.body) |bodyCallback| {
|
||||
bodyCallback(self.callbacks.self, new_body);
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.consumed = i,
|
||||
.done = done,
|
||||
};
|
||||
},
|
||||
else => {
|
||||
const res = try self.consumeChar(&chars[i]);
|
||||
i += 1;
|
||||
if (res == .done) return .{
|
||||
.consumed = i,
|
||||
.done = true,
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.consumed = chars.len,
|
||||
.done = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
|
||||
const c = c_ptr.*;
|
||||
const c_slice = @as([*]const u8, @ptrCast(c_ptr))[0..1];
|
||||
switch (self.state) {
|
||||
.init => switch (c) {
|
||||
'D' => self.state = .method_d,
|
||||
'G' => self.state = .method_g,
|
||||
'H' => self.state = .method_h,
|
||||
'P' => self.state = .method_p,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_d => switch (c) {
|
||||
'E' => self.state = .method_de,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_g => switch (c) {
|
||||
'E' => self.state = .method_ge,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_h => switch (c) {
|
||||
'E' => self.state = .method_he,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_p => switch (c) {
|
||||
'A' => self.state = .method_pa,
|
||||
'O' => self.state = .method_po,
|
||||
'U' => self.state = .method_pu,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_de => switch (c) {
|
||||
'L' => self.state = .method_del,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_ge => switch (c) {
|
||||
'T' => self.state = .methodComplete(.GET),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_he => switch (c) {
|
||||
'A' => self.state = .method_hea,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_pa => switch (c) {
|
||||
'T' => self.state = .method_pat,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_po => switch (c) {
|
||||
'S' => self.state = .method_pos,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_pu => switch (c) {
|
||||
'T' => self.state = .methodComplete(.PUT),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_del => switch (c) {
|
||||
'E' => self.state = .method_dele,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_hea => switch (c) {
|
||||
'D' => self.state = .methodComplete(.HEAD),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_pat => switch (c) {
|
||||
'C' => self.state = .method_patc,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_pos => switch (c) {
|
||||
'T' => self.state = .methodComplete(.POST),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_dele => switch (c) {
|
||||
'T' => self.state = .method_delet,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_patc => switch (c) {
|
||||
'H' => self.state = .methodComplete(.PATCH),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_delet => switch (c) {
|
||||
'E' => self.state = .methodComplete(.DELETE),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_complete => |s| switch (c) {
|
||||
' ' => self.state = .pathname(s.method, @as([*]const u8, @ptrCast(c_ptr))[1..1]),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.pathname_state => |s| switch (c) {
|
||||
' ' => {
|
||||
self.state = .pathname_complete;
|
||||
if (self.callbacks.route) |routeCallback| {
|
||||
routeCallback(self.callbacks.self, .{
|
||||
.method = s.method,
|
||||
.pathname = s.pathname,
|
||||
});
|
||||
}
|
||||
},
|
||||
else => self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + 1]),
|
||||
},
|
||||
.pathname_complete => switch (c) {
|
||||
'H' => self.state = .version_h,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.version_h => switch (c) {
|
||||
'T' => self.state = .version_ht,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.version_ht => switch (c) {
|
||||
'T' => self.state = .version_htt,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.version_htt => switch (c) {
|
||||
'P' => self.state = .version_http,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.version_http => switch (c) {
|
||||
'/' => self.state = .@"version_http/@",
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.@"version_http/@" => switch (c) {
|
||||
'1' => self.state = .@"version_http/1@",
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.@"version_http/1@" => switch (c) {
|
||||
'.' => self.state = .@"version_http/1.@",
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.@"version_http/1.@" => switch (c) {
|
||||
'1' => self.state = .version_complete,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.version_complete => switch (c) {
|
||||
'\r' => self.state = .start_line_end,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.start_line_end => switch (c) {
|
||||
'\n' => self.state = .header_name_start,
|
||||
else => return error.MissingLineFeed,
|
||||
},
|
||||
.header_name_start => switch (c) {
|
||||
'\r' => self.state = .headers_end,
|
||||
else => self.state = .{ .header_name = c_slice },
|
||||
},
|
||||
.header_name => |name| switch (c) {
|
||||
':' => {
|
||||
self.state = .headerValue(name, @as([*]const u8, @ptrCast(c_ptr))[1..1]);
|
||||
self.current_header_is_content_length = std.ascii.eqlIgnoreCase(name, "Content-Length");
|
||||
},
|
||||
else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] },
|
||||
},
|
||||
.header_value => |s| switch (c) {
|
||||
'\r' => {
|
||||
self.state = .header_line_end;
|
||||
const value_trimmed = std.mem.trim(u8, s.value, " \t");
|
||||
if (self.current_header_is_content_length) {
|
||||
self.content_length = std.fmt.parseInt(usize, value_trimmed, 10) catch return error.InvalidContentLength;
|
||||
self.current_header_is_content_length = false;
|
||||
}
|
||||
if (self.callbacks.header) |headerCallback| {
|
||||
headerCallback(self.callbacks.self, s.name, value_trimmed);
|
||||
}
|
||||
},
|
||||
else => self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + 1]),
|
||||
},
|
||||
.header_line_end => switch (c) {
|
||||
'\n' => self.state = .header_name_start,
|
||||
else => return error.MissingLineFeed,
|
||||
},
|
||||
.headers_end => switch (c) {
|
||||
'\n' => {
|
||||
if (self.content_length == 0) {
|
||||
if (self.callbacks.body) |bodyCallback| {
|
||||
bodyCallback(self.callbacks.self, &.{});
|
||||
}
|
||||
return .done;
|
||||
}
|
||||
self.state = .{ .body = @as([*]const u8, @ptrCast(c_ptr))[1..1] };
|
||||
},
|
||||
else => return error.MissingLineFeed,
|
||||
},
|
||||
.body => |body| {
|
||||
const new_body = body.ptr[0 .. body.len + 1];
|
||||
self.state = .{ .body = new_body };
|
||||
if (new_body.len >= self.content_length) {
|
||||
if (self.callbacks.body) |bodyCallback| {
|
||||
bodyCallback(self.callbacks.self, new_body);
|
||||
}
|
||||
return .done;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return .not_done;
|
||||
}
|
||||
6
packages/myid/src/id.zig
Normal file
6
packages/myid/src/id.zig
Normal file
@@ -0,0 +1,6 @@
|
||||
const std = @import("std");
|
||||
const web = @import("web");
|
||||
|
||||
pub const App = web.Id(.app);
|
||||
pub const Session = web.Id(.session);
|
||||
pub const User = web.Id(.user);
|
||||
@@ -1,235 +1,3 @@
|
||||
const std = @import("std");
|
||||
const sqlite = @import("sqlite");
|
||||
const uuid = @import("uuid.zig");
|
||||
|
||||
fn Id(comptime _tag: @Type(.enum_literal)) type {
|
||||
return struct {
|
||||
pub const tag = _tag;
|
||||
|
||||
bytes: [16]u8,
|
||||
|
||||
pub fn new() @This() {
|
||||
return .{ .bytes = uuid.uuid_v7() };
|
||||
}
|
||||
|
||||
pub fn eql(a: @This(), b: @This()) bool {
|
||||
return std.mem.eql(u8, &a.bytes, &b.bytes);
|
||||
}
|
||||
|
||||
pub fn decode(encoded: *const [22]u8) !@This() {
|
||||
var bytes: [16]u8 = undefined;
|
||||
try std.base64.url_safe_no_pad.Decoder.decode(&bytes, encoded);
|
||||
return .{ .bytes = bytes };
|
||||
}
|
||||
|
||||
pub fn encode(self: @This()) [22]u8 {
|
||||
var text: [22]u8 = undefined;
|
||||
std.base64.url_safe_no_pad.Encoder.encode(&text, self.bytes);
|
||||
return text;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const AppId = Id(.app_id);
|
||||
pub const UserId = Id(.user_id);
|
||||
|
||||
pub const CreateAppResult = struct {
|
||||
aid: AppId,
|
||||
plain_secret: [32]u8,
|
||||
};
|
||||
|
||||
pub const Database = struct {
|
||||
db: sqlite.Db,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn init(path: [:0]const u8, allocator: std.mem.Allocator) !Database {
|
||||
var db = try sqlite.Db.init(.{
|
||||
.mode = .{ .File = path },
|
||||
.open_flags = .{
|
||||
.create = true,
|
||||
.write = true,
|
||||
},
|
||||
.threading_mode = .MultiThread,
|
||||
});
|
||||
errdefer db.deinit();
|
||||
|
||||
_ = try db.one(void, "PRAGMA journal_mode = WAL", .{}, .{});
|
||||
_ = try db.one(void, "PRAGMA foreign_keys = ON", .{}, .{});
|
||||
|
||||
return .{
|
||||
.db = db,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Database) void {
|
||||
self.db.deinit();
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
// --- MIGRATION -----------------------------------------------------------
|
||||
|
||||
fn getUserVersion(self: *Database) !i32 {
|
||||
const version = try self.db.one(i32, "PRAGMA user_version", .{}, .{});
|
||||
return version.?;
|
||||
}
|
||||
|
||||
fn setUserVersion(self: *Database, version: i32) !void {
|
||||
var buf: [100]u8 = undefined;
|
||||
const query = std.fmt.bufPrint(&buf, "PRAGMA user_version = {d}", .{version}) catch unreachable;
|
||||
_ = try self.db.oneDynamic(void, query, .{}, .{});
|
||||
}
|
||||
|
||||
pub fn migrate(self: *Database) !void {
|
||||
var user_version = try self.getUserVersion();
|
||||
|
||||
if (user_version == 0) {
|
||||
_ = try self.db.exec(
|
||||
\\CREATE TABLE users (
|
||||
\\ uid BLOB NOT NULL,
|
||||
\\ name TEXT NOT NULL,
|
||||
\\ email TEXT NOT NULL UNIQUE,
|
||||
\\ password TEXT NOT NULL,
|
||||
\\ PRIMARY KEY (uid)
|
||||
\\)
|
||||
, .{}, .{});
|
||||
_ = try self.db.exec(
|
||||
\\CREATE TABLE apps (
|
||||
\\ aid BLOB NOT NULL,
|
||||
\\ name TEXT NOT NULL,
|
||||
\\ secret TEXT NOT NULL,
|
||||
\\ PRIMARY KEY (aid)
|
||||
\\)
|
||||
, .{}, .{});
|
||||
_ = try self.db.exec(
|
||||
\\CREATE TABLE app_callbacks (
|
||||
\\ aid BLOB NOT NULL,
|
||||
\\ callback TEXT NOT NULL,
|
||||
\\ PRIMARY KEY (aid, callback),
|
||||
\\ FOREIGN KEY (aid) REFERENCES apps (aid)
|
||||
\\ ON UPDATE CASCADE
|
||||
\\ ON DELETE CASCADE
|
||||
\\)
|
||||
, .{}, .{});
|
||||
user_version += 1;
|
||||
try self.setUserVersion(user_version);
|
||||
}
|
||||
}
|
||||
|
||||
// --- USERS ---------------------------------------------------------------
|
||||
|
||||
pub fn createUser(self: *Database, name: []const u8, email: []const u8, plain_password: []const u8) !UserId {
|
||||
const uid = UserId.new();
|
||||
var password_buf: [1000]u8 = undefined;
|
||||
const password = try std.crypto.pwhash.argon2.strHash(plain_password, .{
|
||||
.allocator = self.allocator,
|
||||
.mode = .argon2id,
|
||||
.params = .owasp_2id,
|
||||
}, &password_buf);
|
||||
|
||||
try self.db.exec("INSERT INTO users (uid, name, email, password) VALUES (?, ?, ?, ?)", .{}, .{
|
||||
.uid = uid.bytes,
|
||||
.name = name,
|
||||
.email = email,
|
||||
.password = password,
|
||||
});
|
||||
|
||||
return uid;
|
||||
}
|
||||
|
||||
// --- APPS ----------------------------------------------------------------
|
||||
|
||||
pub fn createApp(self: *Database, name: []const u8, callbacks: []const []const u8) !CreateAppResult {
|
||||
_ = try self.db.exec("BEGIN", .{}, .{});
|
||||
errdefer self.db.exec("ROLLBACK", .{}, .{}) catch unreachable;
|
||||
|
||||
const secret_bytes = blk: {
|
||||
var bytes: [24]u8 = undefined;
|
||||
std.crypto.random.bytes(&bytes);
|
||||
break :blk bytes;
|
||||
};
|
||||
|
||||
var plain_secret_buf: [32]u8 = undefined;
|
||||
const plain_secret = std.base64.url_safe_no_pad.Encoder.encode(&plain_secret_buf, &secret_bytes);
|
||||
std.debug.assert(plain_secret_buf.len == plain_secret.len);
|
||||
|
||||
const aid = AppId.new();
|
||||
var secret_buf: [1000]u8 = undefined;
|
||||
const secret = try std.crypto.pwhash.argon2.strHash(plain_secret, .{
|
||||
.allocator = self.allocator,
|
||||
.mode = .argon2id,
|
||||
.params = .owasp_2id,
|
||||
}, &secret_buf);
|
||||
|
||||
try self.db.exec("INSERT INTO apps (aid, name, secret) VALUES (?, ?, ?)", .{}, .{
|
||||
.aid = aid.bytes,
|
||||
.name = name,
|
||||
.secret = secret,
|
||||
});
|
||||
|
||||
var insert_callback = try self.db.prepare("INSERT INTO app_callbacks (aid, callback) VALUES (?, ?)");
|
||||
defer insert_callback.deinit();
|
||||
for (callbacks) |callback| {
|
||||
try insert_callback.exec(.{}, .{ .aid = aid.bytes, .callback = callback });
|
||||
insert_callback.reset();
|
||||
}
|
||||
|
||||
try self.db.exec("COMMIT", .{}, .{});
|
||||
return .{
|
||||
.aid = aid,
|
||||
.plain_secret = plain_secret_buf,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
test "user version" {
|
||||
var db = try Database.init(":memory:", std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try std.testing.expectEqual(0, try db.getUserVersion());
|
||||
try db.setUserVersion(1);
|
||||
try std.testing.expectEqual(1, try db.getUserVersion());
|
||||
}
|
||||
|
||||
test "migrate" {
|
||||
var db = try Database.init(":memory:", std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try db.migrate();
|
||||
}
|
||||
|
||||
test "create user" {
|
||||
var db = try Database.init(":memory:", std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try db.migrate();
|
||||
|
||||
const uid = try db.createUser("admin", "admin@test.invalid", "admin");
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
const maybe_user = try db.db.oneAlloc(struct {
|
||||
name: []const u8,
|
||||
email: []const u8,
|
||||
password: []const u8,
|
||||
}, arena.allocator(), "SELECT name, email, password FROM users WHERE uid = ?", .{}, .{
|
||||
.uid = uid.bytes,
|
||||
});
|
||||
defer arena.deinit();
|
||||
|
||||
try std.testing.expect(maybe_user != null);
|
||||
if (maybe_user) |user| {
|
||||
try std.testing.expectEqualSlices(u8, "admin", user.name);
|
||||
}
|
||||
}
|
||||
|
||||
test "create app" {
|
||||
var db = try Database.init(":memory:", std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try db.migrate();
|
||||
|
||||
_ = try db.createApp("app", &.{
|
||||
"http://localhost:3000/callback",
|
||||
"https://example.com/callback",
|
||||
});
|
||||
}
|
||||
const web = @import("web");
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
var lock: std.Thread.Mutex = .{};
|
||||
var last_timestamp: std.atomic.Value(u64) = .{ .raw = 0 };
|
||||
var counter: std.atomic.Value(u32) = .{ .raw = 0 };
|
||||
|
||||
fn getCount(timestamp: u64) u32 {
|
||||
lock.lock();
|
||||
defer lock.unlock();
|
||||
|
||||
if (last_timestamp.swap(timestamp, .monotonic) != timestamp) {
|
||||
counter.store(0, .monotonic);
|
||||
}
|
||||
|
||||
return counter.fetchAdd(1, .monotonic) % 4096;
|
||||
}
|
||||
|
||||
pub fn uuid_v7() [16]u8 {
|
||||
const timestamp: u64 = @intCast(@max(0, std.time.milliTimestamp()));
|
||||
const count = getCount(timestamp);
|
||||
const random = blk: {
|
||||
var bytes: [8]u8 = undefined;
|
||||
std.crypto.random.bytes(&bytes);
|
||||
break :blk bytes;
|
||||
};
|
||||
|
||||
var res: [16]u8 = undefined;
|
||||
|
||||
res[0] = @truncate(timestamp >> 40);
|
||||
res[1] = @truncate(timestamp >> 32);
|
||||
res[2] = @truncate(timestamp >> 24);
|
||||
res[3] = @truncate(timestamp >> 16);
|
||||
res[4] = @truncate(timestamp >> 8);
|
||||
res[5] = @truncate(timestamp);
|
||||
res[6] = (@as(u8, 7) << 4) | @as(u8, @truncate((count >> 8) & 0x0F));
|
||||
res[7] = @truncate(count);
|
||||
res[8] = 0x80 | (random[0] & 0x3F);
|
||||
@memcpy(res[9..16], random[1..8]);
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -1,7 +1,19 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
_ = b.addModule("vecmath", .{
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
const mod = b.addModule("vecmath", .{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
});
|
||||
|
||||
const mod_tests = b.addTest(.{
|
||||
.root_module = mod,
|
||||
});
|
||||
|
||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_mod_tests.step);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.{
|
||||
.name = .vecmath,
|
||||
.version = "0.0.0",
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.minimum_zig_version = "0.16.0",
|
||||
.paths = .{
|
||||
"src",
|
||||
"build.zig",
|
||||
|
||||
@@ -93,7 +93,11 @@ pub const Color = extern struct {
|
||||
return @bitCast(self);
|
||||
}
|
||||
|
||||
pub fn format(self: Color, w: *std.io.Writer) !void {
|
||||
pub fn format(self: Color, w: *std.Io.Writer) !void {
|
||||
try w.print("#{X:0>2}{X:0>2}{X:0>2}{X:0>2}", .{ self.r, self.g, self.b, self.a });
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,7 +24,11 @@ pub const ColorHdr = extern struct {
|
||||
return @bitCast(self);
|
||||
}
|
||||
|
||||
pub fn format(self: ColorHdr, w: *std.io.Writer) !void {
|
||||
pub fn format(self: ColorHdr, w: *std.Io.Writer) !void {
|
||||
try w.print("ColorHdr[{d}, {d}, {d}, {d}]", .{ self.r, self.g, self.b, self.a });
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -252,4 +252,8 @@ pub const Matrix3x2 = extern struct {
|
||||
.ty = -inv_det * (self.tx * iy + self.ty * jy),
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -406,4 +406,8 @@ pub const Matrix3x2x8 = struct {
|
||||
.ty = -inv_det * (self.tx * iy + self.ty * jy),
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -507,4 +507,8 @@ pub const Matrix4x4 = extern struct {
|
||||
// zig fmt: on
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -763,4 +763,8 @@ pub const Matrix4x4x8 = extern struct {
|
||||
// zig fmt: on
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,5 +110,5 @@ pub inline fn unlerpInt64(a: i64, b: i64, x: i64) f32 {
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -123,7 +123,11 @@ pub const Complex = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(self: Complex, w: *std.io.Writer) !void {
|
||||
pub fn format(self: Complex, w: *std.Io.Writer) !void {
|
||||
try w.print("Complex[{d:.3}, {d:.3}]", .{ self.re, self.im });
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -193,4 +193,8 @@ pub const Complex_x8 = struct {
|
||||
.im = @mulAdd(vm.f32x8, vm.ps(t), b.im, @mulAdd(vm.f32x8, -vm.ps(t), a.im, a.im)),
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -179,7 +179,11 @@ pub const Quaternion = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(self: Quaternion, w: *std.io.Writer) !void {
|
||||
pub fn format(self: Quaternion, w: *std.Io.Writer) !void {
|
||||
try w.print("Quaternion[{d:.3}, {d:.3}, {d:.3}, {d:.3}]", .{ self.x, self.y, self.z, self.w });
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -278,4 +278,8 @@ pub const Quaternion_x8 = struct {
|
||||
.w = @mulAdd(vm.f32x8, vm.ps(t), b.w, @mulAdd(vm.f32x8, -vm.ps(t), a.w, a.w)),
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -31,3 +31,7 @@ pub inline fn epi64(value: i64) i64x4 {
|
||||
pub inline fn epu64(value: u64) u64x4 {
|
||||
return @splat(value);
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -235,3 +235,7 @@ test cossin_x8 {
|
||||
cossin_x8(.{ -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75 }),
|
||||
);
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -167,7 +167,11 @@ pub const Vector2 = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(self: Vector2, w: *std.io.Writer) !void {
|
||||
pub fn format(self: Vector2, w: *std.Io.Writer) !void {
|
||||
try w.print("[{d}, {d}]", .{ self.x, self.y });
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,10 +110,14 @@ pub const Vector2Int = extern struct {
|
||||
return self.x * other.y - self.y * other.x;
|
||||
}
|
||||
|
||||
pub fn format(self: Vector2Int, w: *std.io.Writer) !void {
|
||||
pub fn format(self: Vector2Int, w: *std.Io.Writer) !void {
|
||||
try w.print("[{X:0>8}, {X:0>8}]", .{
|
||||
@as(u32, @bitCast(self.x)),
|
||||
@as(u32, @bitCast(self.y)),
|
||||
});
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -171,4 +171,8 @@ pub const Vector2Int_x8 = struct {
|
||||
pub inline fn cross(self: Vector2Int_x8, other: Vector2Int_x8) vm.i32x8 {
|
||||
return self.x * other.y - self.y * other.x;
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,4 +231,8 @@ pub const Vector2x8 = struct {
|
||||
.y = self.x * vm.ps(m.iy) + self.y * vm.ps(m.jy),
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -192,7 +192,11 @@ pub const Vector3 = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(self: Vector3, w: *std.io.Writer) !void {
|
||||
pub fn format(self: Vector3, w: *std.Io.Writer) !void {
|
||||
try w.print("[{d}, {d}, {d}]", .{ self.x, self.y, self.z });
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,11 +117,15 @@ pub const Vector3Int = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(self: Vector3Int, w: *std.io.Writer) !void {
|
||||
pub fn format(self: Vector3Int, w: *std.Io.Writer) !void {
|
||||
try w.print("[{X:0>8}, {X:0>8}, {X:0>8}]", .{
|
||||
@as(u32, @bitCast(self.x)),
|
||||
@as(u32, @bitCast(self.y)),
|
||||
@as(u32, @bitCast(self.z)),
|
||||
});
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -180,4 +180,8 @@ pub const Vector3Int_x8 = struct {
|
||||
.z = self.x * other.y - self.y * other.x,
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -258,4 +258,8 @@ pub const Vector3x8 = struct {
|
||||
.z = v.x * vm.ps(self.iz) + v.y * vm.ps(self.jz) + v.z * vm.ps(self.kz),
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,7 +147,11 @@ pub const Vector4 = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(self: Vector4, w: *std.io.Writer) !void {
|
||||
pub fn format(self: Vector4, w: *std.Io.Writer) !void {
|
||||
try w.print("[{d}, {d}, {d}, {d}]", .{ self.x, self.y, self.z, self.w });
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,7 +112,7 @@ pub const Vector4Int = extern struct {
|
||||
return self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w;
|
||||
}
|
||||
|
||||
pub fn format(self: Vector4Int, w: *std.io.Writer) !void {
|
||||
pub fn format(self: Vector4Int, w: *std.Io.Writer) !void {
|
||||
try w.print("[{X:0>8}, {X:0>8}, {X:0>8}, {X:0>8}]", .{
|
||||
@as(u32, @bitCast(self.x)),
|
||||
@as(u32, @bitCast(self.y)),
|
||||
@@ -120,4 +120,8 @@ pub const Vector4Int = extern struct {
|
||||
@as(u32, @bitCast(self.w)),
|
||||
});
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -177,4 +177,8 @@ pub const Vector4Int_x8 = struct {
|
||||
pub inline fn dot(self: Vector4Int_x8, other: Vector4Int_x8) vm.i32x8 {
|
||||
return self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w;
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -217,4 +217,8 @@ pub const Vector4x8 = struct {
|
||||
.w = self.x * vm.ps(m.iw) + self.y * vm.ps(m.jw) + self.z * vm.ps(m.kw) + self.w * vm.ps(m.tw),
|
||||
};
|
||||
}
|
||||
|
||||
test "refAllDecls" {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const module = b.addModule("http", .{
|
||||
const module = b.addModule("web", .{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
|
||||
@@ -33,13 +33,16 @@ pub const FileDescriptor = enum(i32) {
|
||||
}
|
||||
|
||||
pub fn accept(self: FileDescriptor, noalias addr: ?*linux.sockaddr, noalias len: ?*linux.socklen_t) !FileDescriptor {
|
||||
while (true) {
|
||||
const rc = linux.accept(@intFromEnum(self), addr, len);
|
||||
return switch (errno(rc)) {
|
||||
.SUCCESS => @enumFromInt(@as(i32, @intCast(rc))),
|
||||
switch (errno(rc)) {
|
||||
.SUCCESS => return @enumFromInt(@as(i32, @intCast(rc))),
|
||||
.INPROGRESS, .AGAIN => continue,
|
||||
.CONNABORTED => return error.ConnectionAborted,
|
||||
.PERM => return error.BlockedByFirewall,
|
||||
else => return error.SystemError,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind(self: FileDescriptor, addr: *const linux.sockaddr, len: linux.socklen_t) !void {
|
||||
@@ -90,6 +93,7 @@ pub const FileDescriptor = enum(i32) {
|
||||
switch (errno(rc)) {
|
||||
.SUCCESS => return rc,
|
||||
.INTR => continue,
|
||||
.AGAIN => return error.Timeout,
|
||||
else => return error.SystemError,
|
||||
}
|
||||
}
|
||||
@@ -111,6 +115,7 @@ pub const FileDescriptor = enum(i32) {
|
||||
switch (errno(rc)) {
|
||||
.SUCCESS => return rc,
|
||||
.INTR => continue,
|
||||
.AGAIN, .WOULDBLOCK => error.Timeout,
|
||||
else => return error.SystemError,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const Request = @import("Request.zig");
|
||||
const RequestHandler = @import("RequestHandler.zig");
|
||||
const Worker = @import("Worker.zig");
|
||||
|
||||
const log = std.log.scoped(.Server);
|
||||
const linux = std.os.linux;
|
||||
const errno = linux.E.init;
|
||||
|
||||
@@ -18,6 +19,7 @@ ssl_ctx: ?*openssl.SslContext,
|
||||
workers: []Worker,
|
||||
threads: []std.Thread,
|
||||
request_handler: RequestHandler,
|
||||
read_timeout_us: u64,
|
||||
|
||||
connection_queue: std.DoublyLinkedList,
|
||||
// NOTE Connection pool has no need for being doubly-linked, but the queue has
|
||||
@@ -65,6 +67,10 @@ pub const Options = struct {
|
||||
/// bodies generated with the body writer and not to bodies sent with
|
||||
/// `sendfile`.
|
||||
body_write_buffer_huge_pages: u32 = 1,
|
||||
/// How much time should a worker wait on an idle connection before closing
|
||||
/// it. Specifically, how much time can a `read` syscall block for, before
|
||||
/// the connection is forcefully closed.
|
||||
read_timeout_us: u64 = 1 * std.time.us_per_s,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: Options) !Server {
|
||||
@@ -220,6 +226,7 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !Server {
|
||||
.workers = workers,
|
||||
.threads = threads,
|
||||
.request_handler = options.request_handler,
|
||||
.read_timeout_us = options.read_timeout_us,
|
||||
|
||||
.connection_queue = .{},
|
||||
.connection_pool = connection_pool,
|
||||
@@ -232,6 +239,7 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !Server {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Server, allocator: std.mem.Allocator) void {
|
||||
log.debug("Deinitializing Server.", .{});
|
||||
const worker_count = self.workers.len;
|
||||
|
||||
const single_read_buffers_size = self.workers[0].read_buffer_size;
|
||||
@@ -273,14 +281,18 @@ pub fn listen(self: *Server, running: *const std.atomic.Value(bool)) !void {
|
||||
|
||||
var spawned: usize = 0;
|
||||
defer {
|
||||
log.debug("Storing `false` into worker_running.", .{});
|
||||
worker_running.store(false, .release);
|
||||
log.debug("Broadcasting connection queued condition variable.", .{});
|
||||
self.cond_connection_queued.broadcast();
|
||||
for (self.threads[0..spawned]) |*thread| {
|
||||
for (self.threads[0..spawned], 0..) |*thread, i| {
|
||||
log.debug("Joining the thread of worker #{d}.", .{i});
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
for (self.workers, 0..) |*worker, i| {
|
||||
log.debug("Spawning thread for worker #{d}.", .{i});
|
||||
self.threads[i] = try std.Thread.spawn(.{}, Worker.worker, .{ worker, self, &worker_running });
|
||||
spawned += 1;
|
||||
}
|
||||
@@ -289,34 +301,53 @@ pub fn listen(self: *Server, running: *const std.atomic.Value(bool)) !void {
|
||||
var address: std.net.Address = undefined;
|
||||
var address_size: u32 = @sizeOf(std.net.Address);
|
||||
|
||||
log.debug("Accepting connection.", .{});
|
||||
const fd = self.fd.accept(&address.any, &address_size) catch |e| {
|
||||
std.log.err("Error while accepting connection: {}", .{e});
|
||||
log.err("Error while accepting connection: {}", .{e});
|
||||
continue;
|
||||
};
|
||||
log.debug("Accepted connection from {f}", .{address});
|
||||
|
||||
const timeout: linux.timeval = .{
|
||||
.sec = @intCast(self.read_timeout_us / std.time.us_per_s),
|
||||
.usec = @intCast(self.read_timeout_us % std.time.us_per_s),
|
||||
};
|
||||
try fd.setsockopt(linux.SOL.SOCKET, linux.SO.RCVTIMEO, std.mem.asBytes(&timeout));
|
||||
|
||||
const ssl: ?*openssl.Ssl = self.maybeInitSsl(fd) catch |e| {
|
||||
std.log.err("Error while estabilishing SSL connection: {}", .{e});
|
||||
log.err("Error while estabilishing SSL connection: {}", .{e});
|
||||
fd.close();
|
||||
continue;
|
||||
};
|
||||
|
||||
{
|
||||
log.debug("Acquiring mutex.", .{});
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
log.debug("Acquired mutex.", .{});
|
||||
defer {
|
||||
log.debug("Unlocking mutex.", .{});
|
||||
self.mutex.unlock();
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (self.connection_pool.pop()) |node| {
|
||||
const connection: *Connection = @fieldParentPtr("node", node);
|
||||
connection.reinit(address, fd, ssl);
|
||||
log.debug("Adding connection to {f} to the connection queue.", .{connection.address});
|
||||
self.connection_queue.prepend(node);
|
||||
break;
|
||||
}
|
||||
|
||||
log.debug("Waiting on connection freed condition variable.", .{});
|
||||
self.cond_connection_freed.wait(&self.mutex);
|
||||
log.debug("Woken up on connection freed condition variable.", .{});
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Signaling connection queued condition variable.", .{});
|
||||
self.cond_connection_queued.signal();
|
||||
} else {
|
||||
log.debug("Loaded `false` from running, the accept loop exited.", .{});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +355,9 @@ fn maybeInitSsl(self: *const Server, fd: FileDescriptor) !?*openssl.Ssl {
|
||||
if (self.ssl_ctx) |ssl_ctx| {
|
||||
const ssl = try openssl.Ssl.new(ssl_ctx);
|
||||
try ssl.setFd(fd);
|
||||
log.debug("Accepting SSL layer.", .{});
|
||||
try ssl.accept();
|
||||
log.debug("Accepted SSL layer.", .{});
|
||||
return ssl;
|
||||
} else {
|
||||
return null;
|
||||
|
||||
@@ -9,6 +9,8 @@ const RequestHandler = @import("RequestHandler.zig");
|
||||
const Response = @import("Response.zig");
|
||||
const Server = @import("Server.zig");
|
||||
|
||||
const log = std.log.scoped(.Worker);
|
||||
|
||||
/// Integer unique for this worker. Has no functional meaning. Can be used for
|
||||
/// debugging and profiling.
|
||||
worker_id: usize,
|
||||
@@ -61,6 +63,8 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !Worker {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Worker, allocator: std.mem.Allocator) void {
|
||||
log.debug("[#{d}] Deinitializing Worker.", .{self.worker_id});
|
||||
|
||||
allocator.free(self.header_value_buffer);
|
||||
self.header_hash_map.deinit(allocator);
|
||||
|
||||
@@ -72,26 +76,43 @@ pub fn worker(
|
||||
server: *Server,
|
||||
running: *const std.atomic.Value(bool),
|
||||
) void {
|
||||
log.debug("[#{d}] Acquiring mutex.", .{self.worker_id});
|
||||
server.mutex.lock();
|
||||
defer server.mutex.unlock();
|
||||
log.debug("[#{d}] Acquired mutex.", .{self.worker_id});
|
||||
defer {
|
||||
log.debug("[#{d}] Unlocking mutex.", .{self.worker_id});
|
||||
server.mutex.unlock();
|
||||
}
|
||||
|
||||
while (running.load(.acquire)) {
|
||||
if (server.connection_queue.pop()) |node| {
|
||||
const connection: *Connection = @fieldParentPtr("node", node);
|
||||
log.debug("[#{d}] Popped connection to {f} from the connection queue.", .{ self.worker_id, connection.address });
|
||||
|
||||
log.debug("[#{d}] Unlocking mutex.", .{self.worker_id});
|
||||
server.mutex.unlock();
|
||||
defer {
|
||||
log.debug("[#{d}] Acquiring mutex.", .{self.worker_id});
|
||||
server.mutex.lock();
|
||||
log.debug("[#{d}] Acquired mutex.", .{self.worker_id});
|
||||
|
||||
log.debug("[#{d}] Returning connection to connection pool.", .{self.worker_id});
|
||||
server.connection_pool.append(&connection.node);
|
||||
log.debug("[#{d}] Signaling connection freed condition variable.", .{self.worker_id});
|
||||
server.cond_connection_freed.signal();
|
||||
}
|
||||
|
||||
log.debug("[#{d}] Handling connection to {f}.", .{ self.worker_id, connection.address });
|
||||
self.handleConnection(server.request_handler, connection, running) catch |err| {
|
||||
std.log.err("Error while handling connection: {}", .{err});
|
||||
log.err("[#{d}] Error while handling connection: {}", .{ self.worker_id, err });
|
||||
};
|
||||
} else {
|
||||
log.debug("[#{d}] Waiting on connection queued condition variable.", .{self.worker_id});
|
||||
server.cond_connection_queued.wait(&server.mutex);
|
||||
log.debug("[#{d}] Woken up on connection queued condition variable.", .{self.worker_id});
|
||||
}
|
||||
} else {
|
||||
log.debug("[#{d}] Loaded `false` from running, the worker loop exited.", .{self.worker_id});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,11 +126,16 @@ fn handleConnection(
|
||||
|
||||
while (running.load(.acquire)) {
|
||||
const res = self.handleRequest(request_handler, connection) catch |err| {
|
||||
std.log.err("Error while handling request: {}", .{err});
|
||||
log.err("[#{d}] Error while handling request: {}", .{ self.worker_id, err });
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!res) break;
|
||||
if (!res) {
|
||||
log.debug("[#{d}] Request handler indicated to stop handling the connection to {f}.", .{ self.worker_id, connection.address });
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
log.debug("[#{d}] Loaded `false` from running, the connection handler loop exited.", .{self.worker_id});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +157,7 @@ fn handleRequest(
|
||||
|
||||
var next_header_index: usize = 0;
|
||||
var ignore: bool = false;
|
||||
var client_closed: bool = false;
|
||||
|
||||
var leftover_bytes = self.read_tail - self.read_head;
|
||||
const max_read_tail = self.read_head + self.read_buffer_size;
|
||||
@@ -145,7 +172,17 @@ fn handleRequest(
|
||||
leftover_bytes = 0;
|
||||
} else {
|
||||
const read_tail = self.read_tail;
|
||||
bytes_read = try connection.read(self.read_buffer_ptr[read_tail..max_read_tail]);
|
||||
bytes_read = connection.read(self.read_buffer_ptr[read_tail..max_read_tail]) catch |err| switch (err) {
|
||||
error.Timeout => {
|
||||
log.debug("[#{d}] Connection to {f} timed out.", .{ self.worker_id, connection.address });
|
||||
return false;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
if (bytes_read == 0) {
|
||||
log.debug("[#{d}] Read zero bytes from connection to {f}.", .{ self.worker_id, connection.address });
|
||||
return false;
|
||||
}
|
||||
chunk = self.read_buffer_ptr[read_tail .. read_tail + bytes_read];
|
||||
self.read_tail += bytes_read;
|
||||
}
|
||||
@@ -175,6 +212,10 @@ fn handleRequest(
|
||||
.method => |method| request.method = method,
|
||||
.pathname => |pathname| request.pathname = pathname,
|
||||
.header => |header| blk: {
|
||||
if (header.isNamedKnown(.Connection) and std.mem.eql(u8, header.value, "close")) {
|
||||
client_closed = true;
|
||||
}
|
||||
|
||||
if (ignore) {
|
||||
break :blk;
|
||||
}
|
||||
@@ -258,7 +299,7 @@ fn handleRequest(
|
||||
leftover_bytes = bytes_read - res.consumed;
|
||||
self.read_head = (self.read_tail - leftover_bytes) & ~(self.read_buffer_size - 1);
|
||||
self.read_tail = self.read_head + leftover_bytes;
|
||||
return true;
|
||||
return !client_closed;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,24 @@ const UUID = web.UUID;
|
||||
|
||||
var running: std.atomic.Value(bool) = .init(true);
|
||||
|
||||
fn interruptionHandler(signal: i32) callconv(.c) void {
|
||||
switch (signal) {
|
||||
fn interruptionHandler(sig: i32) callconv(.c) void {
|
||||
var buf: [32]u8 = undefined;
|
||||
|
||||
const signal_name = blk: inline for (@typeInfo(linux.SIG).@"struct".decls) |decl| {
|
||||
if (comptime std.mem.eql(u8, decl.name, "BLOCK") or
|
||||
std.mem.eql(u8, decl.name, "UNBLOCK") or
|
||||
std.mem.eql(u8, decl.name, "SETMASK")) continue;
|
||||
|
||||
const decl_value = @field(linux.SIG, decl.name);
|
||||
if (@TypeOf(decl_value) != comptime_int) continue;
|
||||
if (decl_value == sig) break :blk "SIG" ++ decl.name;
|
||||
} else {
|
||||
break :blk std.fmt.bufPrint(&buf, "#{d}", .{sig}) catch unreachable;
|
||||
};
|
||||
|
||||
std.log.debug("Interrupted with signal {s}.", .{signal_name});
|
||||
|
||||
switch (sig) {
|
||||
linux.SIG.INT, linux.SIG.TERM => {
|
||||
running.store(false, .release);
|
||||
},
|
||||
@@ -19,6 +35,12 @@ fn interruptionHandler(signal: i32) callconv(.c) void {
|
||||
|
||||
const Handler = struct {
|
||||
fn handle(_: *anyopaque, request: *web.Request, response: *web.Response) !void {
|
||||
std.log.info("{f} | {s} {s}", .{
|
||||
response.connection.address,
|
||||
@tagName(request.method),
|
||||
request.pathname,
|
||||
});
|
||||
|
||||
if (!std.mem.eql(u8, request.pathname, "/")) {
|
||||
try response.body_writer.writeAll("Not Found\n");
|
||||
|
||||
@@ -96,14 +118,16 @@ pub fn main() !void {
|
||||
.mask = linux.sigemptyset(),
|
||||
.flags = linux.SA.RESETHAND,
|
||||
};
|
||||
const rc = linux.sigaction(linux.SIG.INT, &sigaction, null);
|
||||
switch (errno(rc)) {
|
||||
.SUCCESS => {},
|
||||
else => |e| {
|
||||
std.log.err("Error while estabilishing interruption handler: {s}", .{@tagName(e)});
|
||||
return error.SystemError;
|
||||
},
|
||||
}
|
||||
signal(linux.SIG.INT, &sigaction);
|
||||
signal(linux.SIG.TERM, &sigaction);
|
||||
|
||||
try server.listen(&running);
|
||||
}
|
||||
|
||||
fn signal(sig: u8, action: *const linux.Sigaction) void {
|
||||
var old_action = std.mem.zeroes(linux.Sigaction);
|
||||
_ = linux.sigaction(sig, null, &old_action);
|
||||
if (old_action.handler.handler == linux.SIG.IGN) return;
|
||||
|
||||
_ = linux.sigaction(sig, action, null);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ pub const Ssl = opaque {
|
||||
import.SSL_free(self);
|
||||
}
|
||||
|
||||
pub inline fn getError(self: *const Ssl, ret_code: i32) i32 {
|
||||
return import.SSL_get_error(self, ret_code);
|
||||
}
|
||||
|
||||
pub inline fn new(ctx: *SslContext) !*Ssl {
|
||||
return import.SSL_new(ctx) orelse error.OpenSslError;
|
||||
}
|
||||
@@ -26,7 +30,12 @@ pub const Ssl = opaque {
|
||||
pub inline fn read(self: *Ssl, buf: []u8) !usize {
|
||||
var bytes_read: usize = undefined;
|
||||
const res = import.SSL_read_ex(self, buf.ptr, buf.len, &bytes_read);
|
||||
return if (res <= 0) error.OpenSslError else bytes_read;
|
||||
if (res <= 0) {
|
||||
const err = self.getError(res);
|
||||
return if (err == c_ssl.SSL_ERROR_ZERO_RETURN) 0 else error.OpenSslError;
|
||||
} else {
|
||||
return bytes_read;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn readAll(self: *Ssl, buf: []u8) !void {
|
||||
|
||||
Reference in New Issue
Block a user