Skip to content

tls_codec: add variable-length integer type TlsVarInt#1656

Open
boxdot wants to merge 9 commits intoRustCrypto:masterfrom
boxdot:varint
Open

tls_codec: add variable-length integer type TlsVarInt#1656
boxdot wants to merge 9 commits intoRustCrypto:masterfrom
boxdot:varint

Conversation

@boxdot
Copy link
Contributor

@boxdot boxdot commented Feb 13, 2025

As defined in #rfc9000.

Also use this type (with an internal thin wrapper ContentLength) when
encoding/deconding the content length of vectors.

As defined in #[rfc9000].

Also use this type (with an internal thin wrapper `ContentLength`) when
encoding/deconding the content length of vectors.

[rfc9000]: https://www.rfc-editor.org/rfc/rfc9000#name-variable-length-integer-enc
@franziskuskiefer franziskuskiefer self-requested a review February 17, 2025 08:35
@franziskuskiefer
Copy link
Contributor

This totally fell of my radar. Sorry for that. I'll look at this soon.

Copy link
Contributor

@franziskuskiefer franziskuskiefer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you say a little more about what this PR is supposed to achieve? I struggle a little with understanding what it tries to do beyond refactoring. The variable length encoding is really only internal to the TLS encoding.

/// Returns the number of bytes required to encode this variable-length int.
pub(crate) const fn bytes_len(&self) -> usize {
let value = self.0;
if !cfg!(fuzzing) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check doesn't make sense anymore since it's not possible anymore to create value like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Removed.

let (len_len_byte, mut remainder) = u8::tls_deserialize_bytes(bytes)?;
impl ContentLength {
#[cfg(not(feature = "mls"))]
#[allow(dead_code)] // used in arbitrary
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it only dead code here, but not when mls is enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also put it behind the arbitrary feature which makes dead_code unnecessary.

/// Wraps an unsinged integer as variable-length int.
///
/// Returns `None` if the value is larger than [`Self::MAX`].
pub const fn new(value: u64) -> Option<Self> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should rather return a Result. Then you also don't need the try_new.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment on lines 56 to 61
if !cfg!(fuzzing) {
debug_assert!(len <= 8, "Invalid varint len {len}");
}
if len > 8 {
return Err(Error::LibraryError);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it's caught by the catch all _ in the match below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this check and changed the error variant in the catch all case to library error.

#[inline(always)]
fn read_variable_length_bytes(bytes: &[u8]) -> Result<((usize, usize), &[u8]), Error> {
// The length is encoded in the first two bits of the first byte.
/// Thin wrapper around [`TlsVarInt`] representing the length of encoded vector content in bytes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep comments within 80 characters as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reformatted comments here and below.

let mut out = Vec::with_capacity(content_length + len_len);
out.append(&mut length);
out.resize(len_len, 0);
length.0.write_bytes(&mut out)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a footgun. Why not just extend the out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a performance optimization to avoid constructing a temporary buffer which is expensive espcially in hot loops.

There is no buffer which contains the bytes of length.0: TlsVarInt. Before, the code was constructing a buffer with write_variable_length.

use crate::{Deserialize, Serialize};

impl Deserialize for ContentLength {
fn tls_deserialize<R: std::io::Read>(bytes: &mut R) -> Result<Self, Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this get inlined as well?

@boxdot
Copy link
Contributor Author

boxdot commented Feb 9, 2026

Can you say a little more about what this PR is supposed to achieve? I struggle a little with understanding what it tries to do beyond refactoring. The variable length encoding is really only internal to the TLS encoding.

The idea was to expose an additional type in the codec, to allow encoding/decoding of variable length integers. This is especially useful when prefixing small TLS encoded structs with such integers.

However, personally I stopped using this type, so if there no need for it for the TLS codec users, we can also close this PR. WDYT?

@franziskuskiefer
Copy link
Contributor

However, personally I stopped using this type, so if there no need for it for the TLS codec users, we can also close this PR. WDYT?

Thanks for clarifying.
I think there's some nice refactoring in here. I just wasn't sure about the purpose of making it public.
But encapsulating the variable length encoding looks much nicer.

@franziskuskiefer
Copy link
Contributor

@boxdot do you want to address the comments? I'd be happy to get this in then (with public or non-public varint, if it's something that's useful sometimes, there's no harm in exposing it).

let (len_len_byte, mut remainder) = u8::tls_deserialize_bytes(bytes)?;
impl ContentLength {
#[cfg(not(feature = "mls"))]
#[allow(dead_code)] // used in arbitrary
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also put it behind the arbitrary feature which makes dead_code unnecessary.

#[inline(always)]
fn read_variable_length_bytes(bytes: &[u8]) -> Result<((usize, usize), &[u8]), Error> {
// The length is encoded in the first two bits of the first byte.
/// Thin wrapper around [`TlsVarInt`] representing the length of encoded vector content in bytes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reformatted comments here and below.

let mut out = Vec::with_capacity(content_length + len_len);
out.append(&mut length);
out.resize(len_len, 0);
length.0.write_bytes(&mut out)?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a performance optimization to avoid constructing a temporary buffer which is expensive espcially in hot loops.

There is no buffer which contains the bytes of length.0: TlsVarInt. Before, the code was constructing a buffer with write_variable_length.

/// Wraps an unsinged integer as variable-length int.
///
/// Returns `None` if the value is larger than [`Self::MAX`].
pub const fn new(value: u64) -> Option<Self> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

/// Returns the number of bytes required to encode this variable-length int.
pub(crate) const fn bytes_len(&self) -> usize {
let value = self.0;
if !cfg!(fuzzing) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Removed.

Comment on lines 56 to 61
if !cfg!(fuzzing) {
debug_assert!(len <= 8, "Invalid varint len {len}");
}
if len > 8 {
return Err(Error::LibraryError);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this check and changed the error variant in the catch all case to library error.

}
}

#[inline(always)]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this inline, because it is a private function and it will be inlined anyways.

@boxdot
Copy link
Contributor Author

boxdot commented Feb 17, 2026

@boxdot do you want to address the comments? I'd be happy to get this in then (with public or non-public varint, if it's something that's useful sometimes, there's no harm in exposing it).

I addressed review comments. We can do another review round.

@franziskuskiefer franziskuskiefer self-requested a review February 17, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants