openMSX
FilePool.cc
Go to the documentation of this file.
1 #include "FilePool.hh"
2 #include "File.hh"
3 #include "FileException.hh"
4 #include "FileContext.hh"
5 #include "FileOperations.hh"
6 #include "TclObject.hh"
7 #include "ReadDir.hh"
8 #include "Date.hh"
9 #include "CommandController.hh"
10 #include "CommandException.hh"
11 #include "Display.hh"
12 #include "EventDistributor.hh"
13 #include "CliComm.hh"
14 #include "Reactor.hh"
15 #include "Timer.hh"
16 #include "StringOp.hh"
17 #include "memory.hh"
18 #include "sha1.hh"
19 #include "stl.hh"
20 #include <fstream>
21 
22 using std::ifstream;
23 using std::make_tuple;
24 using std::ofstream;
25 using std::string;
26 using std::vector;
27 
28 namespace openmsx {
29 
30 class Sha1SumCommand final : public Command
31 {
32 public:
33  Sha1SumCommand(CommandController& commandController, FilePool& filePool);
34  void execute(array_ref<TclObject> tokens, TclObject& result) override;
35  string help(const vector<string>& tokens) const override;
36  void tabCompletion(vector<string>& tokens) const override;
37 private:
38  FilePool& filePool;
39 };
40 
41 
42 const char* const FILE_CACHE = "/.filecache";
43 
44 static string initialFilePoolSettingValue()
45 {
46  TclObject result;
47 
48  for (auto& p : systemFileContext().getPaths()) {
49  TclObject entry1;
50  entry1.addListElement("-path");
51  entry1.addListElement(FileOperations::join(p, "systemroms"));
52  entry1.addListElement("-types");
53  entry1.addListElement("system_rom");
54  result.addListElement(entry1);
55 
56  TclObject entry2;
57  entry2.addListElement("-path");
58  entry2.addListElement(FileOperations::join(p, "software"));
59  entry2.addListElement("-types");
60  entry2.addListElement("rom disk tape");
61  result.addListElement(entry2);
62  }
63  return result.getString().str();
64 }
65 
67  : filePoolSetting(
68  controller, "__filepool",
69  "This is an internal setting. Don't change this directly, "
70  "instead use the 'filepool' command.",
71  initialFilePoolSettingValue())
72  , reactor(reactor_)
73  , quit(false)
74 {
75  filePoolSetting.attach(*this);
77  try {
78  readSha1sums();
79  } catch (MSXException&) {
80  // ignore, probably .filecache doesn't exist yet
81  }
82  needWrite = false;
83 
84  sha1SumCommand = make_unique<Sha1SumCommand>(controller, *this);
85 }
86 
88 {
89  if (needWrite) {
90  writeSha1sums();
91  }
93  filePoolSetting.detach(*this);
94 }
95 
96 void FilePool::insert(const Sha1Sum& sum, time_t time, const string& filename)
97 {
98  auto it = upper_bound(begin(pool), end(pool), sum,
99  ComparePool());
100  stringBuffer.push_back(filename);
101  pool.emplace(it, sum, time, stringBuffer.back().c_str());
102  needWrite = true;
103 }
104 
105 void FilePool::remove(Pool::iterator it)
106 {
107  pool.erase(it);
108  needWrite = true;
109 }
110 
111 // Change the sha1sum of the element pointed to by 'it' into 'newSum'.
112 // Also re-arrange the items so that pool remains sorted on sha1sum. Internally
113 // this method doesn't actually sort, it merely rotates the elements.
114 // Returns false if the new position is before (or at) the old position.
115 // Returns true if the new position is after the old position.
116 bool FilePool::adjust(Pool::iterator it, const Sha1Sum& newSum)
117 {
118  needWrite = true;
119  auto newIt = upper_bound(begin(pool), end(pool), newSum,
120  ComparePool());
121  it->sum = newSum; // update sum
122  if (newIt > it) {
123  // move to back
124  rotate(it, it + 1, newIt);
125  return true;
126  } else {
127  if (newIt < it) {
128  // move to front
129  rotate(newIt, it, it + 1);
130  } else {
131  // (unlikely) sha1sum has changed, but after
132  // resorting item would remain in the same
133  // position
134  }
135  return false;
136  }
137 }
138 
139 time_t FilePool::PoolEntry::getTime()
140 {
141  if (time == time_t(-1)) {
142  time = Date::fromString(timeStr);
143  }
144  return time;
145 }
146 
147 void FilePool::PoolEntry::setTime(time_t t)
148 {
149  time = t;
150  timeStr = nullptr;
151 }
152 
153 static bool parse(char* line, char* line_end,
154  Sha1Sum& sha1, const char*& timeStr, const char*& filename)
155 {
156  if ((line_end - line) <= 68) return false; // minumum length (only filename is variable)
157 
158  // only perform quick sanity check on date/time format
159  if (line[40] != ' ') return false; // two space between sha1sum and date
160  if (line[41] != ' ') return false;
161  if (line[45] != ' ') return false; // space between day-of-week and month
162  if (line[49] != ' ') return false; // space between month and day of month
163  if (line[52] != ' ') return false; // space between day of month and hour
164  if (line[55] != ':') return false; // colon between hour and minutes
165  if (line[58] != ':') return false; // colon between minutes and seconds
166  if (line[61] != ' ') return false; // space between seconds and year
167  if (line[66] != ' ') return false; // two spaces between date and filename
168  if (line[67] != ' ') return false;
169 
170  try {
171  sha1.parse40(line);
172  } catch (MSXException& /*e*/) {
173  return false;
174  }
175 
176  timeStr = line + 42; // not guaranteed to be a correct date/time
177  line[66] = '\0'; // zero-terminate timeStr, so that it can be printed
178 
179  filename = line + 68;
180  *line_end = '\0'; // ok because there is certainly a '\n' after this line
181  return true;
182 }
183 
184 void FilePool::readSha1sums()
185 {
186  assert(pool.empty());
187  assert(fileMem.empty());
188 
189  File file(FileOperations::getUserDataDir() + FILE_CACHE);
190  auto size = file.getSize();
191  fileMem.resize(size + 1);
192  file.read(fileMem.data(), size);
193  fileMem[size] = '\n'; // ensure there's always a '\n' at the end
194 
195  // Process each line.
196  // Assume lines are separated by "\n", "\r\n" or "\n\r" (but not "\r").
197  char* data = fileMem.data();
198  char* data_end = data + size + 1;
199  while (data != data_end) {
200  // memchr() seems better optimized than std::find_if()
201  char* it = static_cast<char*>(memchr(data, '\n', data_end - data));
202  if (it == nullptr) it = data_end;
203  if ((it != data) && (it[-1] == '\r')) --it;
204 
206  const char* timeStr;
207  const char* filename;
208  if (parse(data, it, sum, timeStr, filename)) {
209  pool.emplace_back(sum, timeStr, filename);
210  }
211 
212  data = std::find_if(it + 1, data_end, [](byte c) {
213  return !(c == '\n' || c == '\r');
214  });
215  }
216 
217  if (!std::is_sorted(begin(pool), end(pool), ComparePool())) {
218  // This should _rarely_ happen. In fact it should only happen
219  // when .filecache was manually edited. Though because it's
220  // very important that pool is indeed sorted I've added this
221  // safety mechanism.
222  sort(begin(pool), end(pool), ComparePool());
223  }
224 }
225 
226 void FilePool::writeSha1sums()
227 {
228  string cacheFile = FileOperations::getUserDataDir() + FILE_CACHE;
229  ofstream file;
230  FileOperations::openofstream(file, cacheFile);
231  if (!file.is_open()) {
232  return;
233  }
234  for (auto& p : pool) {
235  file << p.sum.toString() << " ";
236  if (p.timeStr) {
237  file << p.timeStr;
238  } else {
239  assert(p.time != time_t(-1));
240  file << Date::toString(p.time);
241  }
242  file << " " << p.filename << '\n';
243  }
244 }
245 
246 static int parseTypes(Interpreter& interp, const TclObject& list)
247 {
248  int result = 0;
249  unsigned num = list.getListLength(interp);
250  for (unsigned i = 0; i < num; ++i) {
251  string_ref elem = list.getListIndex(interp, i).getString();
252  if (elem == "system_rom") {
253  result |= FilePool::SYSTEM_ROM;
254  } else if (elem == "rom") {
255  result |= FilePool::ROM;
256  } else if (elem == "disk") {
257  result |= FilePool::DISK;
258  } else if (elem == "tape") {
259  result |= FilePool::TAPE;
260  } else {
261  throw CommandException("Unknown type: " + elem);
262  }
263  }
264  return result;
265 }
266 
267 void FilePool::update(const Setting& setting)
268 {
269  assert(&setting == &filePoolSetting); (void)setting;
270  getDirectories(); // check for syntax errors
271 }
272 
273 FilePool::Directories FilePool::getDirectories() const
274 {
275  Directories result;
276  auto& interp = filePoolSetting.getInterpreter();
277  const TclObject& all = filePoolSetting.getValue();
278  unsigned numLines = all.getListLength(interp);
279  for (unsigned i = 0; i < numLines; ++i) {
280  Entry entry;
281  bool hasPath = false;
282  entry.types = 0;
283  TclObject line = all.getListIndex(interp, i);
284  unsigned numItems = line.getListLength(interp);
285  if (numItems & 1) {
286  throw CommandException(
287  "Expected a list with an even number "
288  "of elements, but got " + line.getString());
289  }
290  for (unsigned j = 0; j < numItems; j += 2) {
291  string_ref name = line.getListIndex(interp, j + 0).getString();
292  TclObject value = line.getListIndex(interp, j + 1);
293  if (name == "-path") {
294  entry.path = value.getString().str();
295  hasPath = true;
296  } else if (name == "-types") {
297  entry.types = parseTypes(interp, value);
298  } else {
299  throw CommandException(
300  "Unknown item: " + name);
301  }
302  }
303  if (!hasPath) {
304  throw CommandException(
305  "Missing -path item: " + line.getString());
306  }
307  if (entry.types == 0) {
308  throw CommandException(
309  "Missing -types item: " + line.getString());
310  }
311  result.push_back(entry);
312  }
313  return result;
314 }
315 
316 File FilePool::getFile(FileType fileType, const Sha1Sum& sha1sum)
317 {
318  File result = getFromPool(sha1sum);
319  if (result.is_open()) return result;
320 
321  // not found in cache, need to scan directories
322  ScanProgress progress;
323  progress.lastTime = Timer::getTime();
324  progress.amountScanned = 0;
325 
326  Directories directories;
327  try {
328  directories = getDirectories();
329  } catch (CommandException& e) {
330  reactor.getCliComm().printWarning(
331  "Error while parsing '__filepool' setting" + e.getMessage());
332  }
333  for (auto& d : directories) {
334  if (d.types & fileType) {
335  string path = FileOperations::expandTilde(d.path);
336  result = scanDirectory(sha1sum, path, d.path, progress);
337  if (result.is_open()) return result;
338  }
339  }
340 
341  return result; // not found
342 }
343 
344 static void reportProgress(const string& filename, size_t percentage,
345  Reactor& reactor)
346 {
347  reactor.getCliComm().printProgress(
348  "Calculating SHA1 sum for " + filename + "... " + StringOp::toString(percentage) + '%');
349  reactor.getDisplay().repaint();
350 }
351 
352 static Sha1Sum calcSha1sum(File& file, Reactor& reactor)
353 {
354  // Calculate sha1 in several steps so that we can show progress
355  // information. We take a fixed step size for an efficient calculation.
356  static const size_t STEP_SIZE = 1024 * 1024; // 1MB
357 
358  size_t size;
359  const byte* data = file.mmap(size);
360  string filename = file.getOriginalName();
361 
362  SHA1 sha1;
363  size_t done = 0;
364  size_t remaining = size;
365  auto lastShowedProgress = Timer::getTime();
366  bool everShowedProgress = false;
367 
368  // Loop over all-but-the last blocks. For small files this loop is skipped.
369  while (remaining > STEP_SIZE) {
370  sha1.update(&data[done], STEP_SIZE);
371  done += STEP_SIZE;
372  remaining -= STEP_SIZE;
373 
374  auto now = Timer::getTime();
375  if ((now - lastShowedProgress) > 1000000) {
376  reportProgress(filename, (100 * done) / size, reactor);
377  lastShowedProgress = now;
378  everShowedProgress = true;
379  }
380  }
381  // last block
382  sha1.update(&data[done], remaining);
383  if (everShowedProgress) {
384  reportProgress(filename, 100, reactor);
385  }
386  return sha1.digest();
387 }
388 
389 File FilePool::getFromPool(const Sha1Sum& sha1sum)
390 {
391  auto bound = equal_range(begin(pool), end(pool), sha1sum,
392  ComparePool());
393  // use indices instead of iterators
394  auto i = distance(begin(pool), bound.first);
395  auto last = distance(begin(pool), bound.second);
396  while (i != last) {
397  auto it = begin(pool) + i;
398  if (it->getTime() == time_t(-1)) {
399  // Invalid time/date format. Remove from
400  // database and continue searching.
401  remove(it);
402  --last;
403  continue;
404  }
405  try {
406  File file(it->filename);
407  auto newTime = file.getModificationDate();
408  if (it->time == newTime) {
409  // When modification time is unchanged, assume
410  // sha1sum is also unchanged. So avoid
411  // expensive sha1sum calculation.
412  return file;
413  }
414  it->setTime(newTime); // update timestamp
415  needWrite = true;
416  auto newSum = calcSha1sum(file, reactor);
417  if (newSum == sha1sum) {
418  // Modification time was changed, but
419  // (recalculated) sha1sum is still the same.
420  return file;
421  }
422  // Sha1sum has changed: update sha1sum, move entry to
423  // new position new sum and continue searching.
424  if (adjust(it, newSum)) {
425  // after
426  --last; // no ++i
427  } else {
428  // before (or at)
429  ++i;
430  }
431  } catch (FileException&) {
432  // Error reading file: remove from db and continue
433  // searching.
434  remove(it);
435  --last;
436  }
437  }
438  return File(); // not found
439 }
440 
441 File FilePool::scanDirectory(
442  const Sha1Sum& sha1sum, const string& directory, const string& poolPath,
443  ScanProgress& progress)
444 {
445  ReadDir dir(directory);
446  while (dirent* d = dir.getEntry()) {
447  if (quit) {
448  // Scanning can take a long time. Allow to exit
449  // openmsx when it takes too long. Stop scanning
450  // by pretending we didn't find the file.
451  return File();
452  }
453  string file = d->d_name;
454  string path = directory + '/' + file;
456  if (FileOperations::getStat(path, st)) {
457  File result;
459  result = scanFile(sha1sum, path, st, poolPath, progress);
460  } else if (FileOperations::isDirectory(st)) {
461  if ((file != ".") && (file != "..")) {
462  result = scanDirectory(sha1sum, path, poolPath, progress);
463  }
464  }
465  if (result.is_open()) return result;
466  }
467  }
468  return File(); // not found
469 }
470 
471 File FilePool::scanFile(const Sha1Sum& sha1sum, const string& filename,
472  const FileOperations::Stat& st, const string& poolPath,
473  ScanProgress& progress)
474 {
475  ++progress.amountScanned;
476  // Periodically send a progress message with the current filename
477  auto now = Timer::getTime();
478  if (now > (progress.lastTime + 250000)) { // 4Hz
479  progress.lastTime = now;
480  reactor.getCliComm().printProgress("Searching for file with sha1sum " +
481  sha1sum.toString() + "...\nIndexing filepool " + poolPath +
482  ": [" + StringOp::toString(progress.amountScanned) + "]: " +
483  filename.substr(poolPath.size()));
484  }
485 
486  // deliverEvents() is relatively cheap when there are no events to
487  // deliver, so it's ok to call on each file.
489 
490  auto it = findInDatabase(filename);
491  if (it == end(pool)) {
492  // not in pool
493  try {
494  File file(filename);
495  auto sum = calcSha1sum(file, reactor);
496  auto time = FileOperations::getModificationDate(st);
497  insert(sum, time, filename);
498  if (sum == sha1sum) {
499  return file;
500  }
501  } catch (FileException&) {
502  // ignore
503  }
504  } else {
505  // already in pool
506  assert(filename == it->filename);
507  assert(it->time != time_t(-1));
508  try {
509  auto time = FileOperations::getModificationDate(st);
510  if (it->time == time) {
511  // db is still up to date
512  if (it->sum == sha1sum) {
513  return File(filename);
514  }
515  } else {
516  // db outdated
517  File file(filename);
518  auto sum = calcSha1sum(file, reactor);
519  it->setTime(time);
520  adjust(it, sum);
521  if (sum == sha1sum) {
522  return file;
523  }
524  }
525  } catch (FileException&) {
526  // error reading file, remove from db
527  remove(it);
528  }
529  }
530  return File(); // not found
531 }
532 
533 FilePool::Pool::iterator FilePool::findInDatabase(const string& filename)
534 {
535  // Linear search in pool for filename.
536  // Search from back to front because often, soon after this search, we
537  // will insert/remove an element from the vector. This requires
538  // shifting all elements in the vector starting from a certain
539  // position. Starting the search from the back increases the likelihood
540  // that the to-be-shifted elements are already in the memory cache.
541  auto i = pool.size();
542  while (i) {
543  --i;
544  auto it = begin(pool) + i;
545  if (it->filename == filename) {
546  // ensure 'time' is valid
547  if (it->getTime() == time_t(-1)) {
548  // invalid time/date format, remove from db
549  // and continue searching
550  pool.erase(it);
551  continue;
552  }
553  return it;
554  }
555  }
556  return end(pool); // not found
557 }
558 
560 {
561  auto time = file.getModificationDate();
562  const auto& filename = file.getURL();
563 
564  auto it = findInDatabase(filename);
565  assert((it == end(pool)) || (it->time != time_t(-1)));
566  if ((it != end(pool)) && (it->time == time)) {
567  // in database and modification time matches,
568  // assume sha1sum also matches
569  return it->sum;
570  }
571 
572  // not in database or timestamp mismatch
573  auto sum = calcSha1sum(file, reactor);
574  if (it == end(pool)) {
575  // was not yet in database, insert new entry
576  insert(sum, time, filename);
577  } else {
578  // was already in database, but with wrong timestamp (and sha1sum)
579  it->setTime(time);
580  adjust(it, sum);
581  }
582  return sum;
583 }
584 
585 int FilePool::signalEvent(const std::shared_ptr<const Event>& event)
586 {
587  (void)event; // avoid warning for non-assert compiles
588  assert(event->getType() == OPENMSX_QUIT_EVENT);
589  quit = true;
590  return 0;
591 }
592 
593 
594 // class Sha1SumCommand
595 
597  CommandController& commandController_, FilePool& filePool_)
598  : Command(commandController_, "sha1sum")
599  , filePool(filePool_)
600 {
601 }
602 
604 {
605  if (tokens.size() != 2) throw SyntaxError();
606  File file(tokens[1].getString());
607  result.setString(filePool.getSha1Sum(file).toString());
608 }
609 
610 string Sha1SumCommand::help(const vector<string>& /*tokens*/) const
611 {
612  return "Calculate sha1 value for the given file. If the file is "
613  "(g)zipped the sha1 is calculated on the unzipped version.";
614 }
615 
616 void Sha1SumCommand::tabCompletion(vector<string>& tokens) const
617 {
619 }
620 
621 } // namespace openmsx
Contains the main loop of openMSX.
Definition: Reactor.hh:63
bool isRegularFile(const Stat &st)
void update(const uint8_t *data, size_t len)
Incrementally calculate the hash value.
Definition: utils/sha1.cc:318
string_ref::const_iterator end(const string_ref &x)
Definition: string_ref.hh:161
const std::string getURL() const
Returns the URL of this file object.
Definition: File.cc:124
void registerEventListener(EventType type, EventListener &listener, Priority priority=OTHER)
Registers a given object to receive certain events.
string help(const vector< string > &tokens) const override
Print help for this command.
Definition: FilePool.cc:610
File getFile(FileType fileType, const Sha1Sum &sha1sum)
Search file with the given sha1sum.
Definition: FilePool.cc:316
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
FileContext systemFileContext()
Definition: FileContext.cc:149
string toString(long long a)
Definition: StringOp.cc:150
void openofstream(std::ofstream &stream, const std::string &filename)
Open an ofstream in a platform-independent manner.
void printWarning(string_ref message)
Definition: CliComm.cc:20
uint8_t byte
8 bit unsigned integer
Definition: openmsx.hh:26
FilePool(CommandController &controler, Reactor &reactor)
Definition: FilePool.cc:66
string join(string_ref part1, string_ref part2)
Join two paths.
unsigned getListLength(Interpreter &interp) const
Definition: TclObject.cc:152
This class implements a subset of the proposal for std::string_ref (proposed for the next c++ standar...
Definition: string_ref.hh:18
void execute(array_ref< TclObject > tokens, TclObject &result) override
Execute this command.
Definition: FilePool.cc:603
Sha1SumCommand(CommandController &commandController, FilePool &filePool)
Definition: FilePool.cc:596
T sum(const vecN< N, T > &x)
Definition: gl_vec.hh:300
EventDistributor & getEventDistributor()
Definition: Reactor.hh:78
void deliverEvents()
This actually delivers the events.
bool getStat(string_ref filename_, Stat &st)
Call stat() and return the stat structure.
static void completeFileName(std::vector< std::string > &tokens, const FileContext &context, const RANGE &extra)
Definition: Completer.hh:122
const std::string & getMessage() const
Definition: MSXException.hh:13
void attach(Observer< T > &observer)
Definition: Subject.hh:52
void repaint()
Redraw the display.
Definition: Display.cc:317
This class represents the result of a sha1 calculation (a 160-bit value).
Definition: sha1.hh:19
This class implements a subset of the proposal for std::array_ref (proposed for the next c++ standard...
Definition: array_ref.hh:19
Sha1Sum digest()
Get the final hash.
Definition: utils/sha1.cc:355
const TclObject & getValue() const final override
Gets the current value of this setting as a TclObject.
Definition: Setting.hh:133
void resize(size_t size)
Grow or shrink the memory block.
Definition: MemBuffer.hh:120
bool is_open() const
Return true iff this file handle refers to an open file.
Definition: File.hh:57
const T * data() const
Returns pointer to the start of the memory buffer.
Definition: MemBuffer.hh:90
string getUserDataDir()
Get the openMSX data dir in the user&#39;s home directory.
time_t getModificationDate()
Get the date/time of last modification.
Definition: File.cc:145
std::string str() const
Definition: string_ref.cc:12
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
TclObject getListIndex(Interpreter &interp, unsigned index) const
Definition: TclObject.cc:170
std::string toString(time_t time)
Definition: Date.cc:152
Helper class to perform a sha1 calculation.
Definition: sha1.hh:78
std::iterator_traits< octet_iterator >::difference_type distance(octet_iterator first, octet_iterator last)
const std::string getOriginalName()
Get Original filename for this object.
Definition: File.cc:134
string_ref getString() const
Definition: TclObject.cc:139
size_t size() const
FileContext userFileContext(string_ref savePath)
Definition: FileContext.cc:161
size_type size() const
Definition: array_ref.hh:61
std::string toString() const
Definition: utils/sha1.cc:229
void addListElement(string_ref element)
Definition: TclObject.cc:69
bool isDirectory(const Stat &st)
void setString(string_ref value)
Definition: TclObject.cc:14
void parse40(const char *str)
Parse from a 40-character long buffer.
Definition: utils/sha1.cc:138
const char *const FILE_CACHE
Definition: FilePool.cc:42
void printProgress(string_ref message)
Definition: CliComm.cc:30
void tabCompletion(vector< string > &tokens) const override
Attempt tab completion for this command.
Definition: FilePool.cc:616
uint8_t * data()
Sha1Sum getSha1Sum(File &file)
Calculate sha1sum for the given File object.
Definition: FilePool.cc:559
time_t fromString(const char *p)
Definition: Date.cc:31
Simple wrapper around openmdir() / readdir() / closedir() functions.
Definition: ReadDir.hh:15
const byte * mmap(size_t &size)
Map file in memory.
Definition: File.cc:89
void detach(Observer< T > &observer)
Definition: Subject.hh:58
string expandTilde(string_ref path)
Expand the &#39;~&#39; character to the users home directory.
CliComm & getCliComm()
Definition: Reactor.cc:272
Interpreter & getInterpreter() const
Definition: Setting.cc:162
Display & getDisplay()
Definition: Reactor.hh:82
uint64_t getTime()
Get current (real) time in us.
Definition: Timer.cc:8
mat4 rotate(float angle, const vec3 &axis)
Definition: gl_transform.hh:56
string_ref::const_iterator begin(const string_ref &x)
Definition: string_ref.hh:160
struct dirent * getEntry()
Get directory entry for next file.
Definition: ReadDir.cc:17
bool empty() const
No memory allocated?
Definition: MemBuffer.hh:112
time_t getModificationDate(const Stat &st)
Get the date/time of last modification.