use_archive_manifest/
lib.rs1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use std::collections::BTreeSet;
7
8use use_archive_entry::{ArchiveEntry, ArchiveEntryKind};
9use use_archive_format::ArchiveEncoding;
10use use_archive_path::is_safe_relative_archive_path;
11
12#[derive(Clone, Debug, Default, Eq, PartialEq)]
14pub struct ArchiveManifest {
15 pub format: ArchiveEncoding,
17 pub entries: Vec<ArchiveEntry>,
19}
20
21impl ArchiveManifest {
22 #[must_use]
24 pub const fn new(format: ArchiveEncoding) -> Self {
25 Self {
26 format,
27 entries: Vec::new(),
28 }
29 }
30
31 #[must_use]
33 pub fn with_entries(mut self, entries: Vec<ArchiveEntry>) -> Self {
34 self.entries = entries;
35 self
36 }
37
38 pub fn push_entry(&mut self, entry: ArchiveEntry) {
40 self.entries.push(entry);
41 }
42
43 #[must_use]
45 pub const fn format(&self) -> ArchiveEncoding {
46 self.format
47 }
48
49 #[must_use]
51 pub fn entries(&self) -> &[ArchiveEntry] {
52 &self.entries
53 }
54
55 #[must_use]
57 pub fn entry_count(&self) -> usize {
58 self.entries.len()
59 }
60
61 #[must_use]
63 pub fn total_size(&self) -> u64 {
64 self.entries.iter().filter_map(ArchiveEntry::size).sum()
65 }
66
67 #[must_use]
69 pub fn known_size_count(&self) -> usize {
70 self.entries
71 .iter()
72 .filter(|entry| entry.size().is_some())
73 .count()
74 }
75
76 #[must_use]
78 pub fn file_count(&self) -> usize {
79 self.count_kind(ArchiveEntryKind::File)
80 }
81
82 #[must_use]
84 pub fn directory_count(&self) -> usize {
85 self.count_kind(ArchiveEntryKind::Directory)
86 }
87
88 #[must_use]
90 pub fn symlink_count(&self) -> usize {
91 self.count_kind(ArchiveEntryKind::Symlink)
92 }
93
94 #[must_use]
96 pub fn has_unsafe_paths(&self) -> bool {
97 self.entries
98 .iter()
99 .any(|entry| !is_safe_relative_archive_path(entry.path()))
100 }
101
102 #[must_use]
104 pub fn unsafe_paths(&self) -> Vec<&ArchiveEntry> {
105 self.entries
106 .iter()
107 .filter(|entry| !is_safe_relative_archive_path(entry.path()))
108 .collect()
109 }
110
111 #[must_use]
113 pub fn extensions(&self) -> Vec<String> {
114 let extensions = self
115 .entries
116 .iter()
117 .filter_map(|entry| entry_extension(entry.path()))
118 .collect::<BTreeSet<_>>();
119
120 extensions.into_iter().collect()
121 }
122
123 fn count_kind(&self, kind: ArchiveEntryKind) -> usize {
124 self.entries
125 .iter()
126 .filter(|entry| entry.kind() == kind)
127 .count()
128 }
129}
130
131fn entry_extension(path: &str) -> Option<String> {
132 let leaf = path.rsplit('/').next().unwrap_or(path);
133 let (stem, extension) = leaf.rsplit_once('.')?;
134
135 if stem.is_empty() || extension.is_empty() {
136 None
137 } else {
138 Some(extension.to_ascii_lowercase())
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::ArchiveManifest;
145 use use_archive_entry::{ArchiveEntry, ArchiveEntryKind};
146 use use_archive_format::{ArchiveEncoding, ArchiveFormat, CompressionFormat};
147
148 #[test]
149 fn summarizes_entry_counts_and_sizes() {
150 let manifest = ArchiveManifest::new(ArchiveEncoding::new(
151 ArchiveFormat::Tar,
152 CompressionFormat::Zstd,
153 ))
154 .with_entries(vec![
155 ArchiveEntry::new("docs/readme.md", ArchiveEntryKind::File).with_size(128),
156 ArchiveEntry::new("docs", ArchiveEntryKind::Directory),
157 ArchiveEntry::new("bin/tool", ArchiveEntryKind::File).with_size(256),
158 ]);
159
160 assert_eq!(manifest.entry_count(), 3);
161 assert_eq!(manifest.file_count(), 2);
162 assert_eq!(manifest.directory_count(), 1);
163 assert_eq!(manifest.total_size(), 384);
164 assert_eq!(manifest.known_size_count(), 2);
165 }
166
167 #[test]
168 fn detects_unsafe_paths_and_extensions() {
169 let manifest = ArchiveManifest::new(ArchiveEncoding::from_extension("release.zip"))
170 .with_entries(vec![
171 ArchiveEntry::file("docs/readme.MD"),
172 ArchiveEntry::file("../secrets.env"),
173 ]);
174
175 assert!(manifest.has_unsafe_paths());
176 assert_eq!(manifest.unsafe_paths().len(), 1);
177 assert_eq!(
178 manifest.extensions(),
179 vec!["env".to_owned(), "md".to_owned()]
180 );
181 }
182}