Skip to main content

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}