<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv=Content-Type content="text/html; charset=utf8">
<title>/usr/web/sources/contrib/bedo/flickrfs.c - Plan 9 from Bell Labs</title>
<!-- THIS FILE IS AUTOMATICALLY GENERATED. -->
<!-- EDIT sources.tr INSTEAD. -->
</meta>
</head>
<body>
<p style="margin-top: 0; margin-bottom: 0.17in"></p>
<p style="line-height: 1.2em; margin-left: 1.00in; text-indent: 0.00in; margin-right: 1.00in; margin-top: 0; margin-bottom: 0; text-align: center;">
<span style="font-size: 10pt"><a href="/plan9/">Plan 9 from Bell Labs</a>&rsquo;s /usr/web/sources/contrib/bedo/flickrfs.c</span></p>
<p style="margin-top: 0; margin-bottom: 0.17in"></p>
<p style="margin-top: 0; margin-bottom: 0.17in"></p>
<center><font size=-1>
Copyright © 2009 Alcatel-Lucent.<br />
Distributed under the
<a href="/plan9/license.html">Lucent Public License version 1.02</a>.
<br />
<a href="/plan9/download.html">Download the Plan 9 distribution.</a>
</font>
</center>
<p style="margin-top: 0; margin-bottom: 0.17in"></p>
<table width="100%" cellspacing=0 border=0><tr><td align="center">
<table cellspacing=0 cellpadding=5 bgcolor="#eeeeff"><tr><td align="left">
<pre>
<!-- END HEADER -->
#include&lt;u.h&gt;
#include&lt;libc.h&gt;
#include&lt;mp.h&gt;
#include&lt;libsec.h&gt;
#include&lt;regexp.h&gt;
#include&lt;bio.h&gt;
#include&lt;auth.h&gt;
#include&lt;fcall.h&gt;
#include&lt;thread.h&gt;
#include&lt;9p.h&gt;

const char KEY[] = "a1b693d302635eb916d330aebd0bd5c8";
const char SECRET[] = "36eb40a37bf10381";
const char BOUNDARY[] = "thisismyboundarytherearemanylikeitbutthisoneismine";
char *token;

/* Webfs */
char *webmtpt = "/mnt/web";
int ctlfd, conn;

int
webclone(int *c)
{
       char buf[128];
       int n, fd;

       snprint(buf, sizeof buf, "%s/clone", webmtpt);
       if((fd = open(buf, ORDWR)) &lt; 0)
               sysfatal("couldn't open %s: %r", buf);
       if((n = read(fd, buf, sizeof buf-1)) &lt; 0)
               sysfatal("reading clone: %r");
       if(n == 0)
               sysfatal("short read on clone");
       buf[n] = '\0';
       *c = atoi(buf);

       return fd;
}

/* Formatters for URL and MD5 digest encoding */
#define ALPHANUM(x) ((x) &gt;= 'a' &amp;&amp; (x) &lt;= 'z' || \
       (x) &gt;= 'A' &amp;&amp; (x) &lt;= 'Z' || \
       (x) &gt;= '0' &amp;&amp; (x) &lt;= '9' || \
       (x) == '_' || (x) == '.' || (x) == '-')
#pragma varargck type "U" char*

static int
urlfmt(Fmt *fmt)
{
       char buf[1024];
       char *p, *q;


       for(p = va_arg(fmt-&gt;args, char*), q = buf; *p; p++)
               if(ALPHANUM(*p))
                       *q++ = *p;
               else
                       q += sprint(q, "%%%X", (uchar)*p);
       *q = '\0';

       return fmtstrcpy(fmt, buf);
}

#pragma varargck type "M" uchar*

static int
digestfmt(Fmt *fmt)
{
       char buf[MD5dlen*2+1];
       uchar *p;
       int i;

       p = va_arg(fmt-&gt;args, uchar*);
       for(i=0; i&lt;MD5dlen; i++)
               sprint(buf+2*i, "%.2ux", p[i]);
       return fmtstrcpy(fmt, buf);
}


/* Flickr API requests */
typedef struct{
       char *name;
       char *value;
} Parameter;

typedef struct{
       char url[256];
       uint nparam;
       Parameter params[16];
} Request;

Request fr;

int
pcmp(Parameter *a, Parameter *b)
{
       int c = strcmp(a-&gt;name, b-&gt;name);
       if(c != 0) return c;

       return strcmp(a-&gt;value, b-&gt;value);
}

void
sortreq(Request *r)
{
       qsort(r-&gt;params, r-&gt;nparam, sizeof(Parameter), (int(*)(void *, void *))pcmp);
}

void
add(Request *r, char *name, char *value)
{
       r-&gt;params[r-&gt;nparam].name = estrdup9p(name);
       r-&gt;params[r-&gt;nparam++].value = estrdup9p(value);
}

void
reset(Request *r)
{
       uint i;

       for(i = 0; i &lt; r-&gt;nparam; i++){
               free(r-&gt;params[i].name);
               free(r-&gt;params[i].value);
       }
       r-&gt;nparam = 0;
       strcpy(r-&gt;url, "http://flickr.com/services/rest/");
}

void
sign(Request *r)
{
       uchar digest[MD5dlen];
       char buffer[1024];
       uint len, i;

       sortreq(r);
       len = snprint(buffer, sizeof(buffer), "%s", SECRET);
       for(i = 0; i &lt; r-&gt;nparam; i++)
               len += snprint(buffer + len, sizeof(buffer) - len,
                       "%s%s", r-&gt;params[i].name, r-&gt;params[i].value);

       md5((uchar *)buffer, strlen(buffer), digest, nil);
       snprint(buffer, sizeof buffer, "%M", digest);
       add(r, "api_sig", buffer);
}

void
auth(Request *r)
{
       add(r, "auth_token", token);
       add(r, "api_key", KEY);
       sign(r);
}

/* Flickr unique photo ids */
typedef struct{
       char *id; /* if nil then it's a unuploaded buffer not a reference */
       union{
               struct{char *farm, *secret, *server;};
               struct{uchar *buf; ulong sz;};
       };
} Pid;

/* Makes a get via webfs given a request */
Biobuf *
get(Request *r)
{
       char buf[2056], *ptr;
       int i, n;
       Biobuf *fp;


       /* Compile url */
       ptr = buf + snprint(buf, sizeof buf, "url %s", r-&gt;url);
       for(i = 0; i &lt; r-&gt;nparam; i++)
               ptr += snprint(ptr, sizeof buf + buf - ptr, "%c%U=%U", i == 0 ? '?':'&amp;',
                       r-&gt;params[i].name, r-&gt;params[i].value);

       if(write(ctlfd, buf, n = strlen(buf)) != n)
               sysfatal("get: write: %r");

       /* Response */
       snprint(buf, sizeof buf, "%s/%d/body", webmtpt, conn);
       if((fp = Bopen(buf, OREAD)) == nil)
               sysfatal("get: couldn't open body: %r");

       return fp;
}

/* Posts a photo to flickr */
Biobuf *
post(Request *r, Pid *image)
{
       char buf[2056];
       int i, n;
       Biobuf *fp;

       /* our own webfs connection */
       int myconn, myctl;
       myctl = webclone(&amp;myconn);

       /* Compile url */
       snprint(buf, sizeof buf, "url %s", r-&gt;url);
       if(write(myctl, buf, n = strlen(buf)) != n)
               sysfatal("post: write: %r");
       snprint(buf, sizeof buf, "contenttype multipart/form-data; boundary=%s", BOUNDARY);
       if(write(myctl, buf, n = strlen(buf)) != n)
               sysfatal("post: write: %r");

       /* Open postbody */
       snprint(buf, sizeof buf, "%s/%d/postbody", webmtpt, myconn);
       if((fp = Bopen(buf, OWRITE)) == nil)
               sysfatal("post: opening postbody: %r");

       /* Post parameters */
       for(i = 0; i &lt; r-&gt;nparam; i++){
               Bprint(fp, "--%s\r\n", BOUNDARY);
               Bprint(fp, "Content-disposition: form-data; name=\"%s\"\r\n\r\n", r-&gt;params[i].name);
               Bprint(fp, "%s\r\n", r-&gt;params[i].value);
       }

       /* Now the image itself */
       Bprint(fp, "--%s\r\n", BOUNDARY);
       Bprint(fp, "Content-disposition: form-data; name=\"photo\"; filename=\"photo.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n");
       Bwrite(fp, image-&gt;buf, image-&gt;sz);
       Bprint(fp, "\r\n--%s\r\n", BOUNDARY);
       Bterm(fp);

       /* Response */
       snprint(buf, sizeof buf, "%s/%d/body", webmtpt, myconn);
       if((fp = Bopen(buf, OREAD)) == nil)
               sysfatal("post: opening body: %r");

       close(myctl);
       return fp;
}

/* Dumps a request to stdout instead of webfs */
int
dump(Request *r)
{
       uint i;

       print("%s", r-&gt;url);
       for(i = 0; i &lt; r-&gt;nparam; i++)
               print("%c%s=%s", i == 0?'?':'&amp;', r-&gt;params[i].name,
                       r-&gt;params[i].value);
       print("\n");

       return 0;
}

/* XML shallow parsing */
struct{
       char frob[128];
       char token[128];
       char pages[16];
       char desc[1024];
       char id[32];
       char title[1024];
       char farm[128];
       char secret[128];
       char server[128];
} Parsed;

typedef void (*parser)(char *);
void
parse(Biobuf *body, uint n, ...)
{
       char *line;
       uint i;
       parser p;
       va_list parsers;

       memset(&amp;Parsed, 0, sizeof Parsed);
       while(line = Brdstr(body, '\n', 1)){
               /*if(n == 0)
                       fprint(2, "fparse: %s\n", line);*/
               va_start(parsers, n);
               for(i = 0; i &lt; n; i++){
                       p = va_arg(parsers, parser);
                       p(line);
               }
               va_end(parsers);
               free(line);
       }

       Bterm(body);
}

int
parseregex(char *line, Reprog *rx, uint ndest, ...)
{
       Resub *match;
       va_list dests;
       char *dest;
       uint i;

       ndest++;
       match = emalloc9p(sizeof(*match) * ndest);

       match[0].sp = match[0].ep = 0;
       if(regexec(rx, line, match, ndest) != 1)
               return -1;

       va_start(dests, ndest);
       for(i = 1; i &lt; ndest; i++){
               dest = va_arg(dests, char*);
               strncpy(dest, match[i].sp, match[i].ep - match[i].sp);
               dest[match[i].ep - match[i].sp] = '\0';
       }
       va_end(dests);

       free(match);

       return 0;
}

void
parsephoto(char *line)
{
       static Reprog *rx = nil;
       static Reprog *trx = nil;

       if(rx == nil &amp;&amp; !(rx = regcomp("&lt;photo[ \t].*id=\"([^\"]+)\".*secret=\"([^\"]+)\".*server=\"([^\"]+)\".*farm=\"([^\"]+)\".*&gt;")))
               sysfatal("parsephoto: couldn't compile rx");
       if(trx == nil &amp;&amp; !(trx = regcomp("title=\"([^\"]+)\".*/&gt;")))
               sysfatal("parsephoto: couldn't compile trx");

       if(!parseregex(line, rx, 4, Parsed.id, Parsed.secret, Parsed.server, Parsed.farm))
               parseregex(line, trx, 1, Parsed.title);
}

void
parsefrob(char *line)
{
       static Reprog *rx = nil;

       if(rx == nil &amp;&amp; !(rx = regcomp("&lt;frob&gt;(.*)&lt;/frob&gt;")))
               sysfatal("getfrob: couldn't compile rx");

       parseregex(line, rx, 1, Parsed.frob);
}

void
parsetoken(char *line)
{
       static Reprog *rx = nil;

       if(rx == nil &amp;&amp; !(rx = regcomp("&lt;token&gt;(.*)&lt;/token&gt;")))
               sysfatal("getfrob: couldn't compile rx");

       parseregex(line, rx, 1, Parsed.token);
}

void
parsedesc(char *line)
{
       static Reprog *rx = nil;

       if(rx == nil &amp;&amp; !(rx = regcomp("&lt;description&gt;(.*)&lt;/description&gt;")))
               sysfatal("getfrob: couldn't compile rx");

       parseregex(line, rx, 1, Parsed.desc);
}

void
parseid(char *line)
{
       static Reprog *rx = nil;

       if(rx == nil &amp;&amp; !(rx = regcomp("&lt;photoid&gt;(.*)&lt;/photoid&gt;")))
               sysfatal("getfrob: couldn't compile rx");

       parseregex(line, rx, 1, Parsed.id);
}

void
parsepages(char *line)
{
       static Reprog *rx = nil;

       if(rx == nil &amp;&amp; !(rx = regcomp("pages=\"([^\"]+)\"")))
               sysfatal("parsesearch: couldn't compile rx");

       parseregex(line, rx, 1, Parsed.pages);

}

/* Cache for reading images */
struct{
       char url[1024];
       ulong sz;
       ulong n;
       uchar *data;
} Filecache;

uchar *
cache(char *url, long *n)
{
       Biobuf *fp;
       long r;

       /* If already cached */
       if(!strncmp(url, Filecache.url, sizeof Filecache.url) &amp;&amp; Filecache.n &gt; 0){
               *n = Filecache.n;
               return Filecache.data;
       }

       /* Load file from flickr */
       Filecache.n = *n = 0;
       strncpy(Filecache.url, url, sizeof Filecache.url);
       reset(&amp;fr);
       strncpy(fr.url, url, sizeof fr.url);
       if((fp = get(&amp;fr)) == nil)
               return nil;
       do{
               if(Filecache.sz &lt;= Filecache.n){
                       Filecache.sz = (Filecache.sz + 1) &lt;&lt; 1;
                       Filecache.data = erealloc9p(Filecache.data, sizeof(*Filecache.data) * Filecache.sz);
               }
               r = Bread(fp, Filecache.data + Filecache.n,
                       Filecache.sz - Filecache.n);
               Filecache.n += r;
       }while(r &gt; 0);
       Bterm(fp);

       *n = Filecache.n;
       return Filecache.data;
}

/* 9p */
void
fsread(Req *r)
{
       Pid *p;
       char buf[1024];
       void *c;
       long n;

       p = (Pid*)r-&gt;fid-&gt;file-&gt;aux;
       if(!p){
               respond(r, "empty aux");
               return;
       }

       if(p-&gt;id == nil){
               respond(r, "no associated id");
               return;
       }

       snprint(buf, sizeof buf, "http://farm%s.staticflickr.com/%s/%s_%s_b.jpg", p-&gt;farm, p-&gt;server, p-&gt;id,  p-&gt;secret);
       c = cache(buf, &amp;n);
       if(n == 0){
               respond(r, "cache error");
               return;
       }
       readbuf(r, c, n);
       respond(r, nil);
}

void
fswstat(Req *r)
{
       char *p, *q;
       Pid *aux;

       aux = (Pid*)r-&gt;fid-&gt;file-&gt;aux;

       /* Name changes */
       if(r-&gt;d.name &amp;&amp; r-&gt;d.name[0]){
               /* Check extension */
               p = strrchr(r-&gt;d.name, '.');
               q = strrchr(r-&gt;fid-&gt;file-&gt;Dir.name, '.');
               if(p == nil || strcmp(p, q)){
                       respond(r, "cannot change extension");
                       return;
               }
               *p = '\0';

               /* Get description */
               reset(&amp;fr);
               add(&amp;fr, "method", "flickr.photos.getInfo");
               add(&amp;fr, "photo_id", aux-&gt;id);
               auth(&amp;fr);
               parse(get(&amp;fr), 1, parsedesc);

               /* Update flickr */
               reset(&amp;fr);
               add(&amp;fr, "method", "flickr.photos.setMeta");
               add(&amp;fr, "photo_id", aux-&gt;id);
               add(&amp;fr, "title", r-&gt;d.name);
               add(&amp;fr, "description", Parsed.desc);
               auth(&amp;fr);
               parse(get(&amp;fr), 0);

               /* Success */
               *p = '.';
               free(r-&gt;fid-&gt;file-&gt;Dir.name);
               r-&gt;fid-&gt;file-&gt;Dir.name = estrdup9p(r-&gt;d.name);
       }

       respond(r, nil);
}

void
fsremove(Req *r)
{
       Pid *aux;

       if(r-&gt;fid-&gt;file == nil || r-&gt;fid-&gt;file-&gt;aux == nil){
               respond(r, nil);
               return;
       }
       aux = (Pid*)r-&gt;fid-&gt;file-&gt;aux;
       reset(&amp;fr);
       add(&amp;fr, "method", "flickr.photos.delete");
       add(&amp;fr, "photo_id", aux-&gt;id);
       auth(&amp;fr);
       parse(get(&amp;fr), 0);
       respond(r, nil);
}

void
fscreate(Req *r)
{
       Pid *aux;
       char *p;
       File *f;


       p = strrchr(r-&gt;ifcall.name, '.');
       if(p == nil || strcmp(p, ".jpg"))
               respond(r, "invalid filename");

       if((f = createfile(r-&gt;fid-&gt;file, r-&gt;ifcall.name, nil, 0666, nil)) == nil){
               respond(r, "couldn't create file");
               return;
       }

       aux = emalloc9p(sizeof(*aux));
       aux-&gt;id = nil;
       aux-&gt;buf = nil;
       aux-&gt;sz = 0;
       f-&gt;aux = aux;
       r-&gt;fid-&gt;file = f;
       r-&gt;ofcall.qid = f-&gt;qid;

       respond(r, nil);
}

void
fswrite(Req *r)
{
       Pid *aux;
       vlong offset;
       long count;

       aux = (Pid*) r-&gt;fid-&gt;file-&gt;aux;
       if(aux-&gt;id){
               respond(r, "replacing files not supported");
               return;
       }

       offset = r-&gt;ifcall.offset;
       count = r-&gt;ifcall.count;
       if(offset+count &gt;= aux-&gt;sz){
               aux-&gt;buf = erealloc9p(aux-&gt;buf, offset+count+1);
               aux-&gt;sz = offset+count;
       }

       memmove(aux-&gt;buf+offset, r-&gt;ifcall.data, count);
       r-&gt;ofcall.count = count;

       respond(r, nil);
}

void
fsdestroyfid(Fid *fid)
{
       Pid *aux;
       char *p;

       if(fid-&gt;file == nil)
               return;

       aux = (Pid*)fid-&gt;file-&gt;aux;
       if(aux == nil)
               return;

       if(aux-&gt;id == nil){
               /* Upload buffer to flickr */
               reset(&amp;fr);
               strcpy(fr.url, "http://api.flickr.com/services/upload/");
               p = strrchr(fid-&gt;file-&gt;name, '.');
               *p = '\0';
               add(&amp;fr, "title", fid-&gt;file-&gt;name);
               *p = '.';
               auth(&amp;fr);

               /* Parse response */
               parse(post(&amp;fr, aux), 1, parseid);
               if(Parsed.id[0] == '\0')
                       sysfatal("fsdestroyfid: bad response");
               //fprint(2, "got id: %s", Parsed.id);
               free(aux-&gt;buf);

               /* Query image to find farm/server/secret */
               reset(&amp;fr);
               add(&amp;fr, "method", "flickr.photos.getInfo");
               add(&amp;fr, "photo_id", Parsed.id);
               auth(&amp;fr);
               parse(get(&amp;fr), 1, parsephoto);

               if(Parsed.id[0] == '\0')
                       sysfatal("fsdestroyfid: getinfo failed");
               aux-&gt;id = estrdup9p(Parsed.id);
               aux-&gt;farm = estrdup9p(Parsed.farm);
               aux-&gt;server = estrdup9p(Parsed.server);
               aux-&gt;secret = estrdup9p(Parsed.secret);
       }
}

Srv fs = {
       .destroyfid=    fsdestroyfid,
       .read=  fsread,
       .write= fswrite,
       .wstat= fswstat,
       .remove=        fsremove,
       .create=        fscreate,
};


void
fsdestroyfile(File *f)
{
       Pid *aux;
       aux = (Pid*)f-&gt;aux;
       if(aux != nil){
               if(aux-&gt;id){
                       free(aux-&gt;secret);
                       free(aux-&gt;farm);
                       free(aux-&gt;id);
                       free(aux-&gt;server);
                       free(aux);
               }else
                       if(aux-&gt;sz &gt; 0)
                               free(aux-&gt;buf);
       }
}

/* Flickr searching to build file tree */
void
parsesearch(char *line)
{
       char fn[1024];
       File *f;
       Pid *aux;

       Parsed.title[0] = '\0';
       parsephoto(line);

       if(Parsed.title[0]){
               aux = emalloc9p(sizeof(*aux));
               memset(aux, 0, sizeof(*aux));
               snprint(fn, sizeof fn, "%s.jpg", Parsed.title);
               f = createfile(fs.tree-&gt;root, fn, nil, 0666, aux);
               if(f == nil){
                       fprint(2, "cannot create file: %s\n", fn);
                       free(aux);
                       return;
               }
               aux-&gt;id = estrdup9p(Parsed.id);
               aux-&gt;farm = estrdup9p(Parsed.farm);
               aux-&gt;secret = estrdup9p(Parsed.secret);
               aux-&gt;server = estrdup9p(Parsed.server);
               closefile(f);
       }

}

void
searchflickr(void)
{
       uint page = 1;
       char buf[16];

       do{
               reset(&amp;fr);
               add(&amp;fr, "method", "flickr.photos.search");
               add(&amp;fr, "user_id", "me");
               add(&amp;fr, "per_page", "500");
               snprint(buf, sizeof buf, "%d", page);
               add(&amp;fr, "page", buf);
               auth(&amp;fr);
               parse(get(&amp;fr), 2, parsesearch, parsepages);
               if(Parsed.pages[0] == '\0'){
                       fprint(2, "searchflickr: warning couldn't parse pages\n");
                       break;
               }
               page++;
       }while(page &lt; atoi(Parsed.pages));
}


void
usage(void)
{
       fprint(2, "%s: [-D] [-w webfs mtpt] [-s srvname] [-m mtpt]\n", argv0);
       exits("usage");
}

void
main(int argc, char *argv[])
{
       UserPasswd *up;
       char buf[128];
       char *mtpt;
       char *srvname;
       Qid q;

       memset(&amp;Filecache, 0, sizeof Filecache);
       mtpt = "/n/flickr";
       srvname = nil;

       ARGBEGIN{
       case 'D':
               chatty9p++;
               break;
       case 'w':
               webmtpt = EARGF(usage());
               break;
       case 'm':
               mtpt = EARGF(usage());
               break;
       case 's':
               srvname = EARGF(usage());
               break;
       case 'h':
       default:
               usage();
       }ARGEND;

       if(argc)
               usage();

       fmtinstall('M', digestfmt);
       fmtinstall('U', urlfmt);

       ctlfd = webclone(&amp;conn);

       /* Try finding a token */
       up = auth_getuserpasswd(nil, "proto=pass role=client server=flickr.com user=%s", KEY);

       /* No token */
       if(up == nil){
               /* Get a frob */
               reset(&amp;fr);
               add(&amp;fr, "method", "flickr.auth.getFrob");
               add(&amp;fr, "api_key", KEY);
               sign(&amp;fr);
               parse(get(&amp;fr), 1, parsefrob);
               if(Parsed.frob == nil)
                       sysfatal("couldn't parse frob");

               /* Authentication url */
               reset(&amp;fr);
               strcpy(fr.url, "http://flickr.com/services/auth/");
               add(&amp;fr, "api_key", KEY);
               add(&amp;fr, "perms", "delete");
               add(&amp;fr, "frob", Parsed.frob);
               sign(&amp;fr);
               print("Authenticate then press enter: ");
               dump(&amp;fr);
               read(0, buf, 1);

               /* Fetch token */
               reset(&amp;fr);
               strcpy(fr.url, "http://flickr.com/services/rest/");
               add(&amp;fr, "method", "flickr.auth.getToken");
               add(&amp;fr, "api_key", KEY);
               add(&amp;fr, "frob", Parsed.frob);
               sign(&amp;fr);
               parse(get(&amp;fr), 1, parsetoken);
               if(Parsed.token == nil)
                       sysfatal("couldn't parse token");
               print("key proto=pass role=client server=flickr.com user=%s !password=%s\n", KEY, Parsed.token);

               exits(0);
       }

       /* We got a token */
       token = up-&gt;passwd;

       /* Populate our tree */
       fs.tree = alloctree(nil, nil, DMDIR|0777, fsdestroyfile);
       q = fs.tree-&gt;root-&gt;qid;
       searchflickr();

       /* Mount ourselves */
       postmountsrv(&amp;fs, srvname, mtpt, MREPL|MCREATE);

       exits(0);
}
<!-- BEGIN TAIL -->
</pre>
</td></tr></table>
</td></tr></table>
<p style="margin-top: 0; margin-bottom: 0.17in"></p>
<p style="line-height: 1.2em; margin-left: 1.00in; text-indent: 0.00in; margin-right: 1.00in; margin-top: 0; margin-bottom: 0; text-align: center;">
<span style="font-size: 10pt"></span></p>
<p style="margin-top: 0; margin-bottom: 0.50in"></p>
<p style="margin-top: 0; margin-bottom: 0.33in"></p>
<center><table border="0"><tr>
<td valign="middle"><a href="http://www.alcatel-lucent.com/"><img border="0" src="/plan9/img/logo_ft.gif" alt="Bell Labs" />
</a></td>
<td valign="middle"><a href="http://www.opensource.org"><img border="0" alt="OSI certified" src="/plan9/img/osi-certified-60x50.gif" />
</a></td>
<td><img style="padding-right: 45px;" alt="Powered by Plan 9" src="/plan9/img/power36.gif" />
</td>
</tr></table></center>
<p style="margin-top: 0; margin-bottom: 0.17in"></p>
<center>
<span style="font-size: 10pt">(<a href="/plan9/">Return to Plan 9 Home Page</a>)</span>
</center>
<p style="margin-top: 0; margin-bottom: 0.17in"></p>
<center><font size=-1>
<span style="font-size: 10pt"><a href="http://www.lucent.com/copyright.html">Copyright</a></span>
<span style="font-size: 10pt">© 2009 Alcatel-Lucent.</span>
<span style="font-size: 10pt">All Rights Reserved.</span>
<br />
<span style="font-size: 10pt">Comments to</span>
<span style="font-size: 10pt"><a href="mailto:[email protected]">[email protected]</a>.</span>
</font></center>
</body>
</html>