// 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 { [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()); } }