From 97dec27e23cc5fb2d884f43ce0f8b9c82aec6640 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Thu, 07 May 2026 21:10:06 +0000 Subject: [PATCH] feat(jrb): symlinks --- --- 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), + Symlink(LocalSource), } --- 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(()) }) -- gitstuff