parsav  Check-in [419d1a1ebe]

Overview
Comment:milestone
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 419d1a1ebe4ca565be0130e83bed00227fee120c08c3d46605168be664120074
User & Date: lexi on 2020-12-22 23:01:30
Other Links: manifest | tags
Context
2020-12-25
03:59
big ol iteration check-in: 5b3a03ad34 user: lexi tags: trunk
2020-12-22
23:01
milestone check-in: 419d1a1ebe user: lexi tags: trunk
2020-12-21
01:08
continued iteration check-in: 25e05466d5 user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [2e402a5b93] to [17bd63f8c3].

    29     29   				bio, rank, quota, key
    30     30   			from parsav_actors
    31     31   				where id = $1::bigint
    32     32   		]];
    33     33   	};
    34     34   
    35     35   	actor_fetch_xid = {
    36         -		params = {rawstring}, sql = [[
           36  +		params = {lib.mem.ptr(int8)}, sql = [[
    37     37   			select a.id, a.nym, a.handle, a.origin,
    38         -			       a.bio, a.rank, a.quota, a.key, $1::text,
           38  +			       a.bio, a.rank, a.quota, a.key, 
           39  +				   coalesce(a.handle || '@' || s.domain,
           40  +				            '@' || a.handle) as xid,
    39     41   
    40     42   				coalesce(s.domain,
    41     43   				        (select value from parsav_config
    42     44   							where key='domain' limit 1)) as domain
    43     45   
    44     46   			from      parsav_actors  as a
    45     47   			left join parsav_servers as s
    46     48   				on a.origin = s.id
    47     49   
    48     50   			where $1::text = (a.handle || '@' || domain) or
    49     51   			      $1::text = ('@' || a.handle || '@' || domain) or
    50         -				  (a.origin is null and $1::text = ('@' || a.handle))
           52  +				  (a.origin is null and
           53  +					  $1::text = a.handle or
           54  +					  $1::text = ('@' || a.handle))
    51     55   		]];
    52     56   	};
    53     57   
    54     58   	actor_enum_local = {
    55     59   		params = {}, sql = [[
    56     60   			select id, nym, handle, origin,
    57     61   			       bio, rank, quota, key,
................................................................................
    62     66   		]];
    63     67   	};
    64     68   
    65     69   	actor_enum = {
    66     70   		params = {}, sql = [[
    67     71   			select a.id, a.nym, a.handle, a.origin,
    68     72   			       a.bio, a.rank, a.quota, a.key,
    69         -				a.handle ||'@'||
    70         -				coalesce(s.domain,
    71         -				        (select value from parsav_config
    72         -							where key='domain' limit 1)) as xid
           73  +				   coalesce(a.handle || '@' || s.domain,
           74  +				            '@' || a.handle) as xid
    73     75   			from parsav_actors a
    74     76   			left join parsav_servers s on s.id = a.origin
    75     77   		]];
    76     78   	};
           79  +
           80  +	actor_auth_how = {
           81  +		params = {rawstring, lib.store.inet}, sql = [[
           82  +		with mts as (select a.kind from parsav_auth as a
           83  +			left join parsav_actors as u on u.id = a.uid
           84  +			where (a.uid is null or u.handle = $1::text or (
           85  +					a.uid = 0 and a.name = $1::text
           86  +				)) and
           87  +				(a.netmask is null or a.netmask >> $2::inet) and
           88  +				blacklist = false)
           89  +
           90  +			select
           91  +				(select count(*) from mts where kind like 'pw-%') > 0,
           92  +				(select count(*) from mts where kind like 'otp-%') > 0,
           93  +				(select count(*) from mts where kind like 'challenge-%') > 0,
           94  +				(select count(*) from mts where kind = 'trust') > 0
           95  +		]]; -- cheat
           96  +	};
           97  +
           98  +	actor_session_fetch = {
           99  +		params = {uint64, lib.store.inet}, sql = [[
          100  +			select a.id, a.nym, a.handle, a.origin,
          101  +			       a.bio, a.rank, a.quota, a.key,
          102  +				   coalesce(a.handle || '@' || s.domain,
          103  +				            '@' || a.handle) as xid,
          104  +
          105  +			       au.restrict,
          106  +						array['post'  ] <@ au.restrict as can_post,
          107  +						array['edit'  ] <@ au.restrict as can_edit,
          108  +						array['acct'  ] <@ au.restrict as can_acct,
          109  +						array['upload'] <@ au.restrict as can_upload,
          110  +						array['censor'] <@ au.restrict as can_censor,
          111  +						array['admin' ] <@ au.restrict as can_admin
          112  +
          113  +			from      parsav_auth au
          114  +			left join parsav_actors a     on au.uid = a.id
          115  +			left join parsav_servers s    on a.origin = s.id
          116  +
          117  +			where au.aid = $1::bigint and au.blacklist = false and
          118  +				(au.netmask is null or au.netmask >> $2::inet)
          119  +		]];
          120  +	};
    77    121   }
    78    122   
    79    123   local struct pqr {
    80    124   	sz: intptr
    81    125   	res: &lib.pq.PGresult
    82    126   }
    83    127   terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end
................................................................................
    85    129   	return (lib.pq.PQgetisnull(self.res, row, col) == 1)
    86    130   end
    87    131   terra pqr:len(row: intptr, col: intptr)
    88    132   	return lib.pq.PQgetlength(self.res, row, col)
    89    133   end
    90    134   terra pqr:cols() return lib.pq.PQnfields(self.res) end
    91    135   terra pqr:string(row: intptr, col: intptr) -- not to be exported!!
          136  +	if self:null(row,col) then return nil end
    92    137   	var v = lib.pq.PQgetvalue(self.res, row, col)
    93    138   --	var r: lib.mem.ptr(int8)
    94    139   --	r.ct = lib.str.sz(v)
    95    140   --	r.ptr = v
    96    141   	return v
    97    142   end
    98    143   terra pqr:bin(row: intptr, col: intptr) -- not to be exported!! DO NOT FREE
    99    144   	return [lib.mem.ptr(uint8)] {
   100    145   		ptr = [&uint8](lib.pq.PQgetvalue(self.res, row, col));
   101    146   		ct = lib.pq.PQgetlength(self.res, row, col);
   102    147   	}
   103    148   end
   104    149   terra pqr:String(row: intptr, col: intptr) -- suitable to be exported
          150  +	if self:null(row,col) then return [lib.mem.ptr(int8)] {ptr=nil,ct=0} end
   105    151   	var s = [lib.mem.ptr(int8)] { ptr = lib.str.dup(self:string(row,col)) }
   106    152   	s.ct = lib.pq.PQgetlength(self.res, row, col)
   107    153   	return s
   108    154   end
   109    155   terra pqr:bool(row: intptr, col: intptr)
   110    156   	var v = lib.pq.PQgetvalue(self.res, row, col)
   111    157   	if @v == 0x01 then return true else return false end
................................................................................
   176    222   	local args, casts, counters, fixers, ft, yield = {}, {}, {}, {}, {}, {}
   177    223   	for i, ty in ipairs(q.params) do
   178    224   		args[i] = symbol(ty)
   179    225   		ft[i] = `1
   180    226   		if ty == rawstring then
   181    227   			counters[i] = `lib.trn([args[i]] == nil, 0, lib.str.sz([args[i]]))
   182    228   			casts[i] = `[&int8]([args[i]])
          229  +		elseif ty == lib.store.inet then -- assume not CIDR
          230  +			counters[i] = `lib.trn([args[i]].pv == 4,4,16)+4
          231  +			casts[i] = quote
          232  +				var ipbuf: int8[20]
          233  +				;[pqt[lib.store.inet](false)]([args[i]], [&uint8](&ipbuf))
          234  +			in &ipbuf[0] end
          235  +		elseif ty.ptr_basetype == int8 or ty.ptr_basetype == uint8 then
          236  +			counters[i] = `[args[i]].ct
          237  +			casts[i] = `[&int8]([args[i]].ptr)
   183    238   		elseif ty:isintegral() then
   184    239   			counters[i] = ty.bytes
   185    240   			casts[i] = `[&int8](&[args[i]])
   186    241   			fixers[#fixers + 1] = quote
   187    242   				--lib.io.fmt('uid=%llu(%llx)\n',[args[i]],[args[i]])
   188    243   				[args[i]] = lib.math.netswap(ty, [args[i]])
   189    244   			end
................................................................................
   212    267   			return pqr {ct, res}
   213    268   		end
   214    269   	end
   215    270   end
   216    271   
   217    272   local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
   218    273   	var a: lib.mem.ptr(lib.store.actor)
          274  +
   219    275   	if r:cols() >= 8 then 
   220    276   		a = [ lib.str.encapsulate(lib.store.actor, {
   221         -			nym = {`r:string(row, 1); `r:len(row,1) + 1};
          277  +			nym = {`r:string(row,1), `r:len(row,1)+1};
          278  +			bio = {`r:string(row,4), `r:len(row,4)+1};
   222    279   			handle = {`r:string(row, 2); `r:len(row,2) + 1};
   223         -			bio = {`r:string(row, 4); `r:len(row,4) + 1};
   224    280   			xid = {`r:string(row, 8); `r:len(row,8) + 1};
   225    281   		}) ]
   226    282   	else
   227    283   		a = [ lib.str.encapsulate(lib.store.actor, {
   228         -			nym = {`r:string(row, 1); `r:len(row,1) + 1};
          284  +			nym = {`r:string(row,1), `r:len(row,1)+1};
          285  +			bio = {`r:string(row,4), `r:len(row,4)+1};
   229    286   			handle = {`r:string(row, 2); `r:len(row,2) + 1};
   230         -			bio = {`r:string(row, 4); `r:len(row,4) + 1};
   231    287   		}) ]
   232    288   		a.ptr.xid = nil
   233    289   	end
   234    290   	a.ptr.id = r:int(uint64, row, 0);
   235    291   	a.ptr.rights = lib.store.rights_default();
   236    292   	a.ptr.rights.rank = r:int(uint16, row, 5);
   237    293   	a.ptr.rights.quota = r:int(uint32, row, 6);
................................................................................
   345    401   			return [lib.mem.ptr(lib.store.actor)] { ct = 0, ptr = nil }
   346    402   		else defer r:free()
   347    403   			var a = row_to_actor(&r, 0)
   348    404   			a.ptr.source = src
   349    405   			return a
   350    406   		end
   351    407   	end];
          408  +
          409  +	actor_fetch_xid = [terra(src: &lib.store.source, xid: lib.mem.ptr(int8))
          410  +		var r = queries.actor_fetch_xid.exec(src, xid)
          411  +		if r.sz == 0 then
          412  +			return [lib.mem.ptr(lib.store.actor)] { ct = 0, ptr = nil }
          413  +		else defer r:free()
          414  +			var a = row_to_actor(&r, 0)
          415  +			a.ptr.source = src
          416  +			return a
          417  +		end
          418  +	end];
   352    419   
   353    420   	actor_enum = [terra(src: &lib.store.source)
   354    421   		var r = queries.actor_enum.exec(src)
   355    422   		if r.sz == 0 then
   356    423   			return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil }
   357    424   		else defer r:free()
   358    425   			var mem = lib.mem.heapa([&lib.store.actor], r.sz)
................................................................................
   373    440   	end];
   374    441   
   375    442   	actor_auth_how = [terra(
   376    443   			src: &lib.store.source,
   377    444   			ip: lib.store.inet,
   378    445   			username: rawstring
   379    446   		)
   380         -		var authview = src:conf_get('auth-source') defer authview:free()
   381         -		var a: lib.str.acc defer a:free()
   382         -		a:compose('with mts as (select a.kind from ',authview,[' ' .. sqlsquash [[as a
   383         -			left join parsav_actors as u on u.id = a.uid
   384         -			where (a.uid is null or u.handle = $1::text or (
   385         -					a.uid = 0 and a.name = $1::text
   386         -				)) and
   387         -				(a.netmask is null or a.netmask >> $2::inet) and
   388         -				blacklist = false)
   389         -
   390         -			select
   391         -				(select count(*) from mts where kind like 'pw-%') > 0,
   392         -				(select count(*) from mts where kind like 'otp-%') > 0,
   393         -				(select count(*) from mts where kind like 'challenge-%') > 0,
   394         -				(select count(*) from mts where kind = 'trust') > 0 ]]]) -- cheat
   395    447   		var cs: lib.store.credset cs:clear();
   396         -		var ipbuf: int8[20]
   397         -		;[pqt[lib.store.inet](false)](ip, [&uint8](&ipbuf))
   398         -		var ipbl: intptr if ip.pv == 4 then ipbl = 8 else ipbl = 20 end
   399         -		var params = arrayof(rawstring, username, [&int8](&ipbuf))
   400         -		var params_sz = arrayof(int, lib.str.sz(username), ipbl)
   401         -		var params_ft = arrayof(int, 1, 1)
   402         -		var res = lib.pq.PQexecParams([&lib.pq.PGconn](src.handle), a.buf, 2, nil,
   403         -			params, params_sz, params_ft, 1)
   404         -		if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
   405         -			if res == nil then
   406         -				lib.bail('grievous error occurred checking for auth methods')
   407         -			end
   408         -			lib.bail('could not get auth methods for user ',username,':\n',lib.pq.PQresultErrorMessage(res))
   409         -		end
   410         -		var r = pqr { res = res, sz = lib.pq.PQntuples(res) } 
          448  +		var r = queries.actor_auth_how.exec(src, username, ip) 
   411    449   		if r.sz == 0 then return cs end -- just in case
          450  +		defer r:free()
   412    451   		(cs.pw << r:bool(0,0))
   413    452   		(cs.otp << r:bool(0,1))
   414    453   		(cs.challenge << r:bool(0,2))
   415    454   		(cs.trust << r:bool(0,3))
   416         -		lib.pq.PQclear(res)
   417    455   		return cs
   418    456   	end];
   419    457   	 
   420    458   	actor_auth_pw = [terra(
   421    459   			src: &lib.store.source,
   422    460   			ip: lib.store.inet,
   423    461   			username: rawstring,
   424    462   			cred: rawstring
   425    463   		)
   426         -		var authview = src:conf_get('auth-source') defer authview:free()
   427         -		var a: lib.str.acc defer a:free()
   428         -		a:compose('select a.aid from ',authview,[' ' .. sqlsquash [[as a
          464  +		var q = [[select a.aid from parsav_auth as a
   429    465   			left join parsav_actors as u on u.id = a.uid
   430    466   			where (a.uid is null or u.handle = $1::text or (
   431    467   					a.uid = 0 and a.name = $1::text
   432    468   				)) and
   433    469   				(a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and
   434    470   				(a.netmask is null or a.netmask >> $4::inet)
   435         -			order by blacklist desc limit 1]]])
          471  +			order by blacklist desc limit 1]]
   436    472   
   437         -		[ checksha(`src.handle, `a.buf, 256, ip, username, cred) ] -- most common
   438         -		[ checksha(`src.handle, `a.buf, 512, ip, username, cred) ] -- most secure
   439         -		[ checksha(`src.handle, `a.buf, 384, ip, username, cred) ] -- weird
   440         -		[ checksha(`src.handle, `a.buf, 224, ip, username, cred) ] -- weirdest
          473  +		[ checksha(`src.handle, q, 256, ip, username, cred) ] -- most common
          474  +		[ checksha(`src.handle, q, 512, ip, username, cred) ] -- most secure
          475  +		[ checksha(`src.handle, q, 384, ip, username, cred) ] -- weird
          476  +		[ checksha(`src.handle, q, 224, ip, username, cred) ] -- weirdest
   441    477   
   442    478   		-- TODO: check pbkdf2-hmac
   443    479   		-- TODO: check OTP
   444    480   		return 0
   445    481   	end];
          482  +
          483  +	actor_session_fetch = [terra(
          484  +		src: &lib.store.source,
          485  +		aid: uint64,
          486  +		ip : lib.store.inet
          487  +	): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) }
          488  +		var r = queries.actor_session_fetch.exec(src, aid, ip)
          489  +		if r.sz == 0 then goto fail end
          490  +		do defer r:free()
          491  +
          492  +			if r:null(0,0) then goto fail end
          493  +
          494  +			var a = row_to_actor(&r, 0)
          495  +			a.ptr.source = src
          496  +
          497  +			var au = [lib.stat(lib.store.auth)] { ok = true }
          498  +			au.val.aid = aid
          499  +			au.val.uid = a.ptr.id
          500  +			if not r:null(0,10) then -- restricted?
          501  +				au.val.privs:clear()
          502  +				(au.val.privs.post   << r:bool(0,11)) 
          503  +				(au.val.privs.edit   << r:bool(0,12))
          504  +				(au.val.privs.acct   << r:bool(0,13))
          505  +				(au.val.privs.upload << r:bool(0,14))
          506  +				(au.val.privs.censor << r:bool(0,15))
          507  +				(au.val.privs.admin  << r:bool(0,16))
          508  +			else au.val.privs:fill() end
          509  +
          510  +			return au, a
          511  +		end
          512  +
          513  +		::fail:: return [lib.stat   (lib.store.auth) ] { ok = false        },
          514  +			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
          515  +	end];
   446    516   }
   447    517   
   448    518   return b

Modified cmdparse.t from [bad20dd1d0] to [50677a3c0c].

     2      2   return function(tbl)
     3      3   	local options = terralib.types.newstruct('options') do
     4      4   		local flags = '' for _,d in pairs(tbl) do flags = flags .. d[1] end
     5      5   		local helpstr = 'usage: parsav [-' .. flags .. '] [<arg>...]\n'
     6      6   		options.entries = {
     7      7   			{field = 'arglist', type = lib.mem.ptr(rawstring)}
     8      8   		}
     9         -		local shortcases, longcases, init = {}, {}, {}
            9  +		local shortcases, longcases, init, verifiers = {}, {}, {}, {}
    10     10   		local self = symbol(&options)
    11     11   		local arg = symbol(rawstring)
    12         -		local idxo = symbol(uint)
           12  +		local idx = symbol(uint)
           13  +		local argv = symbol(&rawstring)
           14  +		local argc = symbol(int)
           15  +		local optstack = symbol(intptr)
    13     16   		local skip = label()
    14     17   		local sanitize = function(s) return s:gsub('_','-') end
    15     18   		for o,desc in pairs(tbl) do
    16     19   			local consume = desc[3] or 0
    17     20   			options.entries[#options.entries + 1] = {
    18         -				field = o, type = (consume > 0) and uint or bool
           21  +				field = o, type = (consume > 0) and &rawstring or bool
    19     22   			}
    20     23   			helpstr = helpstr .. string.format('    -%s --%s: %s\n',
    21     24   				desc[1], sanitize(o), desc[2])
    22     25   		end
    23     26   		for o,desc in pairs(tbl) do
    24     27   			local flag = desc[1]
    25     28   			local consume = desc[3] or 0
    26         -			init[#init + 1] = quote [self].[o] = [consume > 0 and 0 or false] end
    27         -			local ch if consume > 0 then ch = quote
    28         -				[self].[o] = idxo
    29         -				idxo = idxo + consume
    30         -			end else ch = quote
           29  +			init[#init + 1] = quote [self].[o] = [(consume > 0 and `nil) or false] end
           30  +			local ch if consume > 0 then
           31  +				ch = quote
           32  +					[self].[o] = argv+(idx+1+optstack)
           33  +					optstack = optstack + consume
           34  +				end
           35  +				verifiers[#verifiers+1] = quote
           36  +					var terminus = argv + argc
           37  +					if [self].[o] ~= nil and [self].[o] >= terminus then
           38  +						lib.bail(['missing argument for command line option ' .. sanitize(o)])
           39  +					end
           40  +				end
           41  +			else ch = quote
    31     42   				[self].[o] = true
    32     43   			end end
    33     44   			shortcases[#shortcases + 1] = quote
    34     45   				case [int8]([string.byte(flag)]) then [ch] end
    35     46   			end
    36     47   			longcases[#longcases + 1] = quote
    37     48   				if lib.str.cmp([arg]+2, [sanitize(o)]) == 0 then [ch] goto [skip] end
    38     49   			end
    39     50   		end
    40     51   		terra options:free() self.arglist:free() end
    41         -		options.methods.parse = terra([self], argc: int, argv: &rawstring)
           52  +		options.methods.parse = terra([self], [argc], [argv])
    42     53   			[init]
    43     54   			var parseopts = true
    44         -			var [idxo] = 1
           55  +			var [optstack] = 0
    45     56   			self.arglist = lib.mem.heapa(rawstring, argc)
    46     57   			var finalargc = 0
    47         -			for i=1,argc do
    48         -				var [arg] = argv[i]
    49         -				if arg[0] == ('-')[0] and parseopts then
    50         -					if arg[1] == ('-')[0] then -- long option
           58  +			for [idx]=1,argc do
           59  +				var [arg] = argv[idx]
           60  +				if optstack > 0 then optstack = optstack - 1 goto [skip] end
           61  +				if arg[0] == @'-' and parseopts then
           62  +					if arg[1] == @'-' then -- long option
    51     63   						if arg[2] == 0 then -- last option
    52     64   							parseopts = false
    53     65   						else [longcases] end
    54     66   					else -- short options
    55         -						var j = 1
    56         -						while arg[j] ~= 0 do
           67  +						var j = 1 while arg[j] ~= 0 do
    57     68   							switch arg[j] do [shortcases] end
    58     69   							j = j + 1
    59     70   						end
    60     71   					end
    61     72   				else
    62     73   					self.arglist.ptr[finalargc] = arg
    63     74   					finalargc = finalargc + 1
    64     75   				end
    65     76   				::[skip]::
    66     77   			end
           78  +			[verifiers]
    67     79   			if finalargc == 0 then self.arglist:free()
    68     80   							  else self.arglist:resize(finalargc) end
    69     81   		end
    70     82   		options.helptxt = helpstr
    71     83   	end
    72     84   	return options
    73     85   end

Modified config.lua from [53779583db] to [7cb566b503].

    36     36   		id = u.rndstr(6);
    37     37   		release = u.ingest('release');
    38     38   		when = os.date();
    39     39   	};
    40     40   	feat = {};
    41     41   	backends = defaultlist('parsav_backends', 'pgsql');
    42     42   	braingeniousmode = false;
           43  +	embeds = {
           44  +		{'style.css', 'text/css'};
           45  +	};
    43     46   }
    44     47   if u.ping '.fslckout' or u.ping '_FOSSIL_' then
    45     48   	if u.ping '_FOSSIL_' then default_os = 'windows' end
    46     49   	conf.build.branch = u.exec { 'fossil', 'branch', 'current' }
    47     50   	conf.build.checkout = (u.exec { 'fossil', 'sql',
    48     51   		[[select value from localdb.vvar where name = 'checkout-hash']]
    49     52   	}):gsub("^'(.*)'$", '%1')

Modified crypt.t from [709e2a6426] to [48369b50e0].

    13     13   const.maxpemsz = math.floor((const.keybits / 8)*6.4) + 128 -- idk why this formula works but it basically seems to
    14     14   
    15     15   local ctx = lib.pk.mbedtls_pk_context
    16     16   
    17     17   local struct hashalg { id: uint8 bytes: intptr }
    18     18   local m = {
    19     19   	pemfile = uint8[const.maxpemsz];
    20         -	alg = {
    21         -		sha1 =   `hashalg {id = lib.md.MBEDTLS_MD_SHA1; bytes = 160/8};
    22         -		sha256 = `hashalg {id = lib.md.MBEDTLS_MD_SHA256; bytes = 256/8};
    23         -		sha512 = `hashalg {id = lib.md.MBEDTLS_MD_SHA512; bytes = 512/8};
    24         -		sha384 = `hashalg {id = lib.md.MBEDTLS_MD_SHA384; bytes = 384/8};
    25         -		sha224 = `hashalg {id = lib.md.MBEDTLS_MD_SHA224; bytes = 224/8};
    26         -		-- md5 = {id = lib.md.MBEDTLS_MD_MD5};-- !!!
    27         -	};
           20  +	algsz = {
           21  +		sha1 =   160/8;
           22  +		sha256 = 256/8;
           23  +		sha512 = 512/8;
           24  +		sha384 = 384/8;
           25  +		sha224 = 224/8;
           26  +	}
    28     27   }
           28  +m.alg = {
           29  +	sha1 =   `hashalg {id = lib.md.MBEDTLS_MD_SHA1;   bytes = m.algsz.sha1};
           30  +	sha256 = `hashalg {id = lib.md.MBEDTLS_MD_SHA256; bytes = m.algsz.sha256};
           31  +	sha512 = `hashalg {id = lib.md.MBEDTLS_MD_SHA512; bytes = m.algsz.sha512};
           32  +	sha384 = `hashalg {id = lib.md.MBEDTLS_MD_SHA384; bytes = m.algsz.sha384};
           33  +	sha224 = `hashalg {id = lib.md.MBEDTLS_MD_SHA224; bytes = m.algsz.sha224};
           34  +	-- md5 = {id = lib.md.MBEDTLS_MD_MD5};-- !!!
           35  +};
    29     36   local callbacks = {}
    30     37   if config.feat.randomizer == 'kern' then
    31     38   	local rnd = terralib.externfunction('getrandom', {&opaque, intptr, uint} -> ptrdiff);
    32     39   	terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr): int
    33     40   		return rnd(dest, sz, 0)
    34     41   	end
    35     42   elseif config.feat.randomizer == 'devfs' then

Modified file.t from [fc7770c3f7] to [a196c3def2].

     1      1   -- vim: ft=terra
     2      2   -- TODO: add support for windows IO calls
     3      3   local handle_type = int
     4      4   local posix = terralib.includec 'fcntl.h'
     5      5   local unistd = terralib.includec 'unistd.h'
            6  +local mm = terralib.includec 'sys/mman.h'
     6      7   
     7      8   struct file {
     8      9   	handle: handle_type
     9     10   	read: bool
    10     11   	write: bool
    11     12   }
    12     13   
................................................................................
    61     62   		elseif wh == [file.seek.eof] then
    62     63   			whence = unistd.SEEK_END
    63     64   		else lib.bail('invalid seek mode') end
    64     65   	
    65     66   		return unistd.lseek(self.handle, ofs, whence)
    66     67   	end;
    67     68   }
           69  +
           70  +terra file:len(): intptr
           71  +	var cur = self:seek(0, [file.seek.ofs])
           72  +	var sz = self:seek(0, [file.seek.eof])
           73  +	self:seek(cur, [file.seek.abs])
           74  +	return sz
           75  +end
           76  +
           77  +local struct mapping {
           78  +	addr: &opaque
           79  +	sz: intptr
           80  +}
           81  +terra mapping:unmap()
           82  +	lib.dbg('releasing file mapping')
           83  +	return mm.munmap(self.addr, self.sz)
           84  +end
           85  +-- provide for syncing mechanism?
           86  +
           87  +terra file:mapin(ofs: intptr, sz: intptr)
           88  +	var prot = 0
           89  +	if self.read then prot = mm.PROT_READ end 
           90  +	if self.write then prot = prot or mm.PROT_WRITE end
           91  +	if sz == 0 then sz = self:len() end
           92  +	lib.dbg('mapping file into memory')
           93  +	return mapping {
           94  +		addr = mm.mmap(nil, sz, prot, mm.MAP_PRIVATE, self.handle, ofs);
           95  +		sz = sz;
           96  +	}
           97  +end
    68     98   
    69     99   return file

Modified http.t from [6d6c40fc94] to [e5e590a634].

     1      1   -- vim: ft=terra
     2      2   local m = {}
     3      3   local util = dofile('common.lua')
     4      4   
            5  +m.method = lib.enum { 'get', 'post', 'head', 'options', 'put', 'delete' }
            6  +
            7  +m.findheader = terralib.externfunction('mg_http_get_header', {&lib.net.mg_http_message, rawstring} -> &lib.mem.ref(int8)) -- unfortunately necessary to access this function, as its return type conflicts with a function name
            8  +
     5      9   struct m.header {
     6     10   	key: rawstring
     7     11   	value: rawstring
     8     12   }
     9     13   struct m.page {
    10     14   	respcode: uint16
    11     15   	body: lib.mem.ptr(int8)
................................................................................
    15     19   local resps = {
    16     20   	[200] = 'OK';
    17     21   	[201] = 'Created';
    18     22   	[301] = 'Moved Permanently';
    19     23   	[302] = 'Found';
    20     24   	[303] = 'See Other';
    21     25   	[307] = 'Temporary Redirect';
    22         -	[404] = 'Not Found';
           26  +	[400] = 'Bad Request';
    23     27   	[401] = 'Unauthorized';
           28  +	[402] = 'Payment Required';
    24     29   	[403] = 'Forbidden';
           30  +	[404] = 'Not Found';
           31  +	[405] = 'Method Not Allowed';
           32  +	[406] = 'Not Acceptable';
    25     33   	[418] = 'I\'m a teapot';
    26     34   	[405] = 'Method Not Allowed';
    27     35   	[500] = 'Internal Server Error';
    28     36   }
    29     37   local resptext = symbol(rawstring)
    30     38   local resplen = symbol(intptr)
    31     39   local respbranches = {}
................................................................................
    36     44   	end
    37     45   end
    38     46   m.codestr = terra(code: uint16)
    39     47   	var [resptext] var [resplen]
    40     48   	switch code do [respbranches] end
    41     49   	return resptext, resplen
    42     50   end
           51  +
           52  +terra m.hier(uri: lib.mem.ptr(int8)): lib.mem.ptr(lib.mem.ref(int8))
           53  +	if uri.ct == 0 then return [lib.mem.ptr(lib.mem.ref(int8))] { ptr = nil, ct = 0 } end
           54  +	var sz = 1
           55  +	var start = 0 if uri.ptr[0] == @'/' then start = 1 end
           56  +	for i = start, uri.ct do if uri.ptr[i] == @'/' then sz = sz + 1 end end
           57  +	var lst = lib.mem.heapa([lib.mem.ref(int8)], sz)
           58  +	if sz == 0 then
           59  +		lst.ptr[0].ptr = uri.ptr
           60  +		lst.ptr[0].ct = uri.ct
           61  +		return lst
           62  +	end
           63  +
           64  +	var idx: intptr = 0
           65  +	var len: intptr = 0
           66  +	lst.ptr[0].ptr = uri.ptr
           67  +	for i = 0, uri.ct do
           68  +		if uri.ptr[i] == @'/' then
           69  +			if len == 0 then
           70  +				lst.ptr[idx].ptr = lst.ptr[idx].ptr + 1
           71  +				goto skip
           72  +			end
           73  +			lst.ptr[idx].ct = len
           74  +			idx = idx + 1
           75  +			lst.ptr[idx].ptr = uri.ptr + i + 1
           76  +			len = 0
           77  +		else
           78  +			len = len + 1
           79  +		end
           80  +	::skip::end
           81  +	lst.ptr[idx].ct = len
           82  +	
           83  +	return lst
           84  +end
           85  +
    43     86   m.page.methods = {
    44     87   	free = terra(self: &m.page)
    45     88   		self.body:free()
    46     89   		self.headers:free()
    47     90   	end;
    48     91   	send = terra(self: &m.page, con: &lib.net.mg_connection)
    49     92   		var code: rawstring

Modified makefile from [40c8adaeb1] to [2d2acfe121].

    24     24   	mkdir $@
    25     25   # generate a shim static library so mongoose cooperates
    26     26   # with the build apparatus. note that parsav is designed
    27     27   # to be fronted by a real web server like nginx if SSL
    28     28   # is to be used, so we don't turn on SSL in mongoose
    29     29   lib/mongoose/libmongoose.a: lib/mongoose lib/mongoose/mongoose.c lib/mongoose/mongoose.h
    30     30   	$(CC) -c $</mongoose.c -o lib/mongoose/mongoose.o \
    31         -		-DMG_ENABLE_THREADS \
    32         -		-DMG_ENABLE_IPV6 \
    33         -		-DMG_ENABLE_HTTP_WEBDAV \
           31  +		-DMG_ENABLE_THREADS=1 \
           32  +		-DMG_ENABLE_IPV6=1 \
           33  +		-DMG_ENABLE_HTTP_WEBDAV=1 \
    34     34   		-DMG_ENABLE_HTTP_WEBSOCKET=0
    35     35   	ar rcs $@ lib/mongoose/*.o
    36     36   	ranlib $@
    37     37   
    38     38   lib/json-c/Makefile: lib/json-c lib/json-c/CMakeLists.txt
    39     39   	cd lib/json-c && cmake .
    40     40   lib/json-c/libjson-c.a: lib/json-c/Makefile

Modified math.t from [f642fd73ba] to [f661c3d77e].

    78     78   	for i = 0, len do
    79     79   		var v, ok = m.shorthand.cval(s[i])
    80     80   		if ok == false then return 0, false end
    81     81   		val = (val * 64) + v
    82     82   	end
    83     83   	return val, true
    84     84   end
           85  +
           86  +terra m.truncate64(val: &uint8, len: intptr): uint64
           87  +	var r: uint64 = 0
           88  +	for i=0, len do
           89  +		r = r << 3
           90  +		r = r + @val
           91  +		val = val + 1
           92  +	end
           93  +	return r
           94  +end
           95  +
           96  +m.biggest = macro(function(a,b)
           97  +	local ty = a.tree.type
           98  +	if b.tree.type.bytes > ty.bytes then ty = b.tree.type end
           99  +	return quote
          100  +		var _a = [a]
          101  +		var _b = [b]
          102  +		var r: ty if _a > _b then r = _a else r = _b end
          103  +	in r end
          104  +end)
          105  +
          106  +m.smallest = macro(function(a,b)
          107  +	local ty = a.tree.type
          108  +	if b.tree.type.bytes < ty.bytes then ty = b.tree.type end
          109  +	return quote
          110  +		var _a = [a]
          111  +		var _b = [b]
          112  +		var r: ty if _a < _b then r = _a else r = _b end
          113  +	in r end
          114  +end)
    85    115   
    86    116   terra m.hexdigit(hb: uint8): int8
    87    117   	var a = hb and 0x0F
    88    118   	if a < 10 then return 0x30 + a
    89    119   	else return 0x61 + (a-10) end
    90    120   end
    91    121   

Modified mem.t from [07d2cb4a7e] to [e41dd1991a].

    15     15   	local p = m.ptr(ty:astype())
    16     16   	return `p {
    17     17   		ptr = [&ty:astype()](m.heapa_raw(sizeof(ty) * sz));
    18     18   		ct = sz;
    19     19   	}
    20     20   end)
    21     21   
    22         -m.ptr = terralib.memoize(function(ty)
    23         -	local t = terralib.types.newstruct(string.format('ptr<%s>', ty))
           22  +local function mkptr(ty, dyn)
           23  +	local t = terralib.types.newstruct(string.format('%s<%s>', dyn and 'ptr' or 'ref', ty))
    24     24   	t.entries = {
    25     25   		{'ptr', &ty};
    26     26   		{'ct', intptr};
    27     27   	}
    28     28   	t.ptr_basetype = ty
    29     29   	local recurse = false
    30     30   	if ty:isstruct() then
    31     31   		if ty.methods.free then recurse = true end
    32     32   	end
    33     33   	t.metamethods.__not = macro(function(self)
    34     34   		return `self.ptr
    35     35   	end)
    36         -	t.methods = {
    37         -		free = terra(self: &t): bool
    38         -			[recurse and quote
    39         -				self.ptr:free()
    40         -			end or {}]
    41         -			if self.ct > 0 then
    42         -				m.heapf(self.ptr)
    43         -				self.ct = 0
    44         -				return true
           36  +	if dyn then
           37  +		t.methods = {
           38  +			free = terra(self: &t): bool
           39  +				[recurse and quote
           40  +					self.ptr:free()
           41  +				end or {}]
           42  +				if self.ct > 0 then
           43  +					m.heapf(self.ptr)
           44  +					self.ct = 0
           45  +					return true
           46  +				end
           47  +				return false
           48  +			end;
           49  +			init = terra(self: &t, newct: intptr): bool
           50  +				var nv = [&ty](m.heapa_raw(sizeof(ty) * newct))
           51  +				if nv ~= nil then
           52  +					self.ptr = nv
           53  +					self.ct = newct
           54  +					return true
           55  +				else return false end
           56  +			end;
           57  +			resize = terra(self: &t, newct: intptr): bool
           58  +				var nv: &ty
           59  +				if self.ct > 0
           60  +					then nv = [&ty](m.heapr_raw(self.ptr, sizeof(ty) * newct))
           61  +					else nv = [&ty](m.heapa_raw(sizeof(ty) * newct))
           62  +				end
           63  +				if nv ~= nil then
           64  +					self.ptr = nv
           65  +					self.ct = newct
           66  +					return true
           67  +				else return false end
           68  +			end;
           69  +		}
           70  +		terra t:ensure(n: intptr)
           71  +			if self.ptr == nil then
           72  +				if not self:init(n) then return t {ptr=nil,ct=0} end
    45     73   			end
    46         -			return false
    47         -		end;
    48         -		init = terra(self: &t, newct: intptr): bool
    49         -			var nv = [&ty](m.heapa_raw(sizeof(ty) * newct))
    50         -			if nv ~= nil then
    51         -				self.ptr = nv
    52         -				self.ct = newct
    53         -				return true
    54         -			else return false end
    55         -		end;
    56         -		resize = terra(self: &t, newct: intptr): bool
    57         -			var nv: &ty
    58         -			if self.ct > 0
    59         -				then nv = [&ty](m.heapr_raw(self.ptr, sizeof(ty) * newct))
    60         -				else nv = [&ty](m.heapa_raw(sizeof(ty) * newct))
           74  +			if self.ct >= n then return @self end
           75  +			self:resize(n)
           76  +			return @self
           77  +		end
           78  +	end
           79  +	terra t:advance(n: intptr)
           80  +		self.ptr = self.ptr + n
           81  +		self.ct = self.ct - n
           82  +		return self.ptr
           83  +	end
           84  +	if not ty:isstruct() then
           85  +		terra t:cmp_raw(other: &ty)
           86  +			for i=0, self.ct do
           87  +				if self.ptr[i] ~= other[i] then return false end
    61     88   			end
    62         -			if nv ~= nil then
    63         -				self.ptr = nv
    64         -				self.ct = newct
    65         -				return true
    66         -			else return false end
    67         -		end;
    68         -	}
           89  +			return true
           90  +		end
           91  +		terra t:cmp(other: t)
           92  +			if other.ct ~= self.ct then return false end
           93  +			return self:cmp_raw(other.ptr)
           94  +		end
           95  +	end
    69     96   	return t
    70         -end)
           97  +end
           98  +
           99  +m.ptr = terralib.memoize(function(ty) return mkptr(ty, true) end)
          100  +m.ref = terralib.memoize(function(ty) return mkptr(ty, false) end)
    71    101   
    72    102   m.vec = terralib.memoize(function(ty)
    73    103   	local v = terralib.types.newstruct(string.format('vec<%s>', ty.name))
    74    104   	v.entries = {
    75    105   		{field = 'storage', type = m.ptr(ty)};
    76    106   		{field = 'sz', type = intptr};
    77    107   		{field = 'run', type = intptr};

Modified parsav.t from [fcc06cebed] to [41c6f93980].

     4      4   local buildopts, buildargs = util.parseargs{...}
     5      5   config = dofile('config.lua')
     6      6   
     7      7   lib = {
     8      8   	init = {};
     9      9   	load = function(lst)
    10     10   		for _, l in pairs(lst) do
    11         -			lib[l] = terralib.loadfile(l .. '.t')()
           11  +			local path = {}
           12  +			for m in l:gmatch('([^:]+)') do path[#path+1]=m end
           13  +			local tgt = lib
           14  +			for i=1,#path-1 do
           15  +				if tgt[path[i]] == nil then tgt[path[i]] = {} end
           16  +				tgt = tgt[path[i]]
           17  +			end
           18  +			tgt[path[#path]] = terralib.loadfile(l:gsub(':','/') .. '.t')()
    12     19   		end
    13     20   	end;
    14     21   	loadlib = function(name,hdr)
    15     22   		local p = config.pkg[name]
    16     23   		-- for _,v in pairs(p.dylibs) do
    17     24   		-- 	terralib.linklibrary(p.libdir .. '/' .. v)
    18     25   		-- end
................................................................................
    22     29   		return macro(function(v,...)
    23     30   			for ty,fn in pairs(tbl) do
    24     31   				if v.tree.type == ty then return fn(v,...) end
    25     32   			end
    26     33   			return (tbl[false])(v,...)
    27     34   		end)
    28     35   	end;
    29         -	emit_unitary = function(fd,...)
           36  +	emit_unitary = function(nl,fd,...)
    30     37   		local code = {}
    31     38   		for i,v in ipairs{...} do
    32     39   			if type(v) == 'string' or type(v) == 'number' then
    33     40   				local str = tostring(v)
    34     41   				code[#code+1] = `lib.io.send(2, str, [#str])
    35     42   			elseif type(v) == 'table' and #v == 2 then
    36     43   				code[#code+1] = `lib.io.send(2, [v[1]], [v[2]])
................................................................................
    38     45   				local str = tostring(v:asvalue())
    39     46   				code[#code+1] = `lib.io.send(2, str, [#str])
    40     47   			else
    41     48   				code[#code+1] = quote var n = v in
    42     49   					lib.io.send(2, n, lib.str.sz(n)) end
    43     50   			end
    44     51   		end
    45         -		code[#code+1] = `lib.io.send(fd, '\n', 1)
           52  +		if nl then code[#code+1] = `lib.io.send(fd, '\n', 1) end
    46     53   		return code
    47     54   	end;
    48         -	emitv = function(fd,...)
           55  +	emitv = function(nl,fd,...)
    49     56   		local vec = {}
    50     57   		local defs = {}
    51     58   		for i,v in ipairs{...} do
    52     59   			local str, ct
    53     60   			if type(v) == 'table' and v.tree and not (v.tree:is 'constant') then
    54     61   				if v.tree.type.convertible == 'tuple' then
    55     62   					str = `v._0
................................................................................
    66     73   				else--if v.tree:is 'constant' then
    67     74   					str = tostring(v:asvalue())
    68     75   				end
    69     76   				ct = ct or #str
    70     77   			end
    71     78   			vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](str), iov_len = ct}
    72     79   		end
    73         -		vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1}
           80  +		if nl then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1} end
    74     81   		return quote
    75     82   			[defs]
    76     83   			var strs = array( [vec] )
    77     84   		in lib.uio.writev(fd, strs, [#vec]) end
    78     85   	end;
    79     86   	trn = macro(function(cond, i, e)
    80     87   		return quote
    81     88   			var c: bool = [cond]
    82     89   			var r: i.tree.type
    83     90   			if c == true then r = i else r = e end
    84     91   		in r end
           92  +	end);
           93  +	coalesce = macro(function(...)
           94  +		local args = {...}
           95  +		local ty = args[1].tree.type
           96  +		local val = symbol(ty)
           97  +		local empty if ty.type == 'integer'
           98  +			then empty = `0
           99  +			else empty = `nil
          100  +		end
          101  +		local exp = quote val = [empty] end
          102  +
          103  +		for i=#args, 1, -1 do
          104  +			local v = args[i]
          105  +			exp = quote
          106  +				if [v] ~= [empty]
          107  +					then val = v
          108  +					else [exp]
          109  +				end
          110  +			end
          111  +		end
          112  +
          113  +		local q = quote
          114  +			var [val]
          115  +			[exp]
          116  +		in val end
          117  +		return q
    85    118   	end);
    86    119   	proc = {
    87    120   		exit = terralib.externfunction('exit', int -> {});
    88    121   		getenv = terralib.externfunction('getenv', rawstring -> rawstring);
    89    122   	};
    90    123   	io = {
    91    124   		send = terralib.externfunction('write', {int, rawstring, intptr} -> ptrdiff);
................................................................................
    97    130   	str = { sz = terralib.externfunction('strlen', rawstring -> intptr) };
    98    131   	copy = function(tbl)
    99    132   		local new = {}
   100    133   		for k,v in pairs(tbl) do new[k] = v end
   101    134   		setmetatable(new, getmetatable(tbl))
   102    135   		return new
   103    136   	end;
          137  +	osclock = terralib.includec 'time.h';
   104    138   }
   105    139   if config.posix then
   106    140   	lib.uio = terralib.includec 'sys/uio.h';
   107    141   	lib.emit = lib.emitv -- use more efficient call where available
   108    142   else lib.emit = lib.emit_unitary end
   109    143   
          144  +local starttime = global(lib.osclock.time_t)
          145  +local lastnoisetime = global(lib.osclock.time_t)
   110    146   local noise = global(uint8,1)
   111    147   local noise_header = function(code,txt,mod)
   112    148   	if mod then
   113         -		return string.format('\27[%s;1m(parsav::%s %s)\27[m ', code,mod,txt)
          149  +		return string.format('\27[%s;1m(%s %s)\27[m ', code,mod,txt)
   114    150   	else
   115         -		return string.format('\27[%s;1m(parsav %s)\27[m ', code,txt)
          151  +		return string.format('\27[%s;1m(%s)\27[m ', code,txt)
          152  +	end
          153  +end
          154  +
          155  +local terra timehdr()
          156  +	var now = lib.osclock.time(nil)
          157  +	var diff = now - lastnoisetime
          158  +	if diff > 30 then -- print cur time
          159  +		lastnoisetime = now
          160  +		var curtime: int8[26]
          161  +		lib.osclock.ctime_r(&now, &curtime[0])
          162  +		for i=0,26 do if curtime[i] == @'\n' then curtime[i] = 0 break end end -- :/
          163  +		[ lib.emit(false, 2, '\27[1m[', `&curtime[0], ']\27[;36m\n +00 ') ]
          164  +	else -- print time since last msg
          165  +		var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0)
          166  +		[ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ]
   116    167   	end
   117    168   end
          169  +
   118    170   local defrep = function(level,n,code)
   119    171   	return macro(function(...)
   120         -		local q = lib.emit(2, noise_header(code,n), ...)
   121         -		return quote
   122         -			if noise >= level then [q] end
   123         -		end
          172  +		local q = lib.emit(true, 2, noise_header(code,n), ...)
          173  +		return quote if noise >= level then timehdr(); [q] end end
   124    174   	end);
   125    175   end
   126    176   lib.dbg = defrep(3,'debug', '32')
   127    177   lib.report = defrep(2,'info', '35')
   128    178   lib.warn = defrep(1,'warn', '33')
   129    179   lib.bail = macro(function(...)
   130         -	local q = lib.emit(2, noise_header('31','fatal'), ...)
          180  +	local q = lib.emit(true, 2, noise_header('31','fatal'), ...)
   131    181   	return quote
   132         -		[q]
          182  +		timehdr(); [q]
   133    183   		lib.proc.exit(1)
   134    184   	end
   135    185   end);
   136    186   lib.stat = terralib.memoize(function(ty)
   137    187   	local n = struct {
   138    188   		ok: bool
   139    189   		union {
................................................................................
   159    209   lib.set = function(tbl)
   160    210   	local bytes = math.ceil(#tbl / 8)
   161    211   	local o = {}
   162    212   	for i, name in ipairs(tbl) do o[name] = i end
   163    213   	local struct set { _store: uint8[bytes] }
   164    214   	local struct bit { _v: intptr _set: &set}
   165    215   	terra set:clear() for i=0,bytes do self._store[i] = 0 end end
          216  +	terra set:fill() for i=0,bytes do self._store[i] = 0xFF end end
   166    217   	set.members = tbl
   167    218   	set.name = string.format('set<%s>', table.concat(tbl, '|'))
   168    219   	set.metamethods.__entrymissing = macro(function(val, obj)
   169    220   		if o[val] == nil then error('value ' .. val .. ' not in set') end
   170    221   		return `bit { _v=[o[val] - 1], _set = &obj }
   171    222   	end)
   172    223   	set.methods.dump = macro(function(self)
................................................................................
   219    270   lib.pk = lib.loadlib('mbedtls','mbedtls/pk.h')
   220    271   lib.md = lib.loadlib('mbedtls','mbedtls/md.h')
   221    272   lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h')
   222    273   lib.net = lib.loadlib('mongoose','mongoose.h')
   223    274   lib.pq = lib.loadlib('libpq','libpq-fe.h')
   224    275   
   225    276   lib.load {
   226         -	'mem', 'str', 'file', 'math', 'crypt';
   227         -	'http', 'tpl', 'store';
          277  +	'mem',  'math', 'str', 'file', 'crypt';
          278  +	'http', 'session', 'tpl', 'store';
   228    279   }
   229    280   
   230    281   local be = {}
   231    282   for _, b in pairs(config.backends) do
   232    283   	be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')()
   233    284   end
   234    285   lib.store.backends = global(`array([be]))
   235    286   
   236    287   lib.cmdparse = terralib.loadfile('cmdparse.t')()
   237         -lib.srv = terralib.loadfile('srv.t')()
   238    288   
   239    289   do local collate = function(path,f, ...)
   240    290   	return loadfile(path..'/'..f..'.lua')(path, ...)
   241    291   end
   242    292   data = {
   243    293   	view = collate('view','load');
          294  +	static = {};
          295  +	stmap = global(lib.mem.ref(int8)[#config.embeds]); -- array of pointers to static content
   244    296   } end
          297  +for i,e in ipairs(config.embeds) do local v = e[1]
          298  +	local fh = io.open('static/' .. v,'r')
          299  +	if fh == nil then error('static file ' .. v .. ' missing') end
          300  +	data.static[v] = fh:read('*a') fh:close()
          301  +end
   245    302   
   246    303   -- slightly silly -- because we're using plain lua to gather up
   247    304   -- the template sources, they have to be actually turned into
   248    305   -- templates in the terra code, so we "mutate" them here
   249    306   for k,v in pairs(data.view) do
   250    307   	local t = lib.tpl.mk { body = v, id = 'view/'..k }
   251    308   	data.view[k] = t
   252    309   end
   253    310   
   254         -local pemdump = macro(function(pub, kp)
   255         -	local msg = (pub:asvalue() and ' * emitting public key\n') or ' * emitting private key\n'
   256         -	return quote
   257         -		var buf: lib.crypt.pemfile
   258         -		lib.mem.zero(buf)
   259         -		lib.crypt.pem(pub, &kp, buf)
   260         -		lib.emit(msg, buf, '\n')
   261         -		--lib.io.send(1, msg, [#msg])
   262         -		--lib.io.send(1, [rawstring](&buf), lib.str.sz([rawstring](&buf)))
   263         -		--lib.io.send(1, '\n', 1)
   264         -	end
   265         -end)
          311  +lib.load {
          312  +	'srv';
          313  +	'render:profile';
          314  +	'render:userpage';
          315  +	'route';
          316  +}
   266    317   
   267    318   do
   268    319   	local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
   269    320   	terra version() lib.io.send(1, p, [#p]) end
   270    321   end
          322  +
   271    323   terra noise_init()
          324  +	starttime = lib.osclock.time(nil)
          325  +	lastnoisetime = 0
   272    326   	var n = lib.proc.getenv('parsav_noise')
   273    327   	if n ~= nil then
   274    328   		if n[0] >= 0x30 and n[0] <= 0x39 and n[1] == 0 then
   275    329   			noise = n[0] - 0x30
   276    330   			return
   277    331   		end
   278    332   	end
................................................................................
   280    334   end
   281    335   
   282    336   local options = lib.cmdparse {
   283    337   	version = {'V', 'display information about the binary build and exit'};
   284    338   	quiet = {'q', 'do not print to standard out'};
   285    339   	help = {'h', 'display this list'};
   286    340   	backend_file = {'b', 'init from specified backend file', 1};
          341  +	static_dir = {'S', 'directory with overrides for static content', 1};
          342  +	builtin_data = {'B', 'do not load static content overrides at runtime under any circumstances'};
   287    343   }
   288    344   
          345  +
          346  +local static_setup = quote end
          347  +local mapin = quote end
          348  +local odir = symbol(rawstring)
          349  +local pathbuf = symbol(lib.str.acc)
          350  +for i,e in ipairs(config.embeds) do local v = e[1]
          351  +	local d = data.static[v]
          352  +	static_setup = quote [static_setup]
          353  +		([data.stmap])[([i-1])] = ([lib.mem.ref(int8)] { ptr = [d], ct = [#d] })
          354  +	end
          355  +	mapin = quote [mapin]
          356  +		var osz = pathbuf.sz
          357  +		pathbuf:push([v],[#v])
          358  +		var f = lib.file.open(pathbuf.buf, [lib.file.mode.read])
          359  +		if f.ok then defer f.val:close()
          360  +			var map = f.val:mapin(0,0) -- currently maps are leaked, maybe do something more elegant in future
          361  +			lib.report('loading static override content from ', pathbuf.buf)
          362  +			([data.stmap])[([i-1])] = ([lib.mem.ref(int8)] {
          363  +				ptr = [rawstring](map.addr);
          364  +				ct = map.sz;
          365  +			})
          366  +		end
          367  +		pathbuf.sz = osz
          368  +	end
          369  +end
          370  +local terra static_init(mode: &options)
          371  +	[static_setup]
          372  +	if mode.builtin_data then return end
          373  +
          374  +	var [odir] = lib.proc.getenv('parsav_override_dir')
          375  +	if mode.static_dir ~= nil then
          376  +		odir=@mode.static_dir
          377  +	end
          378  +	if odir == nil then return end
          379  +
          380  +	var [pathbuf] defer pathbuf:free()
          381  +	pathbuf:compose(odir,'/')
          382  +	[mapin]
          383  +end
          384  +
   289    385   terra entry(argc: int, argv: &rawstring): int
   290    386   	if argc < 1 then lib.bail('bad invocation!') end
   291    387   
   292    388   	noise_init()
   293    389   	[lib.init]
   294    390   
   295    391   	-- shut mongoose the fuck up
   296    392   	lib.net.mg_log_set_callback([terra(msg: &opaque, sz: int, u: &opaque) end], nil)
   297         -	var srv: lib.srv
          393  +	var srv: lib.srv.overlord
   298    394   
   299    395   	do var mode: options
   300    396   		mode:parse(argc,argv) defer mode:free()
          397  +		static_init(&mode)
   301    398   		if mode.version then version() return 0 end
   302    399   		if mode.help then
   303    400   			lib.io.send(1,  [options.helptxt], [#options.helptxt])
   304    401   			return 0
   305    402   		end
   306    403   		var cnf: rawstring
   307         -		if mode.backend_file ~= 0
   308         -			then if mode.arglist.ct >= mode.backend_file
   309         -					then cnf = mode.arglist.ptr[mode.backend_file - 1]
   310         -					else lib.bail('bad invocation, backend file not specified') end
          404  +		if mode.backend_file ~= nil
          405  +			then cnf = @mode.backend_file
   311    406   			else cnf = lib.proc.getenv('parsav_backend_file')
   312    407   		end
   313    408   		if cnf == nil then cnf = "backend.conf" end
   314    409   
   315    410   		srv:start(cnf)
   316    411   	end
   317    412   

Added render/profile.t version [a405db9158].

            1  +-- vim: ft=terra
            2  +local terra 
            3  +render_profile(actor: &lib.store.actor)
            4  +	var profile = data.view.profile {
            5  +		nym = lib.coalesce(actor.nym, actor.handle);
            6  +		bio = lib.coalesce(actor.bio, "tall, dark, and mysterious");
            7  +		xid = actor.xid;
            8  +		avatar = "/no-avatars-yet.png";
            9  +
           10  +		nposts = '0', nfollows = '0';
           11  +		nfollowers = '0', nmutuals = '0';
           12  +		tweetday = 'novembuary 67th';
           13  +	}
           14  +
           15  +	return profile:tostr()
           16  +end
           17  +
           18  +return render_profile

Added render/userpage.t version [052285d84c].

            1  +-- vim: ft=terra
            2  +local terra 
            3  +render_userpage(co: &lib.srv.convo, actor: &lib.store.actor)
            4  +	var ti: lib.str.acc defer ti:free()
            5  +	if co.aid ~= 0 and co.who.id == actor.id then
            6  +		ti:compose('my profile')
            7  +	else
            8  +		ti:compose('profile :: ', actor.handle)
            9  +	end
           10  +	var pftxt = lib.render.profile(actor) defer pftxt:free()
           11  +
           12  +	var doc = data.view.docskel {
           13  +		instance = co.srv.cfg.instance.ptr;
           14  +		title = ti.buf;
           15  +		body = pftxt.ptr;
           16  +		class = 'profile';
           17  +	}
           18  +
           19  +	var hdrs = array(
           20  +		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
           21  +	)
           22  +	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
           23  +end
           24  +
           25  +return render_userpage

Added route.t version [70d0da6b8e].

            1  +-- vim: ft=terra
            2  +local r = lib.srv.route
            3  +local method = lib.http.method
            4  +local http = {}
            5  +
            6  +terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
            7  +	var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
            8  +	for i=2,uri.ct do
            9  +		if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end
           10  +	end
           11  +	if handle.ct == 0 then
           12  +		handle.ct = uri.ct - 2
           13  +		uri:advance(uri.ct)
           14  +	else
           15  +		if handle.ct + 2 < uri.ct then
           16  +			uri:advance(handle.ct + 2)
           17  +			--uri.ptr = uri.ptr + (handle.ct + 2)
           18  +			--uri.ct = uri.ct - (handle.ct + 2)
           19  +		end
           20  +	end
           21  +
           22  +	lib.dbg('looking up user by xid "', {handle.ptr,handle.ct} ,'", path: ', {uri.ptr,uri.ct})
           23  +
           24  +	var path = lib.http.hier(uri) defer path:free()
           25  +	for i=0,path.ct do
           26  +		lib.dbg('got path component ', {path.ptr[i].ptr, path.ptr[i].ct})
           27  +	end
           28  +
           29  +	var actor = co.srv:actor_fetch_xid(handle)
           30  +	if actor.ptr == nil then
           31  +		co:complain(404,'no such user','no such user known to this server')
           32  +		return
           33  +	end
           34  +	defer actor:free()
           35  +
           36  +	lib.render.userpage(co, actor.ptr)
           37  +end
           38  +
           39  +terra http.actor_profile_uid(co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), meth: method.t)
           40  +	if path.ct < 2 then
           41  +		co:complain(404,'bad url','invalid user url')
           42  +		return
           43  +	end
           44  +
           45  +	var uid, ok = lib.math.shorthand.parse(path.ptr[1].ptr, path.ptr[1].ct)
           46  +	if not ok then
           47  +		co:complain(400, 'bad user ID', 'that user ID is not valid')
           48  +		return
           49  +	end
           50  +
           51  +	var actor = co.srv:actor_fetch_uid(uid)
           52  +	if actor.ptr == nil then
           53  +		co:complain(404, 'no such user', 'no user by that ID is known to this instance')
           54  +		return
           55  +	end
           56  +	defer actor:free()
           57  +
           58  +	lib.render.userpage(co, actor.ptr)
           59  +end
           60  +
           61  +do local branches = quote end
           62  +	local filename, flen = symbol(&int8), symbol(intptr)
           63  +	local page = symbol(lib.http.page)
           64  +	local send = label()
           65  +	local storage = data.stmap
           66  +	for i,e in ipairs(config.embeds) do local id,mime = e[1],e[2]
           67  +		local d = data.static[id]
           68  +		branches = quote [branches];
           69  +			if lib.str.ncmp(filename, id, lib.math.biggest([#id], flen)) == 0 then
           70  +				page.headers.ptr[0].value = mime;
           71  +				page.body = [lib.mem.ptr(int8)] {
           72  +					ptr = storage[([i-1])].ptr;
           73  +					ct  = storage[([i-1])].ct;
           74  +				}
           75  +				goto [send]
           76  +			end
           77  +		end
           78  +	end
           79  +	terra http.static_content(co: &lib.srv.convo, [filename], [flen])
           80  +		var hdrs = array(lib.http.header{'Content-Type',nil})
           81  +		var [page] = lib.http.page {
           82  +			respcode = 200;
           83  +			headers = [lib.mem.ptr(lib.http.header)] {
           84  +				ptr = &hdrs[0], ct = 1
           85  +			}
           86  +		}
           87  +		[branches]
           88  +		do return false end
           89  +		::[send]:: page:send(co.con) return true
           90  +	end
           91  +end
           92  +
           93  +http.static_content:printpretty()
           94  +
           95  +-- entry points
           96  +terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
           97  +	if uri.ptr[0] ~= @'/' then
           98  +		co:complain(404, 'what the hell', 'how did you do that')
           99  +	elseif uri.ptr[1] == @'@' then
          100  +		http.actor_profile_xid(co, uri, meth)
          101  +	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
          102  +		if meth ~= method.get then goto wrongmeth end
          103  +		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
          104  +	else
          105  +		var path = lib.http.hier(uri) defer path:free()
          106  +		if path.ptr[0]:cmp(lib.str.lit('user')) then
          107  +			http.actor_profile_uid(co, path, meth)
          108  +		else goto notfound end
          109  +	end
          110  +
          111  +	::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this path') do return end
          112  +	::notfound:: co:complain(404, 'not found', 'no such resource available') do return end
          113  +end

Modified schema.sql from [6d12737279] to [636689e0dd].

     1      1   \prompt 'domain name: ' domain
            2  +\prompt 'instance name: ' inst
     2      3   \prompt 'bind to socket: ' bind
     3      4   \qecho 'by default, parsav tracks rights on its own. you can override this later by replacing the rights table with a view, but you''ll then need to set appropriate rules on the view to allow administrators to modify rights from the web UI, or set the rights-readonly flag in the config table to true. for now, enter the name of an actor who will be granted full rights when she logs in.'
     4      5   \prompt 'admin actor: ' admin
     5         -\qecho 'you will need to create an authentication view mapping your user database to something parsav can understand; see auth.sql for an example. enter the name of the view to use.'
     6         -\prompt 'auth view: ' auth
            6  +\qecho 'you will need to create an authentication view named parsav_auth mapping your user database to something parsav can understand; see auth.sql for an example.'
     7      7   
     8      8   begin;
     9      9   
    10     10   drop table if exists parsav_config;
    11     11   create table if not exists parsav_config (
    12     12   	key text primary key,
    13     13   	value text
    14     14   );
    15     15   
    16     16   insert into parsav_config (key,value) values
    17     17   	('bind',:'bind'),
    18     18   	('domain',:'domain'),
    19         -	('auth-source',:'auth'),
           19  +	('instance-name',:'inst'),
    20     20   	('administrator',:'admin'),
    21     21   	('server-secret', encode(
    22     22   			digest(int8send((2^63 * (random()*2 - 1))::bigint),
    23     23   		'sha512'), 'base64'));
    24     24   
    25     25   -- note that valid ids should always > 0, as 0 is reserved for null
    26     26   -- on the client side, vastly simplifying code

Added session.t version [58f0eab21d].

            1  +-- vim: ft=terra
            2  +-- sessions are implemented so as to avoid any local data storage. they
            3  +-- are tracked by storing an encrypted cookie which contains an authid,
            4  +-- a login epoch time, and a truncated hmac code authenticating both, all
            5  +-- encoded using Shorthand. we need functions to generate and parse these
            6  +
            7  +local m = {
            8  +	maxlen = lib.math.shorthand.maxlen*3 + 2;
            9  +	maxage = 2 * 60 * 60; -- 2 hours
           10  +}
           11  +
           12  +terra m.cookie_gen(secret: lib.mem.ptr(int8), authid: uint64, time: uint64, out: &int8): intptr
           13  +	var ptr = out
           14  +	ptr = ptr + lib.math.shorthand.gen(authid, ptr)
           15  +	@ptr = @'.' ptr = ptr + 1
           16  +	ptr = ptr + lib.math.shorthand.gen(time, ptr)
           17  +	@ptr = @'.' ptr = ptr + 1
           18  +	var len = ptr - out
           19  +	var hash: uint8[lib.crypt.algsz.sha256]
           20  +	lib.crypt.hmac(lib.crypt.alg.sha256,
           21  +		[lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct},
           22  +		[lib.mem.ptr( int8)] {ptr = out, ct = len},
           23  +	&hash[0])
           24  +	ptr = ptr + lib.math.shorthand.gen(lib.math.truncate64(hash, [hash.type.N]), ptr)
           25  +	return ptr - out
           26  +end
           27  +
           28  +terra m.cookie_interpret(secret: lib.mem.ptr(int8), c: lib.mem.ptr(int8), now: uint64): uint64 -- returns either 0 or a valid authid
           29  +	var authid_sz = lib.str.cspan(c.ptr, lib.str.lit '.', c.ct)
           30  +	if authid_sz == 0 then return 0 end
           31  +	if authid_sz + 1 > c.ct then return 0 end
           32  +	var time_sz = lib.str.cspan(c.ptr+authid_sz+1, lib.str.lit '.', c.ct - (authid_sz+1))
           33  +	if time_sz == 0 then return 0 end
           34  +	if (authid_sz + time_sz + 2) > c.ct then return 0 end
           35  +	var hash_sz = c.ct - (authid_sz + time_sz + 2)
           36  +
           37  +	var knownhash: uint8[lib.crypt.algsz.sha256]
           38  +	lib.crypt.hmac(lib.crypt.alg.sha256,
           39  +		[lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct},
           40  +		[lib.mem.ptr( int8)] {ptr = c.ptr, ct = c.ct - hash_sz},
           41  +	&knownhash[0])
           42  +
           43  +	var authid, authok = lib.math.shorthand.parse(c.ptr, authid_sz)
           44  +	var time, timeok = lib.math.shorthand.parse(c.ptr + authid_sz + 1, time_sz)
           45  +	var hash, hashok = lib.math.shorthand.parse(c.ptr + c.ct - hash_sz, hash_sz)
           46  +	if not (timeok and authok and hashok) then return 0 end
           47  +	if lib.math.truncate64(knownhash, [knownhash.type.N]) ~= hash then return 0 end
           48  +	if now - time > m.maxage then return 0 end
           49  +
           50  +	return authid
           51  +end
           52  +
           53  +return m

Modified srv.t from [aed7239c9c] to [ed3d5ec62e].

     1      1   -- vim: ft=terra
     2      2   local util = dofile 'common.lua'
            3  +
            4  +local struct srv
            5  +local struct cfgcache {
            6  +	secret: lib.mem.ptr(int8)
            7  +	instance: lib.mem.ptr(int8)
            8  +	overlord: &srv
            9  +}
     3     10   local struct srv {
     4     11   	sources: lib.mem.ptr(lib.store.source)
     5     12   	webmgr: lib.net.mg_mgr
     6     13   	webcon: &lib.net.mg_connection
           14  +	cfg: cfgcache
     7     15   }
     8     16   
     9         -local handle = {
    10         -	http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque)
    11         -		switch event do
    12         -			case lib.net.MG_EV_HTTP_MSG then
    13         -				lib.dbg('routing HTTP request')
    14         -				var msg = [&lib.net.mg_http_message](p)
    15         -
    16         -			end
    17         -		end
    18         -	end;
    19         -}
    20         -local char = macro(function(ch) return `[string.byte(ch:asvalue())] end)
    21         -local terra cfg(s: &srv, befile: rawstring)
    22         -	lib.report('configuring backends from ', befile)
    23         -
    24         -	var fr = lib.file.open(befile, [lib.file.mode.read])
    25         -	if fr.ok == false then
    26         -		lib.bail('could not open configuration file ', befile)
    27         -	end
    28         -
    29         -	var f = fr.val
    30         -	var c: lib.mem.vec(lib.store.source) c:init(8)
    31         -	var text: lib.str.acc text:init(64)
    32         -	do var buf: int8[64]
    33         -		while true do
    34         -			var ct = f:read(buf, [buf.type.N])
    35         -			if ct == 0 then break end
    36         -			text:push(buf, ct)
    37         -		end
    38         -	end
    39         -	f:close()
    40         -
    41         -	var cur = text.buf
    42         -	var segs: tuple(&int8, &int8)[3] = array(
    43         -		{[&int8](0),[&int8](0)},
    44         -		{[&int8](0),[&int8](0)},
    45         -		{[&int8](0),[&int8](0)}
    46         -	)
    47         -	var segdup = [terra(s: {rawstring, rawstring})
    48         -		var sz = s._1 - s._0
    49         -		var str = s._0
    50         -		return [lib.mem.ptr(int8)] {
    51         -			ptr = lib.str.ndup(str, sz);
    52         -			ct = sz;
    53         -		}
    54         -	end]
    55         -	var fld = 0
    56         -	while (cur - text.buf) < text.sz do
    57         -		if segs[fld]._0 == nil then
    58         -			if not (@cur == char(' ') or @cur == char('\t') or @cur == char('\n')) then
    59         -				segs[fld] = {cur, nil}
    60         -			end
    61         -		else
    62         -			if fld < 2 and @cur == char(' ') or @cur == char('\t') then
    63         -				segs[fld]._1 = cur
    64         -				fld = fld + 1
    65         -				segs[fld] = {nil, nil}
    66         -			elseif @cur == char('\n') or cur == text.buf + (text.sz-1) then
    67         -				if fld < 2 then lib.bail('incomplete backend line in ', befile) else
    68         -					segs[fld]._1 = cur
    69         -					var src = c:new()
    70         -					src.id = segdup(segs[0])
    71         -					src.string = segdup(segs[2])
    72         -					src.backend = nil
    73         -					for i = 0,[lib.store.backends.type.N] do
    74         -						if lib.str.ncmp(segs[1]._0, lib.store.backends[i].id, segs[1]._1 - segs[1]._0) == 0 then
    75         -							src.backend = &lib.store.backends[i]
    76         -							break
    77         -						end
    78         -					end
    79         -					if src.backend == nil then
    80         -						lib.bail('unknown backend in ', befile)
    81         -					end
    82         -					src.handle = nil
    83         -					fld = 0
    84         -					segs[0] = {nil, nil}
    85         -				end
    86         -			end
    87         -		end
    88         -		cur = cur + 1
    89         -	end
    90         -	text:free()
    91         -
    92         -	s.sources = c:crush()
           17  +terra cfgcache:free() -- :/
           18  +	self.secret:free()
           19  +	self.instance:free()
    93     20   end
    94     21   
    95         ---srv.methods.conf_set = terra(self: &srv, key: rawstring, val:rawstring)
    96         ---	self.sources.ptr[0]:conf_set(key, val)
    97         ---end
    98         -
    99         -terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring)
   100         -	var cs: lib.store.credset cs:clear()
   101         -	for i=0,self.sources.ct do
   102         -		var set: lib.store.credset = self.sources.ptr[i]:actor_auth_how(ip, usn)
   103         -		cs = cs + set
   104         -	end
   105         -	return cs
   106         -end
   107     22   srv.metamethods.__methodmissing = macro(function(meth, self, ...)
   108     23   	local primary, ptr, stat, simple, oid = 0,1,2,3,4
   109     24   	local tk, rt = primary
   110     25   	local expr = {...}
   111     26   	for _,f in pairs(lib.store.backend.entries) do
   112     27   		local fn = f.field or f[1]
   113     28   		local ft = f.type or f[2]
................................................................................
   146     61   					if [ok] then break
   147     62   						else r = empty end
   148     63   				end
   149     64   			end
   150     65   		in r end
   151     66   	end
   152     67   end)
           68  +
           69  +local struct convo {
           70  +	srv: &srv
           71  +	con: &lib.net.mg_connection
           72  +	msg: &lib.net.mg_http_message
           73  +	aid: uint64 -- 0 if logged out
           74  +	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
           75  +}
           76  +
           77  +-- this is unfortunately necessary to work around a terra bug
           78  +-- it can't seem to handle forward-declarations of structs in C
           79  +
           80  +local getpeer
           81  +do local struct strucheader {
           82  +		next: &lib.net.mg_connection
           83  +		mgr: &lib.net.mg_mgr
           84  +		peer: lib.net.mg_addr
           85  +	}
           86  +	terra getpeer(con: &lib.net.mg_connection)
           87  +		return [&strucheader](con).peer
           88  +	end
           89  +end
           90  +
           91  +terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
           92  +	var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' })
           93  +
           94  +	var ti: lib.str.acc ti:compose('error :: ', title) defer ti:free()
           95  +	var body = data.view.docskel {
           96  +		instance = self.srv.cfg.instance.ptr;
           97  +		title = ti.buf;
           98  +		body = msg;
           99  +		class = 'error';
          100  +	}
          101  +
          102  +	if body.body == nil then
          103  +		body.body = "i'm sorry, dave. i can't let you do that"
          104  +	end
          105  +
          106  +	body:send(self.con, code, [lib.mem.ptr(lib.http.header)] {
          107  +		ptr = &hdrs[0], ct = [hdrs.type.N]
          108  +	})
          109  +end
          110  +
          111  +local urimatch = macro(function(uri, ptn)
          112  +	return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1)
          113  +end)
          114  +
          115  +local route = {} -- these are defined in route.t, as they need access to renderers
          116  +terra route.dispatch_http ::  {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {}
          117  +
          118  +local handle = {
          119  +	http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque)
          120  +		var server = [&srv](ext)
          121  +		var mgpeer = getpeer(con)
          122  +		var peer = lib.store.inet { port = mgpeer.port; }
          123  +		if mgpeer.is_ip6 then peer.pv = 6 else peer.pv = 4 end
          124  +		if peer.pv == 6 then
          125  +			for i = 0, 16 do peer.v6[i] = mgpeer.ip6[i] end
          126  +		else -- v4
          127  +			@[&uint32](&peer.v4) = mgpeer.ip
          128  +		end
          129  +		-- the peer property is currently broken and there is precious
          130  +		-- little i can do about this -- it always reports a peer v4 IP
          131  +		-- of 0.0.0.0, altho the port seems to come through correctly.
          132  +		-- for now i'm leaving it as is, but note that netmask restrictions
          133  +		-- WILL NOT WORK until upstream gets its shit together. FIXME
          134  +
          135  +		switch event do
          136  +			case lib.net.MG_EV_HTTP_MSG then
          137  +				lib.dbg('routing HTTP request')
          138  +				var msg = [&lib.net.mg_http_message](p)
          139  +				var co = convo {
          140  +					con = con, srv = server, msg = msg;
          141  +					aid = 0, who = nil;
          142  +				}
          143  +
          144  +				-- we need to check if there's any cookies sent with the request,
          145  +				-- and if so, whether they contain any credentials. this will be
          146  +				-- used to set the auth parameters in the http conversation
          147  +				var cookies_p = lib.http.findheader(msg, 'Cookie')
          148  +				if cookies_p ~= nil then
          149  +					var cookies = cookies_p.ptr
          150  +					var key = [lib.mem.ref(int8)] {ptr = cookies, ct = 0}
          151  +					var val = [lib.mem.ref(int8)] {ptr = nil, ct = 0}
          152  +					var i = 0 while i < cookies_p.ct    and
          153  +					                cookies[i] ~= 0     and
          154  +					                cookies[i] ~= @'\r' and
          155  +									cookies[i] ~= @'\n' do -- cover all the bases
          156  +						if val.ptr == nil then
          157  +							if cookies[i] == @'=' then
          158  +								key.ct = (cookies + i) - key.ptr
          159  +								val.ptr = cookies + i + 1
          160  +							end
          161  +							i = i + 1
          162  +						else
          163  +							if cookies[i] == @';' then
          164  +								val.ct = (cookies + i) - val.ptr
          165  +								if lib.str.ncmp(key.ptr, 'auth', key.ct) == 0 then
          166  +									goto foundcookie
          167  +								end
          168  +
          169  +								i = i + 1
          170  +								i = lib.str.ffw(cookies + i, cookies_p.ct - i) - cookies
          171  +								key.ptr = cookies + i
          172  +								val.ptr = nil
          173  +							else i = i + 1 end
          174  +						end
          175  +					end
          176  +					if val.ptr == nil then goto nocookie end
          177  +					val.ct = (cookies + i) - val.ptr
          178  +					if lib.str.ncmp(key.ptr, 'auth', key.ct) ~= 0 then
          179  +						goto nocookie
          180  +					end
          181  +					::foundcookie:: do
          182  +						var aid = lib.session.cookie_interpret(server.cfg.secret,
          183  +							[lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct},
          184  +							lib.osclock.time(nil))
          185  +						if aid ~= 0 then co.aid = aid end
          186  +					end ::nocookie::;
          187  +				end
          188  +
          189  +				if co.aid ~= 0 then
          190  +					var sess, usr = co.srv:actor_session_fetch(co.aid, peer)
          191  +					if sess.ok == false then co.aid = 0 else co.who = usr.ptr end
          192  +				end
          193  +
          194  +				var uridec = lib.mem.heapa(int8, msg.uri.len) defer uridec:free()
          195  +				var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1)
          196  +
          197  +				var uri = uridec
          198  +				if urideclen == -1 then
          199  +					for i = 0,msg.uri.len do
          200  +						if msg.uri.ptr[i] == @'+'
          201  +							then uri.ptr[i] = @' '
          202  +							else uri.ptr[i] = msg.uri.ptr[i]
          203  +						end
          204  +					end
          205  +					uri.ct = msg.uri.len
          206  +				else uri.ct = urideclen end
          207  +				lib.dbg('routing URI ', {uri.ptr, uri.ct})
          208  +				
          209  +				if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then
          210  +					route.dispatch_http(&co, uri, [lib.http.method.get])
          211  +				else
          212  +					co:complain(400,'unknown method','you have submitted an invalid http request')
          213  +				end
          214  +
          215  +				if co.aid ~= 0 then lib.mem.heapf(co.who) end
          216  +			end
          217  +		end
          218  +	end;
          219  +}
          220  +
          221  +local terra cfg(s: &srv, befile: rawstring)
          222  +	lib.report('configuring backends from ', befile)
          223  +
          224  +	var fr = lib.file.open(befile, [lib.file.mode.read])
          225  +	if fr.ok == false then
          226  +		lib.bail('could not open configuration file ', befile)
          227  +	end
          228  +
          229  +	var f = fr.val
          230  +	var c: lib.mem.vec(lib.store.source) c:init(8)
          231  +	var text: lib.str.acc text:init(64)
          232  +	do var buf: int8[64]
          233  +		while true do
          234  +			var ct = f:read(buf, [buf.type.N])
          235  +			if ct == 0 then break end
          236  +			text:push(buf, ct)
          237  +		end
          238  +	end
          239  +	f:close()
          240  +
          241  +	var cur = text.buf
          242  +	var segs: tuple(&int8, &int8)[3] = array(
          243  +		{[&int8](0),[&int8](0)},
          244  +		{[&int8](0),[&int8](0)},
          245  +		{[&int8](0),[&int8](0)}
          246  +	)
          247  +	var segdup = [terra(s: {rawstring, rawstring})
          248  +		var sz = s._1 - s._0
          249  +		var str = s._0
          250  +		return [lib.mem.ptr(int8)] {
          251  +			ptr = lib.str.ndup(str, sz);
          252  +			ct = sz;
          253  +		}
          254  +	end]
          255  +	var fld = 0
          256  +	while (cur - text.buf) < text.sz do
          257  +		if segs[fld]._0 == nil then
          258  +			if not (@cur == @' ' or @cur == @'\t' or @cur == @'\n') then
          259  +				segs[fld] = {cur, nil}
          260  +			end
          261  +		else
          262  +			if fld < 2 and @cur == @' ' or @cur == @'\t' then
          263  +				segs[fld]._1 = cur
          264  +				fld = fld + 1
          265  +				segs[fld] = {nil, nil}
          266  +			elseif @cur == @'\n' or cur == text.buf + (text.sz-1) then
          267  +				if fld < 2 then lib.bail('incomplete backend line in ', befile) else
          268  +					segs[fld]._1 = cur
          269  +					var src = c:new()
          270  +					src.id = segdup(segs[0])
          271  +					src.string = segdup(segs[2])
          272  +					src.backend = nil
          273  +					for i = 0,[lib.store.backends.type.N] do
          274  +						if lib.str.ncmp(segs[1]._0, lib.store.backends[i].id, segs[1]._1 - segs[1]._0) == 0 then
          275  +							src.backend = &lib.store.backends[i]
          276  +							break
          277  +						end
          278  +					end
          279  +					if src.backend == nil then
          280  +						lib.bail('unknown backend in ', befile)
          281  +					end
          282  +					src.handle = nil
          283  +					fld = 0
          284  +					segs[0] = {nil, nil}
          285  +				end
          286  +			end
          287  +		end
          288  +		cur = cur + 1
          289  +	end
          290  +	text:free()
          291  +
          292  +	if c.sz > 0 then
          293  +		s.sources = c:crush()
          294  +	else
          295  +		s.sources.ptr = nil
          296  +		s.sources.ct = 0
          297  +	end
          298  +end
          299  +
          300  +terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring)
          301  +	var cs: lib.store.credset cs:clear()
          302  +	for i=0,self.sources.ct do
          303  +		var set: lib.store.credset = self.sources.ptr[i]:actor_auth_how(ip, usn)
          304  +		cs = cs + set
          305  +	end
          306  +	return cs
          307  +end
          308  +
          309  +terra cfgcache.methods.load :: {&cfgcache} -> {}
          310  +terra cfgcache:init(o: &srv)
          311  +	self.overlord = o
          312  +	self:load()
          313  +end
   153    314   
   154    315   srv.methods.start = terra(self: &srv, befile: rawstring)
   155    316   	cfg(self, befile)
   156    317   	var success = false
          318  +	if self.sources.ct == 0 then lib.bail('no data sources specified') end
   157    319   	for i=0,self.sources.ct do var src = self.sources.ptr + i
   158    320   		lib.report('opening data source ', src.id.ptr, '(', src.backend.id, ')')
   159    321   		src.handle = src.backend.open(src)
   160    322   		if src.handle ~= nil then success = true end
   161    323   	end
   162    324   	if not success then
   163    325   		lib.bail('could not connect to any data sources!')
   164    326   	end
          327  +
          328  +	self.cfg:init(self)
   165    329   
   166    330   	var dbbind = self:conf_get('bind')
   167    331   	var envbind = lib.proc.getenv('parsav_bind')
   168    332   	var bind: rawstring
   169    333   	if envbind ~= nil then
   170    334   		bind = envbind
   171    335   	elseif dbbind.ptr ~= nil then
   172    336   		bind = dbbind.ptr
   173    337   	else bind = '[::]:10917' end
   174    338   
   175    339   	lib.report('binding to ', bind)
   176    340   	lib.net.mg_mgr_init(&self.webmgr)
   177         -	self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, nil)
          341  +	self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, self)
   178    342   
          343  +	var buf: int8[lib.session.maxlen]
          344  +	var len = lib.session.cookie_gen(self.cfg.secret, 9139084444658983115ULL, lib.osclock.time(nil), &buf[0])
          345  +	buf[len] = 0
          346  +	
          347  +	var authid = lib.session.cookie_interpret(self.cfg.secret, [lib.mem.ptr(int8)] {ptr=buf, ct=len}, lib.osclock.time(nil))
          348  +	lib.io.fmt('generated cookie %s -- got authid %llu\n', buf, authid)
   179    349   
   180    350   	if dbbind.ptr ~= nil then dbbind:free() end
   181    351   end
   182    352   
   183    353   srv.methods.poll = terra(self: &srv)
   184    354   	lib.net.mg_mgr_poll(&self.webmgr,1000)
   185    355   end
................................................................................
   189    359   	for i=0,self.sources.ct do var src = self.sources.ptr + i
   190    360   		lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')')
   191    361   		src:close()
   192    362   	end
   193    363   	self.sources:free()
   194    364   end
   195    365   
   196         -return srv
          366  +terra cfgcache:load()
          367  +	self.instance = self.overlord:conf_get('instance-name')
          368  +	self.secret = self.overlord:conf_get('server-secret')
          369  +end
          370  +
          371  +return {
          372  +	overlord = srv;
          373  +	convo = convo;
          374  +	route = route;
          375  +}

Added static/style.scss version [a7ffc6f8bf].


Modified store.t from [2c2e954f5c] to [213b3d2729].

    10     10   	};
    11     11   	relation = lib.enum {
    12     12   		'follow', 'mute', 'block'
    13     13   	};
    14     14   	credset = lib.set {
    15     15   		'pw', 'otp', 'challenge', 'trust'
    16     16   	};
           17  +	privset = lib.set {
           18  +		'post', 'edit', 'acct', 'upload', 'censor', 'admin'
           19  +	}
    17     20   }
    18     21   
    19     22   local str = rawstring --lib.mem.ptr(int8)
    20     23   
    21     24   struct m.source
    22     25   
    23     26   struct m.rights {
................................................................................
    28     31   	-- user powers -- default on
    29     32   	login: bool
    30     33   	visible: bool
    31     34   	post: bool
    32     35   	shout: bool
    33     36   	propagate: bool
    34     37   	upload: bool
           38  +	acct: bool
           39  +	edit: bool
    35     40   
    36     41   	-- admin powers -- default off
    37     42   	ban: bool
    38     43   	config: bool
    39     44   	censor: bool
    40     45   	suspend: bool
    41     46   	rebrand: bool -- modify site's brand identity
................................................................................
   140    145   end
   141    146   
   142    147   struct m.auth {
   143    148   	aid: uint64
   144    149   	uid: uint64
   145    150   	aname: str
   146    151   	netmask: m.inet
   147         -	restrict: lib.mem.ptr(rawstring)
          152  +	privs: m.privset
   148    153   	blacklist: bool
   149    154   }
   150         -
   151    155   
   152    156   -- backends only handle content on the local server
   153    157   struct m.backend { id: rawstring
   154    158   	open: &m.source -> &opaque
   155    159   	close: &m.source -> {}
   156    160   
   157    161   	conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8)
   158    162   	conf_set: {&m.source, rawstring, rawstring} -> {}
   159    163   	conf_reset: {&m.source, rawstring} -> {}
   160    164   
   161    165   	actor_save: {&m.source, m.actor} -> bool
   162    166   	actor_create: {&m.source, m.actor} -> bool
   163         -	actor_fetch_xid: {&m.source, rawstring} -> lib.mem.ptr(m.actor)
          167  +	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
   164    168   	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
   165    169   	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
   166    170   	actor_enum: {&m.source} -> lib.mem.ptr(&m.actor)
   167    171   	actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor)
   168    172   
   169    173   	actor_auth_how: {&m.source, m.inet, rawstring} -> m.credset
   170    174   		-- returns a set of auth method categories that are available for a
................................................................................
   183    187   			-- fingerprint: rawstring
   184    188   	actor_auth_api:    {&m.source, m.inet, rawstring, rawstring} -> uint64
   185    189   		-- handles API authentication
   186    190   			-- origin: inet
   187    191   			-- handle: rawstring
   188    192   			-- key:    rawstring (X-API-Key)
   189    193   	actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth)
          194  +	actor_session_fetch: {&m.source, uint64, m.inet} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)}
          195  +		-- retrieves an auth record + actor combo suitable by AID suitable
          196  +		-- for determining session validity & caps
          197  +			-- aid:    uint64
          198  +			-- origin: inet
   190    199   
   191    200   	actor_conf_str: cnf(rawstring, lib.mem.ptr(int8))
   192    201   	actor_conf_int: cnf(intptr, lib.stat(intptr))
   193    202   
   194    203   	post_save: {&m.source, &m.post} -> bool
   195    204   	post_create: {&m.source, &m.post} -> bool
   196    205   	actor_post_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(m.post)

Modified str.t from [4b8724b0aa] to [c91733fef5].

     1      1   -- vim: ft=terra
     2      2   -- string.t: string classes
            3  +local util = dofile('common.lua')
     3      4   
     4      5   local m = {
     5      6   	sz = terralib.externfunction('strlen', rawstring -> intptr);
     6      7   	cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int);
     7      8   	ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int);
     8      9   	cpy = terralib.externfunction('stpcpy',{rawstring, rawstring} -> rawstring);
     9     10   	ncpy = terralib.externfunction('stpncpy',{rawstring, rawstring, intptr} -> rawstring);
    10     11   	dup = terralib.externfunction('strdup',rawstring -> rawstring);
    11     12   	ndup = terralib.externfunction('strndup',{rawstring, intptr} -> rawstring);
    12     13   	fmt = terralib.externfunction('asprintf',
    13     14   		terralib.types.funcpointer({&rawstring,rawstring},{int},true));
    14     15   	bfmt = terralib.externfunction('sprintf',
    15     16   		terralib.types.funcpointer({rawstring,rawstring},{int},true));
           17  +	span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring);
    16     18   }
    17     19   
    18     20   (lib.mem.ptr(int8)).metamethods.__cast = function(from,to,e)
    19     21   	if from == &int8 then
    20     22   		return `[lib.mem.ptr(int8)]{ptr = e, ct = m.sz(e)}
    21     23   	elseif to == &int8 then
    22     24   		return e.ptr
................................................................................
    31     33   }
    32     34   
    33     35   local terra biggest(a: intptr, b: intptr)
    34     36   	if a > b then return a else return b end
    35     37   end
    36     38   
    37     39   terra m.acc:init(run: intptr)
    38         -	lib.dbg('initializing string accumulator')
           40  +	--lib.dbg('initializing string accumulator')
    39     41   	self.buf = [rawstring](lib.mem.heapa_raw(run))
    40     42   	self.run = run
    41     43   	self.space = run
    42     44   	self.sz = 0
    43     45   	return self
    44     46   end;
    45     47   
    46     48   terra m.acc:free()
    47         -	lib.dbg('freeing string accumulator')
           49  +	--lib.dbg('freeing string accumulator')
    48     50   	if self.buf ~= nil and self.space > 0 then
    49     51   		lib.mem.heapf(self.buf)
    50     52   	end
    51     53   end;
    52     54   
    53     55   terra m.acc:crush()
    54     56   	lib.dbg('crushing string accumulator')
................................................................................
    65     67   	pt.ct = self.sz
    66     68   	self.buf = nil
    67     69   	self.sz = 0
    68     70   	return pt
    69     71   end;
    70     72   
    71     73   terra m.acc:push(str: rawstring, len: intptr)
    72         -	var llen = len
           74  +	--var llen = len
    73     75   	if str == nil then return self end
    74         -	if str[len - 1] == 0xA then llen = llen - 1 end -- don't display newlines in debug output
    75         -	lib.dbg('pushing "',{str,llen},'" onto accumulator')
           76  +	--if str[len - 1] == 0xA then llen = llen - 1 end -- don't display newlines in debug output
           77  +	-- lib.dbg('pushing "',{str,llen},'" onto accumulator')
    76     78   	if self.buf == nil then self:init(self.run) end
    77     79   	if len == 0 then len = m.sz(str) end
    78     80   	if len >= self.space - self.sz then
    79     81   		self.space = self.space + biggest(self.run,len + 1)
    80     82   		self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.space))
    81     83   	end
    82     84   	lib.mem.cpy(self.buf + self.sz, str, len)
    83     85   	self.sz = self.sz + len
    84     86   	self.buf[self.sz] = 0
    85     87   	return self
    86     88   end;
           89  +
           90  +m.lit = macro(function(str)
           91  +	return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]}
           92  +end)
           93  +
    87     94   m.acc.methods.ppush = terra(self: &m.acc, str: lib.mem.ptr(int8))
    88     95   	self:push(str.ptr, str.ct)            return self end;
    89     96   m.acc.methods.merge = terra(self: &m.acc, str: lib.mem.ptr(int8))
    90     97   	self:push(str.ptr, str.ct) str:free() return self end;
    91     98   m.acc.methods.compose = macro(function(self, ...)
    92     99   	local minlen = 0
    93    100   	local pstrs = {}
................................................................................
   138    145   	local ptr = symbol(&int8)
   139    146   	local box = symbol(&m.box(ty))
   140    147   	local memreq_exp = `0
   141    148   	local copiers = {}
   142    149   	for k,v in pairs(vals) do
   143    150   		local ty = (`box.obj.[k]).tree.type
   144    151   		local kp
          152  +		local isnull, nullify
   145    153   		if ty.ptr_basetype then
   146         -			kp = quote [box].obj.[k] = [ty] { [ptr] = [&ty.ptr_basetype]([ptr]) } ; end
          154  +			kp = quote [box].obj.[k] = [ty] { ptr = [&ty.ptr_basetype]([ptr]) } ; end
          155  +			nullify = quote [box].obj.[k] = [ty] { ptr = nil, ct = 0 } end
   147    156   		else
   148    157   			kp = quote [box].obj.[k] = [ty]([ptr]) ; end
          158  +			nullify = quote [box].obj.[k] = nil end
   149    159   		end
   150    160   
   151    161   		local cpy
   152    162   		if type(v) ~= 'table' or #v ~= 2 then
   153    163   			cpy = quote [kp] ; [ptr] = m.cpy(ptr, v) end
          164  +			isnull = `v == nil
   154    165   		end
   155    166   		if type(v) == 'string' then
   156    167   			memreq_const = memreq_const + #v + 1
          168  +			isnull = `false
   157    169   		elseif type(v) == 'table' and v.tree and (v.tree.type.ptr_basetype == int8 or v.tree.type.ptr_basetype == uint8) then
   158    170   			cpy = quote [kp]; [ptr] = [&int8](lib.mem.cpy([ptr], [v].ptr, [v].ct)) end
   159    171   			if ty.ptr_basetype then
   160    172   				cpy = quote [cpy]; [box].obj.[k].ct = [v].ct end
   161    173   			end
          174  +			isnull = `[v].ptr == nil
   162    175   		elseif type(v) == 'table' and v.asvalue and type(v:asvalue()) == 'string' then
   163    176   			local str = tostring(v:asvalue())
   164    177   			memreq_const = memreq_const + #str + 1
          178  +			isnull = `false
   165    179   		elseif type(v) == 'table' and #v == 2 then
   166    180   			local str,sz = v[1],v[2]
   167    181   			if type(sz) == 'number' then
   168    182   				memreq_const = memreq_const + sz
   169    183   			elseif type(sz:asvalue()) == 'number' then
   170    184   				memreq_const = memreq_const + sz:asvalue()
   171    185   			else memreq_exp = `[sz] + [memreq_exp] end
   172    186   
   173    187   			cpy = quote [kp] ; [ptr] = [&int8](lib.mem.cpy([ptr], str, sz)) end
   174    188   			if ty.ptr_basetype then
   175    189   				cpy = quote [cpy]; [box].obj.[k].ct = sz end
   176    190   			end
          191  +			isnull = `[str] == nil
   177    192   		else
   178    193   			memreq_exp = `(m.sz(v) + 1) + [memreq_exp] -- make room for NUL
          194  +			isnull = `v == nil
   179    195   			if ty.ptr_basetype then
   180    196   				cpy = quote [cpy]; [box].obj.[k].ct = m.sz(v) end
   181    197   			end
   182    198   		end
   183         -		copiers[#copiers + 1] = cpy
          199  +
          200  +		copiers[#copiers + 1] = quote
          201  +			if [isnull] then [nullify]
          202  +			            else [cpy] end
          203  +		end
   184    204   	end
   185    205   
   186    206   	return quote
   187    207   		var sz: intptr = memreq_const + [memreq_exp]
   188    208   		var [box] = [&m.box(ty)](lib.mem.heapa_raw(sz))
   189    209   		var [ptr] = [box].storage
   190    210   		[copiers]
   191    211   	in [lib.mem.ptr(ty)] { ct = 1, ptr = &([box].obj) } end
   192    212   end
          213  +
          214  +terra m.cspan(str: lib.mem.ptr(int8), reject: lib.mem.ref(int8), maxlen: intptr)
          215  +	for i=0, lib.math.smallest(maxlen,str.ct) do
          216  +		if str.ptr[i] == 0 then return 0 end
          217  +		for j=0, reject.ct do
          218  +			if str.ptr[i] == reject.ptr[j] then return i end
          219  +		end
          220  +	end
          221  +	return maxlen
          222  +end
          223  +
          224  +terra m.ffw(str: &int8, maxlen: intptr)
          225  +	while maxlen > 0 and @str ~= 0 and
          226  +	      (@str == @' ' or @str == @'\t' or @str == @'\n') do
          227  +		str = str + 1
          228  +		maxlen = maxlen - 1
          229  +	end
          230  +	return str
          231  +end
          232  +
          233  +terra m.ffw_unsafe(str: &int8)
          234  +	while  @str ~= 0 and
          235  +	      (@str == @' ' or @str == @'\t' or @str == @'\n') do
          236  +		str = str + 1
          237  +	end
          238  +	return str
          239  +end
   193    240   
   194    241   return m

Modified view/docskel.tpl from [1d38dac966] to [004398018e].

     1      1   <!doctype html>
     2      2   <html>
     3      3   	<head>
     4      4   		<title>@instance :: @title</title>
     5         -		<link rel="stylesheet" href="/style.css">
            5  +		<link rel="stylesheet" href="/s/style.css">
     6      6   	</head>
     7         -	<body>
            7  +	<body class="@class">
     8      8   		<h1>@title</h1>
     9      9   		@body
    10     10   	</body>
    11     11   </html>

Modified view/load.lua from [f2a65d7b61] to [53cfafaa7c].

     2      2   -- file that indexes the templates manually, and
     3      3   -- copies them into a data structure we can then
     4      4   -- create templates from when we return to terra
     5      5   local path = ...
     6      6   local sources = {
     7      7   	'docskel';
     8      8   	'tweet';
            9  +	'profile';
     9     10   }
    10     11   
    11     12   local ingest = function(filename)
    12     13   	local hnd = io.open(path..'/'..filename)
    13     14   	local txt = hnd:read('*a')
    14     15   	io.close(hnd)
    15     16   	txt = txt:gsub('([^\\])!%b[]', '%1')

Added view/profile.tpl version [abc7153f4d].

            1  +<div class="profile">
            2  +	<div class="banner">
            3  +		<img class="avatar" src="@avatar">
            4  +		<div class="id">@nym [@xid]</div>
            5  +		<div class="bio">
            6  +			@bio
            7  +		</div>
            8  +	</div>
            9  +	<table class="stats">
           10  +		<tr><th>posts</th> <td>@nposts</td></tr>
           11  +		<tr><th>following</th> <td>@nfollows</td></tr>
           12  +		<tr><th>followers</th> <td>@nfollowers</td></tr>
           13  +		<tr><th>mutuals</th> <td>@nmutuals</td></tr>
           14  +		<tr><th>account created</th> <td>@tweetday</td></tr>
           15  +	</table>
           16  +	<div class="menu">
           17  +		<a href="/\@@xid">posts</a>
           18  +		<a href="/\@@xid/media">media</a>
           19  +		<a href="/\@@xid/follows">follows</a>
           20  +		<a href="/\@@xid/chat">chat</a>
           21  +	</div>
           22  +</div>

Modified view/tweet.tpl from [59ae4e4ad9] to [4ad2e9eaa9].

     1      1   <div class="post">
     2      2   	<div class="detail">
     3      3   		<a class="username" href="@link">
     4         -			<span class="nym">@nym</span> <span class="handle">[@handle]</span>
            4  +			<span class="nym">@nym</span> <span class="handle">[@xid]</span>
     5      5   		</div>
     6      6   		<div class="when">@when</div>
     7      7   	</div>
     8      8   	<div class="content">
     9      9   		<img class="avatar" src="@avatar">
    10     10   		<div class="text">@text</div>
    11     11   	</div>
    12     12   </div>