| /* |
| * Copyright (C) 2017 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "ConfigFile.h" |
| |
| #include "Options.h" |
| #include <limits.h> |
| #include <mutex> |
| #include <stdio.h> |
| #include <string.h> |
| #include <wtf/ASCIICType.h> |
| #include <wtf/DataLog.h> |
| #include <wtf/text/StringBuilder.h> |
| |
| #if HAVE(REGEX_H) |
| #include <regex.h> |
| #endif |
| |
| #if OS(UNIX) |
| #include <unistd.h> |
| #endif |
| |
| namespace JSC { |
| |
| static const size_t s_processNameMax = 128; |
| char ConfigFile::s_processName[s_processNameMax + 1] = { 0 }; |
| char ConfigFile::s_parentProcessName[s_processNameMax + 1] = { 0 }; |
| |
| class ConfigFileScanner { |
| public: |
| ConfigFileScanner(const char* filename) |
| : m_filename(filename) |
| , m_lineNumber(0) |
| { |
| m_srcPtr = &m_buffer[0]; |
| m_bufferEnd = &m_buffer[0]; |
| } |
| |
| bool start() |
| { |
| m_file = fopen(m_filename, "r"); |
| if (!m_file) { |
| dataLogF("Failed to open file JSC Config file '%s'.\n", m_filename); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| unsigned lineNumber() |
| { |
| return m_lineNumber; |
| } |
| |
| const char* currentBuffer() |
| { |
| if (!m_srcPtr || m_srcPtr == m_bufferEnd) |
| return ""; |
| |
| return m_srcPtr; |
| } |
| |
| bool atFileEnd() |
| { |
| if (!fillBufferIfNeeded()) |
| return true; |
| |
| return false; |
| } |
| |
| bool tryConsume(char c) |
| { |
| if (!fillBufferIfNeeded()) |
| return false; |
| |
| if (c == *m_srcPtr) { |
| m_srcPtr++; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| template <size_t length> |
| bool tryConsume(const char (&token) [length]) |
| { |
| if (!fillBufferIfNeeded()) |
| return false; |
| |
| size_t tokenLength = length - 1; |
| if (!strncmp(m_srcPtr, token, tokenLength)) { |
| m_srcPtr += tokenLength; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| char* tryConsumeString() |
| { |
| if (!fillBufferIfNeeded()) |
| return nullptr; |
| |
| if (*m_srcPtr != '"') |
| return nullptr; |
| |
| char* stringStart = ++m_srcPtr; |
| |
| char* stringEnd = strchr(m_srcPtr, '"'); |
| if (stringEnd) { |
| *stringEnd = '\0'; |
| m_srcPtr = stringEnd + 1; |
| return stringStart; |
| } |
| |
| return nullptr; |
| } |
| |
| char* tryConsumeRegExPattern(bool& ignoreCase) |
| { |
| if (!fillBufferIfNeeded()) |
| return nullptr; |
| |
| if (*m_srcPtr != '/') |
| return nullptr; |
| |
| char* stringStart = m_srcPtr + 1; |
| |
| char* stringEnd = strchr(stringStart, '/'); |
| if (stringEnd) { |
| *stringEnd = '\0'; |
| m_srcPtr = stringEnd + 1; |
| if (*m_srcPtr == 'i') { |
| ignoreCase = true; |
| m_srcPtr++; |
| } else |
| ignoreCase = false; |
| |
| return stringStart; |
| } |
| |
| return nullptr; |
| } |
| |
| char* tryConsumeUpto(bool& foundChar, char c) |
| { |
| if (!fillBufferIfNeeded()) |
| return nullptr; |
| |
| char* start = m_srcPtr; |
| foundChar = false; |
| |
| char* cPosition = strchr(m_srcPtr, c); |
| if (cPosition) { |
| *cPosition = '\0'; |
| m_srcPtr = cPosition + 1; |
| foundChar = true; |
| } else |
| m_srcPtr = m_bufferEnd; |
| |
| return start; |
| } |
| |
| private: |
| bool fillBufferIfNeeded() |
| { |
| if (!m_srcPtr) |
| return false; |
| |
| while (true) { |
| while (m_srcPtr != m_bufferEnd && isASCIISpace(*m_srcPtr)) |
| m_srcPtr++; |
| |
| if (m_srcPtr != m_bufferEnd) |
| break; |
| |
| if (!fillBuffer()) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool fillBuffer() |
| { |
| do { |
| m_srcPtr = fgets(m_buffer, sizeof(m_buffer), m_file); |
| if (!m_srcPtr) { |
| fclose(m_file); |
| return false; |
| } |
| |
| m_lineNumber++; |
| |
| m_bufferEnd = strchr(m_srcPtr, '#'); |
| |
| if (m_bufferEnd) |
| *m_bufferEnd = '\0'; |
| else { |
| m_bufferEnd = m_srcPtr + strlen(m_srcPtr); |
| if (m_bufferEnd > m_srcPtr && m_bufferEnd[-1] == '\n') { |
| m_bufferEnd--; |
| *m_bufferEnd = '\0'; |
| } |
| } |
| } while (m_bufferEnd == m_srcPtr); |
| |
| return true; |
| } |
| |
| const char* m_filename; |
| unsigned m_lineNumber; |
| FILE* m_file; |
| char m_buffer[BUFSIZ]; |
| char* m_srcPtr; |
| char* m_bufferEnd; |
| }; |
| |
| ConfigFile::ConfigFile(const char* filename) |
| { |
| if (!filename) |
| m_filename[0] = '\0'; |
| else { |
| strncpy(m_filename, filename, s_maxPathLength); |
| m_filename[s_maxPathLength] = '\0'; |
| } |
| |
| m_configDirectory[0] = '\0'; |
| } |
| |
| void ConfigFile::setProcessName(const char* processName) |
| { |
| strncpy(s_processName, processName, s_processNameMax); |
| } |
| |
| void ConfigFile::setParentProcessName(const char* parentProcessName) |
| { |
| strncpy(s_parentProcessName, parentProcessName, s_processNameMax); |
| } |
| |
| void ConfigFile::parse() |
| { |
| enum StatementNesting { TopLevelStatment, NestedStatement, NestedStatementFailedCriteria }; |
| enum ParseResult { ParseOK, ParseError, NestedStatementDone }; |
| |
| canonicalizePaths(); |
| |
| ConfigFileScanner scanner(m_filename); |
| |
| if (!scanner.start()) |
| return; |
| |
| char logPathname[s_maxPathLength + 1] = { 0 }; |
| |
| StringBuilder jscOptionsBuilder; |
| |
| auto parseLogFile = [&](StatementNesting statementNesting) { |
| char* filename = nullptr; |
| if (scanner.tryConsume('=') && (filename = scanner.tryConsumeString())) { |
| if (statementNesting != NestedStatementFailedCriteria) { |
| if (filename[0] != '/') { |
| int spaceRequired = snprintf(logPathname, s_maxPathLength + 1, "%s/%s", m_configDirectory, filename); |
| if (static_cast<unsigned>(spaceRequired) > s_maxPathLength) |
| return ParseError; |
| } else |
| strncpy(logPathname, filename, s_maxPathLength); |
| } |
| |
| return ParseOK; |
| } |
| |
| return ParseError; |
| }; |
| |
| auto parseJSCOptions = [&](StatementNesting statementNesting) { |
| if (scanner.tryConsume('{')) { |
| StringBuilder builder; |
| |
| bool foundClosingBrace = false; |
| char* currentLine = nullptr; |
| |
| while ((currentLine = scanner.tryConsumeUpto(foundClosingBrace, '}'))) { |
| char* p = currentLine; |
| |
| do { |
| if (foundClosingBrace && !*p) |
| break; |
| |
| char* optionNameStart = p; |
| |
| while (*p && !isASCIISpace(*p) && *p != '=') |
| p++; |
| |
| builder.append(optionNameStart, p - optionNameStart); |
| |
| while (*p && isASCIISpace(*p) && *p != '=') |
| p++; |
| |
| if (!*p) |
| return ParseError; |
| p++; // Advance past the '=' |
| |
| builder.append('='); |
| |
| while (*p && isASCIISpace(*p)) |
| p++; |
| |
| if (!*p) |
| return ParseError; |
| |
| char* optionValueStart = p; |
| |
| while (*p && !isASCIISpace(*p)) |
| p++; |
| |
| builder.append(optionValueStart, p - optionValueStart); |
| builder.append('\n'); |
| |
| while (*p && isASCIISpace(*p)) |
| p++; |
| } while (*p); |
| |
| if (foundClosingBrace) |
| break; |
| } |
| |
| if (statementNesting != NestedStatementFailedCriteria) |
| jscOptionsBuilder.append(builder); |
| |
| return ParseOK; |
| } |
| |
| return ParseError; |
| }; |
| |
| auto parseNestedStatement = [&](StatementNesting statementNesting) { |
| if (scanner.tryConsume("jscOptions")) |
| return parseJSCOptions(statementNesting); |
| |
| if (scanner.tryConsume("logFile")) |
| return parseLogFile(statementNesting); |
| |
| if (scanner.tryConsume('}')) |
| return NestedStatementDone; |
| |
| return ParseError; |
| }; |
| |
| auto parsePredicate = [&](bool& predicateMatches, const char* matchValue) { |
| if (scanner.tryConsume("==")) { |
| char* predicateValue = nullptr; |
| if ((predicateValue = scanner.tryConsumeString()) && matchValue) { |
| predicateMatches = !strcmp(predicateValue, matchValue); |
| return true; |
| } |
| } |
| #if HAVE(REGEX_H) |
| else if (scanner.tryConsume("=~")) { |
| char* predicateRegExString = nullptr; |
| bool ignoreCase { false }; |
| if ((predicateRegExString = scanner.tryConsumeRegExPattern(ignoreCase)) && matchValue) { |
| regex_t predicateRegEx; |
| int regexFlags = REG_EXTENDED; |
| if (ignoreCase) |
| regexFlags |= REG_ICASE; |
| if (regcomp(&predicateRegEx, predicateRegExString, regexFlags)) |
| return false; |
| |
| predicateMatches = !regexec(&predicateRegEx, matchValue, 0, nullptr, 0); |
| return true; |
| } |
| } |
| #endif |
| |
| return false; |
| }; |
| |
| auto parseConditionalBlock = [&](StatementNesting statementNesting) { |
| if (statementNesting == NestedStatement) { |
| StatementNesting subNesting = NestedStatement; |
| |
| while (true) { |
| bool predicateMatches; |
| const char* actualValue = nullptr; |
| |
| if (scanner.tryConsume("processName")) |
| actualValue = s_processName; |
| else if (scanner.tryConsume("parentProcessName")) |
| actualValue = s_parentProcessName; |
| else if (scanner.tryConsume("build")) |
| #ifndef NDEBUG |
| actualValue = "Debug"; |
| #else |
| actualValue = "Release"; |
| #endif |
| else |
| return ParseError; |
| |
| if (parsePredicate(predicateMatches, actualValue)) { |
| if (!predicateMatches) |
| subNesting = NestedStatementFailedCriteria; |
| |
| if (!scanner.tryConsume("&&")) |
| break; |
| } |
| } |
| |
| if (!scanner.tryConsume('{')) |
| return ParseError; |
| |
| ParseResult parseResult = ParseOK; |
| while (parseResult == ParseOK && !scanner.atFileEnd()) |
| parseResult = parseNestedStatement(subNesting); |
| |
| if (parseResult == NestedStatementDone) |
| return ParseOK; |
| } |
| |
| return ParseError; |
| }; |
| |
| auto parseStatement = [&](StatementNesting statementNesting) { |
| if (scanner.tryConsume("jscOptions")) |
| return parseJSCOptions(statementNesting); |
| |
| if (scanner.tryConsume("logFile")) |
| return parseLogFile(statementNesting); |
| |
| if (statementNesting == TopLevelStatment) |
| return parseConditionalBlock(NestedStatement); |
| |
| return ParseError; |
| }; |
| |
| ParseResult parseResult = ParseOK; |
| |
| while (parseResult == ParseOK && !scanner.atFileEnd()) |
| parseResult = parseStatement(TopLevelStatment); |
| |
| if (parseResult == ParseOK) { |
| if (strlen(logPathname)) |
| WTF::setDataFile(logPathname); |
| |
| if (!jscOptionsBuilder.isEmpty()) { |
| Options::enableRestrictedOptions(true); |
| Options::setOptions(jscOptionsBuilder.toString().utf8().data()); |
| } |
| } else |
| WTF::dataLogF("Error in JSC Config file on or near line %u, parsing '%s'\n", scanner.lineNumber(), scanner.currentBuffer()); |
| } |
| |
| void ConfigFile::canonicalizePaths() |
| { |
| if (!m_filename[0]) |
| return; |
| |
| #if OS(UNIX) || OS(DARWIN) |
| if (m_filename[0] != '/') { |
| // Relative path |
| char filenameBuffer[s_maxPathLength + 1]; |
| |
| if (getcwd(filenameBuffer, sizeof(filenameBuffer))) { |
| size_t pathnameLength = strlen(filenameBuffer); |
| bool shouldAddPathSeparator = filenameBuffer[pathnameLength - 1] != '/'; |
| if (sizeof(filenameBuffer) - 1 >= pathnameLength + shouldAddPathSeparator) { |
| if (shouldAddPathSeparator) |
| strncat(filenameBuffer, "/", 2); // Room for '/' plus NUL |
| #if COMPILER(GCC) |
| #if GCC_VERSION_AT_LEAST(8, 0, 0) |
| IGNORE_WARNINGS_BEGIN("stringop-truncation") |
| #endif |
| #endif |
| strncat(filenameBuffer, m_filename, sizeof(filenameBuffer) - strlen(filenameBuffer) - 1); |
| #if COMPILER(GCC) |
| #if GCC_VERSION_AT_LEAST(8, 0, 0) |
| IGNORE_WARNINGS_END |
| #endif |
| #endif |
| strncpy(m_filename, filenameBuffer, s_maxPathLength); |
| m_filename[s_maxPathLength] = '\0'; |
| } |
| } |
| } |
| #endif |
| |
| char* lastPathSeperator = strrchr(m_filename, '/'); |
| |
| if (lastPathSeperator) { |
| unsigned dirnameLength = lastPathSeperator - &m_filename[0]; |
| strncpy(m_configDirectory, m_filename, dirnameLength); |
| m_configDirectory[dirnameLength] = '\0'; |
| } else { |
| m_configDirectory[0] = '/'; |
| m_configDirectory[1] = '\0'; |
| } |
| } |
| |
| void processConfigFile(const char* configFilename, const char* processName, const char* parentProcessName) |
| { |
| static std::once_flag processConfigFileOnceFlag; |
| |
| if (!configFilename || !strlen(configFilename)) |
| return; |
| |
| std::call_once(processConfigFileOnceFlag, [&]{ |
| if (configFilename) { |
| ConfigFile configFile(configFilename); |
| configFile.setProcessName(processName); |
| if (parentProcessName) |
| configFile.setParentProcessName(parentProcessName); |
| configFile.parse(); |
| } |
| }); |
| } |
| |
| } // namespace JSC |