1717//!
1818//! Reads are FIFO-safe and capped ([`MAX_FILES`], [`MAX_FILE_BYTES`]);
1919//! `.scala-build` and `.bloop` must be real directories (a symlink there is
20- //! no evidence), and symlinked project files are skipped.
20+ //! no evidence), and symlinked project files are skipped. A project file
21+ //! that is over the cap, unreadable or not a Bloop project fails closed (no
22+ //! evidence at all), as the sbt reader does: it could be the record naming
23+ //! a conflicting version.
2124
2225use std:: collections:: BTreeSet ;
2326use std:: path:: { Path , PathBuf } ;
@@ -33,7 +36,7 @@ use crate::utils::fs::read_regular_to_bytes_sync;
3336pub const BLOOP_DIR : & str = ".scala-build/.bloop" ;
3437/// Most project files read; over it there is no evidence at all.
3538pub const MAX_FILES : usize = 256 ;
36- /// Largest project file read; a bigger one is skipped .
39+ /// Largest project file read; a bigger one means no evidence at all .
3740pub const MAX_FILE_BYTES : u64 = 8 << 20 ;
3841/// Source extensions scala-cli compiles from a directory input.
3942const SOURCE_EXTENSIONS : & [ & str ] = & [ "scala" , "sc" , "java" ] ;
@@ -199,20 +202,34 @@ pub fn discover(root: &Path) -> Option<ScalaEvidence> {
199202 let Ok ( meta) = std:: fs:: symlink_metadata ( & path) else {
200203 continue ;
201204 } ;
202- if !meta. is_file ( ) || meta. len ( ) > MAX_FILE_BYTES {
203- debug_log ( & format ! ( "scala-cli evidence: skipping {}" , path. display( ) ) ) ;
205+ if !meta. is_file ( ) {
204206 continue ;
205207 }
208+ // A project file that cannot be read whole (over the cap, a read
209+ // error, a truncated or foreign record) could be the one naming a
210+ // conflicting version: no evidence at all, as for sbt
211+ // (`formats::sbt::evidence::resolution`), never a partial one.
212+ if meta. len ( ) > MAX_FILE_BYTES {
213+ debug_log ( & format ! (
214+ "scala-cli evidence: {} is over the {MAX_FILE_BYTES}-byte cap" ,
215+ path. display( )
216+ ) ) ;
217+ return None ;
218+ }
206219 let ( Ok ( bytes) , Ok ( written) ) = ( read_regular_to_bytes_sync ( & path) , meta. modified ( ) ) else {
207- continue ;
220+ debug_log ( & format ! (
221+ "scala-cli evidence: cannot read {}" ,
222+ path. display( )
223+ ) ) ;
224+ return None ;
208225 } ;
209226 let mtime = compiled_at ( & dir. join ( name. trim_end_matches ( ".json" ) ) , written) ;
210227 let Some ( project) = parse_bloop ( & bytes) else {
211228 debug_log ( & format ! (
212229 "scala-cli evidence: {} is not a Bloop project" ,
213230 path. display( )
214231 ) ) ;
215- continue ;
232+ return None ;
216233 } ;
217234 if !project. workspace_dir . as_deref ( ) . is_some_and ( same_workspace) {
218235 continue ;
@@ -717,7 +734,18 @@ mod tests {
717734 & dir. join ( "big.json" ) ,
718735 & " " . repeat ( MAX_FILE_BYTES as usize + 1 ) ,
719736 ) ;
720- assert_eq ! ( discover( & root) , None , "an oversized file is skipped" ) ;
737+ assert_eq ! ( discover( & root) , None , "an oversized file is no evidence" ) ;
738+ write (
739+ & dir. join ( "p.json" ) ,
740+ & bloop_json ( "p" , & root, & [ ] , & [ ( "g" , "a" , "1" , & [ ] ) ] ) ,
741+ ) ;
742+ assert_eq ! (
743+ discover( & root) ,
744+ None ,
745+ "an oversized project file fails closed beside a readable one"
746+ ) ;
747+ std:: fs:: remove_file ( dir. join ( "big.json" ) ) . unwrap ( ) ;
748+ assert ! ( discover( & root) . is_some( ) ) ;
721749 for n in 0 ..=MAX_FILES {
722750 write ( & dir. join ( format ! ( "p{n}.txt" ) ) , "" ) ;
723751 }
@@ -732,6 +760,81 @@ mod tests {
732760 ) ;
733761 }
734762
763+ /// #1270: a truncated, oversized or foreign `-test` twin (or newest
764+ /// project) used to be skipped, so the gate judged the main project's
765+ /// resolution alone and passed a `test.dep` at another version. Every
766+ /// unreadable record now fails closed, as the sbt reader does.
767+ #[ test]
768+ fn an_unreadable_project_file_hides_no_conflict ( ) {
769+ use crate :: vendor:: jvm:: coursier_gate:: gate;
770+ use crate :: vendor:: jvm:: sbt_gate:: GateStop ;
771+ let code = |e : Option < & ScalaEvidence > , root : & Path | match gate ( root, e, "g" , "a" , "1.0.0" ) {
772+ Ok ( _) => "ok" ,
773+ Err ( GateStop :: Skip ( w) ) => w. code ,
774+ Err ( GateStop :: Refuse ( r) ) => r. code ,
775+ Err ( GateStop :: Silent ) => "silent" ,
776+ } ;
777+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
778+ let root = tmp. path ( ) . canonicalize ( ) . unwrap ( ) ;
779+ let main = root. join ( "main.scala" ) ;
780+ let test_src = root. join ( "main.test.scala" ) ;
781+ write ( & main, "//> using dep g:a:1.0.0\n " ) ;
782+ write ( & test_src, "//> using test.dep g:a:2.0.0\n " ) ;
783+ let t0 = SystemTime :: now ( ) - Duration :: from_secs ( 600 ) ;
784+ set_mtime ( & main, t0) ;
785+ set_mtime ( & test_src, t0) ;
786+ let dir = root. join ( BLOOP_DIR ) ;
787+ let p = dir. join ( "sc_p.json" ) ;
788+ let twin = dir. join ( "sc_p-test.json" ) ;
789+ let twin_body = bloop_json (
790+ "sc_p-test" ,
791+ & root,
792+ & [ & main, & test_src] ,
793+ & [ ( "g" , "a" , "2.0.0" , & [ ( "" , "/c/a-2.jar" ) ] ) ] ,
794+ ) ;
795+ write (
796+ & p,
797+ & bloop_json (
798+ "sc_p" ,
799+ & root,
800+ & [ & main] ,
801+ & [ ( "g" , "a" , "1.0.0" , & [ ( "" , "/c/a-1.jar" ) ] ) ] ,
802+ ) ,
803+ ) ;
804+ let reset = |body : & str | {
805+ write ( & twin, body) ;
806+ set_mtime ( & p, t0 + Duration :: from_secs ( 100 ) ) ;
807+ set_mtime ( & twin, t0 + Duration :: from_secs ( 90 ) ) ;
808+ } ;
809+
810+ reset ( & twin_body) ;
811+ let e = discover ( & root) . unwrap ( ) ;
812+ assert_eq ! ( e. files. len( ) , 2 ) ;
813+ assert_eq ! ( e. resolution. versions( "g" , "a" ) , [ "1.0.0" , "2.0.0" ] ) ;
814+ assert_eq ! ( code( Some ( & e) , & root) , "vendor_scala_cli_version_conflict" ) ;
815+
816+ let oversized = format ! ( "{twin_body}{}" , " " . repeat( MAX_FILE_BYTES as usize ) ) ;
817+ for ( why, body) in [
818+ ( "truncated" , & twin_body[ ..twin_body. len ( ) / 2 ] ) ,
819+ ( "not a Bloop project" , "{\" version\" :\" 1.4.0\" }" ) ,
820+ ( "oversized" , oversized. as_str ( ) ) ,
821+ ] {
822+ reset ( body) ;
823+ let e = discover ( & root) ;
824+ assert_eq ! ( e, None , "a {why} -test twin is no evidence" ) ;
825+ assert_eq ! (
826+ code( e. as_ref( ) , & root) ,
827+ "vendor_scala_cli_resolution_missing" ,
828+ "a {why} -test twin never lets the gate pass"
829+ ) ;
830+ }
831+ // The newest main project unreadable: no older project stands in.
832+ reset ( & twin_body) ;
833+ write ( & p, "{" ) ;
834+ set_mtime ( & p, t0 + Duration :: from_secs ( 100 ) ) ;
835+ assert_eq ! ( discover( & root) , None ) ;
836+ }
837+
735838 #[ cfg( unix) ]
736839 #[ test]
737840 fn symlinks_and_fifos_are_not_evidence ( ) {
0 commit comments