Skip to content

Commit 2ab50d6

Browse files
hyperpolymathclaude
andcommitted
v-ecosystem: receive v-ambientops-emergency-room V source
Arrives from systems-ecosystem/ambientops/emergency-room/src/ after the Zig port landed. Panic-safe intake + offline-first recovery framework (~2762 LOC, 11 V files). Packaged for donation to the V community. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 728ea82 commit 2ab50d6

12 files changed

Lines changed: 2769 additions & 0 deletions

File tree

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Quick backup functionality with preview mode
3+
// CRIT-002 fix: Path validation to prevent command injection
4+
5+
module main
6+
7+
import os
8+
import time
9+
10+
// Shell metacharacters that could enable injection attacks
11+
const shell_dangerous_chars = [';', '|', '&', r'$', '`', '(', ')', '{', '}', '[', ']', '<', '>', '\n', '\r', '*', '?', '~', '!', '#']
12+
13+
// Validate a path is safe for shell interpolation
14+
// Returns error if path contains dangerous characters
15+
fn validate_safe_path(path string) !string {
16+
if path.len == 0 {
17+
return error('Empty path')
18+
}
19+
20+
// Check for shell metacharacters
21+
for c in shell_dangerous_chars {
22+
if path.contains(c) {
23+
return error('Path contains dangerous character: ${c}')
24+
}
25+
}
26+
27+
// Check for path traversal attempts beyond expected scope
28+
normalized := os.norm_path(path)
29+
if normalized.contains('..') && !path.starts_with(os.home_dir()) {
30+
return error('Path traversal not allowed outside home directory')
31+
}
32+
33+
return normalized
34+
}
35+
36+
struct BackupPlan {
37+
source_dirs []string
38+
dest_path string
39+
mut:
40+
total_files int
41+
total_size i64
42+
items []BackupItem
43+
}
44+
45+
struct BackupItem {
46+
path string
47+
size i64
48+
is_dir bool
49+
will_backup bool
50+
reason string
51+
}
52+
53+
fn run_quick_backup(incident Incident, config Config) {
54+
dest := config.quick_backup_dest
55+
56+
// Validate destination
57+
if !os.exists(dest) {
58+
eprintln('${c_red}[ERROR]${c_reset} Backup destination does not exist: ${dest}')
59+
eprintln('${c_blue}[INFO]${c_reset} Please create the directory first or mount the drive.')
60+
log_error(incident.logs_path, 'backup', 'Backup destination does not exist', {
61+
'destination': dest
62+
})
63+
return
64+
}
65+
66+
if !os.is_dir(dest) {
67+
eprintln('${c_red}[ERROR]${c_reset} Backup destination is not a directory: ${dest}')
68+
log_error(incident.logs_path, 'backup', 'Backup destination is not a directory', {
69+
'destination': dest
70+
})
71+
return
72+
}
73+
74+
// Default source directories for quick backup
75+
home := os.home_dir()
76+
source_dirs := [
77+
os.join_path(home, 'Documents'),
78+
os.join_path(home, 'Desktop'),
79+
os.join_path(home, '.ssh'),
80+
os.join_path(home, '.gnupg'),
81+
os.join_path(home, '.config'),
82+
]
83+
84+
println('')
85+
println('${c_blue}━━━ Quick Backup Preview ━━━${c_reset}')
86+
println('')
87+
88+
// Create backup plan
89+
plan := create_backup_plan(source_dirs, dest)
90+
91+
println('Source directories:')
92+
for dir in source_dirs {
93+
if os.exists(dir) {
94+
println(' ${c_green}✓${c_reset} ${dir}')
95+
} else {
96+
println(' ${c_yellow}○${c_reset} ${dir} (not found)')
97+
}
98+
}
99+
println('')
100+
println('Destination: ${dest}')
101+
println('')
102+
println('Summary:')
103+
println(' Files to backup: ${plan.total_files}')
104+
println(' Estimated size: ${format_size(plan.total_size)}')
105+
println('')
106+
107+
// Log the backup plan
108+
log_backup_plan(incident, plan, config)
109+
110+
if config.dry_run {
111+
println('${c_cyan}[DRY-RUN]${c_reset} Would perform backup of ${plan.total_files} files')
112+
println('${c_cyan}[DRY-RUN]${c_reset} Backup log written to incident bundle')
113+
return
114+
}
115+
116+
// Actually perform backup
117+
println('${c_blue}[INFO]${c_reset} Starting backup...')
118+
perform_backup(plan, incident, config)
119+
}
120+
121+
fn create_backup_plan(source_dirs []string, dest string) BackupPlan {
122+
mut plan := BackupPlan{
123+
source_dirs: source_dirs
124+
dest_path: dest
125+
items: []
126+
}
127+
128+
for dir in source_dirs {
129+
if !os.exists(dir) {
130+
continue
131+
}
132+
133+
// Walk directory and count files
134+
scan_directory(dir, mut plan)
135+
}
136+
137+
return plan
138+
}
139+
140+
fn scan_directory(path string, mut plan BackupPlan) {
141+
entries := os.ls(path) or { return }
142+
143+
for entry in entries {
144+
full_path := os.join_path(path, entry)
145+
146+
// Skip hidden git directories to save space
147+
if entry == '.git' {
148+
continue
149+
}
150+
151+
if os.is_dir(full_path) {
152+
// Recurse into subdirectories (with depth limit)
153+
depth := full_path.count(os.path_separator)
154+
if depth < 10 {
155+
scan_directory(full_path, mut plan)
156+
}
157+
} else {
158+
size := os.file_size(full_path)
159+
plan.total_files++
160+
plan.total_size += size
161+
162+
plan.items << BackupItem{
163+
path: full_path
164+
size: size
165+
is_dir: false
166+
will_backup: true
167+
}
168+
}
169+
}
170+
}
171+
172+
fn perform_backup(plan BackupPlan, incident Incident, config Config) {
173+
timestamp := time.now().custom_format('YYYYMMDD-HHmmss')
174+
175+
// CRIT-002 fix: Validate destination path before use
176+
safe_dest := validate_safe_path(plan.dest_path) or {
177+
eprintln('${c_red}[ERROR]${c_reset} Invalid backup destination path: ${err}')
178+
log_error(incident.logs_path, 'backup', 'Invalid backup destination path', {
179+
'path': plan.dest_path
180+
'error': err.str()
181+
})
182+
return
183+
}
184+
185+
backup_dir := os.join_path(safe_dest, 'emergency-backup-${timestamp}')
186+
187+
os.mkdir_all(backup_dir) or {
188+
eprintln('${c_red}[ERROR]${c_reset} Failed to create backup directory: ${err}')
189+
log_error(incident.logs_path, 'backup', 'Failed to create backup directory', {
190+
'directory': backup_dir
191+
'error': err.str()
192+
})
193+
return
194+
}
195+
196+
mut copied := 0
197+
mut failed := 0
198+
199+
for dir in plan.source_dirs {
200+
if !os.exists(dir) {
201+
continue
202+
}
203+
204+
// CRIT-002 fix: Validate source path before shell interpolation
205+
safe_source := validate_safe_path(dir) or {
206+
eprintln('${c_yellow}[WARN]${c_reset} Skipping unsafe path: ${dir}')
207+
log_warn(incident.logs_path, 'backup', 'Skipping unsafe source path: ${dir}')
208+
failed++
209+
continue
210+
}
211+
212+
dir_name := os.base(safe_source)
213+
dest_dir := os.join_path(backup_dir, dir_name)
214+
215+
// Validate constructed destination path
216+
safe_dest_dir := validate_safe_path(dest_dir) or {
217+
eprintln('${c_yellow}[WARN]${c_reset} Skipping invalid destination: ${dest_dir}')
218+
log_warn(incident.logs_path, 'backup', 'Skipping invalid destination: ${dest_dir}')
219+
failed++
220+
continue
221+
}
222+
223+
// Use system copy for efficiency - paths are now validated
224+
$if windows {
225+
result := os.execute('xcopy /E /I /H /Y "${safe_source}" "${safe_dest_dir}"')
226+
if result.exit_code == 0 {
227+
copied++
228+
println(' ${c_green}✓${c_reset} ${dir_name}')
229+
} else {
230+
failed++
231+
println(' ${c_red}✗${c_reset} ${dir_name}')
232+
}
233+
} $else {
234+
result := os.execute('cp -r "${safe_source}" "${safe_dest_dir}" 2>/dev/null')
235+
if result.exit_code == 0 {
236+
copied++
237+
println(' ${c_green}✓${c_reset} ${dir_name}')
238+
} else {
239+
failed++
240+
println(' ${c_red}✗${c_reset} ${dir_name}')
241+
}
242+
}
243+
}
244+
245+
println('')
246+
if failed == 0 {
247+
println('${c_green}[OK]${c_reset} Backup complete: ${backup_dir}')
248+
} else {
249+
println('${c_yellow}[WARN]${c_reset} Backup completed with ${failed} errors')
250+
}
251+
252+
// Log backup result
253+
log_backup_result(incident, backup_dir, copied, failed)
254+
}
255+
256+
fn log_backup_plan(incident Incident, plan BackupPlan, config Config) {
257+
log_path := os.join_path(incident.logs_path, 'backup_plan.log')
258+
259+
mut lines := []string{}
260+
lines << 'schema_version: ${schema_version}'
261+
lines << ''
262+
lines << 'Quick Backup Plan'
263+
lines << '================='
264+
lines << ''
265+
lines << 'Destination: ${plan.dest_path}'
266+
lines << 'Total files: ${plan.total_files}'
267+
lines << 'Total size: ${format_size(plan.total_size)}'
268+
lines << ''
269+
lines << 'Source directories:'
270+
for dir in plan.source_dirs {
271+
exists := if os.exists(dir) { 'exists' } else { 'not found' }
272+
lines << ' - ${dir} (${exists})'
273+
}
274+
lines << ''
275+
lines << 'Dry run: ${config.dry_run}'
276+
277+
if config.dry_run {
278+
println('${c_cyan}[DRY-RUN]${c_reset} Would write backup plan to logs')
279+
return
280+
}
281+
282+
// HIGH-006: Use atomic write to prevent corruption
283+
atomic_write_file(log_path, lines.join('\n')) or {
284+
eprintln('${c_yellow}[WARN]${c_reset} Could not write backup plan: ${err}')
285+
}
286+
}
287+
288+
fn log_backup_result(incident Incident, backup_dir string, copied int, failed int) {
289+
log_path := os.join_path(incident.logs_path, 'backup_result.log')
290+
291+
content := 'schema_version: ${schema_version}
292+
293+
Backup Result
294+
=============
295+
296+
Backup directory: ${backup_dir}
297+
Directories copied: ${copied}
298+
Directories failed: ${failed}
299+
Status: ${if failed == 0 { 'SUCCESS' } else { 'PARTIAL' }}
300+
'
301+
302+
// HIGH-006: Use atomic write to prevent corruption
303+
atomic_write_file(log_path, content) or {
304+
eprintln('${c_yellow}[WARN]${c_reset} Could not write backup result: ${err}')
305+
}
306+
}
307+
308+
fn format_size(bytes i64) string {
309+
if bytes >= 1073741824 {
310+
return '${f64(bytes) / 1073741824.0:.1}G'
311+
} else if bytes >= 1048576 {
312+
return '${f64(bytes) / 1048576.0:.1}M'
313+
} else if bytes >= 1024 {
314+
return '${f64(bytes) / 1024.0:.1}K'
315+
}
316+
return '${bytes}B'
317+
}

0 commit comments

Comments
 (0)