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
9use tracing::warn;9use tracing::warn;
10use zip::{ZipArchive, result::ZipError};10use zip::{ZipArchive, result::ZipError};
1111
12use crate::jsonnet_bundler::{SubDir, SubDirEscapeError};12use crate::jsonnet_bundler::{LocalSource, SubDir, SubDirEscapeError};
1313
14#[derive(thiserror::Error, Debug)]14#[derive(thiserror::Error, Debug)]
15pub enum Error {15pub enum Error {
105 name.clone(),105 name.clone(),
106 if entry.is_dir() {106 if entry.is_dir() {
107 AccessorEntry::Dir107 AccessorEntry::Dir
108 } else if entry.is_file() {108 } else if entry.is_symlink() {
109 let mut target = Vec::new();
110 entry.read_to_end(&mut target).map_err(Error::ZipIo)?;
111 let Ok(target_str) = std::str::from_utf8(&target) else {
112 warn!("non-utf8 symlink target in zip entry: {name:?}");
113 continue;
114 };
115 let Ok(target) = LocalSource::from_str(target_str) else {
116 warn!("symlink target {target_str:?} at {name:?} escapes sandbox; skipping");
117 continue;
118 };
119 AccessorEntry::Symlink(target)
120 } else if entry.is_file() {
109 let mut data = Vec::new();121 let mut data = Vec::new();
110 entry.read_to_end(&mut data).map_err(Error::ZipIo)?;122 entry.read_to_end(&mut data).map_err(Error::ZipIo)?;
111 AccessorEntry::File(data)123 AccessorEntry::File(data)
112 } else {124 } else {
113 // TODO: Symlinks?
114 panic!("unknown accessor entry type: {name:?}")125 warn!("unknown accessor entry type: {name:?}");
126 continue;
115 },127 },
116 )?;128 )?;
117 }129 }
133pub enum AccessorEntry {145pub enum AccessorEntry {
134 Dir,146 Dir,
135 File(Vec<u8>),147 File(Vec<u8>),
148 Symlink(LocalSource),
136}149}
137150
modifiedcrates/jrsonnet-pkg/src/install/github.rsdiffbeforeafterboth
--- a/crates/jrsonnet-pkg/src/install/github.rs
+++ b/crates/jrsonnet-pkg/src/install/github.rs
@@ -7,8 +7,9 @@
 	path::{Path, PathBuf},
 };
 
+use camino::Utf8PathBuf;
 use reqwest::{blocking::Response, header};
-use tracing::{debug, info};
+use tracing::{debug, info, warn};
 
 use super::{
 	Error, LocalExtraction, ResolveResult, Result, VendorSource,
@@ -85,9 +86,27 @@
 	)?)
 }
 
+#[cfg(unix)]
+fn make_symlink(target: &str, link: &Path) -> std::io::Result<()> {
+	std::os::unix::fs::symlink(target, link)
+}
+
+#[cfg(windows)]
+fn make_symlink(target: &str, link: &Path) -> std::io::Result<()> {
+	std::os::windows::fs::symlink_file(target, link)
+}
+
+#[cfg(not(any(unix, windows)))]
+fn make_symlink(_target: &str, _link: &Path) -> std::io::Result<()> {
+	Err(std::io::Error::new(
+		std::io::ErrorKind::Unsupported,
+		"symlinks are not supported on this platform",
+	))
+}
+
 fn extract_subdir(archive: &ZipFileAccessor, subdir: &SubDir, dest: &Path) -> Result<()> {
 	archive.iter(subdir, &mut |name, entry| {
-		let target = dest.join(name);
+		let target = dest.join(&name);
 		match entry {
 			AccessorEntry::Dir => {
 				fs::create_dir_all(&target).map_err(|e| Error::Io(target, e))?;
@@ -98,6 +117,24 @@
 				}
 				fs::write(&target, &data).map_err(|e| Error::Io(target, e))?;
 			}
+			AccessorEntry::Symlink(link_target) => {
+				let symlink_parent = name
+					.as_path()
+					.parent()
+					.map(|p| SubDir::try_from(Utf8PathBuf::from(p)))
+					.transpose()
+					.expect("parent of a SubDir is a SubDir")
+					.unwrap_or_else(SubDir::empty);
+				if link_target.resolve_under(&symlink_parent).is_err() {
+					warn!("symlink {name} -> {link_target} escapes extraction; skipping");
+					return Ok(());
+				}
+				if let Some(parent) = target.parent() {
+					fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;
+				}
+				make_symlink(&link_target.to_string(), &target)
+					.map_err(|e| Error::Io(target, e))?;
+			}
 		}
 		Ok(())
 	})