diff --git a/CHANGELOG.md b/CHANGELOG.md index 091d1141..a2a39078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Recaf Simple reader and writer - Added `OuterClassNameInheritingVisitor` - Added `MappingFormat#hasWriter` boolean +- Added cross-format `StandardProperties` class +- Allowed Tiny v1 to save arbitrary metadata, not only intermediary counters ## [0.5.1] - 2023-11-30 - Improved documentation diff --git a/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java b/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java index 4ecf455a..327f6a00 100644 --- a/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java +++ b/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java @@ -39,7 +39,7 @@ * - * - * - - * ✔ (Currently limited support) + * ✔ * * * Tiny v2 diff --git a/src/main/java/net/fabricmc/mappingio/format/StandardProperties.java b/src/main/java/net/fabricmc/mappingio/format/StandardProperties.java new file mode 100644 index 00000000..d33b8538 --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/format/StandardProperties.java @@ -0,0 +1,109 @@ +/* + * 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; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +public final class StandardProperties { + private StandardProperties() { + } + + public static Collection values() { + return Collections.unmodifiableCollection(valuesById.values()); + } + + @Nullable + public static StandardProperty getByName(MappingFormat format, String name) { + return valuesByFormatAndName.get(new AbstractMap.SimpleEntry<>(format, name)); + } + + @Nullable + @ApiStatus.Internal + public static StandardProperty getById(String id) { + return valuesById.get(id); + } + + public static final StandardProperty NEXT_INTERMEDIARY_CLASS; + public static final StandardProperty NEXT_INTERMEDIARY_FIELD; + public static final StandardProperty NEXT_INTERMEDIARY_METHOD; + public static final StandardProperty NEXT_INTERMEDIARY_COMPONENT; + public static final StandardProperty MISSING_LVT_INDICES; + public static final StandardProperty ESCAPED_NAMES; + private static final Map, StandardProperty> valuesByFormatAndName = new HashMap<>(); + private static final Map valuesById = new HashMap<>(); + + static { + NEXT_INTERMEDIARY_CLASS = register("next-intermediary-class") + .addMapping(MappingFormat.TINY_FILE, "INTERMEDIARY_COUNTER class") + .addMapping(MappingFormat.TINY_2_FILE, "next-intermediary-class"); + NEXT_INTERMEDIARY_FIELD = register("next-intermediary-field") + .addMapping(MappingFormat.TINY_FILE, "INTERMEDIARY_COUNTER field") + .addMapping(MappingFormat.TINY_2_FILE, "next-intermediary-field"); + NEXT_INTERMEDIARY_METHOD = register("next-intermediary-method") + .addMapping(MappingFormat.TINY_FILE, "INTERMEDIARY_COUNTER method") + .addMapping(MappingFormat.TINY_2_FILE, "next-intermediary-method"); + NEXT_INTERMEDIARY_COMPONENT = register("next-intermediary-component") + .addMapping(MappingFormat.TINY_FILE, "INTERMEDIARY_COUNTER component") + .addMapping(MappingFormat.TINY_2_FILE, "next-intermediary-component"); + MISSING_LVT_INDICES = register("missing-lvt-indices") + .addMapping(MappingFormat.TINY_2_FILE, "missing-lvt-indices"); + ESCAPED_NAMES = register("escaped-names") + .addMapping(MappingFormat.TINY_2_FILE, "escaped-names"); + } + + private static StandardPropertyImpl register(String id) { + return new StandardPropertyImpl(id); + } + + private static class StandardPropertyImpl implements StandardProperty { + StandardPropertyImpl(String id) { + this.id = id; + valuesById.put(id, this); + } + + private StandardPropertyImpl addMapping(MappingFormat format, String name) { + nameByFormat.put(format, name); + valuesByFormatAndName.put(new AbstractMap.SimpleEntry<>(format, name), this); + return this; + } + + @Override + public boolean isApplicableTo(MappingFormat format) { + return nameByFormat.containsKey(format); + } + + @Override + public String getNameFor(MappingFormat format) { + return nameByFormat.get(format); + } + + @Override + public String getId() { + return id; + } + + private final String id; + private final Map nameByFormat = new HashMap<>(4); + } +} diff --git a/src/main/java/net/fabricmc/mappingio/format/StandardProperty.java b/src/main/java/net/fabricmc/mappingio/format/StandardProperty.java new file mode 100644 index 00000000..e57fd5b0 --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/format/StandardProperty.java @@ -0,0 +1,34 @@ +/* + * 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; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.NonExtendable +public interface StandardProperty { + boolean isApplicableTo(MappingFormat format); + + @Nullable + String getNameFor(MappingFormat format); + + /** + * Used internally by MappingTrees, consistency between JVM sessions or library versions isn't guaranteed! + */ + @ApiStatus.Internal + String getId(); +} diff --git a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileReader.java b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileReader.java index f74f7f7b..19ffe721 100644 --- a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileReader.java @@ -19,7 +19,9 @@ import java.io.IOException; import java.io.Reader; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import net.fabricmc.mappingio.MappedElementKind; @@ -27,6 +29,8 @@ import net.fabricmc.mappingio.MappingVisitor; import net.fabricmc.mappingio.format.ColumnFileReader; import net.fabricmc.mappingio.format.MappingFormat; +import net.fabricmc.mappingio.format.StandardProperties; +import net.fabricmc.mappingio.format.StandardProperty; import net.fabricmc.mappingio.tree.MappingTree; import net.fabricmc.mappingio.tree.MemoryMappingTree; @@ -98,11 +102,21 @@ private static void read(ColumnFileReader reader, MappingVisitor visitor) throws String lastClass = null; boolean lastClassDstNamed = false;; boolean visitLastClass = false; + Map potentialFooterMetadata = new LinkedHashMap<>(); while (reader.nextLine(0)) { - boolean isMethod; + boolean isClass = false; + boolean isMethod = false; + boolean isField = false; + + if ((isClass = reader.nextCol("CLASS")) + || (isMethod = reader.nextCol("METHOD")) + || (isField = reader.nextCol("FIELD"))) { + // Footer metadata can't be followed by a new class/method/field, so any stored data was comments + potentialFooterMetadata.clear(); + } - if (reader.nextCol("CLASS")) { // class: CLASS ... + if (isClass) { // class: CLASS ... String srcName = reader.nextCol(); if (srcName == null || srcName.isEmpty()) throw new IOException("missing class-name-a in line "+reader.getLineNumber()); @@ -116,7 +130,7 @@ private static void read(ColumnFileReader reader, MappingVisitor visitor) throws visitLastClass = visitor.visitElementContent(MappedElementKind.CLASS); } } - } else if ((isMethod = reader.nextCol("METHOD")) || reader.nextCol("FIELD")) { // method: METHOD cls-a desc-a ... or field: FIELD cls-a desc-a ... + } else if (isMethod || isField) { // method: METHOD cls-a desc-a ... or field: FIELD cls-a desc-a ... String srcOwner = reader.nextCol(); if (srcOwner == null || srcOwner.isEmpty()) throw new IOException("missing class-name-a in line "+reader.getLineNumber()); @@ -141,31 +155,37 @@ private static void read(ColumnFileReader reader, MappingVisitor visitor) throws } } else { String line = reader.nextCol(); - final String prefix = "# INTERMEDIARY-COUNTER "; - String[] parts; - - if (line.startsWith(prefix) - && (parts = line.substring(prefix.length()).split(" ")).length == 2) { - String property = null; - - switch (parts[0]) { - case "class": - property = nextIntermediaryClassProperty; - break; - case "field": - property = nextIntermediaryFieldProperty; - break; - case "method": - property = nextIntermediaryMethodProperty; - break; + + if (line.startsWith("# ") && line.length() >= 3 && line.charAt(3) != ' ') { // Potentially metadata + line = line.substring(2); + String[] parts = line.split(" "); + String value = parts[parts.length - 1]; + String key = line.substring(0, line.lastIndexOf(value) - 1); + + if (key.isEmpty()) { + String oldValue = value; + value = key; + key = oldValue; } + StandardProperty property = StandardProperties.getByName(format, key); + if (property != null) { - visitor.visitMetadata(property, parts[1]); + key = property.getId(); + } + + if (lastClass == null) { // Header metadata + visitor.visitMetadata(key, value); + } else { + potentialFooterMetadata.put(key, value); } } } } + + for (Map.Entry entry : potentialFooterMetadata.entrySet()) { + visitor.visitMetadata(entry.getKey(), entry.getValue()); + } } if (visitor.visitEnd()) break; @@ -187,7 +207,5 @@ private static void readDstNames(ColumnFileReader reader, MappedElementKind subj } } - static final String nextIntermediaryClassProperty = "next-intermediary-class"; - static final String nextIntermediaryFieldProperty = "next-intermediary-field"; - static final String nextIntermediaryMethodProperty = "next-intermediary-method"; + private static final MappingFormat format = MappingFormat.TINY_FILE; } diff --git a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileWriter.java b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileWriter.java index f419e3ef..daab46f9 100644 --- a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileWriter.java +++ b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileWriter.java @@ -29,6 +29,8 @@ import net.fabricmc.mappingio.MappingFlag; import net.fabricmc.mappingio.MappingWriter; import net.fabricmc.mappingio.format.MappingFormat; +import net.fabricmc.mappingio.format.StandardProperties; +import net.fabricmc.mappingio.format.StandardProperty; /** * {@linkplain MappingFormat#TINY_1 Tiny v1 file} writer. @@ -65,30 +67,18 @@ public void visitNamespaces(String srcNamespace, List dstNamespaces) thr @Override public void visitMetadata(String key, @Nullable String value) throws IOException { - switch (key) { - case Tiny1FileReader.nextIntermediaryClassProperty: - case Tiny1FileReader.nextIntermediaryFieldProperty: - case Tiny1FileReader.nextIntermediaryMethodProperty: - write("# INTERMEDIARY-COUNTER "); - - switch (key) { - case Tiny1FileReader.nextIntermediaryClassProperty: - write("class"); - break; - case Tiny1FileReader.nextIntermediaryFieldProperty: - write("field"); - break; - case Tiny1FileReader.nextIntermediaryMethodProperty: - write("method"); - break; - default: - throw new IllegalStateException(); - } + StandardProperty property = StandardProperties.getById(key); - write(" "); - write(value); - writeLn(); + if (property != null) { + if (!property.isApplicableTo(format)) return; + key = property.getNameFor(format); } + + write("# "); + write(key); + write(" "); + write(value); + writeLn(); } @Override @@ -197,6 +187,7 @@ private void writeTab() throws IOException { } private static final Set flags = EnumSet.of(MappingFlag.NEEDS_SRC_FIELD_DESC, MappingFlag.NEEDS_SRC_METHOD_DESC); + private static final MappingFormat format = MappingFormat.TINY_FILE; private final Writer writer; private String classSrcName; diff --git a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileReader.java b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileReader.java index be4b408b..707f7b02 100644 --- a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileReader.java @@ -26,6 +26,8 @@ import net.fabricmc.mappingio.MappingVisitor; import net.fabricmc.mappingio.format.ColumnFileReader; import net.fabricmc.mappingio.format.MappingFormat; +import net.fabricmc.mappingio.format.StandardProperties; +import net.fabricmc.mappingio.format.StandardProperty; /** * {@linkplain MappingFormat#TINY_2 Tiny v2 file} reader. @@ -96,16 +98,21 @@ private static void read(ColumnFileReader reader, MappingVisitor visitor) throws if (visitHeader || firstIteration) { while (reader.nextLine(1)) { if (!visitHeader) { - if (!escapeNames && reader.nextCol(Tiny2Util.escapedNamesProperty)) { + if (!escapeNames && reader.nextCol(StandardProperties.ESCAPED_NAMES.getNameFor(format))) { escapeNames = true; } } else { String key = reader.nextCol(); if (key == null) throw new IOException("missing property key in line "+reader.getLineNumber()); String value = reader.nextEscapedCol(); // may be missing -> null + StandardProperty property = StandardProperties.getByName(format, key); - if (key.equals(Tiny2Util.escapedNamesProperty)) { - escapeNames = true; + if (property != null) { + key = property.getId(); + + if (property == StandardProperties.ESCAPED_NAMES) { + escapeNames = true; + } } visitor.visitMetadata(key, value); @@ -222,4 +229,6 @@ private static void readDstNames(ColumnFileReader reader, MappedElementKind subj if (!name.isEmpty()) visitor.visitDstName(subjectKind, dstNs, name); } } + + private static final MappingFormat format = MappingFormat.TINY_2_FILE; } diff --git a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileWriter.java b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileWriter.java index f2b5b017..7cfa29c3 100644 --- a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileWriter.java +++ b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileWriter.java @@ -29,6 +29,8 @@ import net.fabricmc.mappingio.MappingFlag; import net.fabricmc.mappingio.MappingWriter; import net.fabricmc.mappingio.format.MappingFormat; +import net.fabricmc.mappingio.format.StandardProperties; +import net.fabricmc.mappingio.format.StandardProperty; /** * {@linkplain MappingFormat#TINY_2 Tiny v2 file} writer. @@ -66,9 +68,16 @@ public void visitNamespaces(String srcNamespace, List dstNamespaces) thr @Override public void visitMetadata(String key, @Nullable String value) throws IOException { - if (key.equals(Tiny2Util.escapedNamesProperty)) { - escapeNames = true; - wroteEscapedNamesProperty = true; + StandardProperty property = StandardProperties.getById(key); + + if (property != null) { + if (!property.isApplicableTo(format)) return; + key = property.getNameFor(format); + + if (property == StandardProperties.ESCAPED_NAMES) { + escapeNames = true; + wroteEscapedNamesProperty = true; + } } writeTab(); @@ -86,7 +95,7 @@ public void visitMetadata(String key, @Nullable String value) throws IOException public boolean visitContent() throws IOException { if (escapeNames && !wroteEscapedNamesProperty) { write("\t"); - write(Tiny2Util.escapedNamesProperty); + write(StandardProperties.ESCAPED_NAMES.getNameFor(format)); writeLn(); } @@ -206,6 +215,7 @@ private void writeTabs(int count) throws IOException { } } + private static final MappingFormat format = MappingFormat.TINY_2_FILE; private static final Set flags = EnumSet.of( MappingFlag.NEEDS_HEADER_METADATA, MappingFlag.NEEDS_METADATA_UNIQUENESS, diff --git a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2Util.java b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2Util.java index 68bee840..341d297d 100644 --- a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2Util.java +++ b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2Util.java @@ -84,6 +84,4 @@ public static String unescape(String str) { private static final String toEscape = "\\\n\r\0\t"; private static final String escaped = "\\nr0t"; - - static final String escapedNamesProperty = "escaped-names"; } diff --git a/src/main/java/net/fabricmc/mappingio/tree/MappingTree.java b/src/main/java/net/fabricmc/mappingio/tree/MappingTree.java index df8db66d..4ac8182c 100644 --- a/src/main/java/net/fabricmc/mappingio/tree/MappingTree.java +++ b/src/main/java/net/fabricmc/mappingio/tree/MappingTree.java @@ -21,6 +21,8 @@ import org.jetbrains.annotations.Nullable; +import net.fabricmc.mappingio.format.StandardProperty; + /** * Mutable mapping tree. */ @@ -44,6 +46,14 @@ public interface MappingTree extends MappingTreeView { @Override List getMetadata(String key); + /** + * @return An unmodifiable list of all metadata entries currently present + * in the tree whose key is equal to the passed {@link StandardProperty}'s ID. + * The list's order is equal to the order in which the entries have been originally added. + */ + @Override + List getMetadata(StandardProperty property); + void addMetadata(MetadataEntry entry); /** diff --git a/src/main/java/net/fabricmc/mappingio/tree/MappingTreeView.java b/src/main/java/net/fabricmc/mappingio/tree/MappingTreeView.java index d6f3eac4..d4e1eccb 100644 --- a/src/main/java/net/fabricmc/mappingio/tree/MappingTreeView.java +++ b/src/main/java/net/fabricmc/mappingio/tree/MappingTreeView.java @@ -23,6 +23,7 @@ import org.jetbrains.annotations.Nullable; import net.fabricmc.mappingio.MappingVisitor; +import net.fabricmc.mappingio.format.StandardProperty; /** * Read-only mapping tree. @@ -72,6 +73,7 @@ default String getNamespaceName(int id) { List getMetadata(); List getMetadata(String key); + List getMetadata(StandardProperty property); Collection getClasses(); @Nullable diff --git a/src/main/java/net/fabricmc/mappingio/tree/MemoryMappingTree.java b/src/main/java/net/fabricmc/mappingio/tree/MemoryMappingTree.java index fcf6829b..b9c6375e 100644 --- a/src/main/java/net/fabricmc/mappingio/tree/MemoryMappingTree.java +++ b/src/main/java/net/fabricmc/mappingio/tree/MemoryMappingTree.java @@ -39,6 +39,7 @@ import net.fabricmc.mappingio.MappedElementKind; import net.fabricmc.mappingio.MappingFlag; import net.fabricmc.mappingio.MappingVisitor; +import net.fabricmc.mappingio.format.StandardProperty; /** * {@link VisitableMappingTree} implementation that stores all data in memory. @@ -227,6 +228,13 @@ public List getMetadata(String key) { .collect(Collectors.toList())); } + @Override + public List getMetadata(StandardProperty property) { + return Collections.unmodifiableList(metadata.stream() + .filter(entry -> entry.getKey().equals(property.getId())) + .collect(Collectors.toList())); + } + @Override public void addMetadata(MetadataEntry entry) { metadata.add(entry);