Plan 9 from Bell Labs’s /usr/web/sources/extra/i/iutils.c

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


#include "i.h"

typedef struct NGchanitem NGchanitem;
typedef struct ImageCache ImageCache;

// Item sent allong ngchan
struct NGchanitem {
	int			req;
	ByteSource*	bs;
	Netconn*		nc;
	Channel*		anschan;
};

// In-memory cache of CImages
struct ImageCache
{
	CImage*	imhd;		// head (LRU) of cache chain (linked through CImage.next)
	CImage*	imtl;			// tail MRU) of cache chain
	int		n;			// size of chain
	int		memused;	// current total of image mem used by cached images
	int		memlimit;		// keep memused less than this
	int		nlimit;		// keep n less than this
};

// Some constants
enum {
	NCTimeout = 100000,	// free NC slot after 100 seconds
	UBufsize = 40960,		// initial buffer size for unknown lengths
	UEBufsize = 1024,		// initial buffer size for unknown lengths, error responses
	Maxnthreads = 10,		// max allowed config.nthreads
};

Rune crlf[] = L"\r\n";
Rune  sptab[] =  L" \t";

// globals
int			bytesourceid = 0;	// for allocating bytesource ids
int			netconnid = 0;		// for allocating netconn ids
int			dbg;				// config.dbg['d']
int			warn;			// dbg || config.dbg['w']
int			dbgev;			// config.dbg['e']
int			dbgproto;			// are we debugging netget protocol?
int			aborting;			// in the process of aborting a get
Netconn**	netconns;			// array of netconns
int			nnetconns;		// current number of slots in netconns
Channel*		ngchan;			// Channel of NGchanitem
Channel*		reqanschan;		// Channel of ByteSource*
Channel*		abortanschan;		// Channel of ByteSource*
Config		config;
ResourceState	startres;
ImageCache	imcache;

// Track HTTP methods in i.h
// (upper-case, since that's required in HTTP requests)
Rune* hmeth[] = { L"GET", L"POST" };

// Track media types in charon.h
// keep in alphabetical order
Rune* mnames[] = {
	L"application/msword",
	L"application/octet-stream",
	L"application/pdf",
	L"application/postscript",
	L"application/rtf",
	L"application/vnd.framemaker",
	L"application/vnd.ms-excel",
	L"application/vnd.ms-powerpoint",
	L"application/x-unknown",
	L"audio/32kadpcm",
	L"audio/basic",
	L"image/cgm",
	L"image/g3fax",
	L"image/gif",
	L"image/ief",
	L"image/jpeg",
	L"image/png",
	L"image/tiff",
	L"image/x-bit",
	L"image/x-bit2",
	L"image/x-bitmulti",
	L"image/x-xbitmap",
	L"model/vrml",
	L"multipart/digest",
	L"multipart/mixed",
	L"text/css",
	L"text/enriched",
	L"text/html",
	L"text/javascript",
	L"text/plain",
	L"text/richtext",
	L"text/sgml",
	L"text/tab-separated-values",
	L"text/xml",
	L"video/mpeg",
	L"video/quicktime"
};

// Track Charsets in i.h
//(cf rfc1945 for other names, those we don't bother handling)
Rune* chsetnames[] = {
	L"unknown", L"us-ascii", L"iso-8859-1", L"unicode-1-1-utf-8", L"unicode-1-1"
};

// Track Netconn states in i.h
Rune* ncstatenames[] = {
	L"free", L"idle", L"connect", L"gethdr", L"getdata",
	L"done", L"err"
};

// Track major status values in i.h
Rune* hsnames[] = {
	L"none", L"information", L"ok", L"redirect", L"request error", L"server error"
};

// File extension to Media type lookup table
// (keep sorted on file extension)
static StringInt fileexttable[] = {
	{L"ai", ApplPostscript},
	{L"au", AudioBasic},
	{L"bit", ImageXBit},
	{L"bit2", ImageXBit2},
	{L"bitm", ImageXBitmulti},
	{L"eps", ApplPostscript},
	{L"gif", ImageGif},
	{L"htm", TextHtml},
	{L"html", TextHtml},
	{L"jpe", ImageJpeg},
	{L"jpeg", ImageJpeg},
	{L"jpg", ImageJpeg},
	{L"pdf", ApplPdf},
	{L"ps", ApplPostscript},
	{L"text", TextPlain},
	{L"tif", ImageTiff},
	{L"tiff", ImageTiff},
	{L"txt", TextPlain}
};
#define NFILEEXTS (sizeof(fileexttable)/sizeof(StringInt))

// Beginning of a JPEG file
static uchar jpmagic[] = {0xFF, 0xD8, 0xFF, 0xE0,
		0, 0, 'J', 'F', 'I', 'F', 0};

// Color lookup table
static StringInt color_tab[] = {
	{L"aqua", 0x00FFFF},
	{L"black", Black},
	{L"blue", Blue},
	{L"fuchsia", 0xFF00FF},
	{L"gray", 0x808080},
	{L"green", 0x008000},
	{L"lime", 0x00FF00},
	{L"maroon", 0x800000},
	{L"navy", Navy},
	{L"olive", 0x808000},
	{L"purple", 0x800080},
	{L"red", Red},
	{L"silver", 0xC0C0C0},
	{L"teal", 0x008080},
	{L"white", White},
	{L"yellow", 0xFFFF00}
};
#define NCOLORS (sizeof(color_tab)/sizeof(StringInt))

// track Error codes in i.h
static Rune* errphrases[] = {
	L"(no error)",
	L"Stopped",
	L"Unsupported scheme",
	L"Unexpected HTTP answer code",
	L"Redirect loop",
	L"Can't get file status",
	L"Read error",
	L"Write error",
	L"Unexpected end of file",
	L"Couldn't connect",
	L"FTP protocol error",
	L"FTP login failed",
	L"HTTP protocol error",
	L"Authorization failed",
	L"Unsupported image type",
	L"Not enough memory",
	L"Image encoding is bad"
};
#define NERRPHRASES (sizeof(errphrases)/sizeof(Rune*))

static StringInt 		*targetmap;
static int			targetmapsize;
static int			ntargets;
static void			runnetconn(void *arg);
static int			strhash(Rune* s);
static Transport*	gettransport(int scheme);
static Netconn*		newnetconn(int id);
static void			makefree(Netconn* nc);
static ByteSource*	newbytesource(void);
static void			imcacheinit(void);
static void			setconfig(int argc, char** argv);
static int			setopt(Rune* key, int keylen, Rune* val, int vallen);
static void			targetmapinit(void);
static int			rnhandler(void* ureg, char* note);
static void			sendprocintr(int pid);
static int			countrefgo(void);


// Initialize Chutils globals
void
iutilsinit(int argc, char** argv)
{
	startres = curresstate();

	// Setconfig needs urlinit() to have been done.
	// Rest of inits are done after setconfig, in case their
	// actions depend on configuration settings.
	// guiinit() is done in i.c.
	urlinit();
	setconfig(argc, argv);
	transportinit();
	lexinit();
	buildinit();
	layoutinit();
	imginit();
	eventinit();
	imcacheinit();
	targetmapinit();
	gcinit();
	threadnotify(rnhandler, 1);
	aborting = 0;
	nnetconns = 10;
	netconns = emallocz(sizeof(Netconn)*nnetconns);
	dbgproto = config.dbg['p'];
	dbg = config.dbg['d'];
	warn = config.dbg['w'];
	dbgev = config.dbg['e'];
	ngchan = chancreate(sizeof(NGchanitem), 0);
	reqanschan = chancreate(sizeof(ByteSource*), 0);
	abortanschan = chancreate(sizeof(ByteSource*), 0);
	if(ngchan == nil || reqanschan == nil || abortanschan == nil)
		fatalerror("can't create channels");
}

// Make a ByteSource for given request, and make sure
// that it is on the queue of some Netconn.
// If don't have a transport for the request's scheme,
// the returned bs will have err set.
ByteSource*
startreq(ReqInfo* req)
{
	ByteSource*	bs;
	ByteSource*	bsans;
	NGchanitem	ngi;

	bs = newbytesource();
	bs->req = req;
	bs->refgo = 1;
	bs->refnc = 1;
	if(config.showprogress)
		sendprogress(bs->id, Pstart, 0, req->url->url);
	ngi.req = NGstartreq;
	ngi.bs = bs;
	ngi.nc = nil;
	ngi.anschan = reqanschan;
	if(send(ngchan, &ngi) < 0)
		finish();
	if(recv(reqanschan, &bsans) < 0)
		finish();
	return bs;
}

// Wait for some ByteSource to
// have a state change that go hasn't seen yet.
// There's a race between go doing a freebs
// and netconn returning a statechange;
// fix that race by only returning bs's that
// still have refgo == 1.
ByteSource*
waitreq(void)
{
	ByteSource*	bs;
	NGchanitem	ngi;

	ngi.req = NGwaitreq;
	do {
		ngi.bs = nil;
		ngi.nc = nil;
		ngi.anschan = reqanschan;
		if(send(ngchan, &ngi) < 0)
			finish();
		if(recv(reqanschan, &bs) < 0)
			finish();
		if(bs == nil)
			return nil;
	} while(bs->refgo == 0);
	return bs;
}

// Notify netget that goproc is finished with bs.
// See waitreq comment about waitfreq/freebs race.
void
freebs(ByteSource* bs)
{
	NGchanitem	ngi;

	bs->refgo = 0;
	ngi.req = NGfreebs;
	ngi.bs = bs;
	ngi.nc = nil;
	ngi.anschan = reqanschan;
	if(send(ngchan, &ngi) < 0)
		finish();
	if(recv(reqanschan, &bs) < 0)
		finish();
}

// Tell netget to abort all gets.
// This involves sending interrupts to any runnetconns that
// may be stuck in I/O.
// All bs's will then be returned to waitreq with an "aborted"
// error, after which this function can return.
void
abortgo(void)
{
	NGchanitem	ngi;

	if(dbg)
		trace("abort go\n");

	ngi.req = NGabort;
	ngi.bs = nil;
	ngi.nc = nil;
	ngi.anschan = abortanschan;

	if(send(ngchan, &ngi) < 0)
		finish();
	if(recv(abortanschan, nil) < 0)
		finish();
}

int
forkrunnetconn(Netconn *nc, void **rncargs)
{
	int id;

	assert(nc->rnpid == 0 && nc->rntid == 0);
	if(config.nthreads > 0)
		id = proccreate(runnetconn, (void*)rncargs, STACKSIZE);
	else
		id = threadcreate(runnetconn, (void*)rncargs, STACKSIZE);
	if(id < 0)
		return -1;
	nc->rntid = id;
	return 0;
}

// This runs as a separate thread.
// It acts as a monitor to synchronize access to the Netconn data
// structures, as a dispatcher to start runnetconn's as needed to
// process work on Netconn queues, and as a notifier to let goproc
// know when any ByteSources have advanced their state.
void
netget(void* arg)
{
	int	msg;
	int	n;
	int	i;
	int	toldgo;
	ByteSource*	bs;
	ByteSource*	bsnil;
	Netconn*	nc;
	Channel*	c;
	Channel*	pendingc;
	int	waitpending;
	int	maxconn;
	Netconn*	x;
	int	minqlen;
	Rune*	sport;
	int	port;
	int	freen;
	int	scheme;
	Rune*	host;
	int	err;
	int	totlen;
	ByteSource*	sendtopending;
	NGchanitem	ngi;
	Transport*	tpt;
	int		gncs[Maxnthreads];
	void*	rncargs[2];

	USED(arg);
	waitpending = 0;
	maxconn = config.nthreads;
	if(maxconn < 1)
		maxconn = 1;
	if(maxconn > Maxnthreads)
		maxconn = Maxnthreads;
	for(n = 0; n < nnetconns; n++)
		netconns[n] = newnetconn(n);
	bsnil = nil;
	pendingc = nil;
	sendtopending = nil;
	toldgo = 0;
	while(1) {
    mainloop_start:
		if(aborting) {
			if(toldgo && countrefgo() == 0) {
				if(send(abortanschan, nil) < 0)
					finish();
				aborting = 0;
			}
		}
		if(recv(ngchan, &ngi) < 0)
			finish();
		msg = ngi.req;
		bs = ngi.bs;
		nc = ngi.nc;
		c = ngi.anschan;
		switch(msg) {
		case NGstartreq:
			// bs has req filled in, and is otherwise in its initial state.
			// Find a suitable Netconn and add bs to its queue of work,
			// then send nil along c to let goproc continue.

			if(dbgproto)
				trace("Startreq BS=%d for %U\n", bs->id, bs->req->url);
			scheme = bs->req->url->scheme;
			host = Strndup(bs->req->url->host, bs->req->url->nhost);
			err = 0;
			tpt = nil;
			if(aborting)
				err = ERRaborted;
			else if(scheme < 0 || scheme >= TRANSMAX || transports[scheme] == nil)
				err = ERRunsupscheme;
			else 
				tpt = transports[scheme];
			if(err)
				bs->err = err;
			else {
				sport = Strndup(bs->req->url->port, bs->req->url->nport);
				if(sport == nil)
					port = (tpt->defaultport)(scheme);
				else
					port = Strtol(sport, nil, 10);
				i = 0;
				freen = -1;
				for(n = 0; n < nnetconns && (i < maxconn || freen == -1); n++) {
					nc = netconns[n];
					if(nc->state == NCfree) {
						if(freen == -1)
							freen = n;
					}
					else if(!Strcmp(nc->host, host) && nc->port == port
							&& nc->scheme == scheme && i < maxconn) {
						gncs[i++] = n;
					}
				}
				if(i < maxconn) {
					//  use a new netconn for this bs
					if(freen == -1) {
						// need to make netconns bigger
						netconns = (Netconn**)erealloc(netconns, (freen + 10) * sizeof(Netconn*));
						for(i = nnetconns; i < nnetconns+10; i++)
							netconns[i] = newnetconn(i);
						freen = nnetconns;
						nnetconns += 10;
					}
					nc = netconns[freen];
					nc->rnpid = 0;
					nc->rntid = 0;
					nc->host = host;
					nc->port = port;
					nc->scheme = scheme;
					nc->qlen = 0;
					nc->ngcur = 0;
					nc->gocur = 0;
					nc->reqsent = 0;
					nc->pipeline = 0;
					nc->connected = 0;
				}
				else {
					// use existing netconn with fewest outstanding requests
					nc = netconns[gncs[0]];
					if(maxconn > 1) {
						minqlen = nc->qlen - nc->gocur;
						for(i = 1; i < maxconn; i++) {
							x = netconns[gncs[i]];
							if(x->qlen - x->gocur < minqlen) {
								nc = x;
								minqlen = x->qlen - x->gocur;
							}
						}
					}
				}
				if(nc->qlen == nc->qalloclen) {
					// need to make queue bigger
					nc->queue = (ByteSource**)erealloc(nc->queue, (nc->qalloclen + 10) * sizeof(ByteSource*));
					nc->qalloclen += 10;
				}
				nc->queue[nc->qlen++] = bs;
				bs->net = nc;
				if(dbgproto)
					trace("Chose NC=%d for BS %d, qlen=%d\n", nc->id, bs->id, nc->qlen);
				if(nc->state == NCfree || nc->state == NCidle) {
					if(nc->connected) {
						nc->state = NCgethdr;
						if(dbgproto)
							trace("NC %d: starting runnetconn in gethdr state\n", nc->id);
					}
					else {
						nc->state = NCconnect;
						if(dbgproto)
							trace("NC %d: starting runnetconn in connect state\n", nc->id);
					}
					rncargs[0] = nc;
					rncargs[1] = tpt;
					if(forkrunnetconn(nc, rncargs) < 0){
						trace("Can't start runnetconn!\n");
						return;
					}
				}
			}
			if(send(c, &bsnil) < 0)
				finish();
			break;

		case NGwaitreq:
			// goproc wants to be notified when some ByteSource
			// changes to a state that the goproc hasn't seen yet.
			// Send such a ByteSource along return channel c.

			if(dbgproto)
				trace("Waitreq\n");
			assert(!waitpending);
			if(aborting) {
				if(dbgproto)
					trace("Send <abort> to waitreq\n");
				send(c, &bsnil);
				toldgo = 1;
				goto mainloop_start;
			}
			for(n = 0; n < nnetconns; n++) {
				nc = netconns[n];
				if(nc->state == NCfree)
					continue;
				if(nc->gocur < nc->qlen) {
					bs = nc->queue[nc->gocur];
					if(!bs->refgo)
						continue;
					if(bs->err ||
					   (bs->hdr != nil && !bs->seenhdr) ||
					   (bs->edata > bs->lim) || (nc->gocur < nc->ngcur)) {
						if(dbgproto)
							trace("Send BS %d to waitreq\n", bs->id);
						if(send(c, &bs) < 0)
							finish();
						goto mainloop_start;
					}
				}
			}
			if(dbgproto)
				trace("Waitpending\n");
			waitpending = 1;
			pendingc = c;
			break;

		case NGfreebs:
			if(dbgproto)
				trace("Freebs BS=%d\n", bs->id);
			nc = bs->net;
			if(bs->refnc == 0) {
				if(nc != nil)
					nc->queue[nc->gocur] = nil;
			}
			if(nc != nil) {
				// can be nil if no transport was found
				nc->gocur++;
				if(dbgproto)
					trace("NC %d: gocur=%d, ngcur=%d, qlen=%d\n",
						nc->id, nc->gocur, nc->ngcur, nc->qlen);
				if(nc->gocur == nc->qlen && nc->ngcur == nc->qlen) {
					if(!nc->connected)
						makefree(nc);
				}
			}
			if(send(c, &bsnil) < 0)
				finish();
			break;

		case NGstatechg:
			// Some runnetconn is telling us tht it changed the
			// state of nc.  Send a nil along c to let it continue.

			if(dbgproto)
				trace("Statechg NC=%d, state=%S\n", nc->id, ncstatenames[nc->state]);
			sendtopending = nil;
			if(!aborting && waitpending && nc->gocur < nc->qlen) {
				bs = nc->queue[nc->gocur];
				if(dbgproto) {
					totlen = 0;
					if(bs->hdr != nil)
						totlen = bs->hdr->length;
					trace("BS %d: havehdr=%d seenhdr=%d edata=%d lim=%d, length=%d\n",
						 bs->id, bs->hdr != nil, bs->seenhdr, bs->edata, bs->lim, totlen);
					if(bs->err)
						trace("   err=%S\n", errphrase(bs->err));
				}
				if(bs->refgo &&
				   (bs->err ||
				   (bs->hdr != nil && !bs->seenhdr) ||
				   (bs->edata > bs->lim)))
					sendtopending = bs;
			}
			if(nc->state == NCdone || nc->state == NCerr) {
				if(dbgproto)
					trace("NC %d: runnetconn finishing\n", nc->id);
if(nc->ngcur >= nc->qlen) {
trace("NC %d has ngcur=%d, qlen=%d\n", nc->id, nc->ngcur, nc->qlen);
}
				assert(nc->ngcur < nc->qlen);
				bs = nc->queue[nc->ngcur];
				bs->refnc = 0;
				if(bs->refgo == 0)
					nc->queue[nc->ngcur] = nil;
				nc->ngcur++;
				if(dbgproto)
					trace("NC %d: ngcur=%d\n", nc->id, nc->ngcur);
				nc->state = NCidle;
				nc->rnpid = 0;
				nc->rntid = 0;
				if(dbgproto)
					trace("NC %d: idle\n", nc->id);
				if(aborting) {
					/* free ByteSources! */
					makefree(nc);
				}
				else if(nc->ngcur < nc->qlen) {
					if(nc->connected) {
						nc->state = NCgethdr;
						if(dbgproto)
							trace("NC %d: starting runnetconn in gethdr state\n", nc->id);
					}
					else {
						nc->state = NCconnect;
						if(dbgproto)
							trace("NC %d: starting runnetconn in connect state\n", nc->id);
					}
					rncargs[0] = nc;
					rncargs[1] = transports[nc->scheme];
					if(forkrunnetconn(nc, rncargs) < 0){
						trace("Can't start runnetconn!\n");
						return;
					}
				}
				else if(!nc->connected)
					makefree(nc);
			}
if(dbgproto)
	trace("Send reply to chan 0x%p\n", c);
			send(c, &bsnil);
if(dbgproto)
	trace("done sending reply to chan 0x%p\n", c);
			if(sendtopending != nil) {
				if(dbgproto)
					trace("Send BS %d to pending waitreq\n", bs->id);
				send(pendingc, &sendtopending);
				waitpending = 0;
			}
			break;
		case NGabort:
			if(dbgproto)
				trace("NGabort received\n");
			aborting = 1;
			if(waitpending) {
				if(dbgproto)
					trace("Send <abort> to pending waitreq\n");
				send(pendingc, &bsnil);
				waitpending = 0;
				toldgo = 1;
			}
			else
				toldgo = !countrefgo();
			for(n = 0; n < nnetconns; n++) {
				nc = netconns[n];
				if(nc->rntid != 0) {
					if(config.nthreads != 0)
						sendprocintr(nc->rnpid);
					/* else we can't interrupt */
				}
			}
			break;
		}
	}
}

// Return number of ByteSources in netget queues
// that have refgo
static int
countrefgo(void)
{
	int ans, i, n;
	Netconn*	nc;
	ByteSource* bs;

	ans = 0;
	for(n = 0; n < nnetconns; n++) {
		nc = netconns[n];
		if(nc->state != NCfree) {
			for(i = 0; i < nc->qlen; i++) {
				bs = nc->queue[i];
				if(bs == nil)
					continue;
				if(bs->refgo)
					ans++;
			}
		}
	}
	return ans;
}

static void
sendprocintr(int pid)
{
	int fd;
	char buf[100];

	if(dbgproto)
		trace("proc %d sending rnintr to proc %d\n", getpid(), pid);
	sprint(buf, "/proc/%d/note", pid);
	fd = open(buf, OWRITE);
	if(fd >= 0) {
		write(fd, "rnintr", 6);
		close(fd);
	}
}

static int
rnhandler(void* ureg, char* note)
{
	USED(ureg);
	if(strncmp(note, "rnintr", 6) == 0) {
		if(dbgproto)
			trace("proc %d got rnintr\n", getpid());
		return 1;
	}
	return 0;
}

// A separate proc, to handle ngcur request of transport.
// The arguments should be: {Netconn*, Transport*}.
// If config.nthreads == 0, it is run as thread rather than proc
// (for debugging).
static void
runnetconn(void* arg)
{
	Netconn*	nc;
	Transport*	tpt;
	int	totlen;
	Channel*	ach;
	ByteSource*	bs;
	ByteSource*	bsans;
	NGchanitem	ngi;
	void**	args;

	if(config.nthreads != 0)
		meminit(GRnet);
	args = (void**)arg;
	nc = (Netconn*)args[0];
	nc->rnpid = getpid();
	tpt = (Transport*)args[1];
	ach = chancreate(sizeof(ByteSource*), 0);
	assert(nc->ngcur < nc->qlen);
	bs = nc->queue[nc->ngcur];
	ngi.req = NGstatechg;
	ngi.bs = nil;
	ngi.nc = nc;
	ngi.anschan = ach;
	bs->data = nil;

	if(nc->state == NCconnect) {
		(tpt->connect)(nc, bs);
		if(bs->err || aborting)
			goto finished;
		nc->state = NCgethdr;
	}
	assert(nc->state == NCgethdr && nc->connected);
	if(config.showprogress)
		sendprogress(bs->id, Pconnected, 0, nil);

	// Write enough requests so that nc->ngcur header
	//will be next to be retrieved
	while(nc->reqsent < nc->qlen) {
		(tpt->writereq)(nc, nc->queue[nc->reqsent]);
		if(aborting)
			goto finished;
		if(nc->queue[nc->reqsent]->err) {
			if(nc->reqsent > nc->ngcur) {
				nc->queue[nc->reqsent]->err = 0;
				break;
			}
			else
				goto finished;
		}
		nc->reqsent++;
		if(nc->reqsent >= nc->ngcur + 1 || !nc->pipeline)
			break;
	}
	assert(nc->reqsent > nc->ngcur);

	// Get the header
	(tpt->gethdr)(nc, bs);
	if(bs->err || aborting)
		goto finished;
	assert(bs->hdr != nil);
	if(config.showprogress)
		sendprogress(bs->id, Phavehdr, 0, nil);

	totlen = bs->hdr->length;
	if(totlen > 0) {
		nc->state = NCgetdata;
		if(send(ngchan, &ngi) == -1)
			goto cleanup;
		if(recv(ach, &bsans) == -1)
			goto cleanup;
		bs->data = (uchar*)emalloc(totlen);
		bs->dalloclen = totlen;
		while(bs->edata < totlen) {
			(tpt->getdata)(nc, bs);
			if(bs->err || aborting)
				goto finished;
			if(config.showprogress)
				sendprogress(bs->id, Phavedata, 100*bs->edata/totlen, nil);
			if(send(ngchan, &ngi) == -1)
				goto cleanup;
			if(recv(ach, &bsans) == -1)
				goto cleanup;
		}
	}
	else if(totlen == -1) {
		// Unknown length.
		// To simplify consumer semantics, we want bs->data to
		// not change underfoot, so for now, simply accumlate
		// everything before telling consumer about a state change.
		//
		// Report progress percentage based on current totlen (wrong
		// of course, but at least shows trend)
		if(bs->hdr->code == HCOk || bs->hdr->code == HCOkNonAuthoritative)
			totlen = UBufsize;
		else
			totlen = UEBufsize;
		nc->state = NCgetdata;
		bs->hdr->length = 100000000;	// avoid BS free during following loop
		if(send(ngchan, &ngi) == -1)
			goto cleanup;
		if(recv(ach, &bsans) == -1)
			goto cleanup;
		bs->data = (uchar*)emalloc(totlen);
		bs->dalloclen = totlen;
		while(1) {
			(tpt->getdata)(nc, bs);
			if(aborting)
				goto finished;
			if(config.showprogress)
				sendprogress(bs->id, Phavedata, 100*bs->edata/totlen, nil);
			if(bs->err) {
				// assume EOF
				bs->data = (uchar*)erealloc(bs->data, bs->edata);
				bs->dalloclen = bs->edata;
				bs->err = 0;
				bs->hdr->length = bs->edata;
				nc->connected = 0;
				break;
			}
			if(bs->edata == totlen) {
				totlen *= 2;
				bs->data = (uchar*)erealloc(bs->data, totlen);
				bs->dalloclen = totlen;
			}
		}
	}
	nc->state = NCdone;
	if(config.showprogress)
		sendprogress(bs->id, Phavedata, 100, nil);

finished:
	if(aborting)
		bs->err = ERRaborted;
	if(bs->err) {
		nc->state = NCerr;
		nc->connected = 0;
		/*
		 * If we're aborting, then the event loop is stuck in abortgo
		 * waiting for us to finish.  Don't update the progress meter,
		 * since that requires having the event loop.
		 */
		if(!aborting && config.showprogress)
			sendprogress(bs->id, Perr, 0, errphrase(bs->err));
	}
	if(send(ngchan, &ngi) == -1)
		goto cleanup;
	recv(ach, &bsans);
	if(dbgproto)
		trace("runnetconn in proc %d exiting\n", getpid());
	threadexits("");

cleanup:
	if(aborting)
		goto finished;
	if(dbgproto)
		trace("runnetconn in proc %d exiting\n", getpid());
	threadexits("");
}

// Allocate a new Netconn, set its id and all other fields to "unused" state
static Netconn*
newnetconn(int id)
{
	Netconn* nc;

	nc = (Netconn*)emalloc(sizeof(Netconn));
	nc->id = id;
	nc->qalloclen = 10;
	nc->queue = (ByteSource**)emalloc(nc->qalloclen*sizeof(ByteSource*));
	makefree(nc);
	return nc;
}

// Reset the fields of nc so that it is free to service another connection to anywhere.
static void
makefree(Netconn* nc)
{
	if(dbgproto)
		trace("NC %d: free\n", nc->id);
	nc->state = NCfree;
	nc->rnpid = 0;
	nc->host = nil;
	nc->port = 0;
	nc->dfd = -1;
	nc->cfd = -1;
	nc->qlen = 0;
	nc->gocur = 0;
	nc->ngcur = 0;
	nc->reqsent = 0;
	nc->pipeline = 0;
	nc->connected = 0;
	nc->tstate = 0;
	nc->tn1 = 0;
	nc->tn2 = 0;
	nc->tbuf = nil;
	nc->tbuflen = 0;
	memset(nc->queue, 0, nc->qalloclen*sizeof(ByteSource*));
}

// Allocate a new ByteSource, and set its fields to initial state.
static ByteSource*
newbytesource(void)
{
	ByteSource* bs;

	bs = (ByteSource*)emallocz(sizeof(ByteSource));
	bs->id = bytesourceid++;
	return bs;
}

// Return an ByteSource that is completely filled, from string s.
ByteSource*
newstringbytesource(Rune* s)
{
	ByteSource* bs;
	Header* hdr;
	uchar* a;
	int n;

	n = Strlen(s);
	a = (uchar*)emalloc(n*sizeof(Rune));
	memmove(a, s, n*sizeof(Rune));
	hdr = newheader();
	hdr->code = HCOk;
	hdr->length = n;
	hdr->mtype = TextHtml;
	hdr->chset = Unicode;
	bs = newbytesource();
	bs->hdr = hdr;
	bs->data = a;
	bs->dalloclen = n*sizeof(Rune);
	bs->edata = bs->dalloclen;
	bs->refgo = 1;
	bs->seenhdr = 1;
	return bs;
}

// Allocate a new ReqInfo structure, fill it in, and return.
ReqInfo*
newreqinfo(ParsedUrl* url, int method, uchar* body, int bodylen, Rune* auth, int target)
{
	ReqInfo* r;

	r = (ReqInfo*)emalloc(sizeof(ReqInfo));
	r->url = url;
	r->method = method;
	r->body = body;
	r->bodylen = bodylen;
	r->auth = auth;
	r->target = target;
	return r;
}

// Allocate a new MaskedImage, with im for the image field.
MaskedImage*
newmaskedimage(Image* im)
{
	MaskedImage* m;

	m = (MaskedImage*)emallocz(sizeof(MaskedImage));
	m->im = im;
	return m;
}

// Allocate a new CImage, with the give src, width, and height.
// Start with a reference count of 1.
CImage*
newcimage(ParsedUrl* src, int width, int height)
{
	CImage* c;

	c = (CImage*)emalloc(sizeof(CImage));
	c->src = src;
	c->actual = nil;
	if(src != nil)
		c->imhash = strhash(src->host) + strhash(src->path);
	else
		c->imhash = 0;
	c->width = width;
	c->height = height;
	c->refcnt = 1;
	c->next = nil;
	c->mims = nil;
	c->mimslen = 0;
	return c;
}

// Decrement the refcnt, and free when it reaches 0.
void
freecimage(CImage* c)
{
	int i;
	MaskedImage* m;

	if(--c->refcnt <= 0) {
		if(c->mims != nil) {
			for(i = 0; i < c->mimslen; i++) {
				m = c->mims[i];
				if(m->im != nil)
					freeimage(m->im);
				if(m->mask != nil)
					freeimage(m->mask);
			}
		}
	}
}

// Return true if Cimages a and b represent the same image.
// As well as matching the src urls, the specified widths and heights must match too.
// (Widths and heights are specified if at least one of those is not zero.)
int
cimagematch(CImage* a, CImage* b)
{
	if(a->imhash == b->imhash) {
		if(urlequal(a->src, b->src)) {
			return (a->width == 0 || b->width == 0 || a->width == b->width) &&
				(a->height == 0 || b->height == 0 || a->height == b->height);
			// (above is not quite enough: should also check that don't have
			// situation where one has width set, not height, and the other has reverse,
			// but it is unusual for an image to have a spec in only one dimension anyway)
		}
	}
	return 0;
}

// Return approximate number of bytes due to Images in ci
int
cimagebytes(CImage* ci)
{
	int tot, i;
	MaskedImage* mim;
	Image* dim;

	tot = 0;
	for(i = 0; i < ci->mimslen; i++) {
		mim = ci->mims[i];
		dim = mim->im;
		if(dim != nil)
			tot += ((dim->r.max.x-dim->r.min.x)>>(3-log2[dim->depth])) *
					(dim->r.max.y-dim->r.min.y);
		dim = mim->mask;
		if(dim != nil)
			tot += ((dim->r.max.x-dim->r.min.x)>>(3-log2[dim->depth])) *
					(dim->r.max.y-dim->r.min.y);
	}
	return tot;
}

// Call this after initial windows have been made,
// so that resetlimits() will exclude the images for those
// windows from the available memory.
static void
imcacheinit(void)
{
	memset(&imcache, 0, sizeof(imcache));
	imcacheresetlimits();
}

// Call resetlimits when amount of non-image-cache image
// memory might have changed significantly (e.g., on main window resize).
void
imcacheresetlimits(void)
{
	int avail;
	ResourceState rs;

	rs = curresstate();
	avail = rs.memavail;
	avail = 6*avail/10;	// allow 40% slop for nonimage data and other applications
	imcache.memlimit = config.imagecachemem;
	if(imcache.memlimit > avail)
		imcache.memlimit = avail;
	imcache.nlimit = config.imagecachenum;
	imcacheneed(0);	// if resized, perhaps need to shed some images
}

// Look for a CImage matching ci, and if found, move it
// to the tail position (i.e., MRU)
CImage*
imcachelook(CImage* ci)
{
	CImage*	ans;
	CImage*	prev;
	CImage*	i;

	ans = nil;
	prev = nil;
	for(i = imcache.imhd; i != nil; i = i->next) {
		if(cimagematch(i, ci)) {
			if(imcache.imtl != i) {
				if(prev != nil)
					prev->next = i->next;
				else
					imcache.imhd = i->next;
				i->next = nil;
				imcache.imtl->next = i;
				imcache.imtl = i;
			}
			ans = i;
			break;
		}
		prev = i;
	}
	return ans;
}

// Call this to add ci as MRU of cache chain (should only call if
// it is known that a ci with same image isn't already there).
// Update imcache.memused.
// Assume imcacheneed has been called to ensure that neither
// memlimit nor nlimit will be exceeded.
void
imcacheadd(CImage* ci)
{
	ci->next = nil;
	if(imcache.imhd == nil)
		imcache.imhd = ci;
	else
		imcache.imtl->next = ci;
	imcache.imtl = ci;
	imcache.memused += cimagebytes(ci);
	imcache.n++;
}

// Delete least-recently-used image in image cache
// and update memused and n.
void
imcachedeletelru(void)
{
	CImage*	ci;

	ci = imcache.imhd;
	if(ci != nil) {
		imcache.imhd = ci->next;
		if(imcache.imhd == nil) {
			imcache.imtl = nil;
			imcache.memused = 0;
		}
		else
			imcache.memused -= cimagebytes(ci);
		freecimage(ci);
		imcache.n--;
	}
}

void
imcacheclear(void)
{
	while(imcache.imhd != nil)
		imcachedeletelru();
}

int
imcacheneed(int nbytes)
{
	while(imcache.n >=imcache.nlimit || imcache.memused + nbytes > imcache.memlimit) {
		if(imcache.imhd == nil)
			return 0;
		imcachedeletelru();
	}
	return 1;
}

// Return new Header with default values for fields
Header*
newheader(void)
{
	Header* h;

	h = (Header*)emalloc(sizeof(Header));
	h->code = HCOk;
	h->actual = nil;
	h->base = nil;
	h->location = nil;
	h->length = -1;
	h->mtype = UnknownType;
	h->chset = UnknownCharset;
	h->msg = nil;
	h->refresh = nil;
	h->chal = nil;
	h->warn = nil;
	return h;
}

// Set the mtype (and possibly chset) fields of h based on (in order):
//	first bytes of file, if unambigous
//	file name extension
//	first bytes of file, even if ambigous (guess)
//	if all else fails, then leave as UnknownType.
// If it's a text type, also set the chset.
// (HTTP Transport will try to use Content-Type first, and call this if that
// doesn't work; other Transports will have to rely on this "guessing" function.)
void
setmediatype(Header* h, Rune* name, uchar* first, int firstlen)
{
	int	n;
	int	mt;
	int	i;
	Rune*	s;
	int	val;
	Rune*	ext;
	Rune*	file;
	uchar	b;
	int	nctl;
	int	ntophalf;

	n = firstlen;
	mt = UnknownType;
	for(i = 0; i < n; i++)
		if(!isspace(first[i]))
			break;
	if(n - i >= 6) {
		s = toStr(first, 6, US_Ascii);
		if(!Strcmp(s, L"<html>") || !Strcmp(s, L"<head>") || !Strcmp(s, L"<title")) {
			h->mtype = TextHtml;
		}
		else if(!Strcmp(s, L"<!doct")) {
				mt = TextHtml;
		}
		else if(!Strcmp(s, L"gif87a") || !Strcmp(s, L"gif89a")) {
			if(i == 0)
				mt = ImageGif;
		}
		else if(!Strcmp(s, L"#defin")) {
			h->mtype = ImageXXBitmap;
		}
		if(h->mtype == UnknownType) {
			if(i == 0 && n >= sizeof(jpmagic)) {
				for(; i < sizeof(jpmagic); i++)
					if(jpmagic[i] > (uchar)0 && first[i] != jpmagic[i])
						break;
				if(i == sizeof(jpmagic))
					mt = ImageJpeg;
			}
		}
	}
	if(mt == UnknownType && name != nil) {
		// Try file name extension
		file = Strrclass(name, L"/");
		if(file == nil)
			file = name;
		else
			file++;
		if(*file != 0) {
			ext = Strrclass(file, L".");
			if(ext != nil) {
				ext++;
				if(lookup(fileexttable, NFILEEXTS, ext, Strlen(ext), &val))
					mt = val;
			}
		}
	}
	if(mt == UnknownType || (mt >= TextHtml && mt <= TextSgml)) {
		// Try file statistics
		nctl = 0;
		ntophalf = 0;
		if(n > 1024)
			n = 1024;
		for(i = 0; i < n; i++) {
			b = first[i];
			if(iscntrl(b))
				nctl++;
			if(b >= 128)
				ntophalf++;
		}
		if(nctl < n/100 && ntophalf < n/10) {
			if(mt == UnknownType)
				mt = TextPlain;
			if(ntophalf == 0)
				h->chset = US_Ascii;
			else
				h->chset = ISO_8859_1;
		}
		else if(mt == UnknownType) {
			if(n == 0) {
				mt = TextHtml;
				h->chset = ISO_8859_1;
			}
			else
				mt = ApplOctets;
		}
		else
			h->chset = ISO_8859_1;
	}
	h->mtype = mt;
}

void
printheader(Header* h)
{
	Rune*	mtype;
	Rune*	chset;

	mtype = L"?";
	if(h->mtype >= 0 && h->mtype < NMEDIATYPES)
		mtype = mnames[h->mtype];
	chset = L"?";
	if(h->chset >= 0 && h->chset < NCHARSETS)
		chset = chsetnames[h->chset];
	trace("code=%d (%S) length=%d mtype=%S chset=%S\n",
			h->code, hcphrase(h->code), h->length, mtype, chset);
	if(h->base != nil)
		trace("  base=%U\n", h->base);
	if(h->location != nil)
		trace("  location=%U\n", h->location);
	if(h->refresh != nil)
		trace("  refresh=%S\n", h->refresh);
	if(h->chal != nil)
		trace("  chal=%S\n", h->chal);
	if(h->warn != nil)
		trace("  warn=%S\n", h->warn);
}


#define PAGESIZE 4096
ResourceState
curresstate(void)
{
	int n;
	static int fd = -1;
	char* p;
	ResourceState r;
	uchar buf[SMALLBUFSIZE];

	r.ms = (int)cputime();
	r.mem = 1*1024*1024;
	r.memavail = 10*1024*1024;
	if(fd < 0)
		fd = open("/dev/swap", OREAD);
	if(fd >= 0) {
		seek(fd, 0, 0);
		n = read(fd, buf, (sizeof buf) - 1);
		if(n > 3) {
			buf[n] = 0;
			r.mem = strtol((char*)buf, &p, 10) * PAGESIZE;
			if(p != nil && *p == '/') {
				r.memavail = strtol((char*)(p+1), &p, 10) * PAGESIZE;
			}
		}
	}
	return r;
}

ResourceState
resstatesince(ResourceState rnew, ResourceState rold)
{
	ResourceState r;

	r.ms = rnew.ms - rold.ms;
	r.mem = rnew.mem - rold.mem;
	r.memavail = rnew.memavail - rold.memavail;
	return r;
}

void
resstateprint(ResourceState r, char* msg)
{
	int d, dms;

	d = r.ms/1000;
	dms = r.ms % 1000;
	trace("%s:\n\ttime: %d.%#.3ds; memory: %dK, available memory %dK\n",
				msg, d, dms, r.mem/1024, r.memavail/1024);
}

// Decide what to do based on Header and whether this is
// for the main entity or not, and the number of redirections-so-far.
// Return value is "use"; other return values put in *perror, *pchallenge, and *predir.
// Action to do is:
//	If use==1, use the entity else drain its byte source.
//	If *perror != nil, mesg was put in progress bar
//	If (pchallenge != nil, get auth info and make new request with auth
//	Else if *predir != nil, make a new request with redir for url
//
// (if challenge or redir is non-nil, use will be 0)
int
hdraction(ByteSource* bs, int ismain, int nredirs,
					int* perror, Rune** pchallenge, ParsedUrl** predir)
{
	int	use;
	int	error;
	Rune*	challenge;
	ParsedUrl*	redir;
	Header*	h;
	int	code;

	use = 1;
	error = 0;
	challenge = nil;
	redir = nil;
	h = bs->hdr;
	assert(h != nil);
	bs->seenhdr = 1;
	code = h->code;
	switch(code/100) {
	case HSOk:
		if(code != HCOk)
			error = ERRunexphscode;
		break;
	case HSRedirect:
		if(h->location != nil) {
			redir = h->location;
			if(redir->scheme == NOSCHEME)
				redir = makeabsoluteurl(redir, h->base);
			if(dbg)
				trace("redirect %U to %U\n", h->actual, redir);
			if(nredirs >= Maxredir) {
				redir = nil;
				error = ERRredirloop;
			}
			else
				use = 0;
		}
		break;
	case HSError:
		if(code == HCUnauthorized && h->chal != nil) {
			challenge = h->chal;
			use = 0;
		}
		else {
			error = code;
			use = ismain;
		}
		break;
	case HSServererr:
		error = code;
		use = ismain;
		break;
	default:
		error = ERRunexphscode;
		use = 0;
		break;
	}
	if(error && config.showprogress)
		sendprogress(bs->id, Perr, 0, errphrase(error));
	*perror = error;
	*pchallenge = challenge;
	*predir = redir;
	return use;
}

void
sendprogress(int bsid, int state, int pcnt, Rune* s)
{
	EV prog;

	prog.tag = EVprogresstag;
	prog.genframeid = -1;	// TODO: pass in correct frameid
	prog.u.progress.bsid = bsid;
	prog.u.progress.state = state;
	prog.u.progress.pcnt = pcnt;
	prog.u.progress.s = s;
	send(evchan, &prog);
}

// Read a line up to and including cr/lf (be tolerant and allowing missing cr).
// Look first in buf[*pbstart:*pbend], and if that isn't sufficient to get whole line,
// refill buf from fd as needed.
// Since most of the time the needed line will be in the passed buffer, we sometimes
// return pointers into that buffer.  We only allocated the answer if the line straddles
// buffer reads.
//
// Return values:
//	getline returns 1 if got eof, else 0.
//	*pans points to line, *panslen points to its length (not including cr/lf).
//	*pbstart and *pbend are updated to new valid portion of buf (after cr/lf).
int
getline(int fd, uchar* buf, int bufsize, int* pbstart, int* pbend,
	uchar** pans, int* panslen)
{
	int	i, k, eof, bstart, bend;
	int	anslen, lastlen;
	uchar*	ans;
	uchar*	last;

	ans = nil;
	anslen = 0;
	eof = 0;
	bstart = *pbstart;
	bend = *pbend;
	while(1) {
		for(i = bstart; i < bend; i++) {
			if(buf[i] == '\n') {
				k = i;
				if(k > bstart && buf[k - 1] == '\r')
					k--;
				last = buf+bstart;
				lastlen = k-bstart;
				bstart = i + 1;
				goto mainloop_done;
			}
		}
		k = bend-bstart;
		if(k > 0) {
			if(anslen > 0)
				ans = (uchar*)erealloc(ans, anslen+k);
			else
				ans = (uchar*)emalloc(k);
			memmove(ans+anslen, buf+bstart, k);
			anslen += k;
		}
		last = nil;
		lastlen = 0;
		bstart = 0;
		bend = read(fd, buf, bufsize);
		if(bend <= 0) {
			eof = 1;
			bend = 0;
			break;
		}
	}
   mainloop_done:
	if(anslen == 0) {
		ans = last;
		anslen = lastlen;
	}
	else {
		if(lastlen > 0) {
			ans = (uchar*)erealloc(ans, anslen+lastlen);
			memmove(ans+anslen, last, lastlen);
			anslen += lastlen;
		}
	}
	*pans = ans;
	*panslen = anslen;
	*pbstart = bstart;
	*pbend = bend;
//trace("*pans=%d, *panslen=%d, *pbstart=%d, *pbend=%d, eof=%d\n", ans, anslen, bstart, bend, eof);
//if(anslen > 0) { write(1, ans, anslen); trace("\n"); }
	return eof;
}

// Look (linearly) through a for s[0:slen] (case insensitive); return its index if found, else -1.
int
Strlookup(Rune** a, int n, Rune* s, int slen)
{
	int	i;

	for(i = 0; i < n; i++)
		if(!Strncmpci(s, slen, a[i]))
			return i;
	return -1;
}

// Return string representation of HTTP code.
// Note: returned values are constants, and should not be freed.
Rune*
hcphrase(int code)
{
	Rune*	ans;

	switch(code) {
	case HCContinue:
		ans = L"Continue";
		break;
	case HCSwitchProto:
		ans = L"Switching Protcols";
		break;
	case HCOk:
		ans = L"Ok";
		break;
	case HCCreated:
		ans = L"Created";
		break;
	case HCAccepted:
		ans = L"Accepted";
		break;
	case HCOkNonAuthoritative:
		ans = L"Non-Authoratative Information";
		break;
	case HCNoContent:
		ans = L"No content";
		break;
	case HCResetContent:
		ans = L"Reset content";
		break;
	case HCPartialContent:
		ans = L"Partial content";
		break;
	case HCMultipleChoices:
		ans = L"Multiple choices";
		break;
	case HCMovedPerm:
		ans = L"Moved permanently";
		break;
	case HCMovedTemp:
		ans = L"Moved temporarily";
		break;
	case HCSeeOther:
		ans = L"See other";
		break;
	case HCNotModified:
		ans = L"Not modified";
		break;
	case HCUseProxy:
		ans = L"Use proxy";
		break;
	case HCBadRequest:
		ans = L"Bad request";
		break;
	case HCUnauthorized:
		ans = L"Unauthorized";
		break;
	case HCPaymentRequired:
		ans = L"Payment required";
		break;
	case HCForbidden:
		ans = L"Forbidden";
		break;
	case HCNotFound:
		ans = L"Not found";
		break;
	case HCMethodNotAllowed:
		ans = L"Method not allowed";
		break;
	case HCNotAcceptable:
		ans = L"Not Acceptable";
		break;
	case HCProxyAuthRequired:
		ans = L"Proxy authentication required";
		break;
	case HCRequestTimeout:
		ans = L"Request timed-out";
		break;
	case HCConflict:
		ans = L"Conflict";
		break;
	case HCGone:
		ans = L"Gone";
		break;
	case HCLengthRequired:
		ans = L"Length required";
		break;
	case HCPreconditionFailed:
		ans = L"Precondition failed";
		break;
	case HCRequestTooLarge:
		ans = L"Request entity too large";
		break;
	case HCRequestURITooLarge:
		ans = L"Request-URI too large";
		break;
	case HCUnsupportedMedia:
		ans = L"Unsupported media type";
		break;
	case HCRangeInvalid:
		ans = L"Requested range not valid";
		break;
	case HCExpectFailed:
		ans = L"Expectation failed";
		break;
	case HCServerError:
		ans = L"Internal server error";
		break;
	case HCNotImplemented:
		ans = L"Not implemented";
		break;
	case HCBadGateway:
		ans = L"Bad gateway";
		break;
	case HCServiceUnavailable:
		ans = L"Service unavailable";
		break;
	case HCGatewayTimeout:
		ans = L"Gateway time-out";
		break;
	case HCVersionUnsupported:
		ans = L"HTTP version not supported";
		break;
	case HCRedirectionFailed:
		ans = L"Redirection failed";
		break;
	default:
		ans = L"Unknown code";
		break;
	}
	return ans;
}

// Error codes can be either one of the ERR... values or one of the HC... values
Rune*
errphrase(int code)
{
	if(code >=0 && code < NERRPHRASES)
		return errphrases[code];
	return hcphrase(code);
}

// Make a StringInt table out of a[0:n], mapping each string
// to its index.  Check that entries are in alphabetical order.
// These tables use the real malloc(), and are never freed.
StringInt*
makestrinttab(Rune** a, int n)
{
	StringInt*	ans;
	int	i;

	ans = (StringInt*)malloc(n * sizeof(StringInt));
	for(i = 0; i < n; i++) {
		ans[i].key = a[i];
		ans[i].val = i;
		assert(i == 0 || Strcmp(a[i], a[i - 1]) >= 0);
	}
	return ans;
}

// Use targetmap array to keep track of name <-> targetid mapping.
// Use real malloc(), and never free
static void
targetmapinit(void)
{
	targetmapsize = 10;
	targetmap = (StringInt*)malloc(targetmapsize*sizeof(StringInt));
	memset(targetmap, 0, targetmapsize*sizeof(StringInt));
	targetmap[0].key = L"_top";
	targetmap[0].val = FTtop;
	targetmap[1].key = L"_self";
	targetmap[1].val = FTself;
	targetmap[2].key = L"_parent";
	targetmap[2].val = FTparent;
	targetmap[3].key = L"_blank";
	targetmap[3].val = FTblank;
	ntargets = 4;
}

int
targetid(Rune* s)
{
	int i;
	int n;

	n = Strlen(s);
	if(n == 0)
		return FTself;
	for(i = 0; i < ntargets; i++)
		if(Strcmp(s, targetmap[i].key) == 0)
			return targetmap[i].val;
	if(i >= targetmapsize) {
		targetmapsize += 10;
		targetmap = (StringInt*)realloc(targetmap, targetmapsize*sizeof(StringInt));
	}
	targetmap[i].key = (Rune*)malloc((n+1)*sizeof(Rune));
	memmove(targetmap[i].key, s, (n+1)*sizeof(Rune));
	targetmap[i].val = i;
	ntargets++;
	return i;
}

Rune*
targetname(int targid)
{
	int i;

	for(i = 0; i < ntargets; i++)
		if(targetmap[i].val == targid)
			return targetmap[i].key;
	return L"?";
}

// Use logtime when only care about time stamps
void
logtime(char* msg, int data)
{
	trace("%s: %d %d\n", msg, (int)cputime() - startres.ms, data);
}

// Convert HTML color spec to RGB value, returning dflt if can't.
// Argument is supposed to be a valid HTML color, or "".
// Return the RGB value of the color, using dflt if s
// is nil or an invalid color.
int
color(Rune* s, int dflt)
{
	int v;
	Rune* rest;

	if(s == nil)
		return dflt;
	if(lookup(color_tab, NCOLORS, s, Strlen(s), &v))
		return v;
	if(s[0] == '#')
		s++;
	v = Strtol(s, &rest, 16);
	if(*rest == 0)
		return v;
	return dflt;
}

static int
strhash(Rune* s)
{
	int	hash;
	enum { prime = 8388617 };

	hash = 0;
	if(s != nil) {
		while(*s != 0)
			hash = ((hash%prime) << 7) + *s++;
	}
	return hash;
}

// Set up config global to defaults, then try to read user-specifiic
// config data from /usr/<username>/lib/iconfig, then try to
// override from command line arguments.
static void
setconfig(int argc, char** argv)
{
	Rune*	user;
	int	fd;
	int	n;
	Rune*	line;
	int		linelen;
	Rune*	key;
	int		keylen;
	Rune*	val;
	int		vallen;
	int	i;
	int	j;
	uchar*	aline;
	int		alinelen;
	int	eof;
	int	cfgio;
	Rune*	a;
	Rune*	b;
	char*	s;
	uchar	buf[BIGBUFSIZE];

	config.userdir = Strdup(L"/");
	config.starturl = makeurl(L"file:/lib/i/start.html", 0);
	config.homeurl = copyurl(config.starturl);
	config.httpproxy = nil;
	config.defaultwidth = 800;
	config.defaultheight = 800;
	config.x = 0;
	config.y = 0;
	config.nocache = 0;
	config.maxstale = 0;
	config.imagelvl = ImgFull;
	config.imagecachenum = 60;
	config.imagecachemem = 100000000;
	config.docookies = 0;
	config.doscripts = 0;
	config.saveauthinfo = 0;
	config.showprogress = 1;
	config.usecci = 0;
	config.httpminor = 0;
	config.agentname = Strdup(L"i/2.0 (Plan 9)");
	config.nthreads = 1;
	config.dbgfile = nil;
	user = nil;
	fd = open("/dev/user", OREAD);
	if(fd != -1) {
		n = read(fd, buf, sizeof(buf));
		if(n > 0)
			user = toStr(buf, n, UTF_8);
		close(fd);
	}
	if(user != nil) {
		config.userdir = Strdup3(L"/usr/", user, L"/lib");
		snprint((char*)buf, sizeof(buf), "%S/iconfig", config.userdir);
		cfgio = open((char*)buf, OREAD);
		if(cfgio != -1) {
			i = 0;
			j = 0;
			while(1) {
				eof = getline(cfgio, buf, sizeof(buf), &i, &j, &aline, &alinelen);
				if(eof)
					break;
				if(alinelen == 0)
					continue;
				line = toStr(aline, alinelen, UTF_8);
				linelen = Strlen(line);
				if(alinelen > 0 && line[0] != '#') {
					splitl(line, linelen, L" \t=", &key, &keylen, &val, &vallen);
					if(keylen != 0) {
						if(vallen != 0) {
							a = Strnclass(val, L"^ \t=", vallen);
							if(a == nil)
								vallen = 0;
							else {
								vallen -= (a-val);
								val = a;
								a = Strnclass(val, L"#\r\n", vallen);
								if(a != nil)
									vallen = a-val;
							}
						}
						if(!setopt(key, keylen, val, vallen))
							trace("couldn't set option from line '%S'\n", line);
					}
				}
			}
			close(cfgio);
		}
	}
	argc--;
	argv++;
	while(argc > 0) {
		s = argv[0];
		if(*s == 0)
			continue;
		else if(s[0] == '-') {
			a = toStr((uchar*)(s+1), strlen(s+1), UTF_8);
			b = nil;
			if(argc > 1) {
				b = toStr((uchar*)argv[1], strlen(argv[1]), UTF_8);
				if(b[0] != '-') {
					argc--;
					argv++;
				}
			}
			if(!setopt(a, Strlen(a), b, Strlen(b)))
				trace("couldn't set option from arg '%s'\n", s);
		}
		else {
			if(argc == 1) {
				a = toStr((uchar*)s, strlen(s), UTF_8);
				if(!setopt(L"starturl", 8, a, Strlen(a)))
					trace("couldn't set starturl from arg '%s'\n", s);
			}
			else
				trace("ignoring unknown option %s\n", s);
		}
		argc--;
		argv++;
	}
}

static int
setopt(Rune* key, int keylen, Rune* val, int vallen)
{
	int	ok;
	int	v;
	int	c;
	int	i;
	Rune*	s;

	ok = 1;
	if(vallen == 0) {
		s = nil;
		v = 1;
	}
	else {
		s = Strndup(val, vallen);
		v = Strtol(s, nil, 0);
	}
	if(!Strncmpci(key, keylen, L"userdir")) {
		config.userdir = Strdup(s);
	}
	else if(!Strncmpci(key, keylen, L"starturl")) {
		if(vallen != 0) {
			config.starturl = makeurl(s, 1);
			assert(config.starturl != nil);
		}
		else
			ok = 0;
	}
	else if(!Strncmpci(key, keylen, L"homeurl")) {
		if(vallen != 0) {
			config.homeurl = makeurl(s, 1);
		}
		else
			ok = 0;
	}
	else if(!Strncmpci(key, keylen, L"httpproxy")) {
		if(vallen != 0) {
			config.httpproxy = makeurl(s, 1);
		}
		else
			config.httpproxy = nil;
	}
	else if(!Strncmpci(key, keylen, L"defaultwidth") || !Strncmpci(key, keylen, L"width")) {
		if(v > 200)
			config.defaultwidth = v;
		else
			ok = 0;
	}
	else if(!Strncmpci(key, keylen, L"defaultheight") || !Strncmpci(key, keylen, L"height")) {
		if(v > 100)
			config.defaultheight = v;
		else
			ok = 0;
	}
	else if(!Strncmpci(key, keylen, L"x")) {
		config.x = v;
	}
	else if(!Strncmpci(key, keylen, L"y")) {
		config.y = v;
	}
	else if(!Strncmpci(key, keylen, L"nocache")) {
		config.nocache = v;
	}
	else if(!Strncmpci(key, keylen, L"maxstale")) {
		config.maxstale = v;
	}
	else if(!Strncmpci(key, keylen, L"imagelvl")) {
		config.imagelvl = v;
	}
	else if(!Strncmpci(key, keylen, L"imagecachenum")) {
		config.imagecachenum = v;
	}
	else if(!Strncmpci(key, keylen, L"imagecachemem")) {
		config.imagecachemem = v;
	}
	else if(!Strncmpci(key, keylen, L"docookies")) {
		config.docookies = v;
	}
	else if(!Strncmpci(key, keylen, L"doscripts")) {
		config.doscripts = v;
	}
	else if(!Strncmpci(key, keylen, L"saveauthinfo")) {
		config.saveauthinfo = v;
	}
	else if(!Strncmpci(key, keylen, L"showprogress")) {
		config.showprogress = v;
	}
	else if(!Strncmpci(key, keylen, L"usecci")){
		config.usecci = v;
	}
	else if(!Strncmpci(key, keylen, L"http")) {
		if(!Strncmpci(val, vallen, L"1.1"))
			config.httpminor = 1;
		else
			config.httpminor = 0;
	}
	else if(!Strncmpci(key, keylen, L"agentname")) {
		config.agentname = Strdup(s);
	}
	else if(!Strncmpci(key, keylen, L"nthreads")) {
		config.nthreads = v;
	}
	else if(!Strncmpci(key, keylen, L"dbgfile")) {
		config.dbgfile = Strdup(s);
	}
	else if(!Strncmpci(key, keylen, L"dbg")) {
		for(i = 0; i < vallen; i++) {
			c = val[i];
			if(c < sizeof(config.dbg))
				config.dbg[c]++;
			else {
				ok = 0;
				break;
			}
		}
	}
	else
		ok = 0;
	return ok;
}

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to webmaster@9p.io.