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
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
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
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
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
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
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
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
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
cd codex-rs
cargo test -p codex-apply-patch --lib streaming_parser::tests::test_streaming_patch_parser_large_patch_split_by_character -- --test-threads=16.2 边界行
同一测试模块覆盖 CRLF、无结尾 newline、EOF marker 后空行、重复 Environment ID、错误 hunk header 和 End Patch 后内容。它证明增量状态与错误边界,不证明文件内容匹配。
源码位置:codex-rs/apply-patch/src/streaming_parser.rs :: tests
cd codex-rs
cargo test -p codex-apply-patch --lib streaming_parser::tests -- --test-threads=17. 源码排查
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。
