1#[non_exhaustive]
67pub struct Archive {
69 pub comment: String,
71
72 pub files: Vec<File>,
74}
75
76impl Archive {
77 pub fn new() -> Self {
79 Archive {
80 comment: String::new(),
81 files: Vec::new(),
82 }
83 }
84
85 pub fn from_file(path: &str) -> Result<Self, std::io::Error> {
87 let mut f = std::fs::File::open(path)?;
88 Archive::read(&mut f)
89 }
90
91 pub fn read(reader: &mut impl std::io::Read) -> Result<Self, std::io::Error> {
93 let mut s = String::new();
94 reader.read_to_string(&mut s)?;
95 Ok(Archive::from(s.as_str()))
96 }
97
98 pub fn contains(&self, name: &str) -> bool {
100 self.files.iter().any(|f| f.name.as_str() == name)
101 }
102
103 pub fn get(&self, name: &str) -> Option<&File> {
106 self.files.iter().find(|f| f.name.as_str() == name)
107 }
108}
109
110impl Default for Archive {
111 fn default() -> Self {
112 Self::new()
113 }
114}
115
116impl From<&str> for Archive {
118 fn from(value: &str) -> Self {
120 let (comment, mut file_name, mut after) = find_next_marker(value);
121 let mut files = Vec::new();
122 while !file_name.is_empty() {
123 let (data, next_file, after_next) = find_next_marker(after);
124
125 let content = fix_newline(data);
126
127 let file = File::new(file_name, &content);
128 files.push(file);
129
130 file_name = next_file;
131 after = after_next;
132 }
133
134 Archive {
135 comment: comment.to_owned(),
136 files,
137 }
138 }
139}
140
141impl std::ops::Index<&str> for Archive {
142 type Output = File;
143
144 fn index(&self, index: &str) -> &Self::Output {
147 match self.files.iter().find(|f| f.name.as_str() == index) {
148 Some(f) => f,
149 None => panic!("Archive doesn't contain file: {}", index),
150 }
151 }
152}
153
154#[non_exhaustive]
156pub struct File {
157 pub name: String,
159
160 pub content: String,
162}
163
164impl File {
165 pub fn new(name: &str, content: &str) -> File {
167 File {
168 name: name.to_owned(),
169 content: content.to_owned(),
170 }
171 }
172}
173
174const MARKER: &str = "-- ";
175const NEWLINE_MARKER: &str = "\n-- ";
176const MARKER_END: &str = " --";
177
178fn find_next_marker(s: &str) -> (&str, &str, &str) {
182 let mut i = 0;
183 loop {
184 let (name, after) = parse_leading_marker(&s[i..]);
185 if !name.is_empty() {
186 return (&s[0..i], name, after);
187 }
188
189 if let Some(index) = s[i..].find(NEWLINE_MARKER) {
190 i += index + 1;
191 } else {
192 return (s, "", "");
193 }
194 }
195}
196
197fn fix_newline(s: &str) -> String {
198 let mut owned = s.to_owned();
199 if !owned.is_empty() && !owned.ends_with('\n') {
200 owned.push('\n');
201 }
202 owned
203}
204
205fn parse_leading_marker(s: &str) -> (&str, &str) {
208 if !s.starts_with(MARKER) {
209 return ("", "");
210 }
211
212 let (mut data, after) = match s.split_once('\n') {
213 None => (s, ""),
214 Some((x, y)) => (x, y),
215 };
216
217 if data.ends_with('\r') {
218 data = &data[0..data.len() - 1];
219 }
220
221 if !data.ends_with(MARKER_END) || data.len() <= (MARKER.len() + MARKER_END.len()) {
222 return ("", "");
223 }
224
225 let name = data[MARKER.len()..data.len() - MARKER_END.len()].trim();
226 (name, after)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 fn do_parse_test(s: &'static str, expect: Archive) {
234 let parsed = Archive::from(s);
235 assert_eq!(
236 parsed.comment, expect.comment,
237 "parsed.comment == expected.comment"
238 );
239
240 let compare_max = usize::min(parsed.files.len(), expect.files.len());
241 for i in 0..compare_max {
242 let parsed_file = &parsed.files[i];
243 let expected_file = &expect.files[i];
244
245 assert_eq!(
246 parsed_file.name, expected_file.name,
247 "parsed.files[{}].name == expected.files[{}].name",
248 i, i
249 );
250
251 assert_eq!(
252 parsed_file.content, expected_file.content,
253 "parsed.files[{}].content == expected.files[{}].content",
254 i, i
255 );
256 }
257 }
258
259 macro_rules! parse_test {
260 ($name:ident, $str:expr, $expect:expr) => {
261 #[test]
262 fn $name() {
263 do_parse_test($str, $expect);
264 }
265 };
266 }
267
268 parse_test!(
269 parse_basic,
270 r"comment1
271comment2
272-- file1 --
273File 1 text.
274-- foo ---
275More file 1 text.
276-- file 2 --
277File 2 text.
278-- empty --
279-- empty filename line --
280some content
281-- --
282-- noNL --
283hello world",
284 Archive {
285 comment: "comment1\ncomment2\n".to_owned(),
286 files: vec![
287 File::new("file1", "File 1 text.\n-- foo ---\nMore file 1 text.\n"),
288 File::new("file 2", "File 2 text.\n"),
289 File::new("empty", ""),
290 File::new("empty filename line", "some content\n-- --\n"),
291 File::new("noNL", "hello world\n"),
292 ]
293 }
294 );
295}