1use arrow_array::{Array, GenericBinaryArray, GenericStringArray, OffsetSizeTrait};
10use arrow_buffer::{Buffer, OffsetBuffer};
11use arrow_schema::ArrowError;
12use base64::encoded_len;
13use base64::engine::Config;
14
15pub use base64::prelude::*;
16
17pub fn b64_encode<E: Engine, O: OffsetSizeTrait>(
25 engine: &E,
26 array: &GenericBinaryArray<O>,
27) -> GenericStringArray<O> {
28 let lengths = array.offsets().windows(2).map(|w| {
29 let len = w[1].as_usize() - w[0].as_usize();
30 encoded_len(len, engine.config().encode_padding()).unwrap()
31 });
32 let offsets = OffsetBuffer::<O>::from_lengths(lengths);
33 let buffer_len = offsets.last().as_usize();
34 let mut buffer = vec![0_u8; buffer_len];
35 let mut offset = 0;
36
37 for i in 0..array.len() {
38 let len = engine
39 .encode_slice(array.value(i), &mut buffer[offset..])
40 .unwrap();
41 offset += len;
42 }
43 assert_eq!(offset, buffer_len);
44
45 GenericStringArray::try_new(offsets, Buffer::from_vec(buffer), array.nulls().cloned())
47 .expect("Engine produced invalid UTF-8")
48}
49
50pub fn b64_decode<E: Engine, O: OffsetSizeTrait>(
56 engine: &E,
57 array: &GenericBinaryArray<O>,
58) -> Result<GenericBinaryArray<O>, ArrowError> {
59 let estimated_len = array.values().len(); let mut buffer = vec![0; estimated_len];
61
62 let mut offsets = Vec::with_capacity(array.len() + 1);
63 offsets.push(O::usize_as(0));
64 let mut offset = 0;
65
66 for v in array {
67 if let Some(v) = v {
68 let len = engine
69 .decode_slice(v, &mut buffer[offset..])
70 .map_err(|err| {
71 ArrowError::InvalidArgumentError(format!("Failed to decode base64: {err}"))
72 })?;
73 offset += len;
75 }
76 offsets.push(O::usize_as(offset));
77 }
78
79 let offsets = unsafe { OffsetBuffer::new_unchecked(offsets.into()) };
81
82 GenericBinaryArray::try_new(offsets, Buffer::from_vec(buffer), array.nulls().cloned())
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use arrow_array::BinaryArray;
89 use rand::{RngExt, rng};
90
91 fn test_engine<E: Engine>(e: &E, a: &BinaryArray) {
92 let encoded = b64_encode(e, a);
93 encoded.to_data().validate_full().unwrap();
94
95 let to_decode = encoded.into();
96 let decoded = b64_decode(e, &to_decode).unwrap();
97 decoded.to_data().validate_full().unwrap();
98
99 assert_eq!(&decoded, a);
100 }
101
102 #[test]
103 #[cfg_attr(miri, ignore)] fn test_b64() {
105 let mut rng = rng();
106 let len = rng.random_range(1024..1050);
107 let data: BinaryArray = (0..len)
108 .map(|_| {
109 let len = rng.random_range(0..16);
110 Some((0..len).map(|_| rng.random::<u8>()).collect::<Vec<u8>>())
111 })
112 .collect();
113
114 test_engine(&BASE64_STANDARD, &data);
115 test_engine(&BASE64_STANDARD_NO_PAD, &data);
116 }
117
118 #[test]
119 fn test_b64_decode_invalid_input() {
120 let data: BinaryArray = vec![Some(b"!!!not base64!!!".to_vec())]
121 .into_iter()
122 .collect();
123 let err = b64_decode(&BASE64_STANDARD, &data).unwrap_err().to_string();
124 assert!(err.contains("Failed to decode base64"), "{err}");
125 }
126
127 struct EvilEngine;
131
132 impl Engine for EvilEngine {
133 type Config = <base64::engine::GeneralPurpose as Engine>::Config;
134 type DecodeEstimate = <base64::engine::GeneralPurpose as Engine>::DecodeEstimate;
135
136 fn internal_encode(&self, input: &[u8], output: &mut [u8]) -> usize {
137 BASE64_STANDARD.internal_encode(input, output)
138 }
139 fn internal_decoded_len_estimate(&self, input_len: usize) -> Self::DecodeEstimate {
140 BASE64_STANDARD.internal_decoded_len_estimate(input_len)
141 }
142 fn internal_decode(
143 &self,
144 input: &[u8],
145 output: &mut [u8],
146 estimate: Self::DecodeEstimate,
147 ) -> Result<base64::engine::DecodeMetadata, base64::DecodeSliceError> {
148 BASE64_STANDARD.internal_decode(input, output, estimate)
149 }
150 fn config(&self) -> &Self::Config {
151 BASE64_STANDARD.config()
152 }
153 fn encode_slice<T: AsRef<[u8]>>(
154 &self,
155 input: T,
156 output_buf: &mut [u8],
157 ) -> Result<usize, base64::EncodeSliceError> {
158 let len = BASE64_STANDARD.encode_slice(input, output_buf)?;
159 for b in &mut output_buf[..len] {
160 *b = 0xFF; }
162 Ok(len)
163 }
164
165 fn padding(&self) -> base64::alphabet::Symbol {
166 base64::alphabet::Symbol::new(b'=').unwrap()
167 }
168 }
169
170 #[test]
171 #[should_panic(expected = "produced invalid UTF-8")]
172 fn test_b64_encode_rejects_invalid_utf8() {
173 let data: BinaryArray = vec![Some(b"hello".to_vec())].into_iter().collect();
174 let _ = b64_encode(&EvilEngine, &data);
175 }
176}