2011-03-30 12:46:40 +00:00
|
|
|
/*
|
|
|
|
* This is a URL parser, written to parse "Common Internet Scheme" URL
|
|
|
|
* syntax as described in RFC1738 and updated by RFC2396. Only absolute URLs
|
|
|
|
* are supported, using "server-based" naming authorities in the schemes.
|
|
|
|
* Support for literal IPv6 addresses is included, per RFC2732.
|
|
|
|
*
|
|
|
|
* Current "known" schemes: http, ftp, file.
|
|
|
|
*
|
|
|
|
* We can do all the parsing operations without Runes since URLs are
|
|
|
|
* defined to be composed of US-ASCII printable characters.
|
|
|
|
* See RFC1738, RFC2396.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <u.h>
|
|
|
|
#include <libc.h>
|
|
|
|
#include <ctype.h>
|
|
|
|
#include <regexp.h>
|
|
|
|
#include <plumb.h>
|
|
|
|
#include <thread.h>
|
|
|
|
#include <fcall.h>
|
|
|
|
#include <9p.h>
|
|
|
|
#include "dat.h"
|
|
|
|
#include "fns.h"
|
|
|
|
|
|
|
|
int urldebug;
|
|
|
|
|
|
|
|
/* If set, relative paths with leading ".." segments will have them trimmed */
|
|
|
|
#define RemoveExtraRelDotDots 0
|
|
|
|
#define ExpandCurrentDocUrls 1
|
|
|
|
|
|
|
|
static char*
|
|
|
|
schemestrtab[] =
|
|
|
|
{
|
|
|
|
nil,
|
|
|
|
"http",
|
|
|
|
"https",
|
|
|
|
"ftp",
|
|
|
|
"file",
|
|
|
|
};
|
|
|
|
|
|
|
|
static int
|
|
|
|
ischeme(char *s)
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
|
|
|
|
for(i=0; i<nelem(schemestrtab); i++)
|
|
|
|
if(schemestrtab[i] && strcmp(s, schemestrtab[i])==0)
|
|
|
|
return i;
|
|
|
|
return USunknown;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* URI splitting regexp is from RFC2396, Appendix B:
|
|
|
|
* ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
|
|
|
|
* 12 3 4 5 6 7 8 9
|
|
|
|
*
|
|
|
|
* Example: "http://www.ics.uci.edu/pub/ietf/uri/#Related"
|
|
|
|
* $2 = scheme "http"
|
|
|
|
* $4 = authority "www.ics.uci.edu"
|
|
|
|
* $5 = path "/pub/ietf/uri/"
|
|
|
|
* $7 = query <undefined>
|
|
|
|
* $9 = fragment "Related"
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
* RFC2396, Sec 3.1, contains:
|
|
|
|
*
|
|
|
|
* Scheme names consist of a sequence of characters beginning with a
|
|
|
|
* lower case letter and followed by any combination of lower case
|
|
|
|
* letters, digits, plus ("+"), period ("."), or hyphen ("-"). For
|
|
|
|
* resiliency, programs interpreting URI should treat upper case letters
|
|
|
|
* as equivalent to lower case in scheme names (e.g., allow "HTTP" as
|
|
|
|
* well as "http").
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
* For server-based naming authorities (RFC2396 Sec 3.2.2):
|
|
|
|
* server = [ [ userinfo "@" ] hostport ]
|
|
|
|
* userinfo = *( unreserved | escaped |
|
|
|
|
* ";" | ":" | "&" | "=" | "+" | "$" | "," )
|
|
|
|
* hostport = host [ ":" port ]
|
|
|
|
* host = hostname | IPv4address
|
|
|
|
* hostname = *( domainlabel "." ) toplabel [ "." ]
|
|
|
|
* domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
|
|
|
|
* toplabel = alpha | alpha *( alphanum | "-" ) alphanum
|
|
|
|
* IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit
|
|
|
|
* port = *digit
|
|
|
|
*
|
|
|
|
* The host is a domain name of a network host, or its IPv4 address as a
|
|
|
|
* set of four decimal digit groups separated by ".". Literal IPv6
|
|
|
|
* addresses are not supported.
|
|
|
|
*
|
|
|
|
* Note that literal IPv6 address support is outlined in RFC2732:
|
|
|
|
* host = hostname | IPv4address | IPv6reference
|
|
|
|
* ipv6reference = "[" IPv6address "]" (RFC2373)
|
|
|
|
*
|
|
|
|
* Since hostnames and numbers will have to be resolved by the OS anyway,
|
|
|
|
* we don't have to parse them too pedantically (counting '.'s, checking
|
|
|
|
* for well-formed literal IP addresses, etc.).
|
|
|
|
*
|
|
|
|
* In FTP/file paths, we reject most ";param"s and querys. In HTTP paths,
|
|
|
|
* we just pass them through.
|
|
|
|
*
|
|
|
|
* Instead of letting a "path" be 0-or-more characters as RFC2396 suggests,
|
|
|
|
* we'll say it's 1-or-more characters, 0-or-1 times. This way, an absent
|
|
|
|
* path yields a nil substring match, instead of an empty one.
|
|
|
|
*
|
|
|
|
* We're more restrictive than RFC2396 indicates with "userinfo" strings,
|
|
|
|
* insisting they have the form "[user[:password]]". This may need to
|
|
|
|
* change at some point, however.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/* RE character-class components -- these go in brackets */
|
|
|
|
#define PUNCT "\\-_.!~*'()"
|
|
|
|
#define ALNUM "a-zA-Z0-9"
|
|
|
|
#define HEX "0-9a-fA-F"
|
|
|
|
#define UNRES ALNUM PUNCT
|
|
|
|
|
|
|
|
/* RE components; _N => has N parenthesized subexpressions when expanded */
|
2011-10-06 03:14:59 +00:00
|
|
|
#define USERINFO_2 "([" UNRES ";:&=+$,]|(%[" HEX "][" HEX "]))"
|
2011-03-30 12:46:40 +00:00
|
|
|
|
|
|
|
typedef struct Retab Retab;
|
|
|
|
struct Retab
|
|
|
|
{
|
|
|
|
char *str;
|
|
|
|
Reprog *prog;
|
|
|
|
int size;
|
|
|
|
int ind[5];
|
|
|
|
};
|
|
|
|
|
|
|
|
enum
|
|
|
|
{
|
|
|
|
REsplit = 0,
|
|
|
|
REscheme,
|
|
|
|
REauthority,
|
|
|
|
REhost,
|
|
|
|
REuserinfo,
|
|
|
|
REftppath,
|
|
|
|
|
|
|
|
MaxResub= 20,
|
|
|
|
};
|
|
|
|
|
|
|
|
Retab retab[] = /* view in constant width Font */
|
|
|
|
{
|
|
|
|
[REsplit]
|
|
|
|
"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]+)?(\\?([^#]*))?(#(.*))?$", nil, 0,
|
|
|
|
/* |-scheme-| |-auth.-| |path--| |query| |--|frag */
|
|
|
|
{ 2, 4, 5, 7, 9},
|
|
|
|
|
|
|
|
[REscheme]
|
|
|
|
"^[a-z][a-z0-9+-.]*$", nil, 0,
|
|
|
|
{ 0, },
|
|
|
|
|
|
|
|
[REauthority]
|
|
|
|
"^(((" USERINFO_2 "*)@)?(((\\[[^\\]@]+\\])|([^:\\[@]+))(:([0-9]*))?)?)?$", nil, 0,
|
|
|
|
/* |----user info-----| |--------host----------------| |-port-| */
|
|
|
|
{ 3, 7, 11, },
|
|
|
|
|
|
|
|
[REhost]
|
|
|
|
"^(([a-zA-Z0-9\\-.]+)|(\\[([a-fA-F0-9.:]+)\\]))$", nil, 0,
|
|
|
|
/* |--regular host--| |-IPv6 literal-| */
|
|
|
|
{ 2, 4, },
|
|
|
|
|
|
|
|
[REuserinfo]
|
|
|
|
"^(([^:]*)(:([^:]*))?)$", nil, 0,
|
|
|
|
/* |user-| |pass-| */
|
|
|
|
{ 2, 4, },
|
|
|
|
|
|
|
|
[REftppath]
|
|
|
|
"^(.+)(;[tT][yY][pP][eE]=([aAiIdD]))?$", nil, 0,
|
|
|
|
/*|--|-path |ftptype-| */
|
|
|
|
{ 1, 3, },
|
|
|
|
};
|
|
|
|
|
|
|
|
static int
|
|
|
|
countleftparen(char *s)
|
|
|
|
{
|
|
|
|
int n;
|
|
|
|
|
|
|
|
n = 0;
|
|
|
|
for(; *s; s++)
|
|
|
|
if(*s == '(')
|
|
|
|
n++;
|
|
|
|
return n;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
initurl(void)
|
|
|
|
{
|
|
|
|
int i, j;
|
|
|
|
|
|
|
|
for(i=0; i<nelem(retab); i++){
|
|
|
|
retab[i].prog = regcomp(retab[i].str);
|
|
|
|
if(retab[i].prog == nil)
|
|
|
|
sysfatal("recomp(%s): %r", retab[i].str);
|
|
|
|
retab[i].size = countleftparen(retab[i].str)+1;
|
|
|
|
for(j=0; j<nelem(retab[i].ind); j++)
|
|
|
|
if(retab[i].ind[j] >= retab[i].size)
|
|
|
|
sysfatal("bad index in regexp table: retab[%d].ind[%d] = %d >= %d",
|
|
|
|
i, j, retab[i].ind[j], retab[i].size);
|
|
|
|
if(MaxResub < retab[i].size)
|
|
|
|
sysfatal("MaxResub too small: %d < %d", MaxResub, retab[i].size);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
typedef struct SplitUrl SplitUrl;
|
|
|
|
struct SplitUrl
|
|
|
|
{
|
|
|
|
struct {
|
|
|
|
char *s;
|
|
|
|
char *e;
|
|
|
|
} url, scheme, authority, path, query, fragment;
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Implements the algorithm in RFC2396 sec 5.2 step 6.
|
|
|
|
* Returns number of chars written, excluding NUL terminator.
|
|
|
|
* dest is known to be >= strlen(base)+rel_len.
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
merge_relative_path(char *base, char *rel_st, int rel_len, char *dest)
|
|
|
|
{
|
|
|
|
char *s, *p, *e, *pdest;
|
|
|
|
|
|
|
|
pdest = dest;
|
|
|
|
|
|
|
|
/* 6a: start with base, discard last segment */
|
|
|
|
if(base && base[0]){
|
|
|
|
/* Empty paths don't match in our scheme; 'base' should be nil */
|
|
|
|
assert(base[0] == '/');
|
|
|
|
e = strrchr(base, '/');
|
|
|
|
e++;
|
|
|
|
memmove(pdest, base, e-base);
|
|
|
|
pdest += e-base;
|
|
|
|
}else{
|
|
|
|
/* Artistic license on my part */
|
|
|
|
*pdest++ = '/';
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 6b: append relative component */
|
|
|
|
if(rel_st){
|
|
|
|
memmove(pdest, rel_st, rel_len);
|
|
|
|
pdest += rel_len;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 6c: remove any occurrences of "./" as a complete segment */
|
|
|
|
s = dest;
|
|
|
|
*pdest = '\0';
|
|
|
|
while(e = strstr(s, "./")){
|
|
|
|
if((e == dest) || (*(e-1) == '/')){
|
|
|
|
memmove(e, e+2, pdest+1-(e+2)); /* +1 for NUL */
|
|
|
|
pdest -= 2;
|
|
|
|
}else
|
|
|
|
s = e+1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 6d: remove a trailing "." as a complete segment */
|
|
|
|
if(pdest>dest && *(pdest-1)=='.' &&
|
|
|
|
(pdest==dest+1 || *(pdest-2)=='/'))
|
|
|
|
*--pdest = '\0';
|
|
|
|
|
|
|
|
/* 6e: remove occurences of "seg/../", where seg != "..", left->right */
|
|
|
|
s = dest+1;
|
|
|
|
while(e = strstr(s, "/../")){
|
|
|
|
p = e - 1;
|
|
|
|
while(p >= dest && *p != '/')
|
|
|
|
p--;
|
|
|
|
if(memcmp(p, "/../", 4) != 0){
|
|
|
|
memmove(p+1, e+4, pdest+1-(e+4));
|
|
|
|
pdest -= (e+4) - (p+1);
|
|
|
|
}else
|
|
|
|
s = e+1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 6f: remove a trailing "seg/..", where seg isn't ".." */
|
|
|
|
if(pdest-3 > dest && memcmp(pdest-3, "/..", 3)==0){
|
|
|
|
p = pdest-3 - 1;
|
|
|
|
while(p >= dest && *p != '/')
|
|
|
|
p--;
|
|
|
|
if(memcmp(p, "/../", 4) != 0){
|
|
|
|
pdest = p+1;
|
|
|
|
*pdest = '\0';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 6g: leading ".." segments are errors -- we'll just blat them out. */
|
|
|
|
if(RemoveExtraRelDotDots){
|
|
|
|
p = dest;
|
|
|
|
if (p[0] == '/')
|
|
|
|
p++;
|
|
|
|
s = p;
|
|
|
|
while(s[0]=='.' && s[1]=='.' && (s[2]==0 || s[2]=='/'))
|
|
|
|
s += 3;
|
|
|
|
if(s > p){
|
|
|
|
memmove(p, s, pdest+1-s);
|
|
|
|
pdest -= s-p;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
USED(pdest);
|
|
|
|
|
|
|
|
if(urldebug)
|
|
|
|
fprint(2, "merge_relative_path: '%s' + '%.*s' -> '%s'\n", base, rel_len,
|
|
|
|
rel_st, dest);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* See RFC2396 sec 5.2 for info on resolving relative URIs to absolute form.
|
|
|
|
*
|
|
|
|
* If successful, this just ends up freeing and replacing "u->url".
|
|
|
|
*/
|
|
|
|
static int
|
|
|
|
resolve_relative(SplitUrl *su, Url *base, Url *u)
|
|
|
|
{
|
|
|
|
char *url, *path;
|
|
|
|
char *purl, *ppath;
|
|
|
|
int currentdoc, ulen, plen;
|
|
|
|
|
|
|
|
if(base == nil){
|
|
|
|
werrstr("relative URI given without base");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if(base->scheme == nil){
|
|
|
|
werrstr("relative URI given with no scheme");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if(base->ischeme == USunknown){
|
|
|
|
werrstr("relative URI given with unknown scheme");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if(base->ischeme == UScurrent){
|
|
|
|
werrstr("relative URI given with incomplete base");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
assert(su->scheme.s == nil);
|
|
|
|
|
|
|
|
/* Sec 5.2 step 2 */
|
|
|
|
currentdoc = 0;
|
|
|
|
if(su->path.s==nil && su->scheme.s==nil && su->authority.s==nil && su->query.s==nil){
|
|
|
|
/* Reference is to current document */
|
|
|
|
if(urldebug)
|
|
|
|
fprint(2, "url %s is relative to current document\n", u->url);
|
|
|
|
u->ischeme = UScurrent;
|
|
|
|
if(!ExpandCurrentDocUrls)
|
|
|
|
return 0;
|
|
|
|
currentdoc = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Over-estimate the maximum lengths, for allocation purposes */
|
|
|
|
/* (constants are for separators) */
|
|
|
|
plen = 1;
|
|
|
|
if(base->path)
|
|
|
|
plen += strlen(base->path);
|
|
|
|
if(su->path.s)
|
|
|
|
plen += 1 + (su->path.e - su->path.s);
|
|
|
|
|
|
|
|
ulen = 0;
|
|
|
|
ulen += strlen(base->scheme) + 1;
|
|
|
|
if(su->authority.s)
|
|
|
|
ulen += 2 + (su->authority.e - su->authority.s);
|
|
|
|
else
|
|
|
|
ulen += 2 + ((base->authority) ? strlen(base->authority) : 0);
|
|
|
|
ulen += plen;
|
|
|
|
if(su->query.s)
|
|
|
|
ulen += 1 + (su->query.e - su->query.s);
|
|
|
|
else if(currentdoc && base->query)
|
|
|
|
ulen += 1 + strlen(base->query);
|
|
|
|
if(su->fragment.s)
|
|
|
|
ulen += 1 + (su->fragment.e - su->fragment.s);
|
|
|
|
else if(currentdoc && base->fragment)
|
|
|
|
ulen += 1 + strlen(base->fragment);
|
|
|
|
url = emalloc(ulen+1);
|
|
|
|
path = emalloc(plen+1);
|
|
|
|
|
|
|
|
url[0] = '\0';
|
|
|
|
purl = url;
|
|
|
|
path[0] = '\0';
|
|
|
|
ppath = path;
|
|
|
|
|
|
|
|
if(su->authority.s || (su->path.s && (su->path.s[0] == '/'))){
|
|
|
|
/* Is a "network-path" or "absolute-path"; don't merge with base path */
|
|
|
|
/* Sec 5.2 steps 4,5 */
|
|
|
|
if(su->path.s){
|
|
|
|
memmove(ppath, su->path.s, su->path.e - su->path.s);
|
|
|
|
ppath += su->path.e - su->path.s;
|
|
|
|
*ppath = '\0';
|
|
|
|
}
|
|
|
|
}else if(currentdoc){
|
|
|
|
/* Is a current-doc reference; just copy the path from the base URL */
|
|
|
|
if(base->path){
|
|
|
|
strcpy(ppath, base->path);
|
|
|
|
ppath += strlen(ppath);
|
|
|
|
}
|
|
|
|
USED(ppath);
|
|
|
|
}else{
|
|
|
|
/* Is a relative-path reference; we have to merge it */
|
|
|
|
/* Sec 5.2 step 6 */
|
|
|
|
merge_relative_path(base->path,
|
|
|
|
su->path.s, su->path.e - su->path.s, ppath);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Build new URL from pieces, inheriting from base where needed */
|
|
|
|
strcpy(purl, base->scheme);
|
|
|
|
purl += strlen(purl);
|
|
|
|
*purl++ = ':';
|
|
|
|
if(su->authority.s){
|
|
|
|
strcpy(purl, "//");
|
|
|
|
purl += strlen(purl);
|
|
|
|
memmove(purl, su->authority.s, su->authority.e - su->authority.s);
|
|
|
|
purl += su->authority.e - su->authority.s;
|
|
|
|
}else if(base->authority){
|
|
|
|
strcpy(purl, "//");
|
|
|
|
purl += strlen(purl);
|
|
|
|
strcpy(purl, base->authority);
|
|
|
|
purl += strlen(purl);
|
|
|
|
}
|
|
|
|
assert((path[0] == '\0') || (path[0] == '/'));
|
|
|
|
strcpy(purl, path);
|
|
|
|
purl += strlen(purl);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The query and fragment are not inherited from the base,
|
|
|
|
* except in case of "current document" URLs, which inherit any query
|
|
|
|
* and may inherit the fragment.
|
|
|
|
*/
|
|
|
|
if(su->query.s){
|
|
|
|
*purl++ = '?';
|
|
|
|
memmove(purl, su->query.s, su->query.e - su->query.s);
|
|
|
|
purl += su->query.e - su->query.s;
|
|
|
|
}else if(currentdoc && base->query){
|
|
|
|
*purl++ = '?';
|
|
|
|
strcpy(purl, base->query);
|
|
|
|
purl += strlen(purl);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(su->fragment.s){
|
|
|
|
*purl++ = '#';
|
|
|
|
memmove(purl, su->query.s, su->query.e - su->query.s);
|
|
|
|
purl += su->fragment.e - su->fragment.s;
|
|
|
|
}else if(currentdoc && base->fragment){
|
|
|
|
*purl++ = '#';
|
|
|
|
strcpy(purl, base->fragment);
|
|
|
|
purl += strlen(purl);
|
|
|
|
}
|
|
|
|
USED(purl);
|
|
|
|
|
|
|
|
if(urldebug)
|
|
|
|
fprint(2, "resolve_relative: '%s' + '%s' -> '%s'\n", base->url, u->url, url);
|
|
|
|
free(u->url);
|
|
|
|
u->url = url;
|
|
|
|
free(path);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
regx(Reprog *prog, char *s, Resub *m, int nm)
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
|
|
|
|
if(s == nil)
|
|
|
|
s = m[0].sp; /* why is this necessary? */
|
|
|
|
|
|
|
|
i = regexec(prog, s, m, nm);
|
|
|
|
/*
|
|
|
|
if(i >= 0)
|
|
|
|
for(j=0; j<nm; j++)
|
|
|
|
fprint(2, "match%d: %.*s\n", j, utfnlen(m[j].sp, m[j].ep-m[j].sp), m[j].sp);
|
|
|
|
*/
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
ismatch(int i, char *s, char *desc)
|
|
|
|
{
|
|
|
|
Resub m[1];
|
|
|
|
|
|
|
|
m[0].sp = m[0].ep = nil;
|
|
|
|
if(!regx(retab[i].prog, s, m, 1)){
|
|
|
|
werrstr("malformed %s: %q", desc, s);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
spliturl(char *url, SplitUrl *su)
|
|
|
|
{
|
|
|
|
Resub m[MaxResub];
|
|
|
|
Retab *t;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Newlines are not valid in a URI, but regexp(2) treats them specially
|
|
|
|
* so it's best to make sure there are none before proceeding.
|
|
|
|
*/
|
|
|
|
if(strchr(url, '\n')){
|
|
|
|
werrstr("newline in URI");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
m[0].sp = m[0].ep = nil;
|
|
|
|
t = &retab[REsplit];
|
|
|
|
if(!regx(t->prog, url, m, t->size)){
|
|
|
|
werrstr("malformed URI: %q", url);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
su->url.s = m[0].sp;
|
|
|
|
su->url.e = m[0].ep;
|
|
|
|
su->scheme.s = m[t->ind[0]].sp;
|
|
|
|
su->scheme.e = m[t->ind[0]].ep;
|
|
|
|
su->authority.s = m[t->ind[1]].sp;
|
|
|
|
su->authority.e = m[t->ind[1]].ep;
|
|
|
|
su->path.s = m[t->ind[2]].sp;
|
|
|
|
su->path.e = m[t->ind[2]].ep;
|
|
|
|
su->query.s = m[t->ind[3]].sp;
|
|
|
|
su->query.e = m[t->ind[3]].ep;
|
|
|
|
su->fragment.s = m[t->ind[4]].sp;
|
|
|
|
su->fragment.e = m[t->ind[4]].ep;
|
|
|
|
|
|
|
|
if(urldebug)
|
|
|
|
fprint(2, "split url %s into %.*q %.*q %.*q %.*q %.*q %.*q\n",
|
|
|
|
url,
|
|
|
|
su->url.s ? utfnlen(su->url.s, su->url.e-su->url.s) : 10, su->url.s ? su->url.s : "",
|
|
|
|
su->scheme.s ? utfnlen(su->scheme.s, su->scheme.e-su->scheme.s) : 10, su->scheme.s ? su->scheme.s : "",
|
|
|
|
su->authority.s ? utfnlen(su->authority.s, su->authority.e-su->authority.s) : 10, su->authority.s ? su->authority.s : "",
|
|
|
|
su->path.s ? utfnlen(su->path.s, su->path.e-su->path.s) : 10, su->path.s ? su->path.s : "",
|
|
|
|
su->query.s ? utfnlen(su->query.s, su->query.e-su->query.s) : 10, su->query.s ? su->query.s : "",
|
|
|
|
su->fragment.s ? utfnlen(su->fragment.s, su->fragment.e-su->fragment.s) : 10, su->fragment.s ? su->fragment.s : "");
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
parse_scheme(SplitUrl *su, Url *u)
|
|
|
|
{
|
|
|
|
if(su->scheme.s == nil){
|
|
|
|
werrstr("missing scheme");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
u->scheme = estredup(su->scheme.s, su->scheme.e);
|
|
|
|
strlower(u->scheme);
|
|
|
|
|
|
|
|
if(!ismatch(REscheme, u->scheme, "scheme"))
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
u->ischeme = ischeme(u->scheme);
|
|
|
|
if(urldebug)
|
|
|
|
fprint(2, "parse_scheme %s => %d\n", u->scheme, u->ischeme);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
parse_unknown_part(SplitUrl *su, Url *u)
|
|
|
|
{
|
|
|
|
char *s, *e;
|
|
|
|
|
|
|
|
assert(u->ischeme == USunknown);
|
|
|
|
assert(su->scheme.e[0] == ':');
|
|
|
|
|
|
|
|
s = su->scheme.e+1;
|
|
|
|
if(su->fragment.s){
|
|
|
|
e = su->fragment.s-1;
|
|
|
|
assert(*e == '#');
|
|
|
|
}else
|
|
|
|
e = s+strlen(s);
|
|
|
|
|
|
|
|
u->schemedata = estredup(s, e);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
parse_userinfo(char *s, char *e, Url *u)
|
|
|
|
{
|
|
|
|
Resub m[MaxResub];
|
|
|
|
Retab *t;
|
|
|
|
|
|
|
|
m[0].sp = s;
|
|
|
|
m[0].ep = e;
|
|
|
|
t = &retab[REuserinfo];
|
|
|
|
if(!regx(t->prog, nil, m, t->size)){
|
|
|
|
werrstr("malformed userinfo: %.*q", utfnlen(s, e-s), s);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if(m[t->ind[0]].sp)
|
|
|
|
u->user = estredup(m[t->ind[0]].sp, m[t->ind[0]].ep);
|
|
|
|
if(m[t->ind[1]].sp)
|
|
|
|
u->user = estredup(m[t->ind[1]].sp, m[t->ind[1]].ep);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
parse_host(char *s, char *e, Url *u)
|
|
|
|
{
|
|
|
|
Resub m[MaxResub];
|
|
|
|
Retab *t;
|
|
|
|
|
|
|
|
m[0].sp = s;
|
|
|
|
m[0].ep = e;
|
|
|
|
t = &retab[REhost];
|
|
|
|
if(!regx(t->prog, nil, m, t->size)){
|
|
|
|
werrstr("malformed host: %.*q", utfnlen(s, e-s), s);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
assert(m[t->ind[0]].sp || m[t->ind[1]].sp);
|
|
|
|
|
|
|
|
if(m[t->ind[0]].sp) /* regular */
|
|
|
|
u->host = estredup(m[t->ind[0]].sp, m[t->ind[0]].ep);
|
|
|
|
else
|
|
|
|
u->host = estredup(m[t->ind[1]].sp, m[t->ind[1]].ep);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
parse_authority(SplitUrl *su, Url *u)
|
|
|
|
{
|
|
|
|
Resub m[MaxResub];
|
|
|
|
Retab *t;
|
|
|
|
char *host;
|
|
|
|
char *userinfo;
|
|
|
|
|
|
|
|
if(su->authority.s == nil)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
u->authority = estredup(su->authority.s, su->authority.e);
|
|
|
|
m[0].sp = m[0].ep = nil;
|
|
|
|
t = &retab[REauthority];
|
|
|
|
if(!regx(t->prog, u->authority, m, t->size)){
|
|
|
|
werrstr("malformed authority: %q", u->authority);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(m[t->ind[0]].sp)
|
|
|
|
if(parse_userinfo(m[t->ind[0]].sp, m[t->ind[0]].ep, u) < 0)
|
|
|
|
return -1;
|
|
|
|
if(m[t->ind[1]].sp)
|
|
|
|
if(parse_host(m[t->ind[1]].sp, m[t->ind[1]].ep, u) < 0)
|
|
|
|
return -1;
|
|
|
|
if(m[t->ind[2]].sp)
|
|
|
|
u->port = estredup(m[t->ind[2]].sp, m[t->ind[2]].ep);
|
|
|
|
|
|
|
|
|
|
|
|
if(urldebug > 0){
|
|
|
|
userinfo = estredup(m[t->ind[0]].sp, m[t->ind[0]].ep);
|
|
|
|
host = estredup(m[t->ind[1]].sp, m[t->ind[1]].ep);
|
|
|
|
fprint(2, "port: %q, authority %q\n", u->port, u->authority);
|
|
|
|
fprint(2, "host %q, userinfo %q\n", host, userinfo);
|
|
|
|
free(host);
|
|
|
|
free(userinfo);
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
parse_abspath(SplitUrl *su, Url *u)
|
|
|
|
{
|
2011-10-06 03:14:59 +00:00
|
|
|
char *s;
|
|
|
|
|
2011-03-30 12:46:40 +00:00
|
|
|
if(su->path.s == nil)
|
|
|
|
return 0;
|
2011-10-06 03:14:59 +00:00
|
|
|
s = estredup(su->path.s, su->path.e);
|
|
|
|
u->path = unescapeurl(s, "/");
|
|
|
|
free(s);
|
2011-03-30 12:46:40 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
parse_query(SplitUrl *su, Url *u)
|
|
|
|
{
|
2011-10-06 03:14:59 +00:00
|
|
|
char *s;
|
|
|
|
|
2011-03-30 12:46:40 +00:00
|
|
|
if(su->query.s == nil)
|
|
|
|
return 0;
|
2011-10-06 03:14:59 +00:00
|
|
|
s = estredup(su->query.s, su->query.e);
|
|
|
|
u->query = unescapeurl(s, "&=");
|
|
|
|
free(s);
|
2011-03-30 12:46:40 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
parse_fragment(SplitUrl *su, Url *u)
|
|
|
|
{
|
2011-10-06 03:14:59 +00:00
|
|
|
char *s;
|
|
|
|
|
2011-03-30 12:46:40 +00:00
|
|
|
if(su->fragment.s == nil)
|
|
|
|
return 0;
|
2011-10-06 03:14:59 +00:00
|
|
|
s = estredup(su->fragment.s, su->fragment.e);
|
|
|
|
u->fragment = unescapeurl(s, "");
|
|
|
|
free(s);
|
2011-03-30 12:46:40 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
postparse_http(Url *u)
|
|
|
|
{
|
2011-10-06 03:14:59 +00:00
|
|
|
char *p, *q;
|
|
|
|
|
2011-03-30 12:46:40 +00:00
|
|
|
u->open = httpopen;
|
|
|
|
u->read = httpread;
|
|
|
|
u->close = httpclose;
|
|
|
|
|
|
|
|
if(u->authority==nil){
|
|
|
|
werrstr("missing authority (hostname, port, etc.)");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if(u->host == nil){
|
|
|
|
werrstr("missing host specification");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(u->path == nil){
|
|
|
|
u->http.page_spec = estrdup("/");
|
|
|
|
return 0;
|
|
|
|
}
|
2011-10-06 03:14:59 +00:00
|
|
|
p = escapeurl(u->path, " \"<>#%\\");
|
2011-03-30 12:46:40 +00:00
|
|
|
if(u->query){
|
2011-10-06 03:14:59 +00:00
|
|
|
q = escapeurl(u->query, " \"<>#%\\");
|
|
|
|
u->http.page_spec = emalloc(strlen(p)+1+strlen(q)+1);
|
|
|
|
strcpy(u->http.page_spec, p);
|
2011-03-30 12:46:40 +00:00
|
|
|
strcat(u->http.page_spec, "?");
|
2011-10-06 03:14:59 +00:00
|
|
|
strcat(u->http.page_spec, q);
|
|
|
|
free(q);
|
|
|
|
free(p);
|
2011-03-30 12:46:40 +00:00
|
|
|
}else
|
2011-10-06 03:14:59 +00:00
|
|
|
u->http.page_spec = p;
|
2011-03-30 12:46:40 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
postparse_ftp(Url *u)
|
|
|
|
{
|
|
|
|
Resub m[MaxResub];
|
|
|
|
Retab *t;
|
|
|
|
|
|
|
|
if(u->authority==nil){
|
|
|
|
werrstr("missing authority (hostname, port, etc.)");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if(u->query){
|
|
|
|
werrstr("unexpected \"?query\" in ftp path");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if(u->host == nil){
|
|
|
|
werrstr("missing host specification");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(u->path == nil){
|
|
|
|
u->ftp.path_spec = estrdup("/");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
m[0].sp = m[0].ep = nil;
|
|
|
|
t = &retab[REftppath];
|
|
|
|
if(!regx(t->prog, u->path, m, t->size)){
|
|
|
|
werrstr("malformed ftp path: %q", u->path);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(m[t->ind[0]].sp){
|
|
|
|
u->ftp.path_spec = estredup(m[t->ind[0]].sp, m[t->ind[0]].ep);
|
|
|
|
if(strchr(u->ftp.path_spec, ';')){
|
|
|
|
werrstr("unexpected \";param\" in ftp path");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}else
|
|
|
|
u->ftp.path_spec = estrdup("/");
|
|
|
|
|
|
|
|
if(m[t->ind[1]].sp){
|
|
|
|
u->ftp.type = estredup(m[t->ind[1]].sp, m[t->ind[1]].ep);
|
|
|
|
strlower(u->ftp.type);
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
postparse_file(Url *u)
|
|
|
|
{
|
|
|
|
if(u->user || u->passwd){
|
|
|
|
werrstr("user information not valid with file scheme");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if(u->query){
|
|
|
|
werrstr("unexpected \"?query\" in file path");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if(u->port){
|
|
|
|
werrstr("port not valid with file scheme");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if(u->path == nil){
|
|
|
|
werrstr("missing path in file scheme");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if(strchr(u->path, ';')){
|
|
|
|
werrstr("unexpected \";param\" in file path");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* "localhost" is equivalent to no host spec, we'll chose the latter */
|
|
|
|
if(u->host && cistrcmp(u->host, "localhost") == 0){
|
|
|
|
free(u->host);
|
|
|
|
u->host = nil;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int (*postparse[])(Url*) = {
|
|
|
|
nil,
|
|
|
|
postparse_http,
|
|
|
|
postparse_http,
|
|
|
|
postparse_ftp,
|
|
|
|
postparse_file,
|
|
|
|
};
|
|
|
|
|
|
|
|
Url*
|
|
|
|
parseurl(char *url, Url *base)
|
|
|
|
{
|
|
|
|
Url *u;
|
|
|
|
SplitUrl su;
|
|
|
|
|
|
|
|
if(urldebug)
|
|
|
|
fprint(2, "parseurl %s with base %s\n", url, base ? base->url : "<none>");
|
|
|
|
|
|
|
|
u = emalloc(sizeof(Url));
|
|
|
|
u->url = estrdup(url);
|
|
|
|
if(spliturl(u->url, &su) < 0){
|
|
|
|
Fail:
|
|
|
|
freeurl(u);
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* RFC2396 sec 3.1 says relative URIs are distinguished by absent scheme */
|
|
|
|
if(su.scheme.s==nil){
|
|
|
|
if(urldebug)
|
|
|
|
fprint(2, "parseurl has nil scheme\n");
|
|
|
|
if(resolve_relative(&su, base, u) < 0 || spliturl(u->url, &su) < 0)
|
|
|
|
goto Fail;
|
|
|
|
if(u->ischeme == UScurrent){
|
|
|
|
/* 'u.url' refers to current document; set fragment and return */
|
|
|
|
if(parse_fragment(&su, u) < 0)
|
|
|
|
goto Fail;
|
2011-10-06 03:14:59 +00:00
|
|
|
goto Done;
|
2011-03-30 12:46:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(parse_scheme(&su, u) < 0
|
|
|
|
|| parse_fragment(&su, u) < 0)
|
|
|
|
goto Fail;
|
|
|
|
|
|
|
|
if(u->ischeme == USunknown){
|
|
|
|
if(parse_unknown_part(&su, u) < 0)
|
|
|
|
goto Fail;
|
2011-10-06 03:14:59 +00:00
|
|
|
goto Done;
|
2011-03-30 12:46:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if(parse_query(&su, u) < 0
|
|
|
|
|| parse_authority(&su, u) < 0
|
|
|
|
|| parse_abspath(&su, u) < 0)
|
|
|
|
goto Fail;
|
|
|
|
|
|
|
|
if(u->ischeme < nelem(postparse) && postparse[u->ischeme])
|
|
|
|
if((*postparse[u->ischeme])(u) < 0)
|
|
|
|
goto Fail;
|
|
|
|
|
2011-10-06 03:14:59 +00:00
|
|
|
Done:
|
2011-03-30 12:46:40 +00:00
|
|
|
setmalloctag(u, getcallerpc(&url));
|
2011-10-06 03:14:59 +00:00
|
|
|
rewriteurl(u);
|
2011-03-30 12:46:40 +00:00
|
|
|
return u;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
freeurl(Url *u)
|
|
|
|
{
|
|
|
|
if(u == nil)
|
|
|
|
return;
|
|
|
|
free(u->url);
|
|
|
|
free(u->scheme);
|
|
|
|
free(u->schemedata);
|
|
|
|
free(u->authority);
|
|
|
|
free(u->user);
|
|
|
|
free(u->passwd);
|
|
|
|
free(u->host);
|
|
|
|
free(u->port);
|
|
|
|
free(u->path);
|
|
|
|
free(u->query);
|
|
|
|
free(u->fragment);
|
|
|
|
switch(u->ischeme){
|
|
|
|
case UShttp:
|
|
|
|
free(u->http.page_spec);
|
|
|
|
break;
|
|
|
|
case USftp:
|
|
|
|
free(u->ftp.path_spec);
|
|
|
|
free(u->ftp.type);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
free(u);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
rewriteurl(Url *u)
|
|
|
|
{
|
|
|
|
char *s;
|
|
|
|
|
|
|
|
if(u->schemedata)
|
|
|
|
s = estrmanydup(u->scheme, ":", u->schemedata, nil);
|
|
|
|
else
|
|
|
|
s = estrmanydup(u->scheme, "://",
|
|
|
|
u->user ? u->user : "",
|
|
|
|
u->passwd ? ":" : "", u->passwd ? u->passwd : "",
|
|
|
|
u->user ? "@" : "", u->host ? u->host : "",
|
|
|
|
u->port ? ":" : "", u->port ? u->port : "",
|
|
|
|
u->path,
|
|
|
|
u->query ? "?" : "", u->query ? u->query : "",
|
|
|
|
u->fragment ? "#" : "", u->fragment ? u->fragment : "",
|
|
|
|
nil);
|
|
|
|
free(u->url);
|
|
|
|
u->url = s;
|
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
seturlquery(Url *u, char *query)
|
|
|
|
{
|
|
|
|
if(query == nil){
|
|
|
|
free(u->query);
|
|
|
|
u->query = nil;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
free(u->query);
|
2011-10-06 03:14:59 +00:00
|
|
|
u->query = unescapeurl(query, "&=");
|
2011-03-30 12:46:40 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
dupp(char **p)
|
|
|
|
{
|
|
|
|
if(*p)
|
|
|
|
*p = estrdup(*p);
|
|
|
|
}
|
|
|
|
|
|
|
|
Url*
|
|
|
|
copyurl(Url *u)
|
|
|
|
{
|
|
|
|
Url *v;
|
|
|
|
|
|
|
|
v = emalloc(sizeof(Url));
|
|
|
|
*v = *u;
|
|
|
|
dupp(&v->url);
|
|
|
|
dupp(&v->scheme);
|
|
|
|
dupp(&v->schemedata);
|
|
|
|
dupp(&v->authority);
|
|
|
|
dupp(&v->user);
|
|
|
|
dupp(&v->passwd);
|
|
|
|
dupp(&v->host);
|
|
|
|
dupp(&v->port);
|
|
|
|
dupp(&v->path);
|
|
|
|
dupp(&v->query);
|
|
|
|
dupp(&v->fragment);
|
|
|
|
|
|
|
|
switch(v->ischeme){
|
|
|
|
case UShttp:
|
|
|
|
dupp(&v->http.page_spec);
|
|
|
|
break;
|
|
|
|
case USftp:
|
|
|
|
dupp(&v->ftp.path_spec);
|
|
|
|
dupp(&v->ftp.type);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return v;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
dhex(char c)
|
|
|
|
{
|
|
|
|
if('0' <= c && c <= '9')
|
|
|
|
return c-'0';
|
|
|
|
if('a' <= c && c <= 'f')
|
|
|
|
return c-'a'+10;
|
|
|
|
if('A' <= c && c <= 'F')
|
|
|
|
return c-'A'+10;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
char*
|
2011-10-06 03:14:59 +00:00
|
|
|
escapeurl(char *s, char *special)
|
2011-03-30 12:46:40 +00:00
|
|
|
{
|
|
|
|
int n;
|
|
|
|
char *t, *u;
|
|
|
|
static char *hex = "0123456789abcdef";
|
|
|
|
|
|
|
|
n = 0;
|
|
|
|
for(t=s; *t; t++)
|
2011-10-06 03:14:59 +00:00
|
|
|
if(*t <= 0x1F || *t >= 0x7F || strchr(special, *t))
|
2011-03-30 12:46:40 +00:00
|
|
|
n++;
|
|
|
|
u = emalloc(strlen(s)+2*n+1);
|
|
|
|
t = u;
|
|
|
|
for(; *s; s++){
|
2011-10-06 03:14:59 +00:00
|
|
|
if(s[0] == '%' && isxdigit(s[1]) && isxdigit(s[2]))
|
|
|
|
*u++ = *s;
|
|
|
|
else if(*s <= 0x1F || *s >= 0x7F || strchr(special, *s)){
|
2011-03-30 12:46:40 +00:00
|
|
|
*u++ = '%';
|
2011-10-06 03:14:59 +00:00
|
|
|
*u++ = hex[(*s>>4)&0xF];
|
|
|
|
*u++ = hex[*s&0xF];
|
2011-03-30 12:46:40 +00:00
|
|
|
}else
|
2011-10-06 03:14:59 +00:00
|
|
|
*u++ = *s;
|
2011-03-30 12:46:40 +00:00
|
|
|
}
|
|
|
|
*u = '\0';
|
|
|
|
return t;
|
|
|
|
}
|
|
|
|
|
|
|
|
char*
|
2011-10-06 03:14:59 +00:00
|
|
|
unescapeurl(char *s, char *special)
|
2011-03-30 12:46:40 +00:00
|
|
|
{
|
2011-10-06 03:14:59 +00:00
|
|
|
char *r, *w, x;
|
2011-03-30 12:46:40 +00:00
|
|
|
|
|
|
|
s = estrdup(s);
|
2011-10-06 03:14:59 +00:00
|
|
|
for(r=w=s; x = *r; r++){
|
|
|
|
if(x=='%' && isxdigit(r[1]) && isxdigit(r[2])){
|
|
|
|
x = (dhex(r[1])<<4)|dhex(r[2]);
|
|
|
|
if(x == 0 || (x > 0x1F && x < 0x7F && strchr(special, x)))
|
|
|
|
x = *r;
|
|
|
|
else
|
|
|
|
r += 2;
|
|
|
|
}
|
|
|
|
*w++ = x;
|
2011-03-30 12:46:40 +00:00
|
|
|
}
|
|
|
|
*w = '\0';
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|