1use std::collections::HashMap;
6
7use a7n_proto::limits;
8use a7n_proto::v1::{FileKind, FileManifest, FileResume};
9
10mod path;
11pub use path::{ManifestPath, PathError};
12
13const NAME_ATTEMPTS_MAX: usize = 1000;
14
15#[derive(Debug, thiserror::Error, PartialEq, Eq)]
16pub enum ManifestError {
17 #[error("manifest exceeds the entry limit")]
18 TooManyEntries,
19 #[error("entry {index} has an invalid path: {source}")]
20 Path { index: usize, source: PathError },
21 #[error("entry {index} duplicates a path")]
22 DuplicatePath { index: usize },
23 #[error("entry {index} precedes a parent directory")]
24 ParentOrder { index: usize },
25 #[error("entry {index} has a file as a parent")]
26 FileParent { index: usize },
27 #[error("entry {index} has an unspecified or unknown kind")]
28 Kind { index: usize },
29 #[error("entry {index} has an invalid SHA-256 length")]
30 Digest { index: usize },
31 #[error("directory entry {index} has nonzero size")]
32 DirectorySize { index: usize },
33 #[error("directory entry {index} has a digest")]
34 DirectoryDigest { index: usize },
35 #[error("file size sum overflows at entry {index}")]
36 SizeOverflow { index: usize },
37 #[error("file size sum does not match total size")]
38 TotalSize,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum EntryKind {
44 File { size: u64, sha256: [u8; 32] },
45 Directory,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct Entry {
50 pub path: ManifestPath,
51 pub kind: EntryKind,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct ValidManifest {
56 entries: Vec<Entry>,
57}
58
59impl ValidManifest {
60 #[must_use]
61 pub fn entries(&self) -> &[Entry] {
62 &self.entries
63 }
64}
65
66impl TryFrom<&FileManifest> for ValidManifest {
67 type Error = ManifestError;
68
69 fn try_from(manifest: &FileManifest) -> Result<Self, Self::Error> {
70 if manifest.entries.len() > limits::MANIFEST_ENTRIES_MAX {
71 return Err(ManifestError::TooManyEntries);
72 }
73 let mut entries = Vec::with_capacity(manifest.entries.len());
74 let mut paths = HashMap::with_capacity(manifest.entries.len());
75 let mut total = 0_u64;
76 for (index, entry) in manifest.entries.iter().enumerate() {
77 let path = ManifestPath::try_from(entry.path.as_str())
78 .map_err(|source| ManifestError::Path { index, source })?;
79 let kind = validate_kind(entry, index)?;
80 if let EntryKind::File { size, .. } = kind {
81 total = total
82 .checked_add(size)
83 .ok_or(ManifestError::SizeOverflow { index })?;
84 }
85 if paths.insert(path.as_str().to_lowercase(), index).is_some() {
86 return Err(ManifestError::DuplicatePath { index });
87 }
88 entries.push(Entry { path, kind });
89 }
90 validate_parents(&entries, &paths)?;
91 if total != manifest.total_size {
92 return Err(ManifestError::TotalSize);
93 }
94 Ok(Self { entries })
95 }
96}
97
98fn validate_kind(
99 entry: &a7n_proto::v1::FileEntry,
100 index: usize,
101) -> Result<EntryKind, ManifestError> {
102 match FileKind::try_from(entry.kind) {
103 Ok(FileKind::File) => Ok(EntryKind::File {
104 size: entry.size,
105 sha256: entry
106 .sha256
107 .as_slice()
108 .try_into()
109 .map_err(|_| ManifestError::Digest { index })?,
110 }),
111 Ok(FileKind::Directory) => {
112 if entry.size != 0 {
113 return Err(ManifestError::DirectorySize { index });
114 }
115 if !entry.sha256.is_empty() {
116 return Err(ManifestError::DirectoryDigest { index });
117 }
118 Ok(EntryKind::Directory)
119 }
120 _ => Err(ManifestError::Kind { index }),
121 }
122}
123
124fn validate_parents(
125 entries: &[Entry],
126 paths: &HashMap<String, usize>,
127) -> Result<(), ManifestError> {
128 for (index, entry) in entries.iter().enumerate() {
129 let folded = entry.path.as_str().to_lowercase();
130 for (end, _) in folded.match_indices('/') {
131 let Some(&parent) = paths.get(&folded[..end]) else {
132 continue;
133 };
134 if entries[parent].kind != EntryKind::Directory {
135 return Err(ManifestError::FileParent { index });
136 }
137 if parent > index {
138 return Err(ManifestError::ParentOrder { index });
139 }
140 }
141 }
142 Ok(())
143}
144
145#[derive(Debug, thiserror::Error, PartialEq, Eq)]
146pub enum NameError {
147 #[error(transparent)]
148 Path(#[from] PathError),
149 #[error("target name must be a single path segment")]
150 Nested,
151 #[error("target name search exhausted")]
152 Exhausted,
153}
154
155pub fn unique_name(name: &str, taken: impl Fn(&str) -> bool) -> Result<String, NameError> {
162 let path = ManifestPath::try_from(name)?;
163 if path.segments().count() != 1 {
164 return Err(NameError::Nested);
165 }
166 if !taken(name) {
167 return Ok(name.to_owned());
168 }
169 let (stem, extension) = name
170 .rsplit_once('.')
171 .filter(|(stem, _)| !stem.is_empty())
172 .map_or((name, String::new()), |(stem, ext)| {
173 (stem, format!(".{ext}"))
174 });
175 for number in 2..=NAME_ATTEMPTS_MAX {
176 let candidate = format!("{stem} ({number}){extension}");
177 if ManifestPath::try_from(candidate.as_str()).is_ok() && !taken(&candidate) {
178 return Ok(candidate);
179 }
180 }
181 Err(NameError::Exhausted)
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub struct Partial {
187 pub file_index: u32,
188 pub len: u64,
189}
190
191#[must_use]
195pub fn resume_plan(manifest: &ValidManifest, partial: &[Partial]) -> Vec<FileResume> {
196 partial
197 .iter()
198 .filter_map(|part| {
199 let index = usize::try_from(part.file_index).ok()?;
200 let EntryKind::File { size, .. } = manifest.entries.get(index)?.kind else {
201 return None;
202 };
203 (part.len > 0 && part.len < size).then_some(FileResume {
204 file_index: part.file_index,
205 offset: part.len,
206 })
207 })
208 .collect()
209}
210
211#[cfg(test)]
212mod tests;