Mercurial > hg > monetdb-java
changeset 789:88c5b678e974 monetdbs
URL parser passes the tests.
Some tests had to change to accomodate Java.
author | Joeri van Ruth <joeri.van.ruth@monetdbsolutions.com> |
---|---|
date | Wed, 29 Nov 2023 13:28:52 +0100 (16 months ago) |
parents | 2f36ac68ac35 |
children | 547eca89fc5e |
files | src/main/java/org/monetdb/mcl/net/MonetUrlParser.java src/main/java/org/monetdb/mcl/net/Parameter.java src/main/java/org/monetdb/mcl/net/ParameterType.java src/main/java/org/monetdb/mcl/net/Target.java src/main/java/org/monetdb/mcl/net/ValidationError.java src/main/java/org/monetdb/mcl/net/Verify.java tests/UrlTester.java tests/tests.md |
diffstat | 8 files changed, 2634 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/src/main/java/org/monetdb/mcl/net/MonetUrlParser.java @@ -0,0 +1,305 @@ +package org.monetdb.mcl.net; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.util.Properties; + +public class MonetUrlParser { + private final Properties props; + private final String urlText; + private final URI url; + boolean userWasSet = false; + boolean passwordWasSet = false; + + public MonetUrlParser(Properties props, String url) throws URISyntaxException { + this.props = props; + this.urlText = url; + this.url = new URI(url); + } + + public static void parse(Properties props, String url) throws URISyntaxException { + boolean modern = true; + if (url.startsWith("mapi:")) { + modern = false; + url = url.substring(5); + if (url.equals("monetdb://")) { + // deal with peculiarity of Java's URI parser + url = "monetdb:///"; + } + + } + try { + MonetUrlParser parser = new MonetUrlParser(props, url); + if (modern) { + parser.parseModern(); + } else { + parser.parseClassic(); + } + if (parser.userWasSet && !parser.passwordWasSet) parser.clear(Parameter.PASSWORD); + } catch (URISyntaxException e) { + int idx = e.getIndex(); + if (idx >= 0 && !modern) { + // "mapi:" + idx += 5; + } + throw new URISyntaxException(e.getInput(), e.getReason(), idx); + } + } + + private static String percentDecode(String context, String text) throws URISyntaxException { + try { + return URLDecoder.decode(text, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("should be unreachable: UTF-8 unknown??", e); + } catch (IllegalArgumentException e) { + throw new URISyntaxException(text, context + ": invalid percent escape"); + } + } + + private void set(Parameter parm, String value) { + parm = keyMagic(parm); + props.setProperty(parm.name, value != null ? value : ""); + } + + private void set(String key, String value) { + Parameter parm = Parameter.forName(key); + if (parm != null) + set(parm, value); + else + props.setProperty(key, value); + } + + private void clear(Parameter parm) { + parm = keyMagic(parm); + String value = parm.type.format(Target.getDefault(parm)); + props.setProperty(parm.name, value); + } + + private Parameter keyMagic(Parameter key) { + switch (key) { + case USER: + userWasSet = true; + break; + case PASSWORD: + passwordWasSet = true; + break; + case FETCHSIZE: + key = Parameter.REPLYSIZE; + break; + default: + break; + } + return key; + } + + private void parseModern() throws URISyntaxException { + clearBasic(); + + String scheme = url.getScheme(); + if (scheme == null) throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://"); + switch (scheme) { + case "monetdb": + set(Parameter.TLS, "false"); + break; + case "monetdbs": + set(Parameter.TLS, "true"); + break; + default: + throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://"); + } + + // The built-in getHost and getPort methods do strange things + // in edge cases such as percent-encoded host names and + // invalid port numbers + String authority = url.getAuthority(); + String host; + String remainder; + int pos; + String raw = url.getRawSchemeSpecificPart(); + if (authority == null) { + if (!url.getRawSchemeSpecificPart().startsWith("//")) { + throw new URISyntaxException(urlText, "expected //"); + } + host = ""; + remainder = ""; + } else if (authority.startsWith("[")) { + // IPv6 + pos = authority.indexOf(']'); + if (pos < 0) + throw new URISyntaxException(urlText, "unmatched '['"); + host = authority.substring(1, pos); + remainder = authority.substring(pos + 1); + } else if ((pos = authority.indexOf(':')) >= 0){ + host = authority.substring(0, pos); + remainder = authority.substring(pos); + } else { + host = authority; + remainder = ""; + } + switch (host) { + case "localhost": + set(Parameter.HOST, ""); + break; + case "localhost.": + set(Parameter.HOST, "localhost"); + break; + default: + set(Parameter.HOST, host); + break; + } + + if (remainder.isEmpty()) { + // do nothing + } else if (remainder.startsWith(":")) { + String portStr = remainder.substring(1); + try { + int port = Integer.parseInt(portStr); + if (port <= 0 || port > 65535) + portStr = null; + } catch (NumberFormatException e) { + portStr = null; + } + if (portStr == null) + throw new URISyntaxException(urlText, "invalid port number"); + set(Parameter.PORT, portStr); + } + + String path = url.getRawPath(); + String[] parts = path.split("/", 5); + // <0: empty before leading slash> / <1: database> / <2: tableschema> / <3: table> / <4: should not exist> + switch (parts.length) { + case 5: + throw new URISyntaxException(urlText, "table name should not contain slashes"); + case 4: + set(Parameter.TABLE, percentDecode(Parameter.TABLE.name, parts[3])); + // fallthrough + case 3: + set(Parameter.TABLESCHEMA, percentDecode(Parameter.TABLESCHEMA.name, parts[2])); + // fallthrough + case 2: + set(Parameter.DATABASE, percentDecode(Parameter.DATABASE.name, parts[1])); + case 1: + case 0: + // fallthrough + break; + } + + final String query = url.getRawQuery(); + if (query != null) { + final String args[] = query.split("&"); + for (int i = 0; i < args.length; i++) { + pos = args[i].indexOf('='); + if (pos <= 0) { + throw new URISyntaxException(args[i], "invalid key=value pair"); + } + String key = args[i].substring(0, pos); + key = percentDecode(key, key); + Parameter parm = Parameter.forName(key); + if (parm != null && parm.isCore) + throw new URISyntaxException(key, key + "= is not allowed as a query parameter"); + + String value = args[i].substring(pos + 1); + set(key, percentDecode(key, value)); + } + } + } + + + private void parseClassic() throws URISyntaxException { + String scheme = url.getScheme(); + if (scheme == null) throw new URISyntaxException(urlText, "URL scheme must be mapi:monetdb:// or mapi:merovingian://"); + switch (scheme) { + case "monetdb": + clearBasic(); + break; + case "merovingian": + throw new IllegalStateException("mapi:merovingian: not supported yet"); + default: + throw new URISyntaxException(urlText, "URL scheme must be mapi:monetdb:// or mapi:merovingian://"); + } + + if (!url.getRawSchemeSpecificPart().startsWith("//")) { + throw new URISyntaxException(urlText, "expected //"); + } + + String authority = url.getRawAuthority(); + String host; + String portStr; + int pos; + if (authority == null) { + host = ""; + portStr = ""; + } else if (authority.indexOf('@') >= 0) { + throw new URISyntaxException(urlText, "user@host syntax is not allowed"); + } else if ((pos = authority.indexOf(':')) >= 0) { + host = authority.substring(0, pos); + portStr = authority.substring(pos + 1); + } else { + host = authority; + portStr = ""; + } + + if (!portStr.isEmpty()) { + int port; + try { + port = Integer.parseInt(portStr); + } catch (NumberFormatException e) { + port = -1; + } + if (port <= 0) { + throw new URISyntaxException(urlText, "invalid port number"); + } + set(Parameter.PORT, portStr); + } + + String path = url.getRawPath(); + boolean isUnix; + if (host.isEmpty() && portStr.isEmpty()) { + // socket + isUnix = true; + clear(Parameter.HOST); + set(Parameter.SOCK, path != null ? path : ""); + } else { + // tcp + isUnix = false; + clear(Parameter.SOCK); + set(Parameter.HOST, host); + if (path == null || path.isEmpty()) { + // do nothing + } else if (!path.startsWith("/")) { + throw new URISyntaxException(urlText, "expect path to start with /"); + } else { + String database = path.substring(1); + if (database.contains("/")) + throw new URISyntaxException(urlText, "no slashes allowed in database name"); + set(Parameter.DATABASE, database); + } + } + + final String query = url.getRawQuery(); + if (query != null) { + final String args[] = query.split("&"); + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if (arg.startsWith("language=")) { + String language = arg.substring(9); + set(Parameter.LANGUAGE, language); + } else if (arg.startsWith("database=")) { + String database = arg.substring(9); + set(Parameter.DATABASE, database); + } else { + // ignore + } + } + } + } + + private void clearBasic() { + clear(Parameter.HOST); + clear(Parameter.PORT); + clear(Parameter.SOCK); + clear(Parameter.DATABASE); + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/org/monetdb/mcl/net/Parameter.java @@ -0,0 +1,81 @@ +package org.monetdb.mcl.net; + + +public enum Parameter { + TLS("tls", ParameterType.Bool, false, "secure the connection using TLS", true), + HOST("host", ParameterType.Str, "", "IP number, domain name or one of the special values `localhost` and `localhost.`", true), + PORT("port", ParameterType.Int, -1, "Port to connect to, 1..65535 or -1 for 'not set'", true), + DATABASE("database", ParameterType.Str, "", "name of database to connect to", true), + TABLESCHEMA("tableschema", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true), + TABLE("table", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true), + SOCK("sock", ParameterType.Path, "", "path to Unix domain socket to connect to", false), + SOCKDIR("sockdir", ParameterType.Path, "/tmp", "Directory for implicit Unix domain sockets (.s.monetdb.PORT)", false), + CERT("cert", ParameterType.Path, "", "path to TLS certificate to authenticate server with", false), + CERTHASH("certhash", ParameterType.Str, "", "hash of server TLS certificate must start with these hex digits; overrides cert", false), + CLIENTKEY("clientkey", ParameterType.Path, "", "path to TLS key (+certs) to authenticate with as client", false), + CLIENTCERT("clientcert", ParameterType.Path, "", "path to TLS certs for 'clientkey', if not included there", false), + USER("user", ParameterType.Str, "", "user name to authenticate as", false), + PASSWORD("password", ParameterType.Str, "", "password to authenticate with", false), + LANGUAGE("language", ParameterType.Str, "sql", "for example, \"sql\", \"mal\", \"msql\", \"profiler\"", false), + AUTOCOMMIT("autocommit", ParameterType.Bool, false, "initial value of autocommit", false), + SCHEMA("schema", ParameterType.Str, "", "initial schema", false), + TIMEZONE("timezone", ParameterType.Int, null, "client time zone as minutes east of UTC", false), + BINARY("binary", ParameterType.Str, "on", "whether to use binary result set format (number or bool)", false), + REPLYSIZE("replysize", ParameterType.Int, 200, "rows beyond this limit are retrieved on demand, <1 means unlimited", false), + FETCHSIZE("fetchsize", ParameterType.Int, null, "alias for replysize, specific to jdbc", false), + HASH("hash", ParameterType.Str, "", "specific to jdbc", false), + DEBUG("debug", ParameterType.Bool, false, "specific to jdbc", false), + LOGFILE("logfile", ParameterType.Str, "", "specific to jdbc", false), + + ; + + public final String name; + public final ParameterType type; + public final Object defaultValue; + public final String description; + public final boolean isCore; + + Parameter(String name, ParameterType type, Object defaultValue, String description, boolean isCore) { + this.name = name; + this.type = type; + this.isCore = isCore; + this.defaultValue = defaultValue; + this.description = description; + } + + public static Parameter forName(String name) { + switch (name) { + case "tls": return TLS; + case "host": return HOST; + case "port": return PORT; + case "database": return DATABASE; + case "tableschema": return TABLESCHEMA; + case "table": return TABLE; + case "sock": return SOCK; + case "sockdir": return SOCKDIR; + case "cert": return CERT; + case "certhash": return CERTHASH; + case "clientkey": return CLIENTKEY; + case "clientcert": return CLIENTCERT; + case "user": return USER; + case "password": return PASSWORD; + case "language": return LANGUAGE; + case "autocommit": return AUTOCOMMIT; + case "schema": return SCHEMA; + case "timezone": return TIMEZONE; + case "binary": return BINARY; + case "replysize": return REPLYSIZE; + case "fetchsize": return FETCHSIZE; + case "hash": return HASH; + case "debug": return DEBUG; + case "logfile": return LOGFILE; + default: return null; + } + } + + public static boolean isIgnored(String name) { + if (Parameter.forName(name) != null) + return false; + return name.contains("_"); + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/org/monetdb/mcl/net/ParameterType.java @@ -0,0 +1,68 @@ +package org.monetdb.mcl.net; + +public enum ParameterType { + Str, + Int, + Bool, + Path; + + public Object parse(String name, String value) throws ValidationError { + if (value == null) + throw new NullPointerException(); + + try { + switch (this) { + case Bool: + return parseBool(value); + case Int: + return Integer.parseInt(value); + case Str: + case Path: + return value; + default: + throw new IllegalStateException("unreachable"); + } + } catch (IllegalArgumentException e) { + String message = e.toString(); + throw new ValidationError(name, message); + } + } + + public String format(Object value) { + switch (this) { + case Bool: + return (Boolean)value ? "true": "false"; + case Int: + return Integer.toString((Integer)value); + case Str: + case Path: + return (String) value; + default: + throw new IllegalStateException("unreachable"); + } + } + + public static boolean parseBool(String value) { + boolean lowered = false; + String original = value; + while (true) { + switch (value) { + case "true": + case "yes": + case "on": + return true; + case "false": + case "no": + case "off": + return false; + default: + if (!lowered) { + value = value.toLowerCase(); + lowered = true; + continue; + } + throw new IllegalArgumentException("invalid boolean value: " + original); + } + } + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/org/monetdb/mcl/net/Target.java @@ -0,0 +1,330 @@ +package org.monetdb.mcl.net; + +import java.util.Calendar; +import java.util.Properties; +import java.util.regex.Pattern; + +public class Target { + private static Pattern namePattern = Pattern.compile("^[a-zA-Z_][-a-zA-Z0-9_.]*$"); + private static Pattern hashPattern = Pattern.compile("^sha256:[0-9a-fA-F:]*$"); + private final boolean tls; + private final String host; + private final int port; + private final String database; + private final String tableschema; + private final String table; + private final String sock; + private final String sockdir; + private final String cert; + private final String certhash; + private final String clientkey; + private final String clientcert; + private final String user; + private final String password; + private final String language; + private final boolean autocommit; + private final String schema; + private final int timezone; + private final int binary; + private final int replysize; + private final String hash; + private final boolean debug; + private final String logfile; + + public Target(Properties properties) throws ValidationError { + + // 1. The parameters have the types listed in the table in [Section + // Parameters](#parameters). + tls = validateBoolean(properties, Parameter.TLS); + host = validateString(properties, Parameter.HOST); + port = validateInt(properties, Parameter.PORT); + database = validateString(properties, Parameter.DATABASE); + tableschema = validateString(properties, Parameter.TABLESCHEMA); + table = validateString(properties, Parameter.TABLE); + sock = validateString(properties, Parameter.SOCK); + sockdir = validateString(properties, Parameter.SOCKDIR); + cert = validateString(properties, Parameter.CERT); + certhash = validateString(properties, Parameter.CERTHASH); + clientkey = validateString(properties, Parameter.CLIENTKEY); + clientcert = validateString(properties, Parameter.CLIENTCERT); + user = validateString(properties, Parameter.USER); + password = validateString(properties, Parameter.PASSWORD); + language = validateString(properties, Parameter.LANGUAGE); + autocommit = validateBoolean(properties, Parameter.AUTOCOMMIT); + schema = validateString(properties, Parameter.SCHEMA); + timezone = validateInt(properties, Parameter.TIMEZONE); + replysize = validateInt(properties, Parameter.REPLYSIZE); + hash = validateString(properties, Parameter.HASH); + debug = validateBoolean(properties, Parameter.DEBUG); + logfile = validateString(properties, Parameter.LOGFILE); + + for (String name: properties.stringPropertyNames()) { + if (Parameter.forName(name) != null) + continue; + if (name.contains("_")) + continue; + throw new ValidationError("unknown parameter: " + name); + } + + String binaryString = validateString(properties, Parameter.BINARY); + int binaryInt; + try { + binaryInt = (int) ParameterType.Int.parse(Parameter.BINARY.name, binaryString); + } catch (ValidationError e) { + try { + boolean b = (boolean) ParameterType.Bool.parse(Parameter.BINARY.name, binaryString); + binaryInt = b ? 65535 : 0; + } catch (ValidationError ee) { + throw new ValidationError("binary= must be either a number or true/yes/on/false/no/off"); + } + } + if (binaryInt < 0) + throw new ValidationError("binary= cannot be negative"); + binary = binaryInt; + + + // 2. At least one of **sock** and **host** must be empty. + if (!sock.isEmpty() && !host.isEmpty()) + throw new ValidationError("sock=" + sock + " cannot be combined with host=" + host); + + // 3. The string parameter **binary** must either parse as a boolean or as a + // non-negative integer. + // + // (checked above) + + // 4. If **sock** is not empty, **tls** must be 'off'. + if (!sock.isEmpty() && tls) throw new ValidationError("monetdbs:// cannot be combined with sock="); + + // 5. If **certhash** is not empty, it must be of the form `{sha256}hexdigits` + // where hexdigits is a non-empty sequence of 0-9, a-f, A-F and colons. + // TODO + if (!certhash.isEmpty()) { + if (!certhash.toLowerCase().startsWith("sha256:")) + throw new ValidationError("certificate hash must start with 'sha256:'"); + if (!hashPattern.matcher(certhash).matches()) + throw new ValidationError("invalid certificate hash"); + } + + // 6. If **tls** is 'off', **cert** and **certhash** must be 'off' as well. + if (!tls) { + if (!cert.isEmpty() || !certhash.isEmpty()) + throw new ValidationError("cert= and certhash= are only allowed in combination with monetdbs://"); + } + + // 7. Parameters **database**, **tableschema** and **table** must consist only of + // upper- and lowercase letters, digits, periods, dashes and underscores. They must not + // start with a dash. + // If **table** is not empty, **tableschema** must also not be empty. + // If **tableschema** is not empty, **database** must also not be empty. + if (database.isEmpty() && !tableschema.isEmpty()) + throw new ValidationError("table schema cannot be set without database"); + if (tableschema.isEmpty() && !table.isEmpty()) + throw new ValidationError("table cannot be set without schema"); + if (!database.isEmpty() && !namePattern.matcher(database).matches()) + throw new ValidationError("invalid database name"); + if (!tableschema.isEmpty() && !namePattern.matcher(tableschema).matches()) + throw new ValidationError("invalid table schema name"); + if (!table.isEmpty() && !namePattern.matcher(table).matches()) + throw new ValidationError("invalid table name"); + + + // 8. Parameter **port** must be -1 or in the range 1-65535. + if (port < -1 || port == 0 || port > 65535) throw new ValidationError("invalid port number " + port); + + // 9. If **clientcert** is set, **clientkey** must also be set. + if (!clientcert.isEmpty() && clientkey.isEmpty()) + throw new ValidationError("clientcert= is only valid in combination with clientkey="); + } + + public static boolean validateBoolean(Properties props, Parameter parm) throws ValidationError { + Object value = props.get(parm.name); + if (value != null) { + return (Boolean) parm.type.parse(parm.name, (String) value); + } else { + return (Boolean) getDefault(parm); + } + } + + public static int validateInt(Properties props, Parameter parm) throws ValidationError { + Object value = props.get(parm.name); + if (value != null) { + return (Integer) parm.type.parse(parm.name, (String) value); + } else { + return (Integer) getDefault(parm); + } + } + + public static String validateString(Properties props, Parameter parm) throws ValidationError { + Object value = props.get(parm.name); + if (value != null) { + return (String) parm.type.parse(parm.name, (String) value); + } else { + return (String) getDefault(parm); + } + } + + private static int timezone() { + Calendar cal = Calendar.getInstance(); + int offsetMillis = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET); + int offsetSeconds = offsetMillis / 1000; + return offsetSeconds; + } + + public static Object getDefault(Parameter parm) { + if (parm == Parameter.TIMEZONE) return timezone(); + else return parm.defaultValue; + } + + public static Properties defaultProperties() { + Properties props = new Properties(); + return props; + } + + public boolean getTls() { + return tls; + } + + // Getter is private because you probably want connectTcp() instead + private String getHost() { + return host; + } + + // Getter is private because you probably want connectPort() instead + private int getPort() { + return port; + } + + public String getDatabase() { + return database; + } + + public String getTableschema() { + return tableschema; + } + + public String getTable() { + return table; + } + + // Getter is private because you probably want connectUnix() instead + private String getSock() { + return sock; + } + + public String getSockdir() { + return sockdir; + } + + public String getCert() { + return cert; + } + + public String getCerthash() { + return certhash; + } + + public String getClientkey() { + return clientkey; + } + + public String getClientcert() { + return clientcert; + } + + public String getUser() { + return user; + } + + public String getPassword() { + return password; + } + + public String getLanguage() { + return language; + } + + public boolean getAutocommit() { + return autocommit; + } + + public String getSchema() { + return schema; + } + + public int getTimezone() { + return timezone; + } + + // Getter is private because you probably want connectBinary() instead + public int getBinary() { + return binary; + } + + public int getReplysize() { + return replysize; + } + + public String getHash() { + return hash; + } + + public boolean getDebug() { + return debug; + } + + public String getLogfile() { + return logfile; + } + + public boolean connectScan() { + if (database.isEmpty()) return false; + if (!sock.isEmpty() || !host.isEmpty() || port != -1) return false; + return !tls; + } + + public int connectPort() { + return port == -1 ? 50000 : port; + } + + public String connectUnix() { + if (!sock.isEmpty()) return sock; + if (tls) return ""; + if (host.isEmpty()) return sockdir + "/.s.monetdb." + connectPort(); + return ""; + } + + public String connectTcp() { + if (!sock.isEmpty()) return ""; + if (host.isEmpty()) return "localhost"; + return host; + } + + public Verify connectVerify() { + if (!tls) return Verify.None; + if (!certhash.isEmpty()) return Verify.Hash; + if (!cert.isEmpty()) return Verify.Cert; + return Verify.System; + } + + public String connectCertHashDigits() { + if (!tls) return null; + StringBuilder builder = new StringBuilder(certhash.length()); + for (int i = "sha256:".length(); i < certhash.length(); i++) { + char c = certhash.charAt(i); + if (Character.digit(c, 16) >= 0) builder.append(Character.toLowerCase(c)); + } + return builder.toString(); + } + + public int connectBinary() { + return binary; + } + + public String connectClientKey() { + return clientkey; + } + + public String connectClientCert() { + return clientcert.isEmpty() ? clientkey : clientcert; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/org/monetdb/mcl/net/ValidationError.java @@ -0,0 +1,11 @@ +package org.monetdb.mcl.net; + +public class ValidationError extends Exception { + public ValidationError(String parameter, String message) { + super(parameter + ": " + message); + } + + public ValidationError(String message) { + super(message); + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/org/monetdb/mcl/net/Verify.java @@ -0,0 +1,8 @@ +package org.monetdb.mcl.net; + +public enum Verify { + None, + Cert, + Hash, + System; +}
new file mode 100644 --- /dev/null +++ b/tests/UrlTester.java @@ -0,0 +1,373 @@ +import org.monetdb.mcl.net.*; + +import java.io.*; +import java.net.URISyntaxException; +import java.util.Properties; + +public class UrlTester { + String filename = null; + int verbose = 0; + BufferedReader reader = null; + int lineno = 0; + int testCount = 0; + Properties props = null; + Target validated = null; + + public UrlTester() { + } + + public UrlTester(String filename) { + this.filename = filename; + } + + public static void main(String[] args) throws Exception { + int exitcode; + UrlTester tester = new UrlTester(); + exitcode = tester.parseArgs(args); + if (exitcode == 0) + exitcode = tester.run(); + System.exit(exitcode); + } + + private int parseArgs(String[] args) { + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if (arg.startsWith("-")) { + int result = handleFlags(arg); + if (result != 0) + return result; + } else if (filename == null) { + filename = arg; + } else { + System.err.println("Unexpected argument: " + arg); + return 1; + } + } + return 0; + } + + private int run() throws IOException { + if (filename != null) { + reader = new BufferedReader(new FileReader(filename)); + } else { + String resourceName = "/tests.md"; + InputStream stream = this.getClass().getResourceAsStream(resourceName); + if (stream == null) { + System.err.println("Resource " + resourceName + " not found"); + return 1; + } + reader = new BufferedReader(new InputStreamReader(stream)); + filename = "tests/tests.md"; + } + + try { + processFile(); + } catch (Failure e) { + System.err.println("" + filename + ":" + lineno + ": " + e.getMessage()); + return 1; + } + return 0; + } + + private int handleFlags(String arg) { + if (!arg.startsWith("-") || arg.equals("-")) { + System.err.println("Invalid flag: " + arg); + } + String a = arg.substring(1); + + while (!a.isEmpty()) { + char letter = a.charAt(0); + a = a.substring(1); + switch (letter) { + case 'v': + verbose++; + break; + default: + System.err.println("Unexpected flag " + letter + " in " + arg); + return -1; + } + } + + return 0; + } + + private void processFile() throws IOException, Failure { + while (true) { + String line = reader.readLine(); + if (line == null) + break; + lineno++; + processLine(line); + } + if (verbose >= 1) { + System.out.println(); + System.out.println("Ran " + testCount + " tests in " + lineno + " lines"); + } + } + + private void processLine(String line) throws Failure { + line = line.replaceFirst("\\s+$", ""); // remove trailing + if (props == null && line.equals("```test")) { + if (verbose >= 2) { + if (testCount > 0) { + System.out.println(); + } + System.out.println("\u25B6 " + filename + ":" + lineno); + } + props = Target.defaultProperties(); + testCount++; + return; + } + if (props != null) { + if (line.equals("```")) { + stopProcessing(); + return; + } + handleCommand(line); + } + } + + private void stopProcessing() { + props = null; + validated = null; + } + + private void handleCommand(String line) throws Failure { + if (verbose >= 3) { + System.out.println(line); + } + if (line.isEmpty()) + return; + + String[] parts = line.split("\\s+", 2); + String command = parts[0]; + switch (command.toUpperCase()) { + case "ONLY": + handleOnly(true, parts[1]); + return; + case "NOT": + handleOnly(false, parts[1]); + return; + case "PARSE": + handleParse(parts[1], null); + return; + case "ACCEPT": + handleParse(parts[1], true); + return; + case "REJECT": + handleParse(parts[1], false); + return; + case "SET": + handleSet(parts[1]); + return; + case "EXPECT": + handleExpect(parts[1]); + return; + default: + throw new Failure("Unexpected command: " + command); + } + + } + + private void handleOnly(boolean mustBePresent, String rest) throws Failure { + boolean found = false; + for (String part: rest.split("\\s+")) { + if (part.equals("jdbc")) { + found = true; + break; + } + } + if (found != mustBePresent) { + // do not further process this block + stopProcessing(); + } + } + + private int findEqualSign(String rest) throws Failure { + int index = rest.indexOf('='); + if (index < -1) + throw new Failure("Expected to find a '='"); + return index; + } + + private String splitKey(String rest) throws Failure { + int index = findEqualSign(rest); + return rest.substring(0, index); + } + + private String splitValue(String rest) throws Failure { + int index = findEqualSign(rest); + return rest.substring(index + 1); + } + + private void handleSet(String rest) throws Failure { + validated = null; + String key = splitKey(rest); + String value = splitValue(rest); + + props.put(key, value); + } + + private void handleParse(String rest, Boolean shouldSucceed) throws Failure { + URISyntaxException parseError = null; + ValidationError validationError = null; + + validated = null; + try { + MonetUrlParser.parse(props, rest); + } catch (URISyntaxException e) { + parseError = e; + } + + if (parseError == null) { + try { + validated = new Target(props); + } catch (ValidationError e) { + validationError = e; + } + } + + if (shouldSucceed == Boolean.FALSE) { + if (parseError != null || validationError != null) + return; // happy + else + throw new Failure("URL unexpectedly parsed and validated"); + } + + if (parseError != null) + throw new Failure("Parse error: " + parseError); + if (validationError != null && shouldSucceed == Boolean.TRUE) + throw new Failure("Validation error: " + validationError); + } + + private void handleExpect(String rest) throws Failure { + String key = splitKey(rest); + String expectedString = splitValue(rest); + + Object actual = null; + try { + actual = extract(key); + } catch (ValidationError e) { + throw new Failure(e.getMessage()); + } + + Object expected; + try { + if (actual instanceof Boolean) + expected = ParameterType.Bool.parse(key, expectedString); + else if (actual instanceof Integer) + expected = ParameterType.Int.parse(key, expectedString); + else + expected = expectedString; + } catch (ValidationError e) { + String typ = actual.getClass().getName(); + throw new Failure("Cannot convert expected value <" + expectedString + "> to " + typ + ": " + e.getMessage()); + } + + if (actual.equals(expected)) + return; + throw new Failure("Expected " + key + "=<" + expectedString + ">, found <" + actual + ">"); + } + + private Target tryValidate() throws ValidationError { + if (validated == null) + validated = new Target(props); + return validated; + } + + private Object extract(String key) throws ValidationError, Failure { + switch (key) { + case "tls": + return Target.validateBoolean(props, Parameter.TLS); + case "host": + return Target.validateString(props, Parameter.HOST); + case "port": + return Target.validateInt(props, Parameter.PORT); + case "database": + return Target.validateString(props, Parameter.DATABASE); + case "tableschema": + return Target.validateString(props, Parameter.TABLESCHEMA); + case "table": + return Target.validateString(props, Parameter.TABLE); + case "sock": + return Target.validateString(props, Parameter.SOCK); + case "sockdir": + return Target.validateString(props, Parameter.SOCKDIR); + case "cert": + return Target.validateString(props, Parameter.CERT); + case "certhash": + return Target.validateString(props, Parameter.CERTHASH); + case "clientkey": + return Target.validateString(props, Parameter.CLIENTKEY); + case "clientcert": + return Target.validateString(props, Parameter.CLIENTCERT); + case "user": + return Target.validateString(props, Parameter.USER); + case "password": + return Target.validateString(props, Parameter.PASSWORD); + case "language": + return Target.validateString(props, Parameter.LANGUAGE); + case "autocommit": + return Target.validateBoolean(props, Parameter.AUTOCOMMIT); + case "schema": + return Target.validateString(props, Parameter.SCHEMA); + case "timezone": + return Target.validateInt(props, Parameter.TIMEZONE); + case "binary": + return Target.validateString(props, Parameter.BINARY); + case "replysize": + return Target.validateInt(props, Parameter.REPLYSIZE); + case "fetchsize": + return Target.validateInt(props, Parameter.FETCHSIZE); + case "hash": + return Target.validateString(props, Parameter.HASH); + case "debug": + return Target.validateBoolean(props, Parameter.DEBUG); + case "logfile": + return Target.validateString(props, Parameter.LOGFILE); + + case "valid": + try { + tryValidate(); + } catch (ValidationError e) { + return Boolean.FALSE; + } + return Boolean.TRUE; + + case "connect_scan": + return tryValidate().connectScan(); + case "connect_port": + return tryValidate().connectPort(); + case "connect_unix": + return tryValidate().connectUnix(); + case "connect_tcp": + return tryValidate().connectTcp(); + case "connect_tls_verify": + switch (tryValidate().connectVerify()) { + case None: return ""; + case Cert: return "cert"; + case Hash: return "hash"; + case System: return "system"; + default: + throw new IllegalStateException("unreachable"); + } + case "connect_certhash_digits": + return tryValidate().connectCertHashDigits(); + case "connect_binary": + return tryValidate().connectBinary(); + case "connect_clientkey": + return tryValidate().connectClientKey(); + case "connect_clientcert": + return tryValidate().connectClientCert(); + + default: + throw new Failure("Unknown attribute: " + key); + } + } + + private class Failure extends Exception { + public Failure(String message) { + super(message); + } + } +}
new file mode 100644 --- /dev/null +++ b/tests/tests.md @@ -0,0 +1,1458 @@ +# Tests + +This document contains a large number of test cases. +They are embedded in the Markdown source, in <code>```test</code> +. . .</code>```</code> blocks. + + +The tests are written in a mini language with the following +keywords: + +* `PARSE url`: parse the URL, this should succeed. The validity checks need + not be satisfied. + +* `ACCEPT url`: parse the URL, this should succeed. The validity checks + should pass. + +* `REJECT url`: parse the URL, it should be rejected either in the parsing stage + or by the validity checks. + +* `SET key=value`: modify a parameter, can occur before or after parsing the URL. + Used to model command line parameters, Java Properties objects, etc. + +* `EXPECT key=value`: verify that the given parameter now has the given + value. Fail the test case if the value is different. + +* `ONLY pymonetdb`: only process the rest of the block when testing + pymonetdb, ignore it for other implementations. + +* `NOT pymonetdb`: ignore the rest of the block when testing pymonetdb, + do process it for other implementations. + +At the start of each block the parameters are reset to their default values. + +The EXPECT clause can verify all parameters listen in the Parameters section of +the spec, all 'virtual parameters' and also the special case `valid` which is a +boolean indicating whether all validity rules in section 'Interpreting the +parameters' hold. + +Note: an `EXPECT` of the virtual parameters implies `EXPECT valid=true`. + +TODO before 1.0 does the above explanation make sense? + + +## Tests from the examples + +```test +ACCEPT monetdb:///demo +EXPECT database=demo +EXPECT connect_scan=true +``` + +```test +ACCEPT monetdb://localhost/demo +EXPECT connect_scan=true +EXPECT database=demo +``` + +```test +ACCEPT monetdb://localhost./demo +EXPECT connect_scan=false +EXPECT connect_unix= +EXPECT connect_tcp=localhost +EXPECT connect_port=50000 +EXPECT tls=off +EXPECT database=demo +``` + +```test +ACCEPT monetdb://localhost.:12345/demo +EXPECT connect_scan=false +EXPECT connect_unix= +EXPECT connect_tcp=localhost +EXPECT connect_port=12345 +EXPECT tls=off +EXPECT database=demo +``` + +```test +ACCEPT monetdb://localhost:12345/demo +EXPECT connect_scan=false +EXPECT connect_unix=/tmp/.s.monetdb.12345 +EXPECT connect_tcp=localhost +EXPECT connect_port=12345 +EXPECT tls=off +EXPECT database=demo +``` + +```test +ACCEPT monetdb:///demo?user=monetdb&password=monetdb +EXPECT connect_scan=true +EXPECT database=demo +EXPECT user=monetdb +EXPECT password=monetdb +``` + +```test +ACCEPT monetdb://mdb.example.com:12345/demo +EXPECT connect_scan=false +EXPECT connect_unix= +EXPECT connect_tcp=mdb.example.com +EXPECT connect_port=12345 +EXPECT tls=off +EXPECT database=demo +``` + +```test +ACCEPT monetdb://192.168.13.4:12345/demo +EXPECT connect_scan=false +EXPECT connect_unix= +EXPECT connect_tcp=192.168.13.4 +EXPECT connect_port=12345 +EXPECT tls=off +EXPECT database=demo +``` + +```test +ACCEPT monetdb://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:12345/demo +EXPECT host=2001:0db8:85a3:0000:0000:8a2e:0370:7334 +EXPECT connect_scan=false +EXPECT connect_unix= +EXPECT connect_tcp=2001:0db8:85a3:0000:0000:8a2e:0370:7334 +EXPECT connect_port=12345 +EXPECT tls=off +EXPECT database=demo +``` + +```test +ACCEPT monetdb://localhost/ +EXPECT connect_unix=/tmp/.s.monetdb.50000 +EXPECT connect_scan=false +EXPECT connect_tcp=localhost +EXPECT tls=off +EXPECT database= +``` + +```test +ACCEPT monetdb://localhost +EXPECT connect_scan=false +EXPECT connect_unix=/tmp/.s.monetdb.50000 +EXPECT connect_tcp=localhost +EXPECT tls=off +EXPECT database= +``` + +```test +ACCEPT monetdbs://mdb.example.com/demo +EXPECT connect_scan=false +EXPECT connect_unix= +EXPECT connect_tcp=mdb.example.com +EXPECT connect_port=50000 +EXPECT tls=on +EXPECT connect_tls_verify=system +EXPECT database=demo +``` + +```test +ACCEPT monetdbs:///demo +EXPECT connect_scan=false +EXPECT connect_unix= +EXPECT connect_tcp=localhost +EXPECT connect_port=50000 +EXPECT tls=on +EXPECT connect_tls_verify=system +EXPECT database=demo +``` + +```test +ACCEPT monetdbs://mdb.example.com/demo?cert=/home/user/server.crt +EXPECT connect_scan=false +EXPECT connect_unix= +EXPECT connect_tcp=mdb.example.com +EXPECT connect_port=50000 +EXPECT tls=on +EXPECT connect_tls_verify=cert +EXPECT cert=/home/user/server.crt +EXPECT database=demo +``` + +```test +ACCEPT monetdbs://mdb.example.com/demo?certhash=sha256:fb:67:20:aa:00:9f:33:4c +EXPECT connect_scan=false +EXPECT connect_unix= +EXPECT connect_tcp=mdb.example.com +EXPECT connect_port=50000 +EXPECT tls=on +EXPECT connect_tls_verify=hash +EXPECT certhash=sha256:fb:67:20:aa:00:9f:33:4c +EXPECT connect_certhash_digits=fb6720aa009f334c +EXPECT database=demo +``` + +```test +ACCEPT monetdb:///demo?sock=/var/monetdb/_sock&user=dbuser +EXPECT connect_scan=false +EXPECT connect_unix=/var/monetdb/_sock +EXPECT connect_tcp= +EXPECT tls=off +EXPECT database=demo +EXPECT user=dbuser +EXPECT password= +``` + +```test +ACCEPT monetdb://localhost/demo?sock=/var/monetdb/_sock&user=dbuser +EXPECT connect_scan=false +EXPECT connect_unix=/var/monetdb/_sock +EXPECT connect_tcp= +EXPECT tls=off +EXPECT database=demo +EXPECT user=dbuser +EXPECT password= +``` + + +## Parameter tests + +Tests derived from the parameter section. Test data types and defaults. + +Everything can be SET and EXPECTed + +```test +SET tls=on +EXPECT tls=on +SET host=bananahost +EXPECT host=bananahost +SET port=123 +EXPECT port=123 +SET database=bananadatabase +EXPECT database=bananadatabase +SET tableschema=bananatableschema +EXPECT tableschema=bananatableschema +SET table=bananatable +EXPECT table=bananatable +SET sock=c:\foo.txt +EXPECT sock=c:\foo.txt +SET cert=c:\foo.txt +EXPECT cert=c:\foo.txt +SET certhash=bananacerthash +EXPECT certhash=bananacerthash +SET clientkey=c:\foo.txt +EXPECT clientkey=c:\foo.txt +SET clientcert=c:\foo.txt +EXPECT clientcert=c:\foo.txt +SET user=bananauser +EXPECT user=bananauser +SET password=bananapassword +EXPECT password=bananapassword +SET language=bananalanguage +EXPECT language=bananalanguage +SET autocommit=on +EXPECT autocommit=on +SET schema=bananaschema +EXPECT schema=bananaschema +SET timezone=123 +EXPECT timezone=123 +SET binary=bananabinary +EXPECT binary=bananabinary +SET replysize=123 +EXPECT replysize=123 +SET fetchsize=123 +EXPECT fetchsize=123 +``` + +### core defaults + +```test +EXPECT tls=false +EXPECT host= +EXPECT port=-1 +EXPECT database= +EXPECT tableschema= +EXPECT table= +EXPECT binary=on +``` + +### sock + +Not supported on Windows, but they should still parse. + +```test +EXPECT sock= +ACCEPT monetdb:///?sock=/tmp/sock +EXPECT sock=/tmp/sock +ACCEPT monetdb:///?sock=C:/TEMP/sock +EXPECT sock=C:/TEMP/sock +NOT jdbc +ACCEPT monetdb:///?sock=C:\TEMP\sock +EXPECT sock=C:\TEMP\sock +``` + +### sockdir + +```test +EXPECT sockdir=/tmp +ACCEPT monetdb:///demo?sockdir=/tmp/nonstandard +EXPECT sockdir=/tmp/nonstandard +EXPECT connect_unix=/tmp/nonstandard/.s.monetdb.50000 +``` + +### cert + +```test +EXPECT cert= +ACCEPT monetdbs:///?cert=/tmp/cert.pem +EXPECT cert=/tmp/cert.pem +ACCEPT monetdbs:///?cert=C:/TEMP/cert.pem +EXPECT cert=C:/TEMP/cert.pem +NOT jdbc +ACCEPT monetdbs:///?cert=C:\TEMP\cert.pem +EXPECT cert=C:\TEMP\cert.pem +``` + +### certhash + +```test +EXPECT certhash= +ACCEPT monetdbs:///?certhash=sha256:001122ff +ACCEPT monetdbs:///?certhash=sha256:00:11:22:ff +ACCEPT monetdbs:///?certhash=sha256:::::aa::ff::::: +ACCEPT monetdbs:///?certhash=sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +``` + +This string of hexdigits is longer than the length of a SHA-256 digest. +It still parses, it will just never match. + +```test +ACCEPT monetdbs:///?certhash=sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8550 +ACCEPT monetdbs:///?certhash=sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855000000000000000000000000000000000000000001 +``` + +```test +REJECT monetdbs:///?certhash=001122ff +REJECT monetdbs:///?certhash=Sha256:001122ff +REJECT monetdbs:///?certhash=sha256:001122gg +REJECT monetdbs:///?certhash=sha +REJECT monetdbs:///?certhash=sha1:aabbcc +REJECT monetdbs:///?certhash=sha1: +REJECT monetdbs:///?certhash=sha1:X +REJECT monetdbs:///?certhash=sha99:aabbcc +REJECT monetdbs:///?certhash=sha99: +REJECT monetdbs:///?certhash=sha99:X +``` + +### clientkey, clientcert + +```test +EXPECT clientkey= +EXPECT clientcert= +ACCEPT monetdbs:///?clientkey=/tmp/clientkey.pem +EXPECT clientkey=/tmp/clientkey.pem +ACCEPT monetdbs:///?clientkey=C:/TEMP/clientkey.pem +EXPECT clientkey=C:/TEMP/clientkey.pem +NOT jdbc +ACCEPT monetdbs:///?clientkey=C:\TEMP\clientkey.pem +EXPECT clientkey=C:\TEMP\clientkey.pem +``` + +```test +EXPECT connect_clientkey= +EXPECT connect_clientcert= +``` + +```test +SET clientkey=/tmp/key.pem +SET clientcert=/tmp/cert.pem +EXPECT valid=true +EXPECT connect_clientkey=/tmp/key.pem +EXPECT connect_clientcert=/tmp/cert.pem +``` + +```test +SET clientkey=/tmp/key.pem +EXPECT valid=true +EXPECT connect_clientkey=/tmp/key.pem +EXPECT connect_clientcert=/tmp/key.pem +``` + +```test +SET clientcert=/tmp/cert.pem +EXPECT valid=false +``` + +```test +SET clientkey=dummy +EXPECT clientcert= +ACCEPT monetdbs:///?clientcert=/tmp/clientcert.pem +EXPECT clientcert=/tmp/clientcert.pem +ACCEPT monetdbs:///?clientcert=C:/TEMP/clientcert.pem +EXPECT clientcert=C:/TEMP/clientcert.pem +NOT jdbc +ACCEPT monetdbs:///?clientcert=C:\TEMP\clientcert.pem +EXPECT clientcert=C:\TEMP\clientcert.pem +``` + +### user, password + +Not testing the default because they are (unfortunately) +implementation specific. + +```test +ACCEPT monetdb:///?user=monetdb +EXPECT user=monetdb +ACCEPT monetdb:///?user=me&password=? +EXPECT user=me +EXPECT password=? +``` + +### language + +```test +EXPECT language=sql +ACCEPT monetdb:///?language=msql +EXPECT language=msql +ACCEPT monetdb:///?language=sql +EXPECT language=sql +``` + +### autocommit + +```test +ACCEPT monetdb:///?autocommit=true +EXPECT autocommit=true +ACCEPT monetdb:///?autocommit=on +EXPECT autocommit=true +ACCEPT monetdb:///?autocommit=yes +EXPECT autocommit=true +``` + +```test +ACCEPT monetdb:///?autocommit=false +EXPECT autocommit=false +ACCEPT monetdb:///?autocommit=off +EXPECT autocommit=false +ACCEPT monetdb:///?autocommit=no +EXPECT autocommit=false +``` + +```test +REJECT monetdb:///?autocommit= +REJECT monetdb:///?autocommit=banana +REJECT monetdb:///?autocommit=0 +REJECT monetdb:///?autocommit=1 +``` + +### schema, timezone + +Must be accepted, no constraints on content + +```test +EXPECT schema= +ACCEPT monetdb:///?schema=foo +EXPECT schema=foo +ACCEPT monetdb:///?schema= +EXPECT schema= +ACCEPT monetdb:///?schema=foo +``` + +```test +ACCEPT monetdb:///?timezone=0 +EXPECT timezone=0 +ACCEPT monetdb:///?timezone=120 +EXPECT timezone=120 +ACCEPT monetdb:///?timezone=-120 +EXPECT timezone=-120 +REJECT monetdb:///?timezone=banana +REJECT monetdb:///?timezone= +``` + +### replysize and fetchsize + +Note we never check `EXPECT fetchsize=`, it doesn't exist. + +```test +ACCEPT monetdb:///?replysize=150 +EXPECT replysize=150 +ACCEPT monetdb:///?fetchsize=150 +EXPECT replysize=150 +ACCEPT monetdb:///?fetchsize=100&replysize=200 +EXPECT replysize=200 +ACCEPT monetdb:///?replysize=100&fetchsize=200 +EXPECT replysize=200 +REJECT monetdb:///?replysize= +REJECT monetdb:///?fetchsize= +``` + +### binary + +```test +EXPECT binary=on +EXPECT connect_binary=65535 +``` + +```test +ACCEPT monetdb:///?binary=on +EXPECT connect_binary=65535 + +ACCEPT monetdb:///?binary=yes +EXPECT connect_binary=65535 + +ACCEPT monetdb:///?binary=true +EXPECT connect_binary=65535 + +ACCEPT monetdb:///?binary=yEs +EXPECT connect_binary=65535 +``` + +```test +ACCEPT monetdb:///?binary=off +EXPECT connect_binary=0 + +ACCEPT monetdb:///?binary=no +EXPECT connect_binary=0 + +ACCEPT monetdb:///?binary=false +EXPECT connect_binary=0 +``` + +```test +ACCEPT monetdb:///?binary=0 +EXPECT connect_binary=0 + +ACCEPT monetdb:///?binary=5 +EXPECT connect_binary=5 + +ACCEPT monetdb:///?binary=0100 +EXPECT connect_binary=100 +``` + +```test +REJECT monetdb:///?binary= +REJECT monetdb:///?binary=-1 +REJECT monetdb:///?binary=1.0 +REJECT monetdb:///?binary=banana +``` + +### unknown parameters + +```test +REJECT monetdb:///?banana=bla +``` + +```test +ACCEPT monetdb:///?ban_ana=bla +ACCEPT monetdb:///?hash=sha1 +ACCEPT monetdb:///?debug=true +ACCEPT monetdb:///?logfile=banana +``` + +Unfortunately we can't easily test that it won't allow us +to SET banana. + +```test +SET ban_ana=bla +SET hash=sha1 +SET debug=true +SET logfile=banana +``` + +## Combining sources + +The defaults have been tested in the previous section. + +Rule: If there is overlap, later sources take precedence. + +```test +SET schema=a +ACCEPT monetdb:///db1?schema=b +EXPECT schema=b +EXPECT database=db1 +EXPECT tls=off +ACCEPT monetdbs:///db2?schema=c +EXPECT tls=on +EXPECT database=db2 +EXPECT schema=c +``` + +Rule: a source that sets user must set password or clear. + +```skiptest +ACCEPT monetdb:///?user=foo +EXPECT user=foo +EXPECT password= +SET password=banana +EXPECT user=foo +EXPECT password=banana +SET user=bar +EXPECT password= +``` + +Rule: fetchsize is an alias for replysize, last occurrence counts + +```test +SET replysize=200 +ACCEPT monetdb:///?fetchsize=400 +EXPECT replysize=400 +ACCEPT monetdb:///?replysize=500&fetchsize=600 +EXPECT replysize=600 +``` + +```test +NOT jdbc +SET replysize=200 +SET fetchsize=300 +EXPECT replysize=300 +``` + + + +Rule: parsing a URL sets all of tls, host, port and database +even if left out of the URL + +```test +SET tls=on +SET host=banana +SET port=12345 +SET database=foo +SET timezone=120 +ACCEPT monetdb:/// +EXPECT tls=off +EXPECT host= +EXPECT port=-1 +EXPECT database= +``` + +```test +SET tls=on +SET host=banana +SET port=12345 +SET database=foo +SET timezone=120 +ACCEPT monetdb://dbhost/dbdb +EXPECT tls=off +EXPECT host=dbhost +EXPECT port=-1 +EXPECT database=dbdb +``` + +Careful around passwords + +```test +SET user=alan +SET password=turing +ACCEPT monetdbs:/// +EXPECT user=alan +EXPECT password=turing +``` + +```test +SET user=alan +SET password=turing +ACCEPT monetdbs:///?user=mathison +EXPECT user=mathison +EXPECT password= +``` + +The rule is, "if **user** is set", not "if **user** is changed". + +```test +SET user=alan +SET password=turing +ACCEPT monetdbs:///?user=alan +EXPECT user=alan +EXPECT password= +``` + +## URL syntax + +General form + +```test +REJECT monetdb: +REJECT monetdbs: +REJECT monetdb:/ +REJECT monetdbs:/ +REJECT monetdb:// +REJECT monetdbs:// +ACCEPT monetdb:/// +ACCEPT monetdbs:/// +``` + + +```test +ACCEPT monetdb://host:12345/db1/schema2/table3?user=mr&password=bean +EXPECT tls=off +EXPECT host=host +EXPECT port=12345 +EXPECT database=db1 +EXPECT tableschema=schema2 +EXPECT table=table3 +EXPECT user=mr +EXPECT password=bean +``` + +Also, TLS and percent-escapes + +```test +ACCEPT monetdbs://h%6Fst:12345/db%31/schema%32/table%33?user=%6Dr&p%61ssword=bean +EXPECT tls=on +EXPECT host=host +EXPECT port=12345 +EXPECT database=db1 +EXPECT tableschema=schema2 +EXPECT table=table3 +EXPECT user=mr +EXPECT password=bean +``` + +Port number + +```test +REJECT monetdb://banana:0/ +REJECT monetdb://banana:-1/ +REJECT monetdb://banana:65536/ +REJECT monetdb://banana:100000/ +``` + +Trailing slash can be left off + +```test +ACCEPT monetdb://host?user=claude&password=m%26ms +EXPECT host=host +EXPECT user=claude +EXPECT password=m&ms +``` + +Error to set tls, host, port, database, tableschema and table as query parameters. + +```test +REJECT monetdb://foo:1/bar?tls=off +REJECT monetdb://foo:1/bar?host=localhost +REJECT monetdb://foo:1/bar?port=12345 +REJECT monetdb://foo:1/bar?database=allmydata +REJECT monetdb://foo:1/bar?tableschema=banana +REJECT monetdb://foo:1/bar?table=tabularity +``` + +Last wins, already tested elsewhere but for completeness + +```test +ACCEPT monetdbs:///?timezone=10&timezone=20 +EXPECT timezone=20 +``` + +Interesting case: setting user must clear the password but does +that also happen with repetitions within a URL? +Not sure. For the time being, no. This makes it easier for +situations where for example the query parameters come in +alphabetical order + +```test +ACCEPT monetdb:///?user=foo&password=banana&user=bar +EXPECT user=bar +EXPECT password=banana +``` + +Similar but even simpler: user comes after password but does not +clear it. + +```test +ACCEPT monetdb:///?password=pw&user=foo +EXPECT user=foo +EXPECT password=pw +``` + +Ways of writing booleans and the binary property have already been tested above. + +Ip numbers: + +```test +ACCEPT monetdb://192.168.1.1:12345/foo +EXPECT connect_unix= +EXPECT connect_tcp=192.168.1.1 +EXPECT database=foo +``` + +```test +ACCEPT monetdb://[::1]:12345/foo +EXPECT connect_unix= +EXPECT connect_tcp=::1 +EXPECT database=foo +``` + +Bad percent escapes: + +```test +REJECT monetdb:///m%xxbad +``` + + +## Interpreting + +Testing the validity constraints. +They apply both when parsing a URL and with ad-hoc settings. + +Rule 1, the type constraints, has already been tested in [Section Parameter +tests](#parameter-tests). + +Rule 2, interaction between **sock** and **host** is tested below in +[the next subsection](#interaction-between-tls-host-sock-and-database). + +Rule 3, about **binary**, is tested in [Subsection Binary](#binary). + +Rule 4, **sock** vs **tls** is tested below in [the next +subsection](#interaction-between-tls-host-sock-and-database). + +Rule 5, **certhash** syntax, is tested in [Subsection Certhash](#certhash). + +Rule 6, **tls** **cert** **certhash** interaction, is tested +in [Subsection Interaction between tls, cert and certhash](#interaction-between-tls-cert-and-certhash). + +Rule 7, **database**, **tableschema**, **table** is tested in [Subsection +Database, schema, table name +constraints](#database-schema-table-name-constraints). + +Here are some tests for Rule 8, **port**. + +```test +SET port=1 +EXPECT valid=true +SET port=10 +EXPECT valid=true +SET port=000010 +EXPECT valid=true +SET port=65535 +EXPECT valid=true +SET port=-1 +EXPECT valid=true +SET port=0 +EXPECT valid=false +SET port=-2 +EXPECT valid=false +SET port=65536 +EXPECT valid=false +``` + +### Database, schema, table name constraints + +```test +SET database= +EXPECT valid=yes +SET database=banana +EXPECT valid=yes +SET database=UPPERCASE +EXPECT valid=yes +SET database=_under_score_ +EXPECT valid=yes +SET database=with-dashes +EXPECT valid=yes +``` + +```test +SET database=with/slash +EXPECT valid=no +SET database=-flag +EXPECT valid=no +SET database=with space +EXPECT valid=no +SET database=with.period +EXPECT valid=yes +SET database=with%percent +EXPECT valid=no +SET database=with!exclamation +EXPECT valid=no +SET database=with?questionmark +EXPECT valid=no +``` + +```test +SET database=demo +SET tableschema= +EXPECT valid=yes +SET tableschema=banana +EXPECT valid=yes +SET tableschema=UPPERCASE +EXPECT valid=yes +SET tableschema=_under_score_ +EXPECT valid=yes +SET tableschema=with-dashes +EXPECT valid=yes +``` + +```test +SET database=demo +SET tableschema=with/slash +EXPECT valid=no +SET tableschema=-flag +EXPECT valid=no +SET tableschema=with space +EXPECT valid=no +SET tableschema=with.period +EXPECT valid=yes +SET tableschema=with%percent +EXPECT valid=no +SET tableschema=with!exclamation +EXPECT valid=no +SET tableschema=with?questionmark +EXPECT valid=no +``` + +```test +SET database=demo +SET tableschema=sys +SET table= +EXPECT valid=yes +SET table=banana +EXPECT valid=yes +SET table=UPPERCASE +EXPECT valid=yes +SET table=_under_score_ +EXPECT valid=yes +SET table=with-dashes +EXPECT valid=yes +``` + +```test +SET database=demo +SET tableschema=sys +SET table=with/slash +EXPECT valid=no +SET table=-flag +EXPECT valid=no +SET table=with space +EXPECT valid=no +SET table=with.period +EXPECT valid=yes +SET table=with%percent +EXPECT valid=no +SET table=with!exclamation +EXPECT valid=no +SET table=with?questionmark +EXPECT valid=no +``` + +### Interaction between tls, cert and certhash + +```test +ACCEPT monetdbs:///?cert=/a/path +EXPECT connect_tls_verify=cert +ACCEPT monetdbs:///?certhash=sha256:aa +EXPECT connect_tls_verify=hash +ACCEPT monetdbs:///?cert=/a/path&certhash=sha256:aa +EXPECT connect_tls_verify=hash +REJECT monetdb:///?cert=/a/path +REJECT monetdb:///?certhash=sha256:aa +``` + +```test +SET tls=off +SET cert= +SET certhash= +EXPECT valid=yes +EXPECT connect_tls_verify= +``` + +```test +SET tls=off +SET cert= +SET certhash=sha256:abcdef +EXPECT valid=no +``` + +```test +SET tls=off +SET cert=/foo +SET certhash= +EXPECT valid=no +``` + +```test +SET tls=off +SET cert=/foo +SET certhash=sha256:abcdef +EXPECT valid=no +``` + +```test +SET tls=on +SET cert= +SET certhash= +EXPECT valid=yes +EXPECT connect_tls_verify=system +``` + +```test +SET tls=on +SET cert= +SET certhash=sha256:abcdef +EXPECT valid=yes +EXPECT connect_tls_verify=hash +``` + +```test +SET tls=on +SET cert=/foo +SET certhash= +EXPECT valid=yes +EXPECT connect_tls_verify=cert +``` + +```test +SET tls=on +SET cert=/foo +SET certhash=sha256:abcdef +EXPECT valid=yes +EXPECT connect_tls_verify=hash +``` + + +### Interaction between tls, host, sock and database + +The following tests should exhaustively test all variants. + +```test +ACCEPT monetdb:/// +EXPECT connect_scan=off +EXPECT connect_unix=/tmp/.s.monetdb.50000 +EXPECT connect_tcp=localhost +``` + +```test +ACCEPT monetdb:///?sock=/a/path +EXPECT connect_scan=off +EXPECT connect_unix=/a/path +EXPECT connect_tcp= +``` + +```test +ACCEPT monetdb://localhost/ +EXPECT connect_scan=off +EXPECT connect_unix=/tmp/.s.monetdb.50000 +EXPECT connect_tcp=localhost +``` + +```test +ACCEPT monetdb://localhost/?sock=/a/path +EXPECT connect_scan=off +EXPECT connect_unix=/a/path +EXPECT connect_tcp= +``` + +```test +ACCEPT monetdb://localhost./ +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=localhost +``` + +```test +REJECT monetdb://localhost./?sock=/a/path +``` + +```test +ACCEPT monetdb://not.localhost/ +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=not.localhost +``` + +```test +REJECT monetdb://not.localhost/?sock=/a/path +``` + +```test +ACCEPT monetdbs:/// +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=localhost +``` + +```test +REJECT monetdbs:///?sock=/a/path +``` + +```test +ACCEPT monetdbs://localhost/ +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=localhost +``` + +```test +REJECT monetdbs://localhost/?sock=/a/path +``` + +```test +ACCEPT monetdbs://localhost./ +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=localhost +``` + +```test +REJECT monetdbs://localhost./?sock=/a/path +``` + +```test +ACCEPT monetdbs://not.localhost/ +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=not.localhost +``` + +```test +REJECT monetdbs://not.localhost/?sock=/a/path +``` + +```test +ACCEPT monetdb:///demo +EXPECT connect_scan=on +``` + +```test +ACCEPT monetdb:///demo?sock=/a/path +EXPECT connect_scan=off +EXPECT connect_unix=/a/path +EXPECT connect_tcp= +``` + +```test +ACCEPT monetdb://localhost/demo +EXPECT connect_scan=on +``` + +```test +ACCEPT monetdb://localhost/demo?sock=/a/path +EXPECT connect_scan=off +EXPECT connect_unix=/a/path +EXPECT connect_tcp= +``` + +```test +ACCEPT monetdb://localhost./demo +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=localhost +``` + +```test +REJECT monetdb://localhost./?sock=/a/path +``` + +```test +ACCEPT monetdb://not.localhost/demo +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=not.localhost +``` + +```test +REJECT monetdb://not.localhost/?sock=/a/path +``` + +```test +ACCEPT monetdbs:///demo +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=localhost +``` + +```test +REJECT monetdbs:///?sock=/a/path +``` + +```test +ACCEPT monetdbs://localhost/demo +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=localhost +``` + +```test +REJECT monetdbs://localhost/?sock=/a/path +``` + +```test +ACCEPT monetdbs://localhost./demo +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=localhost +``` + +```test +REJECT monetdbs://localhost./?sock=/a/path +``` + +```test +ACCEPT monetdbs://not.localhost/demo +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=not.localhost +``` + +```test +REJECT monetdbs://not.localhost/?sock=/a/path +``` + +### sock and sockdir + +Sockdir only applies to implicit Unix domain sockets, +not to ones that are given explicitly + +```test +EXPECT sockdir=/tmp +EXPECT port=-1 +EXPECT host= +EXPECT connect_unix=/tmp/.s.monetdb.50000 +SET sockdir=/somewhere/else +EXPECT connect_unix=/somewhere/else/.s.monetdb.50000 +SET port=12345 +EXPECT connect_unix=/somewhere/else/.s.monetdb.12345 +``` + +## Legacy URL's + +```test +REJECT mapi: +REJECT mapi:monetdb +REJECT mapi:monetdb: +REJECT mapi:monetdb:/ +``` + +```test +ACCEPT mapi:monetdb://monet.db:12345/demo +EXPECT host=monet.db +EXPECT port=12345 +EXPECT database=demo +EXPECT tls=off +EXPECT language=sql +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=monet.db +``` + +This one is the golden standard: + +```test +ACCEPT mapi:monetdb://localhost:12345/demo +EXPECT host=localhost +EXPECT port=12345 +EXPECT database=demo +EXPECT tls=off +EXPECT language=sql +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=localhost +``` + +```test +ACCEPT mapi:monetdb://localhost:12345/ +EXPECT host=localhost +EXPECT port=12345 +EXPECT database= +EXPECT tls=off +EXPECT language=sql +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=localhost +``` + +```test +ACCEPT mapi:monetdb://localhost:12345 +EXPECT host=localhost +EXPECT port=12345 +EXPECT database= +EXPECT tls=off +EXPECT language=sql +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=localhost +``` + +```test +ACCEPT mapi:monetdb://localhost/demo +EXPECT connect_scan=false +EXPECT connect_unix= +EXPECT connect_tcp=localhost +EXPECT connect_port=50000 +``` + +```test +ACCEPT mapi:monetdb://:12345/demo +EXPECT host= +EXPECT port=12345 +EXPECT database=demo +EXPECT tls=off +EXPECT language=sql +EXPECT connect_scan=off +EXPECT connect_unix=/tmp/.s.monetdb.12345 +EXPECT connect_tcp=localhost +``` + +```test +ACCEPT mapi:monetdb://127.0.0.1:12345/demo +EXPECT host=127.0.0.1 +EXPECT port=12345 +EXPECT database=demo +EXPECT tls=off +EXPECT language=sql +EXPECT connect_scan=off +EXPECT connect_unix= +EXPECT connect_tcp=127.0.0.1 +``` + +Database parameter allowed, overrides path + +```test +ACCEPT mapi:monetdb://localhost:12345/demo?database=foo +EXPECT database=foo +``` + +User, username and password parameters are ignored: + +```test +SET user=alan +SET password=turing +ACCEPT mapi:monetdb://localhost:12345/demo?user=foo +EXPECT user=alan +EXPECT password=turing +ACCEPT mapi:monetdb://localhost:12345/demo?password=foo +EXPECT user=alan +EXPECT password=turing +``` + +Pymonetdb used to accept user name and password before +the host name and should continue to do so. + + +```test +ONLY pymonetdb +SET user=alan +SET password=turing +ACCEPT mapi:monetdb://foo:bar@localhost:12345/demo +EXPECT user=foo +EXPECT password=bar +ACCEPT mapi:monetdb://banana@localhost:12345/demo +EXPECT user=banana +EXPECT password= +``` + +```test +NOT pymonetdb +SET user=alan +SET password=turing +REJECT mapi:monetdb://foo:bar@localhost:12345/demo +REJECT mapi:monetdb://banana@localhost:12345/demo +``` + +Unix domain sockets + +```test +ACCEPT mapi:monetdb:///path/to/sock?database=demo +EXPECT host= +EXPECT sock=/path/to/sock +EXPECT port=-1 +EXPECT database=demo +EXPECT tls=off +EXPECT language=sql +EXPECT connect_unix=/path/to/sock +EXPECT connect_tcp= +``` + +```test +ACCEPT mapi:monetdb:///path/to/sock +EXPECT host= +EXPECT sock=/path/to/sock +EXPECT port=-1 +EXPECT database= +EXPECT tls=off +EXPECT language=sql +EXPECT connect_unix=/path/to/sock +EXPECT connect_tcp= +``` + +Corner case: both libmapi and pymonetdb interpret this as an attempt +to connect to socket '/'. This will fail of course but the URL does parse + +```test +ACCEPT mapi:monetdb:/// +EXPECT host= +EXPECT sock=/ +EXPECT connect_unix=/ +EXPECT connect_tcp= +``` + +```test +NOT pymonetdb +PARSE mapi:monetdb:///foo:bar@path/to/sock +EXPECT sock=/foo:bar@path/to/sock +REJECT mapi:monetdb://foo:bar@/path/to/sock +``` + +```test +ONLY pymonetdb +SET user=alan +SET password=turing +ACCEPT mapi:monetdb://foo:bar@/path/to/sock +EXPECT host= +EXPECT sock=/path/to/sock +EXPECT user=foo +EXPECT password=bar +EXPECT connect_unix=/path/to/sock +EXPECT connect_tcp= +``` + +```test +ONLY pymonetdb +SET user=alan +SET password=turing +ACCEPT mapi:monetdb://foo@/path/to/sock +EXPECT host= +EXPECT sock=/path/to/sock +EXPECT user=foo +EXPECT password= +EXPECT connect_unix=/path/to/sock +EXPECT connect_tcp= +``` + +Language is supported + +```test +SET language=sql +ACCEPT mapi:monetdb://localhost:12345?language=mal +EXPECT host=localhost +EXPECT sock= +EXPECT language=mal +SET language=sql +ACCEPT mapi:monetdb:///path/to/sock?language=mal +EXPECT host= +EXPECT sock=/path/to/sock +EXPECT language=mal +``` + +No percent decoding is performed + +```test +REJECT mapi:monetdb://localhost:1234%35/demo +PARSE mapi:monetdb://loc%61lhost:12345/d%61tabase +EXPECT host=loc%61lhost +EXPECT database=d%61tabase +EXPECT valid=no +``` + +```test +PARSE mapi:monetdb://localhost:12345/db?database=b%61r&language=m%61l +EXPECT database=b%61r +EXPECT language=m%61l +EXPECT valid=no +``` + +l%61nguage is an unknown parameter, thus ignored not rejected + +```test +SET language=sql +ACCEPT mapi:monetdb://localhost:12345/db?l%61nguage=mal +EXPECT language=sql +ACCEPT mapi:monetdb://localhost:12345/db?_l%61nguage=mal +``` +