rust 119 lines · 3 tabs

Parsing Unified Diff Hunks and Applying Them to a Text Buffer in Rust

Shared by codesnips Sep 2026
3 tabs
#[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 {}
3 files · rust Explain with highlit

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

javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
ruby
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

rails reliability background-jobs
by codesnips 4 tabs
typescript
import axios from 'axios';

export type NormalizedErrors = {
  fields: Record<string, string>;
  formLevel: string | null;
};

Frontend: normalize and display server validation errors

ux typescript react
by codesnips 3 tabs
rust
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

rust error-handling cli
by Marcus Chen 1 tab
python
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

xxe xml parsing
by Kai Nakamura 1 tab
ruby
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

rails api error-handling
by Alex Kumar 1 tab

Share this code

Here's the card — post it anywhere.

Parsing Unified Diff Hunks and Applying Them to a Text Buffer in Rust — share card
Link copied