/* * Copyright (C) 2016, 2018 Player, asie * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package net.fabricmc.tinyremapper; import java.nio.file.Path; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.objectweb.asm.Opcodes; import net.fabricmc.tinyremapper.MemberInstance.MemberType; import net.fabricmc.tinyremapper.TinyRemapper.Direction; public final class ClassInstance { ClassInstance(TinyRemapper context, boolean isInput, InputTag[] inputTags, Path srcFile, byte[] data) { this.context = context; this.isInput = isInput; this.inputTags = inputTags; this.srcPath = srcFile; this.data = data; } void init(String name, String superName, int access, String[] interfaces) { this.name = name; this.superName = superName; this.access = access; this.interfaces = interfaces; } MemberInstance addMember(MemberInstance member) { return members.put(member.getId(), member); } void addInputTags(InputTag[] tags) { if (tags == null || tags.length == 0) return; InputTag[] oldTags; InputTag[] newTags; do { // cas loop oldTags = inputTags; if (oldTags == null) { newTags = tags; } else { // both old and new tags, merge int missingTags = 0; for (InputTag newTag : tags) { boolean found = false; for (InputTag oldTag : oldTags) { if (newTag == oldTag) { found = true; break; } } if (!found) missingTags++; } if (missingTags == 0) return; newTags = Arrays.copyOf(tags, oldTags.length + missingTags); for (InputTag newTag : tags) { boolean found = false; for (InputTag oldTag : oldTags) { if (newTag == oldTag) { found = true; break; } } if (!found) { newTags[newTags.length - missingTags] = newTag; missingTags--; } } } } while (!inputTagsUpdater.compareAndSet(this, oldTags, newTags)); } InputTag[] getInputTags() { return inputTags; } boolean hasAnyInputTag(InputTag[] reqTags) { InputTag[] availTags = inputTags; if (availTags == null) return true; for (InputTag reqTag : reqTags) { for (InputTag availTag : availTags) { if (availTag == reqTag) { return true; } } } return false; } public String getName() { return name; } public String getSuperName() { return superName; } public boolean isInterface() { return (access & Opcodes.ACC_INTERFACE) != 0; } public boolean isPublicOrPrivate() { return (access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PRIVATE)) != 0; } public String[] getInterfaces() { return interfaces; } public Collection<MemberInstance> getMembers() { return members.values(); } public MemberInstance getMember(MemberType type, String id) { return members.get(id); } /** * Rename the member src to dst and continue propagating in dir. * * @param type Member type. * @param idSrc Existing name. * @param idDst New name. * @param dir Futher propagation direction. */ void propagate(TinyRemapper remapper, MemberType type, String originatingCls, String idSrc, String nameDst, Direction dir, boolean isVirtual, boolean first, Set<ClassInstance> visitedUp, Set<ClassInstance> visitedDown) { /* * initial private member or static method in interface: only local * non-virtual: up to matching member (if not already in this), then down until matching again (exclusive) * virtual: all across the hierarchy, only non-private|static can change direction - skip private|static in interfaces */ MemberInstance member = getMember(type, idSrc); if (member != null) { if (!first && !isVirtual) { // down propagation from non-virtual (static) member matching the signature again, which starts its own namespace return; } if (first // directly mapped || (member.access & (Opcodes.ACC_STATIC | Opcodes.ACC_PRIVATE)) == 0 // not private and not static || remapper.propagatePrivate || !remapper.forcePropagation.isEmpty() && remapper.forcePropagation.contains(name.replace('/', '.')+"."+member.name)) { // don't rename private members unless forced or initial (=dir any) if (!member.setNewName(nameDst)) { remapper.conflicts.computeIfAbsent(member, x -> Collections.newSetFromMap(new ConcurrentHashMap<>())).add(originatingCls+"/"+nameDst); } else { member.newNameOriginatingCls = originatingCls; } } if (first && ((member.access & Opcodes.ACC_PRIVATE) != 0 // private members don't propagate, but they may get skipped over by overriding virtual methods || type == MemberType.METHOD && isInterface() && !isVirtual)) { // non-virtual interface methods don't propagate either, the jvm only resolves direct accesses to them return; } } else { // member == null assert !first && (type == MemberType.FIELD || !isInterface() || isVirtual); // potentially intermediately accessed location, handled through resolution in the remapper } assert isVirtual || dir == Direction.DOWN; /* * Propagate the mapping along the hierarchy tree. * * The mapping ensures that overriding and shadowing behaviors remains the same. * * Direction.ANY is from where the current element was the initial node as specified * in the mappings. The member == null + dir checks above already verified that the * member exists in the current node. * * Direction.UP/DOWN handle propagation skipping across nodes which don't contain the * specific member, thus having no direct reference. * * isVirtual && ... handles propagation to an existing matching virtual member, which * spawns a new initial node from the propagation perspective. This is necessary as * different branches of the hierarchy tree that were not visited before may access it. */ if (dir == Direction.ANY || dir == Direction.UP || isVirtual && member != null && (member.access & (Opcodes.ACC_STATIC | Opcodes.ACC_PRIVATE)) == 0) { for (ClassInstance node : parents) { if (visitedUp.add(node)) { node.propagate(remapper, type, originatingCls, idSrc, nameDst, Direction.UP, isVirtual, false, visitedUp, visitedDown); } } } if (dir == Direction.ANY || dir == Direction.DOWN || isVirtual && member != null && (member.access & (Opcodes.ACC_STATIC | Opcodes.ACC_PRIVATE)) == 0) { for (ClassInstance node : children) { if (visitedDown.add(node)) { node.propagate(remapper, type, originatingCls, idSrc, nameDst, Direction.DOWN, isVirtual, false, visitedUp, visitedDown); } } } } public MemberInstance resolve(MemberType type, String id) { MemberInstance member = getMember(type, id); if (member != null) return member; // get from cache member = resolvedMembers.get(id); if (member == null) { // compute member = resolve0(type, id); assert member != null; // put in cache MemberInstance prev = resolvedMembers.putIfAbsent(id, member); if (prev != null) member = prev; } return member != nullMember ? member : null; } private MemberInstance resolve0(MemberType type, String id) { boolean isField = type == MemberType.FIELD; Set<ClassInstance> visited = Collections.newSetFromMap(new IdentityHashMap<>()); Deque<ClassInstance> queue = new ArrayDeque<>(); visited.add(this); ClassInstance context = this; MemberInstance secondaryMatch = null; do { // overall-recursion for fields // step 1 // method: search in all super classes recursively // field: search in all direct super interfaces recursively ClassInstance cls = context; do { for (ClassInstance parent : cls.parents) { if (parent.isInterface() == isField && visited.add(parent)) { MemberInstance ret = parent.getMember(type, id); if (ret != null) return ret; queue.addLast(parent); } } } while ((cls = queue.pollLast()) != null); if (!isField) { visited.clear(); visited.add(context); } // step 2 // method: search for non-static, non-private, non-abstract in all super interfaces recursively // (breadth first search to obtain the potentially maximally-specific superinterface directly) // field: search in all super classes recursively (self-lookup and queue only, outer loop will recurse) // step 3 // method: search for non-static, non-private in all super interfaces recursively // step 3 is a super set of step 2 with any option being able to be "arbitrarily chosen" as per the jvm // spec, so step 2 ignoring the "exactly one" match requirement doesn't matter and >potentially< // maximally-specific superinterface is good enough cls = context; do { for (ClassInstance parent : cls.parents) { if ((!isField || !parent.isInterface()) && visited.add(parent)) { // field -> class, method -> any if (parent.isInterface() != isField) { // field -> class, method -> interface; look in parent MemberInstance parentMember = parent.getMember(type, id); if (parentMember != null && (isField || (parentMember.access & (Opcodes.ACC_STATIC | Opcodes.ACC_PRIVATE)) == 0)) { // potential match if (!isField && (parentMember.access & (Opcodes.ACC_ABSTRACT)) != 0) { secondaryMatch = parentMember; } else { return parentMember; } } } queue.addLast(parent); } } } while (!isField && (cls = queue.pollFirst()) != null); } while ((context = queue.pollFirst()) != null); // overall-recursion for fields return secondaryMatch != null ? secondaryMatch : nullMember; } public MemberInstance resolvePartial(MemberType type, String name, String descPrefix) { String idPrefix = MemberInstance.getId(type, name, descPrefix != null ? descPrefix : "", context.ignoreFieldDesc); boolean isField = type == MemberType.FIELD; MemberInstance member = getMemberPartial(type, idPrefix); if (member == nullMember) return null; // non-unique match Set<ClassInstance> visited = Collections.newSetFromMap(new IdentityHashMap<>()); Deque<ClassInstance> queue = new ArrayDeque<>(); queue.add(this); ClassInstance context = this; MemberInstance secondaryMatch = null; do { // overall-recursion for fields // step 1 // method: search in all super classes recursively // field: search in all direct super interfaces recursively ClassInstance cls = context; do { for (ClassInstance parent : cls.parents) { if (parent.isInterface() == isField && visited.add(parent)) { MemberInstance ret = parent.getMemberPartial(type, idPrefix); if (ret != null) { if (ret == nullMember) { return null; // non-unique match } else if (member == null) { member = ret; } else if (!member.desc.equals(ret.desc)) { return null; // non-unique match } } queue.addLast(parent); } } } while ((cls = queue.pollLast()) != null); if (!isField) { visited.clear(); visited.add(context); } // step 2 // method: search for non-static, non-private, non-abstract in all super interfaces recursively // (breadth first search to obtain the potentially maximally-specific superinterface directly) // field: search in all super classes recursively (self-lookup and queue only, outer loop will recurse) // step 3 // method: search for non-static, non-private in all super interfaces recursively // step 3 is a super set of step 2 with any option being able to be "arbitrarily chosen" as per the jvm // spec, so step 2 ignoring the "exactly one" match requirement doesn't matter and >potentially< // maximally-specific superinterface is good enough cls = context; do { for (ClassInstance parent : cls.parents) { if ((!isField || !parent.isInterface()) && visited.add(parent)) { // field -> class, method -> any if (parent.isInterface() != isField) { // field -> class, method -> interface; look in parent MemberInstance parentMember = parent.getMemberPartial(type, idPrefix); if (parentMember != null && (isField || (parentMember.access & (Opcodes.ACC_STATIC | Opcodes.ACC_PRIVATE)) == 0)) { // potential match if (parentMember == nullMember) { return null; // non-unique match } else if (member == null) { if (!isField && (parentMember.access & (Opcodes.ACC_ABSTRACT)) != 0) { if (secondaryMatch != null && !secondaryMatch.desc.equals(parentMember.desc)) { return null; // non-unique match } else { secondaryMatch = parentMember; } } else { member = parentMember; } } else if (!member.desc.equals(parentMember.desc)) { return null; // non-unique match } } } queue.addLast(parent); } } } while (!isField && (cls = queue.pollFirst()) != null); } while ((context = queue.pollFirst()) != null); // overall-recursion for fields if (secondaryMatch == null) { return member; } else if (member == null) { return secondaryMatch; } else if (member.desc.equals(secondaryMatch.desc)) { return member; } else { return null; // non-unique match } } private MemberInstance getMemberPartial(MemberType type, String idPrefix) { MemberInstance ret = null; for (Map.Entry<String, MemberInstance> entry : members.entrySet()) { if (entry.getValue().type == type && entry.getKey().startsWith(idPrefix)) { if (ret == null) { ret = entry.getValue(); } else { return nullMember; // non-unique match } } } return ret; } @Override public String toString() { return name; } private static final MemberInstance nullMember = new MemberInstance(null, null, null, null, 0); private static final AtomicReferenceFieldUpdater<ClassInstance, InputTag[]> inputTagsUpdater = AtomicReferenceFieldUpdater.newUpdater(ClassInstance.class, InputTag[].class, "inputTags"); final TinyRemapper context; final boolean isInput; private volatile InputTag[] inputTags; // cow input tag list, null for none final Path srcPath; byte[] data; private final Map<String, MemberInstance> members = new HashMap<>(); // methods and fields are distinct due to their different desc separators private final ConcurrentMap<String, MemberInstance> resolvedMembers = new ConcurrentHashMap<>(); final Set<ClassInstance> parents = new HashSet<>(); final Set<ClassInstance> children = new HashSet<>(); private String name; private String superName; private int access; private String[] interfaces; }