diff --git a/CHANGELOG.md b/CHANGELOG.md index 35a5ded2..4c18ffd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.
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] +- Fixed NPE in `MemoryMappingTree` +- Fixed TSRG2 reader not handling multiple passes correctly + ## [0.5.0] - 2023-11-15 - Actually marked `HierarchyInfoProvider` as experimental - Added changelog diff --git a/src/main/java/net/fabricmc/mappingio/format/ColumnFileReader.java b/src/main/java/net/fabricmc/mappingio/format/ColumnFileReader.java index 17fb1077..3c4c44e0 100644 --- a/src/main/java/net/fabricmc/mappingio/format/ColumnFileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/ColumnFileReader.java @@ -28,8 +28,9 @@ @ApiStatus.Internal public final class ColumnFileReader implements Closeable { - public ColumnFileReader(Reader reader, char columnSeparator) { + public ColumnFileReader(Reader reader, char indentationChar, char columnSeparator) { this.reader = reader; + this.indentationChar = indentationChar; this.columnSeparator = columnSeparator; } @@ -43,45 +44,20 @@ public void close() throws IOException { * *

The reader will point to the next column or end of line if successful, otherwise remains unchanged. * - * @param expect content to expect + * @param expected content to expect * @return true if the column was read and had the expected content, false otherwise * @throws IOException */ - public boolean nextCol(String expect) throws IOException { - if (eol) return false; - - int len = expect.length(); - if (!fillBuffer(len)) return false; - - for (int i = 0; i < len; i++) { - if (buffer[bufferPos + i] != expect.charAt(i)) return false; // read failed, not all of expect available - } - - char trailing = 0; - - if (fillBuffer(len + 1) // not eof - && (trailing = buffer[bufferPos + len]) != columnSeparator // not end of column - && trailing != '\n' // not end of line - && trailing != '\r') { - return false; // read failed, column contains data beyond expect - } - - // successful read - - bufferPos += expect.length(); - - // seek to the start of the next column - if (trailing == columnSeparator) { - bufferPos++; - } else { - eol = true; - } + public boolean nextCol(String expected) throws IOException { + if (read(false, false, true, expected) == noMatch) return false; return true; } /** * Read and consume a column without unescaping. + * + * @return null if nothing has been read (first char was eol), otherwise the read string (may be empty). */ @Nullable public String nextCol() throws IOException { @@ -89,36 +65,80 @@ public String nextCol() throws IOException { } /** - * Read and consume a column with unescaping. + * Read and consume a column, and unescape it if requested. + * + * @return null if nothing has been read (first char was eol), otherwise the read string (may be empty). + */ + @Nullable + public String nextCol(boolean unescape) throws IOException { + return read(unescape, true, true, null); + } + + /** + * Read a column without consuming, and unescape if requested. + * Since it doesn't consume, it won't (un)mark bof, eol or eof. + * + * @return null if nothing has been read (first char was eol), otherwise the read string (may be empty). */ @Nullable - public String nextEscapedCol() throws IOException { - return nextCol(true); + public String peekCol(boolean unescape) throws IOException { + return read(unescape, false, true, null); } /** - * Read and consume a column and unescape it if requested. + * @param unescape Whether to unescape the read string. + * @param consume Whether to advance the bufferPos. + * @param stopAtNextCol Whether to only read one column. + * @param expected If not null, the read string must match this exactly, otherwise we early-exit with {@link #noMatch}. Always consumes if matched. + * + * @return null if nothing has been read (first char was eol), otherwise the read string (may be empty). + * If {@code expected} is not null, it will be returned if matched, otherwise {@link #noMatch}. */ @Nullable - public String nextCol(boolean unescape) throws IOException { - if (eol) return null; + private String read(boolean unescape, boolean consume, boolean stopAtNextCol, @Nullable String expected) throws IOException { + if (eol) return expected == null ? null : noMatch; + + int expectedLength = expected != null ? expected.length() : -1; + + // Check if the buffer needs to be filled and if we hit eof while doing so + if (expectedLength > 0 && bufferPos + expectedLength >= bufferLimit) { + if (!fillBuffer(expectedLength, !consume, false)) return noMatch; + } int start; - int end = bufferPos; + int end = this.bufferPos; int firstEscaped = -1; + int contentCharsRead = 0; + int modifiedBufferPos = -1; + int startOffset = 0; + boolean readAnything = false; + boolean filled = true; readLoop: for (;;) { while (end < bufferLimit) { char c = buffer[end]; + boolean isColumnSeparator = (c == columnSeparator); - if (c == columnSeparator || c == '\n' || c == '\r') { // end of the current column + // skip leading column separator + if (isColumnSeparator && !readAnything) { + startOffset = 1; + contentCharsRead = -1; + } + + readAnything = true; + + if (expected != null && contentCharsRead > -1) { + if ((contentCharsRead < expectedLength && c != expected.charAt(contentCharsRead)) + || contentCharsRead > expectedLength) { + return noMatch; + } + } + + if (c == '\n' || c == '\r' || (isColumnSeparator && stopAtNextCol && contentCharsRead > -1)) { // stop reading start = bufferPos; - bufferPos = end; + modifiedBufferPos = end; - // seek to the start of the next column - if (c == columnSeparator) { - bufferPos++; - } else { + if (!isColumnSeparator && (consume || expected != null)) { eol = true; } @@ -127,13 +147,14 @@ public String nextCol(boolean unescape) throws IOException { firstEscaped = bufferPos; } + contentCharsRead++; end++; } // buffer ran out, refill int oldStart = bufferPos; - boolean filled = fillBuffer(end - bufferPos + 1); + filled = fillBuffer(end - bufferPos + 1, !consume, consume); int posShift = bufferPos - oldStart; // fillBuffer may compact the data, shifting it to the buffer start assert posShift <= 0; end += posShift; @@ -141,74 +162,70 @@ public String nextCol(boolean unescape) throws IOException { if (!filled) { start = bufferPos; - bufferPos = end; - eol = true; break; } } - int len = end - start; + start += startOffset; + String ret; - if (len == 0) { - return ""; - } else if (firstEscaped >= 0) { - return Tiny2Util.unescape(String.valueOf(buffer, start, len)); + if (expected != null) { + consume = true; + ret = expected; } else { - return String.valueOf(buffer, start, len); + int len = end - start; + + if (len == 0) { + ret = readAnything ? "" : null; + } else if (firstEscaped >= 0) { + ret = Tiny2Util.unescape(String.valueOf(buffer, start, len)); + } else { + ret = String.valueOf(buffer, start, len); + } } - } - /** - * Read and consume all column until eol and unescape if requested. - */ - @Nullable - public String nextCols(boolean unescape) throws IOException { - if (eol) return null; + if (consume) { + if (readAnything) bof = false; + if (!filled) eof = eol = true; + if (modifiedBufferPos != -1) bufferPos = modifiedBufferPos; - int end = bufferPos; - int firstEscaped = -1; - boolean filled; + if (eol && !eof) { // manually check for eof + int charsToRead = buffer[bufferPos] == '\r' ? 2 : 1; // 2 for \r\n, 1 for just \n - readLoop: do { - while (end < bufferLimit) { - char c = buffer[end]; - - if (c == '\n' || c == '\r') { // end of the current column - break readLoop; - } else if (unescape && c == '\\' && firstEscaped < 0) { - firstEscaped = bufferPos; + if (end >= bufferLimit - charsToRead) { + fillBuffer(charsToRead, false, true); } - - end++; } + } - // buffer ran out, refill - - int oldStart = bufferPos; - filled = fillBuffer(end - bufferPos + 1); - int posShift = bufferPos - oldStart; // fillBuffer may compact the data, shifting it to the buffer start - assert posShift <= 0; - end += posShift; - if (firstEscaped >= 0) firstEscaped += posShift; - } while (filled); - - int start = bufferPos; - bufferPos = end; - eol = true; + return ret; + } - int len = end - start; + /** + * Read and consume all columns until eol, and unescape if requested. + * + * @return null if nothing has been read (first char was eol), otherwise the read string (may be empty). + */ + @Nullable + public String nextCols(boolean unescape) throws IOException { + return read(unescape, true, false, null); + } - if (len == 0) { - return ""; - } else if (firstEscaped >= 0) { - return Tiny2Util.unescape(String.valueOf(buffer, start, len)); - } else { - return String.valueOf(buffer, start, len); - } + /** + * Read all columns until eol without consuming, and unescape if requested. + * Since it doesn't consume, it won't (un)mark bof, eol or eof. + * + * @return null if nothing has been read (first char was eol), otherwise the read string (may be empty). + */ + @Nullable + public String peekCols(boolean unescape) throws IOException { + return read(unescape, false, false, null); } /** * Read and consume a column and convert it to integer. + * + * @return -1 if nothing has been read (first char was eol), otherwise the number present. */ public int nextIntCol() throws IOException { String str = nextCol(false); @@ -221,87 +238,167 @@ public int nextIntCol() throws IOException { } public boolean nextLine(int indent) throws IOException { - fillLopo: do { + fillLoop: do { while (bufferPos < bufferLimit) { char c = buffer[bufferPos]; if (c == '\n') { if (indent == 0) { // skip empty lines if indent is 0 - if (!fillBuffer(2)) break fillLopo; + if (!fillBuffer(2, false, true)) break fillLoop; c = buffer[bufferPos + 1]; if (c == '\n' || c == '\r') { // 2+ consecutive new lines, consume first nl and retry bufferPos++; lineNumber++; + bof = false; continue; } } - if (!fillBuffer(indent + 1)) return false; + if (!fillBuffer(indent + 1, false, true)) return false; for (int i = 1; i <= indent; i++) { - if (buffer[bufferPos + i] != '\t') return false; + if (buffer[bufferPos + i] != indentationChar) return false; } bufferPos += indent + 1; lineNumber++; + bof = false; eol = false; return true; } bufferPos++; + bof = false; } - } while (fillBuffer(1)); + } while (fillBuffer(1, false, true)); return false; } public boolean hasExtraIndents() throws IOException { - return fillBuffer(1) && buffer[bufferPos] == '\t'; + return fillBuffer(1, false, false) && buffer[bufferPos] == indentationChar; } public int getLineNumber() { return lineNumber; } + /** + * Whether or not EOL has been encountered in the current line yet. + */ + public boolean isAtEol() { + return eol; + } + + public boolean isAtBof() { + return bof; + } + public boolean isAtEof() { return eof; } - public void mark() { - if (bufferPos > 0) { + /** + * Marks the present position in the stream. Subsequent calls to + * {@link #reset()} will reposition the stream to this point. + * In comparison to {@link java.io.Reader#mark(int)} this method stacks, + * so don't forget to call {@link #discardMark()} if you don't need the mark anymore. + * + * @return the mark index (starting at 1) + */ + public int mark() { + if (markIdx == 0 && bufferPos > 0) { // save memory int available = bufferLimit - bufferPos; System.arraycopy(buffer, bufferPos, buffer, 0, available); bufferPos = 0; bufferLimit = available; - markedLineNumber = lineNumber; - markedEol = eol; - markedEof = eof; } - mark = bufferPos; + if (markIdx == markedBufferPositions.length) { + markedBufferPositions = Arrays.copyOf(markedBufferPositions, markedBufferPositions.length * 2); + markedLineNumbers = Arrays.copyOf(markedLineNumbers, markedLineNumbers.length * 2); + markedBofs = Arrays.copyOf(markedBofs, markedBofs.length * 2); + markedEols = Arrays.copyOf(markedEols, markedEols.length * 2); + markedEofs = Arrays.copyOf(markedEofs, markedEofs.length * 2); + } + + markedBufferPositions[markIdx] = bufferPos; + markedLineNumbers[markIdx] = lineNumber; + markedBofs[markIdx] = bof; + markedEols[markIdx] = eol; + markedEofs[markIdx] = eof; + + return ++markIdx; } - public void reset() { - if (mark < 0) throw new IllegalStateException("not marked"); + /** + * Discard the last mark. + */ + public void discardMark() { + discardMark(markIdx); + } + + /** + * Discard the mark at specified index and all above, if present. + */ + private void discardMark(int index) { + if (markIdx == 0) throw new IllegalStateException("no mark to discard"); + if (index < 1 || index > markIdx) throw new IllegalStateException("index out of bounds"); + + for (int i = markIdx; i >= index; i--) { + markedBufferPositions[i-1] = 0; + markedLineNumbers[i-1] = 0; + } + + markIdx = index - 1; + } + + /** + * Reset to last mark. The marked data isn't discarded, so can be called multiple times. + * If you want to reset to an older mark, use {@link #reset(int)}. + * + * @return The index of the mark that was reset to. + */ + public int reset() { + reset(markIdx); + return markIdx; + } + + /** + * Reset to the mark with the specified index. + * Unless reset to {@code 0}, the marked data isn't discarded afterwards, + * so can be called multiple times. + * Use negative indices to reset to a mark relative to the current one. + */ + public void reset(int indexToResetTo) { + if (markIdx == 0) throw new IllegalStateException("no mark to reset to"); + if (indexToResetTo < -markIdx || indexToResetTo > markIdx) throw new IllegalStateException("index out of bounds"); + + if (indexToResetTo < 0) indexToResetTo += markIdx; + int arrayIdx = indexToResetTo == 0 ? indexToResetTo : indexToResetTo - 1; + + bufferPos = markedBufferPositions[arrayIdx]; + lineNumber = markedLineNumbers[arrayIdx]; + bof = markedBofs[arrayIdx]; + eol = markedEols[arrayIdx]; + eof = markedEofs[arrayIdx]; - bufferPos = mark; - lineNumber = markedLineNumber; - eol = markedEol; - eof = markedEof; + if (indexToResetTo == 0) discardMark(1); + markIdx = indexToResetTo; } - private boolean fillBuffer(int count) throws IOException { + private boolean fillBuffer(int count, boolean preventCompaction, boolean markEof) throws IOException { int available = bufferLimit - bufferPos; int req = count - available; if (req <= 0) return true; if (bufferPos + count > buffer.length) { // not enough remaining buffer space - if (mark >= 0) { // marked for rewind -> grow + if (markIdx > 0 || preventCompaction) { // can't compact -> grow buffer = Arrays.copyOf(buffer, Math.max(bufferPos + count, buffer.length * 2)); - } else { // not marked, compact and grow as needed + } else { // compact and grow as needed if (count > buffer.length) { // too small for compacting to suffice -> grow and compact char[] newBuffer = new char[Math.max(count, buffer.length * 2)]; System.arraycopy(buffer, bufferPos, newBuffer, 0, available); @@ -321,7 +418,7 @@ private boolean fillBuffer(int count) throws IOException { int read = reader.read(buffer, bufferLimit, buffer.length - bufferLimit); if (read < 0) { - eof = eol = true; + if (markEof) eof = eol = true; return false; } @@ -331,16 +428,21 @@ private boolean fillBuffer(int count) throws IOException { return true; } + private static final String noMatch = new String(); private final Reader reader; + private final char indentationChar; private final char columnSeparator; private char[] buffer = new char[4096 * 4]; private int bufferPos; private int bufferLimit; - private int mark = -1; private int lineNumber = 1; + private boolean bof = true; private boolean eol; // tracks whether the last column has been read, otherwise ambiguous if the last col is empty private boolean eof; - private int markedLineNumber; - private boolean markedEol; - private boolean markedEof; + private int markIdx = 0; // 0 means no mark + private int[] markedBufferPositions = new int[3]; + private int[] markedLineNumbers = new int[3]; + private boolean[] markedBofs = new boolean[3]; + private boolean[] markedEols = new boolean[3]; + private boolean[] markedEofs = new boolean[3]; } diff --git a/src/main/java/net/fabricmc/mappingio/format/enigma/EnigmaFileReader.java b/src/main/java/net/fabricmc/mappingio/format/enigma/EnigmaFileReader.java index cafb1811..f6d5180b 100644 --- a/src/main/java/net/fabricmc/mappingio/format/enigma/EnigmaFileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/enigma/EnigmaFileReader.java @@ -38,7 +38,7 @@ public static void read(Reader reader, MappingVisitor visitor) throws IOExceptio } public static void read(Reader reader, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException { - read(new ColumnFileReader(reader, ' '), sourceNs, targetNs, visitor); + read(new ColumnFileReader(reader, '\t', ' '), sourceNs, targetNs, visitor); } public static void read(ColumnFileReader reader, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException { @@ -61,6 +61,8 @@ public static void read(ColumnFileReader reader, String sourceNs, String targetN do { if (reader.nextCol("CLASS")) { // class: CLASS [] readClass(reader, 0, null, null, commentSb, finalVisitor); + } else { + invalidLine(reader, "CLASS"); } } while (reader.nextLine(0)); } @@ -89,6 +91,8 @@ private static void readClass(ColumnFileReader reader, int indent, String outerS String dstInnerName = reader.nextCol(); String dstName = dstInnerName; + if (dstName != null && dstName.isEmpty()) throw new IOException("empty class-name-b in line "+reader.getLineNumber()); // null is allowed, empty is not + // merge with outer name if available if (outerDstClass != null || dstInnerName != null && outerSrcClass != null) { @@ -98,6 +102,7 @@ private static void readClass(ColumnFileReader reader, int indent, String outerS dstName = String.format("%s$%s", outerDstClass, dstInnerName); } + checkEol(reader); readClassBody(reader, indent, srcName, dstName, commentSb, visitor); } @@ -136,16 +141,25 @@ private static void readClassBody(ColumnFileReader reader, int indent, String sr dstName = null; srcDesc = dstNameOrSrcDesc; } else { + if (srcDesc.isEmpty()) throw new IOException("missing member-desc-a in line "+reader.getLineNumber()); dstName = dstNameOrSrcDesc; } - if (isMethod && visitor.visitMethod(srcName, srcDesc)) { - if (dstName != null && !dstName.isEmpty()) visitor.visitDstName(MappedElementKind.METHOD, 0, dstName); - readMethod(reader, indent, commentSb, visitor); - } else if (!isMethod && visitor.visitField(srcName, srcDesc)) { + if (isMethod) { + if (!srcDesc.startsWith("(")) throw new IOException("invalid method-desc-a in line "+reader.getLineNumber()); + + if (visitor.visitMethod(srcName, srcDesc)) { + if (dstName != null && !dstName.isEmpty()) visitor.visitDstName(MappedElementKind.METHOD, 0, dstName); + checkEol(reader); + readMethod(reader, indent, commentSb, visitor); + } + } else if (visitor.visitField(srcName, srcDesc)) { if (dstName != null && !dstName.isEmpty()) visitor.visitDstName(MappedElementKind.FIELD, 0, dstName); + checkEol(reader); readElement(reader, MappedElementKind.FIELD, indent, commentSb, visitor); } + } else { + invalidLine(reader, "CLASS, METHOD, FIELD or COMMENT"); } } @@ -199,8 +213,12 @@ private static void readMethod(ColumnFileReader reader, int indent, StringBuilde readElement(reader, MappedElementKind.METHOD_ARG, indent, commentSb, visitor); } + } else { + invalidLine(reader, "COMMENT or ARG"); } } + + checkEol(reader); } submitComment(MappedElementKind.METHOD, commentSb, visitor); @@ -212,7 +230,11 @@ private static void readElement(ColumnFileReader reader, MappedElementKind kind, while (reader.nextLine(indent + kind.level + 1)) { if (reader.nextCol("COMMENT")) { // comment: COMMENT readComment(reader, commentSb); + } else { + invalidLine(reader, "COMMENT"); } + + checkEol(reader); } submitComment(kind, commentSb, visitor); @@ -234,4 +256,24 @@ private static void submitComment(MappedElementKind kind, StringBuilder commentS visitor.visitComment(kind, commentSb.toString()); commentSb.setLength(0); } + + private static void invalidLine(ColumnFileReader reader, String expected) throws IOException { + String line = reader.nextCol(); + + if (line != null && line.startsWith(" ")) { + throw new IOException("Found indentation using spaces in line "+reader.getLineNumber()+", expected tab"); + } else if (!reader.isAtBof()) { // empty files are allowed + throw new IOException("invalid line "+reader.getLineNumber()+", expected "+expected); + } + } + + private static void checkEol(ColumnFileReader reader) throws IOException { + if (!reader.isAtEol()) { + String rest = reader.nextCols(false); + + if (rest != null && !rest.trim().startsWith("# ")) { + throw new IOException("line ending expected in line "+reader.getLineNumber()+", found: '"+rest+"'"); + } + } + } } diff --git a/src/main/java/net/fabricmc/mappingio/format/srg/SrgFileReader.java b/src/main/java/net/fabricmc/mappingio/format/srg/SrgFileReader.java index 8fe35146..ea392140 100644 --- a/src/main/java/net/fabricmc/mappingio/format/srg/SrgFileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/srg/SrgFileReader.java @@ -39,7 +39,7 @@ public static void read(Reader reader, MappingVisitor visitor) throws IOExceptio } public static void read(Reader reader, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException { - read(new ColumnFileReader(reader, ' '), sourceNs, targetNs, visitor); + read(new ColumnFileReader(reader, '\t', ' '), sourceNs, targetNs, visitor); } private static void read(ColumnFileReader reader, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException { @@ -97,7 +97,15 @@ private static void read(ColumnFileReader reader, String sourceNs, String target cols[i] = reader.nextCol(); } - if (!isMethod && cols[1] != null && cols[2] != null) format = MappingFormat.XSRG_FILE; + if (!isMethod) { + if (cols[1] != null && cols[2] != null) { + format = MappingFormat.XSRG_FILE; + } else if (cols[1] != null || cols[2] != null) { + String line = cols[1] == null ? cols[2] : (cols[2] == null ? cols[1] : cols[1] + cols[2]); + throw new IOException("unexpected content at line ending in line "+reader.getLineNumber()+": '"+line+"'"); + } + } + String srcDesc; String dstName; String dstDesc; @@ -142,6 +150,16 @@ private static void read(ColumnFileReader reader, String sourceNs, String target visitor.visitElementContent(kind); } } + } else { + invalidLine(reader, "'CL:', 'MD:' or 'FD:'"); + } + + if (!reader.isAtEol()) { + String rest = reader.nextCols(false); + + if (rest != null && !rest.trim().startsWith("# ")) { + throw new IOException("line ending expected in line "+reader.getLineNumber()+", found: '"+rest+"'"); + } } } while (reader.nextLine(0)); } @@ -155,4 +173,14 @@ private static void read(ColumnFileReader reader, String sourceNs, String target ((MappingTree) visitor).accept(parentVisitor); } } + + private static void invalidLine(ColumnFileReader reader, String expected) throws IOException { + String line = reader.nextCol(false); + + if (line != null && line.startsWith(" ")) { + throw new IOException("Found indentation using spaces in line "+reader.getLineNumber()+", expected tab"); + } else if (!reader.isAtBof()) { // empty files are allowed + throw new IOException("invalid line "+reader.getLineNumber()+", expected "+expected); + } + } } diff --git a/src/main/java/net/fabricmc/mappingio/format/srg/TsrgFileReader.java b/src/main/java/net/fabricmc/mappingio/format/srg/TsrgFileReader.java index f0e32c28..b25ad7ae 100644 --- a/src/main/java/net/fabricmc/mappingio/format/srg/TsrgFileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/srg/TsrgFileReader.java @@ -16,7 +16,6 @@ package net.fabricmc.mappingio.format.srg; -import java.io.CharArrayReader; import java.io.IOException; import java.io.Reader; import java.util.ArrayList; @@ -36,7 +35,7 @@ private TsrgFileReader() { } public static List getNamespaces(Reader reader) throws IOException { - return getNamespaces(new ColumnFileReader(reader, ' ')); + return getNamespaces(new ColumnFileReader(reader, '\t', ' ')); } private static List getNamespaces(ColumnFileReader reader) throws IOException { @@ -58,32 +57,16 @@ public static void read(Reader reader, MappingVisitor visitor) throws IOExceptio read(reader, MappingUtil.NS_SOURCE_FALLBACK, MappingUtil.NS_TARGET_FALLBACK, visitor); } - public static void read(Reader r, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException { - ColumnFileReader reader; - CharArrayReader parentReader = null; - - if (visitor.getFlags().contains(MappingFlag.NEEDS_MULTIPLE_PASSES)) { - char[] buffer = new char[100_000]; - int pos = 0; - int len; - - while ((len = r.read(buffer, pos, buffer.length - pos)) >= 0) { - pos += len; - if (pos == buffer.length) buffer = Arrays.copyOf(buffer, buffer.length * 2); - } - - parentReader = new CharArrayReader(buffer, 0, pos); - reader = new ColumnFileReader(parentReader, ' '); - } else { - reader = new ColumnFileReader(r, ' '); - } + public static void read(Reader reader, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException { + read(new ColumnFileReader(reader, '\t', ' '), sourceNs, targetNs, visitor); + } - MappingFormat format = MappingFormat.TSRG_FILE; - if (reader.nextCol("tsrg2")) format = MappingFormat.TSRG_2_FILE; + public static void read(ColumnFileReader reader, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException { + MappingFormat format = reader.nextCol("tsrg2") ? format = MappingFormat.TSRG_2_FILE : MappingFormat.TSRG_FILE; String srcNamespace; List dstNamespaces; - if (format == MappingFormat.TSRG_2_FILE) { // tsrg2 magic + if (format == MappingFormat.TSRG_2_FILE) { srcNamespace = reader.nextCol(); dstNamespaces = new ArrayList<>(); String dstNamespace; @@ -98,13 +81,15 @@ public static void read(Reader r, String sourceNs, String targetNs, MappingVisit dstNamespaces = Collections.singletonList(targetNs); } + if (visitor.getFlags().contains(MappingFlag.NEEDS_MULTIPLE_PASSES)) { + reader.mark(); + } + int dstNsCount = dstNamespaces.size(); List nameTmp = dstNamespaces.size() > 1 ? new ArrayList<>(dstNamespaces.size() - 1) : null; for (;;) { - boolean visitHeader = visitor.visitHeader(); - - if (visitHeader) { + if (visitor.visitHeader()) { visitor.visitNamespaces(srcNamespace, dstNamespaces); } @@ -116,8 +101,14 @@ public static void read(Reader r, String sourceNs, String targetNs, MappingVisit if (reader.hasExtraIndents()) continue; reader.mark(); String line = reader.nextCols(false); - if (line == null && reader.isAtEof()) continue; + + if ((line == null || line.isEmpty()) && reader.isAtEof()) { + reader.discardMark(); + continue; + } + reader.reset(); + reader.discardMark(); String[] parts = line.split("((?<= )|(?= ))"); // Split on spaces, but keep them if (format != MappingFormat.TSRG_2_FILE && parts.length >= 4 && !parts[3].startsWith("#")) { // CSRG @@ -179,12 +170,8 @@ public static void read(Reader r, String sourceNs, String targetNs, MappingVisit if (visitor.visitEnd()) break; - if (parentReader == null) { - throw new IllegalStateException("repeated visitation requested without NEEDS_MULTIPLE_PASSES"); - } else { - parentReader.reset(); - reader = new ColumnFileReader(parentReader, ' '); - } + int markIdx = reader.reset(); + assert markIdx == 1; } } 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 d0ddcc36..1c0b9b37 100644 --- a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileReader.java @@ -34,7 +34,7 @@ private Tiny1FileReader() { } public static List getNamespaces(Reader reader) throws IOException { - return getNamespaces(new ColumnFileReader(reader, '\t')); + return getNamespaces(new ColumnFileReader(reader, '\t', '\t')); } private static List getNamespaces(ColumnFileReader reader) throws IOException { @@ -53,7 +53,7 @@ private static List getNamespaces(ColumnFileReader reader) throws IOExce } public static void read(Reader reader, MappingVisitor visitor) throws IOException { - read(new ColumnFileReader(reader, '\t'), visitor); + read(new ColumnFileReader(reader, '\t', '\t'), visitor); } private static void read(ColumnFileReader reader, MappingVisitor visitor) throws IOException { @@ -137,25 +137,35 @@ private static void read(ColumnFileReader reader, MappingVisitor visitor) throws 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 (property != null) { - visitor.visitMetadata(property, parts[1]); + if (line.startsWith("# ")) { // metadata + if (line.startsWith(prefix.substring(2)) + && (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 (property != null) { + visitor.visitMetadata(property, parts[1]); + } } + } else if (line.isEmpty()) { + throw new IOException("Found indentation without content in line "+reader.getLineNumber()); + } else if (line.startsWith(" ")) { + throw new IOException("Found indentation using spaces in line "+reader.getLineNumber()+", expected tab"); + } else if (line.startsWith("CLASS") + || line.startsWith("FIELD") + || line.startsWith("METHOD")) { + throw new IOException("Found invalid character after element kind declaration in line "+reader.getLineNumber()); } } } @@ -178,6 +188,12 @@ private static void readDstNames(ColumnFileReader reader, MappedElementKind subj if (!name.isEmpty()) visitor.visitDstName(subjectKind, dstNs, name); } + + String col; + + if (!reader.isAtEol() && (col = reader.nextCol()) != null && !col.startsWith("#")) { + throw new IOException("Found invalid additional column in line "+reader.getLineNumber()); + } } static final String nextIntermediaryClassProperty = "next-intermediary-class"; 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 6381d80f..21f70ae4 100644 --- a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileReader.java @@ -31,7 +31,7 @@ private Tiny2FileReader() { } public static List getNamespaces(Reader reader) throws IOException { - return getNamespaces(new ColumnFileReader(reader, '\t')); + return getNamespaces(new ColumnFileReader(reader, '\t', '\t')); } private static List getNamespaces(ColumnFileReader reader) throws IOException { @@ -52,7 +52,7 @@ private static List getNamespaces(ColumnFileReader reader) throws IOExce } public static void read(Reader reader, MappingVisitor visitor) throws IOException { - read(new ColumnFileReader(reader, '\t'), visitor); + read(new ColumnFileReader(reader, '\t', '\t'), visitor); } private static void read(ColumnFileReader reader, MappingVisitor visitor) throws IOException { @@ -95,7 +95,7 @@ private static void read(ColumnFileReader reader, MappingVisitor visitor) throws } 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 + String value = reader.nextCol(true); // may be missing -> null if (key.equals(Tiny2Util.escapedNamesProperty)) { escapeNames = true; @@ -115,6 +115,8 @@ private static void read(ColumnFileReader reader, MappingVisitor visitor) throws if (visitor.visitClass(srcName)) { readClass(reader, dstNsCount, escapeNames, visitor); } + } else { + unrecognizedLine(reader); } } } @@ -151,6 +153,8 @@ private static void readClass(ColumnFileReader reader, int dstNsCount, boolean e } } else if (reader.nextCol("c")) { // comment: c readComment(reader, MappedElementKind.CLASS, visitor); + } else { + unrecognizedLine(reader); } } } @@ -185,6 +189,8 @@ private static void readMethod(ColumnFileReader reader, int dstNsCount, boolean } } else if (reader.nextCol("c")) { // comment: c readComment(reader, MappedElementKind.METHOD, visitor); + } else { + unrecognizedLine(reader); } } } @@ -196,12 +202,14 @@ private static void readElement(ColumnFileReader reader, MappedElementKind kind, while (reader.nextLine(kind.level + 1)) { if (reader.nextCol("c")) { // comment: c readComment(reader, kind, visitor); + } else { + unrecognizedLine(reader); } } } private static void readComment(ColumnFileReader reader, MappedElementKind subjectKind, MappingVisitor visitor) throws IOException { - String comment = reader.nextEscapedCol(); + String comment = reader.nextCol(true); if (comment == null) throw new IOException("missing comment in line "+reader.getLineNumber()); visitor.visitComment(subjectKind, comment); @@ -214,5 +222,19 @@ private static void readDstNames(ColumnFileReader reader, MappedElementKind subj if (!name.isEmpty()) visitor.visitDstName(subjectKind, dstNs, name); } + + String col; + + if (!reader.isAtEol() && (col = reader.nextCol()) != null && !col.startsWith("#")) { + throw new IOException("Found invalid additional column in line "+reader.getLineNumber()); + } + } + + private static void unrecognizedLine(ColumnFileReader reader) throws IOException { + String col = reader.peekCol(false); + + if (col != null && (col.startsWith(" ") || col.substring(1).startsWith(" "))) { + throw new IOException("Found indentation using spaces in line "+reader.getLineNumber()+", expected tab"); + } } } diff --git a/src/main/java/net/fabricmc/mappingio/tree/MemoryMappingTree.java b/src/main/java/net/fabricmc/mappingio/tree/MemoryMappingTree.java index a3515874..2836c320 100644 --- a/src/main/java/net/fabricmc/mappingio/tree/MemoryMappingTree.java +++ b/src/main/java/net/fabricmc/mappingio/tree/MemoryMappingTree.java @@ -709,6 +709,7 @@ public void visitComment(MappedElementKind targetKind, String comment) { abstract static class Entry> implements ElementMapping { protected Entry(MemoryMappingTree tree, String srcName) { + this.tree = tree; this.srcName = srcName; this.dstNames = new String[tree.dstNamespaces.size()]; } @@ -814,6 +815,7 @@ protected void copyFrom(T o, boolean replace) { // TODO: copy args+vars } + protected final MemoryMappingTree tree; protected String srcName; protected String[] dstNames; protected String comment; @@ -822,15 +824,11 @@ protected void copyFrom(T o, boolean replace) { static final class ClassEntry extends Entry implements ClassMapping { ClassEntry(MemoryMappingTree tree, String srcName) { super(tree, srcName); - - this.tree = tree; } ClassEntry(MemoryMappingTree tree, ClassMapping src, int srcNsEquivalent) { super(tree, src, srcNsEquivalent); - this.tree = tree; - for (FieldMapping field : src.getFields()) { addField(field); } @@ -1125,7 +1123,6 @@ public String toString() { private static final byte FLAG_HAS_ANY_METHOD_DESC = 4; private static final byte FLAG_MISSES_ANY_METHOD_DESC = 8; - protected final MemoryMappingTree tree; private Map fields = null; private Map methods = null; private byte flags; diff --git a/src/test/java/net/fabricmc/mappingio/SubsetAssertingVisitor.java b/src/test/java/net/fabricmc/mappingio/SubsetAssertingVisitor.java new file mode 100644 index 00000000..907bc033 --- /dev/null +++ b/src/test/java/net/fabricmc/mappingio/SubsetAssertingVisitor.java @@ -0,0 +1,259 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; + +import net.fabricmc.mappingio.format.MappingFormat; +import net.fabricmc.mappingio.tree.MappingTreeView; +import net.fabricmc.mappingio.tree.MappingTreeView.ClassMappingView; +import net.fabricmc.mappingio.tree.MappingTreeView.FieldMappingView; +import net.fabricmc.mappingio.tree.MappingTreeView.MethodArgMappingView; +import net.fabricmc.mappingio.tree.MappingTreeView.MethodMappingView; +import net.fabricmc.mappingio.tree.MappingTreeView.MethodVarMappingView; + +public class SubsetAssertingVisitor implements FlatMappingVisitor { + public SubsetAssertingVisitor(MappingTreeView supTree, @Nullable MappingFormat supFormat, @Nullable MappingFormat subFormat) { + this.supTree = supTree; + this.supDstNsCount = supTree.getMaxNamespaceId(); + this.subHasNamespaces = subFormat == null ? true : subFormat.hasNamespaces; + this.supHasNamespaces = supFormat == null ? true : supFormat.hasNamespaces; + this.supHasFieldDesc = supFormat == null ? true : supFormat.hasFieldDescriptors; + this.supHasArgs = supFormat == null ? true : supFormat.supportsArgs; + this.supHasVars = supFormat == null ? true : supFormat.supportsLocals; + this.supHasComments = supFormat == null ? true : supFormat.supportsComments; + } + + @Override + public void visitNamespaces(String srcNamespace, List dstNamespaces) throws IOException { + assertTrue(srcNamespace.equals(subHasNamespaces ? supTree.getSrcNamespace() : MappingUtil.NS_SOURCE_FALLBACK)); + this.dstNamespaces = dstNamespaces; + + if (!subHasNamespaces) { + assertTrue(dstNamespaces.size() == 1); + assertTrue(dstNamespaces.get(0).equals(MappingUtil.NS_TARGET_FALLBACK)); + return; + } + + for (int i = 0; i < dstNamespaces.size(); i++) { + String dstNs = dstNamespaces.get(i); + boolean contained = supTree.getDstNamespaces().contains(dstNs); + + if (!supHasNamespaces) { + if (contained) return; + } else { + assertTrue(contained); + } + } + + if (!supHasNamespaces) throw new RuntimeException("SubTree namespace not contained in SupTree"); + } + + @Override + public boolean visitClass(String srcName, String[] dstNames) throws IOException { + ClassMappingView supCls = supTree.getClass(srcName); + Map supDstNamesByNsName = new HashMap<>(); + + if (supCls == null) { + String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; + if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return false; + throw new RuntimeException("SubTree class not contained in SupTree: " + srcName); + } + + for (int supNs = 0; supNs < supDstNsCount; supNs++) { + supDstNamesByNsName.put(supTree.getNamespaceName(supNs), supCls.getDstName(supNs)); + } + + for (int subNs = 0; subNs < dstNames.length; subNs++) { + String supDstName = supDstNamesByNsName.get(dstNamespaces.get(subNs)); + if (!supHasNamespaces && supDstName == null) continue; + assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); + } + + return true; + } + + @Override + public void visitClassComment(String srcName, String[] dstNames, String comment) throws IOException { + if (!supHasComments) return; + assertEquals(supTree.getClass(srcName).getComment(), comment); + } + + @Override + public boolean visitField(String srcClsName, String srcName, String srcDesc, + String[] dstClsNames, String[] dstNames, String[] dstDescs) throws IOException { + FieldMappingView supFld = supTree.getClass(srcClsName).getField(srcName, srcDesc); + Map supDstDataByNsName = new HashMap<>(); + + if (supFld == null) { + String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; + if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return false; + throw new RuntimeException("SubTree field not contained in SupTree: " + srcName); + } + + for (int supNs = 0; supNs < supDstNsCount; supNs++) { + supDstDataByNsName.put(supTree.getNamespaceName(supNs), new String[]{supFld.getDstName(supNs), supFld.getDstDesc(supNs)}); + } + + for (int subNs = 0; subNs < dstNames.length; subNs++) { + String[] supDstData = supDstDataByNsName.get(dstNamespaces.get(subNs)); + if (!supHasNamespaces && supDstData == null) continue; + + String supDstName = supDstData[0]; + assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); + + if (!supHasFieldDesc) continue; + String supDstDesc = supDstData[1]; + assertTrue(dstDescs == null || dstDescs[subNs] == null || dstDescs[subNs].equals(supDstDesc)); + } + + return true; + } + + @Override + public void visitFieldComment(String srcClsName, String srcName, String srcDesc, + String[] dstClsNames, String[] dstNames, String[] dstDescs, String comment) throws IOException { + if (!supHasComments) return; + assertEquals(supTree.getClass(srcClsName).getField(srcName, srcDesc).getComment(), comment); + } + + @Override + public boolean visitMethod(String srcClsName, String srcName, String srcDesc, + String[] dstClsNames, String[] dstNames, String[] dstDescs) throws IOException { + MethodMappingView supMth = supTree.getClass(srcClsName).getMethod(srcName, srcDesc); + Map supDstDataByNsName = new HashMap<>(); + + if (supMth == null) { + String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; + if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return false; + throw new RuntimeException("SubTree method not contained in SupTree: " + srcName); + } + + for (int supNs = 0; supNs < supDstNsCount; supNs++) { + supDstDataByNsName.put(supTree.getNamespaceName(supNs), new String[]{supMth.getDstName(supNs), supMth.getDstDesc(supNs)}); + } + + for (int subNs = 0; subNs < dstNames.length; subNs++) { + String[] supDstData = supDstDataByNsName.get(dstNamespaces.get(subNs)); + if (!supHasNamespaces && supDstData == null) continue; + + String supDstName = supDstData[0]; + assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); + + String supDstDesc = supDstData[1]; + assertTrue(dstDescs == null || dstDescs[subNs] == null || dstDescs[subNs].equals(supDstDesc)); + } + + return true; + } + + @Override + public void visitMethodComment(String srcClsName, String srcName, String srcDesc, + String[] dstClsNames, String[] dstNames, String[] dstDescs, String comment) throws IOException { + if (!supHasComments) return; + assertEquals(supTree.getClass(srcClsName).getMethod(srcName, srcDesc).getComment(), comment); + } + + @Override + public boolean visitMethodArg(String srcClsName, String srcMethodName, String srcMethodDesc, int argPosition, int lvIndex, String srcName, + String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames) throws IOException { + if (!supHasArgs) return false; + MethodArgMappingView supArg = supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getArg(argPosition, lvIndex, srcName); + Map supDstNamesByNsName = new HashMap<>(); + + if (supArg == null) { + String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; + if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return false; + throw new RuntimeException("SubTree arg not contained in SupTree: " + srcName); + } + + for (int supNs = 0; supNs < supDstNsCount; supNs++) { + supDstNamesByNsName.put(supTree.getNamespaceName(supNs), supArg.getDstName(supNs)); + } + + for (int subNs = 0; subNs < dstNames.length; subNs++) { + String supDstName = supDstNamesByNsName.get(dstNamespaces.get(subNs)); + if (!supHasNamespaces && supDstName == null) continue; + assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); + } + + return true; + } + + @Override + public void visitMethodArgComment(String srcClsName, String srcMethodName, String srcMethodDesc, int argPosition, int lvIndex, String srcArgName, + String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames, String comment) throws IOException { + if (!supHasComments) return; + assertEquals(supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getArg(argPosition, lvIndex, srcArgName).getComment(), comment); + } + + @Override + public boolean visitMethodVar(String srcClsName, String srcMethodName, String srcMethodDesc, + int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, String srcName, + String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames) throws IOException { + if (!supHasVars) return false; + MethodVarMappingView supVar = supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getVar(lvtRowIndex, lvIndex, startOpIdx, endOpIdx, srcName); + Map supDstNamesByNsName = new HashMap<>(); + + if (supVar == null) { + String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; + if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return false; + throw new RuntimeException("SubTree var not contained in SupTree: " + srcName); + } + + for (int supNs = 0; supNs < supDstNsCount; supNs++) { + supDstNamesByNsName.put(supTree.getNamespaceName(supNs), supVar.getDstName(supNs)); + } + + for (int subNs = 0; subNs < dstNames.length; subNs++) { + String supDstName = supDstNamesByNsName.get(dstNamespaces.get(subNs)); + if (!supHasNamespaces && supDstName == null) continue; + assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); + } + + return true; + } + + @Override + public void visitMethodVarComment(String srcClsName, String srcMethodName, String srcMethodDesc, + int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, String srcVarName, + String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames, String comment) throws IOException { + if (!supHasComments) return; + assertEquals(supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getVar(lvtRowIndex, lvIndex, startOpIdx, endOpIdx, srcVarName).getComment(), comment); + } + + private final MappingTreeView supTree; + private final int supDstNsCount; + private final boolean subHasNamespaces; + private final boolean supHasNamespaces; + private final boolean supHasFieldDesc; + private final boolean supHasArgs; + private final boolean supHasVars; + private final boolean supHasComments; + private List dstNamespaces; +} + diff --git a/src/test/java/net/fabricmc/mappingio/TestHelper.java b/src/test/java/net/fabricmc/mappingio/TestHelper.java index c563d422..4b1dbabb 100644 --- a/src/test/java/net/fabricmc/mappingio/TestHelper.java +++ b/src/test/java/net/fabricmc/mappingio/TestHelper.java @@ -204,7 +204,7 @@ private static void visitMethod(MemoryMappingTree tree, int... dstNs) { } private static void visitMethodArg(MemoryMappingTree tree, int... dstNs) { - tree.visitMethodArg(counter.getAndIncrement(), counter.getAndIncrement(), nameGen.src(argKind)); + tree.visitMethodArg(nameGen.getCounter().getAndIncrement(), nameGen.getCounter().getAndIncrement(), nameGen.src(argKind)); for (int ns : dstNs) { tree.visitDstName(argKind, ns, nameGen.dst(argKind, ns)); @@ -212,7 +212,8 @@ private static void visitMethodArg(MemoryMappingTree tree, int... dstNs) { } private static void visitMethodVar(MemoryMappingTree tree, int... dstNs) { - tree.visitMethodVar(counter.get(), counter.get(), counter.getAndIncrement(), counter.getAndIncrement(), nameGen.src(varKind)); + tree.visitMethodVar(nameGen.getCounter().get(), nameGen.getCounter().get(), + nameGen.getCounter().getAndIncrement(), nameGen.getCounter().getAndIncrement(), nameGen.src(varKind)); for (int ns : dstNs) { tree.visitDstName(varKind, ns, nameGen.dst(varKind, ns)); @@ -233,6 +234,7 @@ public void reset() { argNum.get().set(0); varNum.get().set(0); nsNum.get().set(0); + counter.get().set(0); } private void resetNsNum() { @@ -300,6 +302,10 @@ public String dst(MappedElementKind kind, int ns) { return sb.toString(); } + public AtomicInteger getCounter() { + return counter.get(); + } + private AtomicInteger getCounter(MappedElementKind kind) { switch (kind) { case CLASS: @@ -348,9 +354,17 @@ private String getPrefix(MappedElementKind kind) { private ThreadLocal argNum = ThreadLocal.withInitial(() -> new AtomicInteger()); private ThreadLocal varNum = ThreadLocal.withInitial(() -> new AtomicInteger()); private ThreadLocal nsNum = ThreadLocal.withInitial(() -> new AtomicInteger()); + private ThreadLocal counter = ThreadLocal.withInitial(() -> new AtomicInteger()); } public static class MappingDirs { + @Nullable + public static MemoryMappingTree getCorrespondingTree(Path dir) { + if (dir.equals(VALID)) return createTestTree(); + if (dir.equals(VALID_WITH_HOLES)) return createTestTreeWithHoles(); + return null; + } + public static final Path DETECTION = getResource("/detection/"); public static final Path VALID = getResource("/read/valid/"); public static final Path VALID_WITH_HOLES = getResource("/read/valid-with-holes/"); @@ -360,7 +374,6 @@ public static class MappingDirs { private static final String mthDesc = "()I"; private static final String comment = "This is a comment"; private static final NameGen nameGen = new NameGen(); - private static final AtomicInteger counter = new AtomicInteger(); private static final MappedElementKind clsKind = MappedElementKind.CLASS; private static final MappedElementKind fldKind = MappedElementKind.FIELD; private static final MappedElementKind mthKind = MappedElementKind.METHOD; diff --git a/src/test/java/net/fabricmc/mappingio/read/InvalidContentReadTest.java b/src/test/java/net/fabricmc/mappingio/read/InvalidContentReadTest.java new file mode 100644 index 00000000..e2d250b8 --- /dev/null +++ b/src/test/java/net/fabricmc/mappingio/read/InvalidContentReadTest.java @@ -0,0 +1,281 @@ +/* + * 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.read; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.StringReader; + +import org.junit.jupiter.api.Test; + +import net.fabricmc.mappingio.MappedElementKind; +import net.fabricmc.mappingio.MappingReader; +import net.fabricmc.mappingio.NopMappingVisitor; +import net.fabricmc.mappingio.format.MappingFormat; + +public class InvalidContentReadTest { + String tinyHeader = "v1 source target\n"; + String tiny2Header = "tiny 2 0 source target\n"; + + @Test + public void enigmaFile() throws Exception { + MappingFormat format = MappingFormat.ENIGMA_FILE; + + checkThrows(" ", format); + checkThrows(" ", format); + checkThrows(" CLASS", format); + + checkEnigmaLine(MappedElementKind.CLASS); + checkEnigmaLine(MappedElementKind.FIELD); + checkEnigmaLine(MappedElementKind.METHOD); + // TODO: args + } + + private void checkEnigmaLine(MappedElementKind kind) throws Exception { + MappingFormat format = MappingFormat.ENIGMA_FILE; + String prefix = (kind == MappedElementKind.CLASS ? "" : " ") + kind.name(); + + // Tabs for separation + checkThrows(prefix + " ", format); + checkThrows(prefix + " src", format); + + // Spaces for separation + prefix += " src"; + String suffix = ""; + + if (kind != MappedElementKind.CLASS) { + prefix = "CLASS src\n" + prefix; + suffix += kind == MappedElementKind.FIELD ? " I" : " ()V"; + } + + check(prefix, format, kind != MappedElementKind.CLASS); + checkWorks(prefix + suffix, format); + + checkThrows(prefix + " ", format); + checkThrows(prefix + " " + suffix, format); + + check(prefix + " dst", format, kind == MappedElementKind.METHOD); // field normally too, but doesn't have descriptor validation yet + checkWorks(prefix + " dst" + suffix, format); + + checkThrows(prefix + " dst ", format); + checkThrows(prefix + " dst " + suffix, format); + + check(prefix + " dst dst2", format, kind != MappedElementKind.FIELD); + checkThrows(prefix + " dst dst2" + suffix, format); + } + + @Test + public void tinyFile() throws Exception { + MappingFormat format = MappingFormat.TINY_FILE; + + checkThrows(" ", format); + checkWorks(tinyHeader, format); + checkThrows(tinyHeader + " ", format); + checkThrows(tinyHeader + " ", format); + + checkTinyLine(MappedElementKind.CLASS); + checkTinyLine(MappedElementKind.FIELD); + checkTinyLine(MappedElementKind.METHOD); + // TODO: args, vars + } + + private void checkTinyLine(MappedElementKind kind) throws Exception { + MappingFormat format = MappingFormat.TINY_FILE; + String prefix = tinyHeader + kind.name(); + + // No source/target + checkThrows(prefix, format); + + // Spaces for separation + checkThrows(prefix + " ", format); + checkThrows(prefix + " src", format); + + // Tabs for separation + prefix += " "; + checkThrows(prefix, format); + prefix += "src"; + checkThrows(prefix, format); + + if (kind != MappedElementKind.CLASS) { + prefix += " "; + checkThrows(prefix, format); + + prefix += kind == MappedElementKind.FIELD ? "I" : "()V"; + checkThrows(prefix, format); + + prefix += " src"; + checkThrows(prefix, format); + } + + checkWorks(prefix + " ", format); + checkWorks(prefix + " dst", format); + checkThrows(prefix + " dst ", format); + checkThrows(prefix + " dst dst2", format); + } + + @Test + public void tinyV2File() throws Exception { + MappingFormat format = MappingFormat.TINY_2_FILE; + + checkThrows(" ", format); + checkWorks(tiny2Header, format); + checkThrows(tiny2Header + " ", format); + checkThrows(tiny2Header + " ", format); + + checkTiny2Line(MappedElementKind.CLASS); + checkTiny2Line(MappedElementKind.FIELD); + checkTiny2Line(MappedElementKind.METHOD); + // TODO: args, vars + } + + private void checkTiny2Line(MappedElementKind kind) throws Exception { + MappingFormat format = MappingFormat.TINY_2_FILE; + String prefix = tiny2Header; + + if (kind == MappedElementKind.CLASS) { + prefix += "c"; + } else { + prefix += "c src \n " + (kind == MappedElementKind.FIELD ? "f" : "m"); + } + + // No source/target + checkThrows(prefix, format); + + // Spaces for separation + checkThrows(prefix + " ", format); + checkThrows(prefix + " src", format); + + // Tabs for separation + if (kind != MappedElementKind.CLASS) { + checkThrows(prefix, format); + prefix += " "; + checkThrows(prefix, format); + + prefix += kind == MappedElementKind.FIELD ? "I" : "()V"; + checkThrows(prefix, format); + } + + prefix += " "; + checkThrows(prefix, format); + prefix += "src"; + + checkThrows(prefix, format); + checkWorks(prefix + " ", format); + checkWorks(prefix + " dst", format); + checkThrows(prefix + " dst ", format); + checkThrows(prefix + " dst dst2", format); + } + + @Test + public void srgFile() throws Exception { + MappingFormat format = MappingFormat.SRG_FILE; + + checkThrows(" ", format); + checkThrows(" ", format); + + // TODO: packages + checkSrgLine(MappedElementKind.CLASS, format); + checkSrgLine(MappedElementKind.FIELD, format); + checkSrgLine(MappedElementKind.METHOD, format); + } + + @Test + public void xsrgFile() throws Exception { + MappingFormat format = MappingFormat.XSRG_FILE; + + checkThrows(" ", format); + checkThrows(" ", format); + + // TODO: packages + checkSrgLine(MappedElementKind.CLASS, format); + checkSrgLine(MappedElementKind.FIELD, format); + checkSrgLine(MappedElementKind.METHOD, format); + } + + private void checkSrgLine(MappedElementKind kind, MappingFormat format) throws Exception { + String prefix; + + if (kind == MappedElementKind.CLASS) { + prefix = "CL:"; + } else { + prefix = (kind == MappedElementKind.FIELD ? "FD:" : "MD:"); + } + + // No source/target + checkThrows(prefix, format); + + // Tabs for separation + checkThrows(prefix + " ", format); + checkThrows(prefix + " src", format); + checkThrows(prefix + " src dst", format); + + // Spaces for separation + prefix += " "; + checkThrows(prefix, format); + prefix += "src"; + checkThrows(prefix, format); + String suffix = ""; + + if (kind != MappedElementKind.CLASS) { + prefix += "/"; + checkThrows(prefix, format); + + prefix += "src"; + checkThrows(prefix, format); + + if (kind == MappedElementKind.METHOD || format == MappingFormat.XSRG_FILE) { + prefix += " "; + checkThrows(prefix, format); + + prefix += kind == MappedElementKind.FIELD ? "I" : "()V"; + checkThrows(prefix, format); + } + + prefix += " dst/"; + checkThrows(prefix, format); + + if (kind == MappedElementKind.METHOD) { + suffix += " ()V"; + } else if (format == MappingFormat.XSRG_FILE) { + suffix += " I"; + } + } else { + prefix += " "; + } + + checkThrows(prefix + "" + suffix, format); + checkWorks(prefix + "dst" + suffix, format); + checkThrows(prefix + "dst" + suffix + " ", format); + checkThrows(prefix + "dst" + suffix + " dst2", format); + } + + private void check(String fileContent, MappingFormat format, boolean shouldThrow) throws Exception { + if (shouldThrow) { + checkThrows(fileContent, format); + } else { + checkWorks(fileContent, format); + } + } + + private void checkWorks(String fileContent, MappingFormat format) throws Exception { + MappingReader.read(new StringReader(fileContent), format, new NopMappingVisitor(true)); + } + + private void checkThrows(String fileContent, MappingFormat format) { + assertThrows(Exception.class, () -> checkWorks(fileContent, format)); + } +} diff --git a/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java b/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java index ec8e6889..fe1a4279 100644 --- a/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java +++ b/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java @@ -16,32 +16,16 @@ package net.fabricmc.mappingio.read; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import net.fabricmc.mappingio.FlatMappingVisitor; import net.fabricmc.mappingio.MappingReader; -import net.fabricmc.mappingio.MappingUtil; +import net.fabricmc.mappingio.SubsetAssertingVisitor; import net.fabricmc.mappingio.TestHelper; import net.fabricmc.mappingio.adapter.FlatAsRegularMappingVisitor; import net.fabricmc.mappingio.format.MappingFormat; import net.fabricmc.mappingio.tree.MappingTree; -import net.fabricmc.mappingio.tree.MappingTree.ClassMapping; -import net.fabricmc.mappingio.tree.MappingTree.FieldMapping; -import net.fabricmc.mappingio.tree.MappingTree.MethodArgMapping; -import net.fabricmc.mappingio.tree.MappingTree.MethodMapping; -import net.fabricmc.mappingio.tree.MappingTree.MethodVarMapping; import net.fabricmc.mappingio.tree.MemoryMappingTree; import net.fabricmc.mappingio.tree.VisitableMappingTree; @@ -146,210 +130,6 @@ private VisitableMappingTree checkHoles(MappingFormat format) throws Exception { } private void assertSubset(MappingTree subTree, @Nullable MappingFormat subFormat, MappingTree supTree, @Nullable MappingFormat supFormat) throws Exception { - int supDstNsCount = supTree.getMaxNamespaceId(); - boolean subHasNamespaces = subFormat == null ? true : subFormat.hasNamespaces; - boolean supHasNamespaces = supFormat == null ? true : supFormat.hasNamespaces; - boolean supHasFieldDesc = supFormat == null ? true : supFormat.hasFieldDescriptors; - boolean supHasArgs = supFormat == null ? true : supFormat.supportsArgs; - boolean supHasVars = supFormat == null ? true : supFormat.supportsLocals; - boolean supHasComments = supFormat == null ? true : supFormat.supportsComments; - - subTree.accept(new FlatAsRegularMappingVisitor(new FlatMappingVisitor() { - @Override - public void visitNamespaces(String srcNamespace, List dstNamespaces) throws IOException { - assertTrue(srcNamespace.equals(subHasNamespaces ? supTree.getSrcNamespace() : MappingUtil.NS_SOURCE_FALLBACK)); - - if (!subHasNamespaces) { - assertTrue(dstNamespaces.size() == 1); - assertTrue(dstNamespaces.get(0).equals(MappingUtil.NS_TARGET_FALLBACK)); - return; - } - - for (String dstNs : dstNamespaces) { - boolean contained = supTree.getDstNamespaces().contains(dstNs); - - if (!supHasNamespaces) { - if (contained) return; - } else { - assertTrue(contained); - } - } - - if (!supHasNamespaces) throw new RuntimeException("SubTree namespace not contained in SupTree"); - } - - @Override - public boolean visitClass(String srcName, String[] dstNames) throws IOException { - ClassMapping supCls = supTree.getClass(srcName); - Map supDstNamesByNsName = new HashMap<>(); - - if (supCls == null) { - String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; - if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return false; - throw new RuntimeException("SubTree class not contained in SupTree: " + srcName); - } - - for (int supNs = 0; supNs < supDstNsCount; supNs++) { - supDstNamesByNsName.put(supTree.getNamespaceName(supNs), supCls.getDstName(supNs)); - } - - for (int subNs = 0; subNs < dstNames.length; subNs++) { - String supDstName = supDstNamesByNsName.get(subTree.getNamespaceName(subNs)); - if (!supHasNamespaces && supDstName == null) continue; - assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); - } - - return true; - } - - @Override - public void visitClassComment(String srcName, String[] dstNames, String comment) throws IOException { - if (!supHasComments) return; - assertEquals(supTree.getClass(srcName).getComment(), comment); - } - - @Override - public boolean visitField(String srcClsName, String srcName, String srcDesc, - String[] dstClsNames, String[] dstNames, String[] dstDescs) throws IOException { - FieldMapping supFld = supTree.getClass(srcClsName).getField(srcName, srcDesc); - Map supDstDataByNsName = new HashMap<>(); - - if (supFld == null) { - String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; - if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return false; - throw new RuntimeException("SubTree field not contained in SupTree: " + srcName); - } - - for (int supNs = 0; supNs < supDstNsCount; supNs++) { - supDstDataByNsName.put(supTree.getNamespaceName(supNs), new String[]{supFld.getDstName(supNs), supFld.getDstDesc(supNs)}); - } - - for (int subNs = 0; subNs < dstNames.length; subNs++) { - String[] supDstData = supDstDataByNsName.get(subTree.getNamespaceName(subNs)); - if (!supHasNamespaces && supDstData == null) continue; - - String supDstName = supDstData[0]; - assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); - - if (!supHasFieldDesc) continue; - String supDstDesc = supDstData[1]; - assertTrue(dstDescs == null || dstDescs[subNs] == null || dstDescs[subNs].equals(supDstDesc)); - } - - return true; - } - - @Override - public void visitFieldComment(String srcClsName, String srcName, String srcDesc, - String[] dstClsNames, String[] dstNames, String[] dstDescs, String comment) throws IOException { - if (!supHasComments) return; - assertEquals(supTree.getClass(srcClsName).getField(srcName, srcDesc).getComment(), comment); - } - - @Override - public boolean visitMethod(String srcClsName, String srcName, String srcDesc, - String[] dstClsNames, String[] dstNames, String[] dstDescs) throws IOException { - MethodMapping supMth = supTree.getClass(srcClsName).getMethod(srcName, srcDesc); - Map supDstDataByNsName = new HashMap<>(); - - if (supMth == null) { - String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; - if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return false; - throw new RuntimeException("SubTree method not contained in SupTree: " + srcName); - } - - for (int supNs = 0; supNs < supDstNsCount; supNs++) { - supDstDataByNsName.put(supTree.getNamespaceName(supNs), new String[]{supMth.getDstName(supNs), supMth.getDstDesc(supNs)}); - } - - for (int subNs = 0; subNs < dstNames.length; subNs++) { - String[] supDstData = supDstDataByNsName.get(subTree.getNamespaceName(subNs)); - if (!supHasNamespaces && supDstData == null) continue; - - String supDstName = supDstData[0]; - assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); - - String supDstDesc = supDstData[1]; - assertTrue(dstDescs == null || dstDescs[subNs] == null || dstDescs[subNs].equals(supDstDesc)); - } - - return true; - } - - @Override - public void visitMethodComment(String srcClsName, String srcName, String srcDesc, - String[] dstClsNames, String[] dstNames, String[] dstDescs, String comment) throws IOException { - if (!supHasComments) return; - assertEquals(supTree.getClass(srcClsName).getMethod(srcName, srcDesc).getComment(), comment); - } - - @Override - public boolean visitMethodArg(String srcClsName, String srcMethodName, String srcMethodDesc, int argPosition, int lvIndex, String srcName, - String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames) throws IOException { - if (!supHasArgs) return false; - MethodArgMapping supArg = supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getArg(argPosition, lvIndex, srcName); - Map supDstNamesByNsName = new HashMap<>(); - - if (supArg == null) { - String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; - if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return false; - throw new RuntimeException("SubTree arg not contained in SupTree: " + srcName); - } - - for (int supNs = 0; supNs < supDstNsCount; supNs++) { - supDstNamesByNsName.put(supTree.getNamespaceName(supNs), supArg.getDstName(supNs)); - } - - for (int subNs = 0; subNs < dstNames.length; subNs++) { - String supDstName = supDstNamesByNsName.get(subTree.getNamespaceName(subNs)); - if (!supHasNamespaces && supDstName == null) continue; - assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); - } - - return true; - } - - @Override - public void visitMethodArgComment(String srcClsName, String srcMethodName, String srcMethodDesc, int argPosition, int lvIndex, String srcArgName, - String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames, String comment) throws IOException { - if (!supHasComments) return; - assertEquals(supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getArg(argPosition, lvIndex, srcArgName).getComment(), comment); - } - - @Override - public boolean visitMethodVar(String srcClsName, String srcMethodName, String srcMethodDesc, - int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, String srcName, - String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames) throws IOException { - if (!supHasVars) return false; - MethodVarMapping supVar = supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getVar(lvtRowIndex, lvIndex, startOpIdx, endOpIdx, srcName); - Map supDstNamesByNsName = new HashMap<>(); - - if (supVar == null) { - String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; - if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return false; - throw new RuntimeException("SubTree var not contained in SupTree: " + srcName); - } - - for (int supNs = 0; supNs < supDstNsCount; supNs++) { - supDstNamesByNsName.put(supTree.getNamespaceName(supNs), supVar.getDstName(supNs)); - } - - for (int subNs = 0; subNs < dstNames.length; subNs++) { - String supDstName = supDstNamesByNsName.get(subTree.getNamespaceName(subNs)); - if (!supHasNamespaces && supDstName == null) continue; - assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); - } - - return true; - } - - @Override - public void visitMethodVarComment(String srcClsName, String srcMethodName, String srcMethodDesc, - int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, String srcVarName, - String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames, String comment) throws IOException { - if (!supHasComments) return; - assertEquals(supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getVar(lvtRowIndex, lvIndex, startOpIdx, endOpIdx, srcVarName).getComment(), comment); - } - })); + subTree.accept(new FlatAsRegularMappingVisitor(new SubsetAssertingVisitor(supTree, supFormat, subFormat))); } } diff --git a/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java b/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java index e0266869..e2db8117 100644 --- a/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java +++ b/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java @@ -21,152 +21,269 @@ import java.io.IOException; import java.nio.file.Path; import java.util.EnumSet; -import java.util.HashSet; import java.util.List; import java.util.Set; import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import net.fabricmc.mappingio.MappedElementKind; import net.fabricmc.mappingio.MappingFlag; import net.fabricmc.mappingio.MappingReader; import net.fabricmc.mappingio.MappingVisitor; +import net.fabricmc.mappingio.SubsetAssertingVisitor; import net.fabricmc.mappingio.TestHelper; +import net.fabricmc.mappingio.adapter.FlatAsRegularMappingVisitor; import net.fabricmc.mappingio.format.MappingFormat; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MappingTreeView; +import net.fabricmc.mappingio.tree.MemoryMappingTree; public class VisitEndTest { - private static Set dirs = new HashSet<>(); + @Test + public void enigmaFile() throws Exception { + MappingFormat format = MappingFormat.ENIGMA_FILE; + check(format); + } + + @Test + public void enigmaDirectory() throws Exception { + MappingFormat format = MappingFormat.ENIGMA_DIR; + check(format); + } + + @Test + public void tinyFile() throws Exception { + MappingFormat format = MappingFormat.TINY_FILE; + check(format); + } + + @Test + public void tinyV2File() throws Exception { + MappingFormat format = MappingFormat.TINY_2_FILE; + check(format); + } - @BeforeAll - public static void setup() throws Exception { - dirs.add(TestHelper.MappingDirs.DETECTION); - dirs.add(TestHelper.MappingDirs.VALID); - dirs.add(TestHelper.MappingDirs.VALID_WITH_HOLES); + @Test + public void srgFile() throws Exception { + MappingFormat format = MappingFormat.SRG_FILE; + check(format); } @Test - public void testVisitEnd() throws Exception { - for (MappingFormat format : MappingFormat.values()) { - String filename = TestHelper.getFileName(format); - if (filename == null) continue; + public void xrgFile() throws Exception { + MappingFormat format = MappingFormat.XSRG_FILE; + check(format); + } - for (Path dir : dirs) { - MappingReader.read(dir.resolve(filename), format, new VisitEndTestVisitor(1, true)); - MappingReader.read(dir.resolve(filename), format, new VisitEndTestVisitor(1, false)); + @Test + public void csrgFile() throws Exception { + MappingFormat format = MappingFormat.CSRG_FILE; + check(format); + } - VisitEndTestVisitor threePassVisitor = new VisitEndTestVisitor(2, true); - MappingReader.read(dir.resolve(filename), format, threePassVisitor); - assertTrue(threePassVisitor.finishedVisitPassCount == threePassVisitor.visitPassCountToFinish); + @Test + public void tsrgFile() throws Exception { + MappingFormat format = MappingFormat.TSRG_FILE; + check(format); + } - threePassVisitor = new VisitEndTestVisitor(2, false); + @Test + public void tsrg2File() throws Exception { + MappingFormat format = MappingFormat.TSRG_2_FILE; + check(format); + } - try { - MappingReader.read(dir.resolve(filename), format, threePassVisitor); - } catch (Exception e) { - continue; // Reader doesn't support multiple passes without NEEDS_MULTIPLE_PASSES - } + @Test + public void proguardFile() throws Exception { + MappingFormat format = MappingFormat.PROGUARD_FILE; + check(format); + } - // Reader didn't throw an exception, make sure it actually behaved as expected - assertTrue(threePassVisitor.finishedVisitPassCount == threePassVisitor.visitPassCountToFinish); - } + private void check(MappingFormat format) throws Exception { + checkDir(TestHelper.MappingDirs.DETECTION, format); + checkDir(TestHelper.MappingDirs.VALID, format); + checkDir(TestHelper.MappingDirs.VALID_WITH_HOLES, format); + } + + private void checkDir(Path dir, MappingFormat format) throws Exception { + Path path = dir.resolve(TestHelper.getFileName(format)); + MappingTreeView supTree = TestHelper.MappingDirs.getCorrespondingTree(dir); + + checkCompliance(format, path, 1, true, supTree); + checkCompliance(format, path, 1, false, supTree); + + checkCompliance(format, path, 2, true, supTree); + checkCompliance(format, path, 3, true, supTree); + + VisitEndTestVisitor nonFlaggedVisitor; + + try { + nonFlaggedVisitor = checkCompliance(format, path, 2, false, supTree); + } catch (Exception e) { + return; // Reader doesn't support multiple passes without NEEDS_MULTIPLE_PASSES } + + // Reader didn't throw an exception, make sure it actually behaved as expected + assertTrue(nonFlaggedVisitor.finishedVisitPassCount == nonFlaggedVisitor.visitPassCountToFinish); + } + + private VisitEndTestVisitor checkCompliance(MappingFormat format, Path path, int visitPassCountToFinish, boolean setFlag, MappingTreeView supTree) throws Exception { + VisitEndTestVisitor visitor = new VisitEndTestVisitor(visitPassCountToFinish, setFlag, supTree, format); + MappingReader.read(path, format, visitor); + assertTrue(visitor.finishedVisitPassCount == visitPassCountToFinish); + return visitor; } private static class VisitEndTestVisitor implements MappingVisitor { - private VisitEndTestVisitor(int visitPassCountToFinish, boolean setFlag) { + private VisitEndTestVisitor(int visitPassCountToFinish, boolean setFlag, MappingTreeView supTree, MappingFormat subFormat) { this.visitPassCountToFinish = visitPassCountToFinish; this.setFlag = setFlag; + this.supTree = supTree; + this.subFormat = subFormat; + this.tree = new MemoryMappingTree(); + this.oldTrees = new MappingTree[visitPassCountToFinish - 1]; } @Override public Set getFlags() { return setFlag ? EnumSet.of(MappingFlag.NEEDS_MULTIPLE_PASSES) - : MappingVisitor.super.getFlags(); + : MappingFlag.NONE; } @Override public boolean visitHeader() throws IOException { check(); + tree.visitHeader(); return true; } @Override public void visitNamespaces(String srcNamespace, List dstNamespaces) throws IOException { check(); + tree.visitNamespaces(srcNamespace, dstNamespaces); + } + + @Override + public void visitMetadata(String key, @Nullable String value) throws IOException { + check(); + tree.visitMetadata(key, value); } @Override public boolean visitContent() throws IOException { check(); + tree.visitContent(); return true; } @Override public boolean visitClass(String srcName) throws IOException { check(); + tree.visitClass(srcName); return true; } @Override public boolean visitField(String srcName, @Nullable String srcDesc) throws IOException { check(); + tree.visitField(srcName, srcDesc); return true; } @Override public boolean visitMethod(String srcName, @Nullable String srcDesc) throws IOException { check(); + tree.visitMethod(srcName, srcDesc); return true; } @Override public boolean visitMethodArg(int argPosition, int lvIndex, @Nullable String srcName) throws IOException { check(); + tree.visitMethodArg(argPosition, lvIndex, srcName); return true; } @Override public boolean visitMethodVar(int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, @Nullable String srcName) throws IOException { check(); + tree.visitMethodVar(lvtRowIndex, lvIndex, startOpIdx, endOpIdx, srcName); return true; } @Override public void visitDstName(MappedElementKind targetKind, int namespace, String name) throws IOException { check(); + tree.visitDstName(targetKind, namespace, name); } @Override public void visitDstDesc(MappedElementKind targetKind, int namespace, String desc) throws IOException { check(); + tree.visitDstDesc(targetKind, namespace, desc); } @Override public boolean visitElementContent(MappedElementKind targetKind) throws IOException { check(); + tree.visitElementContent(targetKind); return true; } @Override public void visitComment(MappedElementKind targetKind, String comment) throws IOException { check(); + tree.visitComment(targetKind, comment); } @Override public boolean visitEnd() throws IOException { + check(); + tree.visitEnd(); + checkContent(); finishedVisitPassCount++; - return finishedVisitPassCount == visitPassCountToFinish; + + if (finishedVisitPassCount == visitPassCountToFinish) { + return true; + } + + oldTrees[finishedVisitPassCount - 1] = new MemoryMappingTree(tree); + tree = new MemoryMappingTree(); + return false; } private void check() { assertTrue(finishedVisitPassCount < visitPassCountToFinish); } + /** + * Ensures every visit pass contains the same content. + */ + private void checkContent() throws IOException { + MappingTreeView subTree, supTree; + MappingFormat supFormat = null; + + if (finishedVisitPassCount == 0) { + if (this.supTree == null) return; + supTree = this.supTree; + } else { + supTree = oldTrees[finishedVisitPassCount - 1]; + if (finishedVisitPassCount > 1) supFormat = subFormat; + } + + subTree = tree; + subTree.accept(new FlatAsRegularMappingVisitor(new SubsetAssertingVisitor(supTree, supFormat, subFormat))); + supTree.accept(new FlatAsRegularMappingVisitor(new SubsetAssertingVisitor(subTree, subFormat, supFormat))); + } + private final int visitPassCountToFinish; private final boolean setFlag; + private final MappingTreeView supTree; + private final MappingFormat subFormat; + private final MappingTree[] oldTrees; private int finishedVisitPassCount; + private MemoryMappingTree tree; } } diff --git a/src/test/resources/read/valid-with-holes/enigma-dir/class_32.mapping b/src/test/resources/read/valid-with-holes/enigma-dir/class_32.mapping index 22751d60..6a269a1e 100644 --- a/src/test/resources/read/valid-with-holes/enigma-dir/class_32.mapping +++ b/src/test/resources/read/valid-with-holes/enigma-dir/class_32.mapping @@ -18,13 +18,13 @@ CLASS class_32 METHOD method_6 ()I COMMENT This is a comment METHOD method_7 ()I - ARG 5 + ARG 1 + ARG 3 + ARG 5 param3Ns0Rename ARG 7 - ARG 9 param3Ns0Rename - ARG 11 COMMENT This is a comment - ARG 13 param5Ns0Rename + ARG 9 param5Ns0Rename COMMENT This is a comment - ARG 15 + ARG 11 COMMENT This is a comment METHOD method_8 ()I diff --git a/src/test/resources/read/valid-with-holes/enigma.mappings b/src/test/resources/read/valid-with-holes/enigma.mappings index 059a030e..f40fb44f 100644 --- a/src/test/resources/read/valid-with-holes/enigma.mappings +++ b/src/test/resources/read/valid-with-holes/enigma.mappings @@ -58,13 +58,13 @@ CLASS class_32 METHOD method_6 ()I COMMENT This is a comment METHOD method_7 ()I - ARG 5 + ARG 1 + ARG 3 + ARG 5 param3Ns0Rename ARG 7 - ARG 9 param3Ns0Rename - ARG 11 COMMENT This is a comment - ARG 13 param5Ns0Rename + ARG 9 param5Ns0Rename COMMENT This is a comment - ARG 15 + ARG 11 COMMENT This is a comment METHOD method_8 ()I diff --git a/src/test/resources/read/valid-with-holes/tinyV2.tiny b/src/test/resources/read/valid-with-holes/tinyV2.tiny index 19c97d02..bc6c49fe 100644 --- a/src/test/resources/read/valid-with-holes/tinyV2.tiny +++ b/src/test/resources/read/valid-with-holes/tinyV2.tiny @@ -46,22 +46,22 @@ c class_32 m ()I method_6 method6Ns1Rename c This is a comment m ()I method_7 - p 5 param_1 - p 7 param_2 param2Ns1Rename - p 9 param_3 param3Ns0Rename - p 11 param_4 + p 1 param_1 + p 3 param_2 param2Ns1Rename + p 5 param_3 param3Ns0Rename + p 7 param_4 c This is a comment - p 13 param_5 param5Ns0Rename + p 9 param_5 param5Ns0Rename c This is a comment - p 15 param_6 param6Ns1Rename + p 11 param_6 param6Ns1Rename c This is a comment m ()I method_8 - v 16 16 16 var_1 - v 18 18 18 var_2 var2Ns1Rename - v 20 20 20 var_3 var3Ns0Rename - v 22 22 22 var_4 + v 12 12 12 var_1 + v 14 14 14 var_2 var2Ns1Rename + v 16 16 16 var_3 var3Ns0Rename + v 18 18 18 var_4 c This is a comment - v 24 24 24 var_5 var5Ns0Rename + v 20 20 20 var_5 var5Ns0Rename c This is a comment - v 26 26 26 var_6 var6Ns1Rename + v 22 22 22 var_6 var6Ns1Rename c This is a comment diff --git a/src/test/resources/read/valid-with-holes/tsrg2.tsrg b/src/test/resources/read/valid-with-holes/tsrg2.tsrg index c656ad3d..3fd2de0e 100644 --- a/src/test/resources/read/valid-with-holes/tsrg2.tsrg +++ b/src/test/resources/read/valid-with-holes/tsrg2.tsrg @@ -31,10 +31,10 @@ class_32 class_32 class_32 method_5 ()I method5Ns0Rename method_5 method_6 ()I method_6 method6Ns1Rename method_7 ()I method_7 method_7 - 5 param_1 param_1 param_1 - 7 param_2 param_2 param2Ns1Rename - 9 param_3 param3Ns0Rename param_3 - 11 param_4 param_4 param_4 - 13 param_5 param5Ns0Rename param_5 - 15 param_6 param_6 param6Ns1Rename + 1 param_1 param_1 param_1 + 3 param_2 param_2 param2Ns1Rename + 5 param_3 param3Ns0Rename param_3 + 7 param_4 param_4 param_4 + 9 param_5 param5Ns0Rename param_5 + 11 param_6 param_6 param6Ns1Rename method_8 ()I method_8 method_8