initial import

main
Steve Ayerhart 2023-02-16 17:26:23 -05:00
commit 7025952bd2
No known key found for this signature in database
GPG Key ID: 4CB33EB9BB156C97
8 changed files with 718 additions and 0 deletions

1
src/xmpp/core.scm Normal file
View File

@ -0,0 +1 @@
(define-module (xmpp core))

51
src/xmpp/core/auth.scm Normal file
View File

@ -0,0 +1,51 @@
(define-module (xmpp core auth)
#:use-module (sxml match)
#:use-module (xmpp core stanza)
#:use-module (xmpp core stream)
#:use-module (xmpp core namespaces)
#:use-module (xmpp core jid)
#:use-module (xmpp core)
#:use-module (xmpp utils base64)
#:export (xmpp-negotiate-sasl-mechanisms))
(define (plain-client-message authcid password)
(base64-encode
(string-append
"\0" authcid
"\0" password)))
(define (xmpp-negotiate-sasl-plain xmpp-stream)
(let ((data (plain-client-message (jid-local (xmpp-stream-jid xmpp-stream))
(current-xmpp-client-password))))
(xmpp-write-stanza xmpp-stream `(auth (@ (xmlns ,(cdr *sasl-ns*)) (mechanism "PLAIN")) ,data))
(let ((plain-response (xmpp-read-stanza xmpp-stream)))
(sxml-match
plain-response
((*TOP* (sasl:success))
(xmpp-negotiate-features (xmpp-restart-stream xmpp-stream)))
((*TOP* (sasl:failure
(sasl:not-authorized
(sasl:text (@ (xml:lang ,lang)) ,message))))
(begin
(xmpp-stream-error-set! xmpp-stream 'not-authorized)
xmpp-stream))
((*TOP* (stream:error . ,the-error))
; (stream-error:not-well-formed
; (stream-error:text (@ (xml:lang ,lang)) . ,error-text))))
(error the-error))
(,otherwise
(format #t "~a\n" plain-response))))))
(define (xmpp-negotiate-sasl-choose-mechanism xmpp-stream mechanisms)
(xmpp-negotiate-sasl-plain xmpp-stream))
(define (xmpp-negotiate-sasl-mechanisms xmpp-stream)
(let ((mechanisms-stanza (xmpp-read-stanza xmpp-stream)))
(sxml-match
mechanisms-stanza
((*TOP* (stream:features (sasl:mechanisms . ,mechanisms)))
(xmpp-negotiate-sasl-choose-mechanism xmpp-stream mechanisms))
(,otherwise
mechanisms-stanza))))

View File

@ -0,0 +1,45 @@
(define-module (xmpp core handlers)
#:use-module (ice-9 optargs)
#:use-module (fibers)
#:use-module (xmpp core stream)
#:export (xmpp-stream-handler-initialize!
xmpp-stream-handler-stop!
xmpp-stream-handler-set-callback!
xmpp-stream-handler-unset-callback!
xmpp-stream-handler-callbacks))
(define-record-type <xmpp-stream-handler>
(make-xmpp-stream handler thread callbacks)
xmpp-stream-handler
(handler xmpp-stream-handler-handler xmpp-stream-handler-handler-set!)
(thread xmpp-stream-handler-thread xmpp-stream-handler-thread-set!)
(callbacks xmpp-stream-handler-callbacks xmpp-stream-handler-callbacks-set!))
(define (default-xmpp-handler xmpp-stream stanza)
#t)
(define-values
(xmpp-stream-handler-initialize!
xmpp-stream-handler-stop!
xmpp-stream-handler-set-callback!
xmpp-stream-handler-unset-callback!
xmpp-stream-handler-callbacks)
(let ((handler-hash (make-hash-table)))
(values
;; initialize!
(lambda* (xmpp-stream #:optional (handler default-xmpp-handler))
#t)
;; stop!
(λ (xmpp-stream)
#t)
;; set-callback!
(λ (xmpp-stream callback)
#t)
;; unset-callback!
(λ (xmpp-stream callback)
#t)
;; callbacks
(λ (xmpp-stream)
(xmpp-stream-handler-callbacks (xmpp-stream-handler-handler xmpp-stream)))))

127
src/xmpp/core/jid.scm Normal file
View File

@ -0,0 +1,127 @@
(define-module (xmpp core jid)
#:use-module (ice-9 regex)
#:use-module (ice-9 optargs)
#:use-module (srfi srfi-9)
#:use-module (srfi srfi-13)
#:use-module (srfi srfi-14)
#:export (jid?
jid-local jid-host jid-resource set-jid-resource!
jid->string
build-jid string->jid jid-or-string->jid))
(define-record-type <jid>
(make-jid local host resource)
jid?
(local jid-local)
(host jid-host)
(resource jid-resource set-jid-resource!))
;; host validation code stolen from guile's web/uri module
(define (jid-error message . args)
(throw 'jid-error message args))
(define host-regex
(make-regexp "^([a-z0-9][a-z0-9-]*[a-z0-9][.])+[a-z0-9][a-z0-9-]*[a-z0-9]$" regexp/icase))
(define ipv4-regexp
(make-regexp "^([0-9.]+)$"))
(define ipv6-regexp
(make-regexp "^([0-9a-fA-F:.]+)$"))
(define host-label-regexp
(make-regexp "^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$"))
(define top-label-regexp
(make-regexp "^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?$"))
(define (valid-host? host)
(cond
((regexp-exec ipv4-regexp host)
(false-if-exception (inet-pton AF_INET host)))
((regexp-exec ipv6-regexp host)
(false-if-exception (inet-pton AF_INET6 host)))
(else
(let lp ((start 0))
(let ((end (string-index host #\. start)))
(if end
(and (regexp-exec host-label-regexp
(substring host start end))
(lp (1+ end)))
(regexp-exec top-label-regexp host start)))))))
(define (valid-local? local)
(not (string-any
(λ (c)
(or (char<=? c #\x20)
(char=? c #\x22)
(char=? c #\x26)
(char=? c #\x27)
(char=? c #\x2F)
(char=? c #\x3A)
(char=? c #\x3C)
(char=? c #\x3E)
(char=? c #\x40)
(char=? c #\x7F)
(char=? c #\xFFFF)
(char=? c #\xFFFE)))
local)))
(define (valid-resource? resource)
(not (string-any (λ (s)
(or (char-ci<? s #\x20)
(char-ci=? s #\xFFFF)
(char-ci=? s #\xFFFE)))
resource)))
(define* (build-jid host #:optional (local #f) (resource #f))
(cond
((not (string? host))
(jid-error "Expected string for host: ~s" host))
((and local (or (not (string? local)) (not (valid-local? local))))
(jid-error "Invalid local part: ~s" local))
((not (valid-host? host))
(jid-error "Invalid host: ~s" host))
((and resource (or (not (string? resource)) (not (valid-resource? resource))))
(jid-error "Invalid resource: ~s" resource)))
(make-jid local host resource))
(define (bare-jid-from-string str)
(if (string? str)
(car (string-split str #\/))
(jid-error "Expected a string: ~s" str)))
(define (local-from-string str)
(let ((bare (bare-jid-from-string str)))
(if (string-contains bare "@")
(car (string-split bare #\@))
#f)))
(define (host-from-string str)
(let ((bare (bare-jid-from-string str)))
(if (string-contains bare "@")
(cadr (string-split bare #\@))
bare)))
(define (resource-from-string str)
(if (string? str)
(let ((parts (string-split str #\/)))
(if (>= (length parts) 2)
(cadr parts)
#f))
(jid-error "Expected string: ~s" str)))
(define (jid-or-string->jid jid-or-string)
(cond ((jid? jid-or-string)
jid-or-string)
((string? jid-or-string)
(string->jid jid-or-string))
(else (jid-error "Argument must be a jid or a string. Given: " jid-or-string))))
(define (string->jid str)
(build-jid (host-from-string str) (local-from-string str) (resource-from-string str)))
(define (jid->string jid)
(if (jid-resource jid)
(string-append (jid-local jid) "@" (jid-host jid) "/" (jid-resource jid))
(string-append (jid-local jid) "@" (jid-host jid))))

View File

@ -0,0 +1,44 @@
(define-module (xmpp core namespaces))
(define-public *stream-ns* '(stream . "http://etherx.jabber.org/streams"))
(define-public *tls-ns* '(tls . "urn:ietf:params:xml:ns:xmpp-tls"))
(define-public *sasl-ns* '(sasl . "urn:ietf:params:xml:ns:xmpp-sasl"))
(define-public *compression-ns* '(compression . "http://jabber.org/features/compress"))
(define-public *bind-ns* '(bind . "urn:ietf:params:xml:ns:xmpp-bind"))
(define-public *amp-ns* '(amp . "http://jabber.org/features/amp"))
(define-public *bidi-ns* '(bidi . "urn:xmpp:bidi"))
(define-public *server-dialback-ns* '(dialback . "urn:xmpp:features:dialback"))
(define-public *iq-auth-ns* '(iq-quth . "http://jabber.org/features/iq-auth"))
(define-public *iq-register-ns* '(iq-register . "http://jabber.org/features/iq-register"))
(define-public *session-ns* '(session . "urn:ietf:params:xml:ns:xmpp-session"))
(define-public *sm3-ns* '(sm3 . "urn:xmpp:sm:3"))
(define-public *sm2-ns* '(sm2 . "urn:xmpp:sm:2"))
(define-public *caps-ns* '(caps . "http://jabber.org/protocol/caps"))
(define-public *ver-ns* '(ver . "urn:xmpp:features:rosterver"))
(define-public *csi-ns* '(csi . "urn:xmpp:csi:0"))
(define-public *client-ns* '(client . "jabber:client"))
(define-public *server-ns* '(server . "jabber:server"))
(define-public *stream-error-ns* '(stream-error . "urn:ietf:params:xml:ns:xmpp-streams"))
(define-public *stanza-error-ns* '(stanza-error . "urn:ietf:params:xml:ns:xmpp-stanza"))
(define-public %default-namespaces
(list *stream-ns*
*tls-ns*
*sasl-ns*
*compression-ns*
*bind-ns*
*amp-ns*
*bidi-ns*
*server-dialback-ns*
*iq-auth-ns*
*iq-register-ns*
*session-ns*
*sm3-ns*
*sm2-ns*
*caps-ns*
*ver-ns*
*csi-ns*
*client-ns*
*server-ns*
*stream-error-ns*
*stanza-error-ns*))

56
src/xmpp/core/stanza.scm Normal file
View File

@ -0,0 +1,56 @@
(define-module (xmpp core stanza)
#:use-module (sxml match)
#:use-module (sxml ssax)
#:use-module (sxml ssax input-parse)
#:use-module (sxml simple)
#:use-module (ice-9 match)
#:use-module (ice-9 rdelim)
#:use-module (ice-9 regex)
#:use-module (ice-9 format)
#:use-module (rnrs io ports)
#:use-module (xmpp core namespaces)
#:use-module (xmpp core stream)
#:use-module (xmpp core jid)
#:export (xmpp-write-stream-header
xmpp-read-stream-header
xmpp-read-stanza
xmpp-write-stanza))
(define* (xmpp-write-stream-header port host #:optional (from #f))
(format
port
"<?xml version=\"1.0\"?><~a:stream version=\"1.0\" from=\"~a\" to=\"~a\" xml:lang=\"en\" xmlns:~a=\"~a\" xmlns=\"jabber:client\">"
(symbol->string (car *stream-ns*))
(jid->string from)
host
(symbol->string (car *stream-ns*))
(cdr *stream-ns*))
(force-output port))
(define (xmpp-read-stream-header socket-port)
(let* ((pi (read-delimited ">" socket-port 'concat))
(stream (regexp-substitute #f (string-match ">$" (read-delimited ">" socket-port 'concat)) 'pre "/>"))
(xml (string-join (list pi stream) ""))
(header-stanza (xml->sxml xml #:trim-whitespace? #t #:namespaces (list *stream-ns*))))
(sxml-match header-stanza
((*TOP*
(*PI* . ,pi)
(stream:stream
(@ (id ,stream-id) (version ,stream-version) (xml:lang ,lang) (from ,from) (to ,to) . ,attribs) . ,stream))
(values stream-id from (string->jid to) stream-version lang))
(,otherwise
(error header-stanza)))))
(define (xmpp-read-stanza xmpp-stream)
(let ((port (xmpp-stream-get-port xmpp-stream)))
(if (eof-object? (peek-char port))
eof-object
(xml->sxml port #:trim-whitespace? #t #:namespaces (xmpp-stream-namespaces xmpp-stream)))))
(define (xmpp-write-stanza xmpp-stream stanza)
(let ((port (xmpp-stream-get-port xmpp-stream)))
(sxml->xml stanza port)
(force-output port)))

169
src/xmpp/core/stream.scm Normal file
View File

@ -0,0 +1,169 @@
(define-module (xmpp core stream)
#:use-module (fibers)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-9)
#:use-module (rnrs bytevectors)
#:use-module (rnrs io ports)
#:use-module (ice-9 receive)
#:use-module (ice-9 match)
#:use-module (ice-9 rdelim)
#:use-module (ice-9 regex)
#:use-module (ice-9 format)
#:use-module (ice-9 suspendable-ports)
#:use-module (gnutls)
#:use-module (sxml match)
#:use-module (sxml simple)
#:use-module (xmpp core jid)
#:use-module (xmpp core auth)
#:use-module (xmpp core stanza)
#:use-module (xmpp core namespaces)
#:export (make-xmpp-stream
xmpp-close-stream
xmpp-stream-get-port
xmpp-stream-error-set!
xmpp-stream-jid
current-xmpp-client-password
xmpp-restart-stream
xmpp-stream-namespaces
xmpp-negotiate-features))
(define current-xmpp-stream (make-parameter #f))
(define current-xmpp-client-jid (make-parameter #f (λ (val) (if (or (eq? #f val) (jid? val)) val (error "must be a jid")))))
(define current-xmpp-client-password (make-parameter #f))
(define %default-xmpp-port* 5222)
(define %disconnect-timeout* 2000)
(define %connect-timeout* 5000)
(define *stream-version* "1.0")
(define *stream-lang* "en")
(define-record-type <xmpp-stream>
(make-xmpp-stream host jid stream-loop namespaces handler callbacks tls-session port id version lang features error connected? encrypted?)
xmpp-stream?
(host xmpp-stream-host xmpp-stream-host-set!)
(tls-session xmpp-stream-tls-session xmpp-stream-tls-session-set!)
(jid xmpp-stream-jid xmpp-stream-jid-set!)
(password xmpp-stream-password xmpp-stream-password-set!)
(stream-loop xmpp-stream-stream-loop xmpp-stream-stream-loop-set!)
(namespaces xmpp-stream-namespaces xmpp-stream-namespaces-set!)
(handler xmpp-stream-handler)
(callbacks xmpp-stream-callbacks xmpp-stream-callbacks-set!)
(port xmpp-stream-port xmpp-stream-port-set!)
(id stream-id xmpp-stream-id-set!)
(version xmpp-stream-version xmpp-stream-version-set!)
(lang stream-lang)
(features xmpp-stream-features xmpp-stream-features-set!)
(error xmpp-stream-error xmpp-stream-error-set!)
(connected? xmpp-stream-connected? xmpp-stream-connected?-set!)
(encrypted? xmpp-stream-encrypted? xmpp-stream-encrypted?-set!))
(define (xmpp-restart-stream xmpp-stream)
(let ((port (xmpp-stream-get-port xmpp-stream)))
(xmpp-write-stream-header
port
(xmpp-stream-host xmpp-stream)
(if (xmpp-stream-jid xmpp-stream) (xmpp-stream-jid xmpp-stream) #f))
(receive (id from jid version lang)
(xmpp-read-stream-header port)
(xmpp-stream-version-set! xmpp-stream version)
(xmpp-stream-id-set! xmpp-stream id)
xmpp-stream)))
(define (xmpp-restart-stream-tls xmpp-stream)
(let ((session (make-session connection-end/client))
(credentials (make-certificate-credentials)))
(set-session-default-priority! session)
(set-session-server-name! session server-name-type/dns (xmpp-stream-host xmpp-stream))
(set-session-credentials! session credentials)
(set-session-priorities! session "NORMAL:%COMPAT:-VERS-SSL3.0")
(set-session-dh-prime-bits! session 2048)
(set-session-transport-fd! session (fileno (xmpp-stream-port xmpp-stream)))
;; set nonblocking
(setvbuf (xmpp-stream-port xmpp-stream) 'block 1024)
(setsockopt client OPPROTO_TCP TCP_NODELAY 1)
(handshake session)
(catch 'gnutls-error
(λ ()
(let ((port (session-record-port session)))
(xmpp-write-stream-header port (xmpp-stream-host xmpp-stream) (xmpp-stream-jid xmpp-stream))
(receive (id from jid version lang)
(xmpp-read-stream-header port)
(make-xmpp-stream from jid #f %default-namespaces '() session #f id version lang '() #f #t #t))))
(λ (key error function)
(close-port (xmpp-stream-port xmpp-stream))
(scm-error 'xmpp-stream "negotiate-tls" "handshake error" function #f)))))
;;; if the stream is encrypted the port is encapsulated in a tls session
;;; instead of a plain port
(define (xmpp-stream-get-port xmpp-stream)
(if (xmpp-stream-encrypted? xmpp-stream)
(session-record-port (xmpp-stream-tls-session xmpp-stream))
(xmpp-stream-port xmpp-stream)))
(define* (xmpp-open-client-stream jid #:optional (host #f) (s2s? #f))
(let* ((host (if host host (jid-host jid)))
(service (if s2s? "xmpp-server" "xmpp-client"))
(addresses (delete-duplicates (getaddrinfo host service)
(λ (a1 a2)
(equal? (addrinfo:addr a1)
(addrinfo:addr a2))))))
(let try-connect ((addresses addresses))
(let* ((ai (car addresses))
(socket-port (with-fluids ((%default-port-encoding #f))
(socket (addrinfo:fam ai)
(addrinfo:socktype ai)
(addrinfo:protocol ai)))))
(catch 'system-error
(λ ()
(connect socket-port (addrinfo:addr ai))
(xmpp-write-stream-header socket-port host jid)
(receive (id from jid version lang)
(xmpp-read-stream-header socket-port)
(make-xmpp-stream from jid #f %default-namespaces '() #f socket-port id version lang '() #f #t #f)))
(λ args
(close socket-port)
(if (null? (cdr addresses))
(apply throw args)
(try-connect (cdr addresses)))))))))
(define (xmpp-negotiate-tls-starttls xmpp-stream)
(xmpp-write-stanza xmpp-stream `(starttls (@ (xmlns ,(cdr *tls-ns*))))))
(define (xmpp-close-stream xmpp-stream)
(format (xmpp-stream-get-port xmpp-stream) "</stream:stream>")
(force-output (xmpp-stream-get-port xmpp-stream)))
(define (xmpp-negotiate-tls xmpp-stream)
(xmpp-negotiate-tls-starttls xmpp-stream)
(let ((starttls-stanza (xmpp-read-stanza xmpp-stream)))
(sxml-match
starttls-stanza
((*TOP* (tls:proceed))
(xmpp-negotiate-sasl-mechanisms (xmpp-restart-stream-tls xmpp-stream)))
(,otherwise starttls-stanza))))
(define (xmpp-negotiate-features xmpp-stream)
(let ((features-stanza (xmpp-read-stanza xmpp-stream)))
(sxml-match
features-stanza
((*TOP* (stream:features (tls:starttls (tls:required))))
(xmpp-negotiate-tls xmpp-stream))
((*TOP* (stream:features . ,features))
(xmpp-stream-features-set! xmpp-stream features)
xmpp-stream)
(,otherwise
features-stanza))))
(define* (xmpp-stream-connect-client jid password #:optional (host #f))
(parameterize ((current-xmpp-client-jid jid) (current-xmpp-client-password password))
(let ((xmpp-stream (xmpp-open-client-stream jid host)))
(xmpp-negotiate-features xmpp-stream))))

225
src/xmpp/utils/base64.scm Normal file
View File

@ -0,0 +1,225 @@
;; -*- indent-tabs-mode:nil; coding: utf-8 -*-
;; Copyright © 2009, 2010, 2012, 2013 Göran Weinholt <goran@weinholt.se>
;; Copyright 2018 Mu Lei known as NalaGinrut <nalaginrut@gmail.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.
;; RFC 4648 Base-N Encodings
(define-module (xmpp utils base64)
#:use-module ((rnrs) :version (6))
#:export (base64-encode base64-decode))
(define base64-alphabet
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
(define base64url-alphabet
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")
(define %base64-encode
(case-lambda
;; Simple interface. Returns a string containing the canonical
;; base64 representation of the given bytevector.
((bv)
(%base64-encode bv 0 (bytevector-length bv) #f #f base64-alphabet #f))
((bv start)
(%base64-encode bv start (bytevector-length bv) #f #f base64-alphabet #f))
((bv start end)
(%base64-encode bv start end #f #f base64-alphabet #f))
((bv start end line-length)
(%base64-encode bv start end line-length #f base64-alphabet #f))
((bv start end line-length no-padding)
(%base64-encode bv start end line-length no-padding base64-alphabet #f))
((bv start end line-length no-padding alphabet)
(%base64-encode bv start end line-length no-padding alphabet #f))
;; Base64 encodes the bytes [start,end[ in the given bytevector.
;; Lines are limited to line-length characters (unless #f),
;; which must be a multiple of four. To omit the padding
;; characters (#\=) set no-padding to a true value. If port is
;; #f, returns a string.
((bv start end line-length no-padding alphabet port)
(assert (or (not line-length) (zero? (mod line-length 4))))
(let-values (((p extract) (if port
(values port (lambda () (values)))
(open-string-output-port))))
(letrec ((put (if line-length
(let ((chars 0))
(lambda (p c)
(when (fx=? chars line-length)
(set! chars 0)
(put-char p #\linefeed))
(set! chars (fx+ chars 1))
(put-char p c)))
put-char)))
(let lp ((i start))
(cond ((= i end))
((<= (+ i 3) end)
(let ((x (bytevector-uint-ref bv i (endianness big) 3)))
(put p (string-ref alphabet (fxbit-field x 18 24)))
(put p (string-ref alphabet (fxbit-field x 12 18)))
(put p (string-ref alphabet (fxbit-field x 6 12)))
(put p (string-ref alphabet (fxbit-field x 0 6)))
(lp (+ i 3))))
((<= (+ i 2) end)
(let ((x (fxarithmetic-shift-left (bytevector-u16-ref bv i (endianness big)) 8)))
(put p (string-ref alphabet (fxbit-field x 18 24)))
(put p (string-ref alphabet (fxbit-field x 12 18)))
(put p (string-ref alphabet (fxbit-field x 6 12)))
(unless no-padding
(put p #\=))))
(else
(let ((x (fxarithmetic-shift-left (bytevector-u8-ref bv i) 16)))
(put p (string-ref alphabet (fxbit-field x 18 24)))
(put p (string-ref alphabet (fxbit-field x 12 18)))
(unless no-padding
(put p #\=)
(put p #\=)))))))
(extract)))))
;; Create a lookup table for the alphabet and remember the latest table.
(define get-decode-table
(let ((ascii-table #f)
(extra-table '()) ;in the unlikely case of unicode chars
(table-alphabet #f))
(lambda (alphabet)
(unless (eq? alphabet table-alphabet)
;; Rebuild the table.
(do ((ascii (make-vector 128 #f))
(extra '())
(i 0 (+ i 1)))
((= i (string-length alphabet))
(set! ascii-table ascii)
(set! extra-table extra))
(let ((c (char->integer (string-ref alphabet i))))
(if (fx<=? c 127)
(vector-set! ascii c i)
(set! extra (cons (cons c i) extra)))))
(set! table-alphabet alphabet))
(values ascii-table extra-table))))
;; Decodes a correctly padded base64 string, optionally ignoring
;; non-alphabet characters.
(define %base64-decode
(case-lambda
((str)
(%base64-decode str base64-alphabet #f))
((str alphabet)
(%base64-decode str alphabet #f))
((str alphabet port)
(%base64-decode str alphabet port #t))
((str alphabet port strict?)
(define (pad? c) (eqv? c (char->integer #\=)))
(let-values (((p extract) (if port
(values port (lambda () (values)))
(open-bytevector-output-port)))
((ascii extra) (get-decode-table alphabet)))
(define-syntax lookup
(syntax-rules ()
((_ c) (or (and (fx<=? c 127) (vector-ref ascii c))
(cond ((assv c extra) => cdr)
(else #f))))))
(let* ((len (if strict?
(string-length str)
(let lp ((i (fx- (string-length str) 1)))
;; Skip trailing invalid chars.
(cond ((fxzero? i) 0)
((let ((c (char->integer (string-ref str i))))
(or (lookup c) (pad? c)))
(fx+ i 1))
(else (lp (fx- i 1))))))))
(let lp ((i 0))
(cond
((fx=? i len)
(extract))
((fx<=? i (fx- len 4))
(let lp* ((c1 (char->integer (string-ref str i)))
(c2 (char->integer (string-ref str (fx+ i 1))))
(c3 (char->integer (string-ref str (fx+ i 2))))
(c4 (char->integer (string-ref str (fx+ i 3))))
(i i))
(let ((i1 (lookup c1)) (i2 (lookup c2))
(i3 (lookup c3)) (i4 (lookup c4)))
(cond
((and i1 i2 i3 i4)
;; All characters present and accounted for.
;; The most common case.
(let ((x (fxior (fxarithmetic-shift-left i1 18)
(fxarithmetic-shift-left i2 12)
(fxarithmetic-shift-left i3 6)
i4)))
(put-u8 p (fxbit-field x 16 24))
(put-u8 p (fxbit-field x 8 16))
(put-u8 p (fxbit-field x 0 8))
(lp (fx+ i 4))))
((and i1 i2 i3 (pad? c4) (= i (- len 4)))
;; One padding character at the end of the input.
(let ((x (fxior (fxarithmetic-shift-left i1 18)
(fxarithmetic-shift-left i2 12)
(fxarithmetic-shift-left i3 6))))
(put-u8 p (fxbit-field x 16 24))
(put-u8 p (fxbit-field x 8 16))
(lp (fx+ i 4))))
((and i1 i2 (pad? c3) (pad? c4) (= i (- len 4)))
;; Two padding characters.
(let ((x (fxior (fxarithmetic-shift-left i1 18)
(fxarithmetic-shift-left i2 12))))
(put-u8 p (fxbit-field x 16 24))
(lp (fx+ i 4))))
((not strict?)
;; Non-alphabet characters.
(let lp ((i i) (c* '()) (n 4))
(cond ((fxzero? n)
;; Found four valid characters.
(lp* (cadddr c*) (caddr c*) (cadr c*) (car c*)
(fx- i 4)))
((fx=? i len)
(error '%base64-decode
"Invalid input in non-strict mode."
i c*))
(else
;; Gather alphabetic (or valid
;; padding) characters.
(let ((c (char->integer (string-ref str i))))
(cond ((or (lookup c)
(and (pad? c)
(fx<=? n 2)
(fx=? i (fx- len n))))
(lp (fx+ i 1) (cons c c*) (fx- n 1)))
(else
(lp (fx+ i 1) c* n))))))))
(else
(error '%base64-decode
"Invalid input in strict mode."
c1 c2 c3 c4))))))
(else
(error '%base64-decode
"The input is too short, it may be missing padding."
i)))))))))
(define (get-line-comp f port)
(if (port-eof? port)
(eof-object)
(f (get-line port))))
(define (base64-encode str/bv)
(%base64-encode (if (string? str/bv) (string->utf8 str/bv) str/bv)))
(define (base64-decode str/bv)
(%base64-decode (if (string? str/bv) (string->utf8 str/bv) str/bv)))