44import java .io .File ;
55import java .io .IOException ;
66import java .util .Arrays ;
7+ import java .util .Collections ;
8+ import java .util .Comparator ;
79import java .util .Enumeration ;
810import java .util .HashMap ;
911import java .util .LinkedHashMap ;
12+ import java .util .LinkedList ;
13+ import java .util .List ;
1014import java .util .Map ;
1115import java .util .Objects ;
16+ import java .util .regex .Matcher ;
1217import java .util .regex .Pattern ;
1318import java .util .zip .ZipEntry ;
1419import java .util .zip .ZipFile ;
2934
3035import com .semmle .util .concurrent .LockDirectory ;
3136import com .semmle .util .concurrent .LockDirectory .LockingMode ;
37+ import com .semmle .util .data .Pair ;
3238import com .semmle .util .exception .CatastrophicError ;
3339import com .semmle .util .exception .NestedError ;
3440import com .semmle .util .exception .ResourceError ;
4349import com .semmle .util .trap .pathtransformers .PathTransformer ;
4450
4551public class OdasaOutput {
52+ // By default we use lockless TRAP writing, but this can be set
53+ // if we want to use the old TRAP locking for any reason.
54+ private final boolean use_trap_locking = Env .systemEnv ().getBoolean ("CODEQL_EXTRACTOR_JAVA_TRAP_LOCKING" , false );
4655
4756 // either these are set ...
4857 private final File trapFolder ;
@@ -260,22 +269,59 @@ private void deleteTrapFileAndDependencies(IrDeclaration sym, String signature)
260269 * Any unique suffix needed to distinguish `sym` from other declarations with the same name.
261270 * For functions for example, this means its parameter signature.
262271 */
263- private TrapFileManager getMembersWriterForDecl (File trap , IrDeclaration sym , String signature ) {
264- TrapClassVersion currVersion = TrapClassVersion .fromSymbol (sym , log );
265- String shortName = sym instanceof IrDeclarationWithName ? ((IrDeclarationWithName )sym ).getName ().asString () : "(name unknown)" ;
266- if (trap .exists ()) {
267- // Only re-write an existing trap file if we encountered a newer version of the same class.
268- TrapClassVersion trapVersion = readVersionInfo (trap );
269- if (!currVersion .isValid ()) {
270- log .warn ("Not rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap );
271- } else if (currVersion .newerThan (trapVersion )) {
272- log .trace ("Rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap );
273- deleteTrapFileAndDependencies (sym , signature );
272+ private TrapFileManager getMembersWriterForDecl (File trap , File trapFileBase , TrapClassVersion trapFileVersion , IrDeclaration sym , String signature ) {
273+ if (use_trap_locking ) {
274+ TrapClassVersion currVersion = TrapClassVersion .fromSymbol (sym , log );
275+ String shortName = sym instanceof IrDeclarationWithName ? ((IrDeclarationWithName )sym ).getName ().asString () : "(name unknown)" ;
276+ if (trap .exists ()) {
277+ // Only re-write an existing trap file if we encountered a newer version of the same class.
278+ TrapClassVersion trapVersion = readVersionInfo (trap );
279+ if (!currVersion .isValid ()) {
280+ log .warn ("Not rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap );
281+ } else if (currVersion .newerThan (trapVersion )) {
282+ log .trace ("Rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap );
283+ deleteTrapFileAndDependencies (sym , signature );
284+ } else {
285+ return null ;
286+ }
274287 } else {
275- return null ;
288+ log . trace ( "Writing trap file for: " + shortName + " " + currVersion + " " + trap ) ;
276289 }
277290 } else {
278- log .trace ("Writing trap file for: " + shortName + " " + currVersion + " " + trap );
291+ // If the TRAP file already exists then we
292+ // don't need to write it.
293+ if (trap .exists ()) {
294+ log .warn ("Not rewriting trap file for " + trap .toString () + " as it exists" );
295+ return null ;
296+ }
297+ // If the TRAP file was written in the past, and
298+ // then renamed to its trap-old name, then we
299+ // don't need to rewrite it only to rename it
300+ // again.
301+ File trapFileDir = trap .getParentFile ();
302+ File trapOld = new File (trapFileDir , trap .getName ().replace (".trap.gz" , ".trap-old.gz" ));
303+ if (trapOld .exists ()) {
304+ log .warn ("Not rewriting trap file for " + trap .toString () + " as the trap-old exists" );
305+ return null ;
306+ }
307+ // Otherwise, if any newer TRAP file has already
308+ // been written then we don't need to write
309+ // anything.
310+ if (trapFileBase != null && trapFileVersion != null && trapFileDir .exists ()) {
311+ String trapFileBaseName = trapFileBase .getName ();
312+
313+ for (File f : FileUtil .list (trapFileDir )) {
314+ String name = f .getName ();
315+ Matcher m = selectClassVersionComponents .matcher (name );
316+ if (m .matches () && m .group (1 ).equals (trapFileBaseName )) {
317+ TrapClassVersion v = new TrapClassVersion (Integer .valueOf (m .group (2 )), Integer .valueOf (m .group (3 )), Long .valueOf (m .group (4 )), m .group (5 ));
318+ if (v .newerThan (trapFileVersion )) {
319+ log .warn ("Not rewriting trap file for " + trap .toString () + " as " + f .toString () + " exists" );
320+ return null ;
321+ }
322+ }
323+ }
324+ }
279325 }
280326 return trapWriter (trap , sym , signature );
281327 }
@@ -328,19 +374,24 @@ public void close() {
328374 }
329375
330376 writeTrapDependencies (trapDependenciesForClass );
331- // Record major/minor version information for extracted class files.
332- // This is subsequently used to determine whether to re-extract (a newer version of) the same class.
333- File metadataFile = new File (trapFile .getAbsolutePath ().replace (".trap.gz" , ".metadata" ));
334- try {
335- Map <String , String > versionMap = new LinkedHashMap <>();
336- TrapClassVersion tcv = TrapClassVersion .fromSymbol (sym , log );
337- versionMap .put (MAJOR_VERSION , String .valueOf (tcv .getMajorVersion ()));
338- versionMap .put (MINOR_VERSION , String .valueOf (tcv .getMinorVersion ()));
339- versionMap .put (LAST_MODIFIED , String .valueOf (tcv .getLastModified ()));
340- versionMap .put (EXTRACTOR_NAME , tcv .getExtractorName ());
341- FileUtil .writePropertiesCSV (metadataFile , versionMap );
342- } catch (IOException e ) {
343- log .warn ("Could not save trap metadata file: " + metadataFile .getAbsolutePath (), e );
377+
378+ // If we are using TRAP locking then we
379+ // need to write a metadata file.
380+ if (use_trap_locking ) {
381+ // Record major/minor version information for extracted class files.
382+ // This is subsequently used to determine whether to re-extract (a newer version of) the same class.
383+ File metadataFile = new File (trapFile .getAbsolutePath ().replace (".trap.gz" , ".metadata" ));
384+ try {
385+ Map <String , String > versionMap = new LinkedHashMap <>();
386+ TrapClassVersion tcv = TrapClassVersion .fromSymbol (sym , log );
387+ versionMap .put (MAJOR_VERSION , String .valueOf (tcv .getMajorVersion ()));
388+ versionMap .put (MINOR_VERSION , String .valueOf (tcv .getMinorVersion ()));
389+ versionMap .put (LAST_MODIFIED , String .valueOf (tcv .getLastModified ()));
390+ versionMap .put (EXTRACTOR_NAME , tcv .getExtractorName ());
391+ FileUtil .writePropertiesCSV (metadataFile , versionMap );
392+ } catch (IOException e ) {
393+ log .warn ("Could not save trap metadata file: " + metadataFile .getAbsolutePath (), e );
394+ }
344395 }
345396 }
346397 private void writeTrapDependencies (TrapDependencies trapDependencies ) {
@@ -358,6 +409,8 @@ public void setHasError() {
358409 * Trap file locking.
359410 */
360411
412+ private final Pattern selectClassVersionComponents = Pattern .compile ("(.*)#(-?[0-9]+)\\ .(-?[0-9]+)-(-?[0-9]+)-(.*)\\ .trap\\ .gz" );
413+
361414 /**
362415 * <b>CAUTION</b>: to avoid the potential for deadlock between multiple concurrent extractor processes,
363416 * only one source file {@link TrapLocker} may be open at any time, and the lock must be obtained
@@ -414,6 +467,10 @@ public TrapLocker getTrapLockerForDecl(IrDeclaration sym, String signature) {
414467 public class TrapLocker implements AutoCloseable {
415468 private final IrDeclaration sym ;
416469 private final File trapFile ;
470+ // trapFileBase is used when doing lockless TRAP file writing.
471+ // It is trapFile without the #metadata.trap.gz suffix.
472+ private File trapFileBase = null ;
473+ private TrapClassVersion trapFileVersion = null ;
417474 private final String signature ;
418475 private TrapLocker (IrDeclaration decl , String signature ) {
419476 this .sym = decl ;
@@ -422,7 +479,20 @@ private TrapLocker(IrDeclaration decl, String signature) {
422479 log .error ("Null symbol passed for Kotlin TRAP locker" );
423480 trapFile = null ;
424481 } else {
425- trapFile = getTrapFileForDecl (sym , signature );
482+ File normalTrapFile = getTrapFileForDecl (sym , signature );
483+ if (use_trap_locking ) {
484+ trapFile = normalTrapFile ;
485+ } else {
486+ // We encode the metadata into the filename, so that the
487+ // TRAP filenames for different metadatas don't overlap.
488+ trapFileVersion = TrapClassVersion .fromSymbol (sym , log );
489+ String baseName = normalTrapFile .getName ().replace (".trap.gz" , "" );
490+ // If a class has lots of inner classes, then we get lots of files
491+ // in a single directory. This makes our directory listings later slow.
492+ // To avoid this, rather than using files named .../Foo*, we use .../Foo/Foo*.
493+ trapFileBase = new File (new File (normalTrapFile .getParentFile (), baseName ), baseName );
494+ trapFile = new File (trapFileBase .getPath () + '#' + trapFileVersion .toString () + ".trap.gz" );
495+ }
426496 }
427497 }
428498 private TrapLocker (File jarFile ) {
@@ -437,20 +507,83 @@ private TrapLocker(String moduleName) {
437507 }
438508 public TrapFileManager getTrapFileManager () {
439509 if (trapFile !=null ) {
440- lockTrapFile (trapFile );
441- return getMembersWriterForDecl (trapFile , sym , signature );
510+ if (use_trap_locking ) {
511+ lockTrapFile (trapFile );
512+ }
513+ return getMembersWriterForDecl (trapFile , trapFileBase , trapFileVersion , sym , signature );
442514 } else {
443515 return null ;
444516 }
445517 }
518+
446519 @ Override
447520 public void close () {
448521 if (trapFile !=null ) {
449522 try {
450- unlockTrapFile (trapFile );
523+ if (use_trap_locking ) {
524+ unlockTrapFile (trapFile );
525+ }
451526 } catch (NestedError e ) {
452527 log .warn ("Error unlocking trap file " + trapFile .getAbsolutePath (), e );
453528 }
529+
530+ // If we are writing TRAP file locklessly, then now that we
531+ // have finished writing our TRAP file, we want to rename
532+ // and TRAP file that matches our trapFileBase but doesn't
533+ // have the latest metadata.
534+ // Renaming it to trap-old means that it won't be imported,
535+ // but we can still use its presence to avoid future
536+ // invocations rewriting it, and it means that the information
537+ // is in the TRAP directory if we need it for debugging.
538+ if (!use_trap_locking && sym != null ) {
539+ File trapFileDir = trapFileBase .getParentFile ();
540+ String trapFileBaseName = trapFileBase .getName ();
541+
542+ List <Pair <File , TrapClassVersion >> pairs = new LinkedList <Pair <File , TrapClassVersion >>();
543+ for (File f : FileUtil .list (trapFileDir )) {
544+ String name = f .getName ();
545+ Matcher m = selectClassVersionComponents .matcher (name );
546+ if (m .matches ()) {
547+ if (m .group (1 ).equals (trapFileBaseName )) {
548+ TrapClassVersion v = new TrapClassVersion (Integer .valueOf (m .group (2 )), Integer .valueOf (m .group (3 )), Long .valueOf (m .group (4 )), m .group (5 ));
549+ pairs .add (new Pair <File , TrapClassVersion >(f , v ));
550+ } else {
551+ // Everything in this directory should be for the same TRAP file base
552+ log .error ("Unexpected sibling " + m .group (1 ) + " when extracting " + trapFileBaseName );
553+ }
554+ }
555+ }
556+ if (pairs .isEmpty ()) {
557+ log .error ("Wrote TRAP file, but no TRAP files exist for " + trapFile .getAbsolutePath ());
558+ } else {
559+ Comparator <Pair <File , TrapClassVersion >> comparator = new Comparator <Pair <File , TrapClassVersion >>() {
560+ @ Override
561+ public int compare (Pair <File , TrapClassVersion > p1 , Pair <File , TrapClassVersion > p2 ) {
562+ TrapClassVersion v1 = p1 .snd ();
563+ TrapClassVersion v2 = p2 .snd ();
564+ if (v1 .equals (v2 )) {
565+ return 0 ;
566+ } else if (v1 .newerThan (v2 )) {
567+ return 1 ;
568+ } else {
569+ return -1 ;
570+ }
571+ }
572+ };
573+ TrapClassVersion latestVersion = Collections .max (pairs , comparator ).snd ();
574+
575+ for (Pair <File , TrapClassVersion > p : pairs ) {
576+ if (!latestVersion .equals (p .snd ())) {
577+ File f = p .fst ();
578+ File fOld = new File (f .getParentFile (), f .getName ().replace (".trap.gz" , ".trap-old.gz" ));
579+ // We aren't interested in whether or not this succeeds;
580+ // it may fail because a concurrent extractor has already
581+ // renamed it.
582+ f .renameTo (fOld );
583+ }
584+ }
585+ }
586+ }
454587 }
455588 }
456589
@@ -505,6 +638,17 @@ private TrapClassVersion(int majorVersion, int minorVersion, long lastModified,
505638 this .lastModified = lastModified ;
506639 this .extractorName = extractorName ;
507640 }
641+
642+ @ Override
643+ public boolean equals (Object obj ) {
644+ if (obj instanceof TrapClassVersion ) {
645+ TrapClassVersion other = (TrapClassVersion )obj ;
646+ return majorVersion == other .majorVersion && minorVersion == other .minorVersion && lastModified == other .lastModified && extractorName .equals (other .extractorName );
647+ } else {
648+ return false ;
649+ }
650+ }
651+
508652 private boolean newerThan (TrapClassVersion tcv ) {
509653 // Classes being compiled from source have major version 0 but should take precedence
510654 // over any classes with the same qualified name loaded from the classpath
0 commit comments