libmedusa_zip/
lib.rs

1/*
2 * Description: ???
3 *
4 * Copyright (C) 2023 Danny McClanahan <dmcC2@hypnicjerk.ai>
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * Licensed under the Apache License, Version 2.0 (see LICENSE).
8 */
9
10//! ???
11
12/* These clippy lint descriptions are purely non-functional and do not affect the functionality
13 * or correctness of the code. */
14// #![warn(missing_docs)]
15
16/* Note: run clippy with: rustup run nightly cargo-clippy! */
17#![deny(unsafe_code)]
18/* Ensure any doctest warnings fails the doctest! */
19#![doc(test(attr(deny(warnings))))]
20/* Enable all clippy lints except for many of the pedantic ones. It's a shame this needs to be
21 * copied and pasted across crates, but there doesn't appear to be a way to include inner
22 * attributes from a common source. */
23#![deny(
24  clippy::all,
25  clippy::default_trait_access,
26  clippy::expl_impl_clone_on_copy,
27  clippy::if_not_else,
28  clippy::needless_continue,
29  clippy::single_match_else,
30  clippy::unseparated_literal_suffix,
31  clippy::used_underscore_binding
32)]
33/* It is often more clear to show that nothing is being moved. */
34#![allow(clippy::match_ref_pats)]
35/* Subjective style. */
36#![allow(
37  clippy::derived_hash_with_manual_eq,
38  clippy::len_without_is_empty,
39  clippy::redundant_field_names,
40  clippy::too_many_arguments,
41  clippy::single_component_path_imports,
42  clippy::double_must_use
43)]
44/* Default isn't as big a deal as people seem to think it is. */
45#![allow(clippy::new_without_default, clippy::new_ret_no_self)]
46/* Arc<Mutex> can be more clear than needing to grok Orderings. */
47#![allow(clippy::mutex_atomic)]
48
49use displaydoc::Display;
50use thiserror::Error;
51
52use std::{cmp, fmt, ops::Range, path::PathBuf};
53
54/// Allowed zip format quirks that we refuse to handle right now.
55#[derive(Debug, Display, Error)]
56pub enum MedusaNameFormatError {
57  /// name is empty
58  NameIsEmpty,
59  /// name starts with '/': {0}
60  NameStartsWithSlash(String),
61  /// name starts wtih './': {0}
62  NameStartsWithDotSlash(String),
63  /// name ends with '/': {0}
64  NameEndsWithSlash(String),
65  /// name has '//': {0}
66  NameHasDoubleSlash(String),
67}
68
69/* TODO: figure out how to make this represent both file and directory names
70 * without coughing up blood. */
71#[derive(Clone, Debug, PartialEq, Eq, Hash)]
72pub struct EntryName {
73  name: String,
74  components: Vec<Range<usize>>,
75}
76
77impl fmt::Display for EntryName {
78  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "'{}'", self.name) }
79}
80
81impl cmp::PartialOrd for EntryName {
82  fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { Some(self.cmp(other)) }
83}
84
85impl cmp::Ord for EntryName {
86  fn cmp(&self, other: &Self) -> cmp::Ordering {
87    self.components_vec().cmp(&other.components_vec())
88  }
89}
90
91impl EntryName {
92  pub(crate) fn is_empty(&self) -> bool { self.name.is_empty() }
93
94  pub(crate) fn empty() -> Self {
95    Self {
96      name: "".to_string(),
97      components: Vec::new(),
98    }
99  }
100
101  pub fn into_string(self) -> String {
102    if self.is_empty() {
103      panic!("attempted to write an empty EntryName!");
104    }
105    self.name
106  }
107
108  pub(crate) fn add_prefix(&mut self, prefix: &Self) {
109    if prefix.is_empty() {
110      return;
111    }
112    self.name = format!("{}/{}", prefix.name, self.name);
113    self.components = Self::split_indices(&self.name);
114  }
115
116  fn split_indices(s: &str) -> Vec<Range<usize>> {
117    let mut prev_begin: usize = 0;
118    let mut components: Vec<Range<usize>> = Vec::new();
119    for (match_start, matched_str) in s.match_indices('/') {
120      components.push(prev_begin..match_start);
121      prev_begin = match_start + matched_str.len();
122    }
123    components.push(prev_begin..s.len());
124    components
125  }
126
127  fn iter_components(&self, range: Range<usize>) -> impl Iterator<Item=&str> {
128    self.components[range].iter().map(|r| &self.name[r.clone()])
129  }
130
131  pub fn validate(name: String) -> Result<Self, MedusaNameFormatError> {
132    if name.is_empty() {
133      Err(MedusaNameFormatError::NameIsEmpty)
134    } else if name.starts_with('/') {
135      /* We won't produce any non-relative paths. */
136      Err(MedusaNameFormatError::NameStartsWithSlash(name))
137    } else if name.starts_with("./") {
138      /* We refuse to try to process ./ paths, asking the user to strip them
139       * instead. */
140      Err(MedusaNameFormatError::NameStartsWithDotSlash(name))
141    } else if name.ends_with('/') {
142      /* We only enter file names. */
143      Err(MedusaNameFormatError::NameEndsWithSlash(name))
144    } else if name.contains("//") {
145      Err(MedusaNameFormatError::NameHasDoubleSlash(name))
146    } else {
147      let components = Self::split_indices(&name);
148      Ok(Self { name, components })
149    }
150  }
151
152  pub fn all_components(&self) -> impl Iterator<Item=&str> {
153    self.iter_components(0..self.components.len())
154  }
155
156  fn components_vec(&self) -> Vec<&str> { self.all_components().collect() }
157
158  pub(crate) fn parent_components(&self) -> impl Iterator<Item=&str> {
159    self.iter_components(0..self.components.len() - 1)
160  }
161}
162
163#[derive(Clone, Debug, PartialEq, Eq)]
164pub struct FileSource {
165  pub name: EntryName,
166  pub source: PathBuf,
167}
168
169/* Implement {Partial,}Ord to sort a vector of these by name without
170 * additional allocation, because Vec::sort_by_key() gets mad if the key
171 * possesses a lifetime, otherwise requiring the `name` string to be
172 * cloned. */
173impl cmp::PartialOrd for FileSource {
174  fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { Some(self.cmp(other)) }
175}
176
177impl cmp::Ord for FileSource {
178  fn cmp(&self, other: &Self) -> cmp::Ordering { self.name.cmp(&other.name) }
179}
180
181pub mod destination;
182
183pub mod crawl;
184
185pub mod zip;
186
187pub mod merge;
188
189/* FIXME: add tests! */
190/* #[cfg(test)] */
191/* mod test { */
192/* use super::*; */
193
194/* use proptest::{prelude::*, strategy::Strategy}; */
195/* } */
196/* use proptest::{prelude::*, strategy::Strategy}; */
197/* } */