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, ©_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