1use 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(¬e.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, ¬e, &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}