Vuo  2.4.1
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 return VuoText_makeWithoutCopying(escapedUrl);
386}
387
392{
393 // Figure out how many characters we need to allocate for the escaped string.
394 unsigned long inLength = strlen(url);
395 unsigned long escapedLength = 0;
396 for (unsigned long i = 0; i < inLength; ++i)
397 {
398 if ((unsigned char)url[i] > 0x7f)
399 escapedLength += 2; // Expanding 1 character to "%xx"
400 ++escapedLength;
401 }
402
403 // Escape the string.
404 char *escapedUrl = (char *)malloc(escapedLength + 1);
405 unsigned long outIndex = 0;
406 const char *hexCharSet = "0123456789ABCDEF"; // Uppercase per https://tools.ietf.org/html/rfc3987#section-3.1
407 for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex)
408 {
409 unsigned char c = url[inIndex];
410 if (c > 0x7f)
411 {
412 escapedUrl[outIndex++] = '%';
413 escapedUrl[outIndex++] = hexCharSet[c >> 4];
414 escapedUrl[outIndex++] = hexCharSet[c & 0x0f];
415 }
416 else
417 escapedUrl[outIndex++] = c;
418 }
419 escapedUrl[outIndex] = 0;
420
421 return VuoText_makeWithoutCopying(escapedUrl);
422}
423
424static const char *VuoUrl_fileScheme = "file:";
425static const char *VuoUrl_httpScheme = "http://";
426
447{
448 if (!url)
449 return NULL;
450
451 // Trim off the file schema, if present, and handle such URLs via the absolute/user-relative/relative file path cases below.
452 VuoText trimmedUrl;
453 unsigned long fileSchemeLength = strlen(VuoUrl_fileScheme);
454 if (strncmp(url, VuoUrl_fileScheme, fileSchemeLength) == 0)
455 {
456 VuoText u = url + fileSchemeLength;
457 if (strncmp(u, "//", 2) == 0)
458 u += 2;
459
460 // Assume a URL with a schema is already escaped; unescape it for consistent processing below.
461 trimmedUrl = VuoUrl_decodeRFC3986(u);
462 }
463 else
464 trimmedUrl = url;
465 VuoRetain(trimmedUrl);
466
467 char *resolvedUrl;
468
469 // Case: The url contains a scheme.
470 if (VuoUrl_urlContainsScheme(trimmedUrl))
471 {
472 // Some URLs have literal spaces, which we need to transform into '%20' before passing to cURL.
473 size_t urlLen = strlen(trimmedUrl);
474 size_t spaceCount = 0;
475 for (size_t i = 0; i < urlLen; ++i)
476 if (trimmedUrl[i] == ' ')
477 ++spaceCount;
478 if (spaceCount)
479 {
480 resolvedUrl = (char *)malloc(strlen(trimmedUrl) + spaceCount*2);
481 size_t p = 0;
482 for (size_t i = 0; i < urlLen; ++i)
483 if (trimmedUrl[i] == ' ')
484 {
485 resolvedUrl[p++] = '%';
486 resolvedUrl[p++] = '2';
487 resolvedUrl[p++] = '0';
488 }
489 else
490 resolvedUrl[p++] = trimmedUrl[i];
491 resolvedUrl[p] = 0;
492 }
493 else
494 resolvedUrl = strdup(trimmedUrl);
495 }
496
497 // Case: The url contains an absolute file path.
498 else if (VuoUrl_urlIsAbsoluteFilePath(trimmedUrl))
499 {
500 char *filePath = (char *)trimmedUrl;
501
502 if (!(flags & VuoUrlNormalize_forSaving) && strncmp(filePath, "/tmp/", 5) == 0)
503 {
504 // We're trying to read a file from `/tmp`.
505 // If it doesn't exist there, also check the user's private temporary directory.
506 // Enables protocol driver compositions to find their default images.
507 if (access(filePath, 0) != 0)
508 {
509 char userTempDir[PATH_MAX];
510 size_t userTempDirLen;
511 if ((userTempDirLen = confstr(_CS_DARWIN_USER_TEMP_DIR, userTempDir, PATH_MAX)) > 0)
512 {
513 size_t filePathLen = strlen(filePath);
514 size_t mallocSize = userTempDirLen + filePathLen + 1;
515 char *privateFilePath = (char *)malloc(mallocSize);
516 strlcpy(privateFilePath, userTempDir, mallocSize);
517 strlcat(privateFilePath, filePath + 4, mallocSize);
518 filePath = privateFilePath;
519 }
520 }
521 }
522
523 if ((flags & VuoUrlNormalize_forLaunching) && strncmp(filePath, "/Applications/", 14) == 0)
524 {
525 // If the app doesn't exist at `/Applications`, check `/System/Applications`.
526 if (access(filePath, 0) != 0)
527 {
528 const char *systemPrefix = "/System";
529 size_t systemPrefixLen = strlen(systemPrefix);
530 size_t filePathLen = strlen(filePath);
531 size_t mallocSize = systemPrefixLen + filePathLen + 1;
532 char *systemPath = (char *)malloc(mallocSize);
533 strlcpy(systemPath, systemPrefix, mallocSize);
534 strlcat(systemPath, filePath, mallocSize);
535 if (access(systemPath, 0) == 0)
536 filePath = systemPath;
537 else
538 free(systemPath);
539 }
540 }
541
542 char *realPath = realpath(filePath, NULL);
543 // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
544 VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : filePath);
545 VuoRetain(escapedPath);
546 if (realPath)
547 free(realPath);
548 if (filePath != trimmedUrl)
549 free(filePath);
550
551 size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
552 resolvedUrl = (char *)malloc(mallocSize);
553 strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
554 strlcat(resolvedUrl, escapedPath, mallocSize);
555
556 VuoRelease(escapedPath);
557 }
558
559 // Case: The url contains a user-relative (`~/…`) file path.
560 else if (VuoUrl_urlIsUserRelativeFilePath(trimmedUrl))
561 {
562 // 1. Expand the tilde into an absolute path.
563 VuoText absolutePath;
564 {
565 char *homeDir = getenv("HOME");
566 VuoText paths[2] = { homeDir, trimmedUrl+1 };
567 absolutePath = VuoText_append(paths, 2);
568 }
569 VuoLocal(absolutePath);
570
571 // 2. Try to canonicalize the absolute path, and URL-escape it.
572 char *realPath = realpath(absolutePath, NULL);
573 // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
574 VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : absolutePath);
575 VuoLocal(escapedPath);
576 if (realPath)
577 free(realPath);
578
579 // 3. Prepend the URL scheme.
580 size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
581 resolvedUrl = (char *)malloc(mallocSize);
582 strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
583 strlcat(resolvedUrl, escapedPath, mallocSize);
584 }
585
586 // Case: The url contains a web link without a protocol/scheme.
587 else if (flags & VuoUrlNormalize_assumeHttp)
588 {
589 // Prepend the URL scheme.
590 size_t mallocSize = strlen(VuoUrl_httpScheme) + strlen(trimmedUrl) + 1;
591 resolvedUrl = (char *)malloc(mallocSize);
592 strlcpy(resolvedUrl, VuoUrl_httpScheme, mallocSize);
593 strlcat(resolvedUrl, trimmedUrl, mallocSize);
594 }
595
596 // Case: The url contains a relative file path.
597 else
598 {
599 const char *currentWorkingDir = VuoGetWorkingDirectory();
600
601 // - When macOS LaunchServices launches an app, it sets the current working directory to root.
602 // - When macOS ScreenSaverEngine launches a plugin, it sets the current working directory to
603 // `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data`.
604 bool compositionIsExportedAppOrPlugin = false;
605 char *absolutePath;
606 if (strcmp(currentWorkingDir, "/") == 0
607 || strstr(currentWorkingDir, "/Library/Containers/"))
608 {
609 // If we're running as an exported app or plugin,
610 // resolve loaded resources relative to the app bundle's "Resources" directory, and
611 // resolve saved resources relative to the user's Desktop.
612
613 // Get the exported executable path.
614 char rawExecutablePath[PATH_MAX+1];
615 uint32_t size = sizeof(rawExecutablePath);
616 _NSGetExecutablePath(rawExecutablePath, &size);
617
618 char cleanedExecutablePath[PATH_MAX+1];
619 realpath(rawExecutablePath, cleanedExecutablePath);
620
621 // Derive the path of the app bundle's "Resources" directory from its executable path.
622 size_t pathSize = PATH_MAX + 1;
623 char executableDir[pathSize];
624 strlcpy(executableDir, dirname(cleanedExecutablePath), pathSize);
625
626 const char *resourcesPathFromExecutable = "/../Resources";
627 pathSize = strlen(executableDir) + strlen(resourcesPathFromExecutable) + 1;
628 char rawResourcesPath[pathSize];
629 strlcpy(rawResourcesPath, executableDir, pathSize);
630 strlcat(rawResourcesPath, resourcesPathFromExecutable, pathSize);
631
632 char cleanedResourcesPath[PATH_MAX+1];
633 realpath(rawResourcesPath, cleanedResourcesPath);
634
635 // If the "Resources" directory does not exist, we must not be dealing with an exported app after all.
636 // If it does, proceed under the assumption that we are.
637 if (access(cleanedResourcesPath, 0) == 0)
638 {
639 if (flags & VuoUrlNormalize_forSaving)
640 {
641 char *homeDir = getenv("HOME");
642 if (homeDir)
643 {
644 const char *desktop = "/Desktop/";
645 size_t mallocSize = strlen(homeDir) + strlen(desktop) + strlen(trimmedUrl) + 1;
646 absolutePath = (char *)malloc(mallocSize);
647 strlcpy(absolutePath, homeDir, mallocSize);
648 strlcat(absolutePath, desktop, mallocSize);
649 strlcat(absolutePath, trimmedUrl, mallocSize);
650 compositionIsExportedAppOrPlugin = true;
651 }
652 }
653 else
654 {
655 size_t mallocSize = strlen(cleanedResourcesPath) + strlen("/") + strlen(trimmedUrl) + 1;
656 absolutePath = (char *)malloc(mallocSize);
657 strlcpy(absolutePath, cleanedResourcesPath, mallocSize);
658 strlcat(absolutePath, "/", mallocSize);
659 strlcat(absolutePath, trimmedUrl, mallocSize);
660 compositionIsExportedAppOrPlugin = true;
661 }
662 }
663 }
664
665 // If we are not working with an exported app, resolve resources relative to the current working directory.
666 if (!compositionIsExportedAppOrPlugin)
667 {
668 size_t mallocSize = strlen(currentWorkingDir) + strlen("/") + strlen(trimmedUrl) + 1;
669 absolutePath = (char *)malloc(mallocSize);
670 strlcpy(absolutePath, currentWorkingDir, mallocSize);
671 strlcat(absolutePath, "/", mallocSize);
672 strlcat(absolutePath, trimmedUrl, mallocSize);
673 }
674
675 // If we're looking for an app, and it isn't found in the exported app or current working directory,
676 // try the standard applications folders.
677 if ((flags & VuoUrlNormalize_forLaunching) && access(absolutePath, 0) != 0)
678 {
679 // Check `~/Applications`.
680 char *homeDir = getenv("HOME");
681 if (homeDir)
682 {
683 const char *applicationsPrefix = "/Applications/";
684 size_t homeDirLen = strlen(homeDir);
685 size_t applicationsPrefixLen = strlen(applicationsPrefix);
686 size_t trimmedUrlLen = strlen(trimmedUrl);
687 size_t mallocSize = homeDirLen + applicationsPrefixLen + trimmedUrlLen + 1;
688 char *path = (char *)malloc(mallocSize);
689 strlcpy(path, homeDir, mallocSize);
690 strlcat(path, applicationsPrefix, mallocSize);
691 strlcat(path, trimmedUrl, mallocSize);
692 if (access(path, 0) == 0)
693 {
694 free(absolutePath);
695 absolutePath = path;
696 }
697 else
698 free(path);
699 }
700
701 // Check `/Applications`.
702 {
703 const char *applicationsPrefix = "/Applications/";
704 size_t applicationsPrefixLen = strlen(applicationsPrefix);
705 size_t trimmedUrlLen = strlen(trimmedUrl);
706 size_t mallocSize = applicationsPrefixLen + trimmedUrlLen + 1;
707 char *path = (char *)malloc(mallocSize);
708 strlcpy(path, applicationsPrefix, mallocSize);
709 strlcat(path, trimmedUrl, mallocSize);
710 if (access(path, 0) == 0)
711 {
712 free(absolutePath);
713 absolutePath = path;
714 }
715 else
716 free(path);
717 }
718
719 // Check `/System/Applications`.
720 {
721 const char *applicationsPrefix = "/System/Applications/";
722 size_t applicationsPrefixLen = strlen(applicationsPrefix);
723 size_t trimmedUrlLen = strlen(trimmedUrl);
724 size_t mallocSize = applicationsPrefixLen + trimmedUrlLen + 1;
725 char *path = (char *)malloc(mallocSize);
726 strlcpy(path, applicationsPrefix, mallocSize);
727 strlcat(path, trimmedUrl, mallocSize);
728 if (access(path, 0) == 0)
729 {
730 free(absolutePath);
731 absolutePath = path;
732 }
733 else
734 free(path);
735 }
736 }
737
738
739 char *realPath = realpath(absolutePath, NULL);
740 // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
741 VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : absolutePath);
742 VuoLocal(escapedPath);
743 if (realPath)
744 free(realPath);
745 free(absolutePath);
746
747 // Prepend the URL scheme.
748 size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
749 resolvedUrl = (char *)malloc(mallocSize);
750 strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
751 strlcat(resolvedUrl, escapedPath, mallocSize);
752 }
753
754 VuoRelease(trimmedUrl);
755
756 // Remove trailing slash, if any.
757 size_t lastIndex = strlen(resolvedUrl) - 1;
758 if (resolvedUrl[lastIndex] == '/')
759 resolvedUrl[lastIndex] = 0;
760
761 VuoText resolvedUrlVT = VuoText_makeWithoutCopying(resolvedUrl);
762 VuoRetain(resolvedUrlVT);
763
764 VuoText escapedUrl = VuoUrl_escapeUTF8(resolvedUrlVT);
765 VuoRelease(resolvedUrlVT);
766 return escapedUrl;
767}
768
775{
776 unsigned long inLength = strlen(url);
777 char *unescapedUrl = (char *)malloc(inLength + 1);
778 unsigned long outIndex = 0;
779 for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex, ++outIndex)
780 {
781 char c = url[inIndex];
782 if (c == '%')
783 {
784 if (inIndex + 2 >= inLength)
785 break;
786 char highNibbleASCII = url[++inIndex];
787 char lowNibbleASCII = url[++inIndex];
788 unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
789 }
790 else
791 unescapedUrl[outIndex] = c;
792 }
793 unescapedUrl[outIndex] = 0;
794
795 return VuoText_makeWithoutCopying(unescapedUrl);
796}
797
804{
805 if (!url)
806 return NULL;
807
808 unsigned long fileSchemeLength = strlen(VuoUrl_fileScheme);
809 if (strncmp(url, VuoUrl_fileScheme, fileSchemeLength) != 0)
810 return NULL;
811
812 // https://tools.ietf.org/html/rfc8089#appendix-B
813 if (strncmp(url + fileSchemeLength, "//", 2) == 0)
814 fileSchemeLength += 2;
815
816 // Unescape the string.
817 unsigned long inLength = strlen(url);
818 // Make room for Unicode colons.
819 char *unescapedUrl = (char *)malloc(inLength*3 + 1);
820 unsigned long outIndex = 0;
821 for (unsigned long inIndex = fileSchemeLength; inIndex < inLength; ++inIndex, ++outIndex)
822 {
823 char c = url[inIndex];
824 if (c == '%')
825 {
826 char highNibbleASCII = url[++inIndex];
827 char lowNibbleASCII = url[++inIndex];
828 unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
829 }
830 else
831 unescapedUrl[outIndex] = c;
832
833 // https://b33p.net/kosada/node/12361
834 // macOS presents colons as forward-slashes (https://developer.apple.com/library/mac/qa/qa1392/).
835 // To avoid confusion with dates, change ASCII-7 colon to UTF-8 "Modifier Letter Colon"
836 // (which looks visually identical to ASCII-7 colon).
837 if (unescapedUrl[outIndex] == ':')
838 {
839 unescapedUrl[outIndex++] = 0xea;
840 unescapedUrl[outIndex++] = 0x9e;
841 unescapedUrl[outIndex] = 0x89;
842 }
843 }
844 unescapedUrl[outIndex] = 0;
845
846 return VuoText_makeWithoutCopying(unescapedUrl);
847}
848
858bool VuoUrl_isBundle(const VuoUrl url)
859{
860 if (VuoText_isEmpty(url))
861 return false;
862
863 {
864 VuoText path = VuoUrl_getPosixPath(url);
865 if (!path)
866 return false;
867 VuoRetain(path);
868 VuoRelease(path);
869 }
870
871 CFStringRef urlCFS = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
872 CFURLRef cfurl = CFURLCreateWithString(NULL, urlCFS, NULL);
873 CFRelease(urlCFS);
874 if (!cfurl)
875 {
876 VUserLog("Error: Couldn't check '%s': Invalid URL.", url);
877 return false;
878 }
879
880 CFTypeRef info = NULL;
881 CFErrorRef error = NULL;
882 bool ret = CFURLCopyResourcePropertyForKey(cfurl, kCFURLIsPackageKey, &info, &error);
883 CFRelease(cfurl);
884 if (!ret)
885 {
886 CFStringRef errorCFS = CFErrorCopyDescription(error);
887 CFRelease(error);
888 VuoText errorText = VuoText_makeFromCFString(errorCFS);
889 CFRelease(errorCFS);
890 VuoRetain(errorText);
891 VUserLog("Error: Couldn't check '%s': %s", url, errorText);
892 VuoRelease(errorText);
893 return false;
894 }
895 return info == kCFBooleanTrue;
896}
897
901VuoUrl VuoUrl_appendFileExtension(const char *filename, struct json_object *validExtensions)
902{
903 char* fileSuffix = strrchr(filename, '.');
904 char* curExtension = fileSuffix != NULL ? strdup(fileSuffix+1) : NULL;
905
906 if(curExtension != NULL)
907 for(char *p = &curExtension[0]; *p; p++) *p = tolower(*p);
908
909 // if the string already has one of the valid file extension suffixes, return.
910 for (int i = 0; i < json_object_array_length(validExtensions); ++i)
911 {
912 if (curExtension != NULL && strcmp(curExtension, json_object_get_string(json_object_array_get_idx(validExtensions, i))) == 0)
913 {
914 free(curExtension);
915 return VuoText_make(filename);
916 }
917 }
918
919 free(curExtension);
920
921 const char *chosenExtension = json_object_get_string(json_object_array_get_idx(validExtensions, 0));
922 size_t buf_size = strlen(filename) + strlen(chosenExtension) + 2;
923 char* newfilepath = (char*)malloc(buf_size * sizeof(char));
924 snprintf(newfilepath, buf_size, "%s.%s", filename, chosenExtension);
925
926 return VuoText_makeWithoutCopying(newfilepath);
927}