source: cl-darcs/tags/0.2.0/util.lisp

Last change on this file was 101, checked in by Magnus Henoch, 17 years ago

Tag 0.2.0

File size: 11.6 KB
Line 
1;;; Copyright (C) 2006 Magnus Henoch
2;;;
3;;; This program is free software; you can redistribute it and/or
4;;; modify it under the terms of the GNU General Public License as
5;;; published by the Free Software Foundation; either version 2 of the
6;;; License, or (at your option) any later version.
7;;;
8;;; This program is distributed in the hope that it will be useful,
9;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
10;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11;;; General Public License for more details.
12;;;
13;;; You should have received a copy of the GNU General Public License
14;;; along with this program; if not, write to the Free Software
15;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
16
17(in-package :darcs)
18
19(eval-when (:compile-toplevel :load-toplevel :execute)
20  (defparameter +debugged-modules+ '(get #|read-patch patchinfo|# apply-patch #|init upath|# util diff)
21    "Modules emitting debug output."))
22
23(defvar *http-proxy* nil
24  "HTTP proxy to use.
25This should be either NIL or \"proxy.example.com:3128\".")
26
27(defmacro dformat (&rest format-args)
28  "Print FORMAT-ARGS to standard output if debugging applies."
29  (let ((pathname (or (and (boundp '*load-pathname*) *load-pathname*)
30                      (and (boundp '*compile-file-pathname*) *compile-file-pathname*)
31                      t)))              ;loaded in repl
32    (when (or (eql pathname t)
33              (member (string-upcase (pathname-name pathname))
34                      +debugged-modules+ :test #'string=))
35      `(format t ,@format-args))))
36
37(defun isspace (c)
38  (ctypecase c
39    (character
40     (member c '(#\Space #\Tab #\Newline #\Linefeed)))
41    (number
42     (member c '(32 9 10 13)))))
43
44(defun hex-to-number (c)
45  "Turn C, the octet value of a hex digit, into the corresponding number."
46  (cond
47    ((<= (char-code #\0) c (char-code #\9))
48     (- c (char-code #\0)))
49    ((<= (char-code #\A) c (char-code #\F))
50     (- c (char-code #\A)))
51    ((<= (char-code #\a) c (char-code #\f))
52     (- c (char-code #\a)))
53    (t
54     (error "Invalid hex digit ~A." c))))
55
56(defun make-matcher (delimiters)
57  "Return a predicate based on DELIMITERS.
58If DELIMITERS is an atom, checks for equality.
59If DELIMITERS is a list, checks for membership.
60If DELIMITERS is a function, returns it unchanged."
61  (ctypecase delimiters
62    (function delimiters)
63    (atom (lambda (c) (eql c delimiters)))
64    (list (lambda (c) (member c delimiters)))))
65
66;; These two functions should be eachother's inverses, and be defined
67;; for all possible 8-bit values.
68(defun bytes-to-string (sequence)
69  "Convert SEQUENCE, a sequence of binary values, to a string."
70  (map 'string #'code-char sequence))
71
72(defun string-to-bytes (string)
73  "Convert STRING to a vector of (unsigned-byte 8)."
74  (map '(vector (unsigned-byte 8)) #'char-code string))
75
76;; These functions read vaguely character-like data from binary
77;; streams.
78
79(defun read-until (delimiters stream &optional (eof-error-p t) eof-value)
80  "Read from STREAM until encountering DELIMITERS.
81DELIMITERS is an atom, or a list of atoms, or a predicate function.
82
83Returns two values:
84 - vector of elements read
85 - encountered delimiter, or EOF-VALUE"
86  (let ((predicate (make-matcher delimiters))
87        (bytes (make-array 80 :element-type '(unsigned-byte 8)
88                           :adjustable t :fill-pointer 0)))
89    (loop for char = (read-byte stream eof-error-p)
90       while (and char (not (funcall predicate char)))
91       do (vector-push-extend char bytes)
92       finally (return (values bytes (or char eof-value))))))
93
94(defmethod read-binary-line ((stream stream) &optional (eof-error-p t) eof-value)
95  "Read from STREAM until encountering a newline or end-of-file.
96Return a vector of binary values.  Return EOF-VALUE if EOF-ERROR-P
97is nil and end-of-file occurs before any data is read."
98  (multiple-value-bind (line delim)
99      (read-until 10 stream eof-error-p eof-value)
100    (if (or (not (zerop (length line))) (eql delim 10))
101        line
102        delim)))
103
104(defun read-token (stream)
105  "Read and return a whitespace-separated token from STREAM.
106The first value returned is a string containing the token,
107without the terminating whitespace.
108The second value is a list of bytes containing the token and
109the terminating whitespace.
110STREAM is assumed to be an UNREADABLE-STREAM."
111  (loop for i = (read-byte stream)
112     while (isspace i)
113     finally (unread-byte stream i))
114  (multiple-value-bind (token char)
115      (read-until #'isspace stream)
116    (values
117     (bytes-to-string token)
118     (progn
119       (vector-push-extend char token)
120       token))))
121
122(defun uncompress-file (infile outfile)
123  "Uncompress INFILE (a gzipped file) and write contents to OUTFILE."
124  (setf infile (make-upath infile))
125  (cond
126    #+clisp
127    ((pathnamep infile)
128     ;; C gunzip is magnitudes faster than the Lisp implementation, at
129     ;; least in CLISP.
130     (dformat "~&Uncompressing ~A through external program..." infile)
131     (ext:run-program "gunzip" :input (namestring infile) :output (namestring outfile)
132                      :if-output-exists :error)
133     (dformat "done"))
134    (t
135     (dformat "~&Uncompressing ~A through Lisp function..." infile)
136     (with-open-stream (in (open-upath infile :binary t))
137       (with-open-file (out outfile :direction :output :element-type '(unsigned-byte 8)
138                            :if-exists :error)
139         (util.zip:skip-gzip-header in)
140         (util.zip:inflate in out)
141         (dformat "done"))))))
142
143(defun compress-file (infile outfile)
144  "Compress INFILE and write contents to OUTFILE."
145  (setf infile (make-upath infile))
146  (cond
147    #+clisp
148    ((pathnamep infile)
149     (dformat "~&Compressing ~A through external program..." outfile)
150     (ext:run-program "gzip" :input (namestring infile) :output (namestring outfile)
151                      :if-output-exists :error)
152     (dformat "done"))
153    #+sbcl
154    ((pathnamep infile)
155     (dformat "~&Compressing ~A through external program..." outfile)
156     (sb-ext:run-program "/usr/bin/gzip" nil :input infile :output outfile
157                         :if-output-exists :error))
158    (t
159     (cerror "Assume compression performed."
160             "Don't know how to gzip ~A to ~A." infile outfile))))
161
162(defun make-temp-file-name ()
163  "Create a random name for a temporary file.
164This is hopefully random enough to avoid problems."
165  ;; this ought to be fine, though unix-specific...
166  (make-pathname :directory '(:absolute "tmp") 
167                 :name (format nil "~A" (random most-positive-fixnum))))
168
169(defun make-temp-file (&rest options)
170  "Open a temporary file with the given OPTIONS.
171If OPTIONS specifies no direction, :OUTPUT is assumed."
172  (let ((filename (make-temp-file-name))
173        (options (if (getf options :direction)
174                     options
175                     (cons :direction (cons :output options)))))
176    (apply 'open filename :direction :io options)))
177
178(defmacro with-temp-file-name (filename-variable &body body)
179  "Bind FILENAME-VARIABLE to a name generated by
180MAKE-TEMP-FILE-NAME.
181Delete that file after executing BODY."
182  `(let ((,filename-variable (make-temp-file-name)))
183     (unwind-protect
184          (progn
185            ,@body)
186       (delete-file ,filename-variable))))
187
188(defmacro with-temp-file ((stream &rest options) &body body)
189  "Open a temporary file and bind the stream to STREAM.
190Execute BODY, and remove the file."
191  `(let ((,stream (make-temp-file ,@options)))
192     (unwind-protect
193          (progn
194            ,@body)
195       (close ,stream)
196       (delete-file ,stream))))
197
198(defun sanitize-filename (filename &key (type :file))
199  "Convert FILENAME into a pathname.
200Signal an error if FILENAME doesn't denote a relative path going
201strictly down.
202If TYPE is :DIRECTORY, return pathname in directory form."
203  (let ((components (split-sequence:split-sequence #\/ filename)))
204    (setf components (delete "." components :test #'string=))
205    (when (member ".." components :test #'string=)
206      (error "Filename ~S tries to go up in directory tree." filename))
207    (ecase type
208      (:directory
209       (make-pathname :directory (cons :relative components)))
210      (:file
211       (let* ((directory (butlast components))
212              (filename (car (last components)))
213              (last-dot (position #\. filename :from-end t))
214              (filename-without-dot
215               (if (and last-dot (/= 0 last-dot))
216                   (subseq filename 0 last-dot) filename))
217              (type
218               (when (and last-dot (/= 0 last-dot))
219                 (subseq filename (1+ last-dot)))))
220         (make-pathname :directory (cons :relative directory)
221                        :name filename-without-dot :type type))))))
222
223(defun pathname-to-string (pathname)
224  "Convert PATHNAME to a string usable in darcs patch files.
225PATHNAME is assumed to be a relative pathname going strictly down,
226as returned by SANITIZE-FILENAME."
227  (apply #'concatenate 'string
228         "./"
229         (append
230          (mapcan (lambda (d)
231                    (list d "/"))
232                  (cdr (pathname-directory pathname)))
233          (when (pathname-name pathname)
234            (cons (pathname-name pathname)
235                  (when (pathname-type pathname)
236                    (list "." (pathname-type pathname))))))))
237
238(defun make-dir (pathname)
239  "Create directory PATHNAME."
240  (with-simple-restart (ignore-error "Ignore ~A directory creation error." pathname)
241    #+clisp (ext:make-dir pathname)
242    #+sbcl  (sb-posix:mkdir pathname #o777)
243    #-(or clisp sbcl)
244    (error "MAKE-DIR not implemented for ~A." (lisp-implementation-type))))
245
246(defun delete-dir (pathname)
247  "Delete directory PATHNAME."
248  #+clisp (ext:delete-dir pathname)
249  #+sbcl  (sb-posix:rmdir pathname)
250  #-(or clisp sbcl)
251  (error "DELETE-DIR not implemented for ~A." (lisp-implementation-type)))
252
253(defun copy-directory (source target &key excluding)
254  "Copy all files and directories in SOURCE to TARGET.
255SOURCE and TARGET are pathnames designating directories, both of
256which must exist.  EXCLUDING is a list of files and directories
257to exclude.
258
259Symlinks will confuse the function."
260  (let* ((wild (make-pathname :directory '(:relative :wild-inferiors)
261                              :name :wild
262                              :type :wild
263                              :version :wild))
264         (source-wild (merge-pathnames wild source))
265         (target-wild (merge-pathnames wild target))
266         (excluding-wild (mapcar
267                          (lambda (excluded) (merge-pathnames wild excluded))
268                          excluding))
269
270         (files (fad:list-directory (truename source))))
271    (dolist (source-file files)
272      (let ((target-file (translate-pathname source-file source-wild target-wild)))
273        (cond
274          ((some (lambda (excluded) (pathname-match-p source-file excluded)) excluding-wild)
275           ;; File excluded - do nothing.
276           )
277          ((fad:directory-pathname-p source-file)
278           (make-dir target-file)
279           (copy-directory source-file target-file :excluding excluding))
280          (t
281           (fad:copy-file source-file target-file)))))))
282
283(defvar *scanner-cache* (make-hash-table :test 'equal)
284  "Hash table for scanners created for filename regexp tests.
285Creating a scanner is slow, but using it is fast.")
286
287(defun matches-one-of (regexps string)
288  "Return true if some of REGEXPS match STRING.
289Cache scanners for faster execution beyond first time."
290  ;; These scanners use _a lot_ of memory, so we build just one, and
291  ;; hope that the exact combination of regexps will be used often
292  ;; enough.
293  (setq regexps (sort (copy-seq regexps) #'string>))
294  (let* ((combined-regexp
295          (apply
296           #'concatenate 'string
297           (loop for regexp in regexps
298              for n upfrom 0
299              unless (zerop n) collect "|"
300              collect regexp)))
301         (scanner (or
302                   (gethash combined-regexp *scanner-cache*)
303                   (setf (gethash combined-regexp *scanner-cache*)
304                         (cl-ppcre:create-scanner combined-regexp)))))
305    (when (cl-ppcre:scan scanner string)
306      t)))
307
308(defun file-binary-p (repo filename)
309  "Return true if FILENAME names a binary file.
310Uses the regexps specified in REPO."
311  (let ((binary-regexps (get-preflist repo "binaries")))
312    (matches-one-of binary-regexps filename)))
313
314(defun file-boring-p (repo filename)
315  "Return true if FILENAME names a boring file.
316Uses the regexps specified in REPO."
317  (let ((binary-regexps (get-preflist repo "boring")))
318    (matches-one-of binary-regexps filename)))
Note: See TracBrowser for help on using the repository browser.