difftreelog
feat(jrb) symlinks
in: master
2 files changed
crates/jrsonnet-pkg/src/install/accessor.rsdiffbeforeafterboth1use std::{2 fs::File,3 io::{self, Read},4 result,5 str::FromStr as _,6 sync::Mutex,7};89use tracing::warn;10use zip::{ZipArchive, result::ZipError};1112use crate::jsonnet_bundler::{LocalSource, SubDir, SubDirEscapeError};1314#[derive(thiserror::Error, Debug)]15pub enum Error {16 #[error(transparent)]17 Zip(#[from] ZipError),18 #[error("invalid prefixed archive")]19 ZipInvalidPrefix,20 #[error("zip io: {0}")]21 ZipIo(io::Error),22 #[error("subdir not found: {0}")]23 SubDirNotFound(SubDir),24 #[error(transparent)]25 SubdirEscape(#[from] SubDirEscapeError),26}27type Result<T, E = Error> = result::Result<T, E>;2829pub trait SourceAccessor {}3031pub struct ZipFileAccessor {32 archive: Mutex<ZipArchive<File>>,33 // Github archives have top-level directory with repo name34 prefix: SubDir,35}3637impl ZipFileAccessor {38 pub fn new_prefixed(file: File) -> Result<Self> {39 let archive = ZipArchive::new(file)?;40 let prefix = archive.name_for_index(0).ok_or(Error::ZipInvalidPrefix)?;4142 Ok(Self {43 prefix: SubDir::from_str(prefix)?,44 archive: Mutex::new(archive),45 })46 }47 /// Read a file from inside the archive's logical root (after stripping the48 /// github-style `<repo>-<sha>/` prefix).49 #[allow(clippy::significant_drop_tightening, reason = "false-positive")]50 pub fn read(&self, name: &SubDir) -> Result<Option<Vec<u8>>> {51 let prefixed = self52 .prefix53 .join(name)54 .expect("prefix and name are both subdirs");55 let mut archive = self.archive.lock().expect("not poisoned");56 let mut v = match archive.by_name(prefixed.as_str()) {57 Ok(v) => v,58 Err(ZipError::FileNotFound) => return Ok(None),59 Err(e) => return Err(e.into()),60 };61 if !v.is_file() {62 return Ok(None);63 }64 let mut out = Vec::new();65 v.read_to_end(&mut out).map_err(Error::ZipIo)?;66 Ok(Some(out))67 }68 #[allow(clippy::significant_drop_tightening, reason = "false-positive")]69 #[allow(70 clippy::iter_not_returning_iterator,71 reason = "idk for a better name, it is still inner iteration"72 )]73 pub fn iter<E>(74 &self,75 subdir: &SubDir,76 cb: &mut dyn FnMut(SubDir, AccessorEntry) -> Result<(), E>,77 ) -> Result<(), E>78 where79 E: From<Error>,80 {81 let mut archive = self.archive.lock().expect("not poisoned");82 let len = archive.len();8384 let mut found = false;85 for i in 0..len {86 let mut entry = archive.by_index(i).map_err(Error::from)?;87 let raw = entry.name();88 let Ok(full_name) = SubDir::from_str(raw) else {89 warn!("invalid zip entry name: {raw}");90 continue;91 };92 // Peel off the github-archive top-level `<repo>-<sha>/` prefix.93 let Some(in_repo) = full_name.strip_prefix(&self.prefix) else {94 continue;95 };96 let Some(name) = in_repo.strip_prefix(subdir) else {97 continue;98 };99 found = true;100 if name.is_empty() && entry.is_dir() {101 continue;102 }103104 cb(105 name.clone(),106 if entry.is_dir() {107 AccessorEntry::Dir108 } 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() {121 let mut data = Vec::new();122 entry.read_to_end(&mut data).map_err(Error::ZipIo)?;123 AccessorEntry::File(data)124 } else {125 warn!("unknown accessor entry type: {name:?}");126 continue;127 },128 )?;129 }130131 if !found {132 return Err(Error::SubDirNotFound(subdir.clone()).into());133 }134135 Ok(())136 }137 pub fn len(&self) -> usize {138 self.archive.lock().expect("not poisoned").len()139 }140 pub fn is_empty(&self) -> bool {141 self.len() == 0142 }143}144145pub enum AccessorEntry {146 Dir,147 File(Vec<u8>),148 Symlink(LocalSource),149}crates/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(())
})