use std::collections::HashMap;
use std::fs::File;
use std::os::unix::fs::FileExt;
use std::path::Path;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use arrow::array::{Array, StringArray};
use arrow::record_batch::RecordBatch;
use crate::codec;
use crate::index::{read_znippy_index_filtered, IndexFilter};
pub const RUST_PKG_TYPE: i8 = 1;
pub const PYTHON_PKG_TYPE: i8 = 2;
pub const MAVEN_PKG_TYPE: i8 = 3;
pub const NPM_PKG_TYPE: i8 = 6;
pub const GEM_PKG_TYPE: i8 = 11;
pub const CONDA_PKG_TYPE: i8 = 14;
#[derive(Debug, Clone)]
pub struct FileLoc {
pub chunks: Vec<ChunkRef>,
pub uncompressed_size: u64,
pub(crate) relative_path: String,
}
#[derive(Debug, Clone, Copy)]
pub struct ChunkRef {
pub blob_offset: u64,
pub blob_size: u64,
pub fdata_offset: u64,
pub compressed: bool,
}
impl FileLoc {
#[allow(dead_code)]
pub(crate) fn relative_path(&self) -> &str {
&self.relative_path
}
fn read_bytes(&self, archive: &File) -> Result<Vec<u8>> {
let mut result = Vec::with_capacity(self.uncompressed_size as usize);
let mut blob = Vec::new();
let mut decomp = Vec::new();
for chunk in &self.chunks {
blob.resize(chunk.blob_size as usize, 0);
archive.read_exact_at(&mut blob, chunk.blob_offset)?;
if chunk.compressed {
codec::decompress_into(&blob, &mut decomp)?;
result.extend_from_slice(&decomp);
} else {
result.extend_from_slice(&blob);
}
}
Ok(result)
}
}
fn group_rows_by_file(batch: &RecordBatch) -> Result<HashMap<String, FileLoc>> {
use arrow::array::{BooleanArray, UInt64Array};
let col = |n: &str| {
batch
.column_by_name(n)
.ok_or_else(|| anyhow!("index missing column {n}"))
};
let paths = col("relative_path")?
.as_any()
.downcast_ref::<StringArray>()
.ok_or_else(|| anyhow!("relative_path not StringArray"))?;
let compressed = col("compressed")?
.as_any()
.downcast_ref::<BooleanArray>()
.ok_or_else(|| anyhow!("compressed not BooleanArray"))?;
let sizes = col("uncompressed_size")?
.as_any()
.downcast_ref::<UInt64Array>()
.ok_or_else(|| anyhow!("uncompressed_size not UInt64Array"))?;
let blob_offset = col("blob_offset")?
.as_any()
.downcast_ref::<UInt64Array>()
.ok_or_else(|| anyhow!("blob_offset not UInt64Array"))?;
let blob_size = col("blob_size")?
.as_any()
.downcast_ref::<UInt64Array>()
.ok_or_else(|| anyhow!("blob_size not UInt64Array"))?;
let fdata = col("fdata_offset")?
.as_any()
.downcast_ref::<UInt64Array>()
.ok_or_else(|| anyhow!("fdata_offset not UInt64Array"))?;
let mut by_path: HashMap<String, FileLoc> = HashMap::new();
for i in 0..batch.num_rows() {
let path = paths.value(i);
let entry = by_path.entry(path.to_string()).or_insert_with(|| FileLoc {
chunks: Vec::new(),
uncompressed_size: 0,
relative_path: path.to_string(),
});
entry.uncompressed_size += sizes.value(i);
entry.chunks.push(ChunkRef {
blob_offset: blob_offset.value(i),
blob_size: blob_size.value(i),
fdata_offset: fdata.value(i),
compressed: compressed.value(i),
});
}
for f in by_path.values_mut() {
f.chunks.sort_by_key(|c| c.fdata_offset);
}
Ok(by_path)
}
fn file_name(rel_path: &str) -> &str {
rel_path.rsplit('/').next().unwrap_or(rel_path)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct RustKey {
name: String,
version: String,
}
pub struct RustView {
archive: Arc<File>,
coords: HashMap<RustKey, FileLoc>,
}
pub struct RustPackage {
archive: Arc<File>,
loc: FileLoc,
name: String,
version: String,
}
impl RustView {
fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
let (_schema, batches) =
read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(RUST_PKG_TYPE), repo: None })?;
let mut coords = HashMap::new();
for batch in &batches {
let name = batch
.column_by_name("crate_name")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let version = batch
.column_by_name("version")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let (Some(name), Some(version)) = (name, version) else {
continue;
};
let locs = group_rows_by_file(batch)?;
let paths = batch
.column_by_name("relative_path")
.and_then(|c| c.as_any().downcast_ref::<StringArray>())
.ok_or_else(|| anyhow!("missing relative_path"))?;
let mut seen = std::collections::HashSet::new();
for i in 0..batch.num_rows() {
let p = paths.value(i);
if !seen.insert(p) {
continue;
}
if name.is_null(i) || version.is_null(i) {
continue;
}
if let Some(loc) = locs.get(p) {
coords.insert(
RustKey { name: name.value(i).to_string(), version: version.value(i).to_string() },
loc.clone(),
);
}
}
}
Ok(Self { archive, coords })
}
pub fn get(&self, name: &str, version: &str) -> Option<RustPackage> {
let loc = self
.coords
.get(&RustKey { name: name.to_string(), version: version.to_string() })?;
Some(RustPackage {
archive: Arc::clone(&self.archive),
loc: loc.clone(),
name: name.to_string(),
version: version.to_string(),
})
}
pub fn list(&self) -> Vec<(String, String)> {
self.coords.keys().map(|k| (k.name.clone(), k.version.clone())).collect()
}
pub fn len(&self) -> usize {
self.coords.len()
}
pub fn is_empty(&self) -> bool {
self.coords.is_empty()
}
}
impl RustPackage {
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &str {
&self.version
}
pub fn size(&self) -> u64 {
self.loc.uncompressed_size
}
pub fn bytes(&self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
pub fn into_bytes(self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct MavenKey {
group: String,
artifact: String,
version: String,
classifier: Option<String>,
}
pub struct MavenView {
archive: Arc<File>,
coords: HashMap<MavenKey, FileLoc>,
}
pub struct MavenPackage {
archive: Arc<File>,
loc: FileLoc,
group: String,
artifact: String,
version: String,
classifier: Option<String>,
}
impl MavenView {
fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
let (_schema, batches) =
read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(MAVEN_PKG_TYPE), repo: None })?;
let mut coords = HashMap::new();
for batch in &batches {
let group = batch
.column_by_name("group_id")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let artifact = batch
.column_by_name("artifact_id")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let version = batch
.column_by_name("version")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let (Some(group), Some(artifact), Some(version)) = (group, artifact, version) else {
continue;
};
let classifier_col = batch
.column_by_name("classifier")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let locs = group_rows_by_file(batch)?;
let paths = batch
.column_by_name("relative_path")
.and_then(|c| c.as_any().downcast_ref::<StringArray>())
.ok_or_else(|| anyhow!("missing relative_path"))?;
let mut seen = std::collections::HashSet::new();
for i in 0..batch.num_rows() {
let p = paths.value(i);
if !seen.insert(p) {
continue;
}
if group.is_null(i) || artifact.is_null(i) || version.is_null(i) {
continue;
}
let classifier = match classifier_col {
Some(c) if !c.is_null(i) && !c.value(i).is_empty() => Some(c.value(i).to_string()),
_ => derive_classifier(file_name(p), artifact.value(i), version.value(i)),
};
if let Some(loc) = locs.get(p) {
coords.insert(
MavenKey {
group: group.value(i).to_string(),
artifact: artifact.value(i).to_string(),
version: version.value(i).to_string(),
classifier,
},
loc.clone(),
);
}
}
}
Ok(Self { archive, coords })
}
pub fn get(&self, group: &str, artifact: &str, version: &str) -> Option<MavenPackage> {
self.get_classified(group, artifact, version, None)
}
pub fn get_classified(
&self,
group: &str,
artifact: &str,
version: &str,
classifier: Option<&str>,
) -> Option<MavenPackage> {
let key = MavenKey {
group: group.to_string(),
artifact: artifact.to_string(),
version: version.to_string(),
classifier: classifier.map(|s| s.to_string()),
};
let loc = self.coords.get(&key)?;
Some(MavenPackage {
archive: Arc::clone(&self.archive),
loc: loc.clone(),
group: group.to_string(),
artifact: artifact.to_string(),
version: version.to_string(),
classifier: classifier.map(|s| s.to_string()),
})
}
pub fn list(&self) -> Vec<(String, String, String, Option<String>)> {
self.coords
.keys()
.map(|k| (k.group.clone(), k.artifact.clone(), k.version.clone(), k.classifier.clone()))
.collect()
}
pub fn len(&self) -> usize {
self.coords.len()
}
pub fn is_empty(&self) -> bool {
self.coords.is_empty()
}
}
fn derive_classifier(filename: &str, artifact: &str, version: &str) -> Option<String> {
let stem = filename.rsplit_once('.').map(|(s, _)| s).unwrap_or(filename);
let prefix = format!("{artifact}-{version}");
let rest = stem.strip_prefix(&prefix)?;
let rest = rest.strip_prefix('-')?;
if rest.is_empty() {
None
} else {
Some(rest.to_string())
}
}
impl MavenPackage {
pub fn group(&self) -> &str {
&self.group
}
pub fn artifact(&self) -> &str {
&self.artifact
}
pub fn version(&self) -> &str {
&self.version
}
pub fn classifier(&self) -> Option<&str> {
self.classifier.as_deref()
}
pub fn coords(&self) -> (&str, &str, &str, Option<&str>) {
(&self.group, &self.artifact, &self.version, self.classifier.as_deref())
}
pub fn size(&self) -> u64 {
self.loc.uncompressed_size
}
pub fn bytes(&self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
pub fn into_bytes(self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PythonKind {
Wheel,
Sdist,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct PythonKey {
name: String,
version: String,
}
pub struct PythonView {
archive: Arc<File>,
coords: HashMap<PythonKey, FileLoc>,
kinds: HashMap<PythonKey, PythonKind>,
}
pub struct PythonPackage {
archive: Arc<File>,
loc: FileLoc,
name: String,
version: String,
kind: PythonKind,
}
impl PythonView {
fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
let (_schema, batches) =
read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(PYTHON_PKG_TYPE), repo: None })?;
let mut coords = HashMap::new();
let mut kinds = HashMap::new();
for batch in &batches {
let name = batch
.column_by_name("name")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let version = batch
.column_by_name("version")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let (Some(name), Some(version)) = (name, version) else {
continue;
};
let locs = group_rows_by_file(batch)?;
let paths = batch
.column_by_name("relative_path")
.and_then(|c| c.as_any().downcast_ref::<StringArray>())
.ok_or_else(|| anyhow!("missing relative_path"))?;
let mut seen = std::collections::HashSet::new();
for i in 0..batch.num_rows() {
let p = paths.value(i);
if !seen.insert(p) {
continue;
}
if name.is_null(i) || version.is_null(i) {
continue;
}
let key = PythonKey { name: name.value(i).to_string(), version: version.value(i).to_string() };
let kind = if file_name(p).ends_with(".whl") {
PythonKind::Wheel
} else {
PythonKind::Sdist
};
if let Some(loc) = locs.get(p) {
let replace = matches!(kind, PythonKind::Wheel)
|| !coords.contains_key(&key);
if replace {
coords.insert(key.clone(), loc.clone());
kinds.insert(key, kind);
}
}
}
}
Ok(Self { archive, coords, kinds })
}
pub fn get(&self, name: &str, version: &str) -> Option<PythonPackage> {
let key = PythonKey { name: name.to_string(), version: version.to_string() };
let loc = self.coords.get(&key)?;
let kind = self.kinds.get(&key).copied().unwrap_or(PythonKind::Sdist);
Some(PythonPackage {
archive: Arc::clone(&self.archive),
loc: loc.clone(),
name: name.to_string(),
version: version.to_string(),
kind,
})
}
pub fn list(&self) -> Vec<(String, String)> {
self.coords.keys().map(|k| (k.name.clone(), k.version.clone())).collect()
}
pub fn len(&self) -> usize {
self.coords.len()
}
pub fn is_empty(&self) -> bool {
self.coords.is_empty()
}
}
impl PythonPackage {
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &str {
&self.version
}
pub fn kind(&self) -> PythonKind {
self.kind
}
pub fn size(&self) -> u64 {
self.loc.uncompressed_size
}
pub fn bytes(&self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
pub fn into_bytes(self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct NpmKey {
name: String,
version: String,
}
pub struct NpmView {
archive: Arc<File>,
coords: HashMap<NpmKey, FileLoc>,
}
pub struct NpmPackage {
archive: Arc<File>,
loc: FileLoc,
name: String,
version: String,
}
impl NpmView {
fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
let (_schema, batches) =
read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(NPM_PKG_TYPE), repo: None })?;
let mut coords = HashMap::new();
for batch in &batches {
let name = batch
.column_by_name("name")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let version = batch
.column_by_name("version")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let (Some(name), Some(version)) = (name, version) else {
continue;
};
let locs = group_rows_by_file(batch)?;
let paths = batch
.column_by_name("relative_path")
.and_then(|c| c.as_any().downcast_ref::<StringArray>())
.ok_or_else(|| anyhow!("missing relative_path"))?;
let mut seen = std::collections::HashSet::new();
for i in 0..batch.num_rows() {
let p = paths.value(i);
if !seen.insert(p) {
continue;
}
if name.is_null(i) || version.is_null(i) {
continue;
}
if let Some(loc) = locs.get(p) {
coords.insert(
NpmKey { name: name.value(i).to_string(), version: version.value(i).to_string() },
loc.clone(),
);
}
}
}
Ok(Self { archive, coords })
}
pub fn get(&self, name: &str, version: &str) -> Option<NpmPackage> {
let loc = self
.coords
.get(&NpmKey { name: name.to_string(), version: version.to_string() })?;
Some(NpmPackage {
archive: Arc::clone(&self.archive),
loc: loc.clone(),
name: name.to_string(),
version: version.to_string(),
})
}
pub fn list(&self) -> Vec<(String, String)> {
self.coords.keys().map(|k| (k.name.clone(), k.version.clone())).collect()
}
pub fn len(&self) -> usize {
self.coords.len()
}
pub fn is_empty(&self) -> bool {
self.coords.is_empty()
}
}
impl NpmPackage {
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &str {
&self.version
}
pub fn size(&self) -> u64 {
self.loc.uncompressed_size
}
pub fn bytes(&self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
pub fn into_bytes(self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct GemKey {
name: String,
version: String,
platform: String,
}
pub struct GemView {
archive: Arc<File>,
coords: HashMap<GemKey, FileLoc>,
}
pub struct GemPackage {
archive: Arc<File>,
loc: FileLoc,
name: String,
version: String,
platform: String,
}
impl GemView {
fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
let (_schema, batches) =
read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(GEM_PKG_TYPE), repo: None })?;
let mut coords = HashMap::new();
for batch in &batches {
let name = batch
.column_by_name("name")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let version = batch
.column_by_name("version")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let (Some(name), Some(version)) = (name, version) else {
continue;
};
let platform_col = batch
.column_by_name("platform")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let locs = group_rows_by_file(batch)?;
let paths = batch
.column_by_name("relative_path")
.and_then(|c| c.as_any().downcast_ref::<StringArray>())
.ok_or_else(|| anyhow!("missing relative_path"))?;
let mut seen = std::collections::HashSet::new();
for i in 0..batch.num_rows() {
let p = paths.value(i);
if !seen.insert(p) {
continue;
}
if name.is_null(i) || version.is_null(i) {
continue;
}
let platform = match platform_col {
Some(c) if !c.is_null(i) && !c.value(i).is_empty() => c.value(i).to_string(),
_ => "ruby".to_string(),
};
if let Some(loc) = locs.get(p) {
coords.insert(
GemKey {
name: name.value(i).to_string(),
version: version.value(i).to_string(),
platform,
},
loc.clone(),
);
}
}
}
Ok(Self { archive, coords })
}
pub fn get(&self, name: &str, version: &str) -> Option<GemPackage> {
self.get_platform(name, version, "ruby")
}
pub fn get_platform(&self, name: &str, version: &str, platform: &str) -> Option<GemPackage> {
let key = GemKey {
name: name.to_string(),
version: version.to_string(),
platform: platform.to_string(),
};
let loc = self.coords.get(&key)?;
Some(GemPackage {
archive: Arc::clone(&self.archive),
loc: loc.clone(),
name: name.to_string(),
version: version.to_string(),
platform: platform.to_string(),
})
}
pub fn list(&self) -> Vec<(String, String, String)> {
self.coords
.keys()
.map(|k| (k.name.clone(), k.version.clone(), k.platform.clone()))
.collect()
}
pub fn len(&self) -> usize {
self.coords.len()
}
pub fn is_empty(&self) -> bool {
self.coords.is_empty()
}
}
impl GemPackage {
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &str {
&self.version
}
pub fn platform(&self) -> &str {
&self.platform
}
pub fn size(&self) -> u64 {
self.loc.uncompressed_size
}
pub fn bytes(&self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
pub fn into_bytes(self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct CondaKey {
name: String,
version: String,
build: String,
subdir: String,
}
pub struct CondaView {
archive: Arc<File>,
coords: HashMap<CondaKey, FileLoc>,
}
pub struct CondaPackage {
archive: Arc<File>,
loc: FileLoc,
name: String,
version: String,
build: String,
subdir: String,
}
impl CondaView {
fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
let (_schema, batches) = read_znippy_index_filtered(
path,
&IndexFilter { pkg_type: Some(CONDA_PKG_TYPE), repo: None },
)?;
let mut coords = HashMap::new();
for batch in &batches {
let name = batch
.column_by_name("name")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let version = batch
.column_by_name("version")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let (Some(name), Some(version)) = (name, version) else {
continue;
};
let build_col = batch
.column_by_name("build")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let subdir_col = batch
.column_by_name("subdir")
.and_then(|c| c.as_any().downcast_ref::<StringArray>());
let locs = group_rows_by_file(batch)?;
let paths = batch
.column_by_name("relative_path")
.and_then(|c| c.as_any().downcast_ref::<StringArray>())
.ok_or_else(|| anyhow!("missing relative_path"))?;
let mut seen = std::collections::HashSet::new();
for i in 0..batch.num_rows() {
let p = paths.value(i);
if !seen.insert(p) {
continue;
}
if name.is_null(i) || version.is_null(i) {
continue;
}
let build = match build_col {
Some(c) if !c.is_null(i) => c.value(i).to_string(),
_ => String::new(),
};
let subdir = match subdir_col {
Some(c) if !c.is_null(i) && !c.value(i).is_empty() => c.value(i).to_string(),
_ => String::new(),
};
if let Some(loc) = locs.get(p) {
coords.insert(
CondaKey {
name: name.value(i).to_string(),
version: version.value(i).to_string(),
build,
subdir,
},
loc.clone(),
);
}
}
}
Ok(Self { archive, coords })
}
pub fn get(&self, name: &str, version: &str) -> Option<CondaPackage> {
let (key, loc) = self
.coords
.iter()
.find(|(k, _)| k.name == name && k.version == version)?;
Some(CondaPackage {
archive: Arc::clone(&self.archive),
loc: loc.clone(),
name: key.name.clone(),
version: key.version.clone(),
build: key.build.clone(),
subdir: key.subdir.clone(),
})
}
pub fn get_exact(
&self,
name: &str,
version: &str,
build: &str,
subdir: &str,
) -> Option<CondaPackage> {
let key = CondaKey {
name: name.to_string(),
version: version.to_string(),
build: build.to_string(),
subdir: subdir.to_string(),
};
let loc = self.coords.get(&key)?;
Some(CondaPackage {
archive: Arc::clone(&self.archive),
loc: loc.clone(),
name: name.to_string(),
version: version.to_string(),
build: build.to_string(),
subdir: subdir.to_string(),
})
}
pub fn list(&self) -> Vec<(String, String, String, String)> {
self.coords
.keys()
.map(|k| (k.name.clone(), k.version.clone(), k.build.clone(), k.subdir.clone()))
.collect()
}
pub fn len(&self) -> usize {
self.coords.len()
}
pub fn is_empty(&self) -> bool {
self.coords.is_empty()
}
}
impl CondaPackage {
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &str {
&self.version
}
pub fn build(&self) -> &str {
&self.build
}
pub fn subdir(&self) -> &str {
&self.subdir
}
pub fn size(&self) -> u64 {
self.loc.uncompressed_size
}
pub fn bytes(&self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
pub fn into_bytes(self) -> Result<Vec<u8>> {
self.loc.read_bytes(&self.archive)
}
}
pub(crate) fn build_rust_view(path: &Path, archive: Arc<File>) -> Result<Option<RustView>> {
let view = RustView::build(path, archive)?;
Ok(if view.is_empty() { None } else { Some(view) })
}
pub(crate) fn build_maven_view(path: &Path, archive: Arc<File>) -> Result<Option<MavenView>> {
let view = MavenView::build(path, archive)?;
Ok(if view.is_empty() { None } else { Some(view) })
}
pub(crate) fn build_python_view(path: &Path, archive: Arc<File>) -> Result<Option<PythonView>> {
let view = PythonView::build(path, archive)?;
Ok(if view.is_empty() { None } else { Some(view) })
}
pub(crate) fn build_npm_view(path: &Path, archive: Arc<File>) -> Result<Option<NpmView>> {
let view = NpmView::build(path, archive)?;
Ok(if view.is_empty() { None } else { Some(view) })
}
pub(crate) fn build_gem_view(path: &Path, archive: Arc<File>) -> Result<Option<GemView>> {
let view = GemView::build(path, archive)?;
Ok(if view.is_empty() { None } else { Some(view) })
}
pub(crate) fn build_conda_view(path: &Path, archive: Arc<File>) -> Result<Option<CondaView>> {
let view = CondaView::build(path, archive)?;
Ok(if view.is_empty() { None } else { Some(view) })
}