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 chunks: Vec<ChunkRef>,
71 pub uncompressed_size: u64,
73}
74
75#[derive(Debug, Clone, Copy)]
77pub struct ChunkRef {
78 pub blob_offset: u64,
79 pub blob_size: u64,
80 pub fdata_offset: u64,
81 pub compressed: bool,
82}
83
84impl FileLoc {
85 fn read_bytes(&self, archive: &File) -> Result<Vec<u8>> {
88 let mut result = Vec::with_capacity(self.uncompressed_size as usize);
89 let mut blob = Vec::new();
90 let mut decomp = Vec::new();
91 for chunk in &self.chunks {
92 blob.resize(chunk.blob_size as usize, 0);
93 archive.read_exact_at(&mut blob, chunk.blob_offset)?;
94 if chunk.compressed {
95 codec::decompress_into(&blob, &mut decomp)?;
96 result.extend_from_slice(&decomp);
97 } else {
98 result.extend_from_slice(&blob);
99 }
100 }
101 Ok(result)
102 }
103}
104
105fn group_rows_by_file(batch: &RecordBatch) -> Result<HashMap<String, FileLoc>> {
108 use arrow::array::{BooleanArray, UInt64Array};
109
110 let col = |n: &str| {
111 batch
112 .column_by_name(n)
113 .ok_or_else(|| anyhow!("index missing column {n}"))
114 };
115 let paths = col("relative_path")?
116 .as_any()
117 .downcast_ref::<StringArray>()
118 .ok_or_else(|| anyhow!("relative_path not StringArray"))?;
119 let compressed = col("compressed")?
120 .as_any()
121 .downcast_ref::<BooleanArray>()
122 .ok_or_else(|| anyhow!("compressed not BooleanArray"))?;
123 let sizes = col("uncompressed_size")?
124 .as_any()
125 .downcast_ref::<UInt64Array>()
126 .ok_or_else(|| anyhow!("uncompressed_size not UInt64Array"))?;
127 let blob_offset = col("blob_offset")?
128 .as_any()
129 .downcast_ref::<UInt64Array>()
130 .ok_or_else(|| anyhow!("blob_offset not UInt64Array"))?;
131 let blob_size = col("blob_size")?
132 .as_any()
133 .downcast_ref::<UInt64Array>()
134 .ok_or_else(|| anyhow!("blob_size not UInt64Array"))?;
135 let fdata = col("fdata_offset")?
136 .as_any()
137 .downcast_ref::<UInt64Array>()
138 .ok_or_else(|| anyhow!("fdata_offset not UInt64Array"))?;
139
140 let mut by_path: HashMap<String, FileLoc> = HashMap::new();
141 for i in 0..batch.num_rows() {
142 let path = paths.value(i);
143 let entry = by_path.entry(path.to_string()).or_insert_with(|| FileLoc {
144 chunks: Vec::new(),
145 uncompressed_size: 0,
146 });
147 entry.uncompressed_size += sizes.value(i);
148 entry.chunks.push(ChunkRef {
149 blob_offset: blob_offset.value(i),
150 blob_size: blob_size.value(i),
151 fdata_offset: fdata.value(i),
152 compressed: compressed.value(i),
153 });
154 }
155 for f in by_path.values_mut() {
156 f.chunks.sort_by_key(|c| c.fdata_offset);
157 }
158 Ok(by_path)
159}
160
161fn file_name(rel_path: &str) -> &str {
165 rel_path.rsplit('/').next().unwrap_or(rel_path)
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Hash)]
174struct RustKey {
175 name: String,
176 version: String,
177}
178
179pub struct RustView {
181 archive: Arc<File>,
182 coords: HashMap<RustKey, FileLoc>,
183}
184
185pub struct RustPackage {
188 archive: Arc<File>,
189 loc: FileLoc,
190 name: String,
191 version: String,
192}
193
194impl RustView {
195 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
196 let (_schema, batches) =
197 read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(RUST_PKG_TYPE), repo: None })?;
198 let mut coords = HashMap::new();
199 for batch in &batches {
200 let name = batch
201 .column_by_name("crate_name")
202 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
203 let version = batch
204 .column_by_name("version")
205 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
206 let (Some(name), Some(version)) = (name, version) else {
207 continue;
208 };
209 let locs = group_rows_by_file(batch)?;
210 let paths = batch
213 .column_by_name("relative_path")
214 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
215 .ok_or_else(|| anyhow!("missing relative_path"))?;
216 let mut seen = std::collections::HashSet::new();
217 for i in 0..batch.num_rows() {
218 let p = paths.value(i);
219 if !seen.insert(p) {
220 continue;
221 }
222 if name.is_null(i) || version.is_null(i) {
223 continue;
224 }
225 if let Some(loc) = locs.get(p) {
226 coords.insert(
227 RustKey { name: name.value(i).to_string(), version: version.value(i).to_string() },
228 loc.clone(),
229 );
230 }
231 }
232 }
233 Ok(Self { archive, coords })
234 }
235
236 pub fn get(&self, name: &str, version: &str) -> Option<RustPackage> {
238 let loc = self
239 .coords
240 .get(&RustKey { name: name.to_string(), version: version.to_string() })?;
241 Some(RustPackage {
242 archive: Arc::clone(&self.archive),
243 loc: loc.clone(),
244 name: name.to_string(),
245 version: version.to_string(),
246 })
247 }
248
249 pub fn list(&self) -> Vec<(String, String)> {
251 self.coords.keys().map(|k| (k.name.clone(), k.version.clone())).collect()
252 }
253
254 pub fn len(&self) -> usize {
256 self.coords.len()
257 }
258 pub fn is_empty(&self) -> bool {
259 self.coords.is_empty()
260 }
261}
262
263impl RustPackage {
264 pub fn name(&self) -> &str {
266 &self.name
267 }
268 pub fn version(&self) -> &str {
270 &self.version
271 }
272 pub fn size(&self) -> u64 {
274 self.loc.uncompressed_size
275 }
276 pub fn bytes(&self) -> Result<Vec<u8>> {
278 self.loc.read_bytes(&self.archive)
279 }
280 pub fn into_bytes(self) -> Result<Vec<u8>> {
282 self.loc.read_bytes(&self.archive)
283 }
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Hash)]
293struct MavenKey {
294 group: String,
295 artifact: String,
296 version: String,
297 classifier: Option<String>,
298}
299
300pub struct MavenView {
302 archive: Arc<File>,
303 coords: HashMap<MavenKey, FileLoc>,
304}
305
306pub struct MavenPackage {
309 archive: Arc<File>,
310 loc: FileLoc,
311 group: String,
312 artifact: String,
313 version: String,
314 classifier: Option<String>,
315}
316
317impl MavenView {
318 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
319 let (_schema, batches) =
320 read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(MAVEN_PKG_TYPE), repo: None })?;
321 let mut coords = HashMap::new();
322 for batch in &batches {
323 let group = batch
324 .column_by_name("group_id")
325 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
326 let artifact = batch
327 .column_by_name("artifact_id")
328 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
329 let version = batch
330 .column_by_name("version")
331 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
332 let (Some(group), Some(artifact), Some(version)) = (group, artifact, version) else {
333 continue;
334 };
335 let classifier_col = batch
337 .column_by_name("classifier")
338 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
339 let locs = group_rows_by_file(batch)?;
340 let paths = batch
341 .column_by_name("relative_path")
342 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
343 .ok_or_else(|| anyhow!("missing relative_path"))?;
344 let mut seen = std::collections::HashSet::new();
345 for i in 0..batch.num_rows() {
346 let p = paths.value(i);
347 if !seen.insert(p) {
348 continue;
349 }
350 if group.is_null(i) || artifact.is_null(i) || version.is_null(i) {
351 continue;
352 }
353 let classifier = match classifier_col {
356 Some(c) if !c.is_null(i) && !c.value(i).is_empty() => Some(c.value(i).to_string()),
357 _ => derive_classifier(file_name(p), artifact.value(i), version.value(i)),
358 };
359 if let Some(loc) = locs.get(p) {
360 coords.insert(
361 MavenKey {
362 group: group.value(i).to_string(),
363 artifact: artifact.value(i).to_string(),
364 version: version.value(i).to_string(),
365 classifier,
366 },
367 loc.clone(),
368 );
369 }
370 }
371 }
372 Ok(Self { archive, coords })
373 }
374
375 pub fn get(&self, group: &str, artifact: &str, version: &str) -> Option<MavenPackage> {
377 self.get_classified(group, artifact, version, None)
378 }
379
380 pub fn get_classified(
383 &self,
384 group: &str,
385 artifact: &str,
386 version: &str,
387 classifier: Option<&str>,
388 ) -> Option<MavenPackage> {
389 let key = MavenKey {
390 group: group.to_string(),
391 artifact: artifact.to_string(),
392 version: version.to_string(),
393 classifier: classifier.map(|s| s.to_string()),
394 };
395 let loc = self.coords.get(&key)?;
396 Some(MavenPackage {
397 archive: Arc::clone(&self.archive),
398 loc: loc.clone(),
399 group: group.to_string(),
400 artifact: artifact.to_string(),
401 version: version.to_string(),
402 classifier: classifier.map(|s| s.to_string()),
403 })
404 }
405
406 pub fn list(&self) -> Vec<(String, String, String, Option<String>)> {
408 self.coords
409 .keys()
410 .map(|k| (k.group.clone(), k.artifact.clone(), k.version.clone(), k.classifier.clone()))
411 .collect()
412 }
413
414 pub fn len(&self) -> usize {
415 self.coords.len()
416 }
417 pub fn is_empty(&self) -> bool {
418 self.coords.is_empty()
419 }
420}
421
422fn derive_classifier(filename: &str, artifact: &str, version: &str) -> Option<String> {
426 let stem = filename.rsplit_once('.').map(|(s, _)| s).unwrap_or(filename);
428 let prefix = format!("{artifact}-{version}");
429 let rest = stem.strip_prefix(&prefix)?;
430 let rest = rest.strip_prefix('-')?;
431 if rest.is_empty() {
432 None
433 } else {
434 Some(rest.to_string())
435 }
436}
437
438impl MavenPackage {
439 pub fn group(&self) -> &str {
441 &self.group
442 }
443 pub fn artifact(&self) -> &str {
445 &self.artifact
446 }
447 pub fn version(&self) -> &str {
449 &self.version
450 }
451 pub fn classifier(&self) -> Option<&str> {
453 self.classifier.as_deref()
454 }
455 pub fn coords(&self) -> (&str, &str, &str, Option<&str>) {
457 (&self.group, &self.artifact, &self.version, self.classifier.as_deref())
458 }
459 pub fn size(&self) -> u64 {
460 self.loc.uncompressed_size
461 }
462 pub fn bytes(&self) -> Result<Vec<u8>> {
464 self.loc.read_bytes(&self.archive)
465 }
466 pub fn into_bytes(self) -> Result<Vec<u8>> {
467 self.loc.read_bytes(&self.archive)
468 }
469}
470
471#[derive(Debug, Clone, Copy, PartialEq, Eq)]
477pub enum PythonKind {
478 Wheel,
479 Sdist,
480}
481
482#[derive(Debug, Clone, PartialEq, Eq, Hash)]
484struct PythonKey {
485 name: String,
486 version: String,
487}
488
489pub struct PythonView {
491 archive: Arc<File>,
492 coords: HashMap<PythonKey, FileLoc>,
493 kinds: HashMap<PythonKey, PythonKind>,
494}
495
496pub struct PythonPackage {
498 archive: Arc<File>,
499 loc: FileLoc,
500 name: String,
501 version: String,
502 kind: PythonKind,
503}
504
505impl PythonView {
506 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
507 let (_schema, batches) =
508 read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(PYTHON_PKG_TYPE), repo: None })?;
509 let mut coords = HashMap::new();
510 let mut kinds = HashMap::new();
511 for batch in &batches {
512 let name = batch
513 .column_by_name("name")
514 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
515 let version = batch
516 .column_by_name("version")
517 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
518 let (Some(name), Some(version)) = (name, version) else {
519 continue;
520 };
521 let locs = group_rows_by_file(batch)?;
522 let paths = batch
523 .column_by_name("relative_path")
524 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
525 .ok_or_else(|| anyhow!("missing relative_path"))?;
526 let mut seen = std::collections::HashSet::new();
527 for i in 0..batch.num_rows() {
528 let p = paths.value(i);
529 if !seen.insert(p) {
530 continue;
531 }
532 if name.is_null(i) || version.is_null(i) {
533 continue;
534 }
535 let key = PythonKey { name: name.value(i).to_string(), version: version.value(i).to_string() };
536 let kind = if file_name(p).ends_with(".whl") {
537 PythonKind::Wheel
538 } else {
539 PythonKind::Sdist
540 };
541 if let Some(loc) = locs.get(p) {
542 let replace = matches!(kind, PythonKind::Wheel)
544 || !coords.contains_key(&key);
545 if replace {
546 coords.insert(key.clone(), loc.clone());
547 kinds.insert(key, kind);
548 }
549 }
550 }
551 }
552 Ok(Self { archive, coords, kinds })
553 }
554
555 pub fn get(&self, name: &str, version: &str) -> Option<PythonPackage> {
557 let key = PythonKey { name: name.to_string(), version: version.to_string() };
558 let loc = self.coords.get(&key)?;
559 let kind = self.kinds.get(&key).copied().unwrap_or(PythonKind::Sdist);
560 Some(PythonPackage {
561 archive: Arc::clone(&self.archive),
562 loc: loc.clone(),
563 name: name.to_string(),
564 version: version.to_string(),
565 kind,
566 })
567 }
568
569 pub fn list(&self) -> Vec<(String, String)> {
571 self.coords.keys().map(|k| (k.name.clone(), k.version.clone())).collect()
572 }
573
574 pub fn len(&self) -> usize {
575 self.coords.len()
576 }
577 pub fn is_empty(&self) -> bool {
578 self.coords.is_empty()
579 }
580}
581
582impl PythonPackage {
583 pub fn name(&self) -> &str {
584 &self.name
585 }
586 pub fn version(&self) -> &str {
587 &self.version
588 }
589 pub fn kind(&self) -> PythonKind {
591 self.kind
592 }
593 pub fn size(&self) -> u64 {
594 self.loc.uncompressed_size
595 }
596 pub fn bytes(&self) -> Result<Vec<u8>> {
598 self.loc.read_bytes(&self.archive)
599 }
600 pub fn into_bytes(self) -> Result<Vec<u8>> {
601 self.loc.read_bytes(&self.archive)
602 }
603}
604
605#[derive(Debug, Clone, PartialEq, Eq, Hash)]
613struct NpmKey {
614 name: String,
615 version: String,
616}
617
618pub struct NpmView {
620 archive: Arc<File>,
621 coords: HashMap<NpmKey, FileLoc>,
622}
623
624pub struct NpmPackage {
628 archive: Arc<File>,
629 loc: FileLoc,
630 name: String,
631 version: String,
632}
633
634impl NpmView {
635 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
636 let (_schema, batches) =
637 read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(NPM_PKG_TYPE), repo: None })?;
638 let mut coords = HashMap::new();
639 for batch in &batches {
640 let name = batch
641 .column_by_name("name")
642 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
643 let version = batch
644 .column_by_name("version")
645 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
646 let (Some(name), Some(version)) = (name, version) else {
647 continue;
648 };
649 let locs = group_rows_by_file(batch)?;
650 let paths = batch
651 .column_by_name("relative_path")
652 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
653 .ok_or_else(|| anyhow!("missing relative_path"))?;
654 let mut seen = std::collections::HashSet::new();
655 for i in 0..batch.num_rows() {
656 let p = paths.value(i);
657 if !seen.insert(p) {
658 continue;
659 }
660 if name.is_null(i) || version.is_null(i) {
661 continue;
662 }
663 if let Some(loc) = locs.get(p) {
664 coords.insert(
665 NpmKey { name: name.value(i).to_string(), version: version.value(i).to_string() },
666 loc.clone(),
667 );
668 }
669 }
670 }
671 Ok(Self { archive, coords })
672 }
673
674 pub fn get(&self, name: &str, version: &str) -> Option<NpmPackage> {
677 let loc = self
678 .coords
679 .get(&NpmKey { name: name.to_string(), version: version.to_string() })?;
680 Some(NpmPackage {
681 archive: Arc::clone(&self.archive),
682 loc: loc.clone(),
683 name: name.to_string(),
684 version: version.to_string(),
685 })
686 }
687
688 pub fn list(&self) -> Vec<(String, String)> {
690 self.coords.keys().map(|k| (k.name.clone(), k.version.clone())).collect()
691 }
692
693 pub fn len(&self) -> usize {
694 self.coords.len()
695 }
696 pub fn is_empty(&self) -> bool {
697 self.coords.is_empty()
698 }
699}
700
701impl NpmPackage {
702 pub fn name(&self) -> &str {
704 &self.name
705 }
706 pub fn version(&self) -> &str {
708 &self.version
709 }
710 pub fn size(&self) -> u64 {
712 self.loc.uncompressed_size
713 }
714 pub fn bytes(&self) -> Result<Vec<u8>> {
716 self.loc.read_bytes(&self.archive)
717 }
718 pub fn into_bytes(self) -> Result<Vec<u8>> {
719 self.loc.read_bytes(&self.archive)
720 }
721}
722
723#[derive(Debug, Clone, PartialEq, Eq, Hash)]
732struct GemKey {
733 name: String,
734 version: String,
735 platform: String,
736}
737
738pub struct GemView {
740 archive: Arc<File>,
741 coords: HashMap<GemKey, FileLoc>,
742}
743
744pub struct GemPackage {
748 archive: Arc<File>,
749 loc: FileLoc,
750 name: String,
751 version: String,
752 platform: String,
753}
754
755impl GemView {
756 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
757 let (_schema, batches) =
758 read_znippy_index_filtered(path, &IndexFilter { pkg_type: Some(GEM_PKG_TYPE), repo: None })?;
759 let mut coords = HashMap::new();
760 for batch in &batches {
761 let name = batch
762 .column_by_name("name")
763 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
764 let version = batch
765 .column_by_name("version")
766 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
767 let (Some(name), Some(version)) = (name, version) else {
768 continue;
769 };
770 let platform_col = batch
772 .column_by_name("platform")
773 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
774 let locs = group_rows_by_file(batch)?;
775 let paths = batch
776 .column_by_name("relative_path")
777 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
778 .ok_or_else(|| anyhow!("missing relative_path"))?;
779 let mut seen = std::collections::HashSet::new();
780 for i in 0..batch.num_rows() {
781 let p = paths.value(i);
782 if !seen.insert(p) {
783 continue;
784 }
785 if name.is_null(i) || version.is_null(i) {
786 continue;
787 }
788 let platform = match platform_col {
789 Some(c) if !c.is_null(i) && !c.value(i).is_empty() => c.value(i).to_string(),
790 _ => "ruby".to_string(),
791 };
792 if let Some(loc) = locs.get(p) {
793 coords.insert(
794 GemKey {
795 name: name.value(i).to_string(),
796 version: version.value(i).to_string(),
797 platform,
798 },
799 loc.clone(),
800 );
801 }
802 }
803 }
804 Ok(Self { archive, coords })
805 }
806
807 pub fn get(&self, name: &str, version: &str) -> Option<GemPackage> {
809 self.get_platform(name, version, "ruby")
810 }
811
812 pub fn get_platform(&self, name: &str, version: &str, platform: &str) -> Option<GemPackage> {
814 let key = GemKey {
815 name: name.to_string(),
816 version: version.to_string(),
817 platform: platform.to_string(),
818 };
819 let loc = self.coords.get(&key)?;
820 Some(GemPackage {
821 archive: Arc::clone(&self.archive),
822 loc: loc.clone(),
823 name: name.to_string(),
824 version: version.to_string(),
825 platform: platform.to_string(),
826 })
827 }
828
829 pub fn list(&self) -> Vec<(String, String, String)> {
831 self.coords
832 .keys()
833 .map(|k| (k.name.clone(), k.version.clone(), k.platform.clone()))
834 .collect()
835 }
836
837 pub fn len(&self) -> usize {
838 self.coords.len()
839 }
840 pub fn is_empty(&self) -> bool {
841 self.coords.is_empty()
842 }
843}
844
845impl GemPackage {
846 pub fn name(&self) -> &str {
848 &self.name
849 }
850 pub fn version(&self) -> &str {
852 &self.version
853 }
854 pub fn platform(&self) -> &str {
856 &self.platform
857 }
858 pub fn size(&self) -> u64 {
860 self.loc.uncompressed_size
861 }
862 pub fn bytes(&self) -> Result<Vec<u8>> {
864 self.loc.read_bytes(&self.archive)
865 }
866 pub fn into_bytes(self) -> Result<Vec<u8>> {
867 self.loc.read_bytes(&self.archive)
868 }
869}
870
871#[derive(Debug, Clone, PartialEq, Eq, Hash)]
880struct CondaKey {
881 name: String,
882 version: String,
883 build: String,
884 subdir: String,
885}
886
887pub struct CondaView {
889 archive: Arc<File>,
890 coords: HashMap<CondaKey, FileLoc>,
891}
892
893pub struct CondaPackage {
897 archive: Arc<File>,
898 loc: FileLoc,
899 name: String,
900 version: String,
901 build: String,
902 subdir: String,
903}
904
905impl CondaView {
906 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
907 let (_schema, batches) = read_znippy_index_filtered(
908 path,
909 &IndexFilter { pkg_type: Some(CONDA_PKG_TYPE), repo: None },
910 )?;
911 let mut coords = HashMap::new();
912 for batch in &batches {
913 let name = batch
914 .column_by_name("name")
915 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
916 let version = batch
917 .column_by_name("version")
918 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
919 let (Some(name), Some(version)) = (name, version) else {
920 continue;
921 };
922 let build_col = batch
923 .column_by_name("build")
924 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
925 let subdir_col = batch
926 .column_by_name("subdir")
927 .and_then(|c| c.as_any().downcast_ref::<StringArray>());
928 let locs = group_rows_by_file(batch)?;
929 let paths = batch
930 .column_by_name("relative_path")
931 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
932 .ok_or_else(|| anyhow!("missing relative_path"))?;
933 let mut seen = std::collections::HashSet::new();
934 for i in 0..batch.num_rows() {
935 let p = paths.value(i);
936 if !seen.insert(p) {
937 continue;
938 }
939 if name.is_null(i) || version.is_null(i) {
940 continue;
941 }
942 let build = match build_col {
943 Some(c) if !c.is_null(i) => c.value(i).to_string(),
944 _ => String::new(),
945 };
946 let subdir = match subdir_col {
947 Some(c) if !c.is_null(i) && !c.value(i).is_empty() => c.value(i).to_string(),
948 _ => String::new(),
949 };
950 if let Some(loc) = locs.get(p) {
951 coords.insert(
952 CondaKey {
953 name: name.value(i).to_string(),
954 version: version.value(i).to_string(),
955 build,
956 subdir,
957 },
958 loc.clone(),
959 );
960 }
961 }
962 }
963 Ok(Self { archive, coords })
964 }
965
966 pub fn get(&self, name: &str, version: &str) -> Option<CondaPackage> {
969 let (key, loc) = self
970 .coords
971 .iter()
972 .find(|(k, _)| k.name == name && k.version == version)?;
973 Some(CondaPackage {
974 archive: Arc::clone(&self.archive),
975 loc: loc.clone(),
976 name: key.name.clone(),
977 version: key.version.clone(),
978 build: key.build.clone(),
979 subdir: key.subdir.clone(),
980 })
981 }
982
983 pub fn get_exact(
985 &self,
986 name: &str,
987 version: &str,
988 build: &str,
989 subdir: &str,
990 ) -> Option<CondaPackage> {
991 let key = CondaKey {
992 name: name.to_string(),
993 version: version.to_string(),
994 build: build.to_string(),
995 subdir: subdir.to_string(),
996 };
997 let loc = self.coords.get(&key)?;
998 Some(CondaPackage {
999 archive: Arc::clone(&self.archive),
1000 loc: loc.clone(),
1001 name: name.to_string(),
1002 version: version.to_string(),
1003 build: build.to_string(),
1004 subdir: subdir.to_string(),
1005 })
1006 }
1007
1008 pub fn list(&self) -> Vec<(String, String, String, String)> {
1010 self.coords
1011 .keys()
1012 .map(|k| (k.name.clone(), k.version.clone(), k.build.clone(), k.subdir.clone()))
1013 .collect()
1014 }
1015
1016 pub fn len(&self) -> usize {
1017 self.coords.len()
1018 }
1019 pub fn is_empty(&self) -> bool {
1020 self.coords.is_empty()
1021 }
1022}
1023
1024impl CondaPackage {
1025 pub fn name(&self) -> &str {
1027 &self.name
1028 }
1029 pub fn version(&self) -> &str {
1031 &self.version
1032 }
1033 pub fn build(&self) -> &str {
1035 &self.build
1036 }
1037 pub fn subdir(&self) -> &str {
1039 &self.subdir
1040 }
1041 pub fn size(&self) -> u64 {
1043 self.loc.uncompressed_size
1044 }
1045 pub fn bytes(&self) -> Result<Vec<u8>> {
1047 self.loc.read_bytes(&self.archive)
1048 }
1049 pub fn into_bytes(self) -> Result<Vec<u8>> {
1050 self.loc.read_bytes(&self.archive)
1051 }
1052}
1053
1054#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1062struct RpmKey {
1063 name: String,
1064 version: String,
1065 release: String,
1066 arch: String,
1067}
1068
1069#[derive(Clone)]
1073struct RpmEntry {
1074 loc: FileLoc,
1075 epoch: Option<String>,
1076 summary: Option<String>,
1077 license: Option<String>,
1078 url: Option<String>,
1079 vendor: Option<String>,
1080 sourcerpm: Option<String>,
1081 provides: Vec<String>,
1083 requires: Vec<String>,
1084}
1085
1086#[derive(Debug, Clone, PartialEq, Eq)]
1089pub struct RpmMetaRow {
1090 pub name: String,
1091 pub version: String,
1092 pub release: String,
1093 pub arch: String,
1094 pub epoch: Option<String>,
1095 pub summary: Option<String>,
1096 pub license: Option<String>,
1097 pub url: Option<String>,
1098 pub vendor: Option<String>,
1099 pub sourcerpm: Option<String>,
1100 pub provides: Vec<String>,
1101 pub requires: Vec<String>,
1102}
1103
1104pub struct RpmView {
1108 archive: Arc<File>,
1109 coords: HashMap<RpmKey, RpmEntry>,
1110}
1111
1112pub struct RpmPackage {
1114 archive: Arc<File>,
1115 loc: FileLoc,
1116 name: String,
1117 version: String,
1118 release: String,
1119 arch: String,
1120 epoch: Option<String>,
1121}
1122
1123impl RpmView {
1124 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
1125 let (_schema, batches) = read_znippy_index_filtered(
1126 path,
1127 &IndexFilter { pkg_type: Some(RPM_PKG_TYPE), repo: None },
1128 )?;
1129 let mut coords = HashMap::new();
1130 for batch in &batches {
1131 let col = |n: &str| {
1132 batch.column_by_name(n).and_then(|c| c.as_any().downcast_ref::<StringArray>())
1133 };
1134 let (Some(name), Some(version)) = (col("name"), col("version")) else {
1135 continue;
1136 };
1137 let (release_c, arch_c, epoch_c) = (col("release"), col("arch"), col("epoch"));
1138 let (summary_c, license_c, url_c) = (col("summary"), col("license"), col("url"));
1141 let (vendor_c, sourcerpm_c) = (col("vendor"), col("sourcerpm"));
1142 let (provides_c, requires_c) = (col("provides"), col("requires"));
1143 let paths = col("relative_path").ok_or_else(|| anyhow!("missing relative_path"))?;
1144 let locs = group_rows_by_file(batch)?;
1145 let mut seen = std::collections::HashSet::new();
1146 for i in 0..batch.num_rows() {
1147 let p = paths.value(i);
1148 if !seen.insert(p) {
1149 continue;
1150 }
1151 if name.is_null(i) || version.is_null(i) {
1152 continue;
1153 }
1154 let Some(loc) = locs.get(p) else { continue };
1155 coords.insert(
1156 RpmKey {
1157 name: name.value(i).to_string(),
1158 version: version.value(i).to_string(),
1159 release: opt_col(release_c, i).unwrap_or_default(),
1160 arch: opt_col(arch_c, i).unwrap_or_default(),
1161 },
1162 RpmEntry {
1163 loc: loc.clone(),
1164 epoch: opt_col(epoch_c, i),
1165 summary: opt_col(summary_c, i),
1166 license: opt_col(license_c, i),
1167 url: opt_col(url_c, i),
1168 vendor: opt_col(vendor_c, i),
1169 sourcerpm: opt_col(sourcerpm_c, i),
1170 provides: split_lines(opt_col(provides_c, i)),
1171 requires: split_lines(opt_col(requires_c, i)),
1172 },
1173 );
1174 }
1175 }
1176 Ok(Self { archive, coords })
1177 }
1178
1179 pub fn get(&self, name: &str, version: &str, release: &str, arch: &str) -> Option<RpmPackage> {
1181 let key = RpmKey {
1182 name: name.to_string(),
1183 version: version.to_string(),
1184 release: release.to_string(),
1185 arch: arch.to_string(),
1186 };
1187 let entry = self.coords.get(&key)?;
1188 Some(RpmPackage {
1189 archive: Arc::clone(&self.archive),
1190 loc: entry.loc.clone(),
1191 name: name.to_string(),
1192 version: version.to_string(),
1193 release: release.to_string(),
1194 arch: arch.to_string(),
1195 epoch: entry.epoch.clone(),
1196 })
1197 }
1198
1199 pub fn list(&self) -> Vec<(String, String, String, String, Option<String>)> {
1202 self.coords
1203 .iter()
1204 .map(|(k, e)| {
1205 (k.name.clone(), k.version.clone(), k.release.clone(), k.arch.clone(), e.epoch.clone())
1206 })
1207 .collect()
1208 }
1209
1210 pub fn list_meta(&self) -> Vec<RpmMetaRow> {
1213 self.coords
1214 .iter()
1215 .map(|(k, e)| RpmMetaRow {
1216 name: k.name.clone(),
1217 version: k.version.clone(),
1218 release: k.release.clone(),
1219 arch: k.arch.clone(),
1220 epoch: e.epoch.clone(),
1221 summary: e.summary.clone(),
1222 license: e.license.clone(),
1223 url: e.url.clone(),
1224 vendor: e.vendor.clone(),
1225 sourcerpm: e.sourcerpm.clone(),
1226 provides: e.provides.clone(),
1227 requires: e.requires.clone(),
1228 })
1229 .collect()
1230 }
1231
1232 pub fn len(&self) -> usize {
1233 self.coords.len()
1234 }
1235 pub fn is_empty(&self) -> bool {
1236 self.coords.is_empty()
1237 }
1238}
1239
1240impl RpmPackage {
1241 pub fn name(&self) -> &str {
1242 &self.name
1243 }
1244 pub fn version(&self) -> &str {
1245 &self.version
1246 }
1247 pub fn release(&self) -> &str {
1248 &self.release
1249 }
1250 pub fn arch(&self) -> &str {
1251 &self.arch
1252 }
1253 pub fn epoch(&self) -> Option<&str> {
1255 self.epoch.as_deref()
1256 }
1257 pub fn size(&self) -> u64 {
1259 self.loc.uncompressed_size
1260 }
1261 pub fn bytes(&self) -> Result<Vec<u8>> {
1263 self.loc.read_bytes(&self.archive)
1264 }
1265 pub fn into_bytes(self) -> Result<Vec<u8>> {
1266 self.loc.read_bytes(&self.archive)
1267 }
1268}
1269
1270fn opt_col(c: Option<&StringArray>, i: usize) -> Option<String> {
1272 c.filter(|a| !a.is_null(i)).map(|a| a.value(i).to_string())
1273}
1274
1275fn split_lines(v: Option<String>) -> Vec<String> {
1278 v.map(|s| s.lines().filter(|l| !l.is_empty()).map(str::to_string).collect())
1279 .unwrap_or_default()
1280}
1281
1282#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1288struct DebKey {
1289 name: String,
1290 version: String,
1291 arch: String,
1292}
1293
1294#[derive(Clone)]
1300struct DebEntry {
1301 loc: FileLoc,
1302 control: Option<String>,
1303}
1304
1305pub struct DebView {
1309 archive: Arc<File>,
1310 coords: HashMap<DebKey, DebEntry>,
1311}
1312
1313pub struct DebPackage {
1315 archive: Arc<File>,
1316 loc: FileLoc,
1317 name: String,
1318 version: String,
1319 arch: String,
1320 control: Option<String>,
1321}
1322
1323impl DebView {
1324 fn build(path: &Path, archive: Arc<File>) -> Result<Self> {
1325 let (_schema, batches) = read_znippy_index_filtered(
1326 path,
1327 &IndexFilter { pkg_type: Some(DEB_PKG_TYPE), repo: None },
1328 )?;
1329 let mut coords = HashMap::new();
1330 for batch in &batches {
1331 let col = |n: &str| {
1332 batch.column_by_name(n).and_then(|c| c.as_any().downcast_ref::<StringArray>())
1333 };
1334 let Some(name) = col("name") else { continue };
1335 let (version_c, arch_c, control_c) = (col("version"), col("arch"), col("control"));
1336 let paths = col("relative_path").ok_or_else(|| anyhow!("missing relative_path"))?;
1337 let locs = group_rows_by_file(batch)?;
1338 let mut seen = std::collections::HashSet::new();
1339 for i in 0..batch.num_rows() {
1340 let p = paths.value(i);
1341 if !seen.insert(p) {
1342 continue;
1343 }
1344 if name.is_null(i) {
1345 continue;
1346 }
1347 let Some(loc) = locs.get(p) else { continue };
1348 coords.insert(
1349 DebKey {
1350 name: name.value(i).to_string(),
1351 version: opt_col(version_c, i).unwrap_or_default(),
1352 arch: opt_col(arch_c, i).unwrap_or_default(),
1353 },
1354 DebEntry { loc: loc.clone(), control: opt_col(control_c, i) },
1355 );
1356 }
1357 }
1358 Ok(Self { archive, coords })
1359 }
1360
1361 pub fn get(&self, name: &str, version: &str, arch: &str) -> Option<DebPackage> {
1363 let key = DebKey {
1364 name: name.to_string(),
1365 version: version.to_string(),
1366 arch: arch.to_string(),
1367 };
1368 let entry = self.coords.get(&key)?;
1369 Some(DebPackage {
1370 archive: Arc::clone(&self.archive),
1371 loc: entry.loc.clone(),
1372 name: name.to_string(),
1373 version: version.to_string(),
1374 arch: arch.to_string(),
1375 control: entry.control.clone(),
1376 })
1377 }
1378
1379 pub fn list(&self) -> Vec<(String, String, String, Option<String>)> {
1382 self.coords
1383 .iter()
1384 .map(|(k, e)| (k.name.clone(), k.version.clone(), k.arch.clone(), e.control.clone()))
1385 .collect()
1386 }
1387
1388 pub fn len(&self) -> usize {
1389 self.coords.len()
1390 }
1391 pub fn is_empty(&self) -> bool {
1392 self.coords.is_empty()
1393 }
1394}
1395
1396impl DebPackage {
1397 pub fn name(&self) -> &str {
1398 &self.name
1399 }
1400 pub fn version(&self) -> &str {
1401 &self.version
1402 }
1403 pub fn arch(&self) -> &str {
1404 &self.arch
1405 }
1406 pub fn control(&self) -> Option<&str> {
1409 self.control.as_deref()
1410 }
1411 pub fn size(&self) -> u64 {
1412 self.loc.uncompressed_size
1413 }
1414 pub fn bytes(&self) -> Result<Vec<u8>> {
1415 self.loc.read_bytes(&self.archive)
1416 }
1417 pub fn into_bytes(self) -> Result<Vec<u8>> {
1418 self.loc.read_bytes(&self.archive)
1419 }
1420}
1421
1422pub(crate) fn build_rust_view(path: &Path, archive: Arc<File>) -> Result<Option<RustView>> {
1425 let view = RustView::build(path, archive)?;
1426 Ok(if view.is_empty() { None } else { Some(view) })
1427}
1428
1429pub(crate) fn build_maven_view(path: &Path, archive: Arc<File>) -> Result<Option<MavenView>> {
1430 let view = MavenView::build(path, archive)?;
1431 Ok(if view.is_empty() { None } else { Some(view) })
1432}
1433
1434pub(crate) fn build_python_view(path: &Path, archive: Arc<File>) -> Result<Option<PythonView>> {
1435 let view = PythonView::build(path, archive)?;
1436 Ok(if view.is_empty() { None } else { Some(view) })
1437}
1438
1439pub(crate) fn build_npm_view(path: &Path, archive: Arc<File>) -> Result<Option<NpmView>> {
1440 let view = NpmView::build(path, archive)?;
1441 Ok(if view.is_empty() { None } else { Some(view) })
1442}
1443
1444pub(crate) fn build_gem_view(path: &Path, archive: Arc<File>) -> Result<Option<GemView>> {
1445 let view = GemView::build(path, archive)?;
1446 Ok(if view.is_empty() { None } else { Some(view) })
1447}
1448
1449pub(crate) fn build_conda_view(path: &Path, archive: Arc<File>) -> Result<Option<CondaView>> {
1450 let view = CondaView::build(path, archive)?;
1451 Ok(if view.is_empty() { None } else { Some(view) })
1452}
1453
1454pub(crate) fn build_rpm_view(path: &Path, archive: Arc<File>) -> Result<Option<RpmView>> {
1455 let view = RpmView::build(path, archive)?;
1456 Ok(if view.is_empty() { None } else { Some(view) })
1457}
1458
1459pub(crate) fn build_deb_view(path: &Path, archive: Arc<File>) -> Result<Option<DebView>> {
1460 let view = DebView::build(path, archive)?;
1461 Ok(if view.is_empty() { None } else { Some(view) })
1462}