Coverage Report

Created: 2026-08-31 07:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mercurial/mercurial/cext/charencode.c
Line
Count
Source
1
/*
2
 charencode.c - miscellaneous character encoding
3
4
 Copyright 2008 Olivia Mackall <olivia@selenic.com> and others
5
6
 This software may be used and distributed according to the terms of
7
 the GNU General Public License, incorporated herein by reference.
8
*/
9
10
#define PY_SSIZE_T_CLEAN
11
#include <Python.h>
12
#include <assert.h>
13
14
#include "charencode.h"
15
#include "compat.h"
16
#include "util.h"
17
18
/* clang-format off */
19
static const char lowertable[128] = {
20
  '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
21
  '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f',
22
  '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17',
23
  '\x18', '\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f',
24
  '\x20', '\x21', '\x22', '\x23', '\x24', '\x25', '\x26', '\x27',
25
  '\x28', '\x29', '\x2a', '\x2b', '\x2c', '\x2d', '\x2e', '\x2f',
26
  '\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37',
27
  '\x38', '\x39', '\x3a', '\x3b', '\x3c', '\x3d', '\x3e', '\x3f',
28
  '\x40',
29
          '\x61', '\x62', '\x63', '\x64', '\x65', '\x66', '\x67', /* A-G */
30
  '\x68', '\x69', '\x6a', '\x6b', '\x6c', '\x6d', '\x6e', '\x6f', /* H-O */
31
  '\x70', '\x71', '\x72', '\x73', '\x74', '\x75', '\x76', '\x77', /* P-W */
32
  '\x78', '\x79', '\x7a',                                         /* X-Z */
33
                          '\x5b', '\x5c', '\x5d', '\x5e', '\x5f',
34
  '\x60', '\x61', '\x62', '\x63', '\x64', '\x65', '\x66', '\x67',
35
  '\x68', '\x69', '\x6a', '\x6b', '\x6c', '\x6d', '\x6e', '\x6f',
36
  '\x70', '\x71', '\x72', '\x73', '\x74', '\x75', '\x76', '\x77',
37
  '\x78', '\x79', '\x7a', '\x7b', '\x7c', '\x7d', '\x7e', '\x7f'
38
};
39
40
static const char uppertable[128] = {
41
  '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
42
  '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f',
43
  '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17',
44
  '\x18', '\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f',
45
  '\x20', '\x21', '\x22', '\x23', '\x24', '\x25', '\x26', '\x27',
46
  '\x28', '\x29', '\x2a', '\x2b', '\x2c', '\x2d', '\x2e', '\x2f',
47
  '\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37',
48
  '\x38', '\x39', '\x3a', '\x3b', '\x3c', '\x3d', '\x3e', '\x3f',
49
  '\x40', '\x41', '\x42', '\x43', '\x44', '\x45', '\x46', '\x47',
50
  '\x48', '\x49', '\x4a', '\x4b', '\x4c', '\x4d', '\x4e', '\x4f',
51
  '\x50', '\x51', '\x52', '\x53', '\x54', '\x55', '\x56', '\x57',
52
  '\x58', '\x59', '\x5a', '\x5b', '\x5c', '\x5d', '\x5e', '\x5f',
53
  '\x60',
54
    '\x41', '\x42', '\x43', '\x44', '\x45', '\x46', '\x47', /* a-g */
55
  '\x48', '\x49', '\x4a', '\x4b', '\x4c', '\x4d', '\x4e', '\x4f', /* h-o */
56
  '\x50', '\x51', '\x52', '\x53', '\x54', '\x55', '\x56', '\x57', /* p-w */
57
  '\x58', '\x59', '\x5a',           /* x-z */
58
        '\x7b', '\x7c', '\x7d', '\x7e', '\x7f'
59
};
60
61
/* 1: no escape, 2: \<c>, 6: \u<x> */
62
static const uint8_t jsonlentable[256] = {
63
  6, 6, 6, 6, 6, 6, 6, 6, 2, 2, 2, 6, 2, 2, 6, 6, /* b, t, n, f, r */
64
  6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
65
  1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* " */
66
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
67
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
68
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, /* \\ */
69
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
70
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, /* DEL */
71
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
72
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
73
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
74
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
75
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
76
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
77
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
78
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
79
};
80
81
static const uint8_t jsonparanoidlentable[128] = {
82
  6, 6, 6, 6, 6, 6, 6, 6, 2, 2, 2, 6, 2, 2, 6, 6, /* b, t, n, f, r */
83
  6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
84
  1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* " */
85
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 6, 1, /* <, > */
86
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
87
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, /* \\ */
88
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
89
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, /* DEL */
90
};
91
92
static const char hexchartable[16] = {
93
  '0', '1', '2', '3', '4', '5', '6', '7',
94
  '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
95
};
96
/* clang-format on */
97
98
/*
99
 * Turn a hex-encoded string into binary.
100
 */
101
PyObject *unhexlify(const char *str, Py_ssize_t len)
102
0
{
103
0
  PyObject *ret;
104
0
  char *d;
105
0
  Py_ssize_t i;
106
107
0
  ret = PyBytes_FromStringAndSize(NULL, len / 2);
108
109
0
  if (!ret) {
110
0
    return NULL;
111
0
  }
112
113
0
  d = PyBytes_AsString(ret);
114
115
0
  for (i = 0; i < len;) {
116
0
    int hi = hexdigit(str, i++);
117
0
    int lo = hexdigit(str, i++);
118
0
    *d++ = (hi << 4) | lo;
119
0
  }
120
121
0
  return ret;
122
0
}
123
124
PyObject *isasciistr(PyObject *self, PyObject *args)
125
968
{
126
968
  const char *buf;
127
968
  Py_ssize_t i, len;
128
968
  if (!PyArg_ParseTuple(args, "y#:isasciistr", &buf, &len)) {
129
0
    return NULL;
130
0
  }
131
968
  i = 0;
132
  /* char array in PyStringObject should be at least 4-byte aligned */
133
968
  if (((uintptr_t)buf & 3) == 0) {
134
968
    const uint32_t *p = (const uint32_t *)buf;
135
1.71M
    for (; i < len / 4; i++) {
136
1.71M
      if (p[i] & 0x80808080U) {
137
259
        Py_RETURN_FALSE;
138
259
      }
139
1.71M
    }
140
709
    i *= 4;
141
709
  }
142
1.82k
  for (; i < len; i++) {
143
1.20k
    if (buf[i] & 0x80) {
144
96
      Py_RETURN_FALSE;
145
96
    }
146
1.20k
  }
147
709
  Py_RETURN_TRUE;
148
709
}
149
150
static inline PyObject *
151
_asciitransform(PyObject *str_obj, const char table[128], PyObject *fallback_fn)
152
1.93k
{
153
1.93k
  char *str, *newstr;
154
1.93k
  Py_ssize_t i, len;
155
1.93k
  PyObject *newobj = NULL;
156
1.93k
  PyObject *ret = NULL;
157
158
1.93k
  str = PyBytes_AS_STRING(str_obj);
159
1.93k
  len = PyBytes_GET_SIZE(str_obj);
160
161
1.93k
  newobj = PyBytes_FromStringAndSize(NULL, len);
162
1.93k
  if (!newobj) {
163
0
    goto quit;
164
0
  }
165
166
1.93k
  newstr = PyBytes_AS_STRING(newobj);
167
168
13.6M
  for (i = 0; i < len; i++) {
169
13.6M
    char c = str[i];
170
13.6M
    if (c & 0x80) {
171
710
      if (fallback_fn != NULL) {
172
0
        ret = PyObject_CallFunctionObjArgs(
173
0
            fallback_fn, str_obj, NULL);
174
710
      } else {
175
710
        PyObject *err = PyUnicodeDecodeError_Create(
176
710
            "ascii", str, len, i, (i + 1),
177
710
            "unexpected code byte");
178
710
        PyErr_SetObject(PyExc_UnicodeDecodeError, err);
179
710
        Py_XDECREF(err);
180
710
      }
181
710
      goto quit;
182
710
    }
183
13.6M
    newstr[i] = table[(unsigned char)c];
184
13.6M
  }
185
186
1.22k
  ret = newobj;
187
1.22k
  Py_INCREF(ret);
188
1.93k
quit:
189
1.93k
  Py_XDECREF(newobj);
190
1.93k
  return ret;
191
1.22k
}
192
193
PyObject *asciilower(PyObject *self, PyObject *args)
194
968
{
195
968
  PyObject *str_obj;
196
968
  if (!PyArg_ParseTuple(args, "O!:asciilower", &PyBytes_Type, &str_obj)) {
197
0
    return NULL;
198
0
  }
199
968
  return _asciitransform(str_obj, lowertable, NULL);
200
968
}
201
202
PyObject *asciiupper(PyObject *self, PyObject *args)
203
968
{
204
968
  PyObject *str_obj;
205
968
  if (!PyArg_ParseTuple(args, "O!:asciiupper", &PyBytes_Type, &str_obj)) {
206
0
    return NULL;
207
0
  }
208
968
  return _asciitransform(str_obj, uppertable, NULL);
209
968
}
210
211
PyObject *make_file_foldmap(PyObject *self, PyObject *args)
212
0
{
213
0
  PyObject *dmap, *spec_obj, *normcase_fallback;
214
0
  PyObject *file_foldmap = NULL;
215
0
  enum normcase_spec spec;
216
0
  PyObject *k, *v;
217
0
  dirstateItemObject *tuple;
218
0
  Py_ssize_t pos = 0;
219
0
  const char *table;
220
221
0
  if (!PyArg_ParseTuple(args, "O!O!O!:make_file_foldmap", &PyDict_Type,
222
0
                        &dmap, &PyLong_Type, &spec_obj, &PyFunction_Type,
223
0
                        &normcase_fallback)) {
224
0
    goto quit;
225
0
  }
226
227
0
  spec = (int)PyLong_AS_LONG(spec_obj);
228
0
  switch (spec) {
229
0
  case NORMCASE_LOWER:
230
0
    table = lowertable;
231
0
    break;
232
0
  case NORMCASE_UPPER:
233
0
    table = uppertable;
234
0
    break;
235
0
  case NORMCASE_OTHER:
236
0
    table = NULL;
237
0
    break;
238
0
  default:
239
0
    PyErr_SetString(PyExc_TypeError, "invalid normcasespec");
240
0
    goto quit;
241
0
  }
242
243
  /* Add some more entries to deal with additions outside this
244
     function. */
245
0
  file_foldmap = _PyDict_NewPresized((PyDict_Size(dmap) / 10) * 11);
246
0
  if (file_foldmap == NULL) {
247
0
    goto quit;
248
0
  }
249
250
0
  while (PyDict_Next(dmap, &pos, &k, &v)) {
251
0
    if (!dirstate_tuple_check(v)) {
252
0
      PyErr_SetString(PyExc_TypeError,
253
0
                      "expected a dirstate tuple");
254
0
      goto quit;
255
0
    }
256
257
0
    tuple = (dirstateItemObject *)v;
258
0
    if (tuple->flags | dirstate_flag_wc_tracked) {
259
0
      PyObject *normed;
260
0
      if (table != NULL) {
261
0
        normed = _asciitransform(k, table,
262
0
                                 normcase_fallback);
263
0
      } else {
264
0
        normed = PyObject_CallFunctionObjArgs(
265
0
            normcase_fallback, k, NULL);
266
0
      }
267
268
0
      if (normed == NULL) {
269
0
        goto quit;
270
0
      }
271
0
      if (PyDict_SetItem(file_foldmap, normed, k) == -1) {
272
0
        Py_DECREF(normed);
273
0
        goto quit;
274
0
      }
275
0
      Py_DECREF(normed);
276
0
    }
277
0
  }
278
0
  return file_foldmap;
279
0
quit:
280
0
  Py_XDECREF(file_foldmap);
281
0
  return NULL;
282
0
}
283
284
/* calculate length of JSON-escaped string; returns -1 if unsupported */
285
static Py_ssize_t jsonescapelen(const char *buf, Py_ssize_t len, bool paranoid)
286
350
{
287
350
  Py_ssize_t i, esclen = 0;
288
289
350
  if (paranoid) {
290
    /* don't want to process multi-byte escapes in C */
291
1.45M
    for (i = 0; i < len; i++) {
292
1.45M
      char c = buf[i];
293
1.45M
      if (c & 0x80) {
294
54
        PyErr_SetString(PyExc_ValueError,
295
54
                        "cannot process non-ascii str");
296
54
        return -1;
297
54
      }
298
1.45M
      esclen += jsonparanoidlentable[(unsigned char)c];
299
1.45M
      if (esclen < 0) {
300
0
        PyErr_SetString(PyExc_MemoryError,
301
0
                        "overflow in jsonescapelen");
302
0
        return -1;
303
0
      }
304
1.45M
    }
305
199
  } else {
306
26.2M
    for (i = 0; i < len; i++) {
307
26.2M
      char c = buf[i];
308
26.2M
      esclen += jsonlentable[(unsigned char)c];
309
26.2M
      if (esclen < 0) {
310
0
        PyErr_SetString(PyExc_MemoryError,
311
0
                        "overflow in jsonescapelen");
312
0
        return -1;
313
0
      }
314
26.2M
    }
315
199
  }
316
317
296
  return esclen;
318
350
}
319
320
/* map '\<c>' escape character */
321
static char jsonescapechar2(char c)
322
323k
{
323
323k
  switch (c) {
324
1.30k
  case '\b':
325
1.30k
    return 'b';
326
197k
  case '\t':
327
197k
    return 't';
328
75.1k
  case '\n':
329
75.1k
    return 'n';
330
1.45k
  case '\f':
331
1.45k
    return 'f';
332
44.9k
  case '\r':
333
44.9k
    return 'r';
334
1.32k
  case '"':
335
1.32k
    return '"';
336
1.82k
  case '\\':
337
1.82k
    return '\\';
338
323k
  }
339
0
  return '\0'; /* should not happen */
340
323k
}
341
342
/* convert 'origbuf' to JSON-escaped form 'escbuf'; 'origbuf' should only
343
   include characters mappable by json(paranoid)lentable */
344
static void encodejsonescape(char *escbuf, Py_ssize_t esclen,
345
                             const char *origbuf, Py_ssize_t origlen,
346
                             bool paranoid)
347
270
{
348
270
  const uint8_t *lentable =
349
270
      (paranoid) ? jsonparanoidlentable : jsonlentable;
350
270
  Py_ssize_t i, j;
351
352
27.6M
  for (i = 0, j = 0; i < origlen; i++) {
353
27.6M
    char c = origbuf[i];
354
27.6M
    uint8_t l = lentable[(unsigned char)c];
355
27.6M
    assert(j + l <= esclen);
356
27.6M
    switch (l) {
357
1.36M
    case 1:
358
1.36M
      escbuf[j] = c;
359
1.36M
      break;
360
323k
    case 2:
361
323k
      escbuf[j] = '\\';
362
323k
      escbuf[j + 1] = jsonescapechar2(c);
363
323k
      break;
364
25.9M
    case 6:
365
25.9M
      memcpy(escbuf + j, "\\u00", 4);
366
25.9M
      escbuf[j + 4] = hexchartable[(unsigned char)c >> 4];
367
25.9M
      escbuf[j + 5] = hexchartable[(unsigned char)c & 0xf];
368
25.9M
      break;
369
27.6M
    }
370
27.6M
    j += l;
371
27.6M
  }
372
270
}
373
374
PyObject *jsonescapeu8fast(PyObject *self, PyObject *args)
375
350
{
376
350
  PyObject *origstr, *escstr;
377
350
  const char *origbuf;
378
350
  Py_ssize_t origlen, esclen;
379
350
  int paranoid;
380
350
  if (!PyArg_ParseTuple(args, "O!i:jsonescapeu8fast", &PyBytes_Type,
381
350
                        &origstr, &paranoid)) {
382
0
    return NULL;
383
0
  }
384
385
350
  origbuf = PyBytes_AS_STRING(origstr);
386
350
  origlen = PyBytes_GET_SIZE(origstr);
387
350
  esclen = jsonescapelen(origbuf, origlen, paranoid);
388
350
  if (esclen < 0) {
389
54
    return NULL; /* unsupported char found or overflow */
390
54
  }
391
296
  if (origlen == esclen) {
392
26
    Py_INCREF(origstr);
393
26
    return origstr;
394
26
  }
395
396
270
  escstr = PyBytes_FromStringAndSize(NULL, esclen);
397
270
  if (!escstr) {
398
0
    return NULL;
399
0
  }
400
270
  encodejsonescape(PyBytes_AS_STRING(escstr), esclen, origbuf, origlen,
401
270
                   paranoid);
402
403
270
  return escstr;
404
270
}