1use std::io::Write;
4
5use crate::chunked_write::ByteSink;
6use crate::file_writer::FileWriter as FormatWriter;
7use crate::type_builders::{
8 AttrValue, DatasetBuilder as FormatDatasetBuilder, FinishedGroup,
9 GroupBuilder as FormatGroupBuilder,
10};
11
12use crate::error::{Error, FormatError};
13use crate::file_space_info::FileSpaceStrategy;
14use crate::libver::LibVer;
15
16pub struct FileBuilder {
30 writer: FormatWriter,
31}
32
33impl FileBuilder {
34 pub fn new() -> Self {
36 Self {
37 writer: FormatWriter::new(),
38 }
39 }
40
41 pub fn create_dataset(&mut self, name: &str) -> &mut FormatDatasetBuilder {
44 self.writer.create_dataset(name)
45 }
46
47 pub fn create_group(&mut self, name: &str) -> FormatGroupBuilder {
50 self.writer.create_group(name)
51 }
52
53 pub fn add_group(&mut self, group: FinishedGroup) {
55 self.writer.add_group(group);
56 }
57
58 pub fn with_userblock(&mut self, size: u64) -> &mut Self {
64 self.writer.with_userblock(size);
65 self
66 }
67
68 pub fn with_libver_bounds(&mut self, low: LibVer, high: LibVer) -> &mut Self {
79 self.writer.with_libver_bounds(low, high);
80 self
81 }
82
83 pub fn with_file_space_strategy(
93 &mut self,
94 strategy: FileSpaceStrategy,
95 persist: bool,
96 threshold: u64,
97 ) -> &mut Self {
98 self.writer
99 .with_file_space_strategy(strategy, persist, threshold);
100 self
101 }
102
103 pub fn with_file_space_page_size(&mut self, page_size: u64) -> &mut Self {
106 self.writer.with_file_space_page_size(page_size);
107 self
108 }
109
110 pub fn set_attr(&mut self, name: &str, value: AttrValue) {
112 self.writer.set_root_attr(name, value);
113 }
114
115 pub fn finish(self) -> Result<Vec<u8>, Error> {
117 Ok(self.writer.finish()?)
118 }
119
120 pub fn finish_to<W: Write>(self, w: W) -> Result<(), Error> {
130 let mut sink = WriteSink::new(std::io::BufWriter::new(w));
131 if let Err(fe) = self.writer.finish_to_sink(&mut sink) {
132 return match sink.err.take() {
135 Some(io_err) => Err(Error::Io(io_err)),
136 None => Err(Error::Format(fe)),
137 };
138 }
139 sink.into_inner().flush().map_err(Error::Io)
140 }
141
142 pub fn write<P: AsRef<std::path::Path>>(self, path: P) -> Result<(), Error> {
147 let file = std::fs::File::create(path).map_err(Error::Io)?;
148 self.finish_to(file)
149 }
150}
151
152struct WriteSink<W: Write> {
157 inner: W,
158 written: u64,
159 err: Option<std::io::Error>,
160}
161
162impl<W: Write> WriteSink<W> {
163 fn new(inner: W) -> Self {
164 Self {
165 inner,
166 written: 0,
167 err: None,
168 }
169 }
170
171 fn into_inner(self) -> W {
172 self.inner
173 }
174}
175
176impl<W: Write> ByteSink for WriteSink<W> {
177 fn put(&mut self, bytes: &[u8]) -> Result<(), FormatError> {
178 match self.inner.write_all(bytes) {
179 Ok(()) => {
180 self.written += bytes.len() as u64;
181 Ok(())
182 }
183 Err(e) => {
184 self.err = Some(e);
185 Err(FormatError::SerializationError(
188 "streaming output write failed".into(),
189 ))
190 }
191 }
192 }
193
194 fn put_zeros(&mut self, n: usize) -> Result<(), FormatError> {
195 const ZEROS: [u8; 4096] = [0u8; 4096];
198 let mut remaining = n;
199 while remaining > 0 {
200 let take = remaining.min(ZEROS.len());
201 self.put(&ZEROS[..take])?;
202 remaining -= take;
203 }
204 Ok(())
205 }
206
207 fn position(&self) -> u64 {
208 self.written
209 }
210}
211
212impl Default for FileBuilder {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218#[cfg(test)]
219mod streaming_tests {
220 use super::*;
221 use crate::chunked_write::{ChunkMeta, ChunkProvider};
222 use std::sync::{Arc, Mutex};
223
224 type Calls = Arc<Mutex<Vec<usize>>>;
225
226 struct MemProvider {
233 chunks: Vec<Vec<u8>>,
234 calls: Calls,
235 short_slot: Option<usize>,
236 }
237
238 impl ChunkProvider for MemProvider {
239 fn chunk_bytes(&self, index: usize) -> Result<Vec<u8>, FormatError> {
240 self.calls.lock().unwrap().push(index);
241 let mut bytes = self.chunks[index].clone();
242 if self.short_slot == Some(index) {
243 bytes.pop();
244 }
245 Ok(bytes)
246 }
247 }
248
249 fn f64_chunk(vals: &[f64]) -> Vec<u8> {
250 let mut v = Vec::new();
251 for &x in vals {
252 v.extend_from_slice(&x.to_le_bytes());
253 }
254 v
255 }
256
257 fn meta_of(chunk_bytes: &[Vec<u8>]) -> Vec<ChunkMeta> {
258 chunk_bytes
259 .iter()
260 .map(|c| ChunkMeta {
261 compressed_size: c.len() as u64,
262 filter_mask: 0,
263 })
264 .collect()
265 }
266
267 fn stage_lazy(
271 b: &mut FileBuilder,
272 name: &str,
273 chunk_bytes: Vec<Vec<u8>>,
274 dims: &[u64],
275 chunk_dims: &[u64],
276 maxshape: Option<&[u64]>,
277 calls: Calls,
278 short_slot: Option<usize>,
279 ) {
280 let meta = meta_of(&chunk_bytes);
281 let provider = MemProvider {
282 chunks: chunk_bytes,
283 calls,
284 short_slot,
285 };
286 b.create_dataset(name).with_raw_chunks_lazy(
287 crate::type_builders::make_f64_type(),
288 dims,
289 maxshape,
290 chunk_dims,
291 8,
292 None,
293 meta,
294 Box::new(provider),
295 );
296 }
297
298 fn build_lazy(
300 chunk_bytes: Vec<Vec<u8>>,
301 dims: &[u64],
302 chunk_dims: &[u64],
303 maxshape: Option<&[u64]>,
304 calls: Calls,
305 short_slot: Option<usize>,
306 ) -> FileBuilder {
307 let mut b = FileBuilder::new();
308 stage_lazy(
309 &mut b,
310 "d",
311 chunk_bytes,
312 dims,
313 chunk_dims,
314 maxshape,
315 calls,
316 short_slot,
317 );
318 b
319 }
320
321 fn read_back_f64(bytes: &[u8], path: &str) -> Vec<f64> {
322 let file = crate::reader::File::from_bytes(bytes.to_vec()).unwrap();
323 let raw = file.dataset(path).unwrap().read_raw().unwrap();
324 raw.chunks_exact(8)
325 .map(|b| f64::from_le_bytes(b.try_into().unwrap()))
326 .collect()
327 }
328
329 #[test]
330 fn streamed_output_matches_buffered_and_streams_one_chunk_at_a_time() {
331 let chunks = vec![
332 f64_chunk(&[1.0, 2.0]),
333 f64_chunk(&[3.0, 4.0]),
334 f64_chunk(&[5.0, 6.0]),
335 ];
336
337 let calls_buf = Arc::new(Mutex::new(Vec::new()));
338 let buffered = build_lazy(chunks.clone(), &[6], &[2], None, calls_buf.clone(), None)
339 .finish()
340 .unwrap();
341
342 let calls_str = Arc::new(Mutex::new(Vec::new()));
343 let mut streamed = Vec::new();
344 build_lazy(chunks.clone(), &[6], &[2], None, calls_str.clone(), None)
345 .finish_to(&mut streamed)
346 .unwrap();
347
348 assert_eq!(
351 buffered, streamed,
352 "streamed output must be byte-identical to buffered output"
353 );
354 assert_eq!(*calls_buf.lock().unwrap(), vec![0, 1, 2]);
357 assert_eq!(*calls_str.lock().unwrap(), vec![0, 1, 2]);
358 assert_eq!(
360 read_back_f64(&buffered, "d"),
361 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
362 );
363 }
364
365 #[test]
366 fn file_builder_keeps_its_auto_traits() {
367 fn assert_auto_traits<
373 T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe,
374 >() {
375 }
376 assert_auto_traits::<FileBuilder>();
377 }
378
379 #[test]
380 fn streaming_writer_rejects_provider_size_mismatch() {
381 for short_slot in [0usize, 2] {
384 let chunks = vec![
385 f64_chunk(&[1.0, 2.0]),
386 f64_chunk(&[3.0, 4.0]),
387 f64_chunk(&[5.0, 6.0]),
388 ];
389 let calls = Arc::new(Mutex::new(Vec::new()));
390 let err = build_lazy(chunks, &[6], &[2], None, calls, Some(short_slot))
391 .finish()
392 .unwrap_err();
393 match err {
394 Error::Format(FormatError::ChunkedReadError(_)) => {}
395 other => panic!("slot {short_slot}: expected ChunkedReadError, got {other:?}"),
396 }
397 }
398 }
399
400 fn assert_variant_streams_identically(
403 chunks: Vec<Vec<u8>>,
404 dims: &[u64],
405 chunk_dims: &[u64],
406 maxshape: Option<&[u64]>,
407 expected: &[f64],
408 ) {
409 let buffered = build_lazy(
410 chunks.clone(),
411 dims,
412 chunk_dims,
413 maxshape,
414 Arc::new(Mutex::new(Vec::new())),
415 None,
416 )
417 .finish()
418 .unwrap();
419 let mut streamed = Vec::new();
420 build_lazy(
421 chunks,
422 dims,
423 chunk_dims,
424 maxshape,
425 Arc::new(Mutex::new(Vec::new())),
426 None,
427 )
428 .finish_to(&mut streamed)
429 .unwrap();
430 assert_eq!(
431 buffered, streamed,
432 "index variant dims={dims:?} chunk={chunk_dims:?} must stream identically"
433 );
434 assert_eq!(
436 read_back_f64(&buffered, "d"),
437 expected,
438 "index variant dims={dims:?} chunk={chunk_dims:?} must read back correctly"
439 );
440 }
441
442 #[test]
443 fn streamed_equals_buffered_across_index_variants() {
444 assert_variant_streams_identically(
448 vec![f64_chunk(&[1.0, 2.0])],
449 &[2],
450 &[2],
451 None,
452 &[1.0, 2.0],
453 );
454 assert_variant_streams_identically(
455 (0..5)
456 .map(|i| f64_chunk(&[i as f64, i as f64 + 0.5]))
457 .collect(),
458 &[10],
459 &[2],
460 None,
461 &[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5],
462 );
463 assert_variant_streams_identically(
464 vec![f64_chunk(&[1.0, 2.0]), f64_chunk(&[3.0, 4.0])],
465 &[4],
466 &[2],
467 Some(&[u64::MAX]),
468 &[1.0, 2.0, 3.0, 4.0],
469 );
470 }
471
472 struct FailAfter {
475 remaining: usize,
476 }
477 impl Write for FailAfter {
478 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
479 if self.remaining == 0 {
480 return Err(std::io::Error::other("write limit reached"));
481 }
482 let n = buf.len().min(self.remaining);
483 self.remaining -= n;
484 Ok(n)
485 }
486 fn flush(&mut self) -> std::io::Result<()> {
487 Ok(())
488 }
489 }
490
491 #[test]
492 fn streaming_io_error_surfaces_as_error_io() {
493 let chunks: Vec<Vec<u8>> = (0..12).map(|_| f64_chunk(&[1.0; 256])).collect(); let builder = build_lazy(
498 chunks,
499 &[3072],
500 &[256],
501 None,
502 Arc::new(Mutex::new(Vec::new())),
503 None,
504 );
505 let err = builder
506 .finish_to(FailAfter { remaining: 4096 })
507 .unwrap_err();
508 assert!(
509 matches!(err, Error::Io(_)),
510 "a failing sink must surface as Error::Io, got {err:?}"
511 );
512 }
513
514 #[test]
515 fn streamed_dataset_with_attribute_and_contiguous_sibling() {
516 let build = || {
522 let chunks = vec![f64_chunk(&[1.0, 2.0]), f64_chunk(&[3.0, 4.0])];
523 let meta = meta_of(&chunks);
524 let provider = MemProvider {
525 chunks,
526 calls: Arc::new(Mutex::new(Vec::new())),
527 short_slot: None,
528 };
529 let mut b = FileBuilder::new();
530 b.create_dataset("chunked")
533 .with_raw_chunks_lazy(
534 crate::type_builders::make_f64_type(),
535 &[4],
536 None,
537 &[2],
538 8,
539 None,
540 meta,
541 Box::new(provider),
542 )
543 .set_attr("units", AttrValue::I64(7));
544 b.create_dataset("contig")
545 .with_f64_data(&[10.0, 11.0, 12.0]);
546 b.create_dataset("empty").with_f64_data(&[]);
547 b
548 };
549 let buffered = build().finish().unwrap();
550 let mut streamed = Vec::new();
551 build().finish_to(&mut streamed).unwrap();
552 assert_eq!(buffered, streamed, "mixed file must stream identically");
553 assert_eq!(
554 read_back_f64(&buffered, "chunked"),
555 vec![1.0, 2.0, 3.0, 4.0]
556 );
557 assert_eq!(read_back_f64(&buffered, "contig"), vec![10.0, 11.0, 12.0]);
558 }
559}