Skip to content

Commit 7206a37

Browse files
committed
feat: improve soldeer.lock verification with checksum integrity checks
1 parent 036fff5 commit 7206a37

File tree

3 files changed

+100
-29
lines changed

3 files changed

+100
-29
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/forge/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ parking_lot.workspace = true
7474
regex = { workspace = true, default-features = false }
7575
semver.workspace = true
7676
serde_json.workspace = true
77+
sha2 = "0.10"
7778
similar = { version = "2", features = ["inline"] }
7879
solar.workspace = true
7980
strum = { workspace = true, features = ["derive"] }

crates/forge/src/cmd/build.rs

Lines changed: 98 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ use foundry_config::{
2323
filter::expand_globs,
2424
};
2525
use serde::{Deserialize, Serialize};
26-
use std::{path::PathBuf, process::Command};
26+
use sha2::{Digest, Sha256};
27+
use std::{
28+
fs,
29+
io::Read,
30+
path::{Path, PathBuf},
31+
process::Command,
32+
};
2733

2834
foundry_config::merge_impl_figment_convert!(BuildArgs, build);
2935

@@ -32,8 +38,8 @@ struct SoldeerLockEntry {
3238
name: String,
3339
version: String,
3440
source: String,
35-
#[serde(default, rename = "checksum")]
36-
_checksum: Option<String>,
41+
#[serde(default)]
42+
checksum: Option<String>,
3743
}
3844

3945
#[derive(Debug, Deserialize)]
@@ -224,7 +230,7 @@ impl BuildArgs {
224230
})
225231
}
226232

227-
/// Check soldeer.lock file consistency with actual git revisions
233+
/// Check soldeer.lock file consistency with checksums and git revisions
228234
fn check_soldeer_lock_consistency(&self, config: &Config) {
229235
let soldeer_lock_path = config.root.join("soldeer.lock");
230236
if !soldeer_lock_path.exists() {
@@ -242,37 +248,100 @@ impl BuildArgs {
242248
};
243249

244250
for dep in &soldeer_lock.dependencies {
245-
if let Some((_, expected_rev)) = dep.source.split_once('#') {
246-
let dep_dir_name = format!("{}-{}", dep.name, dep.version);
247-
let dep_path = config.root.join("dependencies").join(&dep_dir_name);
248-
249-
if dep_path.exists() {
250-
let actual_rev = Command::new("git")
251-
.args(["rev-parse", "HEAD"])
252-
.current_dir(&dep_path)
253-
.output();
254-
255-
if let Ok(output) = actual_rev
256-
&& output.status.success()
251+
let dep_dir_name = format!("{}-{}", dep.name, dep.version);
252+
let dep_path = config.root.join("dependencies").join(&dep_dir_name);
253+
254+
if !dep_path.exists() {
255+
continue;
256+
}
257+
258+
// Check checksum if available
259+
if let Some(expected_checksum) = &dep.checksum
260+
&& let Ok(actual_checksum) = self.calculate_dependency_checksum(&dep_path)
261+
&& expected_checksum != &actual_checksum
262+
{
263+
sh_warn!(
264+
"Dependency '{}' integrity check failed: \n Expected checksum: {}\n Actual checksum: {}",
265+
dep.name,
266+
expected_checksum,
267+
actual_checksum
268+
).ok();
269+
continue;
270+
}
271+
272+
// For git dependencies, also check revision
273+
if let Some((_, expected_rev)) = dep.source.split_once('#')
274+
&& dep_path.join(".git").exists()
275+
{
276+
let actual_rev =
277+
Command::new("git").args(["rev-parse", "HEAD"]).current_dir(&dep_path).output();
278+
279+
if let Ok(output) = actual_rev
280+
&& output.status.success()
281+
{
282+
let actual_rev = String::from_utf8_lossy(&output.stdout).trim().to_string();
283+
284+
if !actual_rev.starts_with(expected_rev)
285+
&& !expected_rev.starts_with(&actual_rev)
257286
{
258-
let actual_rev = String::from_utf8_lossy(&output.stdout).trim().to_string();
259-
260-
if !actual_rev.starts_with(expected_rev)
261-
&& !expected_rev.starts_with(&actual_rev)
262-
{
263-
sh_warn!(
264-
"Dependency '{}' revision mismatch: \n Expected (from soldeer.lock): {}\n Actual (in {}): {}",
265-
dep.name,
266-
expected_rev,
267-
dep_dir_name,
268-
actual_rev
269-
).ok();
270-
}
287+
sh_warn!(
288+
"Dependency '{}' revision mismatch: \n Expected (from soldeer.lock): {}\n Actual: {}",
289+
dep.name,
290+
expected_rev,
291+
actual_rev
292+
).ok();
271293
}
272294
}
273295
}
274296
}
275297
}
298+
299+
/// Calculate checksum for a dependency directory
300+
fn calculate_dependency_checksum(&self, dep_path: &Path) -> Result<String> {
301+
let mut hasher = Sha256::new();
302+
let mut files = Vec::new();
303+
304+
// Collect all files recursively, excluding .git directory
305+
collect_files_recursive(dep_path, &mut files)?;
306+
307+
// Sort files for consistent hashing
308+
files.sort();
309+
310+
for file_path in files {
311+
// Hash the relative path
312+
let relative_path = file_path.strip_prefix(dep_path).unwrap_or(&file_path);
313+
hasher.update(relative_path.to_string_lossy().as_bytes());
314+
315+
// Hash the file contents
316+
let mut file = fs::File::open(&file_path)?;
317+
let mut buffer = Vec::new();
318+
file.read_to_end(&mut buffer)?;
319+
hasher.update(&buffer);
320+
}
321+
322+
let result = hasher.finalize();
323+
Ok(format!("{result:x}"))
324+
}
325+
}
326+
327+
/// Recursively collect all files in a directory, excluding .git
328+
fn collect_files_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
329+
if dir.file_name() == Some(std::ffi::OsStr::new(".git")) {
330+
return Ok(());
331+
}
332+
333+
for entry in fs::read_dir(dir)? {
334+
let entry = entry?;
335+
let path = entry.path();
336+
337+
if path.is_dir() {
338+
collect_files_recursive(&path, files)?;
339+
} else if path.is_file() {
340+
files.push(path);
341+
}
342+
}
343+
344+
Ok(())
276345
}
277346

278347
// Make this args a `figment::Provider` so that it can be merged into the `Config`

0 commit comments

Comments
 (0)