Skip to main content

debian_control/lossless/
buildinfo.rs

1//! Parser for Debian buildinfo files
2//!
3//! The buildinfo file format is a Debian-specific format that is used to store
4//! information about the build environment of a package. See <https://wiki.debian.org/Buildinfo> for
5//! more information.
6
7use crate::fields::{Md5Checksum, Sha1Checksum, Sha256Checksum};
8use crate::lossless::relations::Relations;
9use rowan::ast::AstNode;
10
11/// A buildinfo file
12pub struct Buildinfo(deb822_lossless::Paragraph);
13
14impl From<deb822_lossless::Paragraph> for Buildinfo {
15    fn from(paragraph: deb822_lossless::Paragraph) -> Self {
16        Self(paragraph)
17    }
18}
19
20impl Default for Buildinfo {
21    fn default() -> Self {
22        let mut para = deb822_lossless::Paragraph::new();
23        para.set("Format", "1.0");
24        Self(para)
25    }
26}
27
28impl Buildinfo {
29    /// Create a new source package
30    pub fn new() -> Self {
31        Self(deb822_lossless::Paragraph::new())
32    }
33
34    /// Parse buildinfo text, returning a Parse result
35    ///
36    /// Note: This expects a single paragraph, not a full deb822 document
37    pub fn parse(text: &str) -> deb822_lossless::Parse<Buildinfo> {
38        let deb822_parse = deb822_lossless::Deb822::parse(text);
39        let green = deb822_parse.green().clone();
40        let mut errors = deb822_parse.errors().to_vec();
41
42        // Check if there's exactly one paragraph
43        if errors.is_empty() {
44            let deb822 = deb822_parse.tree();
45            let paragraph_count = deb822.paragraphs().count();
46            if paragraph_count == 0 {
47                errors.push("No paragraphs found".to_string());
48            } else if paragraph_count > 1 {
49                errors.push("Multiple paragraphs found, expected one".to_string());
50            }
51        }
52
53        deb822_lossless::Parse::new(green, errors)
54    }
55
56    /// Get the source name
57    pub fn source(&self) -> Option<String> {
58        self.0.get("Source").map(|s| s.to_string())
59    }
60
61    /// Set the package name
62    pub fn set_source(&mut self, package: &str) {
63        self.0.set("Source", package);
64    }
65
66    /// Get the binary package names
67    pub fn binaries(&self) -> Option<Vec<String>> {
68        self.0.get("Binary").map(|s| {
69            s.split(' ')
70                .map(|s| s.trim().to_string())
71                .collect::<Vec<String>>()
72        })
73    }
74
75    /// Set the binary package names
76    pub fn set_binaries(&mut self, binaries: Vec<String>) {
77        self.0.set("Binary", &binaries.join(" "));
78    }
79
80    /// Get the version of the package
81    pub fn version(&self) -> Option<debversion::Version> {
82        self.0.get("Version").map(|s| s.parse().unwrap())
83    }
84
85    /// Set the version of the package
86    pub fn set_version(&mut self, version: debversion::Version) {
87        self.0.set("Version", &version.to_string());
88    }
89
90    /// Get the build architecture
91    pub fn build_architecture(&self) -> Option<String> {
92        self.0.get("Build-Architecture").map(|s| s.to_string())
93    }
94
95    /// Set the build architecture
96    pub fn set_build_architecture(&mut self, arch: &str) {
97        self.0.set("Build-Architecture", arch);
98    }
99
100    /// Get the architecture
101    pub fn architecture(&self) -> Option<String> {
102        self.0.get("Architecture").map(|s| s.to_string())
103    }
104
105    /// Set the architecture
106    pub fn set_architecture(&mut self, arch: &str) {
107        self.0.set("Architecture", arch);
108    }
109
110    /// Get Sha256 checksums
111    pub fn checksums_sha256(&self) -> Vec<Sha256Checksum> {
112        self.0
113            .get("Checksums-Sha256")
114            .map(|s| {
115                s.lines()
116                    .map(|line| line.parse().unwrap())
117                    .collect::<Vec<Sha256Checksum>>()
118            })
119            .unwrap_or_default()
120    }
121
122    /// Set Sha256 checksums
123    pub fn set_checksums_sha256(&mut self, checksums: Vec<Sha256Checksum>) {
124        self.0.set(
125            "Checksums-Sha256",
126            &checksums
127                .iter()
128                .map(|c| c.to_string())
129                .collect::<Vec<String>>()
130                .join("\n"),
131        );
132    }
133
134    /// Get SHA1 checksums
135    pub fn checksums_sha1(&self) -> Vec<Sha1Checksum> {
136        self.0
137            .get("Checksums-Sha1")
138            .map(|s| {
139                s.lines()
140                    .map(|line| line.parse().unwrap())
141                    .collect::<Vec<Sha1Checksum>>()
142            })
143            .unwrap_or_default()
144    }
145
146    /// Set SHA1 checksums
147    pub fn set_checksums_sha1(&mut self, checksums: Vec<Sha1Checksum>) {
148        self.0.set(
149            "Checksums-Sha1",
150            &checksums
151                .iter()
152                .map(|c| c.to_string())
153                .collect::<Vec<String>>()
154                .join("\n"),
155        );
156    }
157
158    /// Get MD5 checksums
159    pub fn checksums_md5(&self) -> Vec<Md5Checksum> {
160        self.0
161            .get("Checksums-Md5")
162            .map(|s| {
163                s.lines()
164                    .map(|line| line.parse().unwrap())
165                    .collect::<Vec<Md5Checksum>>()
166            })
167            .unwrap_or_default()
168    }
169
170    /// Set MD5 checksums
171    pub fn set_checksums_md5(&mut self, checksums: Vec<Md5Checksum>) {
172        self.0.set(
173            "Checksums-Md5",
174            &checksums
175                .iter()
176                .map(|c| c.to_string())
177                .collect::<Vec<String>>()
178                .join("\n"),
179        );
180    }
181
182    /// Get the build origin
183    pub fn build_origin(&self) -> Option<String> {
184        self.0.get("Build-Origin").map(|s| s.to_string())
185    }
186
187    /// Set the build origin
188    pub fn set_build_origin(&mut self, origin: &str) {
189        self.0.set("Build-Origin", origin);
190    }
191
192    /// Date on which the package was built
193    pub fn build_date(&self) -> Option<String> {
194        self.0.get("Build-Date").map(|s| s.to_string())
195    }
196
197    /// Set the build date
198    pub fn set_build_date(&mut self, date: &str) {
199        self.0.set("Build-Date", date);
200    }
201
202    /// Get the build tainted by field list
203    pub fn build_tainted_by(&self) -> Option<Vec<String>> {
204        self.0
205            .get("Build-Tainted-By")
206            .map(|s| s.split(' ').map(|s| s.to_string()).collect())
207    }
208
209    /// Set the build tainted by field list
210    pub fn set_build_tainted_by(&mut self, tainted_by: Vec<String>) {
211        self.0.set("Build-Tainted-By", &tainted_by.join(" "));
212    }
213
214    /// Get the source format of the package
215    pub fn format(&self) -> Option<String> {
216        self.0.get("Format").map(|s| s.to_string())
217    }
218
219    /// Set the format of the package
220    pub fn set_format(&mut self, format: &str) {
221        self.0.set("Format", format);
222    }
223
224    /// Get the build path
225    pub fn build_path(&self) -> Option<String> {
226        self.0.get("Build-Path").map(|s| s.to_string())
227    }
228
229    /// Set the build path
230    pub fn set_build_path(&mut self, path: &str) {
231        self.0.set("Build-Path", path);
232    }
233
234    /// Get the build environment
235    pub fn environment(&self) -> Option<std::collections::HashMap<String, String>> {
236        self.0.get("Environment").map(|s| {
237            s.lines()
238                .map(|line| {
239                    let (key, value) = line.split_once('=').unwrap();
240                    (key.to_string(), value.to_string())
241                })
242                .collect()
243        })
244    }
245
246    /// Set the build environment
247    pub fn set_environment(&mut self, env: std::collections::HashMap<String, String>) {
248        let mut s = String::new();
249        for (key, value) in env {
250            if !s.is_empty() {
251                s.push('\n');
252            }
253            s.push_str(&format!("{}={}", key, value));
254        }
255        self.0.set("Environment", &s);
256    }
257
258    /// Get the list of installed build depends
259    pub fn installed_build_depends(&self) -> Option<Relations> {
260        self.0
261            .get("Installed-Build-Depends")
262            .map(|s| s.parse().unwrap())
263    }
264
265    /// Set the list of installed build depends
266    pub fn set_installed_build_depends(&mut self, depends: Relations) {
267        self.0.set("Installed-Build-Depends", &depends.to_string());
268    }
269}
270
271impl std::str::FromStr for Buildinfo {
272    type Err = deb822_lossless::ParseError;
273
274    fn from_str(s: &str) -> Result<Self, Self::Err> {
275        Buildinfo::parse(s).to_result()
276    }
277}
278
279impl AstNode for Buildinfo {
280    type Language = deb822_lossless::Lang;
281
282    fn can_cast(kind: <Self::Language as rowan::Language>::Kind) -> bool {
283        deb822_lossless::Paragraph::can_cast(kind) || deb822_lossless::Deb822::can_cast(kind)
284    }
285
286    fn cast(syntax: rowan::SyntaxNode<Self::Language>) -> Option<Self> {
287        if let Some(para) = deb822_lossless::Paragraph::cast(syntax.clone()) {
288            Some(Buildinfo(para))
289        } else if let Some(deb822) = deb822_lossless::Deb822::cast(syntax) {
290            deb822.paragraphs().next().map(Buildinfo)
291        } else {
292            None
293        }
294    }
295
296    fn syntax(&self) -> &rowan::SyntaxNode<Self::Language> {
297        self.0.syntax()
298    }
299}
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_parse() {
306        let s = include_str!("../../testdata/ruff.buildinfo");
307        let buildinfo: Buildinfo = s.parse().unwrap();
308        assert_eq!(buildinfo.format(), Some("1.0".to_string()));
309    }
310
311    #[test]
312    fn test_set_environment() {
313        let s = include_str!("../../testdata/ruff.buildinfo");
314        let mut buildinfo: Buildinfo = s.parse().unwrap();
315
316        let mut env = std::collections::HashMap::new();
317        env.insert("TEST_VAR".to_string(), "test_value".to_string());
318        env.insert("ANOTHER_VAR".to_string(), "another_value".to_string());
319
320        buildinfo.set_environment(env);
321        let env_field = buildinfo.0.get("Environment").unwrap();
322        assert!(env_field.contains("TEST_VAR=test_value"));
323        assert!(env_field.contains("ANOTHER_VAR=another_value"));
324    }
325
326    #[test]
327    fn test_set_build_path() {
328        let s = include_str!("../../testdata/ruff.buildinfo");
329        let mut buildinfo: Buildinfo = s.parse().unwrap();
330
331        buildinfo.set_build_path("/tmp/build/path");
332        assert_eq!(buildinfo.build_path(), Some("/tmp/build/path".to_string()));
333    }
334
335    #[test]
336    fn test_build_origin() {
337        let s = include_str!("../../testdata/ruff.buildinfo");
338        let buildinfo: Buildinfo = s.parse().unwrap();
339        assert_eq!(buildinfo.build_origin(), Some("Debian".to_string()));
340    }
341}