Index: acl.t ================================================================== --- acl.t +++ acl.t @@ -1,1 +1,29 @@ -- vim: ft=terra +local m = { + agentkind = lib.enum { + 'user', 'circle' + }; +} + +struct m.agent { + kind: m.agentkind.t + id: uint64 +} + +terra m.eval(expr: lib.str.t, agent: m.agent) + +end + +terra lib.store.post:save(ctupdate: bool) +-- this post handles the messy details of registering a post's +-- circles and actors, and increments the edit-count if ctupdate +-- is true, which is should be in almost all cases. + if ctupdate then + self.chgcount = self.chgcount + 1 + self.edited = lib.osclock.time(nil) + end + -- TODO extract mentions from body, circles from acl + self.source:post_save(self) +end + +return m Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -169,11 +169,11 @@ (select count(tweets.*)::bigint from tweets), (select count(follows.*)::bigint from follows), (select count(followers.*)::bigint from followers), (select count(mutuals.*)::bigint from mutuals) ) - ]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation[r]) end) + ]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation.idvmap[r]) end) }; actor_auth_how = { params = {rawstring, lib.store.inet}, sql = [[ with mts as (select a.kind from parsav_auth as a @@ -191,11 +191,11 @@ (select count(*) from mts where kind = 'trust') > 0 ]]; -- cheat }; actor_session_fetch = { - params = {uint64, lib.store.inet}, sql = [[ + params = {uint64, lib.store.inet, int64}, sql = [[ select a.id, a.nym, a.handle, a.origin, a.bio, a.avataruri, a.rank, a.quota, a.key, a.epithet, extract(epoch from a.knownsince)::bigint, coalesce(a.handle || '@' || s.domain, '@' || a.handle) as xid, @@ -211,11 +211,14 @@ from parsav_auth au left join parsav_actors a on au.uid = a.id left join parsav_servers s on a.origin = s.id where au.aid = $1::bigint and au.blacklist = false and - (au.netmask is null or au.netmask >> $2::inet) + (au.netmask is null or au.netmask >> $2::inet) and + ($3::bigint = 0 or --slightly abusing the epoch time fmt here, but + ((a.authtime is null or a.authtime <= to_timestamp($3::bigint)) and + (au.valperiod is null or au.valperiod <= to_timestamp($3::bigint)))) ]]; }; actor_powers_fetch = { params = {uint64}, sql = [[ @@ -236,10 +239,25 @@ delete from parsav_rights where actor = $1::bigint and key = $2::text ]] }; + + auth_sigtime_user_fetch = { + params = {uint64}, sql = [[ + select extract(epoch from authtime)::bigint + from parsav_actors where id = $1::bigint + ]]; + }; + + auth_sigtime_user_alter = { + params = {uint64,int64}, cmd = true, sql = [[ + update parsav_actors set + authtime = to_timestamp($2::bigint) + where id = $1::bigint + ]]; + }; auth_create_pw = { params = {uint64, lib.mem.ptr(uint8)}, cmd = true, sql = [[ insert into parsav_auth (uid, name, kind, cred) values ( $1::bigint, @@ -254,10 +272,25 @@ delete from parsav_auth where ((uid = 0 and name = $1::text) or uid = $2::bigint) and kind like $3::text ]] }; + + post_save = { + params = { + uint64, uint32, int64; + rawstring, rawstring, rawstring; + }, cmd = true, sql = [[ + update parsav_posts set + subject = $4::text, + acl = $5::text, + body = $6::text, + chgcount = $2::integer, + edited = to_timestamp($3::bigint) + where id = $1::bigint + ]] + }; post_create = { params = {uint64, rawstring, rawstring, rawstring}, sql = [[ insert into parsav_posts ( author, subject, acl, body, @@ -268,18 +301,33 @@ $3::text, $4::text, now(), now(), array[]::bigint[], array[]::bigint[] ) returning id ]]; -- TODO array handling }; + + post_fetch = { + params = {uint64}, sql = [[ + select a.origin is null, + p.id, p.author, p.subject, p.acl, p.body, + extract(epoch from p.posted )::bigint, + extract(epoch from p.discovered)::bigint, + extract(epoch from p.edited )::bigint, + p.parent, p.convoheaduri, p.chgcount + from parsav_posts as p + inner join parsav_actors as a on p.author = a.id + where p.id = $1::bigint + ]]; + }; post_enum_author_uid = { params = {uint64,uint64,uint64,uint64, uint64}, sql = [[ select a.origin is null, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, - p.parent, p.convoheaduri + extract(epoch from p.edited )::bigint, + p.parent, p.convoheaduri, p.chgcount from parsav_posts as p inner join parsav_actors as a on p.author = a.id where p.author = $5::bigint and ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) @@ -296,11 +344,12 @@ params = {uint64, uint64, uint64, uint64}, sql = [[ select true, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, - p.parent, null::text + extract(epoch from p.edited )::bigint, + p.parent, null::text, p.chgcount from parsav_posts as p inner join parsav_actors as a on p.author = a.id where ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and @@ -567,13 +616,13 @@ var cvhu: rawstring, cvhlen: intptr if r:null(row,3) then subj = nil sblen = 0 else subj = r:string(row,3) sblen = r:len(row,3)+1 end - if r:null(row,9) + if r:null(row,10) then cvhu = nil cvhlen = 0 - else cvhu = r:string(row,9) cvhlen = r:len(row,9)+1 + else cvhu = r:string(row,10) cvhlen = r:len(row,10)+1 end var p = [ lib.str.encapsulate(lib.store.post, { subject = { `subj, `sblen }; acl = {`r:string(row,4), `r:len(row,4)+1}; body = {`r:string(row,5), `r:len(row,5)+1}; @@ -581,13 +630,18 @@ }) ] p.ptr.id = r:int(uint64,row,1) p.ptr.author = r:int(uint64,row,2) p.ptr.posted = r:int(uint64,row,6) p.ptr.discovered = r:int(uint64,row,7) - if r:null(row,8) + p.ptr.edited = r:int(uint64,row,8) + if r:null(row,9) then p.ptr.parent = 0 - else p.ptr.parent = r:int(uint64,row,8) + else p.ptr.parent = r:int(uint64,row,9) + end + if r:null(row,11) + then p.ptr.chgcount = 0 + else p.ptr.chgcount = r:int(uint32,row,11) end p.ptr.localpost = r:bool(row,0) return p end @@ -920,13 +974,14 @@ end]; actor_session_fetch = [terra( src: &lib.store.source, aid: uint64, - ip : lib.store.inet + ip : lib.store.inet, + issuetime: lib.store.timepoint ): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) } - var r = queries.actor_session_fetch.exec(src, aid, ip) + var r = queries.actor_session_fetch.exec(src, aid, ip, issuetime) if r.sz == 0 then goto fail end do defer r:free() if r:null(0,0) then goto fail end @@ -961,10 +1016,21 @@ if r.sz == 0 then return 0 end defer r:free() var id = r:int(uint64,0,0) return id end]; + + post_fetch = [terra( + src: &lib.store.source, + post: uint64 + ): lib.mem.ptr(lib.store.post) + var r = queries.post_fetch.exec(src, post) + if r.sz == 0 then return [lib.mem.ptr(lib.store.post)].null() end + var p = row_to_post(&r, 0) + p.ptr.source = src + return p + end]; timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range) var r = pqr { sz = 0 } var A,B,C,D = rg:matrix() -- :/ r = queries.timeline_instance_fetch.exec(src,A,B,C,D) @@ -1108,10 +1174,36 @@ if detach then queries.post_attach_ctl_del.exec(src,post,artifact) else queries.post_attach_ctl_ins.exec(src,post,artifact) end end]; + + post_save = [terra( + src: &lib.store.source, + post: &lib.store.post + ): {} + queries.post_save.exec(src, + post.id, post.chgcount, post.edited, + post.subject, post.acl, post.body) + end]; + + auth_sigtime_user_fetch = [terra( + src: &lib.store.source, + uid: uint64 + ): lib.store.timepoint + var r = queries.auth_sigtime_user_fetch.exec(src, uid) + if r.sz > 0 then defer r:free() + var t = r:int(int64,0,0) + return t + else return 0 end + end]; + + auth_sigtime_user_alter = [terra( + src: &lib.store.source, + uid: uint64, + time: lib.store.timepoint + ): {} queries.auth_sigtime_user_alter.exec(src, uid, time) end]; actor_auth_register_uid = nil; -- TODO better support non-view based auth } return b Index: backend/schema/pgsql-auth.sql ================================================================== --- backend/schema/pgsql-auth.sql +++ backend/schema/pgsql-auth.sql @@ -44,8 +44,12 @@ -- uid = null, kind = trust, cidr = (untrusted IP range) valperiod timestamp default now(), -- cookies bearing timestamps earlier than this point in time -- will be considered invalid and will not grant access + + comment text, + -- a field the user can use to identify the specific credential, + -- in order to aid credential management unique(name,kind,cred) ); Index: backend/schema/pgsql.sql ================================================================== --- backend/schema/pgsql.sql +++ backend/schema/pgsql.sql @@ -26,11 +26,11 @@ id bigint primary key default (1+random()*(2^63-1))::bigint, nym text, handle text not null, -- nym [@handle@origin] origin bigint references parsav_servers(id) on delete cascade, -- null origin = local actor - knownsince timestamp, + knownsince timestamp not null default now(), bio text, avatarid bigint, -- artifact id, null if remote avataruri text, -- null if local rank smallint not null default 0, quota integer not null default 1000, @@ -58,10 +58,12 @@ subject text, acl text not null default 'all', -- just store the script raw 🤷 body text, posted timestamp not null, discovered timestamp not null, + chgcount integer not null default 0, + edited timestamp, parent bigint not null default 0, -- if post: part of conversation; if chatroom: top-level post circles bigint[], -- TODO at edit or creation, iterate through each circle mentions bigint[], -- a user has, check if it can see her post, and if so add artifacts bigint[], Index: cmdparse.t ================================================================== --- cmdparse.t +++ cmdparse.t @@ -23,12 +23,17 @@ local incr = desc.inc or 0 options.entries[#options.entries + 1] = { field = o, type = (consume > 0) and &rawstring or (incr > 0) and uint or bool } - helpstr = helpstr .. string.format(' -%s --%s: %s\n', - desc[1], sanitize(o), desc[2]) + if desc[1] then + helpstr = helpstr .. string.format(' -%s --%s: %s\n', + desc[1], sanitize(o), desc[2]) + else + helpstr = helpstr .. string.format(' --%s: %s\n', + sanitize(o), desc[2]) + end end for o,desc in pairs(tbl) do local flag = desc[1] local consume = desc.consume or 0 local incr = desc.inc or 0 @@ -50,12 +55,14 @@ elseif incr > 0 then ch = quote [self].[o] = [self].[o] + incr end else ch = quote [self].[o] = true end end - shortcases[#shortcases + 1] = quote - case [int8]([string.byte(flag)]) then [ch] end + if flag ~= nil then + shortcases[#shortcases + 1] = quote + case [int8]([string.byte(flag)]) then [ch] end + end end longcases[#longcases + 1] = quote if lib.str.cmp([arg]+2, [sanitize(o)]) == 0 then [ch] goto [skip] end end end Index: config.lua ================================================================== --- config.lua +++ config.lua @@ -46,14 +46,20 @@ feat = {}; debug = u.tobool(default('parsav_enable_debug',true)); backends = defaultlist('parsav_backends', 'pgsql'); braingeniousmode = false; embeds = { + -- TODO with gzip compression, svg is dramatically superior to webp + -- we should have a build-time option to serve svg so instances + -- proxied behind nginx can serve svgz, or possibly just straight-up + -- add support for content-encoding headers and pre-compress the + -- damn things before compiling {'style.css', 'text/css'}; {'default-avatar.webp', 'image/webp'}; {'padlock.webp', 'image/webp'}; {'warn.webp', 'image/webp'}; + {'query.webp', 'image/webp'}; }; } if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then conf.braingeniousmode = true -- SOUND GENERAL QUARTERS end Index: math.t ================================================================== --- math.t +++ math.t @@ -1,9 +1,11 @@ -- vim: ft=terra local m = { shorthand = {maxlen = 14} } + +local pstring = lib.mem.ptr(int8) -- swap in place -- faster on little endian m.netswap_ip = macro(function(ty, src, dest) if ty:astype().type ~= 'integer' then error('bad type') end local bytes = ty:astype().bytes @@ -184,7 +186,60 @@ buf = buf - 1 @buf = 0x30 end return buf end + +terra m.ndigits(n: intptr, base: intptr): intptr + var c = base + var i = 1 + while true do + if n < c then return i end + c = c * base + i = i + 1 + end +end + +terra m.fsz_parse(f: pstring): {intptr, bool} +-- take a string representing a file size and return {nbytes, true} +-- or {0, false} if the parse fails + if f.ct == 0 then f.ct = lib.str.sz(f.ptr) end + var sz: intptr = 0 + for i = 0, f.ct do + if f(i) == @',' then goto skip end + if f(i) >= 0x30 and f(i) <= 0x39 then + sz = sz * 10 + sz = sz + f(i) - 0x30 + else + if i+1 == f.ct or f(i) == 0 then return sz, true end + if i+2 == f.ct or f(i+1) == 0 then + if f(i) == @'b' then return sz/8, true end -- bits + else + var s: intptr = 0 + if i+3 == f.ct or f(i+2) == 0 then + s = i + 1 + elseif (i+4 == f.ct or f(i+3) == 0) and f(i+1) == @'i' then + -- grudgingly tolerate ~mebibits~ and its ilk, without + -- affecting the result in any way + s = i + 2 + else return 0, false end + + if f(s) == @'b' then sz = sz/8 -- bits + elseif f(s) ~= @'B' then return 0, false end -- wth + end + var c = f(i) + if c >= @'A' and c <= @'Z' then c = c - 0x20 end + switch c do -- normal char literal syntax doesn't work here, leads to llvm error (!!) + case [uint8]([string.byte('k')]) then return sz * [1024ULL ^ 1], true end + case [uint8]([string.byte('m')]) then return sz * [1024ULL ^ 2], true end + case [uint8]([string.byte('g')]) then return sz * [1024ULL ^ 3], true end + case [uint8]([string.byte('t')]) then return sz * [1024ULL ^ 4], true end + case [uint8]([string.byte('e')]) then return sz * [1024ULL ^ 5], true end + case [uint8]([string.byte('y')]) then return sz * [1024ULL ^ 6], true end + else return sz, true + end + end + ::skip::end + return sz, true +end return m Index: mgtool.t ================================================================== --- mgtool.t +++ mgtool.t @@ -299,10 +299,12 @@ lib.bail('you are attempting to completely obliterate all data! make sure you have selected your target correctly. if you really want to do this, pass the confirmation string ', &cfmstr[0]) elseif dbmode.arglist.ct == 2 then if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then lib.warn('completely obliterating all data!') dlg:obliterate_everything() + elseif lib.str.cmp(dbmode.arglist(1), 'print-confirmation-string') == 0 then + lib.io.send(1, cfmstr, lib.str.sz(cfmstr)) else lib.bail('you passed an incorrect confirmation string; pass ', &cfmstr[0], ' if you really want to destroy everything') end else goto cmderr end else goto cmderr end @@ -331,19 +333,23 @@ if cfmode.arglist.ct == 1 then if lib.str.cmp(cfmode.arglist(0),'chsec') == 0 then var sec: int8[65] gensec(&sec[0]) dlg:conf_set('server-secret', &sec[0]) lib.report('server secret reset') - -- FIXME notify server to reload its config elseif lib.str.cmp(cfmode.arglist(0),'refresh') == 0 then - -- TODO notify server to reload config + cfmode.no_notify = false -- duh else goto cmderr end elseif cfmode.arglist.ct == 3 and lib.str.cmp(cfmode.arglist(0),'set') == 0 then dlg:conf_set(cfmode.arglist(1),cfmode.arglist(2)) lib.report('parameter set') else goto cmderr end + + -- successful commands fall through + if not cfmode.no_notify then + dlg:ipc_send(lib.ipc.cmd.cfgrefresh,0) + end else srv:setup(cnf) srv:conprep(lib.store.prepmode.full) if lib.str.cmp(mode.arglist(0),'mkroot') == 0 then var cfmode: pbasic cfmode:parse(mode.arglist.ct, &mode.arglist(0)) @@ -366,11 +372,12 @@ var epithets = array( 'root', 'god', 'regional jehovah', 'titan king', 'king of olympus', 'cyberpharaoh', 'electric ellimist', "rampaging c'tan", 'deathless tweetlord', 'postmaster', 'faerie queene', 'lord of the posts', 'ruthless cybercrat', - 'general secretary', 'commissar', 'kwisatz haderach' + 'general secretary', 'commissar', 'kwisatz haderach', + 'dedicated hyperturing' -- feel free to add more ) root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])] root.rights.powers:fill() -- grant omnipotence root.rights.rank = 1 Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -127,11 +127,10 @@ in val end return q end); proc = { fork = terralib.externfunction('fork', {} -> int); - daemonize = terralib.externfunction('daemon', {int,int} -> {}); exit = terralib.externfunction('exit', int -> {}); getenv = terralib.externfunction('getenv', rawstring -> rawstring); exec = terralib.externfunction('execv', {rawstring,&rawstring} -> int); execp = terralib.externfunction('execvp', {rawstring,&rawstring} -> int); }; @@ -140,10 +139,11 @@ recv = terralib.externfunction('read', {int, rawstring, intptr} -> ptrdiff); close = terralib.externfunction('close', {int} -> int); say = macro(function(msg) return `lib.io.send(2, msg, [#(msg:asvalue())]) end); fmt = terralib.externfunction('printf', terralib.types.funcpointer({rawstring},{int},true)); + ttyp = terralib.externfunction('isatty', int -> int); }; str = { sz = terralib.externfunction('strlen', rawstring -> intptr) }; copy = function(tbl) local new = {} for k,v in pairs(tbl) do new[k] = v end @@ -275,10 +275,23 @@ end end end return q end) + terra set:setbit(i: intptr, val: bool) + if val then + self._store[i/8] = self._store[i/8] or (1 << (i % 8)) + else + self._store[i/8] = self._store[i/8] and not (1 << (i % 8)) + end + end + set.bits = {} + set.idvmap = {} + for i,v in ipairs(tbl) do + set.idvmap[v] = i + set.bits[v] = quote var b: set b:clear() b:setbit(i, true) in b end + end set.metamethods.__add = macro(function(self,other) local new = symbol(set) local q = quote var [new] new:clear() end for i = 0, bytes - 1 do q = quote [q] @@ -294,10 +307,31 @@ q = quote [q] new._store[i] = self._store[i] and other._store[i] end end return quote [q] in new end + end) + set.metamethods.__eq = macro(function(self,other) + local rt = symbol(bool) + local fb if #tbl % 8 == 0 then fb = bytes - 1 else fb = bytes - 2 end + local q = quote rt = true end + for i = 0, fb do + q = quote + if self._store[i] ~= other._store[i] then rt = false else [q] end + end + end + -- we need to mask out any extraneous bits the values might have, as we + -- don't want the kind of noise introduced by :fill() to affect comparison + if #tbl % 8 ~= 0 then + local last = #tbl-1 + local msk = (2 ^ (#tbl % 8)) - 1 + q = quote + if (self._store [last] and [uint8](msk)) ~= + (other._store[last] and [uint8](msk)) then rt = false else [q] end + end + end + return quote var [rt]; [q] in rt end end) set.metamethods.__not = macro(function(self) local new = symbol(set) local q = quote var [new] new:clear() end for i = 0, bytes - 1 do @@ -347,11 +381,11 @@ lib.net = lib.loadlib('mongoose','mongoose.h') lib.pq = lib.loadlib('libpq','libpq-fe.h') lib.load { 'mem', 'math', 'str', 'file', 'crypt', 'ipc'; - 'http', 'html', 'session', 'tpl', 'store'; + 'http', 'html', 'session', 'tpl', 'store', 'acl'; 'smackdown'; -- md-alike parser } local be = {} @@ -389,19 +423,20 @@ 'srv'; 'render:nav'; 'render:nym'; 'render:login'; 'render:profile'; - 'render:compose'; 'render:tweet'; - 'render:userpage'; + 'render:tweet-page'; + 'render:user-page'; 'render:timeline'; 'render:docpage'; 'render:conf:profile'; + 'render:conf:sec'; 'render:conf'; 'route'; } do Index: render/compose.t ================================================================== --- render/compose.t +++ render/compose.t @@ -1,30 +1,31 @@ -- vim: ft=terra local terra -render_compose(co: &lib.srv.convo, edit: &lib.store.post) +render_compose(co: &lib.srv.convo, edit: &lib.store.post, acc: &lib.str.acc) var target, tgtlen = co:getv('to') var form: data.view.compose + form = data.view.compose { + handle = co.who.handle; + circles = ''; -- TODO: list user's circles, rooms, and saved aclexps + } if edit == nil then - form = data.view.compose { - content = lib.coalesce(target, ''); - acl = lib.trn(target == nil, 'all', 'mentioned'); -- TODO default acl setting? - handle = co.who.handle; - circles = ''; -- TODO: list user's circles, rooms, and saved aclexps - } + form.content = lib.coalesce(target, '') + form.acl = lib.trn(target == nil, 'all', 'mentioned') -- TODO default acl setting? + else + form.content = lib.coalesce(edit.body, '') + form.acl = edit.acl end + if acc ~= nil then form:append(acc) return end + var cotxt = form:tostr() defer cotxt:free() - var doc = data.view.docskel { - instance = co.srv.cfg.instance; + var doc = [lib.srv.convo.page] { title = lib.str.plit 'compose'; body = cotxt; class = lib.str.plit 'compose'; - navlinks = co.navbar; + cache = true; } - var hdrs = array( - lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } - ) - doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]}) + co:stdpage(doc) end return render_compose Index: render/conf/profile.t ================================================================== --- render/conf/profile.t +++ render/conf/profile.t @@ -6,15 +6,14 @@ return pstr { ptr = s, ct = lib.str.sz(s) } end local terra render_conf_profile(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr - var c = data.view.conf_profile { handle = cs(co.who.handle); nym = cs(lib.coalesce(co.who.nym,'')); bio = cs(lib.coalesce(co.who.bio,'')); } return c:tostr() end return render_conf_profile ADDED render/conf/sec.t Index: render/conf/sec.t ================================================================== --- render/conf/sec.t +++ render/conf/sec.t @@ -0,0 +1,25 @@ +-- vim: ft=terra +local pstr = lib.mem.ptr(int8) +local pref = lib.mem.ref(int8) +local terra +render_conf_sec(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr + var time: lib.store.timepoint = co.who.source:auth_sigtime_user_fetch(co.who.id) + var tstr: int8[26] + lib.osclock.ctime_r(&time, &tstr[0]) + var body = data.view.conf_sec { + lastreset = pstr { + ptr = &tstr[0], ct = lib.str.sz(&tstr[0]) + } + } + + if co.srv.cfg.credmgd then + var a: lib.str.acc a:init(768) + body:append(&a) + var credmgr = data.view.conf_sec_credmg { + credlist = '' + } + credmgr:append(&a) + return a:finalize() + else return body:tostr() end +end +return render_conf_sec Index: render/docpage.t ================================================================== --- render/docpage.t +++ render/docpage.t @@ -69,11 +69,11 @@ pushbranches(list: &lib.str.acc, idx: intptr, ps: lib.store.powerset): {} var [pages] = array([allpages]) var started = false for i=0,[pages.type.N] do if pages[i].parent == idx+1 and (pages[i].priv:sz() == 0 or - (ps and pages[i].priv):sz() > 0) then + (ps and pages[i].priv) == pages[i].priv) then if not started then started = true list:lpush('