Skip to content

Commit 491a5b1

Browse files
ariel-miculasdwrensha
authored andcommitted
Add NoAllocBufferSegments, suitable for no_alloc environments
It is similar to BufferSegments, but it avoids any allocations.
1 parent 1433eb7 commit 491a5b1

File tree

2 files changed

+168
-1
lines changed

2 files changed

+168
-1
lines changed

capnp/src/serialize.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
//! where each message is preceded by a segment table indicating the size of its segments.
2525
2626
mod no_alloc_slice_segments;
27-
pub use no_alloc_slice_segments::NoAllocSliceSegments;
27+
pub use no_alloc_slice_segments::{NoAllocBufferSegments, NoAllocSliceSegments};
2828

2929
#[cfg(feature = "alloc")]
3030
use crate::io::{Read, Write};

capnp/src/serialize/no_alloc_slice_segments.rs

+167
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::message::ReaderOptions;
44
use crate::message::ReaderSegments;
55
use crate::private::units::BYTES_PER_WORD;
66
use crate::{Error, ErrorKind, Result};
7+
use core::ops::Deref;
78

89
use super::SEGMENTS_COUNT_LIMIT;
910

@@ -168,6 +169,172 @@ impl<'b> ReaderSegments for NoAllocSliceSegments<'b> {
168169
}
169170
}
170171

172+
enum NoAllocBufferSegmentType {
173+
SingleSegment(usize, usize),
174+
MultipleSegments,
175+
}
176+
177+
/// Segments read from a buffer, useful for when you have the message in a buffer and don't want the
178+
/// extra copy of `read_message`.
179+
///
180+
/// `NoAllocBufferSegments` is similar to [`crate::serialize::BufferSegments`] but optimized for
181+
/// low memory embedded environment. It does not do heap allocations.
182+
///
183+
/// # Performance considerations
184+
///
185+
/// Due to lack of heap allocations, `NoAllocBufferSegments` does not cache segments offset and
186+
/// length and has to parse message header every time `NoAllocBufferSegments::get_segment` is called.
187+
/// The parsing has O(N) complexity where N is total number of segments in the message.
188+
/// `NoAllocBufferSegments` has optimization for single segment messages: if message has only one
189+
/// segment, it will be parsed only once during creation and no parsing will be required on `get_segment` calls
190+
pub struct NoAllocBufferSegments<T> {
191+
buffer: T,
192+
segment_type: NoAllocBufferSegmentType,
193+
}
194+
195+
impl<T: Deref<Target = [u8]>> NoAllocBufferSegments<T> {
196+
/// Reads a serialized message (including a segment table) from a buffer and takes ownership, without copying.
197+
/// The buffer is allowed to extend beyond the end of the message.
198+
///
199+
/// ALIGNMENT: If the "unaligned" feature is enabled, then there are no alignment requirements on `buffer`.
200+
/// Otherwise, `buffer` must be 8-byte aligned (attempts to read the message will trigger errors).
201+
pub fn try_new(buffer: T, options: ReaderOptions) -> Result<Self> {
202+
let mut remaining = &*buffer;
203+
204+
verify_alignment(remaining.as_ptr())?;
205+
206+
let segments_count = u32_to_segments_count(read_u32_le(&mut remaining)?)?;
207+
208+
if segments_count >= SEGMENTS_COUNT_LIMIT {
209+
return Err(Error::from_kind(ErrorKind::InvalidNumberOfSegments(
210+
segments_count,
211+
)));
212+
}
213+
214+
let mut total_segments_length_bytes = 0_usize;
215+
216+
for _ in 0..segments_count {
217+
let segment_length_in_bytes =
218+
u32_to_segment_length_bytes(read_u32_le(&mut remaining)?)?;
219+
220+
total_segments_length_bytes = total_segments_length_bytes
221+
.checked_add(segment_length_in_bytes)
222+
.ok_or_else(|| Error::from_kind(ErrorKind::MessageSizeOverflow))?;
223+
}
224+
225+
// Don't accept a message which the receiver couldn't possibly traverse without hitting the
226+
// traversal limit. Without this check, a malicious client could transmit a very large segment
227+
// size to make the receiver allocate excessive space and possibly crash.
228+
if let Some(limit) = options.traversal_limit_in_words {
229+
let total_segments_length_words = total_segments_length_bytes / 8;
230+
if total_segments_length_words > limit {
231+
return Err(Error::from_kind(ErrorKind::MessageTooLarge(
232+
total_segments_length_words,
233+
)));
234+
}
235+
}
236+
237+
// If number of segments is even, header length will not be aligned by 8, we need to consume
238+
// padding from the remainder of the message
239+
if segments_count % 2 == 0 {
240+
let _padding = read_u32_le(&mut remaining)?;
241+
}
242+
243+
let expected_data_offset = calculate_data_offset(segments_count)
244+
.ok_or_else(|| Error::from_kind(ErrorKind::MessageSizeOverflow))?;
245+
246+
let consumed_bytes = buffer.len() - remaining.len();
247+
248+
assert_eq!(
249+
expected_data_offset, consumed_bytes,
250+
"Expected header size and actual header size must match, otherwise we have a bug in this code"
251+
);
252+
253+
// If data section of the message is smaller than calculated total segments length, the message
254+
// is malformed. It looks like it's ok to have extra bytes in the end, according to
255+
// of `SliceSegments` implementation.
256+
if remaining.len() < total_segments_length_bytes {
257+
return Err(Error::from_kind(ErrorKind::MessageEndsPrematurely(
258+
total_segments_length_bytes / BYTES_PER_WORD,
259+
remaining.len() / BYTES_PER_WORD,
260+
)));
261+
}
262+
263+
let message_length = expected_data_offset + total_segments_length_bytes;
264+
265+
if segments_count == 1 {
266+
Ok(Self {
267+
buffer,
268+
segment_type: NoAllocBufferSegmentType::SingleSegment(
269+
expected_data_offset,
270+
message_length,
271+
),
272+
})
273+
} else {
274+
Ok(Self {
275+
buffer,
276+
segment_type: NoAllocBufferSegmentType::MultipleSegments,
277+
})
278+
}
279+
}
280+
}
281+
282+
impl<T: Deref<Target = [u8]>> ReaderSegments for NoAllocBufferSegments<T> {
283+
fn get_segment(&self, idx: u32) -> Option<&[u8]> {
284+
// panic safety: we are doing a lot of `unwrap` here. We assume that underlying message slice
285+
// holds valid capnp message - we already verified slice in NoAllocBufferSegments::try_new,
286+
// so these unwraps are not expected to panic unless we have bug in the code.
287+
288+
let idx: usize = idx.try_into().unwrap();
289+
290+
match self.segment_type {
291+
NoAllocBufferSegmentType::SingleSegment(start, end) => {
292+
if idx == 0 {
293+
Some(&self.buffer[start..end])
294+
} else {
295+
None
296+
}
297+
}
298+
NoAllocBufferSegmentType::MultipleSegments => {
299+
let mut buf = &*self.buffer;
300+
301+
let segments_count = u32_to_segments_count(read_u32_le(&mut buf).unwrap()).unwrap();
302+
303+
if idx >= segments_count {
304+
return None;
305+
}
306+
307+
let mut segment_offset = calculate_data_offset(segments_count).unwrap();
308+
309+
for _ in 0..idx {
310+
segment_offset = segment_offset
311+
.checked_add(
312+
u32_to_segment_length_bytes(read_u32_le(&mut buf).unwrap()).unwrap(),
313+
)
314+
.unwrap();
315+
}
316+
317+
let segment_length =
318+
u32_to_segment_length_bytes(read_u32_le(&mut buf).unwrap()).unwrap();
319+
320+
Some(&self.buffer[segment_offset..(segment_offset + segment_length)])
321+
}
322+
}
323+
}
324+
325+
fn len(&self) -> usize {
326+
// panic safety: we are doing a lot of `unwrap` here. We assume that underlying message slice
327+
// holds valid capnp message - we already verified slice in NoAllocBufferSegments::try_new
328+
329+
match self.segment_type {
330+
NoAllocBufferSegmentType::SingleSegment { .. } => 1,
331+
NoAllocBufferSegmentType::MultipleSegments => {
332+
u32_to_segments_count(read_u32_le(&mut &*self.buffer).unwrap()).unwrap()
333+
}
334+
}
335+
}
336+
}
337+
171338
/// Verifies whether pointer meets alignment requirements
172339
///
173340
/// If crate is compiled with "unaligned" feature, then this function does nothing since

0 commit comments

Comments
 (0)