gitoriaLog in with ident

tickets

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit7538b0347538b034mission 011 (ticket #25 web part): short page URLs /<slug> and /<slug>/<number>; /projects/<slug>, /projects/<slug>/<n>, /projects/<slug>/tickets/<n> and /tickets/<ref> answer 301 (current slug); slug pages moved behind hl:web's own routes after construction (they answered /__hl/* and /components/*.hl); reserved slugs refused; API paths unchanged. gate 293/0, connect 60/0, live-data copy 187/0, API old vs new: only page links differmre7538b034/plugins/http/ws_common.zig

21.4 KB

  1. // Shared WebSocket core — RFC 6455 framing, transport-agnostic (decision D8).
  2. // Compiled into each HTTP plugin like http_common.zig; NOT a standalone .so.
  3. //
  4. // Responsibilities:
  5. // - Sec-WebSocket-Accept computation (SHA1 + base64 of key + RFC GUID)
  6. // - Frame encoding (unmasked, server → client) via a write callback
  7. // - Frame decoding + message assembly (client-masked inbound), fragmentation,
  8. // ping/pong surfacing, close handshake with codes, UTF-8 validation
  9. //
  10. // The per-protocol handshakes stay in each plugin: h1 = Upgrade/101 (http1.zig),
  11. // h2 = RFC 8441 extended CONNECT (future), h3 = RFC 9220 (future). Everything
  12. // after the handshake — the byte stream — goes through this module unchanged.
  13. //
  14. // Transport neutrality: encoding writes through `WriteFn` callbacks and decoding
  15. // consumes raw bytes via `Decoder.feed()`; the module never touches a socket.
  16. const std = @import("std");
  17. pub const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
  18. /// Write callback: return false on transport failure (peer gone).
  19. pub const WriteFn = *const fn (ctx: ?*anyopaque, data: []const u8) bool;
  20. pub const Opcode = enum(u4) {
  21. continuation = 0x0,
  22. text = 0x1,
  23. binary = 0x2,
  24. close = 0x8,
  25. ping = 0x9,
  26. pong = 0xA,
  27. _,
  28. pub fn isControl(self: Opcode) bool {
  29. return @intFromEnum(self) >= 0x8;
  30. }
  31. };
  32. // Close codes used by the core
  33. pub const CLOSE_NORMAL: u16 = 1000;
  34. pub const CLOSE_GOING_AWAY: u16 = 1001;
  35. pub const CLOSE_PROTOCOL_ERROR: u16 = 1002;
  36. pub const CLOSE_INVALID_PAYLOAD: u16 = 1007;
  37. pub const CLOSE_TOO_BIG: u16 = 1009;
  38. pub const CLOSE_NO_STATUS: u16 = 1005; // synthesized when close frame has no code — never sent on the wire
  39. /// Sec-WebSocket-Accept: base64(SHA1(key ++ GUID)). Output is always 28 chars.
  40. pub fn computeAcceptKey(key: []const u8, out: *[28]u8) []const u8 {
  41. var sha = std.crypto.hash.Sha1.init(.{});
  42. sha.update(key);
  43. sha.update(GUID);
  44. var digest: [20]u8 = undefined;
  45. sha.final(&digest);
  46. return std.base64.standard.Encoder.encode(out, &digest);
  47. }
  48. /// Encode a frame header into `buf` (needs >= 10 bytes). Returns header length.
  49. /// Server frames are unmasked (RFC 6455 §5.1).
  50. pub fn encodeFrameHeader(buf: *[10]u8, opcode: Opcode, payload_len: usize, fin: bool) usize {
  51. buf[0] = (if (fin) @as(u8, 0x80) else 0) | @as(u8, @intFromEnum(opcode));
  52. if (payload_len <= 125) {
  53. buf[1] = @intCast(payload_len);
  54. return 2;
  55. } else if (payload_len <= 65535) {
  56. buf[1] = 126;
  57. std.mem.writeInt(u16, buf[2..4], @intCast(payload_len), .big);
  58. return 4;
  59. } else {
  60. buf[1] = 127;
  61. std.mem.writeInt(u64, buf[2..10], payload_len, .big);
  62. return 10;
  63. }
  64. }
  65. /// Write one complete unmasked frame through the callback. True on success.
  66. pub fn writeFrame(write: WriteFn, ctx: ?*anyopaque, opcode: Opcode, payload: []const u8) bool {
  67. var hdr: [10]u8 = undefined;
  68. const hlen = encodeFrameHeader(&hdr, opcode, payload.len, true);
  69. if (!write(ctx, hdr[0..hlen])) return false;
  70. if (payload.len > 0 and !write(ctx, payload)) return false;
  71. return true;
  72. }
  73. /// Write a close frame with a code and optional reason (reason truncated to 123).
  74. pub fn writeClose(write: WriteFn, ctx: ?*anyopaque, code: u16, reason: []const u8) bool {
  75. var payload: [125]u8 = undefined;
  76. std.mem.writeInt(u16, payload[0..2], code, .big);
  77. const rlen = @min(reason.len, 123);
  78. @memcpy(payload[2 .. 2 + rlen], reason[0..rlen]);
  79. return writeFrame(write, ctx, .close, payload[0 .. 2 + rlen]);
  80. }
  81. /// Write one complete MASKED frame — the client's half of RFC 6455 §5.1 ("a
  82. /// client MUST mask all frames"). The mask is fresh per frame from the kernel
  83. /// CSPRNG (`getrandom(2)`, not a seeded generator: a predictable mask is not
  84. /// a security property this module is willing to guess wrong about — same
  85. /// reasoning as hl:http1's session-token `randomToken`). A getrandom() short
  86. /// read is vanishingly rare for 4 bytes; the leftover is zeroed rather than
  87. /// left uninitialized, which only ever makes the mask weaker, never wrong.
  88. /// Masking is applied through a small stack buffer so a large payload needs
  89. /// no allocation.
  90. pub fn writeFrameMasked(write: WriteFn, ctx: ?*anyopaque, opcode: Opcode, payload: []const u8) bool {
  91. var hdr: [10]u8 = undefined;
  92. const hlen = encodeFrameHeader(&hdr, opcode, payload.len, true);
  93. hdr[1] |= 0x80; // mask bit
  94. var mask: [4]u8 = .{ 0, 0, 0, 0 };
  95. _ = std.os.linux.getrandom(&mask, mask.len, 0);
  96. if (!write(ctx, hdr[0..hlen])) return false;
  97. if (!write(ctx, &mask)) return false;
  98. if (payload.len == 0) return true;
  99. var buf: [4096]u8 = undefined;
  100. var i: usize = 0;
  101. while (i < payload.len) {
  102. const n = @min(buf.len, payload.len - i);
  103. for (0..n) |j| buf[j] = payload[i + j] ^ mask[(i + j) % 4];
  104. if (!write(ctx, buf[0..n])) return false;
  105. i += n;
  106. }
  107. return true;
  108. }
  109. /// Masked `writeClose` — see `writeFrameMasked`.
  110. pub fn writeCloseMasked(write: WriteFn, ctx: ?*anyopaque, code: u16, reason: []const u8) bool {
  111. var payload: [125]u8 = undefined;
  112. std.mem.writeInt(u16, payload[0..2], code, .big);
  113. const rlen = @min(reason.len, 123);
  114. @memcpy(payload[2 .. 2 + rlen], reason[0..rlen]);
  115. return writeFrameMasked(write, ctx, .close, payload[0 .. 2 + rlen]);
  116. }
  117. /// A decoded inbound event. Payload slices are allocated with the decoder's
  118. /// allocator and owned by the CALLER (free after use).
  119. pub const Event = union(enum) {
  120. text: []u8,
  121. binary: []u8,
  122. ping: []u8, // payload to echo in the pong
  123. pong: []u8,
  124. close: Close,
  125. /// Protocol violation — caller should send a close frame with this code
  126. /// and drop the connection.
  127. protocol_error: u16,
  128. pub const Close = struct {
  129. code: u16, // CLOSE_NO_STATUS when the frame carried no code
  130. reason: []u8,
  131. };
  132. pub fn deinitPayload(self: *Event, alloc: std.mem.Allocator) void {
  133. switch (self.*) {
  134. .text, .binary, .ping, .pong => |p| alloc.free(p),
  135. .close => |cl| alloc.free(cl.reason),
  136. .protocol_error => {},
  137. }
  138. }
  139. };
  140. fn validCloseCode(code: u16) bool {
  141. return switch (code) {
  142. 1000...1003, 1007...1011 => true,
  143. 3000...4999 => true,
  144. else => false,
  145. };
  146. }
  147. /// Streaming frame decoder + message assembler. One per connection.
  148. /// feed() raw transport bytes, then loop next() until it returns null.
  149. pub const Decoder = struct {
  150. alloc: std.mem.Allocator,
  151. /// Client frames must be masked (server side). Set false for client use.
  152. require_masked: bool = true,
  153. max_message_size: usize = 16 * 1024 * 1024,
  154. buf: std.ArrayListUnmanaged(u8) = .empty, // raw incoming bytes
  155. read_pos: usize = 0,
  156. msg: std.ArrayListUnmanaged(u8) = .empty, // fragmented-message assembly
  157. msg_opcode: ?Opcode = null,
  158. failed: bool = false, // after a protocol error the stream is dead
  159. pub fn init(alloc: std.mem.Allocator) Decoder {
  160. return .{ .alloc = alloc };
  161. }
  162. pub fn deinit(self: *Decoder) void {
  163. self.buf.deinit(self.alloc);
  164. self.msg.deinit(self.alloc);
  165. }
  166. pub fn feed(self: *Decoder, bytes: []const u8) !void {
  167. // Compact consumed prefix before growing
  168. if (self.read_pos > 0) {
  169. const remaining = self.buf.items.len - self.read_pos;
  170. std.mem.copyForwards(u8, self.buf.items[0..remaining], self.buf.items[self.read_pos..]);
  171. self.buf.shrinkRetainingCapacity(remaining);
  172. self.read_pos = 0;
  173. }
  174. try self.buf.appendSlice(self.alloc, bytes);
  175. }
  176. fn fail(self: *Decoder, code: u16) Event {
  177. self.failed = true;
  178. return .{ .protocol_error = code };
  179. }
  180. /// Pop the next complete event, or null if more bytes are needed.
  181. pub fn next(self: *Decoder) !?Event {
  182. if (self.failed) return null;
  183. const data = self.buf.items[self.read_pos..];
  184. if (data.len < 2) return null;
  185. const b0 = data[0];
  186. const b1 = data[1];
  187. const fin = (b0 & 0x80) != 0;
  188. const rsv = b0 & 0x70;
  189. const opcode: Opcode = @enumFromInt(@as(u4, @truncate(b0 & 0x0F)));
  190. const masked = (b1 & 0x80) != 0;
  191. const len7: u8 = b1 & 0x7F;
  192. if (rsv != 0) return self.fail(CLOSE_PROTOCOL_ERROR); // no extensions negotiated
  193. switch (@intFromEnum(opcode)) {
  194. 0x0, 0x1, 0x2, 0x8, 0x9, 0xA => {},
  195. else => return self.fail(CLOSE_PROTOCOL_ERROR),
  196. }
  197. if (self.require_masked and !masked) return self.fail(CLOSE_PROTOCOL_ERROR);
  198. if (opcode.isControl() and (!fin or len7 > 125)) return self.fail(CLOSE_PROTOCOL_ERROR);
  199. var offset: usize = 2;
  200. var payload_len: u64 = len7;
  201. if (len7 == 126) {
  202. if (data.len < offset + 2) return null;
  203. payload_len = std.mem.readInt(u16, data[offset..][0..2], .big);
  204. offset += 2;
  205. if (payload_len <= 125) return self.fail(CLOSE_PROTOCOL_ERROR); // non-minimal encoding
  206. } else if (len7 == 127) {
  207. if (data.len < offset + 8) return null;
  208. payload_len = std.mem.readInt(u64, data[offset..][0..8], .big);
  209. offset += 8;
  210. if (payload_len <= 65535) return self.fail(CLOSE_PROTOCOL_ERROR);
  211. if (payload_len > (1 << 62)) return self.fail(CLOSE_PROTOCOL_ERROR); // MSB must be 0
  212. }
  213. if (payload_len > self.max_message_size or
  214. self.msg.items.len + payload_len > self.max_message_size)
  215. return self.fail(CLOSE_TOO_BIG);
  216. var mask: [4]u8 = .{ 0, 0, 0, 0 };
  217. if (masked) {
  218. if (data.len < offset + 4) return null;
  219. mask = data[offset..][0..4].*;
  220. offset += 4;
  221. }
  222. const plen: usize = @intCast(payload_len);
  223. if (data.len < offset + plen) return null; // frame incomplete
  224. // Unmask into an owned copy
  225. const payload = try self.alloc.alloc(u8, plen);
  226. errdefer self.alloc.free(payload);
  227. for (0..plen) |i| {
  228. payload[i] = data[offset + i] ^ mask[i % 4];
  229. }
  230. self.read_pos += offset + plen;
  231. switch (opcode) {
  232. .ping => return .{ .ping = payload },
  233. .pong => return .{ .pong = payload },
  234. .close => {
  235. if (plen == 0) return .{ .close = .{ .code = CLOSE_NO_STATUS, .reason = payload } };
  236. if (plen == 1) {
  237. self.alloc.free(payload);
  238. return self.fail(CLOSE_PROTOCOL_ERROR);
  239. }
  240. const code = std.mem.readInt(u16, payload[0..2], .big);
  241. if (!validCloseCode(code)) {
  242. self.alloc.free(payload);
  243. return self.fail(CLOSE_PROTOCOL_ERROR);
  244. }
  245. if (!std.unicode.utf8ValidateSlice(payload[2..])) {
  246. self.alloc.free(payload);
  247. return self.fail(CLOSE_INVALID_PAYLOAD);
  248. }
  249. const reason = try self.alloc.alloc(u8, plen - 2);
  250. @memcpy(reason, payload[2..]);
  251. self.alloc.free(payload);
  252. return .{ .close = .{ .code = code, .reason = reason } };
  253. },
  254. .text, .binary => {
  255. if (self.msg_opcode != null) {
  256. self.alloc.free(payload);
  257. return self.fail(CLOSE_PROTOCOL_ERROR); // new data frame while assembling
  258. }
  259. if (fin) {
  260. if (opcode == .text and !std.unicode.utf8ValidateSlice(payload)) {
  261. self.alloc.free(payload);
  262. return self.fail(CLOSE_INVALID_PAYLOAD);
  263. }
  264. return if (opcode == .text) .{ .text = payload } else .{ .binary = payload };
  265. }
  266. // First fragment
  267. self.msg_opcode = opcode;
  268. try self.msg.appendSlice(self.alloc, payload);
  269. self.alloc.free(payload);
  270. return self.next(); // more frames may already be buffered
  271. },
  272. .continuation => {
  273. const start_op = self.msg_opcode orelse {
  274. self.alloc.free(payload);
  275. return self.fail(CLOSE_PROTOCOL_ERROR); // continuation without a message
  276. };
  277. try self.msg.appendSlice(self.alloc, payload);
  278. self.alloc.free(payload);
  279. if (!fin) return self.next();
  280. // Message complete
  281. const full = try self.msg.toOwnedSlice(self.alloc);
  282. self.msg_opcode = null;
  283. if (start_op == .text and !std.unicode.utf8ValidateSlice(full)) {
  284. self.alloc.free(full);
  285. return self.fail(CLOSE_INVALID_PAYLOAD);
  286. }
  287. return if (start_op == .text) .{ .text = full } else .{ .binary = full };
  288. },
  289. _ => return self.fail(CLOSE_PROTOCOL_ERROR), // unreachable — filtered above
  290. }
  291. }
  292. };
  293. // ===========================================================================
  294. // Tests
  295. // ===========================================================================
  296. const t = std.testing;
  297. test "computeAcceptKey RFC 6455 sample" {
  298. var out: [28]u8 = undefined;
  299. const accept = computeAcceptKey("dGhlIHNhbXBsZSBub25jZQ==", &out);
  300. try t.expectEqualStrings("s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", accept);
  301. }
  302. fn maskedFrame(alloc: std.mem.Allocator, fin: bool, opcode: Opcode, payload: []const u8, mask: [4]u8) ![]u8 {
  303. var out = std.ArrayListUnmanaged(u8).empty;
  304. var hdr: [10]u8 = undefined;
  305. const hlen = encodeFrameHeader(&hdr, opcode, payload.len, fin);
  306. hdr[1] |= 0x80; // set mask bit
  307. try out.appendSlice(alloc, hdr[0..hlen]);
  308. try out.appendSlice(alloc, &mask);
  309. for (payload, 0..) |b, i| try out.append(alloc, b ^ mask[i % 4]);
  310. return out.toOwnedSlice(alloc);
  311. }
  312. test "decode masked text frame" {
  313. var dec = Decoder.init(t.allocator);
  314. defer dec.deinit();
  315. const frame = try maskedFrame(t.allocator, true, .text, "Hello", .{ 0x37, 0xfa, 0x21, 0x3d });
  316. defer t.allocator.free(frame);
  317. try dec.feed(frame);
  318. var ev = (try dec.next()).?;
  319. defer ev.deinitPayload(t.allocator);
  320. try t.expectEqualStrings("Hello", ev.text);
  321. try t.expectEqual(@as(?Event, null), try dec.next());
  322. }
  323. test "decode split across feeds" {
  324. var dec = Decoder.init(t.allocator);
  325. defer dec.deinit();
  326. const frame = try maskedFrame(t.allocator, true, .text, "chunked delivery", .{ 1, 2, 3, 4 });
  327. defer t.allocator.free(frame);
  328. try dec.feed(frame[0..3]);
  329. try t.expectEqual(@as(?Event, null), try dec.next());
  330. try dec.feed(frame[3..]);
  331. var ev = (try dec.next()).?;
  332. defer ev.deinitPayload(t.allocator);
  333. try t.expectEqualStrings("chunked delivery", ev.text);
  334. }
  335. test "fragmented message assembly" {
  336. var dec = Decoder.init(t.allocator);
  337. defer dec.deinit();
  338. const f1 = try maskedFrame(t.allocator, false, .text, "Hel", .{ 9, 8, 7, 6 });
  339. defer t.allocator.free(f1);
  340. const f2 = try maskedFrame(t.allocator, false, .continuation, "lo ", .{ 5, 4, 3, 2 });
  341. defer t.allocator.free(f2);
  342. const f3 = try maskedFrame(t.allocator, true, .continuation, "WS", .{ 1, 1, 1, 1 });
  343. defer t.allocator.free(f3);
  344. try dec.feed(f1);
  345. try dec.feed(f2);
  346. try t.expectEqual(@as(?Event, null), try dec.next());
  347. try dec.feed(f3);
  348. var ev = (try dec.next()).?;
  349. defer ev.deinitPayload(t.allocator);
  350. try t.expectEqualStrings("Hello WS", ev.text);
  351. }
  352. test "control frame interleaved with fragments" {
  353. var dec = Decoder.init(t.allocator);
  354. defer dec.deinit();
  355. const f1 = try maskedFrame(t.allocator, false, .text, "par", .{ 2, 2, 2, 2 });
  356. defer t.allocator.free(f1);
  357. const ping = try maskedFrame(t.allocator, true, .ping, "hb", .{ 3, 3, 3, 3 });
  358. defer t.allocator.free(ping);
  359. const f2 = try maskedFrame(t.allocator, true, .continuation, "tial", .{ 4, 4, 4, 4 });
  360. defer t.allocator.free(f2);
  361. try dec.feed(f1);
  362. try dec.feed(ping);
  363. var ev1 = (try dec.next()).?;
  364. defer ev1.deinitPayload(t.allocator);
  365. try t.expectEqualStrings("hb", ev1.ping);
  366. try dec.feed(f2);
  367. var ev2 = (try dec.next()).?;
  368. defer ev2.deinitPayload(t.allocator);
  369. try t.expectEqualStrings("partial", ev2.text);
  370. }
  371. test "unmasked client frame is a protocol error" {
  372. var dec = Decoder.init(t.allocator);
  373. defer dec.deinit();
  374. var hdr: [10]u8 = undefined;
  375. const hlen = encodeFrameHeader(&hdr, .text, 2, true);
  376. try dec.feed(hdr[0..hlen]);
  377. try dec.feed("hi");
  378. const ev = (try dec.next()).?;
  379. try t.expectEqual(CLOSE_PROTOCOL_ERROR, ev.protocol_error);
  380. try t.expectEqual(@as(?Event, null), try dec.next()); // stream dead
  381. }
  382. test "invalid UTF-8 in text is 1007" {
  383. var dec = Decoder.init(t.allocator);
  384. defer dec.deinit();
  385. const bad = [_]u8{ 0xC3, 0x28 }; // invalid 2-byte sequence
  386. const frame = try maskedFrame(t.allocator, true, .text, &bad, .{ 7, 7, 7, 7 });
  387. defer t.allocator.free(frame);
  388. try dec.feed(frame);
  389. const ev = (try dec.next()).?;
  390. try t.expectEqual(CLOSE_INVALID_PAYLOAD, ev.protocol_error);
  391. }
  392. test "close with code and reason" {
  393. var dec = Decoder.init(t.allocator);
  394. defer dec.deinit();
  395. var payload: [7]u8 = undefined;
  396. std.mem.writeInt(u16, payload[0..2], 1000, .big);
  397. @memcpy(payload[2..], "done!");
  398. const frame = try maskedFrame(t.allocator, true, .close, &payload, .{ 6, 6, 6, 6 });
  399. defer t.allocator.free(frame);
  400. try dec.feed(frame);
  401. var ev = (try dec.next()).?;
  402. defer ev.deinitPayload(t.allocator);
  403. try t.expectEqual(@as(u16, 1000), ev.close.code);
  404. try t.expectEqualStrings("done!", ev.close.reason);
  405. }
  406. test "close with invalid code is 1002" {
  407. var dec = Decoder.init(t.allocator);
  408. defer dec.deinit();
  409. var payload: [2]u8 = undefined;
  410. std.mem.writeInt(u16, payload[0..2], 1006, .big); // 1006 must never appear on the wire
  411. const frame = try maskedFrame(t.allocator, true, .close, &payload, .{ 1, 2, 3, 4 });
  412. defer t.allocator.free(frame);
  413. try dec.feed(frame);
  414. const ev = (try dec.next()).?;
  415. try t.expectEqual(CLOSE_PROTOCOL_ERROR, ev.protocol_error);
  416. }
  417. test "fragmented control frame is 1002" {
  418. var dec = Decoder.init(t.allocator);
  419. defer dec.deinit();
  420. const frame = try maskedFrame(t.allocator, false, .ping, "x", .{ 1, 2, 3, 4 });
  421. defer t.allocator.free(frame);
  422. try dec.feed(frame);
  423. const ev = (try dec.next()).?;
  424. try t.expectEqual(CLOSE_PROTOCOL_ERROR, ev.protocol_error);
  425. }
  426. test "continuation without message is 1002" {
  427. var dec = Decoder.init(t.allocator);
  428. defer dec.deinit();
  429. const frame = try maskedFrame(t.allocator, true, .continuation, "x", .{ 1, 2, 3, 4 });
  430. defer t.allocator.free(frame);
  431. try dec.feed(frame);
  432. const ev = (try dec.next()).?;
  433. try t.expectEqual(CLOSE_PROTOCOL_ERROR, ev.protocol_error);
  434. }
  435. test "new data frame while assembling is 1002" {
  436. var dec = Decoder.init(t.allocator);
  437. defer dec.deinit();
  438. const f1 = try maskedFrame(t.allocator, false, .text, "a", .{ 1, 2, 3, 4 });
  439. defer t.allocator.free(f1);
  440. const f2 = try maskedFrame(t.allocator, true, .text, "b", .{ 1, 2, 3, 4 });
  441. defer t.allocator.free(f2);
  442. try dec.feed(f1);
  443. try dec.feed(f2);
  444. const ev = (try dec.next()).?;
  445. try t.expectEqual(CLOSE_PROTOCOL_ERROR, ev.protocol_error);
  446. }
  447. test "oversize message is 1009" {
  448. var dec = Decoder.init(t.allocator);
  449. defer dec.deinit();
  450. dec.max_message_size = 8;
  451. const frame = try maskedFrame(t.allocator, true, .text, "123456789", .{ 1, 2, 3, 4 });
  452. defer t.allocator.free(frame);
  453. try dec.feed(frame);
  454. const ev = (try dec.next()).?;
  455. try t.expectEqual(CLOSE_TOO_BIG, ev.protocol_error);
  456. }
  457. test "16-bit extended length round trip" {
  458. var dec = Decoder.init(t.allocator);
  459. defer dec.deinit();
  460. const big = try t.allocator.alloc(u8, 300);
  461. defer t.allocator.free(big);
  462. @memset(big, 'A');
  463. const frame = try maskedFrame(t.allocator, true, .binary, big, .{ 9, 9, 9, 9 });
  464. defer t.allocator.free(frame);
  465. try dec.feed(frame);
  466. var ev = (try dec.next()).?;
  467. defer ev.deinitPayload(t.allocator);
  468. try t.expectEqual(@as(usize, 300), ev.binary.len);
  469. try t.expectEqual(@as(u8, 'A'), ev.binary[150]);
  470. }
  471. test "writeFrame emits parseable frame" {
  472. const Sink = struct {
  473. var buf: std.ArrayListUnmanaged(u8) = .empty;
  474. fn write(_: ?*anyopaque, data: []const u8) bool {
  475. buf.appendSlice(t.allocator, data) catch return false;
  476. return true;
  477. }
  478. };
  479. defer Sink.buf.deinit(t.allocator);
  480. try t.expect(writeFrame(&Sink.write, null, .text, "server says hi"));
  481. // Parse it back with an unmasked-tolerant decoder (client role)
  482. var dec = Decoder.init(t.allocator);
  483. dec.require_masked = false;
  484. defer dec.deinit();
  485. try dec.feed(Sink.buf.items);
  486. var ev = (try dec.next()).?;
  487. defer ev.deinitPayload(t.allocator);
  488. try t.expectEqualStrings("server says hi", ev.text);
  489. }
  490. test "writeClose encodes code and reason" {
  491. const Sink = struct {
  492. var buf: std.ArrayListUnmanaged(u8) = .empty;
  493. fn write(_: ?*anyopaque, data: []const u8) bool {
  494. buf.appendSlice(t.allocator, data) catch return false;
  495. return true;
  496. }
  497. };
  498. defer Sink.buf.deinit(t.allocator);
  499. try t.expect(writeClose(&Sink.write, null, 1000, "bye"));
  500. var dec = Decoder.init(t.allocator);
  501. dec.require_masked = false;
  502. defer dec.deinit();
  503. try dec.feed(Sink.buf.items);
  504. var ev = (try dec.next()).?;
  505. defer ev.deinitPayload(t.allocator);
  506. try t.expectEqual(@as(u16, 1000), ev.close.code);
  507. try t.expectEqualStrings("bye", ev.close.reason);
  508. }

Branches

Latest commits

  • 7538b034mission 011 (ticket #25 web part): short page URLs /<slug> and /<slug>/<number>; /projects/<slug>, /projects/<slug>/<n>, /projects/<slug>/tickets/<n> and /tickets/<ref> answer 301 (current slug); slug pages moved behind hl:web's own routes after construction (they answered /__hl/* and /components/*.hl); reserved slugs refused; API paths unchanged. gate 293/0, connect 60/0, live-data copy 187/0, API old vs new: only page links differmre
  • 0369106emission 010 (code order) 4/4: README file map + import order + 'Same output' test, STATUS (entry, lessons, how to verify), LOG, report; tests/realdata-baseline.mjs + realdata-compare*.py (a cleanup answers the same on live data), tests/letcount.pymre
  • a75e0279mission 010 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (456 lets → plain declarations; Hybriel refuses a plain declaration inside a loop on its 2nd pass). gate 249/0, connect 60/0, real-data reads identical, a 50-step write sequence (API + faces) identical to the old codemre
  • e9d5c618mission 010 (code order) 2/4: one lib/ file per topic — store.hl split into projects / tickets (+ relations) / events / tickets-helpers, util.hl shared helpers (env, storage dir, URLs, sorts, Vienna time), the function routes out of project.hl into lib/api.hl (thin; auth/filters/Accept in api-helpers.hl), invite + member-removal logic out of the faces/routes into invites.hl / tickets.hl; project.hl is the map. /login/callback gets req + the session store by reference. gate 249/0, connect 60/0, real-data reads identicalmre
  • 97e269b5mission 010 (code order) 1/4: .hl files out of the root — lib/ (store, users, connections, invites, migrate, markdown, mdview, import = ticketfile, util = localtime, jsoncheck, api-helpers = api), tools/import.hl, components/styles.hl; import paths only. gate 249/0, connect 60/0, real-data reads identicalmre
  • 38f9d10ftickets: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gate 249/0, connect 60/0mre
  • d3db6139tickets: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gate 249/0, connect 60/0mre
  • bce182e3tickets: Hybriel master 7eea0d32 (#126 memory, #48 lambda copies its argument); migrate.hl lambdas take &logmre
  • 4137be0fantcolony#40: mission references point to the moved missionsmre
  • 9bfba36aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • c7bd2645tickets: Hybriel master 73267707 (#122 fixed); compactNow workaround removed (#110 covered)mre
  • 2ab91ee9tickets: gate checks rows appear once (session sync); re-vendor to ff51cf46 stopped on hybriel#122, stays 837fe120mre
  • e01c2b1dtickets#24: installable app (manifest, service worker, offline list), own icon; gate waits for the hello's pongmre
  • 752fbb7fdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • 38bdd5e4deploy.sh: never send .git or .gitignore to Byrodinmre
  • f12fa1bcState of 2026-09-27, before the move to gitoriamre