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::{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 pub fn iter<E>(70 &self,71 subdir: &SubDir,72 cb: &mut dyn FnMut(SubDir, AccessorEntry) -> Result<(), E>,73 ) -> Result<(), E>74 where75 E: From<Error>,76 {77 let mut archive = self.archive.lock().expect("not poisoned");78 let len = archive.len();7980 let mut found = false;81 for i in 0..len {82 let mut entry = archive.by_index(i).map_err(Error::from)?;83 let raw = entry.name();84 let Ok(full_name) = SubDir::from_str(raw) else {85 warn!("invalid zip entry name: {raw}");86 continue;87 };88 89 let Some(in_repo) = full_name.strip_prefix(&self.prefix) else {90 continue;91 };92 let Some(name) = in_repo.strip_prefix(subdir) else {93 continue;94 };95 found = true;96 if name.is_empty() && entry.is_dir() {97 continue;98 }99100 cb(101 name.clone(),102 if entry.is_dir() {103 AccessorEntry::Dir104 } else if entry.is_file() {105 let mut data = Vec::new();106 entry.read_to_end(&mut data).map_err(Error::ZipIo)?;107 AccessorEntry::File(data)108 } else {109 110 panic!("unknown accessor entry type: {name:?}")111 },112 )?;113 }114115 if !found {116 return Err(Error::SubDirNotFound(subdir.clone()).into());117 }118119 Ok(())120 }121 pub fn len(&self) -> usize {122 self.archive.lock().expect("not poisoned").len()123 }124 pub fn is_empty(&self) -> bool {125 self.len() == 0126 }127}128129pub enum AccessorEntry {130 Dir,131 File(Vec<u8>),132}