1 /* -------------------------------------------------------------------------- *
2  *                       Simbody(tm): SimTKcommon                             *
3  * -------------------------------------------------------------------------- *
4  * This is part of the SimTK biosimulation toolkit originating from           *
5  * Simbios, the NIH National Center for Physics-Based Simulation of           *
6  * Biological Structures at Stanford, funded under the NIH Roadmap for        *
7  * Medical Research, grant U54 GM072970. See https://simtk.org/home/simbody.  *
8  *                                                                            *
9  * Portions copyright (c) 2009-15 Stanford University and the Authors.        *
10  * Authors: Michael Sherman, Carmichael Ong                                   *
11  * Contributors:                                                              *
12  *                                                                            *
13  * Licensed under the Apache License, Version 2.0 (the "License"); you may    *
14  * not use this file except in compliance with the License. You may obtain a  *
15  * copy of the License at http://www.apache.org/licenses/LICENSE-2.0.         *
16  *                                                                            *
17  * Unless required by applicable law or agreed to in writing, software        *
18  * distributed under the License is distributed on an "AS IS" BASIS,          *
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   *
20  * See the License for the specific language governing permissions and        *
21  * limitations under the License.                                             *
22  * -------------------------------------------------------------------------- */
23 
24 #include "SimTKcommon/internal/common.h"
25 #include "SimTKcommon/internal/String.h"
26 #include "SimTKcommon/internal/Pathname.h"
27 
28 #include <string>
29 using std::string;
30 
31 #include <cctype>
32 using std::tolower;
33 
34 #include <iostream>
35 #include <fstream>
36 
37 #ifdef _WIN32
38     #define WIN32_LEAN_AND_MEAN
39     #if !defined(NOMINMAX)
40     #define NOMINMAX
41     #endif
42     #include <windows.h>
43     #include <direct.h>
44     #pragma warning(disable:4996) // getenv() is apparently unsafe
45 #else
46     #ifdef __APPLE__
47         #include <mach-o/dyld.h>
48     #endif
49     #include <dlfcn.h>
50     #include <dirent.h>
51     #include <unistd.h>
52 #endif
53 
54 
55 using namespace SimTK;
56 
57 
58 #ifdef _WIN32
59     static const bool IsWindows          = true;
60     static const char MyPathSeparator    = '\\';
61     static const char OtherPathSeparator = '/';
62 #else
63     static const bool IsWindows          = false;
64     static const char MyPathSeparator    = '/';
65     static const char OtherPathSeparator = '\\';
66 #endif
67 
getPathSeparatorChar()68 char Pathname::getPathSeparatorChar() {
69     return MyPathSeparator;
70 }
71 
getPathSeparator()72 string Pathname::getPathSeparator() {
73     return String(getPathSeparatorChar());
74 }
75 
makeNativeSlashesInPlace(string & inout)76 static void makeNativeSlashesInPlace(string& inout) {
77     for (unsigned i=0; i < inout.size(); ++i)
78         if (inout[i] == OtherPathSeparator)
79             inout[i] = MyPathSeparator;
80 }
81 
82 // Check for either / or \ at the beginning, regardless of platform.
beginsWithPathSeparator(const string & in)83 static bool beginsWithPathSeparator(const string& in) {
84     return !in.empty() && Pathname::isPathSeparator(in[0]);
85 }
86 
87 // Check for either / or \ at the end, regardless of platform.
endsWithPathSeparator(const string & in)88 static bool endsWithPathSeparator(const string& in) {
89     return !in.empty() && Pathname::isPathSeparator(in[in.size()-1]);
90 }
91 
92 // Make sure this path ends in the right slash for this platform, unless the
93 // input is empty in which case we leave it alone.
addFinalSeparatorInPlace(string & inout)94 static void addFinalSeparatorInPlace(string& inout) {
95     if (inout.empty()) return;
96     if (Pathname::isPathSeparator(inout[inout.size()-1]))
97         inout.erase(inout.size()-1); // might be the wrong one
98     inout += MyPathSeparator;
99 }
100 
101 // Platform-dependent function that returns true if a string
102 // can be considered an absolute path. Assumes path has been
103 // cleaned of white space and "\" replaced with "/".
isAbsolutePath(const string & in)104 static bool isAbsolutePath(const string& in) {
105     if (IsWindows)
106         return in.size() >= 3 && in.substr(1, 2) == ":/";
107     else
108         return !in.empty() && in[0] == '/';
109 }
110 
isExecutableDirectoryPath(const string & in)111 static bool isExecutableDirectoryPath(const string &in) {
112     return !in.empty() && in.substr(0, 2) == "@/";
113 }
114 
115 // Remove the last segment of a path name and the separator. The
116 // returned component does not include a separator.
removeLastPathComponentInPlace(string & inout,string & component)117 static void removeLastPathComponentInPlace(string& inout, string& component) {
118     component.clear();
119     if (inout.empty()) return;
120 
121     const string::size_type lastSeparator = inout.find_last_of("/\\");
122     if (lastSeparator == string::npos) {
123         component = inout;
124         inout.clear();
125     } else {
126         component = inout.substr(lastSeparator+1);
127         inout.erase(lastSeparator);
128     }
129 }
130 
removeDriveInPlace(string & inout,string & drive)131 static void removeDriveInPlace(string& inout, string& drive) {
132     drive.clear();
133     if (IsWindows && inout.size() >= 2 && inout[1]==':') {
134         drive = (char)tolower(inout[0]);
135         inout.erase(0,2);
136     }
137 }
138 
139 // We assume a path name structure like this:
140 //   (1) Everything up to and including the final directory separator
141 //       character is the directory; the rest is the file name. On return
142 //       we fix the slashes in the directory name to suit the current
143 //       system ('\' for Windows, '/' otherwise).
144 //   (2) If the file name contains a ".", characters after the last
145 //       "." are the extension and the last "." is removed.
146 //   (3) What's left is the fileName.
147 // We accept both "/" and "\" as separator characters. We leave the
148 // case as it was supplied.
149 // Leading and trailing white space is removed; embedded white space
150 // remains.
151 // Leading "X:" for some drive letter X is recognized on Windows as
152 // a drive specification, otherwise the drive is the current drive.
153 // That is then removed for further processing.
154 // Absolute paths are designated like this:
155 //      Leading "/" means root relative (on the drive).
156 //      Leading "./" means current working directory relative (on the drive).
157 //      Leading "../" is interpreted as "./..".
158 //      Leading "@/" means relative to executable location.
159 // Above leading characters are removed and replaced with the
160 // full path name they represent, then the entire path is divided
161 // into components. If a component is "." or "" (empty) it is
162 // removed. If a component is ".." it and the previous component
163 // if any are removed. (".." as a first component will report as
164 // ill formed.)
165 // If there is something ill-formed about the file name we'll return
166 // false.
deconstructPathname(const string & pathname,bool & dontApplySearchPath,string & directory,string & fileName,string & extension)167 void Pathname::deconstructPathname( const string&   pathname,
168                                     bool&           dontApplySearchPath,
169                                     string&         directory,
170                                     string&         fileName,
171                                     string&         extension)
172 {
173     dontApplySearchPath = false;
174     directory.erase(); fileName.erase(); extension.erase();
175 
176     // Remove all the white space and make all the slashes be forward ones.
177     // (For Windows they'll be changed to backslashes later.)
178     String processed = String::trimWhiteSpace(pathname)
179                        .replaceAllChar('\\', '/');
180     if (processed.empty())
181         return; // pathname consisted only of white space
182 
183     string drive;
184     removeDriveInPlace(processed, drive);
185 
186     // Now the drive if any has been removed and we're looking at
187     // the beginning of the pathname.
188 
189     // If the pathname in its entirety is just one of these, append
190     // a slash to avoid special cases below.
191     if (processed == "." || processed == ".." || processed == "@")
192         processed += "/";
193 
194     // If the path begins with "../" we'll make it ./../ to simplify handling.
195     if (processed.substr(0, 3) == "../")
196         processed.insert(0, "./");
197 
198     if (processed.substr(0, 1) == "/") {
199         dontApplySearchPath = true;
200         processed.erase(0, 1);
201         if (drive.empty()) drive = getCurrentDriveLetter();
202     }
203     else if (processed.substr(0, 2) == "./") {
204         dontApplySearchPath = true;
205         processed.replace(0, 2, getCurrentWorkingDirectory(drive));
206         removeDriveInPlace(processed, drive);
207     }
208     else if (processed.substr(0, 2) == "@/") {
209         dontApplySearchPath = true;
210         processed.replace(0, 2, getThisExecutableDirectory());
211         removeDriveInPlace(processed, drive);
212     }
213     else if (!drive.empty()) {
214         // Looks like a relative pathname. But if it had an initial
215         // drive specification, e.g. X:something.txt, that is supposed
216         // to be interpreted relative to the current working directory
217         // on drive X, just as though it were X:./something.txt.
218         dontApplySearchPath = true;
219         processed.insert(0, getCurrentWorkingDirectory(drive));
220         removeDriveInPlace(processed, drive);
221     }
222 
223     // We may have picked up a new batch of backslashes above.
224     processed.replaceAllChar('\\', '/');
225 
226     // Now we have the full pathname if this is absolute, otherwise
227     // we're looking at a relative pathname. In any case the last
228     // component is the file name if it isn't empty, ".", or "..".
229 
230     // Process the ".." segments and eliminate meaningless ones
231     // as we go through.
232     Array_<string> segmentsInReverse;
233     bool isFinalSegment = true; // first time around might be the fileName
234     int numDotDotsSeen = 0;
235     while (!processed.empty()) {
236         string component;
237         removeLastPathComponentInPlace(processed, component);
238         if (component == "..")
239             ++numDotDotsSeen;
240         else if (!component.empty() && component != ".") {
241             if (numDotDotsSeen)
242                 --numDotDotsSeen;   // skip component
243             else if (isFinalSegment) fileName = component;
244             else segmentsInReverse.push_back(component);
245         }
246         isFinalSegment = false;
247     }
248 
249     // Now we can put together the canonicalized directory.
250     if (dontApplySearchPath) {
251         if (!drive.empty())
252             directory = drive + ":";
253         directory += "/";
254     }
255 
256     for (int i = (int)segmentsInReverse.size() - 1; i >= 0; --i)
257         directory += segmentsInReverse[i] + "/";
258 
259     // Fix the slashes.
260     makeNativeSlashesInPlace(directory);
261 
262     // If there is a .extension, strip it off.
263     string::size_type lastDot = fileName.rfind('.');
264     if (lastDot != string::npos) {
265         extension = fileName.substr(lastDot);
266         fileName.erase(lastDot);
267     }
268 }
269 
deconstructPathnameUsingSpecifiedWorkingDirectory(const std::string & swd,const std::string & pathname,std::string & directory,std::string & fileName,std::string & extension)270 void Pathname::deconstructPathnameUsingSpecifiedWorkingDirectory
271    (const std::string&  swd,
272     const std::string&  pathname,
273     std::string&        directory,
274     std::string&        fileName,
275     std::string&        extension)
276 {
277     directory.erase(); fileName.erase(); extension.erase();
278     string pathdrive, swddrive, finaldrive;
279     String processed;
280 
281     // Remove all the white space and make all the slashes be forward ones.
282     // (For Windows they'll be changed to backslashes later.)
283     String pathcleaned = String::trimWhiteSpace(pathname)
284                          .replaceAllChar('\\', '/');
285 
286     // If pathname is absolute it is handled as though there were no swd.
287     if (isAbsolutePath(pathcleaned) || isExecutableDirectoryPath(pathcleaned)) {
288         deconstructAbsolutePathname(pathname, directory, fileName, extension);
289         return;
290     }
291     if (pathcleaned.empty())
292         return; // pathname consisted only of white space
293     removeDriveInPlace(pathcleaned, pathdrive);
294 
295     String swdcleaned = String::trimWhiteSpace(swd)
296                         .replaceAllChar('\\', '/');
297 
298     // If swd was empty, then use the usual cwd method instead.
299     if (swdcleaned.empty()) {
300         deconstructAbsolutePathname(pathname, directory, fileName, extension);
301         return;
302     }
303 
304     // If swd wasn't empty, then add a trailing "/" if necessary.
305     if (swdcleaned[swdcleaned.size() - 1] != '/')
306         swdcleaned += '/';
307     removeDriveInPlace(swdcleaned, swddrive);
308 
309     /* PREPROCESSING THE SWD */
310     // Preprocess the swd if it leads with "/". Just grab current drive letter.
311     if (swdcleaned.substr(0, 1) == "/") {
312         if (swddrive.empty())
313             swddrive = getCurrentDriveLetter();
314     }
315     // Preprocess the swd if it leads with a "./"
316     else if (swdcleaned.substr(0, 2) == "./") {
317         swdcleaned.replace(0, 2, getCurrentWorkingDirectory(swddrive));
318         removeDriveInPlace(swdcleaned, swddrive);
319     }
320     // Also preprocess if swd starts with something of the form "C:folder/".
321     // Resolve by finding the current directory of this drive.
322     else if (!swddrive.empty()) {
323         swdcleaned.insert(0, getCurrentWorkingDirectory(swddrive));
324         removeDriveInPlace(swdcleaned, swddrive);
325     }
326 
327     /* CHECKING IF THE SWD SHOULD BE PREPENDED TO THE PATH */
328     // If pathname starts with "/", use it for the whole path (but deal with
329     // drive later).
330     if (pathcleaned.substr(0, 1) == "/") {
331         processed = pathcleaned;
332     }
333     // If path starts with "./": remove the "./", concatenate with swdcleaned.
334     else if (pathcleaned.substr(0, 2) == "./") {
335         pathcleaned.erase(0, 2);
336         processed = swdcleaned + pathcleaned;
337     }
338     // Looks like a relative pathname (i.e. pathcleaned starts immediately with
339     // a directory or file name).
340     else {
341         processed = swdcleaned + pathcleaned;
342     }
343 
344     /* RESOLVING THE FULL PATH */
345     // May have picked up some backslashes through getCurrentWorkingDirectory().
346     processed.replaceAllChar('\\', '/');
347 
348     // If the pathname in its entirety is just one of these, append
349     // a slash to avoid special cases below.
350     if (processed == "." || processed == ".." || processed == "@")
351         processed += "/";
352 
353     // If the path begins with "../" we'll make it ./../ to simplify handling.
354     if (processed.substr(0, 3) == "../")
355         processed.insert(0, "./");
356 
357     // Full path is determined except for drive letter. Resolve drive letter
358     // with swd, then path, then current drive.
359     if (processed.substr(0, 1) == "/") {
360         if (!swddrive.empty()) finaldrive = swddrive;
361         else if (!pathdrive.empty()) finaldrive = pathdrive;
362         else finaldrive = getCurrentDriveLetter();
363     }
364 
365     else if (processed.substr(0, 2) == "@/") {
366         processed.replace(0, 2, getThisExecutableDirectory());
367         removeDriveInPlace(processed, finaldrive);
368     }
369     // Looks like a relative pathname. But if either path had
370     // an initial drive specification, e.g. X:something.txt, that is
371     // supposed to be interpreted relative to the current working directory
372     // on drive X, just as though it were X:./something.txt.
373     // Note that we do not need to check swd as it has either been preprocessed
374     // (and thus has been taken care of in the "/" case) or is of the form
375     // "folder/file.ext".
376     else if (!pathdrive.empty()) {
377         processed.insert(0, getCurrentWorkingDirectory(pathdrive));
378         removeDriveInPlace(processed, finaldrive);
379     }
380 
381     // Must be a relative pathname. Just prepend the current working directory.
382     else {
383         processed.insert(0, getCurrentWorkingDirectory());
384         removeDriveInPlace(processed, finaldrive);
385     }
386 
387     // Build up the final pathname, then use deconstructAbsolutePathname() to
388     // find the final directory, fileName, and extension.
389     if (processed.substr(0, 1) != "/") processed.insert(0, "/");
390     if (!finaldrive.empty()) processed.insert(0, finaldrive + ":");
391     deconstructAbsolutePathname(processed, directory, fileName, extension);
392 }
393 
fileExists(const std::string & fileName)394 bool Pathname::fileExists(const std::string& fileName) {
395     std::ifstream f(fileName.c_str());
396     return f.good();
397     // File is closed by destruction of f.
398 }
399 
getDefaultInstallDir()400 string Pathname::getDefaultInstallDir() {
401     string installDir;
402     #ifdef _WIN32
403         installDir = getEnvironmentVariable("ProgramFiles");
404         if (!installDir.empty()) {
405             makeNativeSlashesInPlace(installDir);
406             addFinalSeparatorInPlace(installDir);
407         } else
408             installDir = "c:\\Program Files\\";
409     #else
410         installDir = "/usr/local/";
411     #endif
412     return installDir;
413 }
414 
addDirectoryOffset(const string & base,const string & offset)415 string Pathname::addDirectoryOffset(const string& base, const string& offset) {
416     string cleanOffset = beginsWithPathSeparator(offset)
417         ? offset.substr(1) : offset; // remove leading path separator
418     if (endsWithPathSeparator(cleanOffset))
419         cleanOffset.erase(cleanOffset.size()-1); // remove final separator
420     string result = endsWithPathSeparator(base)
421         ? base.substr(0, base.size()-1) : base;  // remove final separator
422     result += MyPathSeparator + cleanOffset + MyPathSeparator;
423     makeNativeSlashesInPlace(result);
424     return result;
425 }
426 
getInstallDir(const std::string & envInstallDir,const std::string & offsetFromDefaultInstallDir)427 string Pathname::getInstallDir(const std::string& envInstallDir,
428                                  const std::string& offsetFromDefaultInstallDir)
429 {   std::string installDir = getEnvironmentVariable(envInstallDir);
430     if (!installDir.empty()) {
431         makeNativeSlashesInPlace(installDir);
432         addFinalSeparatorInPlace(installDir);
433     } else
434         installDir = addDirectoryOffset(getDefaultInstallDir(), offsetFromDefaultInstallDir);
435     return installDir;
436 }
437 
getThisExecutablePath()438 string Pathname::getThisExecutablePath() {
439     char buf[2048];
440     #if defined(_WIN32)
441         const DWORD nBytes = GetModuleFileName((HMODULE)0, (LPTSTR)buf, sizeof(buf));
442         buf[0] = (char)tolower(buf[0]); // drive name
443     #elif defined(__APPLE__)
444         uint32_t sz = (uint32_t)sizeof(buf);
445         const int status = _NSGetExecutablePath(buf, &sz);
446         SimTK_ERRCHK_ALWAYS(status==0,
447                 "Pathname::getThisExecutablePath()",
448                 "2048-byte buffer is not big enough to store executable path.");
449     #elif defined(__FreeBSD__) || defined(__DragonFly__)
450         // This isn't automatically null terminated.
451         const size_t nBytes = readlink("/proc/curproc/file", buf, sizeof(buf));
452         buf[nBytes] = '\0';
453     #else // Linux
454         // This isn't automatically null terminated.
455         const size_t nBytes = readlink("/proc/self/exe", buf, sizeof(buf));
456         buf[nBytes] = '\0';
457     #endif
458     return string(buf);
459 }
460 
getThisExecutableDirectory()461 string Pathname::getThisExecutableDirectory() {
462     string path = getThisExecutablePath();
463     string component;
464     removeLastPathComponentInPlace(path, component);
465     path += MyPathSeparator;
466     return path;
467 }
468 
getFunctionLibraryDirectory(void * func,std::string & path)469 bool Pathname::getFunctionLibraryDirectory(void* func,
470                                            std::string& path) {
471     #if defined(_WIN32)
472         return false;
473     #else
474         // Dl_info and dladdr are defined in dlfcn.h. dladdr() is not POSIX,
475         // but is provided on macOS and in Glibc (at least on Ubuntu). It is
476         // likely not available on all UNIX variants.
477         Dl_info dl_info;
478         int status = dladdr(func, &dl_info);
479         // Returns 0 on error, non-zero on success.
480         if (status == 0) return false;
481 
482         // Convert the path to an absolute path and get rid of the filename.
483         // On macOS, dli_fname is an absolute path; on Ubuntu, it's relative to
484         // the working directory.
485         bool dontApplySearchPath;
486         std::string filename, extension;
487         deconstructPathname(dl_info.dli_fname, dontApplySearchPath, path,
488                             filename, extension);
489         return true;
490     #endif
491 }
492 
getCurrentDriveLetter()493 string Pathname::getCurrentDriveLetter() {
494     #ifdef _WIN32
495         const int which = _getdrive();
496         return string() + (char)('a' + which-1);
497     #else
498         return string();
499     #endif
500 }
501 
getCurrentDrive()502 string Pathname::getCurrentDrive() {
503     #ifdef _WIN32
504         return getCurrentDriveLetter() + ":";
505     #else
506         return string();
507     #endif
508 }
509 
510 
getCurrentWorkingDirectory(const string & drive)511 string Pathname::getCurrentWorkingDirectory(const string& drive) {
512     char buf[2048];
513 
514     #ifdef _WIN32
515         const int which = drive.empty() ? 0 : (tolower(drive[0]) - 'a') + 1;
516         assert(which >= 0);
517         if (which != 0) {
518             // Make sure this drive exists.
519             const ULONG mask = _getdrives();
520             if (!(mask & (1<<(which-1))))
521                 return getRootDirectory(drive);
522         }
523         _getdcwd(which, buf, sizeof(buf));
524         buf[0] = (char)tolower(buf[0]); // drive letter
525     #else
526         const char* bufp = getcwd(buf, sizeof(buf));
527         SimTK_ERRCHK_ALWAYS(bufp != 0,
528             "Pathname::getCurrentWorkingDirectory()",
529             "2048-byte buffer not big enough for current working directory.");
530     #endif
531 
532     string cwd(buf);
533     if (cwd.size() && cwd[cwd.size()-1] != MyPathSeparator)
534         cwd += MyPathSeparator;
535     return cwd;
536 }
537 
getRootDirectory(const string & drive)538 string Pathname::getRootDirectory(const string& drive) {
539     #ifdef _WIN32
540         if (drive.empty())
541             return getCurrentDrive() + getPathSeparator();
542         return String(drive[0]).toLower() + ":" + getPathSeparator();
543     #else
544         return getPathSeparator();
545     #endif
546 }
547 
environmentVariableExists(const string & name)548 bool Pathname::environmentVariableExists(const string& name) {
549     return getenv(name.c_str()) != 0;
550 }
551 
getEnvironmentVariable(const string & name)552 string Pathname::getEnvironmentVariable(const string& name) {
553     char* value;
554     value = getenv(name.c_str());
555     return value ? string(value) : string();
556 }
557 
558 
559