Fixing the mess-ups that was apktool v1.4.8 :(

This commit is contained in:
Connor Tumbleson 2012-07-08 17:46:02 -05:00
parent b5a476bb1b
commit 97451619b8
6 changed files with 169 additions and 187 deletions

View File

@ -149,10 +149,9 @@ final public class AndrolibResources {
inApk = apkFile.getDirectory(); inApk = apkFile.getDirectory();
out = new FileDirectory(outDir); out = new FileDirectory(outDir);
LOGGER.info("Decoding AndroidManifest.xml with resources..."); fileDecoder.decode(
inApk, "AndroidManifest.xml", out, "AndroidManifest.xml",
fileDecoder.decodeManifest( "xml");
inApk, "AndroidManifest.xml", out, "AndroidManifest.xml");
if (inApk.containsDir("res")) { if (inApk.containsDir("res")) {
in = inApk.getDir("res"); in = inApk.getDir("res");

View File

@ -51,15 +51,7 @@ public class ResPluralsValue extends ResBagValue implements ResValuesXmlSerializ
} }
serializer.startTag(null, "item"); serializer.startTag(null, "item");
serializer.attribute(null, "quantity", QUANTITY_MAP[i]); serializer.attribute(null, "quantity", QUANTITY_MAP[i]);
serializer.text(item.encodeAsResXmlValue());
String rawValue = item.encodeAsResXmlValueExt();
//AAPT don`t parse formatted for item tag(only for string and string-array tag),
// so adding "formatted='fasle'" is useless.
/*if (ResXmlEncoders.hasMultipleNonPositionalSubstitutions(rawValue)) {
serializer.attribute(null, "formatted", "false");
}*/
serializer.text(rawValue);
serializer.endTag(null, "item"); serializer.endTag(null, "item");
} }
serializer.endTag(null, "plurals"); serializer.endTag(null, "plurals");

View File

@ -52,7 +52,7 @@ public abstract class ResScalarValue extends ResValue
if (mRawValue != null) { if (mRawValue != null) {
return mRawValue; return mRawValue;
} }
return encodeAsResXml(); return encodeAsResXmlValueExt();
} }
public String encodeAsResXmlValueExt() throws AndrolibException { public String encodeAsResXmlValueExt() throws AndrolibException {
@ -60,7 +60,7 @@ public abstract class ResScalarValue extends ResValue
if (rawValue != null) { if (rawValue != null) {
if (ResXmlEncoders.hasMultipleNonPositionalSubstitutions(rawValue)) { if (ResXmlEncoders.hasMultipleNonPositionalSubstitutions(rawValue)) {
int count = 1; int count = 1;
StringBuffer result = new StringBuffer(); StringBuilder result = new StringBuilder();
String tmp1[] = rawValue.split("%%", -1); String tmp1[] = rawValue.split("%%", -1);
int tmp1_sz = tmp1.length; int tmp1_sz = tmp1.length;
for(int i=0;i<tmp1_sz;i++) { for(int i=0;i<tmp1_sz;i++) {

View File

@ -247,13 +247,13 @@ public class ARSCDecoder {
byte keyboard = mIn.readByte(); byte keyboard = mIn.readByte();
byte navigation = mIn.readByte(); byte navigation = mIn.readByte();
byte inputFlags = mIn.readByte(); byte inputFlags = mIn.readByte();
/*inputPad0*/ mIn.skipBytes(1); mIn.skipBytes(1);
short screenWidth = mIn.readShort(); short screenWidth = mIn.readShort();
short screenHeight = mIn.readShort(); short screenHeight = mIn.readShort();
short sdkVersion = mIn.readShort(); short sdkVersion = mIn.readShort();
/*minorVersion, now must always be 0*/ mIn.skipBytes(2); mIn.skipBytes(2);
byte screenLayout = 0; byte screenLayout = 0;
byte uiMode = 0; byte uiMode = 0;

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package brut.androlib.res.decoder; package brut.androlib.res.decoder;
import android.content.res.XmlResourceParser; import android.content.res.XmlResourceParser;
@ -40,14 +39,13 @@ import java.util.logging.Logger;
* *
* Binary xml files parser. * Binary xml files parser.
* *
* Parser has only two states: * Parser has only two states: (1) Operational state, which parser obtains after
* (1) Operational state, which parser obtains after first successful call * first successful call to next() and retains until open(), close(), or failed
* to next() and retains until open(), close(), or failed call to next(). * call to next(). (2) Closed state, which parser obtains after open(), close(),
* (2) Closed state, which parser obtains after open(), close(), or failed * or failed call to next(). In this state methods return invalid values or
* call to next(). In this state methods return invalid values or throw exceptions. * throw exceptions.
* *
* TODO: * TODO: * check all methods in closed state
* * check all methods in closed state
* *
*/ */
public class AXmlResourceParser implements XmlResourceParser { public class AXmlResourceParser implements XmlResourceParser {
@ -319,20 +317,19 @@ public class AXmlResourceParser implements XmlResourceParser {
try { try {
return mAttrDecoder.decode(valueType, valueData, return mAttrDecoder.decode(valueType, valueData,
valueRaw == -1 ? null : ResXmlEncoders.escapeXmlChars( valueRaw == -1 ? null : ResXmlEncoders.escapeXmlChars(
m_strings.getString(valueRaw)), m_strings.getString(valueRaw)),
getAttributeNameResource(index)); getAttributeNameResource(index));
} catch (AndrolibException ex) { } catch (AndrolibException ex) {
setFirstError(ex); setFirstError(ex);
LOGGER.log(Level.WARNING, String.format( LOGGER.log(Level.WARNING, String.format(
"Could not decode attr value, using undecoded value " + "Could not decode attr value, using undecoded value "
"instead: ns=%s, name=%s, value=0x%08x", + "instead: ns=%s, name=%s, value=0x%08x",
getAttributePrefix(index), getAttributeName(index), getAttributePrefix(index), getAttributeName(index),
valueData valueData), ex);
), ex);
} }
} else { } else {
if (valueType==TypedValue.TYPE_STRING) { if (valueType == TypedValue.TYPE_STRING) {
return m_strings.getString(valueRaw); return ResXmlEncoders.escapeXmlChars(m_strings.getString(valueRaw));
} }
} }
@ -493,20 +490,16 @@ public class AXmlResourceParser implements XmlResourceParser {
///////////////////////////////////////////// implementation ///////////////////////////////////////////// implementation
/** /**
* Namespace stack, holds prefix+uri pairs, as well as * Namespace stack, holds prefix+uri pairs, as well as depth information.
* depth information. * All information is stored in one int[] array. Array consists of depth
* All information is stored in one int[] array. * frames: Data=DepthFrame*; DepthFrame=Count+[Prefix+Uri]*+Count;
* Array consists of depth frames: * Count='count of Prefix+Uri pairs'; Yes, count is stored twice, to enable
* Data=DepthFrame*; * bottom-up traversal. increaseDepth adds depth frame, decreaseDepth
* DepthFrame=Count+[Prefix+Uri]*+Count; * removes it. push/pop operations operate only in current depth frame.
* Count='count of Prefix+Uri pairs'; * decreaseDepth removes any remaining (not pop'ed) namespace pairs. findXXX
* Yes, count is stored twice, to enable bottom-up traversal. * methods search all depth frames starting from the last namespace pair of
* increaseDepth adds depth frame, decreaseDepth removes it. * current depth frame. All functions that operate with int, use -1 as
* push/pop operations operate only in current depth frame. * 'invalid value'.
* decreaseDepth removes any remaining (not pop'ed) namespace pairs.
* findXXX methods search all depth frames starting
* from the last namespace pair of current depth frame.
* All functions that operate with int, use -1 as 'invalid value'.
* *
* !! functions expect 'prefix'+'uri' pairs, not 'uri'+'prefix' !! * !! functions expect 'prefix'+'uri' pairs, not 'uri'+'prefix' !!
* *
@ -764,7 +757,7 @@ public class AXmlResourceParser implements XmlResourceParser {
if (m_event != START_TAG) { if (m_event != START_TAG) {
throw new IndexOutOfBoundsException("Current event is not START_TAG."); throw new IndexOutOfBoundsException("Current event is not START_TAG.");
} }
int offset = index * ATTRIBUTE_LENGHT; int offset = index * ATTRIBUTE_LENGHT;
if (offset >= m_attributes.length) { if (offset >= m_attributes.length) {
throw new IndexOutOfBoundsException("Invalid attribute index (" + index + ")."); throw new IndexOutOfBoundsException("Invalid attribute index (" + index + ").");
} }
@ -806,7 +799,9 @@ public class AXmlResourceParser implements XmlResourceParser {
// Delayed initialization. // Delayed initialization.
if (m_strings == null) { if (m_strings == null) {
m_reader.skipCheckInt(CHUNK_AXML_FILE); m_reader.skipCheckInt(CHUNK_AXML_FILE);
/*chunkSize*/ m_reader.skipInt(); /*
* chunkSize
*/ m_reader.skipInt();
m_strings = StringBlock.read(m_reader); m_strings = StringBlock.read(m_reader);
m_namespaces.increaseDepth(); m_namespaces.increaseDepth();
m_operational = true; m_operational = true;
@ -856,9 +851,13 @@ public class AXmlResourceParser implements XmlResourceParser {
} }
// Common header. // Common header.
/*chunkSize*/ m_reader.skipInt(); /*
* chunkSize
*/ m_reader.skipInt();
int lineNumber = m_reader.readInt(); int lineNumber = m_reader.readInt();
/*0xFFFFFFFF*/ m_reader.skipInt(); /*
* 0xFFFFFFFF
*/ m_reader.skipInt();
// Fake START_DOCUMENT event. // Fake START_DOCUMENT event.
if (chunkType == CHUNK_XML_START_TAG && event == -1) { if (chunkType == CHUNK_XML_START_TAG && event == -1) {
@ -874,8 +873,12 @@ public class AXmlResourceParser implements XmlResourceParser {
int uri = m_reader.readInt(); int uri = m_reader.readInt();
m_namespaces.push(prefix, uri); m_namespaces.push(prefix, uri);
} else { } else {
/*prefix*/ m_reader.skipInt(); /*
/*uri*/ m_reader.skipInt(); * prefix
*/ m_reader.skipInt();
/*
* uri
*/ m_reader.skipInt();
m_namespaces.pop(); m_namespaces.pop();
} }
continue; continue;
@ -888,7 +891,9 @@ public class AXmlResourceParser implements XmlResourceParser {
if (chunkType == CHUNK_XML_START_TAG) { if (chunkType == CHUNK_XML_START_TAG) {
m_namespaceUri = m_reader.readInt(); m_namespaceUri = m_reader.readInt();
m_name = m_reader.readInt(); m_name = m_reader.readInt();
/*flags?*/ m_reader.skipInt(); /*
* flags?
*/ m_reader.skipInt();
int attributeCount = m_reader.readInt(); int attributeCount = m_reader.readInt();
m_idAttribute = (attributeCount >>> 16) - 1; m_idAttribute = (attributeCount >>> 16) - 1;
attributeCount &= 0xFFFF; attributeCount &= 0xFFFF;
@ -903,7 +908,7 @@ public class AXmlResourceParser implements XmlResourceParser {
m_namespaces.increaseDepth(); m_namespaces.increaseDepth();
m_event = START_TAG; m_event = START_TAG;
m_strings.touch(m_name, m_name); m_strings.touch(m_name, m_name);
for(int i = 0; i<attributeCount; i++) { for (int i = 0; i < attributeCount; i++) {
m_strings.touch(m_attributes[ATTRIBUTE_IX_NAME], m_name); m_strings.touch(m_attributes[ATTRIBUTE_IX_NAME], m_name);
m_strings.touch(m_attributes[ATTRIBUTE_IX_VALUE_STRING], m_name); m_strings.touch(m_attributes[ATTRIBUTE_IX_VALUE_STRING], m_name);
} }
@ -922,119 +927,122 @@ public class AXmlResourceParser implements XmlResourceParser {
if (chunkType == CHUNK_XML_TEXT) { if (chunkType == CHUNK_XML_TEXT) {
m_name = m_reader.readInt(); m_name = m_reader.readInt();
/*?*/ m_reader.skipInt(); /*
/*?*/ m_reader.skipInt(); * ?
*/ m_reader.skipInt();
/*
* ?
*/ m_reader.skipInt();
m_event = TEXT; m_event = TEXT;
break; break;
} }
} }
} }
private static String formatArray(int[] array, int min, int max) { private static String formatArray(int[] array, int min, int max) {
if(max > array.length) if (max > array.length) {
max = array.length; max = array.length;
if(min < 0) }
min = 0; if (min < 0) {
StringBuffer sb = new StringBuffer("["); min = 0;
int i = min; }
while(true) { StringBuffer sb = new StringBuffer("[");
sb.append(array[i]); int i = min;
i++; while (true) {
if(i < max) { sb.append(array[i]);
sb.append(", "); i++;
} else { if (i < max) {
sb.append("]"); sb.append(", ");
break; } else {
} sb.append("]");
} break;
return sb.toString();
}
private boolean compareAttr(int[] attr1, int[] attr2) {
//TODO: sort Attrs
/*
* ATTRIBUTE_IX_VALUE_TYPE == TYPE_STRING : ATTRIBUTE_IX_VALUE_STRING
* : ATTRIBUTE_IX_NAMESPACE_URI
* ATTRIBUTE_IX_NAMESPACE_URI : ATTRIBUTE_IX_NAME
* id
*
*/
if(attr1[ATTRIBUTE_IX_VALUE_TYPE] == TypedValue.TYPE_STRING &&
attr1[ATTRIBUTE_IX_VALUE_TYPE] == attr2[ATTRIBUTE_IX_VALUE_TYPE] &&
//(m_strings.touch(attr1[ATTRIBUTE_IX_VALUE_STRING], m_name) ||
// m_strings.touch(attr2[ATTRIBUTE_IX_VALUE_STRING], m_name)) &&
//m_strings.touch(attr1[ATTRIBUTE_IX_VALUE_STRING], m_name) &&
attr1[ATTRIBUTE_IX_VALUE_STRING] != attr2[ATTRIBUTE_IX_VALUE_STRING]) {
return (attr1[ATTRIBUTE_IX_VALUE_STRING] < attr2[ATTRIBUTE_IX_VALUE_STRING]);
} else if ((attr1[ATTRIBUTE_IX_NAMESPACE_URI] == attr2[ATTRIBUTE_IX_NAMESPACE_URI]) && (attr1[ATTRIBUTE_IX_NAMESPACE_URI] != -1) &&
//(m_strings.touch(attr1[ATTRIBUTE_IX_NAME], m_name) ||
// m_strings.touch(attr2[ATTRIBUTE_IX_NAME], m_name)) &&
//m_strings.touch(attr1[ATTRIBUTE_IX_NAME], m_name) &&
(attr1[ATTRIBUTE_IX_NAME] != attr2[ATTRIBUTE_IX_NAME])) {
return (attr1[ATTRIBUTE_IX_NAME] < attr2[ATTRIBUTE_IX_NAME]);
//} else if (attr1[ATTRIBUTE_IX_NAMESPACE_URI] < attr2[ATTRIBUTE_IX_NAMESPACE_URI]) {
// return true;
} else {
return false;
}
}
private void sortAttrs() {
int attributeCount = m_attributes.length/ATTRIBUTE_LENGHT;
int tmp1[][] = new int[attributeCount][];
int tmp2[] = null;
for (int i = 0; i < attributeCount;i++) {
tmp1[i] = new int[ATTRIBUTE_LENGHT+1];
for(int j = 0; j < ATTRIBUTE_LENGHT; j++) {
tmp1[i][j] = m_attributes[i*ATTRIBUTE_LENGHT+j];
}
tmp1[i][ATTRIBUTE_LENGHT] = i;
if(DBG) {
try {
if (dbgOut == null) {
dbgOut = new BufferedWriter(new FileWriter("C:\\res.log",false));
}
dbgOut.write("Namespace: " + getAttributeNamespace (i) +
", Name: " + getAttributeName (i)+
", Value: " + getAttributeValue (i) + ", Array: " +
formatArray(tmp1[i], 0, ATTRIBUTE_LENGHT) + "\n");
dbgOut.flush();
} catch (IOException e) {
e.printStackTrace();
}
} }
} }
for (int j = 1; j < attributeCount;j++) { return sb.toString();
for (int i = 1; i < attributeCount;i++) { }
if(compareAttr(tmp1[i], tmp1[i-1])) {
tmp2 = tmp1[i-1]; private boolean compareAttr(int[] attr1, int[] attr2) {
tmp1[i-1] = tmp1[i]; //TODO: sort Attrs
/*
* ATTRIBUTE_IX_VALUE_TYPE == TYPE_STRING : ATTRIBUTE_IX_VALUE_STRING :
* ATTRIBUTE_IX_NAMESPACE_URI ATTRIBUTE_IX_NAMESPACE_URI :
* ATTRIBUTE_IX_NAME id
*
*/
if (attr1[ATTRIBUTE_IX_VALUE_TYPE] == TypedValue.TYPE_STRING
&& attr1[ATTRIBUTE_IX_VALUE_TYPE] == attr2[ATTRIBUTE_IX_VALUE_TYPE]
&& //(m_strings.touch(attr1[ATTRIBUTE_IX_VALUE_STRING], m_name) ||
// m_strings.touch(attr2[ATTRIBUTE_IX_VALUE_STRING], m_name)) &&
//m_strings.touch(attr1[ATTRIBUTE_IX_VALUE_STRING], m_name) &&
attr1[ATTRIBUTE_IX_VALUE_STRING] != attr2[ATTRIBUTE_IX_VALUE_STRING]) {
return (attr1[ATTRIBUTE_IX_VALUE_STRING] < attr2[ATTRIBUTE_IX_VALUE_STRING]);
} else if ((attr1[ATTRIBUTE_IX_NAMESPACE_URI] == attr2[ATTRIBUTE_IX_NAMESPACE_URI]) && (attr1[ATTRIBUTE_IX_NAMESPACE_URI] != -1)
&& //(m_strings.touch(attr1[ATTRIBUTE_IX_NAME], m_name) ||
// m_strings.touch(attr2[ATTRIBUTE_IX_NAME], m_name)) &&
//m_strings.touch(attr1[ATTRIBUTE_IX_NAME], m_name) &&
(attr1[ATTRIBUTE_IX_NAME] != attr2[ATTRIBUTE_IX_NAME])) {
return (attr1[ATTRIBUTE_IX_NAME] < attr2[ATTRIBUTE_IX_NAME]);
//} else if (attr1[ATTRIBUTE_IX_NAMESPACE_URI] < attr2[ATTRIBUTE_IX_NAMESPACE_URI]) {
// return true;
} else {
return false;
}
}
private void sortAttrs() {
int attributeCount = m_attributes.length / ATTRIBUTE_LENGHT;
int tmp1[][] = new int[attributeCount][];
int tmp2[] = null;
for (int i = 0; i < attributeCount; i++) {
tmp1[i] = new int[ATTRIBUTE_LENGHT + 1];
for (int j = 0; j < ATTRIBUTE_LENGHT; j++) {
tmp1[i][j] = m_attributes[i * ATTRIBUTE_LENGHT + j];
}
tmp1[i][ATTRIBUTE_LENGHT] = i;
if (DBG) {
try {
if (dbgOut == null) {
dbgOut = new BufferedWriter(new FileWriter("C:\\res.log", false));
}
dbgOut.write("Namespace: " + getAttributeNamespace(i)
+ ", Name: " + getAttributeName(i)
+ ", Value: " + getAttributeValue(i) + ", Array: "
+ formatArray(tmp1[i], 0, ATTRIBUTE_LENGHT) + "\n");
dbgOut.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
for (int j = 1; j < attributeCount; j++) {
for (int i = 1; i < attributeCount; i++) {
if (compareAttr(tmp1[i], tmp1[i - 1])) {
tmp2 = tmp1[i - 1];
tmp1[i - 1] = tmp1[i];
tmp1[i] = tmp2; tmp1[i] = tmp2;
} }
} }
} }
for (int i = 0; i < attributeCount;i++) { for (int i = 0; i < attributeCount; i++) {
for(int j = 0; j < ATTRIBUTE_LENGHT; j++) { for (int j = 0; j < ATTRIBUTE_LENGHT; j++) {
m_attributes[i*ATTRIBUTE_LENGHT+j] = tmp1[i][j]; m_attributes[i * ATTRIBUTE_LENGHT + j] = tmp1[i][j];
} }
} }
} }
private void setFirstError(AndrolibException error) { private void setFirstError(AndrolibException error) {
if (mFirstError == null) { if (mFirstError == null) {
mFirstError = error; mFirstError = error;
} }
} }
/////////////////////////////////// data /////////////////////////////////// data
/* /*
* All values are essentially indices, e.g. m_name is * All values are essentially indices, e.g. m_name is an index of name in
* an index of name in m_strings. * m_strings.
*/ */
private ExtDataInput m_reader; private ExtDataInput m_reader;
private ResAttrDecoder mAttrDecoder; private ResAttrDecoder mAttrDecoder;
private AndrolibException mFirstError; private AndrolibException mFirstError;
private boolean m_operational = false; private boolean m_operational = false;
private StringBlock m_strings; private StringBlock m_strings;
private int[] m_resourceIDs; private int[] m_resourceIDs;
@ -1048,28 +1056,24 @@ public class AXmlResourceParser implements XmlResourceParser {
private int m_idAttribute; private int m_idAttribute;
private int m_classAttribute; private int m_classAttribute;
private int m_styleAttribute; private int m_styleAttribute;
private final static Logger LOGGER = private final static Logger LOGGER =
Logger.getLogger(AXmlResourceParser.class.getName()); Logger.getLogger(AXmlResourceParser.class.getName());
private static final String E_NOT_SUPPORTED = "Method is not supported."; private static final String E_NOT_SUPPORTED = "Method is not supported.";
private static final int private static final int ATTRIBUTE_IX_NAMESPACE_URI = 0,
ATTRIBUTE_IX_NAMESPACE_URI = 0, ATTRIBUTE_IX_NAME = 1,
ATTRIBUTE_IX_NAME = 1, ATTRIBUTE_IX_VALUE_STRING = 2,
ATTRIBUTE_IX_VALUE_STRING = 2, ATTRIBUTE_IX_VALUE_TYPE = 3,
ATTRIBUTE_IX_VALUE_TYPE = 3, ATTRIBUTE_IX_VALUE_DATA = 4,
ATTRIBUTE_IX_VALUE_DATA = 4, ATTRIBUTE_LENGHT = 5;
ATTRIBUTE_LENGHT = 5; private static final int CHUNK_AXML_FILE = 0x00080003,
CHUNK_RESOURCEIDS = 0x00080180,
private static final int CHUNK_XML_FIRST = 0x00100100,
CHUNK_AXML_FILE = 0x00080003,
CHUNK_RESOURCEIDS = 0x00080180,
CHUNK_XML_FIRST = 0x00100100,
CHUNK_XML_START_NAMESPACE = 0x00100100, CHUNK_XML_START_NAMESPACE = 0x00100100,
CHUNK_XML_END_NAMESPACE = 0x00100101, CHUNK_XML_END_NAMESPACE = 0x00100101,
CHUNK_XML_START_TAG = 0x00100102, CHUNK_XML_START_TAG = 0x00100102,
CHUNK_XML_END_TAG = 0x00100103, CHUNK_XML_END_TAG = 0x00100103,
CHUNK_XML_TEXT = 0x00100104, CHUNK_XML_TEXT = 0x00100104,
CHUNK_XML_LAST = 0x00100104; CHUNK_XML_LAST = 0x00100104;
private Writer dbgOut = null; private Writer dbgOut = null;
private final static boolean DBG = false; private final static boolean DBG = false;
} }

View File

@ -156,37 +156,24 @@ public final class ResXmlEncoders {
return out.toString(); return out.toString();
} }
/** private static List<Integer> findNonPositionalSubstitutions(String str,
* It searches for "%", but not "%%" nor "%(\d)+\$"
*/
private static List<Integer> findNonPositionalSubstitutions(String str,
int max) { int max) {
int pos = 0; int pos = 0;
int pos2 = 0;
int count = 0; int count = 0;
int length = str.length(); int length = str.length();
List<Integer> ret = new ArrayList<Integer>(); List<Integer> ret = new ArrayList<Integer>();
while((pos2 = (pos = str.indexOf('%', pos2)) + 1) != 0) { while((pos = str.indexOf('%', pos)) != -1) {
if (pos2 == length) { if (pos + 1 == length) {
break; break;
} }
char c = str.charAt(pos2++); char c = str.charAt(pos + 1);
if (c == '%') { if (c >= 'a' && c <= 'z') {
continue; ret.add(pos);
} if (max != -1 && ++count >= max) {
if (c >= '0' && c <= '9' && pos2 < length) { break;
do {
c = str.charAt(pos2++);
} while (c >= '0' && c <= '9' && pos2 < length);
if (c == '$') {
continue;
} }
} }
pos += 2;
ret.add(pos);
if (max != -1 && ++count >= max) {
break;
}
} }
return ret; return ret;