pacsea/logic/files/pkgbuild_parse.rs
1//! PKGBUILD parsing functions.
2
3/// What: Parse backup array from PKGBUILD content.
4///
5/// Inputs:
6/// - `pkgbuild`: Raw PKGBUILD file content.
7///
8/// Output:
9/// - Returns a vector of backup file paths.
10///
11/// Details:
12/// - Parses bash array syntax: `backup=('file1' 'file2' '/etc/config')`
13/// - Handles single-line and multi-line array definitions.
14#[must_use]
15pub fn parse_backup_from_pkgbuild(pkgbuild: &str) -> Vec<String> {
16 let mut backup_files = Vec::new();
17 let mut in_backup_array = false;
18
19 for line in pkgbuild.lines() {
20 let line = line.trim();
21
22 // Skip comments and empty lines
23 if line.is_empty() || line.starts_with('#') {
24 continue;
25 }
26
27 // Look for backup= array declaration
28 if line.starts_with("backup=") || line.starts_with("backup =") {
29 in_backup_array = true;
30
31 // Check if array is on single line: backup=('file1' 'file2')
32 if let Some(start) = line.find('(')
33 && let Some(end) = line.rfind(')')
34 {
35 let array_content = &line[start + 1..end];
36 parse_backup_array_content(array_content, &mut backup_files);
37 in_backup_array = false;
38 } else if line.contains('(') {
39 // Multi-line array starting
40 if let Some(start) = line.find('(') {
41 let array_content = &line[start + 1..];
42 parse_backup_array_content(array_content, &mut backup_files);
43 }
44 }
45 } else if in_backup_array {
46 // Continuation of multi-line array
47 // Check if array ends
48 if line.contains(')') {
49 if let Some(end) = line.rfind(')') {
50 let remaining = &line[..end];
51 parse_backup_array_content(remaining, &mut backup_files);
52 }
53 in_backup_array = false;
54 } else {
55 // Still in array, parse this line
56 parse_backup_array_content(line, &mut backup_files);
57 }
58 }
59 }
60
61 backup_files
62}
63
64/// What: Parse backup array content (handles quoted strings).
65///
66/// Inputs:
67/// - `content`: String content containing quoted file paths.
68/// - `backup_files`: Vector to append parsed file paths to.
69///
70/// Details:
71/// - Extracts quoted strings (single or double quotes) from array content.
72pub fn parse_backup_array_content(content: &str, backup_files: &mut Vec<String>) {
73 let mut in_quotes = false;
74 let mut quote_char = '\0';
75 let mut current_file = String::new();
76
77 for ch in content.chars() {
78 match ch {
79 '\'' | '"' => {
80 if !in_quotes {
81 in_quotes = true;
82 quote_char = ch;
83 } else if ch == quote_char {
84 // End of quoted string
85 if !current_file.is_empty() {
86 backup_files.push(current_file.clone());
87 current_file.clear();
88 }
89 in_quotes = false;
90 quote_char = '\0';
91 } else {
92 // Different quote type, treat as part of string
93 current_file.push(ch);
94 }
95 }
96 _ if in_quotes => {
97 current_file.push(ch);
98 }
99 _ => {
100 // Skip whitespace and other characters outside quotes
101 }
102 }
103 }
104
105 // Handle unclosed quote (edge case)
106 if !current_file.is_empty() && in_quotes {
107 backup_files.push(current_file);
108 }
109}
110
111/// What: Parse backup array from .SRCINFO content.
112///
113/// Inputs:
114/// - `srcinfo`: Raw .SRCINFO file content.
115///
116/// Output:
117/// - Returns a vector of backup file paths.
118///
119/// Details:
120/// - Parses key-value pairs: `backup = file1`
121/// - Handles multiple backup entries.
122#[must_use]
123pub fn parse_backup_from_srcinfo(srcinfo: &str) -> Vec<String> {
124 let mut backup_files = Vec::new();
125
126 for line in srcinfo.lines() {
127 let line = line.trim();
128 if line.is_empty() || line.starts_with('#') {
129 continue;
130 }
131
132 // .SRCINFO format: backup = file_path
133 if let Some((key, value)) = line.split_once('=') {
134 let key = key.trim();
135 let value = value.trim();
136
137 if key == "backup" && !value.is_empty() {
138 backup_files.push(value.to_string());
139 }
140 }
141 }
142
143 backup_files
144}
145
146/// What: Parse install paths from PKGBUILD content.
147///
148/// Inputs:
149/// - `pkgbuild`: Raw PKGBUILD file content.
150/// - `pkgname`: Package name (used for default install paths).
151///
152/// Output:
153/// - Returns a vector of file paths that would be installed.
154///
155/// Details:
156/// - Parses `package()` functions and `install` scripts to extract file paths.
157/// - Handles common patterns like `install -Dm755`, `cp`, `mkdir -p`, etc.
158/// - Extracts paths from `package()` functions that use `install` commands.
159/// - This is a best-effort heuristic and may not capture all files.
160#[must_use]
161pub fn parse_install_paths_from_pkgbuild(pkgbuild: &str, pkgname: &str) -> Vec<String> {
162 let mut files = Vec::new();
163 let mut in_package_function = false;
164 let mut package_function_depth = 0;
165
166 for line in pkgbuild.lines() {
167 let trimmed = line.trim();
168
169 // Skip comments and empty lines
170 if trimmed.is_empty() || trimmed.starts_with('#') {
171 continue;
172 }
173
174 // Detect package() function start
175 if trimmed.starts_with("package()") || trimmed.starts_with("package_") {
176 in_package_function = true;
177 package_function_depth = 0;
178 continue;
179 }
180
181 // Track function depth (handle nested functions)
182 if in_package_function {
183 if trimmed.contains('{') {
184 package_function_depth += trimmed.matches('{').count();
185 }
186 if trimmed.contains('}') {
187 let closing_count = trimmed.matches('}').count();
188 if package_function_depth >= closing_count {
189 package_function_depth -= closing_count;
190 } else {
191 package_function_depth = 0;
192 }
193 if package_function_depth == 0 {
194 in_package_function = false;
195 continue;
196 }
197 }
198
199 // Parse install commands within package() function
200 // Common patterns:
201 // install -Dm755 "$srcdir/binary" "$pkgdir/usr/bin/binary"
202 // install -Dm644 "$srcdir/config" "$pkgdir/etc/config"
203 // cp -r "$srcdir/data" "$pkgdir/usr/share/app"
204
205 if trimmed.contains("install") && trimmed.contains("$pkgdir") {
206 // Extract destination path from install command
207 // Pattern: install ... "$pkgdir/path/to/file"
208 if let Some(pkgdir_pos) = trimmed.find("$pkgdir") {
209 let after_pkgdir = &trimmed[pkgdir_pos + 7..]; // Skip "$pkgdir"
210 // Find the path (may be quoted)
211 let path_start = after_pkgdir
212 .chars()
213 .position(|c| c != ' ' && c != '/' && c != '"' && c != '\'')
214 .unwrap_or(0);
215 let path_part = &after_pkgdir[path_start..];
216
217 // Extract path until space, quote, or end
218 let path_end = path_part
219 .chars()
220 .position(|c| c == ' ' || c == '"' || c == '\'' || c == ';')
221 .unwrap_or(path_part.len());
222
223 let mut path = path_part[..path_end].to_string();
224 // Remove leading slash if present (we'll add it)
225 if path.starts_with('/') {
226 path.remove(0);
227 }
228 if !path.is_empty() {
229 {
230 let path_str = &path;
231 files.push(format!("/{path_str}"));
232 }
233 }
234 }
235 } else if trimmed.contains("cp") && trimmed.contains("$pkgdir") {
236 // Extract destination from cp command
237 // Pattern: cp ... "$pkgdir/path/to/file"
238 if let Some(pkgdir_pos) = trimmed.find("$pkgdir") {
239 let after_pkgdir = &trimmed[pkgdir_pos + 7..];
240 let path_start = after_pkgdir
241 .chars()
242 .position(|c| c != ' ' && c != '/' && c != '"' && c != '\'')
243 .unwrap_or(0);
244 let path_part = &after_pkgdir[path_start..];
245 let path_end = path_part
246 .chars()
247 .position(|c| c == ' ' || c == '"' || c == '\'' || c == ';')
248 .unwrap_or(path_part.len());
249
250 let mut path = path_part[..path_end].to_string();
251 if path.starts_with('/') {
252 path.remove(0);
253 }
254 if !path.is_empty() {
255 {
256 let path_str = &path;
257 files.push(format!("/{path_str}"));
258 }
259 }
260 }
261 }
262 }
263 }
264
265 // Remove duplicates and sort
266 files.sort();
267 files.dedup();
268
269 // If we didn't find any files, try to infer common paths based on package name
270 if files.is_empty() {
271 // Common default paths for AUR packages
272 files.push(format!("/usr/bin/{pkgname}"));
273 files.push(format!("/usr/share/{pkgname}"));
274 }
275
276 files
277}