1 /*
2 This file is part of solidity.
3
4 solidity is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 solidity is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with solidity. If not, see <http://www.gnu.org/licenses/>.
16 */
17 // SPDX-License-Identifier: GPL-3.0
18 #include <libsolidity/interface/FileReader.h>
19
20 #include <liblangutil/Exceptions.h>
21
22 #include <libsolutil/CommonIO.h>
23 #include <libsolutil/Exceptions.h>
24 #include <libsolutil/StringUtils.h>
25
26 #include <boost/algorithm/string/predicate.hpp>
27 #include <range/v3/view/transform.hpp>
28
29 #include <range/v3/range/conversion.hpp>
30
31 #include <functional>
32
33 using solidity::frontend::ReadCallback;
34 using solidity::langutil::InternalCompilerError;
35 using solidity::util::errinfo_comment;
36 using solidity::util::readFileAsString;
37 using solidity::util::joinHumanReadable;
38 using std::map;
39 using std::reference_wrapper;
40 using std::string;
41 using std::vector;
42
43 namespace solidity::frontend
44 {
45
FileReader(boost::filesystem::path _basePath,vector<boost::filesystem::path> const & _includePaths,FileSystemPathSet _allowedDirectories)46 FileReader::FileReader(
47 boost::filesystem::path _basePath,
48 vector<boost::filesystem::path> const& _includePaths,
49 FileSystemPathSet _allowedDirectories
50 ):
51 m_allowedDirectories(std::move(_allowedDirectories)),
52 m_sourceCodes()
53 {
54 setBasePath(_basePath);
55 for (boost::filesystem::path const& includePath: _includePaths)
56 addIncludePath(includePath);
57
58 for (boost::filesystem::path const& allowedDir: m_allowedDirectories)
59 solAssert(!allowedDir.empty(), "");
60 }
61
setBasePath(boost::filesystem::path const & _path)62 void FileReader::setBasePath(boost::filesystem::path const& _path)
63 {
64 if (_path.empty())
65 {
66 // Empty base path is a special case that does not make sense when include paths are used.
67 solAssert(m_includePaths.empty(), "");
68 m_basePath = "";
69 }
70 else
71 m_basePath = normalizeCLIPathForVFS(_path);
72 }
73
addIncludePath(boost::filesystem::path const & _path)74 void FileReader::addIncludePath(boost::filesystem::path const& _path)
75 {
76 solAssert(!m_basePath.empty(), "");
77 solAssert(!_path.empty(), "");
78 m_includePaths.push_back(normalizeCLIPathForVFS(_path));
79 }
80
allowDirectory(boost::filesystem::path _path)81 void FileReader::allowDirectory(boost::filesystem::path _path)
82 {
83 solAssert(!_path.empty(), "");
84 m_allowedDirectories.insert(std::move(_path));
85 }
86
addOrUpdateFile(boost::filesystem::path const & _path,SourceCode _source)87 void FileReader::addOrUpdateFile(boost::filesystem::path const& _path, SourceCode _source)
88 {
89 m_sourceCodes[cliPathToSourceUnitName(_path)] = std::move(_source);
90 }
91
setStdin(SourceCode _source)92 void FileReader::setStdin(SourceCode _source)
93 {
94 m_sourceCodes["<stdin>"] = std::move(_source);
95 }
96
setSourceUnits(StringMap _sources)97 void FileReader::setSourceUnits(StringMap _sources)
98 {
99 m_sourceCodes = std::move(_sources);
100 }
101
readFile(string const & _kind,string const & _sourceUnitName)102 ReadCallback::Result FileReader::readFile(string const& _kind, string const& _sourceUnitName)
103 {
104 try
105 {
106 if (_kind != ReadCallback::kindString(ReadCallback::Kind::ReadFile))
107 solAssert(false, "ReadFile callback used as callback kind " + _kind);
108 string strippedSourceUnitName = _sourceUnitName;
109 if (strippedSourceUnitName.find("file://") == 0)
110 strippedSourceUnitName.erase(0, 7);
111
112 vector<boost::filesystem::path> candidates;
113 vector<reference_wrapper<boost::filesystem::path>> prefixes = {m_basePath};
114 prefixes += (m_includePaths | ranges::to<vector<reference_wrapper<boost::filesystem::path>>>);
115
116 for (auto const& prefix: prefixes)
117 {
118 boost::filesystem::path canonicalPath = normalizeCLIPathForVFS(prefix / strippedSourceUnitName, SymlinkResolution::Enabled);
119
120 if (boost::filesystem::exists(canonicalPath))
121 candidates.push_back(std::move(canonicalPath));
122 }
123
124 auto pathToQuotedString = [](boost::filesystem::path const& _path){ return "\"" + _path.string() + "\""; };
125
126 if (candidates.empty())
127 return ReadCallback::Result{false, "File not found."};
128
129 if (candidates.size() >= 2)
130 return ReadCallback::Result{
131 false,
132 "Ambiguous import. "
133 "Multiple matching files found inside base path and/or include paths: " +
134 joinHumanReadable(candidates | ranges::views::transform(pathToQuotedString), ", ") +
135 "."
136 };
137
138 FileSystemPathSet extraAllowedPaths = {m_basePath.empty() ? "." : m_basePath};
139 extraAllowedPaths += m_includePaths;
140
141 bool isAllowed = false;
142 for (boost::filesystem::path const& allowedDir: m_allowedDirectories + extraAllowedPaths)
143 if (isPathPrefix(normalizeCLIPathForVFS(allowedDir, SymlinkResolution::Enabled), candidates[0]))
144 {
145 isAllowed = true;
146 break;
147 }
148
149 if (!isAllowed)
150 return ReadCallback::Result{false, "File outside of allowed directories."};
151
152 if (!boost::filesystem::is_regular_file(candidates[0]))
153 return ReadCallback::Result{false, "Not a valid file."};
154
155 // NOTE: we ignore the FileNotFound exception as we manually check above
156 auto contents = readFileAsString(candidates[0]);
157 solAssert(m_sourceCodes.count(_sourceUnitName) == 0, "");
158 m_sourceCodes[_sourceUnitName] = contents;
159 return ReadCallback::Result{true, contents};
160 }
161 catch (util::Exception const& _exception)
162 {
163 return ReadCallback::Result{false, "Exception in read callback: " + boost::diagnostic_information(_exception)};
164 }
165 catch (std::exception const& _exception)
166 {
167 return ReadCallback::Result{false, "Exception in read callback: " + boost::diagnostic_information(_exception)};
168 }
169 catch (...)
170 {
171 return ReadCallback::Result{false, "Unknown exception in read callback: " + boost::current_exception_diagnostic_information()};
172 }
173 }
174
cliPathToSourceUnitName(boost::filesystem::path const & _cliPath) const175 string FileReader::cliPathToSourceUnitName(boost::filesystem::path const& _cliPath) const
176 {
177 vector<boost::filesystem::path> prefixes = {m_basePath.empty() ? normalizeCLIPathForVFS(".") : m_basePath};
178 prefixes += m_includePaths;
179
180 boost::filesystem::path normalizedPath = normalizeCLIPathForVFS(_cliPath);
181 for (boost::filesystem::path const& prefix: prefixes)
182 if (isPathPrefix(prefix, normalizedPath))
183 {
184 // Multiple prefixes can potentially match the path. We take the first one.
185 normalizedPath = stripPrefixIfPresent(prefix, normalizedPath);
186 break;
187 }
188
189 return normalizedPath.generic_string();
190 }
191
detectSourceUnitNameCollisions(FileSystemPathSet const & _cliPaths) const192 map<string, FileReader::FileSystemPathSet> FileReader::detectSourceUnitNameCollisions(FileSystemPathSet const& _cliPaths) const
193 {
194 map<string, FileReader::FileSystemPathSet> nameToPaths;
195 for (boost::filesystem::path const& cliPath: _cliPaths)
196 {
197 string sourceUnitName = cliPathToSourceUnitName(cliPath);
198 boost::filesystem::path normalizedPath = normalizeCLIPathForVFS(cliPath);
199 nameToPaths[sourceUnitName].insert(normalizedPath);
200 }
201
202 map<string, FileReader::FileSystemPathSet> collisions;
203 for (auto&& [sourceUnitName, cliPaths]: nameToPaths)
204 if (cliPaths.size() >= 2)
205 collisions[sourceUnitName] = std::move(cliPaths);
206
207 return collisions;
208 }
209
normalizeCLIPathForVFS(boost::filesystem::path const & _path,SymlinkResolution _symlinkResolution)210 boost::filesystem::path FileReader::normalizeCLIPathForVFS(
211 boost::filesystem::path const& _path,
212 SymlinkResolution _symlinkResolution
213 )
214 {
215 // Detailed normalization rules:
216 // - Makes the path either be absolute or have slash as root (note that on Windows paths with
217 // slash as root are not considered absolute by Boost). If it is empty, it becomes
218 // the current working directory.
219 // - Collapses redundant . and .. segments.
220 // - Removes leading .. segments from an absolute path (i.e. /../../ becomes just /).
221 // - Squashes sequences of multiple path separators into one.
222 // - Ensures that forward slashes are used as path separators on all platforms.
223 // - Removes the root name (e.g. drive letter on Windows) when it matches the root name in the
224 // path to the current working directory.
225 //
226 // Also note that this function:
227 // - Does NOT resolve symlinks (except for symlinks in the path to the current working directory)
228 // unless explicitly requested.
229 // - Does NOT check if the path refers to a file or a directory. If the path ends with a slash,
230 // the slash is preserved even if it's a file.
231 // - The only exception are paths where the file name is a dot (e.g. '.' or 'a/b/.'). These
232 // always have a trailing slash after normalization.
233 // - Preserves case. Even if the filesystem is case-insensitive but case-preserving and the
234 // case differs, the actual case from disk is NOT detected.
235
236 boost::filesystem::path canonicalWorkDir = boost::filesystem::weakly_canonical(boost::filesystem::current_path());
237
238 // NOTE: On UNIX systems the path returned from current_path() has symlinks resolved while on
239 // Windows it does not. To get consistent results we resolve them on all platforms.
240 boost::filesystem::path absolutePath = boost::filesystem::absolute(_path, canonicalWorkDir);
241
242 boost::filesystem::path normalizedPath;
243 if (_symlinkResolution == SymlinkResolution::Enabled)
244 {
245 // NOTE: weakly_canonical() will not convert a relative path into an absolute one if no
246 // directory included in the path actually exists.
247 normalizedPath = boost::filesystem::weakly_canonical(absolutePath);
248
249 // The three corner cases in which lexically_normal() includes a trailing slash in the
250 // normalized path but weakly_canonical() does not. Note that the trailing slash is not
251 // ignored when comparing paths with ==.
252 if ((_path == "." || _path == "./" || _path == "../") && !boost::ends_with(normalizedPath.generic_string(), "/"))
253 normalizedPath = normalizedPath.parent_path() / (normalizedPath.filename().string() + "/");
254 }
255 else
256 {
257 solAssert(_symlinkResolution == SymlinkResolution::Disabled, "");
258
259 // NOTE: boost path preserves certain differences that are ignored by its operator ==.
260 // E.g. "a//b" vs "a/b" or "a/b/" vs "a/b/.". lexically_normal() does remove these differences.
261 normalizedPath = absolutePath.lexically_normal();
262 }
263 solAssert(normalizedPath.is_absolute() || normalizedPath.root_path() == "/", "");
264
265 // If the path is on the same drive as the working dir, for portability we prefer not to
266 // include the root name. Do this only for non-UNC paths - my experiments show that on Windows
267 // when the working dir is an UNC path, / does not not actually refer to the root of the UNC path.
268 boost::filesystem::path normalizedRootPath = normalizedPath.root_path();
269 if (!isUNCPath(normalizedPath))
270 {
271 boost::filesystem::path workingDirRootPath = canonicalWorkDir.root_path();
272 if (normalizedRootPath == workingDirRootPath)
273 normalizedRootPath = "/";
274 }
275
276 // lexically_normal() will not squash paths like "/../../" into "/". We have to do it manually.
277 boost::filesystem::path dotDotPrefix = absoluteDotDotPrefix(normalizedPath);
278
279 boost::filesystem::path normalizedPathNoDotDot = normalizedPath;
280 if (dotDotPrefix.empty())
281 normalizedPathNoDotDot = normalizedRootPath / normalizedPath.relative_path();
282 else
283 normalizedPathNoDotDot = normalizedRootPath / normalizedPath.lexically_relative(normalizedPath.root_path() / dotDotPrefix);
284 solAssert(!hasDotDotSegments(normalizedPathNoDotDot), "");
285
286 // NOTE: On Windows lexically_normal() converts all separators to forward slashes. Convert them back.
287 // Separators do not affect path comparison but remain in internal representation returned by native().
288 // This will also normalize the root name to start with // in UNC paths.
289 normalizedPathNoDotDot = normalizedPathNoDotDot.generic_string();
290
291 // For some reason boost considers "/." different than "/" even though for other directories
292 // the trailing dot is ignored.
293 if (normalizedPathNoDotDot == "/.")
294 return "/";
295
296 return normalizedPathNoDotDot;
297 }
298
isPathPrefix(boost::filesystem::path const & _prefix,boost::filesystem::path const & _path)299 bool FileReader::isPathPrefix(boost::filesystem::path const& _prefix, boost::filesystem::path const& _path)
300 {
301 solAssert(!_prefix.empty() && !_path.empty(), "");
302 // NOTE: On Windows paths starting with a slash (rather than a drive letter) are considered relative by boost.
303 solAssert(_prefix.is_absolute() || isUNCPath(_prefix) || _prefix.root_path() == "/", "");
304 solAssert(_path.is_absolute() || isUNCPath(_path) || _path.root_path() == "/", "");
305 solAssert(_prefix == _prefix.lexically_normal() && _path == _path.lexically_normal(), "");
306 solAssert(!hasDotDotSegments(_prefix) && !hasDotDotSegments(_path), "");
307
308 boost::filesystem::path strippedPath = _path.lexically_relative(
309 // Before 1.72.0 lexically_relative() was not handling paths with empty, dot and dot dot segments
310 // correctly (see https://github.com/boostorg/filesystem/issues/76). The only case where this
311 // is possible after our normalization is a directory name ending in a slash (filename is a dot).
312 _prefix.filename_is_dot() ? _prefix.parent_path() : _prefix
313 );
314 return !strippedPath.empty() && *strippedPath.begin() != "..";
315 }
316
stripPrefixIfPresent(boost::filesystem::path const & _prefix,boost::filesystem::path const & _path)317 boost::filesystem::path FileReader::stripPrefixIfPresent(boost::filesystem::path const& _prefix, boost::filesystem::path const& _path)
318 {
319 if (!isPathPrefix(_prefix, _path))
320 return _path;
321
322 boost::filesystem::path strippedPath = _path.lexically_relative(
323 _prefix.filename_is_dot() ? _prefix.parent_path() : _prefix
324 );
325 solAssert(strippedPath.empty() || *strippedPath.begin() != "..", "");
326 return strippedPath;
327 }
328
absoluteDotDotPrefix(boost::filesystem::path const & _path)329 boost::filesystem::path FileReader::absoluteDotDotPrefix(boost::filesystem::path const& _path)
330 {
331 solAssert(_path.is_absolute() || _path.root_path() == "/", "");
332
333 boost::filesystem::path _pathWithoutRoot = _path.relative_path();
334 boost::filesystem::path prefix;
335 for (boost::filesystem::path const& segment: _pathWithoutRoot)
336 if (segment.filename_is_dot_dot())
337 prefix /= segment;
338
339 return prefix;
340 }
341
hasDotDotSegments(boost::filesystem::path const & _path)342 bool FileReader::hasDotDotSegments(boost::filesystem::path const& _path)
343 {
344 for (boost::filesystem::path const& segment: _path)
345 if (segment.filename_is_dot_dot())
346 return true;
347
348 return false;
349 }
350
isUNCPath(boost::filesystem::path const & _path)351 bool FileReader::isUNCPath(boost::filesystem::path const& _path)
352 {
353 string rootName = _path.root_name().string();
354
355 return (
356 rootName.size() == 2 ||
357 (rootName.size() > 2 && rootName[2] != rootName[1])
358 ) && (
359 (rootName[0] == '/' && rootName[1] == '/')
360 #if defined(_WIN32)
361 || (rootName[0] == '\\' && rootName[1] == '\\')
362 #endif
363 );
364 }
365
366 }
367