#[derive(Debug, Clone, PartialEq)]
pub enum Line {
Context(String),
Remove(String),
Add(String),
}
#[derive(Debug, Clone)]
pub struct Hunk {
pub old_start: usize,
pub old_len: usize,
pub new_start: usize,
pub new_len: usize,
pub lines: Vec<Line>,
}
#[derive(Debug)]
pub enum PatchError {
BadHeader(String),
ContextMismatch { hunk: usize, at: usize },
}
impl std::fmt::Display for PatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PatchError::BadHeader(h) => write!(f, "bad hunk header: {}", h),
PatchError::ContextMismatch { hunk, at } => {
write!(f, "hunk {} does not match buffer at line {}", hunk, at)
}
}
}
}
impl std::error::Error for PatchError {}
use crate::hunk::{Hunk, Line, PatchError};
fn parse_range(spec: &str) -> Option<(usize, usize)> {
let spec = spec.trim_start_matches(['-', '+']);
let mut it = spec.splitn(2, ',');
let start: usize = it.next()?.parse().ok()?;
let len: usize = match it.next() {
Some(n) => n.parse().ok()?,
None => 1,
};
Some((start, len))
}
fn parse_header(line: &str) -> Result<(usize, usize, usize, usize), PatchError> {
let bad = || PatchError::BadHeader(line.to_string());
let inner = line.strip_prefix("@@ ").and_then(|s| s.split(" @@").next()).ok_or_else(bad)?;
let mut parts = inner.split_whitespace();
let old = parts.next().and_then(parse_range).ok_or_else(bad)?;
let new = parts.next().and_then(parse_range).ok_or_else(bad)?;
Ok((old.0, old.1, new.0, new.1))
}
pub fn parse_patch(input: &str) -> Result<Vec<Hunk>, PatchError> {
let mut hunks: Vec<Hunk> = Vec::new();
for raw in input.lines() {
if raw.starts_with("@@") {
let (os, ol, ns, nl) = parse_header(raw)?;
hunks.push(Hunk { old_start: os, old_len: ol, new_start: ns, new_len: nl, lines: Vec::new() });
continue;
}
let hunk = match hunks.last_mut() {
Some(h) => h,
None => continue, // skip file headers / preamble
};
let (tag, rest) = raw.split_at(raw.char_indices().next().map(|(_, c)| c.len_utf8()).unwrap_or(0));
match tag {
" " => hunk.lines.push(Line::Context(rest.to_string())),
"-" => hunk.lines.push(Line::Remove(rest.to_string())),
"+" => hunk.lines.push(Line::Add(rest.to_string())),
_ => {}
}
}
Ok(hunks)
}
use crate::hunk::{Hunk, Line, PatchError};
pub fn apply_hunks(buffer: &[String], hunks: &[Hunk]) -> Result<Vec<String>, PatchError> {
let mut out: Vec<String> = buffer.to_vec();
let mut offset: isize = 0;
for (idx, hunk) in hunks.iter().enumerate() {
let base = (hunk.old_start as isize - 1 + offset) as usize;
let mut cursor = base;
let mut spliced: Vec<String> = Vec::new();
let mut removed = 0usize;
for line in &hunk.lines {
match line {
Line::Context(text) => {
check(&out, cursor, text, idx)?;
spliced.push(text.clone());
cursor += 1;
removed += 1;
}
Line::Remove(text) => {
check(&out, cursor, text, idx)?;
cursor += 1;
removed += 1;
}
Line::Add(text) => spliced.push(text.clone()),
}
}
out.splice(base..base + removed, spliced.clone());
offset += spliced.len() as isize - removed as isize;
}
Ok(out)
}
fn check(buf: &[String], at: usize, expected: &str, hunk: usize) -> Result<(), PatchError> {
match buf.get(at) {
Some(actual) if actual == expected => Ok(()),
_ => Err(PatchError::ContextMismatch { hunk, at }),
}
}
A unified diff is the format git diff and diff -u emit: a sequence of hunks, each introduced by an @@ -old_start,old_len +new_start,new_len @@ header, followed by context lines (prefixed with a space), removals (-), and additions (+). Applying such a patch means walking the target buffer, matching the context and removed lines against what the hunk claims should be there, and splicing in the additions. This snippet models that process across three collaborating files.
In hunk.rs, the data model is defined: a Line enum distinguishes Context, Remove, and Add, and a Hunk carries the header offsets plus its ordered lines. The header numbers are one-based in the diff format, so old_start is stored as given and converted to a zero-based index only at apply time. Keeping the parsed structure dumb and value-only makes it trivial to test and reason about.
In parser.rs, parse_patch splits the input into lines and recognizes hunk headers with parse_header, which slices out the two -/+ ranges. A small state machine accumulates body lines into the current Hunk until the next @@ header or end of input. Malformed headers produce a PatchError::BadHeader rather than a panic, which matters because patch input is frequently untrusted or hand-edited.
In apply.rs, apply_hunks does the real work. It processes hunks in order while tracking an offset that accumulates the net line-count change from earlier hunks, so later hunk positions stay correct after insertions and deletions. For each hunk it verifies that every Context and Remove line matches the buffer at the expected position; a mismatch yields PatchError::ContextMismatch so the caller learns the patch does not apply cleanly instead of silently corrupting the file.
The key trade-off here is strictness: this applier requires exact context matches and offers no fuzz factor or fallback search, unlike patch(1). That keeps the logic small and predictable, and is the right choice when patches are machine-generated against a known base. The zero-based conversion at header.old_start - 1 and the running offset are the two easiest spots to get wrong, which is why they are isolated in one function.
Related snips
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
class CreateDeadJobs < ActiveRecord::Migration[7.1]
def change
create_table :dead_jobs do |t|
t.string :jid, null: false
t.string :queue, null: false
t.string :klass, null: false
Background Job Dead Letter Queue (DLQ) Table
import axios from 'axios';
export type NormalizedErrors = {
fields: Record<string, string>;
formLevel: string | null;
};
Frontend: normalize and display server validation errors
use anyhow::{Context, Result};
use std::fs;
fn load_config(path: &str) -> Result<String> {
fs::read_to_string(path)
.with_context(|| format!("failed to read config from {}", path))
anyhow::Context for adding error context without custom types
from defusedxml.ElementTree import fromstring
payload = request.data.decode('utf-8')
root = fromstring(payload)
invoice_number = root.findtext('invoice_number')
XXE safe XML parsing with external entity resolution disabled
module ApiErrorHandler
extend ActiveSupport::Concern
included do
rescue_from StandardError, with: :handle_standard_error
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
Structured JSON error responses
Share this code
Here's the card — post it anywhere.