Vuo  2.3.2
VuoUrl.c
Go to the documentation of this file.
1 
10 #include "type.h"
11 #include "VuoUrl.h"
12 #include "VuoOsStatus.h"
13 
14 #include <regex.h>
15 #include <mach-o/dyld.h> // for _NSGetExecutablePath()
16 #include <libgen.h> // for dirname()
17 #include <CoreServices/CoreServices.h>
18 #include "VuoUrlParser.h"
19 
20 
22 #ifdef VUO_COMPILER
24  "title" : "URL",
25  "description" : "Uniform Resource Locator.",
26  "keywords" : [ "link" ],
27  "version" : "1.0.0",
28  "dependencies" : [
29  "CoreServices.framework",
30  "VuoInteger",
31  "VuoOsStatus",
32  "VuoText",
33  "VuoUrlParser"
34  ]
35  });
36 #endif
38 
43 {
44  const char *textString = "";
45  if (json_object_get_type(js) == json_type_string)
46  textString = json_object_get_string(js);
47 
48  VuoUrl url;
49  if (textString)
50  url = strdup(textString);
51  else
52  url = strdup("");
53  VuoRegister(url, free);
54 
55  return url;
56 }
57 
62 {
63  if (!value)
64  return json_object_new_string("");
65 
66  return json_object_new_string(value);
67 }
68 
72 char *VuoUrl_getSummary(const VuoUrl value)
73 {
74  VuoText t = VuoText_truncateWithEllipsis(value, 1024, VuoTextTruncation_End);
75  VuoRetain(t);
76  char *summary = strdup(t);
77  VuoRelease(t);
78  return summary;
79 }
80 
86 bool VuoUrl_getParts(const VuoUrl url, VuoText *scheme, VuoText *user, VuoText *host, VuoInteger *port, VuoText *path, VuoText *query, VuoText *fragment)
87 {
88  if (VuoText_isEmpty(url))
89  return false;
90 
91  const char *localURL = url;
92  char *urlWithEmptyAuthority = NULL;
93  struct http_parser_url parsedUrl;
94  size_t urlLen = strlen(url);
95  if (http_parser_parse_url(url, urlLen, false, &parsedUrl))
96  {
97  // Maybe this is a "data:" URI (which http_parser_parse_url can't parse).
98  if (strncmp(url, "data:", 5) == 0)
99  {
100  if (scheme)
101  *scheme = VuoText_make("data");
102  if (user)
103  *user = NULL;
104  if (host)
105  *host = NULL;
106  if (port)
107  *port = 0;
108  if (path)
109  *path = NULL;
110  if (query)
111  *query = NULL;
112  if (fragment)
113  *fragment = NULL;
114  return true;
115  }
116 
117  // Maybe this is a "no-authority" `file:` URI (which http_parser_parse_url can't parse);
118  // change it to an "empty-authority" URI.
119  else if (strncmp(url, "file:/", 6) == 0 && urlLen > 6 && url[6] != '/')
120  {
121  urlWithEmptyAuthority = malloc(urlLen + 2 + 1);
122  strcpy(urlWithEmptyAuthority, "file://");
123  strcat(urlWithEmptyAuthority, url + 5);
124  if (http_parser_parse_url(urlWithEmptyAuthority, urlLen + 2, false, &parsedUrl))
125  {
126  free(urlWithEmptyAuthority);
127  return false;
128  }
129  localURL = urlWithEmptyAuthority;
130  }
131 
132  else
133  return false;
134  }
135 
136  if (scheme)
137  {
138  if (parsedUrl.field_set & (1 << UF_SCHEMA))
139  *scheme = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_SCHEMA ].off, parsedUrl.field_data[UF_SCHEMA ].len);
140  else
141  *scheme = NULL;
142  }
143 
144  if (user)
145  {
146  if (parsedUrl.field_set & (1 << UF_USERINFO))
147  *user = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_USERINFO].off, parsedUrl.field_data[UF_USERINFO].len);
148  else
149  *user = NULL;
150  }
151 
152  if (host)
153  {
154  if (parsedUrl.field_set & (1 << UF_HOST))
155  *host = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_HOST ].off, parsedUrl.field_data[UF_HOST ].len);
156  else
157  *host = NULL;
158  }
159 
160  if (port)
161  {
162  if (parsedUrl.field_set & (1 << UF_PORT))
163  // Explicitly-specified port
164  *port = parsedUrl.port;
165  else
166  {
167  // Guess the port from the scheme
168  *port = 0;
169  if (strcmp(*scheme, "http") == 0)
170  *port = 80;
171  else if (strcmp(*scheme, "https") == 0)
172  *port = 443;
173  }
174  }
175 
176  if (path)
177  {
178  if (parsedUrl.field_set & (1 << UF_PATH))
179  *path = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_PATH ].off, parsedUrl.field_data[UF_PATH ].len);
180  else
181  *path = NULL;
182  }
183 
184  if (query)
185  {
186  if (parsedUrl.field_set & (1 << UF_QUERY))
187  *query = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_QUERY ].off, parsedUrl.field_data[UF_QUERY ].len);
188  else
189  *query = NULL;
190  }
191 
192  if (fragment)
193  {
194  if (parsedUrl.field_set & (1 << UF_FRAGMENT))
195  *fragment = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_FRAGMENT].off, parsedUrl.field_data[UF_FRAGMENT].len);
196  else
197  *fragment = NULL;
198  }
199 
200  if (urlWithEmptyAuthority)
201  free(urlWithEmptyAuthority);
202 
203  return true;
204 }
205 
213 bool VuoUrl_getFileParts(const VuoUrl url, VuoText *path, VuoText *folder, VuoText *filename, VuoText *extension)
214 {
215  if (VuoText_isEmpty(url))
216  return false;
217 
218  *path = VuoUrl_getPosixPath(url);
219  if (!*path)
220  return false;
221 
222  size_t separatorIndex = VuoText_findLastOccurrence(*path, "/");
223  VuoText fileAndExtension;
224  if (separatorIndex)
225  {
226  *folder = VuoText_substring(*path, 1, separatorIndex);
227  size_t length = VuoText_length(*path);
228  if (separatorIndex < length)
229  fileAndExtension = VuoText_substring(*path, separatorIndex + 1, length);
230  else
231  fileAndExtension = NULL;
232  }
233  else
234  {
235  *folder = NULL;
236  fileAndExtension = VuoText_make(*path);
237  }
238  VuoRetain(fileAndExtension);
239 
240  size_t dotIndex = VuoText_findLastOccurrence(fileAndExtension, ".");
241  if (dotIndex)
242  {
243  *filename = VuoText_substring(fileAndExtension, 1, dotIndex - 1);
244  *extension = VuoText_substring(fileAndExtension, dotIndex + 1, VuoText_length(fileAndExtension));
245  }
246  else
247  {
248  *filename = VuoText_make(fileAndExtension);
249  *extension = NULL;
250  }
251 
252  VuoRelease(fileAndExtension);
253 
254  return true;
255 }
256 
260 bool VuoUrl_areEqual(const VuoText a, const VuoText b)
261 {
262  return VuoText_areEqual(a,b);
263 }
264 
268 bool VuoUrl_isLessThan(const VuoText a, const VuoText b)
269 {
270  return VuoText_isLessThan(a,b);
271 }
272 
276 static bool VuoUrl_urlContainsScheme(const char *url)
277 {
278  const char *urlWithSchemePattern = "^[a-zA-Z][a-zA-Z0-9+-\\.]+:";
279  regex_t urlWithSchemeRegExp;
280  size_t nmatch = 0;
281  regmatch_t pmatch[0];
282 
283  regcomp(&urlWithSchemeRegExp, urlWithSchemePattern, REG_EXTENDED);
284  bool matchFound = !regexec(&urlWithSchemeRegExp, url, nmatch, pmatch, 0);
285  regfree(&urlWithSchemeRegExp);
286 
287  return matchFound;
288 }
289 
293 static bool VuoUrl_urlIsAbsoluteFilePath(const char *url)
294 {
295  return ((strlen(url) >= 1) && (url[0] == '/'));
296 }
297 
301 static bool VuoUrl_urlIsUserRelativeFilePath(const char *url)
302 {
303  return ((strlen(url) >= 1) && (url[0] == '~'));
304 }
305 
310 {
311  return !((VuoText_length(url)==0) ||
315 }
316 
320 static const char VuoUrl_reservedCharacters[] =
321 {
322  0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
323  0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
324  '"', '$', '&', '+', ',', ':', ';', '=', '?', '@', '#', ' ',
325  // Percent must be last, so we don't escape the escapes.
326  '%'
327 };
328 
333 {
334  // Figure out how many characters we need to allocate for the escaped string.
335  unsigned long inLength = strlen(path);
336  unsigned long escapedLength = 0;
337  for (unsigned long i = 0; i < inLength; ++i)
338  {
339  char c = path[i];
340  for (int j = 0; j < sizeof(VuoUrl_reservedCharacters); ++j)
341  if (c == VuoUrl_reservedCharacters[j])
342  {
343  escapedLength += 2; // Expanding 1 character to "%xx"
344  break;
345  }
346  ++escapedLength;
347  }
348 
349  // Escape the string.
350  char *escapedUrl = (char *)malloc(escapedLength + 1);
351  unsigned long outIndex = 0;
352  const char *hexCharSet = "0123456789ABCDEF"; // Uppercase per https://tools.ietf.org/html/rfc3987#section-3.1
353  for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex)
354  {
355  unsigned char c = path[inIndex];
356  bool foundEscape = false;
357  for (int j = 0; j < sizeof(VuoUrl_reservedCharacters); ++j)
358  if (c == VuoUrl_reservedCharacters[j])
359  {
360  escapedUrl[outIndex++] = '%';
361  escapedUrl[outIndex++] = hexCharSet[c >> 4];
362  escapedUrl[outIndex++] = hexCharSet[c & 0x0f];
363  foundEscape = true;
364  break;
365  }
366 
367  // https://b33p.net/kosada/node/12361
368  // If it's a UTF-8 "Modifier Letter Colon", translate it back into an escaped ASCII-7 colon
369  // (since URLs can handle colons, unlike POSIX paths on macOS).
370  if (inIndex+2 < inLength)
371  if (c == 0xea && (unsigned char)path[inIndex+1] == 0x9e && (unsigned char)path[inIndex+2] == 0x89)
372  {
373  escapedUrl[outIndex++] = '%';
374  escapedUrl[outIndex++] = hexCharSet[':' >> 4];
375  escapedUrl[outIndex++] = hexCharSet[':' & 0x0f];
376  foundEscape = true;
377  inIndex += 2;
378  }
379 
380  if (!foundEscape)
381  escapedUrl[outIndex++] = c;
382  }
383  escapedUrl[outIndex] = 0;
384 
385  VuoText escapedUrlVT = VuoText_make(escapedUrl);
386  free(escapedUrl);
387 
388  return escapedUrlVT;
389 }
390 
395 {
396  // Figure out how many characters we need to allocate for the escaped string.
397  unsigned long inLength = strlen(url);
398  unsigned long escapedLength = 0;
399  for (unsigned long i = 0; i < inLength; ++i)
400  {
401  if ((unsigned char)url[i] > 0x7f)
402  escapedLength += 2; // Expanding 1 character to "%xx"
403  ++escapedLength;
404  }
405 
406  // Escape the string.
407  char *escapedUrl = (char *)malloc(escapedLength + 1);
408  unsigned long outIndex = 0;
409  const char *hexCharSet = "0123456789ABCDEF"; // Uppercase per https://tools.ietf.org/html/rfc3987#section-3.1
410  for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex)
411  {
412  unsigned char c = url[inIndex];
413  if (c > 0x7f)
414  {
415  escapedUrl[outIndex++] = '%';
416  escapedUrl[outIndex++] = hexCharSet[c >> 4];
417  escapedUrl[outIndex++] = hexCharSet[c & 0x0f];
418  }
419  else
420  escapedUrl[outIndex++] = c;
421  }
422  escapedUrl[outIndex] = 0;
423 
424  VuoText escapedUrlVT = VuoText_make(escapedUrl);
425  free(escapedUrl);
426 
427  return escapedUrlVT;
428 }
429 
430 static const char *VuoUrl_fileScheme = "file:";
431 static const char *VuoUrl_httpScheme = "http://";
432 
453 {
454  if (!url)
455  return NULL;
456 
457  // Trim off the file schema, if present, and handle such URLs via the absolute/user-relative/relative file path cases below.
458  VuoText trimmedUrl;
459  unsigned long fileSchemeLength = strlen(VuoUrl_fileScheme);
460  if (strncmp(url, VuoUrl_fileScheme, fileSchemeLength) == 0)
461  {
462  VuoText u = url + fileSchemeLength;
463  if (strncmp(u, "//", 2) == 0)
464  u += 2;
465 
466  // Assume a URL with a schema is already escaped; unescape it for consistent processing below.
467  trimmedUrl = VuoUrl_decodeRFC3986(u);
468  }
469  else
470  trimmedUrl = url;
471  VuoRetain(trimmedUrl);
472 
473  char *resolvedUrl;
474 
475  // Case: The url contains a scheme.
476  if (VuoUrl_urlContainsScheme(trimmedUrl))
477  {
478  // Some URLs have literal spaces, which we need to transform into '%20' before passing to cURL.
479  size_t urlLen = strlen(trimmedUrl);
480  size_t spaceCount = 0;
481  for (size_t i = 0; i < urlLen; ++i)
482  if (trimmedUrl[i] == ' ')
483  ++spaceCount;
484  if (spaceCount)
485  {
486  resolvedUrl = (char *)malloc(strlen(trimmedUrl) + spaceCount*2);
487  size_t p = 0;
488  for (size_t i = 0; i < urlLen; ++i)
489  if (trimmedUrl[i] == ' ')
490  {
491  resolvedUrl[p++] = '%';
492  resolvedUrl[p++] = '2';
493  resolvedUrl[p++] = '0';
494  }
495  else
496  resolvedUrl[p++] = trimmedUrl[i];
497  resolvedUrl[p] = 0;
498  }
499  else
500  resolvedUrl = strdup(trimmedUrl);
501  }
502 
503  // Case: The url contains an absolute file path.
504  else if (VuoUrl_urlIsAbsoluteFilePath(trimmedUrl))
505  {
506  char *filePath = (char *)trimmedUrl;
507 
508  if (!(flags & VuoUrlNormalize_forSaving) && strncmp(filePath, "/tmp/", 5) == 0)
509  {
510  // We're trying to read a file from `/tmp`.
511  // If it doesn't exist there, also check the user's private temporary directory.
512  // Enables protocol driver compositions to find their default images.
513  if (access(filePath, 0) != 0)
514  {
515  char userTempDir[PATH_MAX];
516  size_t userTempDirLen;
517  if ((userTempDirLen = confstr(_CS_DARWIN_USER_TEMP_DIR, userTempDir, PATH_MAX)) > 0)
518  {
519  size_t filePathLen = strlen(filePath);
520  size_t mallocSize = userTempDirLen + filePathLen + 1;
521  char *privateFilePath = (char *)malloc(mallocSize);
522  strlcpy(privateFilePath, userTempDir, mallocSize);
523  strlcat(privateFilePath, filePath + 4, mallocSize);
524  filePath = privateFilePath;
525  }
526  }
527  }
528 
529  if ((flags & VuoUrlNormalize_forLaunching) && strncmp(filePath, "/Applications/", 14) == 0)
530  {
531  // If the app doesn't exist at `/Applications`, check `/System/Applications`.
532  if (access(filePath, 0) != 0)
533  {
534  const char *systemPrefix = "/System";
535  size_t systemPrefixLen = strlen(systemPrefix);
536  size_t filePathLen = strlen(filePath);
537  size_t mallocSize = systemPrefixLen + filePathLen + 1;
538  char *systemPath = (char *)malloc(mallocSize);
539  strlcpy(systemPath, systemPrefix, mallocSize);
540  strlcat(systemPath, filePath, mallocSize);
541  if (access(systemPath, 0) == 0)
542  filePath = systemPath;
543  else
544  free(systemPath);
545  }
546  }
547 
548  char *realPath = realpath(filePath, NULL);
549  // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
550  VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : filePath);
551  VuoRetain(escapedPath);
552  if (realPath)
553  free(realPath);
554  if (filePath != trimmedUrl)
555  free(filePath);
556 
557  size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
558  resolvedUrl = (char *)malloc(mallocSize);
559  strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
560  strlcat(resolvedUrl, escapedPath, mallocSize);
561 
562  VuoRelease(escapedPath);
563  }
564 
565  // Case: The url contains a user-relative (`~/…`) file path.
566  else if (VuoUrl_urlIsUserRelativeFilePath(trimmedUrl))
567  {
568  // 1. Expand the tilde into an absolute path.
569  VuoText absolutePath;
570  {
571  char *homeDir = getenv("HOME");
572  VuoText paths[2] = { homeDir, trimmedUrl+1 };
573  absolutePath = VuoText_append(paths, 2);
574  }
575  VuoLocal(absolutePath);
576 
577  // 2. Try to canonicalize the absolute path, and URL-escape it.
578  char *realPath = realpath(absolutePath, NULL);
579  // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
580  VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : absolutePath);
581  VuoLocal(escapedPath);
582  if (realPath)
583  free(realPath);
584 
585  // 3. Prepend the URL scheme.
586  size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
587  resolvedUrl = (char *)malloc(mallocSize);
588  strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
589  strlcat(resolvedUrl, escapedPath, mallocSize);
590  }
591 
592  // Case: The url contains a web link without a protocol/scheme.
593  else if (flags & VuoUrlNormalize_assumeHttp)
594  {
595  // Prepend the URL scheme.
596  size_t mallocSize = strlen(VuoUrl_httpScheme) + strlen(trimmedUrl) + 1;
597  resolvedUrl = (char *)malloc(mallocSize);
598  strlcpy(resolvedUrl, VuoUrl_httpScheme, mallocSize);
599  strlcat(resolvedUrl, trimmedUrl, mallocSize);
600  }
601 
602  // Case: The url contains a relative file path.
603  else
604  {
605  const char *currentWorkingDir = VuoGetWorkingDirectory();
606 
607  // - When macOS LaunchServices launches an app, it sets the current working directory to root.
608  // - When macOS ScreenSaverEngine launches a plugin, it sets the current working directory to
609  // `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data`.
610  bool compositionIsExportedAppOrPlugin = false;
611  char *absolutePath;
612  if (strcmp(currentWorkingDir, "/") == 0
613  || strstr(currentWorkingDir, "/Library/Containers/"))
614  {
615  // If we're running as an exported app or plugin,
616  // resolve loaded resources relative to the app bundle's "Resources" directory, and
617  // resolve saved resources relative to the user's Desktop.
618 
619  // Get the exported executable path.
620  char rawExecutablePath[PATH_MAX+1];
621  uint32_t size = sizeof(rawExecutablePath);
622  _NSGetExecutablePath(rawExecutablePath, &size);
623 
624  char cleanedExecutablePath[PATH_MAX+1];
625  realpath(rawExecutablePath, cleanedExecutablePath);
626 
627  // Derive the path of the app bundle's "Resources" directory from its executable path.
628  size_t pathSize = PATH_MAX + 1;
629  char executableDir[pathSize];
630  strlcpy(executableDir, dirname(cleanedExecutablePath), pathSize);
631 
632  const char *resourcesPathFromExecutable = "/../Resources";
633  pathSize = strlen(executableDir) + strlen(resourcesPathFromExecutable) + 1;
634  char rawResourcesPath[pathSize];
635  strlcpy(rawResourcesPath, executableDir, pathSize);
636  strlcat(rawResourcesPath, resourcesPathFromExecutable, pathSize);
637 
638  char cleanedResourcesPath[PATH_MAX+1];
639  realpath(rawResourcesPath, cleanedResourcesPath);
640 
641  // If the "Resources" directory does not exist, we must not be dealing with an exported app after all.
642  // If it does, proceed under the assumption that we are.
643  if (access(cleanedResourcesPath, 0) == 0)
644  {
645  compositionIsExportedAppOrPlugin = true;
646 
647  if (flags & VuoUrlNormalize_forSaving)
648  {
649  char *homeDir = getenv("HOME");
650  const char *desktop = "/Desktop/";
651  size_t mallocSize = strlen(homeDir) + strlen(desktop) + strlen(trimmedUrl) + 1;
652  absolutePath = (char *)malloc(mallocSize);
653  strlcpy(absolutePath, homeDir, mallocSize);
654  strlcat(absolutePath, desktop, mallocSize);
655  strlcat(absolutePath, trimmedUrl, mallocSize);
656  }
657  else
658  {
659  size_t mallocSize = strlen(cleanedResourcesPath) + strlen("/") + strlen(trimmedUrl) + 1;
660  absolutePath = (char *)malloc(mallocSize);
661  strlcpy(absolutePath, cleanedResourcesPath, mallocSize);
662  strlcat(absolutePath, "/", mallocSize);
663  strlcat(absolutePath, trimmedUrl, mallocSize);
664  }
665  }
666  }
667 
668  // If we are not working with an exported app, resolve resources relative to the current working directory.
669  if (!compositionIsExportedAppOrPlugin)
670  {
671  size_t mallocSize = strlen(currentWorkingDir) + strlen("/") + strlen(trimmedUrl) + 1;
672  absolutePath = (char *)malloc(mallocSize);
673  strlcpy(absolutePath, currentWorkingDir, mallocSize);
674  strlcat(absolutePath, "/", mallocSize);
675  strlcat(absolutePath, trimmedUrl, mallocSize);
676  }
677 
678  // If we're looking for an app, and it isn't found in the exported app or current working directory,
679  // try the standard applications folders.
680  if ((flags & VuoUrlNormalize_forLaunching) && access(absolutePath, 0) != 0)
681  {
682  // Check `~/Applications`.
683  {
684  char *homeDir = getenv("HOME");
685  const char *applicationsPrefix = "/Applications/";
686  size_t homeDirLen = strlen(homeDir);
687  size_t applicationsPrefixLen = strlen(applicationsPrefix);
688  size_t trimmedUrlLen = strlen(trimmedUrl);
689  size_t mallocSize = homeDirLen + applicationsPrefixLen + trimmedUrlLen + 1;
690  char *path = (char *)malloc(mallocSize);
691  strlcpy(path, homeDir, mallocSize);
692  strlcat(path, applicationsPrefix, mallocSize);
693  strlcat(path, trimmedUrl, mallocSize);
694  if (access(path, 0) == 0)
695  {
696  free(absolutePath);
697  absolutePath = path;
698  }
699  else
700  free(path);
701  }
702 
703  // Check `/Applications`.
704  {
705  const char *applicationsPrefix = "/Applications/";
706  size_t applicationsPrefixLen = strlen(applicationsPrefix);
707  size_t trimmedUrlLen = strlen(trimmedUrl);
708  size_t mallocSize = applicationsPrefixLen + trimmedUrlLen + 1;
709  char *path = (char *)malloc(mallocSize);
710  strlcpy(path, applicationsPrefix, mallocSize);
711  strlcat(path, trimmedUrl, mallocSize);
712  if (access(path, 0) == 0)
713  {
714  free(absolutePath);
715  absolutePath = path;
716  }
717  else
718  free(path);
719  }
720 
721  // Check `/System/Applications`.
722  {
723  const char *applicationsPrefix = "/System/Applications/";
724  size_t applicationsPrefixLen = strlen(applicationsPrefix);
725  size_t trimmedUrlLen = strlen(trimmedUrl);
726  size_t mallocSize = applicationsPrefixLen + trimmedUrlLen + 1;
727  char *path = (char *)malloc(mallocSize);
728  strlcpy(path, applicationsPrefix, mallocSize);
729  strlcat(path, trimmedUrl, mallocSize);
730  if (access(path, 0) == 0)
731  {
732  free(absolutePath);
733  absolutePath = path;
734  }
735  else
736  free(path);
737  }
738  }
739 
740 
741  char *realPath = realpath(absolutePath, NULL);
742  // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
743  VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : absolutePath);
744  VuoLocal(escapedPath);
745  if (realPath)
746  free(realPath);
747  free(absolutePath);
748 
749  // Prepend the URL scheme.
750  size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
751  resolvedUrl = (char *)malloc(mallocSize);
752  strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
753  strlcat(resolvedUrl, escapedPath, mallocSize);
754  }
755 
756  VuoRelease(trimmedUrl);
757 
758  // Remove trailing slash, if any.
759  size_t lastIndex = strlen(resolvedUrl) - 1;
760  if (resolvedUrl[lastIndex] == '/')
761  resolvedUrl[lastIndex] = 0;
762 
763  VuoText resolvedUrlVT = VuoText_make(resolvedUrl);
764  VuoRetain(resolvedUrlVT);
765  free(resolvedUrl);
766 
767  VuoText escapedUrl = VuoUrl_escapeUTF8(resolvedUrlVT);
768  VuoRelease(resolvedUrlVT);
769  return escapedUrl;
770 }
771 
778 {
779  unsigned long inLength = strlen(url);
780  char *unescapedUrl = (char *)malloc(inLength + 1);
781  unsigned long outIndex = 0;
782  for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex, ++outIndex)
783  {
784  char c = url[inIndex];
785  if (c == '%')
786  {
787  if (inIndex + 2 >= inLength)
788  break;
789  char highNibbleASCII = url[++inIndex];
790  char lowNibbleASCII = url[++inIndex];
791  unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
792  }
793  else
794  unescapedUrl[outIndex] = c;
795  }
796  unescapedUrl[outIndex] = 0;
797 
798  VuoText unescapedUrlVT = VuoText_make(unescapedUrl);
799  free(unescapedUrl);
800  return unescapedUrlVT;
801 }
802 
809 {
810  if (!url)
811  return NULL;
812 
813  unsigned long fileSchemeLength = strlen(VuoUrl_fileScheme);
814  if (strncmp(url, VuoUrl_fileScheme, fileSchemeLength) != 0)
815  return NULL;
816 
817  // https://tools.ietf.org/html/rfc8089#appendix-B
818  if (strncmp(url + fileSchemeLength, "//", 2) == 0)
819  fileSchemeLength += 2;
820 
821  // Unescape the string.
822  unsigned long inLength = strlen(url);
823  // Make room for Unicode colons.
824  char *unescapedUrl = (char *)malloc(inLength*3 + 1);
825  unsigned long outIndex = 0;
826  for (unsigned long inIndex = fileSchemeLength; inIndex < inLength; ++inIndex, ++outIndex)
827  {
828  char c = url[inIndex];
829  if (c == '%')
830  {
831  char highNibbleASCII = url[++inIndex];
832  char lowNibbleASCII = url[++inIndex];
833  unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
834  }
835  else
836  unescapedUrl[outIndex] = c;
837 
838  // https://b33p.net/kosada/node/12361
839  // macOS presents colons as forward-slashes (https://developer.apple.com/library/mac/qa/qa1392/).
840  // To avoid confusion with dates, change ASCII-7 colon to UTF-8 "Modifier Letter Colon"
841  // (which looks visually identical to ASCII-7 colon).
842  if (unescapedUrl[outIndex] == ':')
843  {
844  unescapedUrl[outIndex++] = 0xea;
845  unescapedUrl[outIndex++] = 0x9e;
846  unescapedUrl[outIndex] = 0x89;
847  }
848  }
849  unescapedUrl[outIndex] = 0;
850 
851  VuoText unescapedUrlVT = VuoText_make(unescapedUrl);
852  free(unescapedUrl);
853 
854  return unescapedUrlVT;
855 }
856 
866 bool VuoUrl_isBundle(const VuoUrl url)
867 {
868  if (VuoText_isEmpty(url))
869  return false;
870 
871  {
872  VuoText path = VuoUrl_getPosixPath(url);
873  if (!path)
874  return false;
875  VuoRetain(path);
876  VuoRelease(path);
877  }
878 
879  CFStringRef urlCFS = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
880  CFURLRef cfurl = CFURLCreateWithString(NULL, urlCFS, NULL);
881  CFRelease(urlCFS);
882  if (!cfurl)
883  {
884  VUserLog("Error: Couldn't check '%s': Invalid URL.", url);
885  return false;
886  }
887 
888  CFTypeRef info = NULL;
889  CFErrorRef error = NULL;
890  bool ret = CFURLCopyResourcePropertyForKey(cfurl, kCFURLIsPackageKey, &info, &error);
891  CFRelease(cfurl);
892  if (!ret)
893  {
894  CFStringRef errorCFS = CFErrorCopyDescription(error);
895  CFRelease(error);
896  VuoText errorText = VuoText_makeFromCFString(errorCFS);
897  CFRelease(errorCFS);
898  VuoRetain(errorText);
899  VUserLog("Error: Couldn't check '%s': %s", url, errorText);
900  VuoRelease(errorText);
901  return false;
902  }
903  return info == kCFBooleanTrue;
904 }
905 
909 VuoUrl VuoUrl_appendFileExtension(const char *filename, struct json_object *validExtensions)
910 {
911  char* fileSuffix = strrchr(filename, '.');
912  char* curExtension = fileSuffix != NULL ? strdup(fileSuffix+1) : NULL;
913 
914  if(curExtension != NULL)
915  for(char *p = &curExtension[0]; *p; p++) *p = tolower(*p);
916 
917  // if the string already has one of the valid file extension suffixes, return.
918  for (int i = 0; i < json_object_array_length(validExtensions); ++i)
919  {
920  if (curExtension != NULL && strcmp(curExtension, json_object_get_string(json_object_array_get_idx(validExtensions, i))) == 0)
921  {
922  free(curExtension);
923  return VuoText_make(filename);
924  }
925  }
926 
927  free(curExtension);
928 
929  const char *chosenExtension = json_object_get_string(json_object_array_get_idx(validExtensions, 0));
930  size_t buf_size = strlen(filename) + strlen(chosenExtension) + 2;
931  char* newfilepath = (char*)malloc(buf_size * sizeof(char));
932  snprintf(newfilepath, buf_size, "%s.%s", filename, chosenExtension);
933 
934  VuoText text = VuoText_make(newfilepath);
935  free(newfilepath);
936 
937  return text;
938 }