/************************************************************************* ** FilePath.cpp ** ** ** ** This file is part of dvisvgm -- a fast DVI to SVG converter ** ** Copyright (C) 2005-2024 Martin Gieseking ** ** ** ** This program is free software; you can redistribute it and/or ** ** modify it under the terms of the GNU General Public License as ** ** published by the Free Software Foundation; either version 3 of ** ** the License, or (at your option) any later version. ** ** ** ** This program is distributed in the hope that it will be useful, but ** ** WITHOUT ANY WARRANTY; without even the implied warranty of ** ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** ** GNU General Public License for more details. ** ** ** ** You should have received a copy of the GNU General Public License ** ** along with this program; if not, see . ** *************************************************************************/ #include #include "FilePath.hpp" #include "FileSystem.hpp" #include "MessageException.hpp" #include "utility.hpp" using namespace std; /** Removes redundant slashes from a given path. */ static string& single_slashes (string &str) { string::size_type pos=0; while ((pos = str.find("//", pos)) != string::npos) str.replace(pos, 2, "/"); return str; } #ifdef _WIN32 /** Returns the drive letter of a given path string or 0 if there's none. */ static char drive_letter (const string &path) { if (path.length() >= 2 && path[1] == ':' && isalpha(path[0])) return tolower(path[0]); return '\0'; } /** Removes the drive letter and following colon from a given path string if present. * @param[in] path path to strip drive letter from * @return drive letter or 0 if there was none */ static char strip_drive_letter (string &path) { char letter = '\0'; if (path.length() >= 2 && path[1] == ':' && isalpha(path[0])) { letter = path[0]; path.erase(0, 2); } return tolower(letter); } #endif bool FilePath::Directory::operator == (const Directory &dir) const { #ifdef _WIN32 // letter case is not significant on Windows systems return util::tolower(_dirstr) == util::tolower(dir._dirstr); #else return _dirstr == dir._dirstr; #endif } /** Constructs a FilePath object. * @param[in] path absolute or relative path to a file or directory * @param[in] isfile true if 'path' references a file, false if a directory is referenced * @param[in] current_dir if 'path' is a relative path expression it will be related to 'current_dir' */ FilePath::FilePath (const string &path, bool isfile, const string ¤t_dir) { init(path, isfile, current_dir); } /** Assigns a new path of a file or directory that already exists. * Relative paths are relative to the current working directory. * @param[in] path absolute or relative path to a file or directory */ void FilePath::set (const string &path) { set(path, !FileSystem::isDirectory(path)); } /** Assigns a new path. Relative paths are relative to the current working directory. * @param[in] path absolute or relative path to a file or directory * @param[in] isfile true if 'path' references a file, false if a directory is referenced */ void FilePath::set (const string &path, bool isfile) { init(path, isfile, ""); } /** Assigns a new path. Relative paths are relative to the current working directory. * @param[in] path absolute or relative path to a file or directory * @param[in] isfile true if 'path' references a file, false if a directory is referenced * @param[in] current_dir if 'path' is a relative path expression it will be related to 'current_dir' */ void FilePath::set (const string &path, bool isfile, const string ¤t_dir) { init(path, isfile, current_dir); } /** Initializes a FilePath object. This method should be called by the constructors only. * @param[in] path absolute or relative path to a file or directory * @param[in] isfile true if 'path' references a file, false if a directory is referenced * @param[in] current_dir if 'path' is a relative path expression it will be related to 'current_dir' */ void FilePath::init (string path, bool isfile, string current_dir) { _dirs.clear(); _fname.clear(); single_slashes(path); single_slashes(current_dir); #ifndef _WIN32 if (current_dir.empty()) current_dir = FileSystem::getcwd(); #else _drive = strip_drive_letter(path); if (current_dir.empty() || drive_letter(current_dir) != _drive) current_dir = FileSystem::getcwd(_drive); if (!_drive) _drive = drive_letter(current_dir); strip_drive_letter(current_dir); path = FileSystem::ensureForwardSlashes(path); #endif if (isfile) { auto pos = path.rfind('/'); _fname = path.substr((pos == string::npos) ? 0 : pos+1); // remove filename from path if (pos == 0 && _fname.length() > 1) // file in root directory? path.erase(1); else if (pos != string::npos) path.erase(pos); else path.clear(); } if ((path.empty() || path[0] != '/') && !current_dir.empty()) path.insert(0, current_dir + "/"); string elem; for (char c : path) { if (c != '/') elem += c; else if (!elem.empty()){ add(elem); elem.clear(); } } if (!elem.empty()) add(elem); } /** Adds a location step to the current path. */ void FilePath::add (const string &dir) { if (dir == ".." && !_dirs.empty()) _dirs.pop_back(); else if (!dir.empty() && dir != ".") _dirs.emplace_back(dir); } /** Returns the suffix of the filename. If FilePath represents the * location of a directory (and not of a file) an empty string * is returned. */ string FilePath::suffix () const { string::size_type start = 0; // ignore leading dots while (start < _fname.length() && _fname[start] == '.') start++; string sub = _fname.substr(start); auto pos = sub.rfind('.'); if (pos != string::npos && pos < sub.length()-1) return sub.substr(pos+1); return ""; } /** Changes the suffix of the filename. If FilePath represents the * location of a directory (and not of a file) nothing happens. An * empty new suffix leads to the removal of the current one. * @param[in] newSuffix the new suffix */ void FilePath::suffix (const string &newSuffix) { if (!_fname.empty()) { string current_suffix = suffix(); if (!current_suffix.empty()) _fname.erase(_fname.length()-current_suffix.length()-1); if (!newSuffix.empty()) _fname += "."+newSuffix; } } /** Returns the filename without suffix. * Example: FilePath("/a/b/c.def", true) == "c" */ string FilePath::basename () const { if (!_fname.empty()) { auto len = suffix().length(); if (len > 0) len++; // strip dot too return _fname.substr(0, _fname.length()-len); } return ""; } /** Returns the absolute path string of this FilePath. * @param[in] with_filename if false, the filename is omitted * @return the absolute path string */ string FilePath::absolute (bool with_filename) const { string path; #ifdef _WIN32 if (_drive) path = string(1, _drive) + ":"; #endif for (const Directory &dir : _dirs) path += "/" + string(dir); if (path.empty()) path = "/"; if (with_filename && !_fname.empty()) path += "/"+_fname; return single_slashes(path); } /** Returns a path string of this FilePath relative to reldir. If we wanted to * navigate from /a/b/c/d to /a/b/e/f using the shell command "cd", we could do that * with the relative path argument: "cd ../../e/f". This function returns such a relative * path. Example: FilePath("/a/b/e/f").relative("/a/b/c/d") => "../../e/f". * @param[in] reldir absolute path to a directory * @param[in] with_filename if false, the filename is omitted * @return the relative path string */ string FilePath::relative (string reldir, bool with_filename) const { #ifdef _WIN32 char reldrive = drive_letter(reldir); if (reldir.empty()) { reldir = FileSystem::getcwd(_drive); reldrive = drive_letter(FileSystem::getcwd()); } bool isAbsolute = (reldir[0] == '/') || (reldir.length() >= 3 && isalpha(reldir[0]) && reldir[1] == ':' && reldir[2] == '/'); #else if (reldir.empty()) reldir = FileSystem::getcwd(); bool isAbsolute = (reldir[0] == '/'); #endif if (!isAbsolute) return absolute(); FilePath relpath(reldir, false); string path; auto it1 = _dirs.begin(); auto it2 = relpath._dirs.begin(); while (it1 != _dirs.end() && it2 != relpath._dirs.end() && *it1 == *it2) ++it1, ++it2; for (; it2 != relpath._dirs.end(); ++it2) path += "../"; for (; it1 != _dirs.end(); ++it1) path += string(*it1) + "/"; if (!path.empty() && path.back() == '/') path.erase(path.length()-1, 1); // remove trailing slash if (with_filename && !_fname.empty()) { if (!path.empty() && path != "/") path += "/"; path += _fname; } if (path.empty()) path = "."; #ifdef _WIN32 if (relpath._drive && _drive && _drive != reldrive) path.insert(0, string(1, _drive) + ":"); #endif return single_slashes(path); } string FilePath::relative (const FilePath &filepath, bool with_filename) const { return relative(filepath.absolute(false), with_filename); } /** Return the absolute or relative path whichever is shorter. * @param[in] reldir absolute path to a directory * @param[in] with_filename if false, the filename is omitted */ string FilePath::shorterAbsoluteOrRelative (string reldir, bool with_filename) const { string abs = absolute(with_filename); string rel = relative(reldir, with_filename); return abs.length() < rel.length() ? abs : rel; } bool FilePath::exists () const { return empty() ? false : FileSystem::exists(absolute()); } /** Checks if a given path is absolute or relative. * @param[in] path path string to check * @return true if path is absolute */ bool FilePath::isAbsolute (string path) { path = util::trim(path); #ifdef _WIN32 path = FileSystem::ensureForwardSlashes(path); if (path.length() >= 2 && path[1] == ':' && isalpha(path[0])) path.erase(0, 2); // remove drive letter and colon #endif return !path.empty() && path[0] == '/'; }