Vuo 2.4.4
Loading...
Searching...
No Matches
VuoUrl.c
Go to the documentation of this file.
1
10#include "VuoUrl.h"
11#include "VuoOsStatus.h"
12
13#include <regex.h>
14#include <mach-o/dyld.h> // for _NSGetExecutablePath()
15#include <libgen.h> // for dirname()
16#include <CoreServices/CoreServices.h>
17#include "VuoUrlParser.h"
18
19
21#ifdef VUO_COMPILER
23 "title" : "URL",
24 "description" : "Uniform Resource Locator.",
25 "keywords" : [ "link" ],
26 "version" : "1.0.0",
27 "dependencies" : [
28 "CoreServices.framework",
29 "VuoInteger",
30 "VuoOsStatus",
31 "VuoText",
32 "VuoUrlParser"
33 ]
34 });
35#endif
37
42{
43 const char *textString = "";
44 if (json_object_get_type(js) == json_type_string)
45 textString = json_object_get_string(js);
46
47 VuoUrl url;
48 if (textString)
49 url = strdup(textString);
50 else
51 url = strdup("");
52 VuoRegister(url, free);
53
54 return url;
55}
56
60json_object *VuoUrl_getJson(const VuoUrl value)
61{
62 if (!value)
63 return json_object_new_string("");
64
65 return json_object_new_string(value);
66}
67
71char *VuoUrl_getSummary(const VuoUrl value)
72{
73 VuoText t = VuoText_truncateWithEllipsis(value, 1024, VuoTextTruncation_End);
74 VuoRetain(t);
75 char *summary = strdup(t);
76 VuoRelease(t);
77 return summary;
78}
79
85bool VuoUrl_getParts(const VuoUrl url, VuoText *scheme, VuoText *user, VuoText *host, VuoInteger *port, VuoText *path, VuoText *query, VuoText *fragment)
86{
87 if (VuoText_isEmpty(url))
88 return false;
89
90 const char *localURL = url;
91 char *urlWithEmptyAuthority = NULL;
92 struct http_parser_url parsedUrl;
93 size_t urlLen = strlen(url);
94 if (http_parser_parse_url(url, urlLen, false, &parsedUrl))
95 {
96 // Maybe this is a "data:" URI (which http_parser_parse_url can't parse).
97 if (strncmp(url, "data:", 5) == 0)
98 {
99 if (scheme)
100 *scheme = VuoText_make("data");
101 if (user)
102 *user = NULL;
103 if (host)
104 *host = NULL;
105 if (port)
106 *port = 0;
107 if (path)
108 *path = NULL;
109 if (query)
110 *query = NULL;
111 if (fragment)
112 *fragment = NULL;
113 return true;
114 }
115
116 // Maybe this is a "no-authority" `file:` URI (which http_parser_parse_url can't parse);
117 // change it to an "empty-authority" URI.
118 else if (strncmp(url, "file:/", 6) == 0 && urlLen > 6 && url[6] != '/')
119 {
120 urlWithEmptyAuthority = malloc(urlLen + 2 + 1);
121 strcpy(urlWithEmptyAuthority, "file://");
122 strcat(urlWithEmptyAuthority, url + 5);
123 if (http_parser_parse_url(urlWithEmptyAuthority, urlLen + 2, false, &parsedUrl))
124 {
125 free(urlWithEmptyAuthority);
126 return false;
127 }
128 localURL = urlWithEmptyAuthority;
129 }
130
131 else
132 return false;
133 }
134
135 if (scheme)
136 {
137 if (parsedUrl.field_set & (1 << UF_SCHEMA))
138 *scheme = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_SCHEMA ].off, parsedUrl.field_data[UF_SCHEMA ].len);
139 else
140 *scheme = NULL;
141 }
142
143 if (user)
144 {
145 if (parsedUrl.field_set & (1 << UF_USERINFO))
146 *user = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_USERINFO].off, parsedUrl.field_data[UF_USERINFO].len);
147 else
148 *user = NULL;
149 }
150
151 if (host)
152 {
153 if (parsedUrl.field_set & (1 << UF_HOST))
154 *host = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_HOST ].off, parsedUrl.field_data[UF_HOST ].len);
155 else
156 *host = NULL;
157 }
158
159 if (port)
160 {
161 if (parsedUrl.field_set & (1 << UF_PORT))
162 // Explicitly-specified port
163 *port = parsedUrl.port;
164 else
165 {
166 // Guess the port from the scheme
167 *port = 0;
168 if (strcmp(*scheme, "http") == 0)
169 *port = 80;
170 else if (strcmp(*scheme, "https") == 0)
171 *port = 443;
172 }
173 }
174
175 if (path)
176 {
177 if (parsedUrl.field_set & (1 << UF_PATH))
178 *path = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_PATH ].off, parsedUrl.field_data[UF_PATH ].len);
179 else
180 *path = NULL;
181 }
182
183 if (query)
184 {
185 if (parsedUrl.field_set & (1 << UF_QUERY))
186 *query = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_QUERY ].off, parsedUrl.field_data[UF_QUERY ].len);
187 else
188 *query = NULL;
189 }
190
191 if (fragment)
192 {
193 if (parsedUrl.field_set & (1 << UF_FRAGMENT))
194 *fragment = VuoText_makeWithMaxLength(localURL + parsedUrl.field_data[UF_FRAGMENT].off, parsedUrl.field_data[UF_FRAGMENT].len);
195 else
196 *fragment = NULL;
197 }
198
199 if (urlWithEmptyAuthority)
200 free(urlWithEmptyAuthority);
201
202 return true;
203}
204
212bool VuoUrl_getFileParts(const VuoUrl url, VuoText *path, VuoText *folder, VuoText *filename, VuoText *extension)
213{
214 if (VuoText_isEmpty(url))
215 return false;
216
217 *path = VuoUrl_getPosixPath(url);
218 if (!*path)
219 return false;
220
221 size_t separatorIndex = VuoText_findLastOccurrence(*path, "/");
222 VuoText fileAndExtension;
223 if (separatorIndex)
224 {
225 *folder = VuoText_substring(*path, 1, separatorIndex);
226 size_t length = VuoText_length(*path);
227 if (separatorIndex < length)
228 fileAndExtension = VuoText_substring(*path, separatorIndex + 1, length);
229 else
230 fileAndExtension = NULL;
231 }
232 else
233 {
234 *folder = NULL;
235 fileAndExtension = VuoText_make(*path);
236 }
237 VuoRetain(fileAndExtension);
238
239 size_t dotIndex = VuoText_findLastOccurrence(fileAndExtension, ".");
240 if (dotIndex)
241 {
242 *filename = VuoText_substring(fileAndExtension, 1, dotIndex - 1);
243 *extension = VuoText_substring(fileAndExtension, dotIndex + 1, VuoText_length(fileAndExtension));
244 }
245 else
246 {
247 *filename = VuoText_make(fileAndExtension);
248 *extension = NULL;
249 }
250
251 VuoRelease(fileAndExtension);
252
253 return true;
254}
255
259bool VuoUrl_areEqual(const VuoText a, const VuoText b)
260{
261 return VuoText_areEqual(a,b);
262}
263
267bool VuoUrl_isLessThan(const VuoText a, const VuoText b)
268{
269 return VuoText_isLessThan(a,b);
270}
271
275static bool VuoUrl_urlContainsScheme(const char *url)
276{
277 __block bool matchFound;
278 VuoText_performWithUTF8Locale(^(locale_t locale){
279 const char *urlWithSchemePattern = "^[a-zA-Z][a-zA-Z0-9+-\\.]+:";
280 regex_t urlWithSchemeRegExp;
281 size_t nmatch = 0;
282 regmatch_t pmatch[0];
283 regcomp_l(&urlWithSchemeRegExp, urlWithSchemePattern, REG_EXTENDED, locale);
284 matchFound = !regexec(&urlWithSchemeRegExp, url, nmatch, pmatch, 0);
285 regfree(&urlWithSchemeRegExp);
286 });
287
288 return matchFound;
289}
290
294static bool VuoUrl_urlIsAbsoluteFilePath(const char *url)
295{
296 return ((strlen(url) >= 1) && (url[0] == '/'));
297}
298
302static bool VuoUrl_urlIsUserRelativeFilePath(const char *url)
303{
304 return ((strlen(url) >= 1) && (url[0] == '~'));
305}
306
311{
312 return !((VuoText_length(url)==0) ||
316}
317
321static const char VuoUrl_reservedCharacters[] =
322{
323 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
324 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
325 '"', '$', '&', '+', ',', ':', ';', '=', '?', '@', '#', ' ',
326 // Percent must be last, so we don't escape the escapes.
327 '%'
328};
329
334{
335 // Figure out how many characters we need to allocate for the escaped string.
336 unsigned long inLength = strlen(path);
337 unsigned long escapedLength = 0;
338 for (unsigned long i = 0; i < inLength; ++i)
339 {
340 char c = path[i];
341 for (int j = 0; j < sizeof(VuoUrl_reservedCharacters); ++j)
342 if (c == VuoUrl_reservedCharacters[j])
343 {
344 escapedLength += 2; // Expanding 1 character to "%xx"
345 break;
346 }
347 ++escapedLength;
348 }
349
350 // Escape the string.
351 char *escapedUrl = (char *)malloc(escapedLength + 1);
352 unsigned long outIndex = 0;
353 const char *hexCharSet = "0123456789ABCDEF"; // Uppercase per https://tools.ietf.org/html/rfc3987#section-3.1
354 for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex)
355 {
356 unsigned char c = path[inIndex];
357 bool foundEscape = false;
358 for (int j = 0; j < sizeof(VuoUrl_reservedCharacters); ++j)
359 if (c == VuoUrl_reservedCharacters[j])
360 {
361 escapedUrl[outIndex++] = '%';
362 escapedUrl[outIndex++] = hexCharSet[c >> 4];
363 escapedUrl[outIndex++] = hexCharSet[c & 0x0f];
364 foundEscape = true;
365 break;
366 }
367
368 // https://b33p.net/kosada/node/12361
369 // If it's a UTF-8 "Modifier Letter Colon", translate it back into an escaped ASCII-7 colon
370 // (since URLs can handle colons, unlike POSIX paths on macOS).
371 if (inIndex+2 < inLength)
372 if (c == 0xea && (unsigned char)path[inIndex+1] == 0x9e && (unsigned char)path[inIndex+2] == 0x89)
373 {
374 escapedUrl[outIndex++] = '%';
375 escapedUrl[outIndex++] = hexCharSet[':' >> 4];
376 escapedUrl[outIndex++] = hexCharSet[':' & 0x0f];
377 foundEscape = true;
378 inIndex += 2;
379 }
380
381 if (!foundEscape)
382 escapedUrl[outIndex++] = c;
383 }
384 escapedUrl[outIndex] = 0;
385
386 return VuoText_makeWithoutCopying(escapedUrl);
387}
388
393{
394 // Figure out how many characters we need to allocate for the escaped string.
395 unsigned long inLength = strlen(url);
396 unsigned long escapedLength = 0;
397 for (unsigned long i = 0; i < inLength; ++i)
398 {
399 if ((unsigned char)url[i] > 0x7f)
400 escapedLength += 2; // Expanding 1 character to "%xx"
401 ++escapedLength;
402 }
403
404 // Escape the string.
405 char *escapedUrl = (char *)malloc(escapedLength + 1);
406 unsigned long outIndex = 0;
407 const char *hexCharSet = "0123456789ABCDEF"; // Uppercase per https://tools.ietf.org/html/rfc3987#section-3.1
408 for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex)
409 {
410 unsigned char c = url[inIndex];
411 if (c > 0x7f)
412 {
413 escapedUrl[outIndex++] = '%';
414 escapedUrl[outIndex++] = hexCharSet[c >> 4];
415 escapedUrl[outIndex++] = hexCharSet[c & 0x0f];
416 }
417 else
418 escapedUrl[outIndex++] = c;
419 }
420 escapedUrl[outIndex] = 0;
421
422 return VuoText_makeWithoutCopying(escapedUrl);
423}
424
425static const char *VuoUrl_fileScheme = "file:";
426static 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 if ((flags & VuoUrlNormalize_forLaunching) && strncmp(filePath, "/Applications/", 14) == 0)
525 {
526 // If the app doesn't exist at `/Applications`, check `/System/Applications`.
527 if (access(filePath, 0) != 0)
528 {
529 const char *systemPrefix = "/System";
530 size_t systemPrefixLen = strlen(systemPrefix);
531 size_t filePathLen = strlen(filePath);
532 size_t mallocSize = systemPrefixLen + filePathLen + 1;
533 char *systemPath = (char *)malloc(mallocSize);
534 strlcpy(systemPath, systemPrefix, mallocSize);
535 strlcat(systemPath, filePath, mallocSize);
536 if (access(systemPath, 0) == 0)
537 filePath = systemPath;
538 else
539 free(systemPath);
540 }
541 }
542
543 char *realPath = realpath(filePath, NULL);
544 // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
545 VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : filePath);
546 VuoRetain(escapedPath);
547 if (realPath)
548 free(realPath);
549 if (filePath != trimmedUrl)
550 free(filePath);
551
552 size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
553 resolvedUrl = (char *)malloc(mallocSize);
554 strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
555 strlcat(resolvedUrl, escapedPath, mallocSize);
556
557 VuoRelease(escapedPath);
558 }
559
560 // Case: The url contains a user-relative (`~/…`) file path.
561 else if (VuoUrl_urlIsUserRelativeFilePath(trimmedUrl))
562 {
563 // 1. Expand the tilde into an absolute path.
564 VuoText absolutePath;
565 {
566 char *homeDir = getenv("HOME");
567 VuoText paths[2] = { homeDir, trimmedUrl+1 };
568 absolutePath = VuoText_append(paths, 2);
569 }
570 VuoLocal(absolutePath);
571
572 // 2. Try to canonicalize the absolute path, and URL-escape it.
573 char *realPath = realpath(absolutePath, NULL);
574 // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
575 VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : absolutePath);
576 VuoLocal(escapedPath);
577 if (realPath)
578 free(realPath);
579
580 // 3. Prepend the URL scheme.
581 size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
582 resolvedUrl = (char *)malloc(mallocSize);
583 strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
584 strlcat(resolvedUrl, escapedPath, mallocSize);
585 }
586
587 // Case: The url contains a web link without a protocol/scheme.
588 else if (flags & VuoUrlNormalize_assumeHttp)
589 {
590 // Prepend the URL scheme.
591 size_t mallocSize = strlen(VuoUrl_httpScheme) + strlen(trimmedUrl) + 1;
592 resolvedUrl = (char *)malloc(mallocSize);
593 strlcpy(resolvedUrl, VuoUrl_httpScheme, mallocSize);
594 strlcat(resolvedUrl, trimmedUrl, mallocSize);
595 }
596
597 // Case: The url contains a relative file path.
598 else
599 {
600 const char *currentWorkingDir = VuoGetWorkingDirectory();
601
602 // - When macOS LaunchServices launches an app, it sets the current working directory to root.
603 // - When macOS ScreenSaverEngine launches a plugin, it sets the current working directory to
604 // `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data`.
605 bool compositionIsExportedAppOrPlugin = false;
606 char *absolutePath;
607 if (strcmp(currentWorkingDir, "/") == 0
608 || strstr(currentWorkingDir, "/Library/Containers/"))
609 {
610 // If we're running as an exported app or plugin,
611 // resolve loaded resources relative to the app bundle's "Resources" directory, and
612 // resolve saved resources relative to the user's Desktop.
613
614 // Get the exported executable path.
615 char rawExecutablePath[PATH_MAX+1];
616 uint32_t size = sizeof(rawExecutablePath);
617 _NSGetExecutablePath(rawExecutablePath, &size);
618
619 char cleanedExecutablePath[PATH_MAX+1];
620 realpath(rawExecutablePath, cleanedExecutablePath);
621
622 // Derive the path of the app bundle's "Resources" directory from its executable path.
623 size_t pathSize = PATH_MAX + 1;
624 char executableDir[pathSize];
625 strlcpy(executableDir, dirname(cleanedExecutablePath), pathSize);
626
627 const char *resourcesPathFromExecutable = "/../Resources";
628 pathSize = strlen(executableDir) + strlen(resourcesPathFromExecutable) + 1;
629 char rawResourcesPath[pathSize];
630 strlcpy(rawResourcesPath, executableDir, pathSize);
631 strlcat(rawResourcesPath, resourcesPathFromExecutable, pathSize);
632
633 char cleanedResourcesPath[PATH_MAX+1];
634 realpath(rawResourcesPath, cleanedResourcesPath);
635
636 // If the "Resources" directory does not exist, we must not be dealing with an exported app after all.
637 // If it does, proceed under the assumption that we are.
638 if (access(cleanedResourcesPath, 0) == 0)
639 {
640 if (flags & VuoUrlNormalize_forSaving)
641 {
642 char *homeDir = getenv("HOME");
643 if (homeDir)
644 {
645 const char *desktop = "/Desktop/";
646 size_t mallocSize = strlen(homeDir) + strlen(desktop) + strlen(trimmedUrl) + 1;
647 absolutePath = (char *)malloc(mallocSize);
648 strlcpy(absolutePath, homeDir, mallocSize);
649 strlcat(absolutePath, desktop, mallocSize);
650 strlcat(absolutePath, trimmedUrl, mallocSize);
651 compositionIsExportedAppOrPlugin = true;
652 }
653 }
654 else
655 {
656 size_t mallocSize = strlen(cleanedResourcesPath) + strlen("/") + strlen(trimmedUrl) + 1;
657 absolutePath = (char *)malloc(mallocSize);
658 strlcpy(absolutePath, cleanedResourcesPath, mallocSize);
659 strlcat(absolutePath, "/", mallocSize);
660 strlcat(absolutePath, trimmedUrl, mallocSize);
661 compositionIsExportedAppOrPlugin = true;
662 }
663 }
664 }
665
666 // If we are not working with an exported app, resolve resources relative to the current working directory.
667 if (!compositionIsExportedAppOrPlugin)
668 {
669 size_t mallocSize = strlen(currentWorkingDir) + strlen("/") + strlen(trimmedUrl) + 1;
670 absolutePath = (char *)malloc(mallocSize);
671 strlcpy(absolutePath, currentWorkingDir, mallocSize);
672 strlcat(absolutePath, "/", mallocSize);
673 strlcat(absolutePath, trimmedUrl, mallocSize);
674 }
675
676 // If we're looking for an app, and it isn't found in the exported app or current working directory,
677 // try the standard applications folders.
678 if ((flags & VuoUrlNormalize_forLaunching) && access(absolutePath, 0) != 0)
679 {
680 // Check `~/Applications`.
681 char *homeDir = getenv("HOME");
682 if (homeDir)
683 {
684 const char *applicationsPrefix = "/Applications/";
685 size_t homeDirLen = strlen(homeDir);
686 size_t applicationsPrefixLen = strlen(applicationsPrefix);
687 size_t trimmedUrlLen = strlen(trimmedUrl);
688 size_t mallocSize = homeDirLen + applicationsPrefixLen + trimmedUrlLen + 1;
689 char *path = (char *)malloc(mallocSize);
690 strlcpy(path, homeDir, mallocSize);
691 strlcat(path, applicationsPrefix, mallocSize);
692 strlcat(path, trimmedUrl, mallocSize);
693 if (access(path, 0) == 0)
694 {
695 free(absolutePath);
696 absolutePath = path;
697 }
698 else
699 free(path);
700 }
701
702 // Check `/Applications`.
703 {
704 const char *applicationsPrefix = "/Applications/";
705 size_t applicationsPrefixLen = strlen(applicationsPrefix);
706 size_t trimmedUrlLen = strlen(trimmedUrl);
707 size_t mallocSize = applicationsPrefixLen + trimmedUrlLen + 1;
708 char *path = (char *)malloc(mallocSize);
709 strlcpy(path, applicationsPrefix, mallocSize);
710 strlcat(path, trimmedUrl, mallocSize);
711 if (access(path, 0) == 0)
712 {
713 free(absolutePath);
714 absolutePath = path;
715 }
716 else
717 free(path);
718 }
719
720 // Check `/System/Applications`.
721 {
722 const char *applicationsPrefix = "/System/Applications/";
723 size_t applicationsPrefixLen = strlen(applicationsPrefix);
724 size_t trimmedUrlLen = strlen(trimmedUrl);
725 size_t mallocSize = applicationsPrefixLen + trimmedUrlLen + 1;
726 char *path = (char *)malloc(mallocSize);
727 strlcpy(path, applicationsPrefix, mallocSize);
728 strlcat(path, trimmedUrl, mallocSize);
729 if (access(path, 0) == 0)
730 {
731 free(absolutePath);
732 absolutePath = path;
733 }
734 else
735 free(path);
736 }
737 }
738
739
740 char *realPath = realpath(absolutePath, NULL);
741 // If realpath() fails, it's probably because the path doesn't exist, so just use the non-realpath()ed path.
742 VuoText escapedPath = VuoUrl_escapePosixPath(realPath ? realPath : absolutePath);
743 VuoLocal(escapedPath);
744 if (realPath)
745 free(realPath);
746 free(absolutePath);
747
748 // Prepend the URL scheme.
749 size_t mallocSize = strlen(VuoUrl_fileScheme) + strlen(escapedPath) + 1;
750 resolvedUrl = (char *)malloc(mallocSize);
751 strlcpy(resolvedUrl, VuoUrl_fileScheme, mallocSize);
752 strlcat(resolvedUrl, escapedPath, mallocSize);
753 }
754
755 VuoRelease(trimmedUrl);
756
757 // Remove trailing slash, if any.
758 size_t lastIndex = strlen(resolvedUrl) - 1;
759 if (resolvedUrl[lastIndex] == '/')
760 resolvedUrl[lastIndex] = 0;
761
762 VuoText resolvedUrlVT = VuoText_makeWithoutCopying(resolvedUrl);
763 VuoRetain(resolvedUrlVT);
764
765 VuoText escapedUrl = VuoUrl_escapeUTF8(resolvedUrlVT);
766 VuoRelease(resolvedUrlVT);
767 return escapedUrl;
768}
769
776{
777 unsigned long inLength = strlen(url);
778 char *unescapedUrl = (char *)malloc(inLength + 1);
779 unsigned long outIndex = 0;
780 for (unsigned long inIndex = 0; inIndex < inLength; ++inIndex, ++outIndex)
781 {
782 char c = url[inIndex];
783 if (c == '%')
784 {
785 if (inIndex + 2 >= inLength)
786 break;
787 char highNibbleASCII = url[++inIndex];
788 char lowNibbleASCII = url[++inIndex];
789 unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
790 }
791 else
792 unescapedUrl[outIndex] = c;
793 }
794 unescapedUrl[outIndex] = 0;
795
796 return VuoText_makeWithoutCopying(unescapedUrl);
797}
798
805{
806 if (!url)
807 return NULL;
808
809 unsigned long fileSchemeLength = strlen(VuoUrl_fileScheme);
810 if (strncmp(url, VuoUrl_fileScheme, fileSchemeLength) != 0)
811 return NULL;
812
813 // https://tools.ietf.org/html/rfc8089#appendix-B
814 if (strncmp(url + fileSchemeLength, "//", 2) == 0)
815 fileSchemeLength += 2;
816
817 // Unescape the string.
818 unsigned long inLength = strlen(url);
819 // Make room for Unicode colons.
820 char *unescapedUrl = (char *)malloc(inLength*3 + 1);
821 unsigned long outIndex = 0;
822 for (unsigned long inIndex = fileSchemeLength; inIndex < inLength; ++inIndex, ++outIndex)
823 {
824 char c = url[inIndex];
825 if (c == '%')
826 {
827 char highNibbleASCII = url[++inIndex];
828 char lowNibbleASCII = url[++inIndex];
829 unescapedUrl[outIndex] = (VuoInteger_makeFromHexByte(highNibbleASCII) << 4) + VuoInteger_makeFromHexByte(lowNibbleASCII);
830 }
831 else
832 unescapedUrl[outIndex] = c;
833
834 // https://b33p.net/kosada/node/12361
835 // macOS presents colons as forward-slashes (https://developer.apple.com/library/mac/qa/qa1392/).
836 // To avoid confusion with dates, change ASCII-7 colon to UTF-8 "Modifier Letter Colon"
837 // (which looks visually identical to ASCII-7 colon).
838 if (unescapedUrl[outIndex] == ':')
839 {
840 unescapedUrl[outIndex++] = 0xea;
841 unescapedUrl[outIndex++] = 0x9e;
842 unescapedUrl[outIndex] = 0x89;
843 }
844 }
845 unescapedUrl[outIndex] = 0;
846
847 return VuoText_makeWithoutCopying(unescapedUrl);
848}
849
859bool VuoUrl_isBundle(const VuoUrl url)
860{
861 if (VuoText_isEmpty(url))
862 return false;
863
864 {
865 VuoText path = VuoUrl_getPosixPath(url);
866 if (!path)
867 return false;
868 VuoRetain(path);
869 VuoRelease(path);
870 }
871
872 CFStringRef urlCFS = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
873 CFURLRef cfurl = CFURLCreateWithString(NULL, urlCFS, NULL);
874 CFRelease(urlCFS);
875 if (!cfurl)
876 {
877 VUserLog("Error: Couldn't check '%s': Invalid URL.", url);
878 return false;
879 }
880
881 CFTypeRef info = NULL;
882 CFErrorRef error = NULL;
883 bool ret = CFURLCopyResourcePropertyForKey(cfurl, kCFURLIsPackageKey, &info, &error);
884 CFRelease(cfurl);
885 if (!ret)
886 {
887 CFStringRef errorCFS = CFErrorCopyDescription(error);
888 CFRelease(error);
889 VuoText errorText = VuoText_makeFromCFString(errorCFS);
890 CFRelease(errorCFS);
891 VuoRetain(errorText);
892 VUserLog("Error: Couldn't check '%s': %s", url, errorText);
893 VuoRelease(errorText);
894 return false;
895 }
896 return info == kCFBooleanTrue;
897}
898
902VuoUrl VuoUrl_appendFileExtension(const char *filename, struct json_object *validExtensions)
903{
904 char* fileSuffix = strrchr(filename, '.');
905 char* curExtension = fileSuffix != NULL ? strdup(fileSuffix+1) : NULL;
906
907 if(curExtension != NULL)
908 for(char *p = &curExtension[0]; *p; p++) *p = tolower(*p);
909
910 // if the string already has one of the valid file extension suffixes, return.
911 for (int i = 0; i < json_object_array_length(validExtensions); ++i)
912 {
913 if (curExtension != NULL && strcmp(curExtension, json_object_get_string(json_object_array_get_idx(validExtensions, i))) == 0)
914 {
915 free(curExtension);
916 return VuoText_make(filename);
917 }
918 }
919
920 free(curExtension);
921
922 const char *chosenExtension = json_object_get_string(json_object_array_get_idx(validExtensions, 0));
923 size_t buf_size = strlen(filename) + strlen(chosenExtension) + 2;
924 char* newfilepath = (char*)malloc(buf_size * sizeof(char));
925 snprintf(newfilepath, buf_size, "%s.%s", filename, chosenExtension);
926
927 return VuoText_makeWithoutCopying(newfilepath);
928}