1use core::fmt;
2use std::cmp::{Ordering, min};
3use std::collections::{HashMap, HashSet};
4use std::marker::PhantomData;
5use std::str::FromStr;
6
7use serde::{Deserialize, Serialize};
8use serde_repr::{Deserialize_repr, Serialize_repr};
9use thiserror::Error;
10use xet_core_structures::merklehash::{MerkleHash, MerkleHashSubtree};
11
12mod key;
13pub use key::*;
14
15pub const SESSION_ID_HEADER: &str = "X-Xet-Session-Id";
18pub const REQUEST_ID_HEADER: &str = "X-Request-Id";
20
21#[derive(Debug, Serialize, Deserialize, Clone)]
22pub struct UploadXorbResponse {
23 pub was_inserted: bool,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Copy)]
29pub struct _C;
30#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Copy)]
31pub struct _F;
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Copy)]
33pub struct _H;
34
35pub type ChunkRange = Range<u32, _C>;
37pub type FileRange = Range<u64, _F>;
39pub type HttpRange = Range<u64, _H>;
41
42impl FileRange {
43 pub fn full() -> Self {
44 Self::new(0, u64::MAX)
45 }
46
47 pub fn take_segment(self, segment_size: u64) -> (Self, Option<Self>) {
50 let segment = FileRange {
51 start: self.start,
52 end: min(self.end, self.start + segment_size),
53 _marker: PhantomData,
54 };
55
56 let remainder = if segment.end == self.end {
57 None
58 } else {
59 Some(FileRange {
60 start: segment.end,
61 end: self.end,
62 _marker: PhantomData,
63 })
64 };
65
66 (segment, remainder)
67 }
68
69 pub fn length(&self) -> u64 {
70 self.end - self.start
71 }
72}
73
74impl From<HttpRange> for FileRange {
75 fn from(value: HttpRange) -> Self {
76 FileRange::new(value.start, value.end + 1)
78 }
79}
80
81impl HttpRange {
82 pub fn range_header(&self) -> String {
83 format!("bytes={self}")
84 }
85
86 pub fn length(&self) -> u64 {
87 self.end - self.start + 1
88 }
89}
90
91impl From<FileRange> for HttpRange {
92 fn from(value: FileRange) -> Self {
93 HttpRange::new(value.start, value.end - 1)
95 }
96}
97
98#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, PartialOrd, Ord, Default, Hash)]
100pub struct Range<Idx, Kind> {
101 pub start: Idx,
102 pub end: Idx,
103 #[serde(skip)]
104 pub _marker: PhantomData<Kind>,
105}
106
107impl<Idx, _C> fmt::Debug for Range<Idx, _C>
108where
109 Idx: fmt::Debug,
110{
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.debug_struct("Range")
113 .field("start", &self.start)
114 .field("end", &self.end)
115 .finish()
116 }
117}
118
119impl<Idx, Kind> Range<Idx, Kind> {
120 pub fn new(start: Idx, end: Idx) -> Self {
121 Self {
122 start,
123 end,
124 _marker: PhantomData,
125 }
126 }
127}
128
129impl<T: Copy, Kind: Copy> Copy for Range<T, Kind> {}
130
131impl<Idx: fmt::Display, Kind> fmt::Display for Range<Idx, Kind> {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(f, "{}-{}", self.start, self.end)
134 }
135}
136
137#[derive(Error, Debug)]
138pub enum RangeParseError<Idx: std::str::FromStr> {
139 #[error("Invalid format, expect [start]-[end]")]
140 InvalidFormat,
141 #[error("Incorrect number: {0}")]
142 ParseError(Idx::Err),
143}
144
145impl<Idx: FromStr, Kind> TryFrom<&str> for Range<Idx, Kind> {
146 type Error = RangeParseError<Idx>;
147
148 fn try_from(value: &str) -> Result<Self, Self::Error> {
149 let parts: Vec<&str> = value.splitn(2, '-').collect();
150
151 if parts.len() != 2 {
152 return Err(RangeParseError::InvalidFormat);
153 }
154
155 let start = parts[0].parse::<Idx>().map_err(RangeParseError::ParseError)?;
156 let end = parts[1].parse::<Idx>().map_err(RangeParseError::ParseError)?;
157
158 Ok(Range {
159 start,
160 end,
161 _marker: PhantomData,
162 })
163 }
164}
165
166impl<Idx: FromStr, Kind> FromStr for Range<Idx, Kind> {
167 type Err = RangeParseError<Idx>;
168
169 fn from_str(value: &str) -> Result<Self, Self::Err> {
170 Self::try_from(value)
171 }
172}
173
174#[derive(Debug, Serialize, Deserialize, Clone)]
180pub struct XorbReconstructionTerm {
181 pub hash: HexMerkleHash,
182 pub unpacked_length: u32,
185 pub range: ChunkRange,
187}
188
189#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
196pub struct XorbReconstructionFetchInfo {
197 pub range: ChunkRange,
199 pub url: String,
200 pub url_range: HttpRange,
202}
203
204#[derive(Debug, Serialize, Deserialize, Clone)]
205pub struct QueryReconstructionResponse {
206 pub offset_into_first_range: u64,
209 pub terms: Vec<XorbReconstructionTerm>,
212 pub fetch_info: HashMap<HexMerkleHash, Vec<XorbReconstructionFetchInfo>>,
217}
218
219#[derive(Debug, Serialize, Deserialize, Clone)]
223pub struct QueryReconstructionResponseV2 {
224 pub offset_into_first_range: u64,
225 pub terms: Vec<XorbReconstructionTerm>,
226 pub xorbs: HashMap<HexMerkleHash, Vec<XorbMultiRangeFetch>>,
230}
231
232#[derive(Debug, Serialize, Deserialize, Clone)]
234pub struct XorbMultiRangeFetch {
235 pub url: String,
238 pub ranges: Vec<XorbRangeDescriptor>,
240}
241
242#[derive(Debug, Serialize, Deserialize, Clone)]
244pub struct XorbRangeDescriptor {
245 pub chunks: ChunkRange,
247 pub bytes: HttpRange,
249}
250
251impl From<QueryReconstructionResponse> for QueryReconstructionResponseV2 {
252 fn from(v1: QueryReconstructionResponse) -> Self {
253 let xorbs = v1
254 .fetch_info
255 .into_iter()
256 .map(|(hash, fetch_infos)| {
257 let fetch = fetch_infos
258 .into_iter()
259 .map(|info| XorbMultiRangeFetch {
260 url: info.url,
261 ranges: vec![XorbRangeDescriptor {
262 chunks: info.range,
263 bytes: info.url_range,
264 }],
265 })
266 .collect();
267 (hash, fetch)
268 })
269 .collect();
270
271 QueryReconstructionResponseV2 {
272 offset_into_first_range: v1.offset_into_first_range,
273 terms: v1.terms,
274 xorbs,
275 }
276 }
277}
278
279pub type BatchQueryReconstructionRequest = HashSet<HexKey>;
283
284#[derive(Debug, Serialize, Deserialize, Clone)]
286pub struct BatchQueryReconstructionResponse {
287 pub files: HashMap<HexMerkleHash, Vec<XorbReconstructionTerm>>,
290 pub fetch_info: HashMap<HexMerkleHash, Vec<XorbReconstructionFetchInfo>>,
295}
296
297#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, Copy, PartialEq)]
298#[repr(u8)]
299pub enum UploadShardResponseType {
300 Exists = 0,
301 SyncPerformed = 1,
302}
303
304#[derive(Debug, Serialize, Deserialize, Clone)]
305pub struct UploadShardResponse {
306 pub result: UploadShardResponseType,
307}
308
309#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
312#[serde(rename_all = "snake_case")]
313pub enum CommitStage {
314 Uploading = 0,
316 Syncing = 1,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
321#[serde(tag = "type", rename_all = "snake_case")]
322pub enum ShardUploadEvent {
323 Validating { verified: u64, total: u64 },
327 Committing { stage: CommitStage },
329 Result,
331 Error {
334 message: String,
335 #[serde(default)]
338 retryable: bool,
339 },
340 #[serde(other)]
342 Unknown,
343}
344
345impl PartialOrd for ShardUploadEvent {
350 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
351 match self {
352 Self::Validating { .. } => match other {
353 Self::Validating { .. } => None,
354 Self::Error { .. } | Self::Unknown => None,
355 _ => Some(Ordering::Less),
356 },
357 Self::Committing { stage } => match other {
358 Self::Validating { .. } => Some(Ordering::Greater),
359 Self::Committing { stage: other_stage } => Some(stage.cmp(other_stage)),
360 Self::Result => Some(Ordering::Less),
361 Self::Error { .. } | Self::Unknown => None,
362 },
363 Self::Result => match other {
364 Self::Result => Some(Ordering::Equal),
365 Self::Error { .. } | Self::Unknown => None,
366 _ => Some(Ordering::Greater),
367 },
368 Self::Error { .. } | Self::Unknown => None,
369 }
370 }
371}
372
373impl ShardUploadEvent {
374 pub fn precede(&self, other: &Self) -> bool {
375 matches!(self.partial_cmp(other), Some(Ordering::Less))
376 }
377}
378
379#[derive(Debug, Serialize, Deserialize, Clone)]
380pub struct QueryChunkResponse {
381 pub shard: MerkleHash,
382}
383
384pub const X_RANGE_DIRTY_HEADER: &str = "X-Range-Dirty";
390
391#[derive(Debug, Serialize, Deserialize, Clone)]
396#[serde(rename_all = "camelCase")]
397pub struct ChunkWindow {
398 pub dirty_byte_range: [u64; 2],
399}
400
401#[derive(Debug, Serialize, Deserialize, Clone)]
408#[serde(rename_all = "camelCase")]
409pub struct FileChunkHashesResponse {
410 pub total_chunks: u64,
411 pub file_size: u64,
412 pub windows: Vec<ChunkWindow>,
413 pub hash_ranges: Vec<Option<MerkleHashSubtree>>,
414 pub gap_verification: Vec<HexMerkleHash>,
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_file_range_segment() {
426 let file_range = FileRange::full();
427 let segment_size = 824820;
428
429 let (segment, remainder) = file_range.take_segment(segment_size);
430
431 assert_eq!(segment, FileRange::new(0, segment_size));
432 assert_eq!(remainder, Some(FileRange::new(segment_size, u64::MAX)));
433 }
434
435 #[test]
436 fn test_file_range_segment_no_remainder() {
437 let file_range = FileRange::new(50, 100);
438 let segment_size = 40;
439
440 let (s1, remainder) = file_range.take_segment(segment_size);
441
442 assert_eq!(s1, FileRange::new(50, 90));
443 assert_eq!(remainder, Some(FileRange::new(90, 100)));
444
445 let (s2, remainder) = remainder.unwrap().take_segment(segment_size);
446
447 assert_eq!(s2, FileRange::new(90, 100));
448 assert_eq!(remainder, None);
449 }
450
451 #[test]
452 fn test_http_range_type_casting() {
453 assert_eq!(HttpRange::from(FileRange::new(0, 10)), HttpRange::new(0, 9));
454
455 assert_eq!(FileRange::from(HttpRange::new(0, 10)), FileRange::new(0, 11));
456 }
457
458 #[test]
459 fn test_shard_upload_event_validating_json_roundtrip() {
460 let event = ShardUploadEvent::Validating { verified: 3, total: 7 };
461 let json = serde_json::to_string(&event).unwrap();
462 assert_eq!(json, r#"{"type":"validating","verified":3,"total":7}"#);
463 assert_eq!(serde_json::from_str::<ShardUploadEvent>(&json).unwrap(), event);
464 }
465
466 #[test]
467 fn test_shard_upload_event_committing_json_roundtrip() {
468 for (stage, tag) in [(CommitStage::Uploading, "uploading"), (CommitStage::Syncing, "syncing")] {
469 let event = ShardUploadEvent::Committing { stage };
470 let json = serde_json::to_string(&event).unwrap();
471 assert_eq!(json, format!(r#"{{"type":"committing","stage":"{tag}"}}"#));
472 assert_eq!(serde_json::from_str::<ShardUploadEvent>(&json).unwrap(), event);
473 }
474 }
475
476 #[test]
477 fn test_shard_upload_event_result_json_roundtrip() {
478 let event = ShardUploadEvent::Result;
479 let json = serde_json::to_string(&event).unwrap();
480 assert_eq!(json, r#"{"type":"result"}"#);
481 assert_eq!(serde_json::from_str::<ShardUploadEvent>(&json).unwrap(), event);
482 }
483
484 #[test]
485 fn test_shard_upload_event_error_json_roundtrip() {
486 let event = ShardUploadEvent::Error {
487 message: "boom".to_string(),
488 retryable: false,
489 };
490 let json = serde_json::to_string(&event).unwrap();
491 assert_eq!(json, r#"{"type":"error","message":"boom","retryable":false}"#);
492 assert_eq!(serde_json::from_str::<ShardUploadEvent>(&json).unwrap(), event);
493
494 let retryable = ShardUploadEvent::Error {
495 message: "transient".to_string(),
496 retryable: true,
497 };
498 let json = serde_json::to_string(&retryable).unwrap();
499 assert_eq!(json, r#"{"type":"error","message":"transient","retryable":true}"#);
500 assert_eq!(serde_json::from_str::<ShardUploadEvent>(&json).unwrap(), retryable);
501
502 let omitted = serde_json::from_str::<ShardUploadEvent>(r#"{"type":"error","message":"boom"}"#).unwrap();
504 assert_eq!(
505 omitted,
506 ShardUploadEvent::Error {
507 message: "boom".to_string(),
508 retryable: false,
509 }
510 );
511 }
512
513 #[test]
514 fn test_shard_upload_event_partial_cmp_progression_matrix() {
515 let cases = [
518 ShardUploadEvent::Validating { verified: 1, total: 2 },
519 ShardUploadEvent::Committing {
520 stage: CommitStage::Uploading,
521 },
522 ShardUploadEvent::Committing {
523 stage: CommitStage::Syncing,
524 },
525 ShardUploadEvent::Result,
526 ShardUploadEvent::Error {
527 message: "boom".to_string(),
528 retryable: false,
529 },
530 ];
531 let labels = [
532 "validating",
533 "committing_uploading",
534 "committing_syncing",
535 "result",
536 "error",
537 ];
538
539 #[rustfmt::skip]
540 let expected: [[Option<Ordering>; 5]; 5] = [
541 [None, Some(Ordering::Less), Some(Ordering::Less), Some(Ordering::Less), None],
542 [Some(Ordering::Greater), Some(Ordering::Equal), Some(Ordering::Less), Some(Ordering::Less), None],
543 [Some(Ordering::Greater), Some(Ordering::Greater), Some(Ordering::Equal), Some(Ordering::Less), None],
544 [Some(Ordering::Greater), Some(Ordering::Greater), Some(Ordering::Greater), Some(Ordering::Equal), None],
545 [None, None, None, None, None],
546 ];
547
548 for (i, a) in cases.iter().enumerate() {
549 for (j, b) in cases.iter().enumerate() {
550 assert_eq!(
551 a.partial_cmp(b),
552 expected[i][j],
553 "{}.partial_cmp({}) should be {:?}",
554 labels[i],
555 labels[j],
556 expected[i][j]
557 );
558 assert_eq!(
561 a.precede(b),
562 matches!(expected[i][j], Some(Ordering::Less)),
563 "{}.precede({}) disagrees with its partial_cmp result",
564 labels[i],
565 labels[j]
566 );
567 }
568 }
569 }
570
571 #[test]
572 fn test_shard_upload_event_partial_cmp_ignores_payload_within_same_variant() {
573 let a = ShardUploadEvent::Result;
575 let b = ShardUploadEvent::Result;
576 assert_eq!(a, b);
577 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
578 assert!(!a.precede(&b));
579
580 let low = ShardUploadEvent::Validating { verified: 1, total: 2 };
584 let high = ShardUploadEvent::Validating { verified: 9, total: 9 };
585 assert_eq!(low.partial_cmp(&high), None);
586 assert!(!low.precede(&high));
587 assert!(!high.precede(&low));
588
589 let err_a = ShardUploadEvent::Error {
591 message: "a".to_string(),
592 retryable: false,
593 };
594 let err_b = ShardUploadEvent::Error {
595 message: "b".to_string(),
596 retryable: true,
597 };
598 assert_eq!(err_a.partial_cmp(&err_b), None);
599 assert!(!err_a.precede(&err_b));
600 assert!(!err_b.precede(&err_a));
601
602 assert_eq!(ShardUploadEvent::Unknown.partial_cmp(&ShardUploadEvent::Result), None);
603 assert!(!ShardUploadEvent::Unknown.precede(&ShardUploadEvent::Result));
604 assert!(!ShardUploadEvent::Result.precede(&ShardUploadEvent::Unknown));
605 }
606
607 #[test]
608 fn test_shard_upload_event_unknown_type_deserializes() {
609 let event: ShardUploadEvent = serde_json::from_str(r#"{"type":"heartbeat"}"#).unwrap();
610 assert_eq!(event, ShardUploadEvent::Unknown);
611
612 let event: ShardUploadEvent = serde_json::from_str(r#"{"type":"future_stage","detail":{"n":1}}"#).unwrap();
614 assert_eq!(event, ShardUploadEvent::Unknown);
615 }
616
617 #[test]
618 fn test_shard_upload_event_unknown_is_incomparable() {
619 let known = [
620 ShardUploadEvent::Validating { verified: 1, total: 2 },
621 ShardUploadEvent::Committing {
622 stage: CommitStage::Uploading,
623 },
624 ShardUploadEvent::Committing {
625 stage: CommitStage::Syncing,
626 },
627 ShardUploadEvent::Result,
628 ShardUploadEvent::Error {
629 message: "boom".to_string(),
630 retryable: false,
631 },
632 ShardUploadEvent::Unknown,
633 ];
634
635 for other in &known {
636 assert_eq!(ShardUploadEvent::Unknown.partial_cmp(other), None);
637 assert_eq!(other.partial_cmp(&ShardUploadEvent::Unknown), None);
638 assert!(!ShardUploadEvent::Unknown.precede(other));
639 assert!(!other.precede(&ShardUploadEvent::Unknown));
640 }
641 }
642
643 #[test]
644 fn test_shard_upload_event_known_variant_ignores_extra_fields() {
645 let event: ShardUploadEvent =
647 serde_json::from_str(r#"{"type":"validating","verified":1,"total":2,"extra":true}"#).unwrap();
648 assert_eq!(event, ShardUploadEvent::Validating { verified: 1, total: 2 });
649 }
650}