diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2d39ca3a..01e3ae45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+- Added JOBF reader and writer
## [0.5.1] - 2023-11-30
- Improved documentation
diff --git a/src/main/java/net/fabricmc/mappingio/MappingReader.java b/src/main/java/net/fabricmc/mappingio/MappingReader.java
index 0deae9bf..b0d5c758 100644
--- a/src/main/java/net/fabricmc/mappingio/MappingReader.java
+++ b/src/main/java/net/fabricmc/mappingio/MappingReader.java
@@ -31,6 +31,7 @@
import net.fabricmc.mappingio.format.MappingFormat;
import net.fabricmc.mappingio.format.enigma.EnigmaDirReader;
import net.fabricmc.mappingio.format.enigma.EnigmaFileReader;
+import net.fabricmc.mappingio.format.jobf.JobfFileReader;
import net.fabricmc.mappingio.format.proguard.ProGuardFileReader;
import net.fabricmc.mappingio.format.srg.SrgFileReader;
import net.fabricmc.mappingio.format.srg.TsrgFileReader;
@@ -89,7 +90,13 @@ public static MappingFormat detectFormat(Reader reader) throws IOException {
String headerStr = String.valueOf(buffer, 0, pos);
- if (headerStr.contains(" -> ")) {
+ if ((headerStr.startsWith("p ")
+ || headerStr.startsWith("c ")
+ || headerStr.startsWith("f ")
+ || headerStr.startsWith("m "))
+ && headerStr.contains(" = ")) {
+ return MappingFormat.JOBF_FILE;
+ } else if (headerStr.contains(" -> ")) {
return MappingFormat.PROGUARD_FILE;
} else if (headerStr.contains("\n\t")) {
return MappingFormat.TSRG_FILE;
@@ -269,6 +276,9 @@ public static void read(Reader reader, MappingFormat format, MappingVisitor visi
case PROGUARD_FILE:
ProGuardFileReader.read(reader, visitor);
break;
+ case JOBF_FILE:
+ JobfFileReader.read(reader, visitor);
+ break;
default:
throw new IllegalStateException();
}
diff --git a/src/main/java/net/fabricmc/mappingio/MappingWriter.java b/src/main/java/net/fabricmc/mappingio/MappingWriter.java
index 1cbd99e3..fe429f39 100644
--- a/src/main/java/net/fabricmc/mappingio/MappingWriter.java
+++ b/src/main/java/net/fabricmc/mappingio/MappingWriter.java
@@ -27,6 +27,7 @@
import net.fabricmc.mappingio.format.MappingFormat;
import net.fabricmc.mappingio.format.enigma.EnigmaDirWriter;
import net.fabricmc.mappingio.format.enigma.EnigmaFileWriter;
+import net.fabricmc.mappingio.format.jobf.JobfFileWriter;
import net.fabricmc.mappingio.format.proguard.ProGuardFileWriter;
import net.fabricmc.mappingio.format.srg.SrgFileWriter;
import net.fabricmc.mappingio.format.tiny.Tiny1FileWriter;
@@ -56,6 +57,7 @@ static MappingWriter create(Writer writer, MappingFormat format) throws IOExcept
case SRG_FILE: return new SrgFileWriter(writer, false);
case XSRG_FILE: return new SrgFileWriter(writer, true);
case PROGUARD_FILE: return new ProGuardFileWriter(writer);
+ case JOBF_FILE: return new JobfFileWriter(writer);
default: return null;
}
}
diff --git a/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java b/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java
index c81e5249..ac954d2e 100644
--- a/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java
+++ b/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java
@@ -102,6 +102,15 @@
*
- |
* - |
*
+ *
+ * JOBF |
+ * - |
+ * src |
+ * - |
+ * - |
+ * - |
+ * - |
+ *
*
*/
// Format order is determined by importance to Fabric tooling, format family and release order therein.
@@ -156,7 +165,12 @@ public enum MappingFormat {
/**
* ProGuard's mapping format, as specified here.
*/
- PROGUARD_FILE("ProGuard file", "txt", false, true, false, false, false);
+ PROGUARD_FILE("ProGuard file", "txt", false, true, false, false, false),
+
+ /**
+ * The {@code JOBF} mapping format, as specified here.
+ */
+ JOBF_FILE("JOBF file", "jobf", false, true, false, false, false);
MappingFormat(String name, @Nullable String fileExt,
boolean hasNamespaces, boolean hasFieldDescriptors,
diff --git a/src/main/java/net/fabricmc/mappingio/format/jobf/JobfFileReader.java b/src/main/java/net/fabricmc/mappingio/format/jobf/JobfFileReader.java
new file mode 100644
index 00000000..a0e5a2e5
--- /dev/null
+++ b/src/main/java/net/fabricmc/mappingio/format/jobf/JobfFileReader.java
@@ -0,0 +1,148 @@
+/*
+ * 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.jobf;
+
+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#JOBF_FILE JOBF file} reader.
+ */
+public class JobfFileReader {
+ 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 (;;) {
+ boolean visitHeader = visitor.visitHeader();
+
+ if (visitHeader) {
+ visitor.visitNamespaces(sourceNs, Collections.singletonList(targetNs));
+ }
+
+ if (visitor.visitContent()) {
+ String lastClass = null;
+ boolean visitLastClass = false;
+
+ do {
+ boolean isField;
+
+ if (reader.nextCol("c")) { // class: c =
+ 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) {
+ readSeparator(reader);
+
+ 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 ((isField = reader.nextCol("f")) || reader.nextCol("m")) {
+ // field: f .: =
+ // method: m . =
+ String src = reader.nextCol();
+ if (src == null || src.isEmpty()) throw new IOException("missing class/name/desc a in line "+reader.getLineNumber());
+
+ int nameSepPos = src.lastIndexOf('.');
+ if (nameSepPos <= 0 || nameSepPos == src.length() - 1) throw new IOException("invalid class/name/desc a in line "+reader.getLineNumber());
+
+ int descSepPos = src.lastIndexOf(isField ? ':' : '(');
+ if (descSepPos <= 0 || descSepPos == src.length() - 1) throw new IOException("invalid name/desc a in line "+reader.getLineNumber());
+
+ readSeparator(reader);
+
+ String dstName = reader.nextCol();
+ if (dstName == null || dstName.isEmpty()) throw new IOException("missing name-b in line "+reader.getLineNumber());
+
+ String srcOwner = src.substring(0, nameSepPos);
+
+ if (!srcOwner.equals(lastClass)) {
+ lastClass = srcOwner;
+ visitLastClass = visitor.visitClass(srcOwner);
+
+ if (visitLastClass) {
+ visitLastClass = visitor.visitElementContent(MappedElementKind.CLASS);
+ }
+ }
+
+ if (visitLastClass) {
+ String srcName = src.substring(nameSepPos + 1, descSepPos);
+ String srcDesc = src.substring(descSepPos + (isField ? 1 : 0));
+
+ if (isField && visitor.visitField(srcName, srcDesc)
+ || !isField && visitor.visitMethod(srcName, srcDesc)) {
+ MappedElementKind kind = isField ? MappedElementKind.FIELD : MappedElementKind.METHOD;
+ visitor.visitDstName(kind, 0, dstName);
+ visitor.visitElementContent(kind);
+ }
+ }
+ } else if (reader.nextCol("p")) { // package: p =
+ // TODO
+ }
+ } while (reader.nextLine(0));
+ }
+
+ if (visitor.visitEnd()) break;
+
+ reader.reset();
+ }
+
+ if (parentVisitor != null) {
+ ((MappingTree) visitor).accept(parentVisitor);
+ }
+ }
+
+ private static void readSeparator(ColumnFileReader reader) throws IOException {
+ if (!reader.nextCol("=")) {
+ throw new IOException("missing separator in line "+reader.getLineNumber()+" (expected \" = \")");
+ }
+ }
+}
diff --git a/src/main/java/net/fabricmc/mappingio/format/jobf/JobfFileWriter.java b/src/main/java/net/fabricmc/mappingio/format/jobf/JobfFileWriter.java
new file mode 100644
index 00000000..cb457818
--- /dev/null
+++ b/src/main/java/net/fabricmc/mappingio/format/jobf/JobfFileWriter.java
@@ -0,0 +1,150 @@
+/*
+ * 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.jobf;
+
+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;
+import net.fabricmc.mappingio.format.MappingFormat;
+
+/**
+ * {@linkplain MappingFormat#JOBF_FILE JOBF file} writer.
+ */
+public final class JobfFileWriter implements MappingWriter {
+ public JobfFileWriter(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;
+
+ return true;
+ }
+
+ @Override
+ public boolean visitField(String srcName, @Nullable String srcDesc) throws IOException {
+ memberSrcName = srcName;
+ memberSrcDesc = srcDesc;
+
+ return true;
+ }
+
+ @Override
+ public boolean visitMethod(String srcName, @Nullable String srcDesc) throws IOException {
+ memberSrcName = srcName;
+ memberSrcDesc = srcDesc;
+
+ return true;
+ }
+
+ @Override
+ public boolean visitMethodArg(int argPosition, int lvIndex, @Nullable String srcName) throws IOException {
+ return false; // not supported, skip
+ }
+
+ @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;
+
+ dstName = name;
+ }
+
+ @Override
+ public boolean visitElementContent(MappedElementKind targetKind) throws IOException {
+ boolean isClass = targetKind == MappedElementKind.CLASS;
+ boolean isField = false;
+
+ if (dstName == null) return isClass;
+
+ if ((isClass)) {
+ write("c ");
+ } else if ((isField = targetKind == MappedElementKind.FIELD)
+ || targetKind == MappedElementKind.METHOD) {
+ if (memberSrcDesc == null) return false;
+ write(isField ? "f " : "m ");
+ } else {
+ throw new IllegalStateException("unexpected invocation for "+targetKind);
+ }
+
+ write(classSrcName);
+
+ if (!isClass) {
+ write(".");
+ write(memberSrcName);
+
+ if (isField) write(":");
+ write(memberSrcDesc);
+ }
+
+ write(" = ");
+ write(dstName);
+
+ writeLn();
+
+ dstName = null;
+ return isClass; // only members are supported, skip anything but class contents
+ }
+
+ @Override
+ public void visitComment(MappedElementKind targetKind, String comment) throws IOException {
+ // not supported, skip
+ }
+
+ private void write(String str) throws IOException {
+ writer.write(str);
+ }
+
+ 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);
+
+ private final Writer writer;
+ private String classSrcName;
+ private String memberSrcName;
+ private String memberSrcDesc;
+ private String dstName;
+}
diff --git a/src/test/java/net/fabricmc/mappingio/TestHelper.java b/src/test/java/net/fabricmc/mappingio/TestHelper.java
index df7a5a0a..6e6c53f0 100644
--- a/src/test/java/net/fabricmc/mappingio/TestHelper.java
+++ b/src/test/java/net/fabricmc/mappingio/TestHelper.java
@@ -61,6 +61,8 @@ public static String getFileName(MappingFormat format) {
return "tsrg2.tsrg";
case PROGUARD_FILE:
return "proguard.txt";
+ case JOBF_FILE:
+ return "jobf.jobf";
default:
return null;
}
diff --git a/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java b/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java
index 69a6c706..7e6438dd 100644
--- a/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java
+++ b/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java
@@ -96,6 +96,12 @@ public void proguardFile() throws Exception {
check(format);
}
+ @Test
+ public void jobfFile() throws Exception {
+ MappingFormat format = MappingFormat.JOBF_FILE;
+ check(format);
+ }
+
private void check(MappingFormat format) throws Exception {
Path path = dir.resolve(TestHelper.getFileName(format));
assertEquals(format, MappingReader.detectFormat(path));
diff --git a/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java b/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java
index a915520e..bd3d839a 100644
--- a/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java
+++ b/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java
@@ -24,6 +24,7 @@
import org.junit.jupiter.api.Test;
import net.fabricmc.mappingio.format.enigma.EnigmaFileReader;
+import net.fabricmc.mappingio.format.jobf.JobfFileReader;
import net.fabricmc.mappingio.format.proguard.ProGuardFileReader;
import net.fabricmc.mappingio.format.srg.SrgFileReader;
import net.fabricmc.mappingio.format.srg.TsrgFileReader;
@@ -64,4 +65,9 @@ public void emptySrgFile() throws Exception {
public void emptyTsrgFile() throws Exception {
TsrgFileReader.read(new StringReader(""), tree);
}
+
+ @Test
+ public void emptyJobfFile() throws Exception {
+ JobfFileReader.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 fe1a4279..1a03e7da 100644
--- a/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java
+++ b/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java
@@ -109,6 +109,13 @@ public void proguardFile() throws Exception {
checkHoles(format);
}
+ @Test
+ public void jobfFile() throws Exception {
+ MappingFormat format = MappingFormat.JOBF_FILE;
+ checkDefault(format);
+ checkHoles(format);
+ }
+
private VisitableMappingTree checkDefault(MappingFormat format) throws Exception {
VisitableMappingTree tree = new MemoryMappingTree();
MappingReader.read(TestHelper.MappingDirs.VALID.resolve(TestHelper.getFileName(format)), format, tree);
diff --git a/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java b/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java
index e2db8117..e1b78650 100644
--- a/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java
+++ b/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java
@@ -100,6 +100,12 @@ public void proguardFile() throws Exception {
check(format);
}
+ @Test
+ public void jobfFile() throws Exception {
+ MappingFormat format = MappingFormat.JOBF_FILE;
+ check(format);
+ }
+
private void check(MappingFormat format) throws Exception {
checkDir(TestHelper.MappingDirs.DETECTION, format);
checkDir(TestHelper.MappingDirs.VALID, format);
diff --git a/src/test/java/net/fabricmc/mappingio/write/WriteTest.java b/src/test/java/net/fabricmc/mappingio/write/WriteTest.java
index 4daed966..8d1ae38e 100644
--- a/src/test/java/net/fabricmc/mappingio/write/WriteTest.java
+++ b/src/test/java/net/fabricmc/mappingio/write/WriteTest.java
@@ -78,6 +78,11 @@ public void proguardFile() throws Exception {
check(MappingFormat.PROGUARD_FILE);
}
+ @Test
+ public void jobfFile() throws Exception {
+ check(MappingFormat.JOBF_FILE);
+ }
+
private void check(MappingFormat format) throws Exception {
dogfood(validTree, dir, format);
dogfood(validWithHolesTree, dir, format);
diff --git a/src/test/resources/detection/jobf.jobf b/src/test/resources/detection/jobf.jobf
new file mode 100644
index 00000000..193f195f
--- /dev/null
+++ b/src/test/resources/detection/jobf.jobf
@@ -0,0 +1 @@
+c class_1 = RenamedClass
diff --git a/src/test/resources/read/valid-with-holes/jobf.jobf b/src/test/resources/read/valid-with-holes/jobf.jobf
new file mode 100644
index 00000000..9825b88d
--- /dev/null
+++ b/src/test/resources/read/valid-with-holes/jobf.jobf
@@ -0,0 +1,10 @@
+c class_2 = class2Ns0Rename
+c class_5 = class5Ns0Rename
+c class_7$class_8 = class_7$class8Ns0Rename
+c class_13$class_14 = class_13$class14Ns0Rename
+c class_17$class_18$class_19 = class_17$class_18$class19Ns0Rename
+c class_26$class_27$class_28 = class_26$class_27$class28Ns0Rename
+f class_32.field_2:I = field2Ns0Rename
+f class_32.field_5:I = field5Ns0Rename
+m class_32.method_2()I = method2Ns0Rename
+m class_32.method_5()I = method5Ns0Rename
diff --git a/src/test/resources/read/valid/jobf.jobf b/src/test/resources/read/valid/jobf.jobf
new file mode 100644
index 00000000..cb332365
--- /dev/null
+++ b/src/test/resources/read/valid/jobf.jobf
@@ -0,0 +1,6 @@
+c class_1 = class1Ns0Rename
+f class_1.field_1:I = field1Ns0Rename
+m class_1.method_1()I = method1Ns0Rename
+c class_1$class_2 = class1Ns0Rename$class2Ns0Rename
+f class_1$class_2.field_2:I = field2Ns0Rename
+c class_3 = class3Ns0Rename