xref: /aosp_15_r20/development/tools/external_crates/crate_tool/src/patch.rs (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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