All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fitbur.bytecode.StackMapTable Maven / Gradle / Ivy

There is a newer version: 1.0.0
Show newest version
/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in com.fitburpliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later,
 * or the Apache License Version 2.0.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package com.fitbur.bytecode;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.Map;
import com.fitbur.CannotCompileException;

/**
 * stack_map attribute.
 *
 * 

This is an entry in the attributes table of a Code attribute. * It was introduced by J2SE 6 for the verification by * typechecking. * * @see StackMap * @since 3.4 */ public class StackMapTable extends AttributeInfo { /** * The name of this attribute "StackMapTable". */ public static final String tag = "StackMapTable"; /** * Constructs a stack_map attribute. */ StackMapTable(ConstPool cp, byte[] newInfo) { super(cp, tag, newInfo); } StackMapTable(ConstPool cp, int name_id, DataInputStream in) throws IOException { super(cp, name_id, in); } /** * Makes a copy. * * @exception RuntimeCopyException if a BadBytecode * exception is thrown while copying, * it is converted into * RuntimeCopyException. * */ public AttributeInfo copy(ConstPool newCp, Map classnames) throws RuntimeCopyException { try { return new StackMapTable(newCp, new Copier(this.constPool, info, newCp, classnames).doit()); } catch (BadBytecode e) { throw new RuntimeCopyException("bad bytecode. fatal?"); } } /** * An exception that may be thrown by copy() * in StackMapTable. */ public static class RuntimeCopyException extends RuntimeException { /** * Constructs an exception. */ public RuntimeCopyException(String s) { super(s); } } void write(DataOutputStream out) throws IOException { super.write(out); } /** * Top_variable_info.tag. */ public static final int TOP = 0; /** * Integer_variable_info.tag. */ public static final int INTEGER = 1; /** * Float_variable_info.tag. */ public static final int FLOAT = 2; /** * Double_variable_info.tag. */ public static final int DOUBLE = 3; /** * Long_variable_info.tag. */ public static final int LONG = 4; /** * Null_variable_info.tag. */ public static final int NULL = 5; /** * UninitializedThis_variable_info.tag. */ public static final int THIS = 6; /** * Object_variable_info.tag. */ public static final int OBJECT = 7; /** * Uninitialized_variable_info.tag. */ public static final int UNINIT = 8; /** * A code walker for a StackMapTable attribute. */ public static class Walker { byte[] info; int numOfEntries; /** * Constructs a walker. * * @param smt the StackMapTable that this walker * walks around. */ public Walker(StackMapTable smt) { this(smt.get()); } /** * Constructs a walker. * * @param data the info field of the * attribute_info structure. * It can be obtained by get() * in the AttributeInfo class. */ public Walker(byte[] data) { info = data; numOfEntries = ByteArray.readU16bit(data, 0); } /** * Returns the number of the entries. */ public final int size() { return numOfEntries; } /** * Visits each entry of the stack map frames. */ public void parse() throws BadBytecode { int n = numOfEntries; int pos = 2; for (int i = 0; i < n; i++) pos = stackMapFrames(pos, i); } /** * Invoked when the next entry of the stack map frames is visited. * * @param pos the position of the frame in the info * field of attribute_info structure. * @param nth the frame is the N-th * (0, 1st, 2nd, 3rd, 4th, ...) entry. * @return the position of the next frame. */ int stackMapFrames(int pos, int nth) throws BadBytecode { int type = info[pos] & 0xff; if (type < 64) { sameFrame(pos, type); pos++; } else if (type < 128) pos = sameLocals(pos, type); else if (type < 247) throw new BadBytecode("bad frame_type in StackMapTable"); else if (type == 247) // SAME_LOCALS_1_STACK_ITEM_EXTENDED pos = sameLocals(pos, type); else if (type < 251) { int offset = ByteArray.readU16bit(info, pos + 1); chopFrame(pos, offset, 251 - type); pos += 3; } else if (type == 251) { // SAME_FRAME_EXTENDED int offset = ByteArray.readU16bit(info, pos + 1); sameFrame(pos, offset); pos += 3; } else if (type < 255) pos = appendFrame(pos, type); else // FULL_FRAME pos = fullFrame(pos); return pos; } /** * Invoked if the visited frame is a same_frame or * a same_frame_extended. * * @param pos the position of this frame in the info * field of attribute_info structure. * @param offsetDelta */ public void sameFrame(int pos, int offsetDelta) throws BadBytecode {} private int sameLocals(int pos, int type) throws BadBytecode { int top = pos; int offset; if (type < 128) offset = type - 64; else { // type == 247 offset = ByteArray.readU16bit(info, pos + 1); pos += 2; } int tag = info[pos + 1] & 0xff; int data = 0; if (tag == OBJECT || tag == UNINIT) { data = ByteArray.readU16bit(info, pos + 2); objectOrUninitialized(tag, data, pos + 2); pos += 2; } sameLocals(top, offset, tag, data); return pos + 2; } /** * Invoked if the visited frame is a same_locals_1_stack_item_frame * or a same_locals_1_stack_item_frame_extended. * * @param pos the position. * @param offsetDelta * @param stackTag stack[0].tag. * @param stackData stack[0].cpool_index * if the tag is OBJECT, * or stack[0].offset * if the tag is UNINIT. */ public void sameLocals(int pos, int offsetDelta, int stackTag, int stackData) throws BadBytecode {} /** * Invoked if the visited frame is a chop_frame. * * @param pos the position. * @param offsetDelta * @param k the k last locals are absent. */ public void chopFrame(int pos, int offsetDelta, int k) throws BadBytecode {} private int appendFrame(int pos, int type) throws BadBytecode { int k = type - 251; int offset = ByteArray.readU16bit(info, pos + 1); int[] tags = new int[k]; int[] data = new int[k]; int p = pos + 3; for (int i = 0; i < k; i++) { int tag = info[p] & 0xff; tags[i] = tag; if (tag == OBJECT || tag == UNINIT) { data[i] = ByteArray.readU16bit(info, p + 1); objectOrUninitialized(tag, data[i], p + 1); p += 3; } else { data[i] = 0; p++; } } appendFrame(pos, offset, tags, data); return p; } /** * Invoked if the visited frame is a append_frame. * * @param pos the position. * @param offsetDelta * @param tags locals[i].tag. * @param data locals[i].cpool_index * or locals[i].offset. */ public void appendFrame(int pos, int offsetDelta, int[] tags, int[] data) throws BadBytecode {} private int fullFrame(int pos) throws BadBytecode { int offset = ByteArray.readU16bit(info, pos + 1); int numOfLocals = ByteArray.readU16bit(info, pos + 3); int[] localsTags = new int[numOfLocals]; int[] localsData = new int[numOfLocals]; int p = verifyTypeInfo(pos + 5, numOfLocals, localsTags, localsData); int numOfItems = ByteArray.readU16bit(info, p); int[] itemsTags = new int[numOfItems]; int[] itemsData = new int[numOfItems]; p = verifyTypeInfo(p + 2, numOfItems, itemsTags, itemsData); fullFrame(pos, offset, localsTags, localsData, itemsTags, itemsData); return p; } /** * Invoked if the visited frame is full_frame. * * @param pos the position. * @param offsetDelta * @param localTags locals[i].tag * @param localData locals[i].cpool_index * or locals[i].offset * @param stackTags stack[i].tag * @param stackData stack[i].cpool_index * or stack[i].offset */ public void fullFrame(int pos, int offsetDelta, int[] localTags, int[] localData, int[] stackTags, int[] stackData) throws BadBytecode {} private int verifyTypeInfo(int pos, int n, int[] tags, int[] data) { for (int i = 0; i < n; i++) { int tag = info[pos++] & 0xff; tags[i] = tag; if (tag == OBJECT || tag == UNINIT) { data[i] = ByteArray.readU16bit(info, pos); objectOrUninitialized(tag, data[i], pos); pos += 2; } } return pos; } /** * Invoked if Object_variable_info * or Uninitialized_variable_info is visited. * * @param tag OBJECT or UNINIT. * @param data the value of cpool_index or offset. * @param pos the position of cpool_index or offset. */ public void objectOrUninitialized(int tag, int data, int pos) {} } static class SimpleCopy extends Walker { private Writer writer; public SimpleCopy(byte[] data) { super(data); writer = new Writer(data.length); } public byte[] doit() throws BadBytecode { parse(); return writer.toByteArray(); } public void sameFrame(int pos, int offsetDelta) { writer.sameFrame(offsetDelta); } public void sameLocals(int pos, int offsetDelta, int stackTag, int stackData) { writer.sameLocals(offsetDelta, stackTag, copyData(stackTag, stackData)); } public void chopFrame(int pos, int offsetDelta, int k) { writer.chopFrame(offsetDelta, k); } public void appendFrame(int pos, int offsetDelta, int[] tags, int[] data) { writer.appendFrame(offsetDelta, tags, copyData(tags, data)); } public void fullFrame(int pos, int offsetDelta, int[] localTags, int[] localData, int[] stackTags, int[] stackData) { writer.fullFrame(offsetDelta, localTags, copyData(localTags, localData), stackTags, copyData(stackTags, stackData)); } protected int copyData(int tag, int data) { return data; } protected int[] copyData(int[] tags, int[] data) { return data; } } static class Copier extends SimpleCopy { private ConstPool srcPool, com.fitburstPool; private Map classnames; public Copier(ConstPool src, byte[] data, ConstPool com.fitburst, Map names) { super(data); srcPool = src; com.fitburstPool = com.fitburst; classnames = names; } protected int copyData(int tag, int data) { if (tag == OBJECT) return srcPool.copy(data, com.fitburstPool, classnames); else return data; } protected int[] copyData(int[] tags, int[] data) { int[] newData = new int[data.length]; for (int i = 0; i < data.length; i++) if (tags[i] == OBJECT) newData[i] = srcPool.copy(data[i], com.fitburstPool, classnames); else newData[i] = data[i]; return newData; } } /** * Updates this stack map table when a new local variable is inserted * for a new parameter. * * @param index the index of the added local variable. * @param tag the type tag of that local variable. * @param classInfo the index of the CONSTANT_Class_info structure * in a constant pool table. This should be zero unless the tag * is ITEM_Object. * * @see com.fitbur.CtBehavior#addParameter(com.fitbur.CtClass) * @see #typeTagOf(char) * @see ConstPool */ public void insertLocal(int index, int tag, int classInfo) throws BadBytecode { byte[] data = new InsertLocal(this.get(), index, tag, classInfo).doit(); this.set(data); } /** * Returns the tag of the type specified by the * com.fitburscriptor. This method returns INTEGER * unless the com.fitburscriptor is either D (double), F (float), * J (long), L (class type), or [ (array). * * @param com.fitburscriptor the type com.fitburscriptor. * @see Descriptor */ public static int typeTagOf(char com.fitburscriptor) { switch (com.fitburscriptor) { case 'D' : return DOUBLE; case 'F' : return FLOAT; case 'J' : return LONG; case 'L' : case '[' : return OBJECT; // case 'V' : com.fitburfault : return INTEGER; } } /* This implementation assumes that a local variable initially * holding a parameter value is never changed to be a different * type. * */ static class InsertLocal extends SimpleCopy { private int varIndex; private int varTag, varData; public InsertLocal(byte[] data, int varIndex, int varTag, int varData) { super(data); this.varIndex = varIndex; this.varTag = varTag; this.varData = varData; } public void fullFrame(int pos, int offsetDelta, int[] localTags, int[] localData, int[] stackTags, int[] stackData) { int len = localTags.length; if (len < varIndex) { super.fullFrame(pos, offsetDelta, localTags, localData, stackTags, stackData); return; } int typeSize = (varTag == LONG || varTag == DOUBLE) ? 2 : 1; int[] localTags2 = new int[len + typeSize]; int[] localData2 = new int[len + typeSize]; int index = varIndex; int j = 0; for (int i = 0; i < len; i++) { if (j == index) j += typeSize; localTags2[j] = localTags[i]; localData2[j++] = localData[i]; } localTags2[index] = varTag; localData2[index] = varData; if (typeSize > 1) { localTags2[index + 1] = TOP; localData2[index + 1] = 0; } super.fullFrame(pos, offsetDelta, localTags2, localData2, stackTags, stackData); } } /** * A writer of stack map tables. */ public static class Writer { ByteArrayOutputStream output; int numOfEntries; /** * Constructs a writer. * @param size the initial buffer size. */ public Writer(int size) { output = new ByteArrayOutputStream(size); numOfEntries = 0; output.write(0); // u2 number_of_entries output.write(0); } /** * Returns the stack map table written out. */ public byte[] toByteArray() { byte[] b = output.toByteArray(); ByteArray.write16bit(numOfEntries, b, 0); return b; } /** * Constructs and a return a stack map table containing * the written stack map entries. * * @param cp the constant pool used to write * the stack map entries. */ public StackMapTable toStackMapTable(ConstPool cp) { return new StackMapTable(cp, toByteArray()); } /** * Writes a same_frame or a same_frame_extended. */ public void sameFrame(int offsetDelta) { numOfEntries++; if (offsetDelta < 64) output.write(offsetDelta); else { output.write(251); // SAME_FRAME_EXTENDED write16(offsetDelta); } } /** * Writes a same_locals_1_stack_item * or a same_locals_1_stack_item_extended. * * @param tag stack[0].tag. * @param data stack[0].cpool_index * if the tag is OBJECT, * or stack[0].offset * if the tag is UNINIT. * Otherwise, this parameter is not used. */ public void sameLocals(int offsetDelta, int tag, int data) { numOfEntries++; if (offsetDelta < 64) output.write(offsetDelta + 64); else { output.write(247); // SAME_LOCALS_1_STACK_ITEM_EXTENDED write16(offsetDelta); } writeTypeInfo(tag, data); } /** * Writes a chop_frame. * * @param k the number of absent locals. 1, 2, or 3. */ public void chopFrame(int offsetDelta, int k) { numOfEntries++; output.write(251 - k); write16(offsetDelta); } /** * Writes a append_frame. The number of the appended * locals is specified by the length of tags. * * @param tags locals[].tag. * The length of this array must be * either 1, 2, or 3. * @param data locals[].cpool_index * if the tag is OBJECT, * or locals[].offset * if the tag is UNINIT. * Otherwise, this parameter is not used. */ public void appendFrame(int offsetDelta, int[] tags, int[] data) { numOfEntries++; int k = tags.length; // k is 1, 2, or 3 output.write(k + 251); write16(offsetDelta); for (int i = 0; i < k; i++) writeTypeInfo(tags[i], data[i]); } /** * Writes a full_frame. * number_of_locals and number_of_stack_items * are specified by the the length of localTags and * stackTags. * * @param localTags locals[].tag. * @param localData locals[].cpool_index * if the tag is OBJECT, * or locals[].offset * if the tag is UNINIT. * Otherwise, this parameter is not used. * @param stackTags stack[].tag. * @param stackData stack[].cpool_index * if the tag is OBJECT, * or stack[].offset * if the tag is UNINIT. * Otherwise, this parameter is not used. */ public void fullFrame(int offsetDelta, int[] localTags, int[] localData, int[] stackTags, int[] stackData) { numOfEntries++; output.write(255); // FULL_FRAME write16(offsetDelta); int n = localTags.length; write16(n); for (int i = 0; i < n; i++) writeTypeInfo(localTags[i], localData[i]); n = stackTags.length; write16(n); for (int i = 0; i < n; i++) writeTypeInfo(stackTags[i], stackData[i]); } private void writeTypeInfo(int tag, int data) { output.write(tag); if (tag == OBJECT || tag == UNINIT) write16(data); } private void write16(int value) { output.write((value >>> 8) & 0xff); output.write(value & 0xff); } } /** * Prints the stack table map. */ public void println(PrintWriter w) { Printer.print(this, w); } /** * Prints the stack table map. * * @param ps a print stream such as System.out. */ public void println(java.io.PrintStream ps) { Printer.print(this, new java.io.PrintWriter(ps, true)); } static class Printer extends Walker { private PrintWriter writer; private int offset; /** * Prints the stack table map. */ public static void print(StackMapTable smt, PrintWriter writer) { try { new Printer(smt.get(), writer).parse(); } catch (BadBytecode e) { writer.println(e.getMessage()); } } Printer(byte[] data, PrintWriter pw) { super(data); writer = pw; offset = -1; } public void sameFrame(int pos, int offsetDelta) { offset += offsetDelta + 1; writer.println(offset + " same frame: " + offsetDelta); } public void sameLocals(int pos, int offsetDelta, int stackTag, int stackData) { offset += offsetDelta + 1; writer.println(offset + " same locals: " + offsetDelta); printTypeInfo(stackTag, stackData); } public void chopFrame(int pos, int offsetDelta, int k) { offset += offsetDelta + 1; writer.println(offset + " chop frame: " + offsetDelta + ", " + k + " last locals"); } public void appendFrame(int pos, int offsetDelta, int[] tags, int[] data) { offset += offsetDelta + 1; writer.println(offset + " append frame: " + offsetDelta); for (int i = 0; i < tags.length; i++) printTypeInfo(tags[i], data[i]); } public void fullFrame(int pos, int offsetDelta, int[] localTags, int[] localData, int[] stackTags, int[] stackData) { offset += offsetDelta + 1; writer.println(offset + " full frame: " + offsetDelta); writer.println("[locals]"); for (int i = 0; i < localTags.length; i++) printTypeInfo(localTags[i], localData[i]); writer.println("[stack]"); for (int i = 0; i < stackTags.length; i++) printTypeInfo(stackTags[i], stackData[i]); } private void printTypeInfo(int tag, int data) { String msg = null; switch (tag) { case TOP : msg = "top"; break; case INTEGER : msg = "integer"; break; case FLOAT : msg = "float"; break; case DOUBLE : msg = "double"; break; case LONG : msg = "long"; break; case NULL : msg = "null"; break; case THIS : msg = "this"; break; case OBJECT : msg = "object (cpool_index " + data + ")"; break; case UNINIT : msg = "uninitialized (offset " + data + ")"; break; } writer.print(" "); writer.println(msg); } } void shiftPc(int where, int gapSize, boolean exclusive) throws BadBytecode { new OffsetShifter(this, where, gapSize).parse(); new Shifter(this, where, gapSize, exclusive).doit(); } static class OffsetShifter extends Walker { int where, gap; public OffsetShifter(StackMapTable smt, int where, int gap) { super(smt); this.where = where; this.gap = gap; } public void objectOrUninitialized(int tag, int data, int pos) { if (tag == UNINIT) if (where <= data) ByteArray.write16bit(data + gap, info, pos); } } static class Shifter extends Walker { private StackMapTable stackMap; int where, gap; int position; byte[] updatedInfo; boolean exclusive; public Shifter(StackMapTable smt, int where, int gap, boolean exclusive) { super(smt); stackMap = smt; this.where = where; this.gap = gap; this.position = 0; this.updatedInfo = null; this.exclusive = exclusive; } public void doit() throws BadBytecode { parse(); if (updatedInfo != null) stackMap.set(updatedInfo); } public void sameFrame(int pos, int offsetDelta) { update(pos, offsetDelta, 0, 251); } public void sameLocals(int pos, int offsetDelta, int stackTag, int stackData) { update(pos, offsetDelta, 64, 247); } void update(int pos, int offsetDelta, int base, int entry) { int oldPos = position; position = oldPos + offsetDelta + (oldPos == 0 ? 0 : 1); boolean match; if (exclusive) match = oldPos < where && where <= position; else match = oldPos <= where && where < position; if (match) { int newDelta = offsetDelta + gap; position += gap; if (newDelta < 64) info[pos] = (byte)(newDelta + base); else if (offsetDelta < 64) { byte[] newinfo = insertGap(info, pos, 2); newinfo[pos] = (byte)entry; ByteArray.write16bit(newDelta, newinfo, pos + 1); updatedInfo = newinfo; } else ByteArray.write16bit(newDelta, info, pos + 1); } } static byte[] insertGap(byte[] info, int where, int gap) { int len = info.length; byte[] newinfo = new byte[len + gap]; for (int i = 0; i < len; i++) newinfo[i + (i < where ? 0 : gap)] = info[i]; return newinfo; } public void chopFrame(int pos, int offsetDelta, int k) { update(pos, offsetDelta); } public void appendFrame(int pos, int offsetDelta, int[] tags, int[] data) { update(pos, offsetDelta); } public void fullFrame(int pos, int offsetDelta, int[] localTags, int[] localData, int[] stackTags, int[] stackData) { update(pos, offsetDelta); } void update(int pos, int offsetDelta) { int oldPos = position; position = oldPos + offsetDelta + (oldPos == 0 ? 0 : 1); boolean match; if (exclusive) match = oldPos < where && where <= position; else match = oldPos <= where && where < position; if (match) { int newDelta = offsetDelta + gap; ByteArray.write16bit(newDelta, info, pos + 1); position += gap; } } } /** * @see CodeIterator.Switcher#adjustOffsets(int, int) */ void shiftForSwitch(int where, int gapSize) throws BadBytecode { new SwitchShifter(this, where, gapSize).doit(); } static class SwitchShifter extends Shifter { SwitchShifter(StackMapTable smt, int where, int gap) { super(smt, where, gap, false); } void update(int pos, int offsetDelta, int base, int entry) { int oldPos = position; position = oldPos + offsetDelta + (oldPos == 0 ? 0 : 1); int newDelta = offsetDelta; if (where == position) newDelta = offsetDelta - gap; else if (where == oldPos) newDelta = offsetDelta + gap; else return; if (offsetDelta < 64) if (newDelta < 64) info[pos] = (byte)(newDelta + base); else { byte[] newinfo = insertGap(info, pos, 2); newinfo[pos] = (byte)entry; ByteArray.write16bit(newDelta, newinfo, pos + 1); updatedInfo = newinfo; } else if (newDelta < 64) { byte[] newinfo = com.fitburleteGap(info, pos, 2); newinfo[pos] = (byte)(newDelta + base); updatedInfo = newinfo; } else ByteArray.write16bit(newDelta, info, pos + 1); } static byte[] com.fitburleteGap(byte[] info, int where, int gap) { where += gap; int len = info.length; byte[] newinfo = new byte[len - gap]; for (int i = 0; i < len; i++) newinfo[i - (i < where ? 0 : gap)] = info[i]; return newinfo; } void update(int pos, int offsetDelta) { int oldPos = position; position = oldPos + offsetDelta + (oldPos == 0 ? 0 : 1); int newDelta = offsetDelta; if (where == position) newDelta = offsetDelta - gap; else if (where == oldPos) newDelta = offsetDelta + gap; else return; ByteArray.write16bit(newDelta, info, pos + 1); } } /** * Undocumented method. Do not use; internal-use only. * *

This method is for com.fitbur.convert.TransformNew. * It is called to update the stack map table when * the NEW opcode (and the following DUP) is removed. * * @param where the position of the removed NEW opcode. */ public void removeNew(int where) throws CannotCompileException { try { byte[] data = new NewRemover(this.get(), where).doit(); this.set(data); } catch (BadBytecode e) { throw new CannotCompileException("bad stack map table", e); } } static class NewRemover extends SimpleCopy { int posOfNew; public NewRemover(byte[] data, int pos) { super(data); posOfNew = pos; } public void sameLocals(int pos, int offsetDelta, int stackTag, int stackData) { if (stackTag == UNINIT && stackData == posOfNew) super.sameFrame(pos, offsetDelta); else super.sameLocals(pos, offsetDelta, stackTag, stackData); } public void fullFrame(int pos, int offsetDelta, int[] localTags, int[] localData, int[] stackTags, int[] stackData) { int n = stackTags.length - 1; for (int i = 0; i < n; i++) if (stackTags[i] == UNINIT && stackData[i] == posOfNew && stackTags[i + 1] == UNINIT && stackData[i + 1] == posOfNew) { n++; int[] stackTags2 = new int[n - 2]; int[] stackData2 = new int[n - 2]; int k = 0; for (int j = 0; j < n; j++) if (j == i) j++; else { stackTags2[k] = stackTags[j]; stackData2[k++] = stackData[j]; } stackTags = stackTags2; stackData = stackData2; break; } super.fullFrame(pos, offsetDelta, localTags, localData, stackTags, stackData); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy