@@ -1161,30 +1161,21 @@ async fn npm_get_ghsa_hosted_narrows_and_installs() {
11611161 manifestless_tail ( & fx, & fresh, installed, & [ ] ) ;
11621162}
11631163
1164- /// Regression (#1756): on Linux and macOS npm's `bin-links` rewrites a
1165- /// `bin` file's CRLF shebang to LF at install. Agent-mode `apply` patches
1166- /// that install (it was refused as drift), keeps the bin runnable (a
1167- /// written-back `#!/usr/bin/env node\r` makes `env` exit 127), re-applies
1168- /// as a no-op, `vex` attests it, and `rollback` restores npm's form of the
1169- /// original. Offline: the package is a local tarball.
1170- #[ cfg( unix) ]
1171- #[ tokio:: test( flavor = "multi_thread" ) ]
1172- #[ ignore = "wall-bound real-npm install; runs on the e2e CI matrix leg" ]
1173- async fn npm_agent_patches_crlf_shebang_bin_npm_rewrote ( ) {
1174- const SUITE : & str = "npm_agent_patches_crlf_shebang_bin_npm_rewrote" ;
1175- if npm_e2e_common:: npm_version ( ) . is_none ( ) {
1176- npm_e2e_common:: skip ( SUITE , "npm not on PATH" ) ;
1177- return ;
1178- }
1179- let before: & [ u8 ] = b"#!/usr/bin/env node\r \n console.log(\" hi\" );\r \n " ;
1180- let after: & [ u8 ] = b"#!/usr/bin/env node\r \n console.log(\" hi\" );\r \n // SOCKET-PATCHED\r \n " ;
1181- // What npm leaves on disk for each: the first line's `\r` dropped.
1182- let installed_before: & [ u8 ] = b"#!/usr/bin/env node\n console.log(\" hi\" );\r \n " ;
1183- let installed_after: & [ u8 ] =
1184- b"#!/usr/bin/env node\n console.log(\" hi\" );\r \n // SOCKET-PATCHED\r \n " ;
1164+ /// The tarball bytes of a bin published from Windows (CRLF shebang), its
1165+ /// patched twin, and what npm's `fixBin` leaves on disk for each: the
1166+ /// first line's `\r` dropped (#1756).
1167+ const CRLF_BIN_BEFORE : & [ u8 ] = b"#!/usr/bin/env node\r \n console.log(\" hi\" );\r \n " ;
1168+ const CRLF_BIN_AFTER : & [ u8 ] =
1169+ b"#!/usr/bin/env node\r \n console.log(\" hi\" );\r \n // SOCKET-PATCHED\r \n " ;
1170+ const NPM_BIN_BEFORE : & [ u8 ] = b"#!/usr/bin/env node\n console.log(\" hi\" );\r \n " ;
1171+ const NPM_BIN_AFTER : & [ u8 ] = b"#!/usr/bin/env node\n console.log(\" hi\" );\r \n // SOCKET-PATCHED\r \n " ;
11851172
1186- let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
1187- let pkg = tmp. path ( ) . join ( "pkg" ) ;
1173+ /// A project under `tmp` with the `crlfbin` tarball (a CRLF-shebang
1174+ /// `bin`) npm-installed, and an agent manifest patching that bin with
1175+ /// its blobs. Returns the project dir.
1176+ fn crlf_bin_project ( tmp : & Path ) -> PathBuf {
1177+ let ( before, after) = ( CRLF_BIN_BEFORE , CRLF_BIN_AFTER ) ;
1178+ let pkg = tmp. join ( "pkg" ) ;
11881179 std:: fs:: create_dir_all ( & pkg) . unwrap ( ) ;
11891180 std:: fs:: write (
11901181 pkg. join ( "package.json" ) ,
@@ -1193,18 +1184,18 @@ async fn npm_agent_patches_crlf_shebang_bin_npm_rewrote() {
11931184 . unwrap ( ) ;
11941185 std:: fs:: write ( pkg. join ( "cli.js" ) , before) . unwrap ( ) ;
11951186 std:: fs:: write (
1196- tmp. path ( ) . join ( "crlfbin-1.0.0.tgz" ) ,
1187+ tmp. join ( "crlfbin-1.0.0.tgz" ) ,
11971188 npm_e2e_common:: npm_pack_like ( & pkg) ,
11981189 )
11991190 . unwrap ( ) ;
1200- let proj = tmp. path ( ) . join ( "proj" ) ;
1191+ let proj = tmp. join ( "proj" ) ;
12011192 std:: fs:: create_dir_all ( & proj) . unwrap ( ) ;
12021193 std:: fs:: write (
12031194 proj. join ( "package.json" ) ,
12041195 r#"{"name":"p","version":"1.0.0","dependencies":{"crlfbin":"file:../crlfbin-1.0.0.tgz"}}"# ,
12051196 )
12061197 . unwrap ( ) ;
1207- let cache = tmp. path ( ) . join ( "cache" ) ;
1198+ let cache = tmp. join ( "cache" ) ;
12081199 let out = npm_e2e_common:: npm (
12091200 & proj,
12101201 & [
@@ -1221,11 +1212,7 @@ async fn npm_agent_patches_crlf_shebang_bin_npm_rewrote() {
12211212 npm_e2e_common:: output_text( & out)
12221213 ) ;
12231214 let cli = proj. join ( "node_modules/crlfbin/cli.js" ) ;
1224- assert_eq ! (
1225- std:: fs:: read( & cli) . unwrap( ) ,
1226- installed_before,
1227- "npm's fixBin"
1228- ) ;
1215+ assert_eq ! ( std:: fs:: read( & cli) . unwrap( ) , NPM_BIN_BEFORE , "npm's fixBin" ) ;
12291216
12301217 let ( b, a) = ( git_sha256 ( before) , git_sha256 ( after) ) ;
12311218 let blobs = proj. join ( ".socket/blobs" ) ;
@@ -1243,12 +1230,42 @@ async fn npm_agent_patches_crlf_shebang_bin_npm_rewrote() {
12431230 )
12441231 . unwrap ( ) ;
12451232
1246- let run_bin = || {
1247- std:: process:: Command :: new ( proj. join ( "node_modules/.bin/crlfbin" ) )
1248- . current_dir ( & proj)
1249- . output ( )
1250- . unwrap ( )
1251- } ;
1233+ proj
1234+ }
1235+
1236+ /// Runs the project's `crlfbin` bin, asserting it prints `hi`.
1237+ fn assert_crlf_bin_runs ( proj : & Path , what : & str ) {
1238+ let ran = std:: process:: Command :: new ( proj. join ( "node_modules/.bin/crlfbin" ) )
1239+ . current_dir ( proj)
1240+ . output ( )
1241+ . unwrap ( ) ;
1242+ assert ! (
1243+ ran. status. success( ) ,
1244+ "{what}: {}" ,
1245+ npm_e2e_common:: output_text( & ran)
1246+ ) ;
1247+ assert_eq ! ( String :: from_utf8_lossy( & ran. stdout) . trim( ) , "hi" , "{what}" ) ;
1248+ }
1249+
1250+ /// Regression (#1756): on Linux and macOS npm's `bin-links` rewrites a
1251+ /// `bin` file's CRLF shebang to LF at install. Agent-mode `apply` patches
1252+ /// that install (it was refused as drift), keeps the bin runnable (a
1253+ /// written-back `#!/usr/bin/env node\r` makes `env` exit 127), re-applies
1254+ /// as a no-op, `vex` attests it, and `rollback` restores npm's form of the
1255+ /// original. Offline: the package is a local tarball.
1256+ #[ cfg( unix) ]
1257+ #[ tokio:: test( flavor = "multi_thread" ) ]
1258+ #[ ignore = "wall-bound real-npm install; runs on the e2e CI matrix leg" ]
1259+ async fn npm_agent_patches_crlf_shebang_bin_npm_rewrote ( ) {
1260+ const SUITE : & str = "npm_agent_patches_crlf_shebang_bin_npm_rewrote" ;
1261+ if npm_e2e_common:: npm_version ( ) . is_none ( ) {
1262+ npm_e2e_common:: skip ( SUITE , "npm not on PATH" ) ;
1263+ return ;
1264+ }
1265+ let ( installed_before, installed_after) = ( NPM_BIN_BEFORE , NPM_BIN_AFTER ) ;
1266+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
1267+ let proj = crlf_bin_project ( tmp. path ( ) ) ;
1268+ let cli = proj. join ( "node_modules/crlfbin/cli.js" ) ;
12521269
12531270 let ( code, stdout, stderr) = run_socket ( & proj, & [ "apply" , "--offline" ] ) ;
12541271 assert_eq ! ( code, 0 , "apply\n stdout:\n {stdout}\n stderr:\n {stderr}" ) ;
@@ -1257,13 +1274,7 @@ async fn npm_agent_patches_crlf_shebang_bin_npm_rewrote() {
12571274 "no drift warning: {stderr}"
12581275 ) ;
12591276 assert_eq ! ( std:: fs:: read( & cli) . unwrap( ) , installed_after) ;
1260- let ran = run_bin ( ) ;
1261- assert ! (
1262- ran. status. success( ) ,
1263- "{}" ,
1264- npm_e2e_common:: output_text( & ran)
1265- ) ;
1266- assert_eq ! ( String :: from_utf8_lossy( & ran. stdout) . trim( ) , "hi" ) ;
1277+ assert_crlf_bin_runs ( & proj, "after apply" ) ;
12671278
12681279 let ( code, stdout, stderr) = run_socket ( & proj, & [ "apply" , "--offline" ] ) ;
12691280 assert_eq ! ( code, 0 , "re-apply\n stdout:\n {stdout}\n stderr:\n {stderr}" ) ;
@@ -1291,10 +1302,46 @@ async fn npm_agent_patches_crlf_shebang_bin_npm_rewrote() {
12911302 let ( code, stdout, stderr) = run_socket ( & proj, & [ "rollback" , "--offline" ] ) ;
12921303 assert_eq ! ( code, 0 , "rollback\n stdout:\n {stdout}\n stderr:\n {stderr}" ) ;
12931304 assert_eq ! ( std:: fs:: read( & cli) . unwrap( ) , installed_before) ;
1294- let ran = run_bin ( ) ;
1305+ assert_crlf_bin_runs ( & proj, "after rollback" ) ;
1306+ }
1307+
1308+ /// Regression (#1791): v5.0.1 wrote the raw CRLF patched bytes over the
1309+ /// bin npm had fixed, so it exits 127. `apply --check` reports that drift,
1310+ /// `apply` rewrites the bin in npm's form, and from the same broken state
1311+ /// `rollback` restores npm's runnable form of the original.
1312+ #[ cfg( unix) ]
1313+ #[ tokio:: test( flavor = "multi_thread" ) ]
1314+ #[ ignore = "wall-bound real-npm install; runs on the e2e CI matrix leg" ]
1315+ async fn npm_agent_recovers_crlf_bin_left_by_raw_write ( ) {
1316+ const SUITE : & str = "npm_agent_recovers_crlf_bin_left_by_raw_write" ;
1317+ if npm_e2e_common:: npm_version ( ) . is_none ( ) {
1318+ npm_e2e_common:: skip ( SUITE , "npm not on PATH" ) ;
1319+ return ;
1320+ }
1321+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
1322+ let proj = crlf_bin_project ( tmp. path ( ) ) ;
1323+ let cli = proj. join ( "node_modules/crlfbin/cli.js" ) ;
1324+ // What v5.0.1's apply left: the recorded bytes, CRLF shebang and all.
1325+ std:: fs:: write ( & cli, CRLF_BIN_AFTER ) . unwrap ( ) ;
1326+
1327+ let ( code, stdout, stderr) = run_socket ( & proj, & [ "apply" , "--check" , "--offline" ] ) ;
1328+ assert_eq ! ( code, 1 , "check\n stdout:\n {stdout}\n stderr:\n {stderr}" ) ;
12951329 assert ! (
1296- ran. status. success( ) ,
1297- "{}" ,
1298- npm_e2e_common:: output_text( & ran)
1330+ format!( "{stdout}{stderr}" ) . contains( "pkg:npm/crlfbin@1.0.0" ) ,
1331+ "names the drifted patch\n stdout:\n {stdout}\n stderr:\n {stderr}"
12991332 ) ;
1333+
1334+ let ( code, stdout, stderr) = run_socket ( & proj, & [ "apply" , "--offline" ] ) ;
1335+ assert_eq ! ( code, 0 , "apply\n stdout:\n {stdout}\n stderr:\n {stderr}" ) ;
1336+ assert_eq ! ( std:: fs:: read( & cli) . unwrap( ) , NPM_BIN_AFTER ) ;
1337+ assert_crlf_bin_runs ( & proj, "after apply" ) ;
1338+
1339+ let ( code, stdout, stderr) = run_socket ( & proj, & [ "apply" , "--check" , "--offline" ] ) ;
1340+ assert_eq ! ( code, 0 , "re-check\n stdout:\n {stdout}\n stderr:\n {stderr}" ) ;
1341+
1342+ std:: fs:: write ( & cli, CRLF_BIN_AFTER ) . unwrap ( ) ;
1343+ let ( code, stdout, stderr) = run_socket ( & proj, & [ "rollback" , "--offline" ] ) ;
1344+ assert_eq ! ( code, 0 , "rollback\n stdout:\n {stdout}\n stderr:\n {stderr}" ) ;
1345+ assert_eq ! ( std:: fs:: read( & cli) . unwrap( ) , NPM_BIN_BEFORE ) ;
1346+ assert_crlf_bin_runs ( & proj, "after rollback" ) ;
13001347}
0 commit comments