Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 139 additions & 19 deletions src/delta/decode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const COPY_OFFSET_BYTES: u8 = 4;
const COPY_SIZE_BYTES: u8 = 3;
const COPY_ZERO_SIZE: usize = 0x10000;

fn decoder_error(message: impl Into<String>) -> GitDeltaError {
GitDeltaError::DeltaDecoderError(message.into())
}

/// Apply a delta stream to `base_info`, returning the reconstructed target bytes.
/// The stream format matches Git's delta encoding (see `delta::encode`):
/// - leading base size, then result size (varint)
Expand All @@ -20,64 +24,95 @@ pub fn delta_decode(
base_info: &[u8],
) -> Result<Vec<u8>, GitDeltaError> {
// Read declared base size and result size
let base_size = utils::read_size_encoding(&mut stream).unwrap();
let base_size =
utils::read_size_encoding(&mut stream).map_err(|err| decoder_error(err.to_string()))?;
if base_info.len() != base_size {
return Err(GitDeltaError::DeltaDecoderError(
"base object len is not equal".to_owned(),
));
return Err(decoder_error("base object len is not equal"));
}

let result_size = utils::read_size_encoding(&mut stream).unwrap();
let mut buffer = Vec::with_capacity(result_size);
let result_size =
utils::read_size_encoding(&mut stream).map_err(|err| decoder_error(err.to_string()))?;
let mut buffer = Vec::new();
loop {
// Check if the stream has ended, meaning the new object is done
let instruction = match utils::read_bytes(stream) {
Ok([instruction]) => instruction,
Err(err) if err.kind() == ErrorKind::UnexpectedEof => break,
Err(err) => {
panic!("{}", format!("Wrong instruction in delta :{err}"));
return Err(decoder_error(format!("Wrong instruction in delta: {err}")));
}
};

if instruction & COPY_INSTRUCTION_FLAG == 0 {
// Data instruction; the instruction byte specifies the number of data bytes
if instruction == 0 {
// Appending 0 bytes doesn't make sense, so git disallows it
panic!(
"{}",
GitDeltaError::DeltaDecoderError(String::from("Invalid data instruction"))
);
return Err(decoder_error("Invalid data instruction"));
}

// Append the provided bytes
let mut data = vec![0; instruction as usize];
stream.read_exact(&mut data).unwrap();
stream
.read_exact(&mut data)
.map_err(|err| decoder_error(err.to_string()))?;
prepare_result_append(&mut buffer, data.len(), result_size)?;
buffer.extend_from_slice(&data);
// result.extend_from_slice(&data);
} else {
// Copy instruction
let mut nonzero_bytes = instruction;
let offset =
utils::read_partial_int(&mut stream, COPY_OFFSET_BYTES, &mut nonzero_bytes)
.unwrap();
.map_err(|err| decoder_error(err.to_string()))?;
let mut size =
utils::read_partial_int(&mut stream, COPY_SIZE_BYTES, &mut nonzero_bytes).unwrap();
utils::read_partial_int(&mut stream, COPY_SIZE_BYTES, &mut nonzero_bytes)
.map_err(|err| decoder_error(err.to_string()))?;
if size == 0 {
// Copying 0 bytes doesn't make sense, so git assumes a different size
size = COPY_ZERO_SIZE;
}
// Copy bytes from the base object
let base_data = base_info.get(offset..(offset + size)).ok_or_else(|| {
GitDeltaError::DeltaDecoderError("Invalid copy instruction".to_string())
});
let end = offset
.checked_add(size)
.ok_or_else(|| decoder_error("Invalid copy instruction"))?;
let base_data = base_info
.get(offset..end)
.ok_or_else(|| decoder_error("Invalid copy instruction"));

buffer.extend_from_slice(base_data?);
let base_data = base_data?;
prepare_result_append(&mut buffer, base_data.len(), result_size)?;
buffer.extend_from_slice(base_data);
}
}
assert!(buffer.len() == result_size);
if buffer.len() != result_size {
return Err(decoder_error(format!(
"result object len is not equal: expected {result_size}, got {}",
buffer.len()
)));
}
Ok(buffer)
}

fn prepare_result_append(
buffer: &mut Vec<u8>,
instruction_size: usize,
result_size: usize,
) -> Result<(), GitDeltaError> {
let new_size = buffer
.len()
.checked_add(instruction_size)
.ok_or_else(|| decoder_error("result object size overflow"))?;
if new_size > result_size {
return Err(decoder_error(format!(
"result object exceeds declared size: expected {result_size}, got at least {new_size}"
)));
}
buffer
.try_reserve_exact(instruction_size)
.map_err(|err| decoder_error(format!("cannot allocate result object: {err}")))?;
Ok(())
}

#[cfg(test)]
mod tests {
use std::io::Cursor;
Expand Down Expand Up @@ -109,4 +144,89 @@ mod tests {
let err = delta_decode(&mut cursor, b"xx").unwrap_err();
assert!(matches!(err, GitDeltaError::DeltaDecoderError(_)));
}

/// Truncated delta headers should return an error instead of panicking.
#[test]
fn truncated_header_returns_error() {
let mut cursor = Cursor::new(Vec::<u8>::new());
let err = delta_decode(&mut cursor, b"").unwrap_err();
assert!(matches!(err, GitDeltaError::DeltaDecoderError(_)));
}

/// Git disallows a zero-length literal instruction; report it as malformed input.
#[test]
fn zero_literal_instruction_returns_error() {
let mut cursor = Cursor::new(vec![0, 0, 0]);
let err = delta_decode(&mut cursor, b"").unwrap_err();
assert!(matches!(err, GitDeltaError::DeltaDecoderError(_)));
}

/// Literal instructions whose payload is shorter than declared should return an error.
#[test]
fn truncated_literal_instruction_returns_error() {
let mut cursor = Cursor::new(vec![0, 3, 3, b'a']);
let err = delta_decode(&mut cursor, b"").unwrap_err();
assert!(matches!(err, GitDeltaError::DeltaDecoderError(_)));
}

/// Copy instructions whose operand bytes are missing should return an error.
#[test]
fn truncated_copy_instruction_returns_error() {
let mut cursor = Cursor::new(vec![3, 1, 0x81]);
let err = delta_decode(&mut cursor, b"abc").unwrap_err();
assert!(matches!(err, GitDeltaError::DeltaDecoderError(_)));
}

/// Deltas that end before producing the declared result size should return an error.
#[test]
fn result_size_mismatch_returns_error() {
let mut cursor = Cursor::new(vec![0, 1]);
let err = delta_decode(&mut cursor, b"").unwrap_err();
assert!(matches!(err, GitDeltaError::DeltaDecoderError(_)));
}

/// Size varints wider than usize should be rejected instead of overflowing a shift.
#[test]
fn overlong_size_varint_returns_error() {
let mut bytes = vec![0x80; 10];
bytes.push(0);
let mut cursor = Cursor::new(bytes);

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
delta_decode(&mut cursor, b"")
}));

assert!(result.is_ok(), "overlong size varint should not panic");
assert!(matches!(
result.unwrap(),
Err(GitDeltaError::DeltaDecoderError(_))
));
}

/// An unallocatable declared result size should be rejected without reserving it eagerly.
#[test]
fn unallocatable_result_size_returns_error() {
let mut bytes = vec![0];
bytes.extend([0xff; 9]);
bytes.push(1);
let mut cursor = Cursor::new(bytes);

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
delta_decode(&mut cursor, b"")
}));

assert!(result.is_ok(), "unallocatable result size should not panic");
assert!(matches!(
result.unwrap(),
Err(GitDeltaError::DeltaDecoderError(_))
));
}

/// Instructions may not produce more bytes than the declared result size.
#[test]
fn instruction_exceeding_result_size_returns_error() {
let mut cursor = Cursor::new(vec![0, 1, 2, b'a', b'b']);
let err = delta_decode(&mut cursor, b"").unwrap_err();
assert!(matches!(err, GitDeltaError::DeltaDecoderError(_)));
}
}
2 changes: 2 additions & 0 deletions src/delta/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ mod encode;
mod errors;
mod utils;

pub(crate) use decode::delta_decode;

const SAMPLE_STEP: usize = 64;
const MIN_DELTA_RATE: f64 = 0.5;

Expand Down
15 changes: 12 additions & 3 deletions src/delta/utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Shared readers for Git delta streams: length parsing, partial integer decoding, and VarInt helpers
//! that both encoder and decoder reuse.

use std::io::Read;
use std::io::{self, Read};

const VAR_INT_ENCODING_BITS: u8 = 7;
const VAR_INT_CONTINUE_FLAG: u8 = 1 << VAR_INT_ENCODING_BITS;
Expand All @@ -21,13 +21,22 @@ pub fn read_size_encoding<R: Read>(stream: &mut R) -> std::io::Result<usize> {
let mut length = 0;

loop {
let (byte_value, more_bytes) = read_var_int_byte(stream).unwrap();
let (byte_value, more_bytes) = read_var_int_byte(stream)?;
if length >= usize::BITS
|| (length + u32::from(VAR_INT_ENCODING_BITS) > usize::BITS
&& (byte_value as usize) > (usize::MAX >> length))
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"delta size varint exceeds usize",
));
}
value |= (byte_value as usize) << length;
if !more_bytes {
return Ok(value);
}

length += VAR_INT_ENCODING_BITS;
length += u32::from(VAR_INT_ENCODING_BITS);
}
}

Expand Down
Loading
Loading