Skip to main content

hrx_get/
lib.rs

1//! Implement simple reading of Human Readable Archive (.hrx) data.
2//!
3//! The Human Readable Achive format specification lives at
4//! [https://github.com/google/hrx](https://github.com/google/hrx).
5//!
6//! This crate only supports _reading_ `.hrx` data.
7//!
8//! # Example
9//!
10//! ```
11//! # fn main() -> Result<(), hrx_get::Error> {
12//! let archive = hrx_get::Archive::parse(
13//!     "<===> one.txt\n\
14//!      Content of one text file\n\
15//!      <===>\n\
16//!      This is a comment\n\
17//!      <===> subdir/file.txt\n\
18//!      Contents of a file in a subdir.\n\
19//!      <===>\n"
20//! )?;
21//! assert_eq!(archive.get("one.txt"), Some("Content of one text file"));
22//! # Ok(())
23//! # }
24//! ```
25#![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/// Parsed Human Readable Archive data.
33#[derive(Debug)]
34pub struct Archive {
35    files: BTreeMap<String, String>,
36}
37
38impl Archive {
39    /// Load hrx data from a file system path.
40    ///
41    /// # Errors
42    ///
43    /// Returns a [`FileError::Io`] if the path can't be read or a
44    /// [`FileError::Data`] if the contents isn't proper hrx.
45    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    /// Parse hrx data from an in-memory buffer.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if `data` is not proper hrx contents.
55    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                // item is a comment, ignore it.
61            } 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                    // Directory / empty file
66                    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    /// Get a vec of the file names in the archive.
76    pub fn names(&self) -> Vec<&str> {
77        self.files.keys().map(AsRef::as_ref).collect()
78    }
79
80    /// Get the contents of a file in the archive.
81    pub fn get(&self, name: &str) -> Option<&str> {
82        self.files.get(name).map(AsRef::as_ref)
83    }
84
85    /// Iterate over (name, content) pairs for the files in the archive.
86    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/// An error reading or parsing a .hrx archive.
103#[derive(Debug)]
104pub enum FileError {
105    /// Data error parsing archive
106    Data(PathBuf, Error),
107    /// I/O error reading archive
108    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/// An error parsing a .hrx archive.
127#[derive(Debug)]
128pub enum Error {
129    /// No archive bound found
130    NoBoundary,
131    /// Invalid item in archive
132    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}