TextToHtml: Change UriParser interface

This commit is contained in:
cketti 2018-01-28 06:18:53 +01:00
parent 6767f41505
commit ace7557a6c
13 changed files with 376 additions and 268 deletions

View file

@ -4,26 +4,26 @@ package com.fsck.k9.message.html;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class BitcoinUriParser implements UriParser {
private static final Pattern BITCOIN_URI_PATTERN =
Pattern.compile("bitcoin:[1-9a-km-zA-HJ-NP-Z]{27,34}(\\?[a-zA-Z0-9$\\-_.+!*'(),%:@&=]*)?");
@Nullable
@Override
public int linkifyUri(String text, int startPos, StringBuffer outputBuffer) {
public UriMatch parseUri(@NotNull CharSequence text, int startPos) {
Matcher matcher = BITCOIN_URI_PATTERN.matcher(text);
if (!matcher.find(startPos) || matcher.start() != startPos) {
return startPos;
return null;
}
String bitcoinUri = matcher.group();
outputBuffer.append("<a href=\"")
.append(bitcoinUri)
.append("\">")
.append(bitcoinUri)
.append("</a>");
return matcher.end();
int startIndex = matcher.start();
int endIndex = matcher.end();
CharSequence uri = text.subSequence(startIndex, endIndex);
return new UriMatch(startIndex, endIndex, uri);
}
}

View file

@ -4,6 +4,10 @@ package com.fsck.k9.message.html;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Parses ERC-67 URIs
* https://github.com/ethereum/EIPs/issues/67
@ -12,21 +16,18 @@ class EthereumUriParser implements UriParser {
private static final Pattern ETHEREUM_URI_PATTERN =
Pattern.compile("ethereum:0x[0-9a-fA-F]*(\\?[a-zA-Z0-9$\\-_.+!*'(),%:@&=]*)?");
@Nullable
@Override
public int linkifyUri(String text, int startPos, StringBuffer outputBuffer) {
public UriMatch parseUri(@NotNull CharSequence text, int startPos) {
Matcher matcher = ETHEREUM_URI_PATTERN.matcher(text);
if (!matcher.find(startPos) || matcher.start() != startPos) {
return startPos;
return null;
}
String ethereumURI = matcher.group();
outputBuffer.append("<a href=\"")
.append(ethereumURI)
.append("\">")
.append(ethereumURI)
.append("</a>");
return matcher.end();
int startIndex = matcher.start();
int endIndex = matcher.end();
CharSequence uri = text.subSequence(startIndex, endIndex);
return new UriMatch(startIndex, endIndex, uri);
}
}

View file

@ -4,9 +4,12 @@ package com.fsck.k9.message.html;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Parses and "linkifies" http links.
* Parses http/https/rtsp URIs
* <p>
* This class is in parts inspired by OkHttp's
* <a href="https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/HttpUrl.java">HttpUrl</a>.
@ -16,6 +19,7 @@ import java.util.regex.Pattern;
class HttpUriParser implements UriParser {
// This string represent character group sub-delim as described in RFC 3986
private static final String SUB_DELIM = "!$&'()*+,;=";
private static final Pattern SCHEME_PATTERN = Pattern.compile("(https?|rtsp)://", Pattern.CASE_INSENSITIVE);
private static final Pattern DOMAIN_PATTERN =
Pattern.compile("[\\da-z](?:[\\da-z-]*[\\da-z])*(?:\\.[\\da-z](?:[\\da-z-]*[\\da-z])*)*(?::(\\d{0,5}))?",
Pattern.CASE_INSENSITIVE);
@ -23,27 +27,20 @@ class HttpUriParser implements UriParser {
Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})(:(\\d{0,5}))?");
@Nullable
@Override
public int linkifyUri(String text, int startPos, StringBuffer outputBuffer) {
int currentPos = startPos;
// Scheme
String shortScheme = text.substring(currentPos, Math.min(currentPos + 7, text.length()));
String longScheme = text.substring(currentPos, Math.min(currentPos + 8, text.length()));
if (longScheme.equalsIgnoreCase("https://")) {
currentPos += "https://".length();
} else if (shortScheme.equalsIgnoreCase("http://")) {
currentPos += "http://".length();
} else if (shortScheme.equalsIgnoreCase("rtsp://")) {
currentPos += "rtsp://".length();
} else {
return startPos;
public UriMatch parseUri(@NotNull CharSequence text, int startPos) {
Matcher schemeMatcher = SCHEME_PATTERN.matcher(text);
if (!schemeMatcher.find(startPos) || schemeMatcher.start() != startPos) {
return null;
}
int currentPos = schemeMatcher.end();
// Authority
int matchedAuthorityEnd = tryMatchAuthority(text, currentPos);
if (matchedAuthorityEnd == currentPos) {
return startPos;
return null;
}
currentPos = matchedAuthorityEnd;
@ -62,18 +59,12 @@ class HttpUriParser implements UriParser {
currentPos = matchUnreservedPCTEncodedSubDelimClassesGreedy(text, currentPos + 1, ":@/?");
}
String httpUri = text.substring(startPos, currentPos);
outputBuffer.append("<a href=\"")
.append(httpUri)
.append("\">")
.append(httpUri)
.append("</a>");
return currentPos;
CharSequence uri = text.subSequence(startPos, currentPos);
return new UriMatch(startPos, currentPos, uri);
}
private int tryMatchAuthority(String text, int startPos) {
int authorityLimit = text.indexOf('/', startPos);
private int tryMatchAuthority(CharSequence text, int startPos) {
int authorityLimit = indexOf(text, '/', startPos);
if (authorityLimit == -1) {
authorityLimit = text.length();
}
@ -97,8 +88,8 @@ class HttpUriParser implements UriParser {
return startPos;
}
private int tryMatchUserInfo(String text, int startPos, int limit) {
int userInfoEnd = text.indexOf('@', startPos);
private int tryMatchUserInfo(CharSequence text, int startPos, int limit) {
int userInfoEnd = indexOf(text, '@', startPos);
if (userInfoEnd != -1 && userInfoEnd < limit) {
if (matchUnreservedPCTEncodedSubDelimClassesGreedy(text, startPos, ":") != userInfoEnd) {
// Illegal character in user info
@ -109,7 +100,7 @@ class HttpUriParser implements UriParser {
return startPos;
}
private int tryMatchDomainName(String text, int startPos) {
private int tryMatchDomainName(CharSequence text, int startPos) {
try {
Matcher matcher = DOMAIN_PATTERN.matcher(text);
if (!matcher.find(startPos) || matcher.start() != startPos) {
@ -130,7 +121,7 @@ class HttpUriParser implements UriParser {
}
}
private int tryMatchIpv4Address(String text, int startPos, boolean portAllowed) {
private int tryMatchIpv4Address(CharSequence text, int startPos, boolean portAllowed) {
Matcher matcher = IPv4_PATTERN.matcher(text);
if (!matcher.find(startPos) || matcher.start() != startPos) {
return startPos;
@ -158,12 +149,12 @@ class HttpUriParser implements UriParser {
return matcher.end();
}
private int tryMatchIpv6Address(String text, int startPos) {
if (startPos == text.length() || text.codePointAt(startPos) != '[') {
private int tryMatchIpv6Address(CharSequence text, int startPos) {
if (startPos == text.length() || text.charAt(startPos) != '[') {
return startPos;
}
int addressEnd = text.indexOf(']');
int addressEnd = indexOf(text, ']', startPos);
if (addressEnd == -1) {
return startPos;
}
@ -174,13 +165,13 @@ class HttpUriParser implements UriParser {
int endSegmentsCount = 0;
// Handle :: separator and segments in front of it
int compressionPos = text.indexOf("::");
int compressionPos = indexOf(text, "::", currentPos);
boolean compressionEnabled = compressionPos != -1 && compressionPos < addressEnd;
if (compressionEnabled) {
while (currentPos < compressionPos) {
// Check segment separator
if (beginSegmentsCount > 0) {
if (text.codePointAt(currentPos) != ':') {
if (text.charAt(currentPos) != ':') {
return startPos;
} else {
++currentPos;
@ -204,7 +195,7 @@ class HttpUriParser implements UriParser {
while (currentPos < addressEnd && (beginSegmentsCount + endSegmentsCount) < 8) {
// Check segment separator
if (endSegmentsCount > 0) {
if (text.codePointAt(currentPos) != ':') {
if (text.charAt(currentPos) != ':') {
return startPos;
} else {
++currentPos;
@ -212,7 +203,7 @@ class HttpUriParser implements UriParser {
}
// Small look ahead, do not run into IPv4 tail (7 is IPv4 minimum length)
int nextColon = text.indexOf(':', currentPos);
int nextColon = indexOf(text, ':', currentPos);
if ((nextColon == -1 || nextColon > addressEnd) && (addressEnd - currentPos) >= 7) {
break;
}
@ -246,14 +237,14 @@ class HttpUriParser implements UriParser {
}
// Check optional port
if (currentPos == text.length() || text.codePointAt(currentPos) != ':') {
if (currentPos == text.length() || text.charAt(currentPos) != ':') {
return currentPos;
}
++currentPos;
int port = 0;
for (; currentPos < text.length(); currentPos++) {
int c = text.codePointAt(currentPos);
int c = text.charAt(currentPos);
if (c < '0' || c > '9') {
break;
}
@ -262,21 +253,22 @@ class HttpUriParser implements UriParser {
return (port <= 65535) ? currentPos : startPos;
}
private int parse16BitHexSegment(String text, int startPos, int endPos) {
private int parse16BitHexSegment(CharSequence text, int startPos, int endPos) {
int currentPos = startPos;
while (isHexDigit(text.codePointAt(currentPos)) && currentPos < endPos) {
while (isHexDigit(text.charAt(currentPos)) && currentPos < endPos) {
++currentPos;
}
return currentPos;
}
private int matchUnreservedPCTEncodedSubDelimClassesGreedy(String text, int startPos, String additionalCharacters) {
private int matchUnreservedPCTEncodedSubDelimClassesGreedy(CharSequence text, int startPos,
String additionalCharacters) {
String allowedCharacters = SUB_DELIM + "-._~" + additionalCharacters;
int currentPos;
int shouldBeHex = 0;
for (currentPos = startPos; currentPos < text.length(); currentPos++) {
int c = text.codePointAt(currentPos);
int c = text.charAt(currentPos);
if (isHexDigit(c)) {
shouldBeHex = Math.max(shouldBeHex - 1, 0);
@ -299,4 +291,35 @@ class HttpUriParser implements UriParser {
private boolean isHexDigit(int c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
}
private int indexOf(CharSequence text, char ch, int fromIndex) {
for (int i = fromIndex, end = text.length(); i < end; i++) {
if (text.charAt(i) == ch) {
return i;
}
}
return -1;
}
private int indexOf(CharSequence text, String str, int fromIndex) {
char ch = str.charAt(0);
for (int i = fromIndex, end = text.length(); i < end; i++) {
if (text.charAt(i) == ch) {
boolean found = true;
for (int j = 1, strLen = str.length(); j < strLen; j++) {
if (text.charAt(i + j) != str.charAt(j)) {
found = false;
break;
}
}
if (found) {
return i;
}
}
}
return -1;
}
}

View file

@ -1,64 +0,0 @@
package com.fsck.k9.message.html;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.text.TextUtils;
public class UriLinkifier {
private static final Pattern URI_SCHEME;
private static final Map<String, UriParser> SUPPORTED_URIS;
private static final String SCHEME_SEPARATORS = " (\\n>";
private static final String ALLOWED_SEPARATORS_PATTERN = "(?:^|[" + SCHEME_SEPARATORS + "])";
static {
SUPPORTED_URIS = new HashMap<>();
SUPPORTED_URIS.put("ethereum:", new EthereumUriParser());
SUPPORTED_URIS.put("bitcoin:", new BitcoinUriParser());
UriParser httpParser = new HttpUriParser();
SUPPORTED_URIS.put("http:", httpParser);
SUPPORTED_URIS.put("https:", httpParser);
SUPPORTED_URIS.put("rtsp:", httpParser);
String allSchemes = TextUtils.join("|", SUPPORTED_URIS.keySet());
String pattern = ALLOWED_SEPARATORS_PATTERN + "(" + allSchemes + ")";
URI_SCHEME = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
}
public static void linkifyText(String text, StringBuffer outputBuffer) {
int currentPos = 0;
Matcher matcher = URI_SCHEME.matcher(text);
while (matcher.find(currentPos)) {
int startPos = matcher.start(1);
String textBeforeMatch = text.substring(currentPos, startPos);
outputBuffer.append(textBeforeMatch);
String scheme = matcher.group(1).toLowerCase(Locale.US);
UriParser parser = SUPPORTED_URIS.get(scheme);
int newPos = parser.linkifyUri(text, startPos, outputBuffer);
boolean uriWasNotLinkified = newPos <= startPos;
if (uriWasNotLinkified) {
outputBuffer.append(text.charAt(startPos));
currentPos = startPos + 1;
} else {
currentPos = (newPos > currentPos) ? newPos : currentPos + 1;
}
if (currentPos >= text.length()) {
break;
}
}
String textAfterLastMatch = text.substring(currentPos);
outputBuffer.append(textAfterLastMatch);
}
}

View file

@ -0,0 +1,31 @@
package com.fsck.k9.message.html
@Deprecated("Helper to be able to transition to the new text to HTML conversion in smaller steps")
object UriLinkifier {
@JvmStatic
fun linkifyText(text: String, html: StringBuffer) {
val uriMatches = UriMatcher.findUris(text)
var currentIndex = 0
uriMatches.forEach { uriMatch ->
append(html, text, currentIndex, uriMatch.startIndex)
html.append("<a href=\"")
html.append(uriMatch.uri)
html.append("\">")
html.append(uriMatch.uri)
html.append("</a>")
currentIndex = uriMatch.endIndex
}
append(html, text, currentIndex, text.length)
}
private fun append(html: StringBuffer, text: String, startIndex: Int, endIndex: Int) {
for (i in startIndex until endIndex) {
html.append(text[i])
}
}
}

View file

@ -0,0 +1,7 @@
package com.fsck.k9.message.html
data class UriMatch(
val startIndex: Int,
val endIndex: Int,
val uri: CharSequence
)

View file

@ -0,0 +1,35 @@
package com.fsck.k9.message.html
import java.util.*
object UriMatcher {
private val SUPPORTED_URIS = { httpUriParser: HttpUriParser ->
mapOf(
"ethereum:" to EthereumUriParser(),
"bitcoin:" to BitcoinUriParser(),
"http:" to httpUriParser,
"https:" to httpUriParser,
"rtsp:" to httpUriParser
)
}.invoke(HttpUriParser())
// FIXME: Remove > once the text to HTML code has been replaced
private const val SCHEME_SEPARATORS = " (\\n<>"
private const val ALLOWED_SEPARATORS_PATTERN = "(?:^|[$SCHEME_SEPARATORS])"
private val URI_SCHEME = Regex(
"$ALLOWED_SEPARATORS_PATTERN(${ SUPPORTED_URIS.keys.joinToString("|") })",
RegexOption.IGNORE_CASE
)
fun findUris(text: CharSequence): List<UriMatch> {
return URI_SCHEME.findAll(text).map { matchResult ->
val matchGroup = matchResult.groups[1]!!
val startIndex = matchGroup.range.start
val scheme = matchGroup.value.toLowerCase(Locale.ROOT)
val parser = SUPPORTED_URIS[scheme] ?: throw AssertionError("Scheme not found: $scheme")
parser.parseUri(text, startIndex)
}.filterNotNull().toList()
}
}

View file

@ -1,19 +0,0 @@
package com.fsck.k9.message.html;
public interface UriParser {
/**
* Parse and linkify scheme specific URI beginning from given position. The result will be written to given buffer.
*
* @param text
* String to parse URI from.
* @param startPos
* Position where URI starts (first letter of scheme).
* @param outputBuffer
* Buffer where linkified variant of URI is written to.
*
* @return Index where parsed URI ends (first non-URI letter). Should be {@code startPos} or smaller if no valid
* URI was found.
*/
int linkifyUri(String text, int startPos, StringBuffer outputBuffer);
}

View file

@ -0,0 +1,14 @@
package com.fsck.k9.message.html
internal interface UriParser {
/**
* Parse scheme specific URI beginning from given position.
*
* @param text String to parse URI from.
* @param startPos Position where URI starts (first letter of scheme).
*
* @return [UriMatch] if a valid URI was found. `null` otherwise.
*/
fun parseUri(text: CharSequence, startPos: Int): UriMatch?
}

View file

@ -3,28 +3,28 @@ package com.fsck.k9.message.html;
import org.junit.Test;
import static com.fsck.k9.message.html.UriParserTestHelper.assertLinkOnly;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
public class BitcoinUriParserTest {
BitcoinUriParser parser = new BitcoinUriParser();
StringBuffer outputBuffer = new StringBuffer();
@Test
public void basicBitcoinUri() throws Exception {
assertLinkify("bitcoin:19W6QZkx8SYPG7BBCS7odmWGRxqRph5jFU");
assertValidUri("bitcoin:19W6QZkx8SYPG7BBCS7odmWGRxqRph5jFU");
}
@Test
public void bitcoinUriWithAmount() throws Exception {
assertLinkify("bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2");
assertValidUri("bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2");
}
@Test
public void bitcoinUriWithQueryParameters() throws Exception {
assertLinkify("bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2" +
assertValidUri("bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2" +
"&message=Payment&label=Satoshi&extra=other-param");
}
@ -34,51 +34,40 @@ public class BitcoinUriParserTest {
String uri = "bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2";
String text = prefix + uri;
parser.linkifyUri(text, prefix.length(), outputBuffer);
UriMatch uriMatch = parser.parseUri(text, prefix.length());
assertLinkOnly(uri, outputBuffer);
assertUriMatch(uri, uriMatch, prefix.length());
}
@Test
public void invalidScheme() throws Exception {
assertNotLinkify("bitcion:19W6QZkx8SYPG7BBCS7odmWGRxqRph5jFU");
assertInvalidUri("bitcion:19W6QZkx8SYPG7BBCS7odmWGRxqRph5jFU");
}
@Test
public void invalidAddress() throws Exception {
assertNotLinkify("bitcoin:[invalid]");
}
@Test
public void invalidBitcoinUri_shouldReturnStartingPosition() throws Exception {
String uri = "bitcoin:[invalid]";
int newPos = linkify(uri);
assertEquals(0, newPos);
}
@Test
public void invalidBitcoinUri_shouldNotWriteToOutputBuffer() throws Exception {
String uri = "bitcoin:[invalid]";
linkify(uri);
assertEquals(0, outputBuffer.length());
assertInvalidUri("bitcoin:[invalid]");
}
int linkify(String uri) {
return parser.linkifyUri(uri, 0, outputBuffer);
private void assertValidUri(String uri) {
UriMatch uriMatch = parser.parseUri(uri, 0);
assertUriMatch(uri, uriMatch);
}
void assertLinkify(String uri) {
linkify(uri);
assertLinkOnly(uri, outputBuffer);
private void assertUriMatch(String uri, UriMatch uriMatch) {
assertUriMatch(uri, uriMatch, 0);
}
void assertNotLinkify(String text) {
int newPos = linkify(text);
assertEquals(0, newPos);
private void assertUriMatch(String uri, UriMatch uriMatch, int offset) {
assertNotNull(uriMatch);
assertEquals(offset, uriMatch.getStartIndex());
assertEquals(uri.length() + offset, uriMatch.getEndIndex());
assertEquals(uri, uriMatch.getUri().toString());
}
private void assertInvalidUri(String text) {
UriMatch uriMatch = parser.parseUri(text, 0);
assertNull(uriMatch);
}
}

View file

@ -2,28 +2,29 @@ package com.fsck.k9.message.html;
import org.junit.Test;
import static com.fsck.k9.message.html.UriParserTestHelper.assertLinkOnly;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
public class EthereumUriParserTest {
EthereumUriParser parser = new EthereumUriParser();
StringBuffer outputBuffer = new StringBuffer();
@Test
public void basicEthereumUri() throws Exception {
assertLinkify("ethereum:0xfdf1210fc262c73d0436236a0e07be419babbbc4");
assertValidUri("ethereum:0xfdf1210fc262c73d0436236a0e07be419babbbc4");
}
@Test
public void ethereumUriWithValue() throws Exception {
assertLinkify("ethereum:0xfdf1210fc262c73d0436236a0e07be419babbbc4?value=42");
assertValidUri("ethereum:0xfdf1210fc262c73d0436236a0e07be419babbbc4?value=42");
}
@Test
public void ethereumUriWithQueryParameters() throws Exception {
assertLinkify("ethereum:0xfdf1210fc262c73d0436236a0e07be419babbbc4?value=42" +
assertValidUri("ethereum:0xfdf1210fc262c73d0436236a0e07be419babbbc4?value=42" +
"&gas=100000&bytecode=0xa9059cbb0000000000000000000000000000000dead");
}
@ -33,51 +34,40 @@ public class EthereumUriParserTest {
String uri = "ethereum:0xfdf1210fc262c73d0436236a0e07be419babbbc4?value=42";
String text = prefix + uri;
parser.linkifyUri(text, prefix.length(), outputBuffer);
UriMatch uriMatch = parser.parseUri(text, prefix.length());
assertLinkOnly(uri, outputBuffer);
assertUriMatch(uri, uriMatch, prefix.length());
}
@Test
public void invalidScheme() throws Exception {
assertNotLinkify("ethereMU:0xfdf1210fc262c73d0436236a0e07be419babbbc4");
assertInvalidUri("ethereMU:0xfdf1210fc262c73d0436236a0e07be419babbbc4");
}
@Test
public void invalidAddress() throws Exception {
assertNotLinkify("ethereum:[invalid]");
}
@Test
public void invalidEthereumUri_shouldReturnStartingPosition() throws Exception {
String uri = "ethereum:[invalid]";
int newPos = linkify(uri);
assertEquals(0, newPos);
}
@Test
public void invalidEthereumUri_shouldNotWriteToOutputBuffer() throws Exception {
String uri = "ethereum:[invalid]";
linkify(uri);
assertEquals(0, outputBuffer.length());
assertInvalidUri("ethereum:[invalid]");
}
int linkify(String uri) {
return parser.linkifyUri(uri, 0, outputBuffer);
private void assertValidUri(String uri) {
UriMatch uriMatch = parser.parseUri(uri, 0);
assertUriMatch(uri, uriMatch);
}
void assertLinkify(String uri) {
linkify(uri);
assertLinkOnly(uri, outputBuffer);
private void assertUriMatch(String uri, UriMatch uriMatch) {
assertUriMatch(uri, uriMatch, 0);
}
void assertNotLinkify(String text) {
int newPos = linkify(text);
assertEquals(0, newPos);
private void assertUriMatch(String uri, UriMatch uriMatch, int offset) {
assertNotNull(uriMatch);
assertEquals(offset, uriMatch.getStartIndex());
assertEquals(uri.length() + offset, uriMatch.getEndIndex());
assertEquals(uri, uriMatch.getUri().toString());
}
private void assertInvalidUri(String text) {
UriMatch uriMatch = parser.parseUri(text, 0);
assertNull(uriMatch);
}
}

View file

@ -1,180 +1,177 @@
package com.fsck.k9.message.html;
import org.junit.Assert;
import org.junit.Test;
import static com.fsck.k9.message.html.UriParserTestHelper.assertLinkOnly;
import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
public class HttpUriParserTest {
private final HttpUriParser parser = new HttpUriParser();
private final StringBuffer outputBuffer = new StringBuffer();
@Test
public void emptyUriIgnored() {
assertLinkIgnored("http://");
assertInvalidUri("http://");
}
@Test
public void emptyAuthorityIgnored() {
assertLinkIgnored("http:///");
assertInvalidUri("http:///");
}
@Test
public void simpleDomain() {
assertLinkify("http://www.google.com");
assertValidUri("http://www.google.com");
}
@Test
public void simpleDomainWithHttps() {
assertLinkify("https://www.google.com");
assertValidUri("https://www.google.com");
}
@Test
public void simpleRtspUri() {
assertLinkify("rtsp://example.com/media.mp4");
assertValidUri("rtsp://example.com/media.mp4");
}
@Test
public void invalidDomainIgnored() {
assertLinkIgnored("http://-www.google.com");
assertInvalidUri("http://-www.google.com");
}
@Test
public void domainWithTrailingSlash() {
assertLinkify("http://www.google.com/");
assertValidUri("http://www.google.com/");
}
@Test
public void domainWithUserInfo() {
assertLinkify("http://test@google.com/");
assertValidUri("http://test@google.com/");
}
@Test
public void domainWithFullUserInfo() {
assertLinkify("http://test:secret@google.com/");
assertValidUri("http://test:secret@google.com/");
}
@Test
public void domainWithoutWww() {
assertLinkify("http://google.com/");
assertValidUri("http://google.com/");
}
@Test
public void query() {
assertLinkify("http://google.com/give/me/?q=mode&c=information");
assertValidUri("http://google.com/give/me/?q=mode&c=information");
}
@Test
public void fragment() {
assertLinkify("http://google.com/give/me#only-the-best");
assertValidUri("http://google.com/give/me#only-the-best");
}
@Test
public void queryAndFragment() {
assertLinkify("http://google.com/give/me/?q=mode&c=information#only-the-best");
assertValidUri("http://google.com/give/me/?q=mode&c=information#only-the-best");
}
@Test
public void ipv4Address() {
assertLinkify("http://127.0.0.1");
assertValidUri("http://127.0.0.1");
}
@Test
public void ipv4AddressWithTrailingSlash() {
assertLinkify("http://127.0.0.1/");
assertValidUri("http://127.0.0.1/");
}
@Test
public void ipv4AddressWithEmptyPort() {
assertLinkify("http://127.0.0.1:");
assertValidUri("http://127.0.0.1:");
}
@Test
public void ipv4AddressWithPort() {
assertLinkify("http://127.0.0.1:524/");
assertValidUri("http://127.0.0.1:524/");
}
@Test
public void ipv6Address() {
assertLinkify("http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]");
assertValidUri("http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]");
}
@Test
public void ipv6AddressWithPort() {
assertLinkify("http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80");
assertValidUri("http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80");
}
@Test
public void ipv6AddressWithTrailingSlash() {
assertLinkify("http://[1080:0:0:0:8:800:200C:417A]/");
assertValidUri("http://[1080:0:0:0:8:800:200C:417A]/");
}
@Test
public void ipv6AddressWithEndCompression() {
assertLinkify("http://[3ffe:2a00:100:7031::1]");
assertValidUri("http://[3ffe:2a00:100:7031::1]");
}
@Test
public void ipv6AddressWithBeginCompression() {
assertLinkify("http://[1080::8:800:200C:417A]/");
assertValidUri("http://[1080::8:800:200C:417A]/");
}
@Test
public void ipv6AddressWithCompressionPort() {
assertLinkify("http://[::FFFF:129.144.52.38]:80/");
assertValidUri("http://[::FFFF:129.144.52.38]:80/");
}
@Test
public void ipv6AddressWithPrependedCompression() {
assertLinkify("http://[::192.9.5.5]/");
assertValidUri("http://[::192.9.5.5]/");
}
@Test
public void ipv6AddressWithTrailingIp4AndPort() {
assertLinkify("http://[::192.9.5.5]:80/");
assertValidUri("http://[::192.9.5.5]:80/");
}
@Test
public void ipv6WithoutClosingSquareBracketIgnored() {
assertLinkIgnored("http://[1080:0:0:0:8:80:200C:417A/");
assertInvalidUri("http://[1080:0:0:0:8:80:200C:417A/");
}
@Test
public void ipv6InvalidClosingSquareBracketIgnored() {
assertLinkIgnored("http://[1080:0:0:0:8:800:270C:417A/]");
assertInvalidUri("http://[1080:0:0:0:8:800:270C:417A/]");
}
@Test
public void domainWithTrailingSpace() {
String text = "http://google.com/ ";
int endPos = parser.linkifyUri(text, 0, outputBuffer);
UriMatch uriMatch = parser.parseUri(text, 0);
assertLinkOnly("http://google.com/", outputBuffer);
assertEquals(text.length() - 1, endPos);
assertUriMatch("http://google.com/", uriMatch);
}
@Test
public void domainWithTrailingNewline() {
String text = "http://google.com/\n";
int endPos = parser.linkifyUri(text, 0, outputBuffer);
UriMatch uriMatch = parser.parseUri(text, 0);
assertLinkOnly("http://google.com/", outputBuffer);
assertEquals(text.length() - 1, endPos);
assertUriMatch("http://google.com/", uriMatch);
}
@Test
public void domainWithTrailingAngleBracket() {
String text = "<http://google.com/>";
int endPos = parser.linkifyUri(text, 1, outputBuffer);
UriMatch uriMatch = parser.parseUri(text, 1);
assertLinkOnly("http://google.com/", outputBuffer);
assertEquals(text.length() - 1, endPos);
assertUriMatch("http://google.com/", uriMatch, 1);
}
@Test
@ -183,9 +180,9 @@ public class HttpUriParserTest {
String uri = "http://google.com/";
String text = prefix + uri;
parser.linkifyUri(text, prefix.length(), outputBuffer);
UriMatch uriMatch = parser.parseUri(text, prefix.length());
assertLinkOnly(uri, outputBuffer);
assertUriMatch("http://google.com/", uriMatch, prefix.length());
}
@Test
@ -195,25 +192,30 @@ public class HttpUriParserTest {
String postfix = " postfix";
String text = prefix + uri + postfix;
parser.linkifyUri(text, prefix.length(), outputBuffer);
UriMatch uriMatch = parser.parseUri(text, prefix.length());
assertLinkOnly(uri, outputBuffer);
assertUriMatch("http://google.com/", uriMatch, prefix.length());
}
int linkify(String uri) {
return parser.linkifyUri(uri, 0, outputBuffer);
private void assertValidUri(String uri) {
UriMatch uriMatch = parser.parseUri(uri, 0);
assertUriMatch(uri, uriMatch);
}
void assertLinkify(String uri) {
linkify(uri);
assertLinkOnly(uri, outputBuffer);
private void assertUriMatch(String uri, UriMatch uriMatch) {
assertUriMatch(uri, uriMatch, 0);
}
void assertLinkIgnored(String uri) {
int endPos = linkify(uri);
private void assertUriMatch(String uri, UriMatch uriMatch, int offset) {
assertNotNull(uriMatch);
Assert.assertEquals(offset, uriMatch.getStartIndex());
Assert.assertEquals(uri.length() + offset, uriMatch.getEndIndex());
Assert.assertEquals(uri, uriMatch.getUri().toString());
}
assertEquals("", outputBuffer.toString());
assertEquals(0, endPos);
private void assertInvalidUri(String uri) {
UriMatch uriMatch = parser.parseUri(uri, 0);
assertNull(uriMatch);
}
}

View file

@ -0,0 +1,99 @@
package com.fsck.k9.message.html;
import java.util.List;
import org.junit.Test;
import static com.google.common.truth.Truth.assertThat;
public class UriMatcherTest {
@Test
public void emptyText() {
assertNoMatch("");
}
@Test
public void textWithoutUri() {
assertNoMatch("some text here");
}
@Test
public void simpleUri() {
assertUrisFound("http://example.org", "http://example.org");
}
@Test
public void uriPrecededBySpace() {
assertUrisFound(" http://example.org", "http://example.org");
}
@Test
public void uriPrecededByOpeningParenthesis() {
assertUrisFound("(http://example.org", "http://example.org");
}
@Test
public void uriPrecededBySomeText() {
assertUrisFound("Check out my fantastic URI: http://example.org", "http://example.org");
}
@Test
public void uriWithTrailingText() {
assertUrisFound("http://example.org/ is the best", "http://example.org/");
}
@Test
public void uriEmbeddedInText() {
assertUrisFound("prefix http://example.org/ suffix", "http://example.org/");
}
@Test
public void uriWithUppercaseScheme() {
assertUrisFound("HTTP://example.org/", "HTTP://example.org/");
}
@Test
public void uriNotPrecededByValidSeparator() {
assertNoMatch("myhttp://example.org");
}
@Test
public void uriNotPrecededByValidSeparatorFollowedByValidUri() {
assertUrisFound("myhttp: http://example.org", "http://example.org");
}
@Test
public void schemaMatchWithInvalidUriInMiddleOfTextFollowedByValidUri() {
assertUrisFound("prefix http:42 http://example.org", "http://example.org");
}
@Test
public void multipleValidUrisInRow() {
assertUrisFound("prefix http://uri1.example.org some text http://uri2.example.org/path postfix",
"http://uri1.example.org", "http://uri2.example.org/path");
}
private void assertNoMatch(String text) {
List<UriMatch> uriMatches = UriMatcher.INSTANCE.findUris(text);
assertThat(uriMatches).isEmpty();
}
private void assertUrisFound(String text, String... uris) {
List<UriMatch> uriMatches = UriMatcher.INSTANCE.findUris(text);
assertThat(uriMatches).hasSize(uris.length);
for (int i = 0, end = uris.length; i < end; i++) {
String uri = uris[i];
int startIndex = text.indexOf(uri);
assertThat(startIndex).isNotEqualTo(-1);
UriMatch uriMatch = uriMatches.get(i);
assertThat(uriMatch.getStartIndex()).isEqualTo(startIndex);
assertThat(uriMatch.getEndIndex()).isEqualTo(startIndex + uri.length());
assertThat(uriMatch.getUri()).isEqualTo(uri);
}
}
}