use crate::{
ArtifactAuthenticityVerifier, ArtifactDescriptor, ArtifactTarget, UpdateError, UpdateResult,
};
use appcore_contracts::{ApplicationId, BuildId};
use serde::{Deserialize, Serialize};
pub const UPDATE_OFFER_CAPABILITY: &str = "appcore.update.offer";
pub const UPDATE_CHUNK_CAPABILITY: &str = "appcore.update.chunk";
pub const UPDATE_PEER_METADATA_MAX_BYTES: usize = 16 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ArtifactPeerStatusV2 {
Accepted,
Unauthorized,
Unavailable,
Stale,
Incompatible,
Corrupted,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactOfferRequestV2 {
pub application_id: ApplicationId,
pub build_id: BuildId,
pub target: ArtifactTarget,
pub sha256: String,
pub size_bytes: u64,
pub protocol_version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactOfferResponseV2 {
pub status: ArtifactPeerStatusV2,
pub sha256: String,
pub size_bytes: u64,
pub max_chunk_bytes: u32,
pub protocol_version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LatestCompatibleOfferRequestV2 {
pub application_id: ApplicationId,
pub target: ArtifactTarget,
pub channel: String,
pub current_version: String,
pub runtime_version: String,
pub protocol_version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LatestCompatibleOfferResponseV2 {
pub status: ArtifactPeerStatusV2,
pub descriptor: Option<ArtifactDescriptor>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactChunkRequestV2 {
pub sha256: String,
pub offset: u64,
pub len: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactChunkResponseV2 {
pub sha256: String,
pub offset: u64,
pub len: u32,
pub chunk_sha256: String,
pub total_size_bytes: u64,
}
impl ArtifactOfferRequestV2 {
pub fn validate(&self) -> UpdateResult<()> {
validate_digest(&self.sha256)?;
validate_protocol(&self.protocol_version)?;
if self.size_bytes == 0 {
return Err(UpdateError::InvalidArtifact(
"peer offer size_bytes must be greater than zero".to_string(),
));
}
validate_metadata_size(self)
}
}
impl LatestCompatibleOfferRequestV2 {
pub fn validate(&self) -> UpdateResult<()> {
validate_target(&self.target)?;
validate_text("channel", &self.channel, 64)?;
validate_version("current_version", &self.current_version)?;
validate_version("runtime_version", &self.runtime_version)?;
validate_protocol(&self.protocol_version)?;
validate_metadata_size(self)
}
}
impl LatestCompatibleOfferResponseV2 {
pub fn validate_against(&self, request: &LatestCompatibleOfferRequestV2) -> UpdateResult<()> {
request.validate()?;
match (&self.status, &self.descriptor) {
(ArtifactPeerStatusV2::Accepted, Some(descriptor)) => {
descriptor.validate()?;
let target = descriptor.target().ok_or_else(|| {
UpdateError::Incompatible(
"latest compatible offer must include a platform target".to_string(),
)
})?;
if descriptor.application_id() != &request.application_id
|| descriptor.channel() != request.channel
|| target != &request.target
{
return Err(UpdateError::Incompatible(
"latest compatible offer does not match the request".to_string(),
));
}
let offered_version = semver::Version::parse(descriptor.application_version())
.map_err(|error| {
UpdateError::InvalidArtifact(format!("invalid offered version: {error}"))
})?;
let current_version =
semver::Version::parse(&request.current_version).map_err(|error| {
UpdateError::Incompatible(format!("invalid installed version: {error}"))
})?;
if offered_version <= current_version {
return Err(UpdateError::Incompatible(
"latest compatible offer is not newer than the installed version"
.to_string(),
));
}
descriptor
.ensure_compatible(&request.runtime_version, &request.protocol_version)?;
}
(ArtifactPeerStatusV2::Accepted, None) => {
return Err(UpdateError::Transfer(
"accepted latest compatible offer has no descriptor".to_string(),
));
}
(_, Some(_)) => {
return Err(UpdateError::Transfer(
"non-accepted latest compatible offer must not include a descriptor"
.to_string(),
));
}
(_, None) => {}
}
validate_metadata_size(self)
}
pub fn verify_descriptor(
&self,
verifier: &dyn ArtifactAuthenticityVerifier,
) -> UpdateResult<()> {
if let Some(descriptor) = &self.descriptor {
verifier.verify(descriptor)?;
}
Ok(())
}
}
impl ArtifactOfferResponseV2 {
pub fn validate_against(&self, request: &ArtifactOfferRequestV2) -> UpdateResult<()> {
validate_digest(&self.sha256)?;
validate_protocol(&self.protocol_version)?;
if self.status == ArtifactPeerStatusV2::Accepted
&& (self.sha256 != request.sha256
|| self.size_bytes != request.size_bytes
|| self.protocol_version != request.protocol_version
|| self.max_chunk_bytes == 0)
{
return Err(UpdateError::Transfer(
"peer offer response does not repeat the requested artifact identity".to_string(),
));
}
validate_metadata_size(self)
}
}
impl ArtifactChunkRequestV2 {
pub fn validate_against(
&self,
offer: &ArtifactOfferRequestV2,
max_chunk_bytes: u32,
) -> UpdateResult<()> {
validate_digest(&self.sha256)?;
if self.sha256 != offer.sha256
|| self.len == 0
|| self.len > max_chunk_bytes
|| self.offset >= offer.size_bytes
|| self.offset.saturating_add(self.len as u64) > offer.size_bytes
{
return Err(UpdateError::Transfer(
"peer chunk request is outside the offered artifact bounds".to_string(),
));
}
validate_metadata_size(self)
}
}
impl ArtifactChunkResponseV2 {
pub fn validate_against(
&self,
request: &ArtifactChunkRequestV2,
total_size_bytes: u64,
) -> UpdateResult<()> {
validate_digest(&self.sha256)?;
validate_digest(&self.chunk_sha256)?;
if self.sha256 != request.sha256
|| self.offset != request.offset
|| self.len == 0
|| self.len > request.len
|| self.total_size_bytes != total_size_bytes
|| self.offset.saturating_add(self.len as u64) > total_size_bytes
{
return Err(UpdateError::Transfer(
"peer chunk response does not repeat the requested bounds".to_string(),
));
}
validate_metadata_size(self)
}
}
fn validate_digest(value: &str) -> UpdateResult<()> {
if value.len() != 64
|| !value
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
{
return Err(UpdateError::Transfer(
"peer update digest must be lowercase SHA-256".to_string(),
));
}
Ok(())
}
fn validate_protocol(value: &str) -> UpdateResult<()> {
if value.trim().is_empty() || value.len() > 64 || value.chars().any(char::is_control) {
return Err(UpdateError::Incompatible(
"peer update protocol version is invalid".to_string(),
));
}
Ok(())
}
fn validate_version(name: &str, value: &str) -> UpdateResult<()> {
validate_text(name, value, 64)?;
semver::Version::parse(value)
.map(|_| ())
.map_err(|error| UpdateError::Incompatible(format!("{name} is invalid: {error}")))
}
fn validate_text(name: &str, value: &str, max: usize) -> UpdateResult<()> {
if value.trim().is_empty() || value.len() > max || value.chars().any(char::is_control) {
return Err(UpdateError::Incompatible(format!(
"peer update {name} is invalid"
)));
}
Ok(())
}
fn validate_target(target: &ArtifactTarget) -> UpdateResult<()> {
for (name, value) in [
("target.os", target.os()),
("target.architecture", target.architecture()),
("target.format", target.format()),
] {
validate_text(name, value, 64)?;
}
Ok(())
}
fn validate_metadata_size<T: Serialize>(value: &T) -> UpdateResult<()> {
let size = serde_json::to_vec(value)
.map_err(|error| UpdateError::Transfer(error.to_string()))?
.len();
if size > UPDATE_PEER_METADATA_MAX_BYTES {
return Err(UpdateError::Transfer(
"peer update metadata exceeds configured limit".to_string(),
));
}
Ok(())
}