git.delta.rocks / jrsonnet / refs/commits / 97dec27e23cc

difftreelog

feat(jrb) symlinks

kmklkzzmYaroslav Bolyukin2026-05-07parent: #6710888.patch.diff
in: master

2 files changed

modifiedcrates/jrsonnet-pkg/src/install/accessor.rsdiffbeforeafterboth
--- a/crates/jrsonnet-pkg/src/install/accessor.rs
+++ b/crates/jrsonnet-pkg/src/install/accessor.rs
@@ -9,7 +9,7 @@
 use tracing::warn;
 use zip::{ZipArchive, result::ZipError};
 
-use crate::jsonnet_bundler::{SubDir, SubDirEscapeError};
+use crate::jsonnet_bundler::{LocalSource, SubDir, SubDirEscapeError};
 
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
@@ -105,13 +105,25 @@
 				name.clone(),
 				if entry.is_dir() {
 					AccessorEntry::Dir
+				} else if entry.is_symlink() {
+					let mut target = Vec::new();
+					entry.read_to_end(&mut target).map_err(Error::ZipIo)?;
+					let Ok(target_str) = std::str::from_utf8(&target) else {
+						warn!("non-utf8 symlink target in zip entry: {name:?}");
+						continue;
+					};
+					let Ok(target) = LocalSource::from_str(target_str) else {
+						warn!("symlink target {target_str:?} at {name:?} escapes sandbox; skipping");
+						continue;
+					};
+					AccessorEntry::Symlink(target)
 				} else if entry.is_file() {
 					let mut data = Vec::new();
 					entry.read_to_end(&mut data).map_err(Error::ZipIo)?;
 					AccessorEntry::File(data)
 				} else {
-					// TODO: Symlinks?
-					panic!("unknown accessor entry type: {name:?}")
+					warn!("unknown accessor entry type: {name:?}");
+					continue;
 				},
 			)?;
 		}
@@ -133,4 +145,5 @@
 pub enum AccessorEntry {
 	Dir,
 	File(Vec<u8>),
+	Symlink(LocalSource),
 }
modifiedcrates/jrsonnet-pkg/src/install/github.rsdiffbeforeafterboth
before · crates/jrsonnet-pkg/src/install/github.rs
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}