
This commit updates a *lot* of rather questionable path handling logic to be robust. Specifically: * All path operations go through Platform::Path. * All ad-hoc path handling functions are removed, together with PATH_SEP. This removes code that was in platform-independent parts, but had platform-dependent behavior. * Group::linkFileRel is removed; only an absolute path is stored in Group::linkFile. However, only Group::linkFileRel is saved, with the relative path calculated on the fly, from the filename passed into SaveToFile. This eliminates dependence on global state, and makes it unnecessary to have separare code paths for saved and not yet saved files. * In a departure from previous practice, functions with platform-independent code but platform-dependent behavior are all grouped under platform/. This makes it easy to grep for functions with platform-dependent behavior. * Similarly, new (GUI-independent) code for all platforms is added in the same platform.cpp file, guarded with #ifs. It turns out that implementations for different platforms had a lot of shared code that tended to go out of sync.
573 lines
17 KiB
C++
573 lines
17 KiB
C++
//-----------------------------------------------------------------------------
|
|
// Platform-dependent functionality.
|
|
//
|
|
// Copyright 2017 whitequark
|
|
//-----------------------------------------------------------------------------
|
|
#if defined(__APPLE__)
|
|
// Include Apple headers before solvespace.h to avoid identifier clashes.
|
|
# include <CoreFoundation/CFString.h>
|
|
# include <CoreFoundation/CFURL.h>
|
|
# include <CoreFoundation/CFBundle.h>
|
|
#endif
|
|
#include "solvespace.h"
|
|
#include "config.h"
|
|
#if defined(WIN32)
|
|
// Conversely, include Microsoft headers after solvespace.h to avoid clashes.
|
|
# include <windows.h>
|
|
#else
|
|
# include <unistd.h>
|
|
# include <sys/stat.h>
|
|
#endif
|
|
|
|
namespace SolveSpace {
|
|
namespace Platform {
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// UTF-8 ⟷ UTF-16 conversion, on Windows.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#if defined(WIN32)
|
|
|
|
std::string Narrow(const wchar_t *in)
|
|
{
|
|
std::string out;
|
|
DWORD len = WideCharToMultiByte(CP_UTF8, 0, in, -1, NULL, 0, NULL, NULL);
|
|
out.resize(len - 1);
|
|
ssassert(WideCharToMultiByte(CP_UTF8, 0, in, -1, &out[0], len, NULL, NULL),
|
|
"Invalid UTF-16");
|
|
return out;
|
|
}
|
|
|
|
std::string Narrow(const std::wstring &in)
|
|
{
|
|
if(in == L"") return "";
|
|
|
|
std::string out;
|
|
out.resize(WideCharToMultiByte(CP_UTF8, 0, &in[0], (int)in.length(),
|
|
NULL, 0, NULL, NULL));
|
|
ssassert(WideCharToMultiByte(CP_UTF8, 0, &in[0], (int)in.length(),
|
|
&out[0], (int)out.length(), NULL, NULL),
|
|
"Invalid UTF-16");
|
|
return out;
|
|
}
|
|
|
|
std::wstring Widen(const char *in)
|
|
{
|
|
std::wstring out;
|
|
DWORD len = MultiByteToWideChar(CP_UTF8, 0, in, -1, NULL, 0);
|
|
out.resize(len - 1);
|
|
ssassert(MultiByteToWideChar(CP_UTF8, 0, in, -1, &out[0], len),
|
|
"Invalid UTF-8");
|
|
return out;
|
|
}
|
|
|
|
std::wstring Widen(const std::string &in)
|
|
{
|
|
if(in == "") return L"";
|
|
|
|
std::wstring out;
|
|
out.resize(MultiByteToWideChar(CP_UTF8, 0, &in[0], (int)in.length(), NULL, 0));
|
|
ssassert(MultiByteToWideChar(CP_UTF8, 0, &in[0], (int)in.length(),
|
|
&out[0], (int)out.length()),
|
|
"Invalid UTF-8");
|
|
return out;
|
|
}
|
|
|
|
#endif
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Path utility functions.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
static std::vector<std::string> Split(const std::string &joined, char separator) {
|
|
std::vector<std::string> parts;
|
|
|
|
size_t oldpos = 0, pos = 0;
|
|
while(true) {
|
|
oldpos = pos;
|
|
pos = joined.find(separator, pos);
|
|
if(pos == std::string::npos) break;
|
|
parts.push_back(joined.substr(oldpos, pos - oldpos));
|
|
pos += 1;
|
|
}
|
|
|
|
if(oldpos != joined.length() - 1) {
|
|
parts.push_back(joined.substr(oldpos));
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
static std::string Concat(const std::vector<std::string> &parts, char separator) {
|
|
std::string joined;
|
|
|
|
bool first = true;
|
|
for(auto &part : parts) {
|
|
if(!first) joined += separator;
|
|
joined += part;
|
|
first = false;
|
|
}
|
|
|
|
return joined;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Path manipulation.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#if defined(WIN32)
|
|
const char SEPARATOR = '\\';
|
|
#else
|
|
const char SEPARATOR = '/';
|
|
#endif
|
|
|
|
Path Path::From(std::string raw) {
|
|
Path path = { raw };
|
|
return path;
|
|
}
|
|
|
|
Path Path::CurrentDirectory() {
|
|
#if defined(WIN32)
|
|
// On Windows, OpenFile needs an absolute UNC path proper, so get that.
|
|
std::wstring rawW;
|
|
rawW.resize(GetCurrentDirectoryW(0, NULL));
|
|
DWORD length = GetCurrentDirectoryW((int)rawW.length(), &rawW[0]);
|
|
ssassert(length > 0 && length == rawW.length() - 1, "Cannot get current directory");
|
|
rawW.resize(length);
|
|
return From(Narrow(rawW));
|
|
#else
|
|
char *raw = getcwd(NULL, 0);
|
|
ssassert(raw != NULL, "Cannot get current directory");
|
|
Path path = From(raw);
|
|
free(raw);
|
|
return path;
|
|
#endif
|
|
}
|
|
|
|
std::string Path::FileName() const {
|
|
std::string fileName = raw;
|
|
size_t slash = fileName.rfind(SEPARATOR);
|
|
if(slash != std::string::npos) {
|
|
fileName = fileName.substr(slash + 1);
|
|
}
|
|
return fileName;
|
|
}
|
|
|
|
std::string Path::FileStem() const {
|
|
std::string baseName = FileName();
|
|
size_t dot = baseName.rfind('.');
|
|
if(dot != std::string::npos) {
|
|
baseName = baseName.substr(0, dot);
|
|
}
|
|
return baseName;
|
|
}
|
|
|
|
std::string Path::Extension() const {
|
|
size_t dot = raw.rfind('.');
|
|
if(dot != std::string::npos) {
|
|
return raw.substr(dot + 1);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
bool Path::HasExtension(std::string theirExt) const {
|
|
std::string ourExt = Extension();
|
|
std::transform(ourExt.begin(), ourExt.end(), ourExt.begin(), ::tolower);
|
|
std::transform(theirExt.begin(), theirExt.end(), theirExt.begin(), ::tolower);
|
|
return ourExt == theirExt;
|
|
}
|
|
|
|
Path Path::WithExtension(std::string ext) const {
|
|
Path withExt = *this;
|
|
size_t dot = withExt.raw.rfind('.');
|
|
if(dot != std::string::npos) {
|
|
withExt.raw.erase(dot);
|
|
}
|
|
withExt.raw += ".";
|
|
withExt.raw += ext;
|
|
return withExt;
|
|
}
|
|
|
|
static void FindPrefix(const std::string &raw, size_t *pos) {
|
|
*pos = std::string::npos;
|
|
#if defined(WIN32)
|
|
if(raw.size() >= 7 && raw[2] == '?' && raw[3] == '\\' &&
|
|
isalpha(raw[4]) && raw[5] == ':' && raw[6] == '\\') {
|
|
*pos = 7;
|
|
} else if(raw.size() >= 3 && isalpha(raw[0]) && raw[1] == ':' && raw[2] == '\\') {
|
|
*pos = 3;
|
|
} else if(raw.size() >= 2 && raw[0] == '\\' && raw[1] == '\\') {
|
|
size_t slashAt = raw.find('\\', 2);
|
|
if(slashAt != std::string::npos) {
|
|
*pos = raw.find('\\', slashAt + 1);
|
|
}
|
|
}
|
|
#else
|
|
if(raw.size() >= 1 && raw[0] == '/') {
|
|
*pos = 1;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool Path::IsAbsolute() const {
|
|
size_t pos;
|
|
FindPrefix(raw, &pos);
|
|
return pos != std::string::npos;
|
|
}
|
|
|
|
// Removes one component from the end of the path.
|
|
// Returns an empty path if the path consists only of a root.
|
|
Path Path::Parent() const {
|
|
Path parent = { raw };
|
|
if(!parent.raw.empty() && parent.raw.back() == SEPARATOR) {
|
|
parent.raw.pop_back();
|
|
}
|
|
size_t slash = parent.raw.rfind(SEPARATOR);
|
|
if(slash != std::string::npos) {
|
|
parent.raw = parent.raw.substr(0, slash + 1);
|
|
} else {
|
|
parent.raw.clear();
|
|
}
|
|
if(IsAbsolute() && !parent.IsAbsolute()) {
|
|
return From("");
|
|
}
|
|
return parent;
|
|
}
|
|
|
|
// Concatenates a component to this path.
|
|
// Returns an empty path if this path or the component is empty.
|
|
Path Path::Join(const std::string &component) const {
|
|
ssassert(component.find(SEPARATOR) == std::string::npos,
|
|
"Use the Path::Join(const Path &) overload to append an entire path");
|
|
return Join(Path::From(component));
|
|
}
|
|
|
|
// Concatenates a relative path to this path.
|
|
// Returns an empty path if either path is empty, or the other path is absolute.
|
|
Path Path::Join(const Path &other) const {
|
|
if(IsEmpty() || other.IsEmpty() || other.IsAbsolute()) {
|
|
return From("");
|
|
}
|
|
|
|
Path joined = { raw };
|
|
if(joined.raw.back() != SEPARATOR) {
|
|
joined.raw += SEPARATOR;
|
|
}
|
|
joined.raw += other.raw;
|
|
return joined;
|
|
}
|
|
|
|
// Expands the "." and ".." components in this path.
|
|
// On Windows, additionally prepends the UNC prefix to absolute paths without one.
|
|
// Returns an empty path if a ".." component would escape from the root.
|
|
Path Path::Expand(bool fromCurrentDirectory) const {
|
|
Path source;
|
|
Path expanded;
|
|
|
|
if(fromCurrentDirectory && !IsAbsolute()) {
|
|
source = CurrentDirectory().Join(*this);
|
|
} else {
|
|
source = *this;
|
|
}
|
|
|
|
size_t splitAt;
|
|
FindPrefix(source.raw, &splitAt);
|
|
if(splitAt != std::string::npos) {
|
|
expanded.raw = source.raw.substr(0, splitAt);
|
|
} else {
|
|
splitAt = 0;
|
|
}
|
|
|
|
std::vector<std::string> expandedComponents;
|
|
for(std::string component : Split(source.raw.substr(splitAt), SEPARATOR)) {
|
|
if(component == ".") {
|
|
// skip
|
|
} else if(component == "..") {
|
|
if(!expandedComponents.empty()) {
|
|
expandedComponents.pop_back();
|
|
} else {
|
|
return From("");
|
|
}
|
|
} else if(!component.empty()) {
|
|
expandedComponents.push_back(component);
|
|
}
|
|
}
|
|
|
|
if(expanded.IsEmpty()) {
|
|
if(expandedComponents.empty()) {
|
|
expandedComponents.push_back(".");
|
|
}
|
|
expanded = From(Concat(expandedComponents, SEPARATOR));
|
|
} else if(!expandedComponents.empty()) {
|
|
expanded = expanded.Join(From(Concat(expandedComponents, SEPARATOR)));
|
|
}
|
|
|
|
#if defined(WIN32)
|
|
if(expanded.IsAbsolute() && expanded.raw.substr(0, 2) != "\\\\") {
|
|
expanded.raw = "\\\\?\\" + expanded.raw;
|
|
}
|
|
#endif
|
|
|
|
return expanded;
|
|
}
|
|
|
|
static std::string FilesystemNormalize(const std::string &str) {
|
|
#if defined(WIN32)
|
|
std::wstring strW = Widen(str);
|
|
std::transform(strW.begin(), strW.end(), strW.begin(), towlower);
|
|
return Narrow(strW);
|
|
#elif defined(__APPLE__)
|
|
CFMutableStringRef cfStr =
|
|
CFStringCreateMutableCopy(NULL, 0,
|
|
CFStringCreateWithBytesNoCopy(NULL, (const UInt8*)str.data(), str.size(),
|
|
kCFStringEncodingUTF8, /*isExternalRepresentation=*/false, kCFAllocatorNull));
|
|
CFStringLowercase(cfStr, NULL);
|
|
std::string normalizedStr;
|
|
normalizedStr.resize(CFStringGetMaximumSizeOfFileSystemRepresentation(cfStr));
|
|
CFStringGetFileSystemRepresentation(cfStr, &normalizedStr[0], normalizedStr.size());
|
|
normalizedStr.erase(normalizedStr.find('\0'));
|
|
return normalizedStr;
|
|
#else
|
|
return str;
|
|
#endif
|
|
}
|
|
|
|
bool Path::Equals(const Path &other) const {
|
|
return FilesystemNormalize(raw) == FilesystemNormalize(other.raw);
|
|
}
|
|
|
|
// Returns a relative path from a given base path.
|
|
// Returns an empty path if any of the paths is not absolute, or
|
|
// if they belong to different roots, or
|
|
// if they cannot be expanded.
|
|
Path Path::RelativeTo(const Path &base) const {
|
|
Path expanded = Expand();
|
|
Path baseExpanded = base.Expand();
|
|
if(!(expanded.IsAbsolute() && baseExpanded.IsAbsolute())){
|
|
return From("");
|
|
}
|
|
|
|
size_t splitAt;
|
|
FindPrefix(expanded.raw, &splitAt);
|
|
size_t baseSplitAt;
|
|
FindPrefix(baseExpanded.raw, &baseSplitAt);
|
|
if(FilesystemNormalize(expanded.raw.substr(0, splitAt)) !=
|
|
FilesystemNormalize(baseExpanded.raw.substr(0, splitAt))) {
|
|
return From("");
|
|
}
|
|
|
|
std::vector<std::string> components =
|
|
Split(expanded.raw.substr(splitAt), SEPARATOR);
|
|
std::vector<std::string> baseComponents =
|
|
Split(baseExpanded.raw.substr(baseSplitAt), SEPARATOR);
|
|
size_t common;
|
|
for(common = 0; common < baseComponents.size() &&
|
|
common < components.size(); common++) {
|
|
if(FilesystemNormalize(baseComponents[common]) !=
|
|
FilesystemNormalize(components[common])) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> resultComponents;
|
|
for(size_t i = common; i < baseComponents.size(); i++) {
|
|
resultComponents.push_back("..");
|
|
}
|
|
resultComponents.insert(resultComponents.end(),
|
|
components.begin() + common, components.end());
|
|
if(resultComponents.empty()) {
|
|
resultComponents.push_back(".");
|
|
}
|
|
return From(Concat(resultComponents, SEPARATOR));
|
|
}
|
|
|
|
Path Path::FromPortable(const std::string &repr) {
|
|
return From(Concat(Split(repr, '/'), SEPARATOR));
|
|
}
|
|
|
|
std::string Path::ToPortable() const {
|
|
ssassert(!IsAbsolute(), "absolute paths cannot be made portable");
|
|
|
|
return Concat(Split(raw, SEPARATOR), '/');
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// File manipulation.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
FILE *OpenFile(const Platform::Path &filename, const char *mode) {
|
|
ssassert(filename.raw.length() == strlen(filename.raw.c_str()),
|
|
"Unexpected null byte in middle of a path");
|
|
#if defined(WIN32)
|
|
return _wfopen(Widen(filename.Expand().raw).c_str(), Widen(mode).c_str());
|
|
#else
|
|
return fopen(filename.raw.c_str(), mode);
|
|
#endif
|
|
}
|
|
|
|
void RemoveFile(const Platform::Path &filename) {
|
|
ssassert(filename.raw.length() == strlen(filename.raw.c_str()),
|
|
"Unexpected null byte in middle of a path");
|
|
#if defined(WIN32)
|
|
_wremove(Widen(filename.Expand().raw).c_str());
|
|
#else
|
|
remove(filename.raw.c_str());
|
|
#endif
|
|
}
|
|
|
|
bool ReadFile(const Platform::Path &filename, std::string *data) {
|
|
FILE *f = OpenFile(filename, "rb");
|
|
if(f == NULL) return false;
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
data->resize(ftell(f));
|
|
fseek(f, 0, SEEK_SET);
|
|
fread(&(*data)[0], 1, data->size(), f);
|
|
fclose(f);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool WriteFile(const Platform::Path &filename, const std::string &data) {
|
|
FILE *f = OpenFile(filename, "wb");
|
|
if(f == NULL) return false;
|
|
|
|
fwrite(&data[0], 1, data.size(), f);
|
|
fclose(f);
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Loading resources, on Windows.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#if defined(WIN32)
|
|
|
|
const void *LoadResource(const std::string &name, size_t *size) {
|
|
HRSRC hres = FindResourceW(NULL, Widen(name).c_str(), RT_RCDATA);
|
|
ssassert(hres != NULL, "Cannot find resource");
|
|
HGLOBAL res = ::LoadResource(NULL, hres);
|
|
ssassert(res != NULL, "Cannot load resource");
|
|
|
|
*size = SizeofResource(NULL, hres);
|
|
return LockResource(res);
|
|
}
|
|
|
|
#endif
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Loading resources, on *nix.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#if defined(__APPLE__)
|
|
|
|
static Platform::Path PathFromCFURL(CFURLRef cfUrl) {
|
|
Path path;
|
|
CFStringRef cfPath = CFURLCopyFileSystemPath(cfUrl, kCFURLPOSIXPathStyle);
|
|
path.raw.resize(CFStringGetMaximumSizeOfFileSystemRepresentation(cfPath));
|
|
CFStringGetFileSystemRepresentation(cfPath, &path.raw[0], path.raw.size());
|
|
path.raw.erase(path.raw.find('\0'));
|
|
CFRelease(cfPath);
|
|
return path;
|
|
}
|
|
|
|
static Platform::Path ResourcePath(const std::string &name) {
|
|
Path path;
|
|
|
|
// First, try to get the URL from the bundle.
|
|
CFStringRef cfName = CFStringCreateWithCString(kCFAllocatorDefault, name.c_str(),
|
|
kCFStringEncodingUTF8);
|
|
CFURLRef cfUrl = CFBundleCopyResourceURL(CFBundleGetMainBundle(), cfName, NULL, NULL);
|
|
if(cfUrl != NULL) {
|
|
path = PathFromCFURL(cfUrl);
|
|
CFRelease(cfUrl);
|
|
}
|
|
CFRelease(cfName);
|
|
|
|
if(!path.IsEmpty()) return path;
|
|
|
|
// If that failed, it means we aren't running from the bundle.
|
|
// Reference off the executable path, then.
|
|
cfUrl = CFBundleCopyExecutableURL(CFBundleGetMainBundle());
|
|
if(cfUrl != NULL) {
|
|
path = PathFromCFURL(cfUrl).Parent().Parent().Join("res");
|
|
path = path.Join(Path::FromPortable(name));
|
|
CFRelease(cfUrl);
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
#elif !defined(WIN32)
|
|
|
|
# if defined(__linux__)
|
|
static const char *selfSymlink = "/proc/self/exe";
|
|
# elif defined(__NetBSD__)
|
|
static const char *selfSymlink = "/proc/curproc/exe"
|
|
# elif defined(__OpenBSD__) || defined(__FreeBSD__)
|
|
static const char *selfSymlink = "/proc/curproc/file";
|
|
# else
|
|
static const char *selfSymlink = "";
|
|
# endif
|
|
|
|
static Platform::Path FindLocalResourceDir() {
|
|
// Find out the path to the running binary.
|
|
Platform::Path selfPath;
|
|
char *expandedSelfPath = realpath(selfSymlink, NULL);
|
|
if(expandedSelfPath != NULL) {
|
|
selfPath = Path::From(expandedSelfPath);
|
|
}
|
|
free(expandedSelfPath);
|
|
|
|
Platform::Path resourceDir;
|
|
if(selfPath.IsEmpty()) {
|
|
// We don't know how to find the local resource directory on this platform,
|
|
// so use the global one (by returning an empty string).
|
|
return Path::From(UNIX_DATADIR);
|
|
} else {
|
|
resourceDir = selfPath.Parent().Parent().Join("res");
|
|
}
|
|
|
|
struct stat st;
|
|
if(stat(resourceDir.raw.c_str(), &st) != -1) {
|
|
// An executable-adjacent resource directory exists, good.
|
|
return resourceDir;
|
|
}
|
|
|
|
// No executable-adjacent resource directory; use the one from compile-time prefix.
|
|
return Path::From(UNIX_DATADIR);
|
|
}
|
|
|
|
static Platform::Path ResourcePath(const std::string &name) {
|
|
static Platform::Path resourceDir;
|
|
if(resourceDir.IsEmpty()) {
|
|
resourceDir = FindLocalResourceDir();
|
|
}
|
|
|
|
return resourceDir.Join(Path::FromPortable(name));
|
|
}
|
|
|
|
#endif
|
|
|
|
#if !defined(WIN32)
|
|
|
|
const void *LoadResource(const std::string &name, size_t *size) {
|
|
static std::map<std::string, std::string> cache;
|
|
|
|
auto it = cache.find(name);
|
|
if(it == cache.end()) {
|
|
ssassert(ReadFile(ResourcePath(name), &cache[name]), "Cannot read resource");
|
|
it = cache.find(name);
|
|
}
|
|
|
|
const std::string &content = (*it).second;
|
|
*size = content.size();
|
|
return (const void*)content.data();
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
}
|