Skip to content

Commit 20b897a

Browse files
committed
Share one hosted Pipenv URL recognizer
Hosted Pipenv refused to rotate its own pin when --patch-server-url carried a path prefix, and treated its own hosted sdist pins as user sources. Vendored Pipenv called any https host's /patch/pypi/ URL a Socket reference and told the user to run rollback for a foreign source, while a path-prefixed or sdist hosted pin got the "user-declared" remedy instead. Both now ask lock_inventory::pypi::hosted_pypi_reference: the hosted_patch_url_uuids origin policy plus the hosted_artifact_url tail grammar. The two private segment-count grammars are deleted. Vendored Pipenv passes the run's --patch-server-url origin. Fixes #563 Assisted-by: Claude Code:claude-opus-5-5
1 parent 9168a0e commit 20b897a

4 files changed

Lines changed: 205 additions & 85 deletions

File tree

‎crates/socket-patch-core/src/patch/redirect/pipenv.rs‎

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use serde_json::{json, Value};
66

77
use super::{DepOverride, FileEdit, RewriteResult, RewriteWarning};
88
use crate::crawlers::python_crawler::canonicalize_pypi_name;
9+
use crate::vendor::lock_inventory::pypi::hosted_pypi_reference;
910

1011
pub(super) struct Property {
1112
pub(super) name: String,
@@ -242,40 +243,16 @@ pub(super) fn rewrite(
242243
}
243244
}
244245

245-
/// Whether `value` is a Socket-issued hosted reference for `dep` — served
246-
/// from the same origin as the grant's own artifact URL (patch.socket.dev,
247-
/// or a `--patch-server-url` host), with the `/patch/pypi/<name>/<version>/
248-
/// <grant>/<uuid>/<wheel>` shape for this package and version. Such an entry
249-
/// is ours to rotate; anything else is a user's or a fork's source.
246+
/// Whether `value` is a Socket-issued hosted reference for `dep`: the shared
247+
/// recognizer ([`hosted_pypi_reference`]) accepts it on the grant's own
248+
/// origin (patch.socket.dev, or a `--patch-server-url` host, path prefix
249+
/// included), and it names this package and version. Such an entry is ours
250+
/// to rotate; anything else is a user's or a fork's source.
250251
fn owned_url(value: &str, dep: &DepOverride) -> bool {
251-
let Ok(url) = reqwest::Url::parse(value) else {
252-
return false;
253-
};
254-
let Ok(ours) = reqwest::Url::parse(&dep.artifact_url) else {
255-
return false;
256-
};
257-
let same_origin = url.scheme() == ours.scheme()
258-
&& url.host_str() == ours.host_str()
259-
&& url.port_or_known_default() == ours.port_or_known_default();
260-
let parts: Vec<_> = url.path().split('/').collect();
261-
(same_origin
262-
|| (url.scheme() == "https" && url.host_str() == Some(super::SOCKET_PATCH_SERVER_HOST)))
263-
&& url.username().is_empty()
264-
&& url.password().is_none()
265-
&& url.query().is_none()
266-
&& parts.len() == 8
267-
&& parts[1] == "patch"
268-
&& parts[2] == "pypi"
269-
&& canonicalize_pypi_name(parts[3]) == canonicalize_pypi_name(&dep.name)
270-
&& parts[4] == dep.version
271-
&& !parts[5].is_empty()
272-
&& !parts[6].is_empty()
273-
&& parts[7].ends_with(".whl")
274-
&& parts[7]
275-
.split('-')
276-
.next()
277-
.is_some_and(|name| canonicalize_pypi_name(name) == canonicalize_pypi_name(&dep.name))
278-
&& parts[7].split('-').nth(1) == Some(dep.version.as_str())
252+
hosted_pypi_reference(value, std::slice::from_ref(&dep.artifact_url)).is_some_and(|coords| {
253+
canonicalize_pypi_name(&coords.name) == canonicalize_pypi_name(&dep.name)
254+
&& coords.version == dep.version
255+
})
279256
}
280257

281258
/// Why a Pipfile.lock plan did not happen. Only a [`PlanError::Conflict`]
@@ -651,6 +628,30 @@ mod tests {
651628
assert!(second.contains("/rotated/") && !second.contains("/tok/"));
652629
}
653630

631+
632+
/// Hosted Pipenv recognizes its own pins through the shared recognizer
633+
/// (#563): a path-prefixed `--patch-server-url` deployment rotates its
634+
/// grant instead of refusing its own previous reference, and a hosted
635+
/// sdist pin is ours too.
636+
#[test]
637+
fn owned_url_accepts_path_prefixed_origins_and_sdists() {
638+
let mut dep = dependency("urllib3", "1.26.18", "patch-one");
639+
dep.artifact_url = "https://patches.internal.example/socket/patch/pypi/urllib3/1.26.18/tok/patch-one/urllib3-1.26.18-py3-none-any.whl".into();
640+
assert!(
641+
owned_url(&dep.artifact_url, &dep),
642+
"the URL hosted mode just wrote is ours"
643+
);
644+
assert!(owned_url("https://patch.socket.dev/patch/pypi/urllib3/1.26.18/tok/patch-one/urllib3-1.26.18.tar.gz", &dep));
645+
assert!(!owned_url("https://patches.internal.example.org/socket/patch/pypi/urllib3/1.26.18/tok/patch-one/urllib3-1.26.18-py3-none-any.whl", &dep));
646+
assert!(!owned_url("https://user@patch.socket.dev/patch/pypi/urllib3/1.26.18/tok/patch-one/urllib3-1.26.18-py3-none-any.whl", &dep));
647+
assert!(!owned_url("https://patch.socket.dev/patch/pypi/requests/2.28.1/tok/patch-one/requests-2.28.1-py3-none-any.whl", &dep));
648+
let (first, _) = plan(&lock(), &dep, None).unwrap();
649+
dep.artifact_url = dep.artifact_url.replace("/tok/", "/rotated/");
650+
let (second, rotation) =
651+
plan(&first, &dep, None).expect("rotation on a path-prefixed origin");
652+
assert!(!rotation.is_empty());
653+
assert!(second.contains("/rotated/") && !second.contains("/tok/"));
654+
}
654655
}
655656

656657
#[cfg(test)]

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,25 @@ pub(crate) fn hosted_artifact_url(url: &str) -> Result<HostedArtifactUrl, String
165165

166166
// ── registry view ──
167167

168+
/// A Socket-HOSTED pypi patch reference's coordinates, or `None` when
169+
/// `url` is not one — the ONE "is this lock entry ours" grammar hosted
170+
/// Pipenv rotation (`redirect::pipenv`) and the vendored Pipenv guard
171+
/// (`vendor::pypi_pipenv`) share. Ours means both: served from an accepted
172+
/// origin (patch.socket.dev or one of `origins`, with no userinfo —
173+
/// [`crate::patch::redirect::hosted_patch_url_uuids`]'s origin policy), and
174+
/// a [`hosted_artifact_url`] whose `…/patch/pypi/<name>/<version>/<grant>/
175+
/// <uuid>/<artifact>` tail is matched from the END, so a path-prefixed
176+
/// `--patch-server-url` deployment and a hosted sdist are recognized too.
177+
pub(crate) fn hosted_pypi_reference(url: &str, origins: &[String]) -> Option<HostedArtifactUrl> {
178+
crate::patch::redirect::hosted_patch_url_uuids(url, origins)?;
179+
hosted_artifact_url(url).ok().filter(|coords| {
180+
coords
181+
.uuid_level
182+
.as_deref()
183+
.is_some_and(|uuid| !uuid.is_empty())
184+
})
185+
}
186+
168187
/// Inventory the pypi lock the project carries. Fetchable resolution
169188
/// (URL + sha256 of a pure `-none-any` wheel) comes from `uv.lock` and
170189
/// PEP 751 / PEP 723 script locks; `poetry.lock` entries carry the pure

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

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,7 @@ async fn pypi_prelude<'p>(
694694
dry_run: bool,
695695
pipenv_version: &tokio::sync::OnceCell<Option<u32>>,
696696
installed_sites: &InstalledSiteListings,
697+
hosted_origins: &[String],
697698
) -> Result<PypiPrelude<'p>, VendorOutcome> {
698699
// The purl may carry `?artifact_id=` variant qualifiers; everything here
699700
// keys off the qualifier-free base.
@@ -866,6 +867,7 @@ async fn pypi_prelude<'p>(
866867
&canon_name,
867868
&record.uuid,
868869
version,
870+
hosted_origins,
869871
) {
870872
Ok(target) => target,
871873
// A refusal carries no warnings: probe nothing for it.
@@ -1011,6 +1013,9 @@ pub(crate) async fn service_preflight(
10111013
false,
10121014
pipenv_version,
10131015
installed_sites,
1016+
// Only the verdict matters here, and the hosted-reference refusal
1017+
// carries the same code as the user-declared one.
1018+
&[],
10141019
)
10151020
.await
10161021
.ok()
@@ -1038,6 +1043,10 @@ pub async fn vendor_pypi_with_pipenv_version<'a>(
10381043
installed_sites: &InstalledSiteListings,
10391044
) -> VendorOutcome {
10401045
let site_packages = site_packages.into();
1046+
let hosted_origins: Vec<String> = service
1047+
.and_then(|s| s.patch_server_url.clone())
1048+
.into_iter()
1049+
.collect();
10411050
let PypiPrelude {
10421051
base,
10431052
raw_name,
@@ -1058,6 +1067,7 @@ pub async fn vendor_pypi_with_pipenv_version<'a>(
10581067
dry_run,
10591068
pipenv_version,
10601069
installed_sites,
1070+
&hosted_origins,
10611071
)
10621072
.await
10631073
{
@@ -1283,6 +1293,7 @@ pub async fn vendor_pypi_with_pipenv_version<'a>(
12831293
&rel_wheel,
12841294
&artifact.sha256_hex,
12851295
&record.uuid,
1296+
&hosted_origins,
12861297
)
12871298
.await
12881299
.map(|(wiring, meta)| (wiring, MetaSlot::Pipenv(meta))),
@@ -4214,10 +4225,18 @@ wheels = [
42144225
.unwrap();
42154226
let rel_wheel = format!(".socket/vendor/pypi/{UUID}/six-1.16.0-py2.py3-none-any.whl");
42164227
let p = load_pipenv_project(root).await.unwrap();
4217-
let (wiring, _meta) =
4218-
wire_pipenv(&p, root, "six", "1.16.0", &rel_wheel, &"0".repeat(64), UUID)
4219-
.await
4220-
.unwrap();
4228+
let (wiring, _meta) = wire_pipenv(
4229+
&p,
4230+
root,
4231+
"six",
4232+
"1.16.0",
4233+
&rel_wheel,
4234+
&"0".repeat(64),
4235+
UUID,
4236+
&[],
4237+
)
4238+
.await
4239+
.unwrap();
42214240
let uuid_dir = root.join(format!(".socket/vendor/pypi/{UUID}"));
42224241
tokio::fs::create_dir_all(&uuid_dir).await.unwrap();
42234242
let wheel = uuid_dir.join("six-1.16.0-py2.py3-none-any.whl");
@@ -4279,6 +4298,7 @@ wheels = [
42794298
&rel_wheel,
42804299
&"0".repeat(64),
42814300
UUID,
4301+
&[],
42824302
)
42834303
.await
42844304
.unwrap_or_else(|_| panic!("rewire"));
@@ -4354,10 +4374,18 @@ wheels = [
43544374
.unwrap();
43554375
let rel_wheel = format!(".socket/vendor/pypi/{UUID}/six-1.16.0-py2.py3-none-any.whl");
43564376
let p = load_pipenv_project(root).await.unwrap();
4357-
let (wiring, _meta) =
4358-
wire_pipenv(&p, root, "six", "1.16.0", &rel_wheel, &"0".repeat(64), UUID)
4359-
.await
4360-
.unwrap();
4377+
let (wiring, _meta) = wire_pipenv(
4378+
&p,
4379+
root,
4380+
"six",
4381+
"1.16.0",
4382+
&rel_wheel,
4383+
&"0".repeat(64),
4384+
UUID,
4385+
&[],
4386+
)
4387+
.await
4388+
.unwrap();
43614389
let uuid_dir = root.join(format!(".socket/vendor/pypi/{UUID}"));
43624390
tokio::fs::create_dir_all(&uuid_dir).await.unwrap();
43634391
let wheel = uuid_dir.join("six-1.16.0-py2.py3-none-any.whl");
@@ -4489,10 +4517,18 @@ wheels = [
44894517
.unwrap();
44904518
let rel_wheel = format!(".socket/vendor/pypi/{UUID}/six-1.16.0-py2.py3-none-any.whl");
44914519
let p = load_pipenv_project(root).await.unwrap();
4492-
let (wiring, _meta) =
4493-
wire_pipenv(&p, root, "six", "1.16.0", &rel_wheel, &"0".repeat(64), UUID)
4494-
.await
4495-
.unwrap();
4520+
let (wiring, _meta) = wire_pipenv(
4521+
&p,
4522+
root,
4523+
"six",
4524+
"1.16.0",
4525+
&rel_wheel,
4526+
&"0".repeat(64),
4527+
UUID,
4528+
&[],
4529+
)
4530+
.await
4531+
.unwrap();
44964532
let uuid_dir = root.join(format!(".socket/vendor/pypi/{UUID}"));
44974533
tokio::fs::create_dir_all(&uuid_dir).await.unwrap();
44984534
tokio::fs::write(uuid_dir.join("six-1.16.0-py2.py3-none-any.whl"), b"wheel")

0 commit comments

Comments
 (0)