From b200fadd945e91343ba15e7fb2c2429af85324b4 Mon Sep 17 00:00:00 2001 From: XiaoPangxie732 <47449269+XiaoPangxie732@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:05:14 +0800 Subject: [PATCH] `Documented` component rework Now docs are correctly escaped and unescaped in tiny v2 format --- .../MinecraftDecompilerCommandLine.java | 1 + .../mapping/NamespacedMapping.java | 2 - .../mapping/component/Component.java | 6 +++ .../mapping/component/Documented.java | 49 ++++++++++++++++--- .../mapping/generator/MappingGenerators.java | 15 +++--- .../mapping/processor/MappingProcessors.java | 7 +-- .../mapping/util/DescriptorRemapper.java | 2 +- .../mcdecompiler/mapping/util/TinyUtil.java | 47 ++++++++++++++++++ 8 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/util/TinyUtil.java diff --git a/modules/cli/src/main/java/cn/maxpixel/mcdecompiler/MinecraftDecompilerCommandLine.java b/modules/cli/src/main/java/cn/maxpixel/mcdecompiler/MinecraftDecompilerCommandLine.java index 7604aabb..d3887be5 100644 --- a/modules/cli/src/main/java/cn/maxpixel/mcdecompiler/MinecraftDecompilerCommandLine.java +++ b/modules/cli/src/main/java/cn/maxpixel/mcdecompiler/MinecraftDecompilerCommandLine.java @@ -52,6 +52,7 @@ public class MinecraftDecompilerCommandLine { private static final Logger LOGGER = LogManager.getLogger("CommandLine"); public static void main(String[] args) throws Throwable { + if (Constants.IS_DEV) LOGGER.info("MCD Begin");// Used to measure time OptionParser parser = new OptionParser(); ArgumentAcceptingOptionSpec sideTypeO = parser.acceptsAll(of("s", "side"), "Side to deobfuscate/" + "decompile. Values are \"CLIENT\" and \"SERVER\". With this option, you must specify --version option and can't " + diff --git a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/NamespacedMapping.java b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/NamespacedMapping.java index f0427c6a..c63a1779 100644 --- a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/NamespacedMapping.java +++ b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/NamespacedMapping.java @@ -165,8 +165,6 @@ public NamespacedMapping(String[] namespaces, String[] names, int nameStart, Com if (namespaces.length != (names.length - Objects.checkIndex(nameStart, names.length))) throw new IllegalArgumentException(); for (int i = 0; i < namespaces.length; i++) { -// var n = i + nameStart;// FIXME: Maybe useless? -// this.names.put(Objects.requireNonNull(namespaces[i]), n >= names.length ? names[names.length - 1] : names[n]); this.names.put(Objects.requireNonNull(namespaces[i]), names[i + nameStart]); } } diff --git a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/component/Component.java b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/component/Component.java index e9f24da2..ddc7187a 100644 --- a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/component/Component.java +++ b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/component/Component.java @@ -22,4 +22,10 @@ * Base component class. Every component must implement this interface */ public interface Component { + /** + * Validates this component. + * @throws IllegalStateException if the validation fails + */ + default void validate() throws IllegalStateException {// TODO + } } \ No newline at end of file diff --git a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/component/Documented.java b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/component/Documented.java index 70388e3f..cc7f4233 100644 --- a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/component/Documented.java +++ b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/component/Documented.java @@ -18,28 +18,63 @@ package cn.maxpixel.mcdecompiler.mapping.component; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import org.jetbrains.annotations.NotNull; + +import java.util.List; import java.util.Objects; +/** + * The component that holds documentation about a class, field, method, or parameter. + * + * @apiNote The content is a list of strings, which each element represents exactly one line, + * so {@code \n} and {@code \r} are not allowed. An empty line must be represented by {@code ""}. + * {@code null} elements are prohibited + */ public class Documented implements Component { - private String doc; + /** + * The contents + */ + public final ObjectArrayList contents = new ObjectArrayList<>(); + + /** + * Gets the contents + * @return The contents + */ + public List getContents() { + return contents; + } - public String getDoc() { - return doc; + /** + * Join the contents with {@code \n} + * @return the joined string + */ + public @NotNull String getContentString() { + if (contents.isEmpty()) return ""; + return String.join("\n", contents); } - public void setDoc(String doc) { - this.doc = Objects.requireNonNull(doc, "null documentation isn't meaningful huh?"); + /** + * Breaks the string into lines and sets them as contents + * @param content the string + */ + public void setContents(@NotNull String content) { + int mark = 0; + for (int i = content.indexOf('\n'); i >= 0; i = content.indexOf('\n', mark)) { + contents.add(content.substring(mark, content.charAt(i - 1) == '\r' ? i - 1 : i)); + mark = i + 1; + } } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Documented that)) return false; - return Objects.equals(doc, that.doc); + return Objects.equals(contents, that.contents); } @Override public int hashCode() { - return Objects.hashCode(doc); + return Objects.hashCode(contents); } } \ No newline at end of file diff --git a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/generator/MappingGenerators.java b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/generator/MappingGenerators.java index 9b67a9bb..37d72370 100644 --- a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/generator/MappingGenerators.java +++ b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/generator/MappingGenerators.java @@ -29,6 +29,7 @@ import cn.maxpixel.mcdecompiler.mapping.remapper.ClassifiedMappingRemapper; import cn.maxpixel.mcdecompiler.mapping.trait.NamespacedTrait; import cn.maxpixel.mcdecompiler.mapping.util.MappingUtil; +import cn.maxpixel.mcdecompiler.mapping.util.TinyUtil; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectList; @@ -339,8 +340,8 @@ public ObjectList generate(ClassifiedMapping mappings synchronized (lines) { lines.add("\tf\t" + desc + '\t' + NamingUtil.concatNamespaces(namespaces, field::getName, "\t")); if (field.hasComponent(Documented.class)) { - String doc = field.getComponent(Documented.class).getDoc(); - if (doc != null && !doc.isBlank()) lines.add("\t\tc\t" + doc); + String doc = field.getComponent(Documented.class).getContentString(); + if (!doc.isBlank()) lines.add("\t\tc\t" + TinyUtil.escape(doc)); } } }); @@ -349,8 +350,8 @@ public ObjectList generate(ClassifiedMapping mappings synchronized (lines) { lines.add("\tm\t" + desc + '\t' + NamingUtil.concatNamespaces(namespaces, method::getName, "\t")); if (method.hasComponent(Documented.class)) { - String doc = method.getComponent(Documented.class).getDoc(); - if (doc != null && !doc.isBlank()) lines.add("\t\tc\t" + doc); + String doc = method.getComponent(Documented.class).getContentString(); + if (!doc.isBlank()) lines.add("\t\tc\t" + TinyUtil.escape(doc)); } if (method.hasComponent(LocalVariableTable.Namespaced.class)) { LocalVariableTable.Namespaced lvt = method.getComponent(LocalVariableTable.Namespaced.class); @@ -364,9 +365,9 @@ public ObjectList generate(ClassifiedMapping mappings return name; }, "\t"); lines.add("\t\tp\t" + index + '\t' + names); - if(localVariable.hasComponent(Documented.class)) { - String doc = localVariable.getComponent(Documented.class).getDoc(); - if(doc != null && !doc.isBlank()) lines.add("\t\t\tc\t" + doc); + if (localVariable.hasComponent(Documented.class)) { + String doc = localVariable.getComponent(Documented.class).getContentString(); + if (!doc.isBlank()) lines.add("\t\t\tc\t" + TinyUtil.escape(doc)); } }); } diff --git a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/processor/MappingProcessors.java b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/processor/MappingProcessors.java index 04bbe7a4..ee79d393 100644 --- a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/processor/MappingProcessors.java +++ b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/processor/MappingProcessors.java @@ -30,6 +30,7 @@ import cn.maxpixel.mcdecompiler.mapping.format.MappingFormats; import cn.maxpixel.mcdecompiler.mapping.trait.NamespacedTrait; import cn.maxpixel.mcdecompiler.mapping.util.MappingUtil; +import cn.maxpixel.mcdecompiler.mapping.util.TinyUtil; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectList; @@ -424,7 +425,7 @@ private static int processTree(int index, int size, String[] namespaces, ObjectL if (s.charAt(0) == '\t') { String[] sa = MappingUtil.split(s.substring(3), '\t'); switch (s.charAt(1)) { - case 'c' -> classMapping.mapping.getComponent(Documented.class).setDoc(sa[0]); + case 'c' -> classMapping.mapping.getComponent(Documented.class).setContents(TinyUtil.unescape(sa[0])); case 'f' -> { NamespacedMapping fieldMapping = MappingUtil.Namespaced.dduo(namespaces, sa, 1, namespaces[0], sa[0]); index = processTree1(index, size, namespaces, content, fieldMapping); @@ -448,7 +449,7 @@ private static int processTree1(int index, int size, String[] namespaces, Object String s = content.get(index); if (s.charAt(1) == '\t' && s.charAt(0) == '\t') { switch (s.charAt(2)) { - case 'c' -> mapping.getComponent(Documented.class).setDoc(s.substring(4)); + case 'c' -> mapping.getComponent(Documented.class).setContents(TinyUtil.unescape(s.substring(4))); case 'p' -> { String[] sa = MappingUtil.split(s.substring(4), '\t'); NamespacedMapping localVariable = MappingUtil.Namespaced.d(namespaces, sa, 1); @@ -467,7 +468,7 @@ private static int processTree2(int index, int size, ObjectList content, if (++index < size) { String s = content.get(index); if (s.charAt(2) == '\t' && s.charAt(1) == '\t' && s.charAt(0) == '\t') { - if(s.charAt(3) == 'c') localVariable.getComponent(Documented.class).setDoc(s.substring(5)); + if (s.charAt(3) == 'c') localVariable.getComponent(Documented.class).setContents(TinyUtil.unescape(s.substring(5))); else error(); return index; } diff --git a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/util/DescriptorRemapper.java b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/util/DescriptorRemapper.java index 02a2f820..9cc15160 100644 --- a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/util/DescriptorRemapper.java +++ b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/util/DescriptorRemapper.java @@ -16,7 +16,7 @@ /** * Lightweight remapper for descriptors in place of the general heavyweight {@link cn.maxpixel.mcdecompiler.mapping.remapper.MappingRemapper}s. */ -public class DescriptorRemapper {// TODO +public class DescriptorRemapper { private final Object2ObjectOpenHashMap> mappingByUnm; private final Object2ObjectOpenHashMap> mappingByMap; diff --git a/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/util/TinyUtil.java b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/util/TinyUtil.java new file mode 100644 index 00000000..79e75d56 --- /dev/null +++ b/modules/mapping-api/src/main/java/cn/maxpixel/mcdecompiler/mapping/util/TinyUtil.java @@ -0,0 +1,47 @@ +package cn.maxpixel.mcdecompiler.mapping.util; + +public final class TinyUtil { + private TinyUtil() { + throw new AssertionError("No instances"); + } + + public static String escape(String unescaped) { + StringBuilder sb = new StringBuilder(unescaped.length() + 16); + int mark = 0; + for (int i = 0; i < unescaped.length(); i++) { + char escaped = switch (unescaped.charAt(i)) { + case '\\' -> '\\'; + case '\n' -> 'n'; + case '\r' -> 'r'; + case '\t' -> 't'; + case '\0' -> '0'; + default -> '\0'; + }; + if (escaped != 0) { + if (mark < i) sb.append(unescaped, mark, i); + mark = i + 1; + sb.append('\\').append(escaped); + } + } + return sb.append(unescaped, mark, unescaped.length()).toString(); + } + + public static String unescape(String escaped) { + StringBuilder sb = new StringBuilder(escaped.length()); + int mark = 0; + for (int i = escaped.indexOf('\\'); i >= 0; i = escaped.indexOf('\\', mark)) { + char unescaped = switch (escaped.charAt(++i)) { + case '\\' -> '\\'; + case 'n' -> '\n'; + case 'r' -> '\r'; + case 't' -> '\t'; + case '0' -> '\0'; + default -> throw new IllegalArgumentException("Unknown escape character: \\" + escaped.charAt(i)); + }; + if (mark < i - 1) sb.append(escaped, mark, i - 1); + mark = i + 1; + sb.append(unescaped); + } + return sb.append(escaped, mark, escaped.length()).toString(); + } +} \ No newline at end of file