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