1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
use self::super::{parse, ErroneousBodyPath, HrxError}; use jetscii::Substring as SubstringSearcher; use self::super::output::write_archive; use std::io::{Error as IoError, Write}; use self::super::util::boundary_str; use linked_hash_map::LinkedHashMap; use std::num::NonZeroUsize; use std::borrow::Borrow; use std::str::FromStr; use std::fmt; /// A Human-Readable Archive, consisting of an optional comment and some entries, all separated by the boundary. /// /// The archive boundary consists of a particular-length sequence of `=`s bounded with `<` and `>` on either side; /// that sequence must be consistent across the entirety of the archive, which means that no `body` /// (be it a comment or file contents) can contain a newline followed by the boundary. /// /// However, there is no way to enforce that on the typesystem level, /// meaning that the entries and comments can be modified at will, /// so instead the archive will automatically check for boundary validity when /// /// 1. changing the global boundary length (via [`set_boundary_length()`](#method.set_boundary_length)) and /// 2. serialising to an output stream (usually via [`serialise()`](#method.serialise)) /// /// and return the paths to the erroneous (i.e. boundary-containing) `body`s. #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct HrxArchive { /// Some optional metadata. /// /// Cannot contain a newline followed by a boundary. pub comment: Option<String>, /// Some optional archive entries with their paths. pub entries: LinkedHashMap<HrxPath, HrxEntry>, pub(crate) boundary_length: NonZeroUsize, } /// A single entry in the archive, consisting of an optional comment and some data. #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct HrxEntry { /// Some optional metadata. /// /// Cannot contain a newline followed by a boundary. pub comment: Option<String>, /// The specific entry data. pub data: HrxEntryData, } /// Some variant of an entry's contained data. #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] pub enum HrxEntryData { /// File with some optional contents. /// /// Cannot contain a newline followed by a boundary nor start with a boundary. File { body: Option<String>, }, /// Bodyless directory. Directory, } /// Verified-valid path to an entry in the archive. /// /// Paths consist of `/`-separated components, each one consisting of characters higher than U+001F, except `/`, `\\` and `:`. /// Components cannot be `.` nor `..`. /// /// # Examples /// /// ``` /// # use hrx::HrxPath; /// # use std::str::FromStr; /// let path = HrxPath::from_str("хэнло/communism.exe").unwrap(); /// assert_eq!(path.as_ref(), "хэнло/communism.exe"); /// assert_eq!(path.to_string(), "хэнло/communism.exe"); /// /// let raw = path.into_inner(); /// assert_eq!(raw, "хэнло/communism.exe"); /// ``` #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct HrxPath(pub(crate) String); impl HrxArchive { /// Create an empty archive with the specified boundary length. pub fn new(boundary_length: NonZeroUsize) -> HrxArchive { HrxArchive { comment: None, entries: LinkedHashMap::new(), boundary_length: boundary_length, } } /// Get the current boundary length, i.e. the amount of `=` characters in the boundary. /// /// # Examples /// /// ``` /// # use std::str::FromStr; /// # use hrx::HrxArchive; /// let arch_str = r#"<===> input.scss /// ul { /// margin-left: 1em; /// li { /// list-style-type: none; /// } /// } /// /// <===> output.css /// ul { /// margin-left: 1em; /// } /// ul li { /// list-style-type: none; /// }"#; /// /// let arch = HrxArchive::from_str(arch_str).unwrap(); /// assert_eq!(arch.boundary_length().get(), 3); /// ``` pub fn boundary_length(&self) -> NonZeroUsize { self.boundary_length } /// Set new boundary length, if valid. /// /// Checks, whether any `body`s within the archive contain the new boundary; /// if so – errors out with their paths, /// otherwise sets the boundary length to the specified value. /// /// # Examples /// /// ``` /// # use hrx::{ErroneousBodyPath, HrxArchive}; /// # use std::num::NonZeroUsize; /// # use std::str::FromStr; /// let arch_str = r#"<===> boundary-5.txt /// This file contains a 5-length boundary: /// <=====> /// ^ right there /// /// <===> /// This is a comment, /// <=======> /// which contains a 7-length boundary. /// /// <===> fine.txt /// This file consists of /// multiple lines, but none of them /// starts with any sort of boundary-like string"#; /// /// let mut arch = HrxArchive::from_str(arch_str).unwrap(); /// assert_eq!(arch.boundary_length().get(), 3); /// /// assert_eq!(arch.set_boundary_length(NonZeroUsize::new(4).unwrap()), Ok(())); /// assert_eq!(arch.boundary_length().get(), 4); /// /// assert_eq!(arch.set_boundary_length(NonZeroUsize::new(5).unwrap()), /// Err(ErroneousBodyPath::EntryData("boundary-5.txt".to_string()).into())); /// assert_eq!(arch.boundary_length().get(), 4); /// /// assert_eq!(arch.set_boundary_length(NonZeroUsize::new(6).unwrap()), Ok(())); /// assert_eq!(arch.boundary_length().get(), 6); /// /// assert_eq!(arch.set_boundary_length(NonZeroUsize::new(7).unwrap()), /// Err(ErroneousBodyPath::EntryComment("fine.txt".to_string()).into())); /// assert_eq!(arch.boundary_length().get(), 6); /// /// assert_eq!(arch.set_boundary_length(NonZeroUsize::new(8).unwrap()), Ok(())); /// assert_eq!(arch.boundary_length().get(), 8); /// ``` pub fn set_boundary_length(&mut self, new_len: NonZeroUsize) -> Result<(), HrxError> { self.validate_boundlen(new_len)?; self.boundary_length = new_len; Ok(()) } /// Validate that no `body`s contain a `boundary` or error out with the paths to the ones that do, /// /// # Examples /// /// ``` /// # use hrx::{ErroneousBodyPath, HrxEntryData, HrxArchive, HrxEntry}; /// # use std::num::NonZeroUsize; /// let mut arch = HrxArchive::new(NonZeroUsize::new(3).unwrap()); /// arch.comment = Some("Yeehaw! the comment\n<===>\n contains the boundary!".to_string()); /// /// arch.entries.insert("directory/dsc.txt".parse().unwrap(), HrxEntry { /// comment: None, /// data: HrxEntryData::File { /// body: Some("As does this file\n<===>, whew!".to_string()), /// }, /// }); /// /// assert_eq!(arch.validate_content(), /// Err(vec![ErroneousBodyPath::RootComment, /// ErroneousBodyPath::EntryData("directory/dsc.txt".to_string())].into())); /// ``` pub fn validate_content(&self) -> Result<(), HrxError> { self.validate_boundlen(self.boundary_length) } fn validate_boundlen(&self, len: NonZeroUsize) -> Result<(), HrxError> { let bound = boundary_str(len); let ss = SubstringSearcher::new(&bound); let mut paths = vec![]; let _ = verify_opt(&self.comment, &ss).map_err(|_| paths.push(ErroneousBodyPath::RootComment)); for (pp, dt) in &self.entries { let _ = verify_opt(&dt.comment, &ss).map_err(|_| paths.push(ErroneousBodyPath::EntryComment(pp.to_string()))); match dt.data { HrxEntryData::File { ref body } => { let _ = verify_opt(&body, &ss).map_err(|_| paths.push(ErroneousBodyPath::EntryData(pp.to_string()))); } HrxEntryData::Directory => {} } } if !paths.is_empty() { Err(paths.into()) } else { Ok(()) } } /// Write the archive out to the specified output stream, after verification. /// /// The compound result type is due to the fact that `std::io::Error` doesn't play well with having it in an enum variant. /// /// # Examples /// /// Failed validation: /// /// ``` /// # use hrx::{ErroneousBodyPath, HrxArchive, HrxPath}; /// # use std::num::NonZeroUsize; /// let mut arch = HrxArchive::new(NonZeroUsize::new(3).unwrap()); /// arch.comment = Some("Yeehaw! the comment\n<===>\n contains the boundary!".to_string()); /// /// let mut out = vec![]; /// assert_eq!(arch.serialise(&mut out).unwrap_err().unwrap(), /// ErroneousBodyPath::RootComment.into()); /// // Note how the returned result cannot be directly compared to, /// // as a byproduct of `std::io::Error` being contained therein. /// ``` /// /// Generation: /// /// ``` /// # use hrx::{ErroneousBodyPath, HrxEntryData, HrxArchive, HrxEntry, HrxPath}; /// # use std::num::NonZeroUsize; /// let mut arch = HrxArchive::new(NonZeroUsize::new(5).unwrap()); /// arch.comment = /// Some("This is the archive comment, forthlaying its contents' description".to_string()); /// /// arch.entries.insert("directory".parse().unwrap(), HrxEntry { /// comment: Some("This directory contains files!".to_string()), /// data: HrxEntryData::Directory, /// }); /// /// arch.entries.insert("directory/dsc.txt".parse().unwrap(), HrxEntry { /// comment: /// Some("This file forthlays the building blocks of any stable society".to_string()), /// data: HrxEntryData::File { /// body: Some("Коммунизм!\n".to_string()), /// }, /// }); /// /// let mut out = vec![]; /// arch.serialise(&mut out).unwrap(); /// assert_eq!(String::from_utf8(out).unwrap(), r#"<=====> /// This directory contains files! /// <=====> directory/ /// <=====> /// This file forthlays the building blocks of any stable society /// <=====> directory/dsc.txt /// Коммунизм! /// /// <=====> /// This is the archive comment, forthlaying its contents' description"#); /// ``` /// /// Transserialisation: /// /// ``` /// # use std::str::FromStr; /// # use hrx::HrxArchive; /// let arch_str = r#"<===> input.scss /// ul { /// margin-left: 1em; /// li { /// list-style-type: none; /// } /// } /// /// <===> output.css /// ul { /// margin-left: 1em; /// } /// ul li { /// list-style-type: none; /// }"#; /// /// let arch = HrxArchive::from_str(arch_str).unwrap(); /// /// let mut out = vec![]; /// arch.serialise(&mut out).unwrap(); /// assert_eq!(String::from_utf8(out).unwrap(), arch_str); /// ``` pub fn serialise<W: Write>(&self, into: &mut W) -> Result<(), Result<HrxError, IoError>> { write_archive(&self, into) } } fn verify_opt(which: &Option<String>, with: &SubstringSearcher) -> Result<(), ()> { if let Some(dt) = which.as_ref() { if with.find(dt).is_some() { return Err(()); } } Ok(()) } impl FromStr for HrxArchive { type Err = HrxError; fn from_str(s: &str) -> Result<Self, Self::Err> { let width = parse::discover_first_boundary_length(s).ok_or(HrxError::NoBoundary)?; let (comment, entries, boundary_length) = parse::archive(s, width)?; Ok(HrxArchive { comment: comment, entries: parse::reduce_raw_entries_and_validate_directory_tree(entries)?, boundary_length: boundary_length, }) } } impl HrxPath { /// Unwraps the contained path. pub fn into_inner(self) -> String { self.0 } } impl fmt::Display for HrxPath { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.write_str(&self.0) } } impl FromStr for HrxPath { type Err = HrxError; fn from_str(s: &str) -> Result<Self, Self::Err> { let parsed = parse::path(s, NonZeroUsize::new(1).unwrap())?; Ok(parsed) } } impl Borrow<str> for HrxPath { fn borrow(&self) -> &str { &self.0 } } impl AsRef<str> for HrxPath { fn as_ref(&self) -> &str { &self.0 } }