Coverage Report

Created: 2026-10-17 06:15

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/cpython/Python/pystrhex.c
Line
Count
Source
1
/* Format bytes as hexadecimal */
2
3
#include "Python.h"
4
#include "pycore_strhex.h"        // _Py_strhex_with_sep()
5
#include "pycore_unicodeobject.h" // _PyUnicode_CheckConsistency()
6
7
/* Scalar hexlify: convert len bytes to 2*len hex characters.
8
   Uses table lookup via Py_hexdigits for the conversion. */
9
static inline void
10
_Py_hexlify_scalar(const unsigned char *src, Py_UCS1 *dst, Py_ssize_t len)
11
360
{
12
    /* Various optimizations like using math instead of a table lookup,
13
       manually unrolling the loop, storing the global table pointer locally,
14
       and doing wider dst writes have been tried and benchmarked; all produced
15
       nearly identical performance on gcc 15.  Using a 256 entry uint16_t
16
       table was a bit slower.  So we keep our old simple and obvious code. */
17
1.67k
    for (Py_ssize_t i = 0; i < len; i++) {
18
1.31k
        unsigned char c = src[i];
19
1.31k
        *dst++ = Py_hexdigits[c >> 4];
20
1.31k
        *dst++ = Py_hexdigits[c & 0x0f];
21
1.31k
    }
22
360
}
23
24
/* Portable SIMD optimization for hexlify using GCC/Clang vector extensions.
25
   Uses __builtin_shufflevector for portable interleave that compiles to
26
   native SIMD instructions (SSE2 punpcklbw/punpckhbw on x86-64 [always],
27
   NEON zip1/zip2 on ARM64 [always], & vzip on ARM32 when compiler flags
28
   for the target microarch allow it [try -march=native if running 32-bit
29
   on an RPi3 or later]).
30
31
   Performance:
32
   - For more common small data it varies between 1.1-3x faster.
33
   - Up to 11x faster on larger data than the scalar code.
34
35
   While faster is possible for big data using AVX2 or AVX512, that
36
   adds a ton of complication. Who ever really hexes huge data?
37
   The 16-64 byte boosts align nicely with md5 - sha512 hexdigests.
38
*/
39
#ifdef _Py_HAVE_EFFICIENT_BUILTIN_SHUFFLEVECTOR
40
41
/* 128-bit vector of 16 unsigned bytes */
42
typedef unsigned char v16u8 __attribute__((vector_size(16)));
43
/* 128-bit vector of 16 signed bytes - for efficient comparison.
44
   Using signed comparison generates pcmpgtb on x86-64 instead of
45
   the slower psubusb+pcmpeqb sequence from unsigned comparison.
46
   ARM NEON performs the same either way. */
47
typedef signed char v16s8 __attribute__((vector_size(16)));
48
49
/* Splat a byte value across all 16 lanes */
50
static inline v16u8
51
v16u8_splat(unsigned char x)
52
1.33k
{
53
1.33k
    return (v16u8){x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x};
54
1.33k
}
55
56
static inline v16s8
57
v16s8_splat(signed char x)
58
334
{
59
334
    return (v16s8){x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x};
60
334
}
61
62
/* Portable SIMD hexlify: converts 16 bytes to 32 hex chars per iteration.
63
   Compiles to native SSE2 on x86-64, NEON on ARM64 (and some ARM32). */
64
static void
65
_Py_hexlify_simd(const unsigned char *src, Py_UCS1 *dst, Py_ssize_t len)
66
334
{
67
334
    const v16u8 mask_0f = v16u8_splat(0x0f);
68
334
    const v16u8 ascii_0 = v16u8_splat('0');
69
334
    const v16u8 offset = v16u8_splat('a' - '0' - 10);  /* 0x27 */
70
334
    const v16u8 four = v16u8_splat(4);
71
334
    const v16s8 nine = v16s8_splat(9);
72
73
334
    Py_ssize_t i = 0;
74
75
    /* Process 16 bytes at a time */
76
1.43M
    for (; i + 16 <= len; i += 16, dst += 32) {
77
        /* Load 16 bytes (memcpy for safe unaligned access) */
78
1.43M
        v16u8 data;
79
1.43M
        memcpy(&data, src + i, 16);
80
81
        /* Extract high and low nibbles using vector operators */
82
1.43M
        v16u8 hi = (data >> four) & mask_0f;
83
1.43M
        v16u8 lo = data & mask_0f;
84
85
        /* Compare > 9 using signed comparison for efficient codegen.
86
           Nibble values 0-15 are safely in signed byte range.
87
           This generates pcmpgtb on x86-64, avoiding the slower
88
           psubusb+pcmpeqb sequence from unsigned comparison. */
89
1.43M
        v16u8 hi_gt9 = (v16u8)((v16s8)hi > nine);
90
1.43M
        v16u8 lo_gt9 = (v16u8)((v16s8)lo > nine);
91
92
        /* Convert nibbles to hex ASCII */
93
1.43M
        hi = hi + ascii_0 + (hi_gt9 & offset);
94
1.43M
        lo = lo + ascii_0 + (lo_gt9 & offset);
95
96
        /* Interleave hi/lo nibbles using portable shufflevector.
97
           This compiles to punpcklbw/punpckhbw on x86-64, zip1/zip2 on ARM64,
98
           or vzip on ARM32. */
99
1.43M
        v16u8 result0 = __builtin_shufflevector(hi, lo,
100
1.43M
            0, 16, 1, 17, 2, 18, 3, 19, 4, 20, 5, 21, 6, 22, 7, 23);
101
1.43M
        v16u8 result1 = __builtin_shufflevector(hi, lo,
102
1.43M
            8, 24, 9, 25, 10, 26, 11, 27, 12, 28, 13, 29, 14, 30, 15, 31);
103
104
        /* Store 32 hex characters */
105
1.43M
        memcpy(dst, &result0, 16);
106
1.43M
        memcpy(dst + 16, &result1, 16);
107
1.43M
    }
108
109
    /* Scalar fallback for remaining 0-15 bytes */
110
334
    _Py_hexlify_scalar(src + i, dst, len - i);
111
334
}
112
113
#endif /* _Py_HAVE_EFFICIENT_BUILTIN_SHUFFLEVECTOR */
114
115
static PyObject *
116
_Py_strhex_impl(const char* argbuf, Py_ssize_t arglen,
117
                PyObject* sep, Py_ssize_t bytes_per_sep_group,
118
                int return_bytes)
119
360
{
120
360
    assert(arglen >= 0);
121
122
360
    Py_UCS1 sep_char = 0;
123
360
    if (sep) {
124
0
        Py_ssize_t seplen = PyObject_Length((PyObject*)sep);
125
0
        if (seplen < 0) {
126
0
            return NULL;
127
0
        }
128
0
        if (seplen != 1) {
129
0
            PyErr_SetString(PyExc_ValueError, "sep must be length 1.");
130
0
            return NULL;
131
0
        }
132
0
        if (PyUnicode_Check(sep)) {
133
0
            if (PyUnicode_KIND(sep) != PyUnicode_1BYTE_KIND) {
134
0
                PyErr_SetString(PyExc_ValueError, "sep must be ASCII.");
135
0
                return NULL;
136
0
            }
137
0
            sep_char = PyUnicode_READ_CHAR(sep, 0);
138
0
        }
139
0
        else if (PyBytes_Check(sep)) {
140
0
            sep_char = PyBytes_AS_STRING(sep)[0];
141
0
        }
142
0
        else {
143
0
            PyErr_SetString(PyExc_TypeError, "sep must be str or bytes.");
144
0
            return NULL;
145
0
        }
146
0
        if (sep_char > 127 && !return_bytes) {
147
0
            PyErr_SetString(PyExc_ValueError, "sep must be ASCII.");
148
0
            return NULL;
149
0
        }
150
0
    }
151
360
    else {
152
360
        bytes_per_sep_group = 0;
153
360
    }
154
360
    size_t abs_bytes_per_sep = _Py_ABS_CAST(size_t, bytes_per_sep_group);
155
360
    Py_ssize_t resultlen = 0;
156
360
    if (bytes_per_sep_group && arglen > 0) {
157
        /* How many sep characters we'll be inserting. */
158
0
        resultlen = (arglen - 1) / abs_bytes_per_sep;
159
0
    }
160
    /* Bounds checking for our Py_ssize_t indices. */
161
360
    if (arglen >= PY_SSIZE_T_MAX / 2 - resultlen) {
162
0
        return PyErr_NoMemory();
163
0
    }
164
360
    resultlen += arglen * 2;
165
166
360
    if ((size_t)abs_bytes_per_sep >= (size_t)arglen) {
167
8
        bytes_per_sep_group = 0;
168
8
        abs_bytes_per_sep = 0;
169
8
    }
170
171
360
    PyObject *retval = NULL;
172
360
    PyBytesWriter *bytes_writer = NULL;
173
360
    Py_UCS1 *retbuf;
174
360
    if (return_bytes) {
175
        /* If _PyBytes_FromSize() were public we could avoid malloc+copy. */
176
178
        bytes_writer = PyBytesWriter_Create(resultlen);
177
178
        if (!bytes_writer) {
178
0
            return NULL;
179
0
        }
180
178
        retbuf = PyBytesWriter_GetData(bytes_writer);
181
178
    }
182
182
    else {
183
182
        retval = PyUnicode_New(resultlen, 127);
184
182
        if (!retval) {
185
0
            return NULL;
186
0
        }
187
182
        retbuf = PyUnicode_1BYTE_DATA(retval);
188
182
    }
189
190
    /* Hexlify */
191
360
    Py_ssize_t i, j;
192
360
    unsigned char c;
193
194
360
    if (bytes_per_sep_group == 0) {
195
360
#ifdef _Py_HAVE_EFFICIENT_BUILTIN_SHUFFLEVECTOR
196
360
        if (arglen >= 16) {
197
334
            _Py_hexlify_simd((const unsigned char *)argbuf, retbuf, arglen);
198
334
        }
199
26
        else
200
26
#endif
201
26
        {
202
26
            _Py_hexlify_scalar((const unsigned char *)argbuf, retbuf, arglen);
203
26
        }
204
360
    }
205
0
    else {
206
        /* The number of complete chunk+sep periods */
207
0
        Py_ssize_t chunks = (arglen - 1) / abs_bytes_per_sep;
208
0
        Py_ssize_t chunk;
209
0
        size_t k;
210
211
0
        if (bytes_per_sep_group < 0) {
212
0
            i = j = 0;
213
0
            for (chunk = 0; chunk < chunks; chunk++) {
214
0
                for (k = 0; k < abs_bytes_per_sep; k++) {
215
0
                    c = argbuf[i++];
216
0
                    retbuf[j++] = Py_hexdigits[c >> 4];
217
0
                    retbuf[j++] = Py_hexdigits[c & 0x0f];
218
0
                }
219
0
                retbuf[j++] = sep_char;
220
0
            }
221
0
            while (i < arglen) {
222
0
                c = argbuf[i++];
223
0
                retbuf[j++] = Py_hexdigits[c >> 4];
224
0
                retbuf[j++] = Py_hexdigits[c & 0x0f];
225
0
            }
226
0
            assert(j == resultlen);
227
0
        }
228
0
        else {
229
0
            i = arglen - 1;
230
0
            j = resultlen - 1;
231
0
            for (chunk = 0; chunk < chunks; chunk++) {
232
0
                for (k = 0; k < abs_bytes_per_sep; k++) {
233
0
                    c = argbuf[i--];
234
0
                    retbuf[j--] = Py_hexdigits[c & 0x0f];
235
0
                    retbuf[j--] = Py_hexdigits[c >> 4];
236
0
                }
237
0
                retbuf[j--] = sep_char;
238
0
            }
239
0
            while (i >= 0) {
240
0
                c = argbuf[i--];
241
0
                retbuf[j--] = Py_hexdigits[c & 0x0f];
242
0
                retbuf[j--] = Py_hexdigits[c >> 4];
243
0
            }
244
0
            assert(j == -1);
245
0
        }
246
0
    }
247
248
360
    if (return_bytes) {
249
178
        return PyBytesWriter_Finish(bytes_writer);
250
178
    }
251
182
    else {
252
#ifdef Py_DEBUG
253
        assert(_PyUnicode_CheckConsistency(retval, 1));
254
#endif
255
182
        return retval;
256
182
    }
257
360
}
258
259
PyObject * _Py_strhex(const char* argbuf, Py_ssize_t arglen)
260
182
{
261
182
    return _Py_strhex_impl(argbuf, arglen, NULL, 0, 0);
262
182
}
263
264
/* Same as above but returns a bytes() instead of str() to avoid the
265
 * need to decode the str() when bytes are needed. */
266
PyObject* _Py_strhex_bytes(const char* argbuf, Py_ssize_t arglen)
267
0
{
268
0
    return _Py_strhex_impl(argbuf, arglen, NULL, 0, 1);
269
0
}
270
271
/* These variants include support for a separator between every N bytes: */
272
273
PyObject* _Py_strhex_with_sep(const char* argbuf, Py_ssize_t arglen,
274
                              PyObject* sep, Py_ssize_t bytes_per_group)
275
0
{
276
0
    return _Py_strhex_impl(argbuf, arglen, sep, bytes_per_group, 0);
277
0
}
278
279
/* Same as above but returns a bytes() instead of str() to avoid the
280
 * need to decode the str() when bytes are needed. */
281
PyObject* _Py_strhex_bytes_with_sep(const char* argbuf, Py_ssize_t arglen,
282
                                    PyObject* sep, Py_ssize_t bytes_per_group)
283
178
{
284
178
    return _Py_strhex_impl(argbuf, arglen, sep, bytes_per_group, 1);
285
178
}