// Copyright 2015 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. // +build ignore #include "find.h" #include <dirent.h> #include <fnmatch.h> #include <limits.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <algorithm> #include <memory> #include <vector> //#undef NOLOG #include "fileutil.h" #include "log.h" #include "string_piece.h" #include "strutil.h" #include "timeutil.h" class FindCond { public: virtual ~FindCond() = default; virtual bool IsTrue(const string& path, unsigned char type) const = 0; virtual bool Countable() const = 0; virtual unsigned Count() const = 0; protected: FindCond() = default; }; namespace { class NameCond : public FindCond { public: explicit NameCond(const string& n) : name_(n) { has_wildcard_ = (n.find_first_of("?*[") != string::npos); } virtual bool IsTrue(const string& path, unsigned char) const override { return fnmatch(name_.c_str(), Basename(path).data(), 0) == 0; } virtual bool Countable() const override { return !has_wildcard_; } virtual unsigned Count() const override { return 1; } private: string name_; bool has_wildcard_; }; class TypeCond : public FindCond { public: explicit TypeCond(unsigned char t) : type_(t) { } virtual bool IsTrue(const string&, unsigned char type) const override { return type == type_; } virtual bool Countable() const override { return false; } virtual unsigned Count() const override { return 0; } private: unsigned char type_; }; class NotCond : public FindCond { public: NotCond(FindCond* c) : c_(c) { } virtual bool IsTrue(const string& path, unsigned char type) const override { return !c_->IsTrue(path, type); } virtual bool Countable() const override { return false; } virtual unsigned Count() const override { return 0; } private: unique_ptr<FindCond> c_; }; class AndCond : public FindCond { public: AndCond(FindCond* c1, FindCond* c2) : c1_(c1), c2_(c2) { } virtual bool IsTrue(const string& path, unsigned char type) const override { if (c1_->IsTrue(path, type)) return c2_->IsTrue(path, type); return false; } virtual bool Countable() const override { return false; } virtual unsigned Count() const override { return 0; } private: unique_ptr<FindCond> c1_, c2_; }; class OrCond : public FindCond { public: OrCond(FindCond* c1, FindCond* c2) : c1_(c1), c2_(c2) { } virtual bool IsTrue(const string& path, unsigned char type) const override { if (!c1_->IsTrue(path, type)) return c2_->IsTrue(path, type); return true; } virtual bool Countable() const override { return c1_->Countable() && c2_->Countable();; } virtual unsigned Count() const override { return c1_->Count() + c2_->Count(); } private: unique_ptr<FindCond> c1_, c2_; }; class DirentNode { public: virtual ~DirentNode() = default; virtual const DirentNode* FindDir(StringPiece) const { return NULL; } virtual bool RunFind(const FindCommand& fc, const Loc& loc, int d, string* path, unordered_map<const DirentNode*, string>* cur_read_dirs, vector<string>& out) const = 0; virtual bool IsDirectory() const = 0; const string& base() const { return base_; } protected: explicit DirentNode(const string& name) { base_ = Basename(name).as_string(); } void PrintIfNecessary(const FindCommand& fc, const string& path, unsigned char type, int d, vector<string>& out) const { if (fc.print_cond && !fc.print_cond->IsTrue(path, type)) return; if (d < fc.mindepth) return; out.push_back(path); } string base_; }; class DirentFileNode : public DirentNode { public: DirentFileNode(const string& name, unsigned char type) : DirentNode(name), type_(type) { } virtual bool RunFind(const FindCommand& fc, const Loc&, int d, string* path, unordered_map<const DirentNode*, string>*, vector<string>& out) const override { PrintIfNecessary(fc, *path, type_, d, out); return true; } virtual bool IsDirectory() const override { return false; } private: unsigned char type_; }; struct ScopedReadDirTracker { public: ScopedReadDirTracker(const DirentNode* n, const string& path, unordered_map<const DirentNode*, string>* cur_read_dirs) : n_(NULL), cur_read_dirs_(cur_read_dirs) { const auto& p = cur_read_dirs->emplace(n, path); if (p.second) { n_ = n; } else { conflicted_ = p.first->second; } } ~ScopedReadDirTracker() { if (n_) cur_read_dirs_->erase(n_); } bool ok() const { return conflicted_.empty(); } const string& conflicted() const { return conflicted_; } private: string conflicted_; const DirentNode* n_; unordered_map<const DirentNode*, string>* cur_read_dirs_; }; class DirentDirNode : public DirentNode { public: explicit DirentDirNode(const string& name) : DirentNode(name) { } ~DirentDirNode() { for (auto& p : children_) { delete p.second; } } virtual const DirentNode* FindDir(StringPiece d) const override { if (d.empty() || d == ".") return this; size_t index = d.find('/'); const string& p = d.substr(0, index).as_string(); for (auto& child : children_) { if (p == child.first) { if (index == string::npos) return child.second; StringPiece nd = d.substr(index + 1); return child.second->FindDir(nd); } } return NULL; } virtual bool RunFind(const FindCommand& fc, const Loc& loc, int d, string* path, unordered_map<const DirentNode*, string>* cur_read_dirs, vector<string>& out) const override { ScopedReadDirTracker srdt(this, *path, cur_read_dirs); if (!srdt.ok()) { WARN_LOC(loc, "FindEmulator: find: File system loop detected; `%s' " "is part of the same file system loop as `%s'.", path->c_str(), srdt.conflicted().c_str()); return true; } fc.read_dirs->insert(*path); if (fc.prune_cond && fc.prune_cond->IsTrue(*path, DT_DIR)) { if (fc.type != FindCommandType::FINDLEAVES) { out.push_back(*path); } return true; } PrintIfNecessary(fc, *path, DT_DIR, d, out); if (d >= fc.depth) return true; size_t orig_path_size = path->size(); if (fc.type == FindCommandType::FINDLEAVES) { size_t orig_out_size = out.size(); for (const auto& p : children_) { DirentNode* c = p.second; // We will handle directories later. if (c->IsDirectory()) continue; if ((*path)[path->size()-1] != '/') *path += '/'; *path += c->base(); if (!c->RunFind(fc, loc, d + 1, path, cur_read_dirs, out)) return false; path->resize(orig_path_size); } // Found a leaf, stop the search. if (orig_out_size != out.size()) { // If we've found all possible files in this directory, we don't need // to add a regen dependency on the directory, we just need to ensure // that the files are not removed. if (fc.print_cond->Countable() && fc.print_cond->Count() == out.size() - orig_out_size) { fc.read_dirs->erase(*path); for (unsigned i = orig_out_size; i < out.size(); i++) { fc.found_files->push_back(out[i]); } } return true; } for (const auto& p : children_) { DirentNode* c = p.second; if (!c->IsDirectory()) continue; if ((*path)[path->size()-1] != '/') *path += '/'; *path += c->base(); if (!c->RunFind(fc, loc, d + 1, path, cur_read_dirs, out)) return false; path->resize(orig_path_size); } } else { for (const auto& p : children_) { DirentNode* c = p.second; if ((*path)[path->size()-1] != '/') *path += '/'; *path += c->base(); if (!c->RunFind(fc, loc, d + 1, path, cur_read_dirs, out)) return false; path->resize(orig_path_size); } } return true; } virtual bool IsDirectory() const override { return true; } void Add(const string& name, DirentNode* c) { children_.emplace(children_.end(), name, c); } private: vector<pair<string, DirentNode*>> children_; }; class DirentSymlinkNode : public DirentNode { public: explicit DirentSymlinkNode(const string& name) : DirentNode(name), to_(NULL), errno_(0) { } virtual const DirentNode* FindDir(StringPiece d) const override { if (errno_ == 0 && to_) return to_->FindDir(d); return NULL; } virtual bool RunFind(const FindCommand& fc, const Loc& loc, int d, string* path, unordered_map<const DirentNode*, string>* cur_read_dirs, vector<string>& out) const override { unsigned char type = DT_LNK; if (fc.follows_symlinks && errno_ != ENOENT) { if (errno_) { if (fc.type != FindCommandType::FINDLEAVES) { WARN_LOC(loc, "FindEmulator: find: `%s': %s", path->c_str(), strerror(errno_)); } return true; } if (!to_) { LOG("FindEmulator does not support %s", path->c_str()); return false; } return to_->RunFind(fc, loc, d, path, cur_read_dirs, out); } PrintIfNecessary(fc, *path, type, d, out); return true; } virtual bool IsDirectory() const override { return errno_ == 0 && to_ && to_->IsDirectory(); } void set_to(const DirentNode* to) { to_ = to; } void set_errno(int e) { errno_ = e; } private: const DirentNode* to_; int errno_; }; class FindCommandParser { public: FindCommandParser(StringPiece cmd, FindCommand* fc) : cmd_(cmd), fc_(fc), has_if_(false) { } bool Parse() { cur_ = cmd_; if (!ParseImpl()) { LOG("FindEmulator: Unsupported find command: %.*s", SPF(cmd_)); return false; } CHECK(TrimLeftSpace(cur_).empty()); return true; } private: bool GetNextToken(StringPiece* tok) { if (!unget_tok_.empty()) { *tok = unget_tok_; unget_tok_.clear(); return true; } cur_ = TrimLeftSpace(cur_); if (cur_[0] == ';') { *tok = cur_.substr(0, 1); cur_ = cur_.substr(1); return true; } if (cur_[0] == '&') { if (cur_.get(1) != '&') { return false; } *tok = cur_.substr(0, 2); cur_ = cur_.substr(2); return true; } size_t i = 0; while (i < cur_.size() && !isspace(cur_[i]) && cur_[i] != ';' && cur_[i] != '&') { i++; } *tok = cur_.substr(0, i); cur_ = cur_.substr(i); const char c = tok->get(0); if (c == '\'' || c == '"') { if (tok->size() < 2 || (*tok)[tok->size()-1] != c) return false; *tok = tok->substr(1, tok->size() - 2); return true; } return true; } void UngetToken(StringPiece tok) { CHECK(unget_tok_.empty()); if (!tok.empty()) unget_tok_ = tok; } bool ParseTest() { if (has_if_ || !fc_->testdir.empty()) return false; StringPiece tok; if (!GetNextToken(&tok) || tok != "-d") return false; if (!GetNextToken(&tok) || tok.empty()) return false; fc_->testdir = tok.as_string(); return true; } FindCond* ParseFact(StringPiece tok) { if (tok == "-not" || tok == "\\!") { if (!GetNextToken(&tok) || tok.empty()) return NULL; unique_ptr<FindCond> c(ParseFact(tok)); if (!c.get()) return NULL; return new NotCond(c.release()); } else if (tok == "\\(") { if (!GetNextToken(&tok) || tok.empty()) return NULL; unique_ptr<FindCond> c(ParseExpr(tok)); if (!GetNextToken(&tok) || tok != "\\)") { return NULL; } return c.release(); } else if (tok == "-name") { if (!GetNextToken(&tok) || tok.empty()) return NULL; return new NameCond(tok.as_string()); } else if (tok == "-type") { if (!GetNextToken(&tok) || tok.empty()) return NULL; char type; if (tok == "b") type = DT_BLK; else if (tok == "c") type = DT_CHR; else if (tok == "d") type = DT_DIR; else if (tok == "p") type = DT_FIFO; else if (tok == "l") type = DT_LNK; else if (tok == "f") type = DT_REG; else if (tok == "s") type = DT_SOCK; else return NULL; return new TypeCond(type); } else { UngetToken(tok); return NULL; } } FindCond* ParseTerm(StringPiece tok) { unique_ptr<FindCond> c(ParseFact(tok)); if (!c.get()) return NULL; while (true) { if (!GetNextToken(&tok)) return NULL; if (tok != "-and" && tok != "-a") { UngetToken(tok); return c.release(); } if (!GetNextToken(&tok) || tok.empty()) return NULL; unique_ptr<FindCond> r(ParseFact(tok)); if (!r.get()) { return NULL; } c.reset(new AndCond(c.release(), r.release())); } } FindCond* ParseExpr(StringPiece tok) { unique_ptr<FindCond> c(ParseTerm(tok)); if (!c.get()) return NULL; while (true) { if (!GetNextToken(&tok)) return NULL; if (tok != "-or" && tok != "-o") { UngetToken(tok); return c.release(); } if (!GetNextToken(&tok) || tok.empty()) return NULL; unique_ptr<FindCond> r(ParseTerm(tok)); if (!r.get()) { return NULL; } c.reset(new OrCond(c.release(), r.release())); } } // <expr> ::= <term> {<or> <term>} // <term> ::= <fact> {<and> <fact>} // <fact> ::= <not> <fact> | '\(' <expr> '\)' | <pred> // <not> ::= '-not' | '\!' // <and> ::= '-and' | '-a' // <or> ::= '-or' | '-o' // <pred> ::= <name> | <type> | <maxdepth> // <name> ::= '-name' NAME // <type> ::= '-type' TYPE // <maxdepth> ::= '-maxdepth' MAXDEPTH FindCond* ParseFindCond(StringPiece tok) { return ParseExpr(tok); } bool ParseFind() { fc_->type = FindCommandType::FIND; StringPiece tok; while (true) { if (!GetNextToken(&tok)) return false; if (tok.empty() || tok == ";") return true; if (tok == "-L") { fc_->follows_symlinks = true; } else if (tok == "-prune") { if (!fc_->print_cond || fc_->prune_cond) return false; if (!GetNextToken(&tok) || tok != "-o") return false; fc_->prune_cond.reset(fc_->print_cond.release()); } else if (tok == "-print") { if (!GetNextToken(&tok) || !tok.empty()) return false; return true; } else if (tok == "-maxdepth") { if (!GetNextToken(&tok) || tok.empty()) return false; const string& depth_str = tok.as_string(); char* endptr; long d = strtol(depth_str.c_str(), &endptr, 10); if (endptr != depth_str.data() + depth_str.size() || d < 0 || d > INT_MAX) { return false; } fc_->depth = d; } else if (tok[0] == '-' || tok == "\\(") { if (fc_->print_cond.get()) return false; FindCond* c = ParseFindCond(tok); if (!c) return false; fc_->print_cond.reset(c); } else if (tok == "2>") { if (!GetNextToken(&tok) || tok != "/dev/null") { return false; } fc_->redirect_to_devnull = true; } else if (tok.find_first_of("|;&><*'\"") != string::npos) { return false; } else { fc_->finddirs.push_back(tok.as_string()); } } } bool ParseFindLeaves() { fc_->type = FindCommandType::FINDLEAVES; fc_->follows_symlinks = true; StringPiece tok; vector<string> findfiles; while (true) { if (!GetNextToken(&tok)) return false; if (tok.empty()) { if (fc_->finddirs.size() == 0) { // backwards compatibility if (findfiles.size() < 2) return false; fc_->finddirs.swap(findfiles); fc_->print_cond.reset(new NameCond(fc_->finddirs.back())); fc_->finddirs.pop_back(); } else { if (findfiles.size() < 1) return false; for (auto& file : findfiles) { FindCond* cond = new NameCond(file); if (fc_->print_cond.get()) { cond = new OrCond(fc_->print_cond.release(), cond); } CHECK(!fc_->print_cond.get()); fc_->print_cond.reset(cond); } } return true; } if (HasPrefix(tok, "--prune=")) { FindCond* cond = new NameCond( tok.substr(strlen("--prune=")).as_string()); if (fc_->prune_cond.get()) { cond = new OrCond(fc_->prune_cond.release(), cond); } CHECK(!fc_->prune_cond.get()); fc_->prune_cond.reset(cond); } else if (HasPrefix(tok, "--mindepth=")) { string mindepth_str = tok.substr(strlen("--mindepth=")).as_string(); char* endptr; long d = strtol(mindepth_str.c_str(), &endptr, 10); if (endptr != mindepth_str.data() + mindepth_str.size() || d < INT_MIN || d > INT_MAX) { return false; } fc_->mindepth = d; } else if (HasPrefix(tok, "--dir=")) { StringPiece dir= tok.substr(strlen("--dir=")); fc_->finddirs.push_back(dir.as_string()); } else if (HasPrefix(tok, "--")) { WARN("Unknown flag in findleaves.py: %.*s", SPF(tok)); return false; } else { findfiles.push_back(tok.as_string()); } } } bool ParseImpl() { while (true) { StringPiece tok; if (!GetNextToken(&tok)) return false; if (tok.empty()) return true; if (tok == "cd") { if (!GetNextToken(&tok) || tok.empty() || !fc_->chdir.empty()) return false; fc_->chdir = tok.as_string(); if (!GetNextToken(&tok) || (tok != ";" && tok != "&&")) return false; } else if (tok == "if") { if (!GetNextToken(&tok) || tok != "[") return false; if (!ParseTest()) return false; if (!GetNextToken(&tok) || tok != "]") return false; if (!GetNextToken(&tok) || tok != ";") return false; if (!GetNextToken(&tok) || tok != "then") return false; has_if_ = true; } else if (tok == "test") { if (!fc_->chdir.empty()) return false; if (!ParseTest()) return false; if (!GetNextToken(&tok) || tok != "&&") return false; } else if (tok == "find") { if (!ParseFind()) return false; if (has_if_) { if (!GetNextToken(&tok) || tok != "fi") return false; } if (!GetNextToken(&tok) || !tok.empty()) return false; return true; } else if (tok == "build/tools/findleaves.py") { if (!ParseFindLeaves()) return false; return true; } else { return false; } } } StringPiece cmd_; StringPiece cur_; FindCommand* fc_; bool has_if_; StringPiece unget_tok_; }; static FindEmulator* g_instance; class FindEmulatorImpl : public FindEmulator { public: FindEmulatorImpl() : node_cnt_(0), is_initialized_(false) { g_instance = this; } virtual ~FindEmulatorImpl() = default; bool CanHandle(StringPiece s) const { return (!HasPrefix(s, "../") && !HasPrefix(s, "/") && !HasPrefix(s, ".repo") && !HasPrefix(s, ".git")); } const DirentNode* FindDir(StringPiece d, bool* should_fallback) { const DirentNode* r = root_->FindDir(d); if (!r) { *should_fallback = Exists(d); } return r; } virtual bool HandleFind(const string& cmd UNUSED, const FindCommand& fc, const Loc& loc, string* out) override { if (!CanHandle(fc.chdir)) { LOG("FindEmulator: Cannot handle chdir (%.*s): %s", SPF(fc.chdir), cmd.c_str()); return false; } if (!is_initialized_) { ScopedTimeReporter tr("init find emulator time"); root_.reset(ConstructDirectoryTree("")); if (!root_) { ERROR("FindEmulator: Cannot open root directory"); } ResolveSymlinks(); LOG_STAT("%d find nodes", node_cnt_); is_initialized_ = true; } if (!fc.testdir.empty()) { if (!CanHandle(fc.testdir)) { LOG("FindEmulator: Cannot handle test dir (%.*s): %s", SPF(fc.testdir), cmd.c_str()); return false; } bool should_fallback = false; if (!FindDir(fc.testdir, &should_fallback)) { LOG("FindEmulator: Test dir (%.*s) not found: %s", SPF(fc.testdir), cmd.c_str()); return !should_fallback; } } if (!fc.chdir.empty()) { if (!CanHandle(fc.chdir)) { LOG("FindEmulator: Cannot handle chdir (%.*s): %s", SPF(fc.chdir), cmd.c_str()); return false; } bool should_fallback = false; if (!FindDir(fc.chdir, &should_fallback)) { if (should_fallback) return false; if (!fc.redirect_to_devnull) { WARN_LOC(loc, "FindEmulator: cd: %.*s: No such file or directory", SPF(fc.chdir)); } return true; } } vector<string> results; for (const string& finddir : fc.finddirs) { const string dir = ConcatDir(fc.chdir, finddir); if (!CanHandle(dir)) { LOG("FindEmulator: Cannot handle find dir (%s): %s", dir.c_str(), cmd.c_str()); return false; } bool should_fallback = false; const DirentNode* base = FindDir(dir, &should_fallback); if (!base) { if (should_fallback) { return false; } if (!fc.redirect_to_devnull) { WARN_LOC(loc, "FindEmulator: find: `%s': No such file or directory", ConcatDir(fc.chdir, finddir).c_str()); } continue; } string path = finddir; unordered_map<const DirentNode*, string> cur_read_dirs; if (!base->RunFind(fc, loc, 0, &path, &cur_read_dirs, results)) { LOG("FindEmulator: RunFind failed: %s", cmd.c_str()); return false; } } if (results.size() > 0) { // Calculate and reserve necessary space in out size_t new_length = 0; for (const string& result : results) { new_length += result.size() + 1; } out->reserve(out->size() + new_length - 1); if (fc.type == FindCommandType::FINDLEAVES) { sort(results.begin(), results.end()); } WordWriter writer(out); for (const string& result : results) { writer.Write(result); } } LOG("FindEmulator: OK"); return true; } private: static unsigned char GetDtTypeFromStat(const struct stat& st) { if (S_ISREG(st.st_mode)) { return DT_REG; } else if (S_ISDIR(st.st_mode)) { return DT_DIR; } else if (S_ISCHR(st.st_mode)) { return DT_CHR; } else if (S_ISBLK(st.st_mode)) { return DT_BLK; } else if (S_ISFIFO(st.st_mode)) { return DT_FIFO; } else if (S_ISLNK(st.st_mode)) { return DT_LNK; } else if (S_ISSOCK(st.st_mode)) { return DT_SOCK; } else { return DT_UNKNOWN; } } static unsigned char GetDtType(const string& path) { struct stat st; if (lstat(path.c_str(), &st)) { PERROR("stat for %s", path.c_str()); } return GetDtTypeFromStat(st); } DirentNode* ConstructDirectoryTree(const string& path) { DIR* dir = opendir(path.empty() ? "." : path.c_str()); if (!dir) { if (errno == ENOENT) { LOG("opendir failed: %s", path.c_str()); return NULL; } else { PERROR("opendir failed: %s", path.c_str()); } } DirentDirNode* n = new DirentDirNode(path); struct dirent* ent; while ((ent = readdir(dir)) != NULL) { if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..") || !strcmp(ent->d_name, ".repo") || !strcmp(ent->d_name, ".git")) continue; string npath = path; if (!path.empty()) npath += '/'; npath += ent->d_name; DirentNode* c = NULL; auto d_type = ent->d_type; if (d_type == DT_UNKNOWN) { d_type = GetDtType(npath); CHECK(d_type != DT_UNKNOWN); } if (d_type == DT_DIR) { c = ConstructDirectoryTree(npath); if (c == NULL) { continue; } } else if (d_type == DT_LNK) { auto s = new DirentSymlinkNode(npath); symlinks_.push_back(make_pair(npath, s)); c = s; } else { c = new DirentFileNode(npath, d_type); } node_cnt_++; n->Add(ent->d_name, c); } closedir(dir); return n; } void ResolveSymlinks() { vector<pair<string, DirentSymlinkNode*>> symlinks; symlinks.swap(symlinks_); for (const auto& p : symlinks) { const string& path = p.first; DirentSymlinkNode* s = p.second; char buf[PATH_MAX+1]; buf[PATH_MAX] = 0; ssize_t len = readlink(path.c_str(), buf, PATH_MAX); if (len < 0) { WARN("readlink failed: %s", path.c_str()); continue; } buf[len] = 0; struct stat st; unsigned char type = DT_UNKNOWN; if (stat(path.c_str(), &st) == 0) { type = GetDtTypeFromStat(st); } else { s->set_errno(errno); LOG("stat failed: %s: %s", path.c_str(), strerror(errno)); } if (*buf != '/') { const string npath = ConcatDir(Dirname(path), buf); bool should_fallback = false; const DirentNode* to = FindDir(npath, &should_fallback); if (to) { s->set_to(to); continue; } } if (type == DT_DIR) { if (path.find('/') == string::npos) { DirentNode* dir = ConstructDirectoryTree(path); if (dir != NULL) { s->set_to(dir); } else { s->set_errno(errno); } } } else if (type != DT_LNK && type != DT_UNKNOWN) { s->set_to(new DirentFileNode(path, type)); } } if (!symlinks_.empty()) ResolveSymlinks(); } unique_ptr<DirentNode> root_; vector<pair<string, DirentSymlinkNode*>> symlinks_; int node_cnt_; bool is_initialized_; }; } // namespace FindCommand::FindCommand() : follows_symlinks(false), depth(INT_MAX), mindepth(INT_MIN), redirect_to_devnull(false), found_files(new vector<string>()), read_dirs(new unordered_set<string>()) { } FindCommand::~FindCommand() { } bool FindCommand::Parse(const string& cmd) { FindCommandParser fcp(cmd, this); if (!HasWord(cmd, "find") && !HasWord(cmd, "build/tools/findleaves.py")) return false; if (!fcp.Parse()) return false; NormalizePath(&chdir); NormalizePath(&testdir); if (finddirs.empty()) finddirs.push_back("."); return true; } FindEmulator* FindEmulator::Get() { return g_instance; } void InitFindEmulator() { new FindEmulatorImpl(); }