diff --git a/arrow-cast/src/base64.rs b/arrow-cast/src/base64.rs index 6a8da0141dea..ea53aa5dcc89 100644 --- a/arrow-cast/src/base64.rs +++ b/arrow-cast/src/base64.rs @@ -27,7 +27,11 @@ use base64::engine::Config; pub use base64::prelude::*; -/// Bas64 encode each element of `array` with the provided [`Engine`] +/// Base64 encode each element of `array` with the provided [`Engine`] +/// +/// Panics if the `Engine` emits output that is not valid UTF-8. A correct +/// `Engine` never does, but it is a safe trait so a misbehaving impl could; +/// validating keeps the returned [`GenericStringArray`] sound (#10284). pub fn b64_encode( engine: &E, array: &GenericBinaryArray, @@ -49,10 +53,9 @@ pub fn b64_encode( } assert_eq!(offset, buffer_len); - // Safety: Base64 is valid UTF-8 - unsafe { - GenericStringArray::new_unchecked(offsets, Buffer::from_vec(buffer), array.nulls().cloned()) - } + // `try_new` validates UTF-8 instead of trusting the (safe-trait) Engine. + GenericStringArray::try_new(offsets, Buffer::from_vec(buffer), array.nulls().cloned()) + .expect("Engine produced invalid UTF-8") } /// Base64 decode each element of `array` with the provided [`Engine`] @@ -113,4 +116,50 @@ mod tests { test_engine(&BASE64_STANDARD, &data); test_engine(&BASE64_STANDARD_NO_PAD, &data); } + + /// Safe-Rust `Engine` that writes invalid UTF-8 into the encode buffer + /// (#10284). `b64_encode` must reject it rather than build an unsound + /// `StringArray`. + struct EvilEngine; + + impl Engine for EvilEngine { + type Config = ::Config; + type DecodeEstimate = ::DecodeEstimate; + + fn internal_encode(&self, input: &[u8], output: &mut [u8]) -> usize { + BASE64_STANDARD.internal_encode(input, output) + } + fn internal_decoded_len_estimate(&self, input_len: usize) -> Self::DecodeEstimate { + BASE64_STANDARD.internal_decoded_len_estimate(input_len) + } + fn internal_decode( + &self, + input: &[u8], + output: &mut [u8], + estimate: Self::DecodeEstimate, + ) -> Result { + BASE64_STANDARD.internal_decode(input, output, estimate) + } + fn config(&self) -> &Self::Config { + BASE64_STANDARD.config() + } + fn encode_slice>( + &self, + input: T, + output_buf: &mut [u8], + ) -> Result { + let len = BASE64_STANDARD.encode_slice(input, output_buf)?; + for b in output_buf[..len].iter_mut() { + *b = 0xFF; // invalid UTF-8, but correct length + } + Ok(len) + } + } + + #[test] + #[should_panic(expected = "produced invalid UTF-8")] + fn test_b64_encode_rejects_invalid_utf8() { + let data: BinaryArray = vec![Some(b"hello".to_vec())].into_iter().collect(); + let _ = b64_encode(&EvilEngine, &data); + } }