Share my setup, links: a waybetter://setup link opened from outside reaches the shell as text on the install-link event (shape and size checked natively, never decoded or applied there); the install controller hands it to the app's onSetupLink and stages nothing
This commit is contained in:
parent
95e0443abd
commit
d1a1918119
4 changed files with 73 additions and 8 deletions
|
|
@ -20,8 +20,14 @@ pub const RECORDS_FILE: &str = "store-installed.json";
|
|||
/// The last verified catalog and its signature (verified again when read back).
|
||||
pub const CATALOG_CACHE: &str = "store-catalog.json";
|
||||
pub const CATALOG_SIG_CACHE: &str = "store-catalog.json.sig";
|
||||
/// Shell event: a validated install link (`{ link: { kind, url } }`) or `{ error }`.
|
||||
/// Shell event: a validated install link (`{ link: { kind, url } }`), a "Share my setup" link
|
||||
/// (`{ setup }`: its text, for the shell's preview) or `{ error }`.
|
||||
pub const EVENT_LINK: &str = "install-link";
|
||||
/// "Share my setup" links: `waybetter://setup?app=…&d=…`. Native code only checks the shape and
|
||||
/// hands the text to the shell; it never decodes or applies a setup (the shell shows what would
|
||||
/// change and asks first).
|
||||
pub const SETUP_LINK: &str = "waybetter://setup";
|
||||
const MAX_SETUP_LINK: usize = 24 * 1024;
|
||||
/// Links received before the shell asked for them (startup, or while the shell reloads).
|
||||
const MAX_PENDING: usize = 8;
|
||||
const MAX_STAGED: usize = 8;
|
||||
|
|
@ -38,6 +44,22 @@ pub struct LinkEvent {
|
|||
pub link: Option<InstallLink>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
/// A "Share my setup" link, as text (see [`SETUP_LINK`]).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub setup: Option<String>,
|
||||
}
|
||||
|
||||
/// `Some` when `raw` is a setup link: its text when it has the plain shape (the path, a query of
|
||||
/// URL-safe characters, bounded), else an error. `None`: not a setup link.
|
||||
pub fn setup_link(raw: &str) -> Option<Result<String, String>> {
|
||||
let rest = raw.strip_prefix(SETUP_LINK)?;
|
||||
let query = rest.strip_prefix("/?").or_else(|| rest.strip_prefix('?'));
|
||||
let ok = query.is_some_and(|query| {
|
||||
!query.is_empty()
|
||||
&& raw.len() <= MAX_SETUP_LINK
|
||||
&& query.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'=' | b'&' | b'%' | b'.'))
|
||||
});
|
||||
Some(if ok { Ok(raw.to_owned()) } else { Err("LINK: this setup link is not valid".to_owned()) })
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
|
|
@ -220,9 +242,13 @@ impl Store {
|
|||
/// A link reached the app (OS deep link, second launch): validate it and queue it for the
|
||||
/// shell. Returns the event to emit ([`EVENT_LINK`]). Nothing is downloaded or installed here.
|
||||
pub fn receive_link(&self, raw: &str) -> LinkEvent {
|
||||
let event = match dist::parse_install_link(self.config, raw) {
|
||||
Ok(link) => LinkEvent { link: Some(link), error: None },
|
||||
Err(error) => LinkEvent { link: None, error: Some(error) },
|
||||
let event = match setup_link(raw) {
|
||||
Some(Ok(text)) => LinkEvent { link: None, error: None, setup: Some(text) },
|
||||
Some(Err(error)) => LinkEvent { link: None, error: Some(error), setup: None },
|
||||
None => match dist::parse_install_link(self.config, raw) {
|
||||
Ok(link) => LinkEvent { link: Some(link), error: None, setup: None },
|
||||
Err(error) => LinkEvent { link: None, error: Some(error), setup: None },
|
||||
},
|
||||
};
|
||||
let mut inner = self.lock();
|
||||
if inner.pending.len() >= MAX_PENDING {
|
||||
|
|
@ -598,6 +624,19 @@ mod tests {
|
|||
assert!(ok.link.is_some() && ok.error.is_none());
|
||||
let bad = store.receive_link("waybetter://install?kind=theme&url=https://evil.example/a.json");
|
||||
assert!(bad.link.is_none() && bad.error.unwrap().starts_with("LINK:"));
|
||||
// A "Share my setup" link is only handed over as text; its data is never read here.
|
||||
let setup = store.receive_link("waybetter://setup?app=x&d=eyJmb3JtYXQiOiJ3YXliZXR0ZXItc2V0dXAifQ");
|
||||
assert_eq!(setup.setup.as_deref(), Some("waybetter://setup?app=x&d=eyJmb3JtYXQiOiJ3YXliZXR0ZXItc2V0dXAifQ"));
|
||||
assert!(setup.link.is_none() && setup.error.is_none());
|
||||
for bad in ["waybetter://setup", "waybetter://setup?", "waybetter://setup?d=<script>", "waybetter://setup?d=a b", "waybetter://setupx?d=a"] {
|
||||
let event = store.receive_link(bad);
|
||||
assert!(event.setup.is_none(), "{bad}");
|
||||
assert!(event.error.is_some(), "{bad}");
|
||||
}
|
||||
let long = format!("waybetter://setup?d={}", "a".repeat(MAX_SETUP_LINK));
|
||||
assert!(store.receive_link(&long).setup.is_none(), "bounded");
|
||||
assert_eq!(serde_json::to_string(&setup).unwrap(), r#"{"setup":"waybetter://setup?app=x&d=eyJmb3JtYXQiOiJ3YXliZXR0ZXItc2V0dXAifQ"}"#);
|
||||
store.take_pending();
|
||||
for _ in 0..20 {
|
||||
store.receive_link("waybetter://install?kind=theme&url=https://waybetter.fyi/a.json");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue