app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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 }