normalize_manifest/
rockspec.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
9
10pub struct RockspecParser;
17
18impl ManifestParser for RockspecParser {
19 fn filename(&self) -> &'static str {
20 "*.rockspec"
21 }
22
23 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
24 let mut name = None;
25 let mut version = None;
26 let mut deps = Vec::new();
27 let mut in_deps = false;
28
29 for line in content.lines() {
30 let trimmed = line.trim();
31 if trimmed.is_empty() || trimmed.starts_with("--") {
32 continue;
33 }
34
35 if trimmed.starts_with("package")
37 && trimmed.contains('=')
38 && name.is_none()
39 && let Some(v) = extract_lua_string(trimmed)
40 {
41 name = Some(v);
42 continue;
43 }
44
45 if trimmed.starts_with("version")
47 && trimmed.contains('=')
48 && version.is_none()
49 && let Some(v) = extract_lua_string(trimmed)
50 {
51 version = Some(v);
52 continue;
53 }
54
55 if trimmed.starts_with("dependencies") && trimmed.contains('{') {
57 in_deps = true;
58 extract_dep_strings(trimmed, &mut deps);
59 if trimmed.contains('}') {
60 in_deps = false;
61 }
62 continue;
63 }
64
65 if in_deps {
66 extract_dep_strings(trimmed, &mut deps);
67 if trimmed.contains('}') {
68 in_deps = false;
69 }
70 }
71 }
72
73 Ok(ParsedManifest {
74 ecosystem: "luarocks",
75 name,
76 version,
77 dependencies: deps,
78 })
79 }
80}
81
82fn extract_lua_string(line: &str) -> Option<String> {
83 let start = line.find('"')? + 1;
84 let end = line[start..].find('"')?;
85 Some(line[start..start + end].to_string())
86}
87
88fn extract_dep_strings(line: &str, out: &mut Vec<DeclaredDep>) {
89 let mut s = line;
90 while let Some(q_start) = s.find('"') {
91 s = &s[q_start + 1..];
92 if let Some(q_end) = s.find('"') {
93 let spec = s[..q_end].trim();
94 if let Some(dep) = parse_rockspec_spec(spec) {
95 out.push(dep);
96 }
97 s = &s[q_end + 1..];
98 } else {
99 break;
100 }
101 }
102}
103
104fn parse_rockspec_spec(spec: &str) -> Option<DeclaredDep> {
105 let spec = spec.trim();
106 if spec.is_empty() {
107 return None;
108 }
109
110 const OPS: &[&str] = &[">=", "<=", "!=", ">", "<", "==", "~>"];
111 for op in OPS {
112 if let Some(idx) = spec.find(op) {
113 let name = spec[..idx].trim().to_string();
114 if name.is_empty() || name == "lua" {
115 return None; }
117 let version_req = spec[idx..].trim().to_string();
118 return Some(DeclaredDep {
119 name,
120 version_req: Some(version_req),
121 kind: DepKind::Normal,
122 });
123 }
124 }
125
126 if spec == "lua" {
127 return None;
128 }
129 Some(DeclaredDep {
130 name: spec.to_string(),
131 version_req: None,
132 kind: DepKind::Normal,
133 })
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::ManifestParser;
140
141 #[test]
142 fn test_parse_rockspec() {
143 let content = r#"package = "mypkg"
144version = "1.0-1"
145source = { url = "https://example.com/mypkg-1.0.tar.gz" }
146description = { summary = "My package" }
147
148dependencies = {
149 "lua >= 5.1",
150 "luasocket >= 3.0",
151 "dkjson ~> 2.5",
152 "argparse"
153}
154
155build = { type = "builtin" }
156"#;
157 let m = RockspecParser.parse(content).unwrap();
158 assert_eq!(m.ecosystem, "luarocks");
159 assert_eq!(m.name.as_deref(), Some("mypkg"));
160 assert_eq!(m.version.as_deref(), Some("1.0-1"));
161
162 assert!(!m.dependencies.iter().any(|d| d.name == "lua"));
164
165 let socket = m
166 .dependencies
167 .iter()
168 .find(|d| d.name == "luasocket")
169 .unwrap();
170 assert_eq!(socket.version_req.as_deref(), Some(">= 3.0"));
171
172 let argparse = m
173 .dependencies
174 .iter()
175 .find(|d| d.name == "argparse")
176 .unwrap();
177 assert!(argparse.version_req.is_none());
178 }
179}