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