Skip to content

Commit 9966d0c

Browse files
committed
feat(msgpack): add MessagePack timestamp type support
- implement Timestamp struct with seconds and nanoseconds fields - support MessagePack timestamp 32, 64, and 96 formats in serialization and deserialization - extend Payload union to include timestamp variant and utility constructors - add packing and unpacking logic for timestamps in Pack - provide comprehensive tests for timestamp encoding, decoding, edge cases, and format selection
1 parent 0aec4c4 commit 9966d0c

File tree

2 files changed

+614
-4
lines changed

2 files changed

+614
-4
lines changed

src/msgpack.zig

Lines changed: 236 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,38 @@ pub fn wrapEXT(t: i8, data: []u8) EXT {
6464
};
6565
}
6666

67+
/// the Timestamp Type
68+
/// Represents an instantaneous point on the time-line in the world
69+
/// that is independent from time zones or calendars.
70+
/// Maximum precision is nanoseconds.
71+
pub const Timestamp = struct {
72+
/// seconds since 1970-01-01 00:00:00 UTC
73+
seconds: i64,
74+
/// nanoseconds (0-999999999)
75+
nanoseconds: u32,
76+
77+
/// Create a new timestamp
78+
pub fn new(seconds: i64, nanoseconds: u32) Timestamp {
79+
return Timestamp{
80+
.seconds = seconds,
81+
.nanoseconds = nanoseconds,
82+
};
83+
}
84+
85+
/// Create timestamp from seconds only (nanoseconds = 0)
86+
pub fn fromSeconds(seconds: i64) Timestamp {
87+
return Timestamp{
88+
.seconds = seconds,
89+
.nanoseconds = 0,
90+
};
91+
}
92+
93+
/// Get total seconds as f64 (including fractional nanoseconds)
94+
pub fn toFloat(self: Timestamp) f64 {
95+
return @as(f64, @floatFromInt(self.seconds)) + @as(f64, @floatFromInt(self.nanoseconds)) / 1_000_000_000.0;
96+
}
97+
};
98+
6799
/// the map of payload
68100
pub const Map = std.StringHashMap(Payload);
69101

@@ -87,6 +119,7 @@ pub const Payload = union(enum) {
87119
arr: []Payload,
88120
map: Map,
89121
ext: EXT,
122+
timestamp: Timestamp,
90123

91124
/// get array element
92125
pub fn getArrElement(self: Payload, index: usize) !Payload {
@@ -222,6 +255,20 @@ pub const Payload = union(enum) {
222255
};
223256
}
224257

258+
/// get a timestamp payload
259+
pub fn timestampToPayload(seconds: i64, nanoseconds: u32) Payload {
260+
return Payload{
261+
.timestamp = Timestamp.new(seconds, nanoseconds),
262+
};
263+
}
264+
265+
/// get a timestamp payload from seconds only
266+
pub fn timestampFromSeconds(seconds: i64) Payload {
267+
return Payload{
268+
.timestamp = Timestamp.fromSeconds(seconds),
269+
};
270+
}
271+
225272
/// free the all memeory for this payload and sub payloads
226273
/// the allocator is payload's allocator
227274
pub fn free(self: Payload, allocator: Allocator) void {
@@ -847,6 +894,43 @@ pub fn Pack(
847894
}
848895
}
849896

897+
/// write timestamp
898+
fn writeTimestamp(self: Self, timestamp: Timestamp) !void {
899+
// According to MessagePack spec, timestamp uses extension type -1
900+
const TIMESTAMP_TYPE: i8 = -1;
901+
902+
// timestamp 32 format: seconds fit in 32-bit unsigned int and nanoseconds is 0
903+
if (timestamp.nanoseconds == 0 and timestamp.seconds >= 0 and timestamp.seconds <= 0xffffffff) {
904+
var data: [4]u8 = undefined;
905+
std.mem.writeInt(u32, &data, @intCast(timestamp.seconds), big_endian);
906+
const ext = EXT{ .type = TIMESTAMP_TYPE, .data = &data };
907+
try self.writeExt(ext);
908+
return;
909+
}
910+
911+
// timestamp 64 format: seconds fit in 34-bit and nanoseconds <= 999999999
912+
if (timestamp.seconds >= 0 and (timestamp.seconds >> 34) == 0 and timestamp.nanoseconds <= 999999999) {
913+
const data64: u64 = (@as(u64, timestamp.nanoseconds) << 34) | @as(u64, @intCast(timestamp.seconds));
914+
var data: [8]u8 = undefined;
915+
std.mem.writeInt(u64, &data, data64, big_endian);
916+
const ext = EXT{ .type = TIMESTAMP_TYPE, .data = &data };
917+
try self.writeExt(ext);
918+
return;
919+
}
920+
921+
// timestamp 96 format: full range with signed 64-bit seconds and 32-bit nanoseconds
922+
if (timestamp.nanoseconds <= 999999999) {
923+
var data: [12]u8 = undefined;
924+
std.mem.writeInt(u32, data[0..4], timestamp.nanoseconds, big_endian);
925+
std.mem.writeInt(i64, data[4..12], timestamp.seconds, big_endian);
926+
const ext = EXT{ .type = TIMESTAMP_TYPE, .data = &data };
927+
try self.writeExt(ext);
928+
return;
929+
}
930+
931+
return MsGPackError.INVALID_TYPE;
932+
}
933+
850934
/// write payload
851935
pub fn write(self: Self, payload: Payload) !void {
852936
switch (payload) {
@@ -912,6 +996,9 @@ pub fn Pack(
912996
.ext => |ext| {
913997
try self.writeExt(ext);
914998
},
999+
.timestamp => |timestamp| {
1000+
try self.writeTimestamp(timestamp);
1001+
},
9151002
}
9161003
}
9171004

@@ -1284,6 +1371,153 @@ pub fn Pack(
12841371
};
12851372
}
12861373

1374+
/// read ext value or timestamp if it's timestamp type (-1)
1375+
fn readExtValueOrTimestamp(self: Self, marker: Markers, allocator: Allocator) !Payload {
1376+
const TIMESTAMP_TYPE: i8 = -1;
1377+
1378+
// First, check if this could be a timestamp format
1379+
if (marker == .FIXEXT4 or marker == .FIXEXT8 or marker == .EXT8) {
1380+
// Read and check length for EXT8
1381+
var actual_len: usize = 0;
1382+
if (marker == .EXT8) {
1383+
actual_len = try self.readV8Value();
1384+
if (actual_len != 12) {
1385+
// Not timestamp 96, read as regular EXT
1386+
const ext_type = try self.readI8Value();
1387+
const ext_data = try allocator.alloc(u8, actual_len);
1388+
_ = try self.readFrom(ext_data);
1389+
return Payload{ .ext = EXT{ .type = ext_type, .data = ext_data } };
1390+
}
1391+
} else if (marker == .FIXEXT4) {
1392+
actual_len = 4;
1393+
} else if (marker == .FIXEXT8) {
1394+
actual_len = 8;
1395+
}
1396+
1397+
// Read the type
1398+
const ext_type = try self.readI8Value();
1399+
1400+
if (ext_type == TIMESTAMP_TYPE) {
1401+
// This is a timestamp
1402+
if (marker == .FIXEXT4) {
1403+
// timestamp 32
1404+
const seconds = try self.readU32Value();
1405+
return Payload{ .timestamp = Timestamp.new(@intCast(seconds), 0) };
1406+
} else if (marker == .FIXEXT8) {
1407+
// timestamp 64
1408+
const data64 = try self.readU64Value();
1409+
const nanoseconds: u32 = @intCast(data64 >> 34);
1410+
const seconds: i64 = @intCast(data64 & 0x3ffffffff);
1411+
return Payload{ .timestamp = Timestamp.new(seconds, nanoseconds) };
1412+
} else if (marker == .EXT8) {
1413+
// timestamp 96
1414+
const nanoseconds = try self.readU32Value();
1415+
const seconds = try self.readI64Value();
1416+
return Payload{ .timestamp = Timestamp.new(seconds, nanoseconds) };
1417+
}
1418+
} else {
1419+
// Not a timestamp, read as regular EXT
1420+
const ext_data = try allocator.alloc(u8, actual_len);
1421+
_ = try self.readFrom(ext_data);
1422+
return Payload{ .ext = EXT{ .type = ext_type, .data = ext_data } };
1423+
}
1424+
}
1425+
1426+
// Regular EXT processing
1427+
const val = try self.readExtValue(marker, allocator);
1428+
return Payload{ .ext = val };
1429+
}
1430+
1431+
/// try to read timestamp from ext data, return error if not timestamp
1432+
fn tryReadTimestamp(self: Self, marker: Markers, _: Allocator) !Timestamp {
1433+
const TIMESTAMP_TYPE: i8 = -1;
1434+
1435+
switch (marker) {
1436+
.FIXEXT4 => {
1437+
// timestamp 32 format
1438+
const ext_type = try self.readI8Value();
1439+
if (ext_type != TIMESTAMP_TYPE) {
1440+
return MsGPackError.INVALID_TYPE;
1441+
}
1442+
const seconds = try self.readU32Value();
1443+
return Timestamp.new(@intCast(seconds), 0);
1444+
},
1445+
.FIXEXT8 => {
1446+
// timestamp 64 format
1447+
const ext_type = try self.readI8Value();
1448+
if (ext_type != TIMESTAMP_TYPE) {
1449+
return MsGPackError.INVALID_TYPE;
1450+
}
1451+
const data64 = try self.readU64Value();
1452+
const nanoseconds: u32 = @intCast(data64 >> 34);
1453+
const seconds: i64 = @intCast(data64 & 0x3ffffffff);
1454+
return Timestamp.new(seconds, nanoseconds);
1455+
},
1456+
.EXT8 => {
1457+
// timestamp 96 format (length should be 12)
1458+
const len = try self.readV8Value();
1459+
if (len != 12) {
1460+
return MsGPackError.INVALID_TYPE;
1461+
}
1462+
const ext_type = try self.readI8Value();
1463+
if (ext_type != TIMESTAMP_TYPE) {
1464+
return MsGPackError.INVALID_TYPE;
1465+
}
1466+
const nanoseconds = try self.readU32Value();
1467+
const seconds = try self.readI64Value();
1468+
return Timestamp.new(seconds, nanoseconds);
1469+
},
1470+
else => {
1471+
return MsGPackError.INVALID_TYPE;
1472+
},
1473+
}
1474+
}
1475+
1476+
/// read timestamp from ext data
1477+
fn readTimestamp(self: Self, marker: Markers, _: Allocator) !Timestamp {
1478+
const TIMESTAMP_TYPE: i8 = -1;
1479+
1480+
switch (marker) {
1481+
.FIXEXT4 => {
1482+
// timestamp 32 format
1483+
const ext_type = try self.readI8Value();
1484+
if (ext_type != TIMESTAMP_TYPE) {
1485+
return MsGPackError.INVALID_TYPE;
1486+
}
1487+
const seconds = try self.readU32Value();
1488+
return Timestamp.new(@intCast(seconds), 0);
1489+
},
1490+
.FIXEXT8 => {
1491+
// timestamp 64 format
1492+
const ext_type = try self.readI8Value();
1493+
if (ext_type != TIMESTAMP_TYPE) {
1494+
return MsGPackError.INVALID_TYPE;
1495+
}
1496+
const data64 = try self.readU64Value();
1497+
const nanoseconds: u32 = @intCast(data64 >> 34);
1498+
const seconds: i64 = @intCast(data64 & 0x3ffffffff);
1499+
return Timestamp.new(seconds, nanoseconds);
1500+
},
1501+
.EXT8 => {
1502+
// timestamp 96 format (length should be 12)
1503+
const len = try self.readV8Value();
1504+
if (len != 12) {
1505+
return MsGPackError.INVALID_TYPE;
1506+
}
1507+
const ext_type = try self.readI8Value();
1508+
if (ext_type != TIMESTAMP_TYPE) {
1509+
return MsGPackError.INVALID_TYPE;
1510+
}
1511+
const nanoseconds = try self.readU32Value();
1512+
const seconds = try self.readI64Value();
1513+
return Timestamp.new(seconds, nanoseconds);
1514+
},
1515+
else => {
1516+
return MsGPackError.INVALID_TYPE;
1517+
},
1518+
}
1519+
}
1520+
12871521
fn readExtValue(self: Self, marker: Markers, allocator: Allocator) !EXT {
12881522
switch (marker) {
12891523
.FIXEXT1 => {
@@ -1446,10 +1680,8 @@ pub fn Pack(
14461680
.EXT16,
14471681
.EXT32,
14481682
=> {
1449-
const val = try self.readExtValue(marker, allocator);
1450-
res = Payload{
1451-
.ext = val,
1452-
};
1683+
const ext_result = try self.readExtValueOrTimestamp(marker, allocator);
1684+
res = ext_result;
14531685
},
14541686
}
14551687
return res;

0 commit comments

Comments
 (0)