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}