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