1#![allow(clippy::result_large_err)]23use std::{4 collections::HashSet,5 fs::{self, File},6 io::Write as _,7 path::{Path, PathBuf},8};910use reqwest::{blocking::Response, header};11use tracing::{debug, info};1213use super::{14 Error, LocalExtraction, ResolveResult, Result, VendorSource,15 accessor::{AccessorEntry, ZipFileAccessor},16};17use crate::{18 install::{PKG_USER_AGENT, cache_dir},19 jsonnet_bundler::{Dependency, GitSource, JsonnetFile, Source, SubDir},20};2122fn is_sha(s: &str) -> bool {23 s.len() == 40 && s.bytes().all(|b| b.is_ascii_hexdigit())24}2526fn commit_cache_path(source: &GitSource, sha: &str) -> Result<PathBuf> {27 Ok(cache_dir("github")?28 .join(source.plain_repo_name())29 .join(format!("{sha}.zip")))30}3132fn resolve_sha(source: &GitSource, version: &str) -> Result<String> {33 let url = format!(34 "https://api.github.com/repos/{}/commits/{}",35 source.plain_repo_name(),36 version37 );38 let response = reqwest::blocking::Client::new()39 .get(&url)40 .header(header::ACCEPT, "application/vnd.github.sha")41 .header(header::USER_AGENT, PKG_USER_AGENT)42 .send()43 .and_then(Response::error_for_status)?;44 let sha = response.text()?;45 Ok(sha.trim().to_owned())46}4748fn fetch_zip(source: &GitSource, sha: &str) -> Result<ZipFileAccessor> {49 let cached = commit_cache_path(source, sha)?;50 if cached.exists() {51 debug!("using cached archive {}", cached.display());52 return Ok(ZipFileAccessor::new_prefixed(53 File::open(&cached).map_err(|e| Error::Io(cached.clone(), e))?,54 )?);55 }5657 let url = format!(58 "https://github.com/{}/archive/{}.zip",59 source.plain_repo_name(),60 sha61 );62 info!("downloading {url}");6364 let bytes = reqwest::blocking::Client::new()65 .get(&url)66 .header(header::USER_AGENT, PKG_USER_AGENT)67 .send()68 .and_then(Response::error_for_status)?69 .bytes()?;7071 if let Some(parent) = cached.parent() {72 fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;73 }74 let mut downloaded = File::create_new(&cached).map_err(|e| Error::Io(cached.clone(), e))?;75 downloaded76 .write_all(&bytes)77 .map_err(|e| Error::Io(cached.clone(), e))?;7879 Ok(ZipFileAccessor::new_prefixed(downloaded)?)80}8182fn open_cached_zip(zip_path: &Path) -> Result<ZipFileAccessor> {83 Ok(ZipFileAccessor::new_prefixed(84 File::open(zip_path).map_err(|e| Error::Io(zip_path.to_owned(), e))?,85 )?)86}8788fn extract_subdir(archive: &ZipFileAccessor, subdir: &SubDir, dest: &Path) -> Result<()> {89 archive.iter(subdir, &mut |name, entry| {90 let target = dest.join(name);91 match entry {92 AccessorEntry::Dir => {93 fs::create_dir_all(&target).map_err(|e| Error::Io(target, e))?;94 }95 AccessorEntry::File(data) => {96 if let Some(parent) = target.parent() {97 fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;98 }99 fs::write(&target, &data).map_err(|e| Error::Io(target, e))?;100 }101 }102 Ok(())103 })104}105106fn collect_archive_deps(107 archive: &ZipFileAccessor,108 dir: &SubDir,109 git_deps: &mut Vec<Dependency>,110 local_extractions: &mut Vec<LocalExtraction>,111 visited: &mut HashSet<SubDir>,112) -> Result<()> {113 if !visited.insert(dir.clone()) {114 return Ok(());115 }116117 let manifest_path = dir118 .join("jsonnetfile.json")119 .expect("appending a literal filename keeps it within parent");120121 let Some(data) = archive.read(&manifest_path)? else {122 return Ok(());123 };124 let Ok(manifest) = serde_json::from_slice::<JsonnetFile>(&data) else {125 return Ok(());126 };127128 for dep in manifest.dependencies {129 match &dep.source {130 Source::Git(_) => git_deps.push(dep),131 Source::Local(local) => {132 let Ok(child_dir) = local.resolve_under(dir) else {133 tracing::info!("local source {local} escapes its package; skipping");134 continue;135 };136 let name = child_dir137 .file_name()138 .map_or_else(|| local.to_string(), str::to_owned);139 local_extractions.push(LocalExtraction {140 tree_path: child_dir.clone(),141 name,142 });143 collect_archive_deps(archive, &child_dir, git_deps, local_extractions, visited)?;144 }145 }146 }147 Ok(())148}149150pub(super) fn resolve(source: &GitSource, version: Option<&str>) -> Result<ResolveResult> {151 let version_str = version.unwrap_or("HEAD");152 let sha = if is_sha(version_str) {153 version_str.to_owned()154 } else {155 let resolved = resolve_sha(source, version_str)?;156 info!("resolved {version_str} to {resolved}");157 resolved158 };159160 let archive = fetch_zip(source, &sha)?;161162 let mut transitive_git_deps = Vec::new();163 let mut local_extractions = Vec::new();164 let mut visited = HashSet::new();165 collect_archive_deps(166 &archive,167 &source.subdir,168 &mut transitive_git_deps,169 &mut local_extractions,170 &mut visited,171 )?;172173 let zip_path = commit_cache_path(source, &sha)?;174175 Ok(ResolveResult {176 version: sha.clone(),177 transitive_git_deps,178 local_extractions,179 source: VendorSource::GithubZip {180 zip_path,181 commit_sha: sha,182 subdir: source.subdir.clone(),183 },184 })185}186187pub(super) fn extract(zip_path: &Path, subdir: &SubDir, dest: &Path) -> Result<()> {188 let archive = open_cached_zip(zip_path)?;189 extract_subdir(&archive, subdir, dest)190}