diff options
Diffstat (limited to 'src/main/java/moe/nea/zwirn')
-rw-r--r-- | src/main/java/moe/nea/zwirn/TinyBaseMerger.java | 206 | ||||
-rw-r--r-- | src/main/java/moe/nea/zwirn/TinyDiffer.java | 264 | ||||
-rw-r--r-- | src/main/java/moe/nea/zwirn/Zwirn.java | 16 | ||||
-rw-r--r-- | src/main/java/moe/nea/zwirn/ZwirnUtil.java | 2 |
4 files changed, 487 insertions, 1 deletions
diff --git a/src/main/java/moe/nea/zwirn/TinyBaseMerger.java b/src/main/java/moe/nea/zwirn/TinyBaseMerger.java new file mode 100644 index 0000000..96ddf2a --- /dev/null +++ b/src/main/java/moe/nea/zwirn/TinyBaseMerger.java @@ -0,0 +1,206 @@ +package moe.nea.zwirn; + +import net.fabricmc.stitch.commands.tinyv2.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +public abstract class TinyBaseMerger { + protected final @NotNull TinyFile base; + protected final @NotNull TinyFile overlay; + protected final @NotNull String sharedNamespace; + protected final int baseSharedIndex; + protected final int overlaySharedIndex; + protected final @NotNull Map<@NotNull String, @NotNull TinyClass> overlayLUT; + protected final @NotNull Map<@NotNull String, @NotNull TinyClass> baseLUT; + protected final Integer[] baseToOverlayIndexMap; + + protected TinyBaseMerger(TinyFile base, TinyFile overlay, String sharedNamespace) { + this.base = base; + this.overlay = overlay; + this.sharedNamespace = sharedNamespace; + this.baseSharedIndex = base.getHeader().getNamespaces().indexOf(sharedNamespace); + this.overlaySharedIndex = overlay.getHeader().getNamespaces().indexOf(sharedNamespace); + this.overlayLUT = overlay.getClassEntries().stream() + .collect(Collectors.toMap(it -> it.getClassNames().get(this.overlaySharedIndex), Function.identity())); + this.baseLUT = base.getClassEntries().stream() + .collect(Collectors.toMap(it -> it.getClassNames().get(this.baseSharedIndex), Function.identity())); + this.baseToOverlayIndexMap = base.getHeader().getNamespaces() + .stream().map(it -> overlay.getHeader().getNamespaces().indexOf(it)) + .map(it -> it < 0 ? null : it) + .toArray(Integer[]::new); + } + + public @NotNull TinyFile merge() { + var baseHeader = base.getHeader(); + return new TinyFile( + new TinyHeader(baseHeader.getNamespaces(), baseHeader.getMajorVersion(), baseHeader.getMinorVersion(), baseHeader.getProperties()), + mergeClasses() + ); + } + + + protected abstract @NotNull List<@NotNull String> mergeNames( + @NotNull List<@NotNull String> baseNames, + @NotNull List<@NotNull String> overlayNames); + + protected <T> List<String> mergeNames(@Nullable T baseObject, @Nullable T overlayObject, @NotNull Function<@NotNull T, @NotNull List<@NotNull String>> nameExtractor) { + if (baseObject == null && overlayObject == null) + throw new IllegalArgumentException("Either baseObject or overlayObject must not be null"); + if (overlayObject == null) + return nameExtractor.apply(baseObject); + if (baseObject == null) { + if (base.getHeader().getNamespaces().size() != overlay.getHeader().getNamespaces().size()) + throw new IllegalStateException("Can only merge unnamed base objects if both files have the same namespaces"); + List<String> names = new ArrayList<>(); + for (int i = 0; i < base.getHeader().getNamespaces().size(); i++) { + names.add(""); + } + List<@NotNull String> mergedNames = mergeNames(names, nameExtractor.apply(overlayObject)); + for (var name : mergedNames) { + if (name.isEmpty()) + throw new RuntimeException("Invariant violated: unnamed overlay name"); + } + return mergedNames; + } + return mergeNames(nameExtractor.apply(baseObject), nameExtractor.apply(overlayObject)); + } + + protected abstract <T> @NotNull List<@NotNull String> mergeComments(@Nullable T baseObject, @Nullable T overlayObject, @NotNull Function<@NotNull T, ? extends @NotNull Collection<@NotNull String>> commentExtractor); + + protected TinyClass mergeClass(@Nullable TinyClass baseClass, @Nullable TinyClass overlayClass) { + return new TinyClass( + mergeNames(baseClass, overlayClass, TinyClass::getClassNames), + mergeMethods(baseClass, overlayClass), + mergeFields(baseClass, overlayClass), + mergeComments(baseClass, overlayClass, TinyClass::getComments) + ); + } + + protected Collection<TinyField> mergeFields(@Nullable TinyClass baseClass, @Nullable TinyClass overlayClass) { + return mergeChildren(baseClass, overlayClass, + it -> it.getFieldNames().get(baseSharedIndex), + it -> it.getFieldNames().get(overlaySharedIndex), + TinyClass::getFields, + this::mergeField); + } + + protected TinyField mergeField(@Nullable TinyField baseField, @Nullable TinyField overlayField) { + var mergedNames = mergeNames(baseField, overlayField, TinyField::getFieldNames); + return new TinyField( + mergedNames.get(0), + mergedNames, + mergeComments(baseField, overlayField, TinyField::getComments) + ); + } + + protected Collection<TinyMethod> mergeMethods(@Nullable TinyClass baseClass, @Nullable TinyClass overlayClass) { + return mergeChildren(baseClass, overlayClass, + it -> it.getMethodNames().get(baseSharedIndex), + it -> it.getMethodNames().get(overlaySharedIndex), + TinyClass::getMethods, + this::mergeMethod); + } + + protected @NotNull TinyMethod mergeMethod(@Nullable TinyMethod baseMethod, @Nullable TinyMethod overlayMethod) { + var mergedNames = mergeNames(baseMethod, overlayMethod, TinyMethod::getMethodNames); + return new TinyMethod( + mergedNames.get(0), + mergedNames, + mergeChildren(baseMethod, overlayMethod, + TinyMethodParameter::getLvIndex, + TinyMethodParameter::getLvIndex, + TinyMethod::getParameters, + this::mergeMethodParameter), + mergeChildren(baseMethod, overlayMethod, + TinyLocalVariable::getLvIndex, + TinyLocalVariable::getLvIndex, + TinyMethod::getLocalVariables, + this::mergeLocalVariable), + mergeComments(baseMethod, overlayMethod, TinyMethod::getComments) + ); + } + + protected TinyLocalVariable mergeLocalVariable( + @Nullable TinyLocalVariable baseVariable, + @Nullable TinyLocalVariable overlayVariable) { + if (baseVariable == null && overlayVariable == null) + throw new NullPointerException(); + if (overlayVariable != null && baseVariable != null) { + if (overlayVariable.getLvIndex() != baseVariable.getLvIndex()) + throw new IllegalArgumentException("lvIndex must match on local variables with the same shared name"); + if (overlayVariable.getLvStartOffset() != baseVariable.getLvStartOffset()) + throw new IllegalArgumentException("lvStartOffset must match on local variables with the same shared name"); + if (overlayVariable.getLvTableIndex() != baseVariable.getLvTableIndex()) + throw new IllegalArgumentException("lvTableIndex must match on local variables with the same shared name"); + } + return new TinyLocalVariable( + baseVariable == null ? overlayVariable.getLvIndex() : baseVariable.getLvIndex(), + baseVariable == null ? overlayVariable.getLvStartOffset() : baseVariable.getLvStartOffset(), + baseVariable == null ? overlayVariable.getLvTableIndex() : baseVariable.getLvTableIndex(), + mergeNames(baseVariable, overlayVariable, TinyLocalVariable::getLocalVariableNames), + mergeComments(baseVariable, overlayVariable, TinyLocalVariable::getComments) + ); + } + + protected TinyMethodParameter mergeMethodParameter( + @Nullable TinyMethodParameter baseParam, + @Nullable TinyMethodParameter overlayParam) { + if (baseParam == null && overlayParam == null) + throw new NullPointerException(); + if (overlayParam != null && baseParam != null && overlayParam.getLvIndex() != baseParam.getLvIndex()) + throw new IllegalArgumentException("lvIndex must match on method parameters with the same shared name"); + return new TinyMethodParameter( + baseParam != null ? baseParam.getLvIndex() : overlayParam.getLvIndex(), + mergeNames(baseParam, overlayParam, TinyMethodParameter::getParameterNames), + mergeComments(baseParam, overlayParam, TinyMethodParameter::getComments) + ); + } + + protected <B, C, K> List<C> mergeChildren( + @Nullable B base, + @Nullable B overlay, + @NotNull Function<@NotNull C, @NotNull K> sharedBaseName, + @NotNull Function<@NotNull C, @NotNull K> sharedOverlayName, + @NotNull Function<@NotNull B, ? extends @NotNull Collection<@NotNull C>> childExtractor, + @NotNull BiFunction<@Nullable C, @Nullable C, @NotNull C> childMerger + ) { + Map<K, C> baseIndex = + base == null ? Collections.emptyMap() : childExtractor.apply(base).stream() + .collect(Collectors.toMap(sharedBaseName, Function.identity())); + Map<K, C> overlayIndex = + overlay == null ? Collections.emptyMap() : childExtractor.apply(overlay).stream() + .collect(Collectors.toMap(sharedOverlayName, Function.identity())); + var results = new ArrayList<C>(); + if (base != null) + for (var baseObject : childExtractor.apply(base)) { + var sharedName = sharedBaseName.apply(baseObject); + var overlayObject = overlayIndex.get(sharedName); + var mergedClass = childMerger.apply(baseObject, overlayObject); + results.add(mergedClass); + } + if (overlay != null) + for (var overlayObject : childExtractor.apply(overlay)) { + var sharedName = sharedOverlayName.apply(overlayObject); + if (baseIndex.containsKey(sharedName)) + continue; + results.add(overlayObject); + } + return results; + } + + protected List<TinyClass> mergeClasses() { + return (mergeChildren( + base, + overlay, + it -> it.getClassNames().get(baseSharedIndex), + it -> it.getClassNames().get(overlaySharedIndex), + TinyFile::getClassEntries, + this::mergeClass + )); + } +} diff --git a/src/main/java/moe/nea/zwirn/TinyDiffer.java b/src/main/java/moe/nea/zwirn/TinyDiffer.java new file mode 100644 index 0000000..f5fc471 --- /dev/null +++ b/src/main/java/moe/nea/zwirn/TinyDiffer.java @@ -0,0 +1,264 @@ +package moe.nea.zwirn; + +import net.fabricmc.stitch.commands.tinyv2.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class TinyDiffer { + private final TinyFile base; + private final TinyFile overlay; + private final List<String> retainedNamespaces; + int[] retainedToNormalLookup; + protected final int baseSharedIndex; + protected final int overlaySharedIndex; + + public TinyDiffer(@NotNull TinyFile base, @NotNull TinyFile overlay, @NotNull List<String> retainedNamespaces) { + this.base = base; + this.overlay = overlay; + this.retainedNamespaces = retainedNamespaces; + this.baseSharedIndex = base.getHeader().getNamespaces().indexOf(retainedNamespaces.get(0));// TODO: shared namespace argument + this.overlaySharedIndex = overlay.getHeader().getNamespaces().indexOf(retainedNamespaces.get(0)); + + retainedToNormalLookup = retainedNamespaces.stream() + .mapToInt(it -> base.getHeader().getNamespaces().indexOf(it)) + .toArray(); + } + + + public TinyFile createDiff() { + return new TinyFile(new TinyHeader(retainedNamespaces, + overlay.getHeader().getMajorVersion(), + overlay.getHeader().getMinorVersion(), + overlay.getHeader().getProperties()), + diffClasses()); + } + + private <B, C extends Mapping> Retained<List<C>> diffChildrenByMapping( + @Nullable B base, + @Nullable B overlay, + @NotNull Function<@NotNull B, ? extends @NotNull Collection<@NotNull C>> childExtractor, + @NotNull BiFunction<@Nullable C, @Nullable C, @NotNull Retained<@NotNull C>> childDiffer + ) { + return this.diffChildren(base, overlay, + it -> it.getMapping().get(baseSharedIndex), + it -> it.getMapping().get(overlaySharedIndex), + childExtractor, + childDiffer + ); + } + + private <B, C, K> Retained<List<C>> diffChildren( + @Nullable B base, + @Nullable B overlay, + @NotNull Function<@NotNull C, @NotNull K> sharedBaseName, + @NotNull Function<@NotNull C, @NotNull K> sharedOverlayName, + @NotNull Function<@NotNull B, ? extends @NotNull Collection<@NotNull C>> childExtractor, + @NotNull BiFunction<@Nullable C, @Nullable C, @NotNull Retained<@NotNull C>> childDiffer + ) { + Map<K, C> baseIndex = + base == null ? Collections.emptyMap() : childExtractor.apply(base).stream() + .collect(Collectors.toMap(sharedBaseName, Function.identity())); + Map<K, C> overlayIndex = + overlay == null ? Collections.emptyMap() : childExtractor.apply(overlay).stream() + .collect(Collectors.toMap(sharedOverlayName, Function.identity())); + var results = new ArrayList<@NotNull C>(); + if (base != null) + for (var baseObject : childExtractor.apply(base)) { + var sharedName = sharedBaseName.apply(baseObject); + var overlayObject = overlayIndex.get(sharedName); + var mergedClass = childDiffer.apply(baseObject, overlayObject); + if (mergedClass.isMeaningfullyDifferent) + results.add(mergedClass.diff); + } + if (overlay != null) + for (var overlayObject : childExtractor.apply(overlay)) { + var sharedName = sharedOverlayName.apply(overlayObject); + if (baseIndex.containsKey(sharedName)) + continue; + results.add(overlayObject); + } + if (results.isEmpty()) + return Retained.empty(results); + return Retained.strong(results); + } + + private Collection<TinyClass> diffClasses() { + return diffChildrenByMapping(base, overlay, + TinyFile::getClassEntries, + this::diffClass).diff; + } + + private List<String> retainNames(List<String> names) { + List<String> newNames = new ArrayList<>(retainedToNormalLookup.length); + for (int j : retainedToNormalLookup) { + newNames.add(names.get(j)); + } + return newNames; + } + + + private <T extends Mapping> @NotNull Retained<List<String>> diffNamesWithMappings(@Nullable T base, @Nullable T overlay) { + return diffNames(mapNull(base, Mapping::getMapping), mapNull(overlay, Mapping::getMapping)); + } + + private @NotNull Retained<List<String>> diffNames(@Nullable List<String> base, @Nullable List<String> overlay) { + if (overlay == null) return Retained.empty(retainNames(base)); + if (base == null) + return Retained.strong(retainNames(overlay)); + var b = retainNames(base); + var o = retainNames(overlay); + if (o.equals(b)) { + // TODO: return only the shared name and censor others + return Retained.empty(o); + } + return Retained.strong(o); + } + + static class Retained<T> { + private final T diff; + private final boolean isMeaningfullyDifferent; + + public Retained(T diff, boolean isMeaningfullyDifferent) { + this.diff = diff; + this.isMeaningfullyDifferent = isMeaningfullyDifferent; + } + + public static <T> Retained<T> empty(T base) { + return new Retained<>(base, false); + } + + public static <T> Retained<T> strong(T base) { + return new Retained<>(base, true); + } + + public static <T> Retained<T> keep(T base, Retained<?>... dependencies) { + return new Retained<>(base, shouldRetainAny(Arrays.asList(dependencies))); + } + + public static boolean shouldRetainAny(Iterable<Retained<?>> list) { + for (Retained<?> retained : list) { + if (retained.isMeaningfullyDifferent) + return true; + } + return false; + } + } + + private <T, K> @Nullable K mapNull(@Nullable T t, @NotNull Function<@NotNull T, @NotNull K> mapper) { + return t == null ? null : mapper.apply(t); + } + + private Retained<TinyClass> diffClass( + @Nullable TinyClass baseClass, + @Nullable TinyClass overlayClass) { + var names = diffNamesWithMappings(baseClass, overlayClass); + var fields = diffChildrenByMapping(baseClass, overlayClass, TinyClass::getFields, this::diffField); + var methods = diffChildrenByMapping(baseClass, overlayClass, TinyClass::getMethods, this::diffMethod); + var comments = diffComments(baseClass, overlayClass, TinyClass::getComments); + return Retained.keep( + new TinyClass( + names.diff, + methods.diff, + fields.diff, + comments.diff + ), + names, + methods, + fields, + comments + ); + } + + private <T> Retained<List<String>> diffComments(T baseObject, T overlayObject, Function<T, ? extends Collection<String>> commentExtractor) { + if (baseObject == null) + return Retained.strong(new ArrayList<>(commentExtractor.apply(overlayObject))); + if (overlayObject == null) + return Retained.empty(new ArrayList<>(commentExtractor.apply(baseObject))); + var comments = new ArrayList<>(commentExtractor.apply(overlayObject)); + comments.removeAll(commentExtractor.apply(baseObject)); + if (comments.isEmpty()) + return Retained.empty(comments); + return Retained.strong(comments); + } + + private Retained<TinyMethod> diffMethod(@Nullable TinyMethod baseMethod, @Nullable TinyMethod overlayMethod) { + var names = diffNamesWithMappings(baseMethod, overlayMethod); + var params = diffChildren(baseMethod, overlayMethod, TinyMethodParameter::getLvIndex, TinyMethodParameter::getLvIndex, TinyMethod::getParameters, this::diffParam); + var variables = diffChildren(baseMethod, overlayMethod, TinyLocalVariable::getLvIndex, TinyLocalVariable::getLvIndex, TinyMethod::getLocalVariables, this::diffVariable); + var comments = diffComments(baseMethod, overlayMethod, TinyMethod::getComments); + return Retained.keep( + new TinyMethod( + names.diff.get(0), + names.diff, + params.diff, + variables.diff, + comments.diff + ), + names, params, variables, comments + ); + } + + private Retained<TinyLocalVariable> diffVariable(TinyLocalVariable baseVariable, TinyLocalVariable overlayVariable) { + if (overlayVariable == null && baseVariable == null) + throw new NullPointerException(); + if (overlayVariable != null && baseVariable != null) { + if (overlayVariable.getLvIndex() != baseVariable.getLvIndex()) + throw new IllegalArgumentException("lvIndex must match on local variables with the same shared name"); + if (overlayVariable.getLvStartOffset() != baseVariable.getLvStartOffset()) + throw new IllegalArgumentException("lvStartOffset must match on local variables with the same shared name"); + if (overlayVariable.getLvTableIndex() != baseVariable.getLvTableIndex()) + throw new IllegalArgumentException("lvTableIndex must match on local variables with the same shared name"); + } + var names = diffNamesWithMappings(baseVariable, overlayVariable); + var comments = diffComments(baseVariable, overlayVariable, TinyLocalVariable::getComments); + return Retained.keep( + new TinyLocalVariable( + overlayVariable == null ? baseVariable.getLvIndex() : overlayVariable.getLvIndex(), + overlayVariable == null ? baseVariable.getLvStartOffset() : overlayVariable.getLvStartOffset(), + overlayVariable == null ? baseVariable.getLvTableIndex() : overlayVariable.getLvTableIndex(), + names.diff, + comments.diff + ), + names, + comments + ); + } + + private Retained<TinyMethodParameter> diffParam(TinyMethodParameter baseParam, TinyMethodParameter overlayParam) { + if (baseParam == null && overlayParam == null) + throw new NullPointerException(); + if (overlayParam != null && baseParam != null && overlayParam.getLvIndex() != baseParam.getLvIndex()) + throw new IllegalArgumentException("lvIndex must match on method parameters with the same shared name"); + + var names = diffNamesWithMappings(baseParam, overlayParam); + var comments = diffComments(baseParam, overlayParam, TinyMethodParameter::getComments); + return Retained.keep( + new TinyMethodParameter( + baseParam != null ? baseParam.getLvIndex() : overlayParam.getLvIndex(), + names.diff, + comments.diff + ), + names, + comments + ); + } + + private Retained<TinyField> diffField(TinyField baseField, TinyField overlayField) { + var names = diffNamesWithMappings(baseField, overlayField); + var comments = diffComments(baseField, overlayField, TinyField::getComments); + return Retained.keep( + new TinyField( + names.diff.get(0), + names.diff, + comments.diff + ), + names, + comments + ); + } +} diff --git a/src/main/java/moe/nea/zwirn/Zwirn.java b/src/main/java/moe/nea/zwirn/Zwirn.java index db616d7..746c993 100644 --- a/src/main/java/moe/nea/zwirn/Zwirn.java +++ b/src/main/java/moe/nea/zwirn/Zwirn.java @@ -3,6 +3,8 @@ package moe.nea.zwirn; import net.fabricmc.stitch.commands.tinyv2.TinyFile; import org.jetbrains.annotations.NotNull; +import java.util.List; + public class Zwirn { public static @NotNull TinyFile mergeTinyFile( @NotNull TinyFile base, @NotNull TinyFile overlay, @@ -14,4 +16,18 @@ public class Zwirn { throw new IllegalArgumentException("When merging a tiny file, overlay must contain the shared namespace"); return new TinyMerger(base, overlay, sharedNamespace).merge(); } + + public static @NotNull TinyFile createOverlayTinyFile( + @NotNull TinyFile base, @NotNull TinyFile overlay, + @NotNull List<@NotNull String> retainedNamespaces, + @NotNull String sharedNamespace + ) { + if (!base.getHeader().getNamespaces().equals(overlay.getHeader().getNamespaces())) + throw new IllegalArgumentException("Namespaces in input must be equal"); + if (!base.getHeader().getNamespaces().containsAll(retainedNamespaces)) + throw new IllegalArgumentException("Retained namespaces must be present in input files"); + if (!retainedNamespaces.contains(sharedNamespace)) + throw new IllegalArgumentException("Shared namespace must be retained"); + return new TinyDiffer(base, overlay, retainedNamespaces).createDiff(); + } } diff --git a/src/main/java/moe/nea/zwirn/ZwirnUtil.java b/src/main/java/moe/nea/zwirn/ZwirnUtil.java index f17eae6..805155f 100644 --- a/src/main/java/moe/nea/zwirn/ZwirnUtil.java +++ b/src/main/java/moe/nea/zwirn/ZwirnUtil.java @@ -1,7 +1,7 @@ package moe.nea.zwirn; public class ZwirnUtil { - private static <T> T TODO(String message) { + public static <T> T TODO(String message) { throw new RuntimeException("Not yet implemented: " + message); } } |