/* * Copyright (C) 2005-2018 Team Kodi * This file is part of Kodi - https://kodi.tv * * SPDX-License-Identifier: GPL-2.0-or-later * See LICENSES/README.md for more information. */ #include "RegExp.h" #include "log.h" #include "utils/StringUtils.h" #include "utils/Utf8Utils.h" #include #include #include using namespace PCRE; #ifndef PCRE_UCP #define PCRE_UCP 0 #endif // PCRE_UCP #ifdef PCRE_CONFIG_JIT #define PCRE_HAS_JIT_CODE 1 #endif #ifndef PCRE_STUDY_JIT_COMPILE #define PCRE_STUDY_JIT_COMPILE 0 #endif #ifndef PCRE_INFO_JIT // some unused number #define PCRE_INFO_JIT 2048 #endif #ifndef PCRE_HAS_JIT_CODE #define pcre_free_study(x) pcre_free((x)) #endif int CRegExp::m_Utf8Supported = -1; int CRegExp::m_UcpSupported = -1; int CRegExp::m_JitSupported = -1; CRegExp::CRegExp(bool caseless /*= false*/, CRegExp::utf8Mode utf8 /*= asciiOnly*/) { InitValues(caseless, utf8); } void CRegExp::InitValues(bool caseless /*= false*/, CRegExp::utf8Mode utf8 /*= asciiOnly*/) { m_utf8Mode = utf8; m_re = NULL; m_sd = NULL; m_iOptions = PCRE_DOTALL | PCRE_NEWLINE_ANY; if(caseless) m_iOptions |= PCRE_CASELESS; if (m_utf8Mode == forceUtf8) { if (IsUtf8Supported()) m_iOptions |= PCRE_UTF8; if (AreUnicodePropertiesSupported()) m_iOptions |= PCRE_UCP; } m_offset = 0; m_jitCompiled = false; m_bMatched = false; m_iMatchCount = 0; m_jitStack = NULL; memset(m_iOvector, 0, sizeof(m_iOvector)); } CRegExp::CRegExp(bool caseless, CRegExp::utf8Mode utf8, const char *re, studyMode study /*= NoStudy*/) { if (utf8 == autoUtf8) utf8 = requireUtf8(re) ? forceUtf8 : asciiOnly; InitValues(caseless, utf8); RegComp(re, study); } bool CRegExp::requireUtf8(const std::string& regexp) { // enable UTF-8 mode if regexp string has UTF-8 multibyte sequences if (CUtf8Utils::checkStrForUtf8(regexp) == CUtf8Utils::utf8string) return true; // check for explicit Unicode Properties (\p, \P, \X) and for Unicode character codes (greater than 0xFF) in form \x{hhh..} // note: PCRE change meaning of \w, \s, \d (and \W, \S, \D) when Unicode Properties are enabled, // but in auto mode we enable UNP for US-ASCII regexp only if regexp contains explicit \p, \P, \X or Unicode character code const char* const regexpC = regexp.c_str(); const size_t len = regexp.length(); size_t pos = 0; while (pos < len) { const char chr = regexpC[pos]; if (chr == '\\') { const char nextChr = regexpC[pos + 1]; if (nextChr == 'p' || nextChr == 'P' || nextChr == 'X') return true; // found Unicode Properties else if (nextChr == 'Q') pos = regexp.find("\\E", pos + 2); // skip all literals in "\Q...\E" else if (nextChr == 'x' && regexpC[pos + 2] == '{') { // Unicode character with hex code if (readCharXCode(regexp, pos) >= 0x100) return true; // found Unicode character code } else if (nextChr == '\\' || nextChr == '(' || nextChr == ')' || nextChr == '[' || nextChr == ']') pos++; // exclude next character from analyze } // chr != '\\' else if (chr == '(' && regexpC[pos + 1] == '?' && regexpC[pos + 2] == '#') // comment in regexp pos = regexp.find(')', pos); // skip comment else if (chr == '[') { if (isCharClassWithUnicode(regexp, pos)) return true; } if (pos == std::string::npos) // check results of regexp.find() and isCharClassWithUnicode return false; pos++; } // no Unicode Properties was found return false; } inline int CRegExp::readCharXCode(const std::string& regexp, size_t& pos) { // read hex character code in form "\x{hh..}" // 'pos' must point to '\' if (pos >= regexp.length()) return -1; const char* const regexpC = regexp.c_str(); if (regexpC[pos] != '\\' || regexpC[pos + 1] != 'x' || regexpC[pos + 2] != '{') return -1; pos++; const size_t startPos = pos; // 'startPos' points to 'x' const size_t closingBracketPos = regexp.find('}', startPos + 2); if (closingBracketPos == std::string::npos) return 0; // return character zero code, leave 'pos' at 'x' pos++; // 'pos' points to '{' int chCode = 0; while (++pos < closingBracketPos) { const int xdigitVal = StringUtils::asciixdigitvalue(regexpC[pos]); if (xdigitVal >= 0) chCode = chCode * 16 + xdigitVal; else { // found non-hexdigit pos = startPos; // reset 'pos' to 'startPos', process "{hh..}" as non-code return 0; // return character zero code } } return chCode; } bool CRegExp::isCharClassWithUnicode(const std::string& regexp, size_t& pos) { const char* const regexpC = regexp.c_str(); const size_t len = regexp.length(); if (pos > len || regexpC[pos] != '[') return false; // look for Unicode character code "\x{hhh..}" and Unicode properties "\P", "\p" and "\X" // find end (terminating ']') of character class (like "[a-h45]") // detect nested POSIX classes like "[[:lower:]]" and escaped brackets like "[\]]" bool needUnicode = false; while (++pos < len) { if (regexpC[pos] == '[' && regexpC[pos + 1] == ':') { // possible POSIX character class, like "[:alpha:]" const size_t nextClosingBracketPos = regexp.find(']', pos + 2); // don't care about "\]", as it produce error if used inside POSIX char class if (nextClosingBracketPos == std::string::npos) { // error in regexp: no closing ']' for character class pos = std::string::npos; return needUnicode; } else if (regexpC[nextClosingBracketPos - 1] == ':') pos = nextClosingBracketPos; // skip POSIX character class // if ":]" is not found, process "[:..." as part of normal character class } else if (regexpC[pos] == ']') return needUnicode; // end of character class else if (regexpC[pos] == '\\') { const char nextChar = regexpC[pos + 1]; if (nextChar == ']' || nextChar == '[') pos++; // skip next character else if (nextChar == 'Q') { pos = regexp.find("\\E", pos + 2); if (pos == std::string::npos) return needUnicode; // error in regexp: no closing "\E" after "\Q" in character class else pos++; // skip "\E" } else if (nextChar == 'p' || nextChar == 'P' || nextChar == 'X') needUnicode = true; // don't care about property name as it can contain only ASCII chars else if (nextChar == 'x') { if (readCharXCode(regexp, pos) >= 0x100) needUnicode = true; } } } pos = std::string::npos; // closing square bracket was not found return needUnicode; } CRegExp::CRegExp(const CRegExp& re) { m_re = NULL; m_sd = NULL; m_jitStack = NULL; m_utf8Mode = re.m_utf8Mode; m_iOptions = re.m_iOptions; *this = re; } CRegExp& CRegExp::operator=(const CRegExp& re) { size_t size; Cleanup(); m_jitCompiled = false; m_pattern = re.m_pattern; if (re.m_re) { if (pcre_fullinfo(re.m_re, NULL, PCRE_INFO_SIZE, &size) >= 0) { if ((m_re = (pcre*)malloc(size))) { memcpy(m_re, re.m_re, size); memcpy(m_iOvector, re.m_iOvector, OVECCOUNT*sizeof(int)); m_offset = re.m_offset; m_iMatchCount = re.m_iMatchCount; m_bMatched = re.m_bMatched; m_subject = re.m_subject; m_iOptions = re.m_iOptions; } else CLog::Log(LOGFATAL, "{}: Failed to allocate memory", __FUNCTION__); } } return *this; } CRegExp::~CRegExp() { Cleanup(); } bool CRegExp::RegComp(const char *re, studyMode study /*= NoStudy*/) { if (!re) return false; m_offset = 0; m_jitCompiled = false; m_bMatched = false; m_iMatchCount = 0; const char *errMsg = NULL; int errOffset = 0; int options = m_iOptions; if (m_utf8Mode == autoUtf8 && requireUtf8(re)) options |= (IsUtf8Supported() ? PCRE_UTF8 : 0) | (AreUnicodePropertiesSupported() ? PCRE_UCP : 0); Cleanup(); m_re = pcre_compile(re, options, &errMsg, &errOffset, NULL); if (!m_re) { m_pattern.clear(); CLog::Log(LOGERROR, "PCRE: {}. Compilation failed at offset {} in expression '{}'", errMsg, errOffset, re); return false; } m_pattern = re; if (study) { const bool jitCompile = (study == StudyWithJitComp) && IsJitSupported(); const int studyOptions = jitCompile ? PCRE_STUDY_JIT_COMPILE : 0; m_sd = pcre_study(m_re, studyOptions, &errMsg); if (errMsg != NULL) { CLog::Log(LOGWARNING, "{}: PCRE error \"{}\" while studying expression", __FUNCTION__, errMsg); if (m_sd != NULL) { pcre_free_study(m_sd); m_sd = NULL; } } else if (jitCompile) { int jitPresent = 0; m_jitCompiled = (pcre_fullinfo(m_re, m_sd, PCRE_INFO_JIT, &jitPresent) == 0 && jitPresent == 1); } } return true; } int CRegExp::RegFind(const char *str, unsigned int startoffset /*= 0*/, int maxNumberOfCharsToTest /*= -1*/) { return PrivateRegFind(strlen(str), str, startoffset, maxNumberOfCharsToTest); } int CRegExp::PrivateRegFind(size_t bufferLen, const char *str, unsigned int startoffset /* = 0*/, int maxNumberOfCharsToTest /*= -1*/) { m_offset = 0; m_bMatched = false; m_iMatchCount = 0; if (!m_re) { CLog::Log(LOGERROR, "PCRE: Called before compilation"); return -1; } if (!str) { CLog::Log(LOGERROR, "PCRE: Called without a string to match"); return -1; } if (startoffset > bufferLen) { CLog::Log(LOGERROR, "{}: startoffset is beyond end of string to match", __FUNCTION__); return -1; } #ifdef PCRE_HAS_JIT_CODE if (m_jitCompiled && !m_jitStack) { m_jitStack = pcre_jit_stack_alloc(32*1024, 512*1024); if (m_jitStack == NULL) CLog::Log(LOGWARNING, "{}: can't allocate address space for JIT stack", __FUNCTION__); pcre_assign_jit_stack(m_sd, NULL, m_jitStack); } #endif if (maxNumberOfCharsToTest >= 0) bufferLen = std::min(bufferLen, startoffset + maxNumberOfCharsToTest); m_subject.assign(str + startoffset, bufferLen - startoffset); int rc = pcre_exec(m_re, NULL, m_subject.c_str(), m_subject.length(), 0, 0, m_iOvector, OVECCOUNT); if (rc<1) { static const int fragmentLen = 80; // length of excerpt before erroneous char for log switch(rc) { case PCRE_ERROR_NOMATCH: return -1; case PCRE_ERROR_MATCHLIMIT: CLog::Log(LOGERROR, "PCRE: Match limit reached"); return -1; #ifdef PCRE_ERROR_SHORTUTF8 case PCRE_ERROR_SHORTUTF8: { const size_t startPos = (m_subject.length() > fragmentLen) ? CUtf8Utils::RFindValidUtf8Char(m_subject, m_subject.length() - fragmentLen) : 0; if (startPos != std::string::npos) CLog::Log( LOGERROR, "PCRE: Bad UTF-8 character at the end of string. Text before bad character: \"{}\"", m_subject.substr(startPos)); else CLog::Log(LOGERROR, "PCRE: Bad UTF-8 character at the end of string"); return -1; } #endif case PCRE_ERROR_BADUTF8: { const size_t startPos = (m_iOvector[0] > fragmentLen) ? CUtf8Utils::RFindValidUtf8Char(m_subject, m_iOvector[0] - fragmentLen) : 0; if (m_iOvector[0] >= 0 && startPos != std::string::npos) CLog::Log(LOGERROR, "PCRE: Bad UTF-8 character, error code: {}, position: {}. Text before bad " "char: \"{}\"", m_iOvector[1], m_iOvector[0], m_subject.substr(startPos, m_iOvector[0] - startPos + 1)); else CLog::Log(LOGERROR, "PCRE: Bad UTF-8 character, error code: {}, position: {}", m_iOvector[1], m_iOvector[0]); return -1; } case PCRE_ERROR_BADUTF8_OFFSET: CLog::Log(LOGERROR, "PCRE: Offset is pointing to the middle of UTF-8 character"); return -1; default: CLog::Log(LOGERROR, "PCRE: Unknown error: {}", rc); return -1; } } m_offset = startoffset; m_bMatched = true; m_iMatchCount = rc; return m_iOvector[0] + m_offset; } int CRegExp::GetCaptureTotal() const { int c = -1; if (m_re) pcre_fullinfo(m_re, NULL, PCRE_INFO_CAPTURECOUNT, &c); return c; } std::string CRegExp::GetReplaceString(const std::string& sReplaceExp) const { if (!m_bMatched || sReplaceExp.empty()) return ""; const char* const expr = sReplaceExp.c_str(); size_t pos = sReplaceExp.find_first_of("\\&"); std::string result(sReplaceExp, 0, pos); result.reserve(sReplaceExp.size()); // very rough estimate while(pos != std::string::npos) { if (expr[pos] == '\\') { // string is null-terminated and current char isn't null, so it's safe to advance to next char pos++; // advance to next char const char nextChar = expr[pos]; if (nextChar == '&' || nextChar == '\\') { // this is "\&" or "\\" combination result.push_back(nextChar); // add '&' or '\' to result pos++; } else if (isdigit(nextChar)) { // this is "\0" - "\9" combination int subNum = nextChar - '0'; pos++; // advance to second next char const char secondNextChar = expr[pos]; if (isdigit(secondNextChar)) { // this is "\00" - "\99" combination subNum = subNum * 10 + (secondNextChar - '0'); pos++; } result.append(GetMatch(subNum)); } } else { // '&' char result.append(GetMatch(0)); pos++; } const size_t nextPos = sReplaceExp.find_first_of("\\&", pos); result.append(sReplaceExp, pos, nextPos - pos); pos = nextPos; } return result; } int CRegExp::GetSubStart(int iSub) const { if (!IsValidSubNumber(iSub)) return -1; return m_iOvector[iSub*2] + m_offset; } int CRegExp::GetSubStart(const std::string& subName) const { return GetSubStart(GetNamedSubPatternNumber(subName.c_str())); } int CRegExp::GetSubLength(int iSub) const { if (!IsValidSubNumber(iSub)) return -1; return m_iOvector[(iSub*2)+1] - m_iOvector[(iSub*2)]; } int CRegExp::GetSubLength(const std::string& subName) const { return GetSubLength(GetNamedSubPatternNumber(subName.c_str())); } std::string CRegExp::GetMatch(int iSub /* = 0 */) const { if (!IsValidSubNumber(iSub)) return ""; int pos = m_iOvector[(iSub*2)]; int len = m_iOvector[(iSub*2)+1] - pos; if (pos < 0 || len <= 0) return ""; return m_subject.substr(pos, len); } std::string CRegExp::GetMatch(const std::string& subName) const { return GetMatch(GetNamedSubPatternNumber(subName.c_str())); } bool CRegExp::GetNamedSubPattern(const char* strName, std::string& strMatch) const { strMatch.clear(); int iSub = pcre_get_stringnumber(m_re, strName); if (!IsValidSubNumber(iSub)) return false; strMatch = GetMatch(iSub); return true; } int CRegExp::GetNamedSubPatternNumber(const char* strName) const { return pcre_get_stringnumber(m_re, strName); } void CRegExp::DumpOvector(int iLog /* = LOGDEBUG */) { if (iLog < LOGDEBUG || iLog > LOGNONE) return; std::string str = "{"; int size = GetSubCount(); // past the subpatterns is junk for (int i = 0; i <= size; i++) { std::string t = StringUtils::Format("[{},{}]", m_iOvector[(i * 2)], m_iOvector[(i * 2) + 1]); if (i != size) t += ","; str += t; } str += "}"; CLog::Log(iLog, "regexp ovector={}", str); } void CRegExp::Cleanup() { if (m_re) { pcre_free(m_re); m_re = NULL; } if (m_sd) { pcre_free_study(m_sd); m_sd = NULL; } #ifdef PCRE_HAS_JIT_CODE if (m_jitStack) { pcre_jit_stack_free(m_jitStack); m_jitStack = NULL; } #endif } inline bool CRegExp::IsValidSubNumber(int iSub) const { return iSub >= 0 && iSub <= m_iMatchCount && iSub <= m_MaxNumOfBackrefrences; } bool CRegExp::IsUtf8Supported(void) { if (m_Utf8Supported == -1) { if (pcre_config(PCRE_CONFIG_UTF8, &m_Utf8Supported) != 0) m_Utf8Supported = 0; } return m_Utf8Supported == 1; } bool CRegExp::AreUnicodePropertiesSupported(void) { #if defined(PCRE_CONFIG_UNICODE_PROPERTIES) && PCRE_UCP != 0 if (m_UcpSupported == -1) { if (pcre_config(PCRE_CONFIG_UNICODE_PROPERTIES, &m_UcpSupported) != 0) m_UcpSupported = 0; } #endif return m_UcpSupported == 1; } bool CRegExp::LogCheckUtf8Support(void) { bool utf8FullSupport = true; if (!CRegExp::IsUtf8Supported()) { utf8FullSupport = false; CLog::Log(LOGWARNING, "UTF-8 is not supported in PCRE lib, support for national symbols is limited!"); } if (!CRegExp::AreUnicodePropertiesSupported()) { utf8FullSupport = false; CLog::Log(LOGWARNING, "Unicode properties are not enabled in PCRE lib, support for national symbols may be limited!"); } if (!utf8FullSupport) { CLog::Log(LOGINFO, "Consider installing PCRE lib version 8.10 or later with enabled Unicode properties " "and UTF-8 support. Your PCRE lib version: {}", PCRE::pcre_version()); #if PCRE_UCP == 0 CLog::Log(LOGINFO, "You will need to rebuild XBMC after PCRE lib update."); #endif } return utf8FullSupport; } bool CRegExp::IsJitSupported(void) { if (m_JitSupported == -1) { #ifdef PCRE_HAS_JIT_CODE if (pcre_config(PCRE_CONFIG_JIT, &m_JitSupported) != 0) #endif m_JitSupported = 0; } return m_JitSupported == 1; }