sprawling-runtime 0.0.11

One run of a sprawling resident, from dispatch to freeze: turns, tools, replay and the sandbox that executes code.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-- Copyright (c) 2026 2youg1 and the sprawling contributors

/-!
# Windows native execution
规定 `crates/runtime/src/tools/exec/native_windows.rs`、
`crates/runtime/src/tools/exec/native_windows/msvc.rs` 与
`crates/desktop/ffi/src/confinement.rs`、`crates/desktop/ffi/zig/confinement.zig`。

D53:Windows native 使用无 capability 的 AppContainer 与匿名 Job Object;
CreateProcessW 以 CREATE_SUSPENDED 创建,设置 command Job 的 CPU hard cap 与 kill-on-close
(User 填写的 aggregate committed-memory 上限在 run Job 上,D54),AssignProcessToJobObject 成功之后才 ResumeThread。
没有准入安全 API 的平台调用进入既有 Zig leaf;win32job 与 winsafe 的现有安全
CreateProcess 面没有 SECURITY_CAPABILITIES / STARTUPINFOEX,不能承担此契约。

D54:native 的 CPU hard cap 默认 50% 整机份额,未设置 run memory 时不设置内存上限;仅配置的 Shares::CpuAndMemory 请求
aggregate committed-memory 上限,且只设在 run job 上。CPU 默认数只在 native_windows.rs 定义。run job 在 table lock 内先创建、
设上限并挂上撞限的 watch(`crates/runtime/spec/Tools/Exec.lean` D95),native command 同时加入 command job 与 run job,再恢复;
command job 嵌在 run job 里,run job 的上限管住整棵树,多个命令的 aggregate 内存不能通过每条命令分别领限额来扩大。
command job 不另设同值上限:两只同值 job 嵌套时撞限消息落到内层 command job,run job 的 watch 读不到(实测见 Exec.lean D95),一个读处才说得清撞没撞。
拒绝设置限额或 watch 时停止启动。
内存不设缺省值是产品约束:没有填写上限的命令不能被隐式限额拒绝。

输入由 Rust 决定:已复制工作目录、明确程序和 argv、允许的环境、CPU 上限、
唯一 profile 名及 output files;叶子只执行平台操作。AppContainer SID
只获这次副本的继承读写 ACL,不获源树 ACL,无网络 capability 与 loopback exemption。
D57:显式环境声明的 CARGO_HOME 与 RUSTUP_HOME 可获得该 profile SID 的继承
read/execute ACE,不授 write、delete 或 ACL 修改权限。未声明的工具目录不自动授权。
授予与撤销在跨 Windows session 的 Global 命名 mutex 内读取并改写当前 DACL;
mutex 的 DACL 只给 Authenticated Users synchronize/modify-state,不给 WRITE_DAC,
因此其它账户的授权操作也使用同一排他机制,而文件的 WRITE_DAC 仍由 OS 单独检查。
声明根内的普通目录与文件先由 Rust 枚举,不跟随子 reparse point;
叶子只在声明根和启用 DACL protection 的后代授予 ACE,避免保护位阻止继承。
撤销只移除本次唯一 SID,不恢复旧 snapshot,因此另一个仍运行的 SID 授权不会丢失。
cleanup 等待整棵 job 退出后才撤销,撤销失败保留拥有 SID 与路径的资源并重试。
权限变更只涉及声明根及其后代;目录不存在表示资源已消失,不能改其父目录。
FFI packet 的第七个字段用 LF 分隔声明根与枚举出的后代;
`declared_roots` 标记字段开头有多少个声明根,空字段表示无额外授权;
Rust 拒绝路径中的 NUL 与 LF,叶子复制该字段,借出的 packet 地址不跨调用。
平台的 SetNamedSecurityInfo 继承传播与 mutex 排他属于环境假设,以下模型
证明在该假设下撤销一个 SID 保留其余 SID;disposable fixtures 验证实际并发清理。SetNamedSecurityInfo 可设置
SE_DACL_AUTO_INHERITED 标志,这只是继承已处理的记录,不改变权限;清理对拍
比较除该 bookkeeping 标志外的 SDDL,DACL protection 与全部 ACE 仍须一致。
Microsoft SECURITY_DESCRIPTOR_CONTROL 定义该标志的含义。
scratch 的继承 mandatory-integrity label 为 Low,避免 medium 默认标签即使
DACL 已授权仍因 write-up 禁止而拒绝 AppContainer 写入;只修改这次复制目录,
使用 LABEL_SECURITY_INFORMATION,不索取 SeSecurityPrivilege。
叶子保留 GetNamedSecurityInfo 分配的原 security descriptor 与授权路径的自有副本,
Rust 借出的 packet 地址不保留;command 结束后恢复副本根的
原 DACL 与 mandatory label,再释放 descriptor,避免复用副本时积累旧 SID 的 ACE。
原目录已被命令删除时视为权限资源已经不存在,不尝试改动它的父目录。
环境继续从 Command 的显式 allowlist 取值,Windows native loader 的 OS 根目录
变量由安全 GetSystemDirectory API 的父目录提供,并通过 Command 的 Windows
case-insensitive key 规则替换别名;它们是平台启动信息,不从 host environment 继承,
也不携带用户或 provider 凭据。USERPROFILE、APPDATA、TEMP 与 TMP 由 disposable
working directory 提供,不继承 host 路径。Microsoft 的 Implementing an AppContainer
「Creating the Profile」规定启动时把 LOCALAPPDATA/TEMP/TMP 重定向到 profile;
平台拼这条路径时取的是子进程环境块里的 LOCALAPPDATA,得
`<LOCALAPPDATA>\Packages\<profile>\AC` 与其下的 `Temp`,而
CreateAppContainerProfile 只在创建它的账户的 LocalAppData 下建这个目录并授本次 SID。
LOCALAPPDATA 因此取 serving 进程自己的 LOCALAPPDATA(profile 由同一账户创建),
重定向后的 TEMP 落在 profile 的私有存储里,随 profile 删除;LOCALAPPDATA 若给成
副本,TEMP 指向副本下不存在的 `Packages` 路径,windows-latest 上 link.exe 因此以
LNK1104 打不开临时文件。被否的备选是在副本里预建该路径:临时文件会混进
resident 看见的工作树,且不随 profile 删除。serving 进程没有 LOCALAPPDATA 时拒绝
起动并给恢复语。显式环境缺失这些初始化字段时,平台可能以 ERROR_ENVVAR_NOT_FOUND
拒绝;disposable production axes 的 child 写入自己的 TEMP,验证该启动条件。
错误携带真实失败阶段,避免把 loader 拒绝记作 guard 通过。
profile 的私有存储也是这次执行拥有的资源。harness/provider 不进入 AppContainer。

D58:Rust canonicalize 的 verbatim DOS/UNC prefix 在 native loader 边界转换为
普通 DOS/UNC prefix,剩余 UTF16 units 原样保留,argv 不参与该转换。
.NET Framework 的 FileIOPermission.CheckIllegalCharacters 拒绝 verbatim prefix 的
问号;AppDomainSetup.VerifyDir 因此可能在 PowerShell 的配置初始化中抛出,
不能把 ServicePointManager 的外层异常归因于缺少网络 capability。
仅承认磁盘与 UNC 的 verbatim 形;其它 device namespace 拒绝启动并给恢复语。
Microsoft 的 Naming Files, Paths, and Names 规定这两类 prefix,.NET reference
source 的 FileIOPermission 规定非法字符检查;disposable Framework probe 与
两种 PowerShell 初始化回归确认环境前提,网络 capability 仍为空。

D59:子进程自己打开 NUL 设备时(Rust std 的 `Stdio::null`,即 `Command::output`
与 `status` 的缺省 stdin;cmd 的 `>nul`),平台按 `\Device\Null` 的 DACL 判定,
而 AppContainer 的访问检查只认 package SID、capability SID 与
ALL APPLICATION PACKAGES (AC),不认 Everyone。Windows 11 client 的 NUL DACL
带 `(A;;0x1201bf;;;AC)` 与 `(A;;0x1201bf;;;S-1-15-2-2)`;GitHub windows-latest 的
Windows Server 2025 镜像只有 WD、SY、BA、RC 四条 ACE,于是 cargo 起
`rustc -vV` 时被拒(os error 5,cargo 报 never executed)。叶子给子进程的 stdin
是父进程打开、经 handle list 继承的 NUL handle,不受此前提影响。
harness 不改设备 DACL,因为它对整台机器的所有进程生效、要管理员的 WRITE_DAC,
且重启后复原;被否的备选是叶子在声明根授权的同一 Global mutex 内给本次 SID
加 NUL ACE,它在未提权的进程里必然失败,并把平台缺省的差异藏进每次起动。
disposable acceptance 先把 runner 的 NUL DACL 补上 client 的这两条 ACE,再运行
回归;`native_windows_disposable_argv_and_unrequested_memory` 的 `nul` child 经
Rust std 的缺省 capture 与直接打开 NUL 判定该前提。
未决:缺这两条 ACE 的主机(Windows Server)上明写 native 时,子进程自开 NUL
的失败只出现在命令的输出里。可选做法是起动前以挂起子进程的 token 对 NUL 的
security descriptor 做 AccessCheck 并拒绝起动,或由 doctor 的 native 行报告;
判定证据是 Windows Server 上真实工具链(cargo、cmd)在缺 ACE 时的命令结果,
缺省改为 native 之前须定。

D60:AppContainer 子进程自己找不到 MSVC。rustc 找 `link.exe`、cc 找 `cl.exe` 用的是
find-msvc-tools:先经 Visual Studio Setup Configuration 的 COM 枚举实例,再试
vswhere,二者读的 InprocServer32 DLL 与实例状态都在 `%ProgramData%\Microsoft\VisualStudio`
下,那里的 DACL 只给 Users 与 Everyone 读,没有 ALL APPLICATION PACKAGES。发现失败后
rustc 退回 PATH 上的裸 `link.exe`;GitHub windows-latest 的 PATH 上 Git 的
`usr\bin\link.exe`(msys coreutils)排在前面,它在容器里建不了 msys 的命名对象
(0xC0000022),以 0xc0000142 退出,`exec_builds` 的 Rust 构建因此失败。
launch 因此在容器外、以子进程已声明的环境为输入调用 find-msvc-tools 的
`find_tool_with_env`,求 host 架构的 `link.exe`,把它给出的 PATH、LIB、INCLUDE 目录
前置于子进程已声明的同名值(未声明时只含这些目录),与 vcvars 相同;子进程内的
rustc 退回裸 `link.exe` 时便落到 MSVC,cc 的 `cl.exe` 也同样。getter 不交出
PATH、LIB、INCLUDE 的原值,拼接由 runtime 用 `;` 完成,所以子进程原值里的字符
不会让该 crate 的 `join_paths` 在 harness 内 panic。建筑声明了 `VCINSTALLDIR`
即已是 developer prompt 环境,不再添加;主机没有 MSVC 时什么也不加。这些目录都在
Program Files 下,平台已给 ALL APPLICATION PACKAGES 读与执行,无须 ACL 授权,
名字与值也不含凭据。用 find-msvc-tools 而非自写发现,是因为它就是不受限的 rustc
会用的那一份逻辑,于是两种臂找到同一个实例;它已作为 cc 的依赖在 lockfile 里。
被否:①给本次 SID 授 `%ProgramData%\Microsoft\VisualStudio` 的读 ACE——要该目录的
WRITE_DAC,普通账户没有,且与 D59 一样把主机差异藏进每次起动;②要求建筑声明
vcvars 的变量——harness 进程通常不在 developer prompt 里,声明的名字取不到值;
③设 `CARGO_TARGET_<triple>_LINKER`——只覆盖 cargo 的链接,cc 的 `cl.exe` 仍找不到。
限制:前置的是 host 架构的目录,子进程交叉编译到别的架构时须自己声明 developer
prompt 变量。重开条件:VS 的实例状态对 AppContainer 可读,或 find-msvc-tools 在
AppContainer 内能发现实例。`msvc` 模块的测试在装有 MSVC 的 Windows runner 上检查
别名合并与前置顺序;`exec_builds` 的 native acceptance 检查实际构建。

失败拒绝,不再起动普通子进程;cleanup 拒绝时保留错误及未释放资源的 owner,
模型的 closing 只表示不能恢复执行,closed 才表示 cleanup 已成功。
launch failure 的未释放资源随 typed Failure 返回,由 run Job 保留,后续 native
准入先重试清理;release 时仍由这个 owner 执行最后的 teardown,并报告拒绝。创建后任何失败均终止挂起进程、关闭句柄和
删除 profile;正常结束、取消与 owner 释放均先终止 Job 中剩余进程,再删除 profile。
终止、root wait 或 Job tree wait 拒绝时,process/job 句柄、security descriptor 与 profile
保持在同一 owner,后续重试重新验证;只有肯定退出后才关闭句柄、恢复权限与删除 profile。
读取退出状态或清理失败保留操作与 OS code,并由 runtime 转成 E_SANDBOX_DENIED;
Backlog 的 settle/harvest 遇 native poll/cleanup 拒绝时返回该错误并保留 member,
下一次 harvest 由同一 owner 重试,不能借用 host 的 Unknown ending 删除资源。

下面模型量化所有平台回答组成的 trace;Win32 按文档实现悬挂、job 继承、ACL
与 capability 检查属于环境假设,定理不证明操作系统。Rust-reference/leaf packet
等价与两侧 fuzz 验证边界;Windows disposable acceptance 验证实际五轴与清理。
权威文档:Microsoft CreateAppContainerProfile、SECURITY_CAPABILITIES、CreateProcessW、
UpdateProcThreadAttribute、AssignProcessToJobObject、Job Objects、SetNamedSecurityInfoW。
-/
namespace Runtime.NativeWindows

inductive Phase where
  | empty | profile | confined | suspended | assigned | running | closing | closed
  deriving DecidableEq, Repr

inductive Answer where
  | profile | limitsAndAcl | createSuspended | assign | resume | close | failure
  deriving DecidableEq, Repr

def step : Phase → Answer → Phase
  | .closed, _ => .closed
  | .closing, .close => .closed
  | .closing, _ => .closing
  | .empty, .profile => .profile
  | .profile, .limitsAndAcl => .confined
  | .confined, .createSuspended => .suspended
  | .suspended, .assign => .assigned
  | .assigned, .resume => .running
  | _, .close => .closed
  | _, .failure => .closing
  | state, _ => state

def run (state : Phase) : List Answer → Phase
  | [] => state
  | answer :: rest => run (step state answer) rest

/-- 唯一能开始执行的 transition 来自已装进 job 的挂起进程。 -/
theorem resume_requires_assignment (state : Phase) (answer : Answer)
    (before : state ≠ .running) (after : step state answer = .running) :
    state = .assigned ∧ answer = .resume := by
  cases state <;> cases answer <;> simp_all [step]

/-- 正常路可达;拒绝所有操作不构成兑现保障。 -/
theorem launch_is_reachable :
    run .empty [.profile, .limitsAndAcl, .createSuspended, .assign, .resume] = .running := rfl

/-- 已清理的 owner 不能被任意后续回答重新执行。 -/
theorem closed_is_terminal (answers : List Answer) : run .closed answers = .closed := by
  induction answers with
  | nil => rfl
  | cons answer rest ih =>
    cases answer <;> simp [run, step, ih]

/-- 未成功清理的资源仍有 owner,不能通过后续回答再次恢复执行。 -/
theorem closing_cannot_resume (answers : List Answer) :
    run .closing answers = .closing ∨ run .closing answers = .closed := by
  induction answers with
  | nil => simp [run]
  | cons answer rest ih =>
    cases answer <;> simp [run, step, closed_is_terminal, ih]

/-- 失败停止起动;OS 拒绝清理时保持 closing,而不是宣称资源已经释放。 -/
theorem failure_cannot_fall_back (state : Phase) (rest : List Answer) :
    run state (.failure :: rest) ≠ .running := by
  have stopped := closing_cannot_resume rest
  have neverRunning : run .closing rest ≠ .running := by
    rcases stopped with retained | removed
    · simp [retained]
    · simp [removed]
  cases state <;> simp_all [run, step, closed_is_terminal]

structure Readiness where
  runAssigned : Bool
  commandAssigned : Bool
  identityVerified : Bool

def mayResume (ready : Readiness) : Bool :=
  ready.runAssigned && ready.commandAssigned && ready.identityVerified

/-- 恢复前的实际叶子守卫要求两次 job membership 与独立 AppContainer 身份皆已读回。 -/
theorem resume_requires_all_guards (ready : Readiness) (accepted : mayResume ready = true) :
    ready.runAssigned = true ∧ ready.commandAssigned = true ∧ ready.identityVerified = true := by
  cases ready with
  | mk run command identity =>
    cases run <;> cases command <;> cases identity <;> simp_all [mayResume]

/-- 每个 string span 必须在借来的 packet 内,尾部 NUL 不进入其内容。 -/
def terminated (units : List Nat) : Bool := !units.isEmpty && units.getLast? == some 0

def packetValid (units : List Nat) : Bool :=
  terminated units && terminated (units.dropLast)

theorem accepted_packet_has_two_terminators (units : List Nat)
    (accepted : packetValid units = true) :
    terminated units = true ∧ terminated units.dropLast = true := by
  simpa [packetValid, Bool.and_eq_true] using accepted

/-- 清理的每一轮必须重新得到终止与整棵 Job 退出的肯定回答;失败不消耗资源所有权。 -/
structure CleanupOwner where
  process : Bool
  job : Bool
  security : Bool
  profile : Bool
  deriving DecidableEq, Repr

inductive CleanupAnswer where
  | refused | timedOut | stopped
  deriving DecidableEq, Repr

def retainUntilStopped (owner : CleanupOwner) : CleanupAnswer → CleanupOwner
  | .refused => owner
  | .timedOut => owner
  | .stopped => ⟨false, false, false, false⟩

/-- 任意次拒绝或超时的 trace 都保留同一 owner;重试不能因丢失句柄伪报成功。 -/
theorem cleanup_failure_trace_preserves_owner (owner : CleanupOwner)
    (answers : List CleanupAnswer)
    (failed : ∀ answer ∈ answers, answer ≠ .stopped) :
    answers.foldl retainUntilStopped owner = owner := by
  induction answers generalizing owner with
  | nil => rfl
  | cons answer rest ih =>
    have notStopped := failed answer (by simp)
    have remaining : ∀ answer ∈ rest, answer ≠ .stopped := by
      intro answer member
      exact failed answer (by simp [member])
    cases answer <;> simp_all [retainUntilStopped]

/-- 每次 profile 的 SID 是唯一授权键;集合模型独立于 Win32 ACL 的布局。 -/
def revokeGrant (sid : Nat) (grants : List Nat) : List Nat :=
  grants.filter (· != sid)

/-- 清理不再授予自己的 SID,任意重复项都被移除。 -/
theorem revoked_sid_is_absent (sid : Nat) (grants : List Nat) :
    sid ∉ revokeGrant sid grants := by
  simp [revokeGrant]

/-- 并发 owner 的授权在另一 owner 的清理后仍存在。 -/
theorem revoke_preserves_other_owner (sid other : Nat) (grants : List Nat)
    (different : other ≠ sid) :
    other ∈ revokeGrant sid grants ↔ other ∈ grants := by
  simp [revokeGrant, different]

/-! ## argv 保全(D55)

D55:`crates/desktop/ffi/src/confinement/packet.rs` 的 `quoted` 是唯一生产编码器;
这里规定保全性质,runtime 与 Zig 不再编码 argv。微软 CRT 对普通参数的
反斜杠/引号规则规定 `CrtTail`,程序名的独立规则规定 `CrtProgram`;
Microsoft [Parsing C command-line arguments](https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments)
是两者的环境前提,操作系统与 CRT 实现该规则不是本模型证明的事。

支持域为非零 UTF16 units,包含空参数与孤立 surrogate;程序名另外要求是
非空文件路径,不含引号且末尾不是反斜杠,允许路径中有空格。`quoteChecked`
只规定 NUL 内容拒绝,生产 `packet::encode` 还可因 checked arithmetic、
command line 或 environment 长度、profile 与限额验证而答 `Action::Encode`。
保全性质以编码成功为条件,不保证任意大小输入都能启动。

`encoded_tail_preserves_units` 与 `encoded_arguments_preserve_order` 量化所有
admitted 输入;它们证明 parser relation 接受编码结果,未证明 parser 的
确定性。当前 Rust 派生检查调用生产 `packet::encode`,再用独立 CRT decoder
检查 UTF16 units 与参数顺序;disposable acceptance 的
native_windows_disposable_argv_and_unrequested_memory 另编译独立 Rust child,
由 native Backlog 经过生产 packet 起动,让 child 以 args_os/encode_wide 读回
包含空参数、引号、空格、尾反斜杠与孤立 surrogate 的实际 argv。
同一 child 在 Shares::Unset 与 Shares::Cpu 下分配并写入 320 MiB 内存;资源轴对拍另明确给定 Shares::CpuAndMemory,不能依赖默认额度。
有限 child 对拍验证实现与模型之间的环境假设,不构成对 CRT 的形式证明。

D56:packet 按已解析程序的文件名区分 cmd.exe/cmd 与 CRT 程序,比较不分 ASCII 大小写。
cmd 的开关不加 CRT 引号,/C 或 /K 后的各项按单个空格拼成脚本文本;
它们是 shell source,不承诺 argv 保全。编码器在原开关前加 /D /S,禁用
AutoRun,并使 cmd 按 /S 规则去掉脚本文本外的一对引号。脚本内容原样保留,
不把其中的引号变成反斜杠引号;NUL 与长度超界仍在启动前拒绝。
调用方若需把一个值当作 shell literal,应按 cmd 语法在 source 内写出它,
不能把 CRT quoting 当成 cmd escaping。没有 /C 或 /K 时只原样拼接开关。
`CmdSource` 模型以 Microsoft 的 /S 外层引号删除为环境假设,证明包装不改
source;生产检查经 ExecTool 执行 exit、环境读取、文件写入与 accounting 的 type。
Microsoft
[cmd](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd)
规定这些开关的脚本行为。击败的备选是在 runtime 或 Zig 再写编码器,
因为调用链只需传递 packet,复制规则会产生第二权威;重开条件是调用方
需要另一种明确的解析语法,此时该语法应有自己的契约。
-/
namespace Argv

/-- quoted 的输入是 UTF16 code units;surrogate 不做归一化,NUL 不属于 admitted。 -/
def admitted (units : List Nat) : Prop :=
  ∀ unit ∈ units, 0 < unit ∧ unit < 65536

/-- 结束引号后必须是参数边界;相邻引号还在当前参数内,不能无条件结束。 -/
def argumentBoundary (suffix : List Nat) : Prop :=
  suffix = [] ∨ suffix.head? = some 32 ∨ suffix.head? = some 9

/-- 待决反斜杠只在后继已知时输出,对应 packet::quoted 的 slashes。 -/
def encodeTail (slashes : Nat) : List Nat → List Nat → List Nat
  | [], suffix => List.replicate (2 * slashes) 92 ++ 34 :: suffix
  | unit :: rest, suffix =>
    if unit = 92 then encodeTail (slashes + 1) rest suffix
    else if unit = 34 then
      List.replicate (2 * slashes + 1) 92 ++ 34 :: encodeTail 0 rest suffix
    else List.replicate slashes 92 ++ unit :: encodeTail 0 rest suffix

/-- CRT 已打开引号内的规则;suffix 在结束引号之后,因而覆盖边界。
规则按 Microsoft 文档独立规定,不以 encodeTail 的结果定义解析成功。 -/
inductive CrtTail : List Nat → List Nat → List Nat → Prop where
  | close (slashes : Nat) (suffix : List Nat) (boundary : argumentBoundary suffix) :
      CrtTail (List.replicate (2 * slashes) 92 ++ 34 :: suffix)
        (List.replicate slashes 92) suffix
  | escaped (slashes : Nat) (input output suffix : List Nat)
      (next : CrtTail input output suffix) :
      CrtTail (List.replicate (2 * slashes + 1) 92 ++ 34 :: input)
        (List.replicate slashes 92 ++ 34 :: output) suffix
  | literal (slashes unit : Nat) (input output suffix : List Nat)
      (notSlash : unit ≠ 92) (notQuote : unit ≠ 34) (notNul : unit ≠ 0)
      (next : CrtTail input output suffix) :
      CrtTail (List.replicate slashes 92 ++ unit :: input)
        (List.replicate slashes 92 ++ unit :: output) suffix

/-- 循环不变量量化所有 admitted 参数、所有待决反斜杠数量与以参数边界开头的 suffix。 -/
theorem encoded_tail_preserves_units (units : List Nat) (slashes : Nat)
    (suffix : List Nat) (valid : admitted units) (boundary : argumentBoundary suffix) :
    CrtTail (encodeTail slashes units suffix)
      (List.replicate slashes 92 ++ units) suffix := by
  induction units generalizing slashes with
  | nil => simpa [encodeTail] using CrtTail.close slashes suffix boundary
  | cons unit rest ih =>
    have restValid : admitted rest := by
      intro value member
      exact valid value (List.mem_cons_of_mem unit member)
    have notNul : unit ≠ 0 := by
      have positive := (valid unit (by simp)).1
      omega
    by_cases slash : unit = 92
    · subst unit
      simpa [encodeTail, List.replicate_succ', List.append_assoc] using
        ih (slashes + 1) restValid
    · by_cases quote : unit = 34
      · subst unit
        simpa [encodeTail] using
          CrtTail.escaped slashes (encodeTail 0 rest suffix) rest suffix
            (by simpa using ih 0 restValid)
      · simpa [encodeTail, slash, quote] using
          CrtTail.literal slashes unit (encodeTail 0 rest suffix) rest suffix
            slash quote notNul (by simpa using ih 0 restValid)

/-- argv 尾部只有加引号的参数与单空格边界。 -/
def encodeArguments : List (List Nat) → List Nat
  | [] => []
  | units :: rest => 34 :: encodeTail 0 units
      (if rest.isEmpty then [] else 32 :: encodeArguments rest)

inductive CrtArguments : List Nat → List (List Nat) → Prop where
  | empty : CrtArguments [] []
  | next (input units suffix : List Nat) (rest : List (List Nat))
      (argument : CrtTail input units
        (if rest.isEmpty then [] else 32 :: suffix))
      (remaining : CrtArguments suffix rest) :
      CrtArguments (34 :: input) (units :: rest)

/-- 任意长度 argv[1..] 保留所有 units 与次序,包含空参数;不规定 cmd 脚本解释。 -/
theorem encoded_arguments_preserve_order (args : List (List Nat))
    (valid : ∀ units ∈ args, admitted units) :
    CrtArguments (encodeArguments args) args := by
  induction args with
  | nil => exact CrtArguments.empty
  | cons units rest ih =>
    apply CrtArguments.next
    · simpa using encoded_tail_preserves_units units 0
        (if rest.isEmpty then [] else 32 :: encodeArguments rest)
        (valid units (by simp)) (by
          cases rest.isEmpty <;> simp [argumentBoundary])
    · exact ih (by
        intro units member
        exact valid units (List.mem_cons_of_mem _ member))

/-- 内容检查拒绝 NUL;Nat 模型不包含生产 checked arithmetic 或 packet 长度拒绝。 -/
def quoteChecked (units : List Nat) : Option (List Nat) :=
  if 0 ∈ units then none else some (34 :: encodeTail 0 units [])

theorem nul_is_exactly_the_content_refusal (units : List Nat) :
    quoteChecked units = none ↔ 0 ∈ units := by
  simp [quoteChecked]

/-- 无引号的文件路径使用 CRT 独立的程序名规则;末尾反斜杠是目录,不在该支持域。 -/
theorem program_body_preserves_path (pathFront : List Nat) (last slashes : Nat)
    (suffix : List Nat) (noQuotes : 34 ∉ pathFront)
    (lastNotQuote : last ≠ 34) (lastNotSlash : last ≠ 92) :
    encodeTail slashes (pathFront ++ [last]) suffix =
      List.replicate slashes 92 ++ pathFront ++ last :: 34 :: suffix := by
  induction pathFront generalizing slashes with
  | nil => simp [encodeTail, lastNotQuote, lastNotSlash]
  | cons unit rest ih =>
    have unitNotQuote : unit ≠ 34 := by
      intro equal
      exact noQuotes (by simp [equal])
    have restNoQuotes : 34 ∉ rest := by
      intro member
      exact noQuotes (List.mem_cons_of_mem unit member)
    by_cases slash : unit = 92
    · subst unit
      simpa [encodeTail, List.replicate_succ', List.append_assoc] using
        ih (slashes + 1) restNoQuotes
    · simp [encodeTail, slash, unitNotQuote, ih 0 restNoQuotes, List.append_assoc]

inductive CrtProgram : List Nat → List Nat → List Nat → Prop where
  | quoted (program suffix : List Nat) (valid : admitted program)
      (noQuotes : 34 ∉ program) (boundary : argumentBoundary suffix) :
      CrtProgram (34 :: (program ++ 34 :: suffix)) program suffix

theorem encoded_program_preserves_path (pathFront : List Nat) (last : Nat)
    (suffix : List Nat) (noQuotes : 34 ∉ pathFront)
    (lastNotQuote : last ≠ 34) (lastNotSlash : last ≠ 92)
    (valid : admitted (pathFront ++ [last])) (boundary : argumentBoundary suffix) :
    CrtProgram (34 :: encodeTail 0 (pathFront ++ [last]) suffix)
      (pathFront ++ [last]) suffix := by
  rw [program_body_preserves_path pathFront last 0 suffix noQuotes lastNotQuote lastNotSlash]
  simpa [List.append_assoc] using
    CrtProgram.quoted (pathFront ++ [last]) suffix valid
      (by simp [noQuotes, Ne.symm lastNotQuote]) boundary

/-- cmd /S 的 source 边界;内部文字不经过 CRT 转义。 -/
def cmdSource (source : List Nat) : List Nat := 34 :: (source ++ [34])

/-- Microsoft /S 删除首尾引号,余下是同一段 source。 -/
def stripCmdSource (line : List Nat) : List Nat := line.drop 1 |>.dropLast

/-- 对全部 source(含引号、反斜杠与空格)保留每个单元与次序。 -/
theorem cmd_source_preserves_units (source : List Nat) :
    stripCmdSource (cmdSource source) = source := by
  simp [stripCmdSource, cmdSource]

end Argv

end Runtime.NativeWindows