shrub_core/snapshot.rs
1//! A copy of a shell's state that a fresh shell, in another process or another
2//! build of shrub, can take on.
3//!
4//! [`Shell::snapshot`](crate::Shell::snapshot) copies the state a command can
5//! change: the working directory, variables, functions, aliases, traps,
6//! options, disabled builtins, the directory stack, the positional parameters
7//! and `$?`. [`Shell::restore_snapshot`](crate::Shell::restore_snapshot)
8//! applies a copy to another shell through the same API a builtin uses, so
9//! nothing in a snapshot is run as a command. A function is kept as its
10//! source text, the one item with no other form that every build reads the
11//! same way.
12//!
13//! The types are plain data with no serialization of their own, so the
14//! embedder chooses how a snapshot is stored. A snapshot holds whatever values
15//! the shell held, which can include secrets a command put there; an embedder
16//! that stores one keeps that in mind, and [`SnapshotOptions::exclude`] keeps
17//! named variables out of it.
18//!
19//! What a snapshot cannot carry is listed in [`ShellSnapshot::not_kept`]:
20//! background jobs, open file descriptors above 2, and functions whose source
21//! does not parse back to the same definition, such as one holding a
22//! here-document, whose body shrub prints right after its operator. A fresh
23//! shell rebuilds the command hash table, completion specifications and
24//! history itself, and `OLDPWD`, `RANDOM`'s seed and `SECONDS`' offset are not
25//! carried.
26
27use std::collections::{BTreeMap, BTreeSet};
28use std::path::PathBuf;
29
30use crate::variables::{ShellValueUnsetType, ShellVariableUpdateTransform};
31
32/// What [`Shell::snapshot`](crate::Shell::snapshot) leaves out.
33#[derive(Clone, Debug, Default)]
34pub struct SnapshotOptions {
35 /// Exported values the embedder gives every new shell itself. A variable
36 /// that still holds exactly its baseline value, exported and with no
37 /// other attribute, is left out, and a baseline name the shell no longer
38 /// has goes into [`ShellSnapshot::unset_variables`].
39 pub baseline: BTreeMap<String, String>,
40 /// Variable names never copied, whatever their value.
41 pub exclude: BTreeSet<String>,
42}
43
44/// A shell's state between two commands.
45#[derive(Clone, Debug, Default, PartialEq, Eq)]
46pub struct ShellSnapshot {
47 /// The working directory.
48 pub working_dir: PathBuf,
49 /// The directory stack of `pushd` and `popd`, without the working
50 /// directory.
51 pub directory_stack: Vec<PathBuf>,
52 /// `$?`.
53 pub last_exit_status: u8,
54 /// `$1` and on.
55 pub positional_parameters: Vec<String>,
56 /// Global variables, by name.
57 pub variables: BTreeMap<String, SavedVariable>,
58 /// Baseline names the shell no longer has.
59 pub unset_variables: BTreeSet<String>,
60 /// Functions, by name.
61 pub functions: BTreeMap<String, SavedFunction>,
62 /// Aliases: name to value.
63 pub aliases: BTreeMap<String, String>,
64 /// Trap handlers: signal name, as `trap -p` prints it, to command text.
65 pub traps: BTreeMap<String, String>,
66 /// `set -o` options, by name.
67 pub set_options: BTreeMap<String, bool>,
68 /// `shopt` options, by name.
69 pub shopt_options: BTreeMap<String, bool>,
70 /// Builtins turned off with `enable -n`.
71 pub disabled_builtins: BTreeSet<String>,
72 /// State the snapshot could not copy.
73 pub not_kept: Vec<NotKept>,
74}
75
76/// A variable and its attributes.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct SavedVariable {
79 /// The value.
80 pub value: SavedValue,
81 /// `declare -x`.
82 pub exported: bool,
83 /// `declare -r`.
84 pub readonly: bool,
85 /// `declare -i`.
86 pub integer: bool,
87 /// `declare -n`; the value is the name it refers to.
88 pub nameref: bool,
89 /// `declare -t`.
90 pub trace: bool,
91 /// `declare -l`, `-u` or `-c`; the value is already transformed.
92 pub transform: ShellVariableUpdateTransform,
93}
94
95/// A variable's value.
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub enum SavedValue {
98 /// Declared with no value, such as `declare -a name` or `export name`.
99 Unset(ShellValueUnsetType),
100 /// A string.
101 String(String),
102 /// An indexed array.
103 IndexedArray(BTreeMap<u64, String>),
104 /// An associative array.
105 AssociativeArray(BTreeMap<String, String>),
106}
107
108/// A function.
109#[derive(Clone, Debug, PartialEq, Eq)]
110pub struct SavedFunction {
111 /// The definition after its name, starting with `()`, as an exported
112 /// function's `BASH_FUNC_` value holds it.
113 pub body: String,
114 /// `declare -fx`.
115 pub exported: bool,
116}
117
118/// An item a snapshot could not copy.
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct NotKept {
121 /// What kind of item it is.
122 pub kind: NotKeptKind,
123 /// Which one: a job spec and command name such as `%1 sleep`, a
124 /// descriptor number, or a function name. Never a value.
125 pub name: String,
126}
127
128/// The kinds of item a snapshot cannot copy.
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum NotKeptKind {
131 /// A background or stopped job: a process cannot move to another shell.
132 Job,
133 /// A file descriptor above 2, opened with `exec`.
134 OpenFile,
135 /// A function whose source does not parse back to the same definition.
136 Function,
137}
138
139/// What [`Shell::restore_snapshot`](crate::Shell::restore_snapshot) could
140/// not apply. Every other item of the snapshot was applied.
141#[derive(Clone, Debug, Default, PartialEq, Eq)]
142pub struct RestoreReport {
143 /// The items that failed, in the order they were applied.
144 pub failed: Vec<RestoreFailure>,
145}
146
147impl RestoreReport {
148 /// Returns whether every item was applied.
149 pub const fn is_complete(&self) -> bool {
150 self.failed.is_empty()
151 }
152
153 /// Records that the item `name` of `kind` failed to apply.
154 pub(crate) fn fail(&mut self, kind: StateKind, name: &str, reason: RestoreFailureReason) {
155 self.failed.push(RestoreFailure {
156 kind,
157 name: name.to_owned(),
158 reason,
159 });
160 }
161}
162
163/// One item that could not be applied.
164#[derive(Clone, Debug, PartialEq, Eq)]
165pub struct RestoreFailure {
166 /// What kind of item it is.
167 pub kind: StateKind,
168 /// Which one: a path, variable, function, alias, signal, option or
169 /// builtin name. Never a value.
170 pub name: String,
171 /// Why it failed.
172 pub reason: RestoreFailureReason,
173}
174
175/// The kinds of state in a snapshot.
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub enum StateKind {
178 /// The working directory.
179 WorkingDir,
180 /// A variable to set.
181 Variable,
182 /// A variable to unset.
183 UnsetVariable,
184 /// A function.
185 Function,
186 /// A trap handler.
187 Trap,
188 /// A `set -o` option.
189 SetOption,
190 /// A `shopt` option.
191 ShoptOption,
192 /// A builtin to turn off.
193 DisabledBuiltin,
194}
195
196/// Why an item could not be applied. It names no value, so it is safe to
197/// show wherever the item's name is.
198#[derive(Clone, Copy, Debug, PartialEq, Eq)]
199pub enum RestoreFailureReason {
200 /// The directory is gone or is not a directory.
201 NotADirectory,
202 /// The shell already holds a readonly variable of that name with another
203 /// value, or a readonly variable to unset, or a readonly `PWD` or
204 /// `OLDPWD` that changing the working directory must set.
205 Readonly,
206 /// The function's source does not parse in this shell.
207 DoesNotParse,
208 /// This shell knows no signal of that name.
209 UnknownSignal,
210 /// This shell knows no option of that name.
211 UnknownOption,
212 /// This shell has no builtin of that name.
213 UnknownBuiltin,
214}