| // 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(); |
| } |