diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParser.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParser.java index 617b8d707ce..a908e0766d2 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParser.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParser.java @@ -1,6 +1,8 @@ package datadog.trace.bootstrap.instrumentation.jdbc; import static datadog.trace.bootstrap.instrumentation.jdbc.DBInfo.DEFAULT; +import static datadog.trace.util.IntStringUtils.ALLOW_LEADING_PLUS; +import static datadog.trace.util.IntStringUtils.parseNonNegativeInt; import static java.lang.Math.max; import datadog.trace.api.Pair; @@ -140,7 +142,7 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { final int portLoc = serverName.indexOf(':'); if (portLoc > 1) { - port = Integer.parseInt(serverName.substring(portLoc + 1)); + port = parsePort(serverName, portLoc + 1, serverName.length()); serverName = serverName.substring(0, portLoc); } @@ -221,10 +223,7 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { if (portLoc > 0) { hostEndLoc = portLoc; - try { - builder.port(Integer.parseInt(jdbcUrl.substring(portLoc + 1, dbLoc))); - } catch (final NumberFormatException ignored) { - } + setPort(builder, parsePort(jdbcUrl, portLoc + 1, dbLoc)); } else { hostEndLoc = dbLoc; } @@ -268,10 +267,7 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { if (portLoc > 0) { hostEndLoc = portLoc; final int portEndLoc = clusterSepLoc > 0 ? clusterSepLoc : dbLoc; - try { - builder.port(Integer.parseInt(jdbcUrl.substring(portLoc + 1, portEndLoc))); - } catch (final NumberFormatException ignored) { - } + setPort(builder, parsePort(jdbcUrl, portLoc + 1, portEndLoc)); } else { hostEndLoc = clusterSepLoc > 0 ? clusterSepLoc : dbLoc; } @@ -303,7 +299,7 @@ DBInfo.Builder doParse(String jdbcUrl, final DBInfo.Builder builder) { final Matcher portMatcher = PORT_REGEX.matcher(jdbcUrl); if (portMatcher.find()) { - builder.port(Integer.parseInt(portMatcher.group(1))); + setPort(builder, parsePort(portMatcher.group(1))); } final Matcher userMatcher = USER_REGEX.matcher(jdbcUrl); @@ -390,62 +386,8 @@ DBInfo.Builder doParse(String jdbcUrl, final DBInfo.Builder builder) { ORACLE_CONNECT_INFO() { @Override - DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { - - final String host; - final Integer port; - final String instance; - - final int hostEnd = jdbcUrl.indexOf(':'); - final int instanceLoc = jdbcUrl.indexOf('/'); - if (hostEnd > 0) { - host = jdbcUrl.substring(0, hostEnd); - final int afterHostEnd = jdbcUrl.indexOf(':', hostEnd + 1); - if (afterHostEnd > 0) { - port = Integer.parseInt(jdbcUrl.substring(hostEnd + 1, afterHostEnd)); - instance = jdbcUrl.substring(afterHostEnd + 1); - } else { - if (instanceLoc > 0) { - instance = jdbcUrl.substring(instanceLoc + 1); - port = Integer.parseInt(jdbcUrl.substring(hostEnd + 1, instanceLoc)); - } else { - final String portOrInstance = jdbcUrl.substring(hostEnd + 1); - Integer parsedPort = null; - try { - parsedPort = Integer.parseInt(portOrInstance); - } catch (final NumberFormatException ignored) { - } - if (parsedPort == null) { - port = null; - instance = portOrInstance; - } else { - port = parsedPort; - instance = null; - } - } - } - } else { - if (instanceLoc > 0) { - host = jdbcUrl.substring(0, instanceLoc); - port = null; - instance = jdbcUrl.substring(instanceLoc + 1); - } else { - if (jdbcUrl.isEmpty()) { - return builder; - } else { - host = null; - port = null; - instance = jdbcUrl; - } - } - } - if (host != null) { - builder.host(host); - } - if (port != null) { - builder.port(port); - } - return builder.instance(instance); + DBInfo.Builder doParse(final String connectInfo, final DBInfo.Builder builder) { + return parseOracleConnectInfo(connectInfo, builder, true); } }, @@ -469,17 +411,21 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { } final int hostStart; + final int protocolEnd = connectInfo.indexOf("://"); + // an LDAP name is not EZConnect: it can contain '/', ':' and '?' that are not suffixes + final boolean ldap = connectInfo.startsWith("ldap://") || connectInfo.startsWith("ldaps://"); if (connectInfo.startsWith("//")) { hostStart = "//".length(); - } else if (connectInfo.startsWith("ldap://")) { - hostStart = "ldap://".length(); + } else if (protocolEnd > 0 && protocolEnd == connectInfo.indexOf(':')) { + // protocol prefix, e.g. ldap://, tcp://, tcps:// + hostStart = protocolEnd + "://".length(); } else { hostStart = 0; } if (user != null) { builder.user(user); } - return ORACLE_CONNECT_INFO.doParse(connectInfo.substring(hostStart), builder); + return parseOracleConnectInfo(connectInfo.substring(hostStart), builder, !ldap); } }, @@ -511,7 +457,7 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { final Matcher portMatcher = PORT_REGEX.matcher(urlPart2); if (portMatcher.find()) { - builder.port(Integer.parseInt(portMatcher.group(1))); + setPort(builder, parsePort(portMatcher.group(1))); } final Matcher instanceMatcher = INSTANCE_REGEX.matcher(urlPart2); @@ -697,7 +643,7 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { final int portLoc = url.indexOf(':'); if (portLoc > 0) { host = url.substring(0, portLoc); - builder.port(Integer.parseInt(url.substring(portLoc + 1))); + setPort(builder, parsePort(url, portLoc + 1, url.length())); } else { host = url; } @@ -726,6 +672,10 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { final int protoLoc = jdbcUrl.indexOf("://"); final int typeEndLoc = dbInfo.getType().length(); + if (protoLoc <= typeEndLoc) { + // no "jtds:://" prefix to parse + return builder; + } final String subtype = jdbcUrl.substring(typeEndLoc + 1, protoLoc); builder.subtype(subtype); @@ -740,40 +690,37 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { } } + // [:][/][;=[;...]] final String details = jdbcUrl.substring(protoLoc + "://".length()); - final int hostEndLoc; - final int portLoc = details.indexOf(':', typeEndLoc + 1); - final int dbLoc = details.indexOf('/', typeEndLoc); - final int paramLoc = details.indexOf(';', dbLoc); - - if (paramLoc > 0) { + final int paramLoc = details.indexOf(';'); + final String address; + if (paramLoc >= 0) { populateStandardProperties(builder, splitQuery(details.substring(paramLoc + 1), ';')); - if (dbLoc > 0) { - builder.db(details.substring(dbLoc + 1, paramLoc)); - } + address = details.substring(0, paramLoc); } else { - if (dbLoc > 0) { - builder.db(details.substring(dbLoc + 1)); - } + address = details; } - if (portLoc > 0) { - hostEndLoc = portLoc; - final int portEndLoc = dbLoc > 0 ? dbLoc : (paramLoc > 0 ? paramLoc : details.length()); - try { - builder.port(Integer.parseInt(details.substring(portLoc + 1, portEndLoc))); - } catch (final NumberFormatException ignored) { - } - } else if (dbLoc > 0) { - hostEndLoc = dbLoc; - } else if (paramLoc > 0) { - hostEndLoc = paramLoc; + final int dbLoc = address.indexOf('/'); + final String hostAndPort; + if (dbLoc >= 0) { + builder.db(address.substring(dbLoc + 1)); + hostAndPort = address.substring(0, dbLoc); } else { - hostEndLoc = details.length(); + hostAndPort = address; } - builder.host(details.substring(0, hostEndLoc)); + // skip a bracketed IPv6 literal so its colons are not taken as the port separator + final int hostSearchStart = + hostAndPort.startsWith("[") ? Math.max(hostAndPort.indexOf(']'), 0) : 0; + final int portLoc = hostAndPort.indexOf(':', hostSearchStart); + if (portLoc >= 0) { + setPort(builder, parsePort(hostAndPort, portLoc + 1, hostAndPort.length())); + builder.host(hostAndPort.substring(0, portLoc)); + } else { + builder.host(hostAndPort); + } return builder; } @@ -898,6 +845,124 @@ public static DBInfo parse(String connectionUrl, final Properties props) { } } + /** + * Parses an Oracle connect string after any {@code @} and protocol prefix. + * + * @param ezConnect whether the service may carry EZConnect suffixes to drop + */ + private static DBInfo.Builder parseOracleConnectInfo( + final String connectInfo, final DBInfo.Builder builder, final boolean ezConnect) { + + final String host; + final Integer port; + final String instance; + + // EZConnect Plus parameters (?key=value) are not part of the address + final int queryLoc = ezConnect ? connectInfo.indexOf('?') : -1; + final String jdbcUrl = queryLoc >= 0 ? connectInfo.substring(0, queryLoc) : connectInfo; + + // skip a bracketed IPv6 literal so its colons are not taken as the port separator + final int hostSearchStart = jdbcUrl.startsWith("[") ? Math.max(jdbcUrl.indexOf(']'), 0) : 0; + final int instanceLoc = jdbcUrl.indexOf('/', hostSearchStart); + final int colonLoc = jdbcUrl.indexOf(':', hostSearchStart); + // a ':' after the service name separates the server type, not the port + final int hostEnd = instanceLoc >= 0 && colonLoc > instanceLoc ? -1 : colonLoc; + if (hostEnd > 0) { + host = jdbcUrl.substring(0, hostEnd); + final int afterHostEnd = jdbcUrl.indexOf(':', hostEnd + 1); + if (afterHostEnd > 0 && (instanceLoc < 0 || afterHostEnd < instanceLoc)) { + // host:port:sid + port = parsePort(jdbcUrl, hostEnd + 1, afterHostEnd); + instance = jdbcUrl.substring(afterHostEnd + 1); + } else { + if (instanceLoc > 0) { + // host:port/service[:server_type][/instance_name] + instance = serviceName(jdbcUrl.substring(instanceLoc + 1), ezConnect); + port = parsePort(jdbcUrl, hostEnd + 1, instanceLoc); + } else { + final String portOrInstance = jdbcUrl.substring(hostEnd + 1); + final Integer parsedPort = parsePort(portOrInstance); + if (parsedPort == null) { + port = null; + instance = portOrInstance; + } else { + port = parsedPort; + instance = null; + } + } + } + } else { + if (instanceLoc > 0) { + host = jdbcUrl.substring(0, instanceLoc); + port = null; + instance = serviceName(jdbcUrl.substring(instanceLoc + 1), ezConnect); + } else { + if (jdbcUrl.isEmpty()) { + return builder; + } else { + host = null; + port = null; + instance = jdbcUrl; + } + } + } + if (host != null) { + builder.host(host); + } + if (port != null) { + builder.port(port); + } + return builder.instance(instance); + } + + /** + * Drops the optional {@code :server_type} and {@code /instance_name} EZConnect suffixes. Other + * forms, such as an LDAP distinguished name, can contain {@code :} and {@code /}, so they are + * kept whole. + */ + private static String serviceName(final String service, final boolean ezConnect) { + if (!ezConnect) { + return service; + } + int end = service.length(); + final int serverTypeLoc = service.indexOf(':'); + if (serverTypeLoc >= 0) { + end = serverTypeLoc; + } + final int instanceNameLoc = service.indexOf('/'); + if (instanceNameLoc >= 0 && instanceNameLoc < end) { + end = instanceNameLoc; + } + return service.substring(0, end); + } + + /** + * A leading {@code '+'} is not legal in a port by spec (RFC 3986's {@code port = *DIGIT}), but it + * was accepted here through {@link Integer#parseInt}, and drivers that parse ports the same way + * connect to {@code +1444} as port 1444. Accepting it keeps the tagged port accurate. + * + * @return the port, or {@code null} if {@code s[start, end)} is not a valid port number + */ + private static Integer parsePort(final CharSequence s, final int start, final int end) { + final int port = parseNonNegativeInt(s, start, end - start, ALLOW_LEADING_PLUS); + return port >= 0 ? port : null; + } + + /** + * @return the port, or {@code null} if {@code s} is null or not a valid port number + */ + private static Integer parsePort(final String s) { + final int port = parseNonNegativeInt(s, ALLOW_LEADING_PLUS); + return port >= 0 ? port : null; + } + + /** Sets the port only when one was parsed, keeping any default already on the builder. */ + private static void setPort(final DBInfo.Builder builder, final Integer port) { + if (port != null) { + builder.port(port); + } + } + // Source: https://stackoverflow.com/a/13592567 @SuppressForbidden private static Map splitQuery(final String query, final char separator) { @@ -956,12 +1021,7 @@ private static void populateStandardProperties( } if (props.containsKey("portnumber")) { - final String portNumber = (String) props.get("portnumber"); - try { - builder.port(Integer.parseInt(portNumber)); - } catch (final NumberFormatException e) { - ExceptionLogger.LOGGER.debug("Error parsing portnumber property: {}", portNumber, e); - } + setPort(builder, parsePort((String) props.get("portnumber"))); } if (props.containsKey("servicename")) { // this property is used to specify the db to use for Sybase connection strings @@ -969,12 +1029,7 @@ private static void populateStandardProperties( } if (props.containsKey("portNumber")) { - final String portNumber = (String) props.get("portNumber"); - try { - builder.port(Integer.parseInt(portNumber)); - } catch (final NumberFormatException e) { - ExceptionLogger.LOGGER.debug("Error parsing portNumber property: {}", portNumber, e); - } + setPort(builder, parsePort((String) props.get("portNumber"))); } } } diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserJtdsTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserJtdsTest.java new file mode 100644 index 00000000000..67966023576 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserJtdsTest.java @@ -0,0 +1,37 @@ +package datadog.trace.bootstrap.instrumentation.jdbc; + +import static datadog.trace.bootstrap.instrumentation.jdbc.JDBCConnectionUrlParser.extractDBInfo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.tabletest.junit.TableTest; + +/** + * Tests for jTDS URLs ({@code jdbc:jtds:://[:][/][;]}). + * + *

The port and database separators used to be searched for across the whole remainder of the + * URL, starting at an offset unrelated to it, so a ':' or '/' in a property value or a short host + * name produced a {@code StringIndexOutOfBoundsException} or a wrong host. + */ +class JDBCConnectionUrlParserJtdsTest { + + @TableTest({ + "scenario | url | subtype | host | port | db ", + "host port and db | jdbc:jtds:sqlserver://dbhost:1500/mydb | sqlserver | dbhost | 1500 | mydb", + "colon in property after db | jdbc:jtds:sqlserver://dbhost:1500/mydb;appname=a:b | sqlserver | dbhost | 1500 | mydb", + "colon in property, no port | jdbc:jtds:sqlserver://dbhost/mydb;password=p:w | sqlserver | dbhost | 1433 | mydb", + "colon in property, no db | jdbc:jtds:sqlserver://dbhost;password=p:w | sqlserver | dbhost | 1433 | ", + "slash in property, no db | jdbc:jtds:sqlserver://dbhost;password=p/w | sqlserver | dbhost | 1433 | ", + "short host with port | jdbc:jtds:sqlserver://db:1500/mydb | sqlserver | db | 1500 | mydb", + "sybase with port | jdbc:jtds:sybase://dbhost:7200/mydb | sybase | dbhost | 7200 | mydb", + "IPv6 literal with port | jdbc:jtds:sqlserver://[::1]:1500/mydb | sqlserver | '[::1]' | 1500 | mydb", + "missing protocol separator | jdbc:jtds:sqlserver:dbhost | | | | " + }) + void parsesJtdsUrls(String url, String subtype, String host, Integer port, String db) { + DBInfo info = extractDBInfo(url, null); + assertEquals("jtds", info.getType()); + assertEquals(subtype, info.getSubtype()); + assertEquals(host, info.getHost()); + assertEquals(port, info.getPort()); + assertEquals(db, info.getDb()); + } +} diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserOracleTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserOracleTest.java new file mode 100644 index 00000000000..cf88717116b --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserOracleTest.java @@ -0,0 +1,54 @@ +package datadog.trace.bootstrap.instrumentation.jdbc; + +import static datadog.trace.bootstrap.instrumentation.jdbc.JDBCConnectionUrlParser.extractDBInfo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +/** + * Tests for Oracle {@code @} connect strings that previously threw {@code NumberFormatException}. + * + *

A second ':' after the port (EZConnect {@code :server_type}, IPv6 literals, {@code ?params}) + * or a protocol prefix other than {@code ldap://} was taken as the {@code host:port:sid} form, so a + * non-numeric segment was parsed as the port and the whole URL fell back to defaults. + */ +class JDBCConnectionUrlParserOracleTest { + + @TableTest({ + "scenario | url | host | port | instance", + "SID form | jdbc:oracle:thin:@orcl.host:55:orclsn | orcl.host | 55 | orclsn ", + "service form | jdbc:oracle:thin:@//orcl.host:55/orclsn | orcl.host | 55 | orclsn ", + "service with server type | jdbc:oracle:thin:@//orcl.host:55/orclsn:dedicated | orcl.host | 55 | orclsn ", + "service with type and instance | jdbc:oracle:thin:@//orcl.host:55/orclsn:pooled/inst1 | orcl.host | 55 | orclsn ", + "service with instance name | jdbc:oracle:thin:@//orcl.host:55/orclsn/inst1 | orcl.host | 55 | orclsn ", + "service without port | jdbc:oracle:thin:@//orcl.host/orclsn:dedicated | orcl.host | 1521 | orclsn ", + "tcps protocol | jdbc:oracle:thin:@tcps://orcl.host:2484/orclsn | orcl.host | 2484 | orclsn ", + "tcp protocol | jdbc:oracle:thin:@tcp://orcl.host:55/orclsn | orcl.host | 55 | orclsn ", + "EZConnect Plus params | jdbc:oracle:thin:@tcps://orcl.host:2484/orclsn?wallet_location=c:/w | orcl.host | 2484 | orclsn ", + "IPv6 literal | jdbc:oracle:thin:@//[::1]:55/orclsn | '[::1]' | 55 | orclsn ", + "non-numeric port | jdbc:oracle:thin:@orcl.host:abc:orclsn | orcl.host | 1521 | orclsn ", + "leading plus on port | jdbc:oracle:thin:@orcl.host:+55:orclsn | orcl.host | 55 | orclsn " + }) + void parsesOracleConnectStrings(String url, String host, Integer port, String instance) { + DBInfo info = extractDBInfo(url, null); + assertEquals("oracle", info.getType()); + assertEquals("thin", info.getSubtype()); + assertEquals(host, info.getHost()); + assertEquals(port, info.getPort()); + assertEquals(instance, info.getInstance()); + } + + @Test + void keepsAnLdapDistinguishedNameWhole() { + // '/' and ':' are legal inside an LDAP name; only EZConnect suffixes are stripped + DBInfo info = + extractDBInfo( + "jdbc:oracle:thin:@ldap://orcl.host:389/cn=orcl,cn=OracleContext,ou=R/D,dc=example,dc=com", + null); + assertEquals("orcl.host", info.getHost()); + assertEquals(389, info.getPort()); + assertEquals("cn=orcl,cn=oraclecontext,ou=r/d,dc=example,dc=com", info.getInstance()); + assertEquals(info.getInstance(), info.getDb()); + } +} diff --git a/internal-api/src/main/java/datadog/trace/util/IntStringUtils.java b/internal-api/src/main/java/datadog/trace/util/IntStringUtils.java new file mode 100644 index 00000000000..7ade00f3f27 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/util/IntStringUtils.java @@ -0,0 +1,59 @@ +package datadog.trace.util; + +import javax.annotation.Nullable; + +/** Exception-free {@code int} parsing, the {@code int} counterpart of {@code LongStringUtils}. */ +public final class IntStringUtils { + + /** + * Accept a single leading {@code '+'}, as {@link Integer#parseInt(String)} does. Not every + * grammar allows it, but callers that must match lenient parsers elsewhere may need it. + */ + public static final boolean ALLOW_LEADING_PLUS = true; + + /** Accept ASCII digits only; any sign makes the input invalid. */ + public static final boolean REJECT_LEADING_PLUS = false; + + private IntStringUtils() {} + + /** + * Parses a non-negative decimal {@code int} from the whole sequence without throwing. + * + * @param allowLeadingPlus {@link #ALLOW_LEADING_PLUS} or {@link #REJECT_LEADING_PLUS} + * @return the parsed value, or {@code -1} if {@code s} is null or empty, contains anything other + * than ASCII digits (apart from an allowed leading {@code '+'}), or overflows {@code int} + */ + public static int parseNonNegativeInt(@Nullable CharSequence s, boolean allowLeadingPlus) { + return s == null ? -1 : parseNonNegativeInt(s, 0, s.length(), allowLeadingPlus); + } + + /** + * Parses a non-negative decimal {@code int} from the {@code len} characters of {@code s} starting + * at {@code start}, without throwing or allocating. + * + * @param allowLeadingPlus {@link #ALLOW_LEADING_PLUS} or {@link #REJECT_LEADING_PLUS} + * @return the parsed value, or {@code -1} if the range is empty or out of bounds, contains + * anything other than ASCII digits (apart from an allowed leading {@code '+'}), or overflows + * {@code int} + */ + public static int parseNonNegativeInt( + @Nullable CharSequence s, int start, int len, boolean allowLeadingPlus) { + if (s == null || start < 0 || len <= 0 || start > s.length() - len) { + return -1; + } + int i = start; + final int end = start + len; + if (allowLeadingPlus && s.charAt(i) == '+' && ++i == end) { + return -1; // a sign with no digits + } + int result = 0; + for (; i < end; i++) { + int digit = s.charAt(i) - '0'; + if (digit < 0 || digit > 9 || result > (Integer.MAX_VALUE - digit) / 10) { + return -1; + } + result = result * 10 + digit; + } + return result; + } +} diff --git a/internal-api/src/test/java/datadog/trace/util/IntStringUtilsTest.java b/internal-api/src/test/java/datadog/trace/util/IntStringUtilsTest.java new file mode 100644 index 00000000000..07e3a2515d1 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/util/IntStringUtilsTest.java @@ -0,0 +1,72 @@ +package datadog.trace.util; + +import static datadog.trace.util.IntStringUtils.ALLOW_LEADING_PLUS; +import static datadog.trace.util.IntStringUtils.REJECT_LEADING_PLUS; +import static datadog.trace.util.IntStringUtils.parseNonNegativeInt; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +class IntStringUtilsTest { + + @TableTest({ + "scenario | input | expected ", + "zero | 0 | 0 ", + "port | 1521 | 1521 ", + "leading zeros | 007 | 7 ", + "int max | 2147483647 | 2147483647", + "int max plus one | 2147483648 | -1 ", + "far overflow | 99999999999 | -1 ", + "empty | '' | -1 ", + "minus sign | -1 | -1 ", + "plus sign | +1 | -1 ", + "letters | abc | -1 ", + "trailing garbage | 12a | -1 ", + "whitespace | ' 1' | -1 ", + "non-ASCII digit | '\u0661' | -1 " + }) + void parseNonNegativeIntWholeSequence(String input, int expected) { + assertEquals(expected, parseNonNegativeInt(input, REJECT_LEADING_PLUS)); + } + + @TableTest({ + "scenario | input | start | len | expected", + "middle of string | host:1521/db | 5 | 4 | 1521 ", + "to end | host:1521 | 5 | 4 | 1521 ", + "empty range | host:/db | 5 | 0 | -1 ", + "negative length | host:1521 | 6 | -1 | -1 ", + "negative start | 1521 | -1 | 4 | -1 ", + "past end | 1521 | 1 | 4 | -1 ", + "start past length | 1521 | 5 | 1 | -1 ", + "range spans garbage | host:1521/db | 4 | 5 | -1 " + }) + void parseNonNegativeIntRange(String input, int start, int len, int expected) { + assertEquals(expected, parseNonNegativeInt(input, start, len, REJECT_LEADING_PLUS)); + } + + @TableTest({ + "scenario | input | start | len | expected", + "plus then digits | +1444 | 0 | 5 | 1444 ", + "plus inside range | host:+1521/x | 5 | 5 | 1521 ", + "digits only | 1521 | 0 | 4 | 1521 ", + "plus only | + | 0 | 1 | -1 ", + "two pluses | ++1 | 0 | 3 | -1 ", + "minus still bad | -1 | 0 | 2 | -1 ", + "plus not leading | 1+1 | 0 | 3 | -1 ", + "plus then overflow | +2147483648 | 0 | 11 | -1 " + }) + void parseNonNegativeIntAllowingLeadingPlus(String input, int start, int len, int expected) { + assertEquals(expected, parseNonNegativeInt(input, start, len, ALLOW_LEADING_PLUS)); + if (start == 0 && len == input.length()) { + assertEquals(expected, parseNonNegativeInt(input, ALLOW_LEADING_PLUS)); + } + } + + @Test + void parseNonNegativeIntNull() { + assertEquals(-1, parseNonNegativeInt(null, REJECT_LEADING_PLUS)); + assertEquals(-1, parseNonNegativeInt(null, 0, 0, REJECT_LEADING_PLUS)); + assertEquals(-1, parseNonNegativeInt(null, ALLOW_LEADING_PLUS)); + } +}