Skip to content

Commit

Permalink
Fix get_canonical/absolute/normalize_path() functions (fix #98)
Browse files Browse the repository at this point in the history
Several refactors in this commit:

- Moved is_path_separator() function and path_separator as a constexpr
  to the header file
- Added path_separators string (for Windows, which includes \ and /)
- get_canonical_path() works only when the file exists (returns an
  empty string in other case)
- normalize_path() and get_absolute_path() work in a lexical level,
  handling only strings and getting the current path when needed, but
  without checking the existence of the file
  • Loading branch information
dacap committed Jul 3, 2024
1 parent 16b33ec commit e965896
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 88 deletions.
117 changes: 77 additions & 40 deletions base/fs.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// LAF Base Library
// Copyright (c) 2021-2022 Igara Studio S.A.
// Copyright (c) 2021-2024 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
Expand Down Expand Up @@ -27,16 +27,18 @@

namespace base {

// On Windows we can use \ or / as path separators, but on Unix-like
// platforms it's just /, as \ can be part of the file name.
#if LAF_WINDOWS
const std::string::value_type path_separator = '\\';
const std::string::value_type* path_separators = "\\/";
#else
const std::string::value_type path_separator = '/';
const std::string::value_type* path_separators = "/";
#endif

void make_all_directories(const std::string& path)
{
std::vector<std::string> parts;
split_string(path, parts, "/\\");
split_string(path, parts, path_separators);

std::string intermediate;
for (const std::string& component : parts) {
Expand All @@ -55,31 +57,6 @@ void make_all_directories(const std::string& path)
}
}

std::string get_absolute_path(const std::string& filename)
{
std::string fn = filename;
if (fn.size() > 2 &&
#if LAF_WINDOWS
fn[1] != ':'
#else
fn[0] != '/'
#endif
) {
fn = base::join_path(base::get_current_path(), fn);
}
fn = base::get_canonical_path(fn);
return fn;
}

bool is_path_separator(std::string::value_type chr)
{
return (
#if LAF_WINDOWS
chr == '\\' ||
#endif
chr == '/');
}

std::string get_file_path(const std::string& filename)
{
std::string::const_reverse_iterator rit;
Expand Down Expand Up @@ -213,10 +190,10 @@ std::string get_file_title_with_path(const std::string& filename)
std::string get_relative_path(const std::string& filename, const std::string& base_path)
{
std::vector<std::string> baseDirs;
split_string(base_path, baseDirs, "/\\");
split_string(base_path, baseDirs, path_separators);

std::vector<std::string> toParts;
split_string(filename, toParts, "/\\");
split_string(filename, toParts, path_separators);

// Find the common prefix
auto itFrom = baseDirs.begin();
Expand Down Expand Up @@ -270,19 +247,79 @@ std::string remove_path_separator(const std::string& path)

std::string fix_path_separators(const std::string& filename)
{
std::string result(filename);

// Replace any separator with the system path separator.
std::replace_if(result.begin(), result.end(),
is_path_separator, path_separator);

std::string result;
result.reserve(filename.size());
for (auto chr : filename) {
if (is_path_separator(chr)) {
if (result.empty() || !is_path_separator(result.back()))
result.push_back(path_separator);
}
else
result.push_back(chr);
}
return result;
}

std::string normalize_path(const std::string& filename)
// It tries to replicate the standard path::lexically_normal()
// algorithm from https://en.cppreference.com/w/cpp/filesystem/path
std::string normalize_path(const std::string& _path)
{
std::string fn = base::get_canonical_path(filename);
fn = base::fix_path_separators(fn);
// Normal form of an empty path is an empty path.
if (_path.empty())
return std::string();

// Replace multiple slashes with a single path_separator.
std::string path = fix_path_separators(_path);

std::string fn;
if (!path.empty() && path[0] == path_separator)
fn.push_back(path_separator);

std::vector<std::string> parts;
split_string(path, parts, path_separators);

// Last element generates a final dot or slash in normalized path.
bool last_dot = false;

auto n = int(parts.size());
for (int i=0; i<n; ++i) {
const auto& part = parts[i];

// Remove each dot part.
if (part == ".") {
last_dot = true;

if (i+1 == n)
break;

fn = join_path(fn, std::string());
continue;
}

if (!part.empty())
last_dot = false;

if (part != ".." && i+1 < n &&
parts[i+1] == "..") {
// Skip this "part/.."
++i;
last_dot = true;
}
else if (!part.empty()) {
fn = join_path(fn, part);
}
else
last_dot = true;
}
if (last_dot) {
if (fn.empty())
fn = ".";
else if (fn.back() != path_separator &&
// Don't include trailing slash for ".." filename
get_file_name(fn) != "..") {
fn.push_back(path_separator);
}
}
return fn;
}

Expand Down
36 changes: 26 additions & 10 deletions base/fs.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ namespace base {

class Time;

// Default path separator (on Windows it is '\' and on Unix-like systems it is '/').
extern const std::string::value_type path_separator;
// Default path separator (on Windows it is '\' and on Unix-like
// systems it is '/').
#if LAF_WINDOWS
static constexpr const std::string::value_type path_separator = '\\';
#else
static constexpr const std::string::value_type path_separator = '/';
#endif
extern const std::string::value_type* path_separators;

bool is_file(const std::string& path);
bool is_directory(const std::string& path);
Expand Down Expand Up @@ -48,18 +54,28 @@ namespace base {
std::string get_lib_app_support_path();
#endif

// If the given filename is a relative path, it converts the
// filename to an absolute one.
// Converts an existing file path to an absolute one, or returns an
// empty string if the file doesn't exist. It uses realpath() on
// POSIX-like systems and GetFullPathName() on Windows.
std::string get_canonical_path(const std::string& path);

// TODO why get_canonical_path() is not enough?
std::string get_absolute_path(const std::string& filename);
// Returns the absolute path using lexical/string operations, and
// get_current_path() when needed. Doesn't require an existing file
// in "path". The returned path shouldn't contain "." or ".."
// elements (is a normalized path).
std::string get_absolute_path(const std::string& path);

paths list_files(const std::string& path);

// Returns true if the given character is a valud path separator
// (any of '\' or '/' characters).
bool is_path_separator(std::string::value_type chr);
inline constexpr bool is_path_separator(std::string::value_type chr) {
return (
#if LAF_WINDOWS
chr == '\\' ||
#endif
chr == '/');
}

// Returns only the path (without the last trailing slash).
std::string get_file_path(const std::string& filename);
Expand Down Expand Up @@ -89,9 +105,9 @@ namespace base {
// Replaces all separators with the system separator.
std::string fix_path_separators(const std::string& filename);

// Calls get_canonical_path() and fix_path_separators() for the
// given filename.
std::string normalize_path(const std::string& filename);
// Remove superfluous path elements ("/../" and "/./") and call
// fix_path_separators() for the given path.
std::string normalize_path(const std::string& path);

// Returns true if the filename contains one of the specified
// extensions. The "extensions" parameter must be a set of possible
Expand Down
Loading

0 comments on commit e965896

Please sign in to comment.