Skip to content

Commit 70b2147

Browse files
committed
Refuse an unreadable include outside the root
Final-review follow-up for #1851. If an out-of-root -r include existed but couldn't be read or decoded, the planner skipped it and wired the lock anyway, while discovery reported the same file as unreadable. pip may still install pins from that file, so vendor now refuses the package with pypi_requirements_outside_root and names the file. The vendor-dir check for direct references also normalizes Windows backslashes now, matching discovery. Refs #1851 Assisted-by: Claude Code:claude-opus-5-5
1 parent 713eec9 commit 70b2147

2 files changed

Lines changed: 50 additions & 12 deletions

File tree

‎crates/socket-patch-core/src/vendor/pypi.rs‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ async fn refuse_pin_outside_root(
258258
return Ok(());
259259
};
260260
match super::pypi_requirements::pin_outside_root(project_root, name, version).await {
261-
Some(rel) => Err(super::pypi_requirements::outside_root_refusal(&rel, name)),
261+
Some(refusal) => Err(refusal),
262262
None => Ok(()),
263263
}
264264
}
@@ -4242,6 +4242,24 @@ mod tests {
42424242
);
42434243
}
42444244

4245+
// An include pip may read but this walk can't decode: its pins are
4246+
// unknowable, so it refuses too.
4247+
{
4248+
let tmp = tempfile::tempdir().unwrap();
4249+
let root = tmp.path().join("app");
4250+
tokio::fs::create_dir_all(&root).await.unwrap();
4251+
tokio::fs::write(tmp.path().join("base.txt"), b"\x80\x81six==1.16.0\n")
4252+
.await
4253+
.unwrap();
4254+
touch(&root, "pylock.toml", &pylock).await;
4255+
touch(&root, "requirements.txt", "-r ../base.txt\n").await;
4256+
let (code, detail) = detect_pypi_flavor(&root, Some(("six", "1.16.0")))
4257+
.await
4258+
.unwrap_err();
4259+
assert_eq!(code, "pypi_requirements_outside_root", "{detail}");
4260+
assert!(detail.contains("cannot be read"), "{detail}");
4261+
}
4262+
42454263
// A bare artifact of another release, or of another package, is
42464264
// not this pin.
42474265
for other in [

‎crates/socket-patch-core/src/vendor/pypi_requirements.rs‎

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -459,25 +459,45 @@ pub(super) async fn sibling_pin_wirable(root: &Path, canon_name: &str, version:
459459
/// per-requirement options allowed), a direct reference or editable
460460
/// checkout naming the package (any version: its artifact is not ours),
461461
/// and a bare url / VCS / path line whose artifact filename or `#egg=`
462-
/// names it (at the filename's version, else any).
462+
/// names it (at the filename's version, else any). An out-of-root include
463+
/// that exists but can't be read or decoded refuses too: its pins are
464+
/// unknowable. `Some` is the refusal to return.
463465
pub(super) async fn pin_outside_root(
464466
root: &Path,
465467
canon_name: &str,
466468
version: &str,
467-
) -> Option<String> {
469+
) -> Option<(&'static str, String)> {
468470
let view = crate::vendor::lock_inventory::ProjectView::Disk(root);
469471
let mut found = None;
470472
let _ = walk_requirements_tree(view, decode_requirements, |rel, read| {
471-
let Ok(content) = read else {
473+
if found.is_some() {
472474
return Ok::<bool, std::convert::Infallible>(false);
473-
};
474-
if found.is_none()
475-
&& !is_in_root_rel(rel)
476-
&& installs_outside_vendor(&content, canon_name, version)
477-
{
478-
found = Some(rel.to_string());
479475
}
480-
Ok(true)
476+
let outside = !is_in_root_rel(rel);
477+
match read {
478+
Ok(content) => {
479+
if outside && installs_outside_vendor(&content, canon_name, version) {
480+
found = Some(outside_root_refusal(rel, canon_name));
481+
}
482+
Ok(true)
483+
}
484+
// pip reads an include this walk can't (or fails the install):
485+
// its pins are unknowable, so fail closed rather than wire the
486+
// lock beside it. Discovery diagnoses the same file.
487+
Err(e) if outside && e.kind() != std::io::ErrorKind::NotFound => {
488+
found = Some((
489+
"pypi_requirements_outside_root",
490+
format!(
491+
"{rel}: a requirements include outside the project root cannot be \
492+
read ({e}), so vendor cannot tell whether pip installs {canon_name} \
493+
from it; make it readable UTF-8 text or use agent mode (`scan --mode \
494+
agent` + `socket-patch apply`) instead"
495+
),
496+
));
497+
Ok(false)
498+
}
499+
Err(_) => Ok(false),
500+
}
481501
})
482502
.await;
483503
found
@@ -504,7 +524,7 @@ fn installs_outside_vendor(content: &str, canon_name: &str, version: &str) -> bo
504524
return false;
505525
};
506526
req.direct_reference
507-
.is_some_and(|location| !location.contains(".socket/vendor/"))
527+
.is_some_and(|location| !location.replace('\\', "/").contains(".socket/vendor/"))
508528
|| req
509529
.exact_version()
510530
.is_some_and(|pinned| crate::utils::pep440::versions_equal(pinned, version))

0 commit comments

Comments
 (0)