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