From c793aa2429aaceb367ed2701c09c64d062882382 Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Sun, 30 Apr 2023 10:19:38 +0200 Subject: [PATCH 1/2] Add checkstyle and update README Also adds an editorconfig, enhances the gitignore and updates the broken licenser plugin. --- .editorconfig | 25 + .gitignore | 31 +- README.md | 3 + build.gradle | 9 +- checkstyle.xml | 178 +++ gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 61574 bytes gradle/wrapper/gradle-wrapper.properties | 11 +- gradlew | 269 +++-- gradlew.bat | 34 +- settings.gradle | 1 - .../java/net/fabricmc/stitch/Command.java | 14 +- src/main/java/net/fabricmc/stitch/Main.java | 115 +- .../stitch/commands/CommandAsmTrace.java | 48 +- .../commands/CommandGenerateIntermediary.java | 100 +- .../CommandGeneratePrefixRemapper.java | 18 +- .../stitch/commands/CommandMatcherToTiny.java | 31 +- .../stitch/commands/CommandMergeJar.java | 107 +- .../stitch/commands/CommandMergeTiny.java | 69 +- .../commands/CommandProposeFieldNames.java | 211 ++-- .../stitch/commands/CommandReorderTiny.java | 202 ++-- .../commands/CommandRewriteIntermediary.java | 87 +- .../commands/CommandUpdateIntermediary.java | 117 +- .../commands/CommandValidateRecords.java | 7 +- .../net/fabricmc/stitch/commands/GenMap.java | 228 ++-- .../fabricmc/stitch/commands/GenState.java | 1033 +++++++++-------- .../commands/tinyv2/CommandMergeTinyV2.java | 92 +- .../tinyv2/CommandProposeV2FieldNames.java | 42 +- .../commands/tinyv2/CommandReorderTinyV2.java | 99 +- .../stitch/commands/tinyv2/TinyClass.java | 4 +- .../stitch/commands/tinyv2/TinyField.java | 1 - .../stitch/commands/tinyv2/TinyHeader.java | 3 +- .../commands/tinyv2/TinyLocalVariable.java | 6 +- .../stitch/commands/tinyv2/TinyMethod.java | 3 +- .../stitch/commands/tinyv2/TinyV2Reader.java | 51 +- .../stitch/commands/tinyv2/TinyV2Writer.java | 19 +- .../stitch/enigma/StitchEnigmaPlugin.java | 2 - ...tchIntermediaryObfuscationTestService.java | 1 + .../enigma/StitchNameProposalService.java | 19 +- .../fabricmc/stitch/merge/ClassMerger.java | 422 +++---- .../net/fabricmc/stitch/merge/JarMerger.java | 411 +++---- .../representation/AbstractJarEntry.java | 74 +- .../stitch/representation/Access.java | 35 +- .../representation/ClassPropagationTree.java | 78 +- .../stitch/representation/ClassStorage.java | 2 +- .../stitch/representation/JarClassEntry.java | 390 ++++--- .../stitch/representation/JarFieldEntry.java | 58 +- .../stitch/representation/JarMethodEntry.java | 217 ++-- .../stitch/representation/JarReader.java | 580 ++++----- .../stitch/representation/JarRootEntry.java | 129 +- .../fabricmc/stitch/util/FieldNameFinder.java | 50 +- .../net/fabricmc/stitch/util/MatcherUtil.java | 121 +- .../stitch/util/NameFinderVisitor.java | 13 +- .../java/net/fabricmc/stitch/util/Pair.java | 90 +- .../fabricmc/stitch/util/RecordValidator.java | 33 +- .../stitch/util/SnowmanClassVisitor.java | 2 + .../net/fabricmc/stitch/util/StitchUtil.java | 209 ++-- .../util/SyntheticParameterClassVisitor.java | 154 +-- .../net/fabricmc/stitch/tinyv1/Commands.java | 12 +- .../net/fabricmc/stitch/tinyv2/Commands.java | 4 - .../fabricmc/stitch/tinyv2/Snapshot37a.java | 25 +- .../fabricmc/stitch/tinyv2/Stable1_14_4.java | 40 +- .../stitch/tinyv2/TestTinyV2ReadAndWrite.java | 5 +- .../fabricmc/stitch/tinyv2/TestToString.java | 13 +- 63 files changed, 3471 insertions(+), 2986 deletions(-) create mode 100644 .editorconfig create mode 100644 checkstyle.xml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8eca64d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# FabricMC editorconfig v1.0 + +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{gradle,java}] +ij_continuation_indent_size = 8 +ij_java_imports_layout = $*,|,java.**,|,javax.**,|,*,|,net.fabricmc.** +ij_java_class_count_to_use_import_on_demand = 999 + +[*.{yml,yaml,json,toml,mcmeta}] +indent_size = 2 + +[*.{yml,yaml,properties}] +indent_style = space + +[*.bat] +end_of_line = crlf diff --git a/.gitignore b/.gitignore index e3ff6b0..2466c66 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,36 @@ -local/ +# FabricMC gitignore v1.0 + +# Gradle .gradle/ -out/ build/ +out/ +classes/ + +# IntelliJ Idea .idea/ +*.iml +*.ipr +*.iws -# eclipse +# Eclipse +.eclipse/ +*.launch + +# VS Code +.vscode/ .settings/ bin/ .classpath .project + +# Fleet +.fleet/ + +# MacOS +*.DS_Store + +# Java +hs_err_*.log +replay_*.log +*.hprof +*.jfr diff --git a/README.md b/README.md index 9aed8b9..6c0204d 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ # Stitch +Stitch is a set of tools for working with and updating mappings in the `.tiny` format. + +Large parts of its functionality have been replaced by other tools though, and nowadays, it's mostly only used for [intermediary generation and updating](https://fabricmc.net/wiki/tutorial:updating_yarn). diff --git a/build.gradle b/build.gradle index 6beae2b..6e0a06f 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,12 @@ plugins { id "java" id "java-library" id "maven-publish" - id "net.minecrell.licenser" version "0.4.1" + id "org.cadixdev.licenser" version "0.6.1" + id 'checkstyle' +} + +checkstyle { + configFile = file("checkstyle.xml") } sourceCompatibility = 1.8 @@ -123,4 +128,4 @@ task checkVersion { } } -publish.mustRunAfter checkVersion \ No newline at end of file +publish.mustRunAfter checkVersion diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..1d5aceb --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c053550b91381bbd28b1afc82d634bf73a8a..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644 GIT binary patch delta 39133 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 Ih2Pfy0TaOAA^-pY delta 36560 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 Fx;w9QV4r z|C*cNPYu>;jvqNIN32p`#N-2mu|Kq< z#r*y5rC>ix)Ok!ZYWRH&-oI4FAYq}fUYtdD`q5UN(Bv1y-T={{v`DT6RKN{amGFNh z=w2vmwO{rh(j}a_d0Os+q~Db(=Bj5OrMFpIVtU+ecC6vAV!6~Xc&yH2ZUkc*72-+gv5{rwZ>1c$qNlBSJ#-okiya4gk`%$3??G-y}$ z>gIVf^6jhoEp64&7c1kw4OuTaCp;+X+9nxS5TKlMRFY_Aq}0|Qw05NX*JDdR-zfrlO1t)EtUOnH`M^{i}PV7L@;%c_E_Oq_xl z`JvvMc1&Qj)rfZ6$tBOJx6JhD$v*CZ1KU8Pd_MmXQgRJ8lxUl*?BN%*HV z^E-+AmZuPdb!n;0ieU~B4V&zU3jWxs*l=9-e9GHDunIwM1=ipgZwris-%b^-t<);k*K+vaT4QEbu<0T zwzVjNM6Gsz6!xurk&^V#ok}kI5figuC}c|||0Hn&yP6ueJ+S79rH1_66|5+3Zh!9W z#~C)`)>SrPC|-4k`WP2axTP!E#qKG!BSu~1W!cw8;i$E;En3v!8>e_2ni?#plIX!Zl;414h747*VDv;tMsN@%xmQ{)RC`XQ$(L??y{r8oSCHk)%rSJ_AipxN zlAn5FXh!5++za)rzGj%|D^YX!Sp3Vg*Y6T3!j34-DFwuQAXO+#saJRYYHl@gCp}=gyt#p#+A@NtlWBdcZa}b zW|u4fbk0w9qB+}>f~nBzFek5i_sXT%sqG>HNBGCiOHEiDFJ%TC|LCjJm{!LPyR{Ns zWpJuN*P`g-=fZaT3;jJkEm&+t4yzxehr1cv#~c(B(~WI%Bo+8D@{0AnObXJJ2XfOc zR8nRQ9XM+|KSgr~qP;C1_Zxjnpw+BY_;A$Z32AkN8w`$;y{$Tfwky#X3hU;zB|$d` zv>JiIXQm5Er9ak%$kv6TYsOt6$qvMD&(y0eu_9}o8a(OiVe#*HN`pUgsHUC8 zc&hbG*`a}%By&=@*Cj=2!|!_DZw!CqcM^*|$f=~zxZFLve83s&UAFuvEb+)2;_Vau zPh*1W;@5LLZQd6-}CloFo8^Fd`mN!XG z+EgSHau*dsVm#=><%$W?Iqnf=`oy?g$Er{vBO5i2LPA=5@wb+PR<<=h>EqZ<%ieJ#Umjzj5P3i!#t8f`o2` zx$_0*mA#jBIsKVuawCivpUWuHZKcxJI;6gCx7c~nc$c|zt}UH#7MxpMjGoHuM=q0rDb>Uf>VWV`PW8?R<*@DG!W+~p6_d|gjanA=E zK{!AUB8E1v zERxR-1Q!Q;XVl@DmrP!f=49ic0-tfw;Dg802=_rYw3bFx`FB>xjcgQh_g zkem~Rl+;;GmU9yDuOpz4B{?ZJJ&Xnm`ku=La_4r#|9?~ivN`^*D~nX1{|hvr2m+pu zD41;<{a@%t$1fZHgn5DeM=W&OhE+}b2*Yf)hr|X>*#881 z!I^nw$k~Neoo(6!Xo%kb_IKdmVDEu1VFSFw#KLUp@@%%fF8r6_;xY<$a}5me20p`R z_*Y68!b-F8bbpYuQwE3)VW9&7U^kCEBEleHh!64!0OTP&lo&E1(0hI(^;w(o~oo)F|udyc%;2iYvr|6+CE{gGW8 zR$;b9aW+sap9Lx(6=#8Li{fmcbrAwe0pCewsGtT+pOFR4lURt_jEGVIGo=9yw*iYO zcc`=jAX!}-!$ \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,105 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 5093609..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +65,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,17 +72,19 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/settings.gradle b/settings.gradle index 6659293..57291e9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1 @@ rootProject.name = 'stitch' - diff --git a/src/main/java/net/fabricmc/stitch/Command.java b/src/main/java/net/fabricmc/stitch/Command.java index 9fbb549..21b2021 100644 --- a/src/main/java/net/fabricmc/stitch/Command.java +++ b/src/main/java/net/fabricmc/stitch/Command.java @@ -17,13 +17,13 @@ package net.fabricmc.stitch; public abstract class Command { - public final String name; + public final String name; - public Command(String name) { - this.name = name; - } + public Command(String name) { + this.name = name; + } - public abstract String getHelpString(); - public abstract boolean isArgumentCountValid(int count); - public abstract void run(String[] args) throws Exception; + public abstract String getHelpString(); + public abstract boolean isArgumentCountValid(int count); + public abstract void run(String[] args) throws Exception; } diff --git a/src/main/java/net/fabricmc/stitch/Main.java b/src/main/java/net/fabricmc/stitch/Main.java index 04eb5a4..2548cbf 100644 --- a/src/main/java/net/fabricmc/stitch/Main.java +++ b/src/main/java/net/fabricmc/stitch/Main.java @@ -16,63 +16,78 @@ package net.fabricmc.stitch; -import net.fabricmc.stitch.commands.*; -import net.fabricmc.stitch.commands.tinyv2.CommandMergeTinyV2; -import net.fabricmc.stitch.commands.tinyv2.CommandProposeV2FieldNames; -import net.fabricmc.stitch.commands.tinyv2.CommandReorderTinyV2; - import java.util.Locale; import java.util.Map; import java.util.TreeMap; +import net.fabricmc.stitch.commands.CommandAsmTrace; +import net.fabricmc.stitch.commands.CommandGenerateIntermediary; +import net.fabricmc.stitch.commands.CommandGeneratePrefixRemapper; +import net.fabricmc.stitch.commands.CommandMatcherToTiny; +import net.fabricmc.stitch.commands.CommandMergeJar; +import net.fabricmc.stitch.commands.CommandMergeTiny; +import net.fabricmc.stitch.commands.CommandProposeFieldNames; +import net.fabricmc.stitch.commands.CommandReorderTiny; +import net.fabricmc.stitch.commands.CommandRewriteIntermediary; +import net.fabricmc.stitch.commands.CommandUpdateIntermediary; +import net.fabricmc.stitch.commands.CommandValidateRecords; +import net.fabricmc.stitch.commands.tinyv2.CommandMergeTinyV2; +import net.fabricmc.stitch.commands.tinyv2.CommandProposeV2FieldNames; +import net.fabricmc.stitch.commands.tinyv2.CommandReorderTinyV2; + public class Main { - private static final Map COMMAND_MAP = new TreeMap<>(); + private static final Map COMMAND_MAP = new TreeMap<>(); + + public static void addCommand(Command command) { + COMMAND_MAP.put(command.name.toLowerCase(Locale.ROOT), command); + } + + static { + addCommand(new CommandAsmTrace()); + addCommand(new CommandGenerateIntermediary()); + addCommand(new CommandGeneratePrefixRemapper()); + addCommand(new CommandMatcherToTiny()); + addCommand(new CommandMergeJar()); + addCommand(new CommandMergeTiny()); + addCommand(new CommandProposeFieldNames()); + addCommand(new CommandReorderTiny()); + addCommand(new CommandRewriteIntermediary()); + addCommand(new CommandUpdateIntermediary()); + addCommand(new CommandReorderTinyV2()); + addCommand(new CommandMergeTinyV2()); + addCommand(new CommandProposeV2FieldNames()); + addCommand(new CommandValidateRecords()); + } + + public static void main(String[] args) { + if (args.length == 0 + || !COMMAND_MAP.containsKey(args[0].toLowerCase(Locale.ROOT)) + || !COMMAND_MAP.get(args[0].toLowerCase(Locale.ROOT)).isArgumentCountValid(args.length - 1)) { + if (args.length > 0) { + System.out.println("Invalid command: " + args[0]); + } + + System.out.println("Available commands:"); + + for (Command command : COMMAND_MAP.values()) { + System.out.println("\t" + command.name + " " + command.getHelpString()); + } - public static void addCommand(Command command) { - COMMAND_MAP.put(command.name.toLowerCase(Locale.ROOT), command); - } + System.out.println(); + return; + } - static { - addCommand(new CommandAsmTrace()); - addCommand(new CommandGenerateIntermediary()); - addCommand(new CommandGeneratePrefixRemapper()); - addCommand(new CommandMatcherToTiny()); - addCommand(new CommandMergeJar()); - addCommand(new CommandMergeTiny()); - addCommand(new CommandProposeFieldNames()); - addCommand(new CommandReorderTiny()); - addCommand(new CommandRewriteIntermediary()); - addCommand(new CommandUpdateIntermediary()); - addCommand(new CommandReorderTinyV2()); - addCommand(new CommandMergeTinyV2()); - addCommand(new CommandProposeV2FieldNames()); - addCommand(new CommandValidateRecords()); - } + try { + String[] argsCommand = new String[args.length - 1]; - public static void main(String[] args) { - if (args.length == 0 - || !COMMAND_MAP.containsKey(args[0].toLowerCase(Locale.ROOT)) - || !COMMAND_MAP.get(args[0].toLowerCase(Locale.ROOT)).isArgumentCountValid(args.length - 1)) { - if (args.length > 0) { - System.out.println("Invalid command: " + args[0]); - } - System.out.println("Available commands:"); - for (Command command : COMMAND_MAP.values()) { - System.out.println("\t" + command.name + " " + command.getHelpString()); - } - System.out.println(); - return; - } + if (args.length > 1) { + System.arraycopy(args, 1, argsCommand, 0, argsCommand.length); + } - try { - String[] argsCommand = new String[args.length - 1]; - if (args.length > 1) { - System.arraycopy(args, 1, argsCommand, 0, argsCommand.length); - } - COMMAND_MAP.get(args[0].toLowerCase(Locale.ROOT)).run(argsCommand); - } catch (Exception e) { - e.printStackTrace(); - System.exit(1); - } - } + COMMAND_MAP.get(args[0].toLowerCase(Locale.ROOT)).run(argsCommand); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } } diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandAsmTrace.java b/src/main/java/net/fabricmc/stitch/commands/CommandAsmTrace.java index ab82848..0ea1160 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandAsmTrace.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandAsmTrace.java @@ -16,34 +16,34 @@ package net.fabricmc.stitch.commands; -import net.fabricmc.stitch.Command; +import java.io.FileInputStream; +import java.io.PrintWriter; + import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.util.Textifier; import org.objectweb.asm.util.TraceClassVisitor; -import java.io.FileInputStream; -import java.io.PrintWriter; +import net.fabricmc.stitch.Command; public class CommandAsmTrace extends Command { - public CommandAsmTrace() { - super("asmTrace"); - } - - @Override - public String getHelpString() { - return ""; - } - - @Override - public boolean isArgumentCountValid(int count) { - return count == 1; - } - - @Override - public void run(String[] args) throws Exception { - ClassReader cr = new ClassReader(new FileInputStream(args[0])); - ClassVisitor tcv = new TraceClassVisitor(new PrintWriter(System.out)); - cr.accept(tcv, 0); - } + public CommandAsmTrace() { + super("asmTrace"); + } + + @Override + public String getHelpString() { + return ""; + } + + @Override + public boolean isArgumentCountValid(int count) { + return count == 1; + } + + @Override + public void run(String[] args) throws Exception { + ClassReader cr = new ClassReader(new FileInputStream(args[0])); + ClassVisitor tcv = new TraceClassVisitor(new PrintWriter(System.out)); + cr.accept(tcv, 0); + } } diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandGenerateIntermediary.java b/src/main/java/net/fabricmc/stitch/commands/CommandGenerateIntermediary.java index e1e6dd4..23528c5 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandGenerateIntermediary.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandGenerateIntermediary.java @@ -16,62 +16,66 @@ package net.fabricmc.stitch.commands; -import net.fabricmc.stitch.Command; -import net.fabricmc.stitch.representation.*; - -import java.io.*; +import java.io.File; +import java.io.IOException; import java.util.Locale; +import net.fabricmc.stitch.Command; +import net.fabricmc.stitch.representation.JarReader; +import net.fabricmc.stitch.representation.JarRootEntry; + public class CommandGenerateIntermediary extends Command { - public CommandGenerateIntermediary() { - super("generateIntermediary"); - } + public CommandGenerateIntermediary() { + super("generateIntermediary"); + } + + @Override + public String getHelpString() { + return " [-t|--target-namespace ] [-p|--obfuscation-pattern ]..."; + } - @Override - public String getHelpString() { - return " [-t|--target-namespace ] [-p|--obfuscation-pattern ]..."; - } + @Override + public boolean isArgumentCountValid(int count) { + return count >= 2; + } - @Override - public boolean isArgumentCountValid(int count) { - return count >= 2; - } + @Override + public void run(String[] args) throws Exception { + File file = new File(args[0]); + JarRootEntry jarEntry = new JarRootEntry(file); - @Override - public void run(String[] args) throws Exception { - File file = new File(args[0]); - JarRootEntry jarEntry = new JarRootEntry(file); - try { - JarReader reader = new JarReader(jarEntry); - reader.apply(); - } catch (IOException e) { - e.printStackTrace(); - } + try { + JarReader reader = new JarReader(jarEntry); + reader.apply(); + } catch (IOException e) { + e.printStackTrace(); + } - GenState state = new GenState(); - boolean clearedPatterns = false; + GenState state = new GenState(); + boolean clearedPatterns = false; - for (int i = 2; i < args.length; i++) { - switch (args[i].toLowerCase(Locale.ROOT)) { - case "-t": - case "--target-namespace": - state.setTargetNamespace(args[i + 1]); - i++; - break; - case "-p": - case "--obfuscation-pattern": - if (!clearedPatterns) - state.clearObfuscatedPatterns(); - clearedPatterns = true; + for (int i = 2; i < args.length; i++) { + switch (args[i].toLowerCase(Locale.ROOT)) { + case "-t": + case "--target-namespace": + state.setTargetNamespace(args[i + 1]); + i++; + break; + case "-p": + case "--obfuscation-pattern": + if (!clearedPatterns) { + state.clearObfuscatedPatterns(); + } - state.addObfuscatedPattern(args[i + 1]); - i++; - break; - } - } + clearedPatterns = true; + state.addObfuscatedPattern(args[i + 1]); + i++; + break; + } + } - System.err.println("Generating new mappings..."); - state.generate(new File(args[1]), jarEntry, null); - System.err.println("Done!"); - } + System.err.println("Generating new mappings..."); + state.generate(new File(args[1]), jarEntry, null); + System.err.println("Done!"); + } } diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandGeneratePrefixRemapper.java b/src/main/java/net/fabricmc/stitch/commands/CommandGeneratePrefixRemapper.java index 60bf59f..91bc449 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandGeneratePrefixRemapper.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandGeneratePrefixRemapper.java @@ -16,12 +16,16 @@ package net.fabricmc.stitch.commands; -import net.fabricmc.stitch.Command; - -import java.io.*; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; +import net.fabricmc.stitch.Command; + public class CommandGeneratePrefixRemapper extends Command { public CommandGeneratePrefixRemapper() { super("genPrefixedTiny"); @@ -40,10 +44,10 @@ public boolean isArgumentCountValid(int count) { @Override public void run(String[] args) throws Exception { try (FileInputStream fis = new FileInputStream(new File(args[0])); - FileOutputStream fos = new FileOutputStream(new File(args[2])); - OutputStreamWriter osw = new OutputStreamWriter(fos); - BufferedWriter writer = new BufferedWriter(osw)) { - writer.write("v1\t" + (args.length >= 5 ? args[3] : "input") + "\t" + (args.length >= 5 ? args[4] : "output") + "\n"); + FileOutputStream fos = new FileOutputStream(new File(args[2])); + OutputStreamWriter osw = new OutputStreamWriter(fos); + BufferedWriter writer = new BufferedWriter(osw)) { + writer.write("v1\t" + (args.length >= 5 ? args[3] : "input") + "\t" + (args.length >= 5 ? args[4] : "output") + "\n"); JarInputStream jis = new JarInputStream(fis); JarEntry entry; diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandMatcherToTiny.java b/src/main/java/net/fabricmc/stitch/commands/CommandMatcherToTiny.java index 43069e0..7296644 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandMatcherToTiny.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandMatcherToTiny.java @@ -16,13 +16,19 @@ package net.fabricmc.stitch.commands; -import net.fabricmc.stitch.Command; -import net.fabricmc.stitch.util.MatcherUtil; - -import java.io.*; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; import java.util.HashMap; import java.util.Map; +import net.fabricmc.stitch.Command; +import net.fabricmc.stitch.util.MatcherUtil; + public class CommandMatcherToTiny extends Command { public CommandMatcherToTiny() { super("matcherToTiny"); @@ -45,26 +51,19 @@ public void run(String[] args) throws Exception { Map methodNames = new HashMap<>(); System.out.println("Loading..."); - try ( - FileInputStream fis = new FileInputStream(new File(args[0])); + try (FileInputStream fis = new FileInputStream(new File(args[0])); InputStreamReader isr = new InputStreamReader(fis); - BufferedReader reader = new BufferedReader(isr) - ) { - + BufferedReader reader = new BufferedReader(isr)) { MatcherUtil.read(reader, false, classNames::put, (src, dst) -> fieldNames.put(src.getOwner() + "\t" + src.getDesc() + "\t" + src.getName(), dst.getName()), - (src, dst) -> methodNames.put(src.getOwner() + "\t" + src.getDesc() + "\t" + src.getName(), dst.getName()) - ); + (src, dst) -> methodNames.put(src.getOwner() + "\t" + src.getDesc() + "\t" + src.getName(), dst.getName())); } System.out.println("Saving..."); - try ( - FileOutputStream fos = new FileOutputStream(new File(args[1])); + try (FileOutputStream fos = new FileOutputStream(new File(args[1])); OutputStreamWriter osw = new OutputStreamWriter(fos); - BufferedWriter writer = new BufferedWriter(osw) - ) { - + BufferedWriter writer = new BufferedWriter(osw)) { writer.write("v1\t" + args[2] + "\t" + args[3] + "\n"); for (String s : classNames.keySet()) { diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandMergeJar.java b/src/main/java/net/fabricmc/stitch/commands/CommandMergeJar.java index 4ea1cd6..9768dcf 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandMergeJar.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandMergeJar.java @@ -16,75 +16,72 @@ package net.fabricmc.stitch.commands; -import net.fabricmc.stitch.Command; -import net.fabricmc.stitch.merge.JarMerger; - import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.util.Locale; -public class CommandMergeJar extends Command { - public CommandMergeJar() { - super("mergeJar"); - } +import net.fabricmc.stitch.Command; +import net.fabricmc.stitch.merge.JarMerger; - @Override - public String getHelpString() { - return " [--removeSnowman] [--syntheticparams]"; - } +public class CommandMergeJar extends Command { + public CommandMergeJar() { + super("mergeJar"); + } - @Override - public boolean isArgumentCountValid(int count) { - return count >= 3; - } + @Override + public String getHelpString() { + return " [--removeSnowman] [--syntheticparams]"; + } - @Override - public void run(String[] args) throws Exception { - File in1f = new File(args[0]); - File in2f = new File(args[1]); - File outf = new File(args[2]); - boolean removeSnowman = false, syntheticParams = false; + @Override + public boolean isArgumentCountValid(int count) { + return count >= 3; + } - for (int i = 3; i < args.length; i++) { - if (args[i].startsWith("--")) { - switch (args[i].substring(2).toLowerCase(Locale.ROOT)) { - case "removesnowman": - removeSnowman = true; - break; - case "syntheticparams": - syntheticParams = true; - break; - } - } - } + @Override + public void run(String[] args) throws Exception { + File in1f = new File(args[0]); + File in2f = new File(args[1]); + File outf = new File(args[2]); + boolean removeSnowman = false, syntheticParams = false; - if (!in1f.exists() || !in1f.isFile()) { - throw new FileNotFoundException("Client JAR could not be found!"); - } + for (int i = 3; i < args.length; i++) { + if (args[i].startsWith("--")) { + switch (args[i].substring(2).toLowerCase(Locale.ROOT)) { + case "removesnowman": + removeSnowman = true; + break; + case "syntheticparams": + syntheticParams = true; + break; + } + } + } - if (!in2f.exists() || !in2f.isFile()) { - throw new FileNotFoundException("Server JAR could not be found!"); - } + if (!in1f.exists() || !in1f.isFile()) { + throw new FileNotFoundException("Client JAR could not be found!"); + } - try (JarMerger merger = new JarMerger(in1f, in2f, outf)) { - if (removeSnowman) { - merger.enableSnowmanRemoval(); - } + if (!in2f.exists() || !in2f.isFile()) { + throw new FileNotFoundException("Server JAR could not be found!"); + } - if (syntheticParams) { - merger.enableSyntheticParamsOffset(); - } + try (JarMerger merger = new JarMerger(in1f, in2f, outf)) { + if (removeSnowman) { + merger.enableSnowmanRemoval(); + } - System.out.println("Merging..."); + if (syntheticParams) { + merger.enableSyntheticParamsOffset(); + } - merger.merge(); + System.out.println("Merging..."); + merger.merge(); - System.out.println("Merge completed!"); - } catch (IOException e) { - e.printStackTrace(); - } - } + System.out.println("Merge completed!"); + } catch (IOException e) { + e.printStackTrace(); + } + } } diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandMergeTiny.java b/src/main/java/net/fabricmc/stitch/commands/CommandMergeTiny.java index 29b1572..decae55 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandMergeTiny.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandMergeTiny.java @@ -16,15 +16,23 @@ package net.fabricmc.stitch.commands; -import net.fabricmc.stitch.Command; - import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import net.fabricmc.stitch.Command; // TODO: Remap descriptors on fields and methods. public class CommandMergeTiny extends Command { @@ -108,30 +116,36 @@ public static class TinyFile { public TinyFile(File f) throws IOException { try (BufferedReader reader = Files.newBufferedReader(f.toPath(), Charset.forName("UTF-8"))) { String[] header = reader.readLine().trim().split("\t"); + if (header.length < 3 || !header[0].trim().equals("v1")) { throw new RuntimeException("Invalid header!"); } typeCount = header.length - 1; indexList = new String[typeCount]; + for (int i = 0; i < typeCount; i++) { indexList[i] = header[i + 1].trim(); } String line; + while ((line = reader.readLine()) != null) { line = line.trim(); + if (line.length() == 0 || line.charAt(0) == '#') { continue; } String[] parts = line.split("\t"); + for (int i = 0; i < parts.length; i++) { parts[i] = parts[i].trim(); } StringBuilder prefix = new StringBuilder(); prefix.append(parts[0]); + for (int i = 1; i < parts.length - typeCount; i++) { prefix.append('\t'); prefix.append(parts[i]); @@ -143,15 +157,18 @@ public TinyFile(File f) throws IOException { for (int i = 0; i < (type == TinyEntryType.CLASS ? path.length - 1 : path.length); i++) { TinyEntry nextParent = parent.getChild(indexList[0], path[i]); + if (nextParent == null) { nextParent = new TinyEntry(TinyEntryType.CLASS, "CLASS"); nextParent.names.put(indexList[0], path[i]); parent.addChild(nextParent, ""); } + parent = nextParent; } TinyEntry entry; + if (type == TinyEntryType.CLASS && parent.containsChild(indexList[0], path[path.length - 1])) { entry = parent.getChild(indexList[0], path[path.length - 1]); } else { @@ -159,8 +176,10 @@ public TinyFile(File f) throws IOException { } String[] names = new String[typeCount]; + for (int i = 0; i < typeCount; i++) { names[i] = parts[parts.length - typeCount + i]; + if (type == TinyEntryType.CLASS) { // add classes by their final inner class name String[] splitly = names[i].split("\\$"); @@ -171,26 +190,25 @@ public TinyFile(File f) throws IOException { } switch (type) { - case CLASS: - parent.addChild(entry, ""); - break; - case FIELD: - case METHOD: - parent.addChild(entry, parts[2]); - break; + case CLASS: + parent.addChild(entry, ""); + break; + case FIELD: + case METHOD: + parent.addChild(entry, parts[2]); + break; } } } } -/* - public String match(String[] entries, String key) { - if (indexMap.containsKey(key)) { - return entries[indexMap.get(key)]; - } else { - return null; - } - } -*/ + + // public String match(String[] entries, String key) { + // if (indexMap.containsKey(key)) { + // return entries[indexMap.get(key)]; + // } else { + // return null; + // } + // } } private List mappingBlankFillOrder = new ArrayList<>(); @@ -221,6 +239,7 @@ private String fixMatch(TinyEntry a, TinyEntry b, String matchA, String index) { // First, map to the shared index name (sharedIndexName) String officialPath = a.names.get(sharedIndexName); TinyEntry officialEntry = a.getParent(); + while (officialEntry.type == TinyEntryType.CLASS) { officialPath = officialEntry.names.get(sharedIndexName) + "$" + officialPath; officialEntry = officialEntry.getParent(); @@ -305,11 +324,12 @@ private String getEntry(TinyEntry a, TinyEntry b, List totalIndexList) { for (String index : totalIndexList) { entry.append('\t'); - String match = getMatch(a, b, index, index); + if (match == null) { for (String s : mappingBlankFillOrder) { match = getMatch(a, b, s, index); + if (match != null) { break; } @@ -345,6 +365,7 @@ public void write(TinyEntry inputA, TinyEntry inputB, String index, String c, Bu Set subKeys = new TreeSet<>(); if (classA != null) subKeys.addAll(classA.getChildRow(index).keySet()); if (classB != null) subKeys.addAll(classB.getChildRow(index).keySet()); + for (String cc : subKeys) { write(classA, classB, index, cc, writer, totalIndexList, indent + 1); } @@ -364,6 +385,7 @@ public void run(File inputAf, File inputBf, File outputf, String... mappingBlank inputB = new TinyFile(inputBf); System.out.println("Processing..."); + try (BufferedWriter writer = Files.newBufferedWriter(outputf.toPath(), Charset.forName("UTF-8"))) { if (!inputA.indexList[0].equals(inputB.indexList[0])) { throw new RuntimeException("TODO"); @@ -374,20 +396,24 @@ public void run(File inputAf, File inputBf, File outputf, String... mappingBlank // Set matchedIndexes = Sets.intersection(inputA.indexMap.keySet(), inputB.indexMap.keySet()); Set matchedIndexes = Collections.singleton(inputA.indexList[0]); List totalIndexList = new ArrayList<>(Arrays.asList(inputA.indexList)); + for (String s : inputB.indexList) { if (!totalIndexList.contains(s)) { totalIndexList.add(s); } } + int totalIndexCount = totalIndexList.size(); // emit header StringBuilder header = new StringBuilder(); header.append("v1"); + for (String s : totalIndexList) { header.append('\t'); header.append(s); } + writer.write(header.append('\n').toString()); // collect classes @@ -401,6 +427,7 @@ public void run(File inputAf, File inputBf, File outputf, String... mappingBlank write(inputA.root, inputB.root, index, c, writer, totalIndexList, 0); } } + System.out.println("Done!"); } @@ -409,8 +436,8 @@ public void run(String[] args) throws Exception { File inputAf = new File(args[0]); File inputBf = new File(args[1]); File outputf = new File(args[2]); - String[] mbforder = new String[args.length - 3]; + for (int i = 3; i < args.length; i++) { mbforder[i - 3] = args[i]; } diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandProposeFieldNames.java b/src/main/java/net/fabricmc/stitch/commands/CommandProposeFieldNames.java index ca72c02..680735f 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandProposeFieldNames.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandProposeFieldNames.java @@ -16,6 +16,16 @@ package net.fabricmc.stitch.commands; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.HashMap; +import java.util.Map; + import net.fabricmc.mappings.EntryTriple; import net.fabricmc.mappings.FieldEntry; import net.fabricmc.mappings.Mappings; @@ -23,105 +33,104 @@ import net.fabricmc.stitch.Command; import net.fabricmc.stitch.util.FieldNameFinder; -import java.io.*; -import java.util.*; - public class CommandProposeFieldNames extends Command { - public CommandProposeFieldNames() { - super("proposeFieldNames"); - } - - @Override - public String getHelpString() { - return " "; - } - - @Override - public boolean isArgumentCountValid(int count) { - return count == 3; - } - - @Override - public void run(String[] args) throws Exception { - Map fieldNamesO = new FieldNameFinder().findNames(new File(args[0])); - - System.err.println("Found " + fieldNamesO.size() + " interesting names."); - - // i didn't fuss too much on this... this needs a rewrite once we get a mapping writer library - Map fieldNames = new HashMap<>(); - - Mappings mappings; - try (FileInputStream fileIn = new FileInputStream(new File(args[1]))) { - mappings = MappingsProvider.readTinyMappings(fileIn, false); - } - - try (FileInputStream fileIn = new FileInputStream(new File(args[1])); - FileOutputStream fileOut = new FileOutputStream(new File(args[2])); - InputStreamReader fileInReader = new InputStreamReader(fileIn); - OutputStreamWriter fileOutWriter = new OutputStreamWriter(fileOut); - BufferedReader reader = new BufferedReader(fileInReader); - BufferedWriter writer = new BufferedWriter(fileOutWriter)) { - - int headerPos = -1; - - String line; - while ((line = reader.readLine()) != null) { - String[] tabSplit = line.split("\t"); - - if (headerPos < 0) { - // first line - if (tabSplit.length < 3) { - throw new RuntimeException("Invalid mapping file!"); - } - - for (int i = 2; i < tabSplit.length; i++) { - if (tabSplit[i].equals("named")) { - headerPos = i; - break; - } - } - - if (headerPos < 0) { - throw new RuntimeException("Could not find 'named' mapping position!"); - } - - if (!tabSplit[1].equals("official")) { - for (FieldEntry e : mappings.getFieldEntries()) { - EntryTriple officialFieldMapping = e.get("official"); - String name = fieldNamesO.get(officialFieldMapping); - if (name != null) { - fieldNames.put(e.get(tabSplit[1]), name); - } - } - } else { - fieldNames = fieldNamesO; - } - - mappings = null; // save memory - } else { - // second+ line - if (tabSplit[0].equals("FIELD")) { - EntryTriple key = new EntryTriple(tabSplit[1], tabSplit[3], tabSplit[2]); - String value = tabSplit[headerPos + 2]; - if (value.startsWith("field_") && fieldNames.containsKey(key)) { - tabSplit[headerPos + 2] = fieldNames.get(key); - - StringBuilder builder = new StringBuilder(tabSplit[0]); - for (int i = 1; i < tabSplit.length; i++) { - builder.append('\t'); - builder.append(tabSplit[i]); - } - line = builder.toString(); - } - } - } - - if (!line.endsWith("\n")) { - line = line + "\n"; - } - - writer.write(line); - } - } - } -} \ No newline at end of file + public CommandProposeFieldNames() { + super("proposeFieldNames"); + } + + @Override + public String getHelpString() { + return " "; + } + + @Override + public boolean isArgumentCountValid(int count) { + return count == 3; + } + + @Override + public void run(String[] args) throws Exception { + Map fieldNamesO = new FieldNameFinder().findNames(new File(args[0])); + + System.err.println("Found " + fieldNamesO.size() + " interesting names."); + + // i didn't fuss too much on this... this needs a rewrite once we get a mapping writer library + Map fieldNames = new HashMap<>(); + Mappings mappings; + + try (FileInputStream fileIn = new FileInputStream(new File(args[1]))) { + mappings = MappingsProvider.readTinyMappings(fileIn, false); + } + + try (FileInputStream fileIn = new FileInputStream(new File(args[1])); + FileOutputStream fileOut = new FileOutputStream(new File(args[2])); + InputStreamReader fileInReader = new InputStreamReader(fileIn); + OutputStreamWriter fileOutWriter = new OutputStreamWriter(fileOut); + BufferedReader reader = new BufferedReader(fileInReader); + BufferedWriter writer = new BufferedWriter(fileOutWriter)) { + int headerPos = -1; + String line; + + while ((line = reader.readLine()) != null) { + String[] tabSplit = line.split("\t"); + + if (headerPos < 0) { + // first line + if (tabSplit.length < 3) { + throw new RuntimeException("Invalid mapping file!"); + } + + for (int i = 2; i < tabSplit.length; i++) { + if (tabSplit[i].equals("named")) { + headerPos = i; + break; + } + } + + if (headerPos < 0) { + throw new RuntimeException("Could not find 'named' mapping position!"); + } + + if (!tabSplit[1].equals("official")) { + for (FieldEntry e : mappings.getFieldEntries()) { + EntryTriple officialFieldMapping = e.get("official"); + String name = fieldNamesO.get(officialFieldMapping); + + if (name != null) { + fieldNames.put(e.get(tabSplit[1]), name); + } + } + } else { + fieldNames = fieldNamesO; + } + + mappings = null; // save memory + } else { + // second+ line + if (tabSplit[0].equals("FIELD")) { + EntryTriple key = new EntryTriple(tabSplit[1], tabSplit[3], tabSplit[2]); + String value = tabSplit[headerPos + 2]; + + if (value.startsWith("field_") && fieldNames.containsKey(key)) { + tabSplit[headerPos + 2] = fieldNames.get(key); + StringBuilder builder = new StringBuilder(tabSplit[0]); + + for (int i = 1; i < tabSplit.length; i++) { + builder.append('\t'); + builder.append(tabSplit[i]); + } + + line = builder.toString(); + } + } + } + + if (!line.endsWith("\n")) { + line = line + "\n"; + } + + writer.write(line); + } + } + } +} diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandReorderTiny.java b/src/main/java/net/fabricmc/stitch/commands/CommandReorderTiny.java index 89bcf05..7c6d28c 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandReorderTiny.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandReorderTiny.java @@ -16,11 +16,6 @@ package net.fabricmc.stitch.commands; -import net.fabricmc.mappings.EntryTriple; -import net.fabricmc.mappings.Mappings; -import net.fabricmc.mappings.MappingsProvider; -import net.fabricmc.stitch.Command; - import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; @@ -28,99 +23,112 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.util.Comparator; -import java.util.Locale; + +import net.fabricmc.mappings.EntryTriple; +import net.fabricmc.mappings.Mappings; +import net.fabricmc.mappings.MappingsProvider; +import net.fabricmc.stitch.Command; public class CommandReorderTiny extends Command { - public CommandReorderTiny() { - super("reorderTiny"); - } - - @Override - public String getHelpString() { - return " [name order...]"; - } - - @Override - public boolean isArgumentCountValid(int count) { - return count >= 4; - } - - private int compareTriples(EntryTriple a, EntryTriple b) { - int c = a.getOwner().compareTo(b.getOwner()); - if (c == 0) { - c = a.getDesc().compareTo(b.getDesc()); - if (c == 0) { - c = a.getName().compareTo(b.getName()); - } - } - return c; - } - - @Override - public void run(String[] args) throws Exception { - File fileOld = new File(args[0]); - File fileNew = new File(args[1]); - String[] names = new String[args.length - 2]; - System.arraycopy(args, 2, names, 0, names.length); - - System.err.println("Loading mapping file..."); - - Mappings input; - try (FileInputStream stream = new FileInputStream(fileOld)) { - input = MappingsProvider.readTinyMappings(stream, false); - } - - System.err.println("Rewriting mappings..."); - - try (FileOutputStream stream = new FileOutputStream(fileNew); - OutputStreamWriter osw = new OutputStreamWriter(stream); - BufferedWriter writer = new BufferedWriter(osw)) { - - StringBuilder firstLineBuilder = new StringBuilder("v1"); - for (String name : names) { - firstLineBuilder.append('\t').append(name); - } - writer.write(firstLineBuilder.append('\n').toString()); - input.getClassEntries().stream().sorted(Comparator.comparing((a) -> a.get(names[0]))).forEach((entry) -> { - try { - StringBuilder s = new StringBuilder("CLASS"); - for (String name : names) { - s.append('\t').append(entry.get(name)); - } - writer.write(s.append('\n').toString()); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - input.getFieldEntries().stream().sorted((a, b) -> compareTriples(a.get(names[0]), b.get(names[0]))).forEach((entry) -> { - try { - StringBuilder s = new StringBuilder("FIELD"); - EntryTriple first = entry.get(names[0]); - s.append('\t').append(first.getOwner()).append('\t').append(first.getDesc()); - for (String name : names) { - s.append('\t').append(entry.get(name).getName()); - } - writer.write(s.append('\n').toString()); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - input.getMethodEntries().stream().sorted((a, b) -> compareTriples(a.get(names[0]), b.get(names[0]))).forEach((entry) -> { - try { - StringBuilder s = new StringBuilder("METHOD"); - EntryTriple first = entry.get(names[0]); - s.append('\t').append(first.getOwner()).append('\t').append(first.getDesc()); - for (String name : names) { - s.append('\t').append(entry.get(name).getName()); - } - writer.write(s.append('\n').toString()); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - System.err.println("Done!"); - } + public CommandReorderTiny() { + super("reorderTiny"); + } + + @Override + public String getHelpString() { + return " [name order...]"; + } + + @Override + public boolean isArgumentCountValid(int count) { + return count >= 4; + } + + private int compareTriples(EntryTriple a, EntryTriple b) { + int c = a.getOwner().compareTo(b.getOwner()); + + if (c == 0) { + c = a.getDesc().compareTo(b.getDesc()); + + if (c == 0) { + c = a.getName().compareTo(b.getName()); + } + } + + return c; + } + + @Override + public void run(String[] args) throws Exception { + File fileOld = new File(args[0]); + File fileNew = new File(args[1]); + String[] names = new String[args.length - 2]; + System.arraycopy(args, 2, names, 0, names.length); + + System.err.println("Loading mapping file..."); + Mappings input; + + try (FileInputStream stream = new FileInputStream(fileOld)) { + input = MappingsProvider.readTinyMappings(stream, false); + } + + System.err.println("Rewriting mappings..."); + + try (FileOutputStream stream = new FileOutputStream(fileNew); + OutputStreamWriter osw = new OutputStreamWriter(stream); + BufferedWriter writer = new BufferedWriter(osw)) { + StringBuilder firstLineBuilder = new StringBuilder("v1"); + + for (String name : names) { + firstLineBuilder.append('\t').append(name); + } + + writer.write(firstLineBuilder.append('\n').toString()); + input.getClassEntries().stream().sorted(Comparator.comparing((a) -> a.get(names[0]))).forEach((entry) -> { + try { + StringBuilder s = new StringBuilder("CLASS"); + + for (String name : names) { + s.append('\t').append(entry.get(name)); + } + + writer.write(s.append('\n').toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + input.getFieldEntries().stream().sorted((a, b) -> compareTriples(a.get(names[0]), b.get(names[0]))).forEach((entry) -> { + try { + StringBuilder s = new StringBuilder("FIELD"); + EntryTriple first = entry.get(names[0]); + s.append('\t').append(first.getOwner()).append('\t').append(first.getDesc()); + + for (String name : names) { + s.append('\t').append(entry.get(name).getName()); + } + + writer.write(s.append('\n').toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + input.getMethodEntries().stream().sorted((a, b) -> compareTriples(a.get(names[0]), b.get(names[0]))).forEach((entry) -> { + try { + StringBuilder s = new StringBuilder("METHOD"); + EntryTriple first = entry.get(names[0]); + s.append('\t').append(first.getOwner()).append('\t').append(first.getDesc()); + + for (String name : names) { + s.append('\t').append(entry.get(name).getName()); + } + + writer.write(s.append('\n').toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + System.err.println("Done!"); + } } diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandRewriteIntermediary.java b/src/main/java/net/fabricmc/stitch/commands/CommandRewriteIntermediary.java index b193733..c53ad09 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandRewriteIntermediary.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandRewriteIntermediary.java @@ -16,61 +16,62 @@ package net.fabricmc.stitch.commands; -import net.fabricmc.stitch.Command; -import net.fabricmc.stitch.representation.JarRootEntry; -import net.fabricmc.stitch.representation.JarReader; - import java.io.File; import java.io.IOException; import java.util.Locale; +import net.fabricmc.stitch.Command; +import net.fabricmc.stitch.representation.JarReader; +import net.fabricmc.stitch.representation.JarRootEntry; + public class CommandRewriteIntermediary extends Command { - public CommandRewriteIntermediary() { - super("rewriteIntermediary"); - } + public CommandRewriteIntermediary() { + super("rewriteIntermediary"); + } + + @Override + public String getHelpString() { + return " [--writeAll]"; + } - @Override - public String getHelpString() { - return " [--writeAll]"; - } + @Override + public boolean isArgumentCountValid(int count) { + return count >= 3; + } - @Override - public boolean isArgumentCountValid(int count) { - return count >= 3; - } + @Override + public void run(String[] args) throws Exception { + File fileOld = new File(args[0]); + JarRootEntry jarOld = new JarRootEntry(fileOld); - @Override - public void run(String[] args) throws Exception { - File fileOld = new File(args[0]); - JarRootEntry jarOld = new JarRootEntry(fileOld); - try { - JarReader reader = new JarReader(jarOld); - reader.apply(); - } catch (IOException e) { - e.printStackTrace(); - } + try { + JarReader reader = new JarReader(jarOld); + reader.apply(); + } catch (IOException e) { + e.printStackTrace(); + } - GenState state = new GenState(); + GenState state = new GenState(); - for (int i = 3; i < args.length; i++) { - switch (args[i].toLowerCase(Locale.ROOT)) { - case "--writeall": - state.setWriteAll(true); - break; - } - } + for (int i = 3; i < args.length; i++) { + switch (args[i].toLowerCase(Locale.ROOT)) { + case "--writeall": + state.setWriteAll(true); + break; + } + } - System.err.println("Loading mapping file..."); - state.prepareRewrite(new File(args[1])); + System.err.println("Loading mapping file..."); + state.prepareRewrite(new File(args[1])); - File outFile = new File(args[2]); - if (outFile.exists()) { - outFile.delete(); - } + File outFile = new File(args[2]); - System.err.println("Rewriting mappings..."); - state.generate(outFile, jarOld, jarOld); - System.err.println("Done!"); - } + if (outFile.exists()) { + outFile.delete(); + } + System.err.println("Rewriting mappings..."); + state.generate(outFile, jarOld, jarOld); + System.err.println("Done!"); + } } diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandUpdateIntermediary.java b/src/main/java/net/fabricmc/stitch/commands/CommandUpdateIntermediary.java index 59a1265..c23ed7f 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandUpdateIntermediary.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandUpdateIntermediary.java @@ -16,76 +16,79 @@ package net.fabricmc.stitch.commands; -import net.fabricmc.stitch.Command; -import net.fabricmc.stitch.representation.*; - import java.io.File; import java.io.IOException; import java.util.Locale; +import net.fabricmc.stitch.Command; +import net.fabricmc.stitch.representation.JarReader; +import net.fabricmc.stitch.representation.JarRootEntry; + public class CommandUpdateIntermediary extends Command { - public CommandUpdateIntermediary() { - super("updateIntermediary"); - } + public CommandUpdateIntermediary() { + super("updateIntermediary"); + } + + @Override + public String getHelpString() { + return " [-t|--target-namespace ] [-p|--obfuscation-pattern ]"; + } - @Override - public String getHelpString() { - return " [-t|--target-namespace ] [-p|--obfuscation-pattern ]"; - } + @Override + public boolean isArgumentCountValid(int count) { + return count >= 5; + } - @Override - public boolean isArgumentCountValid(int count) { - return count >= 5; - } + @Override + public void run(String[] args) throws Exception { + File fileOld = new File(args[0]); + JarRootEntry jarOld = new JarRootEntry(fileOld); - @Override - public void run(String[] args) throws Exception { - File fileOld = new File(args[0]); - JarRootEntry jarOld = new JarRootEntry(fileOld); - try { - JarReader reader = new JarReader(jarOld); - reader.apply(); - } catch (IOException e) { - e.printStackTrace(); - } + try { + JarReader reader = new JarReader(jarOld); + reader.apply(); + } catch (IOException e) { + e.printStackTrace(); + } - File fileNew = new File(args[1]); - JarRootEntry jarNew = new JarRootEntry(fileNew); - try { - JarReader reader = new JarReader(jarNew); - reader.apply(); - } catch (IOException e) { - e.printStackTrace(); - } + File fileNew = new File(args[1]); + JarRootEntry jarNew = new JarRootEntry(fileNew); - GenState state = new GenState(); - boolean clearedPatterns = false; + try { + JarReader reader = new JarReader(jarNew); + reader.apply(); + } catch (IOException e) { + e.printStackTrace(); + } - for (int i = 5; i < args.length; i++) { - switch (args[i].toLowerCase(Locale.ROOT)) { - case "-t": - case "--target-namespace": - state.setTargetNamespace(args[i + 1]); - i++; - break; - case "-p": - case "--obfuscation-pattern": - if (!clearedPatterns) - state.clearObfuscatedPatterns(); - clearedPatterns = true; + GenState state = new GenState(); + boolean clearedPatterns = false; - state.addObfuscatedPattern(args[i + 1]); - i++; - break; - } - } + for (int i = 5; i < args.length; i++) { + switch (args[i].toLowerCase(Locale.ROOT)) { + case "-t": + case "--target-namespace": + state.setTargetNamespace(args[i + 1]); + i++; + break; + case "-p": + case "--obfuscation-pattern": + if (!clearedPatterns) { + state.clearObfuscatedPatterns(); + } - System.err.println("Loading remapping files..."); - state.prepareUpdate(new File(args[2]), new File(args[4])); + clearedPatterns = true; + state.addObfuscatedPattern(args[i + 1]); + i++; + break; + } + } - System.err.println("Generating new mappings..."); - state.generate(new File(args[3]), jarNew, jarOld); - System.err.println("Done!"); - } + System.err.println("Loading remapping files..."); + state.prepareUpdate(new File(args[2]), new File(args[4])); + System.err.println("Generating new mappings..."); + state.generate(new File(args[3]), jarNew, jarOld); + System.err.println("Done!"); + } } diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandValidateRecords.java b/src/main/java/net/fabricmc/stitch/commands/CommandValidateRecords.java index f234ad2..015cf00 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandValidateRecords.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandValidateRecords.java @@ -16,12 +16,12 @@ package net.fabricmc.stitch.commands; -import net.fabricmc.stitch.Command; -import net.fabricmc.stitch.util.RecordValidator; - import java.io.File; import java.io.FileNotFoundException; +import net.fabricmc.stitch.Command; +import net.fabricmc.stitch.util.RecordValidator; + public class CommandValidateRecords extends Command { public CommandValidateRecords() { super("validateRecords"); @@ -52,6 +52,7 @@ public void run(String[] args) throws Exception { for (String error : e.errors) { System.err.println(error); } + throw e; } } diff --git a/src/main/java/net/fabricmc/stitch/commands/GenMap.java b/src/main/java/net/fabricmc/stitch/commands/GenMap.java index eaf2937..94862f8 100644 --- a/src/main/java/net/fabricmc/stitch/commands/GenMap.java +++ b/src/main/java/net/fabricmc/stitch/commands/GenMap.java @@ -15,121 +15,127 @@ */ package net.fabricmc.stitch.commands; -import net.fabricmc.mappings.*; -import org.checkerframework.checker.nullness.qual.Nullable; import java.util.HashMap; import java.util.Map; import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.Nullable; + +import net.fabricmc.mappings.ClassEntry; +import net.fabricmc.mappings.EntryTriple; +import net.fabricmc.mappings.FieldEntry; +import net.fabricmc.mappings.Mappings; +import net.fabricmc.mappings.MethodEntry; + public class GenMap { - private static class Class { - private final String name; - private final Map fieldMaps = new HashMap<>(); - private final Map methodMaps = new HashMap<>(); - - public Class(String name) { - this.name = name; - } - } - - private final Map map = new HashMap<>(); - - public GenMap() { - } - - public void addClass(String from, String to) { - map.put(from, new Class(to)); - } - - public void addField(EntryTriple from, EntryTriple to) { - map.get(from.getOwner()).fieldMaps.put(from, to); - } - - public void addMethod(EntryTriple from, EntryTriple to) { - map.get(from.getOwner()).methodMaps.put(from, to); - } - - public void load(Mappings mappings, String from, String to) { - for (ClassEntry classEntry : mappings.getClassEntries()) { - map.put(classEntry.get(from), new Class(classEntry.get(to))); - } - - for (FieldEntry fieldEntry : mappings.getFieldEntries()) { - map.get(fieldEntry.get(from).getOwner()).fieldMaps.put(fieldEntry.get(from), fieldEntry.get(to)); - } - - for (MethodEntry methodEntry : mappings.getMethodEntries()) { - map.get(methodEntry.get(from).getOwner()).methodMaps.put(methodEntry.get(from), methodEntry.get(to)); - } - } - - @Nullable - public String getClass(String from) { - return map.containsKey(from) ? map.get(from).name : null; - } - - @Nullable - private EntryTriple get(EntryTriple entry, Function> mapGetter) { - if (map.containsKey(entry.getOwner())) { - return mapGetter.apply(map.get(entry.getOwner())).get(entry); - } - - return null; - } - - @Nullable - public EntryTriple getField(String owner, String name, String desc) { - return get(new EntryTriple(owner, name, desc), (c) -> c.fieldMaps); - } - - @Nullable - public EntryTriple getField(EntryTriple entry) { - return get(entry, (c) -> c.fieldMaps); - } - - @Nullable - public EntryTriple getMethod(String owner, String name, String desc) { - return get(new EntryTriple(owner, name, desc), (c) -> c.methodMaps); - } - - @Nullable - public EntryTriple getMethod(EntryTriple entry) { - return get(entry, (c) -> c.methodMaps); - } - - public static class Dummy extends GenMap { - public Dummy() { - } - - @Nullable - @Override - public String getClass(String from) { - return from; - } - - @Nullable - @Override - public EntryTriple getField(String owner, String name, String desc) { - return new EntryTriple(owner, name, desc); - } - - @Nullable - @Override - public EntryTriple getField(EntryTriple entry) { - return entry; - } - - @Nullable - @Override - public EntryTriple getMethod(String owner, String name, String desc) { - return new EntryTriple(owner, name, desc); - } - - @Nullable - @Override - public EntryTriple getMethod(EntryTriple entry) { - return entry; - } - } + private static class Class { + private final String name; + private final Map fieldMaps = new HashMap<>(); + private final Map methodMaps = new HashMap<>(); + + Class(String name) { + this.name = name; + } + } + + private final Map map = new HashMap<>(); + + public GenMap() { + } + + public void addClass(String from, String to) { + map.put(from, new Class(to)); + } + + public void addField(EntryTriple from, EntryTriple to) { + map.get(from.getOwner()).fieldMaps.put(from, to); + } + + public void addMethod(EntryTriple from, EntryTriple to) { + map.get(from.getOwner()).methodMaps.put(from, to); + } + + public void load(Mappings mappings, String from, String to) { + for (ClassEntry classEntry : mappings.getClassEntries()) { + map.put(classEntry.get(from), new Class(classEntry.get(to))); + } + + for (FieldEntry fieldEntry : mappings.getFieldEntries()) { + map.get(fieldEntry.get(from).getOwner()).fieldMaps.put(fieldEntry.get(from), fieldEntry.get(to)); + } + + for (MethodEntry methodEntry : mappings.getMethodEntries()) { + map.get(methodEntry.get(from).getOwner()).methodMaps.put(methodEntry.get(from), methodEntry.get(to)); + } + } + + @Nullable + public String getClass(String from) { + return map.containsKey(from) ? map.get(from).name : null; + } + + @Nullable + private EntryTriple get(EntryTriple entry, Function> mapGetter) { + if (map.containsKey(entry.getOwner())) { + return mapGetter.apply(map.get(entry.getOwner())).get(entry); + } + + return null; + } + + @Nullable + public EntryTriple getField(String owner, String name, String desc) { + return get(new EntryTriple(owner, name, desc), (c) -> c.fieldMaps); + } + + @Nullable + public EntryTriple getField(EntryTriple entry) { + return get(entry, (c) -> c.fieldMaps); + } + + @Nullable + public EntryTriple getMethod(String owner, String name, String desc) { + return get(new EntryTriple(owner, name, desc), (c) -> c.methodMaps); + } + + @Nullable + public EntryTriple getMethod(EntryTriple entry) { + return get(entry, (c) -> c.methodMaps); + } + + public static class Dummy extends GenMap { + public Dummy() { + } + + @Nullable + @Override + public String getClass(String from) { + return from; + } + + @Nullable + @Override + public EntryTriple getField(String owner, String name, String desc) { + return new EntryTriple(owner, name, desc); + } + + @Nullable + @Override + public EntryTriple getField(EntryTriple entry) { + return entry; + } + + @Nullable + @Override + public EntryTriple getMethod(String owner, String name, String desc) { + return new EntryTriple(owner, name, desc); + } + + @Nullable + @Override + public EntryTriple getMethod(EntryTriple entry) { + return entry; + } + } } diff --git a/src/main/java/net/fabricmc/stitch/commands/GenState.java b/src/main/java/net/fabricmc/stitch/commands/GenState.java index beab5d8..e9ac60b 100644 --- a/src/main/java/net/fabricmc/stitch/commands/GenState.java +++ b/src/main/java/net/fabricmc/stitch/commands/GenState.java @@ -16,504 +16,555 @@ package net.fabricmc.stitch.commands; -import net.fabricmc.mappings.EntryTriple; -import net.fabricmc.mappings.MappingsProvider; -import net.fabricmc.stitch.representation.*; -import net.fabricmc.stitch.util.MatcherUtil; -import net.fabricmc.stitch.util.Pair; -import net.fabricmc.stitch.util.StitchUtil; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.objectweb.asm.Opcodes; - -import java.io.*; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.Set; +import java.util.StringJoiner; +import java.util.TreeSet; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.objectweb.asm.Opcodes; + +import net.fabricmc.mappings.EntryTriple; +import net.fabricmc.mappings.MappingsProvider; +import net.fabricmc.stitch.representation.AbstractJarEntry; +import net.fabricmc.stitch.representation.ClassStorage; +import net.fabricmc.stitch.representation.JarClassEntry; +import net.fabricmc.stitch.representation.JarFieldEntry; +import net.fabricmc.stitch.representation.JarMethodEntry; +import net.fabricmc.stitch.representation.JarRootEntry; +import net.fabricmc.stitch.util.MatcherUtil; +import net.fabricmc.stitch.util.Pair; +import net.fabricmc.stitch.util.StitchUtil; + class GenState { - private final Map counters = new HashMap<>(); - private final Map values = new IdentityHashMap<>(); - private GenMap oldToIntermediary, newToOld; - private GenMap newToIntermediary; - private boolean interactive = true; - private boolean writeAll = false; - private Scanner scanner = new Scanner(System.in); - - private String targetNamespace = "net/minecraft/"; - private final List obfuscatedPatterns = new ArrayList(); - - public GenState() { - this.obfuscatedPatterns.add(Pattern.compile("^[^/]*$")); // Default ofbfuscation. Minecraft classes without a package are obfuscated. - } - - public void setWriteAll(boolean writeAll) { - this.writeAll = writeAll; - } - - public void disableInteractive() { - interactive = false; - } - - public String next(AbstractJarEntry entry, String name) { - return name + "_" + values.computeIfAbsent(entry, (e) -> { - int v = counters.getOrDefault(name, 1); - counters.put(name, v + 1); - return v; - }); - } - - public void setTargetNamespace(final String namespace) { - if (namespace.lastIndexOf("/") != (namespace.length() - 1)) - this.targetNamespace = namespace + "/"; - else - this.targetNamespace = namespace; - } - - public void clearObfuscatedPatterns() { - this.obfuscatedPatterns.clear(); - } - - public void addObfuscatedPattern(String regex) throws PatternSyntaxException { - this.obfuscatedPatterns.add(Pattern.compile(regex)); - } - - public void setCounter(String key, int value) { - counters.put(key, value); - } - - public Map getCounters() { - return Collections.unmodifiableMap(counters); - } - - public void generate(File file, JarRootEntry jarEntry, JarRootEntry jarOld) throws IOException { - if (file.exists()) { - System.err.println("Target file exists - loading..."); - newToIntermediary = new GenMap(); - try (FileInputStream inputStream = new FileInputStream(file)) { - newToIntermediary.load( - MappingsProvider.readTinyMappings(inputStream), - "official", - "intermediary" - ); - } - } - - try (FileWriter fileWriter = new FileWriter(file)) { - try (BufferedWriter writer = new BufferedWriter(fileWriter)) { - writer.write("v1\tofficial\tintermediary\n"); - - for (JarClassEntry c : jarEntry.getClasses()) { - addClass(writer, c, jarOld, jarEntry, this.targetNamespace); - } - - writeCounters(writer); - } - } - } - - public static boolean isMappedClass(ClassStorage storage, JarClassEntry c) { - return !c.isAnonymous(); - } - - public static boolean isMappedField(ClassStorage storage, JarClassEntry c, JarFieldEntry f) { - return isUnmappedFieldName(f.getName()); - } - - public static boolean isUnmappedFieldName(String name) { - return name.length() <= 2 || (name.length() == 3 && name.charAt(2) == '_'); - } - - public static boolean isMappedMethod(ClassStorage storage, JarClassEntry c, JarMethodEntry m) { - return isUnmappedMethodName(m.getName()) && m.isSource(storage, c); - } - - public static boolean isUnmappedMethodName(String name) { - return (name.length() <= 2 || (name.length() == 3 && name.charAt(2) == '_')) && name.charAt(0) != '<'; - } - - @Nullable - private String getFieldName(ClassStorage storage, JarClassEntry c, JarFieldEntry f) { - if (!isMappedField(storage, c, f)) { - return null; - } - - if (newToIntermediary != null) { - EntryTriple findEntry = newToIntermediary.getField(c.getFullyQualifiedName(), f.getName(), f.getDescriptor()); - if (findEntry != null) { - if (findEntry.getName().contains("field_")) { - return findEntry.getName(); - } else { - String newName = next(f, "field"); - System.out.println(findEntry.getName() + " is now " + newName); - return newName; - } - } - } - - if (newToOld != null) { - EntryTriple findEntry = newToOld.getField(c.getFullyQualifiedName(), f.getName(), f.getDescriptor()); - if (findEntry != null) { - findEntry = oldToIntermediary.getField(findEntry); - if (findEntry != null) { - if (findEntry.getName().contains("field_")) { - return findEntry.getName(); - } else { - String newName = next(f, "field"); - System.out.println(findEntry.getName() + " is now " + newName); - return newName; - } - } - } - } - - return next(f, "field"); - } - - private final Map methodNames = new IdentityHashMap<>(); - - private String getPropagation(ClassStorage storage, JarClassEntry classEntry) { - if (classEntry == null) { - return ""; - } - - StringBuilder builder = new StringBuilder(classEntry.getFullyQualifiedName()); - List strings = new ArrayList<>(); - String scs = getPropagation(storage, classEntry.getSuperClass(storage)); - if (!scs.isEmpty()) { - strings.add(scs); - } - - for (JarClassEntry ce : classEntry.getInterfaces(storage)) { - scs = getPropagation(storage, ce); - if (!scs.isEmpty()) { - strings.add(scs); - } - } - - if (!strings.isEmpty()) { - builder.append("<-"); - if (strings.size() == 1) { - builder.append(strings.get(0)); - } else { - builder.append("["); - builder.append(StitchUtil.join(",", strings)); - builder.append("]"); - } - } - - return builder.toString(); - } - - private String getNamesListEntry(ClassStorage storage, JarClassEntry classEntry) { - StringBuilder builder = new StringBuilder(getPropagation(storage, classEntry)); - if (classEntry.isInterface()) { - builder.append("(itf)"); - } - - return builder.toString(); - } - - private Set findNames(ClassStorage storageOld, ClassStorage storageNew, JarClassEntry c, JarMethodEntry m, Map> names) { - Set allEntries = new HashSet<>(); - findNames(storageOld, storageNew, c, m, names, allEntries); - return allEntries; - } - - private void findNames(ClassStorage storageOld, ClassStorage storageNew, JarClassEntry c, JarMethodEntry m, Map> names, Set usedMethods) { - if (!usedMethods.add(m)) { - return; - } - - String suffix = "." + m.getName() + m.getDescriptor(); - - if ((m.getAccess() & Opcodes.ACC_BRIDGE) != 0) { - suffix += "(bridge)"; - } - - List ccList = m.getMatchingEntries(storageNew, c); - - for (JarClassEntry cc : ccList) { - EntryTriple findEntry = null; - if (newToIntermediary != null) { - findEntry = newToIntermediary.getMethod(cc.getFullyQualifiedName(), m.getName(), m.getDescriptor()); - if (findEntry != null) { - names.computeIfAbsent(findEntry.getName(), (s) -> new TreeSet<>()).add(getNamesListEntry(storageNew, cc) + suffix); - } - } - - if (findEntry == null && newToOld != null) { - findEntry = newToOld.getMethod(cc.getFullyQualifiedName(), m.getName(), m.getDescriptor()); - if (findEntry != null) { - EntryTriple newToOldEntry = findEntry; - findEntry = oldToIntermediary.getMethod(newToOldEntry); - if (findEntry != null) { - names.computeIfAbsent(findEntry.getName(), (s) -> new TreeSet<>()).add(getNamesListEntry(storageNew, cc) + suffix); - } else { - // more involved... - JarClassEntry oldBase = storageOld.getClass(newToOldEntry.getOwner(), false); - if (oldBase != null) { - JarMethodEntry oldM = oldBase.getMethod(newToOldEntry.getName() + newToOldEntry.getDesc()); - List cccList = oldM.getMatchingEntries(storageOld, oldBase); - - for (JarClassEntry ccc : cccList) { - findEntry = oldToIntermediary.getMethod(ccc.getFullyQualifiedName(), oldM.getName(), oldM.getDescriptor()); - if (findEntry != null) { - names.computeIfAbsent(findEntry.getName(), (s) -> new TreeSet<>()).add(getNamesListEntry(storageOld, ccc) + suffix); - } - } - } - } - } - } - } - - for (JarClassEntry mc : ccList) { - for (Pair pair : mc.getRelatedMethods(m)) { - findNames(storageOld, storageNew, pair.getLeft(), pair.getLeft().getMethod(pair.getRight()), names, usedMethods); - } - } - } - - @Nullable - private String getMethodName(ClassStorage storageOld, ClassStorage storageNew, JarClassEntry c, JarMethodEntry m) { - if (!isMappedMethod(storageNew, c, m)) { - return null; - } - - if (methodNames.containsKey(m)) { - return methodNames.get(m); - } - - if (newToOld != null || newToIntermediary != null) { - Map> names = new HashMap<>(); - Set allEntries = findNames(storageOld, storageNew, c, m, names); - for (JarMethodEntry mm : allEntries) { - if (methodNames.containsKey(mm)) { - return methodNames.get(mm); - } - } - - if (names.size() > 1) { - System.out.println("Conflict detected - matched same target name!"); - List nameList = new ArrayList<>(names.keySet()); - Collections.sort(nameList); - - for (int i = 0; i < nameList.size(); i++) { - String s = nameList.get(i); - System.out.println((i+1) + ") " + s + " <- " + StitchUtil.join(", ", names.get(s))); - } - - if (!interactive) { - throw new RuntimeException("Conflict detected!"); - } - - while (true) { - String cmd = scanner.nextLine(); - int i; - try { - i = Integer.parseInt(cmd); - } catch (NumberFormatException e) { - e.printStackTrace(); - continue; - } - - if (i >= 1 && i <= nameList.size()) { - for (JarMethodEntry mm : allEntries) { - methodNames.put(mm, nameList.get(i - 1)); - } - System.out.println("OK!"); - return nameList.get(i - 1); - } - } - } else if (names.size() == 1) { - String s = names.keySet().iterator().next(); - for (JarMethodEntry mm : allEntries) { - methodNames.put(mm, s); - } - if (s.contains("method_")) { - return s; - } else { - String newName = next(m, "method"); - System.out.println(s + " is now " + newName); - return newName; - } - } - } - - return next(m, "method"); - } - - private void addClass(BufferedWriter writer, JarClassEntry c, ClassStorage storageOld, ClassStorage storage, String translatedPrefix) throws IOException { - String className = c.getName(); - String cname = ""; - String prefixSaved = translatedPrefix; - - if(!this.obfuscatedPatterns.stream().anyMatch(p -> p.matcher(className).matches())) { - translatedPrefix = c.getFullyQualifiedName(); - } else { - if (!isMappedClass(storage, c)) { - cname = c.getName(); - } else { - cname = null; - - if (newToIntermediary != null) { - String findName = newToIntermediary.getClass(c.getFullyQualifiedName()); - if (findName != null) { - String[] r = findName.split("\\$"); - cname = r[r.length - 1]; - if (r.length == 1) { - translatedPrefix = ""; - } - } - } - - if (cname == null && newToOld != null) { - String findName = newToOld.getClass(c.getFullyQualifiedName()); - if (findName != null) { - findName = oldToIntermediary.getClass(findName); - if (findName != null) { - String[] r = findName.split("\\$"); - cname = r[r.length - 1]; - if (r.length == 1) { - translatedPrefix = ""; - } - - } - } - } - - if (cname != null && !cname.contains("class_")) { - String newName = next(c, "class"); - System.out.println(cname + " is now " + newName); - cname = newName; - translatedPrefix = prefixSaved; - } - - if (cname == null) { - cname = next(c, "class"); - } - } - } - - writer.write("CLASS\t" + c.getFullyQualifiedName() + "\t" + translatedPrefix + cname + "\n"); - - for (JarFieldEntry f : c.getFields()) { - String fName = getFieldName(storage, c, f); - if (fName == null) { - fName = f.getName(); - } - - if (fName != null) { - writer.write("FIELD\t" + c.getFullyQualifiedName() - + "\t" + f.getDescriptor() - + "\t" + f.getName() - + "\t" + fName + "\n"); - } - } - - for (JarMethodEntry m : c.getMethods()) { - String mName = getMethodName(storageOld, storage, c, m); - if (mName == null) { - if (!m.getName().startsWith("<") && m.isSource(storage, c)) { - mName = m.getName(); - } - } - - if (mName != null) { - writer.write("METHOD\t" + c.getFullyQualifiedName() - + "\t" + m.getDescriptor() - + "\t" + m.getName() - + "\t" + mName + "\n"); - } - } - - for (JarClassEntry cc : c.getInnerClasses()) { - addClass(writer, cc, storageOld, storage, translatedPrefix + cname + "$"); - } - } - - public void prepareRewrite(File oldMappings) throws IOException { - oldToIntermediary = new GenMap(); - newToOld = new GenMap.Dummy(); - - // TODO: only read once - readCounters(oldMappings); - - try (FileInputStream inputStream = new FileInputStream(oldMappings)) { - oldToIntermediary.load( - MappingsProvider.readTinyMappings(inputStream), - "official", - "intermediary" - ); - } - } - - public void prepareUpdate(File oldMappings, File matches) throws IOException { - oldToIntermediary = new GenMap(); - newToOld = new GenMap(); - - // TODO: only read once - readCounters(oldMappings); - - try (FileInputStream inputStream = new FileInputStream(oldMappings)) { - oldToIntermediary.load( - MappingsProvider.readTinyMappings(inputStream), - "official", - "intermediary" - ); - } - - try (FileReader fileReader = new FileReader(matches)) { - try (BufferedReader reader = new BufferedReader(fileReader)) { - MatcherUtil.read(reader, true, newToOld::addClass, newToOld::addField, newToOld::addMethod); - } - } - } - - private void readCounters(File counterFile) throws IOException { - Path counterPath = getExternalCounterFile(); - - if (counterPath != null && Files.exists(counterPath)) { - counterFile = counterPath.toFile(); - } - - try (FileReader fileReader = new FileReader(counterFile)) { - try (BufferedReader reader = new BufferedReader(fileReader)) { - String line; - while ((line = reader.readLine()) != null) { - if (line.startsWith("# INTERMEDIARY-COUNTER")) { - String[] parts = line.split(" "); - counters.put(parts[2], Integer.parseInt(parts[3])); - } - } - } - } - } - - private void writeCounters(BufferedWriter writer) throws IOException { - StringJoiner counterLines = new StringJoiner("\n"); - - for (Map.Entry counter : counters.entrySet()) { - counterLines.add("# INTERMEDIARY-COUNTER " + counter.getKey() + " " + counter.getValue()); - } - - writer.write(counterLines.toString()); - Path counterPath = getExternalCounterFile(); - - if (counterPath != null) { - Files.write(counterPath, counterLines.toString().getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - } - } - - private Path getExternalCounterFile() { - if (System.getProperty("stitch.counter") != null) { - return Paths.get(System.getProperty("stitch.counter")); - } - return null; - } + private final Map counters = new HashMap<>(); + private final Map values = new IdentityHashMap<>(); + private GenMap oldToIntermediary, newToOld; + private GenMap newToIntermediary; + private boolean interactive = true; + private boolean writeAll = false; + private Scanner scanner = new Scanner(System.in); + + private String targetNamespace = "net/minecraft/"; + private final List obfuscatedPatterns = new ArrayList(); + + GenState() { + this.obfuscatedPatterns.add(Pattern.compile("^[^/]*$")); // Default obfuscation. Minecraft classes without a package are obfuscated. + } + + public void setWriteAll(boolean writeAll) { + this.writeAll = writeAll; + } + + public void disableInteractive() { + interactive = false; + } + + public String next(AbstractJarEntry entry, String name) { + return name + "_" + values.computeIfAbsent(entry, (e) -> { + int v = counters.getOrDefault(name, 1); + counters.put(name, v + 1); + return v; + }); + } + + public void setTargetNamespace(final String namespace) { + if (namespace.lastIndexOf("/") != (namespace.length() - 1)) { + this.targetNamespace = namespace + "/"; + } else { + this.targetNamespace = namespace; + } + } + + public void clearObfuscatedPatterns() { + this.obfuscatedPatterns.clear(); + } + + public void addObfuscatedPattern(String regex) throws PatternSyntaxException { + this.obfuscatedPatterns.add(Pattern.compile(regex)); + } + + public void setCounter(String key, int value) { + counters.put(key, value); + } + + public Map getCounters() { + return Collections.unmodifiableMap(counters); + } + + public void generate(File file, JarRootEntry jarEntry, JarRootEntry jarOld) throws IOException { + if (file.exists()) { + System.err.println("Target file exists - loading..."); + newToIntermediary = new GenMap(); + + try (FileInputStream inputStream = new FileInputStream(file)) { + newToIntermediary.load( + MappingsProvider.readTinyMappings(inputStream), + "official", + "intermediary" + ); + } + } + + try (FileWriter fileWriter = new FileWriter(file)) { + try (BufferedWriter writer = new BufferedWriter(fileWriter)) { + writer.write("v1\tofficial\tintermediary\n"); + + for (JarClassEntry c : jarEntry.getClasses()) { + addClass(writer, c, jarOld, jarEntry, this.targetNamespace); + } + + writeCounters(writer); + } + } + } + + public static boolean isMappedClass(ClassStorage storage, JarClassEntry c) { + return !c.isAnonymous(); + } + + public static boolean isMappedField(ClassStorage storage, JarClassEntry c, JarFieldEntry f) { + return isUnmappedFieldName(f.getName()); + } + + public static boolean isUnmappedFieldName(String name) { + return name.length() <= 2 || (name.length() == 3 && name.charAt(2) == '_'); + } + + public static boolean isMappedMethod(ClassStorage storage, JarClassEntry c, JarMethodEntry m) { + return isUnmappedMethodName(m.getName()) && m.isSource(storage, c); + } + + public static boolean isUnmappedMethodName(String name) { + return (name.length() <= 2 || (name.length() == 3 && name.charAt(2) == '_')) + && name.charAt(0) != '<'; + } + + @Nullable + private String getFieldName(ClassStorage storage, JarClassEntry c, JarFieldEntry f) { + if (!isMappedField(storage, c, f)) { + return null; + } + + if (newToIntermediary != null) { + EntryTriple findEntry = newToIntermediary.getField(c.getFullyQualifiedName(), f.getName(), f.getDescriptor()); + + if (findEntry != null) { + if (findEntry.getName().contains("field_")) { + return findEntry.getName(); + } else { + String newName = next(f, "field"); + System.out.println(findEntry.getName() + " is now " + newName); + return newName; + } + } + } + + if (newToOld != null) { + EntryTriple findEntry = newToOld.getField(c.getFullyQualifiedName(), f.getName(), f.getDescriptor()); + + if (findEntry != null) { + findEntry = oldToIntermediary.getField(findEntry); + + if (findEntry != null) { + if (findEntry.getName().contains("field_")) { + return findEntry.getName(); + } else { + String newName = next(f, "field"); + System.out.println(findEntry.getName() + " is now " + newName); + return newName; + } + } + } + } + + return next(f, "field"); + } + + private final Map methodNames = new IdentityHashMap<>(); + + private String getPropagation(ClassStorage storage, JarClassEntry classEntry) { + if (classEntry == null) { + return ""; + } + + StringBuilder builder = new StringBuilder(classEntry.getFullyQualifiedName()); + List strings = new ArrayList<>(); + String scs = getPropagation(storage, classEntry.getSuperClass(storage)); + + if (!scs.isEmpty()) { + strings.add(scs); + } + + for (JarClassEntry ce : classEntry.getInterfaces(storage)) { + scs = getPropagation(storage, ce); + + if (!scs.isEmpty()) { + strings.add(scs); + } + } + + if (!strings.isEmpty()) { + builder.append("<-"); + + if (strings.size() == 1) { + builder.append(strings.get(0)); + } else { + builder.append("["); + builder.append(StitchUtil.join(",", strings)); + builder.append("]"); + } + } + + return builder.toString(); + } + + private String getNamesListEntry(ClassStorage storage, JarClassEntry classEntry) { + StringBuilder builder = new StringBuilder(getPropagation(storage, classEntry)); + + if (classEntry.isInterface()) { + builder.append("(itf)"); + } + + return builder.toString(); + } + + private Set findNames(ClassStorage storageOld, ClassStorage storageNew, JarClassEntry c, JarMethodEntry m, Map> names) { + Set allEntries = new HashSet<>(); + findNames(storageOld, storageNew, c, m, names, allEntries); + return allEntries; + } + + private void findNames(ClassStorage storageOld, ClassStorage storageNew, JarClassEntry c, JarMethodEntry m, Map> names, Set usedMethods) { + if (!usedMethods.add(m)) { + return; + } + + String suffix = "." + m.getName() + m.getDescriptor(); + + if ((m.getAccess() & Opcodes.ACC_BRIDGE) != 0) { + suffix += "(bridge)"; + } + + List ccList = m.getMatchingEntries(storageNew, c); + + for (JarClassEntry cc : ccList) { + EntryTriple findEntry = null; + + if (newToIntermediary != null) { + findEntry = newToIntermediary.getMethod(cc.getFullyQualifiedName(), m.getName(), m.getDescriptor()); + + if (findEntry != null) { + names.computeIfAbsent(findEntry.getName(), (s) -> new TreeSet<>()).add(getNamesListEntry(storageNew, cc) + suffix); + } + } + + if (findEntry == null && newToOld != null) { + findEntry = newToOld.getMethod(cc.getFullyQualifiedName(), m.getName(), m.getDescriptor()); + + if (findEntry != null) { + EntryTriple newToOldEntry = findEntry; + findEntry = oldToIntermediary.getMethod(newToOldEntry); + + if (findEntry != null) { + names.computeIfAbsent(findEntry.getName(), (s) -> new TreeSet<>()).add(getNamesListEntry(storageNew, cc) + suffix); + } else { + // more involved... + JarClassEntry oldBase = storageOld.getClass(newToOldEntry.getOwner(), false); + + if (oldBase != null) { + JarMethodEntry oldM = oldBase.getMethod(newToOldEntry.getName() + newToOldEntry.getDesc()); + List cccList = oldM.getMatchingEntries(storageOld, oldBase); + + for (JarClassEntry ccc : cccList) { + findEntry = oldToIntermediary.getMethod(ccc.getFullyQualifiedName(), oldM.getName(), oldM.getDescriptor()); + + if (findEntry != null) { + names.computeIfAbsent(findEntry.getName(), (s) -> new TreeSet<>()).add(getNamesListEntry(storageOld, ccc) + suffix); + } + } + } + } + } + } + } + + for (JarClassEntry mc : ccList) { + for (Pair pair : mc.getRelatedMethods(m)) { + findNames(storageOld, storageNew, pair.getLeft(), pair.getLeft().getMethod(pair.getRight()), names, usedMethods); + } + } + } + + @Nullable + private String getMethodName(ClassStorage storageOld, ClassStorage storageNew, JarClassEntry c, JarMethodEntry m) { + if (!isMappedMethod(storageNew, c, m)) { + return null; + } + + if (methodNames.containsKey(m)) { + return methodNames.get(m); + } + + if (newToOld != null || newToIntermediary != null) { + Map> names = new HashMap<>(); + Set allEntries = findNames(storageOld, storageNew, c, m, names); + + for (JarMethodEntry mm : allEntries) { + if (methodNames.containsKey(mm)) { + return methodNames.get(mm); + } + } + + if (names.size() > 1) { + System.out.println("Conflict detected - matched same target name!"); + List nameList = new ArrayList<>(names.keySet()); + Collections.sort(nameList); + + for (int i = 0; i < nameList.size(); i++) { + String s = nameList.get(i); + System.out.println((i+1) + ") " + s + " <- " + StitchUtil.join(", ", names.get(s))); + } + + if (!interactive) { + throw new RuntimeException("Conflict detected!"); + } + + while (true) { + String cmd = scanner.nextLine(); + int i; + + try { + i = Integer.parseInt(cmd); + } catch (NumberFormatException e) { + e.printStackTrace(); + continue; + } + + if (i >= 1 && i <= nameList.size()) { + for (JarMethodEntry mm : allEntries) { + methodNames.put(mm, nameList.get(i - 1)); + } + + System.out.println("OK!"); + return nameList.get(i - 1); + } + } + } else if (names.size() == 1) { + String s = names.keySet().iterator().next(); + + for (JarMethodEntry mm : allEntries) { + methodNames.put(mm, s); + } + + if (s.contains("method_")) { + return s; + } else { + String newName = next(m, "method"); + System.out.println(s + " is now " + newName); + return newName; + } + } + } + + return next(m, "method"); + } + + private void addClass(BufferedWriter writer, JarClassEntry c, ClassStorage storageOld, ClassStorage storage, String translatedPrefix) throws IOException { + String className = c.getName(); + String cname = ""; + String prefixSaved = translatedPrefix; + + if (!this.obfuscatedPatterns.stream().anyMatch(p -> p.matcher(className).matches())) { + translatedPrefix = c.getFullyQualifiedName(); + } else { + if (!isMappedClass(storage, c)) { + cname = c.getName(); + } else { + cname = null; + + if (newToIntermediary != null) { + String findName = newToIntermediary.getClass(c.getFullyQualifiedName()); + + if (findName != null) { + String[] r = findName.split("\\$"); + cname = r[r.length - 1]; + + if (r.length == 1) { + translatedPrefix = ""; + } + } + } + + if (cname == null && newToOld != null) { + String findName = newToOld.getClass(c.getFullyQualifiedName()); + + if (findName != null) { + findName = oldToIntermediary.getClass(findName); + + if (findName != null) { + String[] r = findName.split("\\$"); + cname = r[r.length - 1]; + + if (r.length == 1) { + translatedPrefix = ""; + } + } + } + } + + if (cname != null && !cname.contains("class_")) { + String newName = next(c, "class"); + System.out.println(cname + " is now " + newName); + cname = newName; + translatedPrefix = prefixSaved; + } + + if (cname == null) { + cname = next(c, "class"); + } + } + } + + writer.write("CLASS\t" + c.getFullyQualifiedName() + "\t" + translatedPrefix + cname + "\n"); + + for (JarFieldEntry f : c.getFields()) { + String fName = getFieldName(storage, c, f); + + if (fName == null) { + fName = f.getName(); + } + + if (fName != null) { + writer.write("FIELD\t" + c.getFullyQualifiedName() + + "\t" + f.getDescriptor() + + "\t" + f.getName() + + "\t" + fName + "\n"); + } + } + + for (JarMethodEntry m : c.getMethods()) { + String mName = getMethodName(storageOld, storage, c, m); + + if (mName == null) { + if (!m.getName().startsWith("<") && m.isSource(storage, c)) { + mName = m.getName(); + } + } + + if (mName != null) { + writer.write("METHOD\t" + c.getFullyQualifiedName() + + "\t" + m.getDescriptor() + + "\t" + m.getName() + + "\t" + mName + "\n"); + } + } + + for (JarClassEntry cc : c.getInnerClasses()) { + addClass(writer, cc, storageOld, storage, translatedPrefix + cname + "$"); + } + } + + public void prepareRewrite(File oldMappings) throws IOException { + oldToIntermediary = new GenMap(); + newToOld = new GenMap.Dummy(); + + // TODO: only read once + readCounters(oldMappings); + + try (FileInputStream inputStream = new FileInputStream(oldMappings)) { + oldToIntermediary.load( + MappingsProvider.readTinyMappings(inputStream), + "official", + "intermediary" + ); + } + } + + public void prepareUpdate(File oldMappings, File matches) throws IOException { + oldToIntermediary = new GenMap(); + newToOld = new GenMap(); + + // TODO: only read once + readCounters(oldMappings); + + try (FileInputStream inputStream = new FileInputStream(oldMappings)) { + oldToIntermediary.load( + MappingsProvider.readTinyMappings(inputStream), + "official", + "intermediary" + ); + } + + try (FileReader fileReader = new FileReader(matches)) { + try (BufferedReader reader = new BufferedReader(fileReader)) { + MatcherUtil.read(reader, true, newToOld::addClass, newToOld::addField, newToOld::addMethod); + } + } + } + + private void readCounters(File counterFile) throws IOException { + Path counterPath = getExternalCounterFile(); + + if (counterPath != null && Files.exists(counterPath)) { + counterFile = counterPath.toFile(); + } + + try (FileReader fileReader = new FileReader(counterFile)) { + try (BufferedReader reader = new BufferedReader(fileReader)) { + String line; + + while ((line = reader.readLine()) != null) { + if (line.startsWith("# INTERMEDIARY-COUNTER")) { + String[] parts = line.split(" "); + counters.put(parts[2], Integer.parseInt(parts[3])); + } + } + } + } + } + + private void writeCounters(BufferedWriter writer) throws IOException { + StringJoiner counterLines = new StringJoiner("\n"); + + for (Map.Entry counter : counters.entrySet()) { + counterLines.add("# INTERMEDIARY-COUNTER " + counter.getKey() + " " + counter.getValue()); + } + + writer.write(counterLines.toString()); + Path counterPath = getExternalCounterFile(); + + if (counterPath != null) { + Files.write(counterPath, counterLines.toString().getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + } + + private Path getExternalCounterFile() { + if (System.getProperty("stitch.counter") != null) { + return Paths.get(System.getProperty("stitch.counter")); + } + + return null; + } } diff --git a/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandMergeTinyV2.java b/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandMergeTinyV2.java index 4257ec0..b6eba4f 100644 --- a/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandMergeTinyV2.java +++ b/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandMergeTinyV2.java @@ -43,34 +43,36 @@ * the same namespace as the first column and a different namespace as the second column. * The first column of the output will contain the shared namespace, * the second column of the output would be the second namespace of input a, - * and the third column of the output would be the second namespace of input b - *

- * Descriptors will remain as-is (using the namespace of the first column) - *

- *

- * For example: - *

- * Input A: - * intermediary named - * c net/minecraft/class_123 net/minecraft/somePackage/someClass - * m (Lnet/minecraft/class_124;)V method_1234 someMethod - *

- * Input B: - * intermediary official - * c net/minecraft/class_123 a - * m (Lnet/minecraft/class_124;)V method_1234 a - *

- * The output will be: - *

- * intermediary named official - * c net/minecraft/class_123 net/minecraft/somePackage/someClass a - * m (Lnet/minecraft/class_124;)V method_1234 someMethod a - *

+ * and the third column of the output would be the second namespace of input b. + * + *

Descriptors will remain as-is (using the namespace of the first column).

+ * + *

For example, for input A: + *


+ * intermediary	 named
+ * c	net/minecraft/class_123	net/minecraft/somePackage/someClass
+ * m	(Lnet/minecraft/class_124;)V	method_1234 someMethod
+ * 
+ * and input B: + *

+ * intermediary	official
+ * c	net/minecraft/class_123	a
+ * m	(Lnet/minecraft/class_124;)V	method_1234	a
+ * 
+ * the output will be: + *

+ * intermediary	named	official
+ * c	net/minecraft/class_123	net/minecraft/somePackage/someClass	a
+ * m	(Lnet/minecraft/class_124;)V	method_1234 someMethod	a
+ * 
+ *

+ * *

* After intermediary-named mappings are obtained, * and official-intermediary mappings are obtained and swapped using CommandReorderTinyV2, Loom merges them using this command, * and then reorders it to official-intermediary-named using CommandReorderTinyV2 again. * This is a convenient way of storing all the mappings in Loom. + *

*/ public class CommandMergeTinyV2 extends Command { public CommandMergeTinyV2() { @@ -78,7 +80,7 @@ public CommandMergeTinyV2() { } /** - * and are the tiny files to be merged. The result will be written to . + * {@code } and {@code } are the tiny files to be merged. The result will be written to {@code }. */ @Override public String getHelpString() { @@ -100,20 +102,21 @@ public void run(String[] args) throws IOException { TinyFile tinyFileB = TinyV2Reader.read(inputB); TinyHeader headerA = tinyFileA.getHeader(); TinyHeader headerB = tinyFileB.getHeader(); + if (headerA.getNamespaces().size() != 2) { throw new IllegalArgumentException(inputA + " must have exactly 2 namespaces."); } + if (headerB.getNamespaces().size() != 2) { throw new IllegalArgumentException(inputB + " must have exactly 2 namespaces."); } if (!headerA.getNamespaces().get(0).equals(headerB.getNamespaces().get(0))) { - throw new IllegalArgumentException( - String.format("The input tiny files must have the same namespaces as the first column. " + - "(%s has %s while %s has %s)", - inputA, headerA.getNamespaces().get(0), inputB, headerB.getNamespaces().get(0)) - ); + throw new IllegalArgumentException(String.format( + "The input tiny files must have the same namespaces as the first column. " + "(%s has %s while %s has %s)", + inputA, headerA.getNamespaces().get(0), inputB, headerB.getNamespaces().get(0))); } + System.out.println("Merging " + inputA + " with " + inputB); TinyFile mergedFile = merge(tinyFileA, tinyFileB); @@ -121,12 +124,10 @@ public void run(String[] args) throws IOException { System.out.println("Merged mappings written to " + Paths.get(args[2])); } - private TinyFile merge(TinyFile inputA, TinyFile inputB) { //TODO: how to merge properties? TinyHeader mergedHeader = mergeHeaders(inputA.getHeader(), inputB.getHeader()); - List keyUnion = keyUnion(inputA.getClassEntries(), inputB.getClassEntries()); Map inputAClasses = inputA.mapClassesByFirstNamespace(); @@ -153,29 +154,28 @@ private TinyClass matchEnclosingClassIfNeeded(String key, TinyClass tinyClass, M } /** - * Takes something like net/minecraft/class_123$class_124 that doesn't have a mapping, tries to find net/minecraft/class_123 - * , say the mapping of net/minecraft/class_123 is path/to/someclass and then returns a class of the form - * path/to/someclass$class124 + * Takes something like {@code net/minecraft/class_123$class_124} that doesn't have a mapping, tries to find {@code net/minecraft/class_123}. + * Say the mapping of {@code net/minecraft/class_123} is {@code path/to/someclass} and then returns a class of the form + * {@code path/to/someclass$class124} */ @Nonnull private String matchEnclosingClass(String sharedName, Map inputBClassBySharedNamespace) { String[] path = sharedName.split(escape("$")); int parts = path.length; + for (int i = parts - 2; i >= 0; i--) { String currentPath = String.join("$", Arrays.copyOfRange(path, 0, i + 1)); TinyClass match = inputBClassBySharedNamespace.get(currentPath); if (match != null && !match.getClassNames().get(1).isEmpty()) { return match.getClassNames().get(1) - + "$" + String.join("$", Arrays.copyOfRange(path, i + 1, path.length)); - + + "$" + String.join("$", Arrays.copyOfRange(path, i + 1, path.length)); } } return sharedName; } - private TinyClass mergeClasses(String sharedClassName, @Nonnull TinyClass classA, @Nonnull TinyClass classB) { List mergedNames = mergeNames(sharedClassName, classA, classB); List mergedComments = mergeComments(classA.getComments(), classB.getComments()); @@ -184,7 +184,7 @@ private TinyClass mergeClasses(String sharedClassName, @Nonnull TinyClass classA Map, TinyMethod> methodsA = classA.mapMethodsByFirstNamespaceAndDescriptor(); Map, TinyMethod> methodsB = classB.mapMethodsByFirstNamespaceAndDescriptor(); List mergedMethods = map(methodKeyUnion, - (Pair k) -> mergeMethods(k.getLeft(), methodsA.get(k), methodsB.get(k))); + (Pair k) -> mergeMethods(k.getLeft(), methodsA.get(k), methodsB.get(k))); List fieldKeyUnion = keyUnion(classA.getFields(), classB.getFields()); Map fieldsA = classA.mapFieldsByFirstNamespace(); @@ -194,8 +194,8 @@ private TinyClass mergeClasses(String sharedClassName, @Nonnull TinyClass classA return new TinyClass(mergedNames, mergedMethods, mergedFields, mergedComments); } - private static final TinyMethod EMPTY_METHOD = new TinyMethod(null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); - + private static final TinyMethod EMPTY_METHOD = new TinyMethod(null, Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), Collections.emptyList()); private TinyMethod mergeMethods(String sharedMethodName, @Nullable TinyMethod methodA, @Nullable TinyMethod methodB) { List mergedNames = mergeNames(sharedMethodName, methodA, methodB); @@ -204,10 +204,9 @@ private TinyMethod mergeMethods(String sharedMethodName, @Nullable TinyMethod me List mergedComments = mergeComments(methodA.getComments(), methodB.getComments()); String descriptor = methodA.getMethodDescriptorInFirstNamespace() != null ? methodA.getMethodDescriptorInFirstNamespace() - : methodB.getMethodDescriptorInFirstNamespace(); + : methodB.getMethodDescriptorInFirstNamespace(); if (descriptor == null) throw new RuntimeException("no descriptor for key " + sharedMethodName); - //TODO: this won't work too well when the first namespace is named or there is more than one named namespace (hack) List mergedParameters = new ArrayList<>(); addParameters(methodA, mergedParameters, 2); @@ -233,18 +232,17 @@ private void addLocalVariables(TinyMethod method, List addTo, List names = new ArrayList<>(localVariable.getLocalVariableNames()); names.add(emptySpacePos, ""); addTo.add(new TinyLocalVariable(localVariable.getLvIndex(), localVariable.getLvStartOffset(), - localVariable.getLvTableIndex(), names, localVariable.getComments())); + localVariable.getLvTableIndex(), names, localVariable.getComments())); } } - private TinyField mergeFields(String sharedFieldName, @Nullable TinyField fieldA, @Nullable TinyField fieldB) { List mergedNames = mergeNames(sharedFieldName, fieldA, fieldB); List mergedComments = mergeComments(fieldA != null ? fieldA.getComments() : Collections.emptyList(), - fieldB != null ? fieldB.getComments() : Collections.emptyList()); + fieldB != null ? fieldB.getComments() : Collections.emptyList()); String descriptor = fieldA != null ? fieldA.getFieldDescriptorInFirstNamespace() - : fieldB != null ? fieldB.getFieldDescriptorInFirstNamespace() : null; + : fieldB != null ? fieldB.getFieldDescriptorInFirstNamespace() : null; if (descriptor == null) throw new RuntimeException("no descriptor for key " + sharedFieldName); return new TinyField(descriptor, mergedNames, mergedComments); @@ -269,7 +267,6 @@ private Stream> mapToFirstNamespaceAndDescriptor(TinyClass return tinyClass.getMethods().stream().map(m -> Pair.of(m.getMapping().get(0), m.getMethodDescriptorInFirstNamespace())); } - private List mergeNames(String key, @Nullable Mapping mappingA, @Nullable Mapping mappingB) { List merged = new ArrayList<>(); merged.add(key); @@ -303,5 +300,4 @@ private static String escape(String str) { private List map(List from, Function mapper) { return from.stream().map(mapper).collect(Collectors.toList()); } - } diff --git a/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandProposeV2FieldNames.java b/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandProposeV2FieldNames.java index 6e74d77..8b47b3c 100644 --- a/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandProposeV2FieldNames.java +++ b/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandProposeV2FieldNames.java @@ -24,14 +24,14 @@ import java.util.Map; import java.util.stream.Collectors; +import javax.annotation.Nullable; + import com.google.common.collect.Lists; import net.fabricmc.mappings.EntryTriple; import net.fabricmc.stitch.Command; import net.fabricmc.stitch.util.FieldNameFinder; -import javax.annotation.Nullable; - /** * Java stores the names of enums in the bytecode, and obfuscation doesn't get rid of it. We can use this for easy mappings. * This command adds all of the field mappings that FieldNameFinder finds (it overwrites existing mappings for those names). @@ -43,10 +43,12 @@ public CommandProposeV2FieldNames() { } /** - * is any Minecraft jar, and are mappings of that jar (the same version). - * with the additional field names will be written to .+ - * Assumes the input mappings are intermediary->yarn mappings! - * is a boolean ("true" or "false") deciding if existing yarn names should be replaced by the generated names. + *
    + *
  • {@code } is any Minecraft jar, and {@code } are mappings of that jar (the same version).
  • + *
  • {@code } with the additional field names will be written to {@code }.
  • + *
  • Assumes the input mappings are intermediary -> Yarn mappings!
  • + *
  • {@code } is a boolean ({@code true} or {@code false}) deciding if existing yarn names should be replaced by the generated names.
  • + *
*/ @Override public String getHelpString() { @@ -60,8 +62,8 @@ public boolean isArgumentCountValid(int count) { private Map generatedNamesOfClass(TinyClass tinyClass) { return tinyClass.getFields().stream().collect(Collectors.toMap( - (TinyField field) -> new EntryTriple(tinyClass.getClassNames().get(0), field.getFieldNames().get(0), field.getFieldDescriptorInFirstNamespace()) - , field -> field)); + (TinyField field) -> new EntryTriple(tinyClass.getClassNames().get(0), field.getFieldNames().get(0), + field.getFieldDescriptorInFirstNamespace()), field -> field)); } @Override @@ -72,10 +74,10 @@ public void run(String[] args) throws Exception { Boolean shouldReplace = parseBooleanOrNull(args[3]); // Validation - if(!inputJar.exists()) throw new IllegalArgumentException("Cannot find input jar at " + inputJar); - if(!Files.exists(inputMappings)) throw new IllegalArgumentException("Cannot find input mappings at " + inputMappings); - if(Files.exists(outputMappings)) System.out.println("Warning: existing file will be replaced by output mappings"); - if(shouldReplace == null) throw new IllegalArgumentException(" must be 'true' or 'false'"); + if (!inputJar.exists()) throw new IllegalArgumentException("Cannot find input jar at " + inputJar); + if (!Files.exists(inputMappings)) throw new IllegalArgumentException("Cannot find input mappings at " + inputMappings); + if (Files.exists(outputMappings)) System.out.println("Warning: existing file will be replaced by output mappings"); + if (shouldReplace == null) throw new IllegalArgumentException(" must be 'true' or 'false'"); // entrytriple from the input jar namespace Map generatedFieldNames = new FieldNameFinder().findNames(new File(args[0])); @@ -85,12 +87,13 @@ public void run(String[] args) throws Exception { Map fieldsMap = new HashMap<>(); tinyFile.getClassEntries().stream().map(this::generatedNamesOfClass).forEach(map -> map.forEach(fieldsMap::put)); Map classMap = tinyFile.mapClassesByFirstNamespace(); - int replaceCount = 0; + for (Map.Entry entry : generatedFieldNames.entrySet()) { EntryTriple key = entry.getKey(); String newName = entry.getValue(); TinyField field = fieldsMap.get(key); + // If the field name exists, replace the name with the auto-generated name, as long as is true. if (field != null) { if (shouldReplace) { @@ -99,13 +102,13 @@ public void run(String[] args) throws Exception { } } else { TinyClass tinyClass = classMap.get(key.getOwner()); + // If field name does not exist, but its class does exist, create a new mapping with the supplied generated name. if (tinyClass != null) { tinyClass.getFields().add(new TinyField(key.getDesc(), Lists.newArrayList(key.getName(), newName), Lists.newArrayList())); replaceCount++; } } - } System.err.println("Replaced " + replaceCount + " names in the mappings."); @@ -118,8 +121,13 @@ public void run(String[] args) throws Exception { @Nullable private Boolean parseBooleanOrNull(String booleanLiteral) { String lowerCase = booleanLiteral.toLowerCase(); - if (lowerCase.equals("true")) return Boolean.TRUE; - else if (lowerCase.equals("false")) return Boolean.FALSE; - else return null; + + if (lowerCase.equals("true")) { + return Boolean.TRUE; + } else if (lowerCase.equals("false")) { + return Boolean.FALSE; + } + + return null; } } diff --git a/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandReorderTinyV2.java b/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandReorderTinyV2.java index bcf1e36..1befa35 100644 --- a/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandReorderTinyV2.java +++ b/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandReorderTinyV2.java @@ -34,23 +34,29 @@ import net.fabricmc.stitch.Command; /** - * - Reorders the columns in the tiny file - * - Remaps the descriptors to use the newly first column. - *

- * For example: - *

- * This: - * intermediary named official - * c net/minecraft/class_123 net/minecraft/somePackage/someClass a - * m (Lnet/minecraft/class_124;)V method_1234 someMethod a - *

+ *

    + *
  • Reorders the columns in the tiny file
  • + *
  • Remaps the descriptors to use the newly first column
  • + *
+ * + *

For example, this: + *


+ * intermediary	named	official
+ * c	net/minecraft/class_123	net/minecraft/somePackage/someClass	a
+ * m	(Lnet/minecraft/class_124;)V	method_1234 someMethod	a
+ * 
* Reordered to official intermediary named: - * official intermediary named - * c a net/minecraft/class_123 net/minecraft/somePackage/someClass - * m (La;)V a method_1234 someMethod + *

+ * official	intermediary	named
+ * c	a	net/minecraft/class_123	net/minecraft/somePackage/someClass
+ * m	(La;)V	a	method_1234	someMethod
+ * 
+ *

+ * *

* This is used to reorder the the official-intermediary mappings to be intermediary-official, so they can be merged with * intermediary-named in CommandMergeTinyV2, and then reorder the outputted intermediary-official-named to official-intermediary-named. + *

*/ public class CommandReorderTinyV2 extends Command { public CommandReorderTinyV2() { @@ -58,8 +64,8 @@ public CommandReorderTinyV2() { } /** - * Reorders the columns in according to [new name order...] and puts the result in . - * new name order is for example "official intermediary named" + * Reorders the columns in {@code } according to {@code [new name order...]} and puts the result in {@code }. + * {@code new name order} is, for example, {@code official intermediary named}. */ @Override public String getHelpString() { @@ -81,8 +87,8 @@ public void run(String[] args) throws Exception { validateNamespaces(newOrder, tinyFile); Map mappingCopy = tinyFile.getClassEntries().stream() - .collect(Collectors.toMap(c -> c.getClassNames().get(0), - c -> new TinyClass(new ArrayList<>(c.getClassNames()), c.getMethods(), c.getFields(), c.getComments()))); + .collect(Collectors.toMap(c -> c.getClassNames().get(0), + c -> new TinyClass(new ArrayList<>(c.getClassNames()), c.getMethods(), c.getFields(), c.getComments()))); int newFirstNamespaceOldIndex = tinyFile.getHeader().getNamespaces().indexOf(newOrder.get(0)); reorder(tinyFile, newOrder); @@ -96,13 +102,14 @@ private void validateNamespaces(List newOrder, TinyFile tinyFile) { HashSet providedNamespacesOrderless = new HashSet<>(newOrder); if (!fileNamespacesOrderless.equals(providedNamespacesOrderless)) { - throw new IllegalArgumentException("The tiny file has different namespaces than those specified." + - " specified: " + providedNamespacesOrderless.toString() + ", file: " + fileNamespacesOrderless.toString()); + throw new IllegalArgumentException("The tiny file has different namespaces than those specified." + + " specified: " + providedNamespacesOrderless.toString() + ", file: " + fileNamespacesOrderless.toString()); } } private void reorder(TinyFile tinyFile, List newOrder) { Map indexMapping = new HashMap<>(); + for (int i = 0; i < newOrder.size(); i++) { indexMapping.put(tinyFile.getHeader().getNamespaces().indexOf(newOrder.get(i)), i); } @@ -112,7 +119,9 @@ private void reorder(TinyFile tinyFile, List newOrder) { for (int i = names.size(); i < newOrder.size(); i++) { names.add(""); } + List namesCopy = new ArrayList<>(names); + for (int i = 0; i < namesCopy.size(); i++) { names.set(indexMapping.get(i), namesCopy.get(i)); } @@ -124,29 +133,34 @@ private void remapDescriptors(TinyFile tinyFile, Map mappings for (TinyMethod method : tinyClass.getMethods()) { remapMethodDescriptor(method, mappings, targetNamespace); } + for (TinyField field : tinyClass.getFields()) { remapFieldDescriptor(field, mappings, targetNamespace); } } } - /** - * In this case the visitor is not a nice man and reorganizes the house as he sees fit + * In this case the visitor is not a nice man and reorganizes the house as he sees fit. */ private void visitNames(TinyFile tinyFile, Consumer> namesVisitor) { namesVisitor.accept(tinyFile.getHeader().getNamespaces()); + for (TinyClass tinyClass : tinyFile.getClassEntries()) { namesVisitor.accept(tinyClass.getClassNames()); + for (TinyMethod method : tinyClass.getMethods()) { namesVisitor.accept(method.getMethodNames()); + for (TinyMethodParameter parameter : method.getParameters()) { namesVisitor.accept(parameter.getParameterNames()); } + for (TinyLocalVariable localVariable : method.getLocalVariables()) { namesVisitor.accept(localVariable.getLocalVariableNames()); } } + for (TinyField field : tinyClass.getFields()) { namesVisitor.accept(field.getFieldNames()); } @@ -164,24 +178,22 @@ private void remapFieldDescriptor(TinyField field, Map mappin private void remapMethodDescriptor(TinyMethod method, Map mappings, int targetNamespace) { String descriptor = method.getMethodDescriptorInFirstNamespace(); String[] paramsAndReturnType = descriptor.split(Pattern.quote(")")); - if (paramsAndReturnType.length != 2) { - throw new IllegalArgumentException( - "method descriptor '" + descriptor + "' is of an unknown format."); - } + + if (paramsAndReturnType.length != 2) { + throw new IllegalArgumentException("method descriptor '" + descriptor + "' is of an unknown format."); + } + List params = parseParameterDescriptors(paramsAndReturnType[0].substring(1)); String returnType = paramsAndReturnType[1]; - List paramsMapped = params.stream().map(p -> remapType(p, mappings, targetNamespace)).collect(Collectors.toList()); String returnTypeMapped = returnType.equals("V") ? "V" : remapType(returnType, mappings, targetNamespace); - String newDescriptor = "(" + String.join("", paramsMapped) + ")" + returnTypeMapped; - method.setMethodDescriptorInFirstNamespace(newDescriptor); + method.setMethodDescriptorInFirstNamespace(newDescriptor); } private static final Collection primitiveTypeNames = Arrays.asList("B", "C", "D", "F", "I", "J", "S", "Z"); - private List parseParameterDescriptors(String concatenatedParameterDescriptors) { List parameterDescriptors = new ArrayList<>(); boolean inClassName = false; @@ -190,12 +202,14 @@ private List parseParameterDescriptors(String concatenatedParameterDescr for (int i = 0; i < concatenatedParameterDescriptors.length(); i++) { char c = concatenatedParameterDescriptors.charAt(i); + if (inClassName) { if (c == ';') { - if (currentClassName.length() == 0) { - throw new IllegalArgumentException( - "Empty class name in parameter list " + concatenatedParameterDescriptors + " at position " + i); - } + if (currentClassName.length() == 0) { + throw new IllegalArgumentException("Empty class name in parameter list " + concatenatedParameterDescriptors + + " at position " + i); + } + parameterDescriptors.add(Strings.repeat("[", inArrayNestingLevel) + "L" + currentClassName.toString() + ";"); inArrayNestingLevel = 0; currentClassName = new StringBuilder(); @@ -212,19 +226,15 @@ private List parseParameterDescriptors(String concatenatedParameterDescr } else if (c == 'L') { inClassName = true; } else { - throw new IllegalArgumentException( - "Unexpected special character " + c + " in parameter descriptor list " - + concatenatedParameterDescriptors); + throw new IllegalArgumentException("Unexpected special character " + c + " in parameter descriptor list " + + concatenatedParameterDescriptors); } } - } return parameterDescriptors; - } - /** * Remaps type from namespace X, to the namespace of targetNamespaceIndex in mappings, where mappings * is a mapping from names in namespace X to the names in all other namespaces. @@ -232,9 +242,11 @@ private List parseParameterDescriptors(String concatenatedParameterDescr private String remapType(String type, Map mappings, int targetNamespaceIndex) { if (type.isEmpty()) throw new IllegalArgumentException("types cannot be empty strings"); if (primitiveTypeNames.contains(type)) return type; - if (type.charAt(0) == '[') { - return "[" + remapType(type.substring(1), mappings, targetNamespaceIndex); - } + + if (type.charAt(0) == '[') { + return "[" + remapType(type.substring(1), mappings, targetNamespaceIndex); + } + if (type.charAt(0) == 'L' && type.charAt(type.length() - 1) == ';') { String className = type.substring(1, type.length() - 1); TinyClass mapping = mappings.get(className); @@ -243,8 +255,5 @@ private String remapType(String type, Map mappings, int targe } throw new IllegalArgumentException("type descriptor '" + type + "' is of an unknown format."); - } - - } diff --git a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyClass.java b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyClass.java index 2b7c9f6..88e22ca 100644 --- a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyClass.java +++ b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyClass.java @@ -28,7 +28,7 @@ public class TinyClass implements Comparable, Mapping { @Override public String toString() { return "TinyClass(names = [" + String.join(", ", classNames) + "], " + methods.size() + " methods, " - + fields.size() + " fields, " + comments.size() + " comments)"; + + fields.size() + " fields, " + comments.size() + " comments)"; } private final List classNames; @@ -50,7 +50,6 @@ public TinyClass(List classNames) { this.comments = new ArrayList<>(); } - /** * Descriptors are also taken into account because methods can overload. * The key format is firstMethodName + descriptor @@ -63,7 +62,6 @@ public Map mapFieldsByFirstNamespace() { return fields.stream().collect(Collectors.toMap(f -> f.getFieldNames().get(0), f -> f)); } - public List getClassNames() { return classNames; } diff --git a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyField.java b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyField.java index 543faa2..ef2c14d 100644 --- a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyField.java +++ b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyField.java @@ -20,7 +20,6 @@ import java.util.List; public class TinyField implements Comparable, Mapping { - /** * For example when we have official -> named mappings the descriptor will be in official, but in named -> official * the descriptor will be in named. diff --git a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyHeader.java b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyHeader.java index a6be00d..4585a18 100644 --- a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyHeader.java +++ b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyHeader.java @@ -20,11 +20,10 @@ import java.util.Map; public class TinyHeader { - private final List namespaces; private final int majorVersion; private final int minorVersion; - private final Map properties; + private final Map properties; public TinyHeader(List namespaces, int majorVersion, int minorVersion, Map properties) { this.namespaces = namespaces; diff --git a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyLocalVariable.java b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyLocalVariable.java index 56e0495..221b1d4 100644 --- a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyLocalVariable.java +++ b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyLocalVariable.java @@ -20,12 +20,9 @@ import java.util.List; public class TinyLocalVariable implements Comparable, Mapping { - private final int lvIndex; private final int lvStartOffset; - /** - * Will be -1 when there is no lvt index - */ + /** Will be -1 when there is no lvt index. */ private final int lvTableIndex; private final List localVariableNames; private final Collection comments; @@ -38,7 +35,6 @@ public TinyLocalVariable(int lvIndex, int lvStartOffset, int lvTableIndex, List< this.comments = comments; } - public int getLvIndex() { return lvIndex; } diff --git a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyMethod.java b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyMethod.java index 14c2796..608da98 100644 --- a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyMethod.java +++ b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyMethod.java @@ -22,7 +22,6 @@ import java.util.stream.Collectors; public class TinyMethod implements Comparable, Mapping { - @Override public String toString() { return "TinyMethod(names = [" + String.join(", ", methodNames) + "], desc = " + methodDescriptorInFirstNamespace @@ -79,7 +78,7 @@ public Collection getComments() { @Override public int compareTo(TinyMethod o) { return (methodNames.get(0) + methodDescriptorInFirstNamespace) - .compareTo(o.methodNames.get(0) + o.methodDescriptorInFirstNamespace); + .compareTo(o.methodNames.get(0) + o.methodDescriptorInFirstNamespace); } public void setMethodDescriptorInFirstNamespace(String methodDescriptorInFirstNamespace) { diff --git a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyV2Reader.java b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyV2Reader.java index 1d935d4..a498958 100644 --- a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyV2Reader.java +++ b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyV2Reader.java @@ -44,7 +44,7 @@ private enum CommentType { private TinyHeader header; private int namespaceAmount; - // private String + // private String private Set classes = new HashSet<>(); private TinyClass currentClass; @@ -61,8 +61,8 @@ private List getNames(MappingGetter getter) { @Override public void start(TinyMetadata metadata) { - header = new TinyHeader(new ArrayList<>(metadata.getNamespaces()), metadata.getMajorVersion(), metadata.getMinorVersion(), - metadata.getProperties()); + header = new TinyHeader(new ArrayList<>(metadata.getNamespaces()), metadata.getMajorVersion(), + metadata.getMinorVersion(), metadata.getProperties()); namespaceAmount = header.getNamespaces().size(); } @@ -82,35 +82,32 @@ public void pushField(MappingGetter name, String descriptor) { @Override public void pushMethod(MappingGetter name, String descriptor) { - currentMethod = new TinyMethod( - descriptor, getNames(name), new HashSet<>(), new HashSet<>(), new ArrayList<>() - ); + currentMethod = new TinyMethod(descriptor, getNames(name), new HashSet<>(), new HashSet<>(), new ArrayList<>()); currentClass.getMethods().add(currentMethod); currentCommentType = CommentType.METHOD; } @Override public void pushParameter(MappingGetter name, int localVariableIndex) { - currentParameter = new TinyMethodParameter( - localVariableIndex, getNames(name), new ArrayList<>() - ); + currentParameter = new TinyMethodParameter(localVariableIndex, getNames(name), new ArrayList<>()); currentMethod.getParameters().add(currentParameter); currentCommentType = CommentType.PARAMETER; } @Override public void pushLocalVariable(MappingGetter name, int localVariableIndex, int localVariableStartOffset, int localVariableTableIndex) { - currentLocalVariable = new TinyLocalVariable( - localVariableIndex, localVariableStartOffset, localVariableTableIndex, getNames(name), new ArrayList<>() - ); + currentLocalVariable = new TinyLocalVariable(localVariableIndex, localVariableStartOffset, + localVariableTableIndex, getNames(name), new ArrayList<>()); currentMethod.getLocalVariables().add(currentLocalVariable); currentCommentType = CommentType.LOCAL_VARIABLE; } @Override public void pushComment(String comment) { - if (inComment) + if (inComment) { throw new RuntimeException("commenting on comment"); + } + switch (currentCommentType) { case CLASS: currentClass.getComments().add(comment); @@ -130,6 +127,7 @@ public void pushComment(String comment) { default: throw new RuntimeException("unexpected comment without parent"); } + inComment = true; } @@ -143,19 +141,19 @@ public void pop(int count) { CommentType last = currentCommentType; switch (last) { - case CLASS: - currentCommentType = null; - break; - case FIELD: - case METHOD: - currentCommentType = CommentType.CLASS; - break; - case PARAMETER: - case LOCAL_VARIABLE: - currentCommentType = CommentType.METHOD; - break; - default: - throw new IllegalStateException("visit stack is empty!"); + case CLASS: + currentCommentType = null; + break; + case FIELD: + case METHOD: + currentCommentType = CommentType.CLASS; + break; + case PARAMETER: + case LOCAL_VARIABLE: + currentCommentType = CommentType.METHOD; + break; + default: + throw new IllegalStateException("visit stack is empty!"); } } } @@ -167,6 +165,7 @@ private TinyFile getAST() { public static TinyFile read(Path readFrom) throws IOException { Visitor visitor = new Visitor(); + try (BufferedReader reader = Files.newBufferedReader(readFrom)) { TinyV2Factory.visit(reader, visitor); } diff --git a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyV2Writer.java b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyV2Writer.java index c27de9a..b365a38 100644 --- a/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyV2Writer.java +++ b/src/main/java/net/fabricmc/stitch/commands/tinyv2/TinyV2Writer.java @@ -57,8 +57,6 @@ private Indents() { public static final int FIELD_COMMENT = 2; public static final int PARAMETER_COMMENT = 3; public static final int LOCAL_VARIABLE_COMMENT = 3; - - } private TinyV2Writer() { @@ -76,10 +74,9 @@ private void instanceWrite(TinyFile tinyFile, Path writeTo) throws IOException { } } - private void writeHeader(TinyHeader header) { writeLine(Indents.HEADER, header.getNamespaces(), Prefixes.HEADER, - Integer.toString(header.getMajorVersion()), Integer.toString(header.getMinorVersion())); + Integer.toString(header.getMajorVersion()), Integer.toString(header.getMinorVersion())); header.getProperties().forEach((key, value) -> writeLine(Indents.PROPERTY, value)); } @@ -90,7 +87,6 @@ private void writeClass(TinyClass tinyClass) { tinyClass.getMethods().stream().sorted().forEach(this::writeMethod); tinyClass.getFields().stream().sorted().forEach(this::writeField); - } private void writeMethod(TinyMethod method) { @@ -100,22 +96,20 @@ private void writeMethod(TinyMethod method) { method.getParameters().stream().sorted().forEach(this::writeMethodParameter); method.getLocalVariables().stream().sorted().forEach(this::writeLocalVariable); - } private void writeMethodParameter(TinyMethodParameter parameter) { writeLine(Indents.PARAMETER, parameter.getParameterNames(), Prefixes.PARAMETER, Integer.toString(parameter.getLvIndex())); + for (String comment : parameter.getComments()) { writeComment(Indents.PARAMETER_COMMENT, comment); } - } private void writeLocalVariable(TinyLocalVariable localVariable) { writeLine(Indents.LOCAL_VARIABLE, localVariable.getLocalVariableNames(), Prefixes.VARIABLE, - Integer.toString(localVariable.getLvIndex()), Integer.toString(localVariable.getLvStartOffset()), - Integer.toString(localVariable.getLvTableIndex()) - ); + Integer.toString(localVariable.getLvIndex()), Integer.toString(localVariable.getLvStartOffset()), + Integer.toString(localVariable.getLvTableIndex())); for (String comment : localVariable.getComments()) { writeComment(Indents.LOCAL_VARIABLE_COMMENT, comment); @@ -127,29 +121,30 @@ private void writeField(TinyField field) { for (String comment : field.getComments()) writeComment(Indents.FIELD_COMMENT, comment); } - private void writeComment(int indentLevel, String comment) { writeLine(indentLevel, Prefixes.COMMENT, escapeComment(comment)); } private static String escapeComment(String old) { StringBuilder sb = new StringBuilder(old.length()); + for (int i = 0; i < old.length(); i++) { char c = old.charAt(i); int t = TO_ESCAPE.indexOf(c); + if (t == -1) { sb.append(c); } else { sb.append('\\').append(ESCAPED.charAt(t)); } } + return sb.toString(); } private static final String TO_ESCAPE = "\\\n\r\0\t"; private static final String ESCAPED = "\\nr0t"; - private void write(int indentLevel, String... tabSeparatedStrings) { try { for (int i = 0; i < indentLevel; i++) writer.write('\t'); diff --git a/src/main/java/net/fabricmc/stitch/enigma/StitchEnigmaPlugin.java b/src/main/java/net/fabricmc/stitch/enigma/StitchEnigmaPlugin.java index 8a820ec..368b082 100644 --- a/src/main/java/net/fabricmc/stitch/enigma/StitchEnigmaPlugin.java +++ b/src/main/java/net/fabricmc/stitch/enigma/StitchEnigmaPlugin.java @@ -21,11 +21,9 @@ import cuchaz.enigma.api.service.ObfuscationTestService; public class StitchEnigmaPlugin implements EnigmaPlugin { - @Override public void init(EnigmaPluginContext ctx) { StitchNameProposalService.register(ctx); ctx.registerService("stitch:intermediary_obfuscation_test", ObfuscationTestService.TYPE, StitchIntermediaryObfuscationTestService::new); } - } diff --git a/src/main/java/net/fabricmc/stitch/enigma/StitchIntermediaryObfuscationTestService.java b/src/main/java/net/fabricmc/stitch/enigma/StitchIntermediaryObfuscationTestService.java index 4580a47..31a8b66 100644 --- a/src/main/java/net/fabricmc/stitch/enigma/StitchIntermediaryObfuscationTestService.java +++ b/src/main/java/net/fabricmc/stitch/enigma/StitchIntermediaryObfuscationTestService.java @@ -43,6 +43,7 @@ public boolean testDeobfuscated(Entry entry) { // all obfuscated components are, at their outermost, class_ String lastComponent = components[components.length - 1]; + if (lastComponent.startsWith(this.classPrefix) || lastComponent.startsWith(this.classPackagePrefix)) { return false; } diff --git a/src/main/java/net/fabricmc/stitch/enigma/StitchNameProposalService.java b/src/main/java/net/fabricmc/stitch/enigma/StitchNameProposalService.java index 52fda16..51e643b 100644 --- a/src/main/java/net/fabricmc/stitch/enigma/StitchNameProposalService.java +++ b/src/main/java/net/fabricmc/stitch/enigma/StitchNameProposalService.java @@ -16,23 +16,24 @@ package net.fabricmc.stitch.enigma; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + import cuchaz.enigma.analysis.index.JarIndex; import cuchaz.enigma.api.EnigmaPluginContext; import cuchaz.enigma.api.service.JarIndexerService; import cuchaz.enigma.api.service.NameProposalService; import cuchaz.enigma.classprovider.ClassProvider; import cuchaz.enigma.translation.representation.entry.FieldEntry; +import org.objectweb.asm.tree.MethodNode; + import net.fabricmc.mappings.EntryTriple; import net.fabricmc.stitch.util.FieldNameFinder; import net.fabricmc.stitch.util.NameFinderVisitor; import net.fabricmc.stitch.util.StitchUtil; -import org.objectweb.asm.tree.MethodNode; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; public class StitchNameProposalService { private Map fieldNames; @@ -41,7 +42,6 @@ private StitchNameProposalService(EnigmaPluginContext ctx) { ctx.registerService("stitch:jar_indexer", JarIndexerService.TYPE, ctx1 -> new JarIndexerService() { @Override public void acceptJar(Set classNames, ClassProvider classProvider, JarIndex jarIndex) { - Map> enumFields = new HashMap<>(); Map> methods = new HashMap<>(); @@ -58,11 +58,12 @@ public void acceptJar(Set classNames, ClassProvider classProvider, JarIn }); ctx.registerService("stitch:name_proposal", NameProposalService.TYPE, ctx12 -> (obfEntry, remapper) -> { - if(obfEntry instanceof FieldEntry){ + if (obfEntry instanceof FieldEntry) { FieldEntry fieldEntry = (FieldEntry) obfEntry; EntryTriple key = new EntryTriple(fieldEntry.getContainingClass().getFullName(), fieldEntry.getName(), fieldEntry.getDesc().toString()); return Optional.ofNullable(fieldNames.get(key)); } + return Optional.empty(); }); } diff --git a/src/main/java/net/fabricmc/stitch/merge/ClassMerger.java b/src/main/java/net/fabricmc/stitch/merge/ClassMerger.java index 738a195..a739845 100644 --- a/src/main/java/net/fabricmc/stitch/merge/ClassMerger.java +++ b/src/main/java/net/fabricmc/stitch/merge/ClassMerger.java @@ -16,210 +16,226 @@ package net.fabricmc.stitch.merge; -import net.fabricmc.stitch.util.StitchUtil; -import org.objectweb.asm.*; -import org.objectweb.asm.tree.*; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InnerClassNode; +import org.objectweb.asm.tree.MethodNode; -import java.util.*; +import net.fabricmc.stitch.util.StitchUtil; public class ClassMerger { - private static final String SIDE_DESCRIPTOR = "Lnet/fabricmc/api/EnvType;"; - private static final String ITF_DESCRIPTOR = "Lnet/fabricmc/api/EnvironmentInterface;"; - private static final String ITF_LIST_DESCRIPTOR = "Lnet/fabricmc/api/EnvironmentInterfaces;"; - private static final String SIDED_DESCRIPTOR = "Lnet/fabricmc/api/Environment;"; - - private abstract class Merger { - private final Map entriesClient, entriesServer; - private final List entryNames; - - public Merger(List entriesClient, List entriesServer) { - this.entriesClient = new LinkedHashMap<>(); - this.entriesServer = new LinkedHashMap<>(); - - List listClient = toMap(entriesClient, this.entriesClient); - List listServer = toMap(entriesServer, this.entriesServer); - - this.entryNames = StitchUtil.mergePreserveOrder(listClient, listServer); - } - - public abstract String getName(T entry); - public abstract void applySide(T entry, String side); - - private final List toMap(List entries, Map map) { - List list = new ArrayList<>(entries.size()); - for (T entry : entries) { - String name = getName(entry); - map.put(name, entry); - list.add(name); - } - return list; - } - - public void merge(List list) { - for (String s : entryNames) { - T entryClient = entriesClient.get(s); - T entryServer = entriesServer.get(s); - - if (entryClient != null && entryServer != null) { - list.add(entryClient); - } else if (entryClient != null) { - applySide(entryClient, "CLIENT"); - list.add(entryClient); - } else { - applySide(entryServer, "SERVER"); - list.add(entryServer); - } - } - } - } - - private static void visitSideAnnotation(AnnotationVisitor av, String side) { - av.visitEnum("value", SIDE_DESCRIPTOR, side.toUpperCase(Locale.ROOT)); - av.visitEnd(); - } - - private static void visitItfAnnotation(AnnotationVisitor av, String side, List itfDescriptors) { - for (String itf : itfDescriptors) { - AnnotationVisitor avItf = av.visitAnnotation(null, ITF_DESCRIPTOR); - avItf.visitEnum("value", SIDE_DESCRIPTOR, side.toUpperCase(Locale.ROOT)); - avItf.visit("itf", Type.getType("L" + itf + ";")); - avItf.visitEnd(); - } - } - - public static class SidedClassVisitor extends ClassVisitor { - private final String side; - - public SidedClassVisitor(int api, ClassVisitor cv, String side) { - super(api, cv); - this.side = side; - } - - @Override - public void visitEnd() { - AnnotationVisitor av = cv.visitAnnotation(SIDED_DESCRIPTOR, true); - visitSideAnnotation(av, side); - super.visitEnd(); - } - } - - public ClassMerger() { - - } - - public byte[] merge(byte[] classClient, byte[] classServer) { - ClassReader readerC = new ClassReader(classClient); - ClassReader readerS = new ClassReader(classServer); - ClassWriter writer = new ClassWriter(0); - - ClassNode nodeC = new ClassNode(StitchUtil.ASM_VERSION); - readerC.accept(nodeC, 0); - - ClassNode nodeS = new ClassNode(StitchUtil.ASM_VERSION); - readerS.accept(nodeS, 0); - - ClassNode nodeOut = new ClassNode(StitchUtil.ASM_VERSION); - nodeOut.version = nodeC.version; - nodeOut.access = nodeC.access; - nodeOut.name = nodeC.name; - nodeOut.signature = nodeC.signature; - nodeOut.superName = nodeC.superName; - nodeOut.sourceFile = nodeC.sourceFile; - nodeOut.sourceDebug = nodeC.sourceDebug; - nodeOut.outerClass = nodeC.outerClass; - nodeOut.outerMethod = nodeC.outerMethod; - nodeOut.outerMethodDesc = nodeC.outerMethodDesc; - nodeOut.module = nodeC.module; - nodeOut.nestHostClass = nodeC.nestHostClass; - nodeOut.nestMembers = nodeC.nestMembers; - nodeOut.attrs = nodeC.attrs; - - if (nodeC.invisibleAnnotations != null) { - nodeOut.invisibleAnnotations = new ArrayList<>(); - nodeOut.invisibleAnnotations.addAll(nodeC.invisibleAnnotations); - } - if (nodeC.invisibleTypeAnnotations != null) { - nodeOut.invisibleTypeAnnotations = new ArrayList<>(); - nodeOut.invisibleTypeAnnotations.addAll(nodeC.invisibleTypeAnnotations); - } - if (nodeC.visibleAnnotations != null) { - nodeOut.visibleAnnotations = new ArrayList<>(); - nodeOut.visibleAnnotations.addAll(nodeC.visibleAnnotations); - } - if (nodeC.visibleTypeAnnotations != null) { - nodeOut.visibleTypeAnnotations = new ArrayList<>(); - nodeOut.visibleTypeAnnotations.addAll(nodeC.visibleTypeAnnotations); - } - - List itfs = StitchUtil.mergePreserveOrder(nodeC.interfaces, nodeS.interfaces); - nodeOut.interfaces = new ArrayList<>(); - - List clientItfs = new ArrayList<>(); - List serverItfs = new ArrayList<>(); - - for (String s : itfs) { - boolean nc = nodeC.interfaces.contains(s); - boolean ns = nodeS.interfaces.contains(s); - nodeOut.interfaces.add(s); - if (nc && !ns) { - clientItfs.add(s); - } else if (ns && !nc) { - serverItfs.add(s); - } - } - - if (!clientItfs.isEmpty() || !serverItfs.isEmpty()) { - AnnotationVisitor envInterfaces = nodeOut.visitAnnotation(ITF_LIST_DESCRIPTOR, false); - AnnotationVisitor eiArray = envInterfaces.visitArray("value"); - - if (!clientItfs.isEmpty()) { - visitItfAnnotation(eiArray, "CLIENT", clientItfs); - } - if (!serverItfs.isEmpty()) { - visitItfAnnotation(eiArray, "SERVER", serverItfs); - } - eiArray.visitEnd(); - envInterfaces.visitEnd(); - } - - new Merger(nodeC.innerClasses, nodeS.innerClasses) { - @Override - public String getName(InnerClassNode entry) { - return entry.name; - } - - @Override - public void applySide(InnerClassNode entry, String side) { - } - }.merge(nodeOut.innerClasses); - - new Merger(nodeC.fields, nodeS.fields) { - @Override - public String getName(FieldNode entry) { - return entry.name + ";;" + entry.desc; - } - - @Override - public void applySide(FieldNode entry, String side) { - AnnotationVisitor av = entry.visitAnnotation(SIDED_DESCRIPTOR, false); - visitSideAnnotation(av, side); - } - }.merge(nodeOut.fields); - - new Merger(nodeC.methods, nodeS.methods) { - @Override - public String getName(MethodNode entry) { - return entry.name + entry.desc; - } - - @Override - public void applySide(MethodNode entry, String side) { - AnnotationVisitor av = entry.visitAnnotation(SIDED_DESCRIPTOR, false); - visitSideAnnotation(av, side); - } - }.merge(nodeOut.methods); - - nodeOut.accept(writer); - return writer.toByteArray(); - } + private static final String SIDE_DESCRIPTOR = "Lnet/fabricmc/api/EnvType;"; + private static final String ITF_DESCRIPTOR = "Lnet/fabricmc/api/EnvironmentInterface;"; + private static final String ITF_LIST_DESCRIPTOR = "Lnet/fabricmc/api/EnvironmentInterfaces;"; + private static final String SIDED_DESCRIPTOR = "Lnet/fabricmc/api/Environment;"; + + private abstract class Merger { + private final Map entriesClient, entriesServer; + private final List entryNames; + + Merger(List entriesClient, List entriesServer) { + this.entriesClient = new LinkedHashMap<>(); + this.entriesServer = new LinkedHashMap<>(); + + List listClient = toMap(entriesClient, this.entriesClient); + List listServer = toMap(entriesServer, this.entriesServer); + + this.entryNames = StitchUtil.mergePreserveOrder(listClient, listServer); + } + + public abstract String getName(T entry); + public abstract void applySide(T entry, String side); + + private List toMap(List entries, Map map) { + List list = new ArrayList<>(entries.size()); + + for (T entry : entries) { + String name = getName(entry); + map.put(name, entry); + list.add(name); + } + + return list; + } + + public void merge(List list) { + for (String s : entryNames) { + T entryClient = entriesClient.get(s); + T entryServer = entriesServer.get(s); + + if (entryClient != null && entryServer != null) { + list.add(entryClient); + } else if (entryClient != null) { + applySide(entryClient, "CLIENT"); + list.add(entryClient); + } else { + applySide(entryServer, "SERVER"); + list.add(entryServer); + } + } + } + } + + private static void visitSideAnnotation(AnnotationVisitor av, String side) { + av.visitEnum("value", SIDE_DESCRIPTOR, side.toUpperCase(Locale.ROOT)); + av.visitEnd(); + } + + private static void visitItfAnnotation(AnnotationVisitor av, String side, List itfDescriptors) { + for (String itf : itfDescriptors) { + AnnotationVisitor avItf = av.visitAnnotation(null, ITF_DESCRIPTOR); + avItf.visitEnum("value", SIDE_DESCRIPTOR, side.toUpperCase(Locale.ROOT)); + avItf.visit("itf", Type.getType("L" + itf + ";")); + avItf.visitEnd(); + } + } + + public static class SidedClassVisitor extends ClassVisitor { + private final String side; + + public SidedClassVisitor(int api, ClassVisitor cv, String side) { + super(api, cv); + this.side = side; + } + + @Override + public void visitEnd() { + AnnotationVisitor av = cv.visitAnnotation(SIDED_DESCRIPTOR, true); + visitSideAnnotation(av, side); + super.visitEnd(); + } + } + + public byte[] merge(byte[] classClient, byte[] classServer) { + ClassReader readerC = new ClassReader(classClient); + ClassReader readerS = new ClassReader(classServer); + ClassWriter writer = new ClassWriter(0); + + ClassNode nodeC = new ClassNode(StitchUtil.ASM_VERSION); + readerC.accept(nodeC, 0); + + ClassNode nodeS = new ClassNode(StitchUtil.ASM_VERSION); + readerS.accept(nodeS, 0); + + ClassNode nodeOut = new ClassNode(StitchUtil.ASM_VERSION); + nodeOut.version = nodeC.version; + nodeOut.access = nodeC.access; + nodeOut.name = nodeC.name; + nodeOut.signature = nodeC.signature; + nodeOut.superName = nodeC.superName; + nodeOut.sourceFile = nodeC.sourceFile; + nodeOut.sourceDebug = nodeC.sourceDebug; + nodeOut.outerClass = nodeC.outerClass; + nodeOut.outerMethod = nodeC.outerMethod; + nodeOut.outerMethodDesc = nodeC.outerMethodDesc; + nodeOut.module = nodeC.module; + nodeOut.nestHostClass = nodeC.nestHostClass; + nodeOut.nestMembers = nodeC.nestMembers; + nodeOut.attrs = nodeC.attrs; + + if (nodeC.invisibleAnnotations != null) { + nodeOut.invisibleAnnotations = new ArrayList<>(); + nodeOut.invisibleAnnotations.addAll(nodeC.invisibleAnnotations); + } + + if (nodeC.invisibleTypeAnnotations != null) { + nodeOut.invisibleTypeAnnotations = new ArrayList<>(); + nodeOut.invisibleTypeAnnotations.addAll(nodeC.invisibleTypeAnnotations); + } + + if (nodeC.visibleAnnotations != null) { + nodeOut.visibleAnnotations = new ArrayList<>(); + nodeOut.visibleAnnotations.addAll(nodeC.visibleAnnotations); + } + + if (nodeC.visibleTypeAnnotations != null) { + nodeOut.visibleTypeAnnotations = new ArrayList<>(); + nodeOut.visibleTypeAnnotations.addAll(nodeC.visibleTypeAnnotations); + } + + List itfs = StitchUtil.mergePreserveOrder(nodeC.interfaces, nodeS.interfaces); + nodeOut.interfaces = new ArrayList<>(); + + List clientItfs = new ArrayList<>(); + List serverItfs = new ArrayList<>(); + + for (String s : itfs) { + boolean nc = nodeC.interfaces.contains(s); + boolean ns = nodeS.interfaces.contains(s); + nodeOut.interfaces.add(s); + + if (nc && !ns) { + clientItfs.add(s); + } else if (ns && !nc) { + serverItfs.add(s); + } + } + + if (!clientItfs.isEmpty() || !serverItfs.isEmpty()) { + AnnotationVisitor envInterfaces = nodeOut.visitAnnotation(ITF_LIST_DESCRIPTOR, false); + AnnotationVisitor eiArray = envInterfaces.visitArray("value"); + + if (!clientItfs.isEmpty()) { + visitItfAnnotation(eiArray, "CLIENT", clientItfs); + } + + if (!serverItfs.isEmpty()) { + visitItfAnnotation(eiArray, "SERVER", serverItfs); + } + + eiArray.visitEnd(); + envInterfaces.visitEnd(); + } + + new Merger(nodeC.innerClasses, nodeS.innerClasses) { + @Override + public String getName(InnerClassNode entry) { + return entry.name; + } + + @Override + public void applySide(InnerClassNode entry, String side) { + } + }.merge(nodeOut.innerClasses); + + new Merger(nodeC.fields, nodeS.fields) { + @Override + public String getName(FieldNode entry) { + return entry.name + ";;" + entry.desc; + } + + @Override + public void applySide(FieldNode entry, String side) { + AnnotationVisitor av = entry.visitAnnotation(SIDED_DESCRIPTOR, false); + visitSideAnnotation(av, side); + } + }.merge(nodeOut.fields); + + new Merger(nodeC.methods, nodeS.methods) { + @Override + public String getName(MethodNode entry) { + return entry.name + entry.desc; + } + + @Override + public void applySide(MethodNode entry, String side) { + AnnotationVisitor av = entry.visitAnnotation(SIDED_DESCRIPTOR, false); + visitSideAnnotation(av, side); + } + }.merge(nodeOut.methods); + + nodeOut.accept(writer); + return writer.toByteArray(); + } } diff --git a/src/main/java/net/fabricmc/stitch/merge/JarMerger.java b/src/main/java/net/fabricmc/stitch/merge/JarMerger.java index addf53b..94fc34c 100644 --- a/src/main/java/net/fabricmc/stitch/merge/JarMerger.java +++ b/src/main/java/net/fabricmc/stitch/merge/JarMerger.java @@ -16,211 +16,226 @@ package net.fabricmc.stitch.merge; -import net.fabricmc.stitch.util.SnowmanClassVisitor; -import net.fabricmc.stitch.util.StitchUtil; -import net.fabricmc.stitch.util.SyntheticParameterClassVisitor; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; - -import java.io.*; +import java.io.File; +import java.io.IOException; import java.nio.charset.Charset; -import java.nio.file.*; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; -import java.util.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; + +import net.fabricmc.stitch.util.SnowmanClassVisitor; +import net.fabricmc.stitch.util.StitchUtil; +import net.fabricmc.stitch.util.SyntheticParameterClassVisitor; + public class JarMerger implements AutoCloseable { - public class Entry { - public final Path path; - public final BasicFileAttributes metadata; - public final byte[] data; - - public Entry(Path path, BasicFileAttributes metadata, byte[] data) { - this.path = path; - this.metadata = metadata; - this.data = data; - } - } - - private static final ClassMerger CLASS_MERGER = new ClassMerger(); - private final StitchUtil.FileSystemDelegate inputClientFs, inputServerFs, outputFs; - private final Path inputClient, inputServer; - private final Map entriesClient, entriesServer; - private final Set entriesAll; - private boolean removeSnowmen = false; - private boolean offsetSyntheticsParams = false; - - public JarMerger(File inputClient, File inputServer, File output) throws IOException { - if (output.exists()) { - if (!output.delete()) { - throw new IOException("Could not delete " + output.getName()); - } - } - - this.inputClient = (inputClientFs = StitchUtil.getJarFileSystem(inputClient, false)).get().getPath("/"); - this.inputServer = (inputServerFs = StitchUtil.getJarFileSystem(inputServer, false)).get().getPath("/"); - this.outputFs = StitchUtil.getJarFileSystem(output, true); - - this.entriesClient = new HashMap<>(); - this.entriesServer = new HashMap<>(); - this.entriesAll = new TreeSet<>(); - } - - public void enableSnowmanRemoval() { - removeSnowmen = true; - } - - public void enableSyntheticParamsOffset() { - offsetSyntheticsParams = true; - } - - @Override - public void close() throws IOException { - inputClientFs.close(); - inputServerFs.close(); - outputFs.close(); - } - - private void readToMap(Map map, Path input, boolean isServer) { - try { - Files.walkFileTree(input, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path path, BasicFileAttributes attr) throws IOException { - if (attr.isDirectory()) { - return FileVisitResult.CONTINUE; - } - - if (!path.getFileName().toString().endsWith(".class")) { - if (path.toString().equals("/META-INF/MANIFEST.MF")) { - map.put("META-INF/MANIFEST.MF", new Entry(path, attr, - "Manifest-Version: 1.0\nMain-Class: net.minecraft.client.Main\n".getBytes(Charset.forName("UTF-8")))); - } else { - if (path.toString().startsWith("/META-INF/")) { - if (path.toString().endsWith(".SF") || path.toString().endsWith(".RSA")) { - return FileVisitResult.CONTINUE; - } - } - - map.put(path.toString().substring(1), new Entry(path, attr, null)); - } - - return FileVisitResult.CONTINUE; - } - - byte[] output = Files.readAllBytes(path); - map.put(path.toString().substring(1), new Entry(path, attr, output)); - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void add(Entry entry) throws IOException { - Path outPath = outputFs.get().getPath(entry.path.toString()); - if (outPath.getParent() != null) { - Files.createDirectories(outPath.getParent()); - } - - if (entry.data != null) { - Files.write(outPath, entry.data, StandardOpenOption.CREATE_NEW); - } else { - Files.copy(entry.path, outPath); - } - - Files.getFileAttributeView(outPath, BasicFileAttributeView.class) - .setTimes( - entry.metadata.creationTime(), - entry.metadata.lastAccessTime(), - entry.metadata.lastModifiedTime() - ); - } - - public void merge() throws IOException { - ExecutorService service = Executors.newFixedThreadPool(2); - service.submit(() -> readToMap(entriesClient, inputClient, false)); - service.submit(() -> readToMap(entriesServer, inputServer, true)); - service.shutdown(); - try { - service.awaitTermination(1, TimeUnit.HOURS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - entriesAll.addAll(entriesClient.keySet()); - entriesAll.addAll(entriesServer.keySet()); - - List entries = entriesAll.parallelStream().map((entry) -> { - boolean isClass = entry.endsWith(".class"); - boolean isMinecraft = entriesClient.containsKey(entry) || entry.startsWith("net/minecraft") || !entry.contains("/"); - Entry result; - String side = null; - - Entry entry1 = entriesClient.get(entry); - Entry entry2 = entriesServer.get(entry); - - if (entry1 != null && entry2 != null) { - if (Arrays.equals(entry1.data, entry2.data)) { - result = entry1; - } else { - if (isClass) { - result = new Entry(entry1.path, entry1.metadata, CLASS_MERGER.merge(entry1.data, entry2.data)); - } else { - // FIXME: More heuristics? - result = entry1; - } - } - } else if ((result = entry1) != null) { - side = "CLIENT"; - } else if ((result = entry2) != null) { - side = "SERVER"; - } - - if (isClass && !isMinecraft && "SERVER".equals(side)) { - // Server bundles libraries, client doesn't - skip them - return null; - } - - if (result != null) { - if (isMinecraft && isClass) { - byte[] data = result.data; - ClassReader reader = new ClassReader(data); - ClassWriter writer = new ClassWriter(0); - ClassVisitor visitor = writer; - - if (side != null) { - visitor = new ClassMerger.SidedClassVisitor(StitchUtil.ASM_VERSION, visitor, side); - } - - if (removeSnowmen) { - visitor = new SnowmanClassVisitor(StitchUtil.ASM_VERSION, visitor); - } - - if (offsetSyntheticsParams) { - visitor = new SyntheticParameterClassVisitor(StitchUtil.ASM_VERSION, visitor); - } - - if (visitor != writer) { - reader.accept(visitor, 0); - data = writer.toByteArray(); - result = new Entry(result.path, result.metadata, data); - } - } - - return result; - } else { - return null; - } - }).filter(Objects::nonNull).collect(Collectors.toList()); - - for (Entry e : entries) { - add(e); - } - } + public class Entry { + public final Path path; + public final BasicFileAttributes metadata; + public final byte[] data; + + public Entry(Path path, BasicFileAttributes metadata, byte[] data) { + this.path = path; + this.metadata = metadata; + this.data = data; + } + } + + private static final ClassMerger CLASS_MERGER = new ClassMerger(); + private final StitchUtil.FileSystemDelegate inputClientFs, inputServerFs, outputFs; + private final Path inputClient, inputServer; + private final Map entriesClient, entriesServer; + private final Set entriesAll; + private boolean removeSnowmen = false; + private boolean offsetSyntheticsParams = false; + + public JarMerger(File inputClient, File inputServer, File output) throws IOException { + if (output.exists()) { + if (!output.delete()) { + throw new IOException("Could not delete " + output.getName()); + } + } + + this.inputClient = (inputClientFs = StitchUtil.getJarFileSystem(inputClient, false)).get().getPath("/"); + this.inputServer = (inputServerFs = StitchUtil.getJarFileSystem(inputServer, false)).get().getPath("/"); + this.outputFs = StitchUtil.getJarFileSystem(output, true); + + this.entriesClient = new HashMap<>(); + this.entriesServer = new HashMap<>(); + this.entriesAll = new TreeSet<>(); + } + + public void enableSnowmanRemoval() { + removeSnowmen = true; + } + + public void enableSyntheticParamsOffset() { + offsetSyntheticsParams = true; + } + + @Override + public void close() throws IOException { + inputClientFs.close(); + inputServerFs.close(); + outputFs.close(); + } + + private void readToMap(Map map, Path input, boolean isServer) { + try { + Files.walkFileTree(input, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes attr) throws IOException { + if (attr.isDirectory()) { + return FileVisitResult.CONTINUE; + } + + if (!path.getFileName().toString().endsWith(".class")) { + if (path.toString().equals("/META-INF/MANIFEST.MF")) { + map.put("META-INF/MANIFEST.MF", new Entry(path, attr, + "Manifest-Version: 1.0\nMain-Class: net.minecraft.client.Main\n".getBytes(Charset.forName("UTF-8")))); + } else { + if (path.toString().startsWith("/META-INF/")) { + if (path.toString().endsWith(".SF") || path.toString().endsWith(".RSA")) { + return FileVisitResult.CONTINUE; + } + } + + map.put(path.toString().substring(1), new Entry(path, attr, null)); + } + + return FileVisitResult.CONTINUE; + } + + byte[] output = Files.readAllBytes(path); + map.put(path.toString().substring(1), new Entry(path, attr, output)); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void add(Entry entry) throws IOException { + Path outPath = outputFs.get().getPath(entry.path.toString()); + + if (outPath.getParent() != null) { + Files.createDirectories(outPath.getParent()); + } + + if (entry.data != null) { + Files.write(outPath, entry.data, StandardOpenOption.CREATE_NEW); + } else { + Files.copy(entry.path, outPath); + } + + Files.getFileAttributeView(outPath, BasicFileAttributeView.class) + .setTimes( + entry.metadata.creationTime(), + entry.metadata.lastAccessTime(), + entry.metadata.lastModifiedTime()); + } + + public void merge() throws IOException { + ExecutorService service = Executors.newFixedThreadPool(2); + service.submit(() -> readToMap(entriesClient, inputClient, false)); + service.submit(() -> readToMap(entriesServer, inputServer, true)); + service.shutdown(); + + try { + service.awaitTermination(1, TimeUnit.HOURS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + entriesAll.addAll(entriesClient.keySet()); + entriesAll.addAll(entriesServer.keySet()); + + List entries = entriesAll.parallelStream().map((entry) -> { + boolean isClass = entry.endsWith(".class"); + boolean isMinecraft = entriesClient.containsKey(entry) + || entry.startsWith("net/minecraft") + || !entry.contains("/"); + Entry result; + String side = null; + + Entry entry1 = entriesClient.get(entry); + Entry entry2 = entriesServer.get(entry); + + if (entry1 != null && entry2 != null) { + if (Arrays.equals(entry1.data, entry2.data)) { + result = entry1; + } else { + if (isClass) { + result = new Entry(entry1.path, entry1.metadata, CLASS_MERGER.merge(entry1.data, entry2.data)); + } else { + // FIXME: More heuristics? + result = entry1; + } + } + } else if ((result = entry1) != null) { + side = "CLIENT"; + } else if ((result = entry2) != null) { + side = "SERVER"; + } + + if (isClass && !isMinecraft && "SERVER".equals(side)) { + // Server bundles libraries, client doesn't - skip them + return null; + } + + if (result != null) { + if (isMinecraft && isClass) { + byte[] data = result.data; + ClassReader reader = new ClassReader(data); + ClassWriter writer = new ClassWriter(0); + ClassVisitor visitor = writer; + + if (side != null) { + visitor = new ClassMerger.SidedClassVisitor(StitchUtil.ASM_VERSION, visitor, side); + } + + if (removeSnowmen) { + visitor = new SnowmanClassVisitor(StitchUtil.ASM_VERSION, visitor); + } + + if (offsetSyntheticsParams) { + visitor = new SyntheticParameterClassVisitor(StitchUtil.ASM_VERSION, visitor); + } + + if (visitor != writer) { + reader.accept(visitor, 0); + data = writer.toByteArray(); + result = new Entry(result.path, result.metadata, data); + } + } + + return result; + } else { + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toList()); + + for (Entry e : entries) { + add(e); + } + } } diff --git a/src/main/java/net/fabricmc/stitch/representation/AbstractJarEntry.java b/src/main/java/net/fabricmc/stitch/representation/AbstractJarEntry.java index 9ff051a..8e6249a 100644 --- a/src/main/java/net/fabricmc/stitch/representation/AbstractJarEntry.java +++ b/src/main/java/net/fabricmc/stitch/representation/AbstractJarEntry.java @@ -17,41 +17,41 @@ package net.fabricmc.stitch.representation; public abstract class AbstractJarEntry { - protected String name; - protected int access; - - public AbstractJarEntry(String name) { - this.name = name; - } - - public int getAccess() { - return access; - } - - protected void setAccess(int value) { - this.access = value; - } - - public String getName() { - return name; - } - - protected String getKey() { - return name; - } - - @Override - public boolean equals(Object other) { - return other != null && other.getClass() == getClass() && ((AbstractJarEntry) other).getKey().equals(getKey()); - } - - @Override - public int hashCode() { - return getKey().hashCode(); - } - - @Override - public String toString() { - return getClass().getSimpleName() + "(" + getKey() + ")"; - } + protected String name; + protected int access; + + public AbstractJarEntry(String name) { + this.name = name; + } + + public int getAccess() { + return access; + } + + protected void setAccess(int value) { + this.access = value; + } + + public String getName() { + return name; + } + + protected String getKey() { + return name; + } + + @Override + public boolean equals(Object other) { + return other != null && other.getClass() == getClass() && ((AbstractJarEntry) other).getKey().equals(getKey()); + } + + @Override + public int hashCode() { + return getKey().hashCode(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + getKey() + ")"; + } } diff --git a/src/main/java/net/fabricmc/stitch/representation/Access.java b/src/main/java/net/fabricmc/stitch/representation/Access.java index 3b3088b..095303d 100644 --- a/src/main/java/net/fabricmc/stitch/representation/Access.java +++ b/src/main/java/net/fabricmc/stitch/representation/Access.java @@ -19,27 +19,26 @@ import org.objectweb.asm.Opcodes; public final class Access { - private Access() { + public static boolean isStatic(int access) { + return (access & Opcodes.ACC_STATIC) != 0; + } - } + public static boolean isPrivate(int access) { + return (access & Opcodes.ACC_PRIVATE) != 0; + } - public static boolean isStatic(int access) { - return (access & Opcodes.ACC_STATIC) != 0; - } + public static boolean isPrivateOrStatic(int access) { + return (access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) != 0; + } - public static boolean isPrivate(int access) { - return (access & Opcodes.ACC_PRIVATE) != 0; - } + public static boolean isInterface(int access) { + return (access & Opcodes.ACC_INTERFACE) != 0; + } - public static boolean isPrivateOrStatic(int access) { - return (access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) != 0; - } + public static boolean isNative(int access) { + return (access & (Opcodes.ACC_NATIVE)) != 0; + } - public static boolean isInterface(int access) { - return (access & Opcodes.ACC_INTERFACE) != 0; - } - - public static boolean isNative(int access) { - return (access & (Opcodes.ACC_NATIVE)) != 0; - } + private Access() { + } } diff --git a/src/main/java/net/fabricmc/stitch/representation/ClassPropagationTree.java b/src/main/java/net/fabricmc/stitch/representation/ClassPropagationTree.java index d46d13f..c9d0d68 100644 --- a/src/main/java/net/fabricmc/stitch/representation/ClassPropagationTree.java +++ b/src/main/java/net/fabricmc/stitch/representation/ClassPropagationTree.java @@ -16,9 +16,12 @@ package net.fabricmc.stitch.representation; -import net.fabricmc.stitch.util.StitchUtil; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.Set; -import java.util.*; +import net.fabricmc.stitch.util.StitchUtil; /** * TODO: This doesn't try to follow the JVM's logic at all. @@ -26,45 +29,48 @@ * where it could get away with naming them differently. */ public class ClassPropagationTree { - private final ClassStorage jar; - private final Set relevantClasses; - private final Set topmostClasses; + private final ClassStorage jar; + private final Set relevantClasses; + private final Set topmostClasses; + + public ClassPropagationTree(ClassStorage jar, JarClassEntry baseClass) { + this.jar = jar; + relevantClasses = StitchUtil.newIdentityHashSet(); + topmostClasses = StitchUtil.newIdentityHashSet(); + + LinkedList queue = new LinkedList<>(); + queue.add(baseClass); + + while (!queue.isEmpty()) { + JarClassEntry entry = queue.remove(); - public ClassPropagationTree(ClassStorage jar, JarClassEntry baseClass) { - this.jar = jar; - relevantClasses = StitchUtil.newIdentityHashSet(); - topmostClasses = StitchUtil.newIdentityHashSet(); + if (entry == null || relevantClasses.contains(entry)) { + continue; + } - LinkedList queue = new LinkedList<>(); - queue.add(baseClass); + relevantClasses.add(entry); + queue.addAll(entry.getSubclasses(jar)); + queue.addAll(entry.getImplementers(jar)); + int qSize = queue.size(); - while (!queue.isEmpty()) { - JarClassEntry entry = queue.remove(); - if (entry == null || relevantClasses.contains(entry)) { - continue; - } - relevantClasses.add(entry); + if (qSize == queue.size()) { + topmostClasses.add(entry); + } - int qSize = queue.size(); - queue.addAll(entry.getSubclasses(jar)); - queue.addAll(entry.getImplementers(jar)); - if (qSize == queue.size()) { - topmostClasses.add(entry); - } + queue.addAll(entry.getInterfaces(jar)); + JarClassEntry superClass = entry.getSuperClass(jar); - queue.addAll(entry.getInterfaces(jar)); - JarClassEntry superClass = entry.getSuperClass(jar); - if (superClass != null) { - queue.add(superClass); - } - } - } + if (superClass != null) { + queue.add(superClass); + } + } + } - public Collection getClasses() { - return Collections.unmodifiableSet(relevantClasses); - } + public Collection getClasses() { + return Collections.unmodifiableSet(relevantClasses); + } - public Collection getTopmostClasses() { - return Collections.unmodifiableSet(topmostClasses); - } + public Collection getTopmostClasses() { + return Collections.unmodifiableSet(topmostClasses); + } } diff --git a/src/main/java/net/fabricmc/stitch/representation/ClassStorage.java b/src/main/java/net/fabricmc/stitch/representation/ClassStorage.java index a06f72e..112ccc5 100644 --- a/src/main/java/net/fabricmc/stitch/representation/ClassStorage.java +++ b/src/main/java/net/fabricmc/stitch/representation/ClassStorage.java @@ -17,5 +17,5 @@ package net.fabricmc.stitch.representation; public interface ClassStorage { - JarClassEntry getClass(String name, boolean create); + JarClassEntry getClass(String name, boolean create); } diff --git a/src/main/java/net/fabricmc/stitch/representation/JarClassEntry.java b/src/main/java/net/fabricmc/stitch/representation/JarClassEntry.java index 90a0074..c2523f0 100644 --- a/src/main/java/net/fabricmc/stitch/representation/JarClassEntry.java +++ b/src/main/java/net/fabricmc/stitch/representation/JarClassEntry.java @@ -16,197 +16,209 @@ package net.fabricmc.stitch.representation; -import net.fabricmc.stitch.util.Pair; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + import org.objectweb.asm.commons.Remapper; -import java.util.*; -import java.util.stream.Collectors; +import net.fabricmc.stitch.util.Pair; public class JarClassEntry extends AbstractJarEntry { - String fullyQualifiedName; - final Map innerClasses; - final Map fields; - final Map methods; - final Map>> relatedMethods; - - String signature; - String superclass; - List interfaces; - List subclasses; - List implementers; - - protected JarClassEntry(String name, String fullyQualifiedName) { - super(name); - - this.fullyQualifiedName = fullyQualifiedName; - this.innerClasses = new TreeMap<>(Comparator.naturalOrder()); - this.fields = new TreeMap<>(Comparator.naturalOrder()); - this.methods = new TreeMap<>(Comparator.naturalOrder()); - this.relatedMethods = new HashMap<>(); - - this.subclasses = new ArrayList<>(); - this.implementers = new ArrayList<>(); - } - - protected void populate(int access, String signature, String superclass, String[] interfaces) { - this.setAccess(access); - this.signature = signature; - this.superclass = superclass; - this.interfaces = Arrays.asList(interfaces); - } - - protected void populateParents(ClassStorage storage) { - JarClassEntry superEntry = getSuperClass(storage); - if (superEntry != null) { - superEntry.subclasses.add(fullyQualifiedName); - } - - for (JarClassEntry itf : getInterfaces(storage)) { - if (itf != null) { - itf.implementers.add(fullyQualifiedName); - } - } - } - - // unstable - public Collection> getRelatedMethods(JarMethodEntry m) { - //noinspection unchecked - return relatedMethods.getOrDefault(m.getKey(), Collections.EMPTY_SET); - } - - public String getFullyQualifiedName() { - return fullyQualifiedName; - } - - public String getSignature() { - return signature; - } - - public String getSuperClassName() { - return superclass; - } - - public JarClassEntry getSuperClass(ClassStorage storage) { - return storage.getClass(superclass, false); - } - - public List getInterfaceNames() { - return Collections.unmodifiableList(interfaces); - } - - public List getInterfaces(ClassStorage storage) { - return toClassEntryList(storage, interfaces); - } - - public List getSubclassNames() { - return Collections.unmodifiableList(subclasses); - } - - public List getSubclasses(ClassStorage storage) { - return toClassEntryList(storage, subclasses); - } - - public List getImplementerNames() { - return Collections.unmodifiableList(implementers); - } - - public List getImplementers(ClassStorage storage) { - return toClassEntryList(storage, implementers); - } - - private List toClassEntryList(ClassStorage storage, List stringList) { - if (stringList == null) { - return Collections.emptyList(); - } - - return stringList.stream() - .map((s) -> storage.getClass(s, false)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - public JarClassEntry getInnerClass(String name) { - return innerClasses.get(name); - } - - public JarFieldEntry getField(String name) { - return fields.get(name); - } - - public JarMethodEntry getMethod(String name) { - return methods.get(name); - } - - public Collection getInnerClasses() { - return innerClasses.values(); - } - - public Collection getFields() { - return fields.values(); - } - - public Collection getMethods() { - return methods.values(); - } - - public boolean isInterface() { - return Access.isInterface(getAccess()); - } - - public boolean isAnonymous() { - return getName().matches("[0-9]+"); - } - - @Override - public String getKey() { - return getFullyQualifiedName(); - } - - public void remap(Remapper remapper) { - String oldName = fullyQualifiedName; - fullyQualifiedName = remapper.map(fullyQualifiedName); - String[] s = fullyQualifiedName.split("\\$"); - name = s[s.length - 1]; - - if (superclass != null) { - superclass = remapper.map(superclass); - } - - interfaces = interfaces.stream().map(remapper::map).collect(Collectors.toList()); - subclasses = subclasses.stream().map(remapper::map).collect(Collectors.toList()); - implementers = implementers.stream().map(remapper::map).collect(Collectors.toList()); - - Map innerClassOld = new HashMap<>(innerClasses); - Map fieldsOld = new HashMap<>(fields); - Map methodsOld = new HashMap<>(methods); - Map methodKeyRemaps = new HashMap<>(); - - innerClasses.clear(); - fields.clear(); - methods.clear(); - - for (Map.Entry entry : innerClassOld.entrySet()) { - entry.getValue().remap(remapper); - innerClasses.put(entry.getValue().name, entry.getValue()); - } - - for (Map.Entry entry : fieldsOld.entrySet()) { - entry.getValue().remap(this, oldName, remapper); - fields.put(entry.getValue().getKey(), entry.getValue()); - } - - for (Map.Entry entry : methodsOld.entrySet()) { - entry.getValue().remap(this, oldName, remapper); - methods.put(entry.getValue().getKey(), entry.getValue()); - methodKeyRemaps.put(entry.getKey(), entry.getValue().getKey()); - } - - // TODO: remap relatedMethods strings??? - Map>> relatedMethodsOld = new HashMap<>(relatedMethods); - relatedMethods.clear(); - - for (Map.Entry>> entry : relatedMethodsOld.entrySet()) { - relatedMethods.put(methodKeyRemaps.getOrDefault(entry.getKey(), entry.getKey()), entry.getValue()); - } - } + String fullyQualifiedName; + final Map innerClasses; + final Map fields; + final Map methods; + final Map>> relatedMethods; + + String signature; + String superclass; + List interfaces; + List subclasses; + List implementers; + + protected JarClassEntry(String name, String fullyQualifiedName) { + super(name); + + this.fullyQualifiedName = fullyQualifiedName; + this.innerClasses = new TreeMap<>(Comparator.naturalOrder()); + this.fields = new TreeMap<>(Comparator.naturalOrder()); + this.methods = new TreeMap<>(Comparator.naturalOrder()); + this.relatedMethods = new HashMap<>(); + + this.subclasses = new ArrayList<>(); + this.implementers = new ArrayList<>(); + } + + protected void populate(int access, String signature, String superclass, String[] interfaces) { + this.setAccess(access); + this.signature = signature; + this.superclass = superclass; + this.interfaces = Arrays.asList(interfaces); + } + + protected void populateParents(ClassStorage storage) { + JarClassEntry superEntry = getSuperClass(storage); + + if (superEntry != null) { + superEntry.subclasses.add(fullyQualifiedName); + } + + for (JarClassEntry itf : getInterfaces(storage)) { + if (itf != null) { + itf.implementers.add(fullyQualifiedName); + } + } + } + + // unstable + public Collection> getRelatedMethods(JarMethodEntry m) { + //noinspection unchecked + return relatedMethods.getOrDefault(m.getKey(), Collections.EMPTY_SET); + } + + public String getFullyQualifiedName() { + return fullyQualifiedName; + } + + public String getSignature() { + return signature; + } + + public String getSuperClassName() { + return superclass; + } + + public JarClassEntry getSuperClass(ClassStorage storage) { + return storage.getClass(superclass, false); + } + + public List getInterfaceNames() { + return Collections.unmodifiableList(interfaces); + } + + public List getInterfaces(ClassStorage storage) { + return toClassEntryList(storage, interfaces); + } + + public List getSubclassNames() { + return Collections.unmodifiableList(subclasses); + } + + public List getSubclasses(ClassStorage storage) { + return toClassEntryList(storage, subclasses); + } + + public List getImplementerNames() { + return Collections.unmodifiableList(implementers); + } + + public List getImplementers(ClassStorage storage) { + return toClassEntryList(storage, implementers); + } + + private List toClassEntryList(ClassStorage storage, List stringList) { + if (stringList == null) { + return Collections.emptyList(); + } + + return stringList.stream() + .map((s) -> storage.getClass(s, false)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + public JarClassEntry getInnerClass(String name) { + return innerClasses.get(name); + } + + public JarFieldEntry getField(String name) { + return fields.get(name); + } + + public JarMethodEntry getMethod(String name) { + return methods.get(name); + } + + public Collection getInnerClasses() { + return innerClasses.values(); + } + + public Collection getFields() { + return fields.values(); + } + + public Collection getMethods() { + return methods.values(); + } + + public boolean isInterface() { + return Access.isInterface(getAccess()); + } + + public boolean isAnonymous() { + return getName().matches("[0-9]+"); + } + + @Override + public String getKey() { + return getFullyQualifiedName(); + } + + public void remap(Remapper remapper) { + String oldName = fullyQualifiedName; + fullyQualifiedName = remapper.map(fullyQualifiedName); + String[] s = fullyQualifiedName.split("\\$"); + name = s[s.length - 1]; + + if (superclass != null) { + superclass = remapper.map(superclass); + } + + interfaces = interfaces.stream().map(remapper::map).collect(Collectors.toList()); + subclasses = subclasses.stream().map(remapper::map).collect(Collectors.toList()); + implementers = implementers.stream().map(remapper::map).collect(Collectors.toList()); + + Map innerClassOld = new HashMap<>(innerClasses); + Map fieldsOld = new HashMap<>(fields); + Map methodsOld = new HashMap<>(methods); + Map methodKeyRemaps = new HashMap<>(); + + innerClasses.clear(); + fields.clear(); + methods.clear(); + + for (Map.Entry entry : innerClassOld.entrySet()) { + entry.getValue().remap(remapper); + innerClasses.put(entry.getValue().name, entry.getValue()); + } + + for (Map.Entry entry : fieldsOld.entrySet()) { + entry.getValue().remap(this, oldName, remapper); + fields.put(entry.getValue().getKey(), entry.getValue()); + } + + for (Map.Entry entry : methodsOld.entrySet()) { + entry.getValue().remap(this, oldName, remapper); + methods.put(entry.getValue().getKey(), entry.getValue()); + methodKeyRemaps.put(entry.getKey(), entry.getValue().getKey()); + } + + // TODO: remap relatedMethods strings??? + Map>> relatedMethodsOld = new HashMap<>(relatedMethods); + relatedMethods.clear(); + + for (Map.Entry>> entry : relatedMethodsOld.entrySet()) { + relatedMethods.put(methodKeyRemaps.getOrDefault(entry.getKey(), entry.getKey()), entry.getValue()); + } + } } diff --git a/src/main/java/net/fabricmc/stitch/representation/JarFieldEntry.java b/src/main/java/net/fabricmc/stitch/representation/JarFieldEntry.java index 9577cd8..52fd2c7 100644 --- a/src/main/java/net/fabricmc/stitch/representation/JarFieldEntry.java +++ b/src/main/java/net/fabricmc/stitch/representation/JarFieldEntry.java @@ -19,33 +19,33 @@ import org.objectweb.asm.commons.Remapper; public class JarFieldEntry extends AbstractJarEntry { - protected String desc; - protected String signature; - - JarFieldEntry(int access, String name, String desc, String signature) { - super(name); - this.setAccess(access); - this.desc = desc; - this.signature = signature; - } - - public String getDescriptor() { - return desc; - } - - public String getSignature() { - return signature; - } - - @Override - protected String getKey() { - return super.getKey() + desc; - } - - public void remap(JarClassEntry classEntry, String oldOwner, Remapper remapper) { - String pastDesc = desc; - - name = remapper.mapFieldName(oldOwner, name, pastDesc); - desc = remapper.mapDesc(pastDesc); - } + protected String desc; + protected String signature; + + JarFieldEntry(int access, String name, String desc, String signature) { + super(name); + this.setAccess(access); + this.desc = desc; + this.signature = signature; + } + + public String getDescriptor() { + return desc; + } + + public String getSignature() { + return signature; + } + + @Override + protected String getKey() { + return super.getKey() + desc; + } + + public void remap(JarClassEntry classEntry, String oldOwner, Remapper remapper) { + String pastDesc = desc; + + name = remapper.mapFieldName(oldOwner, name, pastDesc); + desc = remapper.mapDesc(pastDesc); + } } diff --git a/src/main/java/net/fabricmc/stitch/representation/JarMethodEntry.java b/src/main/java/net/fabricmc/stitch/representation/JarMethodEntry.java index ce45fae..123ff8d 100644 --- a/src/main/java/net/fabricmc/stitch/representation/JarMethodEntry.java +++ b/src/main/java/net/fabricmc/stitch/representation/JarMethodEntry.java @@ -16,111 +16,120 @@ package net.fabricmc.stitch.representation; -import net.fabricmc.stitch.util.StitchUtil; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + import org.objectweb.asm.commons.Remapper; -import java.util.*; +import net.fabricmc.stitch.util.StitchUtil; public class JarMethodEntry extends AbstractJarEntry { - protected String desc; - protected String signature; - - protected JarMethodEntry(int access, String name, String desc, String signature) { - super(name); - this.setAccess(access); - this.desc = desc; - this.signature = signature; - } - - public String getDescriptor() { - return desc; - } - - public String getSignature() { - return signature; - } - - @Override - protected String getKey() { - return super.getKey() + desc; - } - - public boolean isSource(ClassStorage storage, JarClassEntry c) { - if (Access.isPrivateOrStatic(getAccess())) { - return true; - } - - Set entries = StitchUtil.newIdentityHashSet(); - entries.add(c); - getMatchingSources(entries, storage, c); - return entries.size() == 1; - } - - public List getMatchingEntries(ClassStorage storage, JarClassEntry c) { - if (Access.isPrivateOrStatic(getAccess())) { - return Collections.singletonList(c); - } - - Set entries = StitchUtil.newIdentityHashSet(); - Set entriesNew = StitchUtil.newIdentityHashSet(); - entries.add(c); - int lastSize = 0; - - while (entries.size() > lastSize) { - lastSize = entries.size(); - - for (JarClassEntry cc : entries) { - getMatchingSources(entriesNew, storage, cc); - } - entries.addAll(entriesNew); - entriesNew.clear(); - - for (JarClassEntry cc : entries) { - getMatchingEntries(entriesNew, storage, cc, 0); - } - entries.addAll(entriesNew); - entriesNew.clear(); - } - - entries.removeIf(cc -> cc.getMethod(getKey()) == null); - - return new ArrayList<>(entries); - } - - void getMatchingSources(Collection entries, ClassStorage storage, JarClassEntry c) { - JarMethodEntry m = c.getMethod(getKey()); - if (m != null) { - if (!Access.isPrivateOrStatic(m.getAccess())) { - entries.add(c); - } - } - - JarClassEntry superClass = c.getSuperClass(storage); - if (superClass != null) { - getMatchingSources(entries, storage, superClass); - } - - for (JarClassEntry itf : c.getInterfaces(storage)) { - getMatchingSources(entries, storage, itf); - } - } - - void getMatchingEntries(Collection entries, ClassStorage storage, JarClassEntry c, int indent) { - entries.add(c); - - for (JarClassEntry cc : c.getSubclasses(storage)) { - getMatchingEntries(entries, storage, cc, indent + 1); - } - - for (JarClassEntry cc : c.getImplementers(storage)) { - getMatchingEntries(entries, storage, cc, indent + 1); - } - } - - public void remap(JarClassEntry classEntry, String oldOwner, Remapper remapper) { - String pastDesc = desc; - - name = remapper.mapMethodName(oldOwner, name, pastDesc); - desc = remapper.mapMethodDesc(pastDesc); - } -} \ No newline at end of file + protected String desc; + protected String signature; + + protected JarMethodEntry(int access, String name, String desc, String signature) { + super(name); + this.setAccess(access); + this.desc = desc; + this.signature = signature; + } + + public String getDescriptor() { + return desc; + } + + public String getSignature() { + return signature; + } + + @Override + protected String getKey() { + return super.getKey() + desc; + } + + public boolean isSource(ClassStorage storage, JarClassEntry c) { + if (Access.isPrivateOrStatic(getAccess())) { + return true; + } + + Set entries = StitchUtil.newIdentityHashSet(); + entries.add(c); + getMatchingSources(entries, storage, c); + return entries.size() == 1; + } + + public List getMatchingEntries(ClassStorage storage, JarClassEntry c) { + if (Access.isPrivateOrStatic(getAccess())) { + return Collections.singletonList(c); + } + + Set entries = StitchUtil.newIdentityHashSet(); + Set entriesNew = StitchUtil.newIdentityHashSet(); + entries.add(c); + int lastSize = 0; + + while (entries.size() > lastSize) { + lastSize = entries.size(); + + for (JarClassEntry cc : entries) { + getMatchingSources(entriesNew, storage, cc); + } + + entries.addAll(entriesNew); + entriesNew.clear(); + + for (JarClassEntry cc : entries) { + getMatchingEntries(entriesNew, storage, cc, 0); + } + + entries.addAll(entriesNew); + entriesNew.clear(); + } + + entries.removeIf(cc -> cc.getMethod(getKey()) == null); + + return new ArrayList<>(entries); + } + + void getMatchingSources(Collection entries, ClassStorage storage, JarClassEntry c) { + JarMethodEntry m = c.getMethod(getKey()); + + if (m != null) { + if (!Access.isPrivateOrStatic(m.getAccess())) { + entries.add(c); + } + } + + JarClassEntry superClass = c.getSuperClass(storage); + + if (superClass != null) { + getMatchingSources(entries, storage, superClass); + } + + for (JarClassEntry itf : c.getInterfaces(storage)) { + getMatchingSources(entries, storage, itf); + } + } + + void getMatchingEntries(Collection entries, ClassStorage storage, JarClassEntry c, int indent) { + entries.add(c); + + for (JarClassEntry cc : c.getSubclasses(storage)) { + getMatchingEntries(entries, storage, cc, indent + 1); + } + + for (JarClassEntry cc : c.getImplementers(storage)) { + getMatchingEntries(entries, storage, cc, indent + 1); + } + } + + public void remap(JarClassEntry classEntry, String oldOwner, Remapper remapper) { + String pastDesc = desc; + + name = remapper.mapMethodName(oldOwner, name, pastDesc); + desc = remapper.mapMethodDesc(pastDesc); + } +} diff --git a/src/main/java/net/fabricmc/stitch/representation/JarReader.java b/src/main/java/net/fabricmc/stitch/representation/JarReader.java index 4f4c048..886b0cb 100644 --- a/src/main/java/net/fabricmc/stitch/representation/JarReader.java +++ b/src/main/java/net/fabricmc/stitch/representation/JarReader.java @@ -16,293 +16,305 @@ package net.fabricmc.stitch.representation; -import net.fabricmc.stitch.util.StitchUtil; -import org.objectweb.asm.*; -import org.objectweb.asm.commons.Remapper; - import java.io.FileInputStream; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.jar.JarInputStream; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.Remapper; + +import net.fabricmc.stitch.util.StitchUtil; + public class JarReader { - public static class Builder { - private final JarReader reader; - - private Builder(JarReader reader) { - this.reader = reader; - } - - public static Builder create(JarRootEntry jar) { - return new Builder(new JarReader(jar)); - } - - public Builder joinMethodEntries(boolean value) { - reader.joinMethodEntries = value; - return this; - } - - public Builder withRemapper(Remapper remapper) { - reader.remapper = remapper; - return this; - } - - public JarReader build() { - return reader; - } - } - - private final JarRootEntry jar; - private boolean joinMethodEntries = true; - private Remapper remapper; - - public JarReader(JarRootEntry jar) { - this.jar = jar; - } - - private class VisitorClass extends ClassVisitor { - private JarClassEntry entry; - - public VisitorClass(int api, ClassVisitor classVisitor) { - super(api, classVisitor); - } - - @Override - public void visit(final int version, final int access, final String name, final String signature, - final String superName, final String[] interfaces) { - this.entry = jar.getClass(name, true); - this.entry.populate(access, signature, superName, interfaces); - - super.visit(version, access, name, signature, superName, interfaces); - } - - @Override - public FieldVisitor visitField(final int access, final String name, final String descriptor, - final String signature, final Object value) { - JarFieldEntry field = new JarFieldEntry(access, name, descriptor, signature); - this.entry.fields.put(field.getKey(), field); - - return new VisitorField(api, super.visitField(access, name, descriptor, signature, value), - entry, field); - } - - @Override - public MethodVisitor visitMethod(final int access, final String name, final String descriptor, - final String signature, final String[] exceptions) { - JarMethodEntry method = new JarMethodEntry(access, name, descriptor, signature); - this.entry.methods.put(method.getKey(), method); - - return new VisitorMethod(api, super.visitMethod(access, name, descriptor, signature, exceptions), - entry, method); - } - } - - private class VisitorClassStageTwo extends ClassVisitor { - private JarClassEntry entry; - - public VisitorClassStageTwo(int api, ClassVisitor classVisitor) { - super(api, classVisitor); - } - - @Override - public void visit(final int version, final int access, final String name, final String signature, - final String superName, final String[] interfaces) { - this.entry = jar.getClass(name, true); - super.visit(version, access, name, signature, superName, interfaces); - } - - @Override - public MethodVisitor visitMethod(final int access, final String name, final String descriptor, - final String signature, final String[] exceptions) { - JarMethodEntry method = new JarMethodEntry(access, name, descriptor, signature); - this.entry.methods.put(method.getKey(), method); - - if ((access & (Opcodes.ACC_BRIDGE | Opcodes.ACC_SYNTHETIC)) != 0) { - return new VisitorBridge(api, access, super.visitMethod(access, name, descriptor, signature, exceptions), - entry, method); - } else { - return super.visitMethod(access, name, descriptor, signature, exceptions); - } - } - } - - private class VisitorField extends FieldVisitor { - private final JarClassEntry classEntry; - private final JarFieldEntry entry; - - public VisitorField(int api, FieldVisitor fieldVisitor, JarClassEntry classEntry, JarFieldEntry entry) { - super(api, fieldVisitor); - this.classEntry = classEntry; - this.entry = entry; - } - } - - private static class MethodRef { - final String owner, name, descriptor; - - MethodRef(String owner, String name, String descriptor) { - this.owner = owner; - this.name = name; - this.descriptor = descriptor; - } - } - - private class VisitorBridge extends VisitorMethod { - private final boolean hasBridgeFlag; - private final List methodRefs = new ArrayList<>(); - - public VisitorBridge(int api, int access, MethodVisitor methodVisitor, JarClassEntry classEntry, JarMethodEntry entry) { - super(api, methodVisitor, classEntry, entry); - hasBridgeFlag = ((access & Opcodes.ACC_BRIDGE) != 0); - } - - @Override - public void visitMethodInsn( - final int opcode, - final String owner, - final String name, - final String descriptor, - final boolean isInterface) { - super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); - methodRefs.add(new MethodRef(owner, name, descriptor)); - } - - @Override - public void visitEnd() { - /* boolean isBridge = hasBridgeFlag; - - if (!isBridge && methodRefs.size() == 1) { - System.out.println("Found suspicious bridge-looking method: " + classEntry.getFullyQualifiedName() + ":" + entry); - } - - if (isBridge) { - for (MethodRef ref : methodRefs) { - JarClassEntry targetClass = jar.getClass(ref.owner, true); - JarMethodEntry targetMethod = new JarMethodEntry(0, ref.name, ref.descriptor, null); - String targetKey = targetMethod.getKey(); - - targetClass.relatedMethods.computeIfAbsent(targetKey, (a) -> new HashSet<>()).add(Pair.of(classEntry, entry.getKey())); - classEntry.relatedMethods.computeIfAbsent(entry.getKey(), (a) -> new HashSet<>()).add(Pair.of(targetClass, targetKey)); - } - } */ - } - } - - private class VisitorMethod extends MethodVisitor { - final JarClassEntry classEntry; - final JarMethodEntry entry; - - public VisitorMethod(int api, MethodVisitor methodVisitor, JarClassEntry classEntry, JarMethodEntry entry) { - super(api, methodVisitor); - this.classEntry = classEntry; - this.entry = entry; - } - } - - public void apply() throws IOException { - // Stage 1: read .JAR class/field/method meta - try (FileInputStream fileStream = new FileInputStream(jar.file)) { - try (JarInputStream jarStream = new JarInputStream(fileStream)) { - java.util.jar.JarEntry entry; - - while ((entry = jarStream.getNextJarEntry()) != null) { - if (!entry.getName().endsWith(".class")) { - continue; - } - - ClassReader reader = new ClassReader(jarStream); - ClassVisitor visitor = new VisitorClass(StitchUtil.ASM_VERSION, null); - reader.accept(visitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); - } - } - } - - System.err.println("Read " + this.jar.getAllClasses().size() + " (" + this.jar.getClasses().size() + ") classes."); - - // Stage 2: find subclasses - this.jar.getAllClasses().forEach((c) -> c.populateParents(jar)); - System.err.println("Populated subclass entries."); - - // Stage 3: join identical MethodEntries - if (joinMethodEntries) { - System.err.println("Joining MethodEntries..."); - Set traversedClasses = StitchUtil.newIdentityHashSet(); - - int joinedMethods = 1; - int uniqueMethods = 0; - - Collection checkedMethods = StitchUtil.newIdentityHashSet(); - - for (JarClassEntry entry : jar.getAllClasses()) { - if (traversedClasses.contains(entry)) { - continue; - } - - ClassPropagationTree tree = new ClassPropagationTree(jar, entry); - if (tree.getClasses().size() == 1) { - traversedClasses.add(entry); - continue; - } - - for (JarClassEntry c : tree.getClasses()) { - for (JarMethodEntry m : c.getMethods()) { - if (!checkedMethods.add(m)) { - continue; - } - - // get all matching entries - List mList = m.getMatchingEntries(jar, c); - - if (mList.size() > 1) { - for (int i = 0; i < mList.size(); i++) { - JarClassEntry key = mList.get(i); - JarMethodEntry value = key.getMethod(m.getKey()); - if (value != m) { - key.methods.put(m.getKey(), m); - joinedMethods++; - } - } - } - } - } - - traversedClasses.addAll(tree.getClasses()); - } - - System.err.println("Joined " + joinedMethods + " MethodEntries (" + uniqueMethods + " unique, " + traversedClasses.size() + " classes)."); - } - - System.err.println("Collecting additional information..."); - - // Stage 4: collect additional info - /* try (FileInputStream fileStream = new FileInputStream(jar.file)) { - try (JarInputStream jarStream = new JarInputStream(fileStream)) { - java.util.jar.JarEntry entry; - - while ((entry = jarStream.getNextJarEntry()) != null) { - if (!entry.getName().endsWith(".class")) { - continue; - } - - ClassReader reader = new ClassReader(jarStream); - ClassVisitor visitor = new VisitorClassStageTwo(StitchUtil.ASM_VERSION, null); - reader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); - } - } - } */ - - if (remapper != null) { - System.err.println("Remapping..."); - - Map classTree = new HashMap<>(jar.classTree); - jar.classTree.clear(); - - for (Map.Entry entry : classTree.entrySet()) { - entry.getValue().remap(remapper); - jar.classTree.put(entry.getValue().getKey(), entry.getValue()); - } - } - - System.err.println("- Done. -"); - } + public static class Builder { + private final JarReader reader; + + private Builder(JarReader reader) { + this.reader = reader; + } + + public static Builder create(JarRootEntry jar) { + return new Builder(new JarReader(jar)); + } + + public Builder joinMethodEntries(boolean value) { + reader.joinMethodEntries = value; + return this; + } + + public Builder withRemapper(Remapper remapper) { + reader.remapper = remapper; + return this; + } + + public JarReader build() { + return reader; + } + } + + private final JarRootEntry jar; + private boolean joinMethodEntries = true; + private Remapper remapper; + + public JarReader(JarRootEntry jar) { + this.jar = jar; + } + + private class VisitorClass extends ClassVisitor { + private JarClassEntry entry; + + VisitorClass(int api, ClassVisitor classVisitor) { + super(api, classVisitor); + } + + @Override + public void visit(final int version, final int access, final String name, final String signature, + final String superName, final String[] interfaces) { + this.entry = jar.getClass(name, true); + this.entry.populate(access, signature, superName, interfaces); + + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField(final int access, final String name, final String descriptor, + final String signature, final Object value) { + JarFieldEntry field = new JarFieldEntry(access, name, descriptor, signature); + this.entry.fields.put(field.getKey(), field); + + return new VisitorField(api, super.visitField(access, name, descriptor, signature, value), + entry, field); + } + + @Override + public MethodVisitor visitMethod(final int access, final String name, final String descriptor, + final String signature, final String[] exceptions) { + JarMethodEntry method = new JarMethodEntry(access, name, descriptor, signature); + this.entry.methods.put(method.getKey(), method); + + return new VisitorMethod(api, super.visitMethod(access, name, descriptor, signature, exceptions), + entry, method); + } + } + + private class VisitorClassStageTwo extends ClassVisitor { + private JarClassEntry entry; + + VisitorClassStageTwo(int api, ClassVisitor classVisitor) { + super(api, classVisitor); + } + + @Override + public void visit(final int version, final int access, final String name, final String signature, + final String superName, final String[] interfaces) { + this.entry = jar.getClass(name, true); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod(final int access, final String name, final String descriptor, + final String signature, final String[] exceptions) { + JarMethodEntry method = new JarMethodEntry(access, name, descriptor, signature); + this.entry.methods.put(method.getKey(), method); + + if ((access & (Opcodes.ACC_BRIDGE | Opcodes.ACC_SYNTHETIC)) != 0) { + return new VisitorBridge(api, access, super.visitMethod(access, name, descriptor, signature, exceptions), + entry, method); + } else { + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + } + } + + private class VisitorField extends FieldVisitor { + private final JarClassEntry classEntry; + private final JarFieldEntry entry; + + VisitorField(int api, FieldVisitor fieldVisitor, JarClassEntry classEntry, JarFieldEntry entry) { + super(api, fieldVisitor); + this.classEntry = classEntry; + this.entry = entry; + } + } + + private static class MethodRef { + final String owner, name, descriptor; + + MethodRef(String owner, String name, String descriptor) { + this.owner = owner; + this.name = name; + this.descriptor = descriptor; + } + } + + private class VisitorBridge extends VisitorMethod { + private final boolean hasBridgeFlag; + private final List methodRefs = new ArrayList<>(); + + VisitorBridge(int api, int access, MethodVisitor methodVisitor, JarClassEntry classEntry, JarMethodEntry entry) { + super(api, methodVisitor, classEntry, entry); + hasBridgeFlag = ((access & Opcodes.ACC_BRIDGE) != 0); + } + + @Override + public void visitMethodInsn( + final int opcode, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + methodRefs.add(new MethodRef(owner, name, descriptor)); + } + + @Override + public void visitEnd() { + /* boolean isBridge = hasBridgeFlag; + + if (!isBridge && methodRefs.size() == 1) { + System.out.println("Found suspicious bridge-looking method: " + classEntry.getFullyQualifiedName() + ":" + entry); + } + + if (isBridge) { + for (MethodRef ref : methodRefs) { + JarClassEntry targetClass = jar.getClass(ref.owner, true); + JarMethodEntry targetMethod = new JarMethodEntry(0, ref.name, ref.descriptor, null); + String targetKey = targetMethod.getKey(); + + targetClass.relatedMethods.computeIfAbsent(targetKey, (a) -> new HashSet<>()).add(Pair.of(classEntry, entry.getKey())); + classEntry.relatedMethods.computeIfAbsent(entry.getKey(), (a) -> new HashSet<>()).add(Pair.of(targetClass, targetKey)); + } + } */ + } + } + + private class VisitorMethod extends MethodVisitor { + final JarClassEntry classEntry; + final JarMethodEntry entry; + + VisitorMethod(int api, MethodVisitor methodVisitor, JarClassEntry classEntry, JarMethodEntry entry) { + super(api, methodVisitor); + this.classEntry = classEntry; + this.entry = entry; + } + } + + public void apply() throws IOException { + // Stage 1: read .JAR class/field/method meta + try (FileInputStream fileStream = new FileInputStream(jar.file)) { + try (JarInputStream jarStream = new JarInputStream(fileStream)) { + java.util.jar.JarEntry entry; + + while ((entry = jarStream.getNextJarEntry()) != null) { + if (!entry.getName().endsWith(".class")) { + continue; + } + + ClassReader reader = new ClassReader(jarStream); + ClassVisitor visitor = new VisitorClass(StitchUtil.ASM_VERSION, null); + reader.accept(visitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + } + } + } + + System.err.println("Read " + this.jar.getAllClasses().size() + " (" + this.jar.getClasses().size() + ") classes."); + + // Stage 2: find subclasses + this.jar.getAllClasses().forEach((c) -> c.populateParents(jar)); + System.err.println("Populated subclass entries."); + + // Stage 3: join identical MethodEntries + if (joinMethodEntries) { + System.err.println("Joining MethodEntries..."); + Set traversedClasses = StitchUtil.newIdentityHashSet(); + + int joinedMethods = 1; + int uniqueMethods = 0; + + Collection checkedMethods = StitchUtil.newIdentityHashSet(); + + for (JarClassEntry entry : jar.getAllClasses()) { + if (traversedClasses.contains(entry)) { + continue; + } + + ClassPropagationTree tree = new ClassPropagationTree(jar, entry); + + if (tree.getClasses().size() == 1) { + traversedClasses.add(entry); + continue; + } + + for (JarClassEntry c : tree.getClasses()) { + for (JarMethodEntry m : c.getMethods()) { + if (!checkedMethods.add(m)) { + continue; + } + + // get all matching entries + List mList = m.getMatchingEntries(jar, c); + + if (mList.size() > 1) { + for (int i = 0; i < mList.size(); i++) { + JarClassEntry key = mList.get(i); + JarMethodEntry value = key.getMethod(m.getKey()); + + if (value != m) { + key.methods.put(m.getKey(), m); + joinedMethods++; + } + } + } + } + } + + traversedClasses.addAll(tree.getClasses()); + } + + System.err.println("Joined " + joinedMethods + " MethodEntries (" + uniqueMethods + " unique, " + traversedClasses.size() + " classes)."); + } + + System.err.println("Collecting additional information..."); + + // Stage 4: collect additional info + /* try (FileInputStream fileStream = new FileInputStream(jar.file)) { + try (JarInputStream jarStream = new JarInputStream(fileStream)) { + java.util.jar.JarEntry entry; + + while ((entry = jarStream.getNextJarEntry()) != null) { + if (!entry.getName().endsWith(".class")) { + continue; + } + + ClassReader reader = new ClassReader(jarStream); + ClassVisitor visitor = new VisitorClassStageTwo(StitchUtil.ASM_VERSION, null); + reader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + } + } + } */ + + if (remapper != null) { + System.err.println("Remapping..."); + + Map classTree = new HashMap<>(jar.classTree); + jar.classTree.clear(); + + for (Map.Entry entry : classTree.entrySet()) { + entry.getValue().remap(remapper); + jar.classTree.put(entry.getValue().getKey(), entry.getValue()); + } + } + + System.err.println("- Done. -"); + } } diff --git a/src/main/java/net/fabricmc/stitch/representation/JarRootEntry.java b/src/main/java/net/fabricmc/stitch/representation/JarRootEntry.java index 169ecb5..86689e2 100644 --- a/src/main/java/net/fabricmc/stitch/representation/JarRootEntry.java +++ b/src/main/java/net/fabricmc/stitch/representation/JarRootEntry.java @@ -17,67 +17,74 @@ package net.fabricmc.stitch.representation; import java.io.File; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; public class JarRootEntry extends AbstractJarEntry implements ClassStorage { - final Object syncObject = new Object(); - final File file; - final Map classTree; - final List allClasses; - - public JarRootEntry(File file) { - super(file.getName()); - - this.file = file; - this.classTree = new TreeMap<>(Comparator.naturalOrder()); - this.allClasses = new ArrayList<>(); - } - - @Override - public JarClassEntry getClass(String name, boolean create) { - if (name == null) { - return null; - } - - String[] nameSplit = name.split("\\$"); - int i = 0; - - JarClassEntry parent; - JarClassEntry entry = classTree.get(nameSplit[i++]); - if (entry == null && create) { - entry = new JarClassEntry(nameSplit[0], nameSplit[0]); - synchronized (syncObject) { - allClasses.add(entry); - classTree.put(entry.getName(), entry); - } - } - - StringBuilder fullyQualifiedBuilder = new StringBuilder(nameSplit[0]); - - while (i < nameSplit.length && entry != null) { - fullyQualifiedBuilder.append('$'); - fullyQualifiedBuilder.append(nameSplit[i]); - - parent = entry; - entry = entry.getInnerClass(nameSplit[i++]); - - if (entry == null && create) { - entry = new JarClassEntry(nameSplit[i - 1], fullyQualifiedBuilder.toString()); - synchronized (syncObject) { - allClasses.add(entry); - parent.innerClasses.put(entry.getName(), entry); - } - } - } - - return entry; - } - - public Collection getClasses() { - return classTree.values(); - } - - public Collection getAllClasses() { - return Collections.unmodifiableList(allClasses); - } + final Object syncObject = new Object(); + final File file; + final Map classTree; + final List allClasses; + + public JarRootEntry(File file) { + super(file.getName()); + + this.file = file; + this.classTree = new TreeMap<>(Comparator.naturalOrder()); + this.allClasses = new ArrayList<>(); + } + + @Override + public JarClassEntry getClass(String name, boolean create) { + if (name == null) { + return null; + } + + String[] nameSplit = name.split("\\$"); + int i = 0; + + JarClassEntry parent; + JarClassEntry entry = classTree.get(nameSplit[i++]); + + if (entry == null && create) { + entry = new JarClassEntry(nameSplit[0], nameSplit[0]); + synchronized (syncObject) { + allClasses.add(entry); + classTree.put(entry.getName(), entry); + } + } + + StringBuilder fullyQualifiedBuilder = new StringBuilder(nameSplit[0]); + + while (i < nameSplit.length && entry != null) { + fullyQualifiedBuilder.append('$'); + fullyQualifiedBuilder.append(nameSplit[i]); + + parent = entry; + entry = entry.getInnerClass(nameSplit[i++]); + + if (entry == null && create) { + entry = new JarClassEntry(nameSplit[i - 1], fullyQualifiedBuilder.toString()); + synchronized (syncObject) { + allClasses.add(entry); + parent.innerClasses.put(entry.getName(), entry); + } + } + } + + return entry; + } + + public Collection getClasses() { + return classTree.values(); + } + + public Collection getAllClasses() { + return Collections.unmodifiableList(allClasses); + } } diff --git a/src/main/java/net/fabricmc/stitch/util/FieldNameFinder.java b/src/main/java/net/fabricmc/stitch/util/FieldNameFinder.java index 8e3ef6d..df8e4a3 100644 --- a/src/main/java/net/fabricmc/stitch/util/FieldNameFinder.java +++ b/src/main/java/net/fabricmc/stitch/util/FieldNameFinder.java @@ -16,20 +16,6 @@ package net.fabricmc.stitch.util; -import net.fabricmc.mappings.EntryTriple; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.FieldInsnNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.LdcInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.analysis.Analyzer; -import org.objectweb.asm.tree.analysis.Frame; -import org.objectweb.asm.tree.analysis.SourceInterpreter; -import org.objectweb.asm.tree.analysis.SourceValue; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; @@ -44,8 +30,22 @@ import java.util.jar.JarEntry; import java.util.jar.JarInputStream; -public class FieldNameFinder { +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.SourceInterpreter; +import org.objectweb.asm.tree.analysis.SourceValue; +import net.fabricmc.mappings.EntryTriple; + +public class FieldNameFinder { public Map findNames(Iterable classes) throws Exception { Map> methods = new HashMap<>(); Map> enumFields = new HashMap<>(); @@ -68,21 +68,25 @@ public Map findNames(Map> allEnumFields for (Map.Entry> entry : classes.entrySet()) { String owner = entry.getKey(); Set enumFields = allEnumFields.getOrDefault(owner, Collections.emptySet()); + for (MethodNode mn : entry.getValue()) { Frame[] frames = analyzer.analyze(owner, mn); - InsnList instrs = mn.instructions; + for (int i = 1; i < instrs.size(); i++) { AbstractInsnNode instr1 = instrs.get(i - 1); AbstractInsnNode instr2 = instrs.get(i); String s = null; - if (instr2.getOpcode() == Opcodes.PUTSTATIC && ((FieldInsnNode) instr2).owner.equals(owner) - && (instr1 instanceof MethodInsnNode && ((MethodInsnNode) instr1).owner.equals(owner) || enumFields.contains(((FieldInsnNode) instr2).desc + ((FieldInsnNode) instr2).name)) - && (instr1.getOpcode() == Opcodes.INVOKESTATIC || (instr1.getOpcode() == Opcodes.INVOKESPECIAL && "".equals(((MethodInsnNode) instr1).name)))) { - + if (instr2.getOpcode() == Opcodes.PUTSTATIC + && ((FieldInsnNode) instr2).owner.equals(owner) + && (instr1 instanceof MethodInsnNode + && ((MethodInsnNode) instr1).owner.equals(owner) + || enumFields.contains(((FieldInsnNode) instr2).desc + ((FieldInsnNode) instr2).name)) + && (instr1.getOpcode() == Opcodes.INVOKESTATIC || (instr1.getOpcode() == Opcodes.INVOKESPECIAL && "".equals(((MethodInsnNode) instr1).name)))) { for (int j = 0; j < frames[i - 1].getStackSize(); j++) { SourceValue sv = frames[i - 1].getStack(j); + for (AbstractInsnNode ci : sv.insns) { if (ci instanceof LdcInsnNode && ((LdcInsnNode) ci).cst instanceof String) { //if (s == null || !s.equals(((LdcInsnNode) ci).cst)) { @@ -104,14 +108,17 @@ public Map findNames(Map> allEnumFields int separator = s.indexOf('/'); String sFirst = s.substring(0, separator); String sLast; + if (s.contains(".") && s.indexOf('.') > separator) { sLast = s.substring(separator + 1, s.indexOf('.')); } else { sLast = s.substring(separator + 1); } + if (sFirst.endsWith("s")) { sFirst = sFirst.substring(0, sFirst.length() - 1); } + s = sLast + "_" + sFirst; } @@ -163,7 +170,7 @@ public Map findNames(File file) { try { try (FileInputStream fis = new FileInputStream(file); - JarInputStream jis = new JarInputStream(fis)) { + JarInputStream jis = new JarInputStream(fis)) { byte[] buffer = new byte[32768]; JarEntry entry; @@ -174,6 +181,7 @@ public Map findNames(File file) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); int l; + while ((l = jis.read(buffer, 0, buffer.length)) > 0) { stream.write(buffer, 0, l); } diff --git a/src/main/java/net/fabricmc/stitch/util/MatcherUtil.java b/src/main/java/net/fabricmc/stitch/util/MatcherUtil.java index d5865c6..5268853 100644 --- a/src/main/java/net/fabricmc/stitch/util/MatcherUtil.java +++ b/src/main/java/net/fabricmc/stitch/util/MatcherUtil.java @@ -16,73 +16,74 @@ package net.fabricmc.stitch.util; -import net.fabricmc.mappings.EntryTriple; - import java.io.BufferedReader; import java.io.IOException; import java.util.function.BiConsumer; -import java.util.function.UnaryOperator; + +import net.fabricmc.mappings.EntryTriple; public final class MatcherUtil { - private MatcherUtil() { + public static void read(BufferedReader reader, boolean invert, BiConsumer classMappingConsumer, BiConsumer fieldMappingConsumer, BiConsumer methodMappingConsumer) throws IOException { + String line; + String ownerFrom = null, ownerTo = null; + + while ((line = reader.readLine()) != null) { + String[] parts = line.split("\t"); + + if (parts[0].equals("c") && parts.length == 3) { + // class + ownerFrom = parts[1].substring(1, parts[1].length() - 1); + ownerTo = parts[2].substring(1, parts[2].length() - 1); - } + if (invert) { + classMappingConsumer.accept(ownerTo, ownerFrom); + } else { + classMappingConsumer.accept(ownerFrom, ownerTo); + } + } else if (parts[0].equals("") && ownerFrom != null && parts.length >= 2) { + if (parts[1].equals("f") && parts.length == 4) { + String[] fieldFrom = parts[2].split(";;"); + String[] fieldTo = parts[3].split(";;"); - public static void read(BufferedReader reader, boolean invert, BiConsumer classMappingConsumer, BiConsumer fieldMappingConsumer, BiConsumer methodMappingConsumer) throws IOException { - String line; - String ownerFrom = null, ownerTo = null; + if (invert) { + fieldMappingConsumer.accept( + new EntryTriple(ownerTo, fieldTo[0], fieldTo[1]), + new EntryTriple(ownerFrom, fieldFrom[0], fieldFrom[1]) + ); + } else { + fieldMappingConsumer.accept( + new EntryTriple(ownerFrom, fieldFrom[0], fieldFrom[1]), + new EntryTriple(ownerTo, fieldTo[0], fieldTo[1]) + ); + } + } else if (parts[1].equals("m") && parts.length == 4) { + String[] methodFrom = toMethodArray(parts[2]); + String[] methodTo = toMethodArray(parts[3]); - while ((line = reader.readLine()) != null) { - String[] parts = line.split("\t"); + if (invert) { + methodMappingConsumer.accept( + new EntryTriple(ownerTo, methodTo[0], methodTo[1]), + new EntryTriple(ownerFrom, methodFrom[0], methodFrom[1]) + ); + } else { + methodMappingConsumer.accept( + new EntryTriple(ownerFrom, methodFrom[0], methodFrom[1]), + new EntryTriple(ownerTo, methodTo[0], methodTo[1]) + ); + } + } + } + } + } - if (parts[0].equals("c") && parts.length == 3) { - // class - ownerFrom = parts[1].substring(1, parts[1].length() - 1); - ownerTo = parts[2].substring(1, parts[2].length() - 1); - if (invert) { - classMappingConsumer.accept(ownerTo, ownerFrom); - } else { - classMappingConsumer.accept(ownerFrom, ownerTo); - } - } else if (parts[0].equals("") && ownerFrom != null && parts.length >= 2) { - if (parts[1].equals("f") && parts.length == 4) { - String[] fieldFrom = parts[2].split(";;"); - String[] fieldTo = parts[3].split(";;"); - if (invert) { - fieldMappingConsumer.accept( - new EntryTriple(ownerTo, fieldTo[0], fieldTo[1]), - new EntryTriple(ownerFrom, fieldFrom[0], fieldFrom[1]) - ); - } else { - fieldMappingConsumer.accept( - new EntryTriple(ownerFrom, fieldFrom[0], fieldFrom[1]), - new EntryTriple(ownerTo, fieldTo[0], fieldTo[1]) - ); - } - } else if (parts[1].equals("m") && parts.length == 4) { - String[] methodFrom = toMethodArray(parts[2]); - String[] methodTo = toMethodArray(parts[3]); - if (invert) { - methodMappingConsumer.accept( - new EntryTriple(ownerTo, methodTo[0], methodTo[1]), - new EntryTriple(ownerFrom, methodFrom[0], methodFrom[1]) - ); - } else { - methodMappingConsumer.accept( - new EntryTriple(ownerFrom, methodFrom[0], methodFrom[1]), - new EntryTriple(ownerTo, methodTo[0], methodTo[1]) - ); - } - } - } - } - } + private static String[] toMethodArray(String part) { + int parenPos = part.indexOf('('); + return new String[] { + part.substring(0, parenPos), + part.substring(parenPos) + }; + } - private static String[] toMethodArray(String part) { - int parenPos = part.indexOf('('); - return new String[] { - part.substring(0, parenPos), - part.substring(parenPos) - }; - } -} \ No newline at end of file + private MatcherUtil() { + } +} diff --git a/src/main/java/net/fabricmc/stitch/util/NameFinderVisitor.java b/src/main/java/net/fabricmc/stitch/util/NameFinderVisitor.java index 79b62c7..f128ae5 100644 --- a/src/main/java/net/fabricmc/stitch/util/NameFinderVisitor.java +++ b/src/main/java/net/fabricmc/stitch/util/NameFinderVisitor.java @@ -16,18 +16,18 @@ package net.fabricmc.stitch.util; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.FieldVisitor; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.MethodNode; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.MethodNode; + public class NameFinderVisitor extends ClassVisitor { private String owner; private final Map> allEnumFields; @@ -52,6 +52,7 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin throw new IllegalArgumentException("Found two enum fields with the same name \"" + name + "\"!"); } } + return super.visitField(access, name, descriptor, signature, value); } diff --git a/src/main/java/net/fabricmc/stitch/util/Pair.java b/src/main/java/net/fabricmc/stitch/util/Pair.java index 1025f67..4d490a1 100644 --- a/src/main/java/net/fabricmc/stitch/util/Pair.java +++ b/src/main/java/net/fabricmc/stitch/util/Pair.java @@ -19,57 +19,57 @@ import java.util.Objects; public final class Pair { - private final K left; - private final V right; + private final K left; + private final V right; - private Pair(K left, V right) { - this.left = left; - this.right = right; - } + private Pair(K left, V right) { + this.left = left; + this.right = right; + } - public static Pair of(K left, V right) { - return new Pair<>(left, right); - } + public static Pair of(K left, V right) { + return new Pair<>(left, right); + } - public K getLeft() { - return left; - } + public K getLeft() { + return left; + } - public V getRight() { - return right; - } + public V getRight() { + return right; + } - @Override - protected Object clone() throws CloneNotSupportedException { - //noinspection unchecked - return new Pair(left, right); - } + @Override + protected Object clone() throws CloneNotSupportedException { + //noinspection unchecked + return new Pair(left, right); + } - @Override - public boolean equals(Object o) { - if (!(o instanceof Pair)) { - return false; - } else { - Pair other = (Pair) o; - return Objects.equals(other.left, left) && Objects.equals(other.right, right); - } - } + @Override + public boolean equals(Object o) { + if (!(o instanceof Pair)) { + return false; + } else { + Pair other = (Pair) o; + return Objects.equals(other.left, left) && Objects.equals(other.right, right); + } + } - @Override - public int hashCode() { - if (left == null && right == null) { - return 0; - } else if (left == null) { - return right.hashCode(); - } else if (right == null) { - return left.hashCode(); - } else { - return left.hashCode() * 19 + right.hashCode(); - } - } + @Override + public int hashCode() { + if (left == null && right == null) { + return 0; + } else if (left == null) { + return right.hashCode(); + } else if (right == null) { + return left.hashCode(); + } else { + return left.hashCode() * 19 + right.hashCode(); + } + } - @Override - public String toString() { - return "Pair(" + left + "," + right + ")"; - } + @Override + public String toString() { + return "Pair(" + left + "," + right + ")"; + } } diff --git a/src/main/java/net/fabricmc/stitch/util/RecordValidator.java b/src/main/java/net/fabricmc/stitch/util/RecordValidator.java index 35fad3d..e8c7ff4 100644 --- a/src/main/java/net/fabricmc/stitch/util/RecordValidator.java +++ b/src/main/java/net/fabricmc/stitch/util/RecordValidator.java @@ -16,15 +16,6 @@ package net.fabricmc.stitch.util; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.FieldNode; -import org.objectweb.asm.tree.InvokeDynamicInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.RecordComponentNode; - import java.io.File; import java.io.IOException; import java.nio.file.FileVisitResult; @@ -35,6 +26,15 @@ import java.util.LinkedList; import java.util.List; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.RecordComponentNode; + public class RecordValidator implements AutoCloseable { private static final String[] REQUIRED_METHOD_SIGNATURES = new String[]{ "toString()Ljava/lang/String;", @@ -45,7 +45,6 @@ public class RecordValidator implements AutoCloseable { private final StitchUtil.FileSystemDelegate inputFs; private final Path inputJar; private final boolean printInfo; - private final List errors = new LinkedList<>(); public RecordValidator(File jarFile, boolean printInfo) throws IOException { @@ -89,6 +88,7 @@ private boolean validateClass(byte[] classBytes) { for (RecordComponentNode component : classNode.recordComponents) { // Ensure that a matching method is present boolean foundMethod = false; + for (MethodNode method : classNode.methods) { if (method.name.equals(component.name) && method.desc.equals("()" +component.descriptor)) { foundMethod = true; @@ -98,6 +98,7 @@ private boolean validateClass(byte[] classBytes) { // Ensure that a matching field is present boolean foundField = false; + for (FieldNode field : classNode.fields) { if (field.name.equals(component.name) && field.desc.equals(component.descriptor)) { foundField = true; @@ -117,6 +118,7 @@ private boolean validateClass(byte[] classBytes) { // Ensure that all of the expected methods are present for (String requiredMethodSignature : REQUIRED_METHOD_SIGNATURES) { boolean foundMethod = false; + for (MethodNode method : classNode.methods) { if ((method.name + method.desc).equals(requiredMethodSignature)) { foundMethod = true; @@ -140,7 +142,6 @@ private boolean validateClass(byte[] classBytes) { // Just print some info out about the record. private void printInfo(ClassNode classNode) { StringBuilder sb = new StringBuilder(); - sb.append("Found record ").append(classNode.name).append(" with components:\n"); for (RecordComponentNode componentNode : classNode.recordComponents) { @@ -174,12 +175,10 @@ private String extractToString(ClassNode classNode) { for (AbstractInsnNode insnNode : methodNode.instructions) { if (insnNode instanceof InvokeDynamicInsnNode) { InvokeDynamicInsnNode invokeDynamic = (InvokeDynamicInsnNode) insnNode; - if ( - !invokeDynamic.name.equals("toString") || - !invokeDynamic.desc.equals(String.format("(L%s;)Ljava/lang/String;", classNode.name)) || - !invokeDynamic.bsm.getName().equals("bootstrap") || - !invokeDynamic.bsm.getOwner().equals("java/lang/runtime/ObjectMethods") - ) { + if (!invokeDynamic.name.equals("toString") + || !invokeDynamic.desc.equals(String.format("(L%s;)Ljava/lang/String;", classNode.name)) + || !invokeDynamic.bsm.getName().equals("bootstrap") + || !invokeDynamic.bsm.getOwner().equals("java/lang/runtime/ObjectMethods")) { // Not what we are looking for continue; } diff --git a/src/main/java/net/fabricmc/stitch/util/SnowmanClassVisitor.java b/src/main/java/net/fabricmc/stitch/util/SnowmanClassVisitor.java index 7e165f3..fba3333 100644 --- a/src/main/java/net/fabricmc/stitch/util/SnowmanClassVisitor.java +++ b/src/main/java/net/fabricmc/stitch/util/SnowmanClassVisitor.java @@ -44,9 +44,11 @@ public void visitLocalVariable( final Label end, final int index) { String newName = name; + if (name != null && name.startsWith("\u2603")) { newName = "lvt" + index; } + super.visitLocalVariable(newName, descriptor, signature, start, end, index); } } diff --git a/src/main/java/net/fabricmc/stitch/util/StitchUtil.java b/src/main/java/net/fabricmc/stitch/util/StitchUtil.java index 3052ea6..69e32ce 100644 --- a/src/main/java/net/fabricmc/stitch/util/StitchUtil.java +++ b/src/main/java/net/fabricmc/stitch/util/StitchUtil.java @@ -16,114 +16,121 @@ package net.fabricmc.stitch.util; -import org.objectweb.asm.Opcodes; - import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.net.URLEncoder; import java.nio.file.FileSystem; import java.nio.file.FileSystemAlreadyExistsException; import java.nio.file.FileSystems; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; -public final class StitchUtil { +import org.objectweb.asm.Opcodes; - public static int ASM_VERSION = Opcodes.ASM9; - - public static class FileSystemDelegate implements AutoCloseable { - private final FileSystem fileSystem; - private final boolean owner; - - public FileSystemDelegate(FileSystem fileSystem, boolean owner) { - this.fileSystem = fileSystem; - this.owner = owner; - } - - public FileSystem get() { - return fileSystem; - } - - @Override - public void close() throws IOException { - if (owner) { - fileSystem.close(); - } - } - } - - private StitchUtil() { - - } - - private static final Map jfsArgsCreate = new HashMap<>(); - private static final Map jfsArgsEmpty = new HashMap<>(); - - static { - jfsArgsCreate.put("create", "true"); - } - - public static FileSystemDelegate getJarFileSystem(File f, boolean create) throws IOException { - URI jarUri; - try { - jarUri = new URI("jar:file", null, f.toURI().getPath(), ""); - } catch (URISyntaxException e) { - throw new IOException(e); - } - - try { - return new FileSystemDelegate(FileSystems.newFileSystem(jarUri, create ? jfsArgsCreate : jfsArgsEmpty), true); - } catch (FileSystemAlreadyExistsException e) { - return new FileSystemDelegate(FileSystems.getFileSystem(jarUri), false); - } - } - - public static String join(String joiner, Collection c) { - StringBuilder builder = new StringBuilder(); - int i = 0; - for (String s : c) { - if ((i++) > 0) { - builder.append(joiner); - } - - builder.append(s); - } - return builder.toString(); - } - - public static Set newIdentityHashSet() { - return Collections.newSetFromMap(new IdentityHashMap<>()); - } - - public static List mergePreserveOrder(List first, List second) { - List out = new ArrayList<>(); - int i = 0; - int j = 0; - - while (i < first.size() || j < second.size()) { - while (i < first.size() && j < second.size() - && first.get(i).equals(second.get(j))) { - out.add(first.get(i)); - i++; - j++; - } - - while (i < first.size() && !second.contains(first.get(i))) { - out.add(first.get(i)); - i++; - } - - while (j < second.size() && !first.contains(second.get(j))) { - out.add(second.get(j)); - j++; - } - } - - return out; - } - - public static long getTime() { - return new Date().getTime(); - } +public final class StitchUtil { + private static final Map jfsArgsCreate = new HashMap<>(); + private static final Map jfsArgsEmpty = new HashMap<>(); + public static final int ASM_VERSION = Opcodes.ASM9; + + static { + jfsArgsCreate.put("create", "true"); + } + + public static class FileSystemDelegate implements AutoCloseable { + private final FileSystem fileSystem; + private final boolean owner; + + public FileSystemDelegate(FileSystem fileSystem, boolean owner) { + this.fileSystem = fileSystem; + this.owner = owner; + } + + public FileSystem get() { + return fileSystem; + } + + @Override + public void close() throws IOException { + if (owner) { + fileSystem.close(); + } + } + } + + public static FileSystemDelegate getJarFileSystem(File f, boolean create) throws IOException { + URI jarUri; + + try { + jarUri = new URI("jar:file", null, f.toURI().getPath(), ""); + } catch (URISyntaxException e) { + throw new IOException(e); + } + + try { + return new FileSystemDelegate(FileSystems.newFileSystem(jarUri, create ? jfsArgsCreate : jfsArgsEmpty), true); + } catch (FileSystemAlreadyExistsException e) { + return new FileSystemDelegate(FileSystems.getFileSystem(jarUri), false); + } + } + + public static String join(String joiner, Collection c) { + StringBuilder builder = new StringBuilder(); + int i = 0; + + for (String s : c) { + if ((i++) > 0) { + builder.append(joiner); + } + + builder.append(s); + } + + return builder.toString(); + } + + public static Set newIdentityHashSet() { + return Collections.newSetFromMap(new IdentityHashMap<>()); + } + + public static List mergePreserveOrder(List first, List second) { + List out = new ArrayList<>(); + int i = 0; + int j = 0; + + while (i < first.size() || j < second.size()) { + while (i < first.size() && j < second.size() + && first.get(i).equals(second.get(j))) { + out.add(first.get(i)); + i++; + j++; + } + + while (i < first.size() && !second.contains(first.get(i))) { + out.add(first.get(i)); + i++; + } + + while (j < second.size() && !first.contains(second.get(j))) { + out.add(second.get(j)); + j++; + } + } + + return out; + } + + public static long getTime() { + return new Date().getTime(); + } + + private StitchUtil() { + } } diff --git a/src/main/java/net/fabricmc/stitch/util/SyntheticParameterClassVisitor.java b/src/main/java/net/fabricmc/stitch/util/SyntheticParameterClassVisitor.java index 96934a5..2c5c3fd 100644 --- a/src/main/java/net/fabricmc/stitch/util/SyntheticParameterClassVisitor.java +++ b/src/main/java/net/fabricmc/stitch/util/SyntheticParameterClassVisitor.java @@ -25,81 +25,87 @@ * ProGuard has a bug where parameter annotations are applied incorrectly in the presence of * synthetic arguments. This causes javac to balk when trying to load affected classes. * - * We use several heuristics to guess what the synthetic arguments may be for a particular + *

We use several heuristics to guess what the synthetic arguments may be for a particular * constructor. We then check if the constructor matches our guess, and if so, offset all - * parameter annotations. + * parameter annotations.

*/ public class SyntheticParameterClassVisitor extends ClassVisitor { - private class SyntheticMethodVisitor extends MethodVisitor { - private final int offset; - - SyntheticMethodVisitor(int api, int offset, MethodVisitor methodVisitor) { - super(api, methodVisitor); - this.offset = offset; - } - - @Override - public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) { - return super.visitParameterAnnotation(parameter - offset, descriptor, visible); - } - - @Override - public void visitAnnotableParameterCount(int parameterCount, boolean visible) { - super.visitAnnotableParameterCount(parameterCount - offset, visible); - } - } - - private String className; - private int synthetic; - private String syntheticArgs; - private boolean backoff = false; - - public SyntheticParameterClassVisitor(int api, ClassVisitor cv) { - super(api, cv); - } - - @Override - public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - super.visit(version, access, name, signature, superName, interfaces); - - this.className = name; - - // Enums will always have a string name and then the ordinal - if ((access & Opcodes.ACC_ENUM) != 0) { - synthetic = 2; - syntheticArgs = "(Ljava/lang/String;I"; - } - - if (version >= 55) { - // Backoff on java 11 or newer due to nest mates being used. - backoff = true; - } - } - - @Override - public void visitInnerClass(String name, String outerName, String innerName, int access) { - super.visitInnerClass(name, outerName, innerName, access); - - // If we're a non-static, non-anonymous inner class then we can assume the first argument - // is the parent class. - // See https://docs.oracle.com/javase/specs/jls/se11/html/jls-8.html#jls-8.8.1 - if (synthetic == 0 && name.equals(this.className) && innerName != null && outerName != null && (access & Opcodes.ACC_STATIC) == 0) { - this.synthetic = 1; - this.syntheticArgs = "(L" + outerName + ";"; - } - } - - @Override - public MethodVisitor visitMethod( - final int access, - final String name, - final String descriptor, - final String signature, - final String[] exceptions) { - MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); - - return mv != null && synthetic != 0 && name.equals("") && descriptor.startsWith(syntheticArgs) && !backoff - ? new SyntheticMethodVisitor(api, synthetic, mv) - : mv; - } + private class SyntheticMethodVisitor extends MethodVisitor { + private final int offset; + + SyntheticMethodVisitor(int api, int offset, MethodVisitor methodVisitor) { + super(api, methodVisitor); + this.offset = offset; + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) { + return super.visitParameterAnnotation(parameter - offset, descriptor, visible); + } + + @Override + public void visitAnnotableParameterCount(int parameterCount, boolean visible) { + super.visitAnnotableParameterCount(parameterCount - offset, visible); + } + } + + private String className; + private int synthetic; + private String syntheticArgs; + private boolean backoff = false; + + public SyntheticParameterClassVisitor(int api, ClassVisitor cv) { + super(api, cv); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + + this.className = name; + + // Enums will always have a string name and then the ordinal + if ((access & Opcodes.ACC_ENUM) != 0) { + synthetic = 2; + syntheticArgs = "(Ljava/lang/String;I"; + } + + if (version >= 55) { + // Backoff on java 11 or newer due to nest mates being used. + backoff = true; + } + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + super.visitInnerClass(name, outerName, innerName, access); + + // If we're a non-static, non-anonymous inner class then we can assume the first argument + // is the parent class. + // See https://docs.oracle.com/javase/specs/jls/se11/html/jls-8.html#jls-8.8.1 + if (synthetic == 0 && name.equals(this.className) && innerName != null && outerName != null && (access & Opcodes.ACC_STATIC) == 0) { + this.synthetic = 1; + this.syntheticArgs = "(L" + outerName + ";"; + } + } + + @Override + public MethodVisitor visitMethod( + final int access, + final String name, + final String descriptor, + final String signature, + final String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + + if (mv != null + && synthetic != 0 + && name.equals("") + && descriptor.startsWith(syntheticArgs) + && !backoff) { + return new SyntheticMethodVisitor(api, synthetic, mv); + } + + return mv; + } } diff --git a/src/test/java/net/fabricmc/stitch/tinyv1/Commands.java b/src/test/java/net/fabricmc/stitch/tinyv1/Commands.java index 7464e85..0fde5e5 100644 --- a/src/test/java/net/fabricmc/stitch/tinyv1/Commands.java +++ b/src/test/java/net/fabricmc/stitch/tinyv1/Commands.java @@ -27,9 +27,9 @@ public class Commands { @Disabled public void testOrdering() throws Exception { String[] args = { - "local\\unordered-merged-mappings.tiny", - "local\\merged-mappings.tiny", - "official", "intermediary", "named" + "local\\unordered-merged-mappings.tiny", + "local\\merged-mappings.tiny", + "official", "intermediary", "named" }; new CommandReorderTiny().run(args); @@ -39,9 +39,9 @@ public void testOrdering() throws Exception { @Disabled public void testProposing() throws Exception { String[] args = { - "local\\19w37a-merged.jar", - "local\\merged-mappings.tiny", - "local\\merged-mappings-proposed.tiny" + "local\\19w37a-merged.jar", + "local\\merged-mappings.tiny", + "local\\merged-mappings-proposed.tiny" }; new CommandProposeFieldNames().run(args); diff --git a/src/test/java/net/fabricmc/stitch/tinyv2/Commands.java b/src/test/java/net/fabricmc/stitch/tinyv2/Commands.java index 8d6a55e..9e4a445 100644 --- a/src/test/java/net/fabricmc/stitch/tinyv2/Commands.java +++ b/src/test/java/net/fabricmc/stitch/tinyv2/Commands.java @@ -34,15 +34,11 @@ public static void reorder(String toInvert, String outputTo, String... newOrder) new CommandReorderTinyV2().run(args.toArray(new String[0])); } - public static void merge(String mappingA, String mappingB, String mergedLocation) throws Exception { new CommandMergeTinyV2().run(new String[] {mappingA, mappingB, mergedLocation}); } - public static void proposeFieldNames(String mergedJar, String mergedTinyFile, String newTinyFile) throws Exception { new CommandProposeV2FieldNames().run(new String[] {mergedJar, mergedTinyFile, newTinyFile}); } - - } diff --git a/src/test/java/net/fabricmc/stitch/tinyv2/Snapshot37a.java b/src/test/java/net/fabricmc/stitch/tinyv2/Snapshot37a.java index 1e3b4d3..52934ef 100644 --- a/src/test/java/net/fabricmc/stitch/tinyv2/Snapshot37a.java +++ b/src/test/java/net/fabricmc/stitch/tinyv2/Snapshot37a.java @@ -24,40 +24,37 @@ import org.junit.jupiter.api.Test; public class Snapshot37a { - private static final String DIR = new File(Snapshot37a.class.getClassLoader().getResource("snapshot-37a").getPath()).getAbsolutePath() + "/"; -// @Test public void testMerge() throws Exception { Commands.merge(DIR + "intermediate-mappings-inverted-stitch.tinyv2", - DIR + "yarn-mappings-stitch.tinyv2", - DIR + "merged-unordered.tinyv2" - ); + DIR + "yarn-mappings-stitch.tinyv2", + DIR + "merged-unordered.tinyv2"); } @Test public void testReorder2() throws Exception { Commands.reorder(DIR + "intermediate-mappings-stitch.tinyv2", - DIR + "intermediate-mappings-inverted-stitch.tinyv2", - "intermediary", "official" - ); + DIR + "intermediate-mappings-inverted-stitch.tinyv2", + "intermediary", "official"); } @Test public void testReorder3() throws Exception { String path = DIR + "merged-unordered.tinyv2"; - if(!Files.exists(Paths.get(path))) testMerge(); + + if (!Files.exists(Paths.get(path))) testMerge(); + Commands.reorder(path, - DIR + "merged.tinyv2", - "official", "intermediary", "named" - ); + DIR + "merged.tinyv2", + "official", "intermediary", "named"); } - /** You need the 19w37a-merged.jar file under local/ */ + /** You need the 19w37a-merged.jar file under 'local/'. */ @Test @Disabled public void testFieldNameProposal() throws Exception { Commands.proposeFieldNames("local/19w37a-merged.jar", - DIR + "merged.tinyv2", DIR + "merged-proposed.tinyv2"); + DIR + "merged.tinyv2", DIR + "merged-proposed.tinyv2"); } } diff --git a/src/test/java/net/fabricmc/stitch/tinyv2/Stable1_14_4.java b/src/test/java/net/fabricmc/stitch/tinyv2/Stable1_14_4.java index 63056af..db345fd 100644 --- a/src/test/java/net/fabricmc/stitch/tinyv2/Stable1_14_4.java +++ b/src/test/java/net/fabricmc/stitch/tinyv2/Stable1_14_4.java @@ -40,9 +40,8 @@ public class Stable1_14_4 { @Test public void testReorder2() throws Exception { Commands.reorder(DIR + "intermediary-mappings.tinyv2", - DIR + "intermediary-mappings-inverted.tinyv2", - "intermediary", "official" - ); + DIR + "intermediary-mappings-inverted.tinyv2", + "intermediary", "official"); } @Test @@ -50,61 +49,52 @@ public void testReorder2() throws Exception { public void testMerge() throws Exception { String target = DIR + "merged-unordered.tinyv2"; Commands.merge(DIR + "intermediary-mappings-inverted.tinyv2", - DIR + "yarn-mappings.tinyv2", - target - ); + DIR + "yarn-mappings.tinyv2", + target); + try (BufferedReader reader = Files.newBufferedReader(Paths.get(target))) { TinyTree mappings = TinyMappingFactory.load(reader); - ParameterDef blockInitParam = findMethodParameterMapping("intermediary", "net/minecraft/class_2248", - "", "(Lnet/minecraft/class_2248$class_2251;)V", 1, mappings); + "", "(Lnet/minecraft/class_2248$class_2251;)V", 1, mappings); Assertions.assertEquals("settings", blockInitParam.getName("named")); - } - } private ClassDef findClassMapping(String column, String key, TinyTree mappings) { return find(mappings.getClasses(), c -> c.getName(column).equals(key)) - .orElseThrow(() -> new AssertionError("Could not find key " + key + " in namespace " + column)); + .orElseThrow(() -> new AssertionError("Could not find key " + key + " in namespace " + column)); } private MethodDef findMethodMapping(String column, String className, String methodName, String descriptor, TinyTree mappings) { return find(findClassMapping(column, className, mappings).getMethods(), - m -> m.getName(column).equals(methodName) && m.getDescriptor(column).equals(descriptor) - - ).orElseThrow(() -> new AssertionError("Could not find key " + className + " " + descriptor + " " + methodName + " in namespace " + column)); + m -> m.getName(column).equals(methodName) && m.getDescriptor(column).equals(descriptor)) + .orElseThrow(() -> new AssertionError("Could not find key " + className + " " + descriptor + " " + methodName + " in namespace " + column)); } - - private ParameterDef findMethodParameterMapping(String column, String className, String methodName, String descriptor, - int lvIndex, TinyTree mappings) { + private ParameterDef findMethodParameterMapping(String column, String className, String methodName, String descriptor, int lvIndex, TinyTree mappings) { MethodDef method = findMethodMapping(column, className, methodName, descriptor, mappings); - return find(method.getParameters(), - p -> p.getLocalVariableIndex() == lvIndex) - .orElseThrow(() -> new AssertionError("Could not find key" + className + " " + descriptor + " " + methodName + " " + lvIndex + " in namespace " + column)); + return find(method.getParameters(), p -> p.getLocalVariableIndex() == lvIndex) + .orElseThrow(() -> new AssertionError("Could not find key" + className + " " + descriptor + " " + methodName + " " + lvIndex + " in namespace " + column)); } private Optional find(Collection list, Predicate predicate) { return list.stream().filter(predicate).findFirst(); } - @Test @Disabled public void testReorder3() throws Exception { Commands.reorder(DIR + "merged-unordered.tinyv2", - DIR + "merged.tinyv2", - "official", "intermediary", "named" - ); + DIR + "merged.tinyv2", + "official", "intermediary", "named"); } @Test @Disabled public void testFieldNameProposal() throws Exception { Commands.proposeFieldNames("local/1.14.4-merged.jar", - DIR + "merged.tinyv2", DIR + "merged-proposed.tinyv2"); + DIR + "merged.tinyv2", DIR + "merged-proposed.tinyv2"); } // Requirements: diff --git a/src/test/java/net/fabricmc/stitch/tinyv2/TestTinyV2ReadAndWrite.java b/src/test/java/net/fabricmc/stitch/tinyv2/TestTinyV2ReadAndWrite.java index dbd74fb..e0ded30 100644 --- a/src/test/java/net/fabricmc/stitch/tinyv2/TestTinyV2ReadAndWrite.java +++ b/src/test/java/net/fabricmc/stitch/tinyv2/TestTinyV2ReadAndWrite.java @@ -37,14 +37,13 @@ private void tryToReadAndWrite(String path) throws IOException { TinyFile tinyFile = TinyV2Reader.read(intMappings); Path tempLocation = Paths.get(path + ".temp"); -// tempLocation.toFile().deleteOnExit(); + // tempLocation.toFile().deleteOnExit(); TinyV2Writer.write(tinyFile, tempLocation); String originalIntMappings = new String(Files.readAllBytes(intMappings)); String writtenIntMappings = new String(Files.readAllBytes(tempLocation)); // Ensure the file has not changed - Assertions.assertEquals(originalIntMappings.replace("\r\n","\n"), writtenIntMappings.replace("\r\n","\n")); - + Assertions.assertEquals(originalIntMappings.replace("\r\n", "\n"), writtenIntMappings.replace("\r\n", "\n")); } @Test diff --git a/src/test/java/net/fabricmc/stitch/tinyv2/TestToString.java b/src/test/java/net/fabricmc/stitch/tinyv2/TestToString.java index bb5065f..dd7380c 100644 --- a/src/test/java/net/fabricmc/stitch/tinyv2/TestToString.java +++ b/src/test/java/net/fabricmc/stitch/tinyv2/TestToString.java @@ -27,20 +27,17 @@ import net.fabricmc.stitch.commands.tinyv2.TinyMethod; public class TestToString { - @Test public void testTinyClassToString() { TinyClass tinyClass = new TinyClass( - Arrays.asList("name1", "name2", "name3"), - Collections.singletonList(new TinyMethod("", - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList())), - Collections.singletonList(new TinyField("", Collections.emptyList(), Collections.emptyList())), - Collections.singletonList("Asdf") - ); + Arrays.asList("name1", "name2", "name3"), + Collections.singletonList(new TinyMethod("", + Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList())), + Collections.singletonList(new TinyField("", Collections.emptyList(), Collections.emptyList())), + Collections.singletonList("Asdf")); String expected = "TinyClass(names = [name1, name2, name3], 1 methods, 1 fields, 1 comments)"; Assertions.assertEquals(expected, tinyClass.toString()); } - } From aa0b9eea4317e003f2132914f0daed24a2ab4117 Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Sun, 30 Apr 2023 11:25:37 +0200 Subject: [PATCH 2/2] Link alternatives in README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c0204d..c76ff6a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ # Stitch Stitch is a set of tools for working with and updating mappings in the `.tiny` format. -Large parts of its functionality have been replaced by other tools though, and nowadays, it's mostly only used for [intermediary generation and updating](https://fabricmc.net/wiki/tutorial:updating_yarn). +Over the last few years, large parts of its functionality have been made redundant by other tools though, and nowadays, it's mostly only used for [intermediary generation and updating](https://fabricmc.net/wiki/tutorial:updating_yarn). + +Preferable alternatives: +- [Mapping IO](https://github.com/FabricMC/mapping-io) for reading/writing/converting mappings +- [Name Proposal](https://github.com/FabricMC/name-proposal) for proposing names in Enigma