/*
* Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.java.util.jar.pack;
import com.sun.java.util.jar.pack.ConstantPool.Entry;
import com.sun.java.util.jar.pack.ConstantPool.Index;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.sun.java.util.jar.pack.Constants.*;
/**
* Represents an attribute in a class-file.
* Takes care to remember where constant pool indexes occur.
* Implements the "little language" of Pack200 for describing
* attribute layouts.
* @author John Rose
*/
class Attribute implements Comparable<Attribute> {
// Attribute instance fields.
Layout def; // the name and format of this attr
byte[] bytes; // the actual bytes
Object fixups; // reference relocations, if any are required
public String name() { return def.name(); }
public Layout layout() { return def; }
public byte[] bytes() { return bytes; }
public int size() { return bytes.length; }
public Entry getNameRef() { return def.getNameRef(); }
private Attribute(Attribute old) {
this.def = old.def;
this.bytes = old.bytes;
this.fixups = old.fixups;
}
public Attribute(Layout def, byte[] bytes, Object fixups) {
this.def = def;
this.bytes = bytes;
this.fixups = fixups;
Fixups.setBytes(fixups, bytes);
}
public Attribute(Layout def, byte[] bytes) {
this(def, bytes, null);
}
public Attribute addContent(byte[] bytes, Object fixups) {
assert(isCanonical());
if (bytes.length == 0 && fixups == null)
return this;
Attribute res = new Attribute(this);
res.bytes = bytes;
res.fixups = fixups;
Fixups.setBytes(fixups, bytes);
return res;
}
public Attribute addContent(byte[] bytes) {
return addContent(bytes, null);
}
public void finishRefs(Index ix) {
if (fixups != null) {
Fixups.finishRefs(fixups, bytes, ix);
fixups = null;
}
}
public boolean isCanonical() {
return this == def.canon;
}
@Override
public int compareTo(Attribute that) {
return this.def.compareTo(that.def);
}
private static final Map<List<Attribute>, List<Attribute>> canonLists = new HashMap<>();
private static final Map<Layout, Attribute> attributes = new HashMap<>();
private static final Map<Layout, Attribute> standardDefs = new HashMap<>();
// Canonicalized lists of trivial attrs (Deprecated, etc.)
// are used by trimToSize, in order to reduce footprint
// of some common cases. (Note that Code attributes are
// always zero size.)
public static List<Attribute> getCanonList(List<Attribute> al) {
synchronized (canonLists) {
List<Attribute> cl = canonLists.get(al);
if (cl == null) {
cl = new ArrayList<>(al.size());
cl.addAll(al);
cl = Collections.unmodifiableList(cl);
canonLists.put(al, cl);
}
return cl;
}
}
// Find the canonical empty attribute with the given ctype, name, layout.
public static Attribute find(int ctype, String name, String layout) {
Layout key = Layout.makeKey(ctype, name, layout);
synchronized (attributes) {
Attribute a = attributes.get(key);
if (a == null) {
a = new Layout(ctype, name, layout).canonicalInstance();
attributes.put(key, a);
}
return a;
}
}
public static Layout keyForLookup(int ctype, String name) {
return Layout.makeKey(ctype, name);
}
// Find canonical empty attribute with given ctype and name,
// and with the standard layout.
public static Attribute lookup(Map<Layout, Attribute> defs, int ctype,
String name) {
if (defs == null) {
defs = standardDefs;
}
return defs.get(Layout.makeKey(ctype, name));
}
public static Attribute define(Map<Layout, Attribute> defs, int ctype,
String name, String layout) {
Attribute a = find(ctype, name, layout);
defs.put(Layout.makeKey(ctype, name), a);
return a;
}
static {
Map<Layout, Attribute> sd = standardDefs;
define(sd, ATTR_CONTEXT_CLASS, "Signature", "RSH");
define(sd, ATTR_CONTEXT_CLASS, "Synthetic", "");
define(sd, ATTR_CONTEXT_CLASS, "Deprecated", "");
define(sd, ATTR_CONTEXT_CLASS, "SourceFile", "RUH");
define(sd, ATTR_CONTEXT_CLASS, "EnclosingMethod", "RCHRDNH");
define(sd, ATTR_CONTEXT_CLASS, "InnerClasses", "NH[RCHRCNHRUNHFH]");
define(sd, ATTR_CONTEXT_CLASS, "BootstrapMethods", "NH[RMHNH[KLH]]");
define(sd, ATTR_CONTEXT_FIELD, "Signature", "RSH");
define(sd, ATTR_CONTEXT_FIELD, "Synthetic", "");
define(sd, ATTR_CONTEXT_FIELD, "Deprecated", "");
define(sd, ATTR_CONTEXT_FIELD, "ConstantValue", "KQH");
define(sd, ATTR_CONTEXT_METHOD, "Signature", "RSH");
define(sd, ATTR_CONTEXT_METHOD, "Synthetic", "");
define(sd, ATTR_CONTEXT_METHOD, "Deprecated", "");
define(sd, ATTR_CONTEXT_METHOD, "Exceptions", "NH[RCH]");
define(sd, ATTR_CONTEXT_METHOD, "MethodParameters", "NB[RUNHFH]");
//define(sd, ATTR_CONTEXT_METHOD, "Code", "HHNI[B]NH[PHPOHPOHRCNH]NH[RUHNI[B]]");
define(sd, ATTR_CONTEXT_CODE, "StackMapTable",
("[NH[(1)]]" +
"[TB" +
"(64-127)[(2)]" +
"(247)[(1)(2)]" +
"(248-251)[(1)]" +
"(252)[(1)(2)]" +
"(253)[(1)(2)(2)]" +
"(254)[(1)(2)(2)(2)]" +
"(255)[(1)NH[(2)]NH[(2)]]" +
"()[]" +
"]" +
"[H]" +
"[TB(7)[RCH](8)[PH]()[]]"));
define(sd, ATTR_CONTEXT_CODE, "LineNumberTable", "NH[PHH]");
define(sd, ATTR_CONTEXT_CODE, "LocalVariableTable", "NH[PHOHRUHRSHH]");
define(sd, ATTR_CONTEXT_CODE, "LocalVariableTypeTable", "NH[PHOHRUHRSHH]");
//define(sd, ATTR_CONTEXT_CODE, "CharacterRangeTable", "NH[PHPOHIIH]");
//define(sd, ATTR_CONTEXT_CODE, "CoverageTable", "NH[PHHII]");
// Note: Code and InnerClasses are special-cased elsewhere.
// Their layout specs. are given here for completeness.
// The Code spec is incomplete, in that it does not distinguish
// bytecode bytes or locate CP references.
// The BootstrapMethods attribute is also special-cased
// elsewhere as an appendix to the local constant pool.
}
// Metadata.
//
// We define metadata using similar layouts
// for all five kinds of metadata attributes and 2 type metadata attributes
//
// Regular annotations are a counted list of [RSHNH[RUH(1)]][...]
// pack.method.attribute.RuntimeVisibleAnnotations=[NH[(1)]][RSHNH[RUH(1)]][TB...]
//
// Parameter annotations are a counted list of regular annotations.
// pack.method.attribute.RuntimeVisibleParameterAnnotations=[NB[(1)]][NH[(1)]][RSHNH[RUH(1)]][TB...]
//
// RuntimeInvisible annotations are defined similarly...
// Non-method annotations are defined similarly...
//
// Annotation are a simple tagged value [TB...]
// pack.attribute.method.AnnotationDefault=[TB...]
static {
String mdLayouts[] = {
Attribute.normalizeLayoutString
(""
+"\n # parameter_annotations :="
+"\n [ NB[(1)] ] # forward call to annotations"
),
Attribute.normalizeLayoutString
(""
+"\n # annotations :="
+"\n [ NH[(1)] ] # forward call to annotation"
+"\n "
),
Attribute.normalizeLayoutString
(""
+"\n # annotation :="
+"\n [RSH"
+"\n NH[RUH (1)] # forward call to value"
+"\n ]"
),
Attribute.normalizeLayoutString
(""
+"\n # value :="
+"\n [TB # Callable 2 encodes one tagged value."
+"\n (\\B,\\C,\\I,\\S,\\Z)[KIH]"
+"\n (\\D)[KDH]"
+"\n (\\F)[KFH]"
+"\n (\\J)[KJH]"
+"\n (\\c)[RSH]"
+"\n (\\e)[RSH RUH]"
+"\n (\\s)[RUH]"
+"\n (\\[)[NH[(0)]] # backward self-call to value"
+"\n (\\@)[RSH NH[RUH (0)]] # backward self-call to value"
+"\n ()[] ]"
)
};
/*
* RuntimeVisibleTypeAnnotation and RuntimeInvisibleTypeAnnotatation are
* similar to RuntimeVisibleAnnotation and RuntimeInvisibleAnnotation,
* a type-annotation union and a type-path structure precedes the
* annotation structure
*/
String typeLayouts[] = {
Attribute.normalizeLayoutString
(""
+"\n # type-annotations :="
+"\n [ NH[(1)(2)(3)] ] # forward call to type-annotations"
),
Attribute.normalizeLayoutString
( ""
+"\n # type-annotation :="
+"\n [TB"
+"\n (0-1) [B] # {CLASS, METHOD}_TYPE_PARAMETER"
+"\n (16) [FH] # CLASS_EXTENDS"
+"\n (17-18) [BB] # {CLASS, METHOD}_TYPE_PARAMETER_BOUND"
+"\n (19-21) [] # FIELD, METHOD_RETURN, METHOD_RECEIVER"
+"\n (22) [B] # METHOD_FORMAL_PARAMETER"
+"\n (23) [H] # THROWS"
+"\n (64-65) [NH[PHOHH]] # LOCAL_VARIABLE, RESOURCE_VARIABLE"
+"\n (66) [H] # EXCEPTION_PARAMETER"
+"\n (67-70) [PH] # INSTANCEOF, NEW, {CONSTRUCTOR, METHOD}_REFERENCE_RECEIVER"
+"\n (71-75) [PHB] # CAST, {CONSTRUCTOR,METHOD}_INVOCATION_TYPE_ARGUMENT, {CONSTRUCTOR, METHOD}_REFERENCE_TYPE_ARGUMENT"
+"\n ()[] ]"
),
Attribute.normalizeLayoutString
(""
+"\n # type-path"
+"\n [ NB[BB] ]"
)
};
Map<Layout, Attribute> sd = standardDefs;
String defaultLayout = mdLayouts[3];
String annotationsLayout = mdLayouts[1] + mdLayouts[2] + mdLayouts[3];
String paramsLayout = mdLayouts[0] + annotationsLayout;
String typesLayout = typeLayouts[0] + typeLayouts[1] +
typeLayouts[2] + mdLayouts[2] + mdLayouts[3];
for (int ctype = 0; ctype < ATTR_CONTEXT_LIMIT; ctype++) {
if (ctype != ATTR_CONTEXT_CODE) {
define(sd, ctype,
"RuntimeVisibleAnnotations", annotationsLayout);
define(sd, ctype,
"RuntimeInvisibleAnnotations", annotationsLayout);
if (ctype == ATTR_CONTEXT_METHOD) {
define(sd, ctype,
"RuntimeVisibleParameterAnnotations", paramsLayout);
define(sd, ctype,
"RuntimeInvisibleParameterAnnotations", paramsLayout);
define(sd, ctype,
"AnnotationDefault", defaultLayout);
}
}
define(sd, ctype,
"RuntimeVisibleTypeAnnotations", typesLayout);
define(sd, ctype,
"RuntimeInvisibleTypeAnnotations", typesLayout);
}
}
public static String contextName(int ctype) {
switch (ctype) {
case ATTR_CONTEXT_CLASS: return "class";
case ATTR_CONTEXT_FIELD: return "field";
case ATTR_CONTEXT_METHOD: return "method";
case ATTR_CONTEXT_CODE: return "code";
}
return null;
}
/** Base class for any attributed object (Class, Field, Method, Code).
* Flags are included because they are used to help transmit the
* presence of attributes. That is, flags are a mix of modifier
* bits and attribute indicators.
*/
public static abstract
class Holder {
// We need this abstract method to interpret embedded CP refs.
protected abstract Entry[] getCPMap();
protected int flags; // defined here for convenience
protected List<Attribute> attributes;
public int attributeSize() {
return (attributes == null) ? 0 : attributes.size();
}
public void trimToSize() {
if (attributes == null) {
return;
}
if (attributes.isEmpty()) {
attributes = null;
return;
}
if (attributes instanceof ArrayList) {
ArrayList<Attribute> al = (ArrayList<Attribute>)attributes;
al.trimToSize();
boolean allCanon = true;
for (Attribute a : al) {
if (!a.isCanonical()) {
allCanon = false;
}
if (a.fixups != null) {
assert(!a.isCanonical());
a.fixups = Fixups.trimToSize(a.fixups);
}
}
if (allCanon) {
// Replace private writable attribute list
// with only trivial entries by public unique
// immutable attribute list with the same entries.
attributes = getCanonList(al);
}
}
}
public void addAttribute(Attribute a) {
if (attributes == null)
attributes = new ArrayList<>(3);
else if (!(attributes instanceof ArrayList))
attributes = new ArrayList<>(attributes); // unfreeze it
attributes.add(a);
}
public Attribute removeAttribute(Attribute a) {
if (attributes == null) return null;
if (!attributes.contains(a)) return null;
if (!(attributes instanceof ArrayList))
attributes = new ArrayList<>(attributes); // unfreeze it
attributes.remove(a);
return a;
}
public Attribute getAttribute(int n) {
return attributes.get(n);
}
protected void visitRefs(int mode, Collection<Entry> refs) {
if (attributes == null) return;
for (Attribute a : attributes) {
a.visitRefs(this, mode, refs);
}
}
static final List<Attribute> noAttributes = Arrays.asList(new Attribute[0]);
public List<Attribute> getAttributes() {
if (attributes == null)
return noAttributes;
return attributes;
}
public void setAttributes(List<Attribute> attrList) {
if (attrList.isEmpty())
attributes = null;
else
attributes = attrList;
}
public Attribute getAttribute(String attrName) {
if (attributes == null) return null;
for (Attribute a : attributes) {
if (a.name().equals(attrName))
return a;
}
return null;
}
public Attribute getAttribute(Layout attrDef) {
if (attributes == null) return null;
for (Attribute a : attributes) {
if (a.layout() == attrDef)
return a;
}
return null;
}
public Attribute removeAttribute(String attrName) {
return removeAttribute(getAttribute(attrName));
}
public Attribute removeAttribute(Layout attrDef) {
return removeAttribute(getAttribute(attrDef));
}
public void strip(String attrName) {
removeAttribute(getAttribute(attrName));
}
}
// Lightweight interface to hide details of band structure.
// Also used for testing.
public static abstract
class ValueStream {
public int getInt(int bandIndex) { throw undef(); }
public void putInt(int bandIndex, int value) { throw undef(); }
public Entry getRef(int bandIndex) { throw undef(); }
public void putRef(int bandIndex, Entry ref) { throw undef(); }
// Note: decodeBCI goes w/ getInt/Ref; encodeBCI goes w/ putInt/Ref
public int decodeBCI(int bciCode) { throw undef(); }
public int encodeBCI(int bci) { throw undef(); }
public void noteBackCall(int whichCallable) { /* ignore by default */ }
private RuntimeException undef() {
return new UnsupportedOperationException("ValueStream method");
}
}
// Element kinds:
static final byte EK_INT = 1; // B H I SH etc.
static final byte EK_BCI = 2; // PH POH etc.
static final byte EK_BCO = 3; // OH etc.
static final byte EK_FLAG = 4; // FH etc.
static final byte EK_REPL = 5; // NH[...] etc.
static final byte EK_REF = 6; // RUH, RUNH, KQH, etc.
static final byte EK_UN = 7; // TB(...)[...] etc.
static final byte EK_CASE = 8; // (...)[...] etc.
static final byte EK_CALL = 9; // (0), (1), etc.
static final byte EK_CBLE = 10; // [...][...] etc.
static final byte EF_SIGN = 1<<0; // INT is signed
static final byte EF_DELTA = 1<<1; // BCI/BCI value is diff'ed w/ previous
static final byte EF_NULL = 1<<2; // null REF is expected/allowed
static final byte EF_BACK = 1<<3; // call, callable, case is backward
static final int NO_BAND_INDEX = -1;
/** A "class" of attributes, characterized by a context-type, name
* and format. The formats are specified in a "little language".
*/
public static
class Layout implements Comparable<Layout> {
int ctype; // attribute context type, e.g., ATTR_CONTEXT_CODE
String name; // name of attribute
boolean hasRefs; // this kind of attr contains CP refs?
String layout; // layout specification
int bandCount; // total number of elems
Element[] elems; // tokenization of layout
Attribute canon; // canonical instance of this layout
public int ctype() { return ctype; }
public String name() { return name; }
public String layout() { return layout; }
public Attribute canonicalInstance() { return canon; }
public Entry getNameRef() {
return ConstantPool.getUtf8Entry(name());
}
public boolean isEmpty() {
return layout.isEmpty();
}
public Layout(int ctype, String name, String layout) {
this.ctype = ctype;
this.name = name.intern();
this.layout = layout.intern();
assert(ctype < ATTR_CONTEXT_LIMIT);
boolean hasCallables = layout.startsWith("[");
try {
if (!hasCallables) {
this.elems = tokenizeLayout(this, -1, layout);
} else {
String[] bodies = splitBodies(layout);
// Make the callables now, so they can be linked immediately.
Element[] lelems = new Element[bodies.length];
this.elems = lelems;
for (int i = 0; i < lelems.length; i++) {
Element ce = this.new Element();
ce.kind = EK_CBLE;
ce.removeBand();
ce.bandIndex = NO_BAND_INDEX;
ce.layout = bodies[i];
lelems[i] = ce;
}
// Next fill them in.
for (int i = 0; i < lelems.length; i++) {
Element ce = lelems[i];
ce.body = tokenizeLayout(this, i, bodies[i]);
}
//System.out.println(Arrays.asList(elems));
}
} catch (StringIndexOutOfBoundsException ee) {
// simplest way to catch syntax errors...
throw new RuntimeException("Bad attribute layout: "+layout, ee);
}
// Some uses do not make a fresh one for each occurrence.
// For example, if layout == "", we only need one attr to share.
canon = new Attribute(this, noBytes);
}
private Layout() {}
static Layout makeKey(int ctype, String name, String layout) {
Layout def = new Layout();
def.ctype = ctype;
def.name = name.intern();
def.layout = layout.intern();
assert(ctype < ATTR_CONTEXT_LIMIT);
return def;
}
static Layout makeKey(int ctype, String name) {
return makeKey(ctype, name, "");
}
public Attribute addContent(byte[] bytes, Object fixups) {
return canon.addContent(bytes, fixups);
}
public Attribute addContent(byte[] bytes) {
return canon.addContent(bytes, null);
}
@Override
public boolean equals(Object x) {
return ( x != null) && ( x.getClass() == Layout.class ) &&
equals((Layout)x);
}
public boolean equals(Layout that) {
return this.name.equals(that.name)
&& this.layout.equals(that.layout)
&& this.ctype == that.ctype;
}
@Override
public int hashCode() {
return (((17 + name.hashCode())
* 37 + layout.hashCode())
* 37 + ctype);
}
@Override
public int compareTo(Layout that) {
int r;
r = this.name.compareTo(that.name);
if (r != 0) return r;
r = this.layout.compareTo(that.layout);
if (r != 0) return r;
return this.ctype - that.ctype;
}
@Override
public String toString() {
String str = contextName(ctype)+"."+name+"["+layout+"]";
// If -ea, print out more informative strings!
assert((str = stringForDebug()) != null);
return str;
}
private String stringForDebug() {
return contextName(ctype)+"."+name+Arrays.asList(elems);
}
public
class Element {
String layout; // spelling in the little language
byte flags; // EF_SIGN, etc.
byte kind; // EK_UINT, etc.
byte len; // scalar length of element
byte refKind; // CONSTANT_String, etc.
int bandIndex; // which band does this element govern?
int value; // extra parameter
Element[] body; // extra data (for replications, unions, calls)
boolean flagTest(byte mask) { return (flags & mask) != 0; }
Element() {
bandIndex = bandCount++;
}
void removeBand() {
--bandCount;
assert(bandIndex == bandCount);
bandIndex = NO_BAND_INDEX;
}
public boolean hasBand() {
return bandIndex >= 0;
}
public String toString() {
String str = layout;
// If -ea, print out more informative strings!
assert((str = stringForDebug()) != null);
return str;
}
private String stringForDebug() {
Element[] lbody = this.body;
switch (kind) {
case EK_CALL:
lbody = null;
break;
case EK_CASE:
if (flagTest(EF_BACK))
lbody = null;
break;
}
return layout
+ (!hasBand()?"":"#"+bandIndex)
+ "<"+ (flags==0?"":""+flags)+kind+len
+ (refKind==0?"":""+refKind) + ">"
+ (value==0?"":"("+value+")")
+ (lbody==null?"": ""+Arrays.asList(lbody));
}
}
public boolean hasCallables() {
return (elems.length > 0 && elems[0].kind == EK_CBLE);
}
static private final Element[] noElems = {};
public Element[] getCallables() {
if (hasCallables()) {
Element[] nelems = Arrays.copyOf(elems, elems.length);
return nelems;
} else
return noElems; // no callables at all
}
public Element[] getEntryPoint() {
if (hasCallables())
return elems[0].body; // body of first callable
else {
Element[] nelems = Arrays.copyOf(elems, elems.length);
return nelems; // no callables; whole body
}
}
/** Return a sequence of tokens from the given attribute bytes.
* Sequence elements will be 1-1 correspondent with my layout tokens.
*/
public void parse(Holder holder,
byte[] bytes, int pos, int len, ValueStream out) {
int end = parseUsing(getEntryPoint(),
holder, bytes, pos, len, out);
if (end != pos + len)
throw new InternalError("layout parsed "+(end-pos)+" out of "+len+" bytes");
}
/** Given a sequence of tokens, return the attribute bytes.
* Sequence elements must be 1-1 correspondent with my layout tokens.
* The returned object is a cookie for Fixups.finishRefs, which
* must be used to harden any references into integer indexes.
*/
public Object unparse(ValueStream in, ByteArrayOutputStream out) {
Object[] fixups = { null };
unparseUsing(getEntryPoint(), fixups, in, out);
return fixups[0]; // return ref-bearing cookie, if any
}
public String layoutForClassVersion(Package.Version vers) {
if (vers.lessThan(JAVA6_MAX_CLASS_VERSION)) {
// Disallow layout syntax in the oldest protocol version.
return expandCaseDashNotation(layout);
}
return layout;
}
}
public static
class FormatException extends IOException {
private static final long serialVersionUID = -2542243830788066513L;
private int ctype;
private String name;
String layout;
public FormatException(String message,
int ctype, String name, String layout) {
super(ATTR_CONTEXT_NAME[ctype]+ " attribute \"" + name + "\"" +
(message == null? "" : (": " + message)));
this.ctype = ctype;
this.name = name;
this.layout = layout;
}
public FormatException(String message,
int ctype, String name) {
this(message, ctype, name, null);
}
}
void visitRefs(Holder holder, int mode, final Collection<Entry> refs) {
if (mode == VRM_CLASSIC) {
refs.add(getNameRef());
}
// else the name is owned by the layout, and is processed elsewhere
if (bytes.length == 0) return; // quick exit
if (!def.hasRefs) return; // quick exit
if (fixups != null) {
Fixups.visitRefs(fixups, refs);
return;
}
// References (to a local cpMap) are embedded in the bytes.
def.parse(holder, bytes, 0, bytes.length,
new ValueStream() {
@Override
public void putInt(int bandIndex, int value) {
}
@Override
public void putRef(int bandIndex, Entry ref) {
refs.add(ref);
}
@Override
public int encodeBCI(int bci) {
return bci;
}
});
}
public void parse(Holder holder, byte[] bytes, int pos, int len, ValueStream out) {
def.parse(holder, bytes, pos, len, out);
}
public Object unparse(ValueStream in, ByteArrayOutputStream out) {
return def.unparse(in, out);
}
@Override
public String toString() {
return def
+"{"+(bytes == null ? -1 : size())+"}"
+(fixups == null? "": fixups.toString());
}
/** Remove any informal "pretty printing" from the layout string.
* Removes blanks and control chars.
* Removes '#' comments (to end of line).
* Replaces '\c' by the decimal code of the character c.
* Replaces '0xNNN' by the decimal code of the hex number NNN.
*/
static public
String normalizeLayoutString(String layout) {
StringBuilder buf = new StringBuilder();
for (int i = 0, len = layout.length(); i < len; ) {
char ch = layout.charAt(i++);
if (ch <= ' ') {
// Skip whitespace and control chars
continue;
} else if (ch == '#') {
// Skip to end of line.
int end1 = layout.indexOf('\n', i);
int end2 = layout.indexOf('\r', i);
if (end1 < 0) end1 = len;
if (end2 < 0) end2 = len;
i = Math.min(end1, end2);
} else if (ch == '\\') {
// Map a character reference to its decimal code.
buf.append((int) layout.charAt(i++));
} else if (ch == '0' && layout.startsWith("0x", i-1)) {
// Map a hex numeral to its decimal code.
int start = i-1;
int end = start+2;
while (end < len) {
int dig = layout.charAt(end);
if ((dig >= '0' && dig <= '9') ||
(dig >= 'a' && dig <= 'f'))
++end;
else
break;
}
if (end > start) {
String num = layout.substring(start, end);
buf.append(Integer.decode(num));
i = end;
} else {
buf.append(ch);
}
} else {
buf.append(ch);
}
}
String result = buf.toString();
if (false && !result.equals(layout)) {
Utils.log.info("Normalizing layout string");
Utils.log.info(" From: "+layout);
Utils.log.info(" To: "+result);
}
return result;
}
/// Subroutines for parsing and unparsing:
/** Parse the attribute layout language.
<pre>
attribute_layout:
( layout_element )* | ( callable )+
layout_element:
( integral | replication | union | call | reference )
callable:
'[' body ']'
body:
( layout_element )+
integral:
( unsigned_int | signed_int | bc_index | bc_offset | flag )
unsigned_int:
uint_type
signed_int:
'S' uint_type
any_int:
( unsigned_int | signed_int )
bc_index:
( 'P' uint_type | 'PO' uint_type )
bc_offset:
'O' any_int
flag:
'F' uint_type
uint_type:
( 'B' | 'H' | 'I' | 'V' )
replication:
'N' uint_type '[' body ']'
union:
'T' any_int (union_case)* '(' ')' '[' (body)? ']'
union_case:
'(' union_case_tag (',' union_case_tag)* ')' '[' (body)? ']'
union_case_tag:
( numeral | numeral '-' numeral )
call:
'(' numeral ')'
reference:
reference_type ( 'N' )? uint_type
reference_type:
( constant_ref | schema_ref | utf8_ref | untyped_ref )
constant_ref:
( 'KI' | 'KJ' | 'KF' | 'KD' | 'KS' | 'KQ' | 'KM' | 'KT' | 'KL' )
schema_ref:
( 'RC' | 'RS' | 'RD' | 'RF' | 'RM' | 'RI' | 'RY' | 'RB' | 'RN' )
utf8_ref:
'RU'
untyped_ref:
'RQ'
numeral:
'(' ('-')? (digit)+ ')'
digit:
( '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' )
</pre>
*/
static //private
Layout.Element[] tokenizeLayout(Layout self, int curCble, String layout) {
List<Layout.Element> col = new ArrayList<>(layout.length());
tokenizeLayout(self, curCble, layout, col);
Layout.Element[] res = new Layout.Element[col.size()];
col.toArray(res);
return res;
}
static //private
void tokenizeLayout(Layout self, int curCble, String layout, List<Layout.Element> col) {
boolean prevBCI = false;
for (int len = layout.length(), i = 0; i < len; ) {
int start = i;
int body;
Layout.Element e = self.new Element();
byte kind;
//System.out.println("at "+i+": ..."+layout.substring(i));
// strip a prefix
switch (layout.charAt(i++)) {
/// layout_element: integral
case 'B': case 'H': case 'I': case 'V': // unsigned_int
kind = EK_INT;
--i; // reparse
i = tokenizeUInt(e, layout, i);
break;
case 'S': // signed_int
kind = EK_INT;
--i; // reparse
i = tokenizeSInt(e, layout, i);
break;
case 'P': // bc_index
kind = EK_BCI;
if (layout.charAt(i++) == 'O') {
// bc_index: 'PO' tokenizeUInt
e.flags |= EF_DELTA;
// must follow P or PO:
if (!prevBCI)
{ i = -i; continue; } // fail
i++; // move forward
}
--i; // reparse
i = tokenizeUInt(e, layout, i);
break;
case 'O': // bc_offset
kind = EK_BCO;
e.flags |= EF_DELTA;
// must follow P or PO:
if (!prevBCI)
{ i = -i; continue; } // fail
i = tokenizeSInt(e, layout, i);
break;
case 'F': // flag
kind = EK_FLAG;
i = tokenizeUInt(e, layout, i);
break;
case 'N': // replication: 'N' uint '[' elem ... ']'
kind = EK_REPL;
i = tokenizeUInt(e, layout, i);
if (layout.charAt(i++) != '[')
{ i = -i; continue; } // fail
i = skipBody(layout, body = i);
e.body = tokenizeLayout(self, curCble,
layout.substring(body, i++));
break;
case 'T': // union: 'T' any_int union_case* '(' ')' '[' body ']'
kind = EK_UN;
i = tokenizeSInt(e, layout, i);
List<Layout.Element> cases = new ArrayList<>();
for (;;) {
// Keep parsing cases until we hit the default case.
if (layout.charAt(i++) != '(')
{ i = -i; break; } // fail
int beg = i;
i = layout.indexOf(')', i);
String cstr = layout.substring(beg, i++);
int cstrlen = cstr.length();
if (layout.charAt(i++) != '[')
{ i = -i; break; } // fail
// Check for duplication.
if (layout.charAt(i) == ']')
body = i; // missing body, which is legal here
else
i = skipBody(layout, body = i);
Layout.Element[] cbody
= tokenizeLayout(self, curCble,
layout.substring(body, i++));
if (cstrlen == 0) {
Layout.Element ce = self.new Element();
ce.body = cbody;
ce.kind = EK_CASE;
ce.removeBand();
cases.add(ce);
break; // done with the whole union
} else {
// Parse a case string.
boolean firstCaseNum = true;
for (int cp = 0, endp;; cp = endp+1) {
// Look for multiple case tags:
endp = cstr.indexOf(',', cp);
if (endp < 0) endp = cstrlen;
String cstr1 = cstr.substring(cp, endp);
if (cstr1.length() == 0)
cstr1 = "empty"; // will fail parse
int value0, value1;
// Check for a case range (new in 1.6).
int dash = findCaseDash(cstr1, 0);
if (dash >= 0) {
value0 = parseIntBefore(cstr1, dash);
value1 = parseIntAfter(cstr1, dash);
if (value0 >= value1)
{ i = -i; break; } // fail
} else {
value0 = value1 = Integer.parseInt(cstr1);
}
// Add a case for each value in value0..value1
for (;; value0++) {
Layout.Element ce = self.new Element();
ce.body = cbody; // all cases share one body
ce.kind = EK_CASE;
ce.removeBand();
if (!firstCaseNum)
// "backward case" repeats a body
ce.flags |= EF_BACK;
firstCaseNum = false;
ce.value = value0;
cases.add(ce);
if (value0 == value1) break;
}
if (endp == cstrlen) {
break; // done with this case
}
}
}
}
e.body = new Layout.Element[cases.size()];
cases.toArray(e.body);
e.kind = kind;
for (int j = 0; j < e.body.length-1; j++) {
Layout.Element ce = e.body[j];
if (matchCase(e, ce.value) != ce) {
// Duplicate tag.
{ i = -i; break; } // fail
}
}
break;
case '(': // call: '(' '-'? digit+ ')'
kind = EK_CALL;
e.removeBand();
i = layout.indexOf(')', i);
String cstr = layout.substring(start+1, i++);
int offset = Integer.parseInt(cstr);
int target = curCble + offset;
if (!(offset+"").equals(cstr) ||
self.elems == null ||
target < 0 ||
target >= self.elems.length)
{ i = -i; continue; } // fail
Layout.Element ce = self.elems[target];
assert(ce.kind == EK_CBLE);
e.value = target;
e.body = new Layout.Element[]{ ce };
// Is it a (recursive) backward call?
if (offset <= 0) {
// Yes. Mark both caller and callee backward.
e.flags |= EF_BACK;
ce.flags |= EF_BACK;
}
break;
case 'K': // reference_type: constant_ref
kind = EK_REF;
switch (layout.charAt(i++)) {
case 'I': e.refKind = CONSTANT_Integer; break;
case 'J': e.refKind = CONSTANT_Long; break;
case 'F': e.refKind = CONSTANT_Float; break;
case 'D': e.refKind = CONSTANT_Double; break;
case 'S': e.refKind = CONSTANT_String; break;
case 'Q': e.refKind = CONSTANT_FieldSpecific; break;
// new in 1.7:
case 'M': e.refKind = CONSTANT_MethodHandle; break;
case 'T': e.refKind = CONSTANT_MethodType; break;
case 'L': e.refKind = CONSTANT_LoadableValue; break;
default: { i = -i; continue; } // fail
}
break;
case 'R': // schema_ref
kind = EK_REF;
switch (layout.charAt(i++)) {
case 'C': e.refKind = CONSTANT_Class; break;
case 'S': e.refKind = CONSTANT_Signature; break;
case 'D': e.refKind = CONSTANT_NameandType; break;
case 'F': e.refKind = CONSTANT_Fieldref; break;
case 'M': e.refKind = CONSTANT_Methodref; break;
case 'I': e.refKind = CONSTANT_InterfaceMethodref; break;
case 'U': e.refKind = CONSTANT_Utf8; break; //utf8_ref
case 'Q': e.refKind = CONSTANT_All; break; //untyped_ref
// new in 1.7:
case 'Y': e.refKind = CONSTANT_InvokeDynamic; break;
case 'B': e.refKind = CONSTANT_BootstrapMethod; break;
case 'N': e.refKind = CONSTANT_AnyMember; break;
default: { i = -i; continue; } // fail
}
break;
default: { i = -i; continue; } // fail
}
// further parsing of refs
if (kind == EK_REF) {
// reference: reference_type -><- ( 'N' )? tokenizeUInt
if (layout.charAt(i++) == 'N') {
e.flags |= EF_NULL;
i++; // move forward
}
--i; // reparse
i = tokenizeUInt(e, layout, i);
self.hasRefs = true;
}
prevBCI = (kind == EK_BCI);
// store the new element
e.kind = kind;
e.layout = layout.substring(start, i);
col.add(e);
}
}
static //private
String[] splitBodies(String layout) {
List<String> bodies = new ArrayList<>();
// Parse several independent layout bodies: "[foo][bar]...[baz]"
for (int i = 0; i < layout.length(); i++) {
if (layout.charAt(i++) != '[')
layout.charAt(-i); // throw error
int body;
i = skipBody(layout, body = i);
bodies.add(layout.substring(body, i));
}
String[] res = new String[bodies.size()];
bodies.toArray(res);
return res;
}
static private
int skipBody(String layout, int i) {
assert(layout.charAt(i-1) == '[');
if (layout.charAt(i) == ']')
// No empty bodies, please.
return -i;
// skip balanced [...[...]...]
for (int depth = 1; depth > 0; ) {
switch (layout.charAt(i++)) {
case '[': depth++; break;
case ']': depth--; break;
}
}
--i; // get before bracket
assert(layout.charAt(i) == ']');
return i; // return closing bracket
}
static private
int tokenizeUInt(Layout.Element e, String layout, int i) {
switch (layout.charAt(i++)) {
case 'V': e.len = 0; break;
case 'B': e.len = 1; break;
case 'H': e.len = 2; break;
case 'I': e.len = 4; break;
default: return -i;
}
return i;
}
static private
int tokenizeSInt(Layout.Element e, String layout, int i) {
if (layout.charAt(i) == 'S') {
e.flags |= EF_SIGN;
++i;
}
return tokenizeUInt(e, layout, i);
}
static private
boolean isDigit(char c) {
return c >= '0' && c <= '9';
}
/** Find an occurrence of hyphen '-' between two numerals. */
static //private
int findCaseDash(String layout, int fromIndex) {
if (fromIndex <= 0) fromIndex = 1; // minimum dash pos
int lastDash = layout.length() - 2; // maximum dash pos
for (;;) {
int dash = layout.indexOf('-', fromIndex);
if (dash < 0 || dash > lastDash) return -1;
if (isDigit(layout.charAt(dash-1))) {
char afterDash = layout.charAt(dash+1);
if (afterDash == '-' && dash+2 < layout.length())
afterDash = layout.charAt(dash+2);
if (isDigit(afterDash)) {
// matched /[0-9]--?[0-9]/; return position of dash
return dash;
}
}
fromIndex = dash+1;
}
}
static
int parseIntBefore(String layout, int dash) {
int end = dash;
int beg = end;
while (beg > 0 && isDigit(layout.charAt(beg-1))) {
--beg;
}
if (beg == end) return Integer.parseInt("empty");
// skip backward over a sign
if (beg >= 1 && layout.charAt(beg-1) == '-') --beg;
assert(beg == 0 || !isDigit(layout.charAt(beg-1)));
return Integer.parseInt(layout.substring(beg, end));
}
static
int parseIntAfter(String layout, int dash) {
int beg = dash+1;
int end = beg;
int limit = layout.length();
if (end < limit && layout.charAt(end) == '-') ++end;
while (end < limit && isDigit(layout.charAt(end))) {
++end;
}
if (beg == end) return Integer.parseInt("empty");
return Integer.parseInt(layout.substring(beg, end));
}
/** For compatibility with 1.5 pack, expand 1-5 into 1,2,3,4,5. */
static
String expandCaseDashNotation(String layout) {
int dash = findCaseDash(layout, 0);
if (dash < 0) return layout; // no dashes (the common case)
StringBuilder result = new StringBuilder(layout.length() * 3);
int sofar = 0; // how far have we processed the layout?
for (;;) {
// for each dash, collect everything up to the dash
result.append(layout.substring(sofar, dash));
sofar = dash+1; // skip the dash
// then collect intermediate values
int value0 = parseIntBefore(layout, dash);
int value1 = parseIntAfter(layout, dash);
assert(value0 < value1);
result.append(","); // close off value0 numeral
for (int i = value0+1; i < value1; i++) {
result.append(i);
result.append(","); // close off i numeral
}
dash = findCaseDash(layout, sofar);
if (dash < 0) break;
}
result.append(layout.substring(sofar)); // collect the rest
return result.toString();
}
static {
assert(expandCaseDashNotation("1-5").equals("1,2,3,4,5"));
assert(expandCaseDashNotation("-2--1").equals("-2,-1"));
assert(expandCaseDashNotation("-2-1").equals("-2,-1,0,1"));
assert(expandCaseDashNotation("-1-0").equals("-1,0"));
}
// Parse attribute bytes, putting values into bands. Returns new pos.
// Used when reading a class file (local refs resolved with local cpMap).
// Also used for ad hoc scanning.
static
int parseUsing(Layout.Element[] elems, Holder holder,
byte[] bytes, int pos, int len, ValueStream out) {
int prevBCI = 0;
int prevRBCI = 0;
int end = pos + len;
int[] buf = { 0 }; // for calls to parseInt, holds 2nd result
for (int i = 0; i < elems.length; i++) {
Layout.Element e = elems[i];
int bandIndex = e.bandIndex;
int value;
int BCI, RBCI;
switch (e.kind) {
case EK_INT:
pos = parseInt(e, bytes, pos, buf);
value = buf[0];
out.putInt(bandIndex, value);
break;
case EK_BCI: // PH, POH
pos = parseInt(e, bytes, pos, buf);
BCI = buf[0];
RBCI = out.encodeBCI(BCI);
if (!e.flagTest(EF_DELTA)) {
// PH: transmit R(bci), store bci
value = RBCI;
} else {
// POH: transmit D(R(bci)), store bci
value = RBCI - prevRBCI;
}
prevBCI = BCI;
prevRBCI = RBCI;
out.putInt(bandIndex, value);
break;
case EK_BCO: // OH
assert(e.flagTest(EF_DELTA));
// OH: transmit D(R(bci)), store D(bci)
pos = parseInt(e, bytes, pos, buf);
BCI = prevBCI + buf[0];
RBCI = out.encodeBCI(BCI);
value = RBCI - prevRBCI;
prevBCI = BCI;
prevRBCI = RBCI;
out.putInt(bandIndex, value);
break;
case EK_FLAG:
pos = parseInt(e, bytes, pos, buf);
value = buf[0];
out.putInt(bandIndex, value);
break;
case EK_REPL:
pos = parseInt(e, bytes, pos, buf);
value = buf[0];
out.putInt(bandIndex, value);
for (int j = 0; j < value; j++) {
pos = parseUsing(e.body, holder, bytes, pos, end-pos, out);
}
break; // already transmitted the scalar value
case EK_UN:
pos = parseInt(e, bytes, pos, buf);
value = buf[0];
out.putInt(bandIndex, value);
Layout.Element ce = matchCase(e, value);
pos = parseUsing(ce.body, holder, bytes, pos, end-pos, out);
break; // already transmitted the scalar value
case EK_CALL:
// Adjust band offset if it is a backward call.
assert(e.body.length == 1);
assert(e.body[0].kind == EK_CBLE);
if (e.flagTest(EF_BACK))
out.noteBackCall(e.value);
pos = parseUsing(e.body[0].body, holder, bytes, pos, end-pos, out);
break; // no additional scalar value to transmit
case EK_REF:
pos = parseInt(e, bytes, pos, buf);
int localRef = buf[0];
Entry globalRef;
if (localRef == 0) {
globalRef = null; // N.B. global null reference is -1
} else {
Entry[] cpMap = holder.getCPMap();
globalRef = (localRef >= 0 && localRef < cpMap.length
? cpMap[localRef]
: null);
byte tag = e.refKind;
if (globalRef != null && tag == CONSTANT_Signature
&& globalRef.getTag() == CONSTANT_Utf8) {
// Cf. ClassReader.readSignatureRef.
String typeName = globalRef.stringValue();
globalRef = ConstantPool.getSignatureEntry(typeName);
}
String got = (globalRef == null
? "invalid CP index"
: "type=" + ConstantPool.tagName(globalRef.tag));
if (globalRef == null || !globalRef.tagMatches(tag)) {
throw new IllegalArgumentException(
"Bad constant, expected type=" +
ConstantPool.tagName(tag) + " got " + got);
}
}
out.putRef(bandIndex, globalRef);
break;
default: assert(false);
}
}
return pos;
}
static
Layout.Element matchCase(Layout.Element e, int value) {
assert(e.kind == EK_UN);
int lastj = e.body.length-1;
for (int j = 0; j < lastj; j++) {
Layout.Element ce = e.body[j];
assert(ce.kind == EK_CASE);
if (value == ce.value)
return ce;
}
return e.body[lastj];
}
static private
int parseInt(Layout.Element e, byte[] bytes, int pos, int[] buf) {
int value = 0;
int loBits = e.len * 8;
// Read in big-endian order:
for (int bitPos = loBits; (bitPos -= 8) >= 0; ) {
value += (bytes[pos++] & 0xFF) << bitPos;
}
if (loBits < 32 && e.flagTest(EF_SIGN)) {
// sign-extend subword value
int hiBits = 32 - loBits;
value = (value << hiBits) >> hiBits;
}
buf[0] = value;
return pos;
}
// Format attribute bytes, drawing values from bands.
// Used when emptying attribute bands into a package model.
// (At that point CP refs. are not yet assigned indexes.)
static
void unparseUsing(Layout.Element[] elems, Object[] fixups,
ValueStream in, ByteArrayOutputStream out) {
int prevBCI = 0;
int prevRBCI = 0;
for (int i = 0; i < elems.length; i++) {
Layout.Element e = elems[i];
int bandIndex = e.bandIndex;
int value;
int BCI, RBCI; // "RBCI" is R(BCI), BCI's coded representation
switch (e.kind) {
case EK_INT:
value = in.getInt(bandIndex);
unparseInt(e, value, out);
break;
case EK_BCI: // PH, POH
value = in.getInt(bandIndex);
if (!e.flagTest(EF_DELTA)) {
// PH: transmit R(bci), store bci
RBCI = value;
} else {
// POH: transmit D(R(bci)), store bci
RBCI = prevRBCI + value;
}
assert(prevBCI == in.decodeBCI(prevRBCI));
BCI = in.decodeBCI(RBCI);
unparseInt(e, BCI, out);
prevBCI = BCI;
prevRBCI = RBCI;
break;
case EK_BCO: // OH
value = in.getInt(bandIndex);
assert(e.flagTest(EF_DELTA));
// OH: transmit D(R(bci)), store D(bci)
assert(prevBCI == in.decodeBCI(prevRBCI));
RBCI = prevRBCI + value;
BCI = in.decodeBCI(RBCI);
unparseInt(e, BCI - prevBCI, out);
prevBCI = BCI;
prevRBCI = RBCI;
break;
case EK_FLAG:
value = in.getInt(bandIndex);
unparseInt(e, value, out);
break;
case EK_REPL:
value = in.getInt(bandIndex);
unparseInt(e, value, out);
for (int j = 0; j < value; j++) {
unparseUsing(e.body, fixups, in, out);
}
break;
case EK_UN:
value = in.getInt(bandIndex);
unparseInt(e, value, out);
Layout.Element ce = matchCase(e, value);
unparseUsing(ce.body, fixups, in, out);
break;
case EK_CALL:
assert(e.body.length == 1);
assert(e.body[0].kind == EK_CBLE);
unparseUsing(e.body[0].body, fixups, in, out);
break;
case EK_REF:
Entry globalRef = in.getRef(bandIndex);
int localRef;
if (globalRef != null) {
// It's a one-element array, really an lvalue.
fixups[0] = Fixups.addRefWithLoc(fixups[0], out.size(), globalRef);
localRef = 0; // placeholder for fixups
} else {
localRef = 0; // fixed null value
}
unparseInt(e, localRef, out);
break;
default: assert(false); continue;
}
}
}
static private
void unparseInt(Layout.Element e, int value, ByteArrayOutputStream out) {
int loBits = e.len * 8;
if (loBits == 0) {
// It is not stored at all ('V' layout).
return;
}
if (loBits < 32) {
int hiBits = 32 - loBits;
int codedValue;
if (e.flagTest(EF_SIGN))
codedValue = (value << hiBits) >> hiBits;
else
codedValue = (value << hiBits) >>> hiBits;
if (codedValue != value)
throw new InternalError("cannot code in "+e.len+" bytes: "+value);
}
// Write in big-endian order:
for (int bitPos = loBits; (bitPos -= 8) >= 0; ) {
out.write((byte)(value >>> bitPos));
}
}
/*
/// Testing.
public static void main(String av[]) {
int maxVal = 12;
int iters = 0;
boolean verbose;
int ap = 0;
while (ap < av.length) {
if (!av[ap].startsWith("-")) break;
if (av[ap].startsWith("-m"))
maxVal = Integer.parseInt(av[ap].substring(2));
else if (av[ap].startsWith("-i"))
iters = Integer.parseInt(av[ap].substring(2));
else
throw new RuntimeException("Bad option: "+av[ap]);
ap++;
}
verbose = (iters == 0);
if (iters <= 0) iters = 1;
if (ap == av.length) {
av = new String[] {
"HH", // ClassFile.version
"RUH", // SourceFile
"RCHRDNH", // EnclosingMethod
"KQH", // ConstantValue
"NH[RCH]", // Exceptions
"NH[PHH]", // LineNumberTable
"NH[PHOHRUHRSHH]", // LocalVariableTable
"NH[PHPOHIIH]", // CharacterRangeTable
"NH[PHHII]", // CoverageTable
"NH[RCHRCNHRUNHFH]", // InnerClasses
"NH[RMHNH[KLH]]", // BootstrapMethods
"HHNI[B]NH[PHPOHPOHRCNH]NH[RUHNI[B]]", // Code
/**代码未完, 请加载全部代码(NowJava.com).**/