1use std::path::Path;
4
5use andysu_core::keys::{PublicKey, SecretKey};
6use andysu_core::manifest::{Manifest, Release, Rollout};
7use andysu_core::sig::{self, Cert};
8use andysu_core::time;
9
10use crate::{read, write, R};
11
12pub struct Signer {
13 pub key: SecretKey,
14 pub cert: Option<Cert>,
15}
16
17pub struct PublishOptions {
18 pub channel: String,
19 pub release: String,
20 pub rollout: Option<u8>,
21 pub mandatory: bool,
22 pub notes_url: Option<String>,
23 pub keep: usize,
24 pub expires_days: i64,
25 pub now: i64,
26}
27
28pub fn load_release(site: &Path, id: &str) -> R<Release> {
29 let p = site.join("releases").join(format!("{id}.json"));
30 serde_json::from_slice(&read(&p)?).map_err(|e| format!("{}: {e}", p.display()))
31}
32
33fn app_of(site: &Path, r: &Release) -> R<String> {
34 let d = r
35 .components
36 .iter()
37 .find(|c| c.path == andysu_core::manifest::DESCRIPTOR_PATH)
38 .ok_or("release has no descriptor")?;
39 let b = read(&site.join("blobs/sha256").join(&d.sha256)).or_else(|_| {
40 d.urls
41 .iter()
42 .find_map(|u| match andysu_core::transport::get_max(u, None, d.size) {
43 Ok(andysu_core::transport::Got::Body { bytes, .. }) => Some(bytes),
44 _ => None,
45 })
46 .ok_or_else(|| "descriptor blob not available".to_string())
47 })?;
48 Ok(andysu_core::payload::Descriptor::parse(&b).map_err(|e| e.to_string())?.app)
49}
50
51pub fn write_signed(site: &Path, mut m: Manifest, signer: &Signer, now: i64, expires_days: i64) -> R<Manifest> {
53 m.sequence += 1;
54 m.issued = time::format(now);
55 m.expires = time::format(now + expires_days * 86400);
56 m.releases.sort_by_key(|r| r.sequence);
57 m.validate().map_err(|e| e.to_string())?;
58 let mut b = serde_json::to_vec_pretty(&m).unwrap();
59 b.push(b'\n');
60 let s = sig::sign(&b, &signer.key, signer.cert.clone()).map_err(|e| e.to_string())?;
61 let dir = site.join(&m.channel);
62 write(&dir.join("update.json"), &b)?;
63 write(&dir.join("update.json.sig"), &s)?;
64 Ok(m)
65}
66
67pub fn publish(site: &Path, o: &PublishOptions, signer: &Signer) -> R<Manifest> {
68 let mut r = load_release(site, &o.release)?;
69 if let Some(p) = o.rollout {
70 r.rollout = Some(Rollout { percent: p.min(100), salt: r.id.clone() });
71 }
72 r.mandatory = o.mandatory;
73 if o.notes_url.is_some() {
74 r.notes_url = o.notes_url.clone();
75 }
76 let m = match crate::pack::load_manifest(site, &o.channel)? {
77 Some(m) => m,
78 None => Manifest {
79 schema: 1,
80 app: app_of(site, &r)?,
81 channel: o.channel.clone(),
82 issued: time::format(o.now),
83 expires: time::format(o.now),
84 sequence: 0,
85 releases: vec![],
86 },
87 };
88 let mut m = m;
89 m.releases.retain(|x| x.id != r.id);
90 m.releases.push(r);
91 m.releases.sort_by_key(|x| x.sequence);
92 while m.releases.len() > o.keep.max(1) {
93 m.releases.remove(0);
94 }
95 write_signed(site, m, signer, o.now, o.expires_days)
96}
97
98pub fn refresh(
101 site: &Path,
102 channel: &str,
103 signer: &Signer,
104 now: i64,
105 expires_days: i64,
106 rollout: Option<(&str, u8)>,
107) -> R<Manifest> {
108 let mut m =
109 crate::pack::load_manifest(site, channel)?.ok_or_else(|| format!("no manifest for channel {channel}"))?;
110 if let Some((id, p)) = rollout {
111 let r = m.releases.iter_mut().find(|r| r.id == id).ok_or_else(|| format!("no release {id}"))?;
112 r.rollout = Some(Rollout {
113 percent: p.min(100),
114 salt: r.rollout.as_ref().map(|x| x.salt.clone()).unwrap_or_else(|| r.id.clone()),
115 });
116 }
117 write_signed(site, m, signer, now, expires_days)
118}
119
120pub fn rollback(
123 site: &Path,
124 channel: &str,
125 to: &str,
126 signer: &Signer,
127 now: i64,
128 expires_days: i64,
129) -> R<(Manifest, String)> {
130 let mut m =
131 crate::pack::load_manifest(site, channel)?.ok_or_else(|| format!("no manifest for channel {channel}"))?;
132 let good = m
133 .releases
134 .iter()
135 .find(|r| r.id == to)
136 .cloned()
137 .or_else(|| load_release(site, to).ok())
138 .ok_or_else(|| format!("no release {to}"))?;
139 let top = crate::pack::known_releases(site)?
140 .iter()
141 .map(|r| r.sequence)
142 .max()
143 .unwrap_or(0)
144 .max(m.releases.iter().map(|r| r.sequence).max().unwrap_or(0));
145 let mut again = good.clone();
146 again.sequence = top + 1;
147 again.id = format!("{}-r{}", good.id, again.sequence);
148 again.rollout = None;
149 m.releases.retain(|r| r.sequence <= good.sequence && r.id != good.id);
150 m.releases.push(again.clone());
151 write(&site.join("releases").join(format!("{}.json", again.id)), &serde_json::to_vec_pretty(&again).unwrap())?;
152 Ok((write_signed(site, m, signer, now, expires_days)?, again.id))
153}
154
155pub fn verify(
157 src: &str,
158 roots: &[PublicKey],
159 app: Option<&str>,
160 channel: Option<&str>,
161 blobs: bool,
162 now: i64,
163) -> R<String> {
164 let (mb, sb, base) = if src.contains("://") {
165 let get = |u: &str| match andysu_core::transport::get(u, None) {
166 Ok(andysu_core::transport::Got::Body { bytes, .. }) => Ok(bytes),
167 Ok(_) => Err(format!("{u}: not modified?")),
168 Err(e) => Err(e.to_string()),
169 };
170 (get(src)?, get(&format!("{src}.sig"))?, src.to_string())
171 } else {
172 let p = std::fs::canonicalize(src).map_err(|e| format!("{src}: {e}"))?;
173 (read(&p)?, read(Path::new(&format!("{}.sig", p.display())))?, format!("file://{}", p.display()))
174 };
175 let v = sig::verify(&mb, &sb, roots, now).map_err(|e| e.to_string())?;
176 let m = Manifest::parse(&mb).map_err(|e| e.to_string())?;
177 if let Some(a) = app {
178 if m.app != a {
179 return Err(format!("manifest app {} != {a}", m.app));
180 }
181 }
182 if let Some(c) = channel {
183 if m.channel != c {
184 return Err(format!("manifest channel {} != {c}", m.channel));
185 }
186 }
187 if m.expires_ts() <= now {
188 return Err(format!("manifest expired at {}", m.expires));
189 }
190 let mut out = format!(
191 "OK {} channel {} sequence {} expires {} (root {}{})\n",
192 m.app,
193 m.channel,
194 m.sequence,
195 m.expires,
196 v.root_id,
197 v.release_key_id.map(|k| format!(", release key {k}")).unwrap_or_default()
198 );
199 let baseu = url::Url::parse(&base).map_err(|e| e.to_string())?;
200 for r in &m.releases {
201 out.push_str(&format!(" release {} seq {} ({} components)\n", r.id, r.sequence, r.components.len()));
202 if !blobs {
203 continue;
204 }
205 let check = |sha: &str, urls: &[String], default: String, size: u64| -> R<()> {
206 let list = if urls.is_empty() { vec![default] } else { urls.to_vec() };
207 let u = baseu.join(&list[0]).map_err(|e| e.to_string())?;
208 let b = match andysu_core::transport::get_max(u.as_str(), None, size).map_err(|e| e.to_string())? {
209 andysu_core::transport::Got::Body { bytes, .. } => bytes,
210 _ => return Err("not modified".into()),
211 };
212 if b.len() as u64 != size || andysu_core::hash::sha256_hex(&b) != sha {
213 return Err(format!("blob {u} does not match sha256 {sha}"));
214 }
215 Ok(())
216 };
217 for c in &r.components {
218 check(&c.sha256, &c.urls, format!("../blobs/sha256/{}", c.sha256), c.size)?;
219 for d in &c.deltas {
220 check(&d.sha256, &d.urls, format!("../deltas/{}-{}/{}", d.from_sha256, c.sha256, d.sha256), d.size)?;
221 }
222 }
223 out.push_str(" all blobs verified\n");
224 }
225 Ok(out)
226}