1use crate::{
14 ArtifactAuthenticityVerifier, ArtifactDescriptor, ArtifactTarget, UpdateError, UpdateResult,
15};
16use appcore_contracts::{ApplicationId, BuildId};
17use serde::{Deserialize, Serialize};
18
19pub const UPDATE_OFFER_CAPABILITY: &str = "appcore.update.offer";
21pub const UPDATE_CHUNK_CAPABILITY: &str = "appcore.update.chunk";
23pub const UPDATE_PEER_METADATA_MAX_BYTES: usize = 16 * 1024;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum ArtifactPeerStatusV2 {
30 Accepted,
32 Unauthorized,
34 Unavailable,
36 Stale,
38 Incompatible,
40 Corrupted,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct ArtifactOfferRequestV2 {
47 pub application_id: ApplicationId,
49 pub build_id: BuildId,
51 pub target: ArtifactTarget,
53 pub sha256: String,
55 pub size_bytes: u64,
57 pub protocol_version: String,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct ArtifactOfferResponseV2 {
64 pub status: ArtifactPeerStatusV2,
66 pub sha256: String,
68 pub size_bytes: u64,
70 pub max_chunk_bytes: u32,
72 pub protocol_version: String,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct LatestCompatibleOfferRequestV2 {
79 pub application_id: ApplicationId,
81 pub target: ArtifactTarget,
83 pub channel: String,
85 pub current_version: String,
87 pub runtime_version: String,
89 pub protocol_version: String,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct LatestCompatibleOfferResponseV2 {
96 pub status: ArtifactPeerStatusV2,
98 pub descriptor: Option<ArtifactDescriptor>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct ArtifactChunkRequestV2 {
105 pub sha256: String,
107 pub offset: u64,
109 pub len: u32,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub struct ArtifactChunkResponseV2 {
116 pub sha256: String,
118 pub offset: u64,
120 pub len: u32,
122 pub chunk_sha256: String,
124 pub total_size_bytes: u64,
126}
127
128impl ArtifactOfferRequestV2 {
129 pub fn validate(&self) -> UpdateResult<()> {
131 validate_digest(&self.sha256)?;
132 validate_protocol(&self.protocol_version)?;
133 if self.size_bytes == 0 {
134 return Err(UpdateError::InvalidArtifact(
135 "peer offer size_bytes must be greater than zero".to_string(),
136 ));
137 }
138 validate_metadata_size(self)
139 }
140}
141
142impl LatestCompatibleOfferRequestV2 {
143 pub fn validate(&self) -> UpdateResult<()> {
145 validate_target(&self.target)?;
146 validate_text("channel", &self.channel, 64)?;
147 validate_version("current_version", &self.current_version)?;
148 validate_version("runtime_version", &self.runtime_version)?;
149 validate_protocol(&self.protocol_version)?;
150 validate_metadata_size(self)
151 }
152}
153
154impl LatestCompatibleOfferResponseV2 {
155 pub fn validate_against(&self, request: &LatestCompatibleOfferRequestV2) -> UpdateResult<()> {
157 request.validate()?;
158 match (&self.status, &self.descriptor) {
159 (ArtifactPeerStatusV2::Accepted, Some(descriptor)) => {
160 descriptor.validate()?;
161 let target = descriptor.target().ok_or_else(|| {
162 UpdateError::Incompatible(
163 "latest compatible offer must include a platform target".to_string(),
164 )
165 })?;
166 if descriptor.application_id() != &request.application_id
167 || descriptor.channel() != request.channel
168 || target != &request.target
169 {
170 return Err(UpdateError::Incompatible(
171 "latest compatible offer does not match the request".to_string(),
172 ));
173 }
174 let offered_version = semver::Version::parse(descriptor.application_version())
175 .map_err(|error| {
176 UpdateError::InvalidArtifact(format!("invalid offered version: {error}"))
177 })?;
178 let current_version =
179 semver::Version::parse(&request.current_version).map_err(|error| {
180 UpdateError::Incompatible(format!("invalid installed version: {error}"))
181 })?;
182 if offered_version <= current_version {
183 return Err(UpdateError::Incompatible(
184 "latest compatible offer is not newer than the installed version"
185 .to_string(),
186 ));
187 }
188 descriptor
189 .ensure_compatible(&request.runtime_version, &request.protocol_version)?;
190 }
191 (ArtifactPeerStatusV2::Accepted, None) => {
192 return Err(UpdateError::Transfer(
193 "accepted latest compatible offer has no descriptor".to_string(),
194 ));
195 }
196 (_, Some(_)) => {
197 return Err(UpdateError::Transfer(
198 "non-accepted latest compatible offer must not include a descriptor"
199 .to_string(),
200 ));
201 }
202 (_, None) => {}
203 }
204 validate_metadata_size(self)
205 }
206
207 pub fn verify_descriptor(
209 &self,
210 verifier: &dyn ArtifactAuthenticityVerifier,
211 ) -> UpdateResult<()> {
212 if let Some(descriptor) = &self.descriptor {
213 verifier.verify(descriptor)?;
214 }
215 Ok(())
216 }
217}
218
219impl ArtifactOfferResponseV2 {
220 pub fn validate_against(&self, request: &ArtifactOfferRequestV2) -> UpdateResult<()> {
222 validate_digest(&self.sha256)?;
223 validate_protocol(&self.protocol_version)?;
224 if self.status == ArtifactPeerStatusV2::Accepted
225 && (self.sha256 != request.sha256
226 || self.size_bytes != request.size_bytes
227 || self.protocol_version != request.protocol_version
228 || self.max_chunk_bytes == 0)
229 {
230 return Err(UpdateError::Transfer(
231 "peer offer response does not repeat the requested artifact identity".to_string(),
232 ));
233 }
234 validate_metadata_size(self)
235 }
236}
237
238impl ArtifactChunkRequestV2 {
239 pub fn validate_against(
241 &self,
242 offer: &ArtifactOfferRequestV2,
243 max_chunk_bytes: u32,
244 ) -> UpdateResult<()> {
245 validate_digest(&self.sha256)?;
246 if self.sha256 != offer.sha256
247 || self.len == 0
248 || self.len > max_chunk_bytes
249 || self.offset >= offer.size_bytes
250 || self.offset.saturating_add(self.len as u64) > offer.size_bytes
251 {
252 return Err(UpdateError::Transfer(
253 "peer chunk request is outside the offered artifact bounds".to_string(),
254 ));
255 }
256 validate_metadata_size(self)
257 }
258}
259
260impl ArtifactChunkResponseV2 {
261 pub fn validate_against(
263 &self,
264 request: &ArtifactChunkRequestV2,
265 total_size_bytes: u64,
266 ) -> UpdateResult<()> {
267 validate_digest(&self.sha256)?;
268 validate_digest(&self.chunk_sha256)?;
269 if self.sha256 != request.sha256
270 || self.offset != request.offset
271 || self.len == 0
272 || self.len > request.len
273 || self.total_size_bytes != total_size_bytes
274 || self.offset.saturating_add(self.len as u64) > total_size_bytes
275 {
276 return Err(UpdateError::Transfer(
277 "peer chunk response does not repeat the requested bounds".to_string(),
278 ));
279 }
280 validate_metadata_size(self)
281 }
282}
283
284fn validate_digest(value: &str) -> UpdateResult<()> {
285 if value.len() != 64
286 || !value
287 .bytes()
288 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
289 {
290 return Err(UpdateError::Transfer(
291 "peer update digest must be lowercase SHA-256".to_string(),
292 ));
293 }
294 Ok(())
295}
296
297fn validate_protocol(value: &str) -> UpdateResult<()> {
298 if value.trim().is_empty() || value.len() > 64 || value.chars().any(char::is_control) {
299 return Err(UpdateError::Incompatible(
300 "peer update protocol version is invalid".to_string(),
301 ));
302 }
303 Ok(())
304}
305
306fn validate_version(name: &str, value: &str) -> UpdateResult<()> {
307 validate_text(name, value, 64)?;
308 semver::Version::parse(value)
309 .map(|_| ())
310 .map_err(|error| UpdateError::Incompatible(format!("{name} is invalid: {error}")))
311}
312
313fn validate_text(name: &str, value: &str, max: usize) -> UpdateResult<()> {
314 if value.trim().is_empty() || value.len() > max || value.chars().any(char::is_control) {
315 return Err(UpdateError::Incompatible(format!(
316 "peer update {name} is invalid"
317 )));
318 }
319 Ok(())
320}
321
322fn validate_target(target: &ArtifactTarget) -> UpdateResult<()> {
323 for (name, value) in [
324 ("target.os", target.os()),
325 ("target.architecture", target.architecture()),
326 ("target.format", target.format()),
327 ] {
328 validate_text(name, value, 64)?;
329 }
330 Ok(())
331}
332
333fn validate_metadata_size<T: Serialize>(value: &T) -> UpdateResult<()> {
334 let size = serde_json::to_vec(value)
335 .map_err(|error| UpdateError::Transfer(error.to_string()))?
336 .len();
337 if size > UPDATE_PEER_METADATA_MAX_BYTES {
338 return Err(UpdateError::Transfer(
339 "peer update metadata exceeds configured limit".to_string(),
340 ));
341 }
342 Ok(())
343}