Skip to content
Merged
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
67 changes: 55 additions & 12 deletions crates/socket-patch-core/src/utils/pdm_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ pub fn legacy_files_key(package: &Table) -> Option<String> {
Some(format!("{name}{suffix} {version}"))
}

/// The package a `[metadata.files]` key is for: [`legacy_files_key`] read
/// back (`<name> <version>`, or `<name>[<extras>] <version>` for a unit with
/// extras). A key that is a bare name, as Poetry's are, is its own answer.
pub fn files_key_package(key: &str) -> &str {
let unit = key.split_whitespace().next().unwrap_or_default();
unit.strip_suffix(']')
.and_then(|open| open.split_once('['))
.map_or(unit, |(name, _)| name)
}

pub fn files_for<'a>(lock: &'a Table, package: &'a Table) -> Option<&'a Array> {
package.get("files").and_then(Item::as_array).or_else(|| {
lock.get("metadata")?
Expand Down Expand Up @@ -497,7 +507,20 @@ pub(crate) mod tests {
const WHEEL: &str = "urllib3-1.26.18-py2.py3-none-any.whl";
const PATH: &str = "./.socket/vendor/pypi/e828efa5-5c6d-43f3-9909-03f5ac232b98/urllib3-1.26.18-py2.py3-none-any.whl";

fn fixture(name: &str) -> String {
/// The native locks the rewriter takes (tests/fixtures/pdm-native).
pub(crate) const REWRITABLE: [&str; 9] = [
"0.12.3",
"2.8.2",
"2.9.3",
"2.10.4",
"2.11.2",
"2.17.3",
"2.29.2",
"0.12.3-extras",
"2.29.2-extras",
];

pub(crate) fn fixture(name: &str) -> String {
std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/pdm-native")
Expand All @@ -508,17 +531,7 @@ pub(crate) mod tests {

#[test]
fn native_formats_rewrite_and_reverse_byte_exactly() {
for version in [
"0.12.3",
"2.8.2",
"2.9.3",
"2.10.4",
"2.11.2",
"2.17.3",
"2.29.2",
"0.12.3-extras",
"2.29.2-extras",
] {
for version in REWRITABLE {
for ending in ["\n", "\r\n"] {
let original = fixture(version).replace('\n', ending);
for (kind, source) in [("path", PATH), ("path", &PATH.replace('/', "\\")), ("url", "https://patch.socket.dev/patch/pypi/urllib3/1.26.18/7e52b8b6-53f2-4dc8-860a-1ae7ebd8be0e/e828efa5-5c6d-43f3-9909-03f5ac232b98/urllib3-1.26.18-py2.py3-none-any.whl")] {
Expand All @@ -545,6 +558,36 @@ pub(crate) mod tests {
}
}

/// The key the writer builds for a unit reads back as the unit's
/// package, with extras or without: the revert's confinement check
/// depends on it.
#[test]
fn files_key_package_reads_back_every_legacy_key() {
for version in ["0.12.3", "0.12.3-extras"] {
let lock: DocumentMut = fixture(version).parse().unwrap();
let packages = lock["package"].as_array_of_tables().unwrap();
assert!(!packages.is_empty(), "{version}");
for package in packages {
let key = legacy_files_key(package).unwrap();
assert!(lock["metadata"]["files"].get(&key).is_some(), "{key}");
assert_eq!(
files_key_package(&key),
pdm_files_key_name(package["name"].as_str().unwrap()),
"{key}"
);
}
}
for (key, package) in [
("six", "six"),
("zope.interface 5.0", "zope.interface"),
("urllib3[brotli,socks] 1.26.18", "urllib3"),
("odd] 1.0", "odd]"),
("", ""),
] {
assert_eq!(files_key_package(key), package, "{key:?}");
}
}

#[test]
fn native_installers_that_lose_source_identity_refuse() {
for version in ["1.15.5", "2.0.3", "2.1.5", "2.3.4", "2.6.1", "2.7.4"] {
Expand Down
55 changes: 49 additions & 6 deletions crates/socket-patch-core/src/vendor/pypi_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1209,19 +1209,21 @@ fn toml_json(item: &Item) -> serde_json::Value {
/// of the key `parent`: array elements that name the package (a
/// requirement string, a `{ name = … }` entry, a lock unit), its key in a
/// `sources` table or in a lock's `files` / `hashes` table (Poetry's and
/// PDM's older formats, the latter keyed `<name> <version>`), the Hatch
/// PDM's older formats, the latter keyed `<name> <version>`, or
/// `<name>[<extras>] <version>` for a unit with extras), the Hatch
/// direct-reference permission, and then every table and array left
/// empty.
fn drop_package(value: &mut serde_json::Value, parent: &str, canon: &str) {
use serde_json::Value as Json;
let names = |name: &str| canonicalize_pypi_name(name) == canon;
match value {
Json::Object(map) => {
let keyed = matches!(parent, "sources" | "files" | "hashes");
map.retain(|key, _| {
key != "allow-direct-references"
&& !(keyed && names(key.split_whitespace().next().unwrap_or_default()))
});
let own = |key: &str| match parent {
"sources" => names(key.split_whitespace().next().unwrap_or_default()),
"files" | "hashes" => names(crate::utils::pdm_lock::files_key_package(key)),
_ => false,
};
map.retain(|key, _| key != "allow-direct-references" && !own(key));
for (key, child) in map.iter_mut() {
drop_package(child, key, canon);
}
Expand Down Expand Up @@ -3676,6 +3678,47 @@ mod tests {
}
}

/// A revert of what the PDM writer wrote is the package's own in every
/// native lock format. PDM 0.x keys an extras unit's files
/// `<name>[<extras>] <version>`: read as another package's key, it made
/// `rollback` refuse to undo its own vendoring of a dependency declared
/// with extras.
#[test]
fn confinement_allows_a_pdm_revert_in_every_native_format() {
use crate::utils::pdm_lock::tests::{fixture as native_lock, REWRITABLE};
const WHEEL: &str = "urllib3-1.26.18-py2.py3-none-any.whl";
for fixture in REWRITABLE {
let original = native_lock(fixture);
let vendored = crate::utils::pdm_lock::rewrite_pdm_lock(
&original,
"urllib3",
"1.26.18",
("path", &format!("./.socket/vendor/pypi/u/{WHEEL}")),
WHEEL,
&"a".repeat(64),
)
.unwrap();
assert_ne!(vendored, original, "{fixture}");
assert_eq!(
confinement_refusal(&vendored, &original, "urllib3"),
None,
"{fixture}"
);
}

// Another package's extras key is still not the package's own, and
// a `sources` key is a name: nothing is read off the end of one.
let wired = "[metadata]\ncontent_hash = \"h\"\n\n[metadata.files]\n\"six[socks] 1.16.0\" = [{ file = \"six.whl\", hash = \"sha256:cc\" }]\n\"idna[socks] 3.7\" = [{ file = \"idna.whl\", hash = \"sha256:dd\" }]\n";
let restored = wired.replace("sha256:cc", "sha256:bb");
assert_eq!(confinement_refusal(wired, &restored, "six"), None);
assert!(
confinement_refusal(wired, &restored.replace("sha256:dd", "sha256:ee"), "six")
.is_some()
);
let sourced = "[tool.uv.sources]\n\"six[socks]\" = { path = \"a\" }\n";
assert!(confinement_refusal(sourced, &sourced.replace("\"a\"", "\"b\""), "six").is_some());
}

/// ...and what it may not: another package, a setting, or anything in a
/// file it cannot read as TOML.
#[test]
Expand Down
60 changes: 60 additions & 0 deletions crates/socket-patch-core/src/vendor/pypi_pdm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,66 @@ distribution = false
}
}

/// PDM 0.x keeps an extras unit's file hashes under a key of its own,
/// `<name>[<extras>] <version>`. Vendoring rewrites both keys and the
/// revert takes both back (5.1.0 refused it as a change to another
/// package's entry).
#[tokio::test]
async fn revert_round_trip_restores_a_legacy_lock_with_extras() {
const WHEEL: &str = "urllib3-1.26.18-py2.py3-none-any.whl";
let rel_wheel = format!(".socket/vendor/pypi/{UUID}/{WHEEL}");
let before = crate::utils::pdm_lock::tests::fixture("0.12.3-extras");
let tmp = write_project(
&before,
"[tool.pdm]\nname = \"legacy-extras\"\nversion = \"0.0.0\"\npython_requires = \">=3.8\"\n\n[tool.pdm.dependencies]\nurllib3 = {version = \"==1.26.18\", extras = [\"socks\"]}\n",
)
.await;
let p = load_pdm_project(tmp.path()).await.unwrap();
let (wiring, meta) = wire_pdm(
&p,
tmp.path(),
"urllib3",
"1.26.18",
&rel_wheel,
WHEEL,
WHEEL_SHA,
UUID,
&[WHEEL_SHA],
)
.await
.unwrap();
let vendored = read_lock(tmp.path()).await;
assert_eq!(vendored.matches(&rel_wheel).count(), 2, "{vendored}");
assert_eq!(
vendored.matches(&format!("sha256:{WHEEL_SHA}")).count(),
2,
"{vendored}"
);
let entry = VendorEntry {
flavor: Some("pdm".into()),
pdm: Some(meta),
..VendorEntry::new(
"pypi".into(),
"pkg:pypi/urllib3@1.26.18".into(),
UUID.into(),
VendorArtifact {
yarn_berry10c0: None,
path: rel_wheel.clone(),
sha256: WHEEL_SHA.into(),
size: Some(11053),
platform_locked: None,
file_inventory: None,
},
wiring,
)
};

let outcome = revert_pdm(&entry, tmp.path(), false).await;
assert!(outcome.success, "{:?}", outcome.error);
assert!(outcome.warnings.is_empty(), "{:?}", outcome.warnings);
assert_eq!(read_lock(tmp.path()).await, before, "byte-identical revert");
}

/// The lock file is user-owned: wiring the splice must not reset its
/// permission bits (see `atomic_write_bytes_preserving_mode`; the revert
/// leg is covered in common.rs).
Expand Down
Loading