use bitfold_protocol::{
command::CommandPacket,
command_codec::{self, CommandEncoder},
};
use super::Peer;
impl Peer {
pub fn encode_queued_commands(&mut self) -> std::io::Result<Vec<u8>> {
let mut packet = CommandPacket::new();
for command in self.drain_commands() {
packet.add_command(command);
}
let mut scratch = self.tx_pool.allocate();
scratch.clear();
CommandEncoder::encode_packet_into(&mut scratch, &packet)?;
let compression_buffer = self.compression_pool.acquire();
let mut final_data = command_codec::compress_with_buffer(
&scratch,
self.config.compression,
self.config.compression_threshold,
compression_buffer,
)?;
self.tx_pool.deallocate(scratch);
self.record_packet_sent();
if self.config.use_checksums {
command_codec::append_checksum_in_place(&mut final_data);
}
self.record_data_sent(final_data.len());
Ok(final_data)
}
pub fn encode_queued_commands_bounded(
&mut self,
max_size: usize,
) -> std::io::Result<Option<Vec<u8>>> {
if !self.has_queued_commands() {
return Ok(None);
}
let first_is_pmtu_probe = matches!(
self.command_queue.iter().next(),
Some(bitfold_protocol::command::ProtocolCommand::PMTUProbe { .. })
);
let max_size =
if first_is_pmtu_probe { self.config.receive_buffer_max_size } else { max_size };
let compression_overhead = match self.config.compression {
bitfold_core::config::CompressionAlgorithm::Lz4 => 5, _ => 1, };
let checksum_overhead = if self.config.use_checksums { 4 } else { 0 };
let static_overhead = 1 + compression_overhead + checksum_overhead;
let mut selected_count = 0usize;
let mut aggregated_len = 0;
let mut per_command_sizes: Vec<usize> = Vec::new();
for cmd in self.command_queue.iter() {
let encoded = bitfold_protocol::command_codec::CommandEncoder::encode_command(cmd)?;
let cmd_total = 2 + encoded.len();
if static_overhead + aggregated_len + cmd_total > max_size {
break;
}
aggregated_len += cmd_total;
per_command_sizes.push(cmd_total);
selected_count += 1;
}
if selected_count == 0 {
if let Some(first_cmd) = self.command_queue.iter().next() {
if !first_is_pmtu_probe {
let encoded =
bitfold_protocol::command_codec::CommandEncoder::encode_command(first_cmd)?;
let cmd_size = 2 + encoded.len();
let total_with_overhead = static_overhead + cmd_size;
tracing::warn!(
"Command too large for MTU: command type {:?}, encoded size {} bytes, total with overhead {} bytes, max allowed {} bytes. Command will remain queued.",
first_cmd.command_type(),
cmd_size,
total_with_overhead,
max_size
);
}
}
return Ok(None);
}
let drained: Vec<_> = self.drain_commands().collect();
let mut packet = CommandPacket::new();
for cmd in drained.iter().take(selected_count) {
packet.add_command(cmd.clone());
}
for cmd in drained.into_iter().skip(selected_count) {
self.enqueue_command(cmd);
}
let mut scratch = self.tx_pool.allocate();
scratch.clear();
bitfold_protocol::command_codec::CommandEncoder::encode_packet_into(&mut scratch, &packet)?;
let compression_buffer = self.compression_pool.acquire();
let mut final_data = command_codec::compress_with_buffer(
&scratch,
self.config.compression,
self.config.compression_threshold,
compression_buffer,
)?;
self.tx_pool.deallocate(scratch);
self.record_packet_sent();
if self.config.use_checksums {
command_codec::append_checksum_in_place(&mut final_data);
}
self.record_data_sent(final_data.len());
if final_data.len() > max_size {
tracing::error!(
"Encoded packet exceeded max_size after compression: {} bytes > {} bytes max. Selected {} commands, pre-compression size was {} bytes (command bytes) + {} bytes (overhead). Commands will remain queued.",
final_data.len(),
max_size,
selected_count,
aggregated_len,
static_overhead
);
return Ok(None);
}
Ok(Some(final_data))
}
}
#[cfg(test)]
mod tests {
use std::time::Instant;
use bitfold_core::config::{CompressionAlgorithm, Config};
use bitfold_protocol::command::ProtocolCommand;
use super::*;
fn create_virtual_connection() -> Peer {
Peer::new(get_fake_addr(), &Config::default(), Instant::now())
}
fn get_fake_addr() -> std::net::SocketAddr {
"127.0.0.1:0".parse().unwrap()
}
#[test]
fn test_encode_decode_command_packet() {
let mut peer = create_virtual_connection();
peer.enqueue_command(ProtocolCommand::Ping { timestamp: 100 });
peer.enqueue_command(ProtocolCommand::SendUnreliable {
channel_id: 0,
data: vec![1, 2, 3].into(),
});
peer.enqueue_ack_command(Some(5000));
assert_eq!(peer.queued_commands_count(), 3);
let bytes = peer.encode_queued_commands().unwrap();
assert!(!bytes.is_empty());
assert!(!peer.has_queued_commands());
let mut peer2 = create_virtual_connection();
let result = peer2.process_command_packet(&bytes, Instant::now()).unwrap();
let packets: Vec<_> = result.into_iter().collect();
assert_eq!(packets.len(), 1);
assert!(peer2.has_queued_commands());
}
#[test]
fn test_checksum_enabled_end_to_end() {
let mut config = Config::default();
config.use_checksums = true;
let mut peer = Peer::new(get_fake_addr(), &config, Instant::now());
let time = Instant::now();
peer.enqueue_command(ProtocolCommand::Ping { timestamp: 1234 });
let encoded = peer.encode_queued_commands().unwrap();
assert!(encoded.len() > 10);
let result = peer.process_command_packet(&encoded, time);
assert!(result.is_ok()); }
#[test]
fn test_checksum_detects_corruption_in_peer() {
let mut config = Config::default();
config.use_checksums = true;
let mut peer = Peer::new(get_fake_addr(), &config, Instant::now());
let time = Instant::now();
peer.enqueue_command(ProtocolCommand::SendUnreliable {
channel_id: 0,
data: vec![1, 2, 3, 4, 5].into(),
});
let mut encoded = peer.encode_queued_commands().unwrap();
if encoded.len() > 5 {
encoded[0] = 99;
}
let result = peer.process_command_packet(&encoded, time);
assert!(result.is_err());
}
#[test]
fn test_checksum_disabled_backward_compatibility() {
let mut config = Config::default();
config.use_checksums = false;
let mut peer1 = Peer::new(get_fake_addr(), &config, Instant::now());
let mut peer2 = Peer::new(get_fake_addr(), &config, Instant::now());
let time = Instant::now();
peer1.enqueue_command(ProtocolCommand::Ping { timestamp: 5678 });
let encoded = peer1.encode_queued_commands().unwrap();
let result = peer2.process_command_packet(&encoded, time);
assert!(result.is_ok()); }
#[test]
fn test_compression_zlib_end_to_end() {
let mut config = Config::default();
config.compression = CompressionAlgorithm::Zlib;
config.compression_threshold = 10;
let mut peer1 = Peer::new(get_fake_addr(), &config, Instant::now());
let mut peer2 = Peer::new(get_fake_addr(), &config, Instant::now());
let time = Instant::now();
peer1.enqueue_command(ProtocolCommand::SendUnreliable {
channel_id: 0,
data: vec![42; 300].into(), });
let encoded = peer1.encode_queued_commands().unwrap();
assert!(encoded.len() < 100);
let result = peer2.process_command_packet(&encoded, time);
assert!(result.is_ok());
}
#[test]
fn test_compression_lz4_end_to_end() {
let mut config = Config::default();
config.compression = CompressionAlgorithm::Lz4;
config.compression_threshold = 10;
let mut peer1 = Peer::new(get_fake_addr(), &config, Instant::now());
let mut peer2 = Peer::new(get_fake_addr(), &config, Instant::now());
let time = Instant::now();
peer1.enqueue_command(ProtocolCommand::SendUnreliable {
channel_id: 0,
data: vec![99; 300].into(), });
let encoded = peer1.encode_queued_commands().unwrap();
assert!(encoded.len() < 100);
let result = peer2.process_command_packet(&encoded, time);
assert!(result.is_ok());
}
#[test]
fn test_compression_disabled_by_default() {
let config = Config::default();
let mut peer1 = Peer::new(get_fake_addr(), &config, Instant::now());
let mut peer2 = Peer::new(get_fake_addr(), &config, Instant::now());
let time = Instant::now();
peer1.enqueue_command(ProtocolCommand::Ping { timestamp: 1234 });
let encoded = peer1.encode_queued_commands().unwrap();
assert_eq!(encoded[0], 0);
let result = peer2.process_command_packet(&encoded, time);
assert!(result.is_ok());
}
#[test]
fn test_statistics_with_compression() {
let mut config = Config::default();
config.compression = CompressionAlgorithm::Lz4;
config.compression_threshold = 10;
let mut peer = Peer::new(get_fake_addr(), &config, Instant::now());
peer.enqueue_command(ProtocolCommand::SendReliable {
channel_id: 0,
sequence: 0,
ordered: true,
data: vec![1; 100].into(), });
let encoded = peer.encode_queued_commands().unwrap();
assert_eq!(peer.statistics().bytes_sent, encoded.len() as u64);
assert!(peer.statistics().bytes_sent < 100); }
#[test]
fn test_statistics_with_checksums() {
let mut config = Config::default();
config.use_checksums = true;
let mut peer = Peer::new(get_fake_addr(), &config, Instant::now());
peer.enqueue_command(ProtocolCommand::SendReliable {
channel_id: 0,
sequence: 0,
ordered: true,
data: vec![1, 2, 3].into(),
});
let encoded = peer.encode_queued_commands().unwrap();
assert_eq!(peer.statistics().bytes_sent, encoded.len() as u64);
assert!(encoded.len() >= 4); }
#[test]
fn test_mtu_boundary_calculation() {
let mut config = Config::default();
config.use_checksums = false;
config.compression = bitfold_core::config::CompressionAlgorithm::None;
let mut peer = Peer::new(get_fake_addr(), &config, Instant::now());
let max_payload_for_mtu = 1429;
peer.enqueue_command(ProtocolCommand::SendReliable {
channel_id: 0,
sequence: 0,
ordered: true,
data: vec![42u8; max_payload_for_mtu].into(),
});
let result = peer.encode_queued_commands_bounded(1440);
assert!(result.is_ok());
let encoded = result.unwrap();
assert!(encoded.is_some(), "Command should fit within MTU");
let bytes = encoded.unwrap();
assert_eq!(bytes.len(), 1440, "Encoded size should be exactly 1440 bytes");
assert!(!peer.has_queued_commands());
}
#[test]
fn test_mtu_boundary_too_large() {
let mut config = Config::default();
config.use_checksums = false;
config.compression = bitfold_core::config::CompressionAlgorithm::None;
let mut peer = Peer::new(get_fake_addr(), &config, Instant::now());
let too_large_payload = 1430;
peer.enqueue_command(ProtocolCommand::SendReliable {
channel_id: 0,
sequence: 0,
ordered: true,
data: vec![42u8; too_large_payload].into(),
});
let result = peer.encode_queued_commands_bounded(1440);
assert!(result.is_ok());
let encoded = result.unwrap();
assert!(encoded.is_none(), "Command should NOT fit within MTU");
assert!(peer.has_queued_commands());
}
}