Skip to main content

quack_rs/value/
blob.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. <https://github.com/tomtom215/>
3// My way of giving something small back to the open source community
4// and encouraging more Rust development!
5
6use libduckdb_sys::{duckdb_blob, duckdb_free, duckdb_get_blob};
7
8use super::Value;
9use crate::error::ExtensionError;
10
11fn blob_size(blob: &duckdb_blob) -> Result<Option<usize>, ExtensionError> {
12    if blob.data.is_null() {
13        return if blob.size == 0 {
14            Ok(None)
15        } else {
16            Err(ExtensionError::new("duckdb_get_blob returned null data"))
17        };
18    }
19    usize::try_from(blob.size)
20        .map(Some)
21        .map_err(|_| ExtensionError::new("duckdb_get_blob returned an unsupported blob size"))
22}
23
24#[mutants::skip] // DuckDB allocator effects are not observable from safe Rust tests.
25unsafe fn free_blob_data(data: *mut core::ffi::c_void) {
26    if !data.is_null() {
27        unsafe { duckdb_free(data) };
28    }
29}
30
31impl Value {
32    /// Extracts the value as an owned `Vec<u8>` (`BLOB`).
33    ///
34    /// `DuckDB` allocates the blob's backing buffer; this method copies it into
35    /// an owned `Vec<u8>` and frees the original with `duckdb_free`. The bytes
36    /// are copied without UTF-8 validation.
37    ///
38    /// # Errors
39    ///
40    /// Returns `ExtensionError` if the value handle is null, `duckdb_get_blob`
41    /// returns a null data pointer for a non-empty blob, or the blob size cannot
42    /// be represented by `usize` on the current platform.
43    pub fn as_blob(&self) -> Result<Vec<u8>, ExtensionError> {
44        if self.raw.is_null() {
45            return Err(ExtensionError::new("Value is null"));
46        }
47        // SAFETY: self.raw is a valid duckdb_value per constructor contract.
48        let blob: duckdb_blob = unsafe { duckdb_get_blob(self.raw) };
49        let size = match blob_size(&blob) {
50            Ok(None) => return Ok(Vec::new()),
51            Ok(Some(size)) => size,
52            Err(error) => {
53                // SAFETY: non-null blob data was allocated by DuckDB. The
54                // helper also accepts null for the invalid-pointer error path.
55                unsafe { free_blob_data(blob.data) };
56                return Err(error);
57            }
58        };
59        // SAFETY: blob.data is a DuckDB-allocated buffer of exactly `blob.size`
60        // bytes, valid until we free it below.
61        let slice = unsafe { std::slice::from_raw_parts(blob.data.cast::<u8>(), size) };
62        let out = slice.to_vec();
63        // SAFETY: blob.data was allocated by DuckDB and must be freed with duckdb_free.
64        unsafe { free_blob_data(blob.data) };
65        Ok(out)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn blob_size_null_empty_blob_is_valid() {
75        let blob = duckdb_blob {
76            data: std::ptr::null_mut(),
77            size: 0,
78        };
79        assert!(matches!(blob_size(&blob), Ok(None)));
80    }
81
82    #[test]
83    fn blob_size_null_non_empty_blob_is_invalid() {
84        let blob = duckdb_blob {
85            data: std::ptr::null_mut(),
86            size: 1,
87        };
88        assert!(blob_size(&blob).is_err());
89    }
90
91    #[test]
92    fn null_value_as_blob_returns_error() {
93        let value = unsafe { Value::from_raw(std::ptr::null_mut()) };
94        assert!(value.as_blob().is_err());
95    }
96
97    #[cfg(feature = "_duckdb-testing")]
98    #[test]
99    fn blob_value_non_utf8_bytes_round_trip() {
100        let _db = crate::testing::InMemoryDb::open().expect("should initialize DuckDB");
101        let payload = [
102            0x80u8, 0xF0, 0x01, 0x42, 0xFF, 0x00, 0xFE, 0x7F, 0xAA, 0x55, 0xC0, 0xAF, 0x90,
103        ];
104        let len = u64::try_from(payload.len()).expect("test payload length fits in u64");
105        // SAFETY: payload points to `len` readable bytes for the duration of the call.
106        let raw = unsafe { libduckdb_sys::duckdb_create_blob(payload.as_ptr(), len) };
107        // SAFETY: duckdb_create_blob returns an owned value handle.
108        let value = unsafe { Value::from_raw(raw) };
109
110        assert_eq!(value.as_blob().expect("blob should be readable"), payload);
111    }
112
113    #[cfg(feature = "_duckdb-testing")]
114    #[test]
115    fn blob_value_empty_bytes_round_trip() {
116        let _db = crate::testing::InMemoryDb::open().expect("should initialize DuckDB");
117        let payload = [];
118        // SAFETY: payload is readable for zero bytes for the duration of the call.
119        let raw = unsafe { libduckdb_sys::duckdb_create_blob(payload.as_ptr(), 0) };
120        // SAFETY: duckdb_create_blob returns an owned value handle.
121        let value = unsafe { Value::from_raw(raw) };
122
123        assert!(value.as_blob().expect("blob should be readable").is_empty());
124    }
125}