| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you 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. |
| */ |
| |
| /** |
| * @file path.cc |
| * @brief Prefetch path construction helpers. |
| */ |
| |
| #include "path.h" |
| |
| #include <cctype> |
| #include <utility> |
| |
| namespace |
| { |
| String |
| getDirectoryPrefix(const String &path) |
| { |
| const String::size_type lastSlash = path.find_last_of('/'); |
| |
| if (String::npos == lastSlash) { |
| return {}; |
| } |
| return path.substr(0, lastSlash + 1); |
| } |
| |
| bool |
| normalizePath(StringView path, String &normalized) |
| { |
| const bool absolute = !path.empty() && '/' == path.front(); |
| StringList segments; |
| |
| normalized.clear(); |
| |
| for (String::size_type offset = 0; offset <= path.size();) { |
| const String::size_type nextSeparator = path.find('/', offset); |
| const StringView segment = path.substr(offset, nextSeparator - offset); |
| |
| if (segment.empty() || "." == segment) { |
| // Skip empty and current-directory segments. |
| } else if (".." == segment) { |
| if (segments.empty()) { |
| return false; |
| } |
| segments.pop_back(); |
| } else { |
| segments.emplace_back(segment); |
| } |
| |
| if (String::npos == nextSeparator) { |
| break; |
| } |
| offset = nextSeparator + 1; |
| } |
| |
| if (absolute) { |
| normalized.push_back('/'); |
| } |
| |
| for (const auto &segment : segments) { |
| if (!normalized.empty() && '/' != normalized.back()) { |
| normalized.push_back('/'); |
| } |
| normalized.append(segment); |
| } |
| |
| if (!path.empty() && '/' == path.back() && (normalized.empty() || '/' != normalized.back())) { |
| normalized.push_back('/'); |
| } |
| |
| return true; |
| } |
| |
| int |
| hexDigitValue(char c) |
| { |
| if ('0' <= c && c <= '9') { |
| return c - '0'; |
| } |
| c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); |
| if ('a' <= c && c <= 'f') { |
| return c - 'a' + 10; |
| } |
| return -1; |
| } |
| |
| String |
| decodePathSeparatorsAndDots(StringView path) |
| { |
| String decoded; |
| |
| decoded.reserve(path.size()); |
| for (String::size_type i = 0; i < path.size(); ++i) { |
| if ('\\' == path[i]) { |
| decoded.push_back('/'); |
| } else if ('%' == path[i] && i + 2 < path.size()) { |
| const int high = hexDigitValue(path[i + 1]); |
| const int low = hexDigitValue(path[i + 2]); |
| if (0 <= high && 0 <= low) { |
| const char c = static_cast<char>((high << 4) | low); |
| if ('.' == c || '/' == c || '\\' == c) { |
| decoded.push_back('\\' == c ? '/' : c); |
| } else { |
| decoded.append(path.substr(i, 3)); |
| } |
| i += 2; |
| } else { |
| decoded.push_back(path[i]); |
| } |
| } else { |
| decoded.push_back(path[i]); |
| } |
| } |
| |
| return decoded; |
| } |
| |
| bool |
| isUnderPrefix(const String &path, const String &prefix) |
| { |
| return prefix.empty() || path.rfind(prefix, 0) == 0; |
| } |
| |
| bool |
| startsWithSeparator(StringView path) |
| { |
| return !path.empty() && ('/' == path.front() || '\\' == path.front()); |
| } |
| } // namespace |
| |
| bool |
| makeSafeRelativeFetchPath(const String ¤tPath, const String &relativePath, SafeRelativeFetchPath &fetchPath) |
| { |
| fetchPath = {}; |
| |
| if (String::npos != relativePath.find('#')) { |
| return false; |
| } |
| |
| const String::size_type queryStart = relativePath.find('?'); |
| const StringView pathPart = |
| String::npos == queryStart ? StringView{relativePath} : StringView{relativePath}.substr(0, queryStart); |
| const String decodedPathPart = decodePathSeparatorsAndDots(pathPart); |
| |
| if (pathPart.empty() || startsWithSeparator(pathPart) || startsWithSeparator(decodedPathPart)) { |
| return false; |
| } |
| |
| const String basePrefix = getDirectoryPrefix(currentPath); |
| const String candidatePath{basePrefix + String{pathPart}}; |
| |
| String normalizedBasePrefix; |
| String normalizedCandidatePath; |
| if (!normalizePath(basePrefix, normalizedBasePrefix) || !normalizePath(candidatePath, normalizedCandidatePath)) { |
| return false; |
| } |
| |
| String validationBasePrefix; |
| String validationCandidatePath; |
| if (!normalizePath(decodePathSeparatorsAndDots(basePrefix), validationBasePrefix) || |
| !normalizePath(decodePathSeparatorsAndDots(candidatePath), validationCandidatePath)) { |
| return false; |
| } |
| |
| if (!isUnderPrefix(normalizedCandidatePath, normalizedBasePrefix) || |
| !isUnderPrefix(validationCandidatePath, validationBasePrefix)) { |
| return false; |
| } |
| |
| fetchPath.path = std::move(normalizedCandidatePath); |
| if (String::npos != queryStart) { |
| fetchPath.hasQuery = true; |
| fetchPath.query = relativePath.substr(queryStart + 1); |
| } |
| return true; |
| } |