commit bcd10b75d3f83d0f3f41e5a766f60abb4b938b5a Author: LordMZTE Date: Tue Oct 15 16:34:54 2024 +0200 init diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..1bd68c9 --- /dev/null +++ b/.clang-format @@ -0,0 +1,129 @@ +--- +AccessModifierOffset: 0 +AlignAfterOpenBracket: BlockIndent +AlignArrayOfStructures: None +AlignConsecutiveAssignments: None +AlignConsecutiveMacros: None +AlignConsecutiveBitFields: None +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: DontAlign +AlignOperands: DontAlign +AlignTrailingComments: false +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: MultiLine +AttributeMacros: [] +BinPackArguments: false +BinPackParameters: false +BitFieldColonSpacing: After +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakAfterJavaFieldAnnotations: true +#BreakArrays: false +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom +BreakBeforeConceptDeclarations: true +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterColon +BreakStringLiterals: true +ColumnLimit: 90 +CompactNamespaces: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DeriveLineEnding: false +DerivePointerAlignment: false +DisableFormat: false # wtf +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: Always +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +ForEachMacros: ["BOOST_FOREACH"] +IfMacros: [] +IncludeBlocks: Regroup +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: true +IndentExternBlock: Indent +IndentGotoLabels: true +IndentPPDirectives: BeforeHash +#IndentRequiresClause: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +#InsertBraces: false +InsertTrailingCommas: Wrapped +JavaScriptQuotes: Double +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +LambdaBodyIndentation: OuterScope +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +PackConstructorInitializers: NextLine +PointerAlignment: Left +QualifierAlignment: Left +ReferenceAlignment: Left +ReflowComments: true +#RemoveSemicolon: true +#RequiresClausePosition: OwnLine +#RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Always +SortIncludes: false +SortJavaStaticImport: Before +SortUsingDeclarations: true +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceAroundPointerQualifiers: After +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: false +SpaceBeforeInheritanceColon: false +SpaceBeforeParens: ControlStatementsExceptControlMacros +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesInAngles: Never +SpacesInCStyleCastParentheses: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: false +SpacesInLineCommentPrefix: + Minimum: 0 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: c++20 +StatementAttributeLikeMacros: [] +StatementMacros: [] +TabWidth: 4 +TypenameMacros: [] +UseCRLF: false # wtf +UseTab: Never +WhitespaceSensitiveMacros: ["BOOST_PP_STRINGSIZE"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d8ff4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.gradle +.idea +build +gradle.properties +bin +.settings +.classpath +.project diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..c43211f --- /dev/null +++ b/build.gradle @@ -0,0 +1,54 @@ +plugins { + id "java" + id "groovy" + id "java-gradle-plugin" + id "com.gradle.plugin-publish" version "0.10.0" + id "maven-publish" +} + +group "net.anvilcraft" +version "0.1.0" + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: "junit", name: "junit", version: "4.12" + implementation gradleApi() + implementation "org.ow2.asm:asm:9.7.1" + implementation "org.ow2.asm:asm-commons:9.7.1" + implementation "commons-io:commons-io:2.7" +} + +sourceSets { + main { + java { + srcDirs = [] + } + groovy { + srcDirs = ["src/main/java" ] + } + } +} + + +pluginBundle { + website = "https://git.tilera.org/Anvilcraft/gradlehaxe" + vcsUrl = "https://git.tilera.org/Anvilcraft/gradlehaxe" + tags = ["haxe"] +} + +gradlePlugin { + plugins { + gradlehaxe { + id = "net.anvilcraft.gradlehaxe" + displayName = "GradleHaxe" + description = "Haxe compatiblity for Gradle" + implementationClass = "net.anvilcraft.gradlehaxe.BuilderPlugin" + } + } +} + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..94336fc Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3ab0b72 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists 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/settings.gradle b/settings.gradle new file mode 100644 index 0000000..aa7646b --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'modpackbuilder' + diff --git a/src/main/java/net/anvilcraft/gradlehaxe/BuilderPlugin.java b/src/main/java/net/anvilcraft/gradlehaxe/BuilderPlugin.java new file mode 100644 index 0000000..879bccc --- /dev/null +++ b/src/main/java/net/anvilcraft/gradlehaxe/BuilderPlugin.java @@ -0,0 +1,56 @@ +package net.anvilcraft.gradlehaxe; + +import javax.inject.Inject; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.file.SourceDirectorySet; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.plugins.internal.JvmPluginsHelper; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskProvider; + +import net.anvilcraft.gradlehaxe.compile.CompileHaxeTask; + +public class BuilderPlugin implements Plugin { + private final ObjectFactory objects; + + @Inject + public BuilderPlugin(ObjectFactory objects) { + this.objects = objects; + } + + @Override + public void apply(Project proj) { + proj.getPluginManager().apply(JavaBasePlugin.class); + + proj.getExtensions().getByType(SourceSetContainer.class).all(ss -> { + SourceDirectorySet haxeSrc + = this.objects.sourceDirectorySet("haxe", ss.getName() + " Haxe Source."); + haxeSrc.srcDir("src/" + ss.getName() + "/haxe"); + + ss.getExtensions().add("haxe", haxeSrc); + + TaskProvider taskProv = proj.getTasks().register( + ss.getCompileTaskName("haxe"), + CompileHaxeTask.class, + task -> { + task.setSource(haxeSrc); + task.getSS().set(ss); + task.getConventionMapping().map("classpath", ss::getCompileClasspath); + } + ); + JvmPluginsHelper.configureOutputDirectoryForSourceSet( + ss, haxeSrc, proj, taskProv, taskProv.map(CompileHaxeTask::getOptions) + ); + + proj.getTasks().named(ss.getClassesTaskName(), t -> t.dependsOn(taskProv)); + + proj.getTasks() + .register("generateHXML_" + ss.getName(), GenerateHXMLTask.class); + + ss.getExtensions().create("haxeopts", HaxeSourceSetExtension.class); + }); + } +} diff --git a/src/main/java/net/anvilcraft/gradlehaxe/GenerateHXMLTask.java b/src/main/java/net/anvilcraft/gradlehaxe/GenerateHXMLTask.java new file mode 100644 index 0000000..331b656 --- /dev/null +++ b/src/main/java/net/anvilcraft/gradlehaxe/GenerateHXMLTask.java @@ -0,0 +1,28 @@ +package net.anvilcraft.gradlehaxe; + +import java.io.FileWriter; +import java.io.IOException; + +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskAction; + +public abstract class GenerateHXMLTask extends DefaultTask { + @TaskAction + public void action() { + String ssName = this.getName().replace("generateHXML_", ""); + SourceSet ss = this.getProject() + .getExtensions() + .getByType(SourceSetContainer.class) + .getByName(ssName); + + HaxeArguments args = new HaxeArguments(ss, this.getProject(), this.getTemporaryDir() + "/build.jar"); + + try (FileWriter fw = new FileWriter("build.hxml")) { + args.writeHXML(fw); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/net/anvilcraft/gradlehaxe/HaxeArguments.java b/src/main/java/net/anvilcraft/gradlehaxe/HaxeArguments.java new file mode 100644 index 0000000..0254974 --- /dev/null +++ b/src/main/java/net/anvilcraft/gradlehaxe/HaxeArguments.java @@ -0,0 +1,93 @@ +package net.anvilcraft.gradlehaxe; + +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; + +import org.gradle.api.JavaVersion; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.jvm.toolchain.JavaLauncher; +import org.gradle.jvm.toolchain.JavaToolchainService; + +public class HaxeArguments { + public String outpath; + public String classpath; + public ArrayList includedPackages = new ArrayList<>(); + public ArrayList libs = new ArrayList<>(); + + public HaxeArguments(SourceSet ss, Project proj, String outpath) { + this.classpath = "src/" + ss.getName() + "/haxe"; + this.outpath = outpath; + + this.includedPackages.addAll( + ss.getExtensions().getByType(HaxeSourceSetExtension.class).includedPackages + ); + + ss.getCompileClasspath() + .getFiles() + .stream() + .filter(f -> f.getPath().endsWith(".jar")) + .map(File::getPath) + .forEach(this.libs::add); + ; + + JavaPluginExtension java + = proj.getExtensions().getByType(JavaPluginExtension.class); + + if (java.getTargetCompatibility() == JavaVersion.VERSION_1_8) { + JavaToolchainService javaToolchain + = proj.getExtensions().getByType(JavaToolchainService.class); + + Provider launcher = javaToolchain.launcherFor( + spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(8)) + ); + + this.libs.add( + launcher.get().getMetadata().getInstallationPath() + "/jre/lib/jce.jar" + ); + } + } + + public void writeHXML(Writer w) throws IOException { + w.write("-cp " + this.classpath + "\n"); + for (String p : this.includedPackages) { + w.write("--macro \"include('" + p + "')\"\n"); + } + w.write("--jvm " + this.outpath + "\n"); + w.write("\n"); + + for (String lib : this.libs) { + w.write("--java-lib-extern " + lib + "\n"); + } + } + + public String[] toCommand() { + String[] ret + = new String[this.includedPackages.size() * 2 + this.libs.size() * 2 + 5]; + int i = 0; + + ret[i++] = "haxe"; + ret[i++] = "-cp"; + ret[i++] = this.classpath; + ret[i++] = "--jvm"; + ret[i++] = this.outpath; + + for (String p : this.includedPackages) { + ret[i++] = "--macro"; + ret[i++] = "include('" + p + "')"; + } + + for (String lib : this.libs) { + ret[i++] = "--java-lib-extern"; + ret[i++] = lib; + } + + assert i == ret.length; + return ret; + } +} diff --git a/src/main/java/net/anvilcraft/gradlehaxe/HaxeSourceSetExtension.java b/src/main/java/net/anvilcraft/gradlehaxe/HaxeSourceSetExtension.java new file mode 100644 index 0000000..7160e05 --- /dev/null +++ b/src/main/java/net/anvilcraft/gradlehaxe/HaxeSourceSetExtension.java @@ -0,0 +1,17 @@ +package net.anvilcraft.gradlehaxe; + +import java.util.HashSet; +import java.util.Set; + +public class HaxeSourceSetExtension { + public final Set includedPackages = new HashSet<>(); + public String remappedHaxePackageName = null; + + public void includePackage(String pkg) { + this.includedPackages.add(pkg); + } + + public void remapHaxePackage(String name) { + this.remappedHaxePackageName = name; + } +} diff --git a/src/main/java/net/anvilcraft/gradlehaxe/compile/CompileHaxeTask.java b/src/main/java/net/anvilcraft/gradlehaxe/compile/CompileHaxeTask.java new file mode 100644 index 0000000..e6911ba --- /dev/null +++ b/src/main/java/net/anvilcraft/gradlehaxe/compile/CompileHaxeTask.java @@ -0,0 +1,89 @@ +package net.anvilcraft.gradlehaxe.compile; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.jar.JarFile; + +import org.apache.commons.io.FileUtils; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.compile.AbstractCompile; +import org.gradle.api.tasks.compile.CompileOptions; +import org.gradle.tooling.BuildException; + +import net.anvilcraft.gradlehaxe.HaxeArguments; +import net.anvilcraft.gradlehaxe.HaxeSourceSetExtension; +import net.anvilcraft.gradlehaxe.util.JarRelocatorTask; +import net.anvilcraft.gradlehaxe.util.RelocatingRemapper; +import net.anvilcraft.gradlehaxe.util.Relocation; + +public abstract class CompileHaxeTask extends AbstractCompile { + public CompileOptions options; + + public CompileHaxeTask() { + this.options = this.getProject().getObjects().newInstance( + CompileOptions.class, new Object[0] + ); + } + + @TaskAction + public void action() { + String buildJar = this.getTemporaryDir() + "/build.jar"; + + HaxeArguments args + = new HaxeArguments(this.getSS().get(), this.getProject(), buildJar); + + try { + Process proc = Runtime.getRuntime().exec(args.toCommand()); + if (proc.waitFor() != 0) { + StringBuilder sb = new StringBuilder(); + sb.append("Haxe failed.\nstdout:\n"); + copyToSB(proc.getInputStream(), sb); + sb.append("\nstderr:\n"); + copyToSB(proc.getErrorStream(), sb); + + throw new BuildException(sb.toString(), null); + } + + Path dest = this.getDestinationDirectory().getAsFile().get().toPath(); + + // Delete, then re-create destination directory to remove potential excess classes. + FileUtils.cleanDirectory(dest.toFile()); + + String relocatedHaxePkg = this.getSS() + .get() + .getExtensions() + .getByType(HaxeSourceSetExtension.class) + .remappedHaxePackageName; + if (relocatedHaxePkg != null) { + ArrayList relocations = new ArrayList(); + relocations.add(new Relocation("haxe", relocatedHaxePkg)); + + new JarRelocatorTask( + new RelocatingRemapper(relocations), + dest, + new JarFile(buildJar) + ).processEntries(); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public abstract Property getSS(); + + public CompileOptions getOptions() { + return this.options; + } + + private void copyToSB(InputStream is, StringBuilder sb) throws IOException { + byte[] buf = new byte[4096]; + int read; + while ((read = is.read(buf)) > 0) { + sb.append(new String(buf, 0, read)); + } + } +} diff --git a/src/main/java/net/anvilcraft/gradlehaxe/util/JarRelocatorTask.java b/src/main/java/net/anvilcraft/gradlehaxe/util/JarRelocatorTask.java new file mode 100644 index 0000000..bae91fc --- /dev/null +++ b/src/main/java/net/anvilcraft/gradlehaxe/util/JarRelocatorTask.java @@ -0,0 +1,163 @@ +/* + * Copyright Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.anvilcraft.gradlehaxe.util; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.regex.Pattern; + +import org.apache.commons.io.IOUtils; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +/** + * A task that copies {@link JarEntry jar entries} from a {@link JarFile jar input} to a + * {@link JarOutputStream jar output}, applying the relocations defined by a + * {@link RelocatingRemapper}. + */ +public class JarRelocatorTask { + /** + * META-INF/*.SF + * META-INF/*.DSA + * META-INF/*.RSA + * META-INF/SIG-* + * + * Specification + */ + private static final Pattern SIGNATURE_FILE_PATTERN + = Pattern.compile("META-INF/(?:[^/]+\\.(?:DSA|RSA|SF)|SIG-[^/]+)"); + + private final RelocatingRemapper remapper; + private final Path dirOut; + private final JarFile jarIn; + + private final Set resources = new HashSet<>(); + + public JarRelocatorTask(RelocatingRemapper remapper, Path dirOut, JarFile jarIn) { + this.remapper = remapper; + this.dirOut = dirOut; + this.jarIn = jarIn; + } + + public void processEntries() throws IOException { + for (Enumeration entries = this.jarIn.entries(); + entries.hasMoreElements();) { + JarEntry entry = entries.nextElement(); + + // The 'INDEX.LIST' file is an optional file, containing information about the + // packages defined in a jar. Instead of relocating the entries in it, we + // delete it, since it is optional anyway. + // + // We don't process directory entries, and instead opt to recreate them when + // adding classes/resources. + String name = entry.getName(); + if (name.equals("META-INF/INDEX.LIST") || name.equals("META-INF/MANIFEST.MF") + || entry.isDirectory()) { + continue; + } + + // Signatures will become invalid after remapping, so we delete them to avoid + // making the output useless + if (SIGNATURE_FILE_PATTERN.matcher(name).matches()) { + continue; + } + + try (InputStream entryIn = this.jarIn.getInputStream(entry)) { + processEntry(entry, entryIn); + } + } + } + + private void processEntry(JarEntry entry, InputStream entryIn) throws IOException { + String name = entry.getName(); + String mappedName = this.remapper.map(name); + + // ensure the parent directory structure exists for the entry. + processDirectory(mappedName, true); + + if (name.endsWith(".class")) { + processClass(name, entryIn); + } else if (!this.resources.contains(mappedName)) { + processResource(mappedName, entryIn, entry.getTime()); + } + } + + private void processDirectory(String name, boolean parentsOnly) throws IOException { + int index = name.lastIndexOf('/'); + if (index != -1) { + String parentDirectory = name.substring(0, index); + if (!this.resources.contains(parentDirectory)) { + processDirectory(parentDirectory, false); + } + } + + if (parentsOnly) { + return; + } + + this.dirOut.resolve(name).toFile().mkdirs(); + this.resources.add(name); + } + + private void processResource(String name, InputStream entryIn, long lastModified) + throws IOException { + try ( + FileOutputStream fos + = new FileOutputStream(this.dirOut.resolve(name).toFile()) + ) { + IOUtils.copy(entryIn, fos); + } + + this.resources.add(name); + } + + private void processClass(String name, InputStream entryIn) throws IOException { + ClassReader classReader = new ClassReader(entryIn); + ClassWriter classWriter = new ClassWriter(0); + RelocatingClassVisitor classVisitor + = new RelocatingClassVisitor(classWriter, this.remapper, name); + + try { + classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES); + } catch (Throwable e) { + throw new RuntimeException("Error processing class " + name, e); + } + + byte[] renamedClass = classWriter.toByteArray(); + + // Need to take the .class off for remapping evaluation + String mappedName = this.remapper.map(name.substring(0, name.indexOf('.'))); + + // Now we put it back on so the class file is written out with the right + // extension. + try ( + FileOutputStream fos + = new FileOutputStream(this.dirOut.resolve(mappedName + ".class").toFile()) + ) { + fos.write(renamedClass); + } + } +} diff --git a/src/main/java/net/anvilcraft/gradlehaxe/util/RelocatingClassVisitor.java b/src/main/java/net/anvilcraft/gradlehaxe/util/RelocatingClassVisitor.java new file mode 100644 index 0000000..8842e98 --- /dev/null +++ b/src/main/java/net/anvilcraft/gradlehaxe/util/RelocatingClassVisitor.java @@ -0,0 +1,49 @@ +/* + * Copyright Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.anvilcraft.gradlehaxe.util; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.ClassRemapper; + +/** + * A {@link ClassVisitor} that relocates types and names with a {@link + * RelocatingRemapper}. + */ +final class RelocatingClassVisitor extends ClassRemapper { + private final String packageName; + + RelocatingClassVisitor(ClassWriter writer, RelocatingRemapper remapper, String name) { + super(Opcodes.ASM9, writer, remapper); + this.packageName = name.substring(0, name.lastIndexOf('/') + 1); + } + + @Override + public void visitSource(String source, String debug) { + if (source == null) { + super.visitSource(null, debug); + return; + } + + // visit source file name + String name = this.packageName + source; + String mappedName = super.remapper.map(name); + String mappedFileName = mappedName.substring(mappedName.lastIndexOf('/') + 1); + super.visitSource(mappedFileName, debug); + } +} diff --git a/src/main/java/net/anvilcraft/gradlehaxe/util/RelocatingRemapper.java b/src/main/java/net/anvilcraft/gradlehaxe/util/RelocatingRemapper.java new file mode 100644 index 0000000..b7d39f3 --- /dev/null +++ b/src/main/java/net/anvilcraft/gradlehaxe/util/RelocatingRemapper.java @@ -0,0 +1,94 @@ +/* + * Copyright Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.anvilcraft.gradlehaxe.util; + +import org.objectweb.asm.commons.Remapper; + +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Remaps class names and types using defined {@link Relocation} rules. + */ +public class RelocatingRemapper extends Remapper { + private static final Pattern CLASS_PATTERN = Pattern.compile("(\\[*)?L(.+);"); + + // https://docs.oracle.com/javase/10/docs/specs/jar/jar.html#multi-release-jar-files + private static final Pattern VERSION_PATTERN + = Pattern.compile("^(META-INF/versions/\\d+/)(.*)$"); + + private final Collection rules; + + public RelocatingRemapper(Collection rules) { + this.rules = rules; + } + + public Collection getRules() { + return this.rules; + } + + @Override + public String map(String name) { + String relocatedName = relocate(name, false); + if (relocatedName != null) { + return relocatedName; + } + return super.map(name); + } + + @Override + public Object mapValue(Object object) { + if (object instanceof String) { + String relocatedName = relocate((String) object, true); + if (relocatedName != null) { + return relocatedName; + } + } + return super.mapValue(object); + } + + private String relocate(String name, boolean isStringValue) { + String prefix = ""; + String suffix = ""; + + if (isStringValue) { + Matcher m = CLASS_PATTERN.matcher(name); + if (m.matches()) { + prefix = m.group(1) + "L"; + name = m.group(2); + suffix = ";"; + } + } + + Matcher m = VERSION_PATTERN.matcher(name); + if (m.matches()) { + prefix = m.group(1); + name = m.group(2); + } + + for (Relocation r : this.rules) { + if (isStringValue && r.canRelocateClass(name)) { + return prefix + r.relocateClass(name) + suffix; + } else if (r.canRelocatePath(name)) { + return prefix + r.relocatePath(name) + suffix; + } + } + + return null; + } +} diff --git a/src/main/java/net/anvilcraft/gradlehaxe/util/Relocation.java b/src/main/java/net/anvilcraft/gradlehaxe/util/Relocation.java new file mode 100644 index 0000000..b85614e --- /dev/null +++ b/src/main/java/net/anvilcraft/gradlehaxe/util/Relocation.java @@ -0,0 +1,151 @@ +/* + * Copyright Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.anvilcraft.gradlehaxe.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * A relocation rule + */ +public class Relocation { + private final String pattern; + private final String relocatedPattern; + private final String pathPattern; + private final String relocatedPathPattern; + + private final Set includes; + private final Set excludes; + + /** + * Creates a new relocation + * + * @param pattern the pattern to match + * @param relocatedPattern the pattern to relocate to + * @param includes a collection of patterns which this rule should specifically + * include + * @param excludes a collection of patterns which this rule should specifically + * exclude + */ + public Relocation( + String pattern, + String relocatedPattern, + Collection includes, + Collection excludes + ) { + this.pattern = pattern.replace('/', '.'); + this.pathPattern = pattern.replace('.', '/'); + this.relocatedPattern = relocatedPattern.replace('/', '.'); + this.relocatedPathPattern = relocatedPattern.replace('.', '/'); + + if (includes != null && !includes.isEmpty()) { + this.includes = normalizePatterns(includes); + this.includes.addAll(includes); + } else { + this.includes = null; + } + + if (excludes != null && !excludes.isEmpty()) { + this.excludes = normalizePatterns(excludes); + this.excludes.addAll(excludes); + } else { + this.excludes = null; + } + } + + /** + * Creates a new relocation with no specific includes or excludes + * + * @param pattern the pattern to match + * @param relocatedPattern the pattern to relocate to + */ + public Relocation(String pattern, String relocatedPattern) { + this( + pattern, + relocatedPattern, + Collections.emptyList(), + Collections.emptyList() + ); + } + + private boolean isIncluded(String path) { + if (this.includes == null) { + return true; + } + + for (String include : this.includes) { + if (SelectorUtils.matchPath(include, path, true)) { + return true; + } + } + return false; + } + + private boolean isExcluded(String path) { + if (this.excludes == null) { + return false; + } + + for (String exclude : this.excludes) { + if (SelectorUtils.matchPath(exclude, path, true)) { + return true; + } + } + return false; + } + + boolean canRelocatePath(String path) { + if (path.endsWith(".class")) { + path = path.substring(0, path.length() - 6); + } + + if (!isIncluded(path) || isExcluded(path)) { + return false; + } + + return path.startsWith(this.pathPattern) + || path.startsWith("/" + this.pathPattern); + } + + boolean canRelocateClass(String clazz) { + return clazz.indexOf('/') == -1 && canRelocatePath(clazz.replace('.', '/')); + } + + String relocatePath(String path) { + return path.replaceFirst(this.pathPattern, this.relocatedPathPattern); + } + + String relocateClass(String clazz) { + return clazz.replaceFirst(this.pattern, this.relocatedPattern); + } + + private static Set normalizePatterns(Collection patterns) { + Set normalized = new LinkedHashSet<>(); + for (String pattern : patterns) { + String classPattern = pattern.replace('.', '/'); + normalized.add(classPattern); + if (classPattern.endsWith("/*")) { + String packagePattern + = classPattern.substring(0, classPattern.lastIndexOf('/')); + normalized.add(packagePattern); + } + } + return normalized; + } +} diff --git a/src/main/java/net/anvilcraft/gradlehaxe/util/SelectorUtils.java b/src/main/java/net/anvilcraft/gradlehaxe/util/SelectorUtils.java new file mode 100644 index 0000000..5788a0f --- /dev/null +++ b/src/main/java/net/anvilcraft/gradlehaxe/util/SelectorUtils.java @@ -0,0 +1,396 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2002-2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.codehaus.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "Ant" and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact codehaus@codehaus.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +package net.anvilcraft.gradlehaxe.util; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +/** + * This is a stripped down version of org.codehaus.plexus.util.SelectorUtils for + * use in {@link Relocation}. + * + * @author Arnout J. Kuiper ajkuiper@wxs.nl + * @author Magesh Umasankar + * @author Bruce Atherton + */ +final class SelectorUtils { + private static final String PATTERN_HANDLER_PREFIX = "["; + private static final String PATTERN_HANDLER_SUFFIX = "]"; + private static final String REGEX_HANDLER_PREFIX = "%regex" + PATTERN_HANDLER_PREFIX; + private static final String ANT_HANDLER_PREFIX = "%ant" + PATTERN_HANDLER_PREFIX; + + private static boolean isAntPrefixedPattern(String pattern) { + return pattern.length() + > (ANT_HANDLER_PREFIX.length() + PATTERN_HANDLER_SUFFIX.length() + 1) + && pattern.startsWith(ANT_HANDLER_PREFIX) + && pattern.endsWith(PATTERN_HANDLER_SUFFIX); + } + + // When str starts with a File.separator, pattern has to start with a File.separator. + // When pattern starts with a File.separator, str has to start with a File.separator. + private static boolean + separatorPatternStartSlashMismatch(String pattern, String str, String separator) { + return str.startsWith(separator) != pattern.startsWith(separator); + } + + public static boolean matchPath(String pattern, String str, boolean isCaseSensitive) { + return matchPath(pattern, str, File.separator, isCaseSensitive); + } + + private static boolean + matchPath(String pattern, String str, String separator, boolean isCaseSensitive) { + if (isRegexPrefixedPattern(pattern)) { + pattern = pattern.substring( + REGEX_HANDLER_PREFIX.length(), + pattern.length() - PATTERN_HANDLER_SUFFIX.length() + ); + return str.matches(pattern); + } else { + if (isAntPrefixedPattern(pattern)) { + pattern = pattern.substring( + ANT_HANDLER_PREFIX.length(), + pattern.length() - PATTERN_HANDLER_SUFFIX.length() + ); + } + return matchAntPathPattern(pattern, str, separator, isCaseSensitive); + } + } + + private static boolean isRegexPrefixedPattern(String pattern) { + return pattern.length() + > (REGEX_HANDLER_PREFIX.length() + PATTERN_HANDLER_SUFFIX.length() + 1) + && pattern.startsWith(REGEX_HANDLER_PREFIX) + && pattern.endsWith(PATTERN_HANDLER_SUFFIX); + } + + private static boolean matchAntPathPattern( + String pattern, String str, String separator, boolean isCaseSensitive + ) { + if (separatorPatternStartSlashMismatch(pattern, str, separator)) { + return false; + } + String[] patDirs = tokenizePathToString(pattern, separator); + String[] strDirs = tokenizePathToString(str, separator); + return matchAntPathPattern(patDirs, strDirs, isCaseSensitive); + } + + private static boolean + matchAntPathPattern(String[] patDirs, String[] strDirs, boolean isCaseSensitive) { + int patIdxStart = 0; + int patIdxEnd = patDirs.length - 1; + int strIdxStart = 0; + int strIdxEnd = strDirs.length - 1; + + // up to first '**' + while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) { + String patDir = patDirs[patIdxStart]; + if (patDir.equals("**")) { + break; + } + if (!match(patDir, strDirs[strIdxStart], isCaseSensitive)) { + return false; + } + patIdxStart++; + strIdxStart++; + } + if (strIdxStart > strIdxEnd) { + // String is exhausted + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (!patDirs[i].equals("**")) { + return false; + } + } + return true; + } else { + if (patIdxStart > patIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } + } + + // up to last '**' + while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) { + String patDir = patDirs[patIdxEnd]; + if (patDir.equals("**")) { + break; + } + if (!match(patDir, strDirs[strIdxEnd], isCaseSensitive)) { + return false; + } + patIdxEnd--; + strIdxEnd--; + } + if (strIdxStart > strIdxEnd) { + // String is exhausted + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (!patDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) { + int patIdxTmp = -1; + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patDirs[i].equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == patIdxStart + 1) { + // '**/**' situation, so skip one + patIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - patIdxStart - 1); + int strLength = (strIdxEnd - strIdxStart + 1); + int foundIdx = -1; + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = patDirs[patIdxStart + j + 1]; + String subStr = strDirs[strIdxStart + i + j]; + if (!match(subPat, subStr, isCaseSensitive)) { + continue strLoop; + } + } + + foundIdx = strIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + patIdxStart = patIdxTmp; + strIdxStart = foundIdx + patLength; + } + + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (!patDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + private static boolean match(String pattern, String str, boolean isCaseSensitive) { + char[] patArr = pattern.toCharArray(); + char[] strArr = str.toCharArray(); + return match(patArr, strArr, isCaseSensitive); + } + + private static boolean match(char[] patArr, char[] strArr, boolean isCaseSensitive) { + int patIdxStart = 0; + int patIdxEnd = patArr.length - 1; + int strIdxStart = 0; + int strIdxEnd = strArr.length - 1; + char ch; + + boolean containsStar = false; + for (char aPatArr : patArr) { + if (aPatArr == '*') { + containsStar = true; + break; + } + } + + if (!containsStar) { + // No '*'s, so we make a shortcut + if (patIdxEnd != strIdxEnd) { + return false; // Pattern and string do not have the same size + } + for (int i = 0; i <= patIdxEnd; i++) { + ch = patArr[i]; + if (ch != '?' && !equals(ch, strArr[i], isCaseSensitive)) { + return false; // Character mismatch + } + } + return true; // String matches against pattern + } + + if (patIdxEnd == 0) { + return true; // Pattern contains only '*', which matches anything + } + + // Process characters before first star + while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) { + if (ch != '?' && !equals(ch, strArr[strIdxStart], isCaseSensitive)) { + return false; // Character mismatch + } + patIdxStart++; + strIdxStart++; + } + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + // Process characters after last star + while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) { + if (ch != '?' && !equals(ch, strArr[strIdxEnd], isCaseSensitive)) { + return false; // Character mismatch + } + patIdxEnd--; + strIdxEnd--; + } + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + // process pattern between stars. padIdxStart and patIdxEnd point + // always to a '*'. + while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) { + int patIdxTmp = -1; + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patArr[i] == '*') { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == patIdxStart + 1) { + // Two stars next to each other, skip the first one. + patIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - patIdxStart - 1); + int strLength = (strIdxEnd - strIdxStart + 1); + int foundIdx = -1; + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + ch = patArr[patIdxStart + j + 1]; + if (ch != '?' + && !equals(ch, strArr[strIdxStart + i + j], isCaseSensitive)) { + continue strLoop; + } + } + + foundIdx = strIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + patIdxStart = patIdxTmp; + strIdxStart = foundIdx + patLength; + } + + // All characters in the string are used. Check if only '*'s are left + // in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + /** + * Tests whether two characters are equal. + */ + private static boolean equals(char c1, char c2, boolean isCaseSensitive) { + if (c1 == c2) { + return true; + } + if (!isCaseSensitive) { + // NOTE: Try both upper case and lower case as done by + // String.equalsIgnoreCase() + if (Character.toUpperCase(c1) == Character.toUpperCase(c2) + || Character.toLowerCase(c1) == Character.toLowerCase(c2)) { + return true; + } + } + return false; + } + + private static String[] tokenizePathToString(String path, String separator) { + List ret = new ArrayList(); + StringTokenizer st = new StringTokenizer(path, separator); + while (st.hasMoreTokens()) { + ret.add(st.nextToken()); + } + return ret.toArray(new String[ret.size()]); + } + + /** + * Private Constructor + */ + private SelectorUtils() {} +}