Переглянути джерело

Allowing paths to be processed for other operating systems than the local.

David Piuva 3 роки тому
батько
коміт
b2708f2c38
3 змінених файлів з 412 додано та 80 видалено
  1. 253 50
      Source/DFPSR/api/fileAPI.cpp
  2. 99 30
      Source/DFPSR/api/fileAPI.h
  3. 60 0
      Source/test/tests/FileTest.cpp

+ 253 - 50
Source/DFPSR/api/fileAPI.cpp

@@ -21,7 +21,6 @@
 //    3. This notice may not be removed or altered from any source
 //    distribution.
 
-#include "fileAPI.h"
 #ifdef USE_MICROSOFT_WINDOWS
 	#include <windows.h>
 #else
@@ -33,11 +32,25 @@
 #include <cstdlib>
 #include "bufferAPI.h"
 
+// Include fileAPI without falling back on local syntax implicitly.
+//   This prevents any local syntax from being implied in functions that are supposed to use variable pathSyntax.
+#define NO_IMPLICIT_PATH_SYNTAX
+#include "fileAPI.h"
+
 namespace dsr {
 
+constexpr const char32_t* getPathSeparator(PathSyntax pathSyntax) {
+	if (pathSyntax == PathSyntax::Windows) {
+		return U"\\";
+	} else if (pathSyntax == PathSyntax::Posix) {
+		return U"/";
+	} else {
+		return U"?";
+	}
+}
+
 #ifdef USE_MICROSOFT_WINDOWS
 	using NativeChar = wchar_t; // UTF-16
-	static const char32_t* pathSeparator = U"\\";
 	static const CharacterEncoding nativeEncoding = CharacterEncoding::BOM_UTF16LE;
 	#define FILE_ACCESS_FUNCTION _wfopen
 	#define FILE_ACCESS_SELECTION (write ? L"wb" : L"rb")
@@ -55,7 +68,6 @@ namespace dsr {
 	}
 #else
 	using NativeChar = char; // UTF-8
-	static const char32_t* pathSeparator = U"/";
 	static const CharacterEncoding nativeEncoding = CharacterEncoding::BOM_UTF8;
 	#define FILE_ACCESS_FUNCTION fopen
 	#define FILE_ACCESS_SELECTION (write ? "wb" : "rb")
@@ -89,7 +101,7 @@ static FILE* accessFile(const ReadableString &filename, bool write) {
 }
 
 Buffer file_loadBuffer(const ReadableString& filename, bool mustExist) {
-	String modifiedFilename = file_optimizePath(filename);
+	String modifiedFilename = file_optimizePath(filename, LOCAL_PATH_SYNTAX);
 	FILE *file = accessFile(modifiedFilename, false);
 	if (file != nullptr) {
 		// Get the file's size by going to the end, measuring, and going back
@@ -103,7 +115,7 @@ Buffer file_loadBuffer(const ReadableString& filename, bool mustExist) {
 		return buffer;
 	} else {
 		if (mustExist) {
-			throwError(U"Failed to load ", filename, " which was optimized into ", modifiedFilename, ".\n");
+			throwError(U"Failed to load ", modifiedFilename, ".\n");
 		}
 		// If the file cound not be found and opened, an empty buffer is returned
 		return Buffer();
@@ -111,30 +123,41 @@ Buffer file_loadBuffer(const ReadableString& filename, bool mustExist) {
 }
 
 void file_saveBuffer(const ReadableString& filename, Buffer buffer) {
-	String modifiedFilename = file_optimizePath(filename);
+	String modifiedFilename = file_optimizePath(filename, LOCAL_PATH_SYNTAX);
 	if (!buffer_exists(buffer)) {
-		throwError(U"buffer_save: Cannot save a buffer that don't exist to a file.\n");
+		throwError(U"buffer_save: Can't save a buffer that don't exist to a file.\n");
 	} else {
 		FILE *file = accessFile(modifiedFilename, true);
 		if (file != nullptr) {
 			fwrite((void*)buffer_dangerous_getUnsafeData(buffer), buffer_getSize(buffer), 1, file);
 			fclose(file);
 		} else {
-			throwError("Failed to save ", filename, " which was optimized into ", modifiedFilename, ".\n");
+			throwError("Failed to save ", modifiedFilename, ".\n");
 		}
 	}
 }
 
 const char32_t* file_separator() {
-	return pathSeparator;
+	return getPathSeparator(LOCAL_PATH_SYNTAX);
 }
 
 inline bool isSeparator(DsrChar c) {
 	return c == U'\\' || c == U'/';
 }
 
+// Returns the index of the first / or \ in path, or defaultIndex if none existed.
+static int64_t getFirstSeparator(const ReadableString &path, int64_t defaultIndex) {
+	for (int64_t i = 0; i < string_length(path); i++) {
+		DsrChar c = path[i];
+		if (isSeparator(c)) {
+			return i;
+		}
+	}
+	return defaultIndex;
+}
+
 // Returns the index of the last / or \ in path, or defaultIndex if none existed.
-static int64_t getLastSeparator(const ReadableString &path, int defaultIndex) {
+static int64_t getLastSeparator(const ReadableString &path, int64_t defaultIndex) {
 	for (int64_t i = string_length(path) - 1; i >= 0; i--) {
 		DsrChar c = path[i];
 		if (isSeparator(c)) {
@@ -144,18 +167,66 @@ static int64_t getLastSeparator(const ReadableString &path, int defaultIndex) {
 	return defaultIndex;
 }
 
-String file_optimizePath(const ReadableString &path) {
-	String result;
-	int inputLength = string_length(path);
+String file_optimizePath(const ReadableString &path, PathSyntax pathSyntax) {
+	String result; // The final output being appended.
+	String currentEntry; // The current entry.
+	bool hadSeparator = false;
+	bool hadContent = false;
+	int64_t inputLength = string_length(path);
 	string_reserve(result, inputLength);
-	for (int i = 0; i < inputLength; i++) {
+	// Read null terminator from one element outside of the path to allow concluding an entry not followed by any separator.
+	//   The null terminator is not actually stored, but reading out of bound gives a null terminator.
+	for (int64_t i = 0; i <= inputLength; i++) {
 		DsrChar c = path[i];
-		if (isSeparator(c)) {
-			string_append(result, pathSeparator);
+		bool separator = isSeparator(c);
+		if (separator || i == inputLength) {
+			bool appendEntry = true;
+			bool appendSeparator = separator;
+			if (hadSeparator) {
+				if (hadContent && string_length(currentEntry) == 0) {
+					// Reduce non-leading // into / by skipping "" entries.
+					// Any leading multiples of slashes have their count preserved, because some systems use them to indicate special use cases.
+					appendEntry = false;
+					appendSeparator = false;
+				} else if (string_match(currentEntry, U".")) {
+					// Reduce /./ into / by skipping "." entries.
+					appendEntry = false;
+					appendSeparator = false;
+				} else if (string_match(currentEntry, U"..")) {
+					// Reduce the parent directory against the reverse ".." entry.
+					result = file_getRelativeParentFolder(result, pathSyntax);
+					if (string_match(result, U"?")) {
+						return U"?";
+					}
+					appendEntry = false;
+				}
+			}
+			if (appendEntry) {
+				string_append(result, string_removeOuterWhiteSpace(currentEntry));
+			}
+			if (appendSeparator) {
+				string_append(result, getPathSeparator(pathSyntax));
+			}
+			currentEntry = U"";
+			if (separator) {
+				hadSeparator = true;
+			}
 		} else {
-			string_appendChar(result, c);
+			string_appendChar(currentEntry, c);
+			hadContent = true;
 		}
 	}
+	// Remove trailing separators if we had content.
+	if (hadSeparator && hadContent) {
+		int64_t lastNonSeparator = -1;
+		for (int64_t i = string_length(result) - 1; i >= 0; i--) {
+			if (!isSeparator(result[i])) {
+				lastNonSeparator = i;
+				break;
+			}
+		}
+		result = string_until(result, lastNonSeparator);
+	}
 	return result;
 }
 
@@ -163,25 +234,95 @@ ReadableString file_getPathlessName(const ReadableString &path) {
 	return string_after(path, getLastSeparator(path, -1));
 }
 
-ReadableString file_getParentFolder(const ReadableString &path) {
-	return string_before(path, getLastSeparator(path, 0));
+String file_getRelativeParentFolder(const ReadableString &path, PathSyntax pathSyntax) {
+	String optimizedPath = file_optimizePath(path, pathSyntax);
+	if (string_length(optimizedPath) == 0) {
+		// Use .. to go outside of the current directory.
+		return U"..";
+	} else if (string_match(file_getPathlessName(optimizedPath), U"?")) {
+		// From unknown to unknown.
+		return U"?";
+	} else if (file_isRoot(optimizedPath, false, pathSyntax)) {
+		// If it's the known true root, then we know that it does not have a parent and must fail.
+		return U"?";
+	} else if (file_isRoot(optimizedPath, true, pathSyntax)) {
+		// If it's an alias for an arbitrary folder, use .. to leave it.
+		return file_combinePaths(optimizedPath, U"..", pathSyntax);
+	} else if (string_match(file_getPathlessName(optimizedPath), U"..")) {
+		// Add more dots to the path.
+		return file_combinePaths(optimizedPath, U"..", pathSyntax);
+	} else {
+		// Inside of something.
+		int64_t lastSeparator = getLastSeparator(optimizedPath, 0);
+		if (pathSyntax == PathSyntax::Windows) {
+			// Return everything before the last separator.
+			return string_before(optimizedPath, lastSeparator);
+		} else { // PathSyntax::Posix
+			if (file_hasRoot(path, false, pathSyntax) && lastSeparator == 0) {
+				// Keep the absolute root.
+				return U"/";
+			} else {
+				// Keep everything before the last separator.
+				return string_before(optimizedPath, lastSeparator);
+			}
+		}
+	}
 }
 
-bool file_hasRoot(const ReadableString &path, bool treatHomeFolderAsRoot) {
-	#ifdef USE_MICROSOFT_WINDOWS
-		// If a colon is found, it is a root path.
-		return string_findFirst(path, U':') > -1;
-	#else
-		// If the path begins with a separator, it is the root folder in Posix systems.
-		// If the path begins with a tilde (~), it is a home folder.
-		DsrChar firstC = path[0];
-		return firstC == U'/' || (treatHomeFolderAsRoot && firstC == U'~');
-	#endif
+String file_getTheoreticalAbsoluteParentFolder(const ReadableString &path, const ReadableString &currentPath, PathSyntax pathSyntax) {
+	if (file_hasRoot(path, true, LOCAL_PATH_SYNTAX)) {
+		// Absolute paths should be treated the same as a theoretical path.
+		return file_getRelativeParentFolder(path, pathSyntax);
+	} else {
+		// If the input is not absolute, convert it before taking the parent directory.
+		return file_getRelativeParentFolder(file_getTheoreticalAbsolutePath(path, currentPath, pathSyntax), pathSyntax);
+	}
+}
+
+String file_getAbsoluteParentFolder(const ReadableString &path) {
+	return file_getTheoreticalAbsoluteParentFolder(path, file_getCurrentPath(), LOCAL_PATH_SYNTAX);
+}
+
+bool file_isRoot(const ReadableString &path, bool treatHomeFolderAsRoot, PathSyntax pathSyntax) {
+	ReadableString cleanPath = string_removeOuterWhiteSpace(path);
+	int64_t length = string_length(cleanPath);
+	if (length == 0) {
+		// Relative path is not a root.
+		return false;
+	} else if (length == 1) {
+		DsrChar c = cleanPath[0];
+		if (pathSyntax == PathSyntax::Windows) {
+			return c == U'\\'; // Implicit drive root.
+		} else { // PathSyntax::Posix
+			return c == U'/' || (c == U'~' && treatHomeFolderAsRoot); // Root over all drives or home folder.
+		}
+	} else {
+		if (pathSyntax == PathSyntax::Windows && cleanPath[length - 1] == U':') {
+			// C:, D:, ...
+			return true;
+		} else {
+			return false;
+		}
+	}
+}
+
+bool file_hasRoot(const ReadableString &path, bool treatHomeFolderAsRoot, PathSyntax pathSyntax) {
+	int64_t firstSeparator = getFirstSeparator(path, -1);
+	if (firstSeparator == -1) {
+		// If there is no separator, path has a root if it is a root.
+		return file_isRoot(path, treatHomeFolderAsRoot, pathSyntax);
+	} else if (firstSeparator == 0) {
+		// Starting with a separator. Either an implicit drive on Windows or the whole system's root on Posix.
+		return true;
+	} else {
+		// Has a root if the first entry before the first slash is a root.
+		return file_isRoot(string_before(path, firstSeparator), treatHomeFolderAsRoot, pathSyntax);
+	}
 }
 
 bool file_setCurrentPath(const ReadableString &path) {
 	Buffer buffer;
-	const NativeChar *nativePath = toNativeString(file_optimizePath(path), buffer);
+	const NativeChar *nativePath = toNativeString(file_optimizePath(path, LOCAL_PATH_SYNTAX), buffer);
 	#ifdef USE_MICROSOFT_WINDOWS
 		return SetCurrentDirectoryW(nativePath);
 	#else
@@ -201,6 +342,22 @@ String file_getCurrentPath() {
 	#endif
 }
 
+String file_followSymbolicLink(const ReadableString &path, bool mustExist) {
+	String modifiedPath = file_optimizePath(path, LOCAL_PATH_SYNTAX);
+	Buffer buffer;
+	const NativeChar *nativePath = toNativeString(modifiedPath, buffer);
+	#ifdef USE_MICROSOFT_WINDOWS
+		// TODO: Is there anything that can be used as a symbolic link on Windows?
+	#else
+		NativeChar resultBuffer[maxLength + 1] = {0};
+		if (readlink(nativePath, resultBuffer, maxLength) != -1) {
+			return fromNativeString(resultBuffer);
+		}
+	#endif
+	if (mustExist) { throwError(U"The symbolic link ", path, " could not be found!\n"); }
+	return U"?";
+}
+
 String file_getApplicationFolder(bool allowFallback) {
 	#ifdef USE_MICROSOFT_WINDOWS
 		NativeChar resultBuffer[maxLength + 1] = {0};
@@ -210,13 +367,13 @@ String file_getApplicationFolder(bool allowFallback) {
 		NativeChar resultBuffer[maxLength + 1] = {0};
 		if (readlink("/proc/self/exe", resultBuffer, maxLength) != -1) {
 			// Linux detected
-			return file_getParentFolder(fromNativeString(resultBuffer));
+			return file_getAbsoluteParentFolder(fromNativeString(resultBuffer));
 		} else if (readlink("/proc/curproc/file", resultBuffer, maxLength) != -1) {
 			// BSD detected
-			return file_getParentFolder(fromNativeString(resultBuffer));
+			return file_getAbsoluteParentFolder(fromNativeString(resultBuffer));
 		} else if (readlink("/proc/self/path/a.out", resultBuffer, maxLength) != -1) {
 			// Solaris detected
-			return file_getParentFolder(fromNativeString(resultBuffer));
+			return file_getAbsoluteParentFolder(fromNativeString(resultBuffer));
 		} else if (allowFallback) {
 			return file_getCurrentPath();
 		} else {
@@ -226,31 +383,77 @@ String file_getApplicationFolder(bool allowFallback) {
 	#endif
 }
 
-String file_combinePaths(const ReadableString &a, const ReadableString &b) {
-	if (file_hasRoot(b)) {
-		return b;
+String file_combinePaths(const ReadableString &a, const ReadableString &b, PathSyntax pathSyntax) {
+	ReadableString cleanA = string_removeOuterWhiteSpace(a);
+	ReadableString cleanB = string_removeOuterWhiteSpace(b);
+	int64_t lengthA = string_length(cleanA);
+	int64_t lengthB = string_length(cleanB);
+	if (file_hasRoot(b, true, pathSyntax)) {
+		// Restarting from root or home folder.
+		return cleanB;
+	} else if (lengthA == 0) {
+		// Ignoring initial relative path, so that relative paths are not suddenly moved to the root by a new separator.
+		return cleanB;
+	} else if (lengthB == 0) {
+		// Ignoring initial relative path, so that relative paths are not suddenly moved to the root by a new separator.
+		return cleanA;
 	} else {
-		if (isSeparator(a[string_length(a) - 1])) {
+		if (isSeparator(a[lengthA - 1])) {
 			// Already ending with a separator.
-			return string_combine(a, b);
+			return string_combine(cleanA, cleanB);
 		} else {
 			// Combine using a separator.
-			return string_combine(a, pathSeparator, b);
+			return string_combine(cleanA, getPathSeparator(pathSyntax), cleanB);
 		}
 	}
 }
 
-String file_getAbsolutePath(const ReadableString &path) {
-	if (file_hasRoot(path)) {
+// Returns path with the drive letter applied from currentPath if missing in path.
+// Used for converting drive relative paths into true absolute paths on MS-Windows.
+static String applyDriveLetter(const ReadableString &path, const ReadableString &currentPath) {
+	// Convert implicit drive into a named drive.
+	printText("applyDriveLetter(", path, ", ", currentPath, ")\n");
+	if (path[0] == U'\\') {
+		printText("  Implicit drive.\n");
+		int64_t colonIndex = string_findFirst(currentPath, U':', -1);
+		if (colonIndex == -1) {
+			printText("  No colon found!\n");
+			return U"?";
+		} else {
+			// Get the drive letter from the current path.
+			String drive = string_until(currentPath, colonIndex);
+			printText("  drive = ", drive, "\n");
+			return string_combine(drive, path);
+		}
+	} else {
+		printText("  Already absolute.\n");
+		// Already absolute.
 		return path;
+	}
+}
+
+String file_getTheoreticalAbsolutePath(const ReadableString &path, const ReadableString &currentPath, PathSyntax pathSyntax) {
+	// Home folders are absolute enough, because we don't want to lose the account ambiguity by mangling it into hardcoded usernames.
+	if (file_hasRoot(path, true, pathSyntax)) {
+		if (pathSyntax == PathSyntax::Windows) {
+			// Make sure that no drive letter is missing.
+			return applyDriveLetter(file_optimizePath(path, pathSyntax), currentPath);
+		} else {
+			// Already absolute.
+			return file_optimizePath(path, pathSyntax);
+		}
 	} else {
-		return file_combinePaths(file_getCurrentPath(), path);
+		// Convert from relative path.
+		return file_optimizePath(file_combinePaths(currentPath, path, pathSyntax), pathSyntax);
 	}
 }
+String file_getAbsolutePath(const ReadableString &path) {
+	return file_getTheoreticalAbsolutePath(path, file_getCurrentPath(), LOCAL_PATH_SYNTAX);
+}
 
 int64_t file_getFileSize(const ReadableString& filename) {
 	int64_t result = -1;
-	String modifiedFilename = file_optimizePath(filename);
+	String modifiedFilename = file_optimizePath(filename, LOCAL_PATH_SYNTAX);
 	Buffer buffer;
 	const NativeChar *nativePath = toNativeString(modifiedFilename, buffer);
 	#ifdef USE_MICROSOFT_WINDOWS
@@ -289,9 +492,9 @@ String& string_toStreamIndented(String& target, const EntryType& source, const R
 
 EntryType file_getEntryType(const ReadableString &path) {
 	EntryType result = EntryType::NotFound;
-	String modifiedPath = file_optimizePath(path);
+	String optimizedPath = file_optimizePath(path, LOCAL_PATH_SYNTAX);
 	Buffer buffer;
-	const NativeChar *nativePath = toNativeString(modifiedPath, buffer);
+	const NativeChar *nativePath = toNativeString(optimizedPath, buffer);
 	#ifdef USE_MICROSOFT_WINDOWS
 		DWORD dwAttrib = GetFileAttributesW(nativePath);
 		if (dwAttrib != INVALID_FILE_ATTRIBUTES) {
@@ -320,9 +523,9 @@ EntryType file_getEntryType(const ReadableString &path) {
 }
 
 bool file_getFolderContent(const ReadableString& folderPath, std::function<void(const ReadableString& entryPath, const ReadableString& entryName, EntryType entryType)> action) {
-	String modifiedPath = file_optimizePath(folderPath);
+	String optimizedPath = file_optimizePath(folderPath, LOCAL_PATH_SYNTAX);
 	#ifdef USE_MICROSOFT_WINDOWS
-		String pattern = file_combinePaths(modifiedPath, U"*.*");
+		String pattern = file_combinePaths(optimizedPath, U"*.*");
 		Buffer buffer;
 		const NativeChar *nativePattern = toNativeString(pattern, buffer);
 		WIN32_FIND_DATAW findData;
@@ -333,7 +536,7 @@ bool file_getFolderContent(const ReadableString& folderPath, std::function<void(
 			while (true) {
 				String entryName = fromNativeString(findData.cFileName);
 				if (!string_match(entryName, U".") && !string_match(entryName, U"..")) {
-					String entryPath = file_combinePaths(modifiedPath, entryName);
+					String entryPath = file_combinePaths(optimizedPath, entryName);
 					EntryType entryType = EntryType::UnhandledType;
 					if(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
 						entryType = EntryType::Folder;
@@ -348,7 +551,7 @@ bool file_getFolderContent(const ReadableString& folderPath, std::function<void(
 		}
 	#else
 		Buffer buffer;
-		const NativeChar *nativePath = toNativeString(modifiedPath, buffer);
+		const NativeChar *nativePath = toNativeString(optimizedPath, buffer);
 		DIR *directory = opendir(nativePath);
 		if (directory == nullptr) {
 			return false;
@@ -358,7 +561,7 @@ bool file_getFolderContent(const ReadableString& folderPath, std::function<void(
 				if (entry != nullptr) {
 					String entryName = fromNativeString(entry->d_name);
 					if (!string_match(entryName, U".") && !string_match(entryName, U"..")) {
-						String entryPath = file_combinePaths(modifiedPath, entryName);
+						String entryPath = file_combinePaths(optimizedPath, entryName, LOCAL_PATH_SYNTAX);
 						EntryType entryType = file_getEntryType(entryPath);
 						action(entryPath, entryName, entryType);
 					}

+ 99 - 30
Source/DFPSR/api/fileAPI.h

@@ -40,56 +40,74 @@ TODO:
 	#define USE_MICROSOFT_WINDOWS
 #endif
 
-// TODO: Create regression tests for the file system.
-
 // A module for file access that exists to prevent cyclic dependencies between strings and buffers.
 //   Buffers need a filename to be saved or loaded while strings use buffers to store their characters.
 namespace dsr {
+	// The PathSyntax enum allow processing theoreical paths for other operating systems than the local.
+	enum class PathSyntax { Windows, Posix };
+	#ifdef USE_MICROSOFT_WINDOWS
+		// Let the local syntax be for Windows.
+		#define LOCAL_PATH_SYNTAX PathSyntax::Windows
+	#else
+		// Let the local syntax be for Posix.
+		#define LOCAL_PATH_SYNTAX PathSyntax::Posix
+	#endif
+
+	// Define NO_IMPLICIT_PATH_SYNTAX before including the header if you want all PathSyntax arguments to be explicit.
+	// If a function you are calling adds a new pathSyntax argument, defining NO_IMPLICIT_PATH_SYNTAX will make sure that you get a warning from the compiler after upgrading the library.
+	#ifdef NO_IMPLICIT_PATH_SYNTAX
+		// No deafult argument for PathSyntax input.
+		#define IMPLICIT_PATH_SYNTAX
+	#else
+		// Local deafult argument for PathSyntax input.
+		#define IMPLICIT_PATH_SYNTAX = LOCAL_PATH_SYNTAX
+	#endif
+
+	// Path-syntax: According to the local computer.
 	// Post-condition:
 	//   Returns the content of the readable file referred to by file_optimizePath(filename).
 	//   If mustExist is true, then failure to load will throw an exception.
 	//   If mustExist is false, then failure to load will return an empty handle (returning false for buffer_exists).
 	Buffer file_loadBuffer(const ReadableString& filename, bool mustExist = true);
 
+	// Path-syntax: According to the local computer.
 	// Side-effect: Saves buffer to file_optimizePath(filename) as a binary file.
 	// Pre-condition: buffer exists
 	void file_saveBuffer(const ReadableString& filename, Buffer buffer);
 
+	// Path-syntax: According to the local computer.
+	// Pre-condition: file_getEntryType(path) == EntryType::SymbolicLink
+	// Post-condition: Returns the destination of a symbolic link as an absolute path.
+	// Shortcuts with file extensions are counted as files, not links.
+	// TODO: Should shortcuts of known formats be supported anyway by parsing them?
+	String file_followSymbolicLink(const ReadableString &path, bool mustExist = true);
+
+	// Path-syntax: According to the local computer.
 	// Get a path separator for the target operating system.
 	//   Can be used to construct a file path that works for both forward and backward slash separators.
 	const char32_t* file_separator();
 
-	// Turns / and \ into the local system's convention, so that loading and saving files can use either one of them automatically.
-	// TODO: Remove redundant . and .. to reduce the risk of running out of buffer space.
-	String file_optimizePath(const ReadableString &path);
-
-	// Returns the local name of the file or folder after the last path separator, or the whole path if no separator was found.
-	// Examples with / as the path separator:
-	//   file_getFolderPath(U"MyFolder/Cars.txt") == U"Cars.txt"
-	//   file_getFolderPath(U"MyFolder/")         == U""
-	//   file_getFolderPath(U"MyFolder")          == U"MyFolder"
-	//   file_getFolderPath(U"MyFolder/Folder2")  == U"Folder2"
-	ReadableString file_getPathlessName(const ReadableString &path);
-
-	// Returns the parent folder path with anything after the last slash removed, or empty if there was no slash left.
-	// Examples with / as the path separator:
-	//   file_getFolderPath(U"MyFolder/Documents/Cars.txt") == U"MyFolder/Documents"
-	//   file_getFolderPath(U"MyFolder/Documents/")         == U"MyFolder/Documents"
-	//   file_getFolderPath(U"MyFolder/Documents")          == U"MyFolder"
-	//   file_getFolderPath(U"MyFolder")                    == U""
-	ReadableString file_getParentFolder(const ReadableString &path);
+	// Path-syntax: Depends on pathSyntax argument.
+	// Turns / and \ into the path convention specified by pathSyntax, which is the local system's by default.
+	// Removes redundant . and .. to reduce the risk of running out of buffer space when calling the system.
+	String file_optimizePath(const ReadableString &path, PathSyntax pathSyntax IMPLICIT_PATH_SYNTAX);
 
+	// Path-syntax: Depends on pathSyntax argument.
 	// Combines two parts into a path and automatically adding a local separator when needed.
 	// Can be used to get the full path of a file in a folder or add another folder to the path.
 	// b may not begin with a separator, because only a is allowed to contain the root.
-	// Examples with / as the path separator:
-	//   file_combinePaths(U"Folder", U"Document.txt") == U"Folder/Document.txt"
-	//   file_combinePaths(U"Folder/", U"Document.txt") == U"Folder/Document.txt"
-	String file_combinePaths(const ReadableString &a, const ReadableString &b);
+	String file_combinePaths(const ReadableString &a, const ReadableString &b, PathSyntax pathSyntax IMPLICIT_PATH_SYNTAX);
 
-	// Returns true iff path contains a root, according to the local path syntax.
-	// If treatHomeFolderAsRoot is true, starting from the home folder using the Posix ~ alias will be allowed.
-	bool file_hasRoot(const ReadableString &path, bool treatHomeFolderAsRoot = true);
+	// Path-syntax: Depends on pathSyntax argument.
+	// Post-condition: Returns true for relative paths true iff path contains a root, according to the path syntax.
+	//                 Implicit drives on Windows using \ are treated as roots because we know that there is nothing above them.
+	// If treatHomeFolderAsRoot is true, starting from the /home/username folder using the Posix ~ alias will be allowed as a root as well, because we can't append it behind another path.
+	bool file_hasRoot(const ReadableString &path, bool treatHomeFolderAsRoot, PathSyntax pathSyntax IMPLICIT_PATH_SYNTAX);
+
+	// Path-syntax: Depends on pathSyntax argument.
+	// Returns true iff path is a root without any files nor folder names following.
+	//   Does not check if it actually exists, so use file_getEntryType on the actual folders and files for verifying existence.
+	bool file_isRoot(const ReadableString &path, bool treatHomeFolderAsRoot, PathSyntax pathSyntax IMPLICIT_PATH_SYNTAX);
 
 	// DSR_MAIN_CALLER is a convenient wrapper for getting input arguments as a list of portable Unicode strings.
 	//   The actual main function gets placed in DSR_MAIN_CALLER, which calls the given function.
@@ -115,17 +133,52 @@ namespace dsr {
 	List<String> file_impl_convertInputArguments(int argn, void **argv);
 	List<String> file_impl_getInputArguments();
 
-	// Get the current path, from where the application was called and relative paths start.
+	// Path-syntax: According to the local computer.
+	// Post-condition: Returns the current path, from where the application was called and relative paths start.
 	String file_getCurrentPath();
+
+	// Path-syntax: According to the local computer.
 	// Side-effects: Sets the current path to file_optimizePath(path).
 	// Post-condition: Returns Returns true on success and false on failure.
 	bool file_setCurrentPath(const ReadableString &path);
+
+	// Path-syntax: According to the local computer.
 	// Post-condition: Returns  the application's folder path, from where the application is stored.
 	// If not implemented and allowFallback is true,
 	//   the current path is returned instead as a qualified guess instead of raising an exception.
 	String file_getApplicationFolder(bool allowFallback = true);
-	// Gets an absolute version of the path, quickly without removing redundancy.
+
+	// Path-syntax: This trivial operation should work the same independent of operating system.
+	//              Otherwise you just have to add a new argument after upgrading the static library.
+	// Returns the local name of the file or folder after the last path separator, or the whole path if no separator was found.
+	ReadableString file_getPathlessName(const ReadableString &path);
+
+	// Quickly gets the relative parent folder by removing the last entry from the string or appending .. at the end.
+	// Path-syntax: Depends on pathSyntax argument.
+	// This pure syntax function getting the parent folder does not access the system in any way.
+	// Does not guarantee that the resulting path is usable on the system.
+	// It allows using ~ as the root, for writing paths compatible across different user accounts pointing to different but corresponding files.
+	// Going outside of a relative start will add .. to the path.
+	//   Depending on which current directory the result is applied to, the absolute path may end up as a root followed by multiple .. going nowhere.
+	// Going outside of the absolute root returns U"?" as an error code.
+	String file_getRelativeParentFolder(const ReadableString &path, PathSyntax pathSyntax IMPLICIT_PATH_SYNTAX);
+
+	// Gets the canonical parent folder using the current directory.
+	// This function for getting the parent folder treats path relative to the current directory and expands the result into an absolute path.
+	// Make sure that current directory is where you want it when calling this function, because the current directory may change over time when calling file_setCurrentPath.
+	// Path-syntax: According to the local computer.
+	// Pre-conditions:
+	//   path must be valid on the local system, such that you given full permissions could read or create files there relative to the current directory when this function is called.
+	//   ~ is not allowed as the root, because the point of using ~ is to reuse the same path across different user accounts, which does not refer to an absolute home directory.
+	// Post-condition: Returns the absolute parent to the given path, or U"?" if trying to leave the root or use a tilde home alias.
+	String file_getAbsoluteParentFolder(const ReadableString &path);
+
+	// Gets the canonical absolute version of the path.
+	// Path-syntax: According to the local computer.
+	// Post-condition: Returns an absolute version of the path, quickly without removing redundancy.
 	String file_getAbsolutePath(const ReadableString &path);
+
+	// Path-syntax: According to the local computer.
 	// Pre-condition: filename must refer to a file so that file_getEntryType(filename) == EntryType::File.
 	// Post-condition: Returns a structure with information about the file at file_optimizePath(filename), or -1 if no such file exists.
 	int64_t file_getFileSize(const ReadableString& filename);
@@ -134,6 +187,7 @@ namespace dsr {
 	enum class EntryType { NotFound, UnhandledType, File, Folder, SymbolicLink };
 	String& string_toStreamIndented(String& target, const EntryType& source, const ReadableString& indentation);
 
+	// Path-syntax: According to the local computer.
 	// Post-condition: Returns what the file_optimizePath(path) points to in the filesystem.
 	// Different comparisons on the result can be used to check if something exists.
 	//   Use file_getEntryType(filename) == EntryType::File to check if a file exists.
@@ -141,12 +195,27 @@ namespace dsr {
 	//   Use file_getEntryType(path) != EntryType::NotFound to check if the path leads to anything.
 	EntryType file_getEntryType(const ReadableString &path);
 
+	// Path-syntax: According to the local computer.
 	// Side-effects: Calls action with the entry's path, name and type for everything detected in folderPath.
 	//               entryPath equals file_combinePaths(folderPath, entryName), and is used for recursive calls when entryType == EntryType::Folder.
 	//               entryName equals file_getPathlessName(entryPath).
 	//               entryType equals file_getEntryType(entryPath).
 	// Post-condition: Returns true iff the folder could be found.
 	bool file_getFolderContent(const ReadableString& folderPath, std::function<void(const ReadableString& entryPath, const ReadableString& entryName, EntryType entryType)> action);
+
+
+
+	// Functions below are for testing and simulation of other systems by substituting the current directory and operating system with manual settings.
+
+	// A theoretical version of file_getParentFolder for evaluation on a theoretical system without actually calling file_getCurrentPath or running on the given system.
+	// Path-syntax: Depends on pathSyntax argument.
+	// Post-condition: Returns the absolute parent to the given path, or U"?" if trying to leave the root or use a tilde home alias.
+	String file_getTheoreticalAbsoluteParentFolder(const ReadableString &path, const ReadableString &currentPath, PathSyntax pathSyntax);
+
+	// A theoretical version of for evaluation on a theoretical system without actually calling file_getCurrentPath or running on the given system.
+	// Path-syntax: Depends on pathSyntax argument.
+	// Post-condition: Returns an absolute version of the path, quickly without removing redundancy.
+	String file_getTheoreticalAbsolutePath(const ReadableString &path, const ReadableString &currentPath, PathSyntax pathSyntax IMPLICIT_PATH_SYNTAX);
 }
 
 #endif

+ 60 - 0
Source/test/tests/FileTest.cpp

@@ -0,0 +1,60 @@
+
+#define NO_IMPLICIT_PATH_SYNTAX
+#include "../testTools.h"
+
+START_TEST(File)
+	{ // Combining paths
+		ASSERT_MATCH(file_combinePaths(U"", U"myProgram.exe", PathSyntax::Windows), U"myProgram.exe");
+		ASSERT_MATCH(file_combinePaths(U"C:", U"myProgram.exe", PathSyntax::Windows), U"C:\\myProgram.exe");
+		ASSERT_MATCH(file_combinePaths(U"C:\\windows", U"myProgram.exe", PathSyntax::Windows), U"C:\\windows\\myProgram.exe");
+		ASSERT_MATCH(file_combinePaths(U"C:\\windows\\", U"myProgram.exe", PathSyntax::Windows), U"C:\\windows\\myProgram.exe");
+		ASSERT_MATCH(file_combinePaths(U"", U"myProgram", PathSyntax::Posix), U"myProgram");
+		ASSERT_MATCH(file_combinePaths(U"/", U"myProgram", PathSyntax::Posix), U"/myProgram");
+		ASSERT_MATCH(file_combinePaths(U"/home", U"me", PathSyntax::Posix), U"/home/me");
+		ASSERT_MATCH(file_combinePaths(U"/home/", U"me", PathSyntax::Posix), U"/home/me");
+	}
+	{ // Optimizing paths
+		// Preserving leading separators
+		ASSERT_MATCH(file_optimizePath(U"myProgram", PathSyntax::Windows), U"myProgram"); // Relative path
+		ASSERT_MATCH(file_optimizePath(U"\\myProgram", PathSyntax::Windows), U"\\myProgram"); // Implicit drive
+		ASSERT_MATCH(file_optimizePath(U"\\\\myProgram", PathSyntax::Windows), U"\\\\myProgram");
+		ASSERT_MATCH(file_optimizePath(U"\\\\\\myProgram", PathSyntax::Windows), U"\\\\\\myProgram");
+		ASSERT_MATCH(file_optimizePath(U"myProgram", PathSyntax::Posix), U"myProgram"); // Relative path
+		ASSERT_MATCH(file_optimizePath(U"/home", PathSyntax::Posix), U"/home"); // Root path
+		ASSERT_MATCH(file_optimizePath(U"//network", PathSyntax::Posix), U"//network"); // Special path
+		ASSERT_MATCH(file_optimizePath(U"///myProgram", PathSyntax::Posix), U"///myProgram");
+		// Preserving drive letters
+		ASSERT_MATCH(file_optimizePath(U"C:\\myProgram", PathSyntax::Windows), U"C:\\myProgram");
+		// Reducing redundancy
+		ASSERT_MATCH(file_optimizePath(U"/home/user", PathSyntax::Posix), U"/home/user");
+		ASSERT_MATCH(file_optimizePath(U"/home/user/", PathSyntax::Posix), U"/home/user");
+		ASSERT_MATCH(file_optimizePath(U"/home/user//", PathSyntax::Posix), U"/home/user");
+		ASSERT_MATCH(file_optimizePath(U"/home/user///", PathSyntax::Posix), U"/home/user");
+		ASSERT_MATCH(file_optimizePath(U"/home/user/.", PathSyntax::Posix), U"/home/user");
+		ASSERT_MATCH(file_optimizePath(U"/home/user/./", PathSyntax::Posix), U"/home/user");
+		ASSERT_MATCH(file_optimizePath(U"/home/user/.//", PathSyntax::Posix), U"/home/user");
+		ASSERT_MATCH(file_optimizePath(U"/home/user/..", PathSyntax::Posix), U"/home");
+		ASSERT_MATCH(file_optimizePath(U"/home/user/../", PathSyntax::Posix), U"/home");
+		ASSERT_MATCH(file_optimizePath(U"/home/user/..//", PathSyntax::Posix), U"/home");
+		ASSERT_MATCH(file_optimizePath(U"/cars/oldCars/veteranCars/../././../newCars/", PathSyntax::Posix), U"/cars/newCars");
+		ASSERT_MATCH(file_optimizePath(U"C:\\cars\\oldCars\\veteranCars\\..\\..\\newCars\\", PathSyntax::Windows), U"C:\\cars\\newCars");
+		// Error handling
+		ASSERT_MATCH(file_optimizePath(U"C:\\..", PathSyntax::Windows), U"?"); // Can't go outside of C: drive
+		ASSERT_MATCH(file_optimizePath(U"\\..", PathSyntax::Windows), U"?"); // Can't go outside of current drive root
+		ASSERT_MATCH(file_optimizePath(U"..", PathSyntax::Windows), U".."); // Can go outside of the relative path
+		ASSERT_MATCH(file_optimizePath(U"/..", PathSyntax::Posix), U"?"); // Can't go outside of system root
+		ASSERT_MATCH(file_optimizePath(U"..", PathSyntax::Posix), U".."); // Can go outside of the relative path
+	}
+	{ // Absolute canonical paths
+		ASSERT_MATCH(file_getTheoreticalAbsolutePath(U"mediaFolder\\myFile.txt", U"C:\\folder\\anotherFolder", PathSyntax::Windows), U"C:\\folder\\anotherFolder\\mediaFolder\\myFile.txt");
+		ASSERT_MATCH(file_getTheoreticalAbsolutePath(U"mediaFolder\\myFile.txt", U"C:\\folder\\anotherFolder\\", PathSyntax::Windows), U"C:\\folder\\anotherFolder\\mediaFolder\\myFile.txt");
+		ASSERT_MATCH(file_getTheoreticalAbsolutePath(U"myFile.txt", U"C:\\folder", PathSyntax::Windows), U"C:\\folder\\myFile.txt");
+		ASSERT_MATCH(file_getTheoreticalAbsolutePath(U"\\myFile.txt", U"C:\\folder", PathSyntax::Windows), U"C:\\myFile.txt"); // To the root of the current drive C:
+		ASSERT_MATCH(file_getTheoreticalAbsolutePath(U"", U"C:\\folder", PathSyntax::Windows), U"C:\\folder");
+		ASSERT_MATCH(file_getTheoreticalAbsolutePath(U"mediaFolder\\..\\myFile.txt", U"C:\\folder\\anotherFolder", PathSyntax::Windows), U"C:\\folder\\anotherFolder\\myFile.txt");
+	}
+	{ // Parent folders
+		ASSERT_MATCH(file_getRelativeParentFolder(U"mediaFolder\\..\\myFile.txt", PathSyntax::Windows), U"");
+		ASSERT_MATCH(file_getTheoreticalAbsoluteParentFolder(U"mediaFolder\\..\\myFile.txt", U"C:\\folder\\anotherFolder", PathSyntax::Windows), U"C:\\folder\\anotherFolder");
+	}
+END_TEST