Vuo  2.0.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  if (http_parser_parse_url(url, strlen(url), false, &parsedUrl))
93  {
94  // Maybe this is a "data:" URI (which http_parser_parse_url can't parse).
95  if (strncmp(url, "data:", 5) == 0)
96  {
97  if (scheme)
98  *scheme = VuoText_make("data");
99  if (user)
100  *user = NULL;
101  if (host)
102  *host = NULL;
103  if (port)
104  *port = 0;
105  if (path)
106  *path = NULL;
107  if (query)
108  *query = NULL;
109  if (fragment)
110  *fragment = NULL;
111  return true;
112  }
113 
114  return false;
115  }
116 
117  if (scheme)
118  {
119  if (parsedUrl.field_set & (1 << UF_SCHEMA))
120  *scheme = VuoText_makeWithMaxLength(url + parsedUrl.field_data[UF_SCHEMA ].off, parsedUrl.field_data[UF_SCHEMA ].len);
121  else
122  *scheme = NULL;
123  }
124 
125  if (user)
126  {
127  if (parsedUrl.field_set & (1 << UF_USERINFO))
128  *user = VuoText_makeWithMaxLength(url + parsedUrl.field_data[UF_USERINFO].off, parsedUrl.field_data[UF_USERINFO].len);
129  else
130  *user = NULL;
131  }
132 
133  if (host)
134  {
135  if (parsedUrl.field_set & (1 << UF_HOST))
136  *host = VuoText_makeWithMaxLength(url + parsedUrl.field_data[UF_HOST ].off, parsedUrl.field_data[UF_HOST ].len);
137  else
138  *host = NULL;
139  }
140 
141  if (port)
142  {
143  if (parsedUrl.field_set & (1 << UF_PORT))
144  // Explicitly-specified port
145  *port = parsedUrl.port;
146  else
147  {
148  // Guess the port from the scheme
149  *port = 0;
150  if (strcmp(*scheme, "http") == 0)
151  *port = 80;
152  else if (strcmp(*scheme, "https") == 0)
153  *port = 443;
154  }
155  }
156 
157  if (path)
158  {
159  if (parsedUrl.field_set & (1 << UF_PATH))
160  *path = VuoText_makeWithMaxLength(url + parsedUrl.field_data[UF_PATH ].off, parsedUrl.field_data[UF_PATH ].len);
161  else
162  *path = NULL;
163  }
164 
165  if (query)
166  {
167  if (parsedUrl.field_set & (1 << UF_QUERY))
168  *query = VuoText_makeWithMaxLength(url + parsedUrl.field_data[UF_QUERY ].off, parsedUrl.field_data[UF_QUERY ].len);
169  else
170  *query = NULL;
171  }
172 
173  if (fragment)
174  {
175  if (parsedUrl.field_set & (1 << UF_FRAGMENT))
176  *fragment = VuoText_makeWithMaxLength(url + parsedUrl.field_data[UF_FRAGMENT].off, parsedUrl.field_data[UF_FRAGMENT].len);
177  else
178  *fragment = NULL;
179  }
180 
181  return true;
182 }
183 
191 bool VuoUrl_getFileParts(const VuoUrl url, VuoText *path, VuoText *folder, VuoText *filename, VuoText *extension)
192 {
193  if (VuoText_isEmpty(url))
194  return false;
195 
196  *path = VuoUrl_getPosixPath(url);
197  if (!*path)
198  return false;
199 
200  size_t separatorIndex = VuoText_findLastOccurrence(*path, "/");
201  VuoText fileAndExtension;
202  if (separatorIndex)
203  {
204  *folder = VuoText_substring(*path, 1, separatorIndex);
205  size_t length = VuoText_length(*path);
206  if (separatorIndex < length)
207  fileAndExtension = VuoText_substring(*path, separatorIndex + 1, length);
208  else
209  fileAndExtension = NULL;
210  }
211  else
212  {
213  *folder = NULL;
214  fileAndExtension = VuoText_make(*path);
215  }
216  VuoRetain(fileAndExtension);
217 
218  size_t dotIndex = VuoText_findLastOccurrence(fileAndExtension, ".");
219  if (dotIndex)
220  {
221  *filename = VuoText_substring(fileAndExtension, 1, dotIndex - 1);
222  *extension = VuoText_substring(fileAndExtension, dotIndex + 1, VuoText_length(fileAndExtension));
223  }
224  else
225  {
226  *filename = VuoText_make(fileAndExtension);
227  *extension = NULL;
228  }
229 
230  VuoRelease(fileAndExtension);
231 
232  return true;
233 }
234 
238 bool VuoUrl_areEqual(const VuoText a, const VuoText b)
239 {
240  return VuoText_areEqual(a,b);
241 }
242 
246 bool VuoUrl_isLessThan(const VuoText a, const VuoText b)
247 {
248  return VuoText_isLessThan(a,b);
249 }
250 
254 static bool VuoUrl_urlContainsScheme(const char *url)
255 {
256  const char *urlWithSchemePattern = "^[a-zA-Z][a-zA-Z0-9+-\\.]+:";
257  regex_t urlWithSchemeRegExp;
258  size_t nmatch = 0;
259  regmatch_t pmatch[0];
260 
261  regcomp(&urlWithSchemeRegExp, urlWithSchemePattern, REG_EXTENDED);
262  bool matchFound = !regexec(&urlWithSchemeRegExp, url, nmatch, pmatch, 0);
263  regfree(&urlWithSchemeRegExp);
264 
265  return matchFound;
266 }
267 
271 static bool VuoUrl_urlIsAbsoluteFilePath(const char *url)
272 {
273  return ((strlen(url) >= 1) && (url[0] == '/'));
274 }
275 
279 static bool VuoUrl_urlIsUserRelativeFilePath(const char *url)
280 {
281  return ((strlen(url) >= 1) && (url[0] == '~'));
282 }
283 
288 {
289  return !((VuoText_length(url)==0) ||
293 }
294 
298 static const char VuoUrl_reservedCharacters[] =
299 {
300  0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
301  0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
302  '"', '$', '&', '+', ',', ':', ';', '=', '?', '@', '#', ' ',
303  // Percent must be last, so we don't escape the escapes.
304  '%'
305 };
306 
311 {
312  // Figure out how many characters we need to allocate for the escaped string.
313  unsigned long inLength = strlen(path);
314  unsigned long escapedLength = 0;
315  for (unsigned long i = 0; i < inLength; ++i)
316  {
317  char c = path[i];
318  for (int j = 0; j < sizeof(VuoUrl_reservedCharacters); ++j)
319  if (c == VuoUrl_reservedCharacters[j])
320  {
321  escapedLength += 2; // Expanding 1 character to "%xx"
322  break;
323  }
324  ++escapedLength;
325  }
326 
327  // Escape the string.
328  char *escapedUrl = (char *)malloc(escapedLength + 1);
329  unsigned long outIndex = 0;
330  const char *hexCharSet = "0123456789ABCDEF"; // Uppercase per https://tools.ietf.org/html/rfc3987#section-3.1
331  for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex)
332  {
333  unsigned char c = path[inIndex];
334  bool foundEscape = false;
335  for (int j = 0; j < sizeof(VuoUrl_reservedCharacters); ++j)
336  if (c == VuoUrl_reservedCharacters[j])
337  {
338  escapedUrl[outIndex++] = '%';
339  escapedUrl[outIndex++] = hexCharSet[c >> 4];
340  escapedUrl[outIndex++] = hexCharSet[c & 0x0f];
341  foundEscape = true;
342  break;
343  }
344 
345  // https://b33p.net/kosada/node/12361
346  // If it's a UTF-8 "Modifier Letter Colon", translate it back into an escaped ASCII-7 colon
347  // (since URLs can handle colons, unlike POSIX paths on macOS).
348  if (inIndex+2 < inLength)
349  if (c == 0xea && (unsigned char)path[inIndex+1] == 0x9e && (unsigned char)path[inIndex+2] == 0x89)
350  {
351  escapedUrl[outIndex++] = '%';
352  escapedUrl[outIndex++] = hexCharSet[':' >> 4];
353  escapedUrl[outIndex++] = hexCharSet[':' & 0x0f];
354  foundEscape = true;
355  inIndex += 2;
356  }
357 
358  if (!foundEscape)
359  escapedUrl[outIndex++] = c;
360  }
361  escapedUrl[outIndex] = 0;
362 
363  VuoText escapedUrlVT = VuoText_make(escapedUrl);
364  free(escapedUrl);
365 
366  return escapedUrlVT;
367 }
368 
373 {
374  // Figure out how many characters we need to allocate for the escaped string.
375  unsigned long inLength = strlen(url);
376  unsigned long escapedLength = 0;
377  for (unsigned long i = 0; i < inLength; ++i)
378  {
379  if ((unsigned char)url[i] > 0x7f)
380  escapedLength += 2; // Expanding 1 character to "%xx"
381  ++escapedLength;
382  }
383 
384  // Escape the string.
385  char *escapedUrl = (char *)malloc(escapedLength + 1);
386  unsigned long outIndex = 0;
387  const char *hexCharSet = "0123456789ABCDEF"; // Uppercase per https://tools.ietf.org/html/rfc3987#section-3.1
388  for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex)
389  {
390  unsigned char c = url[inIndex];
391  if (c > 0x7f)
392  {
393  escapedUrl[outIndex++] = '%';
394  escapedUrl[outIndex++] = hexCharSet[c >> 4];
395  escapedUrl[outIndex++] = hexCharSet[c & 0x0f];
396  }
397  else
398  escapedUrl[outIndex++] = c;
399  }
400  escapedUrl[outIndex] = 0;
401 
402  VuoText escapedUrlVT = VuoText_make(escapedUrl);
403  free(escapedUrl);
404 
405  return escapedUrlVT;
406 }
407 
408 static const char *VuoUrl_fileScheme = "file://";
409 static const char *VuoUrl_httpScheme = "http://";
410 
431 {
432  if (!url)
433  return NULL;
434 
435  char *resolvedUrl;
436 
437  // Case: The url contains a scheme.
438  if (VuoUrl_urlContainsScheme(url))
439  {
440  // Some URLs have literal spaces, which we need to transform into '%20' before passing to cURL.
441  size_t urlLen = strlen(url);
442  size_t spaceCount = 0;
443  for (size_t i = 0; i < urlLen; ++i)
444  if (url[i] == ' ')
445  ++spaceCount;
446  if (spaceCount)
447  {
448  resolvedUrl = (char *)malloc(strlen(url) + spaceCount*2);
449  size_t p = 0;
450  for (size_t i = 0; i < urlLen; ++i)
451  if (url[i] == ' ')
452  {
453  resolvedUrl[p++] = '%';
454  resolvedUrl[p++] = '2';
455  resolvedUrl[p++] = '0';
456  }
457  else
458  resolvedUrl[p++] = url[i];
459  resolvedUrl[p] = 0;
460  }
461  else
462  resolvedUrl = strdup(url);
463  }
464 
465  // Case: The url contains an absolute file path.
466  else if (VuoUrl_urlIsAbsoluteFilePath(url))
467  {
468  char *filePath = (char *)url;
469 
470  if (!(flags & VuoUrlNormalize_forSaving) && strncmp(filePath, "/tmp/", 5) == 0)
471  {
472  // We're trying to read a file from `/tmp`.
473  // If it doesn't exist there, also check the user's private temporary directory.
474  // Enables protocol driver compositions to find their default images.
475  if (access(filePath, 0) != 0)
476  {
477  char userTempDir[PATH_MAX];
478  size_t userTempDirLen;
479  if ((userTempDirLen = confstr(_CS_DARWIN_USER_TEMP_DIR, userTempDir, PATH_MAX)) > 0)
480  {
481  size_t filePathLen = strlen(filePath);
482  size_t mallocSize = userTempDirLen + filePathLen + 1;
483  char *privateFilePath = (char *)malloc(mallocSize);
484  strlcpy(privateFilePath, userTempDir, mallocSize);
485  strlcat(privateFilePath, filePath + 4, mallocSize);
486  filePath = privateFilePath;
487  }
488  }
489  }
490 
491  char *realPath = realpath(filePath, NULL);
492  // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
493  VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : filePath);
494  VuoRetain(escapedPath);
495  if (realPath)
496  free(realPath);
497  if (filePath != url)
498  free(filePath);
499 
500  size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
501  resolvedUrl = (char *)malloc(mallocSize);
502  strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
503  strlcat(resolvedUrl, escapedPath, mallocSize);
504 
505  VuoRelease(escapedPath);
506  }
507 
508  // Case: The url contains a user-relative (`~/…`) file path.
509  else if (VuoUrl_urlIsUserRelativeFilePath(url))
510  {
511  // 1. Expand the tilde into an absolute path.
512  VuoText absolutePath;
513  {
514  char *homeDir = getenv("HOME");
515  VuoText paths[2] = { homeDir, url+1 };
516  absolutePath = VuoText_append(paths, 2);
517  }
518  VuoLocal(absolutePath);
519 
520  // 2. Try to canonicalize the absolute path, and URL-escape it.
521  char *realPath = realpath(absolutePath, NULL);
522  // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
523  VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : absolutePath);
524  VuoLocal(escapedPath);
525  if (realPath)
526  free(realPath);
527 
528  // 3. Prepend the URL scheme.
529  size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
530  resolvedUrl = (char *)malloc(mallocSize);
531  strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
532  strlcat(resolvedUrl, escapedPath, mallocSize);
533  }
534 
535  // Case: The url contains a web link without a protocol/scheme.
536  else if (flags & VuoUrlNormalize_assumeHttp)
537  {
538  // Prepend the URL scheme.
539  size_t mallocSize = strlen(VuoUrl_httpScheme) + strlen(url) + 1;
540  resolvedUrl = (char *)malloc(mallocSize);
541  strlcpy(resolvedUrl, VuoUrl_httpScheme, mallocSize);
542  strlcat(resolvedUrl, url, mallocSize);
543  }
544 
545  // Case: The url contains a relative file path.
546  else
547  {
548  const char *currentWorkingDir = VuoGetWorkingDirectory();
549 
550  bool compositionIsExportedApp = false;
551 
552  // If the current working directory is "/", assume that we are working with an exported app;
553  // resolve loaded resources relative to the app bundle's "Resources" directory, and
554  // resolve saved resources relative to the user's Desktop.
555  char *absolutePath;
556  if (!strcmp(currentWorkingDir, "/"))
557  {
558  // Get the exported executable path.
559  char rawExecutablePath[PATH_MAX+1];
560  uint32_t size = sizeof(rawExecutablePath);
561  _NSGetExecutablePath(rawExecutablePath, &size);
562 
563  char cleanedExecutablePath[PATH_MAX+1];
564  realpath(rawExecutablePath, cleanedExecutablePath);
565 
566  // Derive the path of the app bundle's "Resources" directory from its executable path.
567  size_t pathSize = PATH_MAX + 1;
568  char executableDir[pathSize];
569  strlcpy(executableDir, dirname(cleanedExecutablePath), pathSize);
570 
571  const char *resourcesPathFromExecutable = "/../Resources";
572  pathSize = strlen(executableDir) + strlen(resourcesPathFromExecutable) + 1;
573  char rawResourcesPath[pathSize];
574  strlcpy(rawResourcesPath, executableDir, pathSize);
575  strlcat(rawResourcesPath, resourcesPathFromExecutable, pathSize);
576 
577  char cleanedResourcesPath[PATH_MAX+1];
578  realpath(rawResourcesPath, cleanedResourcesPath);
579 
580  // If the "Resources" directory does not exist, we must not be dealing with an exported app after all.
581  // If it does, proceed under the assumption that we are.
582  if (access(cleanedResourcesPath, 0) == 0)
583  {
584  compositionIsExportedApp = true;
585 
586  if (flags & VuoUrlNormalize_forSaving)
587  {
588  char *homeDir = getenv("HOME");
589  const char *desktop = "/Desktop/";
590  size_t mallocSize = strlen(homeDir) + strlen(desktop) + strlen(url) + 1;
591  absolutePath = (char *)malloc(mallocSize);
592  strlcpy(absolutePath, homeDir, mallocSize);
593  strlcat(absolutePath, desktop, mallocSize);
594  strlcat(absolutePath, url, mallocSize);
595  }
596  else
597  {
598  size_t mallocSize = strlen(cleanedResourcesPath) + strlen("/") + strlen(url) + 1;
599  absolutePath = (char *)malloc(mallocSize);
600  strlcpy(absolutePath, cleanedResourcesPath, mallocSize);
601  strlcat(absolutePath, "/", mallocSize);
602  strlcat(absolutePath, url, mallocSize);
603  }
604  }
605  }
606 
607  // If we are not working with an exported app, resolve resources relative to the current working directory.
608  if (!compositionIsExportedApp)
609  {
610  size_t mallocSize = strlen(currentWorkingDir) + strlen("/") + strlen(url) + 1;
611  absolutePath = (char *)malloc(mallocSize);
612  strlcpy(absolutePath, currentWorkingDir, mallocSize);
613  strlcat(absolutePath, "/", mallocSize);
614  strlcat(absolutePath, url, mallocSize);
615  }
616 
617  char *realPath = realpath(absolutePath, NULL);
618  // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
619  VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : absolutePath);
620  VuoLocal(escapedPath);
621  if (realPath)
622  free(realPath);
623  free(absolutePath);
624 
625  // Prepend the URL scheme.
626  size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
627  resolvedUrl = (char *)malloc(mallocSize);
628  strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
629  strlcat(resolvedUrl, escapedPath, mallocSize);
630  }
631 
632  // Remove trailing slash, if any.
633  size_t lastIndex = strlen(resolvedUrl) - 1;
634  if (resolvedUrl[lastIndex] == '/')
635  resolvedUrl[lastIndex] = 0;
636 
637  VuoText resolvedUrlVT = VuoText_make(resolvedUrl);
638  VuoRetain(resolvedUrlVT);
639  free(resolvedUrl);
640 
641  VuoText escapedUrl = VuoUrl_escapeUTF8(resolvedUrlVT);
642  VuoRelease(resolvedUrlVT);
643  return escapedUrl;
644 }
645 
652 {
653  unsigned long inLength = strlen(url);
654  char *unescapedUrl = (char *)malloc(inLength + 1);
655  unsigned long outIndex = 0;
656  for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex, ++outIndex)
657  {
658  char c = url[inIndex];
659  if (c == '%')
660  {
661  if (inIndex + 2 >= inLength)
662  break;
663  char highNibbleASCII = url[++inIndex];
664  char lowNibbleASCII = url[++inIndex];
665  unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
666  }
667  else
668  unescapedUrl[outIndex] = c;
669  }
670  unescapedUrl[outIndex] = 0;
671 
672  VuoText unescapedUrlVT = VuoText_make(unescapedUrl);
673  free(unescapedUrl);
674  return unescapedUrlVT;
675 }
676 
683 {
684  if (!url)
685  return NULL;
686 
687  unsigned long fileSchemeLength = strlen(VuoUrl_fileScheme);
688  if (strncmp(url, VuoUrl_fileScheme, fileSchemeLength) != 0)
689  return NULL;
690 
691  // Unescape the string.
692  unsigned long inLength = strlen(url);
693  // Make room for Unicode colons.
694  char *unescapedUrl = (char *)malloc(inLength*3 + 1);
695  unsigned long outIndex = 0;
696  for (unsigned long inIndex = fileSchemeLength; inIndex < inLength; ++inIndex, ++outIndex)
697  {
698  char c = url[inIndex];
699  if (c == '%')
700  {
701  char highNibbleASCII = url[++inIndex];
702  char lowNibbleASCII = url[++inIndex];
703  unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
704  }
705  else
706  unescapedUrl[outIndex] = c;
707 
708  // https://b33p.net/kosada/node/12361
709  // macOS presents colons as forward-slashes (https://developer.apple.com/library/mac/qa/qa1392/).
710  // To avoid confusion with dates, change ASCII-7 colon to UTF-8 "Modifier Letter Colon"
711  // (which looks visually identical to ASCII-7 colon).
712  if (unescapedUrl[outIndex] == ':')
713  {
714  unescapedUrl[outIndex++] = 0xea;
715  unescapedUrl[outIndex++] = 0x9e;
716  unescapedUrl[outIndex] = 0x89;
717  }
718  }
719  unescapedUrl[outIndex] = 0;
720 
721  VuoText unescapedUrlVT = VuoText_make(unescapedUrl);
722  free(unescapedUrl);
723 
724  return unescapedUrlVT;
725 }
726 
736 bool VuoUrl_isBundle(const VuoUrl url)
737 {
738  if (VuoText_isEmpty(url))
739  return false;
740 
741  {
742  VuoText path = VuoUrl_getPosixPath(url);
743  if (!path)
744  return false;
745  VuoRetain(path);
746  VuoRelease(path);
747  }
748 
749  CFStringRef urlCFS = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
750  CFURLRef cfurl = CFURLCreateWithString(NULL, urlCFS, NULL);
751  CFRelease(urlCFS);
752  if (!cfurl)
753  {
754  VUserLog("Error: Couldn't check '%s': Invalid URL.", url);
755  return false;
756  }
757 
758  LSItemInfoRecord outItemInfo;
759  OSStatus ret = LSCopyItemInfoForURL(cfurl, kLSRequestAllFlags, &outItemInfo);
760  CFRelease(cfurl);
761  if (ret)
762  {
763  char *errorString = VuoOsStatus_getText(ret);
764  VUserLog("Error: Couldn't check '%s': %s", url, errorString);
765  free(errorString);
766  return false;
767  }
768  return outItemInfo.flags & kLSItemInfoIsPackage;
769 }
770 
774 VuoUrl VuoUrl_appendFileExtension(const char *filename, struct json_object *validExtensions)
775 {
776  char* fileSuffix = strrchr(filename, '.');
777  char* curExtension = fileSuffix != NULL ? strdup(fileSuffix+1) : NULL;
778 
779  if(curExtension != NULL)
780  for(char *p = &curExtension[0]; *p; p++) *p = tolower(*p);
781 
782  // if the string already has one of the valid file extension suffixes, return.
783  for (int i = 0; i < json_object_array_length(validExtensions); ++i)
784  {
785  if (curExtension != NULL && strcmp(curExtension, json_object_get_string(json_object_array_get_idx(validExtensions, i))) == 0)
786  {
787  free(curExtension);
788  return VuoText_make(filename);
789  }
790  }
791 
792  free(curExtension);
793 
794  const char *chosenExtension = json_object_get_string(json_object_array_get_idx(validExtensions, 0));
795  size_t buf_size = strlen(filename) + strlen(chosenExtension) + 2;
796  char* newfilepath = (char*)malloc(buf_size * sizeof(char));
797  snprintf(newfilepath, buf_size, "%s.%s", filename, chosenExtension);
798 
799  VuoText text = VuoText_make(newfilepath);
800  free(newfilepath);
801 
802  return text;
803 }
804