Skip to main content

normalize_package_index/index/
slackware.rs

1//! Slackware package index fetcher (SlackBuilds).
2//!
3//! Fetches package metadata from slackbuilds.org.
4//!
5//! ## API Strategy
6//! - **fetch**: `github.com/SlackBuildsOrg/slackbuilds/.../info` - GitHub raw files
7//! - **fetch_versions**: Same, single version per package
8//! - **search**: Not supported (no search API)
9//! - **fetch_all**: Not supported (would need to enumerate all directories)
10//!
11//! ## Multi-version Support
12//! ```rust,ignore
13//! use normalize_packages::index::slackware::{Slackware, SlackwareVersion};
14//!
15//! // All versions (default)
16//! let all = Slackware::all();
17//!
18//! // Current (development) only
19//! let current = Slackware::current();
20//!
21//! // Stable versions only
22//! let stable = Slackware::stable();
23//! ```
24
25use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
26use std::collections::HashMap;
27
28/// Available Slackware versions/branches.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum SlackwareVersion {
31    /// Current development branch (master)
32    Current,
33    /// Slackware 15.0 (latest stable)
34    Slack150,
35    /// Slackware 14.2 (older stable)
36    Slack142,
37}
38
39impl SlackwareVersion {
40    /// Get the git branch name for this version.
41    fn branch(&self) -> &'static str {
42        match self {
43            Self::Current => "master",
44            Self::Slack150 => "15.0",
45            Self::Slack142 => "14.2",
46        }
47    }
48
49    /// Get the version name for tagging.
50    pub fn name(&self) -> &'static str {
51        match self {
52            Self::Current => "current",
53            Self::Slack150 => "15.0",
54            Self::Slack142 => "14.2",
55        }
56    }
57
58    /// All available versions.
59    pub fn all() -> &'static [SlackwareVersion] {
60        &[Self::Current, Self::Slack150, Self::Slack142]
61    }
62
63    /// Current (development) only.
64    pub fn current() -> &'static [SlackwareVersion] {
65        &[Self::Current]
66    }
67
68    /// Stable versions only (15.0, 14.2).
69    pub fn stable() -> &'static [SlackwareVersion] {
70        &[Self::Slack150, Self::Slack142]
71    }
72
73    /// Latest stable only (15.0).
74    pub fn latest_stable() -> &'static [SlackwareVersion] {
75        &[Self::Slack150]
76    }
77}
78
79/// SlackBuilds package categories.
80const CATEGORIES: &[&str] = &[
81    "system",
82    "development",
83    "network",
84    "multimedia",
85    "desktop",
86    "misc",
87    "libraries",
88    "games",
89    "graphics",
90    "office",
91    "audio",
92    "academic",
93    "accessibility",
94    "business",
95    "gis",
96    "ham",
97    "haskell",
98    "perl",
99    "python",
100    "ruby",
101];
102
103/// Slackware package index fetcher (SlackBuilds.org) with configurable versions.
104pub struct Slackware {
105    versions: Vec<SlackwareVersion>,
106}
107
108impl Slackware {
109    /// SlackBuilds.org website.
110    const SBO_API: &'static str = "https://slackbuilds.org";
111
112    /// Create a fetcher with all versions.
113    pub fn all() -> Self {
114        Self {
115            versions: SlackwareVersion::all().to_vec(),
116        }
117    }
118
119    /// Create a fetcher with current (development) only.
120    pub fn current() -> Self {
121        Self {
122            versions: SlackwareVersion::current().to_vec(),
123        }
124    }
125
126    /// Create a fetcher with stable versions only.
127    pub fn stable() -> Self {
128        Self {
129            versions: SlackwareVersion::stable().to_vec(),
130        }
131    }
132
133    /// Create a fetcher with latest stable only.
134    pub fn latest_stable() -> Self {
135        Self {
136            versions: SlackwareVersion::latest_stable().to_vec(),
137        }
138    }
139
140    /// Create a fetcher with custom version selection.
141    pub fn with_versions(versions: &[SlackwareVersion]) -> Self {
142        Self {
143            versions: versions.to_vec(),
144        }
145    }
146
147    /// Fetch a package from a specific version.
148    fn fetch_from_version(
149        name: &str,
150        version: SlackwareVersion,
151    ) -> Result<PackageMeta, IndexError> {
152        // Try each category to find the package
153        for category in CATEGORIES {
154            let info_url = format!(
155                "https://raw.githubusercontent.com/SlackBuildsOrg/slackbuilds/{}/{}/{}/{}.info",
156                version.branch(),
157                category,
158                name,
159                name
160            );
161
162            if let Ok(response) = ureq::get(&info_url).call() {
163                if let Ok(body) = response.into_string() {
164                    return parse_sbo_info(&body, name, category, version);
165                }
166            }
167        }
168
169        Err(IndexError::NotFound(format!(
170            "{} in {}",
171            name,
172            version.name()
173        )))
174    }
175}
176
177impl PackageIndex for Slackware {
178    fn ecosystem(&self) -> &'static str {
179        "slackware"
180    }
181
182    fn display_name(&self) -> &'static str {
183        "SlackBuilds.org"
184    }
185
186    fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
187        // Try each configured version until we find the package
188        for &version in &self.versions {
189            match Self::fetch_from_version(name, version) {
190                Ok(pkg) => return Ok(pkg),
191                Err(IndexError::NotFound(_)) => continue,
192                Err(e) => return Err(e),
193            }
194        }
195
196        Err(IndexError::NotFound(name.to_string()))
197    }
198
199    fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
200        let mut all_versions = Vec::new();
201
202        // Check each configured version for this package
203        for &version in &self.versions {
204            if let Ok(pkg) = Self::fetch_from_version(name, version) {
205                all_versions.push(VersionMeta {
206                    version: format!("{} ({})", pkg.version, version.name()),
207                    released: None,
208                    yanked: false,
209                });
210            }
211        }
212
213        if all_versions.is_empty() {
214            return Err(IndexError::NotFound(name.to_string()));
215        }
216
217        Ok(all_versions)
218    }
219
220    fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
221        // SlackBuilds doesn't have a JSON search API
222        // Return an error suggesting to use fetch() directly
223        Err(IndexError::Network(format!(
224            "SlackBuilds search not implemented via API. Use fetch() with exact package name, or visit: {}/result/?search={}",
225            Self::SBO_API,
226            query
227        )))
228    }
229}
230
231fn parse_sbo_info(
232    content: &str,
233    name: &str,
234    category: &str,
235    version: SlackwareVersion,
236) -> Result<PackageMeta, IndexError> {
237    let mut pkg_version = String::new();
238    let mut homepage = None;
239    let mut maintainer = None;
240    let mut email = None;
241
242    for line in content.lines() {
243        if let Some(val) = line.strip_prefix("VERSION=") {
244            pkg_version = val.trim_matches('"').to_string();
245        } else if let Some(val) = line.strip_prefix("HOMEPAGE=") {
246            homepage = Some(val.trim_matches('"').to_string());
247        } else if let Some(val) = line.strip_prefix("MAINTAINER=") {
248            maintainer = Some(val.trim_matches('"').to_string());
249        } else if let Some(val) = line.strip_prefix("EMAIL=") {
250            email = Some(val.trim_matches('"').to_string());
251        }
252    }
253
254    let maintainers = match (maintainer, email) {
255        (Some(m), Some(e)) => vec![format!("{} <{}>", m, e)],
256        (Some(m), None) => vec![m],
257        _ => Vec::new(),
258    };
259
260    let mut extra = HashMap::new();
261    extra.insert(
262        "source_repo".to_string(),
263        serde_json::Value::String(version.name().to_string()),
264    );
265    extra.insert(
266        "category".to_string(),
267        serde_json::Value::String(category.to_string()),
268    );
269
270    Ok(PackageMeta {
271        name: format!("{}/{}", category, name),
272        version: pkg_version,
273        description: None, // Would need to parse README
274        homepage,
275        repository: Some(format!(
276            "https://github.com/SlackBuildsOrg/slackbuilds/tree/{}/{}/{}",
277            version.branch(),
278            category,
279            name
280        )),
281        license: None,
282        binaries: Vec::new(),
283        keywords: Vec::new(),
284        maintainers,
285        published: None,
286        downloads: None,
287        archive_url: None,
288        checksum: None,
289        extra,
290    })
291}