Skip to main content

asdf/
block_ffi.rs

1//! `asdf/block.h`: the low-level binary block API.
2//!
3//! # Ownership
4//!
5//! The header's ownership rules are intricate and are part of the contract,
6//! so they are reproduced rather than simplified:
7//!
8//! - `asdf_block_create` returns a **detached** handle owning its own state.
9//!   It is released with `asdf_block_destroy`.
10//! - `asdf_block_append` transfers that state to the file. The handle becomes
11//!   a *view* and is then released with `asdf_block_close`, not `destroy`.
12//! - `asdf_block_open` returns a view of a block the file already owns, also
13//!   released with `asdf_block_close`.
14//! - `asdf_block_data_set` **borrows** the caller's buffer, which must stay
15//!   valid until the file is written. `asdf_block_data_alloc` allocates a
16//!   buffer the block owns instead.
17
18use core::ffi::{CStr, c_char, c_int, c_void};
19
20use asdf_core::block::header::CHECKSUM_SIZE;
21use asdf_core::compression::Compression;
22use asdf_core::{ChecksumStatus, PendingBlock};
23
24use crate::ffi::write_out;
25use crate::file_ffi::{AsdfFile, file_blocks_mut, file_reader};
26use crate::panic::guard;
27
28/// Where a block's bytes live.
29enum BlockData {
30    /// Nothing assigned yet.
31    Empty,
32    /// A buffer the caller owns, borrowed until the file is written.
33    Borrowed { ptr: *const u8, len: usize },
34    /// A buffer this block owns.
35    Owned(Vec<u8>),
36}
37
38impl BlockData {
39    fn as_slice(&self) -> &[u8] {
40        match self {
41            BlockData::Empty => &[],
42            // SAFETY: the C contract requires the caller to keep this buffer
43            // valid until the file is written.
44            BlockData::Borrowed { ptr, len } => unsafe { core::slice::from_raw_parts(*ptr, *len) },
45            BlockData::Owned(v) => v,
46        }
47    }
48
49    fn len(&self) -> usize {
50        match self {
51            BlockData::Empty => 0,
52            BlockData::Borrowed { len, .. } => *len,
53            BlockData::Owned(v) => v.len(),
54        }
55    }
56}
57
58/// A block handle. Opaque to C.
59pub struct AsdfBlock {
60    /// The file this block belongs to, if any.
61    file: *mut AsdfFile,
62    /// The index in the file, for a view onto an existing block.
63    index: Option<usize>,
64    /// Still detached from any file, so `destroy` rather than `close`.
65    detached: bool,
66    data: BlockData,
67    /// Already-compressed bytes to write verbatim, from
68    /// `asdf_block_data_set_compressed`.
69    data_is_compressed: bool,
70    /// The uncompressed size to record when `data_is_compressed`.
71    declared_data_size: u64,
72    compression: Compression,
73    /// The compression name, NUL-terminated.
74    ///
75    /// Five bytes, not four: the *header* field is four, but a four-character
76    /// name like `bzp2` fills it exactly, leaving no room for a terminator.
77    /// `asdf_block_compression` hands this out as a C string, so the extra
78    /// byte is what keeps that read in bounds.
79    compression_name: [u8; 5],
80    allocated_size: u64,
81    /// Decompressed bytes, cached on first access as libasdf does.
82    decompressed: Option<Vec<u8>>,
83}
84
85impl core::fmt::Debug for AsdfBlock {
86    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
87        f.debug_struct("AsdfBlock")
88            .field("index", &self.index)
89            .field("detached", &self.detached)
90            .field("len", &self.data.len())
91            .field("compression", &self.compression)
92            .finish()
93    }
94}
95
96impl AsdfBlock {
97    fn detached(data: BlockData) -> Self {
98        Self {
99            file: core::ptr::null_mut(),
100            index: None,
101            detached: true,
102            data,
103            data_is_compressed: false,
104            declared_data_size: 0,
105            compression: Compression::None,
106            compression_name: [0; 5],
107            allocated_size: 0,
108            decompressed: None,
109        }
110    }
111
112    /// The block as a value the writer can take.
113    fn to_pending(&self) -> PendingBlock {
114        PendingBlock {
115            data: self.data.as_slice().to_vec(),
116            compression: self.compression,
117            allocated_size: self.allocated_size,
118            // Bytes handed to `asdf_block_data_set_compressed` go out
119            // verbatim, carrying the uncompressed size the caller declared.
120            already_compressed: self.data_is_compressed,
121            uncompressed_size: self.declared_data_size,
122        }
123    }
124}
125
126fn block_ref<'a>(block: *mut AsdfBlock) -> Option<&'a mut AsdfBlock> {
127    unsafe { crate::ffi::as_mut(block) }
128}
129
130/// Open a view of a block the file owns.
131///
132/// # Safety
133/// `file` must be null or a valid file handle. The result must be released
134/// with [`asdf_block_close`].
135#[unsafe(no_mangle)]
136pub unsafe extern "C" fn asdf_block_open(file: *mut AsdfFile, index: usize) -> *mut AsdfBlock {
137    guard("asdf_block_open", core::ptr::null_mut(), || {
138        let Some(reader) = file_reader(file) else {
139            return core::ptr::null_mut();
140        };
141        let Ok(location) = reader.block(index) else {
142            return core::ptr::null_mut();
143        };
144        let header = location.header.clone();
145        let compression =
146            Compression::from_name(header.compression_name()).unwrap_or(Compression::None);
147
148        let mut block = AsdfBlock {
149            file,
150            index: Some(index),
151            detached: false,
152            data: BlockData::Empty,
153            data_is_compressed: compression != Compression::None,
154            declared_data_size: header.data_size,
155            compression,
156            compression_name: {
157                let mut name = [0u8; 5];
158                name[..4].copy_from_slice(&header.compression);
159                name
160            },
161            allocated_size: header.allocated_size,
162            decompressed: None,
163        };
164        // Point at the stored bytes; the file owns them for as long as it is
165        // open, which is exactly the lifetime the C contract gives this view.
166        if let Ok(raw) = reader.block_raw(index) {
167            block.data = BlockData::Borrowed { ptr: raw.as_ptr(), len: raw.len() };
168        }
169        Box::into_raw(Box::new(block))
170    })
171}
172
173/// Release a block view.
174///
175/// # Safety
176/// `block` must be null, or a view from [`asdf_block_open`] or an appended
177/// handle, and must not be used afterwards.
178#[unsafe(no_mangle)]
179pub unsafe extern "C" fn asdf_block_close(block: *mut AsdfBlock) {
180    guard("asdf_block_close", (), || {
181        if !block.is_null() {
182            drop(unsafe { Box::from_raw(block) });
183        }
184    })
185}
186
187/// Create a detached block.
188///
189/// With `data` non-null the buffer is *borrowed* and must stay valid until
190/// the file is written. With `data` null and `size` non-zero, a buffer of
191/// that size is allocated for the caller to fill through
192/// [`asdf_block_data_alloc`].
193///
194/// # Safety
195/// `data` must point to at least `size` readable bytes, or be null. The
196/// result must be released with [`asdf_block_destroy`], or handed to
197/// [`asdf_block_append`].
198#[unsafe(no_mangle)]
199pub unsafe extern "C" fn asdf_block_create(data: *const c_void, size: usize) -> *mut AsdfBlock {
200    guard("asdf_block_create", core::ptr::null_mut(), || {
201        let payload = if !data.is_null() {
202            BlockData::Borrowed { ptr: data.cast::<u8>(), len: size }
203        } else if size > 0 {
204            // `block.h` says this returns NULL on failure, so a size the
205            // allocator cannot satisfy has to come back as NULL rather than
206            // abort the caller's process -- which is what `vec![0u8; size]`
207            // does, and which no panic guard can intercept.
208            let mut buf = Vec::new();
209            if buf.try_reserve_exact(size).is_err() {
210                return core::ptr::null_mut();
211            }
212            buf.resize(size, 0);
213            BlockData::Owned(buf)
214        } else {
215            BlockData::Empty
216        };
217        Box::into_raw(Box::new(AsdfBlock::detached(payload)))
218    })
219}
220
221/// Destroy a detached block that was never appended.
222///
223/// # Safety
224/// `block` must be null or a detached handle from [`asdf_block_create`].
225#[unsafe(no_mangle)]
226pub unsafe extern "C" fn asdf_block_destroy(block: *mut AsdfBlock) {
227    guard("asdf_block_destroy", (), || {
228        if !block.is_null() {
229            drop(unsafe { Box::from_raw(block) });
230        }
231    })
232}
233
234/// Allocate a writable buffer of `size` bytes owned by the block.
235///
236/// An existing owned buffer of exactly `size` bytes is returned as-is rather
237/// than reallocated, so `asdf_block_create(NULL, n)` followed by this call
238/// yields the same buffer.
239///
240/// # Safety
241/// `block` must be null or a valid handle. The returned pointer is valid
242/// until the block is destroyed or its data replaced.
243#[unsafe(no_mangle)]
244pub unsafe extern "C" fn asdf_block_data_alloc(block: *mut AsdfBlock, size: usize) -> *mut c_void {
245    guard("asdf_block_data_alloc", core::ptr::null_mut(), || {
246        let Some(block) = block_ref(block) else {
247            return core::ptr::null_mut();
248        };
249        match &mut block.data {
250            BlockData::Owned(existing) if existing.len() == size => {
251                existing.as_mut_ptr().cast::<c_void>()
252            }
253            _ => {
254                block.data = BlockData::Owned(vec![0u8; size]);
255                let BlockData::Owned(buffer) = &mut block.data else {
256                    unreachable!("just assigned")
257                };
258                buffer.as_mut_ptr().cast::<c_void>()
259            }
260        }
261    })
262}
263
264/// Point the block at a caller-owned buffer.
265///
266/// The buffer is borrowed, not copied, and must stay valid until the file is
267/// written.
268///
269/// # Safety
270/// `data` must point to at least `size` readable bytes.
271#[unsafe(no_mangle)]
272pub unsafe extern "C" fn asdf_block_data_set(
273    block: *mut AsdfBlock,
274    data: *const c_void,
275    size: usize,
276) -> c_int {
277    guard("asdf_block_data_set", -1, || {
278        let Some(block) = block_ref(block) else { return -1 };
279        if data.is_null() && size > 0 {
280            return -1;
281        }
282        block.data = if data.is_null() {
283            BlockData::Empty
284        } else {
285            BlockData::Borrowed { ptr: data.cast::<u8>(), len: size }
286        };
287        block.data_is_compressed = false;
288        block.decompressed = None;
289        0
290    })
291}
292
293/// Point the block at bytes that are already compressed, to be written
294/// verbatim.
295///
296/// Unlike [`asdf_block_data_set`], these bytes are not compressed again, so a
297/// compressed block can be copied without decompressing it.
298///
299/// # Safety
300/// `data` must point to at least `size` readable bytes.
301#[unsafe(no_mangle)]
302pub unsafe extern "C" fn asdf_block_data_set_compressed(
303    block: *mut AsdfBlock,
304    data: *const c_void,
305    size: usize,
306    data_size: u64,
307    compression: *const c_char,
308) -> c_int {
309    guard("asdf_block_data_set_compressed", -1, || {
310        let Some(block) = block_ref(block) else { return -1 };
311        if data.is_null() && size > 0 {
312            return -1;
313        }
314        let name = if compression.is_null() {
315            String::new()
316        } else {
317            unsafe { CStr::from_ptr(compression) }.to_string_lossy().into_owned()
318        };
319        if name.len() > 4 {
320            return -1;
321        }
322
323        block.data = if data.is_null() {
324            BlockData::Empty
325        } else {
326            BlockData::Borrowed { ptr: data.cast::<u8>(), len: size }
327        };
328        block.data_is_compressed = true;
329        block.declared_data_size = data_size;
330        block.compression = Compression::from_name(&name).unwrap_or(Compression::None);
331        block.compression_name = [0; 5];
332        block.compression_name[..name.len()].copy_from_slice(name.as_bytes());
333        block.decompressed = None;
334        0
335    })
336}
337
338/// Reserve space for the block in the file.
339///
340/// A value larger than the used size leaves room for the data to grow later
341/// without moving everything after it. Zero means "the same as used".
342///
343/// # Safety
344/// `block` must be null or a valid handle.
345#[unsafe(no_mangle)]
346pub unsafe extern "C" fn asdf_block_allocated_size_set(
347    block: *mut AsdfBlock,
348    allocated_size: u64,
349) -> c_int {
350    guard("asdf_block_allocated_size_set", -1, || {
351        let Some(block) = block_ref(block) else { return -1 };
352        block.allocated_size = allocated_size;
353        0
354    })
355}
356
357/// Append a detached block to a file, transferring ownership.
358///
359/// The handle becomes a view onto the appended block and should afterwards be
360/// released with [`asdf_block_close`] rather than
361/// [`asdf_block_destroy`]. There is no deduplication: appending the same data
362/// twice writes it twice.
363///
364/// # Safety
365/// `file` must be a file handle open for writing, and `block` a detached
366/// handle from [`asdf_block_create`].
367#[unsafe(no_mangle)]
368pub unsafe extern "C" fn asdf_block_append(
369    file: *mut AsdfFile,
370    block: *mut AsdfBlock,
371) -> *mut AsdfBlock {
372    guard("asdf_block_append", core::ptr::null_mut(), || {
373        let Some(handle) = block_ref(block) else {
374            return core::ptr::null_mut();
375        };
376        if !handle.detached {
377            return core::ptr::null_mut();
378        }
379        let Some(blocks) = file_blocks_mut(file) else {
380            return core::ptr::null_mut();
381        };
382        blocks.push(handle.to_pending());
383
384        handle.file = file;
385        handle.index = Some(blocks.len() - 1);
386        handle.detached = false;
387        block
388    })
389}
390
391/// The uncompressed size of the block's data.
392///
393/// # Safety
394/// `block` must be null or a valid handle.
395#[unsafe(no_mangle)]
396pub unsafe extern "C" fn asdf_block_data_size(block: *mut AsdfBlock) -> usize {
397    guard("asdf_block_data_size", 0, || {
398        let Some(block) = block_ref(block) else { return 0 };
399        if block.data_is_compressed {
400            return usize::try_from(block.declared_data_size).unwrap_or(0);
401        }
402        block.data.len()
403    })
404}
405
406/// The block's compression name, or the empty string.
407///
408/// # Safety
409/// `block` must be null or a valid handle. The pointer is owned by the block.
410#[unsafe(no_mangle)]
411pub unsafe extern "C" fn asdf_block_compression(block: *mut AsdfBlock) -> *const c_char {
412    guard("asdf_block_compression", core::ptr::null(), || {
413        let Some(block) = block_ref(block) else {
414            return core::ptr::null();
415        };
416        // Five bytes wide, so even a four-character name is NUL-terminated.
417        block.compression_name.as_ptr().cast::<c_char>()
418    })
419}
420
421/// Set the compression to use when the block is written.
422///
423/// # Safety
424/// `compression` must be a valid NUL-terminated string or null.
425#[unsafe(no_mangle)]
426pub unsafe extern "C" fn asdf_block_compression_set(
427    block: *mut AsdfBlock,
428    compression: *const c_char,
429) -> c_int {
430    guard("asdf_block_compression_set", -1, || {
431        let Some(block) = block_ref(block) else { return -1 };
432        let name = if compression.is_null() {
433            String::new()
434        } else {
435            unsafe { CStr::from_ptr(compression) }.to_string_lossy().into_owned()
436        };
437        // An unknown compressor is refused rather than silently ignored.
438        let Ok(method) = Compression::from_name(&name) else {
439            return -1;
440        };
441        if !method.is_available() {
442            return -1;
443        }
444        block.compression = method;
445        block.compression_name = [0; 5];
446        block.compression_name[..name.len()].copy_from_slice(name.as_bytes());
447        0
448    })
449}
450
451/// The MD5 digest recorded in the block header, or null.
452///
453/// # Safety
454/// `block` must be null or a valid handle. The pointer is owned by the file.
455#[unsafe(no_mangle)]
456pub unsafe extern "C" fn asdf_block_checksum(block: *mut AsdfBlock) -> *const u8 {
457    guard("asdf_block_checksum", core::ptr::null(), || {
458        let Some(handle) = block_ref(block) else {
459            return core::ptr::null();
460        };
461        let (Some(reader), Some(index)) = (file_reader(handle.file), handle.index) else {
462            return core::ptr::null();
463        };
464        match reader.block(index) {
465            Ok(location) => location.header.checksum.as_ptr(),
466            Err(_) => core::ptr::null(),
467        }
468    })
469}
470
471/// Verify the block's MD5 checksum.
472///
473/// A block with no recorded checksum verifies, since the standard makes it
474/// optional and an all-zero digest means "do not check".
475///
476/// # Safety
477/// `block` must be null or a valid handle; `expected` must be writable for
478/// 16 bytes, or null.
479#[unsafe(no_mangle)]
480pub unsafe extern "C" fn asdf_block_checksum_verify(
481    block: *mut AsdfBlock,
482    expected: *mut u8,
483) -> bool {
484    guard("asdf_block_checksum_verify", false, || {
485        let Some(handle) = block_ref(block) else { return false };
486        let (Some(reader), Some(index)) = (file_reader(handle.file), handle.index) else {
487            return false;
488        };
489        let Ok((status, computed)) = reader.verify_block_checksum(index) else {
490            return false;
491        };
492        if !expected.is_null() {
493            unsafe {
494                core::ptr::copy_nonoverlapping(computed.as_ptr(), expected, CHECKSUM_SIZE);
495            }
496        }
497        matches!(status, ChecksumStatus::Valid | ChecksumStatus::Absent)
498    })
499}
500
501/// The block's data, decompressing on first access if needed.
502///
503/// # Safety
504/// `block` must be null or a valid handle; `size` writable or null. The
505/// returned pointer is owned by the block.
506#[unsafe(no_mangle)]
507pub unsafe extern "C" fn asdf_block_data(block: *mut AsdfBlock, size: *mut usize) -> *const c_void {
508    guard("asdf_block_data", core::ptr::null(), || {
509        let Some(handle) = block_ref(block) else {
510            if !size.is_null() {
511                unsafe { write_out(size, 0) };
512            }
513            return core::ptr::null();
514        };
515
516        if handle.compression == Compression::None {
517            let slice = handle.data.as_slice();
518            if !size.is_null() {
519                unsafe { write_out(size, slice.len()) };
520            }
521            return if slice.is_empty() {
522                core::ptr::null()
523            } else {
524                slice.as_ptr().cast::<c_void>()
525            };
526        }
527
528        // Cached, as libasdf does, so repeated access does not re-inflate.
529        if handle.decompressed.is_none() {
530            let expected = usize::try_from(handle.declared_data_size).unwrap_or(0);
531            match handle.compression.decompress(handle.data.as_slice(), expected) {
532                Ok(bytes) => handle.decompressed = Some(bytes),
533                Err(_) => {
534                    if !size.is_null() {
535                        unsafe { write_out(size, 0) };
536                    }
537                    return core::ptr::null();
538                }
539            }
540        }
541        let bytes = handle.decompressed.as_ref().expect("just decompressed");
542        if !size.is_null() {
543            unsafe { write_out(size, bytes.len()) };
544        }
545        bytes.as_ptr().cast::<c_void>()
546    })
547}
548
549/// The block's bytes exactly as stored, without decompressing.
550///
551/// # Safety
552/// See [`asdf_block_data`].
553#[unsafe(no_mangle)]
554pub unsafe extern "C" fn asdf_block_data_raw(
555    block: *mut AsdfBlock,
556    size: *mut usize,
557) -> *const c_void {
558    guard("asdf_block_data_raw", core::ptr::null(), || {
559        let Some(handle) = block_ref(block) else {
560            if !size.is_null() {
561                unsafe { write_out(size, 0) };
562            }
563            return core::ptr::null();
564        };
565        let slice = handle.data.as_slice();
566        if !size.is_null() {
567            unsafe { write_out(size, slice.len()) };
568        }
569        if slice.is_empty() { core::ptr::null() } else { slice.as_ptr().cast::<c_void>() }
570    })
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use crate::file_ffi::{asdf_block_count, asdf_close, asdf_open_mem_ex, asdf_write_to_mem};
577    use alloc::ffi::CString;
578    use asdf_core::{Writer, writer::PendingBlock as CorePending};
579
580    struct Handle(*mut AsdfFile);
581    impl Drop for Handle {
582        fn drop(&mut self) {
583            unsafe { asdf_close(self.0) };
584        }
585    }
586
587    /// A file with two blocks, one of them compressed.
588    fn sample_file() -> Vec<u8> {
589        let doc = asdf_core::yaml::parse_document(
590            "%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\na: 1\n...\n",
591        )
592        .unwrap();
593        let mut writer = Writer::from_document(doc);
594        writer.add_block(CorePending::new((0..=255u8).collect()));
595        writer.add_block(CorePending::compressed(vec![7u8; 2048], Compression::Zlib));
596        writer.to_bytes().unwrap()
597    }
598
599    fn open_sample() -> (Handle, Vec<u8>) {
600        let bytes = sample_file();
601        let f =
602            unsafe { asdf_open_mem_ex(bytes.as_ptr().cast(), bytes.len(), core::ptr::null_mut()) };
603        assert!(!f.is_null());
604        (Handle(f), bytes)
605    }
606
607    #[test]
608    fn opens_a_block_and_reads_its_data() {
609        let (h, _bytes) = open_sample();
610        assert_eq!(unsafe { asdf_block_count(h.0) }, 2);
611
612        let block = unsafe { asdf_block_open(h.0, 0) };
613        assert!(!block.is_null());
614
615        let mut size = 0usize;
616        let data = unsafe { asdf_block_data(block, &mut size) };
617        assert!(!data.is_null());
618        assert_eq!(size, 256);
619        let slice = unsafe { core::slice::from_raw_parts(data.cast::<u8>(), size) };
620        assert_eq!(slice[0], 0);
621        assert_eq!(slice[255], 255);
622
623        unsafe { asdf_block_close(block) };
624    }
625
626    #[test]
627    fn a_compressed_block_inflates_on_demand() {
628        let (h, _bytes) = open_sample();
629        let block = unsafe { asdf_block_open(h.0, 1) };
630
631        let name = unsafe { CStr::from_ptr(asdf_block_compression(block)) };
632        assert_eq!(name.to_str().unwrap(), "zlib");
633
634        // The raw form is the compressed bytes...
635        let mut raw_size = 0usize;
636        let raw = unsafe { asdf_block_data_raw(block, &mut raw_size) };
637        assert!(!raw.is_null());
638        assert!(raw_size < 2048, "raw should be the compressed form");
639
640        // ...and the plain form inflates.
641        let mut size = 0usize;
642        let data = unsafe { asdf_block_data(block, &mut size) };
643        assert_eq!(size, 2048);
644        let slice = unsafe { core::slice::from_raw_parts(data.cast::<u8>(), size) };
645        assert!(slice.iter().all(|b| *b == 7));
646
647        // A second call must return the cached buffer, not inflate again.
648        let again = unsafe { asdf_block_data(block, &mut size) };
649        assert_eq!(again, data);
650
651        unsafe { asdf_block_close(block) };
652    }
653
654    #[test]
655    fn checksums_verify_through_the_c_api() {
656        let (h, _bytes) = open_sample();
657        for index in 0..2 {
658            let block = unsafe { asdf_block_open(h.0, index) };
659            let checksum = unsafe { asdf_block_checksum(block) };
660            assert!(!checksum.is_null());
661
662            let mut computed = [0u8; CHECKSUM_SIZE];
663            assert!(
664                unsafe { asdf_block_checksum_verify(block, computed.as_mut_ptr()) },
665                "block {index}"
666            );
667            assert!(computed.iter().any(|b| *b != 0), "digest was not written out");
668            unsafe { asdf_block_close(block) };
669        }
670    }
671
672    #[test]
673    fn out_of_range_indices_return_null() {
674        let (h, _bytes) = open_sample();
675        assert!(unsafe { asdf_block_open(h.0, 99) }.is_null());
676        assert!(unsafe { asdf_block_open(core::ptr::null_mut(), 0) }.is_null());
677    }
678
679    #[test]
680    fn a_created_block_borrows_the_callers_buffer() {
681        let payload: Vec<u8> = (0..64u8).collect();
682        let block = unsafe { asdf_block_create(payload.as_ptr().cast(), payload.len()) };
683        assert!(!block.is_null());
684        assert_eq!(unsafe { asdf_block_data_size(block) }, 64);
685
686        let mut size = 0usize;
687        let data = unsafe { asdf_block_data_raw(block, &mut size) };
688        assert_eq!(size, 64);
689        // Borrowed, not copied: the pointer is the caller's own buffer.
690        assert_eq!(data.cast::<u8>(), payload.as_ptr());
691
692        unsafe { asdf_block_destroy(block) };
693    }
694
695    #[test]
696    fn creating_with_a_null_buffer_allocates_one() {
697        let block = unsafe { asdf_block_create(core::ptr::null(), 128) };
698        assert_eq!(unsafe { asdf_block_data_size(block) }, 128);
699
700        // The documented shortcut: data_alloc of the same size returns the
701        // buffer that create already made.
702        let first = unsafe { asdf_block_data_alloc(block, 128) };
703        let second = unsafe { asdf_block_data_alloc(block, 128) };
704        assert!(!first.is_null());
705        assert_eq!(first, second, "an existing buffer of the same size is reused");
706
707        // Filling it through the returned pointer must be visible.
708        unsafe { core::ptr::write_bytes(first.cast::<u8>(), 0xAB, 128) };
709        let mut size = 0usize;
710        let data = unsafe { asdf_block_data_raw(block, &mut size) };
711        let slice = unsafe { core::slice::from_raw_parts(data.cast::<u8>(), size) };
712        assert!(slice.iter().all(|b| *b == 0xAB));
713
714        unsafe { asdf_block_destroy(block) };
715    }
716
717    #[test]
718    fn a_different_size_reallocates() {
719        let block = unsafe { asdf_block_create(core::ptr::null(), 16) };
720        let _ = unsafe { asdf_block_data_alloc(block, 32) };
721        assert_eq!(unsafe { asdf_block_data_size(block) }, 32);
722        unsafe { asdf_block_destroy(block) };
723    }
724
725    #[test]
726    fn appending_transfers_the_block_to_the_file() {
727        let f = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
728        let h = Handle(f);
729
730        let payload = b"appended data".to_vec();
731        let block = unsafe { asdf_block_create(payload.as_ptr().cast(), payload.len()) };
732        let appended = unsafe { asdf_block_append(h.0, block) };
733        assert_eq!(appended, block, "append returns the same handle as a view");
734        assert_eq!(unsafe { asdf_block_count(h.0) }, 1);
735
736        // A second append of the same handle must be refused: it is no
737        // longer detached.
738        assert!(unsafe { asdf_block_append(h.0, block) }.is_null());
739        unsafe { asdf_block_close(appended) };
740
741        // The data must survive into the written file.
742        let mut buf: *mut c_void = core::ptr::null_mut();
743        let mut size = 0usize;
744        assert_eq!(unsafe { asdf_write_to_mem(h.0, &mut buf, &mut size) }, 0);
745        let written = unsafe { core::slice::from_raw_parts(buf.cast::<u8>(), size) }.to_vec();
746        unsafe { libc::free(buf) };
747
748        let reader = asdf_core::Reader::from_bytes(written).unwrap();
749        assert_eq!(reader.block_count(), 1);
750        assert_eq!(&*reader.block_data(0).unwrap(), b"appended data");
751    }
752
753    #[test]
754    fn compression_can_be_set_and_unknown_names_refused() {
755        let block = unsafe { asdf_block_create(core::ptr::null(), 8) };
756
757        let zlib = CString::new("zlib").unwrap();
758        assert_eq!(unsafe { asdf_block_compression_set(block, zlib.as_ptr()) }, 0);
759        assert_eq!(
760            unsafe { CStr::from_ptr(asdf_block_compression(block)) }.to_str().unwrap(),
761            "zlib"
762        );
763
764        // Unknown compressors are an error, not silently ignored.
765        let bogus = CString::new("zstd").unwrap();
766        assert_eq!(unsafe { asdf_block_compression_set(block, bogus.as_ptr()) }, -1);
767        assert_eq!(
768            unsafe { CStr::from_ptr(asdf_block_compression(block)) }.to_str().unwrap(),
769            "zlib",
770            "a rejected name must not change the setting"
771        );
772
773        // The empty string clears it.
774        assert_eq!(unsafe { asdf_block_compression_set(block, core::ptr::null()) }, 0);
775        assert_eq!(unsafe { CStr::from_ptr(asdf_block_compression(block)) }.to_str().unwrap(), "");
776
777        unsafe { asdf_block_destroy(block) };
778    }
779
780    #[test]
781    fn an_appended_block_is_compressed_on_write() {
782        let f = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
783        let h = Handle(f);
784
785        let payload = vec![3u8; 4096];
786        let block = unsafe { asdf_block_create(payload.as_ptr().cast(), payload.len()) };
787        let zlib = CString::new("zlib").unwrap();
788        unsafe { asdf_block_compression_set(block, zlib.as_ptr()) };
789        unsafe { asdf_block_append(h.0, block) };
790        unsafe { asdf_block_close(block) };
791
792        let mut buf: *mut c_void = core::ptr::null_mut();
793        let mut size = 0usize;
794        unsafe { asdf_write_to_mem(h.0, &mut buf, &mut size) };
795        let written = unsafe { core::slice::from_raw_parts(buf.cast::<u8>(), size) }.to_vec();
796        unsafe { libc::free(buf) };
797
798        let reader = asdf_core::Reader::from_bytes(written).unwrap();
799        assert_eq!(reader.block_compression(0).unwrap(), Compression::Zlib);
800        assert_eq!(&*reader.block_data(0).unwrap(), &payload[..]);
801        assert!(reader.block_raw(0).unwrap().len() < payload.len());
802    }
803
804    #[test]
805    fn allocated_size_is_honoured_on_write() {
806        let f = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
807        let h = Handle(f);
808
809        let payload = [1u8; 32];
810        let block = unsafe { asdf_block_create(payload.as_ptr().cast(), payload.len()) };
811        assert_eq!(unsafe { asdf_block_allocated_size_set(block, 1024) }, 0);
812        unsafe { asdf_block_append(h.0, block) };
813        unsafe { asdf_block_close(block) };
814
815        let mut buf: *mut c_void = core::ptr::null_mut();
816        let mut size = 0usize;
817        unsafe { asdf_write_to_mem(h.0, &mut buf, &mut size) };
818        let written = unsafe { core::slice::from_raw_parts(buf.cast::<u8>(), size) }.to_vec();
819        unsafe { libc::free(buf) };
820
821        let reader = asdf_core::Reader::from_bytes(written).unwrap();
822        assert_eq!(reader.block(0).unwrap().header.allocated_size, 1024);
823        assert_eq!(reader.block(0).unwrap().header.used_size, 32);
824    }
825
826    #[test]
827    fn data_set_replaces_the_buffer() {
828        let block = unsafe { asdf_block_create(core::ptr::null(), 0) };
829        let payload = b"replacement".to_vec();
830        assert_eq!(
831            unsafe { asdf_block_data_set(block, payload.as_ptr().cast(), payload.len()) },
832            0
833        );
834        assert_eq!(unsafe { asdf_block_data_size(block) }, payload.len());
835        unsafe { asdf_block_destroy(block) };
836    }
837
838    #[test]
839    fn precompressed_data_records_its_uncompressed_size() {
840        let raw = vec![9u8; 1000];
841        let stored = Compression::Zlib.compress(&raw).unwrap();
842        let block = unsafe { asdf_block_create(core::ptr::null(), 0) };
843        let zlib = CString::new("zlib").unwrap();
844        assert_eq!(
845            unsafe {
846                asdf_block_data_set_compressed(
847                    block,
848                    stored.as_ptr().cast(),
849                    stored.len(),
850                    raw.len() as u64,
851                    zlib.as_ptr(),
852                )
853            },
854            0
855        );
856        // data_size reports the *uncompressed* size, per the header.
857        assert_eq!(unsafe { asdf_block_data_size(block) }, 1000);
858
859        // ...and reading it back inflates to the original.
860        let mut size = 0usize;
861        let data = unsafe { asdf_block_data(block, &mut size) };
862        assert_eq!(size, 1000);
863        let slice = unsafe { core::slice::from_raw_parts(data.cast::<u8>(), size) };
864        assert!(slice.iter().all(|b| *b == 9));
865
866        unsafe { asdf_block_destroy(block) };
867    }
868
869    #[test]
870    fn null_handles_are_tolerated() {
871        let null = core::ptr::null_mut();
872        assert_eq!(unsafe { asdf_block_data_size(null) }, 0);
873        assert!(unsafe { asdf_block_compression(null) }.is_null());
874        assert_eq!(unsafe { asdf_block_compression_set(null, core::ptr::null()) }, -1);
875        assert!(unsafe { asdf_block_checksum(null) }.is_null());
876        assert!(!unsafe { asdf_block_checksum_verify(null, core::ptr::null_mut()) });
877        assert_eq!(unsafe { asdf_block_allocated_size_set(null, 0) }, -1);
878        assert!(unsafe { asdf_block_data_alloc(null, 8) }.is_null());
879
880        let mut size = 123usize;
881        assert!(unsafe { asdf_block_data(null, &mut size) }.is_null());
882        assert_eq!(size, 0, "size must be zeroed when there is no data");
883
884        unsafe { asdf_block_close(null) };
885        unsafe { asdf_block_destroy(null) };
886    }
887}