From 8e23f9cc92d59a77b6d92e16649cf1045b085805 Mon Sep 17 00:00:00 2001 From: arvidn Date: Sun, 21 Jul 2019 15:50:57 -0700 Subject: [PATCH] improve file_storage::sanitize_symlinks --- ChangeLog | 1 + include/libtorrent/aux_/path.hpp | 1 + include/libtorrent/file_storage.hpp | 1 + include/libtorrent/string_view.hpp | 35 ++++ src/file_storage.cpp | 176 +++++++++++++++--- src/path.cpp | 19 ++ test/Makefile.am | 3 +- test/test_file.cpp | 36 ++++ test/test_file_storage.cpp | 129 +++++++++++++ test/test_string.cpp | 14 ++ test/test_torrent_info.cpp | 8 + .../overlapping_symlinks.torrent | Bin 0 -> 6119 bytes 12 files changed, 401 insertions(+), 22 deletions(-) create mode 100644 test/test_torrents/overlapping_symlinks.torrent diff --git a/ChangeLog b/ChangeLog index 7d74b05ce..d301d9f1c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,4 @@ + * improve sanitation of symlinks, to support more complex link targets * add DHT routing table affinity for BEP 42 nodes * add torrent_info constructor overloads to control torrent file limits * feature to disable DHT, PEX and LSD per torrent diff --git a/include/libtorrent/aux_/path.hpp b/include/libtorrent/aux_/path.hpp index 7823601a6..942f33fba 100644 --- a/include/libtorrent/aux_/path.hpp +++ b/include/libtorrent/aux_/path.hpp @@ -139,6 +139,7 @@ namespace libtorrent { // split out a path segment from the left side or right side TORRENT_EXTRA_EXPORT std::pair rsplit_path(string_view p); TORRENT_EXTRA_EXPORT std::pair lsplit_path(string_view p); + TORRENT_EXTRA_EXPORT std::pair lsplit_path(string_view p, std::size_t pos); TORRENT_EXTRA_EXPORT std::string extension(std::string const& f); TORRENT_EXTRA_EXPORT std::string remove_extension(std::string const& f); diff --git a/include/libtorrent/file_storage.hpp b/include/libtorrent/file_storage.hpp index 3a4a0e2ab..bca20d472 100644 --- a/include/libtorrent/file_storage.hpp +++ b/include/libtorrent/file_storage.hpp @@ -532,6 +532,7 @@ namespace libtorrent { private: + std::string internal_file_path(file_index_t index) const; file_index_t last_file() const noexcept; int get_or_add_path(string_view path); diff --git a/include/libtorrent/string_view.hpp b/include/libtorrent/string_view.hpp index 849ba3c7a..4a68522d5 100644 --- a/include/libtorrent/string_view.hpp +++ b/include/libtorrent/string_view.hpp @@ -39,10 +39,33 @@ POSSIBILITY OF SUCH DAMAGE. #if BOOST_VERSION < 106100 #include +#include // for strchr namespace libtorrent { using string_view = boost::string_ref; using wstring_view = boost::wstring_ref; + +inline string_view::size_type find_first_of(string_view const v, char const c + , string_view::size_type pos) +{ + while (pos < v.size()) + { + if (v[pos] == c) return pos; + ++pos; + } + return string_view::npos; +} + +inline string_view::size_type find_first_of(string_view const v, char const* c + , string_view::size_type pos) +{ + while (pos < v.size()) + { + if (std::strchr(c, v[pos]) != nullptr) return pos; + ++pos; + } + return string_view::npos; +} } #else #include @@ -50,6 +73,18 @@ namespace libtorrent { using string_view = boost::string_view; using wstring_view = boost::wstring_view; + +inline string_view::size_type find_first_of(string_view const v, char const c + , string_view::size_type pos) +{ + return v.find_first_of(c, pos); +} + +inline string_view::size_type find_first_of(string_view const v, char const* c + , string_view::size_type pos) +{ + return v.find_first_of(c, pos); +} } #endif diff --git a/src/file_storage.cpp b/src/file_storage.cpp index 3438b3aed..01d069144 100644 --- a/src/file_storage.cpp +++ b/src/file_storage.cpp @@ -44,6 +44,7 @@ POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #if defined(TORRENT_WINDOWS) || defined(TORRENT_OS2) #define TORRENT_SEPARATOR '\\' @@ -657,7 +658,16 @@ namespace { TORRENT_ASSERT_PRECOND(index >= file_index_t(0) && index < end_file()); internal_file_entry const& fe = m_files[index]; TORRENT_ASSERT(fe.symlink_index < int(m_symlinks.size())); - return m_symlinks[fe.symlink_index]; + + auto const& link = m_symlinks[fe.symlink_index]; + + // TODO: 3 this is a hack to retain ABI compatibility with 1.2.1 + // in next major release, make this return by value + static std::string ret; + ret.reserve(m_name.size() + link.size() + 1); + ret.assign(m_name); + append_path(ret, link); + return ret; } std::time_t file_storage::mtime(file_index_t const index) const @@ -818,6 +828,26 @@ namespace { return ret; } + std::string file_storage::internal_file_path(file_index_t const index) const + { + TORRENT_ASSERT_PRECOND(index >= file_index_t(0) && index < end_file()); + internal_file_entry const& fe = m_files[index]; + + if (fe.path_index >= 0) + { + std::string ret; + std::string const& p = m_paths[fe.path_index]; + ret.reserve(p.size() + fe.filename().size() + 2); + append_path(ret, p); + append_path(ret, fe.filename()); + return ret; + } + else + { + return fe.filename().to_string(); + } + } + string_view file_storage::file_name(file_index_t const index) const { TORRENT_ASSERT_PRECOND(index >= file_index_t(0) && index < end_file()); @@ -1112,13 +1142,26 @@ namespace { std::unordered_map file_map; bool file_map_initialized = false; + // lazily instantiated set of all valid directories a symlink may point to + // TODO: in C++17 this could be string_view + std::unordered_set dir_map; + bool dir_map_initialized = false; + + // symbolic links that points to directories + std::unordered_map dir_links; + + // we validate symlinks in (potentially) 2 passes over the files. + // remaining symlinks to validate after the first pass + std::vector symlinks_to_validate; + for (auto const i : file_range()) { if (!(file_flags(i) & file_storage::flag_symlink)) continue; if (!file_map_initialized) { - for (auto const j : file_range()) file_map[file_path(j)] = j; + for (auto const j : file_range()) + file_map.insert({internal_file_path(j), j}); file_map_initialized = true; } @@ -1128,54 +1171,145 @@ namespace { // symlink targets are only allowed to point to files or directories in // this torrent. { - std::string target = symlink(i); + std::string target = m_symlinks[fe.symlink_index]; - // if it points to a directory, that's OK - auto it = std::find(m_paths.begin(), m_paths.end(), target); - if (it != m_paths.end()) + if (is_complete(target)) { - m_symlinks[fe.symlink_index] = combine_path(name(), *it); + // a symlink target is not allowed to be an absolute path, ever + // this symlink is invalid, make it point to itself + m_symlinks[fe.symlink_index] = internal_file_path(i); continue; } - target = combine_path(name(), target); - - auto const idx = file_map.find(target); - if (idx != file_map.end()) + auto const iter = file_map.find(target); + if (iter != file_map.end()) { m_symlinks[fe.symlink_index] = target; + if (file_flags(iter->second) & file_storage::flag_symlink) + { + // we don't know whether this symlink is a file or a + // directory, so make the conservative assumption that it's a + // directory + dir_links[internal_file_path(i)] = target; + } continue; } - } - // this symlink target points to a file that's not part of this torrent - // file structure. That's not allowed by the spec. + // it may point to a directory that doesn't have any files (but only + // other directories), in which case it won't show up in m_paths + if (!dir_map_initialized) + { + for (auto const& p : m_paths) + for (string_view pv = p; !pv.empty(); pv = rsplit_path(pv).first) + dir_map.insert(pv.to_string()); + dir_map_initialized = true; + } + + if (dir_map.count(target)) + { + // it points to a sub directory within the torrent, that's OK + m_symlinks[fe.symlink_index] = target; + dir_links[internal_file_path(i)] = target; + continue; + } + + } // for backwards compatibility, allow paths relative to the link as // well if (fe.path_index >= 0) { std::string target = m_paths[fe.path_index]; - append_path(target, symlink(i)); + append_path(target, m_symlinks[fe.symlink_index]); // if it points to a directory, that's OK - auto it = std::find(m_paths.begin(), m_paths.end(), target); + auto const it = std::find(m_paths.begin(), m_paths.end(), target); if (it != m_paths.end()) { - m_symlinks[fe.symlink_index] = combine_path(name(), *it); + m_symlinks[fe.symlink_index] = *it; + dir_links[internal_file_path(i)] = *it; continue; } - target = combine_path(name(), target); - auto const idx = file_map.find(target); - if (idx != file_map.end()) + if (dir_map.count(target)) + { + // it points to a sub directory within the torrent, that's OK + m_symlinks[fe.symlink_index] = target; + dir_links[internal_file_path(i)] = target; + continue; + } + + auto const iter = file_map.find(target); + if (iter != file_map.end()) { m_symlinks[fe.symlink_index] = target; + if (file_flags(iter->second) & file_storage::flag_symlink) + { + // we don't know whether this symlink is a file or a + // directory, so make the conservative assumption that it's a + // directory + dir_links[internal_file_path(i)] = target; + } continue; } } + // we don't know whether this symlink is a file or a + // directory, so make the conservative assumption that it's a + // directory + dir_links[internal_file_path(i)] = m_symlinks[fe.symlink_index]; + symlinks_to_validate.push_back(i); + } + + // in case there were some "complex" symlinks, we nee a second pass to + // validate those. For example, symlinks whose target rely on other + // symlinks + for (auto const i : symlinks_to_validate) + { + internal_file_entry const& fe = m_files[i]; + TORRENT_ASSERT(fe.symlink_index < int(m_symlinks.size())); + + std::string target = m_symlinks[fe.symlink_index]; + + // to avoid getting stuck in an infinite loop, we only allow traversing + // a symlink once + std::set traversed; + + // this is where we check every path element for existence. If it's not + // among the concrete paths, it may be a symlink, which is also OK + // note that we won't iterate through this for the last step, where the + // filename is included. The filename is validated after the loop + for (string_view branch = lsplit_path(target).first; + branch.size() < target.size(); + branch = lsplit_path(target, branch.size() + 1).first) + { + // this is a concrete directory + if (dir_map.count(branch.to_string())) continue; + + auto const iter = dir_links.find(branch.to_string()); + if (iter == dir_links.end()) goto failed; + if (traversed.count(branch.to_string())) goto failed; + traversed.insert(branch.to_string()); + + // this path element is a symlink. substitute the branch so far by + // the link target + target = combine_path(iter->second, target.substr(branch.size() + 1)); + + // start over with the new (concrete) path + branch = {}; + } + + // the final (resolved) target must be a valid file + // or directory + if (file_map.count(target) == 0 + && dir_map.count(target) == 0) goto failed; + + // this is OK + continue; + +failed: + // this symlink is invalid, make it point to itself - m_symlinks[fe.symlink_index] = file_path(i); + m_symlinks[fe.symlink_index] = internal_file_path(i); } } diff --git a/src/path.cpp b/src/path.cpp index 938e892fa..847e1b2db 100644 --- a/src/path.cpp +++ b/src/path.cpp @@ -910,6 +910,25 @@ namespace { return { p.substr(0, sep), p.substr(sep + 1) }; } + std::pair lsplit_path(string_view p, std::size_t pos) + { + if (p.empty()) return {{}, {}}; + // for absolute paths, skip the initial "/" + if (p.front() == TORRENT_SEPARATOR_CHAR +#if defined(TORRENT_WINDOWS) || defined(TORRENT_OS2) + || p.front() == '/' +#endif + ) + { p.remove_prefix(1); if (pos > 0) --pos; } +#if defined(TORRENT_WINDOWS) || defined(TORRENT_OS2) + auto const sep = find_first_of(p, "/\\", std::string::size_type(pos)); +#else + auto const sep = find_first_of(p, TORRENT_SEPARATOR_CHAR, std::string::size_type(pos)); +#endif + if (sep == string_view::npos) return {p, {}}; + return { p.substr(0, sep), p.substr(sep + 1) }; + } + std::string complete(string_view f) { if (is_complete(f)) return f.to_string(); diff --git a/test/Makefile.am b/test/Makefile.am index 249906115..32247e2eb 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -120,7 +120,8 @@ TEST_TORRENTS = \ url_seed_multi_space.torrent \ url_seed_multi_space_nolist.torrent \ url_seed_multi_single_file.torrent \ - whitespace_url.torrent + whitespace_url.torrent \ + overlapping_symlinks.torrent MUTABLE_TEST_TORRENTS = \ test1.torrent \ diff --git a/test/test_file.cpp b/test/test_file.cpp index 8f6590f37..5965fb9f9 100644 --- a/test/test_file.cpp +++ b/test/test_file.cpp @@ -317,6 +317,42 @@ TORRENT_TEST(split_path) TEST_CHECK(rsplit_path("") == r("", "")); } +TORRENT_TEST(split_path_pos) +{ + using r = std::pair; + +#ifdef TORRENT_WINDOWS + TEST_CHECK(lsplit_path("\\b\\c\\d", 0) == r("b", "c\\d")); + TEST_CHECK(lsplit_path("\\b\\c\\d", 1) == r("b", "c\\d")); + TEST_CHECK(lsplit_path("\\b\\c\\d", 2) == r("b", "c\\d")); + TEST_CHECK(lsplit_path("\\b\\c\\d", 3) == r("b\\c", "d")); + TEST_CHECK(lsplit_path("\\b\\c\\d", 4) == r("b\\c", "d")); + TEST_CHECK(lsplit_path("\\b\\c\\d", 5) == r("b\\c\\d", "")); + TEST_CHECK(lsplit_path("\\b\\c\\d", 6) == r("b\\c\\d", "")); + + TEST_CHECK(lsplit_path("b\\c\\d", 0) == r("b", "c\\d")); + TEST_CHECK(lsplit_path("b\\c\\d", 1) == r("b", "c\\d")); + TEST_CHECK(lsplit_path("b\\c\\d", 2) == r("b\\c", "d")); + TEST_CHECK(lsplit_path("b\\c\\d", 3) == r("b\\c", "d")); + TEST_CHECK(lsplit_path("b\\c\\d", 4) == r("b\\c\\d", "")); + TEST_CHECK(lsplit_path("b\\c\\d", 5) == r("b\\c\\d", "")); +#endif + TEST_CHECK(lsplit_path("/b/c/d", 0) == r("b", "c/d")); + TEST_CHECK(lsplit_path("/b/c/d", 1) == r("b", "c/d")); + TEST_CHECK(lsplit_path("/b/c/d", 2) == r("b", "c/d")); + TEST_CHECK(lsplit_path("/b/c/d", 3) == r("b/c", "d")); + TEST_CHECK(lsplit_path("/b/c/d", 4) == r("b/c", "d")); + TEST_CHECK(lsplit_path("/b/c/d", 5) == r("b/c/d", "")); + TEST_CHECK(lsplit_path("/b/c/d", 6) == r("b/c/d", "")); + + TEST_CHECK(lsplit_path("b/c/d", 0) == r("b", "c/d")); + TEST_CHECK(lsplit_path("b/c/d", 1) == r("b", "c/d")); + TEST_CHECK(lsplit_path("b/c/d", 2) == r("b/c", "d")); + TEST_CHECK(lsplit_path("b/c/d", 3) == r("b/c", "d")); + TEST_CHECK(lsplit_path("b/c/d", 4) == r("b/c/d", "")); + TEST_CHECK(lsplit_path("b/c/d", 5) == r("b/c/d", "")); +} + // file class TORRENT_TEST(file) { diff --git a/test/test_file_storage.cpp b/test/test_file_storage.cpp index 80a181774..415cf6d89 100644 --- a/test/test_file_storage.cpp +++ b/test/test_file_storage.cpp @@ -624,6 +624,135 @@ TORRENT_TEST(map_block_mid) } } +#ifdef TORRENT_WINDOWS +#define SEP "\\" +#else +#define SEP "/" +#endif + +TORRENT_TEST(sanitize_symlinks) +{ + file_storage fs; + fs.set_piece_length(1024); + + // invalid +#if defined(TORRENT_WINDOWS) || defined(TORRENT_OS2) + fs.add_file("test/0", 0, file_storage::flag_symlink, 0, "C:\\invalid\\target\\path"); +#else + fs.add_file("test/0", 0, file_storage::flag_symlink, 0, "/invalid/target/path"); +#endif + + // there is no file with this name, so this is invalid + fs.add_file("test/1", 0, file_storage::flag_symlink, 0, "ZZ"); + + // there is no file with this name, so this is invalid + fs.add_file("test/2", 0, file_storage::flag_symlink, 0, "B" SEP "B" SEP "ZZ"); + + // this should be OK + fs.add_file("test/3", 0, file_storage::flag_symlink, 0, "0"); + + // this should be OK + fs.add_file("test/4", 0, file_storage::flag_symlink, 0, "A"); + + // this is advanced, but OK + fs.add_file("test/5", 0, file_storage::flag_symlink, 0, "4" SEP "B"); + + // this is advanced, but OK + fs.add_file("test/6", 0, file_storage::flag_symlink, 0, "5" SEP "C"); + + // this is not OK + fs.add_file("test/7", 0, file_storage::flag_symlink, 0, "4" SEP "B" SEP "C" SEP "ZZ"); + + // this is the only actual content + fs.add_file("test/A" SEP "B" SEP "C", 10000); + fs.set_num_pieces(int((fs.total_size() + 1023) / 1024)); + + fs.sanitize_symlinks(); + + // these were all invalid symlinks, so they're made to point to themselves + TEST_EQUAL(fs.symlink(file_index_t{0}), "test" SEP "0"); + TEST_EQUAL(fs.symlink(file_index_t{1}), "test" SEP "1"); + TEST_EQUAL(fs.symlink(file_index_t{2}), "test" SEP "2"); + + // ok + TEST_EQUAL(fs.symlink(file_index_t{3}), "test" SEP "0"); + TEST_EQUAL(fs.symlink(file_index_t{4}), "test" SEP "A"); + TEST_EQUAL(fs.symlink(file_index_t{5}), "test" SEP "4" SEP "B"); + TEST_EQUAL(fs.symlink(file_index_t{6}), "test" SEP "5" SEP "C"); + + // does not point to a valid file + TEST_EQUAL(fs.symlink(file_index_t{7}), "test" SEP "7"); +} + +TORRENT_TEST(sanitize_symlinks_single_file) +{ + file_storage fs; + fs.set_piece_length(1024); + fs.add_file("test", 1); + fs.set_num_pieces(int((fs.total_size() + 1023) / 1024)); + + fs.sanitize_symlinks(); + + TEST_EQUAL(fs.file_path(file_index_t{0}), "test"); +} + +TORRENT_TEST(sanitize_symlinks_cascade) +{ + file_storage fs; + fs.set_piece_length(1024); + + fs.add_file("test/0", 0, file_storage::flag_symlink, 0, "1" SEP "ZZ"); + fs.add_file("test/1", 0, file_storage::flag_symlink, 0, "2"); + fs.add_file("test/2", 0, file_storage::flag_symlink, 0, "3"); + fs.add_file("test/3", 0, file_storage::flag_symlink, 0, "4"); + fs.add_file("test/4", 0, file_storage::flag_symlink, 0, "5"); + fs.add_file("test/5", 0, file_storage::flag_symlink, 0, "6"); + fs.add_file("test/6", 0, file_storage::flag_symlink, 0, "7"); + fs.add_file("test/7", 0, file_storage::flag_symlink, 0, "A"); + fs.add_file("test/no-exist", 0, file_storage::flag_symlink, 0, "1" SEP "ZZZ"); + + // this is the only actual content + fs.add_file("test/A" SEP "ZZ", 10000); + fs.set_num_pieces(int((fs.total_size() + 1023) / 1024)); + + fs.sanitize_symlinks(); + + TEST_EQUAL(fs.symlink(file_index_t{0}), "test" SEP "1" SEP "ZZ"); + TEST_EQUAL(fs.symlink(file_index_t{1}), "test" SEP "2"); + TEST_EQUAL(fs.symlink(file_index_t{2}), "test" SEP "3"); + TEST_EQUAL(fs.symlink(file_index_t{3}), "test" SEP "4"); + TEST_EQUAL(fs.symlink(file_index_t{4}), "test" SEP "5"); + TEST_EQUAL(fs.symlink(file_index_t{5}), "test" SEP "6"); + TEST_EQUAL(fs.symlink(file_index_t{6}), "test" SEP "7"); + TEST_EQUAL(fs.symlink(file_index_t{7}), "test" SEP "A"); + TEST_EQUAL(fs.symlink(file_index_t{8}), "test" SEP "no-exist"); +} + +TORRENT_TEST(sanitize_symlinks_circular) +{ + file_storage fs; + fs.set_piece_length(1024); + + fs.add_file("test/0", 0, file_storage::flag_symlink, 0, "1"); + fs.add_file("test/1", 0, file_storage::flag_symlink, 0, "0"); + + // when this is resolved, we end up in an infinite loop. Make sure we can + // handle that + fs.add_file("test/2", 0, file_storage::flag_symlink, 0, "0/ZZ"); + + // this is the only actual content + fs.add_file("test/A" SEP "ZZ", 10000); + fs.set_num_pieces(int((fs.total_size() + 1023) / 1024)); + + fs.sanitize_symlinks(); + + TEST_EQUAL(fs.symlink(file_index_t{0}), "test" SEP "1"); + TEST_EQUAL(fs.symlink(file_index_t{1}), "test" SEP "0"); + + // this was invalid, so it points to itself + TEST_EQUAL(fs.symlink(file_index_t{2}), "test" SEP "2"); +} + // TODO: test file attributes // TODO: test symlinks // TODO: test reorder_file (make sure internal_file_entry::swap() is used) diff --git a/test/test_string.cpp b/test/test_string.cpp index 1807ea3ce..f1ea06cdb 100644 --- a/test/test_string.cpp +++ b/test/test_string.cpp @@ -494,3 +494,17 @@ TORRENT_TEST(string_ptr_move_assign) TEST_CHECK(*p2 == nullptr); } +TORRENT_TEST(find_first_of) +{ + string_view test("01234567891"); + TEST_EQUAL(find_first_of(test, '1', 0), 1); + TEST_EQUAL(find_first_of(test, '1', 1), 1); + TEST_EQUAL(find_first_of(test, '1', 2), 10); + TEST_EQUAL(find_first_of(test, '1', 3), 10); + + TEST_EQUAL(find_first_of(test, "61", 0), 1); + TEST_EQUAL(find_first_of(test, "61", 1), 1); + TEST_EQUAL(find_first_of(test, "61", 2), 6); + TEST_EQUAL(find_first_of(test, "61", 3), 6); + TEST_EQUAL(find_first_of(test, "61", 4), 6); +} diff --git a/test/test_torrent_info.cpp b/test/test_torrent_info.cpp index 741fa33f5..3f1c26b7d 100644 --- a/test/test_torrent_info.cpp +++ b/test/test_torrent_info.cpp @@ -134,6 +134,7 @@ static test_torrent_t test_torrents[] = { "absolute_filename.torrent" }, { "invalid_filename.torrent" }, { "invalid_filename2.torrent" }, + { "overlapping_symlinks.torrent" }, }; struct test_failing_torrent_t @@ -846,6 +847,13 @@ TORRENT_TEST(parse_torrents) { TEST_EQUAL(ti->num_files(), 3); } + else if (t.file == "overlapping_symlinks.torrent"_sv) + { + TEST_CHECK(ti->num_files() > 3); + TEST_EQUAL(ti->files().symlink(file_index_t{0}), "SDL2.framework" SEPARATOR "Versions" SEPARATOR "Current" SEPARATOR "Headers"); + TEST_EQUAL(ti->files().symlink(file_index_t{1}), "SDL2.framework" SEPARATOR "Versions" SEPARATOR "Current" SEPARATOR "Resources"); + TEST_EQUAL(ti->files().symlink(file_index_t{2}), "SDL2.framework" SEPARATOR "Versions" SEPARATOR "Current" SEPARATOR "SDL2"); + } file_storage const& fs = ti->files(); for (file_index_t idx{0}; idx != file_index_t(fs.num_files()); ++idx) diff --git a/test/test_torrents/overlapping_symlinks.torrent b/test/test_torrents/overlapping_symlinks.torrent new file mode 100644 index 0000000000000000000000000000000000000000..43fc68a4e59ca4672d86eae794e27cbf2b6020e2 GIT binary patch literal 6119 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