1 // Copyright (C) 2024 The Android Open Source Project 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 /// A parser to extract the header and footer for patch files we are going 16 /// to recontextualize. 17 18 pub struct Patch<'a> { 19 // The header of the patch, not including the diff stats and the immediately preceding "---", if present. 20 pub header: &'a str, 21 pub footer: &'a str, 22 } 23 24 impl<'a> Patch<'a> { parse(contents: &'a str) -> Self25 pub fn parse(contents: &'a str) -> Self { 26 // The format for patch files is not formally specified, and the GNU patch 27 // program is very forgiving about what it accepts. "git apply" is much 28 // less forgiving. 29 // 30 // Empirically, our patch files consist of: 31 // * An optional header, which we want to preserve if present. 32 // * Some optional diff stats, separated from the header by "---", which want 33 // to replace with new stats by running "git diff -p --stat" 34 // * Unified diffs for one or more files, which we are going to replace. 35 // If generated by git, the diffs will begin with "diff --git", but this may not be present. 36 // * An optional footer, starting with "-- ", if the patch was generated by 37 // "git format-patch", which we want to preserve if present. 38 let (header_and_stat, remainder) = contents.split_once("diff --git").unwrap_or(("", "")); 39 if header_and_stat.trim().is_empty() { 40 Patch { header: "", footer: "" } 41 } else { 42 let header = match header_and_stat.rfind("\n---\n") { 43 Some(stat_sep) => &header_and_stat[..stat_sep], 44 None => header_and_stat, 45 }; 46 let footer = match remainder.rfind("-- \n") { 47 Some(footer_sep) => &remainder[footer_sep..], 48 None => "", 49 }; 50 Patch { header, footer } 51 } 52 } 53 /// Generate a new diff file from the output of "git diff -p --stat". reassemble(&self, new_diff: &[u8]) -> Vec<u8>54 pub fn reassemble(&self, new_diff: &[u8]) -> Vec<u8> { 55 [self.header.as_bytes(), b"\n---\n", new_diff, self.footer.as_bytes()].concat() 56 } 57 } 58 59 #[cfg(test)] 60 mod tests { 61 use super::*; 62 63 #[test] test_ahash()64 fn test_ahash() { 65 let contents = include_str!("testdata/ahash.patch"); 66 let parsed = Patch::parse(contents); 67 assert_eq!(parsed.header, include_str!("testdata/ahash.header")); 68 assert_eq!(parsed.footer, include_str!("testdata/ahash.footer")); 69 } 70 71 #[test] test_mls_rs_core()72 fn test_mls_rs_core() { 73 let contents = include_str!("testdata/mls_rs_core.patch"); 74 let parsed = Patch::parse(contents); 75 assert_eq!(parsed.header, include_str!("testdata/mls_rs_core.header")); 76 assert!(parsed.footer.is_empty()); 77 } 78 79 #[test] test_crossbeam_utils()80 fn test_crossbeam_utils() { 81 let contents = include_str!("testdata/crossbeam-utils.patch"); 82 let parsed = Patch::parse(contents); 83 assert!(parsed.header.is_empty()); 84 assert!(parsed.footer.is_empty()); 85 } 86 } 87