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