parsav  Check-in [c774e2c5a9]

Overview
Comment:wrote mimelib, continued iterating on litepub support; tweets can now be imported into honk
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: c774e2c5a9739cd9fbe1c939815b9a592d7acdcd43e8e612735515034c98fbb7
User & Date: lexi on 2021-01-28 00:51:21
Other Links: manifest | tags
Context
2021-01-28
02:44
add in a bunch of missing pqclears, because i am a *retard*, and wipe out a fuckton of memory leaks check-in: a4e71fdfda user: lexi tags: trunk
00:51
wrote mimelib, continued iterating on litepub support; tweets can now be imported into honk check-in: c774e2c5a9 user: lexi tags: trunk
2021-01-25
14:38
iterating check-in: 050ce7d4fc user: lexi tags: trunk
Changes

Modified api/lp/actor.t from [b336ed6430] to [c96b63a8f1].

     9      9   		"preferredUsername": %$handle,
    10     10   		"name": %$nym,
    11     11   		"summary": %$desc,
    12     12   		"alsoKnownAs": ["https://%+domain/@%+handle"],
    13     13   		"publicKey": {
    14     14   			"id": "%lpid#ident-rsa",
    15     15   			"owner": "%lpid",
    16         -			"publicKeyPem": %rsa
           16  +			"publicKeyPem": %$rsa
    17     17   		},
    18     18   		"icon": {
    19     19   			"type": "Image",
    20     20   			"url": "https://%+domain%+avi"
    21     21   		},
    22     22   		"capabilities": { "acceptsChatMessages": false },
    23     23   		"discoverable": true,
................................................................................
    26     26   		"outbox": "https://%+domain/api/lp/outbox/user/%uid",
    27     27   		"followers": "https://%+domain/api/lp/rel/%uid/followers",
    28     28   		"following": "https://%+domain/api/lp/rel/%uid/following"
    29     29   	}]];
    30     30   }
    31     31   
    32     32   local pstr = lib.str.t
    33         -terra cs(s: rawstring) return pstr {s, lib.str.sz(s)} end
           33  +terra cs(s: rawstring) return pstr {s, lib.trn(s == nil,0,lib.str.sz(s))} end
    34     34   
    35     35   local terra 
    36     36   api_lp_actor(co: &lib.srv.convo, actor: &lib.store.actor)
    37     37   	var lpid = co:stra(64)
    38     38   	lpid:lpush'https://':ppush(co.srv.cfg.domain):lpush'/user/':shpush(actor.id)
    39     39   	var uid = co:stra(32) uid:shpush(actor.id) -- dumb hack bc lazy FIXME
    40     40   
           41  +	var upk = lib.crypt.loadpriv(actor.key)
           42  +	var pem: lib.crypt.pemfile
           43  +
           44  +	if not upk.ok then
           45  +		lib.warn("could not load user's keypair; this is a sign of a bug, a corrupt database, or a problem with mbedtls")
           46  +	else defer upk.val:free()
           47  +		if not lib.crypt.pem(true, &upk.val, &pem[0]) then
           48  +			pem[0] = 0;
           49  +			lib.warn('could not export actor certificate as PEM file; there is a bug, the database is corrupt, or there is a problem in mbedtls')
           50  +		end
           51  +	end
    41     52   	var body = tpl {
    42     53   		domain = co.srv.cfg.domain;
    43     54   		uid = uid:finalize();
    44     55   		lpid = lpid:finalize();
    45     56   		handle = cs(actor.handle);
    46     57   		nym = cs(actor.nym);
    47     58   		desc = cs(actor.bio);
    48     59   		avi = cs(actor.avatar);
    49         -		rsa = '';
           60  +		rsa = cs(&pem[0]);
    50     61   		locked = 'false';
    51     62   	}
    52     63   
    53     64   	co:json(body:poolstr(&co.srv.pool))
           65  +
    54     66   end
    55     67   return api_lp_actor

Modified api/lp/tweet.t from [1ada03554b] to [cbbfbd7e42].

     4      4   local obj = lib.tpl.mk [[{
     5      5   	"\@context": "https://@+domain/s/litepub.jsonld",
     6      6   	"type": "Note",
     7      7   	"id": "https://@+domain/post/@^pid",
     8      8   	"content": @$html,
     9      9   	"source": @$raw,
    10     10   	"attributedTo": "https://@+domain/user/@^uid",
    11         -	"published": "@pubtime"
           11  +	"actor": "https://@+domain/user/@^uid",
           12  +	"published": "@pubtime",
           13  +	"sensitive": false,
           14  +	"directMessage": false,
           15  +	"to": ["https://www.w3.org/ns/activitystreams#Public"],
           16  +	"summary": @$subj
    12     17   	@extra
    13     18   }]]
    14     19   
    15     20   local wrap = lib.tpl.mk [[{
    16     21   	"\@context": "https://@+domain/s/litepub.jsonld",
    17     22   	"type": "@kind",
    18     23   	"actor": "https://@+domain/user/@^uid",
    19     24   	"published": "@pubtime",
    20     25   	"id": "https://@+domain/api/lp/act/@^aid",
           26  +	"to": ["https://www.w3.org/ns/activitystreams#Public"],
    21     27   	"object": @obj
    22     28   }]]
    23     29   
    24     30   local terra 
    25         -lp_tweet(co: &lib.srv.convo, p: &lib.store.post, act_wrap: bool)
    26         -	var opdate = lib.conv.datetime(&co.srv.pool, p.posted)
           31  +lp_tweet(co: &lib.srv.convo, p: &lib.store.post, act_wrap: bool): pstr
           32  +	var opdate = lib.munge.datetime(&co.srv.pool, p.posted)
           33  +
           34  +	var extra: lib.str.acc extra:pool(&co.srv.pool,256)
           35  +	
           36  +	if p.parent ~= 0 then
           37  +		extra:lpush ',"inReplyTo":"'
           38  +		var par = co.srv:post_fetch(p.parent)
           39  +		if not par then
           40  +			lib.warn('database integrity violation: broken parent reference')
           41  +		else defer par:free()
           42  +			if par().localpost then -- gen uri for parent
           43  +				extra:lpush'https://':qpush(co.srv.cfg.domain):lpush'/post/':shpush(p.parent)
           44  +			else extra:push(par().uri,0) end
           45  +		end
           46  +		extra:lpush'"'
           47  +	end
           48  +
           49  +	extra:lpush ',"conversation":"'
           50  +	if p.convoheaduri ~= nil then
           51  +		extra:qpush(p.convoheaduri)
           52  +	else
           53  +		var cid: uint64 = 0
           54  +		if p.parent ~= 0 then
           55  +			var top = co.srv:thread_top_find(p.parent)
           56  +			var tp = co.srv:post_fetch(top)
           57  +			if not tp then
           58  +				lib.warn('database integrity violation: missing thread parent')
           59  +				cid = p.id
           60  +			else
           61  +				if tp().convoheaduri ~= nil then
           62  +					extra:push(tp().convoheaduri,0)
           63  +				elseif tp().localpost == false then
           64  +					extra:push(tp().uri,0)
           65  +				else cid = top end
           66  +			end
           67  +		else
           68  +			cid = p.id
           69  +		end
           70  +		if cid ~= 0 then
           71  +			extra:lpush'https://':qpush(co.srv.cfg.domain)
           72  +			     :lpush'/post/':shpush(cid):lpush'/tree'
           73  +		end
           74  +	end
           75  +	extra:lpush'"'
    27     76   
    28     77   	var tweet = (obj {
    29     78   		domain = co.srv.cfg.domain, uid = p.author, pid = p.id;
    30     79   		html = lib.smackdown.html(&co.srv.pool, p.body, false);
    31         -		raw = p.body, pubtime = opdate, extra = '';
           80  +		raw = p.body, pubtime = opdate, extra = extra:finalize();
           81  +		subj = lib.trn(p.subject ~= nil, pstr(p.subject), pstr'');
    32     82   	}):poolstr(&co.srv.pool)
    33     83   
    34     84   	if act_wrap then
    35     85   		return (wrap {
    36     86   			domain = co.srv.cfg.domain, obj = tweet;
    37     87   			kind = lib.trn(p.rtdby == 0, 'Create', 'Announce');
    38     88   			uid = lib.trn(p.rtdby == 0, p.author, p.rtdby);
    39     89   			aid = lib.trn(p.rtdby == 0, p.id, p.rtact);
    40     90   			pubtime = lib.trn(p.rtdby == 0, opdate,
    41         -				lib.conv.datetime(&co.srv.pool,p.rtdat));
           91  +				lib.munge.datetime(&co.srv.pool,p.rtdat));
    42     92   		}):poolstr(&co.srv.pool)
    43     93   	else
    44     94   		return tweet
    45     95   	end
    46     96   end
    47     97   
    48     98   return lp_tweet

Modified api/webfinger.t from [c64d390bdc] to [06153fd078].

    10     10   			"type": "text/html", "href": @$pfp }
    11     11   	]
    12     12   }]]
    13     13   
    14     14   local terra 
    15     15   webfinger(co: &lib.srv.convo)
    16     16   	var res = co:pgetv('resource')
           17  +	lib.dbg('got webfinger request for resource ', {res.ptr,res.ct})
    17     18   	if (not res) or not res:startswith 'acct:' then goto err end
    18     19   	
    19     20   	var acct = res + 5
    20     21   	var svp = lib.str.find(acct, '@')
    21     22   	if svp:ref() then
    22     23   		acct.ct = (svp.ptr - acct.ptr)
    23     24   		svp:advance(1)

Modified backend/pgsql.t from [18ea7fb9d0] to [d6c61590d0].

   457    457   			where id = $1::bigint
   458    458   		]]
   459    459   	};
   460    460   
   461    461   	post_create = {
   462    462   		params = {
   463    463   			uint64, rawstring, rawstring, rawstring,
   464         -			uint64, uint64, rawstring
          464  +			uint64, uint64, rawstring, rawstring
   465    465   		}, sql = [[
   466    466   			insert into parsav_posts (
   467    467   				author, subject, acl, body,
   468    468   				parent, posted, discovered,
   469         -				circles, mentions, convoheaduri
          469  +				circles, mentions, convoheaduri, uri
   470    470   			) values (
   471    471   				$1::bigint, case when $2::text = '' then null else $2::text end,
   472    472   				$3::text, $4::text, 
   473    473   				$5::bigint, $6::bigint, $6::bigint,
   474         -				array[]::bigint[], array[]::bigint[], $7::text
          474  +				array[]::bigint[], array[]::bigint[], $7::text, $8::text
   475    475   			) returning id
   476    476   		]]; -- TODO array handling
   477    477   	};
   478    478   
   479    479   	post_destroy_prepare = {
   480    480   		params = {uint64}, cmd = true, sql = [[
   481    481   			update parsav_posts set
................................................................................
   502    502   		params = {uint64}, sql = [[
   503    503   			select (p.post).*
   504    504   			from pg_temp.parsavpg_known_content as p
   505    505   				where (p.post).parent = $1::bigint and (p.post).rtdby = 0
   506    506   				order by (p.post).posted, (p.post).discovered asc
   507    507   		]];
   508    508   	};
          509  +
          510  +	thread_top_find = {
          511  +		params = {uint64}, sql = [[
          512  +			with recursive tree(gen,id,par) as (
          513  +				select 0, id, parent from parsav_posts where id = $1::bigint
          514  +			union
          515  +				select tree.gen + 1, p.id, p.parent from tree
          516  +					inner join parsav_posts as p on p.id = tree.par
          517  +			)
          518  +
          519  +			select id from tree order by gen desc limit 1
          520  +		]];
          521  +	};
   509    522   
   510    523   	thread_latest_arrival_calc = {
   511    524   		params = {uint64}, sql = [[
   512    525   			with recursive posts(id) as (
   513    526   				select id from parsav_posts where parent = $1::bigint
   514    527   			union
   515    528   				select p.id from parsav_posts as p
................................................................................
  1022   1035   		if ct == 0 then
  1023   1036   			lib.pq.PQclear(res)
  1024   1037   			return pqr {0, nil}
  1025   1038   		else
  1026   1039   			return pqr {ct, res}
  1027   1040   		end
  1028   1041   	end
         1042  +	q.exec.name = 'pgsql.' .. k .. '.exec'
  1029   1043   end
  1030   1044   
  1031   1045   local terra row_to_artifact(res: &pqr, i: intptr): lib.mem.ptr(lib.store.artifact)
  1032   1046   	var id = res:int(uint64,i,0)
  1033   1047   	var idbuf: int8[lib.math.shorthand.maxlen]
  1034   1048   	var idlen = lib.math.shorthand.gen(id, &idbuf[0])
  1035   1049   	var desc = res:_string(i,2)
................................................................................
  1044   1058   	m.ptr.rid = id
  1045   1059   	return m
  1046   1060   end
  1047   1061   
  1048   1062   local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post)
  1049   1063   	var subj: rawstring, sblen: intptr
  1050   1064   	var cvhu: rawstring, cvhlen: intptr
         1065  +	var uri:  rawstring, urilen: intptr
  1051   1066   	if r:null(row,3)
  1052   1067   		then subj = nil sblen = 0
  1053   1068   		else subj = r:string(row,3) sblen = r:len(row,3)+1
  1054   1069   	end
  1055   1070   	if r:null(row,10)
  1056   1071   		then cvhu = nil cvhlen = 0
  1057   1072   		else cvhu = r:string(row,10) cvhlen = r:len(row,10)+1
  1058   1073   	end
         1074  +	if r:null(row,12)
         1075  +		then uri = nil urilen = 0
         1076  +		else uri = r:string(row,12) urilen = r:len(row,12)+1
         1077  +	end
  1059   1078   	var p = [ lib.str.encapsulate(lib.store.post, {
  1060   1079   		subject = { `subj, `sblen };
  1061   1080   		acl = {`r:string(row,4), `r:len(row,4)+1};
  1062   1081   		body = {`r:string(row,5), `r:len(row,5)+1};
  1063   1082   		convoheaduri = { `cvhu, `cvhlen }; --FIXME
         1083  +		uri = { `uri, `urilen };
  1064   1084   	}) ]
  1065   1085   	p.ptr.id = r:int(uint64,row,1)
  1066   1086   	p.ptr.author = r:int(uint64,row,2)
  1067   1087   	if r:null(row,6)
  1068   1088   		then p.ptr.posted = 0
  1069   1089   		else p.ptr.posted = r:int(uint64,row,6)
  1070   1090   	end
................................................................................
  1077   1097   		else p.ptr.edited = r:int(uint64,row,8)
  1078   1098   	end
  1079   1099   	p.ptr.parent = r:int(uint64,row,9)
  1080   1100   	if r:null(row,11)
  1081   1101   		then p.ptr.chgcount = 0
  1082   1102   		else p.ptr.chgcount = r:int(uint32,row,11)
  1083   1103   	end 
  1084         -	p.ptr.accent = r:int(int16,row,12)
  1085         -	p.ptr.rtdby = r:int(uint64,row,13)
  1086         -	p.ptr.rtdat = r:int(uint64,row,14)
  1087         -	p.ptr.rtact = r:int(uint64,row,15)
  1088         -	p.ptr.likes = r:int(uint32,row,16)
  1089         -	p.ptr.rts = r:int(uint32,row,17)
  1090         -	p.ptr.isreply = r:bool(row,18)
         1104  +	p.ptr.accent = r:int(int16,row,13)
         1105  +	p.ptr.rtdby = r:int(uint64,row,14)
         1106  +	p.ptr.rtdat = r:int(uint64,row,15)
         1107  +	p.ptr.rtact = r:int(uint64,row,16)
         1108  +	p.ptr.likes = r:int(uint32,row,17)
         1109  +	p.ptr.rts   = r:int(uint32,row,18)
         1110  +	p.ptr.isreply = r:bool(row,19)
  1091   1111   	p.ptr.localpost = r:bool(row,0)
  1092   1112   
  1093   1113   	return p
  1094   1114   end
  1095   1115   local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
  1096   1116   	var a: lib.mem.ptr(lib.store.actor)
  1097   1117   	var av: rawstring, avlen: intptr
  1098   1118   	var nym: rawstring, nymlen: intptr
  1099   1119   	var bio: rawstring, biolen: intptr
  1100   1120   	var epi: rawstring, epilen: intptr
         1121  +	var key: &uint8, keylen: intptr
  1101   1122   	var origin: uint64 = 0
  1102   1123   	var handle = r:_string(row, 2)
  1103   1124   	if not r:null(row,3) then origin = r:int(uint64,row,3) end
  1104   1125   
  1105   1126   	var avia = lib.str.acc {buf=nil}
  1106   1127   	if origin == 0 then
  1107   1128   		avia:compose('/avi/',handle)
................................................................................
  1123   1144   		bio = r:string(row,4)
  1124   1145   		biolen = r:len(row,4)+1
  1125   1146   	end
  1126   1147   	if r:null(row,9) then epilen = 0 epi = nil else
  1127   1148   		epi = r:string(row,9)
  1128   1149   		epilen = r:len(row,9)+1
  1129   1150   	end
         1151  +	if r:null(row,8) then
         1152  +		keylen = 0 key = nil
         1153  +	else
         1154  +		var k = r:bin(row,8)
         1155  +		keylen = k.ct key = k.ptr
         1156  +	end
  1130   1157   	a = [ lib.str.encapsulate(lib.store.actor, {
  1131   1158   		nym = {`nym, `nymlen};
  1132   1159   		bio = {`bio, `biolen};
  1133   1160   		epithet = {`epi, `epilen};
  1134   1161   		avatar = {`av,`avlen};
  1135   1162   		handle = {`handle.ptr, `handle.ct + 1};
  1136   1163   		xid = {`r:string(row, 11); `r:len(row,11) + 1};
         1164  +		key = {`key,`keylen};
  1137   1165   	}) ]
  1138   1166   	a.ptr.id = r:int(uint64, row, 0);
  1139   1167   	a.ptr.rights = lib.store.rights_default();
  1140   1168   	a.ptr.rights.rank = r:int(uint16, row, 6);
  1141   1169   	a.ptr.rights.quota = r:int(uint32, row, 7);
  1142   1170   	a.ptr.rights.invites = r:int(uint32, row, 12);
  1143   1171   	a.ptr.knownsince = r:int(int64,row, 10);
  1144   1172   	a.ptr.avatarid = r:int(uint64,row, 13);
  1145         -	if r:null(row,8) then
  1146         -		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
  1147         -	else
  1148         -		a.ptr.key = r:bin(row,8)
  1149         -	end
  1150   1173   	a.ptr.origin = origin
  1151   1174   	if avia.buf ~= nil then avia:free() end
  1152   1175   	return a
  1153   1176   end
  1154   1177   
  1155   1178   local privmap = lib.store.powmap
  1156   1179   
................................................................................
  1464   1487   				blacklist = res:bool(i, 3);
  1465   1488   				pubkey = res:bin(i, 4);
  1466   1489   			}
  1467   1490   		end]
  1468   1491   		if rsakeys.sz > 0 then defer rsakeys:free()
  1469   1492   			for i=0, rsakeys.sz do var props = toprops(&rsakeys, i)
  1470   1493   				lib.dbg('loading next RSA pubkey')
  1471         -				var pub = lib.crypt.loadpub(props.pubkey.ptr, props.pubkey.ct)
         1494  +				var pub = lib.crypt.loadpub(props.pubkey)
  1472   1495   				if pub.ok then defer pub.val:free()
  1473   1496   					lib.dbg('checking pubkey against response')
  1474   1497   					var vfy, secl = lib.crypt.verify(&pub.val, token.ptr, token.ct, sig.ptr, sig.ct)
  1475   1498   					if vfy then
  1476   1499   						lib.dbg('signature verified')
  1477   1500   						if props.blacklist then lib.dbg('key blacklisted!') goto fail end
  1478   1501   						var dupname = lib.str.dup(props.name.ptr)
................................................................................
  1539   1562   
  1540   1563   	post_create = [terra(
  1541   1564   		src: &lib.store.source,
  1542   1565   		post: &lib.store.post
  1543   1566   	): uint64
  1544   1567   		var r = queries.post_create.exec(src,
  1545   1568   			post.author,post.subject,post.acl,post.body,
  1546         -			post.parent,post.posted,post.convoheaduri
         1569  +			post.parent,post.posted,post.convoheaduri,post.uri
  1547   1570   		) 
  1548   1571   		if r.sz == 0 then return 0 end
  1549   1572   		defer r:free()
  1550   1573   		var id = r:int(uint64,0,0)
  1551   1574   		post.source = src
  1552   1575   		return id
  1553   1576   	end];
................................................................................
  2091   2114   		if n.kind == lib.store.noticetype.react then
  2092   2115   			var react = r:_string(0,5)
  2093   2116   			lib.str.ncpy(n.reaction, react.ptr, lib.math.smallest(react.ct,[(`n.reaction).tree.type.N]))
  2094   2117   		end
  2095   2118   
  2096   2119   		return n
  2097   2120   	end];
         2121  +
         2122  +	thread_top_find = [terra(
         2123  +		src: &lib.store.source,
         2124  +		post: uint64
         2125  +	): uint64
         2126  +		var r = queries.thread_top_find.exec(src,post)
         2127  +		if r.sz == 0 then return 0 end
         2128  +		defer r:free()
         2129  +		return r:int(uint64,0,0)
         2130  +	end];
  2098   2131   
  2099   2132   	thread_latest_arrival_calc = [terra(
  2100   2133   		src: &lib.store.source,
  2101   2134   		post: uint64
  2102   2135   	): lib.store.timepoint
  2103   2136   		var r = queries.thread_latest_arrival_calc.exec(src,post)
  2104   2137   		if r.sz == 0 or r:null(0,0) then return 0 end

Modified backend/schema/pgsql-views.sql from [b916bb0a63] to [a4ebd0c2e8].

   106    106   	body		text,
   107    107   	posted		bigint,
   108    108   	discovered	bigint,
   109    109   	edited		bigint,
   110    110   	parent		bigint,
   111    111   	convoheaduri text,
   112    112   	chgcount	integer,
          113  +	uri         text,
   113    114   -- ephemeral
   114    115   	accent		smallint,
   115    116   	rtdby		bigint, -- note that these must be 0 if the record
   116    117   	rtdat		bigint, -- in question does not represent an RT!
   117    118   	rtid		bigint, -- (this one too)
   118    119   	n_likes		integer,
   119    120   	n_rts		integer,
................................................................................
   124    125   pg_temp.parsavpg_translate_post(parsav_posts,bigint,bigint,bigint)
   125    126   returns pg_temp.parsavpg_intern_post as $$
   126    127   	select a.origin is null,
   127    128   		($1).id,     ($1).author,
   128    129   		($1).subject,($1).acl,         ($1).body,
   129    130   		($1).posted, ($1).discovered,  ($1).edited,
   130    131   		($1).parent, ($1).convoheaduri,($1).chgcount,
   131         -		coalesce(c.value, -1)::smallint,
          132  +		($1).uri, coalesce(c.value, -1)::smallint,
   132    133   		$2 as rtdby, $3 as rtdat, $4 as rtid,
   133    134   		re.likes, re.rts,
   134    135   		($1).parent in (select id from parsav_posts)
   135    136   	from parsav_actors as a 
   136    137   		left join parsav_actor_conf_ints as c
   137    138   		          on c.key = 'ui-accent' and
   138    139   		             c.uid = a.id

Modified backend/schema/pgsql.sql from [4c18dab250] to [abc8356ef1].

    59     59   );
    60     60   create index on parsav_rights (actor);
    61     61   comment on table parsav_rights is
    62     62   'a backward-compatible list of every non-default privilege or deprivilege granted to a local user';
    63     63   
    64     64   create table parsav_posts (
    65     65   	id         <def:uniq>,
           66  +	uri        text, -- null if local
    66     67   	author     bigint references parsav_actors(id) on delete cascade,
    67     68   	subject    text,
    68     69   	acl        text not null default 'all', -- just store the script raw 🤷
    69     70   	body       text,
    70     71   	posted     bigint not null,
    71     72   	discovered bigint not null,
    72     73   	chgcount   integer not null default 0,

Added convo.t version [ff68c1adfe].

            1  +-- vim: ft=terra
            2  +local srv = ...
            3  +local pstring = lib.str.t
            4  +
            5  +local struct convo {
            6  +	srv: &srv
            7  +	con: &lib.net.mg_connection
            8  +	msg: &lib.net.mg_http_message
            9  +	aid: uint64 -- 0 if logged out
           10  +	aid_issue: lib.store.timepoint
           11  +	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
           12  +	peer: lib.store.inet
           13  +	reqtype: lib.http.mime.t -- negotiated content type
           14  +	method: lib.http.method.t
           15  +	live_last: lib.store.timepoint
           16  +	uploads: lib.mem.vec(lib.http.upload)
           17  +	body: pstring
           18  +-- cache
           19  +	ui_hue: uint16
           20  +	navbar: pstring
           21  +	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
           22  +-- private
           23  +	varbuf: pstring
           24  +	vbofs: &int8
           25  +}
           26  +
           27  +struct convo.page {
           28  +	title: pstring
           29  +	body: pstring
           30  +	class: pstring
           31  +	cache: bool
           32  +}
           33  +
           34  +local usrdefs = {
           35  +	str = {
           36  +		['acl-follow'    ] = {cfgfld = 'usrdef_pol_follow', fallback = 'local'};
           37  +		['acl-follow-req'] = {cfgfld = 'usrdef_pol_follow_req', fallback = 'all'};
           38  +	};
           39  +}
           40  +
           41  +terra convo:matchmime(mime: lib.http.mime.t): bool
           42  +	return self.reqtype == [lib.http.mime.none]
           43  +		or self.reqtype == mime
           44  +end
           45  +
           46  +terra convo:usercfg_str(uid: uint64, setting: pstring): pstring
           47  +	var set = self.srv:actor_conf_str_get(&self.srv.pool, uid, setting)
           48  +	if not set then
           49  +		[(function()
           50  +			local q = quote return pstring.null() end
           51  +			for key, dfl in pairs(usrdefs.str) do
           52  +				local rv
           53  +				if dfl.cfgfld then
           54  +					rv = quote
           55  +						var cf = self.srv.cfg.[dfl.cfgfld]
           56  +					in terralib.select(not cf, pstring([dfl.fallback]), cf) end
           57  +				elseif dfl.lit then rv = dfl.lit end
           58  +				q = quote
           59  +					if setting:cmp([key]) then return [rv] else [q] end
           60  +				end
           61  +			end
           62  +			return q
           63  +		end)()]
           64  +	else return set end
           65  +end
           66  +
           67  +terra convo:uid2actor_live(uid: uint64)
           68  +	var actor = self.srv:actor_fetch_uid(uid)
           69  +	if actor:ref() then
           70  +		if self.aid ~= 0 and self.who.id ~= uid then
           71  +			actor(0).relationship = self.srv:actor_rel_calc(self.who.id, uid)
           72  +		else -- defensive branch
           73  +			actor(0).relationship = lib.store.relationship {
           74  +				agent = 0, patient = uid;
           75  +				rel   = [lib.store.relation.null],
           76  +				recip = [lib.store.relation.null],
           77  +			}
           78  +		end
           79  +	end
           80  +	return actor
           81  +end
           82  +
           83  +terra convo:uid2actor(uid: uint64)
           84  +	var actor: &lib.store.actor = nil
           85  +	for j = 0, self.actorcache.top do
           86  +		if uid == self.actorcache(j).ptr.id then
           87  +			actor = self.actorcache(j).ptr
           88  +			break
           89  +		end
           90  +	end
           91  +	if actor == nil then
           92  +		actor = self.actorcache:insert(self:uid2actor_live(uid)).ptr
           93  +	end
           94  +	return actor
           95  +end
           96  +
           97  +terra convo:rawpage(code: uint16, pg: convo.page, hdrs: lib.mem.ptr(lib.http.header))
           98  +	var doc = data.view.docskel {
           99  +		instance = self.srv.cfg.instance;
          100  +		title = pg.title;
          101  +		body = pg.body;
          102  +		class = pg.class;
          103  +		navlinks = self.navbar;
          104  +		attr = '';
          105  +	}
          106  +	var attrbuf: int8[32]
          107  +	if self.aid ~= 0 and self.ui_hue ~= 323 then
          108  +		var hdecbuf: int8[21]
          109  +		var hdec = lib.math.decstr(self.ui_hue, &hdecbuf[20])
          110  +		lib.str.cpy(&attrbuf[0], ' style="--hue:')
          111  +		lib.str.cpy(&attrbuf[14], hdec)
          112  +		var len = &hdecbuf[20] - hdec 
          113  +		lib.str.cpy(&attrbuf[14] + len, '"')
          114  +		doc.attr = &attrbuf[0]
          115  +	end
          116  +
          117  +	if self.method == [lib.http.method.head]
          118  +		then doc:head(self.con,code,hdrs)
          119  +		else doc:send(self.con,code,hdrs)
          120  +	end
          121  +end
          122  +
          123  +terra convo:statpage(code: uint16, pg: convo.page)
          124  +	var hdrs = array(
          125  +		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
          126  +		lib.http.header { key = 'Cache-Control', value = 'no-store' }
          127  +	)
          128  +	self:rawpage(code,pg, [lib.mem.ptr(lib.http.header)] {
          129  +		ptr = &hdrs[0];
          130  +		ct = [hdrs.type.N] - lib.trn(pg.cache,1,0);
          131  +	})
          132  +end
          133  +
          134  +terra convo:livepage(pg: convo.page, lastup: lib.store.timepoint)
          135  +	var nbuf: int8[21]
          136  +	var hdrs = array(
          137  +		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
          138  +		lib.http.header { key = 'Cache-Control', value = 'no-store' },
          139  +		lib.http.header {
          140  +			key = 'X-Live-Newest-Artifact';
          141  +			value = lib.math.decstr(lastup, &nbuf[20]);
          142  +		},
          143  +		lib.http.header { key = 'Content-Length', value = '0' }
          144  +	)
          145  +	if self.live_last ~= 0 and self.live_last == lastup then
          146  +		lib.net.mg_printf(self.con, 'HTTP/1.1 %s', lib.http.codestr(200))
          147  +		for i = 0, [hdrs.type.N] do
          148  +			lib.net.mg_printf(self.con, '%s: %s\r\n', hdrs[i].key, hdrs[i].value)
          149  +		end
          150  +		lib.net.mg_printf(self.con, '\r\n')
          151  +	else
          152  +		self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] {
          153  +			ptr = &hdrs[0], ct = 3
          154  +		})
          155  +	end
          156  +end
          157  +
          158  +terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end
          159  +
          160  +terra convo:bytestream_trusted(lockdown: bool, mime: pstring, data: lib.mem.ptr(uint8))
          161  +	var lockhdr = "Content-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\n"
          162  +	if not lockdown then lockhdr = "" end
          163  +	lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\n%sX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2, lockhdr)
          164  +	lib.net.mg_send(self.con, data.ptr, data.ct)
          165  +	lib.net.mg_send(self.con, '\r\n', 2)
          166  +end
          167  +
          168  +terra convo:json(data: pstring)
          169  +	self:bytestream_trusted(false, 'application/activity+json; charset=utf-8', data:blob())
          170  +end
          171  +
          172  +terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8))
          173  +	var ty = lib.mime.lookup(mime)
          174  +	if ty == nil then
          175  +		lib.dbg("mime type ", {mime.ptr,mime.ct}, ' not in database!')
          176  +		mime = 'application/x-octet-stream'
          177  +	else
          178  +		if not ty.safe then
          179  +			lib.dbg("mime type ", {mime.ptr,mime.ct}, ' not safe!')
          180  +			if ty.binary then
          181  +				mime = 'application/x-octet-stream'
          182  +			else
          183  +				mime = 'text/plain'
          184  +			end
          185  +		end
          186  +	end
          187  +	self:bytestream_trusted(true, mime, data)
          188  +end
          189  +
          190  +terra convo:reroute_cookie(dest: rawstring, cookie: rawstring)
          191  +	var hdrs = array(
          192  +		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
          193  +		lib.http.header { key = 'Location',     value = dest },
          194  +		lib.http.header { key = 'Set-Cookie',   value = cookie }
          195  +	)
          196  +
          197  +	var body = data.view.docskel {
          198  +		instance = self.srv.cfg.instance.ptr;
          199  +		title = 'rerouting';
          200  +		body = 'you are being redirected';
          201  +		class = 'error';
          202  +		navlinks = '';
          203  +		attr = '';
          204  +	}
          205  +
          206  +	body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] {
          207  +		ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0)
          208  +	})
          209  +end
          210  +
          211  +terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end
          212  +
          213  +terra convo:installkey(dest: rawstring, aid: uint64)
          214  +	var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
          215  +	do var p = &sesskey[0]
          216  +		p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
          217  +		p = p + lib.session.cookie_gen(self.srv.cfg.secret, aid, lib.osclock.time(nil), p)
          218  +		lib.dbg('sending cookie ',{&sesskey[0],15})
          219  +		p = lib.str.ncpy(p, '; Path=/', 9)
          220  +	end
          221  +	self:reroute_cookie(dest, &sesskey[0])
          222  +end
          223  + 
          224  +terra convo:stra(sz: intptr) -- convenience function
          225  +	var s: lib.str.acc
          226  +	s:pool(&self.srv.pool,sz)
          227  +	return s
          228  +end
          229  +
          230  +convo.methods.qstr = macro(function(self, ...) -- convenience string builder
          231  +	local exp = {...}
          232  +	return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize()
          233  +end)
          234  +
          235  +terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
          236  +	if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end
          237  +
          238  +	if self:matchmime(lib.http.mime.html) then
          239  +		var body = [convo.page] {
          240  +			title = self:qstr('error :: ', title);
          241  +			body = self:qstr('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>');
          242  +			class = 'error';
          243  +			cache = false;
          244  +		}
          245  +
          246  +		self:statpage(code, body)
          247  +	else
          248  +		var pg = lib.http.page { respcode = code, body = pstring.null() }
          249  +		var ctt = lib.http.mime.none
          250  +		if self:matchmime(lib.http.mime.json) then ctt = lib.http.mime.json
          251  +			pg.body = ([lib.tpl.mk'{"_parsav_error":@$ekind, "_parsav_error_desc":@$edesc}']
          252  +				{ekind = title, edesc = msg}):poolstr(&self.srv.pool)
          253  +		elseif self:matchmime(lib.http.mime.text) then ctt = lib.http.mime.text
          254  +			pg.body = self:qstr('error: ',title,'\n',msg)
          255  +		elseif self:matchmime(lib.http.mime.mkdown) then ctt = lib.http.mime.mkdown
          256  +			pg.body = self:qstr('# error :: ',title,'\n\n',msg)
          257  +		elseif self:matchmime(lib.http.mime.ansi) then ctt = lib.http.mime.ansi
          258  +			pg.body = self:qstr('\27[1;31merror :: ',title,'\27[m\n',msg)
          259  +		end
          260  +		var cthdr = lib.http.header { 'Content-Type', 'text/plain' }
          261  +		if ctt == lib.http.mime.none then
          262  +			pg.headers.ct = 0
          263  +		else
          264  +			pg.headers = lib.typeof(pg.headers) { &cthdr, 1 }
          265  +			switch ctt do
          266  +				escape
          267  +					for key,ty in ipairs(lib.mime.types) do
          268  +						if key ~= 'none' and lib.http.mime[key] ~= nil then
          269  +							emit quote case [ctt.type](lib.http.mime.[key]) then cthdr.value = [ty.id[1]] end end
          270  +						end
          271  +					end
          272  +				end
          273  +			end
          274  +		end
          275  +		pg:send(self.con)
          276  +	end
          277  +end
          278  +
          279  +terra convo:fail(code: uint16)
          280  +	switch code do
          281  +		escape
          282  +			local stderrors = {
          283  +				{400, 'bad request', "the action you have attempted on this resource is not meaningful"};
          284  +				{401, 'unauthorized', "this resource is not available at your clearance level"};
          285  +				{403, 'forbidden', "we can neither confirm nor deny the existence of this resource"};
          286  +				{404, 'resource not found', "that resource is not extant on or known to this server"};
          287  +				{405, 'method not allowed', "the method you have attempted on this resource is not meaningful"};
          288  +				{406, 'not acceptable', "none of the suggested content types are a viable representation of this resource"};
          289  +				{500, 'internal server error', "parsav did a fucksy wucksy"};
          290  +			}
          291  +
          292  +			for i,v in ipairs(stderrors) do
          293  +				emit quote case uint16([v[1]]) then
          294  +					self:complain([v])
          295  +				end end
          296  +			end
          297  +		end
          298  +		else self:complain(500,'unknown error','an unrecognized error was thrown. this is a bug')
          299  +	end
          300  +end
          301  +
          302  +terra convo:confirm(title: pstring, msg: pstring, cancel: pstring)
          303  +	var conf = data.view.confirm {
          304  +		title = title;
          305  +		query = msg;
          306  +		cancel = cancel;
          307  +	}
          308  +	var ti: lib.str.acc ti:pcompose(&self.srv.pool,'confirm :: ', title)
          309  +	var body = conf:poolstr(&self.srv.pool) -- defer body:free()
          310  +	var cf = [convo.page] {
          311  +		title = ti:finalize();
          312  +		class = 'query';
          313  +		body = body; cache = false;
          314  +	}
          315  +	self:stdpage(cf)
          316  +	--cf.title:free()
          317  +end
          318  +
          319  +convo.methods.assertpow = macro(function(self, pow)
          320  +	return quote
          321  +		var ok = true
          322  +		if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then
          323  +			ok = false
          324  +			self:complain(403,'insufficient privileges',['you lack the <strong>'..pow:asvalue()..'</strong> power and cannot perform this action'])
          325  +		end
          326  +	in ok end
          327  +end)
          328  +
          329  +local pstr2mg, mg2pstr
          330  +do -- aaaaaaaaaaaaaaaaaaaaaaaa
          331  +	mgstr = lib.util.find(lib.net.mg_http_message.entries, function(v)
          332  +		if v.field == 'body' or v[1] == 'body' then return v.type end
          333  +	end)
          334  +	terra pstr2mg(p: pstring): mgstr
          335  +		return mgstr { ptr = p.ptr, len = p.ct }
          336  +	end
          337  +	terra mg2pstr(m: mgstr): pstring
          338  +		return pstring { ptr = m.ptr, ct = m.len }
          339  +	end
          340  +end
          341  +
          342  +-- CALL ONLY ONCE PER VAR
          343  +terra convo:postv_next(name: pstring, start: &pstring)
          344  +	if self.varbuf.ptr == nil then
          345  +		self.varbuf = self.srv.pool:alloc(int8, self.msg.body.len + self.msg.query.len)
          346  +		self.vbofs = self.varbuf.ptr
          347  +	end
          348  +	var conv = pstr2mg(@start)
          349  +	var o = lib.net.mg_http_get_var(
          350  +		&conv,
          351  +		name.ptr, self.vbofs,
          352  +		self.varbuf.ct - (self.vbofs - self.varbuf.ptr)
          353  +	)
          354  +	if o > 0 then
          355  +		start:advance(name.ct + o + 2)
          356  +		var r = self.vbofs
          357  +		self.vbofs = self.vbofs + o + 1
          358  +		@(self.vbofs - 1) = 0
          359  +		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
          360  +		return norm.ptr, norm.ct
          361  +	else return nil, 0 end
          362  +end
          363  +terra convo:postv(name: pstring)
          364  +	var start = mg2pstr(self.msg.body)
          365  +	return self:postv_next(name, &start)
          366  +end
          367  +terra convo:ppostv(name: pstring)
          368  +	var s,l = self:postv(name)
          369  +	return pstring { ptr = s, ct = l }
          370  +end
          371  +do
          372  +	local struct postiter { co: &convo where: pstring name: pstring }
          373  +	terra convo:eachpostv(name: pstring)
          374  +		return postiter { co = self, where = mg2pstr(self.msg.body), name = name } 
          375  +	end
          376  +	postiter.metamethods.__for = function(self, body)
          377  +		return quote
          378  +			while true do
          379  +				var str, len = self.co:postv_next(self.name, &self.where)
          380  +				if str == nil then break end
          381  +				[ body(`pstring {str, len}) ]
          382  +			end
          383  +		end
          384  +	end
          385  +end
          386  +
          387  +terra convo:getv(name: rawstring)
          388  +	if self.varbuf.ptr == nil then
          389  +		self.varbuf = self.srv.pool:alloc(int8, self.msg.query.len + self.msg.body.len)
          390  +		self.vbofs = self.varbuf.ptr
          391  +	end
          392  +	var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
          393  +	if o > 0 then
          394  +		var r = self.vbofs
          395  +		self.vbofs = self.vbofs + o + 1
          396  +		@(self.vbofs - 1) = 0
          397  +		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
          398  +		return norm.ptr, norm.ct
          399  +	else return nil, 0 end
          400  +end
          401  +terra convo:pgetv(name: rawstring)
          402  +	var s,l = self:getv(name)
          403  +	return pstring { ptr = s, ct = l }
          404  +end
          405  +
          406  +return convo

Modified crypt.t from [a12c25b6dd] to [530b761d29].

    14     14   const.maxdersz = const.maxpemsz -- FIXME this is a safe value but obvs not the correct one
    15     15   
    16     16   local ctx = lib.pk.mbedtls_pk_context
    17     17   terra ctx:free() lib.pk.mbedtls_pk_free(self) end
    18     18   
    19     19   local struct hashalg { id: uint8 bytes: intptr }
    20     20   local m = {
    21         -	pemfile = uint8[const.maxpemsz];
           21  +	pemfile = int8[const.maxpemsz];
           22  +	derfile = uint8[const.maxdersz];
    22     23   	const = const;
    23     24   	algsz = {
    24     25   		sha1 =   160/8;
    25     26   		sha256 = 256/8;
    26     27   		sha512 = 512/8;
    27     28   		sha384 = 384/8;
    28     29   		sha224 = 224/8;
................................................................................
    71     72   		v = v % (to - from) + from -- only works with unsigned!!
    72     73   	in v end
    73     74   end)
    74     75   
    75     76   terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr)
    76     77   	return m.spray(dest,sz) end
    77     78   
    78         -terra m.pem(pub: bool, key: &ctx, buf: &uint8): bool
           79  +terra m.pem(pub: bool, key: &ctx, buf: &int8): bool
    79     80   	if pub then
    80         -		return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0
           81  +		return lib.pk.mbedtls_pk_write_pubkey_pem(key, [&uint8](buf), const.maxpemsz) == 0
    81     82   	else
    82         -		return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0
           83  +		return lib.pk.mbedtls_pk_write_key_pem(key, [&uint8](buf), const.maxpemsz) == 0
    83     84   	end
    84     85   end
    85     86   
    86     87   local binblob = lib.mem.ptr(uint8)
    87     88   terra m.der(pub: bool, key: &ctx, buf: &uint8): binblob
    88     89   	var ofs: ptrdiff
    89     90   	if pub then
................................................................................
   112    113   	lib.pk.mbedtls_pk_setup(&pk, lib.pk.mbedtls_pk_info_from_type(lib.pk.MBEDTLS_PK_RSA))
   113    114   	var rsa = [&lib.rsa.mbedtls_rsa_context](pk.pk_ctx)
   114    115   	lib.rsa.mbedtls_rsa_gen_key(rsa, callbacks.randomize, nil, const.keybits, 65537)
   115    116   
   116    117   	return pk
   117    118   end
   118    119   
   119         -terra m.loadpriv(buf: &uint8, len: intptr): lib.stat(ctx)
          120  +local binblob = lib.mem.ptr(uint8)
          121  +terra m.loadpriv(buf: binblob): lib.stat(ctx)
   120    122   	lib.dbg('parsing saved private key')
   121    123   
   122    124   	var pk: ctx
   123    125   	lib.pk.mbedtls_pk_init(&pk)
   124         -	var rt = lib.pk.mbedtls_pk_parse_key(&pk, buf, len + 1, nil, 0)
          126  +	var rt = lib.pk.mbedtls_pk_parse_key(&pk, buf.ptr, buf.ct, nil, 0)
   125    127   	if rt == 0 then
   126    128   		return [lib.stat(ctx)] { ok = true, val = pk }
   127    129   	else
   128    130   		lib.pk.mbedtls_pk_free(&pk)
   129         -		return [lib.stat(ctx)] { ok = false }
          131  +		return [lib.stat(ctx)] { ok = false, error = rt }
   130    132   	end
   131    133   end
   132    134   
   133         -terra m.loadpub(buf: &uint8, len: intptr): lib.stat(ctx)
          135  +terra m.loadpub(buf: binblob): lib.stat(ctx)
   134    136   	lib.dbg('parsing saved key')
   135    137   
   136    138   	var pk: ctx
   137    139   	lib.pk.mbedtls_pk_init(&pk)
   138         -	var rt = lib.pk.mbedtls_pk_parse_public_key(&pk, buf, len)
          140  +	var rt = lib.pk.mbedtls_pk_parse_public_key(&pk, buf.ptr, buf.ct)
   139    141   	if rt == 0 then
   140    142   		return [lib.stat(ctx)] { ok = true, val = pk }
   141    143   	else
   142    144   		lib.pk.mbedtls_pk_free(&pk)
   143    145   		return [lib.stat(ctx)] { ok = false, error = rt }
   144    146   	end
   145    147   end

Modified mgtool.t from [63607cdb2b] to [aa223ca2fb].

   421    421   					[ lib.emit(false, 1, 'usage: ', `argv[0], ' actor ', umode.type.helptxt.flags, ' <xid> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
   422    422   						{ 'actor <xid> rank <value>', 'set an actor\'s rank to <value> (remote actors cannot exercise rank-related powers, but benefit from rank immunities)' };
   423    423   						{ 'actor <xid> degrade', 'alias for `actor <xid> rank 0`' };
   424    424   						{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
   425    425   						{ 'actor <xid> instantiate', 'instantiate a remote actor, retrieving their profile and posts even if no one follows them' };
   426    426   						{ 'actor <xid> proscribe', 'globally ban an actor from interacting with your server' };
   427    427   						{ 'actor <xid> rehabilitate', 'lift a proscription on an actor' };
          428  +						{ 'actor <xid> xkey [pem|der]', 'extract an actor\'s public key in either PEM or DER form' };
   428    429   						{ 'actor <xid> purge-all <confirm-str>', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' };
   429    430   					}) ]
   430    431   					return 1
   431    432   				end
   432    433   				if umode.arglist.ct >= 2 then
   433    434   					var degrade = lib.str.cmp(umode.arglist(1),'degrade') == 0
   434    435   					var xid = umode.arglist(0)
................................................................................
   467    468   							lib.warn('completely purging actor ', usr.ptr.xid, ' and all related content from database')
   468    469   							dlg:actor_purge_uid(usr.ptr.id)
   469    470   							lib.report('actor purged')
   470    471   						else goto cmderr end
   471    472   					else goto cmderr end
   472    473   				else goto cmderr end
   473    474   			elseif lib.str.cmp(mode.arglist(0),'user') == 0 then
          475  +				if mode.arglist.ct < 3 then goto cmderr end
   474    476   				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
   475    477   				if umode.help then
   476    478   					[ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
   477    479   						{ 'user <handle> create', 'add a new user' };
   478    480   						{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
   479    481   						{ 'user <handle> auth <type> reset', '(where applicable, managed auth only) delete all of a user\'s authentication tokens of the given type and issue a new one' };
   480    482   						{ 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' };
   481    483   						{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
   482    484   						{ 'user <handle> emasculate', 'strip all administrative powers and rank from a user' };
   483    485   						{ 'user <handle> forgive', 'restore all default powers to a user' };
   484    486   						{ 'user <handle> suspend [<timespec>]', '(e.g. \27[1muser jokester suspend 5d 6h 7m 3s\27[m to suspend "jokester" for five days, six hours, seven minutes, and three seconds) suspend a user'};
          487  +						{ 'user <handle> xkey [pem|der]', 'extract an user\'s *private* key in either PEM or DER form' };
   485    488   					}) ]
   486    489   					return 1
   487    490   				end
   488    491   				var handle = umode.arglist(0)
   489    492   				var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)})
   490    493   				if umode.arglist.ct == 2 and lib.str.cmp(umode.arglist(1),'create')==0 then
   491    494   					if usr:ref() then lib.bail('that user already exists') end
................................................................................
   524    527   									end
   525    528   								end
   526    529   							end
   527    530   						end
   528    531   
   529    532   						usr.ptr.rights.powers = newprivs
   530    533   						dlg:actor_save_privs(usr.ptr)
          534  +					elseif lib.str.cmp(umode.arglist(1),'xkey') == 0 and umode.arglist.ct == 3 then
          535  +						if not usr then lib.bail('unknown handle') end
          536  +						if lib.str.cmp(umode.arglist(2),'pem') == 0 then
          537  +							var pk = lib.crypt.loadpriv(usr().key)
          538  +							if not pk.ok then
          539  +								lib.bail('could not parse key! this is probably a bug')
          540  +							end
          541  +							var pem: lib.crypt.pemfile
          542  +							if not lib.crypt.pem(false, &pk.val, &pem[0]) then
          543  +								lib.bail('could not convert key to PEM! this is probably a bug')
          544  +							end
          545  +							lib.io.send(1, pem, lib.str.sz(&pem[0]))
          546  +							pk.val:free()
          547  +						elseif lib.str.cmp(umode.arglist(2),'der') == 0 then
          548  +							-- TODO avoid dumping binary to tty
          549  +							lib.warn('dumping user\'s \x1b[1mprivate\x1b[m key!')
          550  +							lib.io.send(1, [&int8](usr().key.ptr), usr().key.ct)
          551  +						else lib.bail('invalid key format') end
   531    552   					elseif lib.str.cmp(umode.arglist(1),'auth') == 0 and umode.arglist.ct == 4 then
   532    553   						var reset = lib.str.cmp(umode.arglist(3),'reset') == 0
   533    554   						if reset or lib.str.cmp(umode.arglist(3),'new') == 0 then
   534    555   							-- FIXME enable resetting pws for users who have
   535    556   							-- not logged in yet
   536    557   							if not usr then lib.bail('unknown handle') end
   537    558   							if lib.str.cmp(umode.arglist(2),'pw') == 0 then

Modified mime.t from [b6a24abaaf] to [8a0a5cf230].

            1  +-- vim: ft=terra
     1      2   local knowntypes = {
     2         -	['text/csrc'] = {
     3         -		ext = 'c', lang = 'c';
     4         -	};
     5         -	['text/html'] = {
     6         -		ext = 'html', lang = 'html';
     7         -		unsafe = true;
            3  +	html = {
            4  +		ext = 'html', kind = 'markup', unsafe = true, id = {
            5  +			'text/html'; 
            6  +			'application/xhtml+xml';
            7  +			'application/vnd.wap.xhtml+xml';
            8  +		};
     8      9   	};
     9         -	['text/x-lua'] = {
    10         -		ext = 'lua', lang = 'lua';
    11         -	};
    12         -	['text/markdown'] = {
           10  +	flash = { ext = 'swf', kind = 'vm_prog', id = 'application/x-shockwave-flash', unsafe = true, binary = true };
           11  +	java = { ext = 'java', kind = 'vm_prog', id = 'application/java', unsafe = true, binary = true };
           12  +	css = { ext = 'css', kind = 'lang', id = 'text/css'};
           13  +	text = { ext = 'txt', kind = 'text', id = 'text/plain' };
           14  +	c = { ext = 'c', kind = 'prog_lang', id = 'text/csrc' };
           15  +	xml = { ext = 'xml', kind = 'markup', unsafe = true, id = 'text/xml' };
           16  +	lua = { ext = 'lua', kind = 'prog_lang', id = 'text/x-lua' };
           17  +	ansi = { ext = 'ans', kind = 'text', id = 'text/x-ansi', doc = true, binary = true};
           18  +	mkdown = { ext = 'md', kind = 'text', doc = true; id = 'text/markdown';
    13     19   		formatter = 'smackdown';
    14         -		ext = 'md', doc = true;
           20  +	};
           21  +	json = {
           22  +		ext = 'json', kind = 'lang', id = {
           23  +			'application/json';
           24  +			'application/activity+json';
           25  +			'application/ld+json';
           26  +			'application/jrd+json';
           27  +		};
    15     28   	};
           29  +	svg = { ext = 'svg', kind = 'image', id = 'image/svg+xml' };
           30  +	webp = { ext = 'webp', kind = 'image', id = 'image/webp', binary = true };
           31  +	png = { ext = 'png', kind = 'image', id = 'image/png', binary = true };
           32  +	jpeg = { ext = 'jpg', kind = 'image', id = 'image/jpeg', binary = true };
           33  +
           34  +	-- wildcard
           35  +	none = { id = '*/*' };
           36  +}
           37  +
           38  +local idcache = {}
           39  +
           40  +
           41  +local pstr = lib.str.t
           42  +local filekind = lib.enum [[none image text lang prog_lang markup vm_prog]]
           43  +local struct mime {
           44  +	key: pstr
           45  +	canonical: pstr
           46  +	safe: bool
           47  +	binary: bool
           48  +	ext: pstr
           49  +	kind: filekind.t
           50  +	output: lib.http.mime.t
           51  +}
           52  +
           53  +local typestore = {}
           54  +for typecode, ty in pairs(knowntypes) do
           55  +	ty.key = typecode
           56  +	if type(ty.id) == 'string' then ty.id = {ty.id} end
           57  +	for i, mime in ipairs(ty.id) do
           58  +		idcache[mime] = ty
           59  +	end
           60  +
           61  +	local op = lib.http.mime[typecode]
           62  +	if op == nil then op = lib.http.mime.none end
           63  +	print(typecode,op)
           64  +
           65  +	ty.offset = #typestore
           66  +	typestore[#typestore + 1] = `mime {
           67  +		key = typecode;
           68  +		canonical = [ty.id[1]];
           69  +		safe = [not ty.unsafe];
           70  +		ext = [ty.ext or `pstr{nil,0}];
           71  +		kind = [ty.kind and filekind[ty.kind] or filekind.none];
           72  +		binary = [ty.binary or false];
           73  +		output = [op];
           74  +	}
           75  +
           76  +end
           77  +
           78  +local typedex = global(`array([typestore]))
           79  +local struct mimemapping {
           80  +	string: pstr
           81  +	type: &mime
           82  +}
           83  +
           84  +local typemap_l = {}
           85  +for mime, ty in pairs(idcache) do
           86  +	typemap_l[#typemap_l + 1] = `mimemapping {
           87  +		string = mime;
           88  +		type = &typedex[ [ty.offset] ];
           89  +	}
           90  +
           91  +end
           92  +local typemap = global(`array([typemap_l]));
           93  +
           94  +
           95  +return {
           96  +	type = mime;
           97  +	types = knowntypes;
           98  +	tbl = idcache;
           99  +	typedex = typedex;
          100  +	lookup = terra(m: pstr): &mime
          101  +		for i=0, [#typemap_l] do
          102  +			if m:cmp(typemap[i].string) then
          103  +				lib.io.fmt('returning type %s %u\n', typemap[i].type.key, typemap[i].type.output)
          104  +				return typemap[i].type
          105  +			end
          106  +		end
          107  +		return nil
          108  +	end;
    16    109   }

Name change from conv.t to munge.t.


Modified parsav.t from [76fc393228] to [3c7c1240d3].

   254    254   	if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe
   255    255   	elseif #tbl >= 2^16 then ty = uint32
   256    256   	elseif #tbl >= 2^8 then ty = uint16 end
   257    257   	local o = { t = ty, members = tbl }
   258    258   	local strings = {}
   259    259   	for i, name in ipairs(tbl) do
   260    260   		o[name] = `[ty]([i - 1])
   261         -		strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]}
          261  +		strings[i] = `[lib.str.t]{ptr=[name], ct=[#name]}
   262    262   	end
   263    263   	o._str = terra(val: ty)
   264    264   		var l = array([strings])
   265    265   		return l[val]
   266    266   	end
   267    267   	return o
   268    268   end
................................................................................
   440    440   lib.pq = lib.loadlib('libpq','libpq-fe.h')
   441    441   lib.jc = lib.loadlib('json-c','json.h')
   442    442   
   443    443   lib.load {
   444    444   	'mem', 'math', 'str', 'file', 'crypt', 'ipc';
   445    445   	'http', 'html', 'session', 'tpl', 'store', 'acl';
   446    446   
          447  +	'mime'; -- mimetype database & whitelist
   447    448   	'smackdown'; -- md-alike parser
   448         -	'conv'; -- miscellaneous conversion/munging functions
          449  +	'munge'; -- miscellaneous conversion/munging functions
   449    450   }
   450    451   
   451    452   local be = {}
   452    453   for _, b in pairs(config.backends) do
   453    454   	be[#be+1] = terralib.loadfile(string.format('backend/%s.t',b))()
   454    455   end
   455    456   lib.store.backends = global(`array([be]))

Modified route.t from [56f7ddd740] to [1bb0eb41f3].

   282    282   	elseif path.ct == 1 then
   283    283   		lib.render.docpage(co, rstring.null())
   284    284   	else
   285    285   		co:complain(404, 'no such documentation', 'invalid documentation URL')
   286    286   	end
   287    287   end
   288    288   
   289         -terra http.tweet_page(co: &lib.srv.convo, path: hpath, meth: method.t)
          289  +terra http.tweet_page(co: &lib.srv.convo, path: hpath)
   290    290   	var pid, ok = lib.math.shorthand.parse(path(1).ptr, path(1).ct)
   291    291   	if not ok then
   292    292   		co:complain(400, 'bad post ID', 'that post ID is not valid')
   293    293   		return
   294    294   	end
   295    295   	var post = co.srv:post_fetch(pid)
   296    296   	var rt: lib.store.notice
................................................................................
   306    306   	end
   307    307   	defer post:free() -- NOP on null
   308    308   
   309    309   	if path.ct == 3 then
   310    310   		var lnk: lib.str.acc lnk:compose('/post/', path(1))
   311    311   		var lnkp = lnk:finalize() defer lnkp:free()
   312    312   		if post:ref() and path(2):cmp(lib.str.lit 'snitch') then
   313         -			if meth_get(meth) then
          313  +			if meth_get(co.method) then
   314    314   				var ui = data.view.report {
   315    315   					badtweet = lib.render.tweet(co, post.ptr, nil);
   316    316   					clnk = lnkp;
   317    317   				}
   318    318   
   319    319   				co:stdpage([lib.srv.convo.page] {
   320    320   					title = 'post :: report';
................................................................................
   326    326   			end
   327    327   			return
   328    328   		elseif post:ref() and post(0).author ~= co.who.id then
   329    329   			co:complain(403, 'forbidden', 'you cannot alter other people\'s posts')
   330    330   			return
   331    331   		elseif post:ref() and path(2):cmp(lib.str.lit 'edit') then
   332    332   			if not co:assertpow('edit') then return end
   333         -			if meth_get(meth) then
          333  +			if meth_get(co.method) then
   334    334   				lib.render.compose(co, post.ptr, nil)
   335    335   				return
   336         -			elseif meth == method.post then
          336  +			elseif co.method == method.post then
   337    337   				var newbody = co:postv('post')._0
   338    338   				var newacl = co:postv('acl')._0
   339    339   				var newsubj = co:postv('subject')._0
   340    340   				if newbody ~= nil then post(0).body = newbody end
   341    341   				if newacl  ~= nil then post(0).acl = newacl end
   342    342   				if newsubj ~= nil then post(0).subject = newsubj end
   343    343   				post(0):save(true)
   344    344   				co:reroute(lnkp.ptr)
   345    345   			end
   346    346   			return
   347    347   		elseif path(2):cmp(lib.str.lit 'del') then
   348         -			if meth_get(meth) then
          348  +			if meth_get(co.method) then
   349    349   				var conf: data.view.confirm
   350    350   				if post:ref() then
   351    351   					conf = data.view.confirm {
   352         -						title =  'delete post';
   353         -						query =  'are you sure you want to delete this post?';
          352  +						title = 'delete post';
          353  +						query = 'are you sure you want to delete this post?';
   354    354   						cancel = lnkp
   355    355   					}
   356    356   				else
   357    357   					conf = data.view.confirm {
   358         -						title =  'cancel retweet';
   359         -						query =  'are you sure you want to undo this retweet?';
          358  +						title = 'cancel retweet';
          359  +						query = 'are you sure you want to undo this retweet?';
   360    360   						cancel = '/';
   361    361   					}
   362    362   				end
   363    363   				var body = conf:poolstr(&co.srv.pool) --defer body:free()
   364    364   				co:stdpage([lib.srv.convo.page] {
   365         -					title =  'post :: delete';
   366         -					class =  'query';
          365  +					title = 'post :: delete';
          366  +					class = 'query';
   367    367   					body = body; cache = false;
   368    368   				})
   369    369   				return
   370         -			elseif meth == method.post then
          370  +			elseif co.method == method.post then
   371    371   				var act = co:ppostv('act')
   372    372   				if act:cmp('confirm') then
   373    373   					if post:ref() then
   374    374   						post().source:post_destroy(post().id)
   375    375   					elseif rt.kind ~= 0 then
   376    376   						co.srv:post_act_cancel(pid)
   377    377   					end
................................................................................
   378    378   					co:reroute('/') -- TODO maybe return to parent or conversation if possible
   379    379   					return
   380    380   				else goto badop end
   381    381   			end
   382    382   		else goto badurl end
   383    383   	end
   384    384   
   385         -	if post:ref() and meth == method.post then
          385  +	if post:ref() and co.method == method.post then
   386    386   		if co.aid == 0 then goto noauth end
   387    387   		var act = co:ppostv('act')
   388         -		if act:cmp( 'like') and not co.srv:post_liked_uid(co.who.id,pid) then
          388  +		if act:cmp('like') and not co.srv:post_liked_uid(co.who.id,pid) then
   389    389   			co.srv:post_like(co.who.id, pid, false)
   390    390   			post.ptr.likes = post.ptr.likes + 1
   391         -		elseif act:cmp( 'dislike') and co.srv:post_liked_uid(co.who.id,pid) then
          391  +		elseif act:cmp('dislike') and co.srv:post_liked_uid(co.who.id,pid) then
   392    392   			co.srv:post_like(co.who.id, pid, true)
   393    393   			post.ptr.likes = post.ptr.likes - 1
   394         -		elseif act:cmp( 'rt') then
          394  +		elseif act:cmp('rt') then
   395    395   			co.srv:post_retweet(co.who.id, pid, false)
   396    396   			post.ptr.rts = post.ptr.rts + 1
   397         -		elseif act:cmp( 'post') then
          397  +		elseif act:cmp('post') then
   398    398   			var replytext = co:ppostv('post')
   399    399   			var acl = co:ppostv('acl')
   400    400   			var subj = co:ppostv('subject')
   401         -			if not acl then acl =  'all' end
          401  +			if not acl then acl = 'all' end
   402    402   			if not replytext then goto badop end
   403    403   			
   404    404   			var reply = lib.store.post {
   405    405   				author = co.who.id, parent = pid;
   406    406   				subject = subj.ptr, acl = acl.ptr, body = replytext.ptr;
   407    407   			}
   408    408   
   409    409   			reply:publish(co.srv)
   410    410   		else goto badop end
   411    411   	end
   412    412   
   413    413   	if not post then goto badurl end
   414    414   
   415         -	lib.render.tweet_page(co, path, post.ptr)
          415  +	if co:matchmime(lib.http.mime.html) then
          416  +		lib.render.tweet_page(co, path, post.ptr)
          417  +	elseif co:matchmime(lib.http.mime.json) then
          418  +		co:json(lib.api.lp.tweet(co, post.ptr, false))
          419  +	else goto notacc end
   416    420   	do return end
   417    421   
   418    422   	::noauth:: do co:fail(401) return end
   419    423   	::badurl:: do co:fail(404) return end
   420    424   	::badop :: do co:fail(405) return end
          425  +	::notacc:: do co:fail(406) return end
   421    426   end
   422    427   
   423    428   local terra 
   424    429   credsec_for_uid(co: &lib.srv.convo, uid: uint64)
   425    430   	var act = co:ppostv('act')
   426    431   	if not act then return true end
   427    432   	lib.dbg('handling credential action')
................................................................................
   465    470   			var fr = co.srv.pool:frame()
   466    471   			var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), nonce)
   467    472   			if not lib.math.truncate64(hmac.ptr, hmac.ct) == noncevld then
   468    473   				co:complain(403,'nice try','what exactly are you trying to accomplish here, buddy')
   469    474   				return false
   470    475   			end
   471    476   
   472         -			var pkres = lib.crypt.loadpub(rsapub.ptr,rsapub.ct+1) -- needs NUL
          477  +			var pkres = lib.crypt.loadpub(binblob{rsapub.ptr,rsapub.ct+1}) -- needs NUL
   473    478   			if not pkres.ok then
   474    479   				co:complain(400,'invalid key','the key you have supplied is not a valid PEM or DER file')
   475    480   				return false
   476    481   			end
   477    482   			var pk = pkres.val
   478    483   			defer pk:free()
   479    484   
................................................................................
   972    977   			else co:reroute_cookie('/','auth=; Path=/')
   973    978   		end
   974    979   	else -- hierarchical routes
   975    980   		var path = lib.http.hier(&co.srv.pool, uri) --defer path:free()
   976    981   		if path.ct > 1 and path(0):cmp('user') then
   977    982   			http.actor_profile_uid(co, path)
   978    983   		elseif path.ct > 1 and path(0):cmp('post') then
   979         -			http.tweet_page(co, path, meth)
          984  +			http.tweet_page(co, path)
   980    985   		elseif path(0):cmp('tl') then
   981    986   			http.timeline(co, path)
   982    987   		elseif path(0):cmp('.well-known') then
   983    988   			if path(1):cmp('webfinger') then
   984    989   				if not co:matchmime(lib.http.mime.json) then goto nacc end
   985    990   				lib.api.webfinger(co)
   986    991   			end

Modified srv.t from [68c9cc33d4] to [dafa2dc374].

     1      1   -- vim: ft=terra
     2      2   local util = lib.util
     3      3   local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
     4      4   local pstring = lib.mem.ptr(int8)
     5         -local mimetypes = {
     6         -	{'html', 'text/html'};
     7         -	{'json', 'application/json'};
     8         -	{'json', 'application/activity+json'};
     9         -	{'json', 'application/ld+json'};
    10         -	{'mkdown', 'text/markdown'};
    11         -	{'text', 'text/plain'};
    12         -	{'ansi', 'text/x-ansi'};
    13         -}
    14      5   
    15      6   local struct srv
    16      7   local struct cfgcache {
    17      8   	secret: pstring
    18      9   	pol_sec: secmode.t
    19     10   	pol_reg: bool
    20     11   	pol_autoherald: bool
................................................................................
   149    140   
   150    141   terra lib.store.post:publish(s: &srv)
   151    142   	self:comp()
   152    143   	self.posted = lib.osclock.time(nil)
   153    144   	self.discovered = self.posted
   154    145   	self.chgcount = 0
   155    146   	self.edited = 0
          147  +	self.uri = nil -- only for foreign posts
          148  +	self.convoheaduri = nil -- ditto
   156    149   	self.id = s:post_create(self)
   157    150   	return self.id
   158    151   end
   159    152   
   160         -local struct convo {
   161         -	srv: &srv
   162         -	con: &lib.net.mg_connection
   163         -	msg: &lib.net.mg_http_message
   164         -	aid: uint64 -- 0 if logged out
   165         -	aid_issue: lib.store.timepoint
   166         -	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
   167         -	peer: lib.store.inet
   168         -	reqtype: lib.http.mime.t -- negotiated content type
   169         -	method: lib.http.method.t
   170         -	live_last: lib.store.timepoint
   171         -	uploads: lib.mem.vec(lib.http.upload)
   172         -	body: lib.str.t
   173         --- cache
   174         -	ui_hue: uint16
   175         -	navbar: lib.mem.ptr(int8)
   176         -	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
   177         --- private
   178         -	varbuf: lib.mem.ptr(int8)
   179         -	vbofs: &int8
   180         -}
   181         -
   182         -struct convo.page {
   183         -	title: pstring
   184         -	body: pstring
   185         -	class: pstring
   186         -	cache: bool
   187         -}
   188         -
   189         -local usrdefs = {
   190         -	str = {
   191         -		['acl-follow'    ] = {cfgfld = 'usrdef_pol_follow', fallback = 'local'};
   192         -		['acl-follow-req'] = {cfgfld = 'usrdef_pol_follow_req', fallback = 'all'};
   193         -	};
   194         -}
   195         -
   196         -terra convo:matchmime(mime: lib.http.mime.t): bool
   197         -	return self.reqtype == [lib.http.mime.none]
   198         -		or self.reqtype == mime
   199         -end
   200         -
   201         -terra convo:usercfg_str(uid: uint64, setting: pstring): pstring
   202         -	var set = self.srv:actor_conf_str_get(&self.srv.pool, uid, setting)
   203         -	if not set then
   204         -		[(function()
   205         -			local q = quote return pstring.null() end
   206         -			for key, dfl in pairs(usrdefs.str) do
   207         -				local rv
   208         -				if dfl.cfgfld then
   209         -					rv = quote
   210         -						var cf = self.srv.cfg.[dfl.cfgfld]
   211         -					in terralib.select(not cf, pstring([dfl.fallback]), cf) end
   212         -				elseif dfl.lit then rv = dfl.lit end
   213         -				q = quote
   214         -					if setting:cmp([key]) then return [rv] else [q] end
   215         -				end
   216         -			end
   217         -			return q
   218         -		end)()]
   219         -	else return set end
   220         -end
   221         -
          153  +local convo = terralib.loadfile 'convo.t'(srv)
   222    154   -- this is unfortunately necessary to work around a terra bug
   223    155   -- it can't seem to handle forward-declarations of structs in C
   224    156   
   225    157   local getpeer
   226    158   do local struct strucheader {
   227    159   		next: &lib.net.mg_connection
   228    160   		mgr: &lib.net.mg_mgr
................................................................................
   229    161   		peer: lib.net.mg_addr
   230    162   	}
   231    163   	terra getpeer(con: &lib.net.mg_connection)
   232    164   		return [&strucheader](con).peer
   233    165   	end
   234    166   end
   235    167   
   236         -terra convo:uid2actor_live(uid: uint64)
   237         -	var actor = self.srv:actor_fetch_uid(uid)
   238         -	if actor:ref() then
   239         -		if self.aid ~= 0 and self.who.id ~= uid then
   240         -			actor(0).relationship = self.srv:actor_rel_calc(self.who.id, uid)
   241         -		else -- defensive branch
   242         -			actor(0).relationship = lib.store.relationship {
   243         -				agent = 0, patient = uid;
   244         -				rel   = [lib.store.relation.null],
   245         -				recip = [lib.store.relation.null],
   246         -			}
   247         -		end
   248         -	end
   249         -	return actor
   250         -end
   251         -
   252         -terra convo:uid2actor(uid: uint64)
   253         -	var actor: &lib.store.actor = nil
   254         -	for j = 0, self.actorcache.top do
   255         -		if uid == self.actorcache(j).ptr.id then
   256         -			actor = self.actorcache(j).ptr
   257         -			break
   258         -		end
   259         -	end
   260         -	if actor == nil then
   261         -		actor = self.actorcache:insert(self:uid2actor_live(uid)).ptr
   262         -	end
   263         -	return actor
   264         -end
   265         -
   266         -terra convo:rawpage(code: uint16, pg: convo.page, hdrs: lib.mem.ptr(lib.http.header))
   267         -	var doc = data.view.docskel {
   268         -		instance = self.srv.cfg.instance;
   269         -		title = pg.title;
   270         -		body = pg.body;
   271         -		class = pg.class;
   272         -		navlinks = self.navbar;
   273         -		attr = '';
   274         -	}
   275         -	var attrbuf: int8[32]
   276         -	if self.aid ~= 0 and self.ui_hue ~= 323 then
   277         -		var hdecbuf: int8[21]
   278         -		var hdec = lib.math.decstr(self.ui_hue, &hdecbuf[20])
   279         -		lib.str.cpy(&attrbuf[0], ' style="--hue:')
   280         -		lib.str.cpy(&attrbuf[14], hdec)
   281         -		var len = &hdecbuf[20] - hdec 
   282         -		lib.str.cpy(&attrbuf[14] + len, '"')
   283         -		doc.attr = &attrbuf[0]
   284         -	end
   285         -
   286         -	if self.method == [lib.http.method.head]
   287         -		then doc:head(self.con,code,hdrs)
   288         -		else doc:send(self.con,code,hdrs)
   289         -	end
   290         -end
   291         -
   292         -terra convo:statpage(code: uint16, pg: convo.page)
   293         -	var hdrs = array(
   294         -		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
   295         -		lib.http.header { key = 'Cache-Control', value = 'no-store' }
   296         -	)
   297         -	self:rawpage(code,pg, [lib.mem.ptr(lib.http.header)] {
   298         -		ptr = &hdrs[0];
   299         -		ct = [hdrs.type.N] - lib.trn(pg.cache,1,0);
   300         -	})
   301         -end
   302         -
   303         -terra convo:livepage(pg: convo.page, lastup: lib.store.timepoint)
   304         -	var nbuf: int8[21]
   305         -	var hdrs = array(
   306         -		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
   307         -		lib.http.header { key = 'Cache-Control', value = 'no-store' },
   308         -		lib.http.header {
   309         -			key = 'X-Live-Newest-Artifact';
   310         -			value = lib.math.decstr(lastup, &nbuf[20]);
   311         -		},
   312         -		lib.http.header { key = 'Content-Length', value = '0' }
   313         -	)
   314         -	if self.live_last ~= 0 and self.live_last == lastup then
   315         -		lib.net.mg_printf(self.con, 'HTTP/1.1 %s', lib.http.codestr(200))
   316         -		for i = 0, [hdrs.type.N] do
   317         -			lib.net.mg_printf(self.con, '%s: %s\r\n', hdrs[i].key, hdrs[i].value)
   318         -		end
   319         -		lib.net.mg_printf(self.con, '\r\n')
   320         -	else
   321         -		self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] {
   322         -			ptr = &hdrs[0], ct = 3
   323         -		})
   324         -	end
   325         -end
   326         -
   327         -terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end
   328         -
   329         -terra convo:bytestream_trusted(lockdown: bool, mime: pstring, data: lib.mem.ptr(uint8))
   330         -	var lockhdr = "Content-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\n"
   331         -	if not lockdown then lockhdr = "" end
   332         -	lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\n%sX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2, lockhdr)
   333         -	lib.net.mg_send(self.con, data.ptr, data.ct)
   334         -	lib.net.mg_send(self.con, '\r\n', 2)
   335         -end
   336         -
   337         -terra convo:json(data: pstring)
   338         -	self:bytestream_trusted(false, 'application/activity+json; charset=utf-8', data:blob())
   339         -end
   340         -
   341         -terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8))
   342         -	-- TODO this is not a satisfactory solution; it's a bandaid on a gaping
   343         -	-- chest wound. ultimately we need to compile a whitelist of safe mime
   344         -	-- types as part of mimelib, but that is no small task. for now, this
   345         -	-- will keep the patient from immediately bleeding out
   346         -	if mime:cmp('text/html') or
   347         -		mime:cmp('text/xml') or
   348         -		mime:cmp('application/xhtml+xml') or
   349         -		mime:cmp('application/vnd.wap.xhtml+xml')
   350         -	then -- danger will robinson
   351         -		mime = 'text/plain'
   352         -	elseif mime:cmp('application/x-shockwave-flash') then
   353         -		mime = 'application/octet-stream'
   354         -	end
   355         -	self:bytestream_trusted(true, mime, data)
   356         -end
   357         -
   358         -terra convo:reroute_cookie(dest: rawstring, cookie: rawstring)
   359         -	var hdrs = array(
   360         -		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
   361         -		lib.http.header { key = 'Location',     value = dest },
   362         -		lib.http.header { key = 'Set-Cookie',   value = cookie }
   363         -	)
   364         -
   365         -	var body = data.view.docskel {
   366         -		instance = self.srv.cfg.instance.ptr;
   367         -		title = 'rerouting';
   368         -		body = 'you are being redirected';
   369         -		class = 'error';
   370         -		navlinks = '';
   371         -		attr = '';
   372         -	}
   373         -
   374         -	body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] {
   375         -		ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0)
   376         -	})
   377         -end
   378         -
   379         -terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end
   380         -
   381         -terra convo:installkey(dest: rawstring, aid: uint64)
   382         -	var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
   383         -	do var p = &sesskey[0]
   384         -		p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
   385         -		p = p + lib.session.cookie_gen(self.srv.cfg.secret, aid, lib.osclock.time(nil), p)
   386         -		lib.dbg('sending cookie ',{&sesskey[0],15})
   387         -		p = lib.str.ncpy(p, '; Path=/', 9)
   388         -	end
   389         -	self:reroute_cookie(dest, &sesskey[0])
   390         -end
   391         - 
   392         -terra convo:stra(sz: intptr) -- convenience function
   393         -	var s: lib.str.acc
   394         -	s:pool(&self.srv.pool,sz)
   395         -	return s
   396         -end
   397         -
   398         -convo.methods.qstr = macro(function(self, ...) -- convenience string builder
   399         -	local exp = {...}
   400         -	return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize()
   401         -end)
   402         -
   403         -terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
   404         -	if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end
   405         -
   406         -	if self:matchmime(lib.http.mime.html) then
   407         -		var body = [convo.page] {
   408         -			title = self:qstr('error :: ', title);
   409         -			body = self:qstr('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>');
   410         -			class = 'error';
   411         -			cache = false;
   412         -		}
   413         -
   414         -		self:statpage(code, body)
   415         -	else
   416         -		var pg = lib.http.page { respcode = code, body = pstring.null() }
   417         -		var ctt = lib.http.mime.none
   418         -		if self:matchmime(lib.http.mime.json) then ctt = lib.http.mime.json
   419         -			pg.body = ([lib.tpl.mk'{"_parsav_error":@$ekind, "_parsav_error_desc":@$edesc}']
   420         -				{ekind = title, edesc = msg}):poolstr(&self.srv.pool)
   421         -		elseif self:matchmime(lib.http.mime.text) then ctt = lib.http.mime.text
   422         -			pg.body = self:qstr('error: ',title,'\n',msg)
   423         -		elseif self:matchmime(lib.http.mime.mkdown) then ctt = lib.http.mime.mkdown
   424         -			pg.body = self:qstr('# error :: ',title,'\n\n',msg)
   425         -		elseif self:matchmime(lib.http.mime.ansi) then ctt = lib.http.mime.ansi
   426         -			pg.body = self:qstr('\27[1;31merror :: ',title,'\27[m\n',msg)
   427         -		end
   428         -		var cthdr = lib.http.header { 'Content-Type', 'text/plain' }
   429         -		if ctt == lib.http.mime.none then
   430         -			pg.headers.ct = 0
   431         -		else
   432         -			pg.headers = lib.typeof(pg.headers) { &cthdr, 1 }
   433         -			switch ctt do
   434         -				case [ctt.type](lib.http.mime.json) then
   435         -					cthdr.value = 'application/json'
   436         -				end
   437         -				escape
   438         -					for i,v in ipairs(mimetypes) do local key,mime = v[1],v[2]
   439         -						if key ~= 'json' then
   440         -							emit quote case [ctt.type](lib.http.mime.[key]) then cthdr.value = [mime] end end
   441         -						end
   442         -					end
   443         -				end
   444         -			end
   445         -		end
   446         -		pg:send(self.con)
   447         -	end
   448         -end
   449         -
   450         -terra convo:fail(code: uint16)
   451         -	switch code do
   452         -		escape
   453         -			local stderrors = {
   454         -				{400, 'bad request', "the action you have attempted on this resource is not meaningful"};
   455         -				{401, 'unauthorized', "this resource is not available at your clearance level"};
   456         -				{403, 'forbidden', "we can neither confirm nor deny the existence of this resource"};
   457         -				{404, 'resource not found', "that resource is not extant on or known to this server"};
   458         -				{405, 'method not allowed', "the method you have attempted on this resource is not meaningful"};
   459         -				{406, 'not acceptable', "none of the suggested content types are a viable representation of this resource"};
   460         -				{500, 'internal server error', "parsav did a fucksy wucksy"};
   461         -			}
   462         -
   463         -			for i,v in ipairs(stderrors) do
   464         -				emit quote case uint16([v[1]]) then
   465         -					self:complain([v])
   466         -				end end
   467         -			end
   468         -		end
   469         -		else self:complain(500,'unknown error','an unrecognized error was thrown. this is a bug')
   470         -	end
   471         -end
   472         -
   473         -terra convo:confirm(title: pstring, msg: pstring, cancel: pstring)
   474         -	var conf = data.view.confirm {
   475         -		title = title;
   476         -		query = msg;
   477         -		cancel = cancel;
   478         -	}
   479         -	var ti: lib.str.acc ti:pcompose(&self.srv.pool,'confirm :: ', title)
   480         -	var body = conf:poolstr(&self.srv.pool) -- defer body:free()
   481         -	var cf = [convo.page] {
   482         -		title = ti:finalize();
   483         -		class = 'query';
   484         -		body = body; cache = false;
   485         -	}
   486         -	self:stdpage(cf)
   487         -	--cf.title:free()
   488         -end
   489         -
   490         -convo.methods.assertpow = macro(function(self, pow)
   491         -	return quote
   492         -		var ok = true
   493         -		if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then
   494         -			ok = false
   495         -			self:complain(403,'insufficient privileges',['you lack the <strong>'..pow:asvalue()..'</strong> power and cannot perform this action'])
   496         -		end
   497         -	in ok end
   498         -end)
   499         -
   500         -local pstr2mg, mg2pstr
   501         -do -- aaaaaaaaaaaaaaaaaaaaaaaa
   502         -	mgstr = lib.util.find(lib.net.mg_http_message.entries, function(v)
   503         -		if v.field == 'body' or v[1] == 'body' then return v.type end
   504         -	end)
   505         -	terra pstr2mg(p: pstring): mgstr
   506         -		return mgstr { ptr = p.ptr, len = p.ct }
   507         -	end
   508         -	terra mg2pstr(m: mgstr): pstring
   509         -		return pstring { ptr = m.ptr, ct = m.len }
   510         -	end
   511         -end
   512         -
   513         --- CALL ONLY ONCE PER VAR
   514         -terra convo:postv_next(name: pstring, start: &pstring)
   515         -	if self.varbuf.ptr == nil then
   516         -		self.varbuf = self.srv.pool:alloc(int8, self.msg.body.len + self.msg.query.len)
   517         -		self.vbofs = self.varbuf.ptr
   518         -	end
   519         -	var conv = pstr2mg(@start)
   520         -	var o = lib.net.mg_http_get_var(
   521         -		&conv,
   522         -		name.ptr, self.vbofs,
   523         -		self.varbuf.ct - (self.vbofs - self.varbuf.ptr)
   524         -	)
   525         -	if o > 0 then
   526         -		start:advance(name.ct + o + 2)
   527         -		var r = self.vbofs
   528         -		self.vbofs = self.vbofs + o + 1
   529         -		@(self.vbofs - 1) = 0
   530         -		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
   531         -		return norm.ptr, norm.ct
   532         -	else return nil, 0 end
   533         -end
   534         -terra convo:postv(name: pstring)
   535         -	var start = mg2pstr(self.msg.body)
   536         -	return self:postv_next(name, &start)
   537         -end
   538         -terra convo:ppostv(name: pstring)
   539         -	var s,l = self:postv(name)
   540         -	return pstring { ptr = s, ct = l }
   541         -end
   542         -do
   543         -	local struct postiter { co: &convo where: pstring name: pstring }
   544         -	terra convo:eachpostv(name: pstring)
   545         -		return postiter { co = self, where = mg2pstr(self.msg.body), name = name } 
   546         -	end
   547         -	postiter.metamethods.__for = function(self, body)
   548         -		return quote
   549         -			while true do
   550         -				var str, len = self.co:postv_next(self.name, &self.where)
   551         -				if str == nil then break end
   552         -				[ body(`pstring {str, len}) ]
   553         -			end
   554         -		end
   555         -	end
   556         -end
   557         -
   558         -terra convo:getv(name: rawstring)
   559         -	if self.varbuf.ptr == nil then
   560         -		self.varbuf = self.srv.pool:alloc(int8, self.msg.query.len + self.msg.body.len)
   561         -		self.vbofs = self.varbuf.ptr
   562         -	end
   563         -	var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
   564         -	if o > 0 then
   565         -		var r = self.vbofs
   566         -		self.vbofs = self.vbofs + o + 1
   567         -		@(self.vbofs - 1) = 0
   568         -		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
   569         -		return norm.ptr, norm.ct
   570         -	else return nil, 0 end
   571         -end
   572         -terra convo:pgetv(name: rawstring)
   573         -	var s,l = self:getv(name)
   574         -	return pstring { ptr = s, ct = l }
   575         -end
   576         -
   577    168   local route = {} -- these are defined in route.t, as they need access to renderers
   578    169   terra route.dispatch_http ::  {&convo, lib.mem.ptr(int8)} -> {}
   579    170   
   580         -local mimevar = symbol(lib.mem.ref(int8))
   581         -local mimeneg = `lib.http.mime.none
   582         -
   583         -for i, t in ipairs(mimetypes) do
   584         -	local name, mime = t[1], t[2]
   585         -	mimeneg = quote
   586         -		var ret: lib.http.mime.t
   587         -		if lib.str.ncmp(mimevar.ptr, mime, lib.math.biggest(mimevar.ct, [#mime])) == 0 then
   588         -			ret = [lib.http.mime[name]]
   589         -		else ret = [mimeneg] end
   590         -	in ret end
   591         -end
   592         -
   593    171   local handle = {
   594    172   	http = terra(con: &lib.net.mg_connection, event_kind: int, event: &opaque, userdata: &opaque)
   595    173   		var server = [&srv](userdata)
   596    174   		var mgpeer = getpeer(con)
   597    175   		-- var pbuf: int8[128]
   598    176   
   599    177   		-- the peer property is currently broken and there is precious
................................................................................
   634    212   				  co.body.ptr = msg.body.ptr co.body.ct = msg.body.len
   635    213   
   636    214   				-- first, check for an accept header. if it's there, we need to
   637    215   				-- iterate over the values and pick the highest-priority one
   638    216   				do var acc = lib.http.findheader(msg, 'Accept')
   639    217   					-- TODO handle q-value
   640    218   					if acc ~= nil and acc.ptr ~= nil then
   641         -						var [mimevar] = [lib.mem.ref(int8)] { ptr = acc.ptr }
          219  +						var mimevar = [pstring] { ptr = acc.ptr }
          220  +						lib.dbg('accept header is ', {acc.ptr,acc.ct})
   642    221   						var i = 0 while i < acc.ct do
   643    222   							if acc.ptr[i] == @',' or acc.ptr[i] == @';' then
   644    223   								mimevar.ct = (acc.ptr+i) - mimevar.ptr
   645         -								var t = [mimeneg]
   646         -								if t ~= lib.http.mime.none then
   647         -									co.reqtype = t
          224  +								var mk = lib.mime.lookup(mimevar)
          225  +								if mk ~= nil and mk.output ~= lib.http.mime.none then
          226  +									co.reqtype = mk.output
   648    227   									goto foundtype
   649    228   								end
   650    229   
   651    230   								if acc.ptr[i] == @';' then -- fast-forward over q
   652    231   									for j=i+1,acc.ct do i=j
   653    232   										if acc.ptr[j] == @',' then break end
   654    233   									end
................................................................................
   661    240   
   662    241   								mimevar.ptr = acc.ptr + i + 1
   663    242   							end
   664    243   							i=i+1
   665    244   						end
   666    245   						if co.reqtype == lib.http.mime.none then
   667    246   							mimevar.ct = acc.ct - (mimevar.ptr - acc.ptr)
   668         -							co.reqtype = [mimeneg]
   669         -							if co.reqtype == lib.http.mime.none then
   670         -								co.reqtype = lib.http.mime.html
          247  +							var mk = lib.mime.lookup(mimevar)
          248  +							if mk ~= nil and mk.output ~= lib.http.mime.none then
          249  +								co.reqtype = mk.output
   671    250   							end
   672    251   						end
   673         -					else co.reqtype = lib.http.mime.html end
          252  +					end
   674    253   				::foundtype::end
   675    254   
   676    255   				-- we need to check if there's any cookies sent with the request,
   677    256   				-- and if so, whether they contain any credentials. this will be
   678    257   				-- used to set the auth parameters in the http conversation
   679    258   				var cookies_p = lib.http.findheader(msg, 'Cookie')
   680    259   				if cookies_p ~= nil and cookies_p.ptr ~= nil then
................................................................................
   878    457   							end
   879    458   							bsr:free()
   880    459   							upmap:free()
   881    460   						end
   882    461   					end
   883    462   				end
   884    463   
          464  +				var mtt = lib.http.mime._str(co.reqtype)
          465  +				lib.dbg('routing with negotiated type of ', {mtt.ptr,mtt.ct})
   885    466   				route.dispatch_http(&co, uri)
   886    467   
   887    468   				::fail::
   888    469   				if co.uploads.run > 0 then
   889    470   					for i=0,co.uploads.sz do
   890    471   						co.uploads(i).filename:free()
   891    472   						co.uploads(i).field:free()

Modified store.t from [33ecd773b1] to [6e43eba049].

   213    213   	elseif self.mode == 3 then
   214    214   		return 0,0,self.to_idx,self.from_idx
   215    215   	else lib.bail('invalid mode on timeline range!') end
   216    216   end
   217    217   
   218    218   struct m.post {
   219    219   	id: uint64
          220  +	uri: str
   220    221   	author: uint64
   221    222   	subject: str
   222    223   	body: str
   223    224   	acl: str
   224    225   	posted: m.timepoint
   225    226   	discovered: m.timepoint
   226    227   	edited: m.timepoint
................................................................................
   489    490   	circle_destroy: {&m.source, uint64, uint64} -> {}
   490    491   	circle_members_fetch_cid:  {&m.source, &lib.mem.pool, uint64} -> lib.mem.ptr(uint64)
   491    492   	circle_members_fetch_name: {&m.source, &lib.mem.pool, uint64, pstring} -> lib.mem.ptr(uint64)
   492    493   	circle_members_add_uid: {&m.source, uint64, uint64} -> {}
   493    494   	circle_members_del_uid: {&m.source, uint64, uint64} -> {}
   494    495   	circle_memberships_uid: {&m.source, &lib.mem.pool, uint64, uint64} -> lib.mem.ptr(m.circle)
   495    496   
          497  +	thread_top_find: {&m.source, uint64} -> uint64
          498  +		-- NOTE: this won't work if conversations are broken across multiple data sources!
          499  +		-- if this is a thing that's likely to happen, the overlord-side wrapper for this
          500  +		-- function (srv.t) should implement a more sophisticated algorithm over all the
          501  +		-- data sources, instead of just stopping when one parent is found
   496    502   	thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint
   497    503   
   498    504   	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
   499    505   		-- instantiate an artifact in the database, either installing a new
   500    506   		-- artifact or returning the id of an existing artifact with the same hash
   501    507   			-- artifact: bytea
   502    508   			-- mime:     pstring

Modified str.t from [072253bf19] to [bff3416286].

   567    567   		var add, cont = disemvowel_codepoint(cur)
   568    568   		if add:ref() then acc:ppush(add) end
   569    569   		cur = cont
   570    570   	end
   571    571   	return acc:finalize()
   572    572   end
   573    573   
   574         -terra m.qesc(pool: &lib.mem.pool, str: m.t, wrap: bool): m.t
          574  +terra m.acc:qesc(str: m.t, wrap: bool)
   575    575    -- escape double-quotes
   576         -	var a: m.acc a:pool(pool, str.ct + str.ct/2)
   577         -	if wrap then a:lpush '"' end
          576  +	if wrap then self:lpush '"' end
   578    577   	for i=0, str.ct do
   579         -		if     str(i) == @'"'  then a:lpush '\\"'
   580         -		elseif str(i) == @'\\' then a:lpush '\\\\'
          578  +		if     str(i) == @'"'  then self:lpush '\\"'
          579  +		elseif str(i) == @'\\' then self:lpush '\\\\'
          580  +		elseif str(i) == @'\n' then self:lpush '\\n'
          581  +		elseif str(i) == @'\t' then self:lpush '\\t'
   581    582   		elseif str(i) < 0x20 then -- for json
   582    583   			var hex = lib.math.hexbyte(str(i))
   583         -			a:lpush('\\u00'):push(&hex[0], 2)
   584         -		else   a:push(str.ptr + i,1) end
          584  +			self:lpush('\\u00'):push(&hex[0], 2)
          585  +		else   self:push(str.ptr + i,1) end
   585    586   	end
   586         -	if wrap then a:lpush '"' end
          587  +	if wrap then self:lpush '"' end
          588  +	return self
          589  +end
          590  +
          591  +terra m.qesc(pool: &lib.mem.pool, str: m.t, wrap: bool): m.t
          592  + -- convenience function
          593  +	var a: m.acc a:pool(pool, 2 + str.ct + str.ct/2)
          594  +	a:qesc(str,wrap)
   587    595   	return a:finalize()
   588    596   end
          597  +
          598  +terra m.acc:qpush(str: m.t)
          599  + -- convenience adaptor
          600  +	return self:qesc(str, false)
          601  +end
   589    602   
   590    603   return m

Modified tpl.t from [b153c48352] to [f930d7a874].

    34     34   	str = str:gsub('%s+[\n$]','')
    35     35   	str = str:gsub('\n','')
    36     36   	str = str:gsub('</a><a ','</a> <a ') -- keep nav links from getting smooshed
    37     37   	str = str:gsub(tplchar .. '%?([-%w]+)', function(file)
    38     38   		if not docs[file] then docs[file] = data.doc[file] end
    39     39   		return string.format('<a href="#help-%s" class="help">?</a>', file)
    40     40   	end)
           41  +	local detritus = ""
    41     42   	for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([+:!$#%^]?)([-a-zA-Z0-9_]+):?()') do
    42     43   		if string.sub(str,start-1,start-1) ~= '\\' then
    43         -			segs[#segs+1] = string.sub(str,last,start-1)
           44  +			local suffix = ""
           45  +			if mode == '$' then suffix = '"' end
           46  +			segs[#segs+1] = detritus .. string.sub(str,last,start-1) .. suffix
           47  +			detritus = ''
    44     48   			fields[#segs] = { key = key:gsub('-','_'), mode = (mode ~= '' and mode or nil) }
    45     49   			last = stop
           50  +			if mode == '$' then detritus = '"' end
    46     51   		end
    47     52   	end
    48         -	segs[#segs+1] = string.sub(str,last)
           53  +	segs[#segs+1] = detritus .. string.sub(str,last)
    49     54   
    50     55   	for i, s in ipairs(segs) do
    51     56   		segs[i] = string.gsub(s, '\\'..tplchar, tplchar_o)
    52     57   		constlen = constlen + string.len(segs[i])
    53     58   	end
    54     59   
    55     60   	for n,d in pairs(docs) do
................................................................................
   114    119   		senders[#senders+1] = quote lib.net.mg_send([destcon], [seg], [#seg]) end
   115    120   		appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end
   116    121   		if fields[idx] and fields[idx].mode then
   117    122   			local f = fields[idx]
   118    123   			local fp = `symself.[f.key]
   119    124   			local sanexp
   120    125   			local nulexp
   121         -			if f.mode == '$' then sanexp = `lib.str.qesc(pool, fp, true)
          126  +			if f.mode == '$' then sanexp = `lib.str.qesc(pool, fp, false)
          127  +				-- we use the detritus mechanism rather than the quote-wrap mechanism bc, apart
          128  +				-- from being faster, 0-length strings cannot be sanitized into -- >0-length
          129  +				-- strings due to how nullity is indicated (to wit, if fp == 0, ptr can be wild)
   122    130   			elseif f.mode == '+' then sanexp = `lib.str.qesc(pool, fp, false)
   123    131   			elseif f.mode == '#' then
   124    132   				sanexp = quote
   125    133   					var ibuf: int8[21]
   126    134   					var ptr = lib.math.decstr(fp, &ibuf[20])
   127    135   				in pstr {ptr=ptr, ct=&ibuf[20] - ptr} end
   128    136   			elseif f.mode == '^' then