tvl-depot/third_party/lisp/mime4cl/streams.lisp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

356 lines
14 KiB
Common Lisp
Raw Normal View History

;;; streams.lisp --- En/De-coding Streams
;;; Copyright (C) 2012 by Walter C. Pelissero
;;; Copyright (C) 2021-2022 by the TVL Authors
;;; Author: Walter C. Pelissero <walter@pelissero.de>
;;; Project: mime4cl
;;; This library is free software; you can redistribute it and/or
;;; modify it under the terms of the GNU Lesser General Public License
;;; as published by the Free Software Foundation; either version 2.1
;;; of the License, or (at your option) any later version.
;;; This library is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;;; Lesser General Public License for more details.
;;; You should have received a copy of the GNU Lesser General Public
;;; License along with this library; if not, write to the Free
;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
;;; 02111-1307 USA
(in-package :mime4cl)
(defclass coder-stream-mixin ()
((real-stream :type stream
:initarg :stream
:reader real-stream)
(dont-close :initform nil
:initarg :dont-close)))
(defmethod stream-file-position ((stream coder-stream-mixin))
(file-position (slot-value stream 'real-stream)))
(defmethod (setf stream-file-position) (newval (stream coder-stream-mixin))
(file-position (slot-value stream 'real-stream) newval))
(defclass coder-input-stream-mixin (fundamental-binary-input-stream coder-stream-mixin)
())
(defclass coder-output-stream-mixin (fundamental-binary-output-stream coder-stream-mixin)
())
(defclass quoted-printable-decoder-stream (coder-input-stream-mixin quoted-printable-decoder) ())
(defclass base64-decoder-stream (coder-input-stream-mixin base64-decoder) ())
(defclass 8bit-decoder-stream (coder-input-stream-mixin 8bit-decoder) ())
(defclass quoted-printable-encoder-stream (coder-output-stream-mixin quoted-printable-encoder) ())
(defclass base64-encoder-stream (coder-output-stream-mixin base64-encoder) ())
(defclass 8bit-encoder-stream (coder-output-stream-mixin 8bit-encoder) ())
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod initialize-instance :after ((stream coder-stream-mixin) &key &allow-other-keys)
(unless (slot-boundp stream 'real-stream)
(error "REAL-STREAM is unbound. Must provide a :STREAM argument.")))
(defmethod initialize-instance ((stream coder-output-stream-mixin) &key &allow-other-keys)
(call-next-method)
(unless (slot-boundp stream 'output-function)
(setf (slot-value stream 'output-function)
#'(lambda (char)
(write-char char (slot-value stream 'real-stream))))))
(defmethod initialize-instance ((stream coder-input-stream-mixin) &key &allow-other-keys)
(call-next-method)
(unless (slot-boundp stream 'input-function)
(setf (slot-value stream 'input-function)
#'(lambda ()
(read-char (slot-value stream 'real-stream) nil)))))
(defmethod stream-read-byte ((stream coder-input-stream-mixin))
(or (decoder-read-byte stream)
:eof))
(defmethod stream-write-byte ((stream coder-output-stream-mixin) byte)
(encoder-write-byte stream byte))
(defmethod close ((stream coder-stream-mixin) &key abort)
(with-slots (real-stream dont-close) stream
(unless dont-close
(close real-stream :abort abort))))
(defmethod close ((stream coder-output-stream-mixin) &key abort)
(unless abort
(encoder-finish-output stream))
(call-next-method))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass encoder-input-stream (fundamental-character-input-stream coder-stream-mixin)
((encoder)
(buffer-queue :initform (make-queue)))
(:documentation
"This is the base class for encoders with the direction swapped. It
reads from REAL-STREAM a stream of bytes, encodes it and returnes it
in a stream of character."))
(defclass quoted-printable-encoder-input-stream (encoder-input-stream) ())
(defclass base64-encoder-input-stream (encoder-input-stream) ())
(defclass 8bit-encoder-input-stream (fundamental-character-input-stream coder-stream-mixin) ())
(defmethod initialize-instance ((stream quoted-printable-encoder-input-stream) &key &allow-other-keys)
(call-next-method)
(with-slots (encoder buffer-queue) stream
(setf encoder
(make-instance 'quoted-printable-encoder
:output-function #'(lambda (char)
(queue-append buffer-queue char))))))
(defmethod initialize-instance ((stream base64-encoder-input-stream) &key &allow-other-keys)
(call-next-method)
(with-slots (encoder buffer-queue) stream
(setf encoder
(make-instance 'base64-encoder
:output-function #'(lambda (char)
(queue-append buffer-queue char))))))
(defmethod stream-read-char ((stream encoder-input-stream))
(with-slots (encoder buffer-queue real-stream) stream
(loop
while (queue-empty-p buffer-queue)
do (be byte (read-byte real-stream nil)
(if byte
(encoder-write-byte encoder byte)
(progn
(encoder-finish-output encoder)
(queue-append buffer-queue :eof)))))
(queue-pop buffer-queue)))
(defmethod stream-read-char ((stream 8bit-encoder-input-stream))
(with-slots (real-stream) stream
(aif (read-byte real-stream nil)
(code-char it)
:eof)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass input-adapter-stream ()
((source :initarg :source)
(real-stream)
(input-function)))
(defclass binary-input-adapter-stream (fundamental-binary-input-stream input-adapter-stream) ())
(defclass character-input-adapter-stream (fundamental-character-input-stream input-adapter-stream) ())
(defmethod stream-element-type ((stream binary-input-adapter-stream))
'(unsigned-byte 8))
(defmethod initialize-instance ((stream input-adapter-stream) &key &allow-other-keys)
(call-next-method)
(assert (slot-boundp stream 'source)))
(defmethod initialize-instance ((stream binary-input-adapter-stream) &key &allow-other-keys)
(call-next-method)
;; REAL-STREAM slot is set only if we are going to close it later on
(with-slots (source real-stream input-function) stream
(etypecase source
(string
(setf real-stream (make-string-input-stream source)
input-function #'(lambda ()
(awhen (read-char real-stream nil)
(char-code it)))))
((vector (unsigned-byte 8))
(be i 0
(setf input-function #'(lambda ()
(when (< i (length source))
(prog1 (aref source i)
(incf i)))))))
(stream
(assert (input-stream-p source))
(setf input-function (if (subtypep (stream-element-type source) 'character)
#'(lambda ()
(awhen (read-char source nil)
(char-code it)))
#'(lambda ()
(read-byte source nil)))))
(pathname
(setf real-stream (open source :element-type '(unsigned-byte 8))
input-function #'(lambda ()
(read-byte real-stream nil))))
(file-portion
(setf real-stream (open-decoded-file-portion source)
input-function #'(lambda ()
(read-byte real-stream nil)))))))
(defmethod initialize-instance ((stream character-input-adapter-stream) &key &allow-other-keys)
(call-next-method)
;; REAL-STREAM slot is set only if we are going to close later on
(with-slots (source real-stream input-function) stream
(etypecase source
(string
(setf real-stream (make-string-input-stream source)
input-function #'(lambda ()
(read-char real-stream nil))))
((vector (unsigned-byte 8))
(be i 0
(setf input-function #'(lambda ()
(when (< i (length source))
(prog1 (code-char (aref source i))
(incf i)))))))
(stream
(assert (input-stream-p source))
(setf input-function (if (subtypep (stream-element-type source) 'character)
#'(lambda ()
(read-char source nil))
#'(lambda ()
(awhen (read-byte source nil)
(code-char it))))))
(pathname
(setf real-stream (open source :element-type 'character)
input-function #'(lambda ()
(read-char real-stream nil))))
(file-portion
(setf real-stream (open-decoded-file-portion source)
input-function #'(lambda ()
(awhen (read-byte real-stream nil)
(code-char it))))))))
(defmethod close ((stream input-adapter-stream) &key abort)
(when (slot-boundp stream 'real-stream)
(with-slots (real-stream) stream
(close real-stream :abort abort))))
(defmethod stream-read-byte ((stream binary-input-adapter-stream))
(with-slots (input-function) stream
(or (funcall input-function)
:eof)))
(defmethod stream-read-char ((stream character-input-adapter-stream))
(with-slots (input-function) stream
(or (funcall input-function)
:eof)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass delimited-input-stream (fundamental-character-input-stream coder-stream-mixin)
((start-offset :initarg :start
:initform 0
:reader stream-start
:type integer)
(end-offset :initarg :end
:initform nil
:reader stream-end
:type (or null integer))
(current-offset :type integer)))
(defmethod print-object ((object delimited-input-stream) stream)
(if *print-readably*
(call-next-method)
(with-slots (start-offset end-offset) object
(print-unreadable-object (object stream :type t :identity t)
(format stream "start=~A end=~A" start-offset end-offset)))))
(defun base-stream (stream)
(if (typep stream 'delimited-input-stream)
(base-stream (real-stream stream))
stream))
(defmethod initialize-instance ((stream delimited-input-stream) &key &allow-other-keys)
(call-next-method)
(unless (slot-boundp stream 'real-stream)
(error "REAL-STREAM is unbound. Must provide a :STREAM argument."))
(with-slots (start-offset) stream
(file-position stream start-offset)))
(defmethod (setf stream-file-position) (newval (stream delimited-input-stream))
(with-slots (current-offset real-stream) stream
(setf current-offset newval)
(call-next-method)))
(defmethod stream-file-position ((stream delimited-input-stream))
(slot-value stream 'current-offset))
;; Calling file-position with SBCL on every read is quite expensive, since
;; it will invoke lseek each time. This is so expensive that it's faster to
;; /compute/ the amount the stream got advanced by.
;; file-position's behavior however, is quite flexible and it behaves differently
;; not only for different implementation, but also different streams in SBCL.
;; Thus, we should ideally go back to file-position and try to reduce the amount
;; of calls by using read-sequence.
;; TODO(sterni): make decoders use read-sequence and drop offset tracking code
(macrolet ((def-stream-read (name read-fun update-offset-form)
`(defmethod ,name ((stream delimited-input-stream))
(with-slots (real-stream end-offset current-offset) stream
(let ((el (if (or (not end-offset)
(< current-offset end-offset))
(or (,read-fun real-stream nil)
:eof)
:eof)))
(setf current-offset ,update-offset-form)
el)))))
;; Assume we are using an encoding where < 128 is one byte, in all other cases
;; it's hard to guess how much file-position will increase
(def-stream-read stream-read-char read-char
(if (or (eq el :eof) (< (char-code el) 128))
(1+ current-offset)
(file-position real-stream)))
(def-stream-read stream-read-byte read-byte (1+ current-offset)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass my-string-input-stream (fundamental-character-input-stream coder-stream-mixin)
((string :initarg :string
:reader stream-string)))
(defmethod initialize-instance ((stream my-string-input-stream) &key &allow-other-keys)
(call-next-method)
(assert (slot-boundp stream 'string))
(with-slots (string real-stream) stream
(setf real-stream (make-string-input-stream string))))
(defmethod stream-read-char ((stream my-string-input-stream))
(with-slots (real-stream) stream
(or (read-char real-stream nil)
:eof)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defstruct file-portion
data ; string or a pathname
encoding
start
end)
(defun open-file-portion (file-portion)
(be data (file-portion-data file-portion)
(etypecase data
(pathname
(be stream (open data)
(make-instance 'delimited-input-stream
:stream stream
:start (file-portion-start file-portion)
:end (file-portion-end file-portion))))
(string
(make-instance 'delimited-input-stream
:stream (make-string-input-stream data)
:start (file-portion-start file-portion)
:end (file-portion-end file-portion)))
(stream
(make-instance 'delimited-input-stream
:stream data
:dont-close t
:start (file-portion-start file-portion)
:end (file-portion-end file-portion))))))
(defun open-decoded-file-portion (file-portion)
(make-instance (case (file-portion-encoding file-portion)
(:quoted-printable 'quoted-printable-decoder-stream)
(:base64 'base64-decoder-stream)
(t '8bit-decoder-stream))
:stream (open-file-portion file-portion)))