nx_site_publish_gate.nx source
↩ module page · 215 lines · 14758 B
1// nx_site_publish_gate.nx -- gates artifact-publish placement semantics on a THROWAWAY CWD (the docroot
2// is CWD-relative sites/): fresh publish lands byte-identical; republish preserves .prev AND lands v2;
3// traversal ('..', absolute) DENIED with NOTHING written; secret-needle relpath DENIED via the IO layer;
4// absent source honest ABSENT. Runner: mkdir a scratch dir, cd into it, run. Exit 0 only on all-PASS.
5// license_tier: ORIGINAL expect_exit: 0
6import "nx_site_publish_lib.nx"
7import "nx_seg_store.nx" // ss_writefile -- fixture author
8import "nx_gate_verdict.nx"
9
10const SG_CASES: *u8 = "fresh-publish-lands\nfirst-publish-retry-does-not-create-prev\nrepublish-preserves-prev\nchanged-publish-retry-retains-rollback\nbinary-difference-after-nul-and-retry\nrelpath-traversal-denied\nabsolute-site-denied\nordinary-key-name-allowed\nsecret-relpath-denied\nabsent-source-honest\nmissing-parent-dir-fails-loud\nguarded-create-exact\nwrong-artifact-pin-retains-live\nstale-live-pin-retains-live\nguarded-change-retains-predecessor\nguarded-retry-original-precondition\nguarded-reverse-retains-replaced-version\ncontended-target-refuses-without-wait\nreleased-lock-allows-retry\noversized-path-refused-before-construction\nstage-wrong-pin-creates-no-candidate\nstage-exact-candidate\nstage-retry-preserves-candidate\nstage-replacement-refused\nlegacy-publish-cannot-overwrite-staging\nguarded-publish-cannot-overwrite-staging\nstage-refuses-live-route\nstaging-alias-overwrites-refused\nregistered-site-config-written\nregistered-site-plane-seeded\nunregistered-stage-refused-without-placement\nrelease-identity-registered\nlegacy-publish-cannot-ignore-registered-hash\nregistered-stage-places-exact-bytes\nregistered-stage-retry-keeps-predecessor-absent\nconflicting-registration-cannot-authorize-replacement\n" as *u8
11static sg_plan: *i64
12const SG_ARG_CELL_BYTES: i64 = 8 // native x86-64 argv uses i64 pointer cells
13const SG_ARGV_BYTES: i64 = (PB_GUARDED_ARGC + 1) * SG_ARG_CELL_BYTES
14
15func sg_check(name: *u8, ok: i64, pass: *i64) -> i64 {
16 return gv_plan_check(sg_plan,name,ok,pass)
17}
18func sg_run(src: *u8, site: *u8, rel: *u8) -> i64 {
19 let av: *i64 = sys_mmap(SG_ARGV_BYTES) as *i64
20 av[0] = "nx_site_publish" as *u8 as i64
21 av[PB_ARG_VERB] = "publish" as *u8 as i64
22 av[PB_ARG_SRC] = src as i64
23 av[PB_ARG_SITE] = site as i64
24 av[PB_ARG_REL] = rel as i64
25 let rc: i64 = sp_main(PB_ARGC,av)
26 sys_munmap(av as *u8,SG_ARGV_BYTES)
27 return rc
28}
29func sg_eq(path: *u8, expect: *u8) -> i64 {
30 return sg_eq_bytes(path,expect,vw_slen(expect))
31}
32func sg_eq_bytes(path: *u8, expect: *u8, expected_bytes: i64) -> i64 {
33 // One extra byte detects unexpected trailing content instead of accepting a prefix.
34 let read_bytes: i64 = expected_bytes+1
35 let b: *u8 = sys_mmap(read_bytes)
36 let n: i64 = vw_read(path,b,read_bytes)
37 let same: i64 = pb_same_bytes(b,n,expect,expected_bytes)
38 sys_munmap(b,read_bytes)
39 return same
40}
41func sg_absent(path: *u8) -> i64 {
42 let fd: i64 = sys_openat_rd(path)
43 // Linux ENOENT establishes absence; permission and other I/O failures do not.
44 if fd == (0-2) { return 1 }
45 if fd >= 0 { sys_close(fd) }
46 return 0
47}
48
49func main(argc: i64, argv: *i64) -> i64 {
50 // An explicit scratch directory must be new; refuse to run fixtures over an existing tree.
51 if argc > 1 {
52 let scratch: *u8 = argv[1] as *u8
53 if sys_mkdir(scratch, 0x1ed) != 0 { return 2 }
54 if sys_chdir(scratch) != 0 { return 2 }
55 }
56 let pass: *i64 = gv_ctr()
57 sg_plan = gv_plan_new(SG_CASES)
58 sys_mkdir("sites" as *u8, 0x1ed)
59 sys_mkdir("sites/gatesite" as *u8, 0x1ed)
60 ss_writefile("v1.html" as *u8, "<h1>one</h1>" as *u8, vw_slen("<h1>one</h1>" as *u8))
61 ss_writefile("v2.html" as *u8, "<h1>two</h1>" as *u8, vw_slen("<h1>two</h1>" as *u8))
62 // T1 fresh publish lands byte-identical, no .prev
63 var ok1: i64 = 0
64 if sg_run("v1.html" as *u8, "gatesite" as *u8, "page.html" as *u8) == 0 {
65 if sg_eq("sites/gatesite/page.html" as *u8, "<h1>one</h1>" as *u8) == 1 {
66 ok1 = sg_absent("sites/gatesite/page.html.prev" as *u8)
67 }
68 }
69 sg_check("fresh-publish-lands" as *u8, ok1, pass)
70 var retry1: i64 = 0
71 if sg_run("v1.html" as *u8, "gatesite" as *u8, "page.html" as *u8) == 0 {
72 if sg_eq("sites/gatesite/page.html" as *u8, "<h1>one</h1>" as *u8) == 1 {
73 retry1 = sg_absent("sites/gatesite/page.html.prev" as *u8)
74 }
75 }
76 sg_check("first-publish-retry-does-not-create-prev" as *u8, retry1, pass)
77 // T2 republish: .prev holds v1, target holds v2
78 var ok2: i64 = 0
79 if sg_run("v2.html" as *u8, "gatesite" as *u8, "page.html" as *u8) == 0 {
80 if sg_eq("sites/gatesite/page.html" as *u8, "<h1>two</h1>" as *u8) == 1 {
81 ok2 = sg_eq("sites/gatesite/page.html.prev" as *u8, "<h1>one</h1>" as *u8)
82 }
83 }
84 sg_check("republish-preserves-prev" as *u8, ok2, pass)
85 var retry2: i64 = 0
86 if sg_run("v2.html" as *u8, "gatesite" as *u8, "page.html" as *u8) == 0 {
87 if sg_eq("sites/gatesite/page.html" as *u8, "<h1>two</h1>" as *u8) == 1 {
88 retry2 = sg_eq("sites/gatesite/page.html.prev" as *u8, "<h1>one</h1>" as *u8)
89 }
90 }
91 sg_check("changed-publish-retry-retains-rollback" as *u8, retry2, pass)
92 let binary_bytes: i64 = 3 // fixture is exactly [A, NUL, B/C]
93 let binary1: *u8 = sys_mmap(binary_bytes)
94 let binary2: *u8 = sys_mmap(binary_bytes)
95 binary1[0] = 65 as u8; binary1[1] = 0 as u8; binary1[2] = 66 as u8
96 binary2[0] = 65 as u8; binary2[1] = 0 as u8; binary2[2] = 67 as u8
97 ss_writefile("binary1" as *u8, binary1, binary_bytes)
98 ss_writefile("binary2" as *u8, binary2, binary_bytes)
99 var binary_ok: i64 = 0
100 if sg_run("binary1" as *u8, "gatesite" as *u8, "asset.wasm" as *u8) == 0 {
101 if sg_run("binary2" as *u8, "gatesite" as *u8, "asset.wasm" as *u8) == 0 {
102 if sg_run("binary2" as *u8, "gatesite" as *u8, "asset.wasm" as *u8) == 0 {
103 if sg_eq_bytes("sites/gatesite/asset.wasm" as *u8,binary2,binary_bytes) == 1 {
104 binary_ok = sg_eq_bytes("sites/gatesite/asset.wasm.prev" as *u8,binary1,binary_bytes)
105 }
106 }
107 }
108 }
109 sg_check("binary-difference-after-nul-and-retry" as *u8, binary_ok, pass)
110 sys_munmap(binary1,binary_bytes); sys_munmap(binary2,binary_bytes)
111 // T3 traversal in relpath DENIED, nothing written outside
112 var ok3: i64 = 0
113 if sg_run("v1.html" as *u8, "gatesite" as *u8, "../escape.html" as *u8) == FSX_RC_DENIED {
114 ok3 = sg_absent("sites/escape.html" as *u8)
115 }
116 sg_check("relpath-traversal-denied" as *u8, ok3, pass)
117 // T4 absolute site DENIED
118 sg_check("absolute-site-denied" as *u8, (sg_run("v1.html" as *u8, "/etc" as *u8, "x.html" as *u8) == FSX_RC_DENIED) as i64, pass)
119 // Policy distinguishes an ordinary key-containing name from a secret-bearing path.
120 sg_check("ordinary-key-name-allowed" as *u8, (sg_run("v1.html" as *u8, "gatesite" as *u8, "apikey.html" as *u8) == 0) as i64, pass)
121 var secret_ok: i64 = 0
122 if sg_run("v1.html" as *u8, "gatesite" as *u8, "api_secret.html" as *u8) == FSX_RC_DENIED {
123 secret_ok = sg_absent("sites/gatesite/api_secret.html" as *u8)
124 }
125 sg_check("secret-relpath-denied" as *u8, secret_ok, pass)
126 // T6 absent source -> honest ABSENT
127 sg_check("absent-source-honest" as *u8, (sg_run("nope.html" as *u8, "gatesite" as *u8, "y.html" as *u8) == FSX_RC_ABSENT) as i64, pass)
128 // T7 REGRESSION (debt seq910): a relpath whose PARENT DIRECTORY does not exist must fail
129 // LOUD with FSX_RC_IO and write nothing. This is the case T1-T6 never covered -- the gate
130 // was 6/6 GREEN while the only silent exit in sp_main went untested, and a real publish to
131 // "research/rt004.html" returned empty output and read as success.
132 var ok7: i64 = 0
133 if sg_run("v1.html" as *u8, "gatesite" as *u8, "nodir/page.html" as *u8) == FSX_RC_IO {
134 ok7 = sg_absent("sites/gatesite/nodir/page.html" as *u8)
135 }
136 sg_check("missing-parent-dir-fails-loud" as *u8, ok7, pass)
137
138 let av: *i64 = sys_mmap(SG_ARGV_BYTES) as *i64
139 let h1: *u8 = sys_mmap(PB_SHA_HEX+1)
140 let h2: *u8 = sys_mmap(PB_SHA_HEX+1)
141 pb_hash("<h1>one</h1>" as *u8,12,h1)
142 pb_hash("<h1>two</h1>" as *u8,12,h2)
143 av[0]="nx_site_publish" as *u8 as i64;av[1]="publish-guarded" as *u8 as i64
144 av[2]="v1.html" as *u8 as i64;av[3]="gatesite" as *u8 as i64
145 av[4]="guard.html" as *u8 as i64;av[5]=h1 as i64;av[6]="absent" as *u8 as i64
146 let fresh: i64 = sp_main(PB_GUARDED_ARGC,av)
147 sg_check("guarded-create-exact",((fresh==0)&&(sg_eq("sites/gatesite/guard.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass)
148 av[2]="v2.html" as *u8 as i64;av[5]=h1 as i64;av[6]=h1 as i64
149 let wrong: i64 = sp_main(PB_GUARDED_ARGC,av)
150 sg_check("wrong-artifact-pin-retains-live",((wrong==PB_RC_CONFLICT)&&(sg_eq("sites/gatesite/guard.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass)
151 av[5]=h2 as i64;av[6]="absent" as *u8 as i64
152 let stale: i64 = sp_main(PB_GUARDED_ARGC,av)
153 sg_check("stale-live-pin-retains-live",((stale==PB_RC_CONFLICT)&&(sg_absent("sites/gatesite/guard.html.prev" as *u8)==1)) as i64,pass)
154 av[6]=h1 as i64
155 let change: i64 = sp_main(PB_GUARDED_ARGC,av)
156 sg_check("guarded-change-retains-predecessor",((change==0)&&(sg_eq("sites/gatesite/guard.html.prev" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass)
157 let retry: i64 = sp_main(PB_GUARDED_ARGC,av)
158 sg_check("guarded-retry-original-precondition",((retry==0)&&(sg_eq("sites/gatesite/guard.html.prev" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass)
159 av[2]="v1.html" as *u8 as i64;av[5]=h1 as i64;av[6]=h2 as i64
160 let reverse: i64 = sp_main(PB_GUARDED_ARGC,av)
161 sg_check("guarded-reverse-retains-replaced-version",((reverse==0)&&(sg_eq("sites/gatesite/guard.html" as *u8,"<h1>one</h1>" as *u8)==1)&&(sg_eq("sites/gatesite/guard.html.prev" as *u8,"<h1>two</h1>" as *u8)==1)) as i64,pass)
162 let lockfd: i64 = sys_openat_wr("sites/gatesite/guard.html.publish-lock" as *u8,FSX_MODE_RW)
163 var held: i64 = 0
164 if lockfd >= 0 {
165 if sys_flock(lockfd,PB_LOCK_EX_NB)==0 { held=(sp_main(PB_GUARDED_ARGC,av)==PB_RC_BUSY) as i64 }
166 sys_close(lockfd)
167 }
168 sg_check("contended-target-refuses-without-wait",held,pass)
169 sg_check("released-lock-allows-retry",(sp_main(PB_GUARDED_ARGC,av)==0) as i64,pass)
170 let longrel: *u8 = sys_mmap(PB_PATH+1)
171 var li: i64 = 0
172 while li < PB_PATH { longrel[li]=97 as u8;li=li+1 };longrel[li]=0 as u8
173 av[4]=longrel as i64
174 sg_check("oversized-path-refused-before-construction",(sp_main(PB_GUARDED_ARGC,av)==FSX_RC_DENIED) as i64,pass)
175 sys_mkdir("sites/gatesite/releases" as *u8,0x1ed)
176 av[1]="stage" as *u8 as i64;av[2]="v1.html" as *u8 as i64
177 av[4]="releases/candidate.html" as *u8 as i64;av[5]=h2 as i64
178 sg_check("stage-wrong-pin-creates-no-candidate",((sp_main(PB_STAGE_ARGC,av)==PB_RC_CONFLICT)&&(sg_absent("sites/gatesite/releases/candidate.html" as *u8)==1)) as i64,pass)
179 av[5]=h1 as i64
180 sg_check("stage-exact-candidate",((sp_main(PB_STAGE_ARGC,av)==0)&&(sg_eq("sites/gatesite/releases/candidate.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass)
181 sg_check("stage-retry-preserves-candidate",((sp_main(PB_STAGE_ARGC,av)==0)&&(sg_absent("sites/gatesite/releases/candidate.html.prev" as *u8)==1)) as i64,pass)
182 av[2]="v2.html" as *u8 as i64;av[5]=h2 as i64
183 sg_check("stage-replacement-refused",((sp_main(PB_STAGE_ARGC,av)==PB_RC_CONFLICT)&&(sg_eq("sites/gatesite/releases/candidate.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass)
184 sg_check("legacy-publish-cannot-overwrite-staging",(sg_run("v2.html" as *u8,"gatesite" as *u8,"releases/candidate.html" as *u8)==PB_RC_CONFLICT) as i64,pass)
185 av[1]="publish-guarded" as *u8 as i64;av[6]=h1 as i64
186 sg_check("guarded-publish-cannot-overwrite-staging",(sp_main(PB_GUARDED_ARGC,av)==PB_RC_CONFLICT) as i64,pass)
187 av[1]="stage" as *u8 as i64;av[4]="page.html" as *u8 as i64
188 sg_check("stage-refuses-live-route",(sp_main(PB_STAGE_ARGC,av)==FSX_RC_DENIED) as i64,pass)
189 var aliases: i64 = 0
190 if sg_run("v2.html" as *u8,"gatesite" as *u8,"./releases/candidate.html" as *u8)==FSX_RC_DENIED {
191 if sg_run("v2.html" as *u8,"gatesite/releases" as *u8,"candidate.html" as *u8)==FSX_RC_DENIED {
192 if sg_run("v2.html" as *u8,"gatesite" as *u8,"releases//candidate.html" as *u8)==FSX_RC_DENIED {
193 aliases=sg_eq("sites/gatesite/releases/candidate.html" as *u8,"<h1>one</h1>" as *u8)
194 }
195 }
196 }
197 sg_check("staging-alias-overwrites-refused",aliases,pass)
198 sys_mkdir("knowledge" as *u8,0x1ed)
199 let rcfg: *u8 = "gatesite\tsites/gatesite\tknowledge/pub-\thttps://example.invalid\tGate\tprivate/\n" as *u8
200 sg_check("registered-site-config-written",ss_writefile("knowledge/pub_sites.conf" as *u8,rcfg,ss_len(rcfg)) == 0,pass)
201 let rw: *i64 = ss_begin_cap(64)
202 ss_add(rw,1,"q:n" as *u8,"0" as *u8,1)
203 sg_check("registered-site-plane-seeded",ss_commit("knowledge/pub-" as *u8,rw,ss_next_segid("knowledge/pub-" as *u8)) == 0,pass)
204 av[1]="stage" as *u8 as i64; av[2]="v1.html" as *u8 as i64
205 av[4]="releases/registered.html" as *u8 as i64; av[5]=h1 as i64
206 sg_check("unregistered-stage-refused-without-placement",((sp_main(PB_STAGE_ARGC,av)==PB_RC_CONFLICT)&&(sg_absent("sites/gatesite/releases/registered.html" as *u8)==1)) as i64,pass)
207 sg_check("release-identity-registered",pr_register("knowledge/pub-" as *u8,"releases/registered.html" as *u8,h1)==PR_CREATED,pass)
208 sg_check("legacy-publish-cannot-ignore-registered-hash",((sg_run("v2.html" as *u8,"gatesite" as *u8,"releases/registered.html" as *u8)==PB_RC_CONFLICT)&&(sg_absent("sites/gatesite/releases/registered.html" as *u8)==1)) as i64,pass)
209 sg_check("registered-stage-places-exact-bytes",((sp_main(PB_STAGE_ARGC,av)==0)&&(sg_eq("sites/gatesite/releases/registered.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass)
210 sg_check("registered-stage-retry-keeps-predecessor-absent",((sp_main(PB_STAGE_ARGC,av)==0)&&(sg_absent("sites/gatesite/releases/registered.html.prev" as *u8)==1)) as i64,pass)
211 sg_check("conflicting-registration-cannot-authorize-replacement",((pr_register("knowledge/pub-" as *u8,"releases/registered.html" as *u8,h2)==PR_CONFLICT)&&(sg_eq("sites/gatesite/releases/registered.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass)
212 gv_plan_finish(sg_plan,pass)
213 sys_munmap(av as *u8,SG_ARGV_BYTES)
214 return gv_verdict("SITE-PUBLISH-GATE" as *u8,pass,"atomic publication, retry preservation, candidate identity and staging overwrite refusal" as *u8)
215}