normalize_package_index/index/
luarocks.rs1use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
13
14pub struct LuaRocks;
16
17impl LuaRocks {
18 const BASE_URL: &'static str = "https://luarocks.org";
20
21 fn parse_manifest(content: &str, name: &str) -> Option<Vec<String>> {
23 let quoted_search = format!("[\"{}\"] = {{", name);
32 let simple_search = format!("{} = {{", name);
33
34 let start = content
35 .find("ed_search)
36 .or_else(|| content.find(&simple_search))?;
37
38 let rest = &content[start..];
39
40 let brace_pos = rest.find('{')?;
42 let after_brace = &rest[brace_pos + 1..];
43
44 let mut versions = Vec::new();
46 let mut pos = 0;
47
48 while let Some(find_start) = after_brace[pos..].find("[\"") {
49 let version_start = pos + find_start + 2;
50 if let Some(end) = after_brace[version_start..].find("\"]") {
51 let version = &after_brace[version_start..version_start + end];
52 let prefix = &after_brace[..version_start];
56 let open_braces = prefix.matches('{').count();
57 let close_braces = prefix.matches('}').count();
58 if open_braces == close_braces {
59 versions.push(version.to_string());
60 }
61 pos = version_start + end + 2;
62 } else {
63 break;
64 }
65
66 let so_far = &after_brace[..pos];
68 let opens = so_far.matches('{').count();
69 let closes = so_far.matches('}').count();
70 if closes > opens {
71 break;
72 }
73 }
74
75 if versions.is_empty() {
76 None
77 } else {
78 Some(versions)
79 }
80 }
81}
82
83impl PackageIndex for LuaRocks {
84 fn ecosystem(&self) -> &'static str {
85 "luarocks"
86 }
87
88 fn display_name(&self) -> &'static str {
89 "LuaRocks (Lua)"
90 }
91
92 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
93 let manifest_url = format!("{}/manifest", Self::BASE_URL);
95 let manifest = ureq::get(&manifest_url)
96 .call()?
97 .into_string()
98 .map_err(|e| IndexError::Io(e))?;
99
100 let versions = Self::parse_manifest(&manifest, name)
101 .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
102
103 let latest = versions
105 .iter()
106 .filter(|v| !v.contains("scm") && !v.contains("dev"))
107 .max_by(|a, b| version_compare(a, b))
108 .or(versions.first())
109 .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
110
111 let version_clean = latest.rsplit_once('-').map(|(v, _)| v).unwrap_or(latest);
113
114 Ok(PackageMeta {
115 name: name.to_string(),
116 version: version_clean.to_string(),
117 description: None, homepage: Some(format!("{}/modules/{}/{}", Self::BASE_URL, name, name)),
119 repository: None,
120 license: None,
121 binaries: Vec::new(),
122 keywords: Vec::new(),
123 maintainers: Vec::new(),
124 published: None,
125 downloads: None,
126 archive_url: Some(format!("{}/{}-{}.src.rock", Self::BASE_URL, name, latest)),
127 checksum: None,
128 extra: Default::default(),
129 })
130 }
131
132 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
133 let manifest_url = format!("{}/manifest", Self::BASE_URL);
134 let manifest = ureq::get(&manifest_url)
135 .call()?
136 .into_string()
137 .map_err(|e| IndexError::Io(e))?;
138
139 let versions = Self::parse_manifest(&manifest, name)
140 .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
141
142 let mut result: Vec<VersionMeta> = versions
143 .iter()
144 .map(|v| {
145 let version_clean = v.rsplit_once('-').map(|(ver, _)| ver).unwrap_or(v);
146 VersionMeta {
147 version: version_clean.to_string(),
148 released: None,
149 yanked: false,
150 }
151 })
152 .collect();
153
154 result.sort_by(|a, b| version_compare(&b.version, &a.version));
156
157 result.dedup_by(|a, b| a.version == b.version);
159
160 Ok(result)
161 }
162
163 fn search(&self, _query: &str) -> Result<Vec<PackageMeta>, IndexError> {
164 Err(IndexError::Parse(
167 "LuaRocks search requires HTML scraping (not implemented)".into(),
168 ))
169 }
170}
171
172fn version_compare(a: &str, b: &str) -> std::cmp::Ordering {
174 let parse = |s: &str| -> Vec<u32> {
175 s.split(|c: char| !c.is_ascii_digit())
176 .filter_map(|p| p.parse().ok())
177 .collect()
178 };
179 parse(a).cmp(&parse(b))
180}