Skip to content

ApplyPatch解析器

追踪 StreamingPatchParser 的增量状态、hunk切换、EOF处理、环境标识和错误行号。

基于rust-v0.150.0
CodexRustExecutionApplyPatch

ApplyPatch解析器 ​

EXE021 讲了 Apply Patch 的语言构成;本篇深入当前 StreamingPatchParser 如何逐字符接收输入、按换行形成逻辑行、在状态机中切换 hunk,并在错误处记录 line number。它不执行文件匹配,也不负责写盘,因此“解析成功”只代表语法和 parser 不变量成立。v0.150.0 的 parser state 还保存可选 environment_id,并在 chunk 中记录显式上下文行的位置。

本文面向已读过Apply Patch语言与语法并理解 Rust enum/state machine 的读者。范围是 streaming_parser.rs 的增量实现和单测;不展开 seek/context 匹配。读完后,你应能解释 partial delta 为什么不会丢行、Update hunk 为什么必须先有 @@、以及 End of File marker 如何改变下一行的合法性。

1. 增量输入 ​

1.1 parser状态 ​

parser 持有未完成的 line_buffer、状态、已生成 hunks 和当前行号。输入可以一个字符一个字符地 push,只有看到 newline 才处理完整逻辑行。

源码位置:codex-rs/apply-patch/src/streaming_parser.rs :: StreamingPatchParser

rust
pub struct StreamingPatchParser {
    line_buffer: String,
    state: StreamingParserState,
    line_number: usize,
}

#[derive(Default, Clone, Copy)]
enum StreamingParserMode {
    NotStarted,
    StartedPatch,
    AddFile,
    DeleteFile,
    UpdateFile { hunk_line_number: usize },
    EndedPatch,
}

源码位置:codex-rs/apply-patch/src/streaming_parser.rs :: StreamingParserState、StreamingParserMode

rust
struct StreamingParserState {
    mode: StreamingParserMode,
    hunks: Vec<Hunk>,
    environment_id: Option<String>,
}

enum StreamingParserMode {
    NotStarted,
    StartedPatch,
    AddFile,
    DeleteFile,
    UpdateFile { hunk_line_number: usize },
    EndedPatch,
}

1.2 push与finish ​

push_delta 逐字符积累;遇到 \n 才移出 line_buffer、去掉 CR 并增加行号。最后没有 newline 时,finish 会单独处理残余行,再要求状态已经 EndedPatch。

源码位置:codex-rs/apply-patch/src/streaming_parser.rs :: push_delta、finish

rust
for ch in delta.chars() {
    if ch == '\n' {
        let mut line = std::mem::take(&mut self.line_buffer);
        line.truncate(line.strip_suffix('\r').map_or(line.len(), str::len));
        self.line_number += 1;
        self.process_line(&line)?;
    } else {
        self.line_buffer.push(ch);
    }
}

2. 状态切换 ​

2.1 Begin与hunk头 ​

NotStarted 只接受 Begin marker;StartedPatch 接受 Environment ID、Add/Delete/Update header 或 End Patch。每次进入新 hunk 前先检查前一个 Update hunk 是否为空。

源码位置:codex-rs/apply-patch/src/streaming_parser.rs :: process_line、handle_hunk_headers_and_end_patch

rust
if trimmed == BEGIN_PATCH_MARKER {
    self.state.mode = StreamingParserMode::StartedPatch;
    return Ok(());
}
if let Some(path) = trimmed.strip_prefix(ADD_FILE_MARKER) {
    self.ensure_update_hunk_is_not_empty(trimmed)?;
    self.state.hunks.push(AddFile {
        path: PathBuf::from(path),
        contents: String::new(),
    });
    self.state.mode = StreamingParserMode::AddFile;
    return Ok(true);
}

2.2 Add与Delete模式 ​

AddFile 模式只接受以 + 开头的内容;DeleteFile 模式不接受内容行,只等待下一个 header 或 End Patch。非法行使用当前 line number 报 InvalidHunkError。

3. Update解析 ​

3.1 Move与context ​

UpdateFile 开始时可先遇到 Move to;@@ 或 @@ context 会创建带 context 的空 chunk。实现也允许在没有 @@ 时直接用空行、空格、+ 或 - 隐式创建 chunk;一旦已有改动行,后续新的 chunk 必须从 @@ context marker 开始。

源码位置:codex-rs/apply-patch/src/streaming_parser.rs :: process_line UpdateFile branch

rust
if chunks.is_empty()
    && move_path.is_none()
    && let Some(move_to_path) = update_line.strip_prefix(MOVE_TO_MARKER)
{
    *move_path = Some(PathBuf::from(move_to_path));
    return Ok(());
}

if update_line == EMPTY_CHANGE_CONTEXT_MARKER {
    chunks.push(UpdateFileChunk::default());
    return Ok(());
}

3.2 增删行 ​

空行和空格行同时进入 old/new,+ 只进入 new,- 只进入 old。解析器不在这里检查 old_lines 是否真的存在于目标文件。

源码位置:codex-rs/apply-patch/src/streaming_parser.rs :: process_line

rust
if let Some(line_to_add) = line.strip_prefix(' ') {
    if chunks.is_empty() {
        chunks.push(UpdateFileChunk {
            change_context: None,
            old_lines: Vec::new(),
            new_lines: Vec::new(),
            is_end_of_file: false,
        });
    }
    if let Some(chunk) = chunks.last_mut() {
        chunk.push_context_line(line_to_add.to_string());
    }
    return Ok(());
}
if let Some(line_to_remove) = line.strip_prefix('-') {
    if chunks.is_empty() {
        chunks.push(UpdateFileChunk {
            change_context: None,
            old_lines: Vec::new(),
            new_lines: Vec::new(),
            is_end_of_file: false,
        });
    }
    if let Some(chunk) = chunks.last_mut() {
        chunk.old_lines.push(line_to_remove.to_string());
    }
    return Ok(());
}

4. EOF与环境ID ​

4.1 End of File ​

EOF marker 只能作用于已有非空 chunk;设置后下一行只能是空白或新的 context marker。这样 parser 不会把 EOF marker 后的普通内容继续塞进同一个 chunk。

源码位置:codex-rs/apply-patch/src/streaming_parser.rs :: StreamingPatchParser::process_line

rust
if update_line == EOF_MARKER {
    if chunks.last().is_some_and(|chunk| {
        chunk.old_lines.is_empty() && chunk.new_lines.is_empty()
    }) {
        return Err(InvalidHunkError {
            message: "Update hunk does not contain any lines".to_string(),
            line_number: self.line_number,
        });
    }
    if let Some(chunk) = chunks.last_mut() {
        chunk.is_end_of_file = true;
    }
    return Ok(());
}

4.2 Environment ID ​

Environment ID 只允许出现在 StartedPatch 的 header 区,并且只能出现一次、不能为空;它被保存在 parser state,最终由 parse_patch 放入 ApplyPatchArgs。

5. 错误定位 ​

ensure_update_hunk_is_not_empty 在切换 header、End Patch 或发现空 chunk 时检查不变量;空 UpdateFile 使用 hunk 起始行,空 Update chunk 或非法内容使用当前 line number,便于调用方和测试定位 malformed patch。

源码位置:codex-rs/apply-patch/src/streaming_parser.rs :: ensure_update_hunk_is_not_empty

rust
if chunks.is_empty()
    && let StreamingParserMode::UpdateFile { hunk_line_number } = self.state.mode
{
    return Err(InvalidHunkError {
        message: format!("Update file hunk for path '{}' is empty", path.display()),
        line_number: hunk_line_number,
    });
}

6. 验证 ​

6.1 字符流 ​

test_streaming_patch_parser_large_patch_split_by_character 每次只 push 一个字符,断言 hunk 数量只增不减,并最终识别 add/update/delete/move-update 七个操作。测试同时覆盖 UpdateFile 中的 Move to、多个 chunk 和 EOF 前的增量输出。

源码位置:codex-rs/apply-patch/src/streaming_parser.rs :: test_streaming_patch_parser_large_patch_split_by_character

text
cd codex-rs
cargo test -p codex-apply-patch --lib streaming_parser::tests::test_streaming_patch_parser_large_patch_split_by_character -- --test-threads=1

6.2 边界行 ​

同一测试模块覆盖 CRLF、无结尾 newline、EOF marker 后空行、重复 Environment ID、错误 hunk header 和 End Patch 后内容。它证明增量状态与错误边界,不证明文件内容匹配。

源码位置:codex-rs/apply-patch/src/streaming_parser.rs :: tests

text
cd codex-rs
cargo test -p codex-apply-patch --lib streaming_parser::tests -- --test-threads=1

7. 源码排查 ​

text
rg -n "StreamingParserMode|push_delta|finish|process_line" codex-rs/apply-patch/src/streaming_parser.rs
rg -n "ensure_update_hunk_is_not_empty|line_number|EOF_MARKER" codex-rs/apply-patch/src/streaming_parser.rs
rg -n "large_patch_split|environment_id_mode|line_ending_behavior|finish_processes" codex-rs/apply-patch/src/streaming_parser.rs

解析器主线是:字符进入 line buffer,完整行驱动状态机;状态决定 header、内容或错误;Update chunk 维护 old/new、显式 context 索引和 EOF;finish 负责最后一行和 End Patch 证明。下一篇ApplyPatchInvocation模型将分析命令调用识别与 verified invocation。