|
10 | 10 |
|
11 | 11 | /* Standard definitions */ |
12 | 12 | #include "Python.h" |
| 13 | +#include "pycore_fileutils.h" // _Py_open_noraise() |
13 | 14 | #include "pycore_pyatomic_ft_wrappers.h" |
14 | 15 | #include "pycore_pylifecycle.h" // _Py_SetLocaleFromEnv() |
15 | 16 |
|
16 | 17 | #include <errno.h> // errno |
| 18 | +#include <pwd.h> // getpwuid_r() |
17 | 19 | #include <signal.h> // SIGWINCH |
18 | 20 | #include <stdlib.h> // free() |
19 | 21 | #include <string.h> // strdup() |
| 22 | +#ifdef HAVE_FCNTL_H |
| 23 | +# include <fcntl.h> // O_RDWR |
| 24 | +#endif |
20 | 25 | #ifdef HAVE_SYS_SELECT_H |
21 | 26 | # include <sys/select.h> // select() |
22 | 27 | #endif |
| 28 | +#ifdef HAVE_SYS_STAT_H |
| 29 | +# include <sys/stat.h> // fstat() |
| 30 | +#endif |
| 31 | +#ifdef HAVE_UNISTD_H |
| 32 | +# include <unistd.h> // ftruncate() |
| 33 | +#endif |
23 | 34 |
|
24 | 35 | #if defined(HAVE_SETLOCALE) |
25 | 36 | /* GNU readline() mistakenly sets the LC_CTYPE locale. |
@@ -319,6 +330,176 @@ readline_read_history_file_impl(PyObject *module, PyObject *filename_obj) |
319 | 330 |
|
320 | 331 | static int _history_length = -1; /* do not truncate history by default */ |
321 | 332 |
|
| 333 | +#ifndef __APPLE__ |
| 334 | +/* macOS libedit carries a patch fixing the bug this works around. */ |
| 335 | +/* Return libedit's default history file, "~/.history". This must resolve |
| 336 | + * the same file as libedit's private _default_history_file() |
| 337 | + * (https://cvsweb.netbsd.org/bsdweb.cgi/src/lib/libedit/readline.c) so that we |
| 338 | + * truncate the same file write_history() and append_history() just wrote: the |
| 339 | + * home directory comes from the password database, $HOME is not consulted. |
| 340 | + * The result must be freed with PyMem_RawFree(). */ |
| 341 | +static char * |
| 342 | +_py_libedit_default_history_file(void) |
| 343 | +{ |
| 344 | + struct passwd *pw = NULL; |
| 345 | + char *path = NULL; |
| 346 | +#ifdef HAVE_GETPWUID_R |
| 347 | + struct passwd pwd; |
| 348 | + char *buf = NULL; |
| 349 | + Py_ssize_t bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); |
| 350 | + if (bufsize == -1) { |
| 351 | + bufsize = 1024; |
| 352 | + } |
| 353 | + for (;;) { |
| 354 | + char *newbuf = PyMem_RawRealloc(buf, bufsize); |
| 355 | + if (newbuf == NULL) { |
| 356 | + break; |
| 357 | + } |
| 358 | + buf = newbuf; |
| 359 | + int status = getpwuid_r(getuid(), &pwd, buf, bufsize, &pw); |
| 360 | + if (status == 0) { |
| 361 | + break; |
| 362 | + } |
| 363 | + pw = NULL; |
| 364 | + if (status != ERANGE || bufsize > (PY_SSIZE_T_MAX >> 1)) { |
| 365 | + break; |
| 366 | + } |
| 367 | + bufsize <<= 1; |
| 368 | + } |
| 369 | +#else |
| 370 | + pw = getpwuid(getuid()); |
| 371 | +#endif |
| 372 | + if (pw != NULL && pw->pw_dir != NULL) { |
| 373 | + size_t len = strlen(pw->pw_dir) + sizeof("/.history"); |
| 374 | + path = PyMem_RawMalloc(len); |
| 375 | + if (path != NULL) { |
| 376 | + PyOS_snprintf(path, len, "%s/.history", pw->pw_dir); |
| 377 | + } |
| 378 | + } |
| 379 | +#ifdef HAVE_GETPWUID_R |
| 380 | + PyMem_RawFree(buf); |
| 381 | +#endif |
| 382 | + return path; |
| 383 | +} |
| 384 | + |
| 385 | +/* libedit's history_truncate_file() keeps the last nlines lines of the |
| 386 | + * file, which drops the "_HiStOrY_V2_" header line that its own |
| 387 | + * write_history() emits and that its read_history() requires: once |
| 388 | + * truncated, the file can no longer be loaded (gh-123018). Truncate the |
| 389 | + * file ourselves and keep the header. |
| 390 | + * |
| 391 | + * Upstream libedit (NetBSD, and the portable releases from thrysoee.dk) |
| 392 | + * still has this bug. Apple's libedit fork patches its |
| 393 | + * history_truncate_file() to copy the first line, so this workaround is |
| 394 | + * not compiled on macOS. We only keep the header when it is actually |
| 395 | + * there, so a file without one (e.g. written by GNU readline) is not given |
| 396 | + * a bogus header. |
| 397 | + * |
| 398 | + * History files are small, so the whole file is read into memory and |
| 399 | + * rewritten in place, as libedit does. GNU readline instead writes a |
| 400 | + * temporary file and renames it over the original. */ |
| 401 | +static int |
| 402 | +_py_libedit_history_truncate_file(const char *filename, int nlines) |
| 403 | +{ |
| 404 | + static const char cookie[] = "_HiStOrY_V2_\n"; |
| 405 | + const size_t cookie_len = sizeof(cookie) - 1; |
| 406 | + char *default_file = NULL; |
| 407 | + char *buf = NULL; |
| 408 | + FILE *fp = NULL; |
| 409 | + int ret = -1; |
| 410 | + |
| 411 | + if (filename == NULL) { |
| 412 | + default_file = _py_libedit_default_history_file(); |
| 413 | + if (default_file == NULL) { |
| 414 | + return -1; |
| 415 | + } |
| 416 | + filename = default_file; |
| 417 | + } |
| 418 | + int fd = _Py_open_noraise(filename, O_RDWR); |
| 419 | + if (fd < 0) { |
| 420 | + goto done; |
| 421 | + } |
| 422 | + fp = fdopen(fd, "r+"); |
| 423 | + if (fp == NULL) { |
| 424 | + close(fd); |
| 425 | + goto done; |
| 426 | + } |
| 427 | + struct stat st; |
| 428 | + if (fstat(fd, &st) != 0) { |
| 429 | + goto done; |
| 430 | + } |
| 431 | + size_t size = (size_t)st.st_size; |
| 432 | + buf = PyMem_RawMalloc(size); |
| 433 | + if (buf == NULL || fread(buf, 1, size, fp) != size) { |
| 434 | + goto done; |
| 435 | + } |
| 436 | + |
| 437 | + size_t header = 0; |
| 438 | + if (size >= cookie_len && memcmp(buf, cookie, cookie_len) == 0) { |
| 439 | + header = cookie_len; |
| 440 | + } |
| 441 | + const char *end = buf + size; |
| 442 | + |
| 443 | + /* Count the lines following the header. */ |
| 444 | + size_t total = 0; |
| 445 | + for (const char *p = buf + header; |
| 446 | + (p = memchr(p, '\n', end - p)) != NULL; |
| 447 | + p++) { |
| 448 | + total++; |
| 449 | + } |
| 450 | + if (end > buf + header && end[-1] != '\n') { |
| 451 | + total++; /* an unterminated last line */ |
| 452 | + } |
| 453 | + if (total <= (size_t)nlines) { |
| 454 | + ret = 0; /* nothing to drop */ |
| 455 | + goto done; |
| 456 | + } |
| 457 | + |
| 458 | + /* Skip the leading lines, keeping the header and the last nlines. */ |
| 459 | + const char *tail = buf + header; |
| 460 | + for (size_t skip = total - (size_t)nlines; skip > 0; skip--) { |
| 461 | + const char *nl = memchr(tail, '\n', end - tail); |
| 462 | + if (nl == NULL) { |
| 463 | + tail = end; /* the unterminated last line is dropped too */ |
| 464 | + break; |
| 465 | + } |
| 466 | + tail = nl + 1; |
| 467 | + } |
| 468 | + size_t taillen = end - tail; |
| 469 | + if (fseek(fp, 0, SEEK_SET) != 0 |
| 470 | + || fwrite(buf, 1, header, fp) != header |
| 471 | + || fwrite(tail, 1, taillen, fp) != taillen |
| 472 | + || fflush(fp) != 0 |
| 473 | + || ftruncate(fd, (off_t)(header + taillen)) != 0) { |
| 474 | + goto done; |
| 475 | + } |
| 476 | + ret = 0; |
| 477 | + |
| 478 | +done: |
| 479 | + if (fp != NULL) { |
| 480 | + fclose(fp); |
| 481 | + } |
| 482 | + PyMem_RawFree(buf); |
| 483 | + PyMem_RawFree(default_file); |
| 484 | + return ret; |
| 485 | +} |
| 486 | +#endif /* !__APPLE__ */ |
| 487 | + |
| 488 | +/* Truncate the history file after write_history() or append_history(). |
| 489 | + * Like the history_truncate_file() calls this replaces, failures are |
| 490 | + * ignored by the callers: the history was already saved successfully and |
| 491 | + * a failed truncation only leaves the file longer than requested. */ |
| 492 | +static int |
| 493 | +_py_history_truncate_file(const char *filename, int nlines) |
| 494 | +{ |
| 495 | +#ifndef __APPLE__ |
| 496 | + if (using_libedit_emulation) { |
| 497 | + return _py_libedit_history_truncate_file(filename, nlines); |
| 498 | + } |
| 499 | +#endif |
| 500 | + return history_truncate_file(filename, nlines); |
| 501 | +} |
| 502 | + |
322 | 503 | /* Exported function to save a readline history file */ |
323 | 504 |
|
324 | 505 | /*[clinic input] |
@@ -360,7 +541,7 @@ readline_write_history_file_impl(PyObject *module, PyObject *filename_obj) |
360 | 541 | errno = err = write_history(filename); |
361 | 542 | int history_length = FT_ATOMIC_LOAD_INT_RELAXED(_history_length); |
362 | 543 | if (!err && history_length >= 0) |
363 | | - history_truncate_file(filename, history_length); |
| 544 | + _py_history_truncate_file(filename, history_length); |
364 | 545 | Py_XDECREF(filename_bytes); |
365 | 546 | errno = err; |
366 | 547 | if (errno) |
@@ -419,7 +600,7 @@ readline_append_history_file_impl(PyObject *module, int nelements, |
419 | 600 | nelements - libedit_append_replace_history_offset, filename); |
420 | 601 | int history_length = FT_ATOMIC_LOAD_INT_RELAXED(_history_length); |
421 | 602 | if (!err && history_length >= 0) |
422 | | - history_truncate_file(filename, history_length); |
| 603 | + _py_history_truncate_file(filename, history_length); |
423 | 604 | Py_XDECREF(filename_bytes); |
424 | 605 | errno = err; |
425 | 606 | if (errno) |
|
0 commit comments