1use 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] unsafe 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 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 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 unsafe { free_blob_data(blob.data) };
56 return Err(error);
57 }
58 };
59 let slice = unsafe { std::slice::from_raw_parts(blob.data.cast::<u8>(), size) };
62 let out = slice.to_vec();
63 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 let raw = unsafe { libduckdb_sys::duckdb_create_blob(payload.as_ptr(), len) };
107 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 let raw = unsafe { libduckdb_sys::duckdb_create_blob(payload.as_ptr(), 0) };
120 let value = unsafe { Value::from_raw(raw) };
122
123 assert!(value.as_blob().expect("blob should be readable").is_empty());
124 }
125}