pub mod layout;
pub mod memory;
pub mod probe;
#[cfg(feature = "s3")]
pub mod s3;
#[cfg(feature = "s3")]
pub mod rawread;
use async_trait::async_trait;
use bytes::Bytes;
use std::collections::HashMap;
const CRC64_NVME_POLY: u64 = 0x9A6C_9329_AC4B_C9B5;
fn crc64_tables8() -> &'static [[u64; 256]; 8] {
static TABLES: std::sync::OnceLock<[[u64; 256]; 8]> = std::sync::OnceLock::new();
TABLES.get_or_init(|| {
let mut t = [[0u64; 256]; 8];
t[0] = *crc64_table();
let mut i = 0usize;
while i < 256 {
let mut k = 1usize;
while k < 8 {
let prev = t[k - 1][i];
t[k][i] = (prev >> 8) ^ t[0][(prev & 0xFF) as usize];
k += 1;
}
i += 1;
}
t
})
}
fn crc64_table() -> &'static [u64; 256] {
static TABLE: std::sync::OnceLock<[u64; 256]> = std::sync::OnceLock::new();
TABLE.get_or_init(|| {
let mut t = [0u64; 256];
let mut i = 0usize;
while i < 256 {
let mut crc = i as u64;
let mut b = 0;
while b < 8 {
crc = if crc & 1 == 1 { (crc >> 1) ^ CRC64_NVME_POLY } else { crc >> 1 };
b += 1;
}
t[i] = crc;
i += 1;
}
t
})
}
#[derive(Clone)]
pub struct Crc64Nvme(u64);
impl Default for Crc64Nvme {
fn default() -> Self {
Self::new()
}
}
impl Crc64Nvme {
pub fn new() -> Self {
Crc64Nvme(u64::MAX)
}
pub fn update(&mut self, data: &[u8]) {
let t = crc64_tables8();
let mut crc = self.0;
let mut rest = data;
while let Some((head, tail)) = rest.split_first_chunk::<8>() {
let w = u64::from_le_bytes(*head) ^ crc;
crc = t[7][(w & 0xFF) as usize]
^ t[6][((w >> 8) & 0xFF) as usize]
^ t[5][((w >> 16) & 0xFF) as usize]
^ t[4][((w >> 24) & 0xFF) as usize]
^ t[3][((w >> 32) & 0xFF) as usize]
^ t[2][((w >> 40) & 0xFF) as usize]
^ t[1][((w >> 48) & 0xFF) as usize]
^ t[0][((w >> 56) & 0xFF) as usize];
rest = tail;
}
for &b in rest {
crc = t[0][((crc ^ b as u64) & 0xFF) as usize] ^ (crc >> 8);
}
self.0 = crc;
}
pub fn finalize(self) -> u64 {
self.0 ^ u64::MAX
}
}
fn gf2_times(mat: &[u64; 64], mut v: u64) -> u64 {
let mut sum = 0u64;
let mut i = 0usize;
while v != 0 {
if v & 1 == 1 {
sum ^= mat[i];
}
v >>= 1;
i += 1;
}
sum
}
fn gf2_square(out: &mut [u64; 64], mat: &[u64; 64]) {
for n in 0..64 {
out[n] = gf2_times(mat, mat[n]);
}
}
pub fn crc64_combine(crc_a: u64, crc_b: u64, len_b: u64) -> u64 {
if len_b == 0 {
return crc_a;
}
let mut odd = [0u64; 64];
let mut even = [0u64; 64];
odd[0] = CRC64_NVME_POLY;
let mut row = 1u64;
for n in 1..64 {
odd[n] = row;
row <<= 1;
}
gf2_square(&mut even, &odd); gf2_square(&mut odd, &even); let mut crc = crc_a;
let mut len = len_b;
loop {
gf2_square(&mut even, &odd); if len & 1 == 1 {
crc = gf2_times(&even, crc);
}
len >>= 1;
if len == 0 {
break;
}
gf2_square(&mut odd, &even);
if len & 1 == 1 {
crc = gf2_times(&odd, crc);
}
len >>= 1;
if len == 0 {
break;
}
}
crc ^ crc_b
}
pub fn crc64_nvme(data: &[u8]) -> u64 {
let mut c = Crc64Nvme::new();
c.update(data);
c.finalize()
}
pub fn crc64_from_b64(s: &str) -> Option<u64> {
const AL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let s = s.trim();
if s.len() != 12 || !s.ends_with('=') {
return None;
}
let mut acc: u32 = 0;
let mut bits = 0u32;
let mut out: Vec<u8> = Vec::with_capacity(8);
for c in s.bytes() {
if c == b'=' {
break;
}
let v = AL.iter().position(|&a| a == c)? as u32;
acc = (acc << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((acc >> bits) as u8);
}
}
if out.len() != 8 {
return None;
}
let mut b = [0u8; 8];
b.copy_from_slice(&out);
Some(u64::from_be_bytes(b))
}
pub fn crc64_to_b64(crc: u64) -> String {
const AL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let bytes = crc.to_be_bytes();
let mut out = String::with_capacity(12);
for chunk in bytes.chunks(3) {
let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
let n = u32::from_be_bytes([0, b[0], b[1], b[2]]);
out.push(AL[(n >> 18) as usize & 63] as char);
out.push(AL[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 { AL[(n >> 6) as usize & 63] as char } else { '=' });
out.push(if chunk.len() > 2 { AL[n as usize & 63] as char } else { '=' });
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GenerationStamps {
pub generation: u64,
pub epoch: u64,
pub flush_uuid: String,
pub boundary_source: Option<String>,
pub posix: Option<PosixStamps>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PosixStamps {
pub mode: u32,
pub uid: u32,
pub gid: u32,
pub mtime_unix: i64,
}
impl PosixStamps {
pub const META_MODE: &'static str = "flint-mode";
pub const META_UID: &'static str = "flint-uid";
pub const META_GID: &'static str = "flint-gid";
pub const META_MTIME: &'static str = "flint-mtime";
#[cfg(unix)]
pub fn from_metadata(m: &std::fs::Metadata) -> PosixStamps {
use std::os::unix::fs::MetadataExt;
PosixStamps { mode: m.mode(), uid: m.uid(), gid: m.gid(), mtime_unix: m.mtime() }
}
fn to_meta(self) -> Vec<(String, String)> {
vec![
(Self::META_MODE.into(), format!("{:o}", self.mode)),
(Self::META_UID.into(), self.uid.to_string()),
(Self::META_GID.into(), self.gid.to_string()),
(Self::META_MTIME.into(), self.mtime_unix.to_string()),
]
}
pub fn from_meta(meta: &HashMap<String, String>) -> Option<PosixStamps> {
Some(PosixStamps {
mode: u32::from_str_radix(meta.get(Self::META_MODE)?, 8).ok()?,
uid: meta.get(Self::META_UID)?.parse().ok()?,
gid: meta.get(Self::META_GID)?.parse().ok()?,
mtime_unix: meta.get(Self::META_MTIME)?.parse().ok()?,
})
}
}
impl GenerationStamps {
pub const META_GEN: &'static str = "flint-gen";
pub const META_EPOCH: &'static str = "flint-epoch";
pub const META_FLUSH_UUID: &'static str = "flint-flush-uuid";
pub const META_BOUNDARY_SOURCE: &'static str = "flint-boundary-source";
pub fn to_meta(&self) -> Vec<(String, String)> {
let mut v = vec![
(Self::META_GEN.into(), self.generation.to_string()),
(Self::META_EPOCH.into(), self.epoch.to_string()),
(Self::META_FLUSH_UUID.into(), self.flush_uuid.clone()),
];
if let Some(b) = &self.boundary_source {
v.push((Self::META_BOUNDARY_SOURCE.into(), b.clone()));
}
if let Some(p) = self.posix {
v.extend(p.to_meta());
}
v
}
pub fn from_meta(meta: &HashMap<String, String>) -> Option<GenerationStamps> {
Some(GenerationStamps {
generation: meta.get(Self::META_GEN)?.parse().ok()?,
epoch: meta.get(Self::META_EPOCH)?.parse().ok()?,
flush_uuid: meta.get(Self::META_FLUSH_UUID)?.clone(),
boundary_source: meta.get(Self::META_BOUNDARY_SOURCE).cloned(),
posix: PosixStamps::from_meta(meta),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListedVersion {
pub key: String,
pub version_id: String,
pub etag: String,
pub size: u64,
pub is_current: bool,
pub is_delete_marker: bool,
pub last_modified_unix: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectMeta {
pub etag: String,
pub size: u64,
pub crc64_b64: Option<String>,
pub meta: HashMap<String, String>,
pub last_modified_unix: Option<u64>,
pub storage_class: Option<String>,
pub version_id: Option<String>,
}
impl ObjectMeta {
pub fn copy_source_allowed(&self) -> bool {
matches!(self.storage_class.as_deref(), None | Some("STANDARD"))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListedObject {
pub key: String,
pub size: u64,
pub etag: String,
pub last_modified_unix: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingUpload {
pub key: String,
pub upload_id: String,
pub initiated_unix: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PutCondition {
IfMatch(String),
IfNoneMatchAny,
Unconditional,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PartSource {
Local { offset: u64, len: u64 },
BaseCopy { offset: u64, len: u64 },
}
#[derive(Debug, Clone)]
pub struct ComposeSpec<'a> {
pub key: &'a str,
pub local_path: &'a std::path::Path,
pub parts: Vec<PartSource>,
pub base_key: Option<&'a str>,
pub base_etag: Option<String>,
pub condition: PutCondition,
pub stamps: GenerationStamps,
pub crc64: Option<u64>,
pub progress: Option<std::sync::Arc<std::sync::atomic::AtomicU64>>,
}
impl ComposeSpec<'_> {
pub fn note_progress(&self, bytes: u64) {
if let Some(p) = &self.progress {
p.fetch_add(bytes, std::sync::atomic::Ordering::Relaxed);
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("precondition failed (412): {0}")]
PreconditionFailed(String),
#[error("conditional conflict (409): {0}")]
Conflict(String),
#[error("not found: {0}")]
NotFound(String),
#[error("no such upload: {0}")]
NoSuchUpload(String),
#[error("checksum mismatch: {0}")]
ChecksumMismatch(String),
#[error("not authorized: {0}")]
Auth(String),
#[error("store: {0}")]
Other(String),
}
pub type StoreResult<T> = Result<T, StoreError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpochState {
pub holder_id: String,
pub epoch: u64,
pub token: String,
pub last_renew_unix: Option<u64>,
pub echo: Option<String>,
pub released: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpochLease {
pub holder_id: String,
pub epoch: u64,
pub token: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct LeaseEcho {
#[serde(default)]
pub syncer_version: String,
#[serde(default)]
pub protocol: u32,
#[serde(default)]
pub active_boundary_mode: String,
#[serde(default)]
pub last_cited_seq: u64,
#[serde(default)]
pub last_cited_unix: u64,
#[serde(default)]
pub staged_uncited_count: u64,
#[serde(default)]
pub sentinel_verbs_active: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metrics_bound: Option<bool>,
}
#[async_trait]
pub trait ObjectStore: Send + Sync {
async fn put_whole(
&self,
key: &str,
body: Bytes,
condition: &PutCondition,
stamps: &GenerationStamps,
crc64: u64,
) -> StoreResult<ObjectMeta>;
async fn copy_object(
&self,
src_key: &str,
src_if_match: Option<&str>,
dst_key: &str,
condition: &PutCondition,
stamps: &GenerationStamps,
) -> StoreResult<ObjectMeta>;
async fn compose_generation(&self, spec: &ComposeSpec<'_>) -> StoreResult<ObjectMeta>;
async fn head(&self, key: &str) -> StoreResult<ObjectMeta>;
async fn get_whole(&self, key: &str, if_match: Option<&str>) -> StoreResult<(ObjectMeta, Bytes)>;
async fn get_range(
&self,
key: &str,
offset: u64,
len: u64,
if_match: &str,
) -> StoreResult<Bytes>;
async fn get_range_segments(
&self,
key: &str,
offset: u64,
len: u64,
if_match: &str,
) -> StoreResult<Vec<Bytes>> {
Ok(vec![self.get_range(key, offset, len, if_match).await?])
}
async fn list(&self, prefix: &str) -> StoreResult<Vec<ListedObject>>;
async fn delete(&self, key: &str) -> StoreResult<()>;
async fn head_version(&self, key: &str, version_id: &str) -> StoreResult<ObjectMeta> {
let _ = (key, version_id);
Err(StoreError::Other("this backend has no version-scoped HEAD".into()))
}
async fn get_version(&self, key: &str, version_id: &str) -> StoreResult<(ObjectMeta, Bytes)> {
let _ = (key, version_id);
Err(StoreError::Other("this backend has no version-scoped GET".into()))
}
async fn delete_version(&self, key: &str, version_id: &str) -> StoreResult<()> {
let _ = (key, version_id);
Err(StoreError::Other("this backend has no version-scoped DELETE".into()))
}
async fn list_versions(&self, prefix: &str) -> StoreResult<Vec<ListedVersion>> {
let _ = prefix;
Err(StoreError::Other("this backend cannot list object versions".into()))
}
async fn presign_get(&self, key: &str, ttl_secs: u64) -> StoreResult<String> {
let _ = (key, ttl_secs);
Err(StoreError::Other("this backend cannot presign a URL".into()))
}
async fn presign_put(&self, key: &str, ttl_secs: u64) -> StoreResult<String> {
let _ = (key, ttl_secs);
Err(StoreError::Other("this backend cannot presign a URL".into()))
}
async fn lifecycle_rules(&self) -> StoreResult<Vec<LifecycleView>> {
Err(StoreError::Other("this backend cannot read lifecycle rules".into()))
}
async fn ensure_noncurrent_retention(
&self,
prefix: &str,
days: u64,
) -> StoreResult<RetentionOutcome> {
let _ = (prefix, days);
Err(StoreError::Other("this backend cannot write lifecycle rules".into()))
}
async fn list_uploads(&self, prefix: &str) -> StoreResult<Vec<PendingUpload>>;
async fn abort_upload(&self, key: &str, upload_id: &str) -> StoreResult<()>;
async fn bootstrap(&self, prefix: &str) -> StoreResult<BootstrapReport>;
async fn epoch_read(&self, key: &str) -> StoreResult<Option<EpochState>>;
async fn epoch_acquire(
&self,
key: &str,
holder_id: &str,
supersede: Option<&EpochState>,
) -> StoreResult<EpochLease>;
async fn epoch_renew(
&self,
key: &str,
lease: &EpochLease,
echo: Option<&str>,
) -> StoreResult<EpochLease>;
async fn epoch_release(&self, key: &str, lease: &EpochLease) -> StoreResult<()>;
fn min_part_size(&self) -> u64;
fn max_parts(&self) -> usize;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LifecycleView {
pub id: String,
pub enabled: bool,
pub prefix: String,
pub noncurrent_days: Option<u64>,
pub expired_delete_marker: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetentionOutcome {
pub rule_id: String,
pub noncurrent_days: u64,
pub created: bool,
}
#[derive(Debug, Default, Clone)]
pub struct BootstrapReport {
pub notes: Vec<String>,
pub warnings: Vec<String>,
pub errors: Vec<String>,
}
impl BootstrapReport {
pub fn ok(&self) -> bool {
self.errors.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crc64_nvme_catalog_check_value() {
assert_eq!(crc64_nvme(b"123456789"), 0xAE8B_1486_0A79_9888);
let mut c = Crc64Nvme::new();
c.update(b"1234");
c.update(b"56789");
assert_eq!(c.finalize(), 0xAE8B_1486_0A79_9888);
assert_eq!(crc64_nvme(b""), 0);
}
#[test]
fn crc64_slice_by_8_agrees_with_the_byte_at_a_time_form() {
fn byte_at_a_time(data: &[u8]) -> u64 {
let t = crc64_table();
let mut crc = u64::MAX;
for &b in data {
crc = t[((crc ^ b as u64) & 0xFF) as usize] ^ (crc >> 8);
}
crc ^ u64::MAX
}
let data: Vec<u8> =
(0..1000u32).map(|i| (i.wrapping_mul(2654435761) >> 13) as u8).collect();
for n in 0..40usize {
assert_eq!(crc64_nvme(&data[..n]), byte_at_a_time(&data[..n]), "len {n}");
}
assert_eq!(crc64_nvme(&data), byte_at_a_time(&data));
let want = crc64_nvme(&data);
for cut in 0..data.len() {
let mut c = Crc64Nvme::new();
c.update(&data[..cut]);
c.update(&data[cut..]);
assert_eq!(c.finalize(), want, "split at {cut}");
}
let mut c = Crc64Nvme::new();
c.update(&data[..13]);
c.update(&data[13..457]);
c.update(&data[457..]);
assert_eq!(c.finalize(), want);
}
#[test]
fn crc64_b64_is_8_bytes_big_endian_base64() {
assert_eq!(crc64_to_b64(0xAE8B_1486_0A79_9888), "rosUhgp5mIg=");
assert_eq!(crc64_to_b64(0), "AAAAAAAAAAA=");
}
#[test]
fn stamps_roundtrip_and_reject_partial() {
let s = GenerationStamps { generation: 7, epoch: 3, flush_uuid: "u-1".into(), boundary_source: None, posix: None };
let m: HashMap<String, String> = s.to_meta().into_iter().collect();
assert_eq!(GenerationStamps::from_meta(&m), Some(s));
let mut partial = m.clone();
partial.remove(GenerationStamps::META_FLUSH_UUID);
assert_eq!(
GenerationStamps::from_meta(&partial),
None,
"an unstamped/partially-stamped object is foreign by definition"
);
}
#[test]
fn posix_stamps_roundtrip_and_stay_lenient() {
let p = PosixStamps { mode: 0o100644, uid: 501, gid: 20, mtime_unix: 1_723_000_000 };
let s = GenerationStamps {
generation: 2,
epoch: 1,
flush_uuid: "u-2".into(),
boundary_source: None,
posix: Some(p),
};
let m: HashMap<String, String> = s.to_meta().into_iter().collect();
assert_eq!(m.get(PosixStamps::META_MODE).map(String::as_str), Some("100644"));
assert_eq!(GenerationStamps::from_meta(&m), Some(s));
let mut old = m.clone();
old.remove(PosixStamps::META_UID);
let parsed = GenerationStamps::from_meta(&old).expect("identity stamps intact");
assert_eq!(parsed.posix, None);
}
}
#[cfg(test)]
mod combine_tests {
use super::{crc64_combine, crc64_nvme};
#[test]
fn combining_two_parts_equals_hashing_the_whole() {
let whole: Vec<u8> = (0..4096u32).map(|i| (i.wrapping_mul(2654435761) >> 13) as u8).collect();
for split in [0usize, 1, 2, 3, 7, 8, 9, 255, 256, 257, 1023, 2048, 4095, 4096] {
let (a, b) = whole.split_at(split);
assert_eq!(
crc64_combine(crc64_nvme(a), crc64_nvme(b), b.len() as u64),
crc64_nvme(&whole),
"split at {split}"
);
}
}
#[test]
fn combining_many_parts_folds_left_to_right() {
let whole: Vec<u8> = (0..10_000u32).map(|i| (i ^ (i >> 5)) as u8).collect();
let sizes = [1usize, 999, 4096, 1, 4903]; let mut off = 0usize;
let mut acc: Option<(u64, u64)> = None; for sz in sizes {
let part = &whole[off..off + sz];
let c = crc64_nvme(part);
acc = Some(match acc {
None => (c, sz as u64),
Some((pc, plen)) => (crc64_combine(pc, c, sz as u64), plen + sz as u64),
});
off += sz;
}
let (crc, len) = acc.unwrap();
assert_eq!(len, whole.len() as u64, "the grid must cover the object");
assert_eq!(crc, crc64_nvme(&whole), "folded parts must equal one pass");
}
#[test]
fn an_empty_tail_changes_nothing() {
let a = b"some bytes";
assert_eq!(crc64_combine(crc64_nvme(a), crc64_nvme(b""), 0), crc64_nvme(a));
}
}
#[cfg(test)]
mod fold_order_tests {
use super::{crc64_combine, crc64_nvme};
#[test]
fn folding_out_of_order_yields_a_different_object() {
let a = b"first-part-bytes";
let b = b"second";
let c = b"third-part!";
let whole = [&a[..], &b[..], &c[..]].concat();
let fold = |parts: &[&[u8]]| {
let mut acc: Option<u64> = None;
for p in parts {
let x = crc64_nvme(p);
acc = Some(match acc {
None => x,
Some(prev) => crc64_combine(prev, x, p.len() as u64),
});
}
acc.unwrap()
};
assert_eq!(fold(&[a, b, c]), crc64_nvme(&whole), "part order IS the object");
assert_ne!(
fold(&[b, a, c]),
crc64_nvme(&whole),
"a shuffled fold must NOT coincidentally agree, or the sort proves nothing"
);
}
}