From 959cb5fd4f9586ec3bd265b452fe25fe1db82e3f Mon Sep 17 00:00:00 2001 From: jeff Date: Tue, 13 Jan 2015 23:25:04 -0500 Subject: source format change don't hate me too much if you were planning a big merge. =P --- src/cuchaz/enigma/convert/ClassMatcher.java | 412 ++++++++++++---------------- 1 file changed, 169 insertions(+), 243 deletions(-) (limited to 'src/cuchaz/enigma/convert/ClassMatcher.java') diff --git a/src/cuchaz/enigma/convert/ClassMatcher.java b/src/cuchaz/enigma/convert/ClassMatcher.java index 290d90a..fc39ed0 100644 --- a/src/cuchaz/enigma/convert/ClassMatcher.java +++ b/src/cuchaz/enigma/convert/ClassMatcher.java @@ -49,102 +49,92 @@ import cuchaz.enigma.mapping.MappingsWriter; import cuchaz.enigma.mapping.MethodEntry; import cuchaz.enigma.mapping.MethodMapping; -public class ClassMatcher -{ - public static void main( String[] args ) - throws IOException, MappingParseException - { +public class ClassMatcher { + + public static void main(String[] args) throws IOException, MappingParseException { // TEMP - JarFile sourceJar = new JarFile( new File( "input/1.8-pre3.jar" ) ); - JarFile destJar = new JarFile( new File( "input/1.8.jar" ) ); - File inMappingsFile = new File( "../Enigma Mappings/1.8-pre3.mappings" ); - File outMappingsFile = new File( "../Enigma Mappings/1.8.mappings" ); + JarFile sourceJar = new JarFile(new File("input/1.8-pre3.jar")); + JarFile destJar = new JarFile(new File("input/1.8.jar")); + File inMappingsFile = new File("../Enigma Mappings/1.8-pre3.mappings"); + File outMappingsFile = new File("../Enigma Mappings/1.8.mappings"); // define a matching to use when the automated system cannot find a match Map fallbackMatching = Maps.newHashMap(); - fallbackMatching.put( "none/ayb", "none/ayf" ); - fallbackMatching.put( "none/ayd", "none/ayd" ); - fallbackMatching.put( "none/bgk", "unknown/bgk" ); + fallbackMatching.put("none/ayb", "none/ayf"); + fallbackMatching.put("none/ayd", "none/ayd"); + fallbackMatching.put("none/bgk", "unknown/bgk"); // do the conversion - Mappings mappings = new MappingsReader().read( new FileReader( inMappingsFile ) ); - convertMappings( sourceJar, destJar, mappings, fallbackMatching ); + Mappings mappings = new MappingsReader().read(new FileReader(inMappingsFile)); + convertMappings(sourceJar, destJar, mappings, fallbackMatching); // write out the converted mappings - FileWriter writer = new FileWriter( outMappingsFile ); - new MappingsWriter().write( writer, mappings ); + FileWriter writer = new FileWriter(outMappingsFile); + new MappingsWriter().write(writer, mappings); writer.close(); - System.out.println( "Wrote converted mappings to:\n\t" + outMappingsFile.getAbsolutePath() ); + System.out.println("Wrote converted mappings to:\n\t" + outMappingsFile.getAbsolutePath()); } - private static void convertMappings( JarFile sourceJar, JarFile destJar, Mappings mappings, Map fallbackMatching ) - { + private static void convertMappings(JarFile sourceJar, JarFile destJar, Mappings mappings, Map fallbackMatching) { // index jars - System.out.println( "Indexing source jar..." ); + System.out.println("Indexing source jar..."); JarIndex sourceIndex = new JarIndex(); - sourceIndex.indexJar( sourceJar, false ); - System.out.println( "Indexing dest jar..." ); + sourceIndex.indexJar(sourceJar, false); + System.out.println("Indexing dest jar..."); JarIndex destIndex = new JarIndex(); - destIndex.indexJar( destJar, false ); - TranslatingTypeLoader sourceLoader = new TranslatingTypeLoader( sourceJar, sourceIndex ); - TranslatingTypeLoader destLoader = new TranslatingTypeLoader( destJar, destIndex ); - + destIndex.indexJar(destJar, false); + TranslatingTypeLoader sourceLoader = new TranslatingTypeLoader(sourceJar, sourceIndex); + TranslatingTypeLoader destLoader = new TranslatingTypeLoader(destJar, destIndex); + // compute the matching - ClassMatching matching = computeMatching( sourceIndex, sourceLoader, destIndex, destLoader ); + ClassMatching matching = computeMatching(sourceIndex, sourceLoader, destIndex, destLoader); Map>> matchingIndex = matching.getIndex(); // get all the obf class names used in the mappings Set usedClassNames = mappings.getAllObfClassNames(); Set allClassNames = Sets.newHashSet(); - for( ClassEntry classEntry : sourceIndex.getObfClassEntries() ) - { - allClassNames.add( classEntry.getName() ); + for (ClassEntry classEntry : sourceIndex.getObfClassEntries()) { + allClassNames.add(classEntry.getName()); } - usedClassNames.retainAll( allClassNames ); - System.out.println( "Used " + usedClassNames.size() + " classes in the mappings" ); + usedClassNames.retainAll(allClassNames); + System.out.println("Used " + usedClassNames.size() + " classes in the mappings"); // probabilistically match the non-uniquely-matched source classes - for( Map.Entry> entry : matchingIndex.values() ) - { + for (Map.Entry> entry : matchingIndex.values()) { ClassIdentity sourceClass = entry.getKey(); List destClasses = entry.getValue(); // skip classes that are uniquely matched - if( destClasses.size() == 1 ) - { + if (destClasses.size() == 1) { continue; } // skip classes that aren't used in the mappings - if( !usedClassNames.contains( sourceClass.getClassEntry().getName() ) ) - { + if (!usedClassNames.contains(sourceClass.getClassEntry().getName())) { continue; } - System.out.println( "No exact match for source class " + sourceClass.getClassEntry() ); + System.out.println("No exact match for source class " + sourceClass.getClassEntry()); // find the closest classes Multimap scoredMatches = ArrayListMultimap.create(); - for( ClassIdentity c : destClasses ) - { - scoredMatches.put( sourceClass.getMatchScore( c ), c ); + for (ClassIdentity c : destClasses) { + scoredMatches.put(sourceClass.getMatchScore(c), c); } - List scores = new ArrayList( scoredMatches.keySet() ); - Collections.sort( scores, Collections.reverseOrder() ); - printScoredMatches( sourceClass.getMaxMatchScore(), scores, scoredMatches ); + List scores = new ArrayList(scoredMatches.keySet()); + Collections.sort(scores, Collections.reverseOrder()); + printScoredMatches(sourceClass.getMaxMatchScore(), scores, scoredMatches); // does the best match have a non-zero score and the same name? - int bestScore = scores.get( 0 ); - Collection bestMatches = scoredMatches.get( bestScore ); - if( bestScore > 0 && bestMatches.size() == 1 ) - { + int bestScore = scores.get(0); + Collection bestMatches = scoredMatches.get(bestScore); + if (bestScore > 0 && bestMatches.size() == 1) { ClassIdentity bestMatch = bestMatches.iterator().next(); - if( bestMatch.getClassEntry().equals( sourceClass.getClassEntry() ) ) - { + if (bestMatch.getClassEntry().equals(sourceClass.getClassEntry())) { // use it - System.out.println( "\tAutomatically choosing likely match: " + bestMatch.getClassEntry().getName() ); + System.out.println("\tAutomatically choosing likely match: " + bestMatch.getClassEntry().getName()); destClasses.clear(); - destClasses.add( bestMatch ); + destClasses.add(bestMatch); } } } @@ -152,63 +142,46 @@ public class ClassMatcher // group the matching into unique and non-unique matches BiMap matchedClassNames = HashBiMap.create(); Set unmatchedSourceClassNames = Sets.newHashSet(); - for( String className : usedClassNames ) - { + for (String className : usedClassNames) { // is there a match for this class? - Map.Entry> entry = matchingIndex.get( className ); + Map.Entry> entry = matchingIndex.get(className); ClassIdentity sourceClass = entry.getKey(); List matches = entry.getValue(); - if( matches.size() == 1 ) - { + if (matches.size() == 1) { // unique match! We're good to go! - matchedClassNames.put( - sourceClass.getClassEntry().getName(), - matches.get( 0 ).getClassEntry().getName() - ); - } - else - { + matchedClassNames.put(sourceClass.getClassEntry().getName(), matches.get(0).getClassEntry().getName()); + } else { // no match, check the fallback matching - String fallbackMatch = fallbackMatching.get( className ); - if( fallbackMatch != null ) - { - matchedClassNames.put( - sourceClass.getClassEntry().getName(), - fallbackMatch - ); - } - else - { - unmatchedSourceClassNames.add( className ); + String fallbackMatch = fallbackMatching.get(className); + if (fallbackMatch != null) { + matchedClassNames.put(sourceClass.getClassEntry().getName(), fallbackMatch); + } else { + unmatchedSourceClassNames.add(className); } } } // report unmatched classes - if( !unmatchedSourceClassNames.isEmpty() ) - { - System.err.println( "ERROR: there were unmatched classes!" ); - for( String className : unmatchedSourceClassNames ) - { - System.err.println( "\t" + className ); + if (!unmatchedSourceClassNames.isEmpty()) { + System.err.println("ERROR: there were unmatched classes!"); + for (String className : unmatchedSourceClassNames) { + System.err.println("\t" + className); } return; } // get the class name changes from the matched class names Map classChanges = Maps.newHashMap(); - for( Map.Entry entry : matchedClassNames.entrySet() ) - { - if( !entry.getKey().equals( entry.getValue() ) ) - { - classChanges.put( entry.getKey(), entry.getValue() ); - System.out.println( String.format( "Class change: %s -> %s", entry.getKey(), entry.getValue() ) ); + for (Map.Entry entry : matchedClassNames.entrySet()) { + if (!entry.getKey().equals(entry.getValue())) { + classChanges.put(entry.getKey(), entry.getValue()); + System.out.println(String.format("Class change: %s -> %s", entry.getKey(), entry.getValue())); /* DEBUG - System.out.println( String.format( "\n%s\n%s", - new ClassIdentity( sourceLoader.loadClass( entry.getKey() ), null, sourceIndex, false, false ), - new ClassIdentity( destLoader.loadClass( entry.getValue() ), null, destIndex, false, false ) - ) ); + System.out.println(String.format("\n%s\n%s", + new ClassIdentity(sourceLoader.loadClass(entry.getKey()), null, sourceIndex, false, false), + new ClassIdentity( destLoader.loadClass(entry.getValue()), null, destIndex, false, false) + )); */ } } @@ -217,52 +190,42 @@ public class ClassMatcher // ie. if we have the mappings a->b, b->c, we have to apply b->c before a->b LinkedHashMap orderedClassChanges = Maps.newLinkedHashMap(); int numChangesLeft = classChanges.size(); - while( !classChanges.isEmpty() ) - { + while (!classChanges.isEmpty()) { Iterator> iter = classChanges.entrySet().iterator(); - while( iter.hasNext() ) - { + while (iter.hasNext()) { Map.Entry entry = iter.next(); - if( classChanges.get( entry.getValue() ) == null ) - { - orderedClassChanges.put( entry.getKey(), entry.getValue() ); + if (classChanges.get(entry.getValue()) == null) { + orderedClassChanges.put(entry.getKey(), entry.getValue()); iter.remove(); } } // did we remove any changes? - if( numChangesLeft - classChanges.size() > 0 ) - { + if (numChangesLeft - classChanges.size() > 0) { // keep going - numChangesLeft = classChanges.size(); - } - else - { + numChangesLeft = classChanges.size(); + } else { // can't sort anymore. There must be a loop break; } } - if( classChanges.size() > 0 ) - { - throw new Error( String.format( "Unable to sort %d/%d class changes!", classChanges.size(), matchedClassNames.size() ) ); + if (classChanges.size() > 0) { + throw new Error(String.format("Unable to sort %d/%d class changes!", classChanges.size(), matchedClassNames.size())); } // convert the mappings in the correct class order - for( Map.Entry entry : orderedClassChanges.entrySet() ) - { - mappings.renameObfClass( entry.getKey(), entry.getValue() ); + for (Map.Entry entry : orderedClassChanges.entrySet()) { + mappings.renameObfClass(entry.getKey(), entry.getValue()); } // check the method matches - System.out.println( "Checking methods..." ); - for( ClassMapping classMapping : mappings.classes() ) - { - ClassEntry classEntry = new ClassEntry( classMapping.getObfName() ); - for( MethodMapping methodMapping : classMapping.methods() ) - { + System.out.println("Checking methods..."); + for (ClassMapping classMapping : mappings.classes()) { + ClassEntry classEntry = new ClassEntry(classMapping.getObfName()); + for (MethodMapping methodMapping : classMapping.methods()) { + // skip constructors - if( methodMapping.getObfName().equals( "" ) ) - { + if (methodMapping.getObfName().equals("")) { continue; } @@ -271,56 +234,51 @@ public class ClassMatcher methodMapping.getObfName(), methodMapping.getObfSignature() ); - if( !destIndex.containsObfBehavior( methodEntry ) ) - { - System.err.println( "WARNING: method doesn't match: " + methodEntry ); + if (!destIndex.containsObfBehavior(methodEntry)) { + System.err.println("WARNING: method doesn't match: " + methodEntry); // show the available methods - System.err.println( "\tAvailable dest methods:" ); - CtClass c = destLoader.loadClass( classMapping.getObfName() ); - for( CtBehavior behavior : c.getDeclaredBehaviors() ) - { + System.err.println("\tAvailable dest methods:"); + CtClass c = destLoader.loadClass(classMapping.getObfName()); + for (CtBehavior behavior : c.getDeclaredBehaviors()) { MethodEntry declaredMethodEntry = new MethodEntry( - new ClassEntry( classMapping.getObfName() ), + new ClassEntry(classMapping.getObfName()), behavior.getName(), behavior.getSignature() ); - System.err.println( "\t\t" + declaredMethodEntry ); + System.err.println("\t\t" + declaredMethodEntry); } - System.err.println( "\tAvailable source methods:" ); - c = sourceLoader.loadClass( matchedClassNames.inverse().get( classMapping.getObfName() ) ); - for( CtBehavior behavior : c.getDeclaredBehaviors() ) - { + System.err.println("\tAvailable source methods:"); + c = sourceLoader.loadClass(matchedClassNames.inverse().get(classMapping.getObfName())); + for (CtBehavior behavior : c.getDeclaredBehaviors()) { MethodEntry declaredMethodEntry = new MethodEntry( - new ClassEntry( classMapping.getObfName() ), + new ClassEntry(classMapping.getObfName()), behavior.getName(), behavior.getSignature() ); - System.err.println( "\t\t" + declaredMethodEntry ); + System.err.println("\t\t" + declaredMethodEntry); } } } } - System.out.println( "Done!" ); + System.out.println("Done!"); } - public static ClassMatching computeMatching( JarIndex sourceIndex, TranslatingTypeLoader sourceLoader, JarIndex destIndex, TranslatingTypeLoader destLoader ) - { - System.out.println( "Matching classes..." ); + public static ClassMatching computeMatching(JarIndex sourceIndex, TranslatingTypeLoader sourceLoader, JarIndex destIndex, TranslatingTypeLoader destLoader) { + + System.out.println("Matching classes..."); + ClassMatching matching = null; - for( boolean useReferences : Arrays.asList( false, true ) ) - { + for (boolean useReferences : Arrays.asList(false, true)) { int numMatches = 0; - do - { + do { SidedClassNamer sourceNamer = null; SidedClassNamer destNamer = null; - if( matching != null ) - { + if (matching != null) { // build a class namer - ClassNamer namer = new ClassNamer( matching.getUniqueMatches() ); + ClassNamer namer = new ClassNamer(matching.getUniqueMatches()); sourceNamer = namer.getSourceNamer(); destNamer = namer.getDestNamer(); @@ -331,158 +289,126 @@ public class ClassMatcher // get the entries left to match Set sourceClassEntries = Sets.newHashSet(); Set destClassEntries = Sets.newHashSet(); - if( matching == null ) - { - sourceClassEntries.addAll( sourceIndex.getObfClassEntries() ); - destClassEntries.addAll( destIndex.getObfClassEntries() ); + if (matching == null) { + sourceClassEntries.addAll(sourceIndex.getObfClassEntries()); + destClassEntries.addAll(destIndex.getObfClassEntries()); matching = new ClassMatching(); - } - else - { - for( Map.Entry,List> entry : matching.getAmbiguousMatches().entrySet() ) - { - for( ClassIdentity c : entry.getKey() ) - { - sourceClassEntries.add( c.getClassEntry() ); - matching.removeSource( c ); + } else { + for (Map.Entry,List> entry : matching.getAmbiguousMatches().entrySet()) { + for (ClassIdentity c : entry.getKey()) { + sourceClassEntries.add(c.getClassEntry()); + matching.removeSource(c); } - for( ClassIdentity c : entry.getValue() ) - { - destClassEntries.add( c.getClassEntry() ); - matching.removeDest( c ); + for (ClassIdentity c : entry.getValue()) { + destClassEntries.add(c.getClassEntry()); + matching.removeDest(c); } } - for( ClassIdentity c : matching.getUnmatchedSourceClasses() ) - { - sourceClassEntries.add( c.getClassEntry() ); - matching.removeSource( c ); + for (ClassIdentity c : matching.getUnmatchedSourceClasses()) { + sourceClassEntries.add(c.getClassEntry()); + matching.removeSource(c); } - for( ClassIdentity c : matching.getUnmatchedDestClasses() ) - { - destClassEntries.add( c.getClassEntry() ); - matching.removeDest( c ); + for (ClassIdentity c : matching.getUnmatchedDestClasses()) { + destClassEntries.add(c.getClassEntry()); + matching.removeDest(c); } } // compute a matching for the classes - for( ClassEntry classEntry : sourceClassEntries ) - { - CtClass c = sourceLoader.loadClass( classEntry.getName() ); - ClassIdentity sourceClass = new ClassIdentity( c, sourceNamer, sourceIndex, useReferences ); - matching.addSource( sourceClass ); + for (ClassEntry classEntry : sourceClassEntries) { + CtClass c = sourceLoader.loadClass(classEntry.getName()); + ClassIdentity sourceClass = new ClassIdentity(c, sourceNamer, sourceIndex, useReferences); + matching.addSource(sourceClass); } - for( ClassEntry classEntry : destClassEntries ) - { - CtClass c = destLoader.loadClass( classEntry.getName() ); - ClassIdentity destClass = new ClassIdentity( c, destNamer, destIndex, useReferences ); - matching.matchDestClass( destClass ); + for (ClassEntry classEntry : destClassEntries) { + CtClass c = destLoader.loadClass(classEntry.getName()); + ClassIdentity destClass = new ClassIdentity(c, destNamer, destIndex, useReferences); + matching.matchDestClass(destClass); } // TEMP - System.out.println( matching ); - } - while( matching.getUniqueMatches().size() - numMatches > 0 ); + System.out.println(matching); + } while (matching.getUniqueMatches().size() - numMatches > 0); } // check the class matches - System.out.println( "Checking class matches..." ); - ClassNamer namer = new ClassNamer( matching.getUniqueMatches() ); + System.out.println("Checking class matches..."); + ClassNamer namer = new ClassNamer(matching.getUniqueMatches()); SidedClassNamer sourceNamer = namer.getSourceNamer(); SidedClassNamer destNamer = namer.getDestNamer(); - for( Map.Entry entry : matching.getUniqueMatches().entrySet() ) - { + for (Map.Entry entry : matching.getUniqueMatches().entrySet()) { + // check source ClassIdentity sourceClass = entry.getKey(); - CtClass sourceC = sourceLoader.loadClass( sourceClass.getClassEntry().getName() ); - assert( sourceC != null ) - : "Unable to load source class " + sourceClass.getClassEntry(); - assert( sourceClass.matches( sourceC ) ) - : "Source " + sourceClass + " doesn't match " + new ClassIdentity( sourceC, sourceNamer, sourceIndex, false ); + CtClass sourceC = sourceLoader.loadClass(sourceClass.getClassEntry().getName()); + assert (sourceC != null) : "Unable to load source class " + sourceClass.getClassEntry(); + assert (sourceClass.matches(sourceC)) : "Source " + sourceClass + " doesn't match " + new ClassIdentity(sourceC, sourceNamer, sourceIndex, false); // check dest ClassIdentity destClass = entry.getValue(); - CtClass destC = destLoader.loadClass( destClass.getClassEntry().getName() ); - assert( destC != null ) - : "Unable to load dest class " + destClass.getClassEntry(); - assert( destClass.matches( destC ) ) - : "Dest " + destClass + " doesn't match " + new ClassIdentity( destC, destNamer, destIndex, false ); + CtClass destC = destLoader.loadClass(destClass.getClassEntry().getName()); + assert (destC != null) : "Unable to load dest class " + destClass.getClassEntry(); + assert (destClass.matches(destC)) : "Dest " + destClass + " doesn't match " + new ClassIdentity(destC, destNamer, destIndex, false); } // warn about the ambiguous matchings - List,List>> ambiguousMatches = new ArrayList,List>>( matching.getAmbiguousMatches().entrySet() ); - Collections.sort( ambiguousMatches, new Comparator,List>>( ) - { + List,List>> ambiguousMatches = new ArrayList,List>>(matching.getAmbiguousMatches().entrySet()); + Collections.sort(ambiguousMatches, new Comparator,List>>() { @Override - public int compare( Map.Entry,List> a, Map.Entry,List> b ) - { - String aName = a.getKey().get( 0 ).getClassEntry().getName(); - String bName = b.getKey().get( 0 ).getClassEntry().getName(); - return aName.compareTo( bName ); + public int compare(Map.Entry,List> a, Map.Entry,List> b) { + String aName = a.getKey().get(0).getClassEntry().getName(); + String bName = b.getKey().get(0).getClassEntry().getName(); + return aName.compareTo(bName); } - } ); - for( Map.Entry,List> entry : ambiguousMatches ) - { - System.out.println( "Ambiguous matching:" ); - System.out.println( "\tSource: " + getClassNames( entry.getKey() ) ); - System.out.println( "\tDest: " + getClassNames( entry.getValue() ) ); + }); + for (Map.Entry,List> entry : ambiguousMatches) { + System.out.println("Ambiguous matching:"); + System.out.println("\tSource: " + getClassNames(entry.getKey())); + System.out.println("\tDest: " + getClassNames(entry.getValue())); } /* DEBUG Map.Entry,List> entry = ambiguousMatches.get( 7 ); - for( ClassIdentity c : entry.getKey() ) - { - System.out.println( c ); + for (ClassIdentity c : entry.getKey()) { + System.out.println(c); } - for( ClassIdentity c : entry.getKey() ) - { - System.out.println( decompile( sourceLoader, c.getClassEntry() ) ); + for(ClassIdentity c : entry.getKey()) { + System.out.println(decompile(sourceLoader, c.getClassEntry())); } */ return matching; } - private static void printScoredMatches( int maxScore, List scores, Multimap scoredMatches ) - { + private static void printScoredMatches(int maxScore, List scores, Multimap scoredMatches) { int numScoredMatchesShown = 0; - for( int score : scores ) - { - for( ClassIdentity scoredMatch : scoredMatches.get( score ) ) - { - System.out.println( String.format( "\tScore: %3d %3.0f%% %s", - score, - 100.0*score/maxScore, - scoredMatch.getClassEntry().getName() - ) ); - - if( numScoredMatchesShown++ > 10 ) - { + for (int score : scores) { + for (ClassIdentity scoredMatch : scoredMatches.get(score)) { + System.out.println(String.format("\tScore: %3d %3.0f%% %s", score, 100.0 * score / maxScore, scoredMatch.getClassEntry().getName())); + if (numScoredMatchesShown++ > 10) { return; } } } } - private static List getClassNames( Collection classes ) - { + private static List getClassNames(Collection classes) { List out = Lists.newArrayList(); - for( ClassIdentity c : classes ) - { - out.add( c.getClassEntry().getName() ); + for (ClassIdentity c : classes) { + out.add(c.getClassEntry().getName()); } - Collections.sort( out ); + Collections.sort(out); return out; } /* DEBUG - private static String decompile( TranslatingTypeLoader loader, ClassEntry classEntry ) - { + private static String decompile(TranslatingTypeLoader loader, ClassEntry classEntry) { PlainTextOutput output = new PlainTextOutput(); DecompilerSettings settings = DecompilerSettings.javaDefaults(); - settings.setForceExplicitImports( true ); - settings.setShowSyntheticMembers( true ); - settings.setTypeLoader( loader ); - Decompiler.decompile( classEntry.getName(), output, settings ); + settings.setForceExplicitImports(true); + settings.setShowSyntheticMembers(true); + settings.setTypeLoader(loader); + Decompiler.decompile(classEntry.getName(), output, settings); return output.toString(); } */ -- cgit v1.2.3