blob: 9008e12985d2228c9e474c7505c1455bc48bb5bc [file] [log] [blame]
// Copyright 2016 PDFium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com
#include "core/fxcrt/xml/cfx_xmlparser.h"
#include <algorithm>
#include <cwctype>
#include <iterator>
#include <stack>
#include <utility>
#include "core/fxcrt/cfx_seekablestreamproxy.h"
#include "core/fxcrt/fx_codepage.h"
#include "core/fxcrt/fx_extension.h"
#include "core/fxcrt/fx_safe_types.h"
#include "core/fxcrt/xml/cfx_xmlchardata.h"
#include "core/fxcrt/xml/cfx_xmldocument.h"
#include "core/fxcrt/xml/cfx_xmlelement.h"
#include "core/fxcrt/xml/cfx_xmlinstruction.h"
#include "core/fxcrt/xml/cfx_xmlnode.h"
#include "core/fxcrt/xml/cfx_xmltext.h"
#include "third_party/base/check.h"
#include "third_party/base/notreached.h"
namespace {
constexpr size_t kCurrentTextReserve = 128;
constexpr uint32_t kMaxCharRange = 0x10ffff;
bool IsXMLWhiteSpace(wchar_t ch) {
return ch == L' ' || ch == 0x0A || ch == 0x0D || ch == 0x09;
}
struct FX_XMLNAMECHAR {
uint16_t wStart;
uint16_t wEnd;
bool bStartChar;
};
constexpr FX_XMLNAMECHAR kXMLNameChars[] = {
{L'-', L'.', false}, {L'0', L'9', false}, {L':', L':', false},
{L'A', L'Z', true}, {L'_', L'_', true}, {L'a', L'z', true},
{0xB7, 0xB7, false}, {0xC0, 0xD6, true}, {0xD8, 0xF6, true},
{0xF8, 0x02FF, true}, {0x0300, 0x036F, false}, {0x0370, 0x037D, true},
{0x037F, 0x1FFF, true}, {0x200C, 0x200D, true}, {0x203F, 0x2040, false},
{0x2070, 0x218F, true}, {0x2C00, 0x2FEF, true}, {0x3001, 0xD7FF, true},
{0xF900, 0xFDCF, true}, {0xFDF0, 0xFFFD, true},
};
} // namespace
// static
bool CFX_XMLParser::IsXMLNameChar(wchar_t ch, bool bFirstChar) {
auto* it = std::lower_bound(
std::begin(kXMLNameChars), std::end(kXMLNameChars), ch,
[](const FX_XMLNAMECHAR& arg, wchar_t ch) { return arg.wEnd < ch; });
return it != std::end(kXMLNameChars) && ch >= it->wStart &&
(!bFirstChar || it->bStartChar);
}
CFX_XMLParser::CFX_XMLParser(const RetainPtr<IFX_SeekableReadStream>& pStream) {
DCHECK(pStream);
auto proxy = pdfium::MakeRetain<CFX_SeekableStreamProxy>(pStream);
FX_CodePage wCodePage = proxy->GetCodePage();
if (wCodePage != FX_CodePage::kUTF16LE &&
wCodePage != FX_CodePage::kUTF16BE && wCodePage != FX_CodePage::kUTF8) {
proxy->SetCodePage(FX_CodePage::kUTF8);
}
stream_ = proxy;
xml_plane_size_ = std::min(
xml_plane_size_, pdfium::base::checked_cast<size_t>(stream_->GetSize()));
current_text_.reserve(kCurrentTextReserve);
}
CFX_XMLParser::~CFX_XMLParser() = default;
std::unique_ptr<CFX_XMLDocument> CFX_XMLParser::Parse() {
auto doc = std::make_unique<CFX_XMLDocument>();
current_node_ = doc->GetRoot();
return DoSyntaxParse(doc.get()) ? std::move(doc) : nullptr;
}
bool CFX_XMLParser::DoSyntaxParse(CFX_XMLDocument* doc) {
if (xml_plane_size_ <= 0)
return false;
FX_SAFE_SIZE_T alloc_size_safe = xml_plane_size_;
alloc_size_safe += 1; // For NUL.
if (!alloc_size_safe.IsValid())
return false;
FX_FILESIZE current_buffer_idx = 0;
FX_FILESIZE buffer_size = 0;
std::vector<wchar_t, FxAllocAllocator<wchar_t>> buffer;
buffer.resize(alloc_size_safe.ValueOrDie());
std::stack<wchar_t> character_to_skip_too_stack;
std::stack<CFX_XMLNode::Type> node_type_stack;
WideString current_attribute_name;
FDE_XmlSyntaxState current_parser_state = FDE_XmlSyntaxState::Text;
int32_t iCount = 0;
wchar_t current_quote_character = 0;
wchar_t current_character_to_skip_to = 0;
while (true) {
if (current_buffer_idx >= buffer_size) {
if (stream_->IsEOF())
return true;
size_t buffer_chars = stream_->ReadBlock(buffer.data(), xml_plane_size_);
if (buffer_chars == 0)
return true;
current_buffer_idx = 0;
buffer_size = buffer_chars;
}
while (current_buffer_idx < buffer_size) {
wchar_t ch = buffer[current_buffer_idx];
switch (current_parser_state) {
case FDE_XmlSyntaxState::Text:
if (ch == L'<') {
if (!current_text_.empty()) {
current_node_->AppendLastChild(
doc->CreateNode<CFX_XMLText>(GetTextData()));
} else {
current_buffer_idx++;
current_parser_state = FDE_XmlSyntaxState::Node;
}
} else {
// Fail if there is text outside of the root element, ignore
// whitespace/null.
if (node_type_stack.empty() && ch && !FXSYS_iswspace(ch))
return false;
ProcessTextChar(ch);
current_buffer_idx++;
}
break;
case FDE_XmlSyntaxState::Node:
if (ch == L'!') {
current_buffer_idx++;
current_parser_state = FDE_XmlSyntaxState::SkipCommentOrDecl;
} else if (ch == L'/') {
current_buffer_idx++;
current_parser_state = FDE_XmlSyntaxState::CloseElement;
} else if (ch == L'?') {
node_type_stack.push(CFX_XMLNode::Type::kInstruction);
current_buffer_idx++;
current_parser_state = FDE_XmlSyntaxState::Target;
} else {
node_type_stack.push(CFX_XMLNode::Type::kElement);
current_parser_state = FDE_XmlSyntaxState::Tag;
}
break;
case FDE_XmlSyntaxState::Target:
if (!IsXMLNameChar(ch, current_text_.empty())) {
if (current_text_.empty())
return false;
current_parser_state = FDE_XmlSyntaxState::TargetData;
WideString target_name = GetTextData();
if (target_name.EqualsASCII("originalXFAVersion") ||
target_name.EqualsASCII("acrobat")) {
auto* node = doc->CreateNode<CFX_XMLInstruction>(target_name);
current_node_->AppendLastChild(node);
current_node_ = node;
}
} else {
current_text_.push_back(ch);
current_buffer_idx++;
}
break;
case FDE_XmlSyntaxState::Tag:
if (!IsXMLNameChar(ch, current_text_.empty())) {
if (current_text_.empty())
return false;
current_parser_state = FDE_XmlSyntaxState::AttriName;
auto* child = doc->CreateNode<CFX_XMLElement>(GetTextData());
current_node_->AppendLastChild(child);
current_node_ = child;
} else {
current_text_.push_back(ch);
current_buffer_idx++;
}
break;
case FDE_XmlSyntaxState::AttriName:
if (current_text_.empty() && IsXMLWhiteSpace(ch)) {
current_buffer_idx++;
break;
}
if (!IsXMLNameChar(ch, current_text_.empty())) {
if (current_text_.empty()) {
if (node_type_stack.top() == CFX_XMLNode::Type::kElement) {
if (ch == L'>' || ch == L'/') {
current_parser_state = FDE_XmlSyntaxState::BreakElement;
break;
}
} else if (node_type_stack.top() ==
CFX_XMLNode::Type::kInstruction) {
if (ch == L'?') {
current_parser_state = FDE_XmlSyntaxState::CloseInstruction;
current_buffer_idx++;
} else {
current_parser_state = FDE_XmlSyntaxState::TargetData;
}
break;
}
return false;
} else {
if (node_type_stack.top() == CFX_XMLNode::Type::kInstruction) {
if (ch != '=' && !IsXMLWhiteSpace(ch)) {
current_parser_state = FDE_XmlSyntaxState::TargetData;
break;
}
}
current_parser_state = FDE_XmlSyntaxState::AttriEqualSign;
current_attribute_name = GetTextData();
}
} else {
current_text_.push_back(ch);
current_buffer_idx++;
}
break;
case FDE_XmlSyntaxState::AttriEqualSign:
if (IsXMLWhiteSpace(ch)) {
current_buffer_idx++;
break;
}
if (ch != L'=') {
if (node_type_stack.top() == CFX_XMLNode::Type::kInstruction) {
current_parser_state = FDE_XmlSyntaxState::TargetData;
break;
}
return false;
} else {
current_parser_state = FDE_XmlSyntaxState::AttriQuotation;
current_buffer_idx++;
}
break;
case FDE_XmlSyntaxState::AttriQuotation:
if (IsXMLWhiteSpace(ch)) {
current_buffer_idx++;
break;
}
if (ch != L'\"' && ch != L'\'') {
return false;
}
current_quote_character = ch;
current_parser_state = FDE_XmlSyntaxState::AttriValue;
current_buffer_idx++;
break;
case FDE_XmlSyntaxState::AttriValue:
if (ch == current_quote_character) {
if (entity_start_ > -1)
return false;
current_quote_character = 0;
current_buffer_idx++;
current_parser_state = FDE_XmlSyntaxState::AttriName;
CFX_XMLElement* elem = ToXMLElement(current_node_);
if (elem)
elem->SetAttribute(current_attribute_name, GetTextData());
current_attribute_name.clear();
} else {
ProcessTextChar(ch);
current_buffer_idx++;
}
break;
case FDE_XmlSyntaxState::CloseInstruction:
if (ch != L'>') {
current_text_.push_back(ch);
current_parser_state = FDE_XmlSyntaxState::TargetData;
} else if (!current_text_.empty()) {
ProcessTargetData();
} else {
current_buffer_idx++;
if (node_type_stack.empty())
return false;
node_type_stack.pop();
current_parser_state = FDE_XmlSyntaxState::Text;
if (current_node_ &&
current_node_->GetType() == CFX_XMLNode::Type::kInstruction)
current_node_ = current_node_->GetParent();
}
break;
case FDE_XmlSyntaxState::BreakElement:
if (ch == L'>') {
current_parser_state = FDE_XmlSyntaxState::Text;
} else if (ch == L'/') {
current_parser_state = FDE_XmlSyntaxState::CloseElement;
} else {
return false;
}
current_buffer_idx++;
break;
case FDE_XmlSyntaxState::CloseElement:
if (!IsXMLNameChar(ch, current_text_.empty())) {
if (ch == L'>') {
if (node_type_stack.empty())
return false;
node_type_stack.pop();
current_parser_state = FDE_XmlSyntaxState::Text;
CFX_XMLElement* element = ToXMLElement(current_node_);
if (!element)
return false;
WideString element_name = GetTextData();
if (element_name.GetLength() > 0 &&
element_name != element->GetName()) {
return false;
}
current_node_ = current_node_->GetParent();
iCount++;
} else if (!IsXMLWhiteSpace(ch)) {
return false;
}
} else {
current_text_.push_back(ch);
}
current_buffer_idx++;
break;
case FDE_XmlSyntaxState::SkipCommentOrDecl: {
auto current_span =
pdfium::make_span(buffer).subspan(current_buffer_idx);
if (FXSYS_wcsnicmp(current_span.data(), L"--", 2) == 0) {
current_buffer_idx += 2;
current_parser_state = FDE_XmlSyntaxState::SkipComment;
} else if (FXSYS_wcsnicmp(current_span.data(), L"[CDATA[", 7) == 0) {
current_buffer_idx += 7;
current_parser_state = FDE_XmlSyntaxState::SkipCData;
} else {
current_parser_state = FDE_XmlSyntaxState::SkipDeclNode;
current_character_to_skip_to = L'>';
character_to_skip_too_stack.push(L'>');
}
break;
}
case FDE_XmlSyntaxState::SkipCData: {
auto current_span =
pdfium::make_span(buffer).subspan(current_buffer_idx);
if (FXSYS_wcsnicmp(current_span.data(), L"]]>", 3) == 0) {
current_buffer_idx += 3;
current_parser_state = FDE_XmlSyntaxState::Text;
current_node_->AppendLastChild(
doc->CreateNode<CFX_XMLCharData>(GetTextData()));
} else {
current_text_.push_back(ch);
current_buffer_idx++;
}
break;
}
case FDE_XmlSyntaxState::SkipDeclNode:
if (current_character_to_skip_to == L'\'' ||
current_character_to_skip_to == L'\"') {
current_buffer_idx++;
if (ch != current_character_to_skip_to)
break;
character_to_skip_too_stack.pop();
if (character_to_skip_too_stack.empty())
current_parser_state = FDE_XmlSyntaxState::Text;
else
current_character_to_skip_to = character_to_skip_too_stack.top();
} else {
switch (ch) {
case L'<':
current_character_to_skip_to = L'>';
character_to_skip_too_stack.push(L'>');
break;
case L'[':
current_character_to_skip_to = L']';
character_to_skip_too_stack.push(L']');
break;
case L'(':
current_character_to_skip_to = L')';
character_to_skip_too_stack.push(L')');
break;
case L'\'':
current_character_to_skip_to = L'\'';
character_to_skip_too_stack.push(L'\'');
break;
case L'\"':
current_character_to_skip_to = L'\"';
character_to_skip_too_stack.push(L'\"');
break;
default:
if (ch == current_character_to_skip_to) {
character_to_skip_too_stack.pop();
if (character_to_skip_too_stack.empty()) {
current_parser_state = FDE_XmlSyntaxState::Text;
} else {
current_character_to_skip_to =
character_to_skip_too_stack.top();
}
}
break;
}
current_buffer_idx++;
}
break;
case FDE_XmlSyntaxState::SkipComment: {
auto current_span =
pdfium::make_span(buffer).subspan(current_buffer_idx);
if (FXSYS_wcsnicmp(current_span.data(), L"-->", 3) == 0) {
current_buffer_idx += 2;
current_parser_state = FDE_XmlSyntaxState::Text;
}
current_buffer_idx++;
break;
}
case FDE_XmlSyntaxState::TargetData:
if (IsXMLWhiteSpace(ch)) {
if (current_text_.empty()) {
current_buffer_idx++;
break;
}
if (current_quote_character == 0) {
current_buffer_idx++;
ProcessTargetData();
break;
}
}
if (ch == '?') {
current_parser_state = FDE_XmlSyntaxState::CloseInstruction;
current_buffer_idx++;
} else if (ch == '\"') {
if (current_quote_character == 0) {
current_quote_character = ch;
current_buffer_idx++;
} else if (ch == current_quote_character) {
current_quote_character = 0;
current_buffer_idx++;
ProcessTargetData();
} else {
return false;
}
} else {
current_text_.push_back(ch);
current_buffer_idx++;
}
break;
default:
break;
}
}
}
NOTREACHED();
return false;
}
void CFX_XMLParser::ProcessTextChar(wchar_t character) {
current_text_.push_back(character);
if (entity_start_ > -1 && character == L';') {
// Copy the entity out into a string and remove from the vector. When we
// copy the entity we don't want to copy out the & or the ; so we start
// shifted by one and want to copy 2 less characters in total.
WideString csEntity(current_text_.data() + entity_start_ + 1,
current_text_.size() - entity_start_ - 2);
current_text_.erase(current_text_.begin() + entity_start_,
current_text_.end());
int32_t iLen = csEntity.GetLength();
if (iLen > 0) {
if (csEntity[0] == L'#') {
uint32_t ch = 0;
if (iLen > 1 && csEntity[1] == L'x') {
for (int32_t i = 2; i < iLen; i++) {
if (!FXSYS_IsHexDigit(csEntity[i]))
break;
ch = (ch << 4) + FXSYS_HexCharToInt(csEntity[i]);
}
} else {
for (int32_t i = 1; i < iLen; i++) {
if (!FXSYS_IsDecimalDigit(csEntity[i]))
break;
ch = ch * 10 + FXSYS_DecimalCharToInt(csEntity[i]);
}
}
if (ch > kMaxCharRange)
ch = ' ';
character = static_cast<wchar_t>(ch);
if (character != 0)
current_text_.push_back(character);
} else {
if (csEntity == L"amp") {
current_text_.push_back(L'&');
} else if (csEntity == L"lt") {
current_text_.push_back(L'<');
} else if (csEntity == L"gt") {
current_text_.push_back(L'>');
} else if (csEntity == L"apos") {
current_text_.push_back(L'\'');
} else if (csEntity == L"quot") {
current_text_.push_back(L'"');
}
}
}
entity_start_ = -1;
} else if (entity_start_ < 0 && character == L'&') {
entity_start_ = current_text_.size() - 1;
}
}
void CFX_XMLParser::ProcessTargetData() {
WideString target_data = GetTextData();
if (target_data.IsEmpty())
return;
CFX_XMLInstruction* instruction = ToXMLInstruction(current_node_);
if (instruction)
instruction->AppendData(target_data);
}
WideString CFX_XMLParser::GetTextData() {
WideString ret(current_text_.data(), current_text_.size());
entity_start_ = -1;
current_text_.clear();
current_text_.reserve(kCurrentTextReserve);
return ret;
}