Skip to main content

tell_encoding/
log.rs

1use crate::LogEntryParams;
2use crate::UUID_LENGTH;
3use crate::helpers::*;
4
5/// Encode a single LogEntry FlatBuffer.
6///
7/// LogEntry table fields:
8/// - field 0: event_type `u8`
9/// - field 1: session_id `[ubyte]`
10/// - field 2: level `u8`
11/// - field 3: timestamp `u64`
12/// - field 4: source `string`
13/// - field 5: service `string`
14/// - field 6: payload `[ubyte]`
15pub fn encode_log_entry(params: &LogEntryParams<'_>) -> Vec<u8> {
16    let has_session_id = params.session_id.is_some();
17    let has_source = params.source.is_some();
18    let has_service = params.service.is_some();
19    let has_payload = params.payload.is_some();
20
21    // VTable: size(u16) + table_size(u16) + 7 field slots = 18 bytes
22    let vtable_size: u16 = 4 + 7 * 2;
23
24    // Fixed table layout (after soffset):
25    // +4: session_id offset (u32)
26    // +8: source offset (u32)
27    // +12: service offset (u32)
28    // +16: payload offset (u32)
29    // +20: timestamp (u64)
30    // +28: event_type (u8)
31    // +29: level (u8)
32    // +30-31: padding
33    let table_size: u16 = 4 + 28;
34
35    let session_id_size = if has_session_id { 4 + UUID_LENGTH } else { 0 };
36    let source_size = params.source.map(|s| 4 + s.len() + 1).unwrap_or(0);
37    let service_size = params.service.map(|s| 4 + s.len() + 1).unwrap_or(0);
38    let payload_size = params.payload.map(|p| 4 + p.len()).unwrap_or(0);
39
40    let estimated = 4
41        + vtable_size as usize
42        + table_size as usize
43        + session_id_size
44        + source_size
45        + service_size
46        + payload_size
47        + 16;
48    let mut buf = Vec::with_capacity(estimated);
49
50    // Root offset placeholder
51    buf.extend_from_slice(&[0u8; 4]);
52
53    // VTable
54    let vtable_start = buf.len();
55    write_u16(&mut buf, vtable_size);
56    write_u16(&mut buf, table_size);
57
58    // Field offsets
59    write_u16(&mut buf, 28); // field 0: event_type at +28
60    write_u16(&mut buf, if has_session_id { 4 } else { 0 }); // field 1: session_id
61    write_u16(&mut buf, 29); // field 2: level at +29
62    write_u16(&mut buf, 20); // field 3: timestamp at +20
63    write_u16(&mut buf, if has_source { 8 } else { 0 }); // field 4: source
64    write_u16(&mut buf, if has_service { 12 } else { 0 }); // field 5: service
65    write_u16(&mut buf, if has_payload { 16 } else { 0 }); // field 6: payload
66
67    // Align vtable to 4 bytes (18 bytes -> pad 2)
68    buf.extend_from_slice(&[0u8; 2]);
69
70    // Table
71    let table_start = buf.len();
72    let soffset = (table_start - vtable_start) as i32;
73    write_i32(&mut buf, soffset);
74
75    // Offset placeholders
76    let session_id_off_pos = buf.len();
77    write_u32(&mut buf, 0);
78
79    let source_off_pos = buf.len();
80    write_u32(&mut buf, 0);
81
82    let service_off_pos = buf.len();
83    write_u32(&mut buf, 0);
84
85    let payload_off_pos = buf.len();
86    write_u32(&mut buf, 0);
87
88    // timestamp (u64)
89    write_u64(&mut buf, params.timestamp);
90
91    // event_type (u8)
92    buf.push(params.event_type.as_u8());
93
94    // level (u8)
95    buf.push(params.level.as_u8());
96
97    // padding (2 bytes)
98    buf.extend_from_slice(&[0u8; 2]);
99
100    // Vectors and strings
101    align4(&mut buf);
102
103    // session_id
104    let session_id_start = params.session_id.map(|id| write_byte_vector(&mut buf, id));
105    align4(&mut buf);
106
107    // source
108    let source_start = params.source.map(|s| write_string(&mut buf, s));
109    align4(&mut buf);
110
111    // service
112    let service_start = params.service.map(|s| write_string(&mut buf, s));
113    align4(&mut buf);
114
115    // payload
116    let payload_start = params.payload.map(|data| write_byte_vector(&mut buf, data));
117
118    // Fill in offsets
119    buf[0..4].copy_from_slice(&(table_start as u32).to_le_bytes());
120
121    if let Some(start) = session_id_start {
122        patch_offset(&mut buf, session_id_off_pos, start);
123    }
124    if let Some(start) = source_start {
125        patch_offset(&mut buf, source_off_pos, start);
126    }
127    if let Some(start) = service_start {
128        patch_offset(&mut buf, service_off_pos, start);
129    }
130    if let Some(start) = payload_start {
131        patch_offset(&mut buf, payload_off_pos, start);
132    }
133
134    buf
135}
136
137/// Encode a LogData FlatBuffer containing a vector of pre-encoded log entries.
138///
139/// LogData table:
140/// - field 0: logs `[LogEntry]` (vector of tables)
141pub fn encode_log_data(encoded_logs: &[Vec<u8>]) -> Vec<u8> {
142    // VTable: size(u16) + table_size(u16) + 1 field = 6 bytes
143    let vtable_size: u16 = 4 + 2;
144    let table_size: u16 = 8; // soffset(4) + logs_offset(4)
145
146    let logs_total: usize = encoded_logs.iter().map(|l| l.len() + 4).sum();
147    let estimated = 4 + vtable_size as usize + table_size as usize + 4 + logs_total + 64;
148    let mut buf = Vec::with_capacity(estimated);
149
150    // Root offset placeholder
151    buf.extend_from_slice(&[0u8; 4]);
152
153    // VTable
154    let vtable_start = buf.len();
155    write_u16(&mut buf, vtable_size);
156    write_u16(&mut buf, table_size);
157    write_u16(&mut buf, 4); // field 0: logs at table+4
158
159    // Align vtable (6 -> pad 2)
160    buf.extend_from_slice(&[0u8; 2]);
161
162    // Table
163    let table_start = buf.len();
164    let soffset = (table_start - vtable_start) as i32;
165    write_i32(&mut buf, soffset);
166
167    let logs_off_pos = buf.len();
168    write_u32(&mut buf, 0);
169
170    align4(&mut buf);
171
172    // Logs vector
173    let logs_vec_start = buf.len();
174    let count = encoded_logs.len();
175
176    write_u32(&mut buf, count as u32);
177
178    let offsets_start = buf.len();
179    for _ in 0..count {
180        write_u32(&mut buf, 0);
181    }
182
183    align4(&mut buf);
184
185    let mut table_positions = Vec::with_capacity(count);
186    for log_bytes in encoded_logs {
187        align4(&mut buf);
188
189        let log_start = buf.len();
190        let root_offset = if log_bytes.len() >= 4 {
191            u32::from_le_bytes([log_bytes[0], log_bytes[1], log_bytes[2], log_bytes[3]]) as usize
192        } else {
193            0
194        };
195
196        table_positions.push(log_start + root_offset);
197        buf.extend_from_slice(log_bytes);
198    }
199
200    for (i, &table_pos) in table_positions.iter().enumerate() {
201        let offset_pos = offsets_start + i * 4;
202        patch_offset(&mut buf, offset_pos, table_pos);
203    }
204
205    patch_offset(&mut buf, logs_off_pos, logs_vec_start);
206    buf[0..4].copy_from_slice(&(table_start as u32).to_le_bytes());
207
208    buf
209}
210
211/// Encode multiple log entries directly into a caller-owned buffer as a LogData FlatBuffer.
212///
213/// Zero-copy: writes the header first with reserved offset slots, then encodes
214/// entries directly in their final position. No intermediate allocations or copies.
215/// The caller can reuse `buf` across flushes via `buf.clear()`.
216pub fn encode_log_data_into(
217    buf: &mut Vec<u8>,
218    logs: &[LogEntryParams<'_>],
219) -> std::ops::Range<usize> {
220    let data_start = buf.len();
221    let count = logs.len();
222
223    // Header: root(4) + vtable(6+2pad) + table(8) + vec_len(4) + slots(4*N)
224    let root_pos = buf.len();
225    buf.extend_from_slice(&[0u8; 4]);
226
227    let vtable_start = buf.len();
228    write_u16(buf, 6); // vtable_size
229    write_u16(buf, 8); // table_size
230    write_u16(buf, 4); // field 0: logs at table+4
231    buf.extend_from_slice(&[0u8; 2]); // align vtable
232
233    let table_start = buf.len();
234    write_i32(buf, (table_start - vtable_start) as i32);
235
236    let logs_off_pos = buf.len();
237    write_u32(buf, 0);
238
239    align4(buf);
240
241    let logs_vec_start = buf.len();
242    write_u32(buf, count as u32);
243
244    let offsets_start = buf.len();
245    for _ in 0..count {
246        write_u32(buf, 0);
247    }
248
249    align4(buf);
250
251    // Encode entries directly after header — each written once, in final position
252    let mut table_positions = Vec::with_capacity(count);
253    for params in logs {
254        align4(buf);
255        let entry_start = buf.len();
256        encode_log_entry_into(buf, params);
257        let root_offset = u32::from_le_bytes([
258            buf[entry_start],
259            buf[entry_start + 1],
260            buf[entry_start + 2],
261            buf[entry_start + 3],
262        ]) as usize;
263        table_positions.push(entry_start + root_offset);
264    }
265
266    // Patch vector offset slots → each entry's table position
267    for (i, &table_pos) in table_positions.iter().enumerate() {
268        patch_offset(buf, offsets_start + i * 4, table_pos);
269    }
270
271    patch_offset(buf, logs_off_pos, logs_vec_start);
272    buf[root_pos..root_pos + 4].copy_from_slice(&((table_start - data_start) as u32).to_le_bytes());
273
274    data_start..buf.len()
275}
276
277/// Encode a single log entry directly into an existing buffer.
278fn encode_log_entry_into(buf: &mut Vec<u8>, params: &LogEntryParams<'_>) {
279    let has_session_id = params.session_id.is_some();
280    let has_source = params.source.is_some();
281    let has_service = params.service.is_some();
282    let has_payload = params.payload.is_some();
283
284    let vtable_size: u16 = 4 + 7 * 2;
285    let table_size: u16 = 4 + 28;
286
287    let root_pos = buf.len();
288    buf.extend_from_slice(&[0u8; 4]);
289
290    let vtable_start = buf.len();
291    write_u16(buf, vtable_size);
292    write_u16(buf, table_size);
293
294    write_u16(buf, 28);
295    write_u16(buf, if has_session_id { 4 } else { 0 });
296    write_u16(buf, 29);
297    write_u16(buf, 20);
298    write_u16(buf, if has_source { 8 } else { 0 });
299    write_u16(buf, if has_service { 12 } else { 0 });
300    write_u16(buf, if has_payload { 16 } else { 0 });
301
302    buf.extend_from_slice(&[0u8; 2]); // align vtable
303
304    let table_start = buf.len();
305    let soffset = (table_start - vtable_start) as i32;
306    write_i32(buf, soffset);
307
308    let session_id_off_pos = buf.len();
309    write_u32(buf, 0);
310    let source_off_pos = buf.len();
311    write_u32(buf, 0);
312    let service_off_pos = buf.len();
313    write_u32(buf, 0);
314    let payload_off_pos = buf.len();
315    write_u32(buf, 0);
316
317    write_u64(buf, params.timestamp);
318    buf.push(params.event_type.as_u8());
319    buf.push(params.level.as_u8());
320    buf.extend_from_slice(&[0u8; 2]);
321
322    align4(buf);
323
324    let session_id_start = params.session_id.map(|id| write_byte_vector(buf, id));
325    align4(buf);
326    let source_start = params.source.map(|s| write_string(buf, s));
327    align4(buf);
328    let service_start = params.service.map(|s| write_string(buf, s));
329    align4(buf);
330    let payload_start = params.payload.map(|data| write_byte_vector(buf, data));
331
332    // Root offset relative to entry start (not absolute position)
333    buf[root_pos..root_pos + 4].copy_from_slice(&((table_start - root_pos) as u32).to_le_bytes());
334
335    if let Some(start) = session_id_start {
336        patch_offset(buf, session_id_off_pos, start);
337    }
338    if let Some(start) = source_start {
339        patch_offset(buf, source_off_pos, start);
340    }
341    if let Some(start) = service_start {
342        patch_offset(buf, service_off_pos, start);
343    }
344    if let Some(start) = payload_start {
345        patch_offset(buf, payload_off_pos, start);
346    }
347}