M1: doc-level free list, sessions, and a spec runner that no longer overstates #1
29
src/bson.zig
29
src/bson.zig
@@ -74,6 +74,35 @@ pub const Value = union(enum) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The name mongod uses for this type in a TypeMismatch message ("is the
|
||||||
|
/// wrong type 'int', expected type 'object'"). Its own names, not Zig's:
|
||||||
|
/// a driver that matches on the text is matching on these.
|
||||||
|
pub fn type_name(self: Value) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.double => "double",
|
||||||
|
.string => "string",
|
||||||
|
.doc => "object",
|
||||||
|
.array => "array",
|
||||||
|
.binary => "binData",
|
||||||
|
.object_id => "objectId",
|
||||||
|
.bool => "bool",
|
||||||
|
.datetime => "date",
|
||||||
|
.null => "null",
|
||||||
|
.regex => "regex",
|
||||||
|
.code => "javascript",
|
||||||
|
.symbol => "symbol",
|
||||||
|
.int32 => "int",
|
||||||
|
.timestamp => "timestamp",
|
||||||
|
.int64 => "long",
|
||||||
|
.decimal128 => "decimal",
|
||||||
|
.min_key => "minKey",
|
||||||
|
.max_key => "maxKey",
|
||||||
|
// An unparsed value keeps only its tag byte, and the tags this
|
||||||
|
// union does not name are the ones nothing here inspects.
|
||||||
|
.opaque_val => "unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_number(self: Value) bool {
|
pub fn is_number(self: Value) bool {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.double, .int32, .int64 => true,
|
.double, .int32, .int64 => true,
|
||||||
|
|||||||
163
src/wire.zig
163
src/wire.zig
@@ -185,6 +185,81 @@ pub const Message = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A logical session id: a UUID, so 16 bytes of binary subtype 4.
|
||||||
|
pub const SessionId = [16]u8;
|
||||||
|
|
||||||
|
/// Every way an `lsid` can be malformed, named after what is wrong rather
|
||||||
|
/// than after the error the caller will send: the codes belong to the
|
||||||
|
/// command layer, which is where they were measured.
|
||||||
|
pub const LsidError = error{
|
||||||
|
LsidNotDocument,
|
||||||
|
LsidUnknownField,
|
||||||
|
LsidIdMissing,
|
||||||
|
LsidIdNotBinary,
|
||||||
|
LsidIdNotUuid,
|
||||||
|
LsidIdWrongLength,
|
||||||
|
LsidInternalSession,
|
||||||
|
LsidTxnNumberWithoutTxnUuid,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The logical session id, or null when the command carries no `lsid`.
|
||||||
|
///
|
||||||
|
/// An accessor over the command envelope, like `db_name`: called from
|
||||||
|
/// dispatch, never from `parse`, because a malformed session id is a
|
||||||
|
/// command that gets an error reply and not a connection that gets torn
|
||||||
|
/// down.
|
||||||
|
///
|
||||||
|
/// The fields it tolerates were measured against mongod 8.3.7, not
|
||||||
|
/// recalled, and the measurement contradicted the assumption it was
|
||||||
|
/// written on. mongod does *not* ignore unknown fields inside `lsid` --
|
||||||
|
/// it answers IDLUnknownField -- and it does accept `uid`, the hash of
|
||||||
|
/// the credentials that own the session, which a driver sends as soon as
|
||||||
|
/// authentication is on. Both matter to us: the first because tolerating
|
||||||
|
/// what the server rejects is the kind of divergence that only shows up
|
||||||
|
/// under a driver nobody tested, the second because M7 would otherwise
|
||||||
|
/// break every command.
|
||||||
|
pub fn lsid(self: *const Message) LsidError!?SessionId {
|
||||||
|
const v = bson.get_pair(self.body.pairs, "lsid") orelse return null;
|
||||||
|
const doc = switch (v) {
|
||||||
|
.doc => |d| d,
|
||||||
|
else => return error.LsidNotDocument,
|
||||||
|
};
|
||||||
|
|
||||||
|
var id: ?bson.Binary = null;
|
||||||
|
var has_txn_number = false;
|
||||||
|
var has_txn_uuid = false;
|
||||||
|
for (doc) |pair| {
|
||||||
|
if (std.mem.eql(u8, pair.key, "id")) {
|
||||||
|
id = switch (pair.value) {
|
||||||
|
.binary => |b| b,
|
||||||
|
else => return error.LsidIdNotBinary,
|
||||||
|
};
|
||||||
|
} else if (std.mem.eql(u8, pair.key, "uid")) {
|
||||||
|
// Accepted and ignored: it identifies the user a session
|
||||||
|
// belongs to, and this server has exactly one.
|
||||||
|
} else if (std.mem.eql(u8, pair.key, "txnNumber")) {
|
||||||
|
has_txn_number = true;
|
||||||
|
} else if (std.mem.eql(u8, pair.key, "txnUUID")) {
|
||||||
|
has_txn_uuid = true;
|
||||||
|
} else {
|
||||||
|
return error.LsidUnknownField;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A `txnNumber` inside the session id is not the retryable-write one
|
||||||
|
// outside it: together with `txnUUID` the two name an *internal*
|
||||||
|
// session, which only exists to run a transaction on another
|
||||||
|
// session's behalf. Neither can mean anything here, and mongod
|
||||||
|
// refuses them on a standalone too.
|
||||||
|
if (has_txn_number and !has_txn_uuid) return error.LsidTxnNumberWithoutTxnUuid;
|
||||||
|
if (has_txn_uuid) return error.LsidInternalSession;
|
||||||
|
|
||||||
|
const bin = id orelse return error.LsidIdMissing;
|
||||||
|
if (bin.subtype != 4) return error.LsidIdNotUuid;
|
||||||
|
if (bin.data.len != 16) return error.LsidIdWrongLength;
|
||||||
|
return bin.data[0..16].*;
|
||||||
|
}
|
||||||
|
|
||||||
/// Documents of a batch argument (`documents`, `updates`, `deletes`).
|
/// Documents of a batch argument (`documents`, `updates`, `deletes`).
|
||||||
/// Drivers send them either as an OP_MSG document sequence or as an array
|
/// Drivers send them either as an OP_MSG document sequence or as an array
|
||||||
/// inside the command body; callers should not have to care which. The
|
/// inside the command body; callers should not have to care which. The
|
||||||
@@ -378,6 +453,94 @@ test "reply serializes to a parseable message" {
|
|||||||
try testing.expectEqual(@as(usize, 0), msg.seqs.len);
|
try testing.expectEqual(@as(usize, 0), msg.seqs.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An OP_MSG carrying one body section, for tests that care about the command
|
||||||
|
/// envelope rather than about framing.
|
||||||
|
fn fake_op_msg(gpa: std.mem.Allocator, pairs: []const bson.Pair) !Message {
|
||||||
|
var doc: std.ArrayListUnmanaged(u8) = .empty;
|
||||||
|
defer doc.deinit(gpa);
|
||||||
|
try bson.write_doc(pairs, gpa, &doc);
|
||||||
|
|
||||||
|
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||||
|
defer buf.deinit(gpa);
|
||||||
|
try buf.appendSlice(gpa, &[_]u8{ 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0xDD, 0x07, 0, 0, 0, 0, 0, 0 });
|
||||||
|
try buf.append(gpa, 0x00);
|
||||||
|
try buf.appendSlice(gpa, doc.items);
|
||||||
|
std.mem.writeInt(u32, buf.items[0..4], @intCast(buf.items.len), .little);
|
||||||
|
return Message.parse(gpa, buf.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a session id is read out of a command" {
|
||||||
|
const uuid = [_]u8{0xAB} ** 16;
|
||||||
|
var msg = try fake_op_msg(testing.allocator, &.{
|
||||||
|
.{ .key = "ping", .value = .{ .int32 = 1 } },
|
||||||
|
.{ .key = "lsid", .value = .{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } },
|
||||||
|
} } },
|
||||||
|
});
|
||||||
|
defer msg.deinit();
|
||||||
|
try testing.expectEqual(uuid, (try msg.lsid()).?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a command with no lsid has no session" {
|
||||||
|
var msg = try fake_op_msg(testing.allocator, &.{.{ .key = "ping", .value = .{ .int32 = 1 } }});
|
||||||
|
defer msg.deinit();
|
||||||
|
try testing.expect((try msg.lsid()) == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a session id carrying a user hash is still a session id" {
|
||||||
|
// `uid` arrives as soon as authentication is on (M7). Rejecting it as an
|
||||||
|
// unknown field would break every command the moment that lands, which is
|
||||||
|
// exactly the kind of divergence a measurement against a real server is
|
||||||
|
// for -- mongod 8.3.7 answers ok:1 to this.
|
||||||
|
const uuid = [_]u8{0x11} ** 16;
|
||||||
|
var msg = try fake_op_msg(testing.allocator, &.{
|
||||||
|
.{ .key = "ping", .value = .{ .int32 = 1 } },
|
||||||
|
.{ .key = "lsid", .value = .{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } },
|
||||||
|
.{ .key = "uid", .value = .{ .binary = .{ .subtype = 0, .data = &[_]u8{0} ** 32 } } },
|
||||||
|
} } },
|
||||||
|
});
|
||||||
|
defer msg.deinit();
|
||||||
|
try testing.expectEqual(uuid, (try msg.lsid()).?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "every malformed session id is named" {
|
||||||
|
const uuid = [_]u8{0x22} ** 16;
|
||||||
|
const cases = [_]struct { want: anyerror, lsid: bson.Value }{
|
||||||
|
.{ .want = error.LsidNotDocument, .lsid = .{ .int32 = 5 } },
|
||||||
|
.{ .want = error.LsidIdMissing, .lsid = .{ .doc = &.{} } },
|
||||||
|
.{ .want = error.LsidIdNotBinary, .lsid = .{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = .{ .string = "nope" } },
|
||||||
|
} } },
|
||||||
|
.{ .want = error.LsidIdNotUuid, .lsid = .{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = .{ .binary = .{ .subtype = 0, .data = &uuid } } },
|
||||||
|
} } },
|
||||||
|
.{ .want = error.LsidIdWrongLength, .lsid = .{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = uuid[0..15] } } },
|
||||||
|
} } },
|
||||||
|
.{ .want = error.LsidUnknownField, .lsid = .{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } },
|
||||||
|
.{ .key = "bogus", .value = .{ .int32 = 1 } },
|
||||||
|
} } },
|
||||||
|
.{ .want = error.LsidTxnNumberWithoutTxnUuid, .lsid = .{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } },
|
||||||
|
.{ .key = "txnNumber", .value = .{ .int64 = 1 } },
|
||||||
|
} } },
|
||||||
|
.{ .want = error.LsidInternalSession, .lsid = .{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } },
|
||||||
|
.{ .key = "txnUUID", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } },
|
||||||
|
} } },
|
||||||
|
};
|
||||||
|
for (cases) |c| {
|
||||||
|
var msg = try fake_op_msg(testing.allocator, &.{
|
||||||
|
.{ .key = "ping", .value = .{ .int32 = 1 } },
|
||||||
|
.{ .key = "lsid", .value = c.lsid },
|
||||||
|
});
|
||||||
|
defer msg.deinit();
|
||||||
|
try testing.expectError(c.want, msg.lsid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "reject non-OP_MSG non-OP_QUERY opcodes" {
|
test "reject non-OP_MSG non-OP_QUERY opcodes" {
|
||||||
var buf: [20]u8 = undefined;
|
var buf: [20]u8 = undefined;
|
||||||
std.mem.writeInt(u32, buf[0..4], 20, .little);
|
std.mem.writeInt(u32, buf[0..4], 20, .little);
|
||||||
|
|||||||
Reference in New Issue
Block a user