use core::fmt;
use std::cmp::{Ordering, min};
use std::collections::{HashMap, HashSet};
use std::marker::PhantomData;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use thiserror::Error;
use xet_core_structures::merklehash::{MerkleHash, MerkleHashSubtree};
mod key;
pub use key::*;
pub const SESSION_ID_HEADER: &str = "X-Xet-Session-Id";
pub const REQUEST_ID_HEADER: &str = "X-Request-Id";
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UploadXorbResponse {
pub was_inserted: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Copy)]
pub struct _C;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Copy)]
pub struct _F;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Copy)]
pub struct _H;
pub type ChunkRange = Range<u32, _C>;
pub type FileRange = Range<u64, _F>;
pub type HttpRange = Range<u64, _H>;
impl FileRange {
pub fn full() -> Self {
Self::new(0, u64::MAX)
}
pub fn take_segment(self, segment_size: u64) -> (Self, Option<Self>) {
let segment = FileRange {
start: self.start,
end: min(self.end, self.start + segment_size),
_marker: PhantomData,
};
let remainder = if segment.end == self.end {
None
} else {
Some(FileRange {
start: segment.end,
end: self.end,
_marker: PhantomData,
})
};
(segment, remainder)
}
pub fn length(&self) -> u64 {
self.end - self.start
}
}
impl From<HttpRange> for FileRange {
fn from(value: HttpRange) -> Self {
FileRange::new(value.start, value.end + 1)
}
}
impl HttpRange {
pub fn range_header(&self) -> String {
format!("bytes={self}")
}
pub fn length(&self) -> u64 {
self.end - self.start + 1
}
}
impl From<FileRange> for HttpRange {
fn from(value: FileRange) -> Self {
HttpRange::new(value.start, value.end - 1)
}
}
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, PartialOrd, Ord, Default, Hash)]
pub struct Range<Idx, Kind> {
pub start: Idx,
pub end: Idx,
#[serde(skip)]
pub _marker: PhantomData<Kind>,
}
impl<Idx, _C> fmt::Debug for Range<Idx, _C>
where
Idx: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Range")
.field("start", &self.start)
.field("end", &self.end)
.finish()
}
}
impl<Idx, Kind> Range<Idx, Kind> {
pub fn new(start: Idx, end: Idx) -> Self {
Self {
start,
end,
_marker: PhantomData,
}
}
}
impl<T: Copy, Kind: Copy> Copy for Range<T, Kind> {}
impl<Idx: fmt::Display, Kind> fmt::Display for Range<Idx, Kind> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{}", self.start, self.end)
}
}
#[derive(Error, Debug)]
pub enum RangeParseError<Idx: std::str::FromStr> {
#[error("Invalid format, expect [start]-[end]")]
InvalidFormat,
#[error("Incorrect number: {0}")]
ParseError(Idx::Err),
}
impl<Idx: FromStr, Kind> TryFrom<&str> for Range<Idx, Kind> {
type Error = RangeParseError<Idx>;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let parts: Vec<&str> = value.splitn(2, '-').collect();
if parts.len() != 2 {
return Err(RangeParseError::InvalidFormat);
}
let start = parts[0].parse::<Idx>().map_err(RangeParseError::ParseError)?;
let end = parts[1].parse::<Idx>().map_err(RangeParseError::ParseError)?;
Ok(Range {
start,
end,
_marker: PhantomData,
})
}
}
impl<Idx: FromStr, Kind> FromStr for Range<Idx, Kind> {
type Err = RangeParseError<Idx>;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::try_from(value)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct XorbReconstructionTerm {
pub hash: HexMerkleHash,
pub unpacked_length: u32,
pub range: ChunkRange,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct XorbReconstructionFetchInfo {
pub range: ChunkRange,
pub url: String,
pub url_range: HttpRange,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct QueryReconstructionResponse {
pub offset_into_first_range: u64,
pub terms: Vec<XorbReconstructionTerm>,
pub fetch_info: HashMap<HexMerkleHash, Vec<XorbReconstructionFetchInfo>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct QueryReconstructionResponseV2 {
pub offset_into_first_range: u64,
pub terms: Vec<XorbReconstructionTerm>,
pub xorbs: HashMap<HexMerkleHash, Vec<XorbMultiRangeFetch>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct XorbMultiRangeFetch {
pub url: String,
pub ranges: Vec<XorbRangeDescriptor>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct XorbRangeDescriptor {
pub chunks: ChunkRange,
pub bytes: HttpRange,
}
impl From<QueryReconstructionResponse> for QueryReconstructionResponseV2 {
fn from(v1: QueryReconstructionResponse) -> Self {
let xorbs = v1
.fetch_info
.into_iter()
.map(|(hash, fetch_infos)| {
let fetch = fetch_infos
.into_iter()
.map(|info| XorbMultiRangeFetch {
url: info.url,
ranges: vec![XorbRangeDescriptor {
chunks: info.range,
bytes: info.url_range,
}],
})
.collect();
(hash, fetch)
})
.collect();
QueryReconstructionResponseV2 {
offset_into_first_range: v1.offset_into_first_range,
terms: v1.terms,
xorbs,
}
}
}
pub type BatchQueryReconstructionRequest = HashSet<HexKey>;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BatchQueryReconstructionResponse {
pub files: HashMap<HexMerkleHash, Vec<XorbReconstructionTerm>>,
pub fetch_info: HashMap<HexMerkleHash, Vec<XorbReconstructionFetchInfo>>,
}
#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum UploadShardResponseType {
Exists = 0,
SyncPerformed = 1,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UploadShardResponse {
pub result: UploadShardResponseType,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum CommitStage {
Uploading = 0,
Syncing = 1,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ShardUploadEvent {
Validating { verified: u64, total: u64 },
Committing { stage: CommitStage },
Result,
Error {
message: String,
#[serde(default)]
retryable: bool,
},
#[serde(other)]
Unknown,
}
impl PartialOrd for ShardUploadEvent {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match self {
Self::Validating { .. } => match other {
Self::Validating { .. } => None,
Self::Error { .. } | Self::Unknown => None,
_ => Some(Ordering::Less),
},
Self::Committing { stage } => match other {
Self::Validating { .. } => Some(Ordering::Greater),
Self::Committing { stage: other_stage } => Some(stage.cmp(other_stage)),
Self::Result => Some(Ordering::Less),
Self::Error { .. } | Self::Unknown => None,
},
Self::Result => match other {
Self::Result => Some(Ordering::Equal),
Self::Error { .. } | Self::Unknown => None,
_ => Some(Ordering::Greater),
},
Self::Error { .. } | Self::Unknown => None,
}
}
}
impl ShardUploadEvent {
pub fn precede(&self, other: &Self) -> bool {
matches!(self.partial_cmp(other), Some(Ordering::Less))
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct QueryChunkResponse {
pub shard: MerkleHash,
}
pub const X_RANGE_DIRTY_HEADER: &str = "X-Range-Dirty";
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ChunkWindow {
pub dirty_byte_range: [u64; 2],
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FileChunkHashesResponse {
pub total_chunks: u64,
pub file_size: u64,
pub windows: Vec<ChunkWindow>,
pub hash_ranges: Vec<Option<MerkleHashSubtree>>,
pub gap_verification: Vec<HexMerkleHash>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_range_segment() {
let file_range = FileRange::full();
let segment_size = 824820;
let (segment, remainder) = file_range.take_segment(segment_size);
assert_eq!(segment, FileRange::new(0, segment_size));
assert_eq!(remainder, Some(FileRange::new(segment_size, u64::MAX)));
}
#[test]
fn test_file_range_segment_no_remainder() {
let file_range = FileRange::new(50, 100);
let segment_size = 40;
let (s1, remainder) = file_range.take_segment(segment_size);
assert_eq!(s1, FileRange::new(50, 90));
assert_eq!(remainder, Some(FileRange::new(90, 100)));
let (s2, remainder) = remainder.unwrap().take_segment(segment_size);
assert_eq!(s2, FileRange::new(90, 100));
assert_eq!(remainder, None);
}
#[test]
fn test_http_range_type_casting() {
assert_eq!(HttpRange::from(FileRange::new(0, 10)), HttpRange::new(0, 9));
assert_eq!(FileRange::from(HttpRange::new(0, 10)), FileRange::new(0, 11));
}
#[test]
fn test_shard_upload_event_validating_json_roundtrip() {
let event = ShardUploadEvent::Validating { verified: 3, total: 7 };
let json = serde_json::to_string(&event).unwrap();
assert_eq!(json, r#"{"type":"validating","verified":3,"total":7}"#);
assert_eq!(serde_json::from_str::<ShardUploadEvent>(&json).unwrap(), event);
}
#[test]
fn test_shard_upload_event_committing_json_roundtrip() {
for (stage, tag) in [(CommitStage::Uploading, "uploading"), (CommitStage::Syncing, "syncing")] {
let event = ShardUploadEvent::Committing { stage };
let json = serde_json::to_string(&event).unwrap();
assert_eq!(json, format!(r#"{{"type":"committing","stage":"{tag}"}}"#));
assert_eq!(serde_json::from_str::<ShardUploadEvent>(&json).unwrap(), event);
}
}
#[test]
fn test_shard_upload_event_result_json_roundtrip() {
let event = ShardUploadEvent::Result;
let json = serde_json::to_string(&event).unwrap();
assert_eq!(json, r#"{"type":"result"}"#);
assert_eq!(serde_json::from_str::<ShardUploadEvent>(&json).unwrap(), event);
}
#[test]
fn test_shard_upload_event_error_json_roundtrip() {
let event = ShardUploadEvent::Error {
message: "boom".to_string(),
retryable: false,
};
let json = serde_json::to_string(&event).unwrap();
assert_eq!(json, r#"{"type":"error","message":"boom","retryable":false}"#);
assert_eq!(serde_json::from_str::<ShardUploadEvent>(&json).unwrap(), event);
let retryable = ShardUploadEvent::Error {
message: "transient".to_string(),
retryable: true,
};
let json = serde_json::to_string(&retryable).unwrap();
assert_eq!(json, r#"{"type":"error","message":"transient","retryable":true}"#);
assert_eq!(serde_json::from_str::<ShardUploadEvent>(&json).unwrap(), retryable);
let omitted = serde_json::from_str::<ShardUploadEvent>(r#"{"type":"error","message":"boom"}"#).unwrap();
assert_eq!(
omitted,
ShardUploadEvent::Error {
message: "boom".to_string(),
retryable: false,
}
);
}
#[test]
fn test_shard_upload_event_partial_cmp_progression_matrix() {
let cases = [
ShardUploadEvent::Validating { verified: 1, total: 2 },
ShardUploadEvent::Committing {
stage: CommitStage::Uploading,
},
ShardUploadEvent::Committing {
stage: CommitStage::Syncing,
},
ShardUploadEvent::Result,
ShardUploadEvent::Error {
message: "boom".to_string(),
retryable: false,
},
];
let labels = [
"validating",
"committing_uploading",
"committing_syncing",
"result",
"error",
];
#[rustfmt::skip]
let expected: [[Option<Ordering>; 5]; 5] = [
[None, Some(Ordering::Less), Some(Ordering::Less), Some(Ordering::Less), None],
[Some(Ordering::Greater), Some(Ordering::Equal), Some(Ordering::Less), Some(Ordering::Less), None],
[Some(Ordering::Greater), Some(Ordering::Greater), Some(Ordering::Equal), Some(Ordering::Less), None],
[Some(Ordering::Greater), Some(Ordering::Greater), Some(Ordering::Greater), Some(Ordering::Equal), None],
[None, None, None, None, None],
];
for (i, a) in cases.iter().enumerate() {
for (j, b) in cases.iter().enumerate() {
assert_eq!(
a.partial_cmp(b),
expected[i][j],
"{}.partial_cmp({}) should be {:?}",
labels[i],
labels[j],
expected[i][j]
);
assert_eq!(
a.precede(b),
matches!(expected[i][j], Some(Ordering::Less)),
"{}.precede({}) disagrees with its partial_cmp result",
labels[i],
labels[j]
);
}
}
}
#[test]
fn test_shard_upload_event_partial_cmp_ignores_payload_within_same_variant() {
let a = ShardUploadEvent::Result;
let b = ShardUploadEvent::Result;
assert_eq!(a, b);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
assert!(!a.precede(&b));
let low = ShardUploadEvent::Validating { verified: 1, total: 2 };
let high = ShardUploadEvent::Validating { verified: 9, total: 9 };
assert_eq!(low.partial_cmp(&high), None);
assert!(!low.precede(&high));
assert!(!high.precede(&low));
let err_a = ShardUploadEvent::Error {
message: "a".to_string(),
retryable: false,
};
let err_b = ShardUploadEvent::Error {
message: "b".to_string(),
retryable: true,
};
assert_eq!(err_a.partial_cmp(&err_b), None);
assert!(!err_a.precede(&err_b));
assert!(!err_b.precede(&err_a));
assert_eq!(ShardUploadEvent::Unknown.partial_cmp(&ShardUploadEvent::Result), None);
assert!(!ShardUploadEvent::Unknown.precede(&ShardUploadEvent::Result));
assert!(!ShardUploadEvent::Result.precede(&ShardUploadEvent::Unknown));
}
#[test]
fn test_shard_upload_event_unknown_type_deserializes() {
let event: ShardUploadEvent = serde_json::from_str(r#"{"type":"heartbeat"}"#).unwrap();
assert_eq!(event, ShardUploadEvent::Unknown);
let event: ShardUploadEvent = serde_json::from_str(r#"{"type":"future_stage","detail":{"n":1}}"#).unwrap();
assert_eq!(event, ShardUploadEvent::Unknown);
}
#[test]
fn test_shard_upload_event_unknown_is_incomparable() {
let known = [
ShardUploadEvent::Validating { verified: 1, total: 2 },
ShardUploadEvent::Committing {
stage: CommitStage::Uploading,
},
ShardUploadEvent::Committing {
stage: CommitStage::Syncing,
},
ShardUploadEvent::Result,
ShardUploadEvent::Error {
message: "boom".to_string(),
retryable: false,
},
ShardUploadEvent::Unknown,
];
for other in &known {
assert_eq!(ShardUploadEvent::Unknown.partial_cmp(other), None);
assert_eq!(other.partial_cmp(&ShardUploadEvent::Unknown), None);
assert!(!ShardUploadEvent::Unknown.precede(other));
assert!(!other.precede(&ShardUploadEvent::Unknown));
}
}
#[test]
fn test_shard_upload_event_known_variant_ignores_extra_fields() {
let event: ShardUploadEvent =
serde_json::from_str(r#"{"type":"validating","verified":1,"total":2,"extra":true}"#).unwrap();
assert_eq!(event, ShardUploadEvent::Validating { verified: 1, total: 2 });
}
}