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