Vuo  2.1.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
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  char *realPath = realpath(filePath, NULL);
525  // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
526  VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : filePath);
527  VuoRetain(escapedPath);
528  if (realPath)
529  free(realPath);
530  if (filePath != trimmedUrl)
531  free(filePath);
532 
533  size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
534  resolvedUrl = (char *)malloc(mallocSize);
535  strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
536  strlcat(resolvedUrl, escapedPath, mallocSize);
537 
538  VuoRelease(escapedPath);
539  }
540 
541  // Case: The url contains a user-relative (`~/…`) file path.
542  else if (VuoUrl_urlIsUserRelativeFilePath(trimmedUrl))
543  {
544  // 1. Expand the tilde into an absolute path.
545  VuoText absolutePath;
546  {
547  char *homeDir = getenv("HOME");
548  VuoText paths[2] = { homeDir, trimmedUrl+1 };
549  absolutePath = VuoText_append(paths, 2);
550  }
551  VuoLocal(absolutePath);
552 
553  // 2. Try to canonicalize the absolute path, and URL-escape it.
554  char *realPath = realpath(absolutePath, NULL);
555  // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
556  VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : absolutePath);
557  VuoLocal(escapedPath);
558  if (realPath)
559  free(realPath);
560 
561  // 3. Prepend the URL scheme.
562  size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
563  resolvedUrl = (char *)malloc(mallocSize);
564  strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
565  strlcat(resolvedUrl, escapedPath, mallocSize);
566  }
567 
568  // Case: The url contains a web link without a protocol/scheme.
569  else if (flags & VuoUrlNormalize_assumeHttp)
570  {
571  // Prepend the URL scheme.
572  size_t mallocSize = strlen(VuoUrl_httpScheme) + strlen(trimmedUrl) + 1;
573  resolvedUrl = (char *)malloc(mallocSize);
574  strlcpy(resolvedUrl, VuoUrl_httpScheme, mallocSize);
575  strlcat(resolvedUrl, trimmedUrl, mallocSize);
576  }
577 
578  // Case: The url contains a relative file path.
579  else
580  {
581  const char *currentWorkingDir = VuoGetWorkingDirectory();
582 
583  // - When macOS LaunchServices launches an app, it sets the current working directory to root.
584  // - When macOS ScreenSaverEngine launches a plugin, it sets the current working directory to
585  // `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data`.
586  bool compositionIsExportedAppOrPlugin = false;
587  char *absolutePath;
588  if (strcmp(currentWorkingDir, "/") == 0
589  || strstr(currentWorkingDir, "/Library/Containers/"))
590  {
591  // If we're running as an exported app or plugin,
592  // resolve loaded resources relative to the app bundle's "Resources" directory, and
593  // resolve saved resources relative to the user's Desktop.
594 
595  // Get the exported executable path.
596  char rawExecutablePath[PATH_MAX+1];
597  uint32_t size = sizeof(rawExecutablePath);
598  _NSGetExecutablePath(rawExecutablePath, &size);
599 
600  char cleanedExecutablePath[PATH_MAX+1];
601  realpath(rawExecutablePath, cleanedExecutablePath);
602 
603  // Derive the path of the app bundle's "Resources" directory from its executable path.
604  size_t pathSize = PATH_MAX + 1;
605  char executableDir[pathSize];
606  strlcpy(executableDir, dirname(cleanedExecutablePath), pathSize);
607 
608  const char *resourcesPathFromExecutable = "/../Resources";
609  pathSize = strlen(executableDir) + strlen(resourcesPathFromExecutable) + 1;
610  char rawResourcesPath[pathSize];
611  strlcpy(rawResourcesPath, executableDir, pathSize);
612  strlcat(rawResourcesPath, resourcesPathFromExecutable, pathSize);
613 
614  char cleanedResourcesPath[PATH_MAX+1];
615  realpath(rawResourcesPath, cleanedResourcesPath);
616 
617  // If the "Resources" directory does not exist, we must not be dealing with an exported app after all.
618  // If it does, proceed under the assumption that we are.
619  if (access(cleanedResourcesPath, 0) == 0)
620  {
621  compositionIsExportedAppOrPlugin = true;
622 
623  if (flags & VuoUrlNormalize_forSaving)
624  {
625  char *homeDir = getenv("HOME");
626  const char *desktop = "/Desktop/";
627  size_t mallocSize = strlen(homeDir) + strlen(desktop) + strlen(trimmedUrl) + 1;
628  absolutePath = (char *)malloc(mallocSize);
629  strlcpy(absolutePath, homeDir, mallocSize);
630  strlcat(absolutePath, desktop, mallocSize);
631  strlcat(absolutePath, trimmedUrl, mallocSize);
632  }
633  else
634  {
635  size_t mallocSize = strlen(cleanedResourcesPath) + strlen("/") + strlen(trimmedUrl) + 1;
636  absolutePath = (char *)malloc(mallocSize);
637  strlcpy(absolutePath, cleanedResourcesPath, mallocSize);
638  strlcat(absolutePath, "/", mallocSize);
639  strlcat(absolutePath, trimmedUrl, mallocSize);
640  }
641  }
642  }
643 
644  // If we are not working with an exported app, resolve resources relative to the current working directory.
645  if (!compositionIsExportedAppOrPlugin)
646  {
647  size_t mallocSize = strlen(currentWorkingDir) + strlen("/") + strlen(trimmedUrl) + 1;
648  absolutePath = (char *)malloc(mallocSize);
649  strlcpy(absolutePath, currentWorkingDir, mallocSize);
650  strlcat(absolutePath, "/", mallocSize);
651  strlcat(absolutePath, trimmedUrl, mallocSize);
652  }
653 
654  char *realPath = realpath(absolutePath, NULL);
655  // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
656  VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : absolutePath);
657  VuoLocal(escapedPath);
658  if (realPath)
659  free(realPath);
660  free(absolutePath);
661 
662  // Prepend the URL scheme.
663  size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
664  resolvedUrl = (char *)malloc(mallocSize);
665  strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
666  strlcat(resolvedUrl, escapedPath, mallocSize);
667  }
668 
669  VuoRelease(trimmedUrl);
670 
671  // Remove trailing slash, if any.
672  size_t lastIndex = strlen(resolvedUrl) - 1;
673  if (resolvedUrl[lastIndex] == '/')
674  resolvedUrl[lastIndex] = 0;
675 
676  VuoText resolvedUrlVT = VuoText_make(resolvedUrl);
677  VuoRetain(resolvedUrlVT);
678  free(resolvedUrl);
679 
680  VuoText escapedUrl = VuoUrl_escapeUTF8(resolvedUrlVT);
681  VuoRelease(resolvedUrlVT);
682  return escapedUrl;
683 }
684 
691 {
692  unsigned long inLength = strlen(url);
693  char *unescapedUrl = (char *)malloc(inLength + 1);
694  unsigned long outIndex = 0;
695  for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex, ++outIndex)
696  {
697  char c = url[inIndex];
698  if (c == '%')
699  {
700  if (inIndex + 2 >= inLength)
701  break;
702  char highNibbleASCII = url[++inIndex];
703  char lowNibbleASCII = url[++inIndex];
704  unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
705  }
706  else
707  unescapedUrl[outIndex] = c;
708  }
709  unescapedUrl[outIndex] = 0;
710 
711  VuoText unescapedUrlVT = VuoText_make(unescapedUrl);
712  free(unescapedUrl);
713  return unescapedUrlVT;
714 }
715 
722 {
723  if (!url)
724  return NULL;
725 
726  unsigned long fileSchemeLength = strlen(VuoUrl_fileScheme);
727  if (strncmp(url, VuoUrl_fileScheme, fileSchemeLength) != 0)
728  return NULL;
729 
730  // https://tools.ietf.org/html/rfc8089#appendix-B
731  if (strncmp(url + fileSchemeLength, "//", 2) == 0)
732  fileSchemeLength += 2;
733 
734  // Unescape the string.
735  unsigned long inLength = strlen(url);
736  // Make room for Unicode colons.
737  char *unescapedUrl = (char *)malloc(inLength*3 + 1);
738  unsigned long outIndex = 0;
739  for (unsigned long inIndex = fileSchemeLength; inIndex < inLength; ++inIndex, ++outIndex)
740  {
741  char c = url[inIndex];
742  if (c == '%')
743  {
744  char highNibbleASCII = url[++inIndex];
745  char lowNibbleASCII = url[++inIndex];
746  unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
747  }
748  else
749  unescapedUrl[outIndex] = c;
750 
751  // https://b33p.net/kosada/node/12361
752  // macOS presents colons as forward-slashes (https://developer.apple.com/library/mac/qa/qa1392/).
753  // To avoid confusion with dates, change ASCII-7 colon to UTF-8 "Modifier Letter Colon"
754  // (which looks visually identical to ASCII-7 colon).
755  if (unescapedUrl[outIndex] == ':')
756  {
757  unescapedUrl[outIndex++] = 0xea;
758  unescapedUrl[outIndex++] = 0x9e;
759  unescapedUrl[outIndex] = 0x89;
760  }
761  }
762  unescapedUrl[outIndex] = 0;
763 
764  VuoText unescapedUrlVT = VuoText_make(unescapedUrl);
765  free(unescapedUrl);
766 
767  return unescapedUrlVT;
768 }
769 
779 bool VuoUrl_isBundle(const VuoUrl url)
780 {
781  if (VuoText_isEmpty(url))
782  return false;
783 
784  {
785  VuoText path = VuoUrl_getPosixPath(url);
786  if (!path)
787  return false;
788  VuoRetain(path);
789  VuoRelease(path);
790  }
791 
792  CFStringRef urlCFS = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
793  CFURLRef cfurl = CFURLCreateWithString(NULL, urlCFS, NULL);
794  CFRelease(urlCFS);
795  if (!cfurl)
796  {
797  VUserLog("Error: Couldn't check '%s': Invalid URL.", url);
798  return false;
799  }
800 
801  LSItemInfoRecord outItemInfo;
802  OSStatus ret = LSCopyItemInfoForURL(cfurl, kLSRequestAllFlags, &outItemInfo);
803  CFRelease(cfurl);
804  if (ret)
805  {
806  char *errorString = VuoOsStatus_getText(ret);
807  VUserLog("Error: Couldn't check '%s': %s", url, errorString);
808  free(errorString);
809  return false;
810  }
811  return outItemInfo.flags & kLSItemInfoIsPackage;
812 }
813 
817 VuoUrl VuoUrl_appendFileExtension(const char *filename, struct json_object *validExtensions)
818 {
819  char* fileSuffix = strrchr(filename, '.');
820  char* curExtension = fileSuffix != NULL ? strdup(fileSuffix+1) : NULL;
821 
822  if(curExtension != NULL)
823  for(char *p = &curExtension[0]; *p; p++) *p = tolower(*p);
824 
825  // if the string already has one of the valid file extension suffixes, return.
826  for (int i = 0; i < json_object_array_length(validExtensions); ++i)
827  {
828  if (curExtension != NULL && strcmp(curExtension, json_object_get_string(json_object_array_get_idx(validExtensions, i))) == 0)
829  {
830  free(curExtension);
831  return VuoText_make(filename);
832  }
833  }
834 
835  free(curExtension);
836 
837  const char *chosenExtension = json_object_get_string(json_object_array_get_idx(validExtensions, 0));
838  size_t buf_size = strlen(filename) + strlen(chosenExtension) + 2;
839  char* newfilepath = (char*)malloc(buf_size * sizeof(char));
840  snprintf(newfilepath, buf_size, "%s.%s", filename, chosenExtension);
841 
842  VuoText text = VuoText_make(newfilepath);
843  free(newfilepath);
844 
845  return text;
846 }
847