1use crate::ParallelBatch;
2use xdr_common::{BytesM, VecM};
3
4impl ParallelBatch {
5 pub fn empty() -> Self {
7 Self {
8 segment_tokens: VecM(vec![]), segment_bytes: VecM(vec![]),
10 xdr: BytesM(vec![]),
11 }
12 }
13
14 pub fn items(&self) -> u64 {
19 self.segment_tokens.iter().map(|&t| u64::from(t)).sum()
20 }
21
22 pub fn segments(&self) -> usize {
27 self.segment_tokens.len().min(self.segment_bytes.len())
28 }
29
30 pub fn is_empty(&self) -> bool {
32 self.segment_tokens.iter().all(|&t| t == 0)
33 }
34
35 pub fn validate(&self) -> Result<(), BatchValidationError> {
38 if self.segment_tokens.len() != self.segment_bytes.len() {
39 return Err(BatchValidationError::SegmentCountMismatch {
40 tokens: self.segment_tokens.len(),
41 bytes: self.segment_bytes.len(),
42 });
43 }
44 let expected: u64 = self.segment_bytes.iter().map(|&b| b as u64).sum();
45 let actual = self.xdr.len() as u64;
46 if expected != actual {
47 return Err(BatchValidationError::ByteLengthMismatch {
48 expected,
49 actual,
50 });
51 }
52 Ok(())
53 }
54
55 pub fn segment(&self, i: usize) -> Option<&[u8]> {
58 if i >= self.segments() {
59 return None;
60 }
61 let offset: u64 =
64 self.segment_bytes[..i].iter().map(|&b| u64::from(b)).sum();
65 let offset = usize::try_from(offset).ok()?;
66 let len = usize::try_from(self.segment_bytes[i]).ok()?;
67 self.xdr.0.get(offset..offset.checked_add(len)?)
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum BatchValidationError {
73 SegmentCountMismatch { tokens: usize, bytes: usize },
75 ByteLengthMismatch { expected: u64, actual: u64 },
77}
78
79#[cfg(feature = "std")]
80impl std::fmt::Display for BatchValidationError {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 Self::SegmentCountMismatch { tokens, bytes } => {
84 write!(
85 f,
86 "segment count mismatch: segment_tokens has {tokens} entries, segment_bytes has {bytes}"
87 )
88 }
89 Self::ByteLengthMismatch { expected, actual } => {
90 write!(
91 f,
92 "byte length mismatch: segment_bytes sum to {expected}, xdr is {actual} bytes"
93 )
94 }
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::{
103 ParallelNondeterministicProposalDataBatch,
104 ParallelSignedTransactionBatch, ParallelSignedTransactionHashBatch,
105 };
106 use xdr_common::{BytesM, VecM};
107
108 fn make_batch(segments: &[(u32, &[u8])]) -> ParallelBatch {
111 let mut segment_tokens = Vec::new();
112 let mut segment_bytes = Vec::new();
113 let mut xdr = Vec::new();
114 for &(tokens, payload) in segments {
115 segment_tokens.push(tokens);
116 segment_bytes.push(payload.len() as u32);
117 xdr.extend_from_slice(payload);
118 }
119 ParallelBatch {
120 segment_tokens: VecM(segment_tokens),
121 segment_bytes: VecM(segment_bytes),
122 xdr: BytesM(xdr),
123 }
124 }
125
126 #[test]
129 fn validate_empty_batch() {
130 let batch = ParallelBatch::empty();
131 assert!(batch.validate().is_ok());
132 }
133
134 #[test]
135 fn validate_single_segment() {
136 let batch = make_batch(&[(3, &[0xAA; 10])]);
137 assert!(batch.validate().is_ok());
138 }
139
140 #[test]
141 fn validate_multi_segment() {
142 let batch = make_batch(&[(1, &[1, 2, 3]), (2, &[4, 5]), (5, &[6])]);
143 assert!(batch.validate().is_ok());
144 }
145
146 #[test]
147 fn validate_segment_count_mismatch() {
148 let batch = ParallelBatch {
149 segment_tokens: VecM(vec![1, 2]),
150 segment_bytes: VecM(vec![4]),
151 xdr: BytesM(vec![0; 4]),
152 };
153 assert_eq!(
154 batch.validate(),
155 Err(BatchValidationError::SegmentCountMismatch {
156 tokens: 2,
157 bytes: 1,
158 })
159 );
160 }
161
162 #[test]
163 fn segments_and_segment_consistent_on_mismatched_vecs() {
164 let batch = ParallelBatch {
166 segment_tokens: VecM(vec![1, 2, 3]),
167 segment_bytes: VecM(vec![4, 5]),
168 xdr: BytesM(vec![0; 9]),
169 };
170 assert!(batch.validate().is_err());
171 assert_eq!(batch.segments(), 2);
173 assert!(batch.segment(0).is_some());
174 assert!(batch.segment(1).is_some());
175 assert_eq!(batch.segment(2), None);
176
177 let batch = ParallelBatch {
179 segment_tokens: VecM(vec![1]),
180 segment_bytes: VecM(vec![3, 4]),
181 xdr: BytesM(vec![0; 7]),
182 };
183 assert!(batch.validate().is_err());
184 assert_eq!(batch.segments(), 1);
185 assert!(batch.segment(0).is_some());
186 assert_eq!(batch.segment(1), None);
187 }
188
189 #[test]
190 fn validate_byte_length_too_short() {
191 let batch = ParallelBatch {
192 segment_tokens: VecM(vec![1]),
193 segment_bytes: VecM(vec![10]),
194 xdr: BytesM(vec![0; 5]), };
196 assert_eq!(
197 batch.validate(),
198 Err(BatchValidationError::ByteLengthMismatch {
199 expected: 10,
200 actual: 5,
201 })
202 );
203 }
204
205 #[test]
206 fn validate_byte_length_too_long() {
207 let batch = ParallelBatch {
208 segment_tokens: VecM(vec![1]),
209 segment_bytes: VecM(vec![3]),
210 xdr: BytesM(vec![0; 8]), };
212 assert_eq!(
213 batch.validate(),
214 Err(BatchValidationError::ByteLengthMismatch {
215 expected: 3,
216 actual: 8,
217 })
218 );
219 }
220
221 #[test]
224 fn empty_batch_properties() {
225 let batch = ParallelBatch::empty();
226 assert_eq!(batch.items(), 0);
227 assert_eq!(batch.segments(), 0);
228 assert!(batch.is_empty());
229 assert_eq!(batch.segment(0), None);
230 }
231
232 #[test]
233 fn items_sums_across_segments() {
234 let batch = make_batch(&[(3, &[0; 2]), (7, &[0; 4]), (1, &[0; 1])]);
235 assert_eq!(batch.items(), 11);
236 assert_eq!(batch.segments(), 3);
237 assert!(!batch.is_empty());
238 }
239
240 #[test]
241 fn segment_returns_correct_slices() {
242 let batch = make_batch(&[
243 (1, &[0xAA, 0xBB]),
244 (1, &[0xCC]),
245 (1, &[0xDD, 0xEE, 0xFF]),
246 ]);
247 assert_eq!(batch.segment(0), Some([0xAA, 0xBB].as_slice()));
248 assert_eq!(batch.segment(1), Some([0xCC].as_slice()));
249 assert_eq!(batch.segment(2), Some([0xDD, 0xEE, 0xFF].as_slice()));
250 assert_eq!(batch.segment(3), None);
251 }
252
253 #[test]
254 fn segment_out_of_bounds() {
255 let batch = make_batch(&[(1, &[0x01])]);
256 assert_eq!(batch.segment(1), None);
257 assert_eq!(batch.segment(100), None);
258 }
259
260 #[test]
263 fn xdr_cxs_roundtrip_empty() {
264 let batch = ParallelBatch::empty();
265 let encoded = xdrs::cxs::encode(&batch).unwrap();
266 let decoded: ParallelBatch = xdrs::cxs::decode(&encoded).unwrap();
267 assert_eq!(batch, decoded);
268 }
269
270 #[test]
271 fn xdr_cxs_roundtrip_populated() {
272 let batch = make_batch(&[(3, &[1, 2, 3, 4, 5]), (2, &[6, 7, 8])]);
273 let encoded = xdrs::cxs::encode(&batch).unwrap();
274 let decoded: ParallelBatch = xdrs::cxs::decode(&encoded).unwrap();
275 assert_eq!(batch, decoded);
276 }
277
278 #[test]
279 fn xdr_txs_roundtrip_empty() {
280 let batch = ParallelBatch::empty();
281 let encoded = xdrs::txs::encode(&batch).unwrap();
282 let decoded: ParallelBatch = xdrs::txs::decode(&encoded).unwrap();
283 assert_eq!(batch, decoded);
284 }
285
286 #[test]
287 fn xdr_txs_roundtrip_populated() {
288 let batch = make_batch(&[(10, &[0xFF; 32]), (5, &[0x42; 16])]);
289 let encoded = xdrs::txs::encode(&batch).unwrap();
290 let decoded: ParallelBatch = xdrs::txs::decode(&encoded).unwrap();
291 assert_eq!(batch, decoded);
292 }
293
294 #[test]
297 fn xdr_roundtrip_signed_transaction_batch() {
298 let inner = make_batch(&[(2, &[0xAA, 0xBB, 0xCC])]);
299 let batch = ParallelSignedTransactionBatch::from(inner.clone());
300 let encoded = xdrs::cxs::encode(&batch).unwrap();
301 let decoded: ParallelSignedTransactionBatch =
302 xdrs::cxs::decode(&encoded).unwrap();
303 assert_eq!(*decoded, inner);
304 }
305
306 #[test]
307 fn xdr_roundtrip_signed_transaction_hash_batch() {
308 let inner = make_batch(&[(1, &[0xDE, 0xAD])]);
309 let batch = ParallelSignedTransactionHashBatch::from(inner.clone());
310 let encoded = xdrs::txs::encode(&batch).unwrap();
311 let decoded: ParallelSignedTransactionHashBatch =
312 xdrs::txs::decode(&encoded).unwrap();
313 assert_eq!(*decoded, inner);
314 }
315
316 #[test]
317 fn xdr_roundtrip_nondeterministic_proposal_data_batch() {
318 let inner = make_batch(&[(4, &[1; 8]), (3, &[2; 6])]);
319 let batch =
320 ParallelNondeterministicProposalDataBatch::from(inner.clone());
321 let encoded = xdrs::cxs::encode(&batch).unwrap();
322 let decoded: ParallelNondeterministicProposalDataBatch =
323 xdrs::cxs::decode(&encoded).unwrap();
324 assert_eq!(*decoded, inner);
325 }
326
327 #[cfg(feature = "serde")]
330 #[test]
331 fn serde_json_roundtrip_empty() {
332 let batch = ParallelBatch::empty();
333 let json = serde_json::to_string(&batch).unwrap();
334 let decoded: ParallelBatch = serde_json::from_str(&json).unwrap();
335 assert_eq!(batch, decoded);
336 }
337
338 #[cfg(feature = "serde")]
339 #[test]
340 fn serde_json_roundtrip_populated() {
341 let batch = make_batch(&[(3, &[1, 2, 3, 4, 5]), (2, &[6, 7, 8])]);
342 let json = serde_json::to_string(&batch).unwrap();
343 let decoded: ParallelBatch = serde_json::from_str(&json).unwrap();
344 assert_eq!(batch, decoded);
345 }
346
347 #[cfg(feature = "serde")]
348 #[test]
349 fn serde_json_roundtrip_newtypes() {
350 let inner = make_batch(&[(1, &[0xFF; 4])]);
351
352 let tx = ParallelSignedTransactionBatch::from(inner.clone());
353 let json = serde_json::to_string(&tx).unwrap();
354 let decoded: ParallelSignedTransactionBatch =
355 serde_json::from_str(&json).unwrap();
356 assert_eq!(*decoded, inner);
357
358 let hash = ParallelSignedTransactionHashBatch::from(inner.clone());
359 let json = serde_json::to_string(&hash).unwrap();
360 let decoded: ParallelSignedTransactionHashBatch =
361 serde_json::from_str(&json).unwrap();
362 assert_eq!(*decoded, inner);
363
364 let nd =
365 ParallelNondeterministicProposalDataBatch::from(inner.clone());
366 let json = serde_json::to_string(&nd).unwrap();
367 let decoded: ParallelNondeterministicProposalDataBatch =
368 serde_json::from_str(&json).unwrap();
369 assert_eq!(*decoded, inner);
370 }
371
372 #[test]
375 fn newtype_from_and_into_inner() {
376 let inner = make_batch(&[(5, &[0x01; 10])]);
377 let tx = ParallelSignedTransactionBatch::from(inner.clone());
378 assert_eq!(tx.into_inner(), inner);
379
380 let hash = ParallelSignedTransactionHashBatch::new(inner.clone());
381 assert_eq!(hash.into_inner(), inner);
382
383 let nd =
384 ParallelNondeterministicProposalDataBatch::from(inner.clone());
385 assert_eq!(nd.into_inner(), inner);
386 }
387
388 #[test]
389 fn newtype_deref_provides_validation() {
390 let inner = make_batch(&[(2, &[0xAB; 6])]);
391 let tx = ParallelSignedTransactionBatch::from(inner);
392 assert!(tx.validate().is_ok());
394 assert_eq!(tx.items(), 2);
395 assert_eq!(tx.segments(), 1);
396 assert_eq!(tx.segment(0), Some([0xAB; 6].as_slice()));
397 }
398
399 #[test]
400 fn items_does_not_overflow_u32() {
401 let batch = ParallelBatch {
402 segment_tokens: VecM(vec![u32::MAX, u32::MAX]),
403 segment_bytes: VecM(vec![0, 0]),
404 xdr: BytesM(vec![]),
405 };
406 assert_eq!(batch.items(), 2 * u64::from(u32::MAX));
407 assert!(!batch.is_empty());
408 }
409
410 #[test]
411 fn is_empty_with_zero_token_segments() {
412 let batch = make_batch(&[(0, &[]), (0, &[])]);
413 assert!(batch.is_empty());
414 assert_eq!(batch.items(), 0);
415 }
416
417 #[test]
418 fn segment_with_corrupt_lengths_is_none() {
419 let batch = ParallelBatch {
422 segment_tokens: VecM(vec![1, 1, 1]),
423 segment_bytes: VecM(vec![u32::MAX, u32::MAX, 1]),
424 xdr: BytesM(vec![0; 4]),
425 };
426 assert!(batch.validate().is_err());
427 assert_eq!(batch.segment(0), None);
428 assert_eq!(batch.segment(2), None);
429 }
430}