pioneer/src/FileSystem.cpp

370 lines
8.9 KiB
C++

// Copyright © 2008-2020 Pioneer Developers. See AUTHORS.txt for details
// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
#include "FileSystem.h"
#include "StringRange.h"
#include "libs.h"
#include <algorithm>
#include <cassert>
#include <iterator>
#include <sstream>
#include <stdexcept>
namespace FileSystem {
static FileSourceFS dataFilesApp(GetDataDir(), true);
static FileSourceFS dataFilesUser(JoinPath(GetUserDir(), "data"));
FileSourceUnion gameDataFiles;
FileSourceFS userFiles(GetUserDir());
// note: some functions (GetUserDir(), GetDataDir()) are in FileSystem{Posix,Win32}.cpp
std::string SanitiseFileName(const std::string &a)
{
const char *disabled_chars = "\\/'\":?<>|&*";
std::ostringstream ss;
if (a.empty() || (a[0] == '.')) {
ss << "x";
}
for (const char *c = a.c_str(), *end = c + a.size(); c != end; ++c) {
if (strchr(disabled_chars, *c) || (*c < ' ')) {
ss << "_";
ss.setf(std::ios::hex, std::ios::basefield);
ss.fill('0');
ss.width(2);
ss << int(*c);
} else if (*c == ' ') {
ss << '_';
} else {
ss << *c;
}
}
return ss.str();
}
std::string JoinPath(const std::string &a, const std::string &b)
{
if (!b.empty()) {
if (b[0] == '/' || a.empty())
return b;
else
return a + "/" + b;
} else
return a;
}
static void normalise_path(std::string &result, const StringRange &path)
{
StringRange part(path.begin, path.end);
if (!path.Empty() && (path[0] == '/')) {
result += '/';
++part.begin;
}
const size_t initial_result_length = result.size();
while (true) {
part.end = part.FindChar('/'); // returns part.end if the char is not found
if (part.Empty() || (part == ".")) {
// skip this part
} else if (part == "..") {
// pop the last component
if (result.size() <= initial_result_length)
throw std::invalid_argument(path.ToString());
size_t pos = result.rfind('/');
if (pos == std::string::npos) {
pos = 0;
}
assert(pos >= initial_result_length);
result.erase(pos);
} else {
// push the new component
if (result.size() > initial_result_length)
result += '/';
result.append(part.begin, part.Size());
}
if (part.end == path.end) {
break;
}
assert(*part.end == '/');
part.begin = part.end + 1;
part.end = path.end;
}
}
std::string NormalisePath(const std::string &path)
{
std::string result;
result.reserve(path.size());
normalise_path(result, StringRange(path.c_str(), path.size()));
return result;
}
std::string JoinPathBelow(const std::string &base, const std::string &path)
{
if (base.empty())
return path;
if (!path.empty()) {
if ((path[0] == '/') && (base != "/"))
throw std::invalid_argument(path);
else {
std::string result(base);
result.reserve(result.size() + 1 + path.size());
if (result[result.size() - 1] != '/')
result += '/';
StringRange rhs(path.c_str(), path.size());
if (path[0] == '/') {
assert(base == "/");
++rhs.begin;
}
normalise_path(result, rhs);
return result;
}
} else
return base;
}
void Init()
{
gameDataFiles.AppendSource(&dataFilesUser);
gameDataFiles.AppendSource(&dataFilesApp);
}
void Uninit()
{
}
FileInfo::FileInfo(FileSource *source, const std::string &path, FileType type, Time::DateTime modTime) :
m_source(source),
m_path(path),
m_modTime(modTime),
m_dirLen(0),
m_type(type)
{
assert((m_path.size() <= 1) || (m_path[m_path.size() - 1] != '/'));
std::size_t slashpos = m_path.rfind('/');
if (slashpos != std::string::npos) {
m_dirLen = slashpos + 1;
} else {
m_dirLen = 0;
}
}
FileInfo FileSource::MakeFileInfo(const std::string &path, FileInfo::FileType fileType, Time::DateTime modTime)
{
return FileInfo(this, path, fileType, modTime);
}
FileInfo FileSource::MakeFileInfo(const std::string &path, FileInfo::FileType fileType)
{
return MakeFileInfo(path, fileType, Time::DateTime());
}
FileSourceUnion::FileSourceUnion() :
FileSource(":union:") {}
FileSourceUnion::~FileSourceUnion() {}
void FileSourceUnion::PrependSource(FileSource *fs)
{
assert(fs);
RemoveSource(fs);
m_sources.insert(m_sources.begin(), fs);
}
void FileSourceUnion::AppendSource(FileSource *fs)
{
assert(fs);
RemoveSource(fs);
m_sources.push_back(fs);
}
void FileSourceUnion::RemoveSource(FileSource *fs)
{
std::vector<FileSource *>::iterator nend = std::remove(m_sources.begin(), m_sources.end(), fs);
m_sources.erase(nend, m_sources.end());
}
FileInfo FileSourceUnion::Lookup(const std::string &path)
{
for (std::vector<FileSource *>::const_iterator
it = m_sources.begin();
it != m_sources.end(); ++it) {
FileInfo info = (*it)->Lookup(path);
if (info.Exists()) {
return info;
}
}
return MakeFileInfo(path, FileInfo::FT_NON_EXISTENT);
}
std::vector<FileInfo> FileSourceUnion::LookupAll(const std::string &path)
{
std::vector<FileInfo> outFiles;
for (FileSource *fs : m_sources) {
FileInfo info = fs->Lookup(path);
if (info.Exists()) outFiles.push_back(info);
}
return outFiles;
}
RefCountedPtr<FileData> FileSourceUnion::ReadFile(const std::string &path)
{
for (std::vector<FileSource *>::const_iterator
it = m_sources.begin();
it != m_sources.end(); ++it) {
RefCountedPtr<FileData> data = (*it)->ReadFile(path);
if (data) {
return data;
}
}
return RefCountedPtr<FileData>();
}
// Merge two sets of FileInfo's, by path.
// Input vectors must be sorted. Output will be sorted.
// Where a path is present in both inputs, directories are selected
// in preference to non-directories; otherwise, the FileInfo from the
// first vector is selected in preference to the second vector.
static void file_union_merge(
std::vector<FileInfo>::const_iterator a, std::vector<FileInfo>::const_iterator aend,
std::vector<FileInfo>::const_iterator b, std::vector<FileInfo>::const_iterator bend,
std::vector<FileInfo> &output)
{
while ((a != aend) && (b != bend)) {
int order = a->GetPath().compare(b->GetPath());
int which = order;
if (which == 0) {
if (b->IsDir() && !a->IsDir()) {
which = 1;
} else {
which = -1;
}
}
if (which < 0) {
output.push_back(*a++);
if (order == 0) ++b;
} else {
output.push_back(*b++);
if (order == 0) ++a;
}
}
if (a != aend) {
std::copy(a, aend, std::back_inserter(output));
}
if (b != bend) {
std::copy(b, bend, std::back_inserter(output));
}
}
bool FileSourceUnion::ReadDirectory(const std::string &path, std::vector<FileInfo> &output)
{
if (m_sources.empty()) {
return false;
}
if (m_sources.size() == 1) {
return m_sources.front()->ReadDirectory(path, output);
}
bool founddir = false;
std::vector<FileInfo> merged;
for (std::vector<FileSource *>::const_iterator
it = m_sources.begin();
it != m_sources.end(); ++it) {
std::vector<FileInfo> nextfiles;
if ((*it)->ReadDirectory(path, nextfiles)) {
founddir = true;
std::vector<FileInfo> prevfiles;
prevfiles.swap(merged);
// merge order is important
// file_union_merge selects from its first input preferentially
file_union_merge(
prevfiles.begin(), prevfiles.end(),
nextfiles.begin(), nextfiles.end(),
merged);
}
}
output.reserve(output.size() + merged.size());
std::copy(merged.begin(), merged.end(), std::back_inserter(output));
return founddir;
}
FileEnumerator::FileEnumerator(FileSource &fs, int flags) :
m_source(&fs),
m_flags(flags) {}
FileEnumerator::FileEnumerator(FileSource &fs, const std::string &path, int flags) :
m_source(&fs),
m_flags(flags)
{
AddSearchRoot(path);
}
FileEnumerator::~FileEnumerator() {}
void FileEnumerator::AddSearchRoot(const std::string &path)
{
const FileInfo fi = m_source->Lookup(path);
if (fi.IsDir()) {
QueueDirectoryContents(fi);
ExpandDirQueue();
}
}
void FileEnumerator::Next()
{
m_queue.pop_front();
ExpandDirQueue();
}
void FileEnumerator::ExpandDirQueue()
{
while (m_queue.empty() && !m_dirQueue.empty()) {
const FileInfo &nextDir = m_dirQueue.front();
assert(nextDir.IsDir());
QueueDirectoryContents(nextDir);
m_dirQueue.pop_front();
}
}
void FileEnumerator::QueueDirectoryContents(const FileInfo &info)
{
assert(info.IsDir());
std::vector<FileInfo> entries;
m_source->ReadDirectory(info.GetPath(), entries);
for (std::vector<FileInfo>::const_iterator
it = entries.begin();
it != entries.end(); ++it) {
switch (it->GetType()) {
case FileInfo::FT_DIR:
if (m_flags & IncludeDirs) {
m_queue.push_back(*it);
}
if (m_flags & Recurse) {
m_dirQueue.push_back(*it);
}
break;
case FileInfo::FT_FILE:
if (!(m_flags & ExcludeFiles)) {
m_queue.push_back(*it);
}
break;
case FileInfo::FT_SPECIAL:
if (m_flags & IncludeSpecials) {
m_queue.push_back(*it);
}
break;
default: assert(0); break;
}
}
}
} // namespace FileSystem