1use super::{
4 ChunkDescriptor, ChunkSpool, EWF2_DONE_SECTION, EWF2_NEXT_SECTION, Ewf1SegmentSections,
5 Ewf1SegmentWriteContext, Ewf2SegmentWriteContext, WriteFormat, WriteHashState, WriteOptions,
6 WriteResult, automatic_encode_batch_chunks, effective_write_hashes, encode_chunks,
7 estimated_ewf1_segment_size, ewf2_segment_path, is_ewf2_format, normalize_maximum_segment_size,
8 normalize_media_size, publication_segment_paths, segment_path, validate_options,
9 validate_secondary_segment_filename, validate_session_ranges, write_ewf1_segment,
10 write_ewf2_segment, writer_chunk_geometry,
11};
12use crate::publication::Publication;
13use crate::{EwfError, Result};
14use std::collections::BTreeSet;
15use std::fs::{self, File};
16use std::io::Write;
17use std::path::{Path, PathBuf};
18use tempfile::NamedTempFile;
19
20#[derive(Debug, Clone)]
22pub struct SequentialOptions {
23 pub source_size: u64,
25 pub chunks_per_segment: u32,
28 pub write: WriteOptions,
31}
32
33impl SequentialOptions {
34 pub fn new(source_size: u64) -> Self {
36 Self {
37 source_size,
38 chunks_per_segment: 16_375,
39 write: WriteOptions {
40 format: WriteFormat::Ewf2Physical,
41 ..WriteOptions::default()
42 },
43 }
44 }
45}
46
47pub struct SequentialWriter {
59 pub(super) options: WriteOptions,
60 path: PathBuf,
61 source_size: u64,
62 position: u64,
63 logical_size: u64,
64 chunk_size: u64,
65 capacity: usize,
66 per_segment: usize,
67 total_chunks: u32,
68 encoded_chunks: u64,
69 pending: Vec<u8>,
70 current: Group,
71 hashes: WriteHashState,
72 paths: Vec<PathBuf>,
73 mirrors: Vec<PathBuf>,
74 poisoned: bool,
75 publication: Publication,
77}
78
79struct Group {
80 spool: ChunkSpool,
81 chunks: Vec<ChunkDescriptor>,
82}
83
84impl Group {
85 fn new(directory: &Path) -> Result<Self> {
86 Ok(Self {
87 spool: ChunkSpool {
88 file: NamedTempFile::new_in(directory)?,
89 len: 0,
90 position: 0,
91 },
92 chunks: Vec::new(),
93 })
94 }
95}
96
97impl SequentialWriter {
98 pub fn create(path: impl AsRef<Path>, settings: SequentialOptions) -> Result<Self> {
100 let path = path.as_ref().to_path_buf();
101 let options = settings.write;
102 validate_options(&options)?;
103 let is_v2 = is_ewf2_format(options.format);
104 if !(is_v2 || options.format == WriteFormat::Ewf1Physical)
105 || (is_v2 && normalize_maximum_segment_size(options.maximum_segment_size).is_some())
106 {
107 return Err(EwfError::Unsupported(
108 "sequential writing supports EWF1 physical or EWF2 with chunk-count segmentation"
109 .into(),
110 ));
111 }
112 let (chunk_size, capacity) =
113 writer_chunk_geometry(options.sectors_per_chunk, options.bytes_per_sector)?;
114 if chunk_size > 16 * 1024 * 1024
115 || settings.chunks_per_segment == 0
116 || chunk_size
117 .checked_mul(u64::from(settings.chunks_per_segment))
118 .is_none_or(|size| size > 512 * 1024 * 1024)
119 {
120 return Err(EwfError::Unsupported(
121 "sequential writing requires chunks <=16 MiB and segment capacity <=512 MiB".into(),
122 ));
123 }
124 let sector = u64::from(options.bytes_per_sector);
125 let logical_size = settings
126 .source_size
127 .checked_add(sector - 1)
128 .map(|size| size / sector * sector)
129 .ok_or_else(|| EwfError::Malformed("sequential source size overflow".into()))?;
130 if normalize_media_size(options.media_size).is_some_and(|size| size != logical_size) {
131 return Err(EwfError::Unsupported(
132 "media_size differs from padded source size".into(),
133 ));
134 }
135 let total_chunks = u32::try_from(logical_size.div_ceil(chunk_size))
136 .map_err(|_| EwfError::Unsupported("sequential chunk count exceeds u32".into()))?;
137 validate_session_ranges("sessions", &options.sessions, logical_size / sector)?;
138 validate_session_ranges("tracks", &options.tracks, logical_size / sector)?;
139 let per_segment = segment_chunk_limit(&options, chunk_size, settings.chunks_per_segment)?;
140 let path_for_segment = if is_v2 {
141 ewf2_segment_path
142 } else {
143 segment_path
144 };
145 validate_secondary_segment_filename(&path, options.secondary_segment_filename.as_deref())?;
146 if let Some(secondary) = &options.secondary_segment_filename {
147 validate_namespaces(
148 &path,
149 secondary,
150 (total_chunks as usize).div_ceil(per_segment).max(1),
151 path_for_segment,
152 )?;
153 }
154 let publication = Publication::begin(
155 &path,
156 options.secondary_segment_filename.as_deref(),
157 options.overwrite_existing,
158 )?;
159 let staged = publication.stage(0, &path)?;
160 let current = Group::new(staged.parent().expect("staged parent"))?;
161 Ok(Self {
162 options,
163 path,
164 source_size: settings.source_size,
165 position: 0,
166 logical_size,
167 chunk_size,
168 capacity,
169 per_segment,
170 total_chunks,
171 encoded_chunks: 0,
172 pending: Vec::with_capacity(capacity),
173 current,
174 hashes: WriteHashState::new(),
175 paths: Vec::new(),
176 mirrors: Vec::new(),
177 poisoned: false,
178 publication,
179 })
180 }
181
182 pub fn position(&self) -> u64 {
184 self.position
185 }
186
187 pub fn write_all(&mut self, mut bytes: &[u8]) -> Result<()> {
189 self.healthy()?;
190 self.poisoned = true;
191 if bytes.len() as u64 > self.source_size - self.position {
192 return Err(EwfError::Malformed(
193 "sequential input exceeds declared source size".into(),
194 ));
195 }
196 let batch_size = automatic_encode_batch_chunks(self.capacity);
197 while !bytes.is_empty() {
198 let mut chunks = Vec::with_capacity(batch_size);
199 while !bytes.is_empty() && chunks.len() < batch_size {
200 let take = bytes.len().min(self.capacity - self.pending.len());
201 self.pending.extend_from_slice(&bytes[..take]);
202 self.position += take as u64;
203 bytes = &bytes[take..];
204 if self.pending.len() == self.capacity {
205 chunks.push(std::mem::replace(
206 &mut self.pending,
207 Vec::with_capacity(self.capacity),
208 ));
209 }
210 }
211 self.encode_batch(chunks)?;
212 }
213 self.poisoned = false;
214 Ok(())
215 }
216
217 fn healthy(&self) -> Result<()> {
218 if self.poisoned {
219 return Err(EwfError::Aborted);
220 }
221 Ok(())
222 }
223
224 fn encode_batch(&mut self, chunks: Vec<Vec<u8>>) -> Result<()> {
225 for chunk in &chunks {
226 self.hashes.update(chunk);
227 }
228 let encoded = encode_chunks(
229 chunks,
230 self.options.compression,
231 self.options.compression_values,
232 self.chunk_size,
233 is_ewf2_format(self.options.format),
234 !is_ewf2_format(self.options.format) && self.options.compression_values.empty_block,
235 )?;
236 for chunk in encoded {
237 self.current.chunks.push(self.current.spool.append(chunk)?);
238 self.encoded_chunks += 1;
239 if self.current.chunks.len() == self.per_segment
240 && self.encoded_chunks < u64::from(self.total_chunks)
241 {
242 let staged = self.publication.stage(0, &self.path)?;
243 let next = Group::new(staged.parent().expect("staged parent"))?;
244 let mut complete = std::mem::replace(&mut self.current, next);
245 let number = self.paths.len() + 1;
246 self.stage_group(&mut complete, number, false)?;
247 self.paths.push(self.segment_path(&self.path, number)?);
248 }
249 }
250 Ok(())
251 }
252
253 fn stage_group(&self, group: &mut Group, number: usize, last: bool) -> Result<()> {
254 let path = self.segment_path(&self.path, number)?;
255 let staged = self.publication.stage(0, &path)?;
256 let mut file = File::create(&staged)?;
257 let sector_count = self.logical_size / u64::from(self.options.bytes_per_sector);
258 if is_ewf2_format(self.options.format) {
259 write_ewf2_segment(
260 &mut file,
261 &mut group.spool,
262 &group.chunks,
263 &self.options,
264 Ewf2SegmentWriteContext {
265 segment_number: u32::try_from(number)
266 .map_err(|_| EwfError::Unsupported("too many segments".into()))?,
267 first_chunk: (number as u64 - 1) * self.per_segment as u64,
268 total_chunk_count: self.total_chunks,
269 sector_count,
270 terminal_section_type: if last {
271 EWF2_DONE_SECTION
272 } else {
273 EWF2_NEXT_SECTION
274 },
275 },
276 )?;
277 } else {
278 write_ewf1_segment(
279 &mut file,
280 &mut group.spool,
281 &group.chunks,
282 &self.options,
283 Ewf1SegmentWriteContext {
284 segment_number: u16::try_from(number)
285 .map_err(|_| EwfError::Unsupported("too many EWF1 segments".into()))?,
286 chunk_count: self.total_chunks,
287 sector_count,
288 sections: Ewf1SegmentSections::for_segment(number == 1, last),
289 terminal_section: super::TerminalSection::Done,
290 },
291 )?;
292 if let Some(limit) = normalize_maximum_segment_size(self.options.maximum_segment_size)
293 && group.chunks.len() > 1
294 && file.metadata()?.len() > limit
295 {
296 return Err(EwfError::Unsupported(
297 "sequential EWF1 segment exceeds maximum_segment_size".into(),
298 ));
299 }
300 }
301 file.sync_all()?;
302 drop(file);
303 if let Some(base) = &self.options.secondary_segment_filename {
304 let mirror = self.segment_path(base, number)?;
305 let destination = self.publication.stage(1, &mirror)?;
306 fs::copy(staged, &destination)?;
307 fs::OpenOptions::new()
308 .write(true)
309 .open(destination)?
310 .sync_all()?;
311 }
312 Ok(())
313 }
314
315 fn segment_path(&self, base: &Path, number: usize) -> Result<PathBuf> {
316 if is_ewf2_format(self.options.format) {
317 ewf2_segment_path(base, number)
318 } else {
319 segment_path(base, number)
320 }
321 }
322
323 pub fn finish(mut self) -> Result<WriteResult> {
325 self.healthy()?;
326 let is_v2 = is_ewf2_format(self.options.format);
327 if self.position != self.source_size {
328 return Err(EwfError::Malformed(
329 "sequential input is shorter than declared source size".into(),
330 ));
331 }
332 let padding = (self.logical_size - self.source_size) as usize;
333 self.pending.resize(self.pending.len() + padding, 0);
334 if !self.pending.is_empty() {
335 let chunk = std::mem::replace(&mut self.pending, Vec::with_capacity(self.capacity));
336 self.encode_batch(vec![chunk])?;
337 }
338 let (md5, sha1, sha256) = self.hashes.clone().finalize();
339 self.options.hashes = effective_write_hashes(&self.options.hashes, md5, sha1, sha256)?;
340 let staged = self.publication.stage(0, &self.path)?;
341 let empty = Group::new(staged.parent().expect("staged parent"))?;
342 let mut last = std::mem::replace(&mut self.current, empty);
343 let number = self.paths.len() + 1;
344 self.stage_group(&mut last, number, true)?;
345 self.paths.push(self.segment_path(&self.path, number)?);
346 drop(last);
348 self.current.spool.file.close()?;
349 if let Some(base) = &self.options.secondary_segment_filename {
350 for number in 1..=self.paths.len() {
351 self.mirrors.push(if is_v2 {
352 ewf2_segment_path(base, number)?
353 } else {
354 segment_path(base, number)?
355 });
356 }
357 }
358 let mut destinations = vec![publication_segment_paths(&self.paths, is_v2)?];
359 if !self.mirrors.is_empty() {
360 destinations.push(publication_segment_paths(&self.mirrors, is_v2)?);
361 }
362 self.publication.publish(&destinations, self.paths.len())?;
363 Ok(WriteResult {
364 segment_paths: self.paths,
365 secondary_segment_paths: self.mirrors,
366 logical_size: self.logical_size,
367 chunk_size: self.chunk_size,
368 chunk_count: u64::from(self.total_chunks),
369 computed_sha256: sha256,
370 })
371 }
372}
373
374fn segment_chunk_limit(options: &WriteOptions, chunk_size: u64, requested: u32) -> Result<usize> {
375 let Some(maximum_size) = normalize_maximum_segment_size(options.maximum_segment_size) else {
376 return Ok(requested as usize);
377 };
378 if is_ewf2_format(options.format) {
379 return Err(EwfError::Unsupported(
380 "maximum_segment_size is not supported for sequential EWF2".into(),
381 ));
382 }
383 let mut sizing = options.clone();
387 sizing.hashes = effective_write_hashes(&options.hashes, [0; 16], [0; 20], [0; 32])?;
388 let encoded_chunk_size = chunk_size
389 .checked_add(4)
390 .ok_or_else(|| EwfError::Malformed("sequential encoded chunk size overflow".into()))?;
391 let mut low = 1_u32;
392 let mut high = requested;
393 while low < high {
394 let candidate = low + (high - low).div_ceil(2);
395 let payload_size = encoded_chunk_size
396 .checked_mul(u64::from(candidate))
397 .ok_or_else(|| EwfError::Malformed("sequential segment size overflow".into()))?;
398 let estimate = estimated_ewf1_segment_size(candidate as usize, payload_size, 1, &sizing)?;
399 if estimate <= maximum_size {
400 low = candidate;
401 } else {
402 high = candidate - 1;
403 }
404 }
405 Ok(low as usize)
406}
407
408fn validate_namespaces(
412 primary: &Path,
413 secondary: &Path,
414 count: usize,
415 segment_path: fn(&Path, usize) -> Result<PathBuf>,
416) -> Result<()> {
417 fn key(path: &Path) -> Result<PathBuf> {
418 let path = super::normalized_output_path(path)?;
419 let parent = path
420 .parent()
421 .ok_or_else(|| EwfError::Malformed("missing output parent".into()))?;
422 let key = parent.canonicalize()?.join(
423 path.file_name()
424 .ok_or_else(|| EwfError::Malformed("missing output name".into()))?,
425 );
426 #[cfg(windows)]
427 let key = PathBuf::from(key.to_string_lossy().to_lowercase());
428 Ok(key)
429 }
430 let primary: BTreeSet<_> = (1..=count)
431 .map(|n| key(&segment_path(primary, n)?))
432 .collect::<Result<_>>()?;
433 for number in 1..=count {
434 if primary.contains(&key(&segment_path(secondary, number)?)?) {
435 return Err(EwfError::Unsupported(
436 "secondary segment filename overlaps primary output".into(),
437 ));
438 }
439 }
440 Ok(())
441}
442
443impl Write for SequentialWriter {
444 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
445 Self::write_all(self, bytes).map_err(std::io::Error::other)?;
446 Ok(bytes.len())
447 }
448 fn flush(&mut self) -> std::io::Result<()> {
449 self.healthy().map_err(std::io::Error::other)
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::{SequentialOptions, SequentialWriter, WriteFormat};
456 #[test]
457 fn scratch_and_descriptors_do_not_grow_with_completed_segments() {
458 for format in [
459 WriteFormat::Ewf1Physical,
460 WriteFormat::Ewf2Physical,
461 WriteFormat::Ewf2Logical,
462 ] {
463 let dir = tempfile::tempdir().unwrap();
464 let sectors_per_chunk = if format == WriteFormat::Ewf2Logical {
465 16
466 } else {
467 1
468 };
469 let chunk_size = sectors_per_chunk * 512;
470 let mut options = SequentialOptions::new(u64::from(chunk_size) * 100);
471 options.write.format = format;
472 options.write.sectors_per_chunk = sectors_per_chunk;
473 options.chunks_per_segment = 3;
474 let mut writer = SequentialWriter::create(
475 dir.path().join(match format {
476 WriteFormat::Ewf1Physical => "case.E01",
477 WriteFormat::Ewf2Physical => "case.Ex01",
478 WriteFormat::Ewf2Logical => "case.Lx01",
479 _ => unreachable!(),
480 }),
481 options,
482 )
483 .unwrap();
484 let chunk: Vec<u8> = (0..chunk_size).map(|n| (n % 251) as u8).collect();
485 for _ in 0..100 {
486 writer.write_all(&chunk).unwrap();
487 assert!(writer.current.chunks.len() <= 3);
488 assert!(writer.current.spool.len <= u64::from(3 * (chunk_size + 4)));
489 assert!(writer.pending.len() < chunk_size as usize);
490 }
491 writer.finish().unwrap();
492 }
493 }
494}