blob: ddc4d2e07d54363b08a6eade6823377ad5a4d427 [file] [log] [blame]
// Copyright 2017-2020 The Verible Authors.
//
// 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 "common/util/file_util.h"
#include <algorithm>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <streambuf>
#include <string>
#include <system_error>
#include <utility>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "common/util/logging.h"
#ifndef _WIN32
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#endif
namespace fs = std::filesystem;
namespace verible {
namespace file {
absl::string_view Basename(absl::string_view filename) {
auto last_slash_pos = filename.find_last_of("/\\");
return last_slash_pos == absl::string_view::npos
? filename
: filename.substr(last_slash_pos + 1);
}
absl::string_view Dirname(absl::string_view filename) {
auto last_slash_pos = filename.find_last_of("/\\");
return last_slash_pos == absl::string_view::npos
? filename
: filename.substr(0, last_slash_pos);
}
absl::string_view Stem(absl::string_view filename) {
auto last_dot_pos = filename.find_last_of('.');
return last_dot_pos == absl::string_view::npos
? filename
: filename.substr(0, last_dot_pos);
}
// Create an error message derived from "sys_error" (which contains an errno
// number). The message includes the filename as prefix.
// If "sys_error" can not be resolved, creates an UNKNOWN message.
// The "filename" and "fallback_msg" will be copied, no need for them to
// stay alive after the call.
static absl::Status CreateErrorStatusFromSysError(absl::string_view filename,
int sys_error,
const char *fallback_msg) {
const char *const system_msg =
sys_error == 0 ? fallback_msg : strerror(sys_error);
const std::string msg = filename.empty()
? std::string{system_msg}
: absl::StrCat(filename, ": ", system_msg);
switch (sys_error) {
case EPERM:
case EACCES:
return {absl::StatusCode::kPermissionDenied, msg};
case ENOENT:
return {absl::StatusCode::kNotFound, msg};
case EEXIST:
return {absl::StatusCode::kAlreadyExists, msg};
case EINVAL:
case EISDIR:
return {absl::StatusCode::kInvalidArgument, msg};
default:
return {absl::StatusCode::kUnknown, msg};
}
}
static absl::Status CreateErrorStatusFromErrno(absl::string_view filename,
const char *fallback_msg) {
return CreateErrorStatusFromSysError(filename, errno, fallback_msg);
}
static absl::Status CreateErrorStatusFromErr(absl::string_view filename,
const std::error_code &err,
const char *fallback_msg) {
// TODO: this assumes that err.value() returns errno-like values. Might not
// always be the case.
return CreateErrorStatusFromSysError(filename, err.value(), fallback_msg);
}
absl::Status UpwardFileSearch(absl::string_view start,
absl::string_view filename, std::string *result) {
std::error_code err;
const std::string search_file(filename);
fs::path absolute_path = fs::absolute(std::string(start), err);
if (err.value() != 0) {
return CreateErrorStatusFromErr(filename, err,
"invalid config path specified.");
}
fs::path probe_dir = absolute_path;
for (;;) {
*result = (probe_dir / search_file).string();
if (FileExists(*result).ok()) return absl::OkStatus();
fs::path one_up = probe_dir.parent_path();
if (one_up == probe_dir) break;
probe_dir = one_up;
}
return absl::NotFoundError("No matching file found.");
}
absl::Status FileExists(const std::string &filename) {
std::error_code err;
fs::file_status stat = fs::status(filename, err);
if (err.value() != 0) {
return absl::NotFoundError(absl::StrCat(filename, ": ", err.message()));
}
if (fs::is_regular_file(stat) || fs::is_fifo(stat)) {
return absl::OkStatus();
}
if (fs::is_directory(stat)) {
return absl::InvalidArgumentError(
absl::StrCat(filename, ": is a directory, not a file"));
}
return absl::InvalidArgumentError(
absl::StrCat(filename, ": not a regular file."));
}
absl::StatusOr<std::string> GetContentAsString(absl::string_view filename) {
std::string content;
std::ifstream fs;
std::istream *stream = nullptr;
const bool use_stdin = IsStdin(filename);
if (use_stdin) {
stream = &std::cin;
} else {
const std::string filename_str = std::string{filename};
if (absl::Status status = FileExists(filename_str); !status.ok()) {
return status; // Bail
}
fs.open(filename_str.c_str());
std::error_code err;
const size_t prealloc = fs::file_size(filename_str, err);
if (err.value() == 0) content.reserve(prealloc);
stream = &fs;
}
if (!stream->good()) {
return CreateErrorStatusFromErrno(filename, "can't read");
}
char buffer[4096];
while (stream->good() && !stream->eof()) {
stream->read(buffer, sizeof(buffer));
content.append(buffer, stream->gcount());
}
// Allow stdin to be reopened for more input.
if (use_stdin && std::cin.eof()) std::cin.clear();
return std::move(content);
}
static absl::StatusOr<std::unique_ptr<MemBlock>> AttemptMemMapFile(
absl::string_view filename) {
#ifndef _WIN32
class MemMapBlock final : public MemBlock {
public:
MemMapBlock(char *buffer, size_t size) : buffer_(buffer), size_(size) {}
~MemMapBlock() final { munmap(buffer_, size_); }
absl::string_view AsStringView() const final { return {buffer_, size_}; }
private:
char *const buffer_;
const size_t size_;
};
const std::string nul_terminated_filename(filename);
const int fd = open(nul_terminated_filename.c_str(), O_RDONLY);
if (fd < 0) {
return CreateErrorStatusFromErrno(filename, "Can't open file");
}
struct stat s;
if (fstat(fd, &s) < 0) {
close(fd);
return CreateErrorStatusFromErrno(filename, "can't stat");
}
const size_t file_size = s.st_size;
void *const buffer = mmap(nullptr, file_size, PROT_READ, MAP_SHARED, fd, 0);
close(fd);
if (buffer == MAP_FAILED) { // NOLINT(performance-no-int-to-ptr)
return CreateErrorStatusFromErrno(filename, "Can't mmap file");
}
#ifdef POSIX_MADV_WILLNEED
// Trigger read-ahead if possible.
posix_madvise(buffer, file_size, POSIX_MADV_WILLNEED);
#endif
return std::make_unique<MemMapBlock>(reinterpret_cast<char *>(buffer),
file_size);
#else
// TODO: implement some memory mapping on Windows.
return absl::UnimplementedError("No windows mmap implementation yet.");
#endif
}
absl::StatusOr<std::unique_ptr<MemBlock>> GetContentAsMemBlock(
absl::string_view filename) {
auto mmap_result = AttemptMemMapFile(filename);
if (mmap_result.status().ok()) {
return mmap_result;
}
// Still here ? Well, let's try the traditional way
auto content_or = GetContentAsString(filename);
if (!content_or.ok()) return content_or.status();
return std::make_unique<StringMemBlock>(std::move(*content_or));
}
absl::Status SetContents(absl::string_view filename,
absl::string_view content) {
VLOG(1) << __FUNCTION__ << ": Writing file: " << filename;
std::ofstream f(std::string(filename).c_str());
if (!f.good()) return CreateErrorStatusFromErrno(filename, "can't write.");
f << content;
f.close();
if (!f.good()) return CreateErrorStatusFromErrno(filename, "closing.");
return absl::OkStatus();
}
std::string JoinPath(absl::string_view base, absl::string_view name) {
fs::path p = fs::path(std::string(base)) / fs::path(std::string(name));
return p.lexically_normal().string();
}
absl::Status CreateDir(absl::string_view dir) {
const std::string path(dir);
std::error_code err;
if (fs::create_directory(path, err) || err.value() == 0) {
return absl::OkStatus();
}
return CreateErrorStatusFromErr(dir, err, "can't create directory");
}
absl::StatusOr<Directory> ListDir(absl::string_view dir) {
std::error_code err;
Directory d;
d.path = dir.empty() ? "." : std::string(dir);
fs::file_status stat = fs::status(d.path, err);
if (err.value() != 0) {
return CreateErrorStatusFromErr(d.path, err, "Opening directory");
}
if (!fs::is_directory(stat)) {
return absl::InvalidArgumentError(absl::StrCat(dir, ": not a directory"));
}
for (const fs::directory_entry &entry : fs::directory_iterator(d.path)) {
const std::string entry_name = entry.path().string();
if (entry.is_directory()) {
d.directories.push_back(entry_name);
} else {
d.files.push_back(entry_name);
}
}
std::sort(d.files.begin(), d.files.end());
std::sort(d.directories.begin(), d.directories.end());
return d;
}
bool IsStdin(absl::string_view filename) {
static constexpr absl::string_view kStdinFilename = "-";
return filename == kStdinFilename;
}
namespace testing {
std::string RandomFileBasename(absl::string_view prefix) {
return absl::StrCat(prefix, "-", rand());
}
ScopedTestFile::ScopedTestFile(absl::string_view base_dir,
absl::string_view content,
absl::string_view use_this_filename)
// There is no secrecy needed for test files,
// file name just need to be unique enough.
: filename_(JoinPath(base_dir, use_this_filename.empty()
? RandomFileBasename("scoped-file")
: use_this_filename)) {
const absl::Status status = SetContents(filename_, content);
CHECK(status.ok()) << status.message(); // ok for test-only code
}
ScopedTestFile::~ScopedTestFile() { unlink(filename_.c_str()); }
} // namespace testing
} // namespace file
} // namespace verible