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