blob: 13c58c52c60decdb575984ec4702d47ebc255404 [file] [edit]
// Copyright (C) 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "icing/tokenization/rfc822-tokenizer.h"
#include <algorithm>
#include <deque>
#include <queue>
#include <string_view>
#include <utility>
#include <vector>
#include "icing/tokenization/token.h"
#include "icing/tokenization/tokenizer.h"
#include "icing/util/character-iterator.h"
#include "icing/util/i18n-utils.h"
#include "icing/util/status-macros.h"
#include "unicode/umachine.h"
namespace icing {
namespace lib {
namespace {
bool IsDelimiter(UChar32 c) { return c == ',' || c == ';' || c == '\n'; }
} // namespace
class Rfc822TokenIterator : public Tokenizer::Iterator {
public:
// Cursor is the index into the string_view, text_end_ is the length.
explicit Rfc822TokenIterator(std::string_view text)
: text_(std::move(text)),
iterator_(text, 0, 0, 0),
text_end_(text.length()),
token_index_(-1) {}
// Advance will move token_index_ past the end of tokens_
bool Advance() override {
// Stop the token index on a RFC822 token, or one past the end, where the
// next RFC822 token will be if more are generated.
do {
token_index_++;
} while (token_index_ < tokens_.size() &&
tokens_[token_index_].type != Token::Type::RFC822_TOKEN);
// There is still something left, possible if we rewinded and call Advance
if (token_index_ < tokens_.size()) {
return true;
}
// Done with the entire string_view.
if (iterator_.utf8_index() >= text_end_) {
return false;
}
// Parsing a new email, update the current email marker.
AdvancePastWhitespace();
// This may return false, as in the case of "<alex>,,", where after
// processing <alex>, there are no more tokens.
return GetNextRfc822Token();
}
// Returns the current token group, an RFC822_TOKEN along with all it's
// subtokens. For example, "tim@google.com" will return all tokens generated
// from that text.
//
// Returns:
// A vector of Tokens on success
// An empty vector if the token list is empty
// An empty vector if the index is past the end of the token list
std::vector<Token> GetTokens() const override {
std::vector<Token> result;
if (token_index_ < tokens_.size() && token_index_ >= 0) {
int index = token_index_;
do {
result.push_back(tokens_[index]);
} while (++index < tokens_.size() &&
tokens_[index].type != Token::Type::RFC822_TOKEN);
}
return result;
}
bool ResetToTokenStartingAfter(int32_t utf32_offset) override {
CharacterIterator tracker(text_);
for (int new_index = 0; new_index < tokens_.size(); ++new_index) {
const Token& t = tokens_[new_index];
if (t.type != Token::Type::RFC822_TOKEN) {
continue;
}
tracker.AdvanceToUtf8(t.text.begin() - text_.begin());
if (tracker.utf32_index() > utf32_offset) {
token_index_ = new_index;
return true;
}
}
return false;
}
// This will attempt to reset the token_index to point to the last token
// ending before an offset. If it fails, due to there not being any tokens
// before the offset, the token index will become -1.
bool ResetToTokenEndingBefore(int32_t utf32_offset) override {
// First, advance until we pass offset or Advance is false
if (tokens_.empty()) {
if (!Advance()) {
// No tokens available, and Advancing doesn't get more, so return false.
return false;
}
}
CharacterIterator tracker(text_);
// Keep advancing until we parse all the emails, or run past the offset.
// Advance will always make token_index_ point to an RFC822_TOKEN, so we can
// look at that tokens text end to determine if it ends before the offset.
// This first loop will guarantee that we end up either past the offset or
// at the end.
do {
tracker.AdvanceToUtf8(tokens_[token_index_].text.end() - text_.begin());
// When we Advance and have to convert names to email addresses, it's
// possible that multiple RFC822 tokens are added. We need to advance
// through these one at a time, we cannot skip to the top of the line.
} while (tracker.utf32_index() <= utf32_offset && Advance());
// We are either past the offset or at the end. Either way, we now work
// backwards and reset to the first (highest index) RFC822_TOKEN we find.
while (--token_index_ >= 0) {
if (tokens_[token_index_].type != Token::Type::RFC822_TOKEN) {
continue;
}
tracker.MoveToUtf8(tokens_[token_index_].text.end() - text_.begin());
if (tracker.utf32_index() <= utf32_offset) {
return true;
}
}
return false;
}
// Returns a character iterator to the start of the token.
libtextclassifier3::StatusOr<CharacterIterator> CalculateTokenStart()
override {
CharacterIterator token_start = iterator_;
token_start.MoveToUtf8(GetTokens().at(0).text.begin() - text_.begin());
return token_start;
}
// Returns a character iterator to right after the end of the token.
libtextclassifier3::StatusOr<CharacterIterator> CalculateTokenEndExclusive()
override {
CharacterIterator token_end = iterator_;
token_end.MoveToUtf8(GetTokens().at(0).text.end() - text_.begin());
return token_end;
}
// Reset to start moves to the state we're in after the first Advance().
bool ResetToStart() override {
token_index_ = -1;
return Advance();
}
private:
// Advance until the next email delimiter, generating as many tokens as
// necessary.
bool GetNextRfc822Token() {
if (iterator_.utf8_index() >= text_end_) {
return false;
}
int token_start = iterator_.utf8_index();
bool address_found = false;
bool name_found = false;
std::vector<Token> next_tokens;
Token rfc822(Token::Type::RFC822_TOKEN);
// We start at unquoted and run until a ",;\n<( .
while (iterator_.utf8_index() < text_end_) {
UChar32 c = iterator_.GetCurrentChar();
if (IsDelimiter(c)) {
// End of the token, advance cursor past all delimiters then quit.
rfc822.text =
text_.substr(token_start, iterator_.utf8_index() - token_start);
UChar32 delimiter;
do {
AdvanceCursor();
delimiter = iterator_.GetCurrentChar();
// If we get current char on the end, it is not a delimiter so this
// loop will end
} while (IsDelimiter(delimiter));
break;
}
std::vector<Token> consume_result;
if (c == '"') {
consume_result = ConsumeQuotedSection();
name_found |= !consume_result.empty();
} else if (c == '(') {
consume_result = ConsumeParenthesizedSection();
} else if (c == '<') {
// Only set address_found to true if ConsumeAdress returns true.
// Otherwise, keep address_found as is to prevent setting address_found
// back to false if it is true.
consume_result = ConsumeAddress();
address_found |= !consume_result.empty();
} else {
consume_result = ConsumeUnquotedSection();
name_found |= !consume_result.empty();
}
next_tokens.insert(next_tokens.end(), consume_result.begin(),
consume_result.end());
}
if (iterator_.utf8_index() >= text_end_) {
rfc822.text = text_.substr(token_start, text_end_ - token_start);
}
// If an address is found, use the tokens we have.
// If an address isn't found, and a name isn't found, also use the tokens
// we have.
// If an address isn't found but a name is, convert name Tokens to email
// Tokens.
if (!address_found && name_found) {
// We don't add the rfc822 token, as it will be handled by
// ConvertNameToEmail.
std::vector<Token> converted_tokens = ConvertNameToEmail(next_tokens);
tokens_.insert(tokens_.end(), converted_tokens.begin(),
converted_tokens.end());
} else {
if (next_tokens.empty()) {
// Tokens may not be generated in the case of ",,,,,,"
return false;
} else {
// If tokens were generated, push back the RFC822 token for them
tokens_.push_back(rfc822);
tokens_.insert(tokens_.end(), next_tokens.begin(), next_tokens.end());
}
}
return true;
}
// We allow for the "First Last <email>" format, but if there is no email in
// brackets, we won't allow for unquoted spaces. For example, the input
// "alex@google.com tim@google.com" has an unquoted space, so we will split
// it into two emails. We don't need to find more tokens, we just need to
// find @ signs and spaces and convert name tokens to parts of the email.
std::vector<Token> ConvertNameToEmail(std::vector<Token>& name_tokens) {
if (name_tokens.empty()) {
return name_tokens;
}
// There will only be names and comments, and they will be in order.
std::vector<Token> converted_tokens;
// Start at the beginning of the current email.
CharacterIterator scanner(text_);
scanner.MoveToUtf8(name_tokens[0].text.begin() - text_.begin());
int token_processed_index = 0;
bool in_quote = false;
// Setting at_sign_index to before the beginning, it'll only be set to
// something else if we find an @ sign
const char* at_sign_index = nullptr;
// Run to the end
while (scanner.utf8_index() < iterator_.utf8_index()) {
const char* end_of_token = nullptr;
UChar32 c = scanner.GetCurrentChar();
if (c == '\\') {
// Skip the slash, as well as the following token.
scanner.AdvanceToUtf32(scanner.utf32_index() + 1);
scanner.AdvanceToUtf32(scanner.utf32_index() + 1);
continue;
}
if (c == '"') {
in_quote = !in_quote;
}
if (c == '@') {
at_sign_index = text_.begin() + scanner.utf8_index();
}
// If the next character is the end OR we hit an unquoted space.
if (scanner.utf8_index() + i18n_utils::GetUtf8Length(c) ==
iterator_.utf8_index() ||
(!in_quote && c == ' ')) {
if (!in_quote && c == ' ') {
end_of_token = text_.begin() + scanner.utf8_index();
} else {
end_of_token = text_.begin() + iterator_.utf8_index();
}
std::deque<Token> more_tokens = ConvertOneNameToEmail(
name_tokens, at_sign_index, end_of_token, token_processed_index);
converted_tokens.insert(converted_tokens.end(), more_tokens.begin(),
more_tokens.end());
// Reset the at_sign_index
at_sign_index = nullptr;
}
scanner.AdvanceToUtf32(scanner.utf32_index() + 1);
}
// It's possible we left something out.
if (token_processed_index < name_tokens.size()) {
std::deque<Token> more_tokens =
ConvertOneNameToEmail(name_tokens, at_sign_index,
name_tokens[name_tokens.size() - 1].text.end(),
token_processed_index);
converted_tokens.insert(converted_tokens.end(), more_tokens.begin(),
more_tokens.end());
}
return converted_tokens;
}
// Once a name is determined to be an address, convert its tokens to address
// tokens.
std::deque<Token> ConvertOneNameToEmail(const std::vector<Token>& name_tokens,
const char* at_sign_index,
const char* end_of_token,
int& token_processed_index) {
const char* address_start = nullptr;
const char* local_address_end = nullptr;
const char* host_address_start = nullptr;
const char* address_end = nullptr;
const char* token_start = nullptr;
const char* token_end = nullptr;
std::deque<Token> converted_tokens;
// Transform tokens up to end of token pointer.
for (; token_processed_index < name_tokens.size();
++token_processed_index) {
const Token& token = name_tokens[token_processed_index];
if (token.text.end() > end_of_token) {
break;
}
std::string_view text = token.text;
// We need to do this both for comment and name tokens. Comment tokens
// will get a corresponding RFC822 token, but not an address or local
// address.
if (token_start == nullptr) {
token_start = text.begin();
}
token_end = text.end();
if (token.type == Token::Type::RFC822_COMMENT) {
// Comment tokens will stay as they are.
converted_tokens.push_back(token);
} else if (token.type == Token::Type::RFC822_NAME) {
// Names need to be converted to address tokens. We keep the order of
// which the name tokens appeared. Name tokens that appear before an
// @ sign in the name will become RFC822_ADDRESS_COMPONENT_LOCAL, and
// those after will become RFC822_ADDRESS_COMPONENT_HOST. We aren't
// able to determine RFC822_ADDRESS, RFC822_LOCAL_ADDRESS, and
// RFC_HOST_ADDRESS before checking the name tokens, so they will be
// added after the component tokens.
if (address_start == nullptr) {
address_start = text.begin();
}
address_end = text.end();
if (text.begin() > at_sign_index) {
if (host_address_start == nullptr) {
host_address_start = text.begin();
}
// Once this is hit, we switch to COMPONENT_HOST and mark end of the
// local address
converted_tokens.push_back(
Token(Token::Type::RFC822_ADDRESS_COMPONENT_HOST, token.text));
} else {
local_address_end = text.end();
converted_tokens.push_back(
Token(Token::Type::RFC822_ADDRESS_COMPONENT_LOCAL, token.text));
}
}
}
if (address_start != nullptr) {
converted_tokens.push_back(
Token(Token::Type::RFC822_ADDRESS,
std::string_view(address_start, address_end - address_start)));
if (local_address_end != nullptr) {
converted_tokens.push_back(
Token(Token::Type::RFC822_LOCAL_ADDRESS,
std::string_view(address_start,
local_address_end - address_start)));
}
}
if (host_address_start != nullptr && host_address_start < address_end) {
converted_tokens.push_back(
Token(Token::Type::RFC822_HOST_ADDRESS,
text_.substr(host_address_start - text_.begin(),
address_end - host_address_start)));
}
if (token_start != nullptr) {
converted_tokens.push_front(
Token(Token::Type::RFC822_TOKEN,
std::string_view(token_start, token_end - token_start)));
}
return converted_tokens;
}
// Returns name tokens in an unquoted section. This is useful in case we do
// not find an address and have to use the name. An unquoted section may look
// like "Alex Sav", or "alex@google.com". In the absense of a bracketed email
// address, the unquoted section will be used as the email address along with
// the quoted section.
std::vector<Token> ConsumeUnquotedSection() {
UChar32 c;
int token_start = -1;
std::vector<Token> next_tokens;
// Advance to another state or a character marking the end of token, one
// of \n,; .
while (iterator_.utf8_index() < text_end_) {
c = iterator_.GetCurrentChar();
if (i18n_utils::IsAlphaNumeric(c)) {
if (token_start == -1) {
// Start recording
token_start = iterator_.utf8_index();
}
AdvanceCursor();
} else {
if (token_start != -1) {
// The character is non alphabetic, save a token.
next_tokens.push_back(Token(
Token::Type::RFC822_NAME,
text_.substr(token_start, iterator_.utf8_index() - token_start)));
token_start = -1;
}
if (c == '"' || c == '<' || c == '(' || IsDelimiter(c)) {
// Stay on the token.
break;
}
AdvanceCursor();
}
}
if (token_start != -1) {
next_tokens.push_back(Token(
Token::Type::RFC822_NAME,
text_.substr(token_start, iterator_.utf8_index() - token_start)));
}
return next_tokens;
}
// Names that are within quotes should have all characters blindly
// unescaped. When a name is made into an address, it isn't re-escaped.
// Returns name tokens found in a quoted section. This is useful in case we do
// not find an address and have to use the name. The quoted section may
// contain whitespaces.
std::vector<Token> ConsumeQuotedSection() {
// Get past the first quote.
AdvanceCursor();
bool end_quote_found = false;
std::vector<Token> next_tokens;
UChar32 c;
int token_start = -1;
while (!end_quote_found && (iterator_.utf8_index() < text_end_)) {
c = iterator_.GetCurrentChar();
if (i18n_utils::IsAlphaNumeric(c)) {
if (token_start == -1) {
// Start tracking the token.
token_start = iterator_.utf8_index();
}
AdvanceCursor();
} else {
// Non- alphabetic
if (c == '\\') {
// A backslash, let's look at the next character.
CharacterIterator temp = iterator_;
temp.AdvanceToUtf32(iterator_.utf32_index() + 1);
UChar32 n = temp.GetCurrentChar();
if (i18n_utils::IsAlphaNumeric(n)) {
// The next character is alphabetic, skip the slash and don't end
// the last token. For quoted sections, the only things that are
// escaped are double quotes and slashes. For example, in "a\lex",
// an l appears after the slash. We want to treat this as if it
// was just "alex". So we tokenize it as <RFC822_NAME, "a\lex">.
AdvanceCursor();
} else {
// Not alphabetic, so save the last token if necessary.
if (token_start != -1) {
next_tokens.push_back(
Token(Token::Type::RFC822_NAME,
text_.substr(token_start,
iterator_.utf8_index() - token_start)));
token_start = -1;
}
// Skip the backslash.
AdvanceCursor();
if (n == '"' || n == '\\' || n == '@') {
// Skip these too if they're next.
AdvanceCursor();
}
}
} else {
// Not a backslash.
if (token_start != -1) {
next_tokens.push_back(
Token(Token::Type::RFC822_NAME,
text_.substr(token_start,
iterator_.utf8_index() - token_start)));
token_start = -1;
}
if (c == '"') {
end_quote_found = true;
}
// Advance one more time to get past the non-alphabetic character.
AdvanceCursor();
}
}
}
if (token_start != -1) {
next_tokens.push_back(Token(
Token::Type::RFC822_NAME,
text_.substr(token_start, iterator_.utf8_index() - token_start)));
}
return next_tokens;
}
// '(', ')', '\\' chars should be escaped. All other escaped chars should be
// unescaped.
std::vector<Token> ConsumeParenthesizedSection() {
// Skip the initial (
AdvanceCursor();
int paren_layer = 1;
UChar32 c;
std::vector<Token> next_tokens;
int token_start = -1;
while (paren_layer > 0 && (iterator_.utf8_index() < text_end_)) {
c = iterator_.GetCurrentChar();
if (i18n_utils::IsAlphaNumeric(c)) {
if (token_start == -1) {
// Start tracking a token.
token_start = iterator_.utf8_index();
}
AdvanceCursor();
} else {
// Non alphabetic.
if (c == '\\') {
// A backslash, let's look at the next character.
UChar32 n = i18n_utils::GetUChar32At(text_.begin(), text_.length(),
iterator_.utf8_index() + 1);
if (i18n_utils::IsAlphaNumeric(n)) {
// Alphabetic, skip the slash and don't end the last token.
AdvanceCursor();
} else {
// Not alphabetic, save the last token if necessary.
if (token_start != -1) {
next_tokens.push_back(
Token(Token::Type::RFC822_COMMENT,
text_.substr(token_start,
iterator_.utf8_index() - token_start)));
token_start = -1;
}
// Skip the backslash.
AdvanceCursor();
if (n == ')' || n == '(' || n == '\\') {
// Skip these too if they're next.
AdvanceCursor();
}
}
} else {
// Not a backslash.
if (token_start != -1) {
next_tokens.push_back(
Token(Token::Type::RFC822_COMMENT,
text_.substr(token_start,
iterator_.utf8_index() - token_start)));
token_start = -1;
}
if (c == '(') {
paren_layer++;
} else if (c == ')') {
paren_layer--;
}
AdvanceCursor();
}
}
}
if (token_start != -1) {
// Ran past the end of text_ without getting the last token.
// substr returns "a view of the substring [pos, pos + // rcount), where
// rcount is the smaller of count and size() - pos" therefore the count
// argument can be any value >= this->cursor - token_start. Therefore,
// ignoring the mutation warning.
next_tokens.push_back(Token(
Token::Type::RFC822_COMMENT,
text_.substr(token_start, iterator_.utf8_index() - token_start)));
}
return next_tokens;
}
// Returns tokens found in the address.
std::vector<Token> ConsumeAddress() {
// Skip the first <.
AdvanceCursor();
// Save the start position.
CharacterIterator address_start_iterator = iterator_;
std::vector<Token> next_tokens;
// Place the at sign on the '<', so that if no at_sign is found, the default
// is that the entire address is the host part.
int at_sign = -1;
int address_end = -1;
UChar32 c = iterator_.GetCurrentChar();
// Quick scan for @ and > signs.
while (c != '>' && iterator_.utf8_index() < text_end_) {
AdvanceCursor();
c = iterator_.GetCurrentChar();
if (c == '@') {
at_sign = iterator_.utf8_index();
}
}
if (iterator_.utf8_index() <= address_start_iterator.utf8_index()) {
// There is nothing between the brackets, either we have "<" or "<>".
return next_tokens;
}
// Either we find a > or run to the end, either way this is the end of the
// address. The ending bracket will be handled by ConsumeUnquoted.
address_end = iterator_.utf8_index();
// Reset to the start.
iterator_ = address_start_iterator;
int address_start = address_start_iterator.utf8_index();
Token::Type type = Token::Type::RFC822_ADDRESS_COMPONENT_LOCAL;
// Create a local address token.
if (at_sign != -1) {
next_tokens.push_back(
Token(Token::Type::RFC822_LOCAL_ADDRESS,
text_.substr(address_start, at_sign - address_start)));
} else {
// All the tokens in the address are host components.
type = Token::Type::RFC822_ADDRESS_COMPONENT_HOST;
// If no @ is found, treat the entire address as the host address.
at_sign = address_start - 1;
}
// The only case where we don't have a host address part is something like
// <localaddress@>. If there is no @, the at_sign is the default -1, and the
// host address is [0, address_end).
int host_address_start = at_sign + 1;
if (host_address_start < address_end) {
next_tokens.push_back(Token(
Token::Type::RFC822_HOST_ADDRESS,
text_.substr(host_address_start, address_end - host_address_start)));
}
next_tokens.push_back(
Token(Token::Type::RFC822_ADDRESS,
text_.substr(address_start, address_end - address_start)));
int token_start = -1;
while (iterator_.utf8_index() < address_end) {
c = iterator_.GetCurrentChar();
if (i18n_utils::IsAlphaNumeric(c)) {
if (token_start == -1) {
token_start = iterator_.utf8_index();
}
} else {
// non alphabetic
if (c == '\\') {
// A backslash, let's look at the next character.
CharacterIterator temp = iterator_;
temp.AdvanceToUtf32(iterator_.utf32_index() + 1);
UChar32 n = temp.GetCurrentChar();
if (!i18n_utils::IsAlphaNumeric(n)) {
// Not alphabetic, end the last token if necessary.
if (token_start != -1) {
next_tokens.push_back(Token(
type, text_.substr(token_start,
iterator_.utf8_index() - token_start)));
token_start = -1;
}
}
} else {
// Not backslash.
if (token_start != -1) {
next_tokens.push_back(Token(
type, text_.substr(token_start,
iterator_.utf8_index() - token_start)));
token_start = -1;
}
// Switch to host component tokens.
if (iterator_.utf8_index() == at_sign) {
type = Token::Type::RFC822_ADDRESS_COMPONENT_HOST;
}
}
}
AdvanceCursor();
}
if (token_start != -1) {
next_tokens.push_back(Token(
type,
text_.substr(token_start, iterator_.utf8_index() - token_start)));
}
// Unquoted will handle the closing bracket > if these is one.
return next_tokens;
}
void AdvanceCursor() {
iterator_.AdvanceToUtf32(iterator_.utf32_index() + 1);
}
void AdvancePastWhitespace() {
while (i18n_utils::IsWhitespaceAt(text_, iterator_.utf8_index())) {
AdvanceCursor();
}
}
std::string_view text_;
CharacterIterator iterator_;
int text_end_;
// A temporary store of Tokens. As we advance through the provided string,
// we parse entire addresses at a time rather than one token at a time.
// However, since we call the tokenizer with Advance() alternating with
// GetToken(), we need to store tokens for subsequent GetToken calls if
// Advance generates multiple tokens (it usually does). A vector is used as
// we need to iterate back and forth through tokens during snippeting. It is
// cleared by the destructor.
std::vector<Token> tokens_;
// Index to keep track of where we are in tokens_. This will always be set to
// point to an RFC822_TOKEN, or one past the end of the tokens_ vector. The
// only exception is before the first Advance call.
int token_index_;
};
libtextclassifier3::StatusOr<std::unique_ptr<Tokenizer::Iterator>>
Rfc822Tokenizer::Tokenize(std::string_view text) const {
return std::make_unique<Rfc822TokenIterator>(text);
}
libtextclassifier3::StatusOr<std::vector<Token>> Rfc822Tokenizer::TokenizeAll(
std::string_view text) const {
ICING_ASSIGN_OR_RETURN(std::unique_ptr<Tokenizer::Iterator> iterator,
Tokenize(text));
std::vector<Token> tokens;
while (iterator->Advance()) {
std::vector<Token> batch_tokens = iterator->GetTokens();
tokens.insert(tokens.end(), batch_tokens.begin(), batch_tokens.end());
}
return tokens;
}
} // namespace lib
} // namespace icing