1 // Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details 2 // Licensed under the terms of the GPL v3. See licenses/GPL-3.txt 3 4 #include "FileSystem.h" 5 #include "StringRange.h" 6 #include "libs.h" 7 #include <algorithm> 8 #include <cassert> 9 #include <iterator> 10 #include <sstream> 11 #include <stdexcept> 12 13 namespace FileSystem { 14 15 static FileSourceFS dataFilesApp(GetDataDir(), true); 16 static FileSourceFS dataFilesUser(JoinPath(GetUserDir(), "data")); 17 FileSourceUnion gameDataFiles; 18 FileSourceFS userFiles(GetUserDir()); 19 20 // note: some functions (GetUserDir(), GetDataDir()) are in FileSystem{Posix,Win32}.cpp SanitiseFileName(const std::string & a)21 std::string SanitiseFileName(const std::string &a) 22 { 23 const char *disabled_chars = "\\/'\":?<>|&*"; 24 std::ostringstream ss; 25 if (a.empty() || (a[0] == '.')) { 26 ss << "x"; 27 } 28 29 for (const char *c = a.c_str(), *end = c + a.size(); c != end; ++c) { 30 if (strchr(disabled_chars, *c) || (*c < ' ')) { 31 ss << "_"; 32 ss.setf(std::ios::hex, std::ios::basefield); 33 ss.fill('0'); 34 ss.width(2); 35 ss << int(*c); 36 } else if (*c == ' ') { 37 ss << '_'; 38 } else { 39 ss << *c; 40 } 41 } 42 43 return ss.str(); 44 } 45 JoinPath(const std::string & a,const std::string & b)46 std::string JoinPath(const std::string &a, const std::string &b) 47 { 48 if (!b.empty()) { 49 if (b[0] == '/' || a.empty()) 50 return b; 51 else 52 return a + "/" + b; 53 } else 54 return a; 55 } 56 normalise_path(std::string & result,const StringRange & path)57 static void normalise_path(std::string &result, const StringRange &path) 58 { 59 StringRange part(path.begin, path.end); 60 if (!path.Empty() && (path[0] == '/')) { 61 result += '/'; 62 ++part.begin; 63 } 64 const size_t initial_result_length = result.size(); 65 while (true) { 66 part.end = part.FindChar('/'); // returns part.end if the char is not found 67 if (part.Empty() || (part == ".")) { 68 // skip this part 69 } else if (part == "..") { 70 // pop the last component 71 if (result.size() <= initial_result_length) 72 throw std::invalid_argument(path.ToString()); 73 size_t pos = result.rfind('/'); 74 if (pos == std::string::npos) { 75 pos = 0; 76 } 77 assert(pos >= initial_result_length); 78 result.erase(pos); 79 } else { 80 // push the new component 81 if (result.size() > initial_result_length) 82 result += '/'; 83 result.append(part.begin, part.Size()); 84 } 85 if (part.end == path.end) { 86 break; 87 } 88 assert(*part.end == '/'); 89 part.begin = part.end + 1; 90 part.end = path.end; 91 } 92 } 93 NormalisePath(const std::string & path)94 std::string NormalisePath(const std::string &path) 95 { 96 std::string result; 97 result.reserve(path.size()); 98 normalise_path(result, StringRange(path.c_str(), path.size())); 99 return result; 100 } 101 JoinPathBelow(const std::string & base,const std::string & path)102 std::string JoinPathBelow(const std::string &base, const std::string &path) 103 { 104 if (base.empty()) 105 return path; 106 if (!path.empty()) { 107 if ((path[0] == '/') && (base != "/")) 108 throw std::invalid_argument(path); 109 else { 110 std::string result(base); 111 result.reserve(result.size() + 1 + path.size()); 112 if (result[result.size() - 1] != '/') 113 result += '/'; 114 StringRange rhs(path.c_str(), path.size()); 115 if (path[0] == '/') { 116 assert(base == "/"); 117 ++rhs.begin; 118 } 119 normalise_path(result, rhs); 120 return result; 121 } 122 } else 123 return base; 124 } 125 GetRelativePath(const std::string & base,const std::string & path)126 std::string GetRelativePath(const std::string &base, const std::string &path) 127 { 128 // catch all common errors early 129 if (base.empty() || path.empty() || path.size() < base.size()) 130 return path; 131 132 // can't strip a non-root prefix from a root path and vice-versa 133 if ((path[0] == '/' || base[0] == '/') && (path[0] != base[0])) 134 return path; 135 136 // check if <path> exactly starts with <base> 137 if (path.compare(0, base.size(), base) == 0) { 138 if (path.size() == base.size()) 139 return ""; // strip the entire base and return an empty path 140 else if (path[base.size()] == '/') 141 return path.substr(base.size() + 1); // strip the base and the trailing separator 142 } 143 144 // if <path> isn't relative to <base>, return the original <path> 145 return path; 146 } 147 CopyDir(FileSource & sourceFS,std::string sourceDir,FileSourceFS & targetFS,std::string targetDir,FileSystem::CopyMode copymode)148 bool CopyDir(FileSource &sourceFS, std::string sourceDir, FileSourceFS &targetFS, std::string targetDir, FileSystem::CopyMode copymode) 149 { 150 // NOTE: copymode var is not used, because only mode ONLY_MISSING_IN_TARGET is implemented 151 if (!sourceFS.Lookup(sourceDir).IsDir() || !targetFS.Lookup(targetDir).IsDir()) 152 return false; 153 154 // collect files, that are already in the target 155 // NOTE: modification time (in map value) probably will be needed in another mode 156 std::map<std::string, Time::DateTime> targetFiles; 157 if (copymode != CopyMode::OVERWRITE) { 158 // don't bother collecting target file data if we're overwriting 159 for (FileSystem::FileEnumerator files(targetFS, targetDir, FileSystem::FileEnumerator::Recurse | FileSystem::FileEnumerator::IncludeDirs); !files.Finished(); files.Next()) 160 targetFiles[files.Current().GetPath()] = files.Current().GetModificationTime(); 161 } 162 163 for (FileSystem::FileEnumerator files(sourceFS, sourceDir, FileSystem::FileEnumerator::Recurse | FileSystem::FileEnumerator::IncludeDirs); !files.Finished(); files.Next()) { 164 const FileSystem::FileInfo &info = files.Current(); 165 const std::string targetPath = FileSystem::JoinPathBelow(targetDir, FileSystem::GetRelativePath(sourceDir, info.GetPath())); 166 const auto &oldFile = targetFiles.find(targetPath); 167 if (oldFile == targetFiles.end()) { // there is no such file (or dir) in the target 168 if (info.IsFile()) { 169 //copy file 170 RefCountedPtr<FileData> fileData = info.Read(); 171 FILE *outfile = targetFS.OpenWriteStream(targetPath); 172 fwrite(fileData->GetData(), 1, fileData->GetSize(), outfile); 173 fclose(outfile); 174 // Output("copy %s to %s\n", info.GetAbsolutePath(), FileSystem::JoinPath(targetFS.GetRoot(), targetPath)); 175 } else if (info.IsDir()) { 176 //create the subdir 177 targetFS.MakeDirectory(targetPath); 178 // Output("create dir %s\n", FileSystem::JoinPath(targetFS.GetRoot(), targetPath)); 179 } 180 } else 181 //this file is no longer needed when searching 182 targetFiles.erase(oldFile); 183 } 184 185 return true; 186 } 187 Init()188 void Init() 189 { 190 gameDataFiles.AppendSource(&dataFilesUser); 191 gameDataFiles.AppendSource(&dataFilesApp); 192 } 193 Uninit()194 void Uninit() 195 { 196 } 197 FileInfo(FileSource * source,const std::string & path,FileType type,Time::DateTime modTime)198 FileInfo::FileInfo(FileSource *source, const std::string &path, FileType type, Time::DateTime modTime) : 199 m_source(source), 200 m_path(path), 201 m_modTime(modTime), 202 m_dirLen(0), 203 m_type(type) 204 { 205 assert((m_path.size() <= 1) || (m_path[m_path.size() - 1] != '/')); 206 std::size_t slashpos = m_path.rfind('/'); 207 if (slashpos != std::string::npos) { 208 m_dirLen = slashpos + 1; 209 } else { 210 m_dirLen = 0; 211 } 212 } 213 MakeFileInfo(const std::string & path,FileInfo::FileType fileType,Time::DateTime modTime)214 FileInfo FileSource::MakeFileInfo(const std::string &path, FileInfo::FileType fileType, Time::DateTime modTime) 215 { 216 return FileInfo(this, path, fileType, modTime); 217 } 218 MakeFileInfo(const std::string & path,FileInfo::FileType fileType)219 FileInfo FileSource::MakeFileInfo(const std::string &path, FileInfo::FileType fileType) 220 { 221 return MakeFileInfo(path, fileType, Time::DateTime()); 222 } 223 FileSourceUnion()224 FileSourceUnion::FileSourceUnion() : 225 FileSource(":union:") {} ~FileSourceUnion()226 FileSourceUnion::~FileSourceUnion() {} 227 PrependSource(FileSource * fs)228 void FileSourceUnion::PrependSource(FileSource *fs) 229 { 230 assert(fs); 231 RemoveSource(fs); 232 m_sources.insert(m_sources.begin(), fs); 233 } 234 AppendSource(FileSource * fs)235 void FileSourceUnion::AppendSource(FileSource *fs) 236 { 237 assert(fs); 238 RemoveSource(fs); 239 m_sources.push_back(fs); 240 } 241 RemoveSource(FileSource * fs)242 void FileSourceUnion::RemoveSource(FileSource *fs) 243 { 244 std::vector<FileSource *>::iterator nend = std::remove(m_sources.begin(), m_sources.end(), fs); 245 m_sources.erase(nend, m_sources.end()); 246 } 247 Lookup(const std::string & path)248 FileInfo FileSourceUnion::Lookup(const std::string &path) 249 { 250 for (std::vector<FileSource *>::const_iterator 251 it = m_sources.begin(); 252 it != m_sources.end(); ++it) { 253 FileInfo info = (*it)->Lookup(path); 254 if (info.Exists()) { 255 return info; 256 } 257 } 258 return MakeFileInfo(path, FileInfo::FT_NON_EXISTENT); 259 } 260 LookupAll(const std::string & path)261 std::vector<FileInfo> FileSourceUnion::LookupAll(const std::string &path) 262 { 263 std::vector<FileInfo> outFiles; 264 265 for (FileSource *fs : m_sources) { 266 FileInfo info = fs->Lookup(path); 267 if (info.Exists()) outFiles.push_back(info); 268 } 269 270 return outFiles; 271 } 272 ReadFile(const std::string & path)273 RefCountedPtr<FileData> FileSourceUnion::ReadFile(const std::string &path) 274 { 275 for (std::vector<FileSource *>::const_iterator 276 it = m_sources.begin(); 277 it != m_sources.end(); ++it) { 278 RefCountedPtr<FileData> data = (*it)->ReadFile(path); 279 if (data) { 280 return data; 281 } 282 } 283 return RefCountedPtr<FileData>(); 284 } 285 286 // Merge two sets of FileInfo's, by path. 287 // Input vectors must be sorted. Output will be sorted. 288 // Where a path is present in both inputs, directories are selected 289 // in preference to non-directories; otherwise, the FileInfo from the 290 // first vector is selected in preference to the second vector. 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)291 static void file_union_merge( 292 std::vector<FileInfo>::const_iterator a, std::vector<FileInfo>::const_iterator aend, 293 std::vector<FileInfo>::const_iterator b, std::vector<FileInfo>::const_iterator bend, 294 std::vector<FileInfo> &output) 295 { 296 while ((a != aend) && (b != bend)) { 297 int order = a->GetPath().compare(b->GetPath()); 298 int which = order; 299 if (which == 0) { 300 if (b->IsDir() && !a->IsDir()) { 301 which = 1; 302 } else { 303 which = -1; 304 } 305 } 306 if (which < 0) { 307 output.push_back(*a++); 308 if (order == 0) ++b; 309 } else { 310 output.push_back(*b++); 311 if (order == 0) ++a; 312 } 313 } 314 315 if (a != aend) { 316 std::copy(a, aend, std::back_inserter(output)); 317 } 318 if (b != bend) { 319 std::copy(b, bend, std::back_inserter(output)); 320 } 321 } 322 ReadDirectory(const std::string & path,std::vector<FileInfo> & output)323 bool FileSourceUnion::ReadDirectory(const std::string &path, std::vector<FileInfo> &output) 324 { 325 if (m_sources.empty()) { 326 return false; 327 } 328 if (m_sources.size() == 1) { 329 return m_sources.front()->ReadDirectory(path, output); 330 } 331 332 bool founddir = false; 333 334 std::vector<FileInfo> merged; 335 for (std::vector<FileSource *>::const_iterator 336 it = m_sources.begin(); 337 it != m_sources.end(); ++it) { 338 std::vector<FileInfo> nextfiles; 339 if ((*it)->ReadDirectory(path, nextfiles)) { 340 founddir = true; 341 342 std::vector<FileInfo> prevfiles; 343 prevfiles.swap(merged); 344 // merge order is important 345 // file_union_merge selects from its first input preferentially 346 file_union_merge( 347 prevfiles.begin(), prevfiles.end(), 348 nextfiles.begin(), nextfiles.end(), 349 merged); 350 } 351 } 352 353 output.reserve(output.size() + merged.size()); 354 std::copy(merged.begin(), merged.end(), std::back_inserter(output)); 355 356 return founddir; 357 } 358 FileEnumerator(FileSource & fs,int flags)359 FileEnumerator::FileEnumerator(FileSource &fs, int flags) : 360 m_source(&fs), 361 m_flags(flags) {} 362 FileEnumerator(FileSource & fs,const std::string & path,int flags)363 FileEnumerator::FileEnumerator(FileSource &fs, const std::string &path, int flags) : 364 m_source(&fs), 365 m_flags(flags) 366 { 367 AddSearchRoot(path); 368 } 369 ~FileEnumerator()370 FileEnumerator::~FileEnumerator() {} 371 AddSearchRoot(const std::string & path)372 void FileEnumerator::AddSearchRoot(const std::string &path) 373 { 374 const FileInfo fi = m_source->Lookup(path); 375 if (fi.IsDir()) { 376 QueueDirectoryContents(fi); 377 ExpandDirQueue(); 378 } 379 } 380 Next()381 void FileEnumerator::Next() 382 { 383 m_queue.pop_front(); 384 ExpandDirQueue(); 385 } 386 ExpandDirQueue()387 void FileEnumerator::ExpandDirQueue() 388 { 389 while (m_queue.empty() && !m_dirQueue.empty()) { 390 const FileInfo &nextDir = m_dirQueue.front(); 391 assert(nextDir.IsDir()); 392 QueueDirectoryContents(nextDir); 393 m_dirQueue.pop_front(); 394 } 395 } 396 QueueDirectoryContents(const FileInfo & info)397 void FileEnumerator::QueueDirectoryContents(const FileInfo &info) 398 { 399 assert(info.IsDir()); 400 401 std::vector<FileInfo> entries; 402 m_source->ReadDirectory(info.GetPath(), entries); 403 for (std::vector<FileInfo>::const_iterator 404 it = entries.begin(); 405 it != entries.end(); ++it) { 406 407 switch (it->GetType()) { 408 case FileInfo::FT_DIR: 409 if (m_flags & IncludeDirs) { 410 m_queue.push_back(*it); 411 } 412 if (m_flags & Recurse) { 413 m_dirQueue.push_back(*it); 414 } 415 break; 416 case FileInfo::FT_FILE: 417 if (!(m_flags & ExcludeFiles)) { 418 m_queue.push_back(*it); 419 } 420 break; 421 case FileInfo::FT_SPECIAL: 422 if (m_flags & IncludeSpecials) { 423 m_queue.push_back(*it); 424 } 425 break; 426 default: assert(0); break; 427 } 428 } 429 } 430 431 } // namespace FileSystem 432