diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60545ba7..ad172178 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- Added CSRG writer
- Added TSRG and TSRG2 writer
+- Added JAM reader and writer
- Added Recaf Simple reader and writer
- Added `MappingFormat#hasWriter` boolean
diff --git a/src/main/java/net/fabricmc/mappingio/MappingReader.java b/src/main/java/net/fabricmc/mappingio/MappingReader.java
index 5642b8f5..17d1c685 100644
--- a/src/main/java/net/fabricmc/mappingio/MappingReader.java
+++ b/src/main/java/net/fabricmc/mappingio/MappingReader.java
@@ -33,6 +33,7 @@
import net.fabricmc.mappingio.format.enigma.EnigmaFileReader;
import net.fabricmc.mappingio.format.proguard.ProGuardFileReader;
import net.fabricmc.mappingio.format.simple.RecafSimpleFileReader;
+import net.fabricmc.mappingio.format.srg.JamFileReader;
import net.fabricmc.mappingio.format.srg.SrgFileReader;
import net.fabricmc.mappingio.format.srg.TsrgFileReader;
import net.fabricmc.mappingio.format.tiny.Tiny1FileReader;
@@ -83,9 +84,14 @@ public static MappingFormat detectFormat(Reader reader) throws IOException {
return MappingFormat.ENIGMA_FILE;
case "PK:":
case "CL:":
- case "MD:":
case "FD:":
+ case "MD:":
return detectSrgOrXsrg(br);
+ case "CL ":
+ case "FD ":
+ case "MD ":
+ case "MP ":
+ return MappingFormat.JAM_FILE;
}
String headerStr = String.valueOf(buffer, 0, pos);
@@ -262,6 +268,9 @@ public static void read(Reader reader, MappingFormat format, MappingVisitor visi
case XSRG_FILE:
SrgFileReader.read(reader, visitor);
break;
+ case JAM_FILE:
+ JamFileReader.read(reader, visitor);
+ break;
case CSRG_FILE:
case TSRG_FILE:
case TSRG_2_FILE:
diff --git a/src/main/java/net/fabricmc/mappingio/MappingWriter.java b/src/main/java/net/fabricmc/mappingio/MappingWriter.java
index 0e973ee4..b5a5d3f3 100644
--- a/src/main/java/net/fabricmc/mappingio/MappingWriter.java
+++ b/src/main/java/net/fabricmc/mappingio/MappingWriter.java
@@ -29,6 +29,7 @@
import net.fabricmc.mappingio.format.enigma.EnigmaFileWriter;
import net.fabricmc.mappingio.format.proguard.ProGuardFileWriter;
import net.fabricmc.mappingio.format.simple.RecafSimpleFileWriter;
+import net.fabricmc.mappingio.format.srg.JamFileWriter;
import net.fabricmc.mappingio.format.srg.CsrgFileWriter;
import net.fabricmc.mappingio.format.srg.SrgFileWriter;
import net.fabricmc.mappingio.format.srg.TsrgFileWriter;
@@ -58,6 +59,7 @@ static MappingWriter create(Writer writer, MappingFormat format) throws IOExcept
case ENIGMA_FILE: return new EnigmaFileWriter(writer);
case SRG_FILE: return new SrgFileWriter(writer, false);
case XSRG_FILE: return new SrgFileWriter(writer, true);
+ case JAM_FILE: return new JamFileWriter(writer);
case CSRG_FILE: return new CsrgFileWriter(writer);
case TSRG_FILE: return new TsrgFileWriter(writer, false);
case TSRG_2_FILE: return new TsrgFileWriter(writer, true);
diff --git a/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java b/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java
index 73ab0a50..f7e8a5a6 100644
--- a/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java
+++ b/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java
@@ -46,8 +46,8 @@
*
✔ |
* src |
* ✔ |
- * ✔ |
- * ✔ |
+ * lvIdx & srcName |
+ * lvIdx, lvtIdx, startOpIdx & srcName |
* ✔ |
*
*
@@ -55,7 +55,7 @@
* - |
* src |
* ✔ |
- * ✔ |
+ * lvIdx |
* - |
* - |
*
@@ -78,6 +78,15 @@
* - |
*
*
+ * JAM |
+ * - |
+ * src |
+ * - |
+ * argPos |
+ * - |
+ * - |
+ *
+ *
* CSRG/TSRG |
* - |
* - |
@@ -91,7 +100,7 @@
* ✔ |
* src |
* - |
- * ✔ |
+ * lvIdx & srcName |
* - |
* - |
*
@@ -148,6 +157,11 @@ public enum MappingFormat {
*/
XSRG_FILE("XSRG file", "xsrg", false, true, false, false, false, true),
+ /**
+ * The {@code JAM} ("Java Associated Mapping"; formerly {@code SRGX}) mapping format, as specified here.
+ */
+ JAM_FILE("JAM file", "jam", false, true, false, true, false, true),
+
/**
* The {@code CSRG} ("Compact SRG", since it saves disk space over SRG) mapping format, as specified here.
*/
diff --git a/src/main/java/net/fabricmc/mappingio/format/srg/JamFileReader.java b/src/main/java/net/fabricmc/mappingio/format/srg/JamFileReader.java
new file mode 100644
index 00000000..2f9b57a4
--- /dev/null
+++ b/src/main/java/net/fabricmc/mappingio/format/srg/JamFileReader.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2023 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.mappingio.format.srg;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Collections;
+import java.util.Set;
+
+import net.fabricmc.mappingio.MappedElementKind;
+import net.fabricmc.mappingio.MappingFlag;
+import net.fabricmc.mappingio.MappingUtil;
+import net.fabricmc.mappingio.MappingVisitor;
+import net.fabricmc.mappingio.format.ColumnFileReader;
+import net.fabricmc.mappingio.format.MappingFormat;
+import net.fabricmc.mappingio.tree.MappingTree;
+import net.fabricmc.mappingio.tree.MemoryMappingTree;
+
+/**
+ * {@linkplain MappingFormat#JAM_FILE JAM file} reader.
+ *
+ * Crashes if a second visit pass is requested without
+ * {@link MappingFlag#NEEDS_MULTIPLE_PASSES} having been passed beforehand.
+ */
+public final class JamFileReader {
+ private JamFileReader() {
+ }
+
+ public static void read(Reader reader, MappingVisitor visitor) throws IOException {
+ read(reader, MappingUtil.NS_SOURCE_FALLBACK, MappingUtil.NS_TARGET_FALLBACK, visitor);
+ }
+
+ public static void read(Reader reader, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException {
+ read(new ColumnFileReader(reader, ' '), sourceNs, targetNs, visitor);
+ }
+
+ private static void read(ColumnFileReader reader, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException {
+ Set flags = visitor.getFlags();
+ MappingVisitor parentVisitor = null;
+
+ if (flags.contains(MappingFlag.NEEDS_ELEMENT_UNIQUENESS)) {
+ parentVisitor = visitor;
+ visitor = new MemoryMappingTree();
+ } else if (flags.contains(MappingFlag.NEEDS_MULTIPLE_PASSES)) {
+ reader.mark();
+ }
+
+ for (;;) {
+ if (visitor.visitHeader()) {
+ visitor.visitNamespaces(sourceNs, Collections.singletonList(targetNs));
+ }
+
+ if (visitor.visitContent()) {
+ String lastClass = null;
+ boolean visitLastClass = false;
+
+ do {
+ boolean isMethod;
+ boolean isArg = false;
+
+ if (reader.nextCol("CL")) { // class: CL
+ String srcName = reader.nextCol();
+ if (srcName == null || srcName.isEmpty()) throw new IOException("missing class-name-a in line "+reader.getLineNumber());
+
+ if (!srcName.equals(lastClass)) {
+ lastClass = srcName;
+ visitLastClass = visitor.visitClass(srcName);
+
+ if (visitLastClass) {
+ String dstName = reader.nextCol();
+ if (dstName == null || dstName.isEmpty()) throw new IOException("missing class-name-b in line "+reader.getLineNumber());
+
+ visitor.visitDstName(MappedElementKind.CLASS, 0, dstName);
+ visitLastClass = visitor.visitElementContent(MappedElementKind.CLASS);
+ }
+ }
+ } else if ((isMethod = reader.nextCol("MD")) || reader.nextCol("FD") // method/field: MD/FD
+ || (isArg = reader.nextCol("MP"))) { // parameter: MP []
+ String clsSrcClsName = reader.nextCol();
+ if (clsSrcClsName == null) throw new IOException("missing class-name-a in line "+reader.getLineNumber());
+
+ String memberSrcName = reader.nextCol();
+ if (memberSrcName == null || memberSrcName.isEmpty()) throw new IOException("missing member-name-a in line "+reader.getLineNumber());
+
+ String memberSrcDesc = reader.nextCol();
+ if (memberSrcDesc == null || memberSrcDesc.isEmpty()) throw new IOException("missing member-desc-a in line "+reader.getLineNumber());
+
+ String col5 = reader.nextCol();
+ String col6 = reader.nextCol();
+ String col7 = reader.nextCol();
+
+ int argSrcPos = -1;
+ String dstName;
+ String argSrcDesc;
+
+ if (!isArg) {
+ dstName = col5;
+ } else {
+ argSrcPos = Integer.parseInt(col5);
+
+ if (col7 == null || col7.isEmpty()) {
+ dstName = col6;
+ } else {
+ argSrcDesc = col6;
+ if (argSrcDesc == null || argSrcDesc.isEmpty()) throw new IOException("missing parameter-desc-a in line "+reader.getLineNumber());
+
+ dstName = col7;
+ }
+ }
+
+ if (dstName == null || dstName.isEmpty()) throw new IOException("missing name-b in line "+reader.getLineNumber());
+
+ if (!clsSrcClsName.equals(lastClass)) {
+ lastClass = clsSrcClsName;
+ visitLastClass = visitor.visitClass(clsSrcClsName);
+
+ if (visitLastClass) {
+ visitLastClass = visitor.visitElementContent(MappedElementKind.CLASS);
+ }
+ }
+
+ if (!visitLastClass) continue;
+ boolean visitMethod = false;
+
+ if (isMethod || isArg) {
+ visitMethod = visitor.visitMethod(memberSrcName, memberSrcDesc);
+ }
+
+ if (visitMethod) {
+ if (isMethod) {
+ visitor.visitDstName(MappedElementKind.METHOD, 0, dstName);
+ visitor.visitElementContent(MappedElementKind.METHOD);
+ } else {
+ visitor.visitMethodArg(argSrcPos, -1, null);
+ visitor.visitDstName(MappedElementKind.METHOD_ARG, 0, dstName);
+ visitor.visitElementContent(MappedElementKind.METHOD_ARG);
+ }
+ } else if (!isMethod && !isArg && visitor.visitField(memberSrcName, memberSrcDesc)) {
+ visitor.visitDstName(MappedElementKind.FIELD, 0, dstName);
+ visitor.visitElementContent(MappedElementKind.FIELD);
+ }
+ }
+ } while (reader.nextLine(0));
+ }
+
+ if (visitor.visitEnd()) break;
+
+ reader.reset();
+ }
+
+ if (parentVisitor != null) {
+ ((MappingTree) visitor).accept(parentVisitor);
+ }
+ }
+}
diff --git a/src/main/java/net/fabricmc/mappingio/format/srg/JamFileWriter.java b/src/main/java/net/fabricmc/mappingio/format/srg/JamFileWriter.java
new file mode 100644
index 00000000..d9c993d9
--- /dev/null
+++ b/src/main/java/net/fabricmc/mappingio/format/srg/JamFileWriter.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (c) 2023 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.mappingio.format.srg;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.fabricmc.mappingio.MappedElementKind;
+import net.fabricmc.mappingio.MappingFlag;
+import net.fabricmc.mappingio.MappingWriter;
+
+/**
+ * {@linkplain net.fabricmc.mappingio.format.MappingFormat#JAM_FILE JAM file} writer.
+ */
+public final class JamFileWriter implements MappingWriter {
+ public JamFileWriter(Writer writer) {
+ this.writer = writer;
+ }
+
+ @Override
+ public void close() throws IOException {
+ writer.close();
+ }
+
+ @Override
+ public Set getFlags() {
+ return flags;
+ }
+
+ @Override
+ public void visitNamespaces(String srcNamespace, List dstNamespaces) throws IOException {
+ }
+
+ @Override
+ public boolean visitClass(String srcName) throws IOException {
+ classSrcName = srcName;
+ classDstName = null;
+
+ return true;
+ }
+
+ @Override
+ public boolean visitField(String srcName, @Nullable String srcDesc) throws IOException {
+ memberSrcName = srcName;
+ memberSrcDesc = srcDesc;
+ memberDstName = null;
+
+ return true;
+ }
+
+ @Override
+ public boolean visitMethod(String srcName, @Nullable String srcDesc) throws IOException {
+ memberSrcName = srcName;
+ memberSrcDesc = srcDesc;
+ memberDstName = null;
+
+ return true;
+ }
+
+ @Override
+ public boolean visitMethodArg(int argPosition, int lvIndex, @Nullable String srcName) throws IOException {
+ argSrcPosition = argPosition;
+ argDstName = null;
+
+ return true;
+ }
+
+ @Override
+ public boolean visitMethodVar(int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, @Nullable String srcName) throws IOException {
+ return false; // not supported, skip
+ }
+
+ @Override
+ public void visitDstName(MappedElementKind targetKind, int namespace, String name) {
+ if (namespace != 0) return;
+
+ switch (targetKind) {
+ case CLASS:
+ classDstName = name;
+ break;
+ case FIELD:
+ case METHOD:
+ memberDstName = name;
+ break;
+ case METHOD_ARG:
+ argDstName = name;
+ break;
+ default:
+ throw new IllegalStateException("unexpected invocation for "+targetKind);
+ }
+ }
+
+ @Override
+ public boolean visitElementContent(MappedElementKind targetKind) throws IOException {
+ boolean isClass = targetKind == MappedElementKind.CLASS;
+ boolean isMethod = false;
+ boolean isArg = false;
+
+ if (isClass) {
+ if (!classOnlyPass || classDstName == null) {
+ return true;
+ }
+
+ write("CL ");
+ } else if (targetKind == MappedElementKind.FIELD
+ || (isMethod = targetKind == MappedElementKind.METHOD)
+ || (isArg = targetKind == MappedElementKind.METHOD_ARG)) {
+ if (classOnlyPass || memberSrcDesc == null || memberDstName == null) {
+ return isMethod;
+ }
+
+ if (isMethod) {
+ write("MD ");
+ } else if (!isArg) {
+ write("FD ");
+ } else {
+ if (argSrcPosition == -1 || argDstName == null) return false;
+ write("MP ");
+ }
+ } else {
+ throw new IllegalStateException("unexpected invocation for "+targetKind);
+ }
+
+ write(classSrcName);
+ writeSpace();
+
+ if (isClass) {
+ write(classDstName);
+ } else {
+ write(memberSrcName);
+ writeSpace();
+
+ write(memberSrcDesc);
+ writeSpace();
+
+ if (!isArg) {
+ write(memberDstName);
+ } else {
+ write(Integer.toString(argSrcPosition));
+ writeSpace();
+ write(argDstName);
+ }
+ }
+
+ writeLn();
+
+ return isClass || isMethod;
+ }
+
+ @Override
+ public void visitComment(MappedElementKind targetKind, String comment) throws IOException {
+ // not supported, skip
+ }
+
+ @Override
+ public boolean visitEnd() throws IOException {
+ if (classOnlyPass) {
+ classOnlyPass = false;
+ return false;
+ }
+
+ classOnlyPass = true;
+ return MappingWriter.super.visitEnd();
+ }
+
+ private void write(String str) throws IOException {
+ writer.write(str);
+ }
+
+ private void writeSpace() throws IOException {
+ writer.write(' ');
+ }
+
+ private void writeLn() throws IOException {
+ writer.write('\n');
+ }
+
+ private static final Set flags = EnumSet.of(
+ MappingFlag.NEEDS_SRC_FIELD_DESC,
+ MappingFlag.NEEDS_SRC_METHOD_DESC,
+ MappingFlag.NEEDS_MULTIPLE_PASSES);
+
+ private final Writer writer;
+ private boolean classOnlyPass = true;
+ private String classSrcName;
+ private String memberSrcName;
+ private String memberSrcDesc;
+ private int argSrcPosition = -1;
+ private String classDstName;
+ private String memberDstName;
+ private String argDstName;
+}
diff --git a/src/main/java/net/fabricmc/mappingio/format/srg/SrgFileWriter.java b/src/main/java/net/fabricmc/mappingio/format/srg/SrgFileWriter.java
index f6379a7d..b74db414 100644
--- a/src/main/java/net/fabricmc/mappingio/format/srg/SrgFileWriter.java
+++ b/src/main/java/net/fabricmc/mappingio/format/srg/SrgFileWriter.java
@@ -103,7 +103,7 @@ public void visitDstName(MappedElementKind targetKind, int namespace, String nam
memberDstName = name;
break;
default:
- break;
+ throw new IllegalStateException("unexpected invocation for "+targetKind);
}
}
@@ -122,11 +122,11 @@ public boolean visitElementContent(MappedElementKind targetKind) throws IOExcept
write("CL: ");
break;
case FIELD:
- if (memberDstName == null) return false;
+ if (memberSrcDesc == null || memberDstName == null || (xsrg && memberDstDesc == null)) return false;
write("FD: ");
break;
case METHOD:
- if (memberDstName == null || memberDstDesc == null) return false;
+ if (memberSrcDesc == null || memberDstName == null || memberDstDesc == null) return false;
write("MD: ");
break;
default:
diff --git a/src/test/java/net/fabricmc/mappingio/TestHelper.java b/src/test/java/net/fabricmc/mappingio/TestHelper.java
index 2d5cc115..1157d705 100644
--- a/src/test/java/net/fabricmc/mappingio/TestHelper.java
+++ b/src/test/java/net/fabricmc/mappingio/TestHelper.java
@@ -53,6 +53,8 @@ public static String getFileName(MappingFormat format) {
return "srg.srg";
case XSRG_FILE:
return "xsrg.xsrg";
+ case JAM_FILE:
+ return "jam.jam";
case CSRG_FILE:
return "csrg.csrg";
case TSRG_FILE:
diff --git a/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java b/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java
index 88ded7a7..ad727170 100644
--- a/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java
+++ b/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java
@@ -72,6 +72,12 @@ public void xrgFile() throws Exception {
check(format);
}
+ @Test
+ public void jamFile() throws Exception {
+ MappingFormat format = MappingFormat.JAM_FILE;
+ check(format);
+ }
+
@Test
public void csrgFile() throws Exception {
MappingFormat format = MappingFormat.CSRG_FILE;
diff --git a/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java b/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java
index ba8807d1..6aea7581 100644
--- a/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java
+++ b/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java
@@ -26,6 +26,7 @@
import net.fabricmc.mappingio.format.enigma.EnigmaFileReader;
import net.fabricmc.mappingio.format.proguard.ProGuardFileReader;
import net.fabricmc.mappingio.format.simple.RecafSimpleFileReader;
+import net.fabricmc.mappingio.format.srg.JamFileReader;
import net.fabricmc.mappingio.format.srg.SrgFileReader;
import net.fabricmc.mappingio.format.srg.TsrgFileReader;
import net.fabricmc.mappingio.format.tiny.Tiny1FileReader;
@@ -61,6 +62,11 @@ public void emptySrgFile() throws Exception {
SrgFileReader.read(new StringReader(""), tree);
}
+ @Test
+ public void emptyJamFile() throws Exception {
+ JamFileReader.read(new StringReader(""), tree);
+ }
+
@Test
public void emptyTsrgFile() throws Exception {
TsrgFileReader.read(new StringReader(""), tree);
diff --git a/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java b/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java
index 329279f0..4aa5670d 100644
--- a/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java
+++ b/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java
@@ -81,6 +81,13 @@ public void xsrgFile() throws Exception {
checkHoles(format);
}
+ @Test
+ public void jamFile() throws Exception {
+ MappingFormat format = MappingFormat.JAM_FILE;
+ checkDefault(format);
+ checkHoles(format);
+ }
+
@Test
public void csrgFile() throws Exception {
MappingFormat format = MappingFormat.CSRG_FILE;
diff --git a/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java b/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java
index e2db8117..58a8ab96 100644
--- a/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java
+++ b/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java
@@ -76,6 +76,12 @@ public void xrgFile() throws Exception {
check(format);
}
+ @Test
+ public void jamFile() throws Exception {
+ MappingFormat format = MappingFormat.JAM_FILE;
+ check(format);
+ }
+
@Test
public void csrgFile() throws Exception {
MappingFormat format = MappingFormat.CSRG_FILE;
diff --git a/src/test/java/net/fabricmc/mappingio/write/WriteTest.java b/src/test/java/net/fabricmc/mappingio/write/WriteTest.java
index 425101c6..02ab4994 100644
--- a/src/test/java/net/fabricmc/mappingio/write/WriteTest.java
+++ b/src/test/java/net/fabricmc/mappingio/write/WriteTest.java
@@ -73,6 +73,11 @@ public void xsrgFile() throws Exception {
check(MappingFormat.XSRG_FILE);
}
+ @Test
+ public void jamFile() throws Exception {
+ check(MappingFormat.JAM_FILE);
+ }
+
@Test
public void csrgFile() throws Exception {
check(MappingFormat.CSRG_FILE);
diff --git a/src/test/resources/detection/jam.jam b/src/test/resources/detection/jam.jam
new file mode 100644
index 00000000..c1c9f5e3
--- /dev/null
+++ b/src/test/resources/detection/jam.jam
@@ -0,0 +1 @@
+CL class_1 RenamedClass
diff --git a/src/test/resources/read/valid-with-holes/jam.jam b/src/test/resources/read/valid-with-holes/jam.jam
new file mode 100644
index 00000000..bd2f77fd
--- /dev/null
+++ b/src/test/resources/read/valid-with-holes/jam.jam
@@ -0,0 +1,10 @@
+CL class_2 class2Ns0Rename
+CL class_5 class5Ns0Rename
+CL class_7$class_8 class_7$class8Ns0Rename
+CL class_13$class_14 class_13$class14Ns0Rename
+CL class_17$class_18$class_19 class_17$class_18$class19Ns0Rename
+CL class_26$class_27$class_28 class_26$class_27$class28Ns0Rename
+FD class_32 field_2 I field2Ns0Rename
+FD class_32 field_5 I field5Ns0Rename
+MD class_32 method_2 ()I method2Ns0Rename
+MD class_32 method_5 ()I method5Ns0Rename
diff --git a/src/test/resources/read/valid/jam.jam b/src/test/resources/read/valid/jam.jam
new file mode 100644
index 00000000..64972fd5
--- /dev/null
+++ b/src/test/resources/read/valid/jam.jam
@@ -0,0 +1,7 @@
+CL class_1 class1Ns0Rename
+CL class_1$class_2 class1Ns0Rename$class2Ns0Rename
+CL class_3 class3Ns0Rename
+FD class_1 field_1 I field1Ns0Rename
+MD class_1 method_1 ()I method1Ns0Rename
+MP class_1 method_1 ()I 0 param1Ns0Rename
+FD class_1$class_2 field_2 I field2Ns0Rename