1use crate::error::{Par2Error, Result};
2
3const MAX_EXPLICIT_VOLUME_COUNT: u32 = 31;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct RecoveryVolumePlan {
8 pub first_exponent: u32,
10 pub recovery_count: u32,
12 pub filename: String,
14}
15
16pub(crate) fn allocate_volumes(
18 first_exponent: u32,
19 recovery_count: u32,
20 requested_count: Option<u32>,
21 scheme: super::options::VolumeScheme,
22 stem: &str,
23 largest_source_file_size: u64,
24 block_size: u64,
25) -> Result<Vec<RecoveryVolumePlan>> {
26 if let Some(count) = requested_count {
27 if count == 0 {
28 return Err(Par2Error::InvalidCreationOptions {
29 reason: "explicit volume count must be positive".to_string(),
30 });
31 }
32 if count > MAX_EXPLICIT_VOLUME_COUNT {
33 return Err(Par2Error::InvalidCreationOptions {
34 reason: format!(
35 "explicit volume count {count} exceeds maximum {MAX_EXPLICIT_VOLUME_COUNT}"
36 ),
37 });
38 }
39 }
40 if requested_count.is_some() && matches!(scheme, super::options::VolumeScheme::Limited) {
41 return Err(Par2Error::InvalidCreationOptions {
42 reason: "a volume count is not valid with limited volume sizing".to_string(),
43 });
44 }
45 if recovery_count == 0 {
46 if requested_count.is_some() {
47 return Err(Par2Error::InvalidCreationOptions {
48 reason: "a volume count is not valid when recovery count is zero".to_string(),
49 });
50 }
51 return Ok(Vec::new());
52 }
53
54 let allocations = match (requested_count, scheme) {
55 (Some(count), _) => uniform_allocations(first_exponent, recovery_count, count),
56 (None, super::options::VolumeScheme::Uniform) => {
57 let count = bit_length(recovery_count);
58 uniform_allocations(first_exponent, recovery_count, count)
59 }
60 (None, super::options::VolumeScheme::Variable) => {
61 let count = bit_length(recovery_count);
62 variable_allocations(first_exponent, recovery_count, count)
63 }
64 (None, super::options::VolumeScheme::Limited) => limited_allocations(
65 first_exponent,
66 recovery_count,
67 largest_source_file_size,
68 block_size,
69 )?,
70 };
71 let boundary_exponent = first_exponent.checked_add(recovery_count).ok_or_else(|| {
72 Par2Error::InvalidCreationOptions {
73 reason: "recovery exponent range overflows".to_string(),
74 }
75 })?;
76
77 if allocations.is_empty() || allocations.len() as u32 > recovery_count {
78 return Err(Par2Error::InvalidCreationOptions {
79 reason: "recovery volumes must have a positive count".to_string(),
80 });
81 }
82 let exponent = allocations
83 .last()
84 .and_then(|(first, count)| first.checked_add(*count))
85 .ok_or_else(|| Par2Error::InvalidCreationOptions {
86 reason: "recovery exponent range overflows".to_string(),
87 })?;
88 if exponent != boundary_exponent || allocations.iter().any(|(_, count)| *count == 0) {
89 return Err(Par2Error::InvalidCreationOptions {
90 reason: "recovery volume allocation does not cover the recovery range".to_string(),
91 });
92 }
93 let exponent_width = decimal_width(boundary_exponent);
94 let count_width = decimal_width(
95 allocations
96 .iter()
97 .map(|&(_, count)| count)
98 .chain(std::iter::once(0))
99 .max()
100 .unwrap_or(0),
101 );
102 let volumes = allocations
103 .into_iter()
104 .map(|(first_exponent, recovery_count)| RecoveryVolumePlan {
105 filename: format!(
106 "{stem}.vol{first_exponent:0exponent_width$}+{recovery_count:0count_width$}.par2"
107 ),
108 first_exponent,
109 recovery_count,
110 })
111 .collect();
112 Ok(volumes)
113}
114
115fn uniform_allocations(first_exponent: u32, total: u32, volume_count: u32) -> Vec<(u32, u32)> {
116 let base = total / volume_count;
117 let remainder = total % volume_count;
118 let mut exponent = first_exponent;
119 (0..volume_count)
120 .map(|index| {
121 let count = base + u32::from(index < remainder);
122 let allocation = (exponent, count);
123 exponent += count;
124 allocation
125 })
126 .collect()
127}
128
129fn variable_allocations(first_exponent: u32, total: u32, volume_count: u32) -> Vec<(u32, u32)> {
130 let mut low = 1u64;
131 let geometric_sum = if volume_count >= 64 {
132 u64::MAX
133 } else {
134 (1u64 << volume_count) - 1
135 };
136 while low.saturating_mul(geometric_sum) < total as u64 {
137 low = low.saturating_mul(2);
138 }
139
140 let mut remaining = total as u64;
141 let mut allocations = Vec::with_capacity(volume_count as usize);
142 let mut exponent = first_exponent;
143 for _ in 0..volume_count {
144 let count = remaining.min(low);
145 allocations.push((exponent, count as u32));
146 exponent += count as u32;
147 remaining -= count;
148 low = low.saturating_mul(2);
149 }
150 allocations
151}
152
153fn limited_allocations(
154 first_exponent: u32,
155 recovery_count: u32,
156 largest_source_file_size: u64,
157 block_size: u64,
158) -> Result<Vec<(u32, u32)>> {
159 if block_size == 0 || largest_source_file_size == 0 {
160 return Err(Par2Error::InvalidCreationOptions {
161 reason: "limited volume sizing requires a non-empty source and block size".to_string(),
162 });
163 }
164 let largest = u32::try_from(
165 largest_source_file_size
166 .checked_add(block_size - 1)
167 .ok_or_else(|| Par2Error::InvalidCreationOptions {
168 reason: "largest source block count overflows".to_string(),
169 })?
170 / block_size,
171 )
172 .map_err(|_| Par2Error::InvalidCreationOptions {
173 reason: "largest source block count exceeds the supported range".to_string(),
174 })?;
175 if largest == 0 {
176 return Err(Par2Error::InvalidCreationOptions {
177 reason: "limited volume sizing computed zero source blocks".to_string(),
178 });
179 }
180
181 let whole = recovery_count / largest;
182 let whole = whole.saturating_sub(1);
183 let extra = recovery_count - whole * largest;
184 let volume_count =
185 whole
186 .checked_add(bit_length(extra))
187 .ok_or_else(|| Par2Error::InvalidCreationOptions {
188 reason: "limited volume count overflows".to_string(),
189 })?;
190 let mut allocations = vec![(0u32, 0u32); volume_count as usize];
191 let mut filenumber = volume_count;
192 let mut blocks = recovery_count;
193 let mut exponent = first_exponent.checked_add(recovery_count).ok_or_else(|| {
194 Par2Error::InvalidCreationOptions {
195 reason: "recovery exponent range overflows".to_string(),
196 }
197 })?;
198
199 while blocks >= largest.saturating_mul(2) && filenumber > 0 {
200 filenumber -= 1;
201 exponent -= largest;
202 allocations[filenumber as usize] = (exponent, largest);
203 blocks -= largest;
204 }
205 if filenumber == 0 || blocks == 0 {
206 return Err(Par2Error::InvalidCreationOptions {
207 reason: "limited volume allocation cannot place all recovery blocks".to_string(),
208 });
209 }
210
211 exponent = first_exponent;
212 let mut count = 1u32;
213 for allocation in allocations.iter_mut().take(filenumber as usize) {
214 let number = count.min(blocks);
215 if number == 0 {
216 return Err(Par2Error::InvalidCreationOptions {
217 reason: "limited volume allocation produced an empty volume".to_string(),
218 });
219 }
220 *allocation = (exponent, number);
221 exponent += number;
222 blocks -= number;
223 count = count.saturating_mul(2);
224 }
225 if blocks != 0 {
226 return Err(Par2Error::InvalidCreationOptions {
227 reason: "limited volume allocation left recovery blocks unassigned".to_string(),
228 });
229 }
230 Ok(allocations)
231}
232
233fn bit_length(value: u32) -> u32 {
234 u32::BITS - value.leading_zeros()
235}
236
237fn decimal_width(value: u32) -> usize {
238 value.to_string().len().max(1)
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::create::options::VolumeScheme;
245
246 #[test]
247 fn automatic_count_is_bit_length() {
248 let volumes = allocate_volumes(0, 5, None, VolumeScheme::Variable, "set", 20, 4).unwrap();
249 assert_eq!(volumes.len(), 3);
250 assert_eq!(
251 volumes
252 .iter()
253 .map(|volume| volume.recovery_count)
254 .sum::<u32>(),
255 5
256 );
257 }
258
259 #[test]
260 fn uniform_allocation_and_widths_are_stable() {
261 let volumes =
262 allocate_volumes(7, 10, Some(3), VolumeScheme::Uniform, "set", 20, 4).unwrap();
263 assert_eq!(
264 volumes
265 .iter()
266 .map(|volume| volume.recovery_count)
267 .collect::<Vec<_>>(),
268 vec![4, 3, 3]
269 );
270 assert_eq!(volumes[0].filename, "set.vol07+4.par2");
271 assert_eq!(volumes[2].first_exponent, 14);
272 }
273
274 #[test]
275 fn widths_include_the_zero_count_boundary_entry() {
276 let volumes =
277 allocate_volumes(0, 100, Some(3), VolumeScheme::Uniform, "set", 400, 4).unwrap();
278 assert_eq!(volumes[0].filename, "set.vol000+34.par2");
279 assert_eq!(volumes[2].filename, "set.vol067+33.par2");
280 }
281
282 #[test]
283 fn explicit_count_forces_uniform_allocation() {
284 let volumes =
285 allocate_volumes(0, 100, Some(10), VolumeScheme::Variable, "set", 400, 4).unwrap();
286 assert_eq!(
287 volumes
288 .iter()
289 .map(|volume| volume.recovery_count)
290 .collect::<Vec<_>>(),
291 vec![10; 10]
292 );
293 assert_eq!(volumes[0].filename, "set.vol000+10.par2");
294 assert_eq!(volumes[9].filename, "set.vol090+10.par2");
295 }
296
297 #[test]
298 fn explicit_count_above_public_limit_is_rejected() {
299 let error =
300 allocate_volumes(0, 100, Some(32), VolumeScheme::Uniform, "set", 400, 4).unwrap_err();
301 assert!(matches!(error, Par2Error::InvalidCreationOptions { .. }));
302 }
303
304 #[test]
305 fn limited_allocation_matches_largest_source_cap() {
306 for (recovery_count, expected) in [
307 (1, vec![1]),
308 (10, vec![1, 2, 4, 3]),
309 (20, vec![1, 2, 4, 3, 10]),
310 (35, vec![1, 2, 4, 8, 10, 10]),
311 ] {
312 let volumes =
313 allocate_volumes(0, recovery_count, None, VolumeScheme::Limited, "set", 40, 4)
314 .unwrap();
315 assert_eq!(
316 volumes
317 .iter()
318 .map(|volume| volume.recovery_count)
319 .collect::<Vec<_>>(),
320 expected
321 );
322 assert!(volumes.iter().all(|volume| volume.recovery_count <= 10));
323 }
324
325 let volumes = allocate_volumes(7, 20, None, VolumeScheme::Limited, "set", 40, 4).unwrap();
326 assert_eq!(volumes[0].filename, "set.vol07+01.par2");
327 assert_eq!(volumes.last().unwrap().filename, "set.vol17+10.par2");
328 }
329
330 #[test]
331 fn limited_allocation_rejects_explicit_count() {
332 let error =
333 allocate_volumes(0, 8, Some(2), VolumeScheme::Limited, "set", 40, 4).unwrap_err();
334 assert!(matches!(error, Par2Error::InvalidCreationOptions { .. }));
335 }
336}