Skip to main content

tell_encoding/
log.rs

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