1 ;;; ffi-curl.el --- Emacs interface to libcurl.
3 ;; Copyright (C) 2005-2009 by Zajcev Evgeny.
5 ;; Author: Zajcev Evgeny <zevlg@yandex.ru>
6 ;; Keywords: ffi, curl, ftp
8 ;; This file is part of SXEmacs.
10 ;; SXEmacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
15 ;; SXEmacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
23 ;;; Synched up with: Not in FSF
36 ;;{{{ Low-level FFI: types and functions
38 (define-ffi-enum curl-option
39 (:fstream 10001) ; FILE* stream to write to
40 (:url 10002) ; full URL to get/put
41 (:port 3) ; port number to connect to
42 (:write-function 20011)
43 (:read-function 20012)
44 (:timeout 13) ; read timeout in seconds
45 (:post-fields 10015) ; POST static input fields
46 (:header 42) ; throw the header out too
47 (:nobody 44) ; use HEAD to get http document
48 (:post 47) ; HTTP POST method
52 (define-ffi-enum curl-info
53 (:effective-url #x100001)
54 (:response-code #x200002)
55 (:header-size #x20000b)
56 (:content-type #x100012)
57 (:size-download #x300008)
58 (:speed-download #x300009))
60 (cffi:defcfun ("curl_easy_init" curl:curl_easy_init) pointer)
61 (cffi:defcfun ("curl_easy_cleanup" curl:curl_easy_cleanup) void
63 (cffi:defcfun ("curl_easy_perform" curl:curl_easy_perform) int
66 (cffi:defcfun ("curl_easy_setopt" curl:curl_easy_setopt) int
67 (handler pointer) (opt curl-option) &rest)
68 (cffi:defcfun ("curl_easy_getinfo" curl:curl_easy_getinfo) int
69 (handler pointer) (info curl-info) &rest)
75 (defconst curl:errors-alist
76 '((1 . "Unsupported protocol")
78 (3 . "Malformated URL")
80 (5 . "Could not resolve proxy")
81 (6 . "Could not resolve host")
82 (7 . "Could not connect")
83 (8 . "FTP weird server reply")
84 (9 . "FTP access denied")
85 (10 . "FTP user or password is incorrect")
86 (11 . "FTP weird PASS reply")
87 (12 . "FTP weird USER reply")
88 (13 . "FTP weird PASV reply")
89 (14 . "FTP weird 227 format")
90 (15 . "FTP can't get host")
91 (16 . "FTP can't reconnect")
92 (17 . "FTP could not set binary")
94 (19 . "FTP could not RETR file")
95 (20 . "FTP write error")
96 (21 . "FTP quote error")
97 (22 . "HTTP returned errror")
100 (25 . "failed FTP upload")
101 (26 . "could open/read from file")
102 (27 . "Out of memory")
103 (28 . "the timeout time was reached")
104 (29 . "TYPE A failed")
105 (30 . "FTP PORT operation failed")
106 (31 . "the REST command failed")
107 (32 . "the SIZE command failed")
108 (33 . "RANGE \"command\" didn't work")
109 (34 . "HTTP port error")
110 (35 . "wrong when connecting with SSL")
111 (36 . "couldn't resume download")
112 (37 . "FILE could not read file")
113 (38 . "LDAP cannot bind")
114 (39 . "LDAP search failed")
115 (40 . "library not found")
116 (41 . "function not found")
117 (42 . "aborted by callback")
118 (43 . "bad function argument")
120 (45 . "CURLOPT_INTERFACE failed")
122 (47 . "catch endless re-direct loops")
123 (48 . "User specified an unknown option")
124 (49 . "Malformed telnet option")
126 (51 . "peer's certificate wasn't ok")
127 (52 . "when this is a specific error")
128 (53 . "SSL crypto engine not found")
129 (54 . "can not set SSL crypto engine as default")
130 (55 . "failed sending network data")
131 (56 . "failure in receiving network data")
132 (57 . "share is in use")
133 (58 . "problem with the local certificate")
134 (59 . "couldn't use specified cipher")
135 (60 . "problem with the CA cert (path?)")
136 (61 . "Unrecognized transfer encoding")
137 (62 . "Invalid LDAP URL")
138 (63 . "Maximum file size exceeded")
139 (64 . "Requested FTP SSL level failed"))
140 "Alist of error codes and associated clear-text error messages.")
146 (defun curl:easy-init ()
147 "Initialize curl easy interface and return a context handle."
148 (let ((ret (curl:curl_easy_init)))
149 (when (ffi-null-p ret)
150 (error "curl:easy-init: Can't init easy interface"))
153 (defun curl:easy-cleanup (ctx)
154 "Clean up context CTX and free resources allocated with it.
155 This function must be called after every easy session."
156 (curl:curl_easy_cleanup ctx)
157 ;; Remove references to saved values
158 (remprop ctx 'saved-values))
160 (defun curl:easy-setopt (ctx &rest options)
161 "Set OPTIONS for curl transfer.
162 Options are passed as keyword-value-pairs. Supported keywords are:
163 :url string - a valid Uniform Resource Locator.
164 :fstream ffi-fo - a file descriptor to which output is redirected."
166 (let ((option (car options))
167 (value (cadr options))
169 ;; Handle special cases in options
172 (unless (stringp value)
173 (error 'invalid-argument
174 "curl:easy-setopt invalid option value(must be string)"
176 (setq value (ffi-create-fo 'c-string value))
177 ;; Keep reference to value until context is destroyed
178 (push value (get ctx 'saved-values)))
180 ((:read-function :write-function)
181 (setq value (ffi-callback-fo value)))
183 ((:nobody :header :post :nosignal)
184 (setq value (ffi-create-fo 'int (if value 1 0)))))
186 (setq error (curl:curl_easy_setopt ctx option value))
187 (unless (zerop error)
188 (error 'invalid-operation "curl:easy-setopt error" error))
190 (setq options (cddr options)))))
192 (defun curl:easy-perform (ctx)
193 "Perform cURL operation on the context CTX.
194 To control the behaviour of the session or set options into the
195 context, see `curl:easy-setopt'."
196 (let ((err (curl:curl_easy_perform ctx)))
198 (error 'invalid-operation "curl:easy-perform error"
199 (cdr (assq err curl:errors-alist))))
202 (defun curl:easy-perform& (ctx sentinel fs)
203 "Perform cURL operation on CTX, return a worker job object.
204 Afterwards run SENTINEL.
206 The original (curl) context CTX is stored in the plist of the worker job
207 object with key 'ctx to keep it accessible."
208 (if (featurep 'workers)
209 (let* ((job (ffi-call-function&
210 (get 'curl:curl_easy_perform 'ffi-fun)
211 ctx sentinel fs ctx)))
212 ;; add ctx to plist of job
215 (error 'unimplemented "Asynchronous Event Queues")))
217 (defun curl:easy-perform-sentinel (job fs ctx)
218 (curl:easy-cleanup ctx)
219 (unless (car fs) (c:fclose (cdr fs)))
220 (run-hook-with-args 'curl:download&-post-hook job))
222 (defun curl:easy-getinfo (ctx what)
223 "Get info from the context CTX about WHAT."
224 (let* ((ival (cdr (assq what (ffi-enum-values 'curl-info))))
225 (itype (if (not (numberp ival))
226 (error "Unsupported info" what)
227 (ecase (lsh (logand #xf00000 ival) -20)
228 (1 'c-string) (2 'long) (3 'double))))
229 (retfo (make-ffi-object itype)))
230 (unless (zerop (curl:curl_easy_getinfo
231 ctx what (ffi-address-of retfo)))
232 (error 'invalid-operation "curl:easy-getinfo error"))
235 (defvar curl:download-history nil
236 "History for `curl:download' and `curl:download&'.")
239 (define-ffi-callback curl:cb-write-to-buffer int
240 ((ptr pointer) (size int) (nmemb int) (stream pointer))
241 "Writer to STREAM buffer."
242 (let ((buf (ffi-pointer-to-lisp-object stream))
243 (rsz (* size nmemb)))
244 (when (and (positivep rsz) (buffer-live-p buf))
245 (with-current-buffer buf
246 (insert (ffi-get ptr :type (cons 'c-data rsz)))))
250 (defun curl:download (url file-or-buffer &rest options)
251 "Download the contents of URL and write them to FILE-OR-BUFFER.
253 Optionally you can specify keywords in OPTIONS. The options are
254 keyword-value-pairs and are set via `curl:easy-setopt'.
256 When called interactively you can choose, with a prefix arg, to download
257 the HTTP header instead of the actual remote file. Obviously this only
258 works with HTTP URLs."
260 (list (read-string "URL: " (and-fboundp #'ffap-url-at-point
262 curl:download-history)
263 (read-file-name "Local file: " default-directory
264 (expand-file-name (make-temp-name "curl:downloaded:")
266 (when current-prefix-arg
268 (and (y-or-n-p (format "Only download %s's HTTP header? "
269 (file-basename file-or-buffer)))
270 (setq options (list :header t :nobody t))))
272 (let* ((ctx (curl:easy-init))
273 (bufferp (when (bufferp file-or-buffer)
274 (or (boundp 'curl:cb-write-to-buffer)
275 (error 'unimplemented 'ffi-make-callback
276 "for this architecture"))))
278 (ffi-lisp-object-to-pointer file-or-buffer)
279 (c:fopen (expand-file-name file-or-buffer) "w"))))
283 (curl:easy-setopt ctx :write-function 'curl:cb-write-to-buffer))
286 (curl:easy-setopt ctx :nosignal t)
288 (apply #'curl:easy-setopt ctx :fstream fs :url url options)
289 (curl:easy-perform ctx))
290 (unless bufferp (c:fclose fs))
291 (curl:easy-cleanup ctx))))
294 (defun curl:download& (url file-or-buffer &rest options)
295 "Download the contents of URL and write them to FILE asynchronously.
297 Optionally you can specify keywords in OPTIONS. The options are
298 keyword-value-pairs and are set via `curl:easy-setopt'.
300 When called interactively you can choose, with a prefix arg, to download
301 the HTTP header instead of the actual remote file. Obviously this only
302 works with HTTP URLs.
304 After the download operation succeeded the hook `curl:download&-post-hook'
305 is run. Functions in there will be called with an argument JOB."
307 (list (read-string "URL: " (and-fboundp #'ffap-url-at-point
309 curl:download-history)
310 (read-file-name "Local file: " default-directory
311 (expand-file-name (make-temp-name "curl:downloaded:")
313 (when current-prefix-arg
314 (and (y-or-n-p (format "Only download %s's HTTP header? "
315 (file-basename file-or-buffer)))
316 (setq options (list :header t :nobody t))))
318 (if (featurep 'workers)
319 (let* ((ctx (curl:easy-init))
320 (bufferp (when (bufferp file-or-buffer)
321 (or (boundp 'curl:cb-write-to-buffer)
322 (error 'unimplemented 'ffi-make-callback
323 "for this architecture"))))
325 (ffi-lisp-object-to-pointer file-or-buffer)
326 (c:fopen (expand-file-name file-or-buffer) "w"))))
330 (curl:easy-setopt ctx :write-function 'curl:cb-write-to-buffer))
333 (curl:easy-setopt ctx :nosignal t)
335 (apply #'curl:easy-setopt ctx :fstream fs :url url options)
336 (curl:easy-perform& ctx #'curl:easy-perform-sentinel
339 ;; Close FS, cleanup CTX and resignal error
340 (t (unless bufferp (c:fclose fs))
341 (curl:easy-cleanup ctx)
342 (signal (car cerr) (cdr cerr)))))
343 (error 'unimplemented "Asynchronous Event Queues")))
346 (defvar curl:download&-post-hook nil
347 "*Hook run after a `curl:download&' call.
348 Functions in here are called with one argument JOB containing
349 the job which just finished.")
354 ;; There's probably more that could be added here, but for now, just
355 ;; enough to get PUI working seemlessly with ffi-curl (HTTP/FTP) or
359 (defun curl:file-exists-p (uri)
360 "Curl implementation of `file-exists-p'.
362 Don't call this directly, use `file-exists-p' and give it a URI for
363 the FILENAME arg. The underlying file name handlers will take care of
364 calling this function.
366 Currently only HTTP and FTP are supported, and then only if the URL ends
367 in a filename. IOW you can't do \(file-exists-p \"http://example.com/\"\)"
368 (let ((lfile (expand-file-name
369 (make-temp-file "curl:") (temp-directory)))
371 (curl:download uri lfile :header t :nobody t)
373 (insert-file-contents-literally lfile)
374 (and (re-search-forward #r"^\(HTTP/1\.1 200 OK\|Content-Length\)" nil t)
378 (put 'file-exists-p 'curl 'curl:file-exists-p)
381 ;; doesn't make much sense for HTTP, so just alias to file-exists-p
382 (defalias 'curl:file-readable-p 'curl:file-exists-p)
383 (put 'file-readable-p 'curl 'curl:file-readable-p)
385 ;; insert-file-contents-literally
386 (defun curl:insert-file-contents-literally (uri &optional visit
388 "Curl implementation of `insert-file-contents-literally'.
390 Don't call this directly, use `insert-file-contents-literally' and
391 give it a URI for the FILENAME arg. The underlying file name handlers
392 will take care of calling this function.
394 Currently only HTTP and FTP are supported, and then only if the URL
396 (let ((file-name-handler-alist nil)
398 (after-insert-file-functions nil)
399 (coding-system-for-read 'binary)
400 (coding-system-for-write 'binary)
401 (find-buffer-file-type-function
402 (if (fboundp 'find-buffer-file-type)
403 (symbol-function 'find-buffer-file-type)
405 (lfile (expand-file-name
406 (make-temp-file "curl:") (temp-directory))))
409 (curl:download uri lfile)
410 (fset 'find-buffer-file-type (lambda (lfile) t))
411 (insert-file-contents lfile visit start end replace))
412 (if find-buffer-file-type-function
413 (fset 'find-buffer-file-type find-buffer-file-type-function)
414 (fmakunbound 'find-buffer-file-type))
415 (delete-file lfile))))
416 (put 'insert-file-contents-literally
417 'curl 'curl:insert-file-contents-literally)
419 ;;; FIXME: calling `copy-file' interactively on a URI doesn't work. The
420 ;;; minibuffer tries to do expansion or completion or something on the
421 ;;; URI before the file name handlers kick in. --SY.
423 (defun curl:copy-file (uri newname &optional ok-if-already-exists
425 "Curl implementation of `copy-file'.
427 Copy remote file, URI to local file, NEWNAME. Copying in the
428 other direction, local to remote, is not supported and will result in
431 Signals a `file-already-exists' error if file NEWNAME already exists,
432 unless a third argument OK-IF-ALREADY-EXISTS is supplied and non-nil.
433 A number as third arg means request confirmation if NEWNAME already
434 exists. This is the default for interactive use.
436 Don't call this directly, use `copy-file' and give it a URI for
437 the FILENAME arg. The underlying file name handlers will take care of
438 calling this function.
440 Currently only HTTP and FTP are supported, and then only if the URL
442 (when (string-match #r"^\(https?\|s?ftp\)://" newname)
443 (error 'invalid-argument newname "Destination cannot be a URI"))
444 (let ((newname (expand-file-name newname))
446 (if (file-exists-p newname)
447 (if (or (interactive-p) (numberp ok-if-already-exists))
449 (format "Existing file: %s Overwrite? " newname))
451 (if ok-if-already-exists
453 (error 'file-already-exists "Existing file" newname)))
455 (and doit (curl:download uri newname))))
456 (put 'copy-file 'curl 'curl:copy-file)
459 (defun curl:expand-file-name (&rest args)
460 "Return the 1st argument unchanged.
462 Don't use this. In fact, forget that you even saw it. There is no
463 way you're ever going to need to use this. It's sole purpose is to
464 keep `file-exists-p' happy when that is given a URI to check. "
466 (put 'expand-file-name 'curl 'curl:expand-file-name)
468 ;; file-name-directory
469 (defun curl:file-name-directory (uri)
470 "A curl implementation of `file-name-directory'.
472 Returns URI without the filename.
474 Don't call this directly, use `file-name-directory' with URI for the
475 FILENAME arg. The underlying file name handlers will take care of
476 calling this function.
478 Currently only HTTP and FTP are supported, and then only if the URL
481 (string-match curl:file-handler-regexp uri)
482 (substring uri (match-beginning 1) (match-end 2))))
483 (put 'file-name-directory 'curl 'curl:file-name-directory)
485 ;; file-name-nondirectory
486 (defun curl:file-name-nondirectory (uri)
487 "A curl implementation of `file-name-nondirectory'.
489 Returns the filename portion of URI.
491 Don't call this directly, use `file-name-nondirectory' with URI for the
492 FILENAME arg. The underlying file name handlers will take care of
493 calling this function.
495 Currently only HTTP and FTP are supported, and then only if the URL
498 (string-match curl:file-handler-regexp uri)
499 (substring uri (match-end 2))))
500 (put 'file-name-nondirectory 'curl 'curl:file-name-nondirectory)
502 ;; This regexp contains trailing whitespace DO NOT REMOVE OR MANGLE
503 (defregexp curl:file-handler-regexp
504 #r"\(https?://\|s?ftp://\)[^]
506 \"'()<>[^`{}.,;]+\(/\)\([^/].*[^/]$\)"
507 "Regexp used in `file-name-handler-alist'.
509 It matches HTTP/FTP URLs but only if they end with a filename.
510 So, for example, \"http://example.com/file.ext\" would match, but
511 \"http://example.com/\" would not.")
514 (defun curl:file-handler (operation &rest args)
515 (let ((op (get operation 'curl)))
518 (error 'unimplemented
519 (concat "curl:" (symbol-name operation))))))
521 (defvar curl:file-handler
522 (cons curl:file-handler-regexp 'curl:file-handler))
524 (unless (memq curl:file-handler file-name-handler-alist)
525 (setq file-name-handler-alist
526 (cons curl:file-handler file-name-handler-alist)))
533 ;;; ffi-curl.el ends here