@@ -424,6 +424,22 @@ enum Driver { |
424 | 424 | /// appending a source block would declare it twice. Same contract as |
425 | 425 | /// [`Driver::ScanVexDuplicateDeclaration`]. |
426 | 426 | ScanVexEvalGemfile, |
| 427 | + /// [`Driver::ScanVex`] on a Gemfile whose declaration continues on the |
| 428 | + /// next line (`gem "x",` ↵ `require: false`, #340): rewriting the first |
| 429 | + /// line would orphan the continuation after the source block. Same |
| 430 | + /// contract as [`Driver::ScanVexDuplicateDeclaration`]. |
| 431 | + ScanVexMultiLineDeclaration, |
| 432 | + /// [`Driver::ScanVex`] on a Gemfile whose declaration carries an `if` |
| 433 | + /// modifier (#340): rewriting it would drop the condition. Same contract |
| 434 | + /// as [`Driver::ScanVexDuplicateDeclaration`]. |
| 435 | + ScanVexConditionalDeclaration, |
| 436 | + /// A modifier adjacent to a top-level constant is still a modifier, |
| 437 | + /// not a hash label (`if::ENV`, #340). |
| 438 | + ScanVexScopedConstantModifier, |
| 439 | + /// A heredoc option continues beyond the declaration's physical line. |
| 440 | + ScanVexHeredocDeclaration, |
| 441 | + /// A double-quoted interpolation can itself contain a heredoc opener. |
| 442 | + ScanVexInterpolatedHeredocDeclaration, |
427 | 443 | /// [`Driver::ScanVexDualBoot`] with `BUNDLE_GEMFILE=Gemfile` exported to |
428 | 444 | /// socket-patch too (#507): bundler's local app config outranks the |
429 | 445 | /// environment, so bundler still loads `Gemfile.next` and the run must |
@@ -439,6 +455,13 @@ impl Driver { |
439 | 455 | Driver::ScanVexDualBoot => "scan --mode hosted (BUNDLE_GEMFILE=Gemfile.next)", |
440 | 456 | Driver::ScanVexDuplicateDeclaration => "scan --mode hosted (gem in two groups)", |
441 | 457 | Driver::ScanVexEvalGemfile => "scan --mode hosted (gem via eval_gemfile)", |
| 458 | + Driver::ScanVexMultiLineDeclaration => "scan --mode hosted (multi-line gem line)", |
| 459 | + Driver::ScanVexConditionalDeclaration => "scan --mode hosted (gem line with `if`)", |
| 460 | + Driver::ScanVexScopedConstantModifier => "scan --mode hosted (gem line with `if::ENV`)", |
| 461 | + Driver::ScanVexHeredocDeclaration => "scan --mode hosted (heredoc gem option)", |
| 462 | + Driver::ScanVexInterpolatedHeredocDeclaration => { |
| 463 | + "scan --mode hosted (interpolated heredoc gem option)" |
| 464 | + } |
442 | 465 | Driver::ScanVexDualBootEnvGemfile => { |
443 | 466 | "scan --mode hosted (config Gemfile.next, env BUNDLE_GEMFILE=Gemfile)" |
444 | 467 | } |
@@ -717,6 +740,26 @@ async fn redirect_scanned_project( |
717 | 740 | server.uri() |
718 | 741 | ) |
719 | 742 | } |
| 743 | + Driver::ScanVexMultiLineDeclaration => format!( |
| 744 | + "source \"{}/upstream\"\n\ngem \"{DEP}\",\n require: false\n", |
| 745 | + server.uri() |
| 746 | + ), |
| 747 | + Driver::ScanVexConditionalDeclaration => format!( |
| 748 | + "source \"{}/upstream\"\n\ngem \"{DEP}\" if ENV[\"WITH_VULN\"] != \"0\"\n", |
| 749 | + server.uri() |
| 750 | + ), |
| 751 | + Driver::ScanVexScopedConstantModifier => format!( |
| 752 | + "source \"{}/upstream\"\n\ngem \"{DEP}\", \"{DEP_VERSION}\" if::ENV[\"WITH_VULN\"] != \"0\"\n", |
| 753 | + server.uri() |
| 754 | + ), |
| 755 | + Driver::ScanVexHeredocDeclaration => format!( |
| 756 | + "source \"{}/upstream\"\n\ngem \"{DEP}\", require: <<~REQUIRE_PATH.chomp\n vuln_gem\nREQUIRE_PATH\n", |
| 757 | + server.uri() |
| 758 | + ), |
| 759 | + Driver::ScanVexInterpolatedHeredocDeclaration => format!( |
| 760 | + "source \"{}/upstream\"\n\ngem \"{DEP}\", require: \"#{{<<~REQUIRE_PATH}}\".chomp\n vuln_gem\nREQUIRE_PATH\n", |
| 761 | + server.uri() |
| 762 | + ), |
720 | 763 | _ => format!("source \"{}/upstream\"\n\ngem \"{DEP}\"\n", server.uri()), |
721 | 764 | }; |
722 | 765 | std::fs::write(proj.join(gemfile_name), gemfile_body).unwrap(); |
@@ -823,7 +866,12 @@ async fn redirect_scanned_project( |
823 | 866 | | Driver::ScanVexDualBoot |
824 | 867 | | Driver::ScanVexDualBootEnvGemfile |
825 | 868 | | Driver::ScanVexDuplicateDeclaration |
826 | | - | Driver::ScanVexEvalGemfile => vec![ |
| 869 | + | Driver::ScanVexEvalGemfile |
| 870 | + | Driver::ScanVexMultiLineDeclaration |
| 871 | + | Driver::ScanVexConditionalDeclaration |
| 872 | + | Driver::ScanVexScopedConstantModifier |
| 873 | + | Driver::ScanVexHeredocDeclaration |
| 874 | + | Driver::ScanVexInterpolatedHeredocDeclaration => vec![ |
827 | 875 | "scan", |
828 | 876 | "--mode", |
829 | 877 | "hosted", |
@@ -880,6 +928,13 @@ async fn redirect_scanned_project( |
880 | 928 | if let Some(warning) = match driver { |
881 | 929 | Driver::ScanVexDuplicateDeclaration => Some("redirect_gem_declared_more_than_once"), |
882 | 930 | Driver::ScanVexEvalGemfile => Some("redirect_gem_declaration_not_visible"), |
| 931 | + Driver::ScanVexMultiLineDeclaration |
| 932 | + | Driver::ScanVexConditionalDeclaration |
| 933 | + | Driver::ScanVexScopedConstantModifier |
| 934 | + | Driver::ScanVexHeredocDeclaration |
| 935 | + | Driver::ScanVexInterpolatedHeredocDeclaration => { |
| 936 | + Some("redirect_gem_unrecognized_declaration") |
| 937 | + } |
883 | 938 | _ => None, |
884 | 939 | } { |
885 | 940 | assert_unwirable_declaration_redirects_nothing( |
@@ -968,7 +1023,14 @@ async fn redirect_scanned_project( |
968 | 1023 | Driver::ScanVexDualBoot |
969 | 1024 | | Driver::ScanVexDualBootEnvGemfile |
970 | 1025 | | Driver::ScanVexDuplicateDeclaration |
971 | | - | Driver::ScanVexEvalGemfile => unreachable!("asserted and returned above"), |
| 1026 | + | Driver::ScanVexEvalGemfile |
| 1027 | + | Driver::ScanVexMultiLineDeclaration |
| 1028 | + | Driver::ScanVexConditionalDeclaration |
| 1029 | + | Driver::ScanVexScopedConstantModifier |
| 1030 | + | Driver::ScanVexHeredocDeclaration |
| 1031 | + | Driver::ScanVexInterpolatedHeredocDeclaration => { |
| 1032 | + unreachable!("asserted and returned above") |
| 1033 | + } |
972 | 1034 | Driver::GetUuid => { |
973 | 1035 | // get's hosted envelope (CLI_CONTRACT.md "get --mode and |
974 | 1036 | // installed narrowing"): `found` counts the resolved patch; |
@@ -1717,6 +1779,73 @@ async fn gem_hosted_eval_gemfile_direct_dep_is_refused_and_still_installs() { |
1717 | 1779 | assert!(fx.is_none(), "the eval_gemfile driver asserts in place"); |
1718 | 1780 | } |
1719 | 1781 |
|
| 1782 | +/// #340: a `gem` declaration that continues on the next line must not be |
| 1783 | +/// rewritten (the orphaned `require: false` made bundler refuse the Gemfile). |
| 1784 | +#[tokio::test(flavor = "multi_thread")] |
| 1785 | +#[ignore = "host capstone: shells out to a real ruby/gem/bundler (>= 1.17; CHECKSUMS arm >= 2.6); \ |
| 1786 | + the unpinned `test` job skips it, an e2e job with a pinned toolchain runs it via --ignored"] |
| 1787 | +async fn gem_hosted_multi_line_declaration_is_refused_and_still_installs() { |
| 1788 | + let fx = redirect_scanned_project( |
| 1789 | + "multi-line", |
| 1790 | + Spelling::Gemfile, |
| 1791 | + false, |
| 1792 | + true, |
| 1793 | + None, |
| 1794 | + Driver::ScanVexMultiLineDeclaration, |
| 1795 | + ) |
| 1796 | + .await; |
| 1797 | + assert!(fx.is_none(), "the multi-line driver asserts in place"); |
| 1798 | +} |
| 1799 | + |
| 1800 | +/// #340: a `gem` declaration with an `if` modifier must not be rewritten |
| 1801 | +/// (the rewrite dropped the condition and declared the gem unconditionally). |
| 1802 | +#[tokio::test(flavor = "multi_thread")] |
| 1803 | +#[ignore = "host capstone: shells out to a real ruby/gem/bundler (>= 1.17; CHECKSUMS arm >= 2.6); \ |
| 1804 | + the unpinned `test` job skips it, an e2e job with a pinned toolchain runs it via --ignored"] |
| 1805 | +async fn gem_hosted_conditional_declaration_is_refused_and_still_installs() { |
| 1806 | + let fx = redirect_scanned_project( |
| 1807 | + "conditional", |
| 1808 | + Spelling::Gemfile, |
| 1809 | + false, |
| 1810 | + true, |
| 1811 | + None, |
| 1812 | + Driver::ScanVexConditionalDeclaration, |
| 1813 | + ) |
| 1814 | + .await; |
| 1815 | + assert!(fx.is_none(), "the conditional driver asserts in place"); |
| 1816 | +} |
| 1817 | + |
| 1818 | +#[tokio::test(flavor = "multi_thread")] |
| 1819 | +#[ignore = "host capstone: shells out to a real ruby/gem/bundler (>= 1.17); \ |
| 1820 | + run with a pinned toolchain via --ignored"] |
| 1821 | +async fn gem_hosted_scoped_constant_modifier_is_refused_and_still_installs() { |
| 1822 | + let fx = redirect_scanned_project( |
| 1823 | + "scoped-constant-modifier", |
| 1824 | + Spelling::Gemfile, |
| 1825 | + false, |
| 1826 | + true, |
| 1827 | + None, |
| 1828 | + Driver::ScanVexScopedConstantModifier, |
| 1829 | + ) |
| 1830 | + .await; |
| 1831 | + assert!(fx.is_none(), "the scoped modifier driver asserts in place"); |
| 1832 | +} |
| 1833 | + |
| 1834 | +#[tokio::test(flavor = "multi_thread")] |
| 1835 | +#[ignore = "host capstone: shells out to a real ruby/gem/bundler (>= 1.17); \ |
| 1836 | + run with a pinned toolchain via --ignored"] |
| 1837 | +async fn gem_hosted_heredoc_declaration_is_refused_and_still_installs() { |
| 1838 | + for driver in [ |
| 1839 | + Driver::ScanVexHeredocDeclaration, |
| 1840 | + Driver::ScanVexInterpolatedHeredocDeclaration, |
| 1841 | + ] { |
| 1842 | + let fx = |
| 1843 | + redirect_scanned_project(driver.label(), Spelling::Gemfile, false, true, None, driver) |
| 1844 | + .await; |
| 1845 | + assert!(fx.is_none(), "the heredoc driver asserts in place"); |
| 1846 | + } |
| 1847 | +} |
| 1848 | + |
1720 | 1849 | /// #507: the same dual boot with `BUNDLE_GEMFILE=Gemfile` exported. Bundler |
1721 | 1850 | /// ranks the committed `.bundle/config` above the environment (it still |
1722 | 1851 | /// loads `Gemfile.next`), so socket-patch must not follow the env value and |
|