1use crate::args;
2use crate::container::{self, Container};
3use crate::errors::*;
4use crate::http;
5use crate::lockfile::{ContainerLock, PackageLock};
6use crate::manifest::PackagesManifest;
7use crate::paths;
8use crate::utils;
9use data_encoding::BASE64;
10use flate2::bufread::GzDecoder;
11use sha1::Sha1;
12use sha2::{Digest, Sha256};
13use std::collections::{HashMap, HashSet};
14use std::io::{BufRead, BufReader, Read};
15use std::rc::Rc;
16use tokio::fs;
17
18pub fn decode_apk_checksum(checksum: &str) -> Result<Vec<u8>> {
19 let checksum = checksum
20 .strip_prefix("Q1")
21 .with_context(|| anyhow!("Only checksums starting with Q1 are supported: {checksum:?}"))?;
22 let checksum = BASE64
23 .decode(checksum.as_bytes())
24 .context("Failed to decode checksum as base64")?;
25 Ok(checksum)
26}
27
28#[derive(Debug, Default)]
29pub struct DatabaseCache {
30 repos: HashMap<String, Rc<String>>,
31 pkgs: HashMap<String, CacheEntry>,
32}
33
34#[derive(Debug)]
35pub struct CacheEntry {
36 name: String,
37 version: String,
38 arch: String,
39 provides: Vec<String>,
40 checksum: String,
41 repo_url: Rc<String>,
42}
43
44pub struct CacheEntryDraft {
45 pub name: Option<String>,
46 pub version: Option<String>,
47 pub arch: Option<String>,
48 pub provides: Vec<String>,
49 pub checksum: Option<String>,
50 pub repo_url: Rc<String>,
51}
52
53impl TryFrom<CacheEntryDraft> for CacheEntry {
54 type Error = Error;
55
56 fn try_from(draft: CacheEntryDraft) -> Result<Self> {
57 Ok(Self {
58 name: draft.name.context("Missing name field")?,
59 version: draft.version.context("Missing version field")?,
60 arch: draft.arch.context("Missing arch field")?,
61 provides: draft.provides,
62 checksum: draft.checksum.context("Missing checksum field")?,
63 repo_url: draft.repo_url,
64 })
65 }
66}
67
68impl CacheEntryDraft {
69 pub fn new(repo_url: Rc<String>) -> Self {
70 CacheEntryDraft {
71 name: None,
72 version: None,
73 arch: None,
74 provides: vec![],
75 checksum: None,
76 repo_url,
77 }
78 }
79}
80
81impl DatabaseCache {
82 pub fn get(&self, id: &str) -> Result<&CacheEntry> {
83 let entry = self
84 .pkgs
85 .get(id)
86 .context("Failed to find package database entry for: {id:?}")?;
87 Ok(entry)
88 }
89
90 pub fn read_apkindex_text<R: Read>(&mut self, r: R, repo_url: &Rc<String>) -> Result<()> {
91 let reader = BufReader::new(r);
92 let mut draft = CacheEntryDraft::new(repo_url.clone());
93 for line in reader.lines() {
94 let line = line?;
95 if line.is_empty() {
96 let mut new = CacheEntryDraft::new(repo_url.clone());
97 (new, draft) = (draft, new);
98 let pkg = CacheEntry::try_from(new)?;
99 let id = format!("{}-{}", pkg.name, pkg.version);
100 trace!("Inserting pkg into lookup table: {id:?} => {pkg:?}");
101 self.pkgs.insert(id, pkg);
102 } else if let Some((key, value)) = line.split_once(':') {
103 match key {
104 "P" => {
105 trace!("Package name: {value:?}");
106 draft.name = Some(value.to_string());
107 }
108 "V" => {
109 trace!("Package version: {value:?}");
110 draft.version = Some(value.to_string());
111 }
112 "C" => {
113 trace!("Package checksum: {value:?}");
114 let checksum = decode_apk_checksum(value)?;
115 draft.checksum = Some(hex::encode(checksum));
116 }
117 "A" => {
118 trace!("Package architecture: {value:?}");
119 draft.arch = Some(value.to_string());
120 }
121 "p" => {
122 trace!("Package provides: {value:?}");
123 for entry in value.split(' ') {
124 let (name, _) = entry.split_once('=').unwrap_or((entry, ""));
125 draft.provides.push(name.to_string());
126 }
127 }
128 _ => trace!("Ignoring APKINDEX value key={key:?}, value={value:?}"),
129 }
130 } else {
131 bail!("Invalid line in index: {line:?}");
132 }
133 }
134 Ok(())
135 }
136
137 pub fn read_apkindex_container<R: Read>(&mut self, r: R, repo_url: &Rc<String>) -> Result<()> {
138 let mut r = BufReader::new(r);
139 utils::read_gzip_to_end(&mut r).context("Failed to strip signature")?;
140
141 let gz = GzDecoder::new(r);
142 let mut tar = tar::Archive::new(gz);
143
144 for entry in tar.entries()? {
145 let entry = entry?;
146 if entry.header().entry_type() == tar::EntryType::Regular {
147 let path = entry.path()?;
148 if path.to_str() == Some("APKINDEX") {
149 self.read_apkindex_text(entry, repo_url)?;
150 }
151 }
152 }
153
154 Ok(())
155 }
156
157 pub fn import_from_container(&mut self, buf: &[u8]) -> Result<()> {
158 let mut tar = tar::Archive::new(buf);
159
160 for entry in tar.entries()? {
161 let entry = entry?;
162 if entry.header().entry_type() == tar::EntryType::Regular {
163 let path = entry.path()?;
164 let file_name = path
165 .file_name()
166 .context("Failed to detect filename")?
167 .to_str()
168 .unwrap_or("");
169 if let Some(repo_url) = self.repos.get(file_name).cloned() {
170 debug!("Reading package index for repository: {repo_url:?} ({file_name:?})");
171 self.read_apkindex_container(entry, &repo_url)?;
172 }
173 }
174 }
175
176 Ok(())
177 }
178
179 pub fn register_repo(&mut self, repo: String) {
180 let mut hasher = Sha1::new();
181 hasher.update(&repo);
182 let hash = hasher.finalize();
183 let sha1 = hex::encode(&hash[..4]);
184 self.repos
185 .insert(format!("APKINDEX.{sha1}.tar.gz"), Rc::new(repo));
186 }
187
188 pub fn init_repos_from_container(&mut self, buf: &[u8]) -> Result<()> {
189 let mut tar = tar::Archive::new(buf);
190 for entry in tar.entries()? {
191 let entry = entry?;
192 if entry.header().entry_type() == tar::EntryType::Regular {
193 let reader = BufReader::new(entry);
194 for repo in reader.lines() {
195 let repo = repo?;
196 debug!("Found repository in /etc/apk/repositories: {repo:?}");
197 self.register_repo(repo);
198 }
199 }
200 }
201 Ok(())
202 }
203}
204
205pub fn calculate_checksum_for_apk(apk: &[u8]) -> Result<Vec<u8>> {
206 let remaining = {
208 let gz = GzDecoder::new(apk);
209 let mut tar = tar::Archive::new(gz);
210 tar.entries()?.next();
211 tar.into_inner().into_inner()
212 };
213
214 let sig = apk.len() - remaining.len() + 8;
216
217 let mut r = &apk[sig..];
219 utils::read_gzip_to_end(&mut r)?;
220 let content = r.len();
221
222 let control_data = &apk[sig..(apk.len() - content)];
224
225 let mut sha1 = Sha1::new();
226 sha1.update(control_data);
227 let sha1 = sha1.finalize();
228 Ok(sha1.to_vec())
229}
230
231pub async fn detect_installed(container: &Container) -> Result<HashSet<String>> {
232 let buf = container
233 .exec(
234 &["apk", "info", "-v"],
235 container::Exec {
236 capture_stdout: true,
237 ..Default::default()
238 },
239 )
240 .await?;
241 let buf = String::from_utf8(buf).context("Failed to decode apk output as utf8")?;
242
243 let installed = buf.lines().map(String::from).collect();
244 Ok(installed)
245}
246
247pub async fn resolve_dependencies(
248 container: &Container,
249 manifest: &PackagesManifest,
250 dependencies: &mut Vec<PackageLock>,
251) -> Result<()> {
252 info!("Syncing package datatabase...");
253 container
254 .exec(&["apk", "update"], container::Exec::default())
255 .await?;
256
257 let mut dbs = DatabaseCache::default();
258 {
259 let repos = container.tar("/etc/apk/repositories").await?;
261 dbs.init_repos_from_container(&repos)?;
262
263 let tar = container.tar("/var/cache/apk").await?;
264 dbs.import_from_container(&tar)?;
265 }
266
267 info!("Resolving dependencies...");
268 let initial_packages = detect_installed(container).await?;
269
270 container
272 .exec(&["apk", "upgrade"], container::Exec::default())
273 .await?;
274
275 let mut cmd = vec!["apk", "add", "--"];
276 for dep in &manifest.dependencies {
277 cmd.push(dep.as_str());
278 }
279 container.exec(&cmd, container::Exec::default()).await?;
280
281 let packages_afterwards = detect_installed(container).await?;
283 let new_packages = packages_afterwards.difference(&initial_packages);
284
285 info!("Calculating package checksums...");
286 let client = http::Client::new()?;
287 let alpine_cache_dir = paths::alpine_cache_dir()?;
288 for pkg_identifier in new_packages {
289 let pkg = dbs.get(pkg_identifier)?;
290 debug!("Detected dependency: {pkg:?}");
291
292 let url = format!(
293 "{}/{}/{}-{}.apk",
294 pkg.repo_url, pkg.arch, pkg.name, pkg.version
295 );
296
297 let sha256 = if let Some(sha256) = alpine_cache_dir.sha1_read_link(&pkg.checksum).await? {
298 sha256
299 } else {
300 let mut buf = Vec::new();
301
302 let mut response = client
303 .request(&url)
304 .await
305 .with_context(|| anyhow!("Failed to download package from url: {:?}", url))?;
306
307 let mut sha256 = Sha256::new();
308 while let Some(chunk) = response
309 .chunk()
310 .await
311 .context("Failed to read from download stream")?
312 {
313 buf.extend(&chunk);
314 sha256.update(&chunk);
315 }
316
317 let sha256 = hex::encode(sha256.finalize());
318 let sha1 = hex::encode(&calculate_checksum_for_apk(&buf)?);
319
320 if sha1 != pkg.checksum {
321 bail!("Downloaded package (checksum={sha1:?} does not match checksum in APKINDEX (checksum={:?})",
322 pkg.checksum
323 );
324 }
325
326 let (sha1_path, sha256_path) =
327 alpine_cache_dir.sha1_to_sha256(&pkg.checksum, &sha256)?;
328
329 let parent = sha1_path
330 .parent()
331 .context("Failed to determine parent directory")?;
332 fs::create_dir_all(parent).await.with_context(|| {
333 anyhow!("Failed to create parent directories for file: {sha1_path:?}")
334 })?;
335
336 fs::symlink(sha256_path, sha1_path)
337 .await
338 .context("Failed to create sha1 symlink")?;
339
340 sha256
341 };
342
343 let mut provides = Vec::new();
345 for value in &pkg.provides {
346 if manifest.dependencies.contains(value) {
347 provides.push(value.to_string());
348 }
349 }
350
351 dependencies.push(PackageLock {
352 name: pkg.name.to_string(),
353 version: pkg.version.to_string(),
354 system: "alpine".to_string(),
355 url,
356 provides,
357 sha256,
358 signature: None,
359 installed: false,
360 });
361 }
362
363 Ok(())
364}
365
366pub async fn resolve(
367 update: &args::Update,
368 manifest: &PackagesManifest,
369 container: &ContainerLock,
370 dependencies: &mut Vec<PackageLock>,
371) -> Result<()> {
372 let container = Container::create(
373 &container.image,
374 container::Config {
375 mounts: &[],
376 expose_fuse: false,
377 },
378 )
379 .await?;
380 container
381 .run(
382 resolve_dependencies(&container, manifest, dependencies),
383 update.keep,
384 )
385 .await
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn test_checksum_from_apk() -> Result<()> {
394 let checksum = decode_apk_checksum("Q10cGs1h9J5440p6BRXhZC8FO7pVg=")?;
395 let calculated = calculate_checksum_for_apk(crate::test_data::ALPINE_APK_EXAMPLE)?;
396 assert_eq!(checksum, calculated);
397 Ok(())
398 }
399}