focused_availability_query.rs (39848B)
1 use std::error::Error; 2 use std::fmt::Write; 3 4 use harvestcircle_application::{ 5 AvailabilityLocalQueryScope, ScopedAvailabilityQuery, SessionGeneration, 6 }; 7 use harvestcircle_domain::availability::query::{EventTimestamp, FoodAvailabilityStatus}; 8 use harvestcircle_domain::{ 9 AVAILABILITY_CURSOR_MAX_BYTES, AVAILABILITY_PAGE_DEFAULT_ROWS, AVAILABILITY_PAGE_MAX_ROWS, 10 AVAILABILITY_QUERY_TEXT_MAX_BYTES, AvailabilityEventVersion, AvailabilityOrderKey, 11 AvailabilityPage, AvailabilityPageContinuation, AvailabilityPageCursor, AvailabilityPageLimit, 12 AvailabilityQueryContext, AvailabilityQueryError, AvailabilityQueryFilters, 13 AvailabilityQueryFingerprint, AvailabilitySearchText, PublicKey, PublicPublisher, 14 }; 15 16 const AUTHOR_HEX: &str = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; 17 const OTHER_AUTHOR_HEX: &str = "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7"; 18 // Independent pre-production SHA-256 of the specified v1 frame, not an encoder result. 19 const GOLDEN_FINGERPRINT_HEX: &str = 20 "3db9ea644546b310f06abf524cfa0248fb5b1757e8a34620a4c820ba16a20276"; 21 const GOLDEN_FINGERPRINT_BYTES: [u8; 32] = [ 22 0x3d, 0xb9, 0xea, 0x64, 0x45, 0x46, 0xb3, 0x10, 0xf0, 0x6a, 0xbf, 0x52, 0x4c, 0xfa, 0x02, 0x48, 23 0xfb, 0x5b, 0x17, 0x57, 0xe8, 0xa3, 0x46, 0x20, 0xa4, 0xc8, 0x20, 0xba, 0x16, 0xa2, 0x02, 0x76, 24 ]; 25 26 fn author() -> PublicKey { 27 PublicKey::from_hex(AUTHOR_HEX).expect("fixed valid public curve author") 28 } 29 30 fn other_author() -> PublicKey { 31 PublicKey::from_hex(OTHER_AUTHOR_HEX).expect("second fixed valid public curve author") 32 } 33 34 fn publisher() -> PublicPublisher { 35 PublicPublisher::from_public_key(author()) 36 } 37 38 fn other_publisher() -> PublicPublisher { 39 PublicPublisher::from_public_key(other_author()) 40 } 41 42 fn context( 43 context_id: [u8; 32], 44 store_generation: [u8; 32], 45 source_revision: u64, 46 projection_generation: u64, 47 ) -> AvailabilityQueryContext { 48 AvailabilityQueryContext::new( 49 context_id, 50 store_generation, 51 source_revision, 52 projection_generation, 53 ) 54 .expect("nonzero structural query identities") 55 } 56 57 fn base_context() -> AvailabilityQueryContext { 58 context([1; 32], [2; 32], 5, 7) 59 } 60 61 fn filters( 62 search: Option<&str>, 63 publisher: Option<PublicPublisher>, 64 status: Option<FoodAvailabilityStatus>, 65 ) -> AvailabilityQueryFilters { 66 AvailabilityQueryFilters::new( 67 search.map(|text| AvailabilitySearchText::new(text).expect("bounded cached search text")), 68 publisher, 69 status, 70 ) 71 } 72 73 fn base_filters() -> AvailabilityQueryFilters { 74 filters(Some("a"), None, Some(FoodAvailabilityStatus::Active)) 75 } 76 77 fn scope( 78 owner: PublicKey, 79 context: AvailabilityQueryContext, 80 session_generation: u64, 81 ) -> AvailabilityLocalQueryScope { 82 AvailabilityLocalQueryScope::new( 83 owner, 84 context, 85 SessionGeneration::from_value(session_generation), 86 ) 87 } 88 89 fn base_scope() -> AvailabilityLocalQueryScope { 90 scope(author(), base_context(), 3) 91 } 92 93 fn base_fingerprint() -> AvailabilityQueryFingerprint { 94 AvailabilityQueryFingerprint::new( 95 author(), 96 &base_context(), 97 3, 98 &base_filters(), 99 AvailabilityPageLimit::default(), 100 ) 101 } 102 103 fn base_query() -> ScopedAvailabilityQuery { 104 ScopedAvailabilityQuery::new( 105 base_scope(), 106 base_filters(), 107 AvailabilityPageLimit::default(), 108 None, 109 ) 110 .expect("pure local structural query") 111 } 112 113 fn hex(bytes: &[u8]) -> String { 114 let mut text = String::with_capacity(bytes.len() * 2); 115 for byte in bytes { 116 write!(&mut text, "{byte:02x}").expect("writing hexadecimal into a string"); 117 } 118 text 119 } 120 121 fn version(bytes: [u8; 32]) -> AvailabilityEventVersion { 122 AvailabilityEventVersion::from_hex(&hex(&bytes)).expect("exact public event ID") 123 } 124 125 fn order(timestamp: u64, event_id: [u8; 32]) -> AvailabilityOrderKey { 126 AvailabilityOrderKey::new(EventTimestamp::new(timestamp), version(event_id)) 127 } 128 129 fn canonical_cursor(timestamp_hex: &str, event_id: [u8; 32]) -> String { 130 format!( 131 "hcq1:{GOLDEN_FINGERPRINT_HEX}:{timestamp_hex}:{}", 132 hex(&event_id) 133 ) 134 } 135 136 fn text_at_byte_count(unit: &str, count: usize) -> String { 137 unit.repeat(count / unit.len()) + &"x".repeat(count % unit.len()) 138 } 139 140 fn assert_distinct(fingerprints: &[AvailabilityQueryFingerprint]) { 141 for (index, fingerprint) in fingerprints.iter().enumerate() { 142 for other in &fingerprints[index + 1..] { 143 assert_ne!( 144 fingerprint, other, 145 "different framed requests must remain distinct" 146 ); 147 } 148 } 149 } 150 151 #[test] 152 fn page_limits_admit_default_and_exact_maximum() { 153 assert_eq!(AVAILABILITY_PAGE_DEFAULT_ROWS, 50_u16); 154 assert_eq!(AVAILABILITY_PAGE_MAX_ROWS, 100_u16); 155 assert_eq!(AvailabilityPageLimit::default().rows(), 50); 156 157 for rows in 1..=100 { 158 assert_eq!( 159 AvailabilityPageLimit::new(rows) 160 .expect("admitted row limit") 161 .rows(), 162 rows 163 ); 164 } 165 for rows in [0, 101, u16::MAX] { 166 assert_eq!( 167 AvailabilityPageLimit::new(rows).expect_err("outside the exact row bounds"), 168 AvailabilityQueryError::InvalidInput, 169 ); 170 } 171 } 172 173 #[test] 174 fn search_text_preserves_exact_utf8_with_byte_boundaries() { 175 assert_eq!(AVAILABILITY_QUERY_TEXT_MAX_BYTES, 512_usize); 176 for text in [ 177 "", 178 " ", 179 " farm:%_\\\t\n\0🥕 ", 180 "é", 181 "e\u{301}", 182 "a\u{200d}b", 183 ] { 184 let search = AvailabilitySearchText::new(text).expect("opaque cached search text"); 185 assert_eq!(search.as_str().as_bytes(), text.as_bytes()); 186 } 187 assert_ne!( 188 AvailabilitySearchText::new("é") 189 .expect("precomposed") 190 .as_str(), 191 AvailabilitySearchText::new("e\u{301}") 192 .expect("combining") 193 .as_str(), 194 ); 195 196 for unit in ["x", "é", "🥕", "e\u{301}"] { 197 for count in [511, 512] { 198 let text = text_at_byte_count(unit, count); 199 assert_eq!(text.len(), count); 200 assert_eq!( 201 AvailabilitySearchText::new(&text) 202 .expect("exact UTF-8 byte boundary") 203 .as_str(), 204 text, 205 ); 206 } 207 let text = text_at_byte_count(unit, 513); 208 assert_eq!(text.len(), 513); 209 assert_eq!( 210 AvailabilitySearchText::new(&text).expect_err("one byte over the text cap"), 211 AvailabilityQueryError::InputTooLarge, 212 ); 213 } 214 } 215 216 #[test] 217 fn filters_keep_optional_search_publisher_and_shared_status() { 218 for search in [None, Some(""), Some(" \té\0%_\n")] { 219 for publisher in [None, Some(publisher()), Some(other_publisher())] { 220 for status in [ 221 None, 222 Some(FoodAvailabilityStatus::Active), 223 Some(FoodAvailabilityStatus::Sold), 224 ] { 225 let value = filters(search, publisher, status); 226 assert_eq!(value.search().map(AvailabilitySearchText::as_str), search); 227 assert_eq!(value.publisher(), publisher); 228 assert_eq!(value.status(), status); 229 } 230 } 231 } 232 assert_eq!(FoodAvailabilityStatus::Active.as_str(), "active"); 233 assert_eq!(FoodAvailabilityStatus::Sold.as_str(), "sold"); 234 assert!(filters(None, None, None).search().is_none()); 235 assert_eq!( 236 filters(Some(""), None, None) 237 .search() 238 .expect("present empty search") 239 .as_str(), 240 "" 241 ); 242 } 243 244 #[test] 245 fn context_binds_nonzero_identity_and_full_width_generations() { 246 for (source_revision, projection_generation) in [ 247 (0, 0), 248 (5, 7), 249 (1_u64 << 63, (1_u64 << 63) + 1), 250 (u64::MAX, u64::MAX), 251 ] { 252 let value = context([1; 32], [2; 32], source_revision, projection_generation); 253 assert_eq!(value.context_id(), &[1; 32]); 254 assert_eq!(value.store_generation(), &[2; 32]); 255 assert_eq!(value.source_revision(), source_revision); 256 assert_eq!(value.projection_generation(), projection_generation); 257 } 258 for index in [0, 16, 31] { 259 let mut nonzero = [0; 32]; 260 nonzero[index] = 1; 261 let value = context(nonzero, nonzero, 0, 0); 262 assert_eq!(value.context_id(), &nonzero); 263 assert_eq!(value.store_generation(), &nonzero); 264 } 265 for (context_id, store_generation) in 266 [([0; 32], [2; 32]), ([1; 32], [0; 32]), ([0; 32], [0; 32])] 267 { 268 assert_eq!( 269 AvailabilityQueryContext::new(context_id, store_generation, 0, u64::MAX) 270 .expect_err("zero opaque identity"), 271 AvailabilityQueryError::InvalidInput, 272 ); 273 } 274 } 275 276 #[test] 277 fn ordering_uses_descending_signed_time_then_lowest_event_id() { 278 let mut middle_id = [0; 32]; 279 middle_id[16] = 1; 280 let mut last_id = [0; 32]; 281 last_id[31] = 1; 282 let expected = vec![ 283 order(u64::MAX, [0; 32]), 284 order(u64::MAX, [u8::MAX; 32]), 285 order(u64::MAX - 1, [0; 32]), 286 order(1_u64 << 63, [0; 32]), 287 order((1_u64 << 63) - 1, [0; 32]), 288 order(10, [0; 32]), 289 order(10, last_id), 290 order(10, middle_id), 291 order(10, [1; 32]), 292 order(0, [u8::MAX; 32]), 293 ]; 294 let mut reversed = expected.clone(); 295 reversed.reverse(); 296 reversed.sort(); 297 assert_eq!(reversed, expected); 298 let permutation = [7, 3, 9, 1, 5, 0, 8, 4, 2, 6]; 299 let mut permuted: Vec<_> = permutation 300 .into_iter() 301 .map(|index| expected[index]) 302 .collect(); 303 permuted.sort(); 304 assert_eq!(permuted, expected); 305 for rotation in 1..expected.len() { 306 let mut input = expected.clone(); 307 input.rotate_left(rotation); 308 input.sort(); 309 assert_eq!(input, expected); 310 } 311 assert_eq!(expected[0].created_at().as_u64(), u64::MAX); 312 assert_eq!(expected[3].created_at().as_u64(), 1_u64 << 63); 313 assert_eq!(expected[6].version().event_id().as_bytes(), &last_id); 314 assert_eq!( 315 order(10, middle_id).cmp(&order(10, middle_id)), 316 std::cmp::Ordering::Equal 317 ); 318 assert_eq!( 319 order(0, [0; 32]).version(), 320 order(u64::MAX, [0; 32]).version() 321 ); 322 assert!(order(u64::MAX, [0; 32]) < order(0, [0; 32])); 323 } 324 325 #[test] 326 fn continuation_position_is_strict_for_time_and_id_ties() { 327 let mut last_id = [0; 32]; 328 last_id[31] = 1; 329 let mut middle_id = [0; 32]; 330 middle_id[16] = 1; 331 let positions = [ 332 order(u64::MAX, [0; 32]), 333 order(1_u64 << 63, [0; 32]), 334 order((1_u64 << 63) - 1, [0; 32]), 335 order(10, [0; 32]), 336 order(10, last_id), 337 order(10, middle_id), 338 order(10, [u8::MAX; 32]), 339 order(0, [0; 32]), 340 ]; 341 for (previous_index, previous) in positions.into_iter().enumerate() { 342 assert!( 343 !previous.is_after(previous), 344 "equal keys are never continuation positions" 345 ); 346 for (candidate_index, candidate) in positions.into_iter().enumerate() { 347 assert_eq!( 348 candidate.is_after(previous), 349 candidate_index > previous_index 350 ); 351 } 352 } 353 assert!(order(9, [0; 32]).is_after(order(10, [u8::MAX; 32]))); 354 assert!(!order(11, [u8::MAX; 32]).is_after(order(10, [0; 32]))); 355 } 356 357 #[test] 358 fn cursor_roundtrip_preserves_canonical_version_and_full_width_key() { 359 for (timestamp, timestamp_hex) in [ 360 (0, "0000000000000000"), 361 (1, "0000000000000001"), 362 ((1_u64 << 63) - 1, "7fffffffffffffff"), 363 (1_u64 << 63, "8000000000000000"), 364 (u64::MAX, "ffffffffffffffff"), 365 ] { 366 for event_id in [[0; 32], [0xab; 32], [u8::MAX; 32]] { 367 let key = order(timestamp, event_id); 368 let expected = canonical_cursor(timestamp_hex, event_id); 369 let encoded = AvailabilityPageCursor::encode(base_fingerprint(), key); 370 assert_eq!(encoded.as_str().as_bytes(), expected.as_bytes()); 371 assert_eq!(encoded.as_str().len(), 151); 372 assert!(encoded.as_str().is_ascii()); 373 assert_eq!(encoded.after(), key); 374 let parsed = AvailabilityPageCursor::parse(&expected, base_fingerprint()) 375 .expect("independently expected canonical cursor"); 376 assert_eq!(parsed.as_str(), expected); 377 assert_eq!(parsed.after().created_at().as_u64(), timestamp); 378 assert_eq!(parsed.after().version().event_id().as_bytes(), &event_id); 379 } 380 } 381 } 382 383 #[test] 384 fn cursor_rejects_malformed_noncanonical_and_unknown_versions() { 385 let valid = canonical_cursor("ffffffffffffffff", [0xab; 32]); 386 let mut malformed = vec![ 387 String::new(), 388 valid[..150].to_owned(), 389 format!("{valid}x"), 390 valid.replacen("hcq1:", "hcq0:", 1), 391 valid.replacen("hcq1:", "hcq2:", 1), 392 valid.replacen("hcq1:", "HCQ1:", 1), 393 valid.replacen("hcq1:", "hcq01:", 1), 394 format!(" {valid}"), 395 format!("{valid}\n"), 396 valid.replacen(":ffffffffffffffff:", ":fffffffffffffff:", 1), 397 valid.replacen(":ffffffffffffffff:", ":0xffffffffffffffff:", 1), 398 ]; 399 for index in [4, 69, 86] { 400 let mut wrong_separator = valid.clone(); 401 wrong_separator.replace_range(index..index + 1, "-"); 402 malformed.push(wrong_separator); 403 } 404 for index in [5, 70, 87] { 405 for invalid in ["A", "g", " ", "\0"] { 406 let mut changed = valid.clone(); 407 changed.replace_range(index..index + 1, invalid); 408 assert_eq!(changed.len(), 151); 409 malformed.push(changed); 410 } 411 } 412 for (end, replacement) in [(7, "é"), (9, "🥕")] { 413 let mut non_ascii = valid.clone(); 414 non_ascii.replace_range(5..end, replacement); 415 assert_eq!(non_ascii.len(), 151); 416 malformed.push(non_ascii); 417 } 418 for text in malformed { 419 assert_eq!( 420 AvailabilityPageCursor::parse(&text, base_fingerprint()).expect_err("malformed cursor"), 421 AvailabilityQueryError::InvalidInput, 422 ); 423 } 424 let mut other_fingerprint = valid; 425 other_fingerprint.replace_range(5..6, "0"); 426 assert_eq!( 427 AvailabilityPageCursor::parse(&other_fingerprint, base_fingerprint()) 428 .expect_err("canonical different fingerprint"), 429 AvailabilityQueryError::StaleQuery, 430 ); 431 } 432 433 #[test] 434 fn cursor_rejects_overlong_input_before_retention() { 435 assert_eq!(AVAILABILITY_CURSOR_MAX_BYTES, 512_usize); 436 for count in [511, 512] { 437 for unit in ["x", "\0", "é", "🥕"] { 438 let text = text_at_byte_count(unit, count); 439 assert_eq!(text.len(), count); 440 assert_eq!( 441 AvailabilityPageCursor::parse(&text, base_fingerprint()) 442 .expect_err("bounded malformed cursor"), 443 AvailabilityQueryError::InvalidInput, 444 ); 445 } 446 } 447 let valid = canonical_cursor("ffffffffffffffff", [0xab; 32]); 448 let oversized_valid_prefix = format!("{valid}{}", "x".repeat(513 - valid.len())); 449 let mut wrong_fingerprint = valid.clone(); 450 wrong_fingerprint.replace_range(5..6, "0"); 451 let oversized_wrong_fingerprint = format!( 452 "{wrong_fingerprint}{}", 453 "x".repeat(513 - wrong_fingerprint.len()) 454 ); 455 let oversized_unknown_version = format!("hcq9:{}", "g".repeat(508)); 456 for text in [ 457 "x".repeat(513), 458 "\0".repeat(513), 459 text_at_byte_count("é", 513), 460 text_at_byte_count("🥕", 513), 461 oversized_valid_prefix, 462 oversized_wrong_fingerprint, 463 oversized_unknown_version, 464 "g".repeat(4_096), 465 ] { 466 assert!(text.len() > 512); 467 assert_eq!( 468 AvailabilityPageCursor::parse(&text, base_fingerprint()) 469 .expect_err("byte cap takes precedence over shape and fingerprint"), 470 AvailabilityQueryError::InputTooLarge, 471 ); 472 } 473 } 474 475 #[test] 476 fn fingerprint_binds_owner_context_and_store_identity() { 477 let baseline = base_fingerprint(); 478 assert_eq!(baseline, base_fingerprint()); 479 let mut fingerprints = vec![baseline]; 480 fingerprints.push(AvailabilityQueryFingerprint::new( 481 other_author(), 482 &base_context(), 483 3, 484 &base_filters(), 485 AvailabilityPageLimit::default(), 486 )); 487 for index in [0, 16, 31] { 488 let mut changed_context = [1; 32]; 489 changed_context[index] = 3; 490 let mut changed_store = [2; 32]; 491 changed_store[index] = 3; 492 for changed in [ 493 context(changed_context, [2; 32], 5, 7), 494 context([1; 32], changed_store, 5, 7), 495 ] { 496 fingerprints.push(AvailabilityQueryFingerprint::new( 497 author(), 498 &changed, 499 3, 500 &base_filters(), 501 AvailabilityPageLimit::default(), 502 )); 503 } 504 } 505 assert_distinct(&fingerprints); 506 } 507 508 #[test] 509 fn fingerprint_binds_source_projection_and_session_generations() { 510 let mut fingerprints = vec![base_fingerprint()]; 511 for value in [0, 1, 1_u64 << 63, u64::MAX] { 512 for (changed_context, session) in [ 513 (context([1; 32], [2; 32], value, 7), 3), 514 (context([1; 32], [2; 32], 5, value), 3), 515 (base_context(), value), 516 ] { 517 fingerprints.push(AvailabilityQueryFingerprint::new( 518 author(), 519 &changed_context, 520 session, 521 &base_filters(), 522 AvailabilityPageLimit::default(), 523 )); 524 } 525 } 526 for (source, projection, session) in [(1, 2, 3), (2, 1, 3), (1, 3, 2), (3, 2, 1)] { 527 fingerprints.push(AvailabilityQueryFingerprint::new( 528 author(), 529 &context([1; 32], [2; 32], source, projection), 530 session, 531 &base_filters(), 532 AvailabilityPageLimit::default(), 533 )); 534 } 535 assert_distinct(&fingerprints); 536 } 537 538 #[test] 539 fn fingerprint_binds_exact_filters_and_row_limit() { 540 assert_eq!(base_fingerprint().bytes(), &GOLDEN_FINGERPRINT_BYTES); 541 let mut fingerprints = vec![base_fingerprint()]; 542 let exact_ascii = "a".repeat(512); 543 let exact_unicode = "é".repeat(256); 544 for search in [ 545 None, 546 Some(""), 547 Some("A"), 548 Some("a "), 549 Some(" a"), 550 Some("é"), 551 Some("e\u{301}"), 552 Some("a\0"), 553 Some(exact_ascii.as_str()), 554 Some(exact_unicode.as_str()), 555 ] { 556 fingerprints.push(AvailabilityQueryFingerprint::new( 557 author(), 558 &base_context(), 559 3, 560 &filters(search, None, Some(FoodAvailabilityStatus::Active)), 561 AvailabilityPageLimit::default(), 562 )); 563 } 564 for publisher in [publisher(), other_publisher()] { 565 fingerprints.push(AvailabilityQueryFingerprint::new( 566 author(), 567 &base_context(), 568 3, 569 &filters( 570 Some("a"), 571 Some(publisher), 572 Some(FoodAvailabilityStatus::Active), 573 ), 574 AvailabilityPageLimit::default(), 575 )); 576 } 577 for status in [None, Some(FoodAvailabilityStatus::Sold)] { 578 fingerprints.push(AvailabilityQueryFingerprint::new( 579 author(), 580 &base_context(), 581 3, 582 &filters(Some("a"), None, status), 583 AvailabilityPageLimit::default(), 584 )); 585 } 586 for rows in 1..=100 { 587 if rows != 50 { 588 fingerprints.push(AvailabilityQueryFingerprint::new( 589 author(), 590 &base_context(), 591 3, 592 &base_filters(), 593 AvailabilityPageLimit::new(rows).expect("valid distinct row limit"), 594 )); 595 } 596 } 597 assert_distinct(&fingerprints); 598 } 599 600 #[test] 601 fn fingerprint_option_and_length_framing_prevent_ambiguous_queries() { 602 assert_eq!(base_fingerprint().bytes(), &GOLDEN_FINGERPRINT_BYTES); 603 let mut fingerprints = Vec::new(); 604 for search in [ 605 None, 606 Some(""), 607 Some("a"), 608 Some("a\0"), 609 Some("a\0\0"), 610 Some("active"), 611 Some("sold"), 612 Some("None"), 613 Some("0:1:32"), 614 Some(AUTHOR_HEX), 615 ] { 616 for publisher in [None, Some(publisher())] { 617 for status in [ 618 None, 619 Some(FoodAvailabilityStatus::Active), 620 Some(FoodAvailabilityStatus::Sold), 621 ] { 622 fingerprints.push(AvailabilityQueryFingerprint::new( 623 author(), 624 &base_context(), 625 3, 626 &filters(search, publisher, status), 627 AvailabilityPageLimit::default(), 628 )); 629 } 630 } 631 } 632 assert_distinct(&fingerprints); 633 let with_search = AvailabilityQueryFingerprint::new( 634 author(), 635 &base_context(), 636 3, 637 &filters(Some(AUTHOR_HEX), None, None), 638 AvailabilityPageLimit::default(), 639 ); 640 let with_publisher = AvailabilityQueryFingerprint::new( 641 author(), 642 &base_context(), 643 3, 644 &filters(Some(""), Some(publisher()), None), 645 AvailabilityPageLimit::default(), 646 ); 647 assert_ne!( 648 with_search, with_publisher, 649 "search bytes cannot become a publisher field" 650 ); 651 } 652 653 #[test] 654 fn scoped_query_uses_local_owner_without_signing_capability() { 655 for session in [0, 3, u64::MAX] { 656 let local = scope(author(), base_context(), session); 657 assert_eq!(local.owner(), author()); 658 assert_eq!(local.context().context_id(), &[1; 32]); 659 assert_eq!(local.context().store_generation(), &[2; 32]); 660 assert_eq!(local.context().source_revision(), 5); 661 assert_eq!(local.context().projection_generation(), 7); 662 assert_eq!( 663 local.session_generation(), 664 SessionGeneration::from_value(session) 665 ); 666 let query = ScopedAvailabilityQuery::new( 667 local, 668 base_filters(), 669 AvailabilityPageLimit::default(), 670 None, 671 ) 672 .expect("public owner and structural generations require no signing capability"); 673 assert_eq!(query.scope().owner(), author()); 674 assert_eq!(query.scope().session_generation().value(), session); 675 assert_eq!( 676 query.filters().search().expect("exact search").as_str(), 677 "a" 678 ); 679 assert_eq!(query.filters().publisher(), None); 680 assert_eq!( 681 query.filters().status(), 682 Some(FoodAvailabilityStatus::Active) 683 ); 684 assert_eq!(query.limit().rows(), 50); 685 assert!(query.cursor().is_none()); 686 assert_eq!( 687 query.validate_scope(&scope(author(), base_context(), session)), 688 Ok(()) 689 ); 690 } 691 assert_eq!( 692 base_query().fingerprint().bytes(), 693 &GOLDEN_FINGERPRINT_BYTES 694 ); 695 } 696 697 #[test] 698 fn scoped_query_refuses_owner_and_context_changes() { 699 let query = base_query(); 700 assert_eq!(query.validate_scope(&base_scope()), Ok(())); 701 assert_eq!( 702 query.validate_scope(&scope(other_author(), base_context(), 3)), 703 Err(AvailabilityQueryError::ScopeMismatch), 704 ); 705 for index in [0, 16, 31] { 706 let mut changed_context = [1; 32]; 707 changed_context[index] = 3; 708 let changed = scope(author(), context(changed_context, [2; 32], 5, 7), 3); 709 assert_eq!( 710 query.validate_scope(&changed), 711 Err(AvailabilityQueryError::ScopeMismatch) 712 ); 713 } 714 assert_eq!(query.validate_scope(&base_scope()), Ok(())); 715 assert_eq!(query.scope().owner(), author()); 716 assert_eq!(query.scope().context().context_id(), &[1; 32]); 717 } 718 719 #[test] 720 fn scoped_query_refuses_stale_session_store_source_and_projection() { 721 let query = base_query(); 722 for index in [0, 16, 31] { 723 let mut changed_store = [2; 32]; 724 changed_store[index] = 3; 725 let changed = scope(author(), context([1; 32], changed_store, 5, 7), 3); 726 assert_eq!( 727 query.validate_scope(&changed), 728 Err(AvailabilityQueryError::StaleQuery) 729 ); 730 } 731 for value in [0, 1, 1_u64 << 63, u64::MAX] { 732 for changed in [ 733 scope(author(), context([1; 32], [2; 32], value, 7), 3), 734 scope(author(), context([1; 32], [2; 32], 5, value), 3), 735 scope(author(), base_context(), value), 736 ] { 737 assert_eq!( 738 query.validate_scope(&changed), 739 Err(AvailabilityQueryError::StaleQuery) 740 ); 741 assert_eq!(query.validate_scope(&base_scope()), Ok(())); 742 } 743 } 744 let full_width = ScopedAvailabilityQuery::new( 745 scope( 746 author(), 747 context([1; 32], [2; 32], u64::MAX, u64::MAX), 748 u64::MAX, 749 ), 750 base_filters(), 751 AvailabilityPageLimit::default(), 752 None, 753 ) 754 .expect("full-width structural generations"); 755 assert_eq!( 756 full_width.validate_scope(&scope( 757 author(), 758 context([1; 32], [2; 32], u64::MAX, u64::MAX), 759 u64::MAX 760 )), 761 Ok(()), 762 ); 763 assert_eq!( 764 full_width.validate_scope(&scope( 765 author(), 766 context([1; 32], [2; 32], u64::MAX, u64::MAX), 767 (1_u64 << 63) - 1 768 )), 769 Err(AvailabilityQueryError::StaleQuery), 770 ); 771 assert_eq!(query.scope().context().store_generation(), &[2; 32]); 772 assert_eq!(query.scope().context().source_revision(), 5); 773 assert_eq!(query.scope().context().projection_generation(), 7); 774 assert_eq!(query.scope().session_generation().value(), 3); 775 } 776 777 #[test] 778 fn scoped_query_validates_cursor_against_complete_request() { 779 let cursor_text = canonical_cursor("ffffffffffffffff", [0xab; 32]); 780 let query = { 781 let borrowed_input = cursor_text.clone(); 782 ScopedAvailabilityQuery::new( 783 base_scope(), 784 base_filters(), 785 AvailabilityPageLimit::default(), 786 Some(&borrowed_input), 787 ) 788 .expect("valid borrowed cursor retained as a bounded owned value") 789 }; 790 let retained = query.cursor().expect("explicit continuation"); 791 assert_eq!(retained.as_str(), cursor_text); 792 assert_eq!(retained.after().created_at().as_u64(), u64::MAX); 793 assert_eq!( 794 retained.after().version().event_id().as_bytes(), 795 &[0xab; 32] 796 ); 797 assert_eq!(query.fingerprint().bytes(), &GOLDEN_FINGERPRINT_BYTES); 798 assert_eq!(query.validate_scope(&base_scope()), Ok(())); 799 800 let mut changed_requests = vec![( 801 scope(other_author(), base_context(), 3), 802 base_filters(), 803 AvailabilityPageLimit::default(), 804 )]; 805 for index in [0, 16, 31] { 806 let mut changed_context = [1; 32]; 807 changed_context[index] = 3; 808 let mut changed_store = [2; 32]; 809 changed_store[index] = 3; 810 for changed_scope in [ 811 scope(author(), context(changed_context, [2; 32], 5, 7), 3), 812 scope(author(), context([1; 32], changed_store, 5, 7), 3), 813 ] { 814 changed_requests.push(( 815 changed_scope, 816 base_filters(), 817 AvailabilityPageLimit::default(), 818 )); 819 } 820 } 821 for value in [0, 1_u64 << 63, u64::MAX] { 822 for changed_scope in [ 823 scope(author(), context([1; 32], [2; 32], value, 7), 3), 824 scope(author(), context([1; 32], [2; 32], 5, value), 3), 825 scope(author(), base_context(), value), 826 ] { 827 changed_requests.push(( 828 changed_scope, 829 base_filters(), 830 AvailabilityPageLimit::default(), 831 )); 832 } 833 } 834 for search in [ 835 None, 836 Some(""), 837 Some("A"), 838 Some("a "), 839 Some("é"), 840 Some("e\u{301}"), 841 Some("a\0"), 842 ] { 843 changed_requests.push(( 844 base_scope(), 845 filters(search, None, Some(FoodAvailabilityStatus::Active)), 846 AvailabilityPageLimit::default(), 847 )); 848 } 849 for changed_publisher in [publisher(), other_publisher()] { 850 changed_requests.push(( 851 base_scope(), 852 filters( 853 Some("a"), 854 Some(changed_publisher), 855 Some(FoodAvailabilityStatus::Active), 856 ), 857 AvailabilityPageLimit::default(), 858 )); 859 } 860 for status in [None, Some(FoodAvailabilityStatus::Sold)] { 861 changed_requests.push(( 862 base_scope(), 863 filters(Some("a"), None, status), 864 AvailabilityPageLimit::default(), 865 )); 866 } 867 for rows in [1, 49, 51, 100] { 868 changed_requests.push(( 869 base_scope(), 870 base_filters(), 871 AvailabilityPageLimit::new(rows).expect("changed bounded row limit"), 872 )); 873 } 874 assert!( 875 changed_requests.len() >= 20, 876 "complete independent scope/filter/limit mutations" 877 ); 878 for (changed_scope, changed_filters, changed_limit) in changed_requests { 879 assert_eq!( 880 ScopedAvailabilityQuery::new( 881 changed_scope, 882 changed_filters, 883 changed_limit, 884 Some(&cursor_text) 885 ) 886 .expect_err("cursor belongs to a different complete request"), 887 AvailabilityQueryError::StaleQuery, 888 ); 889 } 890 for invalid in ["", "hcq9:invalid", "HCAV_UNTRUSTED_CURSOR_MARKER"] { 891 assert_eq!( 892 ScopedAvailabilityQuery::new( 893 base_scope(), 894 base_filters(), 895 AvailabilityPageLimit::default(), 896 Some(invalid) 897 ) 898 .expect_err("malformed borrowed cursor"), 899 AvailabilityQueryError::InvalidInput, 900 ); 901 } 902 let oversized = "\0".repeat(513); 903 assert_eq!( 904 ScopedAvailabilityQuery::new( 905 base_scope(), 906 base_filters(), 907 AvailabilityPageLimit::default(), 908 Some(&oversized) 909 ) 910 .expect_err("oversized borrowed cursor"), 911 AvailabilityQueryError::InputTooLarge, 912 ); 913 assert_eq!(base_query().fingerprint(), query.fingerprint()); 914 assert!(base_query().cursor().is_none()); 915 } 916 917 #[test] 918 fn page_contract_enforces_rows_and_preserves_projection() { 919 // This type deliberately has neither Clone nor Debug: owned pages need neither. 920 struct Row(u16); 921 922 for (rows, limit) in [(0, 50), (50, 50), (100, 100), (1, 1)] { 923 let items: Vec<_> = (0..rows).map(Row).collect(); 924 let pointer = items.as_ptr(); 925 let capacity = items.capacity(); 926 let page = AvailabilityPage::new( 927 AvailabilityPageLimit::new(limit).expect("bounded row limit"), 928 items, 929 AvailabilityPageContinuation::End, 930 u64::MAX, 931 ) 932 .expect("owned row count within limit"); 933 assert_eq!(page.items().len(), usize::from(rows)); 934 assert_eq!(page.items().as_ptr(), pointer); 935 assert_eq!(page.projection_generation(), u64::MAX); 936 assert!(matches!( 937 page.continuation(), 938 AvailabilityPageContinuation::End 939 )); 940 assert_eq!( 941 page.items().iter().map(|row| row.0).collect::<Vec<_>>(), 942 (0..rows).collect::<Vec<_>>() 943 ); 944 let returned = page.into_items(); 945 assert_eq!(returned.as_ptr(), pointer); 946 assert_eq!(returned.capacity(), capacity); 947 assert_eq!(returned.len(), usize::from(rows)); 948 } 949 for limit in [1, 7, 50, 100] { 950 let items: Vec<_> = (0..=limit).map(Row).collect(); 951 assert_eq!( 952 AvailabilityPage::new( 953 AvailabilityPageLimit::new(limit).expect("bounded row limit"), 954 items, 955 AvailabilityPageContinuation::End, 956 7, 957 ) 958 .expect_err("configured limit plus one, including 101 rows"), 959 AvailabilityQueryError::Capacity, 960 ); 961 } 962 let query = base_query(); 963 assert_eq!( 964 query 965 .page(vec![0_u8; 50], None) 966 .expect("default full page") 967 .projection_generation(), 968 7 969 ); 970 assert_eq!( 971 query 972 .page(vec![0_u8; 51], None) 973 .expect_err("default row overflow"), 974 AvailabilityQueryError::Capacity 975 ); 976 for generation in [0, u64::MAX] { 977 let query = ScopedAvailabilityQuery::new( 978 scope(author(), context([1; 32], [2; 32], 5, generation), 3), 979 base_filters(), 980 AvailabilityPageLimit::new(100).expect("maximum limit"), 981 None, 982 ) 983 .expect("exact projection query"); 984 assert_eq!( 985 query 986 .page(vec![0_u8; 100], None) 987 .expect("maximum full page") 988 .projection_generation(), 989 generation 990 ); 991 assert_eq!( 992 query 993 .page(vec![0_u8; 101], None) 994 .expect_err("maximum overflow"), 995 AvailabilityQueryError::Capacity 996 ); 997 } 998 } 999 1000 #[test] 1001 fn local_page_end_and_continuation_remain_distinct() { 1002 let query = base_query(); 1003 let next = order(u64::MAX, [0xab; 32]); 1004 let ended = query 1005 .page(Vec::<u8>::new(), None) 1006 .expect("explicit local end"); 1007 assert!(ended.items().is_empty()); 1008 assert!(matches!( 1009 ended.continuation(), 1010 AvailabilityPageContinuation::End 1011 )); 1012 assert_eq!(ended.projection_generation(), 7); 1013 let continued = query 1014 .page(vec![42_u8], Some(next)) 1015 .expect("explicit local continuation"); 1016 assert_eq!(continued.items(), &[42]); 1017 assert_eq!(continued.projection_generation(), 7); 1018 let AvailabilityPageContinuation::More(cursor) = continued.continuation() else { 1019 panic!("explicit next position must retain a continuation"); 1020 }; 1021 assert_eq!( 1022 cursor.as_str(), 1023 canonical_cursor("ffffffffffffffff", [0xab; 32]) 1024 ); 1025 assert_eq!(cursor.after(), next); 1026 assert_eq!( 1027 AvailabilityPageCursor::parse(cursor.as_str(), query.fingerprint()) 1028 .expect("query-bound continuation") 1029 .after(), 1030 next 1031 ); 1032 let empty_continued = query 1033 .page(Vec::<u8>::new(), Some(next)) 1034 .expect("caller explicitly supplies continuation"); 1035 assert!(empty_continued.items().is_empty()); 1036 assert!(matches!( 1037 empty_continued.continuation(), 1038 AvailabilityPageContinuation::More(_) 1039 )); 1040 assert_eq!( 1041 empty_continued.projection_generation(), 1042 ended.projection_generation() 1043 ); 1044 assert!(format!("{:?}", ended.continuation()).contains("End")); 1045 assert!(format!("{:?}", continued.continuation()).contains("More")); 1046 } 1047 1048 #[test] 1049 fn query_errors_and_debug_never_echo_untrusted_payloads() { 1050 const SEARCH_MARKER: &str = "HCAV_UNTRUSTED_SEARCH_PAYLOAD_é\0%_"; 1051 const ITEM_MARKER: &str = "HCAV_UNTRUSTED_ITEM_PAYLOAD"; 1052 const CURSOR_MARKER: &str = "HCAV_UNTRUSTED_CURSOR_PAYLOAD"; 1053 let mut event_id = [0xde; 32]; 1054 event_id[..4].copy_from_slice(&[0xde, 0xad, 0xc0, 0xde]); 1055 let search = AvailabilitySearchText::new(SEARCH_MARKER).expect("bounded marker search"); 1056 let marked_filters = filters( 1057 Some(SEARCH_MARKER), 1058 Some(publisher()), 1059 Some(FoodAvailabilityStatus::Sold), 1060 ); 1061 let marked_fingerprint = AvailabilityQueryFingerprint::new( 1062 author(), 1063 &base_context(), 1064 3, 1065 &marked_filters, 1066 AvailabilityPageLimit::default(), 1067 ); 1068 let cursor = AvailabilityPageCursor::encode(marked_fingerprint, order(u64::MAX, event_id)); 1069 let cursor_text = cursor.as_str().to_owned(); 1070 let scope_debug = format!("{:?}", base_scope()); 1071 let query = ScopedAvailabilityQuery::new( 1072 base_scope(), 1073 marked_filters, 1074 AvailabilityPageLimit::default(), 1075 Some(&cursor_text), 1076 ) 1077 .expect("valid query carrying untrusted bounded search and public continuation"); 1078 let search_debug = format!("{search:?}"); 1079 assert!(search_debug.contains(&SEARCH_MARKER.len().to_string())); 1080 let cursor_debug = format!("{cursor:?}"); 1081 assert!(cursor_debug.contains("151")); 1082 let mut debug_values = vec![ 1083 search_debug, 1084 format!("{:?}", query.filters()), 1085 cursor_debug, 1086 scope_debug, 1087 format!("{query:?}"), 1088 format!("{:?}", AvailabilityPageContinuation::End), 1089 ]; 1090 for next in [None, Some(order(u64::MAX, event_id))] { 1091 let page = query 1092 .page(vec![ITEM_MARKER.to_owned()], next) 1093 .expect("bounded marker page"); 1094 debug_values.push(format!("{page:?}")); 1095 debug_values.push(format!("{:?}", page.continuation())); 1096 } 1097 let event_id_hex = hex(&event_id); 1098 let event_id_debug = format!("{event_id:?}"); 1099 let fingerprint_hex = hex(marked_fingerprint.bytes()); 1100 let fingerprint_debug = format!("{:?}", marked_fingerprint.bytes()); 1101 let context_hex = hex(&[1; 32]); 1102 let store_hex = hex(&[2; 32]); 1103 let context_debug = format!("{:?}", [1_u8; 32]); 1104 let store_debug = format!("{:?}", [2_u8; 32]); 1105 for text in debug_values { 1106 for forbidden in [ 1107 "HCAV_UNTRUSTED_SEARCH_PAYLOAD_", 1108 SEARCH_MARKER, 1109 ITEM_MARKER, 1110 CURSOR_MARKER, 1111 cursor_text.as_str(), 1112 event_id_hex.as_str(), 1113 event_id_debug.as_str(), 1114 fingerprint_hex.as_str(), 1115 fingerprint_debug.as_str(), 1116 context_hex.as_str(), 1117 store_hex.as_str(), 1118 context_debug.as_str(), 1119 store_debug.as_str(), 1120 ] { 1121 assert!( 1122 !text.contains(forbidden), 1123 "diagnostics must omit untrusted payload and opaque identities" 1124 ); 1125 } 1126 } 1127 let invalid_cursor = ScopedAvailabilityQuery::new( 1128 base_scope(), 1129 base_filters(), 1130 AvailabilityPageLimit::default(), 1131 Some(CURSOR_MARKER), 1132 ) 1133 .expect_err("untrusted malformed cursor"); 1134 let invalid_search = AvailabilitySearchText::new(&SEARCH_MARKER.repeat(32)) 1135 .expect_err("oversized untrusted search"); 1136 assert_eq!(invalid_cursor, AvailabilityQueryError::InvalidInput); 1137 assert_eq!(invalid_search, AvailabilityQueryError::InputTooLarge); 1138 for (error, name) in [ 1139 (AvailabilityQueryError::InvalidInput, "InvalidInput"), 1140 (AvailabilityQueryError::InputTooLarge, "InputTooLarge"), 1141 (AvailabilityQueryError::ScopeMismatch, "ScopeMismatch"), 1142 (AvailabilityQueryError::StaleQuery, "StaleQuery"), 1143 (AvailabilityQueryError::Capacity, "Capacity"), 1144 ] { 1145 let copied = error; 1146 assert_eq!(copied, error); 1147 let standard_error: &dyn Error = &error; 1148 assert!(standard_error.source().is_none()); 1149 assert_eq!(format!("{error:?}"), name); 1150 let display = error.to_string(); 1151 assert!(!display.is_empty()); 1152 assert_eq!(display, copied.to_string()); 1153 for forbidden in [ 1154 "HCAV_UNTRUSTED_SEARCH_PAYLOAD_", 1155 SEARCH_MARKER, 1156 ITEM_MARKER, 1157 CURSOR_MARKER, 1158 cursor_text.as_str(), 1159 ] { 1160 assert!(!format!("{error:?} {error}").contains(forbidden)); 1161 } 1162 } 1163 for error in [invalid_cursor, invalid_search] { 1164 assert!(!format!("{error:?} {error}").contains("HCAV_UNTRUSTED_SEARCH_PAYLOAD_")); 1165 assert!(!format!("{error:?} {error}").contains(SEARCH_MARKER)); 1166 assert!(!format!("{error:?} {error}").contains(CURSOR_MARKER)); 1167 } 1168 }