193 lines
6.8 KiB
Common Lisp
193 lines
6.8 KiB
Common Lisp
|
|
(in-package #:ql-minitar)
|
||
|
|
|
||
|
|
(defconstant +block-size+ 512)
|
||
|
|
(defconstant +space-code+ 32)
|
||
|
|
(defconstant +newline-code+ 10)
|
||
|
|
(defconstant +equals-code+ 61)
|
||
|
|
|
||
|
|
(defun make-block-buffer ()
|
||
|
|
(make-array +block-size+ :element-type '(unsigned-byte 8) :initial-element 0))
|
||
|
|
|
||
|
|
(defun skip-n-blocks (n stream)
|
||
|
|
(let ((block (make-block-buffer)))
|
||
|
|
(dotimes (i n)
|
||
|
|
(read-sequence block stream))))
|
||
|
|
|
||
|
|
(defun read-octet-vector (length stream)
|
||
|
|
(let ((block (make-block-buffer))
|
||
|
|
(vector (make-array length :element-type '(unsigned-byte 8)))
|
||
|
|
(offset 0)
|
||
|
|
(block-count (ceiling length +block-size+)))
|
||
|
|
(dotimes (i block-count)
|
||
|
|
(read-sequence block stream)
|
||
|
|
(replace vector block :start1 offset)
|
||
|
|
(incf offset +block-size+))
|
||
|
|
vector))
|
||
|
|
|
||
|
|
|
||
|
|
(defun decode-pax-header-record (vector offset)
|
||
|
|
"Decode VECTOR as pax extended header data. Returns the keyword and
|
||
|
|
value it specifies as multiple values."
|
||
|
|
;; Vector format is: "%d %s=%s\n", <length>, <keyword>, <value>
|
||
|
|
;; See http://pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html
|
||
|
|
(let* ((length-start offset)
|
||
|
|
(length-end (position +space-code+ vector :start length-start))
|
||
|
|
(length-string (ascii-subseq vector length-start length-end))
|
||
|
|
(length (parse-integer length-string))
|
||
|
|
(keyword-start (1+ length-end))
|
||
|
|
(keyword-end (position +equals-code+ vector :start keyword-start))
|
||
|
|
(keyword (ascii-subseq vector keyword-start keyword-end))
|
||
|
|
(value-start (1+ keyword-end))
|
||
|
|
(value-end (1- (+ offset length)))
|
||
|
|
(value (ascii-subseq vector value-start value-end)))
|
||
|
|
(values keyword value (+ offset length))))
|
||
|
|
|
||
|
|
(defun decode-pax-header (vector)
|
||
|
|
"Decode VECTOR as a pax header and return it as an alist."
|
||
|
|
(let ((header nil)
|
||
|
|
(offset 0)
|
||
|
|
(length (length vector)))
|
||
|
|
(loop
|
||
|
|
(when (<= length offset)
|
||
|
|
(return header))
|
||
|
|
(multiple-value-bind (keyword value new-offset)
|
||
|
|
(decode-pax-header-record vector offset)
|
||
|
|
(setf header (acons keyword value header))
|
||
|
|
(setf offset new-offset)))))
|
||
|
|
|
||
|
|
(defun pax-header-path (vector)
|
||
|
|
"Decode VECTOR as a pax header and return its 'path' value, if
|
||
|
|
any."
|
||
|
|
(let ((header-alist (decode-pax-header vector)))
|
||
|
|
(cdr (assoc "path" header-alist :test 'equal))))
|
||
|
|
|
||
|
|
(defun ascii-subseq (vector start end)
|
||
|
|
(let ((string (make-string (- end start))))
|
||
|
|
(loop for i from 0
|
||
|
|
for j from start below end
|
||
|
|
do (setf (char string i) (code-char (aref vector j))))
|
||
|
|
string))
|
||
|
|
|
||
|
|
(defun block-asciiz-string (block start length)
|
||
|
|
(let* ((end (+ start length))
|
||
|
|
(eos (or (position 0 block :start start :end end)
|
||
|
|
end)))
|
||
|
|
(ascii-subseq block start eos)))
|
||
|
|
|
||
|
|
(defun prefix (header)
|
||
|
|
(when (plusp (aref header 345))
|
||
|
|
(block-asciiz-string header 345 155)))
|
||
|
|
|
||
|
|
(defun name (header)
|
||
|
|
(block-asciiz-string header 0 100))
|
||
|
|
|
||
|
|
(defun payload-size (header)
|
||
|
|
(values (parse-integer (block-asciiz-string header 124 12) :radix 8)))
|
||
|
|
|
||
|
|
(defun nth-block (n file)
|
||
|
|
(with-open-file (stream file :element-type '(unsigned-byte 8))
|
||
|
|
(let ((block (make-block-buffer)))
|
||
|
|
(skip-n-blocks (1- n) stream)
|
||
|
|
(read-sequence block stream)
|
||
|
|
block)))
|
||
|
|
|
||
|
|
(defun payload-type (code)
|
||
|
|
(case code
|
||
|
|
(0 :file)
|
||
|
|
(48 :file)
|
||
|
|
(50 :symlink)
|
||
|
|
(76 :long-name)
|
||
|
|
(53 :directory)
|
||
|
|
(103 :global-header)
|
||
|
|
(120 :pax-extended-header)
|
||
|
|
(t :unsupported)))
|
||
|
|
|
||
|
|
(defun full-path (header)
|
||
|
|
(let ((prefix (prefix header))
|
||
|
|
(name (name header)))
|
||
|
|
(if prefix
|
||
|
|
(format nil "~A/~A" prefix name)
|
||
|
|
name)))
|
||
|
|
|
||
|
|
(defun save-file (file size stream)
|
||
|
|
(multiple-value-bind (full-blocks partial)
|
||
|
|
(truncate size +block-size+)
|
||
|
|
(ensure-directories-exist file)
|
||
|
|
(with-open-file (outstream file
|
||
|
|
:direction :output
|
||
|
|
:if-exists :supersede
|
||
|
|
:element-type '(unsigned-byte 8))
|
||
|
|
(let ((block (make-block-buffer)))
|
||
|
|
(dotimes (i full-blocks)
|
||
|
|
(read-sequence block stream)
|
||
|
|
(write-sequence block outstream))
|
||
|
|
(when (plusp partial)
|
||
|
|
(read-sequence block stream)
|
||
|
|
(write-sequence block outstream :end partial))))))
|
||
|
|
|
||
|
|
(defun gnu-long-name (size stream)
|
||
|
|
;; GNU long names are simply the filename (null terminated) packed into the
|
||
|
|
;; payload.
|
||
|
|
(let ((payload (read-octet-vector size stream)))
|
||
|
|
(ascii-subseq payload 0 (1- size))))
|
||
|
|
|
||
|
|
(defun unpack-tarball (tarfile &key (directory *default-pathname-defaults*))
|
||
|
|
(let ((block (make-block-buffer))
|
||
|
|
(extended-path nil))
|
||
|
|
(with-open-file (stream tarfile :element-type '(unsigned-byte 8))
|
||
|
|
(loop
|
||
|
|
(let ((size (read-sequence block stream)))
|
||
|
|
(when (zerop size)
|
||
|
|
(return))
|
||
|
|
(unless (= size +block-size+)
|
||
|
|
(error "Bad size on tarfile"))
|
||
|
|
(when (every #'zerop block)
|
||
|
|
(return))
|
||
|
|
(let* ((payload-code (aref block 156))
|
||
|
|
(payload-type (payload-type payload-code))
|
||
|
|
(tar-path (or (shiftf extended-path nil)
|
||
|
|
(full-path block)))
|
||
|
|
(full-path (merge-pathnames tar-path directory))
|
||
|
|
(payload-size (payload-size block))
|
||
|
|
(block-count (ceiling (payload-size block) +block-size+)))
|
||
|
|
(case payload-type
|
||
|
|
(:file
|
||
|
|
(save-file full-path payload-size stream))
|
||
|
|
(:directory
|
||
|
|
(ensure-directories-exist full-path))
|
||
|
|
((:symlink :global-header)
|
||
|
|
;; These block types aren't required for Quicklisp archives
|
||
|
|
(skip-n-blocks block-count stream))
|
||
|
|
(:long-name
|
||
|
|
(setf extended-path (gnu-long-name payload-size stream)))
|
||
|
|
(:pax-extended-header
|
||
|
|
(let* ((pax-header-data (read-octet-vector payload-size stream))
|
||
|
|
(path (pax-header-path pax-header-data)))
|
||
|
|
(when path
|
||
|
|
(setf extended-path path))))
|
||
|
|
(t
|
||
|
|
(warn "Unknown tar block payload code -- ~D" payload-code)
|
||
|
|
(skip-n-blocks block-count stream)))))))))
|
||
|
|
|
||
|
|
(defun contents (tarfile)
|
||
|
|
(let ((block (make-block-buffer))
|
||
|
|
(result '()))
|
||
|
|
(with-open-file (stream tarfile :element-type '(unsigned-byte 8))
|
||
|
|
(loop
|
||
|
|
(let ((size (read-sequence block stream)))
|
||
|
|
(when (zerop size)
|
||
|
|
(return (nreverse result)))
|
||
|
|
(unless (= size +block-size+)
|
||
|
|
(error "Bad size on tarfile"))
|
||
|
|
(when (every #'zerop block)
|
||
|
|
(return (nreverse result)))
|
||
|
|
(let* ((payload-type (payload-type (aref block 156)))
|
||
|
|
(tar-path (full-path block))
|
||
|
|
(payload-size (payload-size block)))
|
||
|
|
(skip-n-blocks (ceiling payload-size +block-size+) stream)
|
||
|
|
(case payload-type
|
||
|
|
(:file
|
||
|
|
(push tar-path result))
|
||
|
|
(:directory
|
||
|
|
(push tar-path result)))))))))
|