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
// boot -- what build_rs.py builds into every program of an app, set up before the program's
// own code runs (docs/PERMISSIONS.md, docs/VFS.md)
//
// start(&rt::boot::Boot) -> Result<(), exit code> console programs (rt::main!): the
// process entry registers it with rt::boot; name, app://, manifest
// name_and_assets(app, assets) -> Result<(), i32> vfs::set_app_id, the JFA1 bundle as app://
// permissions(manifest) -> Result<Option<Key>, i32> install the manifest; with fs.user the
// paths on the command line are the user's (read + write, folders
// with everything below; a local file:// URI as its path); without
// a manifest the process's key (policy::take_key: it answers the
// questions of agent actions). A `sandbox` line then hands the
// manifest to the OS as well (sandbox::apply)
//
// ui::main! calls name_and_assets and permissions itself (ui::boot, with the user's languages
// in between) and keeps the key; rt::main! programs drop it, so their code cannot widen the
// rules. A refused bundle or manifest is reported on stderr: exit code 2.
use alloc::sync::Arc;
use crate::policy::{self, Key, FS_WRITE};
use crate::vfs;
/// (rt::main!, through rt::boot) set a console program up: name, assets, permissions
pub fn start(b: &rt::boot::Boot) -> Result<(), i32> {
name_and_assets(b.app, b.assets)?;
permissions(b.manifest).map(|_| ())
}
/// the app's name for its folders and its assets as app:// (Err: a damaged bundle)
#[doc(hidden)]
pub fn name_and_assets(app: Option<&str>, assets: &'static [u8]) -> Result<(), i32> {
if let Some(id) = app {
vfs::set_app_id(id);
}
if !assets.is_empty() {
let Some(b) = vfs::Bundle::from_static(assets) else {
crate::write_stderr(b"error: the built-in assets (app://) are damaged\n");
return Err(2);
};
vfs::mount("app", Arc::new(b));
}
Ok(())
}
/// install the app's manifest (None: the app has none, everything stays allowed) and grant
/// the command line's paths; Some(key) for the caller to keep or drop (also without a
/// manifest: policy::take_key)
#[doc(hidden)]
pub fn permissions(manifest: Option<&str>) -> Result<Option<Key>, i32> {
let Some(text) = manifest else { return Ok(policy::take_key()) };
let key = match policy::install(text) {
Ok(k) => k,
Err(e) => {
crate::write_stderr(alloc::format!("error: {}\n", e).as_bytes());
return Err(2);
}
};
for a in crate::args().iter().skip(1) {
// "--name=value": the value; other options are no paths
let v = match a.strip_prefix("--") {
Some(o) => match o.split_once('=') {
Some((_, v)) => v,
None => continue,
},
None => a.as_str(),
};
if !v.is_empty() && !v.starts_with('-') {
// a local file:// URI (Exec=... %U of a .desktop file) is the file it names
match crate::assoc::file_uri_path(v) {
Some(p) => grant_user(&key, &p),
None => grant_user(&key, v),
}
}
}
// the OS sandbox (a `sandbox` line in the manifest): after the command-line grants, so the
// kernel also allows the user's files; before the app's own code and any worker thread.
// Not for a one-shot that only registers the app's file types and exits (it writes the
// user's config folders, which the sandbox does not grant; no app code runs).
let registering = crate::args().iter().skip(1).any(|a| matches!(a.as_str(), "--register" | "--register-replace" | "--unregister"));
if !registering {
crate::sandbox::apply();
}
Ok(Some(key))
}
/// a path the user handed over: a folder with everything below, else the file (with fs.user)
#[doc(hidden)]
pub fn grant_user(key: &Key, path: &str) {
if key.trusted(|| crate::file_exists(path)) == 2 {
key.grant_dir(path, FS_WRITE);
} else {
key.grant(path, FS_WRITE);
}
}