blob: 4db754e366dc9584a6b9e6a7aeeab9276f59bf79 [file] [log] [blame]
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/// A parser to extract the header and footer for patch files we are going
/// to recontextualize.
pub struct Patch<'a> {
// The header of the patch, not including the diff stats and the immediately preceding "---", if present.
pub header: &'a str,
pub footer: &'a str,
}
impl<'a> Patch<'a> {
pub fn parse(contents: &'a str) -> Self {
// The format for patch files is not formally specified, and the GNU patch
// program is very forgiving about what it accepts. "git apply" is much
// less forgiving.
//
// Empirically, our patch files consist of:
// * An optional header, which we want to preserve if present.
// * Some optional diff stats, separated from the header by "---", which want
// to replace with new stats by running "git diff -p --stat"
// * Unified diffs for one or more files, which we are going to replace.
// If generated by git, the diffs will begin with "diff --git", but this may not be present.
// * An optional footer, starting with "-- ", if the patch was generated by
// "git format-patch", which we want to preserve if present.
let (header_and_stat, remainder) = contents.split_once("diff --git").unwrap_or(("", ""));
if header_and_stat.trim().is_empty() {
Patch { header: "", footer: "" }
} else {
let header = match header_and_stat.rfind("\n---\n") {
Some(stat_sep) => &header_and_stat[..stat_sep],
None => header_and_stat,
};
let footer = match remainder.rfind("-- \n") {
Some(footer_sep) => &remainder[footer_sep..],
None => "",
};
Patch { header, footer }
}
}
/// Generate a new diff file from the output of "git diff -p --stat".
pub fn reassemble(&self, new_diff: &[u8]) -> Vec<u8> {
[self.header.as_bytes(), b"\n---\n", new_diff, self.footer.as_bytes()].concat()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ahash() {
let contents = include_str!("testdata/ahash.patch");
let parsed = Patch::parse(contents);
assert_eq!(parsed.header, include_str!("testdata/ahash.header"));
assert_eq!(parsed.footer, include_str!("testdata/ahash.footer"));
}
#[test]
fn test_mls_rs_core() {
let contents = include_str!("testdata/mls_rs_core.patch");
let parsed = Patch::parse(contents);
assert_eq!(parsed.header, include_str!("testdata/mls_rs_core.header"));
assert!(parsed.footer.is_empty());
}
#[test]
fn test_crossbeam_utils() {
let contents = include_str!("testdata/crossbeam-utils.patch");
let parsed = Patch::parse(contents);
assert!(parsed.header.is_empty());
assert!(parsed.footer.is_empty());
}
}