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 ;
@@ -261,21 +270,39 @@ private void deleteTrapFileAndDependencies(IrDeclaration sym, String signature)
261270 * For functions for example, this means its parameter signature.
262271 */
263272 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 );
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 trapOld = new File (trap .getParentFile (), trap .getName ().replace (".trap.gz" , ".trap-old.gz" ));
302+ if (trapOld .exists ()) {
303+ log .warn ("Not rewriting trap file for " + trap .toString () + " as the trap-old exists" );
304+ return null ;
305+ }
279306 }
280307 return trapWriter (trap , sym , signature );
281308 }
@@ -328,19 +355,24 @@ public void close() {
328355 }
329356
330357 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 );
358+
359+ // If we are using TRAP locking then we
360+ // need to write a metadata file.
361+ if (use_trap_locking ) {
362+ // Record major/minor version information for extracted class files.
363+ // This is subsequently used to determine whether to re-extract (a newer version of) the same class.
364+ File metadataFile = new File (trapFile .getAbsolutePath ().replace (".trap.gz" , ".metadata" ));
365+ try {
366+ Map <String , String > versionMap = new LinkedHashMap <>();
367+ TrapClassVersion tcv = TrapClassVersion .fromSymbol (sym , log );
368+ versionMap .put (MAJOR_VERSION , String .valueOf (tcv .getMajorVersion ()));
369+ versionMap .put (MINOR_VERSION , String .valueOf (tcv .getMinorVersion ()));
370+ versionMap .put (LAST_MODIFIED , String .valueOf (tcv .getLastModified ()));
371+ versionMap .put (EXTRACTOR_NAME , tcv .getExtractorName ());
372+ FileUtil .writePropertiesCSV (metadataFile , versionMap );
373+ } catch (IOException e ) {
374+ log .warn ("Could not save trap metadata file: " + metadataFile .getAbsolutePath (), e );
375+ }
344376 }
345377 }
346378 private void writeTrapDependencies (TrapDependencies trapDependencies ) {
@@ -414,6 +446,10 @@ public TrapLocker getTrapLockerForDecl(IrDeclaration sym, String signature) {
414446 public class TrapLocker implements AutoCloseable {
415447 private final IrDeclaration sym ;
416448 private final File trapFile ;
449+ // trapFileBase is used when doing lockless TRAP file writing.
450+ // It is trapFile without the #metadata.trap.gz suffix.
451+ private File trapFileBase = null ;
452+ private TrapClassVersion trapFileVersion = null ;
417453 private final String signature ;
418454 private TrapLocker (IrDeclaration decl , String signature ) {
419455 this .sym = decl ;
@@ -422,7 +458,16 @@ private TrapLocker(IrDeclaration decl, String signature) {
422458 log .error ("Null symbol passed for Kotlin TRAP locker" );
423459 trapFile = null ;
424460 } else {
425- trapFile = getTrapFileForDecl (sym , signature );
461+ File normalTrapFile = getTrapFileForDecl (sym , signature );
462+ if (use_trap_locking ) {
463+ trapFile = normalTrapFile ;
464+ } else {
465+ // We encode the metadata into the filename, so that the
466+ // TRAP filenames for different metadatas don't overlap.
467+ trapFileVersion = TrapClassVersion .fromSymbol (sym , log );
468+ trapFileBase = new File (normalTrapFile .getParentFile (), normalTrapFile .getName ().replace (".trap.gz" , "" ));
469+ trapFile = new File (trapFileBase .getPath () + '#' + trapFileVersion .toString () + ".trap.gz" );
470+ }
426471 }
427472 }
428473 private TrapLocker (File jarFile ) {
@@ -437,20 +482,80 @@ private TrapLocker(String moduleName) {
437482 }
438483 public TrapFileManager getTrapFileManager () {
439484 if (trapFile !=null ) {
440- lockTrapFile (trapFile );
485+ if (use_trap_locking ) {
486+ lockTrapFile (trapFile );
487+ }
441488 return getMembersWriterForDecl (trapFile , sym , signature );
442489 } else {
443490 return null ;
444491 }
445492 }
493+
494+ private final Pattern selectClassVersionComponents = Pattern .compile ("(.*)#(-?[0-9]+)\\ .(-?[0-9]+)-(-?[0-9]+)-(.*)\\ .trap\\ .gz" );
495+
446496 @ Override
447497 public void close () {
448498 if (trapFile !=null ) {
449499 try {
450- unlockTrapFile (trapFile );
500+ if (use_trap_locking ) {
501+ unlockTrapFile (trapFile );
502+ }
451503 } catch (NestedError e ) {
452504 log .warn ("Error unlocking trap file " + trapFile .getAbsolutePath (), e );
453505 }
506+
507+ // If we are writing TRAP file locklessly, then now that we
508+ // have finished writing our TRAP file, we want to rename
509+ // and TRAP file that matches our trapFileBase but doesn't
510+ // have the latest metadata.
511+ // Renaming it to trap-old means that it won't be imported,
512+ // but we can still use its presence to avoid future
513+ // invocations rewriting it, and it means that the information
514+ // is in the TRAP directory if we need it for debugging.
515+ if (!use_trap_locking && sym != null ) {
516+ File trapFileDir = trapFileBase .getParentFile ();
517+ String trapFileBaseName = trapFileBase .getName ();
518+
519+ List <Pair <File , TrapClassVersion >> pairs = new LinkedList <Pair <File , TrapClassVersion >>();
520+ for (File f : FileUtil .list (trapFileDir )) {
521+ String name = f .getName ();
522+ Matcher m = selectClassVersionComponents .matcher (name );
523+ if (m .matches () && m .group (1 ).equals (trapFileBaseName )) {
524+ TrapClassVersion v = new TrapClassVersion (Integer .valueOf (m .group (2 )), Integer .valueOf (m .group (3 )), Long .valueOf (m .group (4 )), m .group (5 ));
525+ pairs .add (new Pair <File , TrapClassVersion >(f , v ));
526+ }
527+ }
528+ if (pairs .isEmpty ()) {
529+ log .error ("Wrote TRAP file, but no TRAP files exist for " + trapFile .getAbsolutePath ());
530+ } else {
531+ Comparator <Pair <File , TrapClassVersion >> comparator = new Comparator <Pair <File , TrapClassVersion >>() {
532+ @ Override
533+ public int compare (Pair <File , TrapClassVersion > p1 , Pair <File , TrapClassVersion > p2 ) {
534+ TrapClassVersion v1 = p1 .snd ();
535+ TrapClassVersion v2 = p2 .snd ();
536+ if (v1 .equals (v2 )) {
537+ return 0 ;
538+ } else if (v1 .newerThan (v2 )) {
539+ return 1 ;
540+ } else {
541+ return -1 ;
542+ }
543+ }
544+ };
545+ TrapClassVersion latestVersion = Collections .max (pairs , comparator ).snd ();
546+
547+ for (Pair <File , TrapClassVersion > p : pairs ) {
548+ if (!latestVersion .equals (p .snd ())) {
549+ File f = p .fst ();
550+ File fOld = new File (f .getParentFile (), f .getName ().replace (".trap.gz" , ".trap-old.gz" ));
551+ // We aren't interested in whether or not this succeeds;
552+ // it may fail because a concurrent extractor has already
553+ // renamed it.
554+ f .renameTo (fOld );
555+ }
556+ }
557+ }
558+ }
454559 }
455560 }
456561
@@ -505,6 +610,17 @@ private TrapClassVersion(int majorVersion, int minorVersion, long lastModified,
505610 this .lastModified = lastModified ;
506611 this .extractorName = extractorName ;
507612 }
613+
614+ @ Override
615+ public boolean equals (Object obj ) {
616+ if (obj instanceof TrapClassVersion ) {
617+ TrapClassVersion other = (TrapClassVersion )obj ;
618+ return majorVersion == other .majorVersion && minorVersion == other .minorVersion && lastModified == other .lastModified && extractorName .equals (other .extractorName );
619+ } else {
620+ return false ;
621+ }
622+ }
623+
508624 private boolean newerThan (TrapClassVersion tcv ) {
509625 // Classes being compiled from source have major version 0 but should take precedence
510626 // over any classes with the same qualified name loaded from the classpath
0 commit comments