Vuo  2.3.2
VuoFileUtilities.cc
Go to the documentation of this file.
1 
10 #include <CommonCrypto/CommonDigest.h>
11 #include <dirent.h>
12 #include <spawn.h>
13 #include <sstream>
14 #include <sys/mman.h>
15 #include <sys/stat.h>
16 #include <copyfile.h>
17 #include <fstream>
18 #include <iomanip>
19 #include <iostream>
20 #include <mach-o/dyld.h>
21 #include <sys/time.h>
22 #include "VuoException.hh"
23 #include "VuoFileUtilities.hh"
24 #include "VuoFileUtilitiesCocoa.hh"
25 #include "VuoStringUtilities.hh"
26 
27 
38 void VuoFileUtilities::splitPath(string path, string &dir, string &file, string &extension)
39 {
40  dir = "";
41  file = "";
42  extension = "";
43  const char FILE_SEPARATOR = '/';
44 
45  size_t separatorIndex = path.rfind(FILE_SEPARATOR);
46  string fileAndExtension;
47  if (separatorIndex != string::npos)
48  {
49  dir = path.substr(0, separatorIndex + 1);
50  if (separatorIndex < path.length())
51  fileAndExtension = path.substr(separatorIndex + 1, path.length());
52  }
53  else
54  {
55  fileAndExtension = path;
56  }
57 
58  size_t dotIndex = fileAndExtension.rfind(".");
59  if (dotIndex != string::npos)
60  {
61  file = fileAndExtension.substr(0, dotIndex);
62  extension = fileAndExtension.substr(dotIndex + 1, fileAndExtension.length());
63  }
64  else
65  {
66  file = fileAndExtension;
67  }
68 }
69 
75 {
76  // Remove repeated file separators.
77  for (int i = 0; i < path.length(); ++i)
78  if (path[i] == '/')
79  while (i+1 < path.length() && path[i+1] == '/')
80  path.erase(i+1, 1);
81 
82  // Remove trailing file separator.
83  if (path.length() > 1 && VuoStringUtilities::endsWith(path, "/"))
84  path.erase(path.length()-1);
85 }
86 
93 bool VuoFileUtilities::arePathsEqual(string path1, string path2)
94 {
95  canonicalizePath(path1);
96  canonicalizePath(path2);
97  return path1 == path2;
98 }
99 
103 string VuoFileUtilities::getAbsolutePath(const string &path)
104 {
105  if (VuoStringUtilities::beginsWith(path, "/"))
106  return path;
107 
108  char *cwd = getcwd(NULL, 0);
109  if (! cwd)
110  {
111  VUserLog("Couldn't get current working directory: %s", strerror(errno));
112  return path;
113  }
114 
115  string absolutePath = string(cwd) + "/" + path;
116  free(cwd);
117  return absolutePath;
118 }
119 
127 string VuoFileUtilities::makeTmpFile(string file, string extension, string directory)
128 {
129  if (directory.empty())
130  directory = getTmpDir();
131  if (directory.at(directory.length()-1) != '/')
132  directory += "/";
133  string suffix = (extension.empty() ? "" : ("." + extension));
134  string pathTemplate = directory + file + "-XXXXXX" + suffix;
135  char *path = (char *)calloc(pathTemplate.length() + 1, sizeof(char));
136  strncpy(path, pathTemplate.c_str(), pathTemplate.length());
137  int fd = mkstemps(path, suffix.length());
138  close(fd);
139  string pathStr(path);
140  free(path);
141  return pathStr;
142 }
143 
153 string VuoFileUtilities::makeTmpDir(string prefix)
154 {
155  string pathTemplate = getTmpDir() + "/" + prefix + ".XXXXXX";
156  char *path = (char *)calloc(pathTemplate.length() + 1, sizeof(char));
157  strncpy(path, pathTemplate.c_str(), pathTemplate.length());
158  mkdtemp(path);
159  string pathStr(path);
160  free(path);
161  return pathStr;
162 }
163 
173 {
175 }
176 
189 {
190  char *cwd = getcwd(NULL, 0);
191  if (!cwd)
192  VUserLog("Error in getcwd(): %s", strerror(errno));
193 
194  if (cwd && strstr(cwd, "/Library/Containers/")) // We're in a sandbox.
195  {
196  // https://b33p.net/kosada/node/16374
197  // In the macOS 10.15 screen saver sandbox, at least,
198  // we're not allowed to use the system or user temporary directories for socket-files,
199  // but we are allowed to use the sandbox container directory.
200  string cwdS(cwd);
201  free(cwd);
202  return VuoStringUtilities::endsWith(cwdS, "/") ? VuoStringUtilities::substrBefore(cwdS, "/") : cwdS;
203  }
204  free(cwd);
205 
206  char userTempDir[PATH_MAX];
207  if (confstr(_CS_DARWIN_USER_TEMP_DIR, userTempDir, PATH_MAX) > 0)
208  {
209  string userTempDirS(userTempDir);
210  return VuoStringUtilities::endsWith(userTempDirS, "/") ? VuoStringUtilities::substrBefore(userTempDirS, "/") : userTempDirS;
211  }
212 
213  return "/tmp";
214 }
215 
223 void VuoFileUtilities::makeDir(string path)
224 {
225  if (path.empty())
226  throw VuoException("Couldn't create directory with empty path");
227 
228  if (dirExists(path))
229  return;
230 
231  const char FILE_SEPARATOR = '/';
232  size_t lastNonSeparator = path.find_last_not_of(FILE_SEPARATOR);
233  if (lastNonSeparator != string::npos)
234  path.resize(lastNonSeparator + 1);
235 
236  string parentDir, file, ext;
237  splitPath(path, parentDir, file, ext);
238  makeDir(parentDir);
239 
240  // `mkdir` ANDs the mode with the process's umask,
241  // so by default the created directory's mode will end up being 0755.
242  int ret = mkdir(path.c_str(), 0777);
243  if (ret != 0 && ! dirExists(path))
244  throw VuoException((string("Couldn't create directory \"" + path + "\": ") + strerror(errno)).c_str());
245 }
246 
254 {
255  static string frameworkPath;
256  static dispatch_once_t once = 0;
257  dispatch_once(&once, ^{
258 
259  // First check whether Vuo.framework is in the list of loaded dynamic libraries.
260  const char *frameworkPathFragment = "/Vuo.framework/Versions/";
261  uint32_t imageCount = _dyld_image_count();
262  for (uint32_t i = 0; i < imageCount; ++i)
263  {
264  const char *dylibPath = _dyld_get_image_name(i);
265  const char *found = strstr(dylibPath, frameworkPathFragment);
266  if (found)
267  {
268  char *pathC = strndup(dylibPath, found - dylibPath);
269  string path = string(pathC) + "/Vuo.framework";
270  free(pathC);
271  if (fileExists(path))
272  {
273  frameworkPath = path;
274  return;
275  }
276  }
277  }
278 
279  // Failing that, check for a Vuo.framework bundled with the executable app.
280  char executablePath[PATH_MAX + 1];
281  uint32_t size = sizeof(executablePath);
282 
283  if (! _NSGetExecutablePath(executablePath, &size))
284  {
285  char cleanedExecutablePath[PATH_MAX + 1];
286 
287  realpath(executablePath, cleanedExecutablePath); // remove extra references (e.g., "/./")
288  string path = cleanedExecutablePath;
289  string dir, file, ext;
290  splitPath(path, dir, file, ext); // remove executable name
291  path = dir.substr(0, dir.length() - 1); // remove "/"
292  splitPath(path, dir, file, ext); // remove "MacOS"
293  path = dir.substr(0, dir.length() - 1); // remove "/"
294  path += "/Frameworks/Vuo.framework";
295 
296  if (fileExists(path))
297  {
298  frameworkPath = path;
299  return;
300  }
301  }
302 
303  // Give up.
304  });
305 
306  return frameworkPath;
307 }
308 
314 {
315  static string runnerFrameworkPath;
316  static dispatch_once_t once = 0;
317  dispatch_once(&once, ^{
318  // Check for VuoRunner.framework alongside Vuo.framework.
319  string possibleFrameworkPath = getVuoFrameworkPath() + "/../VuoRunner.framework";
320  if (dirExists(possibleFrameworkPath))
321  {
322  runnerFrameworkPath = possibleFrameworkPath;
323  return;
324  }
325 
326  // Failing that, check whether VuoRunner.framework is in the list of loaded dynamic libraries.
327  const char *frameworkPathFragment = "/VuoRunner.framework/Versions/";
328  uint32_t imageCount = _dyld_image_count();
329  for (uint32_t i = 0; i < imageCount; ++i)
330  {
331  const char *dylibPath = _dyld_get_image_name(i);
332  const char *found = strstr(dylibPath, frameworkPathFragment);
333  if (found)
334  {
335  char *pathC = strndup(dylibPath, found - dylibPath);
336  string path = string(pathC) + "/VuoRunner.framework";
337  free(pathC);
338  if (fileExists(path))
339  {
340  runnerFrameworkPath = path;
341  return;
342  }
343  }
344  }
345 
346  // Failing that, check for a VuoRunner.framework bundled with the executable app.
347  char executablePath[PATH_MAX + 1];
348  uint32_t size = sizeof(executablePath);
349 
350  if (! _NSGetExecutablePath(executablePath, &size))
351  {
352  char cleanedExecutablePath[PATH_MAX + 1];
353 
354  realpath(executablePath, cleanedExecutablePath); // remove extra references (e.g., "/./")
355  string path = cleanedExecutablePath;
356  string dir, file, ext;
357  splitPath(path, dir, file, ext); // remove executable name
358  path = dir.substr(0, dir.length() - 1); // remove "/"
359  splitPath(path, dir, file, ext); // remove "MacOS"
360  path = dir.substr(0, dir.length() - 1); // remove "/"
361  path += "/Frameworks/VuoRunner.framework";
362 
363  if (fileExists(path))
364  {
365  runnerFrameworkPath = path;
366  return;
367  }
368  }
369 
370  // Give up.
371  });
372  return runnerFrameworkPath;
373 }
374 
383 string VuoFileUtilities::getCompositionLocalModulesPath(const string &compositionPath)
384 {
385  string compositionDir, compositionFile, ext;
386  splitPath(getAbsolutePath(compositionPath), compositionDir, compositionFile, ext);
387  canonicalizePath(compositionDir);
388 
389  string parentDir, compositionDirName;
390  splitPath(compositionDir, parentDir, compositionDirName, ext);
391  canonicalizePath(compositionDirName);
392 
393  string compositionBaseDir = (compositionDirName == "Modules" ? parentDir : compositionDir);
394 
395  string compositionModulesDir = compositionBaseDir + "/Modules";
396  canonicalizePath(compositionModulesDir);
397 
398  return compositionModulesDir;
399 }
400 
405 {
406  return string(getenv("HOME")) + "/Library/Application Support/Vuo/Modules";
407 }
408 
413 {
414  return "/Library/Application Support/Vuo/Modules";
415 }
416 
421 {
422  return string(getenv("HOME")) + "/Library/Caches/org.vuo/" + VUO_VERSION_AND_BUILD_STRING;
423 }
424 
429 {
430  if (path.empty())
431  return false;
432 
433  string compositionLocalModulesPath = getCompositionLocalModulesPath(path);
434 
435  string dir, file, ext;
436  VuoFileUtilities::splitPath(path, dir, file, ext);
437 
438  return VuoFileUtilities::arePathsEqual(compositionLocalModulesPath, dir);
439 }
440 
449 void VuoFileUtilities::preserveOriginalFileName(string &fileContents, string originalFileName)
450 {
451  fileContents.insert(getFirstInsertionIndex(fileContents), "#line 1 \"" + originalFileName + "\"\n");
452 }
453 
459 {
460  string bomUtf8 = "\xEF\xBB\xBF";
461  string bomUtf16Be = "\xFE\xFF";
462  string bomUtf16Le = "\xFF\xFE";
463  string boms[] = { bomUtf8, bomUtf16Be, bomUtf16Le };
464  for (int i = 0; i < 3; ++i)
465  {
466  bool isMatch = true;
467  for (int j = 0; j < boms[i].length(); ++j)
468  if (boms[i][j] != s[j])
469  isMatch = false;
470 
471  if (isMatch)
472  return boms[i].length();
473  }
474  return 0;
475 }
476 
481 {
482  // https://stackoverflow.com/questions/201992/how-to-read-until-eof-from-cin-in-c
483 
484  string contents;
485  string line;
486  while (getline(cin, line))
487  contents += line;
488 
489  return contents;
490 }
491 
498 {
499  // https://stackoverflow.com/questions/2602013/read-whole-ascii-file-into-c-stdstring
500 
501  ifstream in(path.c_str(), ios::in | ios::binary);
502  if (! in)
503  throw VuoException(string("Couldn't read file: ") + strerror(errno) + " — " + path);
504 
505  string contents;
506  in.seekg(0, ios::end);
507  contents.resize(in.tellg());
508  in.seekg(0, ios::beg);
509  in.read(&contents[0], contents.size());
510  in.close();
511  return contents;
512 }
513 
521 void VuoFileUtilities::writeRawDataToFile(const char *data, size_t numBytes, string file)
522 {
523  FILE *f = fopen(file.c_str(), "wb");
524  if (! f)
525  throw VuoException(string("Couldn't open file for writing: ") + strerror(errno) + " — " + file);
526 
527  size_t numBytesWritten = fwrite(data, sizeof(char), numBytes, f);
528  if (numBytesWritten != numBytes)
529  {
530  fclose(f);
531  throw VuoException(string("Couldn't write all data: ") + strerror(errno) + " — " + file);
532  }
533 
534  fclose(f);
535 }
536 
544 void VuoFileUtilities::writeStringToFile(string s, string file)
545 {
546  writeRawDataToFile(s.data(), s.length(), file);
547 }
548 
557 void VuoFileUtilities::writeStringToFileSafely(string s, string path)
558 {
559  string dir, file, ext;
560  splitPath(path, dir, file, ext);
561  string tmpPath = makeTmpFile("." + file, ext, dir);
562 
563  writeStringToFile(s, tmpPath);
564 
565  moveFile(tmpPath, path);
566 }
567 
574 {
577 
578  // `access()` automatically dereferences symlinks.
579  return access(path.c_str(), 0) == 0;
580 }
581 
588 {
591 
592  // `stat()` automatically dereferences symlinks.
593  struct stat st_buf;
594  int status = stat(path.c_str(), &st_buf);
595  return (! status && S_ISDIR(st_buf.st_mode));
596 }
597 
602 {
603  struct stat st_buf;
604  int status = lstat(path.c_str(), &st_buf);
605  return (! status && S_ISLNK(st_buf.st_mode));
606 }
607 
614 {
617 
618  // `access()` automatically dereferences symlinks.
619  return access(path.c_str(), R_OK) == 0;
620 }
621 
628 {
631 
632  // `fopen()` automatically dereferences symlinks.
633  FILE *f = fopen(path.c_str(), "rb");
634  if (!f)
635  return false;
636 
637  fseek(f, 0, SEEK_END);
638  long pos = ftell(f);
639  if (pos <= 0)
640  {
641  fclose(f);
642  return false;
643  }
644 
645  // `fopen` and `fseek` both succeed on directories, so also attempt to actually read the data.
646  fseek(f, 0, SEEK_SET);
647  char z;
648  size_t ret = fread(&z, 1, 1, f);
649  fclose(f);
650  return ret > 0;
651 }
652 
657 {
658  FILE *f = fopen(path.c_str(), "a");
659  fclose(f);
660 }
661 
666 {
667  int ret = remove(path.c_str());
668  if (ret != 0 && fileExists(path))
669  VUserLog("Couldn't delete file: %s — %s", strerror(errno), path.c_str());
670 }
671 
676 {
677  if (! dirExists(path))
678  return;
679 
680  DIR *d = opendir(path.c_str());
681  if (! d)
682  {
683  VUserLog("Couldn't read directory: %s — %s", strerror(errno), path.c_str());
684  return;
685  }
686 
687  struct dirent *de;
688  while( (de=readdir(d)) )
689  {
690  string fileName = de->d_name;
691  string filePath = path + "/" + fileName;
692 
693  if (fileName == "." || fileName == "..")
694  continue;
695 
696  if (dirExists(filePath))
697  deleteDir(filePath);
698  else
699  deleteFile(filePath);
700  }
701 
702  closedir(d);
703 
704  deleteFile(path);
705 }
706 
712 void VuoFileUtilities::moveFile(string fromPath, string toPath)
713 {
714  int ret = rename(fromPath.c_str(), toPath.c_str());
715  if (ret != 0)
716  throw VuoException(string("Couldn't move file: ") + strerror(errno) + " — " + toPath);
717 }
718 
725 {
727 }
728 
740 void VuoFileUtilities::copyFile(string fromPath, string toPath, bool preserveMetadata)
741 {
742  int i = open(fromPath.c_str(), O_RDONLY);
743  if (i == -1)
744  throw VuoException(string("Couldn't open copy source: ") + strerror(errno) + " — " + fromPath);
745 
746  struct stat s;
747  fstat(i, &s);
748  int o = open(toPath.c_str(), O_WRONLY | O_CREAT, s.st_mode & 0777);
749  if (o == -1)
750  {
751  close(i);
752  throw VuoException(string("Couldn't open copy destination: ") + strerror(errno) + " — " + toPath);
753  }
754 
755  int ret = fcopyfile(i, o, NULL, COPYFILE_DATA | (preserveMetadata ? COPYFILE_STAT : 0));
756  char *e = strerror(errno);
757  close(o);
758  close(i);
759 
760  if (ret)
761  throw VuoException(string("Couldn't copy ") + fromPath + " to " + toPath + ": " + e);
762 
763  VDebugLog("%s -> %s", fromPath.c_str(), toPath.c_str());
764 }
765 
775 void VuoFileUtilities::copyDirectory(string fromPath, string toPath)
776 {
777  if (isSymlink(fromPath))
778  {
779  // Preserve the relative symlinks found in framework bundles, instead of flattening them.
780  char linkDestination[PATH_MAX + 1];
781  ssize_t len = readlink(fromPath.c_str(), linkDestination, PATH_MAX);
782  linkDestination[len] = 0; // "readlink does not append a NUL character to buf."
783  if (symlink(linkDestination, toPath.c_str()) == -1)
784  throw VuoException(string("Couldn't copy symlink \"") + fromPath + "\" to \"" + toPath + "\": " + strerror(errno));
785 
786  VDebugLog("%s -> %s (symlink)", fromPath.c_str(), toPath.c_str());
787  }
788 
789  else if (!dirExists(fromPath))
790  copyFile(fromPath, toPath);
791 
792  else
793  {
794  VDebugLog("%s ::", fromPath.c_str());
795  auto files = findAllFilesInDirectory(fromPath);
796  makeDir(toPath);
797  for (auto file : files)
798  {
799  string sourceFile = fromPath + "/" + file->getRelativePath();
800  string targetFile = toPath + "/" + file->getRelativePath();
801  copyDirectory(sourceFile, targetFile);
802  }
803  }
804 }
805 
811 string VuoFileUtilities::calculateFileSHA256(const string &path)
812 {
813  int fd = open(path.c_str(), O_RDONLY);
814  if (fd < 0)
815  throw VuoException("Error: Couldn't open \"" + path + "\": " + strerror(errno));
816  VuoDefer(^{ close(fd); });
817 
818  struct stat stat;
819  if (fstat(fd, &stat) != 0)
820  throw VuoException("Error: Couldn't fstat \"" + path + "\": " + strerror(errno));
821 
822  // Instead of reading the file into a string and calling `VuoStringUtilities::calculateSHA256`,
823  // use `mmap` so the OS can efficiently read parts of the file at a time (reducing memory required).
824  void *data = mmap(nullptr, stat.st_size, PROT_READ, MAP_PRIVATE | MAP_NOCACHE, fd, 0);
825  if (data == MAP_FAILED)
826  throw VuoException("Error: Couldn't mmap \"" + path + "\": " + strerror(errno));
827  VuoDefer(^{ munmap(data, stat.st_size); });
828 
829  unsigned char hash[CC_SHA256_DIGEST_LENGTH];
830  if (!CC_SHA256(data, stat.st_size, hash))
831  throw VuoException("Error: CC_SHA256 failed on file \"" + path + "\"");
832 
833  ostringstream oss;
834  oss << setfill('0') << hex;
835  for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; ++i)
836  oss << setw(2) << (int)hash[i];
837  return oss.str();
838 }
839 
845 {
846  struct stat s;
847  lstat(path.c_str(), &s);
848  return s.st_mtimespec.tv_sec; // s.st_mtimespec.tv_nsec is always 0 on some OSes, hence the resolution of 1 second
849 }
850 
855 {
856  struct stat s;
857  lstat(path.c_str(), &s);
858 
859  struct timeval t;
860  gettimeofday(&t, NULL);
861 
862  return t.tv_sec - s.st_atimespec.tv_sec;
863 }
864 
879 set<VuoFileUtilities::File *> VuoFileUtilities::findAllFilesInDirectory(string dirPath, set<string> archiveExtensions,
880  bool shouldSearchRecursively)
881 {
882  set<File *> files;
883 
885  dirPath = VuoFileUtilitiesCocoa_resolveMacAlias(dirPath);
886 
887  // `opendir()` automatically dereferences symlinks.
888  DIR *d = opendir(dirPath.c_str());
889  if (! d)
890  {
891  if (access(dirPath.c_str(), F_OK) != -1)
892  throw VuoException(string("Couldn't read directory: ") + strerror(errno) + " — " + dirPath);
893  return files;
894  }
895 
896  struct dirent *de;
897  while( (de=readdir(d)) )
898  {
899  string fileName = de->d_name;
900  string relativeFilePath = dirPath + "/" + fileName;
901 
902  if (fileName == "." || fileName == "..")
903  continue;
904 
905  bool isArchive = false;
906  for (set<string>::iterator archiveExtension = archiveExtensions.begin(); archiveExtension != archiveExtensions.end(); ++archiveExtension)
907  if (VuoStringUtilities::endsWith(fileName, "." + *archiveExtension))
908  isArchive = true;
909 
910  if (isArchive)
911  {
912  set<File *> fs = findAllFilesInArchive(relativeFilePath);
913  if (fs.empty())
914  isArchive = false;
915  else
916  files.insert(fs.begin(), fs.end());
917  }
918 
919  if (! isArchive)
920  {
921  bool shouldSearchDir = shouldSearchRecursively
922  && dirExists(relativeFilePath)
923  && !isSymlink(relativeFilePath);
924  if (shouldSearchDir)
925  {
926  set<File *> filesInDir = findAllFilesInDirectory(relativeFilePath, archiveExtensions, true);
927  for (set<File *>::iterator i = filesInDir.begin(); i != filesInDir.end(); ++i)
928  {
929  File *f = *i;
930  f->dirPath = dirPath;
931  f->filePath = fileName + "/" + f->filePath;
932  }
933  files.insert(filesInDir.begin(), filesInDir.end());
934  }
935  else
936  {
937  File *f = new File(dirPath, fileName);
938  files.insert(f);
939  }
940  }
941  }
942 
943  closedir(d);
944 
945  return files;
946 }
947 
957 set<VuoFileUtilities::File *> VuoFileUtilities::findFilesInDirectory(string dirPath, set<string> extensions, set<string> archiveExtensions)
958 {
959  set<File *> allFiles = findAllFilesInDirectory(dirPath, archiveExtensions);
960  set<File *> matchingFiles;
961 
962  for (set<File *>::iterator i = allFiles.begin(); i != allFiles.end(); ++i)
963  {
964  bool endsWithExtension = false;
965  for (set<string>::iterator extension = extensions.begin(); extension != extensions.end(); ++extension)
966  if (VuoStringUtilities::endsWith((*i)->getRelativePath(), "." + *extension))
967  endsWithExtension = true;
968 
969  if (endsWithExtension && ((*i)->isInArchive() || !VuoFileUtilities::dirExists((*i)->path())))
970  matchingFiles.insert(*i);
971  else
972  delete *i;
973  }
974 
975  return matchingFiles;
976 }
977 
984 set<VuoFileUtilities::File *> VuoFileUtilities::findAllFilesInArchive(string archivePath)
985 {
986  set<File *> files;
987 
988  Archive *archive = new Archive(archivePath);
989  if (! archive->zipArchive)
990  {
991  delete archive;
992  return files;
993  }
994 
995  mz_uint numFiles = mz_zip_reader_get_num_files(archive->zipArchive);
996  for (mz_uint i = 0; i < numFiles; ++i)
997  {
998  mz_zip_archive_file_stat file_stat;
999  bool success = mz_zip_reader_file_stat(archive->zipArchive, i, &file_stat);
1000  if (! success)
1001  {
1002  VUserLog("Error: Couldn't read file '%u' in zip archive '%s'.", i, archive->path.c_str());
1003  break;
1004  }
1005 
1006  File *f = new File(archive, file_stat.m_filename);
1007  files.insert(f);
1008  }
1009 
1010  if (archive->referenceCount == 0)
1011  delete archive;
1012 
1013  return files;
1014 }
1015 
1025 set<VuoFileUtilities::File *> VuoFileUtilities::findFilesInArchive(string archivePath, string dirPath, set<string> extensions)
1026 {
1027  set<File *> allFiles = findAllFilesInArchive(archivePath);
1028  set<File *> matchingFiles;
1029 
1030  for (set<File *>::iterator i = allFiles.begin(); i != allFiles.end(); ++i)
1031  {
1032  File *f = *i;
1033 
1034  string dir, file, ext;
1035  splitPath(f->getRelativePath(), dir, file, ext);
1036  bool beginsWithDir = VuoStringUtilities::beginsWith(dir, dirPath);
1037 
1038  bool endsWithExtension = false;
1039  for (set<string>::iterator extension = extensions.begin(); extension != extensions.end(); ++extension)
1040  if (VuoStringUtilities::endsWith(f->getRelativePath(), "." + *extension))
1041  endsWithExtension = true;
1042 
1043  if (beginsWithDir && endsWithExtension)
1044  matchingFiles.insert(f);
1045  else
1046  delete f;
1047  }
1048 
1049  return matchingFiles;
1050 }
1051 
1061 string VuoFileUtilities::getArchiveFileContentsAsString(string archivePath, string filePath)
1062 {
1063  string contents;
1064  set<File *> archiveFiles = findAllFilesInArchive(archivePath);
1065  for (set<File *>::iterator i = archiveFiles.begin(); i != archiveFiles.end(); ++i)
1066  {
1067  File *file = *i;
1068  if (filePath == file->getRelativePath())
1069  contents = file->getContentsAsString();
1070  delete file;
1071  }
1072 
1073  return contents;
1074 }
1075 
1084 {
1086 }
1087 
1094  path(path)
1095 {
1096  this->referenceCount = 0;
1097 
1098  // mz_zip_reader_init_file sometimes ends up with a garbage function pointer if the mz_zip_archive isn't initialized to zeroes
1099  this->zipArchive = (mz_zip_archive *)calloc(1, sizeof(mz_zip_archive));
1100 
1101  bool success = mz_zip_reader_init_file(zipArchive, path.c_str(), 0);
1102  if (! success)
1103  {
1104  free(zipArchive);
1105  zipArchive = NULL;
1106  }
1107 }
1108 
1113 {
1114  if (zipArchive)
1115  {
1116  mz_zip_reader_end(zipArchive);
1117  free(zipArchive);
1118  }
1119 }
1120 
1127 {
1128 }
1129 
1133 VuoFileUtilities::File::File(string dirPath, string filePath) :
1134  filePath(filePath),
1135  dirPath(dirPath)
1136 {
1137  this->fileDescriptor = -1;
1138  this->archive = NULL;
1139 }
1140 
1147 VuoFileUtilities::File::File(Archive *archive, string filePath) :
1148  filePath(filePath)
1149 {
1150  this->fileDescriptor = -1;
1151  this->archive = archive;
1152  ++archive->referenceCount;
1153 }
1154 
1160 {
1161  string newFilePath = basename() + "." + extension;
1162 
1163  if (archive)
1164  return File(archive, newFilePath);
1165  else
1166  return File(dirPath, newFilePath);
1167 }
1168 
1174 {
1175  if (archive && --archive->referenceCount == 0)
1176  delete archive;
1177 }
1178 
1183 {
1184  return (archive != NULL);
1185 }
1186 
1193 {
1194  return (archive ? archive->path : "");
1195 }
1196 
1201 {
1202  return filePath;
1203 }
1204 
1211 {
1212  if (archive)
1213  throw VuoException("Can't return a simple absolute path for a file in an archive.");
1214 
1215  return dirPath + "/" + filePath;
1216 }
1217 
1224 {
1225  if (archive)
1226  throw VuoException("Can't return a simple absolute path for a file in an archive.");
1227 
1228  return dirPath;
1229 }
1230 
1235 {
1236  return filePath.substr(0, filePath.find_last_of('.'));
1237 }
1238 
1243 {
1244  return filePath.substr(filePath.find_last_of('.') + 1);
1245 }
1246 
1251 {
1252  if (archive)
1253  return mz_zip_reader_locate_file(archive->zipArchive, filePath.c_str(), nullptr, MZ_ZIP_FLAG_CASE_SENSITIVE) != -1;
1254  else
1255  return VuoFileUtilities::fileExists((dirPath.empty() ? "" : (dirPath + "/")) + filePath);
1256 }
1257 
1267 {
1268  char * buffer;
1269  numBytes = 0;
1270 
1271  if (! archive)
1272  {
1273  // http://www.cplusplus.com/reference/cstdio/fread/
1274 
1275  string fullPath = (dirPath.empty() ? "" : (dirPath + "/")) + filePath;
1276 
1277  FILE *pFile = fopen ( fullPath.c_str() , "rb" );
1278  if (pFile==NULL)
1279  throw VuoException(string("Couldn't open file: ") + strerror(errno) + " — " + fullPath);
1280 
1281  // obtain file size:
1282  fseek (pFile , 0 , SEEK_END);
1283  size_t lSize = ftell (pFile);
1284  rewind (pFile);
1285 
1286  // allocate memory to contain the whole file:
1287  buffer = (char*) malloc (sizeof(char)*lSize);
1288 
1289  // copy the file into the buffer:
1290  numBytes = fread (buffer,1,lSize,pFile);
1291  fclose(pFile);
1292  if (numBytes != lSize)
1293  {
1294  free(buffer);
1295  throw VuoException(string("Couldn't read file: ") + strerror(errno) + " — " + fullPath);
1296  }
1297  }
1298  else
1299  {
1300  buffer = (char *)mz_zip_reader_extract_file_to_heap(archive->zipArchive, filePath.c_str(), &numBytes, 0);
1301  }
1302 
1303  return buffer;
1304 }
1305 
1312 {
1313  size_t numBytes;
1314  char *buffer = getContentsAsRawData(numBytes);
1315  string s(buffer, numBytes);
1316  free(buffer);
1317  return s;
1318 }
1319 
1330 {
1331  if (fileDescriptor < 0)
1332  fileDescriptor = open((dirPath + "/" + filePath).c_str(), O_RDONLY);
1333  int ret = flock(fileDescriptor, LOCK_SH | (nonBlocking ? LOCK_NB : 0));
1334  return ret == 0;
1335 }
1336 
1347 {
1348  if (fileDescriptor < 0)
1349  fileDescriptor = open((dirPath + "/" + filePath).c_str(), O_RDONLY);
1350  int ret = flock(fileDescriptor, LOCK_EX | (nonBlocking ? LOCK_NB : 0));
1351  return ret == 0;
1352 }
1353 
1359 {
1360  flock(fileDescriptor, LOCK_UN);
1361  close(fileDescriptor);
1362  fileDescriptor = -1;
1363 }
1364 
1370 void VuoFileUtilities::focusProcess(pid_t pid, bool force)
1371 {
1373 }
1374 
1383 void VuoFileUtilities::executeProcess(vector<string> processAndArgs, vector<string> environment)
1384 {
1385  string binaryDir, binaryFile, binaryExt;
1386  splitPath(processAndArgs[0], binaryDir, binaryFile, binaryExt);
1387 
1388  string errorPrefix = "Couldn't execute " + binaryFile + ": ";
1389 
1390  // Capture stdout and stderr.
1391  int outputPipe[2];
1392  int ret = pipe(outputPipe);
1393  if (ret)
1394  throw VuoException(errorPrefix + "couldn't open pipe: " + strerror(ret));
1395  int pipeRead = outputPipe[0];
1396  int pipeWrite = outputPipe[1];
1397  VuoDefer(^{ close(pipeRead); });
1398  posix_spawn_file_actions_t fileActions;
1399  posix_spawn_file_actions_init(&fileActions);
1400  posix_spawn_file_actions_adddup2(&fileActions, pipeWrite, STDOUT_FILENO);
1401  posix_spawn_file_actions_adddup2(&fileActions, pipeWrite, STDERR_FILENO);
1402  posix_spawn_file_actions_addclose(&fileActions, pipeRead);
1403 
1404  // Convert args.
1405  // posix_spawn requires a null-terminated array, which `&processAndArgs[0]` doesn't guarantee.
1406  char **processAndArgsZ = (char **)malloc(sizeof(char *) * (processAndArgs.size() + 1));
1407  int argCount = 0;
1408  string commandLine;
1409  for (auto arg : processAndArgs)
1410  {
1411  processAndArgsZ[argCount++] = strdup(arg.c_str());
1412  if (argCount > 0)
1413  commandLine += ' ';
1414  commandLine += '"' + arg + '"';
1415  }
1416  processAndArgsZ[argCount] = nullptr;
1417 
1418 
1419  // Convert environment.
1420  // posix_spawn requires a null-terminated array, which `&processAndArgs[0]` doesn't guarantee.
1421  char **environmentZ = (char **)malloc(sizeof(char *) * (environment.size() + 1));
1422  int environmentVarCount = 0;
1423  for (auto environmentVar : environment)
1424  {
1425  environmentZ[environmentVarCount++] = strdup(environmentVar.c_str());
1426  commandLine = environmentVar + " " + commandLine;
1427  }
1428  environmentZ[environmentVarCount] = nullptr;
1429 
1430 
1431  // Execute.
1432  pid_t pid;
1433  VDebugLog("%s", commandLine.c_str());
1434  ret = posix_spawn(&pid, processAndArgs[0].c_str(), &fileActions, NULL, (char * const *)processAndArgsZ, (char * const *)environmentZ);
1435  close(pipeWrite);
1436  posix_spawn_file_actions_destroy(&fileActions);
1437  for (int i = 0; i < argCount; ++i)
1438  free(processAndArgsZ[i]);
1439 
1440  if (ret)
1441  throw VuoException(errorPrefix + strerror(ret));
1442  else
1443  {
1444  int ret;
1445  int status;
1446  while (true)
1447  {
1448  ret = waitpid(pid, &status, 0);
1449  if (ret == -1 && errno == EINTR)
1450  // This process received a signal while waiting for the other process
1451  // (seems to happen when running under lldb); try waiting again.
1452  continue;
1453  else if (ret == -1)
1454  throw VuoException(errorPrefix + "waitpid failed: " + strerror(errno));
1455  else if (status != 0)
1456  {
1457  string output;
1458  char buf[256];
1459  bzero(buf, 256);
1460  while (read(pipeRead, &buf, 255) > 0)
1461  {
1462  output += buf;
1463  bzero(buf, 256);
1464  }
1465 
1466  throw VuoException(binaryFile + " failed: " + output);
1467  }
1468  else
1469  // The other process completed without error.
1470  break;
1471  }
1472  }
1473 }
1474 
1479 {
1480  return extension == "vuo";
1481 }
1482 
1487 {
1488  set<string> e;
1489  e.insert("c");
1490  e.insert("cc");
1491  e.insert("C");
1492  e.insert("cpp");
1493  e.insert("cxx");
1494  e.insert("c++");
1495  e.insert("m");
1496  e.insert("mm");
1497  return e;
1498 }
1499 
1504 {
1505  auto extensions = getCSourceExtensions();
1506  return extensions.find(extension) != extensions.end();
1507 }
1508 
1513 {
1514  set<string> e;
1515  e.insert("fs");
1516  e.insert("vs");
1517  e.insert("vuoshader");
1518  e.insert("vuoobjectfilter");
1519  return e.find(extension) != e.end();
1520 }
1521 
1525 string VuoFileUtilities::buildModuleCacheDescription(const string &moduleCachePath, bool generated)
1526 {
1527  return string() + "the cache of " + (generated ? "generated" : "installed") + " modules at '" + moduleCachePath + "'";
1528 }
1529 
1533 string VuoFileUtilities::buildModuleCacheIndexPath(const string &moduleCachePath, bool builtIn, bool generated)
1534 {
1535  string moduleCachePathCanonical = moduleCachePath;
1536  canonicalizePath(moduleCachePathCanonical);
1537 
1538  return moduleCachePathCanonical + "/libVuoModuleCache-" + (generated ? "generated" : "installed") + ".txt";
1539 }
1540 
1546 string VuoFileUtilities::buildModuleCacheDylibPath(const string &moduleCachePath, bool builtIn, bool generated)
1547 {
1548  string moduleCachePathCanonical = moduleCachePath;
1549  canonicalizePath(moduleCachePathCanonical);
1550 
1551  string partialPath = moduleCachePathCanonical + "/libVuoModuleCache-" + (generated ? "generated" : "installed");
1552  if (builtIn)
1553  return partialPath + ".dylib";
1554 
1555  string uniquePath;
1556  do {
1557  uniquePath = partialPath + "-" + VuoStringUtilities::makeRandomHash(4) + ".dylib";
1558  } while (fileExists(uniquePath));
1559  return uniquePath;
1560 }
1561 
1565 string VuoFileUtilities::findLatestRevisionOfModuleCacheDylib(const string &moduleCachePath, bool builtIn, bool generated,
1566  unsigned long &lastModified)
1567 {
1568  string path = buildModuleCacheDylibPath(moduleCachePath, builtIn, generated);
1569 
1570  if (builtIn)
1571  {
1572  if (fileExists(path))
1573  {
1574  lastModified = getFileLastModifiedInSeconds(path);
1575  return path;
1576  }
1577  else
1578  {
1579  lastModified = 0;
1580  return "";
1581  }
1582  }
1583 
1584  string dir, file, ext;
1585  splitPath(path, dir, file, ext);
1586  string fileName = file + "." + ext;
1587 
1588  vector< pair<string, unsigned long> > existingRevisions;
1589  for (File *existingFile : findFilesInDirectory(moduleCachePath, {"dylib"}))
1590  {
1591  if (areDifferentRevisionsOfSameModuleCacheDylib(existingFile->getRelativePath(), fileName))
1592  existingRevisions.push_back({existingFile->path(), getFileLastModifiedInSeconds(existingFile->path())});
1593 
1594  delete existingFile;
1595  }
1596 
1597  if (existingRevisions.empty())
1598  {
1599  lastModified = 0;
1600  return "";
1601  }
1602 
1603  auto latest = std::max_element(existingRevisions.begin(), existingRevisions.end(),
1604  [](pair<string, unsigned long> p1, pair<string, unsigned long> p2) { return p1.second < p2.second; });
1605 
1606  lastModified = latest->second;
1607  return latest->first;
1608 }
1609 
1614 {
1615  string moduleCachePath, file, ext;
1616  splitPath(dylibPath, moduleCachePath, file, ext);
1617  string dylibFileName = file + "." + ext;
1618 
1619  for (File *otherFile : findFilesInDirectory(moduleCachePath, {"dylib"}))
1620  {
1621  if (areDifferentRevisionsOfSameModuleCacheDylib(otherFile->getRelativePath(), dylibFileName))
1622  deleteFile(otherFile->path());
1623 
1624  delete otherFile;
1625  }
1626 }
1627 
1631 bool VuoFileUtilities::areDifferentRevisionsOfSameModuleCacheDylib(const string &dylibPath1, const string &dylibPath2)
1632 {
1633  string dir1, dir2, file1, file2, ext;
1634  splitPath(dylibPath1, dir1, file1, ext);
1635  splitPath(dylibPath2, dir2, file2, ext);
1636 
1637  if (arePathsEqual(dir1, dir2))
1638  {
1639  vector<string> parts1 = VuoStringUtilities::split(file1, '-');
1640  vector<string> parts2 = VuoStringUtilities::split(file2, '-');
1641 
1642  if (parts1.size() == 3 && parts2.size() == 3 && parts1.back() != parts2.back())
1643  {
1644  parts1.pop_back();
1645  parts2.pop_back();
1646  return parts1 == parts2;
1647  }
1648  }
1649 
1650  return false;
1651 }