1#![forbid(unsafe_code)]
26#![forbid(missing_docs)]
27use std::collections::BTreeMap;
28use std::fmt::{self, Display};
29use std::fs::read_to_string;
30use std::path::{Path, PathBuf};
31
32#[derive(Debug)]
34pub struct Archive {
35 files: BTreeMap<String, String>,
36}
37
38impl Archive {
39 pub fn load(file: &Path) -> Result<Archive, FileError> {
46 let data = read_to_string(file).map_err(|e| FileError::Io(file.into(), e))?;
47 Archive::parse(&data).map_err(|e| FileError::Data(file.into(), e))
48 }
49
50 pub fn parse(data: &str) -> Result<Archive, Error> {
56 let mut files = BTreeMap::new();
57 let boundary = format!("\n{}", find_boundary(data).ok_or(Error::NoBoundary)?);
58 for item in data[boundary.len() - 1..].split(&boundary) {
59 if item.is_empty() || item.starts_with('\n') {
60 } else if let Some(item) = item.strip_prefix(' ') {
62 if let Some((name, body)) = item.split_once('\n') {
63 files.insert(name.into(), body.into());
64 } else {
65 files.insert(item.into(), String::new());
67 }
68 } else {
69 return Err(Error::InvalidItem(item.into()));
70 }
71 }
72 Ok(Archive { files })
73 }
74
75 pub fn names(&self) -> Vec<&str> {
77 self.files.keys().map(AsRef::as_ref).collect()
78 }
79
80 pub fn get(&self, name: &str) -> Option<&str> {
82 self.files.get(name).map(AsRef::as_ref)
83 }
84
85 pub fn entries(&self) -> impl Iterator<Item = (&str, &str)> {
87 self.files.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))
88 }
89}
90
91fn find_boundary(data: &str) -> Option<&str> {
92 for (i, b) in data.bytes().enumerate() {
93 match (i, b) {
94 (0, b'<') | (_, b'=') => (),
95 (i, b'>') => return Some(&data[0..=i]),
96 _ => return None,
97 }
98 }
99 None
100}
101
102#[derive(Debug)]
104pub enum FileError {
105 Data(PathBuf, Error),
107 Io(PathBuf, std::io::Error),
109}
110
111impl std::error::Error for FileError {}
112
113impl Display for FileError {
114 fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
115 match self {
116 FileError::Data(path, e) => {
117 write!(out, "Failed to parse {path:?}: {e}")
118 }
119 FileError::Io(path, e) => {
120 write!(out, "Failed to read {path:?}: {e}")
121 }
122 }
123 }
124}
125
126#[derive(Debug)]
128pub enum Error {
129 NoBoundary,
131 InvalidItem(String),
133}
134
135impl std::error::Error for Error {}
136
137impl Display for Error {
138 fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
139 match self {
140 Error::NoBoundary => {
141 write!(out, "No archive boundary found")
142 }
143 Error::InvalidItem(item) => {
144 write!(out, "Invalid item: {item:?}")
145 }
146 }
147 }
148}