diff --git a/.gitignore b/.gitignore index a1c2a23..53bd413 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +app/.DS_Store diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..9fcf0b3 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'com.android.application' + id 'top.niunaijun.blackobfuscator' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.0" + + defaultConfig { + applicationId "top.niunaijun.blackobfuscator.asplugin" + minSdkVersion 21 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +BlackObfuscator { + enabled true + obfClass = ["MainActivity", "com.bbb"] +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.google.android.material:material:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/top/niunaijun/blackobfuscator/asplugin/ExampleInstrumentedTest.java b/app/src/androidTest/java/top/niunaijun/blackobfuscator/asplugin/ExampleInstrumentedTest.java new file mode 100644 index 0000000..fe0ba4c --- /dev/null +++ b/app/src/androidTest/java/top/niunaijun/blackobfuscator/asplugin/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package top.niunaijun.blackobfuscator.asplugin; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("top.niunaijun.blackobfuscator.asplugin", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f96d65e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/top/niunaijun/blackobfuscator/asplugin/MainActivity.java b/app/src/main/java/top/niunaijun/blackobfuscator/asplugin/MainActivity.java new file mode 100644 index 0000000..800774f --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackobfuscator/asplugin/MainActivity.java @@ -0,0 +1,20 @@ +package top.niunaijun.blackobfuscator.asplugin; + +import androidx.appcompat.app.AppCompatActivity; + +import android.os.Bundle; +import android.util.Log; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + if (System.currentTimeMillis() > 3) { + Log.d("123", "onCreate: sdfasdffakea"); + } else { + Log.d("123", "onCreate: aax1xaaa"); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..4fc2444 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..eba0b55 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..eb4b874 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + BlackObfuscator-ASPlugin + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..2e9414b --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/test/java/top/niunaijun/blackobfuscator/asplugin/ExampleUnitTest.java b/app/src/test/java/top/niunaijun/blackobfuscator/asplugin/ExampleUnitTest.java new file mode 100644 index 0000000..0b88696 --- /dev/null +++ b/app/src/test/java/top/niunaijun/blackobfuscator/asplugin/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package top.niunaijun.blackobfuscator.asplugin; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..6378090 --- /dev/null +++ b/build.gradle @@ -0,0 +1,35 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + mavenCentral() + maven { url 'https://jitpack.io' } + maven { + url uri('./localRepo') + } + } + dependencies { + classpath "com.android.tools.build:gradle:4.2.0" + classpath "top.niunaijun.blackobfuscator:plugin:1.0.0" + classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + mavenCentral() + jcenter() // Warning: this repository is going to shut down soon + maven { + url uri('./localRepo') + } + maven { url 'https://jitpack.io' } + } +} + + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..6826e61 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8cadffd --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Dec 16 21:12:52 CST 2021 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + 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 +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 +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 + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; 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 + # 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\"" + fi + i=$((i+1)) + 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, 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" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +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 + +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% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..9fffe5f --- /dev/null +++ b/local.properties @@ -0,0 +1,10 @@ +## This file is automatically generated by Android Studio. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file should *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +sdk.dir=/Users/milk/Library/Android/sdk \ No newline at end of file diff --git a/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.jar.md5 b/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.jar.md5 new file mode 100644 index 0000000..6b20fc3 --- /dev/null +++ b/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.jar.md5 @@ -0,0 +1 @@ +eb5248e2716058acae2a9dd02383c2b9 \ No newline at end of file diff --git a/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.jar.sha1 b/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.jar.sha1 new file mode 100644 index 0000000..8d0e816 --- /dev/null +++ b/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.jar.sha1 @@ -0,0 +1 @@ +ed2863a8212fcb1c0613c75573fc455c62ae2174 \ No newline at end of file diff --git a/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.pom b/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.pom new file mode 100644 index 0000000..e5429d4 --- /dev/null +++ b/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.pom @@ -0,0 +1,34 @@ + + + 4.0.0 + top.niunaijun.blackobfuscator + plugin + 1.0.0 + + + com.android.tools.build + gradle + 3.3.1 + runtime + + + com.android.tools.build + transform-api + 1.5.0 + runtime + + + commons-io + commons-io + 2.5 + runtime + + + com.github.CodingGay.BlackObfuscator + dex-tools + 1.0.3 + runtime + + + diff --git a/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.pom.md5 b/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.pom.md5 new file mode 100644 index 0000000..d892484 --- /dev/null +++ b/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.pom.md5 @@ -0,0 +1 @@ +58be27900eade6fbad79a9328c336ad6 \ No newline at end of file diff --git a/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.pom.sha1 b/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.pom.sha1 new file mode 100644 index 0000000..d4baeba --- /dev/null +++ b/localRepo/top/niunaijun/blackobfuscator/plugin/1.0.0/plugin-1.0.0.pom.sha1 @@ -0,0 +1 @@ +4d286fbaa7f641cab7e669d781b564c0db4cf0fd \ No newline at end of file diff --git a/localRepo/top/niunaijun/blackobfuscator/plugin/maven-metadata.xml b/localRepo/top/niunaijun/blackobfuscator/plugin/maven-metadata.xml new file mode 100644 index 0000000..9fcb19b --- /dev/null +++ b/localRepo/top/niunaijun/blackobfuscator/plugin/maven-metadata.xml @@ -0,0 +1,12 @@ + + + top.niunaijun.blackobfuscator + plugin + + 1.0.0 + + 1.0.0 + + 20211217093825 + + diff --git a/localRepo/top/niunaijun/blackobfuscator/plugin/maven-metadata.xml.md5 b/localRepo/top/niunaijun/blackobfuscator/plugin/maven-metadata.xml.md5 new file mode 100644 index 0000000..c5d81df --- /dev/null +++ b/localRepo/top/niunaijun/blackobfuscator/plugin/maven-metadata.xml.md5 @@ -0,0 +1 @@ +5767fd64899b22b1d9fbfae502f7bfbb \ No newline at end of file diff --git a/localRepo/top/niunaijun/blackobfuscator/plugin/maven-metadata.xml.sha1 b/localRepo/top/niunaijun/blackobfuscator/plugin/maven-metadata.xml.sha1 new file mode 100644 index 0000000..9393ba9 --- /dev/null +++ b/localRepo/top/niunaijun/blackobfuscator/plugin/maven-metadata.xml.sha1 @@ -0,0 +1 @@ +28c12c615c6034b1d384ef8a5c5a7282dfc2b942 \ No newline at end of file diff --git a/plugin/.gitignore b/plugin/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/plugin/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/plugin/build.gradle b/plugin/build.gradle new file mode 100644 index 0000000..5779aef --- /dev/null +++ b/plugin/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'groovy' +//apply plugin: 'maven' +apply plugin: 'com.github.dcendents.android-maven' + +group='top.niunaijun.blackobfuscator' +dependencies { + implementation "com.android.tools.build:gradle:3.3.1" + implementation "com.android.tools.build:transform-api:1.5.0" + implementation "commons-io:commons-io:2.5" + //gradle sdk + implementation gradleApi() + //groovy sdk + implementation localGroovy() + implementation "com.github.CodingGay.BlackObfuscator:dex-tools:1.0.3" +} + +repositories { + mavenCentral() + maven { + url uri('localRepo') + } +} + +group='top.niunaijun.blackobfuscator' +version='1.0.0' +//uploadArchives { +// repositories { +// mavenDeployer { +// //提交到远程服务器: +// // repository(url: "http://www.xxx.com/repos") { +// // authentication(userName: "admin", password: "admin") +// // } +// //本地的Maven地址设置为E:/Maven +// repository(url: uri('../localRepo')) +// } +// } +//} \ No newline at end of file diff --git a/plugin/src/main/groovy/top/niunaijun/blackobfuscator/BlackObfuscatorExtension.groovy b/plugin/src/main/groovy/top/niunaijun/blackobfuscator/BlackObfuscatorExtension.groovy new file mode 100644 index 0000000..52b7910 --- /dev/null +++ b/plugin/src/main/groovy/top/niunaijun/blackobfuscator/BlackObfuscatorExtension.groovy @@ -0,0 +1,22 @@ +package top.niunaijun.blackobfuscator +import org.gradle.api.Project + +class BlackObfuscatorExtension { + boolean enabled = false + int depth = 1 + String[] obfClass = [] + + BlackObfuscatorExtension(Project project) { + + } + + + @Override + public String toString() { + return "BlackObfuscatorExtension{" + + "enabled=" + enabled + + ", depth=" + depth + + ", obfClass=" + Arrays.toString(obfClass) + + '}'; + } +} \ No newline at end of file diff --git a/plugin/src/main/groovy/top/niunaijun/blackobfuscator/ObfInject.groovy b/plugin/src/main/groovy/top/niunaijun/blackobfuscator/ObfInject.groovy new file mode 100644 index 0000000..ba4b017 --- /dev/null +++ b/plugin/src/main/groovy/top/niunaijun/blackobfuscator/ObfInject.groovy @@ -0,0 +1,219 @@ +package top.niunaijun.blackobfuscator + +import com.android.dex.util.FileUtils +import com.android.dx.command.Main +import com.googlecode.d2j.converter.IR2JConverter +import com.googlecode.d2j.converter.J2IRConverter +import com.googlecode.dex2jar.ir.IrMethod +import com.googlecode.dex2jar.ir.ts.AggTransformer +import com.googlecode.dex2jar.ir.ts.CleanLabel +import com.googlecode.dex2jar.ir.ts.DeadCodeTransformer +import com.googlecode.dex2jar.ir.ts.ExceptionHandlerTrim +import com.googlecode.dex2jar.ir.ts.Ir2JRegAssignTransformer +import com.googlecode.dex2jar.ir.ts.NewTransformer +import com.googlecode.dex2jar.ir.ts.NpeTransformer +import com.googlecode.dex2jar.ir.ts.RemoveConstantFromSSA +import com.googlecode.dex2jar.ir.ts.RemoveLocalFromSSA +import com.googlecode.dex2jar.ir.ts.TypeTransformer +import com.googlecode.dex2jar.ir.ts.UnSSATransformer +import com.googlecode.dex2jar.ir.ts.VoidInvokeTransformer +import com.googlecode.dex2jar.ir.ts.ZeroTransformer +import com.googlecode.dex2jar.ir.ts.array.FillArrayTransformer +import com.googlecode.dex2jar.tools.Dex2jarCmd +import org.gradle.api.Project +import org.jf.CloseUtils +import org.jf.DexLib2Utils +import org.objectweb.asm2.ClassReader +import org.objectweb.asm2.ClassWriter +import org.objectweb.asm2.tree.ClassNode +import org.objectweb.asm2.tree.MethodNode +import top.niunaijun.blackobfuscator.core.utils.zip.UtilsZip +import top.niunaijun.obfuscator.ObfuscatorConfiguration + +class ObfInject { + private Project mProject + private BlackObfuscatorExtension mObfuscatorExtension + + protected final CleanLabel T_cleanLabel = new CleanLabel(); + protected final Ir2JRegAssignTransformer T_ir2jRegAssign = new Ir2JRegAssignTransformer(); + protected final NewTransformer T_new = new NewTransformer(); + protected final RemoveConstantFromSSA T_removeConst = new RemoveConstantFromSSA(); + protected final RemoveLocalFromSSA T_removeLocal = new RemoveLocalFromSSA(); + protected final ExceptionHandlerTrim T_trimEx = new ExceptionHandlerTrim(); + protected final TypeTransformer T_type = new TypeTransformer(); + protected final DeadCodeTransformer T_deadCode = new DeadCodeTransformer(); + protected final FillArrayTransformer T_fillArray = new FillArrayTransformer(); + protected final AggTransformer T_agg = new AggTransformer(); + protected final UnSSATransformer T_unssa = new UnSSATransformer(); + protected final ZeroTransformer T_zero = new ZeroTransformer(); + protected final VoidInvokeTransformer T_voidInvoke = new VoidInvokeTransformer(); + protected final NpeTransformer T_npe = new NpeTransformer(); + + ObfInject(Project project) { + mProject = project + mObfuscatorExtension = ObfPlugin.sObfuscatorExtension + } + + void addJar(String jar) { + } + + void inject(String path) { + if (!mObfuscatorExtension.enabled) { + return; + } + + File dir = new File(path) + + if (dir.isDirectory()) { + dir.eachFileRecurse { File file -> + if (file.isFile() && file.getAbsolutePath().endsWith(".class")) { + byte[] data = FileUtils.readFile(file) + ClassNode cn = readClassNode(data) + reBuildClass(cn) + createFile(file.getAbsolutePath(), toByteArray(cn)) + } + } + fixClass(dir) + } + } + + private void fixClass(File dir) { + File outDex = new File(dir, "obf.dex") + File fixDex = new File(dir, "obf_fix.dex") + File outJar = new File(dir, "obf.jar") + outDex.delete() + fixDex.delete() + outJar.delete() + Main.main("--dex", "--output=" + outDex.getAbsolutePath(), dir.getAbsolutePath()) + if (outDex.exists()) { + DexLib2Utils.saveDex(outDex, fixDex) + } + + if (fixDex.exists()) { + new Dex2jarCmd(null).doMain( + "-f", + "-o", outJar.getAbsolutePath(), + fixDex.getAbsolutePath()); + } + if (outJar.exists()) { + UtilsZip.release(outJar.getAbsolutePath(), dir.getAbsolutePath(), new UtilsZip.UnzipCallback() { + @Override + boolean accept(String name) { + def newName = name.replace("/", ".") + for (String accept : mObfuscatorExtension.obfClass) { + if (newName.contains(accept)) { + return true + } + } + return false + } + }) + } + outDex.delete() + fixDex.delete() + outJar.delete() + } + + private void reBuildClass(ClassNode cn) { + for (Iterator it = cn.methods.iterator(); it.hasNext(); ) { + MethodNode m = it.next(); + try { + IrMethod irMethod = J2IRConverter.convert(cn.name, m); + opt(irMethod); + // convert ir to m3 + MethodNode m3 = new MethodNode(); + m3.tryCatchBlocks = new ArrayList<>(); + new IR2JConverter() + .ir(irMethod) + .asm(m3) + .obf(new ObfuscatorConfiguration() { + @Override + protected int getObfDepth() { + return mObfuscatorExtension.depth; + } + + @Override + protected boolean accept(String className, String methodName) { + for (String accept : mObfuscatorExtension.obfClass) { + if (className.contains(accept)) { + return true + } + } + return false + } + }) + .convert(); + + // copy back m3 to m + m.maxLocals = -1; + m.maxLocals = -1; + m.instructions = m3.instructions; + m.tryCatchBlocks = m3.tryCatchBlocks; + m.localVariables = null; + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + + void opt(IrMethod irMethod) { + T_deadCode.transform(irMethod); + T_cleanLabel.transform(irMethod); + T_removeLocal.transform(irMethod); + T_removeConst.transform(irMethod); + T_zero.transform(irMethod); + if (T_npe.transformReportChanged(irMethod)) { + T_deadCode.transform(irMethod); + T_removeLocal.transform(irMethod); + T_removeConst.transform(irMethod); + } + T_new.transform(irMethod); + T_fillArray.transform(irMethod); + T_agg.transform(irMethod); + T_voidInvoke.transform(irMethod); + + T_type.transform(irMethod); + T_unssa.transform(irMethod); + T_trimEx.transform(irMethod); + T_ir2jRegAssign.transform(irMethod); + } + + private byte[] toByteArray(ClassNode cn) { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); + cn.accept(cw); + return cw.toByteArray(); + } + + + private ClassNode readClassNode(byte[] data) { + ClassReader cr = new ClassReader(data); + ClassNode cn = new ClassNode(); + cr.accept(cn, ClassReader.EXPAND_FRAMES | ClassReader.SKIP_FRAMES); + return cn + } + + private static boolean createFile(String path, byte[] content) { + FileOutputStream fos = null; + try { + File file = new File(path); + if (file.exists()) { + boolean del = file.delete(); + if (!del) { + return false; + } + } + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + fos = new FileOutputStream(path); + fos.write(content); + fos.flush(); + return file.exists(); + } catch (Exception e) { + e.printStackTrace(); + return false; + } finally { + CloseUtils.close(fos); + } + } +} diff --git a/plugin/src/main/groovy/top/niunaijun/blackobfuscator/ObfPlugin.groovy b/plugin/src/main/groovy/top/niunaijun/blackobfuscator/ObfPlugin.groovy new file mode 100644 index 0000000..7159c61 --- /dev/null +++ b/plugin/src/main/groovy/top/niunaijun/blackobfuscator/ObfPlugin.groovy @@ -0,0 +1,29 @@ +package top.niunaijun.blackobfuscator + +import org.gradle.api.Plugin +import org.gradle.api.Project + +import com.android.build.gradle.AppExtension + +public class ObfPlugin implements Plugin { + private String PLUGIN_NAME = "BlackObfuscator" + private Project mProject + public static BlackObfuscatorExtension sObfuscatorExtension + + void apply(Project project) { + this.mProject = project + def android = project.extensions.getByType(AppExtension) + project.configurations.create(PLUGIN_NAME).extendsFrom(project.configurations.compile) + sObfuscatorExtension = project.extensions.create(PLUGIN_NAME, BlackObfuscatorExtension, project) + + project.afterEvaluate { + System.out.println("=====BlackObfuscator=====") + System.out.println(sObfuscatorExtension.toString()) + System.out.println("=========================") + } + + //注册一个Transform + def classTransform = new ObfTransform(project) + android.registerTransform(classTransform) + } +} \ No newline at end of file diff --git a/plugin/src/main/groovy/top/niunaijun/blackobfuscator/ObfTransform.java b/plugin/src/main/groovy/top/niunaijun/blackobfuscator/ObfTransform.java new file mode 100644 index 0000000..a2bce93 --- /dev/null +++ b/plugin/src/main/groovy/top/niunaijun/blackobfuscator/ObfTransform.java @@ -0,0 +1,83 @@ +package top.niunaijun.blackobfuscator; + + +import com.android.build.api.transform.Context; +import com.android.build.api.transform.DirectoryInput; +import com.android.build.api.transform.Format; +import com.android.build.api.transform.JarInput; +import com.android.build.api.transform.QualifiedContent; +import com.android.build.api.transform.Transform; +import com.android.build.api.transform.TransformException; +import com.android.build.api.transform.TransformInput; +import com.android.build.api.transform.TransformOutputProvider; +import com.android.build.gradle.internal.pipeline.TransformManager; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.gradle.api.Project; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Set; + +public class ObfTransform extends Transform { + + private Project mProject; + private BlackObfuscatorExtension mObfuscatorExtension; + + public ObfTransform(Project p) { + this.mProject = p; + } + + @Override + public String getName() { + return "BlackObfuscatorTransform"; + } + + @Override + public Set getInputTypes() { + return TransformManager.CONTENT_CLASS; + } + + @Override + public Set getScopes() { + return TransformManager.SCOPE_FULL_PROJECT; + } + + @Override + public boolean isIncremental() { + return false; + } + + @Override + public void transform(Context context, + Collection inputs, + Collection referencedInputs, + TransformOutputProvider outputProvider, + boolean isIncremental) throws IOException, TransformException, InterruptedException { +// System.out.println("in aop transform"); + ObfInject inject = new ObfInject(mProject); + for (TransformInput input : inputs) { + for (DirectoryInput directoryInput : input.getDirectoryInputs()) { + inject.inject(directoryInput.getFile().getAbsolutePath()); + File name = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY); + FileUtils.copyDirectory(directoryInput.getFile(), name); +// FileUtils.deleteDirectory(directoryInput.getFile()); +// System.out.println("transform dir: " + name.getAbsolutePath()); + } + + for (JarInput jarInput : input.getJarInputs()) { + String jarName = jarInput.getName(); + String md5Name = DigestUtils.md5Hex(jarInput.getFile().getAbsolutePath()); + if (jarName.endsWith(".jar")) { + jarName = jarName.substring(0, jarName.length() - 4); + } + File name = outputProvider.getContentLocation(jarName + md5Name, jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR); + FileUtils.copyFile(jarInput.getFile(), name); +// System.out.println("transform jar: " + jarInput.getFile()); + inject.addJar(jarInput.getFile().getAbsolutePath()); + } + } + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/MyClass.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/MyClass.java new file mode 100644 index 0000000..d15acf5 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/MyClass.java @@ -0,0 +1,4 @@ +package top.niunaijun.blackobfuscator.core; + +public class MyClass { +} \ No newline at end of file diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/CopyEntryCallback.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/CopyEntryCallback.java new file mode 100644 index 0000000..1dd153c --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/CopyEntryCallback.java @@ -0,0 +1,18 @@ +package top.niunaijun.blackobfuscator.core.utils.zip; + +public interface CopyEntryCallback { + /** + * @param current 当前zipEntry序号 + * @param total ZipEntry总个数 + * @return Null不进行复制 否则进行复制 + */ + ZipEntry filter(ZipEntry zipEntry, int current, int total); + + /** + * @param current 当前已解压字节数 + * @param total 总字节数 + */ + void onProgress(long current, long total); + + void done(ZipEntry zipEntry); +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ExtractCallback.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ExtractCallback.java new file mode 100644 index 0000000..685fc9d --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ExtractCallback.java @@ -0,0 +1,20 @@ +package top.niunaijun.blackobfuscator.core.utils.zip; + +import java.io.File; + +public interface ExtractCallback { + /** + * @param current 当前Entry序号 + * @param total 总Entry个数 + * @return Null不进行解压 否则进行解压 + */ + File filter(ZipEntry zipEntry, int current, int total); + + /** + * @param current 当前文件已解压字节数 + * @param total 当前文件总字节数 + */ + void onProgress(long current, long total); + + void done(ZipEntry zipEntry, File file); +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/JarMarker.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/JarMarker.java new file mode 100644 index 0000000..6a5b8e4 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/JarMarker.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip; + +import top.niunaijun.blackobfuscator.core.utils.zip.extrafield.ZipExtraField; + +import java.util.zip.ZipException; + + +/** + * If this extra field is added as the very first extra field of the + * archive, Solaris will consider it an executable jar file. + * + * @since Ant 1.6.3 + */ +public final class JarMarker implements ZipExtraField { + + private static final ZipShort ID = new ZipShort(0xCAFE); + private static final ZipShort NULL = new ZipShort(0); + private static final byte[] NO_BYTES = new byte[0]; + private static final JarMarker DEFAULT = new JarMarker(); + + /** + * No-arg constructor + */ + public JarMarker() { + // empty + } + + /** + * Since JarMarker is stateless we can always use the same instance. + * + * @return the DEFAULT jarmaker. + */ + public static JarMarker getInstance() { + return DEFAULT; + } + + /** + * The Header-ID. + * + * @return the header id + */ + public ZipShort getHeaderId() { + return ID; + } + + /** + * Length of the extra field in the local file data - without + * Header-ID or length specifier. + * + * @return 0 + */ + public ZipShort getLocalFileDataLength() { + return NULL; + } + + /** + * Length of the extra field in the central directory - without + * Header-ID or length specifier. + * + * @return 0 + */ + public ZipShort getCentralDirectoryLength() { + return NULL; + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return the data + * @since 1.1 + */ + public byte[] getLocalFileDataData() { + return NO_BYTES; + } + + /** + * The actual data to put central directory - without Header-ID or + * length specifier. + * + * @return the data + */ + public byte[] getCentralDirectoryData() { + return NO_BYTES; + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param data an array of bytes + * @param offset the start offset + * @param length the number of bytes in the array from offset + * @throws ZipException on error + */ + public void parseFromLocalFileData(byte[] data, int offset, int length) + throws ZipException { + if (length != 0) { + throw new ZipException("JarMarker doesn't expect any data"); + } + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/UnixStat.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/UnixStat.java new file mode 100644 index 0000000..2592fc0 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/UnixStat.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip; + +/** + * Constants from stat.h on Unix systems. + */ +// CheckStyle:InterfaceIsTypeCheck OFF - backward compatible +@SuppressWarnings("OctalInteger") +public interface UnixStat { + + /** + * Bits used for permissions (and sticky bit) + * + * @since 1.1 + */ + int PERM_MASK = 07777; + /** + * Indicates symbolic links. + * + * @since 1.1 + */ + int LINK_FLAG = 0120000; + /** + * Indicates plain files. + * + * @since 1.1 + */ + int FILE_FLAG = 0100000; + /** + * Indicates directories. + * + * @since 1.1 + */ + int DIR_FLAG = 040000; + + // ---------------------------------------------------------- + // somewhat arbitrary choices that are quite common for shared + // installations + // ----------------------------------------------------------- + + /** + * Default permissions for symbolic links. + * + * @since 1.1 + */ + int DEFAULT_LINK_PERM = 0777; + /** + * Default permissions for directories. + * + * @since 1.1 + */ + int DEFAULT_DIR_PERM = 0755; + /** + * Default permissions for plain files. + * + * @since 1.1 + */ + int DEFAULT_FILE_PERM = 0644; +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/UtilsZip.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/UtilsZip.java new file mode 100644 index 0000000..ec2a053 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/UtilsZip.java @@ -0,0 +1,111 @@ +package top.niunaijun.blackobfuscator.core.utils.zip; + + +import org.apache.commons.io.FileUtils; +import org.jf.CloseUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + + +/** + * Created by sunwanquan on 2019/6/18. + * * ∧_∧ + * (`・ω・∥ + * 丶 つ0 + * しーJ + * 此处无Bug + */ +public class UtilsZip { + + /** + * 解压ZIP + * + * @param zipPath zip路径 + * @param outPath 输出目录 + * @return 是否成功 + */ + public static boolean release(String zipPath, String outPath, UnzipCallback callback) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + File out = new File(outPath); + if (out.exists()) { + if (out.isFile()) + throw new RuntimeException("outPath is a file!"); + } else { + out.mkdirs(); + } + + ZipFile zipFile = new ZipFile(zipPath); + Enumeration entries = zipFile.getEntries(); + while (entries.hasMoreElements()) { + try { + ZipEntry zipEntry = (ZipEntry) entries.nextElement(); + if (callback != null) { + if (!callback.accept(zipEntry.getName())) { + continue; + } + } + File outFile = new File(outPath + File.separator + zipEntry.getName()); + + if (zipEntry.isDirectory()) { + outFile.mkdirs(); + } else { + if (!outFile.getParentFile().exists()) { + outFile.getParentFile().mkdirs(); + } + outFile.createNewFile(); + inputStream = zipFile.getInputStream(zipEntry); + outputStream = new FileOutputStream(outFile); + byte[] bytes = new byte[1024 * 5]; + int read; + while ((read = inputStream.read(bytes)) != -1) { + outputStream.write(bytes, 0, read); + } + } + } finally { + CloseUtils.close(inputStream, outputStream); + } + } + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } finally { + CloseUtils.close(inputStream, outputStream); + } + } + + public interface UnzipCallback { + boolean accept(String name); + } + + private static boolean isExist(List strings, String s) { + for (String str : strings) { + if (s.startsWith(str)) { + return true; + } + } + return false; + } + + public static InputStream getZipEntryInputStream(String zipFile, String entryName) { + ZipFile zipFile1 = null; + try { + zipFile1 = new ZipFile(zipFile); + return zipFile1.getInputStream(zipFile1.getEntry(entryName)); + } catch (IOException e) { + e.printStackTrace(); + } finally { +// CloseUtils.close(zipFile1); + } + return null; + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipCallback.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipCallback.java new file mode 100644 index 0000000..f705c5d --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipCallback.java @@ -0,0 +1,5 @@ +package top.niunaijun.blackobfuscator.core.utils.zip; + +public interface ZipCallback { + void onProgress(long current, long total); +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipEntry.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipEntry.java new file mode 100644 index 0000000..cd18150 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipEntry.java @@ -0,0 +1,601 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip; + + +import top.niunaijun.blackobfuscator.core.utils.zip.extrafield.CentralDirectoryParsingZipExtraField; +import top.niunaijun.blackobfuscator.core.utils.zip.extrafield.ExtraFieldUtils; +import top.niunaijun.blackobfuscator.core.utils.zip.extrafield.UnparseableExtraFieldData; +import top.niunaijun.blackobfuscator.core.utils.zip.extrafield.ZipExtraField; + +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.zip.ZipException; + +/** + * Extension that adds better handling of extra fields and provides + * access to the internal and external file attributes. + *

+ *

The extra data is expected to follow the recommendation of + * the .ZIP File Format Specification created by PKWARE Inc. :

+ *
    + *
  • the extra byte array consists of a sequence of extra fields
  • + *
  • each extra fields starts by a two byte header id followed by + * a two byte sequence holding the length of the remainder of + * data.
  • + *
+ *

+ *

Any extra data that cannot be parsed by the rules above will be + * consumed as "unparseable" extra data and treated differently by the + * methods of this class. Versions prior to Apache Commons Compress + * 1.1 would have thrown an exception if any attempt was made to read + * or write extra data not conforming to the recommendation.

+ * + * @see + * .ZIP File Format Specification + */ +public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { + + public static final int PLATFORM_UNIX = 3; + public static final int PLATFORM_FAT = 0; + private static final int SHORT_MASK = 0xFFFF; + private static final int SHORT_SHIFT = 16; + + private int internalAttributes = 0; + private int platform = PLATFORM_FAT; + private long externalAttributes = 0; + private LinkedHashMap extraFields = null; + private UnparseableExtraFieldData unparseableExtra = null; + + private String name = null; + private String parent = null; + private String simpleName = null; + + /** + * Creates a new zip entry with the specified name. + * + * @param name the name of the entry + * @since 1.1 + */ + public ZipEntry(String name) { + super(name); + setName(name); + } + + /** + * Creates a new zip entry with fields taken from the specified zip entry. + * + * @param entry the entry to get fields from + * @throws ZipException on error + * @since 1.1 + */ + public ZipEntry(java.util.zip.ZipEntry entry) throws ZipException { + super(entry); + setName(entry.getName()); + byte[] extra = entry.getExtra(); + if (extra != null) { + setExtraFields(ExtraFieldUtils.parse(extra, true, + ExtraFieldUtils + .UnparseableExtraField.READ)); + } else { + // initializes extra data to an empty byte array + setExtra(); + } + } + + /** + * Creates a new zip entry with fields taken from the specified zip entry. + * + * @param entry the entry to get fields from + * @throws ZipException on error + * @since 1.1 + */ + public ZipEntry(ZipEntry entry) throws ZipException { + this((java.util.zip.ZipEntry) entry); + setInternalAttributes(entry.getInternalAttributes()); + setExternalAttributes(entry.getExternalAttributes()); + setExtraFields(entry.getExtraFields(true)); + } + + /** + * @since 1.9 + */ + protected ZipEntry() { + super(""); + } + + /** + * Overwrite clone. + * + * @return a cloned copy of this ZipEntry + * @since 1.1 + */ + public Object clone() { + ZipEntry e = (ZipEntry) super.clone(); + + e.setInternalAttributes(getInternalAttributes()); + e.setExternalAttributes(getExternalAttributes()); + e.setExtraFields(getExtraFields(true)); + return e; + } + + /** + * Retrieves the internal file attributes. + * + * @return the internal file attributes + * @since 1.1 + */ + public int getInternalAttributes() { + return internalAttributes; + } + + /** + * Sets the internal file attributes. + * + * @param value an int value + * @since 1.1 + */ + public void setInternalAttributes(int value) { + internalAttributes = value; + } + + /** + * Retrieves the external file attributes. + * + * @return the external file attributes + * @since 1.1 + */ + public long getExternalAttributes() { + return externalAttributes; + } + + /** + * Sets the external file attributes. + * + * @param value an long value + * @since 1.1 + */ + public void setExternalAttributes(long value) { + externalAttributes = value; + } + + /** + * Sets Unix permissions in a way that is understood by Info-Zip's + * unzip command. + * + * @param mode an int value + * @since Ant 1.5.2 + */ + @SuppressWarnings("OctalInteger") + public void setUnixMode(int mode) { + // CheckStyle:MagicNumberCheck OFF - no point + setExternalAttributes((mode << SHORT_SHIFT) + // MS-DOS read-only attribute + | ((mode & 0200) == 0 ? 1 : 0) + // MS-DOS directory flag + | (isDirectory() ? 0x10 : 0)); + // CheckStyle:MagicNumberCheck ON + platform = PLATFORM_UNIX; + } + + /** + * Unix permission. + * + * @return the unix permissions + * @since Ant 1.6 + */ + public int getUnixMode() { + return platform != PLATFORM_UNIX ? 0 : + (int) ((getExternalAttributes() >> SHORT_SHIFT) & SHORT_MASK); + } + + /** + * Platform specification to put into the "version made + * by" part of the central file header. + * + * @return PLATFORM_FAT unless {@link #setUnixMode setUnixMode} + * has been called, in which case PLATORM_UNIX will be returned. + * @since Ant 1.5.2 + */ + public int getPlatform() { + return platform; + } + + /** + * Set the platform (UNIX or FAT). + * + * @param platform an int value - 0 is FAT, 3 is UNIX + * @since 1.9 + */ + protected void setPlatform(int platform) { + this.platform = platform; + } + + /** + * Replaces all currently attached extra fields with the new array. + * + * @param fields an array of extra fields + * @since 1.1 + */ + public void setExtraFields(ZipExtraField[] fields) { + extraFields = new LinkedHashMap<>(); + for (ZipExtraField field : fields) { + if (field instanceof UnparseableExtraFieldData) { + unparseableExtra = (UnparseableExtraFieldData) field; + } else { + extraFields.put(field.getHeaderId(), field); + } + } + setExtra(); + } + + /** + * Retrieves all extra fields that have been parsed successfully. + * + * @return an array of the extra fields + */ + public ZipExtraField[] getExtraFields() { + return getExtraFields(false); + } + + /** + * Retrieves extra fields. + * + * @param includeUnparseable whether to also return unparseable + * extra fields as {@link UnparseableExtraFieldData} if such data + * exists. + * @return an array of the extra fields + * @since 1.1 + */ + public ZipExtraField[] getExtraFields(boolean includeUnparseable) { + if (extraFields == null) { + return !includeUnparseable || unparseableExtra == null + ? new ZipExtraField[0] + : new ZipExtraField[]{unparseableExtra}; + } + List result = new ArrayList<>(extraFields.values()); + if (includeUnparseable && unparseableExtra != null) { + result.add(unparseableExtra); + } + return result.toArray(new ZipExtraField[0]); + } + + /** + * Adds an extra field - replacing an already present extra field + * of the same type. + *

+ *

If no extra field of the same type exists, the field will be + * added as last field.

+ * + * @param ze an extra field + * @since 1.1 + */ + public void addExtraField(ZipExtraField ze) { + if (ze instanceof UnparseableExtraFieldData) { + unparseableExtra = (UnparseableExtraFieldData) ze; + } else { + if (extraFields == null) { + extraFields = new LinkedHashMap<>(); + } + extraFields.put(ze.getHeaderId(), ze); + } + setExtra(); + } + + /** + * Adds an extra field - replacing an already present extra field + * of the same type. + *

+ *

The new extra field will be the first one.

+ * + * @param ze an extra field + * @since 1.1 + */ + public void addAsFirstExtraField(ZipExtraField ze) { + if (ze instanceof UnparseableExtraFieldData) { + unparseableExtra = (UnparseableExtraFieldData) ze; + } else { + LinkedHashMap copy = extraFields; + extraFields = new LinkedHashMap<>(); + extraFields.put(ze.getHeaderId(), ze); + if (copy != null) { + copy.remove(ze.getHeaderId()); + extraFields.putAll(copy); + } + } + setExtra(); + } + + /** + * Remove an extra field. + * + * @param type the type of extra field to remove + * @since 1.1 + */ + public void removeExtraField(ZipShort type) { + if (extraFields == null) { + throw new java.util.NoSuchElementException(); + } + if (extraFields.remove(type) == null) { + throw new java.util.NoSuchElementException(); + } + setExtra(); + } + + /** + * Removes unparseable extra field data. + */ + public void removeUnparseableExtraFieldData() { + if (unparseableExtra == null) { + throw new java.util.NoSuchElementException(); + } + unparseableExtra = null; + setExtra(); + } + + /** + * Looks up an extra field by its header id. + * + * @return null if no such field exists. + */ + public ZipExtraField getExtraField(ZipShort type) { + if (extraFields != null) { + return extraFields.get(type); + } + return null; + } + + /** + * Looks up extra field data that couldn't be parsed correctly. + * + * @return null if no such field exists. + */ + public UnparseableExtraFieldData getUnparseableExtraFieldData() { + return unparseableExtra; + } + + /** + * Parses the given bytes as extra field data and consumes any + * unparseable data as an {@link UnparseableExtraFieldData} + * instance. + * + * @param extra an array of bytes to be parsed into extra fields + * @throws RuntimeException if the bytes cannot be parsed + * @throws RuntimeException on error + * @since 1.1 + */ + public void setExtra(byte[] extra) throws RuntimeException { + try { + ZipExtraField[] local = + ExtraFieldUtils.parse(extra, true, + ExtraFieldUtils.UnparseableExtraField.READ); + mergeExtraFields(local, true); + } catch (Exception e) { + // actually this is not be possible as of Ant 1.8.1 + throw new RuntimeException("Error parsing extra fields for entry: " + + getName() + " - " + e.getMessage(), e); + } + } + + /** + * Unfortunately {@link java.util.zip.ZipOutputStream + * java.util.zip.ZipOutputStream} seems to access the extra data + * directly, so overriding getExtra doesn't help - we need to + * modify super's data directly. + * + * @since 1.1 + */ + protected void setExtra() { + super.setExtra(ExtraFieldUtils.mergeLocalFileDataData(getExtraFields(true))); + } + + /** + * Sets the central directory part of extra fields. + */ + public void setCentralDirectoryExtra(byte[] b) { + try { + ZipExtraField[] central = + ExtraFieldUtils.parse(b, false, + ExtraFieldUtils.UnparseableExtraField.READ); + mergeExtraFields(central, false); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * Retrieves the extra data for the local file data. + * + * @return the extra data for local file + * @since 1.1 + */ + public byte[] getLocalFileDataExtra() { + byte[] extra = getExtra(); + return extra != null ? extra : new byte[0]; + } + + public Date getLastModifiedDate() { + return new Date(getTime()); + } + + /** + * Retrieves the extra data for the central directory. + * + * @return the central directory extra data + * @since 1.1 + */ + public byte[] getCentralDirectoryExtra() { + return ExtraFieldUtils.mergeCentralDirectoryData(getExtraFields(true)); + } + + /** + * Make this class work in JDK 1.1 like a 1.2 class. + *

+ *

This either stores the size for later usage or invokes + * setCompressedSize via reflection.

+ * + * @param size the size to use + * @since 1.2 + * Use setCompressedSize directly. + */ + public void setComprSize(long size) { + setCompressedSize(size); + } + + /** + * Get the name of the entry. + * + * @return the entry name + * @since 1.9 + */ + public String getName() { + return name == null ? super.getName() : name; + } + + public String getParent() { + return parent; + } + + public String getSimpleName() { + return simpleName; + } + + /** + * Is this entry a directory? + * + * @return true if the entry is a directory + * @since 1.10 + */ + public boolean isDirectory() { + return getName().endsWith("/"); + } + + /** + * Set the name of the entry. + * + * @param name the name to use + */ + public void setName(String name) { + if (name != null && getPlatform() == PLATFORM_FAT + && !name.contains("/")) { + name = name.replace('\\', '/'); + } + this.name = name; + setParentAndSimpleName(); + } + + private void setParentAndSimpleName() { + if (name == null || name.length() == 0) + return; + if (name.charAt(name.length() - 1) == '/') { + int index = name.lastIndexOf('/', name.length() - 2); + if (index == -1) { + simpleName = name.substring(0, name.length() - 1); + parent = null; + } else { + simpleName = name.substring(index + 1, name.length() - 1); + parent = name.substring(0, index + 1); + } + } else { + int index = name.lastIndexOf('/'); + if (index == -1) { + simpleName = name; + parent = null; + } else { + simpleName = name.substring(index + 1); + parent = name.substring(0, index + 1); + } + } + } + + private int hash = 0; + /** + * Get the hashCode of the entry. + * This uses the name as the hashcode. + * + * @return a hashcode. + * @since Ant 1.7 + */ + public int hashCode() { + // this method has severe consequences on performance. We cannot rely + // on the super.hashCode() method since super.getName() always return + // the empty string in the current implemention (there's no setter) + // so it is basically draining the performance of a hashmap lookup + int h = hash; + if (h == 0) { + h = getName().hashCode(); + hash = h; + } + return h; + } + + /** + * The equality method. In this case, the implementation returns 'this == o' + * which is basically the equals method of the Object class. + * + * @param obj the object to compare to + * @return true if this object is the same as o + * @since Ant 1.7 + */ + public boolean equals(Object obj) { + return this == obj; + } + + /** + * If there are no extra fields, use the given fields as new extra + * data - otherwise merge the fields assuming the existing fields + * and the new fields stem from different locations inside the + * archive. + * + * @param f the extra fields to merge + * @param local whether the new fields originate from local data + */ + private void mergeExtraFields(ZipExtraField[] f, boolean local) + throws ZipException { + if (extraFields == null) { + setExtraFields(f); + } else { + for (ZipExtraField aF : f) { + ZipExtraField existing; + if (aF instanceof UnparseableExtraFieldData) { + existing = unparseableExtra; + } else { + existing = getExtraField(aF.getHeaderId()); + } + if (existing == null) { + addExtraField(aF); + } else { + if (local + || !(existing + instanceof CentralDirectoryParsingZipExtraField)) { + byte[] b = aF.getLocalFileDataData(); + existing.parseFromLocalFileData(b, 0, b.length); + } else { + byte[] b = aF.getCentralDirectoryData(); + ((CentralDirectoryParsingZipExtraField) existing) + .parseFromCentralDirectoryData(b, 0, b.length); + } + } + } + setExtra(); + } + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipFile.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipFile.java new file mode 100644 index 0000000..f588077 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipFile.java @@ -0,0 +1,747 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip; + + +import top.niunaijun.blackobfuscator.core.utils.zip.encoding.ZipEncoding; +import top.niunaijun.blackobfuscator.core.utils.zip.encoding.ZipEncodingHelper; +import top.niunaijun.blackobfuscator.core.utils.zip.extrafield.AbstractUnicodeExtraField; +import top.niunaijun.blackobfuscator.core.utils.zip.extrafield.UnicodeCommentExtraField; +import top.niunaijun.blackobfuscator.core.utils.zip.extrafield.UnicodePathExtraField; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.zip.CRC32; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.util.zip.ZipException; + +/** + * Replacement for java.util.ZipFile. + *

+ *

This class adds support for file name encodings other than UTF-8 + * (which is required to work on ZIP files created by native zip tools + * and is able to skip a preamble like the one found in self + * extracting archives. Furthermore it returns instances of + * org.apache.tools.zip.ZipEntry instead of + * java.util.zip.ZipEntry.

+ *

+ *

It doesn't extend java.util.zip.ZipFile as it would + * have to reimplement all methods anyway. Like + * java.util.ZipFile, it uses RandomAccessFile under the + * covers and supports compressed and uncompressed entries.

+ *

+ *

The method signatures mimic the ones of + * java.util.zip.ZipFile, with a couple of exceptions: + *

+ *

    + *
  • There is no getName method.
  • + *
  • entries has been renamed to getEntries.
  • + *
  • getEntries and getEntry return + * org.apache.tools.zip.ZipEntry instances.
  • + *
  • close is allowed to throw IOException.
  • + *
+ */ +public class ZipFile implements Closeable { + private static final int HASH_SIZE = 509; + private static final int SHORT = 2; + private static final int WORD = 4; + private static final int NIBLET_MASK = 0x0f; + private static final int BYTE_SHIFT = 8; + private static final int POS_0 = 0; + private static final int POS_1 = 1; + private static final int POS_2 = 2; + private static final int POS_3 = 3; + + /** + * Maps ZipEntrys to Longs, recording the offsets of the local + * file headers. + */ + private final Map entries = new HashMap<>(HASH_SIZE); + + /** + * Maps String to ZipEntrys, name -> actual entry. + */ + private final Map nameMap = new HashMap<>(HASH_SIZE); + + private static final class OffsetEntry { + private long headerOffset = -1; + private long dataOffset = -1; + } + + /** + * The zip encoding to use for filenames and the file comment. + */ + private final ZipEncoding zipEncoding; + + /** + * The actual data source. + */ + private final RandomAccessFile archive; + + /** + * Whether to look for and use Unicode extra fields. + */ + private final boolean useUnicodeExtraFields; + + /** + * Opens the given file for reading, assuming the platform's + * native encoding for file names. + * + * @param f the archive. + * @throws IOException if an error occurs while reading the file. + */ + public ZipFile(File f) throws IOException { + this(f, null); + } + + /** + * Opens the given file for reading, assuming the platform's + * native encoding for file names. + * + * @param name name of the archive. + * @throws IOException if an error occurs while reading the file. + */ + public ZipFile(String name) throws IOException { + this(new File(name), null); + } + + /** + * Opens the given file for reading, assuming the specified + * encoding for file names, scanning unicode extra fields. + * + * @param name name of the archive. + * @param encoding the encoding to use for file names + * @throws IOException if an error occurs while reading the file. + */ + public ZipFile(String name, String encoding) throws IOException { + this(new File(name), encoding, true); + } + + /** + * Opens the given file for reading, assuming the specified + * encoding for file names and scanning for unicode extra fields. + * + * @param f the archive. + * @param encoding the encoding to use for file names, use null + * for the platform's default encoding + * @throws IOException if an error occurs while reading the file. + */ + public ZipFile(File f, String encoding) throws IOException { + this(f, encoding, true); + } + + /** + * Opens the given file for reading, assuming the specified + * encoding for file names. + * + * @param f the archive. + * @param encoding the encoding to use for file names, use null + * for the platform's default encoding + * @param useUnicodeExtraFields whether to use InfoZIP Unicode + * Extra Fields (if present) to set the file names. + * @throws IOException if an error occurs while reading the file. + */ + public ZipFile(File f, String encoding, boolean useUnicodeExtraFields) + throws IOException { + this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); + this.useUnicodeExtraFields = useUnicodeExtraFields; + archive = new RandomAccessFile(f, "r"); + boolean success = false; + try { + Map entriesWithoutUTF8Flag = populateFromCentralDirectory(); + resolveLocalFileHeaderData(entriesWithoutUTF8Flag); + success = true; + } finally { + if (!success) { + try { + archive.close(); + } catch (IOException e2) { + // swallow, throw the original exception instead + } + } + } + } + + public ZipEncoding getZipEncoding() { + return zipEncoding; + } + + public int getEntrySize() { + return entries.size(); + } + + + /** + * The encoding to use for filenames and the file comment. + * + * @return null if using the platform's default character encoding. + */ + public String getEncoding() { + return zipEncoding.getEncoding(); + } + + /** + * Closes the archive. + * + * @throws IOException if an error occurs closing the archive. + */ + @Override + public void close() throws IOException { + archive.close(); + } + + /** + * close a zipfile quietly; throw no io fault, do nothing + * on a null parameter + * + * @param zipfile file to close, can be null + */ + public static void closeQuietly(ZipFile zipfile) { + if (zipfile != null) { + try { + zipfile.close(); + } catch (IOException e) { + //ignore + } + } + } + + /** + * Returns all entries. + * + * @return all entries as {@link ZipEntry} instances + */ + public Enumeration getEntries() { + return Collections.enumeration(entries.keySet()); + } + + /** + * Returns a named entry - or null if no entry by + * that name exists. + * + * @param name name of the entry. + * @return the ZipEntry corresponding to the given name - or + * null if not present. + */ + public ZipEntry getEntry(String name) { + return nameMap.get(name); + } + + /** + * Returns an InputStream for reading the contents of the given entry. + * + * @param ze the entry to get the stream for. + * @return a stream to read the entry from. + * @throws IOException if unable to getInstance an input stream from the zipenty + * @throws ZipException if the zipentry has an unsupported + * compression method + */ + public InputStream getInputStream(ZipEntry ze) + throws IOException { + OffsetEntry offsetEntry = entries.get(ze); + if (offsetEntry == null) { + return null; + } + long start = offsetEntry.dataOffset; + BoundedInputStream bis = + new BoundedInputStream(start, ze.getCompressedSize()); + switch (ze.getMethod()) { + case ZipEntry.STORED: + return bis; + case ZipEntry.DEFLATED: + bis.addDummy(); + final Inflater inflater = new Inflater(true); + return new InflaterInputStream(bis, inflater) { + public void close() throws IOException { + super.close(); + inflater.end(); + } + }; + default: + throw new ZipException("Found unsupported compression method " + + ze.getMethod()); + } + } + + public InputStream getRawInputStream(ZipEntry ze) + throws IOException { + OffsetEntry offsetEntry = entries.get(ze); + if (offsetEntry == null) + return null; + return new BoundedInputStream(offsetEntry.dataOffset, ze.getCompressedSize()); + } + + private static final int CFH_LEN = + /* version made by */ SHORT + /* version needed to extract */ + SHORT + /* general purpose bit flag */ + SHORT + /* compression method */ + SHORT + /* last mod file time */ + SHORT + /* last mod file date */ + SHORT + /* crc-32 */ + WORD + /* compressed size */ + WORD + /* uncompressed size */ + WORD + /* filename length */ + SHORT + /* extra field length */ + SHORT + /* file comment length */ + SHORT + /* disk number start */ + SHORT + /* internal file attributes */ + SHORT + /* external file attributes */ + WORD + /* relative offset of local header */ + WORD; + + /** + * Reads the central directory of the given archive and populates + * the internal tables with ZipEntry instances. + *

+ *

The ZipEntrys will know all data that can be obtained from + * the central directory alone, but not the data that requires the + * local file header or additional data to be read.

+ * + * @return a Map<ZipEntry, NameAndComment>> of + * zipentries that didn't have the language encoding flag set when + * read. + */ + private Map populateFromCentralDirectory() + throws IOException { + HashMap noUTF8Flag = new HashMap<>(); + + positionAtCentralDirectory(); + + byte[] cfh = new byte[CFH_LEN]; + + byte[] signatureBytes = new byte[WORD]; + archive.readFully(signatureBytes); + long sig = ZipLong.getValue(signatureBytes); + final long cfhSig = ZipLong.getValue(ZipOutputStream.CFH_SIG); + if (sig != cfhSig && startsWithLocalFileHeader()) { + throw new IOException("central directory is empty, can't expand" + + " corrupt archive."); + } + while (sig == cfhSig) { + archive.readFully(cfh); + int off = 0; + ZipEntry ze = new ZipEntry(); + + int versionMadeBy = ZipShort.getValue(cfh, off); + off += SHORT; + ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK); + + off += SHORT; // skip version info + + final int generalPurposeFlag = ZipShort.getValue(cfh, off); + final boolean hasUTF8Flag = + (generalPurposeFlag & ZipOutputStream.UFT8_NAMES_FLAG) != 0; + final ZipEncoding entryEncoding = + hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; + + off += SHORT; + + //noinspection MagicConstant + ze.setMethod(ZipShort.getValue(cfh, off)); + off += SHORT; + + // FIXME this is actually not very cpu cycles friendly as we are converting from + // dos to java while the underlying Sun implementation will convert + // from java to dos time for internal storage... + long time = dosToJavaTime(ZipLong.getValue(cfh, off)); + ze.setTime(time); + off += WORD; + + ze.setCrc(ZipLong.getValue(cfh, off)); + off += WORD; + + ze.setCompressedSize(ZipLong.getValue(cfh, off)); + off += WORD; + + ze.setSize(ZipLong.getValue(cfh, off)); + off += WORD; + + int fileNameLen = ZipShort.getValue(cfh, off); + off += SHORT; + + int extraLen = ZipShort.getValue(cfh, off); + off += SHORT; + + int commentLen = ZipShort.getValue(cfh, off); + off += SHORT; + + off += SHORT; // disk number + + ze.setInternalAttributes(ZipShort.getValue(cfh, off)); + off += SHORT; + + ze.setExternalAttributes(ZipLong.getValue(cfh, off)); + off += WORD; + + byte[] fileName = new byte[fileNameLen]; + archive.readFully(fileName); + ze.setName(entryEncoding.decode(fileName)); + + // LFH offset, + OffsetEntry offset = new OffsetEntry(); + offset.headerOffset = ZipLong.getValue(cfh, off); + // data offset will be filled later + entries.put(ze, offset); + + nameMap.put(ze.getName(), ze); + + byte[] cdExtraData = new byte[extraLen]; + archive.readFully(cdExtraData); + ze.setCentralDirectoryExtra(cdExtraData); + + byte[] comment = new byte[commentLen]; + archive.readFully(comment); + ze.setComment(entryEncoding.decode(comment)); + + archive.readFully(signatureBytes); + sig = ZipLong.getValue(signatureBytes); + + if (!hasUTF8Flag && useUnicodeExtraFields) { + noUTF8Flag.put(ze, new NameAndComment(fileName, comment)); + } + } + return noUTF8Flag; + } + + private static final int MIN_EOCD_SIZE = + /* end of central dir signature */ WORD + /* number of this disk */ + SHORT + /* number of the disk with the */ + /* start of the central directory */ + SHORT + /* total number of entries in */ + /* the central dir on this disk */ + SHORT + /* total number of entries in */ + /* the central dir */ + SHORT + /* size of the central directory */ + WORD + /* offset of start of central */ + /* directory with respect to */ + /* the starting disk number */ + WORD + /* zipfile comment length */ + SHORT; + + private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE + /* maximum length of zipfile comment */ + 0xFFFF; + + private static final int CFD_LOCATOR_OFFSET = + /* end of central dir signature */ WORD + /* number of this disk */ + SHORT + /* number of the disk with the */ + /* start of the central directory */ + SHORT + /* total number of entries in */ + /* the central dir on this disk */ + SHORT + /* total number of entries in */ + /* the central dir */ + SHORT + /* size of the central directory */ + WORD; + + /** + * Searches for the "End of central dir record", parses + * it and positions the stream at the first central directory + * record. + */ + private void positionAtCentralDirectory() + throws IOException { + boolean found = false; + long off = archive.length() - MIN_EOCD_SIZE; + final long stopSearching = + Math.max(0L, archive.length() - MAX_EOCD_SIZE); + if (off >= 0) { + final byte[] sig = ZipOutputStream.EOCD_SIG; + for (; off >= stopSearching; off--) { + archive.seek(off); + int curr = archive.read(); + if (curr == -1) { + break; + } + if (curr == sig[POS_0]) { + curr = archive.read(); + if (curr == sig[POS_1]) { + curr = archive.read(); + if (curr == sig[POS_2]) { + curr = archive.read(); + if (curr == sig[POS_3]) { + found = true; + break; + } + } + } + } + } + } + if (!found) { + throw new ZipException("archive is not a ZIP archive"); + } + archive.seek(off + CFD_LOCATOR_OFFSET); + byte[] cfdOffset = new byte[WORD]; + archive.readFully(cfdOffset); + archive.seek(ZipLong.getValue(cfdOffset)); + } + + /** + * Number of bytes in local file header up to the "length of + * filename" entry. + */ + private static final long LFH_OFFSET_FOR_FILENAME_LENGTH = + /* local file header signature */ WORD + /* version needed to extract */ + SHORT + /* general purpose bit flag */ + SHORT + /* compression method */ + SHORT + /* last mod file time */ + SHORT + /* last mod file date */ + SHORT + /* crc-32 */ + WORD + /* compressed size */ + WORD + /* uncompressed size */ + WORD; + + /** + * Walks through all recorded entries and adds the data available + * from the local file header. + *

+ *

Also records the offsets for the data to read from the + * entries.

+ */ + private void resolveLocalFileHeaderData(Map entriesWithoutUTF8Flag) + throws IOException { + Enumeration e = Collections.enumeration(new HashSet<>(entries.keySet())); + while (e.hasMoreElements()) { + ZipEntry ze = e.nextElement(); + OffsetEntry offsetEntry = entries.get(ze); + long offset = offsetEntry.headerOffset; + archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH); + byte[] b = new byte[SHORT]; + archive.readFully(b); + int fileNameLen = ZipShort.getValue(b); + archive.readFully(b); + int extraFieldLen = ZipShort.getValue(b); + int lenToSkip = fileNameLen; + while (lenToSkip > 0) { + int skipped = archive.skipBytes(lenToSkip); + if (skipped <= 0) { + throw new RuntimeException("failed to skip file name in" + + " local file header"); + } + lenToSkip -= skipped; + } + byte[] localExtraData = new byte[extraFieldLen]; + archive.readFully(localExtraData); + ze.setExtra(localExtraData); + /*dataOffsets.put(ze, + new Long(offset + LFH_OFFSET_FOR_FILENAME_LENGTH + + SHORT + SHORT + fileNameLen + extraFieldLen)); + */ + offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH + + SHORT + SHORT + fileNameLen + extraFieldLen; + + if (entriesWithoutUTF8Flag.containsKey(ze)) { + // changing the name of a ZipEntry is going to change + // the hashcode + // - see https://issues.apache.org/jira/browse/COMPRESS-164 + entries.remove(ze); + setNameAndCommentFromExtraFields(ze, entriesWithoutUTF8Flag.get(ze)); + entries.put(ze, offsetEntry); + } + } + } + + /** + * Convert a DOS date/time field to a Date object. + * + * @param zipDosTime contains the stored DOS time. + * @return a Date instance corresponding to the given time. + */ + protected static Date fromDosTime(ZipLong zipDosTime) { + long dosTime = zipDosTime.getValue(); + return new Date(dosToJavaTime(dosTime)); + } + + /* + * Converts DOS time to Java time (number of milliseconds since epoch). + */ + private static long dosToJavaTime(long dosTime) { + Calendar cal = Calendar.getInstance(); + // CheckStyle:MagicNumberCheck OFF - no point + cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980); + cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1); + cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f); + cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f); + cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f); + cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e); + // CheckStyle:MagicNumberCheck ON + return cal.getTime().getTime(); + } + + /** + * Checks whether the archive starts with a LFH. If it doesn't, + * it may be an empty archive. + */ + private boolean startsWithLocalFileHeader() throws IOException { + archive.seek(0); + final byte[] start = new byte[WORD]; + archive.readFully(start); + for (int i = 0; i < start.length; i++) { + if (start[i] != ZipOutputStream.LFH_SIG[i]) { + return false; + } + } + return true; + } + + /** + * If the entry has Unicode*ExtraFields and the CRCs of the + * names/comments match those of the extra fields, transfer the + * known Unicode values from the extra field. + */ + private void setNameAndCommentFromExtraFields(ZipEntry ze, + NameAndComment nc) { + UnicodePathExtraField name = (UnicodePathExtraField) + ze.getExtraField(UnicodePathExtraField.UPATH_ID); + String originalName = ze.getName(); + String newName = getUnicodeStringIfOriginalMatches(name, nc.name); + if (newName != null && !originalName.equals(newName)) { + ze.setName(newName); + nameMap.remove(originalName); + nameMap.put(newName, ze); + } + + if (nc.comment != null && nc.comment.length > 0) { + UnicodeCommentExtraField cmt = (UnicodeCommentExtraField) + ze.getExtraField(UnicodeCommentExtraField.UCOM_ID); + String newComment = + getUnicodeStringIfOriginalMatches(cmt, nc.comment); + if (newComment != null) { + ze.setComment(newComment); + } + } + } + + /** + * If the stored CRC matches the one of the given name, return the + * Unicode name of the given field. + *

+ *

If the field is null or the CRCs don't match, return null + * instead.

+ */ + private String getUnicodeStringIfOriginalMatches(AbstractUnicodeExtraField f, + byte[] orig) { + if (f != null) { + CRC32 crc32 = new CRC32(); + crc32.update(orig); + long origCRC32 = crc32.getValue(); + + if (origCRC32 == f.getNameCRC32()) { + try { + return ZipEncodingHelper + .UTF8_ZIP_ENCODING.decode(f.getUnicodeName()); + } catch (IOException ex) { + // UTF-8 unsupported? should be impossible the + // Unicode*ExtraField must contain some bad bytes + + // TODO log this anywhere? + return null; + } + } + } + return null; + } + + /** + * InputStream that delegates requests to the underlying + * RandomAccessFile, making sure that only bytes from a certain + * range can be read. + */ + private class BoundedInputStream extends InputStream { + private long remaining; + private long loc; + private boolean addDummyByte = false; + + BoundedInputStream(long start, long remaining) { + this.remaining = remaining; + loc = start; + } + + public int read() throws IOException { + if (remaining-- <= 0) { + if (addDummyByte) { + addDummyByte = false; + return 0; + } + return -1; + } + synchronized (archive) { + archive.seek(loc++); + return archive.read(); + } + } + + public int read(byte[] b, int off, int len) throws IOException { + if (remaining <= 0) { + if (addDummyByte) { + addDummyByte = false; + b[off] = 0; + return 1; + } + return -1; + } + + if (len <= 0) { + return 0; + } + + if (len > remaining) { + len = (int) remaining; + } + int ret; + synchronized (archive) { + archive.seek(loc); + ret = archive.read(b, off, len); + } + if (ret > 0) { + loc += ret; + remaining -= ret; + } + return ret; + } + + /** + * Inflater needs an extra dummy byte for nowrap - see + * Inflater's javadocs. + */ + void addDummy() { + addDummyByte = true; + } + } + + private static final class NameAndComment { + private final byte[] name; + private final byte[] comment; + + private NameAndComment(byte[] name, byte[] comment) { + this.name = name; + this.comment = comment; + } + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipItem.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipItem.java new file mode 100644 index 0000000..b51bd9e --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipItem.java @@ -0,0 +1,40 @@ +package top.niunaijun.blackobfuscator.core.utils.zip; + +import java.io.InputStream; + +/** + * Created by sunwanquan on 2019/6/18. + * * ∧_∧ + * (`・ω・∥ + * 丶 つ0 + * しーJ + * 此处无Bug + */ +public class ZipItem { + private InputStream stream; + private String zipEntryName; + + public ZipItem() { + } + + public ZipItem(InputStream stream, String zipEntryName) { + this.stream = stream; + this.zipEntryName = zipEntryName; + } + + public InputStream getStream() { + return stream; + } + + public void setStream(InputStream stream) { + this.stream = stream; + } + + public String getZipEntryName() { + return zipEntryName; + } + + public void setZipEntryName(String zipEntryName) { + this.zipEntryName = zipEntryName; + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipLong.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipLong.java new file mode 100644 index 0000000..762c1e7 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipLong.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip; + +/** + * Utility class that represents a four byte integer with conversion + * rules for the big endian byte order of ZIP files. + */ +public final class ZipLong implements Cloneable { + + private static final int WORD = 4; + private static final int BYTE_MASK = 0xFF; + + private static final int BYTE_1 = 1; + private static final int BYTE_1_MASK = 0xFF00; + private static final int BYTE_1_SHIFT = 8; + + private static final int BYTE_2 = 2; + private static final int BYTE_2_MASK = 0xFF0000; + private static final int BYTE_2_SHIFT = 16; + + private static final int BYTE_3 = 3; + private static final long BYTE_3_MASK = 0xFF000000L; + private static final int BYTE_3_SHIFT = 24; + + private long value; + + /** + * Create instance from a number. + * + * @param value the long to store as a ZipLong + * @since 1.1 + */ + public ZipLong(long value) { + this.value = value; + } + + /** + * Create instance from bytes. + * + * @param bytes the bytes to store as a ZipLong + * @since 1.1 + */ + public ZipLong(byte[] bytes) { + this(bytes, 0); + } + + /** + * Create instance from the four bytes starting at offset. + * + * @param bytes the bytes to store as a ZipLong + * @param offset the offset to start + * @since 1.1 + */ + public ZipLong(byte[] bytes, int offset) { + value = ZipLong.getValue(bytes, offset); + } + + /** + * Get value as four bytes in big endian byte order. + * + * @return value as four bytes in big endian order + * @since 1.1 + */ + public byte[] getBytes() { + return ZipLong.getBytes(value); + } + + /** + * Get value as Java long. + * + * @return value as a long + * @since 1.1 + */ + public long getValue() { + return value; + } + + /** + * put the value as four bytes in big endian byte order. + * + * @param value the Java long to convert to bytes + * @param buf the output buffer + * @param offset The offset within the output buffer of the first byte to be written. + * must be non-negative and no larger than buf.length-4 + */ + public static void putLong(long value, byte[] buf, int offset) { + buf[offset++] = (byte) ((value & BYTE_MASK)); + buf[offset++] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT); + buf[offset++] = (byte) ((value & BYTE_2_MASK) >> BYTE_2_SHIFT); + buf[offset] = (byte) ((value & BYTE_3_MASK) >> BYTE_3_SHIFT); + } + + public void putLong(byte[] buf, int offset) { + putLong(value, buf, offset); + } + + /** + * Get value as four bytes in big endian byte order. + * + * @param value the value to convert + * @return value as four bytes in big endian byte order + */ + public static byte[] getBytes(long value) { + byte[] result = new byte[WORD]; + result[0] = (byte) ((value & BYTE_MASK)); + result[BYTE_1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT); + result[BYTE_2] = (byte) ((value & BYTE_2_MASK) >> BYTE_2_SHIFT); + result[BYTE_3] = (byte) ((value & BYTE_3_MASK) >> BYTE_3_SHIFT); + return result; + } + + /** + * Helper method to get the value as a Java long from four bytes starting at given array offset + * + * @param bytes the array of bytes + * @param offset the offset to start + * @return the correspondanding Java long value + */ + public static long getValue(byte[] bytes, int offset) { + long value = (bytes[offset + BYTE_3] << BYTE_3_SHIFT) & BYTE_3_MASK; + value += (bytes[offset + BYTE_2] << BYTE_2_SHIFT) & BYTE_2_MASK; + value += (bytes[offset + BYTE_1] << BYTE_1_SHIFT) & BYTE_1_MASK; + value += (bytes[offset] & BYTE_MASK); + return value; + } + + /** + * Helper method to get the value as a Java long from a four-byte array + * + * @param bytes the array of bytes + * @return the correspondanding Java long value + */ + public static long getValue(byte[] bytes) { + return getValue(bytes, 0); + } + + /** + * Override to make two instances with same value equal. + * + * @param o an object to compare + * @return true if the objects are equal + * @since 1.1 + */ + public boolean equals(Object o) { + if (o == null || !(o instanceof ZipLong)) { + return false; + } + return value == ((ZipLong) o).getValue(); + } + + /** + * Override to make two instances with same value equal. + * + * @return the value stored in the ZipLong + * @since 1.1 + */ + public int hashCode() { + return (int) value; + } + + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException cnfe) { + // impossible + throw new RuntimeException(cnfe); + } + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipManager.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipManager.java new file mode 100644 index 0000000..91b5cac --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipManager.java @@ -0,0 +1,352 @@ +package top.niunaijun.blackobfuscator.core.utils.zip; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; + +public class ZipManager { + public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yy-MM-dd HH:mm"); + private final File file; + public final ZipFile zipFile; + private final ZipEntry[] ze; + private ZipOutputStream zos; + public File tmp; + +// public static void main(String[] args) { +// test1(); +// } +// private static void extractTest() { +// try { +// ZipManager zipManager = new ZipManager(new File("C:\\b.zip")); +// zipManager.extract(new ExtractCallback() { +// +// @Override +// public File filter(ZipEntry zipEntry, int current, int total) { +// System.out.print("正在解压文件(" + current + "/" + total + ").." + zipEntry.getName() + "\n"); +// File file = new File("C:\\b\\" + zipEntry.getName()); +// if (!file.isDirectory() && file.exists()) +// //noinspection ResultOfMethodCallIgnored +// file.delete(); +// return file; +// } +// +// @Override +// public void onProgress(long current, long total) { +// System.out.print((100 * current / total) + "%\n"); +// } +// +// @Override +// public void done(ZipEntry zipEntry, File file) { +// +// } +// }); +// System.out.print("done\n"); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + +// private static void test2() { +// try { +// ZipManager zipManager = new ZipManager(new File("C:\\b.zip")); +// for (ZipEntry ze : zipManager.ze) { +// System.out.print(ze.getName() + " " + ze.getParent() + " " + ze.getSimpleName() + "\n"); +// } +// System.out.print("===================\n"); +// ArrayList al = zipManager.list("新建文件夹/"); +// for (ZipEntry ze : al) { +// System.out.print(ze.getName() + " " + getEntryTime(ze) + "\n"); +// } +// +// } catch (IOException e) { +// e.printStackTrace(); +// } +// +// } + +// private static void test1() { +// try { +// ZipFile zipFile = new ZipFile(new File("D:\\a\\a.zip")); +// System.out.print(zipFile.getEncoding() + "\n"); +// ZipOutputStream zos = new ZipOutputStream (new File("D:\\a\\b.zip")); +// zos.setLevel(ZipOutputStream.LEVEL_BEST); +// zos.setZipEncoding(zipFile.getZipEncoding()); +// +// Enumeration zes = zipFile.getEntries(); +// while(zes.hasMoreElements()){ +// ZipEntry zipEntry = zes.nextElement(); +// System.out.print(zipEntry.getName() + "\n"); +// +// if(zipEntry.getName().endsWith("/")) +// continue; +// +// if (zipEntry.getName().equalsIgnoreCase("a.txt")) +// zipEntry.setName("我爱蛋蛋.txt"); +// +// zos.copyZipEntry(zipEntry, zipFile); +// } +// zos.putNextEntry("a.txt"); +// zos.writeFully(new FileInputStream(new File("D:\\a\\a.txt"))); +// +// zos.putNextEntry("文件夹/"); +// +// zipFile.close(); +// zos.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + + public long getTotalCompressedSize() { + long size = 0; + for (ZipEntry zipEntry : ze) { + if (!zipEntry.isDirectory()) + size += zipEntry.getCompressedSize(); + } + return size; + } + + public long getTotalSize() { + long size = 0; + for (ZipEntry zipEntry : ze) { + if (!zipEntry.isDirectory()) + size += zipEntry.getSize(); + } + return size; + } + + public ZipEntry[] getZipEntries() { + return ze; + } + + public void zip(File file, long size, String entryName, ZipCallback callback) throws IOException { + if (size > 0) + callback.onProgress(0, size); + InputStream is = new FileInputStream(file); + ZipEntry zipEntry = new ZipEntry(entryName); + zipEntry.setTime(file.lastModified()); + zos.putNextEntry(zipEntry); + byte[] bytes = new byte[ZipOutputStream.BUFFER_SIZE]; + int len; + long current = 0; + while ((len = is.read(bytes)) > 0) { + zos.write(bytes, 0, len); + current += len; + if (size > 0) + callback.onProgress(current, size); + } + is.close(); + zos.closeEntry(); + } + + public HashMap getZipFileEntryMap() { + HashMap map = new HashMap<>(); + for (ZipEntry zipEntry : ze) { + if (!zipEntry.isDirectory()) + map.put(zipEntry.getName().toLowerCase(), zipEntry); + } + return map; + } + + public void copyEntries(CopyEntryCallback callback) throws IOException { + for (int i = 0; i < ze.length; i++) { + ZipEntry zipEntry = callback.filter(ze[i], i + 1, ze.length); + if (zipEntry == null) + continue; + if (zipEntry.isDirectory()) + zos.putNextEntry(zipEntry); + else { + InputStream rawInputStream = zipFile.getRawInputStream(zipEntry); + zos.putNextRawEntry(zipEntry); + int len; + final long total = zipEntry.getCompressedSize(); + long current = 0; + byte[] bytes = new byte[ZipOutputStream.BUFFER_SIZE]; + while ((len = rawInputStream.read(bytes)) > 0) { + zos.writeRaw(bytes, 0, len); + current += len; + callback.onProgress(current, total); + } + } + // rawInputStream.close()是个空方法,是否调用不影响 + zos.closeEntry(); + callback.done(zipEntry); + } + } + + public void copyOtherZipManagerEntries(ZipManager zipManager, CopyEntryCallback callback) throws IOException { + for (int i = 0; i < zipManager.ze.length; i++) { + ZipEntry zipEntry = callback.filter(zipManager.ze[i], i + 1, zipManager.ze.length); + if (zipEntry == null) + continue; + if (zipEntry.isDirectory()) + zos.putNextEntry(zipEntry); + else { + InputStream rawInputStream = zipManager.zipFile.getRawInputStream(zipManager.ze[i]); + zos.putNextRawEntry(zipEntry); + int len; + final long total = zipEntry.getCompressedSize(); + long current = 0; + byte[] bytes = new byte[ZipOutputStream.BUFFER_SIZE]; + while ((len = rawInputStream.read(bytes)) > 0) { + zos.writeRaw(bytes, 0, len); + current += len; + callback.onProgress(current, total); + } + // rawInputStream.close()是个空方法,是否调用不影响 + } + zos.closeEntry(); + callback.done(zipEntry); + } + } + + public void extractZipEntry(ZipEntry zipEntry, File file) throws IOException { + InputStream is = zipFile.getInputStream(zipEntry); + BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file)); + byte[] bytes = new byte[ZipOutputStream.BUFFER_SIZE]; + int len; + while ((len = is.read(bytes)) > 0) { + os.write(bytes, 0, len); + } + is.close(); + os.close(); + } + + public void extract(ExtractCallback callback) throws IOException { + for (int i = 0; i < ze.length; i++) { + ZipEntry zipEntry = ze[i]; + File file = callback.filter(zipEntry, i + 1, ze.length); + if (file == null) + continue; + if (zipEntry.isDirectory()) { + if (!file.exists() && !file.mkdirs()) + throw new IOException("mkdir \"" + file.getPath() + "\" failed"); + } else { + InputStream is = zipFile.getInputStream(zipEntry); + BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file)); + byte[] bytes = new byte[ZipOutputStream.BUFFER_SIZE]; + int len; + long total = zipEntry.getSize(); + long current = 0; + while ((len = is.read(bytes)) > 0) { + os.write(bytes, 0, len); + current += len; + if (total > 0) + callback.onProgress(current, total); + } + is.close(); + os.close(); + } + //noinspection ResultOfMethodCallIgnored + file.setLastModified(zipEntry.getTime()); + callback.done(zipEntry, file); + } + } + + public ZipOutputStream createTempZipOutputStream() throws IOException { + String s = ""; + do { + s += System.currentTimeMillis() % 1000000; + tmp = new File(file.getParentFile(), s + ".tmp"); + } while (tmp.exists()); + zos = new ZipOutputStream(tmp); + zos.setZipEncoding(zipFile.getZipEncoding()); + return zos; + } + + public ZipManager(File file) throws IOException { + zipFile = new ZipFile(this.file = file); + ArrayList al = new ArrayList<>(zipFile.getEntrySize()); + Enumeration entryEnumeration = zipFile.getEntries(); + HashMap map = new HashMap<>(); + while (entryEnumeration.hasMoreElements()) { + ZipEntry z = entryEnumeration.nextElement(); + if (!z.isDirectory()) + al.add(z); + else { + if (!map.containsKey(z.getName())) + map.put(z.getName(), z); + else { + //该文件夹已处理过父目录,无需再处理 + continue; + } + } + String parent = z.getParent(); + if (parent != null) { + long timeZ = z.getTime(); + if (!map.containsKey(parent)) { + // 父目录不存在,自动创建 + do { + ZipEntry dir = new ZipEntry(parent); + dir.setTime(timeZ); + map.put(parent, dir); + parent = dir.getParent(); + } while (parent != null && !map.containsKey(parent)); + } else { + // 父目录存在,更新日期 + do { + ZipEntry dir = map.get(parent); + if (dir.getTime() < timeZ) { + dir.setTime(timeZ); + parent = dir.getParent(); + } else + break; + } while (parent != null); + } + } + } + al.addAll(map.values()); + map.clear(); + Collections.sort(al, new Comparator() { + @Override + public int compare(ZipEntry o1, ZipEntry o2) { + return o1.getName().compareToIgnoreCase(o2.getName()); + } + }); + ze = new ZipEntry[al.size()]; + al.toArray(ze); + al.clear(); + } + + public static String getEntryTime(ZipEntry zipEntry) { + return DATE_FORMAT.format(zipEntry.getLastModifiedDate()); + } + + public int getEntrySize() { + return ze.length; + } + + public void close() throws IOException { + zipFile.close(); + } + + public ArrayList list(String path) { + final String p; + if (path == null || path.length() <= 1) + p = null; + else if (path.charAt(path.length() - 1) != '/') + p = path + "/"; + else + p = path; + final ArrayList l = new ArrayList<>(ze.length); + for (ZipEntry zipEntry : ze) { + String parent = zipEntry.getParent(); + if (parent == null) { + if (p == null) + l.add(zipEntry); + } else if (parent.equals(p)) + l.add(zipEntry); + } + l.trimToSize(); + return l; + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipOutputStream.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipOutputStream.java new file mode 100644 index 0000000..c95bb15 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipOutputStream.java @@ -0,0 +1,1236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip; + + +import top.niunaijun.blackobfuscator.core.utils.zip.encoding.ZipEncoding; +import top.niunaijun.blackobfuscator.core.utils.zip.encoding.ZipEncodingHelper; +import top.niunaijun.blackobfuscator.core.utils.zip.extrafield.UnicodeCommentExtraField; +import top.niunaijun.blackobfuscator.core.utils.zip.extrafield.UnicodePathExtraField; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.zip.CRC32; +import java.util.zip.Deflater; +import java.util.zip.ZipException; + +import static top.niunaijun.blackobfuscator.core.utils.zip.ZipLong.putLong; +import static top.niunaijun.blackobfuscator.core.utils.zip.ZipShort.putShort; +import static top.niunaijun.blackobfuscator.core.utils.zip.encoding.ZipEncodingHelper.UTF8_ZIP_ENCODING; + +/** + * Reimplementation of {@link java.util.zip.ZipOutputStream + * java.util.zip.ZipOutputStream} that does handle the extended + * functionality of this package, especially internal/external file + * attributes and extra fields with different layouts for local file + * data and central directory entries. + *

+ *

This class will try to use {@link RandomAccessFile + * RandomAccessFile} when you know that the output is going to go to a + * file.

+ *

+ *

If RandomAccessFile cannot be used, this implementation will use + * a Data Descriptor to store size and CRC information for {@link + * #DEFLATED DEFLATED} entries, this means, you don't need to + * calculate them yourself. Unfortunately this is not possible for + * the {@link #STORED STORED} method, here setting the CRC and + * uncompressed size information is required before {@link + * #putNextEntry putNextEntry} can be called.

+ */ +public class ZipOutputStream extends FilterOutputStream { + public static final int LEVEL_BEST = Deflater.BEST_COMPRESSION; + public static final int LEVEL_BETTER = 7; + public static final int LEVEL_DEFAULT = Deflater.DEFAULT_COMPRESSION; + public static final int LEVEL_FASTER = 3; + public static final int LEVEL_FASTEST = Deflater.BEST_SPEED; + + private static final int BYTE_MASK = 0xFF; + private static final int SHORT = 2; + private static final int WORD = 4; + public static final int BUFFER_SIZE = 10240; + /* + * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs + * when it gets handed a really big buffer. See + * https://issues.apache.org/bugzilla/show_bug.cgi?id=45396 + * + * Using a buffer size of 8 kB proved to be a good compromise + */ + private static final int DEFLATER_BLOCK_SIZE = 8192; + + /** + * Compression method for deflated entries. + * + * @since 1.1 + */ + public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED; + + /** + * Default compression level for deflated entries. + * + * @since Ant 1.7 + */ + public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION; + + /** + * Compression method for stored entries. + * + * @since 1.1 + */ + public static final int STORED = java.util.zip.ZipEntry.STORED; + + /** + * default encoding for file names and comment. + */ + static final String DEFAULT_ENCODING = null; + + /** + * General purpose flag, which indicates that filenames are + * written in utf-8. + */ + public static final int UFT8_NAMES_FLAG = 1 << 11; + + /** + * General purpose flag, which indicates that filenames are + * written in utf-8. + * + */ + public static final int EFS_FLAG = UFT8_NAMES_FLAG; + + /** + * Current entry. + * + * @since 1.1 + */ + private ZipEntry entry; + + /** + * The file comment. + * + * @since 1.1 + */ + private String comment = ""; + + /** + * Compression level for next entry. + * + * @since 1.1 + */ + private int level = DEFAULT_COMPRESSION; + + /** + * Has the compression level changed when compared to the last + * entry? + * + * @since 1.5 + */ + private boolean hasCompressionLevelChanged = false; + + /** + * Default compression method for next entry. + * + * @since 1.1 + */ + private int method = java.util.zip.ZipEntry.DEFLATED; + + /** + * List of ZipEntries written so far. + * + * @since 1.1 + */ + private final List entries = new LinkedList<>(); + + /** + * CRC instance to avoid parsing DEFLATED data twice. + * + * @since 1.1 + */ + private final CRC32 crc = new CRC32(); + + /** + * Count the bytes written to out. + * + * @since 1.1 + */ + private long written = 0; + + /** + * Data for local header data + * + * @since 1.1 + */ + private long dataStart = 0; + + /** + * Offset for CRC entry in the local file header data for the + * current entry starts here. + * + * @since 1.15 + */ + private long localDataStart = 0; + + /** + * Start of central directory. + * + * @since 1.1 + */ + private long cdOffset = 0; + + /** + * Length of central directory. + * + * @since 1.1 + */ + private long cdLength = 0; + + /** + * Helper, a 0 as ZipShort. + * + * @since 1.1 + */ + private static final byte[] ZERO = {0, 0}; + + /** + * Helper, a 0 as ZipLong. + * + * @since 1.1 + */ + private static final byte[] LZERO = {0, 0, 0, 0}; + + /** + * Holds the offsets of the LFH starts for each entry. + * + * @since 1.1 + */ + private final Map offsets = new HashMap<>(); + + /** + * The encoding to use for filenames and the file comment. + *

+ *

For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. + * Defaults to the platform's default character encoding.

+ * + * @since 1.3 + */ + private String encoding = null; + + /** + * The zip encoding to use for filenames and the file comment. + *

+ * This field is of internal use and will be set in {@link + * #setEncoding(String)}. + */ + private ZipEncoding zipEncoding = UTF8_ZIP_ENCODING; + + // CheckStyle:VisibilityModifier OFF - bc + + /** + * This Deflater object is used for output. + *

+ *

This attribute is only protected to provide a level of API + * backwards compatibility. This class used to extend {@link + * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to + * Revision 1.13.

+ * + * @since 1.14 + */ + protected Deflater def = new Deflater(level, true); + + /** + * This buffer servers as a Deflater. + *

+ *

This attribute is only protected to provide a level of API + * backwards compatibility. This class used to extend {@link + * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to + * Revision 1.13.

+ * + * @since 1.14 + */ + protected byte[] buf = new byte[512]; + + // CheckStyle:VisibilityModifier ON + + /** + * Optional random access output. + * + * @since 1.14 + */ + private RandomAccessFile raf = null; + + /** + * whether to use the general purpose bit flag when writing UTF-8 + * filenames or not. + */ + private boolean useUTF8Flag = true; + + /** + * Whether to encode non-encodable file names as UTF-8. + */ + private boolean fallbackToUTF8 = false; + + /** + * whether to getInstance UnicodePathExtraField-s for each entry. + */ + private UnicodeExtraFieldPolicy createUnicodeExtraFields = + UnicodeExtraFieldPolicy.NEVER; + + /** + * Creates a new ZIP OutputStream filtering the underlying stream. + * + * @param out the outputstream to zip + * @since 1.1 + */ + public ZipOutputStream(OutputStream out) { + super(out); + } + + /** + * Creates a new ZIP OutputStream writing to a File. Will use + * random access if possible. + * + * @param file the file to zip to + * @throws IOException on error + * @since 1.14 + */ + public ZipOutputStream(File file) throws IOException { + //noinspection ConstantConditions + super(null); + + try { + raf = new RandomAccessFile(file, "rw"); + raf.setLength(0); + } catch (IOException e) { + if (raf != null) { + try { + raf.close(); + } catch (IOException inner) { + // ignore + } + raf = null; + } + out = new FileOutputStream(file); + } + } + + /** + * This method indicates whether this archive is writing to a + * seekable stream (i.e., to a random access file). + *

+ *

For seekable streams, you don't need to calculate the CRC or + * uncompressed size for {@link #STORED} entries before + * invoking {@link #putNextEntry}. + * + * @return true if seekable + * @since 1.17 + */ + public boolean isSeekable() { + return raf != null; + } + + /** + * The encoding to use for filenames and the file comment. + *

+ *

For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. + * Defaults to the platform's default character encoding.

+ * + * @param encoding the encoding value + * @since 1.3 + */ + public void setEncoding(final String encoding) { + this.encoding = encoding; + boolean isUTF8 = ZipEncodingHelper.isUTF8(encoding); + this.zipEncoding = isUTF8 ? UTF8_ZIP_ENCODING : ZipEncodingHelper.getZipEncoding(encoding); + if (useUTF8Flag && !isUTF8) { + useUTF8Flag = false; + } + } + + public void setZipEncoding(final ZipEncoding zipEncoding) { + this.zipEncoding = zipEncoding; + this.encoding = zipEncoding.getEncoding(); + if (useUTF8Flag && !ZipEncodingHelper.isUTF8(encoding)) { + useUTF8Flag = false; + } + } + + + /** + * The encoding to use for filenames and the file comment. + * + * @return null if using the platform's default character encoding. + * @since 1.3 + */ + public String getEncoding() { + return encoding; + } + + /** + * Whether to set the language encoding flag if the file name + * encoding is UTF-8. + *

+ *

Defaults to true.

+ */ + public void setUseLanguageEncodingFlag(boolean b) { + useUTF8Flag = b && ZipEncodingHelper.isUTF8(encoding); + } + + /** + * Whether to getInstance Unicode Extra Fields. + *

+ *

Defaults to NEVER.

+ */ + public void setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy b) { + createUnicodeExtraFields = b; + } + + /** + * Whether to fall back to UTF and the language encoding flag if + * the file name cannot be encoded using the specified encoding. + *

+ *

Defaults to false.

+ */ + public void setFallbackToUTF8(boolean b) { + fallbackToUTF8 = b; + } + + /** + * Finishs writing the contents and closes this as well as the + * underlying stream. + * + * @throws IOException on error + * @since 1.1 + */ + public void finish() throws IOException { + closeEntry(); + cdOffset = written; + for (ZipEntry entry1 : entries) { + writeCentralFileHeader(entry1); + } + cdLength = written - cdOffset; + writeCentralDirectoryEnd(); + offsets.clear(); + entries.clear(); + def.end(); + } + + /** + * Writes all necessary data for this entry. + * + * @throws IOException on error + * @since 1.1 + */ + public void closeEntry() throws IOException { + if (entry == null) { + return; + } + + if (currentIsRawEntry) + crc.reset(); + else { + long realCrc = crc.getValue(); + crc.reset(); + + if (entry.getMethod() == DEFLATED) { + def.finish(); + while (!def.finished()) { + deflate(); + } + + entry.setSize(adjustToLong(def.getTotalIn())); + entry.setCompressedSize(adjustToLong(def.getTotalOut())); + entry.setCrc(realCrc); + + def.reset(); + + written += entry.getCompressedSize(); + } else if (raf == null) { + if (entry.getCrc() != realCrc) { + throw new ZipException("bad CRC checksum for entry " + + entry.getName() + ": " + + Long.toHexString(entry.getCrc()) + + " instead of " + + Long.toHexString(realCrc)); + } + + if (entry.getSize() != written - dataStart) { + throw new ZipException("bad size for entry " + + entry.getName() + ": " + + entry.getSize() + + " instead of " + + (written - dataStart)); + } + } else { /* method is STORED and we used RandomAccessFile */ + long size = written - dataStart; + + entry.setSize(size); + entry.setCompressedSize(size); + entry.setCrc(realCrc); + } + + // If random access output, write the local file header containing + // the correct CRC and compressed/uncompressed sizes + if (raf != null) { + long save = raf.getFilePointer(); + + raf.seek(localDataStart); + byte[] data = new byte[12]; + putLong(entry.getCrc(), data, 0); + putLong(entry.getCompressedSize(), data, 4); + putLong(entry.getSize(), data, 8); + writeOut(data); + raf.seek(save); + } + } + + writeDataDescriptor(entry); + entry = null; + } + + public void writeFully(InputStream is) throws IOException { + int len; + byte[] b = new byte[BUFFER_SIZE]; + while ((len = is.read(b)) > 0) + write(b, 0, len); + } + + public void copyZipEntry(ZipEntry zipEntry, ZipFile zipFile) throws IOException { + InputStream rawInputStream = zipFile.getRawInputStream(zipEntry); + if (rawInputStream == null) { + } + putNextRawEntry(zipEntry); + int len; + byte[] b = new byte[BUFFER_SIZE]; + while ((len = rawInputStream.read(b)) > 0) { + writeRaw(b, 0, len); + } + closeEntry(); + } + + private boolean currentIsRawEntry; + + public void putNextEntry(String entryName) throws IOException { + putNextEntry(new ZipEntry(entryName)); + } + + /** + * Begin writing next entry. + * + * @param ze the entry to write + * @throws IOException on error + * @since 1.1 + */ + public void putNextEntry(ZipEntry ze) throws IOException { + closeEntry(); + + entry = ze; + entries.add(entry); + + currentIsRawEntry = false; + + //noinspection WrongConstant + if (entry.getMethod() == -1) { // not specified + entry.setMethod(method); + } + + if (entry.getTime() == -1) { // not specified + entry.setTime(System.currentTimeMillis()); + } + + // Size/CRC not required if RandomAccessFile is used + if (entry.getMethod() == STORED && raf == null) { + if (entry.getSize() == -1) { + throw new ZipException("uncompressed size is required for" + + " STORED method when not writing to a" + + " file"); + } + if (entry.getCrc() == -1) { + throw new ZipException("crc checksum is required for STORED" + + " method when not writing to a file"); + } + entry.setCompressedSize(entry.getSize()); + } + + if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) { + def.setLevel(level); + hasCompressionLevelChanged = false; + } + writeLocalFileHeader(entry); + } + + public void putNextRawEntry(ZipEntry ze) throws IOException { + closeEntry(); + + entry = ze; + entries.add(entry); + + currentIsRawEntry = true; + + // Size/CRC not required if RandomAccessFile is used + if (entry.getMethod() == STORED && raf == null) { + if (entry.getSize() == -1) { + throw new ZipException("uncompressed size is required for" + + " STORED method when not writing to a" + + " file"); + } + if (entry.getCrc() == -1) { + throw new ZipException("crc checksum is required for STORED" + + " method when not writing to a file"); + } + entry.setCompressedSize(entry.getSize()); + } + + if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) { + def.setLevel(level); + hasCompressionLevelChanged = false; + } + writeLocalFileHeader(entry); + } + + /** + * Set the file comment. + * + * @param comment the comment + * @since 1.1 + */ + public void setComment(String comment) { + this.comment = comment; + } + + /** + * Sets the compression level for subsequent entries. + *

+ *

Default is Deflater.DEFAULT_COMPRESSION.

+ * + * @param level the compression level. + * @throws IllegalArgumentException if an invalid compression + * level is specified. + * @since 1.1 + */ + public void setLevel(int level) { + if (level < Deflater.DEFAULT_COMPRESSION + || level > Deflater.BEST_COMPRESSION) { + throw new IllegalArgumentException("Invalid compression level: " + + level); + } + hasCompressionLevelChanged = (this.level != level); + this.level = level; + } + + /** + * Sets the default compression method for subsequent entries. + *

+ *

Default is DEFLATED.

+ * + * @param method an int from java.util.zip.ZipEntry + * @since 1.1 + */ + public void setMethod(int method) { + this.method = method; + } + + /** + * Writes bytes to ZIP entry. + * + * @param b the byte array to write + * @param offset the start position to write from + * @param length the number of bytes to write + * @throws IOException on error + */ + public void write(byte[] b, int offset, int length) throws IOException { + if (entry.getMethod() == DEFLATED) { + if (length > 0) { + if (!def.finished()) { + if (length <= DEFLATER_BLOCK_SIZE) { + def.setInput(b, offset, length); + deflateUntilInputIsNeeded(); + } else { + final int fullblocks = length / DEFLATER_BLOCK_SIZE; + for (int i = 0; i < fullblocks; i++) { + def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE, + DEFLATER_BLOCK_SIZE); + deflateUntilInputIsNeeded(); + } + final int done = fullblocks * DEFLATER_BLOCK_SIZE; + if (done < length) { + def.setInput(b, offset + done, length - done); + deflateUntilInputIsNeeded(); + } + } + } + } + } else { + writeOut(b, offset, length); + written += length; + } + crc.update(b, offset, length); + } + + public void writeRaw(byte[] b, int offset, int length) throws IOException { + writeOut(b, offset, length); + written += length; + } + + public void writeRaw(byte[] b) throws IOException { + writeRaw(b, 0, b.length); + } + + /** + * Writes a single byte to ZIP entry. + *

+ *

Delegates to the three arg method.

+ * + * @param b the byte to write + * @throws IOException on error + * @since 1.14 + */ + public void write(int b) throws IOException { + byte[] buff = new byte[1]; + buff[0] = (byte) (b & BYTE_MASK); + write(buff, 0, 1); + } + + /** + * Closes this output stream and releases any system resources + * associated with the stream. + * + * @throws IOException if an I/O error occurs. + * @since 1.14 + */ + public void close() throws IOException { + finish(); + + if (raf != null) { + raf.close(); + } + if (out != null) { + out.close(); + } + } + + /** + * Flushes this output stream and forces any buffered output bytes + * to be written out to the stream. + * + * @throws IOException if an I/O error occurs. + * @since 1.14 + */ + public void flush() throws IOException { + if (out != null) { + out.flush(); + } + } + + /* + * Various ZIP constants + */ + /** + * local file header signature + * + * @since 1.1 + */ + protected static final byte[] LFH_SIG = ZipLong.getBytes(0X04034B50L); + /** + * data descriptor signature + * + * @since 1.1 + */ + protected static final byte[] DD_SIG = ZipLong.getBytes(0X08074B50L); + /** + * central file header signature + * + * @since 1.1 + */ + protected static final byte[] CFH_SIG = ZipLong.getBytes(0X02014B50L); + /** + * end of central dir signature + * + * @since 1.1 + */ + protected static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L); + + /** + * Writes next block of compressed data to the output stream. + * + * @throws IOException on error + * @since 1.14 + */ + protected final void deflate() throws IOException { + int len = def.deflate(buf, 0, buf.length); + if (len > 0) { + writeOut(buf, 0, len); + } + } + + /** + * Writes the local file header entry + * + * @param ze the entry to write + * @throws IOException on error + * @since 1.1 + */ + protected void writeLocalFileHeader(ZipEntry ze) throws IOException { + + boolean encodable = zipEncoding.canEncode(ze.getName()); + + final ZipEncoding entryEncoding; + + if (!encodable && fallbackToUTF8) { + entryEncoding = UTF8_ZIP_ENCODING; + } else { + entryEncoding = zipEncoding; + } + + ByteBuffer name = entryEncoding.encode(ze.getName()); + + if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) { + + if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS + || !encodable) { + ze.addExtraField(new UnicodePathExtraField(ze.getName(), + name.array(), + name.arrayOffset(), + name.limit())); + } + + String comm = ze.getComment(); + if (comm != null && !"".equals(comm)) { + + boolean commentEncodable = this.zipEncoding.canEncode(comm); + + if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS + || !commentEncodable) { + ByteBuffer commentB = entryEncoding.encode(comm); + ze.addExtraField(new UnicodeCommentExtraField(comm, + commentB.array(), + commentB.arrayOffset(), + commentB.limit()) + ); + } + } + } + + offsets.put(ze, written); + + byte[] data = new byte[30 + name.limit()]; + + putBytes(LFH_SIG, data, 0); + written += WORD; + + //store method in local variable to prevent multiple method calls + final int zipMethod = ze.getMethod(); + + writeVersionNeededToExtractAndGeneralPurposeBits(data, 4, zipMethod, + !encodable && fallbackToUTF8); + written += WORD; + + // compression method + putShort(zipMethod, data, 8); + written += SHORT; + + // last mod. time and date + putLong(toDosTime(ze.getTime()), data, 10); + written += WORD; + + // CRC + // compressed length + // uncompressed length + localDataStart = written; + if (currentIsRawEntry) { + putLong(ze.getCrc(), data, 14); + putLong(ze.getCompressedSize(), data, 18); + putLong(ze.getSize(), data, 22); + } else if (zipMethod != DEFLATED && raf == null) { + putLong(ze.getCrc(), data, 14); + putLong(ze.getSize(), data, 18); + putLong(ze.getSize(), data, 22); + } +// else { +// writeOut(LZERO); +// writeOut(LZERO); +// writeOut(LZERO); +// } + // CheckStyle:MagicNumber OFF + written += 12; + // CheckStyle:MagicNumber ON + + // file name length + putShort(name.limit(), data, 26); + written += SHORT; + + // extra field length + byte[] extra = ze.getLocalFileDataExtra(); + if (ze.getMethod() == STORED) { + // ZipAlign对齐优化 + long len = written + SHORT + name.limit() + extra.length; + if (len % 4 != 0) { + int extraLen = 4 - (int) ((written + SHORT + name.limit()) % 4); + extra = new byte[extraLen]; + } + } + putShort(extra.length, data, 28); + written += SHORT; + + // file name + putBytes(name.array(), name.arrayOffset(), name.limit(), data, 30); + written += name.limit(); + + writeOut(data); + + // extra field + if (extra.length > 0) { + writeOut(extra); + written += extra.length; + } + + dataStart = written; + } + + private static void putBytes(byte[] value, int start, int length, byte[] buf, int offset) { + for (int i = 0; i < length; i++) { + buf[offset++] = value[start + i]; + } + } + + private static void putBytes(byte[] value, byte[] buf, int offset) { + putBytes(value, 0, value.length, buf, offset); + } + + + /** + * Writes the data descriptor entry. + * + * @param ze the entry to write + * @throws IOException on error + * @since 1.1 + */ + protected void writeDataDescriptor(ZipEntry ze) throws IOException { + if (ze.getMethod() != DEFLATED || raf != null) { + return; + } + byte[] data = new byte[16]; + putBytes(DD_SIG, data, 0); + putLong(entry.getCrc(), data, 4); + putLong(entry.getCompressedSize(), data, 8); + putLong(entry.getSize(), data, 12); + writeOut(data); + // CheckStyle:MagicNumber OFF + written += 16; + // CheckStyle:MagicNumber ON + } + + /** + * Writes the central file header entry. + * + * @param ze the entry to write + * @throws IOException on error + * @since 1.1 + */ + protected void writeCentralFileHeader(ZipEntry ze) throws IOException { + final int zipMethod = ze.getMethod(); + final boolean encodable = zipEncoding.canEncode(ze.getName()); + + final ZipEncoding entryEncoding; + + if (!encodable && fallbackToUTF8) { + entryEncoding = UTF8_ZIP_ENCODING; + } else { + entryEncoding = zipEncoding; + } + ByteBuffer name = entryEncoding.encode(ze.getName()); + + byte[] extra = ze.getCentralDirectoryExtra(); + + String comm = ze.getComment(); + if (comm == null) { + comm = ""; + } + + ByteBuffer commentB = entryEncoding.encode(comm); + + + byte[] data = new byte[46 + name.limit() + extra.length + commentB.limit()]; + + putBytes(CFH_SIG, data, 0); + written += WORD; + + // version made by + // CheckStyle:MagicNumber OFF + putShort((ze.getPlatform() << 8) | 20, data, 4); + written += SHORT; + + + writeVersionNeededToExtractAndGeneralPurposeBits(data, 6, zipMethod, + !encodable && fallbackToUTF8); + written += WORD; + + // compression method + putShort(zipMethod, data, 10); + written += SHORT; + + // last mod. time and date + putLong(toDosTime(ze.getTime()), data, 12); + written += WORD; + + // CRC + // compressed length + // uncompressed length + putLong(ze.getCrc(), data, 16); + putLong(ze.getCompressedSize(), data, 20); + putLong(ze.getSize(), data, 24); + // CheckStyle:MagicNumber OFF + written += 12; + // CheckStyle:MagicNumber ON + + // file name length + + putShort(name.limit(), data, 28); + written += SHORT; + + // extra field length + putShort(extra.length, data, 30); + written += SHORT; + + // file comment length + putShort(commentB.limit(), data, 32); + written += SHORT; + + // disk number start + written += SHORT; + + // internal file attributes + putShort(ze.getInternalAttributes(), data, 36); + written += SHORT; + + // external file attributes + putLong(ze.getExternalAttributes(), data, 38); + written += WORD; + + // relative offset of LFH + putLong(offsets.get(ze), data, 42); + written += WORD; + + // file name + putBytes(name.array(), name.arrayOffset(), name.limit(), data, 46); + written += name.limit(); + + // extra field + putBytes(extra, data, 46 + name.limit()); + written += extra.length; + + // file comment + putBytes(commentB.array(), commentB.arrayOffset(), commentB.limit(), + data, 46 + name.limit() + extra.length); + written += commentB.limit(); + writeOut(data); + } + + /** + * Writes the "End of central dir record". + * + * @throws IOException on error + * @since 1.1 + */ + protected void writeCentralDirectoryEnd() throws IOException { + ByteBuffer data = this.zipEncoding.encode(comment); + + byte[] buf = new byte[22 + data.limit()]; + putBytes(EOCD_SIG, buf, 0); +// putBytes(ZERO, buf, 4); +// putBytes(ZERO, buf, 6); + putShort(entries.size(), buf, 8); + putShort(entries.size(), buf, 10); + putLong(cdLength, buf, 12); + putLong(cdOffset, buf, 16); + putShort(data.limit(), buf, 20); + putBytes(data.array(), data.arrayOffset(), data.limit(), buf, 22); + writeOut(buf); +// writeOut(EOCD_SIG);//4 +// +// // disk numbers +// writeOut(ZERO);//2 +// writeOut(ZERO);//2 +// +// // number of entries +// byte[] num = ZipShort.getBytes(entries.size()); +// writeOut(num);//2 +// writeOut(num);//2 +// +// // length and location of CD +// writeOut(ZipLong.getBytes(cdLength));//4 +// writeOut(ZipLong.getBytes(cdOffset));//4 +// +// // ZIP file comment +// writeOut(ZipShort.getBytes(data.limit())); +// writeOut(data.array(), data.arrayOffset(), data.limit()); + } + + /** + * Smallest date/time ZIP can handle. + * + * @since 1.1 + */ +// private static final byte[] DOS_TIME_MIN = ZipLong.getBytes(0x00002100L); + + /** + * Convert a Date object to a DOS date/time field. + *

+ *

Stolen from InfoZip's fileio.c

+ * + * @param t number of milliseconds since the epoch + * @return the date as a byte array + * @since 1.26 + */ + @SuppressWarnings("deprecation") + protected static long toDosTime(long t) { + Date time = new Date(t); + // CheckStyle:MagicNumberCheck OFF - I do not think that using constants + // here will improve the readablity + int year = time.getYear() + 1900; + if (year < 1980) { + return 0x00002100L; + } + int month = time.getMonth() + 1; + return ((year - 1980) << 25) + | (month << 21) + | (time.getDate() << 16) + | (time.getHours() << 11) + | (time.getMinutes() << 5) + | (time.getSeconds() >> 1); + // CheckStyle:MagicNumberCheck ON + } + + /** + * Write bytes to output or random access file. + * + * @param data the byte array to write + * @throws IOException on error + * @since 1.14 + */ + protected final void writeOut(byte[] data) throws IOException { + writeOut(data, 0, data.length); + } + + /** + * Write bytes to output or random access file. + * + * @param data the byte array to write + * @param offset the start position to write from + * @param length the number of bytes to write + * @throws IOException on error + * @since 1.14 + */ + protected final void writeOut(byte[] data, int offset, int length) + throws IOException { + if (raf != null) { + raf.write(data, offset, length); + } else { + out.write(data, offset, length); + } + } + + /** + * Assumes a negative integer really is a positive integer that + * has wrapped around and re-creates the original value. + * + * @param i the value to treat as unsigned int. + * @return the unsigned int as a long. + * @since 1.34 + */ + protected static long adjustToLong(int i) { + if (i < 0) { + return 2 * ((long) Integer.MAX_VALUE) + 2 + i; + } else { + return i; + } + } + + private void deflateUntilInputIsNeeded() throws IOException { + while (!def.needsInput()) { + deflate(); + } + } + + private void writeVersionNeededToExtractAndGeneralPurposeBits( + final byte[] data, int offset, + final int zipMethod, final boolean utfFallback) + throws IOException { + + // CheckStyle:MagicNumber OFF + int versionNeededToExtract = 10; + int generalPurposeFlag = (useUTF8Flag || utfFallback) ? UFT8_NAMES_FLAG : 0; + if (zipMethod == DEFLATED && raf == null) { + // requires version 2 as we are going to store length info + // in the data descriptor + versionNeededToExtract = 20; + // bit3 set to signal, we use a data descriptor + generalPurposeFlag |= 8; + } + // CheckStyle:MagicNumber ON + + // version needed to extract + putShort(versionNeededToExtract, data, offset); + // general purpose bit flag + putShort(generalPurposeFlag, data, offset + SHORT); + } + + /** + * enum that represents the possible policies for creating Unicode + * extra fields. + */ + public static final class UnicodeExtraFieldPolicy { + /** + * Always getInstance Unicode extra fields. + */ + public static final UnicodeExtraFieldPolicy ALWAYS = + new UnicodeExtraFieldPolicy("always"); + /** + * Never getInstance Unicode extra fields. + */ + public static final UnicodeExtraFieldPolicy NEVER = + new UnicodeExtraFieldPolicy("never"); + /** + * Create Unicode extra fields for filenames that cannot be + * encoded using the specified encoding. + */ + public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE = + new UnicodeExtraFieldPolicy("not encodeable"); + + private final String name; + + private UnicodeExtraFieldPolicy(String n) { + name = n; + } + + public String toString() { + return name; + } + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipShort.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipShort.java new file mode 100644 index 0000000..eed8cef --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipShort.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip; + +/** + * Utility class that represents a two byte integer with conversion + * rules for the big endian byte order of ZIP files. + */ +public final class ZipShort implements Cloneable { + private static final int BYTE_MASK = 0xFF; + private static final int BYTE_1_MASK = 0xFF00; + private static final int BYTE_1_SHIFT = 8; + + private int value; + + /** + * Create instance from a number. + * + * @param value the int to store as a ZipShort + * @since 1.1 + */ + public ZipShort(int value) { + this.value = value; + } + + /** + * Create instance from bytes. + * + * @param bytes the bytes to store as a ZipShort + * @since 1.1 + */ + public ZipShort(byte[] bytes) { + this(bytes, 0); + } + + /** + * Create instance from the two bytes starting at offset. + * + * @param bytes the bytes to store as a ZipShort + * @param offset the offset to start + * @since 1.1 + */ + public ZipShort(byte[] bytes, int offset) { + value = ZipShort.getValue(bytes, offset); + } + + /** + * Get value as two bytes in big endian byte order. + * + * @return the value as a a two byte array in big endian byte order + * @since 1.1 + */ + public byte[] getBytes() { + byte[] result = new byte[2]; + result[0] = (byte) (value & BYTE_MASK); + result[1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT); + return result; + } + + /** + * put the value as two bytes in big endian byte order. + * + * @param value the Java int to convert to bytes + * @param buf the output buffer + * @param offset The offset within the output buffer of the first byte to be written. + * must be non-negative and no larger than buf.length-2 + */ + public static void putShort(int value, byte[] buf, int offset) { + buf[offset] = (byte) (value & BYTE_MASK); + buf[offset + 1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT); + } + + /** + * Get value as Java int. + * + * @return value as a Java int + * @since 1.1 + */ + public int getValue() { + return value; + } + + /** + * Get value as two bytes in big endian byte order. + * + * @param value the Java int to convert to bytes + * @return the converted int as a byte array in big endian byte order + */ + public static byte[] getBytes(int value) { + byte[] result = new byte[2]; + result[0] = (byte) (value & BYTE_MASK); + result[1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT); + return result; + } + + /** + * Helper method to get the value as a java int from two bytes starting at given array offset + * + * @param bytes the array of bytes + * @param offset the offset to start + * @return the correspondanding java int value + */ + public static int getValue(byte[] bytes, int offset) { + int value = (bytes[offset + 1] << BYTE_1_SHIFT) & BYTE_1_MASK; + value += (bytes[offset] & BYTE_MASK); + return value; + } + + /** + * Helper method to get the value as a java int from a two-byte array + * + * @param bytes the array of bytes + * @return the correspondanding java int value + */ + public static int getValue(byte[] bytes) { + return getValue(bytes, 0); + } + + /** + * Override to make two instances with same value equal. + * + * @param o an object to compare + * @return true if the objects are equal + * @since 1.1 + */ + public boolean equals(Object o) { + if (o == null || !(o instanceof ZipShort)) { + return false; + } + return value == ((ZipShort) o).getValue(); + } + + /** + * Override to make two instances with same value equal. + * + * @return the value stored in the ZipShort + * @since 1.1 + */ + public int hashCode() { + return value; + } + + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException cnfe) { + // impossible + throw new RuntimeException(cnfe); + } + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipUtil.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipUtil.java new file mode 100644 index 0000000..5b88914 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/ZipUtil.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package top.niunaijun.blackobfuscator.core.utils.zip; + +/** + * Utility class for handling DOS and Java time conversions. + * + * @since Ant 1.8.1 + */ +public abstract class ZipUtil { + /** + * Create a copy of the given array - or return null if the + * argument is null. + */ + public static byte[] copy(byte[] from) { + if (from != null) { + byte[] to = new byte[from.length]; + System.arraycopy(from, 0, to, 0, to.length); + return to; + } + return null; + } + +} \ No newline at end of file diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/DetectEncoding.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/DetectEncoding.java new file mode 100644 index 0000000..c116b90 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/DetectEncoding.java @@ -0,0 +1,2062 @@ +package top.niunaijun.blackobfuscator.core.utils.zip.encoding; + +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; + +@SuppressWarnings("SpellCheckingInspection") +public class DetectEncoding { + + private final static int UTF8 = 0; + private final static int GB2312 = 1; + private final static int GBK = 2; + private final static int UNICODE = 3; + private final static int ASCII = 4; + private final static int BIG5 = 5; + private final static int EUC_TW = 6; + private final static int OTHER = 7; + + private final static int TOTAL_ENCODINGS = 8; + + // Frequency tables to hold the GB, Big5, and EUC-TW character + // frequencies + private static final int GBFreq[][]; + private static final int GBKFreq[][]; + private static final int Big5Freq[][]; + private static final int EUC_TWFreq[][]; + // int UnicodeFreq[94][128]; + + public static String encodeString(byte[] data, boolean mustEncodable) { + DetectEncoding detectEncoding = new DetectEncoding(); + detectEncoding.update(data); + if (mustEncodable && detectEncoding.encoding_guess == OTHER) + return null; + Charset charset = detectEncoding.getEncode(); + return new String(data, charset); + } + + private static final Charset[] encodeCharset; + + public static final Charset CHARSET_UTF8; + public static final Charset CHARSET_GB2312; + public static final Charset CHARSET_GBK; + public static final Charset CHARSET_UNICODE; + public static final Charset CHARSET_BIG5; + public static final Charset CHARSET_EUC_TW; + + // Initialize the Frequency Table for GB, Big5, EUC-TW + static { + GBFreq = new int[94][94]; + GBKFreq = new int[126][191]; + Big5Freq = new int[94][158]; + EUC_TWFreq = new int[94][94]; + + CHARSET_UTF8 = Charset.forName("UTF-8"); + CHARSET_GB2312 = Charset.forName("GB2312"); + CHARSET_GBK = Charset.forName("GBK"); + CHARSET_UNICODE = Charset.forName("Unicode"); + CHARSET_BIG5 = Charset.forName("Big5"); + + Charset charset; + try { + charset = Charset.forName("EUC-TW"); + } catch (UnsupportedCharsetException ignored) { + charset = CHARSET_UTF8; + } + CHARSET_EUC_TW = charset; + + encodeCharset = new Charset[TOTAL_ENCODINGS]; + encodeCharset[UTF8] = CHARSET_UTF8; + encodeCharset[GB2312] = CHARSET_GB2312; + encodeCharset[GBK] = CHARSET_GBK; + encodeCharset[UNICODE] = CHARSET_UNICODE; + encodeCharset[ASCII] = CHARSET_GBK;//Charset.forName("ASCII"); + encodeCharset[BIG5] = CHARSET_BIG5; + encodeCharset[EUC_TW] = CHARSET_EUC_TW; + encodeCharset[OTHER] = CHARSET_UTF8; + initialize_frequencies(); + } + + public void update(byte[] rawtext) { + update(rawtext, 0, rawtext.length); + } + + public void update(byte[] rawtext, int offset, int length) { + int[] scores; + + scores = new int[TOTAL_ENCODINGS]; + + // Assign Scores + scores[UTF8] = utf8_probability(rawtext, offset, length); + scores[GB2312] = gb2312_probability(rawtext, offset, length); + scores[GBK] = gbk_probability(rawtext, offset, length); + scores[UNICODE] = utf16_probability(rawtext, offset, length); + scores[ASCII] = ascii_probability(rawtext, offset, length); + scores[BIG5] = big5_probability(rawtext, offset, length); + scores[EUC_TW] = euc_tw_probability(rawtext, offset, length); + // scores[OTHER] = 0; + +// Log.w("DetectEncoding", "UTF8:" + scores[UTF8] + " GBK:" + scores[GBK]); + int maxScore = 0; + for (int index = 0; index < TOTAL_ENCODINGS; index++) { + if (scores[index] > maxScore) { + encoding_guess = index; + maxScore = scores[index]; + } + } + + if (maxScore <= 50) + encoding_guess = OTHER; + } + + int encoding_guess = OTHER; + + public Charset getEncode() { + return encodeCharset[encoding_guess]; + } + + public static Charset getEncodeByIndex(int index) { + return encodeCharset[index]; + } + + public int getEncodeIndex() { + return encoding_guess; + } + + public static String getEncodeName(int index) { + switch (index) { + case UTF8: + return "UTF-8"; + case GB2312: + return "GB2312"; + case GBK: + return "GBK"; + case UNICODE: + return "Unicode"; + case ASCII: + return "ASCII"; + case BIG5: + return "Big5"; + case EUC_TW: + return "EUC-TW"; + case OTHER: + default: + return "UTF-8"; + } + } + + /* + * Function: gb2312_probability Argument: pointer to byte array Returns : + * number from 0 to 100 representing probability text in array uses GB-2312 + * encoding + */ + int gb2312_dbchars = 1, gb2312_chars = 1; + long gb2312_gbfreq = 0, gb2312_totalfreq = 1; + + private int gb2312_probability(byte[] rawtext, int offset, int length) { + float rangeval, freqval; + int row, column; + + // Stage 1: Check to see if characters fit into acceptable ranges + int rawLength = offset + length - 1; + for (int i = offset; i < rawLength; i++) + if (rawtext[i] < 0) { + gb2312_dbchars++; + if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7 + && (byte) 0xA1 <= rawtext[i + 1] + && rawtext[i + 1] <= (byte) 0xFE) { + gb2312_chars++; + gb2312_totalfreq += 500; + row = rawtext[i] + 256 - 0xA1; + column = rawtext[i + 1] + 256 - 0xA1; + if (GBFreq[row][column] != 0) + gb2312_gbfreq += GBFreq[row][column]; + else if (15 <= row && row < 55) + gb2312_gbfreq += 200; + } + i++; + } + rangeval = 50 * ((float) gb2312_chars / (float) gb2312_dbchars); + freqval = 50 * ((float) gb2312_gbfreq / (float) gb2312_totalfreq); + return (int) (rangeval + freqval); + } + + /* + * Function: gb2312_probability Argument: pointer to byte array Returns : + * number from 0 to 100 representing probability text in array uses GB-2312 + * encoding + */ + int gbk_dbchars = 1, gbk_chars = 1; + long gbk_freq = 0, gbk_totalfreq = 1; + + private int gbk_probability(byte[] rawtext, int offset, int length) { + float rangeval, freqval; + int row, column; + + // Stage 1: Check to see if characters fit into acceptable ranges + int rawLength = offset + length - 1; + for (int i = offset; i < rawLength; i++) + // System.err.println(rawtext[i]); + if (rawtext[i] < 0) { + gbk_dbchars++; + if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7 + && // Original GB range + (byte) 0xA1 <= rawtext[i + 1] + && rawtext[i + 1] <= (byte) 0xFE) { + gbk_chars++; + gbk_totalfreq += 500; + row = rawtext[i] + 256 - 0xA1; + column = rawtext[i + 1] + 256 - 0xA1; + + // System.out.println("original row " + row + " column " + + // column); + if (GBFreq[row][column] != 0) + gbk_freq += GBFreq[row][column]; + else if (15 <= row && row < 55) + gbk_freq += 200; + + } else if ((byte) 0x81 <= rawtext[i] + && rawtext[i] <= (byte) 0xFE && // Extended GB range + ((byte) 0x80 <= rawtext[i + 1] + && rawtext[i + 1] <= (byte) 0xFE || (byte) 0x40 <= rawtext[i + 1] + && rawtext[i + 1] <= (byte) 0x7E)) { + gbk_chars++; + gbk_totalfreq += 500; + row = rawtext[i] + 256 - 0x81; + if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) + column = rawtext[i + 1] - 0x40; + else + column = rawtext[i + 1] + 256 - 0x80; + // System.out.println("extended row " + row + " column " + + // column + " rawtext[i] " + rawtext[i]); + if (GBKFreq[row][column] != 0) + gbk_freq += GBKFreq[row][column]; + } + i++; + } + rangeval = 50 * ((float) gbk_chars / (float) gbk_dbchars); + freqval = 50 * ((float) gbk_freq / (float) gbk_totalfreq); + + // For regular GB files, this would give the same score, so I handicap + // it slightly + return (int) (rangeval + freqval) - 1; + } + + /** + * Function: big5_probability Argument: byte array Returns : number from 0 + * to 100 representing probability text in array uses Big5 encoding + */ + int big5_dbchars = 1, big5_chars = 1; + long big5_freq = 0, big5_totalfreq = 1; + + private int big5_probability(byte[] rawtext, int offset, int length) { + // int score = 0; + float rangeval, freqval; + int row, column; + + // Check to see if characters fit into acceptable ranges + int rawLength = offset + length - 1; + for (int i = offset; i < rawLength; i++) + if (rawtext[i] < 0) { + big5_dbchars++; + if ((byte) 0xA1 <= rawtext[i] + && rawtext[i] <= (byte) 0xF9 + && ((byte) 0x40 <= rawtext[i + 1] + && rawtext[i + 1] <= (byte) 0x7E || (byte) 0xA1 <= rawtext[i + 1] + && rawtext[i + 1] <= (byte) 0xFE)) { + big5_chars++; + big5_totalfreq += 500; + row = rawtext[i] + 256 - 0xA1; + if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) + column = rawtext[i + 1] - 0x40; + else + column = rawtext[i + 1] + 256 - 0x61; + if (Big5Freq[row][column] != 0) + big5_freq += Big5Freq[row][column]; + else if (3 <= row && row <= 37) + big5_freq += 200; + } + i++; + } + rangeval = 50 * ((float) big5_chars / (float) big5_dbchars); + freqval = 50 * ((float) big5_freq / (float) big5_totalfreq); + + return (int) (rangeval + freqval); + } + + /* + * Function: euc_tw_probability Argument: byte array Returns : number from 0 + * to 100 representing probability text in array uses EUC-TW (CNS 11643) + * encoding + */ + int tw_dbchars = 1, tw_chars = 1; + long tw_freq = 0, tw_totalfreq = 1; + + private int euc_tw_probability(byte[] rawtext, int offset, int length) { + float rangeval, freqval; + int row, column; + + // Check to see if characters fit into acceptable ranges + // and have expected frequency of use + int rawLength = offset + length - 1; + for (int i = offset; i < rawLength; i++) + if (rawtext[i] < 0) { + tw_dbchars++; + if (i + 3 < offset + length && (byte) 0x8E == rawtext[i] + && (byte) 0xA1 <= rawtext[i + 1] + && rawtext[i + 1] <= (byte) 0xB0 + && (byte) 0xA1 <= rawtext[i + 2] + && rawtext[i + 2] <= (byte) 0xFE + && (byte) 0xA1 <= rawtext[i + 3] + && rawtext[i + 3] <= (byte) 0xFE) { // Planes 1 - 16 + + tw_chars++; + // System.out.println("plane 2 or above CNS char"); + // These are all less frequent chars so just ignore freq + i += 3; + } else if ((byte) 0xA1 <= rawtext[i] + && rawtext[i] <= (byte) 0xFE + && // Plane 1 + (byte) 0xA1 <= rawtext[i + 1] + && rawtext[i + 1] <= (byte) 0xFE) { + tw_chars++; + tw_totalfreq += 500; + row = rawtext[i] + 256 - 0xA1; + column = rawtext[i + 1] + 256 - 0xA1; + if (EUC_TWFreq[row][column] != 0) + tw_freq += EUC_TWFreq[row][column]; + else if (35 <= row && row <= 92) + tw_freq += 150; + i++; + } + } + + rangeval = 50 * ((float) tw_chars / (float) tw_dbchars); + freqval = 50 * ((float) tw_freq / (float) tw_totalfreq); + + return (int) (rangeval + freqval); + } + + /* + * Function: iso_2022_cn_probability Argument: byte array Returns : number + * from 0 to 100 representing probability text in array uses ISO 2022-CN + * encoding WORKS FOR BASIC CASES, BUT STILL NEEDS MORE WORK + */ +// int iso_dbchars = 1, iso_chars = 1; +// long iso_freq = 0, iso_totalfreq = 1; +// +// private int iso_2022_cn_probability(byte[] rawtext, int offset, int length) { +// float rangeval = 0, freqval = 0; +// int row, column; +// +// // Check to see if characters fit into acceptable ranges +// // and have expected frequency of use +// +// int rawtextlen = offset + length; +// int rawLength = offset + length - 1; +// for (int i = offset; i < rawLength; i++) +// if (rawtext[i] == (byte) 0x1B && i + 3 < rawtextlen) { // Escape +// // char ESC +// if (rawtext[i + 1] == (byte) 0x24 && rawtext[i + 2] == 0x29 +// && rawtext[i + 3] == (byte) 0x41) { // GB Escape $ ) A +// i += 4; +// while (rawtext[i] != (byte) 0x1B) { +// iso_dbchars++; +// if (0x21 <= rawtext[i] && rawtext[i] <= 0x77 +// && 0x21 <= rawtext[i + 1] +// && rawtext[i + 1] <= 0x77) { +// iso_chars++; +// row = rawtext[i] - 0x21; +// column = rawtext[i + 1] - 0x21; +// iso_totalfreq += 500; +// if (GBFreq[row][column] != 0) +// iso_freq += GBFreq[row][column]; +// else if (15 <= row && row < 55) +// iso_freq += 200; +// i++; +// } +// i++; +// } +// } else if (i + 3 < rawtextlen && rawtext[i + 1] == (byte) 0x24 +// && rawtext[i + 2] == (byte) 0x29 +// && rawtext[i + 3] == (byte) 0x47) { +// // CNS Escape $ ) G +// i += 4; +// while (rawtext[i] != (byte) 0x1B) { +// iso_dbchars++; +// if ((byte) 0x21 <= rawtext[i] +// && rawtext[i] <= (byte) 0x7E +// && (byte) 0x21 <= rawtext[i + 1] +// && rawtext[i + 1] <= (byte) 0x7E) { +// iso_chars++; +// iso_totalfreq += 500; +// row = rawtext[i] - 0x21; +// column = rawtext[i + 1] - 0x21; +// if (EUC_TWFreq[row][column] != 0) +// iso_freq += EUC_TWFreq[row][column]; +// else if (35 <= row && row <= 92) +// iso_freq += 150; +// i++; +// } +// i++; +// } +// } +// if (rawtext[i] == (byte) 0x1B && i + 2 < rawtextlen +// && rawtext[i + 1] == (byte) 0x28 +// && rawtext[i + 2] == (byte) 0x42) +// i += 2; +// } +// rangeval = 50 * ((float) iso_chars / (float) iso_dbchars); +// freqval = 50 * ((float) iso_freq / (float) iso_totalfreq); +// +// // System.out.println("isochars dbchars isofreq totalfreq " + isochars + +// // " " + dbchars + " " + isofreq + " " + totalfreq + " " + rangeval + +// // " " + freqval); +// return (int) (rangeval + freqval); +// // return 0; +// } + + /* + * Function: utf8_probability Argument: byte array Returns : number from 0 + * to 100 representing probability text in array uses UTF-8 encoding of + * Unicode + */ + int goodbytes = 0, asciibytes = 0; + int utf8Length = 0; + + private int utf8_probability(byte[] rawtext, int offset, int length) { + // Maybe also use UTF8 Byte Order Mark: EF BB BF + + // Check to see if characters fit into acceptable ranges + int rawtextlen = offset + length; + for (int i = offset; i < rawtextlen; i++) + if ((rawtext[i] & (byte) 0x7F) == rawtext[i]) + asciibytes++; + // Ignore ASCII, can throw off count + else if (-64 <= rawtext[i] && rawtext[i] <= -33 + && // Two bytes + i + 1 < rawtextlen && -128 <= rawtext[i + 1] + && rawtext[i + 1] <= -65) { + goodbytes += 2; + i++; + } else if (-32 <= rawtext[i] + && rawtext[i] <= -17 + && // Three bytes + i + 2 < rawtextlen && -128 <= rawtext[i + 1] + && rawtext[i + 1] <= -65 && -128 <= rawtext[i + 2] + && rawtext[i + 2] <= -65) { + goodbytes += 3; + i += 2; + } + if (asciibytes == length) + return 0; + + utf8Length += length - asciibytes; + int score = (int) (100 * ((float) goodbytes / (float) (utf8Length))); + + // If not above 98, reduce to zero to prevent coincidental matches + // Allows for some (few) bad formed sequences + if (score > 98) + return score; + else if (score > 95 && goodbytes > 30) + return score; + else { + goodbytes = 0; + return 0; + } + + } + + /* + * Function: utf16_probability Argument: byte array Returns : number from 0 + * to 100 representing probability text in array uses UTF-16 encoding of + * Unicode, guess based on BOM // NOT VERY GENERAL, NEEDS MUCH MORE WORK + */ + boolean impossibleUTF16 = false; + + private int utf16_probability(byte[] rawtext, int offset, int length) { + if (impossibleUTF16 || length < 2) + return 0; + // int score = 0; + // int i, rawtextlen = 0; + // int goodbytes = 0, asciibytes = 0; + + if ((byte) 0xFE == rawtext[offset] && (byte) 0xFF == rawtext[offset + 1] || // Big-endian + (byte) 0xFF == rawtext[offset] && (byte) 0xFE == rawtext[offset + 1]) + return 100; + + impossibleUTF16 = true; + return 0; + + /* + * // Check to see if characters fit into acceptable ranges rawtextlen = + * rawtext.length; for (i = 0; i < rawtextlen; i++) { if ((rawtext[i] & + * (byte)0x7F) == rawtext[i]) { // One byte goodbytes += 1; + * asciibytes++; } else if ((rawtext[i] & (byte)0xDF) == rawtext[i]) { + * // Two bytes if (i+1 < rawtextlen && (rawtext[i+1] & (byte)0xBF) == + * rawtext[i+1]) { goodbytes += 2; i++; } } else if ((rawtext[i] & + * (byte)0xEF) == rawtext[i]) { // Three bytes if (i+2 < rawtextlen && + * (rawtext[i+1] & (byte)0xBF) == rawtext[i+1] && (rawtext[i+2] & + * (byte)0xBF) == rawtext[i+2]) { goodbytes += 3; i+=2; } } } + * + * score = (int)(100 * ((float)goodbytes/(float)rawtext.length)); + * + * // An all ASCII file is also a good UTF8 file, but I'd rather it // + * get identified as ASCII. Can delete following 3 lines otherwise if + * (goodbytes == asciibytes) { score = 0; } + * + * // If not above 90, reduce to zero to prevent coincidental matches if + * (score > 90) { return score; } else { return 0; } + */ + + } + + /* + * Function: ascii_probability Argument: byte array Returns : number from 0 + * to 100 representing probability text in array uses all ASCII Description: + * Sees if array has any characters not in ASCII range, if so, score is + * reduced + */ + int ascii_score = 70; + + private int ascii_probability(byte[] rawtext, int offset, int length) { + for (int i = offset; i < length; i++) { + byte aRawtext = rawtext[i]; + if (ascii_score <= 0) { + return 0; + } + if (aRawtext < 0) + ascii_score = ascii_score - 5; + else if (aRawtext == (byte) 0x1B) + ascii_score = ascii_score - 5; + } + + return ascii_score; + } + + @SuppressWarnings("ConstantConditions") + static void initialize_frequencies() { + int i, j; + + for (i = 0; i < 93; i++) + for (j = 0; j < 93; j++) + GBFreq[i][j] = 0; + + for (i = 0; i < 126; i++) + for (j = 0; j < 191; j++) + GBKFreq[i][j] = 0; + + for (i = 0; i < 93; i++) + for (j = 0; j < 157; j++) + Big5Freq[i][j] = 0; + + for (i = 0; i < 93; i++) + for (j = 0; j < 93; j++) + EUC_TWFreq[i][j] = 0; + + GBFreq[20][35] = 599; + GBFreq[49][26] = 598; + GBFreq[41][38] = 597; + GBFreq[17][26] = 596; + GBFreq[32][42] = 595; + GBFreq[39][42] = 594; + GBFreq[45][49] = 593; + GBFreq[51][57] = 592; + GBFreq[50][47] = 591; + GBFreq[42][90] = 590; + GBFreq[52][65] = 589; + GBFreq[53][47] = 588; + GBFreq[19][82] = 587; + GBFreq[31][19] = 586; + GBFreq[40][46] = 585; + GBFreq[24][89] = 584; + GBFreq[23][85] = 583; + GBFreq[20][28] = 582; + GBFreq[42][20] = 581; + GBFreq[34][38] = 580; + GBFreq[45][9] = 579; + GBFreq[54][50] = 578; + GBFreq[25][44] = 577; + GBFreq[35][66] = 576; + GBFreq[20][55] = 575; + GBFreq[18][85] = 574; + GBFreq[20][31] = 573; + GBFreq[49][17] = 572; + GBFreq[41][16] = 571; + GBFreq[35][73] = 570; + GBFreq[20][34] = 569; + GBFreq[29][44] = 568; + GBFreq[35][38] = 567; + GBFreq[49][9] = 566; + GBFreq[46][33] = 565; + GBFreq[49][51] = 564; + GBFreq[40][89] = 563; + GBFreq[26][64] = 562; + GBFreq[54][51] = 561; + GBFreq[54][36] = 560; + GBFreq[39][4] = 559; + GBFreq[53][13] = 558; + GBFreq[24][92] = 557; + GBFreq[27][49] = 556; + GBFreq[48][6] = 555; + GBFreq[21][51] = 554; + GBFreq[30][40] = 553; + GBFreq[42][92] = 552; + GBFreq[31][78] = 551; + GBFreq[25][82] = 550; + GBFreq[47][0] = 549; + GBFreq[34][19] = 548; + GBFreq[47][35] = 547; + GBFreq[21][63] = 546; + GBFreq[43][75] = 545; + GBFreq[21][87] = 544; + GBFreq[35][59] = 543; + GBFreq[25][34] = 542; + GBFreq[21][27] = 541; + GBFreq[39][26] = 540; + GBFreq[34][26] = 539; + GBFreq[39][52] = 538; + GBFreq[50][57] = 537; + GBFreq[37][79] = 536; + GBFreq[26][24] = 535; + GBFreq[22][1] = 534; + GBFreq[18][40] = 533; + GBFreq[41][33] = 532; + GBFreq[53][26] = 531; + GBFreq[54][86] = 530; + GBFreq[20][16] = 529; + GBFreq[46][74] = 528; + GBFreq[30][19] = 527; + GBFreq[45][35] = 526; + GBFreq[45][61] = 525; + GBFreq[30][9] = 524; + GBFreq[41][53] = 523; + GBFreq[41][13] = 522; + GBFreq[50][34] = 521; + GBFreq[53][86] = 520; + GBFreq[47][47] = 519; + GBFreq[22][28] = 518; + GBFreq[50][53] = 517; + GBFreq[39][70] = 516; + GBFreq[38][15] = 515; + GBFreq[42][88] = 514; + GBFreq[16][29] = 513; + GBFreq[27][90] = 512; + GBFreq[29][12] = 511; + GBFreq[44][22] = 510; + GBFreq[34][69] = 509; + GBFreq[24][10] = 508; + GBFreq[44][11] = 507; + GBFreq[39][92] = 506; + GBFreq[49][48] = 505; + GBFreq[31][46] = 504; + GBFreq[19][50] = 503; + GBFreq[21][14] = 502; + GBFreq[32][28] = 501; + GBFreq[18][3] = 500; + GBFreq[53][9] = 499; + GBFreq[34][80] = 498; + GBFreq[48][88] = 497; + GBFreq[46][53] = 496; + GBFreq[22][53] = 495; + GBFreq[28][10] = 494; + GBFreq[44][65] = 493; + GBFreq[20][10] = 492; + GBFreq[40][76] = 491; + GBFreq[47][8] = 490; + GBFreq[50][74] = 489; + GBFreq[23][62] = 488; + GBFreq[49][65] = 487; + GBFreq[28][87] = 486; + GBFreq[15][48] = 485; + GBFreq[22][7] = 484; + GBFreq[19][42] = 483; + GBFreq[41][20] = 482; + GBFreq[26][55] = 481; + GBFreq[21][93] = 480; + GBFreq[31][76] = 479; + GBFreq[34][31] = 478; + GBFreq[20][66] = 477; + GBFreq[51][33] = 476; + GBFreq[34][86] = 475; + GBFreq[37][67] = 474; + GBFreq[53][53] = 473; + GBFreq[40][88] = 472; + GBFreq[39][10] = 471; + GBFreq[24][3] = 470; + GBFreq[27][25] = 469; + GBFreq[26][15] = 468; + GBFreq[21][88] = 467; + GBFreq[52][62] = 466; + GBFreq[46][81] = 465; + GBFreq[38][72] = 464; + GBFreq[17][30] = 463; + GBFreq[52][92] = 462; + GBFreq[34][90] = 461; + GBFreq[21][7] = 460; + GBFreq[36][13] = 459; + GBFreq[45][41] = 458; + GBFreq[32][5] = 457; + GBFreq[26][89] = 456; + GBFreq[23][87] = 455; + GBFreq[20][39] = 454; + GBFreq[27][23] = 453; + GBFreq[25][59] = 452; + GBFreq[49][20] = 451; + GBFreq[54][77] = 450; + GBFreq[27][67] = 449; + GBFreq[47][33] = 448; + GBFreq[41][17] = 447; + GBFreq[19][81] = 446; + GBFreq[16][66] = 445; + GBFreq[45][26] = 444; + GBFreq[49][81] = 443; + GBFreq[53][55] = 442; + GBFreq[16][26] = 441; + GBFreq[54][62] = 440; + GBFreq[20][70] = 439; + GBFreq[42][35] = 438; + GBFreq[20][57] = 437; + GBFreq[34][36] = 436; + GBFreq[46][63] = 435; + GBFreq[19][45] = 434; + GBFreq[21][10] = 433; + GBFreq[52][93] = 432; + GBFreq[25][2] = 431; + GBFreq[30][57] = 430; + GBFreq[41][24] = 429; + GBFreq[28][43] = 428; + GBFreq[45][86] = 427; + GBFreq[51][56] = 426; + GBFreq[37][28] = 425; + GBFreq[52][69] = 424; + GBFreq[43][92] = 423; + GBFreq[41][31] = 422; + GBFreq[37][87] = 421; + GBFreq[47][36] = 420; + GBFreq[16][16] = 419; + GBFreq[40][56] = 418; + GBFreq[24][55] = 417; + GBFreq[17][1] = 416; + GBFreq[35][57] = 415; + GBFreq[27][50] = 414; + GBFreq[26][14] = 413; + GBFreq[50][40] = 412; + GBFreq[39][19] = 411; + GBFreq[19][89] = 410; + GBFreq[29][91] = 409; + GBFreq[17][89] = 408; + GBFreq[39][74] = 407; + GBFreq[46][39] = 406; + GBFreq[40][28] = 405; + GBFreq[45][68] = 404; + GBFreq[43][10] = 403; + GBFreq[42][13] = 402; + GBFreq[44][81] = 401; + GBFreq[41][47] = 400; + GBFreq[48][58] = 399; + GBFreq[43][68] = 398; + GBFreq[16][79] = 397; + GBFreq[19][5] = 396; + GBFreq[54][59] = 395; + GBFreq[17][36] = 394; + GBFreq[18][0] = 393; + GBFreq[41][5] = 392; + GBFreq[41][72] = 391; + GBFreq[16][39] = 390; + GBFreq[54][0] = 389; + GBFreq[51][16] = 388; + GBFreq[29][36] = 387; + GBFreq[47][5] = 386; + GBFreq[47][51] = 385; + GBFreq[44][7] = 384; + GBFreq[35][30] = 383; + GBFreq[26][9] = 382; + GBFreq[16][7] = 381; + GBFreq[32][1] = 380; + GBFreq[33][76] = 379; + GBFreq[34][91] = 378; + GBFreq[52][36] = 377; + GBFreq[26][77] = 376; + GBFreq[35][48] = 375; + GBFreq[40][80] = 374; + GBFreq[41][92] = 373; + GBFreq[27][93] = 372; + GBFreq[15][17] = 371; + GBFreq[16][76] = 370; + GBFreq[51][12] = 369; + GBFreq[18][20] = 368; + GBFreq[15][54] = 367; + GBFreq[50][5] = 366; + GBFreq[33][22] = 365; + GBFreq[37][57] = 364; + GBFreq[28][47] = 363; + GBFreq[42][31] = 362; + GBFreq[18][2] = 361; + GBFreq[43][64] = 360; + GBFreq[23][47] = 359; + GBFreq[28][79] = 358; + GBFreq[25][45] = 357; + GBFreq[23][91] = 356; + GBFreq[22][19] = 355; + GBFreq[25][46] = 354; + GBFreq[22][36] = 353; + GBFreq[54][85] = 352; + GBFreq[46][20] = 351; + GBFreq[27][37] = 350; + GBFreq[26][81] = 349; + GBFreq[42][29] = 348; + GBFreq[31][90] = 347; + GBFreq[41][59] = 346; + GBFreq[24][65] = 345; + GBFreq[44][84] = 344; + GBFreq[24][90] = 343; + GBFreq[38][54] = 342; + GBFreq[28][70] = 341; + GBFreq[27][15] = 340; + GBFreq[28][80] = 339; + GBFreq[29][8] = 338; + GBFreq[45][80] = 337; + GBFreq[53][37] = 336; + GBFreq[28][65] = 335; + GBFreq[23][86] = 334; + GBFreq[39][45] = 333; + GBFreq[53][32] = 332; + GBFreq[38][68] = 331; + GBFreq[45][78] = 330; + GBFreq[43][7] = 329; + GBFreq[46][82] = 328; + GBFreq[27][38] = 327; + GBFreq[16][62] = 326; + GBFreq[24][17] = 325; + GBFreq[22][70] = 324; + GBFreq[52][28] = 323; + GBFreq[23][40] = 322; + GBFreq[28][50] = 321; + GBFreq[42][91] = 320; + GBFreq[47][76] = 319; + GBFreq[15][42] = 318; + GBFreq[43][55] = 317; + GBFreq[29][84] = 316; + GBFreq[44][90] = 315; + GBFreq[53][16] = 314; + GBFreq[22][93] = 313; + GBFreq[34][10] = 312; + GBFreq[32][53] = 311; + GBFreq[43][65] = 310; + GBFreq[28][7] = 309; + GBFreq[35][46] = 308; + GBFreq[21][39] = 307; + GBFreq[44][18] = 306; + GBFreq[40][10] = 305; + GBFreq[54][53] = 304; + GBFreq[38][74] = 303; + GBFreq[28][26] = 302; + GBFreq[15][13] = 301; + GBFreq[39][34] = 300; + GBFreq[39][46] = 299; + GBFreq[42][66] = 298; + GBFreq[33][58] = 297; + GBFreq[15][56] = 296; + GBFreq[18][51] = 295; + GBFreq[49][68] = 294; + GBFreq[30][37] = 293; + GBFreq[51][84] = 292; + GBFreq[51][9] = 291; + GBFreq[40][70] = 290; + GBFreq[41][84] = 289; + GBFreq[28][64] = 288; + GBFreq[32][88] = 287; + GBFreq[24][5] = 286; + GBFreq[53][23] = 285; + GBFreq[42][27] = 284; + GBFreq[22][38] = 283; + GBFreq[32][86] = 282; + GBFreq[34][30] = 281; + GBFreq[38][63] = 280; + GBFreq[24][59] = 279; + GBFreq[22][81] = 278; + GBFreq[32][11] = 277; + GBFreq[51][21] = 276; + GBFreq[54][41] = 275; + GBFreq[21][50] = 274; + GBFreq[23][89] = 273; + GBFreq[19][87] = 272; + GBFreq[26][7] = 271; + GBFreq[30][75] = 270; + GBFreq[43][84] = 269; + GBFreq[51][25] = 268; + GBFreq[16][67] = 267; + GBFreq[32][9] = 266; + GBFreq[48][51] = 265; + GBFreq[39][7] = 264; + GBFreq[44][88] = 263; + GBFreq[52][24] = 262; + GBFreq[23][34] = 261; + GBFreq[32][75] = 260; + GBFreq[19][10] = 259; + GBFreq[28][91] = 258; + GBFreq[32][83] = 257; + GBFreq[25][75] = 256; + GBFreq[53][45] = 255; + GBFreq[29][85] = 254; + GBFreq[53][59] = 253; + GBFreq[16][2] = 252; + GBFreq[19][78] = 251; + GBFreq[15][75] = 250; + GBFreq[51][42] = 249; + GBFreq[45][67] = 248; + GBFreq[15][74] = 247; + GBFreq[25][81] = 246; + GBFreq[37][62] = 245; + GBFreq[16][55] = 244; + GBFreq[18][38] = 243; + GBFreq[23][23] = 242; + GBFreq[38][30] = 241; + GBFreq[17][28] = 240; + GBFreq[44][73] = 239; + GBFreq[23][78] = 238; + GBFreq[40][77] = 237; + GBFreq[38][87] = 236; + GBFreq[27][19] = 235; + GBFreq[38][82] = 234; + GBFreq[37][22] = 233; + GBFreq[41][30] = 232; + GBFreq[54][9] = 231; + GBFreq[32][30] = 230; + GBFreq[30][52] = 229; + GBFreq[40][84] = 228; + GBFreq[53][57] = 227; + GBFreq[27][27] = 226; + GBFreq[38][64] = 225; + GBFreq[18][43] = 224; + GBFreq[23][69] = 223; + GBFreq[28][12] = 222; + GBFreq[50][78] = 221; + GBFreq[50][1] = 220; + GBFreq[26][88] = 219; + GBFreq[36][40] = 218; + GBFreq[33][89] = 217; + GBFreq[41][28] = 216; + GBFreq[31][77] = 215; + GBFreq[46][1] = 214; + GBFreq[47][19] = 213; + GBFreq[35][55] = 212; + GBFreq[41][21] = 211; + GBFreq[27][10] = 210; + GBFreq[32][77] = 209; + GBFreq[26][37] = 208; + GBFreq[20][33] = 207; + GBFreq[41][52] = 206; + GBFreq[32][18] = 205; + GBFreq[38][13] = 204; + GBFreq[20][18] = 203; + GBFreq[20][24] = 202; + GBFreq[45][19] = 201; + GBFreq[18][53] = 200; + + Big5Freq[9][89] = 600; + Big5Freq[11][15] = 599; + Big5Freq[3][66] = 598; + Big5Freq[6][121] = 597; + Big5Freq[3][0] = 596; + Big5Freq[5][82] = 595; + Big5Freq[3][42] = 594; + Big5Freq[5][34] = 593; + Big5Freq[3][8] = 592; + Big5Freq[3][6] = 591; + Big5Freq[3][67] = 590; + Big5Freq[7][139] = 589; + Big5Freq[23][137] = 588; + Big5Freq[12][46] = 587; + Big5Freq[4][8] = 586; + Big5Freq[4][41] = 585; + Big5Freq[18][47] = 584; + Big5Freq[12][114] = 583; + Big5Freq[6][1] = 582; + Big5Freq[22][60] = 581; + Big5Freq[5][46] = 580; + Big5Freq[11][79] = 579; + Big5Freq[3][23] = 578; + Big5Freq[7][114] = 577; + Big5Freq[29][102] = 576; + Big5Freq[19][14] = 575; + Big5Freq[4][133] = 574; + Big5Freq[3][29] = 573; + Big5Freq[4][109] = 572; + Big5Freq[14][127] = 571; + Big5Freq[5][48] = 570; + Big5Freq[13][104] = 569; + Big5Freq[3][132] = 568; + Big5Freq[26][64] = 567; + Big5Freq[7][19] = 566; + Big5Freq[4][12] = 565; + Big5Freq[11][124] = 564; + Big5Freq[7][89] = 563; + Big5Freq[15][124] = 562; + Big5Freq[4][108] = 561; + Big5Freq[19][66] = 560; + Big5Freq[3][21] = 559; + Big5Freq[24][12] = 558; + Big5Freq[28][111] = 557; + Big5Freq[12][107] = 556; + Big5Freq[3][112] = 555; + Big5Freq[8][113] = 554; + Big5Freq[5][40] = 553; + Big5Freq[26][145] = 552; + Big5Freq[3][48] = 551; + Big5Freq[3][70] = 550; + Big5Freq[22][17] = 549; + Big5Freq[16][47] = 548; + Big5Freq[3][53] = 547; + Big5Freq[4][24] = 546; + Big5Freq[32][120] = 545; + Big5Freq[24][49] = 544; + Big5Freq[24][142] = 543; + Big5Freq[18][66] = 542; + Big5Freq[29][150] = 541; + Big5Freq[5][122] = 540; + Big5Freq[5][114] = 539; + Big5Freq[3][44] = 538; + Big5Freq[10][128] = 537; + Big5Freq[15][20] = 536; + Big5Freq[13][33] = 535; + Big5Freq[14][87] = 534; + Big5Freq[3][126] = 533; + Big5Freq[4][53] = 532; + Big5Freq[4][40] = 531; + Big5Freq[9][93] = 530; + Big5Freq[15][137] = 529; + Big5Freq[10][123] = 528; + Big5Freq[4][56] = 527; + Big5Freq[5][71] = 526; + Big5Freq[10][8] = 525; + Big5Freq[5][16] = 524; + Big5Freq[5][146] = 523; + Big5Freq[18][88] = 522; + Big5Freq[24][4] = 521; + Big5Freq[20][47] = 520; + Big5Freq[5][33] = 519; + Big5Freq[9][43] = 518; + Big5Freq[20][12] = 517; + Big5Freq[20][13] = 516; + Big5Freq[5][156] = 515; + Big5Freq[22][140] = 514; + Big5Freq[8][146] = 513; + Big5Freq[21][123] = 512; + Big5Freq[4][90] = 511; + Big5Freq[5][62] = 510; + Big5Freq[17][59] = 509; + Big5Freq[10][37] = 508; + Big5Freq[18][107] = 507; + Big5Freq[14][53] = 506; + Big5Freq[22][51] = 505; + Big5Freq[8][13] = 504; + Big5Freq[5][29] = 503; + Big5Freq[9][7] = 502; + Big5Freq[22][14] = 501; + Big5Freq[8][55] = 500; + Big5Freq[33][9] = 499; + Big5Freq[16][64] = 498; + Big5Freq[7][131] = 497; + Big5Freq[34][4] = 496; + Big5Freq[7][101] = 495; + Big5Freq[11][139] = 494; + Big5Freq[3][135] = 493; + Big5Freq[7][102] = 492; + Big5Freq[17][13] = 491; + Big5Freq[3][20] = 490; + Big5Freq[27][106] = 489; + Big5Freq[5][88] = 488; + Big5Freq[6][33] = 487; + Big5Freq[5][139] = 486; + Big5Freq[6][0] = 485; + Big5Freq[17][58] = 484; + Big5Freq[5][133] = 483; + Big5Freq[9][107] = 482; + Big5Freq[23][39] = 481; + Big5Freq[5][23] = 480; + Big5Freq[3][79] = 479; + Big5Freq[32][97] = 478; + Big5Freq[3][136] = 477; + Big5Freq[4][94] = 476; + Big5Freq[21][61] = 475; + Big5Freq[23][123] = 474; + Big5Freq[26][16] = 473; + Big5Freq[24][137] = 472; + Big5Freq[22][18] = 471; + Big5Freq[5][1] = 470; + Big5Freq[20][119] = 469; + Big5Freq[3][7] = 468; + Big5Freq[10][79] = 467; + Big5Freq[15][105] = 466; + Big5Freq[3][144] = 465; + Big5Freq[12][80] = 464; + Big5Freq[15][73] = 463; + Big5Freq[3][19] = 462; + Big5Freq[8][109] = 461; + Big5Freq[3][15] = 460; + Big5Freq[31][82] = 459; + Big5Freq[3][43] = 458; + Big5Freq[25][119] = 457; + Big5Freq[16][111] = 456; + Big5Freq[7][77] = 455; + Big5Freq[3][95] = 454; + Big5Freq[24][82] = 453; + Big5Freq[7][52] = 452; + Big5Freq[9][151] = 451; + Big5Freq[3][129] = 450; + Big5Freq[5][87] = 449; + Big5Freq[3][55] = 448; + Big5Freq[8][153] = 447; + Big5Freq[4][83] = 446; + Big5Freq[3][114] = 445; + Big5Freq[23][147] = 444; + Big5Freq[15][31] = 443; + Big5Freq[3][54] = 442; + Big5Freq[11][122] = 441; + Big5Freq[4][4] = 440; + Big5Freq[34][149] = 439; + Big5Freq[3][17] = 438; + Big5Freq[21][64] = 437; + Big5Freq[26][144] = 436; + Big5Freq[4][62] = 435; + Big5Freq[8][15] = 434; + Big5Freq[35][80] = 433; + Big5Freq[7][110] = 432; + Big5Freq[23][114] = 431; + Big5Freq[3][108] = 430; + Big5Freq[3][62] = 429; + Big5Freq[21][41] = 428; + Big5Freq[15][99] = 427; + Big5Freq[5][47] = 426; + Big5Freq[4][96] = 425; + Big5Freq[20][122] = 424; + Big5Freq[5][21] = 423; + Big5Freq[4][157] = 422; + Big5Freq[16][14] = 421; + Big5Freq[3][117] = 420; + Big5Freq[7][129] = 419; + Big5Freq[4][27] = 418; + Big5Freq[5][30] = 417; + Big5Freq[22][16] = 416; + Big5Freq[5][64] = 415; + Big5Freq[17][99] = 414; + Big5Freq[17][57] = 413; + Big5Freq[8][105] = 412; + Big5Freq[5][112] = 411; + Big5Freq[20][59] = 410; + Big5Freq[6][129] = 409; + Big5Freq[18][17] = 408; + Big5Freq[3][92] = 407; + Big5Freq[28][118] = 406; + Big5Freq[3][109] = 405; + Big5Freq[31][51] = 404; + Big5Freq[13][116] = 403; + Big5Freq[6][15] = 402; + Big5Freq[36][136] = 401; + Big5Freq[12][74] = 400; + Big5Freq[20][88] = 399; + Big5Freq[36][68] = 398; + Big5Freq[3][147] = 397; + Big5Freq[15][84] = 396; + Big5Freq[16][32] = 395; + Big5Freq[16][58] = 394; + Big5Freq[7][66] = 393; + Big5Freq[23][107] = 392; + Big5Freq[9][6] = 391; + Big5Freq[12][86] = 390; + Big5Freq[23][112] = 389; + Big5Freq[37][23] = 388; + Big5Freq[3][138] = 387; + Big5Freq[20][68] = 386; + Big5Freq[15][116] = 385; + Big5Freq[18][64] = 384; + Big5Freq[12][139] = 383; + Big5Freq[11][155] = 382; + Big5Freq[4][156] = 381; + Big5Freq[12][84] = 380; + Big5Freq[18][49] = 379; + Big5Freq[25][125] = 378; + Big5Freq[25][147] = 377; + Big5Freq[15][110] = 376; + Big5Freq[19][96] = 375; + Big5Freq[30][152] = 374; + Big5Freq[6][31] = 373; + Big5Freq[27][117] = 372; + Big5Freq[3][10] = 371; + Big5Freq[6][131] = 370; + Big5Freq[13][112] = 369; + Big5Freq[36][156] = 368; + Big5Freq[4][60] = 367; + Big5Freq[15][121] = 366; + Big5Freq[4][112] = 365; + Big5Freq[30][142] = 364; + Big5Freq[23][154] = 363; + Big5Freq[27][101] = 362; + Big5Freq[9][140] = 361; + Big5Freq[3][89] = 360; + Big5Freq[18][148] = 359; + Big5Freq[4][69] = 358; + Big5Freq[16][49] = 357; + Big5Freq[6][117] = 356; + Big5Freq[36][55] = 355; + Big5Freq[5][123] = 354; + Big5Freq[4][126] = 353; + Big5Freq[4][119] = 352; + Big5Freq[9][95] = 351; + Big5Freq[5][24] = 350; + Big5Freq[16][133] = 349; + Big5Freq[10][134] = 348; + Big5Freq[26][59] = 347; + Big5Freq[6][41] = 346; + Big5Freq[6][146] = 345; + Big5Freq[19][24] = 344; + Big5Freq[5][113] = 343; + Big5Freq[10][118] = 342; + Big5Freq[34][151] = 341; + Big5Freq[9][72] = 340; + Big5Freq[31][25] = 339; + Big5Freq[18][126] = 338; + Big5Freq[18][28] = 337; + Big5Freq[4][153] = 336; + Big5Freq[3][84] = 335; + Big5Freq[21][18] = 334; + Big5Freq[25][129] = 333; + Big5Freq[6][107] = 332; + Big5Freq[12][25] = 331; + Big5Freq[17][109] = 330; + Big5Freq[7][76] = 329; + Big5Freq[15][15] = 328; + Big5Freq[4][14] = 327; + Big5Freq[23][88] = 326; + Big5Freq[18][2] = 325; + Big5Freq[6][88] = 324; + Big5Freq[16][84] = 323; + Big5Freq[12][48] = 322; + Big5Freq[7][68] = 321; + Big5Freq[5][50] = 320; + Big5Freq[13][54] = 319; + Big5Freq[7][98] = 318; + Big5Freq[11][6] = 317; + Big5Freq[9][80] = 316; + Big5Freq[16][41] = 315; + Big5Freq[7][43] = 314; + Big5Freq[28][117] = 313; + Big5Freq[3][51] = 312; + Big5Freq[7][3] = 311; + Big5Freq[20][81] = 310; + Big5Freq[4][2] = 309; + Big5Freq[11][16] = 308; + Big5Freq[10][4] = 307; + Big5Freq[10][119] = 306; + Big5Freq[6][142] = 305; + Big5Freq[18][51] = 304; + Big5Freq[8][144] = 303; + Big5Freq[10][65] = 302; + Big5Freq[11][64] = 301; + Big5Freq[11][130] = 300; + Big5Freq[9][92] = 299; + Big5Freq[18][29] = 298; + Big5Freq[18][78] = 297; + Big5Freq[18][151] = 296; + Big5Freq[33][127] = 295; + Big5Freq[35][113] = 294; + Big5Freq[10][155] = 293; + Big5Freq[3][76] = 292; + Big5Freq[36][123] = 291; + Big5Freq[13][143] = 290; + Big5Freq[5][135] = 289; + Big5Freq[23][116] = 288; + Big5Freq[6][101] = 287; + Big5Freq[14][74] = 286; + Big5Freq[7][153] = 285; + Big5Freq[3][101] = 284; + Big5Freq[9][74] = 283; + Big5Freq[3][156] = 282; + Big5Freq[4][147] = 281; + Big5Freq[9][12] = 280; + Big5Freq[18][133] = 279; + Big5Freq[4][0] = 278; + Big5Freq[7][155] = 277; + Big5Freq[9][144] = 276; + Big5Freq[23][49] = 275; + Big5Freq[5][89] = 274; + Big5Freq[10][11] = 273; + Big5Freq[3][110] = 272; + Big5Freq[3][40] = 271; + Big5Freq[29][115] = 270; + Big5Freq[9][100] = 269; + Big5Freq[21][67] = 268; + Big5Freq[23][145] = 267; + Big5Freq[10][47] = 266; + Big5Freq[4][31] = 265; + Big5Freq[4][81] = 264; + Big5Freq[22][62] = 263; + Big5Freq[4][28] = 262; + Big5Freq[27][39] = 261; + Big5Freq[27][54] = 260; + Big5Freq[32][46] = 259; + Big5Freq[4][76] = 258; + Big5Freq[26][15] = 257; + Big5Freq[12][154] = 256; + Big5Freq[9][150] = 255; + Big5Freq[15][17] = 254; + Big5Freq[5][129] = 253; + Big5Freq[10][40] = 252; + Big5Freq[13][37] = 251; + Big5Freq[31][104] = 250; + Big5Freq[3][152] = 249; + Big5Freq[5][22] = 248; + Big5Freq[8][48] = 247; + Big5Freq[4][74] = 246; + Big5Freq[6][17] = 245; + Big5Freq[30][82] = 244; + Big5Freq[4][116] = 243; + Big5Freq[16][42] = 242; + Big5Freq[5][55] = 241; + Big5Freq[4][64] = 240; + Big5Freq[14][19] = 239; + Big5Freq[35][82] = 238; + Big5Freq[30][139] = 237; + Big5Freq[26][152] = 236; + Big5Freq[32][32] = 235; + Big5Freq[21][102] = 234; + Big5Freq[10][131] = 233; + Big5Freq[9][128] = 232; + Big5Freq[3][87] = 231; + Big5Freq[4][51] = 230; + Big5Freq[10][15] = 229; + Big5Freq[4][150] = 228; + Big5Freq[7][4] = 227; + Big5Freq[7][51] = 226; + Big5Freq[7][157] = 225; + Big5Freq[4][146] = 224; + Big5Freq[4][91] = 223; + Big5Freq[7][13] = 222; + Big5Freq[17][116] = 221; + Big5Freq[23][21] = 220; + Big5Freq[5][106] = 219; + Big5Freq[14][100] = 218; + Big5Freq[10][152] = 217; + Big5Freq[14][89] = 216; + Big5Freq[6][138] = 215; + Big5Freq[12][157] = 214; + Big5Freq[10][102] = 213; + Big5Freq[19][94] = 212; + Big5Freq[7][74] = 211; + Big5Freq[18][128] = 210; + Big5Freq[27][111] = 209; + Big5Freq[11][57] = 208; + Big5Freq[3][131] = 207; + Big5Freq[30][23] = 206; + Big5Freq[30][126] = 205; + Big5Freq[4][36] = 204; + Big5Freq[26][124] = 203; + Big5Freq[4][19] = 202; + Big5Freq[9][152] = 201; + + EUC_TWFreq[48][49] = 599; + EUC_TWFreq[35][65] = 598; + EUC_TWFreq[41][27] = 597; + EUC_TWFreq[35][0] = 596; + EUC_TWFreq[39][19] = 595; + EUC_TWFreq[35][42] = 594; + EUC_TWFreq[38][66] = 593; + EUC_TWFreq[35][8] = 592; + EUC_TWFreq[35][6] = 591; + EUC_TWFreq[35][66] = 590; + EUC_TWFreq[43][14] = 589; + EUC_TWFreq[69][80] = 588; + EUC_TWFreq[50][48] = 587; + EUC_TWFreq[36][71] = 586; + EUC_TWFreq[37][10] = 585; + EUC_TWFreq[60][52] = 584; + EUC_TWFreq[51][21] = 583; + EUC_TWFreq[40][2] = 582; + EUC_TWFreq[67][35] = 581; + EUC_TWFreq[38][78] = 580; + EUC_TWFreq[49][18] = 579; + EUC_TWFreq[35][23] = 578; + EUC_TWFreq[42][83] = 577; + EUC_TWFreq[79][47] = 576; + EUC_TWFreq[61][82] = 575; + EUC_TWFreq[38][7] = 574; + EUC_TWFreq[35][29] = 573; + EUC_TWFreq[37][77] = 572; + EUC_TWFreq[54][67] = 571; + EUC_TWFreq[38][80] = 570; + EUC_TWFreq[52][74] = 569; + EUC_TWFreq[36][37] = 568; + EUC_TWFreq[74][8] = 567; + EUC_TWFreq[41][83] = 566; + EUC_TWFreq[36][75] = 565; + EUC_TWFreq[49][63] = 564; + EUC_TWFreq[42][58] = 563; + EUC_TWFreq[56][33] = 562; + EUC_TWFreq[37][76] = 561; + EUC_TWFreq[62][39] = 560; + EUC_TWFreq[35][21] = 559; + EUC_TWFreq[70][19] = 558; + EUC_TWFreq[77][88] = 557; + EUC_TWFreq[51][14] = 556; + EUC_TWFreq[36][17] = 555; + EUC_TWFreq[44][51] = 554; + EUC_TWFreq[38][72] = 553; + EUC_TWFreq[74][90] = 552; + EUC_TWFreq[35][48] = 551; + EUC_TWFreq[35][69] = 550; + EUC_TWFreq[66][86] = 549; + EUC_TWFreq[57][20] = 548; + EUC_TWFreq[35][53] = 547; + EUC_TWFreq[36][87] = 546; + EUC_TWFreq[84][67] = 545; + EUC_TWFreq[70][56] = 544; + EUC_TWFreq[71][54] = 543; + EUC_TWFreq[60][70] = 542; + EUC_TWFreq[80][1] = 541; + EUC_TWFreq[39][59] = 540; + EUC_TWFreq[39][51] = 539; + EUC_TWFreq[35][44] = 538; + EUC_TWFreq[48][4] = 537; + EUC_TWFreq[55][24] = 536; + EUC_TWFreq[52][4] = 535; + EUC_TWFreq[54][26] = 534; + EUC_TWFreq[36][31] = 533; + EUC_TWFreq[37][22] = 532; + EUC_TWFreq[37][9] = 531; + EUC_TWFreq[46][0] = 530; + EUC_TWFreq[56][46] = 529; + EUC_TWFreq[47][93] = 528; + EUC_TWFreq[37][25] = 527; + EUC_TWFreq[39][8] = 526; + EUC_TWFreq[46][73] = 525; + EUC_TWFreq[38][48] = 524; + EUC_TWFreq[39][83] = 523; + EUC_TWFreq[60][92] = 522; + EUC_TWFreq[70][11] = 521; + EUC_TWFreq[63][84] = 520; + EUC_TWFreq[38][65] = 519; + EUC_TWFreq[45][45] = 518; + EUC_TWFreq[63][49] = 517; + EUC_TWFreq[63][50] = 516; + EUC_TWFreq[39][93] = 515; + EUC_TWFreq[68][20] = 514; + EUC_TWFreq[44][84] = 513; + EUC_TWFreq[66][34] = 512; + EUC_TWFreq[37][58] = 511; + EUC_TWFreq[39][0] = 510; + EUC_TWFreq[59][1] = 509; + EUC_TWFreq[47][8] = 508; + EUC_TWFreq[61][17] = 507; + EUC_TWFreq[53][87] = 506; + EUC_TWFreq[67][26] = 505; + EUC_TWFreq[43][46] = 504; + EUC_TWFreq[38][61] = 503; + EUC_TWFreq[45][9] = 502; + EUC_TWFreq[66][83] = 501; + EUC_TWFreq[43][88] = 500; + EUC_TWFreq[85][20] = 499; + EUC_TWFreq[57][36] = 498; + EUC_TWFreq[43][6] = 497; + EUC_TWFreq[86][77] = 496; + EUC_TWFreq[42][70] = 495; + EUC_TWFreq[49][78] = 494; + EUC_TWFreq[36][40] = 493; + EUC_TWFreq[42][71] = 492; + EUC_TWFreq[58][49] = 491; + EUC_TWFreq[35][20] = 490; + EUC_TWFreq[76][20] = 489; + EUC_TWFreq[39][25] = 488; + EUC_TWFreq[40][34] = 487; + EUC_TWFreq[39][76] = 486; + EUC_TWFreq[40][1] = 485; + EUC_TWFreq[59][0] = 484; + EUC_TWFreq[39][70] = 483; + EUC_TWFreq[46][14] = 482; + EUC_TWFreq[68][77] = 481; + EUC_TWFreq[38][55] = 480; + EUC_TWFreq[35][78] = 479; + EUC_TWFreq[84][44] = 478; + EUC_TWFreq[36][41] = 477; + EUC_TWFreq[37][62] = 476; + EUC_TWFreq[65][67] = 475; + EUC_TWFreq[69][66] = 474; + EUC_TWFreq[73][55] = 473; + EUC_TWFreq[71][49] = 472; + EUC_TWFreq[66][87] = 471; + EUC_TWFreq[38][33] = 470; + EUC_TWFreq[64][61] = 469; + EUC_TWFreq[35][7] = 468; + EUC_TWFreq[47][49] = 467; + EUC_TWFreq[56][14] = 466; + EUC_TWFreq[36][49] = 465; + EUC_TWFreq[50][81] = 464; + EUC_TWFreq[55][76] = 463; + EUC_TWFreq[35][19] = 462; + EUC_TWFreq[44][47] = 461; + EUC_TWFreq[35][15] = 460; + EUC_TWFreq[82][59] = 459; + EUC_TWFreq[35][43] = 458; + EUC_TWFreq[73][0] = 457; + EUC_TWFreq[57][83] = 456; + EUC_TWFreq[42][46] = 455; + EUC_TWFreq[36][0] = 454; + EUC_TWFreq[70][88] = 453; + EUC_TWFreq[42][22] = 452; + EUC_TWFreq[46][58] = 451; + EUC_TWFreq[36][34] = 450; + EUC_TWFreq[39][24] = 449; + EUC_TWFreq[35][55] = 448; + EUC_TWFreq[44][91] = 447; + EUC_TWFreq[37][51] = 446; + EUC_TWFreq[36][19] = 445; + EUC_TWFreq[69][90] = 444; + EUC_TWFreq[55][35] = 443; + EUC_TWFreq[35][54] = 442; + EUC_TWFreq[49][61] = 441; + EUC_TWFreq[36][67] = 440; + EUC_TWFreq[88][34] = 439; + EUC_TWFreq[35][17] = 438; + EUC_TWFreq[65][69] = 437; + EUC_TWFreq[74][89] = 436; + EUC_TWFreq[37][31] = 435; + EUC_TWFreq[43][48] = 434; + EUC_TWFreq[89][27] = 433; + EUC_TWFreq[42][79] = 432; + EUC_TWFreq[69][57] = 431; + EUC_TWFreq[36][13] = 430; + EUC_TWFreq[35][62] = 429; + EUC_TWFreq[65][47] = 428; + EUC_TWFreq[56][8] = 427; + EUC_TWFreq[38][79] = 426; + EUC_TWFreq[37][64] = 425; + EUC_TWFreq[64][64] = 424; + EUC_TWFreq[38][53] = 423; + EUC_TWFreq[38][31] = 422; + EUC_TWFreq[56][81] = 421; + EUC_TWFreq[36][22] = 420; + EUC_TWFreq[43][4] = 419; + EUC_TWFreq[36][90] = 418; + EUC_TWFreq[38][62] = 417; + EUC_TWFreq[66][85] = 416; + EUC_TWFreq[39][1] = 415; + EUC_TWFreq[59][40] = 414; + EUC_TWFreq[58][93] = 413; + EUC_TWFreq[44][43] = 412; + EUC_TWFreq[39][49] = 411; + EUC_TWFreq[64][2] = 410; + EUC_TWFreq[41][35] = 409; + EUC_TWFreq[60][22] = 408; + EUC_TWFreq[35][91] = 407; + EUC_TWFreq[78][1] = 406; + EUC_TWFreq[36][14] = 405; + EUC_TWFreq[82][29] = 404; + EUC_TWFreq[52][86] = 403; + EUC_TWFreq[40][16] = 402; + EUC_TWFreq[91][52] = 401; + EUC_TWFreq[50][75] = 400; + EUC_TWFreq[64][30] = 399; + EUC_TWFreq[90][78] = 398; + EUC_TWFreq[36][52] = 397; + EUC_TWFreq[55][87] = 396; + EUC_TWFreq[57][5] = 395; + EUC_TWFreq[57][31] = 394; + EUC_TWFreq[42][35] = 393; + EUC_TWFreq[69][50] = 392; + EUC_TWFreq[45][8] = 391; + EUC_TWFreq[50][87] = 390; + EUC_TWFreq[69][55] = 389; + EUC_TWFreq[92][3] = 388; + EUC_TWFreq[36][43] = 387; + EUC_TWFreq[64][10] = 386; + EUC_TWFreq[56][25] = 385; + EUC_TWFreq[60][68] = 384; + EUC_TWFreq[51][46] = 383; + EUC_TWFreq[50][0] = 382; + EUC_TWFreq[38][30] = 381; + EUC_TWFreq[50][85] = 380; + EUC_TWFreq[60][54] = 379; + EUC_TWFreq[73][6] = 378; + EUC_TWFreq[73][28] = 377; + EUC_TWFreq[56][19] = 376; + EUC_TWFreq[62][69] = 375; + EUC_TWFreq[81][66] = 374; + EUC_TWFreq[40][32] = 373; + EUC_TWFreq[76][31] = 372; + EUC_TWFreq[35][10] = 371; + EUC_TWFreq[41][37] = 370; + EUC_TWFreq[52][82] = 369; + EUC_TWFreq[91][72] = 368; + EUC_TWFreq[37][29] = 367; + EUC_TWFreq[56][30] = 366; + EUC_TWFreq[37][80] = 365; + EUC_TWFreq[81][56] = 364; + EUC_TWFreq[70][3] = 363; + EUC_TWFreq[76][15] = 362; + EUC_TWFreq[46][47] = 361; + EUC_TWFreq[35][88] = 360; + EUC_TWFreq[61][58] = 359; + EUC_TWFreq[37][37] = 358; + EUC_TWFreq[57][22] = 357; + EUC_TWFreq[41][23] = 356; + EUC_TWFreq[90][66] = 355; + EUC_TWFreq[39][60] = 354; + EUC_TWFreq[38][0] = 353; + EUC_TWFreq[37][87] = 352; + EUC_TWFreq[46][2] = 351; + EUC_TWFreq[38][56] = 350; + EUC_TWFreq[58][11] = 349; + EUC_TWFreq[48][10] = 348; + EUC_TWFreq[74][4] = 347; + EUC_TWFreq[40][42] = 346; + EUC_TWFreq[41][52] = 345; + EUC_TWFreq[61][92] = 344; + EUC_TWFreq[39][50] = 343; + EUC_TWFreq[47][88] = 342; + EUC_TWFreq[88][36] = 341; + EUC_TWFreq[45][73] = 340; + EUC_TWFreq[82][3] = 339; + EUC_TWFreq[61][36] = 338; + EUC_TWFreq[60][33] = 337; + EUC_TWFreq[38][27] = 336; + EUC_TWFreq[35][83] = 335; + EUC_TWFreq[65][24] = 334; + EUC_TWFreq[73][10] = 333; + EUC_TWFreq[41][13] = 332; + EUC_TWFreq[50][27] = 331; + EUC_TWFreq[59][50] = 330; + EUC_TWFreq[42][45] = 329; + EUC_TWFreq[55][19] = 328; + EUC_TWFreq[36][77] = 327; + EUC_TWFreq[69][31] = 326; + EUC_TWFreq[60][7] = 325; + EUC_TWFreq[40][88] = 324; + EUC_TWFreq[57][56] = 323; + EUC_TWFreq[50][50] = 322; + EUC_TWFreq[42][37] = 321; + EUC_TWFreq[38][82] = 320; + EUC_TWFreq[52][25] = 319; + EUC_TWFreq[42][67] = 318; + EUC_TWFreq[48][40] = 317; + EUC_TWFreq[45][81] = 316; + EUC_TWFreq[57][14] = 315; + EUC_TWFreq[42][13] = 314; + EUC_TWFreq[78][0] = 313; + EUC_TWFreq[35][51] = 312; + EUC_TWFreq[41][67] = 311; + EUC_TWFreq[64][23] = 310; + EUC_TWFreq[36][65] = 309; + EUC_TWFreq[48][50] = 308; + EUC_TWFreq[46][69] = 307; + EUC_TWFreq[47][89] = 306; + EUC_TWFreq[41][48] = 305; + EUC_TWFreq[60][56] = 304; + EUC_TWFreq[44][82] = 303; + EUC_TWFreq[47][35] = 302; + EUC_TWFreq[49][3] = 301; + EUC_TWFreq[49][69] = 300; + EUC_TWFreq[45][93] = 299; + EUC_TWFreq[60][34] = 298; + EUC_TWFreq[60][82] = 297; + EUC_TWFreq[61][61] = 296; + EUC_TWFreq[86][42] = 295; + EUC_TWFreq[89][60] = 294; + EUC_TWFreq[48][31] = 293; + EUC_TWFreq[35][75] = 292; + EUC_TWFreq[91][39] = 291; + EUC_TWFreq[53][19] = 290; + EUC_TWFreq[39][72] = 289; + EUC_TWFreq[69][59] = 288; + EUC_TWFreq[41][7] = 287; + EUC_TWFreq[54][13] = 286; + EUC_TWFreq[43][28] = 285; + EUC_TWFreq[36][6] = 284; + EUC_TWFreq[45][75] = 283; + EUC_TWFreq[36][61] = 282; + EUC_TWFreq[38][21] = 281; + EUC_TWFreq[45][14] = 280; + EUC_TWFreq[61][43] = 279; + EUC_TWFreq[36][63] = 278; + EUC_TWFreq[43][30] = 277; + EUC_TWFreq[46][51] = 276; + EUC_TWFreq[68][87] = 275; + EUC_TWFreq[39][26] = 274; + EUC_TWFreq[46][76] = 273; + EUC_TWFreq[36][15] = 272; + EUC_TWFreq[35][40] = 271; + EUC_TWFreq[79][60] = 270; + EUC_TWFreq[46][7] = 269; + EUC_TWFreq[65][72] = 268; + EUC_TWFreq[69][88] = 267; + EUC_TWFreq[47][18] = 266; + EUC_TWFreq[37][0] = 265; + EUC_TWFreq[37][49] = 264; + EUC_TWFreq[67][37] = 263; + EUC_TWFreq[36][91] = 262; + EUC_TWFreq[75][48] = 261; + EUC_TWFreq[75][63] = 260; + EUC_TWFreq[83][87] = 259; + EUC_TWFreq[37][44] = 258; + EUC_TWFreq[73][54] = 257; + EUC_TWFreq[51][61] = 256; + EUC_TWFreq[46][57] = 255; + EUC_TWFreq[55][21] = 254; + EUC_TWFreq[39][66] = 253; + EUC_TWFreq[47][11] = 252; + EUC_TWFreq[52][8] = 251; + EUC_TWFreq[82][81] = 250; + EUC_TWFreq[36][57] = 249; + EUC_TWFreq[38][54] = 248; + EUC_TWFreq[43][81] = 247; + EUC_TWFreq[37][42] = 246; + EUC_TWFreq[40][18] = 245; + EUC_TWFreq[80][90] = 244; + EUC_TWFreq[37][84] = 243; + EUC_TWFreq[57][15] = 242; + EUC_TWFreq[38][87] = 241; + EUC_TWFreq[37][32] = 240; + EUC_TWFreq[53][53] = 239; + EUC_TWFreq[89][29] = 238; + EUC_TWFreq[81][53] = 237; + EUC_TWFreq[75][3] = 236; + EUC_TWFreq[83][73] = 235; + EUC_TWFreq[66][13] = 234; + EUC_TWFreq[48][7] = 233; + EUC_TWFreq[46][35] = 232; + EUC_TWFreq[35][86] = 231; + EUC_TWFreq[37][20] = 230; + EUC_TWFreq[46][80] = 229; + EUC_TWFreq[38][24] = 228; + EUC_TWFreq[41][68] = 227; + EUC_TWFreq[42][21] = 226; + EUC_TWFreq[43][32] = 225; + EUC_TWFreq[38][20] = 224; + EUC_TWFreq[37][59] = 223; + EUC_TWFreq[41][77] = 222; + EUC_TWFreq[59][57] = 221; + EUC_TWFreq[68][59] = 220; + EUC_TWFreq[39][43] = 219; + EUC_TWFreq[54][39] = 218; + EUC_TWFreq[48][28] = 217; + EUC_TWFreq[54][28] = 216; + EUC_TWFreq[41][44] = 215; + EUC_TWFreq[51][64] = 214; + EUC_TWFreq[47][72] = 213; + EUC_TWFreq[62][67] = 212; + EUC_TWFreq[42][43] = 211; + EUC_TWFreq[61][38] = 210; + EUC_TWFreq[76][25] = 209; + EUC_TWFreq[48][91] = 208; + EUC_TWFreq[36][36] = 207; + EUC_TWFreq[80][32] = 206; + EUC_TWFreq[81][40] = 205; + EUC_TWFreq[37][5] = 204; + EUC_TWFreq[74][69] = 203; + EUC_TWFreq[36][82] = 202; + EUC_TWFreq[46][59] = 201; + + GBKFreq[52][132] = 600; + GBKFreq[73][135] = 599; + GBKFreq[49][123] = 598; + GBKFreq[77][146] = 597; + GBKFreq[81][123] = 596; + GBKFreq[82][144] = 595; + GBKFreq[51][179] = 594; + GBKFreq[83][154] = 593; + GBKFreq[71][139] = 592; + GBKFreq[64][139] = 591; + GBKFreq[85][144] = 590; + GBKFreq[52][125] = 589; + GBKFreq[88][25] = 588; + GBKFreq[81][106] = 587; + GBKFreq[81][148] = 586; + GBKFreq[62][137] = 585; + GBKFreq[94][0] = 584; + GBKFreq[1][64] = 583; + GBKFreq[67][163] = 582; + GBKFreq[20][190] = 581; + GBKFreq[57][131] = 580; + GBKFreq[29][169] = 579; + GBKFreq[72][143] = 578; + GBKFreq[0][173] = 577; + GBKFreq[11][23] = 576; + GBKFreq[61][141] = 575; + GBKFreq[60][123] = 574; + GBKFreq[81][114] = 573; + GBKFreq[82][131] = 572; + GBKFreq[67][156] = 571; + GBKFreq[71][167] = 570; + GBKFreq[20][50] = 569; + GBKFreq[77][132] = 568; + GBKFreq[84][38] = 567; + GBKFreq[26][29] = 566; + GBKFreq[74][187] = 565; + GBKFreq[62][116] = 564; + GBKFreq[67][135] = 563; + GBKFreq[5][86] = 562; + GBKFreq[72][186] = 561; + GBKFreq[75][161] = 560; + GBKFreq[78][130] = 559; + GBKFreq[94][30] = 558; + GBKFreq[84][72] = 557; + GBKFreq[1][67] = 556; + GBKFreq[75][172] = 555; + GBKFreq[74][185] = 554; + GBKFreq[53][160] = 553; + GBKFreq[123][14] = 552; + GBKFreq[79][97] = 551; + GBKFreq[85][110] = 550; + GBKFreq[78][171] = 549; + GBKFreq[52][131] = 548; + GBKFreq[56][100] = 547; + GBKFreq[50][182] = 546; + GBKFreq[94][64] = 545; + GBKFreq[106][74] = 544; + GBKFreq[11][102] = 543; + GBKFreq[53][124] = 542; + GBKFreq[24][3] = 541; + GBKFreq[86][148] = 540; + GBKFreq[53][184] = 539; + GBKFreq[86][147] = 538; + GBKFreq[96][161] = 537; + GBKFreq[82][77] = 536; + GBKFreq[59][146] = 535; + GBKFreq[84][126] = 534; + GBKFreq[79][132] = 533; + GBKFreq[85][123] = 532; + GBKFreq[71][101] = 531; + GBKFreq[85][106] = 530; + GBKFreq[6][184] = 529; + GBKFreq[57][156] = 528; + GBKFreq[75][104] = 527; + GBKFreq[50][137] = 526; + GBKFreq[79][133] = 525; + GBKFreq[76][108] = 524; + GBKFreq[57][142] = 523; + GBKFreq[84][130] = 522; + GBKFreq[52][128] = 521; + GBKFreq[47][44] = 520; + GBKFreq[52][152] = 519; + GBKFreq[54][104] = 518; + GBKFreq[30][47] = 517; + GBKFreq[71][123] = 516; + GBKFreq[52][107] = 515; + GBKFreq[45][84] = 514; + GBKFreq[107][118] = 513; + GBKFreq[5][161] = 512; + GBKFreq[48][126] = 511; + GBKFreq[67][170] = 510; + GBKFreq[43][6] = 509; + GBKFreq[70][112] = 508; + GBKFreq[86][174] = 507; + GBKFreq[84][166] = 506; + GBKFreq[79][130] = 505; + GBKFreq[57][141] = 504; + GBKFreq[81][178] = 503; + GBKFreq[56][187] = 502; + GBKFreq[81][162] = 501; + GBKFreq[53][104] = 500; + GBKFreq[123][35] = 499; + GBKFreq[70][169] = 498; + GBKFreq[69][164] = 497; + GBKFreq[109][61] = 496; + GBKFreq[73][130] = 495; + GBKFreq[62][134] = 494; + GBKFreq[54][125] = 493; + GBKFreq[79][105] = 492; + GBKFreq[70][165] = 491; + GBKFreq[71][189] = 490; + GBKFreq[23][147] = 489; + GBKFreq[51][139] = 488; + GBKFreq[47][137] = 487; + GBKFreq[77][123] = 486; + GBKFreq[86][183] = 485; + GBKFreq[63][173] = 484; + GBKFreq[79][144] = 483; + GBKFreq[84][159] = 482; + GBKFreq[60][91] = 481; + GBKFreq[66][187] = 480; + GBKFreq[73][114] = 479; + GBKFreq[85][56] = 478; + GBKFreq[71][149] = 477; + GBKFreq[84][189] = 476; + GBKFreq[104][31] = 475; + GBKFreq[83][82] = 474; + GBKFreq[68][35] = 473; + GBKFreq[11][77] = 472; + GBKFreq[15][155] = 471; + GBKFreq[83][153] = 470; + GBKFreq[71][1] = 469; + GBKFreq[53][190] = 468; + GBKFreq[50][135] = 467; + GBKFreq[3][147] = 466; + GBKFreq[48][136] = 465; + GBKFreq[66][166] = 464; + GBKFreq[55][159] = 463; + GBKFreq[82][150] = 462; + GBKFreq[58][178] = 461; + GBKFreq[64][102] = 460; + GBKFreq[16][106] = 459; + GBKFreq[68][110] = 458; + GBKFreq[54][14] = 457; + GBKFreq[60][140] = 456; + GBKFreq[91][71] = 455; + GBKFreq[54][150] = 454; + GBKFreq[78][177] = 453; + GBKFreq[78][117] = 452; + GBKFreq[104][12] = 451; + GBKFreq[73][150] = 450; + GBKFreq[51][142] = 449; + GBKFreq[81][145] = 448; + GBKFreq[66][183] = 447; + GBKFreq[51][178] = 446; + GBKFreq[75][107] = 445; + GBKFreq[65][119] = 444; + GBKFreq[69][176] = 443; + GBKFreq[59][122] = 442; + GBKFreq[78][160] = 441; + GBKFreq[85][183] = 440; + GBKFreq[105][16] = 439; + GBKFreq[73][110] = 438; + GBKFreq[104][39] = 437; + GBKFreq[119][16] = 436; + GBKFreq[76][162] = 435; + GBKFreq[67][152] = 434; + GBKFreq[82][24] = 433; + GBKFreq[73][121] = 432; + GBKFreq[83][83] = 431; + GBKFreq[82][145] = 430; + GBKFreq[49][133] = 429; + GBKFreq[94][13] = 428; + GBKFreq[58][139] = 427; + GBKFreq[74][189] = 426; + GBKFreq[66][177] = 425; + GBKFreq[85][184] = 424; + GBKFreq[55][183] = 423; + GBKFreq[71][107] = 422; + GBKFreq[11][98] = 421; + GBKFreq[72][153] = 420; + GBKFreq[2][137] = 419; + GBKFreq[59][147] = 418; + GBKFreq[58][152] = 417; + GBKFreq[55][144] = 416; + GBKFreq[73][125] = 415; + GBKFreq[52][154] = 414; + GBKFreq[70][178] = 413; + GBKFreq[79][148] = 412; + GBKFreq[63][143] = 411; + GBKFreq[50][140] = 410; + GBKFreq[47][145] = 409; + GBKFreq[48][123] = 408; + GBKFreq[56][107] = 407; + GBKFreq[84][83] = 406; + GBKFreq[59][112] = 405; + GBKFreq[124][72] = 404; + GBKFreq[79][99] = 403; + GBKFreq[3][37] = 402; + GBKFreq[114][55] = 401; + GBKFreq[85][152] = 400; + GBKFreq[60][47] = 399; + GBKFreq[65][96] = 398; + GBKFreq[74][110] = 397; + GBKFreq[86][182] = 396; + GBKFreq[50][99] = 395; + GBKFreq[67][186] = 394; + GBKFreq[81][74] = 393; + GBKFreq[80][37] = 392; + GBKFreq[21][60] = 391; + GBKFreq[110][12] = 390; + GBKFreq[60][162] = 389; + GBKFreq[29][115] = 388; + GBKFreq[83][130] = 387; + GBKFreq[52][136] = 386; + GBKFreq[63][114] = 385; + GBKFreq[49][127] = 384; + GBKFreq[83][109] = 383; + GBKFreq[66][128] = 382; + GBKFreq[78][136] = 381; + GBKFreq[81][180] = 380; + GBKFreq[76][104] = 379; + GBKFreq[56][156] = 378; + GBKFreq[61][23] = 377; + GBKFreq[4][30] = 376; + GBKFreq[69][154] = 375; + GBKFreq[100][37] = 374; + GBKFreq[54][177] = 373; + GBKFreq[23][119] = 372; + GBKFreq[71][171] = 371; + GBKFreq[84][146] = 370; + GBKFreq[20][184] = 369; + GBKFreq[86][76] = 368; + GBKFreq[74][132] = 367; + GBKFreq[47][97] = 366; + GBKFreq[82][137] = 365; + GBKFreq[94][56] = 364; + GBKFreq[92][30] = 363; + GBKFreq[19][117] = 362; + GBKFreq[48][173] = 361; + GBKFreq[2][136] = 360; + GBKFreq[7][182] = 359; + GBKFreq[74][188] = 358; + GBKFreq[14][132] = 357; + GBKFreq[62][172] = 356; + GBKFreq[25][39] = 355; + GBKFreq[85][129] = 354; + GBKFreq[64][98] = 353; + GBKFreq[67][127] = 352; + GBKFreq[72][167] = 351; + GBKFreq[57][143] = 350; + GBKFreq[76][187] = 349; + GBKFreq[83][181] = 348; + GBKFreq[84][10] = 347; + GBKFreq[55][166] = 346; + GBKFreq[55][188] = 345; + GBKFreq[13][151] = 344; + GBKFreq[62][124] = 343; + GBKFreq[53][136] = 342; + GBKFreq[106][57] = 341; + GBKFreq[47][166] = 340; + GBKFreq[109][30] = 339; + GBKFreq[78][114] = 338; + GBKFreq[83][19] = 337; + GBKFreq[56][162] = 336; + GBKFreq[60][177] = 335; + GBKFreq[88][9] = 334; + GBKFreq[74][163] = 333; + GBKFreq[52][156] = 332; + GBKFreq[71][180] = 331; + GBKFreq[60][57] = 330; + GBKFreq[72][173] = 329; + GBKFreq[82][91] = 328; + GBKFreq[51][186] = 327; + GBKFreq[75][86] = 326; + GBKFreq[75][78] = 325; + GBKFreq[76][170] = 324; + GBKFreq[60][147] = 323; + GBKFreq[82][75] = 322; + GBKFreq[80][148] = 321; + GBKFreq[86][150] = 320; + GBKFreq[13][95] = 319; + GBKFreq[0][11] = 318; + GBKFreq[84][190] = 317; + GBKFreq[76][166] = 316; + GBKFreq[14][72] = 315; + GBKFreq[67][144] = 314; + GBKFreq[84][44] = 313; + GBKFreq[72][125] = 312; + GBKFreq[66][127] = 311; + GBKFreq[60][25] = 310; + GBKFreq[70][146] = 309; + GBKFreq[79][135] = 308; + GBKFreq[54][135] = 307; + GBKFreq[60][104] = 306; + GBKFreq[55][132] = 305; + GBKFreq[94][2] = 304; + GBKFreq[54][133] = 303; + GBKFreq[56][190] = 302; + GBKFreq[58][174] = 301; + GBKFreq[80][144] = 300; + GBKFreq[85][113] = 299; + } + +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/FallbackZipEncoding.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/FallbackZipEncoding.java new file mode 100644 index 0000000..bf8fabd --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/FallbackZipEncoding.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package top.niunaijun.blackobfuscator.core.utils.zip.encoding; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A fallback ZipEncoding, which uses a java.io means to encode names. + *

+ *

This implementation is not favorable for encodings other than + * utf-8, because java.io encodes unmappable character as question + * marks leading to unreadable ZIP entries on some operating + * systems.

+ *

+ *

Furthermore this implementation is unable to tell, whether a + * given name can be safely encoded or not.

+ *

+ *

The methods of this class are reentrant.

+ */ +@SuppressWarnings("ConstantConditions") +class FallbackZipEncoding implements ZipEncoding { + private final String charset; + private final DetectEncoding de; + + /** + * Construct a fallback zip encoding, which uses the platform's + * default charset. + */ + public FallbackZipEncoding() { + this.charset = null; + de = new DetectEncoding(); + } + + /** + * Construct a fallback zip encoding, which uses the given charset. + * + * @param charset The name of the charset or {@code null} for + * the platform's default character set. + */ + public FallbackZipEncoding(final String charset) { + this.charset = charset; + de = charset != null ? null : new DetectEncoding(); + } + + public boolean canEncode(final String name) { + return true; + } + + public ByteBuffer encode(final String name) throws IOException { + if (this.charset == null) { // i.e. use default charset, see no-args constructor + return ByteBuffer.wrap(name.getBytes(de.getEncode())); + } else { + return ByteBuffer.wrap(name.getBytes(this.charset)); + } + } + + public String decode(final byte[] data) throws IOException { + if (this.charset == null) { + de.update(data); + return new String(data, de.getEncode()); + } else { + return new String(data, this.charset); + } + } + + @Override + public String getEncoding() { + if (this.charset == null) + return de.getEncode().name(); + else + return this.charset; + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/ZipEncoding.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/ZipEncoding.java new file mode 100644 index 0000000..0a935ea --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/ZipEncoding.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package top.niunaijun.blackobfuscator.core.utils.zip.encoding; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * An interface for encoders that do a pretty encoding of ZIP + * filenames. + *

+ *

There are mostly two implementations, one that uses java.nio + * {@link java.nio.charset.Charset Charset} and one implementation, + * which copes with simple 8 bit charsets, because java-1.4 did not + * support Cp437 in java.nio.

+ *

+ *

The main reason for defining an own encoding layer comes from + * the problems with {@link String#getBytes(String) + * String.getBytes}, which encodes unknown characters as ASCII + * quotation marks ('?'). Quotation marks are per definition an + * invalid filename on some operating systems like Windows, which + * leads to ignored ZIP entries.

+ *

+ *

All implementations should implement this interface in a + * reentrant way.

+ */ +public interface ZipEncoding { + + /** + * Check, whether the given string may be losslessly encoded using this + * encoding. + * + * @param name A filename or ZIP comment. + * @return Whether the given name may be encoded with out any losses. + */ + boolean canEncode(String name); + + /** + * Encode a filename or a comment to a byte array suitable for + * storing it to a serialized zip entry. + *

+ *

Examples for CP 437 (in pseudo-notation, right hand side is + * C-style notation):

+ *
+     *  encode("\u20AC_for_Dollar.txt") = "%U20AC_for_Dollar.txt"
+     *  encode("\u00D6lf\u00E4sser.txt") = "\231lf\204sser.txt"
+     * 
+ * + * @param name A filename or ZIP comment. + * @return A byte buffer with a backing array containing the + * encoded name. Unmappable characters or malformed + * character sequences are mapped to a sequence of utf-16 + * words encoded in the format %Uxxxx. It is + * assumed, that the byte buffer is positioned at the + * beinning of the encoded result, the byte buffer has a + * backing array and the limit of the byte buffer points + * to the end of the encoded result. + * @throws IOException + */ + ByteBuffer encode(String name) throws IOException; + + /** + * @param data The byte values to decode. + * @return The decoded string. + * @throws IOException + */ + String decode(byte[] data) throws IOException; + + String getEncoding(); + +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/ZipEncodingHelper.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/ZipEncodingHelper.java new file mode 100644 index 0000000..50a5cee --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/encoding/ZipEncodingHelper.java @@ -0,0 +1,32 @@ +package top.niunaijun.blackobfuscator.core.utils.zip.encoding; + +public class ZipEncodingHelper { + public static final ZipEncoding UTF8_ZIP_ENCODING = new FallbackZipEncoding("UTF-8"); + + /** + * name of the encoding UTF-8 + */ + static final String UTF8 = "UTF8"; + + /** + * variant name of the encoding UTF-8 used for comparisons. + */ + private static final String UTF_DASH_8 = "utf-8"; + + /** + * Whether a given encoding - or the platform's default encoding + * if the parameter is null - is UTF-8. + */ + public static boolean isUTF8(String encoding) { + if (encoding == null) { + // check platform's default encoding + encoding = System.getProperty("file.encoding"); + } + return UTF8.equalsIgnoreCase(encoding) + || UTF_DASH_8.equalsIgnoreCase(encoding); + } + + public static ZipEncoding getZipEncoding(String encoding) { + return new FallbackZipEncoding(encoding); + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/AbstractUnicodeExtraField.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/AbstractUnicodeExtraField.java new file mode 100644 index 0000000..b787a64 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/AbstractUnicodeExtraField.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip.extrafield; + +import top.niunaijun.blackobfuscator.core.utils.zip.ZipLong; +import top.niunaijun.blackobfuscator.core.utils.zip.ZipShort; + +import java.io.UnsupportedEncodingException; +import java.util.zip.CRC32; +import java.util.zip.ZipException; + +/** + * A common base class for Unicode extra information extra fields. + */ +public abstract class AbstractUnicodeExtraField implements ZipExtraField { + private long nameCRC32; + private byte[] unicodeName; + private byte[] data; + + protected AbstractUnicodeExtraField() { + } + + /** + * Assemble as unicode extension from the name/comment and + * encoding of the orginal zip entry. + * + * @param text The file name or comment. + * @param bytes The encoded of the filename or comment in the zip + * file. + * @param off The offset of the encoded filename or comment in + * bytes. + * @param len The length of the encoded filename or commentin + * bytes. + */ + protected AbstractUnicodeExtraField(String text, byte[] bytes, int off, + int len) { + CRC32 crc32 = new CRC32(); + crc32.update(bytes, off, len); + nameCRC32 = crc32.getValue(); + + try { + unicodeName = text.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("FATAL: UTF-8 encoding not supported.", + e); + } + } + + /** + * Assemble as unicode extension from the name/comment and + * encoding of the orginal zip entry. + * + * @param text The file name or comment. + * @param bytes The encoded of the filename or comment in the zip + * file. + */ + protected AbstractUnicodeExtraField(String text, byte[] bytes) { + + this(text, bytes, 0, bytes.length); + } + + private void assembleData() { + if (unicodeName == null) { + return; + } + + data = new byte[5 + unicodeName.length]; + // version 1 + data[0] = 0x01; + System.arraycopy(ZipLong.getBytes(nameCRC32), 0, data, 1, 4); + System.arraycopy(unicodeName, 0, data, 5, unicodeName.length); + } + + /** + * @return The CRC32 checksum of the filename or comment as + * encoded in the central directory of the zip file. + */ + public long getNameCRC32() { + return nameCRC32; + } + + /** + * @param nameCRC32 The CRC32 checksum of the filename as encoded + * in the central directory of the zip file to set. + */ + public void setNameCRC32(long nameCRC32) { + this.nameCRC32 = nameCRC32; + data = null; + } + + /** + * @return The utf-8 encoded name. + */ + public byte[] getUnicodeName() { + return unicodeName; + } + + /** + * @param unicodeName The utf-8 encoded name to set. + */ + public void setUnicodeName(byte[] unicodeName) { + this.unicodeName = unicodeName; + data = null; + } + + public byte[] getCentralDirectoryData() { + if (data == null) { + this.assembleData(); + } + return data; + } + + public ZipShort getCentralDirectoryLength() { + if (data == null) { + assembleData(); + } + return new ZipShort(data.length); + } + + public byte[] getLocalFileDataData() { + return getCentralDirectoryData(); + } + + public ZipShort getLocalFileDataLength() { + return getCentralDirectoryLength(); + } + + public void parseFromLocalFileData(byte[] buffer, int offset, int length) + throws ZipException { + + if (length < 5) { + throw new ZipException("UniCode path extra data must have at least" + + " 5 bytes."); + } + + int version = buffer[offset]; + + if (version != 0x01) { + throw new ZipException("Unsupported version [" + version + + "] for UniCode path extra data."); + } + + nameCRC32 = ZipLong.getValue(buffer, offset + 1); + unicodeName = new byte[length - 5]; + System.arraycopy(buffer, offset + 5, unicodeName, 0, length - 5); + data = null; + } + +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/AsiExtraField.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/AsiExtraField.java new file mode 100644 index 0000000..5b57661 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/AsiExtraField.java @@ -0,0 +1,367 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip.extrafield; + +import top.niunaijun.blackobfuscator.core.utils.zip.UnixStat; +import top.niunaijun.blackobfuscator.core.utils.zip.ZipLong; +import top.niunaijun.blackobfuscator.core.utils.zip.ZipShort; + +import java.util.zip.CRC32; +import java.util.zip.ZipException; + + +/** + * Adds Unix file permission and UID/GID fields as well as symbolic + * link handling. + *

+ *

This class uses the ASi extra field in the format: + *

+ *         Value         Size            Description
+ *         -----         ----            -----------
+ * (Unix3) 0x756e        Short           tag for this extra block type
+ *         TSize         Short           total data size for this block
+ *         CRC           Long            CRC-32 of the remaining data
+ *         Mode          Short           file permissions
+ *         SizDev        Long            symlink'd size OR major/minor dev num
+ *         UID           Short           user ID
+ *         GID           Short           group ID
+ *         (var.)        variable        symbolic link filename
+ * 
+ * taken from appnote.iz (Info-ZIP note, 981119) found at ftp://ftp.uu.net/pub/archiving/zip/doc/

+ *

+ *

+ *

Short is two bytes and Long is four bytes in big endian byte and + * word order, device numbers are currently not supported.

+ */ +public class AsiExtraField implements ZipExtraField, UnixStat, Cloneable { + + private static final ZipShort HEADER_ID = new ZipShort(0x756E); + private static final int WORD = 4; + /** + * Standard Unix stat(2) file mode. + * + * @since 1.1 + */ + private int mode = 0; + /** + * User ID. + * + * @since 1.1 + */ + private int uid = 0; + /** + * Group ID. + * + * @since 1.1 + */ + private int gid = 0; + /** + * File this entry points to, if it is a symbolic link. + *

+ *

empty string - if entry is not a symbolic link.

+ * + * @since 1.1 + */ + private String link = ""; + /** + * Is this an entry for a directory? + * + * @since 1.1 + */ + private boolean dirFlag = false; + + /** + * Instance used to calculate checksums. + * + * @since 1.1 + */ + private CRC32 crc = new CRC32(); + + /** + * Constructor for AsiExtraField. + */ + public AsiExtraField() { + } + + /** + * The Header-ID. + * + * @return the value for the header id for this extrafield + * @since 1.1 + */ + public ZipShort getHeaderId() { + return HEADER_ID; + } + + /** + * Length of the extra field in the local file data - without + * Header-ID or length specifier. + * + * @return a ZipShort for the length of the data of this extra field + * @since 1.1 + */ + public ZipShort getLocalFileDataLength() { + return new ZipShort(WORD // CRC + + 2 // Mode + + WORD // SizDev + + 2 // UID + + 2 // GID + + getLinkedFile().getBytes().length); + } + + /** + * Delegate to local file data. + * + * @return the centralDirectory length + * @since 1.1 + */ + public ZipShort getCentralDirectoryLength() { + return getLocalFileDataLength(); + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return get the data + * @since 1.1 + */ + public byte[] getLocalFileDataData() { + // CRC will be added later + byte[] data = new byte[getLocalFileDataLength().getValue() - WORD]; + System.arraycopy(ZipShort.getBytes(getMode()), 0, data, 0, 2); + + byte[] linkArray = getLinkedFile().getBytes(); + // CheckStyle:MagicNumber OFF + System.arraycopy(ZipLong.getBytes(linkArray.length), + 0, data, 2, WORD); + + System.arraycopy(ZipShort.getBytes(getUserId()), + 0, data, 6, 2); + System.arraycopy(ZipShort.getBytes(getGroupId()), + 0, data, 8, 2); + + System.arraycopy(linkArray, 0, data, 10, linkArray.length); + // CheckStyle:MagicNumber ON + + crc.reset(); + crc.update(data); + long checksum = crc.getValue(); + + byte[] result = new byte[data.length + WORD]; + System.arraycopy(ZipLong.getBytes(checksum), 0, result, 0, WORD); + System.arraycopy(data, 0, result, WORD, data.length); + return result; + } + + /** + * Delegate to local file data. + * + * @return the local file data + * @since 1.1 + */ + public byte[] getCentralDirectoryData() { + return getLocalFileDataData(); + } + + /** + * Set the user id. + * + * @param uid the user id + * @since 1.1 + */ + public void setUserId(int uid) { + this.uid = uid; + } + + /** + * Get the user id. + * + * @return the user id + * @since 1.1 + */ + public int getUserId() { + return uid; + } + + /** + * Set the group id. + * + * @param gid the group id + * @since 1.1 + */ + public void setGroupId(int gid) { + this.gid = gid; + } + + /** + * Get the group id. + * + * @return the group id + * @since 1.1 + */ + public int getGroupId() { + return gid; + } + + /** + * Indicate that this entry is a symbolic link to the given filename. + * + * @param name Name of the file this entry links to, empty String + * if it is not a symbolic link. + * @since 1.1 + */ + public void setLinkedFile(String name) { + link = name; + mode = getMode(mode); + } + + /** + * Name of linked file + * + * @return name of the file this entry links to if it is a + * symbolic link, the empty string otherwise. + * @since 1.1 + */ + public String getLinkedFile() { + return link; + } + + /** + * Is this entry a symbolic link? + * + * @return true if this is a symbolic link + * @since 1.1 + */ + public boolean isLink() { + return getLinkedFile().length() != 0; + } + + /** + * File mode of this file. + * + * @param mode the file mode + * @since 1.1 + */ + public void setMode(int mode) { + this.mode = getMode(mode); + } + + /** + * File mode of this file. + * + * @return the file mode + * @since 1.1 + */ + public int getMode() { + return mode; + } + + /** + * Indicate whether this entry is a directory. + * + * @param dirFlag if true, this entry is a directory + * @since 1.1 + */ + public void setDirectory(boolean dirFlag) { + this.dirFlag = dirFlag; + mode = getMode(mode); + } + + /** + * Is this entry a directory? + * + * @return true if this entry is a directory + * @since 1.1 + */ + public boolean isDirectory() { + return dirFlag && !isLink(); + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param data an array of bytes + * @param offset the start offset + * @param length the number of bytes in the array from offset + * @throws ZipException on error + * @since 1.1 + */ + public void parseFromLocalFileData(byte[] data, int offset, int length) + throws ZipException { + + long givenChecksum = ZipLong.getValue(data, offset); + byte[] tmp = new byte[length - WORD]; + System.arraycopy(data, offset + WORD, tmp, 0, length - WORD); + crc.reset(); + crc.update(tmp); + long realChecksum = crc.getValue(); + if (givenChecksum != realChecksum) { + throw new ZipException("bad CRC checksum " + + Long.toHexString(givenChecksum) + + " instead of " + + Long.toHexString(realChecksum)); + } + + int newMode = ZipShort.getValue(tmp, 0); + // CheckStyle:MagicNumber OFF + byte[] linkArray = new byte[(int) ZipLong.getValue(tmp, 2)]; + uid = ZipShort.getValue(tmp, 6); + gid = ZipShort.getValue(tmp, 8); + + if (linkArray.length == 0) { + link = ""; + } else { + System.arraycopy(tmp, 10, linkArray, 0, linkArray.length); + link = new String(linkArray); + } + // CheckStyle:MagicNumber ON + setDirectory((newMode & DIR_FLAG) != 0); + setMode(newMode); + } + + /** + * Get the file mode for given permissions with the correct file type. + * + * @param mode the mode + * @return the type with the mode + * @since 1.1 + */ + protected int getMode(int mode) { + int type = FILE_FLAG; + if (isLink()) { + type = LINK_FLAG; + } else if (isDirectory()) { + type = DIR_FLAG; + } + return type | (mode & PERM_MASK); + } + + public Object clone() { + try { + AsiExtraField cloned = (AsiExtraField) super.clone(); + cloned.crc = new CRC32(); + return cloned; + } catch (CloneNotSupportedException cnfe) { + // impossible + throw new RuntimeException(cnfe); + } + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/CentralDirectoryParsingZipExtraField.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/CentralDirectoryParsingZipExtraField.java new file mode 100644 index 0000000..b7770a2 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/CentralDirectoryParsingZipExtraField.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip.extrafield; + +import java.util.zip.ZipException; + +/** + * {@link ZipExtraField ZipExtraField} that knows how to parse central + * directory data. + * + * @since Ant 1.8.0 + */ +public interface CentralDirectoryParsingZipExtraField extends ZipExtraField { + /** + * Populate data from this array as if it was in central directory data. + * + * @param data an array of bytes + * @param offset the start offset + * @param length the number of bytes in the array from offset + * @throws ZipException on error + */ + void parseFromCentralDirectoryData(byte[] data, int offset, int length) + throws ZipException; +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/ExtraFieldUtils.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/ExtraFieldUtils.java new file mode 100644 index 0000000..b7a63bd --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/ExtraFieldUtils.java @@ -0,0 +1,324 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip.extrafield; + +import top.niunaijun.blackobfuscator.core.utils.zip.JarMarker; +import top.niunaijun.blackobfuscator.core.utils.zip.ZipShort; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipException; + + +/** + * ZipExtraField related methods + */ +// CheckStyle:HideUtilityClassConstructorCheck OFF (bc) +public class ExtraFieldUtils { + + private static final int WORD = 4; + + /** + * Static registry of known extra fields. + * + * @since 1.1 + */ + private static final Map implementations; + + static { + implementations = new HashMap<>(); + register(AsiExtraField.class); + register(JarMarker.class); + register(UnicodePathExtraField.class); + register(UnicodeCommentExtraField.class); + } + + /** + * Register a ZipExtraField implementation. + *

+ *

The given class must have a no-arg constructor and implement + * the {@link ZipExtraField ZipExtraField interface}.

+ * + * @param c the class to register + * @since 1.1 + */ + public static void register(Class c) { + try { + ZipExtraField ze = (ZipExtraField) c.newInstance(); + implementations.put(ze.getHeaderId(), c); + } catch (ClassCastException cc) { + throw new RuntimeException(c + " doesn\'t implement ZipExtraField"); + } catch (InstantiationException ie) { + throw new RuntimeException(c + " is not a concrete class"); + } catch (IllegalAccessException ie) { + throw new RuntimeException(c + "\'s no-arg constructor is not public"); + } + } + + /** + * Create an instance of the approriate ExtraField, falls back to + * {@link UnrecognizedExtraField UnrecognizedExtraField}. + * + * @param headerId the header identifier + * @return an instance of the appropiate ExtraField + * @throws InstantiationException if unable to instantiate the class + * @throws IllegalAccessException if not allowed to instatiate the class + * @since 1.1 + */ + public static ZipExtraField createExtraField(ZipShort headerId) + throws InstantiationException, IllegalAccessException { + Class c = implementations.get(headerId); + if (c != null) { + return (ZipExtraField) c.newInstance(); + } + UnrecognizedExtraField u = new UnrecognizedExtraField(); + u.setHeaderId(headerId); + return u; + } + + /** + * Split the array into ExtraFields and populate them with the + * given data as local file data, throwing an exception if the + * data cannot be parsed. + * + * @param data an array of bytes as it appears in local file data + * @return an array of ExtraFields + * @throws ZipException on error + */ + public static ZipExtraField[] parse(byte[] data) throws ZipException { + return parse(data, true, UnparseableExtraField.THROW); + } + + /** + * Split the array into ExtraFields and populate them with the + * given data, throwing an exception if the data cannot be parsed. + * + * @param data an array of bytes + * @param local whether data originates from the local file data + * or the central directory + * @return an array of ExtraFields + * @throws ZipException on error + * @since 1.1 + */ + public static ZipExtraField[] parse(byte[] data, boolean local) + throws ZipException { + return parse(data, local, UnparseableExtraField.THROW); + } + + /** + * Split the array into ExtraFields and populate them with the + * given data. + * + * @param data an array of bytes + * @param local whether data originates from the local file data + * or the central directory + * @param onUnparseableData what to do if the extra field data + * cannot be parsed. + * @return an array of ExtraFields + * @throws ZipException on error + * @since Ant 1.8.1 + */ + public static ZipExtraField[] parse(byte[] data, boolean local, + UnparseableExtraField onUnparseableData) + throws ZipException { + List v = new ArrayList<>(); + int start = 0; + LOOP: + while (start <= data.length - WORD) { + ZipShort headerId = new ZipShort(data, start); + int length = (new ZipShort(data, start + 2)).getValue(); + if (start + WORD + length > data.length) { + switch (onUnparseableData.getKey()) { + case UnparseableExtraField.THROW_KEY: + throw new ZipException("bad extra field starting at " + + start + ". Block length of " + + length + " bytes exceeds remaining" + + " data of " + + (data.length - start - WORD) + + " bytes."); + case UnparseableExtraField.READ_KEY: + UnparseableExtraFieldData field = + new UnparseableExtraFieldData(); + if (local) { + field.parseFromLocalFileData(data, start, + data.length - start); + } else { + field.parseFromCentralDirectoryData(data, start, + data.length - start); + } + v.add(field); + /*FALLTHROUGH*/ + case UnparseableExtraField.SKIP_KEY: + // since we cannot parse the data we must assume + // the extra field consumes the whole rest of the + // available data + break LOOP; + default: + throw new ZipException("unknown UnparseableExtraField key: " + + onUnparseableData.getKey()); + } + } + try { + ZipExtraField ze = createExtraField(headerId); + if (local + || !(ze instanceof CentralDirectoryParsingZipExtraField)) { + ze.parseFromLocalFileData(data, start + WORD, length); + } else { + ((CentralDirectoryParsingZipExtraField) ze) + .parseFromCentralDirectoryData(data, start + WORD, + length); + } + v.add(ze); + } catch (InstantiationException ie) { + throw new ZipException(ie.getMessage()); + } catch (IllegalAccessException iae) { + throw new ZipException(iae.getMessage()); + } + start += (length + WORD); + } + + ZipExtraField[] result = new ZipExtraField[v.size()]; + return v.toArray(result); + } + + /** + * Merges the local file data fields of the given ZipExtraFields. + * + * @param data an array of ExtraFiles + * @return an array of bytes + * @since 1.1 + */ + public static byte[] mergeLocalFileDataData(ZipExtraField[] data) { + final boolean lastIsUnparseableHolder = data.length > 0 + && data[data.length - 1] instanceof UnparseableExtraFieldData; + int regularExtraFieldCount = + lastIsUnparseableHolder ? data.length - 1 : data.length; + + int sum = WORD * regularExtraFieldCount; + for (ZipExtraField aData : data) { + sum += aData.getLocalFileDataLength().getValue(); + } + + byte[] result = new byte[sum]; + int start = 0; + for (int i = 0; i < regularExtraFieldCount; i++) { + System.arraycopy(data[i].getHeaderId().getBytes(), + 0, result, start, 2); + System.arraycopy(data[i].getLocalFileDataLength().getBytes(), + 0, result, start + 2, 2); + byte[] local = data[i].getLocalFileDataData(); + System.arraycopy(local, 0, result, start + WORD, local.length); + start += (local.length + WORD); + } + if (lastIsUnparseableHolder) { + byte[] local = data[data.length - 1].getLocalFileDataData(); + System.arraycopy(local, 0, result, start, local.length); + } + return result; + } + + /** + * Merges the central directory fields of the given ZipExtraFields. + * + * @param data an array of ExtraFields + * @return an array of bytes + * @since 1.1 + */ + public static byte[] mergeCentralDirectoryData(ZipExtraField[] data) { + final boolean lastIsUnparseableHolder = data.length > 0 + && data[data.length - 1] instanceof UnparseableExtraFieldData; + int regularExtraFieldCount = + lastIsUnparseableHolder ? data.length - 1 : data.length; + + int sum = WORD * regularExtraFieldCount; + for (ZipExtraField aData : data) { + sum += aData.getCentralDirectoryLength().getValue(); + } + byte[] result = new byte[sum]; + int start = 0; + for (int i = 0; i < regularExtraFieldCount; i++) { + System.arraycopy(data[i].getHeaderId().getBytes(), + 0, result, start, 2); + System.arraycopy(data[i].getCentralDirectoryLength().getBytes(), + 0, result, start + 2, 2); + byte[] local = data[i].getCentralDirectoryData(); + System.arraycopy(local, 0, result, start + WORD, local.length); + start += (local.length + WORD); + } + if (lastIsUnparseableHolder) { + byte[] local = data[data.length - 1].getCentralDirectoryData(); + System.arraycopy(local, 0, result, start, local.length); + } + return result; + } + + /** + * "enum" for the possible actions to take if the extra field + * cannot be parsed. + */ + public static final class UnparseableExtraField { + /** + * Key for "throw an exception" action. + */ + public static final int THROW_KEY = 0; + /** + * Key for "skip" action. + */ + public static final int SKIP_KEY = 1; + /** + * Key for "read" action. + */ + public static final int READ_KEY = 2; + + /** + * Throw an exception if field cannot be parsed. + */ + public static final UnparseableExtraField THROW + = new UnparseableExtraField(THROW_KEY); + + /** + * Skip the extra field entirely and don't make its data + * available - effectively removing the extra field data. + */ + public static final UnparseableExtraField SKIP + = new UnparseableExtraField(SKIP_KEY); + + /** + * Read the extra field data into an instance of {@link + * UnparseableExtraFieldData UnparseableExtraFieldData}. + */ + public static final UnparseableExtraField READ + = new UnparseableExtraField(READ_KEY); + + private final int key; + + private UnparseableExtraField(int k) { + key = k; + } + + /** + * Key of the action to take. + */ + public int getKey() { + return key; + } + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnicodeCommentExtraField.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnicodeCommentExtraField.java new file mode 100644 index 0000000..9255ca5 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnicodeCommentExtraField.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip.extrafield; + + +import top.niunaijun.blackobfuscator.core.utils.zip.ZipShort; + +/** + * Info-ZIP Unicode Comment Extra Field (0x6375): + *

+ *

Stores the UTF-8 version of the file comment as stored in the + * central directory header.

+ *

+ *

+ *         Value         Size        Description
+ *         -----         ----        -----------
+ *  (UCom) 0x6375        Short       tag for this extra block type ("uc")
+ *         TSize         Short       total data size for this block
+ *         Version       1 byte      version of this extra field, currently 1
+ *         ComCRC32      4 bytes     Comment Field CRC32 Checksum
+ *         UnicodeCom    Variable    UTF-8 version of the entry comment
+ * 
+ */ +public class UnicodeCommentExtraField extends AbstractUnicodeExtraField { + + public static final ZipShort UCOM_ID = new ZipShort(0x6375); + + public UnicodeCommentExtraField() { + } + + /** + * Assemble as unicode comment extension from the name given as + * text as well as the encoded bytes actually written to the archive. + * + * @param text The file name + * @param bytes the bytes actually written to the archive + * @param off The offset of the encoded comment in bytes. + * @param len The length of the encoded comment or comment in + * bytes. + */ + public UnicodeCommentExtraField(String text, byte[] bytes, int off, + int len) { + super(text, bytes, off, len); + } + + /** + * Assemble as unicode comment extension from the comment given as + * text as well as the bytes actually written to the archive. + * + * @param comment The file comment + * @param bytes the bytes actually written to the archive + */ + public UnicodeCommentExtraField(String comment, byte[] bytes) { + super(comment, bytes); + } + + public ZipShort getHeaderId() { + return UCOM_ID; + } + +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnicodePathExtraField.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnicodePathExtraField.java new file mode 100644 index 0000000..307b146 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnicodePathExtraField.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip.extrafield; + +import top.niunaijun.blackobfuscator.core.utils.zip.ZipShort; + +/** + * Info-ZIP Unicode Path Extra Field (0x7075): + *

+ *

Stores the UTF-8 version of the file name field as stored in the + * local header and central directory header.

+ *

+ *

+ *         Value         Size        Description
+ *         -----         ----        -----------
+ * (UPath) 0x7075        Short       tag for this extra block type ("up")
+ *         TSize         Short       total data size for this block
+ *         Version       1 byte      version of this extra field, currently 1
+ *         NameCRC32     4 bytes     File Name Field CRC32 Checksum
+ *         UnicodeName   Variable    UTF-8 version of the entry File Name
+ * 
+ */ +public class UnicodePathExtraField extends AbstractUnicodeExtraField { + + public static final ZipShort UPATH_ID = new ZipShort(0x7075); + + public UnicodePathExtraField() { + } + + /** + * Assemble as unicode path extension from the name given as + * text as well as the encoded bytes actually written to the archive. + * + * @param text The file name + * @param bytes the bytes actually written to the archive + * @param off The offset of the encoded filename in bytes. + * @param len The length of the encoded filename or comment in + * bytes. + */ + public UnicodePathExtraField(String text, byte[] bytes, int off, int len) { + super(text, bytes, off, len); + } + + /** + * Assemble as unicode path extension from the name given as + * text as well as the encoded bytes actually written to the archive. + * + * @param name The file name + * @param bytes the bytes actually written to the archive + */ + public UnicodePathExtraField(String name, byte[] bytes) { + super(name, bytes); + } + + public ZipShort getHeaderId() { + return UPATH_ID; + } +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnparseableExtraFieldData.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnparseableExtraFieldData.java new file mode 100644 index 0000000..e197fb9 --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnparseableExtraFieldData.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package top.niunaijun.blackobfuscator.core.utils.zip.extrafield; + +import top.niunaijun.blackobfuscator.core.utils.zip.ZipShort; +import top.niunaijun.blackobfuscator.core.utils.zip.ZipUtil; + +/** + * Wrapper for extra field data that doesn't conform to the recommended format of header-tag + size + data. + *

+ *

The header-id is artificial (and not listed as a know ID in + * the .ZIP File Format Specification). + * Since it isn't used anywhere except to satisfy the + * ZipExtraField contract it shouldn't matter anyway.

+ * + * @see .ZIP File Format Specification + * @since Ant 1.8.1 + */ +public final class UnparseableExtraFieldData + implements CentralDirectoryParsingZipExtraField { + + private static final ZipShort HEADER_ID = new ZipShort(0xACC1); + + private byte[] localFileData; + private byte[] centralDirectoryData; + + /** + * The Header-ID. + * + * @return a completely arbitrary value that should be ignored. + */ + public ZipShort getHeaderId() { + return HEADER_ID; + } + + /** + * Length of the complete extra field in the local file data. + * + * @return The LocalFileDataLength value + */ + public ZipShort getLocalFileDataLength() { + return new ZipShort(localFileData == null ? 0 : localFileData.length); + } + + /** + * Length of the complete extra field in the central directory. + * + * @return The CentralDirectoryLength value + */ + public ZipShort getCentralDirectoryLength() { + return centralDirectoryData == null + ? getLocalFileDataLength() + : new ZipShort(centralDirectoryData.length); + } + + /** + * The actual data to put into local file data. + * + * @return The LocalFileDataData value + */ + public byte[] getLocalFileDataData() { + return ZipUtil.copy(localFileData); + } + + /** + * The actual data to put into central directory. + * + * @return The CentralDirectoryData value + */ + public byte[] getCentralDirectoryData() { + return centralDirectoryData == null + ? getLocalFileDataData() : ZipUtil.copy(centralDirectoryData); + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param buffer the buffer to read data from + * @param offset offset into buffer to read data + * @param length the length of data + */ + public void parseFromLocalFileData(byte[] buffer, int offset, int length) { + localFileData = new byte[length]; + System.arraycopy(buffer, offset, localFileData, 0, length); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param buffer the buffer to read data from + * @param offset offset into buffer to read data + * @param length the length of data + */ + public void parseFromCentralDirectoryData(byte[] buffer, int offset, + int length) { + centralDirectoryData = new byte[length]; + System.arraycopy(buffer, offset, centralDirectoryData, 0, length); + if (localFileData == null) { + parseFromLocalFileData(buffer, offset, length); + } + } + +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnrecognizedExtraField.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnrecognizedExtraField.java new file mode 100644 index 0000000..530f51e --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/UnrecognizedExtraField.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip.extrafield; + +import top.niunaijun.blackobfuscator.core.utils.zip.ZipShort; +import top.niunaijun.blackobfuscator.core.utils.zip.ZipUtil; + +/** + * Simple placeholder for all those extra fields we don't want to deal + * with. + *

+ *

Assumes local file data and central directory entries are + * identical - unless told the opposite.

+ */ +public class UnrecognizedExtraField + implements CentralDirectoryParsingZipExtraField { + + /** + * The Header-ID. + * + * @since 1.1 + */ + private ZipShort headerId; + + /** + * Set the header id. + * + * @param headerId the header id to use + */ + public void setHeaderId(ZipShort headerId) { + this.headerId = headerId; + } + + /** + * Get the header id. + * + * @return the header id + */ + public ZipShort getHeaderId() { + return headerId; + } + + /** + * Extra field data in local file data - without + * Header-ID or length specifier. + * + * @since 1.1 + */ + private byte[] localData; + + /** + * Set the extra field data in the local file data - + * without Header-ID or length specifier. + * + * @param data the field data to use + */ + public void setLocalFileDataData(byte[] data) { + localData = ZipUtil.copy(data); + } + + /** + * Get the length of the local data. + * + * @return the length of the local data + */ + public ZipShort getLocalFileDataLength() { + return new ZipShort(localData.length); + } + + /** + * Get the local data. + * + * @return the local data + */ + public byte[] getLocalFileDataData() { + return ZipUtil.copy(localData); + } + + /** + * Extra field data in central directory - without + * Header-ID or length specifier. + * + * @since 1.1 + */ + private byte[] centralData; + + /** + * Set the extra field data in central directory. + * + * @param data the data to use + */ + public void setCentralDirectoryData(byte[] data) { + centralData = ZipUtil.copy(data); + } + + /** + * Get the central data length. + * If there is no central data, get the local file data length. + * + * @return the central data length + */ + public ZipShort getCentralDirectoryLength() { + if (centralData != null) { + return new ZipShort(centralData.length); + } + return getLocalFileDataLength(); + } + + /** + * Get the central data. + * + * @return the central data if present, else return the local file data + */ + public byte[] getCentralDirectoryData() { + if (centralData != null) { + return ZipUtil.copy(centralData); + } + return getLocalFileDataData(); + } + + /** + * @param data the array of bytes. + * @param offset the source location in the data array. + * @param length the number of bytes to use in the data array. + * @see ZipExtraField#parseFromLocalFileData(byte[], int, int) + */ + public void parseFromLocalFileData(byte[] data, int offset, int length) { + byte[] tmp = new byte[length]; + System.arraycopy(data, offset, tmp, 0, length); + setLocalFileDataData(tmp); + } + + /** + * @param data the array of bytes. + * @param offset the source location in the data array. + * @param length the number of bytes to use in the data array. + */ + public void parseFromCentralDirectoryData(byte[] data, int offset, + int length) { + byte[] tmp = new byte[length]; + System.arraycopy(data, offset, tmp, 0, length); + setCentralDirectoryData(tmp); + if (localData == null) { + setLocalFileDataData(tmp); + } + } + +} diff --git a/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/ZipExtraField.java b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/ZipExtraField.java new file mode 100644 index 0000000..bfa80fb --- /dev/null +++ b/plugin/src/main/java/top/niunaijun/blackobfuscator/core/utils/zip/extrafield/ZipExtraField.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package top.niunaijun.blackobfuscator.core.utils.zip.extrafield; + +import top.niunaijun.blackobfuscator.core.utils.zip.ZipShort; + +import java.util.zip.ZipException; + + +/** + * General format of extra field data. + *

+ *

Extra fields usually appear twice per file, once in the local + * file data and once in the central directory. Usually they are the + * same, but they don't have to be. {@link + * java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream} will + * only use the local file data in both places.

+ */ +public interface ZipExtraField { + + /** + * The Header-ID. + * + * @return the header id + * @since 1.1 + */ + ZipShort getHeaderId(); + + /** + * Length of the extra field in the local file data - without + * Header-ID or length specifier. + * + * @return the length of the field in the local file data + * @since 1.1 + */ + ZipShort getLocalFileDataLength(); + + /** + * Length of the extra field in the central directory - without + * Header-ID or length specifier. + * + * @return the length of the field in the central directory + * @since 1.1 + */ + ZipShort getCentralDirectoryLength(); + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return the data + * @since 1.1 + */ + byte[] getLocalFileDataData(); + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * @return the data + * @since 1.1 + */ + byte[] getCentralDirectoryData(); + + /** + * Populate data from this array as if it was in local file data. + * + * @param data an array of bytes + * @param offset the start offset + * @param length the number of bytes in the array from offset + * @throws ZipException on error + * @since 1.1 + */ + void parseFromLocalFileData(byte[] data, int offset, int length) + throws ZipException; +} diff --git a/plugin/src/main/resources/META-INF/gradle-plugins/top.niunaijun.blackobfuscator.properties b/plugin/src/main/resources/META-INF/gradle-plugins/top.niunaijun.blackobfuscator.properties new file mode 100644 index 0000000..ed1b3da --- /dev/null +++ b/plugin/src/main/resources/META-INF/gradle-plugins/top.niunaijun.blackobfuscator.properties @@ -0,0 +1 @@ +implementation-class=top.niunaijun.blackobfuscator.ObfPlugin \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..1d1f6d6 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = "BlackObfuscator-ASPlugin" +include ':app' +include ':plugin'