cl-rw

Layered streams for Common Lisp
git clone https://logand.com/git/cl-rw.git/
Log | Files | Refs

commit c97bd9aa46d53200da7e12a92e7e532eef541ca9
parent 1f47d2a91045607faa7a39c8672a4ce9cdfea3fd
Author: Tomas Hlavaty <tom@logand.com>
Date:   Sun, 18 Aug 2013 00:31:06 +0200

base64 email filesystem.lisp and xml readers/writers added

Diffstat:
Abase64.lisp | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcl-rw.asd | 6+++++-
Aemail.lisp | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afilesystem.lisp | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Axml.lisp | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 414 insertions(+), 1 deletion(-)

diff --git a/base64.lisp b/base64.lisp @@ -0,0 +1,80 @@ +;;; Copyright (C) 2013 Tomas Hlavaty <tom@logand.com> +;;; +;;; Permission is hereby granted, free of charge, to any person +;;; obtaining a copy of this software and associated documentation +;;; files (the "Software"), to deal in the Software without +;;; restriction, including without limitation the rights to use, copy, +;;; modify, merge, publish, distribute, sublicense, and/or sell copies +;;; of the Software, and to permit persons to whom the Software is +;;; furnished to do so, subject to the following conditions: +;;; +;;; The above copyright notice and this permission notice shall be +;;; included in all copies or substantial portions of the Software. +;;; +;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +;;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +;;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +;;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +;;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +;;; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;;; DEALINGS IN THE SOFTWARE. + +(defpackage :rw.base64 + (:use :cl) + (:export :encode-reader + :decode-reader)) + +(in-package :rw.base64) + +(defun encode-reader (reader &optional table wrap) ;; TODO wrap 76 + (let (pending + (table (or table + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"))) + (lambda () + (cond + (pending (pop pending)) + ((not (rw:peek reader)) nil) + (t + (flet ((%next () + (let ((x (rw:next reader))) + (when x + (logand #xff (if (characterp x) (char-code x) x))))) + (%map (x n) + (char table (ldb (byte 6 n) x)))) + (let* ((a (%next)) + (b (%next)) + (c (%next)) + (x (+ (ash a 16) (ash (or b 0) 8) (or c 0)))) + (push (if c (%map x 0) #\=) pending) + (push (if b (%map x 6) #\=) pending) + (push (%map x 12) pending) + (%map x 18)))))))) + +(defun decode-reader (reader &optional table) ;; TODO skip newlines? + (let (pending + (table (or table + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"))) + (lambda () + (cond + (pending (pop pending)) + ((not (rw:peek reader)) nil) + (t + (flet ((%next () + (let ((x (rw:next reader))) + (unless (eql #\= x) + (position x table)))) + (%map (x n) + (code-char (ldb (byte 8 n) x)))) + (let* ((a (%next)) + (b (%next)) + (c (%next)) + (d (%next)) + (x (+ (ash a 18) (ash b 12) (ash (or c 0) 6) (or d 0)))) + (when d (push (%map x 0) pending)) + (when c (push (%map x 8) pending)) + (%map x 16)))))))) + +;;(rw:till (rw:peek-reader (decode-reader (rw:peek-reader (encode-reader (rw:peek-reader (rw:reader "any carnal pleasure."))))))) +;;(rw:till (rw:peek-reader (encode-reader (rw:peek-reader (rw:reader "any carnal pleasure."))))) +;;(rw:till (rw:peek-reader (decode-reader (rw:peek-reader (rw:reader "YW55IGNhcm5hbCBwbGVhc3VyZS4="))))) diff --git a/cl-rw.asd b/cl-rw.asd @@ -35,4 +35,8 @@ :licence "MIT" :depends-on () :serial t - :components ((:file "rw"))) + :components ((:file "rw") + (:file "filesystem") + (:file "base64") + (:file "xml") + (:file "email"))) diff --git a/email.lisp b/email.lisp @@ -0,0 +1,109 @@ +;;; Copyright (C) 2013 Tomas Hlavaty <tom@logand.com> +;;; +;;; Permission is hereby granted, free of charge, to any person +;;; obtaining a copy of this software and associated documentation +;;; files (the "Software"), to deal in the Software without +;;; restriction, including without limitation the rights to use, copy, +;;; modify, merge, publish, distribute, sublicense, and/or sell copies +;;; of the Software, and to permit persons to whom the Software is +;;; furnished to do so, subject to the following conditions: +;;; +;;; The above copyright notice and this permission notice shall be +;;; included in all copies or substantial portions of the Software. +;;; +;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +;;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +;;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +;;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +;;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +;;; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;;; DEALINGS IN THE SOFTWARE. + +(defpackage :rw.email + (:use :cl) + (:export :directory-reader + :file-reader)) + +(in-package :rw.email) + +;;(with-open-file (s "~/Mail/goethe/27") (rw:till (rw:peek-reader (rw:char-reader s)))) + +(defun header-reader (reader) + (flet ((peek () (rw:peek reader)) + (next () (rw:next reader)) + (skip () (rw:skip reader)) + (till (items) (rw:till reader items))) + (let (eof) + (lambda () + (or eof + (case (peek) + ((nil) (setq eof 'eof)) + (#\newline (next) (setq eof t)) + (t (cons + (prog1 (till '(#\space #\tab #\newline #\:)) + (assert (eql #\: (next))) + (skip)) + (with-output-to-string (s) + (flet ((line () + (write-string (till '(#\newline)) s) + (assert (eql #\newline (next))))) + (line) + (do () + ((not (member (peek) '(#\space #\tab)))) + (terpri s) + (line)))))))))))) + +(defun header-alist (reader) + (rw:till (rw:peek-reader (header-reader (rw:peek-reader (rw:char-reader reader)))))) + +(defun content-type (reader) + (flet ((peek () (rw:peek reader)) + (next () (rw:next reader)) + (skip () (rw:skip reader)) + (till (items) (rw:till reader items))) + (let ((mime (till '(#\space #\tab #\newline #\;)))) + (make + (link mime) + (assert (eql #\; (next))) + (skip) + (do () + ((not (peek))) + (link (let ((k (till '(#\space #\tab #\newline #\=)))) + (cond + ((string= "type" k) :type) + ((string= "boundary" k) :boundary) + (t (error "unknown attribute ~s of content-type ~s" k mime))))) + (assert (eql #\= (next))) + (assert (eql #\" (next))) + (link (till '(#\space #\tab #\newline #\"))) + (assert (eql #\" (next))) + (when (eql #\; (peek)) + (next) + (skip))))))) + +;; https://en.wikipedia.org/wiki/MIME#Multipart_subtypes +(defun parse-nnml-file (pathname) + (with-open-file (s pathname) + (let ((x (rw:peek-reader (rw:char-reader (cdr (assoc "Content-Type" (header-alist s) :test #'string=)))))) + (destructuring-bind (mime &key type boundary) (content-type x) + (cond + #+nil + ((string= "multipart/mixed" mime) + (list mime type boundary)) + ((string= "multipart/alternative" mime) + (list mime type boundary)) + ((string= "multipart/related" mime) + (list mime type boundary)) + #+nil + ((string= "multipart/form-data" mime) + (list mime type boundary)) + #+nil + ((string= "multipart/signed" mime) + (list mime type boundary)) + #+nil + ((string= "multipart/encrypted" mime) + (list mime type boundary)) + (t (error "unknown content-type ~s" mime))))))) + +;;(parse-nnml-file "~/Mail/goethe/27") diff --git a/filesystem.lisp b/filesystem.lisp @@ -0,0 +1,68 @@ +;;; Copyright (C) 2013 Tomas Hlavaty <tom@logand.com> +;;; +;;; Permission is hereby granted, free of charge, to any person +;;; obtaining a copy of this software and associated documentation +;;; files (the "Software"), to deal in the Software without +;;; restriction, including without limitation the rights to use, copy, +;;; modify, merge, publish, distribute, sublicense, and/or sell copies +;;; of the Software, and to permit persons to whom the Software is +;;; furnished to do so, subject to the following conditions: +;;; +;;; The above copyright notice and this permission notice shall be +;;; included in all copies or substantial portions of the Software. +;;; +;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +;;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +;;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +;;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +;;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +;;; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;;; DEALINGS IN THE SOFTWARE. + +(defpackage :rw.filesystem + (:use :cl) + (:export :directory-reader + :file-reader)) + +(in-package :rw.filesystem) + +#+nil ;; TODO already defined on ccl?! but not on sbcl? +(defun directoryp (pathname) + (equal (directory-namestring pathname) (namestring pathname))) + +(defun directory-reader (pathname &optional recurse) + (when (directoryp pathname) + (flet ((expand (x) (directory (merge-pathnames "*" #+nil "*.*" x)))) + (let ((stack (list (expand pathname)))) + (lambda () + (when stack + (let ((x (pop (car stack)))) + (unless (car stack) + (pop stack)) + (prog1 x + (when (and x recurse (directoryp x)) + (let ((y (expand x))) + (when y + (push y stack)))))))))))) + +;;(rw:till (rw:peek-reader (directory-reader "~/Mail/"))) +;;(rw:till (rw:peek-reader (directory-reader "~/News/"))) +;;(rw:till (rw:peek-reader (directory-reader "~/News/" t))) +;;(rw:till (rw:peek-reader (directory-reader "/tmp/"))) +;;(rw:till (rw:peek-reader (directory-reader "/tmp/" t))) + +#+nil +(defun directory-reader (reader) + (lambda () + (do ((x (rw:next reader) (rw:next reader))) + ((or (not x) (directoryp x)) x)))) + +#+nil +(defun file-reader (reader) + (lambda () + (do ((x (rw:next reader) (rw:next reader))) + ((or (not x) (not (directoryp x))) x)))) + +;;(till (rw:peek-reader (directory-reader (dir-reader "/tmp/" t)))) +;;(till (rw:peek-reader (file-reader (dir-reader "/tmp/" t)))) diff --git a/xml.lisp b/xml.lisp @@ -0,0 +1,152 @@ +;;; Copyright (C) 2013 Tomas Hlavaty <tom@logand.com> +;;; +;;; Permission is hereby granted, free of charge, to any person +;;; obtaining a copy of this software and associated documentation +;;; files (the "Software"), to deal in the Software without +;;; restriction, including without limitation the rights to use, copy, +;;; modify, merge, publish, distribute, sublicense, and/or sell copies +;;; of the Software, and to permit persons to whom the Software is +;;; furnished to do so, subject to the following conditions: +;;; +;;; The above copyright notice and this permission notice shall be +;;; included in all copies or substantial portions of the Software. +;;; +;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +;;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +;;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +;;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +;;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +;;; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;;; DEALINGS IN THE SOFTWARE. + +(defpackage :rw.xml + (:use :cl) + (:export :xmarkup-reader + :parse-xml)) + +(in-package :rw.xml) + +;; https://github.com/drewc/smug/blob/master/smug.org +;; http://www.htmlhelp.com/reference/wilbur/misc/comment.html + +;; TODO xml is made of bytes, not chars + +(defun parse-xml-attributes (reader finish) ;; finish='((#\/))|'((#\?)) + (flet ((peek () (rw:peek reader)) + (next () (rw:next reader)) + (skip () (rw:skip reader)) + (till (markers) (rw:till reader markers))) + (do (z) + ((eql #\> (peek)) + (assert (eql #\> (next))) + (let ((f (equal (car z) finish))) + (when f (pop z)) + (values (nreverse z) f))) + (push (cons (prog1 (till '(#\space #\tab #\newline #\> #\=)) + (skip) + (when (eql #\= (peek)) + (next) + (skip))) + (let ((q (peek))) + (when (member q '(#\" #\')) + (next) + (prog1 (till (cons q '(#\space #\tab #\newline #\>))) + (assert (eql q (next))) + (skip))))) + z)))) + +(defun xmarkup-reader (reader) ;; TODO see and move cl-parsers to cl-rw? + (flet ((peek () (rw:peek reader)) + (next () (rw:next reader)) + (skip () (rw:skip reader)) + (till (markers) (rw:till reader markers))) + (lambda () + (case (peek) + ((nil)) + (#\< + (next) + (skip) + (let ((e (till '(#\space #\tab #\newline #\>)))) + (skip) + (case (car e) ;; TODO doctype + (#\? + (multiple-value-bind (a f) + (parse-xml-attributes reader '((#\?))) + (assert f) + (cons :pi (cons (cdr e) a)))) + (#\! + (prog1 (cons :comment (till '(#\>))) ;; TODO properly + (assert (eql #\> (next))))) + (#\/ + (assert (eql #\> (next))) + (cons :end (cdr e))) + (t + (multiple-value-bind (a f) + (parse-xml-attributes reader '((#\/))) + (unless f + (when (equal '(#\/) (last e)) + (setq f t + e (nreverse (cdr (nreverse e)))))) ;; TODO better + (cons (if f :begin/ :begin) (cons e a))))))) + (t (cons :text (till '(#\<)))))))) ;; TODO entities + +(defun parse-xml (x) + (labels ((id (x) + (intern (string-upcase (concatenate 'string x)) :keyword)) + (xattrs (x) + (loop + for (f . r) in x + appending (list (id f) (concatenate 'string r)))) + (parse (r) + (do ((z (list nil)) + (r (xmarkup-reader (rw:skip r))) + a) + ((not (setq a (rw:next r))) + (let ((y (pop z))) + (assert (not z)) + (assert (not (cdr y))) + (car y))) + (ecase (car a) + (:pi) + (:comment) + (:begin/ + (let ((tag (id (cadr a))) + (attrs (xattrs (cddr a)))) + (push (list (if attrs (cons tag attrs) tag)) (car z)))) + (:begin + (let ((tag (cadr a)) + (attrs (xattrs (cddr a)))) + (push (list (if attrs (cons tag attrs) tag)) z)) + (push nil z)) + (:end + (let ((tag (cdr a)) + (b (nreverse (pop z))) + (e (pop z))) + (assert e) + (assert z) + (let* ((h (car e)) + (tag2 (if (atom (car h)) h (car h))) + (attrs (unless (atom (car h)) (cdr h)))) + (assert (equal tag tag2)) + (push (cons (if attrs (cons (id tag) attrs) (id tag)) b) + (car z))))) + (:text + (when (and (cdr z) + (find-if-not + (lambda (c) + (or (member c '(#\space #\tab #\newline)))) + (cdr a))) + (push (string-trim '(#\space #\tab #\newline) + (concatenate 'string (cdr a))) + (car z)))))))) + (etypecase x + (function (parse x)) + ((or list vector) (parse (rw:peek-reader (rw:reader x)))) + (pathname (with-open-file (s x) + (parse (rw:peek-reader (rw:char-reader s)))))))) + +;;(parse-xml "<rss><ahoj/>hi<cau></cau><br x='314'/></rss>") +;;(parse-xml "<rss a='1' b='2'>hi<br/></rss>") +;;(parse-xml "<rss>hi<br/></hello>") +;;(parse-xml #p"/home/tomas/git/cl-rw/a.xml")