From 618ba2fb9a371af8f2c8262746ab65092478a570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Sat, 3 Jul 2021 00:02:58 +0200 Subject: [PATCH 1/3] createSymlinkIfSupportedByFilesystem(): Add support for directory symlinks used on Windows --- test/FilesystemUtils.cpp | 13 +++++++++---- test/FilesystemUtils.h | 8 ++++++-- test/libsolutil/CommonIO.cpp | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/test/FilesystemUtils.cpp b/test/FilesystemUtils.cpp index cdad58e62..c016f7fd1 100644 --- a/test/FilesystemUtils.cpp +++ b/test/FilesystemUtils.cpp @@ -39,7 +39,7 @@ void solidity::test::createFilesWithParentDirs(set cons } } -void solidity::test::createFileWithContent(boost::filesystem::path const& _path, string const& content) +void solidity::test::createFileWithContent(boost::filesystem::path const& _path, string const& _content) { if (boost::filesystem::is_regular_file(_path)) BOOST_THROW_EXCEPTION(runtime_error("File already exists: \"" + _path.string() + "\".")); \ @@ -49,16 +49,21 @@ void solidity::test::createFileWithContent(boost::filesystem::path const& _path, if (newFile.fail() || !boost::filesystem::is_regular_file(_path)) BOOST_THROW_EXCEPTION(runtime_error("Failed to create a file: \"" + _path.string() + "\".")); \ - newFile << content; + newFile << _content; } bool solidity::test::createSymlinkIfSupportedByFilesystem( boost::filesystem::path const& _targetPath, - boost::filesystem::path const& _linkName + boost::filesystem::path const& _linkName, + bool _directorySymlink ) { boost::system::error_code symlinkCreationError; - boost::filesystem::create_symlink(_targetPath, _linkName, symlinkCreationError); + + if (_directorySymlink) + boost::filesystem::create_directory_symlink(_targetPath, _linkName, symlinkCreationError); + else + boost::filesystem::create_symlink(_targetPath, _linkName, symlinkCreationError); if (!symlinkCreationError) return true; diff --git a/test/FilesystemUtils.h b/test/FilesystemUtils.h index 291188114..f993ce03d 100644 --- a/test/FilesystemUtils.h +++ b/test/FilesystemUtils.h @@ -35,16 +35,20 @@ void createFilesWithParentDirs(std::set const& _paths, /// Creates a file with the exact content specified in the second argument. /// Throws an exception if the file already exists or if the parent directory of the file does not. -void createFileWithContent(boost::filesystem::path const& _path, std::string const& content); +void createFileWithContent(boost::filesystem::path const& _path, std::string const& _content); /// Creates a symlink between two paths. /// The target does not have to exist. +/// If @p directorySymlink is true, indicate to the operating system that this is a directory +/// symlink. On some systems (e.g. Windows) it's possible to create a non-directory symlink pointing +/// at a directory, which makes such a symlinks unusable. /// @returns true if the symlink has been successfully created, false if the filesystem does not /// support symlinks. /// Throws an exception of the operation fails for a different reason. bool createSymlinkIfSupportedByFilesystem( boost::filesystem::path const& _targetPath, - boost::filesystem::path const& _linkName + boost::filesystem::path const& _linkName, + bool _directorySymlink ); } diff --git a/test/libsolutil/CommonIO.cpp b/test/libsolutil/CommonIO.cpp index 0f3cde036..57fea38de 100644 --- a/test/libsolutil/CommonIO.cpp +++ b/test/libsolutil/CommonIO.cpp @@ -58,7 +58,7 @@ BOOST_AUTO_TEST_CASE(readFileAsString_symlink) TemporaryDirectory tempDir("common-io-test-"); createFileWithContent(tempDir.path() / "test.txt", "ABC\ndef\n"); - if (!createSymlinkIfSupportedByFilesystem("test.txt", tempDir.path() / "symlink.txt")) + if (!createSymlinkIfSupportedByFilesystem("test.txt", tempDir.path() / "symlink.txt", false)) return; BOOST_TEST(readFileAsString(tempDir.path() / "symlink.txt") == "ABC\ndef\n"); From 2d3ec69a05bca51fdcdac7b5e115abf0159c1802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Fri, 11 Jun 2021 16:39:28 +0200 Subject: [PATCH 2/3] CLI tests for FileReader --- test/solc/CommandLineInterface.cpp | 500 +++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) diff --git a/test/solc/CommandLineInterface.cpp b/test/solc/CommandLineInterface.cpp index 9c73c66ea..e9c16af5b 100644 --- a/test/solc/CommandLineInterface.cpp +++ b/test/solc/CommandLineInterface.cpp @@ -341,6 +341,506 @@ BOOST_AUTO_TEST_CASE(standard_json_remapping) BOOST_TEST(result.stderrContent == expectedMessage); } +BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_no_base_path) +{ + TemporaryDirectory tempDirCurrent(TEST_CASE_NAME); + TemporaryDirectory tempDirOther(TEST_CASE_NAME); + TemporaryWorkingDirectory tempWorkDir(tempDirCurrent.path()); + soltestAssert(tempDirCurrent.path().is_absolute(), ""); + soltestAssert(tempDirOther.path().is_absolute(), ""); + + // NOTE: On macOS the path usually contains symlinks which prevents base path from being stripped. + // Use canonical() to resolve symnlinks and get consistent results on all platforms. + boost::filesystem::path currentDirNoSymlinks = boost::filesystem::canonical(tempDirCurrent.path()); + boost::filesystem::path otherDirNoSymlinks = boost::filesystem::canonical(tempDirOther.path()); + + vector commandLine = { + "solc", + "contract1.sol", // Relative path + "c/d/contract2.sol", // Relative path with subdirectories + currentDirNoSymlinks.string() + "/contract3.sol", // Absolute path inside working dir + otherDirNoSymlinks.string() + "/contract4.sol", // Absolute path outside of working dir + }; + + CommandLineOptions expectedOptions; + expectedOptions.input.paths = { + "contract1.sol", + "c/d/contract2.sol", + currentDirNoSymlinks / "contract3.sol", + otherDirNoSymlinks / "contract4.sol", + }; + expectedOptions.modelChecker.initialize = true; + + map expectedSources = { + {"contract1.sol", ""}, + {"c/d/contract2.sol", ""}, + {currentDirNoSymlinks.generic_string() + "/contract3.sol", ""}, + {otherDirNoSymlinks.generic_string() + "/contract4.sol", ""}, + }; + + FileReader::FileSystemPathSet expectedAllowedDirectories = { + currentDirNoSymlinks / "c/d", + currentDirNoSymlinks, + otherDirNoSymlinks, + }; + + createFilesWithParentDirs(expectedOptions.input.paths); + OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine); + + BOOST_TEST(result.stderrContent == ""); + BOOST_TEST(result.stdoutContent == ""); + BOOST_REQUIRE(result.success); + BOOST_TEST(result.options == expectedOptions); + BOOST_TEST(result.reader.sourceCodes() == expectedSources); + BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); + BOOST_TEST(result.reader.basePath() == ""); +} + +BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_same_as_work_dir) +{ + TemporaryDirectory tempDirCurrent(TEST_CASE_NAME); + TemporaryDirectory tempDirOther(TEST_CASE_NAME); + TemporaryWorkingDirectory tempWorkDir(tempDirCurrent.path()); + soltestAssert(tempDirCurrent.path().is_absolute(), ""); + soltestAssert(tempDirOther.path().is_absolute(), ""); + + // NOTE: On macOS the path usually contains symlinks which prevents base path from being stripped. + // Use canonical() to resolve symnlinks and get consistent results on all platforms. + boost::filesystem::path currentDirNoSymlinks = boost::filesystem::canonical(tempDirCurrent.path()); + boost::filesystem::path otherDirNoSymlinks = boost::filesystem::canonical(tempDirOther.path()); + + vector commandLine = { + "solc", + "--base-path=" + currentDirNoSymlinks.string(), + "contract1.sol", // Relative path + "c/d/contract2.sol", // Relative path with subdirectories + currentDirNoSymlinks.string() + "/contract3.sol", // Absolute path inside working dir + otherDirNoSymlinks.string() + "/contract4.sol", // Absolute path outside of working dir + }; + + CommandLineOptions expectedOptions; + expectedOptions.input.paths = { + "contract1.sol", + "c/d/contract2.sol", + currentDirNoSymlinks / "contract3.sol", + otherDirNoSymlinks / "contract4.sol", + }; + expectedOptions.input.basePath = currentDirNoSymlinks; + expectedOptions.modelChecker.initialize = true; + + map expectedSources = { + {"contract1.sol", ""}, + {"c/d/contract2.sol", ""}, + {currentDirNoSymlinks.generic_string() + "/contract3.sol", ""}, + {otherDirNoSymlinks.generic_string() + "/contract4.sol", ""}, + }; + + FileReader::FileSystemPathSet expectedAllowedDirectories = { + currentDirNoSymlinks / "c/d", + currentDirNoSymlinks, + otherDirNoSymlinks, + }; + + createFilesWithParentDirs(expectedOptions.input.paths); + OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine); + + BOOST_TEST(result.stderrContent == ""); + BOOST_TEST(result.stdoutContent == ""); + BOOST_REQUIRE(result.success); + BOOST_TEST(result.options == expectedOptions); + BOOST_TEST(result.reader.sourceCodes() == expectedSources); + BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); + BOOST_TEST(result.reader.basePath() == expectedOptions.input.basePath); +} + +BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_different_from_work_dir) +{ + TemporaryDirectory tempDirCurrent(TEST_CASE_NAME); + TemporaryDirectory tempDirOther(TEST_CASE_NAME); + TemporaryDirectory tempDirBase(TEST_CASE_NAME); + TemporaryWorkingDirectory tempWorkDir(tempDirCurrent.path()); + soltestAssert(tempDirCurrent.path().is_absolute(), ""); + soltestAssert(tempDirOther.path().is_absolute(), ""); + soltestAssert(tempDirBase.path().is_absolute(), ""); + + // NOTE: On macOS the path usually contains symlinks which prevents base path from being stripped. + // Use canonical() to resolve symnlinks and get consistent results on all platforms. + boost::filesystem::path currentDirNoSymlinks = boost::filesystem::canonical(tempDirCurrent.path()); + boost::filesystem::path otherDirNoSymlinks = boost::filesystem::canonical(tempDirOther.path()); + boost::filesystem::path baseDirNoSymlinks = boost::filesystem::canonical(tempDirBase.path()); + + vector commandLine = { + "solc", + "--base-path=" + baseDirNoSymlinks.string(), + "contract1.sol", // Relative path + "c/d/contract2.sol", // Relative path with subdirectories + currentDirNoSymlinks.string() + "/contract3.sol", // Absolute path inside working dir + otherDirNoSymlinks.string() + "/contract4.sol", // Absolute path outside of working dir + baseDirNoSymlinks.string() + "/contract5.sol", // Absolute path inside base path + }; + + CommandLineOptions expectedOptions; + expectedOptions.input.paths = { + "contract1.sol", + "c/d/contract2.sol", + currentDirNoSymlinks / "contract3.sol", + otherDirNoSymlinks / "contract4.sol", + baseDirNoSymlinks / "contract5.sol", + }; + expectedOptions.input.basePath = baseDirNoSymlinks; + expectedOptions.modelChecker.initialize = true; + + map expectedSources = { + {"contract1.sol", ""}, + {"c/d/contract2.sol", ""}, + {currentDirNoSymlinks.generic_string() + "/contract3.sol", ""}, + {otherDirNoSymlinks.generic_string() + "/contract4.sol", ""}, + {baseDirNoSymlinks.generic_string() + "/contract5.sol", ""}, + }; + + FileReader::FileSystemPathSet expectedAllowedDirectories = { + currentDirNoSymlinks / "c/d", + currentDirNoSymlinks, + otherDirNoSymlinks, + baseDirNoSymlinks, + }; + + createFilesWithParentDirs(expectedOptions.input.paths); + OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine); + + BOOST_TEST(result.stderrContent == ""); + BOOST_TEST(result.stdoutContent == ""); + BOOST_REQUIRE(result.success); + BOOST_TEST(result.options == expectedOptions); + BOOST_TEST(result.reader.sourceCodes() == expectedSources); + BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); + BOOST_TEST(result.reader.basePath() == expectedOptions.input.basePath); +} + +BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_relative_base_path) +{ + TemporaryDirectory tempDirCurrent(TEST_CASE_NAME); + TemporaryDirectory tempDirOther(TEST_CASE_NAME); + TemporaryWorkingDirectory tempWorkDir(tempDirCurrent.path()); + soltestAssert(tempDirCurrent.path().is_absolute(), ""); + soltestAssert(tempDirOther.path().is_absolute(), ""); + + // NOTE: On macOS the path usually contains symlinks which prevents base path from being stripped. + // Use canonical() to resolve symnlinks and get consistent results on all platforms. + boost::filesystem::path currentDirNoSymlinks = boost::filesystem::canonical(tempDirCurrent.path()); + boost::filesystem::path otherDirNoSymlinks = boost::filesystem::canonical(tempDirOther.path()); + + vector commandLine = { + "solc", + "--base-path=base", + "contract1.sol", // Relative path outside of base path + "base/contract2.sol", // Relative path inside base path + currentDirNoSymlinks.string() + "/contract3.sol", // Absolute path inside working dir + currentDirNoSymlinks.string() + "/base/contract4.sol", // Absolute path inside base path + otherDirNoSymlinks.string() + "/contract5.sol", // Absolute path outside of working dir + otherDirNoSymlinks.string() + "/base/contract6.sol", // Absolute path outside of working dir + }; + + CommandLineOptions expectedOptions; + expectedOptions.input.paths = { + "contract1.sol", + "base/contract2.sol", + currentDirNoSymlinks / "contract3.sol", + currentDirNoSymlinks / "base/contract4.sol", + otherDirNoSymlinks / "contract5.sol", + otherDirNoSymlinks / "base/contract6.sol", + }; + expectedOptions.input.basePath = "base"; + expectedOptions.modelChecker.initialize = true; + + map expectedSources = { + {"contract1.sol", ""}, + {"base/contract2.sol", ""}, + {currentDirNoSymlinks.generic_string() + "/contract3.sol", ""}, + {currentDirNoSymlinks.generic_string() + "/base/contract4.sol", ""}, + {otherDirNoSymlinks.generic_string() + "/contract5.sol", ""}, + {otherDirNoSymlinks.generic_string() + "/base/contract6.sol", ""}, + }; + + FileReader::FileSystemPathSet expectedAllowedDirectories = { + currentDirNoSymlinks / "base", + currentDirNoSymlinks, + otherDirNoSymlinks, + otherDirNoSymlinks / "base", + }; + + createFilesWithParentDirs(expectedOptions.input.paths); + OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine); + + BOOST_TEST(result.stderrContent == ""); + BOOST_TEST(result.stdoutContent == ""); + BOOST_REQUIRE(result.success); + BOOST_TEST(result.options == expectedOptions); + BOOST_TEST(result.reader.sourceCodes() == expectedSources); + BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); + BOOST_TEST(result.reader.basePath() == expectedOptions.input.basePath); +} + +BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_normalization_and_weird_names) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + boost::filesystem::create_directories(tempDir.path() / "x/y/z"); + TemporaryWorkingDirectory tempWorkDir(tempDir.path() / "x/y/z"); + soltestAssert(tempDir.path().is_absolute(), ""); + + string uncPath = "//" + tempDir.path().relative_path().generic_string(); + soltestAssert(uncPath[0] == '/' && uncPath[1] == '/', ""); + soltestAssert(uncPath[2] != '/', ""); + + boost::filesystem::path tempDirNoSymlinks = boost::filesystem::canonical(tempDir.path()); + + vector commandLine = { + "solc", + +#if !defined(_WIN32) + // URLs. We interpret them as local paths. + // Note that : is not allowed in file names on Windows. + "file://c/d/contract1.sol", + "file:///c/d/contract2.sol", + "https://example.com/contract3.sol", +#endif + + // Redundant slashes + "a/b//contract4.sol", + "a/b///contract5.sol", + "a/b////contract6.sol", + + // Dot segments + "./a/b/contract7.sol", + "././a/b/contract8.sol", + "a/./b/contract9.sol", + "a/././b/contract10.sol", + + // Dot dot segments + "../a/b/contract11.sol", + "../../a/b/contract12.sol", + "a/../b/contract13.sol", + "a/b/../../contract14.sol", + tempDirNoSymlinks.string() + "/x/y/z/a/../b/contract15.sol", + tempDirNoSymlinks.string() + "/x/y/z/a/b/../../contract16.sol", + + // Dot dot segments going beyond filesystem root + "/../" + tempDir.path().relative_path().generic_string() + "/contract17.sol", + "/../../" + tempDir.path().relative_path().generic_string() + "/contract18.sol", + +#if !defined(_WIN32) + // Name conflict with source unit name of stdin. + // Note that < and > are not allowed in file names on Windows. + "", + + // UNC paths on UNIX just resolve into normal paths. On Windows this would be an network + // share (and an error unless the share actually exists so I can't test it here). + uncPath + "/contract19.sol", + + // Windows paths on non-Windows systems. + // Note that on Windows we tested them already just by using absolute paths. + "a\\b\\contract20.sol", + "C:\\a\\b\\contract21.sol", +#endif + }; + + CommandLineOptions expectedOptions; + expectedOptions.input.paths = { +#if !defined(_WIN32) + "file://c/d/contract1.sol", + "file:///c/d/contract2.sol", + "https://example.com/contract3.sol", +#endif + + "a/b//contract4.sol", + "a/b///contract5.sol", + "a/b////contract6.sol", + + "./a/b/contract7.sol", + "././a/b/contract8.sol", + "a/./b/contract9.sol", + "a/././b/contract10.sol", + + "../a/b/contract11.sol", + "../../a/b/contract12.sol", + "a/../b/contract13.sol", + "a/b/../../contract14.sol", + tempDirNoSymlinks.string() + "/x/y/z/a/../b/contract15.sol", + tempDirNoSymlinks.string() + "/x/y/z/a/b/../../contract16.sol", + + "/../" + tempDir.path().relative_path().string() + "/contract17.sol", + "/../../" + tempDir.path().relative_path().string() + "/contract18.sol", + +#if !defined(_WIN32) + "", + + uncPath + "/contract19.sol", + + "a\\b\\contract20.sol", + "C:\\a\\b\\contract21.sol", +#endif + }; + expectedOptions.modelChecker.initialize = true; + + map expectedSources = { +#if !defined(_WIN32) + {"file://c/d/contract1.sol", ""}, + {"file:///c/d/contract2.sol", ""}, + {"https://example.com/contract3.sol", ""}, +#endif + + {"a/b//contract4.sol", ""}, + {"a/b///contract5.sol", ""}, + {"a/b////contract6.sol", ""}, + + {"./a/b/contract7.sol", ""}, + {"././a/b/contract8.sol", ""}, + {"a/./b/contract9.sol", ""}, + {"a/././b/contract10.sol", ""}, + + {"../a/b/contract11.sol", ""}, + {"../../a/b/contract12.sol", ""}, + {"a/../b/contract13.sol", ""}, + {"a/b/../../contract14.sol", ""}, + {tempDirNoSymlinks.string() + "/x/y/z/a/../b/contract15.sol", ""}, + {tempDirNoSymlinks.string() + "/x/y/z/a/b/../../contract16.sol", ""}, + + {"/../" + tempDir.path().relative_path().generic_string() + "/contract17.sol", ""}, + {"/../../" + tempDir.path().relative_path().generic_string() + "/contract18.sol", ""}, + + +#if !defined(_WIN32) + {"", ""}, + + {uncPath + "/contract19.sol", ""}, + + {"a\\b\\contract20.sol", ""}, + {"C:\\a\\b\\contract21.sol", ""}, +#endif + }; + + FileReader::FileSystemPathSet expectedAllowedDirectories = { +#if !defined(_WIN32) + tempDirNoSymlinks / "x/y/z/file:/c/d", + tempDirNoSymlinks / "x/y/z/https:/example.com", +#endif + tempDirNoSymlinks / "x/y/z/a/b", + tempDirNoSymlinks / "x/y/z", + tempDirNoSymlinks / "x/y/z/b", + tempDirNoSymlinks / "x/y/a/b", + tempDirNoSymlinks / "x/a/b", + tempDirNoSymlinks, +#if !defined(_WIN32) + boost::filesystem::canonical(uncPath), +#endif + }; + + createFilesWithParentDirs(expectedOptions.input.paths); + + OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine); + + BOOST_TEST(result.stderrContent == ""); + BOOST_TEST(result.stdoutContent == ""); + BOOST_REQUIRE(result.success); + BOOST_TEST(result.options == expectedOptions); + BOOST_TEST(result.reader.sourceCodes() == expectedSources); + BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); + BOOST_TEST(result.reader.basePath() == expectedOptions.input.basePath); +} + +BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_symlinks) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + createFilesWithParentDirs({tempDir.path() / "x/y/z/contract.sol"}); + boost::filesystem::create_directories(tempDir.path() / "r"); + TemporaryWorkingDirectory tempWorkDir(tempDir.path() / "r"); + + if ( +#if !defined(_WIN32) + !createSymlinkIfSupportedByFilesystem("../x/y", tempDir.path() / "r/sym", true) || +#else + // NOTE: On Windows / works as a separator in a symlink target only if the target is absolute + !createSymlinkIfSupportedByFilesystem("..\\x\\y", tempDir.path() / "r/sym", true) || +#endif + !createSymlinkIfSupportedByFilesystem("contract.sol", tempDir.path() / "x/y/z/contract_symlink.sol", false) + ) + return; + + + vector commandLine = { + "solc", + + "--base-path=../r/sym/z/", + "sym/z/contract.sol", // File accessed directly + same dir symlink as base path + "../x/y/z/contract.sol", // File accessed directly + different dir symlink than base path + "sym/z/contract_symlink.sol", // File accessed via symlink + same dir symlink as base path + "../x/y/z/contract_symlink.sol", // File accessed via symlink + different dir symlink than base path + }; + + CommandLineOptions expectedOptions; + expectedOptions.input.paths = { + "sym/z/contract.sol", + "../x/y/z/contract.sol", + "sym/z/contract_symlink.sol", + "../x/y/z/contract_symlink.sol", + }; + expectedOptions.input.basePath = "../r/sym/z/"; + expectedOptions.modelChecker.initialize = true; + + map expectedSources = { + {"sym/z/contract.sol", ""}, + {"../x/y/z/contract.sol", ""}, + {"sym/z/contract_symlink.sol", ""}, + {"../x/y/z/contract_symlink.sol", ""}, + }; + + FileReader::FileSystemPathSet expectedAllowedDirectories = { + boost::filesystem::canonical(tempDir.path()) / "x/y/z", + }; + + OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine); + + BOOST_TEST(result.stderrContent == ""); + BOOST_TEST(result.stdoutContent == ""); + BOOST_REQUIRE(result.success); + BOOST_TEST(result.options == expectedOptions); + BOOST_TEST(result.reader.sourceCodes() == expectedSources); + BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); + BOOST_TEST(result.reader.basePath() == expectedOptions.input.basePath); +} + +BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_and_stdin) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + TemporaryWorkingDirectory tempWorkDir(tempDir.path()); + boost::filesystem::create_directories(tempDir.path() / "base"); + + boost::filesystem::path expectedWorkDir = "/" / boost::filesystem::current_path().relative_path(); + + vector commandLine = {"solc", "--base-path=base", "-"}; + + CommandLineOptions expectedOptions; + expectedOptions.input.addStdin = true; + expectedOptions.input.basePath = "base"; + expectedOptions.modelChecker.initialize = true; + + map expectedSources = { + {"", "\n"}, + }; + FileReader::FileSystemPathSet expectedAllowedDirectories = {}; + + createFilesWithParentDirs(expectedOptions.input.paths); + OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine); + + BOOST_TEST(result.stderrContent == ""); + BOOST_TEST(result.stdoutContent == ""); + BOOST_REQUIRE(result.success); + BOOST_TEST(result.options == expectedOptions); + BOOST_TEST(result.reader.sourceCodes() == expectedSources); + BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); + BOOST_TEST(result.reader.basePath() == "base"); +} + BOOST_AUTO_TEST_SUITE_END() } // namespace solidity::frontend::test From 13f46ebb1e8e0075b8167604902156da0ce46acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Tue, 15 Jun 2021 14:52:53 +0200 Subject: [PATCH 3/3] FileReader: Normalize base path and strip it from normalized source paths --- Changelog.md | 1 + docs/path-resolution.rst | 70 +++- libsolidity/interface/FileReader.cpp | 150 +++++++- libsolidity/interface/FileReader.h | 53 ++- solc/CommandLineInterface.cpp | 2 +- test/CMakeLists.txt | 1 + test/libsolidity/interface/FileReader.cpp | 450 ++++++++++++++++++++++ test/solc/CommandLineInterface.cpp | 132 ++++--- 8 files changed, 797 insertions(+), 62 deletions(-) create mode 100644 test/libsolidity/interface/FileReader.cpp diff --git a/Changelog.md b/Changelog.md index 2b6d4cc4e..b569023c5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,7 @@ Language Features: Compiler Features: + * Commandline Interface: Normalize paths specified on the command line and make them relative for files located inside base path. * Immutable variables can be read at construction time once they are initialized. * SMTChecker: Support low level ``call`` as external calls to unknown code. * SMTChecker: Add constraints to better correlate ``address(this).balance`` and ``msg.value``. diff --git a/docs/path-resolution.rst b/docs/path-resolution.rst index 1d4b200de..470bfbf31 100644 --- a/docs/path-resolution.rst +++ b/docs/path-resolution.rst @@ -71,8 +71,10 @@ The initial content of the VFS depends on how you invoke the compiler: solc contract.sol /usr/local/dapp-bin/token.sol - The source unit name of a file loaded this way is simply the specified path after shell expansion - and with platform-specific separators converted to forward slashes. + The source unit name of a file loaded this way is constructed by converting its path to a + canonical form and making it relative to the base path if it is located inside. + See :ref:`Base Path Normalization and Stripping ` for + a detailed description of this process. .. index:: standard JSON @@ -309,9 +311,67 @@ When the source unit name is a relative path, this results in the file being loo directory the compiler has been invoked from. It is also the only value that results in absolute paths in source unit names being actually interpreted as absolute paths on disk. +If the base path itself is relative, it is interpreted as relative to the current working directory +of the compiler. -If the base path itself is relative, it is also interpreted as relative to the current working -directory of the compiler. +.. _base-path-normalization-and-stripping: + +Base Path Normalization and Stripping +------------------------------------- + +On the command line the compiler behaves just as you would expect from any other program: +it accepts paths in a format native to the platform and relative paths are relative to the current +working directory. +The source unit names assigned to files whose paths are specified on the command line, however, +should not change just because the project is being compiled on a different platform or because the +compiler happens to have been invoked from a different directory. +To achieve this, paths to source files coming from the command line must be converted to a canonical +form, and, if possible, made relative to the base path. + +The normalization rules are as follows: + +- If a path is relative, it is made absolute by prepending the current working directory to it. +- Internal ``.`` and ``..`` segments are collapsed. +- Platform-specific path separators are replaced with forward slashes. +- Sequences of multiple consecutive path separators are squashed into a single separator (unless + they are the leading slashes of an `UNC path `_). +- If the path includes a root name (e.g. a drive letter on Windows) and the root is the same as the + root of the current working directory, the root is replaced with ``/``. +- Symbolic links in the path are **not** resolved. + + - The only exception is the path to the current working directory prepended to relative paths in + the process of making them absolute. + On some platforms the working directory is reported always with symbolic links resolved so for + consistency the compiler resolves them everywhere. + +- The original case of the path is preserved even if the filesystem is case-insensitive but + `case-preserving `_ and the actual case on + disk is different. + +.. note:: + + There are situations where paths cannot be made platform-independent. + For example on Windows the compiler can avoid using drive letters by referring to the root + directory of the current drive as ``/`` but drive letters are still necessary for paths leading + to other drives. + You can avoid such situations by ensuring that all the files are available within a single + directory tree on the same drive. + +Once canonicalized, the base path is stripped from all source file paths that start with it. +If the base path is empty or not specified, it is treated as if it was equal to the path to the +current working directory (with all symbolic links resolved). +The result is accepted only if the normalized directory path is the exact prefix of the normalized +file path. +Otherwise the file path remains absolute. +This makes the conversion unambiguous and ensures that the relative path does not start with ``../``. +The resulting file path becomes the source unit name. + +.. note:: + + Prior to version 0.8.8, CLI path stripping was not performed and the only normalization applied + was the conversion of path separators. + When working with older versions of the compiler it is recommended to invoke the compiler from + the base path and to only use relative paths on the command line. .. index:: ! remapping; import, ! import; remapping, ! remapping; context, ! remapping; prefix, ! remapping; target .. _import-remapping: @@ -414,7 +474,7 @@ Here are the detailed rules governing the behaviour of remappings: .. code-block:: bash - solc /project/=/contracts/ /project/contract.sol --base-path /project # source unit name: /project/contract.sol + solc /project/=/contracts/ /project/contract.sol --base-path /project # source unit name: contract.sol .. code-block:: solidity :caption: /project/contract.sol diff --git a/libsolidity/interface/FileReader.cpp b/libsolidity/interface/FileReader.cpp index 1ac1e531e..f00102e9e 100644 --- a/libsolidity/interface/FileReader.cpp +++ b/libsolidity/interface/FileReader.cpp @@ -22,6 +22,8 @@ #include #include +#include + using solidity::frontend::ReadCallback; using solidity::langutil::InternalCompilerError; using solidity::util::errinfo_comment; @@ -31,9 +33,22 @@ using std::string; namespace solidity::frontend { +void FileReader::setBasePath(boost::filesystem::path const& _path) +{ + m_basePath = (_path.empty() ? "" : normalizeCLIPathForVFS(_path)); +} + void FileReader::setSource(boost::filesystem::path const& _path, SourceCode _source) { - m_sourceCodes[_path.generic_string()] = std::move(_source); + boost::filesystem::path normalizedPath = normalizeCLIPathForVFS(_path); + boost::filesystem::path prefix = (m_basePath.empty() ? normalizeCLIPathForVFS(".") : m_basePath); + + m_sourceCodes[stripPrefixIfPresent(prefix, normalizedPath).generic_string()] = std::move(_source); +} + +void FileReader::setStdin(SourceCode _source) +{ + m_sourceCodes[""] = std::move(_source); } void FileReader::setSources(StringMap _sources) @@ -92,5 +107,138 @@ ReadCallback::Result FileReader::readFile(string const& _kind, string const& _so } } +boost::filesystem::path FileReader::normalizeCLIPathForVFS(boost::filesystem::path const& _path) +{ + // Detailed normalization rules: + // - Makes the path either be absolute or have slash as root (note that on Windows paths with + // slash as root are not considered absolute by Boost). If it is empty, it becomes + // the current working directory. + // - Collapses redundant . and .. segments. + // - Removes leading .. segments from an absolute path (i.e. /../../ becomes just /). + // - Squashes sequences of multiple path separators into one. + // - Ensures that forward slashes are used as path separators on all platforms. + // - Removes the root name (e.g. drive letter on Windows) when it matches the root name in the + // path to the current working directory. + // + // Also note that this function: + // - Does NOT resolve symlinks (except for symlinks in the path to the current working directory). + // - Does NOT check if the path refers to a file or a directory. If the path ends with a slash, + // the slash is preserved even if it's a file. + // - The only exception are paths where the file name is a dot (e.g. '.' or 'a/b/.'). These + // always have a trailing slash after normalization. + // - Preserves case. Even if the filesystem is case-insensitive but case-preserving and the + // case differs, the actual case from disk is NOT detected. + + boost::filesystem::path canonicalWorkDir = boost::filesystem::weakly_canonical(boost::filesystem::current_path()); + + // NOTE: On UNIX systems the path returned from current_path() has symlinks resolved while on + // Windows it does not. To get consistent results we resolve them on all platforms. + boost::filesystem::path absolutePath = boost::filesystem::absolute(_path, canonicalWorkDir); + + // NOTE: boost path preserves certain differences that are ignored by its operator ==. + // E.g. "a//b" vs "a/b" or "a/b/" vs "a/b/.". lexically_normal() does remove these differences. + boost::filesystem::path normalizedPath = absolutePath.lexically_normal(); + solAssert(normalizedPath.is_absolute() || normalizedPath.root_path() == "/", ""); + + // If the path is on the same drive as the working dir, for portability we prefer not to + // include the root name. Do this only for non-UNC paths - my experiments show that on Windows + // when the working dir is an UNC path, / does not not actually refer to the root of the UNC path. + boost::filesystem::path normalizedRootPath = normalizedPath.root_path(); + if (!isUNCPath(normalizedPath)) + { + boost::filesystem::path workingDirRootPath = canonicalWorkDir.root_path(); + if (normalizedRootPath == workingDirRootPath) + normalizedRootPath = "/"; + } + + // lexically_normal() will not squash paths like "/../../" into "/". We have to do it manually. + boost::filesystem::path dotDotPrefix = absoluteDotDotPrefix(normalizedPath); + + boost::filesystem::path normalizedPathNoDotDot = normalizedPath; + if (dotDotPrefix.empty()) + normalizedPathNoDotDot = normalizedRootPath / normalizedPath.relative_path(); + else + normalizedPathNoDotDot = normalizedRootPath / normalizedPath.lexically_relative(normalizedPath.root_path() / dotDotPrefix); + solAssert(!hasDotDotSegments(normalizedPathNoDotDot), ""); + + // NOTE: On Windows lexically_normal() converts all separators to forward slashes. Convert them back. + // Separators do not affect path comparison but remain in internal representation returned by native(). + // This will also normalize the root name to start with // in UNC paths. + normalizedPathNoDotDot = normalizedPathNoDotDot.generic_string(); + + // For some reason boost considers "/." different than "/" even though for other directories + // the trailing dot is ignored. + if (normalizedPathNoDotDot == "/.") + return "/"; + + return normalizedPathNoDotDot; } +bool FileReader::isPathPrefix(boost::filesystem::path const& _prefix, boost::filesystem::path const& _path) +{ + solAssert(!_prefix.empty() && !_path.empty(), ""); + // NOTE: On Windows paths starting with a slash (rather than a drive letter) are considered relative by boost. + solAssert(_prefix.is_absolute() || isUNCPath(_prefix) || _prefix.root_path() == "/", ""); + solAssert(_path.is_absolute() || isUNCPath(_path) || _path.root_path() == "/", ""); + solAssert(_prefix == _prefix.lexically_normal() && _path == _path.lexically_normal(), ""); + solAssert(!hasDotDotSegments(_prefix) && !hasDotDotSegments(_path), ""); + + boost::filesystem::path strippedPath = _path.lexically_relative( + // Before 1.72.0 lexically_relative() was not handling paths with empty, dot and dot dot segments + // correctly (see https://github.com/boostorg/filesystem/issues/76). The only case where this + // is possible after our normalization is a directory name ending in a slash (filename is a dot). + _prefix.filename_is_dot() ? _prefix.parent_path() : _prefix + ); + return !strippedPath.empty() && *strippedPath.begin() != ".."; +} + +boost::filesystem::path FileReader::stripPrefixIfPresent(boost::filesystem::path const& _prefix, boost::filesystem::path const& _path) +{ + if (!isPathPrefix(_prefix, _path)) + return _path; + + boost::filesystem::path strippedPath = _path.lexically_relative( + _prefix.filename_is_dot() ? _prefix.parent_path() : _prefix + ); + solAssert(strippedPath.empty() || *strippedPath.begin() != "..", ""); + return strippedPath; +} + +boost::filesystem::path FileReader::absoluteDotDotPrefix(boost::filesystem::path const& _path) +{ + solAssert(_path.is_absolute() || _path.root_path() == "/", ""); + + boost::filesystem::path _pathWithoutRoot = _path.relative_path(); + boost::filesystem::path prefix; + for (boost::filesystem::path const& segment: _pathWithoutRoot) + if (segment.filename_is_dot_dot()) + prefix /= segment; + + return prefix; +} + +bool FileReader::hasDotDotSegments(boost::filesystem::path const& _path) +{ + for (boost::filesystem::path const& segment: _path) + if (segment.filename_is_dot_dot()) + return true; + + return false; +} + +bool FileReader::isUNCPath(boost::filesystem::path const& _path) +{ + string rootName = _path.root_name().string(); + + return ( + rootName.size() == 2 || + (rootName.size() > 2 && rootName[2] != rootName[1]) + ) && ( + (rootName[0] == '/' && rootName[1] == '/') +#if defined(_WIN32) + || (rootName[0] == '\\' && rootName[1] == '\\') +#endif + ); +} + +} diff --git a/libsolidity/interface/FileReader.h b/libsolidity/interface/FileReader.h index 5aca710db..bbe24d264 100644 --- a/libsolidity/interface/FileReader.h +++ b/libsolidity/interface/FileReader.h @@ -45,12 +45,13 @@ public: boost::filesystem::path _basePath = {}, FileSystemPathSet _allowedDirectories = {} ): - m_basePath(std::move(_basePath)), m_allowedDirectories(std::move(_allowedDirectories)), m_sourceCodes() - {} + { + setBasePath(_basePath); + } - void setBasePath(boost::filesystem::path _path) { m_basePath = std::move(_path); } + void setBasePath(boost::filesystem::path const& _path); boost::filesystem::path const& basePath() const noexcept { return m_basePath; } void allowDirectory(boost::filesystem::path _path) { m_allowedDirectories.insert(std::move(_path)); } @@ -58,17 +59,21 @@ public: StringMap const& sourceCodes() const noexcept { return m_sourceCodes; } - /// Retrieves the source code for a given source unit ID. + /// Retrieves the source code for a given source unit name. SourceCode const& sourceCode(SourceUnitName const& _sourceUnitName) const { return m_sourceCodes.at(_sourceUnitName); } - /// Resets all sources to the given map of source unit ID to source codes. + /// Resets all sources to the given map of source unit name to source codes. /// Does not enforce @a allowedDirectories(). void setSources(StringMap _sources); - /// Adds the source code for a given source unit ID. + /// Adds the source code under a source unit name created by normalizing the file path. /// Does not enforce @a allowedDirectories(). void setSource(boost::filesystem::path const& _path, SourceCode _source); + /// Adds the source code under the source unit name of @a . + /// Does not enforce @a allowedDirectories(). + void setStdin(SourceCode _source); + /// Receives a @p _sourceUnitName that refers to a source unit in compiler's virtual filesystem /// and attempts to interpret it as a path and read the corresponding file from disk. /// The read will only succeed if the canonical path of the file is within one of the @a allowedDirectories(). @@ -83,7 +88,43 @@ public: return [this](std::string const& _kind, std::string const& _path) { return readFile(_kind, _path); }; } + /// Normalizes a filesystem path to make it include all components up to the filesystem root, + /// remove small, inconsequential differences that do not affect the meaning and make it look + /// the same on all platforms (if possible). Symlinks in the path are not resolved. + /// The resulting path uses forward slashes as path separators, has no redundant separators, + /// has no redundant . or .. segments and has no root name if removing it does not change the meaning. + /// The path does not have to actually exist. + static boost::filesystem::path normalizeCLIPathForVFS(boost::filesystem::path const& _path); + + /// @returns true if all the path components of @a _prefix are present at the beginning of @a _path. + /// Both paths must be absolute (or have slash as root) and normalized (no . or .. segments, no + /// multiple consecutive slashes). + /// Paths are treated as case-sensitive. Does not require the path to actually exist in the + /// filesystem and does not follow symlinks. Only considers whole segments, e.g. /abc/d is not + /// considered a prefix of /abc/def. Both paths must be non-empty. + /// Ignores the trailing slash, i.e. /a/b/c.sol/ is treated as a valid prefix of /a/b/c.sol. + static bool isPathPrefix(boost::filesystem::path const& _prefix, boost::filesystem::path const& _path); + + /// If @a _prefix is actually a prefix of @p _path, removes it from @a _path to make it relative. + /// @returns The path without the prefix or unchanged path if there is not prefix. + /// If @a _path and @_prefix are identical, the result is '.'. + static boost::filesystem::path stripPrefixIfPresent(boost::filesystem::path const& _prefix, boost::filesystem::path const& _path); + + /// @returns true if the specified path is an UNC path. + /// UNC paths start with // followed by a name (on Windows they can also start with \\). + /// They are used for network shares on Windows. On UNIX systems they do not have the same + /// functionality but usually they are still recognized and treated in a special way. + static bool isUNCPath(boost::filesystem::path const& _path); + private: + /// If @a _path starts with a number of .. segments, returns a path consisting only of those + /// segments (root name is not included). Otherwise returns an empty path. @a _path must be + /// absolute (or have slash as root). + static boost::filesystem::path absoluteDotDotPrefix(boost::filesystem::path const& _path); + + /// @returns true if the path contains any .. segments. + static bool hasDotDotSegments(boost::filesystem::path const& _path); + /// Base path, used for resolving relative paths in imports. boost::filesystem::path m_basePath; diff --git a/solc/CommandLineInterface.cpp b/solc/CommandLineInterface.cpp index 092ebb2b0..549991d61 100644 --- a/solc/CommandLineInterface.cpp +++ b/solc/CommandLineInterface.cpp @@ -451,7 +451,7 @@ bool CommandLineInterface::readInputFiles() m_standardJsonInput = readUntilEnd(m_sin); } else - m_fileReader.setSource(g_stdinFileName, readUntilEnd(m_sin)); + m_fileReader.setStdin(readUntilEnd(m_sin)); } if (m_fileReader.sourceCodes().empty() && !m_standardJsonInput.has_value()) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bb15c256a..fa55d76a2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -103,6 +103,7 @@ set(libsolidity_sources libsolidity/SyntaxTest.h libsolidity/ViewPureChecker.cpp libsolidity/analysis/FunctionCallGraph.cpp + libsolidity/interface/FileReader.cpp ) detect_stray_source_files("${libsolidity_sources}" "libsolidity/") diff --git a/test/libsolidity/interface/FileReader.cpp b/test/libsolidity/interface/FileReader.cpp new file mode 100644 index 000000000..be0cd32f9 --- /dev/null +++ b/test/libsolidity/interface/FileReader.cpp @@ -0,0 +1,450 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +/// Unit tests for libsolidity/interface/FileReader.h + +#include + +#include +#include +#include +#include + +#include +#include + +using namespace std; +using namespace solidity::test; + +#define TEST_CASE_NAME (boost::unit_test::framework::current_test_case().p_name) + +namespace solidity::frontend::test +{ + +BOOST_AUTO_TEST_SUITE(FileReaderTest) + +BOOST_AUTO_TEST_CASE(normalizeCLIPathForVFS_absolute_path) +{ + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/"), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/./"), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/./."), "/"); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a"), "/a"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/"), "/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/."), "/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/./a"), "/a"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/./a/"), "/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/./a/."), "/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/b"), "/a/b"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/b/"), "/a/b/"); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/./b/"), "/a/b/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/../a/b/"), "/a/b/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/b/c/.."), "/a/b"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/b/c/../"), "/a/b/"); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/b/c/../../.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/b/c/../../../"), "/"); +} + +BOOST_AUTO_TEST_CASE(normalizeCLIPathForVFS_relative_path) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + boost::filesystem::create_directories(tempDir.path() / "x/y/z"); + TemporaryWorkingDirectory tempWorkDir(tempDir.path() / "x/y/z"); + + // NOTE: If path to work dir contains symlinks (often the case on macOS), boost might resolve + // them, making the path different from tempDirPath. + boost::filesystem::path expectedPrefix = boost::filesystem::current_path().parent_path().parent_path().parent_path(); + // On Windows tempDir.path() normally contains the drive letter while the normalized path should not. + expectedPrefix = "/" / expectedPrefix.relative_path(); + soltestAssert(expectedPrefix.is_absolute() || expectedPrefix.root_path() == "/", ""); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("."), expectedPrefix / "x/y/z/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("./"), expectedPrefix / "x/y/z/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS(".//"), expectedPrefix / "x/y/z/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS(".."), expectedPrefix / "x/y"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../"), expectedPrefix / "x/y/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("..//"), expectedPrefix / "x/y/"); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a"), expectedPrefix / "x/y/z/a"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/"), expectedPrefix / "x/y/z/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/."), expectedPrefix / "x/y/z/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("./a"), expectedPrefix / "x/y/z/a"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("./a/"), expectedPrefix / "x/y/z/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("./a/."), expectedPrefix / "x/y/z/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("./a/./"), expectedPrefix / "x/y/z/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("./a/.//"), expectedPrefix / "x/y/z/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("./a/./."), expectedPrefix / "x/y/z/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("./a/././"), expectedPrefix / "x/y/z/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("./a/././/"), expectedPrefix / "x/y/z/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/b"), expectedPrefix / "x/y/z/a/b"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/b/"), expectedPrefix / "x/y/z/a/b/"); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../a/b"), expectedPrefix / "x/y/a/b"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../../a/b"), expectedPrefix / "x/a/b"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("./a/b"), expectedPrefix / "x/y/z/a/b"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("././a/b"), expectedPrefix / "x/y/z/a/b"); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/./b/"), expectedPrefix / "x/y/z/a/b/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/../a/b/"), expectedPrefix / "x/y/z/a/b/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/b/c/.."), expectedPrefix / "x/y/z/a/b"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/b/c/../"), expectedPrefix / "x/y/z/a/b/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/b/c/..//"), expectedPrefix / "x/y/z/a/b/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/b/c/../.."), expectedPrefix / "x/y/z/a"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/b/c/../../"), expectedPrefix / "x/y/z/a/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/b/c/../..//"), expectedPrefix / "x/y/z/a/"); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../../a/.././../p/../q/../a/b"), expectedPrefix / "a/b"); +} + +BOOST_AUTO_TEST_CASE(normalizeCLIPathForVFS_redundant_slashes) +{ + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("///"), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("////"), "/"); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("////a/b/"), "/a/b/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a//b/"), "/a/b/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a////b/"), "/a/b/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/b//"), "/a/b/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/b////"), "/a/b/"); +} + +BOOST_AUTO_TEST_CASE(normalizeCLIPathForVFS_unc_path) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + TemporaryWorkingDirectory tempWorkDir(tempDir.path()); + + // On Windows tempDir.path() normally contains the drive letter while the normalized path should not. + boost::filesystem::path expectedWorkDir = "/" / boost::filesystem::current_path().relative_path(); + soltestAssert(expectedWorkDir.is_absolute() || expectedWorkDir.root_path() == "/", ""); + + // UNC paths start with // or \\ followed by a name. They are used for network shares on Windows. + // On UNIX systems they are not supported but still treated in a special way. + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("//host/"), "//host/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("//host/a/b"), "//host/a/b"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("//host/a/b/"), "//host/a/b/"); + +#if defined(_WIN32) + // On Windows an UNC path can also start with \\ instead of // + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("\\\\host/"), "//host/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("\\\\host/a/b"), "//host/a/b"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("\\\\host/a/b/"), "//host/a/b/"); +#else + // On UNIX systems it's just a fancy relative path instead + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("\\\\host/"), expectedWorkDir / "\\\\host/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("\\\\host/a/b"), expectedWorkDir / "\\\\host/a/b"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("\\\\host/a/b/"), expectedWorkDir / "\\\\host/a/b/"); +#endif +} + +BOOST_AUTO_TEST_CASE(normalizeCLIPathForVFS_root_name_only) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + TemporaryWorkingDirectory tempWorkDir(tempDir.path()); + + boost::filesystem::path expectedWorkDir = "/" / boost::filesystem::current_path().relative_path(); + soltestAssert(expectedWorkDir.is_absolute() || expectedWorkDir.root_path() == "/", ""); + + // A root **path** consists of a directory name (typically / or \) and the root name (drive + // letter (C:), UNC host name (//host), etc.). Either can be empty. Root path as a whole may be + // an absolute path but root name on its own is considered relative. For example on Windows + // C:\ represents the root directory of drive C: but C: on its own refers to the current working + // directory. + + // UNC paths + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("//"), "//" / expectedWorkDir); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("//host"), "//host" / expectedWorkDir); + + // On UNIX systems root name is empty. + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS(""), expectedWorkDir); + +#if defined(_WIN32) + boost::filesystem::path driveLetter = boost::filesystem::current_path().root_name(); + solAssert(!driveLetter.empty(), ""); + solAssert(driveLetter.is_relative(), ""); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS(driveLetter), expectedWorkDir); +#endif +} + +BOOST_AUTO_TEST_CASE(normalizeCLIPathForVFS_stripping_root_name) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + TemporaryWorkingDirectory tempWorkDir(tempDir.path()); + + soltestAssert(boost::filesystem::current_path().is_absolute(), ""); +#if defined(_WIN32) + soltestAssert(!boost::filesystem::current_path().root_name().empty(), ""); +#endif + + boost::filesystem::path normalizedPath = FileReader::normalizeCLIPathForVFS(boost::filesystem::current_path()); + BOOST_CHECK_EQUAL(normalizedPath, "/" / boost::filesystem::current_path().relative_path()); + BOOST_TEST(normalizedPath.root_name().empty()); + BOOST_CHECK_EQUAL(normalizedPath.root_directory(), "/"); +} + +BOOST_AUTO_TEST_CASE(normalizeCLIPathForVFS_path_beyond_root) +{ + TemporaryWorkingDirectory tempWorkDir("/"); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/../"), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/../."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/../.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/../a"), "/a"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/../a/.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/../a/../.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/../../a"), "/a"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/../../a/.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/../../a/../.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/../.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("/a/../../b/../.."), "/"); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS(".."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../"), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../a"), "/a"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../a/.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../a/../.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../../a"), "/a"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../../a/.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("../../a/../.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/../.."), "/"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("a/../../b/../.."), "/"); +} + +BOOST_AUTO_TEST_CASE(normalizeCLIPathForVFS_case_sensitivity) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + TemporaryWorkingDirectory tempWorkDir(tempDir.path()); + + boost::filesystem::path expectedPrefix = "/" / tempDir.path().relative_path(); + soltestAssert(expectedPrefix.is_absolute() || expectedPrefix.root_path() == "/", ""); + + BOOST_TEST(FileReader::normalizeCLIPathForVFS(tempDir.path() / "abc") == expectedPrefix / "abc"); + BOOST_TEST(FileReader::normalizeCLIPathForVFS(tempDir.path() / "abc") != expectedPrefix / "ABC"); + BOOST_TEST(FileReader::normalizeCLIPathForVFS(tempDir.path() / "ABC") != expectedPrefix / "abc"); + BOOST_TEST(FileReader::normalizeCLIPathForVFS(tempDir.path() / "ABC") == expectedPrefix / "ABC"); +} + +BOOST_AUTO_TEST_CASE(normalizeCLIPathForVFS_path_separators) +{ + // Even on Windows we want / as a separator. + BOOST_TEST((FileReader::normalizeCLIPathForVFS("/a/b/c").native() == boost::filesystem::path("/a/b/c").native())); +} + +BOOST_AUTO_TEST_CASE(normalizeCLIPathForVFS_should_not_resolve_symlinks) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + soltestAssert(tempDir.path().is_absolute(), ""); + boost::filesystem::create_directories(tempDir.path() / "abc"); + + if (!createSymlinkIfSupportedByFilesystem(tempDir.path() / "abc", tempDir.path() / "sym", true)) + return; + + boost::filesystem::path expectedPrefix = "/" / tempDir.path().relative_path(); + soltestAssert(expectedPrefix.is_absolute() || expectedPrefix.root_path() == "/", ""); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS(tempDir.path() / "sym/contract.sol"), expectedPrefix / "sym/contract.sol"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS(tempDir.path() / "abc/contract.sol"), expectedPrefix / "abc/contract.sol"); +} + +BOOST_AUTO_TEST_CASE(normalizeCLIPathForVFS_should_resolve_symlinks_in_workdir_when_path_is_relative) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + soltestAssert(tempDir.path().is_absolute(), ""); + boost::filesystem::create_directories(tempDir.path() / "abc"); + + if (!createSymlinkIfSupportedByFilesystem(tempDir.path() / "abc", tempDir.path() / "sym", true)) + return; + + TemporaryWorkingDirectory tempWorkDir(tempDir.path() / "sym"); + boost::filesystem::path expectedWorkDir = "/" / boost::filesystem::weakly_canonical(boost::filesystem::current_path()).relative_path(); + soltestAssert(expectedWorkDir.is_absolute() || expectedWorkDir.root_path() == "/", ""); + + boost::filesystem::path expectedPrefix = "/" / tempDir.path().relative_path(); + soltestAssert(expectedPrefix.is_absolute() || expectedPrefix.root_path() == "/", ""); + + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS("contract.sol"), expectedWorkDir / "contract.sol"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS(tempDir.path() / "sym/contract.sol"), expectedPrefix / "sym/contract.sol"); + BOOST_CHECK_EQUAL(FileReader::normalizeCLIPathForVFS(tempDir.path() / "abc/contract.sol"), expectedPrefix / "abc/contract.sol"); +} + +BOOST_AUTO_TEST_CASE(isPathPrefix_file_prefix) +{ + BOOST_TEST(FileReader::isPathPrefix("/", "/contract.sol")); + BOOST_TEST(FileReader::isPathPrefix("/contract.sol", "/contract.sol")); + BOOST_TEST(FileReader::isPathPrefix("/contract.sol/", "/contract.sol")); + BOOST_TEST(FileReader::isPathPrefix("/contract.sol/.", "/contract.sol")); + + BOOST_TEST(FileReader::isPathPrefix("/", "/a/bc/def/contract.sol")); + BOOST_TEST(FileReader::isPathPrefix("/a", "/a/bc/def/contract.sol")); + BOOST_TEST(FileReader::isPathPrefix("/a/", "/a/bc/def/contract.sol")); + BOOST_TEST(FileReader::isPathPrefix("/a/bc", "/a/bc/def/contract.sol")); + BOOST_TEST(FileReader::isPathPrefix("/a/bc/def/contract.sol", "/a/bc/def/contract.sol")); + + BOOST_TEST(FileReader::isPathPrefix("/", "/a/bc/def/contract.sol")); + BOOST_TEST(FileReader::isPathPrefix("/a", "/a/bc/def/contract.sol")); + BOOST_TEST(FileReader::isPathPrefix("/a/", "/a/bc/def/contract.sol")); + BOOST_TEST(FileReader::isPathPrefix("/a/bc", "/a/bc/def/contract.sol")); + BOOST_TEST(FileReader::isPathPrefix("/a/bc/def/contract.sol", "/a/bc/def/contract.sol")); + + BOOST_TEST(!FileReader::isPathPrefix("/contract.sol", "/token.sol")); + BOOST_TEST(!FileReader::isPathPrefix("/contract", "/contract.sol")); + BOOST_TEST(!FileReader::isPathPrefix("/contract.sol", "/contract")); + BOOST_TEST(!FileReader::isPathPrefix("/contract.so", "/contract.sol")); + BOOST_TEST(!FileReader::isPathPrefix("/contract.sol", "/contract.so")); + + BOOST_TEST(!FileReader::isPathPrefix("/a/b/c/contract.sol", "/a/b/contract.sol")); + BOOST_TEST(!FileReader::isPathPrefix("/a/b/contract.sol", "/a/b/c/contract.sol")); + BOOST_TEST(!FileReader::isPathPrefix("/a/b/c/contract.sol", "/a/b/c/d/contract.sol")); + BOOST_TEST(!FileReader::isPathPrefix("/a/b/c/d/contract.sol", "/a/b/c/contract.sol")); + BOOST_TEST(!FileReader::isPathPrefix("/a/b/c/contract.sol", "/contract.sol")); +} + +BOOST_AUTO_TEST_CASE(isPathPrefix_directory_prefix) +{ + BOOST_TEST(FileReader::isPathPrefix("/", "/")); + BOOST_TEST(!FileReader::isPathPrefix("/a/b/c/", "/")); + BOOST_TEST(!FileReader::isPathPrefix("/a/b/c", "/")); + + BOOST_TEST(FileReader::isPathPrefix("/", "/a/bc/")); + BOOST_TEST(FileReader::isPathPrefix("/a", "/a/bc/")); + BOOST_TEST(FileReader::isPathPrefix("/a/", "/a/bc/")); + BOOST_TEST(FileReader::isPathPrefix("/a/bc", "/a/bc/")); + BOOST_TEST(FileReader::isPathPrefix("/a/bc/", "/a/bc/")); + + BOOST_TEST(!FileReader::isPathPrefix("/a", "/b/")); + BOOST_TEST(!FileReader::isPathPrefix("/a/", "/b/")); + BOOST_TEST(!FileReader::isPathPrefix("/a/contract.sol", "/a/b/")); + + BOOST_TEST(!FileReader::isPathPrefix("/a/b/c/", "/a/b/")); + BOOST_TEST(!FileReader::isPathPrefix("/a/b/c", "/a/b/")); +} + +BOOST_AUTO_TEST_CASE(isPathPrefix_unc_path) +{ + BOOST_TEST(FileReader::isPathPrefix("//host/a/b/", "//host/a/b/")); + BOOST_TEST(FileReader::isPathPrefix("//host/a/b", "//host/a/b/")); + BOOST_TEST(FileReader::isPathPrefix("//host/a/", "//host/a/b/")); + BOOST_TEST(FileReader::isPathPrefix("//host/a", "//host/a/b/")); + BOOST_TEST(FileReader::isPathPrefix("//host/", "//host/a/b/")); + + // NOTE: //host and // cannot be passed to isPathPrefix() because they are considered relative. + + BOOST_TEST(!FileReader::isPathPrefix("//host1/", "//host2/")); + BOOST_TEST(!FileReader::isPathPrefix("//host1/a/b/", "//host2/a/b/")); + + BOOST_TEST(!FileReader::isPathPrefix("/a/b/c/", "//a/b/c/")); + BOOST_TEST(!FileReader::isPathPrefix("//a/b/c/", "/a/b/c/")); +} + +BOOST_AUTO_TEST_CASE(isPathPrefix_case_sensitivity) +{ + BOOST_TEST(!FileReader::isPathPrefix("/a.sol", "/A.sol")); + BOOST_TEST(!FileReader::isPathPrefix("/A.sol", "/a.sol")); + BOOST_TEST(!FileReader::isPathPrefix("/A/", "/a/")); + BOOST_TEST(!FileReader::isPathPrefix("/a/", "/A/")); + BOOST_TEST(!FileReader::isPathPrefix("/a/BC/def/", "/a/bc/def/contract.sol")); +} + +BOOST_AUTO_TEST_CASE(stripPrefixIfPresent_file_prefix) +{ + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/", "/contract.sol"), "contract.sol"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/contract.sol", "/contract.sol"), "."); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/contract.sol/", "/contract.sol"), "."); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/contract.sol/.", "/contract.sol"), "."); + + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/", "/a/bc/def/contract.sol"), "a/bc/def/contract.sol"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a", "/a/bc/def/contract.sol"), "bc/def/contract.sol"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/", "/a/bc/def/contract.sol"), "bc/def/contract.sol"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/bc", "/a/bc/def/contract.sol"), "def/contract.sol"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/bc/def/", "/a/bc/def/contract.sol"), "contract.sol"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/bc/def/contract.sol", "/a/bc/def/contract.sol"), "."); + + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/contract.sol", "/token.sol"), "/token.sol"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/contract", "/contract.sol"), "/contract.sol"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/contract.sol", "/contract"), "/contract"); + + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/b/c/contract.sol", "/a/b/contract.sol"), "/a/b/contract.sol"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/b/contract.sol", "/a/b/c/contract.sol"), "/a/b/c/contract.sol"); +} + +BOOST_AUTO_TEST_CASE(stripPrefixIfPresent_directory_prefix) +{ + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/", "/"), "."); + + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/", "/a/bc/def/"), "a/bc/def/"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a", "/a/bc/def/"), "bc/def/"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/", "/a/bc/def/"), "bc/def/"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/bc", "/a/bc/def/"), "def/"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/bc/def/", "/a/bc/def/"), "."); + + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a", "/b/"), "/b/"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/", "/b/"), "/b/"); + + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/b/c/", "/a/b/"), "/a/b/"); + BOOST_CHECK_EQUAL(FileReader::stripPrefixIfPresent("/a/b/c", "/a/b/"), "/a/b/"); +} + +BOOST_AUTO_TEST_CASE(isUNCPath) +{ + BOOST_TEST(FileReader::isUNCPath("//")); + BOOST_TEST(FileReader::isUNCPath("//root")); + BOOST_TEST(FileReader::isUNCPath("//root/")); + +#if defined(_WIN32) + // On Windows boost sees these as ///, which is equivalent to / + BOOST_TEST(!FileReader::isUNCPath("//\\")); + BOOST_TEST(!FileReader::isUNCPath("\\\\/")); + BOOST_TEST(!FileReader::isUNCPath("\\/\\")); + + BOOST_TEST(FileReader::isUNCPath("\\\\")); + BOOST_TEST(FileReader::isUNCPath("\\\\root")); + BOOST_TEST(FileReader::isUNCPath("\\\\root/")); +#else + // On UNIX it's actually an UNC path + BOOST_TEST(FileReader::isUNCPath("//\\")); + + // On UNIX these are just weird relative directory names consisting only of backslashes. + BOOST_TEST(!FileReader::isUNCPath("\\\\/")); + BOOST_TEST(!FileReader::isUNCPath("\\/\\")); + + BOOST_TEST(!FileReader::isUNCPath("\\\\")); + BOOST_TEST(!FileReader::isUNCPath("\\\\root")); + BOOST_TEST(!FileReader::isUNCPath("\\\\root/")); +#endif + + BOOST_TEST(!FileReader::isUNCPath("\\/")); + BOOST_TEST(!FileReader::isUNCPath("/\\")); + + BOOST_TEST(!FileReader::isUNCPath("")); + BOOST_TEST(!FileReader::isUNCPath(".")); + BOOST_TEST(!FileReader::isUNCPath("..")); + BOOST_TEST(!FileReader::isUNCPath("/")); + BOOST_TEST(!FileReader::isUNCPath("a")); + BOOST_TEST(!FileReader::isUNCPath("a/b/c")); + BOOST_TEST(!FileReader::isUNCPath("contract.sol")); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace solidity::frontend::test diff --git a/test/solc/CommandLineInterface.cpp b/test/solc/CommandLineInterface.cpp index e9c16af5b..5ede475ab 100644 --- a/test/solc/CommandLineInterface.cpp +++ b/test/solc/CommandLineInterface.cpp @@ -141,14 +141,19 @@ BOOST_AUTO_TEST_CASE(cli_input) createFilesWithParentDirs({tempDir1.path() / "input1.sol"}); createFilesWithParentDirs({tempDir2.path() / "input2.sol"}); + boost::filesystem::path expectedDir1 = "/" / tempDir1.path().relative_path(); + boost::filesystem::path expectedDir2 = "/" / tempDir2.path().relative_path(); + soltestAssert(expectedDir1.is_absolute() || expectedDir1.root_path() == "/", ""); + soltestAssert(expectedDir2.is_absolute() || expectedDir2.root_path() == "/", ""); + vector expectedRemappings = { {"", "a", "b/c/d"}, {"a", "b", "c/d/e/"}, }; map expectedSources = { {"", "\n"}, - {(tempDir1.path() / "input1.sol").generic_string(), ""}, - {(tempDir2.path() / "input2.sol").generic_string(), ""}, + {(expectedDir1 / "input1.sol").generic_string(), ""}, + {(expectedDir2 / "input2.sol").generic_string(), ""}, }; PathSet expectedAllowedPaths = { boost::filesystem::canonical(tempDir1.path()), @@ -181,8 +186,11 @@ BOOST_AUTO_TEST_CASE(cli_ignore_missing_some_files_exist) TemporaryDirectory tempDir2(TEST_CASE_NAME); createFilesWithParentDirs({tempDir1.path() / "input1.sol"}); + boost::filesystem::path expectedDir1 = "/" / tempDir1.path().relative_path(); + soltestAssert(expectedDir1.is_absolute() || expectedDir1.root_path() == "/", ""); + // NOTE: Allowed paths should not be added for skipped files. - map expectedSources = {{(tempDir1.path() / "input1.sol").generic_string(), ""}}; + map expectedSources = {{(expectedDir1 / "input1.sol").generic_string(), ""}}; PathSet expectedAllowedPaths = {boost::filesystem::canonical(tempDir1.path())}; OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({ @@ -232,6 +240,7 @@ BOOST_AUTO_TEST_CASE(cli_not_a_file) BOOST_AUTO_TEST_CASE(standard_json_base_path) { TemporaryDirectory tempDir(TEST_CASE_NAME); + TemporaryWorkingDirectory tempWorkDir(tempDir.path().root_path()); OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({ "solc", @@ -245,7 +254,7 @@ BOOST_AUTO_TEST_CASE(standard_json_base_path) BOOST_TEST(result.options.input.paths.empty()); BOOST_TEST(result.reader.sourceCodes().empty()); BOOST_TEST(result.reader.allowedDirectories().empty()); - BOOST_TEST(result.reader.basePath() == tempDir.path()); + BOOST_TEST(result.reader.basePath() == "/" / tempDir.path().relative_path()); } BOOST_AUTO_TEST_CASE(standard_json_no_input_file) @@ -354,6 +363,9 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_no_base_path) boost::filesystem::path currentDirNoSymlinks = boost::filesystem::canonical(tempDirCurrent.path()); boost::filesystem::path otherDirNoSymlinks = boost::filesystem::canonical(tempDirOther.path()); + boost::filesystem::path expectedOtherDir = "/" / otherDirNoSymlinks.relative_path(); + soltestAssert(expectedOtherDir.is_absolute() || expectedOtherDir.root_path() == "/", ""); + vector commandLine = { "solc", "contract1.sol", // Relative path @@ -374,8 +386,8 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_no_base_path) map expectedSources = { {"contract1.sol", ""}, {"c/d/contract2.sol", ""}, - {currentDirNoSymlinks.generic_string() + "/contract3.sol", ""}, - {otherDirNoSymlinks.generic_string() + "/contract4.sol", ""}, + {"contract3.sol", ""}, + {expectedOtherDir.generic_string() + "/contract4.sol", ""}, }; FileReader::FileSystemPathSet expectedAllowedDirectories = { @@ -409,6 +421,11 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_same_as_work_dir) boost::filesystem::path currentDirNoSymlinks = boost::filesystem::canonical(tempDirCurrent.path()); boost::filesystem::path otherDirNoSymlinks = boost::filesystem::canonical(tempDirOther.path()); + boost::filesystem::path expectedWorkDir = "/" / boost::filesystem::current_path().relative_path(); + boost::filesystem::path expectedOtherDir = "/" / otherDirNoSymlinks.relative_path(); + soltestAssert(expectedWorkDir.is_absolute() || expectedWorkDir.root_path() == "/", ""); + soltestAssert(expectedOtherDir.is_absolute() || expectedOtherDir.root_path() == "/", ""); + vector commandLine = { "solc", "--base-path=" + currentDirNoSymlinks.string(), @@ -431,8 +448,8 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_same_as_work_dir) map expectedSources = { {"contract1.sol", ""}, {"c/d/contract2.sol", ""}, - {currentDirNoSymlinks.generic_string() + "/contract3.sol", ""}, - {otherDirNoSymlinks.generic_string() + "/contract4.sol", ""}, + {"contract3.sol", ""}, + {expectedOtherDir.generic_string() + "/contract4.sol", ""}, }; FileReader::FileSystemPathSet expectedAllowedDirectories = { @@ -450,7 +467,7 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_same_as_work_dir) BOOST_TEST(result.options == expectedOptions); BOOST_TEST(result.reader.sourceCodes() == expectedSources); BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); - BOOST_TEST(result.reader.basePath() == expectedOptions.input.basePath); + BOOST_TEST(result.reader.basePath() == expectedWorkDir); } BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_different_from_work_dir) @@ -469,6 +486,15 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_different_from_wor boost::filesystem::path otherDirNoSymlinks = boost::filesystem::canonical(tempDirOther.path()); boost::filesystem::path baseDirNoSymlinks = boost::filesystem::canonical(tempDirBase.path()); + boost::filesystem::path expectedWorkDir = "/" / boost::filesystem::current_path().relative_path(); + boost::filesystem::path expectedCurrentDir = "/" / currentDirNoSymlinks.relative_path(); + boost::filesystem::path expectedOtherDir = "/" / otherDirNoSymlinks.relative_path(); + boost::filesystem::path expectedBaseDir = "/" / baseDirNoSymlinks.relative_path(); + soltestAssert(expectedWorkDir.is_absolute() || expectedWorkDir.root_path() == "/", ""); + soltestAssert(expectedCurrentDir.is_absolute() || expectedCurrentDir.root_path() == "/", ""); + soltestAssert(expectedOtherDir.is_absolute() || expectedOtherDir.root_path() == "/", ""); + soltestAssert(expectedBaseDir.is_absolute() || expectedBaseDir.root_path() == "/", ""); + vector commandLine = { "solc", "--base-path=" + baseDirNoSymlinks.string(), @@ -491,11 +517,11 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_different_from_wor expectedOptions.modelChecker.initialize = true; map expectedSources = { - {"contract1.sol", ""}, - {"c/d/contract2.sol", ""}, - {currentDirNoSymlinks.generic_string() + "/contract3.sol", ""}, - {otherDirNoSymlinks.generic_string() + "/contract4.sol", ""}, - {baseDirNoSymlinks.generic_string() + "/contract5.sol", ""}, + {expectedWorkDir.generic_string() + "/contract1.sol", ""}, + {expectedWorkDir.generic_string() + "/c/d/contract2.sol", ""}, + {expectedCurrentDir.generic_string() + "/contract3.sol", ""}, + {expectedOtherDir.generic_string() + "/contract4.sol", ""}, + {"contract5.sol", ""}, }; FileReader::FileSystemPathSet expectedAllowedDirectories = { @@ -514,7 +540,7 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_different_from_wor BOOST_TEST(result.options == expectedOptions); BOOST_TEST(result.reader.sourceCodes() == expectedSources); BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); - BOOST_TEST(result.reader.basePath() == expectedOptions.input.basePath); + BOOST_TEST(result.reader.basePath() == expectedBaseDir); } BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_relative_base_path) @@ -530,6 +556,11 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_relative_base_path) boost::filesystem::path currentDirNoSymlinks = boost::filesystem::canonical(tempDirCurrent.path()); boost::filesystem::path otherDirNoSymlinks = boost::filesystem::canonical(tempDirOther.path()); + boost::filesystem::path expectedWorkDir = "/" / boost::filesystem::current_path().relative_path(); + boost::filesystem::path expectedOtherDir = "/" / otherDirNoSymlinks.relative_path(); + soltestAssert(expectedWorkDir.is_absolute() || expectedWorkDir.root_path() == "/", ""); + soltestAssert(expectedOtherDir.is_absolute() || expectedOtherDir.root_path() == "/", ""); + vector commandLine = { "solc", "--base-path=base", @@ -554,12 +585,12 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_relative_base_path) expectedOptions.modelChecker.initialize = true; map expectedSources = { - {"contract1.sol", ""}, - {"base/contract2.sol", ""}, - {currentDirNoSymlinks.generic_string() + "/contract3.sol", ""}, - {currentDirNoSymlinks.generic_string() + "/base/contract4.sol", ""}, - {otherDirNoSymlinks.generic_string() + "/contract5.sol", ""}, - {otherDirNoSymlinks.generic_string() + "/base/contract6.sol", ""}, + {expectedWorkDir.generic_string() + "/contract1.sol", ""}, + {"contract2.sol", ""}, + {expectedWorkDir.generic_string() + "/contract3.sol", ""}, + {"contract4.sol", ""}, + {expectedOtherDir.generic_string() + "/contract5.sol", ""}, + {expectedOtherDir.generic_string() + "/base/contract6.sol", ""}, }; FileReader::FileSystemPathSet expectedAllowedDirectories = { @@ -578,7 +609,7 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_relative_base_path) BOOST_TEST(result.options == expectedOptions); BOOST_TEST(result.reader.sourceCodes() == expectedSources); BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); - BOOST_TEST(result.reader.basePath() == expectedOptions.input.basePath); + BOOST_TEST(result.reader.basePath() == expectedWorkDir / "base"); } BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_normalization_and_weird_names) @@ -589,11 +620,13 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_normalization_and_weird_name soltestAssert(tempDir.path().is_absolute(), ""); string uncPath = "//" + tempDir.path().relative_path().generic_string(); - soltestAssert(uncPath[0] == '/' && uncPath[1] == '/', ""); - soltestAssert(uncPath[2] != '/', ""); + soltestAssert(FileReader::isUNCPath(uncPath), ""); boost::filesystem::path tempDirNoSymlinks = boost::filesystem::canonical(tempDir.path()); + boost::filesystem::path expectedWorkDir = "/" / boost::filesystem::current_path().relative_path(); + soltestAssert(expectedWorkDir.is_absolute() || expectedWorkDir.root_path() == "/", ""); + vector commandLine = { "solc", @@ -684,30 +717,29 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_normalization_and_weird_name map expectedSources = { #if !defined(_WIN32) - {"file://c/d/contract1.sol", ""}, - {"file:///c/d/contract2.sol", ""}, - {"https://example.com/contract3.sol", ""}, + {"file:/c/d/contract1.sol", ""}, + {"file:/c/d/contract2.sol", ""}, + {"https:/example.com/contract3.sol", ""}, #endif - {"a/b//contract4.sol", ""}, - {"a/b///contract5.sol", ""}, - {"a/b////contract6.sol", ""}, + {"a/b/contract4.sol", ""}, + {"a/b/contract5.sol", ""}, + {"a/b/contract6.sol", ""}, - {"./a/b/contract7.sol", ""}, - {"././a/b/contract8.sol", ""}, - {"a/./b/contract9.sol", ""}, - {"a/././b/contract10.sol", ""}, + {"a/b/contract7.sol", ""}, + {"a/b/contract8.sol", ""}, + {"a/b/contract9.sol", ""}, + {"a/b/contract10.sol", ""}, - {"../a/b/contract11.sol", ""}, - {"../../a/b/contract12.sol", ""}, - {"a/../b/contract13.sol", ""}, - {"a/b/../../contract14.sol", ""}, - {tempDirNoSymlinks.string() + "/x/y/z/a/../b/contract15.sol", ""}, - {tempDirNoSymlinks.string() + "/x/y/z/a/b/../../contract16.sol", ""}, - - {"/../" + tempDir.path().relative_path().generic_string() + "/contract17.sol", ""}, - {"/../../" + tempDir.path().relative_path().generic_string() + "/contract18.sol", ""}, + {expectedWorkDir.parent_path().generic_string() + "/a/b/contract11.sol", ""}, + {expectedWorkDir.parent_path().parent_path().generic_string() + "/a/b/contract12.sol", ""}, + {"b/contract13.sol", ""}, + {"contract14.sol", ""}, + {"b/contract15.sol", ""}, + {"contract16.sol", ""}, + {"/" + tempDir.path().relative_path().generic_string() + "/contract17.sol", ""}, + {"/" + tempDir.path().relative_path().generic_string() + "/contract18.sol", ""}, #if !defined(_WIN32) {"", ""}, @@ -766,6 +798,8 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_symlinks) ) return; + boost::filesystem::path expectedWorkDir = "/" / boost::filesystem::current_path().relative_path(); + soltestAssert(expectedWorkDir.is_absolute() || expectedWorkDir.root_path() == "/", ""); vector commandLine = { "solc", @@ -788,10 +822,10 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_symlinks) expectedOptions.modelChecker.initialize = true; map expectedSources = { - {"sym/z/contract.sol", ""}, - {"../x/y/z/contract.sol", ""}, - {"sym/z/contract_symlink.sol", ""}, - {"../x/y/z/contract_symlink.sol", ""}, + {"contract.sol", ""}, + {(expectedWorkDir.parent_path() / "x/y/z/contract.sol").generic_string(), ""}, + {"contract_symlink.sol", ""}, + {(expectedWorkDir.parent_path() / "x/y/z/contract_symlink.sol").generic_string(), ""}, }; FileReader::FileSystemPathSet expectedAllowedDirectories = { @@ -806,7 +840,7 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_symlinks) BOOST_TEST(result.options == expectedOptions); BOOST_TEST(result.reader.sourceCodes() == expectedSources); BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); - BOOST_TEST(result.reader.basePath() == expectedOptions.input.basePath); + BOOST_TEST(result.reader.basePath() == expectedWorkDir / "sym/z/"); } BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_and_stdin) @@ -838,7 +872,7 @@ BOOST_AUTO_TEST_CASE(cli_paths_to_source_unit_names_base_path_and_stdin) BOOST_TEST(result.options == expectedOptions); BOOST_TEST(result.reader.sourceCodes() == expectedSources); BOOST_TEST(result.reader.allowedDirectories() == expectedAllowedDirectories); - BOOST_TEST(result.reader.basePath() == "base"); + BOOST_TEST(result.reader.basePath() == expectedWorkDir / "base"); } BOOST_AUTO_TEST_SUITE_END()