Skip to main content

datafusion_common/utils/
hex.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Hex encoding of bytes and integers.
19//!
20//! [`encode_bytes`] and [`encode_bytes_into`] encode a byte slice into an
21//! owned `String` or an appended `Vec<u8>`, respectively; [`encode_bytes_to_slice`]
22//! writes into a caller-provided, pre-sized buffer. [`encode_u64`] encodes an
23//! integer, trimming leading zeros. All four take a [`HexCase`] to choose
24//! between lowercase and uppercase digits.
25
26use arrow::datatypes::ArrowNativeType;
27
28use crate::Result;
29use crate::error::_internal_err;
30
31/// Case of the emitted hex digits.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum HexCase {
34    /// Digits `0123456789abcdef`.
35    Lower,
36    /// Digits `0123456789ABCDEF`.
37    Upper,
38}
39
40const LOWER_DIGITS: &[u8; 16] = b"0123456789abcdef";
41const UPPER_DIGITS: &[u8; 16] = b"0123456789ABCDEF";
42
43/// Maps a full byte to its two hex digits, so encoding advances a whole byte
44/// per iteration instead of a nibble.
45const LOOKUP_LOWER: [[u8; 2]; 256] = build_lookup(LOWER_DIGITS);
46const LOOKUP_UPPER: [[u8; 2]; 256] = build_lookup(UPPER_DIGITS);
47
48const fn build_lookup(digits: &[u8; 16]) -> [[u8; 2]; 256] {
49    let mut table = [[0u8; 2]; 256];
50    let mut i = 0;
51    while i < 256 {
52        table[i][0] = digits[i >> 4];
53        table[i][1] = digits[i & 0xF];
54        i += 1;
55    }
56    table
57}
58
59impl HexCase {
60    #[inline]
61    const fn lookup(self) -> &'static [[u8; 2]; 256] {
62        match self {
63            HexCase::Lower => &LOOKUP_LOWER,
64            HexCase::Upper => &LOOKUP_UPPER,
65        }
66    }
67
68    #[inline]
69    const fn digits(self) -> &'static [u8; 16] {
70        match self {
71            HexCase::Lower => LOWER_DIGITS,
72            HexCase::Upper => UPPER_DIGITS,
73        }
74    }
75}
76
77/// Trait for converting integer types to hexadecimal in a buffer
78pub trait ToHex: ArrowNativeType {
79    /// Writes the hex representation into `buf` and returns the written
80    /// subslice. Digits are right-aligned with leading zeros trimmed.
81    fn write_hex(self, case: HexCase, buf: &mut [u8; 16]) -> &[u8];
82}
83
84macro_rules! impl_to_hex_signed {
85    ($ty:ty) => {
86        impl ToHex for $ty {
87            #[inline(always)]
88            fn write_hex(self, case: HexCase, buf: &mut [u8; 16]) -> &[u8] {
89                encode_u64(self as i64 as u64, case, buf)
90            }
91        }
92    };
93}
94
95macro_rules! impl_to_hex_unsigned {
96    ($ty:ty) => {
97        impl ToHex for $ty {
98            #[inline(always)]
99            fn write_hex(self, case: HexCase, buf: &mut [u8; 16]) -> &[u8] {
100                encode_u64(self as u64, case, buf)
101            }
102        }
103    };
104}
105
106impl_to_hex_signed!(i8);
107impl_to_hex_signed!(i16);
108impl_to_hex_signed!(i32);
109impl_to_hex_signed!(i64);
110impl_to_hex_unsigned!(u8);
111impl_to_hex_unsigned!(u16);
112impl_to_hex_unsigned!(u32);
113impl_to_hex_unsigned!(u64);
114
115/// Appends the hex encoding of `bytes` to `out`.
116///
117/// Allocates only through `out`'s own growth. Callers that must bound or guard
118/// that growth should reserve capacity in `out` before calling.
119#[inline(always)]
120pub fn encode_bytes_into(bytes: &[u8], case: HexCase, out: &mut Vec<u8>) {
121    let lookup = case.lookup();
122    for &byte in bytes {
123        out.extend_from_slice(&lookup[byte as usize]);
124    }
125}
126
127/// Writes the hex encoding of `bytes` into `out`.
128///
129/// This is for callers that already own a pre-sized buffer (for example a
130/// slice of a larger, pre-allocated output array) and want to write directly
131/// into it rather than appending to a `Vec`.
132///
133/// Returns an internal error if `out` is not exactly `2 * bytes.len()` bytes
134/// long, without filling any of the `out` buffer.
135///
136/// # Example
137///
138/// ```
139/// use datafusion_common::utils::hex::{HexCase, encode_bytes_to_slice};
140///
141/// let mut out = [0u8; 8];
142/// encode_bytes_to_slice(&[0xde, 0xad, 0xbe, 0xef], HexCase::Lower, &mut out)?;
143/// assert_eq!(&out, b"deadbeef");
144/// # Ok::<(), datafusion_common::DataFusionError>(())
145/// ```
146#[inline(always)]
147pub fn encode_bytes_to_slice(bytes: &[u8], case: HexCase, out: &mut [u8]) -> Result<()> {
148    let expected = bytes.len() * 2;
149    if out.len() != expected {
150        return _internal_err!(
151            "hex output buffer is {} bytes, expected {expected}",
152            out.len()
153        );
154    }
155    let lookup = case.lookup();
156    for (&b, chunk) in bytes.iter().zip(out.chunks_exact_mut(2)) {
157        chunk.copy_from_slice(&lookup[b as usize]);
158    }
159    Ok(())
160}
161
162/// Returns the hex encoding of `bytes` as an owned `String`.
163///
164/// # Example
165///
166/// ```
167/// use datafusion_common::utils::hex::{HexCase, encode_bytes};
168///
169/// assert_eq!(encode_bytes(&[0xde, 0xad, 0xbe, 0xef], HexCase::Lower), "deadbeef");
170/// assert_eq!(encode_bytes(&[0xde, 0xad, 0xbe, 0xef], HexCase::Upper), "DEADBEEF");
171/// ```
172#[inline]
173pub fn encode_bytes(bytes: &[u8], case: HexCase) -> String {
174    let mut out = Vec::with_capacity(bytes.len() * 2);
175    encode_bytes_into(bytes, case, &mut out);
176    // SAFETY: `out` holds only ASCII hex digits, which are valid UTF-8.
177    unsafe { String::from_utf8_unchecked(out) }
178}
179
180/// Writes `v` as hex into `buf` and returns the written subslice.
181///
182/// Digits are written right-aligned with leading zeros trimmed, so the result
183/// borrows the tail of `buf`. Zero encodes as `"0"`.
184///
185/// Signed values should be cast with `as u64`, which yields the two's
186/// complement representation that both `to_hex` and Spark's `hex` produce for
187/// negative input.
188///
189/// # Example
190///
191/// The caller owns the buffer and can reuse it across calls; each call
192/// returns a fresh subslice of it, borrowed for as long as `buf` is:
193///
194/// ```
195/// use datafusion_common::utils::hex::{HexCase, encode_u64};
196///
197/// let mut buf = [0u8; 16];
198/// assert_eq!(encode_u64(0xAB, HexCase::Lower, &mut buf), b"ab");
199/// assert_eq!(encode_u64(0, HexCase::Lower, &mut buf), b"0");
200/// ```
201#[inline(always)]
202pub fn encode_u64(v: u64, case: HexCase, buf: &mut [u8; 16]) -> &[u8] {
203    let start = write_digits(v, case, buf);
204    &buf[start..]
205}
206
207/// Writes the digits of `v` right-aligned in `buf`, returning the index of the
208/// first digit.
209///
210/// Split out from [`encode_u64`] so the mutable borrow of `buf` ends before the
211/// returned slice reborrows it.
212#[inline(always)]
213fn write_digits(v: u64, case: HexCase, buf: &mut [u8; 16]) -> usize {
214    if v == 0 {
215        buf[15] = b'0';
216        return 15;
217    }
218
219    // Consume two nibbles (one full byte) per iteration.
220    let lookup = case.lookup();
221    let mut pos = 16;
222    let mut rest = v;
223    while rest >= 0x10 {
224        pos -= 2;
225        let pair = lookup[(rest & 0xFF) as usize];
226        buf[pos] = pair[0];
227        buf[pos + 1] = pair[1];
228        rest >>= 8;
229    }
230    if rest > 0 {
231        // A single high nibble (0x1..=0xF) remains.
232        pos -= 1;
233        buf[pos] = case.digits()[rest as usize];
234    }
235
236    pos
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    fn hex_u64(v: u64, case: HexCase) -> String {
244        let mut buf = [0u8; 16];
245        String::from_utf8(encode_u64(v, case, &mut buf).to_vec()).unwrap()
246    }
247
248    #[test]
249    fn encode_u64_zero() {
250        assert_eq!(hex_u64(0, HexCase::Lower), "0");
251        assert_eq!(hex_u64(0, HexCase::Upper), "0");
252    }
253
254    #[test]
255    fn encode_u64_single_nibble() {
256        for v in 1..=0xFu64 {
257            assert_eq!(hex_u64(v, HexCase::Lower), format!("{v:x}"));
258            assert_eq!(hex_u64(v, HexCase::Upper), format!("{v:X}"));
259        }
260    }
261
262    #[test]
263    fn encode_u64_digit_count_boundaries() {
264        // Straddle each odd/even digit-count boundary: the two-nibbles-per
265        // iteration loop plus the trailing single-nibble fixup.
266        for v in [
267            0x10u64,
268            0xFF,
269            0x100,
270            0xFFF,
271            0x1000,
272            0xFFFFF,
273            0xFFFF_FFFF,
274            0x1_0000_0000,
275        ] {
276            assert_eq!(hex_u64(v, HexCase::Lower), format!("{v:x}"));
277            assert_eq!(hex_u64(v, HexCase::Upper), format!("{v:X}"));
278        }
279    }
280
281    #[test]
282    fn encode_u64_max() {
283        assert_eq!(hex_u64(u64::MAX, HexCase::Lower), "ffffffffffffffff");
284        assert_eq!(hex_u64(u64::MAX, HexCase::Upper), "FFFFFFFFFFFFFFFF");
285    }
286
287    #[test]
288    fn encode_u64_signed_is_twos_complement() {
289        // Callers cast signed values with `as u64`; this is the behaviour both
290        // `to_hex` and Spark `hex` rely on for negative input.
291        assert_eq!(hex_u64(-1i64 as u64, HexCase::Lower), "ffffffffffffffff");
292        assert_eq!(hex_u64(i64::MIN as u64, HexCase::Upper), "8000000000000000");
293    }
294
295    #[test]
296    fn encode_bytes_empty() {
297        assert_eq!(encode_bytes(&[], HexCase::Lower), "");
298        assert_eq!(encode_bytes(&[], HexCase::Upper), "");
299    }
300
301    #[test]
302    fn encode_bytes_examples() {
303        assert_eq!(encode_bytes(&[0x00], HexCase::Lower), "00");
304        assert_eq!(encode_bytes(&[0xAB], HexCase::Lower), "ab");
305        assert_eq!(encode_bytes(&[0xAB], HexCase::Upper), "AB");
306        assert_eq!(
307            encode_bytes(&[0xde, 0xad, 0xbe, 0xef], HexCase::Lower),
308            "deadbeef"
309        );
310        assert_eq!(
311            encode_bytes(&[0xde, 0xad, 0xbe, 0xef], HexCase::Upper),
312            "DEADBEEF"
313        );
314    }
315
316    #[test]
317    fn encode_bytes_covers_every_byte_value() {
318        let bytes: Vec<u8> = (0..=255u8).collect();
319
320        let expected: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
321        assert_eq!(encode_bytes(&bytes, HexCase::Lower), expected);
322
323        let expected: String = bytes.iter().map(|b| format!("{b:02X}")).collect();
324        assert_eq!(encode_bytes(&bytes, HexCase::Upper), expected);
325    }
326
327    #[test]
328    fn encode_bytes_into_appends_without_clearing() {
329        let mut out = b"prefix-".to_vec();
330        encode_bytes_into(&[0x01, 0x02], HexCase::Lower, &mut out);
331        assert_eq!(out, b"prefix-0102");
332    }
333
334    #[test]
335    fn encode_u64_reused_buffer_leaks_no_stale_digits() {
336        let mut buf = [0u8; 16];
337        assert_eq!(
338            encode_u64(u64::MAX, HexCase::Lower, &mut buf),
339            b"ffffffffffffffff"
340        );
341        assert_eq!(encode_u64(0, HexCase::Lower, &mut buf), b"0");
342        assert_eq!(encode_u64(0xAB, HexCase::Lower, &mut buf), b"ab");
343    }
344
345    #[test]
346    fn encode_bytes_to_slice_empty() -> Result<()> {
347        let mut out: [u8; 0] = [];
348        encode_bytes_to_slice(&[], HexCase::Lower, &mut out)?;
349        assert_eq!(out, [] as [u8; 0]);
350        Ok(())
351    }
352
353    #[test]
354    fn encode_bytes_to_slice_examples() -> Result<()> {
355        let mut out = [0u8; 8];
356        encode_bytes_to_slice(&[0xde, 0xad, 0xbe, 0xef], HexCase::Lower, &mut out)?;
357        assert_eq!(&out, b"deadbeef");
358
359        let mut out = [0u8; 8];
360        encode_bytes_to_slice(&[0xde, 0xad, 0xbe, 0xef], HexCase::Upper, &mut out)?;
361        assert_eq!(&out, b"DEADBEEF");
362        Ok(())
363    }
364
365    #[test]
366    fn encode_bytes_to_slice_agrees_with_encode_bytes() -> Result<()> {
367        let bytes: Vec<u8> = (0..=255u8).collect();
368        for case in [HexCase::Lower, HexCase::Upper] {
369            let mut out = vec![0u8; bytes.len() * 2];
370            encode_bytes_to_slice(&bytes, case, &mut out)?;
371            assert_eq!(String::from_utf8(out).unwrap(), encode_bytes(&bytes, case));
372        }
373        Ok(())
374    }
375
376    #[test]
377    fn encode_bytes_to_slice_rejects_wrong_length() {
378        // Too short: the old `debug_assert` let release builds silently drop
379        // the remaining input.
380        let mut short = [0u8; 6];
381        let err =
382            encode_bytes_to_slice(&[0xde, 0xad, 0xbe, 0xef], HexCase::Lower, &mut short)
383                .unwrap_err();
384        assert!(
385            err.message()
386                .contains("hex output buffer is 6 bytes, expected 8"),
387            "unexpected message: {err}"
388        );
389
390        // Too long: would have left stale bytes at the tail.
391        let mut long = [0u8; 10];
392        assert!(
393            encode_bytes_to_slice(&[0xde, 0xad, 0xbe, 0xef], HexCase::Lower, &mut long)
394                .is_err()
395        );
396    }
397}