@@ -394,23 +394,75 @@ private function _filename_from_subject($str)
394394 }
395395}
396396
397+ /**
398+ * Unfortunately a body line which could be mistaken for an Mbox header,
399+ * and which therefore must be escaped with '>', can be split between
400+ * buckets (and between calls to filter()), so if the bucket being filtered
401+ * ends in a way that *might* be a line needing escaping, postpone placing
402+ * it in the $out brigade until we've seen the next bucket and can decide
403+ * whether or not to escape. Don't bother checking if such a line might be
404+ * split among more than 2 buckets -- given even a little buffering, this
405+ * should never happen in practice.
406+ */
397407class zipdownload_mbox_filter extends \php_user_filter
398408{
409+ private $ prev_bucket ;
410+ private $ prev_match ;
411+ private $ first = true ;
412+
399413 #[\Override]
400414 #[\ReturnTypeWillChange]
401415 public function filter ($ in , $ out , &$ consumed , $ closing )
402416 {
417+ $ out_empty = true ;
418+ $ replaced = 0 ;
419+ $ m = [];
420+
403421 while ($ bucket = stream_bucket_make_writeable ($ in )) {
404- // messages are read line by line
405- if (preg_match ('/^>*From / ' , $ bucket ->data )) {
406- $ bucket ->data = '> ' . $ bucket ->data ;
407- $ bucket ->datalen ++;
422+ // Escape lines which definitely need it
423+ $ anchor = $ this ->first ? '^ ' : "\n" ; // Only the file's first line may match w/o \n
424+ $ this ->first = false ;
425+ $ bucket ->data = preg_replace ("/( {$ anchor }>*)From /m " , '\1>From ' , $ bucket ->data , -1 , $ replaced );
426+ $ consumed += $ bucket ->datalen ;
427+ $ bucket ->datalen += $ replaced ;
428+
429+ // If the previous bucket was postponed...
430+ if ($ this ->prev_bucket ) {
431+ // Escape its last line if necessary
432+ $ joined = $ this ->prev_match . substr ($ bucket ->data , 0 , strspn ($ bucket ->data , '>From ' ));
433+ if (preg_match ('/^>*From / ' , $ joined )) {
434+ if (strlen ($ this ->prev_match )) { // prev_match == end of prev_bucket w/o \n, e.g. 'Fro' or '>>F' or ''
435+ $ this ->prev_bucket ->data = substr_replace (
436+ $ this ->prev_bucket ->data , '> ' . $ this ->prev_match , -strlen ($ this ->prev_match ));
437+ } else {
438+ $ this ->prev_bucket ->data .= '> ' ;
439+ }
440+ $ this ->prev_bucket ->datalen ++;
441+ }
442+
443+ // And in either case send it out
444+ stream_bucket_append ($ out , $ this ->prev_bucket );
445+ $ this ->prev_bucket = null ;
446+ $ out_empty = false ;
408447 }
409448
410- $ consumed += (int ) $ bucket ->datalen ;
411- stream_bucket_append ($ out , $ bucket );
449+ // Decide if the current bucket should be postponed or sent immediately
450+ if (str_contains ("\n>From " , substr ($ bucket ->data , -1 ))
451+ && preg_match ('/\G\n>*(?:F(?:r(?:om?)?)?)?\z/ ' , $ bucket ->data , $ m , 0 , strrpos ($ bucket ->data , "\n" ))
452+ ) {
453+ $ this ->prev_bucket = $ bucket ;
454+ $ this ->prev_match = substr ($ m [0 ], 1 ); // What was matched without the initial \n (could be '')
455+ } else {
456+ stream_bucket_append ($ out , $ bucket );
457+ $ out_empty = false ;
458+ }
412459 }
413460
414- return \PSFS_PASS_ON ;
461+ if ($ closing && $ this ->prev_bucket ) {
462+ stream_bucket_append ($ out , $ this ->prev_bucket );
463+ $ this ->prev_bucket = null ;
464+ $ out_empty = false ;
465+ }
466+ return $ out_empty ? \PSFS_FEED_ME : \PSFS_PASS_ON ;
415467 }
416468}
0 commit comments