pkbfi.zig 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. const std = @import("std");
  2. const engine_mod = @import("engine.zig");
  3. const pkvdb = @import("pkvdb.zig");
  4. pub const header_size = 32;
  5. pub const max_frame_size = pkvdb.max_key_size + pkvdb.max_value_size + 1024;
  6. pub const Opcode = enum(u16) {
  7. ping = 1,
  8. status = 2,
  9. get = 3,
  10. put = 4,
  11. delete = 5,
  12. exists = 6,
  13. multi_get = 7,
  14. batch_write = 8,
  15. scan_open = 9,
  16. scan_next = 10,
  17. scan_close = 11,
  18. };
  19. pub const Frame = struct {
  20. opcode: Opcode,
  21. flags: u16,
  22. request_id: u64,
  23. payload: []const u8,
  24. consumed: usize,
  25. };
  26. const Scan = struct {
  27. id: u64,
  28. prefix: []u8,
  29. cursor: []u8,
  30. include_values: bool,
  31. limit: u32,
  32. };
  33. pub const Session = struct {
  34. allocator: std.mem.Allocator,
  35. scans: std.ArrayListUnmanaged(Scan) = .{},
  36. next_scan_id: u64 = 1,
  37. pub fn init(allocator: std.mem.Allocator) Session {
  38. return .{ .allocator = allocator };
  39. }
  40. pub fn deinit(self: *Session) void {
  41. for (self.scans.items) |scan| {
  42. self.allocator.free(scan.prefix);
  43. self.allocator.free(scan.cursor);
  44. }
  45. self.scans.deinit(self.allocator);
  46. self.* = undefined;
  47. }
  48. fn findScan(self: *Session, id: u64) ?usize {
  49. for (self.scans.items, 0..) |scan, index| if (scan.id == id) return index;
  50. return null;
  51. }
  52. pub fn execute(self: *Session, engine: *engine_mod.Engine, frame: Frame) ![]u8 {
  53. var body = std.ArrayListUnmanaged(u8){};
  54. defer body.deinit(self.allocator);
  55. self.executeBody(engine, frame, &body) catch |err| {
  56. body.clearRetainingCapacity();
  57. try appendInt(u16, &body, self.allocator, 2);
  58. try body.appendSlice(self.allocator, @errorName(err));
  59. };
  60. return encode(self.allocator, @intFromEnum(frame.opcode) | 0x8000, 1, frame.request_id, body.items);
  61. }
  62. fn executeBody(self: *Session, engine: *engine_mod.Engine, frame: Frame, body: *std.ArrayListUnmanaged(u8)) !void {
  63. try appendInt(u16, body, self.allocator, 0);
  64. switch (frame.opcode) {
  65. .ping => try body.appendSlice(self.allocator, frame.payload),
  66. .status => {
  67. if (frame.payload.len != 0) return error.InvalidPayload;
  68. const status = engine.status();
  69. try body.appendSlice(self.allocator, &status.uuid);
  70. inline for (.{ status.file_bytes, status.latest_lsn, status.oldest_lsn, status.checkpoint_lsn, status.journal_bytes_since_checkpoint, status.live_keys, status.keydir_bytes, status.ordered_index_bytes, status.bytes_written, status.checksum_failures, status.partial_tails, status.recovery_ns, status.checkpoint_ns, status.connection_bytes, status.active_requests, status.commit_groups, status.committed_transactions, status.largest_commit_group }) |value| try appendInt(u64, body, self.allocator, value);
  71. },
  72. .get => {
  73. const key = try oneKey(frame.payload);
  74. if (try engine.get(self.allocator, key)) |value| {
  75. defer self.allocator.free(value.bytes);
  76. try appendInt(u64, body, self.allocator, value.lsn);
  77. try appendInt(u32, body, self.allocator, @intCast(value.bytes.len));
  78. try body.appendSlice(self.allocator, value.bytes);
  79. } else {
  80. body.clearRetainingCapacity();
  81. try appendInt(u16, body, self.allocator, 1);
  82. }
  83. },
  84. .put => {
  85. const operation = try putOperation(frame);
  86. const lsn = try engine.put(operation.key, operation.value);
  87. try appendInt(u64, body, self.allocator, lsn);
  88. },
  89. .delete => {
  90. const deleted = try engine.delete(try oneKey(frame.payload));
  91. try body.append(self.allocator, @intFromBool(deleted));
  92. },
  93. .exists => {
  94. const present = try engine.exists(try oneKey(frame.payload));
  95. try body.append(self.allocator, @intFromBool(present));
  96. },
  97. .multi_get => try self.multiGet(engine, frame.payload, body),
  98. .batch_write => try self.batchWrite(engine, frame.payload, body),
  99. .scan_open => try self.scanOpen(frame.payload, body),
  100. .scan_next => try self.scanNext(engine, frame.payload, body),
  101. .scan_close => try self.scanClose(frame.payload, body),
  102. }
  103. }
  104. fn multiGet(self: *Session, engine: *engine_mod.Engine, payload: []const u8, body: *std.ArrayListUnmanaged(u8)) !void {
  105. if (payload.len < 4) return error.InvalidPayload;
  106. const count = readInt(u32, payload, 0);
  107. if (count > pkvdb.max_operations) return error.InvalidPayload;
  108. const keys = try self.allocator.alloc([]const u8, count);
  109. defer self.allocator.free(keys);
  110. var position: usize = 4;
  111. for (keys) |*key| {
  112. if (position > payload.len or payload.len - position < 4) return error.InvalidPayload;
  113. const length = readInt(u32, payload, position);
  114. position = try std.math.add(usize, position, 4);
  115. const end = try std.math.add(usize, position, length);
  116. if (end > payload.len or length > pkvdb.max_key_size) return error.InvalidPayload;
  117. key.* = payload[position..end];
  118. position = end;
  119. }
  120. if (position != payload.len) return error.InvalidPayload;
  121. const values = try engine.multiGet(self.allocator, keys);
  122. defer {
  123. for (values) |value| if (value) |present| self.allocator.free(present.bytes);
  124. self.allocator.free(values);
  125. }
  126. try appendInt(u32, body, self.allocator, count);
  127. for (values) |value| if (value) |present| {
  128. try body.append(self.allocator, 1);
  129. try body.appendNTimes(self.allocator, 0, 3);
  130. try appendInt(u32, body, self.allocator, @intCast(present.bytes.len));
  131. try appendInt(u64, body, self.allocator, present.lsn);
  132. try body.appendSlice(self.allocator, present.bytes);
  133. } else {
  134. try body.appendNTimes(self.allocator, 0, 16);
  135. };
  136. }
  137. fn batchWrite(self: *Session, engine: *engine_mod.Engine, payload: []const u8, body: *std.ArrayListUnmanaged(u8)) !void {
  138. if (payload.len < 8) return error.InvalidPayload;
  139. const count = readInt(u32, payload, 0);
  140. const metadata_length = readInt(u32, payload, 4);
  141. if (count == 0 or count > pkvdb.max_operations or metadata_length > payload.len - 8) return error.InvalidPayload;
  142. const metadata_end = 8 + metadata_length;
  143. const metadata = payload[8..metadata_end];
  144. const operations = try self.allocator.alloc(engine_mod.Operation, count);
  145. defer self.allocator.free(operations);
  146. var position: usize = metadata_end;
  147. for (operations) |*operation| {
  148. if (position > payload.len or payload.len - position < 12) return error.InvalidPayload;
  149. const opcode: pkvdb.Opcode = std.meta.intToEnum(pkvdb.Opcode, payload[position]) catch return error.InvalidPayload;
  150. const key_length = readInt(u32, payload, position + 4);
  151. const value_length = readInt(u32, payload, position + 8);
  152. position += 12;
  153. const key_end = try std.math.add(usize, position, key_length);
  154. const value_end = try std.math.add(usize, key_end, value_length);
  155. if (value_end > payload.len or key_length > pkvdb.max_key_size or value_length > pkvdb.max_value_size or (opcode == .delete and value_length != 0)) return error.InvalidPayload;
  156. operation.* = .{ .opcode = opcode, .key = payload[position..key_end], .value = payload[key_end..value_end] };
  157. position = value_end;
  158. }
  159. if (position != payload.len) return error.InvalidPayload;
  160. try appendInt(u64, body, self.allocator, try engine.batchWrite(operations, metadata));
  161. }
  162. fn scanOpen(self: *Session, payload: []const u8, body: *std.ArrayListUnmanaged(u8)) !void {
  163. if (payload.len < 12 or self.scans.items.len >= 64) return error.InvalidPayload;
  164. const include_values = payload[0] != 0;
  165. const limit = readInt(u32, payload, 4);
  166. const prefix_length = readInt(u32, payload, 8);
  167. if (limit == 0 or limit > 4096 or prefix_length > pkvdb.max_key_size or 12 + prefix_length != payload.len) return error.InvalidPayload;
  168. const prefix = try self.allocator.dupe(u8, payload[12..]);
  169. errdefer self.allocator.free(prefix);
  170. const cursor = try self.allocator.alloc(u8, 0);
  171. errdefer self.allocator.free(cursor);
  172. const id = self.next_scan_id;
  173. self.next_scan_id +%= 1;
  174. if (self.next_scan_id == 0) self.next_scan_id = 1;
  175. try self.scans.append(self.allocator, .{ .id = id, .prefix = prefix, .cursor = cursor, .include_values = include_values, .limit = limit });
  176. try appendInt(u64, body, self.allocator, id);
  177. }
  178. fn scanNext(self: *Session, engine: *engine_mod.Engine, payload: []const u8, body: *std.ArrayListUnmanaged(u8)) !void {
  179. if (payload.len != 12) return error.InvalidPayload;
  180. const index = self.findScan(readInt(u64, payload, 0)) orelse return error.ScanNotFound;
  181. const requested = readInt(u32, payload, 8);
  182. const scan = &self.scans.items[index];
  183. const limit = if (requested == 0) scan.limit else @min(requested, scan.limit);
  184. var batch = try engine.scan(self.allocator, scan.prefix, scan.cursor, limit, scan.include_values, 1024 * 1024);
  185. defer batch.deinit(self.allocator);
  186. try body.append(self.allocator, @intFromBool(batch.done));
  187. try body.appendNTimes(self.allocator, 0, 3);
  188. try appendInt(u32, body, self.allocator, @intCast(batch.entries.len));
  189. for (batch.entries) |entry| {
  190. try appendInt(u32, body, self.allocator, @intCast(entry.key.len));
  191. try appendInt(u32, body, self.allocator, @intCast(if (entry.value) |value| value.len else 0));
  192. try appendInt(u64, body, self.allocator, entry.lsn);
  193. try body.appendSlice(self.allocator, entry.key);
  194. if (entry.value) |value| try body.appendSlice(self.allocator, value);
  195. }
  196. const cursor = try self.allocator.dupe(u8, batch.next_cursor);
  197. self.allocator.free(scan.cursor);
  198. scan.cursor = cursor;
  199. }
  200. fn scanClose(self: *Session, payload: []const u8, body: *std.ArrayListUnmanaged(u8)) !void {
  201. if (payload.len != 8) return error.InvalidPayload;
  202. const index = self.findScan(readInt(u64, payload, 0)) orelse return error.ScanNotFound;
  203. const scan = self.scans.orderedRemove(index);
  204. self.allocator.free(scan.prefix);
  205. self.allocator.free(scan.cursor);
  206. try body.append(self.allocator, 1);
  207. }
  208. };
  209. pub fn putOperation(frame: Frame) !engine_mod.Operation {
  210. if (frame.opcode != .put or frame.payload.len < 8) return error.InvalidPayload;
  211. const key_length = readInt(u32, frame.payload, 0);
  212. const value_length = readInt(u32, frame.payload, 4);
  213. const total = try std.math.add(usize, 8, try std.math.add(usize, key_length, value_length));
  214. if (total != frame.payload.len or key_length > pkvdb.max_key_size or value_length > pkvdb.max_value_size) return error.InvalidPayload;
  215. return .{ .opcode = .put, .key = frame.payload[8 .. 8 + key_length], .value = frame.payload[8 + key_length ..] };
  216. }
  217. fn oneKey(payload: []const u8) ![]const u8 {
  218. if (payload.len < 4) return error.InvalidPayload;
  219. const length = readInt(u32, payload, 0);
  220. if (length > pkvdb.max_key_size or 4 + length != payload.len) return error.InvalidPayload;
  221. return payload[4..];
  222. }
  223. pub fn parse(bytes: []const u8) !Frame {
  224. if (bytes.len < header_size) return error.Incomplete;
  225. if (!std.mem.eql(u8, bytes[0..4], "PKBF")) return error.InvalidMagic;
  226. if (readInt(u16, bytes, 4) != 1) return error.IncompatibleVersion;
  227. const payload_length = readInt(u32, bytes, 20);
  228. if (payload_length > max_frame_size) return error.FrameTooLarge;
  229. const total = try std.math.add(usize, header_size, payload_length);
  230. if (total > bytes.len) return error.Incomplete;
  231. var header: [32]u8 = undefined;
  232. @memcpy(&header, bytes[0..32]);
  233. const header_crc = readInt(u32, &header, 28);
  234. writeInt(u32, &header, 28, 0);
  235. if (pkvdb.crc32c(&header) != header_crc) return error.ChecksumMismatch;
  236. const payload = bytes[header_size..total];
  237. if (pkvdb.crc32c(payload) != readInt(u32, bytes, 24)) return error.ChecksumMismatch;
  238. const opcode = std.meta.intToEnum(Opcode, readInt(u16, bytes, 8) & 0x7fff) catch return error.UnknownOpcode;
  239. return .{ .opcode = opcode, .flags = readInt(u16, bytes, 10), .request_id = readInt(u64, bytes, 12), .payload = payload, .consumed = total };
  240. }
  241. pub fn encode(allocator: std.mem.Allocator, opcode: u16, flags: u16, request_id: u64, payload: []const u8) ![]u8 {
  242. if (payload.len > max_frame_size) return error.FrameTooLarge;
  243. const output = try allocator.alloc(u8, header_size + payload.len);
  244. errdefer allocator.free(output);
  245. @memset(output[0..header_size], 0);
  246. @memcpy(output[0..4], "PKBF");
  247. writeInt(u16, output, 4, 1);
  248. writeInt(u16, output, 6, 0);
  249. writeInt(u16, output, 8, opcode);
  250. writeInt(u16, output, 10, flags);
  251. writeInt(u64, output, 12, request_id);
  252. writeInt(u32, output, 20, @intCast(payload.len));
  253. writeInt(u32, output, 24, pkvdb.crc32c(payload));
  254. writeInt(u32, output, 28, 0);
  255. writeInt(u32, output, 28, pkvdb.crc32c(output[0..header_size]));
  256. @memcpy(output[header_size..], payload);
  257. return output;
  258. }
  259. fn appendInt(comptime T: type, list: *std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator, value: T) !void {
  260. var bytes: [@sizeOf(T)]u8 = undefined;
  261. std.mem.writeInt(T, &bytes, value, .little);
  262. try list.appendSlice(allocator, &bytes);
  263. }
  264. fn readInt(comptime T: type, bytes: []const u8, offset: usize) T {
  265. return std.mem.readInt(T, bytes[offset..][0..@sizeOf(T)], .little);
  266. }
  267. fn writeInt(comptime T: type, bytes: []u8, offset: usize, value: T) void {
  268. std.mem.writeInt(T, bytes[offset..][0..@sizeOf(T)], value, .little);
  269. }
  270. test "PKBFI frame round trip and malicious length" {
  271. const bytes = try encode(std.testing.allocator, @intFromEnum(Opcode.ping), 0, 42, "a\x00b");
  272. defer std.testing.allocator.free(bytes);
  273. const frame = try parse(bytes);
  274. try std.testing.expectEqual(@as(u64, 42), frame.request_id);
  275. try std.testing.expectEqualSlices(u8, "a\x00b", frame.payload);
  276. var bad = [_]u8{0} ** header_size;
  277. @memcpy(bad[0..4], "PKBF");
  278. writeInt(u16, &bad, 4, 1);
  279. writeInt(u32, &bad, 20, max_frame_size + 1);
  280. try std.testing.expectError(error.FrameTooLarge, parse(&bad));
  281. }
  282. test "PKBFI binary point batch and streaming scan" {
  283. var tmp = std.testing.tmpDir(.{});
  284. defer tmp.cleanup();
  285. var path_buffer: [std.fs.max_path_bytes]u8 = undefined;
  286. const directory = try tmp.dir.realpath(".", &path_buffer);
  287. const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/pkbfi.pkvdb", .{directory});
  288. defer std.testing.allocator.free(path);
  289. var engine = try engine_mod.Engine.open(std.testing.allocator, path);
  290. defer engine.close();
  291. var session = Session.init(std.testing.allocator);
  292. defer session.deinit();
  293. var put_payload = [_]u8{0} ** 14;
  294. writeInt(u32, &put_payload, 0, 2);
  295. writeInt(u32, &put_payload, 4, 4);
  296. @memcpy(put_payload[8..10], "k\x00");
  297. @memcpy(put_payload[10..14], "v\x00x\n");
  298. const put_frame = Frame{ .opcode = .put, .flags = 0, .request_id = 1, .payload = &put_payload, .consumed = 0 };
  299. const response = try session.execute(&engine, put_frame);
  300. defer std.testing.allocator.free(response);
  301. const parsed = try parse(response);
  302. try std.testing.expectEqual(@as(u16, 0), readInt(u16, parsed.payload, 0));
  303. var open_payload = [_]u8{0} ** 12;
  304. open_payload[0] = 1;
  305. writeInt(u32, &open_payload, 4, 1);
  306. writeInt(u32, &open_payload, 8, 0);
  307. const open_response = try session.execute(&engine, .{ .opcode = .scan_open, .flags = 0, .request_id = 2, .payload = &open_payload, .consumed = 0 });
  308. defer std.testing.allocator.free(open_response);
  309. const open_frame = try parse(open_response);
  310. const scan_id = readInt(u64, open_frame.payload, 2);
  311. var next_payload: [12]u8 = undefined;
  312. writeInt(u64, &next_payload, 0, scan_id);
  313. writeInt(u32, &next_payload, 8, 1);
  314. const next_response = try session.execute(&engine, .{ .opcode = .scan_next, .flags = 0, .request_id = 3, .payload = &next_payload, .consumed = 0 });
  315. defer std.testing.allocator.free(next_response);
  316. const next_frame = try parse(next_response);
  317. try std.testing.expectEqual(@as(u32, 1), readInt(u32, next_frame.payload, 6));
  318. }