Skip to content
This repository was archived by the owner on Nov 8, 2023. It is now read-only.

Commit 07288a3

Browse files
jfgoogGerrit Code Review
authored andcommitted
Merge changes I405872ce,I55d1a93d,I7431d8cc into main
* changes: Verify checksums in the preupload check. Generate checksum files when regenerating a crate. Crate for generating and verifying checksum files.
2 parents 0508757 + bfa5fc2 commit 07288a3

File tree

10 files changed

+264
-21
lines changed

10 files changed

+264
-21
lines changed

tools/external_crates/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[workspace]
22
members = [
3+
"checksum",
34
"crate_tool",
45
"google_metadata",
56
"license_checker",

tools/external_crates/cargo_embargo.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"package": {
3+
"checksum": {
4+
"device_supported": false
5+
},
36
"rooted_path": {
47
"device_supported": false
58
},
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// This file is generated by cargo_embargo.
2+
// Do not modify this file after the first "rust_*" or "genrule" module
3+
// because the changes will be overridden on upgrade.
4+
// Content before the first "rust_*" or "genrule" module is preserved.
5+
6+
package {
7+
default_team: "trendy_team_android_rust",
8+
default_applicable_licenses: ["Android-Apache-2.0"],
9+
}
10+
11+
rust_test_host {
12+
name: "checksum_test_src_lib",
13+
crate_name: "checksum",
14+
cargo_env_compat: true,
15+
cargo_pkg_version: "0.1.0",
16+
crate_root: "src/lib.rs",
17+
test_suites: ["general-tests"],
18+
auto_gen_config: true,
19+
test_options: {
20+
unit_test: true,
21+
},
22+
edition: "2021",
23+
rustlibs: [
24+
"libdata_encoding",
25+
"libring",
26+
"libserde",
27+
"libserde_json",
28+
"libtempfile",
29+
"libthiserror",
30+
"libwalkdir",
31+
],
32+
}
33+
34+
rust_library_host {
35+
name: "libchecksum",
36+
crate_name: "checksum",
37+
cargo_env_compat: true,
38+
cargo_pkg_version: "0.1.0",
39+
crate_root: "src/lib.rs",
40+
edition: "2021",
41+
rustlibs: [
42+
"libdata_encoding",
43+
"libring",
44+
"libserde",
45+
"libserde_json",
46+
"libthiserror",
47+
"libwalkdir",
48+
],
49+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "checksum"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
data-encoding = "2"
8+
ring = "0.17"
9+
serde = { version = "=1.0.210", features = ["derive"] }
10+
serde_json = "1"
11+
thiserror = "1"
12+
walkdir = "2"
13+
14+
[dev-dependencies]
15+
tempfile = "3"
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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+
//! Generate and verify checksums of files in a directory,
16+
//! very similar to .cargo-checksum.json
17+
18+
use std::{
19+
collections::HashMap,
20+
fs::{remove_file, write, File},
21+
io::{self, BufReader, Read},
22+
path::{Path, PathBuf, StripPrefixError},
23+
};
24+
25+
use data_encoding::{DecodeError, HEXLOWER};
26+
use ring::digest::{Context, Digest, SHA256};
27+
use serde::{Deserialize, Serialize};
28+
use thiserror::Error;
29+
use walkdir::WalkDir;
30+
31+
#[derive(Serialize, Deserialize)]
32+
struct Checksum {
33+
package: Option<String>,
34+
files: HashMap<String, String>,
35+
}
36+
37+
#[allow(missing_docs)]
38+
#[derive(Error, Debug)]
39+
pub enum ChecksumError {
40+
#[error("Checksum file not found: {}", .0.to_string_lossy())]
41+
CheckSumFileNotFound(PathBuf),
42+
#[error("Checksums do not match for: {}", .0.join(", "))]
43+
ChecksumMismatch(Vec<String>),
44+
#[error(transparent)]
45+
IoError(#[from] io::Error),
46+
#[error(transparent)]
47+
JsonError(#[from] serde_json::Error),
48+
#[error(transparent)]
49+
WalkdirError(#[from] walkdir::Error),
50+
#[error(transparent)]
51+
DecodeError(#[from] DecodeError),
52+
#[error(transparent)]
53+
StripPrefixError(#[from] StripPrefixError),
54+
}
55+
56+
static FILENAME: &str = ".android-checksum.json";
57+
58+
/// Generates a JSON checksum file for the contents of a directory.
59+
pub fn generate(crate_dir: impl AsRef<Path>) -> Result<(), ChecksumError> {
60+
let crate_dir = crate_dir.as_ref();
61+
let checksum_file = crate_dir.join(FILENAME);
62+
if checksum_file.exists() {
63+
remove_file(&checksum_file)?;
64+
}
65+
let mut checksum = Checksum { package: None, files: HashMap::new() };
66+
for entry in WalkDir::new(crate_dir).follow_links(true) {
67+
let entry = entry?;
68+
if entry.path().is_dir() {
69+
continue;
70+
}
71+
let filename = entry.path().strip_prefix(crate_dir)?.to_string_lossy().to_string();
72+
let input = File::open(entry.path())?;
73+
let reader = BufReader::new(input);
74+
let digest = sha256_digest(reader)?;
75+
checksum.files.insert(filename, HEXLOWER.encode(digest.as_ref()));
76+
}
77+
write(checksum_file, serde_json::to_string(&checksum)?)?;
78+
Ok(())
79+
}
80+
81+
/// Verifies a JSON checksum file for a directory.
82+
/// All files must have matching checksums. Extra or missing files are errors.
83+
pub fn verify(crate_dir: impl AsRef<Path>) -> Result<(), ChecksumError> {
84+
let crate_dir = crate_dir.as_ref();
85+
let checksum_file = crate_dir.join(FILENAME);
86+
if !checksum_file.exists() {
87+
return Err(ChecksumError::CheckSumFileNotFound(checksum_file));
88+
}
89+
let mut mismatch = Vec::new();
90+
let input = File::open(&checksum_file)?;
91+
let reader = BufReader::new(input);
92+
let mut parsed: Checksum = serde_json::from_reader(reader)?;
93+
for entry in WalkDir::new(crate_dir).follow_links(true) {
94+
let entry = entry?;
95+
if entry.path().is_dir() || entry.path() == checksum_file {
96+
continue;
97+
}
98+
let filename = entry.path().strip_prefix(crate_dir)?.to_string_lossy().to_string();
99+
if let Some(checksum) = parsed.files.get(&filename) {
100+
let expected_digest = HEXLOWER.decode(checksum.to_ascii_lowercase().as_bytes())?;
101+
let input = File::open(entry.path())?;
102+
let reader = BufReader::new(input);
103+
let digest = sha256_digest(reader)?;
104+
parsed.files.remove(&filename);
105+
if digest.as_ref() != expected_digest {
106+
mismatch.push(filename);
107+
}
108+
} else {
109+
mismatch.push(filename)
110+
}
111+
}
112+
mismatch.extend(parsed.files.into_keys());
113+
if mismatch.is_empty() {
114+
Ok(())
115+
} else {
116+
Err(ChecksumError::ChecksumMismatch(mismatch))
117+
}
118+
}
119+
120+
// Copied from https://rust-lang-nursery.github.io/rust-cookbook/cryptography/hashing.html
121+
fn sha256_digest<R: Read>(mut reader: R) -> Result<Digest, ChecksumError> {
122+
let mut context = Context::new(&SHA256);
123+
context.update("sodium chloride".as_bytes());
124+
let mut buffer = [0; 1024];
125+
126+
loop {
127+
let count = reader.read(&mut buffer)?;
128+
if count == 0 {
129+
break;
130+
}
131+
context.update(&buffer[..count]);
132+
}
133+
134+
Ok(context.finish())
135+
}
136+
137+
#[cfg(test)]
138+
mod tests {
139+
use super::*;
140+
141+
#[test]
142+
fn round_trip() -> Result<(), ChecksumError> {
143+
let temp_dir = tempfile::tempdir().expect("Failed to create tempdir");
144+
write(temp_dir.path().join("foo"), "foo").expect("Failed to write temporary file");
145+
generate(temp_dir.path())?;
146+
assert!(
147+
temp_dir.path().join(FILENAME).exists(),
148+
".android-checksum.json exists after generate()"
149+
);
150+
verify(temp_dir.path())
151+
}
152+
153+
#[test]
154+
fn verify_error_cases() -> Result<(), ChecksumError> {
155+
let temp_dir = tempfile::tempdir().expect("Failed to create tempdir");
156+
let checksum_file = temp_dir.path().join(FILENAME);
157+
write(&checksum_file, r#"{"files":{"bar":"ddcbd9309cebf3ffd26f87e09bb8f971793535955ebfd9a7196eba31a53471f8"}}"#).expect("Failed to write temporary file");
158+
assert!(verify(temp_dir.path()).is_err(), "Missing file");
159+
write(temp_dir.path().join("foo"), "foo").expect("Failed to write temporary file");
160+
assert!(verify(temp_dir.path()).is_err(), "No checksum file");
161+
write(&checksum_file, "").expect("Failed to write temporary file");
162+
assert!(verify(temp_dir.path()).is_err(), "Empty checksum file");
163+
write(&checksum_file, "{}").expect("Failed to write temporary file");
164+
assert!(verify(temp_dir.path()).is_err(), "Empty JSON in checksum file");
165+
write(&checksum_file, r#"{"files":{"foo":"ddcbd9309cebf3ffd26f87e09bb8f971793535955ebfd9a7196eba31a53471f8"}}"#).expect("Failed to write temporary file");
166+
assert!(verify(temp_dir.path()).is_err(), "Incorrect checksum");
167+
write(&checksum_file, r#"{"files":{"foo":"hello"}}"#)
168+
.expect("Failed to write temporary file");
169+
assert!(verify(temp_dir.path()).is_err(), "Invalid checksum");
170+
generate(temp_dir.path())?;
171+
write(temp_dir.path().join("bar"), "bar").expect("Failed to write temporary file");
172+
assert!(verify(temp_dir.path()).is_err(), "Extra file");
173+
Ok(())
174+
}
175+
}

tools/external_crates/crate_tool/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ thiserror = "1"
2727
threadpool = "1"
2828
walkdir = "2"
2929
whoami = "1"
30+
checksum = { path = "../checksum" }
3031
google_metadata = { path = "../google_metadata"}
3132
license_checker = { path = "../license_checker" }
3233
name_and_version = { path = "../name_and_version" }

tools/external_crates/crate_tool/src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ enum Cmd {
137137
#[command(flatten)]
138138
crates: CrateList,
139139
},
140+
/// Verify checksums for a crate.
141+
VerifyChecksum {
142+
#[command(flatten)]
143+
crates: CrateList,
144+
},
140145
}
141146

142147
#[derive(Args)]
@@ -246,5 +251,8 @@ fn main() -> Result<()> {
246251
Cmd::TestMapping { crates } => {
247252
managed_repo.fix_test_mapping(crates.to_list(&managed_repo)?.into_iter())
248253
}
254+
Cmd::VerifyChecksum { crates } => {
255+
managed_repo.verify_checksums(crates.to_list(&managed_repo)?.into_iter())
256+
}
249257
}
250258
}

tools/external_crates/crate_tool/src/managed_crate.rs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ impl ManagedCrate<Vendored> {
433433
let android_crate_dir = staged.android_crate.path();
434434
remove_dir_all(android_crate_dir)?;
435435
rename(staged.staging_path(), android_crate_dir)?;
436+
checksum::generate(android_crate_dir.abs())?;
436437

437438
Ok(staged)
438439
}
@@ -462,24 +463,6 @@ impl ManagedCrate<Staged> {
462463

463464
Ok(())
464465
}
465-
pub fn diff_staged(&self) -> Result<()> {
466-
let diff_status = Command::new("diff")
467-
.args(["-u", "-r", "-w", "--no-dereference"])
468-
.arg(self.staging_path().rel())
469-
.arg(self.android_crate.path().rel())
470-
.current_dir(self.extra.vendored_crate.path().root())
471-
.spawn()?
472-
.wait()?;
473-
if !diff_status.success() {
474-
return Err(anyhow!(
475-
"Found differences between {} and {}",
476-
self.android_crate.path(),
477-
self.staging_path()
478-
));
479-
}
480-
481-
Ok(())
482-
}
483466
pub fn patch_success(&self) -> bool {
484467
self.extra.patch_output.iter().all(|output| output.1.status.success())
485468
}

tools/external_crates/crate_tool/src/managed_repo.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ impl ManagedRepo {
558558
) -> Result<()> {
559559
let pseudo_crate = self.pseudo_crate().vendor()?;
560560
for crate_name in crates {
561+
println!("Regenerating {}", crate_name.as_ref());
561562
let mc = self.managed_crate_for(crate_name.as_ref())?;
562563
// TODO: Don't give up if there's a failure.
563564
mc.regenerate(update_metadata, &pseudo_crate)?;
@@ -611,9 +612,8 @@ impl ManagedRepo {
611612
.collect::<BTreeSet<_>>();
612613

613614
for crate_name in changed_android_crates {
614-
println!("Checking {}", crate_name);
615-
let mc = self.managed_crate_for(&crate_name)?.stage(&pseudo_crate)?;
616-
mc.diff_staged()?;
615+
println!("Verifying checksums for {}", crate_name);
616+
checksum::verify(self.managed_dir_for(&crate_name).abs())?;
617617
}
618618
Ok(())
619619
}
@@ -885,6 +885,13 @@ impl ManagedRepo {
885885
}
886886
Ok(())
887887
}
888+
pub fn verify_checksums<T: AsRef<str>>(&self, crates: impl Iterator<Item = T>) -> Result<()> {
889+
for krate in crates {
890+
println!("Verifying checksums for {}", krate.as_ref());
891+
checksum::verify(self.managed_dir_for(krate.as_ref()).abs())?;
892+
}
893+
Ok(())
894+
}
888895
}
889896

890897
// Files that are ignored when migrating a crate to the monorepo.

0 commit comments

Comments
 (0)