1use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
27use crate::cache;
28use rayon::prelude::*;
29use std::collections::HashMap;
30use std::time::Duration;
31
32const CACHE_TTL: Duration = Duration::from_secs(60 * 60);
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum OpenSuseRepo {
38 TumbleweedOss,
41 TumbleweedNonOss,
43 TumbleweedUpdate,
45
46 Leap160Oss,
49 Leap160NonOss,
51
52 Leap156Oss,
55 Leap156NonOss,
57 Leap156UpdateOss,
59 Leap156UpdateNonOss,
61 Leap156Backports,
63 Leap156Sle,
65
66 TumbleweedSrcOss,
69 TumbleweedSrcNonOss,
71 Leap156SrcOss,
73 Leap156SrcNonOss,
75
76 TumbleweedDebug,
79 Leap160Debug,
81 Leap156Debug,
83 Leap156DebugUpdate,
85
86 FactoryOss,
89
90 GamesTumbleweed,
93 GamesLeap156,
95
96 KdeExtraTumbleweed,
99 KdeFrameworksTumbleweed,
101 KdeQt6Tumbleweed,
103
104 GnomeAppsLeap160,
107 GnomeAppsLeap156,
109 GnomeFactory,
111
112 XfceTumbleweed,
115 XfceLeap160,
117 XfceLeap156,
119
120 MozillaTumbleweed,
123 ScienceTumbleweed,
125 WineTumbleweed,
127 ServerHttpTumbleweed,
129 ServerDatabaseTumbleweed,
131}
132
133impl OpenSuseRepo {
134 fn id(&self) -> &'static str {
136 match self {
137 Self::TumbleweedOss => "tumbleweed-oss",
139 Self::TumbleweedNonOss => "tumbleweed-non-oss",
140 Self::TumbleweedUpdate => "tumbleweed-update",
141 Self::Leap160Oss => "leap-16.0-oss",
142 Self::Leap160NonOss => "leap-16.0-non-oss",
143 Self::Leap156Oss => "leap-15.6-oss",
144 Self::Leap156NonOss => "leap-15.6-non-oss",
145 Self::Leap156UpdateOss => "leap-15.6-update-oss",
146 Self::Leap156UpdateNonOss => "leap-15.6-update-non-oss",
147 Self::Leap156Backports => "leap-15.6-backports",
148 Self::Leap156Sle => "leap-15.6-sle",
149 Self::TumbleweedSrcOss => "tumbleweed-src-oss",
151 Self::TumbleweedSrcNonOss => "tumbleweed-src-non-oss",
152 Self::Leap156SrcOss => "leap-15.6-src-oss",
153 Self::Leap156SrcNonOss => "leap-15.6-src-non-oss",
154 Self::TumbleweedDebug => "tumbleweed-debug",
156 Self::Leap160Debug => "leap-16.0-debug",
157 Self::Leap156Debug => "leap-15.6-debug",
158 Self::Leap156DebugUpdate => "leap-15.6-debug-update",
159 Self::FactoryOss => "factory-oss",
160 Self::GamesTumbleweed => "games-tumbleweed",
162 Self::GamesLeap156 => "games-leap-15.6",
163 Self::KdeExtraTumbleweed => "kde-extra-tumbleweed",
164 Self::KdeFrameworksTumbleweed => "kde-frameworks-tumbleweed",
165 Self::KdeQt6Tumbleweed => "kde-qt6-tumbleweed",
166 Self::GnomeAppsLeap160 => "gnome-apps-leap-16.0",
167 Self::GnomeAppsLeap156 => "gnome-apps-leap-15.6",
168 Self::GnomeFactory => "gnome-factory",
169 Self::XfceTumbleweed => "xfce-tumbleweed",
170 Self::XfceLeap160 => "xfce-leap-16.0",
171 Self::XfceLeap156 => "xfce-leap-15.6",
172 Self::MozillaTumbleweed => "mozilla-tumbleweed",
173 Self::ScienceTumbleweed => "science-tumbleweed",
174 Self::WineTumbleweed => "wine-tumbleweed",
175 Self::ServerHttpTumbleweed => "server-http-tumbleweed",
176 Self::ServerDatabaseTumbleweed => "server-database-tumbleweed",
177 }
178 }
179
180 fn base_url(&self) -> &'static str {
182 match self {
183 Self::TumbleweedOss => "https://download.opensuse.org/tumbleweed/repo/oss/repodata",
185 Self::TumbleweedNonOss => {
186 "https://download.opensuse.org/tumbleweed/repo/non-oss/repodata"
187 }
188 Self::TumbleweedUpdate => "https://download.opensuse.org/update/tumbleweed/repodata",
189 Self::Leap160Oss => {
190 "https://download.opensuse.org/distribution/leap/16.0/repo/oss/repodata"
191 }
192 Self::Leap160NonOss => {
193 "https://download.opensuse.org/distribution/leap/16.0/repo/non-oss/repodata"
194 }
195 Self::Leap156Oss => {
196 "https://download.opensuse.org/distribution/leap/15.6/repo/oss/repodata"
197 }
198 Self::Leap156NonOss => {
199 "https://download.opensuse.org/distribution/leap/15.6/repo/non-oss/repodata"
200 }
201 Self::Leap156UpdateOss => "https://download.opensuse.org/update/leap/15.6/oss/repodata",
202 Self::Leap156UpdateNonOss => {
203 "https://download.opensuse.org/update/leap/15.6/non-oss/repodata"
204 }
205 Self::Leap156Backports => {
206 "https://download.opensuse.org/update/leap/15.6/backports/repodata"
207 }
208 Self::Leap156Sle => "https://download.opensuse.org/update/leap/15.6/sle/repodata",
209 Self::TumbleweedSrcOss => {
211 "https://download.opensuse.org/tumbleweed/repo/src-oss/repodata"
212 }
213 Self::TumbleweedSrcNonOss => {
214 "https://download.opensuse.org/tumbleweed/repo/src-non-oss/repodata"
215 }
216 Self::Leap156SrcOss => {
217 "https://download.opensuse.org/source/distribution/leap/15.6/repo/oss/repodata"
218 }
219 Self::Leap156SrcNonOss => {
220 "https://download.opensuse.org/source/distribution/leap/15.6/repo/non-oss/repodata"
221 }
222 Self::TumbleweedDebug => {
224 "https://download.opensuse.org/debug/tumbleweed/repo/oss/repodata"
225 }
226 Self::Leap160Debug => {
227 "https://download.opensuse.org/debug/distribution/leap/16.0/repo/oss/repodata"
228 }
229 Self::Leap156Debug => {
230 "https://download.opensuse.org/debug/distribution/leap/15.6/repo/oss/repodata"
231 }
232 Self::Leap156DebugUpdate => {
233 "https://download.opensuse.org/debug/update/leap/15.6/oss/repodata"
234 }
235 Self::FactoryOss => "https://download.opensuse.org/factory/repo/oss/repodata",
236 Self::GamesTumbleweed => {
238 "https://download.opensuse.org/repositories/games/openSUSE_Tumbleweed/repodata"
239 }
240 Self::GamesLeap156 => "https://download.opensuse.org/repositories/games/15.6/repodata",
241 Self::KdeExtraTumbleweed => {
242 "https://download.opensuse.org/repositories/KDE:/Extra/openSUSE_Tumbleweed/repodata"
243 }
244 Self::KdeFrameworksTumbleweed => {
245 "https://download.opensuse.org/repositories/KDE:/Frameworks/openSUSE_Tumbleweed/repodata"
246 }
247 Self::KdeQt6Tumbleweed => {
248 "https://download.opensuse.org/repositories/KDE:/Qt6/openSUSE_Tumbleweed/repodata"
249 }
250 Self::GnomeAppsLeap160 => {
251 "https://download.opensuse.org/repositories/GNOME:/Apps/16.0/repodata"
252 }
253 Self::GnomeAppsLeap156 => {
254 "https://download.opensuse.org/repositories/GNOME:/Apps/15.6/repodata"
255 }
256 Self::GnomeFactory => {
257 "https://download.opensuse.org/repositories/GNOME:/Factory/openSUSE_Factory/repodata"
258 }
259 Self::XfceTumbleweed => {
260 "https://download.opensuse.org/repositories/X11:/xfce/openSUSE_Tumbleweed/repodata"
261 }
262 Self::XfceLeap160 => {
263 "https://download.opensuse.org/repositories/X11:/xfce/16.0/repodata"
264 }
265 Self::XfceLeap156 => {
266 "https://download.opensuse.org/repositories/X11:/xfce/15.6/repodata"
267 }
268 Self::MozillaTumbleweed => {
269 "https://download.opensuse.org/repositories/mozilla/openSUSE_Tumbleweed/repodata"
270 }
271 Self::ScienceTumbleweed => {
272 "https://download.opensuse.org/repositories/science/openSUSE_Tumbleweed/repodata"
273 }
274 Self::WineTumbleweed => {
275 "https://download.opensuse.org/repositories/Emulators:/Wine/openSUSE_Tumbleweed/repodata"
276 }
277 Self::ServerHttpTumbleweed => {
278 "https://download.opensuse.org/repositories/server:/http/openSUSE_Tumbleweed/repodata"
279 }
280 Self::ServerDatabaseTumbleweed => {
281 "https://download.opensuse.org/repositories/server:/database/openSUSE_Tumbleweed/repodata"
282 }
283 }
284 }
285
286 pub fn all() -> &'static [OpenSuseRepo] {
288 &[
289 Self::TumbleweedOss,
291 Self::TumbleweedNonOss,
292 Self::TumbleweedUpdate,
293 Self::Leap160Oss,
294 Self::Leap160NonOss,
295 Self::Leap156Oss,
296 Self::Leap156NonOss,
297 Self::Leap156UpdateOss,
298 Self::Leap156UpdateNonOss,
299 Self::Leap156Backports,
300 Self::Leap156Sle,
301 Self::TumbleweedSrcOss,
303 Self::TumbleweedSrcNonOss,
304 Self::Leap156SrcOss,
305 Self::Leap156SrcNonOss,
306 Self::TumbleweedDebug,
308 Self::Leap160Debug,
309 Self::Leap156Debug,
310 Self::Leap156DebugUpdate,
311 Self::FactoryOss,
313 Self::GamesTumbleweed,
315 Self::GamesLeap156,
316 Self::KdeExtraTumbleweed,
317 Self::KdeFrameworksTumbleweed,
318 Self::KdeQt6Tumbleweed,
319 Self::GnomeAppsLeap160,
320 Self::GnomeAppsLeap156,
321 Self::GnomeFactory,
322 Self::XfceTumbleweed,
323 Self::XfceLeap160,
324 Self::XfceLeap156,
325 Self::MozillaTumbleweed,
326 Self::ScienceTumbleweed,
327 Self::WineTumbleweed,
328 Self::ServerHttpTumbleweed,
329 Self::ServerDatabaseTumbleweed,
330 ]
331 }
332
333 pub fn official() -> &'static [OpenSuseRepo] {
335 &[
336 Self::TumbleweedOss,
338 Self::TumbleweedNonOss,
339 Self::TumbleweedUpdate,
340 Self::Leap160Oss,
341 Self::Leap160NonOss,
342 Self::Leap156Oss,
343 Self::Leap156NonOss,
344 Self::Leap156UpdateOss,
345 Self::Leap156UpdateNonOss,
346 Self::Leap156Backports,
347 Self::Leap156Sle,
348 Self::TumbleweedSrcOss,
350 Self::TumbleweedSrcNonOss,
351 Self::Leap156SrcOss,
352 Self::Leap156SrcNonOss,
353 Self::TumbleweedDebug,
355 Self::Leap160Debug,
356 Self::Leap156Debug,
357 Self::Leap156DebugUpdate,
358 Self::FactoryOss,
360 ]
361 }
362
363 pub fn binary_only() -> &'static [OpenSuseRepo] {
365 &[
366 Self::TumbleweedOss,
367 Self::TumbleweedNonOss,
368 Self::TumbleweedUpdate,
369 Self::Leap160Oss,
370 Self::Leap160NonOss,
371 Self::Leap156Oss,
372 Self::Leap156NonOss,
373 Self::Leap156UpdateOss,
374 Self::Leap156UpdateNonOss,
375 Self::Leap156Backports,
376 Self::Leap156Sle,
377 Self::FactoryOss,
378 ]
379 }
380
381 pub fn tumbleweed() -> &'static [OpenSuseRepo] {
383 &[
384 Self::TumbleweedOss,
385 Self::TumbleweedNonOss,
386 Self::TumbleweedUpdate,
387 ]
388 }
389
390 pub fn leap_15_6() -> &'static [OpenSuseRepo] {
392 &[
393 Self::Leap156Oss,
394 Self::Leap156NonOss,
395 Self::Leap156UpdateOss,
396 Self::Leap156UpdateNonOss,
397 Self::Leap156Backports,
398 Self::Leap156Sle,
399 ]
400 }
401
402 pub fn leap_16_0() -> &'static [OpenSuseRepo] {
404 &[Self::Leap160Oss, Self::Leap160NonOss]
405 }
406}
407
408pub struct OpenSuse {
410 repos: Vec<OpenSuseRepo>,
411}
412
413impl Default for OpenSuse {
414 fn default() -> Self {
415 Self::all()
416 }
417}
418
419impl OpenSuse {
420 pub fn all() -> Self {
422 Self {
423 repos: OpenSuseRepo::all().to_vec(),
424 }
425 }
426
427 pub fn with_repos(repos: &[OpenSuseRepo]) -> Self {
429 Self {
430 repos: repos.to_vec(),
431 }
432 }
433
434 pub fn tumbleweed() -> Self {
436 Self {
437 repos: OpenSuseRepo::tumbleweed().to_vec(),
438 }
439 }
440
441 pub fn leap_15_6() -> Self {
443 Self {
444 repos: OpenSuseRepo::leap_15_6().to_vec(),
445 }
446 }
447
448 pub fn leap_16_0() -> Self {
450 Self {
451 repos: OpenSuseRepo::leap_16_0().to_vec(),
452 }
453 }
454
455 fn find_primary_url(repo: OpenSuseRepo) -> Result<String, IndexError> {
457 let repomd_url = format!("{}/repomd.xml", repo.base_url());
458 let cache_key = format!("repomd-{}", repo.id());
459 let (data, _) = cache::fetch_with_cache("opensuse", &cache_key, &repomd_url, CACHE_TTL)
460 .map_err(IndexError::Network)?;
461
462 let xml = String::from_utf8_lossy(&data);
463
464 for line in xml.lines() {
466 if line.contains("primary.xml.zst") || line.contains("primary.xml.gz") {
467 if let Some(start) = line.find("href=\"") {
468 let rest = &line[start + 6..];
469 if let Some(end) = rest.find('"') {
470 let href = &rest[..end];
471 let base = repo.base_url().trim_end_matches("/repodata");
472 return Ok(format!("{}/{}", base, href));
473 }
474 }
475 }
476 }
477
478 Err(IndexError::Parse(format!(
479 "primary.xml not found in repomd.xml for {}",
480 repo.id()
481 )))
482 }
483
484 fn parse_primary(xml: &str, repo_id: &str) -> Vec<PackageMeta> {
486 let mut packages = Vec::new();
487 let mut in_package = false;
488 let mut name = String::new();
489 let mut version = String::new();
490 let mut release = String::new();
491 let mut summary = String::new();
492 let mut url = String::new();
493 let mut license = String::new();
494
495 for line in xml.lines() {
496 let line = line.trim();
497
498 if line.starts_with("<package type=\"rpm\">") {
499 in_package = true;
500 name.clear();
501 version.clear();
502 release.clear();
503 summary.clear();
504 url.clear();
505 license.clear();
506 } else if line == "</package>" && in_package {
507 if !name.is_empty() {
508 let mut extra = HashMap::new();
509 extra.insert(
510 "source_repo".to_string(),
511 serde_json::Value::String(repo_id.to_string()),
512 );
513
514 let full_version = if release.is_empty() {
516 version.clone()
517 } else {
518 format!("{}-{}", version, release)
519 };
520
521 packages.push(PackageMeta {
522 name: name.clone(),
523 version: full_version,
524 description: if summary.is_empty() {
525 None
526 } else {
527 Some(summary.clone())
528 },
529 homepage: if url.is_empty() {
530 None
531 } else {
532 Some(url.clone())
533 },
534 repository: Some(
535 "https://build.opensuse.org/project/show/openSUSE:Factory".to_string(),
536 ),
537 license: if license.is_empty() {
538 None
539 } else {
540 Some(license.clone())
541 },
542 binaries: Vec::new(),
543 keywords: Vec::new(),
544 maintainers: Vec::new(),
545 published: None,
546 downloads: None,
547 archive_url: None,
548 checksum: None,
549 extra,
550 });
551 }
552 in_package = false;
553 } else if in_package {
554 if line.starts_with("<name>") && line.ends_with("</name>") {
555 name = line[6..line.len() - 7].to_string();
556 } else if line.starts_with("<summary>") && line.ends_with("</summary>") {
557 summary = line[9..line.len() - 10].to_string();
558 } else if line.starts_with("<url>") && line.ends_with("</url>") {
559 url = line[5..line.len() - 6].to_string();
560 } else if line.starts_with("<rpm:license>") && line.ends_with("</rpm:license>") {
561 license = line[13..line.len() - 14].to_string();
562 } else if line.starts_with("<version ") {
563 if let Some(ver_start) = line.find("ver=\"") {
564 let rest = &line[ver_start + 5..];
565 if let Some(ver_end) = rest.find('"') {
566 version = rest[..ver_end].to_string();
567 }
568 }
569 if let Some(rel_start) = line.find("rel=\"") {
570 let rest = &line[rel_start + 5..];
571 if let Some(rel_end) = rest.find('"') {
572 release = rest[..rel_end].to_string();
573 }
574 }
575 }
576 }
577 }
578
579 packages
580 }
581
582 fn load_repo(repo: OpenSuseRepo) -> Result<Vec<PackageMeta>, IndexError> {
584 let primary_url = Self::find_primary_url(repo)?;
585 let cache_key = format!("primary-{}", repo.id());
586
587 let (data, _was_cached) =
588 cache::fetch_with_cache("opensuse", &cache_key, &primary_url, CACHE_TTL)
589 .map_err(IndexError::Network)?;
590
591 let decompressed = if primary_url.ends_with(".zst") {
593 zstd::decode_all(std::io::Cursor::new(&data))
594 .map_err(|e| IndexError::Decompress(e.to_string()))?
595 } else {
596 use flate2::read::GzDecoder;
597 use std::io::Read;
598 let mut decoder = GzDecoder::new(&data[..]);
599 let mut decompressed = Vec::new();
600 decoder
601 .read_to_end(&mut decompressed)
602 .map_err(|e| IndexError::Decompress(e.to_string()))?;
603 decompressed
604 };
605
606 let xml = String::from_utf8_lossy(&decompressed);
607 Ok(Self::parse_primary(&xml, repo.id()))
608 }
609
610 fn load_packages(&self) -> Result<Vec<PackageMeta>, IndexError> {
612 let results: Vec<_> = self
613 .repos
614 .par_iter()
615 .map(|&repo| Self::load_repo(repo))
616 .collect();
617
618 let mut all_packages = Vec::new();
619 for (repo, result) in self.repos.iter().zip(results) {
620 match result {
621 Ok(packages) => {
622 all_packages.extend(packages);
623 }
624 Err(e) => {
625 eprintln!("Warning: failed to load openSUSE repo {}: {}", repo.id(), e);
626 }
627 }
628 }
629
630 if all_packages.is_empty() {
631 return Err(IndexError::Network(
632 "failed to load any openSUSE repos".into(),
633 ));
634 }
635
636 Ok(all_packages)
637 }
638}
639
640impl PackageIndex for OpenSuse {
641 fn ecosystem(&self) -> &'static str {
642 "opensuse"
643 }
644
645 fn display_name(&self) -> &'static str {
646 "openSUSE (zypper)"
647 }
648
649 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
650 let packages = self.load_packages()?;
651
652 packages
653 .into_iter()
654 .find(|p| p.name.eq_ignore_ascii_case(name))
655 .ok_or_else(|| IndexError::NotFound(name.to_string()))
656 }
657
658 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
659 let packages = self.load_packages()?;
660 let name_lower = name.to_lowercase();
661
662 let versions: Vec<VersionMeta> = packages
663 .into_iter()
664 .filter(|p| p.name.to_lowercase() == name_lower)
665 .map(|p| VersionMeta {
666 version: p.version,
667 released: None,
668 yanked: false,
669 })
670 .collect();
671
672 if versions.is_empty() {
673 return Err(IndexError::NotFound(name.to_string()));
674 }
675
676 Ok(versions)
677 }
678
679 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
680 let packages = self.load_packages()?;
681 let query_lower = query.to_lowercase();
682
683 Ok(packages
684 .into_iter()
685 .filter(|p| {
686 p.name.to_lowercase().contains(&query_lower)
687 || p.description
688 .as_ref()
689 .map(|d| d.to_lowercase().contains(&query_lower))
690 .unwrap_or(false)
691 })
692 .collect())
693 }
694
695 fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
696 self.load_packages()
697 }
698}