Files
rustpad/src/collab.rs
T
2026-08-05 09:53:28 +02:00

827 lines
27 KiB
Rust

/*
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, VecDeque},
error::Error,
fmt,
};
pub const MAX_OPERATION_COMPONENTS: usize = 4096;
const MAX_OPERATION_OWNER_SPANS: usize = 8192;
const MAX_OPERATION_INSERT_BYTES: usize = 2_000_000;
const MAX_OWNER_LENGTH: usize = 120;
const MAX_OPERATION_HISTORY: usize = 512;
const MAX_OPERATION_HISTORY_BYTES: usize = 8 * 1024 * 1024;
const AUTHORSHIP_VERSION: u8 = 2;
const OWNER_COLOR_SEPARATOR: char = '\u{001f}';
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct OwnerSpan {
pub start: usize,
pub end: usize,
pub owner: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum OperationComponent {
Retain {
count: usize,
},
Delete {
count: usize,
},
Insert {
text: String,
#[serde(default)]
owners: Vec<OwnerSpan>,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct TextOperation {
#[serde(default)]
pub components: Vec<OperationComponent>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct OwnerReplacement {
pub owner: String,
pub replacement: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct AppliedOperation {
pub base_revision_id: i64,
pub revision_id: i64,
pub client_id: String,
pub update_id: u64,
pub operation: TextOperation,
pub owner_replacements: Vec<OwnerReplacement>,
}
#[derive(Debug)]
pub struct CollaborativeDocument {
pub content: String,
pub owner_map: String,
pub revision_id: i64,
history: VecDeque<AppliedOperation>,
history_bytes: usize,
acknowledged_updates: HashMap<String, u64>,
}
impl CollaborativeDocument {
pub fn new(content: String, owner_map: String, revision_id: i64) -> Self {
Self {
content,
owner_map,
revision_id,
history: VecDeque::new(),
history_bytes: 0,
acknowledged_updates: HashMap::new(),
}
}
pub fn transform_from(
&self,
base_revision_id: i64,
operation: &TextOperation,
client_id: &str,
update_id: u64,
) -> Result<TextOperation, OperationError> {
let mut transformed = normalize_operation(operation)?;
if base_revision_id == self.revision_id {
return Ok(transformed);
}
let Some(start) = self
.history
.iter()
.position(|entry| entry.base_revision_id == base_revision_id)
else {
return Err(OperationError::RevisionUnavailable);
};
let mut expected_revision = base_revision_id;
for applied in self.history.iter().skip(start) {
if applied.base_revision_id != expected_revision {
return Err(OperationError::RevisionUnavailable);
}
let incoming_has_priority =
operation_key_before(client_id, update_id, &applied.client_id, applied.update_id);
transformed =
transform_operation(&transformed, &applied.operation, incoming_has_priority)?;
expected_revision = applied.revision_id;
if expected_revision == self.revision_id {
return Ok(transformed);
}
}
Err(OperationError::RevisionUnavailable)
}
pub fn acknowledge(&mut self, client_id: &str, update_id: u64) {
self.acknowledged_updates
.entry(client_id.to_owned())
.and_modify(|acknowledged| *acknowledged = (*acknowledged).max(update_id))
.or_insert(update_id);
}
pub fn has_applied_update(&self, client_id: &str, update_id: u64) -> bool {
self.acknowledged_updates
.get(client_id)
.is_some_and(|acknowledged| update_id <= *acknowledged)
}
pub fn acknowledged_updates(&self, client_id: &str) -> Vec<u64> {
self.acknowledged_updates
.get(client_id)
.copied()
.into_iter()
.collect()
}
pub fn operations_after(&self, revision_id: i64) -> Option<Vec<AppliedOperation>> {
if revision_id == self.revision_id {
return Some(Vec::new());
}
let start = self
.history
.iter()
.position(|entry| entry.base_revision_id == revision_id)?;
let mut expected_revision = revision_id;
let mut operations = Vec::new();
for applied in self.history.iter().skip(start) {
if applied.base_revision_id != expected_revision {
return None;
}
operations.push(applied.clone());
expected_revision = applied.revision_id;
if expected_revision == self.revision_id {
return Some(operations);
}
}
None
}
pub fn record(&mut self, operation: AppliedOperation) {
self.acknowledge(&operation.client_id, operation.update_id);
self.history_bytes = self
.history_bytes
.saturating_add(applied_operation_size(&operation));
self.history.push_back(operation);
while self.history.len() > MAX_OPERATION_HISTORY
|| self.history_bytes > MAX_OPERATION_HISTORY_BYTES
{
let Some(removed) = self.history.pop_front() else {
break;
};
self.history_bytes = self
.history_bytes
.saturating_sub(applied_operation_size(&removed));
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ComponentKind {
Retain,
Delete,
Insert,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OperationError {
InvalidComponent,
InvalidUtf16Boundary,
LengthMismatch,
TooManyComponents,
RevisionUnavailable,
Serialization,
}
impl fmt::Display for OperationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::InvalidComponent => "invalid text operation component",
Self::InvalidUtf16Boundary => "text operation splits a UTF-16 character",
Self::LengthMismatch => "text operation length does not match the document",
Self::TooManyComponents => "text operation contains too many components",
Self::RevisionUnavailable => "the base revision is no longer available",
Self::Serialization => "invalid authorship metadata",
};
formatter.write_str(message)
}
}
impl Error for OperationError {}
fn applied_operation_size(operation: &AppliedOperation) -> usize {
let components = operation
.operation
.components
.iter()
.map(|component| match component {
OperationComponent::Retain { .. } | OperationComponent::Delete { .. } => 24,
OperationComponent::Insert { text, owners } => {
32usize.saturating_add(text.len()).saturating_add(
owners
.iter()
.map(|span| 24usize.saturating_add(span.owner.len()))
.sum::<usize>(),
)
}
})
.sum::<usize>();
components
.saturating_add(operation.client_id.len())
.saturating_add(
operation
.owner_replacements
.iter()
.map(|replacement| replacement.owner.len() + replacement.replacement.len() + 16)
.sum::<usize>(),
)
}
fn operation_key_before(
left_client_id: &str,
left_update_id: u64,
right_client_id: &str,
right_update_id: u64,
) -> bool {
left_client_id < right_client_id
|| (left_client_id == right_client_id && left_update_id < right_update_id)
}
fn component_kind(component: &OperationComponent) -> ComponentKind {
match component {
OperationComponent::Retain { .. } => ComponentKind::Retain,
OperationComponent::Delete { .. } => ComponentKind::Delete,
OperationComponent::Insert { .. } => ComponentKind::Insert,
}
}
fn component_length(component: &OperationComponent) -> usize {
match component {
OperationComponent::Retain { count } | OperationComponent::Delete { count } => *count,
OperationComponent::Insert { text, .. } => text.encode_utf16().count(),
}
}
fn normalize_owner_spans(spans: &[OwnerSpan], length: usize) -> Vec<OwnerSpan> {
let mut sorted = spans
.iter()
.filter_map(|span| {
let start = span.start.min(length);
let end = span.end.min(length).max(start);
if span.owner.is_empty() || end <= start {
None
} else {
Some(OwnerSpan {
start,
end,
owner: span.owner.clone(),
})
}
})
.collect::<Vec<_>>();
sorted.sort_by_key(|span| (span.start, span.end));
let mut result: Vec<OwnerSpan> = Vec::new();
for mut span in sorted {
if let Some(previous) = result.last_mut() {
if previous.owner == span.owner && span.start <= previous.end {
previous.end = previous.end.max(span.end);
continue;
}
if span.start < previous.end {
span.start = previous.end;
}
}
if span.end > span.start {
result.push(span);
}
}
result
}
fn slice_owner_spans(spans: &[OwnerSpan], start: usize, length: usize) -> Vec<OwnerSpan> {
let end = start.saturating_add(length);
let sliced = spans
.iter()
.filter_map(|span| {
let overlap_start = start.max(span.start);
let overlap_end = end.min(span.end);
(overlap_end > overlap_start).then(|| OwnerSpan {
start: overlap_start - start,
end: overlap_end - start,
owner: span.owner.clone(),
})
})
.collect::<Vec<_>>();
normalize_owner_spans(&sliced, length)
}
fn shift_owner_spans(spans: &[OwnerSpan], offset: usize) -> Vec<OwnerSpan> {
spans
.iter()
.map(|span| OwnerSpan {
start: span.start + offset,
end: span.end + offset,
owner: span.owner.clone(),
})
.collect()
}
fn append_component(
components: &mut Vec<OperationComponent>,
component: OperationComponent,
) -> Result<(), OperationError> {
match component {
OperationComponent::Retain { count } => {
if count == 0 {
return Ok(());
}
if let Some(OperationComponent::Retain { count: previous }) = components.last_mut() {
*previous = previous
.checked_add(count)
.ok_or(OperationError::InvalidComponent)?;
} else {
components.push(OperationComponent::Retain { count });
}
}
OperationComponent::Delete { count } => {
if count == 0 {
return Ok(());
}
if let Some(OperationComponent::Delete { count: previous }) = components.last_mut() {
*previous = previous
.checked_add(count)
.ok_or(OperationError::InvalidComponent)?;
} else {
components.push(OperationComponent::Delete { count });
}
}
OperationComponent::Insert { text, owners } => {
let length = text.encode_utf16().count();
if length == 0 {
return Ok(());
}
let owners = normalize_owner_spans(&owners, length);
if let Some(OperationComponent::Insert {
text: previous_text,
owners: previous_owners,
}) = components.last_mut()
{
let offset = previous_text.encode_utf16().count();
previous_text.push_str(&text);
previous_owners.extend(shift_owner_spans(&owners, offset));
*previous_owners =
normalize_owner_spans(previous_owners, previous_text.encode_utf16().count());
} else {
components.push(OperationComponent::Insert { text, owners });
}
}
}
if components.len() > MAX_OPERATION_COMPONENTS {
return Err(OperationError::TooManyComponents);
}
Ok(())
}
pub fn normalize_operation(operation: &TextOperation) -> Result<TextOperation, OperationError> {
if operation.components.len() > MAX_OPERATION_COMPONENTS {
return Err(OperationError::TooManyComponents);
}
let mut inserted_bytes = 0usize;
let mut owner_span_count = 0usize;
let mut components = Vec::with_capacity(operation.components.len());
for component in &operation.components {
if let OperationComponent::Insert { text, owners } = component {
inserted_bytes = inserted_bytes
.checked_add(text.len())
.ok_or(OperationError::InvalidComponent)?;
owner_span_count = owner_span_count
.checked_add(owners.len())
.ok_or(OperationError::InvalidComponent)?;
let text_length = text.encode_utf16().count();
if inserted_bytes > MAX_OPERATION_INSERT_BYTES
|| owner_span_count > MAX_OPERATION_OWNER_SPANS
|| owners.iter().any(|span| {
span.start > span.end
|| span.end > text_length
|| utf16_byte_index(text, span.start).is_err()
|| utf16_byte_index(text, span.end).is_err()
|| span.owner.chars().count() > MAX_OWNER_LENGTH
|| span.owner.chars().any(|character| {
character.is_control() && character != OWNER_COLOR_SEPARATOR
})
})
{
return Err(OperationError::InvalidComponent);
}
}
append_component(&mut components, component.clone())?;
}
Ok(TextOperation { components })
}
pub fn operation_base_length(operation: &TextOperation) -> Result<usize, OperationError> {
normalize_operation(operation)?
.components
.iter()
.try_fold(0usize, |length, component| {
let component_length = match component {
OperationComponent::Retain { count } | OperationComponent::Delete { count } => {
*count
}
OperationComponent::Insert { .. } => 0,
};
length
.checked_add(component_length)
.ok_or(OperationError::InvalidComponent)
})
}
pub fn operation_from_edit(
previous_content: &str,
next_content: &str,
next_owner_map: &str,
) -> TextOperation {
let mut previous_prefix_bytes = 0usize;
let mut next_prefix_bytes = 0usize;
for (previous, next) in previous_content.chars().zip(next_content.chars()) {
if previous != next {
break;
}
previous_prefix_bytes += previous.len_utf8();
next_prefix_bytes += next.len_utf8();
}
let previous_remainder = &previous_content[previous_prefix_bytes..];
let next_remainder = &next_content[next_prefix_bytes..];
let mut previous_suffix_bytes = 0usize;
let mut next_suffix_bytes = 0usize;
for (previous, next) in previous_remainder
.chars()
.rev()
.zip(next_remainder.chars().rev())
{
if previous != next {
break;
}
previous_suffix_bytes += previous.len_utf8();
next_suffix_bytes += next.len_utf8();
}
let previous_middle_end = previous_content.len() - previous_suffix_bytes;
let next_middle_end = next_content.len() - next_suffix_bytes;
let previous_prefix = &previous_content[..previous_prefix_bytes];
let previous_middle = &previous_content[previous_prefix_bytes..previous_middle_end];
let next_middle = &next_content[next_prefix_bytes..next_middle_end];
let suffix = &previous_content[previous_middle_end..];
let prefix_length = previous_prefix.encode_utf16().count();
let deleted_length = previous_middle.encode_utf16().count();
let inserted_length = next_middle.encode_utf16().count();
let suffix_length = suffix.encode_utf16().count();
let next_authorship = parse_authorship(next_content, next_owner_map);
let inserted_owners = slice_owner_spans(&next_authorship.spans, prefix_length, inserted_length);
let mut components = Vec::new();
if prefix_length > 0 {
components.push(OperationComponent::Retain {
count: prefix_length,
});
}
if deleted_length > 0 {
components.push(OperationComponent::Delete {
count: deleted_length,
});
}
if !next_middle.is_empty() {
components.push(OperationComponent::Insert {
text: next_middle.to_owned(),
owners: inserted_owners,
});
}
if suffix_length > 0 {
components.push(OperationComponent::Retain {
count: suffix_length,
});
}
TextOperation { components }
}
pub fn replace_operation(
base_length: usize,
content: String,
owners: Vec<OwnerSpan>,
) -> TextOperation {
let mut components = Vec::new();
if base_length > 0 {
components.push(OperationComponent::Delete { count: base_length });
}
if !content.is_empty() {
components.push(OperationComponent::Insert {
text: content,
owners,
});
}
TextOperation { components }
}
struct OperationCursor {
components: Vec<OperationComponent>,
index: usize,
offset: usize,
}
impl OperationCursor {
fn new(operation: &TextOperation) -> Result<Self, OperationError> {
Ok(Self {
components: normalize_operation(operation)?.components,
index: 0,
offset: 0,
})
}
fn current(&self) -> Option<&OperationComponent> {
self.components.get(self.index)
}
fn kind(&self) -> Option<ComponentKind> {
self.current().map(component_kind)
}
fn remaining(&self) -> usize {
self.current()
.map(|component| component_length(component).saturating_sub(self.offset))
.unwrap_or(0)
}
fn take(&mut self, count: usize) -> Result<OperationComponent, OperationError> {
let component = self
.current()
.cloned()
.ok_or(OperationError::InvalidComponent)?;
if count == 0 || count > self.remaining() {
return Err(OperationError::InvalidComponent);
}
let component_length = component_length(&component);
let part = match component {
OperationComponent::Retain { .. } => OperationComponent::Retain { count },
OperationComponent::Delete { .. } => OperationComponent::Delete { count },
OperationComponent::Insert { text, owners } => OperationComponent::Insert {
text: slice_utf16(&text, self.offset, count)?.to_owned(),
owners: slice_owner_spans(&owners, self.offset, count),
},
};
self.offset += count;
if self.offset == component_length {
self.index += 1;
self.offset = 0;
}
Ok(part)
}
fn take_remaining(&mut self) -> Result<OperationComponent, OperationError> {
let count = self.remaining();
self.take(count)
}
}
pub fn transform_operation(
left_operation: &TextOperation,
right_operation: &TextOperation,
left_before_right: bool,
) -> Result<TextOperation, OperationError> {
if operation_base_length(left_operation)? != operation_base_length(right_operation)? {
return Err(OperationError::LengthMismatch);
}
let mut left = OperationCursor::new(left_operation)?;
let mut right = OperationCursor::new(right_operation)?;
let mut left_prime = Vec::new();
while left.current().is_some() || right.current().is_some() {
if left.kind() == Some(ComponentKind::Insert)
&& (right.kind() != Some(ComponentKind::Insert) || left_before_right)
{
append_component(&mut left_prime, left.take_remaining()?)?;
continue;
}
if right.kind() == Some(ComponentKind::Insert) {
let count = right.remaining();
right.take_remaining()?;
append_component(&mut left_prime, OperationComponent::Retain { count })?;
continue;
}
let (Some(left_kind), Some(right_kind)) = (left.kind(), right.kind()) else {
return Err(OperationError::InvalidComponent);
};
let count = left.remaining().min(right.remaining());
match (left_kind, right_kind) {
(ComponentKind::Retain, ComponentKind::Retain) => {
append_component(&mut left_prime, OperationComponent::Retain { count })?;
}
(ComponentKind::Delete, ComponentKind::Retain) => {
append_component(&mut left_prime, OperationComponent::Delete { count })?;
}
(ComponentKind::Retain, ComponentKind::Delete)
| (ComponentKind::Delete, ComponentKind::Delete) => {}
_ => return Err(OperationError::InvalidComponent),
}
left.take(count)?;
right.take(count)?;
}
Ok(TextOperation {
components: left_prime,
})
}
fn utf16_byte_index(value: &str, offset: usize) -> Result<usize, OperationError> {
if offset == 0 {
return Ok(0);
}
let mut current = 0usize;
for (byte_index, character) in value.char_indices() {
if current == offset {
return Ok(byte_index);
}
current += character.len_utf16();
if current > offset {
return Err(OperationError::InvalidUtf16Boundary);
}
}
if current == offset {
Ok(value.len())
} else {
Err(OperationError::LengthMismatch)
}
}
fn slice_utf16(value: &str, start: usize, length: usize) -> Result<&str, OperationError> {
let start_byte = utf16_byte_index(value, start)?;
let end_byte = utf16_byte_index(value, start.saturating_add(length))?;
value
.get(start_byte..end_byte)
.ok_or(OperationError::InvalidUtf16Boundary)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AuthorshipModel {
#[serde(default = "authorship_version")]
version: u8,
#[serde(default)]
spans: Vec<OwnerSpan>,
}
fn authorship_version() -> u8 {
AUTHORSHIP_VERSION
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum RawAuthorship {
Model(AuthorshipModel),
LineOwners(Vec<String>),
}
fn parse_authorship(content: &str, raw: &str) -> AuthorshipModel {
let length = content.encode_utf16().count();
match serde_json::from_str::<RawAuthorship>(raw) {
Ok(RawAuthorship::Model(model)) if model.version == AUTHORSHIP_VERSION => AuthorshipModel {
version: AUTHORSHIP_VERSION,
spans: normalize_owner_spans(&model.spans, length),
},
Ok(RawAuthorship::LineOwners(owners)) => {
let lines = content.split('\n').collect::<Vec<_>>();
let mut offset = 0usize;
let mut spans = Vec::new();
for (index, line) in lines.iter().enumerate() {
let line_length =
line.encode_utf16().count() + usize::from(index + 1 < lines.len());
let owner = owners.get(index).cloned().unwrap_or_default();
if !owner.is_empty() && line_length > 0 {
spans.push(OwnerSpan {
start: offset,
end: offset + line_length,
owner,
});
}
offset += line_length;
}
AuthorshipModel {
version: AUTHORSHIP_VERSION,
spans: normalize_owner_spans(&spans, length),
}
}
_ => AuthorshipModel {
version: AUTHORSHIP_VERSION,
spans: Vec::new(),
},
}
}
fn copy_retained_spans(
target: &mut Vec<OwnerSpan>,
spans: &[OwnerSpan],
source_start: usize,
length: usize,
output_start: usize,
) {
let source_end = source_start + length;
for span in spans {
let start = source_start.max(span.start);
let end = source_end.min(span.end);
if end > start {
target.push(OwnerSpan {
start: output_start + start - source_start,
end: output_start + end - source_start,
owner: span.owner.clone(),
});
}
}
}
pub fn apply_operation_to_document(
content: &str,
owner_map: &str,
operation: &TextOperation,
owner_replacements: &[OwnerReplacement],
) -> Result<(String, String), OperationError> {
let operation = normalize_operation(operation)?;
let content_length = content.encode_utf16().count();
if operation_base_length(&operation)? != content_length {
return Err(OperationError::LengthMismatch);
}
let source_model = parse_authorship(content, owner_map);
let mut output_spans = Vec::new();
let mut source_offset = 0usize;
let mut output_offset = 0usize;
let mut output_content = String::new();
for component in &operation.components {
match component {
OperationComponent::Retain { count } => {
output_content.push_str(slice_utf16(content, source_offset, *count)?);
copy_retained_spans(
&mut output_spans,
&source_model.spans,
source_offset,
*count,
output_offset,
);
source_offset += *count;
output_offset += *count;
}
OperationComponent::Delete { count } => {
source_offset += *count;
}
OperationComponent::Insert { text, owners } => {
output_content.push_str(text);
output_spans.extend(shift_owner_spans(owners, output_offset));
output_offset += text.encode_utf16().count();
}
}
}
if source_offset != content_length {
return Err(OperationError::LengthMismatch);
}
for span in &mut output_spans {
let identity = span
.owner
.split(OWNER_COLOR_SEPARATOR)
.next()
.unwrap_or_default();
if let Some(replacement) = owner_replacements.iter().find(|replacement| {
replacement.owner == identity && !replacement.replacement.is_empty()
}) {
span.owner.clone_from(&replacement.replacement);
}
}
let model = AuthorshipModel {
version: AUTHORSHIP_VERSION,
spans: normalize_owner_spans(&output_spans, output_offset),
};
let owner_map = serde_json::to_string(&model).map_err(|_| OperationError::Serialization)?;
Ok((output_content, owner_map))
}
pub fn owner_spans_from_map(content: &str, owner_map: &str) -> Vec<OwnerSpan> {
parse_authorship(content, owner_map).spans
}
#[cfg(test)]
#[path = "tests/collab.rs"]
mod tests;