blob: 87bc0534960c7daa79218e8debcef1ecbac6e5a3 [file] [log] [blame]
// Copyright 2023 Google Inc. All Rights Reserved.
//
// 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 <windows.h>
#include <algorithm>
#include <map>
#include "stat_cache.h"
#include "util.h"
namespace {
bool IsPathSeparator(char ch) {
return (ch == '/' || ch == '\\');
}
// Compute the dirname and basename of a given path.
// If there is no directory separator, set |*dir| to ".".
void DecomposePath(const std::string& path, std::string* dir,
std::string* base) {
size_t pos = path.size();
// Find first directory separator before base name.
while (pos > 0 && !IsPathSeparator(path[pos - 1]))
--pos;
*base = path.substr(pos);
// Skip over separator(s) to find directory name, might be empty
while (pos > 1 && IsPathSeparator(path[pos - 2]))
--pos;
*dir = path.substr(0, pos);
}
bool IsWindows7OrLater() {
OSVERSIONINFOEX version_info = {
sizeof(OSVERSIONINFOEX), 6, 1, 0, 0, { 0 }, 0, 0, 0, 0, 0
};
DWORDLONG comparison = 0;
VER_SET_CONDITION(comparison, VER_MAJORVERSION, VER_GREATER_EQUAL);
VER_SET_CONDITION(comparison, VER_MINORVERSION, VER_GREATER_EQUAL);
return VerifyVersionInfo(&version_info, VER_MAJORVERSION | VER_MINORVERSION,
comparison);
}
using DirCache = std::map<std::string, TimeStamp>;
// TODO: Neither a map nor a hashmap seems ideal here. If the statcache
// works out, come up with a better data structure.
using Cache = std::map<std::string, DirCache>;
bool StatAllFilesInDir(const std::string& dir, DirCache* stamps,
std::string* err) {
// FindExInfoBasic is 30% faster than FindExInfoStandard.
static bool can_use_basic_info = IsWindows7OrLater();
// This is not in earlier SDKs.
const FINDEX_INFO_LEVELS kFindExInfoBasic =
static_cast<FINDEX_INFO_LEVELS>(1);
FINDEX_INFO_LEVELS level =
can_use_basic_info ? kFindExInfoBasic : FindExInfoStandard;
WIN32_FIND_DATAA ffd;
HANDLE find_handle = FindFirstFileExA((dir + "\\*").c_str(), level, &ffd,
FindExSearchNameMatch, NULL, 0);
if (find_handle == INVALID_HANDLE_VALUE) {
DWORD win_err = GetLastError();
if (win_err == ERROR_FILE_NOT_FOUND || win_err == ERROR_PATH_NOT_FOUND ||
win_err == ERROR_DIRECTORY)
return true;
*err = StringFormat("FindFirstFileExA(%s): %s", dir.c_str(),
GetLastErrorString().c_str());
return false;
}
do {
std::string lowername = ffd.cFileName;
if (lowername == "..") {
// Seems to just copy the timestamp for ".." from ".", which is wrong.
// This is the case at least on NTFS under Windows 7.
continue;
}
std::transform(lowername.begin(), lowername.end(), lowername.begin(),
::tolower);
// C++11 equivalent to
// stamps->try_emplace(std::move(lowername), TimeStampFromFile(...))
stamps->emplace(
std::piecewise_construct, std::forward_as_tuple(std::move(lowername)),
std::forward_as_tuple(TimeStampFromFileTime(ffd.ftLastWriteTime)));
} while (FindNextFileA(find_handle, &ffd));
FindClose(find_handle);
return true;
}
} // namespace
class StatCache::Impl {
public:
void Enable(bool enabled) {
if (!enabled)
cache_.clear();
enabled_ = enabled;
}
TimeStamp Stat(const std::string& path, std::string* err) const {
// MSDN: "Naming Files, Paths, and Namespaces"
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
if (!path.empty() && path[0] != '\\' && path.size() > MAX_PATH) {
*err = StringFormat("Stat(%s): Filename longer than %u characters",
path.c_str(), MAX_PATH);
return -1;
}
if (!enabled_)
return ::GetFileTimestamp(path, err);
std::string dir, base;
DecomposePath(path, &dir, &base);
if (base == "..") {
// StatAllFilesInDir does not report any information for base = "..".
base = ".";
dir = path;
}
std::string dir_lowercase = dir;
std::transform(dir.begin(), dir.end(), dir_lowercase.begin(), ::tolower);
std::transform(base.begin(), base.end(), base.begin(), ::tolower);
// NOTE: The following is the C++11 equivalent of
// cache_.try_emplace(std::move(dir_lowercase))
auto ret = cache_.emplace(std::piecewise_construct,
std::forward_as_tuple(std::move(dir_lowercase)),
std::forward_as_tuple());
Cache::iterator ci = ret.first;
if (ret.second) {
if (!StatAllFilesInDir(dir.empty() ? "." : dir, &ci->second, err)) {
cache_.erase(ci);
return -1;
}
}
DirCache::iterator di = ci->second.find(base);
return di != ci->second.end() ? di->second : 0;
}
void Sync() {
// TODO(digit): Implement this properly!
// FOR NOW, drop everything from the cache to pass the unit-tests!
cache_.clear();
}
void Flush() { cache_.clear(); }
private:
/// Whether stat information can be cached.
bool enabled_ = false;
/// The cache itself.
mutable Cache cache_;
};
StatCache::StatCache() : impl_(new StatCache::Impl()) {}
StatCache::~StatCache() = default;
void StatCache::Enable(bool enabled) {
impl_->Enable(enabled);
}
TimeStamp StatCache::Stat(const std::string& path, std::string* err) const {
return impl_->Stat(path, err);
}
void StatCache::Invalidate(const std::string& path) {
// TODO(digit): Only remove entries from the path's parent directory.
impl_->Sync();
}
void StatCache::Sync() {
impl_->Sync();
}
void StatCache::Flush() {
impl_->Flush();
}