#![allow(clippy::unwrap_used)]
use std::pin::Pin;
use std::task::{Context, Poll};
use std::io::Write as _;
use proptest::prelude::*;
use tokio::io::AsyncWrite;
use super::*;
use crate::svndiff::{SvndiffVersion as EncVersion, encode_fulltext_with_options};
use crate::test_support::run_async;
#[derive(Default)]
struct VecWriter {
buf: Vec<u8>,
}
impl AsyncWrite for VecWriter {
fn poll_write(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
self.buf.extend_from_slice(buf);
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
}
fn svndiff0_window(
sview_offset: u8,
sview_len: u8,
tview_len: u8,
instructions: &[u8],
new_data: &[u8],
) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(b"SVN\0");
out.push(sview_offset);
out.push(sview_len);
out.push(tview_len);
out.push(instructions.len() as u8);
out.push(new_data.len() as u8);
out.extend_from_slice(instructions);
out.extend_from_slice(new_data);
out
}
fn encode_uint_for_test(val: u64, out: &mut Vec<u8>) {
let mut v = val >> 7;
let mut n = 1u32;
while v > 0 {
v >>= 7;
n += 1;
}
while n > 1 {
n -= 1;
out.push((((val >> (n * 7)) | 0x80) & 0xff) as u8);
}
out.push((val & 0x7f) as u8);
}
fn svndiff1_window(tview_len: u64, instructions_wire: &[u8], newdata_wire: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(b"SVN\x01");
encode_uint_for_test(0, &mut out);
encode_uint_for_test(0, &mut out);
encode_uint_for_test(tview_len, &mut out);
encode_uint_for_test(instructions_wire.len() as u64, &mut out);
encode_uint_for_test(newdata_wire.len() as u64, &mut out);
out.extend_from_slice(instructions_wire);
out.extend_from_slice(newdata_wire);
out
}
fn zlib_section_with_trailing_data(data: &[u8]) -> Vec<u8> {
let mut compressed = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(6));
compressed.write_all(data).unwrap();
let compressed = compressed.finish().unwrap();
let mut out = Vec::new();
encode_uint_for_test(data.len() as u64, &mut out);
out.extend_from_slice(&compressed);
out.extend_from_slice(b"trailing");
out
}
fn split_slices_by_seeds<'a>(bytes: &'a [u8], seeds: &[u8]) -> Vec<&'a [u8]> {
if bytes.is_empty() {
return vec![bytes];
}
let mut out = Vec::new();
let mut start = 0usize;
for &seed in seeds {
if start == bytes.len() {
break;
}
let remaining = bytes.len() - start;
let take = (seed as usize) % (remaining + 1);
out.push(&bytes[start..start + take]);
start += take;
}
if start < bytes.len() {
out.push(&bytes[start..]);
}
out
}
fn arb_svndiff_version() -> impl Strategy<Value = EncVersion> {
prop_oneof![
Just(EncVersion::V0),
Just(EncVersion::V1),
Just(EncVersion::V2),
]
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 32,
.. ProptestConfig::default()
})]
#[test]
fn fulltext_svndiff_applies_via_sync_textdelta(
version in arb_svndiff_version(),
zlib_level in 0u32..=9u32,
window_size in 1usize..=8192,
base in prop::collection::vec(any::<u8>(), 0..=1024),
contents in prop::collection::vec(any::<u8>(), 0..=16 * 1024),
seeds in prop::collection::vec(any::<u8>(), 0..=64),
) {
let delta = encode_fulltext_with_options(version, &contents, zlib_level, window_size).unwrap();
let chunks = split_slices_by_seeds(&delta, &seeds);
let mut out = Vec::new();
apply_textdelta_sync(&base, chunks.iter().copied(), &mut out).unwrap();
assert_eq!(out, contents);
}
#[test]
fn truncated_svndiff_is_rejected(
version in arb_svndiff_version(),
zlib_level in 0u32..=9u32,
window_size in 1usize..=8192,
contents in prop::collection::vec(any::<u8>(), 0..=16 * 1024),
) {
let delta = encode_fulltext_with_options(version, &contents, zlib_level, window_size).unwrap();
prop_assume!(delta.len() > 1);
let mut out = Vec::new();
let err = apply_textdelta_sync(&[], [&delta[..delta.len() - 1]], &mut out).unwrap_err();
prop_assert!(matches!(err, SvnError::Protocol(_)));
}
#[test]
fn apply_textdelta_sync_never_panics_on_random_input(
base in prop::collection::vec(any::<u8>(), 0..=256),
delta in prop::collection::vec(any::<u8>(), 0..=2048),
) {
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut out = Vec::new();
let _ = apply_textdelta_sync(&base, [&delta], &mut out);
}));
prop_assert!(res.is_ok());
}
}
#[test]
fn apply_svndiff0_source_and_new() {
run_async(async {
let base = b"abcdef";
let delta = svndiff0_window(
0,
6,
6,
&[
0x02, 0x00, 0x82, 0x02, 0x04, ],
b"XY",
);
let mut out = VecWriter::default();
let mut applier = TextDeltaApplier::new(base);
applier.push(&delta[..3], &mut out).await.unwrap();
applier.push(&delta[3..], &mut out).await.unwrap();
applier.finish(&mut out).await.unwrap();
assert_eq!(out.buf, b"abXYef");
});
}
#[test]
fn apply_svndiff0_target_copy_with_overlap() {
run_async(async {
let delta = svndiff0_window(
0,
0,
6,
&[
0x81, 0x45, 0x00, ],
b"a",
);
let mut out = VecWriter::default();
apply_textdelta(&[], [&delta[..]], &mut out).await.unwrap();
assert_eq!(out.buf, b"aaaaaa");
});
}
#[test]
fn apply_empty_delta_is_identity() {
run_async(async {
let mut out = VecWriter::default();
apply_textdelta(b"base", std::iter::empty::<&[u8]>(), &mut out)
.await
.unwrap();
assert_eq!(out.buf, b"base");
});
}
#[test]
fn apply_svndiff1_fulltext_roundtrips() {
run_async(async {
let contents = vec![0u8; 4096];
let delta = encode_fulltext_with_options(EncVersion::V1, &contents, 5, 64 * 1024).unwrap();
let mut out = VecWriter::default();
let split = (delta.len() / 2).max(1).min(delta.len());
apply_textdelta(&[], [&delta[..split], &delta[split..]], &mut out)
.await
.unwrap();
assert_eq!(out.buf, contents);
});
}
#[test]
fn apply_svndiff1_rejects_trailing_zlib_section_data() {
let mut instructions_wire = Vec::new();
encode_uint_for_test(1, &mut instructions_wire);
instructions_wire.push(0x80 | 3);
let newdata_wire = zlib_section_with_trailing_data(b"abc");
let delta = svndiff1_window(3, &instructions_wire, &newdata_wire);
let mut out = Vec::new();
let err = apply_textdelta_sync(&[], [&delta], &mut out).unwrap_err();
assert!(matches!(err, SvnError::Protocol(message) if message.contains("trailing data")));
}
#[test]
fn apply_svndiff2_fulltext_roundtrips() {
run_async(async {
let contents = vec![0u8; 4096];
let delta = encode_fulltext_with_options(EncVersion::V2, &contents, 5, 64 * 1024).unwrap();
let mut out = VecWriter::default();
let split = (delta.len() / 3).max(1).min(delta.len());
let split2 = (split * 2).min(delta.len());
apply_textdelta(
&[],
[&delta[..split], &delta[split..split2], &delta[split2..]],
&mut out,
)
.await
.unwrap();
assert_eq!(out.buf, contents);
});
}
#[test]
fn recorder_tracks_chunks_and_checksums() {
let mut recorder = TextDeltaRecorder::new();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::OpenFile {
path: "trunk/hello.txt".to_string(),
dir_token: "d1".to_string(),
file_token: "f1".to_string(),
rev: 1,
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::ApplyTextDelta {
file_token: "f1".to_string(),
base_checksum: Some("base".to_string()),
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::TextDeltaChunk {
file_token: "f1".to_string(),
chunk: vec![1, 2, 3],
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::TextDeltaEnd {
file_token: "f1".to_string(),
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::CloseFile {
file_token: "f1".to_string(),
text_checksum: Some("text".to_string()),
},
)
.unwrap();
assert_eq!(recorder.completed().len(), 1);
let d = &recorder.completed()[0];
assert_eq!(d.path.as_deref(), Some("trunk/hello.txt"));
assert_eq!(d.file_token, "f1");
assert_eq!(d.base_checksum.as_deref(), Some("base"));
assert_eq!(d.text_checksum.as_deref(), Some("text"));
assert_eq!(d.chunks, vec![vec![1, 2, 3]]);
}
#[test]
fn recorder_does_not_apply_stale_checksum_when_file_token_is_reused() {
let mut recorder = TextDeltaRecorder::new();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::OpenFile {
path: "trunk/old.txt".to_string(),
dir_token: "d1".to_string(),
file_token: "f1".to_string(),
rev: 1,
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::ApplyTextDelta {
file_token: "f1".to_string(),
base_checksum: None,
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::TextDeltaChunk {
file_token: "f1".to_string(),
chunk: vec![1],
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::TextDeltaEnd {
file_token: "f1".to_string(),
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::CloseFile {
file_token: "f1".to_string(),
text_checksum: Some("old".to_string()),
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::OpenFile {
path: "trunk/new.txt".to_string(),
dir_token: "d1".to_string(),
file_token: "f1".to_string(),
rev: 2,
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::CloseFile {
file_token: "f1".to_string(),
text_checksum: Some("new".to_string()),
},
)
.unwrap();
assert_eq!(recorder.completed().len(), 1);
assert_eq!(
recorder.completed()[0].text_checksum.as_deref(),
Some("old")
);
}
#[test]
fn recorder_rejects_close_file_for_unknown_token() {
let mut recorder = TextDeltaRecorder::new();
let err = crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::CloseFile {
file_token: "missing".to_string(),
text_checksum: None,
},
)
.unwrap_err();
assert!(matches!(err, SvnError::Protocol(message) if message.contains("unknown file token")));
}
#[test]
fn recorder_rejects_close_edit_with_unclosed_file() {
let mut recorder = TextDeltaRecorder::new();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::OpenFile {
path: "trunk/hello.txt".to_string(),
dir_token: "d1".to_string(),
file_token: "f1".to_string(),
rev: 1,
},
)
.unwrap();
let err = crate::editor::EditorEventHandler::on_event(&mut recorder, EditorEvent::CloseEdit)
.unwrap_err();
assert!(matches!(err, SvnError::Protocol(message) if message.contains("unclosed file")));
}
#[test]
fn recorder_clears_pending_state_on_abort_edit() {
let mut recorder = TextDeltaRecorder::new();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::OpenFile {
path: "trunk/hello.txt".to_string(),
dir_token: "d1".to_string(),
file_token: "f1".to_string(),
rev: 1,
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::ApplyTextDelta {
file_token: "f1".to_string(),
base_checksum: None,
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::TextDeltaChunk {
file_token: "f1".to_string(),
chunk: vec![1],
},
)
.unwrap();
crate::editor::EditorEventHandler::on_event(&mut recorder, EditorEvent::AbortEdit).unwrap();
assert!(recorder.completed().is_empty());
let err = crate::editor::EditorEventHandler::on_event(
&mut recorder,
EditorEvent::TextDeltaEnd {
file_token: "f1".to_string(),
},
)
.unwrap_err();
assert!(matches!(err, SvnError::Protocol(message) if message.contains("unknown file token")));
}