blob: 93c1b61f9a573d0e55235fba0bc8b48201b64bb8 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 "util/Files.h"
#include <dirent.h>
#include <sys/stat.h>
#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <string>
#include "android-base/errors.h"
#include "android-base/file.h"
#include "android-base/logging.h"
#include "android-base/unique_fd.h"
#include "android-base/utf8.h"
#include "util/Util.h"
#ifdef _WIN32
// Windows includes.
#include <windows.h>
#endif
using ::android::FileMap;
using ::android::StringPiece;
using ::android::base::ReadFileToString;
using ::android::base::SystemErrorCodeToString;
using ::android::base::unique_fd;
namespace aapt {
namespace file {
#ifdef _WIN32
FileType GetFileType(const std::string& path) {
std::wstring path_utf16;
if (!::android::base::UTF8PathToWindowsLongPath(path.c_str(), &path_utf16)) {
return FileType::kNonExistant;
}
DWORD result = GetFileAttributesW(path_utf16.c_str());
if (result == INVALID_FILE_ATTRIBUTES) {
return FileType::kNonExistant;
}
if (result & FILE_ATTRIBUTE_DIRECTORY) {
return FileType::kDirectory;
}
// Too many types to consider, just let open fail later.
return FileType::kRegular;
}
#else
FileType GetFileType(const std::string& path) {
struct stat sb;
int result = stat(path.c_str(), &sb);
if (result == -1) {
if (errno == ENOENT || errno == ENOTDIR) {
return FileType::kNonExistant;
}
return FileType::kUnknown;
}
if (S_ISREG(sb.st_mode)) {
return FileType::kRegular;
} else if (S_ISDIR(sb.st_mode)) {
return FileType::kDirectory;
} else if (S_ISCHR(sb.st_mode)) {
return FileType::kCharDev;
} else if (S_ISBLK(sb.st_mode)) {
return FileType::kBlockDev;
} else if (S_ISFIFO(sb.st_mode)) {
return FileType::kFifo;
#if defined(S_ISLNK)
} else if (S_ISLNK(sb.st_mode)) {
return FileType::kSymlink;
#endif
#if defined(S_ISSOCK)
} else if (S_ISSOCK(sb.st_mode)) {
return FileType::kSocket;
#endif
} else {
return FileType::kUnknown;
}
}
#endif
bool mkdirs(const std::string& path) {
#ifdef _WIN32
// Start after the long path prefix if present.
bool require_drive = false;
size_t current_pos = 0u;
if (util::StartsWith(path, R"(\\?\)")) {
require_drive = true;
current_pos = 4u;
}
// Start after the drive path if present.
if (path.size() >= 3 && path[current_pos + 1] == ':' &&
(path[current_pos + 2] == '\\' || path[current_pos + 2] == '/')) {
current_pos += 3u;
} else if (require_drive) {
return false;
}
#else
// Start after the first character so that we don't consume the root '/'.
// This is safe to do with unicode because '/' will never match with a continuation character.
size_t current_pos = 1u;
#endif
constexpr const mode_t mode = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP;
while ((current_pos = path.find(sDirSep, current_pos)) != std::string::npos) {
std::string parent_path = path.substr(0, current_pos);
if (parent_path.empty()) {
continue;
}
int result = ::android::base::utf8::mkdir(parent_path.c_str(), mode);
if (result < 0 && errno != EEXIST) {
return false;
}
current_pos += 1;
}
return ::android::base::utf8::mkdir(path.c_str(), mode) == 0 || errno == EEXIST;
}
StringPiece GetStem(StringPiece path) {
const char* start = path.begin();
const char* end = path.end();
for (const char* current = end - 1; current != start - 1; --current) {
if (*current == sDirSep) {
return StringPiece(start, current - start);
}
}
return {};
}
StringPiece GetFilename(StringPiece path) {
const char* end = path.end();
const char* last_dir_sep = path.begin();
for (const char* c = path.begin(); c != end; ++c) {
if (*c == sDirSep || *c == sInvariantDirSep) {
last_dir_sep = c + 1;
}
}
return StringPiece(last_dir_sep, end - last_dir_sep);
}
StringPiece GetExtension(StringPiece path) {
StringPiece filename = GetFilename(path);
const char* const end = filename.end();
const char* c = std::find(filename.begin(), end, '.');
if (c != end) {
return StringPiece(c, end - c);
}
return {};
}
bool IsHidden(android::StringPiece path) {
return util::StartsWith(GetFilename(path), ".");
}
void AppendPath(std::string* base, StringPiece part) {
CHECK(base != nullptr);
const bool base_has_trailing_sep = (!base->empty() && *(base->end() - 1) == sDirSep);
const bool part_has_leading_sep = (!part.empty() && *(part.begin()) == sDirSep);
if (base_has_trailing_sep && part_has_leading_sep) {
// Remove the part's leading sep
part = part.substr(1, part.size() - 1);
} else if (!base_has_trailing_sep && !part_has_leading_sep) {
// None of the pieces has a separator.
*base += sDirSep;
}
base->append(part.data(), part.size());
}
std::string BuildPath(std::vector<const StringPiece>&& args) {
if (args.empty()) {
return "";
}
std::string out{args[0]};
for (int i = 1; i < args.size(); i++) {
file::AppendPath(&out, args[i]);
}
return out;
}
std::string PackageToPath(StringPiece package) {
std::string out_path;
for (StringPiece part : util::Tokenize(package, '.')) {
AppendPath(&out_path, part);
}
return out_path;
}
std::optional<FileMap> MmapPath(const std::string& path, std::string* out_error) {
int flags = O_RDONLY | O_CLOEXEC | O_BINARY;
unique_fd fd(TEMP_FAILURE_RETRY(::android::base::utf8::open(path.c_str(), flags)));
if (fd == -1) {
if (out_error) {
*out_error = SystemErrorCodeToString(errno);
}
return {};
}
struct stat filestats = {};
if (fstat(fd, &filestats) != 0) {
if (out_error) {
*out_error = SystemErrorCodeToString(errno);
}
return {};
}
FileMap filemap;
if (filestats.st_size == 0) {
// mmap doesn't like a length of 0. Instead we return an empty FileMap.
return std::move(filemap);
}
if (!filemap.create(path.c_str(), fd, 0, filestats.st_size, true)) {
if (out_error) {
*out_error = SystemErrorCodeToString(errno);
}
return {};
}
return std::move(filemap);
}
bool AppendArgsFromFile(StringPiece path, std::vector<std::string>* out_arglist,
std::string* out_error) {
std::string contents;
if (!ReadFileToString(std::string(path), &contents, true /*follow_symlinks*/)) {
if (out_error) {
*out_error = "failed to read argument-list file";
}
return false;
}
for (StringPiece line : util::Tokenize(contents, ' ')) {
line = util::TrimWhitespace(line);
if (!line.empty()) {
out_arglist->emplace_back(line);
}
}
return true;
}
bool AppendSetArgsFromFile(StringPiece path, std::unordered_set<std::string>* out_argset,
std::string* out_error) {
std::string contents;
if (!ReadFileToString(std::string(path), &contents, true /*follow_symlinks*/)) {
if (out_error) {
*out_error = "failed to read argument-list file";
}
return false;
}
for (StringPiece line : util::Tokenize(contents, ' ')) {
line = util::TrimWhitespace(line);
if (!line.empty()) {
out_argset->emplace(line);
}
}
return true;
}
bool FileFilter::SetPattern(StringPiece pattern) {
pattern_tokens_ = util::SplitAndLowercase(pattern, ':');
return true;
}
bool FileFilter::operator()(const std::string& filename, FileType type) const {
if (filename == "." || filename == "..") {
return false;
}
const char kDir[] = "dir";
const char kFile[] = "file";
const size_t filename_len = filename.length();
bool chatty = true;
for (const std::string& token : pattern_tokens_) {
const char* token_str = token.c_str();
if (*token_str == '!') {
chatty = false;
token_str++;
}
if (strncasecmp(token_str, kDir, sizeof(kDir)) == 0) {
if (type != FileType::kDirectory) {
continue;
}
token_str += sizeof(kDir);
}
if (strncasecmp(token_str, kFile, sizeof(kFile)) == 0) {
if (type != FileType::kRegular) {
continue;
}
token_str += sizeof(kFile);
}
bool ignore = false;
size_t n = strlen(token_str);
if (*token_str == '*') {
// Math suffix.
token_str++;
n--;
if (n <= filename_len) {
ignore =
strncasecmp(token_str, filename.c_str() + filename_len - n, n) == 0;
}
} else if (n > 1 && token_str[n - 1] == '*') {
// Match prefix.
ignore = strncasecmp(token_str, filename.c_str(), n - 1) == 0;
} else {
ignore = strcasecmp(token_str, filename.c_str()) == 0;
}
if (ignore) {
if (chatty) {
diag_->Warn(android::DiagMessage()
<< "skipping " << (type == FileType::kDirectory ? "dir '" : "file '")
<< filename << "' due to ignore pattern '" << token << "'");
}
return false;
}
}
return true;
}
std::optional<std::vector<std::string>> FindFiles(android::StringPiece path,
android::IDiagnostics* diag,
const FileFilter* filter) {
const auto& root_dir = path;
std::unique_ptr<DIR, decltype(closedir)*> d(opendir(root_dir.data()), closedir);
if (!d) {
diag->Error(android::DiagMessage() << SystemErrorCodeToString(errno) << ": " << root_dir);
return {};
}
std::vector<std::string> files;
std::vector<std::string> subdirs;
while (struct dirent* entry = readdir(d.get())) {
if (util::StartsWith(entry->d_name, ".")) {
continue;
}
std::string file_name = entry->d_name;
std::string full_path{root_dir};
AppendPath(&full_path, file_name);
const FileType file_type = GetFileType(full_path);
if (filter != nullptr) {
if (!(*filter)(file_name, file_type)) {
continue;
}
}
if (file_type == file::FileType::kDirectory) {
subdirs.push_back(std::move(file_name));
} else {
files.push_back(std::move(file_name));
}
}
// Now process subdirs.
for (const std::string& subdir : subdirs) {
std::string full_subdir{root_dir};
AppendPath(&full_subdir, subdir);
std::optional<std::vector<std::string>> subfiles = FindFiles(full_subdir, diag, filter);
if (!subfiles) {
return {};
}
for (const std::string& subfile : subfiles.value()) {
std::string new_file = subdir;
AppendPath(&new_file, subfile);
files.push_back(new_file);
}
}
return files;
}
} // namespace file
} // namespace aapt