use std::sync::OnceLock;
use crate::protocol::{ProtocolError, field_value_error};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LexicalForm {
AsSent,
#[cfg_attr(not(feature = "json-patch"), allow(dead_code))]
ClientDerived,
}
pub(crate) struct DiffDecoder {
pub(crate) tag: char,
pub(crate) apply: fn(base: &str, payload: &str) -> Result<String, ProtocolError>,
pub(crate) needs_base_as_sent: bool,
pub(crate) result_form: LexicalForm,
}
const TLCP_DIFF_DECODER: DiffDecoder = DiffDecoder {
tag: 'T',
apply: apply_tlcp_diff,
needs_base_as_sent: true,
result_form: LexicalForm::AsSent,
};
#[cfg(feature = "json-patch")]
const JSON_PATCH_DECODER: DiffDecoder = DiffDecoder {
tag: 'P',
apply: apply_json_patch,
needs_base_as_sent: false,
result_form: LexicalForm::ClientDerived,
};
#[cfg(feature = "json-patch")]
const DIFF_DECODERS: &[DiffDecoder] = &[TLCP_DIFF_DECODER, JSON_PATCH_DECODER];
#[cfg(not(feature = "json-patch"))]
const DIFF_DECODERS: &[DiffDecoder] = &[TLCP_DIFF_DECODER];
static SUPPORTED_DIFFS: OnceLock<String> = OnceLock::new();
#[must_use]
pub(crate) fn supported_diffs() -> &'static str {
SUPPORTED_DIFFS
.get_or_init(|| {
let mut value = String::new();
for decoder in DIFF_DECODERS {
if !value.is_empty() {
value.push(',');
}
value.push(decoder.tag);
}
value
})
.as_str()
}
#[must_use]
pub(crate) fn decoder_for(tag: char) -> Option<&'static DiffDecoder> {
DIFF_DECODERS.iter().find(|decoder| decoder.tag == tag)
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum DiffInstruction {
Copy(usize),
Add(String),
Del(usize),
}
fn apply_tlcp_diff(base: &str, payload: &str) -> Result<String, ProtocolError> {
reject_astral(base, "the base value")?;
reject_astral(payload, "the diff payload")?;
let base_chars: Vec<char> = base.chars().collect();
let instructions = parse_tlcp_diff(payload)?;
apply_tlcp_instructions(&base_chars, &instructions)
}
fn reject_astral(text: &str, role: &str) -> Result<(), ProtocolError> {
match text.chars().find(|character| character.len_utf16() == 2) {
None => Ok(()),
Some(character) => Err(field_value_error(format!(
"`^T` diff violates its own precondition: {role} contains `{character}` \
(U+{:04X}), whose UTF-16 encoding is a surrogate pair, so character positions \
cannot be counted the same way on both sides",
u32::from(character)
))),
}
}
fn parse_tlcp_diff(payload: &str) -> Result<Vec<DiffInstruction>, ProtocolError> {
let chars: Vec<char> = payload.chars().collect();
if chars.is_empty() {
return Err(field_value_error("TLCP-diff payload is empty"));
}
let mut instructions = Vec::new();
let mut position: usize = 0;
let mut section: u8 = 0;
while position < chars.len() {
let count = decode_encoded_integer(&chars, &mut position)?;
let instruction = match section {
0 => DiffInstruction::Copy(count),
1 => {
let end = position.checked_add(count).ok_or_else(|| {
field_value_error("TLCP-diff ADD length overflows the payload cursor")
})?;
let text = chars.get(position..end).ok_or_else(|| {
field_value_error(format!(
"TLCP-diff ADD at position {position} declares {count} characters but the \
payload is only {} characters long",
chars.len()
))
})?;
position = end;
DiffInstruction::Add(text.iter().collect())
}
_ => DiffInstruction::Del(count),
};
instructions.push(instruction);
section = if section >= 2 { 0 } else { section + 1 };
}
Ok(instructions)
}
fn decode_encoded_integer(chars: &[char], position: &mut usize) -> Result<usize, ProtocolError> {
let mut accumulator: usize = 0;
loop {
let letter = *chars.get(*position).ok_or_else(|| {
field_value_error(
"TLCP-diff encoded integer is truncated: no terminating lowercase letter",
)
})?;
*position += 1;
if letter.is_ascii_uppercase() {
let value = alphabet_index(letter, 'A')?;
accumulator = accumulator
.checked_mul(26)
.and_then(|scaled| scaled.checked_add(value.checked_add(1)?))
.ok_or_else(|| field_value_error("TLCP-diff encoded integer is out of range"))?;
continue;
}
if letter.is_ascii_lowercase() {
let value = alphabet_index(letter, 'a')?;
return accumulator
.checked_mul(26)
.and_then(|scaled| scaled.checked_add(value))
.ok_or_else(|| field_value_error("TLCP-diff encoded integer is out of range"));
}
return Err(field_value_error(format!(
"TLCP-diff encoded integer contains `{letter}`, which is not an ASCII letter"
)));
}
}
fn alphabet_index(letter: char, first: char) -> Result<usize, ProtocolError> {
u32::from(letter)
.checked_sub(u32::from(first))
.and_then(|offset| usize::try_from(offset).ok())
.ok_or_else(|| {
field_value_error(format!(
"TLCP-diff encoded integer contains `{letter}`, which is not an ASCII letter"
))
})
}
fn apply_tlcp_instructions(
base: &[char],
instructions: &[DiffInstruction],
) -> Result<String, ProtocolError> {
let mut result = String::new();
let mut base_pos: usize = 0;
for instruction in instructions {
match instruction {
DiffInstruction::Copy(count) => {
let end = base_pos
.checked_add(*count)
.ok_or_else(|| field_value_error("TLCP-diff COPY overflows the base cursor"))?;
let slice = base.get(base_pos..end).ok_or_else(|| {
field_value_error(format!(
"TLCP-diff COPY({count}) at position {base_pos} runs past the end of the \
{}-character base value",
base.len()
))
})?;
result.extend(slice.iter());
base_pos = end;
}
DiffInstruction::Add(text) => {
result.push_str(text);
}
DiffInstruction::Del(count) => {
base_pos = base_pos
.checked_add(*count)
.ok_or_else(|| field_value_error("TLCP-diff DEL overflows the base cursor"))?;
}
}
}
Ok(result)
}
#[cfg(feature = "json-patch")]
fn apply_json_patch(base: &str, payload: &str) -> Result<String, ProtocolError> {
let mut document: serde_json::Value = serde_json::from_str(base).map_err(|error| {
field_value_error(format!(
"`^P` diff applies to a field whose value is not valid JSON: {error}"
))
})?;
let patch: json_patch::Patch = serde_json::from_str(payload).map_err(|error| {
field_value_error(format!("`^P` diff is not a valid RFC 6902 patch: {error}"))
})?;
json_patch::patch(&mut document, &patch)
.map_err(|error| field_value_error(format!("`^P` diff failed to apply: {error}")))?;
Ok(document.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_tlcp_diff_appendix_d_section_table_p98() -> Result<(), ProtocolError> {
use DiffInstruction::{Add, Copy, Del};
assert_eq!(parse_tlcp_diff("d")?, vec![Copy(3)]);
assert_eq!(
parse_tlcp_diff("bdzap")?,
vec![Copy(1), Add("zap".to_owned())]
);
assert_eq!(
parse_tlcp_diff("bdzapcd")?,
vec![Copy(1), Add("zap".to_owned()), Del(2), Copy(3)]
);
assert_eq!(
parse_tlcp_diff("adzapad")?,
vec![Copy(0), Add("zap".to_owned()), Del(0), Copy(3)]
);
assert_eq!(
parse_tlcp_diff("AdacAa")?,
vec![Copy(29), Add(String::new()), Del(2), Copy(26)]
);
Ok(())
}
#[test]
fn test_decode_encoded_integer_matches_appendix_d_examples_p98() -> Result<(), ProtocolError> {
fn decode(text: &str) -> Result<usize, ProtocolError> {
let chars: Vec<char> = text.chars().collect();
let mut position = 0;
let value = decode_encoded_integer(&chars, &mut position)?;
assert_eq!(position, chars.len(), "consumed the whole integer");
Ok(value)
}
assert_eq!(decode("a")?, 0);
assert_eq!(decode("b")?, 1);
assert_eq!(decode("c")?, 2);
assert_eq!(decode("d")?, 3);
assert_eq!(decode("z")?, 25);
assert_eq!(decode("Aa")?, 26);
assert_eq!(decode("Ad")?, 29);
assert_eq!(decode("AAa")?, 702);
Ok(())
}
#[test]
fn test_apply_tlcp_instructions_appendix_d_application_table_p99() -> Result<(), ProtocolError>
{
use DiffInstruction::{Add, Copy, Del};
fn run(base: &str, instructions: &[DiffInstruction]) -> Result<String, ProtocolError> {
let chars: Vec<char> = base.chars().collect();
apply_tlcp_instructions(&chars, instructions)
}
assert_eq!(run("foo", &[Copy(3)])?, "foo");
assert_eq!(run("foobar", &[Copy(3)])?, "foo");
assert_eq!(run("foobar", &[Copy(1), Add("zap".to_owned())])?, "fzap");
assert_eq!(
run("foobar", &[Copy(1), Add("zap".to_owned()), Del(2), Copy(3)])?,
"fzapbar"
);
assert_eq!(
run("foobar", &[Copy(0), Add("zap".to_owned()), Del(0), Copy(3)])?,
"zapfoo"
);
assert_eq!(
run("foobar", &[Copy(2), Add(String::new()), Del(2), Copy(2)])?,
"foar"
);
Ok(())
}
#[test]
fn test_apply_tlcp_diff_end_to_end_over_both_appendix_d_tables_p98_p99()
-> Result<(), ProtocolError> {
assert_eq!(apply_tlcp_diff("foo", "d")?, "foo");
assert_eq!(apply_tlcp_diff("foobar", "d")?, "foo");
assert_eq!(apply_tlcp_diff("foobar", "bdzap")?, "fzap");
assert_eq!(apply_tlcp_diff("foobar", "bdzapcd")?, "fzapbar");
assert_eq!(apply_tlcp_diff("foobar", "adzapad")?, "zapfoo");
assert_eq!(apply_tlcp_diff("foobar", "cacc")?, "foar");
Ok(())
}
#[test]
fn test_apply_tlcp_diff_counts_characters_not_bytes() -> Result<(), ProtocolError> {
let base = "áéíóú";
assert_eq!(base.len(), 10);
assert_eq!(base.chars().count(), 5);
assert_eq!(apply_tlcp_diff(base, "cbñbc")?, "áéñóú");
Ok(())
}
#[test]
fn test_malformed_tlcp_diff_payloads_are_errors() {
assert!(apply_tlcp_diff("foobar", "").is_err());
assert!(apply_tlcp_diff("foobar", "AB").is_err());
assert!(apply_tlcp_diff("foobar", "3").is_err());
assert!(apply_tlcp_diff("foobar", "azz").is_err());
assert!(apply_tlcp_diff("foobar", "AdacAa").is_err());
}
#[test]
fn test_del_past_the_end_of_the_base_is_tolerated() -> Result<(), ProtocolError> {
assert_eq!(apply_tlcp_diff("foo", "daj")?, "foo");
Ok(())
}
#[test]
fn test_a_copy_after_an_overshooting_del_is_rejected() {
assert!(apply_tlcp_diff("foo", "daja").is_err());
assert!(apply_tlcp_diff("foo", "dajb").is_err());
}
#[test]
fn test_copy_boundaries_against_the_base() -> Result<(), ProtocolError> {
assert_eq!(apply_tlcp_diff("foo", "d")?, "foo");
assert!(apply_tlcp_diff("foo", "e").is_err());
assert_eq!(apply_tlcp_diff("", "a")?, "");
assert!(apply_tlcp_diff("", "b").is_err());
Ok(())
}
#[test]
fn test_tlcp_diff_rejects_a_base_outside_the_bmp() {
assert!(apply_tlcp_diff("😀bc", "d").is_err());
assert!(apply_tlcp_diff("ab😀", "b").is_err());
assert!(apply_tlcp_diff("a😀c", "bbz").is_err());
}
#[test]
fn test_tlcp_diff_rejects_a_payload_outside_the_bmp() {
assert!(apply_tlcp_diff("foo", "db😀").is_err());
}
#[test]
fn test_supported_diffs_is_derived_from_the_compiled_decoders() {
let advertised = supported_diffs();
let tags: Vec<&str> = advertised.split(',').collect();
assert_eq!(tags.len(), DIFF_DECODERS.len());
for (tag, decoder) in tags.iter().zip(DIFF_DECODERS) {
assert_eq!(*tag, decoder.tag.to_string());
}
}
#[cfg(feature = "json-patch")]
#[test]
fn test_supported_diffs_wire_form_with_json_patch() {
assert_eq!(supported_diffs(), "T,P");
}
#[cfg(not(feature = "json-patch"))]
#[test]
fn test_supported_diffs_wire_form_without_json_patch() {
assert_eq!(supported_diffs(), "T");
}
#[test]
fn test_every_advertised_tag_has_a_decoder() {
for tag in supported_diffs().chars().filter(|c| *c != ',') {
assert!(
decoder_for(tag).is_some(),
"advertised `{tag}` has no decoder"
);
}
}
#[test]
fn test_a_tag_that_was_never_advertised_has_no_decoder() {
assert!(decoder_for('Z').is_none());
assert!(decoder_for('t').is_none());
#[cfg(not(feature = "json-patch"))]
assert!(decoder_for('P').is_none());
}
}