max = (int) $max; } /** * Feeds bytes; returns the statements completed so far. * * @param string $bytes Input. * @param bool $final No more input will follow. * @return string[] * @throws \RuntimeException On unterminated input or oversized statements. */ public function feed( $bytes, $final = false ) { $this->buf .= $bytes; $out = array(); $len = strlen( $this->buf ); $stops = "'\"`#-/" . $this->delim[0]; while ( $this->pos < $len ) { switch ( $this->mode ) { case self::N: // DELIMITER lines only matter at the very start of a statement. if ( $this->pos === $this->start ) { $ws = strspn( $this->buf, " \t\r\n", $this->pos ); if ( $this->pos + $ws >= $len && ! $final ) { break 2; // Only whitespace so far: wait for more. } $line_end = strpos( $this->buf, "\n", $this->pos + $ws ); if ( false === $line_end && ! $final && 0 === strncasecmp( substr( $this->buf, $this->pos + $ws, 9 ), 'DELIMITER', min( 9, $len - $this->pos - $ws ) ) ) { break 2; // Might be a DELIMITER line: need all of it. } if ( preg_match( '/\G\s*DELIMITER\s+(\S+)[ \t]*(\r?\n|$)/i', $this->buf, $m, 0, $this->pos ) ) { $this->delim = $m[1]; $stops = "'\"`#-/" . $this->delim[0]; $this->pos += strlen( $m[0] ); $this->start = $this->pos; continue 2; } $this->pos += $ws; $this->start = $this->pos; if ( $this->pos >= $len ) { break 2; } } $this->pos += strcspn( $this->buf, $stops, $this->pos ); if ( $this->pos >= $len ) { break 2; } $c = $this->buf[ $this->pos ]; if ( "'" === $c ) { $this->mode = self::SQ; ++$this->pos; } elseif ( '"' === $c ) { $this->mode = self::DQ; ++$this->pos; } elseif ( '`' === $c ) { $this->mode = self::BT; ++$this->pos; } elseif ( '#' === $c ) { $this->mode = self::LINE; ++$this->pos; } elseif ( '-' === $c ) { if ( $this->pos + 2 >= $len && ! $final ) { break 2; // Need lookahead. } $next = substr( $this->buf, $this->pos + 1, 2 ); if ( '-' === ( $next[0] ?? '' ) && ( ! isset( $next[1] ) || ctype_space( $next[1] ) ) ) { $this->mode = self::LINE; $this->pos += 2; } else { ++$this->pos; } } elseif ( '/' === $c ) { if ( $this->pos + 1 >= $len && ! $final ) { break 2; } if ( '*' === ( $this->buf[ $this->pos + 1 ] ?? '' ) ) { $this->mode = self::BLOCK; $this->pos += 2; } else { ++$this->pos; } } elseif ( substr( $this->buf, $this->pos, strlen( $this->delim ) ) === $this->delim ) { $stmt = trim( substr( $this->buf, $this->start, $this->pos - $this->start ) ); if ( '' !== $stmt && ! self::only_comments( $stmt ) ) { $out[] = $stmt; } $this->pos += strlen( $this->delim ); $this->start = $this->pos; } elseif ( $this->pos + strlen( $this->delim ) > $len && ! $final ) { break 2; // Could be the start of a multi-character delimiter. } else { ++$this->pos; } break; case self::SQ: case self::DQ: case self::BT: $q = self::SQ === $this->mode ? "'" : ( self::DQ === $this->mode ? '"' : '`' ); $this->pos += strcspn( $this->buf, self::BT === $this->mode ? '`' : $q . '\\', $this->pos ); if ( $this->pos >= $len ) { break 2; } if ( '\\' === $this->buf[ $this->pos ] ) { if ( $this->pos + 1 >= $len && ! $final ) { break 2; } $this->pos += 2; break; } // Closing quote, unless doubled ('' "" ``). if ( $this->pos + 1 >= $len && ! $final ) { break 2; } if ( ( $this->buf[ $this->pos + 1 ] ?? '' ) === $q ) { $this->pos += 2; } else { $this->mode = self::N; ++$this->pos; } break; case self::LINE: $nl = strpos( $this->buf, "\n", $this->pos ); if ( false === $nl ) { $this->pos = $len; break 2; } $this->pos = $nl + 1; $this->mode = self::N; break; case self::BLOCK: $end = strpos( $this->buf, '*/', $this->pos ); if ( false === $end ) { $this->pos = max( $this->pos, $len - 1 ); break 2; } $this->pos = $end + 2; $this->mode = self::N; break; } if ( $this->pos - $this->start > $this->max ) { throw new \RuntimeException( 'A SQL statement is larger than ' . size_format( $this->max ) . '.' ); } } // Drop consumed input so memory stays bounded. if ( $this->start > 1048576 || $this->start === $len ) { $this->buf = (string) substr( $this->buf, $this->start ); $this->pos -= $this->start; $this->start = 0; } if ( $final ) { if ( ! in_array( $this->mode, array( self::N, self::LINE ), true ) ) { throw new \RuntimeException( 'The SQL ends inside a quoted string or comment.' ); } $rest = trim( substr( $this->buf, $this->start ) ); if ( '' !== $rest && ! self::only_comments( $rest ) ) { $out[] = $rest; } $this->buf = ''; $this->pos = 0; $this->start = 0; } return $out; } /** * Whether text is only comments and whitespace (not executable /*! comments). * * @param string $sql Text. */ private static function only_comments( $sql ) { // A statement that starts with code is never only comments. This also keeps the regex // below off large INSERTs, where it can hit PCRE's backtrack limit (for example with // "/*" inside quoted data) and return null, which once dropped whole statements. $first = substr( ltrim( $sql ), 0, 1 ); if ( '' === $first ) { return true; } if ( '/' !== $first && '-' !== $first && '#' !== $first ) { return false; } $stripped = preg_replace( '~/\*(?!!).*?\*/|--[ \t][^\n]*|#[^\n]*~s', '', $sql ); if ( null === $stripped ) { return false; // Can't tell: keep it (the database will reject it if it really is empty). } return '' === trim( $stripped ); } }