Vuo  2.4.0
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"
25#include "VuoStringUtilities.hh"
26
27
38void 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
93bool VuoFileUtilities::arePathsEqual(string path1, string path2)
94{
95 canonicalizePath(path1);
96 canonicalizePath(path2);
97 return path1 == path2;
98}
99
103string 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
127string 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
153string 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
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 int mkdirErrno = errno;
244 if (ret != 0 && ! dirExists(path))
245 throw VuoException((string("Couldn't create directory \"" + path + "\": ") + strerror(mkdirErrno)).c_str());
246}
247
255{
256 static string frameworkPath;
257 static dispatch_once_t once = 0;
258 dispatch_once(&once, ^{
259
260 // First check whether Vuo.framework is in the list of loaded dynamic libraries.
261 const char *frameworkPathFragment = "/Vuo.framework/Versions/";
262 uint32_t imageCount = _dyld_image_count();
263 for (uint32_t i = 0; i < imageCount; ++i)
264 {
265 const char *dylibPath = _dyld_get_image_name(i);
266 const char *found = strstr(dylibPath, frameworkPathFragment);
267 if (found)
268 {
269 char *pathC = strndup(dylibPath, found - dylibPath);
270 string path = string(pathC) + "/Vuo.framework";
271 free(pathC);
272 if (fileExists(path))
273 {
274 frameworkPath = path;
275 return;
276 }
277 }
278 }
279
280 // Failing that, check for a Vuo.framework bundled with the executable app.
281 char executablePath[PATH_MAX + 1];
282 uint32_t size = sizeof(executablePath);
283
284 if (! _NSGetExecutablePath(executablePath, &size))
285 {
286 char cleanedExecutablePath[PATH_MAX + 1];
287
288 realpath(executablePath, cleanedExecutablePath); // remove extra references (e.g., "/./")
289 string path = cleanedExecutablePath;
290 string dir, file, ext;
291 splitPath(path, dir, file, ext); // remove executable name
292 path = dir.substr(0, dir.length() - 1); // remove "/"
293 splitPath(path, dir, file, ext); // remove "MacOS"
294 path = dir.substr(0, dir.length() - 1); // remove "/"
295 path += "/Frameworks/Vuo.framework";
296
297 if (fileExists(path))
298 {
299 frameworkPath = path;
300 return;
301 }
302 }
303
304 // Give up.
305 });
306
307 return frameworkPath;
308}
309
315{
316 static string runnerFrameworkPath;
317 static dispatch_once_t once = 0;
318 dispatch_once(&once, ^{
319 // Check for VuoRunner.framework alongside Vuo.framework.
320 string possibleFrameworkPath = getVuoFrameworkPath() + "/../VuoRunner.framework";
321 if (dirExists(possibleFrameworkPath))
322 {
323 runnerFrameworkPath = possibleFrameworkPath;
324 return;
325 }
326
327 // Failing that, check whether VuoRunner.framework is in the list of loaded dynamic libraries.
328 const char *frameworkPathFragment = "/VuoRunner.framework/Versions/";
329 uint32_t imageCount = _dyld_image_count();
330 for (uint32_t i = 0; i < imageCount; ++i)
331 {
332 const char *dylibPath = _dyld_get_image_name(i);
333 const char *found = strstr(dylibPath, frameworkPathFragment);
334 if (found)
335 {
336 char *pathC = strndup(dylibPath, found - dylibPath);
337 string path = string(pathC) + "/VuoRunner.framework";
338 free(pathC);
339 if (fileExists(path))
340 {
341 runnerFrameworkPath = path;
342 return;
343 }
344 }
345 }
346
347 // Failing that, check for a VuoRunner.framework bundled with the executable app.
348 char executablePath[PATH_MAX + 1];
349 uint32_t size = sizeof(executablePath);
350
351 if (! _NSGetExecutablePath(executablePath, &size))
352 {
353 char cleanedExecutablePath[PATH_MAX + 1];
354
355 realpath(executablePath, cleanedExecutablePath); // remove extra references (e.g., "/./")
356 string path = cleanedExecutablePath;
357 string dir, file, ext;
358 splitPath(path, dir, file, ext); // remove executable name
359 path = dir.substr(0, dir.length() - 1); // remove "/"
360 splitPath(path, dir, file, ext); // remove "MacOS"
361 path = dir.substr(0, dir.length() - 1); // remove "/"
362 path += "/Frameworks/VuoRunner.framework";
363
364 if (fileExists(path))
365 {
366 runnerFrameworkPath = path;
367 return;
368 }
369 }
370
371 // Give up.
372 });
373 return runnerFrameworkPath;
374}
375
384string VuoFileUtilities::getCompositionLocalModulesPath(const string &compositionPath)
385{
386 string compositionDir, compositionFile, ext;
387 splitPath(getAbsolutePath(compositionPath), compositionDir, compositionFile, ext);
388 canonicalizePath(compositionDir);
389
390 string parentDir, compositionDirName;
391 splitPath(compositionDir, parentDir, compositionDirName, ext);
392 canonicalizePath(compositionDirName);
393
394 string compositionBaseDir = (compositionDirName == "Modules" ? parentDir : compositionDir);
395
396 string compositionModulesDir = compositionBaseDir + "/Modules";
397 canonicalizePath(compositionModulesDir);
398
399 return compositionModulesDir;
400}
401
411{
412 const char *home = getenv("HOME");
413 if (!home)
414 return string();
415
416 return string(home) + "/Library/Application Support/Vuo/Modules";
417}
418
423{
424 return "/Library/Application Support/Vuo/Modules";
425}
426
436{
437 const char *home = getenv("HOME");
438 if (!home)
439 return string();
440
441 return string(home) + "/Library/Caches/org.vuo/" + VUO_VERSION_AND_BUILD_STRING;
442}
443
448{
449 if (path.empty())
450 return false;
451
452 string compositionLocalModulesPath = getCompositionLocalModulesPath(path);
453
454 string dir, file, ext;
455 VuoFileUtilities::splitPath(path, dir, file, ext);
456
457 return VuoFileUtilities::arePathsEqual(compositionLocalModulesPath, dir);
458}
459
468void VuoFileUtilities::preserveOriginalFileName(string &fileContents, string originalFileName)
469{
470 fileContents.insert(getFirstInsertionIndex(fileContents), "#line 1 \"" + originalFileName + "\"\n");
471}
472
478{
479 string bomUtf8 = "\xEF\xBB\xBF";
480 string bomUtf16Be = "\xFE\xFF";
481 string bomUtf16Le = "\xFF\xFE";
482 string boms[] = { bomUtf8, bomUtf16Be, bomUtf16Le };
483 for (int i = 0; i < 3; ++i)
484 {
485 bool isMatch = true;
486 for (int j = 0; j < boms[i].length(); ++j)
487 if (boms[i][j] != s[j])
488 isMatch = false;
489
490 if (isMatch)
491 return boms[i].length();
492 }
493 return 0;
494}
495
500{
501 // https://stackoverflow.com/questions/201992/how-to-read-until-eof-from-cin-in-c
502
503 string contents;
504 string line;
505 while (getline(cin, line))
506 contents += line;
507
508 return contents;
509}
510
517{
518 // https://stackoverflow.com/questions/2602013/read-whole-ascii-file-into-c-stdstring
519
520 ifstream in(path.c_str(), ios::in | ios::binary);
521 if (! in)
522 throw VuoException(string("Couldn't read file: ") + strerror(errno) + " — " + path);
523
524 string contents;
525 in.seekg(0, ios::end);
526 contents.resize(in.tellg());
527 in.seekg(0, ios::beg);
528 in.read(&contents[0], contents.size());
529 in.close();
530 return contents;
531}
532
540void VuoFileUtilities::writeRawDataToFile(const char *data, size_t numBytes, string file)
541{
542 FILE *f = fopen(file.c_str(), "wb");
543 if (! f)
544 throw VuoException(string("Couldn't open file for writing: ") + strerror(errno) + " — " + file);
545
546 size_t numBytesWritten = fwrite(data, sizeof(char), numBytes, f);
547 if (numBytesWritten != numBytes)
548 {
549 fclose(f);
550 throw VuoException(string("Couldn't write all data: ") + strerror(errno) + " — " + file);
551 }
552
553 fclose(f);
554}
555
563void VuoFileUtilities::writeStringToFile(string s, string file)
564{
565 writeRawDataToFile(s.data(), s.length(), file);
566}
567
577{
578 string dir, file, ext;
579 splitPath(path, dir, file, ext);
580 string tmpPath = makeTmpFile("." + file, ext, dir);
581
582 writeStringToFile(s, tmpPath);
583
584 moveFile(tmpPath, path);
585}
586
593{
596
597 // `access()` automatically dereferences symlinks.
598 return access(path.c_str(), 0) == 0;
599}
600
607{
610
611 // `stat()` automatically dereferences symlinks.
612 struct stat st_buf;
613 int status = stat(path.c_str(), &st_buf);
614 return (! status && S_ISDIR(st_buf.st_mode));
615}
616
621{
622 struct stat st_buf;
623 int status = lstat(path.c_str(), &st_buf);
624 return (! status && S_ISLNK(st_buf.st_mode));
625}
626
633{
636
637 // `access()` automatically dereferences symlinks.
638 return access(path.c_str(), R_OK) == 0;
639}
640
647{
650
651 // `fopen()` automatically dereferences symlinks.
652 FILE *f = fopen(path.c_str(), "rb");
653 if (!f)
654 return false;
655
656 fseek(f, 0, SEEK_END);
657 long pos = ftell(f);
658 if (pos <= 0)
659 {
660 fclose(f);
661 return false;
662 }
663
664 // `fopen` and `fseek` both succeed on directories, so also attempt to actually read the data.
665 fseek(f, 0, SEEK_SET);
666 char z;
667 size_t ret = fread(&z, 1, 1, f);
668 fclose(f);
669 return ret > 0;
670}
671
676{
677 FILE *f = fopen(path.c_str(), "a");
678 fclose(f);
679}
680
685{
686 int ret = remove(path.c_str());
687 if (ret != 0 && fileExists(path))
688 VUserLog("Couldn't delete file: %s — %s", strerror(errno), path.c_str());
689}
690
695{
696 if (! dirExists(path))
697 return;
698
699 DIR *d = opendir(path.c_str());
700 if (! d)
701 {
702 VUserLog("Couldn't read directory: %s — %s", strerror(errno), path.c_str());
703 return;
704 }
705
706 struct dirent *de;
707 while( (de=readdir(d)) )
708 {
709 string fileName = de->d_name;
710 string filePath = path + "/" + fileName;
711
712 if (fileName == "." || fileName == "..")
713 continue;
714
715 if (dirExists(filePath))
716 deleteDir(filePath);
717 else
718 deleteFile(filePath);
719 }
720
721 closedir(d);
722
723 deleteFile(path);
724}
725
731void VuoFileUtilities::moveFile(string fromPath, string toPath)
732{
733 int ret = rename(fromPath.c_str(), toPath.c_str());
734 if (ret != 0)
735 throw VuoException(string("Couldn't move file: ") + strerror(errno) + " — " + toPath);
736}
737
744{
746}
747
759void VuoFileUtilities::copyFile(string fromPath, string toPath, bool preserveMetadata)
760{
761 int i = open(fromPath.c_str(), O_RDONLY);
762 if (i == -1)
763 throw VuoException(string("Couldn't open copy source: ") + strerror(errno) + " — " + fromPath);
764
765 struct stat s;
766 fstat(i, &s);
767 int o = open(toPath.c_str(), O_WRONLY | O_CREAT, s.st_mode & 0777);
768 if (o == -1)
769 {
770 close(i);
771 throw VuoException(string("Couldn't open copy destination: ") + strerror(errno) + " — " + toPath);
772 }
773
774 int ret = fcopyfile(i, o, NULL, COPYFILE_DATA | (preserveMetadata ? COPYFILE_STAT : 0));
775 char *e = strerror(errno);
776 close(o);
777 close(i);
778
779 if (ret)
780 throw VuoException(string("Couldn't copy ") + fromPath + " to " + toPath + ": " + e);
781
782 VDebugLog("%s -> %s", fromPath.c_str(), toPath.c_str());
783}
784
794void VuoFileUtilities::copyDirectory(string fromPath, string toPath)
795{
796 if (isSymlink(fromPath))
797 {
798 // Preserve the relative symlinks found in framework bundles, instead of flattening them.
799 char linkDestination[PATH_MAX + 1];
800 ssize_t len = readlink(fromPath.c_str(), linkDestination, PATH_MAX);
801 linkDestination[len] = 0; // "readlink does not append a NUL character to buf."
802 if (symlink(linkDestination, toPath.c_str()) == -1)
803 throw VuoException(string("Couldn't copy symlink \"") + fromPath + "\" to \"" + toPath + "\": " + strerror(errno));
804
805 VDebugLog("%s -> %s (symlink)", fromPath.c_str(), toPath.c_str());
806 }
807
808 else if (!dirExists(fromPath))
809 copyFile(fromPath, toPath);
810
811 else
812 {
813 VDebugLog("%s ::", fromPath.c_str());
814 auto files = findAllFilesInDirectory(fromPath);
815 makeDir(toPath);
816 for (auto file : files)
817 {
818 string sourceFile = fromPath + "/" + file->getRelativePath();
819 string targetFile = toPath + "/" + file->getRelativePath();
820 copyDirectory(sourceFile, targetFile);
821 }
822 }
823}
824
831{
832 int fd = open(path.c_str(), O_RDONLY);
833 if (fd < 0)
834 throw VuoException("Error: Couldn't open \"" + path + "\": " + strerror(errno));
835 VuoDefer(^{ close(fd); });
836
837 struct stat stat;
838 if (fstat(fd, &stat) != 0)
839 throw VuoException("Error: Couldn't fstat \"" + path + "\": " + strerror(errno));
840
841 // Instead of reading the file into a string and calling `VuoStringUtilities::calculateSHA256`,
842 // use `mmap` so the OS can efficiently read parts of the file at a time (reducing memory required).
843 void *data = mmap(nullptr, stat.st_size, PROT_READ, MAP_PRIVATE | MAP_NOCACHE, fd, 0);
844 if (data == MAP_FAILED)
845 throw VuoException("Error: Couldn't mmap \"" + path + "\": " + strerror(errno));
846 VuoDefer(^{ munmap(data, stat.st_size); });
847
848 unsigned char hash[CC_SHA256_DIGEST_LENGTH];
849 if (!CC_SHA256(data, stat.st_size, hash))
850 throw VuoException("Error: CC_SHA256 failed on file \"" + path + "\"");
851
852 ostringstream oss;
853 oss << setfill('0') << hex;
854 for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; ++i)
855 oss << setw(2) << (int)hash[i];
856 return oss.str();
857}
858
864{
865 struct stat s;
866 lstat(path.c_str(), &s);
867 return s.st_mtimespec.tv_sec; // s.st_mtimespec.tv_nsec is always 0 on some OSes, hence the resolution of 1 second
868}
869
874{
875 struct stat s;
876 lstat(path.c_str(), &s);
877
878 struct timeval t;
879 gettimeofday(&t, NULL);
880
881 return t.tv_sec - s.st_atimespec.tv_sec;
882}
883
898set<VuoFileUtilities::File *> VuoFileUtilities::findAllFilesInDirectory(string dirPath, set<string> archiveExtensions,
899 bool shouldSearchRecursively)
900{
901 set<File *> files;
902
904 dirPath = VuoFileUtilitiesCocoa_resolveMacAlias(dirPath);
905
906 // `opendir()` automatically dereferences symlinks.
907 DIR *d = opendir(dirPath.c_str());
908 if (! d)
909 {
910 if (access(dirPath.c_str(), F_OK) != -1)
911 throw VuoException(string("Couldn't read directory: ") + strerror(errno) + " — " + dirPath);
912 return files;
913 }
914
915 struct dirent *de;
916 while( (de=readdir(d)) )
917 {
918 string fileName = de->d_name;
919 string relativeFilePath = dirPath + "/" + fileName;
920
921 if (fileName == "." || fileName == "..")
922 continue;
923
924 bool isArchive = false;
925 for (set<string>::iterator archiveExtension = archiveExtensions.begin(); archiveExtension != archiveExtensions.end(); ++archiveExtension)
926 if (VuoStringUtilities::endsWith(fileName, "." + *archiveExtension))
927 isArchive = true;
928
929 if (isArchive)
930 {
931 set<File *> fs = findAllFilesInArchive(relativeFilePath);
932 if (fs.empty())
933 isArchive = false;
934 else
935 files.insert(fs.begin(), fs.end());
936 }
937
938 if (! isArchive)
939 {
940 bool shouldSearchDir = shouldSearchRecursively
941 && dirExists(relativeFilePath)
942 && !isSymlink(relativeFilePath);
943 if (shouldSearchDir)
944 {
945 set<File *> filesInDir = findAllFilesInDirectory(relativeFilePath, archiveExtensions, true);
946 for (set<File *>::iterator i = filesInDir.begin(); i != filesInDir.end(); ++i)
947 {
948 File *f = *i;
949 f->dirPath = dirPath;
950 f->filePath = fileName + "/" + f->filePath;
951 }
952 files.insert(filesInDir.begin(), filesInDir.end());
953 }
954 else
955 {
956 File *f = new File(dirPath, fileName);
957 files.insert(f);
958 }
959 }
960 }
961
962 closedir(d);
963
964 return files;
965}
966
976set<VuoFileUtilities::File *> VuoFileUtilities::findFilesInDirectory(string dirPath, set<string> extensions, set<string> archiveExtensions)
977{
978 set<File *> allFiles = findAllFilesInDirectory(dirPath, archiveExtensions);
979 set<File *> matchingFiles;
980
981 for (set<File *>::iterator i = allFiles.begin(); i != allFiles.end(); ++i)
982 {
983 bool endsWithExtension = false;
984 for (set<string>::iterator extension = extensions.begin(); extension != extensions.end(); ++extension)
985 if (VuoStringUtilities::endsWith((*i)->getRelativePath(), "." + *extension))
986 endsWithExtension = true;
987
988 if (endsWithExtension && ((*i)->isInArchive() || !VuoFileUtilities::dirExists((*i)->path())))
989 matchingFiles.insert(*i);
990 else
991 delete *i;
992 }
993
994 return matchingFiles;
995}
996
1003set<VuoFileUtilities::File *> VuoFileUtilities::findAllFilesInArchive(string archivePath)
1004{
1005 set<File *> files;
1006
1007 Archive *archive = new Archive(archivePath);
1008 if (! archive->zipArchive)
1009 {
1010 delete archive;
1011 return files;
1012 }
1013
1014 mz_uint numFiles = mz_zip_reader_get_num_files(archive->zipArchive);
1015 for (mz_uint i = 0; i < numFiles; ++i)
1016 {
1017 mz_zip_archive_file_stat file_stat;
1018 bool success = mz_zip_reader_file_stat(archive->zipArchive, i, &file_stat);
1019 if (! success)
1020 {
1021 VUserLog("Error: Couldn't read file '%u' in zip archive '%s'.", i, archive->path.c_str());
1022 break;
1023 }
1024
1025 File *f = new File(archive, file_stat.m_filename);
1026 files.insert(f);
1027 }
1028
1029 if (archive->referenceCount == 0)
1030 delete archive;
1031
1032 return files;
1033}
1034
1044set<VuoFileUtilities::File *> VuoFileUtilities::findFilesInArchive(string archivePath, string dirPath, set<string> extensions)
1045{
1046 set<File *> allFiles = findAllFilesInArchive(archivePath);
1047 set<File *> matchingFiles;
1048
1049 for (set<File *>::iterator i = allFiles.begin(); i != allFiles.end(); ++i)
1050 {
1051 File *f = *i;
1052
1053 string dir, file, ext;
1054 splitPath(f->getRelativePath(), dir, file, ext);
1055 bool beginsWithDir = VuoStringUtilities::beginsWith(dir, dirPath);
1056
1057 bool endsWithExtension = false;
1058 for (set<string>::iterator extension = extensions.begin(); extension != extensions.end(); ++extension)
1059 if (VuoStringUtilities::endsWith(f->getRelativePath(), "." + *extension))
1060 endsWithExtension = true;
1061
1062 if (beginsWithDir && endsWithExtension)
1063 matchingFiles.insert(f);
1064 else
1065 delete f;
1066 }
1067
1068 return matchingFiles;
1069}
1070
1080string VuoFileUtilities::getArchiveFileContentsAsString(string archivePath, string filePath)
1081{
1082 string contents;
1083 set<File *> archiveFiles = findAllFilesInArchive(archivePath);
1084 for (set<File *>::iterator i = archiveFiles.begin(); i != archiveFiles.end(); ++i)
1085 {
1086 File *file = *i;
1087 if (filePath == file->getRelativePath())
1088 contents = file->getContentsAsString();
1089 delete file;
1090 }
1091
1092 return contents;
1093}
1094
1103{
1105}
1106
1110void VuoFileUtilities::adHocCodeSign(string path, vector<string> environment, string entitlementsPath)
1111{
1112 double t0 = VuoLogGetTime();
1113 vector<string> processAndArgs{
1114 getVuoFrameworkPath() + "/Helpers/codesignWrapper.sh",
1115 "--sign",
1116 "-", // "-" = ad-hoc
1117 path,
1118 };
1119 if (!entitlementsPath.empty())
1120 {
1121 processAndArgs.push_back("--entitlements");
1122 processAndArgs.push_back(entitlementsPath);
1123 }
1124 executeProcess(processAndArgs, environment);
1125
1126 VDebugLog("\tAd-hoc code-signing took %5.2fs", VuoLogGetTime() - t0);
1127}
1128
1135 path(path)
1136{
1137 this->referenceCount = 0;
1138
1139 // mz_zip_reader_init_file sometimes ends up with a garbage function pointer if the mz_zip_archive isn't initialized to zeroes
1140 this->zipArchive = (mz_zip_archive *)calloc(1, sizeof(mz_zip_archive));
1141
1142 bool success = mz_zip_reader_init_file(zipArchive, path.c_str(), 0);
1143 if (! success)
1144 {
1145 free(zipArchive);
1146 zipArchive = NULL;
1147 }
1148}
1149
1154{
1155 if (zipArchive)
1156 {
1157 mz_zip_reader_end(zipArchive);
1158 free(zipArchive);
1159 }
1160}
1161
1168{
1169}
1170
1174VuoFileUtilities::File::File(string dirPath, string filePath) :
1175 filePath(filePath),
1176 dirPath(dirPath)
1177{
1178 this->fileDescriptor = -1;
1179 this->archive = NULL;
1180}
1181
1188VuoFileUtilities::File::File(Archive *archive, string filePath) :
1189 filePath(filePath)
1190{
1191 this->fileDescriptor = -1;
1192 this->archive = archive;
1193 ++archive->referenceCount;
1194}
1195
1201{
1202 string newFilePath = basename() + "." + extension;
1203
1204 if (archive)
1205 return File(archive, newFilePath);
1206 else
1207 return File(dirPath, newFilePath);
1208}
1209
1215{
1216 if (archive && --archive->referenceCount == 0)
1217 delete archive;
1218}
1219
1224{
1225 return (archive != NULL);
1226}
1227
1234{
1235 return (archive ? archive->path : "");
1236}
1237
1242{
1243 return filePath;
1244}
1245
1252{
1253 if (archive)
1254 throw VuoException("Can't return a simple absolute path for a file in an archive.");
1255
1256 return dirPath + "/" + filePath;
1257}
1258
1265{
1266 if (archive)
1267 throw VuoException("Can't return a simple absolute path for a file in an archive.");
1268
1269 return dirPath;
1270}
1271
1276{
1277 return filePath.substr(0, filePath.find_last_of('.'));
1278}
1279
1284{
1285 return filePath.substr(filePath.find_last_of('.') + 1);
1286}
1287
1292{
1293 if (archive)
1294 return mz_zip_reader_locate_file(archive->zipArchive, filePath.c_str(), nullptr, MZ_ZIP_FLAG_CASE_SENSITIVE) != -1;
1295 else
1296 return VuoFileUtilities::fileExists((dirPath.empty() ? "" : (dirPath + "/")) + filePath);
1297}
1298
1308{
1309 char * buffer;
1310 numBytes = 0;
1311
1312 if (! archive)
1313 {
1314 // http://www.cplusplus.com/reference/cstdio/fread/
1315
1316 string fullPath = (dirPath.empty() ? "" : (dirPath + "/")) + filePath;
1317
1318 FILE *pFile = fopen ( fullPath.c_str() , "rb" );
1319 if (pFile==NULL)
1320 throw VuoException(string("Couldn't open file: ") + strerror(errno) + " — " + fullPath);
1321
1322 // obtain file size:
1323 fseek (pFile , 0 , SEEK_END);
1324 size_t lSize = ftell (pFile);
1325 rewind (pFile);
1326
1327 // allocate memory to contain the whole file:
1328 buffer = (char*) malloc (sizeof(char)*lSize);
1329
1330 // copy the file into the buffer:
1331 numBytes = fread (buffer,1,lSize,pFile);
1332 fclose(pFile);
1333 if (numBytes != lSize)
1334 {
1335 free(buffer);
1336 throw VuoException(string("Couldn't read file: ") + strerror(errno) + " — " + fullPath);
1337 }
1338 }
1339 else
1340 {
1341 buffer = (char *)mz_zip_reader_extract_file_to_heap(archive->zipArchive, filePath.c_str(), &numBytes, 0);
1342 }
1343
1344 return buffer;
1345}
1346
1353{
1354 size_t numBytes;
1355 char *buffer = getContentsAsRawData(numBytes);
1356 string s(buffer, numBytes);
1357 free(buffer);
1358 return s;
1359}
1360
1371{
1372 if (fileDescriptor < 0)
1373 fileDescriptor = open((dirPath + "/" + filePath).c_str(), O_RDONLY);
1374 int ret = flock(fileDescriptor, LOCK_SH | (nonBlocking ? LOCK_NB : 0));
1375 return ret == 0;
1376}
1377
1388{
1389 if (fileDescriptor < 0)
1390 fileDescriptor = open((dirPath + "/" + filePath).c_str(), O_RDONLY);
1391 int ret = flock(fileDescriptor, LOCK_EX | (nonBlocking ? LOCK_NB : 0));
1392 return ret == 0;
1393}
1394
1400{
1401 flock(fileDescriptor, LOCK_UN);
1402 close(fileDescriptor);
1403 fileDescriptor = -1;
1404}
1405
1411void VuoFileUtilities::focusProcess(pid_t pid, bool force)
1412{
1414}
1415
1424void VuoFileUtilities::executeProcess(vector<string> processAndArgs, vector<string> environment)
1425{
1426 string binaryDir, binaryFile, binaryExt;
1427 splitPath(processAndArgs[0], binaryDir, binaryFile, binaryExt);
1428
1429 string errorPrefix = "Couldn't execute " + binaryFile + ": ";
1430
1431 // Capture stdout and stderr.
1432 int outputPipe[2];
1433 int ret = pipe(outputPipe);
1434 if (ret)
1435 throw VuoException(errorPrefix + "couldn't open pipe: " + strerror(ret));
1436 int pipeRead = outputPipe[0];
1437 int pipeWrite = outputPipe[1];
1438 VuoDefer(^{ close(pipeRead); });
1439 posix_spawn_file_actions_t fileActions;
1440 posix_spawn_file_actions_init(&fileActions);
1441 posix_spawn_file_actions_adddup2(&fileActions, pipeWrite, STDOUT_FILENO);
1442 posix_spawn_file_actions_adddup2(&fileActions, pipeWrite, STDERR_FILENO);
1443 posix_spawn_file_actions_addclose(&fileActions, pipeRead);
1444
1445 // Convert args.
1446 // posix_spawn requires a null-terminated array, which `&processAndArgs[0]` doesn't guarantee.
1447 char **processAndArgsZ = (char **)malloc(sizeof(char *) * (processAndArgs.size() + 1));
1448 int argCount = 0;
1449 string commandLine;
1450 for (auto arg : processAndArgs)
1451 {
1452 processAndArgsZ[argCount++] = strdup(arg.c_str());
1453 if (argCount > 0)
1454 commandLine += ' ';
1455 commandLine += '"' + arg + '"';
1456 }
1457 processAndArgsZ[argCount] = nullptr;
1458
1459
1460 // Convert environment.
1461 // posix_spawn requires a null-terminated array, which `&processAndArgs[0]` doesn't guarantee.
1462 char **environmentZ = (char **)malloc(sizeof(char *) * (environment.size() + 1));
1463 int environmentVarCount = 0;
1464 for (auto environmentVar : environment)
1465 {
1466 environmentZ[environmentVarCount++] = strdup(environmentVar.c_str());
1467 commandLine = environmentVar + " " + commandLine;
1468 }
1469 environmentZ[environmentVarCount] = nullptr;
1470
1471
1472 // Execute.
1473 pid_t pid;
1474 VDebugLog("%s", commandLine.c_str());
1475 ret = posix_spawn(&pid, processAndArgs[0].c_str(), &fileActions, NULL, (char * const *)processAndArgsZ, (char * const *)environmentZ);
1476 close(pipeWrite);
1477 posix_spawn_file_actions_destroy(&fileActions);
1478 for (int i = 0; i < argCount; ++i)
1479 free(processAndArgsZ[i]);
1480
1481 if (ret)
1482 throw VuoException(errorPrefix + strerror(ret));
1483 else
1484 {
1485 int ret;
1486 int status;
1487 while (true)
1488 {
1489 ret = waitpid(pid, &status, 0);
1490 if (ret == -1 && errno == EINTR)
1491 // This process received a signal while waiting for the other process
1492 // (seems to happen when running under lldb); try waiting again.
1493 continue;
1494 else if (ret == -1)
1495 throw VuoException(errorPrefix + "waitpid failed: " + strerror(errno));
1496 else if (status != 0)
1497 {
1498 string output;
1499 char buf[256];
1500 bzero(buf, 256);
1501 while (read(pipeRead, &buf, 255) > 0)
1502 {
1503 output += buf;
1504 bzero(buf, 256);
1505 }
1506
1507 throw VuoException(binaryFile + " failed: " + output);
1508 }
1509 else
1510 // The other process completed without error.
1511 break;
1512 }
1513 }
1514}
1515
1520{
1521 return extension == "vuo";
1522}
1523
1528{
1529 set<string> e;
1530 e.insert("c");
1531 e.insert("cc");
1532 e.insert("C");
1533 e.insert("cpp");
1534 e.insert("cxx");
1535 e.insert("c++");
1536 e.insert("m");
1537 e.insert("mm");
1538 return e;
1539}
1540
1545{
1546 auto extensions = getCSourceExtensions();
1547 return extensions.find(extension) != extensions.end();
1548}
1549
1554{
1555 set<string> e;
1556 e.insert("fs");
1557 e.insert("vs");
1558 e.insert("vuoshader");
1559 e.insert("vuoobjectfilter");
1560 return e.find(extension) != e.end();
1561}
1562
1566string VuoFileUtilities::buildModuleCacheDescription(const string &moduleCachePath, bool generated)
1567{
1568 return string() + "the cache of " + (generated ? "generated" : "installed") + " modules at '" + moduleCachePath + "'";
1569}
1570
1574string VuoFileUtilities::buildModuleCacheIndexPath(const string &moduleCachePath, bool builtIn, bool generated)
1575{
1576 string moduleCachePathCanonical = moduleCachePath;
1577 canonicalizePath(moduleCachePathCanonical);
1578
1579 return moduleCachePathCanonical + "/libVuoModuleCache-" + (generated ? "generated" : "installed") + ".txt";
1580}
1581
1587string VuoFileUtilities::buildModuleCacheDylibPath(const string &moduleCachePath, bool builtIn, bool generated)
1588{
1589 string moduleCachePathCanonical = moduleCachePath;
1590 canonicalizePath(moduleCachePathCanonical);
1591
1592 string partialPath = moduleCachePathCanonical + "/libVuoModuleCache-" + (generated ? "generated" : "installed");
1593 if (builtIn)
1594 return partialPath + ".dylib";
1595
1596 string uniquePath;
1597 do {
1598 uniquePath = partialPath + "-" + VuoStringUtilities::makeRandomHash(4) + ".dylib";
1599 } while (fileExists(uniquePath));
1600 return uniquePath;
1601}
1602
1606string VuoFileUtilities::findLatestRevisionOfModuleCacheDylib(const string &moduleCachePath, bool builtIn, bool generated,
1607 unsigned long &lastModified)
1608{
1609 string path = buildModuleCacheDylibPath(moduleCachePath, builtIn, generated);
1610
1611 if (builtIn)
1612 {
1613 if (fileExists(path))
1614 {
1615 lastModified = getFileLastModifiedInSeconds(path);
1616 return path;
1617 }
1618 else
1619 {
1620 lastModified = 0;
1621 return "";
1622 }
1623 }
1624
1625 string dir, file, ext;
1626 splitPath(path, dir, file, ext);
1627 string fileName = file + "." + ext;
1628
1629 vector< pair<string, unsigned long> > existingRevisions;
1630 for (File *existingFile : findFilesInDirectory(moduleCachePath, {"dylib"}))
1631 {
1632 if (areDifferentRevisionsOfSameModuleCacheDylib(existingFile->getRelativePath(), fileName))
1633 existingRevisions.push_back({existingFile->path(), getFileLastModifiedInSeconds(existingFile->path())});
1634
1635 delete existingFile;
1636 }
1637
1638 if (existingRevisions.empty())
1639 {
1640 lastModified = 0;
1641 return "";
1642 }
1643
1644 auto latest = std::max_element(existingRevisions.begin(), existingRevisions.end(),
1645 [](pair<string, unsigned long> p1, pair<string, unsigned long> p2) { return p1.second < p2.second; });
1646
1647 lastModified = latest->second;
1648 return latest->first;
1649}
1650
1655{
1656 string moduleCachePath, file, ext;
1657 splitPath(dylibPath, moduleCachePath, file, ext);
1658 string dylibFileName = file + "." + ext;
1659
1660 for (File *otherFile : findFilesInDirectory(moduleCachePath, {"dylib"}))
1661 {
1662 if (areDifferentRevisionsOfSameModuleCacheDylib(otherFile->getRelativePath(), dylibFileName))
1663 deleteFile(otherFile->path());
1664
1665 delete otherFile;
1666 }
1667}
1668
1672bool VuoFileUtilities::areDifferentRevisionsOfSameModuleCacheDylib(const string &dylibPath1, const string &dylibPath2)
1673{
1674 string dir1, dir2, file1, file2, ext;
1675 splitPath(dylibPath1, dir1, file1, ext);
1676 splitPath(dylibPath2, dir2, file2, ext);
1677
1678 if (arePathsEqual(dir1, dir2))
1679 {
1680 vector<string> parts1 = VuoStringUtilities::split(file1, '-');
1681 vector<string> parts2 = VuoStringUtilities::split(file2, '-');
1682
1683 if (parts1.size() == 3 && parts2.size() == 3 && parts1.back() != parts2.back())
1684 {
1685 parts1.pop_back();
1686 parts2.pop_back();
1687 return parts1 == parts2;
1688 }
1689 }
1690
1691 return false;
1692}