-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathbytes.zig
More file actions
437 lines (386 loc) · 17.6 KB
/
bytes.zig
File metadata and controls
437 lines (386 loc) · 17.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
//! This module handles the generation of Zig code for Protocol Buffer bytes and string fields.
//! Both field types are handled similarly as they share the same wire format representation.
//! The module provides functionality to create reader and writer methods, handle defaults,
//! and manage wire format encoding for both bytes and string fields.
// .'\ /`.
// .'.-.`-'.-.`.
// ..._: .-. .-. :_...
// .' '-.(o ) (o ).-' `.
// : _ _ _`~(_)~`_ _ _ :
// : /: ' .-=_ _=-. ` ;\ :
// : :|-.._ ' ` _..-|: :
// : `:| |`:-:-.-:-:'| |:' :
// `. `.| | | | | | |.' .'
// `. `-:_| | |_:-' .'
// `-._ ```` _.-'
// ``-------''
//
// Created by ab, 10.11.2024
const std = @import("std");
const naming = @import("naming.zig");
const Option = @import("../../../parser/main.zig").Option;
/// Formats a string literal for use in Zig code, properly escaping special characters
/// and converting to hexadecimal representation where necessary. This is particularly
/// important for handling default values of both string and bytes fields.
///
/// Parameters:
/// allocator: Memory allocator to use for the result
/// str: Input string to format (including quotes)
/// Returns:
/// Formatted string with proper escaping, suitable for use in Zig code
fn formatStringLiteral(allocator: std.mem.Allocator, str: []const u8) ![]const u8 {
var result = try std.ArrayList(u8).initCapacity(allocator, str.len * 2);
defer result.deinit(allocator);
// Remove surrounding quotes from input
const cropped = str[1 .. str.len - 1];
try result.appendSlice(allocator, "\"");
// Process each character, applying appropriate escaping
for (cropped) |c| {
switch (c) {
// Control characters
0 => try result.appendSlice(allocator, "\\x00"),
1 => try result.appendSlice(allocator, "\\x01"),
7 => try result.appendSlice(allocator, "\\a"),
8 => try result.appendSlice(allocator, "\\b"),
12 => try result.appendSlice(allocator, "\\f"),
'\n' => try result.appendSlice(allocator, "\\n"),
'\r' => try result.appendSlice(allocator, "\\r"),
'\t' => try result.appendSlice(allocator, "\\t"),
11 => try result.appendSlice(allocator, "\\v"),
// Special characters that need escaping
'\\' => try result.appendSlice(allocator, "\\\\"),
'\'' => try result.appendSlice(allocator, "\\'"),
'"' => try result.appendSlice(allocator, "\\\""),
// All other characters are converted to hex for consistent representation
else => {
var buf: [32]u8 = undefined;
const hex = try std.fmt.bufPrint(&buf, "\\x{X:0>2}", .{c});
try result.appendSlice(allocator, hex);
},
}
}
try result.appendSlice(allocator, "\"");
return result.toOwnedSlice(allocator);
}
/// Represents a Protocol Buffer bytes/string field in Zig, managing both reading and writing
/// of the field along with wire format details. The same structure is used for both bytes
/// and string fields as they share the same underlying representation.
pub const ZigBytesField = struct {
// Memory management
allocator: std.mem.Allocator,
// Field properties
custom_default: ?[]const u8, // Optional default value for the field
// Owned struct
writer_struct_name: []const u8,
reader_struct_name: []const u8,
// Generated names for field access
writer_field_name: []const u8, // Name in the writer struct
reader_field_name: []const u8, // Internal name in reader struct
reader_method_name: []const u8, // Public getter method name
// Wire format metadata
wire_const_full_name: []const u8, // Full qualified wire constant name
wire_const_name: []const u8, // Short wire constant name
wire_index: i32, // Field number in the protocol
/// Initialize a new ZigBytesField with the given parameters.
/// This handles setup for both bytes and string fields.
pub fn init(
allocator: std.mem.Allocator,
field_name: []const u8,
field_opts: ?std.ArrayList(Option),
field_index: i32,
wire_prefix: []const u8,
names: *std.ArrayList([]const u8),
writer_struct_name: []const u8,
reader_struct_name: []const u8,
) !ZigBytesField {
// Generate the field name for the writer struct
const name = try naming.structFieldName(allocator, field_name, names);
// Generate wire format constant names
const wirePostfixed = try std.mem.concat(allocator, u8, &[_][]const u8{ field_name, "Wire" });
defer allocator.free(wirePostfixed);
const wireConstName = try naming.constName(allocator, wirePostfixed, names);
const wireName = try std.mem.concat(allocator, u8, &[_][]const u8{
wire_prefix,
".",
wireConstName,
});
// Generate reader method name (get_fieldname)
const reader_prefixed = try std.mem.concat(allocator, u8, &[_][]const u8{ "get_", field_name });
defer allocator.free(reader_prefixed);
const readerMethodName = try naming.structMethodName(allocator, reader_prefixed, names);
// Process field options for default value
var custom_default: ?[]const u8 = null;
if (field_opts) |opts| {
for (opts.items) |*opt| {
if (std.mem.eql(u8, opt.name, "default")) {
custom_default = try formatStringLiteral(allocator, opt.value);
break;
}
}
}
return ZigBytesField{
.allocator = allocator,
.custom_default = custom_default,
.writer_field_name = name,
.reader_field_name = try std.mem.concat(allocator, u8, &[_][]const u8{ "_", name }),
.reader_method_name = readerMethodName,
.wire_const_full_name = wireName,
.wire_const_name = wireConstName,
.wire_index = field_index,
.writer_struct_name = writer_struct_name,
.reader_struct_name = reader_struct_name,
};
}
/// Clean up allocated memory
pub fn deinit(self: *ZigBytesField) void {
self.allocator.free(self.writer_field_name);
self.allocator.free(self.reader_field_name);
self.allocator.free(self.reader_method_name);
self.allocator.free(self.wire_const_full_name);
self.allocator.free(self.wire_const_name);
if (self.custom_default) |d| {
self.allocator.free(d);
}
}
/// Generate wire format constant declaration
pub fn createWireConst(self: *const ZigBytesField) ![]const u8 {
return std.fmt.allocPrint(self.allocator, "const {s}: gremlin.ProtoWireNumber = {d};", .{ self.wire_const_name, self.wire_index });
}
/// Generate writer struct field declaration
pub fn createWriterStructField(self: *const ZigBytesField) ![]const u8 {
return std.fmt.allocPrint(self.allocator, "{s}: ?[]const u8 = null,", .{self.writer_field_name});
}
/// Generate size calculation code for serialization
pub fn createSizeCheck(self: *const ZigBytesField) ![]const u8 {
if (self.custom_default) |d| {
// When default value exists, only include size if value differs from default
return std.fmt.allocPrint(self.allocator,
\\if (self.{s}) |v| {{
\\ if (!std.mem.eql(u8, v, {s})) {{
\\ res += gremlin.sizes.sizeWireNumber({s}) + gremlin.sizes.sizeUsize(v.len) + v.len;
\\ }}
\\}} else {{
\\ res += gremlin.sizes.sizeWireNumber({s}) + gremlin.sizes.sizeUsize(0);
\\}}
, .{ self.writer_field_name, d, self.wire_const_full_name, self.wire_const_full_name });
} else {
// Without default, include size if value exists
return std.fmt.allocPrint(self.allocator,
\\if (self.{s}) |v| {{
\\ if (v.len > 0) {{
\\ res += gremlin.sizes.sizeWireNumber({s}) + gremlin.sizes.sizeUsize(v.len) + v.len;
\\ }}
\\}}
, .{ self.writer_field_name, self.wire_const_full_name });
}
}
/// Generate serialization code
pub fn createWriter(self: *const ZigBytesField) ![]const u8 {
if (self.custom_default) |d| {
// With default value, only write if different from default
return std.fmt.allocPrint(self.allocator,
\\if (self.{s}) |v| {{
\\ if (!std.mem.eql(u8, v, {s})) {{
\\ target.appendBytes({s}, v);
\\ }}
\\}} else {{
\\ target.appendBytes({s}, "");
\\}}
, .{ self.writer_field_name, d, self.wire_const_full_name, self.wire_const_full_name });
} else {
// Without default, write if value exists
return std.fmt.allocPrint(self.allocator,
\\if (self.{s}) |v| {{
\\ if (v.len > 0) {{
\\ target.appendBytes({s}, v);
\\ }}
\\}}
, .{ self.writer_field_name, self.wire_const_full_name });
}
}
/// Generate reader struct field declaration
pub fn createReaderStructField(self: *const ZigBytesField) ![]const u8 {
if (self.custom_default) |d| {
return std.fmt.allocPrint(self.allocator, "{s}: ?[]const u8 = {s},", .{ self.reader_field_name, d });
} else {
return std.fmt.allocPrint(self.allocator, "{s}: ?[]const u8 = null,", .{self.reader_field_name});
}
}
/// Generate deserialization case statement
pub fn createReaderCase(self: *const ZigBytesField) ![]const u8 {
return std.fmt.allocPrint(self.allocator,
\\{s} => {{
\\ const result = try buf.readBytes(offset);
\\ offset += result.size;
\\ res.{s} = result.value;
\\}},
, .{ self.wire_const_full_name, self.reader_field_name });
}
/// Generate getter method for the field
pub fn createReaderMethod(self: *const ZigBytesField) ![]const u8 {
if (self.custom_default) |d| {
return std.fmt.allocPrint(self.allocator,
\\pub inline fn {s}(self: *const {s}) []const u8 {{
\\ return self.{s} orelse {s};
\\}}
, .{ self.reader_method_name, self.reader_struct_name, self.reader_field_name, d });
} else {
return std.fmt.allocPrint(self.allocator,
\\pub inline fn {s}(self: *const {s}) []const u8 {{
\\ return self.{s} orelse &[_]u8{{}};
\\}}
, .{ self.reader_method_name, self.reader_struct_name, self.reader_field_name });
}
}
};
test "basic bytes field" {
const fields = @import("../../../parser/main.zig").fields;
const ScopedName = @import("../../../parser/main.zig").ScopedName;
const ParserBuffer = @import("../../../parser/main.zig").ParserBuffer;
var scope = try ScopedName.init(std.testing.allocator, "");
defer scope.deinit();
var buf = ParserBuffer.init("bytes data_field = 1;");
var f = try fields.NormalField.parse(std.testing.allocator, scope, &buf);
defer f.deinit();
var names = try std.ArrayList([]const u8).initCapacity(std.testing.allocator, 32);
defer names.deinit(std.testing.allocator);
var zig_field = try ZigBytesField.init(
std.testing.allocator,
f.f_name,
f.options,
f.index,
"TestWire",
&names,
"TestWriter",
"TestReader",
);
defer zig_field.deinit();
// Test wire constant
const wire_const_code = try zig_field.createWireConst();
defer std.testing.allocator.free(wire_const_code);
try std.testing.expectEqualStrings("const DATA_FIELD_WIRE: gremlin.ProtoWireNumber = 1;", wire_const_code);
// Test writer field (optional)
const writer_field_code = try zig_field.createWriterStructField();
defer std.testing.allocator.free(writer_field_code);
try std.testing.expectEqualStrings("data_field: ?[]const u8 = null,", writer_field_code);
// Test size check
const size_check_code = try zig_field.createSizeCheck();
defer std.testing.allocator.free(size_check_code);
try std.testing.expectEqualStrings(
\\if (self.data_field) |v| {
\\ if (v.len > 0) {
\\ res += gremlin.sizes.sizeWireNumber(TestWire.DATA_FIELD_WIRE) + gremlin.sizes.sizeUsize(v.len) + v.len;
\\ }
\\}
, size_check_code);
// Test writer
const writer_code = try zig_field.createWriter();
defer std.testing.allocator.free(writer_code);
try std.testing.expectEqualStrings(
\\if (self.data_field) |v| {
\\ if (v.len > 0) {
\\ target.appendBytes(TestWire.DATA_FIELD_WIRE, v);
\\ }
\\}
, writer_code);
// Test reader field
const reader_field_code = try zig_field.createReaderStructField();
defer std.testing.allocator.free(reader_field_code);
try std.testing.expectEqualStrings("_data_field: ?[]const u8 = null,", reader_field_code);
// Test reader case
const reader_case_code = try zig_field.createReaderCase();
defer std.testing.allocator.free(reader_case_code);
try std.testing.expectEqualStrings(
\\TestWire.DATA_FIELD_WIRE => {
\\ const result = try buf.readBytes(offset);
\\ offset += result.size;
\\ res._data_field = result.value;
\\},
, reader_case_code);
// Test reader method
const reader_method_code = try zig_field.createReaderMethod();
defer std.testing.allocator.free(reader_method_code);
try std.testing.expectEqualStrings(
\\pub inline fn getDataField(self: *const TestReader) []const u8 {
\\ return self._data_field orelse &[_]u8{};
\\}
, reader_method_code);
}
test "bytes field with default" {
const fields = @import("../../../parser/main.zig").fields;
const ScopedName = @import("../../../parser/main.zig").ScopedName;
const ParserBuffer = @import("../../../parser/main.zig").ParserBuffer;
var scope = try ScopedName.init(std.testing.allocator, "");
defer scope.deinit();
// Use explicit string with quotes since it comes from parser this way
var buf = ParserBuffer.init("bytes data_field = 1 [default=\"hello\"];");
var f = try fields.NormalField.parse(std.testing.allocator, scope, &buf);
defer f.deinit();
var names = try std.ArrayList([]const u8).initCapacity(std.testing.allocator, 32);
defer names.deinit(std.testing.allocator);
var zig_field = try ZigBytesField.init(
std.testing.allocator,
f.f_name,
f.options,
f.index,
"TestWire",
&names,
"TestWriter",
"TestReader",
);
defer zig_field.deinit();
// Test wire constant
const wire_const_code = try zig_field.createWireConst();
defer std.testing.allocator.free(wire_const_code);
try std.testing.expectEqualStrings("const DATA_FIELD_WIRE: gremlin.ProtoWireNumber = 1;", wire_const_code);
// Test writer field (optional)
const writer_field_code = try zig_field.createWriterStructField();
defer std.testing.allocator.free(writer_field_code);
try std.testing.expectEqualStrings("data_field: ?[]const u8 = null,", writer_field_code);
// Test size check with default handling
const size_check_code = try zig_field.createSizeCheck();
defer std.testing.allocator.free(size_check_code);
try std.testing.expectEqualStrings(
\\if (self.data_field) |v| {
\\ if (!std.mem.eql(u8, v, "\x68\x65\x6C\x6C\x6F")) {
\\ res += gremlin.sizes.sizeWireNumber(TestWire.DATA_FIELD_WIRE) + gremlin.sizes.sizeUsize(v.len) + v.len;
\\ }
\\} else {
\\ res += gremlin.sizes.sizeWireNumber(TestWire.DATA_FIELD_WIRE) + gremlin.sizes.sizeUsize(0);
\\}
, size_check_code);
// Test writer with default handling
const writer_code = try zig_field.createWriter();
defer std.testing.allocator.free(writer_code);
try std.testing.expectEqualStrings(
\\if (self.data_field) |v| {
\\ if (!std.mem.eql(u8, v, "\x68\x65\x6C\x6C\x6F")) {
\\ target.appendBytes(TestWire.DATA_FIELD_WIRE, v);
\\ }
\\} else {
\\ target.appendBytes(TestWire.DATA_FIELD_WIRE, "");
\\}
, writer_code);
// Test reader field
const reader_field_code = try zig_field.createReaderStructField();
defer std.testing.allocator.free(reader_field_code);
try std.testing.expectEqualStrings("_data_field: ?[]const u8 = \"\\x68\\x65\\x6C\\x6C\\x6F\",", reader_field_code);
// Test reader case
const reader_case_code = try zig_field.createReaderCase();
defer std.testing.allocator.free(reader_case_code);
try std.testing.expectEqualStrings(
\\TestWire.DATA_FIELD_WIRE => {
\\ const result = try buf.readBytes(offset);
\\ offset += result.size;
\\ res._data_field = result.value;
\\},
, reader_case_code);
// Test reader method
const reader_method_code = try zig_field.createReaderMethod();
defer std.testing.allocator.free(reader_method_code);
try std.testing.expectEqualStrings(
\\pub inline fn getDataField(self: *const TestReader) []const u8 {
\\ return self._data_field orelse "\x68\x65\x6C\x6C\x6F";
\\}
, reader_method_code);
}