Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

fix prefix bounds: include segment-terminating `.`

avoids all the null-terminating problems *and* avoids panics over segments with longer names that match the last prefix segment as a ... prefix.

or whatever

Changed files
+12 -18
ufos
+6
ufos/src/lib.rs
··· 303 303 #[derive(Debug, Serialize, JsonSchema)] 304 304 pub struct NsidPrefix(String); 305 305 impl NsidPrefix { 306 + /// Input must not include a trailing dot. 306 307 pub fn new(pre: &str) -> EncodingResult<Self> { 307 308 // it's a valid prefix if appending `.name` makes it a valid NSID 308 309 Nsid::new(format!("{pre}.name")).map_err(EncodingError::BadAtriumStringType)?; ··· 319 320 ); 320 321 self.0 == other.domain_authority() 321 322 } 323 + /// The prefix as initialized (no trailing dot) 322 324 pub fn as_str(&self) -> &str { 323 325 self.0.as_str() 326 + } 327 + /// The prefix with a trailing `.` appended to avoid matching a longer segment 328 + pub fn terminated(&self) -> String { 329 + format!("{}.", self.0) 324 330 } 325 331 } 326 332
+6 -18
ufos/src/storage_fjall.rs
··· 665 665 cursor: Option<Vec<u8>>, 666 666 buckets: Vec<CursorBucket>, 667 667 ) -> StorageResult<(JustCount, Vec<PrefixChild>, Option<Vec<u8>>)> { 668 - // TODO: fix up this mess 669 - // the lower/start bound is tricky because `Exclusive()` _without_ a null terminator _will_ include the null- 670 - // terminated exact match. so we actually need the null terminator for an exclusive lower bound! 671 - // 672 - // but for upper/end bound, we *cannot* have a null terminator after the prefix, else we'll only get everything up 673 - // until prefix+null (aka nothing). 674 - // anywayyyyyyyy 675 - let prefix_sub_with_null = prefix.as_str().to_string().to_db_bytes()?; 676 - let prefix_sub = String::sub_prefix(prefix.as_str())?; 668 + // let prefix_sub_with_null = prefix.as_str().to_string().to_db_bytes()?; 669 + let prefix_sub = String::sub_prefix(&prefix.terminated())?; // with trailing dot to ensure full segment match 677 670 let cursor_child = cursor 678 671 .as_deref() 679 672 .map(|encoded_bytes| { 680 673 let decoded: String = db_complete(encoded_bytes)?; 674 + // TODO: write some tests for cursors, there's probably bugs here 681 675 let as_sub_prefix_with_null = decoded.to_db_bytes()?; 682 676 Ok::<_, EncodingError>(as_sub_prefix_with_null) 683 677 }) ··· 689 683 let start = cursor_child 690 684 .as_ref() 691 685 .map(|child| HourlyRollupKey::after_nsid_prefix(*t, child)) 692 - .unwrap_or_else(|| { 693 - HourlyRollupKey::after_nsid_prefix(*t, &prefix_sub_with_null) 694 - })?; 686 + .unwrap_or_else(|| HourlyRollupKey::after_nsid_prefix(*t, &prefix_sub))?; 695 687 let end = HourlyRollupKey::nsid_prefix_end(*t, &prefix_sub)?; 696 688 get_lexi_iter::<HourlyRollupKey>(&snapshot, start, end)? 697 689 } ··· 699 691 let start = cursor_child 700 692 .as_ref() 701 693 .map(|child| WeeklyRollupKey::after_nsid_prefix(*t, child)) 702 - .unwrap_or_else(|| { 703 - WeeklyRollupKey::after_nsid_prefix(*t, &prefix_sub_with_null) 704 - })?; 694 + .unwrap_or_else(|| WeeklyRollupKey::after_nsid_prefix(*t, &prefix_sub))?; 705 695 let end = WeeklyRollupKey::nsid_prefix_end(*t, &prefix_sub)?; 706 696 get_lexi_iter::<WeeklyRollupKey>(&snapshot, start, end)? 707 697 } ··· 709 699 let start = cursor_child 710 700 .as_ref() 711 701 .map(|child| AllTimeRollupKey::after_nsid_prefix(child)) 712 - .unwrap_or_else(|| { 713 - AllTimeRollupKey::after_nsid_prefix(&prefix_sub_with_null) 714 - })?; 702 + .unwrap_or_else(|| AllTimeRollupKey::after_nsid_prefix(&prefix_sub))?; 715 703 let end = AllTimeRollupKey::nsid_prefix_end(&prefix_sub)?; 716 704 get_lexi_iter::<AllTimeRollupKey>(&snapshot, start, end)? 717 705 }