code wiki / _hdl_build / nx_siteedit_history.nx
nx_siteedit_history.nx source
↩ module page · 145 lines · 5901 B
1// nx_siteedit_history.nx -- N-deep revision history for the site editor. PURE enumeration lib (nx_syscalls only,
2// no HTML): sed_version_keep writes a `<cfgpath>.v<epoch>` snapshot on EVERY save, so history already accumulates
3// on disk -- this reads it back. sh_list getdents64's the config's directory, matches `<basename>.v<digits>`,
4// collects epochs; sh_sort_desc orders newest-first; sh_version_path rebuilds a snapshot path for restore/view.
5// The daemon renders the browse+restore page from these + does the (additive) restore. Gated by
6// nx_siteedit_history_gate (fixture dir, no live FS). license_tier: ORIGINAL
7import "nx_syscalls.nx"
8const SH_MAGIC_1024: i64 = 1024
9const SH_MAGIC_65536: i64 = 65536
10
11const SH_OPENAT: i64 = 257 // x86_64 openat(2)
12const SH_GETDENTS64: i64 = 217 // x86_64 getdents64(2)
13const SH_AT_FDCWD: i64 = 0 - 100
14const SH_O_DIRECTORY: i64 = 0x10000 // O_RDONLY(0) | O_DIRECTORY
15
16func sh_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
17func sh_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o] = s[i]; o = o + 1; i = i + 1 } return o }
18func sh_catn(d: *u8, o: i64, v: i64) -> i64 {
19 if v == 0 { d[o] = 48 as u8; return o + 1 }
20 var m: i64 = v
21 var nd: i64 = 0
22 var t: i64 = m
23 while t > 0 { nd = nd + 1; t = t / 10 }
24 var i: i64 = nd - 1
25 while i >= 0 { d[o + i] = (48 + (m % 10)) as u8; m = m / 10; i = i - 1 }
26 return o + nd
27}
28
29// dir(cfgpath) into dbuf (NUL-terminated); returns the basename offset (index just past the last '/').
30func sh_dir_of(cfgpath: *u8, dbuf: *u8) -> i64 {
31 var last: i64 = 0 - 1
32 var i: i64 = 0
33 while cfgpath[i] != (0 as u8) { if (cfgpath[i] as i64) == 47 { last = i } i = i + 1 }
34 if last < 0 { dbuf[0] = 46 as u8; dbuf[1] = 0 as u8; return 0 } // no '/' -> "." (cwd)
35 if last == 0 { dbuf[0] = 47 as u8; dbuf[1] = 0 as u8; return 1 } // root
36 var j: i64 = 0
37 while j < last { dbuf[j] = cfgpath[j]; j = j + 1 }
38 dbuf[last] = 0 as u8
39 return last + 1
40}
41
42// if name == "<bname>.v<digits>" return the epoch, else 0 (rejects <bname>.new, <bname>, <bname>.vX9, others).
43func sh_match(name: *u8, bname: *u8, bnl: i64) -> i64 {
44 var i: i64 = 0
45 while i < bnl { if (name[i] as i64) != (bname[i] as i64) { return 0 } i = i + 1 }
46 if (name[bnl] as i64) != 46 { return 0 } // '.'
47 if (name[bnl + 1] as i64) != 118 { return 0 } // 'v'
48 var k: i64 = bnl + 2
49 var ep: i64 = 0
50 var any: i64 = 0
51 while name[k] != (0 as u8) {
52 let c: i64 = name[k] as i64
53 if c < 48 { return 0 }
54 if c > 57 { return 0 }
55 ep = ep * 10 + (c - 48)
56 any = 1
57 k = k + 1
58 }
59 if any == 0 { return 0 }
60 return ep
61}
62
63func sh_open_dir(path: *u8) -> i64 { return __syscall(SH_OPENAT, SH_AT_FDCWD, path as i64, SH_O_DIRECTORY, 0, 0, 0) }
64func sh_getdents(fd: i64, buf: *u8, cap: i64) -> i64 { return __syscall(SH_GETDENTS64, fd, buf as i64, cap, 0, 0, 0) }
65
66// enumerate <cfgpath>.v<epoch> snapshots -> eps[0..cap); returns count (bounded by cap). 0 on unreadable dir.
67func sh_list(cfgpath: *u8, eps: *i64, cap: i64) -> i64 {
68 let dbuf: *u8 = sys_mmap(SH_MAGIC_1024)
69 let boff: i64 = sh_dir_of(cfgpath, dbuf)
70 let bname: *u8 = ((cfgpath as i64) + boff) as *u8
71 let bnl: i64 = sh_slen(bname)
72 let dfd: i64 = sh_open_dir(dbuf)
73 if dfd < 0 { return 0 }
74 let buf: *u8 = sys_mmap(SH_MAGIC_65536)
75 var cnt: i64 = 0
76 var go: i64 = 1
77 while go == 1 {
78 let n: i64 = sh_getdents(dfd, buf, SH_MAGIC_65536)
79 if n <= 0 { go = 0 } else {
80 var off: i64 = 0
81 while off < n {
82 let rb: *u8 = ((buf as i64) + off + 16) as *u8
83 let reclen: i64 = (rb[0] as i64) | ((rb[1] as i64) << 8)
84 if reclen <= 0 { off = n } else {
85 let nm: *u8 = ((buf as i64) + off + 19) as *u8
86 let ep: i64 = sh_match(nm, bname, bnl)
87 if ep > 0 { if cnt < cap { eps[cnt] = ep; cnt = cnt + 1 } }
88 off = off + reclen
89 }
90 }
91 }
92 }
93 sys_close(dfd)
94 return cnt
95}
96
97// insertion sort DESCENDING (newest epoch first). Small N (bounded by the list cap) -> O(n^2) is fine.
98func sh_sort_desc(eps: *i64, n: i64) -> i64 {
99 var i: i64 = 1
100 while i < n {
101 let key: i64 = eps[i]
102 var j: i64 = i - 1
103 var run: i64 = 1
104 while run == 1 {
105 if j < 0 { run = 0 } else {
106 if eps[j] < key { eps[j + 1] = eps[j]; j = j - 1 } else { run = 0 }
107 }
108 }
109 eps[j + 1] = key
110 i = i + 1
111 }
112 return 0
113}
114
115// parse the &v=<digits> query param from a request path (restore/view carry ?s=<token>&v=<epoch>). Matches the
116// literal "&v=" so a stray 'v' inside the base64url session token can't false-match. Returns the epoch, or 0.
117func sh_qs_epoch(path: *u8, pn: i64) -> i64 {
118 var i: i64 = 0
119 while i + 3 <= pn {
120 if (path[i] as i64) == 38 { if (path[i + 1] as i64) == 118 { if (path[i + 2] as i64) == 61 { // '&' 'v' '='
121 var k: i64 = i + 3
122 var ep: i64 = 0
123 var any: i64 = 0
124 var run: i64 = 1
125 while run == 1 {
126 if k >= pn { run = 0 } else {
127 let c: i64 = path[k] as i64
128 if c < 48 { run = 0 } else { if c > 57 { run = 0 } else { ep = ep * 10 + (c - 48); any = 1; k = k + 1 } }
129 }
130 }
131 if any == 1 { return ep }
132 } } }
133 i = i + 1
134 }
135 return 0
136}
137
138// build "<cfgpath>.v<epoch>" into out (NUL-terminated); returns length.
139func sh_version_path(cfgpath: *u8, ep: i64, out: *u8) -> i64 {
140 var o: i64 = sh_cat(out, 0, cfgpath)
141 o = sh_cat(out, o, ".v" as *u8)
142 o = sh_catn(out, o, ep)
143 out[o] = 0 as u8
144 return o
145}