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
21 changes: 11 additions & 10 deletions crates/socket-patch-core/src/utils/poetry_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub(crate) fn package_files(package: &Table) -> Vec<&dyn TableLike> {

/// The lock-wide `[metadata.files]` entry for package `name` (lock
/// 1.0/1.1; keys compared PEP 503-canonical).
pub(crate) fn metadata_files<'d>(lock: &'d DocumentMut, name: &str) -> Vec<&'d dyn TableLike> {
pub(crate) fn metadata_files<'d>(lock: &'d Table, name: &str) -> Vec<&'d dyn TableLike> {
let canon = canonicalize_pypi_name(name);
let entry = lock
.get("metadata")
Expand All @@ -52,6 +52,10 @@ pub(crate) fn metadata_files<'d>(lock: &'d DocumentMut, name: &str) -> Vec<&'d d
/// Whether a `[metadata.files]` entry is laid out one file per line, which is
/// how Poetry 1.0/1.1 write every NON-empty entry (tomlkit `multiline(True)`);
/// an empty one is `[]`.
///
/// Reads the array's layout, so `item` must come from a `DocumentMut`: a
/// read-only `toml_edit::Document` keeps layout as spans into its source
/// and this would answer `false` for every entry.
pub(crate) fn is_multiline_array(item: &Item) -> bool {
item.as_array().is_some_and(|array| {
let has_break = |raw: Option<&str>| raw.is_some_and(|s| s.contains('\n'));
Expand Down Expand Up @@ -135,13 +139,10 @@ fn multiline_files(files: Array) -> Item {
/// bumps the minor additively — 2.0 → 2.1 kept every shape we rewrite, and the
/// vendored loader already accepts newer minors with an advisory; the hosted
/// path must not refuse what the vendored path accepts).
pub fn lock_version(lock: &DocumentMut) -> Result<&str, String> {
lock_version_of(lock)
}

/// [`lock_version`] of a parsed lock's root table (a `DocumentMut` or a
/// spanned `Document`).
fn lock_version_of(lock: &Table) -> Result<&str, String> {
///
/// Takes the root table of either a `DocumentMut` or a read-only spanned
/// `Document`.
pub fn lock_version(lock: &Table) -> Result<&str, String> {
let metadata = lock
.get("metadata")
.filter(|item| item.is_table_like())
Expand Down Expand Up @@ -616,7 +617,7 @@ fn rewrite_poetry_lock_batch(
&names,
|index, lock| {
if version.is_none() {
*version = lock_version_of(lock).ok().map(str::to_string);
*version = lock_version(lock).ok().map(str::to_string);
}
let dep = &deps[index];
let sha256 = checked_sha256(
Expand Down Expand Up @@ -690,7 +691,7 @@ fn plan_poetry_rewrite(
source_url: &str,
sha256: &str,
) -> Result<Option<PoetryLockPlan>, String> {
let format = lock_version_of(lock)?.to_string();
let format = lock_version(lock)?.to_string();
if format == "0" && source_type == "url" {
return Err("Poetry 0.x ignores URL sources; hosted patches require Poetry >= 1.0".into());
}
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/utils/python_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ impl ArtifactSource<'_> {
/// (uv 0.2.x) or `package` (uv ≥ 0.2.35 and PEP 723 script locks). The
/// lock inventory, lockfile discovery and `lock_inventory::
/// wired_vendor_integrity` all pick the array with this one rule.
pub(crate) fn lock_package_collection(doc: &DocumentMut) -> (&'static str, bool) {
pub(crate) fn lock_package_collection(doc: &Table) -> (&'static str, bool) {
if doc.contains_key("lock-version") {
("packages", true)
} else if doc.contains_key("distribution") {
Expand Down
11 changes: 6 additions & 5 deletions crates/socket-patch-core/src/vendor/lock_inventory/pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use std::path::Path;

use serde_json::Value;
use toml_edit::{DocumentMut, Item, TableLike};
use toml_edit::{Item, Table, TableLike};

use crate::crawlers::python_crawler::canonicalize_pypi_name;
use crate::formats::text::strip_bom;
Expand Down Expand Up @@ -326,7 +326,8 @@ pub(super) fn portable_wheel_artifact(
}

pub(super) fn python_lock_inventory(text: &str) -> Option<Vec<LockfileEntry>> {
let document: DocumentMut = text.parse().ok()?;
// Read-only parse, as in lockfile discovery (`toml_or_diag`, #1473).
let document = toml_edit::Document::parse(text).ok()?;
let (collection, pep751) = lock_package_collection(&document);
// Only the lock formats the fetch layer understands.
let supported = if pep751 {
Expand Down Expand Up @@ -395,7 +396,7 @@ pub(super) fn python_lock_inventory(text: &str) -> Option<Vec<LockfileEntry>> {
/// can be chosen there). A lock that is not TOML contributes nothing.
async fn inventory_poetry_lock(view: &ProjectView<'_>) -> Option<Vec<LockfileEntry>> {
let text = view.read_text("poetry.lock").await.ok()?;
let document: DocumentMut = text.parse().ok()?;
let document = toml_edit::Document::parse(text.as_str()).ok()?;
let pure_wheel_sha = |files: Vec<&dyn TableLike>| {
files.into_iter().find_map(|entry| {
let file = entry.get("file")?.as_str()?;
Expand Down Expand Up @@ -429,7 +430,7 @@ async fn inventory_poetry_lock(view: &ProjectView<'_>) -> Option<Vec<LockfileEnt

/// `(canonical name, version, purl, table)` of every path-safe `[[package]]`
/// of a poetry.lock / pdm.lock document.
fn toml_package_coords(document: &DocumentMut) -> Vec<(String, String, String, &toml_edit::Table)> {
fn toml_package_coords(document: &Table) -> Vec<(String, String, String, &Table)> {
let Some(packages) = document.get("package").and_then(Item::as_array_of_tables) else {
return Vec::new();
};
Expand Down Expand Up @@ -588,7 +589,7 @@ async fn inventory_pipfile_lock(view: &ProjectView<'_>) -> Option<Vec<LockfileEn
/// does not probe, so a lock-only vendored path would not survive a re-scan.
async fn inventory_pdm_lock(view: &ProjectView<'_>) -> Option<Vec<LockfileEntry>> {
let text = view.read_text("pdm.lock").await.ok()?;
let document: DocumentMut = text.parse().ok()?;
let document = toml_edit::Document::parse(text.as_str()).ok()?;
let out: Vec<LockfileEntry> = toml_package_coords(&document)
.into_iter()
.map(|(name, version, purl, _)| LockfileEntry {
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/vex/discover/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ async fn read_toml(ctx: &DiscoverCtx<'_>, file: &str, out: &mut Discovery) -> Op
}

fn parse_toml(file: &str, text: &str, out: &mut Discovery) -> Option<DocumentMut> {
toml_or_diag(file, text, TomlDiag::TrimEnd, out)
toml_or_diag(file, text, TomlDiag::TrimEnd, out).map(toml_edit::Document::into_mut)
}

/// The config file cargo actually reads ([`effective_config_rel_in`]), parsed,
Expand Down
76 changes: 72 additions & 4 deletions crates/socket-patch-core/src/vex/discover/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2245,13 +2245,24 @@ pub(crate) enum TomlDiag {

/// Parse `text` as TOML, recording [`DIAG_LOCKFILE_UNPARSEABLE`] for
/// root-relative `file` (in `style`) on failure.
pub(crate) fn toml_or_diag(
///
/// Discovery only reads, so this is toml_edit's read-only parse: the
/// document keeps byte spans into `text` instead of copying every key,
/// value and comment out of it the way a [`toml_edit::DocumentMut`] does.
/// On a large `poetry.lock` that copy, and freeing it, cost more than the
/// parse itself (#1473). Values read the same either way (`as_str` and
/// friends return the decoded value); only an item's layout (its raw
/// decor, as `poetry_lock::is_multiline_array` reads) needs the source.
/// The PyPI lock extractors read this document directly; the cargo and
/// pyproject/hatch readers still convert it with `into_mut`, as their
/// helpers take a `DocumentMut`.
pub(crate) fn toml_or_diag<S: AsRef<str>>(
file: &str,
text: &str,
text: S,
style: TomlDiag,
out: &mut Discovery,
) -> Option<toml_edit::DocumentMut> {
match text.parse::<toml_edit::DocumentMut>() {
) -> Option<toml_edit::Document<S>> {
match toml_edit::Document::parse(text) {
Ok(doc) => Some(doc),
Err(e) => {
let reason = match style {
Expand Down Expand Up @@ -3158,6 +3169,63 @@ mod tests {
use super::testing::*;
use super::*;

/// The read-only parse [`toml_or_diag`] hands extractors reads every
/// value the way the `DocumentMut` it replaced did (#1473): escaped,
/// literal and multi-line strings decode, quoted and dotted keys
/// resolve, CRLF and a comment between entries change nothing.
#[test]
fn read_only_toml_reads_values_like_a_mutable_parse() {
let text = "# lock\r\n\
[[package]]\r\n\
name = \"a\\u0062c\" # escaped\r\n\
version = '1.0'\r\n\
\"quoted key\" = \"\"\"\r\nmulti\r\nline\"\"\"\r\n\
source.url = 'https://h/x\\y.whl'\r\n\
files = [\r\n {file = \"x.whl\", hash = \"sha256:ab\"}, # one\r\n]\r\n\
\r\n\
[metadata.files]\r\n\
abc = [{file = \"x.whl\", hash = \"sha256:ab\"}]\r\n";
let mutable: toml_edit::DocumentMut = text.parse().unwrap();
let mut out = Discovery::default();
let read_only = toml_or_diag("poetry.lock", text, TomlDiag::Raw, &mut out).unwrap();
assert!(out.diagnostics.is_empty());
let package = |t: &toml_edit::Table| {
t["package"]
.as_array_of_tables()
.unwrap()
.get(0)
.cloned()
.unwrap()
};
let (a, b) = (package(&mutable), package(&read_only));
for key in ["name", "version", "quoted key"] {
assert_eq!(a[key].as_str(), b[key].as_str(), "{key}");
}
assert_eq!(b["name"].as_str(), Some("abc"));
assert_eq!(b["quoted key"].as_str(), Some("multi\r\nline"));
assert_eq!(b["source"]["url"].as_str(), Some("https://h/x\\y.whl"));
let hash = |p: &toml_edit::Table| {
p["files"]
.as_array()
.unwrap()
.get(0)
.unwrap()
.as_inline_table()
.unwrap()["hash"]
.as_str()
.map(str::to_string)
};
assert_eq!(hash(&a), hash(&b));
assert_eq!(
crate::utils::poetry_lock::metadata_files(&read_only, "ABC").len(),
crate::utils::poetry_lock::metadata_files(&mutable, "ABC").len(),
);
assert_eq!(
crate::utils::poetry_lock::metadata_files(&read_only, "ABC").len(),
1
);
}

/// A pyproject-only pypi project (hatch, or any flavor before its first
/// lock) has no install-deciding file: `pyproject.toml` / `hatch.toml`
/// are manifests, so even a pyproject that no longer names the vendored
Expand Down
4 changes: 2 additions & 2 deletions crates/socket-patch-core/src/vex/discover/pypi_locks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ fn str_of<'t>(table: &'t dyn TableLike, key: &str) -> Option<&'t str> {
fn package_tables<'d>(
out: &mut Discovery,
file: &str,
doc: &'d DocumentMut,
doc: &'d Table,
collection: &str,
) -> Option<&'d toml_edit::ArrayOfTables> {
let item = doc.get(collection)?;
Expand Down Expand Up @@ -874,7 +874,7 @@ async fn extract_poetry(ctx: &DiscoverCtx<'_>, out: &mut Discovery) {

/// The pin Poetry verifies `filename` against (see the module docs).
fn poetry_pin(
doc: &DocumentMut,
doc: &Table,
package: &Table,
name: &str,
filename: &str,
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/vex/discover/pypi_other.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ async fn extract_hatch(ctx: &DiscoverCtx<'_>, out: &mut Discovery) {
async fn read_toml(ctx: &DiscoverCtx<'_>, file: &str, out: &mut Discovery) -> Option<DocumentMut> {
let text = ctx.read_text(file, out).await?;
// The TOML lexer skips one leading BOM itself (`formats::text`'s rule).
toml_or_diag(file, &text, TomlDiag::BomFlattened, out)
toml_or_diag(file, &text, TomlDiag::BomFlattened, out).map(toml_edit::Document::into_mut)
}

/// One PEP 508 dependency string. `root_uri_expanded`: whether Hatch
Expand Down
Loading