diff options
| author | Charles.Forsyth <devnull@localhost> | 2006-12-22 17:07:39 +0000 |
|---|---|---|
| committer | Charles.Forsyth <devnull@localhost> | 2006-12-22 17:07:39 +0000 |
| commit | 37da2899f40661e3e9631e497da8dc59b971cbd0 (patch) | |
| tree | cbc6d4680e347d906f5fa7fca73214418741df72 /appl/charon/chutils.b | |
| parent | 54bc8ff236ac10b3eaa928fd6bcfc0cdb2ba46ae (diff) | |
20060303a
Diffstat (limited to 'appl/charon/chutils.b')
| -rw-r--r-- | appl/charon/chutils.b | 2050 |
1 files changed, 2050 insertions, 0 deletions
diff --git a/appl/charon/chutils.b b/appl/charon/chutils.b new file mode 100644 index 00000000..40da2f5f --- /dev/null +++ b/appl/charon/chutils.b @@ -0,0 +1,2050 @@ +implement CharonUtils; + +include "common.m"; +include "transport.m"; +include "date.m"; +include "translate.m"; + + date: Date; + me: CharonUtils; + sys: Sys; + D: Draw; + S: String; + U: Url; + T: StringIntTab; + +Font : import D; +Parsedurl: import U; +convcs : Convcs; +trans : Translate; + Dict : import trans; +dict : ref Dict; + +NCTimeout : con 100000; # free NC slot after 100 seconds +UBufsize : con 40*1024; # initial buffer size for unknown lengths +UEBufsize : con 1024; # initial buffer size for unknown lengths, error responses + +botchexception := "EXInternal: ByteSource protocol botch"; +bytesourceid := 0; +crlf : con "\r\n"; +ctype : array of byte; # local ref to C->ctype[] +dbgproto : int; +dbg: int; +netconnid := 0; +netconns := array[10] of ref Netconn; +sptab : con " \t"; + +THTTP, TFTP, TFILE, TMAX: con iota; +transports := array[TMAX] of Transport; +tpaths := array [TMAX] of { + THTTP => Transport->HTTPPATH, + TFTP => Transport->FTPPATH, + TFILE => Transport->FILEPATH, +}; + +schemes := array [] of { + ("http", THTTP), + ("https", THTTP), + ("ftp", TFTP), + ("file", TFILE), +}; + +ngchan : chan of (int, list of ref ByteSource, ref Netconn, chan of ref ByteSource); + +# must track HTTP methods in chutils.m +# (upper-case, since that's required in HTTP requests) +hmeth = array[] of { "GET", "POST" }; + +# following array must track media type def in chutils.m +# keep in alphabetical order +mnames = array[] of { + "application/msword", + "application/octet-stream", + "application/pdf", + "application/postscript", + "application/rtf", + "application/vnd.framemaker", + "application/vnd.ms-excel", + "application/vnd.ms-powerpoint", + "application/x-unknown", + "audio/32kadpcm", + "audio/basic", + "image/cgm", + "image/g3fax", + "image/gif", + "image/ief", + "image/jpeg", + "image/png", + "image/tiff", + "image/x-bit", + "image/x-bit2", + "image/x-bitmulti", + "image/x-inferno-bit", + "image/x-xbitmap", + "model/vrml", + "multipart/digest", + "multipart/mixed", + "text/css", + "text/enriched", + "text/html", + "text/javascript", + "text/plain", + "text/richtext", + "text/sgml", + "text/tab-separated-values", + "text/xml", + "video/mpeg", + "video/quicktime" +}; + +ncstatenames = array[] of { + "free", "idle", "connect", "gethdr", "getdata", + "done", "err" +}; + +hsnames = array[] of { + "none", "information", "ok", "redirect", "request error", "server error" +}; + +hcphrase(code: int) : string +{ + ans : string; + case code { + HCContinue => ans = X("Continue", "http"); + HCSwitchProto => ans = X("Switching Protocols", "http"); + HCOk => ans = X("Ok", "http"); + HCCreated => ans = X("Created", "http"); + HCAccepted => ans = X("Accepted", "http"); + HCOkNonAuthoritative => ans = X("Non-Authoratative Information", "http"); + HCNoContent => ans = X("No content", "http"); + HCResetContent => ans = X("Reset content", "http"); + HCPartialContent => ans = X("Partial content", "http"); + HCMultipleChoices => ans = X("Multiple choices", "http"); + HCMovedPerm => ans = X("Moved permanently", "http"); + HCMovedTemp => ans = X("Moved temporarily", "http"); + HCSeeOther => ans = X("See other", "http"); + HCNotModified => ans = X("Not modified", "http"); + HCUseProxy => ans = X("Use proxy", "http"); + HCBadRequest => ans = X("Bad request", "http"); + HCUnauthorized => ans = X("Unauthorized", "http"); + HCPaymentRequired => ans = X("Payment required", "http"); + HCForbidden => ans = X("Forbidden", "http"); + HCNotFound => ans = X("Not found", "http"); + HCMethodNotAllowed => ans = X("Method not allowed", "http"); + HCNotAcceptable => ans = X("Not Acceptable", "http"); + HCProxyAuthRequired => ans = X("Proxy authentication required", "http"); + HCRequestTimeout => ans = X("Request timed-out", "http"); + HCConflict => ans = X("Conflict", "http"); + HCGone => ans = X("Gone", "http"); + HCLengthRequired => ans = X("Length required", "http"); + HCPreconditionFailed => ans = X("Precondition failed", "http"); + HCRequestTooLarge => ans = X("Request entity too large", "http"); + HCRequestURITooLarge => ans = X("Request-URI too large", "http"); + HCUnsupportedMedia => ans = X("Unsupported media type", "http"); + HCRangeInvalid => ans = X("Requested range not valid", "http"); + HCExpectFailed => ans = X("Expectation failed", "http"); + HCServerError => ans = X("Internal server error", "http"); + HCNotImplemented => ans = X("Not implemented", "http"); + HCBadGateway => ans = X("Bad gateway", "http"); + HCServiceUnavailable => ans = X("Service unavailable", "http"); + HCGatewayTimeout => ans = X("Gateway time-out", "http"); + HCVersionUnsupported => ans = X("HTTP version not supported", "http"); + HCRedirectionFailed => ans = X("Redirection failed", "http"); + * => ans = X("Unknown code", "http"); + } + return ans; +} + +# This array should be kept sorted +fileexttable := array[] of { T->StringInt + ("ai", ApplPostscript), + ("au", AudioBasic), +# ("bit", ImageXBit), + ("bit", ImageXInfernoBit), + ("bit2", ImageXBit2), + ("bitm", ImageXBitmulti), + ("eps", ApplPostscript), + ("gif", ImageGif), + ("gz", ApplOctets), + ("htm", TextHtml), + ("html", TextHtml), + ("jpe", ImageJpeg), + ("jpeg", ImageJpeg), + ("jpg", ImageJpeg), + ("pdf", ApplPdf), + ("png", ImagePng), + ("ps", ApplPostscript), + ("shtml", TextHtml), + ("text", TextPlain), + ("tif", ImageTiff), + ("tiff", ImageTiff), + ("txt", TextPlain), + ("zip", ApplOctets) +}; + +# argl is command line +# Return string that is empty if all ok, else path of module +# that failed to load. +init(ch: Charon, c: CharonUtils, argl: list of string, evc: chan of ref E->Event, cksrv: Cookiesrv, ckc: ref Cookiesrv->Client) : string +{ + me = c; + sys = load Sys Sys->PATH; + startres = ResourceState.cur(); + D = load Draw Draw->PATH; + CH = ch; + S = load String String->PATH; + if(S == nil) + return String->PATH; + + U = load Url Url->PATH; + if(U == nil) + return Url->PATH; + U->init(); + + T = load StringIntTab StringIntTab->PATH; + if(T == nil) + return StringIntTab->PATH; + + trans = load Translate Translate->PATH; + if (trans != nil) { + trans->init(); + (dict, nil) = trans->opendict(trans->mkdictname(nil, "charon")); + } + + # Now have all the modules needed to process command line + # (hereafter can use our loadpath() function to substitute the + # build directory version if dbg['u'] is set) + + setconfig(argl); + dbg = int config.dbg['d']; + + G = load Gui loadpath(Gui->PATH); + if(G == nil) + return loadpath(Gui->PATH); + + C = load Ctype loadpath(Ctype->PATH); + if(C == nil) + return loadpath(Ctype->PATH); + + E = load Events Events->PATH; + if(E == nil) + return loadpath(Events->PATH); + + J = load Script loadpath(Script->JSCRIPTPATH); + # don't report an error loading JavaScript, handled elsewhere + + LX = load Lex loadpath(Lex->PATH); + if(LX == nil) + return loadpath(Lex->PATH); + + B = load Build loadpath(Build->PATH); + if(B == nil) + return loadpath(Build->PATH); + + I = load Img loadpath(Img->PATH); + if(I == nil) + return loadpath(Img->PATH); + + L = load Layout loadpath(Layout->PATH); + if(L == nil) + return loadpath(Layout->PATH); + date = load Date loadpath(Date->PATH); + if (date == nil) + return loadpath(Date->PATH); + + convcs = load Convcs Convcs->PATH; + if (convcs == nil) + return loadpath(Convcs->PATH); + + + # Intialize all modules after loading all, so that each + # may cache pointers to the other modules + # (G will be initialized in main charon routine, and L has to + # be inited after that, because it needs G's display to allocate fonts) + + E->init(evc); + I->init(me); + err := convcs->init(nil); + if (err != nil) + return err; + if(J != nil) { + err = J->init(me); + if (err != nil) { + # non-fatal: just don't handle javascript + J = nil; + if (dbg) + sys->print("%s\n", err); + } + } + B->init(me); + LX->init(me); + date->init(me); + + if (config.docookies) { + CK = cksrv; + ckclient = ckc; + if (CK == nil) { + path := loadpath(Cookiesrv->PATH); + CK = load Cookiesrv path; + if (CK == nil) + sys->print("cookies: cannot load server %s: %r\n", path); + else + ckclient = CK->start(config.userdir + "/cookies", 0); + } + } + + # preload some transports + gettransport("http"); + gettransport("file"); + + progresschan = chan of (int, int, int, string); + imcache = ref ImageCache; + ctype = C->ctype; + dbgproto = int config.dbg['p']; + ngchan = chan of (int, list of ref ByteSource, ref Netconn, chan of ref ByteSource); + return ""; +} + +# like startreq() but special case for a string ByteSource +# which doesn't need an associated netconn +stringreq(s : string) : ref ByteSource +{ + bs := ByteSource.stringsource(s); + + G->progress <-= (bs.id, G->Pstart, 0, "text"); + anschan := chan of ref ByteSource; + ngchan <-= (NGstartreq, bs :: nil, nil, anschan); + <-anschan; + return bs; +} + +# 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. +startreq(req: ref ReqInfo) : ref ByteSource +{ + bs := ref ByteSource( + bytesourceid++, + req, # req + nil, # hdr + nil, # data + 0, # edata + "", # err + nil, # net + 1, # refgo + 1, # refnc + 0, # eof + 0, # lim + 0 # seenhdr + ); + + G->progress <-= (bs.id, G->Pstart, 0, req.url.tostring()); + anschan := chan of ref ByteSource; + ngchan <-= (NGstartreq, bs::nil, nil, anschan); + <-anschan; + return bs; +} + +# Wait for some ByteSource in current go generation to +# have a state change that goproc hasn't seen yet. +waitreq(bsl: list of ref ByteSource) : ref ByteSource +{ + anschan := chan of ref ByteSource; + ngchan <-= (NGwaitreq, bsl, nil, anschan); + return <-anschan; +} + +# Notify netget that goproc is finished with bs. +freebs(bs: ref ByteSource) +{ + anschan := chan of ref ByteSource; + ngchan <-= (NGfreebs, bs::nil, nil, anschan); + <-anschan; +} + +abortgo(gopgrp: int) +{ + if(int config.dbg['d']) + sys->print("abort go\n"); + kill(gopgrp, 1); + freegoresources(); + # renew the channels so that receives/sends by killed threads don't + # muck things up + ngchan = chan of (int, list of ref ByteSource, ref Netconn, chan of ref ByteSource); +} + +freegoresources() +{ + for(i := 0; i < len netconns; i++) { + nc := netconns[i]; + nc.makefree(); + } +} + +# 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. +netget() +{ + msg, n, i: int; + bsl : list of ref ByteSource; + nc: ref Netconn; + waitix := 0; + c : chan of ref ByteSource; + waitpending : list of (list of ref ByteSource, chan of ref ByteSource); + maxconn := config.nthreads; + gncs := array[maxconn] of int; + + for(n = 0; n < len netconns; n++) + netconns[n] = Netconn.new(n); + + # capture netget chan to prevent abortgo() reset of + # ngchan from breaking us (channel hungup) before kill() does its job + ngc := ngchan; +mainloop: + for(;;) { + (msg,bsl,nc,c) = <- ngc; + case msg { + NGstartreq => + bs := hd bsl; + # 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 ReqInfo is nil then this is a string ByteSource + # in which case we don't need a netconn to service it as we have all + # data already + if (bs.req == nil) { + c <- = nil; + continue; + } + + if(dbgproto) + sys->print("Startreq BS=%d for %s\n", bs.id, bs.req.url.tostring()); + scheme := bs.req.url.scheme; + host := bs.req.url.host; + (transport, err) := gettransport(scheme); + if(err != "") + bs.err = err; + else { + sport :=bs.req.url.port; + if(sport == "") + port := transport->defaultport(scheme); + else + port = int sport; + i = 0; + freen := -1; + for(n = 0; n < len netconns && (i < maxconn || freen == -1); n++) { + nc = netconns[n]; + if(nc.state == NCfree) { + if(freen == -1) + freen = n; + } + else if(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) { + freen = len netconns; + newncs := array[freen+10] of ref Netconn; + newncs[0:] = netconns; + for(n = freen; n < freen+10; n++) + newncs[n] = Netconn.new(n); + netconns = newncs; + } + nc = netconns[freen]; + 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 == len nc.queue) { + nq := array[nc.qlen+10] of ref ByteSource; + nq[0:] = nc.queue; + nc.queue = nq; + } + nc.queue[nc.qlen++] = bs; + bs.net = nc; + if(dbgproto) + sys->print("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) + sys->print("NC %d: starting runnetconn in gethdr state\n", nc.id); + } + else { + nc.state = NCconnect; + if(dbgproto) + sys->print("NC %d: starting runnetconn in connect state\n", nc.id); + } + spawn runnetconn(nc, transport); + } + } + c <-= nil; + + 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) + sys->print("Waitreq\n"); + + for (scanlist := bsl; scanlist != nil; scanlist = tl scanlist) { + bs := hd scanlist; + if (bs.refnc == 0) { + # string ByteSource or completed or error + if (bs.err != nil || bs.edata >= bs.lim) { + c <-= bs; + continue mainloop; + } + continue; + } + # netcon based bytesource + if ((bs.hdr != nil && !bs.seenhdr && bs.hdr.mtype != UnknownType) || bs.edata > bs.lim) { + c <-= bs; + continue mainloop; + } + } + + if(dbgproto) + sys->print("Waitpending\n"); + waitpending = (bsl, c) :: waitpending; + + NGfreebs => + # goproc is finished with bs. + bs := hd bsl; + + if(dbgproto) + sys->print("Freebs BS=%d\n", bs.id); + nc = bs.net; + bs.refgo = 0; + if(bs.refnc == 0) { + bs.free(); + if(nc != nil) + nc.queue[nc.gocur] = nil; + } + if(nc != nil) { + # can be nil if no transport was found + nc.gocur++; + if(dbgproto) + sys->print("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) + nc.makefree(); + } + } + # don't need to check waitpending fro NGwait requests involving bs + # the only thread doing a freebs() should be the only thread that + # can do a waitreq() on the same bs. Same thread cannot be in both states. + + c <-= nil; + + NGstatechg => + # Some runnetconn is telling us tht it changed the + # state of nc. Send a nil along c to let it continue. + bs : ref ByteSource; + if(dbgproto) + sys->print("Statechg NC=%d, state=%s\n", + nc.id, ncstatenames[nc.state]); + sendtopending : ref ByteSource = nil; + pendingchan : chan of ref ByteSource; + if(waitpending != nil && nc.gocur < nc.qlen) { + bs = nc.queue[nc.gocur]; + if(dbgproto) { + totlen := 0; + if(bs.hdr != nil) + totlen = bs.hdr.length; + sys->print("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 != "") + sys->print (" err=%s\n", bs.err); + } + if(bs.refgo && + (bs.err != "" || + (bs.hdr != nil && !bs.seenhdr) || + (nc.gocur == nc.ngcur && nc.state == NCdone) || + (bs.edata > bs.lim))) { + nwp: list of (list of ref ByteSource, chan of ref ByteSource) = nil; + for (waitlist := waitpending; waitlist != nil; waitlist = tl waitlist) { + (bslist, anschan) := hd waitlist; + if (sendtopending != nil) { + nwp = (bslist, anschan) :: nwp; + continue; + } + for (look := bslist; look != nil; look = tl look) { + if (bs == hd look) { + sendtopending = bs; + pendingchan = anschan; + break; + } + } + if (sendtopending == nil) + nwp = (bslist, anschan) :: nwp; + } + waitpending = nwp; + } + } + if(nc.state == NCdone || nc.state == NCerr) { + if(dbgproto) + sys->print("NC %d: runnetconn finishing\n", nc.id); + assert(nc.ngcur < nc.qlen); + bs = nc.queue[nc.ngcur]; + bs.refnc = 0; + if(bs.refgo == 0) { + bs.free(); + nc.queue[nc.ngcur] = nil; + } + nc.ngcur++; + if(dbgproto) + sys->print("NC %d: ngcur=%d\n", nc.id, nc.ngcur); + nc.state = NCidle; + if(dbgproto) + sys->print("NC %d: idle\n", nc.id); + if(nc.ngcur < nc.qlen) { + if(nc.connected) { + nc.state = NCgethdr; + if(dbgproto) + sys->print("NC %d: starting runnetconn in gethdr state\n", nc.id); + } + else { + nc.state = NCconnect; + if(dbgproto) + sys->print("NC %d: starting runnetconn in connect state\n", nc.id); + } + (t, nil) := gettransport(nc.scheme); + spawn runnetconn(nc, t); + } + else if(nc.gocur == nc.qlen && !nc.connected) + nc.makefree(); + } + c <-= nil; + if(sendtopending != nil) { + if(dbgproto) + sys->print("Send BS %d to pending waitreq\n", bs.id); + pendingchan <-= sendtopending; + sendtopending = nil; + } + } + } +} + +# A separate thread, to handle ngcur request of transport. +# If nc.gen ever goes < gen, we have aborted this go. +runnetconn(nc: ref Netconn, t: Transport) +{ + ach := chan of ref ByteSource; + retry := 4; +# retry := 0; + err := ""; + + assert(nc.ngcur < nc.qlen); + bs := nc.queue[nc.ngcur]; + + # dummy loop, just for breaking out of in error cases +eloop: + for(;;) { + # Make the connection, if necessary + if(nc.state == NCconnect) { + t->connect(nc, bs); + if(bs.err != "") { + if (retry) { + retry--; + bs.err = ""; + sys->sleep(100); + continue eloop; + } + break eloop; + } + nc.state = NCgethdr; + } + assert(nc.state == NCgethdr && nc.connected); + if(nc.scheme == "https") + G->progress <-= (bs.id, G->Psslconnected, 0, ""); + else + G->progress <-= (bs.id, G->Pconnected, 0, ""); + + t->writereq(nc, bs); + nc.reqsent++; + if (bs.err != "") { + if (retry) { + retry--; + bs.err = ""; + nc.state = NCconnect; + sys->sleep(100); + continue eloop; + } + break eloop; + } + # managed to write the request + # do not retry if we are doing form POSTs + # See RFC1945 section 12.2 "Safe Methods" + if (bs.req.method == HPost) + retry = 0; + + # Get the header + t->gethdr(nc, bs); + if(bs.err != "") { + if (retry) { + retry--; + bs.err = ""; + nc.state = NCconnect; + sys->sleep(100); + continue eloop; + } + break eloop; + } + assert(bs.hdr != nil); + G->progress <-= (bs.id, G->Phavehdr, 0, ""); + + nc.state = NCgetdata; + + # read enough data to guess media type + while (bs.hdr.mtype == UnknownType && ncgetdata(t, nc, bs)) + bs.hdr.setmediatype(bs.hdr.actual.path, bs.data[:bs.edata]); + if (bs.hdr.mtype == UnknownType) { + bs.hdr.mtype = TextPlain; + bs.hdr.chset = "utf8"; + } + ngchan <-= (NGstatechg,nil,nc,ach); + <- ach; + while (ncgetdata(t, nc, bs)) { + ngchan <-= (NGstatechg,nil,nc,ach); + <- ach; + } + nc.state = NCdone; + G->progress <-= (bs.id, G->Phavedata, 100, ""); + break; + } + if(bs.err != "") { + nc.state = NCerr; + nc.connected = 0; + G->progress <-= (bs.id, G->Perr, 0, bs.err); + } + bs.eof = 1; + ngchan <-= (NGstatechg, nil, nc, ach); + <- ach; +} + +ncgetdata(t: Transport, nc: ref Netconn, bs: ref ByteSource): int +{ + hdr := bs.hdr; + if (bs.data == nil) { + blen := hdr.length; + if (blen <= 0) { + if(hdr.code == HCOk || hdr.code == HCOkNonAuthoritative) + blen = UBufsize; + else + blen = UEBufsize; + } + bs.data = array[blen] of byte; + } + nr := 0; + if (hdr.length > 0) { + if (bs.edata == hdr.length) + return 0; + nr = t->getdata(nc, bs); + if (nr <= 0) + return 0; + } else { + # don't know data length - keep growing input buffer as needed + if (bs.edata == len bs.data) { + nd := array [2*len bs.data] of byte; + nd[:] = bs.data; + bs.data = nd; + } + nr = t->getdata(nc, bs); + if (nr <= 0) { + # assume EOF + bs.data = bs.data[0:bs.edata]; + bs.err = ""; + hdr.length = bs.edata; + nc.connected = 0; + return 0; + } + } + bs.edata += nr; + G->progress <-= (bs.id, G->Phavedata, 100*bs.edata/len bs.data, ""); + return 1; +} + +Netconn.new(id: int) : ref Netconn +{ + return ref Netconn( + id, # id + "", # host + 0, # port + "", # scheme + sys->Connection(nil, nil, ""), # conn + nil, # ssl context + 0, # undetermined ssl version + NCfree, # state + array[10] of ref ByteSource, # queue + 0, # qlen + 0,0,0, # gocur, ngcur, reqsent + 0, # pipeline + 0, # connected + 0, # tstate + nil, # tbuf + 0 # idlestart + ); +} + +Netconn.makefree(nc: self ref Netconn) +{ + if(dbgproto) + sys->print("NC %d: free\n", nc.id); + nc.state = NCfree; + nc.host = ""; + nc.conn.dfd = nil; + nc.conn.cfd = nil; + nc.conn.dir = ""; + nc.qlen = 0; + nc.gocur = 0; + nc.ngcur = 0; + nc.reqsent = 0; + nc.pipeline = 0; + nc.connected = 0; + nc.tstate = 0; + nc.tbuf = nil; + for(i := 0; i < len nc.queue; i++) + nc.queue[i] = nil; +} + +ByteSource.free(bs: self ref ByteSource) +{ + if(dbgproto) + sys->print("BS %d freed\n", bs.id); + if(bs.err == "") + G->progress <-= (bs.id, G->Pdone, 100, ""); + else + G->progress <-= (bs.id, G->Perr, 0, bs.err); + bs.req = nil; + bs.hdr = nil; + bs.data = nil; + bs.err = ""; + bs.net = nil; +} + +# Return an ByteSource that is completely filled, from string s +ByteSource.stringsource(s: string) : ref ByteSource +{ + a := array of byte s; + n := len a; + hdr := ref Header( + HCOk, # code + nil, # actual + nil, # base + nil, # location + n, # length + TextHtml, # mtype + "utf8", # chset + "", # msg + "", # refresh + "", # chal + "", # warn + "" # last-modified + ); + bs := ref ByteSource( + bytesourceid++, + nil, # req + hdr, # hdr + a, # data + n, # edata + "", # err + nil, # net + 1, # refgo + 0, # refnc + 1, # eof - edata is final + 0, # lim + 1 # seenhdr + ); + return bs; +} + +MaskedImage.free(mim: self ref MaskedImage) +{ + mim.im = nil; + mim.mask = nil; +} + +CImage.new(src: ref U->Parsedurl, lowsrc: ref U->Parsedurl, width, height: int) : ref CImage +{ + return ref CImage(src, lowsrc, nil, strhash(src.host + "/" + src.path), width, height, nil, nil, 0); +} + +# 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.) +# +# BUG: the width/height matching code isn't right. If one has width and height +# specified, and the other doesn't, should say "don't match", because the unspecified +# one should come in at its natural size. But we overwrite the width and height fields +# when the actual size comes in, so we can't tell whether width and height are nonzero +# because they were specified or because they're their natural size. +CImage.match(a: self ref CImage, b: ref CImage) : int +{ + 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 in image memory used +# by ci. +CImage.bytes(ci: self ref CImage) : int +{ + tot := 0; + for(i := 0; i < len ci.mims; i++) { + mim := ci.mims[i]; + dim := mim.im; + if(dim != nil) + tot += ((dim.r.max.x-dim.r.min.x)*dim.depth/8) * + (dim.r.max.y-dim.r.min.y); + dim = mim.mask; + if(dim != nil) + tot += ((dim.r.max.x-dim.r.min.x)*dim.depth/8) * + (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. +ImageCache.init(ic: self ref ImageCache) +{ + ic.imhd = nil; + ic.imtl = nil; + ic.n = 0; + ic.memused = 0; + ic.resetlimits(); +} + +# Call resetlimits when amount of non-image-cache image +# memory might have changed significantly (e.g., on main window resize). +ImageCache.resetlimits(ic: self ref ImageCache) +{ + res := ResourceState.cur(); + avail := res.imagelim - (res.image-ic.memused); + # (res.image-ic.memused) is used memory not in image cache + avail = 8*avail/10; # allow 20% slop for other applications, etc. + ic.memlimit = config.imagecachemem; + if(ic.memlimit > avail) + ic.memlimit = avail; +# ic.nlimit = config.imagecachenum; + ic.nlimit = 10000; # let's try this + ic.need(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) +ImageCache.look(ic: self ref ImageCache, ci: ref CImage) : ref CImage +{ + ans : ref CImage = nil; + prev : ref CImage = nil; + for(i := ic.imhd; i != nil; i = i.next) { + if(i.match(ci)) { + if(ic.imtl != i) { + # remove from current place in cache chain + # and put at tail + if(prev != nil) + prev.next = i.next; + else + ic.imhd = i.next; + i.next = nil; + ic.imtl.next = i; + ic.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 ic.memused. +# Assume ic.need has been called to ensure that neither +# memlimit nor nlimit will be exceeded. +ImageCache.add(ic: self ref ImageCache, ci: ref CImage) +{ + ci.next = nil; + if(ic.imhd == nil) + ic.imhd = ci; + else + ic.imtl.next = ci; + ic.imtl = ci; + ic.memused += ci.bytes(); + ic.n++; +} + +# Delete least-recently-used image in image cache +# and update memused and n. +ImageCache.deletelru(ic: self ref ImageCache) +{ + ci := ic.imhd; + if(ci != nil) { + ic.imhd = ci.next; + if(ic.imhd == nil) { + ic.imtl = nil; + ic.memused = 0; + } + else + ic.memused -= ci.bytes(); + for(i := 0; i < len ci.mims; i++) + ci.mims[i].free(); + ci.mims = nil; + ic.n--; + } +} + +ImageCache.clear(ic: self ref ImageCache) +{ + while(ic.imhd != nil) + ic.deletelru(); +} + +# Call this just before allocating an Image that will used nbytes +# of image memory, to ensure that if the image were to be +# added to the image cache then memlimit and nlimit will be ok. +# LRU images will be shed if necessary. +# Return 0 if it will be impossible to make enough memory. +ImageCache.need(ic: self ref ImageCache, nbytes: int) : int +{ + while(ic.n >= ic.nlimit || ic.memused+nbytes > ic.memlimit) { + if(ic.imhd == nil) + return 0; + ic.deletelru(); + } + return 1; +} + +strhash(s: string) : int +{ + prime: con 8388617; + hash := 0; + n := len s; + for(i := 0; i < n; i++) { + hash = hash % prime; + hash = (hash << 7) + s[i]; + } + return hash; +} + +schemeid(s: string): int +{ + for (i := 0; i < len schemes; i++) { + (n, id) := schemes[i]; + if (n == s) + return id; + } + return -1; +} + +schemeok(s: string): int +{ + return schemeid(s) != -1; +} + +gettransport(scheme: string) : (Transport, string) +{ + err := ""; + transport: Transport = nil; + tindex := schemeid(scheme); + if (tindex == -1) + return (nil, "Unknown scheme"); + transport = transports[tindex]; + if (transport == nil) { + transport = load Transport loadpath(tpaths[tindex]); + if(transport == nil) + return (nil, sys->sprint("Can't load transport %s: %r", tpaths[tindex])); + transport->init(me); + transports[tindex] = transport; + } + return (transport, err); +} + +# Return new Header with default values for fields +Header.new() : ref Header +{ + return ref Header( + HCOk, # code + nil, # actual + nil, # base + nil, # location + -1, # length + UnknownType, # mtype + nil, # chset + "", # msg + "", # refresh + "", # chal + "", # warn + "" # last-modified + ); +} + +jpmagic := array[] of {byte 16rFF, byte 16rD8, byte 16rFF, byte 16rE0, + byte 0, byte 0, byte 'J', byte 'F', byte 'I', byte 'F', byte 0}; +pngsig := array[] of { byte 137, byte 80, byte 78, byte 71, byte 13, byte 10, byte 26, byte 10 }; + +# 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 unambigous (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.) +Header.setmediatype(h: self ref Header, name: string, first: array of byte) +{ + # Look for key signatures at beginning of file (perhaps after whitespace) + n := len first; + mt := UnknownType; + for(i := 0; i < n; i++) + if(ctype[int first[i]] != C->W) + break; + if(n - i >= 6) { + s := string first[i:i+6]; + case S->tolower(s) { + "<html " or "<html\t" or "<html>" or "<head>" or "<title" => + mt = TextHtml; + "<!doct" => + if(n - i >= 14 && string first[i+6:i+14] == "ype html") + mt = TextHtml; + "gif87a" or "gif89a" => + if(i == 0) + mt = ImageGif; + "#defin" => + # perhaps should check more definitively... + mt = ImageXXBitmap; + } + + if (mt == UnknownType && n > 0) { + if (first[0] == jpmagic[0] && n >= len jpmagic) { + for(i++; i<len jpmagic; i++) + if(jpmagic[i]>byte 0 && first[i]!=jpmagic[i]) + break; + if (i == len jpmagic) + mt = ImageJpeg; + } else if (first[0] == pngsig[0] && n >= len pngsig) { + for(i++; i<len pngsig; i++) + if (first[i] != pngsig[i]) + break; + if (i == len pngsig) + mt = ImagePng; + } + } + } + + if(mt == UnknownType) { + # Try file name extension + (nil, file) := S->splitr(name, "/"); + if(file != "") { + (f, ext) := S->splitr(file, "."); + if(f != "" && ext != "") { + (fnd, val) := T->lookup(fileexttable, S->tolower(ext)); + if(fnd) + mt = val; + } + } + } + +# if(mt == UnknownType) { +# mt = TextPlain; +# h.chset = "utf8"; +# } + h.mtype = mt; +} + +Header.print(h: self ref Header) +{ + mtype := "?"; + if(h.mtype >= 0 && h.mtype < len mnames) + mtype = mnames[h.mtype]; + chset := "?"; + if(h.chset != nil) + chset = h.chset; + # sys->print("code=%d (%s) length=%d mtype=%s chset=%s\n", + # h.code, hcphrase(h.code), h.length, mtype, chset); + if(h.base != nil) + sys->print(" base=%s\n", h.base.tostring()); + if(h.location != nil) + sys->print(" location=%s\n", h.location.tostring()); + if(h.refresh != "") + sys->print(" refresh=%s\n", h.refresh); + if(h.chal != "") + sys->print(" chal=%s\n", h.chal); + if(h.warn != "") + sys->print(" warn=%s\n", h.warn); +} + + +mfd : ref sys->FD = nil; +ResourceState.cur() : ResourceState +{ + ms := sys->millisec(); + main := 0; + mainlim := 0; + heap := 0; + heaplim := 0; + image := 0; + imagelim := 0; + if(mfd == nil) + mfd = sys->open("/dev/memory", sys->OREAD); + if (mfd == nil) + raisex(sys->sprint("can't open /dev/memory: %r")); + + sys->seek(mfd, big 0, Sys->SEEKSTART); + + buf := array[400] of byte; + n := sys->read(mfd, buf, len buf); + if (n <= 0) + raisex(sys->sprint("can't read /dev/memory: %r")); + + (nil, l) := sys->tokenize(string buf[0:n], "\n"); + # p->cursize, p->maxsize, p->hw, p->nalloc, p->nfree, p->nbrk, poolmax(p), p->name) + while(l != nil) { + s := hd l; + cur_size := int s[0:12]; + max_size := int s[12:24]; + case s[7*12:] { + "main" => + main = cur_size; + mainlim = max_size; + "heap" => + heap = cur_size; + heaplim = max_size; + "image" => + image = cur_size; + imagelim = max_size; + } + l = tl l; + } + + return ResourceState(ms, main, mainlim, heap, heaplim, image, imagelim); +} + +ResourceState.since(rnew: self ResourceState, rold: ResourceState) : ResourceState +{ + return (rnew.ms - rold.ms, + rnew.main - rold.main, + rnew.heaplim, + rnew.heap - rold.heap, + rnew.heaplim, + rnew.image - rold.image, + rnew.imagelim); +} + +ResourceState.print(r: self ResourceState, msg: string) +{ + sys->print("%s:\n\ttime: %d.%#.3ds; memory: main %dk, mainlim %dk, heap %dk, heaplim %dk, image %dk, imagelim %dk\n", + msg, r.ms/1000, r.ms % 1000, r.main / 1024, r.mainlim / 1024, + r.heap / 1024, r.heaplim / 1024, r.image / 1024, r.imagelim / 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 tuple contains: +# (use, error, challenge, redir) +# and action to do is: +# If use==1, use the entity else drain its byte source. +# If error != nil, mesg was put in progress bar +# If challenge != nil, get auth info and make new request with auth +# Else if redir != nil, make a new request with redir for url +# +# (if challenge or redir is non-nil, use will be 0) +hdraction(bs: ref ByteSource, ismain: int, nredirs: int) : (int, string, string, ref U->Parsedurl) +{ + use := 1; + error := ""; + challenge := ""; + redir : ref U->Parsedurl = nil; + + h := bs.hdr; + assert(h != nil); + bs.seenhdr = 1; + code := h.code; + case code/100 { + HSOk => + if(code != HCOk) + error = "unexpected code: " + hcphrase(code); + HSRedirect => + if(h.location != nil) { + redir = h.location; + # spec says url should be absolute, but some + # sites give relative ones + if(redir.scheme == nil) + redir = U->mkabs(redir, h.base); + if(dbg) + sys->print("redirect %s to %s\n", h.actual.tostring(), redir.tostring()); + if(nredirs >= Maxredir) { + redir = nil; + error = "probable redirect loop"; + } + else + use = 0; + } + HSError => + if(code == HCUnauthorized && h.chal != "") { + challenge = h.chal; + use = 0; + } + else { + error = hcphrase(code); + use = ismain; + } + HSServererr => + error = hcphrase(code); + use = ismain; + * => + error = "unexpected code: " + string code; + use = 0; + + } + if(error != "") + G->progress <-= (bs.id, G->Perr, 0, error); + return (use, error, challenge, redir); +} + +# Use event when only care about time stamps on events +event(s: string, data: int) +{ + sys->print("%s: %d %d\n", s, sys->millisec()-startres.ms, data); +} + +kill(pid: int, dogroup: int) +{ + msg : array of byte; + if(dogroup) + msg = array of byte "killgrp"; + else + msg = array of byte "kill"; + ctl := sys->open("#p/" + string pid + "/ctl", sys->OWRITE); + if(ctl != nil) + if (sys->write(ctl, msg, len msg) < 0) + sys->print("charon: kill write failed (pid %d, grp %d): %r\n", pid, dogroup); +} + +# Read a line up to and including cr/lf (be tolerant and allow missing cr). +# Look first in buf[bstart:bend], and if that isn't sufficient to get whole line, +# refill buf from fd as needed. +# Return values: +# array of byte: the line, not including cr/lf +# eof, true if there was no line to get or a read error +# bstart', bend': new valid portion of buf (after cr/lf). +getline(fd: ref sys->FD, buf: array of byte, bstart, bend: int) : + (array of byte, int, int, int) +{ + ans : array of byte = nil; + last : array of byte = nil; + eof := 0; +mainloop: + for(;;) { + for(i := bstart; i < bend; i++) { + if(buf[i] == byte '\n') { + k := i; + if(k > bstart && buf[k-1] == byte '\r') + k--; + last = buf[bstart:k]; + bstart = i+1; + break mainloop; + } + } + if(bend > bstart) + ans = append(ans, buf[bstart:bend]); + last = nil; + bstart = 0; + bend = sys->read(fd, buf, len buf); + if(bend <= 0) { + eof = 1; + bend = 0; + break mainloop; + } + } + return (append(ans, last), eof, bstart, bend); +} + +# Append copy of second array to first, return (possibly new) +# address of the concatenation. +append(a: array of byte, b: array of byte) : array of byte +{ + if(b == nil) + return a; + na := len a; + nb := len b; + ans := realloc(a, nb); + ans[na:] = b; + return ans; +} + +# Return copy of a, but incr bytes bigger +realloc(a: array of byte, incr: int) : array of byte +{ + n := len a; + newa := array[n + incr] of byte; + if(a != nil) + newa[0:] = a; + return newa; +} + +# Look (linearly) through a for s; return its index if found, else -1. +strlookup(a: array of string, s: string) : int +{ + n := len a; + for(i := 0; i < n; i++) + if(s == a[i]) + return i; + return -1; +} + +# Set up config global to defaults, then try to read user-specifiic +# config data from /usr/<username>/charon/config, then try to +# override from command line arguments. +setconfig(argl: list of string) +{ + # Defaults, in absence of any other information + config.userdir = ""; + config.srcdir = "/appl/cmd/charon"; + config.starturl = "file:/services/webget/start.html"; + config.homeurl = config.starturl; + config.change_homeurl = 1; + config.helpurl = "file:/services/webget/help.html"; + config.usessl = SSLV3; # was NOSSL + config.devssl = 0; + config.custbkurl = "/services/config/bookmarks.html"; + config.dualbkurl = "/services/config/dualdisplay.html"; + config.httpproxy = nil; + config.noproxydoms = nil; + config.buttons = "help,resize,hide,exit"; + config.framework = "all"; + config.defaultwidth = 640; + config.defaultheight = 480; + config.x = -1; + config.y = -1; + config.nocache = 0; + config.maxstale = 0; + config.imagelvl = ImgFull; + config.imagecachenum = 120; + config.imagecachemem = 100000000; # 100Meg, will get lowered later + config.docookies = 1; + config.doscripts = 1; + config.httpminor = 0; + config.agentname = "Mozilla/4.08 (Charon; Inferno)"; + config.nthreads = 4; + config.offersave = 1; + config.charset = "windows-1252"; + config.plumbport = "web"; + config.wintitle = "Charon"; # tkclient->titlebar() title, used by GUI + config.dbgfile = ""; + config.dbg = array[128] of { * => byte 0 }; + + # Reading default config file + readconf("/services/config/charon.cfg"); + + # Try reading user config file + user := ""; + fd := sys->open("/dev/user", sys->OREAD); + if(fd != nil) { + b := array[40] of byte; + n := sys->read(fd, b, len b); + if(n > 0) + user = string b[0:n]; + } + if(user != "") { + config.userdir = "/usr/" + user + "/charon"; + readconf(config.userdir + "/config"); + } + + if(argl == nil) + return; + # Try command line arguments + # All should be 'key=val' or '-key' or '-key val', except last which can be url to start + for(l := tl argl; l != nil; l = tl l) { + s := hd l; + if(s == "") + continue; + if (s[0] != '-') + break; + a := s[1:]; + b := ""; + if(tl l != nil) { + b = hd tl l; + if(S->prefix("-", b)) + b = ""; + else + l = tl l; + } + if(!setopt(a, b)) { + if (b != nil) + s += " "+b; + sys->print("couldn't set option from arg '%s'\n", s); + } + } + if(l != nil) { + if (tl l != nil) + # usage error + sys->print("too many URL's\n"); + else + if(!setopt("starturl", hd l)) + sys->print("couldn't set starturl from arg '%s'\n", hd l); + } +} + +readconf(fname: string) +{ + cfgio := sys->open(fname, sys->OREAD); + if(cfgio != nil) { + buf := array[sys->ATOMICIO] of byte; + i := 0; + j := 0; + aline : array of byte; + eof := 0; + for(;;) { + (aline, eof, i, j) = getline(cfgio, buf, i, j); + if(eof) + break; + line := string aline; + if(len line == 0 || line[0]=='#') + continue; + (key, val) := S->splitl(line, " \t="); + if(key != "") { + val = S->take(S->drop(val, " \t="), "^#\r\n"); + if(!setopt(key, val)) + sys->print("couldn't set option from line '%s'\n", line); + } + } + } +} + +# Set config option named 'key' to val, returning 1 if OK +setopt(key: string, val: string) : int +{ + ok := 1; + if(val == "none") + val = ""; + v := int val; + case key { + "userdir" => + config.userdir = val; + "srcdir" => + config.srcdir = val; + "starturl" => + if(val != "") + config.starturl = val; + else + ok = 0; + "change_homeurl" => + config.change_homeurl = v; + "homeurl" => + if(val != "") + if(config.change_homeurl) { + config.homeurl = val; + # order dependent + config.starturl = config.homeurl; + } + else + ok = 0; + "helpurl" => + if(val != "") + config.helpurl = val; + else + ok = 0; + "usessl" => + if(val == "v2") + config.usessl |= SSLV2; + if(val == "v3") + config.usessl |= SSLV3; + "devssl" => + if(v == 0) + config.devssl = 0; + else + config.devssl = 1; +# "custbkurl" => +# "dualbkurl" => + "httpproxy" => + if(val != "") + config.httpproxy = makeabsurl(val); + else + config.httpproxy = nil; + "noproxy" or "noproxydoms" => + (nil, config.noproxydoms) = sys->tokenize(val, ";, \t"); + "buttons" => + config.buttons = S->tolower(val); + "framework" => + config.framework = S->tolower(val); + "defaultwidth" or "width" => + if(v > 200) + config.defaultwidth = v; + else + ok = 0; + "defaultheight" or "height" => + if(v > 100) + config.defaultheight = v; + else + ok = 0; + "x" => + config.x = v; + "y" => + config.y = v; + "nocache" => + config.nocache = v; + "maxstale" => + config.maxstale = v; + "imagelvl" => + config.imagelvl = v; + "imagecachenum" => + config.imagecachenum = v; + "imagecachemem" => + config.imagecachemem = v; + "docookies" => + config.docookies = v; + "doscripts" => + config.doscripts = v; + "http" => + if(val == "1.1") + config.httpminor = 1; + else + config.httpminor = 0; + "agentname" => + config.agentname = val; + "nthreads" => + if (v < 1) + ok = 0; + else + config.nthreads = v; + "offersave" => + if (v < 1) + config.offersave = 0; + else + config.offersave = 1; + "charset" => + config.charset = val; + "plumbport" => + config.plumbport = val; + "wintitle" => + config.wintitle = val; + "dbgfile" => + config.dbgfile = val; + "dbg" => + for(i := 0; i < len val; i++) { + c := val[i]; + if(c < len config.dbg) + config.dbg[c]++; + else { + ok = 0; + break; + } + } + * => + ok = 0; + } + return ok; +} + +saveconfig(): int +{ + fname := config.userdir + "/config"; + buf := array [Sys->ATOMICIO] of byte; + fd := sys->create(fname, Sys->OWRITE, 8r600); + if(fd == nil) + return -1; + + nbyte := savealine(fd, buf, "# Charon user configuration\n", 0); + nbyte = savealine(fd, buf, "userdir=" + config.userdir + "\n", nbyte); + nbyte = savealine(fd, buf, "srcdir=" + config.srcdir +"\n", nbyte); + if(config.change_homeurl){ + nbyte = savealine(fd, buf, "starturl=" + config.starturl + "\n", nbyte); + nbyte = savealine(fd, buf, "homeurl=" + config.homeurl + "\n", nbyte); + } + if(config.httpproxy != nil) + nbyte = savealine(fd, buf, "httpproxy=" + config.httpproxy.tostring() + "\n", nbyte); + if(config.usessl & SSLV23) { + nbyte = savealine(fd, buf, "usessl=v2\n", nbyte); + nbyte = savealine(fd, buf, "usessl=v3\n", nbyte); + } + else { + if(config.usessl & SSLV2) + nbyte = savealine(fd, buf, "usessl=v2\n", nbyte); + if(config.usessl & SSLV3) + nbyte = savealine(fd, buf, "usessl=v3\n", nbyte); + } + if(config.devssl == 0) + nbyte = savealine(fd, buf, "devssl=0\n", nbyte); + else + nbyte = savealine(fd, buf, "devssl=1\n", nbyte); + if(config.noproxydoms != nil) { + doms := ""; + doml := config.noproxydoms; + while(doml != nil) { + doms += hd doml + ","; + doml = tl doml; + } + nbyte = savealine(fd, buf, "noproxy=" + doms + "\n", nbyte); + } + nbyte = savealine(fd, buf, "defaultwidth=" + string config.defaultwidth + "\n", nbyte); + nbyte = savealine(fd, buf, "defaultheight=" + string config.defaultheight + "\n", nbyte); + if(config.x >= 0) + nbyte = savealine(fd, buf, "x=" + string config.x + "\n", nbyte); + if(config.y >= 0) + nbyte = savealine(fd, buf, "y=" + string config.y + "\n", nbyte); + nbyte = savealine(fd, buf, "nocache=" + string config.nocache + "\n", nbyte); + nbyte = savealine(fd, buf, "maxstale=" + string config.maxstale + "\n", nbyte); + nbyte = savealine(fd, buf, "imagelvl=" + string config.imagelvl + "\n", nbyte); + nbyte = savealine(fd, buf, "imagecachenum=" + string config.imagecachenum + "\n", nbyte); + nbyte = savealine(fd, buf, "imagecachemem=" + string config.imagecachemem + "\n", nbyte); + nbyte = savealine(fd, buf, "docookies=" + string config.docookies + "\n", nbyte); + nbyte = savealine(fd, buf, "doscripts=" + string config.doscripts + "\n", nbyte); + nbyte = savealine(fd, buf, "http=" + "1." + string config.httpminor + "\n", nbyte); + nbyte = savealine(fd, buf, "agentname=" + string config.agentname + "\n", nbyte); + nbyte = savealine(fd, buf, "nthreads=" + string config.nthreads + "\n", nbyte); + nbyte = savealine(fd, buf, "charset=" + config.charset + "\n", nbyte); + #for(i := 0; i < len config.dbg; i++) + #nbyte = savealine(fd, buf, "dbg=" + string config.dbg[i] + "\n", nbyte); + + if(nbyte > 0) + sys->write(fd, buf, nbyte); + + return 0; +} + +savealine(fd: ref Sys->FD, buf: array of byte, s: string, n: int): int +{ + if(Sys->ATOMICIO < n + len s) { + sys->write(fd, buf, n); + buf[0:] = array of byte s; + return len s; + } + buf[n:] = array of byte s; + return n + len s; +} + +# Make a StringInt table out of a, mapping each string +# to its index. Check that entries are in alphabetical order. +makestrinttab(a: array of string) : array of T->StringInt +{ + n := len a; + ans := array[n] of T->StringInt; + for(i := 0; i < n; i++) { + ans[i].key = a[i]; + ans[i].val = i; + if(i > 0 && a[i] < a[i-1]) + raisex("EXInternal: table out of alphabetical order"); + } + return ans; +} + +# Should really move into Url module. +# Don't include fragment in test, since we are testing if the +# pointed to docs are the same, not places within docs. +urlequal(a, b: ref U->Parsedurl) : int +{ + return a.scheme == b.scheme + && a.host == b.host + && a.port == b.port + && a.user == b.user + && a.passwd == b.passwd + && a.path == b.path + && a.query == b.query; +} + +# U->makeurl, but add http:// if not an absolute path already +makeabsurl(s: string) : ref Parsedurl +{ + if (s == "") + return nil; + u := U->parse(s); + if (u.scheme != nil) + return u; + if (s[0] == '/') + # try file: + s = "file://localhost" + s; + else + # try http + s = "http://" + s; + u = U->parse(s); + return u; +} + +# Return place to load from, given installed-path name. +# (If config.dbg['u'] is set, change directory to config.srcdir.) +loadpath(s: string) : string +{ + if(config.dbg['u'] == byte 0) + return s; + (nil, f) := S->splitr(s, "/"); + return config.srcdir + "/" + f; +} + +color_tab := array[] of { T->StringInt + ("aqua", 16r00FFFF), + ("black", Black), + ("blue", Blue), + ("fuchsia", 16rFF00FF), + ("gray", 16r808080), + ("green", 16r008000), + ("lime", 16r00FF00), + ("maroon", 16r800000), + ("navy", Navy), + ("olive", 16r808000), + ("purple", 16r800080), + ("red", Red), + ("silver", 16rC0C0C0), + ("teal", 16r008080), + ("white", White), + ("yellow", 16rFFFF00) +}; +# 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 "" or an invalid color. +color(s: string, dflt: int) : int +{ + if(s == "") + return dflt; + s = S->tolower(s); + c := s[0]; + if(c < C->NCTYPE && ctype[c] == C->L) { + (fnd, v) := T->lookup(color_tab, s); + if(fnd) + return v; + } + if(s[0] == '#') + s = s[1:]; + (v, rest) := S->toint(s, 16); + if(rest == "") + return v; + # s was invalid, so choose a valid one + return dflt; +} + +max(a,b: int) : int +{ + if(a > b) + return a; + return b; +} + +min(a,b: int) : int +{ + if(a < b) + return a; + return b; +} + +raisex(e: string) +{ + raise e; +} + +assert(i: int) +{ + if(!i) { + raisex("EXInternal: assertion failed"); +# sys->print("assertion failed\n"); +# s := hmeth[-1]; + } +} + +getcookies(host, path: string, secure: int): string +{ + if (CK == nil || ckclient == nil) + return nil; + Client: import CK; + return ckclient.getcookies(host, path, secure); +} + +setcookie(host, path, cookie: string) +{ + if (CK == nil || ckclient == nil) + return; + Client: import CK; + ckclient.set(host, path, cookie); +} + +ex_mkdir(dirname: string): int +{ + (ok, nil) := sys->stat(dirname); + if(ok < 0) { + f := sys->create(dirname, sys->OREAD, sys->DMDIR + 8r777); + if(f == nil) { + sys->print("mkdir: can't create %s: %r\n", dirname); + return 0; + } + f = nil; + } + return 1; +} + +stripscript(s: string): string +{ + # strip leading whitespace and SGML comment start symbol '<!--' + if (s == nil) + return nil; + cs := "<!--"; + ci := 0; + for (si := 0; si < len s; si++) { + c := s[si]; + if (c == cs[ci]) { + if (++ci >= len cs) + ci = 0; + } else { + ci = 0; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') + continue; + break; + } + } + # strip trailing whitespace and SGML comment terminator '-->' + cs = "-->"; + ci = len cs -1; + for (se := len s - 1; se > si; se--) { + c := s[se]; + if (c == cs[ci]) { + if (ci-- == 0) + ci = len cs -1; + } else { + ci = len cs - 1; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') + continue; + break; + } + } + if (se < si) + return nil; + return s[si:se+1]; +} + +# Split a value (guaranteed trimmed) into sep-separated list of one of +# token +# token = token +# token = "quoted string" +# and put into list of Namevals (lowercase the first token) +Nameval.namevals(s: string, sep: int) : list of Nameval +{ + ans : list of Nameval = nil; + n := len s; + i := 0; + while(i < n) { + tok : string; + (tok, i) = gettok(s, i, n); + if(tok == "") + break; + tok = S->tolower(tok); + val := ""; + while(i < n && ctype[s[i]] == C->W) + i++; + if(i == n || s[i] == sep) + i++; + else if(s[i] == '=') { + i++; + while(i < n && ctype[s[i]] == C->W) + i++; + if (i == n) + break; + if(s[i] == '"') + (val, i) = getqstring(s, i, n); + else + (val, i) = gettok(s, i, n); + } + else + break; + ans = Nameval(tok, val) :: ans; + } + return ans; +} + +gettok(s: string, i,n: int) : (string, int) +{ + while(i < n && ctype[s[i]] == C->W) + i++; + if(i == n) + return ("", i); + is := i; + for(; i < n; i++) { + c := s[i]; + ct := ctype[c]; + if(!(int (ct&(C->D|C->L|C->U|C->N|C->S)))) + if(int (ct&(C->W|C->C)) || S->in(c, "()<>@,;:\\\"/[]?={}")) + break; + } + return (s[is:i], i); +} + +# get quoted string; return it without quotes, and index after it +getqstring(s: string, i,n: int) : (string, int) +{ + while(i < n && ctype[s[i]] == C->W) + i++; + if(i == n || s[i] != '"') + return ("", i); + is := ++i; + for(; i < n; i++) { + c := s[i]; + if(c == '\\') + i++; + else if(c == '"') + return (s[is:i], i+1); + } + return (s[is:i], i); +} + +# Find value corresponding to key (should be lowercase) +# and return (1, value) if found or (0, "") +Nameval.find(l: list of Nameval, key: string) : (int, string) +{ + for(; l != nil; l = tl l) + if((hd l).key == key) + return (1, (hd l).val); + return (0, ""); +} + +# this should be a converter cache +getconv(chset : string) : Btos +{ + (btos, err) := convcs->getbtos(chset); + if (err != nil) + sys->print("Converter error: %s\n", err); + return btos; +} + +X(s, note : string) : string +{ + if (dict == nil) + return s; + return dict.xlaten(s, note); +} |
