difftreelog
feat(jrb) symlinks
in: master
2 files changed
crates/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),
}
crates/jrsonnet-pkg/src/install/github.rsdiffbeforeafterboth1#![allow(clippy::result_large_err)]23use std::{4 collections::HashSet,5 fs::{self, File},6 io::Write as _,7 path::{Path, PathBuf},8};910use camino::Utf8PathBuf;11use reqwest::{blocking::Response, header};12use tracing::{debug, info, warn};1314use super::{15 Error, LocalExtraction, ResolveResult, Result, VendorSource,16 accessor::{AccessorEntry, ZipFileAccessor},17};18use crate::{19 install::{PKG_USER_AGENT, cache_dir},20 jsonnet_bundler::{Dependency, GitSource, JsonnetFile, Source, SubDir},21};2223fn is_sha(s: &str) -> bool {24 s.len() == 40 && s.bytes().all(|b| b.is_ascii_hexdigit())25}2627fn commit_cache_path(source: &GitSource, sha: &str) -> Result<PathBuf> {28 Ok(cache_dir("github")?29 .join(source.plain_repo_name())30 .join(format!("{sha}.zip")))31}3233fn resolve_sha(source: &GitSource, version: &str) -> Result<String> {34 let url = format!(35 "https://api.github.com/repos/{}/commits/{}",36 source.plain_repo_name(),37 version38 );39 let response = reqwest::blocking::Client::new()40 .get(&url)41 .header(header::ACCEPT, "application/vnd.github.sha")42 .header(header::USER_AGENT, PKG_USER_AGENT)43 .send()44 .and_then(Response::error_for_status)?;45 let sha = response.text()?;46 Ok(sha.trim().to_owned())47}4849fn fetch_zip(source: &GitSource, sha: &str) -> Result<ZipFileAccessor> {50 let cached = commit_cache_path(source, sha)?;51 if cached.exists() {52 debug!("using cached archive {}", cached.display());53 return Ok(ZipFileAccessor::new_prefixed(54 File::open(&cached).map_err(|e| Error::Io(cached.clone(), e))?,55 )?);56 }5758 let url = format!(59 "https://github.com/{}/archive/{}.zip",60 source.plain_repo_name(),61 sha62 );63 info!("downloading {url}");6465 let bytes = reqwest::blocking::Client::new()66 .get(&url)67 .header(header::USER_AGENT, PKG_USER_AGENT)68 .send()69 .and_then(Response::error_for_status)?70 .bytes()?;7172 if let Some(parent) = cached.parent() {73 fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;74 }75 let mut downloaded = File::create_new(&cached).map_err(|e| Error::Io(cached.clone(), e))?;76 downloaded77 .write_all(&bytes)78 .map_err(|e| Error::Io(cached.clone(), e))?;7980 Ok(ZipFileAccessor::new_prefixed(downloaded)?)81}8283fn open_cached_zip(zip_path: &Path) -> Result<ZipFileAccessor> {84 Ok(ZipFileAccessor::new_prefixed(85 File::open(zip_path).map_err(|e| Error::Io(zip_path.to_owned(), e))?,86 )?)87}8889#[cfg(unix)]90fn make_symlink(target: &str, link: &Path) -> std::io::Result<()> {91 std::os::unix::fs::symlink(target, link)92}9394#[cfg(windows)]95fn make_symlink(target: &str, link: &Path) -> std::io::Result<()> {96 std::os::windows::fs::symlink_file(target, link)97}9899#[cfg(not(any(unix, windows)))]100fn make_symlink(_target: &str, _link: &Path) -> std::io::Result<()> {101 Err(std::io::Error::new(102 std::io::ErrorKind::Unsupported,103 "symlinks are not supported on this platform",104 ))105}106107fn extract_subdir(archive: &ZipFileAccessor, subdir: &SubDir, dest: &Path) -> Result<()> {108 archive.iter(subdir, &mut |name, entry| {109 let target = dest.join(&name);110 match entry {111 AccessorEntry::Dir => {112 fs::create_dir_all(&target).map_err(|e| Error::Io(target, e))?;113 }114 AccessorEntry::File(data) => {115 if let Some(parent) = target.parent() {116 fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;117 }118 fs::write(&target, &data).map_err(|e| Error::Io(target, e))?;119 }120 AccessorEntry::Symlink(link_target) => {121 let symlink_parent = name122 .as_path()123 .parent()124 .map(|p| SubDir::try_from(Utf8PathBuf::from(p)))125 .transpose()126 .expect("parent of a SubDir is a SubDir")127 .unwrap_or_else(SubDir::empty);128 if link_target.resolve_under(&symlink_parent).is_err() {129 warn!("symlink {name} -> {link_target} escapes extraction; skipping");130 return Ok(());131 }132 if let Some(parent) = target.parent() {133 fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;134 }135 make_symlink(&link_target.to_string(), &target)136 .map_err(|e| Error::Io(target, e))?;137 }138 }139 Ok(())140 })141}142143fn collect_archive_deps(144 archive: &ZipFileAccessor,145 dir: &SubDir,146 git_deps: &mut Vec<Dependency>,147 local_extractions: &mut Vec<LocalExtraction>,148 visited: &mut HashSet<SubDir>,149) -> Result<()> {150 if !visited.insert(dir.clone()) {151 return Ok(());152 }153154 let manifest_path = dir155 .join("jsonnetfile.json")156 .expect("appending a literal filename keeps it within parent");157158 let Some(data) = archive.read(&manifest_path)? else {159 return Ok(());160 };161 let Ok(manifest) = serde_json::from_slice::<JsonnetFile>(&data) else {162 return Ok(());163 };164165 for dep in manifest.dependencies {166 match &dep.source {167 Source::Git(_) => git_deps.push(dep),168 Source::Local(local) => {169 let Ok(child_dir) = local.resolve_under(dir) else {170 tracing::info!("local source {local} escapes its package; skipping");171 continue;172 };173 let name = child_dir174 .file_name()175 .map_or_else(|| local.to_string(), str::to_owned);176 local_extractions.push(LocalExtraction {177 tree_path: child_dir.clone(),178 name,179 });180 collect_archive_deps(archive, &child_dir, git_deps, local_extractions, visited)?;181 }182 }183 }184 Ok(())185}186187pub(super) fn resolve(source: &GitSource, version: Option<&str>) -> Result<ResolveResult> {188 let version_str = version.unwrap_or("HEAD");189 let sha = if is_sha(version_str) {190 version_str.to_owned()191 } else {192 let resolved = resolve_sha(source, version_str)?;193 info!("resolved {version_str} to {resolved}");194 resolved195 };196197 let archive = fetch_zip(source, &sha)?;198199 let mut transitive_git_deps = Vec::new();200 let mut local_extractions = Vec::new();201 let mut visited = HashSet::new();202 collect_archive_deps(203 &archive,204 &source.subdir,205 &mut transitive_git_deps,206 &mut local_extractions,207 &mut visited,208 )?;209210 let zip_path = commit_cache_path(source, &sha)?;211212 Ok(ResolveResult {213 version: sha.clone(),214 transitive_git_deps,215 local_extractions,216 source: VendorSource::GithubZip {217 zip_path,218 commit_sha: sha,219 subdir: source.subdir.clone(),220 },221 })222}223224pub(super) fn extract(zip_path: &Path, subdir: &SubDir, dest: &Path) -> Result<()> {225 let archive = open_cached_zip(zip_path)?;226 extract_subdir(&archive, subdir, dest)227}