app

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

query.rs (16978B)


      1 //! Bounded local availability query values and public snapshot continuations.
      2 //!
      3 //! These values retain structural bindings supplied by a caller. They prove no
      4 //! installed-account admission, event signature, physical inventory, network
      5 //! completeness or current database state. The storage adapter must supply its
      6 //! actual validated store identity, source revision and projection snapshot.
      7 
      8 use std::cmp::Ordering;
      9 use std::fmt;
     10 
     11 pub use radroots_event::envelope::EventTimestamp;
     12 pub use radroots_event::food::availability::FoodAvailabilityStatus;
     13 use sha2::{Digest, Sha256};
     14 
     15 use crate::PublicKey;
     16 
     17 use super::{AvailabilityEventVersion, PublicPublisher};
     18 
     19 pub const AVAILABILITY_PAGE_DEFAULT_ROWS: u16 = 50;
     20 pub const AVAILABILITY_PAGE_MAX_ROWS: u16 = 100;
     21 pub const AVAILABILITY_QUERY_TEXT_MAX_BYTES: usize = 512;
     22 pub const AVAILABILITY_CURSOR_MAX_BYTES: usize = 512;
     23 
     24 const QUERY_FINGERPRINT_DOMAIN: &[u8] = b"harvestcircle.availability.query.v1\0";
     25 const CURSOR_ENCODED_BYTES: usize = 151;
     26 const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
     27 
     28 /// Static query-contract failures without untrusted input or dependent errors.
     29 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     30 pub enum AvailabilityQueryError {
     31     InvalidInput,
     32     InputTooLarge,
     33     ScopeMismatch,
     34     StaleQuery,
     35     Capacity,
     36 }
     37 
     38 impl fmt::Display for AvailabilityQueryError {
     39     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     40         formatter.write_str(match self {
     41             Self::InvalidInput => "The availability query input is invalid.",
     42             Self::InputTooLarge => "The availability query input exceeds its byte limit.",
     43             Self::ScopeMismatch => "The availability query scope does not match.",
     44             Self::StaleQuery => "The availability query is stale.",
     45             Self::Capacity => "The availability page exceeds its row limit.",
     46         })
     47     }
     48 }
     49 
     50 impl std::error::Error for AvailabilityQueryError {}
     51 
     52 /// A positive row limit bounded by the local page contract.
     53 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     54 pub struct AvailabilityPageLimit(u16);
     55 
     56 impl AvailabilityPageLimit {
     57     /// Admits exactly one through one hundred rows.
     58     ///
     59     /// # Errors
     60     ///
     61     /// Returns `InvalidInput` for zero or a limit above the maximum.
     62     pub const fn new(rows: u16) -> Result<Self, AvailabilityQueryError> {
     63         if rows == 0 || rows > AVAILABILITY_PAGE_MAX_ROWS {
     64             return Err(AvailabilityQueryError::InvalidInput);
     65         }
     66         Ok(Self(rows))
     67     }
     68 
     69     #[must_use]
     70     pub const fn rows(self) -> u16 {
     71         self.0
     72     }
     73 }
     74 
     75 impl Default for AvailabilityPageLimit {
     76     fn default() -> Self {
     77         Self(AVAILABILITY_PAGE_DEFAULT_ROWS)
     78     }
     79 }
     80 
     81 /// Exact opaque cached search text, including a present empty value.
     82 #[derive(Clone, Eq, PartialEq)]
     83 pub struct AvailabilitySearchText(String);
     84 
     85 impl AvailabilitySearchText {
     86     /// Retains exact UTF-8 bytes after checking their bound before copying.
     87     ///
     88     /// This applies no trimming, normalization, location policy or SQL wildcard
     89     /// interpretation and initiates no remote search.
     90     ///
     91     /// # Errors
     92     ///
     93     /// Returns `InputTooLarge` when the borrowed input exceeds 512 bytes.
     94     pub fn new(value: &str) -> Result<Self, AvailabilityQueryError> {
     95         if value.len() > AVAILABILITY_QUERY_TEXT_MAX_BYTES {
     96             return Err(AvailabilityQueryError::InputTooLarge);
     97         }
     98         Ok(Self(value.to_owned()))
     99     }
    100 
    101     #[must_use]
    102     pub fn as_str(&self) -> &str {
    103         &self.0
    104     }
    105 }
    106 
    107 impl fmt::Debug for AvailabilitySearchText {
    108     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    109         formatter
    110             .debug_struct("AvailabilitySearchText")
    111             .field("bytes", &self.0.len())
    112             .finish()
    113     }
    114 }
    115 
    116 /// Immutable optional local filters using the selected shared status enum.
    117 #[derive(Clone, Eq, PartialEq)]
    118 pub struct AvailabilityQueryFilters {
    119     search: Option<AvailabilitySearchText>,
    120     publisher: Option<PublicPublisher>,
    121     status: Option<FoodAvailabilityStatus>,
    122 }
    123 
    124 impl AvailabilityQueryFilters {
    125     #[must_use]
    126     pub fn new(
    127         search: Option<AvailabilitySearchText>,
    128         publisher: Option<PublicPublisher>,
    129         status: Option<FoodAvailabilityStatus>,
    130     ) -> Self {
    131         Self {
    132             search,
    133             publisher,
    134             status,
    135         }
    136     }
    137 
    138     #[must_use]
    139     pub const fn search(&self) -> Option<&AvailabilitySearchText> {
    140         self.search.as_ref()
    141     }
    142 
    143     #[must_use]
    144     pub const fn publisher(&self) -> Option<PublicPublisher> {
    145         self.publisher
    146     }
    147 
    148     #[must_use]
    149     pub const fn status(&self) -> Option<FoodAvailabilityStatus> {
    150         self.status
    151     }
    152 }
    153 
    154 impl fmt::Debug for AvailabilityQueryFilters {
    155     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    156         formatter
    157             .debug_struct("AvailabilityQueryFilters")
    158             .field(
    159                 "search_bytes",
    160                 &self.search().map(|value| value.as_str().len()),
    161             )
    162             .field("publisher_present", &self.publisher.is_some())
    163             .field("status", &self.status)
    164             .finish()
    165     }
    166 }
    167 
    168 /// Caller-supplied selected-context and local snapshot bindings.
    169 ///
    170 /// Nonzero opaque identities are structural values, not proof that a context
    171 /// or store is installed, admitted, current or validated by this module.
    172 #[derive(Clone, Copy, Eq, PartialEq)]
    173 pub struct AvailabilityQueryContext {
    174     context_id: [u8; 32],
    175     store_generation: [u8; 32],
    176     source_revision: u64,
    177     projection_generation: u64,
    178 }
    179 
    180 impl AvailabilityQueryContext {
    181     /// Retains exact identities and full-width revisions and generations.
    182     ///
    183     /// # Errors
    184     ///
    185     /// Returns `InvalidInput` if either opaque identity is all zero.
    186     pub fn new(
    187         context_id: [u8; 32],
    188         store_generation: [u8; 32],
    189         source_revision: u64,
    190         projection_generation: u64,
    191     ) -> Result<Self, AvailabilityQueryError> {
    192         if context_id == [0; 32] || store_generation == [0; 32] {
    193             return Err(AvailabilityQueryError::InvalidInput);
    194         }
    195         Ok(Self {
    196             context_id,
    197             store_generation,
    198             source_revision,
    199             projection_generation,
    200         })
    201     }
    202 
    203     #[must_use]
    204     pub const fn context_id(&self) -> &[u8; 32] {
    205         &self.context_id
    206     }
    207 
    208     #[must_use]
    209     pub const fn store_generation(&self) -> &[u8; 32] {
    210         &self.store_generation
    211     }
    212 
    213     #[must_use]
    214     pub const fn source_revision(&self) -> u64 {
    215         self.source_revision
    216     }
    217 
    218     #[must_use]
    219     pub const fn projection_generation(&self) -> u64 {
    220         self.projection_generation
    221     }
    222 }
    223 
    224 impl fmt::Debug for AvailabilityQueryContext {
    225     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    226         formatter
    227             .debug_struct("AvailabilityQueryContext")
    228             .field("source_revision", &self.source_revision)
    229             .field("projection_generation", &self.projection_generation)
    230             .finish_non_exhaustive()
    231     }
    232 }
    233 
    234 /// A page position ordered by descending signed event time and ascending ID.
    235 ///
    236 /// Signed event time retains the shared timestamp's complete `u64` range.
    237 /// Lower `Ord` values come earlier in the page; this selects no event head.
    238 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    239 pub struct AvailabilityOrderKey {
    240     created_at: EventTimestamp,
    241     version: AvailabilityEventVersion,
    242 }
    243 
    244 impl AvailabilityOrderKey {
    245     #[must_use]
    246     pub const fn new(created_at: EventTimestamp, version: AvailabilityEventVersion) -> Self {
    247         Self {
    248             created_at,
    249             version,
    250         }
    251     }
    252 
    253     #[must_use]
    254     pub const fn created_at(self) -> EventTimestamp {
    255         self.created_at
    256     }
    257 
    258     #[must_use]
    259     pub const fn version(self) -> AvailabilityEventVersion {
    260         self.version
    261     }
    262 
    263     #[must_use]
    264     pub fn is_after(self, previous: Self) -> bool {
    265         self > previous
    266     }
    267 }
    268 
    269 impl Ord for AvailabilityOrderKey {
    270     fn cmp(&self, other: &Self) -> Ordering {
    271         other.created_at.cmp(&self.created_at).then_with(|| {
    272             self.version
    273                 .event_id()
    274                 .as_bytes()
    275                 .cmp(other.version.event_id().as_bytes())
    276         })
    277     }
    278 }
    279 
    280 impl PartialOrd for AvailabilityOrderKey {
    281     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
    282         Some(self.cmp(other))
    283     }
    284 }
    285 
    286 /// A versioned public request digest, with no signature or authorization claim.
    287 #[derive(Clone, Copy, Eq, PartialEq)]
    288 pub struct AvailabilityQueryFingerprint([u8; 32]);
    289 
    290 impl AvailabilityQueryFingerprint {
    291     /// Streams the fixed v1 frame through standard SHA-256.
    292     ///
    293     /// The frame binds full owner/context/store bytes, big-endian counters,
    294     /// exact option discriminants and search byte length, shared status and
    295     /// row limit. No secret, host entropy or intermediate request string exists.
    296     #[must_use]
    297     pub fn new(
    298         owner: PublicKey,
    299         context: &AvailabilityQueryContext,
    300         session_generation: u64,
    301         filters: &AvailabilityQueryFilters,
    302         limit: AvailabilityPageLimit,
    303     ) -> Self {
    304         let mut digest = Sha256::new();
    305         digest.update(QUERY_FINGERPRINT_DOMAIN);
    306         digest.update(owner.as_bytes());
    307         digest.update(context.context_id());
    308         digest.update(context.store_generation());
    309         digest.update(context.source_revision().to_be_bytes());
    310         digest.update(context.projection_generation().to_be_bytes());
    311         digest.update(session_generation.to_be_bytes());
    312         match filters.search() {
    313             None => digest.update([0]),
    314             Some(search) => {
    315                 digest.update([1]);
    316                 // The validated text type bounds this length to at most 512.
    317                 digest.update((search.as_str().len() as u64).to_be_bytes());
    318                 digest.update(search.as_str().as_bytes());
    319             }
    320         }
    321         match filters.publisher() {
    322             None => digest.update([0]),
    323             Some(publisher) => {
    324                 digest.update([1]);
    325                 digest.update(publisher.public_key().as_bytes());
    326             }
    327         }
    328         digest.update([match filters.status() {
    329             None => 0,
    330             Some(FoodAvailabilityStatus::Active) => 1,
    331             Some(FoodAvailabilityStatus::Sold) => 2,
    332         }]);
    333         digest.update(limit.rows().to_be_bytes());
    334         Self(digest.finalize().into())
    335     }
    336 
    337     #[must_use]
    338     pub const fn bytes(&self) -> &[u8; 32] {
    339         &self.0
    340     }
    341 }
    342 
    343 impl fmt::Debug for AvailabilityQueryFingerprint {
    344     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    345         formatter
    346             .debug_struct("AvailabilityQueryFingerprint")
    347             .finish_non_exhaustive()
    348     }
    349 }
    350 
    351 /// Canonical public continuation for one exact structural query snapshot.
    352 ///
    353 /// A cursor is forgeable by design. Its fingerprint is correspondence, not a
    354 /// permission token; a future consumer must separately admit the local scope.
    355 #[derive(Clone, Eq, PartialEq)]
    356 pub struct AvailabilityPageCursor {
    357     value: String,
    358     after: AvailabilityOrderKey,
    359 }
    360 
    361 impl AvailabilityPageCursor {
    362     #[must_use]
    363     pub fn encode(fingerprint: AvailabilityQueryFingerprint, after: AvailabilityOrderKey) -> Self {
    364         let mut value = String::with_capacity(CURSOR_ENCODED_BYTES);
    365         value.push_str("hcq1:");
    366         append_hex(&mut value, fingerprint.bytes());
    367         value.push(':');
    368         append_hex(&mut value, &after.created_at().as_u64().to_be_bytes());
    369         value.push(':');
    370         append_hex(&mut value, after.version().event_id().as_bytes());
    371         Self { value, after }
    372     }
    373 
    374     /// Validates the complete borrowed cursor before allocating its owned text.
    375     ///
    376     /// # Errors
    377     ///
    378     /// Returns `InputTooLarge` first for input above 512 bytes, `InvalidInput`
    379     /// for noncanonical version/framing/hex, or `StaleQuery` for a different
    380     /// complete request fingerprint.
    381     pub fn parse(
    382         value: &str,
    383         expected: AvailabilityQueryFingerprint,
    384     ) -> Result<Self, AvailabilityQueryError> {
    385         if value.len() > AVAILABILITY_CURSOR_MAX_BYTES {
    386             return Err(AvailabilityQueryError::InputTooLarge);
    387         }
    388         if value.len() != CURSOR_ENCODED_BYTES || !value.is_ascii() {
    389             return Err(AvailabilityQueryError::InvalidInput);
    390         }
    391         let bytes = value.as_bytes();
    392         if &bytes[..5] != b"hcq1:" || bytes[69] != b':' || bytes[86] != b':' {
    393             return Err(AvailabilityQueryError::InvalidInput);
    394         }
    395         let fingerprint = decode_hex::<32>(&bytes[5..69])?;
    396         let timestamp = u64::from_be_bytes(decode_hex::<8>(&bytes[70..86])?);
    397         let event_id = decode_hex::<32>(&bytes[87..151])?;
    398         if &fingerprint != expected.bytes() {
    399             return Err(AvailabilityQueryError::StaleQuery);
    400         }
    401         let after = AvailabilityOrderKey::new(
    402             EventTimestamp::new(timestamp),
    403             AvailabilityEventVersion::from_canonical(radroots_event::EventId::from_bytes(event_id)),
    404         );
    405         Ok(Self {
    406             value: value.to_owned(),
    407             after,
    408         })
    409     }
    410 
    411     #[must_use]
    412     pub fn as_str(&self) -> &str {
    413         &self.value
    414     }
    415 
    416     #[must_use]
    417     pub const fn after(&self) -> AvailabilityOrderKey {
    418         self.after
    419     }
    420 }
    421 
    422 impl fmt::Debug for AvailabilityPageCursor {
    423     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    424         formatter
    425             .debug_struct("AvailabilityPageCursor")
    426             .field("bytes", &self.value.len())
    427             .finish()
    428     }
    429 }
    430 
    431 fn append_hex(value: &mut String, bytes: &[u8]) {
    432     for byte in bytes {
    433         value.push(char::from(HEX_DIGITS[usize::from(byte >> 4)]));
    434         value.push(char::from(HEX_DIGITS[usize::from(byte & 0x0f)]));
    435     }
    436 }
    437 
    438 fn decode_hex<const N: usize>(input: &[u8]) -> Result<[u8; N], AvailabilityQueryError> {
    439     if input.len() != N * 2 {
    440         return Err(AvailabilityQueryError::InvalidInput);
    441     }
    442     let mut result = [0; N];
    443     for (output, pair) in result.iter_mut().zip(input.chunks_exact(2)) {
    444         *output = (hex_digit(pair[0])? << 4) | hex_digit(pair[1])?;
    445     }
    446     Ok(result)
    447 }
    448 
    449 const fn hex_digit(byte: u8) -> Result<u8, AvailabilityQueryError> {
    450     match byte {
    451         b'0'..=b'9' => Ok(byte - b'0'),
    452         b'a'..=b'f' => Ok(byte - b'a' + 10),
    453         _ => Err(AvailabilityQueryError::InvalidInput),
    454     }
    455 }
    456 
    457 /// End or continuation of this local query snapshot only.
    458 ///
    459 /// `End` proves no exhaustive network/deletion knowledge, fresh inventory or
    460 /// absence of food outside the rows supplied by the local query adapter.
    461 #[derive(Clone, Eq, PartialEq)]
    462 pub enum AvailabilityPageContinuation {
    463     End,
    464     More(AvailabilityPageCursor),
    465 }
    466 
    467 impl fmt::Debug for AvailabilityPageContinuation {
    468     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    469         formatter.write_str(match self {
    470             Self::End => "End",
    471             Self::More(_) => "More",
    472         })
    473     }
    474 }
    475 
    476 /// A bounded-row owned result without a database or serialization-cap claim.
    477 ///
    478 /// The storage adapter remains responsible for bounded scanning, actual
    479 /// snapshot selection, response bytes and deadlines.
    480 pub struct AvailabilityPage<T> {
    481     items: Vec<T>,
    482     continuation: AvailabilityPageContinuation,
    483     projection_generation: u64,
    484 }
    485 
    486 impl<T> AvailabilityPage<T> {
    487     /// Moves the supplied vector after checking its configured row bound.
    488     ///
    489     /// No item is cloned and no additional vector allocation is made.
    490     ///
    491     /// # Errors
    492     ///
    493     /// Returns `Capacity` when the owned row count exceeds the limit.
    494     pub fn new(
    495         limit: AvailabilityPageLimit,
    496         items: Vec<T>,
    497         continuation: AvailabilityPageContinuation,
    498         projection_generation: u64,
    499     ) -> Result<Self, AvailabilityQueryError> {
    500         if items.len() > usize::from(limit.rows()) {
    501             return Err(AvailabilityQueryError::Capacity);
    502         }
    503         Ok(Self {
    504             items,
    505             continuation,
    506             projection_generation,
    507         })
    508     }
    509 
    510     #[must_use]
    511     pub fn items(&self) -> &[T] {
    512         &self.items
    513     }
    514 
    515     #[must_use]
    516     pub const fn continuation(&self) -> &AvailabilityPageContinuation {
    517         &self.continuation
    518     }
    519 
    520     #[must_use]
    521     pub const fn projection_generation(&self) -> u64 {
    522         self.projection_generation
    523     }
    524 
    525     #[must_use]
    526     pub fn into_items(self) -> Vec<T> {
    527         self.items
    528     }
    529 }
    530 
    531 impl<T> fmt::Debug for AvailabilityPage<T> {
    532     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    533         formatter
    534             .debug_struct("AvailabilityPage")
    535             .field("item_count", &self.items.len())
    536             .field("projection_generation", &self.projection_generation)
    537             .field("continuation", &self.continuation)
    538             .finish()
    539     }
    540 }