/********************************************************************************* * TotalCross Software Development Kit * * Copyright (C) 2003-2004 Pierre G. Richard * * Copyright (C) 2003-2012 SuperWaba Ltda. * * All Rights Reserved * * * * This library and virtual machine 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. * * * * This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 * * A copy of this license is located in file license.txt at the root of this * * SDK or can be downloaded here: * * http://www.gnu.org/licenses/lgpl-3.0.txt * * * *********************************************************************************/ package totalcross.net; import totalcross.sys.Vm; /** * An URI represents a Uniform Resource Identifier, or an address of something in the Internet. * * @author Pierre G. Richard */ public class URI { /** The scheme represented in this URI. */ public ByteString scheme; /** The authority represented in this URI. */ public ByteString authority; /** The user information represented in this URI. */ public ByteString userInfo; /** The host represented in this URI. */ public ByteString host; /** The path represented in this URI. */ public URI.Path path; /** The query stored in this URI. */ public ByteString query; /** The fragment stored in this URI. */ public ByteString fragment; /** The port stored in this URI. */ public int port; static private final char[] noEncode = { '\u0000', '\u0000', '\u6401', '\u03ff', '\ufffe', '\u87ff', '\ufffe', '\u07ff' }; static private final char toHexNibble[] = "0123456789ABCDEF".toCharArray(); /** * Constructor from a String * * @param spec String that holds the specified URI */ public URI(String spec) { init(spec.getBytes(), 0, spec.length(), null, false); } /** * Constructor from a String and a base URI * * @param spec String that holds the specified URI * @param baseURI base URI for deriving relative components */ public URI(String spec, URI baseURI) { init(spec.getBytes(), 0, spec.length(), baseURI, false); } /** * Constructor from a byte array. * * @param spec byte array that holds the specified URI * @param start where the URI starts in the byte array * @param len length of the URI */ public URI(byte[] spec, int start, int len) { init(spec, start, len, null, true); } /** * Constructor from a byte array, and a base URI missing components derive from. * * @param spec byte array that holds the specified URI * @param start where the URI starts in the byte array * @param len length of the URI * @param baseURI base URI for deriving relative components */ public URI(byte[] spec, int start, int len, URI baseURI) { init(spec, start, len, baseURI, true); } private byte[] spec; private int start, len, end; private boolean isRelative; /** * Internal method to initialize the URI fields. * * @param _spec byte array that holds the specified URI * @param _start where the URI starts in the byte array * @param _len length of the URI * @param baseURI base URI for deriving relative components * @param meedCopy if the byte array needs to be copied */ private void init(byte[] _spec, int _start, int _len, URI baseURI, boolean needCopy) { int i; start = _start; len = _len; spec = _spec; port = -1; stripURL(); // Copy the byte array if (needCopy) { end -= start; byte[] temp = new byte[end]; Vm.arrayCopy(spec, start, temp, 0, end); spec = temp; start = 0; } // Scheme: $1 in ^(([^:/?#]+):)? (RFC2396, Appendix B) skipPart1(); // Find out if the spec URI is relative. This should just be: | "if ((scheme == null) && (baseURI != null))", | // however, if the scheme of the spec URI matches the scheme of the | base URI, and the base URI is a hierarchical // URI scheme, | then maintain backwards compatibility and treat it as if | the spec didn't contain the scheme; // see 5.2.3 of RFC2396 if ((baseURI != null) && ((scheme == null) || ((scheme.equalsIgnoreCase(baseURI.scheme)) && (baseURI.path != null) && (baseURI.path.len > 0) && (baseURI.path.base[baseURI.path.pos] == '/')))) { scheme = ByteString.copy(baseURI.scheme); authority = ByteString.copy(baseURI.authority); userInfo = ByteString.copy(baseURI.userInfo); host = ByteString.copy(baseURI.host); path = new Path(baseURI.path); port = baseURI.port; isRelative = true; } // Strip off the fragment stripFragment(); if (isPathEmpty(baseURI)) return; // Given the logic above, this section is hit when spec is one of: | - a relative URI (no scheme) starting with // '/' or '?' or something else. | - an absolute URI with '/' or '?' following the scheme | Strip off the query part for (i = start; i < end; ++i) if (spec[i] == '?') { query = new ByteString(i + 1, end - (i + 1), spec); end = i; break; } if (isAbsolute()) return; // Given the logic above, this section is hit when spec is one of: | - a relative URI (no scheme) starting with // '/', | - an absolute URI with '/' following the scheme | It is an absolute path. | Parse the authority part if any parseRelative(); if (start == end) path = new Path(0, 1, new byte[] {'/'}); else path = new Path(start, end - start, spec); } private void parseRelative() { int i; boolean b1 = (start < (end - 1)); boolean b2 = (spec[start + 1] == '/'); if (b1 && b2) { int atSignPos = -1; start += 2; for (i = start; i < end; ++i) { if (spec[i] == '/') break; if ((spec[i] == '@') && (atSignPos == -1)) atSignPos = i; } authority = new ByteString(start, i - start, spec); if (atSignPos != -1) { userInfo = new ByteString(authority.pos, atSignPos - start, spec); host = new ByteString(atSignPos + 1, i - (atSignPos + 1), spec); } else { host = ByteString.copy(authority); } start = i; /// I should check that the host.len is not zero. // Strip off the port, if any port = -1; // default value if (host.len > 0) { i = 0; if (host.base[host.pos] == '[') { // If the host is surrounded by [ and ] | then it is an IPv6 literal address as specified in RFC2732. i = host.indexOf((byte) ']', 0); if ((i < 0) || ((++i < host.len) && (host.base[host.pos + i] != ':'))) i = -1; } else i = host.indexOf((byte) ':', 0); if (i >= 0) { // a colon was found int ppos = host.pos + i; int epos = host.pos + host.len; if (++ppos < epos) { // port can be omitted port = ByteString.convertToInt(host.base, ppos, epos); } host.len = i; } } } } private boolean isAbsolute() { int i; if ((end == start) || (spec[start] != '/')) { // Not a slash. | The spec is either an absolute URL with no path (just a query), | or the spec is a relative // URI. | Take care of the former. | Then, resolve a relative-path according to RFC2396, 5.2.6. | Try to remove // the last segment from the Base URI path | and recompose as said in RFC2396, 5.2.6a and 5.2.6b. | If the Base // URI path has only one segment (no '/'), | a leading slash is needed to end the authority. | If the Base URI // has no authority, then it is useless. if (!isRelative) return true; else { byte[] buffer = null; if ((path != null) && ((i = path.lastIndexOf((byte) '/')) >= 0)) { // inherited path buffer = new byte[(i + 1) + (end - start)]; Vm.arrayCopy(path.base, path.pos, buffer, 0, i + 1); Vm.arrayCopy(spec, start, buffer, i + 1, end - start); } else if (authority != null) { // then inherit authority buffer = new byte[1 + (end - start)]; buffer[0] = (byte) '/'; // insert a slash. Vm.arrayCopy(spec, start, buffer, 1, end - start); } else { // no slash and no (inherited) authority (opaque?) path = new Path(start, end - start, spec); return true; } path = new Path(0, buffer.length, buffer); path.collapsePath(); return true; } } return false; } private boolean isPathEmpty(URI baseURI) { if (start == end) { // The path is empty, and the authority and query are undefined. | If the scheme is also undefined, the // RFC2396 at 5.2.2 implies | that the query and fragment are inheritated from the baseURI | if undefined. if (isRelative) { query = ByteString.copy(baseURI.query); if (fragment == null) fragment = ByteString.copy(baseURI.fragment); } return true; } if ((!isRelative) && (spec[start] != '/') && (spec[start] != '?')) { // The path is in an opaque part - no query part in here path = new Path(start, end - start, spec); return true; } return false; } private void stripFragment() { for (int i = start; i < end; ++i) if (spec[i] == '#') { fragment = new ByteString(i + 1, end - (i + 1), spec); end = i; break; } } private void skipPart1() { for (int i = start; i < end; ++i) { switch (spec[i]) { case (byte) '/': case (byte) '#': case (byte) '?': break; case (byte) ':': if (i > start) { scheme = new ByteString(start, i - start, spec); start = i + 1; } break; default: continue; } break; } } private void stripURL() { // trim spaces - get rid of "url:" for (end = start + len; (end > start) && (spec[end - 1] <= ' '); --end); for (start = 0; (start < end) && (spec[start] <= ' '); ++start); if (((end - start) >= 4) && (spec[start + 3] == ':') && ((spec[start ] == 'u') || ((spec[start ] == 'U'))) && ((spec[start+1] == 'r') || ((spec[start+1] == 'R'))) && ((spec[start+2] == 'l') || ((spec[start+2] == 'L')))) { start += 4; } } /** * Return a String representation of this URI * * @return a String representation of this URI */ public String toString() { StringBuffer sb = new StringBuffer(50); if (scheme != null) { sb.append(scheme.toString()); sb.append(':'); } if (authority != null) { sb.append("//"); sb.append(authority.toString()); } if (path != null) sb.append(path.toString()); if (query != null) { sb.append("?"); sb.append(query.toString()); } if (fragment != null) { sb.append("#"); sb.append(fragment.toString()); } return sb.toString(); } /** * Encodes to a <code>application/x-www-form-urlencoded</code> * * @param val the String to encode * @return the encoded String. */ public static String encode(String val) { StringBuffer sb = new StringBuffer(val.length()*3/2); encode(val, sb); return sb.toString(); } /** * Decodes a <code>application/x-www-form-urlencoded</code> string * * @param val the String to decode * @return the decoded String. */ public static String decode(String val) { StringBuffer sb = new StringBuffer(val.length()*2/3); decode(val, sb); return sb.toString(); } /** * Encodes to a <code>application/x-www-form-urlencoded</code> * * @param val the String to encode * @param sb the StringBuffer where to write the encoded form */ public static void encode(String val, StringBuffer sb) { char chars[] = val.toCharArray(); int len = chars.length; for (int i = 0; i < len; ++i) { char c = chars[i]; if ((c < 128) && (0 != ((noEncode[c >> 4]) & (1 << (c & 0xF))))) { if (c == ' ') c = '+'; sb.append(c); } else { // convert to UTF-8 on the fly if (c >= 0x80) { // 2 to 3 bytes sb.append('%'); if (c >= 0x800) { // 3 bytes sequence int b = (0x80 | ((c >> 6) & 0x3F)); sb.append('E'); sb.append(toHexNibble[c >> 12]); sb.append('%'); sb.append(toHexNibble[b >> 4]); sb.append(toHexNibble[b & 0xF]); } else { sb.append((c > 1023) ? 'D' : 'C'); sb.append(toHexNibble[(c >> 6) & 0xF]); } c = (char) (0x80 | (c & 0x3F)); } sb.append('%'); sb.append(toHexNibble[c >> 4]); sb.append(toHexNibble[c & 0xF]); } } } /** * Decodes a <code>application/x-www-form-urlencoded</code> string * * @param val the String to decode * @param sb the StringBuffer where to write the decoded form */ public static void decode(String val, StringBuffer sb) { char chars[] = val.toCharArray(); // supposed to be all < 0x80 (utf8) int len = chars.length; int i = 0; while (i < len) { char c = chars[i++]; switch (c) { case '+': c = ' '; break; case '%': { // convert from UTF-8 on the fly boolean first = true; char acc = 0; int seq = 0; c = 0; while (i < len) { char tmp = chars[i++]; if (tmp <= '9') { if (tmp < '0') { c = '\uffff'; break; } c += (tmp & 0xF); } else { if ((tmp = (char) ((tmp & ~('a' - 'A')) - 'A')) >= (char) (16 - 10)) { c = '\uffff'; break; } c += (tmp + 10); } if (first) { first = false; c <<= 4; continue; } if (seq == 0) { if (c < 0x80) { // if a 1 byte sequence, break; // success. } acc = (char) (c & 0x1F); // first (clean) byte gotten! seq = ((c & 0xE0) == 0xC0) ? 1 : 2; // bytes sequence } else { // seq is 1 or 2 c ^= 0x80; if ((c & 0xC0) != 0) { // not a follower? c = '\uffff'; // too bad... break; } acc = (char) ((acc << 6) | c); if (seq == 1) { c = acc; break; } seq = 1; } if (chars[i++] != '%') { // expecting a % here --i; c = '\uffff'; // too bad... break; } c = 0; first = true; } } break; default: break; } sb.append(c); } } /** Used as the path component of this URI. */ public static class Path extends ByteString { private static final byte[] sds = { (byte) '/', (byte) '.', (byte) '/' }; private static final byte[] sdds = { (byte) '/', (byte) '.', (byte) '.', (byte) '/' }; /** * Constructor * * @param start where this Path starts in the byte array * @param len length of this Path * @param spec byte array that holds this Path */ Path(int pos, int len, byte[] spec) { super(pos, len, spec); } /** * Copy Constructor * * @param src Path to copy from */ Path(Path src) { super(src.pos, src.len, src.base); } /** * Reduce <code>a/b/./c/d/../e</code> to <code>a/b/c/e</code> */ final void collapsePath() { // Remove embedded /./ for (int found = 0; (found = indexOf(sds, found)) >= 0;) { this.len -= 2; Vm.arrayCopy(base, pos + found + 2, base, pos + found, this.len - found); } // Remove embedded /../ for (int found = 0; (found = indexOf(sdds, found)) >= 0;) { for (int ppos = pos + found - 1; ; --ppos) { if (ppos < pos) { // do not process... ++found; break; } // be careful: "/../../" means nothing. if ((base[ppos] == (byte) '/') && (base[ppos + 1] != (byte) '.')) { Vm.arrayCopy(base, pos + found + 3, base, ppos, this.len - (found + 3)); ppos -= pos; // relative this.len -= (found - ppos + 3); found = ppos; break; } } } // Remove a possibly trailing .. or . if (base[this.len + pos - 1] == (byte) '.') { int ppos = pos + this.len - 3; if ((ppos > pos) && ((base[ppos] == (byte) '/') && (base[ppos + 1] == (byte) '.'))) { do { if (base[--ppos] == (byte) '/') { this.len = ppos - pos + 1; break; } } while (ppos > pos); } else if ((++ppos > pos) && (base[ppos] == (byte) '/')) { --this.len; } } } } }