Skip to main content

appcore_update/
stream.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: stream.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/09/24 00:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/09/24 00:00:00 by dnettoRaw
8//      ###########      S: 1.0.3-rc
9// =============================================================================
10
11//! Bounded streaming contracts for opaque artifact transfer.
12
13use crate::filesystem::open_regular_file;
14use crate::{ArtifactDescriptor, UpdateError, UpdateResult};
15use sha2::{Digest, Sha256};
16use std::io::{Read, Seek, SeekFrom};
17use std::path::Path;
18
19/// Conservative default maximum decoded chunk size.
20pub const DEFAULT_ARTIFACT_CHUNK_BYTES: usize = 64 * 1024;
21
22/// Transfer limits applied before reading or allocating a chunk.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct ArtifactTransferPolicy {
25    /// Maximum bytes accepted in one decoded chunk.
26    pub max_chunk_bytes: usize,
27}
28
29impl ArtifactTransferPolicy {
30    /// Creates a transfer policy with an explicit positive chunk bound.
31    pub fn new(max_chunk_bytes: usize) -> UpdateResult<Self> {
32        if max_chunk_bytes == 0 {
33            return Err(UpdateError::Transfer(
34                "max_chunk_bytes must be greater than zero".to_string(),
35            ));
36        }
37        Ok(Self { max_chunk_bytes })
38    }
39}
40
41impl Default for ArtifactTransferPolicy {
42    fn default() -> Self {
43        Self {
44            max_chunk_bytes: DEFAULT_ARTIFACT_CHUNK_BYTES,
45        }
46    }
47}
48
49/// Reads bounded chunks for one immutable artifact.
50pub trait ArtifactSource: Send + Sync {
51    /// Reads at most `max_len` bytes beginning at `offset`.
52    fn read_chunk(
53        &self,
54        descriptor: &ArtifactDescriptor,
55        offset: u64,
56        max_len: usize,
57    ) -> UpdateResult<Vec<u8>>;
58}
59
60/// Receives chunks without activating or installing the artifact.
61pub trait ArtifactWriter {
62    /// Persists one chunk at its exact stream offset.
63    fn write_chunk(&mut self, offset: u64, bytes: &[u8]) -> UpdateResult<()>;
64}
65
66/// Receives and verifies one complete artifact through a bounded stream.
67pub fn receive_artifact(
68    policy: &ArtifactTransferPolicy,
69    descriptor: &ArtifactDescriptor,
70    source: &dyn ArtifactSource,
71    writer: &mut dyn ArtifactWriter,
72) -> UpdateResult<()> {
73    receive_artifact_from(policy, descriptor, source, writer, 0, Sha256::new())
74}
75
76pub(crate) fn receive_artifact_from(
77    policy: &ArtifactTransferPolicy,
78    descriptor: &ArtifactDescriptor,
79    source: &dyn ArtifactSource,
80    writer: &mut dyn ArtifactWriter,
81    start_offset: u64,
82    mut hasher: Sha256,
83) -> UpdateResult<()> {
84    descriptor.validate()?;
85    if policy.max_chunk_bytes == 0 {
86        return Err(UpdateError::Transfer(
87            "max_chunk_bytes must be greater than zero".to_string(),
88        ));
89    }
90    if start_offset > descriptor.size_bytes() {
91        return Err(UpdateError::Transfer(
92            "artifact start offset exceeds declared size".to_string(),
93        ));
94    }
95    let mut offset = start_offset;
96    while offset < descriptor.size_bytes() {
97        let remaining = descriptor.size_bytes().checked_sub(offset).ok_or_else(|| {
98            UpdateError::Transfer("artifact offset exceeds declared size".to_string())
99        })?;
100        let requested = usize::try_from(remaining)
101            .unwrap_or(policy.max_chunk_bytes)
102            .min(policy.max_chunk_bytes);
103        let chunk = source.read_chunk(descriptor, offset, requested)?;
104        if chunk.is_empty() {
105            return Err(UpdateError::Transfer(
106                "artifact source ended before the declared size".to_string(),
107            ));
108        }
109        if chunk.len() > requested {
110            return Err(UpdateError::Transfer(
111                "artifact source returned more bytes than requested".to_string(),
112            ));
113        }
114        let chunk_len = u64::try_from(chunk.len()).map_err(|_| {
115            UpdateError::Transfer("artifact chunk length overflows offset".to_string())
116        })?;
117        let next_offset = offset
118            .checked_add(chunk_len)
119            .ok_or_else(|| UpdateError::Transfer("artifact offset overflows".to_string()))?;
120        if next_offset > descriptor.size_bytes() {
121            return Err(UpdateError::Transfer(
122                "artifact source exceeded the declared size".to_string(),
123            ));
124        }
125        writer.write_chunk(offset, &chunk)?;
126        hasher.update(&chunk);
127        offset = next_offset;
128    }
129    if encode_hex(&hasher.finalize()) != descriptor.sha256() {
130        return Err(UpdateError::ChecksumMismatch);
131    }
132    Ok(())
133}
134
135/// Local file source for bounded artifact transfer.
136#[derive(Debug, Clone, Copy, Default)]
137pub struct FileArtifactSource;
138
139impl ArtifactSource for FileArtifactSource {
140    fn read_chunk(
141        &self,
142        descriptor: &ArtifactDescriptor,
143        offset: u64,
144        max_len: usize,
145    ) -> UpdateResult<Vec<u8>> {
146        if max_len == 0 || max_len > DEFAULT_ARTIFACT_CHUNK_BYTES {
147            return Err(UpdateError::Transfer(
148                "requested chunk exceeds the local source limit".to_string(),
149            ));
150        }
151        if offset >= descriptor.size_bytes() {
152            return Err(UpdateError::Transfer(
153                "artifact offset is outside the declared size".to_string(),
154            ));
155        }
156        let path = descriptor
157            .artifact_reference()
158            .strip_prefix("file:")
159            .ok_or_else(|| {
160                UpdateError::Provider("file source requires a file: reference".to_string())
161            })?;
162        let mut file = open_regular_file(Path::new(path))
163            .map_err(|error| UpdateError::Provider(error.to_string()))?;
164        let length = file
165            .metadata()
166            .map_err(|error| UpdateError::Provider(error.to_string()))?
167            .len();
168        if length != descriptor.size_bytes() {
169            return Err(UpdateError::ChecksumMismatch);
170        }
171        file.seek(SeekFrom::Start(offset))
172            .map_err(|error| UpdateError::Provider(error.to_string()))?;
173        let remaining = descriptor.size_bytes() - offset;
174        let capacity = usize::try_from(remaining).unwrap_or(max_len).min(max_len);
175        let mut bytes = vec![0_u8; capacity];
176        file.read_exact(&mut bytes)
177            .map_err(|error| UpdateError::Provider(error.to_string()))?;
178        Ok(bytes)
179    }
180}
181
182fn encode_hex(bytes: &[u8]) -> String {
183    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
184}