use crate::config::Compression;
use crate::framer::{Codec, ObjectFramer};
use crate::offset::{self, Position};
use crate::split::SplitRange;
use crate::trim::RangeTrim;
use bytes::Bytes;
use spate_core::framing::RecordFramer;
use std::io;
use std::sync::Arc;
pub const MAX_ORDINAL: u32 = offset::MAX_ORDINAL;
pub const MAX_RECORD_INDEX: u64 = offset::MAX_RECORD_INDEX;
#[must_use]
pub fn encode_position(ordinal: u32, record: u64) -> Option<i64> {
Position { ordinal, record }.encode().ok()
}
#[must_use]
pub fn decode_position(offset: i64) -> (u32, u64) {
let position = Position::decode(offset);
(position.ordinal, position.record)
}
pub fn frame_object<M>(
compression: Compression,
key: &str,
chunks: &[&[u8]],
make_framer: M,
) -> (usize, io::Result<()>)
where
M: Fn() -> Box<dyn RecordFramer> + Send + Sync + 'static,
{
let mut framer = ObjectFramer::new(Arc::new(make_framer));
let mut records = 0;
let mut outcome = framer.begin_object(Codec::resolve(compression, key));
for chunk in chunks {
if outcome.is_err() {
break;
}
outcome = framer.push_chunk(Bytes::copy_from_slice(chunk));
loop {
while framer.pop_record().is_some() {
records += 1;
}
if outcome.is_err() || !framer.has_pending_input() {
break;
}
outcome = framer.pump();
}
}
if outcome.is_ok() {
outcome = framer.finish_object();
while framer.pop_record().is_some() {
records += 1;
}
}
(records, outcome)
}
#[must_use]
pub fn read_range(
object: &[u8],
start: u64,
end: u64,
delimiter: u8,
range_bytes: u64,
chunk_bytes: u64,
) -> Option<Vec<u8>> {
let size = object.len() as u64;
if start >= end || end > size {
return None;
}
let mut trim = RangeTrim::new(SplitRange::new(start, end, delimiter), size);
let mut pos = trim.first_byte();
let mut owned = Vec::new();
while let Some(window_end) = trim.next_window(pos, range_bytes, chunk_bytes) {
assert!(window_end > pos, "an empty window at byte {pos}");
let window = Bytes::copy_from_slice(&object[pos as usize..window_end as usize]);
owned.extend_from_slice(&trim.feed(pos, window));
pos = window_end;
}
Some(owned)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DecodedPart {
pub key: String,
pub etag: Option<String>,
pub range: Option<(u64, u64)>,
pub version: Option<String>,
pub re_enqueues: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DecodedUnit {
Read(Vec<DecodedPart>),
Acknowledge,
Fail,
}
#[must_use]
pub fn decode_unit(body: &[u8], bucket: &str, prefix: &str) -> DecodedUnit {
let scope = crate::unit::Scope {
bucket: Some(bucket.to_owned()),
prefix: object_store::path::Path::from(prefix),
};
let packing = crate::split::Packing {
target_bytes: u64::MAX,
compression: Compression::None,
delimiter: None,
};
match crate::unit::decode(body, &scope, packing) {
crate::unit::Decoded::Read(cursors) => DecodedUnit::Read(
cursors
.into_iter()
.flatten()
.flat_map(|part| {
let (version, re_enqueues) = match part.origin {
crate::unit::Origin::Notification {
version,
re_enqueues,
} => (version, re_enqueues),
crate::unit::Origin::Lister => (None, 0),
};
let range = part.range.map(|r| (r.start, r.end));
part.object
.iter()
.map(|object| DecodedPart {
key: object.key.clone(),
etag: object.etag.clone(),
range,
version: version.clone(),
re_enqueues,
})
.collect::<Vec<_>>()
})
.collect(),
),
crate::unit::Decoded::Acknowledge(_) => DecodedUnit::Acknowledge,
crate::unit::Decoded::Fail(_) => DecodedUnit::Fail,
}
}
#[must_use]
pub fn re_enqueue_body(
bucket: &str,
key: &str,
etag: Option<&str>,
size: u64,
last_modified_ms: i64,
re_enqueues: u32,
) -> Vec<u8> {
let overwrite = crate::fetch::Overwrite {
key: key.to_owned(),
etag: etag.map(str::to_owned),
size,
last_modified_ms,
};
crate::unit::re_enqueue_body(bucket, &overwrite, re_enqueues)
}
#[must_use]
pub fn quoted(etag: &str) -> String {
crate::unit::quoted(etag.to_owned())
}
#[must_use]
pub fn notification_body(
bucket: &str,
event_source: Option<&str>,
key: &str,
size: Option<u64>,
etag: Option<&str>,
version: Option<&str>,
re_enqueues: Option<u32>,
) -> Vec<u8> {
let mut object = serde_json::json!({ "key": key });
if let Some(size) = size {
object["size"] = size.into();
}
if let Some(etag) = etag {
object["eTag"] = etag.into();
}
if let Some(version) = version {
object["versionId"] = version.into();
}
let mut record = serde_json::json!({
"eventName": "ObjectCreated:Put",
"s3": { "bucket": { "name": bucket }, "object": object },
});
if let Some(n) = re_enqueues {
record["spateReEnqueues"] = n.into();
}
if let Some(source) = event_source {
record["eventSource"] = source.into();
}
serde_json::to_vec(&serde_json::json!({ "Records": [record] }))
.expect("a JSON value always serialises")
}
#[must_use]
pub fn addressable(key: &str) -> bool {
crate::listing::addressable(key)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::split::{DescriptorObject, SplitDescriptor};
#[test]
fn decode_unit_yields_every_member_of_a_descriptor() {
let member = |key: &str, etag: &str| DescriptorObject {
key: key.into(),
size: 10,
etag: Some(etag.into()),
last_modified_ms: 1,
};
let body = SplitDescriptor::new(vec![
member("data/a.ndjson", "\"a\""),
member("data/b.ndjson", "\"b\""),
])
.encode()
.unwrap();
let DecodedUnit::Read(parts) = decode_unit(&body, "bucket", "data") else {
panic!("the unit is read");
};
let keys: Vec<&str> = parts.iter().map(|p| p.key.as_str()).collect();
assert_eq!(keys, ["data/a.ndjson", "data/b.ndjson"]);
}
}