• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2022 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 use std::collections::{BTreeMap, BTreeSet};
6 use std::fs::{copy, File};
7 use std::io::{BufRead, BufReader, Read, Write};
8 use std::path::{Path, PathBuf};
9 
10 use anyhow::{anyhow, Context, Result};
11 use serde::{Deserialize, Serialize};
12 use sha2::{Digest, Sha256};
13 
14 /// JSON serde struct.
15 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
16 pub struct PatchDictSchema {
17     pub metadata: Option<BTreeMap<String, serde_json::Value>>,
18     #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
19     pub platforms: BTreeSet<String>,
20     pub rel_patch_path: String,
21     pub version_range: Option<VersionRange>,
22 }
23 
24 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
25 pub struct VersionRange {
26     pub from: Option<u64>,
27     pub until: Option<u64>,
28 }
29 
30 impl PatchDictSchema {
31     /// Return the first version this patch applies to.
get_from_version(&self) -> Option<u64>32     pub fn get_from_version(&self) -> Option<u64> {
33         self.version_range.and_then(|x| x.from)
34     }
35 
36     /// Return the version after the last version this patch
37     /// applies to.
get_until_version(&self) -> Option<u64>38     pub fn get_until_version(&self) -> Option<u64> {
39         self.version_range.and_then(|x| x.until)
40     }
41 }
42 
43 /// Struct to keep track of patches and their relative paths.
44 #[derive(Debug, Clone)]
45 pub struct PatchCollection {
46     pub patches: Vec<PatchDictSchema>,
47     pub workdir: PathBuf,
48 }
49 
50 impl PatchCollection {
51     /// Create a `PatchCollection` from a PATCHES.
parse_from_file(json_file: &Path) -> Result<Self>52     pub fn parse_from_file(json_file: &Path) -> Result<Self> {
53         Ok(Self {
54             patches: serde_json::from_reader(File::open(json_file)?)?,
55             workdir: json_file
56                 .parent()
57                 .ok_or_else(|| anyhow!("failed to get json_file parent"))?
58                 .to_path_buf(),
59         })
60     }
61 
62     /// Create a `PatchCollection` from a string literal and a workdir.
parse_from_str(workdir: PathBuf, contents: &str) -> Result<Self>63     pub fn parse_from_str(workdir: PathBuf, contents: &str) -> Result<Self> {
64         Ok(Self {
65             patches: serde_json::from_str(contents).context("parsing from str")?,
66             workdir,
67         })
68     }
69 
70     /// Copy this collection with patches filtered by given criterion.
filter_patches(&self, f: impl FnMut(&PatchDictSchema) -> bool) -> Self71     pub fn filter_patches(&self, f: impl FnMut(&PatchDictSchema) -> bool) -> Self {
72         Self {
73             patches: self.patches.iter().cloned().filter(f).collect(),
74             workdir: self.workdir.clone(),
75         }
76     }
77 
78     /// Map over the patches.
map_patches(&self, f: impl FnMut(&PatchDictSchema) -> PatchDictSchema) -> Self79     pub fn map_patches(&self, f: impl FnMut(&PatchDictSchema) -> PatchDictSchema) -> Self {
80         Self {
81             patches: self.patches.iter().map(f).collect(),
82             workdir: self.workdir.clone(),
83         }
84     }
85 
86     /// Return true if the collection is tracking any patches.
is_empty(&self) -> bool87     pub fn is_empty(&self) -> bool {
88         self.patches.is_empty()
89     }
90 
91     /// Compute the set-set subtraction, returning a new `PatchCollection` which
92     /// keeps the minuend's workdir.
subtract(&self, subtrahend: &Self) -> Result<Self>93     pub fn subtract(&self, subtrahend: &Self) -> Result<Self> {
94         let mut new_patches = Vec::new();
95         // This is O(n^2) when it could be much faster, but n is always going to be less
96         // than 1k and speed is not important here.
97         for our_patch in &self.patches {
98             let found_in_sub = subtrahend.patches.iter().any(|sub_patch| {
99                 let hash1 = subtrahend
100                     .hash_from_rel_patch(sub_patch)
101                     .expect("getting hash from subtrahend patch");
102                 let hash2 = self
103                     .hash_from_rel_patch(our_patch)
104                     .expect("getting hash from our patch");
105                 hash1 == hash2
106             });
107             if !found_in_sub {
108                 new_patches.push(our_patch.clone());
109             }
110         }
111         Ok(Self {
112             patches: new_patches,
113             workdir: self.workdir.clone(),
114         })
115     }
116 
union(&self, other: &Self) -> Result<Self>117     pub fn union(&self, other: &Self) -> Result<Self> {
118         self.union_helper(
119             other,
120             |p| self.hash_from_rel_patch(p),
121             |p| other.hash_from_rel_patch(p),
122         )
123     }
124 
125     /// Vec of every PatchDictSchema with differing
126     /// version ranges but the same rel_patch_paths.
version_range_diffs(&self, other: &Self) -> Vec<(String, Option<VersionRange>)>127     fn version_range_diffs(&self, other: &Self) -> Vec<(String, Option<VersionRange>)> {
128         let other_map: BTreeMap<_, _> = other
129             .patches
130             .iter()
131             .map(|p| (p.rel_patch_path.clone(), p))
132             .collect();
133         self.patches
134             .iter()
135             .filter_map(|ours| match other_map.get(&ours.rel_patch_path) {
136                 Some(theirs) => {
137                     if ours.get_from_version() != theirs.get_from_version()
138                         || ours.get_until_version() != theirs.get_until_version()
139                     {
140                         Some((ours.rel_patch_path.clone(), ours.version_range))
141                     } else {
142                         None
143                     }
144                 }
145                 _ => None,
146             })
147             .collect()
148     }
149 
150     /// Given a vector of tuples with (rel_patch_path, Option<VersionRange>), replace
151     /// all version ranges in this collection with a matching one in the new_versions parameter.
update_version_ranges(&self, new_versions: &[(String, Option<VersionRange>)]) -> Self152     pub fn update_version_ranges(&self, new_versions: &[(String, Option<VersionRange>)]) -> Self {
153         // new_versions should be really tiny (len() <= 2 for the most part), so
154         // the overhead of O(1) lookups is not worth it.
155         let get_updated_version = |rel_patch_path: &str| -> Option<Option<VersionRange>> {
156             // The first Option indicates whether we are updating it at all.
157             // The second Option indicates we can update it with None.
158             new_versions
159                 .iter()
160                 .find(|i| i.0 == rel_patch_path)
161                 .map(|x| x.1)
162         };
163         let cloned_patches = self
164             .patches
165             .iter()
166             .map(|p| match get_updated_version(&p.rel_patch_path) {
167                 Some(version_range) => PatchDictSchema {
168                     version_range,
169                     ..p.clone()
170                 },
171                 _ => p.clone(),
172             })
173             .collect();
174         Self {
175             workdir: self.workdir.clone(),
176             patches: cloned_patches,
177         }
178     }
179 
union_helper( &self, other: &Self, our_hash_f: impl Fn(&PatchDictSchema) -> Result<String>, their_hash_f: impl Fn(&PatchDictSchema) -> Result<String>, ) -> Result<Self>180     fn union_helper(
181         &self,
182         other: &Self,
183         our_hash_f: impl Fn(&PatchDictSchema) -> Result<String>,
184         their_hash_f: impl Fn(&PatchDictSchema) -> Result<String>,
185     ) -> Result<Self> {
186         // 1. For all our patches:
187         //   a. If there exists a matching patch hash from `other`:
188         //     i. Create a new patch with merged platform info,
189         //     ii. add the new patch to our new collection.
190         //     iii. Mark the other patch as "merged"
191         //   b. Otherwise, copy our patch to the new collection
192         // 2. For all unmerged patches from the `other`
193         //   a. Copy their patch into the new collection
194         let mut combined_patches = Vec::new();
195         let mut other_merged = vec![false; other.patches.len()];
196 
197         // 1.
198         for p in &self.patches {
199             let our_hash = our_hash_f(p)?;
200             let mut found = false;
201             // a.
202             for (idx, merged) in other_merged.iter_mut().enumerate() {
203                 if !*merged {
204                     let other_p = &other.patches[idx];
205                     let their_hash = their_hash_f(other_p)?;
206                     if our_hash == their_hash {
207                         // i.
208                         let new_platforms =
209                             p.platforms.union(&other_p.platforms).cloned().collect();
210                         // ii.
211                         combined_patches.push(PatchDictSchema {
212                             rel_patch_path: p.rel_patch_path.clone(),
213                             platforms: new_platforms,
214                             metadata: p.metadata.clone(),
215                             version_range: p.version_range,
216                         });
217                         // iii.
218                         *merged = true;
219                         found = true;
220                         break;
221                     }
222                 }
223             }
224             // b.
225             if !found {
226                 combined_patches.push(p.clone());
227             }
228         }
229         // 2.
230         // Add any remaining, other-only patches.
231         for (idx, merged) in other_merged.iter().enumerate() {
232             if !*merged {
233                 combined_patches.push(other.patches[idx].clone());
234             }
235         }
236 
237         Ok(Self {
238             workdir: self.workdir.clone(),
239             patches: combined_patches,
240         })
241     }
242 
243     /// Copy all patches from this collection into another existing collection, and write that
244     /// to the existing collection's file.
transpose_write(&self, existing_collection: &mut Self) -> Result<()>245     pub fn transpose_write(&self, existing_collection: &mut Self) -> Result<()> {
246         for p in &self.patches {
247             let original_file_path = self.workdir.join(&p.rel_patch_path);
248             let copy_file_path = existing_collection.workdir.join(&p.rel_patch_path);
249             copy_create_parents(&original_file_path, &copy_file_path)?;
250             existing_collection.patches.push(p.clone());
251         }
252         existing_collection.write_patches_json("PATCHES.json")
253     }
254 
255     /// Write out the patch collection contents to a PATCHES.json file.
write_patches_json(&self, filename: &str) -> Result<()>256     fn write_patches_json(&self, filename: &str) -> Result<()> {
257         let write_path = self.workdir.join(filename);
258         let mut new_patches_file = File::create(&write_path)
259             .with_context(|| format!("writing to {}", write_path.display()))?;
260         new_patches_file.write_all(self.serialize_patches()?.as_bytes())?;
261         Ok(())
262     }
263 
serialize_patches(&self) -> Result<String>264     pub fn serialize_patches(&self) -> Result<String> {
265         let mut serialization_buffer = Vec::<u8>::new();
266         // Four spaces to indent json serialization.
267         let mut serializer = serde_json::Serializer::with_formatter(
268             &mut serialization_buffer,
269             serde_json::ser::PrettyFormatter::with_indent(b"    "),
270         );
271         self.patches
272             .serialize(&mut serializer)
273             .context("serializing patches to JSON")?;
274         // Append a newline at the end if not present. This is necessary to get
275         // past some pre-upload hooks.
276         if serialization_buffer.last() != Some(&b'\n') {
277             serialization_buffer.push(b'\n');
278         }
279         Ok(std::str::from_utf8(&serialization_buffer)?.to_string())
280     }
281 
282     /// Return whether a given patch actually exists on the file system.
patch_exists(&self, patch: &PatchDictSchema) -> bool283     pub fn patch_exists(&self, patch: &PatchDictSchema) -> bool {
284         self.workdir.join(&patch.rel_patch_path).exists()
285     }
286 
hash_from_rel_patch(&self, patch: &PatchDictSchema) -> Result<String>287     fn hash_from_rel_patch(&self, patch: &PatchDictSchema) -> Result<String> {
288         hash_from_patch_path(&self.workdir.join(&patch.rel_patch_path))
289     }
290 }
291 
292 impl std::fmt::Display for PatchCollection {
fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result293     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
294         for (i, p) in self.patches.iter().enumerate() {
295             let title = p
296                 .metadata
297                 .as_ref()
298                 .and_then(|x| x.get("title"))
299                 .and_then(serde_json::Value::as_str)
300                 .unwrap_or("[No Title]");
301             let path = self.workdir.join(&p.rel_patch_path);
302             writeln!(f, "* {}", title)?;
303             if i == self.patches.len() - 1 {
304                 write!(f, "  {}", path.display())?;
305             } else {
306                 writeln!(f, "  {}", path.display())?;
307             }
308         }
309         Ok(())
310     }
311 }
312 
313 /// Represents information which changed between now and an old version of a PATCHES.json file.
314 pub struct PatchTemporalDiff {
315     pub cur_collection: PatchCollection,
316     pub new_patches: PatchCollection,
317     // Store version_updates as a vec, not a map, as it's likely to be very small (<=2),
318     // and the overhead of using a O(1) look up structure isn't worth it.
319     pub version_updates: Vec<(String, Option<VersionRange>)>,
320 }
321 
322 /// Generate a PatchCollection incorporating only the diff between current patches and old patch
323 /// contents.
new_patches( patches_path: &Path, old_patch_contents: &str, platform: &str, ) -> Result<PatchTemporalDiff>324 pub fn new_patches(
325     patches_path: &Path,
326     old_patch_contents: &str,
327     platform: &str,
328 ) -> Result<PatchTemporalDiff> {
329     // Set up the current patch collection.
330     let cur_collection = PatchCollection::parse_from_file(patches_path)
331         .with_context(|| format!("parsing {} PATCHES.json", platform))?;
332     let cur_collection = filter_patches_by_platform(&cur_collection, platform);
333     let cur_collection = cur_collection.filter_patches(|p| cur_collection.patch_exists(p));
334 
335     // Set up the old patch collection.
336     let old_collection = PatchCollection::parse_from_str(
337         patches_path.parent().unwrap().to_path_buf(),
338         old_patch_contents,
339     )?;
340     let old_collection = old_collection.filter_patches(|p| old_collection.patch_exists(p));
341 
342     // Set up the differential values
343     let version_updates = cur_collection.version_range_diffs(&old_collection);
344     let new_patches: PatchCollection = cur_collection.subtract(&old_collection)?;
345     let new_patches = new_patches.map_patches(|p| {
346         let mut platforms = BTreeSet::new();
347         platforms.extend(["android".to_string(), "chromiumos".to_string()]);
348         PatchDictSchema {
349             platforms: platforms.union(&p.platforms).cloned().collect(),
350             ..p.to_owned()
351         }
352     });
353     Ok(PatchTemporalDiff {
354         cur_collection,
355         new_patches,
356         version_updates,
357     })
358 }
359 
360 /// Create a new collection with only the patches that apply to the
361 /// given platform.
362 ///
363 /// If there's no platform listed, the patch should still apply if the patch file exists.
filter_patches_by_platform(collection: &PatchCollection, platform: &str) -> PatchCollection364 pub fn filter_patches_by_platform(collection: &PatchCollection, platform: &str) -> PatchCollection {
365     collection.filter_patches(|p| {
366         p.platforms.contains(platform) || (p.platforms.is_empty() && collection.patch_exists(p))
367     })
368 }
369 
370 /// Get the hash from the patch file contents.
371 ///
372 /// Not every patch file actually contains its own hash,
373 /// we must compute the hash ourselves when it's not found.
hash_from_patch(patch_contents: impl Read) -> Result<String>374 fn hash_from_patch(patch_contents: impl Read) -> Result<String> {
375     let mut reader = BufReader::new(patch_contents);
376     let mut buf = String::new();
377     reader.read_line(&mut buf)?;
378     let mut first_line_iter = buf.trim().split(' ').fuse();
379     let (fst_word, snd_word) = (first_line_iter.next(), first_line_iter.next());
380     if let (Some("commit" | "From"), Some(hash_str)) = (fst_word, snd_word) {
381         // If the first line starts with either "commit" or "From", the following
382         // text is almost certainly a commit hash.
383         Ok(hash_str.to_string())
384     } else {
385         // This is an annoying case where the patch isn't actually a commit.
386         // So we'll hash the entire file, and hope that's sufficient.
387         let mut hasher = Sha256::new();
388         hasher.update(&buf); // Have to hash the first line.
389         reader.read_to_string(&mut buf)?;
390         hasher.update(buf); // Hash the rest of the file.
391         let sha = hasher.finalize();
392         Ok(format!("{:x}", &sha))
393     }
394 }
395 
hash_from_patch_path(patch: &Path) -> Result<String>396 fn hash_from_patch_path(patch: &Path) -> Result<String> {
397     let f = File::open(patch).with_context(|| format!("opening patch file {}", patch.display()))?;
398     hash_from_patch(f)
399 }
400 
401 /// Copy a file from one path to another, and create any parent
402 /// directories along the way.
copy_create_parents(from: &Path, to: &Path) -> Result<()>403 fn copy_create_parents(from: &Path, to: &Path) -> Result<()> {
404     let to_parent = to
405         .parent()
406         .with_context(|| format!("getting parent of {}", to.display()))?;
407     if !to_parent.exists() {
408         std::fs::create_dir_all(to_parent)?;
409     }
410 
411     copy(&from, &to)
412         .with_context(|| format!("copying file from {} to {}", &from.display(), &to.display()))?;
413     Ok(())
414 }
415 
416 #[cfg(test)]
417 mod test {
418 
419     use super::*;
420 
421     /// Test we can extract the hash from patch files.
422     #[test]
test_hash_from_patch()423     fn test_hash_from_patch() {
424         // Example git patch from Gerrit
425         let desired_hash = "004be4037e1e9c6092323c5c9268acb3ecf9176c";
426         let test_file_contents = "commit 004be4037e1e9c6092323c5c9268acb3ecf9176c\n\
427             Author: An Author <some_email>\n\
428             Date:   Thu Aug 6 12:34:16 2020 -0700";
429         assert_eq!(
430             &hash_from_patch(test_file_contents.as_bytes()).unwrap(),
431             desired_hash
432         );
433 
434         // Example git patch from upstream
435         let desired_hash = "6f85225ef3791357f9b1aa097b575b0a2b0dff48";
436         let test_file_contents = "From 6f85225ef3791357f9b1aa097b575b0a2b0dff48\n\
437             Mon Sep 17 00:00:00 2001\n\
438             From: Another Author <another_email>\n\
439             Date: Wed, 18 Aug 2021 15:03:03 -0700";
440         assert_eq!(
441             &hash_from_patch(test_file_contents.as_bytes()).unwrap(),
442             desired_hash
443         );
444     }
445 
446     #[test]
test_union()447     fn test_union() {
448         let patch1 = PatchDictSchema {
449             rel_patch_path: "a".into(),
450             metadata: None,
451             platforms: BTreeSet::from(["x".into()]),
452             version_range: Some(VersionRange {
453                 from: Some(0),
454                 until: Some(1),
455             }),
456         };
457         let patch2 = PatchDictSchema {
458             rel_patch_path: "b".into(),
459             platforms: BTreeSet::from(["x".into(), "y".into()]),
460             ..patch1.clone()
461         };
462         let patch3 = PatchDictSchema {
463             platforms: BTreeSet::from(["z".into(), "x".into()]),
464             ..patch1.clone()
465         };
466         let collection1 = PatchCollection {
467             workdir: PathBuf::new(),
468             patches: vec![patch1, patch2],
469         };
470         let collection2 = PatchCollection {
471             workdir: PathBuf::new(),
472             patches: vec![patch3],
473         };
474         let union = collection1
475             .union_helper(
476                 &collection2,
477                 |p| Ok(p.rel_patch_path.to_string()),
478                 |p| Ok(p.rel_patch_path.to_string()),
479             )
480             .expect("could not create union");
481         assert_eq!(union.patches.len(), 2);
482         assert_eq!(
483             union.patches[0].platforms.iter().collect::<Vec<&String>>(),
484             vec!["x", "z"]
485         );
486         assert_eq!(
487             union.patches[1].platforms.iter().collect::<Vec<&String>>(),
488             vec!["x", "y"]
489         );
490     }
491 
492     #[test]
test_union_empties()493     fn test_union_empties() {
494         let patch1 = PatchDictSchema {
495             rel_patch_path: "a".into(),
496             metadata: None,
497             platforms: Default::default(),
498             version_range: Some(VersionRange {
499                 from: Some(0),
500                 until: Some(1),
501             }),
502         };
503         let collection1 = PatchCollection {
504             workdir: PathBuf::new(),
505             patches: vec![patch1.clone()],
506         };
507         let collection2 = PatchCollection {
508             workdir: PathBuf::new(),
509             patches: vec![patch1],
510         };
511         let union = collection1
512             .union_helper(
513                 &collection2,
514                 |p| Ok(p.rel_patch_path.to_string()),
515                 |p| Ok(p.rel_patch_path.to_string()),
516             )
517             .expect("could not create union");
518         assert_eq!(union.patches.len(), 1);
519         assert_eq!(union.patches[0].platforms.len(), 0);
520     }
521 
522     #[test]
test_version_differentials()523     fn test_version_differentials() {
524         let fixture = version_range_fixture();
525         let diff = fixture[0].version_range_diffs(&fixture[1]);
526         assert_eq!(diff.len(), 1);
527         assert_eq!(
528             &diff,
529             &[(
530                 "a".to_string(),
531                 Some(VersionRange {
532                     from: Some(0),
533                     until: Some(1)
534                 })
535             )]
536         );
537         let diff = fixture[1].version_range_diffs(&fixture[2]);
538         assert_eq!(diff.len(), 0);
539     }
540 
541     #[test]
test_version_updates()542     fn test_version_updates() {
543         let fixture = version_range_fixture();
544         let collection = fixture[0].update_version_ranges(&[("a".into(), None)]);
545         assert_eq!(collection.patches[0].version_range, None);
546         assert_eq!(collection.patches[1], fixture[1].patches[1]);
547         let new_version_range = Some(VersionRange {
548             from: Some(42),
549             until: Some(43),
550         });
551         let collection = fixture[0].update_version_ranges(&[("a".into(), new_version_range)]);
552         assert_eq!(collection.patches[0].version_range, new_version_range);
553         assert_eq!(collection.patches[1], fixture[1].patches[1]);
554     }
555 
version_range_fixture() -> Vec<PatchCollection>556     fn version_range_fixture() -> Vec<PatchCollection> {
557         let patch1 = PatchDictSchema {
558             rel_patch_path: "a".into(),
559             metadata: None,
560             platforms: Default::default(),
561             version_range: Some(VersionRange {
562                 from: Some(0),
563                 until: Some(1),
564             }),
565         };
566         let patch1_updated = PatchDictSchema {
567             version_range: Some(VersionRange {
568                 from: Some(0),
569                 until: Some(3),
570             }),
571             ..patch1.clone()
572         };
573         let patch2 = PatchDictSchema {
574             rel_patch_path: "b".into(),
575             ..patch1.clone()
576         };
577         let collection1 = PatchCollection {
578             workdir: PathBuf::new(),
579             patches: vec![patch1, patch2.clone()],
580         };
581         let collection2 = PatchCollection {
582             workdir: PathBuf::new(),
583             patches: vec![patch1_updated, patch2.clone()],
584         };
585         let collection3 = PatchCollection {
586             workdir: PathBuf::new(),
587             patches: vec![patch2],
588         };
589         vec![collection1, collection2, collection3]
590     }
591 }
592