/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source 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, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.quercus.lib.string; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.security.MessageDigest; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Currency; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import com.caucho.quercus.QuercusException; import com.caucho.quercus.QuercusModuleException; import com.caucho.quercus.annotation.Expect; import com.caucho.quercus.annotation.NotNull; import com.caucho.quercus.annotation.Optional; import com.caucho.quercus.annotation.Reference; import com.caucho.quercus.annotation.UsesSymbolTable; import com.caucho.quercus.env.ArrayValue; import com.caucho.quercus.env.ArrayValueImpl; import com.caucho.quercus.env.BooleanValue; import com.caucho.quercus.env.DefaultValue; import com.caucho.quercus.env.DoubleValue; import com.caucho.quercus.env.Env; import com.caucho.quercus.env.LocaleInfo; import com.caucho.quercus.env.LongValue; import com.caucho.quercus.env.NullValue; import com.caucho.quercus.env.QuercusLocale; import com.caucho.quercus.env.StringValue; import com.caucho.quercus.env.UnexpectedValue; import com.caucho.quercus.env.Value; import com.caucho.quercus.env.Var; import com.caucho.quercus.lib.file.BinaryOutput; import com.caucho.quercus.lib.file.FileModule; import com.caucho.quercus.module.AbstractQuercusModule; import com.caucho.util.CharBuffer; import com.caucho.util.FreeList; import com.caucho.util.IntSet; import com.caucho.util.L10N; import com.caucho.util.RandomUtil; import com.caucho.vfs.ByteToChar; import com.caucho.vfs.Path; /** * PHP functions implemented from the string module */ public class StringModule extends AbstractQuercusModule { private static final Logger log = Logger.getLogger(StringModule.class.getName()); private static final L10N L = new L10N(StringModule.class); public static final int CRYPT_SALT_LENGTH = 2; public static final int CRYPT_STD_DES = 0; public static final int CRYPT_EXT_DES = 0; public static final int CRYPT_MD5 = 0; public static final int CRYPT_BLOWFISH = 0; public static final int CHAR_MAX = 1; public static final int LC_CTYPE = 1; public static final int LC_NUMERIC = 2; public static final int LC_TIME = 3; public static final int LC_COLLATE = 4; public static final int LC_MONETARY = 5; public static final int LC_ALL = 6; public static final int LC_MESSAGES = 7; public static final int STR_PAD_LEFT = 1; public static final int STR_PAD_RIGHT = 0; public static final int STR_PAD_BOTH = 2; private static final DecimalFormatSymbols DEFAULT_DECIMAL_FORMAT_SYMBOLS; private static final BigInteger BIG_TEN = new BigInteger("10"); private static final BigInteger BIG_2_64 = BigInteger.ONE.shiftLeft(64); private static final FreeList<MessageDigest> _md5FreeList = new FreeList<MessageDigest>(16); /** * Escapes a string using C syntax. * * @see #stripcslashes * * @param source the source string to convert * @param characters the set of characters to convert * @return the escaped string */ public static StringValue addcslashes( Env env, StringValue source, String characters) { if (characters == null) characters = ""; boolean []bitmap = parseCharsetBitmap(env, characters); int length = source.length(); StringValue sb = source.createStringBuilder(length * 5 / 4); for (int i = 0; i < length; i++) { char ch = source.charAt(i); if (ch >= 256 || ! bitmap[ch]) { sb.append(ch); continue; } switch (ch) { case 0x07: sb.append("\\a"); break; case '\b': sb.append("\\b"); break; case '\t': sb.append("\\t"); break; case '\n': sb.append("\\n"); break; case 0xb: sb.append("\\v"); break; case '\f': sb.append("\\f"); break; case '\r': sb.append("\\r"); break; default: if (ch < 0x20 || ch >= 0x7f) { // save as octal sb.append("\\"); sb.append((char) ('0' + ((ch >> 6) & 7))); sb.append((char) ('0' + ((ch >> 3) & 7))); sb.append((char) ('0' + ((ch) & 7))); break; } else { sb.append("\\"); sb.append(ch); break; } } } return sb; } /** * Parses the cslashes bitmap returning an actual bitmap. * * @param charset the bitmap string * @return the actual bitmap */ private static boolean []parseCharsetBitmap(Env env, String charset) { boolean []bitmap = new boolean[256]; int length = charset.length(); for (int i = 0; i < length; i++) { char ch = charset.charAt(i); // XXX: the bitmap eventual might need to deal with unicode if (ch >= 256) continue; bitmap[ch] = true; if (length <= i + 3) continue; if (charset.charAt(i + 1) != '.' || charset.charAt(i + 2) != '.') continue; char last = charset.charAt(i + 3); if (last < ch) { env.warning(L.l("character set range is invalid: {0}..{1}", ch, last)); continue; } i += 3; for (; ch <= last; ch++) { bitmap[ch] = true; } // XXX: handling of '@'? } return bitmap; } /** * Escapes a string for db characters. * * @param source the source string to convert * @return the escaped string */ public static StringValue addslashes(StringValue source) { StringValue sb = source.createStringBuilder(source.length() * 5 / 4); int length = source.length(); for (int i = 0; i < length; i++) { char ch = source.charAt(i); switch (ch) { case 0x0: sb.append("\\0"); break; case '\'': sb.append("\\'"); break; case '\"': sb.append("\\\""); break; case '\\': sb.append("\\\\"); break; default: sb.append(ch); break; } } return sb; } /** * Converts a binary value to a hex value. */ public static StringValue bin2hex(Env env, InputStream is) { try { StringValue sb = env.createUnicodeBuilder(); int ch; while ((ch = is.read()) >= 0) { int d = (ch >> 4) & 0xf; if (d < 10) sb.append((char) (d + '0')); else sb.append((char) (d + 'a' - 10)); d = (ch) & 0xf; if (d < 10) sb.append((char) (d + '0')); else sb.append((char) (d + 'a' - 10)); } return sb; } catch (IOException e) { throw new QuercusModuleException(e); } } /** * Alias of rtrim. Removes trailing whitespace. * * @param env the quercus environment * @param str the string to be trimmed * @param charset optional set of characters to trim * @return the trimmed string */ public static StringValue chop(Env env, StringValue str, @Optional String charset) { return rtrim(env, str, charset); } /** * converts a number to its character equivalent * * @param value the integer value * * @return the string equivalent */ public static StringValue chr(Env env, long value) { if (! env.isUnicodeSemantics()) value = value & 0xFF; StringValue sb = env.createUnicodeBuilder(); sb.append((char) value); return sb; } /** * Splits a string into chunks * * @param body the body string * @param chunkLen the optional chunk length, defaults to 76 * @param end the optional end value, defaults to "\r\n" */ public static String chunk_split(String body, @Optional("76") int chunkLen, @Optional("\"\\r\\n\"") String end) { if (body == null) body = ""; if (end == null) end = ""; if (chunkLen < 1) // XXX: real exn throw new IllegalArgumentException(L.l("bad value {0}", chunkLen)); StringBuilder sb = new StringBuilder(); int i = 0; for (; i + chunkLen <= body.length(); i += chunkLen) { sb.append(body.substring(i, i + chunkLen)); sb.append(end); } if (i < body.length()) { sb.append(body.substring(i)); sb.append(end); } return sb.toString(); } /** * Converts from one cyrillic set to another. * * This implementation does nothing, because quercus stores strings as * 16 bit unicode. */ public static String convert_cyr_string(Env env, String str, String from, String to) { env.stub("convert_cyr_string"); return str; } public static Value convert_uudecode(Env env, StringValue source) { try { int length = source.length(); if (length == 0) return BooleanValue.FALSE; ByteToChar byteToChar = env.getByteToChar(); int i = 0; while (i < length) { int ch1 = source.charAt(i++); if (ch1 == 0x60 || ch1 == 0x20) break; else if (ch1 < 0x20 || 0x5f < ch1) continue; int sublen = ch1 - 0x20; while (sublen > 0) { int code; code = ((source.charAt(i++) - 0x20) & 0x3f) << 18; code += ((source.charAt(i++) - 0x20) & 0x3f) << 12; code += ((source.charAt(i++) - 0x20) & 0x3f) << 6; code += ((source.charAt(i++) - 0x20) & 0x3f); byteToChar.addByte(code >> 16); if (sublen > 1) byteToChar.addByte(code >> 8); if (sublen > 2) byteToChar.addByte(code); sublen -= 3; } } return env.createString(byteToChar.getConvertedString()); } catch (IOException e) { throw new QuercusModuleException(e); } } /** * uuencode a string. */ public static Value convert_uuencode(StringValue source) { if (source.length() == 0) return BooleanValue.FALSE; StringValue result = source.createStringBuilder(); int i = 0; int length = source.length(); while (i < length) { int sublen = length - i; if (45 < sublen) sublen = 45; result.append((char) (sublen + 0x20)); int end = i + sublen; while (i < end) { int code = source.charAt(i++) << 16; if (i < length) code += source.charAt(i++) << 8; if (i < length) code += source.charAt(i++); result.append(toUUChar(((code >> 18) & 0x3f))); result.append(toUUChar(((code >> 12) & 0x3f))); result.append(toUUChar(((code >> 6) & 0x3f))); result.append(toUUChar(((code) & 0x3f))); } result.append('\n'); } result.append((char) 0x60); result.append('\n'); return result; } /** * Returns an array of information about the characters. */ public static Value count_chars(StringValue data, @Optional("0") int mode) { int []count = new int[256]; int length = data.length(); for (int i = 0; i < length; i++) { count[data.charAt(i) & 0xff] += 1; } switch (mode) { case 0: { ArrayValue result = new ArrayValueImpl(); for (int i = 0; i < count.length; i++) { result.put(LongValue.create(i), LongValue.create(count[i])); } return result; } case 1: { ArrayValue result = new ArrayValueImpl(); for (int i = 0; i < count.length; i++) { if (count[i] > 0) result.put(LongValue.create(i), LongValue.create(count[i])); } return result; } case 2: { ArrayValue result = new ArrayValueImpl(); for (int i = 0; i < count.length; i++) { if (count[i] == 0) result.put(LongValue.create(i), LongValue.create(count[i])); } return result; } case 3: { StringValue sb = data.createStringBuilder(); for (int i = 0; i < count.length; i++) { if (count[i] > 0) sb.append((char) i); } return sb; } case 4: { StringValue sb = data.createStringBuilder(); for (int i = 0; i < count.length; i++) { if (count[i] == 0) sb.append((char) i); } return sb; } default: return BooleanValue.FALSE; } } /** * Calculates the crc32 value for a string * * @param str the string value * * @return the crc32 hash */ public static long crc32(StringValue str) { return str.getCrc32Value(); } public static String crypt(String string, @Optional String salt) { if (string == null) string = ""; if (salt == null || salt.equals("")) { salt = ("" + Crypt.resultToChar(RandomUtil.nextInt(0x40)) + Crypt.resultToChar(RandomUtil.nextInt(0x40))); } return Crypt.crypt(string, salt); } // XXX: echo /** * Explodes a string into an array * * @param separator the separator string * @param string the string to be exploded * @param limit the max number of elements * @return an array of exploded values */ public static Value explode(Env env, StringValue separator, StringValue string, @Optional("0x7fffffff") long limit) { if (separator.length() == 0) { env.warning(L.l("Delimiter is empty")); return BooleanValue.FALSE; } int head = 0; ArrayValue array = new ArrayValueImpl(); int separatorLength = separator.length(); int stringLength = string.length(); long ulimit; if (limit >= 0) { ulimit = limit; } else { ulimit = 0x7fffffff; } for (int i = 0; i < stringLength; ++i) { if (ulimit <= array.getSize() + 1) { break; } if (string.regionMatches(i, separator, 0, separatorLength)) { StringValue chunk = string.substring(head, i); array.append(chunk); head = i + separatorLength; i = head - 1; } } StringValue chunk = string.substring(head); array.append(chunk); while (array.getSize() > 0 && limit++ < 0) { array.pop(env); } return array; } /** * Use printf style formatting to write a string to a file. * @param fd the file to write to * @param format the format string * @param args the valujes to apply to the format string */ public static Value fprintf(Env env, @NotNull BinaryOutput os, StringValue format, Value []args) { Value value = sprintf(env, format, args); return FileModule.fwrite(env, os, value.toInputStream(), Integer.MAX_VALUE); } /** * implodes an array into a string * * @param glueV the separator string * @param piecesV the array to be imploded * * @return a string of imploded values */ public static Value implode(Env env, Value glueV, @Optional Value piecesV) { StringValue glue; ArrayValue pieces; if ((piecesV.isArray() && glueV.isArray()) || glueV.isArray()) { pieces = glueV.toArrayValue(env); glue = piecesV.toStringValue(); } else if (piecesV.isArray()) { pieces = piecesV.toArrayValue(env); glue = glueV.toStringValue(); } else { env.warning(L.l("neither argument to implode is an array: {0}, {1}", glueV.getClass().getName(), piecesV.getClass().getName())); return NullValue.NULL; } StringValue sb = glue.createStringBuilder(); boolean isFirst = true; Iterator<Value> iter = pieces.getValueIterator(env); while (iter.hasNext()) { if (! isFirst) sb = sb.append(glue); isFirst = false; sb = sb.append(iter.next()); } return sb; } /** * implodes an array into a string * * @param glueV the separator string * @param piecesV the array to be imploded * * @return a string of imploded values */ public static Value join(Env env, Value glueV, Value piecesV) { return implode(env, glueV, piecesV); } // XXX: lcfirst // XXX: levenshtein /** * Gets locale-specific symbols. * XXX: locale charset */ public static ArrayValue localeconv(Env env) { ArrayValueImpl array = new ArrayValueImpl(); QuercusLocale money = env.getLocaleInfo().getMonetary(); Locale locale = money.getLocale(); DecimalFormatSymbols decimal = new DecimalFormatSymbols(locale); Currency currency = NumberFormat.getInstance(locale).getCurrency(); array.put(env.createString("decimal_point"), env.createString(decimal.getDecimalSeparator())); array.put(env.createString("thousands_sep"), env.createString(decimal.getGroupingSeparator())); //array.put("grouping", ""); array.put(env.createString("int_curr_symbol"), env.createString(decimal.getInternationalCurrencySymbol())); array.put(env.createString("currency_symbol"), env.createString(decimal.getCurrencySymbol())); array.put(env.createString("mon_decimal_point"), env.createString(decimal.getMonetaryDecimalSeparator())); array.put(env.createString("mon_thousands_sep"), env.createString(decimal.getGroupingSeparator())); //array.put("mon_grouping", ""); array.put(env.createString("positive_sign"), env.getEmptyString()); array.put(env.createString("negative_sign"), env.createString(decimal.getMinusSign())); array.put(env.createString("int_frac_digits"), LongValue.create(currency.getDefaultFractionDigits())); array.put(env.createString("frac_digits"), LongValue.create(currency.getDefaultFractionDigits())); //array.put("p_cs_precedes", ""); //array.put("p_sep_by_space", ""); //array.put("n_cs_precedes", ""); //array.put("n_sep_by_space", ""); //array.put("p_sign_posn", ""); //array.put("n_sign_posn", ""); return array; } /** * Removes leading whitespace. * * @param string the string to be trimmed * @param characters optional set of characters to trim * @return the trimmed string */ public static StringValue ltrim(Env env, StringValue string, @Optional String characters) { if (characters == null) characters = ""; boolean []trim; if (characters.equals("")) trim = TRIM_WHITESPACE; else trim = parseCharsetBitmap(env, characters); for (int i = 0; i < string.length(); i++) { char ch = string.charAt(i); if (ch >= 256 || ! trim[ch]) { if (i == 0) return string; else return string.substring(i); } } return env.getEmptyString(); } /** * returns the md5 hash * * @param source the string * @param rawOutput if true, return the raw binary * * @return a string of imploded values */ public static Value md5(Env env, InputStream is, @Optional boolean rawOutput) { try { MessageDigest md = _md5FreeList.allocate(); if (md == null) md = MessageDigest.getInstance("MD5"); md.reset(); // XXX: iso-8859-1 int ch; while ((ch = is.read()) >= 0) { md.update((byte) ch); } byte []digest = md.digest(); _md5FreeList.free(md); return hashToValue(env, digest, rawOutput); } catch (Exception e) { throw new QuercusModuleException(e); } } /** * returns the md5 hash * * @param source the string * @param rawOutput if true, return the raw binary * * @return a string of imploded values */ public static Value md5_file(Env env, Path source, @Optional boolean rawOutput) { try { MessageDigest md = MessageDigest.getInstance("MD5"); InputStream is = null; try { is = source.openRead(); int d; while ((d = is.read()) >= 0) { md.update((byte) d); } byte []digest = md.digest(); return hashToValue(env, digest, rawOutput); } catch (IOException e) { log.log(Level.FINE, e.toString(), e); return BooleanValue.FALSE; } finally { try { if (is != null) is.close(); } catch (IOException e) { } } } catch (Exception e) { throw new QuercusModuleException(e); } } private static StringValue digestToString(Env env, byte []digest) { StringValue sb = env.createUnicodeBuilder(); for (int i = 0; i < digest.length; i++) { int d1 = (digest[i] >> 4) & 0xf; int d2 = (digest[i] & 0xf); sb.append(toHexChar(d1)); sb.append(toHexChar(d2)); } return sb; } /** * Returns the metaphone of a string. * This implementation produces identical results to the php version, * which does contain some bugs. */ public static String metaphone(String string) { if (string == null) string = ""; int length = string.length(); int index = 0; char ch = 0; // ignore everything up until first letter for (; index < length; index++) { ch = toUpperCase(string.charAt(index)); if ('A' <= ch && ch <= 'Z') break; } if (index == length) return ""; int lastIndex = length - 1; StringBuilder result = new StringBuilder(length); // special case first letter char nextCh = index < lastIndex ? toUpperCase(string.charAt(index + 1)) : 0; switch (ch) { case 'A': if (nextCh == 'E') { result.append('E'); index += 2; } else { result.append('A'); index += 1; } break; case 'E': case 'I': case 'O': case 'U': result.append(ch); index += 1; break; case 'G': case 'K': case 'P': if (nextCh == 'N') { result.append('N'); index += 2; } break; case 'W': if (nextCh == 'H' || nextCh == 'R') { result.append(nextCh); index += 2; } else { switch (nextCh) { case 'A': case 'E': case 'I': case 'O': case 'U': result.append('W'); index += 2; break; default: break; } } break; case 'X': result.append('S'); index += 1; break; default: break; } // the rest of the letters char prevCh; for (; index < length; index++) { if (index > 0) prevCh = toUpperCase(string.charAt(index - 1)); else prevCh = 0; ch = toUpperCase(string.charAt(index)); if (ch < 'A' || ch > 'Z') continue; if (ch == prevCh && ch != 'C') continue; if (index + 1 < length) nextCh = toUpperCase(string.charAt(index + 1)); else nextCh = 0; char nextnextCh; if (index + 2 < length) nextnextCh = toUpperCase(string.charAt(index + 2)); else nextnextCh = 0; switch (ch) { case 'B': if (prevCh != 'M') result.append('B'); break; case 'C': switch (nextCh) { case 'E': case 'I': case 'Y': // makesoft if (nextCh == 'I' && nextnextCh == 'A') { result.append('X'); } else if (prevCh == 'S') { } else { result.append('S'); } break; default: if (nextCh == 'H') { result.append('X'); index++; } else { result.append('K'); } break; } break; case 'D': if (nextCh == 'G') { switch (nextnextCh) { case 'E': case 'I': case 'Y': // makesoft result.append('J'); index++; break; default: result.append('T'); break; } } else result.append('T'); break; case 'G': if (nextCh == 'H') { boolean isSilent = false; if (index - 3 >= 0) { char prev3Ch = toUpperCase(string.charAt(index - 3)); switch (prev3Ch) { // noghtof case 'B': case 'D': case 'H': isSilent = true; break; default: break; } } if (!isSilent) { if (index - 4 >= 0) { char prev4Ch = toUpperCase(string.charAt(index - 4)); isSilent = (prev4Ch == 'H'); } } if (!isSilent) { result.append('F'); index++; } } else if (nextCh == 'N') { char nextnextnextCh; if (index + 3 < length) nextnextnextCh = toUpperCase(string.charAt(index + 3)); else nextnextnextCh = 0; if (nextnextCh < 'A' || nextnextCh > 'Z') { } else if (nextnextCh == 'E' && nextnextnextCh == 'D') { } else result.append('K'); } else if (prevCh == 'G') { result.append('K'); } else { switch (nextCh) { case 'E': case 'I': case 'Y': // makesoft result.append('J'); break; default: result.append('K'); break; } } break; case 'H': case 'W': case 'Y': switch (nextCh) { case 'A': case 'E': case 'I': case 'O': case 'U': // followed by a vowel if (ch == 'H') { switch (prevCh) { case 'C': case 'G': case 'P': case 'S': case 'T': // affecth break; default: result.append('H'); break; } } else result.append(ch); break; default: // not followed by a vowel break; } break; case 'K': if (prevCh != 'C') result.append('K'); break; case 'P': if (nextCh == 'H') result.append('F'); else result.append('P'); break; case 'Q': result.append('K'); break; case 'S': if (nextCh == 'I' && (nextnextCh == 'O' || nextnextCh == 'A')) { result.append('X'); } else if (nextCh == 'H') { result.append('X'); index++; } else result.append('S'); break; case 'T': if (nextCh == 'I' && (nextnextCh == 'O' || nextnextCh == 'A')) { result.append('X'); } else if (nextCh == 'H') { result.append('0'); index++; } else result.append('T'); break; case 'V': result.append('F'); break; case 'X': result.append('K'); result.append('S'); break; case 'Z': result.append('S'); break; case 'F': case 'J': case 'L': case 'M': case 'N': case 'R': result.append(ch); break; default: break; } } return result.toString(); } /** * Returns a formatted money value. * XXX: locale charset * * @param format the format * @param value the value * * @return a string of formatted values */ public static String money_format(Env env, String format, double value) { QuercusLocale monetary = env.getLocaleInfo().getMonetary(); Locale locale = monetary.getLocale(); return NumberFormat.getCurrencyInstance(locale).format(value); } // XXX: nl_langinfo // XXX: nl2br /** * Returns a formatted number. * * @param value the value * @param decimals the number of decimals * @param pointValue the decimal point string * @param groupValue the thousands separator * * @return a string of the formatted number */ public static String number_format(Env env, double value, @Optional int decimals, @Optional Value pointValue, @Optional Value groupValue) { boolean isGroupDefault = (groupValue instanceof DefaultValue); boolean isPointDefault = (pointValue instanceof DefaultValue); if (!isPointDefault && isGroupDefault) { env.warning(L.l("wrong parameter count")); return null; } String pattern; char point = '.'; if (! pointValue.isNull()) { String pointString = pointValue.toString(); point = (pointString.length() == 0) ? 0 : pointString.charAt(0); } char group = ','; if (! groupValue.isNull()) { String groupString = groupValue.toString(); group = (groupString.length() == 0) ? 0 : groupString.charAt(0); } if (decimals > 0) { StringBuilder patternBuilder = new StringBuilder(6 + decimals); patternBuilder.append(group == 0 ? "###0." : "#,##0."); for (int i = 0; i < decimals; i++) patternBuilder.append('0'); pattern = patternBuilder.toString(); } else { pattern = group == 0 ? "###0" : "#,##0"; } DecimalFormatSymbols decimalFormatSymbols; if (point == '.' && group == ',') decimalFormatSymbols = DEFAULT_DECIMAL_FORMAT_SYMBOLS; else { decimalFormatSymbols = new DecimalFormatSymbols(); decimalFormatSymbols.setDecimalSeparator(point); decimalFormatSymbols.setGroupingSeparator(group); decimalFormatSymbols.setZeroDigit('0'); } DecimalFormat format = new DecimalFormat(pattern, decimalFormatSymbols); String result = format.format(value); if (point == 0 && decimals > 0) { // no way to get DecimalFormat to output nothing for the point, // so remove it here int i = result.lastIndexOf(point); return result.substring(0, i) + result.substring(i + 1, result.length()); } else return result; } /** * Converts the first character to an integer. * * @param string the string to be converted * * @return the integer value */ public static long ord(StringValue string) { if (string.length() == 0) return 0; else { return string.charAt(0); } } /** * Parses the string as a query string. * * @param env the calling environment * @param str the query string * @param array the optional result array */ @UsesSymbolTable public static Value parse_str(Env env, StringValue str, @Optional @Reference Value ref) { boolean isRef = ref instanceof Var; ArrayValue result = null; if (isRef) { result = new ArrayValueImpl(); ref.set(result); } else if (ref instanceof ArrayValue) { result = (ArrayValue) ref; isRef = true; } else result = new ArrayValueImpl(); return StringUtility.parseStr(env, str, result, isRef, env.getHttpInputEncoding()); } /** * Prints the string. * * @param env the quercus environment * @param value the string to print */ public static long print(Env env, Value value) { value.print(env); return 1; } /** * print to the output with a formatter * * @param env the quercus environment * @param format the format string * @param args the format arguments * * @return the formatted string */ public static int printf(Env env, StringValue format, Value []args) { Value str = sprintf(env, format, args); str.print(env); return str.length(); } /** * Converts a RFC2045 quoted printable string to a string. */ // XXX: i18n public static String quoted_printable_decode(String str) { if (str == null) str = ""; StringBuilder sb = new StringBuilder(); int length = str.length(); for (int i = 0; i < length; i++) { char ch = str.charAt(i); if (33 <= ch && ch <= 60) sb.append(ch); else if (62 <= ch && ch <= 126) sb.append(ch); else if (ch == ' ' || ch == '\t') { if (i + 1 < str.length() && (str.charAt(i + 1) == '\r' || str.charAt(i + 1) == '\n')) { sb.append('='); sb.append(toUpperHexChar(ch >> 4)); sb.append(toUpperHexChar(ch)); } else sb.append(ch); } else if (ch == '\r' || ch == '\n') { sb.append(ch); } else { sb.append('='); sb.append(toUpperHexChar(ch >> 4)); sb.append(toUpperHexChar(ch)); } } return sb.toString(); } /** * Escapes meta characters. * * @param string the string to be quoted * * @return the quoted */ public static Value quotemeta(StringValue string) { int len = string.length(); StringValue sb = string.createStringBuilder(len * 5 / 4); for (int i = 0; i < len; i++) { char ch = string.charAt(i); switch (ch) { case '.': case '\\': case '+': case '*': case '?': case '[': case '^': case ']': case '(': case ')': case '$': sb.append("\\"); sb.append(ch); break; default: sb.append(ch); break; } } return sb; } private static final boolean[]TRIM_WHITESPACE = new boolean[256]; static { TRIM_WHITESPACE['\0'] = true; TRIM_WHITESPACE['\b'] = true; TRIM_WHITESPACE[' '] = true; TRIM_WHITESPACE['\t'] = true; TRIM_WHITESPACE['\r'] = true; TRIM_WHITESPACE['\n'] = true; TRIM_WHITESPACE[0x0B] = true; } /** * Removes trailing whitespace. * * @param env the quercus environment * @param string the string to be trimmed * @param characters optional set of characters to trim * @return the trimmed string */ public static StringValue rtrim(Env env, StringValue string, @Optional String characters) { if (characters == null) characters = ""; boolean []trim; if (characters.equals("")) trim = TRIM_WHITESPACE; else trim = parseCharsetBitmap(env, characters); for (int i = string.length() - 1; i >= 0; i--) { char ch = string.charAt(i); if (ch >= 256 || ! trim[ch]) { if (i == string.length()) return string; else return (StringValue) string.subSequence(0, i + 1); } } return env.getEmptyString(); } /** * Sets locale configuration. */ public static Value setlocale(Env env, int category, Value localeArg, Value []fallback) { LocaleInfo localeInfo = env.getLocaleInfo(); if (localeArg instanceof ArrayValue) { for (Value value : ((ArrayValue) localeArg).values()) { QuercusLocale locale = setLocale(localeInfo, category, value.toString()); if (locale != null) return env.createString(locale.toString()); } } else { QuercusLocale locale = setLocale(localeInfo, category, localeArg.toString()); if (locale != null) return env.createString(locale.toString()); } for (int i = 0; i < fallback.length; i++) { QuercusLocale locale = setLocale(localeInfo, category, fallback[i].toString()); if (locale != null) return env.createString(locale.toString()); } return BooleanValue.FALSE; } /** * Sets locale configuration. */ private static QuercusLocale setLocale(LocaleInfo localeInfo, int category, String localeName) { QuercusLocale locale = findLocale(localeName); if (locale == null) return null; switch (category) { case LC_ALL: localeInfo.setAll(locale); return localeInfo.getMessages(); case LC_COLLATE: localeInfo.setCollate(locale); return localeInfo.getCollate(); case LC_CTYPE: localeInfo.setCtype(locale); return localeInfo.getCtype(); case LC_MONETARY: localeInfo.setMonetary(locale); return localeInfo.getMonetary(); case LC_NUMERIC: localeInfo.setNumeric(locale); return localeInfo.getNumeric(); case LC_TIME: localeInfo.setTime(locale); return localeInfo.getTime(); case LC_MESSAGES: localeInfo.setMessages(locale); return localeInfo.getMessages(); default: return null; } } /* * Example locale: fr_FR.UTF-8@euro, french (on Windows) * (French, France, UTF-8, with euro currency support) */ private static QuercusLocale findLocale(String localeName) { String language; String country; String charset = null; String variant = null; CharBuffer sb = CharBuffer.allocate(); int len = localeName.length(); int i = 0; char ch = 0; while (i < len && (ch = localeName.charAt(i++)) != '-' && ch != '_') { sb.append(ch); } language = sb.toString(); sb.clear(); while (i < len && (ch = localeName.charAt(i)) != '.' && ch != '@') { sb.append(ch); i++; } if (ch == '.') i++; country = sb.toString(); sb.clear(); while (i < len && (ch = localeName.charAt(i)) != '@') { sb.append(ch); i++; } if (sb.length() > 0) charset = sb.toString(); if (i + 1 < len) variant = localeName.substring(i + 1); Locale locale; // java versions >= 1.5 should automatically use the euro sign if (variant != null && ! variant.equalsIgnoreCase("euro")) locale = new Locale(language, country, variant); else if (country != null) locale = new Locale(language, country); else locale = new Locale(language); if (isValidLocale(locale)) return new QuercusLocale(locale, charset); else return null; } /** * Returns true if the locale is supported. */ private static boolean isValidLocale(Locale locale) { Locale []validLocales = Locale.getAvailableLocales(); for (int i = 0; i < validLocales.length; i++) { if (validLocales[i].equals(locale)) { return true; } } return false; } /** * returns the md5 hash * * @param source the string * @param rawOutput if true, return the raw binary * * @return a string of imploded values */ public static Value sha1(Env env, String source, @Optional boolean rawOutput) { if (source == null) source = ""; try { MessageDigest md = MessageDigest.getInstance("SHA1"); // XXX: iso-8859-1 for (int i = 0; i < source.length(); i++) { char ch = source.charAt(i); md.update((byte) ch); } byte []digest = md.digest(); return hashToValue(env, digest, rawOutput); } catch (Exception e) { throw new QuercusException(e); } } /** * returns the md5 hash * * @param source the string * @param rawOutput if true, return the raw binary * * @return a string of imploded values */ public static Value sha1_file(Env env, Path source, @Optional boolean rawOutput) { try { MessageDigest md = MessageDigest.getInstance("SHA1"); InputStream is = null; try { is = source.openRead(); int d; while ((d = is.read()) >= 0) { md.update((byte) d); } byte []digest = md.digest(); return hashToValue(env, digest, rawOutput); } catch (IOException e) { log.log(Level.FINE, e.toString(), e); return BooleanValue.FALSE; } finally { try { if (is != null) is.close(); } catch (IOException e) { } } } catch (Exception e) { throw new QuercusException(e); } } private static Value hashToValue(Env env, byte []bytes, boolean isBinary) { if (isBinary) { StringValue v = env.createBinaryBuilder(); v.append(bytes, 0, bytes.length); return v; } else { StringValue v = env.createUnicodeBuilder(); for (int i = 0; i < bytes.length; i++) { int ch = bytes[i]; int d1 = (ch >> 4) & 0xf; int d2 = (ch) & 0xf; if (d1 < 10) v.append((char) ('0' + d1)); else v.append((char) ('a' + d1 - 10)); if (d2 < 10) v.append((char) ('0' + d2)); else v.append((char) ('a' + d2 - 10)); } return v; } } // XXX: similar_text private static final char []SOUNDEX_VALUES = "01230120022455012623010202" .toCharArray(); public static Value soundex(StringValue string) { int length = string.length(); if (length == 0) return BooleanValue.FALSE; StringValue result = string.createStringBuilder(); int count = 0; char lastCode = 0; for (int i = 0; i < length && count < 4; i++) { char ch = toUpperCase(string.charAt(i)); if ('A' <= ch && ch <= 'Z') { char code = SOUNDEX_VALUES[ch - 'A']; if (count == 0) { result.append(ch); count++; } else if (code != '0' && code != lastCode) { result.append(code); count++; } lastCode = code; } } for (; count < 4; count++) { result.append('0'); } return result; } /** * Print to a string with a formatter * * @param format the format string * @param args the format arguments * * @return the formatted string */ public static Value sprintf(Env env, StringValue format, Value []args) { ArrayList<PrintfSegment> segments = parsePrintfFormat(env, format); StringValue sb = format.createStringBuilder(); for (PrintfSegment segment : segments) { if (! segment.apply(env, sb, args)) return BooleanValue.FALSE; } return sb; } private static ArrayList<PrintfSegment> parsePrintfFormat(Env env, StringValue format) { ArrayList<PrintfSegment> segments = new ArrayList<PrintfSegment>(); StringBuilder sb = new StringBuilder(); StringBuilder flags = new StringBuilder(); int length = format.length(); int index = 0; for (int i = 0; i < length; i++) { char ch = format.charAt(i); if (i + 1 < length && ch == '%') { // The C printf silently ignores invalid flags, so we need to // remove them if present. sb.append(ch); boolean isLeft = false; boolean isAlt = false; boolean isShowSign = false; int argIndex = -1; int leftPadLength = 0; int width = 0; int padChar = -1; flags.setLength(0); int j = i + 1; loop: for (; j < length; j++) { ch = format.charAt(j); switch (ch) { case '-': isLeft = true; if (j + 1 < length && format.charAt(j + 1) == '0') { padChar = '0'; j++; } /* for (int k = j + 1; k < length; k++) { char digit = format.charAt(k); if ('0' <= digit && digit <= '9') { leftPadLength = leftPadLength * 10 + digit - '0'; j++; } else break; } */ break; case '#': isAlt = true; break; case '0': if (padChar < 0) padChar = '0'; else { int value = 0; for (int k = j + 1; k < length; k++) { char digit = format.charAt(k); if ('0' <= digit && digit <= '9') { value = value * 10 + digit - '0'; j++; } else break; } if (j + 1 < length && format.charAt(j + 1) == '$') { argIndex = value - 1; j++; } else { width = value; } } break; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': int value = ch - '0'; for (int k = j + 1; k < length; k++) { char digit = format.charAt(k); if ('0' <= digit && digit <= '9') { value = value * 10 + digit - '0'; j++; } else break; } if (j + 1 < length && format.charAt(j + 1) == '$') { argIndex = value - 1; j++; } else { width = value; } break; case '\'': padChar = format.charAt(j + 1); j += 1; break; case '+': isShowSign = true; break; case ' ': case ',': case '(': flags.append(ch); break; default: break loop; } } int head = j; if (argIndex < 0) argIndex = index; loop: for (; j < length; j++) { ch = format.charAt(j); switch (ch) { case '%': i = j; segments.add(new TextPrintfSegment(sb)); sb.setLength(0); break loop; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': case '$': break; case 's': case 'S': sb.setLength(sb.length() - 1); if (width <= 0 && 0 < leftPadLength) width = leftPadLength; index++; segments.add(new StringPrintfSegment( sb, isLeft || isAlt, padChar, ch == 'S', width, format.substring(head, j).toString(), argIndex)); sb.setLength(0); i = j; break loop; case 'c': case 'C': sb.setLength(sb.length() - 1); if (width <= 0 && 0 < leftPadLength) width = leftPadLength; index++; segments.add(new CharPrintfSegment( sb, isLeft || isAlt, padChar, ch == 'C', width, format.substring(head, j).toString(), argIndex)); sb.setLength(0); i = j; break loop; /* case 'u': sb.setLength(sb.length() - 1); if (sb.length() > 0) segments.add(new TextPrintfSegment(sb)); sb.setLength(0); if (isLeft) sb.append('-'); if (isAlt) sb.append('#'); sb.append(flags); sb.append(format, head, j); sb.append(ch); //segments.add(UnsignedLongPrintfSegment.create( env, sb.toString(), index++)); sb.setLength(0); i = j; break loop; */ case 'i': ch = 'd'; case 'd': case 'x': case 'o': case 'X': case 'b': case 'B': case 'u': sb.setLength(sb.length() - 1); if (sb.length() > 0) segments.add(new TextPrintfSegment(sb)); sb.setLength(0); if (isAlt) sb.append('#'); if (isShowSign) sb.append('+'); sb.append(flags); if (width > 0) { if (isLeft) sb.append('-'); else if (padChar == '0') sb.append('0'); sb.append(width); } sb.append(format, head, j); sb.append(ch); index++; segments.add( LongPrintfSegment.create(env, sb.toString(), argIndex)); sb.setLength(0); i = j; break loop; case 'e': case 'E': case 'f': case 'g': case 'G': case 'F': QuercusLocale locale = null; if (ch == 'F') ch = 'f'; else locale = env.getLocaleInfo().getNumeric(); sb.setLength(sb.length() - 1); if (sb.length() > 0) segments.add(new TextPrintfSegment(sb)); sb.setLength(0); if (isAlt) sb.append('#'); if (isShowSign) sb.append('+'); sb.append(flags); if (width > 0) { if (isLeft) sb.append('-'); else if (padChar == '0') sb.append('0'); // '-' and '0' together is not supported by java.util.Formatter //if (padChar == '0') // sb.append((char) padChar); sb.append(width); } sb.append(format, head, j); sb.append(ch); index++; segments.add(new DoublePrintfSegment(sb.toString(), isLeft && padChar == '0', argIndex, locale)); sb.setLength(0); i = j; break loop; default: if (isLeft) sb.append('-'); if (isAlt) sb.append('#'); sb.append(flags); sb.append(format, head, j); sb.append(ch); i = j; break loop; } } } else sb.append(ch); } if (sb.length() > 0) segments.add(new TextPrintfSegment(sb)); return segments; } /** * scans a string * * @param format the format string * @param args the format arguments * * @return the formatted string */ public static Value sscanf(Env env, StringValue string, StringValue format, @Optional @Reference Value []args) { ScanfSegment[] formatArray = sscanfParseFormat(env, format); int strlen = string.length(); int sIndex = 0; boolean isReturnArray = args.length == 0; int argIndex = 0; if (strlen == 0) { return isReturnArray ? NullValue.NULL : LongValue.MINUS_ONE; } ArrayValue array = new ArrayValueImpl(); for (int i = 0; i < formatArray.length; i++) { ScanfSegment segment = formatArray[i]; Value var; if (! segment.isAssigned()) { var = null; } else if (isReturnArray) { var = array; } else { if (argIndex < args.length) { var = args[argIndex]; if (sIndex < strlen) argIndex++; } else { env.warning(L.l("not enough vars passed in")); var = NullValue.NULL; } } sIndex = segment.apply(string, strlen, sIndex, var, isReturnArray); if (sIndex < 0) { if (isReturnArray) return sscanfFillNull(array, formatArray, i); else return LongValue.create(argIndex); } /* else if (sIndex == strlen) { if (isReturnArray) return sscanfFillNull(array, formatArray, i + 1); else return LongValue.create(argIndex); } */ } return sscanfReturn(env, array, args, argIndex, isReturnArray, false); } private static Value sscanfFillNull( ArrayValue array, ScanfSegment[] formatArray, int fIndex) { for (; fIndex < formatArray.length; fIndex++) { ScanfSegment segment = formatArray[fIndex]; if (segment.isAssigned()) array.put(NullValue.NULL); } return array; } /** * scans a string * * @param format the format string * @param args the format arguments * * @return the formatted string */ private static ScanfSegment[] sscanfParseFormat(Env env, StringValue format) { int fmtLen = format.length(); int fIndex = 0; ArrayList<ScanfSegment> segmentList = new ArrayList<ScanfSegment>(); StringBuilder sb = new StringBuilder(); while (fIndex < fmtLen) { char ch = format.charAt(fIndex++); if (isWhitespace(ch)) { for (; (fIndex < fmtLen && isWhitespace(ch = format.charAt(fIndex))); fIndex++) { } scanfAddConstant(segmentList, sb); segmentList.add(ScanfWhitespace.SEGMENT); } else if (ch == '%') { int maxLen = -1; loop: while (fIndex < fmtLen) { ch = format.charAt(fIndex++); switch (ch) { case '%': sb.append('%'); break loop; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if (maxLen < 0) maxLen = 0; maxLen = 10 * maxLen + ch - '0'; break; case 's': { scanfAddConstant(segmentList, sb); segmentList.add(new ScanfString(maxLen)); break loop; } case 'c': { if (maxLen < 0) maxLen = 1; scanfAddConstant(segmentList, sb); segmentList.add(new ScanfString(maxLen)); break loop; } case 'n': { scanfAddConstant(segmentList, sb); segmentList.add(ScanfStringLength.SEGMENT); break loop; } case 'd': { scanfAddConstant(segmentList, sb); segmentList.add(new ScanfInteger(maxLen, 10, false)); break loop; } case 'u': { scanfAddConstant(segmentList, sb); segmentList.add(new ScanfInteger(maxLen, 10, true)); break loop; } case 'o': { scanfAddConstant(segmentList, sb); segmentList.add(new ScanfInteger(maxLen, 8, false)); break loop; } case 'x': case 'X': { scanfAddConstant(segmentList, sb); segmentList.add(new ScanfHex(maxLen)); break loop; } case 'e': case 'f': { scanfAddConstant(segmentList, sb); segmentList.add(new ScanfScientific(maxLen)); break loop; } case '[': { scanfAddConstant(segmentList, sb); if (fmtLen <= fIndex) { env.warning(L.l("expected ']', saw end of string")); break loop; } boolean isNegated = false; if (fIndex < fmtLen && format.charAt(fIndex) == '^') { isNegated = true; fIndex++; } IntSet set = new IntSet(); while (true) { if (fIndex == fmtLen) { env.warning(L.l("expected ']', saw end of string")); break loop; } char ch2 = format.charAt(fIndex++); if (ch2 == ']') { break; } else { set.union(ch2); } } if (isNegated) segmentList.add(new ScanfSetNegated(set)); else segmentList.add(new ScanfSet(set)); break loop; } default: log.fine(L.l("'{0}' is a bad sscanf string.", format)); env.warning(L.l("'{0}' is a bad sscanf string.", format)); // XXX: //return isAssign ? LongValue.create(argIndex) : array; break loop; } } } else sb.append(ch); } scanfAddConstant(segmentList, sb); ScanfSegment[] segmentArray = new ScanfSegment[segmentList.size()]; return segmentList.toArray(segmentArray); } private static void scanfAddConstant( ArrayList<ScanfSegment> segmentList, StringBuilder sb) { if (sb.length() == 0) return; segmentList.add(new ScanfConstant(sb.toString())); sb.setLength(0); } /** * scans a string * * @param format the format string * @param args the format arguments * * @return the formatted string */ public static Value sscanfOld(Env env, StringValue string, StringValue format, @Optional @Reference Value []args) { int fmtLen = format.length(); int strlen = string.length(); int sIndex = 0; int fIndex = 0; boolean isAssign = args.length != 0; boolean isReturnArray = ! isAssign; int argIndex = 0; if (strlen == 0) { return isAssign ? LongValue.MINUS_ONE : NullValue.NULL; } ArrayValue array = new ArrayValueImpl(); while (fIndex < fmtLen) { char ch = format.charAt(fIndex++); if (isWhitespace(ch)) { for (; (fIndex < fmtLen && isWhitespace(ch = format.charAt(fIndex))); fIndex++) { } /*ch = string.charAt(sIndex); if (! isWhitespace(ch)) { // XXX: return false? return sscanfReturn(env, array, args, argIndex, isAssign, true); }*/ for (; sIndex < strlen && isWhitespace(string.charAt(sIndex)); sIndex++) { } } else if (ch == '%') { int maxLen = -1; loop: while (fIndex < fmtLen) { ch = format.charAt(fIndex++); if (sIndex >= strlen && ch != 'n') { array.append(NullValue.NULL); break loop; } Value obj; if (isAssign) { if (argIndex < args.length) obj = args[argIndex++]; else { env.warning(L.l("not enough vars passed in")); break loop; } } else obj = array; switch (ch) { case '%': if (string.charAt(sIndex) != '%') return sscanfReturn(env, array, args, argIndex, isAssign, true); else break loop; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if (maxLen < 0) maxLen = 0; maxLen = 10 * maxLen + ch - '0'; break; case 's': { ScanfSegment seg = new ScanfString(maxLen); sIndex = seg.apply(string, strlen, sIndex, obj, isReturnArray); break loop; } case 'c': { if (maxLen < 0) maxLen = 1; ScanfSegment seg = new ScanfString(maxLen); sIndex = seg.apply(string, strlen, sIndex, obj, isReturnArray); break loop; } case 'n': { ScanfSegment seg = ScanfStringLength.SEGMENT; sIndex = seg.apply(string, strlen, sIndex, obj, isReturnArray); break loop; } case 'd': { ScanfSegment seg = new ScanfInteger(maxLen, 10, false); sIndex = seg.apply(string, strlen, sIndex, obj, isReturnArray); break loop; } case 'u': { ScanfSegment seg = new ScanfInteger(maxLen, 10, true); sIndex = seg.apply(string, strlen, sIndex, obj, isReturnArray); break loop; } case 'o': { ScanfSegment seg = new ScanfInteger(maxLen, 8, false); sIndex = seg.apply(string, strlen, sIndex, obj, isReturnArray); break loop; } case 'x': case 'X': { ScanfSegment seg = new ScanfHex(maxLen); sIndex = seg.apply(string, strlen, sIndex, obj, isReturnArray); break loop; } case 'e': case 'f': { ScanfSegment seg = new ScanfScientific(maxLen); sIndex = seg.apply(string, strlen, sIndex, obj, isReturnArray); break loop; } default: log.fine(L.l("'{0}' is a bad sscanf string.", format)); env.warning(L.l("'{0}' is a bad sscanf string.", format)); return isAssign ? LongValue.create(argIndex) : array; } } } else if (ch == string.charAt(sIndex)) { sIndex++; } else return sscanfReturn(env, array, args, argIndex, false, true); } return sscanfReturn(env, array, args, argIndex, isAssign, false); } private static Value sscanfReturn(Env env, ArrayValue array, Value []args, int argIndex, boolean isReturnArray, boolean isWarn) { if (isReturnArray) return array; else { if (isWarn && argIndex != args.length) env.warning( L.l("{0} vars passed in but saw only {1} '%' args", args.length, argIndex)); return LongValue.create(argIndex); } } /** * Scans a string with a given length. */ private static int sscanfString(StringValue string, int sIndex, int maxLen, Value obj, boolean isAssignment) { int strlen = string.length(); if (maxLen < 0) maxLen = Integer.MAX_VALUE; StringValue sb = string.createStringBuilder(); for (; sIndex < strlen && maxLen-- > 0; sIndex++) { char ch = string.charAt(sIndex); if (! isWhitespace(ch)) sb.append(ch); else break; } sscanfPut(obj, sb, isAssignment); return sIndex; } private static void sscanfPut(Value obj, Value val, boolean isAssignment) { if (isAssignment) obj.set(val); else obj.put(val); } /** * Scans a integer with a given length. */ private static int sscanfInteger(StringValue string, int sIndex, int maxLen, Value obj, boolean isAssign, int base, boolean isUnsigned) { int strlen = string.length(); if (maxLen < 0) maxLen = Integer.MAX_VALUE; int val = 0; int sign = 1; boolean isNotMatched = true; if (sIndex < strlen) { char ch = string.charAt(sIndex); if (ch == '+') { sIndex++; maxLen--; } else if (ch == '-') { sign = -1; sIndex++; maxLen--; } } int topRange = base + '0'; for (; sIndex < strlen && maxLen-- > 0; sIndex++) { char ch = string.charAt(sIndex); if ('0' <= ch && ch < topRange) { val = val * base + ch - '0'; isNotMatched = false; } else if (isNotMatched) { sscanfPut(obj, NullValue.NULL, isAssign); return sIndex; } else break; } if (isUnsigned) { if (sign == -1 && val != 0) sscanfPut(obj, StringValue.create(0xFFFFFFFFL - val + 1), isAssign); else sscanfPut(obj, LongValue.create(val), isAssign); } else sscanfPut(obj, LongValue.create(val * sign), isAssign); return sIndex; } /** * Scans a integer with a given length. */ private static int sscanfHex(StringValue string, int sIndex, int maxLen, Value obj, boolean isAssign) { int strlen = string.length(); if (maxLen < 0) maxLen = Integer.MAX_VALUE; int val = 0; int sign = 1; boolean isMatched = false; if (sIndex < strlen) { char ch = string.charAt(sIndex); if (ch == '+') { sIndex++; maxLen--; } else if (ch == '-') { sign = -1; sIndex++; maxLen--; } } for (; sIndex < strlen && maxLen-- > 0; sIndex++) { char ch = string.charAt(sIndex); if ('0' <= ch && ch <= '9') { val = val * 16 + ch - '0'; isMatched = true; } else if ('a' <= ch && ch <= 'f') { val = val * 16 + ch - 'a' + 10; isMatched = true; } else if ('A' <= ch && ch <= 'F') { val = val * 16 + ch - 'A' + 10; isMatched = true; } else if (! isMatched) { sscanfPut(obj, NullValue.NULL, isAssign); return sIndex; } else break; } sscanfPut(obj, LongValue.create(val * sign), isAssign); return sIndex; } /** * Scans a integer with a given length. */ private static int sscanfScientific(StringValue s, int i, int maxLen, Value obj, boolean isAssign) { if (maxLen < 0) maxLen = Integer.MAX_VALUE; int start = i; int len = s.length(); int ch = 0; if (i < len && maxLen > 0 && ((ch = s.charAt(i)) == '+' || ch == '-')) { i++; maxLen--; } for (; i < len && maxLen > 0 && '0' <= (ch = s.charAt(i)) && ch <= '9'; i++) { maxLen--; } if (ch == '.') { maxLen--; for (i++; i < len && maxLen > 0 && '0' <= (ch = s.charAt(i)) && ch <= '9'; i++) { maxLen--; } } if (ch == 'e' || ch == 'E') { maxLen--; int e = i++; if (start == e) { sscanfPut(obj, NullValue.NULL, isAssign); return start; } if (i < len && maxLen > 0 && (ch = s.charAt(i)) == '+' || ch == '-') { i++; maxLen--; } for (; i < len && maxLen > 0 && '0' <= (ch = s.charAt(i)) && ch <= '9'; i++) { maxLen--; } if (i == e + 1) i = e; } double val; if (i == 0) val = 0; else val = Double.parseDouble(s.substring(start, i).toString()); sscanfPut(obj, DoubleValue.create(val), isAssign); return i; } // XXX: str_getcsv /** * replaces substrings. * * @param search search string * @param replace replacement string * @param subject replacement * @param count return value */ public static Value str_ireplace(Env env, Value search, Value replace, Value subject, @Reference @Optional Value count) { return strReplace(env, search, replace, subject, count, true); } /** * Pads strings * * @param string string * @param length length * @param pad padding string * @param type padding type */ public static StringValue str_pad(StringValue string, int length, @Optional("' '") String pad, @Optional("STR_PAD_RIGHT") int type) { int strLen = string.length(); int padLen = length - strLen; if (padLen <= 0) return string; if (pad == null || pad.length() == 0) pad = " "; int leftPad = 0; int rightPad = 0; switch (type) { case STR_PAD_LEFT: leftPad = padLen; break; case STR_PAD_RIGHT: default: rightPad = padLen; break; case STR_PAD_BOTH: leftPad = padLen / 2; rightPad = padLen - leftPad; break; } int padStringLen = pad.length(); StringValue sb = string.createStringBuilder(string.length() + padLen); for (int i = 0; i < leftPad; i++) sb.append(pad.charAt(i % padStringLen)); sb = sb.append(string); for (int i = 0; i < rightPad; i++) sb.append(pad.charAt(i % padStringLen)); return sb; } /** * repeats a string * * @param string string to repeat * @param count number of times to repeat */ public static Value str_repeat(StringValue string, int count) { StringValue sb = string.createStringBuilder(count * string.length()); for (int i = 0; i < count; i++) sb = sb.append(string); return sb; } /** * replaces substrings. * * @param search search string * @param replace replacement string * @param subject replacement * @param count return value */ public static Value str_replace(Env env, Value search, Value replace, Value subject, @Reference @Optional Value count) { return strReplace(env, search, replace, subject, count, false); } /** * replaces substrings. * * @param search search string * @param replace replacement string * @param subject replacement * @param count return value */ private static Value strReplace(Env env, Value search, Value replace, Value subject, @Reference @Optional Value count, boolean isInsensitive) { count.set(LongValue.ZERO); if (subject.isNull()) return env.getEmptyString(); if (search.isNull()) return subject; if (subject instanceof ArrayValue) { ArrayValue subjectArray = (ArrayValue) subject; ArrayValue resultArray = new ArrayValueImpl(); for (Map.Entry<Value, Value> entry : subjectArray.entrySet()) { if (entry.getValue() instanceof ArrayValue) { resultArray.append(entry.getKey(), entry.getValue()); } else { Value result = strReplaceImpl(env, search, replace, entry.getValue().toStringValue(), count, isInsensitive); resultArray.append(entry.getKey(), result); } } return resultArray; } else { StringValue subjectString = subject.toStringValue(); if (subjectString.length() == 0) return env.getEmptyString(); return strReplaceImpl(env, search, replace, subjectString, count, isInsensitive); } } /** * replaces substrings. * * @param search search string * @param replace replacement string * @param subject replacement * @param count return value */ private static Value strReplaceImpl(Env env, Value search, Value replace, StringValue subject, Value count, boolean isInsensitive) { if (! search.isArray()) { StringValue searchString = search.toStringValue(); if (searchString.length() == 0) return subject; if (replace instanceof ArrayValue) { env.warning(L.l("Array to string conversion")); } subject = strReplaceImpl(env, searchString, replace.toStringValue(), subject, count, isInsensitive); } else if (replace instanceof ArrayValue) { ArrayValue searchArray = (ArrayValue) search; ArrayValue replaceArray = (ArrayValue) replace; Iterator<Value> searchIter = searchArray.values().iterator(); Iterator<Value> replaceIter = replaceArray.values().iterator(); while (searchIter.hasNext()) { Value searchItem = searchIter.next(); Value replaceItem = replaceIter.next(); if (replaceItem == null) replaceItem = NullValue.NULL; subject = strReplaceImpl(env, searchItem.toStringValue(), replaceItem.toStringValue(), subject, count, isInsensitive); } } else { ArrayValue searchArray = (ArrayValue) search; Iterator<Value> searchIter = searchArray.values().iterator(); while (searchIter.hasNext()) { Value searchItem = searchIter.next(); subject = strReplaceImpl(env, searchItem.toStringValue(), replace.toStringValue(), subject, count, isInsensitive); } } return subject; } /** * replaces substrings. * * @param search search string * @param replace replacement string * @param subject replacement * @param countV return value */ private static StringValue strReplaceImpl(Env env, StringValue search, StringValue replace, StringValue subject, Value countV, boolean isInsensitive) { long count = countV.toLong(); int head = 0; int next; int searchLen = search.length(); StringValue result = null; while (head <= (next = indexOf(subject, search, head, isInsensitive))) { if (result == null) result = subject.createStringBuilder(); result = result.append(subject, head, next); result = result.append(replace); if (head < next + searchLen) head = next + searchLen; else head += 1; count++; } if (count != 0 && result != null) { countV.set(LongValue.create(count)); int subjectLength = subject.length(); if (head > 0 && head < subjectLength) result = result.append(subject, head, subjectLength); return result; } else return subject; } /** * Returns the next index. */ private static int indexOf(StringValue subject, StringValue match, int head, boolean isInsensitive) { if (! isInsensitive) return subject.indexOf(match, head); else { int length = subject.length(); int matchLen = match.length(); if (matchLen <= 0) return -1; char ch = Character.toLowerCase(match.charAt(0)); loop: for (; head + matchLen <= length; head++) { if (ch == Character.toLowerCase(subject.charAt(head))) { for (int i = 1; i < matchLen; i++) { if (Character.toLowerCase(subject.charAt(head + i)) != Character.toLowerCase(match.charAt(i))) continue loop; } return head; } } return -1; } } /** * rot13 conversion * * @param string string to convert */ public static Value str_rot13(StringValue string) { if (string == null) return NullValue.NULL; StringValue sb = string.createStringBuilder(string.length()); int len = string.length(); for (int i = 0; i < len; i++) { char ch = string.charAt(i); if ('a' <= ch && ch <= 'z') { int off = ch - 'a'; sb.append((char) ('a' + (off + 13) % 26)); } else if ('A' <= ch && ch <= 'Z') { int off = ch - 'A'; sb.append((char) ('A' + (off + 13) % 26)); } else { sb.append(ch); } } return sb; } /** * shuffles a string */ public static String str_shuffle(String string) { if (string == null) string = ""; char []chars = string.toCharArray(); int length = chars.length; for (int i = 0; i < length; i++) { int rand = RandomUtil.nextInt(length); char temp = chars[rand]; chars[rand] = chars[i]; chars[i] = temp; } return new String(chars); } /** * split into an array * * @param string string to split * @param chunk chunk size */ public static Value str_split(StringValue string, @Optional("1") int chunk) { ArrayValue array = new ArrayValueImpl(); if (string.length() == 0) { array.put(string); return array; } int strLen = string.length(); int currentArrayIndex = 0; for (int i = 0; i < strLen; i += chunk) { Value value; if (i + chunk <= strLen) { value = string.substring(i, i + chunk); } else { value = string.substring(i); } array.put(LongValue.create(currentArrayIndex), value); currentArrayIndex++; } return array; } public static Value str_word_count(StringValue string, @Optional int format, @Optional String additionalWordCharacters) { if (format < 0 || format > 2) return NullValue.NULL; int strlen = string.length(); boolean isAdditionalWordCharacters = false; if (additionalWordCharacters != null) isAdditionalWordCharacters = additionalWordCharacters.length() > 0; ArrayValueImpl resultArray = null; if (format > 0) resultArray = new ArrayValueImpl(); boolean isBetweenWords = true; int wordCount = 0; int lastWordStart = 0; for (int i = 0; i <= strlen; i++) { boolean isWordCharacter; if (i < strlen) { int ch = string.charAt(i); isWordCharacter = Character.isLetter(ch) || ch == '-' || ch == '\'' || (isAdditionalWordCharacters && additionalWordCharacters.indexOf(ch) > -1); } else isWordCharacter = false; if (isWordCharacter) { if (isBetweenWords) { // starting a word isBetweenWords = false; lastWordStart = i; wordCount++; } } else { if (!isBetweenWords) { // finished a word isBetweenWords = true; if (format > 0) { StringValue word = string.substring(lastWordStart, i); if (format == 1) resultArray.append(word); else if (format == 2) resultArray.put(LongValue.create(lastWordStart), word); } } } } if (resultArray == null) return LongValue.create(wordCount); else return resultArray; } /** * Case-insensitive comparison * * @param a left value * @param b right value * @return -1, 0, or 1 */ public static int strcasecmp(StringValue a, StringValue b) { int aLen = a.length(); int bLen = b.length(); for (int i = 0; i < aLen && i < bLen; i++) { char chA = a.charAt(i); char chB = b.charAt(i); if (chA == chB) continue; if (Character.isUpperCase(chA)) chA = Character.toLowerCase(chA); if (Character.isUpperCase(chB)) chB = Character.toLowerCase(chB); if (chA == chB) continue; else if (chA < chB) return -1; else return 1; } if (aLen == bLen) return 0; else if (aLen < bLen) return -1; else return 1; } /** * Finds the index of a substring * * @param env the calling environment */ public static Value strchr(Env env, StringValue haystack, Value needle) { return strstr(env, haystack, needle); } /** * Case-sensitive comparison * * @param a left value * @param b right value * @return -1, 0, or 1 */ public static int strcmp(StringValue a, StringValue b) { int aLen = a.length(); int bLen = b.length(); for (int i = 0; i < aLen && i < bLen; i++) { char chA = a.charAt(i); char chB = b.charAt(i); if (chA == chB) continue; if (chA == chB) continue; else if (chA < chB) return -1; else return 1; } if (aLen == bLen) return 0; else if (aLen < bLen) return -1; else return 1; } /** * Locale-based comparison * XXX: i18n * * @param a left value * @param b right value * @return -1, 0, or 1 */ public static Value strcoll(String a, String b) { if (a == null) a = ""; if (b == null) b = ""; int cmp = a.compareTo(b); if (cmp == 0) return LongValue.ZERO; else if (cmp < 0) return LongValue.MINUS_ONE; else return LongValue.ONE; } /** * Finds the number of initial characters in <i>string</i> that do not match * one of the characters in <i>characters</i> * * @param string the string to search in * @param characters the character set * @param offset the starting offset * @param length the length * * @return the length of the match or FALSE if * the offset or length are invalid */ public static Value strcspn(StringValue string, StringValue characters, @Optional("0") int offset, @Optional("-2147483648") int length) { if (characters.length() == 0) { characters = StringValue.create((char)0); } return strspnImpl(string, characters, offset, length, false); } /** * Removes tags from a string. * * @param string the string to remove * @param allowTags the allowable tags */ public static StringValue strip_tags(StringValue string, @Optional Value allowTags) { StringValue result = string.createStringBuilder(string.length()); HashSet<StringValue> allowedTagMap = null; if (! allowTags.isDefault()) allowedTagMap = getAllowedTags(allowTags.toStringValue()); int len = string.length(); for (int i = 0; i < len; i++) { char ch = string.charAt(i); if (i + 1 >= len || ch != '<') { result.append(ch); continue; } ch = string.charAt(i + 1); if (Character.isWhitespace(ch)) { i++; result.append('<'); result.append(ch); continue; } int tagNameStart = i + 1; if (ch == '/') tagNameStart++; int j = tagNameStart; while (j < len && (ch = string.charAt(j)) != '>' // && ch != '/' && ! Character.isWhitespace(ch)) { j++; } StringValue tagName = string.substring(tagNameStart, j); int tagEnd = 0; if (allowedTagMap != null && allowedTagMap.contains(tagName)) { result.append(string, i, Math.min(j + 1, len)); } else { while (j < len && (ch = string.charAt(j)) != '<') { if (ch == '>') { tagEnd = j; } j++; } } i = (tagEnd != 0) ? tagEnd : j; } return result; } private static HashSet<StringValue> getAllowedTags(StringValue str) { int len = str.length(); HashSet<StringValue> set = new HashSet<StringValue>(); for (int i = 0; i < len; i++) { char ch = str.charAt(i); switch (ch) { case '<': int j = i + 1; while (j < len && (ch = str.charAt(j)) != '>' //&& ch != '/' && ! Character.isWhitespace(ch)) { j++; } if (ch == '>' && i + 1 < j && j < len) set.add(str.substring(i + 1, j)); i = j; default: continue; } } return set; } /** * Strip out the backslashes, recognizing the escape sequences, octal, * and hexadecimal representations. * * @param source the string to clean * @see #addcslashes */ public static String stripcslashes(String source) { if (source == null) source = ""; StringBuilder result = new StringBuilder(source.length()); int length = source.length(); for (int i = 0; i < length; i++) { int ch = source.charAt(i); if (ch == '\\') { i++; if (i == length) ch = '\\'; else { ch = source.charAt(i); switch (ch) { case 'a': ch = 0x07; break; case 'b': ch = '\b'; break; case 't': ch = '\t'; break; case 'n': ch = '\n'; break; case 'v': ch = 0xb; break; case 'f': ch = '\f'; break; case 'r': ch = '\r'; break; case 'x': // up to two digits for a hex number if (i + 1 == length) break; int digitValue = hexToDigit(source.charAt(i + 1)); if (digitValue < 0) break; ch = digitValue; i++; if (i + 1 == length) break; digitValue = hexToDigit(source.charAt(i + 1)); if (digitValue < 0) break; ch = ((ch << 4) | digitValue); i++; break; default: // up to three digits from 0 to 7 for an octal number digitValue = octToDigit((char) ch); if (digitValue < 0) break; ch = digitValue; if (i + 1 == length) break; digitValue = octToDigit(source.charAt(i + 1)); if (digitValue < 0) break; ch = ((ch << 3) | digitValue); i++; if (i + 1 == length) break; digitValue = octToDigit(source.charAt(i + 1)); if (digitValue < 0) break; ch = ((ch << 3) | digitValue); i++; } } } // if ch == '/' result.append((char) ch); } return result.toString(); } /** * Returns the position of a substring, testing case insensitive. * * @param haystack the full argument to check * @param needleV the substring argument to check * @param offsetV optional starting position */ public static Value stripos(Env env, StringValue haystack, Value needleV, @Optional int offset) { StringValue needle; int len = haystack.length(); if (len < offset) { env.warning(L.l("offset cannot exceed string length")); return BooleanValue.FALSE; } if (needleV instanceof StringValue) needle = (StringValue) needleV; else needle = StringValue.create((char) needleV.toInt()); haystack = haystack.toLowerCase(); needle = needle.toLowerCase(); int pos = haystack.indexOf(needle, offset); if (pos < 0) return BooleanValue.FALSE; else return LongValue.create(pos); } /** * Strips out the backslashes. * * @param string the string to clean */ public static StringValue stripslashes(StringValue string) { StringValue sb = string.createStringBuilder(); int len = string.length(); for (int i = 0; i < len; i++) { char ch = string.charAt(i); if (ch == '\\') { if (i + 1 < len) { char ch2 = string.charAt(i + 1); if (ch2 == '0') { ch2 = 0x0; } sb.append(ch2); i++; } } else sb.append(ch); } return sb; } /** * Finds the first instance of a substring, testing case insensitively * * @param haystack the string to search in * @param needleV the string to search for * @return the trailing match or FALSE */ public static Value stristr(StringValue haystack, Value needleV) { CharSequence needleLower; if (needleV instanceof StringValue) { needleLower = ((StringValue) needleV).toLowerCase(); } else { char lower = Character.toLowerCase((char) needleV.toLong()); needleLower = String.valueOf(lower); } StringValue haystackLower = haystack.toLowerCase(); int i = haystackLower.indexOf(needleLower); if (i >= 0) return haystack.substring(i); else return BooleanValue.FALSE; } /** * Returns the length of a string. * * @param value the argument value */ public static Value strlen(Value value) { return LongValue.create(value.length()); } /** * Case-insensitive comparison * * @param a left value * @param b right value * @return -1, 0, or 1 */ public static int strnatcasecmp(StringValue a, StringValue b) { return naturalOrderCompare(a, b, true); } /** * Case-sensitive comparison * * @param a left value * @param b right value * @return -1, 0, or 1 */ public static int strnatcmp(StringValue a, StringValue b) { return naturalOrderCompare(a, b, false); } /** * http://sourcefrog.net/projects/natsort/ */ private static int naturalOrderCompare(StringValue a, StringValue b, boolean ignoreCase) { SimpleStringReader aIn = new SimpleStringReader(a); SimpleStringReader bIn = new SimpleStringReader(b); int aChar = aIn.read(); int bChar = bIn.read(); if (aChar == -1 && bChar >= 0) return -1; else if (aChar >= 0 && bChar == -1) return 1; while (true) { while (Character.isWhitespace(aChar)) { aChar = aIn.read(); } while (Character.isWhitespace(bChar)) { bChar = bIn.read(); } if (aChar == -1 && bChar == -1) { return 0; } // leading zeros // '01' < '2' // '0a' > 'a' if (aChar == '0' && bChar == '0') { while (true) { aChar = aIn.read(); bChar = bIn.read(); if (aChar == '0' && bChar == '0') { continue; } else if (aChar == '0') { if ('1' <= bChar && bChar <= '9') return -1; else return 1; } else if (bChar == 0) { if ('1' <= aChar && aChar <= '9') return 1; else return -1; } else { break; } } } else if ('0' < aChar && aChar <= '9' && '0' < bChar && bChar <= '9') { int aInteger = aIn.readInt(aChar); int bInteger = bIn.readInt(bChar); if (aInteger > bInteger) return 1; else if (aInteger < bInteger) return -1; else { aChar = aIn.read(); bChar = bIn.read(); } } if (ignoreCase) { aChar = Character.toUpperCase(aChar); bChar = Character.toUpperCase(bChar); } if (aChar > bChar) return 1; else if (aChar < bChar) return -1; aChar = aIn.read(); bChar = bIn.read(); // trailing spaces // "abc " > "abc" if (aChar >= 0 && bChar == -1) return 1; else if (aChar == -1 && bChar >= 0) return -1; } } /** * Case-insensitive comparison * * @param a left value * @param b right value * @return -1, 0, or 1 */ public static Value strncasecmp( Env env, StringValue a, StringValue b, int length) { if (length < 0) { env.warning(L.l("strncasecmp() length '{0}' must be non-negative", length)); return BooleanValue.FALSE; } int aLen = a.length(); int bLen = b.length(); for (int i = 0; i < length; i++) { if (aLen <= i) return LongValue.MINUS_ONE; else if (bLen <= i) return LongValue.ONE; char aChar = Character.toUpperCase(a.charAt(i)); char bChar = Character.toUpperCase(b.charAt(i)); if (aChar < bChar) return LongValue.MINUS_ONE; else if (bChar < aChar) return LongValue.ONE; } return LongValue.ZERO; } /** * Case-sensitive comparison * * @param a left value * @param b right value * @return -1, 0, or 1 */ public static Value strncmp(Env env, StringValue a, StringValue b, int length) { if (length < 0) { env.warning(L.l("strncmp() length '{0}' must be non-negative", length)); return BooleanValue.FALSE; } if (length < a.length()) a = a.substring(0, length); if (length < b.length()) b = b.substring(0, length); return LongValue.create(strcmp(a, b)); } /** * Returns a substring of <i>haystack</i> starting from the earliest * occurence of any char in <i>charList</i> * * @param haystack the string to search in * @param charList list of chars that would trigger match * @return substring, else FALSE */ public static Value strpbrk(StringValue haystack, StringValue charList) { int len = haystack.length(); int sublen = charList.length(); for (int i = 0; i < len; i++) { for (int j = 0; j < sublen; j++) { if (haystack.charAt(i) == charList.charAt(j)) return haystack.substring(i); } } return BooleanValue.FALSE; } /** * Returns the position of a substring. * * @param haystack the string to search in * @param needleV the string to search for */ public static Value strpos(Env env, StringValue haystack, Value needleV, @Optional int offset) { StringValue needle; if (offset > haystack.length()) { env.warning(L.l("offset cannot exceed string length")); return BooleanValue.FALSE; } if (needleV.isString()) needle = needleV.toStringValue(); else needle = StringValue.create((char) needleV.toInt()); int pos = haystack.indexOf(needle, offset); if (pos < 0) return BooleanValue.FALSE; else return LongValue.create(pos); } /** * Finds the last instance of a substring * * @param haystack the string to search in * @param needleV the string to search for * @return the trailing match or FALSE */ public static Value strrchr(StringValue haystack, Value needleV) { CharSequence needle; if (needleV instanceof StringValue) needle = (StringValue) needleV; else needle = String.valueOf((char) needleV.toLong()); int i = haystack.lastIndexOf(needle); if (i > 0) return haystack.substring(i); else return BooleanValue.FALSE; } /** * Reverses a string. * */ public static Value strrev(StringValue string) { StringValue sb = string.createStringBuilder(string.length()); for (int i = string.length() - 1; i >= 0; i--) { sb.append(string.charAt(i)); } return sb; } /** * Returns the position of a substring, testing case-insensitive. * * @param haystack the full string to test * @param needleV the substring string to test * @param offsetV the optional offset to start searching */ public static Value strripos(Env env, String haystack, Value needleV, @Optional Value offsetV) { if (haystack == null) haystack = ""; String needle; if (needleV instanceof StringValue) needle = needleV.toString(); else needle = String.valueOf((char) needleV.toInt()); int offset; if (offsetV instanceof DefaultValue) offset = haystack.length(); else { offset = offsetV.toInt(); if (haystack.length() < offset) { env.warning(L.l("offset cannot exceed string length")); return BooleanValue.FALSE; } } haystack = haystack.toLowerCase(Locale.ENGLISH); needle = needle.toLowerCase(Locale.ENGLISH); int pos = haystack.lastIndexOf(needle, offset); if (pos < 0) return BooleanValue.FALSE; else return LongValue.create(pos); } /** * Returns the position of a substring. * * @param haystack the string to search in * @param needleV the string to search for */ public static Value strrpos(Env env, StringValue haystack, Value needleV, @Optional Value offsetV) { StringValue needle; if (needleV instanceof StringValue) needle = needleV.toStringValue(); else needle = StringValue.create((char) needleV.toInt()); int offset = haystack.length() - offsetV.toInt(); if (offset < 0) { env.warning(L.l("offset cannot exceed string length")); return BooleanValue.FALSE; } int pos = haystack.lastIndexOf(needle, offset); if (pos < 0) return BooleanValue.FALSE; else return LongValue.create(pos); } /** * Finds the number of initial characters in <i>string</i> that match one of * the characters in <i>characters</i> * * @param string the string to search in * @param characters the character set * @param offset the starting offset * @param length the length * * @return the length of the match or FALSE * if the offset or length are invalid */ public static Value strspn(StringValue string, StringValue characters, @Optional int offset, @Optional("-2147483648") int length) { return strspnImpl(string, characters, offset, length, true); } private static Value strspnImpl(StringValue string, StringValue characters, int offset, int length, boolean isMatch) { int strlen = string.length(); // see also strcspn which uses the same procedure for determining // effective offset and length if (offset < 0) { offset += strlen; if (offset < 0) offset = 0; } if (offset > strlen) return BooleanValue.FALSE; if (length == -2147483648) length = strlen; else if (length < 0) { length += (strlen - offset); if (length < 0) length = 0; } int end = offset + length; if (strlen < end) end = strlen; int count = 0; for (; offset < end; offset++) { char ch = string.charAt(offset); boolean isPresent = characters.indexOf(ch) > -1; if (isPresent == isMatch) count++; else return LongValue.create(count); } return LongValue.create(count); } /** * Finds the first instance of a needle in haystack and returns * the portion of haystack from the beginning of * needle to the end of haystack. * * @param env the calling environment * @param haystackV the string to search in * @param needleV the string to search for, or the * original value of a character * @return the trailing match or FALSE if needle is not found */ public static Value strstr(Env env, StringValue haystackV, Value needleV) { if (haystackV == null) haystackV = env.getEmptyString(); String needle; if (needleV instanceof StringValue) { needle = needleV.toString(); } else { needle = String.valueOf((char) needleV.toLong()); } if (needle.length() == 0) { env.warning("empty needle"); return BooleanValue.FALSE; } int i = haystackV.indexOf(needle); if (i >= 0) return haystackV.substring(i); else return BooleanValue.FALSE; } /** * Split a string into tokens using any character * in another string as a delimiter. * * The first call establishes the string to * search and the characters to use as tokens, * the first token is returned: * <pre> * strtok("hello, world", ", ") * => "hello" * </pre> * * Subsequent calls pass only the token * characters, the next token is returned: * <pre> * strtok("hello, world", ", ") * => "hello" * strtok(", ") * => "world" * </pre> * * False is returned if there are no more tokens: * <pre> * strtok("hello, world", ", ") * => "hello" * strtok(", ") * => "world" * strtok(", ") * => false * </pre> * * Calls that pass two arguments reset the search string: * <pre> * strtok("hello, world", ", ") * => "hello" * strtok("goodbye, world", ", ") * => "goodbye" * strtok("world") * => false * strtok(", ") * => false * </pre> */ public static Value strtok(Env env, StringValue string1, @Optional Value string2) { StringValue string; StringValue characters; int offset; //StringValue savedToken = null; if (string2.isNull()) { StringValue savedString = (StringValue) env .getSpecialValue("caucho.strtok_string"); Integer savedOffset = (Integer) env .getSpecialValue("caucho.strtok_offset"); //savedToken = (StringValue) env.getSpecialValue("caucho.strtok_token"); string = savedString == null ? env.getEmptyString() : savedString; offset = savedOffset == null ? 0 : savedOffset; //savedToken = savedToken == null ? env.getEmptyString() : savedToken; characters = string1; } else { string = string1; offset = 0; characters = string2.toStringValue(); env.setSpecialValue("caucho.strtok_string", string); //env.setSpecialValue("caucho.strtok_token", string2); } int strlen = string.length(); // skip any at beginning for (; offset < strlen; offset++) { char ch = string.charAt(offset); if (characters.indexOf(ch) < 0) break; } Value result; if (offset == strlen) result = BooleanValue.FALSE; else { //if (string2.isNull() && !(string1.eq(savedToken))) { // offset = offset + savedToken.length(); //} int start = offset; int end = start; // find end for (; end < strlen; end++) { char ch = string.charAt(end); if (characters.indexOf(ch) > -1) break; } for (offset = end; offset < strlen; offset++) { char ch = string.charAt(offset); if (characters.indexOf(ch) < 0) break; } result = string.substring(start, end); } env.setSpecialValue("caucho.strtok_offset", offset); return result; } /** * Converts to lower case. * * @param string the input string */ public static StringValue strtolower(StringValue string) { return string.toLowerCase(); } /** * Converts to upper case. * * @param string the input string */ public static StringValue strtoupper(StringValue string) { return string.toUpperCase(); } /** * Translates characters in a string to target values. * * @param string the source string * @param fromV the from characters * @param to the to character map */ public static StringValue strtr(Env env, StringValue string, Value fromV, @Optional StringValue to) { if (fromV instanceof ArrayValue) return strtrArray(string, (ArrayValue) fromV); StringValue from = fromV.toStringValue(); int len = from.length(); if (to.length() < len) len = to.length(); char []map = new char[256]; for (int i = len - 1; i >= 0; i--) map[from.charAt(i)] = to.charAt(i); StringValue sb = string.createStringBuilder(); len = string.length(); for (int i = 0; i < len; i++) { char ch = string.charAt(i); if (map[ch] != 0) sb.append(map[ch]); else sb.append(ch); } return sb; } /** * Translates characters in a string to target values. * * @param string the source string * @param map the character map */ private static StringValue strtrArray(StringValue string, ArrayValue map) { int size = map.getSize(); StringValue []fromList = new StringValue[size]; StringValue []toList = new StringValue[size]; Map.Entry<Value,Value> [] entryArray = new Map.Entry[size]; int i = 0; for (Map.Entry<Value,Value> entry : map.entrySet()) { entryArray[i++] = entry; } // sort entries in descending fashion Arrays.sort(entryArray, new StrtrComparator<Map.Entry<Value,Value>>()); boolean []charSet = new boolean[256]; for (i = 0; i < size; i++) { fromList[i] = entryArray[i].getKey().toStringValue(); toList[i] = entryArray[i].getValue().toStringValue(); charSet[fromList[i].charAt(0)] = true; } StringValue result = string.createStringBuilder(); int len = string.length(); int head = 0; top: while (head < len) { char ch = string.charAt(head); if (charSet.length <= ch || charSet[ch]) { fromLoop: for (i = 0; i < fromList.length; i++) { StringValue from = fromList[i]; int fromLen = from.length(); if (head + fromLen > len) continue; if (ch != from.charAt(0)) continue; for (int j = 0; j < fromLen; j++) { if (string.charAt(head + j) != from.charAt(j)) continue fromLoop; } result = result.append(toList[i]); head = head + fromLen; continue top; } } result.append(ch); head++; } return result; } /* * Comparator for sorting in descending fashion based on length. */ static class StrtrComparator<T extends Map.Entry<Value,Value>> implements Comparator<T> { public int compare(T a, T b) { int lenA = a.getKey().length(); int lenB = b.getKey().length(); if (lenA < lenB) return 1; else if (lenA == lenB) return 0; else return -1; } } /** * Returns a substring * * @param env the calling environment * @param string the string * @param start the start offset * @param lenV the optional length */ public static Value substr(Env env, StringValue string, int start, @Optional Value lenV) { int len = lenV.toInt(); int strLen = string.length(); if (start < 0) start = strLen + start; if (start < 0 || start >= strLen) return BooleanValue.FALSE; if (lenV.isDefault()) return string.substring(start); else if (len == 0) return string.EMPTY; else { int end; if (len < 0) end = strLen + len; else end = (strLen < len) ? strLen : start + len; if (end <= start) return BooleanValue.FALSE; else if (strLen <= end) return string.substring(start); else return string.substring(start, end); } } public static Value substr_compare(Env env, StringValue mainStr, StringValue str, int offset, @Optional Value lenV, @Optional boolean isCaseInsensitive) { int strLen = mainStr.length(); int len = lenV.toInt(); if (! lenV.isDefault() && len == 0) return BooleanValue.FALSE; if (strLen < offset) { env.warning(L.l("offset can not be greater than length of string")); return BooleanValue.FALSE; } if (len > strLen || len + offset > strLen) { return BooleanValue.FALSE; } mainStr = substr(env, mainStr, offset, lenV).toStringValue(); str = substr(env, str, 0, lenV).toStringValue(); if (isCaseInsensitive) return LongValue.create(strcasecmp(mainStr, str)); else return LongValue.create(strcmp(mainStr, str)); } public static Value substr_count(Env env, StringValue haystackV, StringValue needleV, @Optional int offset, @Optional("-1") int length) { String haystack = haystackV.toString(); String needle = needleV.toString(); if (needle.length() == 0) { env.warning(L.l("empty substr")); return BooleanValue.FALSE; } int haystackLength = haystack.length(); if (offset < 0 || offset > haystackLength) { env.warning(L.l("offset cannot exceed string length", offset)); return BooleanValue.FALSE; } if (length >= 0) { int newLength = offset + length; if (newLength < 0 || newLength > haystackLength) { env.warning(L.l("length cannot exceed string length", length)); return BooleanValue.FALSE; } haystackLength = newLength; } int needleLength = needle.length(); int count = 0; int end = haystackLength - needleLength + 1; for (int i = offset; i < end; i++) { if (haystack.startsWith(needle, i)) { count++; i += needleLength; } } return LongValue.create(count); } /** * Replaces a substring with a replacement * * @param subjectV a string to modify, or an array of strings to modify * @param replacement the replacement string * @param startV the start offset * @param lengthV the optional length */ public static Value substr_replace(Value subjectV, StringValue replacement, Value startV, @Optional Value lengthV) { int start = 0; int length = Integer.MAX_VALUE / 2; if (!(lengthV.isNull() || lengthV.isArray())) length = lengthV.toInt(); if (!(startV.isNull() || startV.isArray())) start = startV.toInt(); Iterator<Value> startIterator = startV.isArray() ? ((ArrayValue) startV).values().iterator() : null; Iterator<Value> lengthIterator = lengthV.isArray() ? ((ArrayValue) lengthV).values().iterator() : null; if (subjectV.isArray()) { ArrayValue resultArray = new ArrayValueImpl(); ArrayValue subjectArray = (ArrayValue) subjectV; for (Value value : subjectArray.values()) { if (lengthIterator != null && lengthIterator.hasNext()) length = lengthIterator.next().toInt(); if (startIterator != null && startIterator.hasNext()) start = startIterator.next().toInt(); Value result = substrReplaceImpl( value.toStringValue(), replacement, start, length); resultArray.append(result); } return resultArray; } else { if (lengthIterator != null && lengthIterator.hasNext()) length = lengthIterator.next().toInt(); if (startIterator != null && startIterator.hasNext()) start = startIterator.next().toInt(); return substrReplaceImpl( subjectV.toStringValue(), replacement, start, length); } } private static Value substrReplaceImpl(StringValue string, StringValue replacement, int start, int len) { int strLen = string.length(); if (start > strLen) start = strLen; else if (start < 0) start = Math.max(strLen + start, 0); int end; if (len < 0) end = Math.max(strLen + len, start); else { end = (strLen < len) ? strLen : (start + len); } StringValue result = string.createStringBuilder(); result = result.append(string.substring(0, start)); result = result.append(replacement); result = result.append(string.substring(end)); return result; } /** * Removes leading and trailing whitespace. * * @param string the string to be trimmed * @param characters optional set of characters to trim * @return the trimmed string */ public static Value trim(Env env, StringValue string, @Optional String characters) { boolean []trim; if (characters == null || characters.equals("")) trim = TRIM_WHITESPACE; else trim = parseCharsetBitmap(env, characters.toString()); int len = string.length(); int head = 0; for (; head < len; head++) { char ch = string.charAt(head); if (ch >= 256 || ! trim[ch]) { break; } } int tail = len - 1; for (; tail >= 0; tail--) { char ch = string.charAt(tail); if (ch >= 256 || ! trim[ch]) { break; } } if (tail < head) return env.getEmptyString(); else { return (StringValue) string.subSequence(head, tail + 1); } } /** * Uppercases the first character * * @param string the input string */ public static StringValue ucfirst(Env env, StringValue string) { if (string == null) return env.getEmptyString(); else if (string.length() == 0) return string; StringValue sb = string.createStringBuilder(); sb = sb.append(Character.toUpperCase(string.charAt(0))); sb = sb.append(string, 1, string.length()); return sb; } /** * Uppercases the first character of each word * * @param string the input string */ public static String ucwords(String string) { if (string == null) string = ""; int strLen = string.length(); boolean isStart = true; StringBuilder sb = new StringBuilder(); for (int i = 0; i < strLen; i++) { char ch = string.charAt(i); switch (ch) { case ' ': case '\t': case '\r': case '\n': isStart = true; sb.append(ch); break; default: if (isStart) sb.append(Character.toUpperCase(ch)); else sb.append(ch); isStart = false; break; } } return sb.toString(); } /** * Formatted strings with array arguments * * @param format the format string * @param array the arguments to apply to the format string */ public static int vprintf(Env env, StringValue format, @NotNull ArrayValue array) { Value []args; if (array != null) { args = new Value[array.getSize()]; int i = 0; for (Value value : array.values()) args[i++] = value; } else args = new Value[0]; return printf(env, format, args); } /** * Formatted strings with array arguments * * @param format the format string * @param array the arguments to apply to the format string */ public static Value vsprintf(Env env, StringValue format, @NotNull ArrayValue array) { Value []args; if (array != null) { args = new Value[array.getSize()]; int i = 0; for (Value value : array.values()) args[i++] = value; } else args = new Value[0]; return sprintf(env, format, args); } /** * Wraps a string to the given number of characters. * * @param string the input string * @param width the width * @param breakString the break string * @param cut if true, break on exact match */ public static Value wordwrap(Env env, @Expect(type = Expect.Type.STRING) Value value, @Optional @Expect(type = Expect.Type.NUMERIC) Value widthV, @Optional @Expect(type = Expect.Type.STRING) Value breakV, @Optional @Expect(type = Expect.Type.BOOLEAN) Value cutV) { if (value instanceof UnexpectedValue) { env.warning(L.l("word must be a string, but {0} given", value.getType())); return NullValue.NULL; } if (widthV instanceof UnexpectedValue) { env.warning(L.l("width must be numeric, but {0} given", widthV.getType())); return NullValue.NULL; } int width = 0; if (widthV.isDefault()) width = 75; else width = widthV.toInt(); String string = value.toString(); if (cutV instanceof UnexpectedValue) { env.warning(L.l("cut must be a boolean, but {0} given", cutV.getType())); return NullValue.NULL; } boolean isCut = cutV.toBoolean(); if (isCut && width == 0 && string.length() > 0) { env.warning(L.l("cannot cut string to width 0")); return BooleanValue.FALSE; } int len = string != null ? string.length() : 0; if (breakV instanceof UnexpectedValue) { env.warning(L.l("break string must be a string, but {0} given", breakV.getType())); return NullValue.NULL; } String breakString = "\n"; if (! breakV.isDefault()) breakString = breakV.toString(); if (breakString == null || breakString.length() == 0) { env.warning(L.l("break string cannot be empty")); return BooleanValue.FALSE; } int breakLen = breakString.length(); int breakChar; if (breakLen == 0) breakChar = -1; else breakChar = breakString.charAt(0); int head = 0; int lastSpace = 0; StringValue sb = env.createStringBuilder(); for (int i = 0; i < len; i++) { char ch = string.charAt(i); if (ch == breakChar && string.regionMatches( i, breakString, 0, breakLen)) { sb.append(string, head, i + breakLen); head = i + breakLen; } else if (width <= i - head) { if (ch == ' ') { sb.append(string, head, i); sb.append(breakString); head = i + 1; } else if (head < lastSpace) { sb.append(string, head, lastSpace); sb.append(breakString); head = lastSpace + 1; } else if (isCut) { sb.append(string, head, i); sb.append(breakString); head = i; } } else if (ch == ' ') { lastSpace = i; } } if (head < len) { sb.append(string, head, len); } return sb; } /** * Returns true if the character is a whitespace character. */ protected static boolean isWhitespace(char ch) { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; } /** * Returns the uppercase equivalent of the caharacter */ protected static char toUpperCase(char ch) { if (ch >= 'a' && ch <= 'z') return (char) ('A' + (ch - 'a')); else return ch; } /** * Converts an integer digit to a uuencoded char. */ protected static char toUUChar(int d) { if (d == 0) return (char) 0x60; else return (char) (0x20 + (d & 0x3f)); } protected static char toHexChar(int d) { d &= 0xf; if (d < 10) return (char) (d + '0'); else return (char) (d - 10 + 'a'); } protected static char toUpperHexChar(int d) { d &= 0xf; if (d < 10) return (char) (d + '0'); else return (char) (d - 10 + 'A'); } protected static int hexToDigit(char ch) { if ('0' <= ch && ch <= '9') return ch - '0'; else if ('a' <= ch && ch <= 'f') return ch - 'a' + 10; else if ('A' <= ch && ch <= 'F') return ch - 'A' + 10; else return -1; } protected static int octToDigit(char ch) { if ('0' <= ch && ch <= '7') return ch - '0'; else return -1; } abstract static class PrintfSegment { abstract public boolean apply(Env env, StringValue sb, Value []args); static boolean hasIndex(String format) { return format.indexOf('$') >= 0; } static int getIndex(String format) { int value = 0; for (int i = 0; i < format.length(); i++) { char ch; if ('0' <= (ch = format.charAt(i)) && ch <= '9') value = 10 * value + ch - '0'; else break; } return value - 1; } static String getIndexFormat(String format) { int p = format.indexOf('$'); return '%' + format.substring(p + 1); } } static class TextPrintfSegment extends PrintfSegment { private final char []_text; TextPrintfSegment(StringBuilder text) { _text = new char[text.length()]; text.getChars(0, _text.length, _text, 0); } @Override public boolean apply(Env env, StringValue sb, Value []args) { sb.append(_text, 0, _text.length); return true; } } static class LongPrintfSegment extends PrintfSegment { private final String _format; private final int _index; private final QuercusLocale _locale; private LongPrintfSegment(String format, int index, QuercusLocale locale) { _format = format; _index = index; _locale = locale; } static PrintfSegment create(Env env, String format, int index) { if (hasIndex(format)) { index = getIndex(format); format = getIndexFormat(format); } else { format = '%' + format; //index = index; } // php/115b // strip out illegal precision specifier from phpBB vote function if (format.length() > 1 && format.charAt(1) == '.') { int i; for (i = 2; i < format.length(); i++) { char ch = format.charAt(i); if (! ('0' <= ch && ch <= '9')) break; } format = '%' + format.substring(i); } if (format.charAt(format.length() - 1) == 'x' || format.charAt(format.length() - 1) == 'X') { HexPrintfSegment hex = HexPrintfSegment.create(format, index); if (hex != null) return hex; } if (format.charAt(format.length() - 1) == 'b' || format.charAt(format.length() - 1) == 'B') { BinaryPrintfSegment bin = BinaryPrintfSegment.create(format, index); if (bin != null) return bin; } if (format.charAt(format.length() - 1) == 'u') { UnsignedPrintfSegment unsign = UnsignedPrintfSegment.create(format, index); if (unsign != null) return unsign; } return new LongPrintfSegment(format, index, env.getLocaleInfo().getNumeric()); } @Override public boolean apply(Env env, StringValue sb, Value []args) { long value; if (_index < args.length) value = args[_index].toLong(); else { env.warning(L.l("printf(): not enough arguments to match format.")); return false; } sb.append(String.format(_locale.getLocale(), _format, value)); return true; } } static class HexPrintfSegment extends PrintfSegment { private final int _index; private final int _min; private final char _pad; private boolean _isUpper; HexPrintfSegment(int index, int min, int pad, boolean isUpper) { _index = index; _min = min; if (pad >= 0) _pad = (char) pad; else _pad = ' '; _isUpper = isUpper; } static HexPrintfSegment create(String format, int index) { int length = format.length(); int offset = 1; boolean isUpper = format.charAt(length - 1) == 'X'; char pad = ' '; if (format.charAt(offset) == ' ') { pad = ' '; offset++; } else if (format.charAt(offset) == '0') { pad = '0'; offset++; } int min = 0; for (; offset < length - 1; offset++) { char ch = format.charAt(offset); if ('0' <= ch && ch <= '9') min = 10 * min + ch - '0'; else return null; } return new HexPrintfSegment(index, min, pad, isUpper); } @Override public boolean apply(Env env, StringValue sb, Value []args) { long value; if (_index >= 0 && _index < args.length) value = args[_index].toLong(); else { env.warning(L.l("printf(): not enough arguments to match format.")); return false; } int digits = 0; long shift = value; for (int i = 0; i < 16; i++) { if (shift != 0) digits = i; shift = shift >>> 4; } for (int i = digits + 1; i < _min; i++) sb.append(_pad); for (; digits >= 0; digits--) { int digit = (int) (value >>> (4 * digits)) & 0xf; if (digit <= 9) sb.append((char) ('0' + digit)); else if (_isUpper) sb.append((char) ('A' + digit - 10)); else sb.append((char) ('a' + digit - 10)); } return true; } } static class UnsignedPrintfSegment extends PrintfSegment { private final int _index; private final int _min; private final char _pad; UnsignedPrintfSegment(int index, int min, int pad) { _index = index; _min = min; if (pad >= 0) _pad = (char) pad; else _pad = ' '; } static UnsignedPrintfSegment create(String format, int index) { int length = format.length(); int offset = 1; if (format.charAt(offset) == '+') offset++; char pad = ' '; if (format.charAt(offset) == ' ') { pad = ' '; offset++; } else if (format.charAt(offset) == '0') { pad = '0'; offset++; } int min = 0; for (; offset < length - 1; offset++) { char ch = format.charAt(offset); if ('0' <= ch && ch <= '9') min = 10 * min + ch - '0'; else return null; } return new UnsignedPrintfSegment(index, min, pad); } @Override public boolean apply(Env env, StringValue sb, Value []args) { long value; if (_index >= 0 && _index < args.length) value = args[_index].toLong(); else { env.warning(L.l("printf(): not enough arguments to match format.")); return false; } char []buf = new char[32]; int digits = buf.length; if (value == 0) { buf[--digits] = '0'; } else if (value > 0) { while (value != 0) { int digit = (int) (value % 10); buf[--digits] = (char) ('0' + digit); value = value / 10; } } else { BigInteger bigInt = new BigInteger(String.valueOf(value)); bigInt = bigInt.add(BIG_2_64); while (bigInt.compareTo(BigInteger.ZERO) != 0) { int digit = bigInt.mod(BIG_TEN).intValue(); buf[--digits] = (char) ('0' + digit); bigInt = bigInt.divide(BIG_TEN); } } for (int i = buf.length - digits; i < _min; i++) sb.append(_pad); for (; digits < buf.length; digits++) { sb.append(buf[digits]); } return true; } } static class BinaryPrintfSegment extends PrintfSegment { private final int _index; private final int _min; private final char _pad; BinaryPrintfSegment(int index, int min, int pad) { _index = index; _min = min; if (pad >= 0) _pad = (char) pad; else _pad = ' '; } static BinaryPrintfSegment create(String format, int index) { int length = format.length(); int offset = 1; char pad = ' '; if (format.charAt(offset) == ' ') { pad = ' '; offset++; } else if (format.charAt(offset) == '0') { pad = '0'; offset++; } int min = 0; for (; offset < length - 1; offset++) { char ch = format.charAt(offset); if ('0' <= ch && ch <= '9') min = 10 * min + ch - '0'; else return null; } return new BinaryPrintfSegment(index, min, pad); } @Override public boolean apply(Env env, StringValue sb, Value []args) { long value; if (_index >= 0 && _index < args.length) value = args[_index].toLong(); else { env.warning(L.l("printf(): not enough arguments to match format.")); return false; } int digits = 0; long shift = value; for (int i = 0; i < 64; i++) { if (shift != 0) digits = i; shift = shift >>> 1; } for (int i = digits + 1; i < _min; i++) sb.append(_pad); for (; digits >= 0; digits--) { int digit = (int) (value >>> (digits)) & 0x1; sb.append((char) ('0' + digit)); } return true; } } static class DoublePrintfSegment extends PrintfSegment { private final String _format; private final boolean _isLeftZero; private final int _index; private final QuercusLocale _locale; DoublePrintfSegment(String format, boolean isLeftZero, int index, QuercusLocale locale) { if (hasIndex(format)) { _index = getIndex(format); _format = getIndexFormat(format); } else { _format = '%' + format; _index = index; } _isLeftZero = isLeftZero; _locale = locale; } @Override public boolean apply(Env env, StringValue sb, Value []args) { double value; if (_index < args.length) value = args[_index].toDouble(); else { env.warning(L.l("printf(): not enough arguments to match format.")); return false; } String s; if (_locale == null) s = String.format(_format, value); else s = String.format(_locale.getLocale(), _format, value); if (_isLeftZero) { int len = s.length(); // php/1174 "-0" not allowed by java formatter for (int i = 0; i < len; i++) { char ch = s.charAt(i); if (ch == ' ') sb.append('0'); else sb.append(ch); } } else { sb.append(s); } return true; } } static class StringPrintfSegment extends PrintfSegment { protected final char []_prefix; protected final int _min; protected final int _max; protected final boolean _isLeft; protected final boolean _isUpper; protected final char _pad; protected final int _index; StringPrintfSegment(StringBuilder prefix, boolean isLeft, int pad, boolean isUpper, int width, String format, int index) { _prefix = new char[prefix.length()]; _isLeft = isLeft; _isUpper = isUpper; if (pad >= 0) _pad = (char) pad; else _pad = ' '; prefix.getChars(0, _prefix.length, _prefix, 0); if (hasIndex(format)) { index = getIndex(format); format = getIndexFormat(format); } int i = 0; int len = format.length(); int min = width; int max = Integer.MAX_VALUE; char ch = ' '; /* for (; i < len && '0' <= (ch = format.charAt(i)) && ch <= '9'; i++) { min = 10 * min + ch - '0'; } */ if (0 < len && format.charAt(0) == '.') { max = 0; for (i++; i < len && '0' <= (ch = format.charAt(i)) && ch <= '9'; i++) { max = 10 * max + ch - '0'; } } _min = min; _max = max; _index = index; } protected String toValue(Value []args) { return args[_index].toString(); } @Override public boolean apply(Env env, StringValue sb, Value []args) { sb.append(_prefix, 0, _prefix.length); String value; if (_index < args.length) value = toValue(args); else { env.warning(L.l("printf(): not enough arguments to match format.")); return false; } int len = value.length(); if (_max < len) { value = value.substring(0, _max); len = _max; } if (_isUpper) value = value.toUpperCase(Locale.ENGLISH); if (! _isLeft) { for (int i = len; i < _min; i++) { sb.append(_pad); } } sb.append(value); if (_isLeft) { for (int i = len; i < _min; i++) { sb.append(_pad); } } return true; } } static class CharPrintfSegment extends StringPrintfSegment { CharPrintfSegment(StringBuilder prefix, boolean isLeft, int pad, boolean isUpper, int width, String format, int index) { super(prefix, isLeft, pad, isUpper, width, format, index); } @Override protected String toValue(Value []args) { if (args.length <= _index) return ""; Value v = args[_index]; if (v.isLongConvertible()) return String.valueOf((char) v.toLong()); else return v.charValueAt(0).toString(); } } static class SimpleStringReader { StringValue _str; int _length; int _index; SimpleStringReader(StringValue str) { _str = str; _length = str.length(); _index = 0; } int read() { if (_index < _length) return _str.charAt(_index++); else return -1; } int peek() { if (_index < _length) return _str.charAt(_index); else return -1; } int readInt(int currChar) { int number = currChar - '0'; while (true) { currChar = peek(); if ('0' <= currChar && currChar <= '9') { number = number * 10 + currChar - '0'; _index++; } else { break; } } return number; } } // sscanf abstract static class ScanfSegment { abstract public boolean isAssigned(); abstract public int apply(StringValue string, int strlen, int sIndex, Value var, boolean isReturnArray); void sscanfPut(Value var, Value val, boolean isReturnArray) { if (isReturnArray) var.put(val); else var.set(val); } } static class ScanfConstant extends ScanfSegment { private final String _string; private final int _strlen; private ScanfConstant(String string) { _string = string; _strlen = string.length(); } @Override public boolean isAssigned() { return false; } @Override public int apply(StringValue string, int strlen, int sIndex, Value var, boolean isReturnArray) { int fStrlen = _strlen; String fString = _string; if (strlen - sIndex < fStrlen) return -1; for (int i = 0; i < fStrlen; i++) { if (string.charAt(sIndex++) != fString.charAt(i)) return -1; } return sIndex; } } static class ScanfWhitespace extends ScanfSegment { static final ScanfWhitespace SEGMENT = new ScanfWhitespace(); private ScanfWhitespace() { } @Override public boolean isAssigned() { return false; } @Override public int apply(StringValue string, int strlen, int sIndex, Value var, boolean isReturnArray) { for (; sIndex < strlen && isWhitespace(string.charAt(sIndex)); sIndex++) { } return sIndex; } } static class ScanfStringLength extends ScanfSegment { static final ScanfStringLength SEGMENT = new ScanfStringLength(); private ScanfStringLength() { } @Override public boolean isAssigned() { return true; } @Override public int apply(StringValue string, int strlen, int sIndex, Value var, boolean isReturnArray) { sscanfPut(var, LongValue.create(sIndex), isReturnArray); return sIndex; } } static class ScanfSet extends ScanfSegment { private IntSet _set; private ScanfSet(IntSet set) { _set = set; } @Override public boolean isAssigned() { return true; } @Override public int apply(StringValue string, int strlen, int sIndex, Value var, boolean isReturnArray) { StringValue sb = string.createStringBuilder(); for (; sIndex < strlen; sIndex++) { char ch = string.charAt(sIndex); if (_set.contains(ch)) { sb.append(ch); } else { break; } } if (sb.length() > 0) sscanfPut(var, sb, isReturnArray); else if (isReturnArray) var.put(NullValue.NULL); return sIndex; } } static class ScanfSetNegated extends ScanfSegment { private IntSet _set; private ScanfSetNegated(IntSet set) { _set = set; } @Override public boolean isAssigned() { return true; } @Override public int apply(StringValue string, int strlen, int sIndex, Value var, boolean isReturnArray) { StringValue sb = string.createStringBuilder(); for (; sIndex < strlen; sIndex++) { char ch = string.charAt(sIndex); if (! _set.contains(ch)) { sb.append(ch); } else { break; } } if (sb.length() > 0) sscanfPut(var, sb, isReturnArray); else if (isReturnArray) var.put(NullValue.NULL); return sIndex; } } static class ScanfScientific extends ScanfSegment { private final int _maxLen; ScanfScientific(int maxLen) { if (maxLen < 0) maxLen = Integer.MAX_VALUE; _maxLen = maxLen; } @Override public boolean isAssigned() { return true; } @Override public int apply(StringValue s, int strlen, int i, Value var, boolean isReturnArray) { if (i == strlen) { if (isReturnArray) var.put(NullValue.NULL); return i; } int start = i; int len = strlen; int ch = 0; int maxLen = _maxLen; if (i < len && maxLen > 0 && ((ch = s.charAt(i)) == '+' || ch == '-')) { i++; maxLen--; } for (; i < len && maxLen > 0 && '0' <= (ch = s.charAt(i)) && ch <= '9'; i++) { maxLen--; } if (ch == '.') { maxLen--; for (i++; i < len && maxLen > 0 && '0' <= (ch = s.charAt(i)) && ch <= '9'; i++) { maxLen--; } } if (ch == 'e' || ch == 'E') { maxLen--; int e = i++; if (start == e) { sscanfPut(var, NullValue.NULL, isReturnArray); return start; } if (i < len && maxLen > 0 && (ch = s.charAt(i)) == '+' || ch == '-') { i++; maxLen--; } for (; i < len && maxLen > 0 && '0' <= (ch = s.charAt(i)) && ch <= '9'; i++) { maxLen--; } if (i == e + 1) i = e; } double val; if (i == 0) val = 0; else val = Double.parseDouble(s.substring(start, i).toString()); sscanfPut(var, DoubleValue.create(val), isReturnArray); return i; } } static class ScanfHex extends ScanfSegment { private final int _maxLen;; ScanfHex(int maxLen) { if (maxLen < 0) maxLen = Integer.MAX_VALUE; _maxLen = maxLen; } @Override public boolean isAssigned() { return true; } @Override public int apply(StringValue string, int strlen, int sIndex, Value var, boolean isReturnArray) { if (sIndex == strlen) { if (isReturnArray) var.put(NullValue.NULL); return sIndex; } int val = 0; int sign = 1; boolean isMatched = false; int maxLen = _maxLen; if (sIndex < strlen) { char ch = string.charAt(sIndex); if (ch == '+') { sIndex++; maxLen--; } else if (ch == '-') { sign = -1; sIndex++; maxLen--; } } for (; sIndex < strlen && maxLen-- > 0; sIndex++) { char ch = string.charAt(sIndex); if ('0' <= ch && ch <= '9') { val = val * 16 + ch - '0'; isMatched = true; } else if ('a' <= ch && ch <= 'f') { val = val * 16 + ch - 'a' + 10; isMatched = true; } else if ('A' <= ch && ch <= 'F') { val = val * 16 + ch - 'A' + 10; isMatched = true; } else if (! isMatched) { sscanfPut(var, NullValue.NULL, isReturnArray); return sIndex; } else break; } sscanfPut(var, LongValue.create(val * sign), isReturnArray); return sIndex; } } static class ScanfInteger extends ScanfSegment { private final int _maxLen; private final int _base; private final boolean _isUnsigned; ScanfInteger(int maxLen, int base, boolean isUnsigned) { if (maxLen < 0) maxLen = Integer.MAX_VALUE; _maxLen = maxLen; _base = base; _isUnsigned = isUnsigned; } @Override public boolean isAssigned() { return true; } @Override public int apply(StringValue string, int strlen, int sIndex, Value var, boolean isReturnArray) { if (sIndex == strlen) { if (isReturnArray) var.put(NullValue.NULL); return sIndex; } // XXX: 32-bit vs 64-bit int val = 0; int sign = 1; boolean isNotMatched = true; int maxLen = _maxLen; if (sIndex < strlen) { char ch = string.charAt(sIndex); if (ch == '+') { sIndex++; maxLen--; } else if (ch == '-') { sign = -1; sIndex++; maxLen--; } } int base = _base; int topRange = base + '0'; for (; sIndex < strlen && maxLen-- > 0; sIndex++) { char ch = string.charAt(sIndex); if ('0' <= ch && ch < topRange) { val = val * base + ch - '0'; isNotMatched = false; } else if (isNotMatched) { sscanfPut(var, NullValue.NULL, isReturnArray); return sIndex; } else break; } if (_isUnsigned) { if (sign == -1 && val != 0) sscanfPut( var, StringValue.create(0xffffffffL - val + 1), isReturnArray); else sscanfPut(var, LongValue.create(val), isReturnArray); } else sscanfPut(var, LongValue.create(val * sign), isReturnArray); return sIndex; } } static class ScanfString extends ScanfSegment { private final int _maxLen; ScanfString(int maxLen) { if (maxLen < 0) maxLen = Integer.MAX_VALUE; _maxLen = maxLen; } @Override public boolean isAssigned() { return true; } /** * Scans a string with a given length. */ @Override public int apply(StringValue string, int strlen, int sIndex, Value var, boolean isReturnArray) { if (sIndex == strlen) { if (isReturnArray) var.put(NullValue.NULL); return sIndex; } StringValue sb = string.createStringBuilder(); int maxLen = _maxLen; for (; sIndex < strlen && maxLen-- > 0; sIndex++) { char ch = string.charAt(sIndex); if (isWhitespace(ch)) break; sb.append(ch); } sscanfPut(var, sb, isReturnArray); return sIndex; } } static { DEFAULT_DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(); DEFAULT_DECIMAL_FORMAT_SYMBOLS.setDecimalSeparator('.'); DEFAULT_DECIMAL_FORMAT_SYMBOLS.setGroupingSeparator(','); DEFAULT_DECIMAL_FORMAT_SYMBOLS.setZeroDigit('0'); } }