use std::fmt;
use prikk_error::PrikkError;
use prikk_object::{BlobKind, BlobPayload, CanonicalEncode, NodeId, ObjectId, ObjectType};
pub(crate) const TEXT_ANCHOR_WINDOW: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TextSpanResolutionFailure {
AnchorMismatch,
NoMatchingSpanId,
Ambiguous,
}
impl fmt::Display for TextSpanResolutionFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::AnchorMismatch => "no occurrence's anchors matched the record",
Self::NoMatchingSpanId => "no anchor-filtered occurrence reproduced the span_id",
Self::Ambiguous => "more than one occurrence reproduced the span_id",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TextSpanSpliceError {
pub(crate) start: usize,
pub(crate) end: usize,
pub(crate) text_len: usize,
}
impl fmt::Display for TextSpanSpliceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid text-span splice range start={} end={} for text of length {}",
self.start, self.end, self.text_len
)
}
}
impl std::error::Error for TextSpanSpliceError {}
pub(crate) fn text_blob_id(content: &[u8]) -> Result<ObjectId, PrikkError> {
let payload = BlobPayload::new(BlobKind::Text, content.to_vec());
let bytes = payload.to_canonical_bytes()?;
Ok(ObjectId::from_canonical_payload(
ObjectType::Blob,
1,
&bytes,
))
}
pub(crate) fn left_anchor(text: &[u8], start: usize) -> [u8; 32] {
let lo = start.saturating_sub(TEXT_ANCHOR_WINDOW);
anchor_hash(
b"PRIKK-TEXT-LEFT-ANCHOR-v1",
text.get(lo..start).unwrap_or(&[]),
)
}
pub(crate) fn right_anchor(text: &[u8], end: usize) -> [u8; 32] {
let hi = end.saturating_add(TEXT_ANCHOR_WINDOW).min(text.len());
anchor_hash(
b"PRIKK-TEXT-RIGHT-ANCHOR-v1",
text.get(end..hi).unwrap_or(&[]),
)
}
fn anchor_hash(domain: &[u8], context: &[u8]) -> [u8; 32] {
let mut preimage = Vec::with_capacity(domain.len() + 4 + context.len());
preimage.extend_from_slice(domain);
preimage.extend_from_slice(&(context.len() as u32).to_be_bytes());
preimage.extend_from_slice(context);
prikk_hash::sha256(&preimage)
}
pub(crate) fn compute_span_id(
node_id: NodeId,
old_span_hash: &[u8; 32],
left: &[u8; 32],
right: &[u8; 32],
dup_index: u32,
) -> [u8; 32] {
let mut preimage = Vec::with_capacity(18 + 32 * 4 + 4);
preimage.extend_from_slice(b"PRIKK-TEXT-SPAN-v1");
preimage.extend_from_slice(node_id.as_bytes());
preimage.extend_from_slice(old_span_hash);
preimage.extend_from_slice(left);
preimage.extend_from_slice(right);
preimage.extend_from_slice(&dup_index.to_be_bytes());
prikk_hash::sha256(&preimage)
}
pub(crate) fn occurrences(text: &[u8], needle: &[u8]) -> Vec<usize> {
if needle.is_empty() {
return (0..=text.len()).collect();
}
let mut out = Vec::new();
let mut i = 0;
while i + needle.len() <= text.len() {
if text.get(i..i + needle.len()) == Some(needle) {
out.push(i);
}
i += 1;
}
out
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn locate_text_span(
text: &[u8],
old_span_text: &[u8],
record_left: &[u8; 32],
record_right: &[u8; 32],
record_span_id: &[u8; 32],
node_id: NodeId,
old_span_hash: &[u8; 32],
) -> Result<(usize, usize), TextSpanResolutionFailure> {
let span_len = old_span_text.len();
let anchor_matching: Vec<(usize, usize)> = occurrences(text, old_span_text)
.into_iter()
.map(|start| (start, start + span_len))
.filter(|&(start, end)| {
left_anchor(text, start) == *record_left && right_anchor(text, end) == *record_right
})
.collect();
if anchor_matching.is_empty() {
return Err(TextSpanResolutionFailure::AnchorMismatch);
}
let mut matches = Vec::new();
for (dup_index, &(start, end)) in anchor_matching.iter().enumerate() {
let sid = compute_span_id(
node_id,
old_span_hash,
record_left,
record_right,
dup_index as u32,
);
if sid == *record_span_id {
matches.push((start, end));
}
}
match matches.as_slice() {
[] => Err(TextSpanResolutionFailure::NoMatchingSpanId),
[one] => Ok(*one),
_ => Err(TextSpanResolutionFailure::Ambiguous),
}
}
pub(crate) fn splice_text(
text: &[u8],
start: usize,
end: usize,
replacement: &[u8],
) -> Result<Vec<u8>, TextSpanSpliceError> {
if start > end || end > text.len() {
return Err(TextSpanSpliceError {
start,
end,
text_len: text.len(),
});
}
let mut new_text = Vec::with_capacity(text.len() - (end - start) + replacement.len());
new_text.extend_from_slice(text.get(..start).unwrap_or(&[]));
new_text.extend_from_slice(replacement);
new_text.extend_from_slice(text.get(end..).unwrap_or(&[]));
Ok(new_text)
}
#[cfg(test)]
mod vectors;