Mail: Detect URLs by parsing ahead instead of checking every character.

This is a *massive* performance improvement; some longer emails that
took 45+ seconds to load before now take < 1 second, at the cost
of a completely rewritten URL detector. This means some URLs that
were detected before (e.g. anything starting with "www", emails
without "mailto:", etc.) are now not detected, though support
for detecting them could be reinstated in the future.

Fixes #4816.
This commit is contained in:
Augustin Cavalier
2018-11-15 20:21:35 -05:00
parent 4d182b9b57
commit a4dcbfcef1
+95 -142
View File
@@ -275,115 +275,56 @@ FilterHTMLTag(int32 &first, char **t, char *end)
}
/** Returns the type and length of the URL in the string if it is one.
* If the "url" string is specified, it will fill it with the complete
* URL that might differ from the one in the text itself (i.e. because
* of an prepended "http://").
/*! Returns the type of the next URL in the string.
*
* If the "url" string is specified, it will fill it with the complete
* URL.
*/
static uint8
CheckForURL(const char *string, size_t &urlLength, BString *url = NULL)
FindURL(const BString& string, int32 startIndex, int32& urlPos,
int32& urlLength, BString* urlString = NULL)
{
const char *urlPrefixes[] = {
"http://",
"ftp://",
"shttp://",
"https://",
"finger://",
"telnet://",
"gopher://",
"news://",
"nntp://",
"file://",
NULL
};
//
// Search for URL prefix
//
uint8 type = 0;
for (const char **prefix = urlPrefixes; *prefix != 0; prefix++) {
if (!cistrncmp(string, *prefix, strlen(*prefix))) {
type = TYPE_URL;
break;
}
}
urlPos = string.Length();
//
// Not a URL? check for "mailto:" or "www."
//
if (type == 0 && !cistrncmp(string, "mailto:", 7))
type = TYPE_MAILTO;
if (type == 0 && !strncmp(string, "www.", 4)) {
// this type will be special cased later (and a http:// is added
// for the enclosure address)
type = TYPE_URL;
}
if (type == 0) {
// check for valid eMail addresses
const char *at = strchr(string, '@');
if (at) {
const char *pos = string;
bool readName = false;
for (; pos < at; pos++) {
// ToDo: are these all allowed characters?
if (!isalnum(pos[0]) && pos[0] != '_' && pos[0] != '.' && pos[0] != '-')
break;
readName = true;
}
if (pos == at && readName)
type = TYPE_MAILTO;
}
}
if (type == 0)
int32 baseOffset = string.FindFirst("://", startIndex),
mailtoOffset = string.FindFirst("mailto:", startIndex);
if (baseOffset == B_ERROR && mailtoOffset == B_ERROR)
return 0;
if (baseOffset == B_ERROR)
baseOffset = string.Length();
if (mailtoOffset == B_ERROR)
mailtoOffset = string.Length();
int32 index = strcspn(string, " \t<>)\"\\,\r\n");
if (baseOffset < mailtoOffset) {
type = TYPE_URL;
// Find the actual start of the URL
urlPos = baseOffset;
while (urlPos >= startIndex && (isalnum(string.ByteAt(urlPos - 1))
|| string.ByteAt(urlPos - 1) == '-'))
urlPos--;
} else if (mailtoOffset < baseOffset) {
type = TYPE_MAILTO;
urlPos = mailtoOffset;
}
// find the end of the URL based on word boundaries
const char* str = string.String() + urlPos;
urlLength = strcspn(str, " \t<>)\"\\,\r\n");
// filter out some punctuation marks if they are the last character
char suffix = string[index - 1];
if (suffix == '.'
|| suffix == ','
|| suffix == '?'
|| suffix == '!'
|| suffix == ':'
|| suffix == ';')
index--;
char suffix = str[urlLength - 1];
while (suffix == '.'
|| suffix == ','
|| suffix == '?'
|| suffix == '!'
|| suffix == ':'
|| suffix == ';')
urlLength--;
if (type == TYPE_URL) {
char *parenthesis = NULL;
// filter out a trailing ')' if there is no left parenthesis before
if (string[index - 1] == ')') {
char *parenthesis = strchr(string, '(');
if (parenthesis == NULL || parenthesis > string + index)
index--;
}
// filter out a trailing ']' if there is no left bracket before
if (parenthesis == NULL && string[index - 1] == ']') {
char *parenthesis = strchr(string, '[');
if (parenthesis == NULL || parenthesis > string + index)
index--;
}
}
if (url != NULL) {
// copy the address to the specified string
if (type == TYPE_URL && string[0] == 'w') {
// URL starts with "www.", so add the protocol to it
url->SetTo("http://");
url->Append(string, index);
} else if (type == TYPE_MAILTO && cistrncmp(string, "mailto:", 7)) {
// eMail address had no "mailto:" prefix
url->SetTo("mailto:");
url->Append(string, index);
} else
url->SetTo(string, index);
}
urlLength = index;
if (urlString != NULL)
*urlString = BString(string.String() + urlPos, urlLength);
return type;
}
@@ -2151,10 +2092,15 @@ TTextView::AddAsContent(BEmailMessage *mail, bool wrap, uint32 charset, mail_enc
// hard-wrap, based on TextView's soft-wrapping
int32 numLines = CountLines();
bool spaceMoved = false;
char *content = (char *)malloc(textLength + numLines * 72); // more we'll ever need
char *content = (char *)malloc(textLength + numLines * 72);
// more we'll ever need
if (content != NULL) {
int32 contentLength = 0;
int32 nextUrlAt = 0, nextUrlLength = 0;
BString textStr(text);
FindURL(text, 0, nextUrlAt, nextUrlLength, NULL);
for (int32 i = 0; i < numLines; i++) {
int32 startOffset = OffsetAt(i);
if (spaceMoved) {
@@ -2164,42 +2110,41 @@ TTextView::AddAsContent(BEmailMessage *mail, bool wrap, uint32 charset, mail_enc
int32 endOffset = OffsetAt(i + 1);
int32 lineLength = endOffset - startOffset;
// quick hack to not break URLs into several parts
for (int32 pos = startOffset; pos < endOffset; pos++) {
size_t urlLength;
uint8 type = CheckForURL(text + pos, urlLength);
if (type != 0)
pos += urlLength;
// don't break URLs into several parts
if (nextUrlAt >= startOffset && nextUrlAt < endOffset
&& (nextUrlAt + nextUrlLength) > endOffset) {
int32 pos = nextUrlAt + nextUrlLength;
if (pos > endOffset) {
// find first break character after the URL
for (; text[pos]; pos++) {
if (isalnum(text[pos]) || isspace(text[pos]))
break;
}
if (text[pos] && isspace(text[pos]) && text[pos] != '\n')
pos++;
endOffset += pos - endOffset;
lineLength = endOffset - startOffset;
// insert a newline (and the same number of quotes) after the
// URL to make sure the rest of the text is properly wrapped
char buffer[64];
if (text[pos] == '\n')
buffer[0] = '\0';
else
strcpy(buffer, "\n");
size_t quoteLength;
CopyQuotes(text + startOffset, lineLength, buffer + strlen(buffer), quoteLength);
Insert(pos, buffer, strlen(buffer));
numLines = CountLines();
text = Text();
i++;
// find first break character after the URL
for (; text[pos]; pos++) {
if (isalnum(text[pos]) || isspace(text[pos]))
break;
}
if (text[pos] && isspace(text[pos]) && text[pos] != '\n')
pos++;
endOffset += pos - endOffset;
lineLength = endOffset - startOffset;
// insert a newline (and the same number of quotes) after the
// URL to make sure the rest of the text is properly wrapped
char buffer[64];
if (text[pos] == '\n')
buffer[0] = '\0';
else
strcpy(buffer, "\n");
size_t quoteLength;
CopyQuotes(text + startOffset, lineLength, buffer + strlen(buffer), quoteLength);
Insert(pos, buffer, strlen(buffer));
numLines = CountLines();
text = Text();
i++;
textStr = BString(text);
FindURL(text, endOffset, nextUrlAt, nextUrlLength, NULL);
}
if (text[endOffset - 1] != ' '
&& text[endOffset - 1] != '\n'
@@ -2370,6 +2315,12 @@ TTextView::Reader::Process(const char *data, int32 data_len, bool isHeader)
char line[522];
int32 count = 0;
const BString dataStr(data, data_len);
BString nextUrl;
int32 nextUrlPos = 0, nextUrlLength = 0;
uint8 nextUrlType
= FindURL(dataStr, 0, nextUrlPos, nextUrlLength, &nextUrl);
for (int32 loop = 0; loop < data_len; loop++) {
if (fView->fStopLoading)
return false;
@@ -2379,9 +2330,7 @@ TTextView::Reader::Process(const char *data, int32 data_len, bool isHeader)
count += strlen(QUOTE);
}
if (!fRaw && fIncoming && (loop < data_len - 7)) {
size_t urlLength;
BString url;
uint8 type = CheckForURL(data + loop, urlLength, &url);
uint8 type = (nextUrlPos == loop) ? nextUrlType : 0;
if (type) {
if (!Insert(line, count, false, isHeader))
@@ -2394,19 +2343,23 @@ TTextView::Reader::Process(const char *data, int32 data_len, bool isHeader)
memset(enclosure, 0, sizeof(hyper_text));
fView->GetSelection(&enclosure->text_start,
&enclosure->text_end);
&enclosure->text_end);
enclosure->type = type;
enclosure->name = strdup(url.String());
enclosure->name = strdup(nextUrl.String());
if (enclosure->name == NULL) {
free(enclosure);
return false;
}
Insert(&data[loop], urlLength, true, isHeader);
enclosure->text_end += urlLength;
loop += urlLength - 1;
Insert(&data[loop], nextUrlLength, true, isHeader);
enclosure->text_end += nextUrlLength;
loop += nextUrlLength - 1;
fEnclosures->AddItem(enclosure);
nextUrlType
= FindURL(dataStr, loop,
nextUrlPos, nextUrlLength, &nextUrl);
continue;
}
}