Skip to main content

rfc9162_sidecar/
sidecar.rs

1//! `rfc9162-sidecar`: the sidecar from a shell. The sidecar of a project at
2//! `DIR` is the pack at `DIR/.sidecar/pack`; every command opens it, takes
3//! the writer lock for the moment it runs, and prints one line per thing.
4//!
5//! ```text
6//! rfc9162-sidecar init    --dir DIR
7//! rfc9162-sidecar sync    --dir DIR [--skip NAME]... [--meta K=V]...
8//! rfc9162-sidecar snapshot --dir DIR --key KEY.pem --origin ORIGIN
9//! rfc9162-sidecar history --dir DIR PATH
10//! rfc9162-sidecar tree    --dir DIR [SIZE]
11//! rfc9162-sidecar diff    --dir DIR FROM TO
12//! rfc9162-sidecar blob    --dir DIR HEX [--out FILE]
13//! rfc9162-sidecar include --dir DIR PATH --note FILE --trusted KEYLINE... [--meta K=V]...
14//! ```
15//!
16//! Status 0 is done, 2 is a command line this binary does not take, 1 is a
17//! store or a proof that did not answer.
18
19use std::collections::BTreeMap;
20use std::io::{self, Read, Write};
21use std::path::{Path, PathBuf};
22use std::process::ExitCode;
23
24use ed25519_dalek::pkcs8::DecodePrivateKey;
25use ed25519_dalek::SigningKey;
26use rfc9162_log::{Op, Sidecar};
27use rfc9162_tiles::{Checkpoint, Note, PackStore, Signer, Verifier};
28
29const SIDECAR_DIR: &str = ".sidecar";
30
31struct Args {
32    command: String,
33    dir: Option<PathBuf>,
34    key: Option<PathBuf>,
35    origin: Option<String>,
36    note: Option<PathBuf>,
37    out: Option<PathBuf>,
38    skip: Vec<String>,
39    trusted: Vec<String>,
40    meta: BTreeMap<String, String>,
41    rest: Vec<String>,
42}
43
44fn usage(what: &str) -> ExitCode {
45    eprintln!("rfc9162-sidecar: {what}");
46    eprintln!("usage: rfc9162-sidecar COMMAND --dir DIR [...]; commands: init sync snapshot history tree diff blob include");
47    ExitCode::from(2)
48}
49
50fn parse(argv: Vec<String>) -> Result<Args, String> {
51    let mut it = argv.into_iter();
52    let command = it.next().ok_or("a command is needed")?;
53    let mut args = Args {
54        command,
55        dir: None,
56        key: None,
57        origin: None,
58        note: None,
59        out: None,
60        skip: Vec::new(),
61        trusted: Vec::new(),
62        meta: BTreeMap::new(),
63        rest: Vec::new(),
64    };
65    while let Some(arg) = it.next() {
66        let mut value = |flag: &str| it.next().ok_or(format!("{flag} needs a value"));
67        match arg.as_str() {
68            "--dir" => args.dir = Some(PathBuf::from(value("--dir")?)),
69            "--key" => args.key = Some(PathBuf::from(value("--key")?)),
70            "--origin" => args.origin = Some(value("--origin")?),
71            "--note" => args.note = Some(PathBuf::from(value("--note")?)),
72            "--out" => args.out = Some(PathBuf::from(value("--out")?)),
73            "--skip" => args.skip.push(value("--skip")?),
74            "--trusted" => args.trusted.push(value("--trusted")?),
75            "--meta" => {
76                let pair = value("--meta")?;
77                let (k, v) = pair
78                    .split_once('=')
79                    .ok_or(format!("--meta {pair:?} is not K=V"))?;
80                args.meta.insert(k.to_string(), v.to_string());
81            }
82            other if other.starts_with("--") => return Err(format!("unknown flag {other}")),
83            _ => args.rest.push(arg),
84        }
85    }
86    Ok(args)
87}
88
89fn open(dir: &Path) -> Result<PackStore, String> {
90    let pack = dir.join(SIDECAR_DIR).join("pack");
91    PackStore::open(&pack).map_err(|e| format!("open {}: {e}", pack.display()))
92}
93
94fn run(args: &Args) -> Result<(), String> {
95    let dir = args.dir.as_deref().ok_or("--dir DIR is needed")?;
96    let store = open(dir)?;
97    let mut sidecar = Sidecar::open(&store).map_err(|e| format!("sidecar: {e}"))?;
98    let out = io::stdout();
99    let mut out = out.lock();
100    match args.command.as_str() {
101        "init" => {
102            writeln!(
103                out,
104                "sidecar at {} size {}",
105                dir.join(SIDECAR_DIR).display(),
106                sidecar.size()
107            )
108            .ok();
109        }
110        "sync" => {
111            let mut skip: Vec<&str> = vec![SIDECAR_DIR];
112            skip.extend(args.skip.iter().map(String::as_str));
113            let synced = sidecar
114                .sync_dir(dir, &skip, &args.meta)
115                .map_err(|e| format!("sync: {e}"))?;
116            for path in &synced.added {
117                writeln!(out, "added {path}").ok();
118            }
119            for path in &synced.changed {
120                writeln!(out, "changed {path}").ok();
121            }
122            for path in &synced.deleted {
123                writeln!(out, "deleted {path}").ok();
124            }
125            writeln!(
126                out,
127                "synced: {} added, {} changed, {} deleted, {} unchanged; entries {}..{}; size {}",
128                synced.added.len(),
129                synced.changed.len(),
130                synced.deleted.len(),
131                synced.unchanged,
132                synced.entries.start,
133                synced.entries.end,
134                sidecar.size()
135            )
136            .ok();
137        }
138        "snapshot" => {
139            let key = args.key.as_deref().ok_or("--key KEY.pem is needed")?;
140            let origin = args.origin.as_deref().ok_or("--origin ORIGIN is needed")?;
141            let signing = SigningKey::read_pkcs8_pem_file(key)
142                .map_err(|e| format!("read signing key {}: {e}", key.display()))?;
143            let signer = Signer::new(origin, signing).map_err(|e| format!("signer: {e}"))?;
144            let note = sidecar
145                .snapshot(&signer, origin)
146                .map_err(|e| format!("snapshot: {e}"))?;
147            let checkpoint =
148                Checkpoint::parse(&note.text).map_err(|e| format!("checkpoint: {e}"))?;
149            writeln!(
150                out,
151                "snapshot {} size {} root {}",
152                checkpoint.origin,
153                checkpoint.size,
154                rfc9162::hex(&checkpoint.root)
155            )
156            .ok();
157        }
158        "history" => {
159            let path = args.rest.first().ok_or("history needs a PATH")?;
160            for found in sidecar.history(path).map_err(|e| format!("history: {e}"))? {
161                writeln!(out, "{}", describe(found.index, &found.change)).ok();
162            }
163        }
164        "tree" => {
165            let size = match args.rest.first() {
166                Some(text) => text.parse::<u64>().map_err(|_| "SIZE is a number")?,
167                None => sidecar.size(),
168            };
169            for (path, (index, change)) in
170                sidecar.tree_at(size).map_err(|e| format!("tree: {e}"))?
171            {
172                writeln!(
173                    out,
174                    "{path}\t{index}\t{}\t{}",
175                    change.blob.as_deref().unwrap_or("-"),
176                    change.size.unwrap_or(0)
177                )
178                .ok();
179            }
180        }
181        "diff" => {
182            let from = args
183                .rest
184                .first()
185                .and_then(|t| t.parse::<u64>().ok())
186                .ok_or("diff needs FROM TO")?;
187            let to = args
188                .rest
189                .get(1)
190                .and_then(|t| t.parse::<u64>().ok())
191                .ok_or("diff needs FROM TO")?;
192            for (index, change) in sidecar.diff(from, to).map_err(|e| format!("diff: {e}"))? {
193                writeln!(out, "{}", describe(index, &change)).ok();
194            }
195        }
196        "blob" => {
197            let hex = args.rest.first().ok_or("blob needs HEX")?;
198            let bytes = sidecar
199                .blob(hex)
200                .map_err(|e| format!("blob: {e}"))?
201                .ok_or(format!("no blob {hex}"))?;
202            match &args.out {
203                Some(file) => std::fs::write(file, &bytes)
204                    .map_err(|e| format!("write {}: {e}", file.display()))?,
205                None => out.write_all(&bytes).map_err(|e| format!("stdout: {e}"))?,
206            }
207        }
208        "include" => {
209            let path = args.rest.first().ok_or("include needs a PATH")?;
210            let note_path = args.note.as_deref().ok_or("--note FILE is needed")?;
211            if args.trusted.is_empty() {
212                return Err("--trusted KEYLINE is needed at least once".to_string());
213            }
214            let mut text = String::new();
215            std::fs::File::open(note_path)
216                .and_then(|mut f| f.read_to_string(&mut text))
217                .map_err(|e| format!("read {}: {e}", note_path.display()))?;
218            let note = Note::parse(&text).map_err(|e| format!("note: {e}"))?;
219            let trusted = args
220                .trusted
221                .iter()
222                .map(|line| Verifier::parse(line).map_err(|e| format!("trusted key: {e}")))
223                .collect::<Result<Vec<_>, _>>()?;
224            let index = sidecar
225                .include_checkpoint(path, &note, &trusted, args.meta.clone())
226                .map_err(|e| format!("include: {e}"))?;
227            writeln!(out, "included {path} at {index}").ok();
228        }
229        other => return Err(format!("unknown command {other}")),
230    }
231    Ok(())
232}
233
234fn describe(index: u64, change: &rfc9162_log::Change) -> String {
235    let op = match change.op {
236        Op::Put => "put",
237        Op::Delete => "delete",
238        Op::Checkpoint => "checkpoint",
239    };
240    let mut line = format!("{index}\t{op}\t{}", change.path);
241    if let Some(blob) = &change.blob {
242        line.push_str(&format!("\t{blob}"));
243    }
244    if let Some(size) = change.size {
245        line.push_str(&format!("\t{size}"));
246    }
247    if let Some(origin) = &change.origin {
248        line.push_str(&format!("\t{origin}"));
249    }
250    line
251}
252
253fn main() -> ExitCode {
254    let argv: Vec<String> = std::env::args().skip(1).collect();
255    let args = match parse(argv) {
256        Ok(args) => args,
257        Err(what) => return usage(&what),
258    };
259    match run(&args) {
260        Ok(()) => ExitCode::SUCCESS,
261        Err(what)
262            if what.ends_with("is needed")
263                || what.starts_with("unknown command")
264                || what.contains("needs ") =>
265        {
266            usage(&what)
267        }
268        Err(what) => {
269            eprintln!("rfc9162-sidecar: {what}");
270            ExitCode::from(1)
271        }
272    }
273}