url: fix out-of-bound read
[vlc.git] / src / text / url.c
1 /*****************************************************************************
2  * url.c: URL related functions
3  *****************************************************************************
4  * Copyright (C) 2006 VLC authors and VideoLAN
5  * Copyright (C) 2008-2012 Rémi Denis-Courmont
6  *
7  * This program is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU Lesser General Public License as published by
9  * the Free Software Foundation; either version 2.1 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public License
18  * along with this program; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
20  *****************************************************************************/
21
22 #ifdef HAVE_CONFIG_H
23 # include "config.h"
24 #endif
25
26 #include <errno.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <assert.h>
30 #ifdef _WIN32
31 # include <io.h>
32 #endif
33
34 #include <vlc_common.h>
35 #include <vlc_url.h>
36 #include <vlc_fs.h>
37 #include <ctype.h>
38
39 /**
40  * Decodes an encoded URI component. See also decode_URI().
41  * \return decoded string allocated on the heap, or NULL on error.
42  */
43 char *decode_URI_duplicate (const char *str)
44 {
45     char *buf = strdup (str);
46     if (decode_URI (buf) == NULL)
47     {
48         free (buf);
49         buf = NULL;
50     }
51     return buf;
52 }
53
54 /**
55  * Decodes an encoded URI component in place.
56  * <b>This function does NOT decode entire URIs.</b> Instead, it decodes one
57  * component at a time (e.g. host name, directory, file name).
58  * Decoded URIs do not exist in the real world (see RFC3986 §2.4).
59  * Complete URIs are always "encoded" (or they are syntaxically invalid).
60  *
61  * Note that URI encoding is different from Javascript escaping. Especially,
62  * white spaces and Unicode non-ASCII code points are encoded differently.
63  *
64  * \param str nul-terminated URI component to decode
65  * \return str on success, NULL if it was not properly encoded
66  */
67 char *decode_URI (char *str)
68 {
69     char *in = str, *out = str;
70     if (in == NULL)
71         return NULL;
72
73     char c;
74     while ((c = *(in++)) != '\0')
75     {
76         if (c == '%')
77         {
78             char hex[3];
79
80             if (!(hex[0] = *(in++)) || !(hex[1] = *(in++)))
81                 return NULL;
82             hex[2] = '\0';
83             *(out++) = strtoul (hex, NULL, 0x10);
84         }
85         else
86             *(out++) = c;
87     }
88     *out = '\0';
89     return str;
90 }
91
92 static inline bool isurisafe (int c)
93 {
94     /* These are the _unreserved_ URI characters (RFC3986 §2.3) */
95     return ((unsigned char)(c - 'a') < 26)
96         || ((unsigned char)(c - 'A') < 26)
97         || ((unsigned char)(c - '0') < 10)
98         || (strchr ("-._~", c) != NULL);
99 }
100
101 static char *encode_URI_bytes (const char *str, size_t *restrict lenp)
102 {
103     char *buf = malloc (3 * *lenp + 1);
104     if (unlikely(buf == NULL))
105         return NULL;
106
107     char *out = buf;
108     for (size_t i = 0; i < *lenp; i++)
109     {
110         static const char hex[16] = "0123456789ABCDEF";
111         unsigned char c = str[i];
112
113         if (isurisafe (c))
114             *(out++) = c;
115         /* This is URI encoding, not HTTP forms:
116          * Space is encoded as '%20', not '+'. */
117         else
118         {
119             *(out++) = '%';
120             *(out++) = hex[c >> 4];
121             *(out++) = hex[c & 0xf];
122         }
123     }
124
125     *lenp = out - buf;
126     out = realloc (buf, *lenp + 1);
127     return likely(out != NULL) ? out : buf;
128 }
129
130 /**
131  * Encodes a URI component (RFC3986 §2).
132  *
133  * @param str nul-terminated UTF-8 representation of the component.
134  * @note Obviously, a URI containing nul bytes cannot be passed.
135  * @return encoded string (must be free()'d), or NULL for ENOMEM.
136  */
137 char *encode_URI_component (const char *str)
138 {
139     size_t len = strlen (str);
140     char *ret = encode_URI_bytes (str, &len);
141     if (likely(ret != NULL))
142         ret[len] = '\0';
143     return ret;
144 }
145
146 /**
147  * Builds a URL representation from a local file path.
148  * @param path path to convert (or URI to copy)
149  * @param scheme URI scheme to use (default is auto: "file", "fd" or "smb")
150  * @return a nul-terminated URI string (use free() to release it),
151  * or NULL in case of error (errno will be set accordingly)
152  */
153 char *vlc_path2uri (const char *path, const char *scheme)
154 {
155     if (path == NULL)
156     {
157         errno = EINVAL;
158         return NULL;
159     }
160     if (scheme == NULL && !strcmp (path, "-"))
161         return strdup ("fd://0"); // standard input
162     /* Note: VLC cannot handle URI schemes without double slash after the
163      * scheme name (such as mailto: or news:). */
164
165     char *buf;
166
167 #ifdef __OS2__
168     char p[strlen (path) + 1];
169
170     for (buf = p; *path; buf++, path++)
171         *buf = (*path == '/') ? DIR_SEP_CHAR : *path;
172     *buf = '\0';
173
174     path = p;
175 #endif
176
177 #if defined( _WIN32 ) || defined( __OS2__ )
178     /* Drive letter */
179     if (isalpha ((unsigned char)path[0]) && (path[1] == ':'))
180     {
181         if (asprintf (&buf, "%s:///%c:", scheme ? scheme : "file",
182                       path[0]) == -1)
183             buf = NULL;
184         path += 2;
185 # warning Drive letter-relative path not implemented!
186         if (path[0] != DIR_SEP_CHAR)
187         {
188             errno = ENOTSUP;
189             return NULL;
190         }
191     }
192     else
193 #endif
194     if (!strncmp (path, "\\\\", 2))
195     {   /* Windows UNC paths */
196 #if !defined( _WIN32 ) && !defined( __OS2__ )
197         if (scheme != NULL)
198         {
199             errno = ENOTSUP;
200             return NULL; /* remote files not supported */
201         }
202
203         /* \\host\share\path -> smb://host/share/path */
204         if (strchr (path + 2, '\\') != NULL)
205         {   /* Convert backslashes to slashes */
206             char *dup = strdup (path);
207             if (dup == NULL)
208                 return NULL;
209             for (size_t i = 2; dup[i]; i++)
210                 if (dup[i] == '\\')
211                     dup[i] = DIR_SEP_CHAR;
212
213             char *ret = vlc_path2uri (dup, scheme);
214             free (dup);
215             return ret;
216         }
217 # define SMB_SCHEME "smb"
218 #else
219         /* \\host\share\path -> file://host/share/path */
220 # define SMB_SCHEME "file"
221 #endif
222         size_t hostlen = strcspn (path + 2, DIR_SEP);
223
224         buf = malloc (sizeof (SMB_SCHEME) + 3 + hostlen);
225         if (buf != NULL)
226             snprintf (buf, sizeof (SMB_SCHEME) + 3 + hostlen,
227                       SMB_SCHEME"://%s", path + 2);
228         path += 2 + hostlen;
229
230         if (path[0] == '\0')
231             return buf; /* Hostname without path */
232     }
233     else
234     if (path[0] != DIR_SEP_CHAR)
235     {   /* Relative path: prepend the current working directory */
236         char *cwd, *ret;
237
238         if ((cwd = vlc_getcwd ()) == NULL)
239             return NULL;
240         if (asprintf (&buf, "%s"DIR_SEP"%s", cwd, path) == -1)
241             buf = NULL;
242
243         free (cwd);
244         ret = (buf != NULL) ? vlc_path2uri (buf, scheme) : NULL;
245         free (buf);
246         return ret;
247     }
248     else
249     if (asprintf (&buf, "%s://", scheme ? scheme : "file") == -1)
250         buf = NULL;
251     if (buf == NULL)
252         return NULL;
253
254     /* Absolute file path */
255     assert (path[0] == DIR_SEP_CHAR);
256     do
257     {
258         size_t len = strcspn (++path, DIR_SEP);
259         path += len;
260
261         char *component = encode_URI_bytes (path - len, &len);
262         if (unlikely(component == NULL))
263         {
264             free (buf);
265             return NULL;
266         }
267         component[len] = '\0';
268
269         char *uri;
270         int val = asprintf (&uri, "%s/%s", buf, component);
271         free (component);
272         free (buf);
273         if (unlikely(val == -1))
274             return NULL;
275         buf = uri;
276     }
277     while (*path);
278
279     return buf;
280 }
281
282 /**
283  * Tries to convert a URI to a local (UTF-8-encoded) file path.
284  * @param url URI to convert
285  * @return NULL on error, a nul-terminated string otherwise
286  * (use free() to release it)
287  */
288 char *make_path (const char *url)
289 {
290     char *ret = NULL;
291     char *end;
292
293     char *path = strstr (url, "://");
294     if (path == NULL)
295         return NULL; /* unsupported scheme or invalid syntax */
296
297     end = memchr (url, '/', path - url);
298     size_t schemelen = ((end != NULL) ? end : path) - url;
299     path += 3; /* skip "://" */
300
301     /* Remove HTML anchor if present */
302     end = strchr (path, '#');
303     if (end)
304         path = strndup (path, end - path);
305     else
306         path = strdup (path);
307     if (unlikely(path == NULL))
308         return NULL; /* boom! */
309
310     /* Decode path */
311     decode_URI (path);
312
313     if (schemelen == 4 && !strncasecmp (url, "file", 4))
314     {
315 #if !defined (_WIN32) && !defined (__OS2__)
316         /* Leading slash => local path */
317         if (*path == '/')
318             return path;
319         /* Local path disguised as a remote one */
320         if (!strncasecmp (path, "localhost/", 10))
321             return memmove (path, path + 9, strlen (path + 9) + 1);
322 #else
323         /* cannot start with a space */
324         if (*path == ' ')
325             goto out;
326         for (char *p = strchr (path, '/'); p; p = strchr (p + 1, '/'))
327             *p = '\\';
328
329         /* Leading backslash => local path */
330         if (*path == '\\')
331             return memmove (path, path + 1, strlen (path + 1) + 1);
332         /* Local path disguised as a remote one */
333         if (!strncasecmp (path, "localhost\\", 10))
334             return memmove (path, path + 10, strlen (path + 10) + 1);
335         /* UNC path */
336         if (*path && asprintf (&ret, "\\\\%s", path) == -1)
337             ret = NULL;
338 #endif
339         /* non-local path :-( */
340     }
341     else
342     if (schemelen == 2 && !strncasecmp (url, "fd", 2))
343     {
344         int fd = strtol (path, &end, 0);
345
346         if (*end)
347             goto out;
348
349 #if !defined( _WIN32 ) && !defined( __OS2__ )
350         switch (fd)
351         {
352             case 0:
353                 ret = strdup ("/dev/stdin");
354                 break;
355             case 1:
356                 ret = strdup ("/dev/stdout");
357                 break;
358             case 2:
359                 ret = strdup ("/dev/stderr");
360                 break;
361             default:
362                 if (asprintf (&ret, "/dev/fd/%d", fd) == -1)
363                     ret = NULL;
364         }
365 #else
366         /* XXX: Does this work on WinCE? */
367         if (fd < 2)
368             ret = strdup ("CON");
369         else
370             ret = NULL;
371 #endif
372     }
373
374 out:
375     free (path);
376     return ret; /* unknown scheme */
377 }
378
379 static char *vlc_idna_to_ascii (const char *);
380
381 /**
382  * Splits an URL into parts.
383  * \param url structure of URL parts [OUT]
384  * \param str nul-terminated URL string to split
385  * \param opt if non-zero, character separating paths from options,
386  *            normally the question mark
387  * \note Use vlc_UrlClean() to free associated resources
388  * \bug Errors cannot be detected.
389  * \return nothing
390  */
391 void vlc_UrlParse (vlc_url_t *restrict url, const char *str, unsigned char opt)
392 {
393     url->psz_protocol = NULL;
394     url->psz_username = NULL;
395     url->psz_password = NULL;
396     url->psz_host = NULL;
397     url->i_port = 0;
398     url->psz_path = NULL;
399     url->psz_option = NULL;
400     url->psz_buffer = NULL;
401
402     if (str == NULL)
403         return;
404
405     char *buf = strdup (str);
406     if (unlikely(buf == NULL))
407         abort ();
408     url->psz_buffer = buf;
409
410     char *cur = buf, *next;
411
412     /* URL scheme */
413     next = buf;
414     while ((*next >= 'A' && *next <= 'Z') || (*next >= 'a' && *next <= 'z')
415         || (*next >= '0' && *next <= '9') || memchr ("+-.", *next, 3) != NULL)
416         next++;
417     /* This is not strictly correct. In principles, the scheme is always
418      * present in an absolute URL and followed by a colon. Depending on the
419      * URL scheme, the two subsequent slashes are not required.
420      * VLC uses a different scheme for historical compatibility reasons - the
421      * scheme is often implicit. */
422     if (!strncmp (next, "://", 3))
423     {
424         *next = '\0';
425         next += 3;
426         url->psz_protocol = cur;
427         cur = next;
428     }
429
430     /* Path */
431     next = strchr (cur, '/');
432     if (next != NULL)
433     {
434         *next = '\0'; /* temporary nul, reset to slash later */
435         url->psz_path = next;
436         if (opt && (next = strchr (next, opt)) != NULL)
437         {
438             *(next++) = '\0';
439             url->psz_option = next;
440         }
441     }
442     /*else
443         url->psz_path = "/";*/
444
445     /* User name */
446     next = strrchr (cur, '@');
447     if (next != NULL)
448     {
449         *(next++) = '\0';
450         url->psz_username = cur;
451         cur = next;
452
453         /* Password (obsolete) */
454         next = strchr (url->psz_username, ':');
455         if (next != NULL)
456         {
457             *(next++) = '\0';
458             url->psz_password = next;
459             decode_URI (url->psz_password);
460         }
461         decode_URI (url->psz_username);
462     }
463
464     /* Host name */
465     if (*cur == '[' && (next = strrchr (cur, ']')) != NULL)
466     {   /* Try IPv6 numeral within brackets */
467         *(next++) = '\0';
468         url->psz_host = strdup (cur + 1);
469
470         if (*next == ':')
471             next++;
472         else
473             next = NULL;
474     }
475     else
476     {
477         next = strchr (cur, ':');
478         if (next != NULL)
479             *(next++) = '\0';
480
481         url->psz_host = vlc_idna_to_ascii (cur);
482     }
483
484     /* Port number */
485     if (next != NULL)
486         url->i_port = atoi (next);
487
488     if (url->psz_path != NULL)
489         *url->psz_path = '/'; /* restore leading slash */
490 }
491
492 /**
493  * Releases resources allocated by vlc_UrlParse().
494  */
495 void vlc_UrlClean (vlc_url_t *restrict url)
496 {
497     free (url->psz_host);
498     free (url->psz_buffer);
499 }
500
501 #if defined (HAVE_IDN)
502 # include <idna.h>
503 #elif defined (_WIN32)
504 # include <windows.h>
505 # include <vlc_charset.h>
506 #endif
507
508 /**
509  * Converts a UTF-8 nul-terminated IDN to nul-terminated ASCII domain name.
510  * \param idn UTF-8 Internationalized Domain Name to convert
511  * \return a heap-allocated string or NULL on error.
512  */
513 static char *vlc_idna_to_ascii (const char *idn)
514 {
515 #if defined (HAVE_IDN)
516     char *adn;
517
518     if (idna_to_ascii_8z (idn, &adn, IDNA_ALLOW_UNASSIGNED) != IDNA_SUCCESS)
519         return NULL;
520     return adn;
521
522 #elif defined (_WIN32) && (_WIN32_WINNT >= 0x0601)
523     char *ret = NULL;
524
525     wchar_t *wide = ToWide (idn);
526     if (wide == NULL)
527         return NULL;
528
529     int len = IdnToAscii (IDN_ALLOW_UNASSIGNED, wide, -1, NULL, 0);
530     if (len == 0)
531         goto error;
532
533     wchar_t *buf = malloc (sizeof (*buf) * len);
534     if (unlikely(buf == NULL))
535         goto error;
536     if (!IdnToAscii (IDN_ALLOW_UNASSIGNED, wide, -1, buf, len))
537     {
538         free (buf);
539         goto error;
540     }
541     ret = FromWide (buf);
542     free (buf);
543 error:
544     free (wide);
545     return ret;
546
547 #else
548     /* No IDN support, filter out non-ASCII domain names */
549     for (const char *p = idn; *p; p++)
550         if (((unsigned char)*p) >= 0x80)
551             return NULL;
552
553     return strdup (idn);
554
555 #endif
556 }