1use std::borrow::Cow;
2use std::io::{Seek, SeekFrom, Write};
3
4use crate::error::{Error, Result};
5use crate::format::btree_v2::BTreeV2Header;
6use crate::format::messages::data_layout::{ChunkIndexType, LayoutClass};
7use crate::format::messages::filter_pipeline::{FILTER_DEFLATE, FILTER_FLETCHER32, FILTER_SHUFFLE};
8
9use super::MutableFile;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12enum WritableChunkIndexKind {
13 BTreeV1,
14 FixedArray,
15 ExtensibleArray,
16 BTreeV2,
17}
18
19impl MutableFile {
20 pub fn write_chunk(
25 &mut self,
26 dataset_path: &str,
27 chunk_coords: &[u64],
28 data: &[u8],
29 ) -> Result<()> {
30 let ds = self.dataset(dataset_path)?;
31 let info = ds.info()?;
32 let index_kind = Self::writable_chunk_index_kind(&info);
33 if self.superblock.base_addr != 0 && index_kind != WritableChunkIndexKind::BTreeV1 {
34 return Err(Error::Unsupported(
35 "write_chunk supports userblock files only for v1 B-tree chunk indexes".into(),
36 ));
37 }
38
39 if info.layout.layout_class != LayoutClass::Chunked {
40 return Err(Error::InvalidFormat(
41 "write_chunk only supports chunked datasets".into(),
42 ));
43 }
44 if info.layout.version > 3
45 && !matches!(
46 info.layout.chunk_index_type,
47 Some(ChunkIndexType::BTreeV1)
48 | Some(ChunkIndexType::FixedArray)
49 | Some(ChunkIndexType::ExtensibleArray)
50 | Some(ChunkIndexType::BTreeV2)
51 )
52 {
53 return Err(Error::Unsupported(
54 "write_chunk currently supports only v1 B-tree, fixed-array, simple extensible-array, and simple v2 B-tree chunk indexes".into(),
55 ));
56 }
57
58 let chunk_data_dims = Self::chunk_data_dims(&info)?;
59 Self::validate_chunk_coords(chunk_coords, &info.dataspace.dims, chunk_data_dims)?;
60
61 let element_size = Self::u64_to_usize(u64::from(info.datatype.size), "datatype size")?;
62 let expected_len = Self::expected_chunk_len(chunk_data_dims, element_size)?;
63 Self::validate_chunk_write_len(data.len(), expected_len)?;
64 let filtered = Self::encode_chunk_write_data(&info, data, element_size)?;
65
66 let index_addr = info
67 .layout
68 .chunk_index_addr
69 .ok_or_else(|| Error::InvalidFormat("chunked dataset missing B-tree address".into()))?;
70
71 self.validate_chunk_index_rewrite(
72 index_kind,
73 index_addr,
74 &info,
75 chunk_coords,
76 chunk_data_dims,
77 expected_len,
78 )?;
79
80 let chunk_physical_addr = self.write_handle.seek(SeekFrom::End(0))?;
81 let chunk_addr =
82 self.logical_addr_from_physical(chunk_physical_addr, "appended chunk address")?;
83 self.write_handle.write_all(&filtered)?;
84 self.rewrite_chunk_index(
85 index_kind,
86 index_addr,
87 ds.addr(),
88 &info,
89 chunk_coords,
90 chunk_data_dims,
91 Self::usize_to_u64(filtered.len(), "filtered chunk size")?,
92 chunk_addr,
93 expected_len,
94 element_size,
95 )?;
96 let physical_eof = self.write_handle.seek(SeekFrom::End(0))?;
97 let logical_eof = physical_eof
98 .checked_sub(self.superblock.base_addr)
99 .ok_or_else(|| Error::InvalidFormat("file EOF is before HDF5 base address".into()))?;
100 self.rewrite_superblock_eof(logical_eof)?;
101 self.write_handle.flush()?;
102 self.reopen_reader()?;
103
104 Ok(())
105 }
106
107 pub(super) fn chunk_data_dims(info: &crate::hl::dataset::DatasetInfo) -> Result<&[u64]> {
108 let chunk_dims = info
109 .layout
110 .chunk_dims
111 .as_ref()
112 .ok_or_else(|| Error::InvalidFormat("chunked layout missing chunk dims".into()))?;
113 if chunk_dims.len() == info.dataspace.dims.len() + 1 {
114 Ok(&chunk_dims[..info.dataspace.dims.len()])
115 } else if chunk_dims.len() == info.dataspace.dims.len() {
116 Ok(chunk_dims)
117 } else {
118 Err(Error::InvalidFormat(format!(
119 "chunk dimension rank {} does not match dataset rank {}",
120 chunk_dims.len(),
121 info.dataspace.dims.len()
122 )))
123 }
124 }
125
126 fn validate_chunk_coords(
127 chunk_coords: &[u64],
128 data_dims: &[u64],
129 chunk_data_dims: &[u64],
130 ) -> Result<()> {
131 if chunk_coords.len() != chunk_data_dims.len() {
132 return Err(Error::InvalidFormat(format!(
133 "chunk coordinate rank {} does not match dataset rank {}",
134 chunk_coords.len(),
135 chunk_data_dims.len()
136 )));
137 }
138 if data_dims.len() != chunk_data_dims.len() {
139 return Err(Error::InvalidFormat(format!(
140 "chunk dimension rank {} does not match dataset rank {}",
141 chunk_data_dims.len(),
142 data_dims.len()
143 )));
144 }
145 for (((idx, &coord), &dim), &chunk) in chunk_coords
146 .iter()
147 .enumerate()
148 .zip(data_dims)
149 .zip(chunk_data_dims)
150 {
151 if chunk == 0 || coord % chunk != 0 {
152 return Err(Error::InvalidFormat(format!(
153 "chunk coordinate {idx}={coord} is not aligned to chunk size {chunk}"
154 )));
155 }
156 if coord >= dim {
157 return Err(Error::Unsupported(format!(
158 "write_chunk coordinate {idx}={coord} is outside dataset extent {dim}"
159 )));
160 }
161 }
162 Ok(())
163 }
164
165 fn expected_chunk_len(chunk_data_dims: &[u64], element_size: usize) -> Result<usize> {
166 let chunk_elements = chunk_data_dims.iter().try_fold(1usize, |acc, &dim| {
167 let dim = Self::u64_to_usize(dim, "chunk dimension")?;
168 acc.checked_mul(dim)
169 .ok_or_else(|| Error::InvalidFormat("chunk element count overflow".into()))
170 })?;
171 chunk_elements
172 .checked_mul(element_size)
173 .ok_or_else(|| Error::InvalidFormat("chunk byte size overflow".into()))
174 }
175
176 fn validate_chunk_write_len(actual_len: usize, expected_len: usize) -> Result<()> {
177 if actual_len != expected_len {
178 return Err(Error::InvalidFormat(format!(
179 "chunk data has {actual_len} bytes, expected {expected_len}",
180 )));
181 }
182 Ok(())
183 }
184
185 fn encode_chunk_write_data<'a>(
186 info: &crate::hl::dataset::DatasetInfo,
187 data: &'a [u8],
188 element_size: usize,
189 ) -> Result<Cow<'a, [u8]>> {
190 let mut filtered = Cow::Borrowed(data);
191 let mut filter_buf = Vec::new();
192 if let Some(ref pipeline) = info.filter_pipeline {
193 for filter in &pipeline.filters {
194 match filter.id {
195 FILTER_SHUFFLE => {
196 filter_buf.clear();
197 filter_buf.resize(filtered.len(), 0);
198 crate::filters::shuffle::shuffle_into(
199 filtered.as_ref(),
200 element_size,
201 &mut filter_buf,
202 )?;
203 Self::replace_encoded_filter_buffer(&mut filtered, &mut filter_buf);
204 }
205 FILTER_DEFLATE => {
206 let level = filter.client_data.first().copied().unwrap_or(6);
207 filter_buf.clear();
208 crate::filters::deflate::compress_into(
209 filtered.as_ref(),
210 level,
211 &mut filter_buf,
212 )?;
213 Self::replace_encoded_filter_buffer(&mut filtered, &mut filter_buf);
214 }
215 FILTER_FLETCHER32 => {
216 crate::filters::fletcher32::append_checksum_in_place(filtered.to_mut())?;
217 }
218 other => {
219 return Err(Error::Unsupported(format!(
220 "write_chunk cannot encode filter {other}"
221 )));
222 }
223 }
224 }
225 }
226 Ok(filtered)
227 }
228
229 fn replace_encoded_filter_buffer<'a>(filtered: &mut Cow<'a, [u8]>, filter_buf: &mut Vec<u8>) {
230 let previous = std::mem::replace(filtered, Cow::Owned(std::mem::take(filter_buf)));
231 if let Cow::Owned(mut previous) = previous {
232 previous.clear();
233 *filter_buf = previous;
234 }
235 }
236
237 fn validate_chunk_index_rewrite(
238 &mut self,
239 index_kind: WritableChunkIndexKind,
240 index_addr: u64,
241 info: &crate::hl::dataset::DatasetInfo,
242 chunk_coords: &[u64],
243 chunk_data_dims: &[u64],
244 unfiltered_chunk_bytes: usize,
245 ) -> Result<()> {
246 match index_kind {
247 WritableChunkIndexKind::FixedArray | WritableChunkIndexKind::ExtensibleArray => {
248 Self::linear_chunk_index(chunk_coords, &info.dataspace.dims, chunk_data_dims)?;
249 Ok(())
250 }
251 WritableChunkIndexKind::BTreeV1 => self.validate_btree_v1_chunk_index(index_addr),
252 WritableChunkIndexKind::BTreeV2 => self.validate_btree_v2_chunk_index(
253 index_addr,
254 info,
255 chunk_coords.len(),
256 unfiltered_chunk_bytes,
257 ),
258 }
259 }
260
261 fn validate_btree_v1_chunk_index(&mut self, btree_addr: u64) -> Result<()> {
262 let mut guard = self.inner.lock();
263 guard.reader.seek(btree_addr)?;
264 let mut magic = [0u8; 4];
265 guard.reader.read_bytes_into(&mut magic)?;
266 if magic != [b'T', b'R', b'E', b'E'] {
267 return Err(Error::InvalidFormat("invalid chunk B-tree magic".into()));
268 }
269 let node_type = guard.reader.read_u8()?;
270 if node_type != 1 {
271 return Err(Error::InvalidFormat(format!(
272 "expected raw-data chunk B-tree, got type {node_type}"
273 )));
274 }
275 Ok(())
276 }
277
278 fn validate_btree_v2_chunk_index(
279 &mut self,
280 index_addr: u64,
281 info: &crate::hl::dataset::DatasetInfo,
282 ndims: usize,
283 unfiltered_chunk_bytes: usize,
284 ) -> Result<()> {
285 let filtered = info
286 .filter_pipeline
287 .as_ref()
288 .map(|pipeline| !pipeline.filters.is_empty())
289 .unwrap_or(false);
290 let chunk_size_len = if filtered {
291 Self::filtered_chunk_size_len(
292 info.layout.version,
293 unfiltered_chunk_bytes,
294 self.superblock.sizeof_size,
295 )
296 } else {
297 0
298 };
299 let expected_record_size = Self::btree_v2_preflight_record_size(
300 usize::from(self.superblock.sizeof_addr),
301 filtered,
302 chunk_size_len,
303 ndims,
304 )?;
305
306 let mut guard = self.inner.lock();
307 let header = BTreeV2Header::read_at(&mut guard.reader, index_addr)?;
308 if usize::from(header.record_size) != expected_record_size {
309 return Err(Error::InvalidFormat(format!(
310 "v2 B-tree chunk record size {} does not match expected {expected_record_size}",
311 header.record_size
312 )));
313 }
314 if header.tree_type != 10 && header.tree_type != 11 {
315 return Err(Error::Unsupported(format!(
316 "write_chunk cannot update v2 B-tree type {} chunk indexes",
317 header.tree_type
318 )));
319 }
320 Ok(())
321 }
322
323 fn btree_v2_preflight_record_size(
324 sizeof_addr: usize,
325 filtered: bool,
326 chunk_size_len: usize,
327 ndims: usize,
328 ) -> Result<usize> {
329 let filter_bytes = if filtered {
330 chunk_size_len
331 .checked_add(4)
332 .ok_or_else(|| Error::InvalidFormat("v2 B-tree record size overflow".into()))?
333 } else {
334 0
335 };
336 let coord_bytes = ndims
337 .checked_mul(8)
338 .ok_or_else(|| Error::InvalidFormat("v2 B-tree record size overflow".into()))?;
339 sizeof_addr
340 .checked_add(filter_bytes)
341 .and_then(|value| value.checked_add(coord_bytes))
342 .ok_or_else(|| Error::InvalidFormat("v2 B-tree record size overflow".into()))
343 }
344
345 #[allow(clippy::too_many_arguments)]
346 fn rewrite_chunk_index(
347 &mut self,
348 index_kind: WritableChunkIndexKind,
349 index_addr: u64,
350 dataset_addr: u64,
351 info: &crate::hl::dataset::DatasetInfo,
352 chunk_coords: &[u64],
353 chunk_data_dims: &[u64],
354 filtered_len: u64,
355 chunk_addr: u64,
356 expected_len: usize,
357 element_size: usize,
358 ) -> Result<()> {
359 match index_kind {
360 WritableChunkIndexKind::FixedArray => self.rewrite_fixed_array_chunk(
361 index_addr,
362 dataset_addr,
363 info,
364 chunk_coords,
365 chunk_data_dims,
366 filtered_len,
367 chunk_addr,
368 expected_len,
369 ),
370 WritableChunkIndexKind::ExtensibleArray => self.rewrite_extensible_array_chunk(
371 index_addr,
372 info,
373 chunk_coords,
374 chunk_data_dims,
375 filtered_len,
376 chunk_addr,
377 expected_len,
378 ),
379 WritableChunkIndexKind::BTreeV2 => self.rewrite_btree_v2_chunk(
380 index_addr,
381 info,
382 chunk_coords,
383 chunk_data_dims,
384 filtered_len,
385 chunk_addr,
386 expected_len,
387 ),
388 WritableChunkIndexKind::BTreeV1 => self.rewrite_leaf_chunk_btree(
389 index_addr,
390 chunk_coords,
391 chunk_data_dims,
392 Self::u64_to_u32(filtered_len, "filtered chunk size")?,
393 chunk_addr,
394 Self::usize_to_u32(element_size, "datatype size")?,
395 ),
396 }
397 }
398
399 fn writable_chunk_index_kind(info: &crate::hl::dataset::DatasetInfo) -> WritableChunkIndexKind {
400 match info.layout.chunk_index_type {
401 Some(ChunkIndexType::FixedArray) => WritableChunkIndexKind::FixedArray,
402 Some(ChunkIndexType::ExtensibleArray) => WritableChunkIndexKind::ExtensibleArray,
403 Some(ChunkIndexType::BTreeV2) => WritableChunkIndexKind::BTreeV2,
404 _ => WritableChunkIndexKind::BTreeV1,
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn expected_chunk_len_rejects_element_count_overflow() {
415 let err = MutableFile::expected_chunk_len(&[u64::MAX, 2], 1).unwrap_err();
416 assert!(
417 err.to_string().contains("chunk element count"),
418 "unexpected error: {err}"
419 );
420 }
421
422 #[test]
423 fn validate_chunk_coords_rejects_out_of_extent_chunk() {
424 let err = MutableFile::validate_chunk_coords(&[10], &[10], &[5]).unwrap_err();
425 assert!(
426 err.to_string().contains("outside dataset extent"),
427 "unexpected error: {err}"
428 );
429 }
430}