Commit 76551bcd authored by David 'Digit' Turner's avatar David 'Digit' Turner Committed by Commit Bot

android: crazy_linker: Allow searching libraries inside zip archives.

This modifies the internal crazy::SearchPathList class to support
looking up libraries inside zip archives, in two ways:

- First, by supporting paths with an exclamation mark, used
  as a delimiter between zip archive paths, and file sub-paths
  within it. For example, looking for:

    /path/to/archive.zip!libs/libfoo.so

  Will search the zip archive at /path/to/archive.zip for a
  file named libs/libfoo.so.

  Note that path items added through AddPaths() can also
  contain an exclamation mark.

- Second, automatically support zip files stored with a
  'crazy.' prefix (e.g. /path/to/archive.zip!libs/libfoo.so
  will match a file named 'libs/crazy.libfoo.so' in the
  archive too.

  This is useful to store uncompressed libraries inside
  Android APKs while preventing the system from extracting
  them when the application is installed or updated.

Note that this does not modify the rest of the crazy linker to
use this for now (e.g. to implement LoadLibraryFromZipFile()),
since this will require more work (to be provided in a future CL).


BUG=802068
R=agrieve@chromium.org,pasko@chromium.org,lizeb@chromium.org

Change-Id: Ic8661332e4e4537a472e325b74c8c001ea2bc113
Reviewed-on: https://chromium-review.googlesource.com/1042394
Commit-Queue: David Turner <digit@chromium.org>
Reviewed-by: default avataragrieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#557131}
parent b7a0746e
...@@ -370,34 +370,17 @@ LibraryView* LibraryList::LoadLibrary(const char* lib_name, ...@@ -370,34 +370,17 @@ LibraryView* LibraryList::LoadLibrary(const char* lib_name,
// Find the full library path. // Find the full library path.
String full_path; String full_path;
if (!strchr(lib_name, '/')) { LOG("Looking through the search path list");
LOG("Looking through the search path list"); SearchPathList::Result probe = search_path_list->FindFile(lib_name);
const char* path = search_path_list->FindFile(lib_name); if (!probe.IsValid()) {
if (!path) { error->Format("Can't find library file %s", lib_name);
error->Format("Can't find library file %s", lib_name); return NULL;
return NULL;
}
full_path = path;
} else {
if (lib_name[0] != '/') {
// Need to transform this into a full path.
full_path = GetCurrentDirectory();
if (full_path.size() && full_path[full_path.size() - 1] != '/')
full_path += '/';
full_path += lib_name;
} else {
// Absolute path. Easy.
full_path = lib_name;
}
LOG("Full library path: %s", full_path.c_str());
if (!PathIsFile(full_path.c_str())) {
error->Format("Library file doesn't exist: %s", full_path.c_str());
return NULL;
}
} }
LOG("Found library: path %s @ 0x%x", probe.path.c_str(), probe.offset);
// Load the library // Load the library
if (!lib->Load(full_path.c_str(), load_address, file_offset, error)) if (!lib->Load(probe.path.c_str(), load_address, file_offset + probe.offset,
error))
return NULL; return NULL;
// Load all dependendent libraries. // Load all dependendent libraries.
......
...@@ -4,17 +4,144 @@ ...@@ -4,17 +4,144 @@
#include "crazy_linker_search_path_list.h" #include "crazy_linker_search_path_list.h"
#include <string.h> #include "crazy_linker.h"
#include "crazy_linker_debug.h" #include "crazy_linker_debug.h"
#include "crazy_linker_system.h" #include "crazy_linker_system.h"
#include "crazy_linker_zip.h"
#include <utility>
#include <string.h>
namespace crazy { namespace crazy {
namespace {
// Helper class used to parse over the items of two column-separated lists.
// Usage is the following:
// 1) Create new instance, passing the first and second list as parameters.
// 2) Call NextItem() in a loop until it returns false. Each call will
// return the current item.
//
// Items of the first list are returned in order before items of the second
// list.
class MultiListParser {
public:
// Constructor.
MultiListParser(const String& a_list, const String& b_list)
: p_(a_list.cbegin()), end_(a_list.cend()), b_list_(b_list) {}
// Grab next item. On success return true and sets |*result|. On end of list,
// just return false.
bool NextItem(String* result) {
for (;;) {
if (p_ == end_) {
if (p_ != b_list_.cend()) {
p_ = b_list_.cbegin();
end_ = b_list_.cend();
continue;
}
return false;
}
// compute current list item, and next item start at the same time.
const char* item = p_;
const char* item_end = item;
while (item_end < end_ && item_end[0] != ':')
item_end++;
p_ = item_end + (item_end < end_);
if (item_end > item) { // Skip over empty entries.
result->Assign(item, item_end - item);
return true;
}
}
}
private:
const char* p_;
const char* end_;
const String& b_list_;
};
// Look into zip archive at |zip_path| for a file named |file_name|.
// As a special convenience trick, this will also try to find a file with
// the same name with a 'crazy.' prefix. In other words, when looking for
// 'lib/libfoo.so', this will first look for 'lib/libfoo.so', then for
// 'lib/crazy.libfoo.so'. This allows storing uncompressed libraries inside
// Android APKs while preventing the system from extracting them at installation
// time (which will always happen before Android M). Note that these files
// should just be renamed, i.e. their internal soname should still be
// 'libfoo.so'.
//
// On success, return offset within zip archive, or CRAZY_OFFSET_FAILED
// if the library is not found.
int32_t FindLibFileInZipArchive(const char* zip_path, const char* file_name) {
// First attempt is direct lookup.
int32_t offset = FindStartOffsetOfFileInZipFile(zip_path, file_name);
if (offset != CRAZY_OFFSET_FAILED) {
LOG(" FOUND_IN_ZIP %s!%s @ 0x%x", zip_path, file_name, offset);
return offset;
}
// Second attempt adding a crazy. prefix to the library name.
String crazy_name;
const char* pos = ::strrchr(file_name, '/');
if (pos) {
crazy_name.Assign(file_name, (pos + 1 - file_name));
file_name = pos + 1;
}
crazy_name.Append("crazy.");
crazy_name.Append(file_name);
offset = FindStartOffsetOfFileInZipFile(zip_path, crazy_name.c_str());
if (offset != CRAZY_OFFSET_FAILED) {
LOG(" FOUND IN ZIP %s!%s @ 0x%x", zip_path, crazy_name.c_str(), offset);
}
return offset;
}
// Try to find the library file pointed by |path|.
// If |path| contains an exclamation mark, this is interpreted as a separator
// between a zip archive file path, and a file contained inside it.
// Also supports crazy. prefix for storing renamed libraries inside the zip
// archive (see comment for FindLibFileInZipArchive).
SearchPathList::Result FindLibFile(const char* path) {
// An exclamation mark in the file name indicates that one should look
// inside a zip archive. This is supported by the platform, see the
// "Opening shared libraries directly from an APK" in the following article:
// https://github.com/aosp-mirror/platform_bionic/blob/master/android-changes-for-ndk-developers.md
const char* bang = ::strchr(path, '!');
if (bang) {
if (bang == path || bang[1] == '\0') {
// An initial or final '!' is always an error.
LOG(" INVALID_ZIP_PATH %s", path);
} else {
String zip_path = MakeAbsolutePathFrom(path, bang - path);
const char* file_name = bang + 1;
int32_t offset = FindLibFileInZipArchive(zip_path.c_str(), bang + 1);
if (offset != CRAZY_OFFSET_FAILED) {
return {std::move(zip_path), offset};
}
}
} else {
// Regular file path.
String file_path = MakeAbsolutePathFrom(path);
if (PathIsFile(file_path.c_str())) {
LOG(" FOUND FILE %s", file_path.c_str());
return {std::move(file_path), 0};
}
}
LOG(" skip %s", path);
return {};
}
} // namespace
void SearchPathList::Reset() { void SearchPathList::Reset() {
list_.Resize(0); list_.Resize(0);
env_list_.Resize(0); env_list_.Resize(0);
full_path_.Resize(0);
} }
void SearchPathList::ResetFromEnv(const char* var_name) { void SearchPathList::ResetFromEnv(const char* var_name) {
...@@ -31,53 +158,35 @@ void SearchPathList::AddPaths(const char* list, const char* list_end) { ...@@ -31,53 +158,35 @@ void SearchPathList::AddPaths(const char* list, const char* list_end) {
list_.Append(list, list_end - list); list_.Append(list, list_end - list);
} }
const char* SearchPathList::FindFile(const char* file_name) { SearchPathList::Result SearchPathList::FindFile(const char* file_name) const {
// Sanity checks.
if (!file_name || !*file_name)
return NULL;
LOG("Looking for %s", file_name); LOG("Looking for %s", file_name);
// Build full list by appending the env_list_ after the regular one. if (::strchr(file_name, '/') != nullptr ||
String full_list = list_; ::strchr(file_name, '!') != nullptr) {
if (env_list_.size() > 0) { // This is an absolute or relative file path, so ignore the search list.
if (full_list.size() > 0 && full_list[full_list.size() - 1] != ':') return FindLibFile(file_name);
full_list += ':';
full_list += env_list_;
} }
// Iterate over all items in the list. // Build full list by appending the env_list_ after the regular one.
const char* p = full_list.c_str(); MultiListParser parser(list_, env_list_);
const char* end = p + full_list.size(); String file_path;
Result result;
while (p < end) { while (parser.NextItem(&file_path)) {
// compute current list item, and next item start at the same time.
const char* item = p;
const char* item_end =
reinterpret_cast<const char*>(memchr(p, ':', end - p));
if (item_end)
p = item_end + 1;
else {
item_end = end;
p = end;
}
full_path_.Assign(item, item_end - item);
// Add trailing directory separator if needed. // Add trailing directory separator if needed.
if (full_path_.size() > 0 && full_path_[full_path_.size() - 1] != '/') if (file_path[file_path.size() - 1] != '/')
full_path_ += '/'; file_path += '/';
full_path_ += file_name; file_path += file_name;
if (PathIsFile(full_path_.c_str())) { result = FindLibFile(file_path.c_str());
LOG(" FOUND %s", full_path_.c_str()); if (result.IsValid()) {
return full_path_.c_str(); return result;
} else }
LOG(" skip %s", full_path_.c_str()); LOG(" SKIPPED %s", file_path.c_str());
} }
return NULL; LOG(" MISSING %s", file_name);
return result;
} }
} // namespace crazy } // namespace crazy
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#ifndef CRAZY_LINKER_SEARCH_PATH_LIST_H #ifndef CRAZY_LINKER_SEARCH_PATH_LIST_H
#define CRAZY_LINKER_SEARCH_PATH_LIST_H #define CRAZY_LINKER_SEARCH_PATH_LIST_H
#include <stdint.h>
#include <string.h> #include <string.h>
#include "crazy_linker_util.h" // for String #include "crazy_linker_util.h" // for String
...@@ -15,7 +16,7 @@ namespace crazy { ...@@ -15,7 +16,7 @@ namespace crazy {
// file system probing with it. // file system probing with it.
class SearchPathList { class SearchPathList {
public: public:
SearchPathList() : list_(), env_list_(), full_path_() {} SearchPathList() = default;
// Reset the list, i.e. make it empty. // Reset the list, i.e. make it empty.
void Reset(); void Reset();
...@@ -26,6 +27,13 @@ class SearchPathList { ...@@ -26,6 +27,13 @@ class SearchPathList {
// Add one or more paths to the list. // Add one or more paths to the list.
// |path_list| contains a list of paths separated by columns. // |path_list| contains a list of paths separated by columns.
// |path_list_end| points after the list's last character. // |path_list_end| points after the list's last character.
//
// NOTE: Adding a zip archive to the list is supported by using an
// exclamation mark as a delimiter inside a path. For example, the value
// '/path/to/archive.zip!lib/armeabi-v7a', means looking for libraries
// inside the zip archive at '/path/to/archive.zip', that are stored
// as 'lib/armeabi-v7a/<libname>', or even 'lib/armeabi-v7a/crazy.<libname>'.
// Read the documentation for FindFile() below for more details.
void AddPaths(const char* path_list, const char* path_list_end); void AddPaths(const char* path_list, const char* path_list_end);
// Convenience function that takes a 0-terminated string. // Convenience function that takes a 0-terminated string.
...@@ -33,17 +41,62 @@ class SearchPathList { ...@@ -33,17 +41,62 @@ class SearchPathList {
AddPaths(path_list, path_list + ::strlen(path_list)); AddPaths(path_list, path_list + ::strlen(path_list));
} }
// Try to find a file named |file_name| by probing the file system // The result of FindFile() below.
// with every item in the list as a suffix. On success, returns the // |path| is the path to the file containing the library, or nullptr on error.
// full path string, or NULL on failure. // |offset| is the byte offset within |path| where the library is located.
const char* FindFile(const char* file_name); struct Result {
String path;
int32_t offset = 0;
// Returns true iff this instance matches a valid file and offset.
inline bool IsValid() const { return !path.IsEmpty(); }
};
// Try to find a library file named |file_name| by probing the directories
// added through AddPaths(). This returns a (path, offset) tuple where
// |path| corresponds to the file path to load the library from, and
// |offset| to its offset inside the file. This allows loading libraries
// directly from zip archives, when they are uncompressed and page-aligned
// within them.
//
// In case of failure, the path will be empty, and the offset will be 0.
//
// Note that if |file_name| does not contain any directory separator or
// exclamation name, the corresponding file will be searched in the list
// of paths added through AddPaths(). Otherwise, this is considered a direct
// file path and the search list will be ignored.
//
// File paths, either given directly by |file_name| or created by prepending
// search list items to it, can contain an exclamation mark to indicate that
// the library should be looked into a zip archive. For a concrete example
// 'path/to/archive.zip!libs/libfoo.so' will look into the zip archive
// at 'path/to/archive.zip' for a file within it named 'libs/libfoo.so'.
//
// This matches the behaviour of the Android system linker, starting with M
// (i.e. API level 23), but can be used on previous Android releases too.
//
// Said libraries must be uncompressed and page-aligned for the linker
// to later be able to load them properly.
//
// NOTE: It is also possible to store library files with a 'crazy.' prefix
// inside zip archives. In the following example, the function will first
// look for 'libs/libfoo.so', and if not found, will also look for a file
// named 'libs/crazy.libfoo.so'.
//
// Using such a prefix is useful on Android: such libraries can still be
// loaded directly from the APK, but will not be extracted by the system
// at installation into the application data directory (at least before
// Android M). Note that said libraries should simply be renamed within
// the zip file (i.e. they should still use the same internal DT_SONAME
// of 'libfoo.so' for the linker to work properly).
//
Result FindFile(const char* file_name) const;
private: private:
String list_; String list_;
String env_list_; String env_list_;
String full_path_;
}; };
} // namespace crazy } // namespace crazy
#endif // CRAZY_LINKER_SEARCH_PATH_LIST_H #endif // CRAZY_LINKER_SEARCH_PATH_LIST_H
\ No newline at end of file
...@@ -6,9 +6,18 @@ ...@@ -6,9 +6,18 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include "crazy_linker_system_mock.h" #include "crazy_linker_system_mock.h"
#include "crazy_linker_zip_test_data.h"
namespace crazy { namespace crazy {
namespace {
// MAGIC CONSTANT WARNING: These offsets have been determined empirically.
// If you update the content of lib_archive_zip, you will have to adjust
// them for these tests to continue to run.
const int32_t kFooFileOffset = 80;
const int32_t kBarFileOffset = 197;
class TestSystem { class TestSystem {
public: public:
TestSystem() : sys_() { TestSystem() : sys_() {
...@@ -24,54 +33,124 @@ class TestSystem { ...@@ -24,54 +33,124 @@ class TestSystem {
sys_.AddRegularFile(path, data, len); sys_.AddRegularFile(path, data, len);
} }
void SetCurrentDir(const char* path) { sys_.SetCurrentDir(path); }
private: private:
SystemMock sys_; SystemMock sys_;
}; };
// A mock CPU ABI name used during testing.
// NOTE: This is hard-coded into the zip test tables, do not change it!
const char kTestAbi[] = "test-abi";
// Small structure used to store test data.
// |input_path| is an input file path.
// |expected_path| is either nullptr or an expected file path.
// |expected_offset| is the expected offset (or 0).
struct TestData {
const char* input_path;
const char* expected_path;
int32_t expected_offset;
};
// Perform one single check for the TestData instance |data|, using |list|.
void CheckData(const TestData& data, const SearchPathList& list) {
auto result = list.FindFile(data.input_path);
if (data.expected_path) {
EXPECT_STREQ(data.expected_path, result.path.c_str())
<< "For: " << data.input_path;
EXPECT_EQ(data.expected_offset, result.offset)
<< "For: " << data.input_path;
} else {
EXPECT_STREQ("", result.path.c_str()) << "For: " << data.input_path;
}
}
} // namespace
TEST(SearchPathList, Empty) { TEST(SearchPathList, Empty) {
TestSystem sys; TestSystem sys;
SearchPathList list; SearchPathList list;
EXPECT_FALSE(list.FindFile("/foo")); sys.SetCurrentDir("/tmp");
EXPECT_FALSE(list.FindFile("/tmp/zoo")); static const TestData kData[] = {
EXPECT_FALSE(list.FindFile("/tmp/foo/bar")); // Paths without a directory separator should not work.
{"foo", nullptr, 0},
// Relative paths should work.
{"foo/bar", "/tmp/foo/bar", 0},
// Absolute paths should work
{"/foo", "/foo", 0},
{"/tmp/zoo", "/tmp/zoo", 0},
{"/tmp/foo/bar", "/tmp/foo/bar", 0},
// File that do not exist should error.
{"/no-such-file", nullptr, 0},
};
for (const auto& data : kData) {
CheckData(data, list);
}
} }
TEST(SearchPathList, OneItem) { TEST(SearchPathList, OneItem) {
TestSystem sys; TestSystem sys;
SearchPathList list; SearchPathList list;
list.AddPaths("/tmp/foo"); list.AddPaths("/tmp/foo");
EXPECT_STREQ("/tmp/foo/bar", list.FindFile("bar"));
EXPECT_FALSE(list.FindFile("zoo")); static const TestData kData[] = {
EXPECT_FALSE(list.FindFile("foo")); {"bar", "/tmp/foo/bar", 0}, {"zoo", nullptr, 0}, {"foo", nullptr, 0},
};
for (const auto& data : kData) {
CheckData(data, list);
}
} }
TEST(SearchPathList, Reset) { TEST(SearchPathList, Reset) {
TestSystem sys; TestSystem sys;
SearchPathList list; SearchPathList list;
list.AddPaths("/tmp/foo"); list.AddPaths("/tmp/foo");
EXPECT_STREQ("/tmp/foo/bar", list.FindFile("bar"));
auto result = list.FindFile("bar");
EXPECT_STREQ("/tmp/foo/bar", result.path.c_str());
list.Reset(); list.Reset();
EXPECT_FALSE(list.FindFile("bar")); result = list.FindFile("bar");
EXPECT_STREQ("", result.path.c_str());
} }
TEST(SearchPathList, ResetFromEnv) { TEST(SearchPathList, ResetFromEnv) {
TestSystem sys; TestSystem sys;
SearchPathList list; SearchPathList list;
list.ResetFromEnv("TEST_LIBRARY_PATH"); list.ResetFromEnv("TEST_LIBRARY_PATH");
EXPECT_STREQ("/tmp/foo/bar", list.FindFile("foo/bar"));
EXPECT_STREQ("/foo", list.FindFile("foo")); static const TestData kData[] = {
// Find file name from env search list.
{"zoo", "/tmp/zoo", 0},
{"foo", "/foo", 0},
// Ignore search list if path contains a directory separator.
{"foo/bar", nullptr, 0},
// Or an exclamation mark.
{"foo!bar", nullptr, 0},
};
for (const auto& data : kData) {
CheckData(data, list);
}
} }
TEST(SearchPathList, ThreeItems) { TEST(SearchPathList, ThreeItems) {
TestSystem sys; TestSystem sys;
SearchPathList list; SearchPathList list;
list.AddPaths("/tmp/foo"); list.AddPaths("/tmp/foo:/tmp/");
list.AddPaths("/tmp/");
static const TestData kData[] = {
EXPECT_STREQ("/tmp/foo/bar", list.FindFile("bar")); // Relative path ignores search list. Current directory is /.
EXPECT_STREQ("/tmp/zoo", list.FindFile("zoo")); {"foo/bar", nullptr, 0},
EXPECT_FALSE(list.FindFile("foo")); {"tmp/zoo", "/tmp/zoo", 0},
// Base name uses search list, finds file in /tmp/.
{"zoo", "/tmp/zoo", 0},
// Base name uses search list, doesn't find file in / which is no listed.
{"foo", nullptr, 0},
};
for (const auto& data : kData) {
CheckData(data, list);
}
} }
TEST(SearchPathList, EnvPathsAfterAddedOnes) { TEST(SearchPathList, EnvPathsAfterAddedOnes) {
...@@ -84,7 +163,74 @@ TEST(SearchPathList, EnvPathsAfterAddedOnes) { ...@@ -84,7 +163,74 @@ TEST(SearchPathList, EnvPathsAfterAddedOnes) {
// This checks that paths added with AddPaths() have priority over // This checks that paths added with AddPaths() have priority over
// paths added with ResetFromEnv(). An invalid implementation would // paths added with ResetFromEnv(). An invalid implementation would
// find '/tmp/foo' instead. // find '/tmp/foo' instead.
EXPECT_STREQ("/opt/foo", list.FindFile("foo")); static const TestData data = {"foo", "/opt/foo", 0};
CheckData(data, list);
}
TEST(SearchPathList, FindDirectlyInsizeZipArchive) {
TestSystem sys;
sys.AddFile("/zips/archive.zip",
reinterpret_cast<const char*>(testing::lib_archive_zip),
testing::lib_archive_zip_len);
sys.SetCurrentDir("/zips");
// Empty search path list.
SearchPathList list;
static const TestData kData[] = {
// Lookup directly in archive. Full path.
{"/zips/archive.zip!lib/test-abi/libfoo.so", "/zips/archive.zip",
kFooFileOffset},
{"/zips/archive.zip!lib/test-abi/crazy.libbar.so", "/zips/archive.zip",
kBarFileOffset},
// Lookup directly in archive, from current directory.
{"archive.zip!lib/test-abi/libfoo.so", "/zips/archive.zip",
kFooFileOffset},
// Cannot find libraries if the zip archive is not in the search list.
{"libfoo.so", nullptr, 0},
{"libbar.so", nullptr, 0},
};
for (const auto& data : kData) {
CheckData(data, list);
}
}
TEST(SearchPathList, FindInsideListedZipArchive) {
TestSystem sys;
sys.AddFile("/zips/archive.zip",
reinterpret_cast<const char*>(testing::lib_archive_zip),
testing::lib_archive_zip_len);
SearchPathList list;
list.AddPaths("/zips/archive.zip!lib/test-abi/");
// MAGIC CONSTANT WARNING: These offsets have been determined empirically.
// If you update the content of lib_archive_zip, you will have to adjust
// them for these tests to continue to run.
static const int32_t kFooFileOffset = 80;
static const int32_t kBarFileOffset = 197;
static const TestData kData[] = {
// Lookup directly in archive. Full path.
{"/zips/archive.zip!lib/test-abi/libfoo.so", "/zips/archive.zip",
kFooFileOffset},
{"/zips/archive.zip!lib/test-abi/crazy.libbar.so", "/zips/archive.zip",
kBarFileOffset},
// Same, but automatically handle crazy. storage prefix!
{"/zips/archive.zip!lib/test-abi/libbar.so", "/zips/archive.zip",
kBarFileOffset},
// Lookup in archive because it is in the search path.
{"libfoo.so", "/zips/archive.zip", kFooFileOffset},
// Same, but automatically handle crazy. storage prefix!
{"libbar.so", "/zips/archive.zip", kBarFileOffset},
};
for (const auto& data : kData) {
CheckData(data, list);
}
} }
} // namespace crazy } // namespace crazy
\ No newline at end of file
...@@ -18,6 +18,41 @@ ...@@ -18,6 +18,41 @@
// Note: unit-testing support files are in crazy_linker_files_mock.cpp // Note: unit-testing support files are in crazy_linker_files_mock.cpp
namespace crazy {
String MakeDirectoryPath(const char* parent) {
return MakeDirectoryPath(parent, ::strlen(parent));
}
String MakeDirectoryPath(const char* parent, size_t parent_len) {
if (parent_len == 0) {
// Special case for empty inputs.
return String("./");
}
String result(parent);
if (parent_len > 0 && parent[parent_len - 1] != '/') {
result += '/';
}
return result;
}
String MakeAbsolutePathFrom(const char* path) {
return MakeAbsolutePathFrom(path, ::strlen(path));
}
String MakeAbsolutePathFrom(const char* path, size_t path_len) {
if (path[0] == '/') {
return String(path, path_len);
} else {
String cur_dir = GetCurrentDirectory();
String result = MakeDirectoryPath(cur_dir.c_str(), cur_dir.size());
result.Append(path, path_len);
return result;
}
}
} // namespace crazy
#ifndef UNIT_TESTS #ifndef UNIT_TESTS
namespace crazy { namespace crazy {
......
...@@ -133,6 +133,19 @@ bool PathIsFile(const char* path_name); ...@@ -133,6 +133,19 @@ bool PathIsFile(const char* path_name);
// Returns the current directory, as a string. // Returns the current directory, as a string.
String GetCurrentDirectory(); String GetCurrentDirectory();
// Convert |path| into a String, and appends a trailing directory separator
// if there isn't already one. NOTE: As a special case, if the input is empty,
// then "./" will be returned.
String MakeDirectoryPath(const char* path);
String MakeDirectoryPath(const char* path, size_t path_len);
// Convert |path| into an absolute path if necessary, always returns a new
// String instance as well.
String MakeAbsolutePathFrom(const char* path);
// Same, but for the [path..path + path_len) input string.
String MakeAbsolutePathFrom(const char* path, size_t path_len);
// Returns the value of a given environment variable. // Returns the value of a given environment variable.
const char* GetEnv(const char* var_name); const char* GetEnv(const char* var_name);
......
...@@ -27,6 +27,64 @@ TEST(System, SingleFile) { ...@@ -27,6 +27,64 @@ TEST(System, SingleFile) {
EXPECT_STREQ(kString, buff2); EXPECT_STREQ(kString, buff2);
} }
TEST(System, MakeDirectoryPath) {
static const struct {
const char* input;
const char* expected;
} kData[] = {
{"", "./"}, {".", "./"}, {"..", "../"},
{"./", "./"}, {"../", "../"}, {"foo", "foo/"},
{"foo/", "foo/"}, {"/foo", "/foo/"}, {"foo/bar", "foo/bar/"},
};
for (const auto& data : kData) {
EXPECT_STREQ(data.expected, MakeDirectoryPath(data.input).c_str())
<< "For [" << data.input << "]";
}
}
TEST(System, MakeAbsolutePathFrom) {
SystemMock sys;
static const struct {
const char* input;
const char* expected;
} kData[] = {
{"/foo", "/foo"},
{"/foo/bar/", "/foo/bar/"},
{"foo", "/home/foo"},
{"foo/bar", "/home/foo/bar"},
{"./foo", "/home/./foo"},
{"../foo", "/home/../foo"},
{"../../foo", "/home/../../foo"},
};
sys.SetCurrentDir("/home");
for (const auto& data : kData) {
EXPECT_STREQ(data.expected, MakeAbsolutePathFrom(data.input).c_str())
<< "For [" << data.input << "]";
}
for (const auto& data : kData) {
EXPECT_STREQ(data.expected,
MakeAbsolutePathFrom(data.input, strlen(data.input)).c_str())
<< "For [" << data.input << "]";
}
sys.SetCurrentDir("/home/");
for (const auto& data : kData) {
EXPECT_STREQ(data.expected, MakeAbsolutePathFrom(data.input).c_str())
<< "For [" << data.input << "]";
}
for (const auto& data : kData) {
EXPECT_STREQ(data.expected,
MakeAbsolutePathFrom(data.input, strlen(data.input)).c_str())
<< "For [" << data.input << "]";
}
}
TEST(System, PathExists) { TEST(System, PathExists) {
SystemMock sys; SystemMock sys;
sys.AddRegularFile("/tmp/foo", "FOO", 3); sys.AddRegularFile("/tmp/foo", "FOO", 3);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment