From 4c84d5f5469ec469732a5656e3b601bd54c2d2a7 Mon Sep 17 00:00:00 2001 From: Gil Date: Tue, 5 Jun 2018 14:31:36 -0700 Subject: Add pathname manipulation utilities (#1376) --- .../Example/Firestore.xcodeproj/project.pbxproj | 4 + .../src/firebase/firestore/util/CMakeLists.txt | 2 + Firestore/core/src/firebase/firestore/util/path.cc | 84 ++++++++ Firestore/core/src/firebase/firestore/util/path.h | 101 +++++++++ .../test/firebase/firestore/util/CMakeLists.txt | 1 + .../core/test/firebase/firestore/util/path_test.cc | 240 +++++++++++++++++++++ 6 files changed, 432 insertions(+) create mode 100644 Firestore/core/src/firebase/firestore/util/path.cc create mode 100644 Firestore/core/src/firebase/firestore/util/path.h create mode 100644 Firestore/core/test/firebase/firestore/util/path_test.cc (limited to 'Firestore') diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index b366cb9..0ce2716 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -131,6 +131,7 @@ 54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */; }; 54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A51F315EE100DD57A1 /* write_spec_test.json */; }; 54EB764D202277B30088B8F3 /* array_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */; }; + 5A080105CCBFDB6BF3F3772D /* path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 403DBF6EFB541DFD01582AA3 /* path_test.cc */; }; 5D405BE298CE4692CB00790A /* Pods_Firestore_Tests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2B50B3A0DF77100EEE887891 /* Pods_Firestore_Tests_iOS.framework */; }; 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; }; @@ -271,6 +272,7 @@ 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = remote_store_spec_test.json; sourceTree = ""; }; 3C81DE3772628FE297055662 /* Pods-Firestore_Example_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS.debug.xcconfig"; sourceTree = ""; }; 3F0992A4B83C60841C52E960 /* Pods-Firestore_Example_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS.release.xcconfig"; sourceTree = ""; }; + 403DBF6EFB541DFD01582AA3 /* path_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = path_test.cc; sourceTree = ""; }; 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = hard_assert_test.cc; sourceTree = ""; }; 54131E9620ADE678001DF3FF /* string_format_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = string_format_test.cc; sourceTree = ""; }; 54511E8D209805F8005BD28F /* hashing_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = hashing_test.cc; sourceTree = ""; }; @@ -587,6 +589,7 @@ 54A0353420A3D8CB003E0143 /* iterator_adaptors_test.cc */, 54C2294E1FECABAE007D065B /* log_test.cc */, AB380D03201BC6E400D97691 /* ordered_code_test.cc */, + 403DBF6EFB541DFD01582AA3 /* path_test.cc */, 54740A531FC913E500713A1A /* secure_random_test.cc */, 54A0352C20A3B3D7003E0143 /* status_test.cc */, 54A0352B20A3B3D7003E0143 /* status_test_util.h */, @@ -1665,6 +1668,7 @@ AB6B908620322E6D00CC290A /* maybe_document_test.cc in Sources */, AB6B908820322E8800CC290A /* no_document_test.cc in Sources */, AB380D04201BC6E400D97691 /* ordered_code_test.cc in Sources */, + 5A080105CCBFDB6BF3F3772D /* path_test.cc in Sources */, 549CCA5920A36E1F00BCEB75 /* precondition_test.cc in Sources */, B686F2B22025000D0028D6BE /* resource_path_test.cc in Sources */, 54740A571FC914BA00713A1A /* secure_random_test.cc in Sources */, diff --git a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt index 30589a0..b2c4195 100644 --- a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt @@ -184,6 +184,8 @@ cc_library( iterator_adaptors.h ordered_code.cc ordered_code.h + path.cc + path.h range.h secure_random.h status.cc diff --git a/Firestore/core/src/firebase/firestore/util/path.cc b/Firestore/core/src/firebase/firestore/util/path.cc new file mode 100644 index 0000000..940f12a --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/path.cc @@ -0,0 +1,84 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Firestore/core/src/firebase/firestore/util/path.h" + +namespace firebase { +namespace firestore { +namespace util { + +absl::string_view Path::Basename(absl::string_view pathname) { + size_t slash = pathname.find_last_of('/'); + + if (slash == absl::string_view::npos) { + // No path separator found => the whole string. + return pathname; + } + + // Otherwise everything after the slash is the basename (even if empty string) + return pathname.substr(slash + 1); +} + +absl::string_view Path::Dirname(absl::string_view pathname) { + size_t last_slash = pathname.find_last_of('/'); + + if (last_slash == absl::string_view::npos) { + // No path separator found => empty string. Conformance with POSIX would + // have us return "." here. + return pathname.substr(0, 0); + } + + // Collapse runs of slashes. + size_t nonslash = pathname.find_last_not_of('/', last_slash); + if (nonslash == absl::string_view::npos) { + // All characters preceding the last path separator are slashes + return pathname.substr(0, 1); + } + + last_slash = nonslash + 1; + + // Otherwise everything up to the slash is the parent directory + return pathname.substr(0, last_slash); +} + +bool Path::IsAbsolute(absl::string_view path) { +#if defined(_WIN32) +#error "Handle drive letters" + +#else + return !path.empty() && path.front() == '/'; +#endif +} + +void Path::JoinAppend(std::string* base, absl::string_view path) { + if (IsAbsolute(path)) { + base->assign(path.data(), path.size()); + + } else { + size_t nonslash = base->find_last_not_of('/'); + if (nonslash != std::string::npos) { + base->resize(nonslash + 1); + base->push_back('/'); + } + + // If path started with a slash we'd treat it as absolute above + base->append(path.data(), path.size()); + } +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/path.h b/Firestore/core/src/firebase/firestore/util/path.h new file mode 100644 index 0000000..3eda40a --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/path.h @@ -0,0 +1,101 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_PATH_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_PATH_H_ + +#include +#include + +#include "absl/strings/string_view.h" + +namespace firebase { +namespace firestore { +namespace util { + +struct Path { + /** + * Returns the unqualified trailing part of the pathname, e.g. "c" for + * "/a/b/c". + */ + static absl::string_view Basename(absl::string_view pathname); + + /** + * Returns the parent directory name, e.g. "/a/b" for "/a/b/c". + * + * Note: + * * Trailing slashes are treated as a separator between an empty path + * segment and the dirname, so Dirname("/a/b/c/") is "/a/b/c". + * * Runs of more than one slash are treated as a single separator, so + * Dirname("/a/b//c") is "/a/b". + * * Paths are not canonicalized, so Dirname("/a//b//c") is "/a//b". + * * Presently only UNIX style paths are supported (but compilation + * intentionally fails on Windows to prompt implementation there). + */ + static absl::string_view Dirname(absl::string_view pathname); + + /** + * Returns true if the given `pathname` is an absolute path. + */ + static bool IsAbsolute(absl::string_view pathname); + + /** + * Returns the paths separated by path separators. + * + * @param base If base is of type std::string&& the result is moved from this + * value. Otherwise the first argument is copied. + * @param paths The rest of the path segments. + */ + template + static std::string Join(S1&& base, const SA&... paths) { + std::string result{std::forward(base)}; + JoinAppend(&result, paths...); + return result; + } + + /** + * Returns the paths separated by path separators. + */ + static std::string Join() { + return {}; + } + + private: + /** + * Joins the given base path with a suffix. If `path` is relative, appends it + * to the given base path. If `path` is absolute, replaces `base`. + */ + static void JoinAppend(std::string* base, absl::string_view path); + + template + static void JoinAppend(std::string* base, + absl::string_view path, + const S&... rest) { + JoinAppend(base, path); + JoinAppend(base, rest...); + } + + static void JoinAppend(std::string* base) { + // Recursive base case; nothing to do. + (void)base; + } +}; + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_PATH_H_ diff --git a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt index 44a3b61..f540d7c 100644 --- a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt @@ -126,6 +126,7 @@ cc_test( hashing_test.cc iterator_adaptors_test.cc ordered_code_test.cc + path_test.cc status_test.cc status_test_util.h statusor_test.cc diff --git a/Firestore/core/test/firebase/firestore/util/path_test.cc b/Firestore/core/test/firebase/firestore/util/path_test.cc new file mode 100644 index 0000000..be8be1e --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/path_test.cc @@ -0,0 +1,240 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Firestore/core/src/firebase/firestore/util/path.h" + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +// There are several potential sources of inspiration for what is correct +// behavior for these functions. +// +// Python: test with +// +// python -c 'import os.path; print(os.path.basename("a/b/c//"))' +// +// POSIX shell: test with +// +// dirname "a/b/c//" +// +// libc++: std::filesystem does not yet ship with Xcode (as of 9.4). Test with a +// new (non-default installed) llvm, e.g. llvm-6.0: +// +// brew install llvm +// llvm=$(brew --prefix)/opt/llvm +// $llvm/bin/clang++ -I$llvm/include -I$llvm/include/c++/v1 -L$llvm/lib \ +// -Wl,-rpath,$llvm/lib test.cc -lc++experimental && ./a.out +// +// test.cc contains something like: +// #include +// #include +// namespace fs = std::experimental::filesystem; +// int main() { +// std::cout << fs::path("/a/b/c//").parent_path() << std::endl; +// } +// +// cppreference: look up example output in functions declared here: +// https://en.cppreference.com/w/cpp/filesystem/path +// +// This implementation mostly follows python's example: +// +// * It's pretty simple to implement +// * POSIX is more complicated than we need +// * std::filesystem is still too experimental (as of 2018-06-05) + +#define EXPECT_BASENAME_EQ(expected, source) \ + do { \ + EXPECT_EQ(std::string{expected}, Path::Basename(source)); \ + } while (0) + +TEST(Path, Basename_NoSeparator) { + // POSIX would require all of these to be ".". + // python and libc++ agree this is "". + EXPECT_BASENAME_EQ("", ""); + EXPECT_BASENAME_EQ("a", "a"); + EXPECT_BASENAME_EQ("foo", "foo"); + EXPECT_BASENAME_EQ(".", "."); + EXPECT_BASENAME_EQ("..", ".."); +} + +TEST(Path, Basename_LeadingSlash) { + EXPECT_BASENAME_EQ("", "/"); + EXPECT_BASENAME_EQ("", "///"); + EXPECT_BASENAME_EQ("a", "/a"); + EXPECT_BASENAME_EQ("a", "//a"); + + EXPECT_BASENAME_EQ(".", "/."); + EXPECT_BASENAME_EQ("..", "/.."); + EXPECT_BASENAME_EQ("..", "//.."); +} + +TEST(Path, Basename_IntermediateSlash) { + EXPECT_BASENAME_EQ("b", "/a/b"); + EXPECT_BASENAME_EQ("b", "/a//b"); + EXPECT_BASENAME_EQ("b", "//a/b"); + EXPECT_BASENAME_EQ("b", "//a//b"); + + EXPECT_BASENAME_EQ("b", "//..//b"); + EXPECT_BASENAME_EQ("b", "//a/./b"); + EXPECT_BASENAME_EQ("b", "//a/.//b"); +} + +TEST(Path, Basename_TrailingSlash) { + // python: "a/b//" => "" + // POSIX: "a/b//" => "b" + // libc++ path::filename(): "a/b//" => "." (cppreference suggests "") + EXPECT_BASENAME_EQ("", "/a/"); + EXPECT_BASENAME_EQ("", "/a///"); + + EXPECT_BASENAME_EQ("", "/a/b/"); + EXPECT_BASENAME_EQ("", "/a/b//"); + EXPECT_BASENAME_EQ("", "/a//b//"); + EXPECT_BASENAME_EQ("", "//a//b//"); +} + +TEST(Path, Basename_RelativePath) { + EXPECT_BASENAME_EQ("b", "a/b"); + EXPECT_BASENAME_EQ("b", "a//b"); + + EXPECT_BASENAME_EQ("b", "..//b"); + EXPECT_BASENAME_EQ("b", "a/./b"); + EXPECT_BASENAME_EQ("b", "a/.//b"); + EXPECT_BASENAME_EQ("b", "a//.//b"); +} + +#define EXPECT_DIRNAME_EQ(expected, source) \ + do { \ + EXPECT_EQ(std::string{expected}, Path::Dirname(source)); \ + } while (0) + +TEST(Path, Dirname_NoSeparator) { + // POSIX would require all of these to be ".". + // python and libc++ agree this is "". + EXPECT_DIRNAME_EQ("", ""); + EXPECT_DIRNAME_EQ("", "a"); + EXPECT_DIRNAME_EQ("", "foo"); + EXPECT_DIRNAME_EQ("", "."); + EXPECT_DIRNAME_EQ("", ".."); +} + +TEST(Path, Dirname_LeadingSlash) { + // POSIX says all "/". + // python starts with "/" but does not strip trailing slashes. + // libc++ path::parent_path() considers all of these be "", though + // cppreference.com indicates this should be "/" in example output so this is + // likely a bug. + EXPECT_DIRNAME_EQ("/", "/"); + EXPECT_DIRNAME_EQ("/", "///"); + EXPECT_DIRNAME_EQ("/", "/a"); + EXPECT_DIRNAME_EQ("/", "//a"); + + EXPECT_DIRNAME_EQ("/", "/."); + EXPECT_DIRNAME_EQ("/", "/.."); + EXPECT_DIRNAME_EQ("/", "//.."); +} + +TEST(Path, Dirname_IntermediateSlash) { + EXPECT_DIRNAME_EQ("/a", "/a/b"); + EXPECT_DIRNAME_EQ("/a", "/a//b"); + EXPECT_DIRNAME_EQ("//a", "//a/b"); + EXPECT_DIRNAME_EQ("//a", "//a//b"); + + EXPECT_DIRNAME_EQ("//..", "//..//b"); + EXPECT_DIRNAME_EQ("//a/.", "//a/./b"); + EXPECT_DIRNAME_EQ("//a/.", "//a/.//b"); +} + +TEST(Path, Dirname_TrailingSlash) { + // POSIX demands stripping trailing slashes before computing dirname, while + // python and libc++ effectively seem to consider the path to contain an empty + // path segment there. + EXPECT_DIRNAME_EQ("/a", "/a/"); + EXPECT_DIRNAME_EQ("/a", "/a///"); + + EXPECT_DIRNAME_EQ("/a/b", "/a/b/"); + EXPECT_DIRNAME_EQ("/a/b", "/a/b//"); + EXPECT_DIRNAME_EQ("/a//b", "/a//b//"); + EXPECT_DIRNAME_EQ("//a//b", "//a//b//"); +} + +TEST(Path, Dirname_RelativePath) { + EXPECT_DIRNAME_EQ("a", "a/b"); + EXPECT_DIRNAME_EQ("a", "a//b"); + + EXPECT_DIRNAME_EQ("..", "..//b"); + EXPECT_DIRNAME_EQ("a/.", "a/./b"); + EXPECT_DIRNAME_EQ("a/.", "a/.//b"); + EXPECT_DIRNAME_EQ("a//.", "a//.//b"); +} + +TEST(Path, IsAbsolute) { + EXPECT_FALSE(Path::IsAbsolute("")); + EXPECT_TRUE(Path::IsAbsolute("/")); + EXPECT_TRUE(Path::IsAbsolute("//")); + EXPECT_TRUE(Path::IsAbsolute("/foo")); + EXPECT_FALSE(Path::IsAbsolute("foo")); + EXPECT_FALSE(Path::IsAbsolute("foo/bar")); +} + +TEST(Path, Join_Absolute) { + EXPECT_EQ("/", Path::Join("/")); + + EXPECT_EQ("/", Path::Join("", "/")); + EXPECT_EQ("/", Path::Join("a", "/")); + EXPECT_EQ("/b", Path::Join("a", "/b")); + + // Alternate root names should be preserved. + EXPECT_EQ("//", Path::Join("a", "//")); + EXPECT_EQ("//b", Path::Join("a", "//b")); + EXPECT_EQ("///b///", Path::Join("a", "///b///")); + + EXPECT_EQ("/", Path::Join("/", "/")); + EXPECT_EQ("/b", Path::Join("/", "/b")); + EXPECT_EQ("//b", Path::Join("//host/a", "//b")); + EXPECT_EQ("//b", Path::Join("//host/a/", "//b")); + + EXPECT_EQ("/", Path::Join("/", "")); + EXPECT_EQ("/a", Path::Join("/", "a")); + EXPECT_EQ("/a/b/c", Path::Join("/", "a", "b", "c")); + EXPECT_EQ("/a/", Path::Join("/", "a/")); + EXPECT_EQ("/.", Path::Join("/", ".")); + EXPECT_EQ("/..", Path::Join("/", "..")); +} + +TEST(Path, Join_Relative) { + EXPECT_EQ("", Path::Join("")); + + EXPECT_EQ("", Path::Join("", "", "", "")); + EXPECT_EQ("a/b/c", Path::Join("a/b", "c")); + EXPECT_EQ("/c/d", Path::Join("a/b", "/c", "d")); + EXPECT_EQ("/c/d", Path::Join("a/b/", "/c", "d")); +} + +TEST(Path, Join_Types) { + EXPECT_EQ("a/b", Path::Join(absl::string_view{"a"}, "b")); + EXPECT_EQ("a/b", Path::Join(std::string{"a"}, "b")); + + std::string a_string{"a"}; + EXPECT_EQ("a/b", Path::Join(a_string, "b")); + EXPECT_EQ("a", a_string); +} + +} // namespace util +} // namespace firestore +} // namespace firebase -- cgit v1.2.3