bitwarden_ipc/fragmentation/
split.rs1use rand::RngExt;
2use thiserror::Error;
3
4use super::{
5 FRAGMENT_TOPIC, FragmentFrame, FragmentHeader, MAX_HEADER_LEN, MAX_PAYLOAD_SIZE, Reassembler,
6};
7use crate::message::OutgoingMessage;
8
9#[derive(Debug, Error)]
11pub(crate) enum FragmentationError {
12 #[error("Payload of {0} bytes exceeds the maximum of {MAX_PAYLOAD_SIZE} bytes")]
13 PayloadTooLarge(usize),
14 #[error("Crypto provider message limit of {0} bytes leaves no room for fragment data")]
15 ProviderLimitTooSmall(usize),
16 #[error("Topic {0:?} is reserved for fragments")]
17 ReservedTopic(String),
18}
19
20pub(crate) enum Fragments {
22 Single(Option<OutgoingMessage>),
23 Many {
24 message: OutgoingMessage,
25 message_id: u64,
26 topic: String,
27 fragment_len: usize,
28 next_index: u32,
29 },
30}
31
32impl Fragments {
33 pub(crate) fn split(
36 message: OutgoingMessage,
37 max_payload_len: usize,
38 ) -> Result<Self, FragmentationError> {
39 if let Some(topic) = &message.topic
41 && Reassembler::original_topic(Some(topic)).is_some()
42 {
43 return Err(FragmentationError::ReservedTopic(topic.clone()));
44 }
45
46 let total_len = message.payload.len();
47 if total_len > MAX_PAYLOAD_SIZE {
48 return Err(FragmentationError::PayloadTooLarge(total_len));
49 }
50 if total_len <= max_payload_len {
51 return Ok(Fragments::Single(Some(message)));
52 }
53 let fragment_len = max_payload_len.saturating_sub(MAX_HEADER_LEN);
54 if fragment_len == 0 {
55 return Err(FragmentationError::ProviderLimitTooSmall(max_payload_len));
56 }
57
58 Ok(Fragments::Many {
59 message_id: bitwarden_random::rng().random(),
60 topic: Self::fragment_topic(message.topic.as_deref()),
61 message,
62 fragment_len,
63 next_index: 0,
64 })
65 }
66
67 pub(super) fn fragment_topic(topic: Option<&str>) -> String {
69 match topic {
70 Some(topic) => format!("{FRAGMENT_TOPIC}/{topic}"),
71 None => FRAGMENT_TOPIC.to_owned(),
72 }
73 }
74}
75
76impl Iterator for Fragments {
77 type Item = OutgoingMessage;
78
79 fn next(&mut self) -> Option<OutgoingMessage> {
80 match self {
81 Fragments::Single(message) => message.take(),
82 Fragments::Many {
83 message,
84 message_id,
85 topic,
86 fragment_len,
87 next_index,
88 } => {
89 let total_len = message.payload.len();
90 let start = *next_index as usize * *fragment_len;
91 if start >= total_len {
92 return None;
93 }
94 let end = total_len.min(start + *fragment_len);
95 let frame = FragmentFrame {
97 header: FragmentHeader {
98 message_id: *message_id,
99 index: *next_index,
100 total_len: total_len as u32,
101 },
102 data: &message.payload[start..end],
103 };
104 *next_index += 1;
105 Some(OutgoingMessage {
106 payload: frame.encode(),
107 destination: message.destination.clone(),
108 topic: Some(topic.clone()),
109 })
110 }
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::{super::test_support::*, *};
118
119 #[test]
120 fn payload_within_provider_limit_is_sent_as_one_unchanged_message() {
121 let small = outgoing(large_payload(MAX_LEN, 0), "bulk");
122 assert_eq!(split_all(small.clone(), MAX_LEN), vec![small]);
123
124 let fragments = split_all(outgoing(large_payload(MAX_LEN + 1, 0), "bulk"), MAX_LEN);
125 assert_eq!(fragments.len(), 2);
126 assert!(fragments.iter().all(|m| {
127 m.topic.as_deref() == Some("IpcFragment/bulk") && m.payload.len() <= MAX_LEN
128 }));
129 }
130
131 #[test]
132 fn split_rejects_payloads_it_cannot_fragment() {
133 for provider_limit in [MAX_LEN, usize::MAX] {
135 assert!(matches!(
136 Fragments::split(
137 outgoing(vec![0; MAX_PAYLOAD_SIZE + 1], "bulk"),
138 provider_limit
139 ),
140 Err(FragmentationError::PayloadTooLarge(_))
141 ));
142 }
143 assert!(matches!(
144 Fragments::split(outgoing(vec![0; 2048], "bulk"), MAX_HEADER_LEN),
145 Err(FragmentationError::ProviderLimitTooSmall(_))
146 ));
147 }
148
149 #[test]
150 fn split_rejects_reserved_topics_even_for_small_payloads() {
151 for topic in [FRAGMENT_TOPIC, "IpcFragment/bulk"] {
152 assert!(matches!(
153 Fragments::split(outgoing(vec![0; 16], topic), MAX_LEN),
154 Err(FragmentationError::ReservedTopic(_))
155 ));
156 }
157 assert!(Fragments::split(outgoing(vec![0; 16], "IpcFragments"), MAX_LEN).is_ok());
158 }
159}