Compare commits

..

7 Commits

57 changed files with 833 additions and 929 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.zig-cache .zig-cache
zig-out zig-out
zig-pkg

88
README.md Normal file
View 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.

View File

@@ -195,7 +195,7 @@ const import = struct {
no_add: bool = false, no_add: bool = false,
/// Internal use. /// Internal use.
no_exotic: bool = false, no_exotic: bool = false,
_pad14: u18 = 0, _pad18: u14 = 0,
}; };
pub const JS_EVAL = packed struct(u32) { pub const JS_EVAL = packed struct(u32) {
@@ -1168,7 +1168,7 @@ pub const Value = extern struct {
} }
pub fn getClassId(self: Value) ClassId { 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 const invalid: ClassId = .{ .class_id = 0 };
pub fn new() ClassId { pub fn new() ClassId {
var class_id: import.JSClassId = 0; var class_id: import.JSClassID = 0;
return .{ .class_id = import.JS_NewClassID(&class_id) }; return .{ .class_id = import.JS_NewClassID(&class_id) };
} }
}; };

View File

@@ -1,15 +1,23 @@
const std = @import("std"); const std = @import("std");
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const vm_dep = b.dependency("vecmath", .{}); const target = b.standardTargetOptions(.{});
const vm_module = vm_dep.module("vecmath");
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"), .root_source_file = b.path("src/root.zig"),
.target = target,
.link_libc = true, .link_libc = true,
.imports = &.{
.{ .name = "vecmath", .module = vm_mod },
},
}); });
root_module.addCSourceFile(.{ mod.addCSourceFile(.{
.file = b.path("src/stbi/stb_image.c"), .file = b.path("src/stbi/stb_image.c"),
.flags = &.{ .flags = &.{
"-std=c17", "-std=c17",
@@ -19,5 +27,12 @@ pub fn build(b: *std.Build) void {
.language = .c, .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);
} }

View File

@@ -1,7 +1,7 @@
.{ .{
.name = .media, .name = .media,
.version = "0.0.0", .version = "0.0.0",
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.16.0",
.paths = .{ .paths = .{
"src", "src",
"build.zig", "build.zig",

View File

@@ -19,3 +19,7 @@ media_type: []const u8,
/// to contain the entire file, only its beginning. The caller asserts that /// to contain the entire file, only its beginning. The caller asserts that
/// `buffer` is at least `magic_length` bytes long. /// `buffer` is at least `magic_length` bytes long.
isFormat: *const fn (buffer: []const u8) bool, isFormat: *const fn (buffer: []const u8) bool,
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -9,14 +9,26 @@ pub const Static = struct {
const sample_rate: f64 = @floatFromInt(self.sample_rate); const sample_rate: f64 = @floatFromInt(self.sample_rate);
return @floatCast(samples / sample_rate); return @floatCast(samples / sample_rate);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const Sample = extern struct { pub const Sample = extern struct {
left: i16, left: i16,
right: i16, right: i16,
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const Stream = struct { pub const Stream = struct {
source: *std.Io.Reader, source: *std.Io.Reader,
sample_rate: u32, sample_rate: u32,
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -19,11 +19,15 @@ pub fn Static(comptime W: u32, comptime H: u32) type {
} }
pub fn fill(self: *@This(), color: vm.Color) void { 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 { pub const Dynamic = struct {
width: u32, width: u32,
height: u32, height: u32,
@@ -65,6 +69,10 @@ pub const Dynamic = struct {
pub fn fill(self: *@This(), color: vm.Color) void { pub fn fill(self: *@This(), color: vm.Color) void {
@memset(self.data[0 .. self.width * self.height], color); @memset(self.data[0 .. self.width * self.height], color);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const Hdr = struct { pub const Hdr = struct {
@@ -108,4 +116,8 @@ pub const Hdr = struct {
pub fn fill(self: *@This(), color: vm.ColorHdr) void { pub fn fill(self: *@This(), color: vm.ColorHdr) void {
@memset(self.data[0 .. self.width * self.height], color); @memset(self.data[0 .. self.width * self.height], color);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -20,6 +20,10 @@ const Info = union(enum) {
pub fn makeFull(full: Header) Info { pub fn makeFull(full: Header) Info {
return .{ .full = full }; return .{ .full = full };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
const Header = struct { 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; 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 /// A standalone marker has no content and is not followed by segment length
/// length parameter. /// parameter.
pub fn isStandalone(self: Marker) bool { pub fn isStandalone(self: Marker) bool {
return self.isRST() != null or return self.isRST() != null or
self == .SOI or self == .SOI or
self == .EOI or self == .EOI or
self == .TEM; self == .TEM;
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
/// The caller asserts that the buffer is at least `format.magic_length` bytes /// 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"); @panic("TODO");
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -35,3 +35,7 @@ pub fn info(buffer: []const u8) ?Header {
@panic("TODO"); @panic("TODO");
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -27,7 +27,7 @@ pub const Header = struct {
filter_method: FilterMethod, filter_method: FilterMethod,
interlace_method: InterlaceMethod, interlace_method: InterlaceMethod,
pub fn decode(chunk: Chunk) Header { pub fn decode(chunk: Chunk) !Header {
std.debug.assert(chunk.chunk_type == .IHDR); std.debug.assert(chunk.chunk_type == .IHDR);
if (chunk.data.len != 13) return error.InvalidPng; if (chunk.data.len != 13) return error.InvalidPng;
@@ -79,6 +79,10 @@ pub const Header = struct {
.interlace_method = @enumFromInt(interlace_method), .interlace_method = @enumFromInt(interlace_method),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const BitDepth = enum(u8) { pub const BitDepth = enum(u8) {
@@ -89,7 +93,11 @@ pub const BitDepth = enum(u8) {
@"16" = 16, @"16" = 16,
pub fn range(self: BitDepth) usize { 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 { pub fn alphaChannelUsed(self: ColorType) bool {
return @intFromEnum(self) & 0b0000_0100 != 0; return @intFromEnum(self) & 0b0000_0100 != 0;
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const CompressionMethod = enum(u8) { pub const CompressionMethod = enum(u8) {
@@ -156,6 +168,10 @@ pub const Palette = struct {
.entries = entries, .entries = entries,
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// --- tRNS -------------------------------------------------------------------- // --- tRNS --------------------------------------------------------------------
@@ -209,6 +225,10 @@ pub const Transparency = union(enum) {
.rgba => error.InvalidPng, .rgba => error.InvalidPng,
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
const TransparencyPalette = struct { const TransparencyPalette = struct {
@@ -218,6 +238,10 @@ const TransparencyPalette = struct {
pub fn asSlice(self: *const TransparencyPalette) []const u8 { pub fn asSlice(self: *const TransparencyPalette) []const u8 {
return self.entries[0..self.len]; return self.entries[0..self.len];
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// --- gAMA -------------------------------------------------------------------- // --- gAMA --------------------------------------------------------------------
@@ -245,6 +269,10 @@ const Gamma = struct {
const gamma = std.mem.readInt(u32, chunk.data[0..4], .big); const gamma = std.mem.readInt(u32, chunk.data[0..4], .big);
return .{ .gamma = gamma }; return .{ .gamma = gamma };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// --- cHRM -------------------------------------------------------------------- // --- cHRM --------------------------------------------------------------------
@@ -274,6 +302,10 @@ const Chromaticities = struct {
.blue_y = std.mem.readInt(u32, chunk.data[28..32], .big), .blue_y = std.mem.readInt(u32, chunk.data[28..32], .big),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// --- sRGB -------------------------------------------------------------------- // --- sRGB --------------------------------------------------------------------
@@ -301,6 +333,10 @@ const RenderingIntent = enum(u8) {
if (rendering_intent > 3) return error.InvalidPng; if (rendering_intent > 3) return error.InvalidPng;
return @enumFromInt(rendering_intent); return @enumFromInt(rendering_intent);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// --- pHYs -------------------------------------------------------------------- // --- pHYs --------------------------------------------------------------------
@@ -310,7 +346,7 @@ pub const PhysicalPixelDimensions = struct {
pixels_per_unit_y: u32, pixels_per_unit_y: u32,
unit: PhysicalUnit, unit: PhysicalUnit,
pub fn decode(chunk: Chunk) PhysicalPixelDimensions { pub fn decode(chunk: Chunk) !PhysicalPixelDimensions {
std.debug.assert(chunk.chunk_type == .pHYs); std.debug.assert(chunk.chunk_type == .pHYs);
if (chunk.data.len != 9) return error.InvalidPng; if (chunk.data.len != 9) return error.InvalidPng;
@@ -328,6 +364,10 @@ pub const PhysicalPixelDimensions = struct {
.unit = @enumFromInt(unit), .unit = @enumFromInt(unit),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const PhysicalUnit = enum(u8) { pub const PhysicalUnit = enum(u8) {
@@ -381,41 +421,61 @@ pub const LastModificationTime = struct {
.second = second, .second = second,
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
pub const ChunkType = enum(u32) { pub const ChunkType = enum(u32) {
IHDR = @bitCast("IHDR".*), IHDR = fromName("IHDR"),
PLTE = @bitCast("PLTE".*), PLTE = fromName("PLTE"),
IDAT = @bitCast("IDAT".*), IDAT = fromName("IDAT"),
IEND = @bitCast("IEND".*), IEND = fromName("IEND"),
tRNS = @bitCast("tRNS".*), tRNS = fromName("tRNS"),
gAMA = @bitCast("gAMA".*), gAMA = fromName("gAMA"),
cHRM = @bitCast("cHRM".*), cHRM = fromName("cHRM"),
sRGB = @bitCast("sRGB".*), sRGB = fromName("sRGB"),
iCCP = @bitCast("iCCP".*), iCCP = fromName("iCCP"),
tEXt = @bitCast("tEXt".*), tEXt = fromName("tEXt"),
zTXt = @bitCast("zTXt".*), zTXt = fromName("zTXt"),
iTXt = @bitCast("iTXt".*), iTXt = fromName("iTXt"),
bKGD = @bitCast("bKGD".*), bKGD = fromName("bKGD"),
pHYs = @bitCast("pHYs".*), pHYs = fromName("pHYs"),
sBIT = @bitCast("sBIT".*), sBIT = fromName("sBIT"),
sPLT = @bitCast("sPLT".*), sPLT = fromName("sPLT"),
hIST = @bitCast("hIST".*), hIST = fromName("hIST"),
tIME = @bitCast("tIME".*), 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 { 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 { 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 { 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 { pub fn isStandardKeyword(keyword: []const u8) ?StandardKeyword {
return map.get(keyword); return map.get(keyword);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
/// The caller asserts that the buffer is at least `format.magic_length` bytes /// 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; 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) { if (chunk.chunk_type != .IHDR) {
return null; 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 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..]; rest = rest[8..];
if (length > 0x7FFF_FFFF) { 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); try writer.writeInt(u32, crc.final(), .big);
} }
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -22,6 +22,10 @@ const Header = union(enum) {
pub fn initStatic(static: HeaderStatic) Header { pub fn initStatic(static: HeaderStatic) Header {
return .{ .static = static }; return .{ .static = static };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
const HeaderStreaming = void; const HeaderStreaming = void;
@@ -36,6 +40,10 @@ const HeaderStatic = struct {
const sample_rate: f64 = @floatFromInt(self.sample_rate); const sample_rate: f64 = @floatFromInt(self.sample_rate);
return @floatCast(samples / 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 /// 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());
}

View File

@@ -68,3 +68,7 @@ pub fn info(buffer: []const u8) ?Header {
.color_space = @enumFromInt(color_space), .color_space = @enumFromInt(color_space),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -1,3 +1,5 @@
const std = @import("std");
pub const audio = @import("audio.zig"); pub const audio = @import("audio.zig");
pub const Format = @import("Format.zig"); pub const Format = @import("Format.zig");
pub const image = @import("image.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 qoa = @import("qoa.zig");
pub const qoi = @import("qoi.zig"); pub const qoi = @import("qoi.zig");
pub const stbi = @import("stbi.zig"); pub const stbi = @import("stbi.zig");
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -5,17 +5,20 @@ const image = @import("image.zig");
const vm = @import("vecmath"); const vm = @import("vecmath");
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
io: std.Io,
mutex: std.Io.Mutex = .init,
allocations: std.AutoHashMapUnmanaged(*anyopaque, usize) = .empty, allocations: std.AutoHashMapUnmanaged(*anyopaque, usize) = .empty,
mutex: std.Thread.Mutex = .{},
allocated_bytes: usize = 0, allocated_bytes: usize = 0,
const alignment: std.mem.Alignment = .@"16"; const alignment: std.mem.Alignment = .@"16";
const VoidPtr = ?*align(alignment.toByteUnits()) anyopaque; const VoidPtr = ?*align(alignment.toByteUnits()) anyopaque;
const log = std.log.scoped(.stbi); 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 .{ return .{
.allocator = allocator, .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))).* }; 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; current_self = self;
defer current_self = undefined; 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. /// 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; current_self = self;
defer current_self = undefined; 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. /// 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; current_self = self;
defer current_self = undefined; 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 { export fn castle_media_stbi_malloc(size: usize) callconv(.c) VoidPtr {
const self = current_self; const self = current_self;
self.mutex.lock(); self.mutex.lock(self.io) catch return null;
defer self.mutex.unlock(); defer self.mutex.unlock(self.io);
self.allocations.ensureUnusedCapacity(self.allocator, 1) catch return null; self.allocations.ensureUnusedCapacity(self.allocator, 1) catch return null;
const memory = self.allocator.alignedAlloc(u8, alignment, size) 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 { export fn castle_media_stbi_realloc(maybe_ptr: VoidPtr, size: usize) callconv(.c) VoidPtr {
const self = current_self; const self = current_self;
self.mutex.lock(); self.mutex.lock(self.io) catch return null;
defer self.mutex.unlock(); defer self.mutex.unlock(self.io);
// NOTE If we were pedantic, we would consider the fact that we might not // NOTE If we were pedantic, we would consider the fact that we might not
// need unused capacity if the memory doesn't get relocated. // 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; const self = current_self;
if (maybe_ptr) |ptr| { if (maybe_ptr) |ptr| {
self.mutex.lock(); self.mutex.lockUncancelable(self.io);
defer self.mutex.unlock(); defer self.mutex.unlock(self.io);
const size = self.allocations.fetchRemove(ptr).?.value; const size = self.allocations.fetchRemove(ptr).?.value;
self.allocated_bytes -= size; self.allocated_bytes -= size;
@@ -301,3 +304,7 @@ export fn castle_media_stbi_free(maybe_ptr: VoidPtr) callconv(.c) void {
self.allocator.free(memory); self.allocator.free(memory);
} }
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -11,11 +11,19 @@ pub fn build(b: *std.Build) void {
const sqlite_mod = sqlite_dep.module("sqlite"); 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", .{ const myid_mod = b.addModule("myid", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target, .target = target,
.imports = &.{ .imports = &.{
.{ .name = "sqlite", .module = sqlite_mod }, .{ .name = "sqlite", .module = sqlite_mod },
.{ .name = "web", .module = web_mod },
}, },
}); });

View File

@@ -13,5 +13,8 @@
.url = "git+https://github.com/vrischmann/zig-sqlite#6d90ee900d186a7fbb6066f28ee13beeaf8be345", .url = "git+https://github.com/vrischmann/zig-sqlite#6d90ee900d186a7fbb6066f28ee13beeaf8be345",
.hash = "sqlite-3.48.0-F2R_a5yODgDFvwwsytm7ZONcSqYBo3qv1PmXOtw3tqLA", .hash = "sqlite-3.48.0-F2R_a5yODgDFvwwsytm7ZONcSqYBo3qv1PmXOtw3tqLA",
}, },
.web = .{
.path = "../web",
},
}, },
} }

259
packages/myid/src/data.zig Normal file
View 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",
});
}

View File

@@ -1,2 +0,0 @@
pub const database_path = "db.sqlite3";
pub const socket_path = "myid.sock";

View File

@@ -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;
}

View File

@@ -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
View 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);

View File

@@ -1,235 +1,3 @@
const std = @import("std"); const std = @import("std");
const sqlite = @import("sqlite"); const sqlite = @import("sqlite");
const uuid = @import("uuid.zig"); const web = @import("web");
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",
});
}

View File

@@ -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;
}

View File

@@ -1,7 +1,19 @@
const std = @import("std"); const std = @import("std");
pub fn build(b: *std.Build) void { 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"), .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);
} }

View File

@@ -1,7 +1,7 @@
.{ .{
.name = .vecmath, .name = .vecmath,
.version = "0.0.0", .version = "0.0.0",
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.16.0",
.paths = .{ .paths = .{
"src", "src",
"build.zig", "build.zig",

View File

@@ -93,7 +93,11 @@ pub const Color = extern struct {
return @bitCast(self); 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 }); 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());
}
}; };

View File

@@ -24,7 +24,11 @@ pub const ColorHdr = extern struct {
return @bitCast(self); 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 }); try w.print("ColorHdr[{d}, {d}, {d}, {d}]", .{ self.r, self.g, self.b, self.a });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -252,4 +252,8 @@ pub const Matrix3x2 = extern struct {
.ty = -inv_det * (self.tx * iy + self.ty * jy), .ty = -inv_det * (self.tx * iy + self.ty * jy),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -406,4 +406,8 @@ pub const Matrix3x2x8 = struct {
.ty = -inv_det * (self.tx * iy + self.ty * jy), .ty = -inv_det * (self.tx * iy + self.ty * jy),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -507,4 +507,8 @@ pub const Matrix4x4 = extern struct {
// zig fmt: on // zig fmt: on
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -763,4 +763,8 @@ pub const Matrix4x4x8 = extern struct {
// zig fmt: on // zig fmt: on
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -110,5 +110,5 @@ pub inline fn unlerpInt64(a: i64, b: i64, x: i64) f32 {
} }
test "refAllDecls" { test "refAllDecls" {
std.testing.refAllDeclsRecursive(@This()); std.testing.refAllDecls(@This());
} }

View File

@@ -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 }); try w.print("Complex[{d:.3}, {d:.3}]", .{ self.re, self.im });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -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)), .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());
}
}; };

View File

@@ -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 }); 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());
}
}; };

View File

@@ -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)), .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());
}
}; };

View File

@@ -31,3 +31,7 @@ pub inline fn epi64(value: i64) i64x4 {
pub inline fn epu64(value: u64) u64x4 { pub inline fn epu64(value: u64) u64x4 {
return @splat(value); return @splat(value);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -235,3 +235,7 @@ test cossin_x8 {
cossin_x8(.{ -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75 }), cossin_x8(.{ -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75 }),
); );
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -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 }); try w.print("[{d}, {d}]", .{ self.x, self.y });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -110,10 +110,14 @@ pub const Vector2Int = extern struct {
return self.x * other.y - self.y * other.x; 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}]", .{ try w.print("[{X:0>8}, {X:0>8}]", .{
@as(u32, @bitCast(self.x)), @as(u32, @bitCast(self.x)),
@as(u32, @bitCast(self.y)), @as(u32, @bitCast(self.y)),
}); });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -171,4 +171,8 @@ pub const Vector2Int_x8 = struct {
pub inline fn cross(self: Vector2Int_x8, other: Vector2Int_x8) vm.i32x8 { pub inline fn cross(self: Vector2Int_x8, other: Vector2Int_x8) vm.i32x8 {
return self.x * other.y - self.y * other.x; return self.x * other.y - self.y * other.x;
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -231,4 +231,8 @@ pub const Vector2x8 = struct {
.y = self.x * vm.ps(m.iy) + self.y * vm.ps(m.jy), .y = self.x * vm.ps(m.iy) + self.y * vm.ps(m.jy),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -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 }); try w.print("[{d}, {d}, {d}]", .{ self.x, self.y, self.z });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -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}]", .{ try w.print("[{X:0>8}, {X:0>8}, {X:0>8}]", .{
@as(u32, @bitCast(self.x)), @as(u32, @bitCast(self.x)),
@as(u32, @bitCast(self.y)), @as(u32, @bitCast(self.y)),
@as(u32, @bitCast(self.z)), @as(u32, @bitCast(self.z)),
}); });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -180,4 +180,8 @@ pub const Vector3Int_x8 = struct {
.z = self.x * other.y - self.y * other.x, .z = self.x * other.y - self.y * other.x,
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -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), .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());
}
}; };

View File

@@ -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 }); try w.print("[{d}, {d}, {d}, {d}]", .{ self.x, self.y, self.z, self.w });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -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; 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}]", .{ try w.print("[{X:0>8}, {X:0>8}, {X:0>8}, {X:0>8}]", .{
@as(u32, @bitCast(self.x)), @as(u32, @bitCast(self.x)),
@as(u32, @bitCast(self.y)), @as(u32, @bitCast(self.y)),
@@ -120,4 +120,8 @@ pub const Vector4Int = extern struct {
@as(u32, @bitCast(self.w)), @as(u32, @bitCast(self.w)),
}); });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -177,4 +177,8 @@ pub const Vector4Int_x8 = struct {
pub inline fn dot(self: Vector4Int_x8, other: Vector4Int_x8) vm.i32x8 { 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; return self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w;
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -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), .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());
}
}; };

View File

@@ -4,7 +4,7 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const module = b.addModule("http", .{ const module = b.addModule("web", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,

View File

@@ -33,13 +33,16 @@ pub const FileDescriptor = enum(i32) {
} }
pub fn accept(self: FileDescriptor, noalias addr: ?*linux.sockaddr, noalias len: ?*linux.socklen_t) !FileDescriptor { pub fn accept(self: FileDescriptor, noalias addr: ?*linux.sockaddr, noalias len: ?*linux.socklen_t) !FileDescriptor {
const rc = linux.accept(@intFromEnum(self), addr, len); while (true) {
return switch (errno(rc)) { const rc = linux.accept(@intFromEnum(self), addr, len);
.SUCCESS => @enumFromInt(@as(i32, @intCast(rc))), switch (errno(rc)) {
.CONNABORTED => return error.ConnectionAborted, .SUCCESS => return @enumFromInt(@as(i32, @intCast(rc))),
.PERM => return error.BlockedByFirewall, .INPROGRESS, .AGAIN => continue,
else => return error.SystemError, .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 { 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)) { switch (errno(rc)) {
.SUCCESS => return rc, .SUCCESS => return rc,
.INTR => continue, .INTR => continue,
.AGAIN => return error.Timeout,
else => return error.SystemError, else => return error.SystemError,
} }
} }
@@ -111,6 +115,7 @@ pub const FileDescriptor = enum(i32) {
switch (errno(rc)) { switch (errno(rc)) {
.SUCCESS => return rc, .SUCCESS => return rc,
.INTR => continue, .INTR => continue,
.AGAIN, .WOULDBLOCK => error.Timeout,
else => return error.SystemError, else => return error.SystemError,
} }
} }

View File

@@ -9,6 +9,7 @@ const Request = @import("Request.zig");
const RequestHandler = @import("RequestHandler.zig"); const RequestHandler = @import("RequestHandler.zig");
const Worker = @import("Worker.zig"); const Worker = @import("Worker.zig");
const log = std.log.scoped(.Server);
const linux = std.os.linux; const linux = std.os.linux;
const errno = linux.E.init; const errno = linux.E.init;
@@ -18,6 +19,7 @@ ssl_ctx: ?*openssl.SslContext,
workers: []Worker, workers: []Worker,
threads: []std.Thread, threads: []std.Thread,
request_handler: RequestHandler, request_handler: RequestHandler,
read_timeout_us: u64,
connection_queue: std.DoublyLinkedList, connection_queue: std.DoublyLinkedList,
// NOTE Connection pool has no need for being doubly-linked, but the queue has // 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 /// bodies generated with the body writer and not to bodies sent with
/// `sendfile`. /// `sendfile`.
body_write_buffer_huge_pages: u32 = 1, 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 { 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, .workers = workers,
.threads = threads, .threads = threads,
.request_handler = options.request_handler, .request_handler = options.request_handler,
.read_timeout_us = options.read_timeout_us,
.connection_queue = .{}, .connection_queue = .{},
.connection_pool = connection_pool, .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 { pub fn deinit(self: *Server, allocator: std.mem.Allocator) void {
log.debug("Deinitializing Server.", .{});
const worker_count = self.workers.len; const worker_count = self.workers.len;
const single_read_buffers_size = self.workers[0].read_buffer_size; 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; var spawned: usize = 0;
defer { defer {
log.debug("Storing `false` into worker_running.", .{});
worker_running.store(false, .release); worker_running.store(false, .release);
log.debug("Broadcasting connection queued condition variable.", .{});
self.cond_connection_queued.broadcast(); 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(); thread.join();
} }
} }
for (self.workers, 0..) |*worker, i| { 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 }); self.threads[i] = try std.Thread.spawn(.{}, Worker.worker, .{ worker, self, &worker_running });
spawned += 1; 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: std.net.Address = undefined;
var address_size: u32 = @sizeOf(std.net.Address); var address_size: u32 = @sizeOf(std.net.Address);
log.debug("Accepting connection.", .{});
const fd = self.fd.accept(&address.any, &address_size) catch |e| { 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; 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| { 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(); fd.close();
continue; continue;
}; };
{ {
log.debug("Acquiring mutex.", .{});
self.mutex.lock(); self.mutex.lock();
defer self.mutex.unlock(); log.debug("Acquired mutex.", .{});
defer {
log.debug("Unlocking mutex.", .{});
self.mutex.unlock();
}
while (true) { while (true) {
if (self.connection_pool.pop()) |node| { if (self.connection_pool.pop()) |node| {
const connection: *Connection = @fieldParentPtr("node", node); const connection: *Connection = @fieldParentPtr("node", node);
connection.reinit(address, fd, ssl); connection.reinit(address, fd, ssl);
log.debug("Adding connection to {f} to the connection queue.", .{connection.address});
self.connection_queue.prepend(node); self.connection_queue.prepend(node);
break; break;
} }
log.debug("Waiting on connection freed condition variable.", .{});
self.cond_connection_freed.wait(&self.mutex); 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(); 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| { if (self.ssl_ctx) |ssl_ctx| {
const ssl = try openssl.Ssl.new(ssl_ctx); const ssl = try openssl.Ssl.new(ssl_ctx);
try ssl.setFd(fd); try ssl.setFd(fd);
log.debug("Accepting SSL layer.", .{});
try ssl.accept(); try ssl.accept();
log.debug("Accepted SSL layer.", .{});
return ssl; return ssl;
} else { } else {
return null; return null;

View File

@@ -9,6 +9,8 @@ const RequestHandler = @import("RequestHandler.zig");
const Response = @import("Response.zig"); const Response = @import("Response.zig");
const Server = @import("Server.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 /// Integer unique for this worker. Has no functional meaning. Can be used for
/// debugging and profiling. /// debugging and profiling.
worker_id: usize, 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 { pub fn deinit(self: *Worker, allocator: std.mem.Allocator) void {
log.debug("[#{d}] Deinitializing Worker.", .{self.worker_id});
allocator.free(self.header_value_buffer); allocator.free(self.header_value_buffer);
self.header_hash_map.deinit(allocator); self.header_hash_map.deinit(allocator);
@@ -72,26 +76,43 @@ pub fn worker(
server: *Server, server: *Server,
running: *const std.atomic.Value(bool), running: *const std.atomic.Value(bool),
) void { ) void {
log.debug("[#{d}] Acquiring mutex.", .{self.worker_id});
server.mutex.lock(); 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)) { while (running.load(.acquire)) {
if (server.connection_queue.pop()) |node| { if (server.connection_queue.pop()) |node| {
const connection: *Connection = @fieldParentPtr("node", 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(); server.mutex.unlock();
defer { defer {
log.debug("[#{d}] Acquiring mutex.", .{self.worker_id});
server.mutex.lock(); 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); server.connection_pool.append(&connection.node);
log.debug("[#{d}] Signaling connection freed condition variable.", .{self.worker_id});
server.cond_connection_freed.signal(); 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| { 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 { } else {
log.debug("[#{d}] Waiting on connection queued condition variable.", .{self.worker_id});
server.cond_connection_queued.wait(&server.mutex); 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)) { while (running.load(.acquire)) {
const res = self.handleRequest(request_handler, connection) catch |err| { 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; 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 next_header_index: usize = 0;
var ignore: bool = false; var ignore: bool = false;
var client_closed: bool = false;
var leftover_bytes = self.read_tail - self.read_head; var leftover_bytes = self.read_tail - self.read_head;
const max_read_tail = self.read_head + self.read_buffer_size; const max_read_tail = self.read_head + self.read_buffer_size;
@@ -145,7 +172,17 @@ fn handleRequest(
leftover_bytes = 0; leftover_bytes = 0;
} else { } else {
const read_tail = self.read_tail; 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]; chunk = self.read_buffer_ptr[read_tail .. read_tail + bytes_read];
self.read_tail += bytes_read; self.read_tail += bytes_read;
} }
@@ -175,6 +212,10 @@ fn handleRequest(
.method => |method| request.method = method, .method => |method| request.method = method,
.pathname => |pathname| request.pathname = pathname, .pathname => |pathname| request.pathname = pathname,
.header => |header| blk: { .header => |header| blk: {
if (header.isNamedKnown(.Connection) and std.mem.eql(u8, header.value, "close")) {
client_closed = true;
}
if (ignore) { if (ignore) {
break :blk; break :blk;
} }
@@ -258,7 +299,7 @@ fn handleRequest(
leftover_bytes = bytes_read - res.consumed; leftover_bytes = bytes_read - res.consumed;
self.read_head = (self.read_tail - leftover_bytes) & ~(self.read_buffer_size - 1); self.read_head = (self.read_tail - leftover_bytes) & ~(self.read_buffer_size - 1);
self.read_tail = self.read_head + leftover_bytes; self.read_tail = self.read_head + leftover_bytes;
return true; return !client_closed;
}, },
} }
} }

View File

@@ -8,8 +8,24 @@ const UUID = web.UUID;
var running: std.atomic.Value(bool) = .init(true); var running: std.atomic.Value(bool) = .init(true);
fn interruptionHandler(signal: i32) callconv(.c) void { fn interruptionHandler(sig: i32) callconv(.c) void {
switch (signal) { 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 => { linux.SIG.INT, linux.SIG.TERM => {
running.store(false, .release); running.store(false, .release);
}, },
@@ -19,6 +35,12 @@ fn interruptionHandler(signal: i32) callconv(.c) void {
const Handler = struct { const Handler = struct {
fn handle(_: *anyopaque, request: *web.Request, response: *web.Response) !void { 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, "/")) { if (!std.mem.eql(u8, request.pathname, "/")) {
try response.body_writer.writeAll("Not Found\n"); try response.body_writer.writeAll("Not Found\n");
@@ -96,14 +118,16 @@ pub fn main() !void {
.mask = linux.sigemptyset(), .mask = linux.sigemptyset(),
.flags = linux.SA.RESETHAND, .flags = linux.SA.RESETHAND,
}; };
const rc = linux.sigaction(linux.SIG.INT, &sigaction, null); signal(linux.SIG.INT, &sigaction);
switch (errno(rc)) { signal(linux.SIG.TERM, &sigaction);
.SUCCESS => {},
else => |e| {
std.log.err("Error while estabilishing interruption handler: {s}", .{@tagName(e)});
return error.SystemError;
},
}
try server.listen(&running); 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);
}

View File

@@ -19,6 +19,10 @@ pub const Ssl = opaque {
import.SSL_free(self); 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 { pub inline fn new(ctx: *SslContext) !*Ssl {
return import.SSL_new(ctx) orelse error.OpenSslError; 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 { pub inline fn read(self: *Ssl, buf: []u8) !usize {
var bytes_read: usize = undefined; var bytes_read: usize = undefined;
const res = import.SSL_read_ex(self, buf.ptr, buf.len, &bytes_read); 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 { pub fn readAll(self: *Ssl, buf: []u8) !void {