Vuo  2.0.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  bool compositionIsExportedApp = false;
584 
585  // If the current working directory is "/", assume that we are working with an exported app;
586  // resolve loaded resources relative to the app bundle's "Resources" directory, and
587  // resolve saved resources relative to the user's Desktop.
588  char *absolutePath;
589  if (!strcmp(currentWorkingDir, "/"))
590  {
591  // Get the exported executable path.
592  char rawExecutablePath[PATH_MAX+1];
593  uint32_t size = sizeof(rawExecutablePath);
594  _NSGetExecutablePath(rawExecutablePath, &size);
595 
596  char cleanedExecutablePath[PATH_MAX+1];
597  realpath(rawExecutablePath, cleanedExecutablePath);
598 
599  // Derive the path of the app bundle's "Resources" directory from its executable path.
600  size_t pathSize = PATH_MAX + 1;
601  char executableDir[pathSize];
602  strlcpy(executableDir, dirname(cleanedExecutablePath), pathSize);
603 
604  const char *resourcesPathFromExecutable = "/../Resources";
605  pathSize = strlen(executableDir) + strlen(resourcesPathFromExecutable) + 1;
606  char rawResourcesPath[pathSize];
607  strlcpy(rawResourcesPath, executableDir, pathSize);
608  strlcat(rawResourcesPath, resourcesPathFromExecutable, pathSize);
609 
610  char cleanedResourcesPath[PATH_MAX+1];
611  realpath(rawResourcesPath, cleanedResourcesPath);
612 
613  // If the "Resources" directory does not exist, we must not be dealing with an exported app after all.
614  // If it does, proceed under the assumption that we are.
615  if (access(cleanedResourcesPath, 0) == 0)
616  {
617  compositionIsExportedApp = true;
618 
619  if (flags & VuoUrlNormalize_forSaving)
620  {
621  char *homeDir = getenv("HOME");
622  const char *desktop = "/Desktop/";
623  size_t mallocSize = strlen(homeDir) + strlen(desktop) + strlen(trimmedUrl) + 1;
624  absolutePath = (char *)malloc(mallocSize);
625  strlcpy(absolutePath, homeDir, mallocSize);
626  strlcat(absolutePath, desktop, mallocSize);
627  strlcat(absolutePath, trimmedUrl, mallocSize);
628  }
629  else
630  {
631  size_t mallocSize = strlen(cleanedResourcesPath) + strlen("/") + strlen(trimmedUrl) + 1;
632  absolutePath = (char *)malloc(mallocSize);
633  strlcpy(absolutePath, cleanedResourcesPath, mallocSize);
634  strlcat(absolutePath, "/", mallocSize);
635  strlcat(absolutePath, trimmedUrl, mallocSize);
636  }
637  }
638  }
639 
640  // If we are not working with an exported app, resolve resources relative to the current working directory.
641  if (!compositionIsExportedApp)
642  {
643  size_t mallocSize = strlen(currentWorkingDir) + strlen("/") + strlen(trimmedUrl) + 1;
644  absolutePath = (char *)malloc(mallocSize);
645  strlcpy(absolutePath, currentWorkingDir, mallocSize);
646  strlcat(absolutePath, "/", mallocSize);
647  strlcat(absolutePath, trimmedUrl, mallocSize);
648  }
649 
650  char *realPath = realpath(absolutePath, NULL);
651  // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
652  VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : absolutePath);
653  VuoLocal(escapedPath);
654  if (realPath)
655  free(realPath);
656  free(absolutePath);
657 
658  // Prepend the URL scheme.
659  size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
660  resolvedUrl = (char *)malloc(mallocSize);
661  strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
662  strlcat(resolvedUrl, escapedPath, mallocSize);
663  }
664 
665  VuoRelease(trimmedUrl);
666 
667  // Remove trailing slash, if any.
668  size_t lastIndex = strlen(resolvedUrl) - 1;
669  if (resolvedUrl[lastIndex] == '/')
670  resolvedUrl[lastIndex] = 0;
671 
672  VuoText resolvedUrlVT = VuoText_make(resolvedUrl);
673  VuoRetain(resolvedUrlVT);
674  free(resolvedUrl);
675 
676  VuoText escapedUrl = VuoUrl_escapeUTF8(resolvedUrlVT);
677  VuoRelease(resolvedUrlVT);
678  return escapedUrl;
679 }
680 
687 {
688  unsigned long inLength = strlen(url);
689  char *unescapedUrl = (char *)malloc(inLength + 1);
690  unsigned long outIndex = 0;
691  for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex, ++outIndex)
692  {
693  char c = url[inIndex];
694  if (c == '%')
695  {
696  if (inIndex + 2 >= inLength)
697  break;
698  char highNibbleASCII = url[++inIndex];
699  char lowNibbleASCII = url[++inIndex];
700  unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
701  }
702  else
703  unescapedUrl[outIndex] = c;
704  }
705  unescapedUrl[outIndex] = 0;
706 
707  VuoText unescapedUrlVT = VuoText_make(unescapedUrl);
708  free(unescapedUrl);
709  return unescapedUrlVT;
710 }
711 
718 {
719  if (!url)
720  return NULL;
721 
722  unsigned long fileSchemeLength = strlen(VuoUrl_fileScheme);
723  if (strncmp(url, VuoUrl_fileScheme, fileSchemeLength) != 0)
724  return NULL;
725 
726  // https://tools.ietf.org/html/rfc8089#appendix-B
727  if (strncmp(url + fileSchemeLength, "//", 2) == 0)
728  fileSchemeLength += 2;
729 
730  // Unescape the string.
731  unsigned long inLength = strlen(url);
732  // Make room for Unicode colons.
733  char *unescapedUrl = (char *)malloc(inLength*3 + 1);
734  unsigned long outIndex = 0;
735  for (unsigned long inIndex = fileSchemeLength; inIndex < inLength; ++inIndex, ++outIndex)
736  {
737  char c = url[inIndex];
738  if (c == '%')
739  {
740  char highNibbleASCII = url[++inIndex];
741  char lowNibbleASCII = url[++inIndex];
742  unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
743  }
744  else
745  unescapedUrl[outIndex] = c;
746 
747  // https://b33p.net/kosada/node/12361
748  // macOS presents colons as forward-slashes (https://developer.apple.com/library/mac/qa/qa1392/).
749  // To avoid confusion with dates, change ASCII-7 colon to UTF-8 "Modifier Letter Colon"
750  // (which looks visually identical to ASCII-7 colon).
751  if (unescapedUrl[outIndex] == ':')
752  {
753  unescapedUrl[outIndex++] = 0xea;
754  unescapedUrl[outIndex++] = 0x9e;
755  unescapedUrl[outIndex] = 0x89;
756  }
757  }
758  unescapedUrl[outIndex] = 0;
759 
760  VuoText unescapedUrlVT = VuoText_make(unescapedUrl);
761  free(unescapedUrl);
762 
763  return unescapedUrlVT;
764 }
765 
775 bool VuoUrl_isBundle(const VuoUrl url)
776 {
777  if (VuoText_isEmpty(url))
778  return false;
779 
780  {
781  VuoText path = VuoUrl_getPosixPath(url);
782  if (!path)
783  return false;
784  VuoRetain(path);
785  VuoRelease(path);
786  }
787 
788  CFStringRef urlCFS = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
789  CFURLRef cfurl = CFURLCreateWithString(NULL, urlCFS, NULL);
790  CFRelease(urlCFS);
791  if (!cfurl)
792  {
793  VUserLog("Error: Couldn't check '%s': Invalid URL.", url);
794  return false;
795  }
796 
797  LSItemInfoRecord outItemInfo;
798  OSStatus ret = LSCopyItemInfoForURL(cfurl, kLSRequestAllFlags, &outItemInfo);
799  CFRelease(cfurl);
800  if (ret)
801  {
802  char *errorString = VuoOsStatus_getText(ret);
803  VUserLog("Error: Couldn't check '%s': %s", url, errorString);
804  free(errorString);
805  return false;
806  }
807  return outItemInfo.flags & kLSItemInfoIsPackage;
808 }
809 
813 VuoUrl VuoUrl_appendFileExtension(const char *filename, struct json_object *validExtensions)
814 {
815  char* fileSuffix = strrchr(filename, '.');
816  char* curExtension = fileSuffix != NULL ? strdup(fileSuffix+1) : NULL;
817 
818  if(curExtension != NULL)
819  for(char *p = &curExtension[0]; *p; p++) *p = tolower(*p);
820 
821  // if the string already has one of the valid file extension suffixes, return.
822  for (int i = 0; i < json_object_array_length(validExtensions); ++i)
823  {
824  if (curExtension != NULL && strcmp(curExtension, json_object_get_string(json_object_array_get_idx(validExtensions, i))) == 0)
825  {
826  free(curExtension);
827  return VuoText_make(filename);
828  }
829  }
830 
831  free(curExtension);
832 
833  const char *chosenExtension = json_object_get_string(json_object_array_get_idx(validExtensions, 0));
834  size_t buf_size = strlen(filename) + strlen(chosenExtension) + 2;
835  char* newfilepath = (char*)malloc(buf_size * sizeof(char));
836  snprintf(newfilepath, buf_size, "%s.%s", filename, chosenExtension);
837 
838  VuoText text = VuoText_make(newfilepath);
839  free(newfilepath);
840 
841  return text;
842 }
843