use std::ops::Range;
#[derive(Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub enum WriteError {
OutOfBounds { range: Range<usize>, len: usize },
Overlap {
first: Range<usize>,
second: Range<usize>,
},
}
impl std::fmt::Display for WriteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OutOfBounds { range, len } => write!(
f,
"replacement range {}..{} is out of bounds (file length {len})",
range.start, range.end,
),
Self::Overlap { first, second } => write!(
f,
"replacement ranges {}..{} and {}..{} overlap",
first.start, first.end, second.start, second.end,
),
}
}
}
impl std::error::Error for WriteError {}
#[allow(dead_code)]
pub fn write_with_replacements(
original: &[u8],
mut replacements: Vec<(Range<usize>, Vec<u8>)>,
) -> Result<Vec<u8>, WriteError> {
if replacements.is_empty() {
return Ok(original.to_vec());
}
let len = original.len();
for (range, _) in &replacements {
if range.start > range.end || range.end > len {
return Err(WriteError::OutOfBounds {
range: range.clone(),
len,
});
}
}
replacements.sort_by_key(|(r, _)| r.start);
for w in replacements.windows(2) {
if w[0].0.end > w[1].0.start {
return Err(WriteError::Overlap {
first: w[0].0.clone(),
second: w[1].0.clone(),
});
}
}
let extra: usize = replacements.iter().map(|(_, b)| b.len()).sum();
let mut out = Vec::with_capacity(len + extra);
let mut cursor = 0;
for (range, bytes) in &replacements {
out.extend_from_slice(&original[cursor..range.start]);
out.extend_from_slice(bytes);
cursor = range.end;
}
out.extend_from_slice(&original[cursor..]);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_replacements_returns_input() {
let input = b"hello world";
let out = write_with_replacements(input, vec![]).unwrap();
assert_eq!(out, input);
}
#[test]
fn one_replacement_in_the_middle() {
let input = b"hello world";
let out = write_with_replacements(input, vec![(6..11, b"earth".to_vec())]).unwrap();
assert_eq!(out, b"hello earth");
}
#[test]
fn replacement_at_start() {
let out = write_with_replacements(b"abcdef", vec![(0..2, b"XY".to_vec())]).unwrap();
assert_eq!(out, b"XYcdef");
}
#[test]
fn replacement_at_end() {
let out = write_with_replacements(b"abcdef", vec![(4..6, b"EF".to_vec())]).unwrap();
assert_eq!(out, b"abcdEF");
}
#[test]
fn replacement_with_growing_length() {
let out = write_with_replacements(b"abcdef", vec![(2..4, b"CCCC".to_vec())]).unwrap();
assert_eq!(out, b"abCCCCef");
}
#[test]
fn replacement_with_shrinking_length() {
let out = write_with_replacements(b"abcdef", vec![(1..5, b"-".to_vec())]).unwrap();
assert_eq!(out, b"a-f");
}
#[test]
fn empty_replacement_deletes_range() {
let out = write_with_replacements(b"abcdef", vec![(2..4, vec![])]).unwrap();
assert_eq!(out, b"abef");
}
#[test]
fn multiple_non_overlapping_replacements_unsorted() {
let out = write_with_replacements(
b"one two three four",
vec![
(14..18, b"FOUR".to_vec()),
(0..3, b"ONE".to_vec()),
(8..13, b"THREE".to_vec()),
],
)
.unwrap();
assert_eq!(out, b"ONE two THREE FOUR");
}
#[test]
fn adjacent_non_overlapping_replacements_allowed() {
let out = write_with_replacements(
b"abcdef",
vec![(2..4, b"CD".to_vec()), (4..6, b"EF".to_vec())],
)
.unwrap();
assert_eq!(out, b"abCDEF");
}
#[test]
fn overlapping_replacements_rejected() {
let err = write_with_replacements(
b"abcdef",
vec![(1..4, b"X".to_vec()), (3..5, b"Y".to_vec())],
)
.unwrap_err();
assert!(matches!(err, WriteError::Overlap { .. }));
}
#[test]
fn out_of_bounds_replacement_rejected() {
let err = write_with_replacements(b"abcdef", vec![(4..10, b"Z".to_vec())]).unwrap_err();
assert!(matches!(err, WriteError::OutOfBounds { .. }));
}
#[test]
fn inverted_range_rejected() {
let inverted = Range { start: 4, end: 2 };
let err = write_with_replacements(b"abcdef", vec![(inverted, b"Z".to_vec())]).unwrap_err();
assert!(matches!(err, WriteError::OutOfBounds { .. }));
}
#[test]
fn empty_range_at_position_is_insertion() {
let out = write_with_replacements(b"abcdef", vec![(3..3, b"XY".to_vec())]).unwrap();
assert_eq!(out, b"abcXYdef");
}
}