|
| 1 | +//! Edge case tests for the PSRP fragmentation/defragmentation layer. |
| 2 | +//! |
| 3 | +//! These tests verify that the defragmenter handles malformed, truncated, |
| 4 | +//! and unexpected fragment data gracefully. |
| 5 | +
|
| 6 | +use byteorder::{BigEndian, WriteBytesExt}; |
| 7 | +use ironposh_psrp::fragmentation::{DefragmentResult, Defragmenter}; |
| 8 | + |
| 9 | +/// Create a minimal valid fragment header + data |
| 10 | +fn create_fragment( |
| 11 | + object_id: u64, |
| 12 | + fragment_id: u64, |
| 13 | + start: bool, |
| 14 | + end: bool, |
| 15 | + data: &[u8], |
| 16 | +) -> Vec<u8> { |
| 17 | + let mut buffer = Vec::new(); |
| 18 | + |
| 19 | + // Object ID (8 bytes, big endian) |
| 20 | + buffer.write_u64::<BigEndian>(object_id).unwrap(); |
| 21 | + |
| 22 | + // Fragment ID (8 bytes, big endian) |
| 23 | + buffer.write_u64::<BigEndian>(fragment_id).unwrap(); |
| 24 | + |
| 25 | + // Start/End flags (1 byte) |
| 26 | + let mut flags = 0u8; |
| 27 | + if start { |
| 28 | + flags |= 0x01; |
| 29 | + } |
| 30 | + if end { |
| 31 | + flags |= 0x02; |
| 32 | + } |
| 33 | + buffer.push(flags); |
| 34 | + |
| 35 | + // Data length (4 bytes, big endian) |
| 36 | + buffer |
| 37 | + .write_u32::<BigEndian>(data.len() as u32) |
| 38 | + .unwrap(); |
| 39 | + |
| 40 | + // Data payload |
| 41 | + buffer.extend_from_slice(data); |
| 42 | + |
| 43 | + buffer |
| 44 | +} |
| 45 | + |
| 46 | +#[cfg(test)] |
| 47 | +mod tests { |
| 48 | + use super::*; |
| 49 | + |
| 50 | + // ========================================================================= |
| 51 | + // TRUNCATION TESTS |
| 52 | + // ========================================================================= |
| 53 | + |
| 54 | + /// Test: Fragment data completely empty |
| 55 | + #[test] |
| 56 | + fn test_empty_input() { |
| 57 | + let mut defrag = Defragmenter::new(); |
| 58 | + let result = defrag.defragment(&[]); |
| 59 | + |
| 60 | + // Empty input should return Incomplete (nothing to process) |
| 61 | + match result { |
| 62 | + Ok(DefragmentResult::Incomplete) => { |
| 63 | + println!("Empty input correctly returns Incomplete"); |
| 64 | + } |
| 65 | + Ok(DefragmentResult::Complete(_)) => { |
| 66 | + panic!("Empty input should not produce complete messages"); |
| 67 | + } |
| 68 | + Err(e) => { |
| 69 | + // Also acceptable - depends on implementation |
| 70 | + println!("Empty input returned error (acceptable): {e}"); |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + /// Test: Fragment header truncated (less than 21 bytes minimum) |
| 76 | + #[test] |
| 77 | + fn test_truncated_header() { |
| 78 | + let mut defrag = Defragmenter::new(); |
| 79 | + |
| 80 | + // Only 10 bytes - not enough for header |
| 81 | + let short_data = vec![0u8; 10]; |
| 82 | + let result = defrag.defragment(&short_data); |
| 83 | + |
| 84 | + assert!( |
| 85 | + result.is_err(), |
| 86 | + "Truncated header should fail, got: {result:?}" |
| 87 | + ); |
| 88 | + |
| 89 | + let err = result.unwrap_err(); |
| 90 | + let err_str = format!("{err}"); |
| 91 | + println!("Truncated header error: {err_str}"); |
| 92 | + assert!( |
| 93 | + err_str.contains("too short") || err_str.contains("truncated") || err_str.contains("21"), |
| 94 | + "Error should mention size requirement" |
| 95 | + ); |
| 96 | + } |
| 97 | + |
| 98 | + /// Test: Header claims more data than available |
| 99 | + #[test] |
| 100 | + fn test_truncated_data_payload() { |
| 101 | + let mut defrag = Defragmenter::new(); |
| 102 | + |
| 103 | + // Create header that claims 1000 bytes of data, but only provide 10 |
| 104 | + let mut buffer = Vec::new(); |
| 105 | + buffer.write_u64::<BigEndian>(1).unwrap(); // object_id |
| 106 | + buffer.write_u64::<BigEndian>(0).unwrap(); // fragment_id |
| 107 | + buffer.push(0x03); // flags: start + end |
| 108 | + buffer.write_u32::<BigEndian>(1000).unwrap(); // claims 1000 bytes |
| 109 | + buffer.extend_from_slice(&[0u8; 10]); // but only 10 bytes present |
| 110 | + |
| 111 | + let result = defrag.defragment(&buffer); |
| 112 | + |
| 113 | + assert!( |
| 114 | + result.is_err(), |
| 115 | + "Truncated payload should fail, got: {result:?}" |
| 116 | + ); |
| 117 | + |
| 118 | + let err = result.unwrap_err(); |
| 119 | + println!("Truncated payload error: {err}"); |
| 120 | + } |
| 121 | + |
| 122 | + /// Test: Exactly 21 bytes (header only, 0-length data) with start+end flags |
| 123 | + #[test] |
| 124 | + fn test_zero_length_data_fragment() { |
| 125 | + let mut defrag = Defragmenter::new(); |
| 126 | + |
| 127 | + // Valid header with 0 bytes of data |
| 128 | + let fragment = create_fragment(1, 0, true, true, &[]); |
| 129 | + |
| 130 | + let result = defrag.defragment(&fragment); |
| 131 | + |
| 132 | + // Zero-length fragment is syntactically valid, but may fail at message parsing |
| 133 | + match result { |
| 134 | + Ok(DefragmentResult::Complete(_)) => { |
| 135 | + println!("Zero-length fragment accepted (lenient)"); |
| 136 | + } |
| 137 | + Ok(DefragmentResult::Incomplete) => { |
| 138 | + println!("Zero-length fragment returned incomplete"); |
| 139 | + } |
| 140 | + Err(e) => { |
| 141 | + // Expected - empty data can't be a valid PSRP message |
| 142 | + println!("Zero-length fragment correctly rejected: {e}"); |
| 143 | + } |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + // ========================================================================= |
| 148 | + // OUT OF ORDER / DUPLICATE TESTS |
| 149 | + // ========================================================================= |
| 150 | + |
| 151 | + /// Test: Fragments arrive in reverse order (2, 1, 0) instead of (0, 1, 2) |
| 152 | + #[test] |
| 153 | + fn test_out_of_order_fragments() { |
| 154 | + let mut defrag = Defragmenter::new(); |
| 155 | + |
| 156 | + // Create 3 fragments for object_id=1 |
| 157 | + // Fragment 0: start, data="AAA" |
| 158 | + // Fragment 1: middle, data="BBB" |
| 159 | + // Fragment 2: end, data="CCC" |
| 160 | + |
| 161 | + let frag0 = create_fragment(1, 0, true, false, b"AAA"); |
| 162 | + let frag1 = create_fragment(1, 1, false, false, b"BBB"); |
| 163 | + let frag2 = create_fragment(1, 2, false, true, b"CCC"); |
| 164 | + |
| 165 | + // Send in reverse order |
| 166 | + let result2 = defrag.defragment(&frag2); |
| 167 | + println!("After frag2 (end): {result2:?}"); |
| 168 | + assert!( |
| 169 | + matches!(result2, Ok(DefragmentResult::Incomplete) | Err(_)), |
| 170 | + "End fragment alone should not complete" |
| 171 | + ); |
| 172 | + |
| 173 | + let result1 = defrag.defragment(&frag1); |
| 174 | + println!("After frag1 (middle): {result1:?}"); |
| 175 | + |
| 176 | + let result0 = defrag.defragment(&frag0); |
| 177 | + println!("After frag0 (start): {result0:?}"); |
| 178 | + |
| 179 | + // Depending on implementation, either: |
| 180 | + // - It reassembles correctly (sorts by fragment_id) |
| 181 | + // - It fails/returns incomplete (strict ordering required) |
| 182 | + // Both are acceptable behaviors - we're testing it doesn't panic |
| 183 | + |
| 184 | + println!( |
| 185 | + "Out-of-order test completed. Pending buffers: {}", |
| 186 | + defrag.pending_count() |
| 187 | + ); |
| 188 | + } |
| 189 | + |
| 190 | + /// Test: Same fragment arrives twice |
| 191 | + #[test] |
| 192 | + fn test_duplicate_fragment() { |
| 193 | + let mut defrag = Defragmenter::new(); |
| 194 | + |
| 195 | + // Create a multi-fragment message |
| 196 | + let frag0 = create_fragment(1, 0, true, false, b"DATA1"); |
| 197 | + let frag1 = create_fragment(1, 1, false, true, b"DATA2"); |
| 198 | + |
| 199 | + // Send frag0 twice |
| 200 | + let _ = defrag.defragment(&frag0); |
| 201 | + let _ = defrag.defragment(&frag0); // duplicate! |
| 202 | + |
| 203 | + let result = defrag.defragment(&frag1); |
| 204 | + println!("Duplicate fragment handling: {result:?}"); |
| 205 | + |
| 206 | + // Should either complete or handle the duplicate gracefully |
| 207 | + // Main test is that we don't panic or corrupt state |
| 208 | + } |
| 209 | + |
| 210 | + // ========================================================================= |
| 211 | + // MISSING FRAGMENT TESTS |
| 212 | + // ========================================================================= |
| 213 | + |
| 214 | + /// Test: Start and end fragments present, but middle is missing |
| 215 | + #[test] |
| 216 | + fn test_missing_middle_fragment() { |
| 217 | + let mut defrag = Defragmenter::new(); |
| 218 | + |
| 219 | + // Fragment 0: start |
| 220 | + let frag0 = create_fragment(1, 0, true, false, b"START"); |
| 221 | + // Fragment 1: (missing!) |
| 222 | + // Fragment 2: end |
| 223 | + let frag2 = create_fragment(1, 2, false, true, b"END"); |
| 224 | + |
| 225 | + let _ = defrag.defragment(&frag0); |
| 226 | + let result = defrag.defragment(&frag2); |
| 227 | + |
| 228 | + // Should remain incomplete or fail - can't reassemble without middle |
| 229 | + println!("Missing middle fragment result: {result:?}"); |
| 230 | + println!("Pending buffers: {}", defrag.pending_count()); |
| 231 | + } |
| 232 | + |
| 233 | + /// Test: Only end fragment, no start |
| 234 | + #[test] |
| 235 | + fn test_missing_start_fragment() { |
| 236 | + let mut defrag = Defragmenter::new(); |
| 237 | + |
| 238 | + // Only send end fragment |
| 239 | + let frag_end = create_fragment(1, 5, false, true, b"END_ONLY"); |
| 240 | + |
| 241 | + let result = defrag.defragment(&frag_end); |
| 242 | + |
| 243 | + // Should not complete - we never got the start |
| 244 | + match result { |
| 245 | + Ok(DefragmentResult::Incomplete) => { |
| 246 | + println!("Missing start correctly returns Incomplete"); |
| 247 | + } |
| 248 | + Ok(DefragmentResult::Complete(_)) => { |
| 249 | + // This would be surprising but not necessarily wrong |
| 250 | + println!("Warning: Completed without start fragment"); |
| 251 | + } |
| 252 | + Err(e) => { |
| 253 | + println!("Missing start returned error: {e}"); |
| 254 | + } |
| 255 | + } |
| 256 | + } |
| 257 | + |
| 258 | + // ========================================================================= |
| 259 | + // INTERLEAVED MESSAGES TESTS |
| 260 | + // ========================================================================= |
| 261 | + |
| 262 | + /// Test: Two different object_ids have fragments interleaved |
| 263 | + #[test] |
| 264 | + fn test_interleaved_objects() { |
| 265 | + let mut defrag = Defragmenter::new(); |
| 266 | + |
| 267 | + // Object 1: fragments 0, 1 |
| 268 | + let obj1_frag0 = create_fragment(1, 0, true, false, b"OBJ1_START"); |
| 269 | + let obj1_frag1 = create_fragment(1, 1, false, true, b"OBJ1_END"); |
| 270 | + |
| 271 | + // Object 2: fragments 0, 1 |
| 272 | + let obj2_frag0 = create_fragment(2, 0, true, false, b"OBJ2_START"); |
| 273 | + let obj2_frag1 = create_fragment(2, 1, false, true, b"OBJ2_END"); |
| 274 | + |
| 275 | + // Interleave: obj1_f0, obj2_f0, obj1_f1, obj2_f1 |
| 276 | + let _ = defrag.defragment(&obj1_frag0); |
| 277 | + assert_eq!(defrag.pending_count(), 1, "Should have 1 pending after obj1_f0"); |
| 278 | + |
| 279 | + let _ = defrag.defragment(&obj2_frag0); |
| 280 | + assert_eq!(defrag.pending_count(), 2, "Should have 2 pending after obj2_f0"); |
| 281 | + |
| 282 | + let result1 = defrag.defragment(&obj1_frag1); |
| 283 | + println!("After obj1 complete: {result1:?}, pending: {}", defrag.pending_count()); |
| 284 | + |
| 285 | + let result2 = defrag.defragment(&obj2_frag1); |
| 286 | + println!("After obj2 complete: {result2:?}, pending: {}", defrag.pending_count()); |
| 287 | + |
| 288 | + // Both should eventually complete, pending should be 0 |
| 289 | + // (or close to 0 if messages failed to parse) |
| 290 | + } |
| 291 | + |
| 292 | + // ========================================================================= |
| 293 | + // INVALID FLAG COMBINATIONS |
| 294 | + // ========================================================================= |
| 295 | + |
| 296 | + /// Test: Fragment with neither start nor end flag (orphan middle) |
| 297 | + #[test] |
| 298 | + fn test_orphan_middle_fragment() { |
| 299 | + let mut defrag = Defragmenter::new(); |
| 300 | + |
| 301 | + // Middle fragment with no start ever sent |
| 302 | + let orphan = create_fragment(99, 5, false, false, b"ORPHAN"); |
| 303 | + |
| 304 | + let result = defrag.defragment(&orphan); |
| 305 | + |
| 306 | + // Should buffer it (waiting for more) or reject it |
| 307 | + println!("Orphan middle fragment: {result:?}"); |
| 308 | + println!("Pending: {}", defrag.pending_count()); |
| 309 | + } |
| 310 | + |
| 311 | + /// Test: Fragment with both start and end (single-fragment message) but invalid content |
| 312 | + #[test] |
| 313 | + fn test_single_fragment_invalid_content() { |
| 314 | + let mut defrag = Defragmenter::new(); |
| 315 | + |
| 316 | + // Valid fragment structure, but garbage data that's not a valid PSRP message |
| 317 | + let garbage = create_fragment(1, 0, true, true, b"NOT_A_VALID_PSRP_MESSAGE"); |
| 318 | + |
| 319 | + let result = defrag.defragment(&garbage); |
| 320 | + |
| 321 | + // Should fail at message parsing stage |
| 322 | + assert!( |
| 323 | + result.is_err(), |
| 324 | + "Invalid PSRP content should fail parsing, got: {result:?}" |
| 325 | + ); |
| 326 | + |
| 327 | + println!("Invalid content correctly rejected: {:?}", result.unwrap_err()); |
| 328 | + } |
| 329 | + |
| 330 | + // ========================================================================= |
| 331 | + // BOUNDARY CONDITIONS |
| 332 | + // ========================================================================= |
| 333 | + |
| 334 | + /// Test: Very large fragment_id values (near u64::MAX) |
| 335 | + #[test] |
| 336 | + fn test_large_fragment_ids() { |
| 337 | + let mut defrag = Defragmenter::new(); |
| 338 | + |
| 339 | + let large_id = u64::MAX - 1; |
| 340 | + let fragment = create_fragment(large_id, large_id, true, true, b"data"); |
| 341 | + |
| 342 | + let result = defrag.defragment(&fragment); |
| 343 | + |
| 344 | + // Should handle large IDs without overflow issues |
| 345 | + println!("Large fragment ID result: {result:?}"); |
| 346 | + } |
| 347 | + |
| 348 | + /// Test: Multiple complete single-fragment messages in one packet |
| 349 | + #[test] |
| 350 | + fn test_multiple_messages_one_packet() { |
| 351 | + let mut defrag = Defragmenter::new(); |
| 352 | + |
| 353 | + // Two complete (start+end) fragments concatenated |
| 354 | + // Note: These will fail at PSRP parsing, but fragment layer should handle them |
| 355 | + let frag1 = create_fragment(1, 0, true, true, b"MSG1"); |
| 356 | + let frag2 = create_fragment(2, 0, true, true, b"MSG2"); |
| 357 | + |
| 358 | + let mut combined = frag1; |
| 359 | + combined.extend_from_slice(&frag2); |
| 360 | + |
| 361 | + let result = defrag.defragment(&combined); |
| 362 | + |
| 363 | + // Fragment layer should extract both, but PSRP parsing will likely fail |
| 364 | + println!("Multiple messages in one packet: {result:?}"); |
| 365 | + } |
| 366 | + |
| 367 | + // ========================================================================= |
| 368 | + // BUFFER MANAGEMENT |
| 369 | + // ========================================================================= |
| 370 | + |
| 371 | + /// Test: clear_buffers() removes pending state |
| 372 | + #[test] |
| 373 | + fn test_clear_buffers() { |
| 374 | + let mut defrag = Defragmenter::new(); |
| 375 | + |
| 376 | + // Start a message but don't finish it |
| 377 | + let incomplete = create_fragment(1, 0, true, false, b"INCOMPLETE"); |
| 378 | + let _ = defrag.defragment(&incomplete); |
| 379 | + |
| 380 | + assert!(defrag.pending_count() > 0, "Should have pending buffer"); |
| 381 | + |
| 382 | + defrag.clear_buffers(); |
| 383 | + |
| 384 | + assert_eq!(defrag.pending_count(), 0, "Buffers should be cleared"); |
| 385 | + println!("clear_buffers() works correctly"); |
| 386 | + } |
| 387 | +} |
0 commit comments