Skip to main content

lingxia_update/
lib.rs

1mod app;
2mod config;
3mod error;
4mod lxapp;
5
6use lingxia_provider::{BoxFuture, ProviderError};
7use serde::{Deserialize, Serialize};
8use std::cmp::Ordering;
9use std::fmt;
10use std::str::FromStr;
11
12pub use app::{
13    AppUpdateApply, AppUpdateEvent, AppUpdateEventReceiver, AppUpdateEventSender, AppUpdateHost,
14    AppUpdateProgressReporter, AppUpdateStage, app_update_scope_key, check_app_update,
15    ensure_app_update_candidate_version, send_app_update_event, send_app_update_failed,
16    subscribe_app_update_events,
17};
18pub use config::{UpdateConfig, configure_update, update_config};
19pub use error::UpdateError;
20pub use lxapp::{
21    LxAppUpdateHost, ensure_first_install as ensure_lxapp_first_install,
22    ensure_force_update_for_installed as ensure_lxapp_force_update_for_installed,
23    ensure_target_version_ready as ensure_lxapp_target_version_ready, lxapp_update_scope_key,
24    spawn_background_update_check as spawn_lxapp_background_update_check,
25};
26
27#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
28#[serde(rename_all = "lowercase")]
29pub enum ReleaseType {
30    #[default]
31    Release,
32    Preview,
33    Developer,
34}
35
36impl ReleaseType {
37    pub fn as_str(self) -> &'static str {
38        match self {
39            Self::Release => "release",
40            Self::Preview => "preview",
41            Self::Developer => "developer",
42        }
43    }
44}
45
46impl fmt::Display for ReleaseType {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str(self.as_str())
49    }
50}
51
52/// A semantic version representation (`major.minor.patch`) shared by update policy
53/// and lxapp metadata persistence.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Version {
56    pub major: u32,
57    pub minor: u32,
58    pub patch: u32,
59}
60
61impl Version {
62    pub fn parse(version_str: &str) -> Result<Self, VersionError> {
63        let parts: Vec<&str> = version_str.split('.').collect();
64        if parts.len() != 3 {
65            return Err(VersionError::InvalidFormat);
66        }
67
68        let major = parts[0]
69            .parse()
70            .map_err(|_| VersionError::InvalidComponent)?;
71        let minor = parts.get(1).map_or(Ok(0), |s| {
72            s.parse().map_err(|_| VersionError::InvalidComponent)
73        })?;
74        let patch = parts.get(2).map_or(Ok(0), |s| {
75            s.parse().map_err(|_| VersionError::InvalidComponent)
76        })?;
77
78        Ok(Self {
79            major,
80            minor,
81            patch,
82        })
83    }
84}
85
86impl FromStr for Version {
87    type Err = VersionError;
88
89    fn from_str(s: &str) -> Result<Self, Self::Err> {
90        Self::parse(s)
91    }
92}
93
94impl fmt::Display for Version {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
97    }
98}
99
100impl PartialOrd for Version {
101    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
102        Some(self.cmp(other))
103    }
104}
105
106impl Ord for Version {
107    fn cmp(&self, other: &Self) -> Ordering {
108        match self.major.cmp(&other.major) {
109            Ordering::Equal => match self.minor.cmp(&other.minor) {
110                Ordering::Equal => self.patch.cmp(&other.patch),
111                ordering => ordering,
112            },
113            ordering => ordering,
114        }
115    }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
119pub enum VersionError {
120    #[error("invalid version format, expected 'major.minor.patch'")]
121    InvalidFormat,
122    #[error("invalid version component, expected unsigned integer")]
123    InvalidComponent,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127pub struct SemanticVersion {
128    pub major: u32,
129    pub minor: u32,
130    pub patch: u32,
131}
132
133impl SemanticVersion {
134    pub fn from_version(version: &Version) -> Self {
135        Self {
136            major: version.major,
137            minor: version.minor,
138            patch: version.patch,
139        }
140    }
141
142    pub fn to_version_string(&self) -> String {
143        format!("{}.{}.{}", self.major, self.minor, self.patch)
144    }
145}
146
147impl fmt::Display for SemanticVersion {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
150    }
151}
152
153#[derive(Clone, Debug, PartialEq, Eq)]
154pub enum LxAppUpdateQuery {
155    Latest { current_version: Option<String> },
156    TargetVersion(String),
157}
158
159impl LxAppUpdateQuery {
160    pub fn latest(current_version: Option<impl Into<String>>) -> Self {
161        Self::Latest {
162            current_version: current_version.map(Into::into),
163        }
164    }
165
166    pub fn target_version(version: impl Into<String>) -> Self {
167        Self::TargetVersion(version.into())
168    }
169}
170
171#[derive(Clone, Debug, PartialEq, Eq)]
172pub enum UpdateTarget {
173    App {
174        current_version: Option<String>,
175    },
176    LxApp {
177        id: String,
178        channel: ReleaseType,
179        query: LxAppUpdateQuery,
180    },
181    Plugin {
182        id: String,
183        version: String,
184    },
185}
186
187impl UpdateTarget {
188    pub fn app(current_version: Option<impl Into<String>>) -> Self {
189        Self::App {
190            current_version: current_version.map(Into::into),
191        }
192    }
193
194    pub fn lxapp(id: impl Into<String>, channel: ReleaseType, query: LxAppUpdateQuery) -> Self {
195        Self::LxApp {
196            id: id.into(),
197            channel,
198            query,
199        }
200    }
201
202    pub fn plugin(id: impl Into<String>, version: impl Into<String>) -> Self {
203        Self::Plugin {
204            id: id.into(),
205            version: version.into(),
206        }
207    }
208
209    /// Stable routing key for dedupe, metrics, and diagnostics.
210    pub fn scope_key(&self) -> String {
211        match self {
212            Self::App { .. } => "app".to_string(),
213            Self::LxApp { id, channel, .. } => format!("lxapp:{id}@{}", channel.as_str()),
214            Self::Plugin { id, version } => format!("plugin:{id}@{version}"),
215        }
216    }
217}
218
219#[derive(Clone, Debug)]
220pub struct UpdatePackageInfo {
221    pub version: String,
222    pub url: String,
223    pub checksum_sha256: String,
224    pub size: Option<u64>,
225    pub release_notes: Option<Vec<String>>,
226    pub is_force_update: bool,
227    pub required_runtime_version: Option<String>,
228}
229
230impl UpdatePackageInfo {
231    pub fn should_replace_version(
232        candidate_version: &str,
233        installed_version: Option<&str>,
234    ) -> bool {
235        installed_version != Some(candidate_version)
236    }
237
238    pub fn should_replace_installed_version(&self, installed_version: Option<&str>) -> bool {
239        Self::should_replace_version(&self.version, installed_version)
240    }
241
242    pub fn required_runtime_version_trimmed(&self) -> Option<&str> {
243        self.required_runtime_version
244            .as_deref()
245            .map(str::trim)
246            .filter(|value| !value.is_empty())
247    }
248
249    pub fn ensure_runtime_compatible(
250        &self,
251        current_runtime_version: &str,
252        target_name: &str,
253    ) -> Result<(), RuntimeCompatibilityError> {
254        let Some(required_runtime_version) = self.required_runtime_version_trimmed() else {
255            return Ok(());
256        };
257
258        let current = Version::parse(current_runtime_version).map_err(|_| {
259            RuntimeCompatibilityError::InvalidCurrentRuntimeVersion {
260                runtime_version: current_runtime_version.to_string(),
261            }
262        })?;
263        let required = Version::parse(required_runtime_version).map_err(|_| {
264            RuntimeCompatibilityError::InvalidRequiredRuntimeVersion {
265                target: target_name.to_string(),
266                update_version: self.version.clone(),
267                runtime_version: required_runtime_version.to_string(),
268            }
269        })?;
270
271        if current < required {
272            return Err(RuntimeCompatibilityError::RequiresRuntimeUpgrade {
273                target: target_name.to_string(),
274                update_version: self.version.clone(),
275                required_runtime_version: required.to_string(),
276                current_runtime_version: current.to_string(),
277            });
278        }
279
280        Ok(())
281    }
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
285pub enum RuntimeCompatibilityError {
286    #[error("invalid SDK runtime version '{runtime_version}'")]
287    InvalidCurrentRuntimeVersion { runtime_version: String },
288    #[error(
289        "invalid minRuntimeVersion '{runtime_version}' from update metadata for {target}@{update_version}"
290    )]
291    InvalidRequiredRuntimeVersion {
292        target: String,
293        update_version: String,
294        runtime_version: String,
295    },
296    #[error(
297        "{target} update {update_version} requires runtime >= {required_runtime_version}, current SDK runtime is {current_runtime_version}; update host app first"
298    )]
299    RequiresRuntimeUpgrade {
300        target: String,
301        update_version: String,
302        required_runtime_version: String,
303        current_runtime_version: String,
304    },
305}
306
307/// Update contract shared by app and lxapp update implementations.
308pub trait UpdateProvider: Send + Sync + 'static {
309    /// Returns `Some(package)` when an update package exists and `None` when the target
310    /// is already up to date or no matching package is available.
311    fn check_update<'a>(
312        &'a self,
313        target: UpdateTarget,
314    ) -> BoxFuture<'a, Result<Option<UpdatePackageInfo>, ProviderError>>;
315}
316
317#[cfg(test)]
318mod tests {
319    use super::Version;
320
321    #[test]
322    fn version_parse_accepts_full_semver_only() {
323        assert!(Version::parse("1.2.3").is_ok());
324        assert!(Version::parse("1").is_err());
325        assert!(Version::parse("1.2").is_err());
326        assert!(Version::parse("1.2.3.4").is_err());
327    }
328}