Skip to content
version.lisp 5.43 KiB
Newer Older
#+xcvb (module (:depends-on ("character-classes")))

Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
(in-package :rpm)

(named-readtables:in-readtable -standard)

(defun valid-rpm-version-component-p (string &key start end)
  (and
   (find-if #'ascii-digit-p string :start start :end end)
   (not (find-if (λ (x) (find x "-~/")) string :start start :end end))
   (not (search ".." string :start2 start :end2 end))))

(defun valid-rpm-architecture-component-p (string &key start end)
  (and
   (find-if #'ascii-letter-p string :start start :end end)
   (not (find-if-not 'ascii-alphanumeric-or-underscore-p
                     string :start start :end end))))

(defun parse-rpm-versioned-name (string &key (start 0) (end (length string)))
  (flet ((err () (error "No valid RPM version in package name ~S" string))
         (split-at (n) (values (subseq string start n)
                               (subseq string (1+ n) end))))
    (let ((pos (position #\- string :from-end t :start start :end end)))
      (unless (and pos (valid-rpm-version-component-p
                        string :start (1+ pos) :end end))
        (err))
      (let ((pos2 (position #\- string :from-end t :start start :end pos)))
        (split-at (if (and pos2
                           (valid-rpm-version-component-p
                            string :start (1+ pos2) :end pos))
                      pos2 pos))))))

(defun rpm-versioned-name-basename (string)
  (nth-value 0 (parse-rpm-versioned-name string)))

(defun rpm-versioned-name-version (string)
  (nth-value 1 (parse-rpm-versioned-name string)))

(defun parse-rpm-pathname (pathname)
  (with-nesting ()
    (let* ((pathname (pathname pathname))
           (directory (pathname-directory-pathname pathname))
           (type (pathname-type pathname))
           (basename (pathname-name pathname))
           (dotpos (position #\. basename :from-end t))))
    (progn
      (assert (equal type "rpm"))
      (assert dotpos)
      (assert (valid-rpm-architecture-component-p basename :start (1+ dotpos))))
    (let ((architecture (subseq basename (1+ dotpos)))))
    (multiple-value-bind (name version)
        (parse-rpm-versioned-name basename :end dotpos))
    (values directory name version architecture)))

(defun rpm-pathname-packagename (pathname)
  (nth-value 1 (parse-rpm-pathname pathname)))

(defun rpm-pathname-version (pathname)
  (nth-value 2 (parse-rpm-pathname pathname)))

;; For version comparison, I followed
;; https://twiki.cern.ch/twiki/bin/view/Main/RPMAndDebVersioning

(defun parse-rpm-version-component (v)
  "Given a version or release component of a RPM, parse it into a list
of numbers and letters, e.g. \"0.99p7\" => (0 99 \"p\" 7)"
  (loop :with r = () :with l = () :with len = (length v) :with i = 0
    :while (< i len) :do
    (flet ((handle-component (predicate push)
             (when (and (< i len) (funcall predicate (char v i)))
               (let ((j (or (position-if-not predicate v :start (1+ i)) len)))
                 (when push (push (funcall push (subseq v i j)) l))
                 (setf i j)))))
      (handle-component #'ascii-letter-p #'parse-integer)
      (handle-component #'ascii-digit-p #'identity)
      (handle-component #'ascii-non-alphanumeric-p nil))
    :finally (return (reverse l))))

(defun compare-rpm-version-chunks (ch1 ch2)
  "Given the first chunks of two respective version numbers,
return the symbol < = > depending on which of predicates hold,
or nil is none does"
  (check-type ch1 (or integer string))
  (check-type ch2 (or integer string))
  (cond
    ((and (integerp ch1) (integerp ch2))
     (cond
       ((< ch1 ch2) '<)
       ((> ch1 ch2) '>)
       (t '=)))
    ;; RPM: integer block beats alphanumeric, so 1.4.1 > 1.4p8
    ((integerp ch1)
     '>)
    ((integerp ch2)
     '<)
    (t
     (cond
       ((string< ch1 ch2) '<)
       ((string> ch1 ch2) '>)
       (t '=)))))

(defun compare-rpm-version-components (v1 v2)
  (loop :with l1 = (parse-rpm-version-component v1)
    :with l2 = (parse-rpm-version-component v2)
    :while (and l1 l2) :do
    (let ((r (compare-rpm-version-chunks (pop l1) (pop l2))))
      (ecase r
        ((< > nil) (return r))
        ((=) nil)))
    :finally
    (cond
      (l1 (return '>))
      (l2 (return '<))
      (t (return '=)))))

(defun parse-rpm-version (x)
  (block nil
    (cl-ppcre:register-groups-bind (epoch version release)
        ("^(?:([0-9]+):)?([^-/~]+)(?:-([^-/~]+))?$" x) ;; also .. forbidden
      (return (values (if (emptyp epoch) 0 (parse-integer epoch))
                      version release)))
    (error "bad rpm version ~S" x)))

(defun compare-rpm-versions (v1 v2)
  (multiple-value-bind (epoch1 version1 release1)
      (parse-rpm-version v1)
    (multiple-value-bind (epoch2 version2 release2)
        (parse-rpm-version v2)
      (cond
        ((> epoch1 epoch2)
         '>)
        ((< epoch1 epoch2)
         '<)
        (t
         (let ((r (compare-rpm-version-components version1 version2)))
           (ecase r
             ((< > nil) r)
             ((=) (compare-rpm-version-components release1 release2)))))))))

(defun rpm-version<= (v1 v2)
  (ecase (compare-rpm-versions v1 v2)
    ((< =) t)
    ((>) nil)))

(defun rpm-version>= (v1 v2)
  (ecase (compare-rpm-versions v1 v2)
    ((> =) t)
    ((<) nil)))

(defun rpm-version< (v1 v2)
  (ecase (compare-rpm-versions v1 v2)
    ((<) t)
    ((> =) nil)))

(defun rpm-version> (v1 v2)
  (ecase (compare-rpm-versions v1 v2)
    ((>) t)
    ((< =) nil)))

(defun rpm-version= (v1 v2)
  (ecase (compare-rpm-versions v1 v2)
    ((=) t)
    ((< >) nil)))