krabka_records_legacy/set/
encode.rs1use {
12 super::ParsedRecord,
13 crate::{
14 error::LegacyRecordsError,
15 message::{Magic, Message, attrs_with_compression},
16 },
17 bytes::{BufMut, BytesMut},
18 krabka_compression::CompressionType,
19};
20
21fn message_for_record(record: ParsedRecord, magic: Magic) -> Message {
22 Message {
23 magic,
24 attributes: 0,
25 timestamp: match magic {
26 Magic::V0 => None,
27 Magic::V1 => Some(record.timestamp.unwrap_or(-1)),
28 },
29 key: record.key,
30 value: record.value,
31 }
32}
33
34pub fn encode_flat_message_set<B: BufMut, I: IntoIterator<Item = ParsedRecord>>(
39 records: I,
40 magic: Magic,
41 buf: &mut B,
42) {
43 for r in records {
44 let offset = r.offset;
45 let msg = message_for_record(r, magic);
46 let msg_len = msg.encoded_len();
47 buf.put_i64(offset.0);
48 buf.put_i32(i32::try_from(msg_len).unwrap_or(i32::MAX));
51 msg.encode_into(buf);
52 }
53}
54
55pub fn encode_compressed_message_set<B: BufMut>(
65 records: &[ParsedRecord],
66 magic: Magic,
67 codec: CompressionType,
68 buf: &mut B,
69) -> Result<(), LegacyRecordsError> {
70 assert2::assert!(
71 codec != CompressionType::None,
72 "use encode_flat_message_set for uncompressed"
73 );
74 if matches!(codec, CompressionType::Zstd) {
75 return Err(LegacyRecordsError::Malformed(
76 "zstd compression not representable in v0/v1".into(),
77 ));
78 }
79 let (Some(first), Some(last)) = (records.first(), records.last()) else {
80 return Ok(());
83 };
84
85 let mut inner = BytesMut::new();
87 let base_offset = first.offset.0;
88 for r in records {
89 let inner_offset = match magic {
90 Magic::V0 => r.offset.0,
91 Magic::V1 => r.offset.0.checked_sub(base_offset).ok_or_else(|| {
93 LegacyRecordsError::Malformed("relative inner offset exceeds i64".into())
94 })?,
95 };
96 let msg = message_for_record(r.clone(), magic);
97 let msg_len = msg.encoded_len();
98 inner.put_i64(inner_offset);
99 inner.put_i32(i32::try_from(msg_len).unwrap_or(i32::MAX));
100 msg.encode_into(&mut inner);
101 }
102
103 let compressed = krabka_compression::compress(codec, &inner)?;
105
106 let wrapper_attributes = attrs_with_compression(0, codec);
108 let wrapper_timestamp = match magic {
109 Magic::V0 => None,
110 Magic::V1 => Some(
111 records
112 .iter()
113 .filter_map(|r| r.timestamp)
114 .max()
115 .unwrap_or(-1),
116 ),
117 };
118 let wrapper = Message {
119 magic,
120 attributes: wrapper_attributes,
121 timestamp: wrapper_timestamp,
122 key: None,
123 value: Some(compressed),
124 };
125 let wrapper_len = wrapper.encoded_len();
126
127 let wrapper_offset = match magic {
130 Magic::V0 => 0,
131 Magic::V1 => last.offset.0,
132 };
133 buf.put_i64(wrapper_offset);
134 buf.put_i32(i32::try_from(wrapper_len).unwrap_or(i32::MAX));
135 wrapper.encode_into(buf);
136 Ok(())
137}
138
139#[cfg(test)]
140mod tests {
141 use {
142 super::*,
143 crate::set::{
144 decode_message_set,
145 test_support::{compressed_bytes, decode_bytes, sample_records_v0, sample_records_v1},
146 },
147 bytes::{Buf, Bytes},
148 krabka_ids::Offset,
149 };
150
151 #[test]
152 fn compressed_v1_writes_relative_sparse_offsets() {
153 let mut records = sample_records_v1();
154 records[0].offset = Offset(100);
155 records[1].offset = Offset(102);
156 records[2].offset = Offset(105);
157 let wire = compressed_bytes(&records, Magic::V1, CompressionType::Gzip);
158 let mut cur = &wire[..];
159 assert2::assert!(cur.get_i64() == 105);
160 let size = usize::try_from(cur.get_i32()).unwrap();
161 let wrapper = Message::decode_from(&mut cur, size).unwrap();
162 let inner = krabka_compression::decompress(
163 CompressionType::Gzip,
164 &wrapper.value.unwrap(),
165 krabka_units::mebibytes(1),
166 )
167 .unwrap();
168 let decoded = decode_bytes(&inner);
169 let mut expected = records.clone();
170 for (record, offset) in expected.iter_mut().zip([0, 2, 5]) {
171 record.offset = Offset(offset);
172 }
173 assert2::assert!(decoded == expected);
174 assert2::assert!(decode_bytes(&wire) == records);
175 }
176
177 #[test]
178 fn compressed_v1_rejects_unrepresentable_relative_span() {
179 let mut records = sample_records_v1();
180 records[0].offset = Offset(i64::MIN);
181 records[1].offset = Offset(0);
182 records[2].offset = Offset(i64::MAX);
183 let mut wire = BytesMut::new();
184 assert2::assert!(matches!(
185 encode_compressed_message_set(&records, Magic::V1, CompressionType::Gzip, &mut wire),
186 Err(LegacyRecordsError::Malformed(_))
187 ));
188 assert2::assert!(wire.is_empty());
189 }
190
191 #[yare::parameterized(
192 flat_v0 = { Magic::V0, None },
193 flat_v1 = { Magic::V1, None },
194 gzip_v0 = { Magic::V0, Some(CompressionType::Gzip) },
195 gzip_v1 = { Magic::V1, Some(CompressionType::Gzip) },
196 snappy_v1 = { Magic::V1, Some(CompressionType::Snappy) },
197 )]
198 fn message_set_roundtrips(magic: Magic, codec: Option<CompressionType>) {
199 let records = match magic {
200 Magic::V0 => sample_records_v0(),
201 Magic::V1 => sample_records_v1(),
202 };
203 let mut buffer = BytesMut::new();
204 if let Some(codec) = codec {
205 encode_compressed_message_set(&records, magic, codec, &mut buffer).unwrap();
206 } else {
207 encode_flat_message_set(records.clone(), magic, &mut buffer);
208 }
209
210 let decoded = decode_message_set(&mut &buffer[..], buffer.len()).unwrap();
211 assert2::assert!(decoded == records);
212 }
213
214 #[test]
215 fn flat_v1_missing_timestamp_encodes_minus_one() {
216 let recs = vec![ParsedRecord {
217 offset: Offset(7),
218 timestamp: None,
219 key: None,
220 value: Some(Bytes::from_static(b"v")),
221 }];
222 let mut buf = BytesMut::new();
223 encode_flat_message_set(recs, Magic::V1, &mut buf);
224 let decoded = decode_bytes(&buf);
225 assert2::assert!(
226 decoded
227 == vec![ParsedRecord {
228 offset: Offset(7),
229 timestamp: Some(-1),
230 key: None,
231 value: Some(Bytes::from_static(b"v")),
232 }]
233 );
234 }
235
236 #[test]
237 fn compressed_v1_missing_timestamps_default_to_minus_one() {
238 let recs = vec![ParsedRecord {
241 offset: Offset(9),
242 timestamp: None,
243 key: None,
244 value: Some(Bytes::from_static(b"v")),
245 }];
246 let buf = compressed_bytes(&recs, Magic::V1, CompressionType::Gzip);
247
248 let mut cur: &[u8] = &buf[..];
250 let _wrapper_offset = cur.get_i64();
251 let wrapper_size = usize::try_from(cur.get_i32()).unwrap();
252 let wrapper = Message::decode_from(&mut cur, wrapper_size).unwrap();
253 let decoded = decode_bytes(&buf);
255 assert2::assert!(wrapper.timestamp == Some(-1));
256 assert2::assert!(decoded[0].timestamp == Some(-1));
257 }
258}