1use std::collections::HashMap;
25use std::fs::File;
26use std::os::unix::fs::FileExt;
27use std::path::Path;
28use std::sync::Arc;
29
30use anyhow::{anyhow, Result};
31use arrow::array::{Array, StringArray};
32use arrow::record_batch::RecordBatch;
33
34use crate::codec;
35use crate::index::{read_znippy_index_filtered, IndexFilter};
36
37pub const RUST_PKG_TYPE: i8 = 1;
49pub const PYTHON_PKG_TYPE: i8 = 2;
51pub const MAVEN_PKG_TYPE: i8 = 3;
53pub const NPM_PKG_TYPE: i8 = 6;
55pub const GEM_PKG_TYPE: i8 = 11;
57pub const RPM_PKG_TYPE: i8 = 8;
59pub const DEB_PKG_TYPE: i8 = 9;
61pub const CONDA_PKG_TYPE: i8 = 14;
63
64#[derive(Debug, Clone)]
68pub struct FileLoc {
69 pub relative_path: String,
82 pub chunks: Vec<ChunkRef>,
84 pub uncompressed_size: u64,
86}
87
88#[derive(Debug, Clone, Copy)]
90pub struct ChunkRef {
91 pub blob_offset: u64,
92 pub blob_size: u64,
93 pub fdata_offset: u64,
94 pub compressed: bool,
95}
96
97impl FileLoc {
98 fn read_bytes(&self, archive: &File) -> Result<Vec<u8>> {
101 let mut result = Vec::with_capacity(self.uncompressed_size as usize);
102 let mut blob = Vec::new();
103 let mut decomp = Vec::new();
104 for chunk in &self.chunks {
105 blob.resize(chunk.blob_size as usize, 0);
106 archive.read_exact_at(&mut blob, chunk.blob_offset)?;
107 if chunk.compressed {
108 codec::decompress_into(&blob, &mut decomp)?;
109 result.extend_from_slice(&decomp);
110 } else {
111 result.extend_from_slice(&blob);
112 }
113 }
114 Ok(result)
115 }
116}
117
118fn group_rows_by_file(batch: &RecordBatch) -> Result<HashMap<String, FileLoc>> {
121 use arrow::array::{BooleanArray, UInt64Array};
122
123 let col = |n: &str| {
124 batch
125 .column_by_name(n)
126 .ok_or_else(|| anyhow!("index missing column {n}"))
127 };
128 let paths = col("relative_path")?
129 .as_any()
130 .downcast_ref::<StringArray>()
131 .ok_or_else(|| anyhow!("relative_path not StringArray"))?;
132 let compressed = col("compressed")?
133 .as_any()
134 .downcast_ref::<BooleanArray>()
135 .ok_or_else(|| anyhow!("compressed not BooleanArray"))?;
136 let sizes = col("uncompressed_size")?
137 .as_any()
138 .downcast_ref::<UInt64Array>()
139 .ok_or_else(|| anyhow!("uncompressed_size not UInt64Array"))?;
140 let blob_offset = col("blob_offset")?
141 .as_any()
142 .downcast_ref::<UInt64Array>()
143 .ok_or_else(|| anyhow!("blob_offset not UInt64Array"))?;
144 let blob_size = col("blob_size")?
145 .as_any()
146 .downcast_ref::<UInt64Array>()
147 .ok_or_else(|| anyhow!("blob_size not UInt64Array"))?;
148 let fdata = col("fdata_offset")?
149 .as_any()
150 .downcast_ref::<UInt64Array>()
151 .ok_or_else(|| anyhow!("fdata_offset not UInt64Array"))?;
152
153 let mut by_path: HashMap<String, FileLoc> = HashMap::new();
154 for i in 0..batch.num_rows() {
155 let path = paths.value(i);
156 let entry = by_path.entry(path.to_string()).or_insert_with(|| FileLoc {
157 relative_path: path.to_string(),
158 chunks: Vec::new(),
159 uncompressed_size: 0,
160 });
161 entry.uncompressed_size += sizes.value(i);
162 entry.chunks.push(ChunkRef {
163 blob_offset: blob_offset.value(i),
164 blob_size: blob_size.value(i),
165 fdata_offset: fdata.value(i),
166 compressed: compressed.value(i),
167 });
168 }
169 for f in by_path.values_mut() {
170 f.chunks.sort_by_key(|c| c.fdata_offset);
171 }
172 Ok(by_path)
173}
174
175fn file_name(rel_path: &str) -> &str {
179 rel_path.rsplit('/').next().unwrap_or(rel_path)
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Hash)]
188struct RustKey {
189 name: String,
190 version: String,
191}
192
193pub struct RustView {
195 archive: Arc<File>,
196 coords: HashMap<RustKey, FileLoc>,
197}
198
199pub struct RustPackage {
202 archive: Arc<File>,
203 loc: FileLoc,
204 name: String,
205 version: String,
206}
207
208impl RustView {
209 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
210 let (_schema, batches) =
211 read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(RUST_PKG_TYPE), repo: None })?;
212 let mut coords = HashMap::new();
213 for batch in &batches {
214 let name = batch
215 .column_by_name("crate_name")
216 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
217 let version = batch
218 .column_by_name("version")
219 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
220 let (Some(name), Some(version)) = (name, version) else {
221 continue;
222 };
223 let locs = group_rows_by_file(batch)?;
224 let paths = batch
227 .column_by_name("relative_path")
228 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
229 .ok_or_else(|| anyhow!("missing relative_path"))?;
230 let mut seen = std::collections::HashSet::new();
231 for i in 0..batch.num_rows() {
232 let p = paths.value(i);
233 if !seen.insert(p) {
234 continue;
235 }
236 if name.is_null(i) || version.is_null(i) {
237 continue;
238 }
239 if let Some(loc) = locs.get(p) {
240 coords.insert(
241 RustKey { name: name.value(i).to_string(), version: version.value(i).to_string() },
242 loc.clone(),
243 );
244 }
245 }
246 }
247 Ok(Self { archive, coords })
248 }
249
250 pub fn get(&self, name: &str, version: &str) -> Option<RustPackage> {
252 let loc = self
253 .coords
254 .get(&RustKey { name: name.to_string(), version: version.to_string() })?;
255 Some(RustPackage {
256 archive: Arc::clone(&self.archive),
257 loc: loc.clone(),
258 name: name.to_string(),
259 version: version.to_string(),
260 })
261 }
262
263 pub fn list(&self) -> Vec<(String, String)> {
265 self.coords.keys().map(|k| (k.name.clone(), k.version.clone())).collect()
266 }
267
268 pub fn len(&self) -> usize {
270 self.coords.len()
271 }
272 pub fn is_empty(&self) -> bool {
273 self.coords.is_empty()
274 }
275}
276
277impl RustPackage {
278 pub fn relative_path(&self) -> &str {
285 &self.loc.relative_path
286 }
287
288 pub fn name(&self) -> &str {
290 &self.name
291 }
292 pub fn version(&self) -> &str {
294 &self.version
295 }
296 pub fn size(&self) -> u64 {
298 self.loc.uncompressed_size
299 }
300 pub fn bytes(&self) -> Result<Vec<u8>> {
302 self.loc.read_bytes(&self.archive)
303 }
304 pub fn into_bytes(self) -> Result<Vec<u8>> {
306 self.loc.read_bytes(&self.archive)
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Hash)]
317struct MavenKey {
318 group: String,
319 artifact: String,
320 version: String,
321 classifier: Option<String>,
322}
323
324pub struct MavenView {
326 archive: Arc<File>,
327 coords: HashMap<MavenKey, FileLoc>,
328}
329
330pub struct MavenPackage {
333 archive: Arc<File>,
334 loc: FileLoc,
335 group: String,
336 artifact: String,
337 version: String,
338 classifier: Option<String>,
339}
340
341impl MavenView {
342 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
343 let (_schema, batches) =
344 read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(MAVEN_PKG_TYPE), repo: None })?;
345 let mut coords = HashMap::new();
346 for batch in &batches {
347 let group = batch
348 .column_by_name("group_id")
349 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
350 let artifact = batch
351 .column_by_name("artifact_id")
352 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
353 let version = batch
354 .column_by_name("version")
355 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
356 let (Some(group), Some(artifact), Some(version)) = (group, artifact, version) else {
357 continue;
358 };
359 let classifier_col = batch
361 .column_by_name("classifier")
362 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
363 let locs = group_rows_by_file(batch)?;
364 let paths = batch
365 .column_by_name("relative_path")
366 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
367 .ok_or_else(|| anyhow!("missing relative_path"))?;
368 let mut seen = std::collections::HashSet::new();
369 for i in 0..batch.num_rows() {
370 let p = paths.value(i);
371 if !seen.insert(p) {
372 continue;
373 }
374 if group.is_null(i) || artifact.is_null(i) || version.is_null(i) {
375 continue;
376 }
377 let classifier = match classifier_col {
380 Some(c) if !c.is_null(i) && !c.value(i).is_empty() => Some(c.value(i).to_string()),
381 _ => derive_classifier(file_name(p), artifact.value(i), version.value(i)),
382 };
383 if let Some(loc) = locs.get(p) {
384 coords.insert(
385 MavenKey {
386 group: group.value(i).to_string(),
387 artifact: artifact.value(i).to_string(),
388 version: version.value(i).to_string(),
389 classifier,
390 },
391 loc.clone(),
392 );
393 }
394 }
395 }
396 Ok(Self { archive, coords })
397 }
398
399 pub fn get(&self, group: &str, artifact: &str, version: &str) -> Option<MavenPackage> {
401 self.get_classified(group, artifact, version, None)
402 }
403
404 pub fn get_classified(
407 &self,
408 group: &str,
409 artifact: &str,
410 version: &str,
411 classifier: Option<&str>,
412 ) -> Option<MavenPackage> {
413 let key = MavenKey {
414 group: group.to_string(),
415 artifact: artifact.to_string(),
416 version: version.to_string(),
417 classifier: classifier.map(|s| s.to_string()),
418 };
419 let loc = self.coords.get(&key)?;
420 Some(MavenPackage {
421 archive: Arc::clone(&self.archive),
422 loc: loc.clone(),
423 group: group.to_string(),
424 artifact: artifact.to_string(),
425 version: version.to_string(),
426 classifier: classifier.map(|s| s.to_string()),
427 })
428 }
429
430 pub fn list(&self) -> Vec<(String, String, String, Option<String>)> {
432 self.coords
433 .keys()
434 .map(|k| (k.group.clone(), k.artifact.clone(), k.version.clone(), k.classifier.clone()))
435 .collect()
436 }
437
438 pub fn len(&self) -> usize {
439 self.coords.len()
440 }
441 pub fn is_empty(&self) -> bool {
442 self.coords.is_empty()
443 }
444}
445
446fn derive_classifier(filename: &str, artifact: &str, version: &str) -> Option<String> {
450 let stem = filename.rsplit_once('.').map(|(s, _)| s).unwrap_or(filename);
452 let prefix = format!("{artifact}-{version}");
453 let rest = stem.strip_prefix(&prefix)?;
454 let rest = rest.strip_prefix('-')?;
455 if rest.is_empty() {
456 None
457 } else {
458 Some(rest.to_string())
459 }
460}
461
462impl MavenPackage {
463 pub fn relative_path(&self) -> &str {
470 &self.loc.relative_path
471 }
472
473 pub fn group(&self) -> &str {
475 &self.group
476 }
477 pub fn artifact(&self) -> &str {
479 &self.artifact
480 }
481 pub fn version(&self) -> &str {
483 &self.version
484 }
485 pub fn classifier(&self) -> Option<&str> {
487 self.classifier.as_deref()
488 }
489 pub fn coords(&self) -> (&str, &str, &str, Option<&str>) {
491 (&self.group, &self.artifact, &self.version, self.classifier.as_deref())
492 }
493 pub fn size(&self) -> u64 {
494 self.loc.uncompressed_size
495 }
496 pub fn bytes(&self) -> Result<Vec<u8>> {
498 self.loc.read_bytes(&self.archive)
499 }
500 pub fn into_bytes(self) -> Result<Vec<u8>> {
501 self.loc.read_bytes(&self.archive)
502 }
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
511pub enum PythonKind {
512 Wheel,
513 Sdist,
514}
515
516#[derive(Debug, Clone, PartialEq, Eq, Hash)]
518struct PythonKey {
519 name: String,
520 version: String,
521}
522
523pub struct PythonView {
525 archive: Arc<File>,
526 coords: HashMap<PythonKey, FileLoc>,
527 kinds: HashMap<PythonKey, PythonKind>,
528}
529
530pub struct PythonPackage {
532 archive: Arc<File>,
533 loc: FileLoc,
534 name: String,
535 version: String,
536 kind: PythonKind,
537}
538
539impl PythonView {
540 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
541 let (_schema, batches) =
542 read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(PYTHON_PKG_TYPE), repo: None })?;
543 let mut coords = HashMap::new();
544 let mut kinds = HashMap::new();
545 for batch in &batches {
546 let name = batch
547 .column_by_name("name")
548 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
549 let version = batch
550 .column_by_name("version")
551 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
552 let (Some(name), Some(version)) = (name, version) else {
553 continue;
554 };
555 let locs = group_rows_by_file(batch)?;
556 let paths = batch
557 .column_by_name("relative_path")
558 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
559 .ok_or_else(|| anyhow!("missing relative_path"))?;
560 let mut seen = std::collections::HashSet::new();
561 for i in 0..batch.num_rows() {
562 let p = paths.value(i);
563 if !seen.insert(p) {
564 continue;
565 }
566 if name.is_null(i) || version.is_null(i) {
567 continue;
568 }
569 let key = PythonKey { name: name.value(i).to_string(), version: version.value(i).to_string() };
570 let kind = if file_name(p).ends_with(".whl") {
571 PythonKind::Wheel
572 } else {
573 PythonKind::Sdist
574 };
575 if let Some(loc) = locs.get(p) {
576 let replace = matches!(kind, PythonKind::Wheel)
578 || !coords.contains_key(&key);
579 if replace {
580 coords.insert(key.clone(), loc.clone());
581 kinds.insert(key, kind);
582 }
583 }
584 }
585 }
586 Ok(Self { archive, coords, kinds })
587 }
588
589 pub fn get(&self, name: &str, version: &str) -> Option<PythonPackage> {
591 let key = PythonKey { name: name.to_string(), version: version.to_string() };
592 let loc = self.coords.get(&key)?;
593 let kind = self.kinds.get(&key).copied().unwrap_or(PythonKind::Sdist);
594 Some(PythonPackage {
595 archive: Arc::clone(&self.archive),
596 loc: loc.clone(),
597 name: name.to_string(),
598 version: version.to_string(),
599 kind,
600 })
601 }
602
603 pub fn list(&self) -> Vec<(String, String)> {
605 self.coords.keys().map(|k| (k.name.clone(), k.version.clone())).collect()
606 }
607
608 pub fn len(&self) -> usize {
609 self.coords.len()
610 }
611 pub fn is_empty(&self) -> bool {
612 self.coords.is_empty()
613 }
614}
615
616impl PythonPackage {
617 pub fn relative_path(&self) -> &str {
624 &self.loc.relative_path
625 }
626
627 pub fn name(&self) -> &str {
628 &self.name
629 }
630 pub fn version(&self) -> &str {
631 &self.version
632 }
633 pub fn kind(&self) -> PythonKind {
635 self.kind
636 }
637 pub fn size(&self) -> u64 {
638 self.loc.uncompressed_size
639 }
640 pub fn bytes(&self) -> Result<Vec<u8>> {
642 self.loc.read_bytes(&self.archive)
643 }
644 pub fn into_bytes(self) -> Result<Vec<u8>> {
645 self.loc.read_bytes(&self.archive)
646 }
647}
648
649#[derive(Debug, Clone, PartialEq, Eq, Hash)]
657struct NpmKey {
658 name: String,
659 version: String,
660}
661
662pub struct NpmView {
664 archive: Arc<File>,
665 coords: HashMap<NpmKey, FileLoc>,
666}
667
668pub struct NpmPackage {
672 archive: Arc<File>,
673 loc: FileLoc,
674 name: String,
675 version: String,
676}
677
678impl NpmView {
679 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
680 let (_schema, batches) =
681 read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(NPM_PKG_TYPE), repo: None })?;
682 let mut coords = HashMap::new();
683 for batch in &batches {
684 let name = batch
685 .column_by_name("name")
686 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
687 let version = batch
688 .column_by_name("version")
689 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
690 let (Some(name), Some(version)) = (name, version) else {
691 continue;
692 };
693 let locs = group_rows_by_file(batch)?;
694 let paths = batch
695 .column_by_name("relative_path")
696 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
697 .ok_or_else(|| anyhow!("missing relative_path"))?;
698 let mut seen = std::collections::HashSet::new();
699 for i in 0..batch.num_rows() {
700 let p = paths.value(i);
701 if !seen.insert(p) {
702 continue;
703 }
704 if name.is_null(i) || version.is_null(i) {
705 continue;
706 }
707 if let Some(loc) = locs.get(p) {
708 coords.insert(
709 NpmKey { name: name.value(i).to_string(), version: version.value(i).to_string() },
710 loc.clone(),
711 );
712 }
713 }
714 }
715 Ok(Self { archive, coords })
716 }
717
718 pub fn get(&self, name: &str, version: &str) -> Option<NpmPackage> {
721 let loc = self
722 .coords
723 .get(&NpmKey { name: name.to_string(), version: version.to_string() })?;
724 Some(NpmPackage {
725 archive: Arc::clone(&self.archive),
726 loc: loc.clone(),
727 name: name.to_string(),
728 version: version.to_string(),
729 })
730 }
731
732 pub fn list(&self) -> Vec<(String, String)> {
734 self.coords.keys().map(|k| (k.name.clone(), k.version.clone())).collect()
735 }
736
737 pub fn len(&self) -> usize {
738 self.coords.len()
739 }
740 pub fn is_empty(&self) -> bool {
741 self.coords.is_empty()
742 }
743}
744
745impl NpmPackage {
746 pub fn relative_path(&self) -> &str {
753 &self.loc.relative_path
754 }
755
756 pub fn name(&self) -> &str {
758 &self.name
759 }
760 pub fn version(&self) -> &str {
762 &self.version
763 }
764 pub fn size(&self) -> u64 {
766 self.loc.uncompressed_size
767 }
768 pub fn bytes(&self) -> Result<Vec<u8>> {
770 self.loc.read_bytes(&self.archive)
771 }
772 pub fn into_bytes(self) -> Result<Vec<u8>> {
773 self.loc.read_bytes(&self.archive)
774 }
775}
776
777#[derive(Debug, Clone, PartialEq, Eq, Hash)]
786struct GemKey {
787 name: String,
788 version: String,
789 platform: String,
790}
791
792pub struct GemView {
794 archive: Arc<File>,
795 coords: HashMap<GemKey, FileLoc>,
796}
797
798pub struct GemPackage {
802 archive: Arc<File>,
803 loc: FileLoc,
804 name: String,
805 version: String,
806 platform: String,
807}
808
809impl GemView {
810 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
811 let (_schema, batches) =
812 read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(GEM_PKG_TYPE), repo: None })?;
813 let mut coords = HashMap::new();
814 for batch in &batches {
815 let name = batch
816 .column_by_name("name")
817 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
818 let version = batch
819 .column_by_name("version")
820 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
821 let (Some(name), Some(version)) = (name, version) else {
822 continue;
823 };
824 let platform_col = batch
826 .column_by_name("platform")
827 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
828 let locs = group_rows_by_file(batch)?;
829 let paths = batch
830 .column_by_name("relative_path")
831 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
832 .ok_or_else(|| anyhow!("missing relative_path"))?;
833 let mut seen = std::collections::HashSet::new();
834 for i in 0..batch.num_rows() {
835 let p = paths.value(i);
836 if !seen.insert(p) {
837 continue;
838 }
839 if name.is_null(i) || version.is_null(i) {
840 continue;
841 }
842 let platform = match platform_col {
843 Some(c) if !c.is_null(i) && !c.value(i).is_empty() => c.value(i).to_string(),
844 _ => "ruby".to_string(),
845 };
846 if let Some(loc) = locs.get(p) {
847 coords.insert(
848 GemKey {
849 name: name.value(i).to_string(),
850 version: version.value(i).to_string(),
851 platform,
852 },
853 loc.clone(),
854 );
855 }
856 }
857 }
858 Ok(Self { archive, coords })
859 }
860
861 pub fn get(&self, name: &str, version: &str) -> Option<GemPackage> {
863 self.get_platform(name, version, "ruby")
864 }
865
866 pub fn get_platform(&self, name: &str, version: &str, platform: &str) -> Option<GemPackage> {
868 let key = GemKey {
869 name: name.to_string(),
870 version: version.to_string(),
871 platform: platform.to_string(),
872 };
873 let loc = self.coords.get(&key)?;
874 Some(GemPackage {
875 archive: Arc::clone(&self.archive),
876 loc: loc.clone(),
877 name: name.to_string(),
878 version: version.to_string(),
879 platform: platform.to_string(),
880 })
881 }
882
883 pub fn list(&self) -> Vec<(String, String, String)> {
885 self.coords
886 .keys()
887 .map(|k| (k.name.clone(), k.version.clone(), k.platform.clone()))
888 .collect()
889 }
890
891 pub fn len(&self) -> usize {
892 self.coords.len()
893 }
894 pub fn is_empty(&self) -> bool {
895 self.coords.is_empty()
896 }
897}
898
899impl GemPackage {
900 pub fn relative_path(&self) -> &str {
907 &self.loc.relative_path
908 }
909
910 pub fn name(&self) -> &str {
912 &self.name
913 }
914 pub fn version(&self) -> &str {
916 &self.version
917 }
918 pub fn platform(&self) -> &str {
920 &self.platform
921 }
922 pub fn size(&self) -> u64 {
924 self.loc.uncompressed_size
925 }
926 pub fn bytes(&self) -> Result<Vec<u8>> {
928 self.loc.read_bytes(&self.archive)
929 }
930 pub fn into_bytes(self) -> Result<Vec<u8>> {
931 self.loc.read_bytes(&self.archive)
932 }
933}
934
935#[derive(Debug, Clone, PartialEq, Eq, Hash)]
944struct CondaKey {
945 name: String,
946 version: String,
947 build: String,
948 subdir: String,
949}
950
951pub struct CondaView {
953 archive: Arc<File>,
954 coords: HashMap<CondaKey, FileLoc>,
955}
956
957pub struct CondaPackage {
961 archive: Arc<File>,
962 loc: FileLoc,
963 name: String,
964 version: String,
965 build: String,
966 subdir: String,
967}
968
969impl CondaView {
970 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
971 let (_schema, batches) = read_znippy_index_filtered(
972 path,
973 &IndexFilter { pkg_type: Some(CONDA_PKG_TYPE), repo: None },
974 )?;
975 let mut coords = HashMap::new();
976 for batch in &batches {
977 let name = batch
978 .column_by_name("name")
979 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
980 let version = batch
981 .column_by_name("version")
982 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
983 let (Some(name), Some(version)) = (name, version) else {
984 continue;
985 };
986 let build_col = batch
987 .column_by_name("build")
988 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
989 let subdir_col = batch
990 .column_by_name("subdir")
991 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
992 let locs = group_rows_by_file(batch)?;
993 let paths = batch
994 .column_by_name("relative_path")
995 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
996 .ok_or_else(|| anyhow!("missing relative_path"))?;
997 let mut seen = std::collections::HashSet::new();
998 for i in 0..batch.num_rows() {
999 let p = paths.value(i);
1000 if !seen.insert(p) {
1001 continue;
1002 }
1003 if name.is_null(i) || version.is_null(i) {
1004 continue;
1005 }
1006 let build = match build_col {
1007 Some(c) if !c.is_null(i) => c.value(i).to_string(),
1008 _ => String::new(),
1009 };
1010 let subdir = match subdir_col {
1011 Some(c) if !c.is_null(i) && !c.value(i).is_empty() => c.value(i).to_string(),
1012 _ => String::new(),
1013 };
1014 if let Some(loc) = locs.get(p) {
1015 coords.insert(
1016 CondaKey {
1017 name: name.value(i).to_string(),
1018 version: version.value(i).to_string(),
1019 build,
1020 subdir,
1021 },
1022 loc.clone(),
1023 );
1024 }
1025 }
1026 }
1027 Ok(Self { archive, coords })
1028 }
1029
1030 pub fn get(&self, name: &str, version: &str) -> Option<CondaPackage> {
1033 let (key, loc) = self
1034 .coords
1035 .iter()
1036 .find(|(k, _)| k.name == name && k.version == version)?;
1037 Some(CondaPackage {
1038 archive: Arc::clone(&self.archive),
1039 loc: loc.clone(),
1040 name: key.name.clone(),
1041 version: key.version.clone(),
1042 build: key.build.clone(),
1043 subdir: key.subdir.clone(),
1044 })
1045 }
1046
1047 pub fn get_exact(
1049 &self,
1050 name: &str,
1051 version: &str,
1052 build: &str,
1053 subdir: &str,
1054 ) -> Option<CondaPackage> {
1055 let key = CondaKey {
1056 name: name.to_string(),
1057 version: version.to_string(),
1058 build: build.to_string(),
1059 subdir: subdir.to_string(),
1060 };
1061 let loc = self.coords.get(&key)?;
1062 Some(CondaPackage {
1063 archive: Arc::clone(&self.archive),
1064 loc: loc.clone(),
1065 name: name.to_string(),
1066 version: version.to_string(),
1067 build: build.to_string(),
1068 subdir: subdir.to_string(),
1069 })
1070 }
1071
1072 pub fn list(&self) -> Vec<(String, String, String, String)> {
1074 self.coords
1075 .keys()
1076 .map(|k| (k.name.clone(), k.version.clone(), k.build.clone(), k.subdir.clone()))
1077 .collect()
1078 }
1079
1080 pub fn len(&self) -> usize {
1081 self.coords.len()
1082 }
1083 pub fn is_empty(&self) -> bool {
1084 self.coords.is_empty()
1085 }
1086}
1087
1088impl CondaPackage {
1089 pub fn relative_path(&self) -> &str {
1096 &self.loc.relative_path
1097 }
1098
1099 pub fn name(&self) -> &str {
1101 &self.name
1102 }
1103 pub fn version(&self) -> &str {
1105 &self.version
1106 }
1107 pub fn build(&self) -> &str {
1109 &self.build
1110 }
1111 pub fn subdir(&self) -> &str {
1113 &self.subdir
1114 }
1115 pub fn size(&self) -> u64 {
1117 self.loc.uncompressed_size
1118 }
1119 pub fn bytes(&self) -> Result<Vec<u8>> {
1121 self.loc.read_bytes(&self.archive)
1122 }
1123 pub fn into_bytes(self) -> Result<Vec<u8>> {
1124 self.loc.read_bytes(&self.archive)
1125 }
1126}
1127
1128#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1136struct RpmKey {
1137 name: String,
1138 version: String,
1139 release: String,
1140 arch: String,
1141}
1142
1143#[derive(Clone)]
1147struct RpmEntry {
1148 loc: FileLoc,
1149 epoch: Option<String>,
1150 summary: Option<String>,
1151 license: Option<String>,
1152 url: Option<String>,
1153 vendor: Option<String>,
1154 sourcerpm: Option<String>,
1155 provides: Vec<String>,
1157 requires: Vec<String>,
1158}
1159
1160#[derive(Debug, Clone, PartialEq, Eq)]
1163pub struct RpmMetaRow {
1164 pub name: String,
1165 pub version: String,
1166 pub release: String,
1167 pub arch: String,
1168 pub epoch: Option<String>,
1169 pub summary: Option<String>,
1170 pub license: Option<String>,
1171 pub url: Option<String>,
1172 pub vendor: Option<String>,
1173 pub sourcerpm: Option<String>,
1174 pub provides: Vec<String>,
1175 pub requires: Vec<String>,
1176}
1177
1178pub struct RpmView {
1182 archive: Arc<File>,
1183 coords: HashMap<RpmKey, RpmEntry>,
1184}
1185
1186pub struct RpmPackage {
1188 archive: Arc<File>,
1189 loc: FileLoc,
1190 name: String,
1191 version: String,
1192 release: String,
1193 arch: String,
1194 epoch: Option<String>,
1195}
1196
1197impl RpmView {
1198 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
1199 let (_schema, batches) = read_znippy_index_filtered(
1200 path,
1201 &IndexFilter { pkg_type: Some(RPM_PKG_TYPE), repo: None },
1202 )?;
1203 let mut coords = HashMap::new();
1204 for batch in &batches {
1205 let col = |n: &str| {
1206 batch.column_by_name(n).and_then(|c| c.as_any().downcast_ref::<StringArray>())
1207 };
1208 let (Some(name), Some(version)) = (col("name"), col("version")) else {
1209 continue;
1210 };
1211 let (release_c, arch_c, epoch_c) = (col("release"), col("arch"), col("epoch"));
1212 let (summary_c, license_c, url_c) = (col("summary"), col("license"), col("url"));
1215 let (vendor_c, sourcerpm_c) = (col("vendor"), col("sourcerpm"));
1216 let (provides_c, requires_c) = (col("provides"), col("requires"));
1217 let paths = col("relative_path").ok_or_else(|| anyhow!("missing relative_path"))?;
1218 let locs = group_rows_by_file(batch)?;
1219 let mut seen = std::collections::HashSet::new();
1220 for i in 0..batch.num_rows() {
1221 let p = paths.value(i);
1222 if !seen.insert(p) {
1223 continue;
1224 }
1225 if name.is_null(i) || version.is_null(i) {
1226 continue;
1227 }
1228 let Some(loc) = locs.get(p) else { continue };
1229 coords.insert(
1230 RpmKey {
1231 name: name.value(i).to_string(),
1232 version: version.value(i).to_string(),
1233 release: opt_col(release_c, i).unwrap_or_default(),
1234 arch: opt_col(arch_c, i).unwrap_or_default(),
1235 },
1236 RpmEntry {
1237 loc: loc.clone(),
1238 epoch: opt_col(epoch_c, i),
1239 summary: opt_col(summary_c, i),
1240 license: opt_col(license_c, i),
1241 url: opt_col(url_c, i),
1242 vendor: opt_col(vendor_c, i),
1243 sourcerpm: opt_col(sourcerpm_c, i),
1244 provides: split_lines(opt_col(provides_c, i)),
1245 requires: split_lines(opt_col(requires_c, i)),
1246 },
1247 );
1248 }
1249 }
1250 Ok(Self { archive, coords })
1251 }
1252
1253 pub fn get(&self, name: &str, version: &str, release: &str, arch: &str) -> Option<RpmPackage> {
1255 let key = RpmKey {
1256 name: name.to_string(),
1257 version: version.to_string(),
1258 release: release.to_string(),
1259 arch: arch.to_string(),
1260 };
1261 let entry = self.coords.get(&key)?;
1262 Some(RpmPackage {
1263 archive: Arc::clone(&self.archive),
1264 loc: entry.loc.clone(),
1265 name: name.to_string(),
1266 version: version.to_string(),
1267 release: release.to_string(),
1268 arch: arch.to_string(),
1269 epoch: entry.epoch.clone(),
1270 })
1271 }
1272
1273 pub fn list(&self) -> Vec<(String, String, String, String, Option<String>)> {
1276 self.coords
1277 .iter()
1278 .map(|(k, e)| {
1279 (k.name.clone(), k.version.clone(), k.release.clone(), k.arch.clone(), e.epoch.clone())
1280 })
1281 .collect()
1282 }
1283
1284 pub fn list_meta(&self) -> Vec<RpmMetaRow> {
1287 self.coords
1288 .iter()
1289 .map(|(k, e)| RpmMetaRow {
1290 name: k.name.clone(),
1291 version: k.version.clone(),
1292 release: k.release.clone(),
1293 arch: k.arch.clone(),
1294 epoch: e.epoch.clone(),
1295 summary: e.summary.clone(),
1296 license: e.license.clone(),
1297 url: e.url.clone(),
1298 vendor: e.vendor.clone(),
1299 sourcerpm: e.sourcerpm.clone(),
1300 provides: e.provides.clone(),
1301 requires: e.requires.clone(),
1302 })
1303 .collect()
1304 }
1305
1306 pub fn len(&self) -> usize {
1307 self.coords.len()
1308 }
1309 pub fn is_empty(&self) -> bool {
1310 self.coords.is_empty()
1311 }
1312}
1313
1314impl RpmPackage {
1315 pub fn relative_path(&self) -> &str {
1322 &self.loc.relative_path
1323 }
1324
1325 pub fn name(&self) -> &str {
1326 &self.name
1327 }
1328 pub fn version(&self) -> &str {
1329 &self.version
1330 }
1331 pub fn release(&self) -> &str {
1332 &self.release
1333 }
1334 pub fn arch(&self) -> &str {
1335 &self.arch
1336 }
1337 pub fn epoch(&self) -> Option<&str> {
1339 self.epoch.as_deref()
1340 }
1341 pub fn size(&self) -> u64 {
1343 self.loc.uncompressed_size
1344 }
1345 pub fn bytes(&self) -> Result<Vec<u8>> {
1347 self.loc.read_bytes(&self.archive)
1348 }
1349 pub fn into_bytes(self) -> Result<Vec<u8>> {
1350 self.loc.read_bytes(&self.archive)
1351 }
1352}
1353
1354fn opt_col(c: Option<&StringArray>, i: usize) -> Option<String> {
1356 c.filter(|a| !a.is_null(i)).map(|a| a.value(i).to_string())
1357}
1358
1359fn split_lines(v: Option<String>) -> Vec<String> {
1362 v.map(|s| s.lines().filter(|l| !l.is_empty()).map(str::to_string).collect())
1363 .unwrap_or_default()
1364}
1365
1366#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1372struct DebKey {
1373 name: String,
1374 version: String,
1375 arch: String,
1376}
1377
1378#[derive(Clone)]
1384struct DebEntry {
1385 loc: FileLoc,
1386 control: Option<String>,
1387}
1388
1389pub struct DebView {
1393 archive: Arc<File>,
1394 coords: HashMap<DebKey, DebEntry>,
1395}
1396
1397pub struct DebPackage {
1399 archive: Arc<File>,
1400 loc: FileLoc,
1401 name: String,
1402 version: String,
1403 arch: String,
1404 control: Option<String>,
1405}
1406
1407impl DebView {
1408 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
1409 let (_schema, batches) = read_znippy_index_filtered(
1410 path,
1411 &IndexFilter { pkg_type: Some(DEB_PKG_TYPE), repo: None },
1412 )?;
1413 let mut coords = HashMap::new();
1414 for batch in &batches {
1415 let col = |n: &str| {
1416 batch.column_by_name(n).and_then(|c| c.as_any().downcast_ref::<StringArray>())
1417 };
1418 let Some(name) = col("name") else { continue };
1419 let (version_c, arch_c, control_c) = (col("version"), col("arch"), col("control"));
1420 let paths = col("relative_path").ok_or_else(|| anyhow!("missing relative_path"))?;
1421 let locs = group_rows_by_file(batch)?;
1422 let mut seen = std::collections::HashSet::new();
1423 for i in 0..batch.num_rows() {
1424 let p = paths.value(i);
1425 if !seen.insert(p) {
1426 continue;
1427 }
1428 if name.is_null(i) {
1429 continue;
1430 }
1431 let Some(loc) = locs.get(p) else { continue };
1432 coords.insert(
1433 DebKey {
1434 name: name.value(i).to_string(),
1435 version: opt_col(version_c, i).unwrap_or_default(),
1436 arch: opt_col(arch_c, i).unwrap_or_default(),
1437 },
1438 DebEntry { loc: loc.clone(), control: opt_col(control_c, i) },
1439 );
1440 }
1441 }
1442 Ok(Self { archive, coords })
1443 }
1444
1445 pub fn get(&self, name: &str, version: &str, arch: &str) -> Option<DebPackage> {
1447 let key = DebKey {
1448 name: name.to_string(),
1449 version: version.to_string(),
1450 arch: arch.to_string(),
1451 };
1452 let entry = self.coords.get(&key)?;
1453 Some(DebPackage {
1454 archive: Arc::clone(&self.archive),
1455 loc: entry.loc.clone(),
1456 name: name.to_string(),
1457 version: version.to_string(),
1458 arch: arch.to_string(),
1459 control: entry.control.clone(),
1460 })
1461 }
1462
1463 pub fn list(&self) -> Vec<(String, String, String, Option<String>)> {
1466 self.coords
1467 .iter()
1468 .map(|(k, e)| (k.name.clone(), k.version.clone(), k.arch.clone(), e.control.clone()))
1469 .collect()
1470 }
1471
1472 pub fn len(&self) -> usize {
1473 self.coords.len()
1474 }
1475 pub fn is_empty(&self) -> bool {
1476 self.coords.is_empty()
1477 }
1478}
1479
1480impl DebPackage {
1481 pub fn relative_path(&self) -> &str {
1488 &self.loc.relative_path
1489 }
1490
1491 pub fn name(&self) -> &str {
1492 &self.name
1493 }
1494 pub fn version(&self) -> &str {
1495 &self.version
1496 }
1497 pub fn arch(&self) -> &str {
1498 &self.arch
1499 }
1500 pub fn control(&self) -> Option<&str> {
1503 self.control.as_deref()
1504 }
1505 pub fn size(&self) -> u64 {
1506 self.loc.uncompressed_size
1507 }
1508 pub fn bytes(&self) -> Result<Vec<u8>> {
1509 self.loc.read_bytes(&self.archive)
1510 }
1511 pub fn into_bytes(self) -> Result<Vec<u8>> {
1512 self.loc.read_bytes(&self.archive)
1513 }
1514}
1515
1516pub(crate) fn build_rust_view(path: &Path, archive: Arc<File>) -> Result<Option<RustView>> {
1519 let view = RustView::build(path, archive)?;
1520 Ok(if view.is_empty() { None } else { Some(view) })
1521}
1522
1523pub(crate) fn build_maven_view(path: &Path, archive: Arc<File>) -> Result<Option<MavenView>> {
1524 let view = MavenView::build(path, archive)?;
1525 Ok(if view.is_empty() { None } else { Some(view) })
1526}
1527
1528pub(crate) fn build_python_view(path: &Path, archive: Arc<File>) -> Result<Option<PythonView>> {
1529 let view = PythonView::build(path, archive)?;
1530 Ok(if view.is_empty() { None } else { Some(view) })
1531}
1532
1533pub(crate) fn build_npm_view(path: &Path, archive: Arc<File>) -> Result<Option<NpmView>> {
1534 let view = NpmView::build(path, archive)?;
1535 Ok(if view.is_empty() { None } else { Some(view) })
1536}
1537
1538pub(crate) fn build_gem_view(path: &Path, archive: Arc<File>) -> Result<Option<GemView>> {
1539 let view = GemView::build(path, archive)?;
1540 Ok(if view.is_empty() { None } else { Some(view) })
1541}
1542
1543pub(crate) fn build_conda_view(path: &Path, archive: Arc<File>) -> Result<Option<CondaView>> {
1544 let view = CondaView::build(path, archive)?;
1545 Ok(if view.is_empty() { None } else { Some(view) })
1546}
1547
1548pub(crate) fn build_rpm_view(path: &Path, archive: Arc<File>) -> Result<Option<RpmView>> {
1549 let view = RpmView::build(path, archive)?;
1550 Ok(if view.is_empty() { None } else { Some(view) })
1551}
1552
1553pub(crate) fn build_deb_view(path: &Path, archive: Arc<File>) -> Result<Option<DebView>> {
1554 let view = DebView::build(path, archive)?;
1555 Ok(if view.is_empty() { None } else { Some(view) })
1556}