Skip to content

Commit

Permalink
Move all unified patch parsing into a single module
Browse files Browse the repository at this point in the history
  • Loading branch information
jelmer committed Sep 23, 2024
1 parent 950e48e commit 4caab79
Show file tree
Hide file tree
Showing 4 changed files with 680 additions and 635 deletions.
66 changes: 52 additions & 14 deletions src/ed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,43 @@ pub struct EdPatch {
pub hunks: Vec<EdHunk>,
}

impl crate::ContentPatch for EdPatch {
fn apply_exact(&self, orig: &[u8]) -> Result<Vec<u8>, crate::ApplyError> {
let lines = splitlines(orig).collect::<Vec<_>>();
let result = self.apply(&lines).map_err(|e| crate::ApplyError::Conflict(e))?;
Ok(result)
}
}

impl EdPatch {
/// Apply the patch to the data.
pub fn apply(&self, data: &[Vec<u8>]) -> Result<Vec<u8>, Vec<u8>> {
pub fn apply(&self, data: &[&[u8]]) -> Result<Vec<u8>, String> {
let mut data = data.to_vec();
for hunk in &self.hunks {
match hunk {
EdHunk::Remove(start, _end, expected)
| EdHunk::Change(start, _end, expected, _) => {
EdHunk::Remove(start, end, expected)
| EdHunk::Change(start, end, expected, _) => {
assert_eq!(start, end);
let existing = match data.get(start - 1) {
Some(existing) => existing,
None => return Err(b"".to_vec()),
None => return Err(format!("line {} does not exist", start).into()),
};
if existing != expected {
return Err(existing.to_vec());
return Err(format!(
"line {} does not match expected: {:?} != {:?}",
start,
String::from_utf8_lossy(existing).to_string(),
String::from_utf8_lossy(expected).to_string(),
));
}
data.remove(start - 1);
}
_ => {}
}
match hunk {
EdHunk::Add(start, _end, added) | EdHunk::Change(start, _end, _, added) => {
data.insert(start - 1, added.to_vec());
EdHunk::Add(start, end, added) | EdHunk::Change(start, end, _, added) => {
assert_eq!(start, end);
data.insert(start - 1, added);
}
_ => {}
}
Expand Down Expand Up @@ -97,11 +112,34 @@ pub fn parse_hunk_line<'a>(prefix: &[u8], line: &'a [u8]) -> Option<&'a [u8]> {
}
}

/// Split lines but preserve trailing newlines
pub fn splitlines(data: &[u8]) -> impl Iterator<Item = &'_ [u8]> {
let mut start = 0;
let mut end = 0;
std::iter::from_fn(move || loop {
if end == data.len() {
if start == end {
return None;
}
let line = &data[start..end];
start = end;
return Some(line);
}
let c = data[end];
end += 1;
if c == b'\n' {
let line = &data[start..end];
start = end;
return Some(line);
}
})
}

impl EdPatch {
/// Parse a patch in the ed format.
pub fn parse_patch(patch: &[u8]) -> Result<EdPatch, Vec<u8>> {
let mut hunks = Vec::new();
let mut lines = crate::parse::splitlines(patch);
let mut lines = splitlines(patch);
while let Some(line) = lines.next() {
if line.is_empty() {
continue;
Expand Down Expand Up @@ -154,17 +192,17 @@ mod apply_patch_tests {
let patch = EdPatch {
hunks: vec![EdHunk::Add(1, 1, b"hello\n".to_vec())],
};
let data = vec![b"world\n".to_vec()];
assert_eq!(patch.apply(&data).unwrap(), b"hello\nworld\n".to_vec());
let data = &[&b"world\n"[..]][..];
assert_eq!(patch.apply(data).unwrap(), b"hello\nworld\n".to_vec());
}

#[test]
fn test_apply_remove() {
let patch = EdPatch {
hunks: vec![EdHunk::Remove(2, 2, b"world\n".to_vec())],
};
let data = vec![b"hello\n".to_vec(), b"world\n".to_vec()];
assert_eq!(patch.apply(&data).unwrap(), b"hello\n".to_vec());
let data = &[&b"hello\n"[..], &b"world\n"[..]];
assert_eq!(patch.apply(data).unwrap(), b"hello\n".to_vec());
}

#[test]
Expand All @@ -177,8 +215,8 @@ mod apply_patch_tests {
b"hello\n".to_vec(),
)],
};
let data = vec![b"hello\n".to_vec(), b"world\n".to_vec()];
assert_eq!(patch.apply(&data).unwrap(), b"hello\nhello\n".to_vec());
let data = &[&b"hello\n"[..], &b"world\n"[..]];
assert_eq!(patch.apply(data).unwrap(), b"hello\nhello\n".to_vec());
}
}

Expand Down
45 changes: 41 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
//! # Examples
//!
//! ```
//! use patchkit::parse::parse_patch;
//! use patchkit::patch::{Patch as _, UnifiedPatch, Hunk, HunkLine};
//! use patchkit::ContentPatch;
//! use patchkit::unified::parse_patch;
//! use patchkit::unified::{UnifiedPatch, Hunk, HunkLine};
//!
//! let patch = UnifiedPatch::parse_patch(vec![
//! "--- a/file1\n",
Expand Down Expand Up @@ -40,8 +41,7 @@
//! ```
pub mod ed;
pub mod parse;
pub mod patch;
pub mod unified;
pub mod quilt;
pub mod timestamp;

Expand All @@ -54,6 +54,43 @@ pub fn strip_prefix(path: &std::path::Path, prefix: usize) -> &std::path::Path {
std::path::Path::new(components.as_path())
}


/// Error that occurs when applying a patch
#[derive(Debug)]
pub enum ApplyError {
/// A conflict occurred
Conflict(String),

/// The patch is unapplyable
Unapplyable,
}

impl std::fmt::Display for ApplyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Conflict(reason) => write!(f, "Conflict: {}", reason),
Self::Unapplyable => write!(f, "Patch unapplyable"),
}
}
}

impl std::error::Error for ApplyError {}

/// A patch to a single file
pub trait SingleFilePatch: ContentPatch {
/// Old file name
fn oldname(&self) -> &[u8];

/// New file name
fn newname(&self) -> &[u8];
}

/// A patch that can be applied to file content
pub trait ContentPatch {
/// Apply this patch to a file
fn apply_exact(&self, orig: &[u8]) -> Result<Vec<u8>, ApplyError>;
}

#[test]
fn test_strip_prefix() {
assert_eq!(
Expand Down
Loading

0 comments on commit 4caab79

Please sign in to comment.