From 72604f88b998609776e4ecedbd6f9c018bd82542 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 18 Jun 2026 18:57:35 -0400 Subject: [PATCH] ext/ftp: preserve bare CR bytes in ftp_get() ASCII mode In ASCII mode ftp_get() stripped every '\r' and emitted only a following '\n', dropping bare CR bytes not part of a CRLF sequence. Fold CRLF to LF but write a lone '\r' through unchanged, carrying a '\r' on the final byte of a read into the next read and flushing it at EOF, so the buffer boundary behaves the same as the in-buffer case. ftp_nb_get() already does this via the lastch carry in ftp_nb_continue_read(). --- ext/ftp/ftp.c | 31 +++++++++++++++++++---- ext/ftp/tests/ftp_get_ascii_bare_cr.phpt | 32 ++++++++++++++++++++++++ ext/ftp/tests/server.inc | 14 +++++++++++ 3 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 ext/ftp/tests/ftp_get_ascii_bare_cr.phpt diff --git a/ext/ftp/ftp.c b/ext/ftp/ftp.c index 1b1af31bb0c9..900f26804b74 100644 --- a/ext/ftp/ftp.c +++ b/ext/ftp/ftp.c @@ -850,6 +850,7 @@ bool ftp_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const size_ goto bail; } + bool pending_cr = false; while ((rcvd = my_recv(ftp, data->fd, data->buf, FTP_BUFSIZE))) { if (rcvd == (size_t)-1) { goto bail; @@ -869,13 +870,30 @@ bool ftp_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const size_ php_stream_write(outstream, ptr, (e - ptr)); ptr = e; #else - while (e > ptr && (s = memchr(ptr, '\r', (e - ptr)))) { - php_stream_write(outstream, ptr, (s - ptr)); - if (s + 1 < e && *(s + 1) == '\n') { - s++; + if (pending_cr) { + pending_cr = false; + if (*ptr == '\n') { php_stream_putc(outstream, '\n'); + ptr++; + } else { + php_stream_putc(outstream, '\r'); + } + } + while (e > ptr && (s = memchr(ptr, '\r', (e - ptr)))) { + if (s + 1 < e) { + if (*(s + 1) == '\n') { + php_stream_write(outstream, ptr, (s - ptr)); + php_stream_putc(outstream, '\n'); + ptr = s + 2; + } else { + php_stream_write(outstream, ptr, (s - ptr) + 1); + ptr = s + 1; + } + } else { + php_stream_write(outstream, ptr, (s - ptr)); + pending_cr = true; + ptr = s + 1; } - ptr = s + 1; } #endif if (ptr < e) { @@ -885,6 +903,9 @@ bool ftp_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const size_ goto bail; } } + if (pending_cr) { + php_stream_putc(outstream, '\r'); + } data_close(ftp); diff --git a/ext/ftp/tests/ftp_get_ascii_bare_cr.phpt b/ext/ftp/tests/ftp_get_ascii_bare_cr.phpt new file mode 100644 index 000000000000..1fe92bf8de7e --- /dev/null +++ b/ext/ftp/tests/ftp_get_ascii_bare_cr.phpt @@ -0,0 +1,32 @@ +--TEST-- +ftp_get() ASCII mode: bare CR is preserved, CRLF folds to LF +--EXTENSIONS-- +ftp +pcntl +--FILE-- + +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc index 04e2ceefa278..251536ff8202 100644 --- a/ext/ftp/tests/server.inc +++ b/ext/ftp/tests/server.inc @@ -398,6 +398,20 @@ if ($pid) { fputs($fs, str_repeat("A", 4095) . "\r\n" . str_repeat("B", 10)); fputs($s, "226 Closing data Connection.\r\n"); break; + case "bare_cr": + // A bare CR (not part of CRLF) mid-stream, plus a bare CR on + // the final byte of the first FTP_BUFSIZE (4096) read followed + // by a non-LF byte in the next read. + fputs($s, "150 File status okay; about to open data connection.\r\n"); + fputs($fs, "line1\r\nba\rre\r\nend" . str_repeat("X", 4078) . "\r" . str_repeat("Y", 10)); + fputs($s, "226 Closing data Connection.\r\n"); + break; + case "trailing_cr": + // The whole transfer ends on a bare CR. + fputs($s, "150 File status okay; about to open data connection.\r\n"); + fputs($fs, "trail\r"); + fputs($s, "226 Closing data Connection.\r\n"); + break; default: fputs($s, "550 {$matches[1]}: No such file or directory \r\n");