├── README.md ├── build.gradle ├── jitpack.yml ├── resources ├── cc.mixins.json └── fabric.mod.json └── src └── com └── chocohead └── cc ├── Extension.java ├── SMAPper.java ├── SourcePool.java ├── mixin ├── CrashReportMixin.java └── CrashReportSectionMixin.java └── smap ├── FileInfo.java ├── InvalidFormat.java ├── LineInfo.java ├── SMAP.java ├── SMAPReader.java ├── Stratum.java └── StratumBuilder.java /README.md: -------------------------------------------------------------------------------- 1 | # Crafty Crashes 2 | A Fabric mod which modifies stack traces in crash reports to include the relevant Mixin and source line number where appropriate, acting as a super [Mixin Trace](//github.com/comp500/mixintrace) whilst still working alongside it. Includes an [SMAP](https://jcp.org/en/jsr/detail?id=45) reader in the off chance you need one too. 3 | 4 | Tested on 1.16.5 and 1.17.1, likely to work on many other versions which have similar crash reporting logic 5 | 6 | ### Example 7 | Using the following Mixin as an example of a Mixin which causes issues at runtime: 8 | ```java 9 | @Mixin(Keyboard.class) 10 | abstract class BadMixin { 11 | @Dynamic("1.16 Lambda") 12 | @Group(min = 1, max = 1) 13 | @Inject(method = "method_1454(I[ZLnet/minecraft/client/gui/ParentElement;III)V", 14 | at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/ParentElement;keyPressed(III)Z")) 15 | private void beforeKeyPressedEvent(int code, boolean[] resultHack, ParentElement parentElement, int key, int scancode, int modifiers, CallbackInfo call) { 16 | if (key == GLFW.GLFW_KEY_T) { 17 | throw new RuntimeException("Oh no a crash"); 18 | } 19 | } 20 | 21 | @Dynamic("1.17 Lambda") 22 | @Group(min = 1, max = 1) 23 | @Inject(method = "method_1454(ILnet/minecraft/client/gui/screen/Screen;[ZIII)V", 24 | at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;keyPressed(III)Z")) 25 | private void beforeKeyPressedEvent(int code, Screen screen, boolean[] resultHack, int key, int scancode, int modifiers, CallbackInfo call) { 26 | beforeKeyPressedEvent(code, resultHack, screen, key, scancode, modifiers, call); 27 | } 28 | } 29 | ``` 30 | Normally when this is run you'd get the following crash report section (which is effectively the top frames of the full stack trace): 31 | ``` 32 | -- Head -- 33 | Thread: Render thread 34 | Stacktrace: 35 | at net.minecraft.class_309.handler$zzg001$beforeKeyPressedEvent(class_309.java:1075) 36 | at net.minecraft.class_309.handler$zzg001$beforeKeyPressedEvent(class_309.java:1084) 37 | at net.minecraft.class_309.method_1454(class_309.java:374) 38 | ``` 39 | `Keyboard` only has around 520 lines, so 1075 and 1084 are entirely unhelpful to finding what is wrong. The only knowledge gained is that the Mixin which crashed had handlers called `beforeKeyPressedEvent`. 40 | 41 | In contrast, running with Crafty Crashes: 42 | ``` 43 | -- Head -- 44 | Thread: Render thread 45 | Stacktrace: 46 | at net.minecraft.class_309.handler$zzg001$beforeKeyPressedEvent(com/chocohead/example/mixin/BadMixin.java [cc-example.mixins.json]:24) 47 | at net.minecraft.class_309.handler$zzg001$beforeKeyPressedEvent(com/chocohead/example/mixin/BadMixin.java [cc-example.mixins.json]:33) 48 | at net.minecraft.class_309.method_1454(class_309.java:374) 49 | ``` 50 | Now we have the fully qualified name of the Mixin which has gone wrong, the Mixin config which registered it, and the correct line numbers for the Mixin. This makes finding and debugging the Mixin much easier, especially as searching for a Mixin config by name on Github is more likely to return a unique result than searching the handler or Mixin class's name. 51 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | mavenCentral() 8 | maven { 9 | name = 'Jitpack' 10 | url 'https://jitpack.io/' 11 | } 12 | } 13 | dependencies { 14 | classpath 'com.github.Chocohead:Fabric-Loom:66ed9fe' 15 | } 16 | } 17 | 18 | apply plugin: 'fabric-loom' 19 | 20 | sourceCompatibility = 1.8 21 | targetCompatibility = 1.8 22 | 23 | archivesBaseName = 'Crafty-Crashes' 24 | version = '1.0' 25 | 26 | repositories { 27 | maven { 28 | name = 'Jitpack' 29 | url 'https://jitpack.io/' 30 | } 31 | } 32 | 33 | dependencies { 34 | def mcVersion = '1.16.3' 35 | def mapping = "$mcVersion+build.5" 36 | 37 | minecraft "com.mojang:minecraft:$mcVersion" 38 | mappings "net.fabricmc:yarn:$mapping:v2" 39 | 40 | modImplementation 'net.fabricmc:fabric-loader:0.11.3' 41 | } 42 | 43 | sourceSets { 44 | main { 45 | java { 46 | srcDir 'src' 47 | } 48 | resources { 49 | srcDir 'resources' 50 | } 51 | } 52 | } 53 | 54 | processResources { 55 | inputs.property 'version', project.version 56 | 57 | from(sourceSets.main.resources.srcDirs) { 58 | include 'fabric.mod.json' 59 | expand 'version': project.version 60 | } 61 | 62 | from(sourceSets.main.resources.srcDirs) { 63 | exclude 'fabric.mod.json' 64 | } 65 | } 66 | 67 | minecraft { 68 | fieldInferenceFilter = {existingName, replacement -> false} 69 | } 70 | 71 | tasks.withType(JavaCompile) { 72 | options.encoding = 'UTF-8' 73 | } 74 | 75 | task sourcesJar(type: Jar, dependsOn: classes) { 76 | classifier = 'sources' 77 | from sourceSets.main.allSource 78 | } -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - mv build.gradle temp.gradle 3 | - gradle wrapper --gradle-version 4.9 --no-daemon --stacktrace 4 | - mv temp.gradle build.gradle -------------------------------------------------------------------------------- /resources/cc.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "com.chocohead.cc.mixin", 4 | "compatibilityLevel": "JAVA_8", 5 | "plugin": "com.chocohead.cc.Extension", 6 | "mixins": [ 7 | "CrashReportMixin", 8 | "CrashReportSectionMixin" 9 | ], 10 | "injectors": { 11 | "defaultRequire": 1 12 | } 13 | } -------------------------------------------------------------------------------- /resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "not-that-cc", 4 | "version": "${version}", 5 | 6 | "name": "Crafty Crashes", 7 | "description": "Like before but nothing like before", 8 | "authors": [ 9 | "Chocohead" 10 | ], 11 | "contact": { 12 | "homepage": "https://www.curseforge.com/minecraft/mc-mods/crafty-crashes", 13 | "sources": "https://github.com/Chocohead/Crafty-Crashes", 14 | "issues": "https://github.com/Chocohead/Crafty-Crashes/issues" 15 | }, 16 | "license": "MPL-2.0", 17 | 18 | "environment": "*", 19 | "depends": { 20 | "fabricloader": ">=0.7" 21 | }, 22 | "entrypoints": { 23 | }, 24 | "mixins": [ 25 | "cc.mixins.json" 26 | ] 27 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/Extension.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import java.util.Set; 6 | 7 | import org.objectweb.asm.tree.ClassNode; 8 | 9 | import org.spongepowered.asm.mixin.MixinEnvironment; 10 | import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; 11 | import org.spongepowered.asm.mixin.extensibility.IMixinInfo; 12 | import org.spongepowered.asm.mixin.transformer.IMixinTransformer; 13 | import org.spongepowered.asm.mixin.transformer.ext.Extensions; 14 | import org.spongepowered.asm.mixin.transformer.ext.IExtension; 15 | import org.spongepowered.asm.mixin.transformer.ext.IExtensionRegistry; 16 | import org.spongepowered.asm.mixin.transformer.ext.ITargetClassContext; 17 | 18 | public class Extension implements IMixinConfigPlugin, IExtension { 19 | @Override 20 | public void onLoad(String mixinPackage) { 21 | Object transformer = MixinEnvironment.getCurrentEnvironment().getActiveTransformer(); 22 | if (!(transformer instanceof IMixinTransformer)) throw new IllegalStateException("Running with an odd transformer: " + transformer); 23 | 24 | IExtensionRegistry extensions = ((IMixinTransformer) transformer).getExtensions(); 25 | if (!(extensions instanceof Extensions)) throw new IllegalStateException("Running with odd extensions: " + extensions); 26 | 27 | ((Extensions) extensions).add(this); 28 | } 29 | 30 | @Override 31 | public boolean checkActive(MixinEnvironment environment) { 32 | return true; 33 | } 34 | 35 | 36 | @Override 37 | public List getMixins() { 38 | return Collections.emptyList(); 39 | } 40 | 41 | @Override 42 | public String getRefMapperConfig() { 43 | return null; 44 | } 45 | 46 | @Override 47 | public void acceptTargets(Set myTargets, Set otherTargets) { 48 | } 49 | 50 | @Override 51 | public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { 52 | return true; 53 | } 54 | 55 | 56 | @Override 57 | public void preApply(ITargetClassContext context) { 58 | } 59 | 60 | @Override 61 | public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { 62 | } 63 | 64 | @Override 65 | public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { 66 | } 67 | 68 | @Override 69 | public void postApply(ITargetClassContext context) { 70 | } 71 | 72 | @Override 73 | public void export(MixinEnvironment env, String name, boolean force, ClassNode node) { 74 | SourcePool.add(name, node.sourceDebug); 75 | } 76 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/SMAPper.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.spongepowered.asm.mixin.extensibility.IMixinInfo; 7 | 8 | import com.chocohead.cc.smap.FileInfo; 9 | import com.chocohead.cc.smap.LineInfo; 10 | import com.chocohead.cc.smap.SMAP; 11 | 12 | public class SMAPper { 13 | public static void apply(Throwable t, String... skippedPackages) { 14 | apply(t, new HashMap(), skippedPackages); 15 | } 16 | 17 | private static void apply(Throwable t, Map cache, String... skippedPackages) { 18 | StackTraceElement[] elements = t.getStackTrace(); 19 | if (apply(elements, cache, skippedPackages)) t.setStackTrace(elements); 20 | 21 | if (t.getCause() != null) apply(t.getCause(), cache, skippedPackages); 22 | for (Throwable suppressed : t.getSuppressed()) { 23 | apply(suppressed, cache, skippedPackages); 24 | } 25 | } 26 | 27 | public static boolean apply(StackTraceElement[] elements, String... skippedPackages) { 28 | return apply(elements, new HashMap<>(), skippedPackages); 29 | } 30 | 31 | private static boolean apply(StackTraceElement[] elements, Map cache, String... skippedPackages) { 32 | boolean modified = false; 33 | 34 | for (int i = 0, end = elements.length; i < end; i++) { 35 | StackTraceElement element = elements[i]; 36 | if (element.isNativeMethod() || element.getLineNumber() < 0) continue; 37 | 38 | String className = element.getClassName(); 39 | 40 | SMAP smap; 41 | if (!cache.containsKey(className)) { 42 | boolean skip = false; 43 | 44 | for (String packageName : skippedPackages) { 45 | if (className.startsWith(packageName)) { 46 | skip = true; 47 | break; 48 | } 49 | } 50 | 51 | smap = null; 52 | if (!skip) { 53 | String source = SourcePool.get(className); 54 | 55 | if (source != null && source.startsWith("SMAP")) { 56 | smap = SMAP.forResolved(source); 57 | } 58 | } 59 | 60 | cache.put(className, smap); 61 | } else { 62 | smap = cache.get(className); 63 | } 64 | 65 | if (smap != null && smap.generatedFileName.equals(element.getFileName())) { 66 | LineInfo realLine = smap.getDefaultStratum().mapLine(element.getLineNumber()); 67 | 68 | if (realLine != null && !realLine.file.name.equals(element.getFileName())) { 69 | elements[i] = new StackTraceElement(element.getClassName(), element.getMethodName(), realLine.file.getBestName("Unspecified?").concat(findMixin(realLine.file)), realLine.line); 70 | modified = true; 71 | } 72 | } 73 | } 74 | 75 | return modified; 76 | } 77 | 78 | private static String findMixin(FileInfo file) { 79 | IMixinInfo mixin = SourcePool.findFor(file); 80 | return mixin != null ? " [" + mixin.getConfig().getName() + ']' : ""; 81 | } 82 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/SourcePool.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc; 2 | 3 | import java.util.HashMap; 4 | import java.util.IdentityHashMap; 5 | import java.util.Map; 6 | 7 | import com.google.common.collect.Iterables; 8 | 9 | import org.spongepowered.asm.mixin.extensibility.IMixinInfo; 10 | import org.spongepowered.asm.mixin.transformer.ClassInfo; 11 | 12 | import com.chocohead.cc.smap.FileInfo; 13 | 14 | public class SourcePool { 15 | private static final Map SOURCES = new HashMap<>(); 16 | private static final Map MIXINS = new IdentityHashMap<>(); 17 | 18 | static void add(String owner, String source) { 19 | if (SOURCES.put(owner, source) != null) { 20 | throw new IllegalArgumentException("Duplicate source mapping for " + owner); 21 | } 22 | } 23 | 24 | public static String get(String owner) { 25 | return SOURCES.get(owner); 26 | } 27 | 28 | public static IMixinInfo findFor(FileInfo file) { 29 | if (!MIXINS.containsKey(file)) { 30 | if (file.path != null && file.path.endsWith(".java")) { 31 | ClassInfo info = ClassInfo.fromCache(file.path.substring(0, file.path.length() - 5)); 32 | 33 | if (info != null && info.isMixin()) {//This is very silly but also useful 34 | IMixinInfo out = Iterables.getOnlyElement(info.getAppliedMixins()); 35 | MIXINS.put(file, out); 36 | return out; 37 | } 38 | } 39 | 40 | MIXINS.put(file, null); 41 | return null; 42 | } else { 43 | return MIXINS.get(file); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/mixin/CrashReportMixin.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc.mixin; 2 | 3 | import org.objectweb.asm.Opcodes; 4 | 5 | import org.spongepowered.asm.mixin.Final; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.Shadow; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.At.Shift; 10 | import org.spongepowered.asm.mixin.injection.Inject; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 12 | 13 | import net.minecraft.util.crash.CrashReport; 14 | 15 | import com.chocohead.cc.SMAPper; 16 | 17 | @Mixin(CrashReport.class) 18 | abstract class CrashReportMixin { 19 | @Shadow 20 | private @Final Throwable cause; 21 | 22 | @Inject(method = "", 23 | at = @At(value = "FIELD", target = "Lnet/minecraft/util/crash/CrashReport;cause:Ljava/lang/Throwable;", opcode = Opcodes.PUTFIELD, shift = Shift.AFTER)) 24 | private void fixCause(CallbackInfo call) { 25 | SMAPper.apply(cause, "java.", "sun.", "net.fabricmc.loader.", "com.mojang.authlib."); 26 | } 27 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/mixin/CrashReportSectionMixin.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc.mixin; 2 | 3 | import org.spongepowered.asm.mixin.Mixin; 4 | import org.spongepowered.asm.mixin.Shadow; 5 | import org.spongepowered.asm.mixin.injection.At; 6 | import org.spongepowered.asm.mixin.injection.Inject; 7 | import org.spongepowered.asm.mixin.injection.At.Shift; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 9 | 10 | import net.minecraft.util.crash.CrashReportSection; 11 | 12 | import com.chocohead.cc.SMAPper; 13 | 14 | @Mixin(CrashReportSection.class) 15 | abstract class CrashReportSectionMixin { 16 | @Shadow 17 | private StackTraceElement[] stackTrace; 18 | 19 | @Inject(method = "initStackTrace", 20 | at = @At(value = "INVOKE", target = "Ljava/lang/System;arraycopy(Ljava/lang/Object;ILjava/lang/Object;II)V", remap = false, shift = Shift.AFTER)) 21 | private void fixCause(CallbackInfoReturnable call) { 22 | SMAPper.apply(stackTrace, "java.", "sun.", "net.fabricmc.loader.", "com.mojang.authlib."); 23 | } 24 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/smap/FileInfo.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc.smap; 2 | 3 | public class FileInfo { 4 | public final String name, path; 5 | 6 | public FileInfo(String name, String path) { 7 | this.name = name; 8 | this.path = path; 9 | } 10 | 11 | public String getBestName() { 12 | return getBestName("?"); 13 | } 14 | 15 | public String getBestName(String finalOption) { 16 | return path != null ? path : name != null && !"null".equals(name) ? name : finalOption; 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return "File[" + name + (path != null ? ", " + path : "") + ']'; 22 | } 23 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/smap/InvalidFormat.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc.smap; 2 | 3 | import java.io.IOException; 4 | 5 | public class InvalidFormat extends IOException { 6 | private static final long serialVersionUID = -2212135394479100394L; 7 | 8 | InvalidFormat(String message) { 9 | super(message); 10 | } 11 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/smap/LineInfo.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc.smap; 2 | 3 | public class LineInfo { 4 | public final FileInfo file; 5 | public final int line; 6 | 7 | public LineInfo(FileInfo file, int line) { 8 | this.file = file; 9 | this.line = line; 10 | } 11 | 12 | @Override 13 | public String toString() { 14 | return "Line[" + line + ']'; 15 | } 16 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/smap/SMAP.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc.smap; 2 | 3 | import java.io.EOFException; 4 | import java.io.IOException; 5 | import java.io.Reader; 6 | import java.io.StringReader; 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import java.util.Collections; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Set; 14 | 15 | public class SMAP { 16 | public final String generatedFileName; 17 | private final String defaultStratum; 18 | private final Map stratums = new HashMap<>(); 19 | private final Map> vendorSections = new HashMap<>(); 20 | 21 | public static SMAP forResolved(Reader in) throws IOException { 22 | return new SMAP(in); 23 | } 24 | 25 | public static SMAP forResolved(String in) { 26 | try { 27 | return new SMAP(new StringReader(in)); 28 | } catch (InvalidFormat | EOFException e) { 29 | throw new IllegalArgumentException(e.getMessage(), e); 30 | } catch (IOException e) { 31 | throw new AssertionError("Impossible?", e); 32 | } 33 | } 34 | 35 | private SMAP(Reader in) throws IOException { 36 | try (SMAPReader reader = new SMAPReader(in)) { 37 | String first = reader.nextLine(); 38 | if (!"SMAP".equals(first)) { 39 | throw new InvalidFormat("First line expected SMAP but had " + first); 40 | } 41 | 42 | generatedFileName = reader.nextString(); //Should match source attribute 43 | defaultStratum = reader.nextString(); //Must be specified (i.e. not blank) 44 | if (defaultStratum.isEmpty()) throw new InvalidFormat("Expected resolved SMAP but default stratum is unspecified"); 45 | 46 | StratumBuilder stratum = null; 47 | boolean seenFile = false; 48 | boolean seenLines = false; 49 | out: while (true) { 50 | switch (reader.chompHeader()) { 51 | case 'S': {//Stratum 52 | String id = reader.chompString(); 53 | if ("Java".equals(id)) throw new UnsupportedOperationException("Resolved SMAPs should not have the final-source stratum"); 54 | 55 | if (stratum != null) { 56 | stratums.put(stratum.id, stratum.validate(seenFile, seenLines)); 57 | seenFile = seenLines = false; 58 | } 59 | 60 | stratum = new StratumBuilder(id); 61 | reader.ensureLineComplete(); 62 | break; 63 | } 64 | 65 | case 'F': //File 66 | if (stratum == null) throw new InvalidFormat("File section declared before stratum"); 67 | if (seenFile) throw new InvalidFormat("Duplicate file sections declared for " + stratum.id + " stratum"); 68 | seenFile = true; 69 | 70 | reader.ensureLineComplete(); 71 | for (String line = reader.peekLine(); line.isEmpty() || line.charAt(0) != '*'; line = reader.peekLine()) { 72 | boolean hasPath = reader.expect('+'); 73 | 74 | int id = reader.chompNumber(); 75 | String name = reader.chompString(); 76 | reader.ensureLineComplete(); 77 | String path = hasPath ? reader.nextLine() : null; 78 | 79 | stratum.addFile(id, name, path); 80 | reader.ensureLineComplete(); 81 | } 82 | break; 83 | 84 | case 'L': //Line 85 | if (stratum == null) throw new InvalidFormat("Line section declared before stratum"); 86 | if (seenLines) throw new InvalidFormat("Duplicate line sections declared for " + stratum.id + " stratum"); 87 | seenLines = true; 88 | 89 | reader.ensureLineComplete(); 90 | for (String line = reader.peekLine(); line.isEmpty() || line.charAt(0) != '*'; line = reader.peekLine()) { 91 | int input = reader.chompNumber(); 92 | 93 | int file; 94 | if (reader.expect('#')) { 95 | file = reader.chompNumber(); 96 | } else { 97 | file = -1; 98 | } 99 | 100 | int repeat; 101 | if (reader.expect(',')) { 102 | repeat = reader.chompNumber(); 103 | } else { 104 | repeat = 1; 105 | } 106 | 107 | if (!reader.expect(':')) { 108 | throw new InvalidFormat("Expected : but found " + line); 109 | } 110 | 111 | int output = reader.chompNumber(); 112 | 113 | int increment; 114 | if (reader.expectIfMore(',')) { 115 | increment = reader.chompNumber(); 116 | } else { 117 | increment = 1; 118 | } 119 | 120 | stratum.addLines(input, file, repeat, output, increment); 121 | reader.ensureLineComplete(); 122 | } 123 | break; 124 | 125 | case 'O': //Open embedded 126 | case 'C': //Close embedded 127 | throw new UnsupportedOperationException("Embedded SMAP found, expected resolved SMAP"); 128 | 129 | case 'V': {//Vendor 130 | reader.ensureLineComplete(); 131 | String id = reader.nextString(); 132 | List vendorInfo = new ArrayList<>(); 133 | 134 | for (String info = reader.peekLine(); info.isEmpty() || info.charAt(0) != '*'; info = reader.peekLine()) { 135 | vendorInfo.add(reader.nextLine()); 136 | } 137 | 138 | vendorSections.put(id, vendorInfo.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(vendorInfo)); 139 | break; 140 | } 141 | 142 | case 'E': //End 143 | if (stratum != null) { 144 | stratums.put(stratum.id, stratum.validate(seenFile, seenLines)); 145 | } else { 146 | throw new InvalidFormat("No stratum found before end section"); 147 | } 148 | 149 | reader.ensureLineComplete(); 150 | reader.ensureComplete(); 151 | break out; 152 | 153 | default: //Future 154 | reader.ensureLineComplete(); 155 | 156 | for (String futureInfo = reader.peekLine(); futureInfo.isEmpty() || futureInfo.charAt(0) != '*'; futureInfo = reader.peekLine()) { 157 | reader.nextLine(); //Flush the last peeked line out 158 | } 159 | 160 | break; //Any unknown sections must be ignored without error 161 | } 162 | } 163 | } 164 | } 165 | 166 | public Stratum getDefaultStratum() { 167 | return getStratum(defaultStratum); 168 | } 169 | 170 | public Stratum getStratum(String id) { 171 | return stratums.get(id); 172 | } 173 | 174 | public Collection getStratums() { 175 | return stratums.values(); 176 | } 177 | 178 | public Set getVendors() { 179 | return vendorSections.keySet(); 180 | } 181 | 182 | public List getVendorInfo(String id) { 183 | return vendorSections.get(id); 184 | } 185 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/smap/SMAPReader.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc.smap; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.Closeable; 5 | import java.io.EOFException; 6 | import java.io.IOException; 7 | import java.io.Reader; 8 | 9 | class SMAPReader implements Closeable { 10 | private final BufferedReader in; 11 | private boolean hasPeeked; 12 | private String linePeek; 13 | 14 | public SMAPReader(Reader in) { 15 | this.in = new BufferedReader(in); 16 | } 17 | 18 | public String peekLine() throws IOException { 19 | if (!hasPeeked) { 20 | linePeek = in.readLine(); 21 | hasPeeked = true; 22 | } 23 | 24 | return linePeek; 25 | } 26 | 27 | public String nextLine() throws IOException { 28 | if (hasPeeked) { 29 | hasPeeked = false; 30 | return linePeek; 31 | } 32 | 33 | return in.readLine(); 34 | } 35 | 36 | private static String trimStart(String value) { 37 | //return CharMatcher.whitespace().trimLeadingFrom(value); 38 | return trimFrom(value, 0); 39 | } 40 | 41 | private static String trimFrom(String value, int start) { 42 | for (int end = value.length(); start < end; start++) { 43 | if (!Character.isWhitespace(value.charAt(start))) { 44 | break; 45 | } 46 | } 47 | 48 | return start > 0 ? value.substring(start) : value; 49 | } 50 | 51 | public char chompHeader() throws IOException { 52 | String line = peekLine(); 53 | if (line == null) throw new EOFException("Expected header but found EOF"); 54 | 55 | char out; 56 | if (line.length() < 2 || line.charAt(0) != '*' || (out = line.charAt(1)) == ' ' || out == '\t') { 57 | throw new InvalidFormat("Expected header but found: " + line); 58 | } 59 | 60 | linePeek = trimFrom(linePeek, 2); 61 | hasPeeked = !linePeek.isEmpty(); 62 | return out; 63 | } 64 | 65 | public String nextString() throws IOException { 66 | return makeString(nextLine()); 67 | } 68 | 69 | public String chompString() throws IOException { 70 | try { 71 | return makeString(peekLine()); 72 | } finally { 73 | hasPeeked = false; //All gone 74 | } 75 | } 76 | 77 | private String makeString(String line) throws IOException { 78 | if (line == null) throw new EOFException("Expected non-asterisk string but found EOF"); 79 | 80 | line = trimStart(line); 81 | if (line.isEmpty() || line.charAt(0) == '*') { 82 | throw new InvalidFormat("Expected non-asterisk string but found: " + line); 83 | } 84 | 85 | return line; 86 | } 87 | 88 | public int nextNumber() throws IOException { 89 | String line = nextLine(); 90 | if (line == null) throw new EOFException("Expected number but found EOF"); 91 | 92 | line = line.trim(); 93 | if (!line.isEmpty() && line.charAt(0) != '+') { 94 | try { 95 | return Integer.parseUnsignedInt(line); 96 | } catch (NumberFormatException e) { 97 | //Fall through 98 | } 99 | } 100 | 101 | throw new InvalidFormat("Expected number but found: " + line); 102 | } 103 | 104 | public int chompNumber() throws IOException { 105 | String line = peekLine(); 106 | if (line == null) throw new EOFException("Expected number but found EOF"); 107 | 108 | int start = 0; 109 | for (int end = line.length(); start < end; start++) { 110 | if (!Character.isWhitespace(line.charAt(start))) { 111 | break; 112 | } 113 | } 114 | 115 | int to = start; 116 | for (int end = line.length(); to < end; to++) { 117 | if (!Character.isDigit(line.charAt(to))) { 118 | break; 119 | } 120 | } 121 | 122 | if (to > start) { 123 | try { 124 | int out = Integer.parseUnsignedInt(line.substring(start, to)); 125 | 126 | linePeek = trimFrom(linePeek, to); 127 | hasPeeked = !linePeek.isEmpty(); 128 | 129 | return out; 130 | } catch (NumberFormatException e) { 131 | //Fall through 132 | } 133 | } 134 | 135 | throw new InvalidFormat("Expected number but found: " + line); 136 | } 137 | 138 | public String chompTo(char expectedChar) throws IOException { 139 | String line = peekLine(); 140 | if (line == null) throw new EOFException("Expected line but found EOF"); 141 | 142 | for (int limit = 0, end = line.length(); limit < end; limit++) { 143 | if (line.charAt(limit) == expectedChar) { 144 | if (++limit == end) { 145 | hasPeeked = false; //Nothing left 146 | } else { 147 | linePeek = trimFrom(line, limit); 148 | hasPeeked = !linePeek.isEmpty(); 149 | } 150 | 151 | return line.substring(0, limit); 152 | } 153 | } 154 | 155 | throw new InvalidFormat("Expected " + expectedChar + " but found: " + line); 156 | } 157 | 158 | public boolean expectIfMore(char expectedChar) throws IOException { 159 | return hasPeeked && expect(expectedChar); 160 | } 161 | 162 | public boolean expect(char expectedChar) throws IOException { 163 | String line = peekLine(); 164 | if (line == null) throw new EOFException("Expected line but found EOF"); 165 | 166 | if (!line.isEmpty() && line.charAt(0) == expectedChar) { 167 | linePeek = line.substring(1); 168 | hasPeeked = !linePeek.isEmpty(); 169 | return true; 170 | } else { 171 | return false; 172 | } 173 | } 174 | 175 | public void skip(int chars) throws IOException { 176 | String line = peekLine(); 177 | if (line == null) throw new EOFException("Expected line but found EOF"); 178 | 179 | if (line.length() < chars) { 180 | throw new InvalidFormat("Expected at least " + chars + " characters but found: " + line); 181 | } 182 | 183 | linePeek = linePeek.substring(chars); 184 | hasPeeked = !linePeek.isEmpty(); 185 | } 186 | 187 | public void ensureLineComplete() throws IOException { 188 | if (hasPeeked) {//If there is still peeked line left it wasn't fully read 189 | throw new InvalidFormat("Expected CR but found: " + linePeek); 190 | } 191 | } 192 | 193 | public void ensureComplete() throws IOException { 194 | String line = in.readLine(); 195 | 196 | if (line != null) { 197 | throw new InvalidFormat("Expected EOF but found: " + line); 198 | } 199 | } 200 | 201 | @Override 202 | public void close() throws IOException { 203 | in.close(); 204 | } 205 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/smap/Stratum.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc.smap; 2 | 3 | import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; 4 | import it.unimi.dsi.fastutil.ints.IntSet; 5 | 6 | public class Stratum { 7 | public final String id; 8 | //private final Int2ReferenceMap files; 9 | private final Int2ReferenceMap lineMapping; 10 | 11 | Stratum(String id, Int2ReferenceMap lineMapping) { 12 | this.id = id; 13 | this.lineMapping = lineMapping; 14 | } 15 | 16 | /*public FileInfo getFile(int id) { 17 | return files.get(id); 18 | } 19 | 20 | public Int2ReferenceMap getFiles() { 21 | return Int2ReferenceMaps.unmodifiable(files); 22 | }*/ 23 | 24 | public boolean hasLine(int line) { 25 | return lineMapping.containsKey(line); 26 | } 27 | 28 | public LineInfo mapLine(int line) { 29 | return lineMapping.get(line); 30 | } 31 | 32 | public IntSet mappedLines() { 33 | return lineMapping.keySet(); 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return "Stratum[" + id + ']'; 39 | } 40 | } -------------------------------------------------------------------------------- /src/com/chocohead/cc/smap/StratumBuilder.java: -------------------------------------------------------------------------------- 1 | package com.chocohead.cc.smap; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import it.unimi.dsi.fastutil.ints.Int2ReferenceLinkedOpenHashMap; 8 | import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; 9 | import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; 10 | import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap; 11 | 12 | class StratumBuilder { 13 | private interface DelayedLine { 14 | void append(Int2ReferenceMap lineMapping) throws IOException; 15 | } 16 | public final String id; 17 | private final Int2ReferenceSortedMap files = new Int2ReferenceLinkedOpenHashMap<>(); 18 | private final List lines = new ArrayList<>(); 19 | 20 | public StratumBuilder(String id) { 21 | this.id = id; 22 | } 23 | 24 | void addFile(int id, String name, String path) throws IOException { 25 | if (files.put(id, new FileInfo(name, path)) != null) { 26 | throw new InvalidFormat("Duplicate file ID " + id + " for " + this.id + " stratum"); 27 | } 28 | } 29 | 30 | void addLines(int input, int inputFile, int repeat, int output, int increment) { 31 | lines.add(lineMapping -> { 32 | FileInfo file = files.get(inputFile < 0 ? files.lastIntKey() : inputFile); 33 | if (file == null) throw new InvalidFormat("Line mapping used unknown file ID: " + inputFile + " for " + id + " stratum"); 34 | 35 | for (int n = 0; n < repeat; n++) { 36 | LineInfo info = new LineInfo(file, input + n); 37 | 38 | //input + n => [output + (n * increment), output + ((n + 1) * increment) - 1] 39 | for (int line = output + (n * increment), end = line + increment; line < end; line++) { 40 | if (!lineMapping.containsKey(line)) { 41 | lineMapping.put(line, info); 42 | } 43 | } 44 | } 45 | }); 46 | } 47 | 48 | Stratum validate(boolean seenFile, boolean seenLines) throws IOException { 49 | if (!seenFile || !seenLines) { 50 | String missed; 51 | if (!seenFile) { 52 | if (!seenLines) { 53 | missed = "file and line sections"; 54 | } else { 55 | missed = "file section"; 56 | } 57 | } else { 58 | assert !seenLines; 59 | missed = "line section"; 60 | } 61 | 62 | throw new InvalidFormat("Missed " + missed + " for " + id + " stratum"); 63 | } 64 | 65 | Int2ReferenceMap lineMapping = new Int2ReferenceOpenHashMap<>(); 66 | 67 | for (DelayedLine delayedLine : lines) { 68 | delayedLine.append(lineMapping); 69 | } 70 | 71 | return new Stratum(id, lineMapping); 72 | } 73 | } --------------------------------------------------------------------------------