Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions crates/socket-patch-core/src/api/blob_fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,21 @@ async fn download_hashes(

match client.fetch_blob(hash).await {
Ok(Some(data)) => {
// Verify content hash matches expected hash before writing
let actual_hash = crate::hash::git_sha256::compute_git_sha256_from_bytes(&data);
if actual_hash != *hash {
results.push(BlobFetchResult {
hash: hash.clone(),
success: false,
error: Some(format!(
"Content hash mismatch: expected {}, got {}",
hash, actual_hash
)),
});
failed += 1;
continue;
}

let blob_path: PathBuf = blobs_path.join(hash);
match tokio::fs::write(&blob_path, &data).await {
Ok(()) => {
Expand Down
15 changes: 11 additions & 4 deletions crates/socket-patch-core/src/manifest/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,24 +104,31 @@ pub fn validate_manifest(value: &serde_json::Value) -> Result<PatchManifest, Str
}

/// Read and parse a manifest from the filesystem.
/// Returns Ok(None) if the file does not exist or cannot be parsed.
/// Returns Ok(None) if the file does not exist.
/// Returns Err for I/O errors, JSON parse errors, or validation errors.
pub async fn read_manifest(path: impl AsRef<Path>) -> Result<Option<PatchManifest>, std::io::Error> {
let path = path.as_ref();

let content = match tokio::fs::read_to_string(path).await {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(_) => return Ok(None),
Err(e) => return Err(e), // FIX: propagate actual I/O error
};

let parsed: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(_) => return Ok(None),
Err(e) => return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to parse manifest JSON: {}", e),
)),
};

match validate_manifest(&parsed) {
Ok(manifest) => Ok(Some(manifest)),
Err(_) => Ok(None),
Err(e) => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
e,
)),
}
}

Expand Down
26 changes: 23 additions & 3 deletions crates/socket-patch-core/src/patch/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,33 @@ pub async fn apply_package_patch(
result.files_verified.push(verify_result);
}

// Check if all files are already patched (or skipped due to NotFound with force)
let all_patched = result
// Check if all files are already patched
let all_already_patched = result
.files_verified
.iter()
.all(|v| v.status == VerifyStatus::AlreadyPatched);

if all_already_patched {
result.success = true;
return result;
}

// Check if all files are either already patched or not found (force mode skip)
let all_done_or_skipped = result
.files_verified
.iter()
.all(|v| v.status == VerifyStatus::AlreadyPatched || v.status == VerifyStatus::NotFound);
if all_patched {

if all_done_or_skipped {
// Some or all files were not found but skipped via --force
let not_found_count = result.files_verified.iter()
.filter(|v| v.status == VerifyStatus::NotFound)
.count();
result.success = true;
result.error = Some(format!(
"All patch files were skipped: {} not found on disk (--force)",
not_found_count
));
return result;
}

Expand Down
Loading