GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Attribute gems in nested Gemfile source blocks (#1999) · SocketDev/socket-patch@70ea095

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • crates/socket-patch-core/src/vex/discover
    • gem.rs
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎crates/socket-patch-core/src/vex/discover/gem.rs‎

Copy file name to clipboardExpand all lines: crates/socket-patch-core/src/vex/discover/gem.rs
+101-48Lines changed: 101 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -212,12 +212,17 @@ async fn source_block_gems(
212212

213213
/// The Gemfile grammar [`source_block_gems`] reads: a line
214214
/// `source "<url>" do` / `source('<url>') do` opens a block, a line starting
215-
/// with `end` closes it, and each `gem "<name>"` / `gem('<name>'…` line in
216-
/// between (the vendor backend's own [`gem_declaration_any`] grammar)
217-
/// declares a block-scoped gem. Anything else (nested blocks,
218-
/// `source` with a block argument, `#` comments, and Ruby's column-0
219-
/// `=begin` … `=end` block comments) is ignored — this only ever ADDS a
220-
/// cross-check to lock evidence, never creates a ref on its own.
215+
/// with `end` closes the innermost open one, and each `gem "<name>"` /
216+
/// `gem('<name>'…` line inside a block (the vendor backend's own
217+
/// [`gem_declaration_any`] grammar) declares a gem scoped to the INNERMOST
218+
/// open block, as bundler's `Dsl#with_source` does. Nesting matters: the
219+
/// hosted redirect wraps a gem the user declared in their own
220+
/// `source "…" do` block in a Socket block INSIDE it, so on a merged
221+
/// (bundler <= 2.1) lock the gem is attributable only through the inner
222+
/// block (#1997). Anything else (`source` with a block argument, `#`
223+
/// comments, and Ruby's column-0 `=begin` … `=end` block comments) is
224+
/// ignored — this only ever ADDS a cross-check to lock evidence, never
225+
/// creates a ref on its own.
221226
fn parse_source_blocks(gemfile: &str) -> Vec<(String, String)> {
222227
parse_source_block_decls(gemfile)
223228
.into_iter()
@@ -236,7 +241,8 @@ struct BlockDecl {
236241
/// [`parse_source_blocks`] keeping each declaration's argument tail.
237242
fn parse_source_block_decls(gemfile: &str) -> Vec<BlockDecl> {
238243
let mut out = Vec::new();
239-
let mut open: Option<String> = None;
244+
// The open `source … do` blocks, innermost last.
245+
let mut open: Vec<String> = Vec::new();
240246
let mut block_comment = false;
241247
for raw in gemfile.lines() {
242248
// `=begin` / `=end` must start at column 0 and be followed by
@@ -257,51 +263,47 @@ fn parse_source_block_decls(gemfile: &str) -> Vec<BlockDecl> {
257263
if line.starts_with('#') {
258264
continue;
259265
}
260-
match &open {
261-
None => {
262-
let Some(rest) = line.strip_prefix("source") else {
263-
continue;
264-
};
265-
let rest = rest.trim_start();
266-
let (rest, paren) = match rest.strip_prefix('(') {
267-
Some(r) => (r.trim_start(), true),
268-
None => (rest, false),
269-
};
270-
let Some((_, url, tail)) = quoted_literal(rest) else {
271-
continue;
272-
};
273-
let tail = tail.trim_start();
274-
let tail = if paren {
275-
match tail.strip_prefix(')') {
276-
Some(t) => t.trim_start(),
277-
None => continue,
278-
}
279-
} else {
280-
tail
281-
};
282-
let tail = tail.split('#').next().unwrap_or_default().trim_end();
283-
if tail == "do" {
284-
open = Some(url.to_string());
285-
}
286-
}
287-
Some(url) => {
288-
if line == "end" || line.starts_with("end ") || line.starts_with("end#") {
289-
open = None;
290-
continue;
291-
}
292-
if let Some(decl) = gem_declaration_any(line) {
293-
out.push(BlockDecl {
294-
url: url.clone(),
295-
name: decl.name.to_string(),
296-
tail: decl.rest.to_string(),
297-
});
298-
}
299-
}
266+
if let Some(url) = source_block_opener(line) {
267+
open.push(url.to_string());
268+
continue;
269+
}
270+
let Some(url) = open.last() else {
271+
continue;
272+
};
273+
if line == "end" || line.starts_with("end ") || line.starts_with("end#") {
274+
open.pop();
275+
continue;
276+
}
277+
if let Some(decl) = gem_declaration_any(line) {
278+
out.push(BlockDecl {
279+
url: url.clone(),
280+
name: decl.name.to_string(),
281+
tail: decl.rest.to_string(),
282+
});
300283
}
301284
}
302285
out
303286
}
304287

288+
/// The URL of a trimmed Gemfile line that opens a `source "<url>" do` /
289+
/// `source('<url>') do` block (a trailing `#` comment allowed), else `None`.
290+
fn source_block_opener(line: &str) -> Option<&str> {
291+
let rest = line.strip_prefix("source")?.trim_start();
292+
let (rest, paren) = match rest.strip_prefix('(') {
293+
Some(r) => (r.trim_start(), true),
294+
None => (rest, false),
295+
};
296+
let (_, url, tail) = quoted_literal(rest)?;
297+
let tail = tail.trim_start();
298+
let tail = if paren {
299+
tail.strip_prefix(')')?.trim_start()
300+
} else {
301+
tail
302+
};
303+
let tail = tail.split('#').next().unwrap_or_default().trim_end();
304+
(tail == "do").then_some(url)
305+
}
306+
305307
/// Whether the bundler manifest text `gemfile` still routes a gem to patch
306308
/// `uuid`'s registry: a `source "<…/patch-registry/gem/<token>/<uuid>>" do`
307309
/// block ([`is_patch_registry_index`], host-agnostic) declaring `gem_name`
@@ -1267,6 +1269,45 @@ mod tests {
12671269
assert_eq!(run(&p).await.refs.len(), 1);
12681270
}
12691271

1272+
/// #1997: a gem the user declared in their own `source "…" do` block is
1273+
/// redirected by a Socket block NESTED inside it (CRLF or not); bundler
1274+
/// resolves it from the innermost source only, so on a merged lock the
1275+
/// inner block attributes it. Nested the other way round (the Socket
1276+
/// block outside, another source innermost), it is not attributed.
1277+
#[tokio::test]
1278+
async fn merged_section_nested_source_block_is_attributed_innermost() {
1279+
let socket = index(UUID_A);
1280+
let nested = format!(
1281+
"source \"https://rubygems.org\"\n\nsource \"https://rubygems.org\" do\n\
1282+
source \"{socket}\" do\n gem \"vuln-gem\", \"1.0.0\"\nend\nend\n"
1283+
);
1284+
for gemfile in [nested.clone(), nested.replace('\n', "\r\n")] {
1285+
let p = Project::new();
1286+
p.write("Gemfile.lock", merged_lock(&socket, "!"));
1287+
p.write("Gemfile", &gemfile);
1288+
let out = run(&p).await;
1289+
assert_refs(
1290+
&out,
1291+
&[("pkg:gem/[email protected]", UUID_A, WiringMode::Hosted)],
1292+
);
1293+
assert!(
1294+
out.diagnostics.is_empty(),
1295+
"{gemfile:?}: {:?}",
1296+
out.diagnostics
1297+
);
1298+
}
1299+
let inverted = format!(
1300+
"source \"{socket}\" do\n source \"https://rubygems.org\" do\n \
1301+
gem \"vuln-gem\", \"1.0.0\"\n end\nend\n"
1302+
);
1303+
let p = Project::new();
1304+
p.write("Gemfile.lock", merged_lock(&socket, "!"));
1305+
p.write("Gemfile", &inverted);
1306+
let out = run(&p).await;
1307+
assert!(out.refs.is_empty(), "{:#?}", out.refs);
1308+
assert_eq!(diag_codes(&out), vec![DIAG_REF_UNATTRIBUTABLE]);
1309+
}
1310+
12701311
/// Each half of the source pin is required, and the block must name
12711312
/// THIS remote.
12721313
#[tokio::test]
@@ -1586,7 +1627,16 @@ mod tests {
15861627
source \"https://d.example/\" do\n gem 'ghost'\nend\n\
15871628
=end\n\
15881629
=beginning is not a marker\n\
1589-
gem 'after'\n",
1630+
gem 'after'\n\
1631+
source 'https://outer.example/' do\n\
1632+
\x20 gem 'outer-1'\n\
1633+
\x20 source 'https://inner.example/' do # nested\n\
1634+
\x20 gem 'inner'\n\
1635+
\x20 end\n\
1636+
\x20 gem 'outer-2'\n\
1637+
end\n\
1638+
end\n\
1639+
gem 'top'\n",
15901640
);
15911641
let got: Vec<(&str, &str)> = got.iter().map(|(u, g)| (u.as_str(), g.as_str())).collect();
15921642
assert_eq!(
@@ -1595,6 +1645,9 @@ mod tests {
15951645
("https://a.example/", "one"),
15961646
("https://a.example/", "two"),
15971647
("https://b.example/", "three"),
1648+
("https://outer.example/", "outer-1"),
1649+
("https://inner.example/", "inner"),
1650+
("https://outer.example/", "outer-2"),
15981651
]
15991652
);
16001653
}

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.