use std::io::Write;
use flate2::Compression;
use flate2::write::ZlibEncoder;
use crate::SvnError;
const ZLIB_MIN_COMPRESS_SIZE: usize = 512;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SvndiffVersion {
V0 = 0,
V1 = 1,
V2 = 2,
}
impl SvndiffVersion {
pub(crate) fn header(self) -> [u8; 4] {
match self {
Self::V0 => *b"SVN\0",
Self::V1 => *b"SVN\x01",
Self::V2 => *b"SVN\x02",
}
}
}
pub(crate) fn encode_fulltext_with_options(
version: SvndiffVersion,
contents: &[u8],
zlib_level: u32,
window_size: usize,
) -> Result<Vec<u8>, SvnError> {
let window_size = window_size.max(1);
let mut out = Vec::new();
out.extend_from_slice(&version.header());
for chunk in contents.chunks(window_size) {
encode_insertion_window(version, chunk, zlib_level, &mut out)?;
}
if contents.is_empty() {
encode_insertion_window(version, &[], zlib_level, &mut out)?;
}
Ok(out)
}
pub(crate) fn encode_insertion_window(
version: SvndiffVersion,
new_data: &[u8],
zlib_level: u32,
out: &mut Vec<u8>,
) -> Result<(), SvnError> {
let mut instructions = Vec::new();
if !new_data.is_empty() {
encode_new_instruction(new_data.len(), &mut instructions);
}
let (instructions_wire, newdata_wire) = match version {
SvndiffVersion::V0 => (instructions, new_data.to_vec()),
SvndiffVersion::V1 => (
compress_zlib(&instructions, zlib_level)?,
compress_zlib(new_data, zlib_level)?,
),
SvndiffVersion::V2 => (compress_lz4(&instructions)?, compress_lz4(new_data)?),
};
encode_uint(0, out); encode_uint(0, out); encode_uint(new_data.len() as u64, out); encode_uint(instructions_wire.len() as u64, out); encode_uint(newdata_wire.len() as u64, out);
out.extend_from_slice(&instructions_wire);
out.extend_from_slice(&newdata_wire);
Ok(())
}
fn encode_new_instruction(len: usize, out: &mut Vec<u8>) {
let len = len as u64;
if (len >> 6) == 0 {
out.push((0x2 << 6) | (len as u8));
} else {
out.push((0x2 << 6) as u8);
encode_uint(len, out);
}
}
fn encode_uint(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 compress_zlib(data: &[u8], zlib_level: u32) -> Result<Vec<u8>, SvnError> {
let mut out = Vec::new();
encode_uint(data.len() as u64, &mut out);
if data.len() < ZLIB_MIN_COMPRESS_SIZE || zlib_level == 0 {
out.extend_from_slice(data);
return Ok(out);
}
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::new(zlib_level));
encoder
.write_all(data)
.map_err(|err| SvnError::Protocol(format!("zlib encode failed: {err}")))?;
let compressed = encoder
.finish()
.map_err(|err| SvnError::Protocol(format!("zlib finish failed: {err}")))?;
if compressed.len() >= data.len() {
out.extend_from_slice(data);
} else {
out.extend_from_slice(&compressed);
}
Ok(out)
}
fn compress_lz4(data: &[u8]) -> Result<Vec<u8>, SvnError> {
let mut out = Vec::new();
encode_uint(data.len() as u64, &mut out);
let compressed = lz4_flex::compress(data);
if compressed.len() >= data.len() {
out.extend_from_slice(data);
} else {
out.extend_from_slice(&compressed);
}
Ok(out)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use std::io::Read;
use super::*;
fn decode_uint(mut input: &[u8]) -> Option<(u64, &[u8])> {
let mut val = 0u64;
loop {
let (&b, rest) = input.split_first()?;
input = rest;
val = val.checked_shl(7)?.checked_add(u64::from(b & 0x7f))?;
if (b & 0x80) == 0 {
return Some((val, input));
}
}
}
fn decode_zlib_section(input: &[u8]) -> Vec<u8> {
let (orig_len, rest) = decode_uint(input).unwrap();
let orig_len = orig_len as usize;
if rest.len() == orig_len {
return rest.to_vec();
}
let mut decoder = flate2::read::ZlibDecoder::new(rest);
let mut out = Vec::new();
decoder.read_to_end(&mut out).unwrap();
out
}
fn decode_lz4_section(input: &[u8]) -> Vec<u8> {
let (orig_len, rest) = decode_uint(input).unwrap();
let orig_len = orig_len as usize;
if rest.len() == orig_len {
return rest.to_vec();
}
lz4_flex::decompress(rest, orig_len).unwrap()
}
fn split_single_window(encoded: &[u8]) -> (u64, u64, u64, Vec<u8>, Vec<u8>) {
let mut input = encoded;
let (sview_offset, rest) = decode_uint(input).unwrap();
input = rest;
let (sview_len, rest) = decode_uint(input).unwrap();
input = rest;
let (tview_len, rest) = decode_uint(input).unwrap();
input = rest;
let (ins_len, rest) = decode_uint(input).unwrap();
input = rest;
let (new_len, rest) = decode_uint(input).unwrap();
input = rest;
let ins_len = ins_len as usize;
let new_len = new_len as usize;
let instructions = input[..ins_len].to_vec();
let newdata = input[ins_len..][..new_len].to_vec();
(sview_offset, sview_len, tview_len, instructions, newdata)
}
#[test]
fn svndiff_v0_fulltext_small_matches_known_bytes() {
let bytes = encode_fulltext_with_options(SvndiffVersion::V0, b"abc", 0, 64).unwrap();
assert_eq!(
bytes,
[
b'S',
b'V',
b'N',
0, 0, 0, 3, 1, 3, 0x80 | 3, b'a',
b'b',
b'c',
]
);
}
#[test]
fn svndiff_v1_small_roundtrips_sections() {
let bytes = encode_fulltext_with_options(SvndiffVersion::V1, b"abc", 5, 64).unwrap();
assert_eq!(&bytes[..4], b"SVN\x01");
let (sview_offset, sview_len, tview_len, instructions_wire, newdata_wire) =
split_single_window(&bytes[4..]);
assert_eq!((sview_offset, sview_len, tview_len), (0, 0, 3));
let instructions = decode_zlib_section(&instructions_wire);
let newdata = decode_zlib_section(&newdata_wire);
assert_eq!(instructions, vec![0x80 | 3]);
assert_eq!(newdata, b"abc");
}
#[test]
fn svndiff_v2_small_roundtrips_sections() {
let bytes = encode_fulltext_with_options(SvndiffVersion::V2, b"abc", 5, 64).unwrap();
assert_eq!(&bytes[..4], b"SVN\x02");
let (sview_offset, sview_len, tview_len, instructions_wire, newdata_wire) =
split_single_window(&bytes[4..]);
assert_eq!((sview_offset, sview_len, tview_len), (0, 0, 3));
let instructions = decode_lz4_section(&instructions_wire);
let newdata = decode_lz4_section(&newdata_wire);
assert_eq!(instructions, vec![0x80 | 3]);
assert_eq!(newdata, b"abc");
}
#[test]
fn svndiff_v1_large_roundtrips_and_compresses_newdata() {
let contents = vec![0u8; 4096];
let bytes =
encode_fulltext_with_options(SvndiffVersion::V1, &contents, 5, 16 * 1024).unwrap();
assert_eq!(&bytes[..4], b"SVN\x01");
let (_sview_offset, _sview_len, tview_len, _instructions_wire, newdata_wire) =
split_single_window(&bytes[4..]);
assert_eq!(tview_len as usize, contents.len());
let (orig_len, rest) = decode_uint(&newdata_wire).unwrap();
assert_eq!(orig_len as usize, contents.len());
assert!(rest.len() < contents.len());
let decoded = decode_zlib_section(&newdata_wire);
assert_eq!(decoded, contents);
}
#[test]
fn svndiff_v2_large_roundtrips_and_compresses_newdata() {
let contents = vec![0u8; 4096];
let bytes =
encode_fulltext_with_options(SvndiffVersion::V2, &contents, 5, 16 * 1024).unwrap();
assert_eq!(&bytes[..4], b"SVN\x02");
let (_sview_offset, _sview_len, tview_len, _instructions_wire, newdata_wire) =
split_single_window(&bytes[4..]);
assert_eq!(tview_len as usize, contents.len());
let (orig_len, rest) = decode_uint(&newdata_wire).unwrap();
assert_eq!(orig_len as usize, contents.len());
assert!(rest.len() < contents.len());
let decoded = decode_lz4_section(&newdata_wire);
assert_eq!(decoded, contents);
}
#[test]
fn svndiff_v0_fulltext_empty_still_emits_a_window() {
let bytes = encode_fulltext_with_options(SvndiffVersion::V0, b"", 0, 64).unwrap();
assert_eq!(
bytes,
[
b'S', b'V', b'N', 0, 0, 0, 0, 0, 0, ]
);
}
}