@@ -81,6 +81,10 @@ pub(crate) async fn form_for_write<'a>(
8181 expected_hash : & str ,
8282) -> Result < ( std:: borrow:: Cow < ' a , [ u8 ] > , String ) , std:: io:: Error > {
8383 let as_recorded = |content| Ok ( ( content, expected_hash. to_string ( ) ) ) ;
84+ // Content `fixBin` leaves alone needs no evidence read from disk.
85+ if fix_bin ( & content) . is_none ( ) {
86+ return as_recorded ( content) ;
87+ }
8488 let path = pkg_path. join ( normalized) ;
8589 let npm_installed = is_listed_bin ( package_key, pkg_path, normalized) . await || {
8690 let lf_shebang = read_prefix ( & path)
@@ -119,11 +123,17 @@ pub(crate) async fn is_listed_bin(package_key: &str, pkg_path: &Path, normalized
119123 lists_bin ( & manifest, normalized)
120124}
121125
122- /// Whether the file holding raw bytes `fixBin` would change (a CRLF
123- /// shebang npm never leaves in a bin) is a bin of the npm package
124- /// `package_key`: the state a release before #1756 left by writing the
125- /// recorded bytes over npm's form, which `env` can't run (#1791).
126- pub ( crate ) async fn needs_fix ( package_key : & str , pkg_path : & Path , normalized : & str ) -> bool {
126+ /// Whether the file at `normalized` holds exactly the recorded content
127+ /// `expected_hash`, in bytes `fixBin` would change (a CRLF shebang npm
128+ /// never leaves in a bin), and is a bin of the npm package `package_key`:
129+ /// the state a release before #1756 left by writing the recorded bytes
130+ /// over npm's form, which `env` can't run (#1791).
131+ pub ( crate ) async fn needs_fix (
132+ package_key : & str ,
133+ pkg_path : & Path ,
134+ normalized : & str ,
135+ expected_hash : & str ,
136+ ) -> bool {
127137 if !is_npm ( package_key) {
128138 return false ;
129139 }
@@ -139,36 +149,51 @@ pub(crate) async fn needs_fix(package_key: &str, pkg_path: &Path, normalized: &s
139149 }
140150 crate :: utils:: fs:: read_regular_to_bytes ( & path)
141151 . await
142- . is_ok_and ( |bytes| fix_bin ( & bytes) . is_some ( ) )
152+ . is_ok_and ( |bytes| {
153+ fix_bin ( & bytes) . is_some ( ) && compute_git_sha256_from_bytes ( & bytes) == expected_hash
154+ } )
143155}
144156
145157/// Whether the `package.json` bytes `manifest` list `normalized` as a
146- /// bin: a `bin` path (string or object values) or, with no `bin`, a file
147- /// under `directories.bin`, as npm's `normalize` reads them. Paths are
148- /// resolved as npm does (`./` and `..` folded, never above the package).
158+ /// bin, as npm's `normalize-package-bin` and `directories.bin` step read
159+ /// them: a `bin` path (a string, when the package has a `name`, an
160+ /// object's values, or an array's entries) or, when `bin` is missing or
161+ /// empty, a non-hidden file under `directories.bin`. Paths are resolved
162+ /// as npm does (`./` and `..` folded, never above the package).
149163fn lists_bin ( manifest : & [ u8 ] , normalized : & str ) -> bool {
150- let Ok ( json) = serde_json:: from_slice :: < serde_json:: Value > ( manifest) else {
164+ use serde_json:: Value ;
165+ let Ok ( json) = serde_json:: from_slice :: < Value > ( manifest) else {
151166 return false ;
152167 } ;
153168 let target = clean_path ( normalized) ;
154169 if target. is_empty ( ) {
155170 return false ;
156171 }
172+ let lists = |bins : & mut dyn Iterator < Item = & Value > | {
173+ bins. filter_map ( Value :: as_str)
174+ . any ( |bin| clean_path ( bin) == target)
175+ } ;
157176 match json. get ( "bin" ) {
158- Some ( serde_json:: Value :: String ( bin) ) => clean_path ( bin) == target,
159- Some ( serde_json:: Value :: Object ( bins) ) => bins
160- . values ( )
161- . filter_map ( serde_json:: Value :: as_str)
162- . any ( |bin| clean_path ( bin) == target) ,
163- Some ( _) => false ,
164- None => json
177+ Some ( Value :: String ( bin) ) if !bin. is_empty ( ) => {
178+ json. get ( "name" )
179+ . and_then ( Value :: as_str)
180+ . is_some_and ( |n| !n. is_empty ( ) )
181+ && clean_path ( bin) == target
182+ }
183+ Some ( Value :: Object ( bins) ) => lists ( & mut bins. values ( ) ) ,
184+ Some ( Value :: Array ( bins) ) => lists ( & mut bins. iter ( ) ) ,
185+ Some ( Value :: String ( _) | Value :: Null | Value :: Bool ( false ) ) | None => json
165186 . get ( "directories" )
166187 . and_then ( |d| d. get ( "bin" ) )
167188 . and_then ( serde_json:: Value :: as_str)
168189 . map ( clean_path)
169190 . is_some_and ( |dir| {
170- !dir. is_empty ( ) && target. len ( ) > dir. len ( ) && target. starts_with ( & dir[ ..] )
191+ !dir. is_empty ( )
192+ && target. len ( ) > dir. len ( )
193+ && target. starts_with ( & dir[ ..] )
194+ && !target[ dir. len ( ) ..] . iter ( ) . any ( |seg| seg. starts_with ( '.' ) )
171195 } ) ,
196+ Some ( _) => false ,
172197 }
173198}
174199
@@ -344,8 +369,8 @@ mod tests {
344369 #[ test]
345370 fn lists_bin_reads_every_bin_shape ( ) {
346371 let listed = |manifest : & str , file : & str | lists_bin ( manifest. as_bytes ( ) , file) ;
347- assert ! ( listed( r#"{"bin":"cli.js"}"# , "cli.js" ) ) ;
348- assert ! ( listed( r#"{"bin":"./bin/../cli.js"}"# , "cli.js" ) ) ;
372+ assert ! ( listed( r#"{"name":"x"," bin":"cli.js"}"# , "cli.js" ) ) ;
373+ assert ! ( listed( r#"{"name":"x"," bin":"./bin/../cli.js"}"# , "cli.js" ) ) ;
349374 assert ! ( listed(
350375 r#"{"bin":{"a":"x.js","b":"./bin/cli.js"}}"# ,
351376 "bin/cli.js"
@@ -360,6 +385,19 @@ mod tests {
360385 "bin/cli.js"
361386 ) ) ;
362387 assert ! ( !listed( r#"{"bin":{"a":"x.js"}}"# , "cli.js" ) ) ;
388+ assert ! ( listed( r#"{"bin":["x.js","./cli.js"]}"# , "cli.js" ) ) ;
389+ // npm drops a string `bin` of a package with no name.
390+ assert ! ( !listed( r#"{"bin":"cli.js"}"# , "cli.js" ) ) ;
391+ // A missing or empty `bin` falls back to `directories.bin`, whose
392+ // glob skips hidden files.
393+ for bin in [ "\" \" " , "null" ] {
394+ let manifest = format ! ( r#"{{"bin":{bin},"directories":{{"bin":"bin"}}}}"# ) ;
395+ assert ! ( listed( & manifest, "bin/cli.js" ) , "{manifest}" ) ;
396+ }
397+ assert ! ( !listed(
398+ r#"{"directories":{"bin":"bin"}}"# ,
399+ "bin/.hidden/cli.js"
400+ ) ) ;
363401 assert ! ( !listed( r#"{"name":"x"}"# , "cli.js" ) ) ;
364402 assert ! ( !listed( r#"{"directories":{"bin":"."}}"# , "cli.js" ) ) ;
365403 assert ! ( !listed( "not json" , "cli.js" ) ) ;
0 commit comments