Skip to main content

rust_apt/
tagfile.rs

1//! Contains structs and functions to parse Debian-styled RFC 822 files.
2use core::iter::Iterator;
3use std::collections::HashMap;
4use std::fmt;
5
6#[derive(Debug)]
7/// The result of a parsing error.
8pub struct ParserError {
9	pub msg: String,
10	pub line: Option<usize>,
11}
12
13impl fmt::Display for ParserError {
14	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
15		if let Some(num) = self.line {
16			write!(f, "{} at line '{num}'", self.msg)?
17		} else {
18			write!(f, "{}", self.msg)?
19		}
20		Ok(())
21	}
22}
23
24impl std::error::Error for ParserError {}
25
26/// A section in a TagFile. A TagFile is made up of double-newline (`\n\n`)
27/// separated paragraphs, each of which make up one of these sections.
28#[derive(Debug)]
29pub struct TagSection {
30	data: HashMap<String, String>,
31}
32
33impl From<TagSection> for HashMap<String, String> {
34	fn from(value: TagSection) -> Self { value.data }
35}
36
37impl TagSection {
38	fn error(msg: &str, line: Option<usize>) -> Result<Self, ParserError> {
39		Err(ParserError {
40			msg: "E:".to_owned() + msg,
41			line,
42		})
43	}
44
45	fn line_is_key(line: &str) -> bool { !line.starts_with(' ') && !line.starts_with('\t') }
46
47	fn next_line_extends_value(lines: &[&str], current_line: usize) -> bool {
48		if let Some(next_line) = lines.get(current_line + 1) {
49			!Self::line_is_key(next_line)
50		} else {
51			false
52		}
53	}
54
55	/// Create a new [`TagSection`] instance.
56	/// # Returns
57	/// * A [`Result`]: The [`Ok`] variant if there was no issue parsing the
58	///   section, and the [`Err`] variant if there was.
59	pub fn new(section: &str) -> Result<Self, ParserError> {
60		// Make sure the string doesn't contain multiple sections.
61		if section.contains("\n\n") {
62			return Self::error("More than one section was found", None);
63		}
64
65		// Make sure the user didn't pass an empty string.
66		if section.is_empty() {
67			return Self::error("An empty string was passed", None);
68		}
69
70		// Start building up the HashMap.
71		let mut data = HashMap::new();
72		let lines = section.lines().collect::<Vec<&str>>();
73
74		// Variables used while parsing.
75		let mut current_key: Option<String> = None;
76		let mut current_value = String::new();
77
78		for (index, line) in lines.iter().enumerate() {
79			// Indexes start at 0, so increase by 1 to get the line number.
80			let line_number = index + 1;
81
82			// If this line starts with a comment ignore it.
83			if line.starts_with('#') {
84				continue;
85			}
86
87			// If this line is a new key, split the line into the key and its
88			// value.
89			if Self::line_is_key(line) {
90				let (key, value) = match line.split_once(':') {
91					Some((key, value)) => {
92						(key.to_string(), value.strip_prefix(' ').unwrap_or(value))
93					},
94					None => {
95						return Self::error(
96							"Line doesn't contain a ':' separator",
97							Some(line_number),
98						);
99					},
100				};
101
102				// Set the current key and value.
103				// If the value is empty, then this is a multiline field, and
104				// it's going to be one of these things:
105				// 1. A multiline field, in which case we want to add a
106				// newline to reflect such.
107				// 2. A key with an empty value, in which case it will
108				// be removed post-processing.
109				current_key = Some(key);
110
111				if value.is_empty() {
112					current_value = "\n".to_string();
113				} else {
114					current_value = value.to_string();
115
116					// If the next extends the value, add the newline before it.
117					if Self::next_line_extends_value(&lines, index) {
118						current_value += "\n";
119					}
120				}
121			}
122
123			// If this line is indented with spaces or tabs, add it to the
124			// current value. This should never end up running in conjunction
125			// with the above `if` block.
126			if line.starts_with(' ') || line.starts_with('\t') {
127				current_value += line;
128
129				// If the next line extends the value, add the newline.
130				// `line_number` conveniently is the next index, so use that
131				// to our advantage.
132				if Self::next_line_extends_value(&lines, index) {
133					current_value += "\n";
134				}
135			}
136
137			// If the next line is a new key or this is the last line, add the
138			// current key and value to the HashMap. `line_number`
139			// conveniently is the next index, so use that to our advantage.
140			if !Self::next_line_extends_value(&lines, index) {
141				// If no key exists, we've defined a paragraph (at the beginning
142				// of the control file) with no key. This would be parsed at
143				// the very beginning, but the file may have an unknown
144				// amount of comment lines, so we just do this here as a
145				// normal step of the parsing stage.
146				if current_key.is_none() {
147					return Self::error(
148						"No key defined for the currently indented line",
149						Some(line_number),
150					);
151				}
152
153				// Add the key and reset the `current_key` and `current_value`
154				// counters.
155				data.insert(current_key.unwrap(), current_value);
156				current_key = None;
157				current_value = String::new();
158			}
159		}
160
161		Ok(Self { data })
162	}
163
164	/// Get the underlying [`HashMap`] used in the generated [`TagSection`].
165	pub fn hashmap(&self) -> &HashMap<String, String> { &self.data }
166
167	/// Get the value of the specified key.
168	pub fn get(&self, key: &str) -> Option<&String> { self.data.get(key) }
169
170	/// Get the value of the specified key,
171	///
172	/// Returns specified default on failure.
173	pub fn get_default<'a>(&'a self, key: &str, default: &'a str) -> &'a str {
174		if let Some(value) = self.data.get(key) {
175			return value;
176		}
177		default
178	}
179}
180
181/// Parses a TagFile: these are files such as Debian `control` and `Packages`
182/// files.
183///
184/// # Returns
185/// * A [`Result`]: The [`Ok`] variant containing the vector of [`TagSection`]
186///   objects if there was no issue parsing the file, and the [`Err`] variant if
187///   there was.
188pub fn parse_tagfile(content: &str) -> Result<Vec<TagSection>, ParserError> {
189	let mut sections = vec![];
190	let section_strings = content.split("\n\n");
191
192	for (iter, section) in section_strings.clone().enumerate() {
193		// If this section is empty (i.e. more than one empty line was placed
194		// between each section), then ignore this section.
195		if section.is_empty() || section.chars().all(|c| c == '\n') {
196			break;
197		}
198
199		match TagSection::new(section) {
200			Ok(section) => sections.push(section),
201			Err(mut err) => {
202				// If an error line was provided, add the number of lines in the
203				// sections before this one. Otherwise no line was
204				// specified, and we'll just specify the number of lines in
205				// the section before this one so we know which section the line
206				// is in.
207				let mut line_count = 0;
208
209				for _ in 0..iter {
210					// Add one for the line separation between each section.
211					line_count += 1;
212
213					// Add the line count in this section.
214					line_count += section_strings.clone().count();
215				}
216
217				if let Some(line) = err.line {
218					err.line = Some(line_count + line);
219				} else {
220					err.line = Some(line_count);
221				}
222			},
223		}
224	}
225
226	Ok(sections)
227}