1use 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
19pub const DEFAULT_ARTIFACT_CHUNK_BYTES: usize = 64 * 1024;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct ArtifactTransferPolicy {
25 pub max_chunk_bytes: usize,
27}
28
29impl ArtifactTransferPolicy {
30 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
49pub trait ArtifactSource: Send + Sync {
51 fn read_chunk(
53 &self,
54 descriptor: &ArtifactDescriptor,
55 offset: u64,
56 max_len: usize,
57 ) -> UpdateResult<Vec<u8>>;
58}
59
60pub trait ArtifactWriter {
62 fn write_chunk(&mut self, offset: u64, bytes: &[u8]) -> UpdateResult<()>;
64}
65
66pub 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#[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}