;;;; client-info.lisp (in-package #:quicklisp-client) (defparameter *client-base-url* "http://beta.quicklisp.org/") (defgeneric info-equal (info1 info2) (:documentation "Return TRUE if INFO1 and INFO2 are 'equal' in some important sense.")) ;;; Information for checking the validity of files fetched for ;;; installing/updating the client code. (defclass client-file-info () ((plist-key :initarg :plist-key :reader plist-key) (file-url :initarg :url :reader file-url) (name :reader name :initarg :name) (size :initarg :size :reader size) (md5 :reader md5 :initarg :md5) (sha256 :reader sha256 :initarg :sha256) (plist :reader plist :initarg :plist))) (defmethod print-object ((info client-file-info) stream) (print-unreadable-object (info stream :type t) (format stream "~S ~D ~S" (name info) (size info) (md5 info)))) (defmethod info-equal ((info1 client-file-info) (info2 client-file-info)) (and (eql (size info1) (size info2)) (equal (name info1) (name info2)) (equal (md5 info1) (md5 info2)))) (defclass asdf-file-info (client-file-info) () (:default-initargs :plist-key :asdf :name "asdf.lisp")) (defclass setup-file-info (client-file-info) () (:default-initargs :plist-key :setup :name "setup.lisp")) (defclass client-tar-file-info (client-file-info) () (:default-initargs :plist-key :client-tar :name "quicklisp.tar")) (define-condition invalid-client-file (error) ((file :initarg :file :reader invalid-client-file-file))) (define-condition badly-sized-client-file (invalid-client-file) ((expected-size :initarg :expected-size :reader badly-sized-client-file-expected-size) (actual-size :initarg :actual-size :reader badly-sized-client-file-actual-size)) (:report (lambda (condition stream) (format stream "Unexpected file size for ~A ~ - expected ~A but got ~A" (invalid-client-file-file condition) (badly-sized-client-file-expected-size condition) (badly-sized-client-file-actual-size condition))))) (defun check-client-file-size (file expected-size) (let ((actual-size (file-size file))) (unless (eql expected-size actual-size) (error 'badly-sized-client-file :file file :expected-size expected-size :actual-size actual-size)))) ;;; TODO: check cryptographic digests too. (defgeneric check-client-file (file client-file-info) (:documentation "Signal an INVALID-CLIENT-FILE error if FILE does not match the metadata in CLIENT-FILE-INFO.") (:method (file client-file-info) (check-client-file-size file (size client-file-info)) client-file-info)) ;;; Structuring and loading information about the Quicklisp client ;;; code (defclass client-info () ((setup-info :reader setup-info :initarg :setup-info) (asdf-info :reader asdf-info :initarg :asdf-info) (client-tar-info :reader client-tar-info :initarg :client-tar-info) (canonical-client-info-url :reader canonical-client-info-url :initarg :canonical-client-info-url) (version :reader version :initarg :version) (subscription-url :reader subscription-url :initarg :subscription-url) (plist :reader plist :initarg :plist) (source-file :reader source-file :initarg :source-file))) (defmethod print-object ((client-info client-info) stream) (print-unreadable-object (client-info stream :type t) (prin1 (version client-info) stream))) (defmethod available-versions-url ((info client-info)) (make-versions-url (subscription-url info))) (defgeneric extract-client-file-info (file-info-class plist) (:method (file-info-class plist) (let* ((instance (make-instance file-info-class)) (key (plist-key instance)) (file-info-plist (getf plist key))) (unless file-info-plist (error "Missing client-info data for ~S" key)) (destructuring-bind (&key url size md5 sha256 &allow-other-keys) file-info-plist (unless (and url size md5 sha256) (error "Missing client-info data for ~S" key)) (reinitialize-instance instance :plist file-info-plist :url url :size size :md5 md5 :sha256 sha256))))) (defun format-client-url (path &rest format-arguments) (if format-arguments (format nil "~A~{~}" *client-base-url* path format-arguments) (format nil "~A~A" *client-base-url* path))) (defun client-info-url-from-version (version) (format-client-url "client/~A/client-info.sexp" version)) (define-condition invalid-client-info (error) ((plist :initarg plist :reader invalid-client-info-plist))) (defun load-client-info (file) (let ((plist (safely-read-file file))) (destructuring-bind (&key subscription-url version canonical-client-info-url &allow-other-keys) plist (make-instance 'client-info :setup-info (extract-client-file-info 'setup-file-info plist) :asdf-info (extract-client-file-info 'asdf-file-info plist) :client-tar-info (extract-client-file-info 'client-tar-file-info plist) :canonical-client-info-url canonical-client-info-url :version version :subscription-url subscription-url :plist plist :source-file (probe-file file))))) (defun mock-client-info () (flet ((mock-client-file-info (class) (make-instance class :size 0 :url "" :md5 "" :sha256 "" :plist nil))) (make-instance 'client-info :version ql-info:*version* :subscription-url (format-client-url "client/quicklisp.sexp") :setup-info (mock-client-file-info 'setup-file-info) :asdf-info (mock-client-file-info 'asdf-file-info) :client-tar-info (mock-client-file-info 'client-tar-file-info)))) (defun fetch-client-info (url) (let ((info-file (qmerge "tmp/client-info.sexp"))) (delete-file-if-exists info-file) (fetch url info-file :quietly t) (handler-case (load-client-info info-file) ;; FIXME: So many other things could go wrong here; I think it ;; would be nice to catch and report them clearly as bogus URLs (invalid-client-info () (error "Invalid client info URL -- ~A" url))))) (defun local-client-info () (let ((info-file (qmerge "client-info.sexp"))) (if (probe-file info-file) (load-client-info info-file) (progn (warn "Missing client-info.sexp, using mock info") (mock-client-info))))) (defun newest-client-info (&optional (info (local-client-info))) (let ((latest (subscription-url info))) (when latest (fetch-client-info latest)))) (defun client-version-lessp (client-info-1 client-info-2) (string-lessp (version client-info-1) (version client-info-2))) (defun client-version () "Return the version for the current local client installation. May or may not be suitable for passing as the :VERSION argument to INSTALL-CLIENT, depending on if it's a standard Quicklisp-provided client." (version (local-client-info))) (defun client-url () "Return an URL suitable for passing as the :URL argument to INSTALL-CLIENT for the current local client installation." (canonical-client-info-url (local-client-info))) (defun available-client-versions () (let ((url (available-versions-url (local-client-info))) (temp-file (qmerge "tmp/client-versions.sexp"))) (when url (handler-case (progn (maybe-fetch-gzipped url temp-file) (prog1 (with-open-file (stream temp-file) (safely-read stream)) (delete-file-if-exists temp-file))) (unexpected-http-status (condition) (unless (url-not-suitable-error-p condition) (error condition)))))))