source: project/release/4/intarweb/trunk/header-parsers.scm @ 11983

Last change on this file since 11983 was 11983, checked in by sjamaan, 13 years ago

Add feeble beginnings of the new spiffy implementation based on intarweb

File size: 17.1 KB
Line 
1;; Change the accuracy of a number to 'digits' number of digits to the
2;; right of the decimal point.
3(define (chop-number num digits)
4  (let ((factor (expt 10 digits)))
5    (/ (round (* num factor)) factor)))
6
7(define (quality-parser str)
8  ;; Anything that's not a number is seen as if the value is missing, hence 1.0
9  (let* ((num       (or (string->number str) 1.0))
10         (imprecise (chop-number num 3)))
11    (max 0.0 (min 1.0 imprecise))))
12
13;; TODO: Make this use SRFI-19
14(define (rfc822-time-parser str)
15  0)
16
17;; Get the raw contents of a header
18(define (header-contents name headers #!optional (default #f))
19  (alist-ref name (headers-v headers) eq? default))
20
21;; Get all values of a header
22(define (header-values header-name headers)
23  (map (cut vector-ref <> 0) (header-contents header-name headers '())))
24
25;; Get the value of a header, assuming it has only one value
26(define (header-value header-name headers #!optional (default #f))
27  (let ((contents (header-contents header-name headers '())))
28    (if (null? contents)
29        default
30        (vector-ref (car contents) 0))))
31
32;; Get the value of a header which is an alist
33;; Example: header = ((foo . bar) (qux . mooh)), header-list-ref foo
34;; will return bar.  (the header itself is encoded in a param+value
35(define (header-list-ref value headers #!optional (equal? eq?))
36  (find (lambda (h) (equal? (car (get-value h)) value)) headers))
37
38;; Get the value from one header entry
39(define get-value (cut vector-ref <> 0))
40
41;; Get all params from one header entry
42(define get-params (cut vector-ref <> 1))
43
44;; Get one specific parameter from one header entry
45(define (get-param param contents)
46  (alist-ref param (vector-ref contents 1) eq?))
47
48;; Get-param, except if no quality is present return 1
49(define (get-quality header-contents)
50  (or (get-param 'q header-contents) 1.0))
51
52;;;; Header parsers
53
54;; Find a matching endpoint for a token, ignoring escaped copies of the token
55(define (escaped-string-end str start stop-char-set)
56  (let ((len (string-length str)))
57    (let loop ((start start))
58      (let ((pos (string-index str (char-set-adjoin stop-char-set #\\) start)))
59        (if pos
60            (if (char=? #\\ (string-ref str pos))
61                ;; Escaped matching closing symbol
62                (if (= len (add1 pos))
63                    pos
64                    (loop (+ pos 2)))
65                ;; Reached the matching closing symbol
66                pos)
67            len))))) ; No matching closing symbol?  "Insert" it at the end
68
69;; RFC 2616 4.2 says multi-headers are a comma-separated list of tokens
70(define (split-multi-header value)
71  (let ((len (string-length value)))
72    (let loop ((result '())
73               (start-pos 0)   ; Where the current header value starts
74               (search-pos 0)) ; Where the searching starts
75      (or (and-let* (((< search-pos len))
76                     (pos (string-index value (char-set #\, #\") search-pos)))
77            (if (char=? #\, (string-ref value pos))
78                (loop (cons (string-copy value start-pos pos) result) (add1 pos) (add1 pos))
79                (let ((end-pos (escaped-string-end value (add1 pos) (char-set #\"))))
80                  (loop result start-pos (add1 end-pos)))))
81          (reverse (cons (string-drop value start-pos) result))))))
82
83;; Remove all escape characters from the input, recognising "escaped escapes"
84(define (unescape str)
85  (let ((last-char (sub1 (string-length str))))
86    (let loop ((result "")
87               (start-pos 0))
88      (or (and-let* ((pos (string-index str #\\ start-pos)))
89            (if (= pos last-char)
90                (string-append result (string-copy str start-pos))
91                (loop (string-append result (string-copy str start-pos pos)
92                                     (string-copy str (add1 pos) (+ pos 2)))
93                      (+ pos 2))))
94          (string-append result (string-copy str start-pos))))))
95
96;; Try to parse a token, starting at the provided offset, up until the
97;; char-set where we should stop.  Returns two values: the token or #f if
98;; there is no token left, and the position on which the token ends.
99(define (parse-token value start-pos stop-char-set)
100  (if (>= start-pos (string-length value))
101      (values #f start-pos)
102      (let ((stop (char-set-adjoin stop-char-set #\")))
103        (let ((pos (string-index value stop start-pos)))
104          (if pos
105              (if (not (char=? #\" (string-ref value pos)))
106                  (values (string-trim-both (string-copy value start-pos pos))
107                          pos) ; Stop-char found, but no quoting
108                  (let ((end-pos (escaped-string-end value (add1 pos)
109                                                     (char-set #\"))))
110                    ;; Found the double quote? Recurse on the remainder
111                    (receive (rest final-pos)
112                      (parse-token value (add1 end-pos) stop-char-set)
113                      (values (string-append
114                               (string-trim-both
115                                (string-copy value start-pos pos))
116                               (if (= pos end-pos)
117                                   (unescape (string-copy value (add1 pos)))
118                                   (unescape (string-copy value (add1 pos) end-pos)))
119                               (or rest ""))
120                              final-pos))))
121              ;; Nothing found?  Then the remainder of the string is the token
122              (values (string-trim-both (string-copy value start-pos))
123                      (string-length value)))))))
124
125;; Just put all header strings in a list, so we can pass it on
126;; Make no assumptions about the contents (only value, don't try to parse params)
127;; This is different from (multiple (without-params generic-header-parser))
128;; because this does not assume it can split up comma-separated values
129(define (unknown-header-parser name contents headers)
130  (update-header-contents! name (list (vector contents '())) headers))
131
132(define (parse-parameters string start-pos param-parsers)
133  (let loop ((start-pos start-pos)
134             (params '()))
135    (receive (key pos)
136      (parse-token string start-pos (char-set #\; #\=))
137      (if key
138          (if (and (< pos (string-length string)) (char=? (string-ref string pos) #\=))
139              (receive (value pos)
140                (parse-token string (add1 pos) (char-set #\;))
141                (let ((key (string->symbol (string-downcase key))))
142                  ;; In case of no value ("foo="), use the empty string as value
143                  (let ((value ((alist-ref key param-parsers eq? identity) (or value ""))))
144                    (loop (add1 pos) (cons (cons key value) params)))))
145              ;; Missing value is interpreted as "present",
146              ;; so #t. If not present, it's #f when looking it up
147              (loop (add1 pos) (cons (cons (string->symbol key) #t) params)))
148          (values (reverse params) pos)))))
149
150(define (parse-value+parameters string start-pos value-parser param-parsers)
151  (receive (value pos)
152    (parse-token string start-pos (char-set #\;))
153    (if (not value)
154        (values #f pos) ;; XXX this is wrong and not expected by the caller!
155        (receive (params pos)
156          (parse-parameters string (add1 pos) param-parsers)
157          (values (vector (value-parser value) params) pos)))))
158
159(define (with-params value-parser parameter-parsers)
160  (lambda (entry)
161    (receive (type+params pos)
162      (parse-value+parameters entry 0 value-parser parameter-parsers)
163      type+params)))
164
165(define (multiple other-parser #!optional (parameter-parsers '()))
166  (lambda (name entries headers)
167    (fold (lambda (entry headers)
168            (update-header-contents!
169             name
170             (list ((with-params other-parser parameter-parsers) entry))
171             headers))
172          headers
173          (split-multi-header entries))))
174
175(define (single other-parser #!optional (parameter-parsers '()))
176  (lambda (name contents headers)
177    (replace-header-contents!
178     name
179     (list ((with-params other-parser parameter-parsers) contents))
180     headers)))
181
182(define (key/values key/value-parsers)
183  (lambda (k/v)
184    ;; We're abusing parse-parameters here to read value
185    ;; instead of params.  This is weird, but it works :)
186    (receive (key+value pos)
187      (parse-parameters k/v 0 key/value-parsers)
188      (car key+value))))
189
190(define symbol-parser-ci
191  (compose string->symbol string-trim-both string-downcase))
192
193(define symbol-parser
194  (compose string->symbol string-trim-both))
195
196(define (natnum-parser contents)
197  (let ((num (string->number contents)))
198    (if num (inexact->exact (max 0 (round num))) 0)))
199
200(define (host-parser contents)
201  (let ((idx (string-index-right contents #\:)))
202    (if idx
203        (cons (substring/shared contents 0 idx)
204              (inexact->exact
205               (round (or (string->number (substring/shared contents (add1 idx)))
206                          80))))
207        (cons contents 80))))
208
209; base64 of 128 bit hex digest as per RFC1864
210(define md5-parser base64:decode)
211
212;; bytes <start>-<end>/<total>
213(define (range-parser s)
214  (and-let* ((parts (string-match "bytes[ \t]+([0-9]+)-([0-9]+)/([0-9]+)" s)))
215            (map string->number (drop parts 1))))
216
217;; rfc1123-date | rfc850-date | asctime-date
218(define http-time-parser rfc822-time-parser)
219
220;; (W/)<string>
221(define (entity-tag-parser name contents headers)
222  (let ((contents (string-trim-both contents)))
223    (replace-header-contents!
224     name
225     (list (if (string-prefix? "W/" contents)
226               (vector `(weak . ,(parse-token contents 2 (char-set))) '())
227               (vector `(strong . ,(parse-token contents 0 (char-set))) '())))
228     headers)))
229
230;;;; MAJOR TODOs
231;; RFC822/1123 mailbox parser - just strings for now
232(define mailbox-parser identity)
233
234;; Either an entity-tag or a http-date
235(define if-range-parser identity)
236
237;; Either delta-seconds or RFC822 timestamp
238(define (retry-after-parser contents)
239  (if (string-match "[ \t]*[0-9]+[ \t]*" contents)
240      (natnum-parser contents)
241      (rfc822-time-parser contents)))
242
243;; Tricky - see 2616 14.45
244;; We probably shouldn't try to do too much parsing here
245(define via-parser (multiple identity))
246
247;; Tricky - see 2616 14.46
248(define warning-parser (multiple identity))
249;;;; END MAJOR TODOs
250
251(define (key/value-parser str)
252  (let ((idx (string-index str #\=)))
253    (cons (string-take str idx) (string-drop str (add1 idx)))))
254
255;; The 'expires' header defined by the Netscape cookie spec contains
256;; an embedded comma.  RFC 2109 cookies use Max-Age instead.
257(define (old-style-cookie? cookie)
258  (not (not (string-search (regexp "^[^=\"]+=[^;]*;.*expires[[:space:]]*=" #f) cookie))))
259
260(define set-cookie-parser
261  (let ((param-parsers `((expires . ,rfc822-time-parser)
262                         (max-age . ,string->number)
263                         (version . ,string->number))))
264    (lambda (name contents headers)
265      (if (old-style-cookie? contents)
266          (update-header-contents!
267           name
268           (list ((with-params key/value-parser param-parsers) contents))
269           headers)
270          ((multiple key/value-parser param-parsers) name contents headers)))))
271
272(define cache-control-parser
273  (let ((splitter (lambda (str) ;; Is this correct?
274                    (map (compose string->symbol string-trim-both)
275                         (string-split str ",")))))
276    (multiple
277     (key/values `((max-age . ,natnum-parser)
278                   (s-maxage . ,natnum-parser)
279                   (max-stale . ,natnum-parser)
280                   (min-fresh . ,natnum-parser)
281                   (private . ,splitter)
282                   (no-cache . ,splitter))))))
283
284;; This too
285(define (pragma-parser name value headers)
286  (update-header-contents! name (parse-parameters value 0 `()) headers))
287
288;; This one includes q parser
289(define (te-parser name value headers)
290  (update-header-contents! name (parse-parameters value 0 `((q . ,quality-parser))) headers))
291
292;; Cookie headers are also braindead: there can be several cookies in one header,
293;; separated by either commas or semicolons. The only way to distinguish a
294;; new cookie from a parameter of the current cookie is the dollar in front
295;; of all parameter names.
296;; Also, there's a $Version attribute that prepends all cookies, which is
297;; considered to apply to all cookies that follow.
298;;
299;; This code is a bit of a hack in the way it abuses parse-parameters
300(define (cookie-parser name value headers)
301  ;; Get all attributes (dollar-prefixed params) and remaining cookies & params
302  (define (split-attribs pairs)
303    (span (lambda (pair)
304            (string-prefix? "$" (symbol->string (car pair))))
305          pairs))
306  (receive (params pos)
307    (parse-parameters value 0 `(($version . ,string->number)
308                               ($port . ,string->number)))
309    (receive (global-attrs remaining)
310      (split-attribs params)
311      (let loop ((remaining remaining)
312                 (cookies '()))
313        (if (null? remaining)
314            (update-header-contents! name (reverse cookies) headers)
315            (let ((cookie (cons (symbol->string (caar remaining))
316                                (cdar remaining)))
317                  (params (cdr remaining)))
318              (receive (local-attrs rest)
319                (split-attribs params)
320                (let* ((all-attrs (append global-attrs local-attrs))
321                       (result (vector cookie all-attrs)))
322                  (loop rest (cons result cookies))))))))))
323
324;;; Unparsers ;;;
325(define (unparse-params params unparsers)
326  (let loop ((params params)
327             (results '()))
328    (if (null? params)
329        (string-join (reverse results) "; " 'prefix)
330        (let* ((unparser (alist-ref unparsers (caar params) eq?
331                                    (lambda (name value)
332                                      (case value
333                                        ;; #t means param is present (no value)
334                                        ((#t) (->string name))
335                                        ;; #f means param is missing
336                                        ((#f) #f)
337                                        (else (conc name "=" value))))))
338               (str (unparser (caar params) (cdar params))))
339          (loop (cdr params) (if str (cons str results) results))))))
340
341(define must-be-quoted-chars (char-set-adjoin char-set:iso-control #\"))
342
343(define quote-string
344  (let ((smap (map (lambda (c)
345                     (cons (string c)
346                           (string-append "\\" (string c))))
347                   (char-set->list must-be-quoted-chars))))
348    (lambda (string)
349      (let ((error-chars (char-set #\newline)))
350        (if (string-any error-chars string)
351            (signal-http-condition "Unencoded newline in header contents! Please encode the newline in a way appropriate for this header"
352                                   'unencoded-header 'value string)
353            (string-append "\"" (string-translate* string smap) "\""))))))
354
355(define unparse-token
356  (let* ((trigger-quoting-chars (char-set-union
357                                 (char-set-adjoin must-be-quoted-chars #\, #\; #\=)
358                                 char-set:blank)))
359   (lambda (token)
360     (if (string-any trigger-quoting-chars token)
361         (quote-string token)
362         token))))
363
364;; There's no need to make a specific header unparser for every header type.
365;; Usually, the Scheme value representing a header can unambiguously be
366;; unparsed into a header just by checking its type.
367(define (default-header-unparser header-name header-contents)
368  (let loop ((headers (reverse header-contents))
369             (result '()))
370    (if (null? headers)
371        (sprintf "~A: ~A\r\n"
372                 (header-name->string header-name)
373                 (string-join result ", "))
374        (let* ((contents (get-value (car headers)))
375               (value (cond
376                       ((pair? contents) ; alist?
377                        (if (eq? (cdr contents) #t)
378                            (unparse-token (->string (car contents)))
379                            (conc (unparse-token (->string (car contents))) "="
380                                  (unparse-token (->string (cdr contents))))))
381                       ((uri? contents) (unparse-token (uri->string contents)))
382                       (else (unparse-token (->string contents)))))
383               (parameter-unparsers '())) ; Maybe we want to make this a param
384         (loop (cdr headers)
385               (cons
386                (string-append
387                 value
388                 (unparse-params (get-params (car headers))
389                                 parameter-unparsers))
390                result))))))
391
392(define (etag-unparser header-name header-contents)
393  (let ((contents (get-value (car header-contents))))
394   (sprintf "~A: ~A\r\n"
395            (header-name->string header-name)
396            (string-append
397             (if (eq? 'weak (car contents)) "W/" "")
398             (if (string-prefix? "W/" (cdr contents))
399                 (quote-string (cdr contents))
400                 (unparse-token (cdr contents)))))))
401
402(define (host-unparser header-name header-contents)
403  (let ((contents (get-value (car header-contents))))
404   ;; XXX: urlencode?
405   (sprintf "~A: ~A\r\n"
406            (header-name->string header-name)
407            (if (= (cdr contents) 80)
408                (car contents)
409                (conc (car contents) ":" (cdr contents))))))
Note: See TracBrowser for help on using the repository browser.