1use 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 34 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 48 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 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!(117 "symlink target {target_str:?} at {name:?} escapes sandbox; skipping"118 );119 continue;120 };121 AccessorEntry::Symlink(target)122 } else if entry.is_file() {123 let mut data = Vec::new();124 entry.read_to_end(&mut data).map_err(Error::ZipIo)?;125 AccessorEntry::File(data)126 } else {127 warn!("unknown accessor entry type: {name:?}");128 continue;129 },130 )?;131 }132133 if !found {134 return Err(Error::SubDirNotFound(subdir.clone()).into());135 }136137 Ok(())138 }139 pub fn len(&self) -> usize {140 self.archive.lock().expect("not poisoned").len()141 }142 pub fn is_empty(&self) -> bool {143 self.len() == 0144 }145}146147pub enum AccessorEntry {148 Dir,149 File(Vec<u8>),150 Symlink(LocalSource),151}